The version of vichan running on lainchan.org
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

7121 line
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. }));