*/\r
\r
(function() {\r
- var Event = tinymce.dom.Event;\r
+ var TreeWalker = tinymce.dom.TreeWalker;\r
+ var externalName = 'contenteditable', internalName = 'data-mce-' + externalName;\r
+ var VK = tinymce.VK;\r
+\r
+ function handleContentEditableSelection(ed) {\r
+ var dom = ed.dom, selection = ed.selection, invisibleChar, caretContainerId = 'mce_noneditablecaret';\r
+\r
+ // Setup invisible character use zero width space on Gecko since it doesn't change the height of the container\r
+ invisibleChar = tinymce.isGecko ? '\u200B' : '\uFEFF';\r
+\r
+ // Returns the content editable state of a node "true/false" or null\r
+ function getContentEditable(node) {\r
+ var contentEditable;\r
+\r
+ // Ignore non elements\r
+ if (node.nodeType === 1) {\r
+ // Check for fake content editable\r
+ contentEditable = node.getAttribute(internalName);\r
+ if (contentEditable && contentEditable !== "inherit") {\r
+ return contentEditable;\r
+ }\r
+\r
+ // Check for real content editable\r
+ contentEditable = node.contentEditable;\r
+ if (contentEditable !== "inherit") {\r
+ return contentEditable;\r
+ }\r
+ }\r
+\r
+ return null;\r
+ };\r
+\r
+ // Returns the noneditable parent or null if there is a editable before it or if it wasn't found\r
+ function getNonEditableParent(node) {\r
+ var state;\r
+\r
+ while (node) {\r
+ state = getContentEditable(node);\r
+ if (state) {\r
+ return state === "false" ? node : null;\r
+ }\r
+\r
+ node = node.parentNode;\r
+ }\r
+ };\r
+\r
+ // Get caret container parent for the specified node\r
+ function getParentCaretContainer(node) {\r
+ while (node) {\r
+ if (node.id === caretContainerId) {\r
+ return node;\r
+ }\r
+\r
+ node = node.parentNode;\r
+ }\r
+ };\r
+\r
+ // Finds the first text node in the specified node\r
+ function findFirstTextNode(node) {\r
+ var walker;\r
+\r
+ if (node) {\r
+ walker = new TreeWalker(node, node);\r
+\r
+ for (node = walker.current(); node; node = walker.next()) {\r
+ if (node.nodeType === 3) {\r
+ return node;\r
+ }\r
+ }\r
+ }\r
+ };\r
+\r
+ // Insert caret container before/after target or expand selection to include block\r
+ function insertCaretContainerOrExpandToBlock(target, before) {\r
+ var caretContainer, rng;\r
+\r
+ // Select block\r
+ if (getContentEditable(target) === "false") {\r
+ if (dom.isBlock(target)) {\r
+ selection.select(target);\r
+ return;\r
+ }\r
+ }\r
+\r
+ rng = dom.createRng();\r
+\r
+ if (getContentEditable(target) === "true") {\r
+ if (!target.firstChild) {\r
+ target.appendChild(ed.getDoc().createTextNode('\u00a0'));\r
+ }\r
+\r
+ target = target.firstChild;\r
+ before = true;\r
+ }\r
+\r
+ //caretContainer = dom.create('span', {id: caretContainerId, 'data-mce-bogus': true, style:'border: 1px solid red'}, invisibleChar);\r
+ caretContainer = dom.create('span', {id: caretContainerId, 'data-mce-bogus': true}, invisibleChar);\r
+\r
+ if (before) {\r
+ target.parentNode.insertBefore(caretContainer, target);\r
+ } else {\r
+ dom.insertAfter(caretContainer, target);\r
+ }\r
+\r
+ rng.setStart(caretContainer.firstChild, 1);\r
+ rng.collapse(true);\r
+ selection.setRng(rng);\r
+\r
+ return caretContainer;\r
+ };\r
+\r
+ // Removes any caret container except the one we might be in\r
+ function removeCaretContainer(caretContainer) {\r
+ var child, currentCaretContainer, lastContainer;\r
+\r
+ if (caretContainer) {\r
+ rng = selection.getRng(true);\r
+ rng.setStartBefore(caretContainer);\r
+ rng.setEndBefore(caretContainer);\r
+\r
+ child = findFirstTextNode(caretContainer);\r
+ if (child && child.nodeValue.charAt(0) == invisibleChar) {\r
+ child = child.deleteData(0, 1);\r
+ }\r
+\r
+ dom.remove(caretContainer, true);\r
+\r
+ selection.setRng(rng);\r
+ } else {\r
+ currentCaretContainer = getParentCaretContainer(selection.getStart());\r
+ while ((caretContainer = dom.get(caretContainerId)) && caretContainer !== lastContainer) {\r
+ if (currentCaretContainer !== caretContainer) {\r
+ child = findFirstTextNode(caretContainer);\r
+ if (child && child.nodeValue.charAt(0) == invisibleChar) {\r
+ child = child.deleteData(0, 1);\r
+ }\r
+\r
+ dom.remove(caretContainer, true);\r
+ }\r
+\r
+ lastContainer = caretContainer;\r
+ }\r
+ }\r
+ };\r
+\r
+ // Modifies the selection to include contentEditable false elements or insert caret containers\r
+ function moveSelection() {\r
+ var nonEditableStart, nonEditableEnd, isCollapsed, rng, element;\r
+\r
+ // Checks if there is any contents to the left/right side of caret returns the noneditable element or any editable element if it finds one inside\r
+ function hasSideContent(element, left) {\r
+ var container, offset, walker, node, len;\r
+\r
+ container = rng.startContainer;\r
+ offset = rng.startOffset;\r
+\r
+ // If endpoint is in middle of text node then expand to beginning/end of element\r
+ if (container.nodeType == 3) {\r
+ len = container.nodeValue.length;\r
+ if ((offset > 0 && offset < len) || (left ? offset == len : offset == 0)) {\r
+ return;\r
+ }\r
+ } else {\r
+ // Can we resolve the node by index\r
+ if (offset < container.childNodes.length) {\r
+ // Browser represents caret position as the offset at the start of an element. When moving right\r
+ // this is the element we are moving into so we consider our container to be child node at offset-1\r
+ var pos = !left && offset > 0 ? offset-1 : offset;\r
+ container = container.childNodes[pos];\r
+ if (container.hasChildNodes()) {\r
+ container = container.firstChild;\r
+ }\r
+ } else {\r
+ // If not then the caret is at the last position in it's container and the caret container should be inserted after the noneditable element\r
+ return !left ? element : null;\r
+ }\r
+ }\r
+\r
+ // Walk left/right to look for contents\r
+ walker = new TreeWalker(container, element);\r
+ while (node = walker[left ? 'prev' : 'next']()) {\r
+ if (node.nodeType === 3 && node.nodeValue.length > 0) {\r
+ return;\r
+ } else if (getContentEditable(node) === "true") {\r
+ // Found contentEditable=true element return this one to we can move the caret inside it\r
+ return node;\r
+ }\r
+ }\r
+\r
+ return element;\r
+ };\r
+\r
+ // Remove any existing caret containers\r
+ removeCaretContainer();\r
+\r
+ // Get noneditable start/end elements\r
+ isCollapsed = selection.isCollapsed();\r
+ nonEditableStart = getNonEditableParent(selection.getStart());\r
+ nonEditableEnd = getNonEditableParent(selection.getEnd());\r
+\r
+ // Is any fo the range endpoints noneditable\r
+ if (nonEditableStart || nonEditableEnd) {\r
+ rng = selection.getRng(true);\r
+\r
+ // If it's a caret selection then look left/right to see if we need to move the caret out side or expand\r
+ if (isCollapsed) {\r
+ nonEditableStart = nonEditableStart || nonEditableEnd;\r
+ var start = selection.getStart();\r
+ if (element = hasSideContent(nonEditableStart, true)) {\r
+ // We have no contents to the left of the caret then insert a caret container before the noneditable element\r
+ insertCaretContainerOrExpandToBlock(element, true);\r
+ } else if (element = hasSideContent(nonEditableStart, false)) {\r
+ // We have no contents to the right of the caret then insert a caret container after the noneditable element\r
+ insertCaretContainerOrExpandToBlock(element, false);\r
+ } else {\r
+ // We are in the middle of a noneditable so expand to select it\r
+ selection.select(nonEditableStart);\r
+ }\r
+ } else {\r
+ rng = selection.getRng(true);\r
+\r
+ // Expand selection to include start non editable element\r
+ if (nonEditableStart) {\r
+ rng.setStartBefore(nonEditableStart);\r
+ }\r
+\r
+ // Expand selection to include end non editable element\r
+ if (nonEditableEnd) {\r
+ rng.setEndAfter(nonEditableEnd);\r
+ }\r
+\r
+ selection.setRng(rng);\r
+ }\r
+ }\r
+ };\r
+\r
+ function handleKey(ed, e) {\r
+ var keyCode = e.keyCode, nonEditableParent, caretContainer, startElement, endElement;\r
+\r
+ function getNonEmptyTextNodeSibling(node, prev) {\r
+ while (node = node[prev ? 'previousSibling' : 'nextSibling']) {\r
+ if (node.nodeType !== 3 || node.nodeValue.length > 0) {\r
+ return node;\r
+ }\r
+ }\r
+ };\r
+\r
+ function positionCaretOnElement(element, start) {\r
+ selection.select(element);\r
+ selection.collapse(start);\r
+ }\r
+\r
+ startElement = selection.getStart()\r
+ endElement = selection.getEnd();\r
+\r
+ // Disable all key presses in contentEditable=false except delete or backspace\r
+ nonEditableParent = getNonEditableParent(startElement) || getNonEditableParent(endElement);\r
+ if (nonEditableParent && (keyCode < 112 || keyCode > 124) && keyCode != VK.DELETE && keyCode != VK.BACKSPACE) {\r
+ e.preventDefault();\r
+\r
+ // Arrow left/right select the element and collapse left/right\r
+ if (keyCode == VK.LEFT || keyCode == VK.RIGHT) {\r
+ var left = keyCode == VK.LEFT;\r
+ // If a block element find previous or next element to position the caret\r
+ if (ed.dom.isBlock(nonEditableParent)) {\r
+ var targetElement = left ? nonEditableParent.previousSibling : nonEditableParent.nextSibling;\r
+ var walker = new TreeWalker(targetElement, targetElement);\r
+ var caretElement = left ? walker.prev() : walker.next();\r
+ positionCaretOnElement(caretElement, !left);\r
+ } else {\r
+ positionCaretOnElement(nonEditableParent, left);\r
+ }\r
+ }\r
+ } else {\r
+ // Is arrow left/right, backspace or delete\r
+ if (keyCode == VK.LEFT || keyCode == VK.RIGHT || keyCode == VK.BACKSPACE || keyCode == VK.DELETE) {\r
+ caretContainer = getParentCaretContainer(startElement);\r
+ if (caretContainer) {\r
+ // Arrow left or backspace\r
+ if (keyCode == VK.LEFT || keyCode == VK.BACKSPACE) {\r
+ nonEditableParent = getNonEmptyTextNodeSibling(caretContainer, true);\r
+\r
+ if (nonEditableParent && getContentEditable(nonEditableParent) === "false") {\r
+ e.preventDefault();\r
+\r
+ if (keyCode == VK.LEFT) {\r
+ positionCaretOnElement(nonEditableParent, true);\r
+ } else {\r
+ dom.remove(nonEditableParent);\r
+ }\r
+ } else {\r
+ removeCaretContainer(caretContainer);\r
+ }\r
+ }\r
+\r
+ // Arrow right or delete\r
+ if (keyCode == VK.RIGHT || keyCode == VK.DELETE) {\r
+ nonEditableParent = getNonEmptyTextNodeSibling(caretContainer);\r
+\r
+ if (nonEditableParent && getContentEditable(nonEditableParent) === "false") {\r
+ e.preventDefault();\r
+\r
+ if (keyCode == VK.RIGHT) {\r
+ positionCaretOnElement(nonEditableParent, false);\r
+ } else {\r
+ dom.remove(nonEditableParent);\r
+ }\r
+ } else {\r
+ removeCaretContainer(caretContainer);\r
+ }\r
+ }\r
+ }\r
+ }\r
+ }\r
+ };\r
+\r
+ ed.onMouseDown.addToTop(function(ed, e){\r
+ // prevent collapsing selection to caret when clicking in a non-editable section\r
+ var node = ed.selection.getNode();\r
+ if (getContentEditable(node) === "false" && node == e.target) {\r
+ e.preventDefault();\r
+ }\r
+ });\r
+ ed.onMouseUp.addToTop(moveSelection);\r
+ ed.onKeyDown.addToTop(handleKey);\r
+ ed.onKeyUp.addToTop(moveSelection);\r
+ };\r
\r
tinymce.create('tinymce.plugins.NonEditablePlugin', {\r
init : function(ed, url) {\r
- var t = this, editClass, nonEditClass;\r
+ var editClass, nonEditClass, nonEditableRegExps;\r
+\r
+ editClass = " " + tinymce.trim(ed.getParam("noneditable_editable_class", "mceEditable")) + " ";\r
+ nonEditClass = " " + tinymce.trim(ed.getParam("noneditable_noneditable_class", "mceNonEditable")) + " ";\r
+\r
+ // Setup noneditable regexps array\r
+ nonEditableRegExps = ed.getParam("noneditable_regexp");\r
+ if (nonEditableRegExps && !nonEditableRegExps.length) {\r
+ nonEditableRegExps = [nonEditableRegExps];\r
+ }\r
+\r
+ ed.onPreInit.add(function() {\r
+ handleContentEditableSelection(ed);\r
+\r
+ if (nonEditableRegExps) {\r
+ ed.onBeforeSetContent.add(function(ed, args) {\r
+ var i = nonEditableRegExps.length, content = args.content, cls = tinymce.trim(nonEditClass);\r
+\r
+ // Don't replace the variables when raw is used for example on undo/redo\r
+ if (args.format == "raw") {\r
+ return;\r
+ }\r
\r
- t.editor = ed;\r
- editClass = ed.getParam("noneditable_editable_class", "mceEditable");\r
- nonEditClass = ed.getParam("noneditable_noneditable_class", "mceNonEditable");\r
+ while (i--) {\r
+ content = content.replace(nonEditableRegExps[i], function() {\r
+ var args = arguments;\r
\r
- ed.onNodeChange.addToTop(function(ed, cm, n) {\r
- var sc, ec;\r
+ return '<span class="' + cls + '" data-mce-content="' + ed.dom.encode(args[0]) + '">' + ed.dom.encode(typeof(args[1]) === "string" ? args[1] : args[0]) + '</span>';\r
+ });\r
+ }\r
\r
- // Block if start or end is inside a non editable element\r
- sc = ed.dom.getParent(ed.selection.getStart(), function(n) {\r
- return ed.dom.hasClass(n, nonEditClass);\r
+ args.content = content;\r
+ });\r
+ }\r
+ \r
+ // Apply contentEditable true/false on elements with the noneditable/editable classes\r
+ ed.parser.addAttributeFilter('class', function(nodes) {\r
+ var i = nodes.length, className, node;\r
+\r
+ while (i--) {\r
+ node = nodes[i];\r
+ className = " " + node.attr("class") + " ";\r
+\r
+ if (className.indexOf(editClass) !== -1) {\r
+ node.attr(internalName, "true");\r
+ } else if (className.indexOf(nonEditClass) !== -1) {\r
+ node.attr(internalName, "false");\r
+ }\r
+ }\r
});\r
\r
- ec = ed.dom.getParent(ed.selection.getEnd(), function(n) {\r
- return ed.dom.hasClass(n, nonEditClass);\r
+ // Remove internal name\r
+ ed.serializer.addAttributeFilter(internalName, function(nodes, name) {\r
+ var i = nodes.length, node;\r
+\r
+ while (i--) {\r
+ node = nodes[i];\r
+\r
+ if (nonEditableRegExps && node.attr('data-mce-content')) {\r
+ node.name = "#text";\r
+ node.type = 3;\r
+ node.raw = true;\r
+ node.value = node.attr('data-mce-content');\r
+ } else {\r
+ node.attr(externalName, null);\r
+ node.attr(internalName, null);\r
+ }\r
+ }\r
});\r
\r
- // Block or unblock\r
- if (sc || ec) {\r
- t._setDisabled(1);\r
- return false;\r
- } else\r
- t._setDisabled(0);\r
+ // Convert external name into internal name\r
+ ed.parser.addAttributeFilter(externalName, function(nodes, name) {\r
+ var i = nodes.length, node;\r
+\r
+ while (i--) {\r
+ node = nodes[i];\r
+ node.attr(internalName, node.attr(externalName));\r
+ node.attr(externalName, null);\r
+ }\r
+ });\r
});\r
},\r
\r
infourl : 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/noneditable',\r
version : tinymce.majorVersion + "." + tinymce.minorVersion\r
};\r
- },\r
-\r
- _block : function(ed, e) {\r
- var k = e.keyCode;\r
-\r
- // Don't block arrow keys, pg up/down, and F1-F12\r
- if ((k > 32 && k < 41) || (k > 111 && k < 124))\r
- return;\r
-\r
- return Event.cancel(e);\r
- },\r
-\r
- _setDisabled : function(s) {\r
- var t = this, ed = t.editor;\r
-\r
- tinymce.each(ed.controlManager.controls, function(c) {\r
- c.setDisabled(s);\r
- });\r
-\r
- if (s !== t.disabled) {\r
- if (s) {\r
- ed.onKeyDown.addToTop(t._block);\r
- ed.onKeyPress.addToTop(t._block);\r
- ed.onKeyUp.addToTop(t._block);\r
- ed.onPaste.addToTop(t._block);\r
- } else {\r
- ed.onKeyDown.remove(t._block);\r
- ed.onKeyPress.remove(t._block);\r
- ed.onKeyUp.remove(t._block);\r
- ed.onPaste.remove(t._block);\r
- }\r
-\r
- t.disabled = s;\r
- }\r
}\r
});\r
\r