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