]>
Commit | Line | Data |
---|---|---|
c63493c8 IB |
1 | /** |
2 | * @license Copyright (c) 2003-2017, CKSource - Frederico Knabben. All rights reserved. | |
3 | * For licensing, see LICENSE.md or http://ckeditor.com/license | |
4 | */ | |
5 | /* exported SF */ | |
6 | ||
7 | 'use strict'; | |
8 | ||
9 | var SF = ( function() { | |
10 | var SF = {}; | |
11 | ||
12 | SF.attachListener = function( elem, evtName, callback ) { | |
13 | if ( elem.addEventListener ) { | |
14 | elem.addEventListener( evtName, callback, false ); | |
15 | } else if ( elem.attachEvent ) { | |
16 | elem.attachEvent( 'on' + evtName , function() { | |
17 | callback.apply( elem, arguments ); | |
18 | } ); | |
19 | } else { | |
20 | throw new Error( 'Could not attach event.' ); | |
21 | } | |
22 | }; | |
23 | ||
24 | SF.indexOf = ( function() { | |
25 | var indexOf = Array.prototype.indexOf; | |
26 | ||
27 | if ( indexOf === 'function' ) { | |
28 | return function( arr, elem ) { | |
29 | return indexOf.call( arr, elem ); | |
30 | }; | |
31 | } else { | |
32 | return function( arr, elem ) { | |
33 | var max = arr.length; | |
34 | ||
35 | for ( var i = 0; i < max; i++ ) { | |
36 | if ( arr[ i ] === elem ) { | |
37 | return i; | |
38 | } | |
39 | } | |
40 | ||
41 | return -1; | |
42 | }; | |
43 | } | |
44 | ||
45 | }() ); | |
46 | ||
47 | SF.accept = function( node, visitor ) { | |
48 | var children; | |
49 | ||
50 | // Handling node as a node and array | |
51 | if ( node.children ) { | |
52 | children = node.children; | |
53 | ||
54 | visitor( node ); | |
55 | } else if ( typeof node.length === 'number' ) { | |
56 | children = node; | |
57 | } | |
58 | ||
59 | var i = children ? ( children.length || 0 ) : 0; | |
60 | while ( i-- ) { | |
61 | SF.accept( children[ i ], visitor ); | |
62 | } | |
63 | }; | |
64 | ||
65 | SF.getByClass = ( function( ) { | |
66 | var getByClass = document.getElementsByClassName; | |
67 | if ( typeof getByClass === 'function' ) { | |
68 | return function( root, className ) { | |
69 | if ( typeof root === 'string' ) { | |
70 | className = root; | |
71 | root = document; | |
72 | } | |
73 | ||
74 | return getByClass.call( root, className ); | |
75 | }; | |
76 | } | |
77 | ||
78 | return function( root, className ) { | |
79 | if ( typeof root === 'string' ) { | |
80 | className = root; | |
81 | root = document.getElementsByTagName( 'html' )[ 0 ]; | |
82 | } | |
83 | var results = []; | |
84 | ||
85 | SF.accept( root, function( elem ) { | |
86 | if ( SF.classList.contains( elem, className ) ) { | |
87 | results.push( elem ); | |
88 | } | |
89 | } ); | |
90 | ||
91 | return results; | |
92 | }; | |
93 | }() ); | |
94 | ||
95 | SF.classList = {}; | |
96 | ||
97 | SF.classList.add = function( elem, className ) { | |
98 | var classes = parseClasses( elem ); | |
99 | classes.push( className ); | |
100 | ||
101 | elem.attributes.setNamedItem( createClassAttr( classes ) ); | |
102 | }; | |
103 | ||
104 | SF.classList.remove = function( elem, className ) { | |
105 | var classes = parseClasses( elem, className ), | |
106 | foundAt = SF.indexOf( classes, className ); | |
107 | ||
108 | if ( foundAt === -1 ) { | |
109 | return; | |
110 | } | |
111 | ||
112 | classes.splice( foundAt, 1 ); | |
113 | elem.attributes.setNamedItem( createClassAttr( classes ) ); | |
114 | }; | |
115 | ||
116 | SF.classList.contains = function( elem, className ) { | |
117 | return findIndex( elem, className ) !== -1; | |
118 | }; | |
119 | ||
120 | SF.classList.toggle = function( elem, className ) { | |
121 | this.contains( elem, className ) ? this.remove( elem, className ) : this.add( elem, className ); | |
122 | }; | |
123 | ||
124 | function findIndex( elem, className ) { | |
125 | return SF.indexOf( parseClasses( elem ), className ); | |
126 | } | |
127 | ||
128 | function parseClasses( elem ) { | |
129 | var classAttr = elem.attributes ? elem.attributes.getNamedItem( 'class' ) : null; | |
130 | ||
131 | return classAttr ? classAttr.value.split( ' ' ) : []; | |
132 | } | |
133 | ||
134 | function createClassAttr( classesArray ) { | |
135 | var attr = document.createAttribute( 'class' ); | |
136 | ||
137 | attr.value = classesArray.join( ' ' ); | |
138 | ||
139 | return attr; | |
140 | } | |
141 | ||
142 | return SF; | |
143 | }() ); | |
144 | ||
145 | /* global SF, picoModal */ | |
146 | ||
147 | 'use strict'; | |
148 | ||
149 | ( function() { | |
150 | // Purges all styles in passed object. | |
151 | function purgeStyles( styles ) { | |
152 | for ( var i in styles ) { | |
153 | delete styles[ i ]; | |
154 | } | |
155 | } | |
156 | ||
157 | SF.modal = function( config ) { | |
158 | // Modal should use the same style set as the rest of the page (.content component). | |
159 | config.modalClass = 'modal content'; | |
160 | config.closeClass = 'modal-close'; | |
161 | ||
162 | // Purge all pre-defined pico styles. Use the lessfile instead. | |
163 | config.modalStyles = purgeStyles; | |
164 | ||
165 | // Close button styles are customized via lessfile. | |
166 | config.closeStyles = purgeStyles; | |
167 | ||
168 | var userDefinedAfterCreate = config.afterCreate, | |
169 | userDefinedAfterClose = config.afterClose; | |
170 | ||
171 | // Close modal on ESC key. | |
172 | function onKeyDown( event ) { | |
173 | if ( event.keyCode == 27 ) { | |
174 | modal.close(); | |
175 | } | |
176 | } | |
177 | ||
178 | // Use afterCreate as a config option rather than function chain. | |
179 | config.afterCreate = function( modal ) { | |
180 | userDefinedAfterCreate && userDefinedAfterCreate( modal ); | |
181 | ||
182 | window.addEventListener( 'keydown', onKeyDown ); | |
183 | }; | |
184 | ||
185 | // Use afterClose as a config option rather than function chain. | |
186 | config.afterClose = function( modal ) { | |
187 | userDefinedAfterClose && userDefinedAfterClose( modal ); | |
188 | ||
189 | window.removeEventListener( 'keydown', onKeyDown ); | |
190 | }; | |
191 | ||
192 | var modal = new picoModal( config ) | |
193 | .afterCreate( config.afterCreate ) | |
194 | .afterClose( config.afterClose ); | |
195 | ||
196 | return modal; | |
197 | }; | |
198 | } )(); | |
199 | 'use strict'; | |
200 | ||
201 | ( function() { | |
202 | // All .tree-a elements in DOM. | |
203 | var expanders = SF.getByClass( 'toggler' ); | |
204 | ||
205 | var i = expanders.length; | |
206 | while ( i-- ) { | |
207 | var expander = expanders[ i ]; | |
208 | ||
209 | SF.attachListener( expander, 'click', function() { | |
210 | var containsIcon = SF.classList.contains( this, 'icon-toggler-expanded' ) || SF.classList.contains( this, 'icon-toggler-collapsed' ), | |
211 | related = document.getElementById( this.getAttribute( 'data-for' ) ); | |
212 | ||
213 | SF.classList.toggle( this, 'collapsed' ); | |
214 | ||
215 | if ( SF.classList.contains( this, 'collapsed' ) ) { | |
216 | SF.classList.add( related, 'collapsed' ); | |
217 | if ( containsIcon ) { | |
218 | SF.classList.remove( this, 'icon-toggler-expanded' ); | |
219 | SF.classList.add( this, 'icon-toggler-collapsed' ); | |
220 | } | |
221 | } else { | |
222 | SF.classList.remove( related, 'collapsed' ); | |
223 | if ( containsIcon ) { | |
224 | SF.classList.remove( this, 'icon-toggler-collapsed' ); | |
225 | SF.classList.add( this, 'icon-toggler-expanded' ); | |
226 | } | |
227 | } | |
228 | } ); | |
229 | } | |
230 | } )(); | |
231 | /* global SF */ | |
232 | ||
233 | 'use strict'; | |
234 | ||
235 | ( function() { | |
236 | // All .tree-a elements in DOM. | |
237 | var trees = SF.getByClass( 'tree-a' ); | |
238 | ||
239 | for ( var i = trees.length; i--; ) { | |
240 | var tree = trees[ i ]; | |
241 | ||
242 | SF.attachListener( tree, 'click', function( evt ) { | |
243 | var target = evt.target || evt.srcElement; | |
244 | ||
245 | // Collapse or expand item groups. | |
246 | if ( target.nodeName === 'H2' && !SF.classList.contains( target, 'tree-a-no-sub' ) ) { | |
247 | SF.classList.toggle( target, 'tree-a-active' ); | |
248 | } | |
249 | } ); | |
250 | } | |
251 | } )(); | |
252 | // jshint ignore:start | |
253 | // jscs:disable | |
254 | /** | |
255 | * Permission is hereby granted, free of charge, to any person obtaining a copy | |
256 | * of this software and associated documentation files (the "Software"), to deal | |
257 | * in the Software without restriction, including without limitation the rights | |
258 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
259 | * copies of the Software, and to permit persons to whom the Software is | |
260 | * furnished to do so, subject to the following conditions: | |
261 | * | |
262 | * The above copyright notice and this permission notice shall be included in | |
263 | * all copies or substantial portions of the Software. | |
264 | * | |
265 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
266 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
267 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
268 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
269 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
270 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
271 | * SOFTWARE. | |
272 | */ | |
273 | ||
274 | /** | |
275 | * A self-contained modal library | |
276 | */ | |
277 | (function(window, document) { | |
278 | "use strict"; | |
279 | ||
280 | /** Returns whether a value is a dom node */ | |
281 | function isNode(value) { | |
282 | if ( typeof Node === "object" ) { | |
283 | return value instanceof Node; | |
284 | } | |
285 | else { | |
286 | return value && | |
287 | typeof value === "object" && | |
288 | typeof value.nodeType === "number"; | |
289 | } | |
290 | } | |
291 | ||
292 | /** Returns whether a value is a string */ | |
293 | function isString(value) { | |
294 | return typeof value === "string"; | |
295 | } | |
296 | ||
297 | /** | |
298 | * Generates observable objects that can be watched and triggered | |
299 | */ | |
300 | function observable() { | |
301 | var callbacks = []; | |
302 | return { | |
303 | watch: callbacks.push.bind(callbacks), | |
304 | trigger: function( modal ) { | |
305 | ||
306 | var unprevented = true; | |
307 | var event = { | |
308 | preventDefault: function preventDefault () { | |
309 | unprevented = false; | |
310 | } | |
311 | }; | |
312 | ||
313 | for (var i = 0; i < callbacks.length; i++) { | |
314 | callbacks[i](modal, event); | |
315 | } | |
316 | ||
317 | return unprevented; | |
318 | } | |
319 | }; | |
320 | } | |
321 | ||
322 | ||
323 | /** | |
324 | * A small interface for creating and managing a dom element | |
325 | */ | |
326 | function Elem( elem ) { | |
327 | this.elem = elem; | |
328 | } | |
329 | ||
330 | /** | |
331 | * Creates a new div | |
332 | */ | |
333 | Elem.div = function ( parent ) { | |
334 | var elem = document.createElement('div'); | |
335 | (parent || document.body).appendChild(elem); | |
336 | return new Elem(elem); | |
337 | }; | |
338 | ||
339 | Elem.prototype = { | |
340 | ||
341 | /** Creates a child of this node */ | |
342 | child: function () { | |
343 | return Elem.div(this.elem); | |
344 | }, | |
345 | ||
346 | /** Applies a set of styles to an element */ | |
347 | stylize: function(styles) { | |
348 | styles = styles || {}; | |
349 | ||
350 | if ( typeof styles.opacity !== "undefined" ) { | |
351 | styles.filter = | |
352 | "alpha(opacity=" + (styles.opacity * 100) + ")"; | |
353 | } | |
354 | ||
355 | for (var prop in styles) { | |
356 | if (styles.hasOwnProperty(prop)) { | |
357 | this.elem.style[prop] = styles[prop]; | |
358 | } | |
359 | } | |
360 | ||
361 | return this; | |
362 | }, | |
363 | ||
364 | /** Adds a class name */ | |
365 | clazz: function (clazz) { | |
366 | this.elem.className += " " + clazz; | |
367 | return this; | |
368 | }, | |
369 | ||
370 | /** Sets the HTML */ | |
371 | html: function (content) { | |
372 | if ( isNode(content) ) { | |
373 | this.elem.appendChild( content ); | |
374 | } | |
375 | else { | |
376 | this.elem.innerHTML = content; | |
377 | } | |
378 | return this; | |
379 | }, | |
380 | ||
381 | /** Adds a click handler to this element */ | |
382 | onClick: function(callback) { | |
383 | this.elem.addEventListener('click', callback); | |
384 | return this; | |
385 | }, | |
386 | ||
387 | /** Removes this element from the DOM */ | |
388 | destroy: function() { | |
389 | document.body.removeChild(this.elem); | |
390 | }, | |
391 | ||
392 | /** Hides this element */ | |
393 | hide: function() { | |
394 | this.elem.style.display = "none"; | |
395 | }, | |
396 | ||
397 | /** Shows this element */ | |
398 | show: function() { | |
399 | this.elem.style.display = "block"; | |
400 | }, | |
401 | ||
402 | /** Sets an attribute on this element */ | |
403 | attr: function ( name, value ) { | |
404 | this.elem.setAttribute(name, value); | |
405 | return this; | |
406 | }, | |
407 | ||
408 | /** Executes a callback on all the ancestors of an element */ | |
409 | anyAncestor: function ( predicate ) { | |
410 | var elem = this.elem; | |
411 | while ( elem ) { | |
412 | if ( predicate( new Elem(elem) ) ) { | |
413 | return true; | |
414 | } | |
415 | else { | |
416 | elem = elem.parentNode; | |
417 | } | |
418 | } | |
419 | return false; | |
420 | } | |
421 | }; | |
422 | ||
423 | ||
424 | /** Generates the grey-out effect */ | |
425 | function buildOverlay( getOption, close ) { | |
426 | return Elem.div() | |
427 | .clazz("pico-overlay") | |
428 | .clazz( getOption("overlayClass", "") ) | |
429 | .stylize({ | |
430 | display: "block", | |
431 | position: "fixed", | |
432 | top: "0px", | |
433 | left: "0px", | |
434 | height: "100%", | |
435 | width: "100%", | |
436 | zIndex: 10000 | |
437 | }) | |
438 | .stylize(getOption('overlayStyles', { | |
439 | opacity: 0.5, | |
440 | background: "#000" | |
441 | })) | |
442 | .onClick(function () { | |
443 | if ( getOption('overlayClose', true) ) { | |
444 | close(); | |
445 | } | |
446 | }); | |
447 | } | |
448 | ||
449 | /** Builds the content of a modal */ | |
450 | function buildModal( getOption, close ) { | |
451 | var width = getOption('width', 'auto'); | |
452 | if ( typeof width === "number" ) { | |
453 | width = "" + width + "px"; | |
454 | } | |
455 | ||
456 | var elem = Elem.div() | |
457 | .clazz("pico-content") | |
458 | .clazz( getOption("modalClass", "") ) | |
459 | .stylize({ | |
460 | display: 'block', | |
461 | position: 'fixed', | |
462 | zIndex: 10001, | |
463 | left: "50%", | |
464 | top: "50px", | |
465 | width: width, | |
466 | '-ms-transform': 'translateX(-50%)', | |
467 | '-moz-transform': 'translateX(-50%)', | |
468 | '-webkit-transform': 'translateX(-50%)', | |
469 | '-o-transform': 'translateX(-50%)', | |
470 | 'transform': 'translateX(-50%)' | |
471 | }) | |
472 | .stylize(getOption('modalStyles', { | |
473 | backgroundColor: "white", | |
474 | padding: "20px", | |
475 | borderRadius: "5px" | |
476 | })) | |
477 | .html( getOption('content') ) | |
478 | .attr("role", "dialog") | |
479 | .onClick(function (event) { | |
480 | var isCloseClick = new Elem(event.target) | |
481 | .anyAncestor(function (elem) { | |
482 | return /\bpico-close\b/.test(elem.elem.className); | |
483 | }); | |
484 | if ( isCloseClick ) { | |
485 | close(); | |
486 | } | |
487 | }); | |
488 | ||
489 | return elem; | |
490 | } | |
491 | ||
492 | /** Builds the close button */ | |
493 | function buildClose ( elem, getOption ) { | |
494 | if ( getOption('closeButton', true) ) { | |
495 | return elem.child() | |
496 | .html( getOption('closeHtml', "×") ) | |
497 | .clazz("pico-close") | |
498 | .clazz( getOption("closeClass") ) | |
499 | .stylize( getOption('closeStyles', { | |
500 | borderRadius: "2px", | |
501 | cursor: "pointer", | |
502 | height: "15px", | |
503 | width: "15px", | |
504 | position: "absolute", | |
505 | top: "5px", | |
506 | right: "5px", | |
507 | fontSize: "16px", | |
508 | textAlign: "center", | |
509 | lineHeight: "15px", | |
510 | background: "#CCC" | |
511 | }) ); | |
512 | } | |
513 | } | |
514 | ||
515 | /** Builds a method that calls a method and returns an element */ | |
516 | function buildElemAccessor( builder ) { | |
517 | return function () { | |
518 | return builder().elem; | |
519 | }; | |
520 | } | |
521 | ||
522 | ||
523 | /** | |
524 | * Displays a modal | |
525 | */ | |
526 | function picoModal(options) { | |
527 | ||
528 | if ( isString(options) || isNode(options) ) { | |
529 | options = { content: options }; | |
530 | } | |
531 | ||
532 | var afterCreateEvent = observable(); | |
533 | var beforeShowEvent = observable(); | |
534 | var afterShowEvent = observable(); | |
535 | var beforeCloseEvent = observable(); | |
536 | var afterCloseEvent = observable(); | |
537 | ||
538 | /** | |
539 | * Returns a named option if it has been explicitly defined. Otherwise, | |
540 | * it returns the given default value | |
541 | */ | |
542 | function getOption ( opt, defaultValue ) { | |
543 | var value = options[opt]; | |
544 | if ( typeof value === "function" ) { | |
545 | value = value( defaultValue ); | |
546 | } | |
547 | return value === undefined ? defaultValue : value; | |
548 | } | |
549 | ||
550 | /** Hides this modal */ | |
551 | function forceClose () { | |
552 | shadowElem().hide(); | |
553 | modalElem().hide(); | |
554 | afterCloseEvent.trigger(iface); | |
555 | } | |
556 | ||
557 | /** Gracefully hides this modal */ | |
558 | function close () { | |
559 | if ( beforeCloseEvent.trigger(iface) ) { | |
560 | forceClose(); | |
561 | } | |
562 | } | |
563 | ||
564 | /** Wraps a method so it returns the modal interface */ | |
565 | function returnIface ( callback ) { | |
566 | return function () { | |
567 | callback.apply(this, arguments); | |
568 | return iface; | |
569 | }; | |
570 | } | |
571 | ||
572 | ||
573 | // The constructed dom nodes | |
574 | var built; | |
575 | ||
576 | /** Builds a method that calls a method and returns an element */ | |
577 | function build ( name ) { | |
578 | if ( !built ) { | |
579 | var modal = buildModal(getOption, close); | |
580 | built = { | |
581 | modal: modal, | |
582 | overlay: buildOverlay(getOption, close), | |
583 | close: buildClose(modal, getOption) | |
584 | }; | |
585 | afterCreateEvent.trigger(iface); | |
586 | } | |
587 | return built[name]; | |
588 | } | |
589 | ||
590 | var modalElem = build.bind(window, 'modal'); | |
591 | var shadowElem = build.bind(window, 'overlay'); | |
592 | var closeElem = build.bind(window, 'close'); | |
593 | ||
594 | ||
595 | var iface = { | |
596 | ||
597 | /** Returns the wrapping modal element */ | |
598 | modalElem: buildElemAccessor(modalElem), | |
599 | ||
600 | /** Returns the close button element */ | |
601 | closeElem: buildElemAccessor(closeElem), | |
602 | ||
603 | /** Returns the overlay element */ | |
604 | overlayElem: buildElemAccessor(shadowElem), | |
605 | ||
606 | /** Shows this modal */ | |
607 | show: function () { | |
608 | if ( beforeShowEvent.trigger(iface) ) { | |
609 | shadowElem().show(); | |
610 | closeElem(); | |
611 | modalElem().show(); | |
612 | afterShowEvent.trigger(iface); | |
613 | } | |
614 | return this; | |
615 | }, | |
616 | ||
617 | /** Hides this modal */ | |
618 | close: returnIface(close), | |
619 | ||
620 | /** | |
621 | * Force closes this modal. This will not call beforeClose | |
622 | * events and will just immediately hide the modal | |
623 | */ | |
624 | forceClose: returnIface(forceClose), | |
625 | ||
626 | /** Destroys this modal */ | |
627 | destroy: function () { | |
628 | modalElem = modalElem().destroy(); | |
629 | shadowElem = shadowElem().destroy(); | |
630 | closeElem = undefined; | |
631 | }, | |
632 | ||
633 | /** | |
634 | * Updates the options for this modal. This will only let you | |
635 | * change options that are re-evaluted regularly, such as | |
636 | * `overlayClose`. | |
637 | */ | |
638 | options: function ( opts ) { | |
639 | options = opts; | |
640 | }, | |
641 | ||
642 | /** Executes after the DOM nodes are created */ | |
643 | afterCreate: returnIface(afterCreateEvent.watch), | |
644 | ||
645 | /** Executes a callback before this modal is closed */ | |
646 | beforeShow: returnIface(beforeShowEvent.watch), | |
647 | ||
648 | /** Executes a callback after this modal is shown */ | |
649 | afterShow: returnIface(afterShowEvent.watch), | |
650 | ||
651 | /** Executes a callback before this modal is closed */ | |
652 | beforeClose: returnIface(beforeCloseEvent.watch), | |
653 | ||
654 | /** Executes a callback after this modal is closed */ | |
655 | afterClose: returnIface(afterCloseEvent.watch) | |
656 | }; | |
657 | ||
658 | return iface; | |
659 | } | |
660 | ||
661 | if ( typeof window.define === "function" && window.define.amd ) { | |
662 | window.define(function () { | |
663 | return picoModal; | |
664 | }); | |
665 | } | |
666 | else { | |
667 | window.picoModal = picoModal; | |
668 | } | |
669 | ||
670 | }(window, document)); | |
671 | ||
672 | // jscs:enable | |
673 | // jshint ignore:end |