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