Fixed #278: Implemented method `editor.destroy()` to properly cleanup the editor

This commit is contained in:
jos 2016-03-20 14:54:47 +01:00
parent 61c6f21f90
commit a78b2ae57a
10 changed files with 143 additions and 64 deletions

View File

@ -5,6 +5,7 @@ https://github.com/josdejong/jsoneditor
## not yet released, version 5.2.0 ## not yet released, version 5.2.0
- Implemented method `editor.destroy()` to properly cleanup the editor (#278).
- Fixed #268: JSONEditor now trims text in fields and values. - Fixed #268: JSONEditor now trims text in fields and values.

View File

@ -111,6 +111,10 @@ Constructs a new JSONEditor.
Collapse all fields. Only applicable for mode 'tree', 'view', and 'form'. Collapse all fields. Only applicable for mode 'tree', 'view', and 'form'.
#### `JSONEditor.destroy()`
Destroy the editor. Clean up DOM, event listeners, and web workers.
#### `JSONEditor.expandAll()` #### `JSONEditor.expandAll()`
Expand all fields. Only applicable for mode 'tree', 'view', and 'form'. Expand all fields. Only applicable for mode 'tree', 'view', and 'form'.

View File

@ -19,7 +19,7 @@
"bugs": "https://github.com/josdejong/jsoneditor/issues", "bugs": "https://github.com/josdejong/jsoneditor/issues",
"scripts": { "scripts": {
"build": "gulp", "build": "gulp",
"watch": "watch", "watch": "gulp watch",
"test": "mocha test" "test": "mocha test"
}, },
"dependencies": { "dependencies": {

View File

@ -7,6 +7,9 @@ var util = require('./util');
*/ */
function History (editor) { function History (editor) {
this.editor = editor; this.editor = editor;
this.history = [];
this.index = -1;
this.clear(); this.clear();
// map with all supported actions // map with all supported actions
@ -249,4 +252,14 @@ History.prototype.redo = function () {
} }
}; };
/**
* Destroy history
*/
History.prototype.destroy = function () {
this.editor = null;
this.history = [];
this.index = -1;
};
module.exports = History; module.exports = History;

View File

@ -130,10 +130,9 @@ JSONEditor.prototype._create = function (container, options, json) {
}; };
/** /**
* Detach the editor from the DOM * Destroy the editor. Clean up DOM, event listeners, and web workers.
* @private
*/ */
JSONEditor.prototype._delete = function () {}; JSONEditor.prototype.destroy = function () {};
/** /**
* Set JSON object in editor * Set JSON object in editor
@ -207,7 +206,7 @@ JSONEditor.prototype.setMode = function (mode) {
name = this.getName(); name = this.getName();
data = this[asText ? 'getText' : 'get'](); // get text or json data = this[asText ? 'getText' : 'get'](); // get text or json
this._delete(); this.destroy();
util.clear(this); util.clear(this);
util.extend(this, config.mixin); util.extend(this, config.mixin);
this.create(container, options); this.create(container, options);

View File

@ -2,64 +2,48 @@ var ContextMenu = require('./ContextMenu');
/** /**
* Create a select box to be used in the editor menu's, which allows to switch mode * Create a select box to be used in the editor menu's, which allows to switch mode
* @param {Object} editor * @param {HTMLElement} container
* @param {String[]} modes Available modes: 'code', 'form', 'text', 'tree', 'view' * @param {String[]} modes Available modes: 'code', 'form', 'text', 'tree', 'view'
* @param {String} current Available modes: 'code', 'form', 'text', 'tree', 'view' * @param {String} current Available modes: 'code', 'form', 'text', 'tree', 'view'
* @returns {HTMLElement} box * @param {function(mode: string)} onSwitch Callback invoked on switch
* @constructor
*/ */
function createModeSwitcher(editor, modes, current) { function ModeSwitcher(container, modes, current, onSwitch) {
// TODO: decouple mode switcher from editor
/**
* Switch the mode of the editor
* @param {String} mode
*/
function switchMode(mode) {
// switch mode
editor.setMode(mode);
// restore focus on mode box
var modeBox = editor.dom && editor.dom.modeBox;
if (modeBox) {
modeBox.focus();
}
}
// available modes // available modes
var availableModes = { var availableModes = {
code: { code: {
'text': 'Code', 'text': 'Code',
'title': 'Switch to code highlighter', 'title': 'Switch to code highlighter',
'click': function () { 'click': function () {
switchMode('code') onSwitch('code')
} }
}, },
form: { form: {
'text': 'Form', 'text': 'Form',
'title': 'Switch to form editor', 'title': 'Switch to form editor',
'click': function () { 'click': function () {
switchMode('form'); onSwitch('form');
} }
}, },
text: { text: {
'text': 'Text', 'text': 'Text',
'title': 'Switch to plain text editor', 'title': 'Switch to plain text editor',
'click': function () { 'click': function () {
switchMode('text'); onSwitch('text');
} }
}, },
tree: { tree: {
'text': 'Tree', 'text': 'Tree',
'title': 'Switch to tree editor', 'title': 'Switch to tree editor',
'click': function () { 'click': function () {
switchMode('tree'); onSwitch('tree');
} }
}, },
view: { view: {
'text': 'View', 'text': 'View',
'title': 'Switch to tree view', 'title': 'Switch to tree view',
'click': function () { 'click': function () {
switchMode('view'); onSwitch('view');
} }
} }
}; };
@ -94,12 +78,35 @@ function createModeSwitcher(editor, modes, current) {
menu.show(box); menu.show(box);
}; };
var div = document.createElement('div'); var frame = document.createElement('div');
div.className = 'jsoneditor-modes'; frame.className = 'jsoneditor-modes';
div.style.position = 'relative'; frame.style.position = 'relative';
div.appendChild(box); frame.appendChild(box);
return div; container.appendChild(frame);
this.dom = {
container: container,
box: box,
frame: frame
};
} }
exports.create = createModeSwitcher; /**
* Set focus to switcher
*/
ModeSwitcher.prototype.focus = function () {
this.dom.box.focus();
};
/**
* Destroy the ModeSwitcher, remove from DOM
*/
ModeSwitcher.prototype.destroy = function () {
if (this.dom && this.dom.frame && this.dom.frame.parentNode) {
this.dom.frame.parentNode.removeChild(this.dom.frame);
}
this.dom = null;
};
module.exports = ModeSwitcher;

