b079d8bf62e157ccca2c8237ec451e1f69bf2629
[Packages/TYPO3.CMS.git] / typo3 / sysext / rtehtmlarea / Resources / Public / JavaScript / HTMLArea / Editor / Editor.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 * Module: TYPO3/CMS/Rtehtmlarea/HTMLArea/Editor/Editor
16 * Editor extends Ext.util.Observable
17 */
18 define(['TYPO3/CMS/Rtehtmlarea/HTMLArea/UserAgent/UserAgent',
19 'TYPO3/CMS/Rtehtmlarea/HTMLArea/Util/Util',
20 'TYPO3/CMS/Rtehtmlarea/HTMLArea/Ajax/Ajax',
21 'TYPO3/CMS/Rtehtmlarea/HTMLArea/DOM/DOM',
22 'TYPO3/CMS/Rtehtmlarea/HTMLArea/Event/Event',
23 'TYPO3/CMS/Rtehtmlarea/HTMLArea/DOM/Selection',
24 'TYPO3/CMS/Rtehtmlarea/HTMLArea/DOM/BookMark',
25 'TYPO3/CMS/Rtehtmlarea/HTMLArea/DOM/Node',
26 'TYPO3/CMS/Rtehtmlarea/HTMLArea/Util/TYPO3',
27 'TYPO3/CMS/Rtehtmlarea/HTMLArea/Editor/Framework',
28 'TYPO3/CMS/Rtehtmlarea/HTMLArea/Editor/Toolbar',
29 'TYPO3/CMS/Rtehtmlarea/HTMLArea/Editor/Iframe',
30 'TYPO3/CMS/Rtehtmlarea/HTMLArea/Editor/TextAreaContainer',
31 'TYPO3/CMS/Rtehtmlarea/HTMLArea/Editor/StatusBar',
32 'TYPO3/CMS/Backend/FormEngine',
33 'TYPO3/CMS/Backend/Notification'],
34 function (UserAgent, Util, Ajax, Dom, Event, Selection, BookMark, Node, Typo3, Framework, Toolbar, Iframe, TextAreaContainer, StatusBar, FormEngine, Notification) {
35
36 /**
37 * Editor constructor method
38 *
39 * @param {Object} config: editor configuration object
40 * @constructor
41 * @exports TYPO3/CMS/Rtehtmlarea/HTMLArea/Editor/Editor
42 */
43 var Editor = function (config) {
44 // Save the config
45 this.config = config;
46 // Establish references to this editor
47 this.editorId = this.config.editorId;
48 RTEarea[this.editorId].editor = this;
49 // Get textarea size and wizard context
50 this.textArea = document.getElementById(this.config.id);
51 var computedStyle = window.getComputedStyle ? window.getComputedStyle(this.textArea) : null;
52 this.textAreaInitialSize = {
53 width: this.config.RTEWidthOverride ? this.config.RTEWidthOverride : (this.textArea.style.width ? this.textArea.style.width : (computedStyle ? computedStyle.width : 0)),
54 height: this.config.fullScreen ? Typo3.getWindowSize().height - 25 : (this.textArea.style.height ? this.textArea.style.height : (computedStyle ? computedStyle.height : 0)),
55 wizardsWidth: 0
56 };
57 // TYPO3 Inline elements and tabs
58 this.nestedParentElements = {
59 all: this.config.tceformsNested,
60 sorted: Typo3.simplifyNested(this.config.tceformsNested)
61 };
62 this.isNested = this.nestedParentElements.sorted.length > 0;
63 // If in BE, get width of wizards
64 if (document.getElementById('typo3-docheader') && !this.config.fullScreen) {
65 this.wizards = this.textArea.parentNode.parentNode.nextSibling;
66 if (this.wizards && this.wizards.nodeType === Dom.ELEMENT_NODE) {
67 if (!this.isNested || Typo3.allElementsAreDisplayed(this.nestedParentElements.sorted)) {
68 this.textAreaInitialSize.wizardsWidth = this.wizards.offsetWidth;
69 } else {
70 // Clone the array of nested tabs and inline levels instead of using a reference as HTMLArea.util.TYPO3.accessParentElements will modify the array
71 var parentElements = [].concat(this.nestedParentElements.sorted);
72 // Walk through all nested tabs and inline levels to get correct size
73 this.textAreaInitialSize.wizardsWidth = Typo3.accessParentElements(parentElements, 'args[0].offsetWidth', [this.wizards]);
74 }
75 // Hide the wizards so that they do not move around while the editor framework is being sized
76 this.wizards.style.display = 'none';
77 }
78 }
79
80 // Create Ajax object
81 this.ajax = new Ajax({
82 editor: this
83 });
84
85 // Plugins register
86 this.plugins = {};
87 // Register the plugins included in the configuration
88 for (var plugin in this.config.plugin) {
89 if (this.config.plugin[plugin]) {
90 this.registerPlugin(plugin);
91 }
92 }
93
94 // Initiate loading of the CSS classes configuration
95 this.getClassesConfiguration();
96
97 // Initialize keyboard input inhibit flag
98 this.inhibitKeyboardInput = false;
99
100 /**
101 * Flag set to true when the editor initialization has completed
102 */
103 this.ready = false;
104
105 /**
106 * The current mode of the editor: 'wysiwyg' or 'textmode'
107 */
108 this.mode = 'textmode';
109
110 /**
111 * The Selection object
112 */
113 this.selection = null;
114
115 /**
116 * The BookMark object
117 */
118 this.bookMark = null;
119
120 /**
121 * The DomNode object
122 */
123 this.domNode = null;
124 };
125
126 /**
127 * Determine whether the editor document is currently contentEditable
128 *
129 * @return boolean true, if the document is contentEditable
130 */
131 Editor.prototype.isEditable = function () {
132 return UserAgent.isIE ? this.document.body.contentEditable : (this.document.designMode === 'on');
133 };
134
135 /**
136 * The selection object
137 */
138 Editor.prototype.getSelection = function () {
139 if (!this.selection) {
140 this.selection = new Selection({
141 editor: this
142 });
143 }
144 return this.selection;
145 };
146
147 /**
148 * The bookmark object
149 */
150 Editor.prototype.getBookMark = function () {
151 if (!this.bookMark) {
152 this.bookMark = new BookMark({
153 editor: this
154 });
155 }
156 return this.bookMark;
157 };
158
159 /**
160 * The DOM node object
161 */
162 Editor.prototype.getDomNode = function () {
163 if (!this.domNode) {
164 this.domNode = new Node({
165 editor: this
166 });
167 }
168 return this.domNode;
169 };
170
171 /**
172 * Generate the editor framework
173 */
174 Editor.prototype.generate = function () {
175 if (this.allPluginsRegistered()) {
176 this.createFramework();
177 } else {
178 var self = this;
179 window.setTimeout(function () {
180 self.generate();
181 }, 50);
182 }
183 };
184
185 /**
186 * Create the htmlArea framework
187 */
188 Editor.prototype.createFramework = function () {
189 // Create the editor framework
190 this.htmlArea = new Framework({
191 id: this.editorId + '-htmlArea',
192 cls: 'htmlarea',
193 editorId: this.editorId,
194 textArea: this.textArea,
195 textAreaInitialSize: this.textAreaInitialSize,
196 fullScreen: this.config.fullScreen,
197 resizable: this.config.resizable,
198 maxHeight: this.config.maxHeight,
199 isNested: this.isNested,
200 nestedParentElements: this.nestedParentElements,
201 items: [new Toolbar({
202 // The toolbar
203 id: this.editorId + '-toolbar',
204 itemId: 'toolbar',
205 editorId: this.editorId
206 }),
207 new Iframe({
208 // The iframe
209 id: this.editorId + '-iframe',
210 itemId: 'iframe',
211 width: (this.textAreaInitialSize.width.indexOf('%') === -1) ? parseInt(this.textAreaInitialSize.width) : 300,
212 height: parseInt(this.textAreaInitialSize.height),
213 autoEl: {
214 id: this.editorId + '-iframe',
215 tag: 'iframe',
216 cls: 'editorIframe',
217 src: UserAgent.isGecko ? 'javascript:void(0);' : (UserAgent.isWebKit ? 'javascript: \'' + Util.htmlEncode(this.config.documentType + this.config.blankDocument) + '\'' : HTMLArea.editorUrl + 'Resources/Public/Html/blank.html')
218 },
219 isNested: this.isNested,
220 nestedParentElements: this.nestedParentElements,
221 editorId: this.editorId
222 }),
223 new TextAreaContainer({
224 // The container for the textarea
225 id: this.editorId + '-textAreaContainer',
226 itemId: 'textAreaContainer',
227 width: (this.textAreaInitialSize.width.indexOf('%') === -1) ? parseInt(this.textAreaInitialSize.width) : 300,
228 textArea: this.textArea
229 }),
230 new StatusBar({
231 // The status bar
232 id: this.editorId + '-statusBar',
233 itemId: 'statusBar',
234 cls: 'statusBar',
235 editorId: this.editorId
236 })
237 ]
238 });
239 // Set some references
240 this.toolbar = this.htmlArea.getToolbar();
241 this.iframe = this.htmlArea.getIframe();
242 this.textAreaContainer = this.htmlArea.getTextAreaContainer();
243 this.statusBar = this.htmlArea.getStatusBar();
244 // Get triggered when the framework becomes ready
245 var self = this;
246 Event.one(this.htmlArea, 'HTMLAreaEventFrameworkReady', function (event) {
247 Event.stopEvent(event);
248 self.onFrameworkReady();
249 return false;
250 });
251 };
252
253 /**
254 * Initialize the editor
255 */
256 Editor.prototype.onFrameworkReady = function () {
257 // Initialize editor mode
258 this.setMode('wysiwyg');
259 // Create the selection object
260 this.getSelection();
261 // Create the bookmark object
262 this.getBookMark();
263 // Create the DOM node object
264 this.getDomNode();
265 // Initiate events listening
266 this.initEventsListening();
267 // Generate plugins
268 this.generatePlugins();
269 // Make the editor visible
270 this.show();
271 this.toolbar.update();
272 // Make the wizards visible again
273 if (this.wizards && this.wizards.nodeType === Dom.ELEMENT_NODE) {
274 this.wizards.style.display = '';
275 }
276 // Focus on the first editor that is not hidden
277 for (var editorId in RTEarea) {
278 var RTE = RTEarea[editorId];
279 if (typeof RTE.editor !== 'object' || RTE.editor === null || (RTE.editor.isNested && !Typo3.allElementsAreDisplayed(RTE.editor.nestedParentElements.sorted))) {
280 continue;
281 } else {
282 RTE.editor.focus();
283 break;
284 }
285 }
286 this.ready = true;
287 /**
288 * @event EditorReady
289 * Fires when initialization of the editor is complete
290 */
291 Event.trigger(this, 'HtmlAreaEventEditorReady');
292 this.appendToLog('HTMLArea.Editor', 'onFrameworkReady', 'Editor ready.', 'info');
293 this.onDOMSubtreeModified();
294 };
295
296 /**
297 * Get the CSS classes configuration
298 *
299 * @return void
300 */
301 Editor.prototype.getClassesConfiguration = function () {
302 this.classesConfigurationLoaded = false;
303 if (this.config.classesUrl && typeof HTMLArea.classesLabels === 'undefined') {
304 this.ajax.getJavascriptFile(this.config.classesUrl, function (options, success, response) {
305 if (success) {
306 try {
307 if (typeof HTMLArea.classesLabels === 'undefined') {
308 eval(response.responseText);
309 }
310 } catch(e) {
311 this.appendToLog('HTMLArea.Editor', 'getClassesConfiguration', 'Error evaluating contents of Javascript file: ' + this.config.classesUrl, 'error');
312 }
313 this.classesConfigurationLoaded = true;
314 }
315 }, this);
316 } else {
317 // There is no classes configuration to be loaded
318 this.classesConfigurationLoaded = true;
319 }
320 };
321
322 /**
323 * Gets the status of the loading process of the CSS classes configuration
324 *
325 * @return boolean true if the classes configuration is loaded
326 */
327 Editor.prototype.classesConfigurationIsLoaded = function() {
328 return this.classesConfigurationLoaded;
329 };
330
331 /**
332 * Set editor mode
333 *
334 * @param string mode: 'textmode' or 'wysiwyg'
335 * @return void
336 */
337 Editor.prototype.setMode = function (mode) {
338 switch (mode) {
339 case 'textmode':
340 this.textArea.value = this.getHTML();
341 this.iframe.setDesignMode(false);
342 this.iframe.hide();
343 this.textAreaContainer.show();
344 this.mode = mode;
345 break;
346 case 'wysiwyg':
347 try {
348 this.document.body.innerHTML = this.getHTML();
349 } catch(e) {
350 this.appendToLog('HTMLArea.Editor', 'setMode', 'The HTML document is not well-formed.', 'warn');
351 Notification.error(
352 'htmlArea RTE',
353 HTMLArea.localize('HTML-document-not-well-formed')
354 );
355 break;
356 }
357 this.textAreaContainer.hide();
358 this.iframe.show();
359 this.iframe.setDesignMode(true);
360 this.mode = mode;
361 break;
362 }
363 /**
364 * @event HTMLAreaEventModeChange
365 * Fires when the editor changes mode
366 */
367 Event.trigger(this, 'HTMLAreaEventModeChange', [this.mode]);
368 this.focus();
369 for (var pluginId in this.plugins) {
370 this.getPlugin(pluginId).onMode(this.mode);
371 }
372 };
373
374 /**
375 * Get current editor mode
376 */
377 Editor.prototype.getMode = function () {
378 return this.mode;
379 };
380
381 /**
382 * Retrieve the HTML
383 * In the case of the wysiwyg mode, the html content is rendered from the DOM tree
384 *
385 * @return string the textual html content from the current editing mode
386 */
387 Editor.prototype.getHTML = function () {
388 switch (this.mode) {
389 case 'wysiwyg':
390 return this.iframe.getHTML();
391 case 'textmode':
392 // Collapse repeated spaces non-editable in wysiwyg
393 // Replace leading and trailing spaces non-editable in wysiwyg
394 return this.textArea.value.
395 replace(/^\x20/g, ' ').
396 replace(/\x20$/g, ' ');
397 default:
398 return '';
399 }
400 };
401
402 /**
403 * Retrieve raw HTML
404 *
405 * @return string the textual html content from the current editing mode
406 */
407 Editor.prototype.getInnerHTML = function () {
408 switch (this.mode) {
409 case 'wysiwyg':
410 return this.document.body.innerHTML;
411 case 'textmode':
412 return this.textArea.value;
413 default:
414 return '';
415 }
416 };
417
418 /**
419 * Replace the html content
420 *
421 * @param string html: the textual html
422 * @return void
423 */
424 Editor.prototype.setHTML = function (html) {
425 switch (this.mode) {
426 case 'wysiwyg':
427 this.document.body.innerHTML = html;
428 break;
429 case 'textmode':
430 this.textArea.value = html;
431 break;
432 }
433 };
434
435 /**
436 * Require and instantiate the specified plugin and register it with the editor
437 *
438 * @param string plugin: the name of the plugin
439 * @return void
440 */
441 Editor.prototype.registerPlugin = function (pluginName) {
442 var self = this;
443 require(['TYPO3/CMS/Rtehtmlarea/Plugins/' + pluginName], function (Plugin) {
444 var pluginInstance = new Plugin(self, pluginName);
445 if (pluginInstance) {
446 var pluginInformation = pluginInstance.getPluginInformation();
447 pluginInformation.instance = pluginInstance;
448 self.plugins[pluginName] = pluginInformation;
449 } else {
450 self.appendToLog('HTMLArea.Editor', 'registerPlugin', 'Could not register plugin ' + pluginName + '.', 'warn');
451 }
452 });
453 };
454
455 /**
456 * Determine if all configured plugins are registered
457 *
458 * @return true if all configured plugins are registered
459 */
460 Editor.prototype.allPluginsRegistered = function () {
461 for (var plugin in this.config.plugin) {
462 if (this.config.plugin[plugin]) {
463 if (!this.plugins[plugin]) {
464 return false;
465 }
466 }
467 }
468 return true;
469 };
470
471 /**
472 * Generate registered plugins
473 */
474 Editor.prototype.generatePlugins = function () {
475 for (var pluginId in this.plugins) {
476 var plugin = this.getPlugin(pluginId);
477 plugin.onGenerate();
478 }
479 };
480
481 /**
482 * Get the instance of the specified plugin, if it exists
483 *
484 * @param string pluginName: the name of the plugin
485 * @return object the plugin instance or null
486 */
487 Editor.prototype.getPlugin = function(pluginName) {
488 return (this.plugins[pluginName] ? this.plugins[pluginName].instance : null);
489 };
490
491 /**
492 * Unregister the instance of the specified plugin
493 *
494 * @param string pluginName: the name of the plugin
495 * @return void
496 */
497 Editor.prototype.unRegisterPlugin = function(pluginName) {
498 delete this.plugins[pluginName].instance;
499 delete this.plugins[pluginName];
500 };
501
502 /**
503 * Update the editor toolbar
504 */
505 Editor.prototype.updateToolbar = function (noStatus) {
506 this.toolbar.update(noStatus);
507 };
508
509 /**
510 * Focus on the editor
511 */
512 Editor.prototype.focus = function () {
513 if (document.activeElement.tagName.toLowerCase() !== 'body') {
514 // Only focus the editor if the body tag is focused, which is
515 // the default after loading a page
516 return;
517 }
518 switch (this.getMode()) {
519 case 'wysiwyg':
520 this.iframe.focus();
521 break;
522 case 'textmode':
523 this.textArea.focus();
524 break;
525 }
526 };
527
528 /**
529 * Scroll the editor window to the current caret position
530 */
531 Editor.prototype.scrollToCaret = function () {
532 if (!UserAgent.isIE) {
533 var contentWindow = this.iframe.getEl().contentWindow;
534 if (contentWindow) {
535 var windowHeight = contentWindow.innerHeight,
536 element = this.getSelection().getParentElement(),
537 elementOffset = element.offsetTop,
538 elementHeight = Dom.getSize(element).height,
539 bodyScrollTop = contentWindow.document.body.scrollTop;
540 // If the current selection is out of view
541 if (elementOffset > windowHeight + bodyScrollTop || elementOffset < bodyScrollTop) {
542 // Scroll the iframe contentWindow
543 contentWindow.scrollTo(0, elementOffset - windowHeight + elementHeight);
544 }
545 }
546 }
547 };
548
549 /**
550 * Add listeners
551 */
552 Editor.prototype.initEventsListening = function () {
553 if (UserAgent.isOpera) {
554 this.iframe.startListening();
555 }
556 // Add unload handler
557 var self = this;
558 Event.one(this.iframe.getIframeWindow(), 'unload', function (event) { return self.onUnload(event); });
559 Event.on(this.iframe.getIframeWindow(), 'DOMSubtreeModified', function (event) { return self.onDOMSubtreeModified(event); });
560 };
561
562 /**
563 * Make the editor framework visible
564 */
565 Editor.prototype.show = function () {
566 document.getElementById('pleasewait' + this.editorId).style.display = 'none';
567 document.getElementById('editorWrap' + this.editorId).style.visibility = 'visible';
568 };
569
570 /**
571 * Append an entry at the end of the troubleshooting log
572 *
573 * @param string functionName: the name of the editor function writing to the log
574 * @param string text: the text of the message
575 * @param string type: the type of message
576 * @return void
577 */
578 Editor.prototype.appendToLog = function (objectName, functionName, text, type) {
579 HTMLArea.appendToLog(this.editorId, objectName, functionName, text, type);
580 };
581
582 /**
583 *
584 * @param {Event} event
585 */
586 Editor.prototype.onDOMSubtreeModified = function(event) {
587 this.textArea.value = this.getHTML().trim();
588 FormEngine.Validation.validate();
589 };
590
591
592 /**
593 * Iframe unload handler: Update the textarea for submission and cleanup
594 */
595 Editor.prototype.onUnload = function (event) {
596 // Save the HTML content into the original textarea for submit, back/forward, etc.
597 if (this.ready) {
598 this.textArea.value = this.getHTML();
599 }
600 // Cleanup
601 for (var pluginId in this.plugins) {
602 this.unRegisterPlugin(pluginId);
603 }
604 Event.off(this.textarea);
605 this.htmlArea.onBeforeDestroy();
606 RTEarea[this.editorId].editor = null;
607 return true;
608 };
609
610 return Editor;
611
612 });