cb4d280af6fbfe8bb649f90ac5d13f52113aa445
[Packages/TYPO3.CMS.git] / typo3 / sysext / rtehtmlarea / htmlarea / plugins / BlockElements / block-elements.js
1 /***************************************************************
2 * Copyright notice
3 *
4 * (c) 2007-2010 Stanislas Rolland <typo3(arobas)sjbr.ca>
5 * All rights reserved
6 *
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.
12 *
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.
17 *
18 *
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.
23 *
24 *
25 * This copyright notice MUST APPEAR in all copies of the script!
26 ***************************************************************/
27 /*
28 * BlockElements Plugin for TYPO3 htmlArea RTE
29 *
30 * TYPO3 SVN ID: $Id$
31 */
32 HTMLArea.BlockElements = HTMLArea.Plugin.extend({
33
34 constructor : function(editor, pluginName) {
35 this.base(editor, pluginName);
36 },
37
38 /*
39 * This function gets called by the class constructor
40 */
41 configurePlugin : function (editor) {
42
43 /*
44 * Setting up some properties from PageTSConfig
45 */
46 this.buttonsConfiguration = this.editorConfiguration.buttons;
47 if (this.buttonsConfiguration.blockstyle) {
48 this.tags = this.editorConfiguration.buttons.blockstyle.tags;
49 }
50 this.useClass = {
51 Indent : "indent",
52 JustifyLeft : "align-left",
53 JustifyCenter : "align-center",
54 JustifyRight : "align-right",
55 JustifyFull : "align-justify"
56 };
57 this.useAlignAttribute = false;
58 for (var buttonId in this.useClass) {
59 if (this.useClass.hasOwnProperty(buttonId)) {
60 if (this.editorConfiguration.buttons[this.buttonList[buttonId][2]]) {
61 this.useClass[buttonId] = this.editorConfiguration.buttons[this.buttonList[buttonId][2]].useClass ? this.editorConfiguration.buttons[this.buttonList[buttonId][2]].useClass : this.useClass[buttonId];
62 if (buttonId === "Indent") {
63 this.useBlockquote = this.editorConfiguration.buttons.indent.useBlockquote ? this.editorConfiguration.buttons.indent.useBlockquote : false;
64 } else {
65 if (this.editorConfiguration.buttons[this.buttonList[buttonId][2]].useAlignAttribute) {
66 this.useAlignAttribute = true;
67 }
68 }
69 }
70 }
71 }
72 this.allowedAttributes = new Array("id", "title", "lang", "xml:lang", "dir", "class");
73 if (Ext.isIE) {
74 this.addAllowedAttribute("className");
75 }
76 this.indentedList = null;
77 // Standard block formating items
78 var standardElements = new Array("address", "blockquote", "div", "h1", "h2", "h3", "h4", "h5", "h6", "p", "pre");
79 this.standardBlockElements = new RegExp( "^(" + standardElements.join("|") + ")$", "i");
80 // Process block formating customization configuration
81 this.formatBlockItems = {};
82 if (this.buttonsConfiguration
83 && this.buttonsConfiguration.formatblock
84 && this.buttonsConfiguration.formatblock.items) {
85 this.formatBlockItems = this.buttonsConfiguration.formatblock.items;
86 }
87 // Build lists of mutually exclusive class names
88 for (var tagName in this.formatBlockItems) {
89 if (this.formatBlockItems.hasOwnProperty(tagName) && this.formatBlockItems[tagName].tagName && this.formatBlockItems[tagName].addClass) {
90 if (!this.formatBlockItems[this.formatBlockItems[tagName].tagName]) {
91 this.formatBlockItems[this.formatBlockItems[tagName].tagName] = {};
92 }
93 if (!this.formatBlockItems[this.formatBlockItems[tagName].tagName].classList) {
94 this.formatBlockItems[this.formatBlockItems[tagName].tagName].classList = new Array();
95 }
96 this.formatBlockItems[this.formatBlockItems[tagName].tagName].classList.push(this.formatBlockItems[tagName].addClass);
97 }
98 }
99 for (var tagName in this.formatBlockItems) {
100 if (this.formatBlockItems.hasOwnProperty(tagName) && this.formatBlockItems[tagName].classList) {
101 this.formatBlockItems[tagName].classList = new RegExp( "^(" + this.formatBlockItems[tagName].classList.join("|") + ")$");
102 }
103 }
104
105 /*
106 * Registering plugin "About" information
107 */
108 var pluginInformation = {
109 version : "1.4",
110 developer : "Stanislas Rolland",
111 developerUrl : "http://www.sjbr.ca/",
112 copyrightOwner : "Stanislas Rolland",
113 sponsor : this.localize("Technische Universitat Ilmenau"),
114 sponsorUrl : "http://www.tu-ilmenau.de/",
115 license : "GPL"
116 };
117 this.registerPluginInformation(pluginInformation);
118
119 /*
120 * Registering the dropdown list
121 */
122 var buttonId = "FormatBlock";
123 var dropDownConfiguration = {
124 id: buttonId,
125 tooltip: this.localize(buttonId + "-Tooltip"),
126 options: this.buttonsConfiguration.formatblock ? this.buttonsConfiguration.formatblock.options : [],
127 action: "onChange"
128 };
129 if (this.buttonsConfiguration.formatblock) {
130 dropDownConfiguration.width = this.buttonsConfiguration.formatblock.width ? parseInt(this.buttonsConfiguration.formatblock.width, 10) : 200;
131 if (this.buttonsConfiguration.formatblock.listWidth) {
132 dropDownConfiguration.listWidth = parseInt(this.buttonsConfiguration.formatblock.listWidth, 10);
133 }
134 if (this.buttonsConfiguration.formatblock.maxHeight) {
135 dropDownConfiguration.maxHeight = parseInt(this.buttonsConfiguration.formatblock.maxHeight, 10);
136 }
137 }
138 this.registerDropDown(dropDownConfiguration);
139 /*
140 * Establishing the list of allowed block elements
141 */
142 var blockElements = new Array();
143 Ext.each(dropDownConfiguration.options, function (option) {
144 if (option[1] != 'none') {
145 blockElements.push(option[1]);
146 }
147 });
148 this.allowedBlockElements = new RegExp( "^(" + blockElements.join("|") + ")$", "i");
149
150 /*
151 * Registering hot keys for the dropdown list items
152 */
153 Ext.each(blockElements, function (blockElement) {
154 var configuredHotKey = this.defaultHotKeys[blockElement];
155 if (this.editorConfiguration.buttons.formatblock
156 && this.editorConfiguration.buttons.formatblock.items
157 && this.editorConfiguration.buttons.formatblock.items[blockElement]
158 && this.editorConfiguration.buttons.formatblock.items[blockElement].hotKey) {
159 configuredHotKey = this.editorConfiguration.buttons.formatblock.items[blockElement].hotKey;
160 }
161 if (configuredHotKey) {
162 var hotKeyConfiguration = {
163 id : configuredHotKey,
164 cmd : buttonId,
165 element : blockElement
166 };
167 this.registerHotKey(hotKeyConfiguration);
168 }
169 }, this);
170 /*
171 * Registering the buttons
172 */
173 for (var buttonId in this.buttonList) {
174 if (this.buttonList.hasOwnProperty(buttonId)) {
175 var button = this.buttonList[buttonId];
176 var buttonConfiguration = {
177 id : buttonId,
178 tooltip : this.localize(buttonId + '-Tooltip') || this.localize(button[2]),
179 iconCls : 'htmlarea-action-' + button[3],
180 contextMenuTitle: this.localize(buttonId + '-contextMenuTitle'),
181 action : 'onButtonPress',
182 hotKey : ((this.buttonsConfiguration[button[2]] && this.buttonsConfiguration[button[2]].hotKey) ? this.buttonsConfiguration[button[2]].hotKey : (button[1] ? button[1] : null))
183 };
184 this.registerButton(buttonConfiguration);
185 }
186 }
187 return true;
188 },
189 /*
190 * The list of buttons added by this plugin
191 */
192 buttonList: {
193 Indent : [null, 'TAB', 'indent', 'indent'],
194 Outdent : [null, 'SHIFT-TAB', 'outdent', 'outdent'],
195 Blockquote : [null, null, 'blockquote', 'blockquote'],
196 InsertParagraphBefore : [null, null, 'insertparagraphbefore', 'paragraph-insert-before'],
197 InsertParagraphAfter : [null, null, 'insertparagraphafter', 'paragraph-insert-after'],
198 JustifyLeft : [null, 'l', 'left', 'justify-left'],
199 JustifyCenter : [null, 'e', 'center', 'justify-center'],
200 JustifyRight : [null, 'r', 'right', 'justify-right'],
201 JustifyFull : [null, 'j', 'justifyfull', 'justify-full'],
202 InsertOrderedList : [null, null, 'orderedlist', 'ordered-list'],
203 InsertUnorderedList : [null, null, 'unorderedlist', 'unordered-list'],
204 InsertHorizontalRule : [null, null, 'inserthorizontalrule', 'horizontal-rule-insert']
205 },
206 /*
207 * The list of hotkeys associated with block elements and registered by default by this plugin
208 */
209 defaultHotKeys: {
210 'p' : 'n',
211 'h1' : '1',
212 'h2' : '2',
213 'h3' : '3',
214 'h4' : '4',
215 'h5' : '5',
216 'h6' : '6'
217 },
218 /*
219 * The function returns true if the type of block element is allowed in the current configuration
220 */
221 isAllowedBlockElement : function (blockName) {
222 return this.allowedBlockElements.test(blockName);
223 },
224
225 /*
226 * This function adds an attribute to the array of attributes allowed on block elements
227 *
228 * @param string attribute: the name of the attribute to be added to the array
229 *
230 * @return void
231 */
232 addAllowedAttribute : function (attribute) {
233 this.allowedAttributes.push(attribute);
234 },
235
236 /*
237 * This function gets called when some block element was selected in the drop-down list
238 */
239 onChange : function (editor, combo, record, index) {
240 this.applyBlockElement(combo.itemId, combo.getValue());
241 },
242
243 applyBlockElement : function(buttonId, blockElement) {
244 var tagName = blockElement;
245 var className = null;
246 if (this.formatBlockItems[tagName]) {
247 if (this.formatBlockItems[tagName].addClass) {
248 className = this.formatBlockItems[tagName].addClass;
249 }
250 if (this.formatBlockItems[tagName].tagName) {
251 tagName = this.formatBlockItems[tagName].tagName;
252 }
253 }
254 if (this.standardBlockElements.test(tagName) || tagName == "none") {
255 switch (tagName) {
256 case "blockquote" :
257 this.onButtonPress(this.editor, "Blockquote", null, className);
258 break;
259 case "div" :
260 case "address" :
261 case "none" :
262 this.onButtonPress(this.editor, tagName, null, className);
263 break;
264 default :
265 var element = tagName;
266 if (Ext.isIE) {
267 element = "<" + element + ">";
268 }
269 this.editor.focus();
270 if (Ext.isWebKit && !this.editor._doc.body.hasChildNodes()) {
271 this.editor._doc.body.appendChild((this.editor._doc.createElement("br")));
272 }
273 try {
274 this.editor._doc.execCommand(buttonId, false, element);
275 } catch(e) {
276 this.appendToLog("applyBlockElement", e + "\n\nby execCommand(" + buttonId + ");");
277 }
278 this.addClassOnBlockElements(tagName, className);
279 }
280 }
281 },
282
283 /*
284 * This function gets called when a button was pressed.
285 *
286 * @param object editor: the editor instance
287 * @param string id: the button id or the key
288 * @param object target: the target element of the contextmenu event, when invoked from the context menu
289 * @param string className: the className to be assigned to the element
290 *
291 * @return boolean false if action is completed
292 */
293 onButtonPress : function (editor, id, target, className) {
294 // Could be a button or its hotkey
295 var buttonId = this.translateHotKey(id);
296 buttonId = buttonId ? buttonId : id;
297 this.editor.focus();
298 var selection = editor._getSelection();
299 var range = editor._createRange(selection);
300 var statusBarSelection = this.editor.statusBar ? this.editor.statusBar.getSelection() : null;
301 var parentElement = statusBarSelection ? statusBarSelection : this.editor.getParentElement(selection, range);
302 if (target) {
303 parentElement = target;
304 }
305 while (parentElement && (!HTMLArea.isBlockElement(parentElement) || /^li$/i.test(parentElement.nodeName))) {
306 parentElement = parentElement.parentNode;
307 }
308 var blockAncestors = this.getBlockAncestors(parentElement);
309 var tableCell = null;
310 if (id === "TAB" || id === "SHIFT-TAB") {
311 for (var i = blockAncestors.length; --i >= 0;) {
312 if (/^(td|th)$/i.test(blockAncestors[i].nodeName)) {
313 tableCell = blockAncestors[i];
314 break;
315 }
316 }
317 }
318 var fullNodeTextSelected = (!Ext.isIE && parentElement.textContent === range.toString()) || (Ext.isIE && parentElement.innerText === range.text);
319 switch (buttonId) {
320 case "Indent" :
321 if (/^(ol|ul)$/i.test(parentElement.nodeName) && !(fullNodeTextSelected && !/^(li)$/i.test(parentElement.parentNode.nodeName))) {
322 if (Ext.isOpera) {
323 try {
324 this.editor._doc.execCommand(buttonId, false, null);
325 } catch(e) {
326 this.appendToLog("onButtonPress", e + "\n\nby execCommand(" + buttonId + ");");
327 }
328 this.indentedList = parentElement;
329 this.makeNestedList(parentElement);
330 this.editor.selectNodeContents(this.indentedList.lastChild, false);
331 } else {
332 this.indentSelectedListElements(parentElement, range);
333 }
334 } else if (tableCell) {
335
336 var tablePart = tableCell.parentNode.parentNode;
337 // Get next cell in same table part
338 var nextCell = tableCell.nextSibling ? tableCell.nextSibling : (tableCell.parentNode.nextSibling ? tableCell.parentNode.nextSibling.cells[0] : null);
339 // Next cell is in other table part
340 if (!nextCell) {
341 switch (tablePart.nodeName.toLowerCase()) {
342 case "thead":
343 nextCell = tablePart.parentNode.tBodies[0].rows[0].cells[0];
344 break;
345 case "tbody":
346 nextCell = tablePart.nextSibling ? tablePart.nextSibling.rows[0].cells[0] : null;
347 break;
348 case "tfoot":
349 this.editor.selectNodeContents(tablePart.parentNode.lastChild.lastChild.lastChild, true);
350 }
351 }
352 if (!nextCell) {
353 if (this.editor.plugins.TableOperations) {
354 this.editor.plugins.TableOperations.instance.onButtonPress(this.editor, "TO-row-insert-under");
355 } else {
356 nextCell = tablePart.parentNode.rows[0].cells[0];
357 }
358 }
359 if (nextCell) {
360 if (Ext.isOpera && !nextCell.hasChildNodes()) {
361 nextCell.appendChild(this.editor.document.createElement('br'));
362 }
363 this.editor.selectNodeContents(nextCell, true);
364 }
365 } else if (this.useBlockquote) {
366 try {
367 this.editor._doc.execCommand(buttonId, false, null);
368 } catch(e) {
369 this.appendToLog("onButtonPress", e + "\n\nby execCommand(" + buttonId + ");");
370 }
371 } else if (this.isAllowedBlockElement("div")) {
372 if (/^div$/i.test(parentElement.nodeName) && !HTMLArea._hasClass(parentElement, this.useClass[buttonId])) {
373 HTMLArea._addClass(parentElement, this.useClass[buttonId]);
374 } else if (!/^div$/i.test(parentElement.nodeName) && /^div$/i.test(parentElement.parentNode.nodeName) && !HTMLArea._hasClass(parentElement.parentNode, this.useClass[buttonId])) {
375 HTMLArea._addClass(parentElement.parentNode, this.useClass[buttonId]);
376 } else {
377 var bookmark = this.editor.getBookmark(range);
378 var newBlock = this.wrapSelectionInBlockElement("div", this.useClass[buttonId], null, true);
379 this.editor.selectRange(this.editor.moveToBookmark(bookmark));
380 }
381 } else {
382 this.addClassOnBlockElements(buttonId);
383 }
384 break;
385 case "Outdent" :
386 if (/^(ol|ul)$/i.test(parentElement.nodeName) && !HTMLArea._hasClass(parentElement, this.useClass.Indent)) {
387 if (/^(li)$/i.test(parentElement.parentNode.nodeName)) {
388 if (Ext.isOpera) {
389 try {
390 this.editor._doc.execCommand(buttonId, false, null);
391 } catch(e) {
392 this.appendToLog("onButtonPress", e + "\n\nby execCommand(" + buttonId + ");");
393 }
394 } else {
395 this.outdentSelectedListElements(parentElement, range);
396 }
397 }
398 } else if (tableCell) {
399 var previousCell = tableCell.previousSibling ? tableCell.previousSibling : (tableCell.parentNode.previousSibling ? tableCell.parentNode.previousSibling.lastChild : null);
400 if (!previousCell) {
401 var table = tableCell.parentNode.parentNode.parentNode;
402 var tablePart = tableCell.parentNode.parentNode.nodeName.toLowerCase();
403 switch (tablePart) {
404 case "tbody":
405 if (table.tHead) {
406 previousCell = table.tHead.rows[table.tHead.rows.length-1].cells[table.tHead.rows[table.tHead.rows.length-1].cells.length-1];
407 break;
408 }
409 case "thead":
410 if (table.tFoot) {
411 previousCell = table.tFoot.rows[table.tFoot.rows.length-1].cells[table.tFoot.rows[table.tFoot.rows.length-1].cells.length-1];
412 break;
413 }
414 case "tfoot":
415 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];
416 }
417 }
418 if (previousCell) {
419 if (Ext.isOpera && !previousCell.hasChildNodes()) {
420 previousCell.appendChild(this.editor.document.createElement('br'));
421 }
422 this.editor.selectNodeContents(previousCell, true);
423 }
424 } else if (this.useBlockquote) {
425 try {
426 this.editor._doc.execCommand(buttonId, false, null);
427 } catch(e) {
428 this.appendToLog("onButtonPress", e + "\n\nby execCommand(" + buttonId + ");");
429 }
430 } else if (this.isAllowedBlockElement("div")) {
431 for (var i = blockAncestors.length; --i >= 0;) {
432 if (HTMLArea._hasClass(blockAncestors[i], this.useClass.Indent)) {
433 var bookmark = this.editor.getBookmark(range);
434 var newBlock = this.wrapSelectionInBlockElement("div", false, blockAncestors[i]);
435 // If not directly under the div, we need to backtrack
436 if (newBlock.parentNode !== blockAncestors[i]) {
437 var parent = newBlock.parentNode;
438 this.removeElement(newBlock);
439 while (parent.parentNode !== blockAncestors[i]) {
440 parent = parent.parentNode;
441 }
442 blockAncestors[i].insertBefore(newBlock, parent);
443 newBlock.appendChild(parent);
444 }
445 newBlock.className = blockAncestors[i].className;
446 HTMLArea._removeClass(newBlock, this.useClass.Indent);
447 if (!newBlock.previousSibling) {
448 while (newBlock.hasChildNodes()) {
449 if (newBlock.firstChild.nodeType == 1) {
450 newBlock.firstChild.className = newBlock.className;
451 }
452 blockAncestors[i].parentNode.insertBefore(newBlock.firstChild, blockAncestors[i]);
453 }
454 } else if (!newBlock.nextSibling) {
455 if (blockAncestors[i].nextSibling) {
456 while (newBlock.hasChildNodes()) {
457 if (newBlock.firstChild.nodeType == 1) {
458 newBlock.lastChild.className = newBlock.className;
459 }
460 blockAncestors[i].parentNode.insertBefore(newBlock.lastChild, blockAncestors[i].nextSibling);
461 }
462 } else {
463 while (newBlock.hasChildNodes()) {
464 if (newBlock.firstChild.nodeType == 1) {
465 newBlock.firstChild.className = newBlock.className;
466 }
467 blockAncestors[i].parentNode.appendChild(newBlock.firstChild);
468 }
469 }
470 } else {
471 var clone = blockAncestors[i].cloneNode(false);
472 if (blockAncestors[i].nextSibling) {
473 blockAncestors[i].parentNode.insertBefore(clone, blockAncestors[i].nextSibling);
474 } else {
475 blockAncestors[i].parentNode.appendChild(clone);
476 }
477 while (newBlock.nextSibling) {
478 clone.appendChild(newBlock.nextSibling);
479 }
480 while (newBlock.hasChildNodes()) {
481 if (newBlock.firstChild.nodeType == 1) {
482 newBlock.firstChild.className = newBlock.className;
483 }
484 blockAncestors[i].parentNode.insertBefore(newBlock.firstChild, clone);
485 }
486 }
487 blockAncestors[i].removeChild(newBlock);
488 if (!blockAncestors[i].hasChildNodes()) {
489 blockAncestors[i].parentNode.removeChild(blockAncestors[i]);
490 }
491 this.editor.selectRange(this.editor.moveToBookmark(bookmark));
492 break;
493 }
494 }
495 } else {
496 this.addClassOnBlockElements(buttonId);
497 }
498 break;
499 case "InsertParagraphBefore" :
500 case "InsertParagraphAfter" :
501 this.insertParagraph(buttonId === "InsertParagraphAfter");
502 break;
503 case "Blockquote" :
504 var commandState = false;
505 for (var i = blockAncestors.length; --i >= 0;) {
506 if (/^blockquote$/i.test(blockAncestors[i].nodeName)) {
507 commandState = true;
508 this.removeElement(blockAncestors[i]);
509 break;
510 }
511 }
512 if (!commandState) {
513 var bookmark = this.editor.getBookmark(range);
514 var newBlock = this.wrapSelectionInBlockElement("blockquote", className, null, true);
515 this.editor.selectRange(this.editor.moveToBookmark(bookmark));
516 }
517 break;
518 case "address" :
519 case "div" :
520 var bookmark = this.editor.getBookmark(range);
521 var newBlock = this.wrapSelectionInBlockElement(buttonId, className, null, true);
522 this.editor.selectRange(this.editor.moveToBookmark(bookmark));
523 break;
524 case "JustifyLeft" :
525 case "JustifyCenter" :
526 case "JustifyRight" :
527 case "JustifyFull" :
528 if (this.useAlignAttribute) {
529 try {
530 this.editor._doc.execCommand(buttonId, false, null);
531 } catch(e) {
532 this.appendToLog("onButtonPress", e + "\n\nby execCommand(" + buttonId + ");");
533 }
534 } else {
535 this.addClassOnBlockElements(buttonId);
536 }
537 break;
538 case "InsertOrderedList":
539 case "InsertUnorderedList":
540 this.insertList(buttonId, parentElement);
541 break;
542 case "InsertHorizontalRule":
543 this.editor.execCommand('InsertHorizontalRule');
544 break;
545 case "none" :
546 if (this.isAllowedBlockElement(parentElement.nodeName)) {
547 this.removeElement(parentElement);
548 }
549 break;
550 default :
551 break;
552 }
553 return false;
554 },
555
556 /*
557 * Get the block ancestors of an element within a given block
558 */
559 getBlockAncestors : function(element, withinBlock) {
560 var ancestors = new Array();
561 var ancestor = element;
562 while (ancestor && (ancestor.nodeType === 1) && !/^(body)$/i.test(ancestor.nodeName) && ancestor != withinBlock) {
563 if (HTMLArea.isBlockElement(ancestor)) {
564 ancestors.unshift(ancestor);
565 }
566 ancestor = ancestor.parentNode;
567 }
568 ancestors.unshift(ancestor);
569 return ancestors;
570 },
571
572 /*
573 * This function wraps the block elements intersecting the current selection in a block element of the given type
574 *
575 * @param string blockName: the type of element to be used as wrapping block
576 * @param string useClass: a class to be assigned to the wrapping block
577 * @param object withinBlock: only elements contained in this block will be wrapped
578 * @param boolean keepValid: make only valid wraps (working wraps may produce temporary invalid hierarchies)
579 *
580 * @return object the wrapping block
581 */
582 wrapSelectionInBlockElement : function(blockName, useClass, withinBlock, keepValid) {
583 var endBlocks = this.editor.getEndBlocks(this.editor._getSelection());
584 var startAncestors = this.getBlockAncestors(endBlocks.start, withinBlock);
585 var endAncestors = this.getBlockAncestors(endBlocks.end, withinBlock);
586 var i = 0;
587 while (i < startAncestors.length && i < endAncestors.length && startAncestors[i] === endAncestors[i]) {
588 ++i;
589 }
590
591 if ((endBlocks.start === endBlocks.end && /^(body)$/i.test(endBlocks.start.nodeName)) || !startAncestors[i] || !endAncestors[i]) {
592 --i;
593 }
594 if (keepValid) {
595 if (endBlocks.start === endBlocks.end) {
596 while (i && /^(thead|tbody|tfoot|tr|dt)$/i.test(startAncestors[i].nodeName)) {
597 --i;
598 }
599 } else {
600 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))) {
601 --i;
602 }
603 }
604 }
605 var blockElement = this.editor._doc.createElement(blockName);
606 if (useClass) {
607 HTMLArea._addClass(blockElement, useClass);
608 }
609 var contextElement = endAncestors[0];
610 if (i) {
611 contextElement = endAncestors[i-1];
612 }
613 var nextElement = endAncestors[i].nextSibling;
614 var block = startAncestors[i], sibling;
615 if ((!/^(body|td|th|li|dd)$/i.test(block.nodeName) || /^(ol|ul|dl)$/i.test(blockName)) && block != withinBlock) {
616 while (block && block != nextElement) {
617 sibling = block.nextSibling;
618 blockElement.appendChild(block);
619 block = sibling;
620 }
621 if (nextElement) {
622 blockElement = nextElement.parentNode.insertBefore(blockElement, nextElement);
623 } else {
624 blockElement = contextElement.appendChild(blockElement);
625 }
626 } else {
627 contextElement = block;
628 block = block.firstChild;
629 while (block) {
630 sibling = block.nextSibling;
631 blockElement.appendChild(block);
632 block = sibling;
633 }
634 blockElement = contextElement.appendChild(blockElement);
635 }
636 // Things go wrong in some browsers when the node is empty
637 if (Ext.isWebKit && !blockElement.hasChildNodes()) {
638 blockElement = blockElement.appendChild(this.editor._doc.createElement("br"));
639 }
640 return blockElement;
641 },
642
643 /*
644 * This function adds a class attribute on blocks sibling of the block containing the start container of the selection
645 */
646 addClassOnBlockElements : function(buttonId, className) {
647 var selection = this.editor._getSelection();
648 var endBlocks = this.editor.getEndBlocks(selection);
649 var startAncestors = this.getBlockAncestors(endBlocks.start);
650 var endAncestors = this.getBlockAncestors(endBlocks.end);
651 var index = 0;
652 while (index < startAncestors.length && index < endAncestors.length && startAncestors[index] === endAncestors[index]) {
653 ++index;
654 }
655 if (endBlocks.start === endBlocks.end) {
656 --index;
657 }
658 for (var block = startAncestors[index]; block; block = block.nextSibling) {
659 if (HTMLArea.isBlockElement(block)) {
660 switch (buttonId) {
661 case "Indent" :
662 if (!HTMLArea._hasClass(block, this.useClass[buttonId])) {
663 HTMLArea._addClass(block, this.useClass[buttonId]);
664 }
665 break;
666 case "Outdent" :
667 if (HTMLArea._hasClass(block, this.useClass["Indent"])) {
668 HTMLArea._removeClass(block, this.useClass["Indent"]);
669 }
670 break;
671 case "JustifyLeft" :
672 case "JustifyCenter" :
673 case "JustifyRight" :
674 case "JustifyFull" :
675 this.toggleAlignmentClass(block, buttonId);
676 break;
677 default :
678 if (this.standardBlockElements.test(buttonId.toLowerCase()) && buttonId.toLowerCase() == block.nodeName.toLowerCase()) {
679 this.cleanClasses(block);
680 if (className) {
681 HTMLArea._addClass(block, className);
682 }
683 }
684 break;
685 }
686 }
687 if (block == endAncestors[index]) {
688 break;
689 }
690 }
691 },
692
693 /*
694 * This function toggles the alignment class on the given block
695 */
696 toggleAlignmentClass : function(block, buttonId) {
697 for (var alignmentButtonId in this.useClass) {
698 if (this.useClass.hasOwnProperty(alignmentButtonId) && alignmentButtonId !== "Indent") {
699 if (HTMLArea._hasClass(block, this.useClass[alignmentButtonId])) {
700 HTMLArea._removeClass(block, this.useClass[alignmentButtonId]);
701 } else if (alignmentButtonId === buttonId) {
702 HTMLArea._addClass(block, this.useClass[alignmentButtonId]);
703 }
704 }
705 }
706 if (/^div$/i.test(block.nodeName) && !this.hasAllowedAttributes(block)) {
707 this.removeElement(block);
708 }
709 },
710
711 /*
712 * This function verifies if the element has any of the allowed attributes
713 */
714 hasAllowedAttributes : function(element) {
715 for (var i = 0; i < this.allowedAttributes.length; ++i) {
716 if (element.getAttribute(this.allowedAttributes[i])) {
717 return true;
718 }
719 }
720 return false;
721 },
722
723 /*
724 * This function removes the given element but keeps its contents
725 */
726 removeElement : function(element) {
727 var selection = this.editor._getSelection();
728 var range = this.editor._createRange(selection);
729 var lastChild;
730 var bookmark = this.editor.getBookmark(range);
731 var parent = element.parentNode;
732 while (element.firstChild) {
733 lastChild = parent.insertBefore(element.firstChild, element);
734 }
735 parent.removeChild(element);
736 var range = this.editor.moveToBookmark(bookmark);
737 this.editor.selectRange(range);
738 },
739
740 insertList : function (buttonId, parentElement) {
741 if (/^(dd)$/i.test(parentElement.nodeName)) {
742 var list = parentElement.appendChild(this.editor._doc.createElement((buttonId === "OrderedList") ? "ol" : "ul"));
743 var first = list.appendChild(this.editor._doc.createElement("li"));
744 first.innerHTML = "<br />";
745 this.editor.selectNodeContents(first,true);
746 } else {
747 // parentElement may be removed by following command
748 var parentNode = parentElement.parentNode;
749 try {
750 this.editor._doc.execCommand(buttonId, false, null);
751 } catch(e) {
752 this.appendToLog("onButtonPress", e + "\n\nby execCommand(" + buttonId + ");");
753 }
754 if (Ext.isWebKit) {
755 this.editor.cleanAppleStyleSpans(parentNode);
756 }
757 }
758 },
759
760 /*
761 * Indent selected list elements
762 */
763 indentSelectedListElements : function (list, range) {
764 var bookmark = this.editor.getBookmark(range);
765 // The selected elements are wrapped into a list element
766 var indentedList = this.wrapSelectionInBlockElement(list.nodeName.toLowerCase(), null, list);
767 // which breaks the range
768 var range = this.editor.moveToBookmark(bookmark);
769 bookmark = this.editor.getBookmark(range);
770
771 // Check if the last element has children. If so, outdent those that do not intersect the selection
772 var last = indentedList.lastChild.lastChild;
773 if (last && /^(ol|ul)$/i.test(last.nodeName)) {
774 var child = last.firstChild, next;
775 while (child) {
776 next = child.nextSibling;
777 if (!this.editor.rangeIntersectsNode(range, child)) {
778 indentedList.appendChild(child);
779 }
780 child = next;
781 }
782 if (!last.hasChildNodes()) {
783 HTMLArea.removeFromParent(last);
784 }
785 }
786 if (indentedList.previousSibling && indentedList.previousSibling.hasChildNodes()) {
787 // Indenting some elements not including the first one
788 if (/^(ol|ul)$/i.test(indentedList.previousSibling.lastChild.nodeName)) {
789 // Some indented elements exist just above our selection
790 // Moving to regroup with these elements
791 while (indentedList.hasChildNodes()) {
792 indentedList.previousSibling.lastChild.appendChild(indentedList.firstChild);
793 }
794 list.removeChild(indentedList);
795 } else {
796 indentedList = indentedList.previousSibling.appendChild(indentedList);
797 }
798 } else {
799 // Indenting the first element and possibly some more
800 var first = this.editor._doc.createElement("li");
801 first.innerHTML = "&nbsp;";
802 list.insertBefore(first, indentedList);
803 indentedList = first.appendChild(indentedList);
804 }
805 this.editor.selectRange(this.editor.moveToBookmark(bookmark));
806 },
807
808 /*
809 * Outdent selected list elements
810 */
811 outdentSelectedListElements : function (list, range) {
812 // We wrap the selected li elements and thereafter move them one level up
813 var bookmark = this.editor.getBookmark(range);
814 var wrappedList = this.wrapSelectionInBlockElement(list.nodeName.toLowerCase(), null, list);
815 // which breaks the range
816 var range = this.editor.moveToBookmark(bookmark);
817 bookmark = this.editor.getBookmark(range);
818
819 if (!wrappedList.previousSibling) {
820 // Outdenting the first element(s) of an indented list
821 var next = list.parentNode.nextSibling;
822 var last = wrappedList.lastChild;
823 while (wrappedList.hasChildNodes()) {
824 if (next) {
825 list.parentNode.parentNode.insertBefore(wrappedList.firstChild, next);
826 } else {
827 list.parentNode.parentNode.appendChild(wrappedList.firstChild);
828 }
829 }
830 list.removeChild(wrappedList);
831 last.appendChild(list);
832 } else if (!wrappedList.nextSibling) {
833 // Outdenting the last element(s) of the list
834 // This will break the gecko bookmark
835 this.editor.moveToBookmark(bookmark);
836 while (wrappedList.hasChildNodes()) {
837 if (list.parentNode.nextSibling) {
838 list.parentNode.parentNode.insertBefore(wrappedList.firstChild, list.parentNode.nextSibling);
839 } else {
840 list.parentNode.parentNode.appendChild(wrappedList.firstChild);
841 }
842 }
843 list.removeChild(wrappedList);
844 this.editor.selectNodeContents(list.parentNode.nextSibling, true);
845 bookmark = this.editor.getBookmark(this.editor._createRange(this.editor._getSelection()));
846 } else {
847 // Outdenting the middle of a list
848 var next = list.parentNode.nextSibling;
849 var last = wrappedList.lastChild;
850 var sibling = wrappedList.nextSibling;
851 while (wrappedList.hasChildNodes()) {
852 if (next) {
853 list.parentNode.parentNode.insertBefore(wrappedList.firstChild, next);
854 } else {
855 list.parentNode.parentNode.appendChild(wrappedList.firstChild);
856 }
857 }
858 while (sibling) {
859 wrappedList.appendChild(sibling);
860 sibling = sibling.nextSibling;
861 }
862 last.appendChild(wrappedList);
863 }
864 // Remove the list if all its elements have been moved up
865 if (!list.hasChildNodes()) {
866 list.parentNode.removeChild(list);
867 }
868 this.editor.selectRange(this.editor.moveToBookmark(bookmark));
869 },
870
871 /*
872 * Make XHTML-compliant nested list
873 * We need this for Opera
874 */
875 makeNestedList : function(el) {
876 var previous;
877 for (var i = el.firstChild; i; i = i.nextSibling) {
878 if (/^li$/i.test(i.nodeName)) {
879 for (var j = i.firstChild; j; j = j.nextSibling) {
880 if (/^(ol|ul)$/i.test(j.nodeName)) {
881 this.makeNestedList(j);
882 }
883 }
884 } else if (/^(ol|ul)$/i.test(i.nodeName)) {
885 previous = i.previousSibling;
886 this.indentedList = i.cloneNode(true);
887 if (!previous) {
888 previous = el.insertBefore(this.editor._doc.createElement("li"), i);
889 this.indentedList = previous.appendChild(this.indentedList);
890 } else {
891 this.indentedList = previous.appendChild(this.indentedList);
892 }
893 HTMLArea.removeFromParent(i);
894 this.makeNestedList(el);
895 break;
896 }
897 }
898 },
899
900 /*
901 * Insert a paragraph
902 */
903 insertParagraph : function(after) {
904 var endBlocks = this.editor.getEndBlocks(this.editor._getSelection());
905 var ancestors = after ? this.getBlockAncestors(endBlocks.end) : this.getBlockAncestors(endBlocks.start);
906 var endElement = ancestors[ancestors.length-1];
907 for (var i = ancestors.length; --i >= 0;) {
908 if (/^(table|div|ul|ol|dl|blockquote|address|pre)$/i.test(ancestors[i].nodeName) && !/^(li)$/i.test(ancestors[i].parentNode.nodeName)) {
909 endElement = ancestors[i];
910 break;
911 }
912 }
913 if (endElement) {
914 var parent = endElement.parentNode;
915 var paragraph = this.editor._doc.createElement("p");
916 if (Ext.isIE) {
917 paragraph.innerHTML = "&nbsp";
918 } else {
919 paragraph.appendChild(this.editor._doc.createElement("br"));
920 }
921 if (after && !endElement.nextSibling) {
922 parent.appendChild(paragraph);
923 } else {
924 parent.insertBefore(paragraph, after ? endElement.nextSibling : endElement);
925 }
926 this.editor.selectNodeContents(paragraph, true);
927 }
928 },
929 /*
930 * This function gets called when the plugin is generated
931 */
932 onGenerate: function () {
933 // Register the enter key handler for IE when the cursor is at the end of a dt or a dd element
934 if (Ext.isIE) {
935 this.editor.iframe.keyMap.addBinding({
936 key: Ext.EventObject.ENTER,
937 shift: false,
938 handler: this.onKey,
939 scope: this
940 });
941 }
942 },
943 /*
944 * This function gets called when the enter key was pressed in IE
945 * It will process the enter key for IE when the cursor is at the end of a dt or a dd element
946 *
947 * @param string key: the key code
948 * @param object event: the Ext event object (keydown)
949 *
950 * @return boolean false, if the event was taken care of
951 */
952 onKey: function (key, event) {
953 var selection = this.editor._getSelection();
954 if (this.editor._selectionEmpty(selection)) {
955 var range = this.editor._createRange(selection);
956 var parentElement = this.editor.getParentElement(selection, range);
957 while (parentElement && !HTMLArea.isBlockElement(parentElement)) {
958 parentElement = parentElement.parentNode;
959 }
960 if (/^(dt|dd)$/i.test(parentElement.nodeName)) {
961 var nodeRange = this.editor._createRange();
962 nodeRange.moveToElementText(parentElement);
963 range.setEndPoint("EndToEnd", nodeRange);
964 if (!range.text || range.text == "\x20") {
965 var item = parentElement.parentNode.insertBefore(this.editor._doc.createElement((parentElement.nodeName.toLowerCase() === "dt") ? "dd" : "dt"), parentElement.nextSibling);
966 item.innerHTML = "\x20";
967 this.editor.selectNodeContents(item, true);
968 event.stopEvent();
969 return false;
970 }
971 } else if (/^(li)$/i.test(parentElement.nodeName)
972 && !parentElement.innerText
973 && parentElement.parentNode.parentNode
974 && /^(dd|td|th)$/i.test(parentElement.parentNode.parentNode.nodeName)) {
975 var item = parentElement.parentNode.parentNode.insertBefore(this.editor._doc.createTextNode("\x20"), parentElement.parentNode.nextSibling);
976 this.editor.selectNodeContents(parentElement.parentNode.parentNode, false);
977 parentElement.parentNode.removeChild(parentElement);
978 event.stopEvent();
979 return false;
980 }
981 }
982 return true;
983 },
984 /*
985 * This function removes any disallowed class or mutually exclusive classes from the class attribute of the node
986 */
987 cleanClasses : function(node) {
988 var classNames = node.className.trim().split(" ");
989 var nodeName = node.nodeName.toLowerCase();
990 for (var i = classNames.length; --i >= 0;) {
991 if (!HTMLArea.reservedClassNames.test(classNames[i])) {
992 if (this.tags && this.tags[nodeName] && this.tags[nodeName].allowedClasses) {
993 if (!this.tags[nodeName].allowedClasses.test(classNames[i])) {
994 HTMLArea._removeClass(node, classNames[i]);
995 }
996 } else if (this.tags && this.tags.all && this.tags.all.allowedClasses) {
997 if (!this.tags.all.allowedClasses.test(classNames[i])) {
998 HTMLArea._removeClass(node, classNames[i]);
999 }
1000 }
1001 if (this.formatBlockItems[nodeName] && this.formatBlockItems[nodeName].classList && this.formatBlockItems[nodeName].classList.test(classNames[i])) {
1002 HTMLArea._removeClass(node, classNames[i]);
1003 }
1004 }
1005 }
1006 },
1007
1008 /*
1009 * This function gets called when the toolbar is updated
1010 */
1011 onUpdateToolbar: function (button, mode, selectionEmpty, ancestors, endPointsInSameBlock) {
1012 if (mode === 'wysiwyg' && this.editor.isEditable()) {
1013 var statusBarSelection = this.editor.statusBar ? this.editor.statusBar.getSelection() : null;
1014 var parentElement = statusBarSelection ? statusBarSelection : this.editor.getParentElement();
1015 if (!/^body$/i.test(parentElement.nodeName)) {
1016 while (parentElement && !HTMLArea.isBlockElement(parentElement) || /^li$/i.test(parentElement.nodeName)) {
1017 parentElement = parentElement.parentNode;
1018 }
1019 var blockAncestors = this.getBlockAncestors(parentElement);
1020 var endBlocks = this.editor.getEndBlocks(this.editor._getSelection());
1021 var startAncestors = this.getBlockAncestors(endBlocks.start);
1022 var endAncestors = this.getBlockAncestors(endBlocks.end);
1023 var index = 0;
1024 while (index < startAncestors.length && index < endAncestors.length && startAncestors[index] === endAncestors[index]) {
1025 ++index;
1026 }
1027 if (endBlocks.start === endBlocks.end || !startAncestors[index]) {
1028 --index;
1029 }
1030 var commandState = false;
1031 switch (button.itemId) {
1032 case 'FormatBlock':
1033 this.updateDropDown(button, blockAncestors[blockAncestors.length-1], startAncestors[index]);
1034 break;
1035 case "Outdent" :
1036 if (this.useBlockquote) {
1037 for (var j = blockAncestors.length; --j >= 0;) {
1038 if (/^blockquote$/i.test(blockAncestors[j].nodeName)) {
1039 commandState = true;
1040 break;
1041 }
1042 }
1043 } else if (/^(ol|ul)$/i.test(parentElement.nodeName)) {
1044 commandState = true;
1045 } else {
1046 for (var j = blockAncestors.length; --j >= 0;) {
1047 if (HTMLArea._hasClass(blockAncestors[j], this.useClass.Indent) || /^(td|th)$/i.test(blockAncestors[j].nodeName)) {
1048 commandState = true;
1049 break;
1050 }
1051 }
1052 }
1053 button.setDisabled(!commandState);
1054 break;
1055 case "Indent" :
1056 break;
1057 case "InsertParagraphBefore" :
1058 case "InsertParagraphAfter" :
1059 button.setDisabled(/^(body)$/i.test(startAncestors[index].nodeName));
1060 break;
1061 case "Blockquote" :
1062 for (var j = blockAncestors.length; --j >= 0;) {
1063 if (/^blockquote$/i.test(blockAncestors[j].nodeName)) {
1064 commandState = true;
1065 break;
1066 }
1067 }
1068 button.setInactive(!commandState);
1069 break;
1070 case "JustifyLeft" :
1071 case "JustifyCenter" :
1072 case "JustifyRight" :
1073 case "JustifyFull" :
1074 if (this.useAlignAttribute) {
1075 try {
1076 commandState = this.editor._doc.queryCommandState(button.itemId);
1077 } catch(e) {
1078 commandState = false;
1079 }
1080 } else {
1081 if (/^(body)$/i.test(startAncestors[index].nodeName)) {
1082 button.setDisabled(true);
1083 } else {
1084 button.setDisabled(false);
1085 commandState = true;
1086 for (var block = startAncestors[index]; block; block = block.nextSibling) {
1087 commandState = commandState && HTMLArea._hasClass(block, this.useClass[button.itemId]);
1088 if (block == endAncestors[index]) {
1089 break;
1090 }
1091 }
1092 }
1093 }
1094 button.setInactive(!commandState);
1095 break;
1096 case "InsertOrderedList":
1097 case "InsertUnorderedList":
1098 try {
1099 commandState = this.editor._doc.queryCommandState(button.itemId);
1100 } catch(e) {
1101 commandState = false;
1102 }
1103 button.setInactive(!commandState);
1104 break;
1105 default :
1106 break;
1107
1108 }
1109 }
1110 }
1111 },
1112
1113 /*
1114 * This function updates the drop-down list of block elements
1115 */
1116 updateDropDown : function(select, deepestBlockAncestor, startAncestor) {
1117 var store = select.getStore();
1118 store.removeAt(0);
1119 var index = -1;
1120 if (deepestBlockAncestor) {
1121 var nodeName = deepestBlockAncestor.nodeName.toLowerCase();
1122 // Could be a custom item ...
1123 index = store.findBy(function(record, id) {
1124 var item = this.formatBlockItems[record.get('value')];
1125 return item && item.tagName == nodeName && item.addClass && HTMLArea._hasClass(deepestBlockAncestor, item.addClass);
1126 }, this);
1127 if (index == -1) {
1128 // ... or a standard one
1129 index = store.findExact('value', nodeName);
1130 }
1131 }
1132 if (index == -1) {
1133 store.insert(0, new store.recordType({
1134 text: this.localize('No block'),
1135 value: 'none'
1136 }));
1137 select.setValue('none');
1138 } else {
1139 store.insert(0, new store.recordType({
1140 text: this.localize('Remove block'),
1141 value: 'none'
1142 }));
1143 select.setValue(store.getAt(index+1).get('value'));
1144 }
1145 },
1146
1147 /*
1148 * This function handles the hotkey events registered on elements of the dropdown list
1149 */
1150 onHotKey : function(editor, key) {
1151 var blockElement;
1152 var hotKeyConfiguration = this.getHotKeyConfiguration(key);
1153 if (hotKeyConfiguration) {
1154 var blockElement = hotKeyConfiguration.element;
1155 }
1156 if (blockElement && this.isAllowedBlockElement(blockElement)) {
1157 this.applyBlockElement(this.translateHotKey(key), blockElement);
1158 return false;
1159 }
1160 return true;
1161 }
1162 });
1163