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