b89fcc1792a678b6c56ec70b52e7ff57593d3de7
1 /***************************************************************
4 * (c) 2007-2012 Stanislas Rolland <typo3(arobas)sjbr.ca>
7 * This script is part of the TYPO3 project. The TYPO3 project is
8 * free software; you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation; either version 2 of the License, or
11 * (at your option) any later version.
13 * The GNU General Public License can be found at
14 * http://www.gnu.org/copyleft/gpl.html.
15 * A copy is found in the textfile GPL.txt and important notices to the license
16 * from the author is found in LICENSE.txt distributed with these scripts.
19 * This script is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
25 * This copyright notice MUST APPEAR in all copies of the script!
26 ***************************************************************/
28 * BlockElements Plugin for TYPO3 htmlArea RTE
30 HTMLArea
.BlockElements
= Ext
.extend(HTMLArea
.Plugin
, {
32 * This function gets called by the class constructor
34 configurePlugin: function (editor
) {
36 * Setting up some properties from PageTSConfig
38 this.buttonsConfiguration
= this.editorConfiguration
.buttons
;
39 if (this.buttonsConfiguration
.blockstyle
) {
40 this.tags
= this.editorConfiguration
.buttons
.blockstyle
.tags
;
44 JustifyLeft
: "align-left",
45 JustifyCenter
: "align-center",
46 JustifyRight
: "align-right",
47 JustifyFull
: "align-justify"
49 this.useAlignAttribute
= false;
50 for (var buttonId
in this.useClass
) {
51 if (this.useClass
.hasOwnProperty(buttonId
)) {
52 if (this.editorConfiguration
.buttons
[this.buttonList
[buttonId
][2]]) {
53 this.useClass
[buttonId
] = this.editorConfiguration
.buttons
[this.buttonList
[buttonId
][2]].useClass
? this.editorConfiguration
.buttons
[this.buttonList
[buttonId
][2]].useClass
: this.useClass
[buttonId
];
54 if (buttonId
=== "Indent") {
55 this.useBlockquote
= this.editorConfiguration
.buttons
.indent
.useBlockquote
? this.editorConfiguration
.buttons
.indent
.useBlockquote
: false;
57 if (this.editorConfiguration
.buttons
[this.buttonList
[buttonId
][2]].useAlignAttribute
) {
58 this.useAlignAttribute
= true;
64 this.allowedAttributes
= new Array('id', 'title', 'lang', 'xml:lang', 'dir', 'class', 'itemscope', 'itemtype', 'itemprop');
66 this.addAllowedAttribute('className');
68 this.indentedList
= null;
69 // Standard block formating items
70 var standardElements
= new Array('address', 'article', 'aside', 'blockquote', 'div', 'footer', 'header', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'nav', 'p', 'pre', 'section');
71 this.standardBlockElements
= new RegExp( '^(' + standardElements
.join('|') + ')$', 'i');
72 // Process block formating customization configuration
73 this.formatBlockItems
= {};
74 if (this.buttonsConfiguration
75 && this.buttonsConfiguration
.formatblock
76 && this.buttonsConfiguration
.formatblock
.items
) {
77 this.formatBlockItems
= this.buttonsConfiguration
.formatblock
.items
;
79 // Build lists of mutually exclusive class names
80 for (var tagName
in this.formatBlockItems
) {
81 if (this.formatBlockItems
.hasOwnProperty(tagName
) && this.formatBlockItems
[tagName
].tagName
&& this.formatBlockItems
[tagName
].addClass
) {
82 if (!this.formatBlockItems
[this.formatBlockItems
[tagName
].tagName
]) {
83 this.formatBlockItems
[this.formatBlockItems
[tagName
].tagName
] = {};
85 if (!this.formatBlockItems
[this.formatBlockItems
[tagName
].tagName
].classList
) {
86 this.formatBlockItems
[this.formatBlockItems
[tagName
].tagName
].classList
= new Array();
88 this.formatBlockItems
[this.formatBlockItems
[tagName
].tagName
].classList
.push(this.formatBlockItems
[tagName
].addClass
);
91 for (var tagName
in this.formatBlockItems
) {
92 if (this.formatBlockItems
.hasOwnProperty(tagName
) && this.formatBlockItems
[tagName
].classList
) {
93 this.formatBlockItems
[tagName
].classList
= new RegExp( "^(" + this.formatBlockItems
[tagName
].classList
.join("|") + ")$");
97 * Registering plugin "About" information
99 var pluginInformation
= {
101 developer
: 'Stanislas Rolland',
102 developerUrl
: 'http://www.sjbr.ca/',
103 copyrightOwner
: 'Stanislas Rolland',
104 sponsor
: this.localize('Technische Universitat Ilmenau'),
105 sponsorUrl
: 'http://www.tu-ilmenau.de/',
108 this.registerPluginInformation(pluginInformation
);
111 * Registering the dropdown list
113 var buttonId
= "FormatBlock";
114 var dropDownConfiguration
= {
116 tooltip
: this.localize(buttonId
+ "-Tooltip"),
117 options
: this.buttonsConfiguration
.formatblock
? this.buttonsConfiguration
.formatblock
.options
: [],
120 if (this.buttonsConfiguration
.formatblock
) {
121 dropDownConfiguration
.width
= this.buttonsConfiguration
.formatblock
.width
? parseInt(this.buttonsConfiguration
.formatblock
.width
, 10) : 200;
122 if (this.buttonsConfiguration
.formatblock
.listWidth
) {
123 dropDownConfiguration
.listWidth
= parseInt(this.buttonsConfiguration
.formatblock
.listWidth
, 10);
125 if (this.buttonsConfiguration
.formatblock
.maxHeight
) {
126 dropDownConfiguration
.maxHeight
= parseInt(this.buttonsConfiguration
.formatblock
.maxHeight
, 10);
129 this.registerDropDown(dropDownConfiguration
);
131 * Establishing the list of allowed block elements
133 var blockElements
= new Array();
134 Ext
.each(dropDownConfiguration
.options
, function (option
) {
135 if (option
[1] != 'none') {
136 blockElements
.push(option
[1]);
139 if (blockElements
.length
) {
140 this.allowedBlockElements
= new RegExp( "^(" + blockElements
.join("|") + ")$", "i");
142 this.allowedBlockElements
= this.standardBlockElements
;
145 * Registering hot keys for the dropdown list items
147 Ext
.each(blockElements
, function (blockElement
) {
148 var configuredHotKey
= this.defaultHotKeys
[blockElement
];
149 if (this.editorConfiguration
.buttons
.formatblock
150 && this.editorConfiguration
.buttons
.formatblock
.items
151 && this.editorConfiguration
.buttons
.formatblock
.items
[blockElement
]
152 && this.editorConfiguration
.buttons
.formatblock
.items
[blockElement
].hotKey
) {
153 configuredHotKey
= this.editorConfiguration
.buttons
.formatblock
.items
[blockElement
].hotKey
;
155 if (configuredHotKey
) {
156 var hotKeyConfiguration
= {
157 id
: configuredHotKey
,
159 element
: blockElement
161 this.registerHotKey(hotKeyConfiguration
);
165 * Registering the buttons
167 for (var buttonId
in this.buttonList
) {
168 if (this.buttonList
.hasOwnProperty(buttonId
)) {
169 var button
= this.buttonList
[buttonId
];
170 var buttonConfiguration
= {
172 tooltip
: this.localize(buttonId
+ '-Tooltip'),
173 iconCls
: 'htmlarea-action-' + button
[3],
174 contextMenuTitle
: this.localize(buttonId
+ '-contextMenuTitle'),
175 helpText
: this.localize(buttonId
+ '-helpText'),
176 action
: 'onButtonPress',
177 hotKey
: ((this.buttonsConfiguration
[button
[2]] && this.buttonsConfiguration
[button
[2]].hotKey
) ? this.buttonsConfiguration
[button
[2]].hotKey
: (button
[1] ? button
[1] : null))
179 this.registerButton(buttonConfiguration
);
185 * The list of buttons added by this plugin
188 Indent
: [null, 'TAB', 'indent', 'indent'],
189 Outdent
: [null, 'SHIFT-TAB', 'outdent', 'outdent'],
190 Blockquote
: [null, null, 'blockquote', 'blockquote'],
191 InsertParagraphBefore
: [null, null, 'insertparagraphbefore', 'paragraph-insert-before'],
192 InsertParagraphAfter
: [null, null, 'insertparagraphafter', 'paragraph-insert-after'],
193 JustifyLeft
: [null, 'l', 'left', 'justify-left'],
194 JustifyCenter
: [null, 'e', 'center', 'justify-center'],
195 JustifyRight
: [null, 'r', 'right', 'justify-right'],
196 JustifyFull
: [null, 'j', 'justifyfull', 'justify-full'],
197 InsertOrderedList
: [null, null, 'orderedlist', 'ordered-list'],
198 InsertUnorderedList
: [null, null, 'unorderedlist', 'unordered-list'],
199 InsertHorizontalRule
: [null, null, 'inserthorizontalrule', 'horizontal-rule-insert']
202 * The list of hotkeys associated with block elements and registered by default by this plugin
214 * The function returns true if the type of block element is allowed in the current configuration
216 isAllowedBlockElement: function (blockName
) {
217 return this.allowedBlockElements
.test(blockName
);
220 * This function adds an attribute to the array of attributes allowed on block elements
222 * @param string attribute: the name of the attribute to be added to the array
226 addAllowedAttribute: function (attribute
) {
227 this.allowedAttributes
.push(attribute
);
230 * This function gets called when some block element was selected in the drop-down list
232 onChange: function (editor
, combo
, record
, index
) {
233 this.applyBlockElement(combo
.itemId
, combo
.getValue());
235 applyBlockElement: function (buttonId
, blockElement
) {
236 var tagName
= blockElement
;
237 var className
= null;
238 if (this.formatBlockItems
[tagName
]) {
239 if (this.formatBlockItems
[tagName
].addClass
) {
240 className
= this.formatBlockItems
[tagName
].addClass
;
242 if (this.formatBlockItems
[tagName
].tagName
) {
243 tagName
= this.formatBlockItems
[tagName
].tagName
;
246 if (this.standardBlockElements
.test(tagName
) || tagName
== "none") {
249 this.onButtonPress(this.editor
, 'Blockquote', null, className
);
260 this.onButtonPress(this.editor
, tagName
, null, className
);
263 var element
= tagName
;
265 element
= '<' + element
+ '>';
269 if (!this.editor
.document
.body
.hasChildNodes()) {
270 this.editor
.document
.body
.appendChild((this.editor
.document
.createElement('br')));
272 // WebKit sometimes leaves empty block at the end of the selection
273 this.editor
.document
.body
.normalize();
276 this.editor
.getSelection().execCommand(buttonId
, false, element
);
278 this.appendToLog('applyBlockElement', e
+ '\n\nby execCommand(' + buttonId
+ ');', 'error');
280 this.addClassOnBlockElements(tagName
, className
);
285 * This function gets called when a button was pressed.
287 * @param object editor: the editor instance
288 * @param string id: the button id or the key
289 * @param object target: the target element of the contextmenu event, when invoked from the context menu
290 * @param string className: the className to be assigned to the element
292 * @return boolean false if action is completed
294 onButtonPress: function (editor
, id
, target
, className
) {
295 // Could be a button or its hotkey
296 var buttonId
= this.translateHotKey(id
);
297 buttonId
= buttonId
? buttonId
: id
;
298 var range
= editor
.getSelection().createRange();
299 var statusBarSelection
= this.editor
.statusBar
? this.editor
.statusBar
.getSelection() : null;
300 var parentElement
= statusBarSelection
? statusBarSelection
: this.editor
.getSelection().getParentElement();
302 parentElement
= target
;
304 while (parentElement
&& (!HTMLArea
.DOM
.isBlockElement(parentElement
) || /^li$/i.test(parentElement
.nodeName
))) {
305 parentElement
= parentElement
.parentNode
;
307 var blockAncestors
= HTMLArea
.DOM
.getBlockAncestors(parentElement
);
308 var tableCell
= null;
309 if (id
=== "TAB" || id
=== "SHIFT-TAB") {
310 for (var i
= blockAncestors
.length
; --i
>= 0;) {
311 if (/^(td|th)$/i.test(blockAncestors
[i
].nodeName
)) {
312 tableCell
= blockAncestors
[i
];
317 var fullNodeTextSelected
= (!Ext
.isIE
&& parentElement
.textContent
=== range
.toString()) || (Ext
.isIE
&& parentElement
.innerText
=== range
.text
);
320 if (/^(ol|ul)$/i.test(parentElement
.nodeName
) && !(fullNodeTextSelected
&& !/^(li)$/i.test(parentElement
.parentNode
.nodeName
))) {
323 this.editor
.getSelection().execCommand(buttonId
, false, null);
325 this.appendToLog('onButtonPress', e
+ '\n\nby execCommand(' + buttonId
+ ');', 'error');
327 this.indentedList
= parentElement
;
328 this.makeNestedList(parentElement
);
329 this.editor
.getSelection().selectNodeContents(this.indentedList
.lastChild
, false);
331 this.indentSelectedListElements(parentElement
, range
);
333 } else if (tableCell
) {
335 var tablePart
= tableCell
.parentNode
.parentNode
;
336 // Get next cell in same table part
337 var nextCell
= tableCell
.nextSibling
? tableCell
.nextSibling
: (tableCell
.parentNode
.nextSibling
? tableCell
.parentNode
.nextSibling
.cells
[0] : null);
338 // Next cell is in other table part
340 switch (tablePart
.nodeName
.toLowerCase()) {
342 nextCell
= tablePart
.parentNode
.tBodies
[0].rows
[0].cells
[0];
345 nextCell
= tablePart
.nextSibling
? tablePart
.nextSibling
.rows
[0].cells
[0] : null;
348 this.editor
.getSelection().selectNodeContents(tablePart
.parentNode
.lastChild
.lastChild
.lastChild
, true);
352 if (this.getPluginInstance('TableOperations')) {
353 this.getPluginInstance('TableOperations').onButtonPress(this.editor
, 'TO-row-insert-under');
355 nextCell
= tablePart
.parentNode
.rows
[0].cells
[0];
359 if (Ext
.isOpera
&& !nextCell
.hasChildNodes()) {
360 nextCell
.appendChild(this.editor
.document
.createElement('br'));
362 this.editor
.getSelection().selectNodeContents(nextCell
, true);
364 } else if (this.useBlockquote
) {
366 this.editor
.getSelection().execCommand(buttonId
, false, null);
368 this.appendToLog('onButtonPress', e
+ '\n\nby execCommand(' + buttonId
+ ');', 'error');
370 } else if (this.isAllowedBlockElement("div")) {
371 if (/^div$/i.test(parentElement
.nodeName
) && !HTMLArea
.DOM
.hasClass(parentElement
, this.useClass
[buttonId
])) {
372 HTMLArea
.DOM
.addClass(parentElement
, this.useClass
[buttonId
]);
373 } else if (!/^div$/i.test(parentElement
.nodeName
) && /^div$/i.test(parentElement
.parentNode
.nodeName
) && !HTMLArea
.DOM
.hasClass(parentElement
.parentNode
, this.useClass
[buttonId
])) {
374 HTMLArea
.DOM
.addClass(parentElement
.parentNode
, this.useClass
[buttonId
]);
376 var bookmark
= this.editor
.getBookMark().get(range
);
377 var newBlock
= this.wrapSelectionInBlockElement('div', this.useClass
[buttonId
], null, true);
378 this.editor
.getSelection().selectRange(this.editor
.getBookMark().moveTo(bookmark
));
381 this.addClassOnBlockElements(buttonId
);
385 if (/^(ol|ul)$/i.test(parentElement
.nodeName
) && !HTMLArea
.DOM
.hasClass(parentElement
, this.useClass
.Indent
)) {
386 if (/^(li)$/i.test(parentElement
.parentNode
.nodeName
)) {
389 this.editor
.getSelection().execCommand(buttonId
, false, null);
391 this.appendToLog('onButtonPress', e
+ '\n\nby execCommand(' + buttonId
+ ');', 'error');
394 this.outdentSelectedListElements(parentElement
, range
);
397 } else if (tableCell
) {
398 var previousCell
= tableCell
.previousSibling
? tableCell
.previousSibling
: (tableCell
.parentNode
.previousSibling
? tableCell
.parentNode
.previousSibling
.lastChild
: null);
400 var table
= tableCell
.parentNode
.parentNode
.parentNode
;
401 var tablePart
= tableCell
.parentNode
.parentNode
.nodeName
.toLowerCase();
405 previousCell
= table
.tHead
.rows
[table
.tHead
.rows
.length
-1].cells
[table
.tHead
.rows
[table
.tHead
.rows
.length
-1].cells
.length
-1];
410 previousCell
= table
.tFoot
.rows
[table
.tFoot
.rows
.length
-1].cells
[table
.tFoot
.rows
[table
.tFoot
.rows
.length
-1].cells
.length
-1];
414 previousCell
= table
.tBodies
[table
.tBodies
.length
-1].rows
[table
.tBodies
[table
.tBodies
.length
-1].rows
.length
-1].cells
[table
.tBodies
[table
.tBodies
.length
-1].rows
[table
.tBodies
[table
.tBodies
.length
-1].rows
.length
-1].cells
.length
-1];
418 if (Ext
.isOpera
&& !previousCell
.hasChildNodes()) {
419 previousCell
.appendChild(this.editor
.document
.createElement('br'));
421 this.editor
.getSelection().selectNodeContents(previousCell
, true);
423 } else if (this.useBlockquote
) {
425 this.editor
.getSelection().execCommand(buttonId
, false, null);
427 this.appendToLog('onButtonPress', e
+ '\n\nby execCommand(' + buttonId
+ ');', 'error');
429 } else if (this.isAllowedBlockElement("div")) {
430 for (var i
= blockAncestors
.length
; --i
>= 0;) {
431 if (HTMLArea
.DOM
.hasClass(blockAncestors
[i
], this.useClass
.Indent
)) {
432 var bookmark
= this.editor
.getBookMark().get(range
);
433 var newBlock
= this.wrapSelectionInBlockElement('div', false, blockAncestors
[i
]);
434 // If not directly under the div, we need to backtrack
435 if (newBlock
.parentNode
!== blockAncestors
[i
]) {
436 var parent
= newBlock
.parentNode
;
437 this.editor
.getDomNode().removeMarkup(newBlock
);
438 while (parent
.parentNode
!== blockAncestors
[i
]) {
439 parent
= parent
.parentNode
;
441 blockAncestors
[i
].insertBefore(newBlock
, parent
);
442 newBlock
.appendChild(parent
);
444 newBlock
.className
= blockAncestors
[i
].className
;
445 HTMLArea
.DOM
.removeClass(newBlock
, this.useClass
.Indent
);
446 if (!newBlock
.previousSibling
) {
447 while (newBlock
.hasChildNodes()) {
448 if (newBlock
.firstChild
.nodeType
=== HTMLArea
.DOM
.ELEMENT_NODE
) {
449 newBlock
.firstChild
.className
= newBlock
.className
;
451 blockAncestors
[i
].parentNode
.insertBefore(newBlock
.firstChild
, blockAncestors
[i
]);
453 } else if (!newBlock
.nextSibling
) {
454 if (blockAncestors
[i
].nextSibling
) {
455 while (newBlock
.hasChildNodes()) {
456 if (newBlock
.firstChild
.nodeType
=== HTMLArea
.DOM
.ELEMENT_NODE
) {
457 newBlock
.lastChild
.className
= newBlock
.className
;
459 blockAncestors
[i
].parentNode
.insertBefore(newBlock
.lastChild
, blockAncestors
[i
].nextSibling
);
462 while (newBlock
.hasChildNodes()) {
463 if (newBlock
.firstChild
.nodeType
=== HTMLArea
.DOM
.ELEMENT_NODE
) {
464 newBlock
.firstChild
.className
= newBlock
.className
;
466 blockAncestors
[i
].parentNode
.appendChild(newBlock
.firstChild
);
470 var clone
= blockAncestors
[i
].cloneNode(false);
471 if (blockAncestors
[i
].nextSibling
) {
472 blockAncestors
[i
].parentNode
.insertBefore(clone
, blockAncestors
[i
].nextSibling
);
474 blockAncestors
[i
].parentNode
.appendChild(clone
);
476 while (newBlock
.nextSibling
) {
477 clone
.appendChild(newBlock
.nextSibling
);
479 while (newBlock
.hasChildNodes()) {
480 if (newBlock
.firstChild
.nodeType
=== HTMLArea
.DOM
.ELEMENT_NODE
) {
481 newBlock
.firstChild
.className
= newBlock
.className
;
483 blockAncestors
[i
].parentNode
.insertBefore(newBlock
.firstChild
, clone
);
486 blockAncestors
[i
].removeChild(newBlock
);
487 if (!blockAncestors
[i
].hasChildNodes()) {
488 blockAncestors
[i
].parentNode
.removeChild(blockAncestors
[i
]);
490 this.editor
.getSelection().selectRange(this.editor
.getBookMark().moveTo(bookmark
));
495 this.addClassOnBlockElements(buttonId
);
498 case "InsertParagraphBefore" :
499 case "InsertParagraphAfter" :
500 this.insertParagraph(buttonId
=== "InsertParagraphAfter");
503 var commandState
= false;
504 for (var i
= blockAncestors
.length
; --i
>= 0;) {
505 if (/^blockquote$/i.test(blockAncestors
[i
].nodeName
)) {
507 this.editor
.getDomNode().removeMarkup(blockAncestors
[i
]);
512 var bookmark
= this.editor
.getBookMark().get(range
);
513 var newBlock
= this.wrapSelectionInBlockElement('blockquote', className
, null, true);
514 this.editor
.getSelection().selectRange(this.editor
.getBookMark().moveTo(bookmark
));
525 var bookmark
= this.editor
.getBookMark().get(range
);
526 var newBlock
= this.wrapSelectionInBlockElement(buttonId
, className
, null, true);
527 this.editor
.getSelection().selectRange(this.editor
.getBookMark().moveTo(bookmark
));
530 case "JustifyCenter" :
531 case "JustifyRight" :
533 if (this.useAlignAttribute
) {
535 this.editor
.getSelection().execCommand(buttonId
, false, null);
537 this.appendToLog('onButtonPress', e
+ '\n\nby execCommand(' + buttonId
+ ');', 'error');
540 this.addClassOnBlockElements(buttonId
);
543 case "InsertOrderedList":
544 case "InsertUnorderedList":
545 this.insertList(buttonId
, parentElement
);
547 case "InsertHorizontalRule":
548 this.insertHorizontalRule();
551 if (this.isAllowedBlockElement(parentElement
.nodeName
)) {
552 this.editor
.getDomNode().removeMarkup(parentElement
);
561 * This function wraps the block elements intersecting the current selection in a block element of the given type
563 * @param string blockName: the type of element to be used as wrapping block
564 * @param string useClass: a class to be assigned to the wrapping block
565 * @param object withinBlock: only elements contained in this block will be wrapped
566 * @param boolean keepValid: make only valid wraps (working wraps may produce temporary invalid hierarchies)
568 * @return object the wrapping block
570 wrapSelectionInBlockElement: function (blockName
, useClass
, withinBlock
, keepValid
) {
571 var endBlocks
= this.editor
.getSelection().getEndBlocks();
572 var startAncestors
= HTMLArea
.DOM
.getBlockAncestors(endBlocks
.start
, withinBlock
);
573 var endAncestors
= HTMLArea
.DOM
.getBlockAncestors(endBlocks
.end
, withinBlock
);
575 while (i
< startAncestors
.length
&& i
< endAncestors
.length
&& startAncestors
[i
] === endAncestors
[i
]) {
579 if ((endBlocks
.start
=== endBlocks
.end
&& /^(body)$/i.test(endBlocks
.start
.nodeName
)) || !startAncestors
[i
] || !endAncestors
[i
]) {
583 if (endBlocks
.start
=== endBlocks
.end
) {
584 while (i
&& /^(thead|tbody|tfoot|tr|dt)$/i.test(startAncestors
[i
].nodeName
)) {
588 while (i
&& (/^(thead|tbody|tfoot|tr|td|li|dd|dt)$/i.test(startAncestors
[i
].nodeName
) || /^(thead|tbody|tfoot|tr|td|li|dd|dt)$/i.test(endAncestors
[i
].nodeName
))) {
593 var blockElement
= this.editor
.document
.createElement(blockName
);
595 HTMLArea
.DOM
.addClass(blockElement
, useClass
);
597 var contextElement
= endAncestors
[0];
599 contextElement
= endAncestors
[i
-1];
601 var nextElement
= endAncestors
[i
].nextSibling
;
602 var block
= startAncestors
[i
], sibling
;
603 if ((!/^(body|td|th|li|dd)$/i.test(block
.nodeName
) || /^(ol|ul|dl)$/i.test(blockName
)) && block
!= withinBlock
) {
604 while (block
&& block
!= nextElement
) {
605 sibling
= block
.nextSibling
;
606 blockElement
.appendChild(block
);
610 blockElement
= nextElement
.parentNode
.insertBefore(blockElement
, nextElement
);
612 blockElement
= contextElement
.appendChild(blockElement
);
615 contextElement
= block
;
616 block
= block
.firstChild
;
618 sibling
= block
.nextSibling
;
619 blockElement
.appendChild(block
);
622 blockElement
= contextElement
.appendChild(blockElement
);
624 // Things go wrong in some browsers when the node is empty
625 if (Ext
.isWebKit
&& !blockElement
.hasChildNodes()) {
626 blockElement
= blockElement
.appendChild(this.editor
.document
.createElement('br'));
631 * This function adds a class attribute on blocks sibling of the block containing the start container of the selection
633 addClassOnBlockElements: function (buttonId
, className
) {
634 var endBlocks
= this.editor
.getSelection().getEndBlocks();
635 var startAncestors
= HTMLArea
.DOM
.getBlockAncestors(endBlocks
.start
);
636 var endAncestors
= HTMLArea
.DOM
.getBlockAncestors(endBlocks
.end
);
638 while (index
< startAncestors
.length
&& index
< endAncestors
.length
&& startAncestors
[index
] === endAncestors
[index
]) {
641 if (endBlocks
.start
=== endBlocks
.end
) {
644 if (!/^(body)$/i.test(startAncestors
[index
].nodeName
)) {
645 for (var block
= startAncestors
[index
]; block
; block
= block
.nextSibling
) {
646 if (HTMLArea
.DOM
.isBlockElement(block
)) {
649 if (!HTMLArea
.DOM
.hasClass(block
, this.useClass
[buttonId
])) {
650 HTMLArea
.DOM
.addClass(block
, this.useClass
[buttonId
]);
654 if (HTMLArea
.DOM
.hasClass(block
, this.useClass
["Indent"])) {
655 HTMLArea
.DOM
.removeClass(block
, this.useClass
["Indent"]);
659 case "JustifyCenter" :
660 case "JustifyRight" :
662 this.toggleAlignmentClass(block
, buttonId
);
665 if (this.standardBlockElements
.test(buttonId
.toLowerCase()) && buttonId
.toLowerCase() == block
.nodeName
.toLowerCase()) {
666 this.cleanClasses(block
);
668 HTMLArea
.DOM
.addClass(block
, className
);
674 if (block
== endAncestors
[index
]) {
681 * This function toggles the alignment class on the given block
683 toggleAlignmentClass: function (block
, buttonId
) {
684 for (var alignmentButtonId
in this.useClass
) {
685 if (this.useClass
.hasOwnProperty(alignmentButtonId
) && alignmentButtonId
!== "Indent") {
686 if (HTMLArea
.DOM
.hasClass(block
, this.useClass
[alignmentButtonId
])) {
687 HTMLArea
.DOM
.removeClass(block
, this.useClass
[alignmentButtonId
]);
688 } else if (alignmentButtonId
=== buttonId
) {
689 HTMLArea
.DOM
.addClass(block
, this.useClass
[alignmentButtonId
]);
693 if (/^div$/i.test(block
.nodeName
) && !HTMLArea
.DOM
.hasAllowedAttributes(block
, this.allowedAttributes
)) {
694 this.editor
.getDomNode().removeMarkup(block
);
698 insertList: function (buttonId
, parentElement
) {
699 if (/^(dd)$/i.test(parentElement
.nodeName
)) {
700 var list
= parentElement
.appendChild(this.editor
.document
.createElement((buttonId
=== 'OrderedList') ? 'ol' : 'ul'));
701 var first
= list
.appendChild(this.editor
.document
.createElement('li'));
702 first
.innerHTML
= '<br />';
703 this.editor
.getSelection().selectNodeContents(first
, true);
705 // parentElement may be removed by following command
706 var parentNode
= parentElement
.parentNode
;
708 this.editor
.getSelection().execCommand(buttonId
, false, null);
710 this.appendToLog('onButtonPress', e
+ '\n\nby execCommand(' + buttonId
+ ');', 'error');
713 this.editor
.getDomNode().cleanAppleStyleSpans(parentNode
);
718 * Indent selected list elements
720 indentSelectedListElements: function (list
, range
) {
721 var bookmark
= this.editor
.getBookMark().get(range
);
722 // The selected elements are wrapped into a list element
723 var indentedList
= this.wrapSelectionInBlockElement(list
.nodeName
.toLowerCase(), null, list
);
724 // which breaks the range
725 var range
= this.editor
.getBookMark().moveTo(bookmark
);
726 bookmark
= this.editor
.getBookMark().get(range
);
728 // Check if the last element has children. If so, outdent those that do not intersect the selection
729 var last
= indentedList
.lastChild
.lastChild
;
730 if (last
&& /^(ol|ul)$/i.test(last
.nodeName
)) {
731 var child
= last
.firstChild
, next
;
733 next
= child
.nextSibling
;
734 if (!HTMLArea
.DOM
.rangeIntersectsNode(range
, child
)) {
735 indentedList
.appendChild(child
);
739 if (!last
.hasChildNodes()) {
740 HTMLArea
.DOM
.removeFromParent(last
);
743 if (indentedList
.previousSibling
&& indentedList
.previousSibling
.hasChildNodes()) {
744 // Indenting some elements not including the first one
745 if (/^(ol|ul)$/i.test(indentedList
.previousSibling
.lastChild
.nodeName
)) {
746 // Some indented elements exist just above our selection
747 // Moving to regroup with these elements
748 while (indentedList
.hasChildNodes()) {
749 indentedList
.previousSibling
.lastChild
.appendChild(indentedList
.firstChild
);
751 list
.removeChild(indentedList
);
753 indentedList
= indentedList
.previousSibling
.appendChild(indentedList
);
756 // Indenting the first element and possibly some more
757 var first
= this.editor
.document
.createElement("li");
758 first
.innerHTML
= " ";
759 list
.insertBefore(first
, indentedList
);
760 indentedList
= first
.appendChild(indentedList
);
762 this.editor
.getSelection().selectRange(this.editor
.getBookMark().moveTo(bookmark
));
765 * Outdent selected list elements
767 outdentSelectedListElements: function (list
, range
) {
768 // We wrap the selected li elements and thereafter move them one level up
769 var bookmark
= this.editor
.getBookMark().get(range
);
770 var wrappedList
= this.wrapSelectionInBlockElement(list
.nodeName
.toLowerCase(), null, list
);
771 // which breaks the range
772 var range
= this.editor
.getBookMark().moveTo(bookmark
);
773 bookmark
= this.editor
.getBookMark().get(range
);
775 if (!wrappedList
.previousSibling
) {
776 // Outdenting the first element(s) of an indented list
777 var next
= list
.parentNode
.nextSibling
;
778 var last
= wrappedList
.lastChild
;
779 while (wrappedList
.hasChildNodes()) {
781 list
.parentNode
.parentNode
.insertBefore(wrappedList
.firstChild
, next
);
783 list
.parentNode
.parentNode
.appendChild(wrappedList
.firstChild
);
786 list
.removeChild(wrappedList
);
787 last
.appendChild(list
);
788 } else if (!wrappedList
.nextSibling
) {
789 // Outdenting the last element(s) of the list
790 // This will break the gecko bookmark
791 this.editor
.getBookMark().moveTo(bookmark
);
792 while (wrappedList
.hasChildNodes()) {
793 if (list
.parentNode
.nextSibling
) {
794 list
.parentNode
.parentNode
.insertBefore(wrappedList
.firstChild
, list
.parentNode
.nextSibling
);
796 list
.parentNode
.parentNode
.appendChild(wrappedList
.firstChild
);
799 list
.removeChild(wrappedList
);
800 this.editor
.getSelection().selectNodeContents(list
.parentNode
.nextSibling
, true);
801 bookmark
= this.editor
.getBookMark().get(this.editor
.getSelection().createRange());
803 // Outdenting the middle of a list
804 var next
= list
.parentNode
.nextSibling
;
805 var last
= wrappedList
.lastChild
;
806 var sibling
= wrappedList
.nextSibling
;
807 while (wrappedList
.hasChildNodes()) {
809 list
.parentNode
.parentNode
.insertBefore(wrappedList
.firstChild
, next
);
811 list
.parentNode
.parentNode
.appendChild(wrappedList
.firstChild
);
815 wrappedList
.appendChild(sibling
);
816 sibling
= sibling
.nextSibling
;
818 last
.appendChild(wrappedList
);
820 // Remove the list if all its elements have been moved up
821 if (!list
.hasChildNodes()) {
822 list
.parentNode
.removeChild(list
);
824 this.editor
.getSelection().selectRange(this.editor
.getBookMark().moveTo(bookmark
));
827 * Make XHTML-compliant nested list
828 * We need this for Opera
830 makeNestedList: function (el
) {
832 for (var i
= el
.firstChild
; i
; i
= i
.nextSibling
) {
833 if (/^li$/i.test(i
.nodeName
)) {
834 for (var j
= i
.firstChild
; j
; j
= j
.nextSibling
) {
835 if (/^(ol|ul)$/i.test(j
.nodeName
)) {
836 this.makeNestedList(j
);
839 } else if (/^(ol|ul)$/i.test(i
.nodeName
)) {
840 previous
= i
.previousSibling
;
841 this.indentedList
= i
.cloneNode(true);
843 previous
= el
.insertBefore(this.editor
.document
.createElement('li'), i
);
844 this.indentedList
= previous
.appendChild(this.indentedList
);
846 this.indentedList
= previous
.appendChild(this.indentedList
);
848 HTMLArea
.DOM
.removeFromParent(i
);
849 this.makeNestedList(el
);
857 insertParagraph: function (after
) {
858 var endBlocks
= this.editor
.getSelection().getEndBlocks();
859 var ancestors
= after
? HTMLArea
.DOM
.getBlockAncestors(endBlocks
.end
) : HTMLArea
.DOM
.getBlockAncestors(endBlocks
.start
);
860 var endElement
= ancestors
[ancestors
.length
-1];
861 for (var i
= ancestors
.length
; --i
>= 0;) {
862 if (/^(table|div|ul|ol|dl|blockquote|address|pre)$/i.test(ancestors
[i
].nodeName
) && !/^(li)$/i.test(ancestors
[i
].parentNode
.nodeName
)) {
863 endElement
= ancestors
[i
];
868 var parent
= endElement
.parentNode
;
869 var paragraph
= this.editor
.document
.createElement("p");
871 paragraph
.innerHTML
= " ";
873 paragraph
.appendChild(this.editor
.document
.createElement("br"));
875 if (after
&& !endElement
.nextSibling
) {
876 parent
.appendChild(paragraph
);
878 parent
.insertBefore(paragraph
, after
? endElement
.nextSibling
: endElement
);
880 this.editor
.getSelection().selectNodeContents(paragraph
, true);
884 * Insert horizontal line
886 insertHorizontalRule: function () {
887 this.editor
.getSelection().execCommand('InsertHorizontalRule');
888 // Apply enterParagraphs rule
889 if (!Ext
.isIE
&& !Ext
.isOpera
&& !this.editor
.config
.disableEnterParagraphs
) {
890 var range
= this.editor
.getSelection().createRange();
891 var startContainer
= range
.startContainer
;
892 if (/^body$/i.test(startContainer
.nodeName
)) {
893 startContainer
.normalize();
894 var ruler
= startContainer
.childNodes
[range
.startOffset
-1];
895 if (ruler
.nextSibling
) {
896 if (ruler
.nextSibling
.nodeType
=== HTMLArea
.DOM
.TEXT_NODE
) {
897 if (/\S/.test(ruler
.nextSibling
.textContent
)) {
898 var paragraph
= this.editor
.document
.createElement('p');
899 paragraph
= startContainer
.appendChild(paragraph
);
900 paragraph
= startContainer
.insertBefore(paragraph
, ruler
.nextSibling
);
901 paragraph
.appendChild(ruler
.nextSibling
);
903 HTMLArea
.DOM
.removeFromParent(ruler
.nextSibling
);
904 var paragraph
= ruler
.nextSibling
;
907 var paragraph
= ruler
.nextSibling
;
909 // Cannot set the cursor on the hr element
910 if (/^hr$/i.test(paragraph
.nodeName
)) {
911 var inBetweenParagraph
= this.editor
.document
.createElement('p');
912 inBetweenParagraph
.innerHTML
= '<br />';
913 paragraph
= startContainer
.insertBefore(inBetweenParagraph
, paragraph
);
916 var paragraph
= this.editor
.document
.createElement('p');
918 paragraph
.innerHTML
= '<br />';
920 paragraph
= startContainer
.appendChild(paragraph
);
922 this.editor
.getSelection().selectNodeContents(paragraph
, true);
927 * This function gets called when the plugin is generated
929 onGenerate: function () {
930 // Register the enter key handler for IE when the cursor is at the end of a dt or a dd element
932 this.editor
.iframe
.keyMap
.addBinding({
933 key
: Ext
.EventObject
.ENTER
,
941 * This function gets called when the enter key was pressed in IE
942 * It will process the enter key for IE when the cursor is at the end of a dt or a dd element
944 * @param string key: the key code
945 * @param object event: the Ext event object (keydown)
947 * @return boolean false, if the event was taken care of
949 onKey: function (key
, event
) {
950 if (this.editor
.getSelection().isEmpty()) {
951 var range
= this.editor
.getSelection().createRange();
952 var parentElement
= this.editor
.getSelection().getParentElement();
953 while (parentElement
&& !HTMLArea
.DOM
.isBlockElement(parentElement
)) {
954 parentElement
= parentElement
.parentNode
;
956 if (/^(dt|dd)$/i.test(parentElement
.nodeName
)) {
957 var nodeRange
= this.editor
.getSelection().createRange();
958 nodeRange
.moveToElementText(parentElement
);
959 range
.setEndPoint("EndToEnd", nodeRange
);
960 if (!range
.text
|| range
.text
== "\x20") {
961 var item
= parentElement
.parentNode
.insertBefore(this.editor
.document
.createElement((parentElement
.nodeName
.toLowerCase() === "dt") ? "dd" : "dt"), parentElement
.nextSibling
);
962 item
.innerHTML
= "\x20";
963 this.editor
.getSelection().selectNodeContents(item
, true);
967 } else if (/^(li)$/i.test(parentElement
.nodeName
)
968 && !parentElement
.innerText
969 && parentElement
.parentNode
.parentNode
970 && /^(dd|td|th)$/i.test(parentElement
.parentNode
.parentNode
.nodeName
)) {
971 var item
= parentElement
.parentNode
.parentNode
.insertBefore(this.editor
.document
.createTextNode("\x20"), parentElement
.parentNode
.nextSibling
);
972 this.editor
.getSelection().selectNodeContents(parentElement
.parentNode
.parentNode
, false);
973 parentElement
.parentNode
.removeChild(parentElement
);
981 * This function removes any disallowed class or mutually exclusive classes from the class attribute of the node
983 cleanClasses: function (node
) {
984 var classNames
= node
.className
.trim().split(" ");
985 var nodeName
= node
.nodeName
.toLowerCase();
986 for (var i
= classNames
.length
; --i
>= 0;) {
987 if (!HTMLArea
.reservedClassNames
.test(classNames
[i
])) {
988 if (this.tags
&& this.tags
[nodeName
] && this.tags
[nodeName
].allowedClasses
) {
989 if (!this.tags
[nodeName
].allowedClasses
.test(classNames
[i
])) {
990 HTMLArea
.DOM
.removeClass(node
, classNames
[i
]);
992 } else if (this.tags
&& this.tags
.all
&& this.tags
.all
.allowedClasses
) {
993 if (!this.tags
.all
.allowedClasses
.test(classNames
[i
])) {
994 HTMLArea
.DOM
.removeClass(node
, classNames
[i
]);
997 if (this.formatBlockItems
[nodeName
] && this.formatBlockItems
[nodeName
].classList
&& this.formatBlockItems
[nodeName
].classList
.test(classNames
[i
])) {
998 HTMLArea
.DOM
.removeClass(node
, classNames
[i
]);
1004 * This function gets called when the toolbar is updated
1006 onUpdateToolbar: function (button
, mode
, selectionEmpty
, ancestors
, endPointsInSameBlock
) {
1007 if (mode
=== 'wysiwyg' && this.editor
.isEditable()) {
1008 var statusBarSelection
= this.editor
.statusBar
? this.editor
.statusBar
.getSelection() : null;
1009 var parentElement
= statusBarSelection
? statusBarSelection
: this.editor
.getSelection().getParentElement();
1010 if (!/^body$/i.test(parentElement
.nodeName
)) {
1011 while (parentElement
&& !HTMLArea
.DOM
.isBlockElement(parentElement
) || /^li$/i.test(parentElement
.nodeName
)) {
1012 parentElement
= parentElement
.parentNode
;
1014 var blockAncestors
= HTMLArea
.DOM
.getBlockAncestors(parentElement
);
1015 var endBlocks
= this.editor
.getSelection().getEndBlocks();
1016 var startAncestors
= HTMLArea
.DOM
.getBlockAncestors(endBlocks
.start
);
1017 var endAncestors
= HTMLArea
.DOM
.getBlockAncestors(endBlocks
.end
);
1019 while (index
< startAncestors
.length
&& index
< endAncestors
.length
&& startAncestors
[index
] === endAncestors
[index
]) {
1022 if (endBlocks
.start
=== endBlocks
.end
|| !startAncestors
[index
]) {
1025 var commandState
= false;
1026 switch (button
.itemId
) {
1028 this.updateDropDown(button
, blockAncestors
[blockAncestors
.length
-1], startAncestors
[index
]);
1031 if (this.useBlockquote
) {
1032 for (var j
= blockAncestors
.length
; --j
>= 0;) {
1033 if (/^blockquote$/i.test(blockAncestors
[j
].nodeName
)) {
1034 commandState
= true;
1038 } else if (/^(ol|ul)$/i.test(parentElement
.nodeName
)) {
1039 commandState
= true;
1041 for (var j
= blockAncestors
.length
; --j
>= 0;) {
1042 if (HTMLArea
.DOM
.hasClass(blockAncestors
[j
], this.useClass
.Indent
) || /^(td|th)$/i.test(blockAncestors
[j
].nodeName
)) {
1043 commandState
= true;
1048 button
.setDisabled(!commandState
);
1052 case "InsertParagraphBefore" :
1053 case "InsertParagraphAfter" :
1054 button
.setDisabled(/^(body)$/i.test(startAncestors
[index
].nodeName
));
1057 for (var j
= blockAncestors
.length
; --j
>= 0;) {
1058 if (/^blockquote$/i.test(blockAncestors
[j
].nodeName
)) {
1059 commandState
= true;
1063 button
.setInactive(!commandState
);
1065 case "JustifyLeft" :
1066 case "JustifyCenter" :
1067 case "JustifyRight" :
1068 case "JustifyFull" :
1069 if (this.useAlignAttribute
) {
1071 commandState
= this.editor
.document
.queryCommandState(button
.itemId
);
1073 commandState
= false;
1076 if (/^(body)$/i.test(startAncestors
[index
].nodeName
)) {
1077 button
.setDisabled(true);
1079 button
.setDisabled(false);
1080 commandState
= true;
1081 for (var block
= startAncestors
[index
]; block
; block
= block
.nextSibling
) {
1082 commandState
= commandState
&& HTMLArea
.DOM
.hasClass(block
, this.useClass
[button
.itemId
]);
1083 if (block
== endAncestors
[index
]) {
1089 button
.setInactive(!commandState
);
1091 case "InsertOrderedList":
1092 case "InsertUnorderedList":
1094 commandState
= this.editor
.document
.queryCommandState(button
.itemId
);
1096 commandState
= false;
1098 button
.setInactive(!commandState
);
1104 // The selection is not contained in any block
1105 switch (button
.itemId
) {
1107 this.updateDropDown(button
);
1110 button
.setDisabled(true);
1114 case 'InsertParagraphBefore' :
1115 case 'InsertParagraphAfter' :
1116 button
.setDisabled(true);
1119 button
.setInactive(true);
1121 case 'JustifyLeft' :
1122 case 'JustifyCenter' :
1123 case 'JustifyRight' :
1124 case 'JustifyFull' :
1125 button
.setInactive(true);
1126 button
.setDisabled(true);
1128 case 'InsertOrderedList':
1129 case 'InsertUnorderedList':
1130 button
.setInactive(true);
1139 * This function updates the drop-down list of block elements
1141 updateDropDown: function(select
, deepestBlockAncestor
, startAncestor
) {
1142 var store
= select
.getStore();
1145 if (deepestBlockAncestor
) {
1146 var nodeName
= deepestBlockAncestor
.nodeName
.toLowerCase();
1147 // Could be a custom item ...
1148 index
= store
.findBy(function(record
, id
) {
1149 var item
= this.formatBlockItems
[record
.get('value')];
1150 return item
&& item
.tagName
== nodeName
&& item
.addClass
&& HTMLArea
.DOM
.hasClass(deepestBlockAncestor
, item
.addClass
);
1153 // ... or a standard one
1154 index
= store
.findExact('value', nodeName
);
1158 store
.insert(0, new store
.recordType({
1159 text
: this.localize('No block'),
1162 select
.setValue('none');
1164 store
.insert(0, new store
.recordType({
1165 text
: this.localize('Remove block'),
1168 select
.setValue(store
.getAt(index
+1).get('value'));
1172 * This function handles the hotkey events registered on elements of the dropdown list
1174 onHotKey: function(editor
, key
) {
1176 var hotKeyConfiguration
= this.getHotKeyConfiguration(key
);
1177 if (hotKeyConfiguration
) {
1178 var blockElement
= hotKeyConfiguration
.element
;
1180 if (blockElement
&& this.isAllowedBlockElement(blockElement
)) {
1181 this.applyBlockElement(this.translateHotKey(key
), blockElement
);