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);