1820c0f51b6d86c0664a731a87249c9fdccbee8e
[Packages/TYPO3.CMS.git] / typo3 / sysext / rtehtmlarea / Resources / Public / JavaScript / HTMLArea / Editor / HTMLArea.Editor.js
1 /***************************************************
2 * HTMLArea.Editor extends Ext.util.Observable
3 ***************************************************/
4 HTMLArea.Editor = Ext.extend(Ext.util.Observable, {
5 /*
6 * HTMLArea.Editor constructor
7 */
8 constructor: function (config) {
9 HTMLArea.Editor.superclass.constructor.call(this, {});
10 // Save the config
11 this.config = config;
12 // Establish references to this editor
13 this.editorId = this.config.editorId;
14 RTEarea[this.editorId].editor = this;
15 // Get textarea size and wizard context
16 this.textArea = Ext.get(this.config.id);
17 this.textAreaInitialSize = {
18 width: this.config.RTEWidthOverride ? this.config.RTEWidthOverride : this.textArea.getStyle('width'),
19 height: this.config.fullScreen ? HTMLArea.util.TYPO3.getWindowSize().height - 20 : this.textArea.getStyle('height'),
20 wizardsWidth: 0
21 };
22 // TYPO3 Inline elements and tabs
23 this.nestedParentElements = {
24 all: this.config.tceformsNested,
25 sorted: HTMLArea.util.TYPO3.simplifyNested(this.config.tceformsNested)
26 };
27 this.isNested = this.nestedParentElements.sorted.length > 0;
28 // If in BE, get width of wizards
29 if (Ext.get('typo3-docheader')) {
30 this.wizards = this.textArea.parent().parent().next();
31 if (this.wizards) {
32 if (!this.isNested || HTMLArea.util.TYPO3.allElementsAreDisplayed(this.nestedParentElements.sorted)) {
33 this.textAreaInitialSize.wizardsWidth = this.wizards.getWidth();
34 } else {
35 // Clone the array of nested tabs and inline levels instead of using a reference as HTMLArea.util.TYPO3.accessParentElements will modify the array
36 var parentElements = [].concat(this.nestedParentElements.sorted);
37 // Walk through all nested tabs and inline levels to get correct size
38 this.textAreaInitialSize.wizardsWidth = HTMLArea.util.TYPO3.accessParentElements(parentElements, 'args[0].getWidth()', [this.wizards]);
39 }
40 // Hide the wizards so that they do not move around while the editor framework is being sized
41 this.wizards.hide();
42 }
43 }
44 // Plugins register
45 this.plugins = {};
46 // Register the plugins included in the configuration
47 for (var plugin in this.config.plugin) {
48 if (this.config.plugin[plugin]) {
49 this.registerPlugin(plugin);
50 }
51 }
52 // Create Ajax object
53 this.ajax = new HTMLArea.Ajax({
54 editor: this
55 });
56 // Initialize keyboard input inhibit flag
57 this.inhibitKeyboardInput = false;
58 this.addEvents(
59 /*
60 * @event HTMLAreaEventEditorReady
61 * Fires when initialization of the editor is complete
62 */
63 'HTMLAreaEventEditorReady',
64 /*
65 * @event HTMLAreaEventModeChange
66 * Fires when the editor changes mode
67 */
68 'HTMLAreaEventModeChange'
69 );
70 },
71 /*
72 * Flag set to true when the editor initialization has completed
73 */
74 ready: false,
75 /*
76 * The current mode of the editor: 'wysiwyg' or 'textmode'
77 */
78 mode: 'textmode',
79 /*
80 * Determine whether the editor document is currently contentEditable
81 *
82 * @return boolean true, if the document is contentEditable
83 */
84 isEditable: function () {
85 return HTMLArea.UserAgent.isIE ? this.document.body.contentEditable : (this.document.designMode === 'on');
86 },
87 /*
88 * The selection object
89 */
90 selection: null,
91 getSelection: function () {
92 if (!this.selection) {
93 this.selection = new HTMLArea.DOM.Selection({
94 editor: this
95 });
96 }
97 return this.selection;
98 },
99 /*
100 * The bookmark object
101 */
102 bookMark: null,
103 getBookMark: function () {
104 if (!this.bookMark) {
105 this.bookMark = new HTMLArea.DOM.BookMark({
106 editor: this
107 });
108 }
109 return this.bookMark;
110 },
111 /*
112 * The DOM node object
113 */
114 domNode: null,
115 getDomNode: function () {
116 if (!this.domNode) {
117 this.domNode = new HTMLArea.DOM.Node({
118 editor: this
119 });
120 }
121 return this.domNode;
122 },
123 /*
124 * Create the htmlArea framework
125 */
126 generate: function () {
127 // Create the editor framework
128 this.htmlArea = new HTMLArea.Framework({
129 id: this.editorId + '-htmlArea',
130 layout: 'anchor',
131 baseCls: 'htmlarea',
132 editorId: this.editorId,
133 textArea: this.textArea,
134 textAreaInitialSize: this.textAreaInitialSize,
135 fullScreen: this.config.fullScreen,
136 resizable: this.config.resizable,
137 maxHeight: this.config.maxHeight,
138 isNested: this.isNested,
139 nestedParentElements: this.nestedParentElements,
140 // The toolbar
141 tbar: {
142 xtype: 'htmlareatoolbar',
143 id: this.editorId + '-toolbar',
144 anchor: '100%',
145 layout: 'form',
146 cls: 'toolbar',
147 editorId: this.editorId
148 },
149 items: [{
150 // The iframe
151 xtype: 'htmlareaiframe',
152 itemId: 'iframe',
153 anchor: '100%',
154 width: (this.textAreaInitialSize.width.indexOf('%') === -1) ? parseInt(this.textAreaInitialSize.width) : 300,
155 height: parseInt(this.textAreaInitialSize.height),
156 autoEl: {
157 id: this.editorId + '-iframe',
158 tag: 'iframe',
159 cls: 'editorIframe',
160 src: HTMLArea.UserAgent.isGecko ? 'javascript:void(0);' : (HTMLArea.UserAgent.isWebKit ? 'javascript: \'' + HTMLArea.util.htmlEncode(this.config.documentType + this.config.blankDocument) + '\'' : HTMLArea.editorUrl + 'Resources/Public/Html/blank.html')
161 },
162 isNested: this.isNested,
163 nestedParentElements: this.nestedParentElements,
164 editorId: this.editorId
165 },{
166 // Box container for the textarea
167 xtype: 'box',
168 itemId: 'textAreaContainer',
169 anchor: '100%',
170 width: (this.textAreaInitialSize.width.indexOf('%') === -1) ? parseInt(this.textAreaInitialSize.width) : 300,
171 // Let the framework swallow the textarea and throw it back
172 listeners: {
173 afterrender: {
174 fn: function (textAreaContainer) {
175 this.originalParent = this.textArea.parent().dom;
176 textAreaContainer.getEl().appendChild(this.textArea);
177 },
178 single: true,
179 scope: this
180 },
181 beforedestroy: {
182 fn: function (textAreaContainer) {
183 this.originalParent.appendChild(this.textArea.dom);
184 return true;
185 },
186 single: true,
187 scope: this
188 }
189 }
190 }
191 ],
192 // The status bar
193 bbar: {
194 xtype: 'htmlareastatusbar',
195 anchor: '100%',
196 cls: 'statusBar',
197 editorId: this.editorId
198 }
199 });
200 // Set some references
201 this.toolbar = this.htmlArea.getTopToolbar();
202 this.statusBar = this.htmlArea.getBottomToolbar();
203 this.iframe = this.htmlArea.getComponent('iframe');
204 this.textAreaContainer = this.htmlArea.getComponent('textAreaContainer');
205 // Get triggered when the framework becomes ready
206 this.relayEvents(this.htmlArea, ['HTMLAreaEventFrameworkReady']);
207 this.on('HTMLAreaEventFrameworkReady', this.onFrameworkReady, this, {single: true});
208 },
209 /**
210 * Initialize the editor
211 */
212 onFrameworkReady: function () {
213 // Initialize editor mode
214 this.setMode('wysiwyg');
215 // Create the selection object
216 this.getSelection();
217 // Create the bookmark object
218 this.getBookMark();
219 // Create the DOM node object
220 this.getDomNode();
221 // Initiate events listening
222 this.initEventsListening();
223 // Load the classes configuration
224 this.getClassesConfiguration();
225 },
226
227 /**
228 * Get the classes configuration
229 * This is required before plugins are generated
230 *
231 * @return void
232 */
233 getClassesConfiguration: function () {
234 if (this.config.classesUrl && typeof HTMLArea.classesLabels === 'undefined') {
235 this.ajax.getJavascriptFile(this.config.classesUrl, function (options, success, response) {
236 if (success) {
237 try {
238 if (typeof HTMLArea.classesLabels === 'undefined') {
239 eval(response.responseText);
240 }
241 } catch(e) {
242 this.appendToLog('HTMLArea.Editor', 'getClassesConfiguration', 'Error evaluating contents of Javascript file: ' + this.config.classesUrl, 'error');
243 }
244 }
245 this.initializeEditor();
246 }, this);
247 } else {
248 this.initializeEditor();
249 }
250 },
251
252 /**
253 * Complete editor initialization
254 *
255 * @return void
256 */
257 initializeEditor: function () {
258 // Generate plugins
259 this.generatePlugins();
260 // Make the editor visible
261 this.show();
262 // Make the wizards visible again
263 if (this.wizards) {
264 this.wizards.show();
265 }
266 // Focus on the first editor that is not hidden
267 for (var editorId in RTEarea) {
268 var RTE = RTEarea[editorId];
269 if (typeof RTE.editor !== 'object' || RTE.editor === null || (RTE.editor.isNested && !HTMLArea.util.TYPO3.allElementsAreDisplayed(RTE.editor.nestedParentElements.sorted))) {
270 continue;
271 } else {
272 RTE.editor.focus();
273 break;
274 }
275 }
276 this.ready = true;
277 this.fireEvent('HTMLAreaEventEditorReady');
278 this.appendToLog('HTMLArea.Editor', 'onFrameworkReady', 'Editor ready.', 'info');
279 },
280
281 /*
282 * Set editor mode
283 *
284 * @param string mode: 'textmode' or 'wysiwyg'
285 *
286 * @return void
287 */
288 setMode: function (mode) {
289 switch (mode) {
290 case 'textmode':
291 this.textArea.set({ value: this.getHTML() }, false);
292 this.iframe.setDesignMode(false);
293 this.iframe.hide();
294 this.textAreaContainer.show();
295 this.mode = mode;
296 break;
297 case 'wysiwyg':
298 try {
299 this.document.body.innerHTML = this.getHTML();
300 } catch(e) {
301 this.appendToLog('HTMLArea.Editor', 'setMode', 'The HTML document is not well-formed.', 'warn');
302 TYPO3.Dialog.ErrorDialog({
303 title: 'htmlArea RTE',
304 msg: HTMLArea.localize('HTML-document-not-well-formed')
305 });
306 break;
307 }
308 this.textAreaContainer.hide();
309 this.iframe.show();
310 this.iframe.setDesignMode(true);
311 this.mode = mode;
312 break;
313 }
314 this.fireEvent('HTMLAreaEventModeChange', this.mode);
315 this.focus();
316 for (var pluginId in this.plugins) {
317 this.getPlugin(pluginId).onMode(this.mode);
318 }
319 },
320 /*
321 * Get current editor mode
322 */
323 getMode: function () {
324 return this.mode;
325 },
326 /*
327 * Retrieve the HTML
328 * In the case of the wysiwyg mode, the html content is rendered from the DOM tree
329 *
330 * @return string the textual html content from the current editing mode
331 */
332 getHTML: function () {
333 switch (this.mode) {
334 case 'wysiwyg':
335 return this.iframe.getHTML();
336 case 'textmode':
337 // Collapse repeated spaces non-editable in wysiwyg
338 // Replace leading and trailing spaces non-editable in wysiwyg
339 return this.textArea.getValue().
340 replace(/[\x20]+/g, '\x20').
341 replace(/^\x20/g, ' ').
342 replace(/\x20$/g, ' ');
343 default:
344 return '';
345 }
346 },
347 /*
348 * Retrieve raw HTML
349 *
350 * @return string the textual html content from the current editing mode
351 */
352 getInnerHTML: function () {
353 switch (this.mode) {
354 case 'wysiwyg':
355 return this.document.body.innerHTML;
356 case 'textmode':
357 return this.textArea.getValue();
358 default:
359 return '';
360 }
361 },
362 /*
363 * Replace the html content
364 *
365 * @param string html: the textual html
366 *
367 * @return void
368 */
369 setHTML: function (html) {
370 switch (this.mode) {
371 case 'wysiwyg':
372 this.document.body.innerHTML = html;
373 break;
374 case 'textmode':
375 this.textArea.set({ value: html }, false);;
376 break;
377 }
378 },
379 /*
380 * Get the node given its position in the document tree.
381 * Adapted from FCKeditor
382 * See HTMLArea.DOM.Node::getPositionWithinTree
383 *
384 * @param array position: the position of the node in the document tree
385 * @param boolean normalized: if true, a normalized position is given
386 *
387 * @return objet the node
388 */
389 getNodeByPosition: function (position, normalized) {
390 var current = this.document.documentElement;
391 var i, j, n, m;
392 for (i = 0, n = position.length; current && i < n; i++) {
393 var target = position[i];
394 if (normalized) {
395 var currentIndex = -1;
396 for (j = 0, m = current.childNodes.length; j < m; j++) {
397 var candidate = current.childNodes[j];
398 if (
399 candidate.nodeType == HTMLArea.DOM.TEXT_NODE
400 && candidate.previousSibling
401 && candidate.previousSibling.nodeType == HTMLArea.DOM.TEXT_NODE
402 ) {
403 continue;
404 }
405 currentIndex++;
406 if (currentIndex == target) {
407 current = candidate;
408 break;
409 }
410 }
411 } else {
412 current = current.childNodes[target];
413 }
414 }
415 return current ? current : null;
416 },
417 /*
418 * Instantiate the specified plugin and register it with the editor
419 *
420 * @param string plugin: the name of the plugin
421 *
422 * @return boolean true if the plugin was successfully registered
423 */
424 registerPlugin: function (pluginName) {
425 var plugin = HTMLArea[pluginName],
426 isRegistered = false;
427 if (typeof plugin === 'function') {
428 var pluginInstance = new plugin(this, pluginName);
429 if (pluginInstance) {
430 var pluginInformation = pluginInstance.getPluginInformation();
431 pluginInformation.instance = pluginInstance;
432 this.plugins[pluginName] = pluginInformation;
433 isRegistered = true;
434 }
435 }
436 if (!isRegistered) {
437 this.appendToLog('HTMLArea.Editor', 'registerPlugin', 'Could not register plugin ' + pluginName + '.', 'warn');
438 }
439 return isRegistered;
440 },
441 /*
442 * Generate registered plugins
443 */
444 generatePlugins: function () {
445 for (var pluginId in this.plugins) {
446 var plugin = this.getPlugin(pluginId);
447 plugin.onGenerate();
448 }
449 },
450 /*
451 * Get the instance of the specified plugin, if it exists
452 *
453 * @param string pluginName: the name of the plugin
454 * @return object the plugin instance or null
455 */
456 getPlugin: function(pluginName) {
457 return (this.plugins[pluginName] ? this.plugins[pluginName].instance : null);
458 },
459 /*
460 * Unregister the instance of the specified plugin
461 *
462 * @param string pluginName: the name of the plugin
463 * @return void
464 */
465 unRegisterPlugin: function(pluginName) {
466 delete this.plugins[pluginName].instance;
467 delete this.plugins[pluginName];
468 },
469 /*
470 * Update the edito toolbar
471 */
472 updateToolbar: function (noStatus) {
473 this.toolbar.update(noStatus);
474 },
475 /*
476 * Focus on the editor
477 */
478 focus: function () {
479 switch (this.getMode()) {
480 case 'wysiwyg':
481 this.iframe.focus();
482 break;
483 case 'textmode':
484 this.textArea.focus();
485 break;
486 }
487 },
488 /*
489 * Scroll the editor window to the current caret position
490 */
491 scrollToCaret: function () {
492 if (!HTMLArea.UserAgent.isIE) {
493 var e = this.getSelection().getParentElement(),
494 w = this.iframe.getEl().dom.contentWindow ? this.iframe.getEl().dom.contentWindow : window,
495 h = w.innerHeight || w.height,
496 d = this.document,
497 t = d.documentElement.scrollTop || d.body.scrollTop;
498 if (e.offsetTop > h+t || e.offsetTop < t) {
499 this.getSelection().getParentElement().scrollIntoView();
500 }
501 }
502 },
503 /*
504 * Add listeners
505 */
506 initEventsListening: function () {
507 if (HTMLArea.UserAgent.isOpera) {
508 this.iframe.startListening();
509 }
510 // Add unload handler
511 var iframe = this.iframe.getEl().dom;
512 Ext.EventManager.on(iframe.contentWindow ? iframe.contentWindow : iframe.contentDocument, 'unload', this.onUnload, this, {single: true});
513 },
514 /*
515 * Make the editor framework visible
516 */
517 show: function () {
518 document.getElementById('pleasewait' + this.editorId).style.display = 'none';
519 document.getElementById('editorWrap' + this.editorId).style.visibility = 'visible';
520 },
521 /*
522 * Append an entry at the end of the troubleshooting log
523 *
524 * @param string functionName: the name of the editor function writing to the log
525 * @param string text: the text of the message
526 * @param string type: the type of message
527 *
528 * @return void
529 */
530 appendToLog: function (objectName, functionName, text, type) {
531 HTMLArea.appendToLog(this.editorId, objectName, functionName, text, type);
532 },
533 /*
534 * Iframe unload handler: Update the textarea for submission and cleanup
535 */
536 onUnload: function (event) {
537 // Save the HTML content into the original textarea for submit, back/forward, etc.
538 if (this.ready) {
539 this.textArea.set({
540 value: this.getHTML()
541 }, false);
542 }
543 // Cleanup
544 Ext.TaskMgr.stopAll();
545 for (var pluginId in this.plugins) {
546 this.unRegisterPlugin(pluginId);
547 }
548 this.purgeListeners();
549 // Cleaning references to DOM in order to avoid IE memory leaks
550 if (this.wizards) {
551 this.wizards.dom = null;
552 this.textArea.parent().parent().dom = null;
553 this.textArea.parent().dom = null;
554 }
555 this.textArea.dom = null;
556 RTEarea[this.editorId].editor = null;
557 // ExtJS is not releasing any resources when the iframe is unloaded
558 this.htmlArea.destroy();
559 }
560 });