The version of vichan running on lainchan.org
Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

7121 lignes
202KB

  1. /**
  2. * Super simple wysiwyg editor v0.8.4
  3. * http://summernote.org/
  4. *
  5. * summernote.js
  6. * Copyright 2013-2016 Alan Hong. and other contributors
  7. * summernote may be freely distributed under the MIT license./
  8. *
  9. * Date: 2017-05-31T03:24Z
  10. */
  11. (function (factory) {
  12. /* global define */
  13. if (typeof define === 'function' && define.amd) {
  14. // AMD. Register as an anonymous module.
  15. define(['jquery'], factory);
  16. } else if (typeof module === 'object' && module.exports) {
  17. // Node/CommonJS
  18. module.exports = factory(require('jquery'));
  19. } else {
  20. // Browser globals
  21. factory(window.jQuery);
  22. }
  23. }(function ($) {
  24. 'use strict';
  25. var isSupportAmd = typeof define === 'function' && define.amd;
  26. /**
  27. * returns whether font is installed or not.
  28. *
  29. * @param {String} fontName
  30. * @return {Boolean}
  31. */
  32. var isFontInstalled = function (fontName) {
  33. var testFontName = fontName === 'Comic Sans MS' ? 'Courier New' : 'Comic Sans MS';
  34. var $tester = $('<div>').css({
  35. position: 'absolute',
  36. left: '-9999px',
  37. top: '-9999px',
  38. fontSize: '200px'
  39. }).text('mmmmmmmmmwwwwwww').appendTo(document.body);
  40. var originalWidth = $tester.css('fontFamily', testFontName).width();
  41. var width = $tester.css('fontFamily', fontName + ',' + testFontName).width();
  42. $tester.remove();
  43. return originalWidth !== width;
  44. };
  45. var userAgent = navigator.userAgent;
  46. var isMSIE = /MSIE|Trident/i.test(userAgent);
  47. var browserVersion;
  48. if (isMSIE) {
  49. var matches = /MSIE (\d+[.]\d+)/.exec(userAgent);
  50. if (matches) {
  51. browserVersion = parseFloat(matches[1]);
  52. }
  53. matches = /Trident\/.*rv:([0-9]{1,}[\.0-9]{0,})/.exec(userAgent);
  54. if (matches) {
  55. browserVersion = parseFloat(matches[1]);
  56. }
  57. }
  58. var isEdge = /Edge\/\d+/.test(userAgent);
  59. var hasCodeMirror = !!window.CodeMirror;
  60. if (!hasCodeMirror && isSupportAmd && typeof require !== 'undefined') {
  61. if (typeof require.resolve !== 'undefined') {
  62. try {
  63. // If CodeMirror can't be resolved, `require.resolve` will throw an
  64. // exception and `hasCodeMirror` won't be set to `true`.
  65. require.resolve('codemirror');
  66. hasCodeMirror = true;
  67. } catch (e) {
  68. // Do nothing.
  69. }
  70. } else if (typeof eval('require').specified !== 'undefined') {
  71. hasCodeMirror = eval('require').specified('codemirror');
  72. }
  73. }
  74. var isSupportTouch =
  75. (('ontouchstart' in window) ||
  76. (navigator.MaxTouchPoints > 0) ||
  77. (navigator.msMaxTouchPoints > 0));
  78. /**
  79. * @class core.agent
  80. *
  81. * Object which check platform and agent
  82. *
  83. * @singleton
  84. * @alternateClassName agent
  85. */
  86. var agent = {
  87. isMac: navigator.appVersion.indexOf('Mac') > -1,
  88. isMSIE: isMSIE,
  89. isEdge: isEdge,
  90. isFF: !isEdge && /firefox/i.test(userAgent),
  91. isPhantom: /PhantomJS/i.test(userAgent),
  92. isWebkit: !isEdge && /webkit/i.test(userAgent),
  93. isChrome: !isEdge && /chrome/i.test(userAgent),
  94. isSafari: !isEdge && /safari/i.test(userAgent),
  95. browserVersion: browserVersion,
  96. jqueryVersion: parseFloat($.fn.jquery),
  97. isSupportAmd: isSupportAmd,
  98. isSupportTouch: isSupportTouch,
  99. hasCodeMirror: hasCodeMirror,
  100. isFontInstalled: isFontInstalled,
  101. isW3CRangeSupport: !!document.createRange
  102. };
  103. /**
  104. * @class core.func
  105. *
  106. * func utils (for high-order func's arg)
  107. *
  108. * @singleton
  109. * @alternateClassName func
  110. */
  111. var func = (function () {
  112. var eq = function (itemA) {
  113. return function (itemB) {
  114. return itemA === itemB;
  115. };
  116. };
  117. var eq2 = function (itemA, itemB) {
  118. return itemA === itemB;
  119. };
  120. var peq2 = function (propName) {
  121. return function (itemA, itemB) {
  122. return itemA[propName] === itemB[propName];
  123. };
  124. };
  125. var ok = function () {
  126. return true;
  127. };
  128. var fail = function () {
  129. return false;
  130. };
  131. var not = function (f) {
  132. return function () {
  133. return !f.apply(f, arguments);
  134. };
  135. };
  136. var and = function (fA, fB) {
  137. return function (item) {
  138. return fA(item) && fB(item);
  139. };
  140. };
  141. var self = function (a) {
  142. return a;
  143. };
  144. var invoke = function (obj, method) {
  145. return function () {
  146. return obj[method].apply(obj, arguments);
  147. };
  148. };
  149. var idCounter = 0;
  150. /**
  151. * generate a globally-unique id
  152. *
  153. * @param {String} [prefix]
  154. */
  155. var uniqueId = function (prefix) {
  156. var id = ++idCounter + '';
  157. return prefix ? prefix + id : id;
  158. };
  159. /**
  160. * returns bnd (bounds) from rect
  161. *
  162. * - IE Compatibility Issue: http://goo.gl/sRLOAo
  163. * - Scroll Issue: http://goo.gl/sNjUc
  164. *
  165. * @param {Rect} rect
  166. * @return {Object} bounds
  167. * @return {Number} bounds.top
  168. * @return {Number} bounds.left
  169. * @return {Number} bounds.width
  170. * @return {Number} bounds.height
  171. */
  172. var rect2bnd = function (rect) {
  173. var $document = $(document);
  174. return {
  175. top: rect.top + $document.scrollTop(),
  176. left: rect.left + $document.scrollLeft(),
  177. width: rect.right - rect.left,
  178. height: rect.bottom - rect.top
  179. };
  180. };
  181. /**
  182. * returns a copy of the object where the keys have become the values and the values the keys.
  183. * @param {Object} obj
  184. * @return {Object}
  185. */
  186. var invertObject = function (obj) {
  187. var inverted = {};
  188. for (var key in obj) {
  189. if (obj.hasOwnProperty(key)) {
  190. inverted[obj[key]] = key;
  191. }
  192. }
  193. return inverted;
  194. };
  195. /**
  196. * @param {String} namespace
  197. * @param {String} [prefix]
  198. * @return {String}
  199. */
  200. var namespaceToCamel = function (namespace, prefix) {
  201. prefix = prefix || '';
  202. return prefix + namespace.split('.').map(function (name) {
  203. return name.substring(0, 1).toUpperCase() + name.substring(1);
  204. }).join('');
  205. };
  206. /**
  207. * Returns a function, that, as long as it continues to be invoked, will not
  208. * be triggered. The function will be called after it stops being called for
  209. * N milliseconds. If `immediate` is passed, trigger the function on the
  210. * leading edge, instead of the trailing.
  211. * @param {Function} func
  212. * @param {Number} wait
  213. * @param {Boolean} immediate
  214. * @return {Function}
  215. */
  216. var debounce = function (func, wait, immediate) {
  217. var timeout;
  218. return function () {
  219. var context = this, args = arguments;
  220. var later = function () {
  221. timeout = null;
  222. if (!immediate) {
  223. func.apply(context, args);
  224. }
  225. };
  226. var callNow = immediate && !timeout;
  227. clearTimeout(timeout);
  228. timeout = setTimeout(later, wait);
  229. if (callNow) {
  230. func.apply(context, args);
  231. }
  232. };
  233. };
  234. return {
  235. eq: eq,
  236. eq2: eq2,
  237. peq2: peq2,
  238. ok: ok,
  239. fail: fail,
  240. self: self,
  241. not: not,
  242. and: and,
  243. invoke: invoke,
  244. uniqueId: uniqueId,
  245. rect2bnd: rect2bnd,
  246. invertObject: invertObject,
  247. namespaceToCamel: namespaceToCamel,
  248. debounce: debounce
  249. };
  250. })();
  251. /**
  252. * @class core.list
  253. *
  254. * list utils
  255. *
  256. * @singleton
  257. * @alternateClassName list
  258. */
  259. var list = (function () {
  260. /**
  261. * returns the first item of an array.
  262. *
  263. * @param {Array} array
  264. */
  265. var head = function (array) {
  266. return array[0];
  267. };
  268. /**
  269. * returns the last item of an array.
  270. *
  271. * @param {Array} array
  272. */
  273. var last = function (array) {
  274. return array[array.length - 1];
  275. };
  276. /**
  277. * returns everything but the last entry of the array.
  278. *
  279. * @param {Array} array
  280. */
  281. var initial = function (array) {
  282. return array.slice(0, array.length - 1);
  283. };
  284. /**
  285. * returns the rest of the items in an array.
  286. *
  287. * @param {Array} array
  288. */
  289. var tail = function (array) {
  290. return array.slice(1);
  291. };
  292. /**
  293. * returns item of array
  294. */
  295. var find = function (array, pred) {
  296. for (var idx = 0, len = array.length; idx < len; idx ++) {
  297. var item = array[idx];
  298. if (pred(item)) {
  299. return item;
  300. }
  301. }
  302. };
  303. /**
  304. * returns true if all of the values in the array pass the predicate truth test.
  305. */
  306. var all = function (array, pred) {
  307. for (var idx = 0, len = array.length; idx < len; idx ++) {
  308. if (!pred(array[idx])) {
  309. return false;
  310. }
  311. }
  312. return true;
  313. };
  314. /**
  315. * returns index of item
  316. */
  317. var indexOf = function (array, item) {
  318. return $.inArray(item, array);
  319. };
  320. /**
  321. * returns true if the value is present in the list.
  322. */
  323. var contains = function (array, item) {
  324. return indexOf(array, item) !== -1;
  325. };
  326. /**
  327. * get sum from a list
  328. *
  329. * @param {Array} array - array
  330. * @param {Function} fn - iterator
  331. */
  332. var sum = function (array, fn) {
  333. fn = fn || func.self;
  334. return array.reduce(function (memo, v) {
  335. return memo + fn(v);
  336. }, 0);
  337. };
  338. /**
  339. * returns a copy of the collection with array type.
  340. * @param {Collection} collection - collection eg) node.childNodes, ...
  341. */
  342. var from = function (collection) {
  343. var result = [], idx = -1, length = collection.length;
  344. while (++idx < length) {
  345. result[idx] = collection[idx];
  346. }
  347. return result;
  348. };
  349. /**
  350. * returns whether list is empty or not
  351. */
  352. var isEmpty = function (array) {
  353. return !array || !array.length;
  354. };
  355. /**
  356. * cluster elements by predicate function.
  357. *
  358. * @param {Array} array - array
  359. * @param {Function} fn - predicate function for cluster rule
  360. * @param {Array[]}
  361. */
  362. var clusterBy = function (array, fn) {
  363. if (!array.length) { return []; }
  364. var aTail = tail(array);
  365. return aTail.reduce(function (memo, v) {
  366. var aLast = last(memo);
  367. if (fn(last(aLast), v)) {
  368. aLast[aLast.length] = v;
  369. } else {
  370. memo[memo.length] = [v];
  371. }
  372. return memo;
  373. }, [[head(array)]]);
  374. };
  375. /**
  376. * returns a copy of the array with all false values removed
  377. *
  378. * @param {Array} array - array
  379. * @param {Function} fn - predicate function for cluster rule
  380. */
  381. var compact = function (array) {
  382. var aResult = [];
  383. for (var idx = 0, len = array.length; idx < len; idx ++) {
  384. if (array[idx]) { aResult.push(array[idx]); }
  385. }
  386. return aResult;
  387. };
  388. /**
  389. * produces a duplicate-free version of the array
  390. *
  391. * @param {Array} array
  392. */
  393. var unique = function (array) {
  394. var results = [];
  395. for (var idx = 0, len = array.length; idx < len; idx ++) {
  396. if (!contains(results, array[idx])) {
  397. results.push(array[idx]);
  398. }
  399. }
  400. return results;
  401. };
  402. /**
  403. * returns next item.
  404. * @param {Array} array
  405. */
  406. var next = function (array, item) {
  407. var idx = indexOf(array, item);
  408. if (idx === -1) { return null; }
  409. return array[idx + 1];
  410. };
  411. /**
  412. * returns prev item.
  413. * @param {Array} array
  414. */
  415. var prev = function (array, item) {
  416. var idx = indexOf(array, item);
  417. if (idx === -1) { return null; }
  418. return array[idx - 1];
  419. };
  420. return { head: head, last: last, initial: initial, tail: tail,
  421. prev: prev, next: next, find: find, contains: contains,
  422. all: all, sum: sum, from: from, isEmpty: isEmpty,
  423. clusterBy: clusterBy, compact: compact, unique: unique };
  424. })();
  425. var NBSP_CHAR = String.fromCharCode(160);
  426. var ZERO_WIDTH_NBSP_CHAR = '\ufeff';
  427. /**
  428. * @class core.dom
  429. *
  430. * Dom functions
  431. *
  432. * @singleton
  433. * @alternateClassName dom
  434. */
  435. var dom = (function () {
  436. /**
  437. * @method isEditable
  438. *
  439. * returns whether node is `note-editable` or not.
  440. *
  441. * @param {Node} node
  442. * @return {Boolean}
  443. */
  444. var isEditable = function (node) {
  445. return node && $(node).hasClass('note-editable');
  446. };
  447. /**
  448. * @method isControlSizing
  449. *
  450. * returns whether node is `note-control-sizing` or not.
  451. *
  452. * @param {Node} node
  453. * @return {Boolean}
  454. */
  455. var isControlSizing = function (node) {
  456. return node && $(node).hasClass('note-control-sizing');
  457. };
  458. /**
  459. * @method makePredByNodeName
  460. *
  461. * returns predicate which judge whether nodeName is same
  462. *
  463. * @param {String} nodeName
  464. * @return {Function}
  465. */
  466. var makePredByNodeName = function (nodeName) {
  467. nodeName = nodeName.toUpperCase();
  468. return function (node) {
  469. return node && node.nodeName.toUpperCase() === nodeName;
  470. };
  471. };
  472. /**
  473. * @method isText
  474. *
  475. *
  476. *
  477. * @param {Node} node
  478. * @return {Boolean} true if node's type is text(3)
  479. */
  480. var isText = function (node) {
  481. return node && node.nodeType === 3;
  482. };
  483. /**
  484. * @method isElement
  485. *
  486. *
  487. *
  488. * @param {Node} node
  489. * @return {Boolean} true if node's type is element(1)
  490. */
  491. var isElement = function (node) {
  492. return node && node.nodeType === 1;
  493. };
  494. /**
  495. * ex) br, col, embed, hr, img, input, ...
  496. * @see http://www.w3.org/html/wg/drafts/html/master/syntax.html#void-elements
  497. */
  498. var isVoid = function (node) {
  499. return node && /^BR|^IMG|^HR|^IFRAME|^BUTTON/.test(node.nodeName.toUpperCase());
  500. };
  501. var isPara = function (node) {
  502. if (isEditable(node)) {
  503. return false;
  504. }
  505. // Chrome(v31.0), FF(v25.0.1) use DIV for paragraph
  506. return node && /^DIV|^P|^LI|^H[1-7]/.test(node.nodeName.toUpperCase());
  507. };
  508. var isHeading = function (node) {
  509. return node && /^H[1-7]/.test(node.nodeName.toUpperCase());
  510. };
  511. var isPre = makePredByNodeName('PRE');
  512. var isLi = makePredByNodeName('LI');
  513. var isPurePara = function (node) {
  514. return isPara(node) && !isLi(node);
  515. };
  516. var isTable = makePredByNodeName('TABLE');
  517. var isData = makePredByNodeName('DATA');
  518. var isInline = function (node) {
  519. return !isBodyContainer(node) &&
  520. !isList(node) &&
  521. !isHr(node) &&
  522. !isPara(node) &&
  523. !isTable(node) &&
  524. !isBlockquote(node) &&
  525. !isData(node);
  526. };
  527. var isList = function (node) {
  528. return node && /^UL|^OL/.test(node.nodeName.toUpperCase());
  529. };
  530. var isHr = makePredByNodeName('HR');
  531. var isCell = function (node) {
  532. return node && /^TD|^TH/.test(node.nodeName.toUpperCase());
  533. };
  534. var isBlockquote = makePredByNodeName('BLOCKQUOTE');
  535. var isBodyContainer = function (node) {
  536. return isCell(node) || isBlockquote(node) || isEditable(node);
  537. };
  538. var isAnchor = makePredByNodeName('A');
  539. var isParaInline = function (node) {
  540. return isInline(node) && !!ancestor(node, isPara);
  541. };
  542. var isBodyInline = function (node) {
  543. return isInline(node) && !ancestor(node, isPara);
  544. };
  545. var isBody = makePredByNodeName('BODY');
  546. /**
  547. * returns whether nodeB is closest sibling of nodeA
  548. *
  549. * @param {Node} nodeA
  550. * @param {Node} nodeB
  551. * @return {Boolean}
  552. */
  553. var isClosestSibling = function (nodeA, nodeB) {
  554. return nodeA.nextSibling === nodeB ||
  555. nodeA.previousSibling === nodeB;
  556. };
  557. /**
  558. * returns array of closest siblings with node
  559. *
  560. * @param {Node} node
  561. * @param {function} [pred] - predicate function
  562. * @return {Node[]}
  563. */
  564. var withClosestSiblings = function (node, pred) {
  565. pred = pred || func.ok;
  566. var siblings = [];
  567. if (node.previousSibling && pred(node.previousSibling)) {
  568. siblings.push(node.previousSibling);
  569. }
  570. siblings.push(node);
  571. if (node.nextSibling && pred(node.nextSibling)) {
  572. siblings.push(node.nextSibling);
  573. }
  574. return siblings;
  575. };
  576. /**
  577. * blank HTML for cursor position
  578. * - [workaround] old IE only works with &nbsp;
  579. * - [workaround] IE11 and other browser works with bogus br
  580. */
  581. var blankHTML = agent.isMSIE && agent.browserVersion < 11 ? '&nbsp;' : '<br>';
  582. /**
  583. * @method nodeLength
  584. *
  585. * returns #text's text size or element's childNodes size
  586. *
  587. * @param {Node} node
  588. */
  589. var nodeLength = function (node) {
  590. if (isText(node)) {
  591. return node.nodeValue.length;
  592. }
  593. if (node) {
  594. return node.childNodes.length;
  595. }
  596. return 0;
  597. };
  598. /**
  599. * returns whether node is empty or not.
  600. *
  601. * @param {Node} node
  602. * @return {Boolean}
  603. */
  604. var isEmpty = function (node) {
  605. var len = nodeLength(node);
  606. if (len === 0) {
  607. return true;
  608. } else if (!isText(node) && len === 1 && node.innerHTML === blankHTML) {
  609. // ex) <p><br></p>, <span><br></span>
  610. return true;
  611. } else if (list.all(node.childNodes, isText) && node.innerHTML === '') {
  612. // ex) <p></p>, <span></span>
  613. return true;
  614. }
  615. return false;
  616. };
  617. /**
  618. * padding blankHTML if node is empty (for cursor position)
  619. */
  620. var paddingBlankHTML = function (node) {
  621. if (!isVoid(node) && !nodeLength(node)) {
  622. node.innerHTML = blankHTML;
  623. }
  624. };
  625. /**
  626. * find nearest ancestor predicate hit
  627. *
  628. * @param {Node} node
  629. * @param {Function} pred - predicate function
  630. */
  631. var ancestor = function (node, pred) {
  632. while (node) {
  633. if (pred(node)) { return node; }
  634. if (isEditable(node)) { break; }
  635. node = node.parentNode;
  636. }
  637. return null;
  638. };
  639. /**
  640. * find nearest ancestor only single child blood line and predicate hit
  641. *
  642. * @param {Node} node
  643. * @param {Function} pred - predicate function
  644. */
  645. var singleChildAncestor = function (node, pred) {
  646. node = node.parentNode;
  647. while (node) {
  648. if (nodeLength(node) !== 1) { break; }
  649. if (pred(node)) { return node; }
  650. if (isEditable(node)) { break; }
  651. node = node.parentNode;
  652. }
  653. return null;
  654. };
  655. /**
  656. * returns new array of ancestor nodes (until predicate hit).
  657. *
  658. * @param {Node} node
  659. * @param {Function} [optional] pred - predicate function
  660. */
  661. var listAncestor = function (node, pred) {
  662. pred = pred || func.fail;
  663. var ancestors = [];
  664. ancestor(node, function (el) {
  665. if (!isEditable(el)) {
  666. ancestors.push(el);
  667. }
  668. return pred(el);
  669. });
  670. return ancestors;
  671. };
  672. /**
  673. * find farthest ancestor predicate hit
  674. */
  675. var lastAncestor = function (node, pred) {
  676. var ancestors = listAncestor(node);
  677. return list.last(ancestors.filter(pred));
  678. };
  679. /**
  680. * returns common ancestor node between two nodes.
  681. *
  682. * @param {Node} nodeA
  683. * @param {Node} nodeB
  684. */
  685. var commonAncestor = function (nodeA, nodeB) {
  686. var ancestors = listAncestor(nodeA);
  687. for (var n = nodeB; n; n = n.parentNode) {
  688. if ($.inArray(n, ancestors) > -1) { return n; }
  689. }
  690. return null; // difference document area
  691. };
  692. /**
  693. * listing all previous siblings (until predicate hit).
  694. *
  695. * @param {Node} node
  696. * @param {Function} [optional] pred - predicate function
  697. */
  698. var listPrev = function (node, pred) {
  699. pred = pred || func.fail;
  700. var nodes = [];
  701. while (node) {
  702. if (pred(node)) { break; }
  703. nodes.push(node);
  704. node = node.previousSibling;
  705. }
  706. return nodes;
  707. };
  708. /**
  709. * listing next siblings (until predicate hit).
  710. *
  711. * @param {Node} node
  712. * @param {Function} [pred] - predicate function
  713. */
  714. var listNext = function (node, pred) {
  715. pred = pred || func.fail;
  716. var nodes = [];
  717. while (node) {
  718. if (pred(node)) { break; }
  719. nodes.push(node);
  720. node = node.nextSibling;
  721. }
  722. return nodes;
  723. };
  724. /**
  725. * listing descendant nodes
  726. *
  727. * @param {Node} node
  728. * @param {Function} [pred] - predicate function
  729. */
  730. var listDescendant = function (node, pred) {
  731. var descendants = [];
  732. pred = pred || func.ok;
  733. // start DFS(depth first search) with node
  734. (function fnWalk(current) {
  735. if (node !== current && pred(current)) {
  736. descendants.push(current);
  737. }
  738. for (var idx = 0, len = current.childNodes.length; idx < len; idx++) {
  739. fnWalk(current.childNodes[idx]);
  740. }
  741. })(node);
  742. return descendants;
  743. };
  744. /**
  745. * wrap node with new tag.
  746. *
  747. * @param {Node} node
  748. * @param {Node} tagName of wrapper
  749. * @return {Node} - wrapper
  750. */
  751. var wrap = function (node, wrapperName) {
  752. var parent = node.parentNode;
  753. var wrapper = $('<' + wrapperName + '>')[0];
  754. parent.insertBefore(wrapper, node);
  755. wrapper.appendChild(node);
  756. return wrapper;
  757. };
  758. /**
  759. * insert node after preceding
  760. *
  761. * @param {Node} node
  762. * @param {Node} preceding - predicate function
  763. */
  764. var insertAfter = function (node, preceding) {
  765. var next = preceding.nextSibling, parent = preceding.parentNode;
  766. if (next) {
  767. parent.insertBefore(node, next);
  768. } else {
  769. parent.appendChild(node);
  770. }
  771. return node;
  772. };
  773. /**
  774. * append elements.
  775. *
  776. * @param {Node} node
  777. * @param {Collection} aChild
  778. */
  779. var appendChildNodes = function (node, aChild) {
  780. $.each(aChild, function (idx, child) {
  781. node.appendChild(child);
  782. });
  783. return node;
  784. };
  785. /**
  786. * returns whether boundaryPoint is left edge or not.
  787. *
  788. * @param {BoundaryPoint} point
  789. * @return {Boolean}
  790. */
  791. var isLeftEdgePoint = function (point) {
  792. return point.offset === 0;
  793. };
  794. /**
  795. * returns whether boundaryPoint is right edge or not.
  796. *
  797. * @param {BoundaryPoint} point
  798. * @return {Boolean}
  799. */
  800. var isRightEdgePoint = function (point) {
  801. return point.offset === nodeLength(point.node);
  802. };
  803. /**
  804. * returns whether boundaryPoint is edge or not.
  805. *
  806. * @param {BoundaryPoint} point
  807. * @return {Boolean}
  808. */
  809. var isEdgePoint = function (point) {
  810. return isLeftEdgePoint(point) || isRightEdgePoint(point);
  811. };
  812. /**
  813. * returns whether node is left edge of ancestor or not.
  814. *
  815. * @param {Node} node
  816. * @param {Node} ancestor
  817. * @return {Boolean}
  818. */
  819. var isLeftEdgeOf = function (node, ancestor) {
  820. while (node && node !== ancestor) {
  821. if (position(node) !== 0) {
  822. return false;
  823. }
  824. node = node.parentNode;
  825. }
  826. return true;
  827. };
  828. /**
  829. * returns whether node is right edge of ancestor or not.
  830. *
  831. * @param {Node} node
  832. * @param {Node} ancestor
  833. * @return {Boolean}
  834. */
  835. var isRightEdgeOf = function (node, ancestor) {
  836. if (!ancestor) {
  837. return false;
  838. }
  839. while (node && node !== ancestor) {
  840. if (position(node) !== nodeLength(node.parentNode) - 1) {
  841. return false;
  842. }
  843. node = node.parentNode;
  844. }
  845. return true;
  846. };
  847. /**
  848. * returns whether point is left edge of ancestor or not.
  849. * @param {BoundaryPoint} point
  850. * @param {Node} ancestor
  851. * @return {Boolean}
  852. */
  853. var isLeftEdgePointOf = function (point, ancestor) {
  854. return isLeftEdgePoint(point) && isLeftEdgeOf(point.node, ancestor);
  855. };
  856. /**
  857. * returns whether point is right edge of ancestor or not.
  858. * @param {BoundaryPoint} point
  859. * @param {Node} ancestor
  860. * @return {Boolean}
  861. */
  862. var isRightEdgePointOf = function (point, ancestor) {
  863. return isRightEdgePoint(point) && isRightEdgeOf(point.node, ancestor);
  864. };
  865. /**
  866. * returns offset from parent.
  867. *
  868. * @param {Node} node
  869. */
  870. var position = function (node) {
  871. var offset = 0;
  872. while ((node = node.previousSibling)) {
  873. offset += 1;
  874. }
  875. return offset;
  876. };
  877. var hasChildren = function (node) {
  878. return !!(node && node.childNodes && node.childNodes.length);
  879. };
  880. /**
  881. * returns previous boundaryPoint
  882. *
  883. * @param {BoundaryPoint} point
  884. * @param {Boolean} isSkipInnerOffset
  885. * @return {BoundaryPoint}
  886. */
  887. var prevPoint = function (point, isSkipInnerOffset) {
  888. var node, offset;
  889. if (point.offset === 0) {
  890. if (isEditable(point.node)) {
  891. return null;
  892. }
  893. node = point.node.parentNode;
  894. offset = position(point.node);
  895. } else if (hasChildren(point.node)) {
  896. node = point.node.childNodes[point.offset - 1];
  897. offset = nodeLength(node);
  898. } else {
  899. node = point.node;
  900. offset = isSkipInnerOffset ? 0 : point.offset - 1;
  901. }
  902. return {
  903. node: node,
  904. offset: offset
  905. };
  906. };
  907. /**
  908. * returns next boundaryPoint
  909. *
  910. * @param {BoundaryPoint} point
  911. * @param {Boolean} isSkipInnerOffset
  912. * @return {BoundaryPoint}
  913. */
  914. var nextPoint = function (point, isSkipInnerOffset) {
  915. var node, offset;
  916. if (nodeLength(point.node) === point.offset) {
  917. if (isEditable(point.node)) {
  918. return null;
  919. }
  920. node = point.node.parentNode;
  921. offset = position(point.node) + 1;
  922. } else if (hasChildren(point.node)) {
  923. node = point.node.childNodes[point.offset];
  924. offset = 0;
  925. } else {
  926. node = point.node;
  927. offset = isSkipInnerOffset ? nodeLength(point.node) : point.offset + 1;
  928. }
  929. return {
  930. node: node,
  931. offset: offset
  932. };
  933. };
  934. /**
  935. * returns whether pointA and pointB is same or not.
  936. *
  937. * @param {BoundaryPoint} pointA
  938. * @param {BoundaryPoint} pointB
  939. * @return {Boolean}
  940. */
  941. var isSamePoint = function (pointA, pointB) {
  942. return pointA.node === pointB.node && pointA.offset === pointB.offset;
  943. };
  944. /**
  945. * returns whether point is visible (can set cursor) or not.
  946. *
  947. * @param {BoundaryPoint} point
  948. * @return {Boolean}
  949. */
  950. var isVisiblePoint = function (point) {
  951. if (isText(point.node) || !hasChildren(point.node) || isEmpty(point.node)) {
  952. return true;
  953. }
  954. var leftNode = point.node.childNodes[point.offset - 1];
  955. var rightNode = point.node.childNodes[point.offset];
  956. if ((!leftNode || isVoid(leftNode)) && (!rightNode || isVoid(rightNode))) {
  957. return true;
  958. }
  959. return false;
  960. };
  961. /**
  962. * @method prevPointUtil
  963. *
  964. * @param {BoundaryPoint} point
  965. * @param {Function} pred
  966. * @return {BoundaryPoint}
  967. */
  968. var prevPointUntil = function (point, pred) {
  969. while (point) {
  970. if (pred(point)) {
  971. return point;
  972. }
  973. point = prevPoint(point);
  974. }
  975. return null;
  976. };
  977. /**
  978. * @method nextPointUntil
  979. *
  980. * @param {BoundaryPoint} point
  981. * @param {Function} pred
  982. * @return {BoundaryPoint}
  983. */
  984. var nextPointUntil = function (point, pred) {
  985. while (point) {
  986. if (pred(point)) {
  987. return point;
  988. }
  989. point = nextPoint(point);
  990. }
  991. return null;
  992. };
  993. /**
  994. * returns whether point has character or not.
  995. *
  996. * @param {Point} point
  997. * @return {Boolean}
  998. */
  999. var isCharPoint = function (point) {
  1000. if (!isText(point.node)) {
  1001. return false;
  1002. }
  1003. var ch = point.node.nodeValue.charAt(point.offset - 1);
  1004. return ch && (ch !== ' ' && ch !== NBSP_CHAR);
  1005. };
  1006. /**
  1007. * @method walkPoint
  1008. *
  1009. * @param {BoundaryPoint} startPoint
  1010. * @param {BoundaryPoint} endPoint
  1011. * @param {Function} handler
  1012. * @param {Boolean} isSkipInnerOffset
  1013. */
  1014. var walkPoint = function (startPoint, endPoint, handler, isSkipInnerOffset) {
  1015. var point = startPoint;
  1016. while (point) {
  1017. handler(point);
  1018. if (isSamePoint(point, endPoint)) {
  1019. break;
  1020. }
  1021. var isSkipOffset = isSkipInnerOffset &&
  1022. startPoint.node !== point.node &&
  1023. endPoint.node !== point.node;
  1024. point = nextPoint(point, isSkipOffset);
  1025. }
  1026. };
  1027. /**
  1028. * @method makeOffsetPath
  1029. *
  1030. * return offsetPath(array of offset) from ancestor
  1031. *
  1032. * @param {Node} ancestor - ancestor node
  1033. * @param {Node} node
  1034. */
  1035. var makeOffsetPath = function (ancestor, node) {
  1036. var ancestors = listAncestor(node, func.eq(ancestor));
  1037. return ancestors.map(position).reverse();
  1038. };
  1039. /**
  1040. * @method fromOffsetPath
  1041. *
  1042. * return element from offsetPath(array of offset)
  1043. *
  1044. * @param {Node} ancestor - ancestor node
  1045. * @param {array} offsets - offsetPath
  1046. */
  1047. var fromOffsetPath = function (ancestor, offsets) {
  1048. var current = ancestor;
  1049. for (var i = 0, len = offsets.length; i < len; i++) {
  1050. if (current.childNodes.length <= offsets[i]) {
  1051. current = current.childNodes[current.childNodes.length - 1];
  1052. } else {
  1053. current = current.childNodes[offsets[i]];
  1054. }
  1055. }
  1056. return current;
  1057. };
  1058. /**
  1059. * @method splitNode
  1060. *
  1061. * split element or #text
  1062. *
  1063. * @param {BoundaryPoint} point
  1064. * @param {Object} [options]
  1065. * @param {Boolean} [options.isSkipPaddingBlankHTML] - default: false
  1066. * @param {Boolean} [options.isNotSplitEdgePoint] - default: false
  1067. * @return {Node} right node of boundaryPoint
  1068. */
  1069. var splitNode = function (point, options) {
  1070. var isSkipPaddingBlankHTML = options && options.isSkipPaddingBlankHTML;
  1071. var isNotSplitEdgePoint = options && options.isNotSplitEdgePoint;
  1072. // edge case
  1073. if (isEdgePoint(point) && (isText(point.node) || isNotSplitEdgePoint)) {
  1074. if (isLeftEdgePoint(point)) {
  1075. return point.node;
  1076. } else if (isRightEdgePoint(point)) {
  1077. return point.node.nextSibling;
  1078. }
  1079. }
  1080. // split #text
  1081. if (isText(point.node)) {
  1082. return point.node.splitText(point.offset);
  1083. } else {
  1084. var childNode = point.node.childNodes[point.offset];
  1085. var clone = insertAfter(point.node.cloneNode(false), point.node);
  1086. appendChildNodes(clone, listNext(childNode));
  1087. if (!isSkipPaddingBlankHTML) {
  1088. paddingBlankHTML(point.node);
  1089. paddingBlankHTML(clone);
  1090. }
  1091. return clone;
  1092. }
  1093. };
  1094. /**
  1095. * @method splitTree
  1096. *
  1097. * split tree by point
  1098. *
  1099. * @param {Node} root - split root
  1100. * @param {BoundaryPoint} point
  1101. * @param {Object} [options]
  1102. * @param {Boolean} [options.isSkipPaddingBlankHTML] - default: false
  1103. * @param {Boolean} [options.isNotSplitEdgePoint] - default: false
  1104. * @return {Node} right node of boundaryPoint
  1105. */
  1106. var splitTree = function (root, point, options) {
  1107. // ex) [#text, <span>, <p>]
  1108. var ancestors = listAncestor(point.node, func.eq(root));
  1109. if (!ancestors.length) {
  1110. return null;
  1111. } else if (ancestors.length === 1) {
  1112. return splitNode(point, options);
  1113. }
  1114. return ancestors.reduce(function (node, parent) {
  1115. if (node === point.node) {
  1116. node = splitNode(point, options);
  1117. }
  1118. return splitNode({
  1119. node: parent,
  1120. offset: node ? dom.position(node) : nodeLength(parent)
  1121. }, options);
  1122. });
  1123. };
  1124. /**
  1125. * split point
  1126. *
  1127. * @param {Point} point
  1128. * @param {Boolean} isInline
  1129. * @return {Object}
  1130. */
  1131. var splitPoint = function (point, isInline) {
  1132. // find splitRoot, container
  1133. // - inline: splitRoot is a child of paragraph
  1134. // - block: splitRoot is a child of bodyContainer
  1135. var pred = isInline ? isPara : isBodyContainer;
  1136. var ancestors = listAncestor(point.node, pred);
  1137. var topAncestor = list.last(ancestors) || point.node;
  1138. var splitRoot, container;
  1139. if (pred(topAncestor)) {
  1140. splitRoot = ancestors[ancestors.length - 2];
  1141. container = topAncestor;
  1142. } else {
  1143. splitRoot = topAncestor;
  1144. container = splitRoot.parentNode;
  1145. }
  1146. // if splitRoot is exists, split with splitTree
  1147. var pivot = splitRoot && splitTree(splitRoot, point, {
  1148. isSkipPaddingBlankHTML: isInline,
  1149. isNotSplitEdgePoint: isInline
  1150. });
  1151. // if container is point.node, find pivot with point.offset
  1152. if (!pivot && container === point.node) {
  1153. pivot = point.node.childNodes[point.offset];
  1154. }
  1155. return {
  1156. rightNode: pivot,
  1157. container: container
  1158. };
  1159. };
  1160. var create = function (nodeName) {
  1161. return document.createElement(nodeName);
  1162. };
  1163. var createText = function (text) {
  1164. return document.createTextNode(text);
  1165. };
  1166. /**
  1167. * @method remove
  1168. *
  1169. * remove node, (isRemoveChild: remove child or not)
  1170. *
  1171. * @param {Node} node
  1172. * @param {Boolean} isRemoveChild
  1173. */
  1174. var remove = function (node, isRemoveChild) {
  1175. if (!node || !node.parentNode) { return; }
  1176. if (node.removeNode) { return node.removeNode(isRemoveChild); }
  1177. var parent = node.parentNode;
  1178. if (!isRemoveChild) {
  1179. var nodes = [];
  1180. var i, len;
  1181. for (i = 0, len = node.childNodes.length; i < len; i++) {
  1182. nodes.push(node.childNodes[i]);
  1183. }
  1184. for (i = 0, len = nodes.length; i < len; i++) {
  1185. parent.insertBefore(nodes[i], node);
  1186. }
  1187. }
  1188. parent.removeChild(node);
  1189. };
  1190. /**
  1191. * @method removeWhile
  1192. *
  1193. * @param {Node} node
  1194. * @param {Function} pred
  1195. */
  1196. var removeWhile = function (node, pred) {
  1197. while (node) {
  1198. if (isEditable(node) || !pred(node)) {
  1199. break;
  1200. }
  1201. var parent = node.parentNode;
  1202. remove(node);
  1203. node = parent;
  1204. }
  1205. };
  1206. /**
  1207. * @method replace
  1208. *
  1209. * replace node with provided nodeName
  1210. *
  1211. * @param {Node} node
  1212. * @param {String} nodeName
  1213. * @return {Node} - new node
  1214. */
  1215. var replace = function (node, nodeName) {
  1216. if (node.nodeName.toUpperCase() === nodeName.toUpperCase()) {
  1217. return node;
  1218. }
  1219. var newNode = create(nodeName);
  1220. if (node.style.cssText) {
  1221. newNode.style.cssText = node.style.cssText;
  1222. }
  1223. appendChildNodes(newNode, list.from(node.childNodes));
  1224. insertAfter(newNode, node);
  1225. remove(node);
  1226. return newNode;
  1227. };
  1228. var isTextarea = makePredByNodeName('TEXTAREA');
  1229. /**
  1230. * @param {jQuery} $node
  1231. * @param {Boolean} [stripLinebreaks] - default: false
  1232. */
  1233. var value = function ($node, stripLinebreaks) {
  1234. var val = isTextarea($node[0]) ? $node.val() : $node.html();
  1235. if (stripLinebreaks) {
  1236. return val.replace(/[\n\r]/g, '');
  1237. }
  1238. return val;
  1239. };
  1240. /**
  1241. * @method html
  1242. *
  1243. * get the HTML contents of node
  1244. *
  1245. * @param {jQuery} $node
  1246. * @param {Boolean} [isNewlineOnBlock]
  1247. */
  1248. var html = function ($node, isNewlineOnBlock) {
  1249. var markup = value($node);
  1250. if (isNewlineOnBlock) {
  1251. var regexTag = /<(\/?)(\b(?!!)[^>\s]*)(.*?)(\s*\/?>)/g;
  1252. markup = markup.replace(regexTag, function (match, endSlash, name) {
  1253. name = name.toUpperCase();
  1254. var isEndOfInlineContainer = /^DIV|^TD|^TH|^P|^LI|^H[1-7]/.test(name) &&
  1255. !!endSlash;
  1256. var isBlockNode = /^BLOCKQUOTE|^TABLE|^TBODY|^TR|^HR|^UL|^OL/.test(name);
  1257. return match + ((isEndOfInlineContainer || isBlockNode) ? '\n' : '');
  1258. });
  1259. markup = $.trim(markup);
  1260. }
  1261. return markup;
  1262. };
  1263. var posFromPlaceholder = function (placeholder) {
  1264. var $placeholder = $(placeholder);
  1265. var pos = $placeholder.offset();
  1266. var height = $placeholder.outerHeight(true); // include margin
  1267. return {
  1268. left: pos.left,
  1269. top: pos.top + height
  1270. };
  1271. };
  1272. var attachEvents = function ($node, events) {
  1273. Object.keys(events).forEach(function (key) {
  1274. $node.on(key, events[key]);
  1275. });
  1276. };
  1277. var detachEvents = function ($node, events) {
  1278. Object.keys(events).forEach(function (key) {
  1279. $node.off(key, events[key]);
  1280. });
  1281. };
  1282. /**
  1283. * @method isCustomStyleTag
  1284. *
  1285. * assert if a node contains a "note-styletag" class,
  1286. * which implies that's a custom-made style tag node
  1287. *
  1288. * @param {Node} an HTML DOM node
  1289. */
  1290. var isCustomStyleTag = function (node) {
  1291. return node && !dom.isText(node) && list.contains(node.classList, 'note-styletag');
  1292. };
  1293. return {
  1294. /** @property {String} NBSP_CHAR */
  1295. NBSP_CHAR: NBSP_CHAR,
  1296. /** @property {String} ZERO_WIDTH_NBSP_CHAR */
  1297. ZERO_WIDTH_NBSP_CHAR: ZERO_WIDTH_NBSP_CHAR,
  1298. /** @property {String} blank */
  1299. blank: blankHTML,
  1300. /** @property {String} emptyPara */
  1301. emptyPara: '<p>' + blankHTML + '</p>',
  1302. makePredByNodeName: makePredByNodeName,
  1303. isEditable: isEditable,
  1304. isControlSizing: isControlSizing,
  1305. isText: isText,
  1306. isElement: isElement,
  1307. isVoid: isVoid,
  1308. isPara: isPara,
  1309. isPurePara: isPurePara,
  1310. isHeading: isHeading,
  1311. isInline: isInline,
  1312. isBlock: func.not(isInline),
  1313. isBodyInline: isBodyInline,
  1314. isBody: isBody,
  1315. isParaInline: isParaInline,
  1316. isPre: isPre,
  1317. isList: isList,
  1318. isTable: isTable,
  1319. isData: isData,
  1320. isCell: isCell,
  1321. isBlockquote: isBlockquote,
  1322. isBodyContainer: isBodyContainer,
  1323. isAnchor: isAnchor,
  1324. isDiv: makePredByNodeName('DIV'),
  1325. isLi: isLi,
  1326. isBR: makePredByNodeName('BR'),
  1327. isSpan: makePredByNodeName('SPAN'),
  1328. isB: makePredByNodeName('B'),
  1329. isU: makePredByNodeName('U'),
  1330. isS: makePredByNodeName('S'),
  1331. isI: makePredByNodeName('I'),
  1332. isImg: makePredByNodeName('IMG'),
  1333. isTextarea: isTextarea,
  1334. isEmpty: isEmpty,
  1335. isEmptyAnchor: func.and(isAnchor, isEmpty),
  1336. isClosestSibling: isClosestSibling,
  1337. withClosestSiblings: withClosestSiblings,
  1338. nodeLength: nodeLength,
  1339. isLeftEdgePoint: isLeftEdgePoint,
  1340. isRightEdgePoint: isRightEdgePoint,
  1341. isEdgePoint: isEdgePoint,
  1342. isLeftEdgeOf: isLeftEdgeOf,
  1343. isRightEdgeOf: isRightEdgeOf,
  1344. isLeftEdgePointOf: isLeftEdgePointOf,
  1345. isRightEdgePointOf: isRightEdgePointOf,
  1346. prevPoint: prevPoint,
  1347. nextPoint: nextPoint,
  1348. isSamePoint: isSamePoint,
  1349. isVisiblePoint: isVisiblePoint,
  1350. prevPointUntil: prevPointUntil,
  1351. nextPointUntil: nextPointUntil,
  1352. isCharPoint: isCharPoint,
  1353. walkPoint: walkPoint,
  1354. ancestor: ancestor,
  1355. singleChildAncestor: singleChildAncestor,
  1356. listAncestor: listAncestor,
  1357. lastAncestor: lastAncestor,
  1358. listNext: listNext,
  1359. listPrev: listPrev,
  1360. listDescendant: listDescendant,
  1361. commonAncestor: commonAncestor,
  1362. wrap: wrap,
  1363. insertAfter: insertAfter,
  1364. appendChildNodes: appendChildNodes,
  1365. position: position,
  1366. hasChildren: hasChildren,
  1367. makeOffsetPath: makeOffsetPath,
  1368. fromOffsetPath: fromOffsetPath,
  1369. splitTree: splitTree,
  1370. splitPoint: splitPoint,
  1371. create: create,
  1372. createText: createText,
  1373. remove: remove,
  1374. removeWhile: removeWhile,
  1375. replace: replace,
  1376. html: html,
  1377. value: value,
  1378. posFromPlaceholder: posFromPlaceholder,
  1379. attachEvents: attachEvents,
  1380. detachEvents: detachEvents,
  1381. isCustomStyleTag: isCustomStyleTag
  1382. };
  1383. })();
  1384. /**
  1385. * @param {jQuery} $note
  1386. * @param {Object} options
  1387. * @return {Context}
  1388. */
  1389. var Context = function ($note, options) {
  1390. var self = this;
  1391. var ui = $.summernote.ui;
  1392. this.memos = {};
  1393. this.modules = {};
  1394. this.layoutInfo = {};
  1395. this.options = options;
  1396. /**
  1397. * create layout and initialize modules and other resources
  1398. */
  1399. this.initialize = function () {
  1400. this.layoutInfo = ui.createLayout($note, options);
  1401. this._initialize();
  1402. $note.hide();
  1403. return this;
  1404. };
  1405. /**
  1406. * destroy modules and other resources and remove layout
  1407. */
  1408. this.destroy = function () {
  1409. this._destroy();
  1410. $note.removeData('summernote');
  1411. ui.removeLayout($note, this.layoutInfo);
  1412. };
  1413. /**
  1414. * destory modules and other resources and initialize it again
  1415. */
  1416. this.reset = function () {
  1417. var disabled = self.isDisabled();
  1418. this.code(dom.emptyPara);
  1419. this._destroy();
  1420. this._initialize();
  1421. if (disabled) {
  1422. self.disable();
  1423. }
  1424. };
  1425. this._initialize = function () {
  1426. // add optional buttons
  1427. var buttons = $.extend({}, this.options.buttons);
  1428. Object.keys(buttons).forEach(function (key) {
  1429. self.memo('button.' + key, buttons[key]);
  1430. });
  1431. var modules = $.extend({}, this.options.modules, $.summernote.plugins || {});
  1432. // add and initialize modules
  1433. Object.keys(modules).forEach(function (key) {
  1434. self.module(key, modules[key], true);
  1435. });
  1436. Object.keys(this.modules).forEach(function (key) {
  1437. self.initializeModule(key);
  1438. });
  1439. };
  1440. this._destroy = function () {
  1441. // destroy modules with reversed order
  1442. Object.keys(this.modules).reverse().forEach(function (key) {
  1443. self.removeModule(key);
  1444. });
  1445. Object.keys(this.memos).forEach(function (key) {
  1446. self.removeMemo(key);
  1447. });
  1448. // trigger custom onDestroy callback
  1449. this.triggerEvent('destroy', this);
  1450. };
  1451. this.code = function (html) {
  1452. var isActivated = this.invoke('codeview.isActivated');
  1453. if (html === undefined) {
  1454. this.invoke('codeview.sync');
  1455. return isActivated ? this.layoutInfo.codable.val() : this.layoutInfo.editable.html();
  1456. } else {
  1457. if (isActivated) {
  1458. this.layoutInfo.codable.val(html);
  1459. } else {
  1460. this.layoutInfo.editable.html(html);
  1461. }
  1462. $note.val(html);
  1463. this.triggerEvent('change', html);
  1464. }
  1465. };
  1466. this.isDisabled = function () {
  1467. return this.layoutInfo.editable.attr('contenteditable') === 'false';
  1468. };
  1469. this.enable = function () {
  1470. this.layoutInfo.editable.attr('contenteditable', true);
  1471. this.invoke('toolbar.activate', true);
  1472. };
  1473. this.disable = function () {
  1474. // close codeview if codeview is opend
  1475. if (this.invoke('codeview.isActivated')) {
  1476. this.invoke('codeview.deactivate');
  1477. }
  1478. this.layoutInfo.editable.attr('contenteditable', false);
  1479. this.invoke('toolbar.deactivate', true);
  1480. };
  1481. this.triggerEvent = function () {
  1482. var namespace = list.head(arguments);
  1483. var args = list.tail(list.from(arguments));
  1484. var callback = this.options.callbacks[func.namespaceToCamel(namespace, 'on')];
  1485. if (callback) {
  1486. callback.apply($note[0], args);
  1487. }
  1488. $note.trigger('summernote.' + namespace, args);
  1489. };
  1490. this.initializeModule = function (key) {
  1491. var module = this.modules[key];
  1492. module.shouldInitialize = module.shouldInitialize || func.ok;
  1493. if (!module.shouldInitialize()) {
  1494. return;
  1495. }
  1496. // initialize module
  1497. if (module.initialize) {
  1498. module.initialize();
  1499. }
  1500. // attach events
  1501. if (module.events) {
  1502. dom.attachEvents($note, module.events);
  1503. }
  1504. };
  1505. this.module = function (key, ModuleClass, withoutIntialize) {
  1506. if (arguments.length === 1) {
  1507. return this.modules[key];
  1508. }
  1509. this.modules[key] = new ModuleClass(this);
  1510. if (!withoutIntialize) {
  1511. this.initializeModule(key);
  1512. }
  1513. };
  1514. this.removeModule = function (key) {
  1515. var module = this.modules[key];
  1516. if (module.shouldInitialize()) {
  1517. if (module.events) {
  1518. dom.detachEvents($note, module.events);
  1519. }
  1520. if (module.destroy) {
  1521. module.destroy();
  1522. }
  1523. }
  1524. delete this.modules[key];
  1525. };
  1526. this.memo = function (key, obj) {
  1527. if (arguments.length === 1) {
  1528. return this.memos[key];
  1529. }
  1530. this.memos[key] = obj;
  1531. };
  1532. this.removeMemo = function (key) {
  1533. if (this.memos[key] && this.memos[key].destroy) {
  1534. this.memos[key].destroy();
  1535. }
  1536. delete this.memos[key];
  1537. };
  1538. /**
  1539. *Some buttons need to change their visual style immediately once they get pressed
  1540. */
  1541. this.createInvokeHandlerAndUpdateState = function (namespace, value) {
  1542. return function (event) {
  1543. self.createInvokeHandler(namespace, value)(event);
  1544. self.invoke('buttons.updateCurrentStyle');
  1545. };
  1546. };
  1547. this.createInvokeHandler = function (namespace, value) {
  1548. return function (event) {
  1549. event.preventDefault();
  1550. var $target = $(event.target);
  1551. self.invoke(namespace, value || $target.closest('[data-value]').data('value'), $target);
  1552. };
  1553. };
  1554. this.invoke = function () {
  1555. var namespace = list.head(arguments);
  1556. var args = list.tail(list.from(arguments));
  1557. var splits = namespace.split('.');
  1558. var hasSeparator = splits.length > 1;
  1559. var moduleName = hasSeparator && list.head(splits);
  1560. var methodName = hasSeparator ? list.last(splits) : list.head(splits);
  1561. var module = this.modules[moduleName || 'editor'];
  1562. if (!moduleName && this[methodName]) {
  1563. return this[methodName].apply(this, args);
  1564. } else if (module && module[methodName] && module.shouldInitialize()) {
  1565. return module[methodName].apply(module, args);
  1566. }
  1567. };
  1568. return this.initialize();
  1569. };
  1570. $.fn.extend({
  1571. /**
  1572. * Summernote API
  1573. *
  1574. * @param {Object|String}
  1575. * @return {this}
  1576. */
  1577. summernote: function () {
  1578. var type = $.type(list.head(arguments));
  1579. var isExternalAPICalled = type === 'string';
  1580. var hasInitOptions = type === 'object';
  1581. var options = hasInitOptions ? list.head(arguments) : {};
  1582. options = $.extend({}, $.summernote.options, options);
  1583. // Update options
  1584. options.langInfo = $.extend(true, {}, $.summernote.lang['en-US'], $.summernote.lang[options.lang]);
  1585. options.icons = $.extend(true, {}, $.summernote.options.icons, options.icons);
  1586. options.tooltip = options.tooltip === 'auto' ? !agent.isSupportTouch : options.tooltip;
  1587. this.each(function (idx, note) {
  1588. var $note = $(note);
  1589. if (!$note.data('summernote')) {
  1590. var context = new Context($note, options);
  1591. $note.data('summernote', context);
  1592. $note.data('summernote').triggerEvent('init', context.layoutInfo);
  1593. }
  1594. });
  1595. var $note = this.first();
  1596. if ($note.length) {
  1597. var context = $note.data('summernote');
  1598. if (isExternalAPICalled) {
  1599. return context.invoke.apply(context, list.from(arguments));
  1600. } else if (options.focus) {
  1601. context.invoke('editor.focus');
  1602. }
  1603. }
  1604. return this;
  1605. }
  1606. });
  1607. var Renderer = function (markup, children, options, callback) {
  1608. this.render = function ($parent) {
  1609. var $node = $(markup);
  1610. if (options && options.contents) {
  1611. $node.html(options.contents);
  1612. }
  1613. if (options && options.className) {
  1614. $node.addClass(options.className);
  1615. }
  1616. if (options && options.data) {
  1617. $.each(options.data, function (k, v) {
  1618. $node.attr('data-' + k, v);
  1619. });
  1620. }
  1621. if (options && options.click) {
  1622. $node.on('click', options.click);
  1623. }
  1624. if (children) {
  1625. var $container = $node.find('.note-children-container');
  1626. children.forEach(function (child) {
  1627. child.render($container.length ? $container : $node);
  1628. });
  1629. }
  1630. if (callback) {
  1631. callback($node, options);
  1632. }
  1633. if (options && options.callback) {
  1634. options.callback($node);
  1635. }
  1636. if ($parent) {
  1637. $parent.append($node);
  1638. }
  1639. return $node;
  1640. };
  1641. };
  1642. var renderer = {
  1643. create: function (markup, callback) {
  1644. return function () {
  1645. var children = $.isArray(arguments[0]) ? arguments[0] : [];
  1646. var options = typeof arguments[1] === 'object' ? arguments[1] : arguments[0];
  1647. if (options && options.children) {
  1648. children = options.children;
  1649. }
  1650. return new Renderer(markup, children, options, callback);
  1651. };
  1652. }
  1653. };
  1654. var editor = renderer.create('<div class="note-editor note-frame panel panel-default"/>');
  1655. var toolbar = renderer.create('<div class="note-toolbar panel-heading"/>');
  1656. var editingArea = renderer.create('<div class="note-editing-area"/>');
  1657. var codable = renderer.create('<textarea class="note-codable"/>');
  1658. var editable = renderer.create('<div class="note-editable panel-body" contentEditable="true"/>');
  1659. var statusbar = renderer.create([
  1660. '<div class="note-statusbar">',
  1661. ' <div class="note-resizebar">',
  1662. ' <div class="note-icon-bar"/>',
  1663. ' <div class="note-icon-bar"/>',
  1664. ' <div class="note-icon-bar"/>',
  1665. ' </div>',
  1666. '</div>'
  1667. ].join(''));
  1668. var airEditor = renderer.create('<div class="note-editor"/>');
  1669. var airEditable = renderer.create('<div class="note-editable" contentEditable="true"/>');
  1670. var buttonGroup = renderer.create('<div class="note-btn-group btn-group">');
  1671. var dropdown = renderer.create('<div class="dropdown-menu">', function ($node, options) {
  1672. var markup = $.isArray(options.items) ? options.items.map(function (item) {
  1673. var value = (typeof item === 'string') ? item : (item.value || '');
  1674. var content = options.template ? options.template(item) : item;
  1675. var option = (typeof item === 'object') ? item.option : undefined;
  1676. var dataValue = 'data-value="' + value + '"';
  1677. var dataOption = (option !== undefined) ? ' data-option="' + option + '"' : '';
  1678. return '<li><a href="#" ' + (dataValue + dataOption) + '>' + content + '</a></li>';
  1679. }).join('') : options.items;
  1680. $node.html(markup);
  1681. });
  1682. var dropdownCheck = renderer.create('<div class="dropdown-menu note-check">', function ($node, options) {
  1683. var markup = $.isArray(options.items) ? options.items.map(function (item) {
  1684. var value = (typeof item === 'string') ? item : (item.value || '');
  1685. var content = options.template ? options.template(item) : item;
  1686. return '<li><a href="#" data-value="' + value + '">' + icon(options.checkClassName) + ' ' + content + '</a></li>';
  1687. }).join('') : options.items;
  1688. $node.html(markup);
  1689. });
  1690. var palette = renderer.create('<div class="note-color-palette"/>', function ($node, options) {
  1691. var contents = [];
  1692. for (var row = 0, rowSize = options.colors.length; row < rowSize; row++) {
  1693. var eventName = options.eventName;
  1694. var colors = options.colors[row];
  1695. var buttons = [];
  1696. for (var col = 0, colSize = colors.length; col < colSize; col++) {
  1697. var color = colors[col];
  1698. buttons.push([
  1699. '<button type="button" class="note-color-btn"',
  1700. 'style="background-color:', color, '" ',
  1701. 'data-event="', eventName, '" ',
  1702. 'data-value="', color, '" ',
  1703. 'title="', color, '" ',
  1704. 'data-toggle="button" tabindex="-1"></button>'
  1705. ].join(''));
  1706. }
  1707. contents.push('<div class="note-color-row">' + buttons.join('') + '</div>');
  1708. }
  1709. $node.html(contents.join(''));
  1710. $node.find('.note-color-btn').tooltip({
  1711. container: 'body',
  1712. trigger: 'hover',
  1713. placement: 'bottom'
  1714. });
  1715. });
  1716. var dialog = renderer.create('<div class="modal" aria-hidden="false" tabindex="-1"/>', function ($node, options) {
  1717. if (options.fade) {
  1718. $node.addClass('fade');
  1719. }
  1720. $node.html([
  1721. '<div class="modal-dialog">',
  1722. ' <div class="modal-content">',
  1723. (options.title ?
  1724. ' <div class="modal-header">' +
  1725. ' <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>' +
  1726. ' <h4 class="modal-title">' + options.title + '</h4>' +
  1727. ' </div>' : ''
  1728. ),
  1729. ' <div class="modal-body">' + options.body + '</div>',
  1730. (options.footer ?
  1731. ' <div class="modal-footer">' + options.footer + '</div>' : ''
  1732. ),
  1733. ' </div>',
  1734. '</div>'
  1735. ].join(''));
  1736. });
  1737. var popover = renderer.create([
  1738. '<div class="note-popover popover in">',
  1739. ' <div class="arrow"/>',
  1740. ' <div class="popover-content note-children-container"/>',
  1741. '</div>'
  1742. ].join(''), function ($node, options) {
  1743. var direction = typeof options.direction !== 'undefined' ? options.direction : 'bottom';
  1744. $node.addClass(direction);
  1745. if (options.hideArrow) {
  1746. $node.find('.arrow').hide();
  1747. }
  1748. });
  1749. var icon = function (iconClassName, tagName) {
  1750. tagName = tagName || 'i';
  1751. return '<' + tagName + ' class="' + iconClassName + '"/>';
  1752. };
  1753. var ui = {
  1754. editor: editor,
  1755. toolbar: toolbar,
  1756. editingArea: editingArea,
  1757. codable: codable,
  1758. editable: editable,
  1759. statusbar: statusbar,
  1760. airEditor: airEditor,
  1761. airEditable: airEditable,
  1762. buttonGroup: buttonGroup,
  1763. dropdown: dropdown,
  1764. dropdownCheck: dropdownCheck,
  1765. palette: palette,
  1766. dialog: dialog,
  1767. popover: popover,
  1768. icon: icon,
  1769. options: {},
  1770. button: function ($node, options) {
  1771. return renderer.create('<button type="button" class="note-btn btn btn-default btn-sm" tabindex="-1">', function ($node, options) {
  1772. if (options && options.tooltip && self.options.tooltip) {
  1773. $node.attr({
  1774. title: options.tooltip
  1775. }).tooltip({
  1776. container: 'body',
  1777. trigger: 'hover',
  1778. placement: 'bottom'
  1779. });
  1780. }
  1781. })($node, options);
  1782. },
  1783. toggleBtn: function ($btn, isEnable) {
  1784. $btn.toggleClass('disabled', !isEnable);
  1785. $btn.attr('disabled', !isEnable);
  1786. },
  1787. toggleBtnActive: function ($btn, isActive) {
  1788. $btn.toggleClass('active', isActive);
  1789. },
  1790. onDialogShown: function ($dialog, handler) {
  1791. $dialog.one('shown.bs.modal', handler);
  1792. },
  1793. onDialogHidden: function ($dialog, handler) {
  1794. $dialog.one('hidden.bs.modal', handler);
  1795. },
  1796. showDialog: function ($dialog) {
  1797. $dialog.modal('show');
  1798. },
  1799. hideDialog: function ($dialog) {
  1800. $dialog.modal('hide');
  1801. },
  1802. createLayout: function ($note, options) {
  1803. self.options = options;
  1804. var $editor = (options.airMode ? ui.airEditor([
  1805. ui.editingArea([
  1806. ui.airEditable()
  1807. ])
  1808. ]) : ui.editor([
  1809. ui.toolbar(),
  1810. ui.editingArea([
  1811. ui.codable(),
  1812. ui.editable()
  1813. ]),
  1814. ui.statusbar()
  1815. ])).render();
  1816. $editor.insertAfter($note);
  1817. return {
  1818. note: $note,
  1819. editor: $editor,
  1820. toolbar: $editor.find('.note-toolbar'),
  1821. editingArea: $editor.find('.note-editing-area'),
  1822. editable: $editor.find('.note-editable'),
  1823. codable: $editor.find('.note-codable'),
  1824. statusbar: $editor.find('.note-statusbar')
  1825. };
  1826. },
  1827. removeLayout: function ($note, layoutInfo) {
  1828. $note.html(layoutInfo.editable.html());
  1829. layoutInfo.editor.remove();
  1830. $note.show();
  1831. }
  1832. };
  1833. $.summernote = $.summernote || {
  1834. lang: {}
  1835. };
  1836. $.extend($.summernote.lang, {
  1837. 'en-US': {
  1838. font: {
  1839. bold: 'Bold',
  1840. italic: 'Italic',
  1841. underline: 'Underline',
  1842. clear: 'Remove Font Style',
  1843. height: 'Line Height',
  1844. name: 'Font Family',
  1845. strikethrough: 'Strikethrough',
  1846. subscript: 'Subscript',
  1847. superscript: 'Superscript',
  1848. size: 'Font Size'
  1849. },
  1850. image: {
  1851. image: 'Picture',
  1852. insert: 'Insert Image',
  1853. resizeFull: 'Resize Full',
  1854. resizeHalf: 'Resize Half',
  1855. resizeQuarter: 'Resize Quarter',
  1856. floatLeft: 'Float Left',
  1857. floatRight: 'Float Right',
  1858. floatNone: 'Float None',
  1859. shapeRounded: 'Shape: Rounded',
  1860. shapeCircle: 'Shape: Circle',
  1861. shapeThumbnail: 'Shape: Thumbnail',
  1862. shapeNone: 'Shape: None',
  1863. dragImageHere: 'Drag image or text here',
  1864. dropImage: 'Drop image or Text',
  1865. selectFromFiles: 'Select from files',
  1866. maximumFileSize: 'Maximum file size',
  1867. maximumFileSizeError: 'Maximum file size exceeded.',
  1868. url: 'Image URL',
  1869. remove: 'Remove Image'
  1870. },
  1871. video: {
  1872. video: 'Video',
  1873. videoLink: 'Video Link',
  1874. insert: 'Insert Video',
  1875. url: 'Video URL?',
  1876. providers: '(YouTube, Vimeo, Vine, Instagram, DailyMotion or Youku)'
  1877. },
  1878. link: {
  1879. link: 'Link',
  1880. insert: 'Insert Link',
  1881. unlink: 'Unlink',
  1882. edit: 'Edit',
  1883. textToDisplay: 'Text to display',
  1884. url: 'To what URL should this link go?',
  1885. openInNewWindow: 'Open in new window'
  1886. },
  1887. table: {
  1888. table: 'Table'
  1889. },
  1890. hr: {
  1891. insert: 'Insert Horizontal Rule'
  1892. },
  1893. style: {
  1894. style: 'Style',
  1895. p: 'Normal',
  1896. blockquote: 'Quote',
  1897. pre: 'Code',
  1898. h1: 'Header 1',
  1899. h2: 'Header 2',
  1900. h3: 'Header 3',
  1901. h4: 'Header 4',
  1902. h5: 'Header 5',
  1903. h6: 'Header 6'
  1904. },
  1905. lists: {
  1906. unordered: 'Unordered list',
  1907. ordered: 'Ordered list'
  1908. },
  1909. options: {
  1910. help: 'Help',
  1911. fullscreen: 'Full Screen',
  1912. codeview: 'Code View'
  1913. },
  1914. paragraph: {
  1915. paragraph: 'Paragraph',
  1916. outdent: 'Outdent',
  1917. indent: 'Indent',
  1918. left: 'Align left',
  1919. center: 'Align center',
  1920. right: 'Align right',
  1921. justify: 'Justify full'
  1922. },
  1923. color: {
  1924. recent: 'Recent Color',
  1925. more: 'More Color',
  1926. background: 'Background Color',
  1927. foreground: 'Foreground Color',
  1928. transparent: 'Transparent',
  1929. setTransparent: 'Set transparent',
  1930. reset: 'Reset',
  1931. resetToDefault: 'Reset to default'
  1932. },
  1933. shortcut: {
  1934. shortcuts: 'Keyboard shortcuts',
  1935. close: 'Close',
  1936. textFormatting: 'Text formatting',
  1937. action: 'Action',
  1938. paragraphFormatting: 'Paragraph formatting',
  1939. documentStyle: 'Document Style',
  1940. extraKeys: 'Extra keys'
  1941. },
  1942. help: {
  1943. 'insertParagraph': 'Insert Paragraph',
  1944. 'undo': 'Undoes the last command',
  1945. 'redo': 'Redoes the last command',
  1946. 'tab': 'Tab',
  1947. 'untab': 'Untab',
  1948. 'bold': 'Set a bold style',
  1949. 'italic': 'Set a italic style',
  1950. 'underline': 'Set a underline style',
  1951. 'strikethrough': 'Set a strikethrough style',
  1952. 'removeFormat': 'Clean a style',
  1953. 'justifyLeft': 'Set left align',
  1954. 'justifyCenter': 'Set center align',
  1955. 'justifyRight': 'Set right align',
  1956. 'justifyFull': 'Set full align',
  1957. 'insertUnorderedList': 'Toggle unordered list',
  1958. 'insertOrderedList': 'Toggle ordered list',
  1959. 'outdent': 'Outdent on current paragraph',
  1960. 'indent': 'Indent on current paragraph',
  1961. 'formatPara': 'Change current block\'s format as a paragraph(P tag)',
  1962. 'formatH1': 'Change current block\'s format as H1',
  1963. 'formatH2': 'Change current block\'s format as H2',
  1964. 'formatH3': 'Change current block\'s format as H3',
  1965. 'formatH4': 'Change current block\'s format as H4',
  1966. 'formatH5': 'Change current block\'s format as H5',
  1967. 'formatH6': 'Change current block\'s format as H6',
  1968. 'insertHorizontalRule': 'Insert horizontal rule',
  1969. 'linkDialog.show': 'Show Link Dialog'
  1970. },
  1971. history: {
  1972. undo: 'Undo',
  1973. redo: 'Redo'
  1974. },
  1975. specialChar: {
  1976. specialChar: 'SPECIAL CHARACTERS',
  1977. select: 'Select Special characters'
  1978. }
  1979. }
  1980. });
  1981. /**
  1982. * @class core.key
  1983. *
  1984. * Object for keycodes.
  1985. *
  1986. * @singleton
  1987. * @alternateClassName key
  1988. */
  1989. var key = (function () {
  1990. var keyMap = {
  1991. 'BACKSPACE': 8,
  1992. 'TAB': 9,
  1993. 'ENTER': 13,
  1994. 'SPACE': 32,
  1995. // Arrow
  1996. 'LEFT': 37,
  1997. 'UP': 38,
  1998. 'RIGHT': 39,
  1999. 'DOWN': 40,
  2000. // Number: 0-9
  2001. 'NUM0': 48,
  2002. 'NUM1': 49,
  2003. 'NUM2': 50,
  2004. 'NUM3': 51,
  2005. 'NUM4': 52,
  2006. 'NUM5': 53,
  2007. 'NUM6': 54,
  2008. 'NUM7': 55,
  2009. 'NUM8': 56,
  2010. // Alphabet: a-z
  2011. 'B': 66,
  2012. 'E': 69,
  2013. 'I': 73,
  2014. 'J': 74,
  2015. 'K': 75,
  2016. 'L': 76,
  2017. 'R': 82,
  2018. 'S': 83,
  2019. 'U': 85,
  2020. 'V': 86,
  2021. 'Y': 89,
  2022. 'Z': 90,
  2023. 'SLASH': 191,
  2024. 'LEFTBRACKET': 219,
  2025. 'BACKSLASH': 220,
  2026. 'RIGHTBRACKET': 221
  2027. };
  2028. return {
  2029. /**
  2030. * @method isEdit
  2031. *
  2032. * @param {Number} keyCode
  2033. * @return {Boolean}
  2034. */
  2035. isEdit: function (keyCode) {
  2036. return list.contains([
  2037. keyMap.BACKSPACE,
  2038. keyMap.TAB,
  2039. keyMap.ENTER,
  2040. keyMap.SPACE
  2041. ], keyCode);
  2042. },
  2043. /**
  2044. * @method isMove
  2045. *
  2046. * @param {Number} keyCode
  2047. * @return {Boolean}
  2048. */
  2049. isMove: function (keyCode) {
  2050. return list.contains([
  2051. keyMap.LEFT,
  2052. keyMap.UP,
  2053. keyMap.RIGHT,
  2054. keyMap.DOWN
  2055. ], keyCode);
  2056. },
  2057. /**
  2058. * @property {Object} nameFromCode
  2059. * @property {String} nameFromCode.8 "BACKSPACE"
  2060. */
  2061. nameFromCode: func.invertObject(keyMap),
  2062. code: keyMap
  2063. };
  2064. })();
  2065. var range = (function () {
  2066. /**
  2067. * return boundaryPoint from TextRange, inspired by Andy Na's HuskyRange.js
  2068. *
  2069. * @param {TextRange} textRange
  2070. * @param {Boolean} isStart
  2071. * @return {BoundaryPoint}
  2072. *
  2073. * @see http://msdn.microsoft.com/en-us/library/ie/ms535872(v=vs.85).aspx
  2074. */
  2075. var textRangeToPoint = function (textRange, isStart) {
  2076. var container = textRange.parentElement(), offset;
  2077. var tester = document.body.createTextRange(), prevContainer;
  2078. var childNodes = list.from(container.childNodes);
  2079. for (offset = 0; offset < childNodes.length; offset++) {
  2080. if (dom.isText(childNodes[offset])) {
  2081. continue;
  2082. }
  2083. tester.moveToElementText(childNodes[offset]);
  2084. if (tester.compareEndPoints('StartToStart', textRange) >= 0) {
  2085. break;
  2086. }
  2087. prevContainer = childNodes[offset];
  2088. }
  2089. if (offset !== 0 && dom.isText(childNodes[offset - 1])) {
  2090. var textRangeStart = document.body.createTextRange(), curTextNode = null;
  2091. textRangeStart.moveToElementText(prevContainer || container);
  2092. textRangeStart.collapse(!prevContainer);
  2093. curTextNode = prevContainer ? prevContainer.nextSibling : container.firstChild;
  2094. var pointTester = textRange.duplicate();
  2095. pointTester.setEndPoint('StartToStart', textRangeStart);
  2096. var textCount = pointTester.text.replace(/[\r\n]/g, '').length;
  2097. while (textCount > curTextNode.nodeValue.length && curTextNode.nextSibling) {
  2098. textCount -= curTextNode.nodeValue.length;
  2099. curTextNode = curTextNode.nextSibling;
  2100. }
  2101. /* jshint ignore:start */
  2102. var dummy = curTextNode.nodeValue; // enforce IE to re-reference curTextNode, hack
  2103. /* jshint ignore:end */
  2104. if (isStart && curTextNode.nextSibling && dom.isText(curTextNode.nextSibling) &&
  2105. textCount === curTextNode.nodeValue.length) {
  2106. textCount -= curTextNode.nodeValue.length;
  2107. curTextNode = curTextNode.nextSibling;
  2108. }
  2109. container = curTextNode;
  2110. offset = textCount;
  2111. }
  2112. return {
  2113. cont: container,
  2114. offset: offset
  2115. };
  2116. };
  2117. /**
  2118. * return TextRange from boundary point (inspired by google closure-library)
  2119. * @param {BoundaryPoint} point
  2120. * @return {TextRange}
  2121. */
  2122. var pointToTextRange = function (point) {
  2123. var textRangeInfo = function (container, offset) {
  2124. var node, isCollapseToStart;
  2125. if (dom.isText(container)) {
  2126. var prevTextNodes = dom.listPrev(container, func.not(dom.isText));
  2127. var prevContainer = list.last(prevTextNodes).previousSibling;
  2128. node = prevContainer || container.parentNode;
  2129. offset += list.sum(list.tail(prevTextNodes), dom.nodeLength);
  2130. isCollapseToStart = !prevContainer;
  2131. } else {
  2132. node = container.childNodes[offset] || container;
  2133. if (dom.isText(node)) {
  2134. return textRangeInfo(node, 0);
  2135. }
  2136. offset = 0;
  2137. isCollapseToStart = false;
  2138. }
  2139. return {
  2140. node: node,
  2141. collapseToStart: isCollapseToStart,
  2142. offset: offset
  2143. };
  2144. };
  2145. var textRange = document.body.createTextRange();
  2146. var info = textRangeInfo(point.node, point.offset);
  2147. textRange.moveToElementText(info.node);
  2148. textRange.collapse(info.collapseToStart);
  2149. textRange.moveStart('character', info.offset);
  2150. return textRange;
  2151. };
  2152. /**
  2153. * Wrapped Range
  2154. *
  2155. * @constructor
  2156. * @param {Node} sc - start container
  2157. * @param {Number} so - start offset
  2158. * @param {Node} ec - end container
  2159. * @param {Number} eo - end offset
  2160. */
  2161. var WrappedRange = function (sc, so, ec, eo) {
  2162. this.sc = sc;
  2163. this.so = so;
  2164. this.ec = ec;
  2165. this.eo = eo;
  2166. // nativeRange: get nativeRange from sc, so, ec, eo
  2167. var nativeRange = function () {
  2168. if (agent.isW3CRangeSupport) {
  2169. var w3cRange = document.createRange();
  2170. w3cRange.setStart(sc, so);
  2171. w3cRange.setEnd(ec, eo);
  2172. return w3cRange;
  2173. } else {
  2174. var textRange = pointToTextRange({
  2175. node: sc,
  2176. offset: so
  2177. });
  2178. textRange.setEndPoint('EndToEnd', pointToTextRange({
  2179. node: ec,
  2180. offset: eo
  2181. }));
  2182. return textRange;
  2183. }
  2184. };
  2185. this.getPoints = function () {
  2186. return {
  2187. sc: sc,
  2188. so: so,
  2189. ec: ec,
  2190. eo: eo
  2191. };
  2192. };
  2193. this.getStartPoint = function () {
  2194. return {
  2195. node: sc,
  2196. offset: so
  2197. };
  2198. };
  2199. this.getEndPoint = function () {
  2200. return {
  2201. node: ec,
  2202. offset: eo
  2203. };
  2204. };
  2205. /**
  2206. * select update visible range
  2207. */
  2208. this.select = function () {
  2209. var nativeRng = nativeRange();
  2210. if (agent.isW3CRangeSupport) {
  2211. var selection = document.getSelection();
  2212. if (selection.rangeCount > 0) {
  2213. selection.removeAllRanges();
  2214. }
  2215. selection.addRange(nativeRng);
  2216. } else {
  2217. nativeRng.select();
  2218. }
  2219. return this;
  2220. };
  2221. /**
  2222. * Moves the scrollbar to start container(sc) of current range
  2223. *
  2224. * @return {WrappedRange}
  2225. */
  2226. this.scrollIntoView = function (container) {
  2227. var height = $(container).height();
  2228. if (container.scrollTop + height < this.sc.offsetTop) {
  2229. container.scrollTop += Math.abs(container.scrollTop + height - this.sc.offsetTop);
  2230. }
  2231. return this;
  2232. };
  2233. /**
  2234. * @return {WrappedRange}
  2235. */
  2236. this.normalize = function () {
  2237. /**
  2238. * @param {BoundaryPoint} point
  2239. * @param {Boolean} isLeftToRight
  2240. * @return {BoundaryPoint}
  2241. */
  2242. var getVisiblePoint = function (point, isLeftToRight) {
  2243. if ((dom.isVisiblePoint(point) && !dom.isEdgePoint(point)) ||
  2244. (dom.isVisiblePoint(point) && dom.isRightEdgePoint(point) && !isLeftToRight) ||
  2245. (dom.isVisiblePoint(point) && dom.isLeftEdgePoint(point) && isLeftToRight) ||
  2246. (dom.isVisiblePoint(point) && dom.isBlock(point.node) && dom.isEmpty(point.node))) {
  2247. return point;
  2248. }
  2249. // point on block's edge
  2250. var block = dom.ancestor(point.node, dom.isBlock);
  2251. if (((dom.isLeftEdgePointOf(point, block) || dom.isVoid(dom.prevPoint(point).node)) && !isLeftToRight) ||
  2252. ((dom.isRightEdgePointOf(point, block) || dom.isVoid(dom.nextPoint(point).node)) && isLeftToRight)) {
  2253. // returns point already on visible point
  2254. if (dom.isVisiblePoint(point)) {
  2255. return point;
  2256. }
  2257. // reverse direction
  2258. isLeftToRight = !isLeftToRight;
  2259. }
  2260. var nextPoint = isLeftToRight ? dom.nextPointUntil(dom.nextPoint(point), dom.isVisiblePoint) :
  2261. dom.prevPointUntil(dom.prevPoint(point), dom.isVisiblePoint);
  2262. return nextPoint || point;
  2263. };
  2264. var endPoint = getVisiblePoint(this.getEndPoint(), false);
  2265. var startPoint = this.isCollapsed() ? endPoint : getVisiblePoint(this.getStartPoint(), true);
  2266. return new WrappedRange(
  2267. startPoint.node,
  2268. startPoint.offset,
  2269. endPoint.node,
  2270. endPoint.offset
  2271. );
  2272. };
  2273. /**
  2274. * returns matched nodes on range
  2275. *
  2276. * @param {Function} [pred] - predicate function
  2277. * @param {Object} [options]
  2278. * @param {Boolean} [options.includeAncestor]
  2279. * @param {Boolean} [options.fullyContains]
  2280. * @return {Node[]}
  2281. */
  2282. this.nodes = function (pred, options) {
  2283. pred = pred || func.ok;
  2284. var includeAncestor = options && options.includeAncestor;
  2285. var fullyContains = options && options.fullyContains;
  2286. // TODO compare points and sort
  2287. var startPoint = this.getStartPoint();
  2288. var endPoint = this.getEndPoint();
  2289. var nodes = [];
  2290. var leftEdgeNodes = [];
  2291. dom.walkPoint(startPoint, endPoint, function (point) {
  2292. if (dom.isEditable(point.node)) {
  2293. return;
  2294. }
  2295. var node;
  2296. if (fullyContains) {
  2297. if (dom.isLeftEdgePoint(point)) {
  2298. leftEdgeNodes.push(point.node);
  2299. }
  2300. if (dom.isRightEdgePoint(point) && list.contains(leftEdgeNodes, point.node)) {
  2301. node = point.node;
  2302. }
  2303. } else if (includeAncestor) {
  2304. node = dom.ancestor(point.node, pred);
  2305. } else {
  2306. node = point.node;
  2307. }
  2308. if (node && pred(node)) {
  2309. nodes.push(node);
  2310. }
  2311. }, true);
  2312. return list.unique(nodes);
  2313. };
  2314. /**
  2315. * returns commonAncestor of range
  2316. * @return {Element} - commonAncestor
  2317. */
  2318. this.commonAncestor = function () {
  2319. return dom.commonAncestor(sc, ec);
  2320. };
  2321. /**
  2322. * returns expanded range by pred
  2323. *
  2324. * @param {Function} pred - predicate function
  2325. * @return {WrappedRange}
  2326. */
  2327. this.expand = function (pred) {
  2328. var startAncestor = dom.ancestor(sc, pred);
  2329. var endAncestor = dom.ancestor(ec, pred);
  2330. if (!startAncestor && !endAncestor) {
  2331. return new WrappedRange(sc, so, ec, eo);
  2332. }
  2333. var boundaryPoints = this.getPoints();
  2334. if (startAncestor) {
  2335. boundaryPoints.sc = startAncestor;
  2336. boundaryPoints.so = 0;
  2337. }
  2338. if (endAncestor) {
  2339. boundaryPoints.ec = endAncestor;
  2340. boundaryPoints.eo = dom.nodeLength(endAncestor);
  2341. }
  2342. return new WrappedRange(
  2343. boundaryPoints.sc,
  2344. boundaryPoints.so,
  2345. boundaryPoints.ec,
  2346. boundaryPoints.eo
  2347. );
  2348. };
  2349. /**
  2350. * @param {Boolean} isCollapseToStart
  2351. * @return {WrappedRange}
  2352. */
  2353. this.collapse = function (isCollapseToStart) {
  2354. if (isCollapseToStart) {
  2355. return new WrappedRange(sc, so, sc, so);
  2356. } else {
  2357. return new WrappedRange(ec, eo, ec, eo);
  2358. }
  2359. };
  2360. /**
  2361. * splitText on range
  2362. */
  2363. this.splitText = function () {
  2364. var isSameContainer = sc === ec;
  2365. var boundaryPoints = this.getPoints();
  2366. if (dom.isText(ec) && !dom.isEdgePoint(this.getEndPoint())) {
  2367. ec.splitText(eo);
  2368. }
  2369. if (dom.isText(sc) && !dom.isEdgePoint(this.getStartPoint())) {
  2370. boundaryPoints.sc = sc.splitText(so);
  2371. boundaryPoints.so = 0;
  2372. if (isSameContainer) {
  2373. boundaryPoints.ec = boundaryPoints.sc;
  2374. boundaryPoints.eo = eo - so;
  2375. }
  2376. }
  2377. return new WrappedRange(
  2378. boundaryPoints.sc,
  2379. boundaryPoints.so,
  2380. boundaryPoints.ec,
  2381. boundaryPoints.eo
  2382. );
  2383. };
  2384. /**
  2385. * delete contents on range
  2386. * @return {WrappedRange}
  2387. */
  2388. this.deleteContents = function () {
  2389. if (this.isCollapsed()) {
  2390. return this;
  2391. }
  2392. var rng = this.splitText();
  2393. var nodes = rng.nodes(null, {
  2394. fullyContains: true
  2395. });
  2396. // find new cursor point
  2397. var point = dom.prevPointUntil(rng.getStartPoint(), function (point) {
  2398. return !list.contains(nodes, point.node);
  2399. });
  2400. var emptyParents = [];
  2401. $.each(nodes, function (idx, node) {
  2402. // find empty parents
  2403. var parent = node.parentNode;
  2404. if (point.node !== parent && dom.nodeLength(parent) === 1) {
  2405. emptyParents.push(parent);
  2406. }
  2407. dom.remove(node, false);
  2408. });
  2409. // remove empty parents
  2410. $.each(emptyParents, function (idx, node) {
  2411. dom.remove(node, false);
  2412. });
  2413. return new WrappedRange(
  2414. point.node,
  2415. point.offset,
  2416. point.node,
  2417. point.offset
  2418. ).normalize();
  2419. };
  2420. /**
  2421. * makeIsOn: return isOn(pred) function
  2422. */
  2423. var makeIsOn = function (pred) {
  2424. return function () {
  2425. var ancestor = dom.ancestor(sc, pred);
  2426. return !!ancestor && (ancestor === dom.ancestor(ec, pred));
  2427. };
  2428. };
  2429. // isOnEditable: judge whether range is on editable or not
  2430. this.isOnEditable = makeIsOn(dom.isEditable);
  2431. // isOnList: judge whether range is on list node or not
  2432. this.isOnList = makeIsOn(dom.isList);
  2433. // isOnAnchor: judge whether range is on anchor node or not
  2434. this.isOnAnchor = makeIsOn(dom.isAnchor);
  2435. // isOnCell: judge whether range is on cell node or not
  2436. this.isOnCell = makeIsOn(dom.isCell);
  2437. // isOnData: judge whether range is on data node or not
  2438. this.isOnData = makeIsOn(dom.isData);
  2439. /**
  2440. * @param {Function} pred
  2441. * @return {Boolean}
  2442. */
  2443. this.isLeftEdgeOf = function (pred) {
  2444. if (!dom.isLeftEdgePoint(this.getStartPoint())) {
  2445. return false;
  2446. }
  2447. var node = dom.ancestor(this.sc, pred);
  2448. return node && dom.isLeftEdgeOf(this.sc, node);
  2449. };
  2450. /**
  2451. * returns whether range was collapsed or not
  2452. */
  2453. this.isCollapsed = function () {
  2454. return sc === ec && so === eo;
  2455. };
  2456. /**
  2457. * wrap inline nodes which children of body with paragraph
  2458. *
  2459. * @return {WrappedRange}
  2460. */
  2461. this.wrapBodyInlineWithPara = function () {
  2462. if (dom.isBodyContainer(sc) && dom.isEmpty(sc)) {
  2463. sc.innerHTML = dom.emptyPara;
  2464. return new WrappedRange(sc.firstChild, 0, sc.firstChild, 0);
  2465. }
  2466. /**
  2467. * [workaround] firefox often create range on not visible point. so normalize here.
  2468. * - firefox: |<p>text</p>|
  2469. * - chrome: <p>|text|</p>
  2470. */
  2471. var rng = this.normalize();
  2472. if (dom.isParaInline(sc) || dom.isPara(sc)) {
  2473. return rng;
  2474. }
  2475. // find inline top ancestor
  2476. var topAncestor;
  2477. if (dom.isInline(rng.sc)) {
  2478. var ancestors = dom.listAncestor(rng.sc, func.not(dom.isInline));
  2479. topAncestor = list.last(ancestors);
  2480. if (!dom.isInline(topAncestor)) {
  2481. topAncestor = ancestors[ancestors.length - 2] || rng.sc.childNodes[rng.so];
  2482. }
  2483. } else {
  2484. topAncestor = rng.sc.childNodes[rng.so > 0 ? rng.so - 1 : 0];
  2485. }
  2486. // siblings not in paragraph
  2487. var inlineSiblings = dom.listPrev(topAncestor, dom.isParaInline).reverse();
  2488. inlineSiblings = inlineSiblings.concat(dom.listNext(topAncestor.nextSibling, dom.isParaInline));
  2489. // wrap with paragraph
  2490. if (inlineSiblings.length) {
  2491. var para = dom.wrap(list.head(inlineSiblings), 'p');
  2492. dom.appendChildNodes(para, list.tail(inlineSiblings));
  2493. }
  2494. return this.normalize();
  2495. };
  2496. /**
  2497. * insert node at current cursor
  2498. *
  2499. * @param {Node} node
  2500. * @return {Node}
  2501. */
  2502. this.insertNode = function (node) {
  2503. var rng = this.wrapBodyInlineWithPara().deleteContents();
  2504. var info = dom.splitPoint(rng.getStartPoint(), dom.isInline(node));
  2505. if (info.rightNode) {
  2506. info.rightNode.parentNode.insertBefore(node, info.rightNode);
  2507. } else {
  2508. info.container.appendChild(node);
  2509. }
  2510. return node;
  2511. };
  2512. /**
  2513. * insert html at current cursor
  2514. */
  2515. this.pasteHTML = function (markup) {
  2516. var contentsContainer = $('<div></div>').html(markup)[0];
  2517. var childNodes = list.from(contentsContainer.childNodes);
  2518. var rng = this.wrapBodyInlineWithPara().deleteContents();
  2519. return childNodes.reverse().map(function (childNode) {
  2520. return rng.insertNode(childNode);
  2521. }).reverse();
  2522. };
  2523. /**
  2524. * returns text in range
  2525. *
  2526. * @return {String}
  2527. */
  2528. this.toString = function () {
  2529. var nativeRng = nativeRange();
  2530. return agent.isW3CRangeSupport ? nativeRng.toString() : nativeRng.text;
  2531. };
  2532. /**
  2533. * returns range for word before cursor
  2534. *
  2535. * @param {Boolean} [findAfter] - find after cursor, default: false
  2536. * @return {WrappedRange}
  2537. */
  2538. this.getWordRange = function (findAfter) {
  2539. var endPoint = this.getEndPoint();
  2540. if (!dom.isCharPoint(endPoint)) {
  2541. return this;
  2542. }
  2543. var startPoint = dom.prevPointUntil(endPoint, function (point) {
  2544. return !dom.isCharPoint(point);
  2545. });
  2546. if (findAfter) {
  2547. endPoint = dom.nextPointUntil(endPoint, function (point) {
  2548. return !dom.isCharPoint(point);
  2549. });
  2550. }
  2551. return new WrappedRange(
  2552. startPoint.node,
  2553. startPoint.offset,
  2554. endPoint.node,
  2555. endPoint.offset
  2556. );
  2557. };
  2558. /**
  2559. * create offsetPath bookmark
  2560. *
  2561. * @param {Node} editable
  2562. */
  2563. this.bookmark = function (editable) {
  2564. return {
  2565. s: {
  2566. path: dom.makeOffsetPath(editable, sc),
  2567. offset: so
  2568. },
  2569. e: {
  2570. path: dom.makeOffsetPath(editable, ec),
  2571. offset: eo
  2572. }
  2573. };
  2574. };
  2575. /**
  2576. * create offsetPath bookmark base on paragraph
  2577. *
  2578. * @param {Node[]} paras
  2579. */
  2580. this.paraBookmark = function (paras) {
  2581. return {
  2582. s: {
  2583. path: list.tail(dom.makeOffsetPath(list.head(paras), sc)),
  2584. offset: so
  2585. },
  2586. e: {
  2587. path: list.tail(dom.makeOffsetPath(list.last(paras), ec)),
  2588. offset: eo
  2589. }
  2590. };
  2591. };
  2592. /**
  2593. * getClientRects
  2594. * @return {Rect[]}
  2595. */
  2596. this.getClientRects = function () {
  2597. var nativeRng = nativeRange();
  2598. return nativeRng.getClientRects();
  2599. };
  2600. };
  2601. /**
  2602. * @class core.range
  2603. *
  2604. * Data structure
  2605. * * BoundaryPoint: a point of dom tree
  2606. * * BoundaryPoints: two boundaryPoints corresponding to the start and the end of the Range
  2607. *
  2608. * See to http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html#Level-2-Range-Position
  2609. *
  2610. * @singleton
  2611. * @alternateClassName range
  2612. */
  2613. return {
  2614. /**
  2615. * create Range Object From arguments or Browser Selection
  2616. *
  2617. * @param {Node} sc - start container
  2618. * @param {Number} so - start offset
  2619. * @param {Node} ec - end container
  2620. * @param {Number} eo - end offset
  2621. * @return {WrappedRange}
  2622. */
  2623. create: function (sc, so, ec, eo) {
  2624. if (arguments.length === 4) {
  2625. return new WrappedRange(sc, so, ec, eo);
  2626. } else if (arguments.length === 2) { //collapsed
  2627. ec = sc;
  2628. eo = so;
  2629. return new WrappedRange(sc, so, ec, eo);
  2630. } else {
  2631. var wrappedRange = this.createFromSelection();
  2632. if (!wrappedRange && arguments.length === 1) {
  2633. wrappedRange = this.createFromNode(arguments[0]);
  2634. return wrappedRange.collapse(dom.emptyPara === arguments[0].innerHTML);
  2635. }
  2636. return wrappedRange;
  2637. }
  2638. },
  2639. createFromSelection: function () {
  2640. var sc, so, ec, eo;
  2641. if (agent.isW3CRangeSupport) {
  2642. var selection = document.getSelection();
  2643. if (!selection || selection.rangeCount === 0) {
  2644. return null;
  2645. } else if (dom.isBody(selection.anchorNode)) {
  2646. // Firefox: returns entire body as range on initialization.
  2647. // We won't never need it.
  2648. return null;
  2649. }
  2650. var nativeRng = selection.getRangeAt(0);
  2651. sc = nativeRng.startContainer;
  2652. so = nativeRng.startOffset;
  2653. ec = nativeRng.endContainer;
  2654. eo = nativeRng.endOffset;
  2655. } else { // IE8: TextRange
  2656. var textRange = document.selection.createRange();
  2657. var textRangeEnd = textRange.duplicate();
  2658. textRangeEnd.collapse(false);
  2659. var textRangeStart = textRange;
  2660. textRangeStart.collapse(true);
  2661. var startPoint = textRangeToPoint(textRangeStart, true),
  2662. endPoint = textRangeToPoint(textRangeEnd, false);
  2663. // same visible point case: range was collapsed.
  2664. if (dom.isText(startPoint.node) && dom.isLeftEdgePoint(startPoint) &&
  2665. dom.isTextNode(endPoint.node) && dom.isRightEdgePoint(endPoint) &&
  2666. endPoint.node.nextSibling === startPoint.node) {
  2667. startPoint = endPoint;
  2668. }
  2669. sc = startPoint.cont;
  2670. so = startPoint.offset;
  2671. ec = endPoint.cont;
  2672. eo = endPoint.offset;
  2673. }
  2674. return new WrappedRange(sc, so, ec, eo);
  2675. },
  2676. /**
  2677. * @method
  2678. *
  2679. * create WrappedRange from node
  2680. *
  2681. * @param {Node} node
  2682. * @return {WrappedRange}
  2683. */
  2684. createFromNode: function (node) {
  2685. var sc = node;
  2686. var so = 0;
  2687. var ec = node;
  2688. var eo = dom.nodeLength(ec);
  2689. // browsers can't target a picture or void node
  2690. if (dom.isVoid(sc)) {
  2691. so = dom.listPrev(sc).length - 1;
  2692. sc = sc.parentNode;
  2693. }
  2694. if (dom.isBR(ec)) {
  2695. eo = dom.listPrev(ec).length - 1;
  2696. ec = ec.parentNode;
  2697. } else if (dom.isVoid(ec)) {
  2698. eo = dom.listPrev(ec).length;
  2699. ec = ec.parentNode;
  2700. }
  2701. return this.create(sc, so, ec, eo);
  2702. },
  2703. /**
  2704. * create WrappedRange from node after position
  2705. *
  2706. * @param {Node} node
  2707. * @return {WrappedRange}
  2708. */
  2709. createFromNodeBefore: function (node) {
  2710. return this.createFromNode(node).collapse(true);
  2711. },
  2712. /**
  2713. * create WrappedRange from node after position
  2714. *
  2715. * @param {Node} node
  2716. * @return {WrappedRange}
  2717. */
  2718. createFromNodeAfter: function (node) {
  2719. return this.createFromNode(node).collapse();
  2720. },
  2721. /**
  2722. * @method
  2723. *
  2724. * create WrappedRange from bookmark
  2725. *
  2726. * @param {Node} editable
  2727. * @param {Object} bookmark
  2728. * @return {WrappedRange}
  2729. */
  2730. createFromBookmark: function (editable, bookmark) {
  2731. var sc = dom.fromOffsetPath(editable, bookmark.s.path);
  2732. var so = bookmark.s.offset;
  2733. var ec = dom.fromOffsetPath(editable, bookmark.e.path);
  2734. var eo = bookmark.e.offset;
  2735. return new WrappedRange(sc, so, ec, eo);
  2736. },
  2737. /**
  2738. * @method
  2739. *
  2740. * create WrappedRange from paraBookmark
  2741. *
  2742. * @param {Object} bookmark
  2743. * @param {Node[]} paras
  2744. * @return {WrappedRange}
  2745. */
  2746. createFromParaBookmark: function (bookmark, paras) {
  2747. var so = bookmark.s.offset;
  2748. var eo = bookmark.e.offset;
  2749. var sc = dom.fromOffsetPath(list.head(paras), bookmark.s.path);
  2750. var ec = dom.fromOffsetPath(list.last(paras), bookmark.e.path);
  2751. return new WrappedRange(sc, so, ec, eo);
  2752. }
  2753. };
  2754. })();
  2755. /**
  2756. * @class core.async
  2757. *
  2758. * Async functions which returns `Promise`
  2759. *
  2760. * @singleton
  2761. * @alternateClassName async
  2762. */
  2763. var async = (function () {
  2764. /**
  2765. * @method readFileAsDataURL
  2766. *
  2767. * read contents of file as representing URL
  2768. *
  2769. * @param {File} file
  2770. * @return {Promise} - then: dataUrl
  2771. */
  2772. var readFileAsDataURL = function (file) {
  2773. return $.Deferred(function (deferred) {
  2774. $.extend(new FileReader(), {
  2775. onload: function (e) {
  2776. var dataURL = e.target.result;
  2777. deferred.resolve(dataURL);
  2778. },
  2779. onerror: function () {
  2780. deferred.reject(this);
  2781. }
  2782. }).readAsDataURL(file);
  2783. }).promise();
  2784. };
  2785. /**
  2786. * @method createImage
  2787. *
  2788. * create `<image>` from url string
  2789. *
  2790. * @param {String} url
  2791. * @return {Promise} - then: $image
  2792. */
  2793. var createImage = function (url) {
  2794. return $.Deferred(function (deferred) {
  2795. var $img = $('<img>');
  2796. $img.one('load', function () {
  2797. $img.off('error abort');
  2798. deferred.resolve($img);
  2799. }).one('error abort', function () {
  2800. $img.off('load').detach();
  2801. deferred.reject($img);
  2802. }).css({
  2803. display: 'none'
  2804. }).appendTo(document.body).attr('src', url);
  2805. }).promise();
  2806. };
  2807. return {
  2808. readFileAsDataURL: readFileAsDataURL,
  2809. createImage: createImage
  2810. };
  2811. })();
  2812. /**
  2813. * @class editing.History
  2814. *
  2815. * Editor History
  2816. *
  2817. */
  2818. var History = function ($editable) {
  2819. var stack = [], stackOffset = -1;
  2820. var editable = $editable[0];
  2821. var makeSnapshot = function () {
  2822. var rng = range.create(editable);
  2823. var emptyBookmark = {s: {path: [], offset: 0}, e: {path: [], offset: 0}};
  2824. return {
  2825. contents: $editable.html(),
  2826. bookmark: (rng ? rng.bookmark(editable) : emptyBookmark)
  2827. };
  2828. };
  2829. var applySnapshot = function (snapshot) {
  2830. if (snapshot.contents !== null) {
  2831. $editable.html(snapshot.contents);
  2832. }
  2833. if (snapshot.bookmark !== null) {
  2834. range.createFromBookmark(editable, snapshot.bookmark).select();
  2835. }
  2836. };
  2837. /**
  2838. * @method rewind
  2839. * Rewinds the history stack back to the first snapshot taken.
  2840. * Leaves the stack intact, so that "Redo" can still be used.
  2841. */
  2842. this.rewind = function () {
  2843. // Create snap shot if not yet recorded
  2844. if ($editable.html() !== stack[stackOffset].contents) {
  2845. this.recordUndo();
  2846. }
  2847. // Return to the first available snapshot.
  2848. stackOffset = 0;
  2849. // Apply that snapshot.
  2850. applySnapshot(stack[stackOffset]);
  2851. };
  2852. /**
  2853. * @method reset
  2854. * Resets the history stack completely; reverting to an empty editor.
  2855. */
  2856. this.reset = function () {
  2857. // Clear the stack.
  2858. stack = [];
  2859. // Restore stackOffset to its original value.
  2860. stackOffset = -1;
  2861. // Clear the editable area.
  2862. $editable.html('');
  2863. // Record our first snapshot (of nothing).
  2864. this.recordUndo();
  2865. };
  2866. /**
  2867. * undo
  2868. */
  2869. this.undo = function () {
  2870. // Create snap shot if not yet recorded
  2871. if ($editable.html() !== stack[stackOffset].contents) {
  2872. this.recordUndo();
  2873. }
  2874. if (0 < stackOffset) {
  2875. stackOffset--;
  2876. applySnapshot(stack[stackOffset]);
  2877. }
  2878. };
  2879. /**
  2880. * redo
  2881. */
  2882. this.redo = function () {
  2883. if (stack.length - 1 > stackOffset) {
  2884. stackOffset++;
  2885. applySnapshot(stack[stackOffset]);
  2886. }
  2887. };
  2888. /**
  2889. * recorded undo
  2890. */
  2891. this.recordUndo = function () {
  2892. stackOffset++;
  2893. // Wash out stack after stackOffset
  2894. if (stack.length > stackOffset) {
  2895. stack = stack.slice(0, stackOffset);
  2896. }
  2897. // Create new snapshot and push it to the end
  2898. stack.push(makeSnapshot());
  2899. };
  2900. };
  2901. /**
  2902. * @class editing.Style
  2903. *
  2904. * Style
  2905. *
  2906. */
  2907. var Style = function () {
  2908. /**
  2909. * @method jQueryCSS
  2910. *
  2911. * [workaround] for old jQuery
  2912. * passing an array of style properties to .css()
  2913. * will result in an object of property-value pairs.
  2914. * (compability with version < 1.9)
  2915. *
  2916. * @private
  2917. * @param {jQuery} $obj
  2918. * @param {Array} propertyNames - An array of one or more CSS properties.
  2919. * @return {Object}
  2920. */
  2921. var jQueryCSS = function ($obj, propertyNames) {
  2922. if (agent.jqueryVersion < 1.9) {
  2923. var result = {};
  2924. $.each(propertyNames, function (idx, propertyName) {
  2925. result[propertyName] = $obj.css(propertyName);
  2926. });
  2927. return result;
  2928. }
  2929. return $obj.css.call($obj, propertyNames);
  2930. };
  2931. /**
  2932. * returns style object from node
  2933. *
  2934. * @param {jQuery} $node
  2935. * @return {Object}
  2936. */
  2937. this.fromNode = function ($node) {
  2938. var properties = ['font-family', 'font-size', 'text-align', 'list-style-type', 'line-height'];
  2939. var styleInfo = jQueryCSS($node, properties) || {};
  2940. styleInfo['font-size'] = parseInt(styleInfo['font-size'], 10);
  2941. return styleInfo;
  2942. };
  2943. /**
  2944. * paragraph level style
  2945. *
  2946. * @param {WrappedRange} rng
  2947. * @param {Object} styleInfo
  2948. */
  2949. this.stylePara = function (rng, styleInfo) {
  2950. $.each(rng.nodes(dom.isPara, {
  2951. includeAncestor: true
  2952. }), function (idx, para) {
  2953. $(para).css(styleInfo);
  2954. });
  2955. };
  2956. /**
  2957. * insert and returns styleNodes on range.
  2958. *
  2959. * @param {WrappedRange} rng
  2960. * @param {Object} [options] - options for styleNodes
  2961. * @param {String} [options.nodeName] - default: `SPAN`
  2962. * @param {Boolean} [options.expandClosestSibling] - default: `false`
  2963. * @param {Boolean} [options.onlyPartialContains] - default: `false`
  2964. * @return {Node[]}
  2965. */
  2966. this.styleNodes = function (rng, options) {
  2967. rng = rng.splitText();
  2968. var nodeName = options && options.nodeName || 'SPAN';
  2969. var expandClosestSibling = !!(options && options.expandClosestSibling);
  2970. var onlyPartialContains = !!(options && options.onlyPartialContains);
  2971. if (rng.isCollapsed()) {
  2972. return [rng.insertNode(dom.create(nodeName))];
  2973. }
  2974. var pred = dom.makePredByNodeName(nodeName);
  2975. var nodes = rng.nodes(dom.isText, {
  2976. fullyContains: true
  2977. }).map(function (text) {
  2978. return dom.singleChildAncestor(text, pred) || dom.wrap(text, nodeName);
  2979. });
  2980. if (expandClosestSibling) {
  2981. if (onlyPartialContains) {
  2982. var nodesInRange = rng.nodes();
  2983. // compose with partial contains predication
  2984. pred = func.and(pred, function (node) {
  2985. return list.contains(nodesInRange, node);
  2986. });
  2987. }
  2988. return nodes.map(function (node) {
  2989. var siblings = dom.withClosestSiblings(node, pred);
  2990. var head = list.head(siblings);
  2991. var tails = list.tail(siblings);
  2992. $.each(tails, function (idx, elem) {
  2993. dom.appendChildNodes(head, elem.childNodes);
  2994. dom.remove(elem);
  2995. });
  2996. return list.head(siblings);
  2997. });
  2998. } else {
  2999. return nodes;
  3000. }
  3001. };
  3002. /**
  3003. * get current style on cursor
  3004. *
  3005. * @param {WrappedRange} rng
  3006. * @return {Object} - object contains style properties.
  3007. */
  3008. this.current = function (rng) {
  3009. var $cont = $(!dom.isElement(rng.sc) ? rng.sc.parentNode : rng.sc);
  3010. var styleInfo = this.fromNode($cont);
  3011. // document.queryCommandState for toggle state
  3012. // [workaround] prevent Firefox nsresult: "0x80004005 (NS_ERROR_FAILURE)"
  3013. try {
  3014. styleInfo = $.extend(styleInfo, {
  3015. 'font-bold': document.queryCommandState('bold') ? 'bold' : 'normal',
  3016. 'font-italic': document.queryCommandState('italic') ? 'italic' : 'normal',
  3017. 'font-underline': document.queryCommandState('underline') ? 'underline' : 'normal',
  3018. 'font-subscript': document.queryCommandState('subscript') ? 'subscript' : 'normal',
  3019. 'font-superscript': document.queryCommandState('superscript') ? 'superscript' : 'normal',
  3020. 'font-strikethrough': document.queryCommandState('strikethrough') ? 'strikethrough' : 'normal',
  3021. 'font-family': document.queryCommandValue('fontname') || styleInfo['font-family']
  3022. });
  3023. } catch (e) {}
  3024. // list-style-type to list-style(unordered, ordered)
  3025. if (!rng.isOnList()) {
  3026. styleInfo['list-style'] = 'none';
  3027. } else {
  3028. var orderedTypes = ['circle', 'disc', 'disc-leading-zero', 'square'];
  3029. var isUnordered = $.inArray(styleInfo['list-style-type'], orderedTypes) > -1;
  3030. styleInfo['list-style'] = isUnordered ? 'unordered' : 'ordered';
  3031. }
  3032. var para = dom.ancestor(rng.sc, dom.isPara);
  3033. if (para && para.style['line-height']) {
  3034. styleInfo['line-height'] = para.style.lineHeight;
  3035. } else {
  3036. var lineHeight = parseInt(styleInfo['line-height'], 10) / parseInt(styleInfo['font-size'], 10);
  3037. styleInfo['line-height'] = lineHeight.toFixed(1);
  3038. }
  3039. styleInfo.anchor = rng.isOnAnchor() && dom.ancestor(rng.sc, dom.isAnchor);
  3040. styleInfo.ancestors = dom.listAncestor(rng.sc, dom.isEditable);
  3041. styleInfo.range = rng;
  3042. return styleInfo;
  3043. };
  3044. };
  3045. /**
  3046. * @class editing.Bullet
  3047. *
  3048. * @alternateClassName Bullet
  3049. */
  3050. var Bullet = function () {
  3051. var self = this;
  3052. /**
  3053. * toggle ordered list
  3054. */
  3055. this.insertOrderedList = function (editable) {
  3056. this.toggleList('OL', editable);
  3057. };
  3058. /**
  3059. * toggle unordered list
  3060. */
  3061. this.insertUnorderedList = function (editable) {
  3062. this.toggleList('UL', editable);
  3063. };
  3064. /**
  3065. * indent
  3066. */
  3067. this.indent = function (editable) {
  3068. var self = this;
  3069. var rng = range.create(editable).wrapBodyInlineWithPara();
  3070. var paras = rng.nodes(dom.isPara, { includeAncestor: true });
  3071. var clustereds = list.clusterBy(paras, func.peq2('parentNode'));
  3072. $.each(clustereds, function (idx, paras) {
  3073. var head = list.head(paras);
  3074. if (dom.isLi(head)) {
  3075. self.wrapList(paras, head.parentNode.nodeName);
  3076. } else {
  3077. $.each(paras, function (idx, para) {
  3078. $(para).css('marginLeft', function (idx, val) {
  3079. return (parseInt(val, 10) || 0) + 25;
  3080. });
  3081. });
  3082. }
  3083. });
  3084. rng.select();
  3085. };
  3086. /**
  3087. * outdent
  3088. */
  3089. this.outdent = function (editable) {
  3090. var self = this;
  3091. var rng = range.create(editable).wrapBodyInlineWithPara();
  3092. var paras = rng.nodes(dom.isPara, { includeAncestor: true });
  3093. var clustereds = list.clusterBy(paras, func.peq2('parentNode'));
  3094. $.each(clustereds, function (idx, paras) {
  3095. var head = list.head(paras);
  3096. if (dom.isLi(head)) {
  3097. self.releaseList([paras]);
  3098. } else {
  3099. $.each(paras, function (idx, para) {
  3100. $(para).css('marginLeft', function (idx, val) {
  3101. val = (parseInt(val, 10) || 0);
  3102. return val > 25 ? val - 25 : '';
  3103. });
  3104. });
  3105. }
  3106. });
  3107. rng.select();
  3108. };
  3109. /**
  3110. * toggle list
  3111. *
  3112. * @param {String} listName - OL or UL
  3113. */
  3114. this.toggleList = function (listName, editable) {
  3115. var rng = range.create(editable).wrapBodyInlineWithPara();
  3116. var paras = rng.nodes(dom.isPara, { includeAncestor: true });
  3117. var bookmark = rng.paraBookmark(paras);
  3118. var clustereds = list.clusterBy(paras, func.peq2('parentNode'));
  3119. // paragraph to list
  3120. if (list.find(paras, dom.isPurePara)) {
  3121. var wrappedParas = [];
  3122. $.each(clustereds, function (idx, paras) {
  3123. wrappedParas = wrappedParas.concat(self.wrapList(paras, listName));
  3124. });
  3125. paras = wrappedParas;
  3126. // list to paragraph or change list style
  3127. } else {
  3128. var diffLists = rng.nodes(dom.isList, {
  3129. includeAncestor: true
  3130. }).filter(function (listNode) {
  3131. return !$.nodeName(listNode, listName);
  3132. });
  3133. if (diffLists.length) {
  3134. $.each(diffLists, function (idx, listNode) {
  3135. dom.replace(listNode, listName);
  3136. });
  3137. } else {
  3138. paras = this.releaseList(clustereds, true);
  3139. }
  3140. }
  3141. range.createFromParaBookmark(bookmark, paras).select();
  3142. };
  3143. /**
  3144. * @param {Node[]} paras
  3145. * @param {String} listName
  3146. * @return {Node[]}
  3147. */
  3148. this.wrapList = function (paras, listName) {
  3149. var head = list.head(paras);
  3150. var last = list.last(paras);
  3151. var prevList = dom.isList(head.previousSibling) && head.previousSibling;
  3152. var nextList = dom.isList(last.nextSibling) && last.nextSibling;
  3153. var listNode = prevList || dom.insertAfter(dom.create(listName || 'UL'), last);
  3154. // P to LI
  3155. paras = paras.map(function (para) {
  3156. return dom.isPurePara(para) ? dom.replace(para, 'LI') : para;
  3157. });
  3158. // append to list(<ul>, <ol>)
  3159. dom.appendChildNodes(listNode, paras);
  3160. if (nextList) {
  3161. dom.appendChildNodes(listNode, list.from(nextList.childNodes));
  3162. dom.remove(nextList);
  3163. }
  3164. return paras;
  3165. };
  3166. /**
  3167. * @method releaseList
  3168. *
  3169. * @param {Array[]} clustereds
  3170. * @param {Boolean} isEscapseToBody
  3171. * @return {Node[]}
  3172. */
  3173. this.releaseList = function (clustereds, isEscapseToBody) {
  3174. var releasedParas = [];
  3175. $.each(clustereds, function (idx, paras) {
  3176. var head = list.head(paras);
  3177. var last = list.last(paras);
  3178. var headList = isEscapseToBody ? dom.lastAncestor(head, dom.isList) :
  3179. head.parentNode;
  3180. var lastList = headList.childNodes.length > 1 ? dom.splitTree(headList, {
  3181. node: last.parentNode,
  3182. offset: dom.position(last) + 1
  3183. }, {
  3184. isSkipPaddingBlankHTML: true
  3185. }) : null;
  3186. var middleList = dom.splitTree(headList, {
  3187. node: head.parentNode,
  3188. offset: dom.position(head)
  3189. }, {
  3190. isSkipPaddingBlankHTML: true
  3191. });
  3192. paras = isEscapseToBody ? dom.listDescendant(middleList, dom.isLi) :
  3193. list.from(middleList.childNodes).filter(dom.isLi);
  3194. // LI to P
  3195. if (isEscapseToBody || !dom.isList(headList.parentNode)) {
  3196. paras = paras.map(function (para) {
  3197. return dom.replace(para, 'P');
  3198. });
  3199. }
  3200. $.each(list.from(paras).reverse(), function (idx, para) {
  3201. dom.insertAfter(para, headList);
  3202. });
  3203. // remove empty lists
  3204. var rootLists = list.compact([headList, middleList, lastList]);
  3205. $.each(rootLists, function (idx, rootList) {
  3206. var listNodes = [rootList].concat(dom.listDescendant(rootList, dom.isList));
  3207. $.each(listNodes.reverse(), function (idx, listNode) {
  3208. if (!dom.nodeLength(listNode)) {
  3209. dom.remove(listNode, true);
  3210. }
  3211. });
  3212. });
  3213. releasedParas = releasedParas.concat(paras);
  3214. });
  3215. return releasedParas;
  3216. };
  3217. };
  3218. /**
  3219. * @class editing.Typing
  3220. *
  3221. * Typing
  3222. *
  3223. */
  3224. var Typing = function () {
  3225. // a Bullet instance to toggle lists off
  3226. var bullet = new Bullet();
  3227. /**
  3228. * insert tab
  3229. *
  3230. * @param {WrappedRange} rng
  3231. * @param {Number} tabsize
  3232. */
  3233. this.insertTab = function (rng, tabsize) {
  3234. var tab = dom.createText(new Array(tabsize + 1).join(dom.NBSP_CHAR));
  3235. rng = rng.deleteContents();
  3236. rng.insertNode(tab, true);
  3237. rng = range.create(tab, tabsize);
  3238. rng.select();
  3239. };
  3240. /**
  3241. * insert paragraph
  3242. */
  3243. this.insertParagraph = function (editable) {
  3244. var rng = range.create(editable);
  3245. // deleteContents on range.
  3246. rng = rng.deleteContents();
  3247. // Wrap range if it needs to be wrapped by paragraph
  3248. rng = rng.wrapBodyInlineWithPara();
  3249. // finding paragraph
  3250. var splitRoot = dom.ancestor(rng.sc, dom.isPara);
  3251. var nextPara;
  3252. // on paragraph: split paragraph
  3253. if (splitRoot) {
  3254. // if it is an empty line with li
  3255. if (dom.isEmpty(splitRoot) && dom.isLi(splitRoot)) {
  3256. // toogle UL/OL and escape
  3257. bullet.toggleList(splitRoot.parentNode.nodeName);
  3258. return;
  3259. // if it is an empty line with para on blockquote
  3260. } else if (dom.isEmpty(splitRoot) && dom.isPara(splitRoot) && dom.isBlockquote(splitRoot.parentNode)) {
  3261. // escape blockquote
  3262. dom.insertAfter(splitRoot, splitRoot.parentNode);
  3263. nextPara = splitRoot;
  3264. // if new line has content (not a line break)
  3265. } else {
  3266. nextPara = dom.splitTree(splitRoot, rng.getStartPoint());
  3267. var emptyAnchors = dom.listDescendant(splitRoot, dom.isEmptyAnchor);
  3268. emptyAnchors = emptyAnchors.concat(dom.listDescendant(nextPara, dom.isEmptyAnchor));
  3269. $.each(emptyAnchors, function (idx, anchor) {
  3270. dom.remove(anchor);
  3271. });
  3272. // replace empty heading, pre or custom-made styleTag with P tag
  3273. if ((dom.isHeading(nextPara) || dom.isPre(nextPara) || dom.isCustomStyleTag(nextPara)) && dom.isEmpty(nextPara)) {
  3274. nextPara = dom.replace(nextPara, 'p');
  3275. }
  3276. }
  3277. // no paragraph: insert empty paragraph
  3278. } else {
  3279. var next = rng.sc.childNodes[rng.so];
  3280. nextPara = $(dom.emptyPara)[0];
  3281. if (next) {
  3282. rng.sc.insertBefore(nextPara, next);
  3283. } else {
  3284. rng.sc.appendChild(nextPara);
  3285. }
  3286. }
  3287. range.create(nextPara, 0).normalize().select().scrollIntoView(editable);
  3288. };
  3289. };
  3290. /**
  3291. * @class editing.Table
  3292. *
  3293. * Table
  3294. *
  3295. */
  3296. var Table = function () {
  3297. /**
  3298. * handle tab key
  3299. *
  3300. * @param {WrappedRange} rng
  3301. * @param {Boolean} isShift
  3302. */
  3303. this.tab = function (rng, isShift) {
  3304. var cell = dom.ancestor(rng.commonAncestor(), dom.isCell);
  3305. var table = dom.ancestor(cell, dom.isTable);
  3306. var cells = dom.listDescendant(table, dom.isCell);
  3307. var nextCell = list[isShift ? 'prev' : 'next'](cells, cell);
  3308. if (nextCell) {
  3309. range.create(nextCell, 0).select();
  3310. }
  3311. };
  3312. /**
  3313. * create empty table element
  3314. *
  3315. * @param {Number} rowCount
  3316. * @param {Number} colCount
  3317. * @return {Node}
  3318. */
  3319. this.createTable = function (colCount, rowCount, options) {
  3320. var tds = [], tdHTML;
  3321. for (var idxCol = 0; idxCol < colCount; idxCol++) {
  3322. tds.push('<td>' + dom.blank + '</td>');
  3323. }
  3324. tdHTML = tds.join('');
  3325. var trs = [], trHTML;
  3326. for (var idxRow = 0; idxRow < rowCount; idxRow++) {
  3327. trs.push('<tr>' + tdHTML + '</tr>');
  3328. }
  3329. trHTML = trs.join('');
  3330. var $table = $('<table>' + trHTML + '</table>');
  3331. if (options && options.tableClassName) {
  3332. $table.addClass(options.tableClassName);
  3333. }
  3334. return $table[0];
  3335. };
  3336. };
  3337. var KEY_BOGUS = 'bogus';
  3338. /**
  3339. * @class Editor
  3340. */
  3341. var Editor = function (context) {
  3342. var self = this;
  3343. var $note = context.layoutInfo.note;
  3344. var $editor = context.layoutInfo.editor;
  3345. var $editable = context.layoutInfo.editable;
  3346. var options = context.options;
  3347. var lang = options.langInfo;
  3348. var editable = $editable[0];
  3349. var lastRange = null;
  3350. var style = new Style();
  3351. var table = new Table();
  3352. var typing = new Typing();
  3353. var bullet = new Bullet();
  3354. var history = new History($editable);
  3355. this.initialize = function () {
  3356. // bind custom events
  3357. $editable.on('keydown', function (event) {
  3358. if (event.keyCode === key.code.ENTER) {
  3359. context.triggerEvent('enter', event);
  3360. }
  3361. context.triggerEvent('keydown', event);
  3362. if (!event.isDefaultPrevented()) {
  3363. if (options.shortcuts) {
  3364. self.handleKeyMap(event);
  3365. } else {
  3366. self.preventDefaultEditableShortCuts(event);
  3367. }
  3368. }
  3369. }).on('keyup', function (event) {
  3370. context.triggerEvent('keyup', event);
  3371. }).on('focus', function (event) {
  3372. context.triggerEvent('focus', event);
  3373. }).on('blur', function (event) {
  3374. context.triggerEvent('blur', event);
  3375. }).on('mousedown', function (event) {
  3376. context.triggerEvent('mousedown', event);
  3377. }).on('mouseup', function (event) {
  3378. context.triggerEvent('mouseup', event);
  3379. }).on('scroll', function (event) {
  3380. context.triggerEvent('scroll', event);
  3381. }).on('paste', function (event) {
  3382. context.triggerEvent('paste', event);
  3383. });
  3384. // init content before set event
  3385. $editable.html(dom.html($note) || dom.emptyPara);
  3386. // [workaround] IE doesn't have input events for contentEditable
  3387. // - see: https://goo.gl/4bfIvA
  3388. var changeEventName = agent.isMSIE ? 'DOMCharacterDataModified DOMSubtreeModified DOMNodeInserted' : 'input';
  3389. $editable.on(changeEventName, func.debounce(function () {
  3390. context.triggerEvent('change', $editable.html());
  3391. }, 250));
  3392. $editor.on('focusin', function (event) {
  3393. context.triggerEvent('focusin', event);
  3394. }).on('focusout', function (event) {
  3395. context.triggerEvent('focusout', event);
  3396. });
  3397. if (!options.airMode) {
  3398. if (options.width) {
  3399. $editor.outerWidth(options.width);
  3400. }
  3401. if (options.height) {
  3402. $editable.outerHeight(options.height);
  3403. }
  3404. if (options.maxHeight) {
  3405. $editable.css('max-height', options.maxHeight);
  3406. }
  3407. if (options.minHeight) {
  3408. $editable.css('min-height', options.minHeight);
  3409. }
  3410. }
  3411. history.recordUndo();
  3412. };
  3413. this.destroy = function () {
  3414. $editable.off();
  3415. };
  3416. this.handleKeyMap = function (event) {
  3417. var keyMap = options.keyMap[agent.isMac ? 'mac' : 'pc'];
  3418. var keys = [];
  3419. if (event.metaKey) { keys.push('CMD'); }
  3420. if (event.ctrlKey && !event.altKey) { keys.push('CTRL'); }
  3421. if (event.shiftKey) { keys.push('SHIFT'); }
  3422. var keyName = key.nameFromCode[event.keyCode];
  3423. if (keyName) {
  3424. keys.push(keyName);
  3425. }
  3426. var eventName = keyMap[keys.join('+')];
  3427. if (eventName) {
  3428. event.preventDefault();
  3429. context.invoke(eventName);
  3430. } else if (key.isEdit(event.keyCode)) {
  3431. this.afterCommand();
  3432. }
  3433. };
  3434. this.preventDefaultEditableShortCuts = function (event) {
  3435. // B(Bold, 66) / I(Italic, 73) / U(Underline, 85)
  3436. if ((event.ctrlKey || event.metaKey) &&
  3437. list.contains([66, 73, 85], event.keyCode)) {
  3438. event.preventDefault();
  3439. }
  3440. };
  3441. /**
  3442. * create range
  3443. * @return {WrappedRange}
  3444. */
  3445. this.createRange = function () {
  3446. this.focus();
  3447. return range.create(editable);
  3448. };
  3449. /**
  3450. * saveRange
  3451. *
  3452. * save current range
  3453. *
  3454. * @param {Boolean} [thenCollapse=false]
  3455. */
  3456. this.saveRange = function (thenCollapse) {
  3457. lastRange = this.createRange();
  3458. if (thenCollapse) {
  3459. lastRange.collapse().select();
  3460. }
  3461. };
  3462. /**
  3463. * restoreRange
  3464. *
  3465. * restore lately range
  3466. */
  3467. this.restoreRange = function () {
  3468. if (lastRange) {
  3469. lastRange.select();
  3470. this.focus();
  3471. }
  3472. };
  3473. this.saveTarget = function (node) {
  3474. $editable.data('target', node);
  3475. };
  3476. this.clearTarget = function () {
  3477. $editable.removeData('target');
  3478. };
  3479. this.restoreTarget = function () {
  3480. return $editable.data('target');
  3481. };
  3482. /**
  3483. * currentStyle
  3484. *
  3485. * current style
  3486. * @return {Object|Boolean} unfocus
  3487. */
  3488. this.currentStyle = function () {
  3489. var rng = range.create();
  3490. if (rng) {
  3491. rng = rng.normalize();
  3492. }
  3493. return rng ? style.current(rng) : style.fromNode($editable);
  3494. };
  3495. /**
  3496. * style from node
  3497. *
  3498. * @param {jQuery} $node
  3499. * @return {Object}
  3500. */
  3501. this.styleFromNode = function ($node) {
  3502. return style.fromNode($node);
  3503. };
  3504. /**
  3505. * undo
  3506. */
  3507. this.undo = function () {
  3508. context.triggerEvent('before.command', $editable.html());
  3509. history.undo();
  3510. context.triggerEvent('change', $editable.html());
  3511. };
  3512. context.memo('help.undo', lang.help.undo);
  3513. /**
  3514. * redo
  3515. */
  3516. this.redo = function () {
  3517. context.triggerEvent('before.command', $editable.html());
  3518. history.redo();
  3519. context.triggerEvent('change', $editable.html());
  3520. };
  3521. context.memo('help.redo', lang.help.redo);
  3522. /**
  3523. * before command
  3524. */
  3525. var beforeCommand = this.beforeCommand = function () {
  3526. context.triggerEvent('before.command', $editable.html());
  3527. // keep focus on editable before command execution
  3528. self.focus();
  3529. };
  3530. /**
  3531. * after command
  3532. * @param {Boolean} isPreventTrigger
  3533. */
  3534. var afterCommand = this.afterCommand = function (isPreventTrigger) {
  3535. history.recordUndo();
  3536. if (!isPreventTrigger) {
  3537. context.triggerEvent('change', $editable.html());
  3538. }
  3539. };
  3540. /* jshint ignore:start */
  3541. // native commands(with execCommand), generate function for execCommand
  3542. var commands = ['bold', 'italic', 'underline', 'strikethrough', 'superscript', 'subscript',
  3543. 'justifyLeft', 'justifyCenter', 'justifyRight', 'justifyFull',
  3544. 'formatBlock', 'removeFormat',
  3545. 'backColor', 'foreColor', 'fontName'];
  3546. for (var idx = 0, len = commands.length; idx < len; idx ++) {
  3547. this[commands[idx]] = (function (sCmd) {
  3548. return function (value) {
  3549. beforeCommand();
  3550. document.execCommand(sCmd, false, value);
  3551. afterCommand(true);
  3552. };
  3553. })(commands[idx]);
  3554. context.memo('help.' + commands[idx], lang.help[commands[idx]]);
  3555. }
  3556. /* jshint ignore:end */
  3557. /**
  3558. * handle tab key
  3559. */
  3560. this.tab = function () {
  3561. var rng = this.createRange();
  3562. if (rng.isCollapsed() && rng.isOnCell()) {
  3563. table.tab(rng);
  3564. } else {
  3565. beforeCommand();
  3566. typing.insertTab(rng, options.tabSize);
  3567. afterCommand();
  3568. }
  3569. };
  3570. context.memo('help.tab', lang.help.tab);
  3571. /**
  3572. * handle shift+tab key
  3573. */
  3574. this.untab = function () {
  3575. var rng = this.createRange();
  3576. if (rng.isCollapsed() && rng.isOnCell()) {
  3577. table.tab(rng, true);
  3578. }
  3579. };
  3580. context.memo('help.untab', lang.help.untab);
  3581. /**
  3582. * run given function between beforeCommand and afterCommand
  3583. */
  3584. this.wrapCommand = function (fn) {
  3585. return function () {
  3586. beforeCommand();
  3587. fn.apply(self, arguments);
  3588. afterCommand();
  3589. };
  3590. };
  3591. /**
  3592. * insert paragraph
  3593. */
  3594. this.insertParagraph = this.wrapCommand(function () {
  3595. typing.insertParagraph(editable);
  3596. });
  3597. context.memo('help.insertParagraph', lang.help.insertParagraph);
  3598. this.insertOrderedList = this.wrapCommand(function () {
  3599. bullet.insertOrderedList(editable);
  3600. });
  3601. context.memo('help.insertOrderedList', lang.help.insertOrderedList);
  3602. this.insertUnorderedList = this.wrapCommand(function () {
  3603. bullet.insertUnorderedList(editable);
  3604. });
  3605. context.memo('help.insertUnorderedList', lang.help.insertUnorderedList);
  3606. this.indent = this.wrapCommand(function () {
  3607. bullet.indent(editable);
  3608. });
  3609. context.memo('help.indent', lang.help.indent);
  3610. this.outdent = this.wrapCommand(function () {
  3611. bullet.outdent(editable);
  3612. });
  3613. context.memo('help.outdent', lang.help.outdent);
  3614. /**
  3615. * insert image
  3616. *
  3617. * @param {String} src
  3618. * @param {String|Function} param
  3619. * @return {Promise}
  3620. */
  3621. this.insertImage = function (src, param) {
  3622. return async.createImage(src, param).then(function ($image) {
  3623. beforeCommand();
  3624. if (typeof param === 'function') {
  3625. param($image);
  3626. } else {
  3627. if (typeof param === 'string') {
  3628. $image.attr('data-filename', param);
  3629. }
  3630. $image.css('width', Math.min($editable.width(), $image.width()));
  3631. }
  3632. $image.show();
  3633. range.create(editable).insertNode($image[0]);
  3634. range.createFromNodeAfter($image[0]).select();
  3635. afterCommand();
  3636. }).fail(function (e) {
  3637. context.triggerEvent('image.upload.error', e);
  3638. });
  3639. };
  3640. /**
  3641. * insertImages
  3642. * @param {File[]} files
  3643. */
  3644. this.insertImages = function (files) {
  3645. $.each(files, function (idx, file) {
  3646. var filename = file.name;
  3647. if (options.maximumImageFileSize && options.maximumImageFileSize < file.size) {
  3648. context.triggerEvent('image.upload.error', lang.image.maximumFileSizeError);
  3649. } else {
  3650. async.readFileAsDataURL(file).then(function (dataURL) {
  3651. return self.insertImage(dataURL, filename);
  3652. }).fail(function () {
  3653. context.triggerEvent('image.upload.error');
  3654. });
  3655. }
  3656. });
  3657. };
  3658. /**
  3659. * insertImagesOrCallback
  3660. * @param {File[]} files
  3661. */
  3662. this.insertImagesOrCallback = function (files) {
  3663. var callbacks = options.callbacks;
  3664. // If onImageUpload options setted
  3665. if (callbacks.onImageUpload) {
  3666. context.triggerEvent('image.upload', files);
  3667. // else insert Image as dataURL
  3668. } else {
  3669. this.insertImages(files);
  3670. }
  3671. };
  3672. /**
  3673. * insertNode
  3674. * insert node
  3675. * @param {Node} node
  3676. */
  3677. this.insertNode = this.wrapCommand(function (node) {
  3678. var rng = this.createRange();
  3679. rng.insertNode(node);
  3680. range.createFromNodeAfter(node).select();
  3681. });
  3682. /**
  3683. * insert text
  3684. * @param {String} text
  3685. */
  3686. this.insertText = this.wrapCommand(function (text) {
  3687. var rng = this.createRange();
  3688. var textNode = rng.insertNode(dom.createText(text));
  3689. range.create(textNode, dom.nodeLength(textNode)).select();
  3690. });
  3691. /**
  3692. * return selected plain text
  3693. * @return {String} text
  3694. */
  3695. this.getSelectedText = function () {
  3696. var rng = this.createRange();
  3697. // if range on anchor, expand range with anchor
  3698. if (rng.isOnAnchor()) {
  3699. rng = range.createFromNode(dom.ancestor(rng.sc, dom.isAnchor));
  3700. }
  3701. return rng.toString();
  3702. };
  3703. /**
  3704. * paste HTML
  3705. * @param {String} markup
  3706. */
  3707. this.pasteHTML = this.wrapCommand(function (markup) {
  3708. var contents = this.createRange().pasteHTML(markup);
  3709. range.createFromNodeAfter(list.last(contents)).select();
  3710. });
  3711. /**
  3712. * formatBlock
  3713. *
  3714. * @param {String} tagName
  3715. */
  3716. this.formatBlock = this.wrapCommand(function (tagName, $target) {
  3717. var onApplyCustomStyle = context.options.callbacks.onApplyCustomStyle;
  3718. if (onApplyCustomStyle) {
  3719. onApplyCustomStyle.call(this, $target, context, this.onFormatBlock);
  3720. } else {
  3721. this.onFormatBlock(tagName);
  3722. }
  3723. });
  3724. this.onFormatBlock = function (tagName) {
  3725. // [workaround] for MSIE, IE need `<`
  3726. tagName = agent.isMSIE ? '<' + tagName + '>' : tagName;
  3727. document.execCommand('FormatBlock', false, tagName);
  3728. };
  3729. this.formatPara = function () {
  3730. this.formatBlock('P');
  3731. };
  3732. context.memo('help.formatPara', lang.help.formatPara);
  3733. /* jshint ignore:start */
  3734. for (var idx = 1; idx <= 6; idx ++) {
  3735. this['formatH' + idx] = function (idx) {
  3736. return function () {
  3737. this.formatBlock('H' + idx);
  3738. };
  3739. }(idx);
  3740. context.memo('help.formatH'+idx, lang.help['formatH' + idx]);
  3741. };
  3742. /* jshint ignore:end */
  3743. /**
  3744. * fontSize
  3745. *
  3746. * @param {String} value - px
  3747. */
  3748. this.fontSize = function (value) {
  3749. var rng = this.createRange();
  3750. if (rng && rng.isCollapsed()) {
  3751. var spans = style.styleNodes(rng);
  3752. var firstSpan = list.head(spans);
  3753. $(spans).css({
  3754. 'font-size': value + 'px'
  3755. });
  3756. // [workaround] added styled bogus span for style
  3757. // - also bogus character needed for cursor position
  3758. if (firstSpan && !dom.nodeLength(firstSpan)) {
  3759. firstSpan.innerHTML = dom.ZERO_WIDTH_NBSP_CHAR;
  3760. range.createFromNodeAfter(firstSpan.firstChild).select();
  3761. $editable.data(KEY_BOGUS, firstSpan);
  3762. }
  3763. } else {
  3764. beforeCommand();
  3765. $(style.styleNodes(rng)).css({
  3766. 'font-size': value + 'px'
  3767. });
  3768. afterCommand();
  3769. }
  3770. };
  3771. /**
  3772. * insert horizontal rule
  3773. */
  3774. this.insertHorizontalRule = this.wrapCommand(function () {
  3775. var hrNode = this.createRange().insertNode(dom.create('HR'));
  3776. if (hrNode.nextSibling) {
  3777. range.create(hrNode.nextSibling, 0).normalize().select();
  3778. }
  3779. });
  3780. context.memo('help.insertHorizontalRule', lang.help.insertHorizontalRule);
  3781. /**
  3782. * remove bogus node and character
  3783. */
  3784. this.removeBogus = function () {
  3785. var bogusNode = $editable.data(KEY_BOGUS);
  3786. if (!bogusNode) {
  3787. return;
  3788. }
  3789. var textNode = list.find(list.from(bogusNode.childNodes), dom.isText);
  3790. var bogusCharIdx = textNode.nodeValue.indexOf(dom.ZERO_WIDTH_NBSP_CHAR);
  3791. if (bogusCharIdx !== -1) {
  3792. textNode.deleteData(bogusCharIdx, 1);
  3793. }
  3794. if (dom.isEmpty(bogusNode)) {
  3795. dom.remove(bogusNode);
  3796. }
  3797. $editable.removeData(KEY_BOGUS);
  3798. };
  3799. /**
  3800. * lineHeight
  3801. * @param {String} value
  3802. */
  3803. this.lineHeight = this.wrapCommand(function (value) {
  3804. style.stylePara(this.createRange(), {
  3805. lineHeight: value
  3806. });
  3807. });
  3808. /**
  3809. * unlink
  3810. *
  3811. * @type command
  3812. */
  3813. this.unlink = function () {
  3814. var rng = this.createRange();
  3815. if (rng.isOnAnchor()) {
  3816. var anchor = dom.ancestor(rng.sc, dom.isAnchor);
  3817. rng = range.createFromNode(anchor);
  3818. rng.select();
  3819. beforeCommand();
  3820. document.execCommand('unlink');
  3821. afterCommand();
  3822. }
  3823. };
  3824. /**
  3825. * create link (command)
  3826. *
  3827. * @param {Object} linkInfo
  3828. */
  3829. this.createLink = this.wrapCommand(function (linkInfo) {
  3830. var linkUrl = linkInfo.url;
  3831. var linkText = linkInfo.text;
  3832. var isNewWindow = linkInfo.isNewWindow;
  3833. var rng = linkInfo.range || this.createRange();
  3834. var isTextChanged = rng.toString() !== linkText;
  3835. // handle spaced urls from input
  3836. if (typeof linkUrl === 'string') {
  3837. linkUrl = linkUrl.trim();
  3838. }
  3839. if (options.onCreateLink) {
  3840. linkUrl = options.onCreateLink(linkUrl);
  3841. } else {
  3842. // if url doesn't match an URL schema, set http:// as default
  3843. linkUrl = /^[A-Za-z][A-Za-z0-9+-.]*\:[\/\/]?/.test(linkUrl) ?
  3844. linkUrl : 'http://' + linkUrl;
  3845. }
  3846. var anchors = [];
  3847. if (isTextChanged) {
  3848. rng = rng.deleteContents();
  3849. var anchor = rng.insertNode($('<A>' + linkText + '</A>')[0]);
  3850. anchors.push(anchor);
  3851. } else {
  3852. anchors = style.styleNodes(rng, {
  3853. nodeName: 'A',
  3854. expandClosestSibling: true,
  3855. onlyPartialContains: true
  3856. });
  3857. }
  3858. $.each(anchors, function (idx, anchor) {
  3859. $(anchor).attr('href', linkUrl);
  3860. if (isNewWindow) {
  3861. $(anchor).attr('target', '_blank');
  3862. } else {
  3863. $(anchor).removeAttr('target');
  3864. }
  3865. });
  3866. var startRange = range.createFromNodeBefore(list.head(anchors));
  3867. var startPoint = startRange.getStartPoint();
  3868. var endRange = range.createFromNodeAfter(list.last(anchors));
  3869. var endPoint = endRange.getEndPoint();
  3870. range.create(
  3871. startPoint.node,
  3872. startPoint.offset,
  3873. endPoint.node,
  3874. endPoint.offset
  3875. ).select();
  3876. });
  3877. /**
  3878. * returns link info
  3879. *
  3880. * @return {Object}
  3881. * @return {WrappedRange} return.range
  3882. * @return {String} return.text
  3883. * @return {Boolean} [return.isNewWindow=true]
  3884. * @return {String} [return.url=""]
  3885. */
  3886. this.getLinkInfo = function () {
  3887. var rng = this.createRange().expand(dom.isAnchor);
  3888. // Get the first anchor on range(for edit).
  3889. var $anchor = $(list.head(rng.nodes(dom.isAnchor)));
  3890. var linkInfo = {
  3891. range: rng,
  3892. text: rng.toString(),
  3893. url: $anchor.length ? $anchor.attr('href') : ''
  3894. };
  3895. // Define isNewWindow when anchor exists.
  3896. if ($anchor.length) {
  3897. linkInfo.isNewWindow = $anchor.attr('target') === '_blank';
  3898. }
  3899. return linkInfo;
  3900. };
  3901. /**
  3902. * setting color
  3903. *
  3904. * @param {Object} sObjColor color code
  3905. * @param {String} sObjColor.foreColor foreground color
  3906. * @param {String} sObjColor.backColor background color
  3907. */
  3908. this.color = this.wrapCommand(function (colorInfo) {
  3909. var foreColor = colorInfo.foreColor;
  3910. var backColor = colorInfo.backColor;
  3911. if (foreColor) { document.execCommand('foreColor', false, foreColor); }
  3912. if (backColor) { document.execCommand('backColor', false, backColor); }
  3913. });
  3914. /**
  3915. * insert Table
  3916. *
  3917. * @param {String} dimension of table (ex : "5x5")
  3918. */
  3919. this.insertTable = this.wrapCommand(function (dim) {
  3920. var dimension = dim.split('x');
  3921. var rng = this.createRange().deleteContents();
  3922. rng.insertNode(table.createTable(dimension[0], dimension[1], options));
  3923. });
  3924. /**
  3925. * float me
  3926. *
  3927. * @param {String} value
  3928. */
  3929. this.floatMe = this.wrapCommand(function (value) {
  3930. var $target = $(this.restoreTarget());
  3931. $target.css('float', value);
  3932. });
  3933. /**
  3934. * resize overlay element
  3935. * @param {String} value
  3936. */
  3937. this.resize = this.wrapCommand(function (value) {
  3938. var $target = $(this.restoreTarget());
  3939. $target.css({
  3940. width: value * 100 + '%',
  3941. height: ''
  3942. });
  3943. });
  3944. /**
  3945. * @param {Position} pos
  3946. * @param {jQuery} $target - target element
  3947. * @param {Boolean} [bKeepRatio] - keep ratio
  3948. */
  3949. this.resizeTo = function (pos, $target, bKeepRatio) {
  3950. var imageSize;
  3951. if (bKeepRatio) {
  3952. var newRatio = pos.y / pos.x;
  3953. var ratio = $target.data('ratio');
  3954. imageSize = {
  3955. width: ratio > newRatio ? pos.x : pos.y / ratio,
  3956. height: ratio > newRatio ? pos.x * ratio : pos.y
  3957. };
  3958. } else {
  3959. imageSize = {
  3960. width: pos.x,
  3961. height: pos.y
  3962. };
  3963. }
  3964. $target.css(imageSize);
  3965. };
  3966. /**
  3967. * remove media object
  3968. */
  3969. this.removeMedia = this.wrapCommand(function () {
  3970. var $target = $(this.restoreTarget()).detach();
  3971. context.triggerEvent('media.delete', $target, $editable);
  3972. });
  3973. /**
  3974. * returns whether editable area has focus or not.
  3975. */
  3976. this.hasFocus = function () {
  3977. return $editable.is(':focus');
  3978. };
  3979. /**
  3980. * set focus
  3981. */
  3982. this.focus = function () {
  3983. // [workaround] Screen will move when page is scolled in IE.
  3984. // - do focus when not focused
  3985. if (!this.hasFocus()) {
  3986. $editable.focus();
  3987. }
  3988. };
  3989. /**
  3990. * returns whether contents is empty or not.
  3991. * @return {Boolean}
  3992. */
  3993. this.isEmpty = function () {
  3994. return dom.isEmpty($editable[0]) || dom.emptyPara === $editable.html();
  3995. };
  3996. /**
  3997. * Removes all contents and restores the editable instance to an _emptyPara_.
  3998. */
  3999. this.empty = function () {
  4000. context.invoke('code', dom.emptyPara);
  4001. };
  4002. };
  4003. var Clipboard = function (context) {
  4004. var self = this;
  4005. var $editable = context.layoutInfo.editable;
  4006. this.events = {
  4007. 'summernote.keydown': function (we, e) {
  4008. if (self.needKeydownHook()) {
  4009. if ((e.ctrlKey || e.metaKey) && e.keyCode === key.code.V) {
  4010. context.invoke('editor.saveRange');
  4011. self.$paste.focus();
  4012. setTimeout(function () {
  4013. self.pasteByHook();
  4014. }, 0);
  4015. }
  4016. }
  4017. }
  4018. };
  4019. this.needKeydownHook = function () {
  4020. return (agent.isMSIE && agent.browserVersion > 10) || agent.isFF;
  4021. };
  4022. this.initialize = function () {
  4023. // [workaround] getting image from clipboard
  4024. // - IE11 and Firefox: CTRL+v hook
  4025. // - Webkit: event.clipboardData
  4026. if (this.needKeydownHook()) {
  4027. this.$paste = $('<div tabindex="-1" />').attr('contenteditable', true).css({
  4028. position: 'absolute',
  4029. left: -100000,
  4030. opacity: 0
  4031. });
  4032. $editable.before(this.$paste);
  4033. this.$paste.on('paste', function (event) {
  4034. context.triggerEvent('paste', event);
  4035. });
  4036. } else {
  4037. $editable.on('paste', this.pasteByEvent);
  4038. }
  4039. };
  4040. this.destroy = function () {
  4041. if (this.needKeydownHook()) {
  4042. this.$paste.remove();
  4043. this.$paste = null;
  4044. }
  4045. };
  4046. this.pasteByHook = function () {
  4047. var node = this.$paste[0].firstChild;
  4048. var src = node && node.src;
  4049. if (dom.isImg(node) && src.indexOf('data:') === 0) {
  4050. var decodedData = atob(node.src.split(',')[1]);
  4051. var array = new Uint8Array(decodedData.length);
  4052. for (var i = 0; i < decodedData.length; i++) {
  4053. array[i] = decodedData.charCodeAt(i);
  4054. }
  4055. var blob = new Blob([array], { type: 'image/png' });
  4056. blob.name = 'clipboard.png';
  4057. context.invoke('editor.restoreRange');
  4058. context.invoke('editor.focus');
  4059. context.invoke('editor.insertImagesOrCallback', [blob]);
  4060. } else {
  4061. var pasteContent = $('<div />').html(this.$paste.html()).html();
  4062. context.invoke('editor.restoreRange');
  4063. context.invoke('editor.focus');
  4064. if (pasteContent) {
  4065. context.invoke('editor.pasteHTML', pasteContent);
  4066. }
  4067. }
  4068. this.$paste.empty();
  4069. };
  4070. /**
  4071. * paste by clipboard event
  4072. *
  4073. * @param {Event} event
  4074. */
  4075. this.pasteByEvent = function (event) {
  4076. var clipboardData = event.originalEvent.clipboardData;
  4077. if (clipboardData && clipboardData.items && clipboardData.items.length) {
  4078. var item = list.head(clipboardData.items);
  4079. if (item.kind === 'file' && item.type.indexOf('image/') !== -1) {
  4080. context.invoke('editor.insertImagesOrCallback', [item.getAsFile()]);
  4081. }
  4082. context.invoke('editor.afterCommand');
  4083. }
  4084. };
  4085. };
  4086. var Dropzone = function (context) {
  4087. var $document = $(document);
  4088. var $editor = context.layoutInfo.editor;
  4089. var $editable = context.layoutInfo.editable;
  4090. var options = context.options;
  4091. var lang = options.langInfo;
  4092. var documentEventHandlers = {};
  4093. var $dropzone = $([
  4094. '<div class="note-dropzone">',
  4095. ' <div class="note-dropzone-message"/>',
  4096. '</div>'
  4097. ].join('')).prependTo($editor);
  4098. var detachDocumentEvent = function () {
  4099. Object.keys(documentEventHandlers).forEach(function (key) {
  4100. $document.off(key.substr(2).toLowerCase(), documentEventHandlers[key]);
  4101. });
  4102. documentEventHandlers = {};
  4103. };
  4104. /**
  4105. * attach Drag and Drop Events
  4106. */
  4107. this.initialize = function () {
  4108. if (options.disableDragAndDrop) {
  4109. // prevent default drop event
  4110. documentEventHandlers.onDrop = function (e) {
  4111. e.preventDefault();
  4112. };
  4113. $document.on('drop', documentEventHandlers.onDrop);
  4114. } else {
  4115. this.attachDragAndDropEvent();
  4116. }
  4117. };
  4118. /**
  4119. * attach Drag and Drop Events
  4120. */
  4121. this.attachDragAndDropEvent = function () {
  4122. var collection = $(),
  4123. $dropzoneMessage = $dropzone.find('.note-dropzone-message');
  4124. documentEventHandlers.onDragenter = function (e) {
  4125. var isCodeview = context.invoke('codeview.isActivated');
  4126. var hasEditorSize = $editor.width() > 0 && $editor.height() > 0;
  4127. if (!isCodeview && !collection.length && hasEditorSize) {
  4128. $editor.addClass('dragover');
  4129. $dropzone.width($editor.width());
  4130. $dropzone.height($editor.height());
  4131. $dropzoneMessage.text(lang.image.dragImageHere);
  4132. }
  4133. collection = collection.add(e.target);
  4134. };
  4135. documentEventHandlers.onDragleave = function (e) {
  4136. collection = collection.not(e.target);
  4137. if (!collection.length) {
  4138. $editor.removeClass('dragover');
  4139. }
  4140. };
  4141. documentEventHandlers.onDrop = function () {
  4142. collection = $();
  4143. $editor.removeClass('dragover');
  4144. };
  4145. // show dropzone on dragenter when dragging a object to document
  4146. // -but only if the editor is visible, i.e. has a positive width and height
  4147. $document.on('dragenter', documentEventHandlers.onDragenter)
  4148. .on('dragleave', documentEventHandlers.onDragleave)
  4149. .on('drop', documentEventHandlers.onDrop);
  4150. // change dropzone's message on hover.
  4151. $dropzone.on('dragenter', function () {
  4152. $dropzone.addClass('hover');
  4153. $dropzoneMessage.text(lang.image.dropImage);
  4154. }).on('dragleave', function () {
  4155. $dropzone.removeClass('hover');
  4156. $dropzoneMessage.text(lang.image.dragImageHere);
  4157. });
  4158. // attach dropImage
  4159. $dropzone.on('drop', function (event) {
  4160. var dataTransfer = event.originalEvent.dataTransfer;
  4161. if (dataTransfer && dataTransfer.files && dataTransfer.files.length) {
  4162. event.preventDefault();
  4163. $editable.focus();
  4164. context.invoke('editor.insertImagesOrCallback', dataTransfer.files);
  4165. } else {
  4166. $.each(dataTransfer.types, function (idx, type) {
  4167. var content = dataTransfer.getData(type);
  4168. if (type.toLowerCase().indexOf('text') > -1) {
  4169. context.invoke('editor.pasteHTML', content);
  4170. } else {
  4171. $(content).each(function () {
  4172. context.invoke('editor.insertNode', this);
  4173. });
  4174. }
  4175. });
  4176. }
  4177. }).on('dragover', false); // prevent default dragover event
  4178. };
  4179. this.destroy = function () {
  4180. detachDocumentEvent();
  4181. };
  4182. };
  4183. var CodeMirror;
  4184. if (agent.hasCodeMirror) {
  4185. if (agent.isSupportAmd) {
  4186. require(['codemirror'], function (cm) {
  4187. CodeMirror = cm;
  4188. });
  4189. } else {
  4190. CodeMirror = window.CodeMirror;
  4191. }
  4192. }
  4193. /**
  4194. * @class Codeview
  4195. */
  4196. var Codeview = function (context) {
  4197. var $editor = context.layoutInfo.editor;
  4198. var $editable = context.layoutInfo.editable;
  4199. var $codable = context.layoutInfo.codable;
  4200. var options = context.options;
  4201. this.sync = function () {
  4202. var isCodeview = this.isActivated();
  4203. if (isCodeview && agent.hasCodeMirror) {
  4204. $codable.data('cmEditor').save();
  4205. }
  4206. };
  4207. /**
  4208. * @return {Boolean}
  4209. */
  4210. this.isActivated = function () {
  4211. return $editor.hasClass('codeview');
  4212. };
  4213. /**
  4214. * toggle codeview
  4215. */
  4216. this.toggle = function () {
  4217. if (this.isActivated()) {
  4218. this.deactivate();
  4219. } else {
  4220. this.activate();
  4221. }
  4222. context.triggerEvent('codeview.toggled');
  4223. };
  4224. /**
  4225. * activate code view
  4226. */
  4227. this.activate = function () {
  4228. $codable.val(dom.html($editable, options.prettifyHtml));
  4229. $codable.height($editable.height());
  4230. context.invoke('toolbar.updateCodeview', true);
  4231. $editor.addClass('codeview');
  4232. $codable.focus();
  4233. // activate CodeMirror as codable
  4234. if (agent.hasCodeMirror) {
  4235. var cmEditor = CodeMirror.fromTextArea($codable[0], options.codemirror);
  4236. // CodeMirror TernServer
  4237. if (options.codemirror.tern) {
  4238. var server = new CodeMirror.TernServer(options.codemirror.tern);
  4239. cmEditor.ternServer = server;
  4240. cmEditor.on('cursorActivity', function (cm) {
  4241. server.updateArgHints(cm);
  4242. });
  4243. }
  4244. // CodeMirror hasn't Padding.
  4245. cmEditor.setSize(null, $editable.outerHeight());
  4246. $codable.data('cmEditor', cmEditor);
  4247. }
  4248. };
  4249. /**
  4250. * deactivate code view
  4251. */
  4252. this.deactivate = function () {
  4253. // deactivate CodeMirror as codable
  4254. if (agent.hasCodeMirror) {
  4255. var cmEditor = $codable.data('cmEditor');
  4256. $codable.val(cmEditor.getValue());
  4257. cmEditor.toTextArea();
  4258. }
  4259. var value = dom.value($codable, options.prettifyHtml) || dom.emptyPara;
  4260. var isChange = $editable.html() !== value;
  4261. $editable.html(value);
  4262. $editable.height(options.height ? $codable.height() : 'auto');
  4263. $editor.removeClass('codeview');
  4264. if (isChange) {
  4265. context.triggerEvent('change', $editable.html(), $editable);
  4266. }
  4267. $editable.focus();
  4268. context.invoke('toolbar.updateCodeview', false);
  4269. };
  4270. this.destroy = function () {
  4271. if (this.isActivated()) {
  4272. this.deactivate();
  4273. }
  4274. };
  4275. };
  4276. var EDITABLE_PADDING = 24;
  4277. var Statusbar = function (context) {
  4278. var $document = $(document);
  4279. var $statusbar = context.layoutInfo.statusbar;
  4280. var $editable = context.layoutInfo.editable;
  4281. var options = context.options;
  4282. this.initialize = function () {
  4283. if (options.airMode || options.disableResizeEditor) {
  4284. this.destroy();
  4285. return;
  4286. }
  4287. $statusbar.on('mousedown', function (event) {
  4288. event.preventDefault();
  4289. event.stopPropagation();
  4290. var editableTop = $editable.offset().top - $document.scrollTop();
  4291. var onMouseMove = function (event) {
  4292. var height = event.clientY - (editableTop + EDITABLE_PADDING);
  4293. height = (options.minheight > 0) ? Math.max(height, options.minheight) : height;
  4294. height = (options.maxHeight > 0) ? Math.min(height, options.maxHeight) : height;
  4295. $editable.height(height);
  4296. };
  4297. $document
  4298. .on('mousemove', onMouseMove)
  4299. .one('mouseup', function () {
  4300. $document.off('mousemove', onMouseMove);
  4301. });
  4302. });
  4303. };
  4304. this.destroy = function () {
  4305. $statusbar.off();
  4306. $statusbar.remove();
  4307. };
  4308. };
  4309. var Fullscreen = function (context) {
  4310. var self = this;
  4311. var $editor = context.layoutInfo.editor;
  4312. var $toolbar = context.layoutInfo.toolbar;
  4313. var $editable = context.layoutInfo.editable;
  4314. var $codable = context.layoutInfo.codable;
  4315. var $window = $(window);
  4316. var $scrollbar = $('html, body');
  4317. this.resizeTo = function (size) {
  4318. $editable.css('height', size.h);
  4319. $codable.css('height', size.h);
  4320. if ($codable.data('cmeditor')) {
  4321. $codable.data('cmeditor').setsize(null, size.h);
  4322. }
  4323. };
  4324. this.onResize = function () {
  4325. self.resizeTo({
  4326. h: $window.height() - $toolbar.outerHeight()
  4327. });
  4328. };
  4329. /**
  4330. * toggle fullscreen
  4331. */
  4332. this.toggle = function () {
  4333. $editor.toggleClass('fullscreen');
  4334. if (this.isFullscreen()) {
  4335. $editable.data('orgHeight', $editable.css('height'));
  4336. $window.on('resize', this.onResize).trigger('resize');
  4337. $scrollbar.css('overflow', 'hidden');
  4338. } else {
  4339. $window.off('resize', this.onResize);
  4340. this.resizeTo({ h: $editable.data('orgHeight') });
  4341. $scrollbar.css('overflow', 'visible');
  4342. }
  4343. context.invoke('toolbar.updateFullscreen', this.isFullscreen());
  4344. };
  4345. this.isFullscreen = function () {
  4346. return $editor.hasClass('fullscreen');
  4347. };
  4348. };
  4349. var Handle = function (context) {
  4350. var self = this;
  4351. var $document = $(document);
  4352. var $editingArea = context.layoutInfo.editingArea;
  4353. var options = context.options;
  4354. this.events = {
  4355. 'summernote.mousedown': function (we, e) {
  4356. if (self.update(e.target)) {
  4357. e.preventDefault();
  4358. }
  4359. },
  4360. 'summernote.keyup summernote.scroll summernote.change summernote.dialog.shown': function () {
  4361. self.update();
  4362. }
  4363. };
  4364. this.initialize = function () {
  4365. this.$handle = $([
  4366. '<div class="note-handle">',
  4367. '<div class="note-control-selection">',
  4368. '<div class="note-control-selection-bg"></div>',
  4369. '<div class="note-control-holder note-control-nw"></div>',
  4370. '<div class="note-control-holder note-control-ne"></div>',
  4371. '<div class="note-control-holder note-control-sw"></div>',
  4372. '<div class="',
  4373. (options.disableResizeImage ? 'note-control-holder' : 'note-control-sizing'),
  4374. ' note-control-se"></div>',
  4375. (options.disableResizeImage ? '' : '<div class="note-control-selection-info"></div>'),
  4376. '</div>',
  4377. '</div>'
  4378. ].join('')).prependTo($editingArea);
  4379. this.$handle.on('mousedown', function (event) {
  4380. if (dom.isControlSizing(event.target)) {
  4381. event.preventDefault();
  4382. event.stopPropagation();
  4383. var $target = self.$handle.find('.note-control-selection').data('target'),
  4384. posStart = $target.offset(),
  4385. scrollTop = $document.scrollTop();
  4386. var onMouseMove = function (event) {
  4387. context.invoke('editor.resizeTo', {
  4388. x: event.clientX - posStart.left,
  4389. y: event.clientY - (posStart.top - scrollTop)
  4390. }, $target, !event.shiftKey);
  4391. self.update($target[0]);
  4392. };
  4393. $document
  4394. .on('mousemove', onMouseMove)
  4395. .one('mouseup', function (e) {
  4396. e.preventDefault();
  4397. $document.off('mousemove', onMouseMove);
  4398. context.invoke('editor.afterCommand');
  4399. });
  4400. if (!$target.data('ratio')) { // original ratio.
  4401. $target.data('ratio', $target.height() / $target.width());
  4402. }
  4403. }
  4404. });
  4405. };
  4406. this.destroy = function () {
  4407. this.$handle.remove();
  4408. };
  4409. this.update = function (target) {
  4410. var isImage = dom.isImg(target);
  4411. var $selection = this.$handle.find('.note-control-selection');
  4412. context.invoke('imagePopover.update', target);
  4413. if (isImage) {
  4414. var $image = $(target);
  4415. var pos = $image.position();
  4416. // include margin
  4417. var imageSize = {
  4418. w: $image.outerWidth(true),
  4419. h: $image.outerHeight(true)
  4420. };
  4421. $selection.css({
  4422. display: 'block',
  4423. left: pos.left,
  4424. top: pos.top,
  4425. width: imageSize.w,
  4426. height: imageSize.h
  4427. }).data('target', $image); // save current image element.
  4428. var sizingText = imageSize.w + 'x' + imageSize.h;
  4429. $selection.find('.note-control-selection-info').text(sizingText);
  4430. context.invoke('editor.saveTarget', target);
  4431. } else {
  4432. this.hide();
  4433. }
  4434. return isImage;
  4435. };
  4436. /**
  4437. * hide
  4438. *
  4439. * @param {jQuery} $handle
  4440. */
  4441. this.hide = function () {
  4442. context.invoke('editor.clearTarget');
  4443. this.$handle.children().hide();
  4444. };
  4445. };
  4446. var AutoLink = function (context) {
  4447. var self = this;
  4448. var defaultScheme = 'http://';
  4449. var linkPattern = /^([A-Za-z][A-Za-z0-9+-.]*\:[\/\/]?|mailto:[A-Z0-9._%+-]+@)?(www\.)?(.+)$/i;
  4450. this.events = {
  4451. 'summernote.keyup': function (we, e) {
  4452. if (!e.isDefaultPrevented()) {
  4453. self.handleKeyup(e);
  4454. }
  4455. },
  4456. 'summernote.keydown': function (we, e) {
  4457. self.handleKeydown(e);
  4458. }
  4459. };
  4460. this.initialize = function () {
  4461. this.lastWordRange = null;
  4462. };
  4463. this.destroy = function () {
  4464. this.lastWordRange = null;
  4465. };
  4466. this.replace = function () {
  4467. if (!this.lastWordRange) {
  4468. return;
  4469. }
  4470. var keyword = this.lastWordRange.toString();
  4471. var match = keyword.match(linkPattern);
  4472. if (match && (match[1] || match[2])) {
  4473. var link = match[1] ? keyword : defaultScheme + keyword;
  4474. var node = $('<a />').html(keyword).attr('href', link)[0];
  4475. this.lastWordRange.insertNode(node);
  4476. this.lastWordRange = null;
  4477. context.invoke('editor.focus');
  4478. }
  4479. };
  4480. this.handleKeydown = function (e) {
  4481. if (list.contains([key.code.ENTER, key.code.SPACE], e.keyCode)) {
  4482. var wordRange = context.invoke('editor.createRange').getWordRange();
  4483. this.lastWordRange = wordRange;
  4484. }
  4485. };
  4486. this.handleKeyup = function (e) {
  4487. if (list.contains([key.code.ENTER, key.code.SPACE], e.keyCode)) {
  4488. this.replace();
  4489. }
  4490. };
  4491. };
  4492. /**
  4493. * textarea auto sync.
  4494. */
  4495. var AutoSync = function (context) {
  4496. var $note = context.layoutInfo.note;
  4497. this.events = {
  4498. 'summernote.change': function () {
  4499. $note.val(context.invoke('code'));
  4500. }
  4501. };
  4502. this.shouldInitialize = function () {
  4503. return dom.isTextarea($note[0]);
  4504. };
  4505. };
  4506. var Placeholder = function (context) {
  4507. var self = this;
  4508. var $editingArea = context.layoutInfo.editingArea;
  4509. var options = context.options;
  4510. this.events = {
  4511. 'summernote.init summernote.change': function () {
  4512. self.update();
  4513. },
  4514. 'summernote.codeview.toggled': function () {
  4515. self.update();
  4516. }
  4517. };
  4518. this.shouldInitialize = function () {
  4519. return !!options.placeholder;
  4520. };
  4521. this.initialize = function () {
  4522. this.$placeholder = $('<div class="note-placeholder">');
  4523. this.$placeholder.on('click', function () {
  4524. context.invoke('focus');
  4525. }).text(options.placeholder).prependTo($editingArea);
  4526. };
  4527. this.destroy = function () {
  4528. this.$placeholder.remove();
  4529. };
  4530. this.update = function () {
  4531. var isShow = !context.invoke('codeview.isActivated') && context.invoke('editor.isEmpty');
  4532. this.$placeholder.toggle(isShow);
  4533. };
  4534. };
  4535. var Buttons = function (context) {
  4536. var self = this;
  4537. var ui = $.summernote.ui;
  4538. var $toolbar = context.layoutInfo.toolbar;
  4539. var options = context.options;
  4540. var lang = options.langInfo;
  4541. var invertedKeyMap = func.invertObject(options.keyMap[agent.isMac ? 'mac' : 'pc']);
  4542. var representShortcut = this.representShortcut = function (editorMethod) {
  4543. var shortcut = invertedKeyMap[editorMethod];
  4544. if (!options.shortcuts || !shortcut) {
  4545. return '';
  4546. }
  4547. if (agent.isMac) {
  4548. shortcut = shortcut.replace('CMD', '⌘').replace('SHIFT', '⇧');
  4549. }
  4550. shortcut = shortcut.replace('BACKSLASH', '\\')
  4551. .replace('SLASH', '/')
  4552. .replace('LEFTBRACKET', '[')
  4553. .replace('RIGHTBRACKET', ']');
  4554. return ' (' + shortcut + ')';
  4555. };
  4556. this.initialize = function () {
  4557. this.addToolbarButtons();
  4558. this.addImagePopoverButtons();
  4559. this.addLinkPopoverButtons();
  4560. this.fontInstalledMap = {};
  4561. };
  4562. this.destroy = function () {
  4563. delete this.fontInstalledMap;
  4564. };
  4565. this.isFontInstalled = function (name) {
  4566. if (!self.fontInstalledMap.hasOwnProperty(name)) {
  4567. self.fontInstalledMap[name] = agent.isFontInstalled(name) ||
  4568. list.contains(options.fontNamesIgnoreCheck, name);
  4569. }
  4570. return self.fontInstalledMap[name];
  4571. };
  4572. this.addToolbarButtons = function () {
  4573. context.memo('button.style', function () {
  4574. return ui.buttonGroup([
  4575. ui.button({
  4576. className: 'dropdown-toggle',
  4577. contents: ui.icon(options.icons.magic) + ' ' + ui.icon(options.icons.caret, 'span'),
  4578. tooltip: lang.style.style,
  4579. data: {
  4580. toggle: 'dropdown'
  4581. }
  4582. }),
  4583. ui.dropdown({
  4584. className: 'dropdown-style',
  4585. items: context.options.styleTags,
  4586. template: function (item) {
  4587. if (typeof item === 'string') {
  4588. item = { tag: item, title: (lang.style.hasOwnProperty(item) ? lang.style[item] : item) };
  4589. }
  4590. var tag = item.tag;
  4591. var title = item.title;
  4592. var style = item.style ? ' style="' + item.style + '" ' : '';
  4593. var className = item.className ? ' class="' + item.className + '"' : '';
  4594. return '<' + tag + style + className + '>' + title + '</' + tag + '>';
  4595. },
  4596. click: context.createInvokeHandler('editor.formatBlock')
  4597. })
  4598. ]).render();
  4599. });
  4600. context.memo('button.bold', function () {
  4601. return ui.button({
  4602. className: 'note-btn-bold',
  4603. contents: ui.icon(options.icons.bold),
  4604. tooltip: lang.font.bold + representShortcut('bold'),
  4605. click: context.createInvokeHandlerAndUpdateState('editor.bold')
  4606. }).render();
  4607. });
  4608. context.memo('button.italic', function () {
  4609. return ui.button({
  4610. className: 'note-btn-italic',
  4611. contents: ui.icon(options.icons.italic),
  4612. tooltip: lang.font.italic + representShortcut('italic'),
  4613. click: context.createInvokeHandlerAndUpdateState('editor.italic')
  4614. }).render();
  4615. });
  4616. context.memo('button.underline', function () {
  4617. return ui.button({
  4618. className: 'note-btn-underline',
  4619. contents: ui.icon(options.icons.underline),
  4620. tooltip: lang.font.underline + representShortcut('underline'),
  4621. click: context.createInvokeHandlerAndUpdateState('editor.underline')
  4622. }).render();
  4623. });
  4624. context.memo('button.clear', function () {
  4625. return ui.button({
  4626. contents: ui.icon(options.icons.eraser),
  4627. tooltip: lang.font.clear + representShortcut('removeFormat'),
  4628. click: context.createInvokeHandler('editor.removeFormat')
  4629. }).render();
  4630. });
  4631. context.memo('button.strikethrough', function () {
  4632. return ui.button({
  4633. className: 'note-btn-strikethrough',
  4634. contents: ui.icon(options.icons.strikethrough),
  4635. tooltip: lang.font.strikethrough + representShortcut('strikethrough'),
  4636. click: context.createInvokeHandlerAndUpdateState('editor.strikethrough')
  4637. }).render();
  4638. });
  4639. context.memo('button.superscript', function () {
  4640. return ui.button({
  4641. className: 'note-btn-superscript',
  4642. contents: ui.icon(options.icons.superscript),
  4643. tooltip: lang.font.superscript,
  4644. click: context.createInvokeHandlerAndUpdateState('editor.superscript')
  4645. }).render();
  4646. });
  4647. context.memo('button.subscript', function () {
  4648. return ui.button({
  4649. className: 'note-btn-subscript',
  4650. contents: ui.icon(options.icons.subscript),
  4651. tooltip: lang.font.subscript,
  4652. click: context.createInvokeHandlerAndUpdateState('editor.subscript')
  4653. }).render();
  4654. });
  4655. context.memo('button.fontname', function () {
  4656. return ui.buttonGroup([
  4657. ui.button({
  4658. className: 'dropdown-toggle',
  4659. contents: '<span class="note-current-fontname"/> ' + ui.icon(options.icons.caret, 'span'),
  4660. tooltip: lang.font.name,
  4661. data: {
  4662. toggle: 'dropdown'
  4663. }
  4664. }),
  4665. ui.dropdownCheck({
  4666. className: 'dropdown-fontname',
  4667. checkClassName: options.icons.menuCheck,
  4668. items: options.fontNames.filter(self.isFontInstalled),
  4669. template: function (item) {
  4670. return '<span style="font-family:' + item + '">' + item + '</span>';
  4671. },
  4672. click: context.createInvokeHandlerAndUpdateState('editor.fontName')
  4673. })
  4674. ]).render();
  4675. });
  4676. context.memo('button.fontsize', function () {
  4677. return ui.buttonGroup([
  4678. ui.button({
  4679. className: 'dropdown-toggle',
  4680. contents: '<span class="note-current-fontsize"/>' + ui.icon(options.icons.caret, 'span'),
  4681. tooltip: lang.font.size,
  4682. data: {
  4683. toggle: 'dropdown'
  4684. }
  4685. }),
  4686. ui.dropdownCheck({
  4687. className: 'dropdown-fontsize',
  4688. checkClassName: options.icons.menuCheck,
  4689. items: options.fontSizes,
  4690. click: context.createInvokeHandler('editor.fontSize')
  4691. })
  4692. ]).render();
  4693. });
  4694. context.memo('button.color', function () {
  4695. return ui.buttonGroup({
  4696. className: 'note-color',
  4697. children: [
  4698. ui.button({
  4699. className: 'note-current-color-button',
  4700. contents: ui.icon(options.icons.font + ' note-recent-color'),
  4701. tooltip: lang.color.recent,
  4702. click: function (e) {
  4703. var $button = $(e.currentTarget);
  4704. context.invoke('editor.color', {
  4705. backColor: $button.attr('data-backColor'),
  4706. foreColor: $button.attr('data-foreColor')
  4707. });
  4708. },
  4709. callback: function ($button) {
  4710. var $recentColor = $button.find('.note-recent-color');
  4711. $recentColor.css('background-color', '#FFFF00');
  4712. $button.attr('data-backColor', '#FFFF00');
  4713. }
  4714. }),
  4715. ui.button({
  4716. className: 'dropdown-toggle',
  4717. contents: ui.icon(options.icons.caret, 'span'),
  4718. tooltip: lang.color.more,
  4719. data: {
  4720. toggle: 'dropdown'
  4721. }
  4722. }),
  4723. ui.dropdown({
  4724. items: [
  4725. '<li>',
  4726. '<div class="btn-group">',
  4727. ' <div class="note-palette-title">' + lang.color.background + '</div>',
  4728. ' <div>',
  4729. ' <button type="button" class="note-color-reset btn btn-default" data-event="backColor" data-value="inherit">',
  4730. lang.color.transparent,
  4731. ' </button>',
  4732. ' </div>',
  4733. ' <div class="note-holder" data-event="backColor"/>',
  4734. '</div>',
  4735. '<div class="btn-group">',
  4736. ' <div class="note-palette-title">' + lang.color.foreground + '</div>',
  4737. ' <div>',
  4738. ' <button type="button" class="note-color-reset btn btn-default" data-event="removeFormat" data-value="foreColor">',
  4739. lang.color.resetToDefault,
  4740. ' </button>',
  4741. ' </div>',
  4742. ' <div class="note-holder" data-event="foreColor"/>',
  4743. '</div>',
  4744. '</li>'
  4745. ].join(''),
  4746. callback: function ($dropdown) {
  4747. $dropdown.find('.note-holder').each(function () {
  4748. var $holder = $(this);
  4749. $holder.append(ui.palette({
  4750. colors: options.colors,
  4751. eventName: $holder.data('event')
  4752. }).render());
  4753. });
  4754. },
  4755. click: function (event) {
  4756. var $button = $(event.target);
  4757. var eventName = $button.data('event');
  4758. var value = $button.data('value');
  4759. if (eventName && value) {
  4760. var key = eventName === 'backColor' ? 'background-color' : 'color';
  4761. var $color = $button.closest('.note-color').find('.note-recent-color');
  4762. var $currentButton = $button.closest('.note-color').find('.note-current-color-button');
  4763. $color.css(key, value);
  4764. $currentButton.attr('data-' + eventName, value);
  4765. context.invoke('editor.' + eventName, value);
  4766. }
  4767. }
  4768. })
  4769. ]
  4770. }).render();
  4771. });
  4772. context.memo('button.ul', function () {
  4773. return ui.button({
  4774. contents: ui.icon(options.icons.unorderedlist),
  4775. tooltip: lang.lists.unordered + representShortcut('insertUnorderedList'),
  4776. click: context.createInvokeHandler('editor.insertUnorderedList')
  4777. }).render();
  4778. });
  4779. context.memo('button.ol', function () {
  4780. return ui.button({
  4781. contents: ui.icon(options.icons.orderedlist),
  4782. tooltip: lang.lists.ordered + representShortcut('insertOrderedList'),
  4783. click: context.createInvokeHandler('editor.insertOrderedList')
  4784. }).render();
  4785. });
  4786. var justifyLeft = ui.button({
  4787. contents: ui.icon(options.icons.alignLeft),
  4788. tooltip: lang.paragraph.left + representShortcut('justifyLeft'),
  4789. click: context.createInvokeHandler('editor.justifyLeft')
  4790. });
  4791. var justifyCenter = ui.button({
  4792. contents: ui.icon(options.icons.alignCenter),
  4793. tooltip: lang.paragraph.center + representShortcut('justifyCenter'),
  4794. click: context.createInvokeHandler('editor.justifyCenter')
  4795. });
  4796. var justifyRight = ui.button({
  4797. contents: ui.icon(options.icons.alignRight),
  4798. tooltip: lang.paragraph.right + representShortcut('justifyRight'),
  4799. click: context.createInvokeHandler('editor.justifyRight')
  4800. });
  4801. var justifyFull = ui.button({
  4802. contents: ui.icon(options.icons.alignJustify),
  4803. tooltip: lang.paragraph.justify + representShortcut('justifyFull'),
  4804. click: context.createInvokeHandler('editor.justifyFull')
  4805. });
  4806. var outdent = ui.button({
  4807. contents: ui.icon(options.icons.outdent),
  4808. tooltip: lang.paragraph.outdent + representShortcut('outdent'),
  4809. click: context.createInvokeHandler('editor.outdent')
  4810. });
  4811. var indent = ui.button({
  4812. contents: ui.icon(options.icons.indent),
  4813. tooltip: lang.paragraph.indent + representShortcut('indent'),
  4814. click: context.createInvokeHandler('editor.indent')
  4815. });
  4816. context.memo('button.justifyLeft', func.invoke(justifyLeft, 'render'));
  4817. context.memo('button.justifyCenter', func.invoke(justifyCenter, 'render'));
  4818. context.memo('button.justifyRight', func.invoke(justifyRight, 'render'));
  4819. context.memo('button.justifyFull', func.invoke(justifyFull, 'render'));
  4820. context.memo('button.outdent', func.invoke(outdent, 'render'));
  4821. context.memo('button.indent', func.invoke(indent, 'render'));
  4822. context.memo('button.paragraph', function () {
  4823. return ui.buttonGroup([
  4824. ui.button({
  4825. className: 'dropdown-toggle',
  4826. contents: ui.icon(options.icons.alignLeft) + ' ' + ui.icon(options.icons.caret, 'span'),
  4827. tooltip: lang.paragraph.paragraph,
  4828. data: {
  4829. toggle: 'dropdown'
  4830. }
  4831. }),
  4832. ui.dropdown([
  4833. ui.buttonGroup({
  4834. className: 'note-align',
  4835. children: [justifyLeft, justifyCenter, justifyRight, justifyFull]
  4836. }),
  4837. ui.buttonGroup({
  4838. className: 'note-list',
  4839. children: [outdent, indent]
  4840. })
  4841. ])
  4842. ]).render();
  4843. });
  4844. context.memo('button.height', function () {
  4845. return ui.buttonGroup([
  4846. ui.button({
  4847. className: 'dropdown-toggle',
  4848. contents: ui.icon(options.icons.textHeight) + ' ' + ui.icon(options.icons.caret, 'span'),
  4849. tooltip: lang.font.height,
  4850. data: {
  4851. toggle: 'dropdown'
  4852. }
  4853. }),
  4854. ui.dropdownCheck({
  4855. items: options.lineHeights,
  4856. checkClassName: options.icons.menuCheck,
  4857. className: 'dropdown-line-height',
  4858. click: context.createInvokeHandler('editor.lineHeight')
  4859. })
  4860. ]).render();
  4861. });
  4862. context.memo('button.table', function () {
  4863. return ui.buttonGroup([
  4864. ui.button({
  4865. className: 'dropdown-toggle',
  4866. contents: ui.icon(options.icons.table) + ' ' + ui.icon(options.icons.caret, 'span'),
  4867. tooltip: lang.table.table,
  4868. data: {
  4869. toggle: 'dropdown'
  4870. }
  4871. }),
  4872. ui.dropdown({
  4873. className: 'note-table',
  4874. items: [
  4875. '<div class="note-dimension-picker">',
  4876. ' <div class="note-dimension-picker-mousecatcher" data-event="insertTable" data-value="1x1"/>',
  4877. ' <div class="note-dimension-picker-highlighted"/>',
  4878. ' <div class="note-dimension-picker-unhighlighted"/>',
  4879. '</div>',
  4880. '<div class="note-dimension-display">1 x 1</div>'
  4881. ].join('')
  4882. })
  4883. ], {
  4884. callback: function ($node) {
  4885. var $catcher = $node.find('.note-dimension-picker-mousecatcher');
  4886. $catcher.css({
  4887. width: options.insertTableMaxSize.col + 'em',
  4888. height: options.insertTableMaxSize.row + 'em'
  4889. }).mousedown(context.createInvokeHandler('editor.insertTable'))
  4890. .on('mousemove', self.tableMoveHandler);
  4891. }
  4892. }).render();
  4893. });
  4894. context.memo('button.link', function () {
  4895. return ui.button({
  4896. contents: ui.icon(options.icons.link),
  4897. tooltip: lang.link.link + representShortcut('linkDialog.show'),
  4898. click: context.createInvokeHandler('linkDialog.show')
  4899. }).render();
  4900. });
  4901. context.memo('button.picture', function () {
  4902. return ui.button({
  4903. contents: ui.icon(options.icons.picture),
  4904. tooltip: lang.image.image,
  4905. click: context.createInvokeHandler('imageDialog.show')
  4906. }).render();
  4907. });
  4908. context.memo('button.video', function () {
  4909. return ui.button({
  4910. contents: ui.icon(options.icons.video),
  4911. tooltip: lang.video.video,
  4912. click: context.createInvokeHandler('videoDialog.show')
  4913. }).render();
  4914. });
  4915. context.memo('button.hr', function () {
  4916. return ui.button({
  4917. contents: ui.icon(options.icons.minus),
  4918. tooltip: lang.hr.insert + representShortcut('insertHorizontalRule'),
  4919. click: context.createInvokeHandler('editor.insertHorizontalRule')
  4920. }).render();
  4921. });
  4922. context.memo('button.fullscreen', function () {
  4923. return ui.button({
  4924. className: 'btn-fullscreen',
  4925. contents: ui.icon(options.icons.arrowsAlt),
  4926. tooltip: lang.options.fullscreen,
  4927. click: context.createInvokeHandler('fullscreen.toggle')
  4928. }).render();
  4929. });
  4930. context.memo('button.codeview', function () {
  4931. return ui.button({
  4932. className: 'btn-codeview',
  4933. contents: ui.icon(options.icons.code),
  4934. tooltip: lang.options.codeview,
  4935. click: context.createInvokeHandler('codeview.toggle')
  4936. }).render();
  4937. });
  4938. context.memo('button.redo', function () {
  4939. return ui.button({
  4940. contents: ui.icon(options.icons.redo),
  4941. tooltip: lang.history.redo + representShortcut('redo'),
  4942. click: context.createInvokeHandler('editor.redo')
  4943. }).render();
  4944. });
  4945. context.memo('button.undo', function () {
  4946. return ui.button({
  4947. contents: ui.icon(options.icons.undo),
  4948. tooltip: lang.history.undo + representShortcut('undo'),
  4949. click: context.createInvokeHandler('editor.undo')
  4950. }).render();
  4951. });
  4952. context.memo('button.help', function () {
  4953. return ui.button({
  4954. contents: ui.icon(options.icons.question),
  4955. tooltip: lang.options.help,
  4956. click: context.createInvokeHandler('helpDialog.show')
  4957. }).render();
  4958. });
  4959. };
  4960. /**
  4961. * image : [
  4962. * ['imagesize', ['imageSize100', 'imageSize50', 'imageSize25']],
  4963. * ['float', ['floatLeft', 'floatRight', 'floatNone' ]],
  4964. * ['remove', ['removeMedia']]
  4965. * ],
  4966. */
  4967. this.addImagePopoverButtons = function () {
  4968. // Image Size Buttons
  4969. context.memo('button.imageSize100', function () {
  4970. return ui.button({
  4971. contents: '<span class="note-fontsize-10">100%</span>',
  4972. tooltip: lang.image.resizeFull,
  4973. click: context.createInvokeHandler('editor.resize', '1')
  4974. }).render();
  4975. });
  4976. context.memo('button.imageSize50', function () {
  4977. return ui.button({
  4978. contents: '<span class="note-fontsize-10">50%</span>',
  4979. tooltip: lang.image.resizeHalf,
  4980. click: context.createInvokeHandler('editor.resize', '0.5')
  4981. }).render();
  4982. });
  4983. context.memo('button.imageSize25', function () {
  4984. return ui.button({
  4985. contents: '<span class="note-fontsize-10">25%</span>',
  4986. tooltip: lang.image.resizeQuarter,
  4987. click: context.createInvokeHandler('editor.resize', '0.25')
  4988. }).render();
  4989. });
  4990. // Float Buttons
  4991. context.memo('button.floatLeft', function () {
  4992. return ui.button({
  4993. contents: ui.icon(options.icons.alignLeft),
  4994. tooltip: lang.image.floatLeft,
  4995. click: context.createInvokeHandler('editor.floatMe', 'left')
  4996. }).render();
  4997. });
  4998. context.memo('button.floatRight', function () {
  4999. return ui.button({
  5000. contents: ui.icon(options.icons.alignRight),
  5001. tooltip: lang.image.floatRight,
  5002. click: context.createInvokeHandler('editor.floatMe', 'right')
  5003. }).render();
  5004. });
  5005. context.memo('button.floatNone', function () {
  5006. return ui.button({
  5007. contents: ui.icon(options.icons.alignJustify),
  5008. tooltip: lang.image.floatNone,
  5009. click: context.createInvokeHandler('editor.floatMe', 'none')
  5010. }).render();
  5011. });
  5012. // Remove Buttons
  5013. context.memo('button.removeMedia', function () {
  5014. return ui.button({
  5015. contents: ui.icon(options.icons.trash),
  5016. tooltip: lang.image.remove,
  5017. click: context.createInvokeHandler('editor.removeMedia')
  5018. }).render();
  5019. });
  5020. };
  5021. this.addLinkPopoverButtons = function () {
  5022. context.memo('button.linkDialogShow', function () {
  5023. return ui.button({
  5024. contents: ui.icon(options.icons.link),
  5025. tooltip: lang.link.edit,
  5026. click: context.createInvokeHandler('linkDialog.show')
  5027. }).render();
  5028. });
  5029. context.memo('button.unlink', function () {
  5030. return ui.button({
  5031. contents: ui.icon(options.icons.unlink),
  5032. tooltip: lang.link.unlink,
  5033. click: context.createInvokeHandler('editor.unlink')
  5034. }).render();
  5035. });
  5036. };
  5037. this.build = function ($container, groups) {
  5038. for (var groupIdx = 0, groupLen = groups.length; groupIdx < groupLen; groupIdx++) {
  5039. var group = groups[groupIdx];
  5040. var groupName = group[0];
  5041. var buttons = group[1];
  5042. var $group = ui.buttonGroup({
  5043. className: 'note-' + groupName
  5044. }).render();
  5045. for (var idx = 0, len = buttons.length; idx < len; idx++) {
  5046. var button = context.memo('button.' + buttons[idx]);
  5047. if (button) {
  5048. $group.append(typeof button === 'function' ? button(context) : button);
  5049. }
  5050. }
  5051. $group.appendTo($container);
  5052. }
  5053. };
  5054. this.updateCurrentStyle = function () {
  5055. var styleInfo = context.invoke('editor.currentStyle');
  5056. this.updateBtnStates({
  5057. '.note-btn-bold': function () {
  5058. return styleInfo['font-bold'] === 'bold';
  5059. },
  5060. '.note-btn-italic': function () {
  5061. return styleInfo['font-italic'] === 'italic';
  5062. },
  5063. '.note-btn-underline': function () {
  5064. return styleInfo['font-underline'] === 'underline';
  5065. },
  5066. '.note-btn-subscript': function () {
  5067. return styleInfo['font-subscript'] === 'subscript';
  5068. },
  5069. '.note-btn-superscript': function () {
  5070. return styleInfo['font-superscript'] === 'superscript';
  5071. },
  5072. '.note-btn-strikethrough': function () {
  5073. return styleInfo['font-strikethrough'] === 'strikethrough';
  5074. }
  5075. });
  5076. if (styleInfo['font-family']) {
  5077. var fontNames = styleInfo['font-family'].split(',').map(function (name) {
  5078. return name.replace(/[\'\"]/g, '')
  5079. .replace(/\s+$/, '')
  5080. .replace(/^\s+/, '');
  5081. });
  5082. var fontName = list.find(fontNames, self.isFontInstalled);
  5083. $toolbar.find('.dropdown-fontname li a').each(function () {
  5084. // always compare string to avoid creating another func.
  5085. var isChecked = ($(this).data('value') + '') === (fontName + '');
  5086. this.className = isChecked ? 'checked' : '';
  5087. });
  5088. $toolbar.find('.note-current-fontname').text(fontName);
  5089. }
  5090. if (styleInfo['font-size']) {
  5091. var fontSize = styleInfo['font-size'];
  5092. $toolbar.find('.dropdown-fontsize li a').each(function () {
  5093. // always compare with string to avoid creating another func.
  5094. var isChecked = ($(this).data('value') + '') === (fontSize + '');
  5095. this.className = isChecked ? 'checked' : '';
  5096. });
  5097. $toolbar.find('.note-current-fontsize').text(fontSize);
  5098. }
  5099. if (styleInfo['line-height']) {
  5100. var lineHeight = styleInfo['line-height'];
  5101. $toolbar.find('.dropdown-line-height li a').each(function () {
  5102. // always compare with string to avoid creating another func.
  5103. var isChecked = ($(this).data('value') + '') === (lineHeight + '');
  5104. this.className = isChecked ? 'checked' : '';
  5105. });
  5106. }
  5107. };
  5108. this.updateBtnStates = function (infos) {
  5109. $.each(infos, function (selector, pred) {
  5110. ui.toggleBtnActive($toolbar.find(selector), pred());
  5111. });
  5112. };
  5113. this.tableMoveHandler = function (event) {
  5114. var PX_PER_EM = 18;
  5115. var $picker = $(event.target.parentNode); // target is mousecatcher
  5116. var $dimensionDisplay = $picker.next();
  5117. var $catcher = $picker.find('.note-dimension-picker-mousecatcher');
  5118. var $highlighted = $picker.find('.note-dimension-picker-highlighted');
  5119. var $unhighlighted = $picker.find('.note-dimension-picker-unhighlighted');
  5120. var posOffset;
  5121. // HTML5 with jQuery - e.offsetX is undefined in Firefox
  5122. if (event.offsetX === undefined) {
  5123. var posCatcher = $(event.target).offset();
  5124. posOffset = {
  5125. x: event.pageX - posCatcher.left,
  5126. y: event.pageY - posCatcher.top
  5127. };
  5128. } else {
  5129. posOffset = {
  5130. x: event.offsetX,
  5131. y: event.offsetY
  5132. };
  5133. }
  5134. var dim = {
  5135. c: Math.ceil(posOffset.x / PX_PER_EM) || 1,
  5136. r: Math.ceil(posOffset.y / PX_PER_EM) || 1
  5137. };
  5138. $highlighted.css({ width: dim.c + 'em', height: dim.r + 'em' });
  5139. $catcher.data('value', dim.c + 'x' + dim.r);
  5140. if (3 < dim.c && dim.c < options.insertTableMaxSize.col) {
  5141. $unhighlighted.css({ width: dim.c + 1 + 'em'});
  5142. }
  5143. if (3 < dim.r && dim.r < options.insertTableMaxSize.row) {
  5144. $unhighlighted.css({ height: dim.r + 1 + 'em'});
  5145. }
  5146. $dimensionDisplay.html(dim.c + ' x ' + dim.r);
  5147. };
  5148. };
  5149. var Toolbar = function (context) {
  5150. var ui = $.summernote.ui;
  5151. var $note = context.layoutInfo.note;
  5152. var $toolbar = context.layoutInfo.toolbar;
  5153. var options = context.options;
  5154. this.shouldInitialize = function () {
  5155. return !options.airMode;
  5156. };
  5157. this.initialize = function () {
  5158. options.toolbar = options.toolbar || [];
  5159. if (!options.toolbar.length) {
  5160. $toolbar.hide();
  5161. } else {
  5162. context.invoke('buttons.build', $toolbar, options.toolbar);
  5163. }
  5164. if (options.toolbarContainer) {
  5165. $toolbar.appendTo(options.toolbarContainer);
  5166. }
  5167. $note.on('summernote.keyup summernote.mouseup summernote.change', function () {
  5168. context.invoke('buttons.updateCurrentStyle');
  5169. });
  5170. context.invoke('buttons.updateCurrentStyle');
  5171. };
  5172. this.destroy = function () {
  5173. $toolbar.children().remove();
  5174. };
  5175. this.updateFullscreen = function (isFullscreen) {
  5176. ui.toggleBtnActive($toolbar.find('.btn-fullscreen'), isFullscreen);
  5177. };
  5178. this.updateCodeview = function (isCodeview) {
  5179. ui.toggleBtnActive($toolbar.find('.btn-codeview'), isCodeview);
  5180. if (isCodeview) {
  5181. this.deactivate();
  5182. } else {
  5183. this.activate();
  5184. }
  5185. };
  5186. this.activate = function (isIncludeCodeview) {
  5187. var $btn = $toolbar.find('button');
  5188. if (!isIncludeCodeview) {
  5189. $btn = $btn.not('.btn-codeview');
  5190. }
  5191. ui.toggleBtn($btn, true);
  5192. };
  5193. this.deactivate = function (isIncludeCodeview) {
  5194. var $btn = $toolbar.find('button');
  5195. if (!isIncludeCodeview) {
  5196. $btn = $btn.not('.btn-codeview');
  5197. }
  5198. ui.toggleBtn($btn, false);
  5199. };
  5200. };
  5201. var LinkDialog = function (context) {
  5202. var self = this;
  5203. var ui = $.summernote.ui;
  5204. var $editor = context.layoutInfo.editor;
  5205. var options = context.options;
  5206. var lang = options.langInfo;
  5207. this.initialize = function () {
  5208. var $container = options.dialogsInBody ? $(document.body) : $editor;
  5209. var body = '<div class="form-group">' +
  5210. '<label>' + lang.link.textToDisplay + '</label>' +
  5211. '<input class="note-link-text form-control" type="text" />' +
  5212. '</div>' +
  5213. '<div class="form-group">' +
  5214. '<label>' + lang.link.url + '</label>' +
  5215. '<input class="note-link-url form-control" type="text" value="http://" />' +
  5216. '</div>' +
  5217. (!options.disableLinkTarget ?
  5218. '<div class="checkbox">' +
  5219. '<label for="sn-checkbox-open-in-new-window">' +
  5220. '<input type="checkbox" id="sn-checkbox-open-in-new-window" checked />' + lang.link.openInNewWindow +
  5221. '</label>' +
  5222. '</div>' : ''
  5223. );
  5224. var footer = '<button href="#" class="btn btn-primary note-link-btn disabled" disabled>' + lang.link.insert + '</button>';
  5225. this.$dialog = ui.dialog({
  5226. className: 'link-dialog',
  5227. title: lang.link.insert,
  5228. fade: options.dialogsFade,
  5229. body: body,
  5230. footer: footer
  5231. }).render().appendTo($container);
  5232. };
  5233. this.destroy = function () {
  5234. ui.hideDialog(this.$dialog);
  5235. this.$dialog.remove();
  5236. };
  5237. this.bindEnterKey = function ($input, $btn) {
  5238. $input.on('keypress', function (event) {
  5239. if (event.keyCode === key.code.ENTER) {
  5240. $btn.trigger('click');
  5241. }
  5242. });
  5243. };
  5244. /**
  5245. * toggle update button
  5246. */
  5247. this.toggleLinkBtn = function ($linkBtn, $linkText, $linkUrl) {
  5248. ui.toggleBtn($linkBtn, $linkText.val() && $linkUrl.val());
  5249. };
  5250. /**
  5251. * Show link dialog and set event handlers on dialog controls.
  5252. *
  5253. * @param {Object} linkInfo
  5254. * @return {Promise}
  5255. */
  5256. this.showLinkDialog = function (linkInfo) {
  5257. return $.Deferred(function (deferred) {
  5258. var $linkText = self.$dialog.find('.note-link-text'),
  5259. $linkUrl = self.$dialog.find('.note-link-url'),
  5260. $linkBtn = self.$dialog.find('.note-link-btn'),
  5261. $openInNewWindow = self.$dialog.find('input[type=checkbox]');
  5262. ui.onDialogShown(self.$dialog, function () {
  5263. context.triggerEvent('dialog.shown');
  5264. // if no url was given, copy text to url
  5265. if (!linkInfo.url) {
  5266. linkInfo.url = linkInfo.text;
  5267. }
  5268. $linkText.val(linkInfo.text);
  5269. var handleLinkTextUpdate = function () {
  5270. self.toggleLinkBtn($linkBtn, $linkText, $linkUrl);
  5271. // if linktext was modified by keyup,
  5272. // stop cloning text from linkUrl
  5273. linkInfo.text = $linkText.val();
  5274. };
  5275. $linkText.on('input', handleLinkTextUpdate).on('paste', function () {
  5276. setTimeout(handleLinkTextUpdate, 0);
  5277. });
  5278. var handleLinkUrlUpdate = function () {
  5279. self.toggleLinkBtn($linkBtn, $linkText, $linkUrl);
  5280. // display same link on `Text to display` input
  5281. // when create a new link
  5282. if (!linkInfo.text) {
  5283. $linkText.val($linkUrl.val());
  5284. }
  5285. };
  5286. $linkUrl.on('input', handleLinkUrlUpdate).on('paste', function () {
  5287. setTimeout(handleLinkUrlUpdate, 0);
  5288. }).val(linkInfo.url).trigger('focus');
  5289. self.toggleLinkBtn($linkBtn, $linkText, $linkUrl);
  5290. self.bindEnterKey($linkUrl, $linkBtn);
  5291. self.bindEnterKey($linkText, $linkBtn);
  5292. var isChecked = linkInfo.isNewWindow !== undefined ?
  5293. linkInfo.isNewWindow : context.options.linkTargetBlank;
  5294. $openInNewWindow.prop('checked', isChecked);
  5295. $linkBtn.one('click', function (event) {
  5296. event.preventDefault();
  5297. deferred.resolve({
  5298. range: linkInfo.range,
  5299. url: $linkUrl.val(),
  5300. text: $linkText.val(),
  5301. isNewWindow: $openInNewWindow.is(':checked')
  5302. });
  5303. self.$dialog.modal('hide');
  5304. });
  5305. });
  5306. ui.onDialogHidden(self.$dialog, function () {
  5307. // detach events
  5308. $linkText.off('input paste keypress');
  5309. $linkUrl.off('input paste keypress');
  5310. $linkBtn.off('click');
  5311. if (deferred.state() === 'pending') {
  5312. deferred.reject();
  5313. }
  5314. });
  5315. ui.showDialog(self.$dialog);
  5316. }).promise();
  5317. };
  5318. /**
  5319. * @param {Object} layoutInfo
  5320. */
  5321. this.show = function () {
  5322. var linkInfo = context.invoke('editor.getLinkInfo');
  5323. context.invoke('editor.saveRange');
  5324. this.showLinkDialog(linkInfo).then(function (linkInfo) {
  5325. context.invoke('editor.restoreRange');
  5326. context.invoke('editor.createLink', linkInfo);
  5327. }).fail(function () {
  5328. context.invoke('editor.restoreRange');
  5329. });
  5330. };
  5331. context.memo('help.linkDialog.show', options.langInfo.help['linkDialog.show']);
  5332. };
  5333. var LinkPopover = function (context) {
  5334. var self = this;
  5335. var ui = $.summernote.ui;
  5336. var options = context.options;
  5337. this.events = {
  5338. 'summernote.keyup summernote.mouseup summernote.change summernote.scroll': function () {
  5339. self.update();
  5340. },
  5341. 'summernote.dialog.shown': function () {
  5342. self.hide();
  5343. }
  5344. };
  5345. this.shouldInitialize = function () {
  5346. return !list.isEmpty(options.popover.link);
  5347. };
  5348. this.initialize = function () {
  5349. this.$popover = ui.popover({
  5350. className: 'note-link-popover',
  5351. callback: function ($node) {
  5352. var $content = $node.find('.popover-content');
  5353. $content.prepend('<span><a target="_blank"></a>&nbsp;</span>');
  5354. }
  5355. }).render().appendTo('body');
  5356. var $content = this.$popover.find('.popover-content');
  5357. context.invoke('buttons.build', $content, options.popover.link);
  5358. };
  5359. this.destroy = function () {
  5360. this.$popover.remove();
  5361. };
  5362. this.update = function () {
  5363. // Prevent focusing on editable when invoke('code') is executed
  5364. if (!context.invoke('editor.hasFocus')) {
  5365. this.hide();
  5366. return;
  5367. }
  5368. var rng = context.invoke('editor.createRange');
  5369. if (rng.isCollapsed() && rng.isOnAnchor()) {
  5370. var anchor = dom.ancestor(rng.sc, dom.isAnchor);
  5371. var href = $(anchor).attr('href');
  5372. this.$popover.find('a').attr('href', href).html(href);
  5373. var pos = dom.posFromPlaceholder(anchor);
  5374. this.$popover.css({
  5375. display: 'block',
  5376. left: pos.left,
  5377. top: pos.top
  5378. });
  5379. } else {
  5380. this.hide();
  5381. }
  5382. };
  5383. this.hide = function () {
  5384. this.$popover.hide();
  5385. };
  5386. };
  5387. var ImageDialog = function (context) {
  5388. var self = this;
  5389. var ui = $.summernote.ui;
  5390. var $editor = context.layoutInfo.editor;
  5391. var options = context.options;
  5392. var lang = options.langInfo;
  5393. this.initialize = function () {
  5394. var $container = options.dialogsInBody ? $(document.body) : $editor;
  5395. var imageLimitation = '';
  5396. if (options.maximumImageFileSize) {
  5397. var unit = Math.floor(Math.log(options.maximumImageFileSize) / Math.log(1024));
  5398. var readableSize = (options.maximumImageFileSize / Math.pow(1024, unit)).toFixed(2) * 1 +
  5399. ' ' + ' KMGTP'[unit] + 'B';
  5400. imageLimitation = '<small>' + lang.image.maximumFileSize + ' : ' + readableSize + '</small>';
  5401. }
  5402. var body = '<div class="form-group note-group-select-from-files">' +
  5403. '<label>' + lang.image.selectFromFiles + '</label>' +
  5404. '<input class="note-image-input form-control" type="file" name="files" accept="image/*" multiple="multiple" />' +
  5405. imageLimitation +
  5406. '</div>' +
  5407. '<div class="form-group note-group-image-url" style="overflow:auto;">' +
  5408. '<label>' + lang.image.url + '</label>' +
  5409. '<input class="note-image-url form-control col-md-12" type="text" />' +
  5410. '</div>';
  5411. var footer = '<button href="#" class="btn btn-primary note-image-btn disabled" disabled>' + lang.image.insert + '</button>';
  5412. this.$dialog = ui.dialog({
  5413. title: lang.image.insert,
  5414. fade: options.dialogsFade,
  5415. body: body,
  5416. footer: footer
  5417. }).render().appendTo($container);
  5418. };
  5419. this.destroy = function () {
  5420. ui.hideDialog(this.$dialog);
  5421. this.$dialog.remove();
  5422. };
  5423. this.bindEnterKey = function ($input, $btn) {
  5424. $input.on('keypress', function (event) {
  5425. if (event.keyCode === key.code.ENTER) {
  5426. $btn.trigger('click');
  5427. }
  5428. });
  5429. };
  5430. this.show = function () {
  5431. context.invoke('editor.saveRange');
  5432. this.showImageDialog().then(function (data) {
  5433. // [workaround] hide dialog before restore range for IE range focus
  5434. ui.hideDialog(self.$dialog);
  5435. context.invoke('editor.restoreRange');
  5436. if (typeof data === 'string') { // image url
  5437. context.invoke('editor.insertImage', data);
  5438. } else { // array of files
  5439. context.invoke('editor.insertImagesOrCallback', data);
  5440. }
  5441. }).fail(function () {
  5442. context.invoke('editor.restoreRange');
  5443. });
  5444. };
  5445. /**
  5446. * show image dialog
  5447. *
  5448. * @param {jQuery} $dialog
  5449. * @return {Promise}
  5450. */
  5451. this.showImageDialog = function () {
  5452. return $.Deferred(function (deferred) {
  5453. var $imageInput = self.$dialog.find('.note-image-input'),
  5454. $imageUrl = self.$dialog.find('.note-image-url'),
  5455. $imageBtn = self.$dialog.find('.note-image-btn');
  5456. ui.onDialogShown(self.$dialog, function () {
  5457. context.triggerEvent('dialog.shown');
  5458. // Cloning imageInput to clear element.
  5459. $imageInput.replaceWith($imageInput.clone()
  5460. .on('change', function () {
  5461. deferred.resolve(this.files || this.value);
  5462. })
  5463. .val('')
  5464. );
  5465. $imageBtn.click(function (event) {
  5466. event.preventDefault();
  5467. deferred.resolve($imageUrl.val());
  5468. });
  5469. $imageUrl.on('keyup paste', function () {
  5470. var url = $imageUrl.val();
  5471. ui.toggleBtn($imageBtn, url);
  5472. }).val('').trigger('focus');
  5473. self.bindEnterKey($imageUrl, $imageBtn);
  5474. });
  5475. ui.onDialogHidden(self.$dialog, function () {
  5476. $imageInput.off('change');
  5477. $imageUrl.off('keyup paste keypress');
  5478. $imageBtn.off('click');
  5479. if (deferred.state() === 'pending') {
  5480. deferred.reject();
  5481. }
  5482. });
  5483. ui.showDialog(self.$dialog);
  5484. });
  5485. };
  5486. };
  5487. var ImagePopover = function (context) {
  5488. var ui = $.summernote.ui;
  5489. var options = context.options;
  5490. this.shouldInitialize = function () {
  5491. return !list.isEmpty(options.popover.image);
  5492. };
  5493. this.initialize = function () {
  5494. this.$popover = ui.popover({
  5495. className: 'note-image-popover'
  5496. }).render().appendTo('body');
  5497. var $content = this.$popover.find('.popover-content');
  5498. context.invoke('buttons.build', $content, options.popover.image);
  5499. };
  5500. this.destroy = function () {
  5501. this.$popover.remove();
  5502. };
  5503. this.update = function (target) {
  5504. if (dom.isImg(target)) {
  5505. var pos = dom.posFromPlaceholder(target);
  5506. this.$popover.css({
  5507. display: 'block',
  5508. left: pos.left,
  5509. top: pos.top
  5510. });
  5511. } else {
  5512. this.hide();
  5513. }
  5514. };
  5515. this.hide = function () {
  5516. this.$popover.hide();
  5517. };
  5518. };
  5519. var VideoDialog = function (context) {
  5520. var self = this;
  5521. var ui = $.summernote.ui;
  5522. var $editor = context.layoutInfo.editor;
  5523. var options = context.options;
  5524. var lang = options.langInfo;
  5525. this.initialize = function () {
  5526. var $container = options.dialogsInBody ? $(document.body) : $editor;
  5527. var body = '<div class="form-group row-fluid">' +
  5528. '<label>' + lang.video.url + ' <small class="text-muted">' + lang.video.providers + '</small></label>' +
  5529. '<input class="note-video-url form-control span12" type="text" />' +
  5530. '</div>';
  5531. var footer = '<button href="#" class="btn btn-primary note-video-btn disabled" disabled>' + lang.video.insert + '</button>';
  5532. this.$dialog = ui.dialog({
  5533. title: lang.video.insert,
  5534. fade: options.dialogsFade,
  5535. body: body,
  5536. footer: footer
  5537. }).render().appendTo($container);
  5538. };
  5539. this.destroy = function () {
  5540. ui.hideDialog(this.$dialog);
  5541. this.$dialog.remove();
  5542. };
  5543. this.bindEnterKey = function ($input, $btn) {
  5544. $input.on('keypress', function (event) {
  5545. if (event.keyCode === key.code.ENTER) {
  5546. $btn.trigger('click');
  5547. }
  5548. });
  5549. };
  5550. this.createVideoNode = function (url) {
  5551. // video url patterns(youtube, instagram, vimeo, dailymotion, youku, mp4, ogg, webm)
  5552. var ytRegExp = /^(?:https?:\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/;
  5553. var ytMatch = url.match(ytRegExp);
  5554. var igRegExp = /(?:www\.|\/\/)instagram\.com\/p\/(.[a-zA-Z0-9_-]*)/;
  5555. var igMatch = url.match(igRegExp);
  5556. var vRegExp = /\/\/vine\.co\/v\/([a-zA-Z0-9]+)/;
  5557. var vMatch = url.match(vRegExp);
  5558. var vimRegExp = /\/\/(player\.)?vimeo\.com\/([a-z]*\/)*(\d+)[?]?.*/;
  5559. var vimMatch = url.match(vimRegExp);
  5560. var dmRegExp = /.+dailymotion.com\/(video|hub)\/([^_]+)[^#]*(#video=([^_&]+))?/;
  5561. var dmMatch = url.match(dmRegExp);
  5562. var youkuRegExp = /\/\/v\.youku\.com\/v_show\/id_(\w+)=*\.html/;
  5563. var youkuMatch = url.match(youkuRegExp);
  5564. var mp4RegExp = /^.+.(mp4|m4v)$/;
  5565. var mp4Match = url.match(mp4RegExp);
  5566. var oggRegExp = /^.+.(ogg|ogv)$/;
  5567. var oggMatch = url.match(oggRegExp);
  5568. var webmRegExp = /^.+.(webm)$/;
  5569. var webmMatch = url.match(webmRegExp);
  5570. var $video;
  5571. if (ytMatch && ytMatch[1].length === 11) {
  5572. var youtubeId = ytMatch[1];
  5573. $video = $('<iframe>')
  5574. .attr('frameborder', 0)
  5575. .attr('src', '//www.youtube.com/embed/' + youtubeId)
  5576. .attr('width', '640').attr('height', '360');
  5577. } else if (igMatch && igMatch[0].length) {
  5578. $video = $('<iframe>')
  5579. .attr('frameborder', 0)
  5580. .attr('src', 'https://instagram.com/p/' + igMatch[1] + '/embed/')
  5581. .attr('width', '612').attr('height', '710')
  5582. .attr('scrolling', 'no')
  5583. .attr('allowtransparency', 'true');
  5584. } else if (vMatch && vMatch[0].length) {
  5585. $video = $('<iframe>')
  5586. .attr('frameborder', 0)
  5587. .attr('src', vMatch[0] + '/embed/simple')
  5588. .attr('width', '600').attr('height', '600')
  5589. .attr('class', 'vine-embed');
  5590. } else if (vimMatch && vimMatch[3].length) {
  5591. $video = $('<iframe webkitallowfullscreen mozallowfullscreen allowfullscreen>')
  5592. .attr('frameborder', 0)
  5593. .attr('src', '//player.vimeo.com/video/' + vimMatch[3])
  5594. .attr('width', '640').attr('height', '360');
  5595. } else if (dmMatch && dmMatch[2].length) {
  5596. $video = $('<iframe>')
  5597. .attr('frameborder', 0)
  5598. .attr('src', '//www.dailymotion.com/embed/video/' + dmMatch[2])
  5599. .attr('width', '640').attr('height', '360');
  5600. } else if (youkuMatch && youkuMatch[1].length) {
  5601. $video = $('<iframe webkitallowfullscreen mozallowfullscreen allowfullscreen>')
  5602. .attr('frameborder', 0)
  5603. .attr('height', '498')
  5604. .attr('width', '510')
  5605. .attr('src', '//player.youku.com/embed/' + youkuMatch[1]);
  5606. } else if (mp4Match || oggMatch || webmMatch) {
  5607. $video = $('<video controls>')
  5608. .attr('src', url)
  5609. .attr('width', '640').attr('height', '360');
  5610. } else {
  5611. // this is not a known video link. Now what, Cat? Now what?
  5612. return false;
  5613. }
  5614. $video.addClass('note-video-clip');
  5615. return $video[0];
  5616. };
  5617. this.show = function () {
  5618. var text = context.invoke('editor.getSelectedText');
  5619. context.invoke('editor.saveRange');
  5620. this.showVideoDialog(text).then(function (url) {
  5621. // [workaround] hide dialog before restore range for IE range focus
  5622. ui.hideDialog(self.$dialog);
  5623. context.invoke('editor.restoreRange');
  5624. // build node
  5625. var $node = self.createVideoNode(url);
  5626. if ($node) {
  5627. // insert video node
  5628. context.invoke('editor.insertNode', $node);
  5629. }
  5630. }).fail(function () {
  5631. context.invoke('editor.restoreRange');
  5632. });
  5633. };
  5634. /**
  5635. * show image dialog
  5636. *
  5637. * @param {jQuery} $dialog
  5638. * @return {Promise}
  5639. */
  5640. this.showVideoDialog = function (text) {
  5641. return $.Deferred(function (deferred) {
  5642. var $videoUrl = self.$dialog.find('.note-video-url'),
  5643. $videoBtn = self.$dialog.find('.note-video-btn');
  5644. ui.onDialogShown(self.$dialog, function () {
  5645. context.triggerEvent('dialog.shown');
  5646. $videoUrl.val(text).on('input', function () {
  5647. ui.toggleBtn($videoBtn, $videoUrl.val());
  5648. }).trigger('focus');
  5649. $videoBtn.click(function (event) {
  5650. event.preventDefault();
  5651. deferred.resolve($videoUrl.val());
  5652. });
  5653. self.bindEnterKey($videoUrl, $videoBtn);
  5654. });
  5655. ui.onDialogHidden(self.$dialog, function () {
  5656. $videoUrl.off('input');
  5657. $videoBtn.off('click');
  5658. if (deferred.state() === 'pending') {
  5659. deferred.reject();
  5660. }
  5661. });
  5662. ui.showDialog(self.$dialog);
  5663. });
  5664. };
  5665. };
  5666. var HelpDialog = function (context) {
  5667. var self = this;
  5668. var ui = $.summernote.ui;
  5669. var $editor = context.layoutInfo.editor;
  5670. var options = context.options;
  5671. var lang = options.langInfo;
  5672. this.createShortCutList = function () {
  5673. var keyMap = options.keyMap[agent.isMac ? 'mac' : 'pc'];
  5674. return Object.keys(keyMap).map(function (key) {
  5675. var command = keyMap[key];
  5676. var $row = $('<div><div class="help-list-item"/></div>');
  5677. $row.append($('<label><kbd>' + key + '</kdb></label>').css({
  5678. 'width': 180,
  5679. 'margin-right': 10
  5680. })).append($('<span/>').html(context.memo('help.' + command) || command));
  5681. return $row.html();
  5682. }).join('');
  5683. };
  5684. this.initialize = function () {
  5685. var $container = options.dialogsInBody ? $(document.body) : $editor;
  5686. var body = [
  5687. '<p class="text-center">',
  5688. '<a href="http://summernote.org/" target="_blank">Summernote 0.8.4</a> · ',
  5689. '<a href="https://github.com/summernote/summernote" target="_blank">Project</a> · ',
  5690. '<a href="https://github.com/summernote/summernote/issues" target="_blank">Issues</a>',
  5691. '</p>'
  5692. ].join('');
  5693. this.$dialog = ui.dialog({
  5694. title: lang.options.help,
  5695. fade: options.dialogsFade,
  5696. body: this.createShortCutList(),
  5697. footer: body,
  5698. callback: function ($node) {
  5699. $node.find('.modal-body').css({
  5700. 'max-height': 300,
  5701. 'overflow': 'scroll'
  5702. });
  5703. }
  5704. }).render().appendTo($container);
  5705. };
  5706. this.destroy = function () {
  5707. ui.hideDialog(this.$dialog);
  5708. this.$dialog.remove();
  5709. };
  5710. /**
  5711. * show help dialog
  5712. *
  5713. * @return {Promise}
  5714. */
  5715. this.showHelpDialog = function () {
  5716. return $.Deferred(function (deferred) {
  5717. ui.onDialogShown(self.$dialog, function () {
  5718. context.triggerEvent('dialog.shown');
  5719. deferred.resolve();
  5720. });
  5721. ui.showDialog(self.$dialog);
  5722. }).promise();
  5723. };
  5724. this.show = function () {
  5725. context.invoke('editor.saveRange');
  5726. this.showHelpDialog().then(function () {
  5727. context.invoke('editor.restoreRange');
  5728. });
  5729. };
  5730. };
  5731. var AirPopover = function (context) {
  5732. var self = this;
  5733. var ui = $.summernote.ui;
  5734. var options = context.options;
  5735. var AIR_MODE_POPOVER_X_OFFSET = 20;
  5736. this.events = {
  5737. 'summernote.keyup summernote.mouseup summernote.scroll': function () {
  5738. self.update();
  5739. },
  5740. 'summernote.change summernote.dialog.shown': function () {
  5741. self.hide();
  5742. },
  5743. 'summernote.focusout': function (we, e) {
  5744. // [workaround] Firefox doesn't support relatedTarget on focusout
  5745. // - Ignore hide action on focus out in FF.
  5746. if (agent.isFF) {
  5747. return;
  5748. }
  5749. if (!e.relatedTarget || !dom.ancestor(e.relatedTarget, func.eq(self.$popover[0]))) {
  5750. self.hide();
  5751. }
  5752. }
  5753. };
  5754. this.shouldInitialize = function () {
  5755. return options.airMode && !list.isEmpty(options.popover.air);
  5756. };
  5757. this.initialize = function () {
  5758. this.$popover = ui.popover({
  5759. className: 'note-air-popover'
  5760. }).render().appendTo('body');
  5761. var $content = this.$popover.find('.popover-content');
  5762. context.invoke('buttons.build', $content, options.popover.air);
  5763. };
  5764. this.destroy = function () {
  5765. this.$popover.remove();
  5766. };
  5767. this.update = function () {
  5768. var styleInfo = context.invoke('editor.currentStyle');
  5769. if (styleInfo.range && !styleInfo.range.isCollapsed()) {
  5770. var rect = list.last(styleInfo.range.getClientRects());
  5771. if (rect) {
  5772. var bnd = func.rect2bnd(rect);
  5773. this.$popover.css({
  5774. display: 'block',
  5775. left: Math.max(bnd.left + bnd.width / 2, 0) - AIR_MODE_POPOVER_X_OFFSET,
  5776. top: bnd.top + bnd.height
  5777. });
  5778. }
  5779. } else {
  5780. this.hide();
  5781. }
  5782. };
  5783. this.hide = function () {
  5784. this.$popover.hide();
  5785. };
  5786. };
  5787. var HintPopover = function (context) {
  5788. var self = this;
  5789. var ui = $.summernote.ui;
  5790. var POPOVER_DIST = 5;
  5791. var hint = context.options.hint || [];
  5792. var direction = context.options.hintDirection || 'bottom';
  5793. var hints = $.isArray(hint) ? hint : [hint];
  5794. this.events = {
  5795. 'summernote.keyup': function (we, e) {
  5796. if (!e.isDefaultPrevented()) {
  5797. self.handleKeyup(e);
  5798. }
  5799. },
  5800. 'summernote.keydown': function (we, e) {
  5801. self.handleKeydown(e);
  5802. },
  5803. 'summernote.dialog.shown': function () {
  5804. self.hide();
  5805. }
  5806. };
  5807. this.shouldInitialize = function () {
  5808. return hints.length > 0;
  5809. };
  5810. this.initialize = function () {
  5811. this.lastWordRange = null;
  5812. this.$popover = ui.popover({
  5813. className: 'note-hint-popover',
  5814. hideArrow: true,
  5815. direction: ''
  5816. }).render().appendTo('body');
  5817. this.$popover.hide();
  5818. this.$content = this.$popover.find('.popover-content');
  5819. this.$content.on('click', '.note-hint-item', function () {
  5820. self.$content.find('.active').removeClass('active');
  5821. $(this).addClass('active');
  5822. self.replace();
  5823. });
  5824. };
  5825. this.destroy = function () {
  5826. this.$popover.remove();
  5827. };
  5828. this.selectItem = function ($item) {
  5829. this.$content.find('.active').removeClass('active');
  5830. $item.addClass('active');
  5831. this.$content[0].scrollTop = $item[0].offsetTop - (this.$content.innerHeight() / 2);
  5832. };
  5833. this.moveDown = function () {
  5834. var $current = this.$content.find('.note-hint-item.active');
  5835. var $next = $current.next();
  5836. if ($next.length) {
  5837. this.selectItem($next);
  5838. } else {
  5839. var $nextGroup = $current.parent().next();
  5840. if (!$nextGroup.length) {
  5841. $nextGroup = this.$content.find('.note-hint-group').first();
  5842. }
  5843. this.selectItem($nextGroup.find('.note-hint-item').first());
  5844. }
  5845. };
  5846. this.moveUp = function () {
  5847. var $current = this.$content.find('.note-hint-item.active');
  5848. var $prev = $current.prev();
  5849. if ($prev.length) {
  5850. this.selectItem($prev);
  5851. } else {
  5852. var $prevGroup = $current.parent().prev();
  5853. if (!$prevGroup.length) {
  5854. $prevGroup = this.$content.find('.note-hint-group').last();
  5855. }
  5856. this.selectItem($prevGroup.find('.note-hint-item').last());
  5857. }
  5858. };
  5859. this.replace = function () {
  5860. var $item = this.$content.find('.note-hint-item.active');
  5861. if ($item.length) {
  5862. var node = this.nodeFromItem($item);
  5863. // XXX: consider to move codes to editor for recording redo/undo.
  5864. this.lastWordRange.insertNode(node);
  5865. range.createFromNode(node).collapse().select();
  5866. this.lastWordRange = null;
  5867. this.hide();
  5868. context.triggerEvent('change', context.layoutInfo.editable.html(), context.layoutInfo.editable);
  5869. context.invoke('editor.focus');
  5870. }
  5871. };
  5872. this.nodeFromItem = function ($item) {
  5873. var hint = hints[$item.data('index')];
  5874. var item = $item.data('item');
  5875. var node = hint.content ? hint.content(item) : item;
  5876. if (typeof node === 'string') {
  5877. node = dom.createText(node);
  5878. }
  5879. return node;
  5880. };
  5881. this.createItemTemplates = function (hintIdx, items) {
  5882. var hint = hints[hintIdx];
  5883. return items.map(function (item, idx) {
  5884. var $item = $('<div class="note-hint-item"/>');
  5885. $item.append(hint.template ? hint.template(item) : item + '');
  5886. $item.data({
  5887. 'index': hintIdx,
  5888. 'item': item
  5889. });
  5890. if (hintIdx === 0 && idx === 0) {
  5891. $item.addClass('active');
  5892. }
  5893. return $item;
  5894. });
  5895. };
  5896. this.handleKeydown = function (e) {
  5897. if (!this.$popover.is(':visible')) {
  5898. return;
  5899. }
  5900. if (e.keyCode === key.code.ENTER) {
  5901. e.preventDefault();
  5902. this.replace();
  5903. } else if (e.keyCode === key.code.UP) {
  5904. e.preventDefault();
  5905. this.moveUp();
  5906. } else if (e.keyCode === key.code.DOWN) {
  5907. e.preventDefault();
  5908. this.moveDown();
  5909. }
  5910. };
  5911. this.searchKeyword = function (index, keyword, callback) {
  5912. var hint = hints[index];
  5913. if (hint && hint.match.test(keyword) && hint.search) {
  5914. var matches = hint.match.exec(keyword);
  5915. hint.search(matches[1], callback);
  5916. } else {
  5917. callback();
  5918. }
  5919. };
  5920. this.createGroup = function (idx, keyword) {
  5921. var $group = $('<div class="note-hint-group note-hint-group-' + idx + '"/>');
  5922. this.searchKeyword(idx, keyword, function (items) {
  5923. items = items || [];
  5924. if (items.length) {
  5925. $group.html(self.createItemTemplates(idx, items));
  5926. self.show();
  5927. }
  5928. });
  5929. return $group;
  5930. };
  5931. this.handleKeyup = function (e) {
  5932. if (list.contains([key.code.ENTER, key.code.UP, key.code.DOWN], e.keyCode)) {
  5933. if (e.keyCode === key.code.ENTER) {
  5934. if (this.$popover.is(':visible')) {
  5935. return;
  5936. }
  5937. }
  5938. } else {
  5939. var wordRange = context.invoke('editor.createRange').getWordRange();
  5940. var keyword = wordRange.toString();
  5941. if (hints.length && keyword) {
  5942. this.$content.empty();
  5943. var bnd = func.rect2bnd(list.last(wordRange.getClientRects()));
  5944. if (bnd) {
  5945. this.$popover.hide();
  5946. this.lastWordRange = wordRange;
  5947. hints.forEach(function (hint, idx) {
  5948. if (hint.match.test(keyword)) {
  5949. self.createGroup(idx, keyword).appendTo(self.$content);
  5950. }
  5951. });
  5952. // set position for popover after group is created
  5953. if (direction === 'top') {
  5954. this.$popover.css({
  5955. left: bnd.left,
  5956. top: bnd.top - this.$popover.outerHeight() - POPOVER_DIST
  5957. });
  5958. } else {
  5959. this.$popover.css({
  5960. left: bnd.left,
  5961. top: bnd.top + bnd.height + POPOVER_DIST
  5962. });
  5963. }
  5964. }
  5965. } else {
  5966. this.hide();
  5967. }
  5968. }
  5969. };
  5970. this.show = function () {
  5971. this.$popover.show();
  5972. };
  5973. this.hide = function () {
  5974. this.$popover.hide();
  5975. };
  5976. };
  5977. $.summernote = $.extend($.summernote, {
  5978. version: '0.8.4',
  5979. ui: ui,
  5980. dom: dom,
  5981. plugins: {},
  5982. options: {
  5983. modules: {
  5984. 'editor': Editor,
  5985. 'clipboard': Clipboard,
  5986. 'dropzone': Dropzone,
  5987. 'codeview': Codeview,
  5988. 'statusbar': Statusbar,
  5989. 'fullscreen': Fullscreen,
  5990. 'handle': Handle,
  5991. // FIXME: HintPopover must be front of autolink
  5992. // - Script error about range when Enter key is pressed on hint popover
  5993. 'hintPopover': HintPopover,
  5994. 'autoLink': AutoLink,
  5995. 'autoSync': AutoSync,
  5996. 'placeholder': Placeholder,
  5997. 'buttons': Buttons,
  5998. 'toolbar': Toolbar,
  5999. 'linkDialog': LinkDialog,
  6000. 'linkPopover': LinkPopover,
  6001. 'imageDialog': ImageDialog,
  6002. 'imagePopover': ImagePopover,
  6003. 'videoDialog': VideoDialog,
  6004. 'helpDialog': HelpDialog,
  6005. 'airPopover': AirPopover
  6006. },
  6007. buttons: {},
  6008. lang: 'en-US',
  6009. // toolbar
  6010. toolbar: [
  6011. ['style', ['style']],
  6012. ['font', ['bold', 'underline', 'clear']],
  6013. ['fontname', ['fontname']],
  6014. ['color', ['color']],
  6015. ['para', ['ul', 'ol', 'paragraph']],
  6016. ['table', ['table']],
  6017. ['insert', ['link', 'picture', 'video']],
  6018. ['view', ['fullscreen', 'codeview', 'help']]
  6019. ],
  6020. // popover
  6021. popover: {
  6022. image: [
  6023. ['imagesize', ['imageSize100', 'imageSize50', 'imageSize25']],
  6024. ['float', ['floatLeft', 'floatRight', 'floatNone']],
  6025. ['remove', ['removeMedia']]
  6026. ],
  6027. link: [
  6028. ['link', ['linkDialogShow', 'unlink']]
  6029. ],
  6030. air: [
  6031. ['color', ['color']],
  6032. ['font', ['bold', 'underline', 'clear']],
  6033. ['para', ['ul', 'paragraph']],
  6034. ['table', ['table']],
  6035. ['insert', ['link', 'picture']]
  6036. ]
  6037. },
  6038. // air mode: inline editor
  6039. airMode: false,
  6040. width: null,
  6041. height: null,
  6042. linkTargetBlank: true,
  6043. focus: false,
  6044. tabSize: 4,
  6045. styleWithSpan: true,
  6046. shortcuts: true,
  6047. textareaAutoSync: true,
  6048. direction: null,
  6049. tooltip: 'auto',
  6050. styleTags: ['p', 'blockquote', 'pre', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
  6051. fontNames: [
  6052. 'Arial', 'Arial Black', 'Comic Sans MS', 'Courier New',
  6053. 'Helvetica Neue', 'Helvetica', 'Impact', 'Lucida Grande',
  6054. 'Tahoma', 'Times New Roman', 'Verdana'
  6055. ],
  6056. fontSizes: ['8', '9', '10', '11', '12', '14', '18', '24', '36'],
  6057. // pallete colors(n x n)
  6058. colors: [
  6059. ['#000000', '#424242', '#636363', '#9C9C94', '#CEC6CE', '#EFEFEF', '#F7F7F7', '#FFFFFF'],
  6060. ['#FF0000', '#FF9C00', '#FFFF00', '#00FF00', '#00FFFF', '#0000FF', '#9C00FF', '#FF00FF'],
  6061. ['#F7C6CE', '#FFE7CE', '#FFEFC6', '#D6EFD6', '#CEDEE7', '#CEE7F7', '#D6D6E7', '#E7D6DE'],
  6062. ['#E79C9C', '#FFC69C', '#FFE79C', '#B5D6A5', '#A5C6CE', '#9CC6EF', '#B5A5D6', '#D6A5BD'],
  6063. ['#E76363', '#F7AD6B', '#FFD663', '#94BD7B', '#73A5AD', '#6BADDE', '#8C7BC6', '#C67BA5'],
  6064. ['#CE0000', '#E79439', '#EFC631', '#6BA54A', '#4A7B8C', '#3984C6', '#634AA5', '#A54A7B'],
  6065. ['#9C0000', '#B56308', '#BD9400', '#397B21', '#104A5A', '#085294', '#311873', '#731842'],
  6066. ['#630000', '#7B3900', '#846300', '#295218', '#083139', '#003163', '#21104A', '#4A1031']
  6067. ],
  6068. lineHeights: ['1.0', '1.2', '1.4', '1.5', '1.6', '1.8', '2.0', '3.0'],
  6069. tableClassName: 'table table-bordered',
  6070. insertTableMaxSize: {
  6071. col: 10,
  6072. row: 10
  6073. },
  6074. dialogsInBody: false,
  6075. dialogsFade: false,
  6076. maximumImageFileSize: null,
  6077. callbacks: {
  6078. onInit: null,
  6079. onFocus: null,
  6080. onBlur: null,
  6081. onEnter: null,
  6082. onKeyup: null,
  6083. onKeydown: null,
  6084. onImageUpload: null,
  6085. onImageUploadError: null
  6086. },
  6087. codemirror: {
  6088. mode: 'text/html',
  6089. htmlMode: true,
  6090. lineNumbers: true
  6091. },
  6092. keyMap: {
  6093. pc: {
  6094. 'ENTER': 'insertParagraph',
  6095. 'CTRL+Z': 'undo',
  6096. 'CTRL+Y': 'redo',
  6097. 'TAB': 'tab',
  6098. 'SHIFT+TAB': 'untab',
  6099. 'CTRL+B': 'bold',
  6100. 'CTRL+I': 'italic',
  6101. 'CTRL+U': 'underline',
  6102. 'CTRL+SHIFT+S': 'strikethrough',
  6103. 'CTRL+BACKSLASH': 'removeFormat',
  6104. 'CTRL+SHIFT+L': 'justifyLeft',
  6105. 'CTRL+SHIFT+E': 'justifyCenter',
  6106. 'CTRL+SHIFT+R': 'justifyRight',
  6107. 'CTRL+SHIFT+J': 'justifyFull',
  6108. 'CTRL+SHIFT+NUM7': 'insertUnorderedList',
  6109. 'CTRL+SHIFT+NUM8': 'insertOrderedList',
  6110. 'CTRL+LEFTBRACKET': 'outdent',
  6111. 'CTRL+RIGHTBRACKET': 'indent',
  6112. 'CTRL+NUM0': 'formatPara',
  6113. 'CTRL+NUM1': 'formatH1',
  6114. 'CTRL+NUM2': 'formatH2',
  6115. 'CTRL+NUM3': 'formatH3',
  6116. 'CTRL+NUM4': 'formatH4',
  6117. 'CTRL+NUM5': 'formatH5',
  6118. 'CTRL+NUM6': 'formatH6',
  6119. 'CTRL+ENTER': 'insertHorizontalRule',
  6120. 'CTRL+K': 'linkDialog.show'
  6121. },
  6122. mac: {
  6123. 'ENTER': 'insertParagraph',
  6124. 'CMD+Z': 'undo',
  6125. 'CMD+SHIFT+Z': 'redo',
  6126. 'TAB': 'tab',
  6127. 'SHIFT+TAB': 'untab',
  6128. 'CMD+B': 'bold',
  6129. 'CMD+I': 'italic',
  6130. 'CMD+U': 'underline',
  6131. 'CMD+SHIFT+S': 'strikethrough',
  6132. 'CMD+BACKSLASH': 'removeFormat',
  6133. 'CMD+SHIFT+L': 'justifyLeft',
  6134. 'CMD+SHIFT+E': 'justifyCenter',
  6135. 'CMD+SHIFT+R': 'justifyRight',
  6136. 'CMD+SHIFT+J': 'justifyFull',
  6137. 'CMD+SHIFT+NUM7': 'insertUnorderedList',
  6138. 'CMD+SHIFT+NUM8': 'insertOrderedList',
  6139. 'CMD+LEFTBRACKET': 'outdent',
  6140. 'CMD+RIGHTBRACKET': 'indent',
  6141. 'CMD+NUM0': 'formatPara',
  6142. 'CMD+NUM1': 'formatH1',
  6143. 'CMD+NUM2': 'formatH2',
  6144. 'CMD+NUM3': 'formatH3',
  6145. 'CMD+NUM4': 'formatH4',
  6146. 'CMD+NUM5': 'formatH5',
  6147. 'CMD+NUM6': 'formatH6',
  6148. 'CMD+ENTER': 'insertHorizontalRule',
  6149. 'CMD+K': 'linkDialog.show'
  6150. }
  6151. },
  6152. icons: {
  6153. 'align': 'note-icon-align',
  6154. 'alignCenter': 'note-icon-align-center',
  6155. 'alignJustify': 'note-icon-align-justify',
  6156. 'alignLeft': 'note-icon-align-left',
  6157. 'alignRight': 'note-icon-align-right',
  6158. 'indent': 'note-icon-align-indent',
  6159. 'outdent': 'note-icon-align-outdent',
  6160. 'arrowsAlt': 'note-icon-arrows-alt',
  6161. 'bold': 'note-icon-bold',
  6162. 'caret': 'note-icon-caret',
  6163. 'circle': 'note-icon-circle',
  6164. 'close': 'note-icon-close',
  6165. 'code': 'note-icon-code',
  6166. 'eraser': 'note-icon-eraser',
  6167. 'font': 'note-icon-font',
  6168. 'frame': 'note-icon-frame',
  6169. 'italic': 'note-icon-italic',
  6170. 'link': 'note-icon-link',
  6171. 'unlink': 'note-icon-chain-broken',
  6172. 'magic': 'note-icon-magic',
  6173. 'menuCheck': 'note-icon-check',
  6174. 'minus': 'note-icon-minus',
  6175. 'orderedlist': 'note-icon-orderedlist',
  6176. 'pencil': 'note-icon-pencil',
  6177. 'picture': 'note-icon-picture',
  6178. 'question': 'note-icon-question',
  6179. 'redo': 'note-icon-redo',
  6180. 'square': 'note-icon-square',
  6181. 'strikethrough': 'note-icon-strikethrough',
  6182. 'subscript': 'note-icon-subscript',
  6183. 'superscript': 'note-icon-superscript',
  6184. 'table': 'note-icon-table',
  6185. 'textHeight': 'note-icon-text-height',
  6186. 'trash': 'note-icon-trash',
  6187. 'underline': 'note-icon-underline',
  6188. 'undo': 'note-icon-undo',
  6189. 'unorderedlist': 'note-icon-unorderedlist',
  6190. 'video': 'note-icon-video'
  6191. }
  6192. }
  6193. });
  6194. }));