dojox
Version:
Dojo eXtensions, a rollup of many useful sub-projects and varying states of maturity – from very stable and robust, to alpha and experimental. See individual projects contain README files for details.
1,253 lines (1,207 loc) • 87.8 kB
JavaScript
define([
"dojo/_base/declare",
"dojo/_base/array",
"dojo/aspect",
"dojo/_base/lang",
"dojo/dom-attr",
"dojo/dom-class",
"dojo/dom-construct",
"dojo/i18n",
"dojo/NodeList-dom",
"dojo/NodeList-traverse",
"dojo/dom-style",
"dojo/sniff",
"dojo/query",
"dijit",
"dojox",
"dijit/_editor/_Plugin",
"dijit/_editor/range",
"dijit/_editor/plugins/EnterKeyHandling",
"dijit/_editor/plugins/FontChoice",
"./NormalizeIndentOutdent",
"dijit/form/ToggleButton",
"dojo/i18n!./nls/BidiSupport"
], function(declare,array,aspect,lang,domAttr,domClass,domConstruct,i18n,listDom,listTraverse,domStyle,has,query,dijit,dojox,_Plugin,rangeapi,EnterKeyHandling,FontChoice,NormIndentOutdent,ToggleButton){
// module:
// rtebidi/BidiSupport
var BidiSupport = declare("dojox.editor.plugins.BidiSupport", _Plugin, {
// summary:
// This plugin provides some advanced BiDi support for
// rich text editing widget. It adds several bidi-specific commands,
// which are not released in native RTE's ('set text direction to left-to-right',
// 'set text direction to right-to-left', 'change text direction to opposite')
// and overrides some existing native commands.
// Override _Plugin.useDefaultCommand.
useDefaultCommand: false,
// Override _Plugin.buttonClass. Plugin uses two buttons, defined below.
buttonClass: null,
// iconClassPrefix: [const] String
// The CSS class name for the button node icon.
iconClassPrefix: "dijitAdditionalEditorIcon",
command: "bidiSupport",
// blockMode: [const] String
// This property decides the behavior of Enter key, actually released by EnterKeyHandling
// plugin. Possible values are 'P' and 'DIV'. Used when EnterKeyHandling isn't included
// into the list of the base plugins, loaded with the current Editor, as well as in case,
// if blockNodeForEnter property of EnterKeyHandling plugin isn't set to 'P' or 'DIV'.
// The default value is "DIV".
blockMode: "DIV",
// shortcutonly: [const] Boolean
// If this property is set to 'false', plugin handles all text direction commands
// and its behavior is controlled both by buttons and by shortcut (Ctrl+Shift+X).
// In opposite case only command 'change text direction to opposite', controlled by shortcut,
// is supported, and buttons don't appear in the toolbar.
// Defaults to false.
shortcutonly: false,
// bogusHtmlContent: [private] String
// HTML to stick into a new empty block
bogusHtmlContent: ' ',
// buttonLtr: [private] dijit/form/ToggleButton
// Used to set direction of the selected text to left-to-right.
buttonLtr: null,
// buttonRtl: [private] dijit/form/ToggleButton
// Used to set direction of the selected text to right-to-left.
buttonRtl: null,
_indentBy: 40,
_lineTextArray: ["DIV","P","LI","H1","H2","H3","H4","H5","H6","ADDRESS","PRE","DT","DE","TD"],
_lineStyledTextArray: ["H1","H2","H3","H4","H5","H6","ADDRESS","PRE","P"],
_tableContainers: ["TABLE","THEAD","TBODY","TR"],
_blockContainers: ["TABLE","OL","UL","BLOCKQUOTE"],
_initButton: function(){
// summary:
// Override _Plugin._initButton(). Creates two buttons, used for
// setting text direction to left-to-right and right-to-left.
if(this.shortcutonly){
return;
}
if(!this.buttonLtr){
this.buttonLtr = this._createButton("ltr");
}
if(!this.buttonRtl){
this.buttonRtl = this._createButton("rtl");
}
},
_createButton: function(direction){
// summary:
// Initialize specific button.
return ToggleButton(lang.mixin({
label: i18n.getLocalization("dojox.editor.plugins", "BidiSupport")[direction],
dir: this.editor.dir,
lang: this.editor.lang,
showLabel: false,
iconClass: this.iconClassPrefix+" "+this.iconClassPrefix + (direction == "ltr"? "ParaLeftToRight" : "ParaRightToLeft"),
onClick: lang.hitch(this, "_changeState", [direction])
}, this.params || {}));
},
setToolbar: function(/*dijit.Toolbar*/ toolbar){
// summary:
// Override _Plugin.setToolbar(). Adds buttons so, that 'ltr' button
// will appear from the left of 'rtl' button regardless of the editor's
// orientation.
if(this.shortcutonly){
return;
}
if(this.editor.isLeftToRight()){
toolbar.addChild(this.buttonLtr);
toolbar.addChild(this.buttonRtl);
}else{
toolbar.addChild(this.buttonRtl);
toolbar.addChild(this.buttonLtr);
}
},
updateState: function(){
// summary:
// Override _Plugin.updateState(). Determines direction of the text in the
// start point of the current selection. Changes state of the buttons
// correspondingly.
if(!this.editor || !this.editor.isLoaded || this.shortcutonly){
return;
}
this.buttonLtr.set("disabled", !!this.disabled);
this.buttonRtl.set("disabled", !!this.disabled);
if(this.disabled){
return;
}
var sel = rangeapi.getSelection(this.editor.window);
if(!sel || sel.rangeCount == 0){
return;
}
var range = sel.getRangeAt(0), node;
if(range.startContainer === this.editor.editNode && !range.startContainer.hasChildNodes()){
node = range.startContainer;
}else{
var startNode = range.startContainer,
startOffset = range.startOffset;
if(this._isBlockElement(startNode)){
while(startNode.hasChildNodes()){
if(startOffset == startNode.childNodes.length){
startOffset--;
}
startNode = startNode.childNodes[startOffset];
startOffset = 0;
}
}
node = this._getBlockAncestor(startNode);
}
var cDir = domStyle.get(node,"direction");
this.buttonLtr.set("checked", "ltr" == cDir);
this.buttonRtl.set("checked", "rtl" == cDir);
},
setEditor: function(/*dijit.Editor*/ editor){
// summary:
// Override _Plugin.setEditor().
// description:
// Sets editor's flag 'advancedBidi' to true, which may be used by other plugins
// as a switch to bidi-specific behaviour. Adds bidi-specific filters, including
// postDom filter, which provides explicit direction settings for the blocks
// of the text, direction of which isn't defined. Overrides some native commands,
// which should be changed or expanded in accordance with bidi-specific needs.
// Loads EnterKeyHandling plugin, if it was not loaded, and changes its
// blockNodeForEnter property, if it is needed. Defines shortcut, which will cause
// execution of 'change text direction to opposite' ('mirror') command.
this.editor = editor;
if(this.blockMode != "P" && this.blockMode != "DIV"){
this.blockMode = "DIV";
}
this._initButton();
var isLtr = this.editor.dir == "ltr";
// FF: RTL-oriented DIV's, containing new lines and/or tabs, can't be converted to lists (exception in native execCommand)
// Delete new lines and tabs from everywhere excluding contents of PRE elements.
this.editor.contentPreFilters.push(this._preFilterNewLines);
// Explicit direction setting
var postDomFilterSetDirExplicitly = lang.hitch(this, function(node){
if(this.disabled || !node.hasChildNodes()){
return node;
}
this._changeStateOfBlocks(this.editor.editNode, this.editor.editNode, this.editor.editNode, "explicitdir", null);
return this.editor.editNode;
});
this.editor.contentDomPostFilters.push(postDomFilterSetDirExplicitly);
// FF: Native change alignment command for more then one DIV doesn't change actually alignment, but damages
// markup so, that selected DIV's are converted into sequence of text elements, separated by <br>'s.
// Override native command
this.editor._justifyleftImpl = lang.hitch(this, function(){
this._changeState("left");
return true;
});
this.editor._justifyrightImpl = lang.hitch(this, function(){
this._changeState("right");
return true;
});
this.editor._justifycenterImpl = lang.hitch(this, function(){
this._changeState("center");
return true;
});
// FF: When blocks are converted into list items, their attributes are lost.
// IE: 1)Instead of converting regular block into list item, IE includes block into the newly created item,
// so attributes and styles don't appear in the item.
// 2)IE always converts list item into <P>.
// Expand native command
this.editor._insertorderedlistImpl = lang.hitch(this, "_insertLists", "insertorderedlist");
this.editor._insertunorderedlistImpl = lang.hitch(this, "_insertLists", "insertunorderedlist");
// IE: Direction of newly created blockquotes is set explicitly,
// which creates problems for further editing of the text.
// Expand native command
this.editor._indentImpl = lang.hitch(this, "_indentAndOutdent", "indent");
// FF: Outdent for the list items from the first level's list converts them into sequence of
// text elements, separated by <BR>s. Attributes of list items are lost.
// Expand native command
this.editor._outdentImpl = lang.hitch(this, "_indentAndOutdent", "outdent");
// FF: 1)Instead of converting <div> to some other block, FF includes newly created block into
// old <div>. Excessive nesting blocks creates problems for further working with the text.
// 2)Formatting contents of more then one list item (but less then all items in the list)
// caused unexpected merge of their contents.
// Expand native command
this.editor._formatblockImpl = lang.hitch(this, "_formatBlocks");
this.editor.onLoadDeferred.addCallback(lang.hitch(this, function(){
var edPlugins = this.editor._plugins,
i, p, ind = edPlugins.length, f = false,
h = lang.hitch(this, "_changeState", "mirror"),
hl = lang.hitch(this, "_changeState", "ltr"),
hr = lang.hitch(this, "_changeState", "rtl");
this.editor.addKeyHandler('9', 1, 0, h); //Ctrl-9
this.editor.addKeyHandler('8', 1, 0, hl); //Ctrl-8
this.editor.addKeyHandler('0', 1, 0, hr); //Ctrl-0
for(i = 0; i < edPlugins.length; i++){
p = edPlugins[i];
if (!p){
continue;
}
if(p.constructor === EnterKeyHandling){
p.destroy();
p = null;
ind = i;
}else if(p.constructor === NormIndentOutdent){
this.editor._normalizeIndentOutdent = true;
this.editor._indentImpl = lang.hitch(this, "_indentAndOutdent", "indent");
this.editor._outdentImpl = lang.hitch(this, "_indentAndOutdent", "outdent");
}else if(p.constructor === FontChoice && p.command === "formatBlock"){
this.own(aspect.before(p.button, "_execCommand", lang.hitch(this, "_handleNoFormat")));
}
}
this.editor.addPlugin({ctor: EnterKeyHandling, blockNodeForEnter: this.blockMode, blockNodes: /^(?:P|H1|H2|H3|H4|H5|H6|LI|DIV)$/}, ind);
p = this.editor._plugins[ind];
this.own(aspect.after(p, "handleEnterKey", lang.hitch(this, "_checkNewLine"), true));
}));
this.own(aspect.after(this.editor, "onNormalizedDisplayChanged", lang.hitch(this, "updateState"), true));
},
_checkNewLine: function(){
var range = rangeapi.getSelection(this.editor.window).getRangeAt(0);
var curBlock = rangeapi.getBlockAncestor(range.startContainer, null, this.editor.editNode).blockNode;
if (curBlock.innerHTML === this.bogusHtmlContent && curBlock.previousSibling){
curBlock.style.cssText = curBlock.previousSibling.style.cssText;
}else if(curBlock.innerHTML !== this.bogusHtmlContent && curBlock.previousSibling && curBlock.previousSibling.innerHTML === this.bogusHtmlContent){
curBlock.previousSibling.style.cssText = curBlock.style.cssText;
}
},
_handleNoFormat: function(editor, command, choice){
if (choice === "noFormat"){
return [editor, command, "DIV"];
}
return arguments;
},
_execNativeCmd: function(cmd, arg, info){
// summary:
// Call native command for nodes inside selection
// cmd:
// Name of the command (like "insertorderedlist" or "indent")
// arg:
// Arguments of the command
// info:
// Object containing
// nodes: array of all block nodes, which should be handled by this command
// groups: array containing groups of nodes. Nodes from each group should be handled by separate
// execution of current command
// cells: array of cells, contents of which should be handled by current command
// description:
// Sometimes native commands "insertorderedlist", "insertunorderedlist", "indent",
// "outdent" and "formatblocks" are the cause of various problems. These problems have
// different levels of severity - from crashing of their executing or damage of the editor's
// content to creating visually correct results, which however can cause the future problems,
// not always bidi-specific. For example, Webkit fails with insertlist commands, if selection
// contains some block element (like <H1> or <DIV>), followed by the table, and few cells of
// this table.IE fails,when try to handle insertlist command for list items, containing <PRE>.
// Mozilla and IE produce wrong results for selection,containing more then one cell. Mozilla
// merges contents of some listitems with "formatblock" command. Safari places DIV's in the
// same list with newly created list items. Webkit creates multilevel blocks to format the text.
// IE adds to the list lines of the text,which are outside the selection. Webkit put whole table
// into list item. All browsers lose direction and alignment styles with outdent command,
// executed for items from one-level list. Etc.
// We try to avoid these problems and produce correct (and more or less similar) results for
// all supported browsers. This is achieved by separating the each above-mentioned command into
// three parts.
// The first ("preparelist","prepareindent","prepareoutdent" and "prepareformat") is performed
// before corresponding native call. It prepares selected region of the editor's content by
// rebuilding it with strong block structure and keeps info about each group of selected
// elements. Separate groups are created for contents of each table cell, as well as for lines of
// text that are before, after and between tables. In case of webkit and insertlist or formatblocks
// commands, each selected block element is placed into separate "group".
// The second part of the command is native call itself. Native calls are executed separately
// for each group of nodes. This requires to reset a selection, each time limiting it to only
// the elements of the current group.
// The third part is perfomed after native call. Its role is to restore missing styles,
// to delete bogus elements, created in the previous steps, to merge sibling lists etc.
// Corresponding commands have the same names, as native (e.g., "insertorderedlist',
// "insertunorderedlist", "indent", "outdent" and "formatblocks"), because just they produce
// the final result. For IE and Mozilla we call one "post" command per native call. For webkit
// we call one "post" command per group of elements.
// If info contains only one group of elements, we, as usually, perform one native call per selection
if(this._isSimpleInfo(info)){
var result = this.editor.document.execCommand(cmd, false, arg);
// After some operations (like insert lists into cells of tables) webkit inserts BR before table
if(has("webkit")){
query("table",this.editor.editNode).prev().forEach(function(x,ind,arr){
if(this._hasTag(x,"BR")){
x.parentNode.removeChild(x);
}
},this);
}
return result;
}
var sel = rangeapi.getSelection(this.editor.window);
if(!sel || sel.rangeCount == 0){
return false;
}
var range = sel.getRangeAt(0), tempRange = range.cloneRange();
var startContainer = range.startContainer, startOffset = range.startOffset,
endContainer = range.endContainer, endOffset = range.endOffset;
for(var i = 0; i < info.groups.length; i++){
var group = info.groups[i];
// Set selection for currrent group of elements
var eOffs = group[group.length-1].childNodes.length;
tempRange.setStart(group[0],0);
tempRange.setEnd(group[group.length-1],eOffs);
sel.removeAllRanges();
sel.addRange(tempRange);
var table = this.editor.selection.getParentOfType(group[0],["TABLE"]);
// Execute native command for current group
var returnValue = this.editor.document.execCommand(cmd, false, arg);
if(has("webkit")){
if(table && this._hasTag(table.previousSibling, "BR")){
table.parentNode.removeChild(table.previousSibling);
}
this.editor.focus();
// Keep info about entire selection
sel = rangeapi.getSelection(this.editor.window);
var internalRange = sel.getRangeAt(0);
if(i == 0){
startContainer = internalRange.endContainer;
startOffset = internalRange.endOffset;
}else if(i == info.groups.length-1){
endContainer = internalRange.endContainer;
endOffset = internalRange.endOffset;
}
}
if(!returnValue){
break;
}
// For webkit we execute "post" command for each group of elements.
if(has("webkit")){
this._changeState(cmd);
}
}
// Restore selection
sel.removeAllRanges();
try{
tempRange.setStart(startContainer, startOffset);
tempRange.setEnd(endContainer, endOffset);
sel.addRange(tempRange);
}catch(e){
}
return true;
},
_insertLists: function(cmd){
// summary:
// Overrides native insertorderlist and insertunorderlist commands.
// cmd:
// Name of the command, one of "insertorderedlist" or "insertunorderedlist"
// description:
// Overrides native insertorderlist and insertunorderlist commands with the goal
// to avoid some bidi-specific problems that arise when these commands are executed.
// Performed in three steps:
// 1) prepares selected fragment of the document to execution of native command.
// 2) executes corresponding native command
// 3) updates contents of selected fragment of the document after execution of
// native comand.
var info = this._changeState("preparelists",cmd);
var returnValue = this._execNativeCmd(cmd, null, info);
if(!returnValue){
return false;
}
// For webkit "post" command executed separately for each group of elements.
if(!has("webkit") || this._isSimpleInfo(info)){
this._changeState(cmd);
}
this._cleanLists();
this._mergeLists();
return true;
},
_indentAndOutdent: function(cmd){
// summary:
// Overrides native indent and outdent commands.
// cmd:
// Name of the command, of of "indent" or "outdent"
// description:
// Overrides native indent and outdent commands with the goal to avoid some
// bidi-specific problems that arise when these commands are executed.
// Performed in three steps:
// 1) prepares selected fragment of the document to execution of native command
// (outdent only).
// 2) executes corresponding native command
// 3) updates contents of selected fragment of the document after execution of
// native comand.
if(this.editor._normalizeIndentOutdent){
// To emulate NormalizeIndentOutdent
this._changeState("normalize" + cmd);
return true;
}
var info = this._changeState("prepare" + cmd);
// If useCSS and styleWithCSS both set to false, mozilla creates blockquotes
if(has("mozilla")){
var oldValue;
try{
oldValue = this.editor.document.queryCommandValue("styleWithCSS");
}catch(e){
oldValue = false;
}
this.editor.document.execCommand("styleWithCSS", false, true);
}
var returnValue = this._execNativeCmd(cmd, null, info);
if(has("mozilla")){
this.editor.document.execCommand("styleWithCSS", false, oldValue);
}
if(!returnValue){
return false;
}
this._changeState(cmd);
this._mergeLists();
return true;
},
_formatBlocks: function(arg){
// summary:
// Overrides native formatblock command.
// arg:
// Tag name of block type, like H1, DIV or P.
// description:
// Overrides native formatblock command with the goal to avoid some
// bidi-specific problems that arise when this command is executed.
// Performed in three steps:
// 1) prepares selected fragment of the document to execution of native command
// (Mozilla only).
// 2) executes native command
// 3) updates contents of selected fragment of the document after execution of
// native comand.
var info;
if(has("mozilla") || has("webkit")){
info = this._changeState("prepareformat", arg);
}
if(has("ie") && arg && arg.charAt(0) != "<"){
arg = "<" + arg + ">";
}
var returnValue = this._execNativeCmd("formatblock", arg, info);
if(!returnValue){
return false;
}
if(!has("webkit") || this._isSimpleInfo(info)){
this._changeState("formatblock", arg);
}
this._mergeLists();
return true;
},
_changeState: function(cmd,arg){
// summary:
// Determines and refines current selection and calls method
// _changeStateOfBlocks(), where given action is actually done
// description:
// The main goal of this method is correctly identify the block elements,
// that are at the beginning and end of the current selection.
// return: nodesInfo
// Object containing
// nodes: array of all block nodes, which should be handled by this command
// groups: array containing groups of nodes. Nodes from each group should be handled by separate
// execution of current command
// cells: array of cells, contents of which should be handled by current command
if(!this.editor.window){
return;
}
this.editor.focus();
var sel = rangeapi.getSelection(this.editor.window);
if(!sel || sel.rangeCount == 0){
return;
}
var range = sel.getRangeAt(0), tempRange = range.cloneRange(),
startNode, endNode, startOffset, endOffset;
startNode = range.startContainer;
startOffset = range.startOffset;
endNode = range.endContainer;
endOffset = range.endOffset;
var isCollapsed = startNode === endNode && startOffset == endOffset;
if(this._isBlockElement(startNode) || this._hasTagFrom(startNode,this._tableContainers)){
while(startNode.hasChildNodes()){
if(startOffset == startNode.childNodes.length){
startOffset--;
}
startNode = startNode.childNodes[startOffset];
startOffset = 0;
}
}
tempRange.setStart(startNode, startOffset);
startNode = this._getClosestBlock(startNode,"start",tempRange);
var supList = rangeapi.getBlockAncestor(startNode, /li/i, this.editor.editNode).blockNode;
if(supList && supList !== startNode){
startNode = supList;
}
endNode = tempRange.endContainer;
endOffset = tempRange.endOffset;
if(this._isBlockElement(endNode) || this._hasTagFrom(endNode,this._tableContainers)){
while(endNode.hasChildNodes()){
if(endOffset == endNode.childNodes.length){
endOffset--;
}
endNode = endNode.childNodes[endOffset];
if(endNode.hasChildNodes()){
endOffset = endNode.childNodes.length;
}else if(endNode.nodeType == 3 && endNode.nodeValue){
endOffset = endNode.nodeValue.length;
}else{
endOffset = 0;
}
}
}
tempRange.setEnd(endNode, endOffset);
endNode = this._getClosestBlock(endNode,"end",tempRange);
supList = rangeapi.getBlockAncestor(endNode, /li/i, this.editor.editNode).blockNode;
if(supList && supList !== endNode){
endNode = supList;
}
sel = rangeapi.getSelection(this.editor.window, true);
sel.removeAllRanges();
sel.addRange(tempRange);
var commonAncestor = rangeapi.getCommonAncestor(startNode, endNode);
var nodesInfo = this._changeStateOfBlocks(startNode, endNode, commonAncestor, cmd, arg, tempRange);
if(isCollapsed){
endNode = tempRange.startContainer;
endOffset = tempRange.startOffset;
tempRange.setEnd(endNode, endOffset);
sel = rangeapi.getSelection(this.editor.window, true);
sel.removeAllRanges();
sel.addRange(tempRange);
}
return nodesInfo;
},
_isBlockElement: function(node){
if(!node || node.nodeType != 1){
return false;
}
var display = domStyle.get(node,"display");
return (display == 'block' || display == "list-item" || display == "table-cell");
},
_isInlineOrTextElement: function(node){
return !this._isBlockElement(node) && (node.nodeType == 1 || node.nodeType == 3 || node.nodeType == 8);
},
_isElement: function(node){
return node && (node.nodeType == 1 || node.nodeType == 3);
},
_isBlockWithText: function(node){
return node !== this.editor.editNode && this._hasTagFrom(node,this._lineTextArray);
},
_getBlockAncestor: function(node){
while(node.parentNode && !this._isBlockElement(node)){
node = node.parentNode;
}
return node;
},
_getClosestBlock: function(node, point, tempRange){
// summary:
// Searches for a closest block element containing the text which
// is at a given point of current selection. Refines current
// selection, if text element from start or end point was merged
// with its neighbors.
if(this._isBlockElement(node)){
return node;
}
var parent = node.parentNode,
firstSibling, lastSibling,
createOwnBlock = false,
multiText = false;
removeOffset = false;
while(true){
var sibling = node;
createOwnBlock = false;
while(true){
if(this._isInlineOrTextElement(sibling)){
firstSibling = sibling;
if(!lastSibling){
lastSibling = sibling;
}
}
sibling = sibling.previousSibling;
if(!sibling){
break;
}else if(this._isBlockElement(sibling) || this._hasTagFrom(sibling,this._blockContainers) || this._hasTag(sibling,"BR")){
createOwnBlock = true;
break;
}else if(sibling.nodeType == 3 && sibling.nextSibling.nodeType == 3){
// Merge neighboring text elements
sibling.nextSibling.nodeValue = sibling.nodeValue + sibling.nextSibling.nodeValue;
multiText = true;
if(point == "start" && sibling === tempRange.startContainer){
tempRange.setStart(sibling.nextSibling, 0);
}else if(point == "end" && (sibling === tempRange.endContainer || sibling.nextSibling === tempRange.endContainer)){
tempRange.setEnd(sibling.nextSibling, sibling.nextSibling.nodeValue.length);
}
sibling = sibling.nextSibling;
sibling.parentNode.removeChild(sibling.previousSibling);
if(!sibling.previousSibling){
break;
}
}
}
sibling = node;
while(true){
if(this._isInlineOrTextElement(sibling)){
if(!firstSibling){
firstSibling = sibling;
}
lastSibling = sibling;
}
sibling = sibling.nextSibling;
if(!sibling){
break;
}else if(this._isBlockElement(sibling) || this._hasTagFrom(sibling,this._blockContainers)){
createOwnBlock = true;
break;
}else if(this._hasTag(sibling,"BR") && sibling.nextSibling && !(this._isBlockElement(sibling.nextSibling) ||
this._hasTagFrom(sibling.nextSibling,this._blockContainers))){
lastSibling = sibling;
createOwnBlock = true;
break;
}else if(sibling.nodeType == 3 && sibling.previousSibling.nodeType == 3){
// Merge neighboring text elements
sibling.previousSibling.nodeValue += sibling.nodeValue;
multiText = true;
if(point == "start" && sibling === tempRange.startContainer){
tempRange.setStart(sibling.previousSibling, 0);
}else if(point == "end" && (sibling === tempRange.endContainer || sibling.previousSibling === tempRange.endContainer)){
tempRange.setEnd(sibling.previousSibling, sibling.previousSibling.nodeValue.length);
}
sibling = sibling.previousSibling;
sibling.parentNode.removeChild(sibling.nextSibling);
if(!sibling.nextSibling){
break;
}
}
}
// If text in the start or end point of the current selection doesn't placed in some block element
// or if it has block siblings, new block, containing this text element (and its inline siblings) is created.
if(createOwnBlock || (this._isBlockElement(parent) &&
!this._isBlockWithText(parent) && firstSibling)){
var origStartOffset = tempRange? tempRange.startOffset : 0,
origEndOffset = tempRange? tempRange.endOffset : 0,
origStartContainer = tempRange? tempRange.startContainer : null,
origEndContainer = tempRange? tempRange.endContainer : null,
divs = this._repackInlineElements(firstSibling, lastSibling, parent),
div = divs[point == "start"? 0 : divs.length-1];
if(tempRange && div && firstSibling === origStartContainer && this._hasTag(firstSibling,"BR")){
origStartContainer = div;
origStartOffset = 0;
if(lastSibling === firstSibling){
origEndContainer = origStartContainer;
origEndOffset = 0;
}
}
if(tempRange){
tempRange.setStart(origStartContainer, origStartOffset);
tempRange.setEnd(origEndContainer, origEndOffset);
}
return div;
}
if(this._isBlockElement(parent)){
return parent;
}
node = parent;
removeOffset = true;
parent = parent.parentNode;
firstSibling = lastSibling = null;
}
},
_changeStateOfBlocks: function(startNode, endNode, commonAncestor, cmd, arg, tempRange){
// summary:
// Collects all block elements, containing text, which are inside of current selection,
// and performs for each of them given action.
// Possible commands and corresponding actions:
// - "ltr": change direction to left-to-right
// - "rtl": change direction to right-to-left
// - "mirror": change direction to opposite
// - "explicitdir": explicit direction setting
// - "left": change alignment to left
// - "right": change alignment to right
// - "center": change alignment to center
// - "preparelists": action should be done before executing native insert[un]orderedlist
// - "prepareoutdent": action should be done before executing native outdent
// - "prepareindent": action should be done before executing native indent
// - "prepareformat": action should be done before executing native formatblock
// - "insertunorderedlist": action should be done after executing native insertunorderedlist
// - "insertorderedlist": action should be done after executing native insertorderedlist
// - "indent": action should be done after executing native indent
// - "outdent": action should be done after executing native outdent
// - "normalizeindent": emulate indent done by NormalizeIndentOutdent plugin
// - "normalizeoutdent": emulate outdent done by NormalizeIndentOutdent plugin
// - "formatblock": action should be done after executing native formatblock
var nodes = [];
// Refine selection, needed for 'explicitdir' command (full selection)
if(startNode === this.editor.editNode){
if(!startNode.hasChildNodes()){
return;
}
if(this._isInlineOrTextElement(startNode.firstChild)){
this._rebuildBlock(startNode);
}
startNode = this._getClosestBlock(startNode.firstChild, "start", null);
}
if(endNode === this.editor.editNode){
if(!endNode.hasChildNodes()){
return;
}
if(this._isInlineOrTextElement(endNode.lastChild)){
this._rebuildBlock(endNode);
}
endNode = this._getClosestBlock(endNode.lastChild, "end", null);
}
// Collect all selected block elements, which contain or can contain text.
// Walk through DOM tree between start and end points of current selection.
var origStartOffset = tempRange? tempRange.startOffset : 0,
origEndOffset = tempRange? tempRange.endOffset : 0,
origStartContainer = tempRange? tempRange.startContainer : null,
origEndContainer = tempRange? tempRange.endContainer : null;
var info = this._collectNodes(startNode, endNode, commonAncestor, tempRange, nodes,
origStartContainer, origStartOffset, origEndContainer, origEndOffset, cmd);
var nodesInfo = {nodes: nodes, groups: info.groups, cells: info.cells};
cmd = cmd.toString();
// Execution of specific action for each element from collection
switch(cmd){
//change direction
case "mirror":
case "ltr":
case "rtl":
//change alignment
case "left":
case "right":
case "center":
//explicit direction setting
case "explicitdir":
this._execDirAndAlignment(nodesInfo, cmd, arg);
break;
//before executing 'insert list' native command
case "preparelists":
this._prepareLists(nodesInfo, arg);
break;
//after executing 'insert list' native command
case "insertorderedlist":
case "insertunorderedlist":
this._execInsertLists(nodesInfo);
break;
//before executing 'outdent' native command
case "prepareoutdent":
this._prepareOutdent(nodesInfo);
break;
//before executing 'indent' native command
case "prepareindent":
this._prepareIndent(nodesInfo);
break;
//after executing 'indent' native command
case "indent":
this._execIndent(nodesInfo);
break;
//after executing 'outdent' native command
case "outdent":
this._execOutdent(nodesInfo);
break;
//replace native 'indent' and 'outdent' commands
case "normalizeindent":
this._execNormalizedIndent(nodesInfo);
break;
case "normalizeoutdent":
this._execNormalizedOutdent(nodesInfo);
break;
//before 'formatblock' native command
case "prepareformat":
this._prepareFormat(nodesInfo, arg);
break;
//after executing 'formatblock' native command
case "formatblock":
this._execFormatBlocks(nodesInfo, arg);
break;
default: console.error("Command " + cmd + " isn't handled");
}
// Refine selection after changes
if(tempRange){
tempRange.setStart(origStartContainer, origStartOffset);
tempRange.setEnd(origEndContainer, origEndOffset);
sel = rangeapi.getSelection(this.editor.window, true);
sel.removeAllRanges();
sel.addRange(tempRange);
this.editor.onDisplayChanged();
}
return nodesInfo;
},
_collectNodes: function(startNode, endNode, commonAncestor, tempRange, nodes, origStartContainer, origStartOffset, origEndContainer, origEndOffset, cmd){
// summary:
// Collect all selected block elements, which contain or can contain text.
// Walk through DOM tree between start and end points of current selection.
var node = startNode, sibling, child, parent = node.parentNode, divs = [],
firstSibling, lastSibling, groups = [], group = [], cells = [], curTD = this.editor.editNode;
var saveNodesAndGroups = lang.hitch(this, function(x){
nodes.push(x);
var cell = this.editor.selection.getParentOfType(x,["TD"]);
if(curTD !== cell || has("webkit") && (cmd === "prepareformat" || cmd === "preparelists")){
if(group.length){
groups.push(group);
}
group = [];
if(curTD != cell){
curTD = cell;
if(curTD){
cells.push(curTD);
}
}
}
group.push(x);
});
this._rebuildBlock(parent);
while(true){
if(this._hasTagFrom(node,this._tableContainers)){
if(node.firstChild){
parent = node;
node = node.firstChild;
continue;
}
}else if(this._isBlockElement(node)){
var supLI = rangeapi.getBlockAncestor(node, /li/i, this.editor.editNode).blockNode;
if(supLI && supLI !== node){
node = supLI;
parent = node.parentNode;
continue;
}
if(!this._hasTag(node,"LI")){
if(node.firstChild){
this._rebuildBlock(node);
if(this._isBlockElement(node.firstChild) || this._hasTagFrom(node.firstChild,this._tableContainers)){
parent = node;
node = node.firstChild;
continue;
}
}
}
if(this._hasTagFrom(node,this._lineTextArray)){
saveNodesAndGroups(node);
}
}else if(this._isInlineOrTextElement(node) && !this._hasTagFrom(node.parentNode,this._tableContainers)){
firstSibling = node;
while(node){
var nextSibling = node.nextSibling;
if(this._isInlineOrTextElement(node)){
lastSibling = node;
if(this._hasTag(node,"BR")){
if(!(this._isBlockElement(parent) && node === parent.lastChild)){
divs = this._repackInlineElements(firstSibling, lastSibling, parent);
node = divs[divs.length-1];
for(var nd = 0; nd < divs.length; nd++){
saveNodesAndGroups(divs[nd]);
}
firstSibling = lastSibling = null;
if(nextSibling && this._isInlineOrTextElement(nextSibling)){
firstSibling = nextSibling;
}
}
}
}else if(this._isBlockElement(node)){
break;
}
node = nextSibling;
}
if(!firstSibling){
continue;
}
divs = this._repackInlineElements(firstSibling, lastSibling, parent);
node = divs[divs.length-1];
for(var nd = 0; nd < divs.length; nd++){
saveNodesAndGroups(divs[nd]);
}
}
if(node === endNode){
break;
}
if(node.nextSibling){
node = node.nextSibling;
}else if(parent !== commonAncestor){
while(!parent.nextSibling){
node = parent;
parent = node.parentNode;
if(parent === commonAncestor){
break;
}
}
if(parent !== commonAncestor && parent.nextSibling){
node = parent.nextSibling;
parent = parent.parentNode;
}else{
break;
}
}else{
break;
}
}
if(group.length){
if(has("webkit") || curTD){
groups.push(group);
}else{
groups.unshift(group);
}
}
return {groups: groups, cells: cells};
},
_execDirAndAlignment: function(nodesInfo, cmd,arg){
// summary:
// Change direction and/or alignment of each node from the given array.
switch(cmd){
//change direction
case "mirror":
case "ltr":
case "rtl":
array.forEach(nodesInfo.nodes, function(x){
var style = domStyle.getComputedStyle(x),
curDir = style.direction,
oppositeDir = curDir == "ltr"? "rtl" : "ltr",
realDir = (cmd != "mirror"? cmd : oppositeDir),
curAlign = style.textAlign,
marginLeft = isNaN(parseInt(style.marginLeft))? 0 : parseInt(style.marginLeft),
marginRight = isNaN(parseInt(style.marginRight))? 0 : parseInt(style.marginRight);
domAttr.remove(x,"dir");
domAttr.remove(x,"align");
domStyle.set(x, {direction: realDir, textAlign: ""});
if(this._hasTag(x,"CENTER")){
return;
}
if(curAlign.indexOf("center") >= 0){
domStyle.set(x,"textAlign","center");
}
if(this._hasTag(x,"LI")){
this._refineLIMargins(x);
var margin = curDir === "rtl"? marginRight : marginLeft;
var level = 0, tNode = x.parentNode, name;
if(curDir != domStyle.get(tNode,"direction")){
while(tNode !== this.editor.editNode){
if(this._hasTagFrom(tNode,["OL","UL"])){
level++;
}
tNode = tNode.parentNode;
}
margin -= this._getMargins(level);
}
var styleMargin = realDir == "rtl"? "marginRight" : "marginLeft";
var cMargin = domStyle.get(x,styleMargin);
var cIndent = isNaN(cMargin)? 0 : parseInt(cMargin);
domStyle.set(x,styleMargin,"" + (cIndent + margin) + "px");
if(has("webkit")){
if(curAlign.indexOf("center") < 0){
domStyle.set(x, "textAlign", (realDir == "rtl"? "right" : "left"));
}
}else if(x.firstChild && x.firstChild.tagName){
if(this._hasTagFrom(x.firstChild,this._lineStyledTextArray)){
var style = domStyle.getComputedStyle(x),
align = this._refineAlignment(style.direction, style.textAlign);
if(has("mozilla")){
domStyle.set(x.firstChild, {textAlign: align});
}else{
domStyle.set(x.firstChild, {direction : realDir, textAlign: align});
}
}
}
}else{
if(realDir == "rtl" && marginLeft != 0){
domStyle.set(x, {marginLeft: "", marginRight: "" + marginLeft + "px"});
}else if(realDir == "ltr" && marginRight != 0){
domStyle.set(x, {marginRight: "", marginLeft: "" + marginRight + "px"});
}
}
},this);
query("table",this.editor.editNode).forEach(function(table,idx,array){
var dir = cmd;
if(cmd === "mirror"){
dir = domStyle.get(table,"direction") === "ltr"? "rtl" : "ltr";
}
var listTD = query("td",table), first = false, last = false;
for(var i = 0; i < nodesInfo.cells.length; i++){
if(!first && listTD[0] === nodesInfo.cells[i]){
first = true;
}else if(listTD[listTD.length-1] === nodesInfo.cells[i]){
last = true;
break;
}
}
if(first && last){
domStyle.set(table,"direction",dir);
for(i = 0; i < listTD.length; i++){
domStyle.set(listTD[i],"direction",dir);
}
}
},this);
break;
//change alignment
case "left":
case "right":
case "center":
array.forEach(nodesInfo.nodes, function(x){
if(this._hasTag(x,"CENTER")){
return;
}
domAttr.remove(x,"align");
domStyle.set(x,"textAlign",cmd);
if(this._hasTag(x,"LI")){
if(x.firstChild && x.firstChild.tagName){
if(this._hasTagFrom(x.firstChild,this._lineStyledTextArray)){
var style = domStyle.getComputedStyle(x),
align = this._refineAlignment(style.direction, style.textAlign);
domStyle.set(x.firstChild, "textAlign", align);
}
}
}
},this);
break;
//explicit direction setting
case "explicitdir":
array.forEach(nodesInfo.nodes, function(x){
var style = domStyle.getComputedStyle(x),
curDir = style.direction;
domAttr.remove(x,"dir");
domStyle.set(x, {direction: curDir});
},this);
break;
}
},
_prepareLists: function(nodesInfo, arg){
// summary:
// Perform changes before native insertorderedlist and
// insertunorderedlist commands for each node from the given array.
array.forEach(nodesInfo.nodes, function(x,index,arr){
if(has("mozilla") || has("webkit")){
//Mozilla not always handles the only block inside cell
if(has("mozilla")){
var cell = this._getParentFrom(x,["TD"]);
if(cell && query("div[tempRole]",cell).length == 0){
domConstruct.create("div",{innerHTML: "<span tempRole='true'>" + this.bogusHtmlContent + "</span", tempRole: "true"},cell);
}
}
var name = this._tag(x);
var styledSpan;
if(has("webkit") && this._hasTagFrom(x,this._lineStyledTextArray) ||
(this._hasTag(x,"LI") && this._hasStyledTextLineTag(x.firstChild))){
var savedName = this._hasTag(x,"LI")? this._tag(x.firstChild) : name;
if(this._hasTag(x,"LI")){
while(x.firstChild.lastChild){
domConstruct.place(x.firstChild.lastChild,x.firstChild,"after");
}
x.removeChild(x.firstChild);
}
styledSpan = domConstruct.create("span",{innerHTML: this.bogusHtmlContent, bogusFormat: savedName},x,"first");
}
if(!has("webkit") && name != "DIV" && name != "P" && name != "LI"){
return;
}
// In some cases, when one of insertlists command is executed for list of another type, and selection
// includes the last item of this list, webkit loses current selection after native call.
// To avoid this we append the list with bogus item, which finally will be removed by "post" action.
if(has("webkit") && this._isListTypeChanged(x, arg) && x === x.parentNode.lastChild){
domConstruct.create("li",{tempRole: "true"},x,"after");
}
if(name == "LI" && x.firstChild && x.firstChild.tagName){
if(this._hasTagFrom(x.firstChild,this._lineStyledTextArray)){
return;
}
}
// When blocks are converted into list items, their attributes are lost.
// We save attributes in some bogus inline element with the goal to restore
// them after execution the command.
var style = domStyle.getComputedStyle(x),curDir = style.direction,curAlign = style.textAlign;
curAlign = this._refineAlignment(curDir, curAlign);
var val = this._getLIIndent(x);
var margin = val == 0? "" : "" + val + "px";
if(has("webkit") && name == "LI"){
domStyle.set(x,"textAlign","");
}
var span = styledSpan? x.firstChild : domConstruct.create("span",{innerHTML: this.bogusHtmlContent},x,"first");
domAttr.set(span,"bogusDir",curDir);
if(curAlign != ""){
domAttr.set(span,"bogusAlign",curAlign);
}
if(margin){
domAttr.set(span,"bogusMargin",margin);
}
}else if(has("ie")){
// When IE executes insertlist command for list item, containing block,
// it saves styles of list item in this block. We should then remove
// bogus margins, if list item is converted into the block.
if(this._hasTag(x,"LI")){
var dir = domStyle.getComputedStyle(x).direction;
domStyle.set(x,"marginRight","");
domStyle.set(x,"marginLeft","");
if(this._getLILevel(x) == 1 && !this._isListTypeChanged(x,cmd)){
if(x.firstChild && this._hasTagFrom(x.firstChild,["P","PRE"])){
domConstruct.create("span",{bogusIEFormat: this._tag(x.firstChild)},x.firstChild,"first");
}
//IE fais, when try to handle list items, containing PRE
if(this._hasTag(x.firstChild,"PRE")){
var p = domConstruct.create("p",null,x.firstChild,"after");
while(x.firstChild.firstChild){
domConstruct.place(x.firstChild.firstChild,p,"last");
}
p.style.cssText = x.style.cssText;
x.removeChild(x.firstChild);
}
}
}
}
},this);
// If selection includes some cells of table and some items of one-level list, which is the table's next sibling,
// webkit doesn't complete the action. For example, having <table>...</table><ul><li>One</li><li>Two</li></ul>,
// after executing "insertunorderedlist" command, we get <table>...</table><ul><li>One</li>Two.
// To avoid this, we add after the table some bogus list, which will be deleted after executing "post native" action.
if(has("webkit")){
query("table",this.editor.editNode).forEach(function(x,ind,arr){
var sibling = x.nextSibling;
if(sibling && this._hasTagFrom(sibling,["UL","OL"])){
domConstruct.create("UL",{tempRole: "true"},x,"after");
}
},this);
}
},
_execInsertLists: function(nodesInfo){
array.forEach(nodesInfo.nodes, function(x,index){
if(this._hasTag(x,"LI")){
//If one of "styled" text blocks, like <h*> or <pre>, is converted
//into list item, it actually is included into new list item,
//created without attributes. This causes problems in subsequent changes
//of orientation or alignment of this list item.
if(x.firstChild && x.firstChild.tagName){
if(this._hasTagFrom(x.firstChild,this._lineStyledTextArray)){
var style = domStyle.getComputedStyle(x.firstChild),
align = this._refineAlignment(style.direction, style.textAlign);
domStyle.set(x,{direction: style.direction, textAlign: align});
var mLeft = this._getIntStyleValue(x,"marginLeft") + this._getIntStyleValue(x.firstChild,"marginLeft");
var mRight = this._getIntStyleValue(x,"marginRight") + this._getIntStyleValue(x.firstChild,"marginRight");
var leftMargin = mLeft? "" + mLeft + "px" : "";
var rightMargin = mRight? "" + mRight + "px" : "";
domStyle.set(x,{marginLeft: leftMargin, marginRight: rightMargin});
domStyle.set(x.firstChild,{direction: "", textAlign: ""});
if(!has("mozilla")){
domStyle.set(x.firstChild,{marginLeft: "", marginRight: ""});
}
}
}
//Mozilla sometimes includes few empty text nodes (or text nodes, containing spaces)
//to the end of newly created list item
while(x.childNodes.length > 1){
if(!(x.lastChild.nodeType == 3 && x.lastChild.previousSibling && x.lastChild.previousSibling.nodeType == 3 && lang.trim(x.lastChild.nodeValue) == "")){
break;
}
x.removeChild(x.lastChild);
}
if(has("safari")){
if(this._hasTag(x.firstChild,"SPAN") && domClass.contains(x.firstChild,"Apple-style-span")){
var child = x.firstChild;
if(this._hasTag(child.firstChild,"SPAN") && domAttr.has(child.firstChild,"bogusFormat")){
while(child.lastChild){
domConstruct.place(child.lastChild,child,"after");
}
x.removeChild(child);
}
}
}
}else if(this._hasTag(x,"DIV") && x.childNodes.length == 0){
x.parentNode.removeChild(x);
return;
}
if(has("ie")){
// IE always converts list items to <P>
if(this._hasTag(x,"P") && this.blockMode.toUpperCase() == "DIV"){
if(this._hasTag(x.firstChild,"SPAN") && domAttr.has(x.firstChild,"bogusIEFormat")){
if(domAttr.get(x.firstChild,"bogusIEFormat").toUpperCase() === "PRE"){
var pre = domConstruct.create("pre",{innerHTML: x.innerHTML},x,"before");
pre.style.cssText = x.style.cssText;
pre.removeChild(pre.firstChild);
x.parentNode.removeChild(x);
}else{
x.removeChild(x.firstChild);
}
return;
}
var nDiv = domConstruct.create("div");
nDiv.style.cssText = x.style.cssText;
x.parentNode.insertBefore(nDiv,x);
while(x.firstChild){
nDiv.appendChild(x.firstChild);
}
x.parentNode.removeChild(x);
}
if(!this._hasTag(x,"LI")){