sf.js 18 KB

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