PageRenderTime 34ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/files/tinymce/4.1.2/plugins/noneditable/plugin.js

https://gitlab.com/Mirros/jsdelivr
JavaScript | 540 lines | 388 code | 84 blank | 68 comment | 143 complexity | 895d72fb047ed52f0b77d1a8cc16af10 MD5 | raw file
  1. /**
  2. * plugin.js
  3. *
  4. * Copyright, Moxiecode Systems AB
  5. * Released under LGPL License.
  6. *
  7. * License: http://www.tinymce.com/license
  8. * Contributing: http://www.tinymce.com/contributing
  9. */
  10. /*jshint loopfunc:true */
  11. /*eslint no-loop-func:0 */
  12. /*global tinymce:true */
  13. tinymce.PluginManager.add('noneditable', function(editor) {
  14. var TreeWalker = tinymce.dom.TreeWalker;
  15. var externalName = 'contenteditable', internalName = 'data-mce-' + externalName;
  16. var VK = tinymce.util.VK;
  17. // Returns the content editable state of a node "true/false" or null
  18. function getContentEditable(node) {
  19. var contentEditable;
  20. // Ignore non elements
  21. if (node.nodeType === 1) {
  22. // Check for fake content editable
  23. contentEditable = node.getAttribute(internalName);
  24. if (contentEditable && contentEditable !== "inherit") {
  25. return contentEditable;
  26. }
  27. // Check for real content editable
  28. contentEditable = node.contentEditable;
  29. if (contentEditable !== "inherit") {
  30. return contentEditable;
  31. }
  32. }
  33. return null;
  34. }
  35. // Returns the noneditable parent or null if there is a editable before it or if it wasn't found
  36. function getNonEditableParent(node) {
  37. var state;
  38. while (node) {
  39. state = getContentEditable(node);
  40. if (state) {
  41. return state === "false" ? node : null;
  42. }
  43. node = node.parentNode;
  44. }
  45. }
  46. function handleContentEditableSelection() {
  47. var dom = editor.dom, selection = editor.selection, caretContainerId = 'mce_noneditablecaret', invisibleChar = '\uFEFF';
  48. // Get caret container parent for the specified node
  49. function getParentCaretContainer(node) {
  50. while (node) {
  51. if (node.id === caretContainerId) {
  52. return node;
  53. }
  54. node = node.parentNode;
  55. }
  56. }
  57. // Finds the first text node in the specified node
  58. function findFirstTextNode(node) {
  59. var walker;
  60. if (node) {
  61. walker = new TreeWalker(node, node);
  62. for (node = walker.current(); node; node = walker.next()) {
  63. if (node.nodeType === 3) {
  64. return node;
  65. }
  66. }
  67. }
  68. }
  69. // Insert caret container before/after target or expand selection to include block
  70. function insertCaretContainerOrExpandToBlock(target, before) {
  71. var caretContainer, rng;
  72. // Select block
  73. if (getContentEditable(target) === "false") {
  74. if (dom.isBlock(target)) {
  75. selection.select(target);
  76. return;
  77. }
  78. }
  79. rng = dom.createRng();
  80. if (getContentEditable(target) === "true") {
  81. if (!target.firstChild) {
  82. target.appendChild(editor.getDoc().createTextNode('\u00a0'));
  83. }
  84. target = target.firstChild;
  85. before = true;
  86. }
  87. /*
  88. caretContainer = dom.create('span', {
  89. id: caretContainerId,
  90. 'data-mce-bogus': true,
  91. style:'border: 1px solid red'
  92. }, invisibleChar);
  93. */
  94. caretContainer = dom.create('span', {id: caretContainerId, 'data-mce-bogus': true}, invisibleChar);
  95. if (before) {
  96. target.parentNode.insertBefore(caretContainer, target);
  97. } else {
  98. dom.insertAfter(caretContainer, target);
  99. }
  100. rng.setStart(caretContainer.firstChild, 1);
  101. rng.collapse(true);
  102. selection.setRng(rng);
  103. return caretContainer;
  104. }
  105. // Removes any caret container except the one we might be in
  106. function removeCaretContainer(caretContainer) {
  107. var rng, child, currentCaretContainer, lastContainer;
  108. if (caretContainer) {
  109. rng = selection.getRng(true);
  110. rng.setStartBefore(caretContainer);
  111. rng.setEndBefore(caretContainer);
  112. child = findFirstTextNode(caretContainer);
  113. if (child && child.nodeValue.charAt(0) == invisibleChar) {
  114. child = child.deleteData(0, 1);
  115. }
  116. dom.remove(caretContainer, true);
  117. selection.setRng(rng);
  118. } else {
  119. currentCaretContainer = getParentCaretContainer(selection.getStart());
  120. while ((caretContainer = dom.get(caretContainerId)) && caretContainer !== lastContainer) {
  121. if (currentCaretContainer !== caretContainer) {
  122. child = findFirstTextNode(caretContainer);
  123. if (child && child.nodeValue.charAt(0) == invisibleChar) {
  124. child = child.deleteData(0, 1);
  125. }
  126. dom.remove(caretContainer, true);
  127. }
  128. lastContainer = caretContainer;
  129. }
  130. }
  131. }
  132. // Modifies the selection to include contentEditable false elements or insert caret containers
  133. function moveSelection() {
  134. var nonEditableStart, nonEditableEnd, isCollapsed, rng, element;
  135. // Checks if there is any contents to the left/right side of caret returns the noneditable element or
  136. // any editable element if it finds one inside
  137. function hasSideContent(element, left) {
  138. var container, offset, walker, node, len;
  139. container = rng.startContainer;
  140. offset = rng.startOffset;
  141. // If endpoint is in middle of text node then expand to beginning/end of element
  142. if (container.nodeType == 3) {
  143. len = container.nodeValue.length;
  144. if ((offset > 0 && offset < len) || (left ? offset == len : offset === 0)) {
  145. return;
  146. }
  147. } else {
  148. // Can we resolve the node by index
  149. if (offset < container.childNodes.length) {
  150. // Browser represents caret position as the offset at the start of an element. When moving right
  151. // this is the element we are moving into so we consider our container to be child node at offset-1
  152. var pos = !left && offset > 0 ? offset - 1 : offset;
  153. container = container.childNodes[pos];
  154. if (container.hasChildNodes()) {
  155. container = container.firstChild;
  156. }
  157. } else {
  158. // If not then the caret is at the last position in it's container and the caret container
  159. // should be inserted after the noneditable element
  160. return !left ? element : null;
  161. }
  162. }
  163. // Walk left/right to look for contents
  164. walker = new TreeWalker(container, element);
  165. while ((node = walker[left ? 'prev' : 'next']())) {
  166. if (node.nodeType === 3 && node.nodeValue.length > 0) {
  167. return;
  168. } else if (getContentEditable(node) === "true") {
  169. // Found contentEditable=true element return this one to we can move the caret inside it
  170. return node;
  171. }
  172. }
  173. return element;
  174. }
  175. // Remove any existing caret containers
  176. removeCaretContainer();
  177. // Get noneditable start/end elements
  178. isCollapsed = selection.isCollapsed();
  179. nonEditableStart = getNonEditableParent(selection.getStart());
  180. nonEditableEnd = getNonEditableParent(selection.getEnd());
  181. // Is any fo the range endpoints noneditable
  182. if (nonEditableStart || nonEditableEnd) {
  183. rng = selection.getRng(true);
  184. // If it's a caret selection then look left/right to see if we need to move the caret out side or expand
  185. if (isCollapsed) {
  186. nonEditableStart = nonEditableStart || nonEditableEnd;
  187. if ((element = hasSideContent(nonEditableStart, true))) {
  188. // We have no contents to the left of the caret then insert a caret container before the noneditable element
  189. insertCaretContainerOrExpandToBlock(element, true);
  190. } else if ((element = hasSideContent(nonEditableStart, false))) {
  191. // We have no contents to the right of the caret then insert a caret container after the noneditable element
  192. insertCaretContainerOrExpandToBlock(element, false);
  193. } else {
  194. // We are in the middle of a noneditable so expand to select it
  195. selection.select(nonEditableStart);
  196. }
  197. } else {
  198. rng = selection.getRng(true);
  199. // Expand selection to include start non editable element
  200. if (nonEditableStart) {
  201. rng.setStartBefore(nonEditableStart);
  202. }
  203. // Expand selection to include end non editable element
  204. if (nonEditableEnd) {
  205. rng.setEndAfter(nonEditableEnd);
  206. }
  207. selection.setRng(rng);
  208. }
  209. }
  210. }
  211. function handleKey(e) {
  212. var keyCode = e.keyCode, nonEditableParent, caretContainer, startElement, endElement;
  213. function getNonEmptyTextNodeSibling(node, prev) {
  214. while ((node = node[prev ? 'previousSibling' : 'nextSibling'])) {
  215. if (node.nodeType !== 3 || node.nodeValue.length > 0) {
  216. return node;
  217. }
  218. }
  219. }
  220. function positionCaretOnElement(element, start) {
  221. selection.select(element);
  222. selection.collapse(start);
  223. }
  224. function canDelete(backspace) {
  225. var rng, container, offset, nonEditableParent;
  226. function removeNodeIfNotParent(node) {
  227. var parent = container;
  228. while (parent) {
  229. if (parent === node) {
  230. return;
  231. }
  232. parent = parent.parentNode;
  233. }
  234. dom.remove(node);
  235. moveSelection();
  236. }
  237. function isNextPrevTreeNodeNonEditable() {
  238. var node, walker, nonEmptyElements = editor.schema.getNonEmptyElements();
  239. walker = new tinymce.dom.TreeWalker(container, editor.getBody());
  240. while ((node = (backspace ? walker.prev() : walker.next()))) {
  241. // Found IMG/INPUT etc
  242. if (nonEmptyElements[node.nodeName.toLowerCase()]) {
  243. break;
  244. }
  245. // Found text node with contents
  246. if (node.nodeType === 3 && tinymce.trim(node.nodeValue).length > 0) {
  247. break;
  248. }
  249. // Found non editable node
  250. if (getContentEditable(node) === "false") {
  251. removeNodeIfNotParent(node);
  252. return true;
  253. }
  254. }
  255. // Check if the content node is within a non editable parent
  256. if (getNonEditableParent(node)) {
  257. return true;
  258. }
  259. return false;
  260. }
  261. if (selection.isCollapsed()) {
  262. rng = selection.getRng(true);
  263. container = rng.startContainer;
  264. offset = rng.startOffset;
  265. container = getParentCaretContainer(container) || container;
  266. // Is in noneditable parent
  267. if ((nonEditableParent = getNonEditableParent(container))) {
  268. removeNodeIfNotParent(nonEditableParent);
  269. return false;
  270. }
  271. // Check if the caret is in the middle of a text node
  272. if (container.nodeType == 3 && (backspace ? offset > 0 : offset < container.nodeValue.length)) {
  273. return true;
  274. }
  275. // Resolve container index
  276. if (container.nodeType == 1) {
  277. container = container.childNodes[offset] || container;
  278. }
  279. // Check if previous or next tree node is non editable then block the event
  280. if (isNextPrevTreeNodeNonEditable()) {
  281. return false;
  282. }
  283. }
  284. return true;
  285. }
  286. startElement = selection.getStart();
  287. endElement = selection.getEnd();
  288. // Disable all key presses in contentEditable=false except delete or backspace
  289. nonEditableParent = getNonEditableParent(startElement) || getNonEditableParent(endElement);
  290. if (nonEditableParent && (keyCode < 112 || keyCode > 124) && keyCode != VK.DELETE && keyCode != VK.BACKSPACE) {
  291. // Is Ctrl+c, Ctrl+v or Ctrl+x then use default browser behavior
  292. if ((tinymce.isMac ? e.metaKey : e.ctrlKey) && (keyCode == 67 || keyCode == 88 || keyCode == 86)) {
  293. return;
  294. }
  295. e.preventDefault();
  296. // Arrow left/right select the element and collapse left/right
  297. if (keyCode == VK.LEFT || keyCode == VK.RIGHT) {
  298. var left = keyCode == VK.LEFT;
  299. // If a block element find previous or next element to position the caret
  300. if (editor.dom.isBlock(nonEditableParent)) {
  301. var targetElement = left ? nonEditableParent.previousSibling : nonEditableParent.nextSibling;
  302. var walker = new TreeWalker(targetElement, targetElement);
  303. var caretElement = left ? walker.prev() : walker.next();
  304. positionCaretOnElement(caretElement, !left);
  305. } else {
  306. positionCaretOnElement(nonEditableParent, left);
  307. }
  308. }
  309. } else {
  310. // Is arrow left/right, backspace or delete
  311. if (keyCode == VK.LEFT || keyCode == VK.RIGHT || keyCode == VK.BACKSPACE || keyCode == VK.DELETE) {
  312. caretContainer = getParentCaretContainer(startElement);
  313. if (caretContainer) {
  314. // Arrow left or backspace
  315. if (keyCode == VK.LEFT || keyCode == VK.BACKSPACE) {
  316. nonEditableParent = getNonEmptyTextNodeSibling(caretContainer, true);
  317. if (nonEditableParent && getContentEditable(nonEditableParent) === "false") {
  318. e.preventDefault();
  319. if (keyCode == VK.LEFT) {
  320. positionCaretOnElement(nonEditableParent, true);
  321. } else {
  322. dom.remove(nonEditableParent);
  323. return;
  324. }
  325. } else {
  326. removeCaretContainer(caretContainer);
  327. }
  328. }
  329. // Arrow right or delete
  330. if (keyCode == VK.RIGHT || keyCode == VK.DELETE) {
  331. nonEditableParent = getNonEmptyTextNodeSibling(caretContainer);
  332. if (nonEditableParent && getContentEditable(nonEditableParent) === "false") {
  333. e.preventDefault();
  334. if (keyCode == VK.RIGHT) {
  335. positionCaretOnElement(nonEditableParent, false);
  336. } else {
  337. dom.remove(nonEditableParent);
  338. return;
  339. }
  340. } else {
  341. removeCaretContainer(caretContainer);
  342. }
  343. }
  344. }
  345. if ((keyCode == VK.BACKSPACE || keyCode == VK.DELETE) && !canDelete(keyCode == VK.BACKSPACE)) {
  346. e.preventDefault();
  347. return false;
  348. }
  349. }
  350. }
  351. }
  352. editor.on('mousedown', function(e) {
  353. var node = editor.selection.getNode();
  354. if (getContentEditable(node) === "false" && node == e.target) {
  355. // Expand selection on mouse down we can't block the default event since it's used for drag/drop
  356. moveSelection();
  357. }
  358. });
  359. editor.on('mouseup keyup', moveSelection);
  360. editor.on('keydown', handleKey);
  361. }
  362. var editClass, nonEditClass, nonEditableRegExps;
  363. // Converts configured regexps to noneditable span items
  364. function convertRegExpsToNonEditable(e) {
  365. var i = nonEditableRegExps.length, content = e.content, cls = tinymce.trim(nonEditClass);
  366. // Don't replace the variables when raw is used for example on undo/redo
  367. if (e.format == "raw") {
  368. return;
  369. }
  370. while (i--) {
  371. content = content.replace(nonEditableRegExps[i], function(match) {
  372. var args = arguments, index = args[args.length - 2];
  373. // Is value inside an attribute then don't replace
  374. if (index > 0 && content.charAt(index - 1) == '"') {
  375. return match;
  376. }
  377. return (
  378. '<span class="' + cls + '" data-mce-content="' + editor.dom.encode(args[0]) + '">' +
  379. editor.dom.encode(typeof(args[1]) === "string" ? args[1] : args[0]) + '</span>'
  380. );
  381. });
  382. }
  383. e.content = content;
  384. }
  385. editClass = " " + tinymce.trim(editor.getParam("noneditable_editable_class", "mceEditable")) + " ";
  386. nonEditClass = " " + tinymce.trim(editor.getParam("noneditable_noneditable_class", "mceNonEditable")) + " ";
  387. // Setup noneditable regexps array
  388. nonEditableRegExps = editor.getParam("noneditable_regexp");
  389. if (nonEditableRegExps && !nonEditableRegExps.length) {
  390. nonEditableRegExps = [nonEditableRegExps];
  391. }
  392. editor.on('PreInit', function() {
  393. handleContentEditableSelection();
  394. if (nonEditableRegExps) {
  395. editor.on('BeforeSetContent', convertRegExpsToNonEditable);
  396. }
  397. // Apply contentEditable true/false on elements with the noneditable/editable classes
  398. editor.parser.addAttributeFilter('class', function(nodes) {
  399. var i = nodes.length, className, node;
  400. while (i--) {
  401. node = nodes[i];
  402. className = " " + node.attr("class") + " ";
  403. if (className.indexOf(editClass) !== -1) {
  404. node.attr(internalName, "true");
  405. } else if (className.indexOf(nonEditClass) !== -1) {
  406. node.attr(internalName, "false");
  407. }
  408. }
  409. });
  410. // Remove internal name
  411. editor.serializer.addAttributeFilter(internalName, function(nodes) {
  412. var i = nodes.length, node;
  413. while (i--) {
  414. node = nodes[i];
  415. if (nonEditableRegExps && node.attr('data-mce-content')) {
  416. node.name = "#text";
  417. node.type = 3;
  418. node.raw = true;
  419. node.value = node.attr('data-mce-content');
  420. } else {
  421. node.attr(externalName, null);
  422. node.attr(internalName, null);
  423. }
  424. }
  425. });
  426. // Convert external name into internal name
  427. editor.parser.addAttributeFilter(externalName, function(nodes) {
  428. var i = nodes.length, node;
  429. while (i--) {
  430. node = nodes[i];
  431. node.attr(internalName, node.attr(externalName));
  432. node.attr(externalName, null);
  433. }
  434. });
  435. });
  436. editor.on('drop', function(e) {
  437. if (getNonEditableParent(e.target)) {
  438. e.preventDefault();
  439. }
  440. });
  441. });