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