0dcffb627a602a730fcba76b5ff77b67e28911c7
[Packages/TYPO3.CMS.git] / typo3 / sysext / rtehtmlarea / Resources / Public / JavaScript / HTMLArea / Plugin / Plugin.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 * @AMD-Module: TYPO3/CMS/Rtehtmlarea/HTMLArea/Plugin/Plugin
16 * HTMLArea.plugin class
17 *
18 * Every plugin should be a subclass of this class
19 *
20 */
21 define([
22 'TYPO3/CMS/Rtehtmlarea/HTMLArea/UserAgent/UserAgent',
23 'TYPO3/CMS/Rtehtmlarea/HTMLArea/Util/Util',
24 'TYPO3/CMS/Rtehtmlarea/HTMLArea/Event/Event',
25 'jquery',
26 'TYPO3/CMS/Backend/Modal',
27 'TYPO3/CMS/Backend/Severity'
28 ], function (UserAgent, Util, Event, $, Modal, Severity) {
29
30 /**
31 * Constructor method
32 *
33 * @param {Object} editor: a reference to the parent object, instance of RTE
34 * @param {String} pluginName: the name of the plugin
35 * @constructor
36 * @exports TYPO3/CMS/Rtehtmlarea/HTMLArea/Plugin/Plugin
37 */
38 var Plugin = function (editor, pluginName) {
39 this.editor = editor;
40 this.editorNumber = editor.editorId;
41 this.editorId = editor.editorId;
42 this.editorConfiguration = editor.config;
43 this.name = pluginName;
44 this.I18N = {};
45 if (typeof HTMLArea.I18N !== 'undefined' && typeof HTMLArea.I18N[this.name] !== 'undefined') {
46 this.I18N = HTMLArea.I18N[this.name];
47 }
48 this.configurePlugin(editor);
49 };
50
51 Plugin.prototype = {
52
53 /**
54 * Configures the plugin
55 * This function is invoked by the class constructor.
56 * This function should be redefined by the plugin subclass. Normal steps would be:
57 * - registering plugin ingormation with method registerPluginInformation;
58 * - registering any buttons with method registerButton;
59 * - registering any drop-down lists with method registerDropDown.
60 *
61 * @param {Object} editor Instance of RTE
62 *
63 * @return {Boolean} True if the plugin was configured
64 */
65 configurePlugin: function (editor) {
66 return false;
67 },
68
69 /**
70 * Registers the plugin "About" information
71 *
72 * @param {Object} pluginInformation
73 * version: the version,
74 * developer: the name of the developer,
75 * developerUrl: the url of the developer,
76 * copyrightOwner: the name of the copyright owner,
77 * sponsor: the name of the sponsor,
78 * sponsorUrl: the url of the sponsor,
79 * license: the type of license (should be "GPL")
80 *
81 * @return {Boolean} True if the information was registered
82 */
83 registerPluginInformation: function (pluginInformation) {
84 if (typeof pluginInformation !== 'object' || pluginInformation === null) {
85 this.appendToLog('registerPluginInformation', 'Plugin information was not provided', 'warn');
86 return false;
87 } else {
88 this.pluginInformation = pluginInformation;
89 this.pluginInformation.name = this.name;
90 return true;
91 }
92 },
93
94 /**
95 * Returns the plugin information
96 *
97 * @return {Object} The plugin information object
98 */
99 getPluginInformation: function () {
100 return this.pluginInformation;
101 },
102
103 /**
104 * Returns a plugin object
105 *
106 * @param {String} pluginName The name of some plugin
107 * @return {Object} The plugin object or null
108 */
109 getPluginInstance: function (pluginName) {
110 return this.editor.getPlugin(pluginName);
111 },
112
113 /**
114 * Returns a current editor mode
115 *
116 * @return {String} Editor mode
117 */
118 getEditorMode: function () {
119 return this.editor.getMode();
120 },
121
122 /**
123 * Returns true if the button is enabled in the toolbar configuration
124 *
125 * @param {String} buttonId Identification of the button
126 *
127 * @return {Boolean} True if the button is enabled in the toolbar configuration
128 */
129 isButtonInToolbar: function (buttonId) {
130 var index = -1;
131 var i, j, n, m;
132 for (i = 0, n = this.editorConfiguration.toolbar.length; i < n; i++) {
133 var row = this.editorConfiguration.toolbar[i];
134 for (j = 0, m = row.length; j < m; j++) {
135 var group = row[j];
136 index = group.indexOf(buttonId);
137 if (index !== -1) {
138 break;
139 }
140 }
141 if (index !== -1) {
142 break;
143 }
144 }
145 return index !== -1;
146 },
147
148 /**
149 * Returns the button object from the toolbar
150 *
151 * @param {String} buttonId Identification of the button
152 *
153 * @return {Object} The toolbar button object
154 */
155 getButton: function (buttonId) {
156 return this.editor.toolbar.getButton(buttonId);
157 },
158
159 /**
160 * Registers a button for inclusion in the toolbar
161 *
162 * @param {Object} buttonConfiguration The configuration object of the button:
163 * id: unique id for the button
164 * tooltip: tooltip for the button
165 * textMode : enable in text mode
166 * action: name of the function invoked when the button is pressed
167 * context: will be disabled if not inside one of listed elements
168 * hide: hide in menu and show only in context menu (deprecated, use hidden)
169 * hidden: synonym of hide
170 * selection: will be disabled if there is no selection
171 * hotkey: hotkey character
172 * dialog: if true, the button opens a dialogue
173 * dimensions: the opening dimensions object of the dialogue window
174 *
175 * @return {Boolean} True if the button was successfully registered
176 */
177 registerButton: function (buttonConfiguration) {
178 if (this.isButtonInToolbar(buttonConfiguration.id)) {
179 if (typeof buttonConfiguration.action === 'string' && buttonConfiguration.action.length > 0 && typeof this[buttonConfiguration.action] === 'function') {
180 buttonConfiguration.plugins = this;
181 if (buttonConfiguration.dialog) {
182 if (!buttonConfiguration.dimensions) {
183 buttonConfiguration.dimensions = { width: 250, height: 250};
184 }
185 buttonConfiguration.dimensions.top = buttonConfiguration.dimensions.top ? buttonConfiguration.dimensions.top : this.editorConfiguration.dialogueWindows.defaultPositionFromTop;
186 buttonConfiguration.dimensions.left = buttonConfiguration.dimensions.left ? buttonConfiguration.dimensions.left : this.editorConfiguration.dialogueWindows.defaultPositionFromLeft;
187 }
188 buttonConfiguration.hidden = buttonConfiguration.hide;
189 // Apply additional ExtJS config properties set in Page TSConfig
190 // May not always work for values that must be integers
191 Util.applyIf(buttonConfiguration, this.editorConfiguration.buttons[this.editorConfiguration.convertButtonId[buttonConfiguration.id]]);
192 if (this.editorConfiguration.registerButton(buttonConfiguration)) {
193 var hotKey = buttonConfiguration.hotKey ? buttonConfiguration.hotKey :
194 ((this.editorConfiguration.buttons[this.editorConfiguration.convertButtonId[buttonConfiguration.id]] && this.editorConfiguration.buttons[this.editorConfiguration.convertButtonId[buttonConfiguration.id]].hotKey) ? this.editorConfiguration.buttons[this.editorConfiguration.convertButtonId[buttonConfiguration.id]].hotKey : null);
195 if (!hotKey && buttonConfiguration.hotKey == "0") {
196 hotKey = "0";
197 }
198 if (!hotKey && this.editorConfiguration.buttons[this.editorConfiguration.convertButtonId[buttonConfiguration.id]] && this.editorConfiguration.buttons[this.editorConfiguration.convertButtonId[buttonConfiguration.id]].hotKey == "0") {
199 hotKey = "0";
200 }
201 if (hotKey || hotKey == "0") {
202 var hotKeyConfiguration = {
203 id : hotKey,
204 cmd : buttonConfiguration.id
205 };
206 return this.registerHotKey(hotKeyConfiguration);
207 }
208 return true;
209 }
210 } else {
211 this.appendToLog('registerButton', 'Function ' + buttonConfiguration.action + ' was not defined when registering button ' + buttonConfiguration.id, 'error');
212 }
213 }
214 return false;
215 },
216
217 /**
218 * Registers a drop-down list for inclusion in the toolbar
219 *
220 * @param {Object} dropDownConfiguration: the configuration object of the drop-down:
221 * id: unique id for the drop-down
222 * tooltip: tooltip for the drop-down
223 * action: name of function to invoke when an option is selected
224 * textMode: enable in text mode
225 *
226 * @return {Boolean} True if the drop-down list was successfully registered
227 */
228 registerDropDown: function (dropDownConfiguration) {
229 if (this.isButtonInToolbar(dropDownConfiguration.id)) {
230 if (typeof dropDownConfiguration.action === 'string' && dropDownConfiguration.action.length > 0 && typeof this[dropDownConfiguration.action] === 'function') {
231 dropDownConfiguration.plugins = this;
232 dropDownConfiguration.hidden = dropDownConfiguration.hide;
233 dropDownConfiguration.xtype = 'htmlareaselect';
234 // Apply additional config properties set in Page TSConfig
235 // May not always work for values that must be integers
236 Util.applyIf(dropDownConfiguration, this.editorConfiguration.buttons[this.editorConfiguration.convertButtonId[dropDownConfiguration.id]]);
237 return this.editorConfiguration.registerButton(dropDownConfiguration);
238 } else {
239 this.appendToLog('registerDropDown', 'Function ' + dropDownConfiguration.action + ' was not defined when registering drop-down ' + dropDownConfiguration.id, 'error');
240 }
241 }
242 return false;
243 },
244
245 /**
246 * Registers a text element for inclusion in the toolbar
247 *
248 * @param {Object} textConfiguration: the configuration object of the text element:
249 * id: unique id for the text item
250 * text: the text litteral
251 * tooltip: tooltip for the text item
252 * cls: a css class to be assigned to the text element
253 *
254 * @return {Boolean} true if the drop-down list was successfully registered
255 */
256 registerText: function (textConfiguration) {
257 if (this.isButtonInToolbar(textConfiguration.id)) {
258 textConfiguration.plugins = this;
259 textConfiguration.xtype = 'htmlareatoolbartext';
260 return this.editorConfiguration.registerButton(textConfiguration);
261 }
262 return false;
263 },
264
265 /**
266 * Returns the drop-down configuration
267 *
268 * @param {String} dropDownId The unique id of the drop-down
269 *
270 * @return {Object} The drop-down configuration object
271 */
272 getDropDownConfiguration: function(dropDownId) {
273 return this.editorConfiguration.buttonsConfig[dropDownId];
274 },
275
276 /**
277 * Registers a hotkey
278 *
279 * @param {Object} hotKeyConfiguration The configuration object of the hotkey:
280 * id: the key
281 * cmd: name of the button corresponding to the hot key
282 * element: value of the record to be selected in the dropDown item
283 *
284 * @return {Boolean} True if the hotkey was successfully registered
285 */
286 registerHotKey: function (hotKeyConfiguration) {
287 return this.editorConfiguration.registerHotKey(hotKeyConfiguration);
288 },
289
290 /**
291 * Returns the buttonId corresponding to the hotkey, if any
292 *
293 * @param {String} key The hotkey
294 *
295 * @return {string} The buttonId or ""
296 */
297 translateHotKey: function(key) {
298 var returnValue = '';
299 if (typeof this.editorConfiguration.hotKeyList[key] !== 'undefined') {
300 var buttonId = this.editorConfiguration.hotKeyList[key].cmd;
301 if (typeof buttonId !== 'undefined') {
302 returnValue = buttonId;
303 }
304 }
305 return returnValue;
306 },
307
308 /**
309 * Returns the hotkey configuration
310 *
311 * @param {String} key The hotkey
312 *
313 * @return {Object} The hotkey configuration object
314 */
315 getHotKeyConfiguration: function(key) {
316 if (typeof this.editorConfiguration.hotKeyList[key] !== 'undefined') {
317 return this.editorConfiguration.hotKeyList[key];
318 }
319 return null;
320 },
321
322 /**
323 * Initializes the plugin
324 * Is invoked when the toolbar component is created (subclass of Ext.ux.HTMLAreaButton or Ext.ux.form.HTMLAreaCombo)
325 *
326 * @param {Object} button The component
327 */
328 init: Util.emptyFunction,
329
330 /**
331 * The toolbar refresh handler of the plugin
332 * This function may be defined by the plugin subclass.
333 * If defined, the function will be invoked whenever the toolbar state is refreshed.
334 *
335 * @return {Boolean}
336 */
337 onUpdateToolbar: Util.emptyFunction,
338
339 /**
340 * The onMode event handler
341 * This function may be redefined by the plugin subclass.
342 * The function is invoked whenever the editor changes mode.
343 *
344 * @param {String} mode "wysiwyg" or "textmode"
345 *
346 * @return {Boolean}
347 */
348 onMode: function(mode) {
349 if (mode === "textmode" && this.dialog && !(this.dialog.buttonId && this.editorConfiguration.buttons[this.dialog.buttonId] && this.editorConfiguration.buttons[this.dialog.buttonId].textMode)) {
350 if (typeof Modal.currentModal !== 'undefined') {
351 Modal.currentModal.trigger('modal-dismiss');
352 }
353 }
354 },
355
356 /**
357 * The onGenerate event handler
358 * This function may be defined by the plugin subclass.
359 * The function is invoked when the editor is initialized
360 *
361 * @return {Boolean}
362 */
363 onGenerate: Util.emptyFunction,
364
365 /**
366 * Localize a string
367 *
368 * @param {String} label The name of the label to localize
369 * @param {Integer} plural
370 *
371 * @return {String} The localization of the label
372 */
373 localize: function (label, plural) {
374 var i = plural || 0;
375 var localized = this.I18N[label];
376 if (typeof localized === 'object' && localized !== null && typeof localized[i] !== 'undefined') {
377 localized = localized[i]['target'];
378 } else {
379 localized = HTMLArea.localize(label, plural);
380 }
381 return localized;
382 },
383
384 /**
385 * Get localized label wrapped with contextual help markup when available
386 *
387 * @param {String} fieldName The name of the field in the CSH file
388 * @param {String} label The name of the label to localize
389 * @param {String} pluginName Overrides this.name
390 *
391 * @return {String} Localized label with CSH markup
392 */
393 getHelpTip: function (fieldName, label, pluginName) {
394 if (typeof TYPO3.ContextHelp !== 'undefined' && typeof fieldName === 'string') {
395 pluginName = typeof pluginName !== 'undefined' ? pluginName : this.name;
396 if (fieldName.length > 0) {
397 fieldName = fieldName.replace(/-|\s/gi, '_');
398 }
399 return '<span class="t3-help-link" href="#" data-table="xEXT_rtehtmlarea_' + pluginName + '" data-field="' + fieldName + '"><abbr class="t3-help-teaser">' + (this.localize(label) || label) + '</abbr></span>';
400 }
401 return this.localize(label) || label;
402 },
403
404 /**
405 * Load a Javascript file asynchronously
406 *
407 * @param {String} url URL of the file to load
408 * @param {Function} callback The callBack function
409 *
410 * @return {Boolean} True on success of the request submission
411 */
412 getJavascriptFile: function (url, callback) {
413 return this.editor.ajax.getJavascriptFile(url, callback, this);
414 },
415
416 /**
417 * Post data to the server
418 *
419 * @param {String} url URL to post data to
420 * @param {Object} data Data to be posted
421 * @param {Function} callback Function that will handle the response returned by the server
422 *
423 * @return {Boolean} True on success
424 */
425 postData: function (url, data, callback) {
426 return this.editor.ajax.postData(url, data, callback, this);
427 },
428
429 /**
430 * Open a window with container iframe
431 *
432 * @param {String} buttonId The id of the button
433 * @param {String} title The window title (will be localized here)
434 * @param {Integer} height The height of the containing iframe
435 * @param {String} url The url to load ino the iframe
436 */
437 openContainerWindow: function (buttonId, title, height, url) {
438 var self = this,
439 $iframe = $('<iframe />', {src: url, 'class': 'content-iframe', style: 'border: 0; height: ' + height * 1 + 'px;'}),
440 $content = $('<div />', {'class': 'htmlarea-window', id: this.editor.editorId + buttonId}).append($iframe);
441
442 this.dialog = Modal.show(this.localize(title) || title, $content, Severity.notice);
443
444 // TODO: dirty CSS hack - provide an API instead?
445 this.dialog.find('.modal-body').css('padding', 0);
446 this.dialog.on('modal-dismiss', function() {
447 self.onCancel();
448 });
449 },
450
451 /**
452 * Make url from module path
453 *
454 * @param {String} modulePath Module path
455 * @param {String} parameters Additional parameters
456 *
457 * @return {String} The url
458 */
459 makeUrlFromModulePath: function (modulePath, parameters) {
460 return modulePath + (modulePath.indexOf("?") === -1 ? "?" : "&") + this.editorConfiguration.RTEtsConfigParams + '&editorNo=' + this.editor.editorId + '&sys_language_content=' + this.editorConfiguration.sys_language_content + '&contentTypo3Language=' + this.editorConfiguration.typo3ContentLanguage + (parameters?parameters:'');
461 },
462
463 /**
464 * Append an entry at the end of the troubleshooting log
465 *
466 * @param {String} functionName The name of the plugin function writing to the log
467 * @param {String} text The text of the message
468 * @param {String} type The typeof of message: 'log', 'info', 'warn' or 'error'
469 */
470 appendToLog: function (functionName, text, type) {
471 this.editor.appendToLog(this.name, functionName, text, type);
472 },
473
474 /**
475 * Add a config element to config array if not empty
476 *
477 * @param {Object} configElement The config element
478 * @param {Array} configArray The config array
479 */
480 addConfigElement: function (configElement, configArray) {
481 if (typeof configElement === 'object' && configElement !== null) {
482 configArray.push(configElement);
483 }
484 },
485
486 /**
487 * Show the dialogue window
488 */
489 show: function () {
490 // Close the window if the editor changes mode
491 var self = this;
492 Event.one(this.editor, 'HTMLAreaEventModeChange', function (event) { self.close(); });
493 this.saveSelection();
494 if (typeof this.dialogueWindowDimensions !== 'undefined') {
495 this.dialog.setPosition(this.dialogueWindowDimensions.positionFromLeft, this.dialogueWindowDimensions.positionFromTop);
496 }
497 this.dialog.show();
498 this.restoreSelection();
499 },
500
501 /**
502 * Remove listeners
503 * This function may be defined by the plugin subclass.
504 * The function is invoked when a plugin dialog is closed
505 */
506 removeListeners: Util.emptyFunction,
507
508 /**
509 * Close the dialogue window (after saving the selection, if IE)
510 */
511 close: function () {
512 this.removeListeners();
513 this.saveSelection();
514 if (typeof Modal.currentModal !== 'undefined') {
515 Modal.currentModal.trigger('modal-dismiss');
516 }
517 },
518
519 /**
520 * Dialogue window onClose handler
521 */
522 onClose: function () {
523 this.removeListeners();
524 this.editor.focus();
525 this.restoreSelection();
526 this.editor.updateToolbar();
527 },
528
529 /**
530 * Handler for window cancel
531 */
532 onCancel: function () {
533 Modal.currentModal.trigger('modal-dismiss');
534
535 this.removeListeners();
536 this.editor.focus();
537 },
538
539 /**
540 * Save selection
541 * Should be called after processing button other than Cancel
542 */
543 saveSelection: function () {
544 // If IE, save the current selection
545 if (UserAgent.isIE) {
546 this.savedRange = this.editor.getSelection().createRange();
547 }
548 },
549
550 /**
551 * Restore selection
552 * Should be called before processing dialogue button or result
553 */
554 restoreSelection: function () {
555 // If IE, restore the selection saved when the window was shown
556 if (UserAgent.isIE && this.savedRange) {
557 // Restoring the selection will not work if the inner html was replaced by the plugin
558 try {
559 this.editor.getSelection().selectRange(this.savedRange);
560 } catch (e) {}
561 }
562 },
563
564 /**
565 * Build the configuration object of a button
566 *
567 * @param {String} button The text of the button
568 * @param {Function} handler Button handler
569 * @param {Boolean} active Whether the button should be active or not
570 * @param {Integer} severity The severity the button is representing
571 *
572 * @return {Object} The button configuration object
573 */
574 buildButtonConfig: function (button, handler, active, severity) {
575 return {
576 text: this.localize(button),
577 active: active,
578 btnClass: 'btn-' + (typeof severity !== 'undefined' ? Severity.getCssClass(severity) : 'default'),
579 trigger: handler
580 };
581 },
582
583 /**
584 * Helper method to generate the select boxes with predefined values
585 *
586 * @param {Object} $fieldset The jQuery object of the current fieldset
587 * @param {String} fieldLabel The label of the form field
588 * @param {String} selectName The name of the select field
589 * @param {Array} availableOptions Nested options array used for the select field. 0: label, 1: value
590 * @param {String} selectedValue The selected value. Used to set the `selected` property
591 * @param {Function} onChangeHandler Callback function triggered on change
592 * @param {Boolean} isDisabled Whether the field should be disabled or not
593 * @return {Object}
594 */
595 attachSelectMarkup: function($fieldset, fieldLabel, selectName, availableOptions, selectedValue, onChangeHandler, isDisabled) {
596 var $select = $('<select />', {'class': 'form-control', name: selectName}),
597 attributeConfiguration = {}
598
599 for (var i = 0; i < availableOptions.length; ++i) {
600 attributeConfiguration = {
601 value: availableOptions[i][1]
602 };
603
604 if (selectedValue && availableOptions[i][1] === selectedValue) {
605 attributeConfiguration.selected = 'selected';
606 }
607
608 $select.append(
609 $('<option />', attributeConfiguration).text(availableOptions[i][0])
610 );
611 }
612
613 if (onChangeHandler && typeof onChangeHandler === 'function') {
614 $select.on('change', onChangeHandler);
615 }
616
617 if (typeof isDisabled === 'boolean' && isDisabled) {
618 $select.prop('disabled', isDisabled);
619 }
620
621 $fieldset.append(
622 $('<div />', {'class': 'form-group'}).append(
623 $('<label />', {'class': 'col-sm-2'}).html(fieldLabel),
624 $('<div />', {'class': 'col-sm-10'}).append($select)
625 )
626 );
627
628 return $fieldset;
629 },
630
631 /**
632 * Helper method that creates the necessary markup for a new tab
633 *
634 * @param {Object} $tabs
635 * @param {Object} $container
636 * @param {String} identifier
637 * @param {Object} elements
638 * @param {String} label
639 */
640 buildTabMarkup: function($tabs, $container, identifier, elements, label) {
641 var $newTabPanel = $('<div />', {role: 'tabpanel', 'class': 'tab-pane', id: identifier});
642 $tabs.append(
643 $('<li />').append(
644 $('<a />', {href: '#' + identifier, 'aria-controls': identifier, role: 'tab', 'data-toggle': 'tab'}).text(label)
645 )
646 );
647 for (var item in elements) {
648 if (elements.hasOwnProperty(item)) {
649 $newTabPanel.append(
650 $('<div />', {'class': 'form-section'}).append(elements[item])
651 );
652 }
653 }
654 $container.append($newTabPanel);
655 },
656 };
657
658 return Plugin;
659
660 });