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