BUGFIX: Use new jQuery.Entwine event capturing, onadd and onremove features to plug some memory leaks

This commit is contained in:
Hamish Friedlander 2012-06-12 22:48:08 +12:00 committed by Ingo Schommer
parent bbd1bb7495
commit b86a787521
12 changed files with 204 additions and 192 deletions

View File

@ -14,12 +14,6 @@
* .cms-edit-form
*/
$('.cms-edit-form.cms-add-form').entwine({
/**
* Variable: Tree
* (DOMElement)
*/
Tree: null,
/**
* Variable: OrigOptions
* (Array) Map of <option> values to an object of "title" and "value"
@ -31,18 +25,24 @@
* (Array) Internal counter to create unique page identifiers prior to ajax saving
*/
NewPages: [],
getTree: function() {
return $('.cms-tree');
},
fromTree: {
onselect_node: function(e, data){
self.refresh(data.rslt.obj);
}
},
/**
* Constructor: onmatch
*/
onmatch: function() {
onadd: function() {
var self = this, typeDropdown = this.find(':input[name=PageType]');
var tree = $('.cms-tree');
this.setTree(tree);
// Event bindings
$(tree).bind('select_node.jstree', function(e, data) {self.refresh(data.rslt.obj);});
typeDropdown.bind('change', function(e) {self.refresh();});
// TODO Bind on tree initialization to set dropdown for selected node
@ -56,9 +56,7 @@
this._super();
},
onunmatch: function() {
this._super();
},
/**
* Function: onsubmit
*

View File

@ -18,12 +18,6 @@
*/
$('#Form_BatchActionsForm').entwine({
/**
* Variable: Tree
* (DOMElement)
*/
Tree: null,
/**
* Variable: Actions
* (Array) Stores all actions that can be performed on the collected IDs as
@ -31,38 +25,40 @@
* a confirmation message, etc.
*/
Actions: [],
getTree: function() {
return $('.cms-tree');
},
fromTree: {
oncheck_node: function(e, data){
this.serializeFromTree();
}
},
/**
* Constructor: onmatch
*/
onmatch: function() {
var self = this, tree = $('.cms-tree');
this.setTree(tree);
tree.bind('check_node.jstree', function(e, data) {
self.serializeFromTree();
});
$('.cms-tree-view-modes :input[name=view-mode]').bind('click', function(e) {
var val = $(e.target).val(), dropdown = self.find(':input[name=Action]');
onadd: function() {
this._updateStateFromViewMode();
this._super();
},
'from .cms-tree-view-modes :input[name=view-mode]': {
onclick: function(e){
var val = $(e.target).val(), dropdown = this.find(':input[name=Action]'), tree = this.getTree();
if(val == 'multiselect') {
tree.addClass('multiple');
self.serializeFromTree();
this.serializeFromTree();
} else {
tree.removeClass('multiple');
tree.removeClass('multiple');
}
self._updateStateFromViewMode();
});
self._updateStateFromViewMode();
this._super();
},
onunmatch: function() {
this._super();
this._updateStateFromViewMode();
}
},
/**
* Updates the select box state according to the current view mode.
*/

View File

@ -10,7 +10,7 @@
*/
$('.cms-content').entwine({
onmatch: function() {
onadd: function() {
var self = this;
// Force initialization of certain UI elements to avoid layout glitches
@ -19,10 +19,7 @@
this._super();
},
onunmatch: function() {
this._super();
},
redraw: function() {
if(window.debug) console.log('redraw', this.attr('class'), this.get(0));
@ -39,7 +36,7 @@
* Load edit form for the selected node when its clicked.
*/
$('.cms-content .cms-tree').entwine({
onmatch: function() {
onadd: function() {
var self = this;
this._super();
@ -80,9 +77,6 @@
self.removeForm();
}
});
},
onunmatch: function() {
this._super();
}
});

View File

@ -50,7 +50,7 @@
/**
* Constructor: onmatch
*/
onmatch: function() {
onadd: function() {
var self = this;
// Turn off autocomplete to fix the access tab randomly switching radio buttons in Firefox
@ -106,7 +106,7 @@
this._super();
},
onunmatch: function() {
onremove: function() {
this.changetracker('destroy');
this._super();
},

View File

@ -78,23 +78,6 @@
onmatch: function() {
var self = this;
var updateMenuFromResponse = function(xhr) {
var controller = xhr.getResponseHeader('X-Controller');
if(controller) {
var item = self.find('li#Menu-' + controller);
if(!item.hasClass('current')) item.select();
}
self.updateItems();
};
$('.cms-container').live('afterstatechange aftersubmitform', function(e, data) {
updateMenuFromResponse(data.xhr);
});
// Sync collapsed state with parent panel
this.parents('.cms-panel:first').bind('toggle', function(e) {
self.toggleClass('collapsed', $(this).hasClass('collapsed'));
});
// Select default element (which might reveal children in hidden parents)
this.find('li.current').select();
@ -105,6 +88,41 @@
onunmatch: function() {
this._super();
},
updateMenuFromResponse: function(xhr) {
var controller = xhr.getResponseHeader('X-Controller');
if(controller) {
var item = this.find('li#Menu-' + controller);
if(!item.hasClass('current')) item.select();
}
this.updateItems();
},
'from .cms-container': {
onafterstatechange: function(e, data){
this.updateMenuFromResponse(data.xhr);
},
onaftersubmitform: function(e, data){
this.updateMenuFromResponse(data.xhr);
}
},
'from .cms-edit-form': {
onrelodeditform: function(e, data){
this.updateMenuFromResponse(data.xmlhttp);
}
},
getContainingPanel: function(){
return this.closest('.cms-panel');
},
fromContainingPanel: {
ontoggle: function(e){
this.toggleClass('collapsed', $(e.target).hasClass('collapsed'));
}
},
updateItems: function() {
// Hide "edit page" commands unless the section is activated
var editPageItem = this.find('#Menu-CMSMain');

View File

@ -11,15 +11,11 @@
*/
PingIntervalSeconds: 5*60,
onmatch: function() {
onadd: function() {
this._setupPinging();
this._super();
},
onunmatch: function() {
this._super();
},
/**
* Function: _setupPinging
*

View File

@ -43,34 +43,6 @@
if(!self.is('.is-collapsed')) self.loadCurrentPage();
});
var updateAfterXhr = function() {
$('.cms-preview-toggle-link')[self.canPreview() ? 'show' : 'hide']();
// Only load when panel is visible (see details in iframe load event handler).
if(self.is('.is-collapsed')) return;
// var url = ui.xmlhttp.getResponseHeader('x-frontend-url');
var url = $('.cms-edit-form').find(':input[name=StageURLSegment]').val();
if(url) {
self.loadUrl(url);
self.unblock();
} else {
self.block();
}
};
// Listen to history state changes
$('.cms-container').bind('afterstatechange aftersubmitform', function(e) {
updateAfterXhr();
});
// Toggle preview when new menu entry is selected.
// Only do this when preview is actually shown,
// to avoid auto-expanding the menu in normal CMS mode
$('.cms-menu-list li').bind('select', function(e) {
if(!self.hasClass('is-collapsed')) self.collapse();
});
if(this.hasClass('is-expanded')) this.expand();
else this.collapse();
this.data('cms-preview-initialized', true);
@ -91,6 +63,40 @@
this.find('iframe').attr('src', url);
},
updateAfterXhr: function(){
$('.cms-preview-toggle-link')[this.canPreview() ? 'show' : 'hide']();
// Only load when panel is visible (see details in iframe load event handler).
if(this.is('.is-collapsed')) return;
// var url = ui.xmlhttp.getResponseHeader('x-frontend-url');
var url = $('.cms-edit-form').find(':input[name=StageURLSegment]').val();
if(url) {
this.loadUrl(url);
this.unblock();
} else {
this.block();
}
},
'from .cms-container': {
onaftersubmitform: function(){
this.updateAfterXhr();
},
onafterstatechange: function(){
this.updateAfterXhr();
}
},
// Toggle preview when new menu entry is selected.
// Only do this when preview is actually shown,
// to avoid auto-expanding the menu in normal CMS mode
'from .cms-menu-list li': {
onselect: function(){
if(!this.hasClass('is-collapsed')) this.collapse();
}
},
/**
* Loads the matching edit form for a page viewed in the preview iframe,
* based on metadata sent along with this document.

View File

@ -9,8 +9,8 @@
$('.cms-tree').entwine({
Hints: null,
onmatch: function() {
onadd: function(){
this._super();
// Don't reapply (expensive) tree behaviour if already present
@ -96,16 +96,22 @@
}
});
});
$('.cms-container').bind('afterstatechange.tree aftersubmitform.tree', function(e, data) {
self.updateFromEditForm(e.origData);
});
},
onunmatch: function() {
$('.cms-container').unbind('afterstatechange.tree aftersubmitform.tree');
onremove: function(){
this.jstree('destroy');
this._super();
},
'from .cms-container': {
onafterstatechange: function(e){
this.updateFromEditForm(e.origData);
},
onaftersubmitform: function(e){
this.updateFromEditForm(e.origData);
}
},
getTreeConfig: function() {
var self = this;
return {

View File

@ -73,7 +73,7 @@ jQuery.noConflict();
/**
* Constructor: onmatch
*/
onmatch: function() {
onadd: function() {
var self = this;
// Browser detection
@ -92,34 +92,27 @@ jQuery.noConflict();
// Initialize layouts
this.redraw();
// Monitor window resizes, panel changes and edit form loads for layout changes.
// Also triggers redraw through handleStateChange()
$(window).resize(function() {
self.redraw();
});
$('.cms-panel').live('toggle', function() {
self.redraw();
});
// Remove loading screen
$('.ss-loading-screen').hide();
$('body').removeClass('loading');
$(window).unbind('resize', positionLoadingSpinner);
History.Adapter.bind(window,'statechange',function(){
self.handleStateChange();
});
this._super();
},
this._super();
fromWindow: {
onstatechange: function(){ this.handleStateChange(); },
onresize: function(){ this.redraw(); }
},
onunmatch: function() {
this._super();
'from .cms-panel': {
ontoggle: function(){ this.redraw(); }
},
onaftersubmitform: function() {
this.redraw();
'from .cms-container': {
onaftersubmitform: function(){ this.redraw(); }
},
redraw: function() {
if(window.debug) console.log('redraw', this.attr('class'), this.get(0));
@ -457,12 +450,13 @@ jQuery.noConflict();
});
$('.cms .ss-ui-button').entwine({
onmatch: function() {
onadd: function() {
if(!this.data('button')) this.button();
this._super();
},
onunmatch: function() {
onremove: function() {
this.button('destroy');
this._super();
}
});
@ -744,11 +738,12 @@ jQuery.noConflict();
*/
window._panelDeferredCache = {};
$('.cms-panel-deferred').entwine({
onmatch: function() {
onadd: function() {
this._super();
this.redraw();
},
onunmatch: function() {
onremove: function() {
console.log('saving', this.data('url'), this);
// Save the HTML state at the last possible moment.
// Don't store the DOM to avoid memory leaks.
if(!this.data('deferredNoCache')) window._panelDeferredCache[this.data('url')] = this.html();
@ -790,12 +785,13 @@ jQuery.noConflict();
* for forms inside the CMS layout.
*/
$('.cms-tabset').entwine({
onmatch: function() {
onadd: function() {
// Can't name redraw() as it clashes with other CMS entwine classes
this.redrawTabs();
this._super();
},
onunmatch: function() {
onremove: function() {
this.tabs('destroy');
this._super();
},
redrawTabs: function() {

View File

@ -5,13 +5,8 @@
* Creates a jQuery UI tab navigation bar, detached from the container DOM structure.
*/
$('.ss-ui-tabs-nav').entwine({
onmatch: function() {
onadd: function() {
this.redraw();
this._super();
},
onunmatch: function() {
this._super();
},
redraw: function() {
this.addClass('ui-tabs ui-widget ui-widget-content ui-corner-all ui-tabs-panel ui-corner-bottom');

View File

@ -198,44 +198,47 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
$('textarea.htmleditor').entwine({
Editor: null,
/**
* Constructor: onmatch
*/
onmatch : function() {
var self = this, edClass = this.data('editor') || ss.editorWrappers['default'], ed = edClass();
onadd: function() {
var edClass = this.data('editor') || ss.editorWrappers['default'], ed = edClass();
this.setEditor(ed);
this.closest('form').bind('beforesubmitform', function() {
// TinyMCE modifies input, so change tracking might get false
// positives when comparing string values - don't save if the editor doesn't think its dirty.
if(self.isChanged()) {
ed.save();
// TinyMCE assigns value attr directly, which doesn't trigger change event
self.trigger('change');
}
});
// Using a global config (generated through HTMLEditorConfig PHP logic).
// Depending on browser cache load behaviour, entwine's DOMMaybeChanged
// can be called before the bottom-most inline script tag is executed,
// which defines the global. If that's the case, wait for the window load.
if(typeof ssTinyMceConfig != 'undefined') {
this.redraw();
} else {
$(window).bind('load', function() {
self.redraw();
});
}
if(typeof ssTinyMceConfig != 'undefined') this.redraw();
this._super();
},
onunmatch: function() {
// TODO Throws exceptions in Firefox, most likely due to the element being removed from the DOM at this point
// var ed = tinyMCE.get(this.attr('id'));
// if(ed) ed.remove();
onremove: function() {
var ed = tinyMCE.get(this.attr('id'));
if (ed) ed.remove();
this._super();
},
getContainingForm: function(){
return this.closest('form');
},
fromContainingForm: {
onbeforesave: function(){
if(this.isChanged()) {
this.getEditor().save();
this.trigger('change'); // TinyMCE assigns value attr directly, which doesn't trigger change event
}
}
},
fromWindow: {
onload: function(){
this.redraw();
}
},
redraw: function() {
// Using a global config (generated through HTMLEditorConfig PHP logic)
var config = ssTinyMceConfig, self = this, ed = this.getEditor();
@ -253,16 +256,18 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
self.css('visibility', 'visible');
});
// Handle editor de-registration by hooking into state changes.
// TODO Move to onunmatch for less coupling (once we figure out how to work with detached DOM nodes in TinyMCE)
$('.cms-container').bind('beforestatechange', function() {
self.css('visibility', 'hidden');
var container = ed.getInstance() ? ed.getContainer() : null;
if(container && container.length) container.remove();
});
this._super();
},
'from .cms-container': {
onbeforestatechange: function(){
this.css('visibility', 'hidden');
var ed = this.getEditor(), container = ed.getInstance() ? ed.getContainer() : null;
if(container && container.length) container.remove();
}
},
isChanged: function() {
var ed = this.getEditor();
return (ed && ed.getInstance() && ed.isDirty());
@ -1103,17 +1108,21 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
});
$('div.ss-assetuploadfield .ss-uploadfield-item-edit, div.ss-assetuploadfield .ss-uploadfield-item-name').entwine({
getEditForm: function() {
return this.closest('.ss-uploadfield-item').find('.ss-uploadfield-item-editform');
},
fromEditForm: {
onchange: function(e){
var form = $(e.target);
form.removeClass('edited'); //so edited class is only there once
form.addClass('edited');
}
},
onclick: function(e) {
var editForm = this.closest('.ss-uploadfield-item').find('.ss-uploadfield-item-editform');
var editForm = this.getEditForm();
// Mark the row as changed if any of its form fields are edited
editForm.ready(function() {
editForm.find(':input').bind('change', function(e){
editForm.removeClass('edited'); //so edited class is only there once
editForm.addClass('edited');
});
});
editForm.parent('.ss-uploadfield-item').removeClass('ui-state-warning');
editForm.toggleEditForm();
@ -1153,7 +1162,7 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
$('form.htmleditorfield-mediaform #ParentID .TreeDropdownField').entwine({
onmatch: function() {
onadd: function() {
this._super();
// TODO Custom event doesn't fire in IE if registered through object literal
@ -1163,9 +1172,6 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE;
fileList.setState('ParentID', self.getValue());
fileList.reload();
});
},
onunmatch: function() {
this._super();
}
});

View File

@ -4,12 +4,13 @@
* Lightweight wrapper around jQuery UI tabs.
*/
$('.ss-tabset').entwine({
onmatch: function() {
onadd: function() {
// Can't name redraw() as it clashes with other CMS entwine classes
this.redrawTabs();
this._super();
},
onunmatch: function() {
onremove: function() {
this.tabs('destroy');
this._super();
},
redrawTabs: function() {