5300a1ee6f19e3a33556067cdf9d04df2765b055
[Packages/TYPO3.CMS.git] / typo3 / sysext / rtehtmlarea / htmlarea / plugins / Language / language.js
1 /***************************************************************
2 * Copyright notice
3 *
4 * (c) 2008-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 * Language Plugin for TYPO3 htmlArea RTE
29 */
30 HTMLArea.Language = Ext.extend(HTMLArea.Plugin, {
31 /*
32 * This function gets called by the class constructor
33 */
34 configurePlugin: function (editor) {
35 /*
36 * Setting up some properties from PageTSConfig
37 */
38 this.buttonsConfiguration = this.editorConfiguration.buttons;
39 this.useAttribute = {};
40 this.useAttribute.lang = (this.buttonsConfiguration.language && this.buttonsConfiguration.language.useLangAttribute) ? this.buttonsConfiguration.language.useLangAttribute : true;
41 this.useAttribute.xmlLang = (this.buttonsConfiguration.language && this.buttonsConfiguration.language.useXmlLangAttribute) ? this.buttonsConfiguration.language.useXmlLangAttribute : false;
42 if (!this.useAttribute.lang && !this.useAttribute.xmlLang) {
43 this.useAttribute.lang = true;
44 }
45
46 // Importing list of allowed attributes
47 if (this.getPluginInstance('TextStyle')) {
48 this.allowedAttributes = this.getPluginInstance('TextStyle').allowedAttributes;
49 }
50 if (!this.allowedAttributes && this.getPluginInstance('InlineElements')) {
51 this.allowedAttributes = this.getPluginInstance('InlineElements').allowedAttributes;
52 }
53 if (!this.allowedAttributes && this.getPluginInstance('BlockElements')) {
54 this.allowedAttributes = this.getPluginInstance('BlockElements').allowedAttributes;
55 }
56 if (!this.allowedAttributes) {
57 this.allowedAttributes = new Array('id', 'title', 'lang', 'xml:lang', 'dir', 'class');
58 if (Ext.isIE) {
59 this.allowedAttributes.push('className');
60 }
61 }
62 /*
63 * Registering plugin "About" information
64 */
65 var pluginInformation = {
66 version : '2.2',
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 * Registering the buttons
77 */
78 var buttonList = this.buttonList, buttonId;
79 for (var i = 0, n = buttonList.length; i < n; ++i) {
80 var button = buttonList[i];
81 buttonId = button[0];
82 var buttonConfiguration = {
83 id : buttonId,
84 tooltip : this.localize(buttonId + '-Tooltip'),
85 iconCls : 'htmlarea-action-' + button[2],
86 action : 'onButtonPress',
87 context : button[1]
88 };
89 this.registerButton(buttonConfiguration);
90 }
91 /*
92 * Registering the dropdown list
93 */
94 var buttonId = 'Language';
95 if (this.buttonsConfiguration[buttonId.toLowerCase()] && this.buttonsConfiguration[buttonId.toLowerCase()].dataUrl) {
96 var dropDownConfiguration = {
97 id : buttonId,
98 tooltip : this.localize(buttonId + '-Tooltip'),
99 storeUrl : this.buttonsConfiguration[buttonId.toLowerCase()].dataUrl,
100 action : 'onChange'
101 };
102 if (this.buttonsConfiguration.language) {
103 dropDownConfiguration.width = this.buttonsConfiguration.language.width ? parseInt(this.buttonsConfiguration.language.width, 10) : 200;
104 if (this.buttonsConfiguration.language.listWidth) {
105 dropDownConfiguration.listWidth = parseInt(this.buttonsConfiguration.language.listWidth, 10);
106 }
107 if (this.buttonsConfiguration.language.maxHeight) {
108 dropDownConfiguration.maxHeight = parseInt(this.buttonsConfiguration.language.maxHeight, 10);
109 }
110 }
111 this.registerDropDown(dropDownConfiguration);
112 }
113 return true;
114 },
115 /*
116 * The list of buttons added by this plugin
117 */
118 buttonList: [
119 ['LeftToRight', null, 'text-direction-left-to-right'],
120 ['RightToLeft', null, 'text-direction-right-to-left'],
121 ['ShowLanguageMarks', null, 'language-marks-show']
122 ],
123 /*
124 * This function gets called when the editor is generated
125 */
126 onGenerate: function () {
127 var select = this.getButton('Language');
128 if (select) {
129 if (select.getStore().getCount() > 1) {
130 this.addLanguageMarkingRules();
131 } else {
132 // Monitor the language combo's store being loaded
133 select.mon(select.getStore(), 'load', function () {
134 this.addLanguageMarkingRules();
135 var selection = this.editor.getSelection(),
136 selectionEmpty = selection.isEmpty(),
137 ancestors = selection.getAllAncestors(),
138 endPointsInSameBlock = selection.endPointsInSameBlock();
139 this.onUpdateToolbar(select, this.getEditorMode(), selectionEmpty, ancestors, endPointsInSameBlock);
140 }, this);
141 }
142 }
143 },
144 /*
145 * This function adds rules to the stylesheet for language mark highlighting
146 * Model: body.htmlarea-show-language-marks *[lang=en]:before { content: "en: "; }
147 * Works in IE8, but not in earlier versions of IE
148 */
149 addLanguageMarkingRules: function () {
150 var select = this.getButton('Language');
151 if (select) {
152 var styleSheet = this.editor.document.styleSheets[0];
153 select.getStore().each(function (option) {
154 var selector = 'body.htmlarea-show-language-marks *[' + 'lang="' + option.get('value') + '"]:before';
155 var style = 'content: "' + option.get('value') + ': ";';
156 var rule = selector + ' { ' + style + ' }';
157 if (!Ext.isIE) {
158 try {
159 styleSheet.insertRule(rule, styleSheet.cssRules.length);
160 } catch (e) {
161 this.appendToLog('onGenerate', 'Error inserting css rule: ' + rule + ' Error text: ' + e, 'warn');
162 }
163 } else {
164 styleSheet.addRule(selector, style);
165 }
166 return true;
167 }, this);
168 }
169 },
170 /*
171 * This function gets called when a button was pressed.
172 *
173 * @param object editor: the editor instance
174 * @param string id: the button id or the key
175 *
176 * @return boolean false if action is completed
177 */
178 onButtonPress: function (editor, id, target) {
179 // Could be a button or its hotkey
180 var buttonId = this.translateHotKey(id);
181 buttonId = buttonId ? buttonId : id;
182
183 switch (buttonId) {
184 case 'RightToLeft':
185 case 'LeftToRight':
186 this.setDirAttribute(buttonId);
187 break;
188 case 'ShowLanguageMarks':
189 this.toggleLanguageMarks();
190 break;
191 default :
192 break;
193 }
194 return false;
195 },
196 /*
197 * Sets the dir attribute
198 *
199 * @param string buttonId: the button id
200 *
201 * @return void
202 */
203 setDirAttribute: function (buttonId) {
204 var direction = (buttonId == 'RightToLeft') ? 'rtl' : 'ltr';
205 var element = this.editor.getSelection().getParentElement();
206 if (element) {
207 if (/^bdo$/i.test(element.nodeName)) {
208 element.dir = direction;
209 } else {
210 element.dir = (element.dir == direction || element.style.direction == direction) ? '' : direction;
211 }
212 element.style.direction = '';
213 }
214 },
215 /*
216 * Toggles the display of language marks
217 *
218 * @param boolean forceLanguageMarks: if set, language marks are displayed whatever the current state
219 *
220 * @return void
221 */
222 toggleLanguageMarks: function (forceLanguageMarks) {
223 var body = this.editor.document.body;
224 if (!HTMLArea.DOM.hasClass(body, 'htmlarea-show-language-marks')) {
225 HTMLArea.DOM.addClass(body,'htmlarea-show-language-marks');
226 } else if (!forceLanguageMarks) {
227 HTMLArea.DOM.removeClass(body,'htmlarea-show-language-marks');
228 }
229 },
230 /*
231 * This function gets called when some language was selected in the drop-down list
232 */
233 onChange: function (editor, combo, record, index) {
234 this.applyLanguageMark(combo.getValue());
235 },
236 /*
237 * This function applies the langauge mark to the selection
238 */
239 applyLanguageMark: function (language) {
240 var statusBarSelection = this.editor.statusBar ? this.editor.statusBar.getSelection() : null;
241 var range = this.editor.getSelection().createRange();
242 var parent = this.editor.getSelection().getParentElement();
243 var selectionEmpty = this.editor.getSelection().isEmpty();
244 var endPointsInSameBlock = this.editor.getSelection().endPointsInSameBlock();
245 var fullNodeSelected = false;
246 if (!selectionEmpty) {
247 if (endPointsInSameBlock) {
248 var ancestors = this.editor.getSelection().getAllAncestors();
249 for (var i = 0; i < ancestors.length; ++i) {
250 fullNodeSelected = (statusBarSelection === ancestors[i])
251 && ((!Ext.isIE && ancestors[i].textContent === range.toString()) || (Ext.isIE && ((this.editor.getSelection().getType() !== 'Control' && ancestors[i].innerText === range.text) || (this.editor.getSelection().getType() === 'Control' && ancestors[i].innerText === range.item(0).text))));
252 if (fullNodeSelected) {
253 parent = ancestors[i];
254 break;
255 }
256 }
257 // Working around bug in Safari selectNodeContents
258 if (!fullNodeSelected && Ext.isWebKit && statusBarSelection && statusBarSelection.textContent === range.toString()) {
259 fullNodeSelected = true;
260 parent = statusBarSelection;
261 }
262 }
263 }
264 if (selectionEmpty || fullNodeSelected) {
265 // Selection is empty or parent is selected in the status bar
266 if (parent) {
267 // Set language attributes
268 this.setLanguageAttributes(parent, language);
269 }
270 } else if (endPointsInSameBlock) {
271 // The selection is not empty, nor full element
272 if (language != 'none') {
273 // Add span element with lang attribute(s)
274 var newElement = this.editor.document.createElement('span');
275 this.setLanguageAttributes(newElement, language);
276 this.editor.getDomNode().wrapWithInlineElement(newElement, range);
277 if (!Ext.isIE) {
278 range.detach();
279 }
280 }
281 } else {
282 this.setLanguageAttributeOnBlockElements(language);
283 }
284 },
285
286 /*
287 * This function gets the language attribute on the given element
288 *
289 * @param object element: the element from which to retrieve the attribute value
290 *
291 * @return string value of the lang attribute, or of the xml:lang attribute
292 */
293 getLanguageAttribute: function (element) {
294 var xmllang = 'none';
295 try {
296 // IE7 complains about xml:lang
297 xmllang = element.getAttribute('xml:lang') ? element.getAttribute('xml:lang') : 'none';
298 } catch(e) { }
299 return element.getAttribute('lang') ? element.getAttribute('lang') : xmllang;
300 },
301 /*
302 * This function sets the language attributes on the given element
303 *
304 * @param object element: the element on which to set the value of the lang and/or xml:lang attribute
305 * @param string language: value of the lang attributes, or "none", in which case, the attribute(s) is(are) removed
306 *
307 * @return void
308 */
309 setLanguageAttributes: function (element, language) {
310 if (element) {
311 if (language == 'none') {
312 // Remove language mark, if any
313 element.removeAttribute('lang');
314 try {
315 // Do not let IE7 complain
316 element.removeAttribute('xml:lang');
317 } catch(e) { }
318 // Remove the span tag if it has no more attribute
319 if (/^span$/i.test(element.nodeName) && !HTMLArea.DOM.hasAllowedAttributes(element, this.allowedAttributes)) {
320 this.editor.getDomNode().removeMarkup(element);
321 }
322 } else {
323 if (this.useAttribute.lang) {
324 element.setAttribute('lang', language);
325 }
326 if (this.useAttribute.xmlLang) {
327 try {
328 // Do not let IE7 complain
329 element.setAttribute('xml:lang', language);
330 } catch(e) { }
331 }
332 }
333 }
334 },
335 /*
336 * This function gets the language attributes from blocks sibling of the block containing the start container of the selection
337 *
338 * @return string value of the lang attribute, or of the xml:lang attribute, or "none", if all blocks sibling do not have the same attribute value as the block containing the start container
339 */
340 getLanguageAttributeFromBlockElements: function () {
341 var endBlocks = this.editor.getSelection().getEndBlocks();
342 var startAncestors = HTMLArea.DOM.getBlockAncestors(endBlocks.start);
343 var endAncestors = HTMLArea.DOM.getBlockAncestors(endBlocks.end);
344 var index = 0;
345 while (index < startAncestors.length && index < endAncestors.length && startAncestors[index] === endAncestors[index]) {
346 ++index;
347 }
348 if (endBlocks.start === endBlocks.end) {
349 --index;
350 }
351 var language = this.getLanguageAttribute(startAncestors[index]);
352 for (var block = startAncestors[index]; block; block = block.nextSibling) {
353 if (HTMLArea.DOM.isBlockElement(block)) {
354 if (this.getLanguageAttribute(block) != language || this.getLanguageAttribute(block) == 'none') {
355 language = 'none';
356 break;
357 }
358 }
359 if (block == endAncestors[index]) {
360 break;
361 }
362 }
363 return language;
364 },
365 /*
366 * This function sets the language attributes on blocks sibling of the block containing the start container of the selection
367 */
368 setLanguageAttributeOnBlockElements: function (language) {
369 var endBlocks = this.editor.getSelection().getEndBlocks();
370 var startAncestors = HTMLArea.DOM.getBlockAncestors(endBlocks.start);
371 var endAncestors = HTMLArea.DOM.getBlockAncestors(endBlocks.end);
372 var index = 0;
373 while (index < startAncestors.length && index < endAncestors.length && startAncestors[index] === endAncestors[index]) {
374 ++index;
375 }
376 if (endBlocks.start === endBlocks.end) {
377 --index;
378 }
379 for (var block = startAncestors[index]; block; block = block.nextSibling) {
380 if (HTMLArea.DOM.isBlockElement(block)) {
381 this.setLanguageAttributes(block, language);
382 }
383 if (block == endAncestors[index]) {
384 break;
385 }
386 }
387 },
388
389 /*
390 * This function gets called when the toolbar is updated
391 */
392 onUpdateToolbar: function (button, mode, selectionEmpty, ancestors, endPointsInSameBlock) {
393 if (mode === 'wysiwyg' && this.editor.isEditable()) {
394 var statusBarSelection = this.editor.statusBar ? this.editor.statusBar.getSelection() : null;
395 var range = this.editor.getSelection().createRange();
396 var parent = this.editor.getSelection().getParentElement();
397 switch (button.itemId) {
398 case 'RightToLeft':
399 case 'LeftToRight':
400 if (parent) {
401 var direction = (button.itemId === 'RightToLeft') ? 'rtl' : 'ltr';
402 button.setInactive(parent.dir != direction && parent.style.direction != direction);
403 button.setDisabled(/^body$/i.test(parent.nodeName));
404 } else {
405 button.setDisabled(true);
406 }
407 break;
408 case 'ShowLanguageMarks':
409 button.setInactive(!HTMLArea.DOM.hasClass(this.editor.document.body, 'htmlarea-show-language-marks'));
410 break;
411 case 'Language':
412 // Updating the language drop-down
413 var fullNodeSelected = false;
414 var language = this.getLanguageAttribute(parent);
415 if (!selectionEmpty) {
416 if (endPointsInSameBlock) {
417 for (var i = 0; i < ancestors.length; ++i) {
418 fullNodeSelected = (statusBarSelection === ancestors[i])
419 && ((!Ext.isIE && ancestors[i].textContent === range.toString()) || (Ext.isIE && ((this.editor.getSelection().getType() !== 'Control' && ancestors[i].innerText === range.text) || (this.editor.getSelection().getType() === 'Control' && ancestors[i].innerText === range.item(0).text))));
420 if (fullNodeSelected) {
421 parent = ancestors[i];
422 break;
423 }
424 }
425 // Working around bug in Safari selectNodeContents
426 if (!fullNodeSelected && Ext.isWebKit && statusBarSelection && statusBarSelection.textContent === range.toString()) {
427 fullNodeSelected = true;
428 parent = statusBarSelection;
429 }
430 language = this.getLanguageAttribute(parent);
431 } else {
432 language = this.getLanguageAttributeFromBlockElements();
433 }
434 }
435 this.updateValue(button, language, selectionEmpty, fullNodeSelected, endPointsInSameBlock);
436 break;
437 default:
438 break;
439 }
440 }
441 },
442 /*
443 * This function updates the language drop-down list
444 */
445 updateValue: function (select, language, selectionEmpty, fullNodeSelected, endPointsInSameBlock) {
446 var store = select.getStore();
447 store.removeAt(0);
448 if ((store.findExact('value', language) != -1) && (selectionEmpty || fullNodeSelected || !endPointsInSameBlock)) {
449 select.setValue(language);
450 store.insert(0, new store.recordType({
451 text: this.localize('Remove language mark'),
452 value: 'none'
453 }));
454 } else {
455 store.insert(0, new store.recordType({
456 text: this.localize('No language mark'),
457 value: 'none'
458 }));
459 select.setValue('none');
460 }
461 select.setDisabled(!(store.getCount()>1) || (selectionEmpty && /^body$/i.test(this.editor.getSelection().getParentElement().nodeName)));
462 }
463 });