| Name | Size | Mode | Actions |
|---|---|---|---|
| caret/ | - | 0755 | rm |
| data/ | - | 0755 | rm |
| dom/ | - | 0755 | rm |
| file/ | - | 0755 | rm |
| fmt/ | - | 0755 | rm |
| geom/ | - | 0755 | rm |
| html/ | - | 0755 | rm |
| text/ | - | 0755 | rm |
| ui/ | - | 0755 | rm |
| util/ | - | 0755 | rm |
| AddOnManager.js | 7664 | 0644 | editdlrm |
| Compat.js | 2065 | 0644 | editdlrm |
| DragDropOverrides.js | 7153 | 0644 | editdlrm |
| Editor.js | 65215 | 0644 | editdlrm |
| EditorCommands.js | 21227 | 0644 | editdlrm |
| EditorManager.js | 18496 | 0644 | editdlrm |
| EditorObservable.js | 5308 | 0644 | editdlrm |
| EditorUpload.js | 5746 | 0644 | editdlrm |
| EnterKey.js | 21487 | 0644 | editdlrm |
| Env.js | 4419 | 0644 | editdlrm |
| FocusManager.js | 8836 | 0644 | editdlrm |
| ForceBlocks.js | 3791 | 0644 | editdlrm |
| Formatter.js | 71892 | 0644 | editdlrm |
| InsertContent.js | 10850 | 0644 | editdlrm |
| InsertList.js | 4825 | 0644 | editdlrm |
| jquery.tinymce.js | 10167 | 0644 | editdlrm |
| LegacyInput.js | 1829 | 0644 | editdlrm |
| Mode.js | 1917 | 0644 | editdlrm |
| NodeChange.js | 4670 | 0644 | editdlrm |
| NotificationManager.js | 3652 | 0644 | editdlrm |
| Register.js | 644 | 0644 | editdlrm |
| SelectionOverrides.js | 26883 | 0644 | editdlrm |
| Shortcuts.js | 5481 | 0644 | editdlrm |
| UndoManager.js | 9691 | 0644 | editdlrm |
| WindowManager.js | 7208 | 0644 | editdlrm |
/home/aqcert/public_html/admin/tinymce/js/tinymce/classes/Formatter.js (71892B)
[a
]b
->[a]
b
function adjustSelectionToVisibleSelection() { function findSelectionEnd(start, end) { var walker = new TreeWalker(end); for (node = walker.prev2(); node; node = walker.prev2()) { if (node.nodeType == 3 && node.data.length > 0) { return node; } if (node.childNodes.length > 1 || node == start || node.tagName == 'BR') { return node; } } } // Adjust selection so that a end container with a end offset of zero is not included in the selection // as this isn't visible to the user. var rng = ed.selection.getRng(); var start = rng.startContainer; var end = rng.endContainer; if (start != end && rng.endOffset === 0) { var newEnd = findSelectionEnd(start, end); var endOffset = newEnd.nodeType == 3 ? newEnd.data.length : newEnd.childNodes.length; rng.setEnd(newEnd, endOffset); } return rng; } function applyRngStyle(rng, bookmark, node_specific) { var newWrappers = [], wrapName, wrapElm, contentEditable = true; // Setup wrapper element wrapName = format.inline || format.block; wrapElm = dom.create(wrapName); setElementFormat(wrapElm); rangeUtils.walk(rng, function(nodes) { var currentWrapElm; /** * Process a list of nodes wrap them. */ function process(node) { var nodeName, parentName, found, hasContentEditableState, lastContentEditable; lastContentEditable = contentEditable; nodeName = node.nodeName.toLowerCase(); parentName = node.parentNode.nodeName.toLowerCase(); // Node has a contentEditable value if (node.nodeType === 1 && getContentEditable(node)) { lastContentEditable = contentEditable; contentEditable = getContentEditable(node) === "true"; hasContentEditableState = true; // We don't want to wrap the container only it's children } // Stop wrapping on br elements if (isEq(nodeName, 'br')) { currentWrapElm = 0; // Remove any br elements when we wrap things if (format.block) { dom.remove(node); } return; } // If node is wrapper type if (format.wrapper && matchNode(node, name, vars)) { currentWrapElm = 0; return; } // Can we rename the block // TODO: Break this if up, too complex if (contentEditable && !hasContentEditableState && format.block && !format.wrapper && isTextBlock(nodeName) && isValid(parentName, wrapName)) { node = dom.rename(node, wrapName); setElementFormat(node); newWrappers.push(node); currentWrapElm = 0; return; } // Handle selector patterns if (format.selector) { // Look for matching formats each(formatList, function(format) { // Check collapsed state if it exists if ('collapsed' in format && format.collapsed !== isCollapsed) { return; } if (dom.is(node, format.selector) && !isCaretNode(node)) { setElementFormat(node, format); found = true; return false; } }); // Continue processing if a selector match wasn't found and a inline element is defined if (!format.inline || found) { currentWrapElm = 0; return; } } // Is it valid to wrap this item // TODO: Break this if up, too complex if (contentEditable && !hasContentEditableState && isValid(wrapName, nodeName) && isValid(parentName, wrapName) && !(!node_specific && node.nodeType === 3 && node.nodeValue.length === 1 && node.nodeValue.charCodeAt(0) === 65279) && !isCaretNode(node) && (!format.inline || !isBlock(node))) { // Start wrapping if (!currentWrapElm) { // Wrap the node currentWrapElm = dom.clone(wrapElm, FALSE); node.parentNode.insertBefore(currentWrapElm, node); newWrappers.push(currentWrapElm); } currentWrapElm.appendChild(node); } else { // Start a new wrapper for possible children currentWrapElm = 0; each(grep(node.childNodes), process); if (hasContentEditableState) { contentEditable = lastContentEditable; // Restore last contentEditable state from stack } // End the last wrapper currentWrapElm = 0; } } // Process siblings from range each(nodes, process); }); // Apply formats to links as well to get the color of the underline to change as well if (format.links === true) { each(newWrappers, function(node) { function process(node) { if (node.nodeName === 'A') { setElementFormat(node, format); } each(grep(node.childNodes), process); } process(node); }); } // Cleanup each(newWrappers, function(node) { var childCount; function getChildCount(node) { var count = 0; each(node.childNodes, function(node) { if (!isWhiteSpaceNode(node) && !isBookmarkNode(node)) { count++; } }); return count; } function mergeStyles(node) { var child, clone; each(node.childNodes, function(node) { if (node.nodeType == 1 && !isBookmarkNode(node) && !isCaretNode(node)) { child = node; return FALSE; // break loop } }); // If child was found and of the same type as the current node if (child && !isBookmarkNode(child) && matchName(child, format)) { clone = dom.clone(child, FALSE); setElementFormat(clone); dom.replace(clone, node, TRUE); dom.remove(child, 1); } return clone || node; } childCount = getChildCount(node); // Remove empty nodes but only if there is multiple wrappers and they are not block // elements so never remove single since that would remove the // current empty block element where the caret is at if ((newWrappers.length > 1 || !isBlock(node)) && childCount === 0) { dom.remove(node, 1); return; } if (format.inline || format.wrapper) { // Merges the current node with it's children of similar type to reduce the number of elements if (!format.exact && childCount === 1) { node = mergeStyles(node); } // Remove/merge children each(formatList, function(format) { // Merge all children of similar type will move styles from child to parent // this: text // will become: text each(dom.select(format.inline, node), function(child) { if (isBookmarkNode(child)) { return; } removeFormat(format, vars, child, format.exact ? child : null); }); }); // Remove child if direct parent is of same type if (matchNode(node.parentNode, name, vars)) { dom.remove(node, 1); node = 0; return TRUE; } // Look for parent with similar style format if (format.merge_with_parents) { dom.getParent(node.parentNode, function(parent) { if (matchNode(parent, name, vars)) { dom.remove(node, 1); node = 0; return TRUE; } }); } // Merge next and previous siblings if they are similar texttext becomes texttext if (node && format.merge_siblings !== false) { node = mergeSiblings(getNonWhiteSpaceSibling(node), node); node = mergeSiblings(node, getNonWhiteSpaceSibling(node, TRUE)); } } }); } if (getContentEditable(selection.getNode()) === "false") { node = selection.getNode(); for (var i = 0, l = formatList.length; i < l; i++) { if (formatList[i].ceFalseOverride && dom.is(node, formatList[i].selector)) { setElementFormat(node, formatList[i]); return; } } return; } if (format) { if (node) { if (node.nodeType) { rng = dom.createRng(); rng.setStartBefore(node); rng.setEndAfter(node); applyRngStyle(expandRng(rng, formatList), null, true); } else { applyRngStyle(node, null, true); } } else { if (!isCollapsed || !format.inline || dom.select('td[data-mce-selected],th[data-mce-selected]').length) { // Obtain selection node before selection is unselected by applyRngStyle() var curSelNode = ed.selection.getNode(); // If the formats have a default block and we can't find a parent block then // start wrapping it with a DIV this is for forced_root_blocks: false // It's kind of a hack but people should be using the default block type P since all desktop editors work that way if (!forcedRootBlock && formatList[0].defaultBlock && !dom.getParent(curSelNode, dom.isBlock)) { apply(formatList[0].defaultBlock); } // Apply formatting to selection ed.selection.setRng(adjustSelectionToVisibleSelection()); bookmark = selection.getBookmark(); applyRngStyle(expandRng(selection.getRng(TRUE), formatList), bookmark); // Colored nodes should be underlined so that the color of the underline matches the text color. if (format.styles && (format.styles.color || format.styles.textDecoration)) { walk(curSelNode, processUnderlineAndColor, 'childNodes'); processUnderlineAndColor(curSelNode); } selection.moveToBookmark(bookmark); moveStart(selection.getRng(TRUE)); ed.nodeChanged(); } else { performCaretAction('apply', name, vars); } } Hooks.postProcess(name, ed); } } /** * Removes the specified format from the current selection or specified node. * * @method remove * @param {String} name Name of format to remove. * @param {Object} vars Optional list of variables to replace within format before removing it. * @param {Node/Range} node Optional node or DOM range to remove the format from defaults to current selection. */ function remove(name, vars, node, similar) { var formatList = get(name), format = formatList[0], bookmark, rng, contentEditable = true; // Merges the styles for each node function process(node) { var children, i, l, lastContentEditable, hasContentEditableState; // Node has a contentEditable value if (node.nodeType === 1 && getContentEditable(node)) { lastContentEditable = contentEditable; contentEditable = getContentEditable(node) === "true"; hasContentEditableState = true; // We don't want to wrap the container only it's children } // Grab the children first since the nodelist might be changed children = grep(node.childNodes); // Process current node if (contentEditable && !hasContentEditableState) { for (i = 0, l = formatList.length; i < l; i++) { if (removeFormat(formatList[i], vars, node, node)) { break; } } } // Process the children if (format.deep) { if (children.length) { for (i = 0, l = children.length; i < l; i++) { process(children[i]); } if (hasContentEditableState) { contentEditable = lastContentEditable; // Restore last contentEditable state from stack } } } } function findFormatRoot(container) { var formatRoot; // Find format root each(getParents(container.parentNode).reverse(), function(parent) { var format; // Find format root element if (!formatRoot && parent.id != '_start' && parent.id != '_end') { // Is the node matching the format we are looking for format = matchNode(parent, name, vars, similar); if (format && format.split !== false) { formatRoot = parent; } } }); return formatRoot; } function wrapAndSplit(formatRoot, container, target, split) { var parent, clone, lastClone, firstClone, i, formatRootParent; // Format root found then clone formats and split it if (formatRoot) { formatRootParent = formatRoot.parentNode; for (parent = container.parentNode; parent && parent != formatRootParent; parent = parent.parentNode) { clone = dom.clone(parent, FALSE); for (i = 0; i < formatList.length; i++) { if (removeFormat(formatList[i], vars, clone, clone)) { clone = 0; break; } } // Build wrapper node if (clone) { if (lastClone) { clone.appendChild(lastClone); } if (!firstClone) { firstClone = clone; } lastClone = clone; } } // Never split block elements if the format is mixed if (split && (!format.mixed || !isBlock(formatRoot))) { container = dom.split(formatRoot, container); } // Wrap container in cloned formats if (lastClone) { target.parentNode.insertBefore(lastClone, target); firstClone.appendChild(target); } } return container; } function splitToFormatRoot(container) { return wrapAndSplit(findFormatRoot(container), container, container, true); } function unwrap(start) { var node = dom.get(start ? '_start' : '_end'), out = node[start ? 'firstChild' : 'lastChild']; // If the end is placed within the start the result will be removed // So this checks if the out node is a bookmark node if it is it // checks for another more suitable node if (isBookmarkNode(out)) { out = out[start ? 'firstChild' : 'lastChild']; } // Since dom.remove removes empty text nodes then we need to try to find a better node if (out.nodeType == 3 && out.data.length === 0) { out = start ? node.previousSibling || node.nextSibling : node.nextSibling || node.previousSibling; } dom.remove(node, true); return out; } function removeRngStyle(rng) { var startContainer, endContainer; var commonAncestorContainer = rng.commonAncestorContainer; rng = expandRng(rng, formatList, TRUE); if (format.split) { startContainer = getContainer(rng, TRUE); endContainer = getContainer(rng); if (startContainer != endContainer) { // WebKit will render the table incorrectly if we wrap a TH or TD in a SPAN // so let's see if we can use the first child instead // This will happen if you triple click a table cell and use remove formatting if (/^(TR|TH|TD)$/.test(startContainer.nodeName) && startContainer.firstChild) { if (startContainer.nodeName == "TR") { startContainer = startContainer.firstChild.firstChild || startContainer; } else { startContainer = startContainer.firstChild || startContainer; } } // Try to adjust endContainer as well if cells on the same row were selected - bug #6410 if (commonAncestorContainer && /^T(HEAD|BODY|FOOT|R)$/.test(commonAncestorContainer.nodeName) && isTableCell(endContainer) && endContainer.firstChild) { endContainer = endContainer.firstChild || endContainer; } if (dom.isChildOf(startContainer, endContainer) && !isBlock(endContainer) && !isTableCell(startContainer) && !isTableCell(endContainer)) { startContainer = wrap(startContainer, 'span', {id: '_start', 'data-mce-type': 'bookmark'}); splitToFormatRoot(startContainer); startContainer = unwrap(TRUE); return; } // Wrap start/end nodes in span element since these might be cloned/moved startContainer = wrap(startContainer, 'span', {id: '_start', 'data-mce-type': 'bookmark'}); endContainer = wrap(endContainer, 'span', {id: '_end', 'data-mce-type': 'bookmark'}); // Split start/end splitToFormatRoot(startContainer); splitToFormatRoot(endContainer); // Unwrap start/end to get real elements again startContainer = unwrap(TRUE); endContainer = unwrap(); } else { startContainer = endContainer = splitToFormatRoot(startContainer); } // Update range positions since they might have changed after the split operations rng.startContainer = startContainer.parentNode ? startContainer.parentNode : startContainer; rng.startOffset = nodeIndex(startContainer); rng.endContainer = endContainer.parentNode ? endContainer.parentNode : endContainer; rng.endOffset = nodeIndex(endContainer) + 1; } // Remove items between start/end rangeUtils.walk(rng, function(nodes) { each(nodes, function(node) { process(node); // Remove parent span if it only contains text-decoration: underline, yet a parent node is also underlined. if (node.nodeType === 1 && ed.dom.getStyle(node, 'text-decoration') === 'underline' && node.parentNode && getTextDecoration(node.parentNode) === 'underline') { removeFormat({ 'deep': false, 'exact': true, 'inline': 'span', 'styles': { 'textDecoration': 'underline' } }, null, node); } }); }); } // Handle node if (node) { if (node.nodeType) { rng = dom.createRng(); rng.setStartBefore(node); rng.setEndAfter(node); removeRngStyle(rng); } else { removeRngStyle(node); } return; } if (getContentEditable(selection.getNode()) === "false") { node = selection.getNode(); for (var i = 0, l = formatList.length; i < l; i++) { if (formatList[i].ceFalseOverride) { if (removeFormat(formatList[i], vars, node, node)) { break; } } } return; } if (!selection.isCollapsed() || !format.inline || dom.select('td[data-mce-selected],th[data-mce-selected]').length) { bookmark = selection.getBookmark(); removeRngStyle(selection.getRng(TRUE)); selection.moveToBookmark(bookmark); // Check if start element still has formatting then we are at: "text|text" // and need to move the start into the next text node if (format.inline && match(name, vars, selection.getStart())) { moveStart(selection.getRng(true)); } ed.nodeChanged(); } else { performCaretAction('remove', name, vars, similar); } } /** * Toggles the specified format on/off. * * @method toggle * @param {String} name Name of format to apply/remove. * @param {Object} vars Optional list of variables to replace within format before applying/removing it. * @param {Node} node Optional node to apply the format to or remove from. Defaults to current selection. */ function toggle(name, vars, node) { var fmt = get(name); if (match(name, vars, node) && (!('toggle' in fmt[0]) || fmt[0].toggle)) { remove(name, vars, node); } else { apply(name, vars, node); } } /** * Return true/false if the specified node has the specified format. * * @method matchNode * @param {Node} node Node to check the format on. * @param {String} name Format name to check. * @param {Object} vars Optional list of variables to replace before checking it. * @param {Boolean} similar Match format that has similar properties. * @return {Object} Returns the format object it matches or undefined if it doesn't match. */ function matchNode(node, name, vars, similar) { var formatList = get(name), format, i, classes; function matchItems(node, format, item_name) { var key, value, items = format[item_name], i; // Custom match if (format.onmatch) { return format.onmatch(node, format, item_name); } // Check all items if (items) { // Non indexed object if (items.length === undef) { for (key in items) { if (items.hasOwnProperty(key)) { if (item_name === 'attributes') { value = dom.getAttrib(node, key); } else { value = getStyle(node, key); } if (similar && !value && !format.exact) { return; } if ((!similar || format.exact) && !isEq(value, normalizeStyleValue(replaceVars(items[key], vars), key))) { return; } } } } else { // Only one match needed for indexed arrays for (i = 0; i < items.length; i++) { if (item_name === 'attributes' ? dom.getAttrib(node, items[i]) : getStyle(node, items[i])) { return format; } } } } return format; } if (formatList && node) { // Check each format in list for (i = 0; i < formatList.length; i++) { format = formatList[i]; // Name name, attributes, styles and classes if (matchName(node, format) && matchItems(node, format, 'attributes') && matchItems(node, format, 'styles')) { // Match classes if ((classes = format.classes)) { for (i = 0; i < classes.length; i++) { if (!dom.hasClass(node, classes[i])) { return; } } } return format; } } } } /** * Matches the current selection or specified node against the specified format name. * * @method match * @param {String} name Name of format to match. * @param {Object} vars Optional list of variables to replace before checking it. * @param {Node} node Optional node to check. * @return {boolean} true/false if the specified selection/node matches the format. */ function match(name, vars, node) { var startNode; function matchParents(node) { var root = dom.getRoot(); if (node === root) { return false; } // Find first node with similar format settings node = dom.getParent(node, function(node) { if (matchesUnInheritedFormatSelector(node, name)) { return true; } return node.parentNode === root || !!matchNode(node, name, vars, true); }); // Do an exact check on the similar format element return matchNode(node, name, vars); } // Check specified node if (node) { return matchParents(node); } // Check selected node node = selection.getNode(); if (matchParents(node)) { return TRUE; } // Check start node if it's different startNode = selection.getStart(); if (startNode != node) { if (matchParents(startNode)) { return TRUE; } } return FALSE; } /** * Matches the current selection against the array of formats and returns a new array with matching formats. * * @method matchAll * @param {Array} names Name of format to match. * @param {Object} vars Optional list of variables to replace before checking it. * @return {Array} Array with matched formats. */ function matchAll(names, vars) { var startElement, matchedFormatNames = [], checkedMap = {}; // Check start of selection for formats startElement = selection.getStart(); dom.getParent(startElement, function(node) { var i, name; for (i = 0; i < names.length; i++) { name = names[i]; if (!checkedMap[name] && matchNode(node, name, vars)) { checkedMap[name] = true; matchedFormatNames.push(name); } } }, dom.getRoot()); return matchedFormatNames; } /** * Returns true/false if the specified format can be applied to the current selection or not. It * will currently only check the state for selector formats, it returns true on all other format types. * * @method canApply * @param {String} name Name of format to check. * @return {boolean} true/false if the specified format can be applied to the current selection/node. */ function canApply(name) { var formatList = get(name), startNode, parents, i, x, selector; if (formatList) { startNode = selection.getStart(); parents = getParents(startNode); for (x = formatList.length - 1; x >= 0; x--) { selector = formatList[x].selector; // Format is not selector based then always return TRUE // Is it has a defaultBlock then it's likely it can be applied for example align on a non block element line if (!selector || formatList[x].defaultBlock) { return TRUE; } for (i = parents.length - 1; i >= 0; i--) { if (dom.is(parents[i], selector)) { return TRUE; } } } } return FALSE; } /** * Executes the specified callback when the current selection matches the formats or not. * * @method formatChanged * @param {String} formats Comma separated list of formats to check for. * @param {function} callback Callback with state and args when the format is changed/toggled on/off. * @param {Boolean} similar True/false state if the match should handle similar or exact formats. */ function formatChanged(formats, callback, similar) { var currentFormats; // Setup format node change logic if (!formatChangeData) { formatChangeData = {}; currentFormats = {}; ed.on('NodeChange', function(e) { var parents = getParents(e.element), matchedFormats = {}; // Ignore bogus nodes like the tag created by moveStart() parents = Tools.grep(parents, function(node) { return node.nodeType == 1 && !node.getAttribute('data-mce-bogus'); }); // 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; } if (matchesUnInheritedFormatSelector(node, format)) { 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: e.element, 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; } /** * Returns a preview css text for the specified format. * * @method getCssText * @param {String/Object} format Format to generate preview css text for. * @return {String} Css text for the specified format. * @example * var cssText1 = editor.formatter.getCssText('bold'); * var cssText2 = editor.formatter.getCssText({inline: 'b'}); */ function getCssText(format) { return Preview.getCssText(ed, format); } // Expose to public extend(this, { get: get, register: register, unregister: unregister, apply: apply, remove: remove, toggle: toggle, match: match, matchAll: matchAll, matchNode: matchNode, canApply: canApply, formatChanged: formatChanged, getCssText: getCssText }); // Initialize defaultFormats(); addKeyboardShortcuts(); ed.on('BeforeGetContent', function(e) { if (markCaretContainersBogus && e.format != 'raw') { markCaretContainersBogus(); } }); ed.on('mouseup keydown', function(e) { if (disableCaretContainer) { disableCaretContainer(e); } }); // Private functions /** * Checks if the specified nodes name matches the format inline/block or selector. * * @private * @param {Node} node Node to match against the specified format. * @param {Object} format Format object o match with. * @return {boolean} true/false if the format matches. */ function matchName(node, format) { // Check for inline match if (isEq(node, format.inline)) { return TRUE; } // Check for block match if (isEq(node, format.block)) { return TRUE; } // Check for selector match if (format.selector) { return node.nodeType == 1 && dom.is(node, format.selector); } } /** * Compares two string/nodes regardless of their case. * * @private * @param {String/Node} str1 Node or string to compare. * @param {String/Node} str2 Node or string to compare. * @return {boolean} True/false if they match. */ function isEq(str1, str2) { str1 = str1 || ''; str2 = str2 || ''; str1 = '' + (str1.nodeName || str1); str2 = '' + (str2.nodeName || str2); return str1.toLowerCase() == str2.toLowerCase(); } /** * Returns the style by name on the specified node. This method modifies the style * contents to make it more easy to match. This will resolve a few browser issues. * * @private * @param {Node} node to get style from. * @param {String} name Style name to get. * @return {String} Style item value. */ function getStyle(node, name) { return normalizeStyleValue(dom.getStyle(node, name), name); } /** * Normalize style value by name. This method modifies the style contents * to make it more easy to match. This will resolve a few browser issues. * * @private * @param {String} value Value to get style from. * @param {String} name Style name to get. * @return {String} Style item value. */ function normalizeStyleValue(value, name) { // Force the format to hex if (name == 'color' || name == 'backgroundColor') { value = dom.toHex(value); } // Opera will return bold as 700 if (name == 'fontWeight' && value == 700) { value = 'bold'; } // Normalize fontFamily so "'Font name', Font" becomes: "Font name,Font" if (name == 'fontFamily') { value = value.replace(/[\'\"]/g, '').replace(/,\s+/g, ','); } return '' + value; } /** * Replaces variables in the value. The variable format is %var. * * @private * @param {String} value Value to replace variables in. * @param {Object} vars Name/value array with variables to replace. * @return {String} New value with replaced variables. */ function replaceVars(value, vars) { if (typeof value != "string") { value = value(vars); } else if (vars) { value = value.replace(/%(\w+)/g, function(str, name) { return vars[name] || str; }); } return value; } function isWhiteSpaceNode(node) { return node && node.nodeType === 3 && /^([\t \r\n]+|)$/.test(node.nodeValue); } function wrap(node, name, attrs) { var wrapper = dom.create(name, attrs); node.parentNode.insertBefore(wrapper, node); wrapper.appendChild(node); return wrapper; } /** * Expands the specified range like object to depending on format. * * For example on block formats it will move the start/end position * to the beginning of the current block. * * @private * @param {Object} rng Range like object. * @param {Array} format Array with formats to expand by. * @param {Boolean} remove * @return {Object} Expanded range like object. */ function expandRng(rng, format, remove) { var lastIdx, leaf, endPoint, startContainer = rng.startContainer, startOffset = rng.startOffset, endContainer = rng.endContainer, endOffset = rng.endOffset; // This function walks up the tree if there is no siblings before/after the node function findParentContainer(start) { var container, parent, sibling, siblingName, root; container = parent = start ? startContainer : endContainer; siblingName = start ? 'previousSibling' : 'nextSibling'; root = dom.getRoot(); function isBogusBr(node) { return node.nodeName == "BR" && node.getAttribute('data-mce-bogus') && !node.nextSibling; } // If it's a text node and the offset is inside the text if (container.nodeType == 3 && !isWhiteSpaceNode(container)) { if (start ? startOffset > 0 : endOffset < container.nodeValue.length) { return container; } } /*eslint no-constant-condition:0 */ while (true) { // Stop expanding on block elements if (!format[0].block_expand && isBlock(parent)) { return parent; } // Walk left/right for (sibling = parent[siblingName]; sibling; sibling = sibling[siblingName]) { if (!isBookmarkNode(sibling) && !isWhiteSpaceNode(sibling) && !isBogusBr(sibling)) { return parent; } } // Check if we can move up are we at root level or body level if (parent == root || parent.parentNode == root) { container = parent; break; } parent = parent.parentNode; } return container; } // This function walks down the tree to find the leaf at the selection. // The offset is also returned as if node initially a leaf, the offset may be in the middle of the text node. function findLeaf(node, offset) { if (offset === undef) { offset = node.nodeType === 3 ? node.length : node.childNodes.length; } while (node && node.hasChildNodes()) { node = node.childNodes[offset]; if (node) { offset = node.nodeType === 3 ? node.length : node.childNodes.length; } } return {node: node, offset: offset}; } // If index based start position then resolve it if (startContainer.nodeType == 1 && startContainer.hasChildNodes()) { lastIdx = startContainer.childNodes.length - 1; startContainer = startContainer.childNodes[startOffset > lastIdx ? lastIdx : startOffset]; if (startContainer.nodeType == 3) { startOffset = 0; } } // If index based end position then resolve it if (endContainer.nodeType == 1 && endContainer.hasChildNodes()) { lastIdx = endContainer.childNodes.length - 1; endContainer = endContainer.childNodes[endOffset > lastIdx ? lastIdx : endOffset - 1]; if (endContainer.nodeType == 3) { endOffset = endContainer.nodeValue.length; } } // Expands the node to the closes contentEditable false element if it exists function findParentContentEditable(node) { var parent = node; while (parent) { if (parent.nodeType === 1 && getContentEditable(parent)) { return getContentEditable(parent) === "false" ? parent : node; } parent = parent.parentNode; } return node; } function findWordEndPoint(container, offset, start) { var walker, node, pos, lastTextNode; function findSpace(node, offset) { var pos, pos2, str = node.nodeValue; if (typeof offset == "undefined") { offset = start ? str.length : 0; } if (start) { pos = str.lastIndexOf(' ', offset); pos2 = str.lastIndexOf('\u00a0', offset); pos = pos > pos2 ? pos : pos2; // Include the space on remove to avoid tag soup if (pos !== -1 && !remove) { pos++; } } else { pos = str.indexOf(' ', offset); pos2 = str.indexOf('\u00a0', offset); pos = pos !== -1 && (pos2 === -1 || pos < pos2) ? pos : pos2; } return pos; } if (container.nodeType === 3) { pos = findSpace(container, offset); if (pos !== -1) { return {container: container, offset: pos}; } lastTextNode = container; } // Walk the nodes inside the block walker = new TreeWalker(container, dom.getParent(container, isBlock) || ed.getBody()); while ((node = walker[start ? 'prev' : 'next']())) { if (node.nodeType === 3) { lastTextNode = node; pos = findSpace(node); if (pos !== -1) { return {container: node, offset: pos}; } } else if (isBlock(node)) { break; } } if (lastTextNode) { if (start) { offset = 0; } else { offset = lastTextNode.length; } return {container: lastTextNode, offset: offset}; } } function findSelectorEndPoint(container, sibling_name) { var parents, i, y, curFormat; if (container.nodeType == 3 && container.nodeValue.length === 0 && container[sibling_name]) { container = container[sibling_name]; } parents = getParents(container); for (i = 0; i < parents.length; i++) { for (y = 0; y < format.length; y++) { curFormat = format[y]; // If collapsed state is set then skip formats that doesn't match that if ("collapsed" in curFormat && curFormat.collapsed !== rng.collapsed) { continue; } if (dom.is(parents[i], curFormat.selector)) { return parents[i]; } } } return container; } function findBlockEndPoint(container, sibling_name) { var node, root = dom.getRoot(); // Expand to block of similar type if (!format[0].wrapper) { node = dom.getParent(container, format[0].block, root); } // Expand to first wrappable block element or any block element if (!node) { node = dom.getParent(container.nodeType == 3 ? container.parentNode : container, function(node) { // Fixes #6183 where it would expand to editable parent element in inline mode return node != root && isTextBlock(node); }); } // Exclude inner lists from wrapping if (node && format[0].wrapper) { node = getParents(node, 'ul,ol').reverse()[0] || node; } // Didn't find a block element look for first/last wrappable element if (!node) { node = container; while (node[sibling_name] && !isBlock(node[sibling_name])) { node = node[sibling_name]; // Break on BR but include it will be removed later on // we can't remove it now since we need to check if it can be wrapped if (isEq(node, 'br')) { break; } } } return node || container; } // Expand to closest contentEditable element startContainer = findParentContentEditable(startContainer); endContainer = findParentContentEditable(endContainer); // Exclude bookmark nodes if possible if (isBookmarkNode(startContainer.parentNode) || isBookmarkNode(startContainer)) { startContainer = isBookmarkNode(startContainer) ? startContainer : startContainer.parentNode; startContainer = startContainer.nextSibling || startContainer; if (startContainer.nodeType == 3) { startOffset = 0; } } if (isBookmarkNode(endContainer.parentNode) || isBookmarkNode(endContainer)) { endContainer = isBookmarkNode(endContainer) ? endContainer : endContainer.parentNode; endContainer = endContainer.previousSibling || endContainer; if (endContainer.nodeType == 3) { endOffset = endContainer.length; } } if (format[0].inline) { if (rng.collapsed) { // Expand left to closest word boundary endPoint = findWordEndPoint(startContainer, startOffset, true); if (endPoint) { startContainer = endPoint.container; startOffset = endPoint.offset; } // Expand right to closest word boundary endPoint = findWordEndPoint(endContainer, endOffset); if (endPoint) { endContainer = endPoint.container; endOffset = endPoint.offset; } } // Avoid applying formatting to a trailing space. leaf = findLeaf(endContainer, endOffset); if (leaf.node) { while (leaf.node && leaf.offset === 0 && leaf.node.previousSibling) { leaf = findLeaf(leaf.node.previousSibling); } if (leaf.node && leaf.offset > 0 && leaf.node.nodeType === 3 && leaf.node.nodeValue.charAt(leaf.offset - 1) === ' ') { if (leaf.offset > 1) { endContainer = leaf.node; endContainer.splitText(leaf.offset - 1); } } } } // Move start/end point up the tree if the leaves are sharp and if we are in different containers // Example * becomes !: !*texttext*
! // This will reduce the number of wrapper elements that needs to be created // Move start point up the tree if (format[0].inline || format[0].block_expand) { if (!format[0].inline || (startContainer.nodeType != 3 || startOffset === 0)) { startContainer = findParentContainer(true); } if (!format[0].inline || (endContainer.nodeType != 3 || endOffset === endContainer.nodeValue.length)) { endContainer = findParentContainer(); } } // Expand start/end container to matching selector if (format[0].selector && format[0].expand !== FALSE && !format[0].inline) { // Find new startContainer/endContainer if there is better one startContainer = findSelectorEndPoint(startContainer, 'previousSibling'); endContainer = findSelectorEndPoint(endContainer, 'nextSibling'); } // Expand start/end container to matching block element or text node if (format[0].block || format[0].selector) { // Find new startContainer/endContainer if there is better one startContainer = findBlockEndPoint(startContainer, 'previousSibling'); endContainer = findBlockEndPoint(endContainer, 'nextSibling'); // Non block element then try to expand up the leaf if (format[0].block) { if (!isBlock(startContainer)) { startContainer = findParentContainer(true); } if (!isBlock(endContainer)) { endContainer = findParentContainer(); } } } // Setup index for startContainer if (startContainer.nodeType == 1) { startOffset = nodeIndex(startContainer); startContainer = startContainer.parentNode; } // Setup index for endContainer if (endContainer.nodeType == 1) { endOffset = nodeIndex(endContainer) + 1; endContainer = endContainer.parentNode; } // Return new range like object return { startContainer: startContainer, startOffset: startOffset, endContainer: endContainer, endOffset: endOffset }; } function isColorFormatAndAnchor(node, format) { return format.links && node.tagName == 'A'; } /** * Removes the specified format for the specified node. It will also remove the node if it doesn't have * any attributes if the format specifies it to do so. * * @private * @param {Object} format Format object with items to remove from node. * @param {Object} vars Name/value object with variables to apply to format. * @param {Node} node Node to remove the format styles on. * @param {Node} compare_node Optional compare node, if specified the styles will be compared to that node. * @return {Boolean} True/false if the node was removed or not. */ function removeFormat(format, vars, node, compare_node) { var i, attrs, stylesModified; // Check if node matches format if (!matchName(node, format) && !isColorFormatAndAnchor(node, format)) { return FALSE; } // Should we compare with format attribs and styles if (format.remove != 'all') { // Remove styles each(format.styles, function(value, name) { value = normalizeStyleValue(replaceVars(value, vars), name); // Indexed array if (typeof name === 'number') { name = value; compare_node = 0; } if (format.remove_similar || (!compare_node || isEq(getStyle(compare_node, name), value))) { dom.setStyle(node, name, ''); } stylesModified = 1; }); // Remove style attribute if it's empty if (stylesModified && dom.getAttrib(node, 'style') === '') { node.removeAttribute('style'); node.removeAttribute('data-mce-style'); } // Remove attributes each(format.attributes, function(value, name) { var valueOut; value = replaceVars(value, vars); // Indexed array if (typeof name === 'number') { name = value; compare_node = 0; } if (!compare_node || isEq(dom.getAttrib(compare_node, name), value)) { // Keep internal classes if (name == 'class') { value = dom.getAttrib(node, name); if (value) { // Build new class value where everything is removed except the internal prefixed classes valueOut = ''; each(value.split(/\s+/), function(cls) { if (/mce\-\w+/.test(cls)) { valueOut += (valueOut ? ' ' : '') + cls; } }); // We got some internal classes left if (valueOut) { dom.setAttrib(node, name, valueOut); return; } } } // IE6 has a bug where the attribute doesn't get removed correctly if (name == "class") { node.removeAttribute('className'); } // Remove mce prefixed attributes if (MCE_ATTR_RE.test(name)) { node.removeAttribute('data-mce-' + name); } node.removeAttribute(name); } }); // Remove classes each(format.classes, function(value) { value = replaceVars(value, vars); if (!compare_node || dom.hasClass(compare_node, value)) { dom.removeClass(node, value); } }); // Check for non internal attributes attrs = dom.getAttribs(node); for (i = 0; i < attrs.length; i++) { var attrName = attrs[i].nodeName; if (attrName.indexOf('_') !== 0 && attrName.indexOf('data-') !== 0) { return FALSE; } } } // Remove the inline child if it's empty for example or if (format.remove != 'none') { removeNode(node, format); return TRUE; } } /** * Removes the node and wrap it's children in paragraphs before doing so or * appends BR elements to the beginning/end of the block element if forcedRootBlocks is disabled. * * If the div in the node below gets removed: * text|
formatNode.parentNode.replaceChild(caretContainer, formatNode); } else { // Insert caret container after the formatted node dom.insertAfter(caretContainer, formatNode); } // Move selection to text node selection.setCursorLocation(node, 1); // If the formatNode is empty, we can remove it safely. if (dom.isEmpty(formatNode)) { dom.remove(formatNode); } } } // 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 caretContainer; caretContainer = getParentCaretContainer(selection.getStart()); if (caretContainer && !dom.isEmpty(caretContainer)) { 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 (!ed._hasCaretEvents) { // Mark current caret container elements as bogus when getting the contents so we don't end up with empty elements markCaretContainersBogus = function() { var nodes = [], i; if (isCaretContainerEmpty(getParentCaretContainer(selection.getStart()), nodes)) { // Mark children i = nodes.length; while (i--) { dom.setAttrib(nodes[i], 'data-mce-bogus', '1'); } } }; disableCaretContainer = function(e) { var keyCode = e.keyCode; removeCaretContainer(); // Remove caret container if it's empty if (keyCode == 8 && selection.isCollapsed() && selection.getStart().innerHTML == INVISIBLE_CHAR) { removeCaretContainer(getParentCaretContainer(selection.getStart())); } // Remove caret container on keydown and it's left/right arrow keys if (keyCode == 37 || keyCode == 39) { removeCaretContainer(getParentCaretContainer(selection.getStart())); } unmarkBogusCaretParents(); }; // Remove bogus state if they got filled by contents using editor.selection.setContent ed.on('SetContent', function(e) { if (e.selection) { unmarkBogusCaretParents(); } }); ed._hasCaretEvents = true; } // Do apply or remove caret format if (type == "apply") { applyCaretFormat(); } else { removeCaretFormat(); } } /** * Moves the start to the first suitable text node. */ function moveStart(rng) { var container = rng.startContainer, offset = rng.startOffset, isAtEndOfText, walker, node, nodes, tmpNode; if (rng.startContainer == rng.endContainer) { if (isInlineBlock(rng.startContainer.childNodes[rng.startOffset])) { return; } } // Convert text node into index if possible if (container.nodeType == 3 && offset >= container.nodeValue.length) { // Get the parent container location and walk from there offset = nodeIndex(container); container = container.parentNode; isAtEndOfText = true; } // Move startContainer/startOffset in to a suitable node if (container.nodeType == 1) { nodes = container.childNodes; container = nodes[Math.min(offset, nodes.length - 1)]; walker = new TreeWalker(container, dom.getParent(container, dom.isBlock)); // If offset is at end of the parent node walk to the next one if (offset > nodes.length - 1 || isAtEndOfText) { walker.next(); } for (node = walker.current(); node; node = walker.next()) { if (node.nodeType == 3 && !isWhiteSpaceNode(node)) { // IE has a "neat" feature where it moves the start node into the closest element // we can avoid this by inserting an element before it and then remove it after we set the selection tmpNode = dom.create('a', {'data-mce-bogus': 'all'}, INVISIBLE_CHAR); node.parentNode.insertBefore(tmpNode, node); // Set selection and remove tmpNode rng.setStart(node, 0); selection.setRng(rng); dom.remove(tmpNode); return; } } } } }; });