diff options
Diffstat (limited to 'js/dojo/dojox/editor/plugins/NormalizeIndentOutdent.js')
| -rw-r--r-- | js/dojo/dojox/editor/plugins/NormalizeIndentOutdent.js | 882 |
1 files changed, 882 insertions, 0 deletions
diff --git a/js/dojo/dojox/editor/plugins/NormalizeIndentOutdent.js b/js/dojo/dojox/editor/plugins/NormalizeIndentOutdent.js new file mode 100644 index 0000000..733a84e --- /dev/null +++ b/js/dojo/dojox/editor/plugins/NormalizeIndentOutdent.js @@ -0,0 +1,882 @@ +//>>built +define("dojox/editor/plugins/NormalizeIndentOutdent", [ + "dojo", + "dijit", + "dojox", + "dijit/_editor/range", + "dijit/_editor/selection", + "dijit/_editor/_Plugin", + "dojo/_base/connect", + "dojo/_base/declare" +], function(dojo, dijit, dojox) { + +dojo.declare("dojox.editor.plugins.NormalizeIndentOutdent",dijit._editor._Plugin,{ + // summary: + // This plugin provides improved indent and outdent handling to + // the editor. It tries to generate valid HTML, as well as be + // consistent about how it indents and outdents lists and blocks/elements. + + // indentBy: [public] number + // The amount to indent by. Valid values are 1+. This is combined with + // the indentUnits parameter to determine how much to indent or outdent + // by for regular text. It does not affect lists. + indentBy: 40, + + // indentUnits: [public] String + // The units to apply to the indent amount. Usually 'px', but can also + // be em. + indentUnits: "px", + + setEditor: function(editor){ + // summary: + // Over-ride for the setting of the editor. + // editor: Object + // The editor to configure for this plugin to use. + this.editor = editor; + + // Register out indent handler via the builtin over-ride mechanism. + editor._indentImpl = dojo.hitch(this, this._indentImpl); + editor._outdentImpl = dojo.hitch(this, this._outdentImpl); + + // Take over the query command enabled function, we want to prevent + // indent of first items in a list, etc. + if(!editor._indentoutdent_queryCommandEnabled){ + editor._indentoutdent_queryCommandEnabled = editor.queryCommandEnabled; + } + editor.queryCommandEnabled = dojo.hitch(this, this._queryCommandEnabled); + + // We need the custom undo code since we manipulate the dom + // outside of the browser natives and only customUndo really handles + // that. It will incur a performance hit, but should hopefully be + // relatively small. + editor.customUndo = true; + }, + + _queryCommandEnabled: function(command){ + // summary: + // An over-ride for the editor's query command enabled, + // so that we can prevent indents, etc, on bad elements + // or positions (like first element in a list). + // command: + // The command passed in to check enablement. + // tags: + // private + var c = command.toLowerCase(); + var ed, sel, range, node, tag, prevNode; + var style = "marginLeft"; + if(!this._isLtr()){ + style = "marginRight"; + } + if(c === "indent"){ + ed = this.editor; + sel = dijit.range.getSelection(ed.window); + if(sel && sel.rangeCount > 0){ + range = sel.getRangeAt(0); + node = range.startContainer; + + // Check for li nodes first, we handle them a certain way. + while(node && node !== ed.document && node !== ed.editNode){ + tag = this._getTagName(node); + if(tag === "li"){ + + prevNode = node.previousSibling; + while(prevNode && prevNode.nodeType !== 1){ + prevNode = prevNode.previousSibling; + } + if(prevNode && this._getTagName(prevNode) === "li"){ + return true; + }else{ + // First item, disallow + return false; + } + }else if(this._isIndentableElement(tag)){ + return true; + } + node = node.parentNode; + } + if(this._isRootInline(range.startContainer)){ + return true; + } + } + }else if(c === "outdent"){ + ed = this.editor; + sel = dijit.range.getSelection(ed.window); + if(sel && sel.rangeCount > 0){ + range = sel.getRangeAt(0); + node = range.startContainer; + // Check for li nodes first, we handle them a certain way. + while(node && node !== ed.document && node !== ed.editNode){ + tag = this._getTagName(node); + if(tag === "li"){ + // Standard list, we can ask the browser. + return this.editor._indentoutdent_queryCommandEnabled(command); + }else if(this._isIndentableElement(tag)){ + // Block, we need to handle the indent check. + var cIndent = node.style?node.style[style]:""; + if(cIndent){ + cIndent = this._convertIndent(cIndent); + if(cIndent/this.indentBy >= 1){ + return true; + } + } + return false; + } + node = node.parentNode; + } + if(this._isRootInline(range.startContainer)){ + return false; + } + } + }else{ + return this.editor._indentoutdent_queryCommandEnabled(command); + } + return false; + }, + + _indentImpl: function(/*String*/ html) { + // summary: + // Improved implementation of indent, generates correct indent for + // ul/ol + var ed = this.editor; + + var sel = dijit.range.getSelection(ed.window); + if(sel && sel.rangeCount > 0){ + var range = sel.getRangeAt(0); + var node = range.startContainer; + var tag, start, end, div; + + + if(range.startContainer === range.endContainer){ + // No selection, just cursor point, we need to see if we're + // in an indentable block, or similar. + if(this._isRootInline(range.startContainer)){ + // Text at the 'root' of the document, + // we'll try to indent it and all inline selements around it + // as they are visually a single line. + + // First, we need to find the toplevel inline element that is rooted + // to the document 'editNode' + start = range.startContainer; + while(start && start.parentNode !== ed.editNode){ + start = start.parentNode; + } + + // Now we need to walk up its siblings and look for the first one in the rooting + // that isn't inline or text, as we want to grab all of that for indent. + while(start && start.previousSibling && ( + this._isTextElement(start) || + (start.nodeType === 1 && this._isInlineFormat(this._getTagName(start)) + ))){ + start = start.previousSibling; + } + if(start && start.nodeType === 1 && !this._isInlineFormat(this._getTagName(start))){ + // Adjust slightly, we're one node too far back in this case. + start = start.nextSibling; + } + + // Okay, we have a configured start, lets grab everything following it that's + // inline and make it an indentable block! + if(start){ + div = ed.document.createElement("div"); + dojo.place(div, start, "after"); + div.appendChild(start); + end = div.nextSibling; + while(end && ( + this._isTextElement(end) || + (end.nodeType === 1 && + this._isInlineFormat(this._getTagName(end))) + )){ + // Add it. + div.appendChild(end); + end = div.nextSibling; + } + this._indentElement(div); + dojo.withGlobal(ed.window, + "selectElementChildren", dijit._editor.selection, [div]); + dojo.withGlobal(ed.window, + "collapse", dijit._editor.selection, [true]); + } + }else{ + while(node && node !== ed.document && node !== ed.editNode){ + tag = this._getTagName(node); + if(tag === "li"){ + this._indentList(node); + return; + }else if(this._isIndentableElement(tag)){ + this._indentElement(node); + return; + } + node = node.parentNode; + } + } + }else{ + var curNode; + // multi-node select. We need to scan over them. + // Find the two containing nodes at start and end. + // then move the end one node past. Then ... lets see + // what we can indent! + start = range.startContainer; + end = range.endContainer; + // Find the non-text nodes. + + while(start && this._isTextElement(start) && start.parentNode !== ed.editNode){ + start = start.parentNode; + } + while(end && this._isTextElement(end) && end.parentNode !== ed.editNode){ + end = end.parentNode; + } + if(end === ed.editNode || end === ed.document.body){ + // Okay, selection end is somewhere after start, we need to find the last node + // that is safely in the range. + curNode = start; + while(curNode.nextSibling && + dojo.withGlobal(ed.window, "inSelection", dijit._editor.selection, [curNode])){ + curNode = curNode.nextSibling; + } + end = curNode; + if(end === ed.editNode || end === ed.document.body){ + // Unable to determine real selection end, so just make it + // a single node indent of start + all following inline styles, if + // present, then just exit. + tag = this._getTagName(start); + if(tag === "li"){ + this._indentList(start); + }else if(this._isIndentableElement(tag)){ + this._indentElement(start); + }else if(this._isTextElement(start) || + this._isInlineFormat(tag)){ + // inline element or textnode, So we want to indent it somehow + div = ed.document.createElement("div"); + dojo.place(div, start, "after"); + + // Find and move all inline tags following the one we inserted also into the + // div so we don't split up content funny. + var next = start; + while(next && ( + this._isTextElement(next) || + (next.nodeType === 1 && + this._isInlineFormat(this._getTagName(next))))){ + div.appendChild(next); + next = div.nextSibling; + } + this._indentElement(div); + } + return; + } + } + + // Has a definite end somewhere, so lets try to indent up to it. + // requires looking at the selections and in some cases, moving nodes + // into indentable blocks. + end = end.nextSibling; + curNode = start; + while(curNode && curNode !== end){ + if(curNode.nodeType === 1){ + tag = this._getTagName(curNode); + if(dojo.isIE){ + // IE sometimes inserts blank P tags, which we want to skip + // as they end up indented, which messes up layout. + if(tag === "p" && this._isEmpty(curNode)){ + curNode = curNode.nextSibling; + continue; + } + } + if(tag === "li"){ + if(div){ + if(this._isEmpty(div)){ + div.parentNode.removeChild(div); + }else{ + this._indentElement(div); + } + div = null; + } + this._indentList(curNode); + }else if(!this._isInlineFormat(tag) && this._isIndentableElement(tag)){ + if(div){ + if(this._isEmpty(div)){ + div.parentNode.removeChild(div); + }else{ + this._indentElement(div); + } + div = null; + } + curNode = this._indentElement(curNode); + }else if(this._isInlineFormat(tag)){ + // inline tag. + if(!div){ + div = ed.document.createElement("div"); + dojo.place(div, curNode, "after"); + div.appendChild(curNode); + curNode = div; + }else{ + div.appendChild(curNode); + curNode = div; + } + } + }else if(this._isTextElement(curNode)){ + if(!div){ + div = ed.document.createElement("div"); + dojo.place(div, curNode, "after"); + div.appendChild(curNode); + curNode = div; + }else{ + div.appendChild(curNode); + curNode = div; + } + } + curNode = curNode.nextSibling; + } + // Okay, indent everything we merged if we haven't yet.. + if(div){ + if(this._isEmpty(div)){ + div.parentNode.removeChild(div); + }else{ + this._indentElement(div); + } + div = null; + } + } + } + }, + + _indentElement: function(node){ + // summary: + // Function to indent a block type tag. + // node: + // The node who's content to indent. + // tags: + // private + var style = "marginLeft"; + if(!this._isLtr()){ + style = "marginRight"; + } + var tag = this._getTagName(node); + if(tag === "ul" || tag === "ol"){ + // Lists indent funny, so lets wrap them in a div + // and indent the div instead. + var div = this.editor.document.createElement("div"); + dojo.place(div, node, "after"); + div.appendChild(node); + node = div; + } + var cIndent = node.style?node.style[style]:""; + if(cIndent){ + cIndent = this._convertIndent(cIndent); + cIndent = (parseInt(cIndent, 10) + this.indentBy) + this.indentUnits; + }else{ + cIndent = this.indentBy + this.indentUnits; + } + dojo.style(node, style, cIndent); + return node; //Return the node that was indented. + }, + + _outdentElement: function(node){ + // summary: + // Function to outdent a block type tag. + // node: + // The node who's content to outdent. + // tags: + // private + var style = "marginLeft"; + if(!this._isLtr()){ + style = "marginRight"; + } + var cIndent = node.style?node.style[style]:""; + if(cIndent){ + cIndent = this._convertIndent(cIndent); + if(cIndent - this.indentBy > 0){ + cIndent = (parseInt(cIndent, 10) - this.indentBy) + this.indentUnits; + }else{ + cIndent = ""; + } + dojo.style(node, style, cIndent); + } + }, + + _outdentImpl: function(/*String*/ html) { + // summary: + // Improved implementation of outdent, generates correct indent for + // ul/ol and other elements. + // tags: + // private + var ed = this.editor; + var sel = dijit.range.getSelection(ed.window); + if(sel && sel.rangeCount > 0){ + var range = sel.getRangeAt(0); + var node = range.startContainer; + var tag; + + if(range.startContainer === range.endContainer){ + // Check for li nodes first, we handle them a certain way. + while(node && node !== ed.document && node !== ed.editNode){ + tag = this._getTagName(node); + if(tag === "li"){ + return this._outdentList(node); + }else if(this._isIndentableElement(tag)){ + return this._outdentElement(node); + } + node = node.parentNode; + } + ed.document.execCommand("outdent", false, html); + }else{ + // multi-node select. We need to scan over them. + // Find the two containing nodes at start and end. + // then move the end one node past. Then ... lets see + // what we can outdent! + var start = range.startContainer; + var end = range.endContainer; + // Find the non-text nodes. + while(start && start.nodeType === 3){ + start = start.parentNode; + } + while(end && end.nodeType === 3){ + end = end.parentNode; + } + end = end.nextSibling; + var curNode = start; + while(curNode && curNode !== end){ + if(curNode.nodeType === 1){ + tag = this._getTagName(curNode); + if(tag === "li"){ + this._outdentList(curNode); + }else if(this._isIndentableElement(tag)){ + this._outdentElement(curNode); + } + + } + curNode = curNode.nextSibling; + } + } + } + return null; + }, + + + _indentList: function(listItem){ + // summary: + // Internal function to handle indenting a list element. + // listItem: + // The list item to indent. + // tags: + // private + var ed = this.editor; + var newList, li; + var listContainer = listItem.parentNode; + var prevTag = listItem.previousSibling; + + // Ignore text, we want elements. + while(prevTag && prevTag.nodeType !== 1){ + prevTag = prevTag.previousSibling; + } + var type = null; + var tg = this._getTagName(listContainer); + + // Try to determine what kind of list item is here to indent. + if(tg === "ol"){ + type = "ol"; + }else if(tg === "ul"){ + type = "ul"; + } + + // Only indent list items actually in a list. + // Bail out if the list is malformed somehow. + if(type){ + // There is a previous node in the list, so we want to append a new list + // element after it that contains a new list of the content to indent it. + if(prevTag && prevTag.tagName.toLowerCase() == "li"){ + // Lets see if we can merge this into another (Eg, + // does the sibling li contain an embedded list already of + // the same type? if so, we move into that one. + var embList; + if(prevTag.childNodes){ + var i; + for(i = 0; i < prevTag.childNodes.length; i++){ + var n = prevTag.childNodes[i]; + if(n.nodeType === 3){ + if(dojo.trim(n.nodeValue)){ + if(embList){ + // Non-empty text after list, exit, can't embed. + break; + } + } + }else if(n.nodeType === 1 && !embList){ + // See if this is a list container. + if(type === n.tagName.toLowerCase()){ + embList = n; + } + }else{ + // Other node present, break, can't embed. + break; + } + } + } + if(embList){ + // We found a list to merge to, so merge. + embList.appendChild(listItem); + }else{ + // Nope, wasn't an embedded list container, + // So lets just create a new one. + newList = ed.document.createElement(type); + dojo.style(newList, { + paddingTop: "0px", + paddingBottom: "0px" + }); + li = ed.document.createElement("li"); + dojo.style(li, { + listStyleImage: "none", + listStyleType: "none" + }); + prevTag.appendChild(newList); + newList.appendChild(listItem); + } + + // Move cursor. + dojo.withGlobal(ed.window, + "selectElementChildren", dijit._editor.selection, [listItem]); + dojo.withGlobal(ed.window, + "collapse", dijit._editor.selection, [true]); + } + } + }, + + _outdentList: function(listItem){ + // summary: + // Internal function to handle outdenting a list element. + // listItem: + // The list item to outdent. + // tags: + // private + var ed = this.editor; + var list = listItem.parentNode; + var type = null; + var tg = list.tagName ? list.tagName.toLowerCase() : ""; + var li; + + // Try to determine what kind of list contains the item. + if(tg === "ol"){ + type = "ol"; + }else if(tg === "ul"){ + type = "ul"; + } + + // Check to see if it is a nested list, as outdenting is handled differently. + var listParent = list.parentNode; + var lpTg = this._getTagName(listParent); + + // We're in a list, so we need to outdent this specially. + // Check for welformed and malformed lists (<ul><ul></ul>U/ul> type stuff). + if(lpTg === "li" || lpTg === "ol" || lpTg === "ul"){ + if(lpTg === "ol" || lpTg === "ul"){ + // Okay, we need to fix this up, this is invalid html, + // So try to combine this into a previous element before + // de do a shuffle of the nodes, to build an HTML compliant + // list. + var prevListLi = list.previousSibling; + while(prevListLi && (prevListLi.nodeType !== 1 || + (prevListLi.nodeType === 1 && + this._getTagName(prevListLi) !== "li")) + ){ + prevListLi = prevListLi.previousSibling; + } + if(prevListLi){ + // Move this list up into the previous li + // to fix malformation. + prevListLi.appendChild(list); + listParent = prevListLi; + }else{ + li = listItem; + var firstItem = listItem; + while(li.previousSibling){ + li = li.previousSibling; + if(li.nodeType === 1 && this._getTagName(li) === "li"){ + firstItem = li; + } + } + + if(firstItem !== listItem){ + dojo.place(firstItem, list, "before"); + firstItem.appendChild(list); + listParent = firstItem; + }else{ + // No previous list item in a malformed list + // ... so create one and move into that. + li = ed.document.createElement("li"); + dojo.place(li, list, "before"); + li.appendChild(list); + listParent = li; + } + dojo.style(list, { + paddingTop: "0px", + paddingBottom: "0px" + }); + } + } + + // find the previous node, if any, + // non-text. + var prevLi = listItem.previousSibling; + while(prevLi && prevLi.nodeType !== 1){ + prevLi = prevLi.previousSibling; + } + var nextLi = listItem.nextSibling; + while(nextLi && nextLi.nodeType !== 1){ + nextLi = nextLi.nextSibling; + } + + if(!prevLi){ + // Top item in a nested list, so just move it out + // and then shuffle the remaining indented list into it. + dojo.place(listItem, listParent, "after"); + listItem.appendChild(list); + }else if(!nextLi){ + // Last item in a nested list, shuffle it out after + // the nsted list only. + dojo.place(listItem, listParent, "after"); + }else{ + // Item is in the middle of an embedded list, so we + // have to split it. + + // Move all the items following current list item into + // a list after it. + var newList = ed.document.createElement(type); + dojo.style(newList, { + paddingTop: "0px", + paddingBottom: "0px" + }); + listItem.appendChild(newList); + while(listItem.nextSibling){ + newList.appendChild(listItem.nextSibling); + } + + // Okay, now place the list item after the + // current list parent (li). + dojo.place(listItem, listParent, "after"); + } + + // Clean up any empty lists left behind. + if(list && this._isEmpty(list)){ + list.parentNode.removeChild(list); + } + if(listParent && this._isEmpty(listParent)){ + listParent.parentNode.removeChild(listParent); + } + + // Move our cursor to the list item we moved. + dojo.withGlobal(ed.window, + "selectElementChildren", dijit._editor.selection, [listItem]); + dojo.withGlobal(ed.window, + "collapse", dijit._editor.selection, [true]); + }else{ + // Not in a nested list, so we can just defer to the + // browser and hope it outdents right. + ed.document.execCommand("outdent", false, null); + } + }, + + _isEmpty: function(node){ + // summary: + // Internal function to determine if a node is 'empty' + // Eg, contains only blank text. Used to determine if + // an empty list element should be removed or not. + // node: + // The node to check. + // tags: + // private + if(node.childNodes){ + var empty = true; + var i; + for(i = 0; i < node.childNodes.length; i++){ + var n = node.childNodes[i]; + if(n.nodeType === 1){ + if(this._getTagName(n) === "p"){ + if(!dojo.trim(n.innerHTML)){ + continue; + } + } + empty = false; + break; + }else if(this._isTextElement(n)){ + // Check for empty text. + var nv = dojo.trim(n.nodeValue); + if(nv && nv !==" " && nv !== "\u00A0"){ + empty = false; + break; + } + }else{ + empty = false; + break; + } + } + return empty; + }else{ + return true; + } + }, + + _isIndentableElement: function(tag){ + // summary: + // Internal function to detect what element types + // are indent-controllable by us. + // tag: + // The tag to check + // tags: + // private + switch(tag){ + case "p": + case "div": + case "h1": + case "h2": + case "h3": + case "center": + case "table": + case "ul": + case "ol": + return true; + default: + return false; + } + }, + + _convertIndent: function(indent){ + // summary: + // Function to convert the current indent style to + // the units we're using by some heuristic. + // indent: + // The indent amount to convert. + // tags: + // private + var pxPerEm = 12; + indent = indent + ""; + indent = indent.toLowerCase(); + var curUnit = (indent.indexOf("px") > 0) ? "px" : (indent.indexOf("em") > 0) ? "em" : "px"; + indent = indent.replace(/(px;?|em;?)/gi, ""); + if(curUnit === "px"){ + if(this.indentUnits === "em"){ + indent = Math.ceil(indent/pxPerEm); + } + }else{ + if(this.indentUnits === "px"){ + indent = indent * pxPerEm; + } + } + return indent; + }, + + _isLtr: function(){ + // summary: + // Function to detect if the editor body is in RTL or LTR. + // tags: + // private + var editDoc = this.editor.document.body; + return dojo.withGlobal(this.editor.window, function(){ + var cs = dojo.getComputedStyle(editDoc); + return cs ? cs.direction == "ltr" : true; + }); + }, + + _isInlineFormat: function(tag){ + // summary: + // Function to determine if the current tag is an inline + // element that does formatting, as we don't want to + // break/indent around it, as it can screw up text. + // tag: + // The tag to examine + // tags: + // private + switch(tag){ + case "a": + case "b": + case "strong": + case "s": + case "strike": + case "i": + case "u": + case "em": + case "sup": + case "sub": + case "span": + case "font": + case "big": + case "cite": + case "q": + case "img": + case "small": + return true; + default: + return false; + } + }, + + _getTagName: function(node){ + // summary: + // Internal function to get the tag name of an element + // if any. + // node: + // The node to look at. + // tags: + // private + var tag = ""; + if(node && node.nodeType === 1){ + tag = node.tagName?node.tagName.toLowerCase():""; + } + return tag; + }, + + _isRootInline: function(node){ + // summary: + // This functions tests whether an indicated node is in root as inline + // or rooted inline elements in the page. + // node: + // The node to start at. + // tags: + // private + var ed = this.editor; + if(this._isTextElement(node) && node.parentNode === ed.editNode){ + return true; + }else if(node.nodeType === 1 && this._isInlineFormat(node) && node.parentNode === ed.editNode){ + return true; + }else if(this._isTextElement(node) && this._isInlineFormat(this._getTagName(node.parentNode))){ + node = node.parentNode; + while(node && node !== ed.editNode && this._isInlineFormat(this._getTagName(node))){ + node = node.parentNode; + } + if(node === ed.editNode){ + return true; + } + } + return false; + }, + + _isTextElement: function(node){ + // summary: + // Helper function to check for text nodes. + // node: + // The node to check. + // tags: + // private + if(node && node.nodeType === 3 || node.nodeType === 4){ + return true; + } + return false; + } +}); + +// Register this plugin. +dojo.subscribe(dijit._scopeName + ".Editor.getPlugin",null,function(o){ + if(o.plugin){ return; } + var name = o.args.name.toLowerCase(); + if(name === "normalizeindentoutdent"){ + o.plugin = new dojox.editor.plugins.NormalizeIndentOutdent({ + indentBy: ("indentBy" in o.args) ? + (o.args.indentBy > 0 ? o.args.indentBy : 40) : + 40, + indentUnits: ("indentUnits" in o.args) ? + (o.args.indentUnits.toLowerCase() == "em"? "em" : "px") : + "px" + }); + } +}); + +return dojox.editor.plugins.NormalizeIndentOutdent; + +}); |
