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