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