Commit 4f0a4f0b authored by Stanislas Rolland's avatar Stanislas Rolland
Browse files

[TASK] RTE: Migrate editor iframe to plain JavaScript

Releases: master
Resolves: #63786
Change-Id: I3a01400a4790e173ed8e1d5410a708eedb23f63e
Reviewed-on: http://review.typo3.org/35332


Reviewed-by: default avatarStanislas Rolland <typo3@sjbr.ca>
Tested-by: default avatarStanislas Rolland <typo3@sjbr.ca>
parent 915427f1
......@@ -269,7 +269,7 @@ define('TYPO3/CMS/Rtehtmlarea/HTMLArea/DOM/DOM',
var types = [types];
}
// Is types a non-empty array?
if (types && toString.apply(types) === '[object Array]' && types.length > 0) {
if (types && Object.prototype.toString.call(types) === '[object Array]' && types.length > 0) {
types = new RegExp( '^(' + types.join('|') + ')$', 'i');
while (parent && parent.nodeType === Dom.ELEMENT_NODE && !/^(body)$/i.test(parent.nodeName)) {
if (types.test(parent.nodeName)) {
......@@ -323,7 +323,7 @@ define('TYPO3/CMS/Rtehtmlarea/HTMLArea/DOM/DOM',
var allowedAttributes = [allowedAttributes];
}
// Is allowedAttributes an array?
if (allowedAttributes && toString.apply(allowedAttributes) === '[object Array]') {
if (allowedAttributes && Object.prototype.toString.call(allowedAttributes) === '[object Array]') {
for (var i = allowedAttributes.length; --i >= 0;) {
value = node.getAttribute(allowedAttributes[i]);
if (value) {
......
......@@ -49,7 +49,7 @@ define('TYPO3/CMS/Rtehtmlarea/HTMLArea/DOM/Selection',
/**
* Reference to the editor iframe window
*/
this.window = this.editor.iframe.getEl().dom.contentWindow;
this.window = this.editor.iframe.getEl().contentWindow;
// Set current selection
this.get();
......
......@@ -538,7 +538,7 @@ define('TYPO3/CMS/Rtehtmlarea/HTMLArea/Editor/Editor',
Editor.prototype.scrollToCaret = function () {
if (!UserAgent.isIE) {
var e = this.getSelection().getParentElement(),
w = this.iframe.getEl().dom.contentWindow ? this.iframe.getEl().dom.contentWindow : window,
w = this.iframe.getEl().contentWindow ? this.iframe.getEl().contentWindow : window,
h = w.innerHeight || w.height,
d = this.document,
t = d.documentElement.scrollTop || d.body.scrollTop;
......@@ -556,9 +556,8 @@ define('TYPO3/CMS/Rtehtmlarea/HTMLArea/Editor/Editor',
this.iframe.startListening();
}
// Add unload handler
var iframe = this.iframe.getEl().dom;
var self = this;
Event.one(iframe.contentWindow ? iframe.contentWindow : iframe.contentDocument, 'unload', function (event) { return self.onUnload(event); });
Event.one(this.iframe.getIframeWindow(), 'unload', function (event) { return self.onUnload(event); });
};
/**
......@@ -595,8 +594,6 @@ define('TYPO3/CMS/Rtehtmlarea/HTMLArea/Editor/Editor',
}
Event.off(this.textarea);
RTEarea[this.editorId].editor = null;
// ExtJS is not releasing any resources when the iframe is unloaded
this.htmlArea.destroy();
return true;
};
......
......@@ -106,6 +106,8 @@ define('TYPO3/CMS/Rtehtmlarea/HTMLArea/Editor/Framework',
}
Event.on(form, 'reset', function (event) { return self.onReset(event); });
}
// Monitor editor being unloaded
Event.one(this.iframe.getIframeWindow(), 'unload', function (event) { return self.onBeforeDestroy(); });
},
/**
......@@ -264,7 +266,7 @@ define('TYPO3/CMS/Rtehtmlarea/HTMLArea/Editor/Framework',
* Resize the framework components
*/
onFrameworkResize: function () {
Dom.setSize(this.iframe.getEl().dom, { width: this.getInnerWidth(), height: this.getInnerHeight()});
Dom.setSize(this.iframe.getEl(), { width: this.getInnerWidth(), height: this.getInnerHeight()});
Dom.setSize(this.textArea, { width: this.getInnerWidth(), height: this.getInnerHeight()});
},
......@@ -272,7 +274,7 @@ define('TYPO3/CMS/Rtehtmlarea/HTMLArea/Editor/Framework',
* Adjust the height to the changing size of the statusbar when the textarea is shown
*/
onTextAreaShow: function () {
Dom.setSize(this.iframe.getEl().dom, { height: this.getInnerHeight()});
Dom.setSize(this.iframe.getEl(), { height: this.getInnerHeight()});
Dom.setSize(this.textArea, { width: this.getInnerWidth(), height: this.getInnerHeight()});
},
......@@ -284,7 +286,7 @@ define('TYPO3/CMS/Rtehtmlarea/HTMLArea/Editor/Framework',
this.onWindowResize();
} else {
//this.iframe.setHeight(this.getInnerHeight());
Dom.setSize(this.iframe.getEl().dom, { height: this.getInnerHeight()});
Dom.setSize(this.iframe.getEl(), { height: this.getInnerHeight()});
Dom.setSize(this.textArea, { height: this.getInnerHeight()});
}
},
......@@ -349,8 +351,9 @@ define('TYPO3/CMS/Rtehtmlarea/HTMLArea/Editor/Framework',
/**
* Cleanup on framework destruction
*/
destroy: function () {
onBeforeDestroy: function () {
Event.off(window);
Event.off(this.iframe);
Event.off(this.textAreaContainer);
// Cleaning references to DOM in order to avoid IE memory leaks
var form = this.textArea.form;
......@@ -358,12 +361,6 @@ define('TYPO3/CMS/Rtehtmlarea/HTMLArea/Editor/Framework',
Event.off(form);
form.htmlAreaPreviousOnReset = null;
}
// ExtJS is not releasing any resources when the iframe is unloaded
this.toolbar.destroy();
this.statusBar.destroy();
while (this.el.firstChild) {
this.el.removeChild(this.el.firstChild);
}
if (this.resizer) {
Resizable.destroy(this.resizer);
}
......
......@@ -11,40 +11,36 @@
* The TYPO3 project - inspiring people to share!
*/
/**
* HTMLArea.Iframe extends Ext.BoxComponent
* The editor iframe
*/
define('TYPO3/CMS/Rtehtmlarea/HTMLArea/Editor/Iframe',
['TYPO3/CMS/Rtehtmlarea/HTMLArea/UserAgent/UserAgent',
'TYPO3/CMS/Rtehtmlarea/HTMLArea/DOM/Walker',
'TYPO3/CMS/Rtehtmlarea/HTMLArea/Util/TYPO3',
'TYPO3/CMS/Rtehtmlarea/HTMLArea/Util/Util',
'TYPO3/CMS/Rtehtmlarea/HTMLArea/DOM/DOM',
'TYPO3/CMS/Rtehtmlarea/HTMLArea/Event/Event',
'TYPO3/CMS/Rtehtmlarea/HTMLArea/Event/KeyMap'],
function (UserAgent, Walker, Typo3, Dom, Event, KeyMap) {
function (UserAgent, Walker, Typo3, Util, Dom, Event, KeyMap) {
var Iframe = Ext.extend(Ext.BoxComponent, {
/**
* Editor iframe constructor
*/
var Iframe = function (config) {
Util.apply(this, config);
};
Iframe.prototype = {
/**
* Constructor
* Render the iframe (called by framework rendering)
*
* @param object container: the container into which to insert the iframe (that is the framework)
* @return void
*/
initComponent: function () {
Iframe.superclass.initComponent.call(this);
this.addListener({
afterrender: {
fn: this.initEventListeners,
single: true
},
show: {
fn: function (iframe) {
Event.trigger(iframe, 'HTMLAreaEventIframeShow');
}
},
beforedestroy: {
fn: this.onBeforeDestroy,
single: true
}
});
render: function (container) {
this.config = this.getEditor().config;
this.createIframe(container);
this.htmlRenderer = new Walker({
keepComments: !this.config.htmlRemoveComments,
removeTags: this.config.htmlRemoveTags,
......@@ -52,18 +48,30 @@ define('TYPO3/CMS/Rtehtmlarea/HTMLArea/Editor/Iframe',
baseUrl: this.config.baseURL
});
if (!this.config.showStatusBar) {
this.addClass('noStatusBar');
Dom.addClass(this.getEl(), 'noStatusBar');
}
this.initEventListeners();
},
/**
* Get the element to which the iframe is rendered
*/
getEl: function () {
return this.el;
},
/**
* Initialize event listeners and the document after the iframe has rendered
*/
initEventListeners: function () {
var self = this;
this.initStyleChangeEventListener();
// Monitor editor becoming ready
var self = this;
Event.one(this.getEditor(), 'HtmlAreaEventEditorReady', function (event) { Event.stopEvent(event); self.onEditorReady(); return false; });
if (UserAgent.isOpera) {
var self = this;
Event.one(this.getEl().dom, 'load', function (event) { Event.stopEvent(event); self.initializeIframe(event); return false; })
Event.one(iframe, 'load', function (event) { self.initializeIframe(event); return true; })
} else {
this.initializeIframe();
}
......@@ -75,7 +83,7 @@ define('TYPO3/CMS/Rtehtmlarea/HTMLArea/Editor/Iframe',
* In all browsers, it breaks the evaluation of the framework dimensions
*/
initStyleChangeEventListener: function () {
if (this.isNested && UserAgent.isGecko) {
if (this.isNested && !UserAgent.isWebKit) {
var self = this;
var options = {
delay: 50
......@@ -84,13 +92,13 @@ define('TYPO3/CMS/Rtehtmlarea/HTMLArea/Editor/Iframe',
var nestedElement = document.getElementById(this.nestedParentElements.sorted[i]);
Event.on(
nestedElement,
'DOMAttrModified',
UserAgent.isIEBeforeIE9 ? 'propertychange' : 'DOMAttrModified',
function (event) { return self.onNestedShow(event); },
options
);
Event.on(
nestedElement.parentNode,
'DOMAttrModified',
UserAgent.isIEBeforeIE9 ? 'propertychange' : 'DOMAttrModified',
function (event) { return self.onNestedShow(event); },
options
);
......@@ -138,35 +146,39 @@ define('TYPO3/CMS/Rtehtmlarea/HTMLArea/Editor/Iframe',
/**
* Create the iframe element at rendering time
*/
onRender: function (ct, position){
// from Ext.Component
if (!this.el && this.autoEl) {
if (typeof this.autoEl === 'string' && this.autoEl.length > 0) {
this.el = document.createElement(this.autoEl);
} else {
// ExtJS Default method will not work with iframe element
this.el = Ext.DomHelper.append(ct, this.autoEl, true);
*
* @param object container: the container into which to insert the iframe (that is the framework)
* @return void
*/
createIframe: function (container) {
if (this.autoEl && this.autoEl.tag) {
this.el = document.createElement(this.autoEl.tag);
if (this.autoEl.id) {
this.el.setAttribute('id', this.autoEl.id);
}
if (!this.el.id) {
this.el.id = this.getId();
if (this.autoEl.cls) {
this.el.setAttribute('class', this.autoEl.cls);
}
}
// from Ext.BoxComponent
if (this.resizeEl){
this.resizeEl = Ext.get(this.resizeEl);
}
if (this.positionEl){
this.positionEl = Ext.get(this.positionEl);
if (this.autoEl.src) {
this.el.setAttribute('src', this.autoEl.src);
}
this.el = container.appendChild(this.el);
}
},
/**
* Get the content window of the iframe
*/
getIframeWindow: function () {
return this.el.contentWindow ? this.el.contentWindow : this.el.contentDocument;
},
/**
* Proceed to build the iframe document head and ensure style sheets are available after the iframe document becomes available
*/
initializeIframe: function () {
var self = this;
var iframe = this.getEl().dom;
var iframe = this.getEl();
// All browsers
if (!iframe || (!iframe.contentWindow && !iframe.contentDocument)) {
window.setTimeout(function () {
......@@ -206,6 +218,21 @@ define('TYPO3/CMS/Rtehtmlarea/HTMLArea/Editor/Iframe',
}
},
/**
* Show the iframe
*/
show: function () {
this.getEl().style.display = '';
Event.trigger(this, 'HTMLAreaEventIframeShow');
},
/**
* Hide the iframe
*/
hide: function () {
this.getEl().style.display = 'none';
},
/**
* Create one of each of the configured custom tags so they are properly parsed by the walker when using IE
* See: http://en.wikipedia.org/wiki/HTML5_Shiv
......@@ -260,29 +287,32 @@ define('TYPO3/CMS/Rtehtmlarea/HTMLArea/Editor/Iframe',
this.getEditor().appendToLog('HTMLArea.Iframe', 'createHead', 'Content CSS set to: ' + link.href, 'info');
}
},
/*
/**
* Focus on the iframe
*/
focus: function () {
try {
if (UserAgent.isWebKit) {
this.getEl().dom.focus();
} else {
this.getEl().dom.contentWindow.focus();
this.getEl().focus();
}
this.getEl().contentWindow.focus();
} catch(e) { }
},
/*
/**
* Flag indicating whether the framework is inside a tab or inline element that may be hidden
* Should be set in config
*/
isNested: false,
/*
/**
* All nested tabs and inline levels in the sorting order they were applied
* Should be set in config
*/
nestedParentElements: {},
/*
/**
* Set designMode
*
* @param boolean on: if true set designMode to on, otherwise set to off
......@@ -315,7 +345,8 @@ define('TYPO3/CMS/Rtehtmlarea/HTMLArea/Editor/Iframe',
}
}
},
/*
/**
* Set editing mode options (if we can... raises exception in Firefox 3)
*
* @return void
......@@ -354,12 +385,10 @@ define('TYPO3/CMS/Rtehtmlarea/HTMLArea/Editor/Iframe',
window.setTimeout(function () {
var styleEvent = true;
// In older versions of Gecko attrName is not set and refering to it causes a non-catchable crash
if ((UserAgent.isGecko && navigator.productSub > 2007112700) || UserAgent.isOpera) {
//styleEvent = (event.browserEvent.attrName == 'style') || (event.browserEvent.attrName == 'className');
styleEvent = (event.attrName == 'style') || (event.attrName == 'className');
} else if (UserAgent.isIE) {
//styleEvent = (event.browserEvent.propertyName == 'style.display');
styleEvent = (event.propertyName == 'style.display');
if ((UserAgent.isGecko && navigator.productSub > 2007112700) || UserAgent.isOpera || (UserAgent.isIE && !UserAgent.isIEBeforeIE9)) {
styleEvent = (event.originalEvent.attrName === 'style') || (event.originalEvent.attrName === 'className') || (event.originalEvent.attrName === 'class');
} else if (UserAgent.isIEBeforeIE9) {
styleEvent = (event.originalEvent.propertyName === 'style.display');
}
if (styleEvent && (self.nestedParentElements.sorted.indexOf(target.id) != -1 || self.nestedParentElements.sorted.indexOf(target.id.replace('_div', '_fields')) != -1)) {
// Check if all container nested elements are displayed
......@@ -368,7 +397,7 @@ define('TYPO3/CMS/Rtehtmlarea/HTMLArea/Editor/Iframe',
if (UserAgent.isGecko) {
self.setDesignMode(true);
}
self.fireEvent('show');
Event.trigger(self, 'HTMLAreaEventIframeShow');
} else {
self.framework.textAreaContainer.fireEvent('show');
}
......@@ -732,6 +761,15 @@ define('TYPO3/CMS/Rtehtmlarea/HTMLArea/Editor/Iframe',
return false;
},
/**
* When the editor becomes ready
*/
onEditorReady: function () {
var self = this;
// Monitor editor being unloaded
Event.one(this.getIframeWindow(), 'unload', function (event) { return self.onBeforeDestroy(); });
},
/**
* Cleanup
*/
......@@ -745,16 +783,17 @@ define('TYPO3/CMS/Rtehtmlarea/HTMLArea/Editor/Iframe',
}
}
Event.off(this);
Event.off(this.getEl().dom);
Event.off(this.getEl());
Event.off(this.document.body);
Event.off(this.document.documentElement);
// Cleaning references to DOM in order to avoid IE memory leaks
this.document = null;
this.getEditor().document = null;
Ext.destroy(this.autoEl, this.el, this.resizeEl, this.positionEl);
this.el = null;
delete this.el;
return true;
}
});
};
return Iframe;
......
......@@ -32,7 +32,7 @@ define('TYPO3/CMS/Rtehtmlarea/HTMLArea/Editor/StatusBar',
/**
* Render the status bar (called by framework rendering)
*
* @param object container: the container into which to insert the status bar (that is the farmework
* @param object container: the container into which to insert the status bar (that is the framework)
* @return void
*/
render: function (container) {
......@@ -60,7 +60,9 @@ define('TYPO3/CMS/Rtehtmlarea/HTMLArea/Editor/StatusBar',
// Monitor editor changing mode
Event.on(this.getEditor(), 'HTMLAreaEventModeChange', function (event, mode) { Event.stopEvent(event); self.onModeChange(mode); return false; });
// Monitor word count change
Event.on(this.framework.iframe, 'HTMLAreaEventWordCountChange', function (event, delay) { Event.stopEvent(event); self.onWordCountChange(delay); return false; });
Event.on(this.framework.iframe, 'HTMLAreaEventWordCountChange', function (event, delay) { Event.stopEvent(event); self.onWordCountChange(delay); return false; });
// Monitor editor being unloaded
Event.one(this.framework.iframe.getIframeWindow(), 'unload', function (event) { return self.onBeforeDestroy(); });
},
/**
......@@ -120,17 +122,14 @@ define('TYPO3/CMS/Rtehtmlarea/HTMLArea/Editor/StatusBar',
* Clear the status bar tree
*/
clear: function () {
var node = this.statusBarTree.firstChild;
while (node) {
var node;
while (node = this.statusBarTree.firstChild) {
if (/^(a)$/i.test(node.nodeName)) {
Event.off(node);
var extNode = Ext.get(node);
Ext.QuickTips.unregister(extNode);
extNode.dom = null;
node.removeAttribute('ext:qtitle');
node.removeAttribute('ext:qtip');
}
var nextNode = node.nextSibling;
Dom.removeFromParent(node);
var node = nextNode;
}
this.setSelection(null);
},
......@@ -337,13 +336,16 @@ define('TYPO3/CMS/Rtehtmlarea/HTMLArea/Editor/StatusBar',
/**
* Cleanup
*/
destroy: function() {
onBeforeDestroy: function() {
this.clear();
while (this.el.firstChild) {
this.el.removeChild(this.el.firstChild);
while (node = this.el.firstChild) {
this.el.removeChild(node);
}
this.statusBarTree = null;
this.statusBarWordCount = null;
this.el = null;
delete this.el;
return true;
}
};
......
......@@ -41,15 +41,9 @@ define('TYPO3/CMS/Rtehtmlarea/HTMLArea/Editor/Toolbar',
* Initialize listeners
*/
initEventListeners: function () {
this.addListener({
beforedestroy: {
fn: this.onBeforeDestroy,
single: true
}
});
// Monitor editor becoming ready
var self = this;
Event.one(this.getEditor(), 'HtmlAreaEventEditorReady', function (event) { Event.stopEvent(event); self.update(); return false; });
Event.one(this.getEditor(), 'HtmlAreaEventEditorReady', function (event) { Event.stopEvent(event); self.onEditorReady(); return false; });
},
/**
......@@ -180,6 +174,16 @@ define('TYPO3/CMS/Rtehtmlarea/HTMLArea/Editor/Toolbar',
Event.trigger(this, 'HTMLAreaEventToolbarUpdate', [mode, selectionEmpty, ancestors, endPointsInSameBlock]);
},
/**
* When the editor becomes ready
*/
onEditorReady: function () {
var self = this;
// Monitor editor being unloaded
Event.one(this.framework.iframe.getIframeWindow(), 'unload', function (event) { return self.onBeforeDestroy(); });
this.update();
},
/**
* Cleanup
*/
......
......@@ -85,8 +85,7 @@ define('TYPO3/CMS/Rtehtmlarea/Plugins/ContextMenu',
// Monitor contextmenu clicks on the iframe
Event.on(this.editor.document.documentElement, 'contextmenu', function (event) { return self.show(event, event.target); });
// Monitor editor being unloaded
var iframe = this.editor.iframe.getEl().dom;
Event.one(iframe.contentWindow ? iframe.contentWindow : iframe.contentDocument, 'unload', function (event) { self.onBeforeDestroy(event); return true; });
Event.one(this.editor.iframe.getIframeWindow(), 'unload', function (event) { self.onBeforeDestroy(event); return true; });
},
/**
......@@ -192,9 +191,10 @@ define('TYPO3/CMS/Rtehtmlarea/Plugins/ContextMenu',
if (!UserAgent.isIEBeforeIE9) {
this.ranges = this.editor.getSelection().getRanges();
}
var iframeEl = this.editor.iframe.getEl();
// Show the context menu
this.menu.showAt([Ext.fly(target).getX() + iframeEl.getX(), Ext.fly(target).getY() + iframeEl.getY()]);
// Show the context menu
var targetPosition = Dom.getPosition(target);
var iframePosition = Dom.getPosition(this.editor.iframe.getEl());
this.menu.showAt([targetPosition.x + iframePosition.x, targetPosition.y + iframePosition.y]);
},
/**
......@@ -223,9 +223,9 @@ define('TYPO3/CMS/Rtehtmlarea/Plugins/ContextMenu',
if (/^(html|body)$/i.test(target.nodeName)) {
this.deleteTarget = null;
} else if (/^(table|thead|tbody|tr|td|th|tfoot)$/i.test(target.nodeName)) {
this.deleteTarget = Ext.fly(target).findParent('table');
this.deleteTarget = Dom.getFirstAncestorOfType(target, 'table');
} else if (/^(ul|ol|dl|li|dd|dt)$/i.test(target.nodeName)) {
this.deleteTarget = Ext.fly(target).findParent('ul') || Ext.fly(target).findParent('ol') || Ext.fly(target).findParent('dl');
this.deleteTarget = Dom.getFirstAncestorOfType(target, ['ul', 'ol', 'dl']);
}
if (this.deleteTarget) {
menuItem.setVisible(true);
......
......@@ -111,7 +111,7 @@ define('TYPO3/CMS/Rtehtmlarea/Plugins/TextIndicator',
} catch (e) { }
// queryCommandValue does not work in Gecko