define([ "dojo", "dijit", "dojox", "dijit/_editor/_Plugin", "dojo/_base/declare" ], function(dojo, dijit, dojox, _Plugin) { var NormalizeIndentOutdent = dojo.declare("dojox.editor.plugins.NormalizeIndentOutdent", _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); ed._sCall("selectElementChildren", [div]); ed._sCall("collapse", [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 && ed._sCall("inSelection", [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. ed._sCall("selectElementChildren", [listItem]); ed._sCall("collapse", [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 (