Mercurial > hg > dml-open-vis
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); |