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