61a5bee04bd3699b5523421fc65190b4ecf6e399
[Packages/TYPO3.CMS.git] / typo3 / sysext / rtehtmlarea / htmlarea / plugins / InlineElements / inline-elements.js
1 /***************************************************************
2 * Copyright notice
3 *
4 * (c) 2007-2012 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 * Inline Elements Plugin for TYPO3 htmlArea RTE
29 */
30 /*
31 * Creation of the class of InlineElements plugins
32 */
33 HTMLArea.InlineElements = Ext.extend(HTMLArea.Plugin, {
34 /*
35 * This function gets called by the base constructor
36 */
37 configurePlugin: function (editor) {
38 // Setting the array of allowed attributes on inline elements
39 if (this.getPluginInstance('TextStyle')) {
40 this.allowedAttributes = this.getPluginInstance('TextStyle').allowedAttributes;
41 } else {
42 this.allowedAttributes = new Array('id', 'title', 'lang', 'xml:lang', 'dir', 'class', 'itemscope', 'itemtype', 'itemprop');
43 if (Ext.isIE) {
44 this.addAllowedAttribute('className');
45 }
46 }
47 // Getting tags configuration for inline elements
48 if (this.editorConfiguration.buttons.textstyle) {
49 this.tags = this.editorConfiguration.buttons.textstyle.tags;
50 }
51 /*
52 * Registering plugin "About" information
53 */
54 var pluginInformation = {
55 version : '2.2',
56 developer : 'Stanislas Rolland',
57 developerUrl : 'http://www.sjbr.ca/',
58 copyrightOwner : 'Stanislas Rolland',
59 sponsor : this.localize('Technische Universitat Ilmenau'),
60 sponsorUrl : 'http://www.tu-ilmenau.de/',
61 license : 'GPL'
62 };
63 this.registerPluginInformation(pluginInformation);
64
65 /*
66 * Registering the dropdown list
67 */
68 var buttonId = "FormatText";
69 var dropDownConfiguration = {
70 id : buttonId,
71 tooltip : this.localize(buttonId + "-Tooltip"),
72 options : (this.editorConfiguration.buttons[buttonId.toLowerCase()] ? this.editorConfiguration.buttons[buttonId.toLowerCase()].options : []),
73 action : "onChange"
74 };
75 if (this.editorConfiguration.buttons.formattext) {
76 if (this.editorConfiguration.buttons.formattext.width) {
77 dropDownConfiguration.listWidth = parseInt(this.editorConfiguration.buttons.formattext.width, 10);
78 }
79 if (this.editorConfiguration.buttons.formattext.listWidth) {
80 dropDownConfiguration.listWidth = parseInt(this.editorConfiguration.buttons.formattext.listWidth, 10);
81 }
82 if (this.editorConfiguration.buttons.formattext.maxHeight) {
83 dropDownConfiguration.maxHeight = parseInt(this.editorConfiguration.buttons.formattext.maxHeight, 10);
84 }
85 }
86 this.registerDropDown(dropDownConfiguration);
87
88 /*
89 * Registering the buttons
90 */
91 var n = this.buttonList.length;
92 for (var i = 0; i < n; ++i) {
93 var button = this.buttonList[i];
94 buttonId = button[0];
95 var buttonConfiguration = {
96 id : buttonId,
97 tooltip : this.localize(buttonId + "-Tooltip"),
98 contextMenuTitle: this.localize(buttonId + '-contextMenuTitle'),
99 helpText : this.localize(buttonId + '-helpText'),
100 action : "onButtonPress",
101 context : button[1],
102 hide : false,
103 selection : false,
104 iconCls : 'htmlarea-action-' + button[2]
105 };
106 this.registerButton(buttonConfiguration);
107 }
108 },
109 /*
110 * The list of buttons added by this plugin
111 */
112 buttonList: [
113 ['BiDiOverride', null, 'bidi-override'],
114 ['Big', null, 'big'],
115 ['Bold', null, 'bold'],
116 ['Citation', null, 'citation'],
117 ['Code', null, 'code'],
118 ['Definition', null, 'definition'],
119 ['DeletedText', null, 'deleted-text'],
120 ['Emphasis', null, 'emphasis'],
121 ['InsertedText', null, 'inserted-text'],
122 ['Italic', null, 'italic'],
123 ['Keyboard', null, 'keyboard'],
124 //['Label', null, 'Label'],
125 ['MonoSpaced', null, 'mono-spaced'],
126 ['Quotation', null, 'quotation'],
127 ['Sample', null, 'sample'],
128 ['Small', null, 'small'],
129 ['Span', null, 'span'],
130 ['StrikeThrough', null, 'strike-through'],
131 ['Strong', null, 'strong'],
132 ['Subscript', null, 'subscript'],
133 ['Superscript', null, 'superscript'],
134 ['Underline', null, 'underline'],
135 ['Variable', null, 'variable']
136 ],
137 /*
138 * Conversion object: button names to corresponding tag names
139 */
140 convertBtn: {
141 BiDiOverride : 'bdo',
142 Big : 'big',
143 Bold : 'b',
144 Citation : 'cite',
145 Code : 'code',
146 Definition : 'dfn',
147 DeletedText : 'del',
148 Emphasis : 'em',
149 InsertedText : 'ins',
150 Italic : 'i',
151 Keyboard : 'kbd',
152 //Label : 'label',
153 MonoSpaced : 'tt',
154 Quotation : 'q',
155 Sample : 'samp',
156 Small : 'small',
157 Span : 'span',
158 StrikeThrough : 'strike',
159 Strong : 'strong',
160 Subscript : 'sub',
161 Superscript : 'sup',
162 Underline : 'u',
163 Variable : 'var'
164 },
165 /*
166 * Regular expression to check if an element is an inline elment
167 */
168 REInlineElements: /^(b|bdo|big|cite|code|del|dfn|em|i|ins|kbd|label|q|samp|small|span|strike|strong|sub|sup|tt|u|var)$/,
169 /*
170 * Function to check if an element is an inline elment
171 */
172 isInlineElement: function (el) {
173 return el && (el.nodeType === HTMLArea.DOM.ELEMENT_NODE) && this.REInlineElements.test(el.nodeName.toLowerCase());
174 },
175 /*
176 * This function adds an attribute to the array of allowed attributes on inline elements
177 *
178 * @param string attribute: the name of the attribute to be added to the array
179 *
180 * @return void
181 */
182 addAllowedAttribute: function (attribute) {
183 this.allowedAttributes.push(attribute);
184 },
185 /*
186 * This function gets called when some inline element button was pressed.
187 */
188 onButtonPress: function (editor, id) {
189 // Could be a button or its hotkey
190 var buttonId = this.translateHotKey(id);
191 buttonId = buttonId ? buttonId : id;
192 var element = this.convertBtn[buttonId];
193 if (element) {
194 this.applyInlineElement(editor, element);
195 return false;
196 } else {
197 this.appendToLog('onButtonPress', 'No element corresponding to button: ' + buttonId, 'warn');
198 }
199 },
200 /*
201 * This function gets called when some inline element was selected in the drop-down list
202 */
203 onChange: function (editor, combo, record, index) {
204 var element = combo.getValue();
205 this.applyInlineElement(editor, element, false);
206 },
207 /*
208 * This function applies to the selection the markup chosen in the drop-down list or corresponding to the button pressed
209 */
210 applyInlineElement: function (editor, element) {
211 var range = editor.getSelection().createRange();
212 var parent = editor.getSelection().getParentElement();
213 var ancestors = editor.getSelection().getAllAncestors();
214 var elementIsAncestor = false;
215 var fullNodeSelected = false;
216 if (Ext.isIE) {
217 var bookmark = editor.getBookMark().get(range);
218 }
219 // Check if the chosen element is among the ancestors
220 for (var i = 0; i < ancestors.length; ++i) {
221 if ((ancestors[i].nodeType === HTMLArea.DOM.ELEMENT_NODE) && (ancestors[i].nodeName.toLowerCase() == element)) {
222 elementIsAncestor = true;
223 var elementAncestorIndex = i;
224 break;
225 }
226 }
227 if (!editor.getSelection().isEmpty()) {
228 var fullySelectedNode = editor.getSelection().getFullySelectedNode();
229 fullNodeSelected = this.isInlineElement(fullySelectedNode);
230 if (fullNodeSelected) {
231 parent = fullySelectedNode;
232 }
233 var statusBarSelection = (editor.statusBar ? editor.statusBar.getSelection() : null);
234 if (element !== "none" && !(fullNodeSelected && elementIsAncestor)) {
235 // Add markup
236 var newElement = editor.document.createElement(element);
237 if (element === "bdo") {
238 newElement.setAttribute("dir", "rtl");
239 }
240 if (!Ext.isIE) {
241 if (fullNodeSelected && statusBarSelection) {
242 if (Ext.isWebKit) {
243 newElement = parent.parentNode.insertBefore(newElement, statusBarSelection);
244 newElement.appendChild(statusBarSelection);
245 newElement.normalize();
246 } else {
247 range.selectNode(parent);
248 editor.getDomNode().wrapWithInlineElement(newElement, range);
249 }
250 editor.getSelection().selectNodeContents(newElement.lastChild, false);
251 } else {
252 editor.getDomNode().wrapWithInlineElement(newElement, range);
253 }
254 range.detach();
255 } else {
256 var tagopen = "<" + element + ">";
257 var tagclose = "</" + element + ">";
258 if (fullNodeSelected) {
259 if (!statusBarSelection) {
260 parent.innerHTML = tagopen + parent.innerHTML + tagclose;
261 if (element === "bdo") {
262 parent.firstChild.setAttribute("dir", "rtl");
263 }
264 editor.getSelection().selectNodeContents(parent, false);
265 } else {
266 var content = parent.outerHTML;
267 var newElement = this.remapMarkup(parent, element);
268 newElement.innerHTML = content;
269 editor.getSelection().selectNodeContents(newElement, false);
270 }
271 } else {
272 editor.getDomNode().wrapWithInlineElement(newElement, range);
273 }
274 }
275 } else {
276 // A complete node is selected: remove the markup
277 if (fullNodeSelected) {
278 if (elementIsAncestor) {
279 parent = ancestors[elementAncestorIndex];
280 }
281 var parentElement = parent.parentNode;
282 editor.getDomNode().removeMarkup(parent);
283 if (Ext.isWebKit && this.isInlineElement(parentElement)) {
284 editor.getSelection().selectNodeContents(parentElement, false);
285 }
286 }
287 }
288 } else {
289 // Remove or remap markup when the selection is collapsed
290 if (parent && !HTMLArea.DOM.isBlockElement(parent)) {
291 if ((element === 'none') || elementIsAncestor) {
292 if (elementIsAncestor) {
293 parent = ancestors[elementAncestorIndex];
294 }
295 editor.getDomNode().removeMarkup(parent);
296 } else {
297 var bookmark = this.editor.getBookMark().get(range);
298 var newElement = this.remapMarkup(parent, element);
299 this.editor.getSelection().selectRange(this.editor.getBookMark().moveTo(bookmark));
300 }
301 }
302 }
303 },
304 /*
305 * This function remaps the given element to the specified tagname
306 */
307 remapMarkup: function (element, tagName) {
308 var attributeValue;
309 var newElement = HTMLArea.DOM.convertNode(element, tagName);
310 if (tagName === 'bdo') {
311 newElement.setAttribute('dir', 'ltr');
312 }
313 for (var i = 0; i < this.allowedAttributes.length; ++i) {
314 if (attributeValue = element.getAttribute(this.allowedAttributes[i])) {
315 newElement.setAttribute(this.allowedAttributes[i], attributeValue);
316 }
317 }
318 // In IE, the above fails to update the class and style attributes.
319 if (Ext.isIE) {
320 if (element.style.cssText) {
321 newElement.style.cssText = element.style.cssText;
322 }
323 if (element.className) {
324 newElement.setAttribute("class", element.className);
325 if (!newElement.className) {
326 // IE before IE8
327 newElement.setAttribute("className", element.className);
328 }
329 } else {
330 newElement.removeAttribute("class");
331 // IE before IE8
332 newElement.removeAttribute("className");
333 }
334 }
335
336 if (this.tags && this.tags[tagName] && this.tags[tagName].allowedClasses) {
337 if (newElement.className && /\S/.test(newElement.className)) {
338 var allowedClasses = this.tags[tagName].allowedClasses;
339 classNames = newElement.className.trim().split(" ");
340 for (var i = 0; i < classNames.length; ++i) {
341 if (!allowedClasses.test(classNames[i])) {
342 HTMLArea.DOM.removeClass(newElement, classNames[i]);
343 }
344 }
345 }
346 }
347 return newElement;
348 },
349 /*
350 * This function gets called when the toolbar is updated
351 */
352 onUpdateToolbar: function (button, mode, selectionEmpty, ancestors, endPointsInSameBlock) {
353 var editor = this.editor;
354 if (mode === 'wysiwyg' && editor.isEditable()) {
355 var tagName = false,
356 fullNodeSelected = false;
357 var range = editor.getSelection().createRange();
358 var parent = editor.getSelection().getParentElement();
359 if (parent && !HTMLArea.DOM.isBlockElement(parent)) {
360 tagName = parent.nodeName.toLowerCase();
361 }
362 if (!selectionEmpty) {
363 var fullySelectedNode = editor.getSelection().getFullySelectedNode();
364 fullNodeSelected = this.isInlineElement(fullySelectedNode);
365 if (fullNodeSelected) {
366 tagName = fullySelectedNode.nodeName.toLowerCase();
367 }
368 }
369 var selectionInInlineElement = tagName && this.REInlineElements.test(tagName);
370 var disabled = !endPointsInSameBlock || (fullNodeSelected && !tagName) || (selectionEmpty && !selectionInInlineElement);
371 switch (button.itemId) {
372 case 'FormatText':
373 this.updateValue(editor, button, tagName, selectionEmpty, fullNodeSelected, disabled);
374 break;
375 default:
376 var activeButton = false;
377 Ext.each(ancestors, function (ancestor) {
378 if (ancestor && this.convertBtn[button.itemId] === ancestor.nodeName.toLowerCase()) {
379 activeButton = true;
380 return false;
381 } else {
382 return true;
383 }
384 }, this);
385 button.setInactive(!activeButton && this.convertBtn[button.itemId] !== tagName);
386 button.setDisabled(disabled);
387 break;
388 }
389 }
390 },
391 /*
392 * This function updates the drop-down list of inline elemenents
393 */
394 updateValue: function (editor, select, tagName, selectionEmpty, fullNodeSelected, disabled) {
395 var store = select.getStore();
396 store.removeAt(0);
397 if ((store.findExact('value', tagName) != -1) && (selectionEmpty || fullNodeSelected)) {
398 select.setValue(tagName);
399 store.insert(0, new store.recordType({
400 text: this.localize('Remove markup'),
401 value: 'none'
402 }));
403 } else {
404 store.insert(0, new store.recordType({
405 text: this.localize('No markup'),
406 value: 'none'
407 }));
408 select.setValue('none');
409 }
410 select.setDisabled(!(store.getCount()>1) || disabled);
411 }
412 });
413