From d016dcc6f6a3daf8c19e2ececd3c676cd274381a Mon Sep 17 00:00:00 2001 From: Thomas <tb@woodcrest.local> Date: Wed, 09 Oct 2013 06:02:52 -0400 Subject: [PATCH] Refactor multi-threaded autocomple contact searching to make it available for other purposes, too --- program/js/tiny_mce/tiny_mce_src.js | 1375 ++++++++++++++++++++++++++++++++++++++++++++++------------- 1 files changed, 1,058 insertions(+), 317 deletions(-) diff --git a/program/js/tiny_mce/tiny_mce_src.js b/program/js/tiny_mce/tiny_mce_src.js index 2cf0f62..e38fb7e 100644 --- a/program/js/tiny_mce/tiny_mce_src.js +++ b/program/js/tiny_mce/tiny_mce_src.js @@ -6,9 +6,9 @@ var tinymce = { majorVersion : '3', - minorVersion : '5', + minorVersion : '5.6', - releaseDate : '2012-05-03', + releaseDate : '2012-07-26', _init : function() { var t = this, d = document, na = navigator, ua = na.userAgent, i, nl, n, base, p, v; @@ -880,12 +880,12 @@ ((s) ? "; secure" : ""); }, - remove : function(n, p) { - var d = new Date(); + remove : function(name, path, domain) { + var date = new Date(); - d.setTime(d.getTime() - 1000); + date.setTime(date.getTime() - 1000); - this.set(n, '', d, p, d); + this.set(name, '', date, path, domain); } }); })(); @@ -1083,6 +1083,11 @@ modifierPressed: function (e) { return e.shiftKey || e.ctrlKey || e.altKey; + }, + + metaKeyPressed: function(e) { + // Check if ctrl or meta key is pressed also check if alt is false for Polish users + return tinymce.isMac ? e.metaKey : e.ctrlKey && !e.altKey; } }; })(tinymce); @@ -1097,6 +1102,12 @@ // Ignore } } + + function getDocumentMode() { + var documentMode = editor.getDoc().documentMode; + + return documentMode ? documentMode : 6; + }; function cleanupStylesWhenDeleting() { function removeMergedFormatSpans(isDelete) { @@ -1157,74 +1168,58 @@ }; function emptyEditorWhenDeleting() { - function getEndPointNode(rng, start) { - var container, offset, prefix = start ? 'start' : 'end'; + function serializeRng(rng) { + var body = dom.create("body"); + var contents = rng.cloneContents(); + body.appendChild(contents); + return selection.serializer.serialize(body, {format: 'html'}); + } - container = rng[prefix + 'Container']; - offset = rng[prefix + 'Offset']; + function allContentsSelected(rng) { + var selection = serializeRng(rng); - // Resolve indexed container - if (container.nodeType == 1 && container.hasChildNodes()) { - container = container.childNodes[Math.min(start ? offset : (offset > 0 ? offset - 1 : 0), container.childNodes.length - 1)] - } + var allRng = dom.createRng(); + allRng.selectNode(editor.getBody()); - return container; - }; + var allSelection = serializeRng(allRng); + return selection === allSelection; + } - function isAtStartEndOfBody(rng, start) { - var container, offset, root, childNode, prefix = start ? 'start' : 'end', isAfter; + editor.onKeyDown.add(function(editor, e) { + var keyCode = e.keyCode, isCollapsed; - container = rng[prefix + 'Container']; - offset = rng[prefix + 'Offset']; - root = dom.getRoot(); - - // Resolve indexed container - if (container.nodeType == 1) { - isAfter = offset >= container.childNodes.length; - container = getEndPointNode(rng, start); - - if (container.nodeType == 3) { - offset = start && !isAfter ? 0 : container.nodeValue.length; - } - } - - // Check if start/end is in the middle of text - if (container.nodeType == 3 && ((start && offset > 0) || (!start && offset < container.nodeValue.length))) { - return false; - } - - // Walk up the DOM tree to see if the endpoint is at the beginning/end of body - while (container !== root) { - childNode = container.parentNode[start ? 'firstChild' : 'lastChild']; - - // If first/last element is a BR then jump to it's sibling in case: <p>x<br></p> - if (childNode.nodeName == "BR") { - childNode = childNode[start ? 'nextSibling' : 'previousSibling'] || childNode; - } - - // If the childNode isn't the container node then break in case <p><span>A</span>[X]</p> - if (childNode !== container) { - return false; - } - - container = container.parentNode; - } - - return true; - }; - - editor.onKeyDown.addToTop(function(editor, e) { - var rng, keyCode = e.keyCode; - + // Empty the editor if it's needed for example backspace at <p><b>|</b></p> if (!e.isDefaultPrevented() && (keyCode == DELETE || keyCode == BACKSPACE)) { - rng = selection.getRng(true); + isCollapsed = editor.selection.isCollapsed(); - if (isAtStartEndOfBody(rng, true) && isAtStartEndOfBody(rng, false) && - (rng.collapsed || dom.findCommonAncestor(getEndPointNode(rng, true), getEndPointNode(rng)) === dom.getRoot())) { - editor.setContent(''); - editor.nodeChanged(); - e.preventDefault(); + // Selection is collapsed but the editor isn't empty + if (isCollapsed && !dom.isEmpty(editor.getBody())) { + return; } + + // IE deletes all contents correctly when everything is selected + if (tinymce.isIE && !isCollapsed) { + return; + } + + // Selection isn't collapsed but not all the contents is selected + if (!isCollapsed && !allContentsSelected(editor.selection.getRng())) { + return; + } + + // Manually empty the editor + editor.setContent(''); + editor.selection.setCursorLocation(editor.getBody(), 0); + editor.nodeChanged(); + } + }); + }; + + function selectAll() { + editor.onKeyDown.add(function(editor, e) { + if (e.keyCode == 65 && VK.metaKeyPressed(e)) { + e.preventDefault(); + editor.execCommand('SelectAll'); } }); }; @@ -1319,7 +1314,7 @@ } function isSelectionAcrossElements() { - return !selection.isCollapsed() && selection.getStart() != selection.getEnd(); + return !selection.isCollapsed() && dom.getParent(selection.getStart(), dom.isBlock) != dom.getParent(selection.getEnd(), dom.isBlock); } function blockEvent(editor, e) { @@ -1393,16 +1388,15 @@ } function addNewLinesBeforeBrInPre() { - var documentMode = editor.getDoc().documentMode; - // IE8+ rendering mode does the right thing with BR in PRE - if (documentMode && documentMode > 7) { + if (getDocumentMode() > 7) { return; } // Enable display: none in area and add a specific class that hides all BR elements in PRE to // avoid the caret from getting stuck at the BR elements while pressing the right arrow key setEditorCommandState('RespectVisibilityInDesign', true); + editor.contentStyles.push('.mceHideBrInPre pre br {display: none}'); dom.addClass(editor.getBody(), 'mceHideBrInPre'); // Adds a \n before all BR elements in PRE to get them visual @@ -1588,6 +1582,14 @@ editor.onSetContent.add(selection.onSetContent.add(fixLinks)); }; + function setDefaultBlockType() { + if (settings.forced_root_block) { + editor.onInit.add(function() { + setEditorCommandState('DefaultParagraphSeparator', settings.forced_root_block); + }); + } + } + function removeGhostSelection() { function repaint(sender, args) { if (!sender || !args.initial) { @@ -1600,16 +1602,268 @@ editor.onSetContent.add(repaint); }; - function deleteImageOnBackSpace() { + function deleteControlItemOnBackSpace() { editor.onKeyDown.add(function(editor, e) { - if (!e.isDefaultPrevented() && e.keyCode == 8 && selection.getNode().nodeName == 'IMG') { - e.preventDefault(); - editor.undoManager.beforeChange(); - dom.remove(selection.getNode()); - editor.undoManager.add(); + var rng; + + if (!e.isDefaultPrevented() && e.keyCode == BACKSPACE) { + rng = editor.getDoc().selection.createRange(); + if (rng && rng.item) { + e.preventDefault(); + editor.undoManager.beforeChange(); + dom.remove(rng.item(0)); + editor.undoManager.add(); + } } }); }; + + function renderEmptyBlocksFix() { + var emptyBlocksCSS; + + // IE10+ + if (getDocumentMode() >= 10) { + emptyBlocksCSS = ''; + tinymce.each('p div h1 h2 h3 h4 h5 h6'.split(' '), function(name, i) { + emptyBlocksCSS += (i > 0 ? ',' : '') + name + ':empty'; + }); + + editor.contentStyles.push(emptyBlocksCSS + '{padding-right: 1px !important}'); + } + }; + + function fakeImageResize() { + var selectedElmX, selectedElmY, selectedElm, selectedElmGhost, selectedHandle, startX, startY, startW, startH, ratio, + resizeHandles, width, height, rootDocument = document, editableDoc = editor.getDoc(); + + if (!settings.object_resizing || settings.webkit_fake_resize === false) { + return; + } + + // Try disabling object resizing if WebKit implements resizing in the future + setEditorCommandState("enableObjectResizing", false); + + // Details about each resize handle how to scale etc + resizeHandles = { + // Name: x multiplier, y multiplier, delta size x, delta size y + n: [.5, 0, 0, -1], + e: [1, .5, 1, 0], + s: [.5, 1, 0, 1], + w: [0, .5, -1, 0], + nw: [0, 0, -1, -1], + ne: [1, 0, 1, -1], + se: [1, 1, 1, 1], + sw : [0, 1, -1, 1] + }; + + function resizeElement(e) { + var deltaX, deltaY; + + // Calc new width/height + deltaX = e.screenX - startX; + deltaY = e.screenY - startY; + + // Calc new size + width = deltaX * selectedHandle[2] + startW; + height = deltaY * selectedHandle[3] + startH; + + // Never scale down lower than 5 pixels + width = width < 5 ? 5 : width; + height = height < 5 ? 5 : height; + + // Constrain proportions when modifier key is pressed or if the nw, ne, sw, se corners are moved on an image + if (VK.modifierPressed(e) || (selectedElm.nodeName == "IMG" && selectedHandle[2] * selectedHandle[3] !== 0)) { + width = Math.round(height / ratio); + height = Math.round(width * ratio); + } + + // Update ghost size + dom.setStyles(selectedElmGhost, { + width: width, + height: height + }); + + // Update ghost X position if needed + if (selectedHandle[2] < 0 && selectedElmGhost.clientWidth <= width) { + dom.setStyle(selectedElmGhost, 'left', selectedElmX + (startW - width)); + } + + // Update ghost Y position if needed + if (selectedHandle[3] < 0 && selectedElmGhost.clientHeight <= height) { + dom.setStyle(selectedElmGhost, 'top', selectedElmY + (startH - height)); + } + } + + function endResize() { + function setSizeProp(name, value) { + if (value) { + // Resize by using style or attribute + if (selectedElm.style[name] || !editor.schema.isValid(selectedElm.nodeName.toLowerCase(), name)) { + dom.setStyle(selectedElm, name, value); + } else { + dom.setAttrib(selectedElm, name, value); + } + } + } + + // Set width/height properties + setSizeProp('width', width); + setSizeProp('height', height); + + dom.unbind(editableDoc, 'mousemove', resizeElement); + dom.unbind(editableDoc, 'mouseup', endResize); + + if (rootDocument != editableDoc) { + dom.unbind(rootDocument, 'mousemove', resizeElement); + dom.unbind(rootDocument, 'mouseup', endResize); + } + + // Remove ghost and update resize handle positions + dom.remove(selectedElmGhost); + showResizeRect(selectedElm); + } + + function showResizeRect(targetElm) { + var position, targetWidth, targetHeight; + + hideResizeRect(); + + // Get position and size of target + position = dom.getPos(targetElm); + selectedElmX = position.x; + selectedElmY = position.y; + targetWidth = targetElm.offsetWidth; + targetHeight = targetElm.offsetHeight; + + // Reset width/height if user selects a new image/table + if (selectedElm != targetElm) { + selectedElm = targetElm; + width = height = 0; + } + + tinymce.each(resizeHandles, function(handle, name) { + var handleElm; + + // Get existing or render resize handle + handleElm = dom.get('mceResizeHandle' + name); + if (!handleElm) { + handleElm = dom.add(editableDoc.documentElement, 'div', { + id: 'mceResizeHandle' + name, + 'class': 'mceResizeHandle', + style: 'cursor:' + name + '-resize; margin:0; padding:0' + }); + + dom.bind(handleElm, 'mousedown', function(e) { + e.preventDefault(); + + endResize(); + + startX = e.screenX; + startY = e.screenY; + startW = selectedElm.clientWidth; + startH = selectedElm.clientHeight; + ratio = startH / startW; + selectedHandle = handle; + + selectedElmGhost = selectedElm.cloneNode(true); + dom.addClass(selectedElmGhost, 'mceClonedResizable'); + dom.setStyles(selectedElmGhost, { + left: selectedElmX, + top: selectedElmY, + margin: 0 + }); + + editableDoc.documentElement.appendChild(selectedElmGhost); + + dom.bind(editableDoc, 'mousemove', resizeElement); + dom.bind(editableDoc, 'mouseup', endResize); + + if (rootDocument != editableDoc) { + dom.bind(rootDocument, 'mousemove', resizeElement); + dom.bind(rootDocument, 'mouseup', endResize); + } + }); + } else { + dom.show(handleElm); + } + + // Position element + dom.setStyles(handleElm, { + left: (targetWidth * handle[0] + selectedElmX) - (handleElm.offsetWidth / 2), + top: (targetHeight * handle[1] + selectedElmY) - (handleElm.offsetHeight / 2) + }); + }); + + // Only add resize rectangle on WebKit and only on images + if (!tinymce.isOpera && selectedElm.nodeName == "IMG") { + selectedElm.setAttribute('data-mce-selected', '1'); + } + } + + function hideResizeRect() { + if (selectedElm) { + selectedElm.removeAttribute('data-mce-selected'); + } + + for (var name in resizeHandles) { + dom.hide('mceResizeHandle' + name); + } + } + + // Add CSS for resize handles, cloned element and selected + editor.contentStyles.push( + '.mceResizeHandle {' + + 'position: absolute;' + + 'border: 1px solid black;' + + 'background: #FFF;' + + 'width: 5px;' + + 'height: 5px;' + + 'z-index: 10000' + + '}' + + '.mceResizeHandle:hover {' + + 'background: #000' + + '}' + + 'img[data-mce-selected] {' + + 'outline: 1px solid black' + + '}' + + 'img.mceClonedResizable, table.mceClonedResizable {' + + 'position: absolute;' + + 'outline: 1px dashed black;' + + 'opacity: .5;' + + 'z-index: 10000' + + '}' + ); + + function updateResizeRect() { + var controlElm = dom.getParent(selection.getNode(), 'table,img'); + + // Remove data-mce-selected from all elements since they might have been copied using Ctrl+c/v + tinymce.each(dom.select('img[data-mce-selected]'), function(img) { + img.removeAttribute('data-mce-selected'); + }); + + if (controlElm) { + showResizeRect(controlElm); + } else { + hideResizeRect(); + } + } + + // Show/hide resize rect when image is selected + editor.onNodeChange.add(updateResizeRect); + + // Fixes WebKit quirk where it returns IMG on getNode if caret is after last image in container + dom.bind(editableDoc, 'selectionchange', updateResizeRect); + + // Remove the internal attribute when serializing the DOM + editor.serializer.addAttributeFilter('data-mce-selected', function(nodes, name) { + var i = nodes.length; + + while (i--) { + nodes[i].attr(name, null); + } + }); + } // All browsers disableBackspaceIntoATable(); @@ -1622,10 +1876,14 @@ cleanupStylesWhenDeleting(); inputMethodFocus(); selectControlElements(); + setDefaultBlockType(); // iOS if (tinymce.isIDevice) { selectionChangeNodeChanged(); + } else { + fakeImageResize(); + selectAll(); } } @@ -1635,7 +1893,8 @@ ensureBodyHasRoleApplication(); addNewLinesBeforeBrInPre(); removePreSerializedStylesWhenSelectingControls(); - deleteImageOnBackSpace(); + deleteControlItemOnBackSpace(); + renderEmptyBlocksFix(); } // Gecko @@ -1646,6 +1905,11 @@ setGeckoEditingOptions(); addBrAfterLastLinks(); removeGhostSelection(); + } + + // Opera + if (tinymce.isOpera) { + fakeImageResize(); } }; (function(tinymce) { @@ -2098,9 +2362,12 @@ if (!html5) { html5 = mapCache.html5 = unpack({ - A : 'id|accesskey|class|dir|draggable|item|hidden|itemprop|role|spellcheck|style|subject|title', - B : '#|a|abbr|area|audio|b|bdo|br|button|canvas|cite|code|command|datalist|del|dfn|em|embed|i|iframe|img|input|ins|kbd|keygen|label|link|map|mark|meta|meter|noscript|object|output|progress|q|ruby|samp|script|select|small|span|strong|sub|sup|svg|textarea|time|var|video', - C : '#|a|abbr|area|address|article|aside|audio|b|bdo|blockquote|br|button|canvas|cite|code|command|datalist|del|details|dfn|dialog|div|dl|em|embed|fieldset|figure|footer|form|h1|h2|h3|h4|h5|h6|header|hgroup|hr|i|iframe|img|input|ins|kbd|keygen|label|link|map|mark|menu|meta|meter|nav|noscript|ol|object|output|p|pre|progress|q|ruby|samp|script|section|select|small|span|strong|style|sub|sup|svg|table|textarea|time|ul|var|video' + A : 'id|accesskey|class|dir|draggable|item|hidden|itemprop|role|spellcheck|style|subject|title|onclick|ondblclick|onmousedown|onmouseup|onmouseover|onmousemove|onmouseout|onkeypress|onkeydown|onkeyup', + B : '#|a|abbr|area|audio|b|bdo|br|button|canvas|cite|code|command|datalist|del|dfn|em|embed|i|iframe|img|input|ins|kbd|keygen|label|link|map|mark|meta|' + + 'meter|noscript|object|output|progress|q|ruby|samp|script|select|small|span|strong|sub|sup|svg|textarea|time|var|video|wbr', + C : '#|a|abbr|area|address|article|aside|audio|b|bdo|blockquote|br|button|canvas|cite|code|command|datalist|del|details|dfn|dialog|div|dl|em|embed|fieldset|' + + 'figure|footer|form|h1|h2|h3|h4|h5|h6|header|hgroup|hr|i|iframe|img|input|ins|kbd|keygen|label|link|map|mark|menu|meta|meter|nav|noscript|ol|object|output|' + + 'p|pre|progress|q|ruby|samp|script|section|select|small|span|strong|style|sub|sup|svg|table|textarea|time|ul|var|video' }, 'html[A|manifest][body|head]' + 'head[A][base|command|link|meta|noscript|script|style|title]' + 'title[A][#]' + @@ -2136,7 +2403,7 @@ 'dl[A][dd|dt]' + 'dt[A][B]' + 'dd[A][C]' + - 'a[A|href|target|ping|rel|media|type][C]' + + 'a[A|href|target|ping|rel|media|type][B]' + 'em[A][B]' + 'strong[A][B]' + 'small[A][B]' + @@ -2182,7 +2449,8 @@ 'form[A|accept-charset|action|autocomplete|enctype|method|name|novalidate|target][C]' + 'fieldset[A|disabled|form|name][C|legend]' + 'label[A|form|for][B]' + - 'input[A|type|accept|alt|autocomplete|checked|disabled|form|formaction|formenctype|formmethod|formnovalidate|formtarget|height|list|max|maxlength|min|multiple|pattern|placeholder|readonly|required|size|src|step|width|files|value][]' + + 'input[A|type|accept|alt|autocomplete|checked|disabled|form|formaction|formenctype|formmethod|formnovalidate|formtarget|height|list|max|maxlength|min|' + + 'multiple|pattern|placeholder|readonly|required|size|src|step|width|files|value|name][]' + 'button[A|autofocus|disabled|form|formaction|formenctype|formmethod|formnovalidate|formtarget|name|value|type][B]' + 'select[A|autofocus|disabled|form|multiple|name|size][option|optgroup]' + 'datalist[A][B|option]' + @@ -2196,7 +2464,7 @@ 'area[A|shape|coords|href|alt|target|media|rel|ping|type][]' + 'mathml[A][]' + 'svg[A][]' + - 'table[A|summary][caption|colgroup|thead|tfoot|tbody|tr]' + + 'table[A|border][caption|colgroup|thead|tfoot|tbody|tr]' + 'caption[A][C]' + 'colgroup[A|span][col]' + 'col[A|span][]' + @@ -2205,7 +2473,8 @@ 'tbody[A][tr]' + 'tr[A][th|td]' + 'th[A|headers|rowspan|colspan|scope][B]' + - 'td[A|headers|rowspan|colspan][C]' + 'td[A|headers|rowspan|colspan][C]' + + 'wbr[A][]' ); } @@ -2385,13 +2654,13 @@ // Setup map objects whiteSpaceElementsMap = createLookupTable('whitespace_elements', 'pre script style textarea'); - selfClosingElementsMap = createLookupTable('self_closing_elements', 'colgroup dd dt li options p td tfoot th thead tr'); - shortEndedElementsMap = createLookupTable('short_ended_elements', 'area base basefont br col frame hr img input isindex link meta param embed source'); + selfClosingElementsMap = createLookupTable('self_closing_elements', 'colgroup dd dt li option p td tfoot th thead tr'); + shortEndedElementsMap = createLookupTable('short_ended_elements', 'area base basefont br col frame hr img input isindex link meta param embed source wbr'); boolAttrMap = createLookupTable('boolean_attributes', 'checked compact declare defer disabled ismap multiple nohref noresize noshade nowrap readonly selected autoplay loop controls'); nonEmptyElementsMap = createLookupTable('non_empty_elements', 'td th iframe video audio object', shortEndedElementsMap); blockElementsMap = createLookupTable('block_elements', 'h1 h2 h3 h4 h5 h6 hr p div address pre form table tbody thead tfoot ' + 'th tr td li ol ul caption blockquote center dl dt dd dir fieldset ' + - 'noscript menu isindex samp header footer article section hgroup aside nav figure'); + 'noscript menu isindex samp header footer article section hgroup aside nav figure option datalist select optgroup'); // Converts a wildcard expression string to a regexp for example *a will become /.*a/. function patternToRegExp(str) { @@ -2713,6 +2982,36 @@ return !!(parent && parent[child]); }; + self.isValid = function(name, attr) { + var attrPatterns, i, rule = getElementRule(name); + + // Check if it's a valid element + if (rule) { + if (attr) { + // Check if attribute name exists + if (rule.attributes[attr]) { + return true; + } + + // Check if attribute matches a regexp pattern + attrPatterns = rule.attributePatterns; + if (attrPatterns) { + i = attrPatterns.length; + while (i--) { + if (attrPatterns[i].pattern.test(name)) { + return true; + } + } + } + } else { + return true; + } + } + + // No match + return false; + }; + self.getElementRule = getElementRule; self.getCustomElements = function() { @@ -2836,7 +3135,7 @@ // Setup lookup tables for empty elements and boolean attributes shortEndedElements = schema.getShortEndedElements(); - selfClosing = schema.getSelfClosingElements(); + selfClosing = settings.self_closing_elements || schema.getSelfClosingElements(); fillAttrsMap = schema.getBoolAttrs(); validate = settings.validate; removeInternalElements = settings.remove_internals; @@ -3584,9 +3883,23 @@ } }; + function cloneAndExcludeBlocks(input) { + var name, output = {}; + + for (name in input) { + if (name !== 'li' && name != 'p') { + output[name] = input[name]; + } + } + + return output; + }; + parser = new tinymce.html.SaxParser({ validate : validate, - fix_self_closing : !validate, // Let the DOM parser handle <li> in <li> or <p> in <p> for better results + + // Exclude P and LI from DOM parsing since it's treated better by the DOM parser + self_closing_elements: cloneAndExcludeBlocks(schema.getSelfClosingElements()), cdata: function(text) { node.append(createNode('#cdata', 4)).value = text; @@ -3764,7 +4077,7 @@ node.empty().append(new Node('#text', '3')).value = '\u00a0'; else { // Leave nodes that have a name like <a name="name"> - if (!node.attributes.map.name) { + if (!node.attributes.map.name && !node.attributes.map.id) { tempNode = node.parent; node.empty().remove(); node = tempNode; @@ -3916,12 +4229,12 @@ // Force anchor names closed, unless the setting "allow_html_in_named_anchor" is explicitly included. if (!settings.allow_html_in_named_anchor) { - self.addAttributeFilter('name', function(nodes, name) { + self.addAttributeFilter('id,name', function(nodes, name) { var i = nodes.length, sibling, prevSibling, parent, node; while (i--) { node = nodes[i]; - if (node.name === 'a' && node.firstChild) { + if (node.name === 'a' && node.firstChild && !node.attr('href')) { parent = node.parent; // Move children after current node @@ -4600,7 +4913,7 @@ // Old API supported multiple targets if (target && target instanceof Array) { - var i = target; + var i = target.length; while (i--) { self.add(target[i], events, func, scope); @@ -4660,12 +4973,20 @@ }; self.prevent = function(e) { + if (!e.preventDefault) { + e = fix(e); + } + e.preventDefault(); return false; }; self.stop = function(e) { + if (!e.stopPropagation) { + e = fix(e); + } + e.stopPropagation(); return false; @@ -5434,6 +5755,32 @@ return this.styles.serialize(o, name); }, + addStyle: function(cssText) { + var doc = this.doc, head; + + // Create style element if needed + styleElm = doc.getElementById('mceDefaultStyles'); + if (!styleElm) { + styleElm = doc.createElement('style'), + styleElm.id = 'mceDefaultStyles'; + styleElm.type = 'text/css'; + + head = doc.getElementsByTagName('head')[0]; + if (head.firstChild) { + head.insertBefore(styleElm, head.firstChild); + } else { + head.appendChild(styleElm); + } + } + + // Append style data to old or new style element + if (styleElm.styleSheet) { + styleElm.styleSheet.cssText += cssText; + } else { + styleElm.appendChild(doc.createTextNode(cssText)); + } + }, + loadCSS : function(u) { var t = this, d = t.doc, head; @@ -5557,13 +5904,13 @@ // This seems to fix this problem // Create new div with HTML contents and a BR infront to keep comments - element = self.create('div'); - element.innerHTML = '<br />' + html; + var newElement = self.create('div'); + newElement.innerHTML = '<br />' + html; // Add all children from div to target - each (element.childNodes, function(node, i) { + each (tinymce.grep(newElement.childNodes), function(node, i) { // Skip br element - if (i) + if (i && element.canHaveHTML) element.appendChild(node); }); } @@ -6158,7 +6505,8 @@ cloneContents : cloneContents, insertNode : insertNode, surroundContents : surroundContents, - cloneRange : cloneRange + cloneRange : cloneRange, + toStringIE : toStringIE }); function createDocumentFragment() { @@ -6798,9 +7146,20 @@ n.parentNode.removeChild(n); }; + + function toStringIE() { + return dom.create('body', null, cloneContents()).outerText; + } + + return t; }; ns.Range = Range; + + // Older IE versions doesn't let you override toString by it's constructor so we have to stick it in the prototype + Range.prototype.toString = function() { + return this.toStringIE(); + }; })(tinymce.dom); (function() { @@ -7156,7 +7515,8 @@ }; this.addRange = function(rng) { - var ieRng, ctrlRng, startContainer, startOffset, endContainer, endOffset, doc = selection.dom.doc, body = doc.body; + var ieRng, ctrlRng, startContainer, startOffset, endContainer, endOffset, sibling, + doc = selection.dom.doc, body = doc.body, nativeRng, ctrlElm; function setEndPoint(start) { var container, offset, marker, tmpRng, nodes; @@ -7214,11 +7574,25 @@ // Trick to place the caret inside an empty block element like <p></p> if (startOffset == endOffset && !startContainer.hasChildNodes()) { if (startContainer.canHaveHTML) { + // Check if previous sibling is an empty block if it is then we need to render it + // IE would otherwise move the caret into the sibling instead of the empty startContainer see: #5236 + // Example this: <p></p><p>|</p> would become this: <p>|</p><p></p> + sibling = startContainer.previousSibling; + if (sibling && !sibling.hasChildNodes() && dom.isBlock(sibling)) { + sibling.innerHTML = '\uFEFF'; + } else { + sibling = null; + } + startContainer.innerHTML = '<span>\uFEFF</span><span>\uFEFF</span>'; ieRng.moveToElementText(startContainer.lastChild); ieRng.select(); dom.doc.selection.clear(); startContainer.innerHTML = ''; + + if (sibling) { + sibling.innerHTML = ''; + } return; } else { startOffset = dom.nodeIndex(startContainer); @@ -7228,10 +7602,17 @@ if (startOffset == endOffset - 1) { try { + ctrlElm = startContainer.childNodes[startOffset]; ctrlRng = body.createControlRange(); - ctrlRng.addElement(startContainer.childNodes[startOffset]); + ctrlRng.addElement(ctrlElm); ctrlRng.select(); - return; + + // Check if the range produced is on the correct element and is a control range + // On IE 8 it will select the parent contentEditable container if you select an inner element see: #5398 + nativeRng = selection.getRng(); + if (nativeRng.item && ctrlElm === nativeRng.item(0)) { + return; + } } catch (ex) { // Ignore } @@ -8816,12 +9197,13 @@ var is = tinymce.is, isIE = tinymce.isIE, each = tinymce.each, TreeWalker = tinymce.dom.TreeWalker; tinymce.create('tinymce.dom.Selection', { - Selection : function(dom, win, serializer) { + Selection : function(dom, win, serializer, editor) { var t = this; t.dom = dom; t.win = win; t.serializer = serializer; + t.editor = editor; // Add events each([ @@ -8977,7 +9359,7 @@ }, getStart : function() { - var rng = this.getRng(), startElement, parentElement, checkRng, node; + var self = this, rng = self.getRng(), startElement, parentElement, checkRng, node; if (rng.duplicate || rng.item) { // Control selection, return first item @@ -8988,6 +9370,9 @@ checkRng = rng.duplicate(); checkRng.collapse(1); startElement = checkRng.parentElement(); + if (startElement.ownerDocument !== self.dom.doc) { + startElement = self.dom.getRoot(); + } // Check if range parent is inside the start element, then return the inner parent element // This will fix issues when a single element is selected, IE would otherwise return the wrong start element @@ -9014,31 +9399,34 @@ }, getEnd : function() { - var t = this, r = t.getRng(), e, eo; + var self = this, rng = self.getRng(), endElement, endOffset; - if (r.duplicate || r.item) { - if (r.item) - return r.item(0); + if (rng.duplicate || rng.item) { + if (rng.item) + return rng.item(0); - r = r.duplicate(); - r.collapse(0); - e = r.parentElement(); + rng = rng.duplicate(); + rng.collapse(0); + endElement = rng.parentElement(); + if (endElement.ownerDocument !== self.dom.doc) { + endElement = self.dom.getRoot(); + } - if (e && e.nodeName == 'BODY') - return e.lastChild || e; + if (endElement && endElement.nodeName == 'BODY') + return endElement.lastChild || endElement; - return e; + return endElement; } else { - e = r.endContainer; - eo = r.endOffset; + endElement = rng.endContainer; + endOffset = rng.endOffset; - if (e.nodeType == 1 && e.hasChildNodes()) - e = e.childNodes[eo > 0 ? eo - 1 : eo]; + if (endElement.nodeType == 1 && endElement.hasChildNodes()) + endElement = endElement.childNodes[endOffset > 0 ? endOffset - 1 : endOffset]; - if (e && e.nodeType == 3) - return e.parentNode; + if (endElement && endElement.nodeType == 3) + return endElement.parentNode; - return e; + return endElement; } }, @@ -9784,14 +10172,66 @@ } }, - destroy : function(s) { - var t = this; + selectorChanged: function(selector, callback) { + var self = this, currentSelectors; - t.win = null; + if (!self.selectorChangedData) { + self.selectorChangedData = {}; + currentSelectors = {}; + + self.editor.onNodeChange.addToTop(function(ed, cm, node) { + var dom = self.dom, parents = dom.getParents(node, null, dom.getRoot()), matchedSelectors = {}; + + // Check for new matching selectors + each(self.selectorChangedData, function(callbacks, selector) { + each(parents, function(node) { + if (dom.is(node, selector)) { + if (!currentSelectors[selector]) { + // Execute callbacks + each(callbacks, function(callback) { + callback(true, {node: node, selector: selector, parents: parents}); + }); + + currentSelectors[selector] = callbacks; + } + + matchedSelectors[selector] = callbacks; + return false; + } + }); + }); + + // Check if current selectors still match + each(currentSelectors, function(callbacks, selector) { + if (!matchedSelectors[selector]) { + delete currentSelectors[selector]; + + each(callbacks, function(callback) { + callback(false, {node: node, selector: selector, parents: parents}); + }); + } + }); + }); + } + + // Add selector listeners + if (!self.selectorChangedData[selector]) { + self.selectorChangedData[selector] = []; + } + + self.selectorChangedData[selector].push(callback); + + return self; + }, + + destroy : function(manual) { + var self = this; + + self.win = null; // Manual destroy then remove unload handler - if (!s) - tinymce.removeUnload(t.destroy); + if (!manual) + tinymce.removeUnload(self.destroy); }, // IE has an issue where you can't select/move the caret by clicking outside the body if the document is in standards mode @@ -10113,7 +10553,7 @@ // Replace all BOM characters for now until we can find a better solution if (!args.cleanup) - args.content = args.content.replace(/\uFEFF|\u200B/g, ''); + args.content = args.content.replace(/\uFEFF/g, ''); // Post process if (!args.no_events) @@ -10207,11 +10647,10 @@ } // Create new script element - elm = dom.create('script', { - id : id, - type : 'text/javascript', - src : tinymce._addVer(url) - }); + elm = document.createElement('script'); + elm.id = id; + elm.type = 'text/javascript'; + elm.src = tinymce._addVer(url); // Add onload listener for non IE browsers since IE9 // fires onload event before the script is parsed and executed @@ -10575,12 +11014,15 @@ t.destroy = function() { each(items, function(item) { - dom.unbind(dom.get(item.id), 'focus', itemFocussed); - dom.unbind(dom.get(item.id), 'blur', itemBlurred); + var elm = dom.get(item.id); + + dom.unbind(elm, 'focus', itemFocussed); + dom.unbind(elm, 'blur', itemBlurred); }); - dom.unbind(dom.get(root), 'focus', rootFocussed); - dom.unbind(dom.get(root), 'keydown', rootKeydown); + var rootElm = dom.get(root); + dom.unbind(rootElm, 'focus', rootFocussed); + dom.unbind(rootElm, 'keydown', rootKeydown); items = dom = root = t.focus = itemFocussed = itemBlurred = rootKeydown = rootFocussed = null; t.destroy = function() {}; @@ -10659,21 +11101,23 @@ // Set up state and listeners for each item. each(items, function(item, idx) { - var tabindex; + var tabindex, elm; if (!item.id) { item.id = dom.uniqueId('_mce_item_'); } + elm = dom.get(item.id); + if (excludeFromTabOrder) { - dom.bind(item.id, 'blur', itemBlurred); + dom.bind(elm, 'blur', itemBlurred); tabindex = '-1'; } else { tabindex = (idx === 0 ? '0' : '-1'); } - dom.setAttrib(item.id, 'tabindex', tabindex); - dom.bind(dom.get(item.id), 'focus', itemFocussed); + elm.setAttribute('tabindex', tabindex); + dom.bind(elm, 'focus', itemFocussed); }); // Setup initial state for root element. @@ -10682,10 +11126,11 @@ } dom.setAttrib(root, 'tabindex', '-1'); - + // Setup listeners for root element. - dom.bind(dom.get(root), 'focus', rootFocussed); - dom.bind(dom.get(root), 'keydown', rootKeydown); + var rootElm = dom.get(root); + dom.bind(rootElm, 'focus', rootFocussed); + dom.bind(rootElm, 'keydown', rootKeydown); } }); })(tinymce); @@ -11326,7 +11771,7 @@ l = DOM.encode(s.label || ''); h = '<a role="button" id="' + this.id + '" href="javascript:;" class="' + cp + ' ' + cp + 'Enabled ' + s['class'] + (l ? ' ' + cp + 'Labeled' : '') +'" onmousedown="return false;" onclick="return false;" aria-labelledby="' + this.id + '_voice" title="' + DOM.encode(s.title) + '">'; if (s.image && !(this.editor &&this.editor.forcedHighContrastMode) ) - h += '<img class="mceIcon" src="' + s.image + '" alt="' + DOM.encode(s.title) + '" />' + l; + h += '<span class="mceIcon ' + s['class'] + '"><img class="mceIcon" src="' + s.image + '" alt="' + DOM.encode(s.title) + '" /></span>' + (l ? '<span class="' + cp + 'Label">' + l + '</span>' : ''); else h += '<span class="mceIcon ' + s['class'] + '"></span>' + (l ? '<span class="' + cp + 'Label">' + l + '</span>' : ''); @@ -12034,6 +12479,16 @@ DOM.select('a', t.id + '_menu')[0].focus(); // Select first link } + t.keyboardNav = new tinymce.ui.KeyboardNavigation({ + root: t.id + '_menu', + items: DOM.select('a', t.id + '_menu'), + onCancel: function() { + t.hideMenu(); + t.focus(); + } + }); + + t.keyboardNav.focus(); t.isMenuVisible = 1; }, @@ -12054,6 +12509,7 @@ t.isMenuVisible = 0; t.onHideMenu.dispatch(); + t.keyboardNav.destroy(); } }, @@ -12118,15 +12574,6 @@ } DOM.addClass(m, 'mceColorSplitMenu'); - - new tinymce.ui.KeyboardNavigation({ - root: t.id + '_menu', - items: DOM.select('a', t.id + '_menu'), - onCancel: function() { - t.hideMenu(); - t.focus(); - } - }); // Prevent IE from scrolling and hindering click to occur #4019 Event.add(t.id + '_menu', 'mousedown', function(e) {return Event.cancel(e);}); @@ -12168,11 +12615,17 @@ }, destroy : function() { - this.parent(); + var self = this; - Event.clear(this.id + '_menu'); - Event.clear(this.id + '_more'); - DOM.remove(this.id + '_menu'); + self.parent(); + + Event.clear(self.id + '_menu'); + Event.clear(self.id + '_more'); + DOM.remove(self.id + '_menu'); + + if (self.keyboardNav) { + self.keyboardNav.destroy(); + } } }); })(tinymce); @@ -12483,11 +12936,6 @@ return c.constructor === RegExp ? c.test(n.className) : DOM.hasClass(n, c); }; - s = extend({ - theme : "simple", - language : "en" - }, s); - t.settings = s; // Legacy call @@ -12767,7 +13215,7 @@ self.settings = settings = extend({ id : id, language : 'en', - theme : 'simple', + theme : 'advanced', skin : 'default', delta_width : 0, delta_height : 0, @@ -12798,8 +13246,8 @@ inline_styles : TRUE, convert_fonts_to_spans : TRUE, indent : 'simple', - indent_before : 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,ul,li,area,table,thead,tfoot,tbody,tr,section,article,hgroup,aside,figure', - indent_after : 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,ul,li,area,table,thead,tfoot,tbody,tr,section,article,hgroup,aside,figure', + indent_before : 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,ul,li,area,table,thead,tfoot,tbody,tr,section,article,hgroup,aside,figure,option,optgroup,datalist', + indent_after : 'p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,ul,li,area,table,thead,tfoot,tbody,tr,section,article,hgroup,aside,figure,option,optgroup,datalist', validate : TRUE, entity_encoding : 'named', url_converter : self.convertURL, @@ -12820,6 +13268,8 @@ self.baseURI = tinymce.baseURI; self.contentCSS = []; + + self.contentStyles = []; // Creates all events like onClick, onSetContent etc see Editor.Events.js for the actual logic self.setupEvents(); @@ -12858,6 +13308,12 @@ // Add hidden input for non input elements inside form elements if (!/TEXTAREA|INPUT/i.test(t.getElement().nodeName) && s.hidden_input && DOM.getParent(id, 'form')) DOM.insertAfter(DOM.create('input', {type : 'hidden', name : id}), id); + + // Hide target element early to prevent content flashing + if (!s.content_editable) { + t.orgVisibility = t.getElement().style.visibility; + t.getElement().style.visibility = 'hidden'; + } if (tinymce.WindowManager) t.windowManager = new tinymce.WindowManager(t); @@ -12920,7 +13376,7 @@ if (s.language && s.language_load !== false) sl.add(tinymce.baseURL + '/langs/' + s.language + '.js'); - if (s.theme && s.theme.charAt(0) != '-' && !ThemeManager.urls[s.theme]) + if (s.theme && typeof s.theme != "function" && s.theme.charAt(0) != '-' && !ThemeManager.urls[s.theme]) ThemeManager.load(s.theme, 'themes/' + s.theme + '/editor_template' + tinymce.suffix + '.js'); each(explode(s.plugins), function(p) { @@ -12954,20 +13410,25 @@ }, init : function() { - var n, t = this, s = t.settings, w, h, e = t.getElement(), o, ti, u, bi, bc, re, i, initializedPlugins = []; + var n, t = this, s = t.settings, w, h, mh, e = t.getElement(), o, ti, u, bi, bc, re, i, initializedPlugins = []; tinymce.add(t); s.aria_label = s.aria_label || DOM.getAttrib(e, 'aria-label', t.getLang('aria.rich_text_area')); if (s.theme) { - s.theme = s.theme.replace(/-/, ''); - o = ThemeManager.get(s.theme); - t.theme = new o(); + if (typeof s.theme != "function") { + s.theme = s.theme.replace(/-/, ''); + o = ThemeManager.get(s.theme); + t.theme = new o(); - if (t.theme.init) - t.theme.init(t, ThemeManager.urls[s.theme] || tinymce.documentBaseURL.replace(/\/$/, '')); + if (t.theme.init) + t.theme.init(t, ThemeManager.urls[s.theme] || tinymce.documentBaseURL.replace(/\/$/, '')); + } else { + t.theme = s.theme; + } } + function initPlugin(p) { var c = PluginManager.get(p), u = PluginManager.urls[p] || tinymce.documentBaseURL.replace(/\/$/, ''), po; if (c && tinymce.inArray(initializedPlugins,p) === -1) { @@ -13001,36 +13462,68 @@ t.controlManager = new tinymce.ControlManager(t); - t.onExecCommand.add(function(ed, c) { - // Don't refresh the select lists until caret move - if (!/^(FontName|FontSize)$/.test(c)) - t.nodeChanged(); - }); - // Enables users to override the control factory t.onBeforeRenderUI.dispatch(t, t.controlManager); // Measure box if (s.render_ui && t.theme) { - w = s.width || e.style.width || e.offsetWidth; - h = s.height || e.style.height || e.offsetHeight; t.orgDisplay = e.style.display; - re = /^[0-9\.]+(|px)$/i; - if (re.test('' + w)) - w = Math.max(parseInt(w, 10) + (o.deltaWidth || 0), 100); + if (typeof s.theme != "function") { + w = s.width || e.style.width || e.offsetWidth; + h = s.height || e.style.height || e.offsetHeight; + mh = s.min_height || 100; + re = /^[0-9\.]+(|px)$/i; - if (re.test('' + h)) - h = Math.max(parseInt(h, 10) + (o.deltaHeight || 0), 100); + if (re.test('' + w)) + w = Math.max(parseInt(w, 10) + (o.deltaWidth || 0), 100); - // Render UI - o = t.theme.renderUI({ - targetNode : e, - width : w, - height : h, - deltaWidth : s.delta_width, - deltaHeight : s.delta_height - }); + if (re.test('' + h)) + h = Math.max(parseInt(h, 10) + (o.deltaHeight || 0), mh); + + // Render UI + o = t.theme.renderUI({ + targetNode : e, + width : w, + height : h, + deltaWidth : s.delta_width, + deltaHeight : s.delta_height + }); + + // Resize editor + DOM.setStyles(o.sizeContainer || o.editorContainer, { + width : w, + height : h + }); + + h = (o.iframeHeight || h) + (typeof(h) == 'number' ? (o.deltaHeight || 0) : ''); + if (h < mh) + h = mh; + } else { + o = s.theme(t, e); + + // Convert element type to id:s + if (o.editorContainer.nodeType) { + o.editorContainer = o.editorContainer.id = o.editorContainer.id || t.id + "_parent"; + } + + // Convert element type to id:s + if (o.iframeContainer.nodeType) { + o.iframeContainer = o.iframeContainer.id = o.iframeContainer.id || t.id + "_iframecontainer"; + } + + // Use specified iframe height or the targets offsetHeight + h = o.iframeHeight || e.offsetHeight; + + // Store away the selection when it's changed to it can be restored later with a editor.focus() call + if (isIE) { + t.onInit.add(function(ed) { + ed.dom.bind(ed.getBody(), 'beforedeactivate keydown', function() { + ed.lastIERng = ed.selection.getRng(); + }); + }); + } + } t.editorContainer = o.editorContainer; } @@ -13042,6 +13535,11 @@ }); } + // Load specified content CSS last + if (s.content_style) { + t.contentStyles.push(s.content_style); + } + // Content editable mode ends here if (s.content_editable) { e = n = o = null; // Fix IE leak @@ -13051,16 +13549,6 @@ // User specified a document.domain value if (document.domain && location.hostname != document.domain) tinymce.relaxedDomain = document.domain; - - // Resize editor - DOM.setStyles(o.sizeContainer || o.editorContainer, { - width : w, - height : h - }); - - h = (o.iframeHeight || h) + (typeof(h) == 'number' ? (o.deltaHeight || 0) : ''); - if (h < 100) - h = 100; t.iframeHTML = s.doctype + '<html><head xmlns="http://www.w3.org/1999/xhtml">'; @@ -13120,7 +13608,14 @@ }); t.contentAreaContainer = o.iframeContainer; - DOM.get(o.editorContainer).style.display = t.orgDisplay; + + if (o.editorContainer) { + DOM.get(o.editorContainer).style.display = t.orgDisplay; + } + + // Restore visibility on target element + e.style.visibility = t.orgVisibility; + DOM.get(t.id).style.display = 'none'; DOM.setAttrib(t.id, 'aria-hidden', true); @@ -13131,7 +13626,7 @@ }, initContentBody : function() { - var self = this, settings = self.settings, targetElm = DOM.get(self.id), doc = self.getDoc(), html, body; + var self = this, settings = self.settings, targetElm = DOM.get(self.id), doc = self.getDoc(), html, body, contentCssText; // Setup iframe body if ((!isIE || !tinymce.relaxedDomain) && !settings.content_editable) { @@ -13230,7 +13725,7 @@ self.serializer = new tinymce.dom.Serializer(settings, self.dom, self.schema); - self.selection = new tinymce.dom.Selection(self.dom, self.getWin(), self.serializer); + self.selection = new tinymce.dom.Selection(self.dom, self.getWin(), self.serializer, self); self.formatter = new tinymce.Formatter(self); @@ -13239,6 +13734,12 @@ self.forceBlocks = new tinymce.ForceBlocks(self); self.enterKey = new tinymce.EnterKey(self); self.editorCommands = new tinymce.EditorCommands(self); + + self.onExecCommand.add(function(editor, command) { + // Don't refresh the select lists until caret move + if (!/^(FontName|FontSize)$/.test(command)) + self.nodeChanged(); + }); // Pass through self.serializer.onPreProcess.add(function(se, o) { @@ -13251,7 +13752,7 @@ self.onPreInit.dispatch(self); - if (!settings.gecko_spellcheck) + if (!settings.browser_spellcheck && !settings.gecko_spellcheck) doc.body.spellcheck = false; if (!settings.readonly) { @@ -13302,6 +13803,17 @@ self.focus(true); self.nodeChanged({initial : true}); + // Add editor specific CSS styles + if (self.contentStyles.length > 0) { + contentCssText = ''; + + each(self.contentStyles, function(style) { + contentCssText += style + "\r\n"; + }); + + self.dom.addStyle(contentCssText); + } + // Load specified content CSS last each(self.contentCSS, function(url) { self.dom.loadCSS(url); @@ -13327,6 +13839,10 @@ var oed, self = this, selection = self.selection, contentEditable = self.settings.content_editable, ieRng, controlElm, doc = self.getDoc(), body; if (!skip_focus) { + if (self.lastIERng) { + selection.setRng(self.lastIERng); + } + // Get selected control element ieRng = selection.getRng(); if (ieRng.item) { @@ -13444,9 +13960,6 @@ // Fix for bug #1896577 it seems that this can not be fired while the editor is loading if (self.initialized) { o = o || {}; - - // Normalize selection for example <b>a</b><i>|a</i> becomes <b>a|</b><i>a</i> - selection.normalize(); // Get start node node = selection.getStart() || self.getBody(); @@ -13792,13 +14305,16 @@ if (!args.no_events) self.onSetContent.dispatch(self, args); - self.selection.normalize(); + // Don't normalize selection if the focused element isn't the body in content editable mode since it will steal focus otherwise + if (!self.settings.content_editable || document.activeElement === self.getBody()) { + self.selection.normalize(); + } return args.content; }, getContent : function(args) { - var self = this, content; + var self = this, content, body = self.getBody(); // Setup args object args = args || {}; @@ -13812,11 +14328,18 @@ // Get raw contents or by default the cleaned contents if (args.format == 'raw') - content = self.getBody().innerHTML; + content = body.innerHTML; + else if (args.format == 'text') + content = body.innerText || body.textContent; else - content = self.serializer.serialize(self.getBody(), args); + content = self.serializer.serialize(body, args); - args.content = tinymce.trim(content); + // Trim whitespace in beginning/end of HTML + if (args.format != 'text') { + args.content = tinymce.trim(content); + } else { + args.content = content; + } // Do post processing if (!args.no_events) @@ -13925,14 +14448,16 @@ return; case 'A': - value = dom.getAttrib(elm, 'name'); - cls = 'mceItemAnchor'; + if (!dom.getAttrib(elm, 'href', false)) { + value = dom.getAttrib(elm, 'name') || elm.id; + cls = 'mceItemAnchor'; - if (value) { - if (self.hasVisual) - dom.addClass(elm, cls); - else - dom.removeClass(elm, cls); + if (value) { + if (self.hasVisual) + dom.addClass(elm, cls); + else + dom.removeClass(elm, cls); + } } return; @@ -13952,13 +14477,12 @@ // Don't clear the window or document if content editable // is enabled since other instances might still be present if (!self.settings.content_editable) { - Event.clear(self.getWin()); - Event.clear(self.getDoc()); + Event.unbind(self.getWin()); + Event.unbind(self.getDoc()); } - Event.clear(self.getBody()); - Event.clear(self.formElement); - Event.unbind(elm); + Event.unbind(self.getBody()); + Event.clear(elm); self.execCallback('remove_instance_callback', self); self.onRemove.dispatch(self); @@ -14160,8 +14684,10 @@ // Handle legacy handle_event_callback option if (settings.handle_event_callback) { self.onEvent.add(function(ed, e, o) { - if (self.execCallback('handle_event_callback', e, ed, o) === false) - Event.cancel(e); + if (self.execCallback('handle_event_callback', e, ed, o) === false) { + e.preventDefault(); + e.stopPropagation(); + } }); } @@ -14227,6 +14753,15 @@ self.focus(true); }; + function nodeChanged(ed, e) { + // Normalize selection for example <b>a</b><i>|a</i> becomes <b>a|</b><i>a</i> except for Ctrl+A since it selects everything + if (e.keyCode != 65 || !tinymce.VK.metaKeyPressed(e)) { + self.selection.normalize(); + } + + self.nodeChanged(); + } + // Add DOM events each(nativeToDispatcherMap, function(dispatcherName, nativeName) { var root = settings.content_editable ? self.getBody() : self.getDoc(); @@ -14261,13 +14796,13 @@ } // Add node change handler - self.onMouseUp.add(self.nodeChanged); + self.onMouseUp.add(nodeChanged); self.onKeyUp.add(function(ed, e) { var keyCode = e.keyCode; if ((keyCode >= 33 && keyCode <= 36) || (keyCode >= 37 && keyCode <= 40) || keyCode == 13 || keyCode == 45 || keyCode == 46 || keyCode == 8 || (tinymce.isMac && (keyCode == 91 || keyCode == 93)) || e.ctrlKey) - self.nodeChanged(); + nodeChanged(ed, e); }); // Add reset handler @@ -14620,7 +15155,7 @@ // Insert bookmark node and get the parent selection.setContent(bookmarkHtml); - parentNode = editor.selection.getNode(); + parentNode = selection.getNode(); rootNode = editor.getBody(); // Opera will return the document node when selection is in root @@ -14692,6 +15227,10 @@ mceInsertRawHTML : function(command, ui, value) { selection.setContent('tiny_mce_marker'); editor.setContent(editor.getContent().replace(/tiny_mce_marker/g, function() { return value })); + }, + + mceToggleFormat : function(command, ui, value) { + toggleFormat(value); }, mceSetContent : function(command, ui, value) { @@ -14783,10 +15322,15 @@ selectAll : function() { var root = dom.getRoot(), rng = dom.createRng(); - rng.setStart(root, 0); - rng.setEnd(root, root.childNodes.length); + // Old IE does a better job with selectall than new versions + if (selection.getRng().setStart) { + rng.setStart(root, 0); + rng.setEnd(root, root.childNodes.length); - editor.selection.setRng(rng); + selection.setRng(rng); + } else { + execNativeCommand('SelectAll'); + } } }); @@ -14825,7 +15369,10 @@ }, 'InsertUnorderedList,InsertOrderedList' : function(command) { - return dom.getParent(selection.getNode(), command == 'insertunorderedlist' ? 'UL' : 'OL'); + var list = dom.getParent(selection.getNode(), 'ul,ol'); + return list && + (command === 'insertunorderedlist' && list.tagName === 'UL' + || command === 'insertorderedlist' && list.tagName === 'OL'); } }, 'state'); @@ -14875,9 +15422,10 @@ }; // Create event instances - onAdd = new Dispatcher(self); - onUndo = new Dispatcher(self); - onRedo = new Dispatcher(self); + onBeforeAdd = new Dispatcher(self); + onAdd = new Dispatcher(self); + onUndo = new Dispatcher(self); + onRedo = new Dispatcher(self); // Pass though onAdd event from UndoManager to Editor as onChange onAdd.add(function(undoman, level) { @@ -14966,6 +15514,8 @@ data : data, typing : false, + + onBeforeAdd: onBeforeAdd, onAdd : onAdd, @@ -14982,6 +15532,8 @@ level = level || {}; level.content = getContent(); + + self.onBeforeAdd.dispatch(self, level); // Add undo level if needed lastLevel = data[index]; @@ -15077,13 +15629,13 @@ var settings = editor.settings, dom = editor.dom, selection = editor.selection, blockElements = editor.schema.getBlockElements(); function addRootBlocks() { - var node = selection.getStart(), rootNode = editor.getBody(), rng, startContainer, startOffset, endContainer, endOffset, rootBlockNode, tempNode, offset = -0xFFFFFF, wrapped; + var node = selection.getStart(), rootNode = editor.getBody(), rng, startContainer, startOffset, endContainer, endOffset, rootBlockNode, tempNode, offset = -0xFFFFFF, wrapped, isInEditorDocument; if (!node || node.nodeType !== 1 || !settings.forced_root_block) return; // Check if node is wrapped in block - while (node != rootNode) { + while (node && node != rootNode) { if (blockElements[node.nodeName]) return; @@ -15105,6 +15657,7 @@ rng.moveToElementText(node); } + isInEditorDocument = rng.parentElement().ownerDocument === editor.getDoc(); tmpRng = rng.duplicate(); tmpRng.collapse(true); startOffset = tmpRng.move('character', offset) * -1; @@ -15120,6 +15673,14 @@ node = rootNode.firstChild; while (node) { if (node.nodeType === 3 || (node.nodeType == 1 && !blockElements[node.nodeName])) { + // Remove empty text nodes + if (node.nodeType === 3 && node.nodeValue.length == 0) { + tempNode = node; + node = node.nextSibling; + dom.remove(tempNode); + continue; + } + if (!rootBlockNode) { rootBlockNode = dom.create(settings.forced_root_block); node.parentNode.insertBefore(rootBlockNode, node); @@ -15135,28 +15696,30 @@ } } - if (rng.setStart) { - rng.setStart(startContainer, startOffset); - rng.setEnd(endContainer, endOffset); - selection.setRng(rng); - } else { - try { - rng = editor.getDoc().body.createTextRange(); - rng.moveToElementText(rootNode); - rng.collapse(true); - rng.moveStart('character', startOffset); - - if (endOffset > 0) - rng.moveEnd('character', endOffset); - - rng.select(); - } catch (ex) { - // Ignore - } - } - - // Only trigger nodeChange when we wrapped nodes to prevent a forever loop if (wrapped) { + if (rng.setStart) { + rng.setStart(startContainer, startOffset); + rng.setEnd(endContainer, endOffset); + selection.setRng(rng); + } else { + // Only select if the previous selection was inside the document to prevent auto focus in quirks mode + if (isInEditorDocument) { + try { + rng = editor.getDoc().body.createTextRange(); + rng.moveToElementText(rootNode); + rng.collapse(true); + rng.moveStart('character', startOffset); + + if (endOffset > 0) + rng.moveEnd('character', endOffset); + + rng.select(); + } catch (ex) { + // Ignore + } + } + } + editor.nodeChanged(); } }; @@ -15224,28 +15787,40 @@ return c; }, - createControl : function(n) { - var c, t = this, ed = t.editor; + createControl : function(name) { + var ctrl, i, l, self = this, editor = self.editor, factories, ctrlName; - each(ed.plugins, function(p) { - if (p.createControl) { - c = p.createControl(n, t); - - if (c) - return false; - } - }); - - switch (n) { - case "|": - case "separator": - return t.createSeparator(); + // Build control factory cache + if (!self.controlFactories) { + self.controlFactories = []; + each(editor.plugins, function(plugin) { + if (plugin.createControl) { + self.controlFactories.push(plugin); + } + }); } - if (!c && ed.buttons && (c = ed.buttons[n])) - return t.createButton(n, c); + // Create controls by asking cached factories + factories = self.controlFactories; + for (i = 0, l = factories.length; i < l; i++) { + ctrl = factories[i].createControl(name, self); - return t.add(c); + if (ctrl) { + return self.add(ctrl); + } + } + + // Create sepearator + if (name === "|" || name === "separator") { + return self.createSeparator(); + } + + // Create control from button collection + if (editor.buttons && (ctrl = editor.buttons[name])) { + return self.createButton(name, ctrl); + } + + return self.add(ctrl); }, createDropMenu : function(id, s, cc) { @@ -15676,10 +16251,11 @@ isBlock = dom.isBlock, forcedRootBlock = ed.settings.forced_root_block, nodeIndex = dom.nodeIndex, - INVISIBLE_CHAR = tinymce.isGecko ? '\u200B' : '\uFEFF', + INVISIBLE_CHAR = '\uFEFF', MCE_ATTR_RE = /^(src|href|style)$/, FALSE = false, TRUE = true, + formatChangeData, undef, getContentEditable = dom.getContentEditable; @@ -16561,7 +17137,7 @@ matchedFormatNames.push(name); } } - }); + }, dom.getRoot()); return matchedFormatNames; }; @@ -16590,6 +17166,62 @@ return FALSE; }; + function formatChanged(formats, callback, similar) { + var currentFormats; + + // Setup format node change logic + if (!formatChangeData) { + formatChangeData = {}; + currentFormats = {}; + + ed.onNodeChange.addToTop(function(ed, cm, node) { + var parents = getParents(node), matchedFormats = {}; + + // Check for new formats + each(formatChangeData, function(callbacks, format) { + each(parents, function(node) { + if (matchNode(node, format, {}, callbacks.similar)) { + if (!currentFormats[format]) { + // Execute callbacks + each(callbacks, function(callback) { + callback(true, {node: node, format: format, parents: parents}); + }); + + currentFormats[format] = callbacks; + } + + matchedFormats[format] = callbacks; + return false; + } + }); + }); + + // Check if current formats still match + each(currentFormats, function(callbacks, format) { + if (!matchedFormats[format]) { + delete currentFormats[format]; + + each(callbacks, function(callback) { + callback(false, {node: node, format: format, parents: parents}); + }); + } + }); + }); + } + + // Add format listeners + each(formats.split(','), function(format) { + if (!formatChangeData[format]) { + formatChangeData[format] = []; + formatChangeData[format].similar = similar; + } + + formatChangeData[format].push(callback); + }); + + return this; + }; + // Expose to public tinymce.extend(this, { get : get, @@ -16600,7 +17232,8 @@ match : match, matchAll : matchAll, matchNode : matchNode, - canApply : canApply + canApply : canApply, + formatChanged: formatChanged }); // Initialize @@ -17506,6 +18139,21 @@ } }; + // Checks if the parent caret container node isn't empty if that is the case it + // will remove the bogus state on all children that isn't empty + function unmarkBogusCaretParents() { + var i, caretContainer, node; + + caretContainer = getParentCaretContainer(selection.getStart()); + if (caretContainer && !dom.isEmpty(caretContainer)) { + tinymce.walk(caretContainer, function(node) { + if (node.nodeType == 1 && node.id !== caretContainerId && !dom.isEmpty(node)) { + dom.setAttrib(node, 'data-mce-bogus', null); + } + }, 'childNodes'); + } + }; + // Only bind the caret events once if (!self._hasCaretEvents) { // Mark current caret container elements as bogus when getting the contents so we don't end up with empty elements @@ -17525,6 +18173,7 @@ tinymce.each('onMouseUp onKeyUp'.split(' '), function(name) { ed[name].addToTop(function() { removeCaretContainer(); + unmarkBogusCaretParents(); }); }); @@ -17535,16 +18184,12 @@ if (keyCode == 8 || keyCode == 37 || keyCode == 39) { removeCaretContainer(getParentCaretContainer(selection.getStart())); } + + unmarkBogusCaretParents(); }); // Remove bogus state if they got filled by contents using editor.selection.setContent - selection.onSetContent.add(function() { - dom.getParent(selection.getStart(), function(node) { - if (node.id !== caretContainerId && dom.getAttrib(node, 'data-mce-bogus') && !dom.isEmpty(node)) { - dom.setAttrib(node, 'data-mce-bogus', null); - } - }); - }); + selection.onSetContent.add(unmarkBogusCaretParents); self._hasCaretEvents = true; } @@ -17661,21 +18306,63 @@ var TreeWalker = tinymce.dom.TreeWalker; tinymce.EnterKey = function(editor) { - var dom = editor.dom, selection = editor.selection, settings = editor.settings, undoManager = editor.undoManager; + var dom = editor.dom, selection = editor.selection, settings = editor.settings, undoManager = editor.undoManager, nonEmptyElementsMap = editor.schema.getNonEmptyElements(); function handleEnterKey(evt) { - var rng = selection.getRng(true), tmpRng, editableRoot, container, offset, parentBlock, documentMode, + var rng = selection.getRng(true), tmpRng, editableRoot, container, offset, parentBlock, documentMode, shiftKey, newBlock, fragment, containerBlock, parentBlockName, containerBlockName, newBlockName, isAfterLastNodeInContainer; // Returns true if the block can be split into two blocks or not function canSplitBlock(node) { return node && dom.isBlock(node) && - !/^(TD|TH|CAPTION)$/.test(node.nodeName) && + !/^(TD|TH|CAPTION|FORM)$/.test(node.nodeName) && !/^(fixed|absolute)/i.test(node.style.position) && dom.getContentEditable(node) !== "true"; }; + // Renders empty block on IE + function renderBlockOnIE(block) { + var oldRng; + + if (tinymce.isIE && dom.isBlock(block)) { + oldRng = selection.getRng(); + block.appendChild(dom.create('span', null, '\u00a0')); + selection.select(block); + block.lastChild.outerHTML = ''; + selection.setRng(oldRng); + } + }; + + // Remove the first empty inline element of the block so this: <p><b><em></em></b>x</p> becomes this: <p>x</p> + function trimInlineElementsOnLeftSideOfBlock(block) { + var node = block, firstChilds = [], i; + + // Find inner most first child ex: <p><i><b>*</b></i></p> + while (node = node.firstChild) { + if (dom.isBlock(node)) { + return; + } + + if (node.nodeType == 1 && !nonEmptyElementsMap[node.nodeName.toLowerCase()]) { + firstChilds.push(node); + } + } + + i = firstChilds.length; + while (i--) { + node = firstChilds[i]; + if (!node.hasChildNodes() || (node.firstChild == node.lastChild && node.firstChild.nodeValue === '')) { + dom.remove(node); + } else { + // Remove <a> </a> see #5381 + if (node.nodeName == "A" && (node.innerText || node.textContent) === ' ') { + dom.remove(node); + } + } + } + }; + // Moves the caret to a suitable position within the root for example in the first non pure whitespace text node or before an image function moveToCaretPosition(root) { var walker, node, rng, y, viewPort, lastNode = root, tempElm; @@ -17692,7 +18379,7 @@ break; } - if (/^(BR|IMG)$/.test(node.nodeName)) { + if (nonEmptyElementsMap[node.nodeName.toLowerCase()]) { rng.setStartBefore(node); rng.setEndBefore(node); break; @@ -17789,6 +18476,11 @@ return true; } + // If the caret if before the first element in parentBlock + if (start && container.nodeType == 1 && container == parentBlock.firstChild) { + return true; + } + // Caret can be before/after a table if (container.nodeName === "TABLE" || (container.previousSibling && container.previousSibling.nodeName == "TABLE")) { return (isAfterLastNodeInContainer && !start) || (!isAfterLastNodeInContainer && start); @@ -17796,20 +18488,34 @@ // Walk the DOM and look for text nodes or non empty elements walker = new TreeWalker(container, parentBlock); - while (node = (start ? walker.prev() : walker.next())) { + + // If caret is in beginning or end of a text block then jump to the next/previous node + if (container.nodeType == 3) { + if (start && offset == 0) { + walker.prev(); + } else if (!start && offset == container.nodeValue.length) { + walker.next(); + } + } + + while (node = walker.current()) { if (node.nodeType === 1) { // Ignore bogus elements - if (node.getAttribute('data-mce-bogus')) { - continue; - } - - // Keep empty elements like <img /> - name = node.nodeName.toLowerCase(); - if (name === 'IMG') { - return false; + if (!node.getAttribute('data-mce-bogus')) { + // Keep empty elements like <img /> <input /> but not trailing br:s like <p>text|<br></p> + name = node.nodeName.toLowerCase(); + if (nonEmptyElementsMap[name] && name !== 'br') { + return false; + } } } else if (node.nodeType === 3 && !/^[ \t\r\n]*$/.test(node.nodeValue)) { return false; + } + + if (start) { + walker.prev(); + } else { + walker.next(); } } @@ -17894,6 +18600,7 @@ } else if (isFirstOrLastLi()) { // Last LI in list then temove LI and add text block after list dom.insertAfter(newBlock, containerBlock); + renderBlockOnIE(newBlock); } else { // Middle LI in list the split the list and insert a text block in the middle // Extract after fragment and insert it after the current block @@ -17930,7 +18637,7 @@ if (container && container.nodeType == 3 && offset >= container.nodeValue.length) { // Insert extra BR element at the end block elements if (!tinymce.isIE && !hasRightSideBr()) { - brElm = dom.create('br') + brElm = dom.create('br'); rng.insertNode(brElm); rng.setStartAfter(brElm); rng.setEndAfter(brElm); @@ -17985,6 +18692,22 @@ return parent !== root ? editableRoot : root; }; + // Adds a BR at the end of blocks that only contains an IMG or INPUT since these might be floated and then they won't expand the block + function addBrToBlockIfNeeded(block) { + var lastChild; + + // IE will render the blocks correctly other browsers needs a BR + if (!tinymce.isIE) { + block.normalize(); // Remove empty text nodes that got left behind by the extract + + // Check if the block is empty or contains a floated last child + lastChild = block.lastChild; + if (!lastChild || (/^(left|right)$/gi.test(dom.getStyle(lastChild, 'float', true)))) { + dom.add(block, 'br'); + } + } + }; + // Delete any selected contents if (!rng.collapsed) { editor.execCommand('Delete'); @@ -17999,15 +18722,20 @@ // Setup range items and newBlockName container = rng.startContainer; offset = rng.startOffset; - newBlockName = settings.forced_root_block; + newBlockName = (settings.force_p_newlines ? 'p' : '') || settings.forced_root_block; newBlockName = newBlockName ? newBlockName.toUpperCase() : ''; documentMode = dom.doc.documentMode; + shiftKey = evt.shiftKey; // Resolve node index if (container.nodeType == 1 && container.hasChildNodes()) { isAfterLastNodeInContainer = offset > container.childNodes.length - 1; container = container.childNodes[Math.min(offset, container.childNodes.length - 1)] || container; - offset = 0; + if (isAfterLastNodeInContainer && container.nodeType == 3) { + offset = container.nodeValue.length; + } else { + offset = 0; + } } // Get editable root node normaly the body element but sometimes a div or span @@ -18022,7 +18750,7 @@ // If editable root isn't block nor the root of the editor if (!dom.isBlock(editableRoot) && editableRoot != dom.getRoot()) { - if (!newBlockName || evt.shiftKey) { + if (!newBlockName || shiftKey) { insertBr(); } @@ -18032,7 +18760,7 @@ // Wrap the current node and it's sibling in a default block if it's needed. // for example this <td>text|<b>text2</b></td> will become this <td><p>text|<b>text2</p></b></td> // This won't happen if root blocks are disabled or the shiftKey is pressed - if ((newBlockName && !evt.shiftKey) || (!newBlockName && evt.shiftKey)) { + if ((newBlockName && !shiftKey) || (!newBlockName && shiftKey)) { container = wrapSelfAndSiblingsInDefaultBlock(container, offset); } @@ -18044,26 +18772,34 @@ parentBlockName = parentBlock ? parentBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5 containerBlockName = containerBlock ? containerBlock.nodeName.toUpperCase() : ''; // IE < 9 & HTML5 - // Handle enter inside an empty list item - if (parentBlockName == 'LI' && dom.isEmpty(parentBlock)) { - // Let the list plugin or browser handle nested lists for now - if (/^(UL|OL|LI)$/.test(containerBlock.parentNode.nodeName)) { - return false; + // Handle enter in LI + if (parentBlockName == 'LI') { + if (!newBlockName && shiftKey) { + insertBr(); + return; } - handleEmptyListItem(); - return; + // Handle enter inside an empty list item + if (dom.isEmpty(parentBlock)) { + // Let the list plugin or browser handle nested lists for now + if (/^(UL|OL|LI)$/.test(containerBlock.parentNode.nodeName)) { + return false; + } + + handleEmptyListItem(); + return; + } } // Don't split PRE tags but insert a BR instead easier when writing code samples etc if (parentBlockName == 'PRE' && settings.br_in_pre !== false) { - if (!evt.shiftKey) { + if (!shiftKey) { insertBr(); return; } } else { // If no root block is configured then insert a BR by default or if the shiftKey is pressed - if ((!newBlockName && !evt.shiftKey && parentBlockName != 'LI') || (newBlockName && evt.shiftKey)) { + if ((!newBlockName && !shiftKey && parentBlockName != 'LI') || (newBlockName && shiftKey)) { insertBr(); return; } @@ -18088,9 +18824,12 @@ } else { dom.insertAfter(newBlock, parentBlock); } + + moveToCaretPosition(newBlock); } else if (isCaretAtStartOrEndOfBlock(true)) { // Insert new block before newBlock = parentBlock.parentNode.insertBefore(createNewBlock(), parentBlock); + renderBlockOnIE(newBlock); } else { // Extract after fragment and insert it after the current block tmpRng = rng.cloneRange(); @@ -18099,10 +18838,12 @@ trimLeadingLineBreaks(fragment); newBlock = fragment.firstChild; dom.insertAfter(fragment, parentBlock); + trimInlineElementsOnLeftSideOfBlock(newBlock); + addBrToBlockIfNeeded(parentBlock); + moveToCaretPosition(newBlock); } dom.setAttrib(newBlock, 'id', ''); // Remove ID since it needs to be document unique - moveToCaretPosition(newBlock); undoManager.add(); } -- Gitblit v1.9.1