1 /**
  2  * @fileOverview
  3  * @author <a href="https://www.labkey.org">LabKey</a> (<a href="mailto:info@labkey.com">info@labkey.com</a>)
  4  * @license Copyright (c) 2014-2016 LabKey Corporation
  5  * <p/>
  6  * Licensed under the Apache License, Version 2.0 (the "License");
  7  * you may not use this file except in compliance with the License.
  8  * You may obtain a copy of the License at
  9  * <p/>
 10  * http://www.apache.org/licenses/LICENSE-2.0
 11  * <p/>
 12  * Unless required by applicable law or agreed to in writing, software
 13  * distributed under the License is distributed on an "AS IS" BASIS,
 14  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 15  * See the License for the specific language governing permissions and
 16  * limitations under the License.
 17  * <p/>
 18  */
 19 (function ($)
 20 {
 21     if (!LABKEY.help)
 22     {
 23         LABKEY.help = {};
 24     }
 25 
 26     /**
 27      * @private
 28      * @namespace API that provides the capability to run a Tour on the page highlighing different areas and aspects
 29      *      to show users. <p/>
 30      *            <p>Additional Documentation:
 31      *              <ul>
 32      *                  <li><span>Not yet provided.</span></li>
 33      *              </ul>
 34      *           </p>
 35      */
 36     LABKEY.help.Tour = new function ()
 37     {
 38         var _hopscotchSessionProperty = 'hopscotch.tour.state',
 39             _localStorageProperty = "LABKEY.tours.state",
 40             _tours = {},
 41             _continue = {},
 42             _queue = [],
 43             _next = 0,
 44             loads = 0,
 45             me = this,
 46             _modeOff = "off",
 47             _modeRunOnce = "runOnce",
 48             _modeRunAlways = "runAlways",
 49             modes = [_modeOff, _modeRunOnce, _modeRunAlways];
 50 
 51         //
 52         // Private functions
 53         //
 54         /**
 55          * Run next tour in queue. Callback in show()
 56          */
 57         var _autoRun = function ()
 58         {
 59             if (_next < _queue.length)
 60             {
 61                 _display(_tours[_queue[_next]], 0);
 62                 _next++;
 63             }
 64         };
 65 
 66         var _autoShowFromDb = function (id, step)
 67         {
 68             if (LABKEY.tours[id].mode != undefined)
 69             {
 70                 var modeIndex = parseInt(LABKEY.tours[id].mode);
 71 
 72                 if (modeIndex > modes.length)
 73                 {
 74                     console.warn("Invalid mode value. TourId: " + id + ", Mode: " + modeIndex);
 75                     return false;
 76                 }
 77                 if (modes[modeIndex] == _modeOff && step < 1)
 78                     return false;
 79 
 80                 if (modes[modeIndex] == _modeRunOnce && seen(id) && step < 1)
 81                     return false;
 82 
 83                 _load(id, step);
 84                 return true;
 85             }
 86             console.warn("Tour mode not found. TourId: " + id);
 87         };
 88 
 89         var _display = function (config, step)
 90         {
 91             _initHopscotch(function ()
 92             {
 93                 hopscotch.listen("end", function ()
 94                 {
 95                     // 22390: Hopscotch doesn't actually end the tours until after this call
 96                     setTimeout(_autoRun, 1);
 97                 });
 98                 if (LABKEY.Utils.isString(step))
 99                     step = parseInt(step);
100                 hopscotch.startTour(config, step || 0);
101                 markSeen(config.id);
102             }, me);
103         };
104 
105         var _get = function (idOrConfig)
106         {
107             var config = idOrConfig;
108             if (LABKEY.Utils.isString(idOrConfig))
109             {
110                 config = _tours[idOrConfig];
111             }
112             if (!config || !config.id)
113             {
114                 console.warn("tour not found, or not configured properly: " + idOrConfig);
115                 return null;
116             }
117             return config;
118         };
119 
120         // Get multipage tour info from URL
121         var _getContinue = function ()
122         {
123             var config = {};
124             var hash = window.location.hash, prefix = "tourstate:";
125             if (hash && hash.charAt(0) == '#')
126                 hash = hash.substring(1);
127             if (hash.substring(0, prefix.length) != prefix)
128                 return config;
129             var tourstate = hash.substring(prefix.length),
130                     endIdx = tourstate.indexOf(':');
131             if (-1 != endIdx)
132             {
133                 config.id = tourstate.substring(0, endIdx);
134                 config.step = tourstate.substring(endIdx + 1);
135             }
136             return config;
137         };
138 
139         var _init = function ()
140         {
141             var config = _getContinue();
142 
143             if (LABKEY.tours)
144             {
145                 $.each(LABKEY.tours, function (tourId, tour)
146                 {
147                     if (!$.isEmptyObject(config) && config.id == tourId)
148                         _autoShowFromDb(tourId, config.step);
149                     else
150                         _autoShowFromDb(tourId, 0);
151                 });
152             }
153         };
154 
155         var _initHopscotch = function (fn, scope)
156         {
157             var script = "/hopscotch/js/hopscotch" + (LABKEY.devMode ? "" : ".min") + ".js";
158             var style = "/hopscotch/css/hopscotch" + (LABKEY.devMode ? "" : ".min") + ".css";
159             LABKEY.requiresScript(script, fn, scope);
160             LABKEY.requiresCss(style);
161         };
162 
163         /**
164          * Queue up tours and start running
165          */
166         var _kickoffTours = function ()
167         {
168             if (!$.isEmptyObject(_continue))
169                 resume(_continue.id, _continue.step);
170 
171             _queue = [];
172             _next = 0;
173 
174             $.each(_tours, function (key, tour)
175             {
176                 if (key != _continue.id)
177                     _queue.push(key);
178             });
179 
180             if ($.isEmptyObject(_continue))
181             {
182                 _autoRun();
183             }
184             _continue = {};
185         };
186 
187         /**
188          * Show tour starting at step
189          * Always loads hopscotch.js
190          */
191         var _load = function(id, step)
192         {
193             loads++;
194 
195             LABKEY.Ajax.request({
196                 url: LABKEY.ActionURL.buildURL('tours', 'getTour'),
197                 jsonData: {id: id},
198                 success: LABKEY.Utils.getCallbackWrapper(function(result)
199                 {
200                     loads--;
201                     _parseAndRegister.call(this, id, step, result);
202 
203                     if (loads == 0)
204                     {
205                         _kickoffTours();
206                     }
207                 }, me, false),
208                 failure: LABKEY.Utils.getCallbackWrapper(function(result)
209                 {
210                     loads--;
211                 }, me, false),
212                 scope: this
213             });
214         };
215 
216         /**
217          * AJAX _load() success callback
218          */
219         var _parseAndRegister = function(id, step, result)
220         {
221             var tour = JSON.parse(result.json);
222             tour.id = id;
223 
224             var realSteps = [];
225             $.each(tour.steps, function(i, step)
226             {
227                 var real = eval(JSON.parse(step.step));
228                 real.target = step.target;
229 
230                 if (!real.placement) {
231                     real.placement = 'bottom'; // required by hopscotch
232                 }
233                 realSteps.push(real);
234             });
235 
236             if (window['_stepcontent'])
237             {
238                 delete window['_stepcontent'];
239             }
240 
241             tour.steps = realSteps;
242             _register(tour, step);
243         };
244 
245         /**
246          * @param config
247          * @param {number} step
248          * @private
249          */
250         var _register = function(config, step)
251         {
252             if (!config.id)
253                 throw "'id' is required to define a tour.";
254             if (!config.steps || !LABKEY.Utils.isArray(config.steps))
255                 throw "'steps' is required to be an Array to define a tour.";
256 
257             if (config.steps.length > 0)
258             {
259                 if (step > 0)
260                 {
261                     _continue.id = config.id;
262                     _continue.step = step;
263                 }
264 
265                 if (!_tours.hasOwnProperty(config.id))
266                 {
267                     _tours[config.id] = config;
268                 }
269             }
270         };
271 
272         //
273         // Public Functions
274         //
275         /**
276          * Show tour if it has never been shown before. Conditionally loads hopscotch.js if the tour needs to be shown.
277          * @param id
278          * @returns {boolean}
279          * @private
280          */
281         var autoShow = function (id)
282         {
283             if (seen(id))
284             {
285                 return false;
286             }
287 
288             show(id, 0);
289             return true;
290         };
291 
292         /**
293          * continueAtLocation() and continueTour() make a simple pattern for multi-page tours
294          * @param {string} href
295          * @private
296          * @example
297 <pre name="code">
298 var tourConfig = {
299     // ...
300     onNext: function() {
301         var url = LABKEY.ActionURL.buildURL('project', 'begin');
302         LABKEY.help.Tour.continueAtLocation(url);
303     }
304     // ...
305 };
306 LABKEY.Utils.onReady(function() {
307     LABKEY.help.Tour.continueTour();
308 });
309 </pre>
310          */
311         var continueAtLocation = function (href)
312         {
313             var context = LABKEY.contextPath;
314 
315             if (href.charAt(0) != "/")
316             {
317                 href = "/" + href;
318             }
319 
320             href = context + href;
321 
322             if (!hopscotch.getCurrTour())
323             {
324                 window.location = href;
325             }
326             var hopscotchState = hopscotch.getCurrTour().id + ":" + hopscotch.getCurrStepNum();
327 
328             var a = document.createElement("A");
329             a.href = href;
330             a.hash = 'tourstate:' + hopscotchState;
331             window.location = a.href;
332         };
333 
334         /**
335         * see continueAtLocation()
336          * @private
337         */
338         var continueTour = function ()
339         {
340             var config = _getContinue();
341             if (!$.isEmptyObject(config))
342             {
343                 return resume(config.id, parseInt(config.step));
344             }
345         };
346 
347         /**
348          * Mark tour as seen so autoShow() will no longer show this tour
349          * @param id
350          * @private
351          */
352         var markSeen = function (id)
353         {
354             var state = {};
355             var v = localStorage.getItem(_localStorageProperty);
356             if (v)
357             {
358                 state = LABKEY.Utils.decode(v);
359             }
360             state[id] = "seen";
361             localStorage.setItem(_localStorageProperty, LABKEY.Utils.encode(state));
362         };
363 
364         /**
365          * @param config
366          */
367         var register = function(config)
368         {
369             _register(config, 0);
370         };
371 
372         /**
373          * @private
374          */
375         var reset = function ()
376         {
377             localStorage.setItem(_localStorageProperty, "{}");
378             _initHopscotch(function ()
379             {
380                 hopscotch.endTour(true, false);
381             });
382         };
383 
384         var resetRegistration = function ()
385         {
386             _tours = {};
387         };
388 
389         /**
390          * Countinue tour if it is currently on the indicated step, useful for multi-page tours
391          * Always loads hopscotch.js
392          * @private
393          */
394         var resume = function (id, step)
395         {
396             var config = _get(id);
397             if (config)
398             {
399                 var testState = config.id + ":" + step;
400                 // peek into hopscotch state w/o loading hopscotch.js
401                 if (testState == sessionStorage.getItem(_hopscotchSessionProperty))
402                 {
403                     _display(config, step);
404                 }
405                 return id;
406             }
407         };
408 
409         /**
410          * Determines if the given tour (by id) has already been seen by the user
411          * @param id
412          * @returns {boolean}
413          * @private
414          */
415         var seen = function (id)
416         {
417             // use one item for all tours, this is a little more complicated, but makes it easier to reset state
418             var state = {};
419             var v = localStorage.getItem(_localStorageProperty);
420             if (v)
421             {
422                 state = LABKEY.Utils.decode(v);
423             }
424             return "seen" == state[id];
425         };
426 
427         /**
428          * @param id
429          * @param step
430          * @private
431          */
432         var show = function(id, step)
433         {
434             var tour = _get(id);
435             if (tour)
436             {
437                 _display(tour, step);
438             }
439         };
440 
441         /**
442          * @param id
443          * @param step
444          * @private
445          */
446         var showFromDb = function(id, step)
447         {
448             _load(id,step);
449         };
450 
451         LABKEY.Utils.onReady(_init);
452 
453         return {
454             autoShow: autoShow,
455             continueAtLocation: continueAtLocation,
456             continueTour: continueTour,
457             markSeen: markSeen,
458             register: register,
459             reset: reset,
460             resume: resume,
461             seen: seen,
462             show: show,
463             showFromDb: showFromDb
464         };
465     };
466 
467 })(jQuery);