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