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