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