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