View File

@ -2073,7 +2073,6 @@ Node.prototype.onEvent = function (event) {
target = event.target || event.srcElement, target = event.target || event.srcElement,
dom = this.dom, dom = this.dom,
node = this, node = this,
focusNode,
expandable = this._hasChilds(); expandable = this._hasChilds();
// check if mouse is on menu or on dragarea. // check if mouse is on menu or on dragarea.
@ -2123,7 +2122,7 @@ Node.prototype.onEvent = function (event) {
//noinspection FallthroughInSwitchStatementJS //noinspection FallthroughInSwitchStatementJS
switch (type) { switch (type) {
case 'focus': case 'focus':
focusNode = this; this.editor.focusNode = this;
break; break;
case 'blur': case 'blur':
@ -2176,7 +2175,7 @@ Node.prototype.onEvent = function (event) {
if (target == domField) { if (target == domField) {
switch (type) { switch (type) {
case 'focus': case 'focus':
focusNode = this; this.editor.focusNode = this;
break; break;
case 'blur': case 'blur':

View File

@ -292,4 +292,19 @@ SearchBox.prototype.clear = function () {
this._onSearch(); this._onSearch();
}; };
/**
* Destroy the search box
*/
SearchBox.prototype.destroy = function () {
this.editor = null;
this.dom.container.removeChild(this.dom.table);
this.dom = null;
this.results = null;
this.activeResult = null;
this._clearDelay();
};
module.exports = SearchBox; module.exports = SearchBox;

View File

@ -6,7 +6,7 @@ catch (err) {
// failed to load ace, no problem, we will fall back to plain text // failed to load ace, no problem, we will fall back to plain text
} }
var modeswitcher = require('./modeswitcher'); var ModeSwitcher = require('./ModeSwitcher');
var util = require('./util'); var util = require('./util');
// create a mixin with the functions for text mode // create a mixin with the functions for text mode
@ -123,9 +123,13 @@ textmode.create = function (container, options) {
// create mode box // create mode box
if (this.options && this.options.modes && this.options.modes.length) { if (this.options && this.options.modes && this.options.modes.length) {
var modeBox = modeswitcher.create(this, this.options.modes, this.options.mode); this.modeSwitcher = new ModeSwitcher(this.menu, this.options.modes, this.options.mode, function onSwitch(mode) {
this.menu.appendChild(modeBox); me.modeSwitcher.destroy();
this.dom.modeBox = modeBox;
// switch mode and restore focus
me.setMode(mode);
me.modeSwitcher.focus();
});
} }
this.content = document.createElement('div'); this.content = document.createElement('div');
@ -253,18 +257,27 @@ textmode._onKeyDown = function (event) {
}; };
/** /**
* Detach the editor from the DOM * Destroy the editor. Clean up DOM, event listeners, and web workers.
* @private
*/ */
textmode._delete = function () { textmode.destroy = function () {
// remove old ace editor // remove old ace editor
if (this.aceEditor) { if (this.aceEditor) {
this.aceEditor.destroy(); this.aceEditor.destroy();
this.aceEditor = null;
} }
if (this.frame && this.container && this.frame.parentNode == this.container) { if (this.frame && this.container && this.frame.parentNode == this.container) {
this.container.removeChild(this.frame); this.container.removeChild(this.frame);
} }
if (this.modeSwitcher) {
this.modeSwitcher.destroy();
this.modeSwitcher = null;
}
this.textarea = null;
this._debouncedValidate = null;
}; };
/** /**

View File

@ -3,7 +3,7 @@ var History = require('./History');
var SearchBox = require('./SearchBox'); var SearchBox = require('./SearchBox');
var ContextMenu = require('./ContextMenu'); var ContextMenu = require('./ContextMenu');
var Node = require('./Node'); var Node = require('./Node');
var modeswitcher = require('./modeswitcher'); var ModeSwitcher = require('./ModeSwitcher');
var util = require('./util'); var util = require('./util');
// create a mixin with the functions for tree mode // create a mixin with the functions for tree mode
@ -43,6 +43,8 @@ treemode.create = function (container, options) {
this.validateSchema = null; // will be set in .setSchema(schema) this.validateSchema = null; // will be set in .setSchema(schema)
this.errorNodes = []; this.errorNodes = [];
this.node = null;
this.focusNode = null;
this._setOptions(options); this._setOptions(options);
@ -55,12 +57,39 @@ treemode.create = function (container, options) {
}; };
/** /**
* Detach the editor from the DOM * Destroy the editor. Clean up DOM, event listeners, and web workers.
* @private
*/ */
treemode._delete = function () { treemode.destroy = function () {
if (this.frame && this.container && this.frame.parentNode == this.container) { if (this.frame && this.container && this.frame.parentNode == this.container) {
this.container.removeChild(this.frame); this.container.removeChild(this.frame);
this.frame = null;
}
this.container = null;
this.dom = null;
this.clear();
this.node = null;
this.focusNode = null;
this.selection = null;
this.multiselection = null;
this.errorNodes = null;
this.validateSchema = null;
this._debouncedValidate = null;
if (this.history) {
this.history.destroy();
this.history = null;
}
if (this.searchBox) {
this.searchBox.destroy();
this.searchBox = null;
}
if (this.modeSwitcher) {
this.modeSwitcher.destroy();
this.modeSwitcher = null;
} }
}; };
@ -94,12 +123,6 @@ treemode._setOptions = function (options) {
this._debouncedValidate = util.debounce(this.validate.bind(this), this.DEBOUNCE_INTERVAL); this._debouncedValidate = util.debounce(this.validate.bind(this), this.DEBOUNCE_INTERVAL);
}; };
// node currently being edited
var focusNode = undefined;
// dom having focus
var domFocus = null;
/** /**
* Set JSON object in editor * Set JSON object in editor
* @param {Object | undefined} json JSON data * @param {Object | undefined} json JSON data
@ -156,8 +179,8 @@ treemode.set = function (json, name) {
*/ */
treemode.get = function () { treemode.get = function () {
// remove focus from currently edited node // remove focus from currently edited node
if (focusNode) { if (this.focusNode) {
focusNode.blur(); this.focusNode.blur();
} }
if (this.node) { if (this.node) {
@ -520,7 +543,7 @@ treemode.getSelection = function () {
} }
return { return {
dom: domFocus, dom: this.dom.focus,
range: range, range: range,
nodes: this.multiselection.nodes.slice(0), nodes: this.multiselection.nodes.slice(0),
scrollTop: this.content ? this.content.scrollTop : 0 scrollTop: this.content ? this.content.scrollTop : 0
@ -686,9 +709,14 @@ treemode._createFrame = function () {
// create mode box // create mode box
if (this.options && this.options.modes && this.options.modes.length) { if (this.options && this.options.modes && this.options.modes.length) {
var modeBox = modeswitcher.create(this, this.options.modes, this.options.mode); var me = this;
this.menu.appendChild(modeBox); this.modeSwitcher = new ModeSwitcher(this.menu, this.options.modes, this.options.mode, function onSwitch(mode) {
this.dom.modeBox = modeBox; me.modeSwitcher.destroy();
// switch mode and restore focus
me.setMode(mode);
me.modeSwitcher.focus();
});
} }
// create search box // create search box
@ -736,7 +764,7 @@ treemode._onEvent = function (event) {
} }
if (event.type == 'focus') { if (event.type == 'focus') {
domFocus = event.target; this.dom.focus = event.target;
} }
if (event.type == 'mousedown') { if (event.type == 'mousedown') {
@ -1011,7 +1039,7 @@ treemode._onKeyDown = function (event) {
if (keynum == 9) { // Tab or Shift+Tab if (keynum == 9) { // Tab or Shift+Tab
setTimeout(function () { setTimeout(function () {
// select all text when moving focus to an editable div // select all text when moving focus to an editable div
util.selectContentEditable(domFocus); util.selectContentEditable(this.dom.focus);
}, 0); }, 0);
} }