annotate src/DML/MainVisBundle/Resources/assets/marionette/modules/HelpModule.js @ 0:493bcb69166c

added public content
author Daniel Wolff
date Tue, 09 Feb 2016 20:54:02 +0100
parents
children
rev   line source
Daniel@0 1 "use strict";
Daniel@0 2
Daniel@0 3 /*
Daniel@0 4 * TODO
Daniel@0 5 * keep scroll position when resize
Daniel@0 6 *
Daniel@0 7 * fix focus outline around links in TOC in FF
Daniel@0 8 * dotted border
Daniel@0 9 */
Daniel@0 10 App.module("HelpModule", function(HelpModule, App, Backbone, Marionette, $, _, Logger) {
Daniel@0 11
Daniel@0 12 // Prevent auto start
Daniel@0 13 HelpModule.startWithParent = false;
Daniel@0 14
Daniel@0 15 // Define options
Daniel@0 16 var defaultModuleOptions = {
Daniel@0 17 contentScrollDuration: 200,
Daniel@0 18 resizeThrottleDuration: 200,
Daniel@0 19 resizeDebounceDuration: 200,
Daniel@0 20 scrollThrottleDuration: 200,
Daniel@0 21 scrollDebounceDuration: 200,
Daniel@0 22 };
Daniel@0 23 var moduleOptions;
Daniel@0 24
Daniel@0 25 // Define private variables
Daniel@0 26 var logger = null;
Daniel@0 27 var $help = null;
Daniel@0 28 var $helpBody = null;
Daniel@0 29 var $helpCloser = null;
Daniel@0 30 var $helpContentContainer = null;
Daniel@0 31 var $helpContent = null;
Daniel@0 32 var $helpContentHeaders = null;
Daniel@0 33 var $helpTocContainer = null;
Daniel@0 34 var $helpToc = null;
Daniel@0 35
Daniel@0 36 var contentScrollSavedPosition = null; // [offset, $materialHeader] $materialHeader - where to offset from
Daniel@0 37 var resizing = false; // true when window is resizing, ignore
Daniel@0 38 var scrolling = false; // true when content is being scrolled for whatever reason
Daniel@0 39 var scrollingTo = null; // data id of an item that is being scrolled to during an animation
Daniel@0 40
Daniel@0 41 var lastShownMaterialId = null;
Daniel@0 42 var pendingMaterialId = null;
Daniel@0 43
Daniel@0 44 var $lastContentHeader = null; // Not last used, but last in the list (to set up bottom margin)
Daniel@0 45
Daniel@0 46 // Initialization checker
Daniel@0 47 var assertModuleIsInitialized = function() {
Daniel@0 48 if (!$help) {
Daniel@0 49 throw "HelpModule has not been initialized";
Daniel@0 50 }
Daniel@0 51 };
Daniel@0 52
Daniel@0 53 // Private functions
Daniel@0 54 var updateContentBottomMargin = null;
Daniel@0 55 var updateContentScrollSavedPosition = null;
Daniel@0 56 var restoreContentScrollSavedPosition = null;
Daniel@0 57
Daniel@0 58 var updateTocCurrentItem = null;
Daniel@0 59
Daniel@0 60 /**
Daniel@0 61 * Module initializer
Daniel@0 62 *
Daniel@0 63 */
Daniel@0 64 HelpModule.addInitializer(function(options){
Daniel@0 65
Daniel@0 66 moduleOptions = _.extend(defaultModuleOptions, options);
Daniel@0 67
Daniel@0 68 logger = Logger.get("HelpModule");
Daniel@0 69 //logger.setLevel(Logger.DEBUG);
Daniel@0 70
Daniel@0 71 // When window is resized, bottom margin of the content should be updated
Daniel@0 72 updateContentBottomMargin = function(makeBigAndLockScroll) {
Daniel@0 73 if (makeBigAndLockScroll) {
Daniel@0 74 $helpContentContainer
Daniel@0 75 .css("overflow", "hidden");
Daniel@0 76 $helpContent
Daniel@0 77 .css("border-bottom-width", 10000);
Daniel@0 78 } else {
Daniel@0 79 $helpContentContainer
Daniel@0 80 .css("overflow", "scroll");
Daniel@0 81 $helpContent
Daniel@0 82 .css('border-bottom-width',
Daniel@0 83 Math.max(0,
Daniel@0 84 $helpContentContainer.outerHeight()
Daniel@0 85 - $helpContent.height()
Daniel@0 86 + $lastContentHeader.position().top
Daniel@0 87 - parseInt($helpContent.css('padding-bottom'), 10)
Daniel@0 88 + parseInt($lastContentHeader.css('margin-top'), 10)
Daniel@0 89 ));
Daniel@0 90 }
Daniel@0 91 };
Daniel@0 92
Daniel@0 93 // Looks at the current scroll position and updates
Daniel@0 94 // contentScrollSavedPosition accordingly
Daniel@0 95 updateContentScrollSavedPosition = function() {
Daniel@0 96 var scrollTop = $helpContentContainer.scrollTop();
Daniel@0 97 var $candidateHeader = $helpContentHeaders.first();
Daniel@0 98
Daniel@0 99 for (var i = 0; i <= $helpContentHeaders.length; i++) {
Daniel@0 100 var $helpContentHeader = $($helpContentHeaders[i]);
Daniel@0 101 if (!$helpContentHeader.length || $helpContentHeader.position().top >= scrollTop) {
Daniel@0 102 contentScrollSavedPosition = [
Daniel@0 103 Math.floor(scrollTop - $candidateHeader.position().top - parseInt($candidateHeader.css('margin-top')), 10),
Daniel@0 104 $candidateHeader
Daniel@0 105 ];
Daniel@0 106 updateTocCurrentItem();
Daniel@0 107 break;
Daniel@0 108 } else {
Daniel@0 109 $candidateHeader = $helpContentHeader;
Daniel@0 110 }
Daniel@0 111 }
Daniel@0 112
Daniel@0 113 App.DataModule.Storage.setStrCache(HelpModule, "saved-scroll-position", contentScrollSavedPosition[0] + " " + contentScrollSavedPosition[1].attr('data-id'));
Daniel@0 114 };
Daniel@0 115
Daniel@0 116 // scrolls to a saved scroll position
Daniel@0 117 restoreContentScrollSavedPosition = function(animate) {
Daniel@0 118 if (animate) {
Daniel@0 119 scrollingTo = contentScrollSavedPosition[1].attr('data-id');
Daniel@0 120 }
Daniel@0 121 $helpContentContainer
Daniel@0 122 .stop(true, false)
Daniel@0 123 .scrollTo(contentScrollSavedPosition[1].position().top + parseInt(contentScrollSavedPosition[1].css('margin-top'), 10) + contentScrollSavedPosition[0], {
Daniel@0 124 duration: animate ? moduleOptions.contentScrollDuration : 0,
Daniel@0 125 }, function() {
Daniel@0 126 scrollingTo = null;
Daniel@0 127 });
Daniel@0 128 };
Daniel@0 129
Daniel@0 130 updateTocCurrentItem = function() {
Daniel@0 131 var newMaterialId = scrollingTo !== null ? scrollingTo : contentScrollSavedPosition[1].attr('data-id');
Daniel@0 132
Daniel@0 133 if (lastShownMaterialId !== newMaterialId) {
Daniel@0 134 $helpToc
Daniel@0 135 .children()
Daniel@0 136 .removeClass("help__toc-element_current");
Daniel@0 137 //.setMod('help', 'toc-element', 'current', false);
Daniel@0 138 $helpToc.find(_.str.sprintf("[data-id='%s']", newMaterialId))
Daniel@0 139 .addClass("help__toc-element_current");
Daniel@0 140 //.setMod('help', 'toc-element', 'current', true);
Daniel@0 141 }
Daniel@0 142
Daniel@0 143 if (lastShownMaterialId != newMaterialId) {
Daniel@0 144 if (HelpModule.isShowing()) {
Daniel@0 145 lastShownMaterialId = newMaterialId;
Daniel@0 146 HelpModule.trigger("show", {"materialId": newMaterialId});
Daniel@0 147 }
Daniel@0 148 }
Daniel@0 149 };
Daniel@0 150
Daniel@0 151 $help = $.bem.generateBlock('help').setMod('help','state','hidden');
Daniel@0 152 $helpBody = $.bem.generateElement('help', 'body');
Daniel@0 153 $helpContentContainer = $.bem.generateElement('help', 'content-container');
Daniel@0 154 $helpContent = $.bem.generateElement('help', 'content');
Daniel@0 155 $helpTocContainer = $.bem.generateElement('help', 'toc-container');
Daniel@0 156 $helpToc = $.bem.generateElement('help', 'toc');
Daniel@0 157 $helpCloser = $.bem.generateElement('help', 'closer');
Daniel@0 158
Daniel@0 159 // Clicking outside help hides it
Daniel@0 160 $help.click(function(event) {
Daniel@0 161 if ($help.hasMod("help", "state_shown")) {
Daniel@0 162 HelpModule.hide();
Daniel@0 163 }
Daniel@0 164 event.stopPropagation();
Daniel@0 165 });
Daniel@0 166 $helpBody.click(function(event) {
Daniel@0 167 event.stopPropagation();
Daniel@0 168 });
Daniel@0 169
Daniel@0 170 // Help content goes from #help-content template
Daniel@0 171 // It is both an element and a block
Daniel@0 172 $helpContent.addClass('help-content')
Daniel@0 173 .append($(Backbone.Marionette.TemplateCache.get("#help-content")({
Daniel@0 174 Ctrl: _.str.capitalize(App.keyboardMappings.ctrlTitle)
Daniel@0 175 })));
Daniel@0 176
Daniel@0 177 // Help TOC (table of contents) is populated from headers in content
Daniel@0 178 var usedDataIds = [];
Daniel@0 179 $helpContentHeaders = $helpContent.find('h1, h2, h3');
Daniel@0 180 $helpContentHeaders.each(function(i, helpContentHeader) {
Daniel@0 181 var $helpContentHeader = $(helpContentHeader);
Daniel@0 182 var title = $helpContentHeader.attr('data-toc');
Daniel@0 183 if (!title) {
Daniel@0 184 title = $helpContentHeader.text();
Daniel@0 185 }
Daniel@0 186 var id = $helpContentHeader.attr('data-id');
Daniel@0 187 if (_.isUndefined(id)) {
Daniel@0 188 id = _.str.slugify($helpContentHeader.text());
Daniel@0 189 }
Daniel@0 190 if (usedDataIds.indexOf(id) != -1) {
Daniel@0 191 throw _.str.sprintf("There are more than one header with id = '%s' in help", id);
Daniel@0 192 }
Daniel@0 193 usedDataIds.push(id);
Daniel@0 194 $helpContentHeader.attr('data-id', id);
Daniel@0 195 var $currentHelpTocElement = $.bem.generateElement('a', 'help', 'toc-element')
Daniel@0 196 .attr('href', id ? _.str.sprintf('#help/%s', id) : '#help')
Daniel@0 197 .attr('data-id', id)
Daniel@0 198 .setMod('help', 'toc-element', 'hierarchy', $helpContentHeader.prop('tagName').slice(1))
Daniel@0 199 .text(title)
Daniel@0 200 .click(function(event) {
Daniel@0 201 if ($.eventsugar.isAttemptToOpenInAnotherWindow(event)) {
Daniel@0 202 return;
Daniel@0 203 }
Daniel@0 204 event.preventDefault();
Daniel@0 205 HelpModule.show({materialId: id, forceScroll: true});
Daniel@0 206 return false;
Daniel@0 207 });
Daniel@0 208
Daniel@0 209 $helpToc.append($currentHelpTocElement);
Daniel@0 210 $lastContentHeader = $helpContentHeader;
Daniel@0 211 });
Daniel@0 212
Daniel@0 213 // Restore scroll position from cache
Daniel@0 214 var rawSavedScrollPosition = App.DataModule.Storage.getStrCache(HelpModule, "saved-scroll-position");
Daniel@0 215 if (rawSavedScrollPosition) {
Daniel@0 216 var i = rawSavedScrollPosition.indexOf(" ");
Daniel@0 217 var offset = parseInt(rawSavedScrollPosition.slice(0,i));
Daniel@0 218 var $materialHeader = $helpContentHeaders.filter(_.str.sprintf("[data-id='%s']", rawSavedScrollPosition.slice(i+1)));
Daniel@0 219 if ($materialHeader.length) {
Daniel@0 220 contentScrollSavedPosition = [offset, $materialHeader];
Daniel@0 221 }
Daniel@0 222 }
Daniel@0 223 if (!contentScrollSavedPosition) {
Daniel@0 224 contentScrollSavedPosition = [0, $helpContentHeaders.first()];
Daniel@0 225 }
Daniel@0 226 lastShownMaterialId = contentScrollSavedPosition[1].attr("data-id");
Daniel@0 227
Daniel@0 228 // Help closer
Daniel@0 229 //// Move to the right for Windows
Daniel@0 230 if (navigator && navigator.appVersion && navigator.appVersion.indexOf("Win") != -1) {
Daniel@0 231 $helpCloser.setMod('help', 'closer', 'position', 'right');
Daniel@0 232 }
Daniel@0 233 //// Close help on click
Daniel@0 234 $helpCloser.click(function(event) {
Daniel@0 235 if ($help.hasMod("help", "state_shown")) {
Daniel@0 236 HelpModule.hide();
Daniel@0 237 }
Daniel@0 238 event.stopPropagation();
Daniel@0 239 });
Daniel@0 240
Daniel@0 241 // Build element hierarchy
Daniel@0 242 $helpContentContainer.append($helpContent);
Daniel@0 243 $helpTocContainer.append($helpToc);
Daniel@0 244 $helpBody.append($helpContentContainer, $helpTocContainer);
Daniel@0 245 $help.append($helpBody, $helpCloser);
Daniel@0 246
Daniel@0 247 $('.app__help').append($help);
Daniel@0 248 $help.setMod("help", "animating", true);
Daniel@0 249
Daniel@0 250 // Window events
Daniel@0 251 var $window = $(window);
Daniel@0 252
Daniel@0 253 $window.on("resize", function() {
Daniel@0 254 if (!HelpModule.isShowing()) {
Daniel@0 255 return;
Daniel@0 256 }
Daniel@0 257 if (!resizing) {
Daniel@0 258 resizing = true;
Daniel@0 259 updateContentBottomMargin(true);
Daniel@0 260 }
Daniel@0 261 restoreContentScrollSavedPosition();
Daniel@0 262 });
Daniel@0 263
Daniel@0 264 $window.on("resize", _.debounce(
Daniel@0 265 function(event) {
Daniel@0 266 if (!HelpModule.isShowing()) {
Daniel@0 267 return;
Daniel@0 268 }
Daniel@0 269 updateContentBottomMargin();
Daniel@0 270 resizing = false;
Daniel@0 271 restoreContentScrollSavedPosition(true);
Daniel@0 272 },
Daniel@0 273 moduleOptions.resizeDebounceDuration));
Daniel@0 274
Daniel@0 275 $helpContentContainer.on("scroll",_.throttle(
Daniel@0 276 function(event){if (!resizing) {scrolling = true; updateContentScrollSavedPosition();}},
Daniel@0 277 moduleOptions.scrollThrottleDuration, {trailing: false}));
Daniel@0 278
Daniel@0 279 $helpContentContainer.on("scroll", _.debounce(
Daniel@0 280 function(event) {
Daniel@0 281 scrolling = false;
Daniel@0 282 updateContentScrollSavedPosition();
Daniel@0 283 if (HelpModule.isShowing()
Daniel@0 284 && pendingMaterialId !== null
Daniel@0 285 && pendingMaterialId != lastShownMaterialId
Daniel@0 286 && pendingMaterialId != contentScrollSavedPosition[1].attr('data-id')) {
Daniel@0 287 contentScrollSavedPosition = [0, $helpContentHeaders.filter(_.str.sprintf("[data-id='%s']", pendingMaterialId))];
Daniel@0 288 pendingMaterialId = null;
Daniel@0 289 restoreContentScrollSavedPosition(true);
Daniel@0 290 } else {
Daniel@0 291 pendingMaterialId = null;
Daniel@0 292 }
Daniel@0 293 },
Daniel@0 294 moduleOptions.scrollDebounceDuration));
Daniel@0 295
Daniel@0 296
Daniel@0 297 /// Scroll fix for help
Daniel@0 298 new ScrollFix($helpContentContainer.get(0));
Daniel@0 299 $helpContentContainer.get(0).addEventListener('touchmove', function(event){
Daniel@0 300 event.stopPropagation();
Daniel@0 301 });
Daniel@0 302
Daniel@0 303 // Embedded content
Daniel@0 304 var helpContentHasVimeo = false;
Daniel@0 305 $helpContent.find("iframe").each(function() {
Daniel@0 306 var $iframe = $(this);
Daniel@0 307 var src = $iframe.attr("src");
Daniel@0 308 if (!_.isString(src)) {
Daniel@0 309 src = "";
Daniel@0 310 }
Daniel@0 311 if (src.indexOf("vimeo") !== -1) {
Daniel@0 312 helpContentHasVimeo = true;
Daniel@0 313 $(this).attr("data-type", "vimeo");
Daniel@0 314 }
Daniel@0 315 });
Daniel@0 316 // enable control of vimeo
Daniel@0 317 // see http://stackoverflow.com/questions/10401819/jquery-how-to-stop-vimeo-video-when-click
Daniel@0 318 if (helpContentHasVimeo) {
Daniel@0 319 var scriptElement = document.createElement('script');
Daniel@0 320 scriptElement.type = 'text/javascript';
Daniel@0 321 scriptElement.async = true;
Daniel@0 322 scriptElement.src = "http://a.vimeocdn.com/js/froogaloop2.min.js";
Daniel@0 323 document.getElementsByTagName('body')[0].appendChild(scriptElement);
Daniel@0 324 }
Daniel@0 325 });
Daniel@0 326
Daniel@0 327 /**
Daniel@0 328 * Shows a specific material id (scrolls to it)
Daniel@0 329 * or shows the top of the help page if no materialId is specified
Daniel@0 330 *
Daniel@0 331 * options
Daniel@0 332 * materialId
Daniel@0 333 * $dispatcher is a jquery element that triggered help page opening
Daniel@0 334 * this parameter helps animate the body of the help (TODO)
Daniel@0 335 *
Daniel@0 336 * forceScroll
Daniel@0 337 * instant
Daniel@0 338 *
Daniel@0 339 */
Daniel@0 340 HelpModule.show = function(options) {
Daniel@0 341
Daniel@0 342 var options = $.extend({}, options);
Daniel@0 343 assertModuleIsInitialized();
Daniel@0 344
Daniel@0 345 var helpIsOpening = false;
Daniel@0 346 if ($help.hasMod('help', 'state_hidden') || $help.hasMod('help', 'state_pre-hidden')) {
Daniel@0 347 helpIsOpening = true;
Daniel@0 348 $help.toggleClass('help_animating', !options.instant);
Daniel@0 349 $help.setMod('help', 'state', 'pre-shown');
Daniel@0 350 lastShownMaterialId = null;
Daniel@0 351 pendingMaterialId = null;
Daniel@0 352 updateContentBottomMargin();
Daniel@0 353 }
Daniel@0 354
Daniel@0 355 var needToScroll = true;
Daniel@0 356 var $materialHeader = $helpContent.find(_.str.sprintf("[data-id='%s']", options.materialId));
Daniel@0 357 if ($materialHeader.length) {
Daniel@0 358 pendingMaterialId = options.materialId;
Daniel@0 359 if (!scrolling) {
Daniel@0 360 if (options.forceScroll || !contentScrollSavedPosition[1].is($materialHeader)) {
Daniel@0 361 contentScrollSavedPosition = [0, $materialHeader];
Daniel@0 362 } else {
Daniel@0 363 needToScroll = false;
Daniel@0 364 }
Daniel@0 365 }
Daniel@0 366 }
Daniel@0 367 if (helpIsOpening || needToScroll && (!scrolling || scrollingTo !== null)) {
Daniel@0 368 restoreContentScrollSavedPosition(!helpIsOpening);
Daniel@0 369 updateTocCurrentItem();
Daniel@0 370 }
Daniel@0 371
Daniel@0 372 if (!needToScroll) {
Daniel@0 373 scrollingTo = null;
Daniel@0 374 pendingMaterialId = null;
Daniel@0 375 }
Daniel@0 376
Daniel@0 377
Daniel@0 378 if ($help.hasMod('help', 'state_pre-shown')) {
Daniel@0 379 $help.setMod('help', 'state', 'shown');
Daniel@0 380 }
Daniel@0 381 if (!!options.instant) {
Daniel@0 382 setTimeout(function() {
Daniel@0 383 $help.setMod('help', 'animating', true);
Daniel@0 384 }, 10)
Daniel@0 385 }
Daniel@0 386 };
Daniel@0 387
Daniel@0 388 /**
Daniel@0 389 * Hides help
Daniel@0 390 */
Daniel@0 391 HelpModule.hide = function() {
Daniel@0 392 assertModuleIsInitialized();
Daniel@0 393
Daniel@0 394 // disable content
Daniel@0 395 //// pause vimeo
Daniel@0 396 $helpContent.find("iframe[data-type=vimeo]").each(function() {
Daniel@0 397 if (window.$f) {
Daniel@0 398 $f(this).api("pause");
Daniel@0 399 }
Daniel@0 400 });
Daniel@0 401
Daniel@0 402 $help.setMod('help', 'state', 'pre-hidden');
Daniel@0 403 HelpModule.trigger("hide");
Daniel@0 404 setTimeout(function() {
Daniel@0 405 if ($help.hasMod('help', 'state_pre-hidden')) {
Daniel@0 406 $help.setMod('help', 'state', 'hidden');
Daniel@0 407 };
Daniel@0 408 }, 1000);
Daniel@0 409 };
Daniel@0 410
Daniel@0 411 /**
Daniel@0 412 * Returns true if help is being shown or is about to be shown
Daniel@0 413 * False is return when help is not visible or is about to be hidden
Daniel@0 414 */
Daniel@0 415 HelpModule.isShowing = function() {
Daniel@0 416 return $help.hasMod("help", "state_pre-shown") || $help.hasMod("help", "state_shown");
Daniel@0 417 };
Daniel@0 418 }, Logger);