diff --git a/HISTORY.md b/HISTORY.md
index 9b6b56b..e45f2e9 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -5,6 +5,8 @@ http://jsoneditoronline.org
## not yet released, version 3.0.0
+- Editor must be loaded as `new JSONEditor(...)` instead of
+ `new jsoneditor.JSONEditor(...)`.
- Large code reorganization.
diff --git a/README.md b/README.md
index b94e930..c702392 100644
--- a/README.md
+++ b/README.md
@@ -74,7 +74,7 @@ download:
+
diff --git a/examples/requirejs_demo/scripts/main.js b/examples/requirejs_demo/scripts/main.js
index 6727839..7c7c94f 100644
--- a/examples/requirejs_demo/scripts/main.js
+++ b/examples/requirejs_demo/scripts/main.js
@@ -1,8 +1,8 @@
var module = '../../../jsoneditor';
-require([module], function (jsoneditor) {
+require([module], function (JSONEditor) {
// create the editor
var container = document.getElementById('jsoneditor');
- var editor = new jsoneditor.JSONEditor(container);
+ var editor = new JSONEditor(container);
// set json
document.getElementById('setJSON').onclick = function () {
diff --git a/examples/requirejs_demo/scripts/require.js b/examples/requirejs_demo/scripts/require.js
index 8de013d..d65036f 100644
--- a/examples/requirejs_demo/scripts/require.js
+++ b/examples/requirejs_demo/scripts/require.js
@@ -1,35 +1,36 @@
/*
- RequireJS 2.1.2 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved.
+ RequireJS 2.1.13 Copyright (c) 2010-2014, The Dojo Foundation All Rights Reserved.
Available via the MIT or new BSD license.
see: http://github.com/jrburke/requirejs for details
*/
var requirejs,require,define;
-(function(Y){function H(b){return"[object Function]"===L.call(b)}function I(b){return"[object Array]"===L.call(b)}function x(b,c){if(b){var d;for(d=0;dthis.depCount&&!this.defined){if(H(n)){if(this.events.error)try{e=j.execCb(c,n,b,e)}catch(d){a=d}else e=j.execCb(c,n,b,e);this.map.isDefine&&((b=this.module)&&void 0!==b.exports&&b.exports!==this.exports?e=b.exports:void 0===e&&this.usingExports&&(e=this.exports));if(a)return a.requireMap=this.map,a.requireModules=[this.map.id],a.requireType="define",C(this.error=a)}else e=n;this.exports=e;if(this.map.isDefine&&
-!this.ignore&&(p[c]=e,l.onResourceLoad))l.onResourceLoad(j,this.map,this.depMaps);delete k[c];this.defined=!0}this.defining=!1;this.defined&&!this.defineEmitted&&(this.defineEmitted=!0,this.emit("defined",this.exports),this.defineEmitComplete=!0)}}else this.fetch()}},callPlugin:function(){var a=this.map,b=a.id,d=h(a.prefix);this.depMaps.push(d);s(d,"defined",t(this,function(e){var n,d;d=this.map.name;var v=this.map.parentMap?this.map.parentMap.name:null,f=j.makeRequire(a.parentMap,{enableBuildCallback:!0,
-skipMap:!0});if(this.map.unnormalized){if(e.normalize&&(d=e.normalize(d,function(a){return c(a,v,!0)})||""),e=h(a.prefix+"!"+d,this.map.parentMap),s(e,"defined",t(this,function(a){this.init([],function(){return a},null,{enabled:!0,ignore:!0})})),d=i(k,e.id)){this.depMaps.push(e);if(this.events.error)d.on("error",t(this,function(a){this.emit("error",a)}));d.enable()}}else n=t(this,function(a){this.init([],function(){return a},null,{enabled:!0})}),n.error=t(this,function(a){this.inited=!0;this.error=
-a;a.requireModules=[b];E(k,function(a){0===a.map.id.indexOf(b+"_unnormalized")&&delete k[a.map.id]});C(a)}),n.fromText=t(this,function(e,c){var d=a.name,u=h(d),v=O;c&&(e=c);v&&(O=!1);q(u);r(m.config,b)&&(m.config[d]=m.config[b]);try{l.exec(e)}catch(k){throw Error("fromText eval for "+d+" failed: "+k);}v&&(O=!0);this.depMaps.push(u);j.completeLoad(d);f([d],n)}),e.load(a.name,f,n,m)}));j.enable(d,this);this.pluginMaps[d.id]=d},enable:function(){this.enabling=this.enabled=!0;x(this.depMaps,t(this,function(a,
-b){var c,e;if("string"===typeof a){a=h(a,this.map.isDefine?this.map:this.map.parentMap,!1,!this.skipMap);this.depMaps[b]=a;if(c=i(N,a.id)){this.depExports[b]=c(this);return}this.depCount+=1;s(a,"defined",t(this,function(a){this.defineDep(b,a);this.check()}));this.errback&&s(a,"error",this.errback)}c=a.id;e=k[c];!r(N,c)&&(e&&!e.enabled)&&j.enable(a,this)}));E(this.pluginMaps,t(this,function(a){var b=i(k,a.id);b&&!b.enabled&&j.enable(a,this)}));this.enabling=!1;this.check()},on:function(a,b){var c=
-this.events[a];c||(c=this.events[a]=[]);c.push(b)},emit:function(a,b){x(this.events[a],function(a){a(b)});"error"===a&&delete this.events[a]}};j={config:m,contextName:b,registry:k,defined:p,urlFetched:S,defQueue:F,Module:W,makeModuleMap:h,nextTick:l.nextTick,configure:function(a){a.baseUrl&&"/"!==a.baseUrl.charAt(a.baseUrl.length-1)&&(a.baseUrl+="/");var b=m.pkgs,c=m.shim,e={paths:!0,config:!0,map:!0};E(a,function(a,b){e[b]?"map"===b?Q(m[b],a,!0,!0):Q(m[b],a,!0):m[b]=a});a.shim&&(E(a.shim,function(a,
-b){I(a)&&(a={deps:a});if((a.exports||a.init)&&!a.exportsFn)a.exportsFn=j.makeShimExports(a);c[b]=a}),m.shim=c);a.packages&&(x(a.packages,function(a){a="string"===typeof a?{name:a}:a;b[a.name]={name:a.name,location:a.location||a.name,main:(a.main||"main").replace(ga,"").replace(aa,"")}}),m.pkgs=b);E(k,function(a,b){!a.inited&&!a.map.unnormalized&&(a.map=h(b))});if(a.deps||a.callback)j.require(a.deps||[],a.callback)},makeShimExports:function(a){return function(){var b;a.init&&(b=a.init.apply(Y,arguments));
-return b||a.exports&&Z(a.exports)}},makeRequire:function(a,d){function f(e,c,u){var i,m;d.enableBuildCallback&&(c&&H(c))&&(c.__requireJsBuild=!0);if("string"===typeof e){if(H(c))return C(J("requireargs","Invalid require call"),u);if(a&&r(N,e))return N[e](k[a.id]);if(l.get)return l.get(j,e,a);i=h(e,a,!1,!0);i=i.id;return!r(p,i)?C(J("notloaded",'Module name "'+i+'" has not been loaded yet for context: '+b+(a?"":". Use require([])"))):p[i]}K();j.nextTick(function(){K();m=q(h(null,a));m.skipMap=d.skipMap;
-m.init(e,c,u,{enabled:!0});B()});return f}d=d||{};Q(f,{isBrowser:z,toUrl:function(b){var d=b.lastIndexOf("."),g=null;-1!==d&&(g=b.substring(d,b.length),b=b.substring(0,d));return j.nameToUrl(c(b,a&&a.id,!0),g)},defined:function(b){return r(p,h(b,a,!1,!0).id)},specified:function(b){b=h(b,a,!1,!0).id;return r(p,b)||r(k,b)}});a||(f.undef=function(b){w();var c=h(b,a,!0),d=i(k,b);delete p[b];delete S[c.url];delete X[b];d&&(d.events.defined&&(X[b]=d.events),delete k[b])});return f},enable:function(a){i(k,
-a.id)&&q(a).enable()},completeLoad:function(a){var b,c,d=i(m.shim,a)||{},h=d.exports;for(w();F.length;){c=F.shift();if(null===c[0]){c[0]=a;if(b)break;b=!0}else c[0]===a&&(b=!0);D(c)}c=i(k,a);if(!b&&!r(p,a)&&c&&!c.inited){if(m.enforceDefine&&(!h||!Z(h)))return y(a)?void 0:C(J("nodefine","No define call for "+a,null,[a]));D([a,d.deps||[],d.exportsFn])}B()},nameToUrl:function(a,b){var c,d,h,f,j,k;if(l.jsExtRegExp.test(a))f=a+(b||"");else{c=m.paths;d=m.pkgs;f=a.split("/");for(j=f.length;0f.attachEvent.toString().indexOf("[native code"))&&!V?(O=!0,f.attachEvent("onreadystatechange",
-b.onScriptLoad)):(f.addEventListener("load",b.onScriptLoad,!1),f.addEventListener("error",b.onScriptError,!1)),f.src=d,K=f,D?A.insertBefore(f,D):A.appendChild(f),K=null,f;$&&(importScripts(d),b.completeLoad(c))};z&&M(document.getElementsByTagName("script"),function(b){A||(A=b.parentNode);if(s=b.getAttribute("data-main"))return q.baseUrl||(G=s.split("/"),ba=G.pop(),ca=G.length?G.join("/")+"/":"./",q.baseUrl=ca,s=ba),s=s.replace(aa,""),q.deps=q.deps?q.deps.concat(s):[s],!0});define=function(b,c,d){var i,
-f;"string"!==typeof b&&(d=c,c=b,b=null);I(c)||(d=c,c=[]);!c.length&&H(d)&&d.length&&(d.toString().replace(ia,"").replace(ja,function(b,d){c.push(d)}),c=(1===d.length?["require"]:["require","exports","module"]).concat(c));if(O){if(!(i=K))P&&"interactive"===P.readyState||M(document.getElementsByTagName("script"),function(b){if("interactive"===b.readyState)return P=b}),i=P;i&&(b||(b=i.getAttribute("data-requiremodule")),f=B[i.getAttribute("data-requirecontext")])}(f?f.defQueue:R).push([b,c,d])};define.amd=
-{jQuery:!0};l.exec=function(b){return eval(b)};l(q)}})(this);
+(function(ba){function G(b){return"[object Function]"===K.call(b)}function H(b){return"[object Array]"===K.call(b)}function v(b,c){if(b){var d;for(d=0;dthis.depCount&&!this.defined){if(G(l)){if(this.events.error&&this.map.isDefine||g.onError!==ca)try{f=i.execCb(c,l,b,f)}catch(d){a=d}else f=i.execCb(c,l,b,f);this.map.isDefine&&void 0===f&&((b=this.module)?f=b.exports:this.usingExports&&
+(f=this.exports));if(a)return a.requireMap=this.map,a.requireModules=this.map.isDefine?[this.map.id]:null,a.requireType=this.map.isDefine?"define":"require",w(this.error=a)}else f=l;this.exports=f;if(this.map.isDefine&&!this.ignore&&(r[c]=f,g.onResourceLoad))g.onResourceLoad(i,this.map,this.depMaps);y(c);this.defined=!0}this.defining=!1;this.defined&&!this.defineEmitted&&(this.defineEmitted=!0,this.emit("defined",this.exports),this.defineEmitComplete=!0)}}else this.fetch()}},callPlugin:function(){var a=
+this.map,b=a.id,d=p(a.prefix);this.depMaps.push(d);q(d,"defined",u(this,function(f){var l,d;d=m(aa,this.map.id);var e=this.map.name,P=this.map.parentMap?this.map.parentMap.name:null,n=i.makeRequire(a.parentMap,{enableBuildCallback:!0});if(this.map.unnormalized){if(f.normalize&&(e=f.normalize(e,function(a){return c(a,P,!0)})||""),f=p(a.prefix+"!"+e,this.map.parentMap),q(f,"defined",u(this,function(a){this.init([],function(){return a},null,{enabled:!0,ignore:!0})})),d=m(h,f.id)){this.depMaps.push(f);
+if(this.events.error)d.on("error",u(this,function(a){this.emit("error",a)}));d.enable()}}else d?(this.map.url=i.nameToUrl(d),this.load()):(l=u(this,function(a){this.init([],function(){return a},null,{enabled:!0})}),l.error=u(this,function(a){this.inited=!0;this.error=a;a.requireModules=[b];B(h,function(a){0===a.map.id.indexOf(b+"_unnormalized")&&y(a.map.id)});w(a)}),l.fromText=u(this,function(f,c){var d=a.name,e=p(d),P=M;c&&(f=c);P&&(M=!1);s(e);t(j.config,b)&&(j.config[d]=j.config[b]);try{g.exec(f)}catch(h){return w(C("fromtexteval",
+"fromText eval for "+b+" failed: "+h,h,[b]))}P&&(M=!0);this.depMaps.push(e);i.completeLoad(d);n([d],l)}),f.load(a.name,n,l,j))}));i.enable(d,this);this.pluginMaps[d.id]=d},enable:function(){V[this.map.id]=this;this.enabling=this.enabled=!0;v(this.depMaps,u(this,function(a,b){var c,f;if("string"===typeof a){a=p(a,this.map.isDefine?this.map:this.map.parentMap,!1,!this.skipMap);this.depMaps[b]=a;if(c=m(L,a.id)){this.depExports[b]=c(this);return}this.depCount+=1;q(a,"defined",u(this,function(a){this.defineDep(b,
+a);this.check()}));this.errback&&q(a,"error",u(this,this.errback))}c=a.id;f=h[c];!t(L,c)&&(f&&!f.enabled)&&i.enable(a,this)}));B(this.pluginMaps,u(this,function(a){var b=m(h,a.id);b&&!b.enabled&&i.enable(a,this)}));this.enabling=!1;this.check()},on:function(a,b){var c=this.events[a];c||(c=this.events[a]=[]);c.push(b)},emit:function(a,b){v(this.events[a],function(a){a(b)});"error"===a&&delete this.events[a]}};i={config:j,contextName:b,registry:h,defined:r,urlFetched:S,defQueue:A,Module:Z,makeModuleMap:p,
+nextTick:g.nextTick,onError:w,configure:function(a){a.baseUrl&&"/"!==a.baseUrl.charAt(a.baseUrl.length-1)&&(a.baseUrl+="/");var b=j.shim,c={paths:!0,bundles:!0,config:!0,map:!0};B(a,function(a,b){c[b]?(j[b]||(j[b]={}),U(j[b],a,!0,!0)):j[b]=a});a.bundles&&B(a.bundles,function(a,b){v(a,function(a){a!==b&&(aa[a]=b)})});a.shim&&(B(a.shim,function(a,c){H(a)&&(a={deps:a});if((a.exports||a.init)&&!a.exportsFn)a.exportsFn=i.makeShimExports(a);b[c]=a}),j.shim=b);a.packages&&v(a.packages,function(a){var b,
+a="string"===typeof a?{name:a}:a;b=a.name;a.location&&(j.paths[b]=a.location);j.pkgs[b]=a.name+"/"+(a.main||"main").replace(ia,"").replace(Q,"")});B(h,function(a,b){!a.inited&&!a.map.unnormalized&&(a.map=p(b))});if(a.deps||a.callback)i.require(a.deps||[],a.callback)},makeShimExports:function(a){return function(){var b;a.init&&(b=a.init.apply(ba,arguments));return b||a.exports&&da(a.exports)}},makeRequire:function(a,e){function j(c,d,m){var n,q;e.enableBuildCallback&&(d&&G(d))&&(d.__requireJsBuild=
+!0);if("string"===typeof c){if(G(d))return w(C("requireargs","Invalid require call"),m);if(a&&t(L,c))return L[c](h[a.id]);if(g.get)return g.get(i,c,a,j);n=p(c,a,!1,!0);n=n.id;return!t(r,n)?w(C("notloaded",'Module name "'+n+'" has not been loaded yet for context: '+b+(a?"":". Use require([])"))):r[n]}J();i.nextTick(function(){J();q=s(p(null,a));q.skipMap=e.skipMap;q.init(c,d,m,{enabled:!0});D()});return j}e=e||{};U(j,{isBrowser:z,toUrl:function(b){var d,e=b.lastIndexOf("."),k=b.split("/")[0];if(-1!==
+e&&(!("."===k||".."===k)||1e.attachEvent.toString().indexOf("[native code"))&&!Y?(M=!0,e.attachEvent("onreadystatechange",b.onScriptLoad)):
+(e.addEventListener("load",b.onScriptLoad,!1),e.addEventListener("error",b.onScriptError,!1)),e.src=d,J=e,D?y.insertBefore(e,D):y.appendChild(e),J=null,e;if(ea)try{importScripts(d),b.completeLoad(c)}catch(m){b.onError(C("importscripts","importScripts failed for "+c+" at "+d,m,[c]))}};z&&!q.skipDataMain&&T(document.getElementsByTagName("script"),function(b){y||(y=b.parentNode);if(I=b.getAttribute("data-main"))return s=I,q.baseUrl||(E=s.split("/"),s=E.pop(),O=E.length?E.join("/")+"/":"./",q.baseUrl=
+O),s=s.replace(Q,""),g.jsExtRegExp.test(s)&&(s=I),q.deps=q.deps?q.deps.concat(s):[s],!0});define=function(b,c,d){var e,g;"string"!==typeof b&&(d=c,c=b,b=null);H(c)||(d=c,c=null);!c&&G(d)&&(c=[],d.length&&(d.toString().replace(ka,"").replace(la,function(b,d){c.push(d)}),c=(1===d.length?["require"]:["require","exports","module"]).concat(c)));if(M){if(!(e=J))N&&"interactive"===N.readyState||T(document.getElementsByTagName("script"),function(b){if("interactive"===b.readyState)return N=b}),e=N;e&&(b||
+(b=e.getAttribute("data-requiremodule")),g=F[e.getAttribute("data-requirecontext")])}(g?g.defQueue:R).push([b,c,d])};define.amd={jQuery:!0};g.exec=function(b){return eval(b)};g(q)}})(this);
diff --git a/gulpfile.js b/gulpfile.js
new file mode 100644
index 0000000..b52b983
--- /dev/null
+++ b/gulpfile.js
@@ -0,0 +1,87 @@
+var fs = require('fs'),
+ gulp = require('gulp'),
+ gutil = require('gulp-util'),
+ webpack = require('webpack'),
+ uglify = require('uglify-js');
+
+var ENTRY = './src/js/JSONEditor.js',
+ HEADER = './src/js/header.js',
+ FILE = 'jsoneditor.js',
+ FILE_MIN = 'jsoneditor.min.js',
+ FILE_MAP = 'jsoneditor.map',
+ DIST = './',
+ JSONEDITOR_JS = DIST + FILE,
+ JSONEDITOR_MIN_JS = DIST + FILE_MIN,
+ JSONEDITOR_MAP_JS = DIST + FILE_MAP;
+
+// generate banner with today's date and correct version
+function createBanner() {
+ var today = gutil.date(new Date(), 'yyyy-mm-dd'); // today, formatted as yyyy-mm-dd
+ var version = require('./package.json').version; // math.js version
+
+ return String(fs.readFileSync(HEADER))
+ .replace('@@date', today)
+ .replace('@@version', version);
+}
+
+var bannerPlugin = new webpack.BannerPlugin(createBanner(), {
+ entryOnly: true,
+ raw: true
+});
+
+var webpackConfig = {
+ entry: ENTRY,
+ output: {
+ library: 'JSONEditor',
+ libraryTarget: 'umd',
+ path: DIST,
+ filename: FILE
+ },
+ plugins: [ bannerPlugin ],
+ cache: true
+};
+
+var uglifyConfig = {
+ outSourceMap: FILE_MAP,
+ output: {
+ comments: /@license/
+ }
+};
+
+// create a single instance of the compiler to allow caching
+var compiler = webpack(webpackConfig);
+
+gulp.task('bundle', function (cb) {
+ // update the banner contents (has a date in it which should stay up to date)
+ bannerPlugin.banner = createBanner();
+
+ compiler.run(function (err, stats) {
+ if (err) {
+ gutil.log(err);
+ }
+
+ gutil.log('bundled ' + JSONEDITOR_JS);
+
+ // TODO: bundle css
+
+ // TODO: bundle and minify assets
+
+ cb();
+ });
+});
+
+gulp.task('minify', ['bundle'], function () {
+ var result = uglify.minify([JSONEDITOR_JS], uglifyConfig);
+
+ fs.writeFileSync(JSONEDITOR_MIN_JS, result.code + '\n//# sourceMappingURL=' + FILE_MAP);
+ fs.writeFileSync(JSONEDITOR_MAP_JS, result.map);
+
+ gutil.log('Minified ' + JSONEDITOR_MIN_JS);
+ gutil.log('Mapped ' + JSONEDITOR_MAP_JS);
+
+ // TODO: minify css
+
+});
+
+// The default task (called when you run `gulp`)
+gulp.task('default', ['bundle', 'minify']);
diff --git a/jsoneditor.js b/jsoneditor.js
index 89dd6e3..c167f5e 100644
--- a/jsoneditor.js
+++ b/jsoneditor.js
@@ -20,6019 +20,6128 @@
* License for the specific language governing permissions and limitations under
* the License.
*
- * Copyright (c) 2011-2013 Jos de Jong, http://jsoneditoronline.org
+ * Copyright (c) 2011-2014 Jos de Jong, http://jsoneditoronline.org
*
* @author Jos de Jong,
* @version 3.0.0-SNAPSHOT
* @date 2014-05-29
*/
-(function () {
-
-/**
- * @constructor JSONEditor
- * @param {Element} container Container element
- * @param {Object} [options] Object with options. available options:
- * {String} mode Editor mode. Available values:
- * 'tree' (default), 'view',
- * 'form', 'text', and 'code'.
- * {function} change Callback method, triggered
- * on change of contents
- * {Boolean} search Enable search box.
- * True by default
- * Only applicable for modes
- * 'tree', 'view', and 'form'
- * {Boolean} history Enable history (undo/redo).
- * True by default
- * Only applicable for modes
- * 'tree', 'view', and 'form'
- * {String} name Field name for the root node.
- * Only applicable for modes
- * 'tree', 'view', and 'form'
- * {Number} indentation Number of indentation
- * spaces. 4 by default.
- * Only applicable for
- * modes 'text' and 'code'
- * @param {Object | undefined} json JSON object
- */
-function JSONEditor (container, options, json) {
- if (!(this instanceof JSONEditor)) {
- throw new Error('JSONEditor constructor called without "new".');
- }
-
- // check for unsupported browser (IE8 and older)
- var ieVersion = util.getInternetExplorerVersion();
- if (ieVersion != -1 && ieVersion < 9) {
- throw new Error('Unsupported browser, IE9 or newer required. ' +
- 'Please install the newest version of your browser.');
- }
-
- if (arguments.length) {
- this._create(container, options, json);
- }
-}
-
-/**
- * Configuration for all registered modes. Example:
- * {
- * tree: {
- * editor: TreeEditor,
- * data: 'json'
- * },
- * text: {
- * editor: TextEditor,
- * data: 'text'
- * }
- * }
- *
- * @type { Object. }
- */
-JSONEditor.modes = {};
-
-/**
- * Create the JSONEditor
- * @param {Element} container Container element
- * @param {Object} [options] See description in constructor
- * @param {Object | undefined} json JSON object
- * @private
- */
-JSONEditor.prototype._create = function (container, options, json) {
- this.container = container;
- this.options = options || {};
- this.json = json || {};
-
- var mode = this.options.mode || 'tree';
- this.setMode(mode);
-};
-
-/**
- * Detach the editor from the DOM
- * @private
- */
-JSONEditor.prototype._delete = function () {};
-
-/**
- * Set JSON object in editor
- * @param {Object | undefined} json JSON data
- */
-JSONEditor.prototype.set = function (json) {
- this.json = json;
-};
-
-/**
- * Get JSON from the editor
- * @returns {Object} json
- */
-JSONEditor.prototype.get = function () {
- return this.json;
-};
-
-/**
- * Set string containing JSON for the editor
- * @param {String | undefined} jsonText
- */
-JSONEditor.prototype.setText = function (jsonText) {
- this.json = util.parse(jsonText);
-};
-
-/**
- * Get stringified JSON contents from the editor
- * @returns {String} jsonText
- */
-JSONEditor.prototype.getText = function () {
- return JSON.stringify(this.json);
-};
-
-/**
- * Set a field name for the root node.
- * @param {String | undefined} name
- */
-JSONEditor.prototype.setName = function (name) {
- if (!this.options) {
- this.options = {};
- }
- this.options.name = name;
-};
-
-/**
- * Get the field name for the root node.
- * @return {String | undefined} name
- */
-JSONEditor.prototype.getName = function () {
- return this.options && this.options.name;
-};
-
-/**
- * Change the mode of the editor.
- * JSONEditor will be extended with all methods needed for the chosen mode.
- * @param {String} mode Available modes: 'tree' (default), 'view', 'form',
- * 'text', and 'code'.
- */
-JSONEditor.prototype.setMode = function (mode) {
- var container = this.container,
- options = util.extend({}, this.options),
- data,
- name;
-
- options.mode = mode;
- var config = JSONEditor.modes[mode];
- if (config) {
- try {
- if (config.data == 'text') {
- // text
- name = this.getName();
- data = this.getText();
-
- this._delete();
- util.clear(this);
- util.extend(this, config.editor.prototype);
- this._create(container, options);
-
- this.setName(name);
- this.setText(data);
- }
- else {
- // json
- name = this.getName();
- data = this.get();
-
- this._delete();
- util.clear(this);
- util.extend(this, config.editor.prototype);
- this._create(container, options);
-
- this.setName(name);
- this.set(data);
- }
-
- if (typeof config.load === 'function') {
- try {
- config.load.call(this);
- }
- catch (err) {}
- }
- }
- catch (err) {
- this._onError(err);
- }
- }
- else {
- throw new Error('Unknown mode "' + options.mode + '"');
- }
-};
-
-/**
- * Throw an error. If an error callback is configured in options.error, this
- * callback will be invoked. Else, a regular error is thrown.
- * @param {Error} err
- * @private
- */
-JSONEditor.prototype._onError = function(err) {
- // TODO: onError is deprecated since version 2.2.0. cleanup some day
- if (typeof this.onError === 'function') {
- util.log('WARNING: JSONEditor.onError is deprecated. ' +
- 'Use options.error instead.');
- this.onError(err);
- }
-
- if (this.options && typeof this.options.error === 'function') {
- this.options.error(err);
- }
- else {
- throw err;
- }
-};
-
-/**
- * @constructor TreeEditor
- * @param {Element} container Container element
- * @param {Object} [options] Object with options. available options:
- * {String} mode Editor mode. Available values:
- * 'tree' (default), 'view',
- * and 'form'.
- * {Boolean} search Enable search box.
- * True by default
- * {Boolean} history Enable history (undo/redo).
- * True by default
- * {function} change Callback method, triggered
- * on change of contents
- * {String} name Field name for the root node.
- * @param {Object | undefined} json JSON object
- */
-function TreeEditor(container, options, json) {
- if (!(this instanceof TreeEditor)) {
- throw new Error('TreeEditor constructor called without "new".');
- }
-
- this._create(container, options, json);
-}
-
-/**
- * Create the TreeEditor
- * @param {Element} container Container element
- * @param {Object} [options] See description in constructor
- * @param {Object | undefined} json JSON object
- * @private
- */
-TreeEditor.prototype._create = function (container, options, json) {
- if (!container) {
- throw new Error('No container element provided.');
- }
- this.container = container;
- this.dom = {};
- this.highlighter = new Highlighter();
- this.selection = undefined; // will hold the last input selection
-
- this._setOptions(options);
-
- if (this.options.history && !this.mode.view) {
- this.history = new History(this);
- }
-
- this._createFrame();
- this._createTable();
-
- this.set(json || {});
-};
-
-/**
- * Detach the editor from the DOM
- * @private
- */
-TreeEditor.prototype._delete = function () {
- if (this.frame && this.container && this.frame.parentNode == this.container) {
- this.container.removeChild(this.frame);
- }
-};
-
-/**
- * Initialize and set default options
- * @param {Object} [options] See description in constructor
- * @private
- */
-TreeEditor.prototype._setOptions = function (options) {
- this.options = {
- search: true,
- history: true,
- mode: 'tree',
- name: undefined // field name of root node
- };
-
- // copy all options
- if (options) {
- for (var prop in options) {
- if (options.hasOwnProperty(prop)) {
- this.options[prop] = options[prop];
- }
- }
-
- // check for deprecated options
- if (options['enableSearch']) {
- // deprecated since version 1.6.0, 2012-11-03
- this.options.search = options['enableSearch'];
- util.log('WARNING: Option "enableSearch" is deprecated. Use "search" instead.');
- }
- if (options['enableHistory']) {
- // deprecated since version 1.6.0, 2012-11-03
- this.options.history = options['enableHistory'];
- util.log('WARNING: Option "enableHistory" is deprecated. Use "history" instead.');
- }
- if (options['mode'] == 'editor') {
- // deprecated since version 2.2.0, 2013-04-30
- this.options.mode = 'tree';
- util.log('WARNING: Mode "editor" is deprecated. Use "tree" instead.');
- }
- if (options['mode'] == 'viewer') {
- // deprecated since version 2.2.0, 2013-04-30
- this.options.mode = 'view';
- util.log('WARNING: Mode "viewer" is deprecated. Use "view" instead.');
- }
- }
-
- // interpret the mode options
- this.mode = {
- edit: (this.options.mode != 'view' && this.options.mode != 'form'),
- view: (this.options.mode == 'view'),
- form: (this.options.mode == 'form')
- };
-};
-
-// node currently being edited
-TreeEditor.focusNode = undefined;
-
-/**
- * Set JSON object in editor
- * @param {Object | undefined} json JSON data
- * @param {String} [name] Optional field name for the root node.
- * Can also be set using setName(name).
- */
-TreeEditor.prototype.set = function (json, name) {
- // adjust field name for root node
- if (name) {
- // TODO: deprecated since version 2.2.0. Cleanup some day.
- util.log('Warning: second parameter "name" is deprecated. ' +
- 'Use setName(name) instead.');
- this.options.name = name;
- }
-
- // verify if json is valid JSON, ignore when a function
- if (json instanceof Function || (json === undefined)) {
- this.clear();
- }
- else {
- this.content.removeChild(this.table); // Take the table offline
-
- // replace the root node
- var params = {
- 'field': this.options.name,
- 'value': json
- };
- var node = new Node(this, params);
- this._setRoot(node);
-
- // expand
- var recurse = false;
- this.node.expand(recurse);
-
- this.content.appendChild(this.table); // Put the table online again
- }
-
- // TODO: maintain history, store last state and previous document
- if (this.history) {
- this.history.clear();
- }
-};
-
-/**
- * Get JSON object from editor
- * @return {Object | undefined} json
- */
-TreeEditor.prototype.get = function () {
- // remove focus from currently edited node
- if (TreeEditor.focusNode) {
- TreeEditor.focusNode.blur();
- }
-
- if (this.node) {
- return this.node.getValue();
- }
- else {
- return undefined;
- }
-};
-
-/**
- * Get the text contents of the TreeEditor
- * @return {String} jsonText
- */
-TreeEditor.prototype.getText = function() {
- return JSON.stringify(this.get());
-};
-
-/**
- * Set the text contents of the TreeEditor
- * @param {String} jsonText
- */
-TreeEditor.prototype.setText = function(jsonText) {
- this.set(util.parse(jsonText));
-};
-
-/**
- * Set a field name for the root node.
- * @param {String | undefined} name
- */
-TreeEditor.prototype.setName = function (name) {
- this.options.name = name;
- if (this.node) {
- this.node.updateField(this.options.name);
- }
-};
-
-/**
- * Get the field name for the root node.
- * @return {String | undefined} name
- */
-TreeEditor.prototype.getName = function () {
- return this.options.name;
-};
-
-/**
- * Remove the root node from the editor
- */
-TreeEditor.prototype.clear = function () {
- if (this.node) {
- this.node.collapse();
- this.tbody.removeChild(this.node.getDom());
- delete this.node;
- }
-};
-
-/**
- * Set the root node for the json editor
- * @param {Node} node
- * @private
- */
-TreeEditor.prototype._setRoot = function (node) {
- this.clear();
-
- this.node = node;
-
- // append to the dom
- this.tbody.appendChild(node.getDom());
-};
-
-/**
- * Search text in all nodes
- * The nodes will be expanded when the text is found one of its childs,
- * else it will be collapsed. Searches are case insensitive.
- * @param {String} text
- * @return {Object[]} results Array with nodes containing the search results
- * The result objects contains fields:
- * - {Node} node,
- * - {String} elem the dom element name where
- * the result is found ('field' or
- * 'value')
- */
-TreeEditor.prototype.search = function (text) {
- var results;
- if (this.node) {
- this.content.removeChild(this.table); // Take the table offline
- results = this.node.search(text);
- this.content.appendChild(this.table); // Put the table online again
- }
- else {
- results = [];
- }
-
- return results;
-};
-
-/**
- * Expand all nodes
- */
-TreeEditor.prototype.expandAll = function () {
- if (this.node) {
- this.content.removeChild(this.table); // Take the table offline
- this.node.expand();
- this.content.appendChild(this.table); // Put the table online again
- }
-};
-
-/**
- * Collapse all nodes
- */
-TreeEditor.prototype.collapseAll = function () {
- if (this.node) {
- this.content.removeChild(this.table); // Take the table offline
- this.node.collapse();
- this.content.appendChild(this.table); // Put the table online again
- }
-};
-
-/**
- * The method onChange is called whenever a field or value is changed, created,
- * deleted, duplicated, etc.
- * @param {String} action Change action. Available values: "editField",
- * "editValue", "changeType", "appendNode",
- * "removeNode", "duplicateNode", "moveNode", "expand",
- * "collapse".
- * @param {Object} params Object containing parameters describing the change.
- * The parameters in params depend on the action (for
- * example for "editValue" the Node, old value, and new
- * value are provided). params contains all information
- * needed to undo or redo the action.
- * @private
- */
-TreeEditor.prototype._onAction = function (action, params) {
- // add an action to the history
- if (this.history) {
- this.history.add(action, params);
- }
-
- // trigger the onChange callback
- if (this.options.change) {
- try {
- this.options.change();
- }
- catch (err) {
- util.log('Error in change callback: ', err);
- }
- }
-};
-
-/**
- * Start autoscrolling when given mouse position is above the top of the
- * editor contents, or below the bottom.
- * @param {Number} mouseY Absolute mouse position in pixels
- */
-TreeEditor.prototype.startAutoScroll = function (mouseY) {
- var me = this;
- var content = this.content;
- var top = util.getAbsoluteTop(content);
- var height = content.clientHeight;
- var bottom = top + height;
- var margin = 24;
- var interval = 50; // ms
-
- if ((mouseY < top + margin) && content.scrollTop > 0) {
- this.autoScrollStep = ((top + margin) - mouseY) / 3;
- }
- else if (mouseY > bottom - margin &&
- height + content.scrollTop < content.scrollHeight) {
- this.autoScrollStep = ((bottom - margin) - mouseY) / 3;
- }
- else {
- this.autoScrollStep = undefined;
- }
-
- if (this.autoScrollStep) {
- if (!this.autoScrollTimer) {
- this.autoScrollTimer = setInterval(function () {
- if (me.autoScrollStep) {
- content.scrollTop -= me.autoScrollStep;
- }
- else {
- me.stopAutoScroll();
- }
- }, interval);
- }
- }
- else {
- this.stopAutoScroll();
- }
-};
-
-/**
- * Stop auto scrolling. Only applicable when scrolling
- */
-TreeEditor.prototype.stopAutoScroll = function () {
- if (this.autoScrollTimer) {
- clearTimeout(this.autoScrollTimer);
- delete this.autoScrollTimer;
- }
- if (this.autoScrollStep) {
- delete this.autoScrollStep;
- }
-};
-
-
-/**
- * Set the focus to an element in the TreeEditor, set text selection, and
- * set scroll position.
- * @param {Object} selection An object containing fields:
- * {Element | undefined} dom The dom element
- * which has focus
- * {Range | TextRange} range A text selection
- * {Number} scrollTop Scroll position
- */
-TreeEditor.prototype.setSelection = function (selection) {
- if (!selection) {
- return;
- }
-
- if ('scrollTop' in selection && this.content) {
- // TODO: animated scroll
- this.content.scrollTop = selection.scrollTop;
- }
- if (selection.range) {
- util.setSelectionOffset(selection.range);
- }
- if (selection.dom) {
- selection.dom.focus();
- }
-};
-
-/**
- * Get the current focus
- * @return {Object} selection An object containing fields:
- * {Element | undefined} dom The dom element
- * which has focus
- * {Range | TextRange} range A text selection
- * {Number} scrollTop Scroll position
- */
-TreeEditor.prototype.getSelection = function () {
- return {
- dom: TreeEditor.domFocus,
- scrollTop: this.content ? this.content.scrollTop : 0,
- range: util.getSelectionOffset()
- };
-};
-
-/**
- * Adjust the scroll position such that given top position is shown at 1/4
- * of the window height.
- * @param {Number} top
- * @param {function(boolean)} [callback] Callback, executed when animation is
- * finished. The callback returns true
- * when animation is finished, or false
- * when not.
- */
-TreeEditor.prototype.scrollTo = function (top, callback) {
- var content = this.content;
- if (content) {
- var editor = this;
- // cancel any running animation
- if (editor.animateTimeout) {
- clearTimeout(editor.animateTimeout);
- delete editor.animateTimeout;
- }
- if (editor.animateCallback) {
- editor.animateCallback(false);
- delete editor.animateCallback;
- }
-
- // calculate final scroll position
- var height = content.clientHeight;
- var bottom = content.scrollHeight - height;
- var finalScrollTop = Math.min(Math.max(top - height / 4, 0), bottom);
-
- // animate towards the new scroll position
- var animate = function () {
- var scrollTop = content.scrollTop;
- var diff = (finalScrollTop - scrollTop);
- if (Math.abs(diff) > 3) {
- content.scrollTop += diff / 3;
- editor.animateCallback = callback;
- editor.animateTimeout = setTimeout(animate, 50);
- }
- else {
- // finished
- if (callback) {
- callback(true);
- }
- content.scrollTop = finalScrollTop;
- delete editor.animateTimeout;
- delete editor.animateCallback;
- }
- };
- animate();
- }
- else {
- if (callback) {
- callback(false);
- }
- }
-};
-
-/**
- * Create main frame
- * @private
- */
-TreeEditor.prototype._createFrame = function () {
- // create the frame
- this.frame = document.createElement('div');
- this.frame.className = 'jsoneditor';
- this.container.appendChild(this.frame);
-
- // create one global event listener to handle all events from all nodes
- var editor = this;
- var onEvent = function (event) {
- editor._onEvent(event);
- };
- this.frame.onclick = function (event) {
- var target = event.target;// || event.srcElement;
-
- onEvent(event);
-
- // prevent default submit action of buttons when TreeEditor is located
- // inside a form
- if (target.nodeName == 'BUTTON') {
- event.preventDefault();
- }
- };
- this.frame.oninput = onEvent;
- this.frame.onchange = onEvent;
- this.frame.onkeydown = onEvent;
- this.frame.onkeyup = onEvent;
- this.frame.oncut = onEvent;
- this.frame.onpaste = onEvent;
- this.frame.onmousedown = onEvent;
- this.frame.onmouseup = onEvent;
- this.frame.onmouseover = onEvent;
- this.frame.onmouseout = onEvent;
- // Note: focus and blur events do not propagate, therefore they defined
- // using an eventListener with useCapture=true
- // see http://www.quirksmode.org/blog/archives/2008/04/delegating_the.html
- util.addEventListener(this.frame, 'focus', onEvent, true);
- util.addEventListener(this.frame, 'blur', onEvent, true);
- this.frame.onfocusin = onEvent; // for IE
- this.frame.onfocusout = onEvent; // for IE
-
- // create menu
- this.menu = document.createElement('div');
- this.menu.className = 'menu';
- this.frame.appendChild(this.menu);
-
- // create expand all button
- var expandAll = document.createElement('button');
- expandAll.className = 'expand-all';
- expandAll.title = 'Expand all fields';
- expandAll.onclick = function () {
- editor.expandAll();
- };
- this.menu.appendChild(expandAll);
-
- // create expand all button
- var collapseAll = document.createElement('button');
- collapseAll.title = 'Collapse all fields';
- collapseAll.className = 'collapse-all';
- collapseAll.onclick = function () {
- editor.collapseAll();
- };
- this.menu.appendChild(collapseAll);
-
- // create undo/redo buttons
- if (this.history) {
- // create undo button
- var undo = document.createElement('button');
- undo.className = 'undo separator';
- undo.title = 'Undo last action (Ctrl+Z)';
- undo.onclick = function () {
- editor._onUndo();
- };
- this.menu.appendChild(undo);
- this.dom.undo = undo;
-
- // create redo button
- var redo = document.createElement('button');
- redo.className = 'redo';
- redo.title = 'Redo (Ctrl+Shift+Z)';
- redo.onclick = function () {
- editor._onRedo();
- };
- this.menu.appendChild(redo);
- this.dom.redo = redo;
-
- // register handler for onchange of history
- this.history.onChange = function () {
- undo.disabled = !editor.history.canUndo();
- redo.disabled = !editor.history.canRedo();
- };
- this.history.onChange();
- }
-
- // create mode box
- if (this.options && this.options.modes && this.options.modes.length) {
- var modeBox = createModeBox(this, this.options.modes, this.options.mode);
- this.menu.appendChild(modeBox);
- this.dom.modeBox = modeBox;
- }
-
- // create search box
- if (this.options.search) {
- this.searchBox = new SearchBox(this, this.menu);
- }
-};
-
-/**
- * Perform an undo action
- * @private
- */
-TreeEditor.prototype._onUndo = function () {
- if (this.history) {
- // undo last action
- this.history.undo();
-
- // trigger change callback
- if (this.options.change) {
- this.options.change();
- }
- }
-};
-
-/**
- * Perform a redo action
- * @private
- */
-TreeEditor.prototype._onRedo = function () {
- if (this.history) {
- // redo last action
- this.history.redo();
-
- // trigger change callback
- if (this.options.change) {
- this.options.change();
- }
- }
-};
-
-/**
- * Event handler
- * @param event
- * @private
- */
-TreeEditor.prototype._onEvent = function (event) {
- var target = event.target;
-
- if (event.type == 'keydown') {
- this._onKeyDown(event);
- }
-
- if (event.type == 'focus') {
- TreeEditor.domFocus = target;
- }
-
- var node = Node.getNodeFromTarget(target);
- if (node) {
- node.onEvent(event);
- }
-};
-
-/**
- * Event handler for keydown. Handles shortcut keys
- * @param {Event} event
- * @private
- */
-TreeEditor.prototype._onKeyDown = function (event) {
- var keynum = event.which || event.keyCode;
- var ctrlKey = event.ctrlKey;
- var shiftKey = event.shiftKey;
- var handled = false;
-
- if (keynum == 9) { // Tab or Shift+Tab
- setTimeout(function () {
- // select all text when moving focus to an editable div
- util.selectContentEditable(TreeEditor.domFocus);
- }, 0);
- }
-
- if (this.searchBox) {
- if (ctrlKey && keynum == 70) { // Ctrl+F
- this.searchBox.dom.search.focus();
- this.searchBox.dom.search.select();
- handled = true;
- }
- else if (keynum == 114 || (ctrlKey && keynum == 71)) { // F3 or Ctrl+G
- var focus = true;
- if (!shiftKey) {
- // select next search result (F3 or Ctrl+G)
- this.searchBox.next(focus);
- }
- else {
- // select previous search result (Shift+F3 or Ctrl+Shift+G)
- this.searchBox.previous(focus);
- }
-
- handled = true;
- }
- }
-
- if (this.history) {
- if (ctrlKey && !shiftKey && keynum == 90) { // Ctrl+Z
- // undo
- this._onUndo();
- handled = true;
- }
- else if (ctrlKey && shiftKey && keynum == 90) { // Ctrl+Shift+Z
- // redo
- this._onRedo();
- handled = true;
- }
- }
-
- if (handled) {
- event.preventDefault();
- event.stopPropagation();
- }
-};
-
-/**
- * Create main table
- * @private
- */
-TreeEditor.prototype._createTable = function () {
- var contentOuter = document.createElement('div');
- contentOuter.className = 'outer';
- this.contentOuter = contentOuter;
-
- this.content = document.createElement('div');
- this.content.className = 'tree';
- contentOuter.appendChild(this.content);
-
- this.table = document.createElement('table');
- this.table.className = 'tree';
- this.content.appendChild(this.table);
-
- // create colgroup where the first two columns don't have a fixed
- // width, and the edit columns do have a fixed width
- var col;
- this.colgroupContent = document.createElement('colgroup');
- if (this.mode.edit) {
- col = document.createElement('col');
- col.width = "24px";
- this.colgroupContent.appendChild(col);
- }
- col = document.createElement('col');
- col.width = "24px";
- this.colgroupContent.appendChild(col);
- col = document.createElement('col');
- this.colgroupContent.appendChild(col);
- this.table.appendChild(this.colgroupContent);
-
- this.tbody = document.createElement('tbody');
- this.table.appendChild(this.tbody);
-
- this.frame.appendChild(contentOuter);
-};
-
-// register modes at the JSONEditor
-JSONEditor.modes.tree = {
- editor: TreeEditor,
- data: 'json'
-};
-JSONEditor.modes.view = {
- editor: TreeEditor,
- data: 'json'
-};
-JSONEditor.modes.form = {
- editor: TreeEditor,
- data: 'json'
-};
-// Deprecated modes (deprecated since version 2.2.0)
-JSONEditor.modes.editor = {
- editor: TreeEditor,
- data: 'json'
-};
-JSONEditor.modes.viewer = {
- editor: TreeEditor,
- data: 'json'
-};
-
-/**
- * Create a TextEditor and attach it to given container
- * @constructor TextEditor
- * @param {Element} container
- * @param {Object} [options] Object with options. available options:
- * {String} mode Available values:
- * "text" (default)
- * or "code".
- * {Number} indentation Number of indentation
- * spaces. 2 by default.
- * {function} change Callback method
- * triggered on change
- * @param {JSON | String} [json] initial contents of the formatter
- */
-function TextEditor(container, options, json) {
- if (!(this instanceof TextEditor)) {
- throw new Error('TextEditor constructor called without "new".');
- }
-
- this._create(container, options, json);
-}
-
-/**
- * Create a TextEditor and attach it to given container
- * @constructor TextEditor
- * @param {Element} container
- * @param {Object} [options] See description in constructor
- * @param {JSON | String} [json] initial contents of the formatter
- * @private
- */
-TextEditor.prototype._create = function (container, options, json) {
- // read options
- options = options || {};
- this.options = options;
- if (options.indentation) {
- this.indentation = Number(options.indentation);
- }
- else {
- this.indentation = 2; // number of spaces
- }
- this.mode = (options.mode == 'code') ? 'code' : 'text';
- if (this.mode == 'code') {
- // verify whether Ace editor is available and supported
- if (typeof ace === 'undefined') {
- this.mode = 'text';
- util.log('WARNING: Cannot load code editor, Ace library not loaded. ' +
- 'Falling back to plain text editor');
- }
- }
-
- var me = this;
- this.container = container;
- this.dom = {};
- this.editor = undefined; // ace code editor
- this.textarea = undefined; // plain text editor (fallback when Ace is not available)
-
- this.width = container.clientWidth;
- this.height = container.clientHeight;
-
- this.frame = document.createElement('div');
- this.frame.className = 'jsoneditor';
- this.frame.onclick = function (event) {
- // prevent default submit action when TextEditor is located inside a form
- event.preventDefault();
- };
-
- // create menu
- this.menu = document.createElement('div');
- this.menu.className = 'menu';
- this.frame.appendChild(this.menu);
-
- // create format button
- var buttonFormat = document.createElement('button');
- buttonFormat.className = 'format';
- buttonFormat.title = 'Format JSON data, with proper indentation and line feeds';
- this.menu.appendChild(buttonFormat);
- buttonFormat.onclick = function () {
- try {
- me.format();
- }
- catch (err) {
- me._onError(err);
- }
- };
-
- // create compact button
- var buttonCompact = document.createElement('button');
- buttonCompact.className = 'compact';
- buttonCompact.title = 'Compact JSON data, remove all whitespaces';
- this.menu.appendChild(buttonCompact);
- buttonCompact.onclick = function () {
- try {
- me.compact();
- }
- catch (err) {
- me._onError(err);
- }
- };
-
- // create mode box
- if (this.options && this.options.modes && this.options.modes.length) {
- var modeBox = createModeBox(this, this.options.modes, this.options.mode);
- this.menu.appendChild(modeBox);
- this.dom.modeBox = modeBox;
- }
-
- this.content = document.createElement('div');
- this.content.className = 'outer';
- this.frame.appendChild(this.content);
-
- this.container.appendChild(this.frame);
-
- if (this.mode == 'code') {
- this.editorDom = document.createElement('div');
- this.editorDom.style.height = '100%'; // TODO: move to css
- this.editorDom.style.width = '100%'; // TODO: move to css
- this.content.appendChild(this.editorDom);
-
- var editor = ace.edit(this.editorDom);
- editor.setTheme('ace/theme/jsoneditor');
- editor.setShowPrintMargin(false);
- editor.setFontSize(13);
- editor.getSession().setMode('ace/mode/json');
- editor.getSession().setTabSize(2);
- editor.getSession().setUseSoftTabs(true);
- editor.getSession().setUseWrapMode(true);
- this.editor = editor;
-
- var poweredBy = document.createElement('a');
- poweredBy.appendChild(document.createTextNode('powered by ace'));
- poweredBy.href = 'http://ace.ajax.org';
- poweredBy.target = '_blank';
- poweredBy.className = 'poweredBy';
- poweredBy.onclick = function () {
- // TODO: this anchor falls below the margin of the content,
- // therefore the normal a.href does not work. We use a click event
- // for now, but this should be fixed.
- window.open(poweredBy.href, poweredBy.target);
- };
- this.menu.appendChild(poweredBy);
-
- if (options.change) {
- // register onchange event
- editor.on('change', function () {
- options.change();
- });
- }
- }
- else {
- // load a plain text textarea
- var textarea = document.createElement('textarea');
- textarea.className = 'text';
- textarea.spellcheck = false;
- this.content.appendChild(textarea);
- this.textarea = textarea;
-
- if (options.change) {
- // register onchange event
- if (this.textarea.oninput === null) {
- this.textarea.oninput = function () {
- options.change();
- }
- }
- else {
- // oninput is undefined. For IE8-
- this.textarea.onchange = function () {
- options.change();
- }
- }
- }
- }
-
- // load initial json object or string
- if (typeof(json) == 'string') {
- this.setText(json);
- }
- else {
- this.set(json);
- }
-};
-
-/**
- * Detach the editor from the DOM
- * @private
- */
-TextEditor.prototype._delete = function () {
- if (this.frame && this.container && this.frame.parentNode == this.container) {
- this.container.removeChild(this.frame);
- }
-};
-
-/**
- * Throw an error. If an error callback is configured in options.error, this
- * callback will be invoked. Else, a regular error is thrown.
- * @param {Error} err
- * @private
- */
-TextEditor.prototype._onError = function(err) {
- // TODO: onError is deprecated since version 2.2.0. cleanup some day
- if (typeof this.onError === 'function') {
- util.log('WARNING: JSONEditor.onError is deprecated. ' +
- 'Use options.error instead.');
- this.onError(err);
- }
-
- if (this.options && typeof this.options.error === 'function') {
- this.options.error(err);
- }
- else {
- throw err;
- }
-};
-
-/**
- * Compact the code in the formatter
- */
-TextEditor.prototype.compact = function () {
- var json = util.parse(this.getText());
- this.setText(JSON.stringify(json));
-};
-
-/**
- * Format the code in the formatter
- */
-TextEditor.prototype.format = function () {
- var json = util.parse(this.getText());
- this.setText(JSON.stringify(json, null, this.indentation));
-};
-
-/**
- * Set focus to the formatter
- */
-TextEditor.prototype.focus = function () {
- if (this.textarea) {
- this.textarea.focus();
- }
- if (this.editor) {
- this.editor.focus();
- }
-};
-
-/**
- * Resize the formatter
- */
-TextEditor.prototype.resize = function () {
- if (this.editor) {
- var force = false;
- this.editor.resize(force);
- }
-};
-
-/**
- * Set json data in the formatter
- * @param {Object} json
- */
-TextEditor.prototype.set = function(json) {
- this.setText(JSON.stringify(json, null, this.indentation));
-};
-
-/**
- * Get json data from the formatter
- * @return {Object} json
- */
-TextEditor.prototype.get = function() {
- return util.parse(this.getText());
-};
-
-/**
- * Get the text contents of the TextEditor
- * @return {String} jsonText
- */
-TextEditor.prototype.getText = function() {
- if (this.textarea) {
- return this.textarea.value;
- }
- if (this.editor) {
- return this.editor.getValue();
- }
- return '';
-};
-
-/**
- * Set the text contents of the TextEditor
- * @param {String} jsonText
- */
-TextEditor.prototype.setText = function(jsonText) {
- if (this.textarea) {
- this.textarea.value = jsonText;
- }
- if (this.editor) {
- this.editor.setValue(jsonText, -1);
- }
-};
-
-// register modes at the JSONEditor
-JSONEditor.modes.text = {
- editor: TextEditor,
- data: 'text',
- load: TextEditor.prototype.format
-};
-JSONEditor.modes.code = {
- editor: TextEditor,
- data: 'text',
- load: TextEditor.prototype.format
-};
-
-/**
- * @constructor Node
- * Create a new Node
- * @param {TreeEditor} editor
- * @param {Object} [params] Can contain parameters:
- * {string} field
- * {boolean} fieldEditable
- * {*} value
- * {String} type Can have values 'auto', 'array',
- * 'object', or 'string'.
- */
-function Node (editor, params) {
- /** @type {TreeEditor} */
- this.editor = editor;
- this.dom = {};
- this.expanded = false;
-
- if(params && (params instanceof Object)) {
- this.setField(params.field, params.fieldEditable);
- this.setValue(params.value, params.type);
- }
- else {
- this.setField('');
- this.setValue(null);
- }
-};
-
-/**
- * Set parent node
- * @param {Node} parent
- */
-Node.prototype.setParent = function(parent) {
- this.parent = parent;
-};
-
-/**
- * Set field
- * @param {String} field
- * @param {boolean} [fieldEditable]
- */
-Node.prototype.setField = function(field, fieldEditable) {
- this.field = field;
- this.fieldEditable = (fieldEditable == true);
-};
-
-/**
- * Get field
- * @return {String}
- */
-Node.prototype.getField = function() {
- if (this.field === undefined) {
- this._getDomField();
- }
-
- return this.field;
-};
-
-/**
- * Set value. Value is a JSON structure or an element String, Boolean, etc.
- * @param {*} value
- * @param {String} [type] Specify the type of the value. Can be 'auto',
- * 'array', 'object', or 'string'
- */
-Node.prototype.setValue = function(value, type) {
- var childValue, child;
-
- // first clear all current childs (if any)
- var childs = this.childs;
- if (childs) {
- while (childs.length) {
- this.removeChild(childs[0]);
- }
- }
-
- // TODO: remove the DOM of this Node
-
- this.type = this._getType(value);
-
- // check if type corresponds with the provided type
- if (type && type != this.type) {
- if (type == 'string' && this.type == 'auto') {
- this.type = type;
- }
- else {
- throw new Error('Type mismatch: ' +
- 'cannot cast value of type "' + this.type +
- ' to the specified type "' + type + '"');
- }
- }
-
- if (this.type == 'array') {
- // array
- this.childs = [];
- for (var i = 0, iMax = value.length; i < iMax; i++) {
- childValue = value[i];
- if (childValue !== undefined && !(childValue instanceof Function)) {
- // ignore undefined and functions
- child = new Node(this.editor, {
- 'value': childValue
- });
- this.appendChild(child);
- }
- }
- this.value = '';
- }
- else if (this.type == 'object') {
- // object
- this.childs = [];
- for (var childField in value) {
- if (value.hasOwnProperty(childField)) {
- childValue = value[childField];
- if (childValue !== undefined && !(childValue instanceof Function)) {
- // ignore undefined and functions
- child = new Node(this.editor, {
- 'field': childField,
- 'value': childValue
- });
- this.appendChild(child);
- }
- }
- }
- this.value = '';
- }
- else {
- // value
- this.childs = undefined;
- this.value = value;
- /* TODO
- if (typeof(value) == 'string') {
- var escValue = JSON.stringify(value);
- this.value = escValue.substring(1, escValue.length - 1);
- util.log('check', value, this.value);
- }
- else {
- this.value = value;
- }
- */
- }
-};
-
-/**
- * Get value. Value is a JSON structure
- * @return {*} value
- */
-Node.prototype.getValue = function() {
- //var childs, i, iMax;
-
- if (this.type == 'array') {
- var arr = [];
- this.childs.forEach (function (child) {
- arr.push(child.getValue());
- });
- return arr;
- }
- else if (this.type == 'object') {
- var obj = {};
- this.childs.forEach (function (child) {
- obj[child.getField()] = child.getValue();
- });
- return obj;
- }
- else {
- if (this.value === undefined) {
- this._getDomValue();
- }
-
- return this.value;
- }
-};
-
-/**
- * Get the nesting level of this node
- * @return {Number} level
- */
-Node.prototype.getLevel = function() {
- return (this.parent ? this.parent.getLevel() + 1 : 0);
-};
-
-/**
- * Create a clone of a node
- * The complete state of a clone is copied, including whether it is expanded or
- * not. The DOM elements are not cloned.
- * @return {Node} clone
- */
-Node.prototype.clone = function() {
- var clone = new Node(this.editor);
- clone.type = this.type;
- clone.field = this.field;
- clone.fieldInnerText = this.fieldInnerText;
- clone.fieldEditable = this.fieldEditable;
- clone.value = this.value;
- clone.valueInnerText = this.valueInnerText;
- clone.expanded = this.expanded;
-
- if (this.childs) {
- // an object or array
- var cloneChilds = [];
- this.childs.forEach(function (child) {
- var childClone = child.clone();
- childClone.setParent(clone);
- cloneChilds.push(childClone);
- });
- clone.childs = cloneChilds;
- }
- else {
- // a value
- clone.childs = undefined;
- }
-
- return clone;
-};
-
-/**
- * Expand this node and optionally its childs.
- * @param {boolean} [recurse] Optional recursion, true by default. When
- * true, all childs will be expanded recursively
- */
-Node.prototype.expand = function(recurse) {
- if (!this.childs) {
- return;
- }
-
- // set this node expanded
- this.expanded = true;
- if (this.dom.expand) {
- this.dom.expand.className = 'expanded';
- }
-
- this.showChilds();
-
- if (recurse != false) {
- this.childs.forEach(function (child) {
- child.expand(recurse);
- });
- }
-};
-
-/**
- * Collapse this node and optionally its childs.
- * @param {boolean} [recurse] Optional recursion, true by default. When
- * true, all childs will be collapsed recursively
- */
-Node.prototype.collapse = function(recurse) {
- if (!this.childs) {
- return;
- }
-
- this.hideChilds();
-
- // collapse childs in case of recurse
- if (recurse != false) {
- this.childs.forEach(function (child) {
- child.collapse(recurse);
- });
-
- }
-
- // make this node collapsed
- if (this.dom.expand) {
- this.dom.expand.className = 'collapsed';
- }
- this.expanded = false;
-};
-
-/**
- * Recursively show all childs when they are expanded
- */
-Node.prototype.showChilds = function() {
- var childs = this.childs;
- if (!childs) {
- return;
- }
- if (!this.expanded) {
- return;
- }
-
- var tr = this.dom.tr;
- var table = tr ? tr.parentNode : undefined;
- if (table) {
- // show row with append button
- var append = this.getAppend();
- var nextTr = tr.nextSibling;
- if (nextTr) {
- table.insertBefore(append, nextTr);
- }
- else {
- table.appendChild(append);
- }
-
- // show childs
- this.childs.forEach(function (child) {
- table.insertBefore(child.getDom(), append);
- child.showChilds();
- });
- }
-};
-
-/**
- * Hide the node with all its childs
- */
-Node.prototype.hide = function() {
- var tr = this.dom.tr;
- var table = tr ? tr.parentNode : undefined;
- if (table) {
- table.removeChild(tr);
- }
- this.hideChilds();
-};
-
-
-/**
- * Recursively hide all childs
- */
-Node.prototype.hideChilds = function() {
- var childs = this.childs;
- if (!childs) {
- return;
- }
- if (!this.expanded) {
- return;
- }
-
- // hide append row
- var append = this.getAppend();
- if (append.parentNode) {
- append.parentNode.removeChild(append);
- }
-
- // hide childs
- this.childs.forEach(function (child) {
- child.hide();
- });
-};
-
-
-/**
- * Add a new child to the node.
- * Only applicable when Node value is of type array or object
- * @param {Node} node
- */
-Node.prototype.appendChild = function(node) {
- if (this._hasChilds()) {
- // adjust the link to the parent
- node.setParent(this);
- node.fieldEditable = (this.type == 'object');
- if (this.type == 'array') {
- node.index = this.childs.length;
- }
- this.childs.push(node);
-
- if (this.expanded) {
- // insert into the DOM, before the appendRow
- var newTr = node.getDom();
- var appendTr = this.getAppend();
- var table = appendTr ? appendTr.parentNode : undefined;
- if (appendTr && table) {
- table.insertBefore(newTr, appendTr);
- }
-
- node.showChilds();
- }
-
- this.updateDom({'updateIndexes': true});
- node.updateDom({'recurse': true});
- }
-};
-
-
-/**
- * Move a node from its current parent to this node
- * Only applicable when Node value is of type array or object
- * @param {Node} node
- * @param {Node} beforeNode
- */
-Node.prototype.moveBefore = function(node, beforeNode) {
- if (this._hasChilds()) {
- // create a temporary row, to prevent the scroll position from jumping
- // when removing the node
- var tbody = (this.dom.tr) ? this.dom.tr.parentNode : undefined;
- if (tbody) {
- var trTemp = document.createElement('tr');
- trTemp.style.height = tbody.clientHeight + 'px';
- tbody.appendChild(trTemp);
- }
-
- if (node.parent) {
- node.parent.removeChild(node);
- }
-
- if (beforeNode instanceof AppendNode) {
- this.appendChild(node);
- }
- else {
- this.insertBefore(node, beforeNode);
- }
-
- if (tbody) {
- tbody.removeChild(trTemp);
- }
- }
-};
-
-/**
- * Move a node from its current parent to this node
- * Only applicable when Node value is of type array or object.
- * If index is out of range, the node will be appended to the end
- * @param {Node} node
- * @param {Number} index
- */
-Node.prototype.moveTo = function (node, index) {
- if (node.parent == this) {
- // same parent
- var currentIndex = this.childs.indexOf(node);
- if (currentIndex < index) {
- // compensate the index for removal of the node itself
- index++;
- }
- }
-
- var beforeNode = this.childs[index] || this.append;
- this.moveBefore(node, beforeNode);
-};
-
-/**
- * Insert a new child before a given node
- * Only applicable when Node value is of type array or object
- * @param {Node} node
- * @param {Node} beforeNode
- */
-Node.prototype.insertBefore = function(node, beforeNode) {
- if (this._hasChilds()) {
- if (beforeNode == this.append) {
- // append to the child nodes
-
- // adjust the link to the parent
- node.setParent(this);
- node.fieldEditable = (this.type == 'object');
- this.childs.push(node);
- }
- else {
- // insert before a child node
- var index = this.childs.indexOf(beforeNode);
- if (index == -1) {
- throw new Error('Node not found');
- }
-
- // adjust the link to the parent
- node.setParent(this);
- node.fieldEditable = (this.type == 'object');
- this.childs.splice(index, 0, node);
- }
-
- if (this.expanded) {
- // insert into the DOM
- var newTr = node.getDom();
- var nextTr = beforeNode.getDom();
- var table = nextTr ? nextTr.parentNode : undefined;
- if (nextTr && table) {
- table.insertBefore(newTr, nextTr);
- }
-
- node.showChilds();
- }
-
- this.updateDom({'updateIndexes': true});
- node.updateDom({'recurse': true});
- }
-};
-
-/**
- * Insert a new child before a given node
- * Only applicable when Node value is of type array or object
- * @param {Node} node
- * @param {Node} afterNode
- */
-Node.prototype.insertAfter = function(node, afterNode) {
- if (this._hasChilds()) {
- var index = this.childs.indexOf(afterNode);
- var beforeNode = this.childs[index + 1];
- if (beforeNode) {
- this.insertBefore(node, beforeNode);
- }
- else {
- this.appendChild(node);
- }
- }
-};
-
-/**
- * Search in this node
- * The node will be expanded when the text is found one of its childs, else
- * it will be collapsed. Searches are case insensitive.
- * @param {String} text
- * @return {Node[]} results Array with nodes containing the search text
- */
-Node.prototype.search = function(text) {
- var results = [];
- var index;
- var search = text ? text.toLowerCase() : undefined;
-
- // delete old search data
- delete this.searchField;
- delete this.searchValue;
-
- // search in field
- if (this.field != undefined) {
- var field = String(this.field).toLowerCase();
- index = field.indexOf(search);
- if (index != -1) {
- this.searchField = true;
- results.push({
- 'node': this,
- 'elem': 'field'
- });
- }
-
- // update dom
- this._updateDomField();
- }
-
- // search in value
- if (this._hasChilds()) {
- // array, object
-
- // search the nodes childs
- if (this.childs) {
- var childResults = [];
- this.childs.forEach(function (child) {
- childResults = childResults.concat(child.search(text));
- });
- results = results.concat(childResults);
- }
-
- // update dom
- if (search != undefined) {
- var recurse = false;
- if (childResults.length == 0) {
- this.collapse(recurse);
- }
- else {
- this.expand(recurse);
- }
- }
- }
- else {
- // string, auto
- if (this.value != undefined ) {
- var value = String(this.value).toLowerCase();
- index = value.indexOf(search);
- if (index != -1) {
- this.searchValue = true;
- results.push({
- 'node': this,
- 'elem': 'value'
- });
- }
- }
-
- // update dom
- this._updateDomValue();
- }
-
- return results;
-};
-
-/**
- * Move the scroll position such that this node is in the visible area.
- * The node will not get the focus
- * @param {function(boolean)} [callback]
- */
-Node.prototype.scrollTo = function(callback) {
- if (!this.dom.tr || !this.dom.tr.parentNode) {
- // if the node is not visible, expand its parents
- var parent = this.parent;
- var recurse = false;
- while (parent) {
- parent.expand(recurse);
- parent = parent.parent;
- }
- }
-
- if (this.dom.tr && this.dom.tr.parentNode) {
- this.editor.scrollTo(this.dom.tr.offsetTop, callback);
- }
-};
-
-
-// stores the element name currently having the focus
-Node.focusElement = undefined;
-
-/**
- * Set focus to this node
- * @param {String} [elementName] The field name of the element to get the
- * focus available values: 'drag', 'menu',
- * 'expand', 'field', 'value' (default)
- */
-Node.prototype.focus = function(elementName) {
- Node.focusElement = elementName;
-
- if (this.dom.tr && this.dom.tr.parentNode) {
- var dom = this.dom;
-
- switch (elementName) {
- case 'drag':
- if (dom.drag) {
- dom.drag.focus();
- }
- else {
- dom.menu.focus();
- }
- break;
-
- case 'menu':
- dom.menu.focus();
- break;
-
- case 'expand':
- if (this._hasChilds()) {
- dom.expand.focus();
- }
- else if (dom.field && this.fieldEditable) {
- dom.field.focus();
- util.selectContentEditable(dom.field);
- }
- else if (dom.value && !this._hasChilds()) {
- dom.value.focus();
- util.selectContentEditable(dom.value);
- }
- else {
- dom.menu.focus();
- }
- break;
-
- case 'field':
- if (dom.field && this.fieldEditable) {
- dom.field.focus();
- util.selectContentEditable(dom.field);
- }
- else if (dom.value && !this._hasChilds()) {
- dom.value.focus();
- util.selectContentEditable(dom.value);
- }
- else if (this._hasChilds()) {
- dom.expand.focus();
- }
- else {
- dom.menu.focus();
- }
- break;
-
- case 'value':
- default:
- if (dom.value && !this._hasChilds()) {
- dom.value.focus();
- util.selectContentEditable(dom.value);
- }
- else if (dom.field && this.fieldEditable) {
- dom.field.focus();
- util.selectContentEditable(dom.field);
- }
- else if (this._hasChilds()) {
- dom.expand.focus();
- }
- else {
- dom.menu.focus();
- }
- break;
- }
- }
-};
-
-/**
- * Select all text in an editable div after a delay of 0 ms
- * @param {Element} editableDiv
- */
-Node.select = function(editableDiv) {
- setTimeout(function () {
- util.selectContentEditable(editableDiv);
- }, 0);
-};
-
-/**
- * Update the values from the DOM field and value of this node
- */
-Node.prototype.blur = function() {
- // retrieve the actual field and value from the DOM.
- this._getDomValue(false);
- this._getDomField(false);
-};
-
-/**
- * Duplicate given child node
- * new structure will be added right before the cloned node
- * @param {Node} node the childNode to be duplicated
- * @return {Node} clone the clone of the node
- * @private
- */
-Node.prototype._duplicate = function(node) {
- var clone = node.clone();
-
- /* TODO: adjust the field name (to prevent equal field names)
- if (this.type == 'object') {
- }
- */
-
- this.insertAfter(clone, node);
-
- return clone;
-};
-
-/**
- * Check if given node is a child. The method will check recursively to find
- * this node.
- * @param {Node} node
- * @return {boolean} containsNode
- */
-Node.prototype.containsNode = function(node) {
- if (this == node) {
- return true;
- }
-
- var childs = this.childs;
- if (childs) {
- // TODO: use the js5 Array.some() here?
- for (var i = 0, iMax = childs.length; i < iMax; i++) {
- if (childs[i].containsNode(node)) {
- return true;
- }
- }
- }
-
- return false;
-};
-
-/**
- * Move given node into this node
- * @param {Node} node the childNode to be moved
- * @param {Node} beforeNode node will be inserted before given
- * node. If no beforeNode is given,
- * the node is appended at the end
- * @private
- */
-Node.prototype._move = function(node, beforeNode) {
- if (node == beforeNode) {
- // nothing to do...
- return;
- }
-
- // check if this node is not a child of the node to be moved here
- if (node.containsNode(this)) {
- throw new Error('Cannot move a field into a child of itself');
- }
-
- // remove the original node
- if (node.parent) {
- node.parent.removeChild(node);
- }
-
- // create a clone of the node
- var clone = node.clone();
- node.clearDom();
-
- // insert or append the node
- if (beforeNode) {
- this.insertBefore(clone, beforeNode);
- }
- else {
- this.appendChild(clone);
- }
-
- /* TODO: adjust the field name (to prevent equal field names)
- if (this.type == 'object') {
- }
- */
-};
-
-/**
- * Remove a child from the node.
- * Only applicable when Node value is of type array or object
- * @param {Node} node The child node to be removed;
- * @return {Node | undefined} node The removed node on success,
- * else undefined
- */
-Node.prototype.removeChild = function(node) {
- if (this.childs) {
- var index = this.childs.indexOf(node);
-
- if (index != -1) {
- node.hide();
-
- // delete old search results
- delete node.searchField;
- delete node.searchValue;
-
- var removedNode = this.childs.splice(index, 1)[0];
-
- this.updateDom({'updateIndexes': true});
-
- return removedNode;
- }
- }
-
- return undefined;
-};
-
-/**
- * Remove a child node node from this node
- * This method is equal to Node.removeChild, except that _remove firex an
- * onChange event.
- * @param {Node} node
- * @private
- */
-Node.prototype._remove = function (node) {
- this.removeChild(node);
-};
-
-/**
- * Change the type of the value of this Node
- * @param {String} newType
- */
-Node.prototype.changeType = function (newType) {
- var oldType = this.type;
-
- if (oldType == newType) {
- // type is not changed
- return;
- }
-
- if ((newType == 'string' || newType == 'auto') &&
- (oldType == 'string' || oldType == 'auto')) {
- // this is an easy change
- this.type = newType;
- }
- else {
- // change from array to object, or from string/auto to object/array
- var table = this.dom.tr ? this.dom.tr.parentNode : undefined;
- var lastTr;
- if (this.expanded) {
- lastTr = this.getAppend();
- }
- else {
- lastTr = this.getDom();
- }
- var nextTr = (lastTr && lastTr.parentNode) ? lastTr.nextSibling : undefined;
-
- // hide current field and all its childs
- this.hide();
- this.clearDom();
-
- // adjust the field and the value
- this.type = newType;
-
- // adjust childs
- if (newType == 'object') {
- if (!this.childs) {
- this.childs = [];
- }
-
- this.childs.forEach(function (child, index) {
- child.clearDom();
- delete child.index;
- child.fieldEditable = true;
- if (child.field == undefined) {
- child.field = '';
- }
- });
-
- if (oldType == 'string' || oldType == 'auto') {
- this.expanded = true;
- }
- }
- else if (newType == 'array') {
- if (!this.childs) {
- this.childs = [];
- }
-
- this.childs.forEach(function (child, index) {
- child.clearDom();
- child.fieldEditable = false;
- child.index = index;
- });
-
- if (oldType == 'string' || oldType == 'auto') {
- this.expanded = true;
- }
- }
- else {
- this.expanded = false;
- }
-
- // create new DOM
- if (table) {
- if (nextTr) {
- table.insertBefore(this.getDom(), nextTr);
- }
- else {
- table.appendChild(this.getDom());
- }
- }
- this.showChilds();
- }
-
- if (newType == 'auto' || newType == 'string') {
- // cast value to the correct type
- if (newType == 'string') {
- this.value = String(this.value);
- }
- else {
- this.value = this._stringCast(String(this.value));
- }
-
- this.focus();
- }
-
- this.updateDom({'updateIndexes': true});
-};
-
-/**
- * Retrieve value from DOM
- * @param {boolean} [silent] If true (default), no errors will be thrown in
- * case of invalid data
- * @private
- */
-Node.prototype._getDomValue = function(silent) {
- if (this.dom.value && this.type != 'array' && this.type != 'object') {
- this.valueInnerText = util.getInnerText(this.dom.value);
- }
-
- if (this.valueInnerText != undefined) {
- try {
- // retrieve the value
- var value;
- if (this.type == 'string') {
- value = this._unescapeHTML(this.valueInnerText);
- }
- else {
- var str = this._unescapeHTML(this.valueInnerText);
- value = this._stringCast(str);
- }
- if (value !== this.value) {
- var oldValue = this.value;
- this.value = value;
- this.editor._onAction('editValue', {
- 'node': this,
- 'oldValue': oldValue,
- 'newValue': value,
- 'oldSelection': this.editor.selection,
- 'newSelection': this.editor.getSelection()
- });
- }
- }
- catch (err) {
- this.value = undefined;
- // TODO: sent an action with the new, invalid value?
- if (silent != true) {
- throw err;
- }
- }
- }
-};
-
-/**
- * Update dom value:
- * - the text color of the value, depending on the type of the value
- * - the height of the field, depending on the width
- * - background color in case it is empty
- * @private
- */
-Node.prototype._updateDomValue = function () {
- var domValue = this.dom.value;
- if (domValue) {
- // set text color depending on value type
- // TODO: put colors in css
- var v = this.value;
- var t = (this.type == 'auto') ? util.type(v) : this.type;
- var isUrl = (t == 'string' && util.isUrl(v));
- var color = '';
- if (isUrl && !this.editor.mode.edit) {
- color = '';
- }
- else if (t == 'string') {
- color = 'green';
- }
- else if (t == 'number') {
- color = 'red';
- }
- else if (t == 'boolean') {
- color = 'darkorange';
- }
- else if (this._hasChilds()) {
- color = '';
- }
- else if (v === null) {
- color = '#004ED0'; // blue
- }
- else {
- // invalid value
- color = 'black';
- }
- domValue.style.color = color;
-
- // make background color light-gray when empty
- var isEmpty = (String(this.value) == '' && this.type != 'array' && this.type != 'object');
- if (isEmpty) {
- util.addClassName(domValue, 'empty');
- }
- else {
- util.removeClassName(domValue, 'empty');
- }
-
- // underline url
- if (isUrl) {
- util.addClassName(domValue, 'url');
- }
- else {
- util.removeClassName(domValue, 'url');
- }
-
- // update title
- if (t == 'array' || t == 'object') {
- var count = this.childs ? this.childs.length : 0;
- domValue.title = this.type + ' containing ' + count + ' items';
- }
- else if (t == 'string' && util.isUrl(v)) {
- if (this.editor.mode.edit) {
- domValue.title = 'Ctrl+Click or Ctrl+Enter to open url in new window';
- }
- }
- else {
- domValue.title = '';
- }
-
- // highlight when there is a search result
- if (this.searchValueActive) {
- util.addClassName(domValue, 'highlight-active');
- }
- else {
- util.removeClassName(domValue, 'highlight-active');
- }
- if (this.searchValue) {
- util.addClassName(domValue, 'highlight');
- }
- else {
- util.removeClassName(domValue, 'highlight');
- }
-
- // strip formatting from the contents of the editable div
- util.stripFormatting(domValue);
- }
-};
-
-/**
- * Update dom field:
- * - the text color of the field, depending on the text
- * - the height of the field, depending on the width
- * - background color in case it is empty
- * @private
- */
-Node.prototype._updateDomField = function () {
- var domField = this.dom.field;
- if (domField) {
- // make backgound color lightgray when empty
- var isEmpty = (String(this.field) == '' && this.parent.type != 'array');
- if (isEmpty) {
- util.addClassName(domField, 'empty');
- }
- else {
- util.removeClassName(domField, 'empty');
- }
-
- // highlight when there is a search result
- if (this.searchFieldActive) {
- util.addClassName(domField, 'highlight-active');
- }
- else {
- util.removeClassName(domField, 'highlight-active');
- }
- if (this.searchField) {
- util.addClassName(domField, 'highlight');
- }
- else {
- util.removeClassName(domField, 'highlight');
- }
-
- // strip formatting from the contents of the editable div
- util.stripFormatting(domField);
- }
-};
-
-/**
- * Retrieve field from DOM
- * @param {boolean} [silent] If true (default), no errors will be thrown in
- * case of invalid data
- * @private
- */
-Node.prototype._getDomField = function(silent) {
- if (this.dom.field && this.fieldEditable) {
- this.fieldInnerText = util.getInnerText(this.dom.field);
- }
-
- if (this.fieldInnerText != undefined) {
- try {
- var field = this._unescapeHTML(this.fieldInnerText);
-
- if (field !== this.field) {
- var oldField = this.field;
- this.field = field;
- this.editor._onAction('editField', {
- 'node': this,
- 'oldValue': oldField,
- 'newValue': field,
- 'oldSelection': this.editor.selection,
- 'newSelection': this.editor.getSelection()
- });
- }
- }
- catch (err) {
- this.field = undefined;
- // TODO: sent an action here, with the new, invalid value?
- if (silent != true) {
- throw err;
- }
- }
- }
-};
-
-/**
- * Clear the dom of the node
- */
-Node.prototype.clearDom = function() {
- // TODO: hide the node first?
- //this.hide();
- // TODO: recursively clear dom?
-
- this.dom = {};
-};
-
-/**
- * Get the HTML DOM TR element of the node.
- * The dom will be generated when not yet created
- * @return {Element} tr HTML DOM TR Element
- */
-Node.prototype.getDom = function() {
- var dom = this.dom;
- if (dom.tr) {
- return dom.tr;
- }
-
- // create row
- dom.tr = document.createElement('tr');
- dom.tr.node = this;
-
- if (this.editor.mode.edit) {
- // create draggable area
- var tdDrag = document.createElement('td');
- if (this.parent) {
- var domDrag = document.createElement('button');
- dom.drag = domDrag;
- domDrag.className = 'dragarea';
- domDrag.title = 'Drag to move this field (Alt+Shift+Arrows)';
- tdDrag.appendChild(domDrag);
- }
- dom.tr.appendChild(tdDrag);
-
- // create context menu
- var tdMenu = document.createElement('td');
- var menu = document.createElement('button');
- dom.menu = menu;
- menu.className = 'contextmenu';
- menu.title = 'Click to open the actions menu (Ctrl+M)';
- tdMenu.appendChild(dom.menu);
- dom.tr.appendChild(tdMenu);
- }
-
- // create tree and field
- var tdField = document.createElement('td');
- dom.tr.appendChild(tdField);
- dom.tree = this._createDomTree();
- tdField.appendChild(dom.tree);
-
- this.updateDom({'updateIndexes': true});
-
- return dom.tr;
-};
-
-/**
- * DragStart event, fired on mousedown on the dragarea at the left side of a Node
- * @param {Event} event
- * @private
- */
-Node.prototype._onDragStart = function (event) {
- var node = this;
- if (!this.mousemove) {
- this.mousemove = util.addEventListener(document, 'mousemove',
- function (event) {
- node._onDrag(event);
- });
- }
-
- if (!this.mouseup) {
- this.mouseup = util.addEventListener(document, 'mouseup',
- function (event ) {
- node._onDragEnd(event);
- });
- }
-
- this.editor.highlighter.lock();
- this.drag = {
- 'oldCursor': document.body.style.cursor,
- 'startParent': this.parent,
- 'startIndex': this.parent.childs.indexOf(this),
- 'mouseX': event.pageX,
- 'level': this.getLevel()
- };
- document.body.style.cursor = 'move';
-
- event.preventDefault();
-};
-
-/**
- * Drag event, fired when moving the mouse while dragging a Node
- * @param {Event} event
- * @private
- */
-Node.prototype._onDrag = function (event) {
- // TODO: this method has grown too large. Split it in a number of methods
- var mouseY = event.pageY;
- var mouseX = event.pageX;
-
- var trThis, trPrev, trNext, trFirst, trLast, trRoot;
- var nodePrev, nodeNext;
- var topThis, topPrev, topFirst, heightThis, bottomNext, heightNext;
- var moved = false;
-
- // TODO: add an ESC option, which resets to the original position
-
- // move up/down
- trThis = this.dom.tr;
- topThis = util.getAbsoluteTop(trThis);
- heightThis = trThis.offsetHeight;
- if (mouseY < topThis) {
- // move up
- trPrev = trThis;
- do {
- trPrev = trPrev.previousSibling;
- nodePrev = Node.getNodeFromTarget(trPrev);
- topPrev = trPrev ? util.getAbsoluteTop(trPrev) : 0;
- }
- while (trPrev && mouseY < topPrev);
-
- if (nodePrev && !nodePrev.parent) {
- nodePrev = undefined;
- }
-
- if (!nodePrev) {
- // move to the first node
- trRoot = trThis.parentNode.firstChild;
- trPrev = trRoot ? trRoot.nextSibling : undefined;
- nodePrev = Node.getNodeFromTarget(trPrev);
- if (nodePrev == this) {
- nodePrev = undefined;
- }
- }
-
- if (nodePrev) {
- // check if mouseY is really inside the found node
- trPrev = nodePrev.dom.tr;
- topPrev = trPrev ? util.getAbsoluteTop(trPrev) : 0;
- if (mouseY > topPrev + heightThis) {
- nodePrev = undefined;
- }
- }
-
- if (nodePrev) {
- nodePrev.parent.moveBefore(this, nodePrev);
- moved = true;
- }
- }
- else {
- // move down
- trLast = (this.expanded && this.append) ? this.append.getDom() : this.dom.tr;
- trFirst = trLast ? trLast.nextSibling : undefined;
- if (trFirst) {
- topFirst = util.getAbsoluteTop(trFirst);
- trNext = trFirst;
- do {
- nodeNext = Node.getNodeFromTarget(trNext);
- if (trNext) {
- bottomNext = trNext.nextSibling ?
- util.getAbsoluteTop(trNext.nextSibling) : 0;
- heightNext = trNext ? (bottomNext - topFirst) : 0;
-
- if (nodeNext.parent.childs.length == 1 && nodeNext.parent.childs[0] == this) {
- // We are about to remove the last child of this parent,
- // which will make the parents appendNode visible.
- topThis += 24 - 1;
- // TODO: dangerous to suppose the height of the appendNode a constant of 24-1 px.
- }
- }
-
- trNext = trNext.nextSibling;
- }
- while (trNext && mouseY > topThis + heightNext);
-
- if (nodeNext && nodeNext.parent) {
- // calculate the desired level
- var diffX = (mouseX - this.drag.mouseX);
- var diffLevel = Math.round(diffX / 24 / 2);
- var level = this.drag.level + diffLevel; // desired level
- var levelNext = nodeNext.getLevel(); // level to be
-
- // find the best fitting level (move upwards over the append nodes)
- trPrev = nodeNext.dom.tr.previousSibling;
- while (levelNext < level && trPrev) {
- nodePrev = Node.getNodeFromTarget(trPrev);
- if (nodePrev == this || nodePrev._isChildOf(this)) {
- // neglect itself and its childs
- }
- else if (nodePrev instanceof AppendNode) {
- var childs = nodePrev.parent.childs;
- if (childs.length > 1 ||
- (childs.length == 1 && childs[0] != this)) {
- // non-visible append node of a list of childs
- // consisting of not only this node (else the
- // append node will change into a visible "empty"
- // text when removing this node).
- nodeNext = Node.getNodeFromTarget(trPrev);
- levelNext = nodeNext.getLevel();
- }
- else {
- break;
- }
- }
- else {
- break;
- }
-
- trPrev = trPrev.previousSibling;
- }
-
- // move the node when its position is changed
- if (trLast.nextSibling != nodeNext.dom.tr) {
- nodeNext.parent.moveBefore(this, nodeNext);
- moved = true;
- }
- }
- }
- }
-
- if (moved) {
- // update the dragging parameters when moved
- this.drag.mouseX = mouseX;
- this.drag.level = this.getLevel();
- }
-
- // auto scroll when hovering around the top of the editor
- this.editor.startAutoScroll(mouseY);
-
- event.preventDefault();
-};
-
-/**
- * Drag event, fired on mouseup after having dragged a node
- * @param {Event} event
- * @private
- */
-Node.prototype._onDragEnd = function (event) {
- var params = {
- 'node': this,
- 'startParent': this.drag.startParent,
- 'startIndex': this.drag.startIndex,
- 'endParent': this.parent,
- 'endIndex': this.parent.childs.indexOf(this)
- };
- if ((params.startParent != params.endParent) ||
- (params.startIndex != params.endIndex)) {
- // only register this action if the node is actually moved to another place
- this.editor._onAction('moveNode', params);
- }
-
- document.body.style.cursor = this.drag.oldCursor;
- this.editor.highlighter.unlock();
- delete this.drag;
-
- if (this.mousemove) {
- util.removeEventListener(document, 'mousemove', this.mousemove);
- delete this.mousemove;}
- if (this.mouseup) {
- util.removeEventListener(document, 'mouseup', this.mouseup);
- delete this.mouseup;
- }
-
- // Stop any running auto scroll
- this.editor.stopAutoScroll();
-
- event.preventDefault();
-};
-
-/**
- * Test if this node is a child of an other node
- * @param {Node} node
- * @return {boolean} isChild
- * @private
- */
-Node.prototype._isChildOf = function (node) {
- var n = this.parent;
- while (n) {
- if (n == node) {
- return true;
- }
- n = n.parent;
- }
-
- return false;
-};
-
-/**
- * Create an editable field
- * @return {Element} domField
- * @private
- */
-Node.prototype._createDomField = function () {
- return document.createElement('div');
-};
-
-/**
- * Set highlighting for this node and all its childs.
- * Only applied to the currently visible (expanded childs)
- * @param {boolean} highlight
- */
-Node.prototype.setHighlight = function (highlight) {
- if (this.dom.tr) {
- this.dom.tr.className = (highlight ? 'highlight' : '');
-
- if (this.append) {
- this.append.setHighlight(highlight);
- }
-
- if (this.childs) {
- this.childs.forEach(function (child) {
- child.setHighlight(highlight);
- });
- }
- }
-};
-
-/**
- * Update the value of the node. Only primitive types are allowed, no Object
- * or Array is allowed.
- * @param {String | Number | Boolean | null} value
- */
-Node.prototype.updateValue = function (value) {
- this.value = value;
- this.updateDom();
-};
-
-/**
- * Update the field of the node.
- * @param {String} field
- */
-Node.prototype.updateField = function (field) {
- this.field = field;
- this.updateDom();
-};
-
-/**
- * Update the HTML DOM, optionally recursing through the childs
- * @param {Object} [options] Available parameters:
- * {boolean} [recurse] If true, the
- * DOM of the childs will be updated recursively.
- * False by default.
- * {boolean} [updateIndexes] If true, the childs
- * indexes of the node will be updated too. False by
- * default.
- */
-Node.prototype.updateDom = function (options) {
- // update level indentation
- var domTree = this.dom.tree;
- if (domTree) {
- domTree.style.marginLeft = this.getLevel() * 24 + 'px';
- }
-
- // update field
- var domField = this.dom.field;
- if (domField) {
- if (this.fieldEditable == true) {
- // parent is an object
- domField.contentEditable = this.editor.mode.edit;
- domField.spellcheck = false;
- domField.className = 'field';
- }
- else {
- // parent is an array this is the root node
- domField.className = 'readonly';
- }
-
- var field;
- if (this.index != undefined) {
- field = this.index;
- }
- else if (this.field != undefined) {
- field = this.field;
- }
- else if (this._hasChilds()) {
- field = this.type;
- }
- else {
- field = '';
- }
- domField.innerHTML = this._escapeHTML(field);
- }
-
- // update value
- var domValue = this.dom.value;
- if (domValue) {
- var count = this.childs ? this.childs.length : 0;
- if (this.type == 'array') {
- domValue.innerHTML = '[' + count + ']';
- }
- else if (this.type == 'object') {
- domValue.innerHTML = '{' + count + '}';
- }
- else {
- domValue.innerHTML = this._escapeHTML(this.value);
- }
- }
-
- // update field and value
- this._updateDomField();
- this._updateDomValue();
-
- // update childs indexes
- if (options && options.updateIndexes == true) {
- // updateIndexes is true or undefined
- this._updateDomIndexes();
- }
-
- if (options && options.recurse == true) {
- // recurse is true or undefined. update childs recursively
- if (this.childs) {
- this.childs.forEach(function (child) {
- child.updateDom(options);
- });
- }
- }
-
- // update row with append button
- if (this.append) {
- this.append.updateDom();
- }
-};
-
-/**
- * Update the DOM of the childs of a node: update indexes and undefined field
- * names.
- * Only applicable when structure is an array or object
- * @private
- */
-Node.prototype._updateDomIndexes = function () {
- var domValue = this.dom.value;
- var childs = this.childs;
- if (domValue && childs) {
- if (this.type == 'array') {
- childs.forEach(function (child, index) {
- child.index = index;
- var childField = child.dom.field;
- if (childField) {
- childField.innerHTML = index;
- }
- });
- }
- else if (this.type == 'object') {
- childs.forEach(function (child) {
- if (child.index != undefined) {
- delete child.index;
-
- if (child.field == undefined) {
- child.field = '';
- }
- }
- });
- }
- }
-};
-
-/**
- * Create an editable value
- * @private
- */
-Node.prototype._createDomValue = function () {
- var domValue;
-
- if (this.type == 'array') {
- domValue = document.createElement('div');
- domValue.className = 'readonly';
- domValue.innerHTML = '[...]';
- }
- else if (this.type == 'object') {
- domValue = document.createElement('div');
- domValue.className = 'readonly';
- domValue.innerHTML = '{...}';
- }
- else {
- if (!this.editor.mode.edit && util.isUrl(this.value)) {
- // create a link in case of read-only editor and value containing an url
- domValue = document.createElement('a');
- domValue.className = 'value';
- domValue.href = this.value;
- domValue.target = '_blank';
- domValue.innerHTML = this._escapeHTML(this.value);
- }
- else {
- // create and editable or read-only div
- domValue = document.createElement('div');
- domValue.contentEditable = !this.editor.mode.view;
- domValue.spellcheck = false;
- domValue.className = 'value';
- domValue.innerHTML = this._escapeHTML(this.value);
- }
- }
-
- return domValue;
-};
-
-/**
- * Create an expand/collapse button
- * @return {Element} expand
- * @private
- */
-Node.prototype._createDomExpandButton = function () {
- // create expand button
- var expand = document.createElement('button');
- if (this._hasChilds()) {
- expand.className = this.expanded ? 'expanded' : 'collapsed';
- expand.title =
- 'Click to expand/collapse this field (Ctrl+E). \n' +
- 'Ctrl+Click to expand/collapse including all childs.';
- }
- else {
- expand.className = 'invisible';
- expand.title = '';
- }
-
- return expand;
-};
-
-
-/**
- * Create a DOM tree element, containing the expand/collapse button
- * @return {Element} domTree
- * @private
- */
-Node.prototype._createDomTree = function () {
- var dom = this.dom;
- var domTree = document.createElement('table');
- var tbody = document.createElement('tbody');
- domTree.style.borderCollapse = 'collapse'; // TODO: put in css
- domTree.className = 'values';
- domTree.appendChild(tbody);
- var tr = document.createElement('tr');
- tbody.appendChild(tr);
-
- // create expand button
- var tdExpand = document.createElement('td');
- tdExpand.className = 'tree';
- tr.appendChild(tdExpand);
- dom.expand = this._createDomExpandButton();
- tdExpand.appendChild(dom.expand);
- dom.tdExpand = tdExpand;
-
- // create the field
- var tdField = document.createElement('td');
- tdField.className = 'tree';
- tr.appendChild(tdField);
- dom.field = this._createDomField();
- tdField.appendChild(dom.field);
- dom.tdField = tdField;
-
- // create a separator
- var tdSeparator = document.createElement('td');
- tdSeparator.className = 'tree';
- tr.appendChild(tdSeparator);
- if (this.type != 'object' && this.type != 'array') {
- tdSeparator.appendChild(document.createTextNode(':'));
- tdSeparator.className = 'separator';
- }
- dom.tdSeparator = tdSeparator;
-
- // create the value
- var tdValue = document.createElement('td');
- tdValue.className = 'tree';
- tr.appendChild(tdValue);
- dom.value = this._createDomValue();
- tdValue.appendChild(dom.value);
- dom.tdValue = tdValue;
-
- return domTree;
-};
-
-/**
- * Handle an event. The event is catched centrally by the editor
- * @param {Event} event
- */
-Node.prototype.onEvent = function (event) {
- var type = event.type,
- target = event.target || event.srcElement,
- dom = this.dom,
- node = this,
- focusNode,
- expandable = this._hasChilds();
-
- // check if mouse is on menu or on dragarea.
- // If so, highlight current row and its childs
- if (target == dom.drag || target == dom.menu) {
- if (type == 'mouseover') {
- this.editor.highlighter.highlight(this);
- }
- else if (type == 'mouseout') {
- this.editor.highlighter.unhighlight();
- }
- }
-
- // drag events
- if (type == 'mousedown' && target == dom.drag) {
- this._onDragStart(event);
- }
-
- // context menu events
- if (type == 'click' && target == dom.menu) {
- var highlighter = node.editor.highlighter;
- highlighter.highlight(node);
- highlighter.lock();
- util.addClassName(dom.menu, 'selected');
- this.showContextMenu(dom.menu, function () {
- util.removeClassName(dom.menu, 'selected');
- highlighter.unlock();
- highlighter.unhighlight();
- });
- }
-
- // expand events
- if (type == 'click' && target == dom.expand) {
- if (expandable) {
- var recurse = event.ctrlKey; // with ctrl-key, expand/collapse all
- this._onExpand(recurse);
- }
- }
-
- // value events
- var domValue = dom.value;
- if (target == domValue) {
- //noinspection FallthroughInSwitchStatementJS
- switch (type) {
- case 'focus':
- focusNode = this;
- break;
-
- case 'blur':
- case 'change':
- this._getDomValue(true);
- this._updateDomValue();
- if (this.value) {
- domValue.innerHTML = this._escapeHTML(this.value);
- }
- break;
-
- case 'input':
- this._getDomValue(true);
- this._updateDomValue();
- break;
-
- case 'keydown':
- case 'mousedown':
- this.editor.selection = this.editor.getSelection();
- break;
-
- case 'click':
- if (event.ctrlKey && this.editor.mode.edit) {
- if (util.isUrl(this.value)) {
- window.open(this.value, '_blank');
- }
- }
- break;
-
- case 'keyup':
- this._getDomValue(true);
- this._updateDomValue();
- break;
-
- case 'cut':
- case 'paste':
- setTimeout(function () {
- node._getDomValue(true);
- node._updateDomValue();
- }, 1);
- break;
- }
- }
-
- // field events
- var domField = dom.field;
- if (target == domField) {
- switch (type) {
- case 'focus':
- focusNode = this;
- break;
-
- case 'blur':
- case 'change':
- this._getDomField(true);
- this._updateDomField();
- if (this.field) {
- domField.innerHTML = this._escapeHTML(this.field);
- }
- break;
-
- case 'input':
- this._getDomField(true);
- this._updateDomField();
- break;
-
- case 'keydown':
- case 'mousedown':
- this.editor.selection = this.editor.getSelection();
- break;
-
- case 'keyup':
- this._getDomField(true);
- this._updateDomField();
- break;
-
- case 'cut':
- case 'paste':
- setTimeout(function () {
- node._getDomField(true);
- node._updateDomField();
- }, 1);
- break;
- }
- }
-
- // focus
- // when clicked in whitespace left or right from the field or value, set focus
- var domTree = dom.tree;
- if (target == domTree.parentNode) {
- switch (type) {
- case 'click':
- var left = (event.offsetX != undefined) ?
- (event.offsetX < (this.getLevel() + 1) * 24) :
- (event.pageX < util.getAbsoluteLeft(dom.tdSeparator));// for FF
- if (left || expandable) {
- // node is expandable when it is an object or array
- if (domField) {
- util.setEndOfContentEditable(domField);
- domField.focus();
- }
- }
- else {
- if (domValue) {
- util.setEndOfContentEditable(domValue);
- domValue.focus();
- }
- }
- break;
- }
- }
- if ((target == dom.tdExpand && !expandable) || target == dom.tdField ||
- target == dom.tdSeparator) {
- switch (type) {
- case 'click':
- if (domField) {
- util.setEndOfContentEditable(domField);
- domField.focus();
- }
- break;
- }
- }
-
- if (type == 'keydown') {
- this.onKeyDown(event);
- }
-};
-
-/**
- * Key down event handler
- * @param {Event} event
- */
-Node.prototype.onKeyDown = function (event) {
- var keynum = event.which || event.keyCode;
- var target = event.target || event.srcElement;
- var ctrlKey = event.ctrlKey;
- var shiftKey = event.shiftKey;
- var altKey = event.altKey;
- var handled = false;
- var prevNode, nextNode, nextDom, nextDom2;
-
- // util.log(ctrlKey, keynum, event.charCode); // TODO: cleanup
- if (keynum == 13) { // Enter
- if (target == this.dom.value) {
- if (!this.editor.mode.edit || event.ctrlKey) {
- if (util.isUrl(this.value)) {
- window.open(this.value, '_blank');
- handled = true;
- }
- }
- }
- else if (target == this.dom.expand) {
- var expandable = this._hasChilds();
- if (expandable) {
- var recurse = event.ctrlKey; // with ctrl-key, expand/collapse all
- this._onExpand(recurse);
- target.focus();
- handled = true;
- }
- }
- }
- else if (keynum == 68) { // D
- if (ctrlKey) { // Ctrl+D
- this._onDuplicate();
- handled = true;
- }
- }
- else if (keynum == 69) { // E
- if (ctrlKey) { // Ctrl+E and Ctrl+Shift+E
- this._onExpand(shiftKey); // recurse = shiftKey
- target.focus(); // TODO: should restore focus in case of recursing expand (which takes DOM offline)
- handled = true;
- }
- }
- else if (keynum == 77) { // M
- if (ctrlKey) { // Ctrl+M
- this.showContextMenu(target);
- handled = true;
- }
- }
- else if (keynum == 46) { // Del
- if (ctrlKey) { // Ctrl+Del
- this._onRemove();
- handled = true;
- }
- }
- else if (keynum == 45) { // Ins
- if (ctrlKey && !shiftKey) { // Ctrl+Ins
- this._onInsertBefore();
- handled = true;
- }
- else if (ctrlKey && shiftKey) { // Ctrl+Shift+Ins
- this._onInsertAfter();
- handled = true;
- }
- }
- else if (keynum == 35) { // End
- if (altKey) { // Alt+End
- // find the last node
- var lastNode = this._lastNode();
- if (lastNode) {
- lastNode.focus(Node.focusElement || this._getElementName(target));
- }
- handled = true;
- }
- }
- else if (keynum == 36) { // Home
- if (altKey) { // Alt+Home
- // find the first node
- var firstNode = this._firstNode();
- if (firstNode) {
- firstNode.focus(Node.focusElement || this._getElementName(target));
- }
- handled = true;
- }
- }
- else if (keynum == 37) { // Arrow Left
- if (altKey && !shiftKey) { // Alt + Arrow Left
- // move to left element
- var prevElement = this._previousElement(target);
- if (prevElement) {
- this.focus(this._getElementName(prevElement));
- }
- handled = true;
- }
- else if (altKey && shiftKey) { // Alt + Shift Arrow left
- if (this.expanded) {
- var appendDom = this.getAppend();
- nextDom = appendDom ? appendDom.nextSibling : undefined;
- }
- else {
- var dom = this.getDom();
- nextDom = dom.nextSibling;
- }
- if (nextDom) {
- nextNode = Node.getNodeFromTarget(nextDom);
- nextDom2 = nextDom.nextSibling;
- nextNode2 = Node.getNodeFromTarget(nextDom2);
- if (nextNode && nextNode instanceof AppendNode &&
- !(this.parent.childs.length == 1) &&
- nextNode2 && nextNode2.parent) {
- nextNode2.parent.moveBefore(this, nextNode2);
- this.focus(Node.focusElement || this._getElementName(target));
- }
- }
- }
- }
- else if (keynum == 38) { // Arrow Up
- if (altKey && !shiftKey) { // Alt + Arrow Up
- // find the previous node
- prevNode = this._previousNode();
- if (prevNode) {
- prevNode.focus(Node.focusElement || this._getElementName(target));
- }
- handled = true;
- }
- else if (altKey && shiftKey) { // Alt + Shift + Arrow Up
- // find the previous node
- prevNode = this._previousNode();
- if (prevNode && prevNode.parent) {
- prevNode.parent.moveBefore(this, prevNode);
- this.focus(Node.focusElement || this._getElementName(target));
- }
- handled = true;
- }
- }
- else if (keynum == 39) { // Arrow Right
- if (altKey && !shiftKey) { // Alt + Arrow Right
- // move to right element
- var nextElement = this._nextElement(target);
- if (nextElement) {
- this.focus(this._getElementName(nextElement));
- }
- handled = true;
- }
- else if (altKey && shiftKey) { // Alt + Shift Arrow Right
- dom = this.getDom();
- var prevDom = dom.previousSibling;
- if (prevDom) {
- prevNode = Node.getNodeFromTarget(prevDom);
- if (prevNode && prevNode.parent &&
- (prevNode instanceof AppendNode)
- && !prevNode.isVisible()) {
- prevNode.parent.moveBefore(this, prevNode);
- this.focus(Node.focusElement || this._getElementName(target));
- }
- }
- }
- }
- else if (keynum == 40) { // Arrow Down
- if (altKey && !shiftKey) { // Alt + Arrow Down
- // find the next node
- nextNode = this._nextNode();
- if (nextNode) {
- nextNode.focus(Node.focusElement || this._getElementName(target));
- }
- handled = true;
- }
- else if (altKey && shiftKey) { // Alt + Shift + Arrow Down
- // find the 2nd next node and move before that one
- if (this.expanded) {
- nextNode = this.append ? this.append._nextNode() : undefined;
- }
- else {
- nextNode = this._nextNode();
- }
- nextDom = nextNode ? nextNode.getDom() : undefined;
- if (this.parent.childs.length == 1) {
- nextDom2 = nextDom;
- }
- else {
- nextDom2 = nextDom ? nextDom.nextSibling : undefined;
- }
- var nextNode2 = Node.getNodeFromTarget(nextDom2);
- if (nextNode2 && nextNode2.parent) {
- nextNode2.parent.moveBefore(this, nextNode2);
- this.focus(Node.focusElement || this._getElementName(target));
- }
- handled = true;
- }
- }
-
- if (handled) {
- event.preventDefault();
- event.stopPropagation();
- }
-};
-
-/**
- * Handle the expand event, when clicked on the expand button
- * @param {boolean} recurse If true, child nodes will be expanded too
- * @private
- */
-Node.prototype._onExpand = function (recurse) {
- if (recurse) {
- // Take the table offline
- var table = this.dom.tr.parentNode; // TODO: not nice to access the main table like this
- var frame = table.parentNode;
- var scrollTop = frame.scrollTop;
- frame.removeChild(table);
- }
-
- if (this.expanded) {
- this.collapse(recurse);
- }
- else {
- this.expand(recurse);
- }
-
- if (recurse) {
- // Put the table online again
- frame.appendChild(table);
- frame.scrollTop = scrollTop;
- }
-};
-
-/**
- * Remove this node
- * @private
- */
-Node.prototype._onRemove = function() {
- this.editor.highlighter.unhighlight();
- var childs = this.parent.childs;
- var index = childs.indexOf(this);
-
- // adjust the focus
- var oldSelection = this.editor.getSelection();
- if (childs[index + 1]) {
- childs[index + 1].focus();
- }
- else if (childs[index - 1]) {
- childs[index - 1].focus();
- }
- else {
- this.parent.focus();
- }
- var newSelection = this.editor.getSelection();
-
- // remove the node
- this.parent._remove(this);
-
- // store history action
- this.editor._onAction('removeNode', {
- 'node': this,
- 'parent': this.parent,
- 'index': index,
- 'oldSelection': oldSelection,
- 'newSelection': newSelection
- });
-};
-
-/**
- * Duplicate this node
- * @private
- */
-Node.prototype._onDuplicate = function() {
- var oldSelection = this.editor.getSelection();
- var clone = this.parent._duplicate(this);
- clone.focus();
- var newSelection = this.editor.getSelection();
-
- this.editor._onAction('duplicateNode', {
- 'node': this,
- 'clone': clone,
- 'parent': this.parent,
- 'oldSelection': oldSelection,
- 'newSelection': newSelection
- });
-};
-
-/**
- * Handle insert before event
- * @param {String} [field]
- * @param {*} [value]
- * @param {String} [type] Can be 'auto', 'array', 'object', or 'string'
- * @private
- */
-Node.prototype._onInsertBefore = function (field, value, type) {
- var oldSelection = this.editor.getSelection();
-
- var newNode = new Node(this.editor, {
- 'field': (field != undefined) ? field : '',
- 'value': (value != undefined) ? value : '',
- 'type': type
- });
- newNode.expand(true);
- this.parent.insertBefore(newNode, this);
- this.editor.highlighter.unhighlight();
- newNode.focus('field');
- var newSelection = this.editor.getSelection();
-
- this.editor._onAction('insertBeforeNode', {
- 'node': newNode,
- 'beforeNode': this,
- 'parent': this.parent,
- 'oldSelection': oldSelection,
- 'newSelection': newSelection
- });
-};
-
-/**
- * Handle insert after event
- * @param {String} [field]
- * @param {*} [value]
- * @param {String} [type] Can be 'auto', 'array', 'object', or 'string'
- * @private
- */
-Node.prototype._onInsertAfter = function (field, value, type) {
- var oldSelection = this.editor.getSelection();
-
- var newNode = new Node(this.editor, {
- 'field': (field != undefined) ? field : '',
- 'value': (value != undefined) ? value : '',
- 'type': type
- });
- newNode.expand(true);
- this.parent.insertAfter(newNode, this);
- this.editor.highlighter.unhighlight();
- newNode.focus('field');
- var newSelection = this.editor.getSelection();
-
- this.editor._onAction('insertAfterNode', {
- 'node': newNode,
- 'afterNode': this,
- 'parent': this.parent,
- 'oldSelection': oldSelection,
- 'newSelection': newSelection
- });
-};
-
-/**
- * Handle append event
- * @param {String} [field]
- * @param {*} [value]
- * @param {String} [type] Can be 'auto', 'array', 'object', or 'string'
- * @private
- */
-Node.prototype._onAppend = function (field, value, type) {
- var oldSelection = this.editor.getSelection();
-
- var newNode = new Node(this.editor, {
- 'field': (field != undefined) ? field : '',
- 'value': (value != undefined) ? value : '',
- 'type': type
- });
- newNode.expand(true);
- this.parent.appendChild(newNode);
- this.editor.highlighter.unhighlight();
- newNode.focus('field');
- var newSelection = this.editor.getSelection();
-
- this.editor._onAction('appendNode', {
- 'node': newNode,
- 'parent': this.parent,
- 'oldSelection': oldSelection,
- 'newSelection': newSelection
- });
-};
-
-/**
- * Change the type of the node's value
- * @param {String} newType
- * @private
- */
-Node.prototype._onChangeType = function (newType) {
- var oldType = this.type;
- if (newType != oldType) {
- var oldSelection = this.editor.getSelection();
- this.changeType(newType);
- var newSelection = this.editor.getSelection();
-
- this.editor._onAction('changeType', {
- 'node': this,
- 'oldType': oldType,
- 'newType': newType,
- 'oldSelection': oldSelection,
- 'newSelection': newSelection
- });
- }
-};
-
-/**
- * Sort the childs of the node. Only applicable when the node has type 'object'
- * or 'array'.
- * @param {String} direction Sorting direction. Available values: "asc", "desc"
- * @private
- */
-Node.prototype._onSort = function (direction) {
- if (this._hasChilds()) {
- var order = (direction == 'desc') ? -1 : 1;
- var prop = (this.type == 'array') ? 'value': 'field';
- this.hideChilds();
-
- var oldChilds = this.childs;
- var oldSort = this.sort;
-
- // copy the array (the old one will be kept for an undo action
- this.childs = this.childs.concat();
-
- // sort the arrays
- this.childs.sort(function (a, b) {
- if (a[prop] > b[prop]) return order;
- if (a[prop] < b[prop]) return -order;
- return 0;
- });
- this.sort = (order == 1) ? 'asc' : 'desc';
-
- this.editor._onAction('sort', {
- 'node': this,
- 'oldChilds': oldChilds,
- 'oldSort': oldSort,
- 'newChilds': this.childs,
- 'newSort': this.sort
- });
-
- this.showChilds();
- }
-};
-
-/**
- * Create a table row with an append button.
- * @return {HTMLElement | undefined} buttonAppend or undefined when inapplicable
- */
-Node.prototype.getAppend = function () {
- if (!this.append) {
- this.append = new AppendNode(this.editor);
- this.append.setParent(this);
- }
- return this.append.getDom();
-};
-
-/**
- * Find the node from an event target
- * @param {Node} target
- * @return {Node | undefined} node or undefined when not found
- * @static
- */
-Node.getNodeFromTarget = function (target) {
- while (target) {
- if (target.node) {
- return target.node;
- }
- target = target.parentNode;
- }
-
- return undefined;
-};
-
-/**
- * Get the previously rendered node
- * @return {Node | null} previousNode
- * @private
- */
-Node.prototype._previousNode = function () {
- var prevNode = null;
- var dom = this.getDom();
- if (dom && dom.parentNode) {
- // find the previous field
- var prevDom = dom;
- do {
- prevDom = prevDom.previousSibling;
- prevNode = Node.getNodeFromTarget(prevDom);
- }
- while (prevDom && (prevNode instanceof AppendNode && !prevNode.isVisible()));
- }
- return prevNode;
-};
-
-/**
- * Get the next rendered node
- * @return {Node | null} nextNode
- * @private
- */
-Node.prototype._nextNode = function () {
- var nextNode = null;
- var dom = this.getDom();
- if (dom && dom.parentNode) {
- // find the previous field
- var nextDom = dom;
- do {
- nextDom = nextDom.nextSibling;
- nextNode = Node.getNodeFromTarget(nextDom);
- }
- while (nextDom && (nextNode instanceof AppendNode && !nextNode.isVisible()));
- }
-
- return nextNode;
-};
-
-/**
- * Get the first rendered node
- * @return {Node | null} firstNode
- * @private
- */
-Node.prototype._firstNode = function () {
- var firstNode = null;
- var dom = this.getDom();
- if (dom && dom.parentNode) {
- var firstDom = dom.parentNode.firstChild;
- firstNode = Node.getNodeFromTarget(firstDom);
- }
-
- return firstNode;
-};
-
-/**
- * Get the last rendered node
- * @return {Node | null} lastNode
- * @private
- */
-Node.prototype._lastNode = function () {
- var lastNode = null;
- var dom = this.getDom();
- if (dom && dom.parentNode) {
- var lastDom = dom.parentNode.lastChild;
- lastNode = Node.getNodeFromTarget(lastDom);
- while (lastDom && (lastNode instanceof AppendNode && !lastNode.isVisible())) {
- lastDom = lastDom.previousSibling;
- lastNode = Node.getNodeFromTarget(lastDom);
- }
- }
- return lastNode;
-};
-
-/**
- * Get the next element which can have focus.
- * @param {Element} elem
- * @return {Element | null} nextElem
- * @private
- */
-Node.prototype._previousElement = function (elem) {
- var dom = this.dom;
- // noinspection FallthroughInSwitchStatementJS
- switch (elem) {
- case dom.value:
- if (this.fieldEditable) {
- return dom.field;
- }
- // intentional fall through
- case dom.field:
- if (this._hasChilds()) {
- return dom.expand;
- }
- // intentional fall through
- case dom.expand:
- return dom.menu;
- case dom.menu:
- if (dom.drag) {
- return dom.drag;
- }
- // intentional fall through
- default:
- return null;
- }
-};
-
-/**
- * Get the next element which can have focus.
- * @param {Element} elem
- * @return {Element | null} nextElem
- * @private
- */
-Node.prototype._nextElement = function (elem) {
- var dom = this.dom;
- // noinspection FallthroughInSwitchStatementJS
- switch (elem) {
- case dom.drag:
- return dom.menu;
- case dom.menu:
- if (this._hasChilds()) {
- return dom.expand;
- }
- // intentional fall through
- case dom.expand:
- if (this.fieldEditable) {
- return dom.field;
- }
- // intentional fall through
- case dom.field:
- if (!this._hasChilds()) {
- return dom.value;
- }
- default:
- return null;
- }
-};
-
-/**
- * Get the dom name of given element. returns null if not found.
- * For example when element == dom.field, "field" is returned.
- * @param {Element} element
- * @return {String | null} elementName Available elements with name: 'drag',
- * 'menu', 'expand', 'field', 'value'
- * @private
- */
-Node.prototype._getElementName = function (element) {
- var dom = this.dom;
- for (var name in dom) {
- if (dom.hasOwnProperty(name)) {
- if (dom[name] == element) {
- return name;
- }
- }
- }
- return null;
-};
-
-/**
- * Test if this node has childs. This is the case when the node is an object
- * or array.
- * @return {boolean} hasChilds
- * @private
- */
-Node.prototype._hasChilds = function () {
- return this.type == 'array' || this.type == 'object';
-};
-
-// titles with explanation for the different types
-Node.TYPE_TITLES = {
- 'auto': 'Field type "auto". ' +
- 'The field type is automatically determined from the value ' +
- 'and can be a string, number, boolean, or null.',
- 'object': 'Field type "object". ' +
- 'An object contains an unordered set of key/value pairs.',
- 'array': 'Field type "array". ' +
- 'An array contains an ordered collection of values.',
- 'string': 'Field type "string". ' +
- 'Field type is not determined from the value, ' +
- 'but always returned as string.'
-};
-
-/**
- * Show a contextmenu for this node
- * @param {HTMLElement} anchor Anchor element to attache the context menu to.
- * @param {function} [onClose] Callback method called when the context menu
- * is being closed.
- */
-Node.prototype.showContextMenu = function (anchor, onClose) {
- var node = this;
- var titles = Node.TYPE_TITLES;
- var items = [];
-
- items.push({
- 'text': 'Type',
- 'title': 'Change the type of this field',
- 'className': 'type-' + this.type,
- 'submenu': [
- {
- 'text': 'Auto',
- 'className': 'type-auto' +
- (this.type == 'auto' ? ' selected' : ''),
- 'title': titles.auto,
- 'click': function () {
- node._onChangeType('auto');
- }
- },
- {
- 'text': 'Array',
- 'className': 'type-array' +
- (this.type == 'array' ? ' selected' : ''),
- 'title': titles.array,
- 'click': function () {
- node._onChangeType('array');
- }
- },
- {
- 'text': 'Object',
- 'className': 'type-object' +
- (this.type == 'object' ? ' selected' : ''),
- 'title': titles.object,
- 'click': function () {
- node._onChangeType('object');
- }
- },
- {
- 'text': 'String',
- 'className': 'type-string' +
- (this.type == 'string' ? ' selected' : ''),
- 'title': titles.string,
- 'click': function () {
- node._onChangeType('string');
- }
- }
- ]
- });
-
- if (this._hasChilds()) {
- var direction = ((this.sort == 'asc') ? 'desc': 'asc');
- items.push({
- 'text': 'Sort',
- 'title': 'Sort the childs of this ' + this.type,
- 'className': 'sort-' + direction,
- 'click': function () {
- node._onSort(direction);
- },
- 'submenu': [
- {
- 'text': 'Ascending',
- 'className': 'sort-asc',
- 'title': 'Sort the childs of this ' + this.type + ' in ascending order',
- 'click': function () {
- node._onSort('asc');
- }
- },
- {
- 'text': 'Descending',
- 'className': 'sort-desc',
- 'title': 'Sort the childs of this ' + this.type +' in descending order',
- 'click': function () {
- node._onSort('desc');
- }
- }
- ]
- });
- }
-
- if (this.parent && this.parent._hasChilds()) {
- // create a separator
- items.push({
- 'type': 'separator'
- });
-
- // create append button (for last child node only)
- var childs = node.parent.childs;
- if (node == childs[childs.length - 1]) {
- items.push({
- 'text': 'Append',
- 'title': 'Append a new field with type \'auto\' after this field (Ctrl+Shift+Ins)',
- 'submenuTitle': 'Select the type of the field to be appended',
- 'className': 'append',
- 'click': function () {
- node._onAppend('', '', 'auto');
- },
- 'submenu': [
- {
- 'text': 'Auto',
- 'className': 'type-auto',
- 'title': titles.auto,
- 'click': function () {
- node._onAppend('', '', 'auto');
- }
- },
- {
- 'text': 'Array',
- 'className': 'type-array',
- 'title': titles.array,
- 'click': function () {
- node._onAppend('', []);
- }
- },
- {
- 'text': 'Object',
- 'className': 'type-object',
- 'title': titles.object,
- 'click': function () {
- node._onAppend('', {});
- }
- },
- {
- 'text': 'String',
- 'className': 'type-string',
- 'title': titles.string,
- 'click': function () {
- node._onAppend('', '', 'string');
- }
- }
- ]
- });
- }
-
- // create insert button
- items.push({
- 'text': 'Insert',
- 'title': 'Insert a new field with type \'auto\' before this field (Ctrl+Ins)',
- 'submenuTitle': 'Select the type of the field to be inserted',
- 'className': 'insert',
- 'click': function () {
- node._onInsertBefore('', '', 'auto');
- },
- 'submenu': [
- {
- 'text': 'Auto',
- 'className': 'type-auto',
- 'title': titles.auto,
- 'click': function () {
- node._onInsertBefore('', '', 'auto');
- }
- },
- {
- 'text': 'Array',
- 'className': 'type-array',
- 'title': titles.array,
- 'click': function () {
- node._onInsertBefore('', []);
- }
- },
- {
- 'text': 'Object',
- 'className': 'type-object',
- 'title': titles.object,
- 'click': function () {
- node._onInsertBefore('', {});
- }
- },
- {
- 'text': 'String',
- 'className': 'type-string',
- 'title': titles.string,
- 'click': function () {
- node._onInsertBefore('', '', 'string');
- }
- }
- ]
- });
-
- // create duplicate button
- items.push({
- 'text': 'Duplicate',
- 'title': 'Duplicate this field (Ctrl+D)',
- 'className': 'duplicate',
- 'click': function () {
- node._onDuplicate();
- }
- });
-
- // create remove button
- items.push({
- 'text': 'Remove',
- 'title': 'Remove this field (Ctrl+Del)',
- 'className': 'remove',
- 'click': function () {
- node._onRemove();
- }
- });
- }
-
- var menu = new ContextMenu(items, {close: onClose});
- menu.show(anchor);
-};
-
-/**
- * get the type of a value
- * @param {*} value
- * @return {String} type Can be 'object', 'array', 'string', 'auto'
- * @private
- */
-Node.prototype._getType = function(value) {
- if (value instanceof Array) {
- return 'array';
- }
- if (value instanceof Object) {
- return 'object';
- }
- if (typeof(value) == 'string' && typeof(this._stringCast(value)) != 'string') {
- return 'string';
- }
-
- return 'auto';
-};
-
-/**
- * cast contents of a string to the correct type. This can be a string,
- * a number, a boolean, etc
- * @param {String} str
- * @return {*} castedStr
- * @private
- */
-Node.prototype._stringCast = function(str) {
- var lower = str.toLowerCase(),
- num = Number(str), // will nicely fail with '123ab'
- numFloat = parseFloat(str); // will nicely fail with ' '
-
- if (str == '') {
- return '';
- }
- else if (lower == 'null') {
- return null;
- }
- else if (lower == 'true') {
- return true;
- }
- else if (lower == 'false') {
- return false;
- }
- else if (!isNaN(num) && !isNaN(numFloat)) {
- return num;
- }
- else {
- return str;
- }
-};
-
-/**
- * escape a text, such that it can be displayed safely in an HTML element
- * @param {String} text
- * @return {String} escapedText
- * @private
- */
-Node.prototype._escapeHTML = function (text) {
- var htmlEscaped = String(text)
- .replace(//g, '>')
- .replace(/ /g, ' ') // replace double space with an nbsp and space
- .replace(/^ /, ' ') // space at start
- .replace(/ $/, ' '); // space at end
-
- var json = JSON.stringify(htmlEscaped);
- return json.substring(1, json.length - 1);
-};
-
-/**
- * unescape a string.
- * @param {String} escapedText
- * @return {String} text
- * @private
- */
-Node.prototype._unescapeHTML = function (escapedText) {
- var json = '"' + this._escapeJSON(escapedText) + '"';
- var htmlEscaped = util.parse(json);
- return htmlEscaped
- .replace(/</g, '<')
- .replace(/>/g, '>')
- .replace(/ |\u00A0/g, ' ');
-};
-
-/**
- * escape a text to make it a valid JSON string. The method will:
- * - replace unescaped double quotes with '\"'
- * - replace unescaped backslash with '\\'
- * - replace returns with '\n'
- * @param {String} text
- * @return {String} escapedText
- * @private
- */
-Node.prototype._escapeJSON = function (text) {
- // TODO: replace with some smart regex (only when a new solution is faster!)
- var escaped = '';
- var i = 0, iMax = text.length;
- while (i < iMax) {
- var c = text.charAt(i);
- if (c == '\n') {
- escaped += '\\n';
- }
- else if (c == '\\') {
- escaped += c;
- i++;
-
- c = text.charAt(i);
- if ('"\\/bfnrtu'.indexOf(c) == -1) {
- escaped += '\\'; // no valid escape character
- }
- escaped += c;
- }
- else if (c == '"') {
- escaped += '\\"';
- }
- else {
- escaped += c;
- }
- i++;
- }
-
- return escaped;
-};
-
-/**
- * @constructor AppendNode
- * @extends Node
- * @param {TreeEditor} editor
- * Create a new AppendNode. This is a special node which is created at the
- * end of the list with childs for an object or array
- */
-function AppendNode (editor) {
- /** @type {TreeEditor} */
- this.editor = editor;
- this.dom = {};
-}
-
-AppendNode.prototype = new Node();
-
-/**
- * Return a table row with an append button.
- * @return {Element} dom TR element
- */
-AppendNode.prototype.getDom = function () {
- // TODO: implement a new solution for the append node
- var dom = this.dom;
-
- if (dom.tr) {
- return dom.tr;
- }
-
- // a row for the append button
- var trAppend = document.createElement('tr');
- trAppend.node = this;
- dom.tr = trAppend;
-
- // TODO: consistent naming
-
- if (this.editor.mode.edit) {
- // a cell for the dragarea column
- dom.tdDrag = document.createElement('td');
-
- // create context menu
- var tdMenu = document.createElement('td');
- dom.tdMenu = tdMenu;
- var menu = document.createElement('button');
- menu.className = 'contextmenu';
- menu.title = 'Click to open the actions menu (Ctrl+M)';
- dom.menu = menu;
- tdMenu.appendChild(dom.menu);
- }
-
- // a cell for the contents (showing text 'empty')
- var tdAppend = document.createElement('td');
- var domText = document.createElement('div');
- domText.innerHTML = '(empty)';
- domText.className = 'readonly';
- tdAppend.appendChild(domText);
- dom.td = tdAppend;
- dom.text = domText;
-
- this.updateDom();
-
- return trAppend;
-};
-
-/**
- * Update the HTML dom of the Node
- */
-AppendNode.prototype.updateDom = function () {
- var dom = this.dom;
- var tdAppend = dom.td;
- if (tdAppend) {
- tdAppend.style.paddingLeft = (this.getLevel() * 24 + 26) + 'px';
- // TODO: not so nice hard coded offset
- }
-
- var domText = dom.text;
- if (domText) {
- domText.innerHTML = '(empty ' + this.parent.type + ')';
- }
-
- // attach or detach the contents of the append node:
- // hide when the parent has childs, show when the parent has no childs
- var trAppend = dom.tr;
- if (!this.isVisible()) {
- if (dom.tr.firstChild) {
- if (dom.tdDrag) {
- trAppend.removeChild(dom.tdDrag);
- }
- if (dom.tdMenu) {
- trAppend.removeChild(dom.tdMenu);
- }
- trAppend.removeChild(tdAppend);
- }
- }
- else {
- if (!dom.tr.firstChild) {
- if (dom.tdDrag) {
- trAppend.appendChild(dom.tdDrag);
- }
- if (dom.tdMenu) {
- trAppend.appendChild(dom.tdMenu);
- }
- trAppend.appendChild(tdAppend);
- }
- }
-};
-
-/**
- * Check whether the AppendNode is currently visible.
- * the AppendNode is visible when its parent has no childs (i.e. is empty).
- * @return {boolean} isVisible
- */
-AppendNode.prototype.isVisible = function () {
- return (this.parent.childs.length == 0);
-};
-
-/**
- * Show a contextmenu for this node
- * @param {HTMLElement} anchor The element to attach the menu to.
- * @param {function} [onClose] Callback method called when the context menu
- * is being closed.
- */
-AppendNode.prototype.showContextMenu = function (anchor, onClose) {
- var node = this;
- var titles = Node.TYPE_TITLES;
- var items = [
- // create append button
- {
- 'text': 'Append',
- 'title': 'Append a new field with type \'auto\' (Ctrl+Shift+Ins)',
- 'submenuTitle': 'Select the type of the field to be appended',
- 'className': 'insert',
- 'click': function () {
- node._onAppend('', '', 'auto');
- },
- 'submenu': [
- {
- 'text': 'Auto',
- 'className': 'type-auto',
- 'title': titles.auto,
- 'click': function () {
- node._onAppend('', '', 'auto');
- }
- },
- {
- 'text': 'Array',
- 'className': 'type-array',
- 'title': titles.array,
- 'click': function () {
- node._onAppend('', []);
- }
- },
- {
- 'text': 'Object',
- 'className': 'type-object',
- 'title': titles.object,
- 'click': function () {
- node._onAppend('', {});
- }
- },
- {
- 'text': 'String',
- 'className': 'type-string',
- 'title': titles.string,
- 'click': function () {
- node._onAppend('', '', 'string');
- }
- }
- ]
- }
- ];
-
- var menu = new ContextMenu(items, {close: onClose});
- menu.show(anchor);
-};
-
-/**
- * Handle an event. The event is catched centrally by the editor
- * @param {Event} event
- */
-AppendNode.prototype.onEvent = function (event) {
- var type = event.type;
- var target = event.target || event.srcElement;
- var dom = this.dom;
-
- // highlight the append nodes parent
- var menu = dom.menu;
- if (target == menu) {
- if (type == 'mouseover') {
- this.editor.highlighter.highlight(this.parent);
- }
- else if (type == 'mouseout') {
- this.editor.highlighter.unhighlight();
- }
- }
-
- // context menu events
- if (type == 'click' && target == dom.menu) {
- var highlighter = this.editor.highlighter;
- highlighter.highlight(this.parent);
- highlighter.lock();
- util.addClassName(dom.menu, 'selected');
- this.showContextMenu(dom.menu, function () {
- util.removeClassName(dom.menu, 'selected');
- highlighter.unlock();
- highlighter.unhighlight();
- });
- }
-
- if (type == 'keydown') {
- this.onKeyDown(event);
- }
-};
-
-/**
- * A context menu
- * @param {Object[]} items Array containing the menu structure
- * TODO: describe structure
- * @param {Object} [options] Object with options. Available options:
- * {function} close Callback called when the
- * context menu is being closed.
- * @constructor
- */
-function ContextMenu (items, options) {
- this.dom = {};
-
- var me = this;
- var dom = this.dom;
- this.anchor = undefined;
- this.items = items;
- this.eventListeners = {};
- this.selection = undefined; // holds the selection before the menu was opened
- this.visibleSubmenu = undefined;
- this.onClose = options ? options.close : undefined;
-
- // create a container element
- var menu = document.createElement('div');
- menu.className = 'jsoneditor-contextmenu';
- dom.menu = menu;
-
- // create a list to hold the menu items
- var list = document.createElement('ul');
- list.className = 'menu';
- menu.appendChild(list);
- dom.list = list;
- dom.items = []; // list with all buttons
-
- // create a (non-visible) button to set the focus to the menu
- var focusButton = document.createElement('button');
- dom.focusButton = focusButton;
- var li = document.createElement('li');
- li.style.overflow = 'hidden';
- li.style.height = '0';
- li.appendChild(focusButton);
- list.appendChild(li);
-
- function createMenuItems (list, domItems, items) {
- items.forEach(function (item) {
- if (item.type == 'separator') {
- // create a separator
- var separator = document.createElement('div');
- separator.className = 'separator';
- li = document.createElement('li');
- li.appendChild(separator);
- list.appendChild(li);
- }
- else {
- var domItem = {};
-
- // create a menu item
- var li = document.createElement('li');
- list.appendChild(li);
-
- // create a button in the menu item
- var button = document.createElement('button');
- button.className = item.className;
- domItem.button = button;
- if (item.title) {
- button.title = item.title;
- }
- if (item.click) {
- button.onclick = function () {
- me.hide();
- item.click();
- };
- }
- li.appendChild(button);
-
- // create the contents of the button
- if (item.submenu) {
- // add the icon to the button
- var divIcon = document.createElement('div');
- divIcon.className = 'icon';
- button.appendChild(divIcon);
- button.appendChild(document.createTextNode(item.text));
-
- var buttonSubmenu;
- if (item.click) {
- // submenu and a button with a click handler
- button.className += ' default';
-
- var buttonExpand = document.createElement('button');
- domItem.buttonExpand = buttonExpand;
- buttonExpand.className = 'expand';
- buttonExpand.innerHTML = '';
- li.appendChild(buttonExpand);
- if (item.submenuTitle) {
- buttonExpand.title = item.submenuTitle;
- }
-
- buttonSubmenu = buttonExpand;
- }
- else {
- // submenu and a button without a click handler
- var divExpand = document.createElement('div');
- divExpand.className = 'expand';
- button.appendChild(divExpand);
-
- buttonSubmenu = button;
- }
-
- // attach a handler to expand/collapse the submenu
- buttonSubmenu.onclick = function () {
- me._onExpandItem(domItem);
- buttonSubmenu.focus();
- };
-
- // create the submenu
- var domSubItems = [];
- domItem.subItems = domSubItems;
- var ul = document.createElement('ul');
- domItem.ul = ul;
- ul.className = 'menu';
- ul.style.height = '0';
- li.appendChild(ul);
- createMenuItems(ul, domSubItems, item.submenu);
- }
- else {
- // no submenu, just a button with clickhandler
- button.innerHTML = '' + item.text;
- }
-
- domItems.push(domItem);
- }
- });
- }
- createMenuItems(list, this.dom.items, items);
-
- // TODO: when the editor is small, show the submenu on the right instead of inline?
-
- // calculate the max height of the menu with one submenu expanded
- this.maxHeight = 0; // height in pixels
- items.forEach(function (item) {
- var height = (items.length + (item.submenu ? item.submenu.length : 0)) * 24;
- me.maxHeight = Math.max(me.maxHeight, height);
- });
-}
-
-/**
- * Get the currently visible buttons
- * @return {Array.} buttons
- * @private
- */
-ContextMenu.prototype._getVisibleButtons = function () {
- var buttons = [];
- var me = this;
- this.dom.items.forEach(function (item) {
- buttons.push(item.button);
- if (item.buttonExpand) {
- buttons.push(item.buttonExpand);
- }
- if (item.subItems && item == me.expandedItem) {
- item.subItems.forEach(function (subItem) {
- buttons.push(subItem.button);
- if (subItem.buttonExpand) {
- buttons.push(subItem.buttonExpand);
- }
- // TODO: change to fully recursive method
- });
- }
- });
-
- return buttons;
-};
-
-// currently displayed context menu, a singleton. We may only have one visible context menu
-ContextMenu.visibleMenu = undefined;
-
-/**
- * Attach the menu to an anchor
- * @param {HTMLElement} anchor
- */
-ContextMenu.prototype.show = function (anchor) {
- this.hide();
-
- // calculate whether the menu fits below the anchor
- var windowHeight = window.innerHeight,
- windowScroll = (window.pageYOffset || document.scrollTop || 0),
- windowBottom = windowHeight + windowScroll,
- anchorHeight = anchor.offsetHeight,
- menuHeight = this.maxHeight;
-
- // position the menu
- var left = util.getAbsoluteLeft(anchor);
- var top = util.getAbsoluteTop(anchor);
- if (top + anchorHeight + menuHeight < windowBottom) {
- // display the menu below the anchor
- this.dom.menu.style.left = left + 'px';
- this.dom.menu.style.top = (top + anchorHeight) + 'px';
- this.dom.menu.style.bottom = '';
- }
- else {
- // display the menu above the anchor
- this.dom.menu.style.left = left + 'px';
- this.dom.menu.style.top = '';
- this.dom.menu.style.bottom = (windowHeight - top) + 'px';
- }
-
- // attach the menu to the document
- document.body.appendChild(this.dom.menu);
-
- // create and attach event listeners
- var me = this;
- var list = this.dom.list;
- this.eventListeners.mousedown = util.addEventListener(
- document, 'mousedown', function (event) {
- // hide menu on click outside of the menu
- var target = event.target;
- if ((target != list) && !me._isChildOf(target, list)) {
- me.hide();
- event.stopPropagation();
- event.preventDefault();
- }
- });
- this.eventListeners.mousewheel = util.addEventListener(
- document, 'mousewheel', function (event) {
- // block scrolling when context menu is visible
- event.stopPropagation();
- event.preventDefault();
- });
- this.eventListeners.keydown = util.addEventListener(
- document, 'keydown', function (event) {
- me._onKeyDown(event);
- });
-
- // move focus to the first button in the context menu
- this.selection = util.getSelection();
- this.anchor = anchor;
- setTimeout(function () {
- me.dom.focusButton.focus();
- }, 0);
-
- if (ContextMenu.visibleMenu) {
- ContextMenu.visibleMenu.hide();
- }
- ContextMenu.visibleMenu = this;
-};
-
-/**
- * Hide the context menu if visible
- */
-ContextMenu.prototype.hide = function () {
- // remove the menu from the DOM
- if (this.dom.menu.parentNode) {
- this.dom.menu.parentNode.removeChild(this.dom.menu);
- if (this.onClose) {
- this.onClose();
- }
- }
-
- // remove all event listeners
- // all event listeners are supposed to be attached to document.
- for (var name in this.eventListeners) {
- if (this.eventListeners.hasOwnProperty(name)) {
- var fn = this.eventListeners[name];
- if (fn) {
- util.removeEventListener(document, name, fn);
- }
- delete this.eventListeners[name];
- }
- }
-
- if (ContextMenu.visibleMenu == this) {
- ContextMenu.visibleMenu = undefined;
- }
-};
-
-/**
- * Expand a submenu
- * Any currently expanded submenu will be hided.
- * @param {Object} domItem
- * @private
- */
-ContextMenu.prototype._onExpandItem = function (domItem) {
- var me = this;
- var alreadyVisible = (domItem == this.expandedItem);
-
- // hide the currently visible submenu
- var expandedItem = this.expandedItem;
- if (expandedItem) {
- //var ul = expandedItem.ul;
- expandedItem.ul.style.height = '0';
- expandedItem.ul.style.padding = '';
- setTimeout(function () {
- if (me.expandedItem != expandedItem) {
- expandedItem.ul.style.display = '';
- util.removeClassName(expandedItem.ul.parentNode, 'selected');
- }
- }, 300); // timeout duration must match the css transition duration
- this.expandedItem = undefined;
- }
-
- if (!alreadyVisible) {
- var ul = domItem.ul;
- ul.style.display = 'block';
- var height = ul.clientHeight; // force a reflow in Firefox
- setTimeout(function () {
- if (me.expandedItem == domItem) {
- ul.style.height = (ul.childNodes.length * 24) + 'px';
- ul.style.padding = '5px 10px';
- }
- }, 0);
- util.addClassName(ul.parentNode, 'selected');
- this.expandedItem = domItem;
- }
-};
-
-/**
- * Handle onkeydown event
- * @param {Event} event
- * @private
- */
-ContextMenu.prototype._onKeyDown = function (event) {
- var target = event.target;
- var keynum = event.which;
- var handled = false;
- var buttons, targetIndex, prevButton, nextButton;
-
- if (keynum == 27) { // ESC
- // hide the menu on ESC key
-
- // restore previous selection and focus
- if (this.selection) {
- util.setSelection(this.selection);
- }
- if (this.anchor) {
- this.anchor.focus();
- }
-
- this.hide();
-
- handled = true;
- }
- else if (keynum == 9) { // Tab
- if (!event.shiftKey) { // Tab
- buttons = this._getVisibleButtons();
- targetIndex = buttons.indexOf(target);
- if (targetIndex == buttons.length - 1) {
- // move to first button
- buttons[0].focus();
- handled = true;
- }
- }
- else { // Shift+Tab
- buttons = this._getVisibleButtons();
- targetIndex = buttons.indexOf(target);
- if (targetIndex == 0) {
- // move to last button
- buttons[buttons.length - 1].focus();
- handled = true;
- }
- }
- }
- else if (keynum == 37) { // Arrow Left
- if (target.className == 'expand') {
- buttons = this._getVisibleButtons();
- targetIndex = buttons.indexOf(target);
- prevButton = buttons[targetIndex - 1];
- if (prevButton) {
- prevButton.focus();
- }
- }
- handled = true;
- }
- else if (keynum == 38) { // Arrow Up
- buttons = this._getVisibleButtons();
- targetIndex = buttons.indexOf(target);
- prevButton = buttons[targetIndex - 1];
- if (prevButton && prevButton.className == 'expand') {
- // skip expand button
- prevButton = buttons[targetIndex - 2];
- }
- if (!prevButton) {
- // move to last button
- prevButton = buttons[buttons.length - 1];
- }
- if (prevButton) {
- prevButton.focus();
- }
- handled = true;
- }
- else if (keynum == 39) { // Arrow Right
- buttons = this._getVisibleButtons();
- targetIndex = buttons.indexOf(target);
- nextButton = buttons[targetIndex + 1];
- if (nextButton && nextButton.className == 'expand') {
- nextButton.focus();
- }
- handled = true;
- }
- else if (keynum == 40) { // Arrow Down
- buttons = this._getVisibleButtons();
- targetIndex = buttons.indexOf(target);
- nextButton = buttons[targetIndex + 1];
- if (nextButton && nextButton.className == 'expand') {
- // skip expand button
- nextButton = buttons[targetIndex + 2];
- }
- if (!nextButton) {
- // move to first button
- nextButton = buttons[0];
- }
- if (nextButton) {
- nextButton.focus();
- handled = true;
- }
- handled = true;
- }
- // TODO: arrow left and right
-
- if (handled) {
- event.stopPropagation();
- event.preventDefault();
- }
-};
-
-/**
- * Test if an element is a child of a parent element.
- * @param {Element} child
- * @param {Element} parent
- * @return {boolean} isChild
- */
-ContextMenu.prototype._isChildOf = function (child, parent) {
- var e = child.parentNode;
- while (e) {
- if (e == parent) {
- return true;
- }
- e = e.parentNode;
- }
-
- return false;
-};
-
-
-/**
- * @constructor History
- * Store action history, enables undo and redo
- * @param {JSONEditor} editor
- */
-function History (editor) {
- this.editor = editor;
- this.clear();
-
- // map with all supported actions
- this.actions = {
- 'editField': {
- 'undo': function (params) {
- params.node.updateField(params.oldValue);
- },
- 'redo': function (params) {
- params.node.updateField(params.newValue);
- }
- },
- 'editValue': {
- 'undo': function (params) {
- params.node.updateValue(params.oldValue);
- },
- 'redo': function (params) {
- params.node.updateValue(params.newValue);
- }
- },
- 'appendNode': {
- 'undo': function (params) {
- params.parent.removeChild(params.node);
- },
- 'redo': function (params) {
- params.parent.appendChild(params.node);
- }
- },
- 'insertBeforeNode': {
- 'undo': function (params) {
- params.parent.removeChild(params.node);
- },
- 'redo': function (params) {
- params.parent.insertBefore(params.node, params.beforeNode);
- }
- },
- 'insertAfterNode': {
- 'undo': function (params) {
- params.parent.removeChild(params.node);
- },
- 'redo': function (params) {
- params.parent.insertAfter(params.node, params.afterNode);
- }
- },
- 'removeNode': {
- 'undo': function (params) {
- var parent = params.parent;
- var beforeNode = parent.childs[params.index] || parent.append;
- parent.insertBefore(params.node, beforeNode);
- },
- 'redo': function (params) {
- params.parent.removeChild(params.node);
- }
- },
- 'duplicateNode': {
- 'undo': function (params) {
- params.parent.removeChild(params.clone);
- },
- 'redo': function (params) {
- params.parent.insertAfter(params.clone, params.node);
- }
- },
- 'changeType': {
- 'undo': function (params) {
- params.node.changeType(params.oldType);
- },
- 'redo': function (params) {
- params.node.changeType(params.newType);
- }
- },
- 'moveNode': {
- 'undo': function (params) {
- params.startParent.moveTo(params.node, params.startIndex);
- },
- 'redo': function (params) {
- params.endParent.moveTo(params.node, params.endIndex);
- }
- },
- 'sort': {
- 'undo': function (params) {
- var node = params.node;
- node.hideChilds();
- node.sort = params.oldSort;
- node.childs = params.oldChilds;
- node.showChilds();
- },
- 'redo': function (params) {
- var node = params.node;
- node.hideChilds();
- node.sort = params.newSort;
- node.childs = params.newChilds;
- node.showChilds();
- }
- }
-
- // TODO: restore the original caret position and selection with each undo
- // TODO: implement history for actions "expand", "collapse", "scroll", "setDocument"
- };
-}
-
-/**
- * The method onChange is executed when the History is changed, and can
- * be overloaded.
- */
-History.prototype.onChange = function () {};
-
-/**
- * Add a new action to the history
- * @param {String} action The executed action. Available actions: "editField",
- * "editValue", "changeType", "appendNode",
- * "removeNode", "duplicateNode", "moveNode"
- * @param {Object} params Object containing parameters describing the change.
- * The parameters in params depend on the action (for
- * example for "editValue" the Node, old value, and new
- * value are provided). params contains all information
- * needed to undo or redo the action.
- */
-History.prototype.add = function (action, params) {
- this.index++;
- this.history[this.index] = {
- 'action': action,
- 'params': params,
- 'timestamp': new Date()
- };
-
- // remove redo actions which are invalid now
- if (this.index < this.history.length - 1) {
- this.history.splice(this.index + 1, this.history.length - this.index - 1);
- }
-
- // fire onchange event
- this.onChange();
-};
-
-/**
- * Clear history
- */
-History.prototype.clear = function () {
- this.history = [];
- this.index = -1;
-
- // fire onchange event
- this.onChange();
-};
-
-/**
- * Check if there is an action available for undo
- * @return {Boolean} canUndo
- */
-History.prototype.canUndo = function () {
- return (this.index >= 0);
-};
-
-/**
- * Check if there is an action available for redo
- * @return {Boolean} canRedo
- */
-History.prototype.canRedo = function () {
- return (this.index < this.history.length - 1);
-};
-
-/**
- * Undo the last action
- */
-History.prototype.undo = function () {
- if (this.canUndo()) {
- var obj = this.history[this.index];
- if (obj) {
- var action = this.actions[obj.action];
- if (action && action.undo) {
- action.undo(obj.params);
- if (obj.params.oldSelection) {
- this.editor.setSelection(obj.params.oldSelection);
- }
- }
- else {
- util.log('Error: unknown action "' + obj.action + '"');
- }
- }
- this.index--;
-
- // fire onchange event
- this.onChange();
- }
-};
-
-/**
- * Redo the last action
- */
-History.prototype.redo = function () {
- if (this.canRedo()) {
- this.index++;
-
- var obj = this.history[this.index];
- if (obj) {
- var action = this.actions[obj.action];
- if (action && action.redo) {
- action.redo(obj.params);
- if (obj.params.newSelection) {
- this.editor.setSelection(obj.params.newSelection);
- }
- }
- else {
- util.log('Error: unknown action "' + obj.action + '"');
- }
- }
-
- // fire onchange event
- this.onChange();
- }
-};
-
-/**
- * create a mode box to be used in the editor menu's
- * @param {JSONEditor} editor
- * @param {String[]} modes Available modes: 'code', 'form', 'text', 'tree', 'view'
- * @param {String} current Available modes: 'code', 'form', 'text', 'tree', 'view'
- * @returns {HTMLElement} box
- */
-function createModeBox(editor, modes, current) {
- /**
- * 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
- var availableModes = {
- code: {
- 'text': 'Code',
- 'title': 'Switch to code highlighter',
- 'click': function () {
- switchMode('code')
- }
- },
- form: {
- 'text': 'Form',
- 'title': 'Switch to form editor',
- 'click': function () {
- switchMode('form');
- }
- },
- text: {
- 'text': 'Text',
- 'title': 'Switch to plain text editor',
- 'click': function () {
- switchMode('text');
- }
- },
- tree: {
- 'text': 'Tree',
- 'title': 'Switch to tree editor',
- 'click': function () {
- switchMode('tree');
- }
- },
- view: {
- 'text': 'View',
- 'title': 'Switch to tree view',
- 'click': function () {
- switchMode('view');
- }
- }
- };
-
- // list the selected modes
- var items = [];
- for (var i = 0; i < modes.length; i++) {
- var mode = modes[i];
- var item = availableModes[mode];
- if (!item) {
- throw new Error('Unknown mode "' + mode + '"');
- }
-
- item.className = 'type-modes' + ((current == mode) ? ' selected' : '');
- items.push(item);
- }
-
- // retrieve the title of current mode
- var currentMode = availableModes[current];
- if (!currentMode) {
- throw new Error('Unknown mode "' + current + '"');
- }
- var currentTitle = currentMode.text;
-
- // create the html element
- var box = document.createElement('button');
- box.className = 'modes separator';
- box.innerHTML = currentTitle + ' ▾';
- box.title = 'Switch editor mode';
- box.onclick = function () {
- var menu = new ContextMenu(items);
- menu.show(box);
- };
-
- return box;
-}
-
-/**
- * @constructor SearchBox
- * Create a search box in given HTML container
- * @param {JSONEditor} editor The JSON Editor to attach to
- * @param {Element} container HTML container element of where to
- * create the search box
- */
-function SearchBox (editor, container) {
- var searchBox = this;
-
- this.editor = editor;
- this.timeout = undefined;
- this.delay = 200; // ms
- this.lastText = undefined;
-
- this.dom = {};
- this.dom.container = container;
-
- var table = document.createElement('table');
- this.dom.table = table;
- table.className = 'search';
- container.appendChild(table);
- var tbody = document.createElement('tbody');
- this.dom.tbody = tbody;
- table.appendChild(tbody);
- var tr = document.createElement('tr');
- tbody.appendChild(tr);
-
- var td = document.createElement('td');
- tr.appendChild(td);
- var results = document.createElement('div');
- this.dom.results = results;
- results.className = 'results';
- td.appendChild(results);
-
- td = document.createElement('td');
- tr.appendChild(td);
- var divInput = document.createElement('div');
- this.dom.input = divInput;
- divInput.className = 'frame';
- divInput.title = 'Search fields and values';
- td.appendChild(divInput);
-
- // table to contain the text input and search button
- var tableInput = document.createElement('table');
- divInput.appendChild(tableInput);
- var tbodySearch = document.createElement('tbody');
- tableInput.appendChild(tbodySearch);
- tr = document.createElement('tr');
- tbodySearch.appendChild(tr);
-
- var refreshSearch = document.createElement('button');
- refreshSearch.className = 'refresh';
- td = document.createElement('td');
- td.appendChild(refreshSearch);
- tr.appendChild(td);
-
- var search = document.createElement('input');
- this.dom.search = search;
- search.oninput = function (event) {
- searchBox._onDelayedSearch(event);
- };
- search.onchange = function (event) { // For IE 9
- searchBox._onSearch(event);
- };
- search.onkeydown = function (event) {
- searchBox._onKeyDown(event);
- };
- search.onkeyup = function (event) {
- searchBox._onKeyUp(event);
- };
- refreshSearch.onclick = function (event) {
- search.select();
- };
-
- // TODO: ESC in FF restores the last input, is a FF bug, https://bugzilla.mozilla.org/show_bug.cgi?id=598819
- td = document.createElement('td');
- td.appendChild(search);
- tr.appendChild(td);
-
- var searchNext = document.createElement('button');
- searchNext.title = 'Next result (Enter)';
- searchNext.className = 'next';
- searchNext.onclick = function () {
- searchBox.next();
- };
- td = document.createElement('td');
- td.appendChild(searchNext);
- tr.appendChild(td);
-
- var searchPrevious = document.createElement('button');
- searchPrevious.title = 'Previous result (Shift+Enter)';
- searchPrevious.className = 'previous';
- searchPrevious.onclick = function () {
- searchBox.previous();
- };
- td = document.createElement('td');
- td.appendChild(searchPrevious);
- tr.appendChild(td);
-}
-
-/**
- * Go to the next search result
- * @param {boolean} [focus] If true, focus will be set to the next result
- * focus is false by default.
- */
-SearchBox.prototype.next = function(focus) {
- if (this.results != undefined) {
- var index = (this.resultIndex != undefined) ? this.resultIndex + 1 : 0;
- if (index > this.results.length - 1) {
- index = 0;
- }
- this._setActiveResult(index, focus);
- }
-};
-
-/**
- * Go to the prevous search result
- * @param {boolean} [focus] If true, focus will be set to the next result
- * focus is false by default.
- */
-SearchBox.prototype.previous = function(focus) {
- if (this.results != undefined) {
- var max = this.results.length - 1;
- var index = (this.resultIndex != undefined) ? this.resultIndex - 1 : max;
- if (index < 0) {
- index = max;
- }
- this._setActiveResult(index, focus);
- }
-};
-
-/**
- * Set new value for the current active result
- * @param {Number} index
- * @param {boolean} [focus] If true, focus will be set to the next result.
- * focus is false by default.
- * @private
- */
-SearchBox.prototype._setActiveResult = function(index, focus) {
- // de-activate current active result
- if (this.activeResult) {
- var prevNode = this.activeResult.node;
- var prevElem = this.activeResult.elem;
- if (prevElem == 'field') {
- delete prevNode.searchFieldActive;
- }
- else {
- delete prevNode.searchValueActive;
- }
- prevNode.updateDom();
- }
-
- if (!this.results || !this.results[index]) {
- // out of range, set to undefined
- this.resultIndex = undefined;
- this.activeResult = undefined;
- return;
- }
-
- this.resultIndex = index;
-
- // set new node active
- var node = this.results[this.resultIndex].node;
- var elem = this.results[this.resultIndex].elem;
- if (elem == 'field') {
- node.searchFieldActive = true;
- }
- else {
- node.searchValueActive = true;
- }
- this.activeResult = this.results[this.resultIndex];
- node.updateDom();
-
- // TODO: not so nice that the focus is only set after the animation is finished
- node.scrollTo(function () {
- if (focus) {
- node.focus(elem);
- }
- });
-};
-
-/**
- * Cancel any running onDelayedSearch.
- * @private
- */
-SearchBox.prototype._clearDelay = function() {
- if (this.timeout != undefined) {
- clearTimeout(this.timeout);
- delete this.timeout;
- }
-};
-
-/**
- * Start a timer to execute a search after a short delay.
- * Used for reducing the number of searches while typing.
- * @param {Event} event
- * @private
- */
-SearchBox.prototype._onDelayedSearch = function (event) {
- // execute the search after a short delay (reduces the number of
- // search actions while typing in the search text box)
- this._clearDelay();
- var searchBox = this;
- this.timeout = setTimeout(function (event) {
- searchBox._onSearch(event);
- },
- this.delay);
-};
-
-/**
- * Handle onSearch event
- * @param {Event} event
- * @param {boolean} [forceSearch] If true, search will be executed again even
- * when the search text is not changed.
- * Default is false.
- * @private
- */
-SearchBox.prototype._onSearch = function (event, forceSearch) {
- this._clearDelay();
-
- var value = this.dom.search.value;
- var text = (value.length > 0) ? value : undefined;
- if (text != this.lastText || forceSearch) {
- // only search again when changed
- this.lastText = text;
- this.results = this.editor.search(text);
- this._setActiveResult(undefined);
-
- // display search results
- if (text != undefined) {
- var resultCount = this.results.length;
- switch (resultCount) {
- case 0: this.dom.results.innerHTML = 'no results'; break;
- case 1: this.dom.results.innerHTML = '1 result'; break;
- default: this.dom.results.innerHTML = resultCount + ' results'; break;
- }
- }
- else {
- this.dom.results.innerHTML = '';
- }
- }
-};
-
-/**
- * Handle onKeyDown event in the input box
- * @param {Event} event
- * @private
- */
-SearchBox.prototype._onKeyDown = function (event) {
- var keynum = event.which;
- if (keynum == 27) { // ESC
- this.dom.search.value = ''; // clear search
- this._onSearch(event);
- event.preventDefault();
- event.stopPropagation();
- }
- else if (keynum == 13) { // Enter
- if (event.ctrlKey) {
- // force to search again
- this._onSearch(event, true);
- }
- else if (event.shiftKey) {
- // move to the previous search result
- this.previous();
- }
- else {
- // move to the next search result
- this.next();
- }
- event.preventDefault();
- event.stopPropagation();
- }
-};
-
-/**
- * Handle onKeyUp event in the input box
- * @param {Event} event
- * @private
- */
-SearchBox.prototype._onKeyUp = function (event) {
- var keynum = event.keyCode;
- if (keynum != 27 && keynum != 13) { // !show and !Enter
- this._onDelayedSearch(event); // For IE 9
- }
-};
-
-/**
- * The highlighter can highlight/unhighlight a node, and
- * animate the visibility of a context menu.
- * @constructor Highlighter
- */
-function Highlighter () {
- this.locked = false;
-}
-
-/**
- * Hightlight given node and its childs
- * @param {Node} node
- */
-Highlighter.prototype.highlight = function (node) {
- if (this.locked) {
- return;
- }
-
- if (this.node != node) {
- // unhighlight current node
- if (this.node) {
- this.node.setHighlight(false);
- }
-
- // highlight new node
- this.node = node;
- this.node.setHighlight(true);
- }
-
- // cancel any current timeout
- this._cancelUnhighlight();
-};
-
-/**
- * Unhighlight currently highlighted node.
- * Will be done after a delay
- */
-Highlighter.prototype.unhighlight = function () {
- if (this.locked) {
- return;
- }
-
- var me = this;
- if (this.node) {
- this._cancelUnhighlight();
-
- // do the unhighlighting after a small delay, to prevent re-highlighting
- // the same node when moving from the drag-icon to the contextmenu-icon
- // or vice versa.
- this.unhighlightTimer = setTimeout(function () {
- me.node.setHighlight(false);
- me.node = undefined;
- me.unhighlightTimer = undefined;
- }, 0);
- }
-};
-
-/**
- * Cancel an unhighlight action (if before the timeout of the unhighlight action)
- * @private
- */
-Highlighter.prototype._cancelUnhighlight = function () {
- if (this.unhighlightTimer) {
- clearTimeout(this.unhighlightTimer);
- this.unhighlightTimer = undefined;
- }
-};
-
-/**
- * Lock highlighting or unhighlighting nodes.
- * methods highlight and unhighlight do not work while locked.
- */
-Highlighter.prototype.lock = function () {
- this.locked = true;
-};
-
-/**
- * Unlock highlighting or unhighlighting nodes
- */
-Highlighter.prototype.unlock = function () {
- this.locked = false;
-};
-
-// create namespace
-util = {};
-
-/**
- * Parse JSON using the parser built-in in the browser.
- * On exception, the jsonString is validated and a detailed error is thrown.
- * @param {String} jsonString
- */
-util.parse = function parse(jsonString) {
- try {
- return JSON.parse(jsonString);
- }
- catch (err) {
- // try to throw a more detailed error message using validate
- util.validate(jsonString);
- throw err;
- }
-};
-
-/**
- * Validate a string containing a JSON object
- * This method uses JSONLint to validate the String. If JSONLint is not
- * available, the built-in JSON parser of the browser is used.
- * @param {String} jsonString String with an (invalid) JSON object
- * @throws Error
- */
-util.validate = function validate(jsonString) {
- if (typeof(jsonlint) != 'undefined') {
- jsonlint.parse(jsonString);
- }
- else {
- JSON.parse(jsonString);
- }
-};
-
-/**
- * Extend object a with the properties of object b
- * @param {Object} a
- * @param {Object} b
- * @return {Object} a
- */
-util.extend = function extend(a, b) {
- for (var prop in b) {
- if (b.hasOwnProperty(prop)) {
- a[prop] = b[prop];
- }
- }
- return a;
-};
-
-/**
- * Remove all properties from object a
- * @param {Object} a
- * @return {Object} a
- */
-util.clear = function clear (a) {
- for (var prop in a) {
- if (a.hasOwnProperty(prop)) {
- delete a[prop];
- }
- }
- return a;
-};
-
-/**
- * Output text to the console, if console is available
- * @param {...*} args
- */
-util.log = function log (args) {
- if (typeof console !== 'undefined' && typeof console.log === 'function') {
- console.log.apply(console, arguments);
- }
-};
-
-/**
- * Get the type of an object
- * @param {*} object
- * @return {String} type
- */
-util.type = function type (object) {
- if (object === null) {
- return 'null';
- }
- if (object === undefined) {
- return 'undefined';
- }
- if ((object instanceof Number) || (typeof object === 'number')) {
- return 'number';
- }
- if ((object instanceof String) || (typeof object === 'string')) {
- return 'string';
- }
- if ((object instanceof Boolean) || (typeof object === 'boolean')) {
- return 'boolean';
- }
- if ((object instanceof RegExp) || (typeof object === 'regexp')) {
- return 'regexp';
- }
- if (Array.isArray(object)) {
- return 'array';
- }
-
- return 'object';
-};
-
-/**
- * Test whether a text contains a url (matches when a string starts
- * with 'http://*' or 'https://*' and has no whitespace characters)
- * @param {String} text
- */
-var isUrlRegex = /^https?:\/\/\S+$/;
-util.isUrl = function isUrl (text) {
- return (typeof text == 'string' || text instanceof String) &&
- isUrlRegex.test(text);
-};
-
-/**
- * Retrieve the absolute left value of a DOM element
- * @param {Element} elem A dom element, for example a div
- * @return {Number} left The absolute left position of this element
- * in the browser page.
- */
-util.getAbsoluteLeft = function getAbsoluteLeft(elem) {
- var rect = elem.getBoundingClientRect();
- return rect.left + window.pageXOffset || document.scrollLeft || 0;
-};
-
-/**
- * Retrieve the absolute top value of a DOM element
- * @param {Element} elem A dom element, for example a div
- * @return {Number} top The absolute top position of this element
- * in the browser page.
- */
-util.getAbsoluteTop = function getAbsoluteTop(elem) {
- var rect = elem.getBoundingClientRect();
- return rect.top + window.pageYOffset || document.scrollTop || 0;
-};
-
-/**
- * add a className to the given elements style
- * @param {Element} elem
- * @param {String} className
- */
-util.addClassName = function addClassName(elem, className) {
- var classes = elem.className.split(' ');
- if (classes.indexOf(className) == -1) {
- classes.push(className); // add the class to the array
- elem.className = classes.join(' ');
- }
-};
-
-/**
- * add a className to the given elements style
- * @param {Element} elem
- * @param {String} className
- */
-util.removeClassName = function removeClassName(elem, className) {
- var classes = elem.className.split(' ');
- var index = classes.indexOf(className);
- if (index != -1) {
- classes.splice(index, 1); // remove the class from the array
- elem.className = classes.join(' ');
- }
-};
-
-/**
- * Strip the formatting from the contents of a div
- * the formatting from the div itself is not stripped, only from its childs.
- * @param {Element} divElement
- */
-util.stripFormatting = function stripFormatting(divElement) {
- var childs = divElement.childNodes;
- for (var i = 0, iMax = childs.length; i < iMax; i++) {
- var child = childs[i];
-
- // remove the style
- if (child.style) {
- // TODO: test if child.attributes does contain style
- child.removeAttribute('style');
- }
-
- // remove all attributes
- var attributes = child.attributes;
- if (attributes) {
- for (var j = attributes.length - 1; j >= 0; j--) {
- var attribute = attributes[j];
- if (attribute.specified == true) {
- child.removeAttribute(attribute.name);
- }
- }
- }
-
- // recursively strip childs
- util.stripFormatting(child);
- }
-};
-
-/**
- * Set focus to the end of an editable div
- * code from Nico Burns
- * http://stackoverflow.com/users/140293/nico-burns
- * http://stackoverflow.com/questions/1125292/how-to-move-cursor-to-end-of-contenteditable-entity
- * @param {Element} contentEditableElement A content editable div
- */
-util.setEndOfContentEditable = function setEndOfContentEditable(contentEditableElement) {
- var range, selection;
- if(document.createRange) {
- range = document.createRange();//Create a range (a range is a like the selection but invisible)
- range.selectNodeContents(contentEditableElement);//Select the entire contents of the element with the range
- range.collapse(false);//collapse the range to the end point. false means collapse to end rather than the start
- selection = window.getSelection();//get the selection object (allows you to change selection)
- selection.removeAllRanges();//remove any selections already made
- selection.addRange(range);//make the range you have just created the visible selection
- }
-};
-
-/**
- * Select all text of a content editable div.
- * http://stackoverflow.com/a/3806004/1262753
- * @param {Element} contentEditableElement A content editable div
- */
-util.selectContentEditable = function selectContentEditable(contentEditableElement) {
- if (!contentEditableElement || contentEditableElement.nodeName != 'DIV') {
- return;
- }
-
- var sel, range;
- if (window.getSelection && document.createRange) {
- range = document.createRange();
- range.selectNodeContents(contentEditableElement);
- sel = window.getSelection();
- sel.removeAllRanges();
- sel.addRange(range);
- }
-};
-
-/**
- * Get text selection
- * http://stackoverflow.com/questions/4687808/contenteditable-selected-text-save-and-restore
- * @return {Range | TextRange | null} range
- */
-util.getSelection = function getSelection() {
- if (window.getSelection) {
- var sel = window.getSelection();
- if (sel.getRangeAt && sel.rangeCount) {
- return sel.getRangeAt(0);
- }
- }
- return null;
-};
-
-/**
- * Set text selection
- * http://stackoverflow.com/questions/4687808/contenteditable-selected-text-save-and-restore
- * @param {Range | TextRange | null} range
- */
-util.setSelection = function setSelection(range) {
- if (range) {
- if (window.getSelection) {
- var sel = window.getSelection();
- sel.removeAllRanges();
- sel.addRange(range);
- }
- }
-};
-
-/**
- * Get selected text range
- * @return {Object} params object containing parameters:
- * {Number} startOffset
- * {Number} endOffset
- * {Element} container HTML element holding the
- * selected text element
- * Returns null if no text selection is found
- */
-util.getSelectionOffset = function getSelectionOffset() {
- var range = util.getSelection();
-
- if (range && 'startOffset' in range && 'endOffset' in range &&
- range.startContainer && (range.startContainer == range.endContainer)) {
- return {
- startOffset: range.startOffset,
- endOffset: range.endOffset,
- container: range.startContainer.parentNode
- };
- }
-
- return null;
-};
-
-/**
- * Set selected text range in given element
- * @param {Object} params An object containing:
- * {Element} container
- * {Number} startOffset
- * {Number} endOffset
- */
-util.setSelectionOffset = function setSelectionOffset(params) {
- if (document.createRange && window.getSelection) {
- var selection = window.getSelection();
- if(selection) {
- var range = document.createRange();
- // TODO: do not suppose that the first child of the container is a textnode,
- // but recursively find the textnodes
- range.setStart(params.container.firstChild, params.startOffset);
- range.setEnd(params.container.firstChild, params.endOffset);
-
- util.setSelection(range);
- }
- }
-};
-
-/**
- * Get the inner text of an HTML element (for example a div element)
- * @param {Element} element
- * @param {Object} [buffer]
- * @return {String} innerText
- */
-util.getInnerText = function getInnerText(element, buffer) {
- var first = (buffer == undefined);
- if (first) {
- buffer = {
- 'text': '',
- 'flush': function () {
- var text = this.text;
- this.text = '';
- return text;
- },
- 'set': function (text) {
- this.text = text;
- }
- };
- }
-
- // text node
- if (element.nodeValue) {
- return buffer.flush() + element.nodeValue;
- }
-
- // divs or other HTML elements
- if (element.hasChildNodes()) {
- var childNodes = element.childNodes;
- var innerText = '';
-
- for (var i = 0, iMax = childNodes.length; i < iMax; i++) {
- var child = childNodes[i];
-
- if (child.nodeName == 'DIV' || child.nodeName == 'P') {
- var prevChild = childNodes[i - 1];
- var prevName = prevChild ? prevChild.nodeName : undefined;
- if (prevName && prevName != 'DIV' && prevName != 'P' && prevName != 'BR') {
- innerText += '\n';
- buffer.flush();
- }
- innerText += util.getInnerText(child, buffer);
- buffer.set('\n');
- }
- else if (child.nodeName == 'BR') {
- innerText += buffer.flush();
- buffer.set('\n');
- }
- else {
- innerText += util.getInnerText(child, buffer);
- }
- }
-
- return innerText;
- }
- else {
- if (element.nodeName == 'P' && util.getInternetExplorerVersion() != -1) {
- // On Internet Explorer, a
with hasChildNodes()==false is
- // rendered with a new line. Note that a
with
- // hasChildNodes()==true is rendered without a new line
- // Other browsers always ensure there is a inside the
,
- // and if not, the
does not render a new line
- return buffer.flush();
- }
- }
-
- // br or unknown
- return '';
-};
-
-/**
- * Returns the version of Internet Explorer or a -1
- * (indicating the use of another browser).
- * Source: http://msdn.microsoft.com/en-us/library/ms537509(v=vs.85).aspx
- * @return {Number} Internet Explorer version, or -1 in case of an other browser
- */
-util.getInternetExplorerVersion = function getInternetExplorerVersion() {
- if (_ieVersion == -1) {
- var rv = -1; // Return value assumes failure.
- if (navigator.appName == 'Microsoft Internet Explorer')
- {
- var ua = navigator.userAgent;
- var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
- if (re.exec(ua) != null) {
- rv = parseFloat( RegExp.$1 );
- }
- }
-
- _ieVersion = rv;
- }
-
- return _ieVersion;
-};
-
-/**
- * Test whether the current browser is Firefox
- * @returns {boolean} isFirefox
- */
-util.isFirefox = function isFirefox () {
- return (navigator.userAgent.indexOf("Firefox") != -1);
-};
-
-/**
- * cached internet explorer version
- * @type {Number}
- * @private
- */
-var _ieVersion = -1;
-
-/**
- * Add and event listener. Works for all browsers
- * @param {Element} element An html element
- * @param {string} action The action, for example "click",
- * without the prefix "on"
- * @param {function} listener The callback function to be executed
- * @param {boolean} [useCapture] false by default
- * @return {function} the created event listener
- */
-util.addEventListener = function addEventListener(element, action, listener, useCapture) {
- if (element.addEventListener) {
- if (useCapture === undefined)
- useCapture = false;
-
- if (action === "mousewheel" && util.isFirefox()) {
- action = "DOMMouseScroll"; // For Firefox
- }
-
- element.addEventListener(action, listener, useCapture);
- return listener;
- } else if (element.attachEvent) {
- // Old IE browsers
- var f = function () {
- return listener.call(element, window.event);
- };
- element.attachEvent("on" + action, f);
- return f;
- }
-};
-
-/**
- * Remove an event listener from an element
- * @param {Element} element An html dom element
- * @param {string} action The name of the event, for example "mousedown"
- * @param {function} listener The listener function
- * @param {boolean} [useCapture] false by default
- */
-util.removeEventListener = function removeEventListener(element, action, listener, useCapture) {
- if (element.removeEventListener) {
- if (useCapture === undefined)
- useCapture = false;
-
- if (action === "mousewheel" && util.isFirefox()) {
- action = "DOMMouseScroll"; // For Firefox
- }
-
- element.removeEventListener(action, listener, useCapture);
- } else if (element.detachEvent) {
- // Old IE browsers
- element.detachEvent("on" + action, listener);
- }
-};
-
-
-// module exports
-var jsoneditor = {
- 'JSONEditor': JSONEditor,
- 'JSONFormatter': function () {
- throw new Error('JSONFormatter is deprecated. ' +
- 'Use JSONEditor with mode "text" or "code" instead');
- },
- 'util': util
-};
-
-/**
- * load jsoneditor.css
- */
-var loadCss = function () {
- // find the script named 'jsoneditor.js' or 'jsoneditor.min.js' or
- // 'jsoneditor.min.js', and use its path to find the css file to be
- // loaded.
- var scripts = document.getElementsByTagName('script');
- for (var s = 0; s < scripts.length; s++) {
- var src = scripts[s].src;
- if (/(^|\/)jsoneditor([-\.]min)?.js$/.test(src)) {
- var jsFile = src.split('?')[0];
- var cssFile = jsFile.substring(0, jsFile.length - 2) + 'css';
-
- // load css file
- var link = document.createElement('link');
- link.type = 'text/css';
- link.rel = 'stylesheet';
- link.href = cssFile;
- document.getElementsByTagName('head')[0].appendChild(link);
-
- break;
- }
- }
-};
-
-/**
- * CommonJS module exports
- */
-if (typeof(module) != 'undefined' && typeof(exports) != 'undefined') {
- loadCss();
- module.exports = exports = jsoneditor;
-}
-
-/**
- * AMD module exports
- */
-if (typeof(require) != 'undefined' && typeof(define) != 'undefined') {
- loadCss();
- define(function () {
- return jsoneditor;
- });
-}
-else {
- // attach the module to the window, load as a regular javascript file
- window['jsoneditor'] = jsoneditor;
-}
-
-
-})();
+(function webpackUniversalModuleDefinition(root, factory) {
+ if(typeof exports === 'object' && typeof module === 'object')
+ module.exports = factory();
+ else if(typeof define === 'function' && define.amd)
+ define(factory);
+ else if(typeof exports === 'object')
+ exports["JSONEditor"] = factory();
+ else
+ root["JSONEditor"] = factory();
+})(this, function() {
+return /******/ (function(modules) { // webpackBootstrap
+/******/
+/******/ // The module cache
+/******/ var installedModules = {};
+/******/
+/******/ // The require function
+/******/ function __webpack_require__(moduleId) {
+/******/ // Check if module is in cache
+/******/ if(installedModules[moduleId])
+/******/ return installedModules[moduleId].exports;
+/******/
+/******/ // Create a new module (and put it into the cache)
+/******/ var module = installedModules[moduleId] = {
+/******/ exports: {},
+/******/ id: moduleId,
+/******/ loaded: false
+/******/ };
+/******/
+/******/ // Execute the module function
+/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
+/******/
+/******/ // Flag the module as loaded
+/******/ module.loaded = true;
+/******/
+/******/ // Return the exports of the module
+/******/ return module.exports;
+/******/ }
+/******/
+/******/
+/******/ // expose the modules object (__webpack_modules__)
+/******/ __webpack_require__.m = modules;
+/******/
+/******/ // expose the module cache
+/******/ __webpack_require__.c = installedModules;
+/******/
+/******/ // __webpack_public_path__
+/******/ __webpack_require__.p = "";
+/******/
+/******/
+/******/ // Load entry module and return exports
+/******/ return __webpack_require__(0);
+/******/ })
+/************************************************************************/
+/******/ ([
+/* 0 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(1), __webpack_require__(2), __webpack_require__(3)], __WEBPACK_AMD_DEFINE_RESULT__ = (function (TreeEditor, TextEditor, util) {
+
+ /**
+ * @constructor JSONEditor
+ * @param {Element} container Container element
+ * @param {Object} [options] Object with options. available options:
+ * {String} mode Editor mode. Available values:
+ * 'tree' (default), 'view',
+ * 'form', 'text', and 'code'.
+ * {function} change Callback method, triggered
+ * on change of contents
+ * {Boolean} search Enable search box.
+ * True by default
+ * Only applicable for modes
+ * 'tree', 'view', and 'form'
+ * {Boolean} history Enable history (undo/redo).
+ * True by default
+ * Only applicable for modes
+ * 'tree', 'view', and 'form'
+ * {String} name Field name for the root node.
+ * Only applicable for modes
+ * 'tree', 'view', and 'form'
+ * {Number} indentation Number of indentation
+ * spaces. 4 by default.
+ * Only applicable for
+ * modes 'text' and 'code'
+ * @param {Object | undefined} json JSON object
+ */
+ function JSONEditor (container, options, json) {
+ if (!(this instanceof JSONEditor)) {
+ throw new Error('JSONEditor constructor called without "new".');
+ }
+
+ // check for unsupported browser (IE8 and older)
+ var ieVersion = util.getInternetExplorerVersion();
+ if (ieVersion != -1 && ieVersion < 9) {
+ throw new Error('Unsupported browser, IE9 or newer required. ' +
+ 'Please install the newest version of your browser.');
+ }
+
+ if (arguments.length) {
+ this._create(container, options, json);
+ }
+ }
+
+ /**
+ * Configuration for all registered modes. Example:
+ * {
+ * tree: {
+ * editor: TreeEditor,
+ * data: 'json'
+ * },
+ * text: {
+ * editor: TextEditor,
+ * data: 'text'
+ * }
+ * }
+ *
+ * @type { Object. }
+ */
+ JSONEditor.modes = {};
+
+ /**
+ * Create the JSONEditor
+ * @param {Element} container Container element
+ * @param {Object} [options] See description in constructor
+ * @param {Object | undefined} json JSON object
+ * @private
+ */
+ JSONEditor.prototype._create = function (container, options, json) {
+ this.container = container;
+ this.options = options || {};
+ this.json = json || {};
+
+ var mode = this.options.mode || 'tree';
+ this.setMode(mode);
+ };
+
+ /**
+ * Detach the editor from the DOM
+ * @private
+ */
+ JSONEditor.prototype._delete = function () {};
+
+ /**
+ * Set JSON object in editor
+ * @param {Object | undefined} json JSON data
+ */
+ JSONEditor.prototype.set = function (json) {
+ this.json = json;
+ };
+
+ /**
+ * Get JSON from the editor
+ * @returns {Object} json
+ */
+ JSONEditor.prototype.get = function () {
+ return this.json;
+ };
+
+ /**
+ * Set string containing JSON for the editor
+ * @param {String | undefined} jsonText
+ */
+ JSONEditor.prototype.setText = function (jsonText) {
+ this.json = util.parse(jsonText);
+ };
+
+ /**
+ * Get stringified JSON contents from the editor
+ * @returns {String} jsonText
+ */
+ JSONEditor.prototype.getText = function () {
+ return JSON.stringify(this.json);
+ };
+
+ /**
+ * Set a field name for the root node.
+ * @param {String | undefined} name
+ */
+ JSONEditor.prototype.setName = function (name) {
+ if (!this.options) {
+ this.options = {};
+ }
+ this.options.name = name;
+ };
+
+ /**
+ * Get the field name for the root node.
+ * @return {String | undefined} name
+ */
+ JSONEditor.prototype.getName = function () {
+ return this.options && this.options.name;
+ };
+
+ /**
+ * Change the mode of the editor.
+ * JSONEditor will be extended with all methods needed for the chosen mode.
+ * @param {String} mode Available modes: 'tree' (default), 'view', 'form',
+ * 'text', and 'code'.
+ */
+ JSONEditor.prototype.setMode = function (mode) {
+ var container = this.container,
+ options = util.extend({}, this.options),
+ data,
+ name;
+
+ options.mode = mode;
+ var config = JSONEditor.modes[mode];
+ if (config) {
+ try {
+ if (config.data == 'text') {
+ // text
+ name = this.getName();
+ data = this.getText();
+
+ this._delete();
+ util.clear(this);
+ util.extend(this, config.editor.prototype);
+ this._create(container, options);
+
+ this.setName(name);
+ this.setText(data);
+ }
+ else {
+ // json
+ name = this.getName();
+ data = this.get();
+
+ this._delete();
+ util.clear(this);
+ util.extend(this, config.editor.prototype);
+ this._create(container, options);
+
+ this.setName(name);
+ this.set(data);
+ }
+
+ if (typeof config.load === 'function') {
+ try {
+ config.load.call(this);
+ }
+ catch (err) {}
+ }
+ }
+ catch (err) {
+ this._onError(err);
+ }
+ }
+ else {
+ throw new Error('Unknown mode "' + options.mode + '"');
+ }
+ };
+
+ /**
+ * Throw an error. If an error callback is configured in options.error, this
+ * callback will be invoked. Else, a regular error is thrown.
+ * @param {Error} err
+ * @private
+ */
+ JSONEditor.prototype._onError = function(err) {
+ // TODO: onError is deprecated since version 2.2.0. cleanup some day
+ if (typeof this.onError === 'function') {
+ util.log('WARNING: JSONEditor.onError is deprecated. ' +
+ 'Use options.error instead.');
+ this.onError(err);
+ }
+
+ if (this.options && typeof this.options.error === 'function') {
+ this.options.error(err);
+ }
+ else {
+ throw err;
+ }
+ };
+
+ /**
+ * Register modes for the JSON Editor
+ * TODO: describe the mode format
+ * @param {Object} modes An object with the mode names as keys, and an object
+ * defining the mode as value
+ */
+ JSONEditor.registerModes = function (modes) {
+ for (var mode in modes) {
+ if (modes.hasOwnProperty(mode)) {
+ if (mode in JSONEditor.modes) {
+ throw new Error('Mode "' + mode + '" already registered');
+ }
+
+ JSONEditor.modes[mode] = modes[mode];
+ }
+ }
+ };
+
+ // register TreeEditor and TextEditor
+ JSONEditor.registerModes(TreeEditor.modes);
+ JSONEditor.registerModes(TextEditor.modes);
+
+ return JSONEditor;
+ }.apply(null, __WEBPACK_AMD_DEFINE_ARRAY__)), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+/***/ },
+/* 1 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(4), __webpack_require__(5), __webpack_require__(6), __webpack_require__(7), __webpack_require__(8), __webpack_require__(3)], __WEBPACK_AMD_DEFINE_RESULT__ = (function (Highlighter, History, SearchBox, Node, modebox, util) {
+
+ /**
+ * @constructor TreeEditor
+ * @param {Element} container Container element
+ * @param {Object} [options] Object with options. available options:
+ * {String} mode Editor mode. Available values:
+ * 'tree' (default), 'view',
+ * and 'form'.
+ * {Boolean} search Enable search box.
+ * True by default
+ * {Boolean} history Enable history (undo/redo).
+ * True by default
+ * {function} change Callback method, triggered
+ * on change of contents
+ * {String} name Field name for the root node.
+ * @param {Object | undefined} json JSON object
+ */
+ function TreeEditor(container, options, json) {
+ if (!(this instanceof TreeEditor)) {
+ throw new Error('TreeEditor constructor called without "new".');
+ }
+
+ this._create(container, options, json);
+ }
+
+ /**
+ * Create the TreeEditor
+ * @param {Element} container Container element
+ * @param {Object} [options] See description in constructor
+ * @param {Object | undefined} json JSON object
+ * @private
+ */
+ TreeEditor.prototype._create = function (container, options, json) {
+ if (!container) {
+ throw new Error('No container element provided.');
+ }
+ this.container = container;
+ this.dom = {};
+ this.highlighter = new Highlighter();
+ this.selection = undefined; // will hold the last input selection
+
+ this._setOptions(options);
+
+ if (this.options.history && !this.mode.view) {
+ this.history = new History(this);
+ }
+
+ this._createFrame();
+ this._createTable();
+
+ this.set(json || {});
+ };
+
+ /**
+ * Detach the editor from the DOM
+ * @private
+ */
+ TreeEditor.prototype._delete = function () {
+ if (this.frame && this.container && this.frame.parentNode == this.container) {
+ this.container.removeChild(this.frame);
+ }
+ };
+
+ /**
+ * Initialize and set default options
+ * @param {Object} [options] See description in constructor
+ * @private
+ */
+ TreeEditor.prototype._setOptions = function (options) {
+ this.options = {
+ search: true,
+ history: true,
+ mode: 'tree',
+ name: undefined // field name of root node
+ };
+
+ // copy all options
+ if (options) {
+ for (var prop in options) {
+ if (options.hasOwnProperty(prop)) {
+ this.options[prop] = options[prop];
+ }
+ }
+ }
+
+ // interpret the mode options
+ this.mode = {
+ edit: (this.options.mode != 'view' && this.options.mode != 'form'),
+ view: (this.options.mode == 'view'),
+ form: (this.options.mode == 'form')
+ };
+ };
+
+ // node currently being edited
+ TreeEditor.focusNode = undefined;
+
+ /**
+ * Set JSON object in editor
+ * @param {Object | undefined} json JSON data
+ * @param {String} [name] Optional field name for the root node.
+ * Can also be set using setName(name).
+ */
+ TreeEditor.prototype.set = function (json, name) {
+ // adjust field name for root node
+ if (name) {
+ // TODO: deprecated since version 2.2.0. Cleanup some day.
+ util.log('Warning: second parameter "name" is deprecated. ' +
+ 'Use setName(name) instead.');
+ this.options.name = name;
+ }
+
+ // verify if json is valid JSON, ignore when a function
+ if (json instanceof Function || (json === undefined)) {
+ this.clear();
+ }
+ else {
+ this.content.removeChild(this.table); // Take the table offline
+
+ // replace the root node
+ var params = {
+ 'field': this.options.name,
+ 'value': json
+ };
+ var node = new Node(this, params);
+ this._setRoot(node);
+
+ // expand
+ var recurse = false;
+ this.node.expand(recurse);
+
+ this.content.appendChild(this.table); // Put the table online again
+ }
+
+ // TODO: maintain history, store last state and previous document
+ if (this.history) {
+ this.history.clear();
+ }
+ };
+
+ /**
+ * Get JSON object from editor
+ * @return {Object | undefined} json
+ */
+ TreeEditor.prototype.get = function () {
+ // remove focus from currently edited node
+ if (TreeEditor.focusNode) {
+ TreeEditor.focusNode.blur();
+ }
+
+ if (this.node) {
+ return this.node.getValue();
+ }
+ else {
+ return undefined;
+ }
+ };
+
+ /**
+ * Get the text contents of the TreeEditor
+ * @return {String} jsonText
+ */
+ TreeEditor.prototype.getText = function() {
+ return JSON.stringify(this.get());
+ };
+
+ /**
+ * Set the text contents of the TreeEditor
+ * @param {String} jsonText
+ */
+ TreeEditor.prototype.setText = function(jsonText) {
+ this.set(util.parse(jsonText));
+ };
+
+ /**
+ * Set a field name for the root node.
+ * @param {String | undefined} name
+ */
+ TreeEditor.prototype.setName = function (name) {
+ this.options.name = name;
+ if (this.node) {
+ this.node.updateField(this.options.name);
+ }
+ };
+
+ /**
+ * Get the field name for the root node.
+ * @return {String | undefined} name
+ */
+ TreeEditor.prototype.getName = function () {
+ return this.options.name;
+ };
+
+ /**
+ * Remove the root node from the editor
+ */
+ TreeEditor.prototype.clear = function () {
+ if (this.node) {
+ this.node.collapse();
+ this.tbody.removeChild(this.node.getDom());
+ delete this.node;
+ }
+ };
+
+ /**
+ * Set the root node for the json editor
+ * @param {Node} node
+ * @private
+ */
+ TreeEditor.prototype._setRoot = function (node) {
+ this.clear();
+
+ this.node = node;
+
+ // append to the dom
+ this.tbody.appendChild(node.getDom());
+ };
+
+ /**
+ * Search text in all nodes
+ * The nodes will be expanded when the text is found one of its childs,
+ * else it will be collapsed. Searches are case insensitive.
+ * @param {String} text
+ * @return {Object[]} results Array with nodes containing the search results
+ * The result objects contains fields:
+ * - {Node} node,
+ * - {String} elem the dom element name where
+ * the result is found ('field' or
+ * 'value')
+ */
+ TreeEditor.prototype.search = function (text) {
+ var results;
+ if (this.node) {
+ this.content.removeChild(this.table); // Take the table offline
+ results = this.node.search(text);
+ this.content.appendChild(this.table); // Put the table online again
+ }
+ else {
+ results = [];
+ }
+
+ return results;
+ };
+
+ /**
+ * Expand all nodes
+ */
+ TreeEditor.prototype.expandAll = function () {
+ if (this.node) {
+ this.content.removeChild(this.table); // Take the table offline
+ this.node.expand();
+ this.content.appendChild(this.table); // Put the table online again
+ }
+ };
+
+ /**
+ * Collapse all nodes
+ */
+ TreeEditor.prototype.collapseAll = function () {
+ if (this.node) {
+ this.content.removeChild(this.table); // Take the table offline
+ this.node.collapse();
+ this.content.appendChild(this.table); // Put the table online again
+ }
+ };
+
+ /**
+ * The method onChange is called whenever a field or value is changed, created,
+ * deleted, duplicated, etc.
+ * @param {String} action Change action. Available values: "editField",
+ * "editValue", "changeType", "appendNode",
+ * "removeNode", "duplicateNode", "moveNode", "expand",
+ * "collapse".
+ * @param {Object} params Object containing parameters describing the change.
+ * The parameters in params depend on the action (for
+ * example for "editValue" the Node, old value, and new
+ * value are provided). params contains all information
+ * needed to undo or redo the action.
+ * @private
+ */
+ TreeEditor.prototype._onAction = function (action, params) {
+ // add an action to the history
+ if (this.history) {
+ this.history.add(action, params);
+ }
+
+ // trigger the onChange callback
+ if (this.options.change) {
+ try {
+ this.options.change();
+ }
+ catch (err) {
+ util.log('Error in change callback: ', err);
+ }
+ }
+ };
+
+ /**
+ * Start autoscrolling when given mouse position is above the top of the
+ * editor contents, or below the bottom.
+ * @param {Number} mouseY Absolute mouse position in pixels
+ */
+ TreeEditor.prototype.startAutoScroll = function (mouseY) {
+ var me = this;
+ var content = this.content;
+ var top = util.getAbsoluteTop(content);
+ var height = content.clientHeight;
+ var bottom = top + height;
+ var margin = 24;
+ var interval = 50; // ms
+
+ if ((mouseY < top + margin) && content.scrollTop > 0) {
+ this.autoScrollStep = ((top + margin) - mouseY) / 3;
+ }
+ else if (mouseY > bottom - margin &&
+ height + content.scrollTop < content.scrollHeight) {
+ this.autoScrollStep = ((bottom - margin) - mouseY) / 3;
+ }
+ else {
+ this.autoScrollStep = undefined;
+ }
+
+ if (this.autoScrollStep) {
+ if (!this.autoScrollTimer) {
+ this.autoScrollTimer = setInterval(function () {
+ if (me.autoScrollStep) {
+ content.scrollTop -= me.autoScrollStep;
+ }
+ else {
+ me.stopAutoScroll();
+ }
+ }, interval);
+ }
+ }
+ else {
+ this.stopAutoScroll();
+ }
+ };
+
+ /**
+ * Stop auto scrolling. Only applicable when scrolling
+ */
+ TreeEditor.prototype.stopAutoScroll = function () {
+ if (this.autoScrollTimer) {
+ clearTimeout(this.autoScrollTimer);
+ delete this.autoScrollTimer;
+ }
+ if (this.autoScrollStep) {
+ delete this.autoScrollStep;
+ }
+ };
+
+
+ /**
+ * Set the focus to an element in the TreeEditor, set text selection, and
+ * set scroll position.
+ * @param {Object} selection An object containing fields:
+ * {Element | undefined} dom The dom element
+ * which has focus
+ * {Range | TextRange} range A text selection
+ * {Number} scrollTop Scroll position
+ */
+ TreeEditor.prototype.setSelection = function (selection) {
+ if (!selection) {
+ return;
+ }
+
+ if ('scrollTop' in selection && this.content) {
+ // TODO: animated scroll
+ this.content.scrollTop = selection.scrollTop;
+ }
+ if (selection.range) {
+ util.setSelectionOffset(selection.range);
+ }
+ if (selection.dom) {
+ selection.dom.focus();
+ }
+ };
+
+ /**
+ * Get the current focus
+ * @return {Object} selection An object containing fields:
+ * {Element | undefined} dom The dom element
+ * which has focus
+ * {Range | TextRange} range A text selection
+ * {Number} scrollTop Scroll position
+ */
+ TreeEditor.prototype.getSelection = function () {
+ return {
+ dom: TreeEditor.domFocus,
+ scrollTop: this.content ? this.content.scrollTop : 0,
+ range: util.getSelectionOffset()
+ };
+ };
+
+ /**
+ * Adjust the scroll position such that given top position is shown at 1/4
+ * of the window height.
+ * @param {Number} top
+ * @param {function(boolean)} [callback] Callback, executed when animation is
+ * finished. The callback returns true
+ * when animation is finished, or false
+ * when not.
+ */
+ TreeEditor.prototype.scrollTo = function (top, callback) {
+ var content = this.content;
+ if (content) {
+ var editor = this;
+ // cancel any running animation
+ if (editor.animateTimeout) {
+ clearTimeout(editor.animateTimeout);
+ delete editor.animateTimeout;
+ }
+ if (editor.animateCallback) {
+ editor.animateCallback(false);
+ delete editor.animateCallback;
+ }
+
+ // calculate final scroll position
+ var height = content.clientHeight;
+ var bottom = content.scrollHeight - height;
+ var finalScrollTop = Math.min(Math.max(top - height / 4, 0), bottom);
+
+ // animate towards the new scroll position
+ var animate = function () {
+ var scrollTop = content.scrollTop;
+ var diff = (finalScrollTop - scrollTop);
+ if (Math.abs(diff) > 3) {
+ content.scrollTop += diff / 3;
+ editor.animateCallback = callback;
+ editor.animateTimeout = setTimeout(animate, 50);
+ }
+ else {
+ // finished
+ if (callback) {
+ callback(true);
+ }
+ content.scrollTop = finalScrollTop;
+ delete editor.animateTimeout;
+ delete editor.animateCallback;
+ }
+ };
+ animate();
+ }
+ else {
+ if (callback) {
+ callback(false);
+ }
+ }
+ };
+
+ /**
+ * Create main frame
+ * @private
+ */
+ TreeEditor.prototype._createFrame = function () {
+ // create the frame
+ this.frame = document.createElement('div');
+ this.frame.className = 'jsoneditor';
+ this.container.appendChild(this.frame);
+
+ // create one global event listener to handle all events from all nodes
+ var editor = this;
+ function onEvent(event) {
+ editor._onEvent(event);
+ }
+ this.frame.onclick = function (event) {
+ var target = event.target;// || event.srcElement;
+
+ onEvent(event);
+
+ // prevent default submit action of buttons when TreeEditor is located
+ // inside a form
+ if (target.nodeName == 'BUTTON') {
+ event.preventDefault();
+ }
+ };
+ this.frame.oninput = onEvent;
+ this.frame.onchange = onEvent;
+ this.frame.onkeydown = onEvent;
+ this.frame.onkeyup = onEvent;
+ this.frame.oncut = onEvent;
+ this.frame.onpaste = onEvent;
+ this.frame.onmousedown = onEvent;
+ this.frame.onmouseup = onEvent;
+ this.frame.onmouseover = onEvent;
+ this.frame.onmouseout = onEvent;
+ // Note: focus and blur events do not propagate, therefore they defined
+ // using an eventListener with useCapture=true
+ // see http://www.quirksmode.org/blog/archives/2008/04/delegating_the.html
+ util.addEventListener(this.frame, 'focus', onEvent, true);
+ util.addEventListener(this.frame, 'blur', onEvent, true);
+ this.frame.onfocusin = onEvent; // for IE
+ this.frame.onfocusout = onEvent; // for IE
+
+ // create menu
+ this.menu = document.createElement('div');
+ this.menu.className = 'menu';
+ this.frame.appendChild(this.menu);
+
+ // create expand all button
+ var expandAll = document.createElement('button');
+ expandAll.className = 'expand-all';
+ expandAll.title = 'Expand all fields';
+ expandAll.onclick = function () {
+ editor.expandAll();
+ };
+ this.menu.appendChild(expandAll);
+
+ // create expand all button
+ var collapseAll = document.createElement('button');
+ collapseAll.title = 'Collapse all fields';
+ collapseAll.className = 'collapse-all';
+ collapseAll.onclick = function () {
+ editor.collapseAll();
+ };
+ this.menu.appendChild(collapseAll);
+
+ // create undo/redo buttons
+ if (this.history) {
+ // create undo button
+ var undo = document.createElement('button');
+ undo.className = 'undo separator';
+ undo.title = 'Undo last action (Ctrl+Z)';
+ undo.onclick = function () {
+ editor._onUndo();
+ };
+ this.menu.appendChild(undo);
+ this.dom.undo = undo;
+
+ // create redo button
+ var redo = document.createElement('button');
+ redo.className = 'redo';
+ redo.title = 'Redo (Ctrl+Shift+Z)';
+ redo.onclick = function () {
+ editor._onRedo();
+ };
+ this.menu.appendChild(redo);
+ this.dom.redo = redo;
+
+ // register handler for onchange of history
+ this.history.onChange = function () {
+ undo.disabled = !editor.history.canUndo();
+ redo.disabled = !editor.history.canRedo();
+ };
+ this.history.onChange();
+ }
+
+ // create mode box
+ if (this.options && this.options.modes && this.options.modes.length) {
+ var modeBox = modebox.create(this, this.options.modes, this.options.mode);
+ this.menu.appendChild(modeBox);
+ this.dom.modeBox = modeBox;
+ }
+
+ // create search box
+ if (this.options.search) {
+ this.searchBox = new SearchBox(this, this.menu);
+ }
+ };
+
+ /**
+ * Perform an undo action
+ * @private
+ */
+ TreeEditor.prototype._onUndo = function () {
+ if (this.history) {
+ // undo last action
+ this.history.undo();
+
+ // trigger change callback
+ if (this.options.change) {
+ this.options.change();
+ }
+ }
+ };
+
+ /**
+ * Perform a redo action
+ * @private
+ */
+ TreeEditor.prototype._onRedo = function () {
+ if (this.history) {
+ // redo last action
+ this.history.redo();
+
+ // trigger change callback
+ if (this.options.change) {
+ this.options.change();
+ }
+ }
+ };
+
+ /**
+ * Event handler
+ * @param event
+ * @private
+ */
+ TreeEditor.prototype._onEvent = function (event) {
+ var target = event.target;
+
+ if (event.type == 'keydown') {
+ this._onKeyDown(event);
+ }
+
+ if (event.type == 'focus') {
+ TreeEditor.domFocus = target;
+ }
+
+ var node = Node.getNodeFromTarget(target);
+ if (node) {
+ node.onEvent(event);
+ }
+ };
+
+ /**
+ * Event handler for keydown. Handles shortcut keys
+ * @param {Event} event
+ * @private
+ */
+ TreeEditor.prototype._onKeyDown = function (event) {
+ var keynum = event.which || event.keyCode;
+ var ctrlKey = event.ctrlKey;
+ var shiftKey = event.shiftKey;
+ var handled = false;
+
+ if (keynum == 9) { // Tab or Shift+Tab
+ setTimeout(function () {
+ // select all text when moving focus to an editable div
+ util.selectContentEditable(TreeEditor.domFocus);
+ }, 0);
+ }
+
+ if (this.searchBox) {
+ if (ctrlKey && keynum == 70) { // Ctrl+F
+ this.searchBox.dom.search.focus();
+ this.searchBox.dom.search.select();
+ handled = true;
+ }
+ else if (keynum == 114 || (ctrlKey && keynum == 71)) { // F3 or Ctrl+G
+ var focus = true;
+ if (!shiftKey) {
+ // select next search result (F3 or Ctrl+G)
+ this.searchBox.next(focus);
+ }
+ else {
+ // select previous search result (Shift+F3 or Ctrl+Shift+G)
+ this.searchBox.previous(focus);
+ }
+
+ handled = true;
+ }
+ }
+
+ if (this.history) {
+ if (ctrlKey && !shiftKey && keynum == 90) { // Ctrl+Z
+ // undo
+ this._onUndo();
+ handled = true;
+ }
+ else if (ctrlKey && shiftKey && keynum == 90) { // Ctrl+Shift+Z
+ // redo
+ this._onRedo();
+ handled = true;
+ }
+ }
+
+ if (handled) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ };
+
+ /**
+ * Create main table
+ * @private
+ */
+ TreeEditor.prototype._createTable = function () {
+ var contentOuter = document.createElement('div');
+ contentOuter.className = 'outer';
+ this.contentOuter = contentOuter;
+
+ this.content = document.createElement('div');
+ this.content.className = 'tree';
+ contentOuter.appendChild(this.content);
+
+ this.table = document.createElement('table');
+ this.table.className = 'tree';
+ this.content.appendChild(this.table);
+
+ // create colgroup where the first two columns don't have a fixed
+ // width, and the edit columns do have a fixed width
+ var col;
+ this.colgroupContent = document.createElement('colgroup');
+ if (this.mode.edit) {
+ col = document.createElement('col');
+ col.width = "24px";
+ this.colgroupContent.appendChild(col);
+ }
+ col = document.createElement('col');
+ col.width = "24px";
+ this.colgroupContent.appendChild(col);
+ col = document.createElement('col');
+ this.colgroupContent.appendChild(col);
+ this.table.appendChild(this.colgroupContent);
+
+ this.tbody = document.createElement('tbody');
+ this.table.appendChild(this.tbody);
+
+ this.frame.appendChild(contentOuter);
+ };
+
+ // define modes
+ TreeEditor.modes = {
+ tree: {
+ editor: TreeEditor,
+ data: 'json'
+ },
+ view: {
+ editor: TreeEditor,
+ data: 'json'
+ },
+ form: {
+ editor: TreeEditor,
+ data: 'json'
+ }
+ };
+
+ return TreeEditor;
+ }.apply(null, __WEBPACK_AMD_DEFINE_ARRAY__)), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+
+/***/ },
+/* 2 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(8), __webpack_require__(3)], __WEBPACK_AMD_DEFINE_RESULT__ = (function (modebox, util) {
+
+ /**
+ * Create a TextEditor and attach it to given container
+ * @constructor TextEditor
+ * @param {Element} container
+ * @param {Object} [options] Object with options. available options:
+ * {String} mode Available values:
+ * "text" (default)
+ * or "code".
+ * {Number} indentation Number of indentation
+ * spaces. 2 by default.
+ * {function} change Callback method
+ * triggered on change
+ * @param {JSON | String} [json] initial contents of the formatter
+ */
+ function TextEditor(container, options, json) {
+ if (!(this instanceof TextEditor)) {
+ throw new Error('TextEditor constructor called without "new".');
+ }
+
+ this._create(container, options, json);
+ }
+
+ /**
+ * Create a TextEditor and attach it to given container
+ * @constructor TextEditor
+ * @param {Element} container
+ * @param {Object} [options] See description in constructor
+ * @param {JSON | String} [json] initial contents of the formatter
+ * @private
+ */
+ TextEditor.prototype._create = function (container, options, json) {
+ // read options
+ options = options || {};
+ this.options = options;
+ if (options.indentation) {
+ this.indentation = Number(options.indentation);
+ }
+ else {
+ this.indentation = 2; // number of spaces
+ }
+ this.mode = (options.mode == 'code') ? 'code' : 'text';
+ if (this.mode == 'code') {
+ // verify whether Ace editor is available and supported
+ if (typeof ace === 'undefined') {
+ this.mode = 'text';
+ util.log('WARNING: Cannot load code editor, Ace library not loaded. ' +
+ 'Falling back to plain text editor');
+ }
+ }
+
+ var me = this;
+ this.container = container;
+ this.dom = {};
+ this.editor = undefined; // ace code editor
+ this.textarea = undefined; // plain text editor (fallback when Ace is not available)
+
+ this.width = container.clientWidth;
+ this.height = container.clientHeight;
+
+ this.frame = document.createElement('div');
+ this.frame.className = 'jsoneditor';
+ this.frame.onclick = function (event) {
+ // prevent default submit action when TextEditor is located inside a form
+ event.preventDefault();
+ };
+
+ // create menu
+ this.menu = document.createElement('div');
+ this.menu.className = 'menu';
+ this.frame.appendChild(this.menu);
+
+ // create format button
+ var buttonFormat = document.createElement('button');
+ buttonFormat.className = 'format';
+ buttonFormat.title = 'Format JSON data, with proper indentation and line feeds';
+ this.menu.appendChild(buttonFormat);
+ buttonFormat.onclick = function () {
+ try {
+ me.format();
+ }
+ catch (err) {
+ me._onError(err);
+ }
+ };
+
+ // create compact button
+ var buttonCompact = document.createElement('button');
+ buttonCompact.className = 'compact';
+ buttonCompact.title = 'Compact JSON data, remove all whitespaces';
+ this.menu.appendChild(buttonCompact);
+ buttonCompact.onclick = function () {
+ try {
+ me.compact();
+ }
+ catch (err) {
+ me._onError(err);
+ }
+ };
+
+ // create mode box
+ if (this.options && this.options.modes && this.options.modes.length) {
+ var modeBox = modebox.create(this, this.options.modes, this.options.mode);
+ this.menu.appendChild(modeBox);
+ this.dom.modeBox = modeBox;
+ }
+
+ this.content = document.createElement('div');
+ this.content.className = 'outer';
+ this.frame.appendChild(this.content);
+
+ this.container.appendChild(this.frame);
+
+ if (this.mode == 'code') {
+ this.editorDom = document.createElement('div');
+ this.editorDom.style.height = '100%'; // TODO: move to css
+ this.editorDom.style.width = '100%'; // TODO: move to css
+ this.content.appendChild(this.editorDom);
+
+ var editor = ace.edit(this.editorDom);
+ editor.setTheme('ace/theme/jsoneditor');
+ editor.setShowPrintMargin(false);
+ editor.setFontSize(13);
+ editor.getSession().setMode('ace/mode/json');
+ editor.getSession().setTabSize(2);
+ editor.getSession().setUseSoftTabs(true);
+ editor.getSession().setUseWrapMode(true);
+ this.editor = editor;
+
+ var poweredBy = document.createElement('a');
+ poweredBy.appendChild(document.createTextNode('powered by ace'));
+ poweredBy.href = 'http://ace.ajax.org';
+ poweredBy.target = '_blank';
+ poweredBy.className = 'poweredBy';
+ poweredBy.onclick = function () {
+ // TODO: this anchor falls below the margin of the content,
+ // therefore the normal a.href does not work. We use a click event
+ // for now, but this should be fixed.
+ window.open(poweredBy.href, poweredBy.target);
+ };
+ this.menu.appendChild(poweredBy);
+
+ if (options.change) {
+ // register onchange event
+ editor.on('change', function () {
+ options.change();
+ });
+ }
+ }
+ else {
+ // load a plain text textarea
+ var textarea = document.createElement('textarea');
+ textarea.className = 'text';
+ textarea.spellcheck = false;
+ this.content.appendChild(textarea);
+ this.textarea = textarea;
+
+ if (options.change) {
+ // register onchange event
+ if (this.textarea.oninput === null) {
+ this.textarea.oninput = function () {
+ options.change();
+ }
+ }
+ else {
+ // oninput is undefined. For IE8-
+ this.textarea.onchange = function () {
+ options.change();
+ }
+ }
+ }
+ }
+
+ // load initial json object or string
+ if (typeof(json) == 'string') {
+ this.setText(json);
+ }
+ else {
+ this.set(json);
+ }
+ };
+
+ /**
+ * Detach the editor from the DOM
+ * @private
+ */
+ TextEditor.prototype._delete = function () {
+ if (this.frame && this.container && this.frame.parentNode == this.container) {
+ this.container.removeChild(this.frame);
+ }
+ };
+
+ /**
+ * Throw an error. If an error callback is configured in options.error, this
+ * callback will be invoked. Else, a regular error is thrown.
+ * @param {Error} err
+ * @private
+ */
+ TextEditor.prototype._onError = function(err) {
+ // TODO: onError is deprecated since version 2.2.0. cleanup some day
+ if (typeof this.onError === 'function') {
+ util.log('WARNING: JSONEditor.onError is deprecated. ' +
+ 'Use options.error instead.');
+ this.onError(err);
+ }
+
+ if (this.options && typeof this.options.error === 'function') {
+ this.options.error(err);
+ }
+ else {
+ throw err;
+ }
+ };
+
+ /**
+ * Compact the code in the formatter
+ */
+ TextEditor.prototype.compact = function () {
+ var json = util.parse(this.getText());
+ this.setText(JSON.stringify(json));
+ };
+
+ /**
+ * Format the code in the formatter
+ */
+ TextEditor.prototype.format = function () {
+ var json = util.parse(this.getText());
+ this.setText(JSON.stringify(json, null, this.indentation));
+ };
+
+ /**
+ * Set focus to the formatter
+ */
+ TextEditor.prototype.focus = function () {
+ if (this.textarea) {
+ this.textarea.focus();
+ }
+ if (this.editor) {
+ this.editor.focus();
+ }
+ };
+
+ /**
+ * Resize the formatter
+ */
+ TextEditor.prototype.resize = function () {
+ if (this.editor) {
+ var force = false;
+ this.editor.resize(force);
+ }
+ };
+
+ /**
+ * Set json data in the formatter
+ * @param {Object} json
+ */
+ TextEditor.prototype.set = function(json) {
+ this.setText(JSON.stringify(json, null, this.indentation));
+ };
+
+ /**
+ * Get json data from the formatter
+ * @return {Object} json
+ */
+ TextEditor.prototype.get = function() {
+ return util.parse(this.getText());
+ };
+
+ /**
+ * Get the text contents of the TextEditor
+ * @return {String} jsonText
+ */
+ TextEditor.prototype.getText = function() {
+ if (this.textarea) {
+ return this.textarea.value;
+ }
+ if (this.editor) {
+ return this.editor.getValue();
+ }
+ return '';
+ };
+
+ /**
+ * Set the text contents of the TextEditor
+ * @param {String} jsonText
+ */
+ TextEditor.prototype.setText = function(jsonText) {
+ if (this.textarea) {
+ this.textarea.value = jsonText;
+ }
+ if (this.editor) {
+ this.editor.setValue(jsonText, -1);
+ }
+ };
+
+ // define modes
+ TextEditor.modes = {
+ text: {
+ editor: TextEditor,
+ data: 'text',
+ load: TextEditor.prototype.format
+ },
+ code: {
+ editor: TextEditor,
+ data: 'text',
+ load: TextEditor.prototype.format
+ }
+ };
+
+ return TextEditor;
+ }.apply(null, __WEBPACK_AMD_DEFINE_ARRAY__)), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+
+/***/ },
+/* 3 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_RESULT__ = (function () {
+
+ // create namespace
+ var util = {};
+
+ /**
+ * Parse JSON using the parser built-in in the browser.
+ * On exception, the jsonString is validated and a detailed error is thrown.
+ * @param {String} jsonString
+ */
+ util.parse = function parse(jsonString) {
+ try {
+ return JSON.parse(jsonString);
+ }
+ catch (err) {
+ // try to throw a more detailed error message using validate
+ util.validate(jsonString);
+ throw err;
+ }
+ };
+
+ /**
+ * Validate a string containing a JSON object
+ * This method uses JSONLint to validate the String. If JSONLint is not
+ * available, the built-in JSON parser of the browser is used.
+ * @param {String} jsonString String with an (invalid) JSON object
+ * @throws Error
+ */
+ util.validate = function validate(jsonString) {
+ if (typeof(jsonlint) != 'undefined') {
+ jsonlint.parse(jsonString);
+ }
+ else {
+ JSON.parse(jsonString);
+ }
+ };
+
+ /**
+ * Extend object a with the properties of object b
+ * @param {Object} a
+ * @param {Object} b
+ * @return {Object} a
+ */
+ util.extend = function extend(a, b) {
+ for (var prop in b) {
+ if (b.hasOwnProperty(prop)) {
+ a[prop] = b[prop];
+ }
+ }
+ return a;
+ };
+
+ /**
+ * Remove all properties from object a
+ * @param {Object} a
+ * @return {Object} a
+ */
+ util.clear = function clear (a) {
+ for (var prop in a) {
+ if (a.hasOwnProperty(prop)) {
+ delete a[prop];
+ }
+ }
+ return a;
+ };
+
+ /**
+ * Output text to the console, if console is available
+ * @param {...*} args
+ */
+ util.log = function log (args) {
+ if (typeof console !== 'undefined' && typeof console.log === 'function') {
+ console.log.apply(console, arguments);
+ }
+ };
+
+ /**
+ * Get the type of an object
+ * @param {*} object
+ * @return {String} type
+ */
+ util.type = function type (object) {
+ if (object === null) {
+ return 'null';
+ }
+ if (object === undefined) {
+ return 'undefined';
+ }
+ if ((object instanceof Number) || (typeof object === 'number')) {
+ return 'number';
+ }
+ if ((object instanceof String) || (typeof object === 'string')) {
+ return 'string';
+ }
+ if ((object instanceof Boolean) || (typeof object === 'boolean')) {
+ return 'boolean';
+ }
+ if ((object instanceof RegExp) || (typeof object === 'regexp')) {
+ return 'regexp';
+ }
+ if (Array.isArray(object)) {
+ return 'array';
+ }
+
+ return 'object';
+ };
+
+ /**
+ * Test whether a text contains a url (matches when a string starts
+ * with 'http://*' or 'https://*' and has no whitespace characters)
+ * @param {String} text
+ */
+ var isUrlRegex = /^https?:\/\/\S+$/;
+ util.isUrl = function isUrl (text) {
+ return (typeof text == 'string' || text instanceof String) &&
+ isUrlRegex.test(text);
+ };
+
+ /**
+ * Retrieve the absolute left value of a DOM element
+ * @param {Element} elem A dom element, for example a div
+ * @return {Number} left The absolute left position of this element
+ * in the browser page.
+ */
+ util.getAbsoluteLeft = function getAbsoluteLeft(elem) {
+ var rect = elem.getBoundingClientRect();
+ return rect.left + window.pageXOffset || document.scrollLeft || 0;
+ };
+
+ /**
+ * Retrieve the absolute top value of a DOM element
+ * @param {Element} elem A dom element, for example a div
+ * @return {Number} top The absolute top position of this element
+ * in the browser page.
+ */
+ util.getAbsoluteTop = function getAbsoluteTop(elem) {
+ var rect = elem.getBoundingClientRect();
+ return rect.top + window.pageYOffset || document.scrollTop || 0;
+ };
+
+ /**
+ * add a className to the given elements style
+ * @param {Element} elem
+ * @param {String} className
+ */
+ util.addClassName = function addClassName(elem, className) {
+ var classes = elem.className.split(' ');
+ if (classes.indexOf(className) == -1) {
+ classes.push(className); // add the class to the array
+ elem.className = classes.join(' ');
+ }
+ };
+
+ /**
+ * add a className to the given elements style
+ * @param {Element} elem
+ * @param {String} className
+ */
+ util.removeClassName = function removeClassName(elem, className) {
+ var classes = elem.className.split(' ');
+ var index = classes.indexOf(className);
+ if (index != -1) {
+ classes.splice(index, 1); // remove the class from the array
+ elem.className = classes.join(' ');
+ }
+ };
+
+ /**
+ * Strip the formatting from the contents of a div
+ * the formatting from the div itself is not stripped, only from its childs.
+ * @param {Element} divElement
+ */
+ util.stripFormatting = function stripFormatting(divElement) {
+ var childs = divElement.childNodes;
+ for (var i = 0, iMax = childs.length; i < iMax; i++) {
+ var child = childs[i];
+
+ // remove the style
+ if (child.style) {
+ // TODO: test if child.attributes does contain style
+ child.removeAttribute('style');
+ }
+
+ // remove all attributes
+ var attributes = child.attributes;
+ if (attributes) {
+ for (var j = attributes.length - 1; j >= 0; j--) {
+ var attribute = attributes[j];
+ if (attribute.specified == true) {
+ child.removeAttribute(attribute.name);
+ }
+ }
+ }
+
+ // recursively strip childs
+ util.stripFormatting(child);
+ }
+ };
+
+ /**
+ * Set focus to the end of an editable div
+ * code from Nico Burns
+ * http://stackoverflow.com/users/140293/nico-burns
+ * http://stackoverflow.com/questions/1125292/how-to-move-cursor-to-end-of-contenteditable-entity
+ * @param {Element} contentEditableElement A content editable div
+ */
+ util.setEndOfContentEditable = function setEndOfContentEditable(contentEditableElement) {
+ var range, selection;
+ if(document.createRange) {
+ range = document.createRange();//Create a range (a range is a like the selection but invisible)
+ range.selectNodeContents(contentEditableElement);//Select the entire contents of the element with the range
+ range.collapse(false);//collapse the range to the end point. false means collapse to end rather than the start
+ selection = window.getSelection();//get the selection object (allows you to change selection)
+ selection.removeAllRanges();//remove any selections already made
+ selection.addRange(range);//make the range you have just created the visible selection
+ }
+ };
+
+ /**
+ * Select all text of a content editable div.
+ * http://stackoverflow.com/a/3806004/1262753
+ * @param {Element} contentEditableElement A content editable div
+ */
+ util.selectContentEditable = function selectContentEditable(contentEditableElement) {
+ if (!contentEditableElement || contentEditableElement.nodeName != 'DIV') {
+ return;
+ }
+
+ var sel, range;
+ if (window.getSelection && document.createRange) {
+ range = document.createRange();
+ range.selectNodeContents(contentEditableElement);
+ sel = window.getSelection();
+ sel.removeAllRanges();
+ sel.addRange(range);
+ }
+ };
+
+ /**
+ * Get text selection
+ * http://stackoverflow.com/questions/4687808/contenteditable-selected-text-save-and-restore
+ * @return {Range | TextRange | null} range
+ */
+ util.getSelection = function getSelection() {
+ if (window.getSelection) {
+ var sel = window.getSelection();
+ if (sel.getRangeAt && sel.rangeCount) {
+ return sel.getRangeAt(0);
+ }
+ }
+ return null;
+ };
+
+ /**
+ * Set text selection
+ * http://stackoverflow.com/questions/4687808/contenteditable-selected-text-save-and-restore
+ * @param {Range | TextRange | null} range
+ */
+ util.setSelection = function setSelection(range) {
+ if (range) {
+ if (window.getSelection) {
+ var sel = window.getSelection();
+ sel.removeAllRanges();
+ sel.addRange(range);
+ }
+ }
+ };
+
+ /**
+ * Get selected text range
+ * @return {Object} params object containing parameters:
+ * {Number} startOffset
+ * {Number} endOffset
+ * {Element} container HTML element holding the
+ * selected text element
+ * Returns null if no text selection is found
+ */
+ util.getSelectionOffset = function getSelectionOffset() {
+ var range = util.getSelection();
+
+ if (range && 'startOffset' in range && 'endOffset' in range &&
+ range.startContainer && (range.startContainer == range.endContainer)) {
+ return {
+ startOffset: range.startOffset,
+ endOffset: range.endOffset,
+ container: range.startContainer.parentNode
+ };
+ }
+
+ return null;
+ };
+
+ /**
+ * Set selected text range in given element
+ * @param {Object} params An object containing:
+ * {Element} container
+ * {Number} startOffset
+ * {Number} endOffset
+ */
+ util.setSelectionOffset = function setSelectionOffset(params) {
+ if (document.createRange && window.getSelection) {
+ var selection = window.getSelection();
+ if(selection) {
+ var range = document.createRange();
+ // TODO: do not suppose that the first child of the container is a textnode,
+ // but recursively find the textnodes
+ range.setStart(params.container.firstChild, params.startOffset);
+ range.setEnd(params.container.firstChild, params.endOffset);
+
+ util.setSelection(range);
+ }
+ }
+ };
+
+ /**
+ * Get the inner text of an HTML element (for example a div element)
+ * @param {Element} element
+ * @param {Object} [buffer]
+ * @return {String} innerText
+ */
+ util.getInnerText = function getInnerText(element, buffer) {
+ var first = (buffer == undefined);
+ if (first) {
+ buffer = {
+ 'text': '',
+ 'flush': function () {
+ var text = this.text;
+ this.text = '';
+ return text;
+ },
+ 'set': function (text) {
+ this.text = text;
+ }
+ };
+ }
+
+ // text node
+ if (element.nodeValue) {
+ return buffer.flush() + element.nodeValue;
+ }
+
+ // divs or other HTML elements
+ if (element.hasChildNodes()) {
+ var childNodes = element.childNodes;
+ var innerText = '';
+
+ for (var i = 0, iMax = childNodes.length; i < iMax; i++) {
+ var child = childNodes[i];
+
+ if (child.nodeName == 'DIV' || child.nodeName == 'P') {
+ var prevChild = childNodes[i - 1];
+ var prevName = prevChild ? prevChild.nodeName : undefined;
+ if (prevName && prevName != 'DIV' && prevName != 'P' && prevName != 'BR') {
+ innerText += '\n';
+ buffer.flush();
+ }
+ innerText += util.getInnerText(child, buffer);
+ buffer.set('\n');
+ }
+ else if (child.nodeName == 'BR') {
+ innerText += buffer.flush();
+ buffer.set('\n');
+ }
+ else {
+ innerText += util.getInnerText(child, buffer);
+ }
+ }
+
+ return innerText;
+ }
+ else {
+ if (element.nodeName == 'P' && util.getInternetExplorerVersion() != -1) {
+ // On Internet Explorer, a
with hasChildNodes()==false is
+ // rendered with a new line. Note that a
with
+ // hasChildNodes()==true is rendered without a new line
+ // Other browsers always ensure there is a inside the
,
+ // and if not, the
does not render a new line
+ return buffer.flush();
+ }
+ }
+
+ // br or unknown
+ return '';
+ };
+
+ /**
+ * Returns the version of Internet Explorer or a -1
+ * (indicating the use of another browser).
+ * Source: http://msdn.microsoft.com/en-us/library/ms537509(v=vs.85).aspx
+ * @return {Number} Internet Explorer version, or -1 in case of an other browser
+ */
+ util.getInternetExplorerVersion = function getInternetExplorerVersion() {
+ if (_ieVersion == -1) {
+ var rv = -1; // Return value assumes failure.
+ if (navigator.appName == 'Microsoft Internet Explorer')
+ {
+ var ua = navigator.userAgent;
+ var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
+ if (re.exec(ua) != null) {
+ rv = parseFloat( RegExp.$1 );
+ }
+ }
+
+ _ieVersion = rv;
+ }
+
+ return _ieVersion;
+ };
+
+ /**
+ * Test whether the current browser is Firefox
+ * @returns {boolean} isFirefox
+ */
+ util.isFirefox = function isFirefox () {
+ return (navigator.userAgent.indexOf("Firefox") != -1);
+ };
+
+ /**
+ * cached internet explorer version
+ * @type {Number}
+ * @private
+ */
+ var _ieVersion = -1;
+
+ /**
+ * Add and event listener. Works for all browsers
+ * @param {Element} element An html element
+ * @param {string} action The action, for example "click",
+ * without the prefix "on"
+ * @param {function} listener The callback function to be executed
+ * @param {boolean} [useCapture] false by default
+ * @return {function} the created event listener
+ */
+ util.addEventListener = function addEventListener(element, action, listener, useCapture) {
+ if (element.addEventListener) {
+ if (useCapture === undefined)
+ useCapture = false;
+
+ if (action === "mousewheel" && util.isFirefox()) {
+ action = "DOMMouseScroll"; // For Firefox
+ }
+
+ element.addEventListener(action, listener, useCapture);
+ return listener;
+ } else if (element.attachEvent) {
+ // Old IE browsers
+ var f = function () {
+ return listener.call(element, window.event);
+ };
+ element.attachEvent("on" + action, f);
+ return f;
+ }
+ };
+
+ /**
+ * Remove an event listener from an element
+ * @param {Element} element An html dom element
+ * @param {string} action The name of the event, for example "mousedown"
+ * @param {function} listener The listener function
+ * @param {boolean} [useCapture] false by default
+ */
+ util.removeEventListener = function removeEventListener(element, action, listener, useCapture) {
+ if (element.removeEventListener) {
+ if (useCapture === undefined)
+ useCapture = false;
+
+ if (action === "mousewheel" && util.isFirefox()) {
+ action = "DOMMouseScroll"; // For Firefox
+ }
+
+ element.removeEventListener(action, listener, useCapture);
+ } else if (element.detachEvent) {
+ // Old IE browsers
+ element.detachEvent("on" + action, listener);
+ }
+ };
+
+ return util;
+ }.call(exports, __webpack_require__, exports, module)), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+/***/ },
+/* 4 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_RESULT__ = (function () {
+
+ /**
+ * The highlighter can highlight/unhighlight a node, and
+ * animate the visibility of a context menu.
+ * @constructor Highlighter
+ */
+ function Highlighter () {
+ this.locked = false;
+ }
+
+ /**
+ * Hightlight given node and its childs
+ * @param {Node} node
+ */
+ Highlighter.prototype.highlight = function (node) {
+ if (this.locked) {
+ return;
+ }
+
+ if (this.node != node) {
+ // unhighlight current node
+ if (this.node) {
+ this.node.setHighlight(false);
+ }
+
+ // highlight new node
+ this.node = node;
+ this.node.setHighlight(true);
+ }
+
+ // cancel any current timeout
+ this._cancelUnhighlight();
+ };
+
+ /**
+ * Unhighlight currently highlighted node.
+ * Will be done after a delay
+ */
+ Highlighter.prototype.unhighlight = function () {
+ if (this.locked) {
+ return;
+ }
+
+ var me = this;
+ if (this.node) {
+ this._cancelUnhighlight();
+
+ // do the unhighlighting after a small delay, to prevent re-highlighting
+ // the same node when moving from the drag-icon to the contextmenu-icon
+ // or vice versa.
+ this.unhighlightTimer = setTimeout(function () {
+ me.node.setHighlight(false);
+ me.node = undefined;
+ me.unhighlightTimer = undefined;
+ }, 0);
+ }
+ };
+
+ /**
+ * Cancel an unhighlight action (if before the timeout of the unhighlight action)
+ * @private
+ */
+ Highlighter.prototype._cancelUnhighlight = function () {
+ if (this.unhighlightTimer) {
+ clearTimeout(this.unhighlightTimer);
+ this.unhighlightTimer = undefined;
+ }
+ };
+
+ /**
+ * Lock highlighting or unhighlighting nodes.
+ * methods highlight and unhighlight do not work while locked.
+ */
+ Highlighter.prototype.lock = function () {
+ this.locked = true;
+ };
+
+ /**
+ * Unlock highlighting or unhighlighting nodes
+ */
+ Highlighter.prototype.unlock = function () {
+ this.locked = false;
+ };
+
+ return Highlighter;
+ }.call(exports, __webpack_require__, exports, module)), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+/***/ },
+/* 5 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(3)], __WEBPACK_AMD_DEFINE_RESULT__ = (function (util) {
+
+ /**
+ * @constructor History
+ * Store action history, enables undo and redo
+ * @param {JSONEditor} editor
+ */
+ function History (editor) {
+ this.editor = editor;
+ this.clear();
+
+ // map with all supported actions
+ this.actions = {
+ 'editField': {
+ 'undo': function (params) {
+ params.node.updateField(params.oldValue);
+ },
+ 'redo': function (params) {
+ params.node.updateField(params.newValue);
+ }
+ },
+ 'editValue': {
+ 'undo': function (params) {
+ params.node.updateValue(params.oldValue);
+ },
+ 'redo': function (params) {
+ params.node.updateValue(params.newValue);
+ }
+ },
+ 'appendNode': {
+ 'undo': function (params) {
+ params.parent.removeChild(params.node);
+ },
+ 'redo': function (params) {
+ params.parent.appendChild(params.node);
+ }
+ },
+ 'insertBeforeNode': {
+ 'undo': function (params) {
+ params.parent.removeChild(params.node);
+ },
+ 'redo': function (params) {
+ params.parent.insertBefore(params.node, params.beforeNode);
+ }
+ },
+ 'insertAfterNode': {
+ 'undo': function (params) {
+ params.parent.removeChild(params.node);
+ },
+ 'redo': function (params) {
+ params.parent.insertAfter(params.node, params.afterNode);
+ }
+ },
+ 'removeNode': {
+ 'undo': function (params) {
+ var parent = params.parent;
+ var beforeNode = parent.childs[params.index] || parent.append;
+ parent.insertBefore(params.node, beforeNode);
+ },
+ 'redo': function (params) {
+ params.parent.removeChild(params.node);
+ }
+ },
+ 'duplicateNode': {
+ 'undo': function (params) {
+ params.parent.removeChild(params.clone);
+ },
+ 'redo': function (params) {
+ params.parent.insertAfter(params.clone, params.node);
+ }
+ },
+ 'changeType': {
+ 'undo': function (params) {
+ params.node.changeType(params.oldType);
+ },
+ 'redo': function (params) {
+ params.node.changeType(params.newType);
+ }
+ },
+ 'moveNode': {
+ 'undo': function (params) {
+ params.startParent.moveTo(params.node, params.startIndex);
+ },
+ 'redo': function (params) {
+ params.endParent.moveTo(params.node, params.endIndex);
+ }
+ },
+ 'sort': {
+ 'undo': function (params) {
+ var node = params.node;
+ node.hideChilds();
+ node.sort = params.oldSort;
+ node.childs = params.oldChilds;
+ node.showChilds();
+ },
+ 'redo': function (params) {
+ var node = params.node;
+ node.hideChilds();
+ node.sort = params.newSort;
+ node.childs = params.newChilds;
+ node.showChilds();
+ }
+ }
+
+ // TODO: restore the original caret position and selection with each undo
+ // TODO: implement history for actions "expand", "collapse", "scroll", "setDocument"
+ };
+ }
+
+ /**
+ * The method onChange is executed when the History is changed, and can
+ * be overloaded.
+ */
+ History.prototype.onChange = function () {};
+
+ /**
+ * Add a new action to the history
+ * @param {String} action The executed action. Available actions: "editField",
+ * "editValue", "changeType", "appendNode",
+ * "removeNode", "duplicateNode", "moveNode"
+ * @param {Object} params Object containing parameters describing the change.
+ * The parameters in params depend on the action (for
+ * example for "editValue" the Node, old value, and new
+ * value are provided). params contains all information
+ * needed to undo or redo the action.
+ */
+ History.prototype.add = function (action, params) {
+ this.index++;
+ this.history[this.index] = {
+ 'action': action,
+ 'params': params,
+ 'timestamp': new Date()
+ };
+
+ // remove redo actions which are invalid now
+ if (this.index < this.history.length - 1) {
+ this.history.splice(this.index + 1, this.history.length - this.index - 1);
+ }
+
+ // fire onchange event
+ this.onChange();
+ };
+
+ /**
+ * Clear history
+ */
+ History.prototype.clear = function () {
+ this.history = [];
+ this.index = -1;
+
+ // fire onchange event
+ this.onChange();
+ };
+
+ /**
+ * Check if there is an action available for undo
+ * @return {Boolean} canUndo
+ */
+ History.prototype.canUndo = function () {
+ return (this.index >= 0);
+ };
+
+ /**
+ * Check if there is an action available for redo
+ * @return {Boolean} canRedo
+ */
+ History.prototype.canRedo = function () {
+ return (this.index < this.history.length - 1);
+ };
+
+ /**
+ * Undo the last action
+ */
+ History.prototype.undo = function () {
+ if (this.canUndo()) {
+ var obj = this.history[this.index];
+ if (obj) {
+ var action = this.actions[obj.action];
+ if (action && action.undo) {
+ action.undo(obj.params);
+ if (obj.params.oldSelection) {
+ this.editor.setSelection(obj.params.oldSelection);
+ }
+ }
+ else {
+ util.log('Error: unknown action "' + obj.action + '"');
+ }
+ }
+ this.index--;
+
+ // fire onchange event
+ this.onChange();
+ }
+ };
+
+ /**
+ * Redo the last action
+ */
+ History.prototype.redo = function () {
+ if (this.canRedo()) {
+ this.index++;
+
+ var obj = this.history[this.index];
+ if (obj) {
+ var action = this.actions[obj.action];
+ if (action && action.redo) {
+ action.redo(obj.params);
+ if (obj.params.newSelection) {
+ this.editor.setSelection(obj.params.newSelection);
+ }
+ }
+ else {
+ util.log('Error: unknown action "' + obj.action + '"');
+ }
+ }
+
+ // fire onchange event
+ this.onChange();
+ }
+ };
+
+ return History;
+ }.apply(null, __WEBPACK_AMD_DEFINE_ARRAY__)), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+
+/***/ },
+/* 6 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_RESULT__ = (function () {
+
+ /**
+ * @constructor SearchBox
+ * Create a search box in given HTML container
+ * @param {JSONEditor} editor The JSON Editor to attach to
+ * @param {Element} container HTML container element of where to
+ * create the search box
+ */
+ function SearchBox (editor, container) {
+ var searchBox = this;
+
+ this.editor = editor;
+ this.timeout = undefined;
+ this.delay = 200; // ms
+ this.lastText = undefined;
+
+ this.dom = {};
+ this.dom.container = container;
+
+ var table = document.createElement('table');
+ this.dom.table = table;
+ table.className = 'search';
+ container.appendChild(table);
+ var tbody = document.createElement('tbody');
+ this.dom.tbody = tbody;
+ table.appendChild(tbody);
+ var tr = document.createElement('tr');
+ tbody.appendChild(tr);
+
+ var td = document.createElement('td');
+ tr.appendChild(td);
+ var results = document.createElement('div');
+ this.dom.results = results;
+ results.className = 'results';
+ td.appendChild(results);
+
+ td = document.createElement('td');
+ tr.appendChild(td);
+ var divInput = document.createElement('div');
+ this.dom.input = divInput;
+ divInput.className = 'frame';
+ divInput.title = 'Search fields and values';
+ td.appendChild(divInput);
+
+ // table to contain the text input and search button
+ var tableInput = document.createElement('table');
+ divInput.appendChild(tableInput);
+ var tbodySearch = document.createElement('tbody');
+ tableInput.appendChild(tbodySearch);
+ tr = document.createElement('tr');
+ tbodySearch.appendChild(tr);
+
+ var refreshSearch = document.createElement('button');
+ refreshSearch.className = 'refresh';
+ td = document.createElement('td');
+ td.appendChild(refreshSearch);
+ tr.appendChild(td);
+
+ var search = document.createElement('input');
+ this.dom.search = search;
+ search.oninput = function (event) {
+ searchBox._onDelayedSearch(event);
+ };
+ search.onchange = function (event) { // For IE 9
+ searchBox._onSearch(event);
+ };
+ search.onkeydown = function (event) {
+ searchBox._onKeyDown(event);
+ };
+ search.onkeyup = function (event) {
+ searchBox._onKeyUp(event);
+ };
+ refreshSearch.onclick = function (event) {
+ search.select();
+ };
+
+ // TODO: ESC in FF restores the last input, is a FF bug, https://bugzilla.mozilla.org/show_bug.cgi?id=598819
+ td = document.createElement('td');
+ td.appendChild(search);
+ tr.appendChild(td);
+
+ var searchNext = document.createElement('button');
+ searchNext.title = 'Next result (Enter)';
+ searchNext.className = 'next';
+ searchNext.onclick = function () {
+ searchBox.next();
+ };
+ td = document.createElement('td');
+ td.appendChild(searchNext);
+ tr.appendChild(td);
+
+ var searchPrevious = document.createElement('button');
+ searchPrevious.title = 'Previous result (Shift+Enter)';
+ searchPrevious.className = 'previous';
+ searchPrevious.onclick = function () {
+ searchBox.previous();
+ };
+ td = document.createElement('td');
+ td.appendChild(searchPrevious);
+ tr.appendChild(td);
+ }
+
+ /**
+ * Go to the next search result
+ * @param {boolean} [focus] If true, focus will be set to the next result
+ * focus is false by default.
+ */
+ SearchBox.prototype.next = function(focus) {
+ if (this.results != undefined) {
+ var index = (this.resultIndex != undefined) ? this.resultIndex + 1 : 0;
+ if (index > this.results.length - 1) {
+ index = 0;
+ }
+ this._setActiveResult(index, focus);
+ }
+ };
+
+ /**
+ * Go to the prevous search result
+ * @param {boolean} [focus] If true, focus will be set to the next result
+ * focus is false by default.
+ */
+ SearchBox.prototype.previous = function(focus) {
+ if (this.results != undefined) {
+ var max = this.results.length - 1;
+ var index = (this.resultIndex != undefined) ? this.resultIndex - 1 : max;
+ if (index < 0) {
+ index = max;
+ }
+ this._setActiveResult(index, focus);
+ }
+ };
+
+ /**
+ * Set new value for the current active result
+ * @param {Number} index
+ * @param {boolean} [focus] If true, focus will be set to the next result.
+ * focus is false by default.
+ * @private
+ */
+ SearchBox.prototype._setActiveResult = function(index, focus) {
+ // de-activate current active result
+ if (this.activeResult) {
+ var prevNode = this.activeResult.node;
+ var prevElem = this.activeResult.elem;
+ if (prevElem == 'field') {
+ delete prevNode.searchFieldActive;
+ }
+ else {
+ delete prevNode.searchValueActive;
+ }
+ prevNode.updateDom();
+ }
+
+ if (!this.results || !this.results[index]) {
+ // out of range, set to undefined
+ this.resultIndex = undefined;
+ this.activeResult = undefined;
+ return;
+ }
+
+ this.resultIndex = index;
+
+ // set new node active
+ var node = this.results[this.resultIndex].node;
+ var elem = this.results[this.resultIndex].elem;
+ if (elem == 'field') {
+ node.searchFieldActive = true;
+ }
+ else {
+ node.searchValueActive = true;
+ }
+ this.activeResult = this.results[this.resultIndex];
+ node.updateDom();
+
+ // TODO: not so nice that the focus is only set after the animation is finished
+ node.scrollTo(function () {
+ if (focus) {
+ node.focus(elem);
+ }
+ });
+ };
+
+ /**
+ * Cancel any running onDelayedSearch.
+ * @private
+ */
+ SearchBox.prototype._clearDelay = function() {
+ if (this.timeout != undefined) {
+ clearTimeout(this.timeout);
+ delete this.timeout;
+ }
+ };
+
+ /**
+ * Start a timer to execute a search after a short delay.
+ * Used for reducing the number of searches while typing.
+ * @param {Event} event
+ * @private
+ */
+ SearchBox.prototype._onDelayedSearch = function (event) {
+ // execute the search after a short delay (reduces the number of
+ // search actions while typing in the search text box)
+ this._clearDelay();
+ var searchBox = this;
+ this.timeout = setTimeout(function (event) {
+ searchBox._onSearch(event);
+ },
+ this.delay);
+ };
+
+ /**
+ * Handle onSearch event
+ * @param {Event} event
+ * @param {boolean} [forceSearch] If true, search will be executed again even
+ * when the search text is not changed.
+ * Default is false.
+ * @private
+ */
+ SearchBox.prototype._onSearch = function (event, forceSearch) {
+ this._clearDelay();
+
+ var value = this.dom.search.value;
+ var text = (value.length > 0) ? value : undefined;
+ if (text != this.lastText || forceSearch) {
+ // only search again when changed
+ this.lastText = text;
+ this.results = this.editor.search(text);
+ this._setActiveResult(undefined);
+
+ // display search results
+ if (text != undefined) {
+ var resultCount = this.results.length;
+ switch (resultCount) {
+ case 0: this.dom.results.innerHTML = 'no results'; break;
+ case 1: this.dom.results.innerHTML = '1 result'; break;
+ default: this.dom.results.innerHTML = resultCount + ' results'; break;
+ }
+ }
+ else {
+ this.dom.results.innerHTML = '';
+ }
+ }
+ };
+
+ /**
+ * Handle onKeyDown event in the input box
+ * @param {Event} event
+ * @private
+ */
+ SearchBox.prototype._onKeyDown = function (event) {
+ var keynum = event.which;
+ if (keynum == 27) { // ESC
+ this.dom.search.value = ''; // clear search
+ this._onSearch(event);
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ else if (keynum == 13) { // Enter
+ if (event.ctrlKey) {
+ // force to search again
+ this._onSearch(event, true);
+ }
+ else if (event.shiftKey) {
+ // move to the previous search result
+ this.previous();
+ }
+ else {
+ // move to the next search result
+ this.next();
+ }
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ };
+
+ /**
+ * Handle onKeyUp event in the input box
+ * @param {Event} event
+ * @private
+ */
+ SearchBox.prototype._onKeyUp = function (event) {
+ var keynum = event.keyCode;
+ if (keynum != 27 && keynum != 13) { // !show and !Enter
+ this._onDelayedSearch(event); // For IE 9
+ }
+ };
+
+ return SearchBox;
+ }.call(exports, __webpack_require__, exports, module)), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+
+
+
+/***/ },
+/* 7 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(9), __webpack_require__(10), __webpack_require__(3)], __WEBPACK_AMD_DEFINE_RESULT__ = (function (ContextMenu, appendNodeFactory, util) {
+
+ /**
+ * @constructor Node
+ * Create a new Node
+ * @param {TreeEditor} editor
+ * @param {Object} [params] Can contain parameters:
+ * {string} field
+ * {boolean} fieldEditable
+ * {*} value
+ * {String} type Can have values 'auto', 'array',
+ * 'object', or 'string'.
+ */
+ function Node (editor, params) {
+ /** @type {TreeEditor} */
+ this.editor = editor;
+ this.dom = {};
+ this.expanded = false;
+
+ if(params && (params instanceof Object)) {
+ this.setField(params.field, params.fieldEditable);
+ this.setValue(params.value, params.type);
+ }
+ else {
+ this.setField('');
+ this.setValue(null);
+ }
+ }
+
+ /**
+ * Set parent node
+ * @param {Node} parent
+ */
+ Node.prototype.setParent = function(parent) {
+ this.parent = parent;
+ };
+
+ /**
+ * Set field
+ * @param {String} field
+ * @param {boolean} [fieldEditable]
+ */
+ Node.prototype.setField = function(field, fieldEditable) {
+ this.field = field;
+ this.fieldEditable = (fieldEditable == true);
+ };
+
+ /**
+ * Get field
+ * @return {String}
+ */
+ Node.prototype.getField = function() {
+ if (this.field === undefined) {
+ this._getDomField();
+ }
+
+ return this.field;
+ };
+
+ /**
+ * Set value. Value is a JSON structure or an element String, Boolean, etc.
+ * @param {*} value
+ * @param {String} [type] Specify the type of the value. Can be 'auto',
+ * 'array', 'object', or 'string'
+ */
+ Node.prototype.setValue = function(value, type) {
+ var childValue, child;
+
+ // first clear all current childs (if any)
+ var childs = this.childs;
+ if (childs) {
+ while (childs.length) {
+ this.removeChild(childs[0]);
+ }
+ }
+
+ // TODO: remove the DOM of this Node
+
+ this.type = this._getType(value);
+
+ // check if type corresponds with the provided type
+ if (type && type != this.type) {
+ if (type == 'string' && this.type == 'auto') {
+ this.type = type;
+ }
+ else {
+ throw new Error('Type mismatch: ' +
+ 'cannot cast value of type "' + this.type +
+ ' to the specified type "' + type + '"');
+ }
+ }
+
+ if (this.type == 'array') {
+ // array
+ this.childs = [];
+ for (var i = 0, iMax = value.length; i < iMax; i++) {
+ childValue = value[i];
+ if (childValue !== undefined && !(childValue instanceof Function)) {
+ // ignore undefined and functions
+ child = new Node(this.editor, {
+ 'value': childValue
+ });
+ this.appendChild(child);
+ }
+ }
+ this.value = '';
+ }
+ else if (this.type == 'object') {
+ // object
+ this.childs = [];
+ for (var childField in value) {
+ if (value.hasOwnProperty(childField)) {
+ childValue = value[childField];
+ if (childValue !== undefined && !(childValue instanceof Function)) {
+ // ignore undefined and functions
+ child = new Node(this.editor, {
+ 'field': childField,
+ 'value': childValue
+ });
+ this.appendChild(child);
+ }
+ }
+ }
+ this.value = '';
+ }
+ else {
+ // value
+ this.childs = undefined;
+ this.value = value;
+ /* TODO
+ if (typeof(value) == 'string') {
+ var escValue = JSON.stringify(value);
+ this.value = escValue.substring(1, escValue.length - 1);
+ util.log('check', value, this.value);
+ }
+ else {
+ this.value = value;
+ }
+ */
+ }
+ };
+
+ /**
+ * Get value. Value is a JSON structure
+ * @return {*} value
+ */
+ Node.prototype.getValue = function() {
+ //var childs, i, iMax;
+
+ if (this.type == 'array') {
+ var arr = [];
+ this.childs.forEach (function (child) {
+ arr.push(child.getValue());
+ });
+ return arr;
+ }
+ else if (this.type == 'object') {
+ var obj = {};
+ this.childs.forEach (function (child) {
+ obj[child.getField()] = child.getValue();
+ });
+ return obj;
+ }
+ else {
+ if (this.value === undefined) {
+ this._getDomValue();
+ }
+
+ return this.value;
+ }
+ };
+
+ /**
+ * Get the nesting level of this node
+ * @return {Number} level
+ */
+ Node.prototype.getLevel = function() {
+ return (this.parent ? this.parent.getLevel() + 1 : 0);
+ };
+
+ /**
+ * Create a clone of a node
+ * The complete state of a clone is copied, including whether it is expanded or
+ * not. The DOM elements are not cloned.
+ * @return {Node} clone
+ */
+ Node.prototype.clone = function() {
+ var clone = new Node(this.editor);
+ clone.type = this.type;
+ clone.field = this.field;
+ clone.fieldInnerText = this.fieldInnerText;
+ clone.fieldEditable = this.fieldEditable;
+ clone.value = this.value;
+ clone.valueInnerText = this.valueInnerText;
+ clone.expanded = this.expanded;
+
+ if (this.childs) {
+ // an object or array
+ var cloneChilds = [];
+ this.childs.forEach(function (child) {
+ var childClone = child.clone();
+ childClone.setParent(clone);
+ cloneChilds.push(childClone);
+ });
+ clone.childs = cloneChilds;
+ }
+ else {
+ // a value
+ clone.childs = undefined;
+ }
+
+ return clone;
+ };
+
+ /**
+ * Expand this node and optionally its childs.
+ * @param {boolean} [recurse] Optional recursion, true by default. When
+ * true, all childs will be expanded recursively
+ */
+ Node.prototype.expand = function(recurse) {
+ if (!this.childs) {
+ return;
+ }
+
+ // set this node expanded
+ this.expanded = true;
+ if (this.dom.expand) {
+ this.dom.expand.className = 'expanded';
+ }
+
+ this.showChilds();
+
+ if (recurse != false) {
+ this.childs.forEach(function (child) {
+ child.expand(recurse);
+ });
+ }
+ };
+
+ /**
+ * Collapse this node and optionally its childs.
+ * @param {boolean} [recurse] Optional recursion, true by default. When
+ * true, all childs will be collapsed recursively
+ */
+ Node.prototype.collapse = function(recurse) {
+ if (!this.childs) {
+ return;
+ }
+
+ this.hideChilds();
+
+ // collapse childs in case of recurse
+ if (recurse != false) {
+ this.childs.forEach(function (child) {
+ child.collapse(recurse);
+ });
+
+ }
+
+ // make this node collapsed
+ if (this.dom.expand) {
+ this.dom.expand.className = 'collapsed';
+ }
+ this.expanded = false;
+ };
+
+ /**
+ * Recursively show all childs when they are expanded
+ */
+ Node.prototype.showChilds = function() {
+ var childs = this.childs;
+ if (!childs) {
+ return;
+ }
+ if (!this.expanded) {
+ return;
+ }
+
+ var tr = this.dom.tr;
+ var table = tr ? tr.parentNode : undefined;
+ if (table) {
+ // show row with append button
+ var append = this.getAppend();
+ var nextTr = tr.nextSibling;
+ if (nextTr) {
+ table.insertBefore(append, nextTr);
+ }
+ else {
+ table.appendChild(append);
+ }
+
+ // show childs
+ this.childs.forEach(function (child) {
+ table.insertBefore(child.getDom(), append);
+ child.showChilds();
+ });
+ }
+ };
+
+ /**
+ * Hide the node with all its childs
+ */
+ Node.prototype.hide = function() {
+ var tr = this.dom.tr;
+ var table = tr ? tr.parentNode : undefined;
+ if (table) {
+ table.removeChild(tr);
+ }
+ this.hideChilds();
+ };
+
+
+ /**
+ * Recursively hide all childs
+ */
+ Node.prototype.hideChilds = function() {
+ var childs = this.childs;
+ if (!childs) {
+ return;
+ }
+ if (!this.expanded) {
+ return;
+ }
+
+ // hide append row
+ var append = this.getAppend();
+ if (append.parentNode) {
+ append.parentNode.removeChild(append);
+ }
+
+ // hide childs
+ this.childs.forEach(function (child) {
+ child.hide();
+ });
+ };
+
+
+ /**
+ * Add a new child to the node.
+ * Only applicable when Node value is of type array or object
+ * @param {Node} node
+ */
+ Node.prototype.appendChild = function(node) {
+ if (this._hasChilds()) {
+ // adjust the link to the parent
+ node.setParent(this);
+ node.fieldEditable = (this.type == 'object');
+ if (this.type == 'array') {
+ node.index = this.childs.length;
+ }
+ this.childs.push(node);
+
+ if (this.expanded) {
+ // insert into the DOM, before the appendRow
+ var newTr = node.getDom();
+ var appendTr = this.getAppend();
+ var table = appendTr ? appendTr.parentNode : undefined;
+ if (appendTr && table) {
+ table.insertBefore(newTr, appendTr);
+ }
+
+ node.showChilds();
+ }
+
+ this.updateDom({'updateIndexes': true});
+ node.updateDom({'recurse': true});
+ }
+ };
+
+
+ /**
+ * Move a node from its current parent to this node
+ * Only applicable when Node value is of type array or object
+ * @param {Node} node
+ * @param {Node} beforeNode
+ */
+ Node.prototype.moveBefore = function(node, beforeNode) {
+ if (this._hasChilds()) {
+ // create a temporary row, to prevent the scroll position from jumping
+ // when removing the node
+ var tbody = (this.dom.tr) ? this.dom.tr.parentNode : undefined;
+ if (tbody) {
+ var trTemp = document.createElement('tr');
+ trTemp.style.height = tbody.clientHeight + 'px';
+ tbody.appendChild(trTemp);
+ }
+
+ if (node.parent) {
+ node.parent.removeChild(node);
+ }
+
+ if (beforeNode instanceof AppendNode) {
+ this.appendChild(node);
+ }
+ else {
+ this.insertBefore(node, beforeNode);
+ }
+
+ if (tbody) {
+ tbody.removeChild(trTemp);
+ }
+ }
+ };
+
+ /**
+ * Move a node from its current parent to this node
+ * Only applicable when Node value is of type array or object.
+ * If index is out of range, the node will be appended to the end
+ * @param {Node} node
+ * @param {Number} index
+ */
+ Node.prototype.moveTo = function (node, index) {
+ if (node.parent == this) {
+ // same parent
+ var currentIndex = this.childs.indexOf(node);
+ if (currentIndex < index) {
+ // compensate the index for removal of the node itself
+ index++;
+ }
+ }
+
+ var beforeNode = this.childs[index] || this.append;
+ this.moveBefore(node, beforeNode);
+ };
+
+ /**
+ * Insert a new child before a given node
+ * Only applicable when Node value is of type array or object
+ * @param {Node} node
+ * @param {Node} beforeNode
+ */
+ Node.prototype.insertBefore = function(node, beforeNode) {
+ if (this._hasChilds()) {
+ if (beforeNode == this.append) {
+ // append to the child nodes
+
+ // adjust the link to the parent
+ node.setParent(this);
+ node.fieldEditable = (this.type == 'object');
+ this.childs.push(node);
+ }
+ else {
+ // insert before a child node
+ var index = this.childs.indexOf(beforeNode);
+ if (index == -1) {
+ throw new Error('Node not found');
+ }
+
+ // adjust the link to the parent
+ node.setParent(this);
+ node.fieldEditable = (this.type == 'object');
+ this.childs.splice(index, 0, node);
+ }
+
+ if (this.expanded) {
+ // insert into the DOM
+ var newTr = node.getDom();
+ var nextTr = beforeNode.getDom();
+ var table = nextTr ? nextTr.parentNode : undefined;
+ if (nextTr && table) {
+ table.insertBefore(newTr, nextTr);
+ }
+
+ node.showChilds();
+ }
+
+ this.updateDom({'updateIndexes': true});
+ node.updateDom({'recurse': true});
+ }
+ };
+
+ /**
+ * Insert a new child before a given node
+ * Only applicable when Node value is of type array or object
+ * @param {Node} node
+ * @param {Node} afterNode
+ */
+ Node.prototype.insertAfter = function(node, afterNode) {
+ if (this._hasChilds()) {
+ var index = this.childs.indexOf(afterNode);
+ var beforeNode = this.childs[index + 1];
+ if (beforeNode) {
+ this.insertBefore(node, beforeNode);
+ }
+ else {
+ this.appendChild(node);
+ }
+ }
+ };
+
+ /**
+ * Search in this node
+ * The node will be expanded when the text is found one of its childs, else
+ * it will be collapsed. Searches are case insensitive.
+ * @param {String} text
+ * @return {Node[]} results Array with nodes containing the search text
+ */
+ Node.prototype.search = function(text) {
+ var results = [];
+ var index;
+ var search = text ? text.toLowerCase() : undefined;
+
+ // delete old search data
+ delete this.searchField;
+ delete this.searchValue;
+
+ // search in field
+ if (this.field != undefined) {
+ var field = String(this.field).toLowerCase();
+ index = field.indexOf(search);
+ if (index != -1) {
+ this.searchField = true;
+ results.push({
+ 'node': this,
+ 'elem': 'field'
+ });
+ }
+
+ // update dom
+ this._updateDomField();
+ }
+
+ // search in value
+ if (this._hasChilds()) {
+ // array, object
+
+ // search the nodes childs
+ if (this.childs) {
+ var childResults = [];
+ this.childs.forEach(function (child) {
+ childResults = childResults.concat(child.search(text));
+ });
+ results = results.concat(childResults);
+ }
+
+ // update dom
+ if (search != undefined) {
+ var recurse = false;
+ if (childResults.length == 0) {
+ this.collapse(recurse);
+ }
+ else {
+ this.expand(recurse);
+ }
+ }
+ }
+ else {
+ // string, auto
+ if (this.value != undefined ) {
+ var value = String(this.value).toLowerCase();
+ index = value.indexOf(search);
+ if (index != -1) {
+ this.searchValue = true;
+ results.push({
+ 'node': this,
+ 'elem': 'value'
+ });
+ }
+ }
+
+ // update dom
+ this._updateDomValue();
+ }
+
+ return results;
+ };
+
+ /**
+ * Move the scroll position such that this node is in the visible area.
+ * The node will not get the focus
+ * @param {function(boolean)} [callback]
+ */
+ Node.prototype.scrollTo = function(callback) {
+ if (!this.dom.tr || !this.dom.tr.parentNode) {
+ // if the node is not visible, expand its parents
+ var parent = this.parent;
+ var recurse = false;
+ while (parent) {
+ parent.expand(recurse);
+ parent = parent.parent;
+ }
+ }
+
+ if (this.dom.tr && this.dom.tr.parentNode) {
+ this.editor.scrollTo(this.dom.tr.offsetTop, callback);
+ }
+ };
+
+
+ // stores the element name currently having the focus
+ Node.focusElement = undefined;
+
+ /**
+ * Set focus to this node
+ * @param {String} [elementName] The field name of the element to get the
+ * focus available values: 'drag', 'menu',
+ * 'expand', 'field', 'value' (default)
+ */
+ Node.prototype.focus = function(elementName) {
+ Node.focusElement = elementName;
+
+ if (this.dom.tr && this.dom.tr.parentNode) {
+ var dom = this.dom;
+
+ switch (elementName) {
+ case 'drag':
+ if (dom.drag) {
+ dom.drag.focus();
+ }
+ else {
+ dom.menu.focus();
+ }
+ break;
+
+ case 'menu':
+ dom.menu.focus();
+ break;
+
+ case 'expand':
+ if (this._hasChilds()) {
+ dom.expand.focus();
+ }
+ else if (dom.field && this.fieldEditable) {
+ dom.field.focus();
+ util.selectContentEditable(dom.field);
+ }
+ else if (dom.value && !this._hasChilds()) {
+ dom.value.focus();
+ util.selectContentEditable(dom.value);
+ }
+ else {
+ dom.menu.focus();
+ }
+ break;
+
+ case 'field':
+ if (dom.field && this.fieldEditable) {
+ dom.field.focus();
+ util.selectContentEditable(dom.field);
+ }
+ else if (dom.value && !this._hasChilds()) {
+ dom.value.focus();
+ util.selectContentEditable(dom.value);
+ }
+ else if (this._hasChilds()) {
+ dom.expand.focus();
+ }
+ else {
+ dom.menu.focus();
+ }
+ break;
+
+ case 'value':
+ default:
+ if (dom.value && !this._hasChilds()) {
+ dom.value.focus();
+ util.selectContentEditable(dom.value);
+ }
+ else if (dom.field && this.fieldEditable) {
+ dom.field.focus();
+ util.selectContentEditable(dom.field);
+ }
+ else if (this._hasChilds()) {
+ dom.expand.focus();
+ }
+ else {
+ dom.menu.focus();
+ }
+ break;
+ }
+ }
+ };
+
+ /**
+ * Select all text in an editable div after a delay of 0 ms
+ * @param {Element} editableDiv
+ */
+ Node.select = function(editableDiv) {
+ setTimeout(function () {
+ util.selectContentEditable(editableDiv);
+ }, 0);
+ };
+
+ /**
+ * Update the values from the DOM field and value of this node
+ */
+ Node.prototype.blur = function() {
+ // retrieve the actual field and value from the DOM.
+ this._getDomValue(false);
+ this._getDomField(false);
+ };
+
+ /**
+ * Duplicate given child node
+ * new structure will be added right before the cloned node
+ * @param {Node} node the childNode to be duplicated
+ * @return {Node} clone the clone of the node
+ * @private
+ */
+ Node.prototype._duplicate = function(node) {
+ var clone = node.clone();
+
+ /* TODO: adjust the field name (to prevent equal field names)
+ if (this.type == 'object') {
+ }
+ */
+
+ this.insertAfter(clone, node);
+
+ return clone;
+ };
+
+ /**
+ * Check if given node is a child. The method will check recursively to find
+ * this node.
+ * @param {Node} node
+ * @return {boolean} containsNode
+ */
+ Node.prototype.containsNode = function(node) {
+ if (this == node) {
+ return true;
+ }
+
+ var childs = this.childs;
+ if (childs) {
+ // TODO: use the js5 Array.some() here?
+ for (var i = 0, iMax = childs.length; i < iMax; i++) {
+ if (childs[i].containsNode(node)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ };
+
+ /**
+ * Move given node into this node
+ * @param {Node} node the childNode to be moved
+ * @param {Node} beforeNode node will be inserted before given
+ * node. If no beforeNode is given,
+ * the node is appended at the end
+ * @private
+ */
+ Node.prototype._move = function(node, beforeNode) {
+ if (node == beforeNode) {
+ // nothing to do...
+ return;
+ }
+
+ // check if this node is not a child of the node to be moved here
+ if (node.containsNode(this)) {
+ throw new Error('Cannot move a field into a child of itself');
+ }
+
+ // remove the original node
+ if (node.parent) {
+ node.parent.removeChild(node);
+ }
+
+ // create a clone of the node
+ var clone = node.clone();
+ node.clearDom();
+
+ // insert or append the node
+ if (beforeNode) {
+ this.insertBefore(clone, beforeNode);
+ }
+ else {
+ this.appendChild(clone);
+ }
+
+ /* TODO: adjust the field name (to prevent equal field names)
+ if (this.type == 'object') {
+ }
+ */
+ };
+
+ /**
+ * Remove a child from the node.
+ * Only applicable when Node value is of type array or object
+ * @param {Node} node The child node to be removed;
+ * @return {Node | undefined} node The removed node on success,
+ * else undefined
+ */
+ Node.prototype.removeChild = function(node) {
+ if (this.childs) {
+ var index = this.childs.indexOf(node);
+
+ if (index != -1) {
+ node.hide();
+
+ // delete old search results
+ delete node.searchField;
+ delete node.searchValue;
+
+ var removedNode = this.childs.splice(index, 1)[0];
+
+ this.updateDom({'updateIndexes': true});
+
+ return removedNode;
+ }
+ }
+
+ return undefined;
+ };
+
+ /**
+ * Remove a child node node from this node
+ * This method is equal to Node.removeChild, except that _remove firex an
+ * onChange event.
+ * @param {Node} node
+ * @private
+ */
+ Node.prototype._remove = function (node) {
+ this.removeChild(node);
+ };
+
+ /**
+ * Change the type of the value of this Node
+ * @param {String} newType
+ */
+ Node.prototype.changeType = function (newType) {
+ var oldType = this.type;
+
+ if (oldType == newType) {
+ // type is not changed
+ return;
+ }
+
+ if ((newType == 'string' || newType == 'auto') &&
+ (oldType == 'string' || oldType == 'auto')) {
+ // this is an easy change
+ this.type = newType;
+ }
+ else {
+ // change from array to object, or from string/auto to object/array
+ var table = this.dom.tr ? this.dom.tr.parentNode : undefined;
+ var lastTr;
+ if (this.expanded) {
+ lastTr = this.getAppend();
+ }
+ else {
+ lastTr = this.getDom();
+ }
+ var nextTr = (lastTr && lastTr.parentNode) ? lastTr.nextSibling : undefined;
+
+ // hide current field and all its childs
+ this.hide();
+ this.clearDom();
+
+ // adjust the field and the value
+ this.type = newType;
+
+ // adjust childs
+ if (newType == 'object') {
+ if (!this.childs) {
+ this.childs = [];
+ }
+
+ this.childs.forEach(function (child, index) {
+ child.clearDom();
+ delete child.index;
+ child.fieldEditable = true;
+ if (child.field == undefined) {
+ child.field = '';
+ }
+ });
+
+ if (oldType == 'string' || oldType == 'auto') {
+ this.expanded = true;
+ }
+ }
+ else if (newType == 'array') {
+ if (!this.childs) {
+ this.childs = [];
+ }
+
+ this.childs.forEach(function (child, index) {
+ child.clearDom();
+ child.fieldEditable = false;
+ child.index = index;
+ });
+
+ if (oldType == 'string' || oldType == 'auto') {
+ this.expanded = true;
+ }
+ }
+ else {
+ this.expanded = false;
+ }
+
+ // create new DOM
+ if (table) {
+ if (nextTr) {
+ table.insertBefore(this.getDom(), nextTr);
+ }
+ else {
+ table.appendChild(this.getDom());
+ }
+ }
+ this.showChilds();
+ }
+
+ if (newType == 'auto' || newType == 'string') {
+ // cast value to the correct type
+ if (newType == 'string') {
+ this.value = String(this.value);
+ }
+ else {
+ this.value = this._stringCast(String(this.value));
+ }
+
+ this.focus();
+ }
+
+ this.updateDom({'updateIndexes': true});
+ };
+
+ /**
+ * Retrieve value from DOM
+ * @param {boolean} [silent] If true (default), no errors will be thrown in
+ * case of invalid data
+ * @private
+ */
+ Node.prototype._getDomValue = function(silent) {
+ if (this.dom.value && this.type != 'array' && this.type != 'object') {
+ this.valueInnerText = util.getInnerText(this.dom.value);
+ }
+
+ if (this.valueInnerText != undefined) {
+ try {
+ // retrieve the value
+ var value;
+ if (this.type == 'string') {
+ value = this._unescapeHTML(this.valueInnerText);
+ }
+ else {
+ var str = this._unescapeHTML(this.valueInnerText);
+ value = this._stringCast(str);
+ }
+ if (value !== this.value) {
+ var oldValue = this.value;
+ this.value = value;
+ this.editor._onAction('editValue', {
+ 'node': this,
+ 'oldValue': oldValue,
+ 'newValue': value,
+ 'oldSelection': this.editor.selection,
+ 'newSelection': this.editor.getSelection()
+ });
+ }
+ }
+ catch (err) {
+ this.value = undefined;
+ // TODO: sent an action with the new, invalid value?
+ if (silent != true) {
+ throw err;
+ }
+ }
+ }
+ };
+
+ /**
+ * Update dom value:
+ * - the text color of the value, depending on the type of the value
+ * - the height of the field, depending on the width
+ * - background color in case it is empty
+ * @private
+ */
+ Node.prototype._updateDomValue = function () {
+ var domValue = this.dom.value;
+ if (domValue) {
+ // set text color depending on value type
+ // TODO: put colors in css
+ var v = this.value;
+ var t = (this.type == 'auto') ? util.type(v) : this.type;
+ var isUrl = (t == 'string' && util.isUrl(v));
+ var color = '';
+ if (isUrl && !this.editor.mode.edit) {
+ color = '';
+ }
+ else if (t == 'string') {
+ color = 'green';
+ }
+ else if (t == 'number') {
+ color = 'red';
+ }
+ else if (t == 'boolean') {
+ color = 'darkorange';
+ }
+ else if (this._hasChilds()) {
+ color = '';
+ }
+ else if (v === null) {
+ color = '#004ED0'; // blue
+ }
+ else {
+ // invalid value
+ color = 'black';
+ }
+ domValue.style.color = color;
+
+ // make background color light-gray when empty
+ var isEmpty = (String(this.value) == '' && this.type != 'array' && this.type != 'object');
+ if (isEmpty) {
+ util.addClassName(domValue, 'empty');
+ }
+ else {
+ util.removeClassName(domValue, 'empty');
+ }
+
+ // underline url
+ if (isUrl) {
+ util.addClassName(domValue, 'url');
+ }
+ else {
+ util.removeClassName(domValue, 'url');
+ }
+
+ // update title
+ if (t == 'array' || t == 'object') {
+ var count = this.childs ? this.childs.length : 0;
+ domValue.title = this.type + ' containing ' + count + ' items';
+ }
+ else if (t == 'string' && util.isUrl(v)) {
+ if (this.editor.mode.edit) {
+ domValue.title = 'Ctrl+Click or Ctrl+Enter to open url in new window';
+ }
+ }
+ else {
+ domValue.title = '';
+ }
+
+ // highlight when there is a search result
+ if (this.searchValueActive) {
+ util.addClassName(domValue, 'highlight-active');
+ }
+ else {
+ util.removeClassName(domValue, 'highlight-active');
+ }
+ if (this.searchValue) {
+ util.addClassName(domValue, 'highlight');
+ }
+ else {
+ util.removeClassName(domValue, 'highlight');
+ }
+
+ // strip formatting from the contents of the editable div
+ util.stripFormatting(domValue);
+ }
+ };
+
+ /**
+ * Update dom field:
+ * - the text color of the field, depending on the text
+ * - the height of the field, depending on the width
+ * - background color in case it is empty
+ * @private
+ */
+ Node.prototype._updateDomField = function () {
+ var domField = this.dom.field;
+ if (domField) {
+ // make backgound color lightgray when empty
+ var isEmpty = (String(this.field) == '' && this.parent.type != 'array');
+ if (isEmpty) {
+ util.addClassName(domField, 'empty');
+ }
+ else {
+ util.removeClassName(domField, 'empty');
+ }
+
+ // highlight when there is a search result
+ if (this.searchFieldActive) {
+ util.addClassName(domField, 'highlight-active');
+ }
+ else {
+ util.removeClassName(domField, 'highlight-active');
+ }
+ if (this.searchField) {
+ util.addClassName(domField, 'highlight');
+ }
+ else {
+ util.removeClassName(domField, 'highlight');
+ }
+
+ // strip formatting from the contents of the editable div
+ util.stripFormatting(domField);
+ }
+ };
+
+ /**
+ * Retrieve field from DOM
+ * @param {boolean} [silent] If true (default), no errors will be thrown in
+ * case of invalid data
+ * @private
+ */
+ Node.prototype._getDomField = function(silent) {
+ if (this.dom.field && this.fieldEditable) {
+ this.fieldInnerText = util.getInnerText(this.dom.field);
+ }
+
+ if (this.fieldInnerText != undefined) {
+ try {
+ var field = this._unescapeHTML(this.fieldInnerText);
+
+ if (field !== this.field) {
+ var oldField = this.field;
+ this.field = field;
+ this.editor._onAction('editField', {
+ 'node': this,
+ 'oldValue': oldField,
+ 'newValue': field,
+ 'oldSelection': this.editor.selection,
+ 'newSelection': this.editor.getSelection()
+ });
+ }
+ }
+ catch (err) {
+ this.field = undefined;
+ // TODO: sent an action here, with the new, invalid value?
+ if (silent != true) {
+ throw err;
+ }
+ }
+ }
+ };
+
+ /**
+ * Clear the dom of the node
+ */
+ Node.prototype.clearDom = function() {
+ // TODO: hide the node first?
+ //this.hide();
+ // TODO: recursively clear dom?
+
+ this.dom = {};
+ };
+
+ /**
+ * Get the HTML DOM TR element of the node.
+ * The dom will be generated when not yet created
+ * @return {Element} tr HTML DOM TR Element
+ */
+ Node.prototype.getDom = function() {
+ var dom = this.dom;
+ if (dom.tr) {
+ return dom.tr;
+ }
+
+ // create row
+ dom.tr = document.createElement('tr');
+ dom.tr.node = this;
+
+ if (this.editor.mode.edit) {
+ // create draggable area
+ var tdDrag = document.createElement('td');
+ if (this.parent) {
+ var domDrag = document.createElement('button');
+ dom.drag = domDrag;
+ domDrag.className = 'dragarea';
+ domDrag.title = 'Drag to move this field (Alt+Shift+Arrows)';
+ tdDrag.appendChild(domDrag);
+ }
+ dom.tr.appendChild(tdDrag);
+
+ // create context menu
+ var tdMenu = document.createElement('td');
+ var menu = document.createElement('button');
+ dom.menu = menu;
+ menu.className = 'contextmenu';
+ menu.title = 'Click to open the actions menu (Ctrl+M)';
+ tdMenu.appendChild(dom.menu);
+ dom.tr.appendChild(tdMenu);
+ }
+
+ // create tree and field
+ var tdField = document.createElement('td');
+ dom.tr.appendChild(tdField);
+ dom.tree = this._createDomTree();
+ tdField.appendChild(dom.tree);
+
+ this.updateDom({'updateIndexes': true});
+
+ return dom.tr;
+ };
+
+ /**
+ * DragStart event, fired on mousedown on the dragarea at the left side of a Node
+ * @param {Event} event
+ * @private
+ */
+ Node.prototype._onDragStart = function (event) {
+ var node = this;
+ if (!this.mousemove) {
+ this.mousemove = util.addEventListener(document, 'mousemove',
+ function (event) {
+ node._onDrag(event);
+ });
+ }
+
+ if (!this.mouseup) {
+ this.mouseup = util.addEventListener(document, 'mouseup',
+ function (event ) {
+ node._onDragEnd(event);
+ });
+ }
+
+ this.editor.highlighter.lock();
+ this.drag = {
+ 'oldCursor': document.body.style.cursor,
+ 'startParent': this.parent,
+ 'startIndex': this.parent.childs.indexOf(this),
+ 'mouseX': event.pageX,
+ 'level': this.getLevel()
+ };
+ document.body.style.cursor = 'move';
+
+ event.preventDefault();
+ };
+
+ /**
+ * Drag event, fired when moving the mouse while dragging a Node
+ * @param {Event} event
+ * @private
+ */
+ Node.prototype._onDrag = function (event) {
+ // TODO: this method has grown too large. Split it in a number of methods
+ var mouseY = event.pageY;
+ var mouseX = event.pageX;
+
+ var trThis, trPrev, trNext, trFirst, trLast, trRoot;
+ var nodePrev, nodeNext;
+ var topThis, topPrev, topFirst, heightThis, bottomNext, heightNext;
+ var moved = false;
+
+ // TODO: add an ESC option, which resets to the original position
+
+ // move up/down
+ trThis = this.dom.tr;
+ topThis = util.getAbsoluteTop(trThis);
+ heightThis = trThis.offsetHeight;
+ if (mouseY < topThis) {
+ // move up
+ trPrev = trThis;
+ do {
+ trPrev = trPrev.previousSibling;
+ nodePrev = Node.getNodeFromTarget(trPrev);
+ topPrev = trPrev ? util.getAbsoluteTop(trPrev) : 0;
+ }
+ while (trPrev && mouseY < topPrev);
+
+ if (nodePrev && !nodePrev.parent) {
+ nodePrev = undefined;
+ }
+
+ if (!nodePrev) {
+ // move to the first node
+ trRoot = trThis.parentNode.firstChild;
+ trPrev = trRoot ? trRoot.nextSibling : undefined;
+ nodePrev = Node.getNodeFromTarget(trPrev);
+ if (nodePrev == this) {
+ nodePrev = undefined;
+ }
+ }
+
+ if (nodePrev) {
+ // check if mouseY is really inside the found node
+ trPrev = nodePrev.dom.tr;
+ topPrev = trPrev ? util.getAbsoluteTop(trPrev) : 0;
+ if (mouseY > topPrev + heightThis) {
+ nodePrev = undefined;
+ }
+ }
+
+ if (nodePrev) {
+ nodePrev.parent.moveBefore(this, nodePrev);
+ moved = true;
+ }
+ }
+ else {
+ // move down
+ trLast = (this.expanded && this.append) ? this.append.getDom() : this.dom.tr;
+ trFirst = trLast ? trLast.nextSibling : undefined;
+ if (trFirst) {
+ topFirst = util.getAbsoluteTop(trFirst);
+ trNext = trFirst;
+ do {
+ nodeNext = Node.getNodeFromTarget(trNext);
+ if (trNext) {
+ bottomNext = trNext.nextSibling ?
+ util.getAbsoluteTop(trNext.nextSibling) : 0;
+ heightNext = trNext ? (bottomNext - topFirst) : 0;
+
+ if (nodeNext.parent.childs.length == 1 && nodeNext.parent.childs[0] == this) {
+ // We are about to remove the last child of this parent,
+ // which will make the parents appendNode visible.
+ topThis += 24 - 1;
+ // TODO: dangerous to suppose the height of the appendNode a constant of 24-1 px.
+ }
+ }
+
+ trNext = trNext.nextSibling;
+ }
+ while (trNext && mouseY > topThis + heightNext);
+
+ if (nodeNext && nodeNext.parent) {
+ // calculate the desired level
+ var diffX = (mouseX - this.drag.mouseX);
+ var diffLevel = Math.round(diffX / 24 / 2);
+ var level = this.drag.level + diffLevel; // desired level
+ var levelNext = nodeNext.getLevel(); // level to be
+
+ // find the best fitting level (move upwards over the append nodes)
+ trPrev = nodeNext.dom.tr.previousSibling;
+ while (levelNext < level && trPrev) {
+ nodePrev = Node.getNodeFromTarget(trPrev);
+ if (nodePrev == this || nodePrev._isChildOf(this)) {
+ // neglect itself and its childs
+ }
+ else if (nodePrev instanceof AppendNode) {
+ var childs = nodePrev.parent.childs;
+ if (childs.length > 1 ||
+ (childs.length == 1 && childs[0] != this)) {
+ // non-visible append node of a list of childs
+ // consisting of not only this node (else the
+ // append node will change into a visible "empty"
+ // text when removing this node).
+ nodeNext = Node.getNodeFromTarget(trPrev);
+ levelNext = nodeNext.getLevel();
+ }
+ else {
+ break;
+ }
+ }
+ else {
+ break;
+ }
+
+ trPrev = trPrev.previousSibling;
+ }
+
+ // move the node when its position is changed
+ if (trLast.nextSibling != nodeNext.dom.tr) {
+ nodeNext.parent.moveBefore(this, nodeNext);
+ moved = true;
+ }
+ }
+ }
+ }
+
+ if (moved) {
+ // update the dragging parameters when moved
+ this.drag.mouseX = mouseX;
+ this.drag.level = this.getLevel();
+ }
+
+ // auto scroll when hovering around the top of the editor
+ this.editor.startAutoScroll(mouseY);
+
+ event.preventDefault();
+ };
+
+ /**
+ * Drag event, fired on mouseup after having dragged a node
+ * @param {Event} event
+ * @private
+ */
+ Node.prototype._onDragEnd = function (event) {
+ var params = {
+ 'node': this,
+ 'startParent': this.drag.startParent,
+ 'startIndex': this.drag.startIndex,
+ 'endParent': this.parent,
+ 'endIndex': this.parent.childs.indexOf(this)
+ };
+ if ((params.startParent != params.endParent) ||
+ (params.startIndex != params.endIndex)) {
+ // only register this action if the node is actually moved to another place
+ this.editor._onAction('moveNode', params);
+ }
+
+ document.body.style.cursor = this.drag.oldCursor;
+ this.editor.highlighter.unlock();
+ delete this.drag;
+
+ if (this.mousemove) {
+ util.removeEventListener(document, 'mousemove', this.mousemove);
+ delete this.mousemove;}
+ if (this.mouseup) {
+ util.removeEventListener(document, 'mouseup', this.mouseup);
+ delete this.mouseup;
+ }
+
+ // Stop any running auto scroll
+ this.editor.stopAutoScroll();
+
+ event.preventDefault();
+ };
+
+ /**
+ * Test if this node is a child of an other node
+ * @param {Node} node
+ * @return {boolean} isChild
+ * @private
+ */
+ Node.prototype._isChildOf = function (node) {
+ var n = this.parent;
+ while (n) {
+ if (n == node) {
+ return true;
+ }
+ n = n.parent;
+ }
+
+ return false;
+ };
+
+ /**
+ * Create an editable field
+ * @return {Element} domField
+ * @private
+ */
+ Node.prototype._createDomField = function () {
+ return document.createElement('div');
+ };
+
+ /**
+ * Set highlighting for this node and all its childs.
+ * Only applied to the currently visible (expanded childs)
+ * @param {boolean} highlight
+ */
+ Node.prototype.setHighlight = function (highlight) {
+ if (this.dom.tr) {
+ this.dom.tr.className = (highlight ? 'highlight' : '');
+
+ if (this.append) {
+ this.append.setHighlight(highlight);
+ }
+
+ if (this.childs) {
+ this.childs.forEach(function (child) {
+ child.setHighlight(highlight);
+ });
+ }
+ }
+ };
+
+ /**
+ * Update the value of the node. Only primitive types are allowed, no Object
+ * or Array is allowed.
+ * @param {String | Number | Boolean | null} value
+ */
+ Node.prototype.updateValue = function (value) {
+ this.value = value;
+ this.updateDom();
+ };
+
+ /**
+ * Update the field of the node.
+ * @param {String} field
+ */
+ Node.prototype.updateField = function (field) {
+ this.field = field;
+ this.updateDom();
+ };
+
+ /**
+ * Update the HTML DOM, optionally recursing through the childs
+ * @param {Object} [options] Available parameters:
+ * {boolean} [recurse] If true, the
+ * DOM of the childs will be updated recursively.
+ * False by default.
+ * {boolean} [updateIndexes] If true, the childs
+ * indexes of the node will be updated too. False by
+ * default.
+ */
+ Node.prototype.updateDom = function (options) {
+ // update level indentation
+ var domTree = this.dom.tree;
+ if (domTree) {
+ domTree.style.marginLeft = this.getLevel() * 24 + 'px';
+ }
+
+ // update field
+ var domField = this.dom.field;
+ if (domField) {
+ if (this.fieldEditable == true) {
+ // parent is an object
+ domField.contentEditable = this.editor.mode.edit;
+ domField.spellcheck = false;
+ domField.className = 'field';
+ }
+ else {
+ // parent is an array this is the root node
+ domField.className = 'readonly';
+ }
+
+ var field;
+ if (this.index != undefined) {
+ field = this.index;
+ }
+ else if (this.field != undefined) {
+ field = this.field;
+ }
+ else if (this._hasChilds()) {
+ field = this.type;
+ }
+ else {
+ field = '';
+ }
+ domField.innerHTML = this._escapeHTML(field);
+ }
+
+ // update value
+ var domValue = this.dom.value;
+ if (domValue) {
+ var count = this.childs ? this.childs.length : 0;
+ if (this.type == 'array') {
+ domValue.innerHTML = '[' + count + ']';
+ }
+ else if (this.type == 'object') {
+ domValue.innerHTML = '{' + count + '}';
+ }
+ else {
+ domValue.innerHTML = this._escapeHTML(this.value);
+ }
+ }
+
+ // update field and value
+ this._updateDomField();
+ this._updateDomValue();
+
+ // update childs indexes
+ if (options && options.updateIndexes == true) {
+ // updateIndexes is true or undefined
+ this._updateDomIndexes();
+ }
+
+ if (options && options.recurse == true) {
+ // recurse is true or undefined. update childs recursively
+ if (this.childs) {
+ this.childs.forEach(function (child) {
+ child.updateDom(options);
+ });
+ }
+ }
+
+ // update row with append button
+ if (this.append) {
+ this.append.updateDom();
+ }
+ };
+
+ /**
+ * Update the DOM of the childs of a node: update indexes and undefined field
+ * names.
+ * Only applicable when structure is an array or object
+ * @private
+ */
+ Node.prototype._updateDomIndexes = function () {
+ var domValue = this.dom.value;
+ var childs = this.childs;
+ if (domValue && childs) {
+ if (this.type == 'array') {
+ childs.forEach(function (child, index) {
+ child.index = index;
+ var childField = child.dom.field;
+ if (childField) {
+ childField.innerHTML = index;
+ }
+ });
+ }
+ else if (this.type == 'object') {
+ childs.forEach(function (child) {
+ if (child.index != undefined) {
+ delete child.index;
+
+ if (child.field == undefined) {
+ child.field = '';
+ }
+ }
+ });
+ }
+ }
+ };
+
+ /**
+ * Create an editable value
+ * @private
+ */
+ Node.prototype._createDomValue = function () {
+ var domValue;
+
+ if (this.type == 'array') {
+ domValue = document.createElement('div');
+ domValue.className = 'readonly';
+ domValue.innerHTML = '[...]';
+ }
+ else if (this.type == 'object') {
+ domValue = document.createElement('div');
+ domValue.className = 'readonly';
+ domValue.innerHTML = '{...}';
+ }
+ else {
+ if (!this.editor.mode.edit && util.isUrl(this.value)) {
+ // create a link in case of read-only editor and value containing an url
+ domValue = document.createElement('a');
+ domValue.className = 'value';
+ domValue.href = this.value;
+ domValue.target = '_blank';
+ domValue.innerHTML = this._escapeHTML(this.value);
+ }
+ else {
+ // create and editable or read-only div
+ domValue = document.createElement('div');
+ domValue.contentEditable = !this.editor.mode.view;
+ domValue.spellcheck = false;
+ domValue.className = 'value';
+ domValue.innerHTML = this._escapeHTML(this.value);
+ }
+ }
+
+ return domValue;
+ };
+
+ /**
+ * Create an expand/collapse button
+ * @return {Element} expand
+ * @private
+ */
+ Node.prototype._createDomExpandButton = function () {
+ // create expand button
+ var expand = document.createElement('button');
+ if (this._hasChilds()) {
+ expand.className = this.expanded ? 'expanded' : 'collapsed';
+ expand.title =
+ 'Click to expand/collapse this field (Ctrl+E). \n' +
+ 'Ctrl+Click to expand/collapse including all childs.';
+ }
+ else {
+ expand.className = 'invisible';
+ expand.title = '';
+ }
+
+ return expand;
+ };
+
+
+ /**
+ * Create a DOM tree element, containing the expand/collapse button
+ * @return {Element} domTree
+ * @private
+ */
+ Node.prototype._createDomTree = function () {
+ var dom = this.dom;
+ var domTree = document.createElement('table');
+ var tbody = document.createElement('tbody');
+ domTree.style.borderCollapse = 'collapse'; // TODO: put in css
+ domTree.className = 'values';
+ domTree.appendChild(tbody);
+ var tr = document.createElement('tr');
+ tbody.appendChild(tr);
+
+ // create expand button
+ var tdExpand = document.createElement('td');
+ tdExpand.className = 'tree';
+ tr.appendChild(tdExpand);
+ dom.expand = this._createDomExpandButton();
+ tdExpand.appendChild(dom.expand);
+ dom.tdExpand = tdExpand;
+
+ // create the field
+ var tdField = document.createElement('td');
+ tdField.className = 'tree';
+ tr.appendChild(tdField);
+ dom.field = this._createDomField();
+ tdField.appendChild(dom.field);
+ dom.tdField = tdField;
+
+ // create a separator
+ var tdSeparator = document.createElement('td');
+ tdSeparator.className = 'tree';
+ tr.appendChild(tdSeparator);
+ if (this.type != 'object' && this.type != 'array') {
+ tdSeparator.appendChild(document.createTextNode(':'));
+ tdSeparator.className = 'separator';
+ }
+ dom.tdSeparator = tdSeparator;
+
+ // create the value
+ var tdValue = document.createElement('td');
+ tdValue.className = 'tree';
+ tr.appendChild(tdValue);
+ dom.value = this._createDomValue();
+ tdValue.appendChild(dom.value);
+ dom.tdValue = tdValue;
+
+ return domTree;
+ };
+
+ /**
+ * Handle an event. The event is catched centrally by the editor
+ * @param {Event} event
+ */
+ Node.prototype.onEvent = function (event) {
+ var type = event.type,
+ target = event.target || event.srcElement,
+ dom = this.dom,
+ node = this,
+ focusNode,
+ expandable = this._hasChilds();
+
+ // check if mouse is on menu or on dragarea.
+ // If so, highlight current row and its childs
+ if (target == dom.drag || target == dom.menu) {
+ if (type == 'mouseover') {
+ this.editor.highlighter.highlight(this);
+ }
+ else if (type == 'mouseout') {
+ this.editor.highlighter.unhighlight();
+ }
+ }
+
+ // drag events
+ if (type == 'mousedown' && target == dom.drag) {
+ this._onDragStart(event);
+ }
+
+ // context menu events
+ if (type == 'click' && target == dom.menu) {
+ var highlighter = node.editor.highlighter;
+ highlighter.highlight(node);
+ highlighter.lock();
+ util.addClassName(dom.menu, 'selected');
+ this.showContextMenu(dom.menu, function () {
+ util.removeClassName(dom.menu, 'selected');
+ highlighter.unlock();
+ highlighter.unhighlight();
+ });
+ }
+
+ // expand events
+ if (type == 'click' && target == dom.expand) {
+ if (expandable) {
+ var recurse = event.ctrlKey; // with ctrl-key, expand/collapse all
+ this._onExpand(recurse);
+ }
+ }
+
+ // value events
+ var domValue = dom.value;
+ if (target == domValue) {
+ //noinspection FallthroughInSwitchStatementJS
+ switch (type) {
+ case 'focus':
+ focusNode = this;
+ break;
+
+ case 'blur':
+ case 'change':
+ this._getDomValue(true);
+ this._updateDomValue();
+ if (this.value) {
+ domValue.innerHTML = this._escapeHTML(this.value);
+ }
+ break;
+
+ case 'input':
+ this._getDomValue(true);
+ this._updateDomValue();
+ break;
+
+ case 'keydown':
+ case 'mousedown':
+ this.editor.selection = this.editor.getSelection();
+ break;
+
+ case 'click':
+ if (event.ctrlKey && this.editor.mode.edit) {
+ if (util.isUrl(this.value)) {
+ window.open(this.value, '_blank');
+ }
+ }
+ break;
+
+ case 'keyup':
+ this._getDomValue(true);
+ this._updateDomValue();
+ break;
+
+ case 'cut':
+ case 'paste':
+ setTimeout(function () {
+ node._getDomValue(true);
+ node._updateDomValue();
+ }, 1);
+ break;
+ }
+ }
+
+ // field events
+ var domField = dom.field;
+ if (target == domField) {
+ switch (type) {
+ case 'focus':
+ focusNode = this;
+ break;
+
+ case 'blur':
+ case 'change':
+ this._getDomField(true);
+ this._updateDomField();
+ if (this.field) {
+ domField.innerHTML = this._escapeHTML(this.field);
+ }
+ break;
+
+ case 'input':
+ this._getDomField(true);
+ this._updateDomField();
+ break;
+
+ case 'keydown':
+ case 'mousedown':
+ this.editor.selection = this.editor.getSelection();
+ break;
+
+ case 'keyup':
+ this._getDomField(true);
+ this._updateDomField();
+ break;
+
+ case 'cut':
+ case 'paste':
+ setTimeout(function () {
+ node._getDomField(true);
+ node._updateDomField();
+ }, 1);
+ break;
+ }
+ }
+
+ // focus
+ // when clicked in whitespace left or right from the field or value, set focus
+ var domTree = dom.tree;
+ if (target == domTree.parentNode) {
+ switch (type) {
+ case 'click':
+ var left = (event.offsetX != undefined) ?
+ (event.offsetX < (this.getLevel() + 1) * 24) :
+ (event.pageX < util.getAbsoluteLeft(dom.tdSeparator));// for FF
+ if (left || expandable) {
+ // node is expandable when it is an object or array
+ if (domField) {
+ util.setEndOfContentEditable(domField);
+ domField.focus();
+ }
+ }
+ else {
+ if (domValue) {
+ util.setEndOfContentEditable(domValue);
+ domValue.focus();
+ }
+ }
+ break;
+ }
+ }
+ if ((target == dom.tdExpand && !expandable) || target == dom.tdField ||
+ target == dom.tdSeparator) {
+ switch (type) {
+ case 'click':
+ if (domField) {
+ util.setEndOfContentEditable(domField);
+ domField.focus();
+ }
+ break;
+ }
+ }
+
+ if (type == 'keydown') {
+ this.onKeyDown(event);
+ }
+ };
+
+ /**
+ * Key down event handler
+ * @param {Event} event
+ */
+ Node.prototype.onKeyDown = function (event) {
+ var keynum = event.which || event.keyCode;
+ var target = event.target || event.srcElement;
+ var ctrlKey = event.ctrlKey;
+ var shiftKey = event.shiftKey;
+ var altKey = event.altKey;
+ var handled = false;
+ var prevNode, nextNode, nextDom, nextDom2;
+
+ // util.log(ctrlKey, keynum, event.charCode); // TODO: cleanup
+ if (keynum == 13) { // Enter
+ if (target == this.dom.value) {
+ if (!this.editor.mode.edit || event.ctrlKey) {
+ if (util.isUrl(this.value)) {
+ window.open(this.value, '_blank');
+ handled = true;
+ }
+ }
+ }
+ else if (target == this.dom.expand) {
+ var expandable = this._hasChilds();
+ if (expandable) {
+ var recurse = event.ctrlKey; // with ctrl-key, expand/collapse all
+ this._onExpand(recurse);
+ target.focus();
+ handled = true;
+ }
+ }
+ }
+ else if (keynum == 68) { // D
+ if (ctrlKey) { // Ctrl+D
+ this._onDuplicate();
+ handled = true;
+ }
+ }
+ else if (keynum == 69) { // E
+ if (ctrlKey) { // Ctrl+E and Ctrl+Shift+E
+ this._onExpand(shiftKey); // recurse = shiftKey
+ target.focus(); // TODO: should restore focus in case of recursing expand (which takes DOM offline)
+ handled = true;
+ }
+ }
+ else if (keynum == 77) { // M
+ if (ctrlKey) { // Ctrl+M
+ this.showContextMenu(target);
+ handled = true;
+ }
+ }
+ else if (keynum == 46) { // Del
+ if (ctrlKey) { // Ctrl+Del
+ this._onRemove();
+ handled = true;
+ }
+ }
+ else if (keynum == 45) { // Ins
+ if (ctrlKey && !shiftKey) { // Ctrl+Ins
+ this._onInsertBefore();
+ handled = true;
+ }
+ else if (ctrlKey && shiftKey) { // Ctrl+Shift+Ins
+ this._onInsertAfter();
+ handled = true;
+ }
+ }
+ else if (keynum == 35) { // End
+ if (altKey) { // Alt+End
+ // find the last node
+ var lastNode = this._lastNode();
+ if (lastNode) {
+ lastNode.focus(Node.focusElement || this._getElementName(target));
+ }
+ handled = true;
+ }
+ }
+ else if (keynum == 36) { // Home
+ if (altKey) { // Alt+Home
+ // find the first node
+ var firstNode = this._firstNode();
+ if (firstNode) {
+ firstNode.focus(Node.focusElement || this._getElementName(target));
+ }
+ handled = true;
+ }
+ }
+ else if (keynum == 37) { // Arrow Left
+ if (altKey && !shiftKey) { // Alt + Arrow Left
+ // move to left element
+ var prevElement = this._previousElement(target);
+ if (prevElement) {
+ this.focus(this._getElementName(prevElement));
+ }
+ handled = true;
+ }
+ else if (altKey && shiftKey) { // Alt + Shift Arrow left
+ if (this.expanded) {
+ var appendDom = this.getAppend();
+ nextDom = appendDom ? appendDom.nextSibling : undefined;
+ }
+ else {
+ var dom = this.getDom();
+ nextDom = dom.nextSibling;
+ }
+ if (nextDom) {
+ nextNode = Node.getNodeFromTarget(nextDom);
+ nextDom2 = nextDom.nextSibling;
+ nextNode2 = Node.getNodeFromTarget(nextDom2);
+ if (nextNode && nextNode instanceof AppendNode &&
+ !(this.parent.childs.length == 1) &&
+ nextNode2 && nextNode2.parent) {
+ nextNode2.parent.moveBefore(this, nextNode2);
+ this.focus(Node.focusElement || this._getElementName(target));
+ }
+ }
+ }
+ }
+ else if (keynum == 38) { // Arrow Up
+ if (altKey && !shiftKey) { // Alt + Arrow Up
+ // find the previous node
+ prevNode = this._previousNode();
+ if (prevNode) {
+ prevNode.focus(Node.focusElement || this._getElementName(target));
+ }
+ handled = true;
+ }
+ else if (altKey && shiftKey) { // Alt + Shift + Arrow Up
+ // find the previous node
+ prevNode = this._previousNode();
+ if (prevNode && prevNode.parent) {
+ prevNode.parent.moveBefore(this, prevNode);
+ this.focus(Node.focusElement || this._getElementName(target));
+ }
+ handled = true;
+ }
+ }
+ else if (keynum == 39) { // Arrow Right
+ if (altKey && !shiftKey) { // Alt + Arrow Right
+ // move to right element
+ var nextElement = this._nextElement(target);
+ if (nextElement) {
+ this.focus(this._getElementName(nextElement));
+ }
+ handled = true;
+ }
+ else if (altKey && shiftKey) { // Alt + Shift Arrow Right
+ dom = this.getDom();
+ var prevDom = dom.previousSibling;
+ if (prevDom) {
+ prevNode = Node.getNodeFromTarget(prevDom);
+ if (prevNode && prevNode.parent &&
+ (prevNode instanceof AppendNode)
+ && !prevNode.isVisible()) {
+ prevNode.parent.moveBefore(this, prevNode);
+ this.focus(Node.focusElement || this._getElementName(target));
+ }
+ }
+ }
+ }
+ else if (keynum == 40) { // Arrow Down
+ if (altKey && !shiftKey) { // Alt + Arrow Down
+ // find the next node
+ nextNode = this._nextNode();
+ if (nextNode) {
+ nextNode.focus(Node.focusElement || this._getElementName(target));
+ }
+ handled = true;
+ }
+ else if (altKey && shiftKey) { // Alt + Shift + Arrow Down
+ // find the 2nd next node and move before that one
+ if (this.expanded) {
+ nextNode = this.append ? this.append._nextNode() : undefined;
+ }
+ else {
+ nextNode = this._nextNode();
+ }
+ nextDom = nextNode ? nextNode.getDom() : undefined;
+ if (this.parent.childs.length == 1) {
+ nextDom2 = nextDom;
+ }
+ else {
+ nextDom2 = nextDom ? nextDom.nextSibling : undefined;
+ }
+ var nextNode2 = Node.getNodeFromTarget(nextDom2);
+ if (nextNode2 && nextNode2.parent) {
+ nextNode2.parent.moveBefore(this, nextNode2);
+ this.focus(Node.focusElement || this._getElementName(target));
+ }
+ handled = true;
+ }
+ }
+
+ if (handled) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ };
+
+ /**
+ * Handle the expand event, when clicked on the expand button
+ * @param {boolean} recurse If true, child nodes will be expanded too
+ * @private
+ */
+ Node.prototype._onExpand = function (recurse) {
+ if (recurse) {
+ // Take the table offline
+ var table = this.dom.tr.parentNode; // TODO: not nice to access the main table like this
+ var frame = table.parentNode;
+ var scrollTop = frame.scrollTop;
+ frame.removeChild(table);
+ }
+
+ if (this.expanded) {
+ this.collapse(recurse);
+ }
+ else {
+ this.expand(recurse);
+ }
+
+ if (recurse) {
+ // Put the table online again
+ frame.appendChild(table);
+ frame.scrollTop = scrollTop;
+ }
+ };
+
+ /**
+ * Remove this node
+ * @private
+ */
+ Node.prototype._onRemove = function() {
+ this.editor.highlighter.unhighlight();
+ var childs = this.parent.childs;
+ var index = childs.indexOf(this);
+
+ // adjust the focus
+ var oldSelection = this.editor.getSelection();
+ if (childs[index + 1]) {
+ childs[index + 1].focus();
+ }
+ else if (childs[index - 1]) {
+ childs[index - 1].focus();
+ }
+ else {
+ this.parent.focus();
+ }
+ var newSelection = this.editor.getSelection();
+
+ // remove the node
+ this.parent._remove(this);
+
+ // store history action
+ this.editor._onAction('removeNode', {
+ 'node': this,
+ 'parent': this.parent,
+ 'index': index,
+ 'oldSelection': oldSelection,
+ 'newSelection': newSelection
+ });
+ };
+
+ /**
+ * Duplicate this node
+ * @private
+ */
+ Node.prototype._onDuplicate = function() {
+ var oldSelection = this.editor.getSelection();
+ var clone = this.parent._duplicate(this);
+ clone.focus();
+ var newSelection = this.editor.getSelection();
+
+ this.editor._onAction('duplicateNode', {
+ 'node': this,
+ 'clone': clone,
+ 'parent': this.parent,
+ 'oldSelection': oldSelection,
+ 'newSelection': newSelection
+ });
+ };
+
+ /**
+ * Handle insert before event
+ * @param {String} [field]
+ * @param {*} [value]
+ * @param {String} [type] Can be 'auto', 'array', 'object', or 'string'
+ * @private
+ */
+ Node.prototype._onInsertBefore = function (field, value, type) {
+ var oldSelection = this.editor.getSelection();
+
+ var newNode = new Node(this.editor, {
+ 'field': (field != undefined) ? field : '',
+ 'value': (value != undefined) ? value : '',
+ 'type': type
+ });
+ newNode.expand(true);
+ this.parent.insertBefore(newNode, this);
+ this.editor.highlighter.unhighlight();
+ newNode.focus('field');
+ var newSelection = this.editor.getSelection();
+
+ this.editor._onAction('insertBeforeNode', {
+ 'node': newNode,
+ 'beforeNode': this,
+ 'parent': this.parent,
+ 'oldSelection': oldSelection,
+ 'newSelection': newSelection
+ });
+ };
+
+ /**
+ * Handle insert after event
+ * @param {String} [field]
+ * @param {*} [value]
+ * @param {String} [type] Can be 'auto', 'array', 'object', or 'string'
+ * @private
+ */
+ Node.prototype._onInsertAfter = function (field, value, type) {
+ var oldSelection = this.editor.getSelection();
+
+ var newNode = new Node(this.editor, {
+ 'field': (field != undefined) ? field : '',
+ 'value': (value != undefined) ? value : '',
+ 'type': type
+ });
+ newNode.expand(true);
+ this.parent.insertAfter(newNode, this);
+ this.editor.highlighter.unhighlight();
+ newNode.focus('field');
+ var newSelection = this.editor.getSelection();
+
+ this.editor._onAction('insertAfterNode', {
+ 'node': newNode,
+ 'afterNode': this,
+ 'parent': this.parent,
+ 'oldSelection': oldSelection,
+ 'newSelection': newSelection
+ });
+ };
+
+ /**
+ * Handle append event
+ * @param {String} [field]
+ * @param {*} [value]
+ * @param {String} [type] Can be 'auto', 'array', 'object', or 'string'
+ * @private
+ */
+ Node.prototype._onAppend = function (field, value, type) {
+ var oldSelection = this.editor.getSelection();
+
+ var newNode = new Node(this.editor, {
+ 'field': (field != undefined) ? field : '',
+ 'value': (value != undefined) ? value : '',
+ 'type': type
+ });
+ newNode.expand(true);
+ this.parent.appendChild(newNode);
+ this.editor.highlighter.unhighlight();
+ newNode.focus('field');
+ var newSelection = this.editor.getSelection();
+
+ this.editor._onAction('appendNode', {
+ 'node': newNode,
+ 'parent': this.parent,
+ 'oldSelection': oldSelection,
+ 'newSelection': newSelection
+ });
+ };
+
+ /**
+ * Change the type of the node's value
+ * @param {String} newType
+ * @private
+ */
+ Node.prototype._onChangeType = function (newType) {
+ var oldType = this.type;
+ if (newType != oldType) {
+ var oldSelection = this.editor.getSelection();
+ this.changeType(newType);
+ var newSelection = this.editor.getSelection();
+
+ this.editor._onAction('changeType', {
+ 'node': this,
+ 'oldType': oldType,
+ 'newType': newType,
+ 'oldSelection': oldSelection,
+ 'newSelection': newSelection
+ });
+ }
+ };
+
+ /**
+ * Sort the childs of the node. Only applicable when the node has type 'object'
+ * or 'array'.
+ * @param {String} direction Sorting direction. Available values: "asc", "desc"
+ * @private
+ */
+ Node.prototype._onSort = function (direction) {
+ if (this._hasChilds()) {
+ var order = (direction == 'desc') ? -1 : 1;
+ var prop = (this.type == 'array') ? 'value': 'field';
+ this.hideChilds();
+
+ var oldChilds = this.childs;
+ var oldSort = this.sort;
+
+ // copy the array (the old one will be kept for an undo action
+ this.childs = this.childs.concat();
+
+ // sort the arrays
+ this.childs.sort(function (a, b) {
+ if (a[prop] > b[prop]) return order;
+ if (a[prop] < b[prop]) return -order;
+ return 0;
+ });
+ this.sort = (order == 1) ? 'asc' : 'desc';
+
+ this.editor._onAction('sort', {
+ 'node': this,
+ 'oldChilds': oldChilds,
+ 'oldSort': oldSort,
+ 'newChilds': this.childs,
+ 'newSort': this.sort
+ });
+
+ this.showChilds();
+ }
+ };
+
+ /**
+ * Create a table row with an append button.
+ * @return {HTMLElement | undefined} buttonAppend or undefined when inapplicable
+ */
+ Node.prototype.getAppend = function () {
+ if (!this.append) {
+ this.append = new AppendNode(this.editor);
+ this.append.setParent(this);
+ }
+ return this.append.getDom();
+ };
+
+ /**
+ * Find the node from an event target
+ * @param {Node} target
+ * @return {Node | undefined} node or undefined when not found
+ * @static
+ */
+ Node.getNodeFromTarget = function (target) {
+ while (target) {
+ if (target.node) {
+ return target.node;
+ }
+ target = target.parentNode;
+ }
+
+ return undefined;
+ };
+
+ /**
+ * Get the previously rendered node
+ * @return {Node | null} previousNode
+ * @private
+ */
+ Node.prototype._previousNode = function () {
+ var prevNode = null;
+ var dom = this.getDom();
+ if (dom && dom.parentNode) {
+ // find the previous field
+ var prevDom = dom;
+ do {
+ prevDom = prevDom.previousSibling;
+ prevNode = Node.getNodeFromTarget(prevDom);
+ }
+ while (prevDom && (prevNode instanceof AppendNode && !prevNode.isVisible()));
+ }
+ return prevNode;
+ };
+
+ /**
+ * Get the next rendered node
+ * @return {Node | null} nextNode
+ * @private
+ */
+ Node.prototype._nextNode = function () {
+ var nextNode = null;
+ var dom = this.getDom();
+ if (dom && dom.parentNode) {
+ // find the previous field
+ var nextDom = dom;
+ do {
+ nextDom = nextDom.nextSibling;
+ nextNode = Node.getNodeFromTarget(nextDom);
+ }
+ while (nextDom && (nextNode instanceof AppendNode && !nextNode.isVisible()));
+ }
+
+ return nextNode;
+ };
+
+ /**
+ * Get the first rendered node
+ * @return {Node | null} firstNode
+ * @private
+ */
+ Node.prototype._firstNode = function () {
+ var firstNode = null;
+ var dom = this.getDom();
+ if (dom && dom.parentNode) {
+ var firstDom = dom.parentNode.firstChild;
+ firstNode = Node.getNodeFromTarget(firstDom);
+ }
+
+ return firstNode;
+ };
+
+ /**
+ * Get the last rendered node
+ * @return {Node | null} lastNode
+ * @private
+ */
+ Node.prototype._lastNode = function () {
+ var lastNode = null;
+ var dom = this.getDom();
+ if (dom && dom.parentNode) {
+ var lastDom = dom.parentNode.lastChild;
+ lastNode = Node.getNodeFromTarget(lastDom);
+ while (lastDom && (lastNode instanceof AppendNode && !lastNode.isVisible())) {
+ lastDom = lastDom.previousSibling;
+ lastNode = Node.getNodeFromTarget(lastDom);
+ }
+ }
+ return lastNode;
+ };
+
+ /**
+ * Get the next element which can have focus.
+ * @param {Element} elem
+ * @return {Element | null} nextElem
+ * @private
+ */
+ Node.prototype._previousElement = function (elem) {
+ var dom = this.dom;
+ // noinspection FallthroughInSwitchStatementJS
+ switch (elem) {
+ case dom.value:
+ if (this.fieldEditable) {
+ return dom.field;
+ }
+ // intentional fall through
+ case dom.field:
+ if (this._hasChilds()) {
+ return dom.expand;
+ }
+ // intentional fall through
+ case dom.expand:
+ return dom.menu;
+ case dom.menu:
+ if (dom.drag) {
+ return dom.drag;
+ }
+ // intentional fall through
+ default:
+ return null;
+ }
+ };
+
+ /**
+ * Get the next element which can have focus.
+ * @param {Element} elem
+ * @return {Element | null} nextElem
+ * @private
+ */
+ Node.prototype._nextElement = function (elem) {
+ var dom = this.dom;
+ // noinspection FallthroughInSwitchStatementJS
+ switch (elem) {
+ case dom.drag:
+ return dom.menu;
+ case dom.menu:
+ if (this._hasChilds()) {
+ return dom.expand;
+ }
+ // intentional fall through
+ case dom.expand:
+ if (this.fieldEditable) {
+ return dom.field;
+ }
+ // intentional fall through
+ case dom.field:
+ if (!this._hasChilds()) {
+ return dom.value;
+ }
+ default:
+ return null;
+ }
+ };
+
+ /**
+ * Get the dom name of given element. returns null if not found.
+ * For example when element == dom.field, "field" is returned.
+ * @param {Element} element
+ * @return {String | null} elementName Available elements with name: 'drag',
+ * 'menu', 'expand', 'field', 'value'
+ * @private
+ */
+ Node.prototype._getElementName = function (element) {
+ var dom = this.dom;
+ for (var name in dom) {
+ if (dom.hasOwnProperty(name)) {
+ if (dom[name] == element) {
+ return name;
+ }
+ }
+ }
+ return null;
+ };
+
+ /**
+ * Test if this node has childs. This is the case when the node is an object
+ * or array.
+ * @return {boolean} hasChilds
+ * @private
+ */
+ Node.prototype._hasChilds = function () {
+ return this.type == 'array' || this.type == 'object';
+ };
+
+ // titles with explanation for the different types
+ Node.TYPE_TITLES = {
+ 'auto': 'Field type "auto". ' +
+ 'The field type is automatically determined from the value ' +
+ 'and can be a string, number, boolean, or null.',
+ 'object': 'Field type "object". ' +
+ 'An object contains an unordered set of key/value pairs.',
+ 'array': 'Field type "array". ' +
+ 'An array contains an ordered collection of values.',
+ 'string': 'Field type "string". ' +
+ 'Field type is not determined from the value, ' +
+ 'but always returned as string.'
+ };
+
+ /**
+ * Show a contextmenu for this node
+ * @param {HTMLElement} anchor Anchor element to attache the context menu to.
+ * @param {function} [onClose] Callback method called when the context menu
+ * is being closed.
+ */
+ Node.prototype.showContextMenu = function (anchor, onClose) {
+ var node = this;
+ var titles = Node.TYPE_TITLES;
+ var items = [];
+
+ items.push({
+ 'text': 'Type',
+ 'title': 'Change the type of this field',
+ 'className': 'type-' + this.type,
+ 'submenu': [
+ {
+ 'text': 'Auto',
+ 'className': 'type-auto' +
+ (this.type == 'auto' ? ' selected' : ''),
+ 'title': titles.auto,
+ 'click': function () {
+ node._onChangeType('auto');
+ }
+ },
+ {
+ 'text': 'Array',
+ 'className': 'type-array' +
+ (this.type == 'array' ? ' selected' : ''),
+ 'title': titles.array,
+ 'click': function () {
+ node._onChangeType('array');
+ }
+ },
+ {
+ 'text': 'Object',
+ 'className': 'type-object' +
+ (this.type == 'object' ? ' selected' : ''),
+ 'title': titles.object,
+ 'click': function () {
+ node._onChangeType('object');
+ }
+ },
+ {
+ 'text': 'String',
+ 'className': 'type-string' +
+ (this.type == 'string' ? ' selected' : ''),
+ 'title': titles.string,
+ 'click': function () {
+ node._onChangeType('string');
+ }
+ }
+ ]
+ });
+
+ if (this._hasChilds()) {
+ var direction = ((this.sort == 'asc') ? 'desc': 'asc');
+ items.push({
+ 'text': 'Sort',
+ 'title': 'Sort the childs of this ' + this.type,
+ 'className': 'sort-' + direction,
+ 'click': function () {
+ node._onSort(direction);
+ },
+ 'submenu': [
+ {
+ 'text': 'Ascending',
+ 'className': 'sort-asc',
+ 'title': 'Sort the childs of this ' + this.type + ' in ascending order',
+ 'click': function () {
+ node._onSort('asc');
+ }
+ },
+ {
+ 'text': 'Descending',
+ 'className': 'sort-desc',
+ 'title': 'Sort the childs of this ' + this.type +' in descending order',
+ 'click': function () {
+ node._onSort('desc');
+ }
+ }
+ ]
+ });
+ }
+
+ if (this.parent && this.parent._hasChilds()) {
+ // create a separator
+ items.push({
+ 'type': 'separator'
+ });
+
+ // create append button (for last child node only)
+ var childs = node.parent.childs;
+ if (node == childs[childs.length - 1]) {
+ items.push({
+ 'text': 'Append',
+ 'title': 'Append a new field with type \'auto\' after this field (Ctrl+Shift+Ins)',
+ 'submenuTitle': 'Select the type of the field to be appended',
+ 'className': 'append',
+ 'click': function () {
+ node._onAppend('', '', 'auto');
+ },
+ 'submenu': [
+ {
+ 'text': 'Auto',
+ 'className': 'type-auto',
+ 'title': titles.auto,
+ 'click': function () {
+ node._onAppend('', '', 'auto');
+ }
+ },
+ {
+ 'text': 'Array',
+ 'className': 'type-array',
+ 'title': titles.array,
+ 'click': function () {
+ node._onAppend('', []);
+ }
+ },
+ {
+ 'text': 'Object',
+ 'className': 'type-object',
+ 'title': titles.object,
+ 'click': function () {
+ node._onAppend('', {});
+ }
+ },
+ {
+ 'text': 'String',
+ 'className': 'type-string',
+ 'title': titles.string,
+ 'click': function () {
+ node._onAppend('', '', 'string');
+ }
+ }
+ ]
+ });
+ }
+
+ // create insert button
+ items.push({
+ 'text': 'Insert',
+ 'title': 'Insert a new field with type \'auto\' before this field (Ctrl+Ins)',
+ 'submenuTitle': 'Select the type of the field to be inserted',
+ 'className': 'insert',
+ 'click': function () {
+ node._onInsertBefore('', '', 'auto');
+ },
+ 'submenu': [
+ {
+ 'text': 'Auto',
+ 'className': 'type-auto',
+ 'title': titles.auto,
+ 'click': function () {
+ node._onInsertBefore('', '', 'auto');
+ }
+ },
+ {
+ 'text': 'Array',
+ 'className': 'type-array',
+ 'title': titles.array,
+ 'click': function () {
+ node._onInsertBefore('', []);
+ }
+ },
+ {
+ 'text': 'Object',
+ 'className': 'type-object',
+ 'title': titles.object,
+ 'click': function () {
+ node._onInsertBefore('', {});
+ }
+ },
+ {
+ 'text': 'String',
+ 'className': 'type-string',
+ 'title': titles.string,
+ 'click': function () {
+ node._onInsertBefore('', '', 'string');
+ }
+ }
+ ]
+ });
+
+ // create duplicate button
+ items.push({
+ 'text': 'Duplicate',
+ 'title': 'Duplicate this field (Ctrl+D)',
+ 'className': 'duplicate',
+ 'click': function () {
+ node._onDuplicate();
+ }
+ });
+
+ // create remove button
+ items.push({
+ 'text': 'Remove',
+ 'title': 'Remove this field (Ctrl+Del)',
+ 'className': 'remove',
+ 'click': function () {
+ node._onRemove();
+ }
+ });
+ }
+
+ var menu = new ContextMenu(items, {close: onClose});
+ menu.show(anchor);
+ };
+
+ /**
+ * get the type of a value
+ * @param {*} value
+ * @return {String} type Can be 'object', 'array', 'string', 'auto'
+ * @private
+ */
+ Node.prototype._getType = function(value) {
+ if (value instanceof Array) {
+ return 'array';
+ }
+ if (value instanceof Object) {
+ return 'object';
+ }
+ if (typeof(value) == 'string' && typeof(this._stringCast(value)) != 'string') {
+ return 'string';
+ }
+
+ return 'auto';
+ };
+
+ /**
+ * cast contents of a string to the correct type. This can be a string,
+ * a number, a boolean, etc
+ * @param {String} str
+ * @return {*} castedStr
+ * @private
+ */
+ Node.prototype._stringCast = function(str) {
+ var lower = str.toLowerCase(),
+ num = Number(str), // will nicely fail with '123ab'
+ numFloat = parseFloat(str); // will nicely fail with ' '
+
+ if (str == '') {
+ return '';
+ }
+ else if (lower == 'null') {
+ return null;
+ }
+ else if (lower == 'true') {
+ return true;
+ }
+ else if (lower == 'false') {
+ return false;
+ }
+ else if (!isNaN(num) && !isNaN(numFloat)) {
+ return num;
+ }
+ else {
+ return str;
+ }
+ };
+
+ /**
+ * escape a text, such that it can be displayed safely in an HTML element
+ * @param {String} text
+ * @return {String} escapedText
+ * @private
+ */
+ Node.prototype._escapeHTML = function (text) {
+ var htmlEscaped = String(text)
+ .replace(//g, '>')
+ .replace(/ /g, ' ') // replace double space with an nbsp and space
+ .replace(/^ /, ' ') // space at start
+ .replace(/ $/, ' '); // space at end
+
+ var json = JSON.stringify(htmlEscaped);
+ return json.substring(1, json.length - 1);
+ };
+
+ /**
+ * unescape a string.
+ * @param {String} escapedText
+ * @return {String} text
+ * @private
+ */
+ Node.prototype._unescapeHTML = function (escapedText) {
+ var json = '"' + this._escapeJSON(escapedText) + '"';
+ var htmlEscaped = util.parse(json);
+ return htmlEscaped
+ .replace(/</g, '<')
+ .replace(/>/g, '>')
+ .replace(/ |\u00A0/g, ' ');
+ };
+
+ /**
+ * escape a text to make it a valid JSON string. The method will:
+ * - replace unescaped double quotes with '\"'
+ * - replace unescaped backslash with '\\'
+ * - replace returns with '\n'
+ * @param {String} text
+ * @return {String} escapedText
+ * @private
+ */
+ Node.prototype._escapeJSON = function (text) {
+ // TODO: replace with some smart regex (only when a new solution is faster!)
+ var escaped = '';
+ var i = 0, iMax = text.length;
+ while (i < iMax) {
+ var c = text.charAt(i);
+ if (c == '\n') {
+ escaped += '\\n';
+ }
+ else if (c == '\\') {
+ escaped += c;
+ i++;
+
+ c = text.charAt(i);
+ if ('"\\/bfnrtu'.indexOf(c) == -1) {
+ escaped += '\\'; // no valid escape character
+ }
+ escaped += c;
+ }
+ else if (c == '"') {
+ escaped += '\\"';
+ }
+ else {
+ escaped += c;
+ }
+ i++;
+ }
+
+ return escaped;
+ };
+
+ // TODO: find a nicer solution to resolve this circular dependency between Node and AppendNode
+ var AppendNode = appendNodeFactory(Node);
+
+ return Node;
+ }.apply(null, __WEBPACK_AMD_DEFINE_ARRAY__)), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+/***/ },
+/* 8 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(9)], __WEBPACK_AMD_DEFINE_RESULT__ = (function (ContextMenu) {
+
+ /**
+ * Create a mode box to be used in the editor menu's
+ * @param {JSONEditor} editor
+ * @param {String[]} modes Available modes: 'code', 'form', 'text', 'tree', 'view'
+ * @param {String} current Available modes: 'code', 'form', 'text', 'tree', 'view'
+ * @returns {HTMLElement} box
+ */
+ function createModeBox(editor, modes, current) {
+ /**
+ * 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
+ var availableModes = {
+ code: {
+ 'text': 'Code',
+ 'title': 'Switch to code highlighter',
+ 'click': function () {
+ switchMode('code')
+ }
+ },
+ form: {
+ 'text': 'Form',
+ 'title': 'Switch to form editor',
+ 'click': function () {
+ switchMode('form');
+ }
+ },
+ text: {
+ 'text': 'Text',
+ 'title': 'Switch to plain text editor',
+ 'click': function () {
+ switchMode('text');
+ }
+ },
+ tree: {
+ 'text': 'Tree',
+ 'title': 'Switch to tree editor',
+ 'click': function () {
+ switchMode('tree');
+ }
+ },
+ view: {
+ 'text': 'View',
+ 'title': 'Switch to tree view',
+ 'click': function () {
+ switchMode('view');
+ }
+ }
+ };
+
+ // list the selected modes
+ var items = [];
+ for (var i = 0; i < modes.length; i++) {
+ var mode = modes[i];
+ var item = availableModes[mode];
+ if (!item) {
+ throw new Error('Unknown mode "' + mode + '"');
+ }
+
+ item.className = 'type-modes' + ((current == mode) ? ' selected' : '');
+ items.push(item);
+ }
+
+ // retrieve the title of current mode
+ var currentMode = availableModes[current];
+ if (!currentMode) {
+ throw new Error('Unknown mode "' + current + '"');
+ }
+ var currentTitle = currentMode.text;
+
+ // create the html element
+ var box = document.createElement('button');
+ box.className = 'modes separator';
+ box.innerHTML = currentTitle + ' ▾';
+ box.title = 'Switch editor mode';
+ box.onclick = function () {
+ var menu = new ContextMenu(items);
+ menu.show(box);
+ };
+
+ return box;
+ }
+
+ return {
+ create: createModeBox
+ }
+ }.apply(null, __WEBPACK_AMD_DEFINE_ARRAY__)), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+
+/***/ },
+/* 9 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(3)], __WEBPACK_AMD_DEFINE_RESULT__ = (function (util) {
+
+ /**
+ * A context menu
+ * @param {Object[]} items Array containing the menu structure
+ * TODO: describe structure
+ * @param {Object} [options] Object with options. Available options:
+ * {function} close Callback called when the
+ * context menu is being closed.
+ * @constructor
+ */
+ function ContextMenu (items, options) {
+ this.dom = {};
+
+ var me = this;
+ var dom = this.dom;
+ this.anchor = undefined;
+ this.items = items;
+ this.eventListeners = {};
+ this.selection = undefined; // holds the selection before the menu was opened
+ this.visibleSubmenu = undefined;
+ this.onClose = options ? options.close : undefined;
+
+ // create a container element
+ var menu = document.createElement('div');
+ menu.className = 'jsoneditor-contextmenu';
+ dom.menu = menu;
+
+ // create a list to hold the menu items
+ var list = document.createElement('ul');
+ list.className = 'menu';
+ menu.appendChild(list);
+ dom.list = list;
+ dom.items = []; // list with all buttons
+
+ // create a (non-visible) button to set the focus to the menu
+ var focusButton = document.createElement('button');
+ dom.focusButton = focusButton;
+ var li = document.createElement('li');
+ li.style.overflow = 'hidden';
+ li.style.height = '0';
+ li.appendChild(focusButton);
+ list.appendChild(li);
+
+ function createMenuItems (list, domItems, items) {
+ items.forEach(function (item) {
+ if (item.type == 'separator') {
+ // create a separator
+ var separator = document.createElement('div');
+ separator.className = 'separator';
+ li = document.createElement('li');
+ li.appendChild(separator);
+ list.appendChild(li);
+ }
+ else {
+ var domItem = {};
+
+ // create a menu item
+ var li = document.createElement('li');
+ list.appendChild(li);
+
+ // create a button in the menu item
+ var button = document.createElement('button');
+ button.className = item.className;
+ domItem.button = button;
+ if (item.title) {
+ button.title = item.title;
+ }
+ if (item.click) {
+ button.onclick = function () {
+ me.hide();
+ item.click();
+ };
+ }
+ li.appendChild(button);
+
+ // create the contents of the button
+ if (item.submenu) {
+ // add the icon to the button
+ var divIcon = document.createElement('div');
+ divIcon.className = 'icon';
+ button.appendChild(divIcon);
+ button.appendChild(document.createTextNode(item.text));
+
+ var buttonSubmenu;
+ if (item.click) {
+ // submenu and a button with a click handler
+ button.className += ' default';
+
+ var buttonExpand = document.createElement('button');
+ domItem.buttonExpand = buttonExpand;
+ buttonExpand.className = 'expand';
+ buttonExpand.innerHTML = '
';
+ li.appendChild(buttonExpand);
+ if (item.submenuTitle) {
+ buttonExpand.title = item.submenuTitle;
+ }
+
+ buttonSubmenu = buttonExpand;
+ }
+ else {
+ // submenu and a button without a click handler
+ var divExpand = document.createElement('div');
+ divExpand.className = 'expand';
+ button.appendChild(divExpand);
+
+ buttonSubmenu = button;
+ }
+
+ // attach a handler to expand/collapse the submenu
+ buttonSubmenu.onclick = function () {
+ me._onExpandItem(domItem);
+ buttonSubmenu.focus();
+ };
+
+ // create the submenu
+ var domSubItems = [];
+ domItem.subItems = domSubItems;
+ var ul = document.createElement('ul');
+ domItem.ul = ul;
+ ul.className = 'menu';
+ ul.style.height = '0';
+ li.appendChild(ul);
+ createMenuItems(ul, domSubItems, item.submenu);
+ }
+ else {
+ // no submenu, just a button with clickhandler
+ button.innerHTML = '' + item.text;
+ }
+
+ domItems.push(domItem);
+ }
+ });
+ }
+ createMenuItems(list, this.dom.items, items);
+
+ // TODO: when the editor is small, show the submenu on the right instead of inline?
+
+ // calculate the max height of the menu with one submenu expanded
+ this.maxHeight = 0; // height in pixels
+ items.forEach(function (item) {
+ var height = (items.length + (item.submenu ? item.submenu.length : 0)) * 24;
+ me.maxHeight = Math.max(me.maxHeight, height);
+ });
+ }
+
+ /**
+ * Get the currently visible buttons
+ * @return {Array.} buttons
+ * @private
+ */
+ ContextMenu.prototype._getVisibleButtons = function () {
+ var buttons = [];
+ var me = this;
+ this.dom.items.forEach(function (item) {
+ buttons.push(item.button);
+ if (item.buttonExpand) {
+ buttons.push(item.buttonExpand);
+ }
+ if (item.subItems && item == me.expandedItem) {
+ item.subItems.forEach(function (subItem) {
+ buttons.push(subItem.button);
+ if (subItem.buttonExpand) {
+ buttons.push(subItem.buttonExpand);
+ }
+ // TODO: change to fully recursive method
+ });
+ }
+ });
+
+ return buttons;
+ };
+
+ // currently displayed context menu, a singleton. We may only have one visible context menu
+ ContextMenu.visibleMenu = undefined;
+
+ /**
+ * Attach the menu to an anchor
+ * @param {HTMLElement} anchor
+ */
+ ContextMenu.prototype.show = function (anchor) {
+ this.hide();
+
+ // calculate whether the menu fits below the anchor
+ var windowHeight = window.innerHeight,
+ windowScroll = (window.pageYOffset || document.scrollTop || 0),
+ windowBottom = windowHeight + windowScroll,
+ anchorHeight = anchor.offsetHeight,
+ menuHeight = this.maxHeight;
+
+ // position the menu
+ var left = util.getAbsoluteLeft(anchor);
+ var top = util.getAbsoluteTop(anchor);
+ if (top + anchorHeight + menuHeight < windowBottom) {
+ // display the menu below the anchor
+ this.dom.menu.style.left = left + 'px';
+ this.dom.menu.style.top = (top + anchorHeight) + 'px';
+ this.dom.menu.style.bottom = '';
+ }
+ else {
+ // display the menu above the anchor
+ this.dom.menu.style.left = left + 'px';
+ this.dom.menu.style.top = '';
+ this.dom.menu.style.bottom = (windowHeight - top) + 'px';
+ }
+
+ // attach the menu to the document
+ document.body.appendChild(this.dom.menu);
+
+ // create and attach event listeners
+ var me = this;
+ var list = this.dom.list;
+ this.eventListeners.mousedown = util.addEventListener(
+ document, 'mousedown', function (event) {
+ // hide menu on click outside of the menu
+ var target = event.target;
+ if ((target != list) && !me._isChildOf(target, list)) {
+ me.hide();
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ });
+ this.eventListeners.mousewheel = util.addEventListener(
+ document, 'mousewheel', function (event) {
+ // block scrolling when context menu is visible
+ event.stopPropagation();
+ event.preventDefault();
+ });
+ this.eventListeners.keydown = util.addEventListener(
+ document, 'keydown', function (event) {
+ me._onKeyDown(event);
+ });
+
+ // move focus to the first button in the context menu
+ this.selection = util.getSelection();
+ this.anchor = anchor;
+ setTimeout(function () {
+ me.dom.focusButton.focus();
+ }, 0);
+
+ if (ContextMenu.visibleMenu) {
+ ContextMenu.visibleMenu.hide();
+ }
+ ContextMenu.visibleMenu = this;
+ };
+
+ /**
+ * Hide the context menu if visible
+ */
+ ContextMenu.prototype.hide = function () {
+ // remove the menu from the DOM
+ if (this.dom.menu.parentNode) {
+ this.dom.menu.parentNode.removeChild(this.dom.menu);
+ if (this.onClose) {
+ this.onClose();
+ }
+ }
+
+ // remove all event listeners
+ // all event listeners are supposed to be attached to document.
+ for (var name in this.eventListeners) {
+ if (this.eventListeners.hasOwnProperty(name)) {
+ var fn = this.eventListeners[name];
+ if (fn) {
+ util.removeEventListener(document, name, fn);
+ }
+ delete this.eventListeners[name];
+ }
+ }
+
+ if (ContextMenu.visibleMenu == this) {
+ ContextMenu.visibleMenu = undefined;
+ }
+ };
+
+ /**
+ * Expand a submenu
+ * Any currently expanded submenu will be hided.
+ * @param {Object} domItem
+ * @private
+ */
+ ContextMenu.prototype._onExpandItem = function (domItem) {
+ var me = this;
+ var alreadyVisible = (domItem == this.expandedItem);
+
+ // hide the currently visible submenu
+ var expandedItem = this.expandedItem;
+ if (expandedItem) {
+ //var ul = expandedItem.ul;
+ expandedItem.ul.style.height = '0';
+ expandedItem.ul.style.padding = '';
+ setTimeout(function () {
+ if (me.expandedItem != expandedItem) {
+ expandedItem.ul.style.display = '';
+ util.removeClassName(expandedItem.ul.parentNode, 'selected');
+ }
+ }, 300); // timeout duration must match the css transition duration
+ this.expandedItem = undefined;
+ }
+
+ if (!alreadyVisible) {
+ var ul = domItem.ul;
+ ul.style.display = 'block';
+ var height = ul.clientHeight; // force a reflow in Firefox
+ setTimeout(function () {
+ if (me.expandedItem == domItem) {
+ ul.style.height = (ul.childNodes.length * 24) + 'px';
+ ul.style.padding = '5px 10px';
+ }
+ }, 0);
+ util.addClassName(ul.parentNode, 'selected');
+ this.expandedItem = domItem;
+ }
+ };
+
+ /**
+ * Handle onkeydown event
+ * @param {Event} event
+ * @private
+ */
+ ContextMenu.prototype._onKeyDown = function (event) {
+ var target = event.target;
+ var keynum = event.which;
+ var handled = false;
+ var buttons, targetIndex, prevButton, nextButton;
+
+ if (keynum == 27) { // ESC
+ // hide the menu on ESC key
+
+ // restore previous selection and focus
+ if (this.selection) {
+ util.setSelection(this.selection);
+ }
+ if (this.anchor) {
+ this.anchor.focus();
+ }
+
+ this.hide();
+
+ handled = true;
+ }
+ else if (keynum == 9) { // Tab
+ if (!event.shiftKey) { // Tab
+ buttons = this._getVisibleButtons();
+ targetIndex = buttons.indexOf(target);
+ if (targetIndex == buttons.length - 1) {
+ // move to first button
+ buttons[0].focus();
+ handled = true;
+ }
+ }
+ else { // Shift+Tab
+ buttons = this._getVisibleButtons();
+ targetIndex = buttons.indexOf(target);
+ if (targetIndex == 0) {
+ // move to last button
+ buttons[buttons.length - 1].focus();
+ handled = true;
+ }
+ }
+ }
+ else if (keynum == 37) { // Arrow Left
+ if (target.className == 'expand') {
+ buttons = this._getVisibleButtons();
+ targetIndex = buttons.indexOf(target);
+ prevButton = buttons[targetIndex - 1];
+ if (prevButton) {
+ prevButton.focus();
+ }
+ }
+ handled = true;
+ }
+ else if (keynum == 38) { // Arrow Up
+ buttons = this._getVisibleButtons();
+ targetIndex = buttons.indexOf(target);
+ prevButton = buttons[targetIndex - 1];
+ if (prevButton && prevButton.className == 'expand') {
+ // skip expand button
+ prevButton = buttons[targetIndex - 2];
+ }
+ if (!prevButton) {
+ // move to last button
+ prevButton = buttons[buttons.length - 1];
+ }
+ if (prevButton) {
+ prevButton.focus();
+ }
+ handled = true;
+ }
+ else if (keynum == 39) { // Arrow Right
+ buttons = this._getVisibleButtons();
+ targetIndex = buttons.indexOf(target);
+ nextButton = buttons[targetIndex + 1];
+ if (nextButton && nextButton.className == 'expand') {
+ nextButton.focus();
+ }
+ handled = true;
+ }
+ else if (keynum == 40) { // Arrow Down
+ buttons = this._getVisibleButtons();
+ targetIndex = buttons.indexOf(target);
+ nextButton = buttons[targetIndex + 1];
+ if (nextButton && nextButton.className == 'expand') {
+ // skip expand button
+ nextButton = buttons[targetIndex + 2];
+ }
+ if (!nextButton) {
+ // move to first button
+ nextButton = buttons[0];
+ }
+ if (nextButton) {
+ nextButton.focus();
+ handled = true;
+ }
+ handled = true;
+ }
+ // TODO: arrow left and right
+
+ if (handled) {
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ };
+
+ /**
+ * Test if an element is a child of a parent element.
+ * @param {Element} child
+ * @param {Element} parent
+ * @return {boolean} isChild
+ */
+ ContextMenu.prototype._isChildOf = function (child, parent) {
+ var e = child.parentNode;
+ while (e) {
+ if (e == parent) {
+ return true;
+ }
+ e = e.parentNode;
+ }
+
+ return false;
+ };
+
+ return ContextMenu;
+ }.apply(null, __WEBPACK_AMD_DEFINE_ARRAY__)), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+
+/***/ },
+/* 10 */
+/***/ function(module, exports, __webpack_require__) {
+
+ var __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;!(__WEBPACK_AMD_DEFINE_ARRAY__ = [__webpack_require__(3)], __WEBPACK_AMD_DEFINE_RESULT__ = (function (util) {
+
+ /**
+ * A factory function to create an AppendNode, which depends on a Node
+ * @param {Node} Node
+ */
+ function appendNodeFactory(Node) {
+ /**
+ * @constructor AppendNode
+ * @extends Node
+ * @param {TreeEditor} editor
+ * Create a new AppendNode. This is a special node which is created at the
+ * end of the list with childs for an object or array
+ */
+ function AppendNode (editor) {
+ /** @type {TreeEditor} */
+ this.editor = editor;
+ this.dom = {};
+ }
+
+ AppendNode.prototype = new Node();
+
+ /**
+ * Return a table row with an append button.
+ * @return {Element} dom TR element
+ */
+ AppendNode.prototype.getDom = function () {
+ // TODO: implement a new solution for the append node
+ var dom = this.dom;
+
+ if (dom.tr) {
+ return dom.tr;
+ }
+
+ // a row for the append button
+ var trAppend = document.createElement('tr');
+ trAppend.node = this;
+ dom.tr = trAppend;
+
+ // TODO: consistent naming
+
+ if (this.editor.mode.edit) {
+ // a cell for the dragarea column
+ dom.tdDrag = document.createElement('td');
+
+ // create context menu
+ var tdMenu = document.createElement('td');
+ dom.tdMenu = tdMenu;
+ var menu = document.createElement('button');
+ menu.className = 'contextmenu';
+ menu.title = 'Click to open the actions menu (Ctrl+M)';
+ dom.menu = menu;
+ tdMenu.appendChild(dom.menu);
+ }
+
+ // a cell for the contents (showing text 'empty')
+ var tdAppend = document.createElement('td');
+ var domText = document.createElement('div');
+ domText.innerHTML = '(empty)';
+ domText.className = 'readonly';
+ tdAppend.appendChild(domText);
+ dom.td = tdAppend;
+ dom.text = domText;
+
+ this.updateDom();
+
+ return trAppend;
+ };
+
+ /**
+ * Update the HTML dom of the Node
+ */
+ AppendNode.prototype.updateDom = function () {
+ var dom = this.dom;
+ var tdAppend = dom.td;
+ if (tdAppend) {
+ tdAppend.style.paddingLeft = (this.getLevel() * 24 + 26) + 'px';
+ // TODO: not so nice hard coded offset
+ }
+
+ var domText = dom.text;
+ if (domText) {
+ domText.innerHTML = '(empty ' + this.parent.type + ')';
+ }
+
+ // attach or detach the contents of the append node:
+ // hide when the parent has childs, show when the parent has no childs
+ var trAppend = dom.tr;
+ if (!this.isVisible()) {
+ if (dom.tr.firstChild) {
+ if (dom.tdDrag) {
+ trAppend.removeChild(dom.tdDrag);
+ }
+ if (dom.tdMenu) {
+ trAppend.removeChild(dom.tdMenu);
+ }
+ trAppend.removeChild(tdAppend);
+ }
+ }
+ else {
+ if (!dom.tr.firstChild) {
+ if (dom.tdDrag) {
+ trAppend.appendChild(dom.tdDrag);
+ }
+ if (dom.tdMenu) {
+ trAppend.appendChild(dom.tdMenu);
+ }
+ trAppend.appendChild(tdAppend);
+ }
+ }
+ };
+
+ /**
+ * Check whether the AppendNode is currently visible.
+ * the AppendNode is visible when its parent has no childs (i.e. is empty).
+ * @return {boolean} isVisible
+ */
+ AppendNode.prototype.isVisible = function () {
+ return (this.parent.childs.length == 0);
+ };
+
+ /**
+ * Show a contextmenu for this node
+ * @param {HTMLElement} anchor The element to attach the menu to.
+ * @param {function} [onClose] Callback method called when the context menu
+ * is being closed.
+ */
+ AppendNode.prototype.showContextMenu = function (anchor, onClose) {
+ var node = this;
+ var titles = Node.TYPE_TITLES;
+ var items = [
+ // create append button
+ {
+ 'text': 'Append',
+ 'title': 'Append a new field with type \'auto\' (Ctrl+Shift+Ins)',
+ 'submenuTitle': 'Select the type of the field to be appended',
+ 'className': 'insert',
+ 'click': function () {
+ node._onAppend('', '', 'auto');
+ },
+ 'submenu': [
+ {
+ 'text': 'Auto',
+ 'className': 'type-auto',
+ 'title': titles.auto,
+ 'click': function () {
+ node._onAppend('', '', 'auto');
+ }
+ },
+ {
+ 'text': 'Array',
+ 'className': 'type-array',
+ 'title': titles.array,
+ 'click': function () {
+ node._onAppend('', []);
+ }
+ },
+ {
+ 'text': 'Object',
+ 'className': 'type-object',
+ 'title': titles.object,
+ 'click': function () {
+ node._onAppend('', {});
+ }
+ },
+ {
+ 'text': 'String',
+ 'className': 'type-string',
+ 'title': titles.string,
+ 'click': function () {
+ node._onAppend('', '', 'string');
+ }
+ }
+ ]
+ }
+ ];
+
+ var menu = new ContextMenu(items, {close: onClose});
+ menu.show(anchor);
+ };
+
+ /**
+ * Handle an event. The event is catched centrally by the editor
+ * @param {Event} event
+ */
+ AppendNode.prototype.onEvent = function (event) {
+ var type = event.type;
+ var target = event.target || event.srcElement;
+ var dom = this.dom;
+
+ // highlight the append nodes parent
+ var menu = dom.menu;
+ if (target == menu) {
+ if (type == 'mouseover') {
+ this.editor.highlighter.highlight(this.parent);
+ }
+ else if (type == 'mouseout') {
+ this.editor.highlighter.unhighlight();
+ }
+ }
+
+ // context menu events
+ if (type == 'click' && target == dom.menu) {
+ var highlighter = this.editor.highlighter;
+ highlighter.highlight(this.parent);
+ highlighter.lock();
+ util.addClassName(dom.menu, 'selected');
+ this.showContextMenu(dom.menu, function () {
+ util.removeClassName(dom.menu, 'selected');
+ highlighter.unlock();
+ highlighter.unhighlight();
+ });
+ }
+
+ if (type == 'keydown') {
+ this.onKeyDown(event);
+ }
+ };
+
+ return AppendNode;
+ }
+
+ // return the factory function
+ return appendNodeFactory;
+ }.apply(null, __WEBPACK_AMD_DEFINE_ARRAY__)), __WEBPACK_AMD_DEFINE_RESULT__ !== undefined && (module.exports = __WEBPACK_AMD_DEFINE_RESULT__));
+
+
+/***/ }
+/******/ ])
+})
diff --git a/jsoneditor.map b/jsoneditor.map
new file mode 100644
index 0000000..f027998
--- /dev/null
+++ b/jsoneditor.map
@@ -0,0 +1 @@
+{"version":3,"file":"jsoneditor.map","sources":["./jsoneditor.js"],"names":["root","factory","exports","module","define","amd","this","modules","__webpack_require__","moduleId","installedModules","id","loaded","call","m","c","p","__WEBPACK_AMD_DEFINE_ARRAY__","__WEBPACK_AMD_DEFINE_RESULT__","TreeEditor","TextEditor","util","JSONEditor","container","options","json","Error","ieVersion","getInternetExplorerVersion","arguments","length","_create","modes","prototype","mode","setMode","_delete","set","get","setText","jsonText","parse","getText","JSON","stringify","setName","name","getName","data","extend","config","clear","editor","load","err","_onError","onError","log","error","registerModes","hasOwnProperty","apply","undefined","Highlighter","History","SearchBox","Node","modebox","dom","highlighter","selection","_setOptions","history","view","_createFrame","_createTable","frame","parentNode","removeChild","search","prop","edit","form","focusNode","Function","content","table","params","field","value","node","_setRoot","recurse","expand","appendChild","blur","getValue","updateField","collapse","tbody","getDom","text","results","expandAll","collapseAll","_onAction","action","add","change","startAutoScroll","mouseY","me","top","getAbsoluteTop","height","clientHeight","bottom","margin","interval","autoScrollStep","scrollTop","scrollHeight","autoScrollTimer","setInterval","stopAutoScroll","clearTimeout","setSelection","range","setSelectionOffset","focus","getSelection","domFocus","getSelectionOffset","scrollTo","callback","animateTimeout","animateCallback","finalScrollTop","Math","min","max","animate","diff","abs","setTimeout","onEvent","event","_onEvent","document","createElement","className","onclick","target","nodeName","preventDefault","oninput","onchange","onkeydown","onkeyup","oncut","onpaste","onmousedown","onmouseup","onmouseover","onmouseout","addEventListener","onfocusin","onfocusout","menu","title","undo","_onUndo","redo","_onRedo","onChange","disabled","canUndo","canRedo","modeBox","create","searchBox","type","_onKeyDown","getNodeFromTarget","keynum","which","keyCode","ctrlKey","shiftKey","handled","selectContentEditable","select","previous","next","stopPropagation","contentOuter","col","colgroupContent","width","tree","indentation","Number","ace","textarea","clientWidth","buttonFormat","format","buttonCompact","compact","editorDom","style","setTheme","setShowPrintMargin","setFontSize","getSession","setTabSize","setUseSoftTabs","setUseWrapMode","poweredBy","createTextNode","href","window","open","on","spellcheck","resize","force","setValue","code","jsonString","validate","jsonlint","a","b","console","object","String","Boolean","RegExp","Array","isArray","isUrlRegex","isUrl","test","getAbsoluteLeft","elem","rect","getBoundingClientRect","left","pageXOffset","scrollLeft","pageYOffset","addClassName","classes","split","indexOf","push","join","removeClassName","index","splice","stripFormatting","divElement","childs","childNodes","i","iMax","child","removeAttribute","attributes","j","attribute","specified","setEndOfContentEditable","contentEditableElement","createRange","selectNodeContents","removeAllRanges","addRange","sel","getRangeAt","rangeCount","startContainer","endContainer","startOffset","endOffset","setStart","firstChild","setEnd","getInnerText","element","buffer","first","flush","nodeValue","hasChildNodes","innerText","prevChild","prevName","_ieVersion","rv","navigator","appName","ua","userAgent","re","exec","parseFloat","$1","isFirefox","listener","useCapture","attachEvent","f","removeEventListener","detachEvent","locked","highlight","setHighlight","_cancelUnhighlight","unhighlight","unhighlightTimer","lock","unlock","actions","editField","oldValue","newValue","editValue","updateValue","appendNode","parent","insertBeforeNode","insertBefore","beforeNode","insertAfterNode","insertAfter","afterNode","removeNode","append","duplicateNode","clone","changeType","oldType","newType","moveNode","startParent","moveTo","startIndex","endParent","endIndex","sort","hideChilds","oldSort","oldChilds","showChilds","newSort","newChilds","timestamp","Date","obj","oldSelection","newSelection","timeout","delay","lastText","tr","td","divInput","input","tableInput","tbodySearch","refreshSearch","_onDelayedSearch","_onSearch","_onKeyUp","searchNext","searchPrevious","resultIndex","_setActiveResult","activeResult","prevNode","prevElem","searchFieldActive","searchValueActive","updateDom","_clearDelay","forceSearch","resultCount","innerHTML","ContextMenu","appendNodeFactory","expanded","Object","setField","fieldEditable","setParent","getField","_getDomField","childValue","_getType","childField","arr","forEach","_getDomValue","getLevel","fieldInnerText","valueInnerText","cloneChilds","childClone","getAppend","nextTr","nextSibling","hide","_hasChilds","newTr","appendTr","updateIndexes","moveBefore","trTemp","AppendNode","currentIndex","toLowerCase","searchField","searchValue","_updateDomField","childResults","concat","_updateDomValue","offsetTop","focusElement","elementName","drag","editableDiv","_duplicate","containsNode","_move","clearDom","removedNode","_remove","lastTr","_stringCast","silent","_unescapeHTML","str","domValue","v","t","color","isEmpty","count","domField","oldField","tdDrag","domDrag","tdMenu","tdField","_createDomTree","_onDragStart","mousemove","_onDrag","mouseup","_onDragEnd","oldCursor","body","cursor","mouseX","pageX","level","trThis","trPrev","trNext","trFirst","trLast","trRoot","nodePrev","nodeNext","topThis","topPrev","topFirst","heightThis","bottomNext","heightNext","pageY","moved","offsetHeight","previousSibling","diffX","diffLevel","round","levelNext","_isChildOf","n","_createDomField","domTree","marginLeft","contentEditable","_escapeHTML","_updateDomIndexes","_createDomValue","_createDomExpandButton","borderCollapse","tdExpand","tdSeparator","tdValue","srcElement","expandable","showContextMenu","_onExpand","offsetX","onKeyDown","nextNode","nextDom","nextDom2","altKey","_onDuplicate","_onRemove","_onInsertBefore","_onInsertAfter","lastNode","_lastNode","_getElementName","firstNode","_firstNode","prevElement","_previousElement","appendDom","nextNode2","_previousNode","nextElement","_nextElement","prevDom","isVisible","_nextNode","newNode","_onAppend","_onChangeType","_onSort","direction","order","firstDom","lastDom","lastChild","TYPE_TITLES","auto","array","string","anchor","onClose","titles","items","submenu","click","submenuTitle","close","show","lower","num","numFloat","isNaN","htmlEscaped","replace","substring","escapedText","_escapeJSON","escaped","charAt","createModeBox","current","switchMode","availableModes","item","currentMode","currentTitle","box","createMenuItems","list","domItems","separator","li","domItem","button","divIcon","buttonSubmenu","buttonExpand","divExpand","_onExpandItem","domSubItems","subItems","ul","eventListeners","visibleSubmenu","focusButton","overflow","maxHeight","_getVisibleButtons","buttons","expandedItem","subItem","visibleMenu","windowHeight","innerHeight","windowScroll","windowBottom","anchorHeight","menuHeight","mousedown","mousewheel","keydown","fn","alreadyVisible","padding","display","targetIndex","prevButton","nextButton","e","trAppend","tdAppend","domText","paddingLeft"],"mappings":"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"}
\ No newline at end of file
diff --git a/jsoneditor.min.js b/jsoneditor.min.js
index eed09d3..fb7f306 100644
--- a/jsoneditor.min.js
+++ b/jsoneditor.min.js
@@ -20,11 +20,12 @@
* License for the specific language governing permissions and limitations under
* the License.
*
- * Copyright (c) 2011-2013 Jos de Jong, http://jsoneditoronline.org
+ * Copyright (c) 2011-2014 Jos de Jong, http://jsoneditoronline.org
*
* @author Jos de Jong,
* @version 3.0.0-SNAPSHOT
* @date 2014-05-29
*/
-!function(){function e(t,i,o){if(!(this instanceof e))throw new Error('JSONEditor constructor called without "new".');var n=util.getInternetExplorerVersion();if(-1!=n&&9>n)throw new Error("Unsupported browser, IE9 or newer required. Please install the newest version of your browser.");arguments.length&&this._create(t,i,o)}function t(e,i,o){if(!(this instanceof t))throw new Error('TreeEditor constructor called without "new".');this._create(e,i,o)}function i(e,t,o){if(!(this instanceof i))throw new Error('TextEditor constructor called without "new".');this._create(e,t,o)}function o(e,t){this.editor=e,this.dom={},this.expanded=!1,t&&t instanceof Object?(this.setField(t.field,t.fieldEditable),this.setValue(t.value,t.type)):(this.setField(""),this.setValue(null))}function n(e){this.editor=e,this.dom={}}function s(e,t){function i(e,t,n){n.forEach(function(n){if("separator"==n.type){var s=document.createElement("div");s.className="separator",a=document.createElement("li"),a.appendChild(s),e.appendChild(a)}else{var r={},a=document.createElement("li");e.appendChild(a);var l=document.createElement("button");if(l.className=n.className,r.button=l,n.title&&(l.title=n.title),n.click&&(l.onclick=function(){o.hide(),n.click()}),a.appendChild(l),n.submenu){var d=document.createElement("div");d.className="icon",l.appendChild(d),l.appendChild(document.createTextNode(n.text));var h;if(n.click){l.className+=" default";var c=document.createElement("button");r.buttonExpand=c,c.className="expand",c.innerHTML='',a.appendChild(c),n.submenuTitle&&(c.title=n.submenuTitle),h=c}else{var u=document.createElement("div");u.className="expand",l.appendChild(u),h=l}h.onclick=function(){o._onExpandItem(r),h.focus()};var p=[];r.subItems=p;var m=document.createElement("ul");r.ul=m,m.className="menu",m.style.height="0",a.appendChild(m),i(m,p,n.submenu)}else l.innerHTML=''+n.text;t.push(r)}})}this.dom={};var o=this,n=this.dom;this.anchor=void 0,this.items=e,this.eventListeners={},this.selection=void 0,this.visibleSubmenu=void 0,this.onClose=t?t.close:void 0;var s=document.createElement("div");s.className="jsoneditor-contextmenu",n.menu=s;var r=document.createElement("ul");r.className="menu",s.appendChild(r),n.list=r,n.items=[];var a=document.createElement("button");n.focusButton=a;var l=document.createElement("li");l.style.overflow="hidden",l.style.height="0",l.appendChild(a),r.appendChild(l),i(r,this.dom.items,e),this.maxHeight=0,e.forEach(function(t){var i=24*(e.length+(t.submenu?t.submenu.length:0));o.maxHeight=Math.max(o.maxHeight,i)})}function r(e){this.editor=e,this.clear(),this.actions={editField:{undo:function(e){e.node.updateField(e.oldValue)},redo:function(e){e.node.updateField(e.newValue)}},editValue:{undo:function(e){e.node.updateValue(e.oldValue)},redo:function(e){e.node.updateValue(e.newValue)}},appendNode:{undo:function(e){e.parent.removeChild(e.node)},redo:function(e){e.parent.appendChild(e.node)}},insertBeforeNode:{undo:function(e){e.parent.removeChild(e.node)},redo:function(e){e.parent.insertBefore(e.node,e.beforeNode)}},insertAfterNode:{undo:function(e){e.parent.removeChild(e.node)},redo:function(e){e.parent.insertAfter(e.node,e.afterNode)}},removeNode:{undo:function(e){var t=e.parent,i=t.childs[e.index]||t.append;t.insertBefore(e.node,i)},redo:function(e){e.parent.removeChild(e.node)}},duplicateNode:{undo:function(e){e.parent.removeChild(e.clone)},redo:function(e){e.parent.insertAfter(e.clone,e.node)}},changeType:{undo:function(e){e.node.changeType(e.oldType)},redo:function(e){e.node.changeType(e.newType)}},moveNode:{undo:function(e){e.startParent.moveTo(e.node,e.startIndex)},redo:function(e){e.endParent.moveTo(e.node,e.endIndex)}},sort:{undo:function(e){var t=e.node;t.hideChilds(),t.sort=e.oldSort,t.childs=e.oldChilds,t.showChilds()},redo:function(e){var t=e.node;t.hideChilds(),t.sort=e.newSort,t.childs=e.newChilds,t.showChilds()}}}}function a(e,t,i){function o(t){e.setMode(t);var i=e.dom&&e.dom.modeBox;i&&i.focus()}for(var n={code:{text:"Code",title:"Switch to code highlighter",click:function(){o("code")}},form:{text:"Form",title:"Switch to form editor",click:function(){o("form")}},text:{text:"Text",title:"Switch to plain text editor",click:function(){o("text")}},tree:{text:"Tree",title:"Switch to tree editor",click:function(){o("tree")}},view:{text:"View",title:"Switch to tree view",click:function(){o("view")}}},r=[],a=0;ae&&i.scrollTop>0?(o+r-e)/3:e>s-r&&n+i.scrollTop3?(i.scrollTop+=n/3,o.animateCallback=t,o.animateTimeout=setTimeout(a,50)):(t&&t(!0),i.scrollTop=r,delete o.animateTimeout,delete o.animateCallback)};a()}else t&&t(!1)},t.prototype._createFrame=function(){this.frame=document.createElement("div"),this.frame.className="jsoneditor",this.container.appendChild(this.frame);var e=this,t=function(t){e._onEvent(t)};this.frame.onclick=function(e){var i=e.target;t(e),"BUTTON"==i.nodeName&&e.preventDefault()},this.frame.oninput=t,this.frame.onchange=t,this.frame.onkeydown=t,this.frame.onkeyup=t,this.frame.oncut=t,this.frame.onpaste=t,this.frame.onmousedown=t,this.frame.onmouseup=t,this.frame.onmouseover=t,this.frame.onmouseout=t,util.addEventListener(this.frame,"focus",t,!0),util.addEventListener(this.frame,"blur",t,!0),this.frame.onfocusin=t,this.frame.onfocusout=t,this.menu=document.createElement("div"),this.menu.className="menu",this.frame.appendChild(this.menu);var i=document.createElement("button");i.className="expand-all",i.title="Expand all fields",i.onclick=function(){e.expandAll()},this.menu.appendChild(i);var o=document.createElement("button");if(o.title="Collapse all fields",o.className="collapse-all",o.onclick=function(){e.collapseAll()},this.menu.appendChild(o),this.history){var n=document.createElement("button");n.className="undo separator",n.title="Undo last action (Ctrl+Z)",n.onclick=function(){e._onUndo()},this.menu.appendChild(n),this.dom.undo=n;var s=document.createElement("button");s.className="redo",s.title="Redo (Ctrl+Shift+Z)",s.onclick=function(){e._onRedo()},this.menu.appendChild(s),this.dom.redo=s,this.history.onChange=function(){n.disabled=!e.history.canUndo(),s.disabled=!e.history.canRedo()},this.history.onChange()}if(this.options&&this.options.modes&&this.options.modes.length){var r=a(this,this.options.modes,this.options.mode);this.menu.appendChild(r),this.dom.modeBox=r}this.options.search&&(this.searchBox=new l(this,this.menu))},t.prototype._onUndo=function(){this.history&&(this.history.undo(),this.options.change&&this.options.change())},t.prototype._onRedo=function(){this.history&&(this.history.redo(),this.options.change&&this.options.change())},t.prototype._onEvent=function(e){var i=e.target;"keydown"==e.type&&this._onKeyDown(e),"focus"==e.type&&(t.domFocus=i);var n=o.getNodeFromTarget(i);n&&n.onEvent(e)},t.prototype._onKeyDown=function(e){var i=e.which||e.keyCode,o=e.ctrlKey,n=e.shiftKey,s=!1;if(9==i&&setTimeout(function(){util.selectContentEditable(t.domFocus)},0),this.searchBox)if(o&&70==i)this.searchBox.dom.search.focus(),this.searchBox.dom.search.select(),s=!0;else if(114==i||o&&71==i){var r=!0;n?this.searchBox.previous(r):this.searchBox.next(r),s=!0}this.history&&(o&&!n&&90==i?(this._onUndo(),s=!0):o&&n&&90==i&&(this._onRedo(),s=!0)),s&&(e.preventDefault(),e.stopPropagation())},t.prototype._createTable=function(){var e=document.createElement("div");e.className="outer",this.contentOuter=e,this.content=document.createElement("div"),this.content.className="tree",e.appendChild(this.content),this.table=document.createElement("table"),this.table.className="tree",this.content.appendChild(this.table);var t;this.colgroupContent=document.createElement("colgroup"),this.mode.edit&&(t=document.createElement("col"),t.width="24px",this.colgroupContent.appendChild(t)),t=document.createElement("col"),t.width="24px",this.colgroupContent.appendChild(t),t=document.createElement("col"),this.colgroupContent.appendChild(t),this.table.appendChild(this.colgroupContent),this.tbody=document.createElement("tbody"),this.table.appendChild(this.tbody),this.frame.appendChild(e)},e.modes.tree={editor:t,data:"json"},e.modes.view={editor:t,data:"json"},e.modes.form={editor:t,data:"json"},e.modes.editor={editor:t,data:"json"},e.modes.viewer={editor:t,data:"json"},i.prototype._create=function(e,t,i){t=t||{},this.options=t,this.indentation=t.indentation?Number(t.indentation):2,this.mode="code"==t.mode?"code":"text","code"==this.mode&&"undefined"==typeof ace&&(this.mode="text",util.log("WARNING: Cannot load code editor, Ace library not loaded. Falling back to plain text editor"));var o=this;this.container=e,this.dom={},this.editor=void 0,this.textarea=void 0,this.width=e.clientWidth,this.height=e.clientHeight,this.frame=document.createElement("div"),this.frame.className="jsoneditor",this.frame.onclick=function(e){e.preventDefault()},this.menu=document.createElement("div"),this.menu.className="menu",this.frame.appendChild(this.menu);var n=document.createElement("button");n.className="format",n.title="Format JSON data, with proper indentation and line feeds",this.menu.appendChild(n),n.onclick=function(){try{o.format()}catch(e){o._onError(e)}};var s=document.createElement("button");if(s.className="compact",s.title="Compact JSON data, remove all whitespaces",this.menu.appendChild(s),s.onclick=function(){try{o.compact()}catch(e){o._onError(e)}},this.options&&this.options.modes&&this.options.modes.length){var r=a(this,this.options.modes,this.options.mode);this.menu.appendChild(r),this.dom.modeBox=r}if(this.content=document.createElement("div"),this.content.className="outer",this.frame.appendChild(this.content),this.container.appendChild(this.frame),"code"==this.mode){this.editorDom=document.createElement("div"),this.editorDom.style.height="100%",this.editorDom.style.width="100%",this.content.appendChild(this.editorDom);var l=ace.edit(this.editorDom);l.setTheme("ace/theme/jsoneditor"),l.setShowPrintMargin(!1),l.setFontSize(13),l.getSession().setMode("ace/mode/json"),l.getSession().setTabSize(2),l.getSession().setUseSoftTabs(!0),l.getSession().setUseWrapMode(!0),this.editor=l;var d=document.createElement("a");d.appendChild(document.createTextNode("powered by ace")),d.href="http://ace.ajax.org",d.target="_blank",d.className="poweredBy",d.onclick=function(){window.open(d.href,d.target)},this.menu.appendChild(d),t.change&&l.on("change",function(){t.change()})}else{var h=document.createElement("textarea");h.className="text",h.spellcheck=!1,this.content.appendChild(h),this.textarea=h,t.change&&(null===this.textarea.oninput?this.textarea.oninput=function(){t.change()}:this.textarea.onchange=function(){t.change()})}"string"==typeof i?this.setText(i):this.set(i)},i.prototype._delete=function(){this.frame&&this.container&&this.frame.parentNode==this.container&&this.container.removeChild(this.frame)},i.prototype._onError=function(e){if("function"==typeof this.onError&&(util.log("WARNING: JSONEditor.onError is deprecated. Use options.error instead."),this.onError(e)),!this.options||"function"!=typeof this.options.error)throw e;this.options.error(e)},i.prototype.compact=function(){var e=util.parse(this.getText());this.setText(JSON.stringify(e))},i.prototype.format=function(){var e=util.parse(this.getText());this.setText(JSON.stringify(e,null,this.indentation))},i.prototype.focus=function(){this.textarea&&this.textarea.focus(),this.editor&&this.editor.focus()},i.prototype.resize=function(){if(this.editor){var e=!1;this.editor.resize(e)}},i.prototype.set=function(e){this.setText(JSON.stringify(e,null,this.indentation))},i.prototype.get=function(){return util.parse(this.getText())},i.prototype.getText=function(){return this.textarea?this.textarea.value:this.editor?this.editor.getValue():""},i.prototype.setText=function(e){this.textarea&&(this.textarea.value=e),this.editor&&this.editor.setValue(e,-1)},e.modes.text={editor:i,data:"text",load:i.prototype.format},e.modes.code={editor:i,data:"text",load:i.prototype.format},o.prototype.setParent=function(e){this.parent=e},o.prototype.setField=function(e,t){this.field=e,this.fieldEditable=1==t},o.prototype.getField=function(){return void 0===this.field&&this._getDomField(),this.field},o.prototype.setValue=function(e,t){var i,n,s=this.childs;if(s)for(;s.length;)this.removeChild(s[0]);if(this.type=this._getType(e),t&&t!=this.type){if("string"!=t||"auto"!=this.type)throw new Error('Type mismatch: cannot cast value of type "'+this.type+' to the specified type "'+t+'"');this.type=t}if("array"==this.type){this.childs=[];for(var r=0,a=e.length;a>r;r++)i=e[r],void 0===i||i instanceof Function||(n=new o(this.editor,{value:i}),this.appendChild(n));this.value=""}else if("object"==this.type){this.childs=[];for(var l in e)e.hasOwnProperty(l)&&(i=e[l],void 0===i||i instanceof Function||(n=new o(this.editor,{field:l,value:i}),this.appendChild(n)));this.value=""}else this.childs=void 0,this.value=e},o.prototype.getValue=function(){if("array"==this.type){var e=[];return this.childs.forEach(function(t){e.push(t.getValue())}),e}if("object"==this.type){var t={};return this.childs.forEach(function(e){t[e.getField()]=e.getValue()}),t}return void 0===this.value&&this._getDomValue(),this.value},o.prototype.getLevel=function(){return this.parent?this.parent.getLevel()+1:0},o.prototype.clone=function(){var e=new o(this.editor);if(e.type=this.type,e.field=this.field,e.fieldInnerText=this.fieldInnerText,e.fieldEditable=this.fieldEditable,e.value=this.value,e.valueInnerText=this.valueInnerText,e.expanded=this.expanded,this.childs){var t=[];this.childs.forEach(function(i){var o=i.clone();o.setParent(e),t.push(o)}),e.childs=t}else e.childs=void 0;return e},o.prototype.expand=function(e){this.childs&&(this.expanded=!0,this.dom.expand&&(this.dom.expand.className="expanded"),this.showChilds(),0!=e&&this.childs.forEach(function(t){t.expand(e)}))},o.prototype.collapse=function(e){this.childs&&(this.hideChilds(),0!=e&&this.childs.forEach(function(t){t.collapse(e)}),this.dom.expand&&(this.dom.expand.className="collapsed"),this.expanded=!1)},o.prototype.showChilds=function(){var e=this.childs;if(e&&this.expanded){var t=this.dom.tr,i=t?t.parentNode:void 0;if(i){var o=this.getAppend(),n=t.nextSibling;n?i.insertBefore(o,n):i.appendChild(o),this.childs.forEach(function(e){i.insertBefore(e.getDom(),o),e.showChilds()})}}},o.prototype.hide=function(){var e=this.dom.tr,t=e?e.parentNode:void 0;t&&t.removeChild(e),this.hideChilds()},o.prototype.hideChilds=function(){var e=this.childs;if(e&&this.expanded){var t=this.getAppend();t.parentNode&&t.parentNode.removeChild(t),this.childs.forEach(function(e){e.hide()})}},o.prototype.appendChild=function(e){if(this._hasChilds()){if(e.setParent(this),e.fieldEditable="object"==this.type,"array"==this.type&&(e.index=this.childs.length),this.childs.push(e),this.expanded){var t=e.getDom(),i=this.getAppend(),o=i?i.parentNode:void 0;i&&o&&o.insertBefore(t,i),e.showChilds()}this.updateDom({updateIndexes:!0}),e.updateDom({recurse:!0})}},o.prototype.moveBefore=function(e,t){if(this._hasChilds()){var i=this.dom.tr?this.dom.tr.parentNode:void 0;if(i){var o=document.createElement("tr");o.style.height=i.clientHeight+"px",i.appendChild(o)}e.parent&&e.parent.removeChild(e),t instanceof n?this.appendChild(e):this.insertBefore(e,t),i&&i.removeChild(o)}},o.prototype.moveTo=function(e,t){if(e.parent==this){var i=this.childs.indexOf(e);t>i&&t++}var o=this.childs[t]||this.append;this.moveBefore(e,o)},o.prototype.insertBefore=function(e,t){if(this._hasChilds()){if(t==this.append)e.setParent(this),e.fieldEditable="object"==this.type,this.childs.push(e);else{var i=this.childs.indexOf(t);if(-1==i)throw new Error("Node not found");e.setParent(this),e.fieldEditable="object"==this.type,this.childs.splice(i,0,e)}if(this.expanded){var o=e.getDom(),n=t.getDom(),s=n?n.parentNode:void 0;n&&s&&s.insertBefore(o,n),e.showChilds()}this.updateDom({updateIndexes:!0}),e.updateDom({recurse:!0})}},o.prototype.insertAfter=function(e,t){if(this._hasChilds()){var i=this.childs.indexOf(t),o=this.childs[i+1];o?this.insertBefore(e,o):this.appendChild(e)}},o.prototype.search=function(e){var t,i=[],o=e?e.toLowerCase():void 0;if(delete this.searchField,delete this.searchValue,void 0!=this.field){var n=String(this.field).toLowerCase();t=n.indexOf(o),-1!=t&&(this.searchField=!0,i.push({node:this,elem:"field"})),this._updateDomField()}if(this._hasChilds()){if(this.childs){var s=[];this.childs.forEach(function(t){s=s.concat(t.search(e))}),i=i.concat(s)}if(void 0!=o){var r=!1;0==s.length?this.collapse(r):this.expand(r)}}else{if(void 0!=this.value){var a=String(this.value).toLowerCase();t=a.indexOf(o),-1!=t&&(this.searchValue=!0,i.push({node:this,elem:"value"}))}this._updateDomValue()}return i},o.prototype.scrollTo=function(e){if(!this.dom.tr||!this.dom.tr.parentNode)for(var t=this.parent,i=!1;t;)t.expand(i),t=t.parent;this.dom.tr&&this.dom.tr.parentNode&&this.editor.scrollTo(this.dom.tr.offsetTop,e)},o.focusElement=void 0,o.prototype.focus=function(e){if(o.focusElement=e,this.dom.tr&&this.dom.tr.parentNode){var t=this.dom;switch(e){case"drag":t.drag?t.drag.focus():t.menu.focus();break;case"menu":t.menu.focus();break;case"expand":this._hasChilds()?t.expand.focus():t.field&&this.fieldEditable?(t.field.focus(),util.selectContentEditable(t.field)):t.value&&!this._hasChilds()?(t.value.focus(),util.selectContentEditable(t.value)):t.menu.focus();break;case"field":t.field&&this.fieldEditable?(t.field.focus(),util.selectContentEditable(t.field)):t.value&&!this._hasChilds()?(t.value.focus(),util.selectContentEditable(t.value)):this._hasChilds()?t.expand.focus():t.menu.focus();break;case"value":default:t.value&&!this._hasChilds()?(t.value.focus(),util.selectContentEditable(t.value)):t.field&&this.fieldEditable?(t.field.focus(),util.selectContentEditable(t.field)):this._hasChilds()?t.expand.focus():t.menu.focus()}}},o.select=function(e){setTimeout(function(){util.selectContentEditable(e)},0)},o.prototype.blur=function(){this._getDomValue(!1),this._getDomField(!1)},o.prototype._duplicate=function(e){var t=e.clone();return this.insertAfter(t,e),t},o.prototype.containsNode=function(e){if(this==e)return!0;var t=this.childs;if(t)for(var i=0,o=t.length;o>i;i++)if(t[i].containsNode(e))return!0;return!1},o.prototype._move=function(e,t){if(e!=t){if(e.containsNode(this))throw new Error("Cannot move a field into a child of itself");e.parent&&e.parent.removeChild(e);var i=e.clone();e.clearDom(),t?this.insertBefore(i,t):this.appendChild(i)}},o.prototype.removeChild=function(e){if(this.childs){var t=this.childs.indexOf(e);if(-1!=t){e.hide(),delete e.searchField,delete e.searchValue;var i=this.childs.splice(t,1)[0];return this.updateDom({updateIndexes:!0}),i}}return void 0},o.prototype._remove=function(e){this.removeChild(e)},o.prototype.changeType=function(e){var t=this.type;if(t!=e){if("string"!=e&&"auto"!=e||"string"!=t&&"auto"!=t){var i,o=this.dom.tr?this.dom.tr.parentNode:void 0;i=this.expanded?this.getAppend():this.getDom();var n=i&&i.parentNode?i.nextSibling:void 0;this.hide(),this.clearDom(),this.type=e,"object"==e?(this.childs||(this.childs=[]),this.childs.forEach(function(e){e.clearDom(),delete e.index,e.fieldEditable=!0,void 0==e.field&&(e.field="")}),("string"==t||"auto"==t)&&(this.expanded=!0)):"array"==e?(this.childs||(this.childs=[]),this.childs.forEach(function(e,t){e.clearDom(),e.fieldEditable=!1,e.index=t}),("string"==t||"auto"==t)&&(this.expanded=!0)):this.expanded=!1,o&&(n?o.insertBefore(this.getDom(),n):o.appendChild(this.getDom())),this.showChilds()}else this.type=e;("auto"==e||"string"==e)&&(this.value="string"==e?String(this.value):this._stringCast(String(this.value)),this.focus()),this.updateDom({updateIndexes:!0})}},o.prototype._getDomValue=function(e){if(this.dom.value&&"array"!=this.type&&"object"!=this.type&&(this.valueInnerText=util.getInnerText(this.dom.value)),void 0!=this.valueInnerText)try{var t;if("string"==this.type)t=this._unescapeHTML(this.valueInnerText);else{var i=this._unescapeHTML(this.valueInnerText);t=this._stringCast(i)}if(t!==this.value){var o=this.value;this.value=t,this.editor._onAction("editValue",{node:this,oldValue:o,newValue:t,oldSelection:this.editor.selection,newSelection:this.editor.getSelection()})}}catch(n){if(this.value=void 0,1!=e)throw n}},o.prototype._updateDomValue=function(){var e=this.dom.value;if(e){var t=this.value,i="auto"==this.type?util.type(t):this.type,o="string"==i&&util.isUrl(t),n="";n=o&&!this.editor.mode.edit?"":"string"==i?"green":"number"==i?"red":"boolean"==i?"darkorange":this._hasChilds()?"":null===t?"#004ED0":"black",e.style.color=n;var s=""==String(this.value)&&"array"!=this.type&&"object"!=this.type;if(s?util.addClassName(e,"empty"):util.removeClassName(e,"empty"),o?util.addClassName(e,"url"):util.removeClassName(e,"url"),"array"==i||"object"==i){var r=this.childs?this.childs.length:0;e.title=this.type+" containing "+r+" items"}else"string"==i&&util.isUrl(t)?this.editor.mode.edit&&(e.title="Ctrl+Click or Ctrl+Enter to open url in new window"):e.title="";this.searchValueActive?util.addClassName(e,"highlight-active"):util.removeClassName(e,"highlight-active"),this.searchValue?util.addClassName(e,"highlight"):util.removeClassName(e,"highlight"),util.stripFormatting(e)}},o.prototype._updateDomField=function(){var e=this.dom.field;if(e){var t=""==String(this.field)&&"array"!=this.parent.type;t?util.addClassName(e,"empty"):util.removeClassName(e,"empty"),this.searchFieldActive?util.addClassName(e,"highlight-active"):util.removeClassName(e,"highlight-active"),this.searchField?util.addClassName(e,"highlight"):util.removeClassName(e,"highlight"),util.stripFormatting(e)}},o.prototype._getDomField=function(e){if(this.dom.field&&this.fieldEditable&&(this.fieldInnerText=util.getInnerText(this.dom.field)),void 0!=this.fieldInnerText)try{var t=this._unescapeHTML(this.fieldInnerText);if(t!==this.field){var i=this.field;this.field=t,this.editor._onAction("editField",{node:this,oldValue:i,newValue:t,oldSelection:this.editor.selection,newSelection:this.editor.getSelection()})}}catch(o){if(this.field=void 0,1!=e)throw o}},o.prototype.clearDom=function(){this.dom={}},o.prototype.getDom=function(){var e=this.dom;if(e.tr)return e.tr;if(e.tr=document.createElement("tr"),e.tr.node=this,this.editor.mode.edit){var t=document.createElement("td");if(this.parent){var i=document.createElement("button");e.drag=i,i.className="dragarea",i.title="Drag to move this field (Alt+Shift+Arrows)",t.appendChild(i)}e.tr.appendChild(t);var o=document.createElement("td"),n=document.createElement("button");e.menu=n,n.className="contextmenu",n.title="Click to open the actions menu (Ctrl+M)",o.appendChild(e.menu),e.tr.appendChild(o)}var s=document.createElement("td");return e.tr.appendChild(s),e.tree=this._createDomTree(),s.appendChild(e.tree),this.updateDom({updateIndexes:!0}),e.tr},o.prototype._onDragStart=function(e){var t=this;this.mousemove||(this.mousemove=util.addEventListener(document,"mousemove",function(e){t._onDrag(e)})),this.mouseup||(this.mouseup=util.addEventListener(document,"mouseup",function(e){t._onDragEnd(e)})),this.editor.highlighter.lock(),this.drag={oldCursor:document.body.style.cursor,startParent:this.parent,startIndex:this.parent.childs.indexOf(this),mouseX:e.pageX,level:this.getLevel()},document.body.style.cursor="move",e.preventDefault()},o.prototype._onDrag=function(e){var t,i,s,r,a,l,d,h,c,u,p,m,f,v,g=e.pageY,y=e.pageX,x=!1;if(t=this.dom.tr,c=util.getAbsoluteTop(t),m=t.offsetHeight,c>g){i=t;do i=i.previousSibling,d=o.getNodeFromTarget(i),u=i?util.getAbsoluteTop(i):0;while(i&&u>g);d&&!d.parent&&(d=void 0),d||(l=t.parentNode.firstChild,i=l?l.nextSibling:void 0,d=o.getNodeFromTarget(i),d==this&&(d=void 0)),d&&(i=d.dom.tr,u=i?util.getAbsoluteTop(i):0,g>u+m&&(d=void 0)),d&&(d.parent.moveBefore(this,d),x=!0)}else if(a=this.expanded&&this.append?this.append.getDom():this.dom.tr,r=a?a.nextSibling:void 0){p=util.getAbsoluteTop(r),s=r;do h=o.getNodeFromTarget(s),s&&(f=s.nextSibling?util.getAbsoluteTop(s.nextSibling):0,v=s?f-p:0,1==h.parent.childs.length&&h.parent.childs[0]==this&&(c+=23)),s=s.nextSibling;while(s&&g>c+v);if(h&&h.parent){var C=y-this.drag.mouseX,b=Math.round(C/24/2),N=this.drag.level+b,E=h.getLevel();for(i=h.dom.tr.previousSibling;N>E&&i;){if(d=o.getNodeFromTarget(i),d==this||d._isChildOf(this));else{if(!(d instanceof n))break;var _=d.parent.childs;if(!(_.length>1||1==_.length&&_[0]!=this))break;h=o.getNodeFromTarget(i),E=h.getLevel()}i=i.previousSibling}a.nextSibling!=h.dom.tr&&(h.parent.moveBefore(this,h),x=!0)
-}}x&&(this.drag.mouseX=y,this.drag.level=this.getLevel()),this.editor.startAutoScroll(g),e.preventDefault()},o.prototype._onDragEnd=function(e){var t={node:this,startParent:this.drag.startParent,startIndex:this.drag.startIndex,endParent:this.parent,endIndex:this.parent.childs.indexOf(this)};(t.startParent!=t.endParent||t.startIndex!=t.endIndex)&&this.editor._onAction("moveNode",t),document.body.style.cursor=this.drag.oldCursor,this.editor.highlighter.unlock(),delete this.drag,this.mousemove&&(util.removeEventListener(document,"mousemove",this.mousemove),delete this.mousemove),this.mouseup&&(util.removeEventListener(document,"mouseup",this.mouseup),delete this.mouseup),this.editor.stopAutoScroll(),e.preventDefault()},o.prototype._isChildOf=function(e){for(var t=this.parent;t;){if(t==e)return!0;t=t.parent}return!1},o.prototype._createDomField=function(){return document.createElement("div")},o.prototype.setHighlight=function(e){this.dom.tr&&(this.dom.tr.className=e?"highlight":"",this.append&&this.append.setHighlight(e),this.childs&&this.childs.forEach(function(t){t.setHighlight(e)}))},o.prototype.updateValue=function(e){this.value=e,this.updateDom()},o.prototype.updateField=function(e){this.field=e,this.updateDom()},o.prototype.updateDom=function(e){var t=this.dom.tree;t&&(t.style.marginLeft=24*this.getLevel()+"px");var i=this.dom.field;if(i){1==this.fieldEditable?(i.contentEditable=this.editor.mode.edit,i.spellcheck=!1,i.className="field"):i.className="readonly";var o;o=void 0!=this.index?this.index:void 0!=this.field?this.field:this._hasChilds()?this.type:"",i.innerHTML=this._escapeHTML(o)}var n=this.dom.value;if(n){var s=this.childs?this.childs.length:0;n.innerHTML="array"==this.type?"["+s+"]":"object"==this.type?"{"+s+"}":this._escapeHTML(this.value)}this._updateDomField(),this._updateDomValue(),e&&1==e.updateIndexes&&this._updateDomIndexes(),e&&1==e.recurse&&this.childs&&this.childs.forEach(function(t){t.updateDom(e)}),this.append&&this.append.updateDom()},o.prototype._updateDomIndexes=function(){var e=this.dom.value,t=this.childs;e&&t&&("array"==this.type?t.forEach(function(e,t){e.index=t;var i=e.dom.field;i&&(i.innerHTML=t)}):"object"==this.type&&t.forEach(function(e){void 0!=e.index&&(delete e.index,void 0==e.field&&(e.field=""))}))},o.prototype._createDomValue=function(){var e;return"array"==this.type?(e=document.createElement("div"),e.className="readonly",e.innerHTML="[...]"):"object"==this.type?(e=document.createElement("div"),e.className="readonly",e.innerHTML="{...}"):!this.editor.mode.edit&&util.isUrl(this.value)?(e=document.createElement("a"),e.className="value",e.href=this.value,e.target="_blank",e.innerHTML=this._escapeHTML(this.value)):(e=document.createElement("div"),e.contentEditable=!this.editor.mode.view,e.spellcheck=!1,e.className="value",e.innerHTML=this._escapeHTML(this.value)),e},o.prototype._createDomExpandButton=function(){var e=document.createElement("button");return this._hasChilds()?(e.className=this.expanded?"expanded":"collapsed",e.title="Click to expand/collapse this field (Ctrl+E). \nCtrl+Click to expand/collapse including all childs."):(e.className="invisible",e.title=""),e},o.prototype._createDomTree=function(){var e=this.dom,t=document.createElement("table"),i=document.createElement("tbody");t.style.borderCollapse="collapse",t.className="values",t.appendChild(i);var o=document.createElement("tr");i.appendChild(o);var n=document.createElement("td");n.className="tree",o.appendChild(n),e.expand=this._createDomExpandButton(),n.appendChild(e.expand),e.tdExpand=n;var s=document.createElement("td");s.className="tree",o.appendChild(s),e.field=this._createDomField(),s.appendChild(e.field),e.tdField=s;var r=document.createElement("td");r.className="tree",o.appendChild(r),"object"!=this.type&&"array"!=this.type&&(r.appendChild(document.createTextNode(":")),r.className="separator"),e.tdSeparator=r;var a=document.createElement("td");return a.className="tree",o.appendChild(a),e.value=this._createDomValue(),a.appendChild(e.value),e.tdValue=a,t},o.prototype.onEvent=function(e){var t,i=e.type,o=e.target||e.srcElement,n=this.dom,s=this,r=this._hasChilds();if((o==n.drag||o==n.menu)&&("mouseover"==i?this.editor.highlighter.highlight(this):"mouseout"==i&&this.editor.highlighter.unhighlight()),"mousedown"==i&&o==n.drag&&this._onDragStart(e),"click"==i&&o==n.menu){var a=s.editor.highlighter;a.highlight(s),a.lock(),util.addClassName(n.menu,"selected"),this.showContextMenu(n.menu,function(){util.removeClassName(n.menu,"selected"),a.unlock(),a.unhighlight()})}if("click"==i&&o==n.expand&&r){var l=e.ctrlKey;this._onExpand(l)}var d=n.value;if(o==d)switch(i){case"focus":t=this;break;case"blur":case"change":this._getDomValue(!0),this._updateDomValue(),this.value&&(d.innerHTML=this._escapeHTML(this.value));break;case"input":this._getDomValue(!0),this._updateDomValue();break;case"keydown":case"mousedown":this.editor.selection=this.editor.getSelection();break;case"click":e.ctrlKey&&this.editor.mode.edit&&util.isUrl(this.value)&&window.open(this.value,"_blank");break;case"keyup":this._getDomValue(!0),this._updateDomValue();break;case"cut":case"paste":setTimeout(function(){s._getDomValue(!0),s._updateDomValue()},1)}var h=n.field;if(o==h)switch(i){case"focus":t=this;break;case"blur":case"change":this._getDomField(!0),this._updateDomField(),this.field&&(h.innerHTML=this._escapeHTML(this.field));break;case"input":this._getDomField(!0),this._updateDomField();break;case"keydown":case"mousedown":this.editor.selection=this.editor.getSelection();break;case"keyup":this._getDomField(!0),this._updateDomField();break;case"cut":case"paste":setTimeout(function(){s._getDomField(!0),s._updateDomField()},1)}var c=n.tree;if(o==c.parentNode)switch(i){case"click":var u=void 0!=e.offsetX?e.offsetX<24*(this.getLevel()+1):e.pageXo[i]?t:e[i]/g,">").replace(/ /g," ").replace(/^ /," ").replace(/ $/," "),i=JSON.stringify(t);return i.substring(1,i.length-1)},o.prototype._unescapeHTML=function(e){var t='"'+this._escapeJSON(e)+'"',i=util.parse(t);return i.replace(/</g,"<").replace(/>/g,">").replace(/ |\u00A0/g," ")},o.prototype._escapeJSON=function(e){for(var t="",i=0,o=e.length;o>i;){var n=e.charAt(i);"\n"==n?t+="\\n":"\\"==n?(t+=n,i++,n=e.charAt(i),-1=='"\\/bfnrtu'.indexOf(n)&&(t+="\\"),t+=n):t+='"'==n?'\\"':n,i++}return t},n.prototype=new o,n.prototype.getDom=function(){var e=this.dom;if(e.tr)return e.tr;var t=document.createElement("tr");if(t.node=this,e.tr=t,this.editor.mode.edit){e.tdDrag=document.createElement("td");var i=document.createElement("td");e.tdMenu=i;var o=document.createElement("button");o.className="contextmenu",o.title="Click to open the actions menu (Ctrl+M)",e.menu=o,i.appendChild(e.menu)}var n=document.createElement("td"),s=document.createElement("div");return s.innerHTML="(empty)",s.className="readonly",n.appendChild(s),e.td=n,e.text=s,this.updateDom(),t},n.prototype.updateDom=function(){var e=this.dom,t=e.td;t&&(t.style.paddingLeft=24*this.getLevel()+26+"px");var i=e.text;i&&(i.innerHTML="(empty "+this.parent.type+")");var o=e.tr;this.isVisible()?e.tr.firstChild||(e.tdDrag&&o.appendChild(e.tdDrag),e.tdMenu&&o.appendChild(e.tdMenu),o.appendChild(t)):e.tr.firstChild&&(e.tdDrag&&o.removeChild(e.tdDrag),e.tdMenu&&o.removeChild(e.tdMenu),o.removeChild(t))},n.prototype.isVisible=function(){return 0==this.parent.childs.length},n.prototype.showContextMenu=function(e,t){var i=this,n=o.TYPE_TITLES,r=[{text:"Append",title:"Append a new field with type 'auto' (Ctrl+Shift+Ins)",submenuTitle:"Select the type of the field to be appended",className:"insert",click:function(){i._onAppend("","","auto")},submenu:[{text:"Auto",className:"type-auto",title:n.auto,click:function(){i._onAppend("","","auto")}},{text:"Array",className:"type-array",title:n.array,click:function(){i._onAppend("",[])}},{text:"Object",className:"type-object",title:n.object,click:function(){i._onAppend("",{})}},{text:"String",className:"type-string",title:n.string,click:function(){i._onAppend("","","string")}}]}],a=new s(r,{close:t});a.show(e)},n.prototype.onEvent=function(e){var t=e.type,i=e.target||e.srcElement,o=this.dom,n=o.menu;if(i==n&&("mouseover"==t?this.editor.highlighter.highlight(this.parent):"mouseout"==t&&this.editor.highlighter.unhighlight()),"click"==t&&i==o.menu){var s=this.editor.highlighter;s.highlight(this.parent),s.lock(),util.addClassName(o.menu,"selected"),this.showContextMenu(o.menu,function(){util.removeClassName(o.menu,"selected"),s.unlock(),s.unhighlight()})}"keydown"==t&&this.onKeyDown(e)},s.prototype._getVisibleButtons=function(){var e=[],t=this;return this.dom.items.forEach(function(i){e.push(i.button),i.buttonExpand&&e.push(i.buttonExpand),i.subItems&&i==t.expandedItem&&i.subItems.forEach(function(t){e.push(t.button),t.buttonExpand&&e.push(t.buttonExpand)})}),e},s.visibleMenu=void 0,s.prototype.show=function(e){this.hide();var t=window.innerHeight,i=window.pageYOffset||document.scrollTop||0,o=t+i,n=e.offsetHeight,r=this.maxHeight,a=util.getAbsoluteLeft(e),l=util.getAbsoluteTop(e);o>l+n+r?(this.dom.menu.style.left=a+"px",this.dom.menu.style.top=l+n+"px",this.dom.menu.style.bottom=""):(this.dom.menu.style.left=a+"px",this.dom.menu.style.top="",this.dom.menu.style.bottom=t-l+"px"),document.body.appendChild(this.dom.menu);var d=this,h=this.dom.list;this.eventListeners.mousedown=util.addEventListener(document,"mousedown",function(e){var t=e.target;t==h||d._isChildOf(t,h)||(d.hide(),e.stopPropagation(),e.preventDefault())}),this.eventListeners.mousewheel=util.addEventListener(document,"mousewheel",function(e){e.stopPropagation(),e.preventDefault()}),this.eventListeners.keydown=util.addEventListener(document,"keydown",function(e){d._onKeyDown(e)}),this.selection=util.getSelection(),this.anchor=e,setTimeout(function(){d.dom.focusButton.focus()},0),s.visibleMenu&&s.visibleMenu.hide(),s.visibleMenu=this},s.prototype.hide=function(){this.dom.menu.parentNode&&(this.dom.menu.parentNode.removeChild(this.dom.menu),this.onClose&&this.onClose());for(var e in this.eventListeners)if(this.eventListeners.hasOwnProperty(e)){var t=this.eventListeners[e];t&&util.removeEventListener(document,e,t),delete this.eventListeners[e]}s.visibleMenu==this&&(s.visibleMenu=void 0)},s.prototype._onExpandItem=function(e){var t=this,i=e==this.expandedItem,o=this.expandedItem;if(o&&(o.ul.style.height="0",o.ul.style.padding="",setTimeout(function(){t.expandedItem!=o&&(o.ul.style.display="",util.removeClassName(o.ul.parentNode,"selected"))},300),this.expandedItem=void 0),!i){var n=e.ul;n.style.display="block";{n.clientHeight}setTimeout(function(){t.expandedItem==e&&(n.style.height=24*n.childNodes.length+"px",n.style.padding="5px 10px")},0),util.addClassName(n.parentNode,"selected"),this.expandedItem=e}},s.prototype._onKeyDown=function(e){var t,i,o,n,s=e.target,r=e.which,a=!1;27==r?(this.selection&&util.setSelection(this.selection),this.anchor&&this.anchor.focus(),this.hide(),a=!0):9==r?e.shiftKey?(t=this._getVisibleButtons(),i=t.indexOf(s),0==i&&(t[t.length-1].focus(),a=!0)):(t=this._getVisibleButtons(),i=t.indexOf(s),i==t.length-1&&(t[0].focus(),a=!0)):37==r?("expand"==s.className&&(t=this._getVisibleButtons(),i=t.indexOf(s),o=t[i-1],o&&o.focus()),a=!0):38==r?(t=this._getVisibleButtons(),i=t.indexOf(s),o=t[i-1],o&&"expand"==o.className&&(o=t[i-2]),o||(o=t[t.length-1]),o&&o.focus(),a=!0):39==r?(t=this._getVisibleButtons(),i=t.indexOf(s),n=t[i+1],n&&"expand"==n.className&&n.focus(),a=!0):40==r&&(t=this._getVisibleButtons(),i=t.indexOf(s),n=t[i+1],n&&"expand"==n.className&&(n=t[i+2]),n||(n=t[0]),n&&(n.focus(),a=!0),a=!0),a&&(e.stopPropagation(),e.preventDefault())},s.prototype._isChildOf=function(e,t){for(var i=e.parentNode;i;){if(i==t)return!0;i=i.parentNode}return!1},r.prototype.onChange=function(){},r.prototype.add=function(e,t){this.index++,this.history[this.index]={action:e,params:t,timestamp:new Date},this.index=0},r.prototype.canRedo=function(){return this.indexthis.results.length-1&&(t=0),this._setActiveResult(t,e)}},l.prototype.previous=function(e){if(void 0!=this.results){var t=this.results.length-1,i=void 0!=this.resultIndex?this.resultIndex-1:t;0>i&&(i=t),this._setActiveResult(i,e)}},l.prototype._setActiveResult=function(e,t){if(this.activeResult){var i=this.activeResult.node,o=this.activeResult.elem;"field"==o?delete i.searchFieldActive:delete i.searchValueActive,i.updateDom()}if(!this.results||!this.results[e])return this.resultIndex=void 0,void(this.activeResult=void 0);this.resultIndex=e;var n=this.results[this.resultIndex].node,s=this.results[this.resultIndex].elem;"field"==s?n.searchFieldActive=!0:n.searchValueActive=!0,this.activeResult=this.results[this.resultIndex],n.updateDom(),n.scrollTo(function(){t&&n.focus(s)})},l.prototype._clearDelay=function(){void 0!=this.timeout&&(clearTimeout(this.timeout),delete this.timeout)},l.prototype._onDelayedSearch=function(){this._clearDelay();var e=this;this.timeout=setTimeout(function(t){e._onSearch(t)},this.delay)},l.prototype._onSearch=function(e,t){this._clearDelay();var i=this.dom.search.value,o=i.length>0?i:void 0;if(o!=this.lastText||t)if(this.lastText=o,this.results=this.editor.search(o),this._setActiveResult(void 0),void 0!=o){var n=this.results.length;switch(n){case 0:this.dom.results.innerHTML="no results";break;case 1:this.dom.results.innerHTML="1 result";break;default:this.dom.results.innerHTML=n+" results"}}else this.dom.results.innerHTML=""},l.prototype._onKeyDown=function(e){var t=e.which;27==t?(this.dom.search.value="",this._onSearch(e),e.preventDefault(),e.stopPropagation()):13==t&&(e.ctrlKey?this._onSearch(e,!0):e.shiftKey?this.previous():this.next(),e.preventDefault(),e.stopPropagation())},l.prototype._onKeyUp=function(e){var t=e.keyCode;27!=t&&13!=t&&this._onDelayedSearch(e)},d.prototype.highlight=function(e){this.locked||(this.node!=e&&(this.node&&this.node.setHighlight(!1),this.node=e,this.node.setHighlight(!0)),this._cancelUnhighlight())},d.prototype.unhighlight=function(){if(!this.locked){var e=this;this.node&&(this._cancelUnhighlight(),this.unhighlightTimer=setTimeout(function(){e.node.setHighlight(!1),e.node=void 0,e.unhighlightTimer=void 0},0))}},d.prototype._cancelUnhighlight=function(){this.unhighlightTimer&&(clearTimeout(this.unhighlightTimer),this.unhighlightTimer=void 0)},d.prototype.lock=function(){this.locked=!0},d.prototype.unlock=function(){this.locked=!1},util={},util.parse=function(e){try{return JSON.parse(e)}catch(t){throw util.validate(e),t}},util.validate=function(e){"undefined"!=typeof jsonlint?jsonlint.parse(e):JSON.parse(e)},util.extend=function(e,t){for(var i in t)t.hasOwnProperty(i)&&(e[i]=t[i]);return e},util.clear=function(e){for(var t in e)e.hasOwnProperty(t)&&delete e[t];return e},util.log=function(){"undefined"!=typeof console&&"function"==typeof console.log&&console.log.apply(console,arguments)},util.type=function(e){return null===e?"null":void 0===e?"undefined":e instanceof Number||"number"==typeof e?"number":e instanceof String||"string"==typeof e?"string":e instanceof Boolean||"boolean"==typeof e?"boolean":e instanceof RegExp||"regexp"==typeof e?"regexp":Array.isArray(e)?"array":"object"};var h=/^https?:\/\/\S+$/;util.isUrl=function(e){return("string"==typeof e||e instanceof String)&&h.test(e)},util.getAbsoluteLeft=function(e){var t=e.getBoundingClientRect();return t.left+window.pageXOffset||document.scrollLeft||0},util.getAbsoluteTop=function(e){var t=e.getBoundingClientRect();return t.top+window.pageYOffset||document.scrollTop||0},util.addClassName=function(e,t){var i=e.className.split(" ");-1==i.indexOf(t)&&(i.push(t),e.className=i.join(" "))},util.removeClassName=function(e,t){var i=e.className.split(" "),o=i.indexOf(t);-1!=o&&(i.splice(o,1),e.className=i.join(" "))},util.stripFormatting=function(e){for(var t=e.childNodes,i=0,o=t.length;o>i;i++){var n=t[i];n.style&&n.removeAttribute("style");var s=n.attributes;if(s)for(var r=s.length-1;r>=0;r--){var a=s[r];1==a.specified&&n.removeAttribute(a.name)}util.stripFormatting(n)}},util.setEndOfContentEditable=function(e){var t,i;document.createRange&&(t=document.createRange(),t.selectNodeContents(e),t.collapse(!1),i=window.getSelection(),i.removeAllRanges(),i.addRange(t))},util.selectContentEditable=function(e){if(e&&"DIV"==e.nodeName){var t,i;window.getSelection&&document.createRange&&(i=document.createRange(),i.selectNodeContents(e),t=window.getSelection(),t.removeAllRanges(),t.addRange(i))}},util.getSelection=function(){if(window.getSelection){var e=window.getSelection();if(e.getRangeAt&&e.rangeCount)return e.getRangeAt(0)}return null},util.setSelection=function(e){if(e&&window.getSelection){var t=window.getSelection();t.removeAllRanges(),t.addRange(e)}},util.getSelectionOffset=function(){var e=util.getSelection();return e&&"startOffset"in e&&"endOffset"in e&&e.startContainer&&e.startContainer==e.endContainer?{startOffset:e.startOffset,endOffset:e.endOffset,container:e.startContainer.parentNode}:null},util.setSelectionOffset=function(e){if(document.createRange&&window.getSelection){var t=window.getSelection();if(t){var i=document.createRange();i.setStart(e.container.firstChild,e.startOffset),i.setEnd(e.container.firstChild,e.endOffset),util.setSelection(i)}}},util.getInnerText=function(e,t){var i=void 0==t;if(i&&(t={text:"",flush:function(){var e=this.text;return this.text="",e},set:function(e){this.text=e}}),e.nodeValue)return t.flush()+e.nodeValue;if(e.hasChildNodes()){for(var o=e.childNodes,n="",s=0,r=o.length;r>s;s++){var a=o[s];if("DIV"==a.nodeName||"P"==a.nodeName){var l=o[s-1],d=l?l.nodeName:void 0;d&&"DIV"!=d&&"P"!=d&&"BR"!=d&&(n+="\n",t.flush()),n+=util.getInnerText(a,t),t.set("\n")}else"BR"==a.nodeName?(n+=t.flush(),t.set("\n")):n+=util.getInnerText(a,t)}return n}return"P"==e.nodeName&&-1!=util.getInternetExplorerVersion()?t.flush():""},util.getInternetExplorerVersion=function(){if(-1==c){var e=-1;if("Microsoft Internet Explorer"==navigator.appName){var t=navigator.userAgent,i=new RegExp("MSIE ([0-9]{1,}[.0-9]{0,})");null!=i.exec(t)&&(e=parseFloat(RegExp.$1))}c=e}return c},util.isFirefox=function(){return-1!=navigator.userAgent.indexOf("Firefox")};var c=-1;util.addEventListener=function(e,t,i,o){if(e.addEventListener)return void 0===o&&(o=!1),"mousewheel"===t&&util.isFirefox()&&(t="DOMMouseScroll"),e.addEventListener(t,i,o),i;if(e.attachEvent){var n=function(){return i.call(e,window.event)};return e.attachEvent("on"+t,n),n}},util.removeEventListener=function(e,t,i,o){e.removeEventListener?(void 0===o&&(o=!1),"mousewheel"===t&&util.isFirefox()&&(t="DOMMouseScroll"),e.removeEventListener(t,i,o)):e.detachEvent&&e.detachEvent("on"+t,i)};var u={JSONEditor:e,JSONFormatter:function(){throw new Error('JSONFormatter is deprecated. Use JSONEditor with mode "text" or "code" instead')},util:util},p=function(){for(var e=document.getElementsByTagName("script"),t=0;ts)throw new Error("Unsupported browser, IE9 or newer required. Please install the newest version of your browser.");arguments.length&&this._create(e,t,n)}return o.modes={},o.prototype._create=function(e,t,i){this.container=e,this.options=t||{},this.json=i||{};var o=this.options.mode||"tree";this.setMode(o)},o.prototype._delete=function(){},o.prototype.set=function(e){this.json=e},o.prototype.get=function(){return this.json},o.prototype.setText=function(e){this.json=i.parse(e)},o.prototype.getText=function(){return JSON.stringify(this.json)},o.prototype.setName=function(e){this.options||(this.options={}),this.options.name=e},o.prototype.getName=function(){return this.options&&this.options.name},o.prototype.setMode=function(e){var t,n,s=this.container,r=i.extend({},this.options);r.mode=e;var a=o.modes[e];if(!a)throw new Error('Unknown mode "'+r.mode+'"');try{if("text"==a.data?(n=this.getName(),t=this.getText(),this._delete(),i.clear(this),i.extend(this,a.editor.prototype),this._create(s,r),this.setName(n),this.setText(t)):(n=this.getName(),t=this.get(),this._delete(),i.clear(this),i.extend(this,a.editor.prototype),this._create(s,r),this.setName(n),this.set(t)),"function"==typeof a.load)try{a.load.call(this)}catch(d){}}catch(d){this._onError(d)}},o.prototype._onError=function(e){if("function"==typeof this.onError&&(i.log("WARNING: JSONEditor.onError is deprecated. Use options.error instead."),this.onError(e)),!this.options||"function"!=typeof this.options.error)throw e;this.options.error(e)},o.registerModes=function(e){for(var t in e)if(e.hasOwnProperty(t)){if(t in o.modes)throw new Error('Mode "'+t+'" already registered');o.modes[t]=e[t]}},o.registerModes(e.modes),o.registerModes(t.modes),o}.apply(null,o),!(void 0!==n&&(e.exports=n))},function(e,t,i){var o,n;o=[i(4),i(5),i(6),i(7),i(8),i(3)],n=function(e,t,i,o,n,s){function r(e,t,i){if(!(this instanceof r))throw new Error('TreeEditor constructor called without "new".');this._create(e,t,i)}return r.prototype._create=function(i,o,n){if(!i)throw new Error("No container element provided.");this.container=i,this.dom={},this.highlighter=new e,this.selection=void 0,this._setOptions(o),this.options.history&&!this.mode.view&&(this.history=new t(this)),this._createFrame(),this._createTable(),this.set(n||{})},r.prototype._delete=function(){this.frame&&this.container&&this.frame.parentNode==this.container&&this.container.removeChild(this.frame)},r.prototype._setOptions=function(e){if(this.options={search:!0,history:!0,mode:"tree",name:void 0},e)for(var t in e)e.hasOwnProperty(t)&&(this.options[t]=e[t]);this.mode={edit:"view"!=this.options.mode&&"form"!=this.options.mode,view:"view"==this.options.mode,form:"form"==this.options.mode}},r.focusNode=void 0,r.prototype.set=function(e,t){if(t&&(s.log('Warning: second parameter "name" is deprecated. Use setName(name) instead.'),this.options.name=t),e instanceof Function||void 0===e)this.clear();else{this.content.removeChild(this.table);var i={field:this.options.name,value:e},n=new o(this,i);this._setRoot(n);var r=!1;this.node.expand(r),this.content.appendChild(this.table)}this.history&&this.history.clear()},r.prototype.get=function(){return r.focusNode&&r.focusNode.blur(),this.node?this.node.getValue():void 0},r.prototype.getText=function(){return JSON.stringify(this.get())},r.prototype.setText=function(e){this.set(s.parse(e))},r.prototype.setName=function(e){this.options.name=e,this.node&&this.node.updateField(this.options.name)},r.prototype.getName=function(){return this.options.name},r.prototype.clear=function(){this.node&&(this.node.collapse(),this.tbody.removeChild(this.node.getDom()),delete this.node)},r.prototype._setRoot=function(e){this.clear(),this.node=e,this.tbody.appendChild(e.getDom())},r.prototype.search=function(e){var t;return this.node?(this.content.removeChild(this.table),t=this.node.search(e),this.content.appendChild(this.table)):t=[],t},r.prototype.expandAll=function(){this.node&&(this.content.removeChild(this.table),this.node.expand(),this.content.appendChild(this.table))},r.prototype.collapseAll=function(){this.node&&(this.content.removeChild(this.table),this.node.collapse(),this.content.appendChild(this.table))},r.prototype._onAction=function(e,t){if(this.history&&this.history.add(e,t),this.options.change)try{this.options.change()}catch(i){s.log("Error in change callback: ",i)}},r.prototype.startAutoScroll=function(e){var t=this,i=this.content,o=s.getAbsoluteTop(i),n=i.clientHeight,r=o+n,a=24,d=50;this.autoScrollStep=o+a>e&&i.scrollTop>0?(o+a-e)/3:e>r-a&&n+i.scrollTop3?(i.scrollTop+=n/3,o.animateCallback=t,o.animateTimeout=setTimeout(a,50)):(t&&t(!0),i.scrollTop=r,delete o.animateTimeout,delete o.animateCallback)};a()}else t&&t(!1)},r.prototype._createFrame=function(){function e(e){t._onEvent(e)}this.frame=document.createElement("div"),this.frame.className="jsoneditor",this.container.appendChild(this.frame);var t=this;this.frame.onclick=function(t){var i=t.target;e(t),"BUTTON"==i.nodeName&&t.preventDefault()},this.frame.oninput=e,this.frame.onchange=e,this.frame.onkeydown=e,this.frame.onkeyup=e,this.frame.oncut=e,this.frame.onpaste=e,this.frame.onmousedown=e,this.frame.onmouseup=e,this.frame.onmouseover=e,this.frame.onmouseout=e,s.addEventListener(this.frame,"focus",e,!0),s.addEventListener(this.frame,"blur",e,!0),this.frame.onfocusin=e,this.frame.onfocusout=e,this.menu=document.createElement("div"),this.menu.className="menu",this.frame.appendChild(this.menu);var o=document.createElement("button");o.className="expand-all",o.title="Expand all fields",o.onclick=function(){t.expandAll()},this.menu.appendChild(o);var r=document.createElement("button");if(r.title="Collapse all fields",r.className="collapse-all",r.onclick=function(){t.collapseAll()},this.menu.appendChild(r),this.history){var a=document.createElement("button");a.className="undo separator",a.title="Undo last action (Ctrl+Z)",a.onclick=function(){t._onUndo()},this.menu.appendChild(a),this.dom.undo=a;var d=document.createElement("button");d.className="redo",d.title="Redo (Ctrl+Shift+Z)",d.onclick=function(){t._onRedo()},this.menu.appendChild(d),this.dom.redo=d,this.history.onChange=function(){a.disabled=!t.history.canUndo(),d.disabled=!t.history.canRedo()},this.history.onChange()}if(this.options&&this.options.modes&&this.options.modes.length){var h=n.create(this,this.options.modes,this.options.mode);this.menu.appendChild(h),this.dom.modeBox=h}this.options.search&&(this.searchBox=new i(this,this.menu))},r.prototype._onUndo=function(){this.history&&(this.history.undo(),this.options.change&&this.options.change())},r.prototype._onRedo=function(){this.history&&(this.history.redo(),this.options.change&&this.options.change())},r.prototype._onEvent=function(e){var t=e.target;"keydown"==e.type&&this._onKeyDown(e),"focus"==e.type&&(r.domFocus=t);var i=o.getNodeFromTarget(t);i&&i.onEvent(e)},r.prototype._onKeyDown=function(e){var t=e.which||e.keyCode,i=e.ctrlKey,o=e.shiftKey,n=!1;if(9==t&&setTimeout(function(){s.selectContentEditable(r.domFocus)},0),this.searchBox)if(i&&70==t)this.searchBox.dom.search.focus(),this.searchBox.dom.search.select(),n=!0;else if(114==t||i&&71==t){var a=!0;o?this.searchBox.previous(a):this.searchBox.next(a),n=!0}this.history&&(i&&!o&&90==t?(this._onUndo(),n=!0):i&&o&&90==t&&(this._onRedo(),n=!0)),n&&(e.preventDefault(),e.stopPropagation())},r.prototype._createTable=function(){var e=document.createElement("div");e.className="outer",this.contentOuter=e,this.content=document.createElement("div"),this.content.className="tree",e.appendChild(this.content),this.table=document.createElement("table"),this.table.className="tree",this.content.appendChild(this.table);var t;this.colgroupContent=document.createElement("colgroup"),this.mode.edit&&(t=document.createElement("col"),t.width="24px",this.colgroupContent.appendChild(t)),t=document.createElement("col"),t.width="24px",this.colgroupContent.appendChild(t),t=document.createElement("col"),this.colgroupContent.appendChild(t),this.table.appendChild(this.colgroupContent),this.tbody=document.createElement("tbody"),this.table.appendChild(this.tbody),this.frame.appendChild(e)},r.modes={tree:{editor:r,data:"json"},view:{editor:r,data:"json"},form:{editor:r,data:"json"}},r}.apply(null,o),!(void 0!==n&&(e.exports=n))},function(e,t,i){var o,n;o=[i(8),i(3)],n=function(e,t){function i(e,t,o){if(!(this instanceof i))throw new Error('TextEditor constructor called without "new".');this._create(e,t,o)}return i.prototype._create=function(i,o,n){o=o||{},this.options=o,this.indentation=o.indentation?Number(o.indentation):2,this.mode="code"==o.mode?"code":"text","code"==this.mode&&"undefined"==typeof ace&&(this.mode="text",t.log("WARNING: Cannot load code editor, Ace library not loaded. Falling back to plain text editor"));var s=this;this.container=i,this.dom={},this.editor=void 0,this.textarea=void 0,this.width=i.clientWidth,this.height=i.clientHeight,this.frame=document.createElement("div"),this.frame.className="jsoneditor",this.frame.onclick=function(e){e.preventDefault()},this.menu=document.createElement("div"),this.menu.className="menu",this.frame.appendChild(this.menu);var r=document.createElement("button");r.className="format",r.title="Format JSON data, with proper indentation and line feeds",this.menu.appendChild(r),r.onclick=function(){try{s.format()}catch(e){s._onError(e)}};var a=document.createElement("button");if(a.className="compact",a.title="Compact JSON data, remove all whitespaces",this.menu.appendChild(a),a.onclick=function(){try{s.compact()}catch(e){s._onError(e)}},this.options&&this.options.modes&&this.options.modes.length){var d=e.create(this,this.options.modes,this.options.mode);this.menu.appendChild(d),this.dom.modeBox=d}if(this.content=document.createElement("div"),this.content.className="outer",this.frame.appendChild(this.content),this.container.appendChild(this.frame),"code"==this.mode){this.editorDom=document.createElement("div"),this.editorDom.style.height="100%",this.editorDom.style.width="100%",this.content.appendChild(this.editorDom);var h=ace.edit(this.editorDom);h.setTheme("ace/theme/jsoneditor"),h.setShowPrintMargin(!1),h.setFontSize(13),h.getSession().setMode("ace/mode/json"),h.getSession().setTabSize(2),h.getSession().setUseSoftTabs(!0),h.getSession().setUseWrapMode(!0),this.editor=h;var l=document.createElement("a");l.appendChild(document.createTextNode("powered by ace")),l.href="http://ace.ajax.org",l.target="_blank",l.className="poweredBy",l.onclick=function(){window.open(l.href,l.target)},this.menu.appendChild(l),o.change&&h.on("change",function(){o.change()})}else{var c=document.createElement("textarea");c.className="text",c.spellcheck=!1,this.content.appendChild(c),this.textarea=c,o.change&&(null===this.textarea.oninput?this.textarea.oninput=function(){o.change()}:this.textarea.onchange=function(){o.change()})}"string"==typeof n?this.setText(n):this.set(n)},i.prototype._delete=function(){this.frame&&this.container&&this.frame.parentNode==this.container&&this.container.removeChild(this.frame)},i.prototype._onError=function(e){if("function"==typeof this.onError&&(t.log("WARNING: JSONEditor.onError is deprecated. Use options.error instead."),this.onError(e)),!this.options||"function"!=typeof this.options.error)throw e;this.options.error(e)},i.prototype.compact=function(){var e=t.parse(this.getText());this.setText(JSON.stringify(e))},i.prototype.format=function(){var e=t.parse(this.getText());this.setText(JSON.stringify(e,null,this.indentation))},i.prototype.focus=function(){this.textarea&&this.textarea.focus(),this.editor&&this.editor.focus()},i.prototype.resize=function(){if(this.editor){var e=!1;this.editor.resize(e)}},i.prototype.set=function(e){this.setText(JSON.stringify(e,null,this.indentation))},i.prototype.get=function(){return t.parse(this.getText())},i.prototype.getText=function(){return this.textarea?this.textarea.value:this.editor?this.editor.getValue():""},i.prototype.setText=function(e){this.textarea&&(this.textarea.value=e),this.editor&&this.editor.setValue(e,-1)},i.modes={text:{editor:i,data:"text",load:i.prototype.format},code:{editor:i,data:"text",load:i.prototype.format}},i}.apply(null,o),!(void 0!==n&&(e.exports=n))},function(e,t,i){var o;o=function(){var e={};e.parse=function(t){try{return JSON.parse(t)}catch(i){throw e.validate(t),i}},e.validate=function(e){"undefined"!=typeof jsonlint?jsonlint.parse(e):JSON.parse(e)},e.extend=function(e,t){for(var i in t)t.hasOwnProperty(i)&&(e[i]=t[i]);return e},e.clear=function(e){for(var t in e)e.hasOwnProperty(t)&&delete e[t];return e},e.log=function(){"undefined"!=typeof console&&"function"==typeof console.log&&console.log.apply(console,arguments)},e.type=function(e){return null===e?"null":void 0===e?"undefined":e instanceof Number||"number"==typeof e?"number":e instanceof String||"string"==typeof e?"string":e instanceof Boolean||"boolean"==typeof e?"boolean":e instanceof RegExp||"regexp"==typeof e?"regexp":Array.isArray(e)?"array":"object"};var t=/^https?:\/\/\S+$/;e.isUrl=function(e){return("string"==typeof e||e instanceof String)&&t.test(e)},e.getAbsoluteLeft=function(e){var t=e.getBoundingClientRect();return t.left+window.pageXOffset||document.scrollLeft||0},e.getAbsoluteTop=function(e){var t=e.getBoundingClientRect();return t.top+window.pageYOffset||document.scrollTop||0},e.addClassName=function(e,t){var i=e.className.split(" ");-1==i.indexOf(t)&&(i.push(t),e.className=i.join(" "))},e.removeClassName=function(e,t){var i=e.className.split(" "),o=i.indexOf(t);-1!=o&&(i.splice(o,1),e.className=i.join(" "))},e.stripFormatting=function(t){for(var i=t.childNodes,o=0,n=i.length;n>o;o++){var s=i[o];s.style&&s.removeAttribute("style");var r=s.attributes;if(r)for(var a=r.length-1;a>=0;a--){var d=r[a];1==d.specified&&s.removeAttribute(d.name)}e.stripFormatting(s)}},e.setEndOfContentEditable=function(e){var t,i;document.createRange&&(t=document.createRange(),t.selectNodeContents(e),t.collapse(!1),i=window.getSelection(),i.removeAllRanges(),i.addRange(t))},e.selectContentEditable=function(e){if(e&&"DIV"==e.nodeName){var t,i;window.getSelection&&document.createRange&&(i=document.createRange(),i.selectNodeContents(e),t=window.getSelection(),t.removeAllRanges(),t.addRange(i))}},e.getSelection=function(){if(window.getSelection){var e=window.getSelection();if(e.getRangeAt&&e.rangeCount)return e.getRangeAt(0)}return null},e.setSelection=function(e){if(e&&window.getSelection){var t=window.getSelection();t.removeAllRanges(),t.addRange(e)}},e.getSelectionOffset=function(){var t=e.getSelection();return t&&"startOffset"in t&&"endOffset"in t&&t.startContainer&&t.startContainer==t.endContainer?{startOffset:t.startOffset,endOffset:t.endOffset,container:t.startContainer.parentNode}:null},e.setSelectionOffset=function(t){if(document.createRange&&window.getSelection){var i=window.getSelection();if(i){var o=document.createRange();o.setStart(t.container.firstChild,t.startOffset),o.setEnd(t.container.firstChild,t.endOffset),e.setSelection(o)}}},e.getInnerText=function(t,i){var o=void 0==i;if(o&&(i={text:"",flush:function(){var e=this.text;return this.text="",e},set:function(e){this.text=e}}),t.nodeValue)return i.flush()+t.nodeValue;if(t.hasChildNodes()){for(var n=t.childNodes,s="",r=0,a=n.length;a>r;r++){var d=n[r];if("DIV"==d.nodeName||"P"==d.nodeName){var h=n[r-1],l=h?h.nodeName:void 0;l&&"DIV"!=l&&"P"!=l&&"BR"!=l&&(s+="\n",i.flush()),s+=e.getInnerText(d,i),i.set("\n")}else"BR"==d.nodeName?(s+=i.flush(),i.set("\n")):s+=e.getInnerText(d,i)}return s}return"P"==t.nodeName&&-1!=e.getInternetExplorerVersion()?i.flush():""},e.getInternetExplorerVersion=function(){if(-1==i){var e=-1;if("Microsoft Internet Explorer"==navigator.appName){var t=navigator.userAgent,o=new RegExp("MSIE ([0-9]{1,}[.0-9]{0,})");null!=o.exec(t)&&(e=parseFloat(RegExp.$1))}i=e}return i},e.isFirefox=function(){return-1!=navigator.userAgent.indexOf("Firefox")};var i=-1;return e.addEventListener=function(t,i,o,n){if(t.addEventListener)return void 0===n&&(n=!1),"mousewheel"===i&&e.isFirefox()&&(i="DOMMouseScroll"),t.addEventListener(i,o,n),o;if(t.attachEvent){var s=function(){return o.call(t,window.event)};return t.attachEvent("on"+i,s),s}},e.removeEventListener=function(t,i,o,n){t.removeEventListener?(void 0===n&&(n=!1),"mousewheel"===i&&e.isFirefox()&&(i="DOMMouseScroll"),t.removeEventListener(i,o,n)):t.detachEvent&&t.detachEvent("on"+i,o)},e}.call(t,i,t,e),!(void 0!==o&&(e.exports=o))},function(e,t,i){var o;o=function(){function e(){this.locked=!1}return e.prototype.highlight=function(e){this.locked||(this.node!=e&&(this.node&&this.node.setHighlight(!1),this.node=e,this.node.setHighlight(!0)),this._cancelUnhighlight())},e.prototype.unhighlight=function(){if(!this.locked){var e=this;this.node&&(this._cancelUnhighlight(),this.unhighlightTimer=setTimeout(function(){e.node.setHighlight(!1),e.node=void 0,e.unhighlightTimer=void 0},0))}},e.prototype._cancelUnhighlight=function(){this.unhighlightTimer&&(clearTimeout(this.unhighlightTimer),this.unhighlightTimer=void 0)},e.prototype.lock=function(){this.locked=!0},e.prototype.unlock=function(){this.locked=!1},e}.call(t,i,t,e),!(void 0!==o&&(e.exports=o))},function(e,t,i){var o,n;o=[i(3)],n=function(e){function t(e){this.editor=e,this.clear(),this.actions={editField:{undo:function(e){e.node.updateField(e.oldValue)},redo:function(e){e.node.updateField(e.newValue)}},editValue:{undo:function(e){e.node.updateValue(e.oldValue)},redo:function(e){e.node.updateValue(e.newValue)}},appendNode:{undo:function(e){e.parent.removeChild(e.node)},redo:function(e){e.parent.appendChild(e.node)}},insertBeforeNode:{undo:function(e){e.parent.removeChild(e.node)},redo:function(e){e.parent.insertBefore(e.node,e.beforeNode)}},insertAfterNode:{undo:function(e){e.parent.removeChild(e.node)},redo:function(e){e.parent.insertAfter(e.node,e.afterNode)}},removeNode:{undo:function(e){var t=e.parent,i=t.childs[e.index]||t.append;t.insertBefore(e.node,i)},redo:function(e){e.parent.removeChild(e.node)}},duplicateNode:{undo:function(e){e.parent.removeChild(e.clone)},redo:function(e){e.parent.insertAfter(e.clone,e.node)}},changeType:{undo:function(e){e.node.changeType(e.oldType)},redo:function(e){e.node.changeType(e.newType)}},moveNode:{undo:function(e){e.startParent.moveTo(e.node,e.startIndex)},redo:function(e){e.endParent.moveTo(e.node,e.endIndex)}},sort:{undo:function(e){var t=e.node;t.hideChilds(),t.sort=e.oldSort,t.childs=e.oldChilds,t.showChilds()},redo:function(e){var t=e.node;t.hideChilds(),t.sort=e.newSort,t.childs=e.newChilds,t.showChilds()}}}}return t.prototype.onChange=function(){},t.prototype.add=function(e,t){this.index++,this.history[this.index]={action:e,params:t,timestamp:new Date},this.index=0},t.prototype.canRedo=function(){return this.indexthis.results.length-1&&(t=0),this._setActiveResult(t,e)}},e.prototype.previous=function(e){if(void 0!=this.results){var t=this.results.length-1,i=void 0!=this.resultIndex?this.resultIndex-1:t;0>i&&(i=t),this._setActiveResult(i,e)}},e.prototype._setActiveResult=function(e,t){if(this.activeResult){var i=this.activeResult.node,o=this.activeResult.elem;"field"==o?delete i.searchFieldActive:delete i.searchValueActive,i.updateDom()}if(!this.results||!this.results[e])return this.resultIndex=void 0,void(this.activeResult=void 0);this.resultIndex=e;var n=this.results[this.resultIndex].node,s=this.results[this.resultIndex].elem;"field"==s?n.searchFieldActive=!0:n.searchValueActive=!0,this.activeResult=this.results[this.resultIndex],n.updateDom(),n.scrollTo(function(){t&&n.focus(s)})},e.prototype._clearDelay=function(){void 0!=this.timeout&&(clearTimeout(this.timeout),delete this.timeout)},e.prototype._onDelayedSearch=function(){this._clearDelay();var e=this;this.timeout=setTimeout(function(t){e._onSearch(t)},this.delay)},e.prototype._onSearch=function(e,t){this._clearDelay();var i=this.dom.search.value,o=i.length>0?i:void 0;if(o!=this.lastText||t)if(this.lastText=o,this.results=this.editor.search(o),this._setActiveResult(void 0),void 0!=o){var n=this.results.length;switch(n){case 0:this.dom.results.innerHTML="no results";break;case 1:this.dom.results.innerHTML="1 result";break;default:this.dom.results.innerHTML=n+" results"}}else this.dom.results.innerHTML=""},e.prototype._onKeyDown=function(e){var t=e.which;27==t?(this.dom.search.value="",this._onSearch(e),e.preventDefault(),e.stopPropagation()):13==t&&(e.ctrlKey?this._onSearch(e,!0):e.shiftKey?this.previous():this.next(),e.preventDefault(),e.stopPropagation())},e.prototype._onKeyUp=function(e){var t=e.keyCode;27!=t&&13!=t&&this._onDelayedSearch(e)},e}.call(t,i,t,e),!(void 0!==o&&(e.exports=o))},function(e,t,i){var o,n;o=[i(9),i(10),i(3)],n=function(e,t,i){function o(e,t){this.editor=e,this.dom={},this.expanded=!1,t&&t instanceof Object?(this.setField(t.field,t.fieldEditable),this.setValue(t.value,t.type)):(this.setField(""),this.setValue(null))}o.prototype.setParent=function(e){this.parent=e},o.prototype.setField=function(e,t){this.field=e,this.fieldEditable=1==t},o.prototype.getField=function(){return void 0===this.field&&this._getDomField(),this.field},o.prototype.setValue=function(e,t){var i,n,s=this.childs;if(s)for(;s.length;)this.removeChild(s[0]);if(this.type=this._getType(e),t&&t!=this.type){if("string"!=t||"auto"!=this.type)throw new Error('Type mismatch: cannot cast value of type "'+this.type+' to the specified type "'+t+'"');this.type=t}if("array"==this.type){this.childs=[];for(var r=0,a=e.length;a>r;r++)i=e[r],void 0===i||i instanceof Function||(n=new o(this.editor,{value:i}),this.appendChild(n));this.value=""}else if("object"==this.type){this.childs=[];for(var d in e)e.hasOwnProperty(d)&&(i=e[d],void 0===i||i instanceof Function||(n=new o(this.editor,{field:d,value:i}),this.appendChild(n)));this.value=""}else this.childs=void 0,this.value=e},o.prototype.getValue=function(){if("array"==this.type){var e=[];return this.childs.forEach(function(t){e.push(t.getValue())}),e}if("object"==this.type){var t={};return this.childs.forEach(function(e){t[e.getField()]=e.getValue()}),t}return void 0===this.value&&this._getDomValue(),this.value},o.prototype.getLevel=function(){return this.parent?this.parent.getLevel()+1:0},o.prototype.clone=function(){var e=new o(this.editor);if(e.type=this.type,e.field=this.field,e.fieldInnerText=this.fieldInnerText,e.fieldEditable=this.fieldEditable,e.value=this.value,e.valueInnerText=this.valueInnerText,e.expanded=this.expanded,this.childs){var t=[];this.childs.forEach(function(i){var o=i.clone();o.setParent(e),t.push(o)}),e.childs=t}else e.childs=void 0;return e},o.prototype.expand=function(e){this.childs&&(this.expanded=!0,this.dom.expand&&(this.dom.expand.className="expanded"),this.showChilds(),0!=e&&this.childs.forEach(function(t){t.expand(e)}))},o.prototype.collapse=function(e){this.childs&&(this.hideChilds(),0!=e&&this.childs.forEach(function(t){t.collapse(e)}),this.dom.expand&&(this.dom.expand.className="collapsed"),this.expanded=!1)},o.prototype.showChilds=function(){var e=this.childs;if(e&&this.expanded){var t=this.dom.tr,i=t?t.parentNode:void 0;if(i){var o=this.getAppend(),n=t.nextSibling;n?i.insertBefore(o,n):i.appendChild(o),this.childs.forEach(function(e){i.insertBefore(e.getDom(),o),e.showChilds()})}}},o.prototype.hide=function(){var e=this.dom.tr,t=e?e.parentNode:void 0;t&&t.removeChild(e),this.hideChilds()},o.prototype.hideChilds=function(){var e=this.childs;if(e&&this.expanded){var t=this.getAppend();t.parentNode&&t.parentNode.removeChild(t),this.childs.forEach(function(e){e.hide()})}},o.prototype.appendChild=function(e){if(this._hasChilds()){if(e.setParent(this),e.fieldEditable="object"==this.type,"array"==this.type&&(e.index=this.childs.length),this.childs.push(e),this.expanded){var t=e.getDom(),i=this.getAppend(),o=i?i.parentNode:void 0;i&&o&&o.insertBefore(t,i),e.showChilds()}this.updateDom({updateIndexes:!0}),e.updateDom({recurse:!0})}},o.prototype.moveBefore=function(e,t){if(this._hasChilds()){var i=this.dom.tr?this.dom.tr.parentNode:void 0;if(i){var o=document.createElement("tr");o.style.height=i.clientHeight+"px",i.appendChild(o)}e.parent&&e.parent.removeChild(e),t instanceof n?this.appendChild(e):this.insertBefore(e,t),i&&i.removeChild(o)}},o.prototype.moveTo=function(e,t){if(e.parent==this){var i=this.childs.indexOf(e);t>i&&t++}var o=this.childs[t]||this.append;this.moveBefore(e,o)},o.prototype.insertBefore=function(e,t){if(this._hasChilds()){if(t==this.append)e.setParent(this),e.fieldEditable="object"==this.type,this.childs.push(e);else{var i=this.childs.indexOf(t);if(-1==i)throw new Error("Node not found");e.setParent(this),e.fieldEditable="object"==this.type,this.childs.splice(i,0,e)}if(this.expanded){var o=e.getDom(),n=t.getDom(),s=n?n.parentNode:void 0;n&&s&&s.insertBefore(o,n),e.showChilds()}this.updateDom({updateIndexes:!0}),e.updateDom({recurse:!0})}},o.prototype.insertAfter=function(e,t){if(this._hasChilds()){var i=this.childs.indexOf(t),o=this.childs[i+1];o?this.insertBefore(e,o):this.appendChild(e)}},o.prototype.search=function(e){var t,i=[],o=e?e.toLowerCase():void 0;if(delete this.searchField,delete this.searchValue,void 0!=this.field){var n=String(this.field).toLowerCase();t=n.indexOf(o),-1!=t&&(this.searchField=!0,i.push({node:this,elem:"field"})),this._updateDomField()}if(this._hasChilds()){if(this.childs){var s=[];this.childs.forEach(function(t){s=s.concat(t.search(e))}),i=i.concat(s)}if(void 0!=o){var r=!1;0==s.length?this.collapse(r):this.expand(r)}}else{if(void 0!=this.value){var a=String(this.value).toLowerCase();t=a.indexOf(o),-1!=t&&(this.searchValue=!0,i.push({node:this,elem:"value"}))}this._updateDomValue()}return i},o.prototype.scrollTo=function(e){if(!this.dom.tr||!this.dom.tr.parentNode)for(var t=this.parent,i=!1;t;)t.expand(i),t=t.parent;this.dom.tr&&this.dom.tr.parentNode&&this.editor.scrollTo(this.dom.tr.offsetTop,e)},o.focusElement=void 0,o.prototype.focus=function(e){if(o.focusElement=e,this.dom.tr&&this.dom.tr.parentNode){var t=this.dom;switch(e){case"drag":t.drag?t.drag.focus():t.menu.focus();break;case"menu":t.menu.focus();break;case"expand":this._hasChilds()?t.expand.focus():t.field&&this.fieldEditable?(t.field.focus(),i.selectContentEditable(t.field)):t.value&&!this._hasChilds()?(t.value.focus(),i.selectContentEditable(t.value)):t.menu.focus();break;case"field":t.field&&this.fieldEditable?(t.field.focus(),i.selectContentEditable(t.field)):t.value&&!this._hasChilds()?(t.value.focus(),i.selectContentEditable(t.value)):this._hasChilds()?t.expand.focus():t.menu.focus();break;case"value":default:t.value&&!this._hasChilds()?(t.value.focus(),i.selectContentEditable(t.value)):t.field&&this.fieldEditable?(t.field.focus(),i.selectContentEditable(t.field)):this._hasChilds()?t.expand.focus():t.menu.focus()}}},o.select=function(e){setTimeout(function(){i.selectContentEditable(e)},0)},o.prototype.blur=function(){this._getDomValue(!1),this._getDomField(!1)},o.prototype._duplicate=function(e){var t=e.clone();return this.insertAfter(t,e),t},o.prototype.containsNode=function(e){if(this==e)return!0;var t=this.childs;if(t)for(var i=0,o=t.length;o>i;i++)if(t[i].containsNode(e))return!0;return!1},o.prototype._move=function(e,t){if(e!=t){if(e.containsNode(this))throw new Error("Cannot move a field into a child of itself");e.parent&&e.parent.removeChild(e);var i=e.clone();e.clearDom(),t?this.insertBefore(i,t):this.appendChild(i)}},o.prototype.removeChild=function(e){if(this.childs){var t=this.childs.indexOf(e);if(-1!=t){e.hide(),delete e.searchField,delete e.searchValue;var i=this.childs.splice(t,1)[0];return this.updateDom({updateIndexes:!0}),i}}return void 0},o.prototype._remove=function(e){this.removeChild(e)
+},o.prototype.changeType=function(e){var t=this.type;if(t!=e){if("string"!=e&&"auto"!=e||"string"!=t&&"auto"!=t){var i,o=this.dom.tr?this.dom.tr.parentNode:void 0;i=this.expanded?this.getAppend():this.getDom();var n=i&&i.parentNode?i.nextSibling:void 0;this.hide(),this.clearDom(),this.type=e,"object"==e?(this.childs||(this.childs=[]),this.childs.forEach(function(e){e.clearDom(),delete e.index,e.fieldEditable=!0,void 0==e.field&&(e.field="")}),("string"==t||"auto"==t)&&(this.expanded=!0)):"array"==e?(this.childs||(this.childs=[]),this.childs.forEach(function(e,t){e.clearDom(),e.fieldEditable=!1,e.index=t}),("string"==t||"auto"==t)&&(this.expanded=!0)):this.expanded=!1,o&&(n?o.insertBefore(this.getDom(),n):o.appendChild(this.getDom())),this.showChilds()}else this.type=e;("auto"==e||"string"==e)&&(this.value="string"==e?String(this.value):this._stringCast(String(this.value)),this.focus()),this.updateDom({updateIndexes:!0})}},o.prototype._getDomValue=function(e){if(this.dom.value&&"array"!=this.type&&"object"!=this.type&&(this.valueInnerText=i.getInnerText(this.dom.value)),void 0!=this.valueInnerText)try{var t;if("string"==this.type)t=this._unescapeHTML(this.valueInnerText);else{var o=this._unescapeHTML(this.valueInnerText);t=this._stringCast(o)}if(t!==this.value){var n=this.value;this.value=t,this.editor._onAction("editValue",{node:this,oldValue:n,newValue:t,oldSelection:this.editor.selection,newSelection:this.editor.getSelection()})}}catch(s){if(this.value=void 0,1!=e)throw s}},o.prototype._updateDomValue=function(){var e=this.dom.value;if(e){var t=this.value,o="auto"==this.type?i.type(t):this.type,n="string"==o&&i.isUrl(t),s="";s=n&&!this.editor.mode.edit?"":"string"==o?"green":"number"==o?"red":"boolean"==o?"darkorange":this._hasChilds()?"":null===t?"#004ED0":"black",e.style.color=s;var r=""==String(this.value)&&"array"!=this.type&&"object"!=this.type;if(r?i.addClassName(e,"empty"):i.removeClassName(e,"empty"),n?i.addClassName(e,"url"):i.removeClassName(e,"url"),"array"==o||"object"==o){var a=this.childs?this.childs.length:0;e.title=this.type+" containing "+a+" items"}else"string"==o&&i.isUrl(t)?this.editor.mode.edit&&(e.title="Ctrl+Click or Ctrl+Enter to open url in new window"):e.title="";this.searchValueActive?i.addClassName(e,"highlight-active"):i.removeClassName(e,"highlight-active"),this.searchValue?i.addClassName(e,"highlight"):i.removeClassName(e,"highlight"),i.stripFormatting(e)}},o.prototype._updateDomField=function(){var e=this.dom.field;if(e){var t=""==String(this.field)&&"array"!=this.parent.type;t?i.addClassName(e,"empty"):i.removeClassName(e,"empty"),this.searchFieldActive?i.addClassName(e,"highlight-active"):i.removeClassName(e,"highlight-active"),this.searchField?i.addClassName(e,"highlight"):i.removeClassName(e,"highlight"),i.stripFormatting(e)}},o.prototype._getDomField=function(e){if(this.dom.field&&this.fieldEditable&&(this.fieldInnerText=i.getInnerText(this.dom.field)),void 0!=this.fieldInnerText)try{var t=this._unescapeHTML(this.fieldInnerText);if(t!==this.field){var o=this.field;this.field=t,this.editor._onAction("editField",{node:this,oldValue:o,newValue:t,oldSelection:this.editor.selection,newSelection:this.editor.getSelection()})}}catch(n){if(this.field=void 0,1!=e)throw n}},o.prototype.clearDom=function(){this.dom={}},o.prototype.getDom=function(){var e=this.dom;if(e.tr)return e.tr;if(e.tr=document.createElement("tr"),e.tr.node=this,this.editor.mode.edit){var t=document.createElement("td");if(this.parent){var i=document.createElement("button");e.drag=i,i.className="dragarea",i.title="Drag to move this field (Alt+Shift+Arrows)",t.appendChild(i)}e.tr.appendChild(t);var o=document.createElement("td"),n=document.createElement("button");e.menu=n,n.className="contextmenu",n.title="Click to open the actions menu (Ctrl+M)",o.appendChild(e.menu),e.tr.appendChild(o)}var s=document.createElement("td");return e.tr.appendChild(s),e.tree=this._createDomTree(),s.appendChild(e.tree),this.updateDom({updateIndexes:!0}),e.tr},o.prototype._onDragStart=function(e){var t=this;this.mousemove||(this.mousemove=i.addEventListener(document,"mousemove",function(e){t._onDrag(e)})),this.mouseup||(this.mouseup=i.addEventListener(document,"mouseup",function(e){t._onDragEnd(e)})),this.editor.highlighter.lock(),this.drag={oldCursor:document.body.style.cursor,startParent:this.parent,startIndex:this.parent.childs.indexOf(this),mouseX:e.pageX,level:this.getLevel()},document.body.style.cursor="move",e.preventDefault()},o.prototype._onDrag=function(e){var t,s,r,a,d,h,l,c,u,p,f,m,v,g,y=e.pageY,x=e.pageX,C=!1;if(t=this.dom.tr,u=i.getAbsoluteTop(t),m=t.offsetHeight,u>y){s=t;do s=s.previousSibling,l=o.getNodeFromTarget(s),p=s?i.getAbsoluteTop(s):0;while(s&&p>y);l&&!l.parent&&(l=void 0),l||(h=t.parentNode.firstChild,s=h?h.nextSibling:void 0,l=o.getNodeFromTarget(s),l==this&&(l=void 0)),l&&(s=l.dom.tr,p=s?i.getAbsoluteTop(s):0,y>p+m&&(l=void 0)),l&&(l.parent.moveBefore(this,l),C=!0)}else if(d=this.expanded&&this.append?this.append.getDom():this.dom.tr,a=d?d.nextSibling:void 0){f=i.getAbsoluteTop(a),r=a;do c=o.getNodeFromTarget(r),r&&(v=r.nextSibling?i.getAbsoluteTop(r.nextSibling):0,g=r?v-f:0,1==c.parent.childs.length&&c.parent.childs[0]==this&&(u+=23)),r=r.nextSibling;while(r&&y>u+g);if(c&&c.parent){var b=x-this.drag.mouseX,N=Math.round(b/24/2),E=this.drag.level+N,_=c.getLevel();for(s=c.dom.tr.previousSibling;E>_&&s;){if(l=o.getNodeFromTarget(s),l==this||l._isChildOf(this));else{if(!(l instanceof n))break;var w=l.parent.childs;if(!(w.length>1||1==w.length&&w[0]!=this))break;c=o.getNodeFromTarget(s),_=c.getLevel()}s=s.previousSibling}d.nextSibling!=c.dom.tr&&(c.parent.moveBefore(this,c),C=!0)}}C&&(this.drag.mouseX=x,this.drag.level=this.getLevel()),this.editor.startAutoScroll(y),e.preventDefault()},o.prototype._onDragEnd=function(e){var t={node:this,startParent:this.drag.startParent,startIndex:this.drag.startIndex,endParent:this.parent,endIndex:this.parent.childs.indexOf(this)};(t.startParent!=t.endParent||t.startIndex!=t.endIndex)&&this.editor._onAction("moveNode",t),document.body.style.cursor=this.drag.oldCursor,this.editor.highlighter.unlock(),delete this.drag,this.mousemove&&(i.removeEventListener(document,"mousemove",this.mousemove),delete this.mousemove),this.mouseup&&(i.removeEventListener(document,"mouseup",this.mouseup),delete this.mouseup),this.editor.stopAutoScroll(),e.preventDefault()},o.prototype._isChildOf=function(e){for(var t=this.parent;t;){if(t==e)return!0;t=t.parent}return!1},o.prototype._createDomField=function(){return document.createElement("div")},o.prototype.setHighlight=function(e){this.dom.tr&&(this.dom.tr.className=e?"highlight":"",this.append&&this.append.setHighlight(e),this.childs&&this.childs.forEach(function(t){t.setHighlight(e)}))},o.prototype.updateValue=function(e){this.value=e,this.updateDom()},o.prototype.updateField=function(e){this.field=e,this.updateDom()},o.prototype.updateDom=function(e){var t=this.dom.tree;t&&(t.style.marginLeft=24*this.getLevel()+"px");var i=this.dom.field;if(i){1==this.fieldEditable?(i.contentEditable=this.editor.mode.edit,i.spellcheck=!1,i.className="field"):i.className="readonly";var o;o=void 0!=this.index?this.index:void 0!=this.field?this.field:this._hasChilds()?this.type:"",i.innerHTML=this._escapeHTML(o)}var n=this.dom.value;if(n){var s=this.childs?this.childs.length:0;n.innerHTML="array"==this.type?"["+s+"]":"object"==this.type?"{"+s+"}":this._escapeHTML(this.value)}this._updateDomField(),this._updateDomValue(),e&&1==e.updateIndexes&&this._updateDomIndexes(),e&&1==e.recurse&&this.childs&&this.childs.forEach(function(t){t.updateDom(e)}),this.append&&this.append.updateDom()},o.prototype._updateDomIndexes=function(){var e=this.dom.value,t=this.childs;e&&t&&("array"==this.type?t.forEach(function(e,t){e.index=t;var i=e.dom.field;i&&(i.innerHTML=t)}):"object"==this.type&&t.forEach(function(e){void 0!=e.index&&(delete e.index,void 0==e.field&&(e.field=""))}))},o.prototype._createDomValue=function(){var e;return"array"==this.type?(e=document.createElement("div"),e.className="readonly",e.innerHTML="[...]"):"object"==this.type?(e=document.createElement("div"),e.className="readonly",e.innerHTML="{...}"):!this.editor.mode.edit&&i.isUrl(this.value)?(e=document.createElement("a"),e.className="value",e.href=this.value,e.target="_blank",e.innerHTML=this._escapeHTML(this.value)):(e=document.createElement("div"),e.contentEditable=!this.editor.mode.view,e.spellcheck=!1,e.className="value",e.innerHTML=this._escapeHTML(this.value)),e},o.prototype._createDomExpandButton=function(){var e=document.createElement("button");return this._hasChilds()?(e.className=this.expanded?"expanded":"collapsed",e.title="Click to expand/collapse this field (Ctrl+E). \nCtrl+Click to expand/collapse including all childs."):(e.className="invisible",e.title=""),e},o.prototype._createDomTree=function(){var e=this.dom,t=document.createElement("table"),i=document.createElement("tbody");t.style.borderCollapse="collapse",t.className="values",t.appendChild(i);var o=document.createElement("tr");i.appendChild(o);var n=document.createElement("td");n.className="tree",o.appendChild(n),e.expand=this._createDomExpandButton(),n.appendChild(e.expand),e.tdExpand=n;var s=document.createElement("td");s.className="tree",o.appendChild(s),e.field=this._createDomField(),s.appendChild(e.field),e.tdField=s;var r=document.createElement("td");r.className="tree",o.appendChild(r),"object"!=this.type&&"array"!=this.type&&(r.appendChild(document.createTextNode(":")),r.className="separator"),e.tdSeparator=r;var a=document.createElement("td");return a.className="tree",o.appendChild(a),e.value=this._createDomValue(),a.appendChild(e.value),e.tdValue=a,t},o.prototype.onEvent=function(e){var t,o=e.type,n=e.target||e.srcElement,s=this.dom,r=this,a=this._hasChilds();if((n==s.drag||n==s.menu)&&("mouseover"==o?this.editor.highlighter.highlight(this):"mouseout"==o&&this.editor.highlighter.unhighlight()),"mousedown"==o&&n==s.drag&&this._onDragStart(e),"click"==o&&n==s.menu){var d=r.editor.highlighter;d.highlight(r),d.lock(),i.addClassName(s.menu,"selected"),this.showContextMenu(s.menu,function(){i.removeClassName(s.menu,"selected"),d.unlock(),d.unhighlight()})}if("click"==o&&n==s.expand&&a){var h=e.ctrlKey;this._onExpand(h)}var l=s.value;if(n==l)switch(o){case"focus":t=this;break;case"blur":case"change":this._getDomValue(!0),this._updateDomValue(),this.value&&(l.innerHTML=this._escapeHTML(this.value));break;case"input":this._getDomValue(!0),this._updateDomValue();break;case"keydown":case"mousedown":this.editor.selection=this.editor.getSelection();break;case"click":e.ctrlKey&&this.editor.mode.edit&&i.isUrl(this.value)&&window.open(this.value,"_blank");break;case"keyup":this._getDomValue(!0),this._updateDomValue();break;case"cut":case"paste":setTimeout(function(){r._getDomValue(!0),r._updateDomValue()},1)}var c=s.field;if(n==c)switch(o){case"focus":t=this;break;case"blur":case"change":this._getDomField(!0),this._updateDomField(),this.field&&(c.innerHTML=this._escapeHTML(this.field));break;case"input":this._getDomField(!0),this._updateDomField();break;case"keydown":case"mousedown":this.editor.selection=this.editor.getSelection();break;case"keyup":this._getDomField(!0),this._updateDomField();break;case"cut":case"paste":setTimeout(function(){r._getDomField(!0),r._updateDomField()},1)}var u=s.tree;if(n==u.parentNode)switch(o){case"click":var p=void 0!=e.offsetX?e.offsetX<24*(this.getLevel()+1):e.pageXo[i]?t:e[i]/g,">").replace(/ /g," ").replace(/^ /," ").replace(/ $/," "),i=JSON.stringify(t);return i.substring(1,i.length-1)},o.prototype._unescapeHTML=function(e){var t='"'+this._escapeJSON(e)+'"',o=i.parse(t);return o.replace(/</g,"<").replace(/>/g,">").replace(/ |\u00A0/g," ")},o.prototype._escapeJSON=function(e){for(var t="",i=0,o=e.length;o>i;){var n=e.charAt(i);"\n"==n?t+="\\n":"\\"==n?(t+=n,i++,n=e.charAt(i),-1=='"\\/bfnrtu'.indexOf(n)&&(t+="\\"),t+=n):t+='"'==n?'\\"':n,i++}return t};var n=t(o);return o}.apply(null,o),!(void 0!==n&&(e.exports=n))},function(e,t,i){var o,n;o=[i(9)],n=function(e){function t(t,i,o){function n(e){t.setMode(e);var i=t.dom&&t.dom.modeBox;i&&i.focus()}for(var s={code:{text:"Code",title:"Switch to code highlighter",click:function(){n("code")}},form:{text:"Form",title:"Switch to form editor",click:function(){n("form")}},text:{text:"Text",title:"Switch to plain text editor",click:function(){n("text")}},tree:{text:"Tree",title:"Switch to tree editor",click:function(){n("tree")}},view:{text:"View",title:"Switch to tree view",click:function(){n("view")}}},r=[],a=0;a',a.appendChild(c),n.submenuTitle&&(c.title=n.submenuTitle),l=c}else{var u=document.createElement("div");u.className="expand",d.appendChild(u),l=d}l.onclick=function(){o._onExpandItem(r),l.focus()};var p=[];r.subItems=p;var f=document.createElement("ul");r.ul=f,f.className="menu",f.style.height="0",a.appendChild(f),i(f,p,n.submenu)}else d.innerHTML=''+n.text;t.push(r)}})}this.dom={};var o=this,n=this.dom;this.anchor=void 0,this.items=e,this.eventListeners={},this.selection=void 0,this.visibleSubmenu=void 0,this.onClose=t?t.close:void 0;var s=document.createElement("div");s.className="jsoneditor-contextmenu",n.menu=s;var r=document.createElement("ul");r.className="menu",s.appendChild(r),n.list=r,n.items=[];var a=document.createElement("button");n.focusButton=a;var d=document.createElement("li");d.style.overflow="hidden",d.style.height="0",d.appendChild(a),r.appendChild(d),i(r,this.dom.items,e),this.maxHeight=0,e.forEach(function(t){var i=24*(e.length+(t.submenu?t.submenu.length:0));o.maxHeight=Math.max(o.maxHeight,i)})}return t.prototype._getVisibleButtons=function(){var e=[],t=this;return this.dom.items.forEach(function(i){e.push(i.button),i.buttonExpand&&e.push(i.buttonExpand),i.subItems&&i==t.expandedItem&&i.subItems.forEach(function(t){e.push(t.button),t.buttonExpand&&e.push(t.buttonExpand)})}),e},t.visibleMenu=void 0,t.prototype.show=function(i){this.hide();var o=window.innerHeight,n=window.pageYOffset||document.scrollTop||0,s=o+n,r=i.offsetHeight,a=this.maxHeight,d=e.getAbsoluteLeft(i),h=e.getAbsoluteTop(i);s>h+r+a?(this.dom.menu.style.left=d+"px",this.dom.menu.style.top=h+r+"px",this.dom.menu.style.bottom=""):(this.dom.menu.style.left=d+"px",this.dom.menu.style.top="",this.dom.menu.style.bottom=o-h+"px"),document.body.appendChild(this.dom.menu);var l=this,c=this.dom.list;this.eventListeners.mousedown=e.addEventListener(document,"mousedown",function(e){var t=e.target;t==c||l._isChildOf(t,c)||(l.hide(),e.stopPropagation(),e.preventDefault())}),this.eventListeners.mousewheel=e.addEventListener(document,"mousewheel",function(e){e.stopPropagation(),e.preventDefault()}),this.eventListeners.keydown=e.addEventListener(document,"keydown",function(e){l._onKeyDown(e)}),this.selection=e.getSelection(),this.anchor=i,setTimeout(function(){l.dom.focusButton.focus()},0),t.visibleMenu&&t.visibleMenu.hide(),t.visibleMenu=this},t.prototype.hide=function(){this.dom.menu.parentNode&&(this.dom.menu.parentNode.removeChild(this.dom.menu),this.onClose&&this.onClose());for(var i in this.eventListeners)if(this.eventListeners.hasOwnProperty(i)){var o=this.eventListeners[i];o&&e.removeEventListener(document,i,o),delete this.eventListeners[i]}t.visibleMenu==this&&(t.visibleMenu=void 0)},t.prototype._onExpandItem=function(t){var i=this,o=t==this.expandedItem,n=this.expandedItem;if(n&&(n.ul.style.height="0",n.ul.style.padding="",setTimeout(function(){i.expandedItem!=n&&(n.ul.style.display="",e.removeClassName(n.ul.parentNode,"selected"))},300),this.expandedItem=void 0),!o){var s=t.ul;s.style.display="block";{s.clientHeight}setTimeout(function(){i.expandedItem==t&&(s.style.height=24*s.childNodes.length+"px",s.style.padding="5px 10px")},0),e.addClassName(s.parentNode,"selected"),this.expandedItem=t}},t.prototype._onKeyDown=function(t){var i,o,n,s,r=t.target,a=t.which,d=!1;27==a?(this.selection&&e.setSelection(this.selection),this.anchor&&this.anchor.focus(),this.hide(),d=!0):9==a?t.shiftKey?(i=this._getVisibleButtons(),o=i.indexOf(r),0==o&&(i[i.length-1].focus(),d=!0)):(i=this._getVisibleButtons(),o=i.indexOf(r),o==i.length-1&&(i[0].focus(),d=!0)):37==a?("expand"==r.className&&(i=this._getVisibleButtons(),o=i.indexOf(r),n=i[o-1],n&&n.focus()),d=!0):38==a?(i=this._getVisibleButtons(),o=i.indexOf(r),n=i[o-1],n&&"expand"==n.className&&(n=i[o-2]),n||(n=i[i.length-1]),n&&n.focus(),d=!0):39==a?(i=this._getVisibleButtons(),o=i.indexOf(r),s=i[o+1],s&&"expand"==s.className&&s.focus(),d=!0):40==a&&(i=this._getVisibleButtons(),o=i.indexOf(r),s=i[o+1],s&&"expand"==s.className&&(s=i[o+2]),s||(s=i[0]),s&&(s.focus(),d=!0),d=!0),d&&(t.stopPropagation(),t.preventDefault())},t.prototype._isChildOf=function(e,t){for(var i=e.parentNode;i;){if(i==t)return!0;i=i.parentNode}return!1},t}.apply(null,o),!(void 0!==n&&(e.exports=n))},function(e,t,i){var o,n;o=[i(3)],n=function(e){function t(t){function i(e){this.editor=e,this.dom={}}return i.prototype=new t,i.prototype.getDom=function(){var e=this.dom;if(e.tr)return e.tr;var t=document.createElement("tr");if(t.node=this,e.tr=t,this.editor.mode.edit){e.tdDrag=document.createElement("td");var i=document.createElement("td");e.tdMenu=i;var o=document.createElement("button");o.className="contextmenu",o.title="Click to open the actions menu (Ctrl+M)",e.menu=o,i.appendChild(e.menu)}var n=document.createElement("td"),s=document.createElement("div");return s.innerHTML="(empty)",s.className="readonly",n.appendChild(s),e.td=n,e.text=s,this.updateDom(),t},i.prototype.updateDom=function(){var e=this.dom,t=e.td;t&&(t.style.paddingLeft=24*this.getLevel()+26+"px");var i=e.text;i&&(i.innerHTML="(empty "+this.parent.type+")");var o=e.tr;this.isVisible()?e.tr.firstChild||(e.tdDrag&&o.appendChild(e.tdDrag),e.tdMenu&&o.appendChild(e.tdMenu),o.appendChild(t)):e.tr.firstChild&&(e.tdDrag&&o.removeChild(e.tdDrag),e.tdMenu&&o.removeChild(e.tdMenu),o.removeChild(t))},i.prototype.isVisible=function(){return 0==this.parent.childs.length},i.prototype.showContextMenu=function(e,i){var o=this,n=t.TYPE_TITLES,s=[{text:"Append",title:"Append a new field with type 'auto' (Ctrl+Shift+Ins)",submenuTitle:"Select the type of the field to be appended",className:"insert",click:function(){o._onAppend("","","auto")},submenu:[{text:"Auto",className:"type-auto",title:n.auto,click:function(){o._onAppend("","","auto")}},{text:"Array",className:"type-array",title:n.array,click:function(){o._onAppend("",[])}},{text:"Object",className:"type-object",title:n.object,click:function(){o._onAppend("",{})}},{text:"String",className:"type-string",title:n.string,click:function(){o._onAppend("","","string")}}]}],r=new ContextMenu(s,{close:i});r.show(e)},i.prototype.onEvent=function(t){var i=t.type,o=t.target||t.srcElement,n=this.dom,s=n.menu;if(o==s&&("mouseover"==i?this.editor.highlighter.highlight(this.parent):"mouseout"==i&&this.editor.highlighter.unhighlight()),"click"==i&&o==n.menu){var r=this.editor.highlighter;r.highlight(this.parent),r.lock(),e.addClassName(n.menu,"selected"),this.showContextMenu(n.menu,function(){e.removeClassName(n.menu,"selected"),r.unlock(),r.unhighlight()})}"keydown"==i&&this.onKeyDown(t)},i}return t}.apply(null,o),!(void 0!==n&&(e.exports=n))}])});
+//# sourceMappingURL=jsoneditor.map
\ No newline at end of file
diff --git a/package.json b/package.json
index 76163f8..4b5fe78 100644
--- a/package.json
+++ b/package.json
@@ -17,7 +17,7 @@
},
"bugs": "https://github.com/josdejong/jsoneditor/issues",
"scripts": {
- "build": "jake"
+ "build": "gulp"
},
"dependencies": {},
"devDependencies": {
@@ -25,6 +25,10 @@
"jake-utils": "latest",
"archiver": "latest",
"clean-css": "latest",
+ "gulp": "latest",
+ "gulp-util": "latest",
+ "webpack": "latest",
+ "uglify-js": "latest",
"jsonlint": "latest",
"ace": "git://github.com/ajaxorg/ace.git"
}
diff --git a/src/js/AppendNode.js b/src/js/AppendNode.js
deleted file mode 100644
index f3d796d..0000000
--- a/src/js/AppendNode.js
+++ /dev/null
@@ -1,211 +0,0 @@
-/**
- * @constructor AppendNode
- * @extends Node
- * @param {TreeEditor} editor
- * Create a new AppendNode. This is a special node which is created at the
- * end of the list with childs for an object or array
- */
-function AppendNode (editor) {
- /** @type {TreeEditor} */
- this.editor = editor;
- this.dom = {};
-}
-
-AppendNode.prototype = new Node();
-
-/**
- * Return a table row with an append button.
- * @return {Element} dom TR element
- */
-AppendNode.prototype.getDom = function () {
- // TODO: implement a new solution for the append node
- var dom = this.dom;
-
- if (dom.tr) {
- return dom.tr;
- }
-
- // a row for the append button
- var trAppend = document.createElement('tr');
- trAppend.node = this;
- dom.tr = trAppend;
-
- // TODO: consistent naming
-
- if (this.editor.mode.edit) {
- // a cell for the dragarea column
- dom.tdDrag = document.createElement('td');
-
- // create context menu
- var tdMenu = document.createElement('td');
- dom.tdMenu = tdMenu;
- var menu = document.createElement('button');
- menu.className = 'contextmenu';
- menu.title = 'Click to open the actions menu (Ctrl+M)';
- dom.menu = menu;
- tdMenu.appendChild(dom.menu);
- }
-
- // a cell for the contents (showing text 'empty')
- var tdAppend = document.createElement('td');
- var domText = document.createElement('div');
- domText.innerHTML = '(empty)';
- domText.className = 'readonly';
- tdAppend.appendChild(domText);
- dom.td = tdAppend;
- dom.text = domText;
-
- this.updateDom();
-
- return trAppend;
-};
-
-/**
- * Update the HTML dom of the Node
- */
-AppendNode.prototype.updateDom = function () {
- var dom = this.dom;
- var tdAppend = dom.td;
- if (tdAppend) {
- tdAppend.style.paddingLeft = (this.getLevel() * 24 + 26) + 'px';
- // TODO: not so nice hard coded offset
- }
-
- var domText = dom.text;
- if (domText) {
- domText.innerHTML = '(empty ' + this.parent.type + ')';
- }
-
- // attach or detach the contents of the append node:
- // hide when the parent has childs, show when the parent has no childs
- var trAppend = dom.tr;
- if (!this.isVisible()) {
- if (dom.tr.firstChild) {
- if (dom.tdDrag) {
- trAppend.removeChild(dom.tdDrag);
- }
- if (dom.tdMenu) {
- trAppend.removeChild(dom.tdMenu);
- }
- trAppend.removeChild(tdAppend);
- }
- }
- else {
- if (!dom.tr.firstChild) {
- if (dom.tdDrag) {
- trAppend.appendChild(dom.tdDrag);
- }
- if (dom.tdMenu) {
- trAppend.appendChild(dom.tdMenu);
- }
- trAppend.appendChild(tdAppend);
- }
- }
-};
-
-/**
- * Check whether the AppendNode is currently visible.
- * the AppendNode is visible when its parent has no childs (i.e. is empty).
- * @return {boolean} isVisible
- */
-AppendNode.prototype.isVisible = function () {
- return (this.parent.childs.length == 0);
-};
-
-/**
- * Show a contextmenu for this node
- * @param {HTMLElement} anchor The element to attach the menu to.
- * @param {function} [onClose] Callback method called when the context menu
- * is being closed.
- */
-AppendNode.prototype.showContextMenu = function (anchor, onClose) {
- var node = this;
- var titles = Node.TYPE_TITLES;
- var items = [
- // create append button
- {
- 'text': 'Append',
- 'title': 'Append a new field with type \'auto\' (Ctrl+Shift+Ins)',
- 'submenuTitle': 'Select the type of the field to be appended',
- 'className': 'insert',
- 'click': function () {
- node._onAppend('', '', 'auto');
- },
- 'submenu': [
- {
- 'text': 'Auto',
- 'className': 'type-auto',
- 'title': titles.auto,
- 'click': function () {
- node._onAppend('', '', 'auto');
- }
- },
- {
- 'text': 'Array',
- 'className': 'type-array',
- 'title': titles.array,
- 'click': function () {
- node._onAppend('', []);
- }
- },
- {
- 'text': 'Object',
- 'className': 'type-object',
- 'title': titles.object,
- 'click': function () {
- node._onAppend('', {});
- }
- },
- {
- 'text': 'String',
- 'className': 'type-string',
- 'title': titles.string,
- 'click': function () {
- node._onAppend('', '', 'string');
- }
- }
- ]
- }
- ];
-
- var menu = new ContextMenu(items, {close: onClose});
- menu.show(anchor);
-};
-
-/**
- * Handle an event. The event is catched centrally by the editor
- * @param {Event} event
- */
-AppendNode.prototype.onEvent = function (event) {
- var type = event.type;
- var target = event.target || event.srcElement;
- var dom = this.dom;
-
- // highlight the append nodes parent
- var menu = dom.menu;
- if (target == menu) {
- if (type == 'mouseover') {
- this.editor.highlighter.highlight(this.parent);
- }
- else if (type == 'mouseout') {
- this.editor.highlighter.unhighlight();
- }
- }
-
- // context menu events
- if (type == 'click' && target == dom.menu) {
- var highlighter = this.editor.highlighter;
- highlighter.highlight(this.parent);
- highlighter.lock();
- util.addClassName(dom.menu, 'selected');
- this.showContextMenu(dom.menu, function () {
- util.removeClassName(dom.menu, 'selected');
- highlighter.unlock();
- highlighter.unhighlight();
- });
- }
-
- if (type == 'keydown') {
- this.onKeyDown(event);
- }
-};
diff --git a/src/js/ContextMenu.js b/src/js/ContextMenu.js
index 1916a9f..8bb3b51 100644
--- a/src/js/ContextMenu.js
+++ b/src/js/ContextMenu.js
@@ -1,440 +1,444 @@
-/**
- * A context menu
- * @param {Object[]} items Array containing the menu structure
- * TODO: describe structure
- * @param {Object} [options] Object with options. Available options:
- * {function} close Callback called when the
- * context menu is being closed.
- * @constructor
- */
-function ContextMenu (items, options) {
- this.dom = {};
+define(['./util'], function (util) {
- var me = this;
- var dom = this.dom;
- this.anchor = undefined;
- this.items = items;
- this.eventListeners = {};
- this.selection = undefined; // holds the selection before the menu was opened
- this.visibleSubmenu = undefined;
- this.onClose = options ? options.close : undefined;
+ /**
+ * A context menu
+ * @param {Object[]} items Array containing the menu structure
+ * TODO: describe structure
+ * @param {Object} [options] Object with options. Available options:
+ * {function} close Callback called when the
+ * context menu is being closed.
+ * @constructor
+ */
+ function ContextMenu (items, options) {
+ this.dom = {};
- // create a container element
- var menu = document.createElement('div');
- menu.className = 'jsoneditor-contextmenu';
- dom.menu = menu;
+ var me = this;
+ var dom = this.dom;
+ this.anchor = undefined;
+ this.items = items;
+ this.eventListeners = {};
+ this.selection = undefined; // holds the selection before the menu was opened
+ this.visibleSubmenu = undefined;
+ this.onClose = options ? options.close : undefined;
- // create a list to hold the menu items
- var list = document.createElement('ul');
- list.className = 'menu';
- menu.appendChild(list);
- dom.list = list;
- dom.items = []; // list with all buttons
+ // create a container element
+ var menu = document.createElement('div');
+ menu.className = 'jsoneditor-contextmenu';
+ dom.menu = menu;
- // create a (non-visible) button to set the focus to the menu
- var focusButton = document.createElement('button');
- dom.focusButton = focusButton;
- var li = document.createElement('li');
- li.style.overflow = 'hidden';
- li.style.height = '0';
- li.appendChild(focusButton);
- list.appendChild(li);
+ // create a list to hold the menu items
+ var list = document.createElement('ul');
+ list.className = 'menu';
+ menu.appendChild(list);
+ dom.list = list;
+ dom.items = []; // list with all buttons
- function createMenuItems (list, domItems, items) {
- items.forEach(function (item) {
- if (item.type == 'separator') {
- // create a separator
- var separator = document.createElement('div');
- separator.className = 'separator';
- li = document.createElement('li');
- li.appendChild(separator);
- list.appendChild(li);
- }
- else {
- var domItem = {};
+ // create a (non-visible) button to set the focus to the menu
+ var focusButton = document.createElement('button');
+ dom.focusButton = focusButton;
+ var li = document.createElement('li');
+ li.style.overflow = 'hidden';
+ li.style.height = '0';
+ li.appendChild(focusButton);
+ list.appendChild(li);
- // create a menu item
- var li = document.createElement('li');
- list.appendChild(li);
-
- // create a button in the menu item
- var button = document.createElement('button');
- button.className = item.className;
- domItem.button = button;
- if (item.title) {
- button.title = item.title;
- }
- if (item.click) {
- button.onclick = function () {
- me.hide();
- item.click();
- };
- }
- li.appendChild(button);
-
- // create the contents of the button
- if (item.submenu) {
- // add the icon to the button
- var divIcon = document.createElement('div');
- divIcon.className = 'icon';
- button.appendChild(divIcon);
- button.appendChild(document.createTextNode(item.text));
-
- var buttonSubmenu;
- if (item.click) {
- // submenu and a button with a click handler
- button.className += ' default';
-
- var buttonExpand = document.createElement('button');
- domItem.buttonExpand = buttonExpand;
- buttonExpand.className = 'expand';
- buttonExpand.innerHTML = '';
- li.appendChild(buttonExpand);
- if (item.submenuTitle) {
- buttonExpand.title = item.submenuTitle;
- }
-
- buttonSubmenu = buttonExpand;
- }
- else {
- // submenu and a button without a click handler
- var divExpand = document.createElement('div');
- divExpand.className = 'expand';
- button.appendChild(divExpand);
-
- buttonSubmenu = button;
- }
-
- // attach a handler to expand/collapse the submenu
- buttonSubmenu.onclick = function () {
- me._onExpandItem(domItem);
- buttonSubmenu.focus();
- };
-
- // create the submenu
- var domSubItems = [];
- domItem.subItems = domSubItems;
- var ul = document.createElement('ul');
- domItem.ul = ul;
- ul.className = 'menu';
- ul.style.height = '0';
- li.appendChild(ul);
- createMenuItems(ul, domSubItems, item.submenu);
+ function createMenuItems (list, domItems, items) {
+ items.forEach(function (item) {
+ if (item.type == 'separator') {
+ // create a separator
+ var separator = document.createElement('div');
+ separator.className = 'separator';
+ li = document.createElement('li');
+ li.appendChild(separator);
+ list.appendChild(li);
}
else {
- // no submenu, just a button with clickhandler
- button.innerHTML = '' + item.text;
- }
+ var domItem = {};
- domItems.push(domItem);
- }
+ // create a menu item
+ var li = document.createElement('li');
+ list.appendChild(li);
+
+ // create a button in the menu item
+ var button = document.createElement('button');
+ button.className = item.className;
+ domItem.button = button;
+ if (item.title) {
+ button.title = item.title;
+ }
+ if (item.click) {
+ button.onclick = function () {
+ me.hide();
+ item.click();
+ };
+ }
+ li.appendChild(button);
+
+ // create the contents of the button
+ if (item.submenu) {
+ // add the icon to the button
+ var divIcon = document.createElement('div');
+ divIcon.className = 'icon';
+ button.appendChild(divIcon);
+ button.appendChild(document.createTextNode(item.text));
+
+ var buttonSubmenu;
+ if (item.click) {
+ // submenu and a button with a click handler
+ button.className += ' default';
+
+ var buttonExpand = document.createElement('button');
+ domItem.buttonExpand = buttonExpand;
+ buttonExpand.className = 'expand';
+ buttonExpand.innerHTML = '';
+ li.appendChild(buttonExpand);
+ if (item.submenuTitle) {
+ buttonExpand.title = item.submenuTitle;
+ }
+
+ buttonSubmenu = buttonExpand;
+ }
+ else {
+ // submenu and a button without a click handler
+ var divExpand = document.createElement('div');
+ divExpand.className = 'expand';
+ button.appendChild(divExpand);
+
+ buttonSubmenu = button;
+ }
+
+ // attach a handler to expand/collapse the submenu
+ buttonSubmenu.onclick = function () {
+ me._onExpandItem(domItem);
+ buttonSubmenu.focus();
+ };
+
+ // create the submenu
+ var domSubItems = [];
+ domItem.subItems = domSubItems;
+ var ul = document.createElement('ul');
+ domItem.ul = ul;
+ ul.className = 'menu';
+ ul.style.height = '0';
+ li.appendChild(ul);
+ createMenuItems(ul, domSubItems, item.submenu);
+ }
+ else {
+ // no submenu, just a button with clickhandler
+ button.innerHTML = '' + item.text;
+ }
+
+ domItems.push(domItem);
+ }
+ });
+ }
+ createMenuItems(list, this.dom.items, items);
+
+ // TODO: when the editor is small, show the submenu on the right instead of inline?
+
+ // calculate the max height of the menu with one submenu expanded
+ this.maxHeight = 0; // height in pixels
+ items.forEach(function (item) {
+ var height = (items.length + (item.submenu ? item.submenu.length : 0)) * 24;
+ me.maxHeight = Math.max(me.maxHeight, height);
});
}
- createMenuItems(list, this.dom.items, items);
- // TODO: when the editor is small, show the submenu on the right instead of inline?
+ /**
+ * Get the currently visible buttons
+ * @return {Array.} buttons
+ * @private
+ */
+ ContextMenu.prototype._getVisibleButtons = function () {
+ var buttons = [];
+ var me = this;
+ this.dom.items.forEach(function (item) {
+ buttons.push(item.button);
+ if (item.buttonExpand) {
+ buttons.push(item.buttonExpand);
+ }
+ if (item.subItems && item == me.expandedItem) {
+ item.subItems.forEach(function (subItem) {
+ buttons.push(subItem.button);
+ if (subItem.buttonExpand) {
+ buttons.push(subItem.buttonExpand);
+ }
+ // TODO: change to fully recursive method
+ });
+ }
+ });
- // calculate the max height of the menu with one submenu expanded
- this.maxHeight = 0; // height in pixels
- items.forEach(function (item) {
- var height = (items.length + (item.submenu ? item.submenu.length : 0)) * 24;
- me.maxHeight = Math.max(me.maxHeight, height);
- });
-}
-
-/**
- * Get the currently visible buttons
- * @return {Array.} buttons
- * @private
- */
-ContextMenu.prototype._getVisibleButtons = function () {
- var buttons = [];
- var me = this;
- this.dom.items.forEach(function (item) {
- buttons.push(item.button);
- if (item.buttonExpand) {
- buttons.push(item.buttonExpand);
- }
- if (item.subItems && item == me.expandedItem) {
- item.subItems.forEach(function (subItem) {
- buttons.push(subItem.button);
- if (subItem.buttonExpand) {
- buttons.push(subItem.buttonExpand);
- }
- // TODO: change to fully recursive method
- });
- }
- });
-
- return buttons;
-};
+ return buttons;
+ };
// currently displayed context menu, a singleton. We may only have one visible context menu
-ContextMenu.visibleMenu = undefined;
-
-/**
- * Attach the menu to an anchor
- * @param {HTMLElement} anchor
- */
-ContextMenu.prototype.show = function (anchor) {
- this.hide();
-
- // calculate whether the menu fits below the anchor
- var windowHeight = window.innerHeight,
- windowScroll = (window.pageYOffset || document.scrollTop || 0),
- windowBottom = windowHeight + windowScroll,
- anchorHeight = anchor.offsetHeight,
- menuHeight = this.maxHeight;
-
- // position the menu
- var left = util.getAbsoluteLeft(anchor);
- var top = util.getAbsoluteTop(anchor);
- if (top + anchorHeight + menuHeight < windowBottom) {
- // display the menu below the anchor
- this.dom.menu.style.left = left + 'px';
- this.dom.menu.style.top = (top + anchorHeight) + 'px';
- this.dom.menu.style.bottom = '';
- }
- else {
- // display the menu above the anchor
- this.dom.menu.style.left = left + 'px';
- this.dom.menu.style.top = '';
- this.dom.menu.style.bottom = (windowHeight - top) + 'px';
- }
-
- // attach the menu to the document
- document.body.appendChild(this.dom.menu);
-
- // create and attach event listeners
- var me = this;
- var list = this.dom.list;
- this.eventListeners.mousedown = util.addEventListener(
- document, 'mousedown', function (event) {
- // hide menu on click outside of the menu
- var target = event.target;
- if ((target != list) && !me._isChildOf(target, list)) {
- me.hide();
- event.stopPropagation();
- event.preventDefault();
- }
- });
- this.eventListeners.mousewheel = util.addEventListener(
- document, 'mousewheel', function (event) {
- // block scrolling when context menu is visible
- event.stopPropagation();
- event.preventDefault();
- });
- this.eventListeners.keydown = util.addEventListener(
- document, 'keydown', function (event) {
- me._onKeyDown(event);
- });
-
- // move focus to the first button in the context menu
- this.selection = util.getSelection();
- this.anchor = anchor;
- setTimeout(function () {
- me.dom.focusButton.focus();
- }, 0);
-
- if (ContextMenu.visibleMenu) {
- ContextMenu.visibleMenu.hide();
- }
- ContextMenu.visibleMenu = this;
-};
-
-/**
- * Hide the context menu if visible
- */
-ContextMenu.prototype.hide = function () {
- // remove the menu from the DOM
- if (this.dom.menu.parentNode) {
- this.dom.menu.parentNode.removeChild(this.dom.menu);
- if (this.onClose) {
- this.onClose();
- }
- }
-
- // remove all event listeners
- // all event listeners are supposed to be attached to document.
- for (var name in this.eventListeners) {
- if (this.eventListeners.hasOwnProperty(name)) {
- var fn = this.eventListeners[name];
- if (fn) {
- util.removeEventListener(document, name, fn);
- }
- delete this.eventListeners[name];
- }
- }
-
- if (ContextMenu.visibleMenu == this) {
- ContextMenu.visibleMenu = undefined;
- }
-};
-
-/**
- * Expand a submenu
- * Any currently expanded submenu will be hided.
- * @param {Object} domItem
- * @private
- */
-ContextMenu.prototype._onExpandItem = function (domItem) {
- var me = this;
- var alreadyVisible = (domItem == this.expandedItem);
-
- // hide the currently visible submenu
- var expandedItem = this.expandedItem;
- if (expandedItem) {
- //var ul = expandedItem.ul;
- expandedItem.ul.style.height = '0';
- expandedItem.ul.style.padding = '';
- setTimeout(function () {
- if (me.expandedItem != expandedItem) {
- expandedItem.ul.style.display = '';
- util.removeClassName(expandedItem.ul.parentNode, 'selected');
- }
- }, 300); // timeout duration must match the css transition duration
- this.expandedItem = undefined;
- }
-
- if (!alreadyVisible) {
- var ul = domItem.ul;
- ul.style.display = 'block';
- var height = ul.clientHeight; // force a reflow in Firefox
- setTimeout(function () {
- if (me.expandedItem == domItem) {
- ul.style.height = (ul.childNodes.length * 24) + 'px';
- ul.style.padding = '5px 10px';
- }
- }, 0);
- util.addClassName(ul.parentNode, 'selected');
- this.expandedItem = domItem;
- }
-};
-
-/**
- * Handle onkeydown event
- * @param {Event} event
- * @private
- */
-ContextMenu.prototype._onKeyDown = function (event) {
- var target = event.target;
- var keynum = event.which;
- var handled = false;
- var buttons, targetIndex, prevButton, nextButton;
-
- if (keynum == 27) { // ESC
- // hide the menu on ESC key
-
- // restore previous selection and focus
- if (this.selection) {
- util.setSelection(this.selection);
- }
- if (this.anchor) {
- this.anchor.focus();
- }
+ ContextMenu.visibleMenu = undefined;
+ /**
+ * Attach the menu to an anchor
+ * @param {HTMLElement} anchor
+ */
+ ContextMenu.prototype.show = function (anchor) {
this.hide();
- handled = true;
- }
- else if (keynum == 9) { // Tab
- if (!event.shiftKey) { // Tab
- buttons = this._getVisibleButtons();
- targetIndex = buttons.indexOf(target);
- if (targetIndex == buttons.length - 1) {
- // move to first button
- buttons[0].focus();
- handled = true;
+ // calculate whether the menu fits below the anchor
+ var windowHeight = window.innerHeight,
+ windowScroll = (window.pageYOffset || document.scrollTop || 0),
+ windowBottom = windowHeight + windowScroll,
+ anchorHeight = anchor.offsetHeight,
+ menuHeight = this.maxHeight;
+
+ // position the menu
+ var left = util.getAbsoluteLeft(anchor);
+ var top = util.getAbsoluteTop(anchor);
+ if (top + anchorHeight + menuHeight < windowBottom) {
+ // display the menu below the anchor
+ this.dom.menu.style.left = left + 'px';
+ this.dom.menu.style.top = (top + anchorHeight) + 'px';
+ this.dom.menu.style.bottom = '';
+ }
+ else {
+ // display the menu above the anchor
+ this.dom.menu.style.left = left + 'px';
+ this.dom.menu.style.top = '';
+ this.dom.menu.style.bottom = (windowHeight - top) + 'px';
+ }
+
+ // attach the menu to the document
+ document.body.appendChild(this.dom.menu);
+
+ // create and attach event listeners
+ var me = this;
+ var list = this.dom.list;
+ this.eventListeners.mousedown = util.addEventListener(
+ document, 'mousedown', function (event) {
+ // hide menu on click outside of the menu
+ var target = event.target;
+ if ((target != list) && !me._isChildOf(target, list)) {
+ me.hide();
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ });
+ this.eventListeners.mousewheel = util.addEventListener(
+ document, 'mousewheel', function (event) {
+ // block scrolling when context menu is visible
+ event.stopPropagation();
+ event.preventDefault();
+ });
+ this.eventListeners.keydown = util.addEventListener(
+ document, 'keydown', function (event) {
+ me._onKeyDown(event);
+ });
+
+ // move focus to the first button in the context menu
+ this.selection = util.getSelection();
+ this.anchor = anchor;
+ setTimeout(function () {
+ me.dom.focusButton.focus();
+ }, 0);
+
+ if (ContextMenu.visibleMenu) {
+ ContextMenu.visibleMenu.hide();
+ }
+ ContextMenu.visibleMenu = this;
+ };
+
+ /**
+ * Hide the context menu if visible
+ */
+ ContextMenu.prototype.hide = function () {
+ // remove the menu from the DOM
+ if (this.dom.menu.parentNode) {
+ this.dom.menu.parentNode.removeChild(this.dom.menu);
+ if (this.onClose) {
+ this.onClose();
}
}
- else { // Shift+Tab
- buttons = this._getVisibleButtons();
- targetIndex = buttons.indexOf(target);
- if (targetIndex == 0) {
- // move to last button
- buttons[buttons.length - 1].focus();
- handled = true;
+
+ // remove all event listeners
+ // all event listeners are supposed to be attached to document.
+ for (var name in this.eventListeners) {
+ if (this.eventListeners.hasOwnProperty(name)) {
+ var fn = this.eventListeners[name];
+ if (fn) {
+ util.removeEventListener(document, name, fn);
+ }
+ delete this.eventListeners[name];
}
}
- }
- else if (keynum == 37) { // Arrow Left
- if (target.className == 'expand') {
+
+ if (ContextMenu.visibleMenu == this) {
+ ContextMenu.visibleMenu = undefined;
+ }
+ };
+
+ /**
+ * Expand a submenu
+ * Any currently expanded submenu will be hided.
+ * @param {Object} domItem
+ * @private
+ */
+ ContextMenu.prototype._onExpandItem = function (domItem) {
+ var me = this;
+ var alreadyVisible = (domItem == this.expandedItem);
+
+ // hide the currently visible submenu
+ var expandedItem = this.expandedItem;
+ if (expandedItem) {
+ //var ul = expandedItem.ul;
+ expandedItem.ul.style.height = '0';
+ expandedItem.ul.style.padding = '';
+ setTimeout(function () {
+ if (me.expandedItem != expandedItem) {
+ expandedItem.ul.style.display = '';
+ util.removeClassName(expandedItem.ul.parentNode, 'selected');
+ }
+ }, 300); // timeout duration must match the css transition duration
+ this.expandedItem = undefined;
+ }
+
+ if (!alreadyVisible) {
+ var ul = domItem.ul;
+ ul.style.display = 'block';
+ var height = ul.clientHeight; // force a reflow in Firefox
+ setTimeout(function () {
+ if (me.expandedItem == domItem) {
+ ul.style.height = (ul.childNodes.length * 24) + 'px';
+ ul.style.padding = '5px 10px';
+ }
+ }, 0);
+ util.addClassName(ul.parentNode, 'selected');
+ this.expandedItem = domItem;
+ }
+ };
+
+ /**
+ * Handle onkeydown event
+ * @param {Event} event
+ * @private
+ */
+ ContextMenu.prototype._onKeyDown = function (event) {
+ var target = event.target;
+ var keynum = event.which;
+ var handled = false;
+ var buttons, targetIndex, prevButton, nextButton;
+
+ if (keynum == 27) { // ESC
+ // hide the menu on ESC key
+
+ // restore previous selection and focus
+ if (this.selection) {
+ util.setSelection(this.selection);
+ }
+ if (this.anchor) {
+ this.anchor.focus();
+ }
+
+ this.hide();
+
+ handled = true;
+ }
+ else if (keynum == 9) { // Tab
+ if (!event.shiftKey) { // Tab
+ buttons = this._getVisibleButtons();
+ targetIndex = buttons.indexOf(target);
+ if (targetIndex == buttons.length - 1) {
+ // move to first button
+ buttons[0].focus();
+ handled = true;
+ }
+ }
+ else { // Shift+Tab
+ buttons = this._getVisibleButtons();
+ targetIndex = buttons.indexOf(target);
+ if (targetIndex == 0) {
+ // move to last button
+ buttons[buttons.length - 1].focus();
+ handled = true;
+ }
+ }
+ }
+ else if (keynum == 37) { // Arrow Left
+ if (target.className == 'expand') {
+ buttons = this._getVisibleButtons();
+ targetIndex = buttons.indexOf(target);
+ prevButton = buttons[targetIndex - 1];
+ if (prevButton) {
+ prevButton.focus();
+ }
+ }
+ handled = true;
+ }
+ else if (keynum == 38) { // Arrow Up
buttons = this._getVisibleButtons();
targetIndex = buttons.indexOf(target);
prevButton = buttons[targetIndex - 1];
+ if (prevButton && prevButton.className == 'expand') {
+ // skip expand button
+ prevButton = buttons[targetIndex - 2];
+ }
+ if (!prevButton) {
+ // move to last button
+ prevButton = buttons[buttons.length - 1];
+ }
if (prevButton) {
prevButton.focus();
}
- }
- handled = true;
- }
- else if (keynum == 38) { // Arrow Up
- buttons = this._getVisibleButtons();
- targetIndex = buttons.indexOf(target);
- prevButton = buttons[targetIndex - 1];
- if (prevButton && prevButton.className == 'expand') {
- // skip expand button
- prevButton = buttons[targetIndex - 2];
- }
- if (!prevButton) {
- // move to last button
- prevButton = buttons[buttons.length - 1];
- }
- if (prevButton) {
- prevButton.focus();
- }
- handled = true;
- }
- else if (keynum == 39) { // Arrow Right
- buttons = this._getVisibleButtons();
- targetIndex = buttons.indexOf(target);
- nextButton = buttons[targetIndex + 1];
- if (nextButton && nextButton.className == 'expand') {
- nextButton.focus();
- }
- handled = true;
- }
- else if (keynum == 40) { // Arrow Down
- buttons = this._getVisibleButtons();
- targetIndex = buttons.indexOf(target);
- nextButton = buttons[targetIndex + 1];
- if (nextButton && nextButton.className == 'expand') {
- // skip expand button
- nextButton = buttons[targetIndex + 2];
- }
- if (!nextButton) {
- // move to first button
- nextButton = buttons[0];
- }
- if (nextButton) {
- nextButton.focus();
handled = true;
}
- handled = true;
- }
- // TODO: arrow left and right
-
- if (handled) {
- event.stopPropagation();
- event.preventDefault();
- }
-};
-
-/**
- * Test if an element is a child of a parent element.
- * @param {Element} child
- * @param {Element} parent
- * @return {boolean} isChild
- */
-ContextMenu.prototype._isChildOf = function (child, parent) {
- var e = child.parentNode;
- while (e) {
- if (e == parent) {
- return true;
+ else if (keynum == 39) { // Arrow Right
+ buttons = this._getVisibleButtons();
+ targetIndex = buttons.indexOf(target);
+ nextButton = buttons[targetIndex + 1];
+ if (nextButton && nextButton.className == 'expand') {
+ nextButton.focus();
+ }
+ handled = true;
}
- e = e.parentNode;
- }
+ else if (keynum == 40) { // Arrow Down
+ buttons = this._getVisibleButtons();
+ targetIndex = buttons.indexOf(target);
+ nextButton = buttons[targetIndex + 1];
+ if (nextButton && nextButton.className == 'expand') {
+ // skip expand button
+ nextButton = buttons[targetIndex + 2];
+ }
+ if (!nextButton) {
+ // move to first button
+ nextButton = buttons[0];
+ }
+ if (nextButton) {
+ nextButton.focus();
+ handled = true;
+ }
+ handled = true;
+ }
+ // TODO: arrow left and right
- return false;
-};
+ if (handled) {
+ event.stopPropagation();
+ event.preventDefault();
+ }
+ };
+ /**
+ * Test if an element is a child of a parent element.
+ * @param {Element} child
+ * @param {Element} parent
+ * @return {boolean} isChild
+ */
+ ContextMenu.prototype._isChildOf = function (child, parent) {
+ var e = child.parentNode;
+ while (e) {
+ if (e == parent) {
+ return true;
+ }
+ e = e.parentNode;
+ }
+
+ return false;
+ };
+
+ return ContextMenu;
+});
diff --git a/src/js/Highlighter.js b/src/js/Highlighter.js
index ab6311f..d2bcf33 100644
--- a/src/js/Highlighter.js
+++ b/src/js/Highlighter.js
@@ -1,82 +1,87 @@
-/**
- * The highlighter can highlight/unhighlight a node, and
- * animate the visibility of a context menu.
- * @constructor Highlighter
- */
-function Highlighter () {
- this.locked = false;
-}
+define(function () {
-/**
- * Hightlight given node and its childs
- * @param {Node} node
- */
-Highlighter.prototype.highlight = function (node) {
- if (this.locked) {
- return;
+ /**
+ * The highlighter can highlight/unhighlight a node, and
+ * animate the visibility of a context menu.
+ * @constructor Highlighter
+ */
+ function Highlighter () {
+ this.locked = false;
}
- if (this.node != node) {
- // unhighlight current node
- if (this.node) {
- this.node.setHighlight(false);
+ /**
+ * Hightlight given node and its childs
+ * @param {Node} node
+ */
+ Highlighter.prototype.highlight = function (node) {
+ if (this.locked) {
+ return;
}
- // highlight new node
- this.node = node;
- this.node.setHighlight(true);
- }
+ if (this.node != node) {
+ // unhighlight current node
+ if (this.node) {
+ this.node.setHighlight(false);
+ }
- // cancel any current timeout
- this._cancelUnhighlight();
-};
+ // highlight new node
+ this.node = node;
+ this.node.setHighlight(true);
+ }
-/**
- * Unhighlight currently highlighted node.
- * Will be done after a delay
- */
-Highlighter.prototype.unhighlight = function () {
- if (this.locked) {
- return;
- }
-
- var me = this;
- if (this.node) {
+ // cancel any current timeout
this._cancelUnhighlight();
+ };
- // do the unhighlighting after a small delay, to prevent re-highlighting
- // the same node when moving from the drag-icon to the contextmenu-icon
- // or vice versa.
- this.unhighlightTimer = setTimeout(function () {
- me.node.setHighlight(false);
- me.node = undefined;
- me.unhighlightTimer = undefined;
- }, 0);
- }
-};
+ /**
+ * Unhighlight currently highlighted node.
+ * Will be done after a delay
+ */
+ Highlighter.prototype.unhighlight = function () {
+ if (this.locked) {
+ return;
+ }
-/**
- * Cancel an unhighlight action (if before the timeout of the unhighlight action)
- * @private
- */
-Highlighter.prototype._cancelUnhighlight = function () {
- if (this.unhighlightTimer) {
- clearTimeout(this.unhighlightTimer);
- this.unhighlightTimer = undefined;
- }
-};
+ var me = this;
+ if (this.node) {
+ this._cancelUnhighlight();
-/**
- * Lock highlighting or unhighlighting nodes.
- * methods highlight and unhighlight do not work while locked.
- */
-Highlighter.prototype.lock = function () {
- this.locked = true;
-};
+ // do the unhighlighting after a small delay, to prevent re-highlighting
+ // the same node when moving from the drag-icon to the contextmenu-icon
+ // or vice versa.
+ this.unhighlightTimer = setTimeout(function () {
+ me.node.setHighlight(false);
+ me.node = undefined;
+ me.unhighlightTimer = undefined;
+ }, 0);
+ }
+ };
-/**
- * Unlock highlighting or unhighlighting nodes
- */
-Highlighter.prototype.unlock = function () {
- this.locked = false;
-};
+ /**
+ * Cancel an unhighlight action (if before the timeout of the unhighlight action)
+ * @private
+ */
+ Highlighter.prototype._cancelUnhighlight = function () {
+ if (this.unhighlightTimer) {
+ clearTimeout(this.unhighlightTimer);
+ this.unhighlightTimer = undefined;
+ }
+ };
+
+ /**
+ * Lock highlighting or unhighlighting nodes.
+ * methods highlight and unhighlight do not work while locked.
+ */
+ Highlighter.prototype.lock = function () {
+ this.locked = true;
+ };
+
+ /**
+ * Unlock highlighting or unhighlighting nodes
+ */
+ Highlighter.prototype.unlock = function () {
+ this.locked = false;
+ };
+
+ return Highlighter;
+});
\ No newline at end of file
diff --git a/src/js/History.js b/src/js/History.js
index 390e32f..72da3f8 100644
--- a/src/js/History.js
+++ b/src/js/History.js
@@ -1,218 +1,223 @@
-/**
- * @constructor History
- * Store action history, enables undo and redo
- * @param {JSONEditor} editor
- */
-function History (editor) {
- this.editor = editor;
- this.clear();
+define(['./util'], function (util) {
- // map with all supported actions
- this.actions = {
- 'editField': {
- 'undo': function (params) {
- params.node.updateField(params.oldValue);
+ /**
+ * @constructor History
+ * Store action history, enables undo and redo
+ * @param {JSONEditor} editor
+ */
+ function History (editor) {
+ this.editor = editor;
+ this.clear();
+
+ // map with all supported actions
+ this.actions = {
+ 'editField': {
+ 'undo': function (params) {
+ params.node.updateField(params.oldValue);
+ },
+ 'redo': function (params) {
+ params.node.updateField(params.newValue);
+ }
},
- 'redo': function (params) {
- params.node.updateField(params.newValue);
- }
- },
- 'editValue': {
- 'undo': function (params) {
- params.node.updateValue(params.oldValue);
+ 'editValue': {
+ 'undo': function (params) {
+ params.node.updateValue(params.oldValue);
+ },
+ 'redo': function (params) {
+ params.node.updateValue(params.newValue);
+ }
},
- 'redo': function (params) {
- params.node.updateValue(params.newValue);
- }
- },
- 'appendNode': {
- 'undo': function (params) {
- params.parent.removeChild(params.node);
+ 'appendNode': {
+ 'undo': function (params) {
+ params.parent.removeChild(params.node);
+ },
+ 'redo': function (params) {
+ params.parent.appendChild(params.node);
+ }
},
- 'redo': function (params) {
- params.parent.appendChild(params.node);
- }
- },
- 'insertBeforeNode': {
- 'undo': function (params) {
- params.parent.removeChild(params.node);
+ 'insertBeforeNode': {
+ 'undo': function (params) {
+ params.parent.removeChild(params.node);
+ },
+ 'redo': function (params) {
+ params.parent.insertBefore(params.node, params.beforeNode);
+ }
},
- 'redo': function (params) {
- params.parent.insertBefore(params.node, params.beforeNode);
- }
- },
- 'insertAfterNode': {
- 'undo': function (params) {
- params.parent.removeChild(params.node);
+ 'insertAfterNode': {
+ 'undo': function (params) {
+ params.parent.removeChild(params.node);
+ },
+ 'redo': function (params) {
+ params.parent.insertAfter(params.node, params.afterNode);
+ }
},
- 'redo': function (params) {
- params.parent.insertAfter(params.node, params.afterNode);
- }
- },
- 'removeNode': {
- 'undo': function (params) {
- var parent = params.parent;
- var beforeNode = parent.childs[params.index] || parent.append;
- parent.insertBefore(params.node, beforeNode);
+ 'removeNode': {
+ 'undo': function (params) {
+ var parent = params.parent;
+ var beforeNode = parent.childs[params.index] || parent.append;
+ parent.insertBefore(params.node, beforeNode);
+ },
+ 'redo': function (params) {
+ params.parent.removeChild(params.node);
+ }
},
- 'redo': function (params) {
- params.parent.removeChild(params.node);
- }
- },
- 'duplicateNode': {
- 'undo': function (params) {
- params.parent.removeChild(params.clone);
+ 'duplicateNode': {
+ 'undo': function (params) {
+ params.parent.removeChild(params.clone);
+ },
+ 'redo': function (params) {
+ params.parent.insertAfter(params.clone, params.node);
+ }
},
- 'redo': function (params) {
- params.parent.insertAfter(params.clone, params.node);
- }
- },
- 'changeType': {
- 'undo': function (params) {
- params.node.changeType(params.oldType);
+ 'changeType': {
+ 'undo': function (params) {
+ params.node.changeType(params.oldType);
+ },
+ 'redo': function (params) {
+ params.node.changeType(params.newType);
+ }
},
- 'redo': function (params) {
- params.node.changeType(params.newType);
- }
- },
- 'moveNode': {
- 'undo': function (params) {
- params.startParent.moveTo(params.node, params.startIndex);
+ 'moveNode': {
+ 'undo': function (params) {
+ params.startParent.moveTo(params.node, params.startIndex);
+ },
+ 'redo': function (params) {
+ params.endParent.moveTo(params.node, params.endIndex);
+ }
},
- 'redo': function (params) {
- params.endParent.moveTo(params.node, params.endIndex);
- }
- },
- 'sort': {
- 'undo': function (params) {
- var node = params.node;
- node.hideChilds();
- node.sort = params.oldSort;
- node.childs = params.oldChilds;
- node.showChilds();
- },
- 'redo': function (params) {
- var node = params.node;
- node.hideChilds();
- node.sort = params.newSort;
- node.childs = params.newChilds;
- node.showChilds();
- }
- }
-
- // TODO: restore the original caret position and selection with each undo
- // TODO: implement history for actions "expand", "collapse", "scroll", "setDocument"
- };
-}
-
-/**
- * The method onChange is executed when the History is changed, and can
- * be overloaded.
- */
-History.prototype.onChange = function () {};
-
-/**
- * Add a new action to the history
- * @param {String} action The executed action. Available actions: "editField",
- * "editValue", "changeType", "appendNode",
- * "removeNode", "duplicateNode", "moveNode"
- * @param {Object} params Object containing parameters describing the change.
- * The parameters in params depend on the action (for
- * example for "editValue" the Node, old value, and new
- * value are provided). params contains all information
- * needed to undo or redo the action.
- */
-History.prototype.add = function (action, params) {
- this.index++;
- this.history[this.index] = {
- 'action': action,
- 'params': params,
- 'timestamp': new Date()
- };
-
- // remove redo actions which are invalid now
- if (this.index < this.history.length - 1) {
- this.history.splice(this.index + 1, this.history.length - this.index - 1);
- }
-
- // fire onchange event
- this.onChange();
-};
-
-/**
- * Clear history
- */
-History.prototype.clear = function () {
- this.history = [];
- this.index = -1;
-
- // fire onchange event
- this.onChange();
-};
-
-/**
- * Check if there is an action available for undo
- * @return {Boolean} canUndo
- */
-History.prototype.canUndo = function () {
- return (this.index >= 0);
-};
-
-/**
- * Check if there is an action available for redo
- * @return {Boolean} canRedo
- */
-History.prototype.canRedo = function () {
- return (this.index < this.history.length - 1);
-};
-
-/**
- * Undo the last action
- */
-History.prototype.undo = function () {
- if (this.canUndo()) {
- var obj = this.history[this.index];
- if (obj) {
- var action = this.actions[obj.action];
- if (action && action.undo) {
- action.undo(obj.params);
- if (obj.params.oldSelection) {
- this.editor.setSelection(obj.params.oldSelection);
+ 'sort': {
+ 'undo': function (params) {
+ var node = params.node;
+ node.hideChilds();
+ node.sort = params.oldSort;
+ node.childs = params.oldChilds;
+ node.showChilds();
+ },
+ 'redo': function (params) {
+ var node = params.node;
+ node.hideChilds();
+ node.sort = params.newSort;
+ node.childs = params.newChilds;
+ node.showChilds();
}
}
- else {
- util.log('Error: unknown action "' + obj.action + '"');
- }
- }
- this.index--;
- // fire onchange event
- this.onChange();
+ // TODO: restore the original caret position and selection with each undo
+ // TODO: implement history for actions "expand", "collapse", "scroll", "setDocument"
+ };
}
-};
-/**
- * Redo the last action
- */
-History.prototype.redo = function () {
- if (this.canRedo()) {
+ /**
+ * The method onChange is executed when the History is changed, and can
+ * be overloaded.
+ */
+ History.prototype.onChange = function () {};
+
+ /**
+ * Add a new action to the history
+ * @param {String} action The executed action. Available actions: "editField",
+ * "editValue", "changeType", "appendNode",
+ * "removeNode", "duplicateNode", "moveNode"
+ * @param {Object} params Object containing parameters describing the change.
+ * The parameters in params depend on the action (for
+ * example for "editValue" the Node, old value, and new
+ * value are provided). params contains all information
+ * needed to undo or redo the action.
+ */
+ History.prototype.add = function (action, params) {
this.index++;
+ this.history[this.index] = {
+ 'action': action,
+ 'params': params,
+ 'timestamp': new Date()
+ };
- var obj = this.history[this.index];
- if (obj) {
- var action = this.actions[obj.action];
- if (action && action.redo) {
- action.redo(obj.params);
- if (obj.params.newSelection) {
- this.editor.setSelection(obj.params.newSelection);
- }
- }
- else {
- util.log('Error: unknown action "' + obj.action + '"');
- }
+ // remove redo actions which are invalid now
+ if (this.index < this.history.length - 1) {
+ this.history.splice(this.index + 1, this.history.length - this.index - 1);
}
// fire onchange event
this.onChange();
- }
-};
+ };
+
+ /**
+ * Clear history
+ */
+ History.prototype.clear = function () {
+ this.history = [];
+ this.index = -1;
+
+ // fire onchange event
+ this.onChange();
+ };
+
+ /**
+ * Check if there is an action available for undo
+ * @return {Boolean} canUndo
+ */
+ History.prototype.canUndo = function () {
+ return (this.index >= 0);
+ };
+
+ /**
+ * Check if there is an action available for redo
+ * @return {Boolean} canRedo
+ */
+ History.prototype.canRedo = function () {
+ return (this.index < this.history.length - 1);
+ };
+
+ /**
+ * Undo the last action
+ */
+ History.prototype.undo = function () {
+ if (this.canUndo()) {
+ var obj = this.history[this.index];
+ if (obj) {
+ var action = this.actions[obj.action];
+ if (action && action.undo) {
+ action.undo(obj.params);
+ if (obj.params.oldSelection) {
+ this.editor.setSelection(obj.params.oldSelection);
+ }
+ }
+ else {
+ util.log('Error: unknown action "' + obj.action + '"');
+ }
+ }
+ this.index--;
+
+ // fire onchange event
+ this.onChange();
+ }
+ };
+
+ /**
+ * Redo the last action
+ */
+ History.prototype.redo = function () {
+ if (this.canRedo()) {
+ this.index++;
+
+ var obj = this.history[this.index];
+ if (obj) {
+ var action = this.actions[obj.action];
+ if (action && action.redo) {
+ action.redo(obj.params);
+ if (obj.params.newSelection) {
+ this.editor.setSelection(obj.params.newSelection);
+ }
+ }
+ else {
+ util.log('Error: unknown action "' + obj.action + '"');
+ }
+ }
+
+ // fire onchange event
+ this.onChange();
+ }
+ };
+
+ return History;
+});
diff --git a/src/js/JSONEditor.js b/src/js/JSONEditor.js
index 43c4564..1410e5e 100644
--- a/src/js/JSONEditor.js
+++ b/src/js/JSONEditor.js
@@ -1,49 +1,51 @@
-/**
- * @constructor JSONEditor
- * @param {Element} container Container element
- * @param {Object} [options] Object with options. available options:
- * {String} mode Editor mode. Available values:
- * 'tree' (default), 'view',
- * 'form', 'text', and 'code'.
- * {function} change Callback method, triggered
- * on change of contents
- * {Boolean} search Enable search box.
- * True by default
- * Only applicable for modes
- * 'tree', 'view', and 'form'
- * {Boolean} history Enable history (undo/redo).
- * True by default
- * Only applicable for modes
- * 'tree', 'view', and 'form'
- * {String} name Field name for the root node.
- * Only applicable for modes
- * 'tree', 'view', and 'form'
- * {Number} indentation Number of indentation
- * spaces. 4 by default.
- * Only applicable for
- * modes 'text' and 'code'
- * @param {Object | undefined} json JSON object
- */
-function JSONEditor (container, options, json) {
- if (!(this instanceof JSONEditor)) {
- throw new Error('JSONEditor constructor called without "new".');
+define(['./TreeEditor', './TextEditor', './util'], function (TreeEditor, TextEditor, util) {
+
+ /**
+ * @constructor JSONEditor
+ * @param {Element} container Container element
+ * @param {Object} [options] Object with options. available options:
+ * {String} mode Editor mode. Available values:
+ * 'tree' (default), 'view',
+ * 'form', 'text', and 'code'.
+ * {function} change Callback method, triggered
+ * on change of contents
+ * {Boolean} search Enable search box.
+ * True by default
+ * Only applicable for modes
+ * 'tree', 'view', and 'form'
+ * {Boolean} history Enable history (undo/redo).
+ * True by default
+ * Only applicable for modes
+ * 'tree', 'view', and 'form'
+ * {String} name Field name for the root node.
+ * Only applicable for modes
+ * 'tree', 'view', and 'form'
+ * {Number} indentation Number of indentation
+ * spaces. 4 by default.
+ * Only applicable for
+ * modes 'text' and 'code'
+ * @param {Object | undefined} json JSON object
+ */
+ function JSONEditor (container, options, json) {
+ if (!(this instanceof JSONEditor)) {
+ throw new Error('JSONEditor constructor called without "new".');
+ }
+
+ // check for unsupported browser (IE8 and older)
+ var ieVersion = util.getInternetExplorerVersion();
+ if (ieVersion != -1 && ieVersion < 9) {
+ throw new Error('Unsupported browser, IE9 or newer required. ' +
+ 'Please install the newest version of your browser.');
+ }
+
+ if (arguments.length) {
+ this._create(container, options, json);
+ }
}
- // check for unsupported browser (IE8 and older)
- var ieVersion = util.getInternetExplorerVersion();
- if (ieVersion != -1 && ieVersion < 9) {
- throw new Error('Unsupported browser, IE9 or newer required. ' +
- 'Please install the newest version of your browser.');
- }
-
- if (arguments.length) {
- this._create(container, options, json);
- }
-}
-
-/**
- * Configuration for all registered modes. Example:
- * {
+ /**
+ * Configuration for all registered modes. Example:
+ * {
* tree: {
* editor: TreeEditor,
* data: 'json'
@@ -53,161 +55,186 @@ function JSONEditor (container, options, json) {
* data: 'text'
* }
* }
- *
- * @type { Object. }
- */
-JSONEditor.modes = {};
+ *
+ * @type { Object. }
+ */
+ JSONEditor.modes = {};
-/**
- * Create the JSONEditor
- * @param {Element} container Container element
- * @param {Object} [options] See description in constructor
- * @param {Object | undefined} json JSON object
- * @private
- */
-JSONEditor.prototype._create = function (container, options, json) {
- this.container = container;
- this.options = options || {};
- this.json = json || {};
+ /**
+ * Create the JSONEditor
+ * @param {Element} container Container element
+ * @param {Object} [options] See description in constructor
+ * @param {Object | undefined} json JSON object
+ * @private
+ */
+ JSONEditor.prototype._create = function (container, options, json) {
+ this.container = container;
+ this.options = options || {};
+ this.json = json || {};
- var mode = this.options.mode || 'tree';
- this.setMode(mode);
-};
+ var mode = this.options.mode || 'tree';
+ this.setMode(mode);
+ };
-/**
- * Detach the editor from the DOM
- * @private
- */
-JSONEditor.prototype._delete = function () {};
+ /**
+ * Detach the editor from the DOM
+ * @private
+ */
+ JSONEditor.prototype._delete = function () {};
-/**
- * Set JSON object in editor
- * @param {Object | undefined} json JSON data
- */
-JSONEditor.prototype.set = function (json) {
- this.json = json;
-};
+ /**
+ * Set JSON object in editor
+ * @param {Object | undefined} json JSON data
+ */
+ JSONEditor.prototype.set = function (json) {
+ this.json = json;
+ };
-/**
- * Get JSON from the editor
- * @returns {Object} json
- */
-JSONEditor.prototype.get = function () {
- return this.json;
-};
+ /**
+ * Get JSON from the editor
+ * @returns {Object} json
+ */
+ JSONEditor.prototype.get = function () {
+ return this.json;
+ };
-/**
- * Set string containing JSON for the editor
- * @param {String | undefined} jsonText
- */
-JSONEditor.prototype.setText = function (jsonText) {
- this.json = util.parse(jsonText);
-};
+ /**
+ * Set string containing JSON for the editor
+ * @param {String | undefined} jsonText
+ */
+ JSONEditor.prototype.setText = function (jsonText) {
+ this.json = util.parse(jsonText);
+ };
-/**
- * Get stringified JSON contents from the editor
- * @returns {String} jsonText
- */
-JSONEditor.prototype.getText = function () {
- return JSON.stringify(this.json);
-};
+ /**
+ * Get stringified JSON contents from the editor
+ * @returns {String} jsonText
+ */
+ JSONEditor.prototype.getText = function () {
+ return JSON.stringify(this.json);
+ };
-/**
- * Set a field name for the root node.
- * @param {String | undefined} name
- */
-JSONEditor.prototype.setName = function (name) {
- if (!this.options) {
- this.options = {};
- }
- this.options.name = name;
-};
+ /**
+ * Set a field name for the root node.
+ * @param {String | undefined} name
+ */
+ JSONEditor.prototype.setName = function (name) {
+ if (!this.options) {
+ this.options = {};
+ }
+ this.options.name = name;
+ };
-/**
- * Get the field name for the root node.
- * @return {String | undefined} name
- */
-JSONEditor.prototype.getName = function () {
- return this.options && this.options.name;
-};
+ /**
+ * Get the field name for the root node.
+ * @return {String | undefined} name
+ */
+ JSONEditor.prototype.getName = function () {
+ return this.options && this.options.name;
+ };
-/**
- * Change the mode of the editor.
- * JSONEditor will be extended with all methods needed for the chosen mode.
- * @param {String} mode Available modes: 'tree' (default), 'view', 'form',
- * 'text', and 'code'.
- */
-JSONEditor.prototype.setMode = function (mode) {
- var container = this.container,
- options = util.extend({}, this.options),
- data,
- name;
+ /**
+ * Change the mode of the editor.
+ * JSONEditor will be extended with all methods needed for the chosen mode.
+ * @param {String} mode Available modes: 'tree' (default), 'view', 'form',
+ * 'text', and 'code'.
+ */
+ JSONEditor.prototype.setMode = function (mode) {
+ var container = this.container,
+ options = util.extend({}, this.options),
+ data,
+ name;
- options.mode = mode;
- var config = JSONEditor.modes[mode];
- if (config) {
- try {
- if (config.data == 'text') {
- // text
- name = this.getName();
- data = this.getText();
+ options.mode = mode;
+ var config = JSONEditor.modes[mode];
+ if (config) {
+ try {
+ if (config.data == 'text') {
+ // text
+ name = this.getName();
+ data = this.getText();
- this._delete();
- util.clear(this);
- util.extend(this, config.editor.prototype);
- this._create(container, options);
+ this._delete();
+ util.clear(this);
+ util.extend(this, config.editor.prototype);
+ this._create(container, options);
- this.setName(name);
- this.setText(data);
- }
- else {
- // json
- name = this.getName();
- data = this.get();
-
- this._delete();
- util.clear(this);
- util.extend(this, config.editor.prototype);
- this._create(container, options);
-
- this.setName(name);
- this.set(data);
- }
-
- if (typeof config.load === 'function') {
- try {
- config.load.call(this);
+ this.setName(name);
+ this.setText(data);
}
- catch (err) {}
+ else {
+ // json
+ name = this.getName();
+ data = this.get();
+
+ this._delete();
+ util.clear(this);
+ util.extend(this, config.editor.prototype);
+ this._create(container, options);
+
+ this.setName(name);
+ this.set(data);
+ }
+
+ if (typeof config.load === 'function') {
+ try {
+ config.load.call(this);
+ }
+ catch (err) {}
+ }
+ }
+ catch (err) {
+ this._onError(err);
}
}
- catch (err) {
- this._onError(err);
+ else {
+ throw new Error('Unknown mode "' + options.mode + '"');
}
- }
- else {
- throw new Error('Unknown mode "' + options.mode + '"');
- }
-};
+ };
-/**
- * Throw an error. If an error callback is configured in options.error, this
- * callback will be invoked. Else, a regular error is thrown.
- * @param {Error} err
- * @private
- */
-JSONEditor.prototype._onError = function(err) {
- // TODO: onError is deprecated since version 2.2.0. cleanup some day
- if (typeof this.onError === 'function') {
- util.log('WARNING: JSONEditor.onError is deprecated. ' +
- 'Use options.error instead.');
- this.onError(err);
- }
+ /**
+ * Throw an error. If an error callback is configured in options.error, this
+ * callback will be invoked. Else, a regular error is thrown.
+ * @param {Error} err
+ * @private
+ */
+ JSONEditor.prototype._onError = function(err) {
+ // TODO: onError is deprecated since version 2.2.0. cleanup some day
+ if (typeof this.onError === 'function') {
+ util.log('WARNING: JSONEditor.onError is deprecated. ' +
+ 'Use options.error instead.');
+ this.onError(err);
+ }
- if (this.options && typeof this.options.error === 'function') {
- this.options.error(err);
- }
- else {
- throw err;
- }
-};
+ if (this.options && typeof this.options.error === 'function') {
+ this.options.error(err);
+ }
+ else {
+ throw err;
+ }
+ };
+
+ /**
+ * Register modes for the JSON Editor
+ * TODO: describe the mode format
+ * @param {Object} modes An object with the mode names as keys, and an object
+ * defining the mode as value
+ */
+ JSONEditor.registerModes = function (modes) {
+ for (var mode in modes) {
+ if (modes.hasOwnProperty(mode)) {
+ if (mode in JSONEditor.modes) {
+ throw new Error('Mode "' + mode + '" already registered');
+ }
+
+ JSONEditor.modes[mode] = modes[mode];
+ }
+ }
+ };
+
+ // register TreeEditor and TextEditor
+ JSONEditor.registerModes(TreeEditor.modes);
+ JSONEditor.registerModes(TextEditor.modes);
+
+ return JSONEditor;
+});
\ No newline at end of file
diff --git a/src/js/Node.js b/src/js/Node.js
index 4915229..92dab3c 100644
--- a/src/js/Node.js
+++ b/src/js/Node.js
@@ -1,2630 +1,2678 @@
-/**
- * @constructor Node
- * Create a new Node
- * @param {TreeEditor} editor
- * @param {Object} [params] Can contain parameters:
- * {string} field
- * {boolean} fieldEditable
- * {*} value
- * {String} type Can have values 'auto', 'array',
- * 'object', or 'string'.
- */
-function Node (editor, params) {
- /** @type {TreeEditor} */
- this.editor = editor;
- this.dom = {};
- this.expanded = false;
+define(['./ContextMenu', './appendNodeFactory', './util'], function (ContextMenu, appendNodeFactory, util) {
- if(params && (params instanceof Object)) {
- this.setField(params.field, params.fieldEditable);
- this.setValue(params.value, params.type);
- }
- else {
- this.setField('');
- this.setValue(null);
- }
-};
+ /**
+ * @constructor Node
+ * Create a new Node
+ * @param {TreeEditor} editor
+ * @param {Object} [params] Can contain parameters:
+ * {string} field
+ * {boolean} fieldEditable
+ * {*} value
+ * {String} type Can have values 'auto', 'array',
+ * 'object', or 'string'.
+ */
+ function Node (editor, params) {
+ /** @type {TreeEditor} */
+ this.editor = editor;
+ this.dom = {};
+ this.expanded = false;
-/**
- * Set parent node
- * @param {Node} parent
- */
-Node.prototype.setParent = function(parent) {
- this.parent = parent;
-};
-
-/**
- * Set field
- * @param {String} field
- * @param {boolean} [fieldEditable]
- */
-Node.prototype.setField = function(field, fieldEditable) {
- this.field = field;
- this.fieldEditable = (fieldEditable == true);
-};
-
-/**
- * Get field
- * @return {String}
- */
-Node.prototype.getField = function() {
- if (this.field === undefined) {
- this._getDomField();
- }
-
- return this.field;
-};
-
-/**
- * Set value. Value is a JSON structure or an element String, Boolean, etc.
- * @param {*} value
- * @param {String} [type] Specify the type of the value. Can be 'auto',
- * 'array', 'object', or 'string'
- */
-Node.prototype.setValue = function(value, type) {
- var childValue, child;
-
- // first clear all current childs (if any)
- var childs = this.childs;
- if (childs) {
- while (childs.length) {
- this.removeChild(childs[0]);
- }
- }
-
- // TODO: remove the DOM of this Node
-
- this.type = this._getType(value);
-
- // check if type corresponds with the provided type
- if (type && type != this.type) {
- if (type == 'string' && this.type == 'auto') {
- this.type = type;
+ if(params && (params instanceof Object)) {
+ this.setField(params.field, params.fieldEditable);
+ this.setValue(params.value, params.type);
}
else {
- throw new Error('Type mismatch: ' +
- 'cannot cast value of type "' + this.type +
- ' to the specified type "' + type + '"');
+ this.setField('');
+ this.setValue(null);
}
}
- if (this.type == 'array') {
- // array
- this.childs = [];
- for (var i = 0, iMax = value.length; i < iMax; i++) {
- childValue = value[i];
- if (childValue !== undefined && !(childValue instanceof Function)) {
- // ignore undefined and functions
- child = new Node(this.editor, {
- 'value': childValue
- });
- this.appendChild(child);
+ /**
+ * Set parent node
+ * @param {Node} parent
+ */
+ Node.prototype.setParent = function(parent) {
+ this.parent = parent;
+ };
+
+ /**
+ * Set field
+ * @param {String} field
+ * @param {boolean} [fieldEditable]
+ */
+ Node.prototype.setField = function(field, fieldEditable) {
+ this.field = field;
+ this.fieldEditable = (fieldEditable == true);
+ };
+
+ /**
+ * Get field
+ * @return {String}
+ */
+ Node.prototype.getField = function() {
+ if (this.field === undefined) {
+ this._getDomField();
+ }
+
+ return this.field;
+ };
+
+ /**
+ * Set value. Value is a JSON structure or an element String, Boolean, etc.
+ * @param {*} value
+ * @param {String} [type] Specify the type of the value. Can be 'auto',
+ * 'array', 'object', or 'string'
+ */
+ Node.prototype.setValue = function(value, type) {
+ var childValue, child;
+
+ // first clear all current childs (if any)
+ var childs = this.childs;
+ if (childs) {
+ while (childs.length) {
+ this.removeChild(childs[0]);
}
}
- this.value = '';
- }
- else if (this.type == 'object') {
- // object
- this.childs = [];
- for (var childField in value) {
- if (value.hasOwnProperty(childField)) {
- childValue = value[childField];
+
+ // TODO: remove the DOM of this Node
+
+ this.type = this._getType(value);
+
+ // check if type corresponds with the provided type
+ if (type && type != this.type) {
+ if (type == 'string' && this.type == 'auto') {
+ this.type = type;
+ }
+ else {
+ throw new Error('Type mismatch: ' +
+ 'cannot cast value of type "' + this.type +
+ ' to the specified type "' + type + '"');
+ }
+ }
+
+ if (this.type == 'array') {
+ // array
+ this.childs = [];
+ for (var i = 0, iMax = value.length; i < iMax; i++) {
+ childValue = value[i];
if (childValue !== undefined && !(childValue instanceof Function)) {
// ignore undefined and functions
child = new Node(this.editor, {
- 'field': childField,
'value': childValue
});
this.appendChild(child);
}
}
+ this.value = '';
}
- this.value = '';
- }
- else {
- // value
- this.childs = undefined;
- this.value = value;
- /* TODO
- if (typeof(value) == 'string') {
- var escValue = JSON.stringify(value);
- this.value = escValue.substring(1, escValue.length - 1);
- util.log('check', value, this.value);
- }
- else {
- this.value = value;
- }
- */
- }
-};
-
-/**
- * Get value. Value is a JSON structure
- * @return {*} value
- */
-Node.prototype.getValue = function() {
- //var childs, i, iMax;
-
- if (this.type == 'array') {
- var arr = [];
- this.childs.forEach (function (child) {
- arr.push(child.getValue());
- });
- return arr;
- }
- else if (this.type == 'object') {
- var obj = {};
- this.childs.forEach (function (child) {
- obj[child.getField()] = child.getValue();
- });
- return obj;
- }
- else {
- if (this.value === undefined) {
- this._getDomValue();
- }
-
- return this.value;
- }
-};
-
-/**
- * Get the nesting level of this node
- * @return {Number} level
- */
-Node.prototype.getLevel = function() {
- return (this.parent ? this.parent.getLevel() + 1 : 0);
-};
-
-/**
- * Create a clone of a node
- * The complete state of a clone is copied, including whether it is expanded or
- * not. The DOM elements are not cloned.
- * @return {Node} clone
- */
-Node.prototype.clone = function() {
- var clone = new Node(this.editor);
- clone.type = this.type;
- clone.field = this.field;
- clone.fieldInnerText = this.fieldInnerText;
- clone.fieldEditable = this.fieldEditable;
- clone.value = this.value;
- clone.valueInnerText = this.valueInnerText;
- clone.expanded = this.expanded;
-
- if (this.childs) {
- // an object or array
- var cloneChilds = [];
- this.childs.forEach(function (child) {
- var childClone = child.clone();
- childClone.setParent(clone);
- cloneChilds.push(childClone);
- });
- clone.childs = cloneChilds;
- }
- else {
- // a value
- clone.childs = undefined;
- }
-
- return clone;
-};
-
-/**
- * Expand this node and optionally its childs.
- * @param {boolean} [recurse] Optional recursion, true by default. When
- * true, all childs will be expanded recursively
- */
-Node.prototype.expand = function(recurse) {
- if (!this.childs) {
- return;
- }
-
- // set this node expanded
- this.expanded = true;
- if (this.dom.expand) {
- this.dom.expand.className = 'expanded';
- }
-
- this.showChilds();
-
- if (recurse != false) {
- this.childs.forEach(function (child) {
- child.expand(recurse);
- });
- }
-};
-
-/**
- * Collapse this node and optionally its childs.
- * @param {boolean} [recurse] Optional recursion, true by default. When
- * true, all childs will be collapsed recursively
- */
-Node.prototype.collapse = function(recurse) {
- if (!this.childs) {
- return;
- }
-
- this.hideChilds();
-
- // collapse childs in case of recurse
- if (recurse != false) {
- this.childs.forEach(function (child) {
- child.collapse(recurse);
- });
-
- }
-
- // make this node collapsed
- if (this.dom.expand) {
- this.dom.expand.className = 'collapsed';
- }
- this.expanded = false;
-};
-
-/**
- * Recursively show all childs when they are expanded
- */
-Node.prototype.showChilds = function() {
- var childs = this.childs;
- if (!childs) {
- return;
- }
- if (!this.expanded) {
- return;
- }
-
- var tr = this.dom.tr;
- var table = tr ? tr.parentNode : undefined;
- if (table) {
- // show row with append button
- var append = this.getAppend();
- var nextTr = tr.nextSibling;
- if (nextTr) {
- table.insertBefore(append, nextTr);
+ else if (this.type == 'object') {
+ // object
+ this.childs = [];
+ for (var childField in value) {
+ if (value.hasOwnProperty(childField)) {
+ childValue = value[childField];
+ if (childValue !== undefined && !(childValue instanceof Function)) {
+ // ignore undefined and functions
+ child = new Node(this.editor, {
+ 'field': childField,
+ 'value': childValue
+ });
+ this.appendChild(child);
+ }
+ }
+ }
+ this.value = '';
}
else {
- table.appendChild(append);
+ // value
+ this.childs = undefined;
+ this.value = value;
+ /* TODO
+ if (typeof(value) == 'string') {
+ var escValue = JSON.stringify(value);
+ this.value = escValue.substring(1, escValue.length - 1);
+ util.log('check', value, this.value);
+ }
+ else {
+ this.value = value;
+ }
+ */
}
+ };
- // show childs
- this.childs.forEach(function (child) {
- table.insertBefore(child.getDom(), append);
- child.showChilds();
- });
- }
-};
+ /**
+ * Get value. Value is a JSON structure
+ * @return {*} value
+ */
+ Node.prototype.getValue = function() {
+ //var childs, i, iMax;
-/**
- * Hide the node with all its childs
- */
-Node.prototype.hide = function() {
- var tr = this.dom.tr;
- var table = tr ? tr.parentNode : undefined;
- if (table) {
- table.removeChild(tr);
- }
- this.hideChilds();
-};
-
-
-/**
- * Recursively hide all childs
- */
-Node.prototype.hideChilds = function() {
- var childs = this.childs;
- if (!childs) {
- return;
- }
- if (!this.expanded) {
- return;
- }
-
- // hide append row
- var append = this.getAppend();
- if (append.parentNode) {
- append.parentNode.removeChild(append);
- }
-
- // hide childs
- this.childs.forEach(function (child) {
- child.hide();
- });
-};
-
-
-/**
- * Add a new child to the node.
- * Only applicable when Node value is of type array or object
- * @param {Node} node
- */
-Node.prototype.appendChild = function(node) {
- if (this._hasChilds()) {
- // adjust the link to the parent
- node.setParent(this);
- node.fieldEditable = (this.type == 'object');
if (this.type == 'array') {
- node.index = this.childs.length;
+ var arr = [];
+ this.childs.forEach (function (child) {
+ arr.push(child.getValue());
+ });
+ return arr;
}
- this.childs.push(node);
-
- if (this.expanded) {
- // insert into the DOM, before the appendRow
- var newTr = node.getDom();
- var appendTr = this.getAppend();
- var table = appendTr ? appendTr.parentNode : undefined;
- if (appendTr && table) {
- table.insertBefore(newTr, appendTr);
+ else if (this.type == 'object') {
+ var obj = {};
+ this.childs.forEach (function (child) {
+ obj[child.getField()] = child.getValue();
+ });
+ return obj;
+ }
+ else {
+ if (this.value === undefined) {
+ this._getDomValue();
}
- node.showChilds();
+ return this.value;
+ }
+ };
+
+ /**
+ * Get the nesting level of this node
+ * @return {Number} level
+ */
+ Node.prototype.getLevel = function() {
+ return (this.parent ? this.parent.getLevel() + 1 : 0);
+ };
+
+ /**
+ * Create a clone of a node
+ * The complete state of a clone is copied, including whether it is expanded or
+ * not. The DOM elements are not cloned.
+ * @return {Node} clone
+ */
+ Node.prototype.clone = function() {
+ var clone = new Node(this.editor);
+ clone.type = this.type;
+ clone.field = this.field;
+ clone.fieldInnerText = this.fieldInnerText;
+ clone.fieldEditable = this.fieldEditable;
+ clone.value = this.value;
+ clone.valueInnerText = this.valueInnerText;
+ clone.expanded = this.expanded;
+
+ if (this.childs) {
+ // an object or array
+ var cloneChilds = [];
+ this.childs.forEach(function (child) {
+ var childClone = child.clone();
+ childClone.setParent(clone);
+ cloneChilds.push(childClone);
+ });
+ clone.childs = cloneChilds;
+ }
+ else {
+ // a value
+ clone.childs = undefined;
}
- this.updateDom({'updateIndexes': true});
- node.updateDom({'recurse': true});
- }
-};
+ return clone;
+ };
-
-/**
- * Move a node from its current parent to this node
- * Only applicable when Node value is of type array or object
- * @param {Node} node
- * @param {Node} beforeNode
- */
-Node.prototype.moveBefore = function(node, beforeNode) {
- if (this._hasChilds()) {
- // create a temporary row, to prevent the scroll position from jumping
- // when removing the node
- var tbody = (this.dom.tr) ? this.dom.tr.parentNode : undefined;
- if (tbody) {
- var trTemp = document.createElement('tr');
- trTemp.style.height = tbody.clientHeight + 'px';
- tbody.appendChild(trTemp);
+ /**
+ * Expand this node and optionally its childs.
+ * @param {boolean} [recurse] Optional recursion, true by default. When
+ * true, all childs will be expanded recursively
+ */
+ Node.prototype.expand = function(recurse) {
+ if (!this.childs) {
+ return;
}
+ // set this node expanded
+ this.expanded = true;
+ if (this.dom.expand) {
+ this.dom.expand.className = 'expanded';
+ }
+
+ this.showChilds();
+
+ if (recurse != false) {
+ this.childs.forEach(function (child) {
+ child.expand(recurse);
+ });
+ }
+ };
+
+ /**
+ * Collapse this node and optionally its childs.
+ * @param {boolean} [recurse] Optional recursion, true by default. When
+ * true, all childs will be collapsed recursively
+ */
+ Node.prototype.collapse = function(recurse) {
+ if (!this.childs) {
+ return;
+ }
+
+ this.hideChilds();
+
+ // collapse childs in case of recurse
+ if (recurse != false) {
+ this.childs.forEach(function (child) {
+ child.collapse(recurse);
+ });
+
+ }
+
+ // make this node collapsed
+ if (this.dom.expand) {
+ this.dom.expand.className = 'collapsed';
+ }
+ this.expanded = false;
+ };
+
+ /**
+ * Recursively show all childs when they are expanded
+ */
+ Node.prototype.showChilds = function() {
+ var childs = this.childs;
+ if (!childs) {
+ return;
+ }
+ if (!this.expanded) {
+ return;
+ }
+
+ var tr = this.dom.tr;
+ var table = tr ? tr.parentNode : undefined;
+ if (table) {
+ // show row with append button
+ var append = this.getAppend();
+ var nextTr = tr.nextSibling;
+ if (nextTr) {
+ table.insertBefore(append, nextTr);
+ }
+ else {
+ table.appendChild(append);
+ }
+
+ // show childs
+ this.childs.forEach(function (child) {
+ table.insertBefore(child.getDom(), append);
+ child.showChilds();
+ });
+ }
+ };
+
+ /**
+ * Hide the node with all its childs
+ */
+ Node.prototype.hide = function() {
+ var tr = this.dom.tr;
+ var table = tr ? tr.parentNode : undefined;
+ if (table) {
+ table.removeChild(tr);
+ }
+ this.hideChilds();
+ };
+
+
+ /**
+ * Recursively hide all childs
+ */
+ Node.prototype.hideChilds = function() {
+ var childs = this.childs;
+ if (!childs) {
+ return;
+ }
+ if (!this.expanded) {
+ return;
+ }
+
+ // hide append row
+ var append = this.getAppend();
+ if (append.parentNode) {
+ append.parentNode.removeChild(append);
+ }
+
+ // hide childs
+ this.childs.forEach(function (child) {
+ child.hide();
+ });
+ };
+
+
+ /**
+ * Add a new child to the node.
+ * Only applicable when Node value is of type array or object
+ * @param {Node} node
+ */
+ Node.prototype.appendChild = function(node) {
+ if (this._hasChilds()) {
+ // adjust the link to the parent
+ node.setParent(this);
+ node.fieldEditable = (this.type == 'object');
+ if (this.type == 'array') {
+ node.index = this.childs.length;
+ }
+ this.childs.push(node);
+
+ if (this.expanded) {
+ // insert into the DOM, before the appendRow
+ var newTr = node.getDom();
+ var appendTr = this.getAppend();
+ var table = appendTr ? appendTr.parentNode : undefined;
+ if (appendTr && table) {
+ table.insertBefore(newTr, appendTr);
+ }
+
+ node.showChilds();
+ }
+
+ this.updateDom({'updateIndexes': true});
+ node.updateDom({'recurse': true});
+ }
+ };
+
+
+ /**
+ * Move a node from its current parent to this node
+ * Only applicable when Node value is of type array or object
+ * @param {Node} node
+ * @param {Node} beforeNode
+ */
+ Node.prototype.moveBefore = function(node, beforeNode) {
+ if (this._hasChilds()) {
+ // create a temporary row, to prevent the scroll position from jumping
+ // when removing the node
+ var tbody = (this.dom.tr) ? this.dom.tr.parentNode : undefined;
+ if (tbody) {
+ var trTemp = document.createElement('tr');
+ trTemp.style.height = tbody.clientHeight + 'px';
+ tbody.appendChild(trTemp);
+ }
+
+ if (node.parent) {
+ node.parent.removeChild(node);
+ }
+
+ if (beforeNode instanceof AppendNode) {
+ this.appendChild(node);
+ }
+ else {
+ this.insertBefore(node, beforeNode);
+ }
+
+ if (tbody) {
+ tbody.removeChild(trTemp);
+ }
+ }
+ };
+
+ /**
+ * Move a node from its current parent to this node
+ * Only applicable when Node value is of type array or object.
+ * If index is out of range, the node will be appended to the end
+ * @param {Node} node
+ * @param {Number} index
+ */
+ Node.prototype.moveTo = function (node, index) {
+ if (node.parent == this) {
+ // same parent
+ var currentIndex = this.childs.indexOf(node);
+ if (currentIndex < index) {
+ // compensate the index for removal of the node itself
+ index++;
+ }
+ }
+
+ var beforeNode = this.childs[index] || this.append;
+ this.moveBefore(node, beforeNode);
+ };
+
+ /**
+ * Insert a new child before a given node
+ * Only applicable when Node value is of type array or object
+ * @param {Node} node
+ * @param {Node} beforeNode
+ */
+ Node.prototype.insertBefore = function(node, beforeNode) {
+ if (this._hasChilds()) {
+ if (beforeNode == this.append) {
+ // append to the child nodes
+
+ // adjust the link to the parent
+ node.setParent(this);
+ node.fieldEditable = (this.type == 'object');
+ this.childs.push(node);
+ }
+ else {
+ // insert before a child node
+ var index = this.childs.indexOf(beforeNode);
+ if (index == -1) {
+ throw new Error('Node not found');
+ }
+
+ // adjust the link to the parent
+ node.setParent(this);
+ node.fieldEditable = (this.type == 'object');
+ this.childs.splice(index, 0, node);
+ }
+
+ if (this.expanded) {
+ // insert into the DOM
+ var newTr = node.getDom();
+ var nextTr = beforeNode.getDom();
+ var table = nextTr ? nextTr.parentNode : undefined;
+ if (nextTr && table) {
+ table.insertBefore(newTr, nextTr);
+ }
+
+ node.showChilds();
+ }
+
+ this.updateDom({'updateIndexes': true});
+ node.updateDom({'recurse': true});
+ }
+ };
+
+ /**
+ * Insert a new child before a given node
+ * Only applicable when Node value is of type array or object
+ * @param {Node} node
+ * @param {Node} afterNode
+ */
+ Node.prototype.insertAfter = function(node, afterNode) {
+ if (this._hasChilds()) {
+ var index = this.childs.indexOf(afterNode);
+ var beforeNode = this.childs[index + 1];
+ if (beforeNode) {
+ this.insertBefore(node, beforeNode);
+ }
+ else {
+ this.appendChild(node);
+ }
+ }
+ };
+
+ /**
+ * Search in this node
+ * The node will be expanded when the text is found one of its childs, else
+ * it will be collapsed. Searches are case insensitive.
+ * @param {String} text
+ * @return {Node[]} results Array with nodes containing the search text
+ */
+ Node.prototype.search = function(text) {
+ var results = [];
+ var index;
+ var search = text ? text.toLowerCase() : undefined;
+
+ // delete old search data
+ delete this.searchField;
+ delete this.searchValue;
+
+ // search in field
+ if (this.field != undefined) {
+ var field = String(this.field).toLowerCase();
+ index = field.indexOf(search);
+ if (index != -1) {
+ this.searchField = true;
+ results.push({
+ 'node': this,
+ 'elem': 'field'
+ });
+ }
+
+ // update dom
+ this._updateDomField();
+ }
+
+ // search in value
+ if (this._hasChilds()) {
+ // array, object
+
+ // search the nodes childs
+ if (this.childs) {
+ var childResults = [];
+ this.childs.forEach(function (child) {
+ childResults = childResults.concat(child.search(text));
+ });
+ results = results.concat(childResults);
+ }
+
+ // update dom
+ if (search != undefined) {
+ var recurse = false;
+ if (childResults.length == 0) {
+ this.collapse(recurse);
+ }
+ else {
+ this.expand(recurse);
+ }
+ }
+ }
+ else {
+ // string, auto
+ if (this.value != undefined ) {
+ var value = String(this.value).toLowerCase();
+ index = value.indexOf(search);
+ if (index != -1) {
+ this.searchValue = true;
+ results.push({
+ 'node': this,
+ 'elem': 'value'
+ });
+ }
+ }
+
+ // update dom
+ this._updateDomValue();
+ }
+
+ return results;
+ };
+
+ /**
+ * Move the scroll position such that this node is in the visible area.
+ * The node will not get the focus
+ * @param {function(boolean)} [callback]
+ */
+ Node.prototype.scrollTo = function(callback) {
+ if (!this.dom.tr || !this.dom.tr.parentNode) {
+ // if the node is not visible, expand its parents
+ var parent = this.parent;
+ var recurse = false;
+ while (parent) {
+ parent.expand(recurse);
+ parent = parent.parent;
+ }
+ }
+
+ if (this.dom.tr && this.dom.tr.parentNode) {
+ this.editor.scrollTo(this.dom.tr.offsetTop, callback);
+ }
+ };
+
+
+// stores the element name currently having the focus
+ Node.focusElement = undefined;
+
+ /**
+ * Set focus to this node
+ * @param {String} [elementName] The field name of the element to get the
+ * focus available values: 'drag', 'menu',
+ * 'expand', 'field', 'value' (default)
+ */
+ Node.prototype.focus = function(elementName) {
+ Node.focusElement = elementName;
+
+ if (this.dom.tr && this.dom.tr.parentNode) {
+ var dom = this.dom;
+
+ switch (elementName) {
+ case 'drag':
+ if (dom.drag) {
+ dom.drag.focus();
+ }
+ else {
+ dom.menu.focus();
+ }
+ break;
+
+ case 'menu':
+ dom.menu.focus();
+ break;
+
+ case 'expand':
+ if (this._hasChilds()) {
+ dom.expand.focus();
+ }
+ else if (dom.field && this.fieldEditable) {
+ dom.field.focus();
+ util.selectContentEditable(dom.field);
+ }
+ else if (dom.value && !this._hasChilds()) {
+ dom.value.focus();
+ util.selectContentEditable(dom.value);
+ }
+ else {
+ dom.menu.focus();
+ }
+ break;
+
+ case 'field':
+ if (dom.field && this.fieldEditable) {
+ dom.field.focus();
+ util.selectContentEditable(dom.field);
+ }
+ else if (dom.value && !this._hasChilds()) {
+ dom.value.focus();
+ util.selectContentEditable(dom.value);
+ }
+ else if (this._hasChilds()) {
+ dom.expand.focus();
+ }
+ else {
+ dom.menu.focus();
+ }
+ break;
+
+ case 'value':
+ default:
+ if (dom.value && !this._hasChilds()) {
+ dom.value.focus();
+ util.selectContentEditable(dom.value);
+ }
+ else if (dom.field && this.fieldEditable) {
+ dom.field.focus();
+ util.selectContentEditable(dom.field);
+ }
+ else if (this._hasChilds()) {
+ dom.expand.focus();
+ }
+ else {
+ dom.menu.focus();
+ }
+ break;
+ }
+ }
+ };
+
+ /**
+ * Select all text in an editable div after a delay of 0 ms
+ * @param {Element} editableDiv
+ */
+ Node.select = function(editableDiv) {
+ setTimeout(function () {
+ util.selectContentEditable(editableDiv);
+ }, 0);
+ };
+
+ /**
+ * Update the values from the DOM field and value of this node
+ */
+ Node.prototype.blur = function() {
+ // retrieve the actual field and value from the DOM.
+ this._getDomValue(false);
+ this._getDomField(false);
+ };
+
+ /**
+ * Duplicate given child node
+ * new structure will be added right before the cloned node
+ * @param {Node} node the childNode to be duplicated
+ * @return {Node} clone the clone of the node
+ * @private
+ */
+ Node.prototype._duplicate = function(node) {
+ var clone = node.clone();
+
+ /* TODO: adjust the field name (to prevent equal field names)
+ if (this.type == 'object') {
+ }
+ */
+
+ this.insertAfter(clone, node);
+
+ return clone;
+ };
+
+ /**
+ * Check if given node is a child. The method will check recursively to find
+ * this node.
+ * @param {Node} node
+ * @return {boolean} containsNode
+ */
+ Node.prototype.containsNode = function(node) {
+ if (this == node) {
+ return true;
+ }
+
+ var childs = this.childs;
+ if (childs) {
+ // TODO: use the js5 Array.some() here?
+ for (var i = 0, iMax = childs.length; i < iMax; i++) {
+ if (childs[i].containsNode(node)) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ };
+
+ /**
+ * Move given node into this node
+ * @param {Node} node the childNode to be moved
+ * @param {Node} beforeNode node will be inserted before given
+ * node. If no beforeNode is given,
+ * the node is appended at the end
+ * @private
+ */
+ Node.prototype._move = function(node, beforeNode) {
+ if (node == beforeNode) {
+ // nothing to do...
+ return;
+ }
+
+ // check if this node is not a child of the node to be moved here
+ if (node.containsNode(this)) {
+ throw new Error('Cannot move a field into a child of itself');
+ }
+
+ // remove the original node
if (node.parent) {
node.parent.removeChild(node);
}
- if (beforeNode instanceof AppendNode) {
- this.appendChild(node);
+ // create a clone of the node
+ var clone = node.clone();
+ node.clearDom();
+
+ // insert or append the node
+ if (beforeNode) {
+ this.insertBefore(clone, beforeNode);
}
else {
- this.insertBefore(node, beforeNode);
+ this.appendChild(clone);
}
- if (tbody) {
- tbody.removeChild(trTemp);
+ /* TODO: adjust the field name (to prevent equal field names)
+ if (this.type == 'object') {
+ }
+ */
+ };
+
+ /**
+ * Remove a child from the node.
+ * Only applicable when Node value is of type array or object
+ * @param {Node} node The child node to be removed;
+ * @return {Node | undefined} node The removed node on success,
+ * else undefined
+ */
+ Node.prototype.removeChild = function(node) {
+ if (this.childs) {
+ var index = this.childs.indexOf(node);
+
+ if (index != -1) {
+ node.hide();
+
+ // delete old search results
+ delete node.searchField;
+ delete node.searchValue;
+
+ var removedNode = this.childs.splice(index, 1)[0];
+
+ this.updateDom({'updateIndexes': true});
+
+ return removedNode;
+ }
}
- }
-};
-/**
- * Move a node from its current parent to this node
- * Only applicable when Node value is of type array or object.
- * If index is out of range, the node will be appended to the end
- * @param {Node} node
- * @param {Number} index
- */
-Node.prototype.moveTo = function (node, index) {
- if (node.parent == this) {
- // same parent
- var currentIndex = this.childs.indexOf(node);
- if (currentIndex < index) {
- // compensate the index for removal of the node itself
- index++;
+ return undefined;
+ };
+
+ /**
+ * Remove a child node node from this node
+ * This method is equal to Node.removeChild, except that _remove firex an
+ * onChange event.
+ * @param {Node} node
+ * @private
+ */
+ Node.prototype._remove = function (node) {
+ this.removeChild(node);
+ };
+
+ /**
+ * Change the type of the value of this Node
+ * @param {String} newType
+ */
+ Node.prototype.changeType = function (newType) {
+ var oldType = this.type;
+
+ if (oldType == newType) {
+ // type is not changed
+ return;
}
- }
- var beforeNode = this.childs[index] || this.append;
- this.moveBefore(node, beforeNode);
-};
-
-/**
- * Insert a new child before a given node
- * Only applicable when Node value is of type array or object
- * @param {Node} node
- * @param {Node} beforeNode
- */
-Node.prototype.insertBefore = function(node, beforeNode) {
- if (this._hasChilds()) {
- if (beforeNode == this.append) {
- // append to the child nodes
-
- // adjust the link to the parent
- node.setParent(this);
- node.fieldEditable = (this.type == 'object');
- this.childs.push(node);
+ if ((newType == 'string' || newType == 'auto') &&
+ (oldType == 'string' || oldType == 'auto')) {
+ // this is an easy change
+ this.type = newType;
}
else {
- // insert before a child node
- var index = this.childs.indexOf(beforeNode);
- if (index == -1) {
- throw new Error('Node not found');
+ // change from array to object, or from string/auto to object/array
+ var table = this.dom.tr ? this.dom.tr.parentNode : undefined;
+ var lastTr;
+ if (this.expanded) {
+ lastTr = this.getAppend();
+ }
+ else {
+ lastTr = this.getDom();
+ }
+ var nextTr = (lastTr && lastTr.parentNode) ? lastTr.nextSibling : undefined;
+
+ // hide current field and all its childs
+ this.hide();
+ this.clearDom();
+
+ // adjust the field and the value
+ this.type = newType;
+
+ // adjust childs
+ if (newType == 'object') {
+ if (!this.childs) {
+ this.childs = [];
+ }
+
+ this.childs.forEach(function (child, index) {
+ child.clearDom();
+ delete child.index;
+ child.fieldEditable = true;
+ if (child.field == undefined) {
+ child.field = '';
+ }
+ });
+
+ if (oldType == 'string' || oldType == 'auto') {
+ this.expanded = true;
+ }
+ }
+ else if (newType == 'array') {
+ if (!this.childs) {
+ this.childs = [];
+ }
+
+ this.childs.forEach(function (child, index) {
+ child.clearDom();
+ child.fieldEditable = false;
+ child.index = index;
+ });
+
+ if (oldType == 'string' || oldType == 'auto') {
+ this.expanded = true;
+ }
+ }
+ else {
+ this.expanded = false;
}
- // adjust the link to the parent
- node.setParent(this);
- node.fieldEditable = (this.type == 'object');
- this.childs.splice(index, 0, node);
+ // create new DOM
+ if (table) {
+ if (nextTr) {
+ table.insertBefore(this.getDom(), nextTr);
+ }
+ else {
+ table.appendChild(this.getDom());
+ }
+ }
+ this.showChilds();
}
- if (this.expanded) {
- // insert into the DOM
- var newTr = node.getDom();
- var nextTr = beforeNode.getDom();
- var table = nextTr ? nextTr.parentNode : undefined;
- if (nextTr && table) {
- table.insertBefore(newTr, nextTr);
+ if (newType == 'auto' || newType == 'string') {
+ // cast value to the correct type
+ if (newType == 'string') {
+ this.value = String(this.value);
+ }
+ else {
+ this.value = this._stringCast(String(this.value));
}
- node.showChilds();
+ this.focus();
}
this.updateDom({'updateIndexes': true});
- node.updateDom({'recurse': true});
- }
-};
-
-/**
- * Insert a new child before a given node
- * Only applicable when Node value is of type array or object
- * @param {Node} node
- * @param {Node} afterNode
- */
-Node.prototype.insertAfter = function(node, afterNode) {
- if (this._hasChilds()) {
- var index = this.childs.indexOf(afterNode);
- var beforeNode = this.childs[index + 1];
- if (beforeNode) {
- this.insertBefore(node, beforeNode);
- }
- else {
- this.appendChild(node);
- }
- }
-};
-
-/**
- * Search in this node
- * The node will be expanded when the text is found one of its childs, else
- * it will be collapsed. Searches are case insensitive.
- * @param {String} text
- * @return {Node[]} results Array with nodes containing the search text
- */
-Node.prototype.search = function(text) {
- var results = [];
- var index;
- var search = text ? text.toLowerCase() : undefined;
-
- // delete old search data
- delete this.searchField;
- delete this.searchValue;
-
- // search in field
- if (this.field != undefined) {
- var field = String(this.field).toLowerCase();
- index = field.indexOf(search);
- if (index != -1) {
- this.searchField = true;
- results.push({
- 'node': this,
- 'elem': 'field'
- });
- }
-
- // update dom
- this._updateDomField();
- }
-
- // search in value
- if (this._hasChilds()) {
- // array, object
-
- // search the nodes childs
- if (this.childs) {
- var childResults = [];
- this.childs.forEach(function (child) {
- childResults = childResults.concat(child.search(text));
- });
- results = results.concat(childResults);
- }
-
- // update dom
- if (search != undefined) {
- var recurse = false;
- if (childResults.length == 0) {
- this.collapse(recurse);
- }
- else {
- this.expand(recurse);
- }
- }
- }
- else {
- // string, auto
- if (this.value != undefined ) {
- var value = String(this.value).toLowerCase();
- index = value.indexOf(search);
- if (index != -1) {
- this.searchValue = true;
- results.push({
- 'node': this,
- 'elem': 'value'
- });
- }
- }
-
- // update dom
- this._updateDomValue();
- }
-
- return results;
-};
-
-/**
- * Move the scroll position such that this node is in the visible area.
- * The node will not get the focus
- * @param {function(boolean)} [callback]
- */
-Node.prototype.scrollTo = function(callback) {
- if (!this.dom.tr || !this.dom.tr.parentNode) {
- // if the node is not visible, expand its parents
- var parent = this.parent;
- var recurse = false;
- while (parent) {
- parent.expand(recurse);
- parent = parent.parent;
- }
- }
-
- if (this.dom.tr && this.dom.tr.parentNode) {
- this.editor.scrollTo(this.dom.tr.offsetTop, callback);
- }
-};
-
-
-// stores the element name currently having the focus
-Node.focusElement = undefined;
-
-/**
- * Set focus to this node
- * @param {String} [elementName] The field name of the element to get the
- * focus available values: 'drag', 'menu',
- * 'expand', 'field', 'value' (default)
- */
-Node.prototype.focus = function(elementName) {
- Node.focusElement = elementName;
-
- if (this.dom.tr && this.dom.tr.parentNode) {
- var dom = this.dom;
-
- switch (elementName) {
- case 'drag':
- if (dom.drag) {
- dom.drag.focus();
- }
- else {
- dom.menu.focus();
- }
- break;
-
- case 'menu':
- dom.menu.focus();
- break;
-
- case 'expand':
- if (this._hasChilds()) {
- dom.expand.focus();
- }
- else if (dom.field && this.fieldEditable) {
- dom.field.focus();
- util.selectContentEditable(dom.field);
- }
- else if (dom.value && !this._hasChilds()) {
- dom.value.focus();
- util.selectContentEditable(dom.value);
- }
- else {
- dom.menu.focus();
- }
- break;
-
- case 'field':
- if (dom.field && this.fieldEditable) {
- dom.field.focus();
- util.selectContentEditable(dom.field);
- }
- else if (dom.value && !this._hasChilds()) {
- dom.value.focus();
- util.selectContentEditable(dom.value);
- }
- else if (this._hasChilds()) {
- dom.expand.focus();
- }
- else {
- dom.menu.focus();
- }
- break;
-
- case 'value':
- default:
- if (dom.value && !this._hasChilds()) {
- dom.value.focus();
- util.selectContentEditable(dom.value);
- }
- else if (dom.field && this.fieldEditable) {
- dom.field.focus();
- util.selectContentEditable(dom.field);
- }
- else if (this._hasChilds()) {
- dom.expand.focus();
- }
- else {
- dom.menu.focus();
- }
- break;
- }
- }
-};
-
-/**
- * Select all text in an editable div after a delay of 0 ms
- * @param {Element} editableDiv
- */
-Node.select = function(editableDiv) {
- setTimeout(function () {
- util.selectContentEditable(editableDiv);
- }, 0);
-};
-
-/**
- * Update the values from the DOM field and value of this node
- */
-Node.prototype.blur = function() {
- // retrieve the actual field and value from the DOM.
- this._getDomValue(false);
- this._getDomField(false);
-};
-
-/**
- * Duplicate given child node
- * new structure will be added right before the cloned node
- * @param {Node} node the childNode to be duplicated
- * @return {Node} clone the clone of the node
- * @private
- */
-Node.prototype._duplicate = function(node) {
- var clone = node.clone();
-
- /* TODO: adjust the field name (to prevent equal field names)
- if (this.type == 'object') {
- }
- */
-
- this.insertAfter(clone, node);
-
- return clone;
-};
-
-/**
- * Check if given node is a child. The method will check recursively to find
- * this node.
- * @param {Node} node
- * @return {boolean} containsNode
- */
-Node.prototype.containsNode = function(node) {
- if (this == node) {
- return true;
- }
-
- var childs = this.childs;
- if (childs) {
- // TODO: use the js5 Array.some() here?
- for (var i = 0, iMax = childs.length; i < iMax; i++) {
- if (childs[i].containsNode(node)) {
- return true;
- }
- }
- }
-
- return false;
-};
-
-/**
- * Move given node into this node
- * @param {Node} node the childNode to be moved
- * @param {Node} beforeNode node will be inserted before given
- * node. If no beforeNode is given,
- * the node is appended at the end
- * @private
- */
-Node.prototype._move = function(node, beforeNode) {
- if (node == beforeNode) {
- // nothing to do...
- return;
- }
-
- // check if this node is not a child of the node to be moved here
- if (node.containsNode(this)) {
- throw new Error('Cannot move a field into a child of itself');
- }
-
- // remove the original node
- if (node.parent) {
- node.parent.removeChild(node);
- }
-
- // create a clone of the node
- var clone = node.clone();
- node.clearDom();
-
- // insert or append the node
- if (beforeNode) {
- this.insertBefore(clone, beforeNode);
- }
- else {
- this.appendChild(clone);
- }
-
- /* TODO: adjust the field name (to prevent equal field names)
- if (this.type == 'object') {
- }
- */
-};
-
-/**
- * Remove a child from the node.
- * Only applicable when Node value is of type array or object
- * @param {Node} node The child node to be removed;
- * @return {Node | undefined} node The removed node on success,
- * else undefined
- */
-Node.prototype.removeChild = function(node) {
- if (this.childs) {
- var index = this.childs.indexOf(node);
-
- if (index != -1) {
- node.hide();
-
- // delete old search results
- delete node.searchField;
- delete node.searchValue;
-
- var removedNode = this.childs.splice(index, 1)[0];
-
- this.updateDom({'updateIndexes': true});
-
- return removedNode;
- }
- }
-
- return undefined;
-};
-
-/**
- * Remove a child node node from this node
- * This method is equal to Node.removeChild, except that _remove firex an
- * onChange event.
- * @param {Node} node
- * @private
- */
-Node.prototype._remove = function (node) {
- this.removeChild(node);
-};
-
-/**
- * Change the type of the value of this Node
- * @param {String} newType
- */
-Node.prototype.changeType = function (newType) {
- var oldType = this.type;
-
- if (oldType == newType) {
- // type is not changed
- return;
- }
-
- if ((newType == 'string' || newType == 'auto') &&
- (oldType == 'string' || oldType == 'auto')) {
- // this is an easy change
- this.type = newType;
- }
- else {
- // change from array to object, or from string/auto to object/array
- var table = this.dom.tr ? this.dom.tr.parentNode : undefined;
- var lastTr;
- if (this.expanded) {
- lastTr = this.getAppend();
- }
- else {
- lastTr = this.getDom();
- }
- var nextTr = (lastTr && lastTr.parentNode) ? lastTr.nextSibling : undefined;
-
- // hide current field and all its childs
- this.hide();
- this.clearDom();
-
- // adjust the field and the value
- this.type = newType;
-
- // adjust childs
- if (newType == 'object') {
- if (!this.childs) {
- this.childs = [];
- }
-
- this.childs.forEach(function (child, index) {
- child.clearDom();
- delete child.index;
- child.fieldEditable = true;
- if (child.field == undefined) {
- child.field = '';
- }
- });
-
- if (oldType == 'string' || oldType == 'auto') {
- this.expanded = true;
- }
- }
- else if (newType == 'array') {
- if (!this.childs) {
- this.childs = [];
- }
-
- this.childs.forEach(function (child, index) {
- child.clearDom();
- child.fieldEditable = false;
- child.index = index;
- });
-
- if (oldType == 'string' || oldType == 'auto') {
- this.expanded = true;
- }
- }
- else {
- this.expanded = false;
- }
-
- // create new DOM
- if (table) {
- if (nextTr) {
- table.insertBefore(this.getDom(), nextTr);
- }
- else {
- table.appendChild(this.getDom());
- }
- }
- this.showChilds();
- }
-
- if (newType == 'auto' || newType == 'string') {
- // cast value to the correct type
- if (newType == 'string') {
- this.value = String(this.value);
- }
- else {
- this.value = this._stringCast(String(this.value));
- }
-
- this.focus();
- }
-
- this.updateDom({'updateIndexes': true});
-};
-
-/**
- * Retrieve value from DOM
- * @param {boolean} [silent] If true (default), no errors will be thrown in
- * case of invalid data
- * @private
- */
-Node.prototype._getDomValue = function(silent) {
- if (this.dom.value && this.type != 'array' && this.type != 'object') {
- this.valueInnerText = util.getInnerText(this.dom.value);
- }
-
- if (this.valueInnerText != undefined) {
- try {
- // retrieve the value
- var value;
- if (this.type == 'string') {
- value = this._unescapeHTML(this.valueInnerText);
- }
- else {
- var str = this._unescapeHTML(this.valueInnerText);
- value = this._stringCast(str);
- }
- if (value !== this.value) {
- var oldValue = this.value;
- this.value = value;
- this.editor._onAction('editValue', {
- 'node': this,
- 'oldValue': oldValue,
- 'newValue': value,
- 'oldSelection': this.editor.selection,
- 'newSelection': this.editor.getSelection()
- });
- }
- }
- catch (err) {
- this.value = undefined;
- // TODO: sent an action with the new, invalid value?
- if (silent != true) {
- throw err;
- }
- }
- }
-};
-
-/**
- * Update dom value:
- * - the text color of the value, depending on the type of the value
- * - the height of the field, depending on the width
- * - background color in case it is empty
- * @private
- */
-Node.prototype._updateDomValue = function () {
- var domValue = this.dom.value;
- if (domValue) {
- // set text color depending on value type
- // TODO: put colors in css
- var v = this.value;
- var t = (this.type == 'auto') ? util.type(v) : this.type;
- var isUrl = (t == 'string' && util.isUrl(v));
- var color = '';
- if (isUrl && !this.editor.mode.edit) {
- color = '';
- }
- else if (t == 'string') {
- color = 'green';
- }
- else if (t == 'number') {
- color = 'red';
- }
- else if (t == 'boolean') {
- color = 'darkorange';
- }
- else if (this._hasChilds()) {
- color = '';
- }
- else if (v === null) {
- color = '#004ED0'; // blue
- }
- else {
- // invalid value
- color = 'black';
- }
- domValue.style.color = color;
-
- // make background color light-gray when empty
- var isEmpty = (String(this.value) == '' && this.type != 'array' && this.type != 'object');
- if (isEmpty) {
- util.addClassName(domValue, 'empty');
- }
- else {
- util.removeClassName(domValue, 'empty');
- }
-
- // underline url
- if (isUrl) {
- util.addClassName(domValue, 'url');
- }
- else {
- util.removeClassName(domValue, 'url');
- }
-
- // update title
- if (t == 'array' || t == 'object') {
- var count = this.childs ? this.childs.length : 0;
- domValue.title = this.type + ' containing ' + count + ' items';
- }
- else if (t == 'string' && util.isUrl(v)) {
- if (this.editor.mode.edit) {
- domValue.title = 'Ctrl+Click or Ctrl+Enter to open url in new window';
- }
- }
- else {
- domValue.title = '';
- }
-
- // highlight when there is a search result
- if (this.searchValueActive) {
- util.addClassName(domValue, 'highlight-active');
- }
- else {
- util.removeClassName(domValue, 'highlight-active');
- }
- if (this.searchValue) {
- util.addClassName(domValue, 'highlight');
- }
- else {
- util.removeClassName(domValue, 'highlight');
- }
-
- // strip formatting from the contents of the editable div
- util.stripFormatting(domValue);
- }
-};
-
-/**
- * Update dom field:
- * - the text color of the field, depending on the text
- * - the height of the field, depending on the width
- * - background color in case it is empty
- * @private
- */
-Node.prototype._updateDomField = function () {
- var domField = this.dom.field;
- if (domField) {
- // make backgound color lightgray when empty
- var isEmpty = (String(this.field) == '' && this.parent.type != 'array');
- if (isEmpty) {
- util.addClassName(domField, 'empty');
- }
- else {
- util.removeClassName(domField, 'empty');
- }
-
- // highlight when there is a search result
- if (this.searchFieldActive) {
- util.addClassName(domField, 'highlight-active');
- }
- else {
- util.removeClassName(domField, 'highlight-active');
- }
- if (this.searchField) {
- util.addClassName(domField, 'highlight');
- }
- else {
- util.removeClassName(domField, 'highlight');
- }
-
- // strip formatting from the contents of the editable div
- util.stripFormatting(domField);
- }
-};
-
-/**
- * Retrieve field from DOM
- * @param {boolean} [silent] If true (default), no errors will be thrown in
- * case of invalid data
- * @private
- */
-Node.prototype._getDomField = function(silent) {
- if (this.dom.field && this.fieldEditable) {
- this.fieldInnerText = util.getInnerText(this.dom.field);
- }
-
- if (this.fieldInnerText != undefined) {
- try {
- var field = this._unescapeHTML(this.fieldInnerText);
-
- if (field !== this.field) {
- var oldField = this.field;
- this.field = field;
- this.editor._onAction('editField', {
- 'node': this,
- 'oldValue': oldField,
- 'newValue': field,
- 'oldSelection': this.editor.selection,
- 'newSelection': this.editor.getSelection()
- });
- }
- }
- catch (err) {
- this.field = undefined;
- // TODO: sent an action here, with the new, invalid value?
- if (silent != true) {
- throw err;
- }
- }
- }
-};
-
-/**
- * Clear the dom of the node
- */
-Node.prototype.clearDom = function() {
- // TODO: hide the node first?
- //this.hide();
- // TODO: recursively clear dom?
-
- this.dom = {};
-};
-
-/**
- * Get the HTML DOM TR element of the node.
- * The dom will be generated when not yet created
- * @return {Element} tr HTML DOM TR Element
- */
-Node.prototype.getDom = function() {
- var dom = this.dom;
- if (dom.tr) {
- return dom.tr;
- }
-
- // create row
- dom.tr = document.createElement('tr');
- dom.tr.node = this;
-
- if (this.editor.mode.edit) {
- // create draggable area
- var tdDrag = document.createElement('td');
- if (this.parent) {
- var domDrag = document.createElement('button');
- dom.drag = domDrag;
- domDrag.className = 'dragarea';
- domDrag.title = 'Drag to move this field (Alt+Shift+Arrows)';
- tdDrag.appendChild(domDrag);
- }
- dom.tr.appendChild(tdDrag);
-
- // create context menu
- var tdMenu = document.createElement('td');
- var menu = document.createElement('button');
- dom.menu = menu;
- menu.className = 'contextmenu';
- menu.title = 'Click to open the actions menu (Ctrl+M)';
- tdMenu.appendChild(dom.menu);
- dom.tr.appendChild(tdMenu);
- }
-
- // create tree and field
- var tdField = document.createElement('td');
- dom.tr.appendChild(tdField);
- dom.tree = this._createDomTree();
- tdField.appendChild(dom.tree);
-
- this.updateDom({'updateIndexes': true});
-
- return dom.tr;
-};
-
-/**
- * DragStart event, fired on mousedown on the dragarea at the left side of a Node
- * @param {Event} event
- * @private
- */
-Node.prototype._onDragStart = function (event) {
- var node = this;
- if (!this.mousemove) {
- this.mousemove = util.addEventListener(document, 'mousemove',
- function (event) {
- node._onDrag(event);
- });
- }
-
- if (!this.mouseup) {
- this.mouseup = util.addEventListener(document, 'mouseup',
- function (event ) {
- node._onDragEnd(event);
- });
- }
-
- this.editor.highlighter.lock();
- this.drag = {
- 'oldCursor': document.body.style.cursor,
- 'startParent': this.parent,
- 'startIndex': this.parent.childs.indexOf(this),
- 'mouseX': event.pageX,
- 'level': this.getLevel()
};
- document.body.style.cursor = 'move';
- event.preventDefault();
-};
-
-/**
- * Drag event, fired when moving the mouse while dragging a Node
- * @param {Event} event
- * @private
- */
-Node.prototype._onDrag = function (event) {
- // TODO: this method has grown too large. Split it in a number of methods
- var mouseY = event.pageY;
- var mouseX = event.pageX;
-
- var trThis, trPrev, trNext, trFirst, trLast, trRoot;
- var nodePrev, nodeNext;
- var topThis, topPrev, topFirst, heightThis, bottomNext, heightNext;
- var moved = false;
-
- // TODO: add an ESC option, which resets to the original position
-
- // move up/down
- trThis = this.dom.tr;
- topThis = util.getAbsoluteTop(trThis);
- heightThis = trThis.offsetHeight;
- if (mouseY < topThis) {
- // move up
- trPrev = trThis;
- do {
- trPrev = trPrev.previousSibling;
- nodePrev = Node.getNodeFromTarget(trPrev);
- topPrev = trPrev ? util.getAbsoluteTop(trPrev) : 0;
- }
- while (trPrev && mouseY < topPrev);
-
- if (nodePrev && !nodePrev.parent) {
- nodePrev = undefined;
+ /**
+ * Retrieve value from DOM
+ * @param {boolean} [silent] If true (default), no errors will be thrown in
+ * case of invalid data
+ * @private
+ */
+ Node.prototype._getDomValue = function(silent) {
+ if (this.dom.value && this.type != 'array' && this.type != 'object') {
+ this.valueInnerText = util.getInnerText(this.dom.value);
}
- if (!nodePrev) {
- // move to the first node
- trRoot = trThis.parentNode.firstChild;
- trPrev = trRoot ? trRoot.nextSibling : undefined;
- nodePrev = Node.getNodeFromTarget(trPrev);
- if (nodePrev == this) {
- nodePrev = undefined;
- }
- }
-
- if (nodePrev) {
- // check if mouseY is really inside the found node
- trPrev = nodePrev.dom.tr;
- topPrev = trPrev ? util.getAbsoluteTop(trPrev) : 0;
- if (mouseY > topPrev + heightThis) {
- nodePrev = undefined;
- }
- }
-
- if (nodePrev) {
- nodePrev.parent.moveBefore(this, nodePrev);
- moved = true;
- }
- }
- else {
- // move down
- trLast = (this.expanded && this.append) ? this.append.getDom() : this.dom.tr;
- trFirst = trLast ? trLast.nextSibling : undefined;
- if (trFirst) {
- topFirst = util.getAbsoluteTop(trFirst);
- trNext = trFirst;
- do {
- nodeNext = Node.getNodeFromTarget(trNext);
- if (trNext) {
- bottomNext = trNext.nextSibling ?
- util.getAbsoluteTop(trNext.nextSibling) : 0;
- heightNext = trNext ? (bottomNext - topFirst) : 0;
-
- if (nodeNext.parent.childs.length == 1 && nodeNext.parent.childs[0] == this) {
- // We are about to remove the last child of this parent,
- // which will make the parents appendNode visible.
- topThis += 24 - 1;
- // TODO: dangerous to suppose the height of the appendNode a constant of 24-1 px.
- }
+ if (this.valueInnerText != undefined) {
+ try {
+ // retrieve the value
+ var value;
+ if (this.type == 'string') {
+ value = this._unescapeHTML(this.valueInnerText);
+ }
+ else {
+ var str = this._unescapeHTML(this.valueInnerText);
+ value = this._stringCast(str);
+ }
+ if (value !== this.value) {
+ var oldValue = this.value;
+ this.value = value;
+ this.editor._onAction('editValue', {
+ 'node': this,
+ 'oldValue': oldValue,
+ 'newValue': value,
+ 'oldSelection': this.editor.selection,
+ 'newSelection': this.editor.getSelection()
+ });
}
-
- trNext = trNext.nextSibling;
}
- while (trNext && mouseY > topThis + heightNext);
+ catch (err) {
+ this.value = undefined;
+ // TODO: sent an action with the new, invalid value?
+ if (silent != true) {
+ throw err;
+ }
+ }
+ }
+ };
- if (nodeNext && nodeNext.parent) {
- // calculate the desired level
- var diffX = (mouseX - this.drag.mouseX);
- var diffLevel = Math.round(diffX / 24 / 2);
- var level = this.drag.level + diffLevel; // desired level
- var levelNext = nodeNext.getLevel(); // level to be
+ /**
+ * Update dom value:
+ * - the text color of the value, depending on the type of the value
+ * - the height of the field, depending on the width
+ * - background color in case it is empty
+ * @private
+ */
+ Node.prototype._updateDomValue = function () {
+ var domValue = this.dom.value;
+ if (domValue) {
+ // set text color depending on value type
+ // TODO: put colors in css
+ var v = this.value;
+ var t = (this.type == 'auto') ? util.type(v) : this.type;
+ var isUrl = (t == 'string' && util.isUrl(v));
+ var color = '';
+ if (isUrl && !this.editor.mode.edit) {
+ color = '';
+ }
+ else if (t == 'string') {
+ color = 'green';
+ }
+ else if (t == 'number') {
+ color = 'red';
+ }
+ else if (t == 'boolean') {
+ color = 'darkorange';
+ }
+ else if (this._hasChilds()) {
+ color = '';
+ }
+ else if (v === null) {
+ color = '#004ED0'; // blue
+ }
+ else {
+ // invalid value
+ color = 'black';
+ }
+ domValue.style.color = color;
- // find the best fitting level (move upwards over the append nodes)
- trPrev = nodeNext.dom.tr.previousSibling;
- while (levelNext < level && trPrev) {
- nodePrev = Node.getNodeFromTarget(trPrev);
- if (nodePrev == this || nodePrev._isChildOf(this)) {
- // neglect itself and its childs
+ // make background color light-gray when empty
+ var isEmpty = (String(this.value) == '' && this.type != 'array' && this.type != 'object');
+ if (isEmpty) {
+ util.addClassName(domValue, 'empty');
+ }
+ else {
+ util.removeClassName(domValue, 'empty');
+ }
+
+ // underline url
+ if (isUrl) {
+ util.addClassName(domValue, 'url');
+ }
+ else {
+ util.removeClassName(domValue, 'url');
+ }
+
+ // update title
+ if (t == 'array' || t == 'object') {
+ var count = this.childs ? this.childs.length : 0;
+ domValue.title = this.type + ' containing ' + count + ' items';
+ }
+ else if (t == 'string' && util.isUrl(v)) {
+ if (this.editor.mode.edit) {
+ domValue.title = 'Ctrl+Click or Ctrl+Enter to open url in new window';
+ }
+ }
+ else {
+ domValue.title = '';
+ }
+
+ // highlight when there is a search result
+ if (this.searchValueActive) {
+ util.addClassName(domValue, 'highlight-active');
+ }
+ else {
+ util.removeClassName(domValue, 'highlight-active');
+ }
+ if (this.searchValue) {
+ util.addClassName(domValue, 'highlight');
+ }
+ else {
+ util.removeClassName(domValue, 'highlight');
+ }
+
+ // strip formatting from the contents of the editable div
+ util.stripFormatting(domValue);
+ }
+ };
+
+ /**
+ * Update dom field:
+ * - the text color of the field, depending on the text
+ * - the height of the field, depending on the width
+ * - background color in case it is empty
+ * @private
+ */
+ Node.prototype._updateDomField = function () {
+ var domField = this.dom.field;
+ if (domField) {
+ // make backgound color lightgray when empty
+ var isEmpty = (String(this.field) == '' && this.parent.type != 'array');
+ if (isEmpty) {
+ util.addClassName(domField, 'empty');
+ }
+ else {
+ util.removeClassName(domField, 'empty');
+ }
+
+ // highlight when there is a search result
+ if (this.searchFieldActive) {
+ util.addClassName(domField, 'highlight-active');
+ }
+ else {
+ util.removeClassName(domField, 'highlight-active');
+ }
+ if (this.searchField) {
+ util.addClassName(domField, 'highlight');
+ }
+ else {
+ util.removeClassName(domField, 'highlight');
+ }
+
+ // strip formatting from the contents of the editable div
+ util.stripFormatting(domField);
+ }
+ };
+
+ /**
+ * Retrieve field from DOM
+ * @param {boolean} [silent] If true (default), no errors will be thrown in
+ * case of invalid data
+ * @private
+ */
+ Node.prototype._getDomField = function(silent) {
+ if (this.dom.field && this.fieldEditable) {
+ this.fieldInnerText = util.getInnerText(this.dom.field);
+ }
+
+ if (this.fieldInnerText != undefined) {
+ try {
+ var field = this._unescapeHTML(this.fieldInnerText);
+
+ if (field !== this.field) {
+ var oldField = this.field;
+ this.field = field;
+ this.editor._onAction('editField', {
+ 'node': this,
+ 'oldValue': oldField,
+ 'newValue': field,
+ 'oldSelection': this.editor.selection,
+ 'newSelection': this.editor.getSelection()
+ });
+ }
+ }
+ catch (err) {
+ this.field = undefined;
+ // TODO: sent an action here, with the new, invalid value?
+ if (silent != true) {
+ throw err;
+ }
+ }
+ }
+ };
+
+ /**
+ * Clear the dom of the node
+ */
+ Node.prototype.clearDom = function() {
+ // TODO: hide the node first?
+ //this.hide();
+ // TODO: recursively clear dom?
+
+ this.dom = {};
+ };
+
+ /**
+ * Get the HTML DOM TR element of the node.
+ * The dom will be generated when not yet created
+ * @return {Element} tr HTML DOM TR Element
+ */
+ Node.prototype.getDom = function() {
+ var dom = this.dom;
+ if (dom.tr) {
+ return dom.tr;
+ }
+
+ // create row
+ dom.tr = document.createElement('tr');
+ dom.tr.node = this;
+
+ if (this.editor.mode.edit) {
+ // create draggable area
+ var tdDrag = document.createElement('td');
+ if (this.parent) {
+ var domDrag = document.createElement('button');
+ dom.drag = domDrag;
+ domDrag.className = 'dragarea';
+ domDrag.title = 'Drag to move this field (Alt+Shift+Arrows)';
+ tdDrag.appendChild(domDrag);
+ }
+ dom.tr.appendChild(tdDrag);
+
+ // create context menu
+ var tdMenu = document.createElement('td');
+ var menu = document.createElement('button');
+ dom.menu = menu;
+ menu.className = 'contextmenu';
+ menu.title = 'Click to open the actions menu (Ctrl+M)';
+ tdMenu.appendChild(dom.menu);
+ dom.tr.appendChild(tdMenu);
+ }
+
+ // create tree and field
+ var tdField = document.createElement('td');
+ dom.tr.appendChild(tdField);
+ dom.tree = this._createDomTree();
+ tdField.appendChild(dom.tree);
+
+ this.updateDom({'updateIndexes': true});
+
+ return dom.tr;
+ };
+
+ /**
+ * DragStart event, fired on mousedown on the dragarea at the left side of a Node
+ * @param {Event} event
+ * @private
+ */
+ Node.prototype._onDragStart = function (event) {
+ var node = this;
+ if (!this.mousemove) {
+ this.mousemove = util.addEventListener(document, 'mousemove',
+ function (event) {
+ node._onDrag(event);
+ });
+ }
+
+ if (!this.mouseup) {
+ this.mouseup = util.addEventListener(document, 'mouseup',
+ function (event ) {
+ node._onDragEnd(event);
+ });
+ }
+
+ this.editor.highlighter.lock();
+ this.drag = {
+ 'oldCursor': document.body.style.cursor,
+ 'startParent': this.parent,
+ 'startIndex': this.parent.childs.indexOf(this),
+ 'mouseX': event.pageX,
+ 'level': this.getLevel()
+ };
+ document.body.style.cursor = 'move';
+
+ event.preventDefault();
+ };
+
+ /**
+ * Drag event, fired when moving the mouse while dragging a Node
+ * @param {Event} event
+ * @private
+ */
+ Node.prototype._onDrag = function (event) {
+ // TODO: this method has grown too large. Split it in a number of methods
+ var mouseY = event.pageY;
+ var mouseX = event.pageX;
+
+ var trThis, trPrev, trNext, trFirst, trLast, trRoot;
+ var nodePrev, nodeNext;
+ var topThis, topPrev, topFirst, heightThis, bottomNext, heightNext;
+ var moved = false;
+
+ // TODO: add an ESC option, which resets to the original position
+
+ // move up/down
+ trThis = this.dom.tr;
+ topThis = util.getAbsoluteTop(trThis);
+ heightThis = trThis.offsetHeight;
+ if (mouseY < topThis) {
+ // move up
+ trPrev = trThis;
+ do {
+ trPrev = trPrev.previousSibling;
+ nodePrev = Node.getNodeFromTarget(trPrev);
+ topPrev = trPrev ? util.getAbsoluteTop(trPrev) : 0;
+ }
+ while (trPrev && mouseY < topPrev);
+
+ if (nodePrev && !nodePrev.parent) {
+ nodePrev = undefined;
+ }
+
+ if (!nodePrev) {
+ // move to the first node
+ trRoot = trThis.parentNode.firstChild;
+ trPrev = trRoot ? trRoot.nextSibling : undefined;
+ nodePrev = Node.getNodeFromTarget(trPrev);
+ if (nodePrev == this) {
+ nodePrev = undefined;
+ }
+ }
+
+ if (nodePrev) {
+ // check if mouseY is really inside the found node
+ trPrev = nodePrev.dom.tr;
+ topPrev = trPrev ? util.getAbsoluteTop(trPrev) : 0;
+ if (mouseY > topPrev + heightThis) {
+ nodePrev = undefined;
+ }
+ }
+
+ if (nodePrev) {
+ nodePrev.parent.moveBefore(this, nodePrev);
+ moved = true;
+ }
+ }
+ else {
+ // move down
+ trLast = (this.expanded && this.append) ? this.append.getDom() : this.dom.tr;
+ trFirst = trLast ? trLast.nextSibling : undefined;
+ if (trFirst) {
+ topFirst = util.getAbsoluteTop(trFirst);
+ trNext = trFirst;
+ do {
+ nodeNext = Node.getNodeFromTarget(trNext);
+ if (trNext) {
+ bottomNext = trNext.nextSibling ?
+ util.getAbsoluteTop(trNext.nextSibling) : 0;
+ heightNext = trNext ? (bottomNext - topFirst) : 0;
+
+ if (nodeNext.parent.childs.length == 1 && nodeNext.parent.childs[0] == this) {
+ // We are about to remove the last child of this parent,
+ // which will make the parents appendNode visible.
+ topThis += 24 - 1;
+ // TODO: dangerous to suppose the height of the appendNode a constant of 24-1 px.
+ }
}
- else if (nodePrev instanceof AppendNode) {
- var childs = nodePrev.parent.childs;
- if (childs.length > 1 ||
- (childs.length == 1 && childs[0] != this)) {
- // non-visible append node of a list of childs
- // consisting of not only this node (else the
- // append node will change into a visible "empty"
- // text when removing this node).
- nodeNext = Node.getNodeFromTarget(trPrev);
- levelNext = nodeNext.getLevel();
+
+ trNext = trNext.nextSibling;
+ }
+ while (trNext && mouseY > topThis + heightNext);
+
+ if (nodeNext && nodeNext.parent) {
+ // calculate the desired level
+ var diffX = (mouseX - this.drag.mouseX);
+ var diffLevel = Math.round(diffX / 24 / 2);
+ var level = this.drag.level + diffLevel; // desired level
+ var levelNext = nodeNext.getLevel(); // level to be
+
+ // find the best fitting level (move upwards over the append nodes)
+ trPrev = nodeNext.dom.tr.previousSibling;
+ while (levelNext < level && trPrev) {
+ nodePrev = Node.getNodeFromTarget(trPrev);
+ if (nodePrev == this || nodePrev._isChildOf(this)) {
+ // neglect itself and its childs
+ }
+ else if (nodePrev instanceof AppendNode) {
+ var childs = nodePrev.parent.childs;
+ if (childs.length > 1 ||
+ (childs.length == 1 && childs[0] != this)) {
+ // non-visible append node of a list of childs
+ // consisting of not only this node (else the
+ // append node will change into a visible "empty"
+ // text when removing this node).
+ nodeNext = Node.getNodeFromTarget(trPrev);
+ levelNext = nodeNext.getLevel();
+ }
+ else {
+ break;
+ }
}
else {
break;
}
- }
- else {
- break;
+
+ trPrev = trPrev.previousSibling;
}
- trPrev = trPrev.previousSibling;
- }
-
- // move the node when its position is changed
- if (trLast.nextSibling != nodeNext.dom.tr) {
- nodeNext.parent.moveBefore(this, nodeNext);
- moved = true;
+ // move the node when its position is changed
+ if (trLast.nextSibling != nodeNext.dom.tr) {
+ nodeNext.parent.moveBefore(this, nodeNext);
+ moved = true;
+ }
}
}
}
- }
- if (moved) {
- // update the dragging parameters when moved
- this.drag.mouseX = mouseX;
- this.drag.level = this.getLevel();
- }
+ if (moved) {
+ // update the dragging parameters when moved
+ this.drag.mouseX = mouseX;
+ this.drag.level = this.getLevel();
+ }
- // auto scroll when hovering around the top of the editor
- this.editor.startAutoScroll(mouseY);
+ // auto scroll when hovering around the top of the editor
+ this.editor.startAutoScroll(mouseY);
- event.preventDefault();
-};
-
-/**
- * Drag event, fired on mouseup after having dragged a node
- * @param {Event} event
- * @private
- */
-Node.prototype._onDragEnd = function (event) {
- var params = {
- 'node': this,
- 'startParent': this.drag.startParent,
- 'startIndex': this.drag.startIndex,
- 'endParent': this.parent,
- 'endIndex': this.parent.childs.indexOf(this)
+ event.preventDefault();
};
- if ((params.startParent != params.endParent) ||
- (params.startIndex != params.endIndex)) {
- // only register this action if the node is actually moved to another place
- this.editor._onAction('moveNode', params);
- }
- document.body.style.cursor = this.drag.oldCursor;
- this.editor.highlighter.unlock();
- delete this.drag;
-
- if (this.mousemove) {
- util.removeEventListener(document, 'mousemove', this.mousemove);
- delete this.mousemove;}
- if (this.mouseup) {
- util.removeEventListener(document, 'mouseup', this.mouseup);
- delete this.mouseup;
- }
-
- // Stop any running auto scroll
- this.editor.stopAutoScroll();
-
- event.preventDefault();
-};
-
-/**
- * Test if this node is a child of an other node
- * @param {Node} node
- * @return {boolean} isChild
- * @private
- */
-Node.prototype._isChildOf = function (node) {
- var n = this.parent;
- while (n) {
- if (n == node) {
- return true;
+ /**
+ * Drag event, fired on mouseup after having dragged a node
+ * @param {Event} event
+ * @private
+ */
+ Node.prototype._onDragEnd = function (event) {
+ var params = {
+ 'node': this,
+ 'startParent': this.drag.startParent,
+ 'startIndex': this.drag.startIndex,
+ 'endParent': this.parent,
+ 'endIndex': this.parent.childs.indexOf(this)
+ };
+ if ((params.startParent != params.endParent) ||
+ (params.startIndex != params.endIndex)) {
+ // only register this action if the node is actually moved to another place
+ this.editor._onAction('moveNode', params);
}
- n = n.parent;
- }
- return false;
-};
+ document.body.style.cursor = this.drag.oldCursor;
+ this.editor.highlighter.unlock();
+ delete this.drag;
-/**
- * Create an editable field
- * @return {Element} domField
- * @private
- */
-Node.prototype._createDomField = function () {
- return document.createElement('div');
-};
+ if (this.mousemove) {
+ util.removeEventListener(document, 'mousemove', this.mousemove);
+ delete this.mousemove;}
+ if (this.mouseup) {
+ util.removeEventListener(document, 'mouseup', this.mouseup);
+ delete this.mouseup;
+ }
-/**
- * Set highlighting for this node and all its childs.
- * Only applied to the currently visible (expanded childs)
- * @param {boolean} highlight
- */
-Node.prototype.setHighlight = function (highlight) {
- if (this.dom.tr) {
- this.dom.tr.className = (highlight ? 'highlight' : '');
+ // Stop any running auto scroll
+ this.editor.stopAutoScroll();
+ event.preventDefault();
+ };
+
+ /**
+ * Test if this node is a child of an other node
+ * @param {Node} node
+ * @return {boolean} isChild
+ * @private
+ */
+ Node.prototype._isChildOf = function (node) {
+ var n = this.parent;
+ while (n) {
+ if (n == node) {
+ return true;
+ }
+ n = n.parent;
+ }
+
+ return false;
+ };
+
+ /**
+ * Create an editable field
+ * @return {Element} domField
+ * @private
+ */
+ Node.prototype._createDomField = function () {
+ return document.createElement('div');
+ };
+
+ /**
+ * Set highlighting for this node and all its childs.
+ * Only applied to the currently visible (expanded childs)
+ * @param {boolean} highlight
+ */
+ Node.prototype.setHighlight = function (highlight) {
+ if (this.dom.tr) {
+ this.dom.tr.className = (highlight ? 'highlight' : '');
+
+ if (this.append) {
+ this.append.setHighlight(highlight);
+ }
+
+ if (this.childs) {
+ this.childs.forEach(function (child) {
+ child.setHighlight(highlight);
+ });
+ }
+ }
+ };
+
+ /**
+ * Update the value of the node. Only primitive types are allowed, no Object
+ * or Array is allowed.
+ * @param {String | Number | Boolean | null} value
+ */
+ Node.prototype.updateValue = function (value) {
+ this.value = value;
+ this.updateDom();
+ };
+
+ /**
+ * Update the field of the node.
+ * @param {String} field
+ */
+ Node.prototype.updateField = function (field) {
+ this.field = field;
+ this.updateDom();
+ };
+
+ /**
+ * Update the HTML DOM, optionally recursing through the childs
+ * @param {Object} [options] Available parameters:
+ * {boolean} [recurse] If true, the
+ * DOM of the childs will be updated recursively.
+ * False by default.
+ * {boolean} [updateIndexes] If true, the childs
+ * indexes of the node will be updated too. False by
+ * default.
+ */
+ Node.prototype.updateDom = function (options) {
+ // update level indentation
+ var domTree = this.dom.tree;
+ if (domTree) {
+ domTree.style.marginLeft = this.getLevel() * 24 + 'px';
+ }
+
+ // update field
+ var domField = this.dom.field;
+ if (domField) {
+ if (this.fieldEditable == true) {
+ // parent is an object
+ domField.contentEditable = this.editor.mode.edit;
+ domField.spellcheck = false;
+ domField.className = 'field';
+ }
+ else {
+ // parent is an array this is the root node
+ domField.className = 'readonly';
+ }
+
+ var field;
+ if (this.index != undefined) {
+ field = this.index;
+ }
+ else if (this.field != undefined) {
+ field = this.field;
+ }
+ else if (this._hasChilds()) {
+ field = this.type;
+ }
+ else {
+ field = '';
+ }
+ domField.innerHTML = this._escapeHTML(field);
+ }
+
+ // update value
+ var domValue = this.dom.value;
+ if (domValue) {
+ var count = this.childs ? this.childs.length : 0;
+ if (this.type == 'array') {
+ domValue.innerHTML = '[' + count + ']';
+ }
+ else if (this.type == 'object') {
+ domValue.innerHTML = '{' + count + '}';
+ }
+ else {
+ domValue.innerHTML = this._escapeHTML(this.value);
+ }
+ }
+
+ // update field and value
+ this._updateDomField();
+ this._updateDomValue();
+
+ // update childs indexes
+ if (options && options.updateIndexes == true) {
+ // updateIndexes is true or undefined
+ this._updateDomIndexes();
+ }
+
+ if (options && options.recurse == true) {
+ // recurse is true or undefined. update childs recursively
+ if (this.childs) {
+ this.childs.forEach(function (child) {
+ child.updateDom(options);
+ });
+ }
+ }
+
+ // update row with append button
if (this.append) {
- this.append.setHighlight(highlight);
+ this.append.updateDom();
}
+ };
- if (this.childs) {
- this.childs.forEach(function (child) {
- child.setHighlight(highlight);
- });
- }
- }
-};
-
-/**
- * Update the value of the node. Only primitive types are allowed, no Object
- * or Array is allowed.
- * @param {String | Number | Boolean | null} value
- */
-Node.prototype.updateValue = function (value) {
- this.value = value;
- this.updateDom();
-};
-
-/**
- * Update the field of the node.
- * @param {String} field
- */
-Node.prototype.updateField = function (field) {
- this.field = field;
- this.updateDom();
-};
-
-/**
- * Update the HTML DOM, optionally recursing through the childs
- * @param {Object} [options] Available parameters:
- * {boolean} [recurse] If true, the
- * DOM of the childs will be updated recursively.
- * False by default.
- * {boolean} [updateIndexes] If true, the childs
- * indexes of the node will be updated too. False by
- * default.
- */
-Node.prototype.updateDom = function (options) {
- // update level indentation
- var domTree = this.dom.tree;
- if (domTree) {
- domTree.style.marginLeft = this.getLevel() * 24 + 'px';
- }
-
- // update field
- var domField = this.dom.field;
- if (domField) {
- if (this.fieldEditable == true) {
- // parent is an object
- domField.contentEditable = this.editor.mode.edit;
- domField.spellcheck = false;
- domField.className = 'field';
- }
- else {
- // parent is an array this is the root node
- domField.className = 'readonly';
- }
-
- var field;
- if (this.index != undefined) {
- field = this.index;
- }
- else if (this.field != undefined) {
- field = this.field;
- }
- else if (this._hasChilds()) {
- field = this.type;
- }
- else {
- field = '';
- }
- domField.innerHTML = this._escapeHTML(field);
- }
-
- // update value
- var domValue = this.dom.value;
- if (domValue) {
- var count = this.childs ? this.childs.length : 0;
- if (this.type == 'array') {
- domValue.innerHTML = '[' + count + ']';
- }
- else if (this.type == 'object') {
- domValue.innerHTML = '{' + count + '}';
- }
- else {
- domValue.innerHTML = this._escapeHTML(this.value);
- }
- }
-
- // update field and value
- this._updateDomField();
- this._updateDomValue();
-
- // update childs indexes
- if (options && options.updateIndexes == true) {
- // updateIndexes is true or undefined
- this._updateDomIndexes();
- }
-
- if (options && options.recurse == true) {
- // recurse is true or undefined. update childs recursively
- if (this.childs) {
- this.childs.forEach(function (child) {
- child.updateDom(options);
- });
- }
- }
-
- // update row with append button
- if (this.append) {
- this.append.updateDom();
- }
-};
-
-/**
- * Update the DOM of the childs of a node: update indexes and undefined field
- * names.
- * Only applicable when structure is an array or object
- * @private
- */
-Node.prototype._updateDomIndexes = function () {
- var domValue = this.dom.value;
- var childs = this.childs;
- if (domValue && childs) {
- if (this.type == 'array') {
- childs.forEach(function (child, index) {
- child.index = index;
- var childField = child.dom.field;
- if (childField) {
- childField.innerHTML = index;
- }
- });
- }
- else if (this.type == 'object') {
- childs.forEach(function (child) {
- if (child.index != undefined) {
- delete child.index;
-
- if (child.field == undefined) {
- child.field = '';
+ /**
+ * Update the DOM of the childs of a node: update indexes and undefined field
+ * names.
+ * Only applicable when structure is an array or object
+ * @private
+ */
+ Node.prototype._updateDomIndexes = function () {
+ var domValue = this.dom.value;
+ var childs = this.childs;
+ if (domValue && childs) {
+ if (this.type == 'array') {
+ childs.forEach(function (child, index) {
+ child.index = index;
+ var childField = child.dom.field;
+ if (childField) {
+ childField.innerHTML = index;
}
- }
- });
- }
- }
-};
+ });
+ }
+ else if (this.type == 'object') {
+ childs.forEach(function (child) {
+ if (child.index != undefined) {
+ delete child.index;
-/**
- * Create an editable value
- * @private
- */
-Node.prototype._createDomValue = function () {
- var domValue;
-
- if (this.type == 'array') {
- domValue = document.createElement('div');
- domValue.className = 'readonly';
- domValue.innerHTML = '[...]';
- }
- else if (this.type == 'object') {
- domValue = document.createElement('div');
- domValue.className = 'readonly';
- domValue.innerHTML = '{...}';
- }
- else {
- if (!this.editor.mode.edit && util.isUrl(this.value)) {
- // create a link in case of read-only editor and value containing an url
- domValue = document.createElement('a');
- domValue.className = 'value';
- domValue.href = this.value;
- domValue.target = '_blank';
- domValue.innerHTML = this._escapeHTML(this.value);
+ if (child.field == undefined) {
+ child.field = '';
+ }
+ }
+ });
+ }
}
- else {
- // create and editable or read-only div
+ };
+
+ /**
+ * Create an editable value
+ * @private
+ */
+ Node.prototype._createDomValue = function () {
+ var domValue;
+
+ if (this.type == 'array') {
domValue = document.createElement('div');
- domValue.contentEditable = !this.editor.mode.view;
- domValue.spellcheck = false;
- domValue.className = 'value';
- domValue.innerHTML = this._escapeHTML(this.value);
+ domValue.className = 'readonly';
+ domValue.innerHTML = '[...]';
}
- }
-
- return domValue;
-};
-
-/**
- * Create an expand/collapse button
- * @return {Element} expand
- * @private
- */
-Node.prototype._createDomExpandButton = function () {
- // create expand button
- var expand = document.createElement('button');
- if (this._hasChilds()) {
- expand.className = this.expanded ? 'expanded' : 'collapsed';
- expand.title =
- 'Click to expand/collapse this field (Ctrl+E). \n' +
- 'Ctrl+Click to expand/collapse including all childs.';
- }
- else {
- expand.className = 'invisible';
- expand.title = '';
- }
-
- return expand;
-};
-
-
-/**
- * Create a DOM tree element, containing the expand/collapse button
- * @return {Element} domTree
- * @private
- */
-Node.prototype._createDomTree = function () {
- var dom = this.dom;
- var domTree = document.createElement('table');
- var tbody = document.createElement('tbody');
- domTree.style.borderCollapse = 'collapse'; // TODO: put in css
- domTree.className = 'values';
- domTree.appendChild(tbody);
- var tr = document.createElement('tr');
- tbody.appendChild(tr);
-
- // create expand button
- var tdExpand = document.createElement('td');
- tdExpand.className = 'tree';
- tr.appendChild(tdExpand);
- dom.expand = this._createDomExpandButton();
- tdExpand.appendChild(dom.expand);
- dom.tdExpand = tdExpand;
-
- // create the field
- var tdField = document.createElement('td');
- tdField.className = 'tree';
- tr.appendChild(tdField);
- dom.field = this._createDomField();
- tdField.appendChild(dom.field);
- dom.tdField = tdField;
-
- // create a separator
- var tdSeparator = document.createElement('td');
- tdSeparator.className = 'tree';
- tr.appendChild(tdSeparator);
- if (this.type != 'object' && this.type != 'array') {
- tdSeparator.appendChild(document.createTextNode(':'));
- tdSeparator.className = 'separator';
- }
- dom.tdSeparator = tdSeparator;
-
- // create the value
- var tdValue = document.createElement('td');
- tdValue.className = 'tree';
- tr.appendChild(tdValue);
- dom.value = this._createDomValue();
- tdValue.appendChild(dom.value);
- dom.tdValue = tdValue;
-
- return domTree;
-};
-
-/**
- * Handle an event. The event is catched centrally by the editor
- * @param {Event} event
- */
-Node.prototype.onEvent = function (event) {
- var type = event.type,
- target = event.target || event.srcElement,
- dom = this.dom,
- node = this,
- focusNode,
- expandable = this._hasChilds();
-
- // check if mouse is on menu or on dragarea.
- // If so, highlight current row and its childs
- if (target == dom.drag || target == dom.menu) {
- if (type == 'mouseover') {
- this.editor.highlighter.highlight(this);
+ else if (this.type == 'object') {
+ domValue = document.createElement('div');
+ domValue.className = 'readonly';
+ domValue.innerHTML = '{...}';
}
- else if (type == 'mouseout') {
- this.editor.highlighter.unhighlight();
+ else {
+ if (!this.editor.mode.edit && util.isUrl(this.value)) {
+ // create a link in case of read-only editor and value containing an url
+ domValue = document.createElement('a');
+ domValue.className = 'value';
+ domValue.href = this.value;
+ domValue.target = '_blank';
+ domValue.innerHTML = this._escapeHTML(this.value);
+ }
+ else {
+ // create and editable or read-only div
+ domValue = document.createElement('div');
+ domValue.contentEditable = !this.editor.mode.view;
+ domValue.spellcheck = false;
+ domValue.className = 'value';
+ domValue.innerHTML = this._escapeHTML(this.value);
+ }
}
- }
- // drag events
- if (type == 'mousedown' && target == dom.drag) {
- this._onDragStart(event);
- }
+ return domValue;
+ };
- // context menu events
- if (type == 'click' && target == dom.menu) {
- var highlighter = node.editor.highlighter;
- highlighter.highlight(node);
- highlighter.lock();
- util.addClassName(dom.menu, 'selected');
- this.showContextMenu(dom.menu, function () {
- util.removeClassName(dom.menu, 'selected');
- highlighter.unlock();
- highlighter.unhighlight();
- });
- }
-
- // expand events
- if (type == 'click' && target == dom.expand) {
- if (expandable) {
- var recurse = event.ctrlKey; // with ctrl-key, expand/collapse all
- this._onExpand(recurse);
+ /**
+ * Create an expand/collapse button
+ * @return {Element} expand
+ * @private
+ */
+ Node.prototype._createDomExpandButton = function () {
+ // create expand button
+ var expand = document.createElement('button');
+ if (this._hasChilds()) {
+ expand.className = this.expanded ? 'expanded' : 'collapsed';
+ expand.title =
+ 'Click to expand/collapse this field (Ctrl+E). \n' +
+ 'Ctrl+Click to expand/collapse including all childs.';
+ }
+ else {
+ expand.className = 'invisible';
+ expand.title = '';
}
- }
- // value events
- var domValue = dom.value;
- if (target == domValue) {
- //noinspection FallthroughInSwitchStatementJS
- switch (type) {
- case 'focus':
- focusNode = this;
- break;
+ return expand;
+ };
- case 'blur':
- case 'change':
- this._getDomValue(true);
- this._updateDomValue();
- if (this.value) {
- domValue.innerHTML = this._escapeHTML(this.value);
- }
- break;
- case 'input':
- this._getDomValue(true);
- this._updateDomValue();
- break;
+ /**
+ * Create a DOM tree element, containing the expand/collapse button
+ * @return {Element} domTree
+ * @private
+ */
+ Node.prototype._createDomTree = function () {
+ var dom = this.dom;
+ var domTree = document.createElement('table');
+ var tbody = document.createElement('tbody');
+ domTree.style.borderCollapse = 'collapse'; // TODO: put in css
+ domTree.className = 'values';
+ domTree.appendChild(tbody);
+ var tr = document.createElement('tr');
+ tbody.appendChild(tr);
- case 'keydown':
- case 'mousedown':
- this.editor.selection = this.editor.getSelection();
- break;
+ // create expand button
+ var tdExpand = document.createElement('td');
+ tdExpand.className = 'tree';
+ tr.appendChild(tdExpand);
+ dom.expand = this._createDomExpandButton();
+ tdExpand.appendChild(dom.expand);
+ dom.tdExpand = tdExpand;
- case 'click':
- if (event.ctrlKey && this.editor.mode.edit) {
- if (util.isUrl(this.value)) {
- window.open(this.value, '_blank');
+ // create the field
+ var tdField = document.createElement('td');
+ tdField.className = 'tree';
+ tr.appendChild(tdField);
+ dom.field = this._createDomField();
+ tdField.appendChild(dom.field);
+ dom.tdField = tdField;
+
+ // create a separator
+ var tdSeparator = document.createElement('td');
+ tdSeparator.className = 'tree';
+ tr.appendChild(tdSeparator);
+ if (this.type != 'object' && this.type != 'array') {
+ tdSeparator.appendChild(document.createTextNode(':'));
+ tdSeparator.className = 'separator';
+ }
+ dom.tdSeparator = tdSeparator;
+
+ // create the value
+ var tdValue = document.createElement('td');
+ tdValue.className = 'tree';
+ tr.appendChild(tdValue);
+ dom.value = this._createDomValue();
+ tdValue.appendChild(dom.value);
+ dom.tdValue = tdValue;
+
+ return domTree;
+ };
+
+ /**
+ * Handle an event. The event is catched centrally by the editor
+ * @param {Event} event
+ */
+ Node.prototype.onEvent = function (event) {
+ var type = event.type,
+ target = event.target || event.srcElement,
+ dom = this.dom,
+ node = this,
+ focusNode,
+ expandable = this._hasChilds();
+
+ // check if mouse is on menu or on dragarea.
+ // If so, highlight current row and its childs
+ if (target == dom.drag || target == dom.menu) {
+ if (type == 'mouseover') {
+ this.editor.highlighter.highlight(this);
+ }
+ else if (type == 'mouseout') {
+ this.editor.highlighter.unhighlight();
+ }
+ }
+
+ // drag events
+ if (type == 'mousedown' && target == dom.drag) {
+ this._onDragStart(event);
+ }
+
+ // context menu events
+ if (type == 'click' && target == dom.menu) {
+ var highlighter = node.editor.highlighter;
+ highlighter.highlight(node);
+ highlighter.lock();
+ util.addClassName(dom.menu, 'selected');
+ this.showContextMenu(dom.menu, function () {
+ util.removeClassName(dom.menu, 'selected');
+ highlighter.unlock();
+ highlighter.unhighlight();
+ });
+ }
+
+ // expand events
+ if (type == 'click' && target == dom.expand) {
+ if (expandable) {
+ var recurse = event.ctrlKey; // with ctrl-key, expand/collapse all
+ this._onExpand(recurse);
+ }
+ }
+
+ // value events
+ var domValue = dom.value;
+ if (target == domValue) {
+ //noinspection FallthroughInSwitchStatementJS
+ switch (type) {
+ case 'focus':
+ focusNode = this;
+ break;
+
+ case 'blur':
+ case 'change':
+ this._getDomValue(true);
+ this._updateDomValue();
+ if (this.value) {
+ domValue.innerHTML = this._escapeHTML(this.value);
}
- }
- break;
+ break;
- case 'keyup':
- this._getDomValue(true);
- this._updateDomValue();
- break;
+ case 'input':
+ this._getDomValue(true);
+ this._updateDomValue();
+ break;
- case 'cut':
- case 'paste':
- setTimeout(function () {
- node._getDomValue(true);
- node._updateDomValue();
- }, 1);
- break;
+ case 'keydown':
+ case 'mousedown':
+ this.editor.selection = this.editor.getSelection();
+ break;
+
+ case 'click':
+ if (event.ctrlKey && this.editor.mode.edit) {
+ if (util.isUrl(this.value)) {
+ window.open(this.value, '_blank');
+ }
+ }
+ break;
+
+ case 'keyup':
+ this._getDomValue(true);
+ this._updateDomValue();
+ break;
+
+ case 'cut':
+ case 'paste':
+ setTimeout(function () {
+ node._getDomValue(true);
+ node._updateDomValue();
+ }, 1);
+ break;
+ }
}
- }
- // field events
- var domField = dom.field;
- if (target == domField) {
- switch (type) {
- case 'focus':
- focusNode = this;
- break;
+ // field events
+ var domField = dom.field;
+ if (target == domField) {
+ switch (type) {
+ case 'focus':
+ focusNode = this;
+ break;
- case 'blur':
- case 'change':
- this._getDomField(true);
- this._updateDomField();
- if (this.field) {
- domField.innerHTML = this._escapeHTML(this.field);
- }
- break;
+ case 'blur':
+ case 'change':
+ this._getDomField(true);
+ this._updateDomField();
+ if (this.field) {
+ domField.innerHTML = this._escapeHTML(this.field);
+ }
+ break;
- case 'input':
- this._getDomField(true);
- this._updateDomField();
- break;
+ case 'input':
+ this._getDomField(true);
+ this._updateDomField();
+ break;
- case 'keydown':
- case 'mousedown':
- this.editor.selection = this.editor.getSelection();
- break;
+ case 'keydown':
+ case 'mousedown':
+ this.editor.selection = this.editor.getSelection();
+ break;
- case 'keyup':
- this._getDomField(true);
- this._updateDomField();
- break;
+ case 'keyup':
+ this._getDomField(true);
+ this._updateDomField();
+ break;
- case 'cut':
- case 'paste':
- setTimeout(function () {
- node._getDomField(true);
- node._updateDomField();
- }, 1);
- break;
+ case 'cut':
+ case 'paste':
+ setTimeout(function () {
+ node._getDomField(true);
+ node._updateDomField();
+ }, 1);
+ break;
+ }
}
- }
- // focus
- // when clicked in whitespace left or right from the field or value, set focus
- var domTree = dom.tree;
- if (target == domTree.parentNode) {
- switch (type) {
- case 'click':
- var left = (event.offsetX != undefined) ?
- (event.offsetX < (this.getLevel() + 1) * 24) :
- (event.pageX < util.getAbsoluteLeft(dom.tdSeparator));// for FF
- if (left || expandable) {
- // node is expandable when it is an object or array
+ // focus
+ // when clicked in whitespace left or right from the field or value, set focus
+ var domTree = dom.tree;
+ if (target == domTree.parentNode) {
+ switch (type) {
+ case 'click':
+ var left = (event.offsetX != undefined) ?
+ (event.offsetX < (this.getLevel() + 1) * 24) :
+ (event.pageX < util.getAbsoluteLeft(dom.tdSeparator));// for FF
+ if (left || expandable) {
+ // node is expandable when it is an object or array
+ if (domField) {
+ util.setEndOfContentEditable(domField);
+ domField.focus();
+ }
+ }
+ else {
+ if (domValue) {
+ util.setEndOfContentEditable(domValue);
+ domValue.focus();
+ }
+ }
+ break;
+ }
+ }
+ if ((target == dom.tdExpand && !expandable) || target == dom.tdField ||
+ target == dom.tdSeparator) {
+ switch (type) {
+ case 'click':
if (domField) {
util.setEndOfContentEditable(domField);
domField.focus();
}
- }
- else {
- if (domValue) {
- util.setEndOfContentEditable(domValue);
- domValue.focus();
+ break;
+ }
+ }
+
+ if (type == 'keydown') {
+ this.onKeyDown(event);
+ }
+ };
+
+ /**
+ * Key down event handler
+ * @param {Event} event
+ */
+ Node.prototype.onKeyDown = function (event) {
+ var keynum = event.which || event.keyCode;
+ var target = event.target || event.srcElement;
+ var ctrlKey = event.ctrlKey;
+ var shiftKey = event.shiftKey;
+ var altKey = event.altKey;
+ var handled = false;
+ var prevNode, nextNode, nextDom, nextDom2;
+
+ // util.log(ctrlKey, keynum, event.charCode); // TODO: cleanup
+ if (keynum == 13) { // Enter
+ if (target == this.dom.value) {
+ if (!this.editor.mode.edit || event.ctrlKey) {
+ if (util.isUrl(this.value)) {
+ window.open(this.value, '_blank');
+ handled = true;
}
}
- break;
- }
- }
- if ((target == dom.tdExpand && !expandable) || target == dom.tdField ||
- target == dom.tdSeparator) {
- switch (type) {
- case 'click':
- if (domField) {
- util.setEndOfContentEditable(domField);
- domField.focus();
- }
- break;
- }
- }
-
- if (type == 'keydown') {
- this.onKeyDown(event);
- }
-};
-
-/**
- * Key down event handler
- * @param {Event} event
- */
-Node.prototype.onKeyDown = function (event) {
- var keynum = event.which || event.keyCode;
- var target = event.target || event.srcElement;
- var ctrlKey = event.ctrlKey;
- var shiftKey = event.shiftKey;
- var altKey = event.altKey;
- var handled = false;
- var prevNode, nextNode, nextDom, nextDom2;
-
- // util.log(ctrlKey, keynum, event.charCode); // TODO: cleanup
- if (keynum == 13) { // Enter
- if (target == this.dom.value) {
- if (!this.editor.mode.edit || event.ctrlKey) {
- if (util.isUrl(this.value)) {
- window.open(this.value, '_blank');
+ }
+ else if (target == this.dom.expand) {
+ var expandable = this._hasChilds();
+ if (expandable) {
+ var recurse = event.ctrlKey; // with ctrl-key, expand/collapse all
+ this._onExpand(recurse);
+ target.focus();
handled = true;
}
}
}
- else if (target == this.dom.expand) {
- var expandable = this._hasChilds();
- if (expandable) {
- var recurse = event.ctrlKey; // with ctrl-key, expand/collapse all
- this._onExpand(recurse);
- target.focus();
+ else if (keynum == 68) { // D
+ if (ctrlKey) { // Ctrl+D
+ this._onDuplicate();
handled = true;
}
}
- }
- else if (keynum == 68) { // D
- if (ctrlKey) { // Ctrl+D
- this._onDuplicate();
- handled = true;
- }
- }
- else if (keynum == 69) { // E
- if (ctrlKey) { // Ctrl+E and Ctrl+Shift+E
- this._onExpand(shiftKey); // recurse = shiftKey
- target.focus(); // TODO: should restore focus in case of recursing expand (which takes DOM offline)
- handled = true;
- }
- }
- else if (keynum == 77) { // M
- if (ctrlKey) { // Ctrl+M
- this.showContextMenu(target);
- handled = true;
- }
- }
- else if (keynum == 46) { // Del
- if (ctrlKey) { // Ctrl+Del
- this._onRemove();
- handled = true;
- }
- }
- else if (keynum == 45) { // Ins
- if (ctrlKey && !shiftKey) { // Ctrl+Ins
- this._onInsertBefore();
- handled = true;
- }
- else if (ctrlKey && shiftKey) { // Ctrl+Shift+Ins
- this._onInsertAfter();
- handled = true;
- }
- }
- else if (keynum == 35) { // End
- if (altKey) { // Alt+End
- // find the last node
- var lastNode = this._lastNode();
- if (lastNode) {
- lastNode.focus(Node.focusElement || this._getElementName(target));
+ else if (keynum == 69) { // E
+ if (ctrlKey) { // Ctrl+E and Ctrl+Shift+E
+ this._onExpand(shiftKey); // recurse = shiftKey
+ target.focus(); // TODO: should restore focus in case of recursing expand (which takes DOM offline)
+ handled = true;
}
- handled = true;
}
- }
- else if (keynum == 36) { // Home
- if (altKey) { // Alt+Home
- // find the first node
- var firstNode = this._firstNode();
- if (firstNode) {
- firstNode.focus(Node.focusElement || this._getElementName(target));
+ else if (keynum == 77) { // M
+ if (ctrlKey) { // Ctrl+M
+ this.showContextMenu(target);
+ handled = true;
}
- handled = true;
}
- }
- else if (keynum == 37) { // Arrow Left
- if (altKey && !shiftKey) { // Alt + Arrow Left
- // move to left element
- var prevElement = this._previousElement(target);
- if (prevElement) {
- this.focus(this._getElementName(prevElement));
+ else if (keynum == 46) { // Del
+ if (ctrlKey) { // Ctrl+Del
+ this._onRemove();
+ handled = true;
}
- handled = true;
}
- else if (altKey && shiftKey) { // Alt + Shift Arrow left
- if (this.expanded) {
- var appendDom = this.getAppend();
- nextDom = appendDom ? appendDom.nextSibling : undefined;
+ else if (keynum == 45) { // Ins
+ if (ctrlKey && !shiftKey) { // Ctrl+Ins
+ this._onInsertBefore();
+ handled = true;
}
- else {
- var dom = this.getDom();
- nextDom = dom.nextSibling;
+ else if (ctrlKey && shiftKey) { // Ctrl+Shift+Ins
+ this._onInsertAfter();
+ handled = true;
}
- if (nextDom) {
- nextNode = Node.getNodeFromTarget(nextDom);
- nextDom2 = nextDom.nextSibling;
- nextNode2 = Node.getNodeFromTarget(nextDom2);
- if (nextNode && nextNode instanceof AppendNode &&
- !(this.parent.childs.length == 1) &&
- nextNode2 && nextNode2.parent) {
- nextNode2.parent.moveBefore(this, nextNode2);
- this.focus(Node.focusElement || this._getElementName(target));
+ }
+ else if (keynum == 35) { // End
+ if (altKey) { // Alt+End
+ // find the last node
+ var lastNode = this._lastNode();
+ if (lastNode) {
+ lastNode.focus(Node.focusElement || this._getElementName(target));
+ }
+ handled = true;
+ }
+ }
+ else if (keynum == 36) { // Home
+ if (altKey) { // Alt+Home
+ // find the first node
+ var firstNode = this._firstNode();
+ if (firstNode) {
+ firstNode.focus(Node.focusElement || this._getElementName(target));
+ }
+ handled = true;
+ }
+ }
+ else if (keynum == 37) { // Arrow Left
+ if (altKey && !shiftKey) { // Alt + Arrow Left
+ // move to left element
+ var prevElement = this._previousElement(target);
+ if (prevElement) {
+ this.focus(this._getElementName(prevElement));
+ }
+ handled = true;
+ }
+ else if (altKey && shiftKey) { // Alt + Shift Arrow left
+ if (this.expanded) {
+ var appendDom = this.getAppend();
+ nextDom = appendDom ? appendDom.nextSibling : undefined;
+ }
+ else {
+ var dom = this.getDom();
+ nextDom = dom.nextSibling;
+ }
+ if (nextDom) {
+ nextNode = Node.getNodeFromTarget(nextDom);
+ nextDom2 = nextDom.nextSibling;
+ nextNode2 = Node.getNodeFromTarget(nextDom2);
+ if (nextNode && nextNode instanceof AppendNode &&
+ !(this.parent.childs.length == 1) &&
+ nextNode2 && nextNode2.parent) {
+ nextNode2.parent.moveBefore(this, nextNode2);
+ this.focus(Node.focusElement || this._getElementName(target));
+ }
}
}
}
- }
- else if (keynum == 38) { // Arrow Up
- if (altKey && !shiftKey) { // Alt + Arrow Up
- // find the previous node
- prevNode = this._previousNode();
- if (prevNode) {
- prevNode.focus(Node.focusElement || this._getElementName(target));
+ else if (keynum == 38) { // Arrow Up
+ if (altKey && !shiftKey) { // Alt + Arrow Up
+ // find the previous node
+ prevNode = this._previousNode();
+ if (prevNode) {
+ prevNode.focus(Node.focusElement || this._getElementName(target));
+ }
+ handled = true;
}
- handled = true;
- }
- else if (altKey && shiftKey) { // Alt + Shift + Arrow Up
- // find the previous node
- prevNode = this._previousNode();
- if (prevNode && prevNode.parent) {
- prevNode.parent.moveBefore(this, prevNode);
- this.focus(Node.focusElement || this._getElementName(target));
- }
- handled = true;
- }
- }
- else if (keynum == 39) { // Arrow Right
- if (altKey && !shiftKey) { // Alt + Arrow Right
- // move to right element
- var nextElement = this._nextElement(target);
- if (nextElement) {
- this.focus(this._getElementName(nextElement));
- }
- handled = true;
- }
- else if (altKey && shiftKey) { // Alt + Shift Arrow Right
- dom = this.getDom();
- var prevDom = dom.previousSibling;
- if (prevDom) {
- prevNode = Node.getNodeFromTarget(prevDom);
- if (prevNode && prevNode.parent &&
- (prevNode instanceof AppendNode)
- && !prevNode.isVisible()) {
+ else if (altKey && shiftKey) { // Alt + Shift + Arrow Up
+ // find the previous node
+ prevNode = this._previousNode();
+ if (prevNode && prevNode.parent) {
prevNode.parent.moveBefore(this, prevNode);
this.focus(Node.focusElement || this._getElementName(target));
}
+ handled = true;
}
}
- }
- else if (keynum == 40) { // Arrow Down
- if (altKey && !shiftKey) { // Alt + Arrow Down
- // find the next node
- nextNode = this._nextNode();
- if (nextNode) {
- nextNode.focus(Node.focusElement || this._getElementName(target));
+ else if (keynum == 39) { // Arrow Right
+ if (altKey && !shiftKey) { // Alt + Arrow Right
+ // move to right element
+ var nextElement = this._nextElement(target);
+ if (nextElement) {
+ this.focus(this._getElementName(nextElement));
+ }
+ handled = true;
+ }
+ else if (altKey && shiftKey) { // Alt + Shift Arrow Right
+ dom = this.getDom();
+ var prevDom = dom.previousSibling;
+ if (prevDom) {
+ prevNode = Node.getNodeFromTarget(prevDom);
+ if (prevNode && prevNode.parent &&
+ (prevNode instanceof AppendNode)
+ && !prevNode.isVisible()) {
+ prevNode.parent.moveBefore(this, prevNode);
+ this.focus(Node.focusElement || this._getElementName(target));
+ }
+ }
}
- handled = true;
}
- else if (altKey && shiftKey) { // Alt + Shift + Arrow Down
- // find the 2nd next node and move before that one
- if (this.expanded) {
- nextNode = this.append ? this.append._nextNode() : undefined;
- }
- else {
+ else if (keynum == 40) { // Arrow Down
+ if (altKey && !shiftKey) { // Alt + Arrow Down
+ // find the next node
nextNode = this._nextNode();
+ if (nextNode) {
+ nextNode.focus(Node.focusElement || this._getElementName(target));
+ }
+ handled = true;
}
- nextDom = nextNode ? nextNode.getDom() : undefined;
- if (this.parent.childs.length == 1) {
- nextDom2 = nextDom;
+ else if (altKey && shiftKey) { // Alt + Shift + Arrow Down
+ // find the 2nd next node and move before that one
+ if (this.expanded) {
+ nextNode = this.append ? this.append._nextNode() : undefined;
+ }
+ else {
+ nextNode = this._nextNode();
+ }
+ nextDom = nextNode ? nextNode.getDom() : undefined;
+ if (this.parent.childs.length == 1) {
+ nextDom2 = nextDom;
+ }
+ else {
+ nextDom2 = nextDom ? nextDom.nextSibling : undefined;
+ }
+ var nextNode2 = Node.getNodeFromTarget(nextDom2);
+ if (nextNode2 && nextNode2.parent) {
+ nextNode2.parent.moveBefore(this, nextNode2);
+ this.focus(Node.focusElement || this._getElementName(target));
+ }
+ handled = true;
}
- else {
- nextDom2 = nextDom ? nextDom.nextSibling : undefined;
- }
- var nextNode2 = Node.getNodeFromTarget(nextDom2);
- if (nextNode2 && nextNode2.parent) {
- nextNode2.parent.moveBefore(this, nextNode2);
- this.focus(Node.focusElement || this._getElementName(target));
- }
- handled = true;
}
- }
- if (handled) {
- event.preventDefault();
- event.stopPropagation();
- }
-};
+ if (handled) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ };
-/**
- * Handle the expand event, when clicked on the expand button
- * @param {boolean} recurse If true, child nodes will be expanded too
- * @private
- */
-Node.prototype._onExpand = function (recurse) {
- if (recurse) {
- // Take the table offline
- var table = this.dom.tr.parentNode; // TODO: not nice to access the main table like this
- var frame = table.parentNode;
- var scrollTop = frame.scrollTop;
- frame.removeChild(table);
- }
+ /**
+ * Handle the expand event, when clicked on the expand button
+ * @param {boolean} recurse If true, child nodes will be expanded too
+ * @private
+ */
+ Node.prototype._onExpand = function (recurse) {
+ if (recurse) {
+ // Take the table offline
+ var table = this.dom.tr.parentNode; // TODO: not nice to access the main table like this
+ var frame = table.parentNode;
+ var scrollTop = frame.scrollTop;
+ frame.removeChild(table);
+ }
- if (this.expanded) {
- this.collapse(recurse);
- }
- else {
- this.expand(recurse);
- }
+ if (this.expanded) {
+ this.collapse(recurse);
+ }
+ else {
+ this.expand(recurse);
+ }
- if (recurse) {
- // Put the table online again
- frame.appendChild(table);
- frame.scrollTop = scrollTop;
- }
-};
+ if (recurse) {
+ // Put the table online again
+ frame.appendChild(table);
+ frame.scrollTop = scrollTop;
+ }
+ };
-/**
- * Remove this node
- * @private
- */
-Node.prototype._onRemove = function() {
- this.editor.highlighter.unhighlight();
- var childs = this.parent.childs;
- var index = childs.indexOf(this);
+ /**
+ * Remove this node
+ * @private
+ */
+ Node.prototype._onRemove = function() {
+ this.editor.highlighter.unhighlight();
+ var childs = this.parent.childs;
+ var index = childs.indexOf(this);
- // adjust the focus
- var oldSelection = this.editor.getSelection();
- if (childs[index + 1]) {
- childs[index + 1].focus();
- }
- else if (childs[index - 1]) {
- childs[index - 1].focus();
- }
- else {
- this.parent.focus();
- }
- var newSelection = this.editor.getSelection();
-
- // remove the node
- this.parent._remove(this);
-
- // store history action
- this.editor._onAction('removeNode', {
- 'node': this,
- 'parent': this.parent,
- 'index': index,
- 'oldSelection': oldSelection,
- 'newSelection': newSelection
- });
-};
-
-/**
- * Duplicate this node
- * @private
- */
-Node.prototype._onDuplicate = function() {
- var oldSelection = this.editor.getSelection();
- var clone = this.parent._duplicate(this);
- clone.focus();
- var newSelection = this.editor.getSelection();
-
- this.editor._onAction('duplicateNode', {
- 'node': this,
- 'clone': clone,
- 'parent': this.parent,
- 'oldSelection': oldSelection,
- 'newSelection': newSelection
- });
-};
-
-/**
- * Handle insert before event
- * @param {String} [field]
- * @param {*} [value]
- * @param {String} [type] Can be 'auto', 'array', 'object', or 'string'
- * @private
- */
-Node.prototype._onInsertBefore = function (field, value, type) {
- var oldSelection = this.editor.getSelection();
-
- var newNode = new Node(this.editor, {
- 'field': (field != undefined) ? field : '',
- 'value': (value != undefined) ? value : '',
- 'type': type
- });
- newNode.expand(true);
- this.parent.insertBefore(newNode, this);
- this.editor.highlighter.unhighlight();
- newNode.focus('field');
- var newSelection = this.editor.getSelection();
-
- this.editor._onAction('insertBeforeNode', {
- 'node': newNode,
- 'beforeNode': this,
- 'parent': this.parent,
- 'oldSelection': oldSelection,
- 'newSelection': newSelection
- });
-};
-
-/**
- * Handle insert after event
- * @param {String} [field]
- * @param {*} [value]
- * @param {String} [type] Can be 'auto', 'array', 'object', or 'string'
- * @private
- */
-Node.prototype._onInsertAfter = function (field, value, type) {
- var oldSelection = this.editor.getSelection();
-
- var newNode = new Node(this.editor, {
- 'field': (field != undefined) ? field : '',
- 'value': (value != undefined) ? value : '',
- 'type': type
- });
- newNode.expand(true);
- this.parent.insertAfter(newNode, this);
- this.editor.highlighter.unhighlight();
- newNode.focus('field');
- var newSelection = this.editor.getSelection();
-
- this.editor._onAction('insertAfterNode', {
- 'node': newNode,
- 'afterNode': this,
- 'parent': this.parent,
- 'oldSelection': oldSelection,
- 'newSelection': newSelection
- });
-};
-
-/**
- * Handle append event
- * @param {String} [field]
- * @param {*} [value]
- * @param {String} [type] Can be 'auto', 'array', 'object', or 'string'
- * @private
- */
-Node.prototype._onAppend = function (field, value, type) {
- var oldSelection = this.editor.getSelection();
-
- var newNode = new Node(this.editor, {
- 'field': (field != undefined) ? field : '',
- 'value': (value != undefined) ? value : '',
- 'type': type
- });
- newNode.expand(true);
- this.parent.appendChild(newNode);
- this.editor.highlighter.unhighlight();
- newNode.focus('field');
- var newSelection = this.editor.getSelection();
-
- this.editor._onAction('appendNode', {
- 'node': newNode,
- 'parent': this.parent,
- 'oldSelection': oldSelection,
- 'newSelection': newSelection
- });
-};
-
-/**
- * Change the type of the node's value
- * @param {String} newType
- * @private
- */
-Node.prototype._onChangeType = function (newType) {
- var oldType = this.type;
- if (newType != oldType) {
+ // adjust the focus
var oldSelection = this.editor.getSelection();
- this.changeType(newType);
+ if (childs[index + 1]) {
+ childs[index + 1].focus();
+ }
+ else if (childs[index - 1]) {
+ childs[index - 1].focus();
+ }
+ else {
+ this.parent.focus();
+ }
var newSelection = this.editor.getSelection();
- this.editor._onAction('changeType', {
+ // remove the node
+ this.parent._remove(this);
+
+ // store history action
+ this.editor._onAction('removeNode', {
'node': this,
- 'oldType': oldType,
- 'newType': newType,
+ 'parent': this.parent,
+ 'index': index,
'oldSelection': oldSelection,
'newSelection': newSelection
});
- }
-};
+ };
-/**
- * Sort the childs of the node. Only applicable when the node has type 'object'
- * or 'array'.
- * @param {String} direction Sorting direction. Available values: "asc", "desc"
- * @private
- */
-Node.prototype._onSort = function (direction) {
- if (this._hasChilds()) {
- var order = (direction == 'desc') ? -1 : 1;
- var prop = (this.type == 'array') ? 'value': 'field';
- this.hideChilds();
+ /**
+ * Duplicate this node
+ * @private
+ */
+ Node.prototype._onDuplicate = function() {
+ var oldSelection = this.editor.getSelection();
+ var clone = this.parent._duplicate(this);
+ clone.focus();
+ var newSelection = this.editor.getSelection();
- var oldChilds = this.childs;
- var oldSort = this.sort;
-
- // copy the array (the old one will be kept for an undo action
- this.childs = this.childs.concat();
-
- // sort the arrays
- this.childs.sort(function (a, b) {
- if (a[prop] > b[prop]) return order;
- if (a[prop] < b[prop]) return -order;
- return 0;
- });
- this.sort = (order == 1) ? 'asc' : 'desc';
-
- this.editor._onAction('sort', {
+ this.editor._onAction('duplicateNode', {
'node': this,
- 'oldChilds': oldChilds,
- 'oldSort': oldSort,
- 'newChilds': this.childs,
- 'newSort': this.sort
+ 'clone': clone,
+ 'parent': this.parent,
+ 'oldSelection': oldSelection,
+ 'newSelection': newSelection
});
+ };
- this.showChilds();
- }
-};
+ /**
+ * Handle insert before event
+ * @param {String} [field]
+ * @param {*} [value]
+ * @param {String} [type] Can be 'auto', 'array', 'object', or 'string'
+ * @private
+ */
+ Node.prototype._onInsertBefore = function (field, value, type) {
+ var oldSelection = this.editor.getSelection();
-/**
- * Create a table row with an append button.
- * @return {HTMLElement | undefined} buttonAppend or undefined when inapplicable
- */
-Node.prototype.getAppend = function () {
- if (!this.append) {
- this.append = new AppendNode(this.editor);
- this.append.setParent(this);
- }
- return this.append.getDom();
-};
+ var newNode = new Node(this.editor, {
+ 'field': (field != undefined) ? field : '',
+ 'value': (value != undefined) ? value : '',
+ 'type': type
+ });
+ newNode.expand(true);
+ this.parent.insertBefore(newNode, this);
+ this.editor.highlighter.unhighlight();
+ newNode.focus('field');
+ var newSelection = this.editor.getSelection();
-/**
- * Find the node from an event target
- * @param {Node} target
- * @return {Node | undefined} node or undefined when not found
- * @static
- */
-Node.getNodeFromTarget = function (target) {
- while (target) {
- if (target.node) {
- return target.node;
+ this.editor._onAction('insertBeforeNode', {
+ 'node': newNode,
+ 'beforeNode': this,
+ 'parent': this.parent,
+ 'oldSelection': oldSelection,
+ 'newSelection': newSelection
+ });
+ };
+
+ /**
+ * Handle insert after event
+ * @param {String} [field]
+ * @param {*} [value]
+ * @param {String} [type] Can be 'auto', 'array', 'object', or 'string'
+ * @private
+ */
+ Node.prototype._onInsertAfter = function (field, value, type) {
+ var oldSelection = this.editor.getSelection();
+
+ var newNode = new Node(this.editor, {
+ 'field': (field != undefined) ? field : '',
+ 'value': (value != undefined) ? value : '',
+ 'type': type
+ });
+ newNode.expand(true);
+ this.parent.insertAfter(newNode, this);
+ this.editor.highlighter.unhighlight();
+ newNode.focus('field');
+ var newSelection = this.editor.getSelection();
+
+ this.editor._onAction('insertAfterNode', {
+ 'node': newNode,
+ 'afterNode': this,
+ 'parent': this.parent,
+ 'oldSelection': oldSelection,
+ 'newSelection': newSelection
+ });
+ };
+
+ /**
+ * Handle append event
+ * @param {String} [field]
+ * @param {*} [value]
+ * @param {String} [type] Can be 'auto', 'array', 'object', or 'string'
+ * @private
+ */
+ Node.prototype._onAppend = function (field, value, type) {
+ var oldSelection = this.editor.getSelection();
+
+ var newNode = new Node(this.editor, {
+ 'field': (field != undefined) ? field : '',
+ 'value': (value != undefined) ? value : '',
+ 'type': type
+ });
+ newNode.expand(true);
+ this.parent.appendChild(newNode);
+ this.editor.highlighter.unhighlight();
+ newNode.focus('field');
+ var newSelection = this.editor.getSelection();
+
+ this.editor._onAction('appendNode', {
+ 'node': newNode,
+ 'parent': this.parent,
+ 'oldSelection': oldSelection,
+ 'newSelection': newSelection
+ });
+ };
+
+ /**
+ * Change the type of the node's value
+ * @param {String} newType
+ * @private
+ */
+ Node.prototype._onChangeType = function (newType) {
+ var oldType = this.type;
+ if (newType != oldType) {
+ var oldSelection = this.editor.getSelection();
+ this.changeType(newType);
+ var newSelection = this.editor.getSelection();
+
+ this.editor._onAction('changeType', {
+ 'node': this,
+ 'oldType': oldType,
+ 'newType': newType,
+ 'oldSelection': oldSelection,
+ 'newSelection': newSelection
+ });
}
- target = target.parentNode;
- }
+ };
- return undefined;
-};
+ /**
+ * Sort the childs of the node. Only applicable when the node has type 'object'
+ * or 'array'.
+ * @param {String} direction Sorting direction. Available values: "asc", "desc"
+ * @private
+ */
+ Node.prototype._onSort = function (direction) {
+ if (this._hasChilds()) {
+ var order = (direction == 'desc') ? -1 : 1;
+ var prop = (this.type == 'array') ? 'value': 'field';
+ this.hideChilds();
-/**
- * Get the previously rendered node
- * @return {Node | null} previousNode
- * @private
- */
-Node.prototype._previousNode = function () {
- var prevNode = null;
- var dom = this.getDom();
- if (dom && dom.parentNode) {
- // find the previous field
- var prevDom = dom;
- do {
- prevDom = prevDom.previousSibling;
- prevNode = Node.getNodeFromTarget(prevDom);
+ var oldChilds = this.childs;
+ var oldSort = this.sort;
+
+ // copy the array (the old one will be kept for an undo action
+ this.childs = this.childs.concat();
+
+ // sort the arrays
+ this.childs.sort(function (a, b) {
+ if (a[prop] > b[prop]) return order;
+ if (a[prop] < b[prop]) return -order;
+ return 0;
+ });
+ this.sort = (order == 1) ? 'asc' : 'desc';
+
+ this.editor._onAction('sort', {
+ 'node': this,
+ 'oldChilds': oldChilds,
+ 'oldSort': oldSort,
+ 'newChilds': this.childs,
+ 'newSort': this.sort
+ });
+
+ this.showChilds();
}
- while (prevDom && (prevNode instanceof AppendNode && !prevNode.isVisible()));
- }
- return prevNode;
-};
+ };
-/**
- * Get the next rendered node
- * @return {Node | null} nextNode
- * @private
- */
-Node.prototype._nextNode = function () {
- var nextNode = null;
- var dom = this.getDom();
- if (dom && dom.parentNode) {
- // find the previous field
- var nextDom = dom;
- do {
- nextDom = nextDom.nextSibling;
- nextNode = Node.getNodeFromTarget(nextDom);
+ /**
+ * Create a table row with an append button.
+ * @return {HTMLElement | undefined} buttonAppend or undefined when inapplicable
+ */
+ Node.prototype.getAppend = function () {
+ if (!this.append) {
+ this.append = new AppendNode(this.editor);
+ this.append.setParent(this);
}
- while (nextDom && (nextNode instanceof AppendNode && !nextNode.isVisible()));
- }
+ return this.append.getDom();
+ };
- return nextNode;
-};
+ /**
+ * Find the node from an event target
+ * @param {Node} target
+ * @return {Node | undefined} node or undefined when not found
+ * @static
+ */
+ Node.getNodeFromTarget = function (target) {
+ while (target) {
+ if (target.node) {
+ return target.node;
+ }
+ target = target.parentNode;
+ }
-/**
- * Get the first rendered node
- * @return {Node | null} firstNode
- * @private
- */
-Node.prototype._firstNode = function () {
- var firstNode = null;
- var dom = this.getDom();
- if (dom && dom.parentNode) {
- var firstDom = dom.parentNode.firstChild;
- firstNode = Node.getNodeFromTarget(firstDom);
- }
+ return undefined;
+ };
- return firstNode;
-};
+ /**
+ * Get the previously rendered node
+ * @return {Node | null} previousNode
+ * @private
+ */
+ Node.prototype._previousNode = function () {
+ var prevNode = null;
+ var dom = this.getDom();
+ if (dom && dom.parentNode) {
+ // find the previous field
+ var prevDom = dom;
+ do {
+ prevDom = prevDom.previousSibling;
+ prevNode = Node.getNodeFromTarget(prevDom);
+ }
+ while (prevDom && (prevNode instanceof AppendNode && !prevNode.isVisible()));
+ }
+ return prevNode;
+ };
-/**
- * Get the last rendered node
- * @return {Node | null} lastNode
- * @private
- */
-Node.prototype._lastNode = function () {
- var lastNode = null;
- var dom = this.getDom();
- if (dom && dom.parentNode) {
- var lastDom = dom.parentNode.lastChild;
- lastNode = Node.getNodeFromTarget(lastDom);
- while (lastDom && (lastNode instanceof AppendNode && !lastNode.isVisible())) {
- lastDom = lastDom.previousSibling;
+ /**
+ * Get the next rendered node
+ * @return {Node | null} nextNode
+ * @private
+ */
+ Node.prototype._nextNode = function () {
+ var nextNode = null;
+ var dom = this.getDom();
+ if (dom && dom.parentNode) {
+ // find the previous field
+ var nextDom = dom;
+ do {
+ nextDom = nextDom.nextSibling;
+ nextNode = Node.getNodeFromTarget(nextDom);
+ }
+ while (nextDom && (nextNode instanceof AppendNode && !nextNode.isVisible()));
+ }
+
+ return nextNode;
+ };
+
+ /**
+ * Get the first rendered node
+ * @return {Node | null} firstNode
+ * @private
+ */
+ Node.prototype._firstNode = function () {
+ var firstNode = null;
+ var dom = this.getDom();
+ if (dom && dom.parentNode) {
+ var firstDom = dom.parentNode.firstChild;
+ firstNode = Node.getNodeFromTarget(firstDom);
+ }
+
+ return firstNode;
+ };
+
+ /**
+ * Get the last rendered node
+ * @return {Node | null} lastNode
+ * @private
+ */
+ Node.prototype._lastNode = function () {
+ var lastNode = null;
+ var dom = this.getDom();
+ if (dom && dom.parentNode) {
+ var lastDom = dom.parentNode.lastChild;
lastNode = Node.getNodeFromTarget(lastDom);
- }
- }
- return lastNode;
-};
-
-/**
- * Get the next element which can have focus.
- * @param {Element} elem
- * @return {Element | null} nextElem
- * @private
- */
-Node.prototype._previousElement = function (elem) {
- var dom = this.dom;
- // noinspection FallthroughInSwitchStatementJS
- switch (elem) {
- case dom.value:
- if (this.fieldEditable) {
- return dom.field;
- }
- // intentional fall through
- case dom.field:
- if (this._hasChilds()) {
- return dom.expand;
- }
- // intentional fall through
- case dom.expand:
- return dom.menu;
- case dom.menu:
- if (dom.drag) {
- return dom.drag;
- }
- // intentional fall through
- default:
- return null;
- }
-};
-
-/**
- * Get the next element which can have focus.
- * @param {Element} elem
- * @return {Element | null} nextElem
- * @private
- */
-Node.prototype._nextElement = function (elem) {
- var dom = this.dom;
- // noinspection FallthroughInSwitchStatementJS
- switch (elem) {
- case dom.drag:
- return dom.menu;
- case dom.menu:
- if (this._hasChilds()) {
- return dom.expand;
- }
- // intentional fall through
- case dom.expand:
- if (this.fieldEditable) {
- return dom.field;
- }
- // intentional fall through
- case dom.field:
- if (!this._hasChilds()) {
- return dom.value;
- }
- default:
- return null;
- }
-};
-
-/**
- * Get the dom name of given element. returns null if not found.
- * For example when element == dom.field, "field" is returned.
- * @param {Element} element
- * @return {String | null} elementName Available elements with name: 'drag',
- * 'menu', 'expand', 'field', 'value'
- * @private
- */
-Node.prototype._getElementName = function (element) {
- var dom = this.dom;
- for (var name in dom) {
- if (dom.hasOwnProperty(name)) {
- if (dom[name] == element) {
- return name;
+ while (lastDom && (lastNode instanceof AppendNode && !lastNode.isVisible())) {
+ lastDom = lastDom.previousSibling;
+ lastNode = Node.getNodeFromTarget(lastDom);
}
}
- }
- return null;
-};
+ return lastNode;
+ };
-/**
- * Test if this node has childs. This is the case when the node is an object
- * or array.
- * @return {boolean} hasChilds
- * @private
- */
-Node.prototype._hasChilds = function () {
- return this.type == 'array' || this.type == 'object';
-};
+ /**
+ * Get the next element which can have focus.
+ * @param {Element} elem
+ * @return {Element | null} nextElem
+ * @private
+ */
+ Node.prototype._previousElement = function (elem) {
+ var dom = this.dom;
+ // noinspection FallthroughInSwitchStatementJS
+ switch (elem) {
+ case dom.value:
+ if (this.fieldEditable) {
+ return dom.field;
+ }
+ // intentional fall through
+ case dom.field:
+ if (this._hasChilds()) {
+ return dom.expand;
+ }
+ // intentional fall through
+ case dom.expand:
+ return dom.menu;
+ case dom.menu:
+ if (dom.drag) {
+ return dom.drag;
+ }
+ // intentional fall through
+ default:
+ return null;
+ }
+ };
+
+ /**
+ * Get the next element which can have focus.
+ * @param {Element} elem
+ * @return {Element | null} nextElem
+ * @private
+ */
+ Node.prototype._nextElement = function (elem) {
+ var dom = this.dom;
+ // noinspection FallthroughInSwitchStatementJS
+ switch (elem) {
+ case dom.drag:
+ return dom.menu;
+ case dom.menu:
+ if (this._hasChilds()) {
+ return dom.expand;
+ }
+ // intentional fall through
+ case dom.expand:
+ if (this.fieldEditable) {
+ return dom.field;
+ }
+ // intentional fall through
+ case dom.field:
+ if (!this._hasChilds()) {
+ return dom.value;
+ }
+ default:
+ return null;
+ }
+ };
+
+ /**
+ * Get the dom name of given element. returns null if not found.
+ * For example when element == dom.field, "field" is returned.
+ * @param {Element} element
+ * @return {String | null} elementName Available elements with name: 'drag',
+ * 'menu', 'expand', 'field', 'value'
+ * @private
+ */
+ Node.prototype._getElementName = function (element) {
+ var dom = this.dom;
+ for (var name in dom) {
+ if (dom.hasOwnProperty(name)) {
+ if (dom[name] == element) {
+ return name;
+ }
+ }
+ }
+ return null;
+ };
+
+ /**
+ * Test if this node has childs. This is the case when the node is an object
+ * or array.
+ * @return {boolean} hasChilds
+ * @private
+ */
+ Node.prototype._hasChilds = function () {
+ return this.type == 'array' || this.type == 'object';
+ };
// titles with explanation for the different types
-Node.TYPE_TITLES = {
- 'auto': 'Field type "auto". ' +
- 'The field type is automatically determined from the value ' +
- 'and can be a string, number, boolean, or null.',
- 'object': 'Field type "object". ' +
- 'An object contains an unordered set of key/value pairs.',
- 'array': 'Field type "array". ' +
- 'An array contains an ordered collection of values.',
- 'string': 'Field type "string". ' +
- 'Field type is not determined from the value, ' +
- 'but always returned as string.'
-};
+ Node.TYPE_TITLES = {
+ 'auto': 'Field type "auto". ' +
+ 'The field type is automatically determined from the value ' +
+ 'and can be a string, number, boolean, or null.',
+ 'object': 'Field type "object". ' +
+ 'An object contains an unordered set of key/value pairs.',
+ 'array': 'Field type "array". ' +
+ 'An array contains an ordered collection of values.',
+ 'string': 'Field type "string". ' +
+ 'Field type is not determined from the value, ' +
+ 'but always returned as string.'
+ };
-/**
- * Show a contextmenu for this node
- * @param {HTMLElement} anchor Anchor element to attache the context menu to.
- * @param {function} [onClose] Callback method called when the context menu
- * is being closed.
- */
-Node.prototype.showContextMenu = function (anchor, onClose) {
- var node = this;
- var titles = Node.TYPE_TITLES;
- var items = [];
+ /**
+ * Show a contextmenu for this node
+ * @param {HTMLElement} anchor Anchor element to attache the context menu to.
+ * @param {function} [onClose] Callback method called when the context menu
+ * is being closed.
+ */
+ Node.prototype.showContextMenu = function (anchor, onClose) {
+ var node = this;
+ var titles = Node.TYPE_TITLES;
+ var items = [];
- items.push({
- 'text': 'Type',
- 'title': 'Change the type of this field',
- 'className': 'type-' + this.type,
- 'submenu': [
- {
- 'text': 'Auto',
- 'className': 'type-auto' +
- (this.type == 'auto' ? ' selected' : ''),
- 'title': titles.auto,
- 'click': function () {
- node._onChangeType('auto');
- }
- },
- {
- 'text': 'Array',
- 'className': 'type-array' +
- (this.type == 'array' ? ' selected' : ''),
- 'title': titles.array,
- 'click': function () {
- node._onChangeType('array');
- }
- },
- {
- 'text': 'Object',
- 'className': 'type-object' +
- (this.type == 'object' ? ' selected' : ''),
- 'title': titles.object,
- 'click': function () {
- node._onChangeType('object');
- }
- },
- {
- 'text': 'String',
- 'className': 'type-string' +
- (this.type == 'string' ? ' selected' : ''),
- 'title': titles.string,
- 'click': function () {
- node._onChangeType('string');
- }
- }
- ]
- });
-
- if (this._hasChilds()) {
- var direction = ((this.sort == 'asc') ? 'desc': 'asc');
items.push({
- 'text': 'Sort',
- 'title': 'Sort the childs of this ' + this.type,
- 'className': 'sort-' + direction,
- 'click': function () {
- node._onSort(direction);
- },
+ 'text': 'Type',
+ 'title': 'Change the type of this field',
+ 'className': 'type-' + this.type,
'submenu': [
{
- 'text': 'Ascending',
- 'className': 'sort-asc',
- 'title': 'Sort the childs of this ' + this.type + ' in ascending order',
+ 'text': 'Auto',
+ 'className': 'type-auto' +
+ (this.type == 'auto' ? ' selected' : ''),
+ 'title': titles.auto,
'click': function () {
- node._onSort('asc');
+ node._onChangeType('auto');
}
},
{
- 'text': 'Descending',
- 'className': 'sort-desc',
- 'title': 'Sort the childs of this ' + this.type +' in descending order',
+ 'text': 'Array',
+ 'className': 'type-array' +
+ (this.type == 'array' ? ' selected' : ''),
+ 'title': titles.array,
'click': function () {
- node._onSort('desc');
+ node._onChangeType('array');
+ }
+ },
+ {
+ 'text': 'Object',
+ 'className': 'type-object' +
+ (this.type == 'object' ? ' selected' : ''),
+ 'title': titles.object,
+ 'click': function () {
+ node._onChangeType('object');
+ }
+ },
+ {
+ 'text': 'String',
+ 'className': 'type-string' +
+ (this.type == 'string' ? ' selected' : ''),
+ 'title': titles.string,
+ 'click': function () {
+ node._onChangeType('string');
}
}
]
});
- }
- if (this.parent && this.parent._hasChilds()) {
- // create a separator
- items.push({
- 'type': 'separator'
- });
-
- // create append button (for last child node only)
- var childs = node.parent.childs;
- if (node == childs[childs.length - 1]) {
+ if (this._hasChilds()) {
+ var direction = ((this.sort == 'asc') ? 'desc': 'asc');
items.push({
- 'text': 'Append',
- 'title': 'Append a new field with type \'auto\' after this field (Ctrl+Shift+Ins)',
- 'submenuTitle': 'Select the type of the field to be appended',
- 'className': 'append',
+ 'text': 'Sort',
+ 'title': 'Sort the childs of this ' + this.type,
+ 'className': 'sort-' + direction,
'click': function () {
- node._onAppend('', '', 'auto');
+ node._onSort(direction);
+ },
+ 'submenu': [
+ {
+ 'text': 'Ascending',
+ 'className': 'sort-asc',
+ 'title': 'Sort the childs of this ' + this.type + ' in ascending order',
+ 'click': function () {
+ node._onSort('asc');
+ }
+ },
+ {
+ 'text': 'Descending',
+ 'className': 'sort-desc',
+ 'title': 'Sort the childs of this ' + this.type +' in descending order',
+ 'click': function () {
+ node._onSort('desc');
+ }
+ }
+ ]
+ });
+ }
+
+ if (this.parent && this.parent._hasChilds()) {
+ // create a separator
+ items.push({
+ 'type': 'separator'
+ });
+
+ // create append button (for last child node only)
+ var childs = node.parent.childs;
+ if (node == childs[childs.length - 1]) {
+ items.push({
+ 'text': 'Append',
+ 'title': 'Append a new field with type \'auto\' after this field (Ctrl+Shift+Ins)',
+ 'submenuTitle': 'Select the type of the field to be appended',
+ 'className': 'append',
+ 'click': function () {
+ node._onAppend('', '', 'auto');
+ },
+ 'submenu': [
+ {
+ 'text': 'Auto',
+ 'className': 'type-auto',
+ 'title': titles.auto,
+ 'click': function () {
+ node._onAppend('', '', 'auto');
+ }
+ },
+ {
+ 'text': 'Array',
+ 'className': 'type-array',
+ 'title': titles.array,
+ 'click': function () {
+ node._onAppend('', []);
+ }
+ },
+ {
+ 'text': 'Object',
+ 'className': 'type-object',
+ 'title': titles.object,
+ 'click': function () {
+ node._onAppend('', {});
+ }
+ },
+ {
+ 'text': 'String',
+ 'className': 'type-string',
+ 'title': titles.string,
+ 'click': function () {
+ node._onAppend('', '', 'string');
+ }
+ }
+ ]
+ });
+ }
+
+ // create insert button
+ items.push({
+ 'text': 'Insert',
+ 'title': 'Insert a new field with type \'auto\' before this field (Ctrl+Ins)',
+ 'submenuTitle': 'Select the type of the field to be inserted',
+ 'className': 'insert',
+ 'click': function () {
+ node._onInsertBefore('', '', 'auto');
},
'submenu': [
{
@@ -2632,7 +2680,7 @@ Node.prototype.showContextMenu = function (anchor, onClose) {
'className': 'type-auto',
'title': titles.auto,
'click': function () {
- node._onAppend('', '', 'auto');
+ node._onInsertBefore('', '', 'auto');
}
},
{
@@ -2640,7 +2688,7 @@ Node.prototype.showContextMenu = function (anchor, onClose) {
'className': 'type-array',
'title': titles.array,
'click': function () {
- node._onAppend('', []);
+ node._onInsertBefore('', []);
}
},
{
@@ -2648,7 +2696,7 @@ Node.prototype.showContextMenu = function (anchor, onClose) {
'className': 'type-object',
'title': titles.object,
'click': function () {
- node._onAppend('', {});
+ node._onInsertBefore('', {});
}
},
{
@@ -2656,204 +2704,164 @@ Node.prototype.showContextMenu = function (anchor, onClose) {
'className': 'type-string',
'title': titles.string,
'click': function () {
- node._onAppend('', '', 'string');
+ node._onInsertBefore('', '', 'string');
}
}
]
});
- }
- // create insert button
- items.push({
- 'text': 'Insert',
- 'title': 'Insert a new field with type \'auto\' before this field (Ctrl+Ins)',
- 'submenuTitle': 'Select the type of the field to be inserted',
- 'className': 'insert',
- 'click': function () {
- node._onInsertBefore('', '', 'auto');
- },
- 'submenu': [
- {
- 'text': 'Auto',
- 'className': 'type-auto',
- 'title': titles.auto,
- 'click': function () {
- node._onInsertBefore('', '', 'auto');
- }
- },
- {
- 'text': 'Array',
- 'className': 'type-array',
- 'title': titles.array,
- 'click': function () {
- node._onInsertBefore('', []);
- }
- },
- {
- 'text': 'Object',
- 'className': 'type-object',
- 'title': titles.object,
- 'click': function () {
- node._onInsertBefore('', {});
- }
- },
- {
- 'text': 'String',
- 'className': 'type-string',
- 'title': titles.string,
- 'click': function () {
- node._onInsertBefore('', '', 'string');
- }
+ // create duplicate button
+ items.push({
+ 'text': 'Duplicate',
+ 'title': 'Duplicate this field (Ctrl+D)',
+ 'className': 'duplicate',
+ 'click': function () {
+ node._onDuplicate();
}
- ]
- });
+ });
- // create duplicate button
- items.push({
- 'text': 'Duplicate',
- 'title': 'Duplicate this field (Ctrl+D)',
- 'className': 'duplicate',
- 'click': function () {
- node._onDuplicate();
- }
- });
-
- // create remove button
- items.push({
- 'text': 'Remove',
- 'title': 'Remove this field (Ctrl+Del)',
- 'className': 'remove',
- 'click': function () {
- node._onRemove();
- }
- });
- }
-
- var menu = new ContextMenu(items, {close: onClose});
- menu.show(anchor);
-};
-
-/**
- * get the type of a value
- * @param {*} value
- * @return {String} type Can be 'object', 'array', 'string', 'auto'
- * @private
- */
-Node.prototype._getType = function(value) {
- if (value instanceof Array) {
- return 'array';
- }
- if (value instanceof Object) {
- return 'object';
- }
- if (typeof(value) == 'string' && typeof(this._stringCast(value)) != 'string') {
- return 'string';
- }
-
- return 'auto';
-};
-
-/**
- * cast contents of a string to the correct type. This can be a string,
- * a number, a boolean, etc
- * @param {String} str
- * @return {*} castedStr
- * @private
- */
-Node.prototype._stringCast = function(str) {
- var lower = str.toLowerCase(),
- num = Number(str), // will nicely fail with '123ab'
- numFloat = parseFloat(str); // will nicely fail with ' '
-
- if (str == '') {
- return '';
- }
- else if (lower == 'null') {
- return null;
- }
- else if (lower == 'true') {
- return true;
- }
- else if (lower == 'false') {
- return false;
- }
- else if (!isNaN(num) && !isNaN(numFloat)) {
- return num;
- }
- else {
- return str;
- }
-};
-
-/**
- * escape a text, such that it can be displayed safely in an HTML element
- * @param {String} text
- * @return {String} escapedText
- * @private
- */
-Node.prototype._escapeHTML = function (text) {
- var htmlEscaped = String(text)
- .replace(//g, '>')
- .replace(/ /g, ' ') // replace double space with an nbsp and space
- .replace(/^ /, ' ') // space at start
- .replace(/ $/, ' '); // space at end
-
- var json = JSON.stringify(htmlEscaped);
- return json.substring(1, json.length - 1);
-};
-
-/**
- * unescape a string.
- * @param {String} escapedText
- * @return {String} text
- * @private
- */
-Node.prototype._unescapeHTML = function (escapedText) {
- var json = '"' + this._escapeJSON(escapedText) + '"';
- var htmlEscaped = util.parse(json);
- return htmlEscaped
- .replace(/</g, '<')
- .replace(/>/g, '>')
- .replace(/ |\u00A0/g, ' ');
-};
-
-/**
- * escape a text to make it a valid JSON string. The method will:
- * - replace unescaped double quotes with '\"'
- * - replace unescaped backslash with '\\'
- * - replace returns with '\n'
- * @param {String} text
- * @return {String} escapedText
- * @private
- */
-Node.prototype._escapeJSON = function (text) {
- // TODO: replace with some smart regex (only when a new solution is faster!)
- var escaped = '';
- var i = 0, iMax = text.length;
- while (i < iMax) {
- var c = text.charAt(i);
- if (c == '\n') {
- escaped += '\\n';
+ // create remove button
+ items.push({
+ 'text': 'Remove',
+ 'title': 'Remove this field (Ctrl+Del)',
+ 'className': 'remove',
+ 'click': function () {
+ node._onRemove();
+ }
+ });
}
- else if (c == '\\') {
- escaped += c;
- i++;
- c = text.charAt(i);
- if ('"\\/bfnrtu'.indexOf(c) == -1) {
- escaped += '\\'; // no valid escape character
- }
- escaped += c;
+ var menu = new ContextMenu(items, {close: onClose});
+ menu.show(anchor);
+ };
+
+ /**
+ * get the type of a value
+ * @param {*} value
+ * @return {String} type Can be 'object', 'array', 'string', 'auto'
+ * @private
+ */
+ Node.prototype._getType = function(value) {
+ if (value instanceof Array) {
+ return 'array';
}
- else if (c == '"') {
- escaped += '\\"';
+ if (value instanceof Object) {
+ return 'object';
+ }
+ if (typeof(value) == 'string' && typeof(this._stringCast(value)) != 'string') {
+ return 'string';
+ }
+
+ return 'auto';
+ };
+
+ /**
+ * cast contents of a string to the correct type. This can be a string,
+ * a number, a boolean, etc
+ * @param {String} str
+ * @return {*} castedStr
+ * @private
+ */
+ Node.prototype._stringCast = function(str) {
+ var lower = str.toLowerCase(),
+ num = Number(str), // will nicely fail with '123ab'
+ numFloat = parseFloat(str); // will nicely fail with ' '
+
+ if (str == '') {
+ return '';
+ }
+ else if (lower == 'null') {
+ return null;
+ }
+ else if (lower == 'true') {
+ return true;
+ }
+ else if (lower == 'false') {
+ return false;
+ }
+ else if (!isNaN(num) && !isNaN(numFloat)) {
+ return num;
}
else {
- escaped += c;
+ return str;
}
- i++;
- }
+ };
- return escaped;
-};
+ /**
+ * escape a text, such that it can be displayed safely in an HTML element
+ * @param {String} text
+ * @return {String} escapedText
+ * @private
+ */
+ Node.prototype._escapeHTML = function (text) {
+ var htmlEscaped = String(text)
+ .replace(//g, '>')
+ .replace(/ /g, ' ') // replace double space with an nbsp and space
+ .replace(/^ /, ' ') // space at start
+ .replace(/ $/, ' '); // space at end
+
+ var json = JSON.stringify(htmlEscaped);
+ return json.substring(1, json.length - 1);
+ };
+
+ /**
+ * unescape a string.
+ * @param {String} escapedText
+ * @return {String} text
+ * @private
+ */
+ Node.prototype._unescapeHTML = function (escapedText) {
+ var json = '"' + this._escapeJSON(escapedText) + '"';
+ var htmlEscaped = util.parse(json);
+ return htmlEscaped
+ .replace(/</g, '<')
+ .replace(/>/g, '>')
+ .replace(/ |\u00A0/g, ' ');
+ };
+
+ /**
+ * escape a text to make it a valid JSON string. The method will:
+ * - replace unescaped double quotes with '\"'
+ * - replace unescaped backslash with '\\'
+ * - replace returns with '\n'
+ * @param {String} text
+ * @return {String} escapedText
+ * @private
+ */
+ Node.prototype._escapeJSON = function (text) {
+ // TODO: replace with some smart regex (only when a new solution is faster!)
+ var escaped = '';
+ var i = 0, iMax = text.length;
+ while (i < iMax) {
+ var c = text.charAt(i);
+ if (c == '\n') {
+ escaped += '\\n';
+ }
+ else if (c == '\\') {
+ escaped += c;
+ i++;
+
+ c = text.charAt(i);
+ if ('"\\/bfnrtu'.indexOf(c) == -1) {
+ escaped += '\\'; // no valid escape character
+ }
+ escaped += c;
+ }
+ else if (c == '"') {
+ escaped += '\\"';
+ }
+ else {
+ escaped += c;
+ }
+ i++;
+ }
+
+ return escaped;
+ };
+
+ // TODO: find a nicer solution to resolve this circular dependency between Node and AppendNode
+ var AppendNode = appendNodeFactory(Node);
+
+ return Node;
+});
\ No newline at end of file
diff --git a/src/js/SearchBox.js b/src/js/SearchBox.js
index 3ce3640..3ecfe02 100644
--- a/src/js/SearchBox.js
+++ b/src/js/SearchBox.js
@@ -1,286 +1,293 @@
-/**
- * @constructor SearchBox
- * Create a search box in given HTML container
- * @param {JSONEditor} editor The JSON Editor to attach to
- * @param {Element} container HTML container element of where to
- * create the search box
- */
-function SearchBox (editor, container) {
- var searchBox = this;
+define(function () {
- this.editor = editor;
- this.timeout = undefined;
- this.delay = 200; // ms
- this.lastText = undefined;
+ /**
+ * @constructor SearchBox
+ * Create a search box in given HTML container
+ * @param {JSONEditor} editor The JSON Editor to attach to
+ * @param {Element} container HTML container element of where to
+ * create the search box
+ */
+ function SearchBox (editor, container) {
+ var searchBox = this;
- this.dom = {};
- this.dom.container = container;
+ this.editor = editor;
+ this.timeout = undefined;
+ this.delay = 200; // ms
+ this.lastText = undefined;
- var table = document.createElement('table');
- this.dom.table = table;
- table.className = 'search';
- container.appendChild(table);
- var tbody = document.createElement('tbody');
- this.dom.tbody = tbody;
- table.appendChild(tbody);
- var tr = document.createElement('tr');
- tbody.appendChild(tr);
+ this.dom = {};
+ this.dom.container = container;
- var td = document.createElement('td');
- tr.appendChild(td);
- var results = document.createElement('div');
- this.dom.results = results;
- results.className = 'results';
- td.appendChild(results);
+ var table = document.createElement('table');
+ this.dom.table = table;
+ table.className = 'search';
+ container.appendChild(table);
+ var tbody = document.createElement('tbody');
+ this.dom.tbody = tbody;
+ table.appendChild(tbody);
+ var tr = document.createElement('tr');
+ tbody.appendChild(tr);
- td = document.createElement('td');
- tr.appendChild(td);
- var divInput = document.createElement('div');
- this.dom.input = divInput;
- divInput.className = 'frame';
- divInput.title = 'Search fields and values';
- td.appendChild(divInput);
+ var td = document.createElement('td');
+ tr.appendChild(td);
+ var results = document.createElement('div');
+ this.dom.results = results;
+ results.className = 'results';
+ td.appendChild(results);
- // table to contain the text input and search button
- var tableInput = document.createElement('table');
- divInput.appendChild(tableInput);
- var tbodySearch = document.createElement('tbody');
- tableInput.appendChild(tbodySearch);
- tr = document.createElement('tr');
- tbodySearch.appendChild(tr);
+ td = document.createElement('td');
+ tr.appendChild(td);
+ var divInput = document.createElement('div');
+ this.dom.input = divInput;
+ divInput.className = 'frame';
+ divInput.title = 'Search fields and values';
+ td.appendChild(divInput);
- var refreshSearch = document.createElement('button');
- refreshSearch.className = 'refresh';
- td = document.createElement('td');
- td.appendChild(refreshSearch);
- tr.appendChild(td);
+ // table to contain the text input and search button
+ var tableInput = document.createElement('table');
+ divInput.appendChild(tableInput);
+ var tbodySearch = document.createElement('tbody');
+ tableInput.appendChild(tbodySearch);
+ tr = document.createElement('tr');
+ tbodySearch.appendChild(tr);
- var search = document.createElement('input');
- this.dom.search = search;
- search.oninput = function (event) {
- searchBox._onDelayedSearch(event);
- };
- search.onchange = function (event) { // For IE 9
- searchBox._onSearch(event);
- };
- search.onkeydown = function (event) {
- searchBox._onKeyDown(event);
- };
- search.onkeyup = function (event) {
- searchBox._onKeyUp(event);
- };
- refreshSearch.onclick = function (event) {
- search.select();
- };
+ var refreshSearch = document.createElement('button');
+ refreshSearch.className = 'refresh';
+ td = document.createElement('td');
+ td.appendChild(refreshSearch);
+ tr.appendChild(td);
- // TODO: ESC in FF restores the last input, is a FF bug, https://bugzilla.mozilla.org/show_bug.cgi?id=598819
- td = document.createElement('td');
- td.appendChild(search);
- tr.appendChild(td);
+ var search = document.createElement('input');
+ this.dom.search = search;
+ search.oninput = function (event) {
+ searchBox._onDelayedSearch(event);
+ };
+ search.onchange = function (event) { // For IE 9
+ searchBox._onSearch(event);
+ };
+ search.onkeydown = function (event) {
+ searchBox._onKeyDown(event);
+ };
+ search.onkeyup = function (event) {
+ searchBox._onKeyUp(event);
+ };
+ refreshSearch.onclick = function (event) {
+ search.select();
+ };
- var searchNext = document.createElement('button');
- searchNext.title = 'Next result (Enter)';
- searchNext.className = 'next';
- searchNext.onclick = function () {
- searchBox.next();
- };
- td = document.createElement('td');
- td.appendChild(searchNext);
- tr.appendChild(td);
+ // TODO: ESC in FF restores the last input, is a FF bug, https://bugzilla.mozilla.org/show_bug.cgi?id=598819
+ td = document.createElement('td');
+ td.appendChild(search);
+ tr.appendChild(td);
- var searchPrevious = document.createElement('button');
- searchPrevious.title = 'Previous result (Shift+Enter)';
- searchPrevious.className = 'previous';
- searchPrevious.onclick = function () {
- searchBox.previous();
- };
- td = document.createElement('td');
- td.appendChild(searchPrevious);
- tr.appendChild(td);
-}
+ var searchNext = document.createElement('button');
+ searchNext.title = 'Next result (Enter)';
+ searchNext.className = 'next';
+ searchNext.onclick = function () {
+ searchBox.next();
+ };
+ td = document.createElement('td');
+ td.appendChild(searchNext);
+ tr.appendChild(td);
-/**
- * Go to the next search result
- * @param {boolean} [focus] If true, focus will be set to the next result
- * focus is false by default.
- */
-SearchBox.prototype.next = function(focus) {
- if (this.results != undefined) {
- var index = (this.resultIndex != undefined) ? this.resultIndex + 1 : 0;
- if (index > this.results.length - 1) {
- index = 0;
- }
- this._setActiveResult(index, focus);
+ var searchPrevious = document.createElement('button');
+ searchPrevious.title = 'Previous result (Shift+Enter)';
+ searchPrevious.className = 'previous';
+ searchPrevious.onclick = function () {
+ searchBox.previous();
+ };
+ td = document.createElement('td');
+ td.appendChild(searchPrevious);
+ tr.appendChild(td);
}
-};
-/**
- * Go to the prevous search result
- * @param {boolean} [focus] If true, focus will be set to the next result
- * focus is false by default.
- */
-SearchBox.prototype.previous = function(focus) {
- if (this.results != undefined) {
- var max = this.results.length - 1;
- var index = (this.resultIndex != undefined) ? this.resultIndex - 1 : max;
- if (index < 0) {
- index = max;
+ /**
+ * Go to the next search result
+ * @param {boolean} [focus] If true, focus will be set to the next result
+ * focus is false by default.
+ */
+ SearchBox.prototype.next = function(focus) {
+ if (this.results != undefined) {
+ var index = (this.resultIndex != undefined) ? this.resultIndex + 1 : 0;
+ if (index > this.results.length - 1) {
+ index = 0;
+ }
+ this._setActiveResult(index, focus);
}
- this._setActiveResult(index, focus);
- }
-};
+ };
-/**
- * Set new value for the current active result
- * @param {Number} index
- * @param {boolean} [focus] If true, focus will be set to the next result.
- * focus is false by default.
- * @private
- */
-SearchBox.prototype._setActiveResult = function(index, focus) {
- // de-activate current active result
- if (this.activeResult) {
- var prevNode = this.activeResult.node;
- var prevElem = this.activeResult.elem;
- if (prevElem == 'field') {
- delete prevNode.searchFieldActive;
+ /**
+ * Go to the prevous search result
+ * @param {boolean} [focus] If true, focus will be set to the next result
+ * focus is false by default.
+ */
+ SearchBox.prototype.previous = function(focus) {
+ if (this.results != undefined) {
+ var max = this.results.length - 1;
+ var index = (this.resultIndex != undefined) ? this.resultIndex - 1 : max;
+ if (index < 0) {
+ index = max;
+ }
+ this._setActiveResult(index, focus);
+ }
+ };
+
+ /**
+ * Set new value for the current active result
+ * @param {Number} index
+ * @param {boolean} [focus] If true, focus will be set to the next result.
+ * focus is false by default.
+ * @private
+ */
+ SearchBox.prototype._setActiveResult = function(index, focus) {
+ // de-activate current active result
+ if (this.activeResult) {
+ var prevNode = this.activeResult.node;
+ var prevElem = this.activeResult.elem;
+ if (prevElem == 'field') {
+ delete prevNode.searchFieldActive;
+ }
+ else {
+ delete prevNode.searchValueActive;
+ }
+ prevNode.updateDom();
+ }
+
+ if (!this.results || !this.results[index]) {
+ // out of range, set to undefined
+ this.resultIndex = undefined;
+ this.activeResult = undefined;
+ return;
+ }
+
+ this.resultIndex = index;
+
+ // set new node active
+ var node = this.results[this.resultIndex].node;
+ var elem = this.results[this.resultIndex].elem;
+ if (elem == 'field') {
+ node.searchFieldActive = true;
}
else {
- delete prevNode.searchValueActive;
+ node.searchValueActive = true;
}
- prevNode.updateDom();
- }
+ this.activeResult = this.results[this.resultIndex];
+ node.updateDom();
- if (!this.results || !this.results[index]) {
- // out of range, set to undefined
- this.resultIndex = undefined;
- this.activeResult = undefined;
- return;
- }
+ // TODO: not so nice that the focus is only set after the animation is finished
+ node.scrollTo(function () {
+ if (focus) {
+ node.focus(elem);
+ }
+ });
+ };
- this.resultIndex = index;
-
- // set new node active
- var node = this.results[this.resultIndex].node;
- var elem = this.results[this.resultIndex].elem;
- if (elem == 'field') {
- node.searchFieldActive = true;
- }
- else {
- node.searchValueActive = true;
- }
- this.activeResult = this.results[this.resultIndex];
- node.updateDom();
-
- // TODO: not so nice that the focus is only set after the animation is finished
- node.scrollTo(function () {
- if (focus) {
- node.focus(elem);
+ /**
+ * Cancel any running onDelayedSearch.
+ * @private
+ */
+ SearchBox.prototype._clearDelay = function() {
+ if (this.timeout != undefined) {
+ clearTimeout(this.timeout);
+ delete this.timeout;
}
- });
-};
+ };
-/**
- * Cancel any running onDelayedSearch.
- * @private
- */
-SearchBox.prototype._clearDelay = function() {
- if (this.timeout != undefined) {
- clearTimeout(this.timeout);
- delete this.timeout;
- }
-};
+ /**
+ * Start a timer to execute a search after a short delay.
+ * Used for reducing the number of searches while typing.
+ * @param {Event} event
+ * @private
+ */
+ SearchBox.prototype._onDelayedSearch = function (event) {
+ // execute the search after a short delay (reduces the number of
+ // search actions while typing in the search text box)
+ this._clearDelay();
+ var searchBox = this;
+ this.timeout = setTimeout(function (event) {
+ searchBox._onSearch(event);
+ },
+ this.delay);
+ };
-/**
- * Start a timer to execute a search after a short delay.
- * Used for reducing the number of searches while typing.
- * @param {Event} event
- * @private
- */
-SearchBox.prototype._onDelayedSearch = function (event) {
- // execute the search after a short delay (reduces the number of
- // search actions while typing in the search text box)
- this._clearDelay();
- var searchBox = this;
- this.timeout = setTimeout(function (event) {
- searchBox._onSearch(event);
- },
- this.delay);
-};
+ /**
+ * Handle onSearch event
+ * @param {Event} event
+ * @param {boolean} [forceSearch] If true, search will be executed again even
+ * when the search text is not changed.
+ * Default is false.
+ * @private
+ */
+ SearchBox.prototype._onSearch = function (event, forceSearch) {
+ this._clearDelay();
-/**
- * Handle onSearch event
- * @param {Event} event
- * @param {boolean} [forceSearch] If true, search will be executed again even
- * when the search text is not changed.
- * Default is false.
- * @private
- */
-SearchBox.prototype._onSearch = function (event, forceSearch) {
- this._clearDelay();
+ var value = this.dom.search.value;
+ var text = (value.length > 0) ? value : undefined;
+ if (text != this.lastText || forceSearch) {
+ // only search again when changed
+ this.lastText = text;
+ this.results = this.editor.search(text);
+ this._setActiveResult(undefined);
- var value = this.dom.search.value;
- var text = (value.length > 0) ? value : undefined;
- if (text != this.lastText || forceSearch) {
- // only search again when changed
- this.lastText = text;
- this.results = this.editor.search(text);
- this._setActiveResult(undefined);
-
- // display search results
- if (text != undefined) {
- var resultCount = this.results.length;
- switch (resultCount) {
- case 0: this.dom.results.innerHTML = 'no results'; break;
- case 1: this.dom.results.innerHTML = '1 result'; break;
- default: this.dom.results.innerHTML = resultCount + ' results'; break;
+ // display search results
+ if (text != undefined) {
+ var resultCount = this.results.length;
+ switch (resultCount) {
+ case 0: this.dom.results.innerHTML = 'no results'; break;
+ case 1: this.dom.results.innerHTML = '1 result'; break;
+ default: this.dom.results.innerHTML = resultCount + ' results'; break;
+ }
+ }
+ else {
+ this.dom.results.innerHTML = '';
}
}
- else {
- this.dom.results.innerHTML = '';
- }
- }
-};
+ };
-/**
- * Handle onKeyDown event in the input box
- * @param {Event} event
- * @private
- */
-SearchBox.prototype._onKeyDown = function (event) {
- var keynum = event.which;
- if (keynum == 27) { // ESC
- this.dom.search.value = ''; // clear search
- this._onSearch(event);
- event.preventDefault();
- event.stopPropagation();
- }
- else if (keynum == 13) { // Enter
- if (event.ctrlKey) {
- // force to search again
- this._onSearch(event, true);
+ /**
+ * Handle onKeyDown event in the input box
+ * @param {Event} event
+ * @private
+ */
+ SearchBox.prototype._onKeyDown = function (event) {
+ var keynum = event.which;
+ if (keynum == 27) { // ESC
+ this.dom.search.value = ''; // clear search
+ this._onSearch(event);
+ event.preventDefault();
+ event.stopPropagation();
}
- else if (event.shiftKey) {
- // move to the previous search result
- this.previous();
+ else if (keynum == 13) { // Enter
+ if (event.ctrlKey) {
+ // force to search again
+ this._onSearch(event, true);
+ }
+ else if (event.shiftKey) {
+ // move to the previous search result
+ this.previous();
+ }
+ else {
+ // move to the next search result
+ this.next();
+ }
+ event.preventDefault();
+ event.stopPropagation();
}
- else {
- // move to the next search result
- this.next();
- }
- event.preventDefault();
- event.stopPropagation();
- }
-};
+ };
+
+ /**
+ * Handle onKeyUp event in the input box
+ * @param {Event} event
+ * @private
+ */
+ SearchBox.prototype._onKeyUp = function (event) {
+ var keynum = event.keyCode;
+ if (keynum != 27 && keynum != 13) { // !show and !Enter
+ this._onDelayedSearch(event); // For IE 9
+ }
+ };
+
+ return SearchBox;
+});
+
-/**
- * Handle onKeyUp event in the input box
- * @param {Event} event
- * @private
- */
-SearchBox.prototype._onKeyUp = function (event) {
- var keynum = event.keyCode;
- if (keynum != 27 && keynum != 13) { // !show and !Enter
- this._onDelayedSearch(event); // For IE 9
- }
-};
diff --git a/src/js/TextEditor.js b/src/js/TextEditor.js
index 68dfe43..3c1e82b 100644
--- a/src/js/TextEditor.js
+++ b/src/js/TextEditor.js
@@ -1,305 +1,312 @@
-/**
- * Create a TextEditor and attach it to given container
- * @constructor TextEditor
- * @param {Element} container
- * @param {Object} [options] Object with options. available options:
- * {String} mode Available values:
- * "text" (default)
- * or "code".
- * {Number} indentation Number of indentation
- * spaces. 2 by default.
- * {function} change Callback method
- * triggered on change
- * @param {JSON | String} [json] initial contents of the formatter
- */
-function TextEditor(container, options, json) {
- if (!(this instanceof TextEditor)) {
- throw new Error('TextEditor constructor called without "new".');
+define(['./modebox', './util'], function (modebox, util) {
+
+ /**
+ * Create a TextEditor and attach it to given container
+ * @constructor TextEditor
+ * @param {Element} container
+ * @param {Object} [options] Object with options. available options:
+ * {String} mode Available values:
+ * "text" (default)
+ * or "code".
+ * {Number} indentation Number of indentation
+ * spaces. 2 by default.
+ * {function} change Callback method
+ * triggered on change
+ * @param {JSON | String} [json] initial contents of the formatter
+ */
+ function TextEditor(container, options, json) {
+ if (!(this instanceof TextEditor)) {
+ throw new Error('TextEditor constructor called without "new".');
+ }
+
+ this._create(container, options, json);
}
- this._create(container, options, json);
-}
-
-/**
- * Create a TextEditor and attach it to given container
- * @constructor TextEditor
- * @param {Element} container
- * @param {Object} [options] See description in constructor
- * @param {JSON | String} [json] initial contents of the formatter
- * @private
- */
-TextEditor.prototype._create = function (container, options, json) {
- // read options
- options = options || {};
- this.options = options;
- if (options.indentation) {
- this.indentation = Number(options.indentation);
- }
- else {
- this.indentation = 2; // number of spaces
- }
- this.mode = (options.mode == 'code') ? 'code' : 'text';
- if (this.mode == 'code') {
- // verify whether Ace editor is available and supported
- if (typeof ace === 'undefined') {
- this.mode = 'text';
- util.log('WARNING: Cannot load code editor, Ace library not loaded. ' +
- 'Falling back to plain text editor');
+ /**
+ * Create a TextEditor and attach it to given container
+ * @constructor TextEditor
+ * @param {Element} container
+ * @param {Object} [options] See description in constructor
+ * @param {JSON | String} [json] initial contents of the formatter
+ * @private
+ */
+ TextEditor.prototype._create = function (container, options, json) {
+ // read options
+ options = options || {};
+ this.options = options;
+ if (options.indentation) {
+ this.indentation = Number(options.indentation);
}
- }
-
- var me = this;
- this.container = container;
- this.dom = {};
- this.editor = undefined; // ace code editor
- this.textarea = undefined; // plain text editor (fallback when Ace is not available)
-
- this.width = container.clientWidth;
- this.height = container.clientHeight;
-
- this.frame = document.createElement('div');
- this.frame.className = 'jsoneditor';
- this.frame.onclick = function (event) {
- // prevent default submit action when TextEditor is located inside a form
- event.preventDefault();
- };
-
- // create menu
- this.menu = document.createElement('div');
- this.menu.className = 'menu';
- this.frame.appendChild(this.menu);
-
- // create format button
- var buttonFormat = document.createElement('button');
- buttonFormat.className = 'format';
- buttonFormat.title = 'Format JSON data, with proper indentation and line feeds';
- this.menu.appendChild(buttonFormat);
- buttonFormat.onclick = function () {
- try {
- me.format();
+ else {
+ this.indentation = 2; // number of spaces
}
- catch (err) {
- me._onError(err);
+ this.mode = (options.mode == 'code') ? 'code' : 'text';
+ if (this.mode == 'code') {
+ // verify whether Ace editor is available and supported
+ if (typeof ace === 'undefined') {
+ this.mode = 'text';
+ util.log('WARNING: Cannot load code editor, Ace library not loaded. ' +
+ 'Falling back to plain text editor');
+ }
}
- };
- // create compact button
- var buttonCompact = document.createElement('button');
- buttonCompact.className = 'compact';
- buttonCompact.title = 'Compact JSON data, remove all whitespaces';
- this.menu.appendChild(buttonCompact);
- buttonCompact.onclick = function () {
- try {
- me.compact();
- }
- catch (err) {
- me._onError(err);
- }
- };
+ var me = this;
+ this.container = container;
+ this.dom = {};
+ this.editor = undefined; // ace code editor
+ this.textarea = undefined; // plain text editor (fallback when Ace is not available)
- // create mode box
- if (this.options && this.options.modes && this.options.modes.length) {
- var modeBox = createModeBox(this, this.options.modes, this.options.mode);
- this.menu.appendChild(modeBox);
- this.dom.modeBox = modeBox;
- }
+ this.width = container.clientWidth;
+ this.height = container.clientHeight;
- this.content = document.createElement('div');
- this.content.className = 'outer';
- this.frame.appendChild(this.content);
-
- this.container.appendChild(this.frame);
-
- if (this.mode == 'code') {
- this.editorDom = document.createElement('div');
- this.editorDom.style.height = '100%'; // TODO: move to css
- this.editorDom.style.width = '100%'; // TODO: move to css
- this.content.appendChild(this.editorDom);
-
- var editor = ace.edit(this.editorDom);
- editor.setTheme('ace/theme/jsoneditor');
- editor.setShowPrintMargin(false);
- editor.setFontSize(13);
- editor.getSession().setMode('ace/mode/json');
- editor.getSession().setTabSize(2);
- editor.getSession().setUseSoftTabs(true);
- editor.getSession().setUseWrapMode(true);
- this.editor = editor;
-
- var poweredBy = document.createElement('a');
- poweredBy.appendChild(document.createTextNode('powered by ace'));
- poweredBy.href = 'http://ace.ajax.org';
- poweredBy.target = '_blank';
- poweredBy.className = 'poweredBy';
- poweredBy.onclick = function () {
- // TODO: this anchor falls below the margin of the content,
- // therefore the normal a.href does not work. We use a click event
- // for now, but this should be fixed.
- window.open(poweredBy.href, poweredBy.target);
+ this.frame = document.createElement('div');
+ this.frame.className = 'jsoneditor';
+ this.frame.onclick = function (event) {
+ // prevent default submit action when TextEditor is located inside a form
+ event.preventDefault();
};
- this.menu.appendChild(poweredBy);
- if (options.change) {
- // register onchange event
- editor.on('change', function () {
- options.change();
- });
- }
- }
- else {
- // load a plain text textarea
- var textarea = document.createElement('textarea');
- textarea.className = 'text';
- textarea.spellcheck = false;
- this.content.appendChild(textarea);
- this.textarea = textarea;
+ // create menu
+ this.menu = document.createElement('div');
+ this.menu.className = 'menu';
+ this.frame.appendChild(this.menu);
- if (options.change) {
- // register onchange event
- if (this.textarea.oninput === null) {
- this.textarea.oninput = function () {
- options.change();
- }
+ // create format button
+ var buttonFormat = document.createElement('button');
+ buttonFormat.className = 'format';
+ buttonFormat.title = 'Format JSON data, with proper indentation and line feeds';
+ this.menu.appendChild(buttonFormat);
+ buttonFormat.onclick = function () {
+ try {
+ me.format();
}
- else {
- // oninput is undefined. For IE8-
- this.textarea.onchange = function () {
+ catch (err) {
+ me._onError(err);
+ }
+ };
+
+ // create compact button
+ var buttonCompact = document.createElement('button');
+ buttonCompact.className = 'compact';
+ buttonCompact.title = 'Compact JSON data, remove all whitespaces';
+ this.menu.appendChild(buttonCompact);
+ buttonCompact.onclick = function () {
+ try {
+ me.compact();
+ }
+ catch (err) {
+ me._onError(err);
+ }
+ };
+
+ // create mode box
+ if (this.options && this.options.modes && this.options.modes.length) {
+ var modeBox = modebox.create(this, this.options.modes, this.options.mode);
+ this.menu.appendChild(modeBox);
+ this.dom.modeBox = modeBox;
+ }
+
+ this.content = document.createElement('div');
+ this.content.className = 'outer';
+ this.frame.appendChild(this.content);
+
+ this.container.appendChild(this.frame);
+
+ if (this.mode == 'code') {
+ this.editorDom = document.createElement('div');
+ this.editorDom.style.height = '100%'; // TODO: move to css
+ this.editorDom.style.width = '100%'; // TODO: move to css
+ this.content.appendChild(this.editorDom);
+
+ var editor = ace.edit(this.editorDom);
+ editor.setTheme('ace/theme/jsoneditor');
+ editor.setShowPrintMargin(false);
+ editor.setFontSize(13);
+ editor.getSession().setMode('ace/mode/json');
+ editor.getSession().setTabSize(2);
+ editor.getSession().setUseSoftTabs(true);
+ editor.getSession().setUseWrapMode(true);
+ this.editor = editor;
+
+ var poweredBy = document.createElement('a');
+ poweredBy.appendChild(document.createTextNode('powered by ace'));
+ poweredBy.href = 'http://ace.ajax.org';
+ poweredBy.target = '_blank';
+ poweredBy.className = 'poweredBy';
+ poweredBy.onclick = function () {
+ // TODO: this anchor falls below the margin of the content,
+ // therefore the normal a.href does not work. We use a click event
+ // for now, but this should be fixed.
+ window.open(poweredBy.href, poweredBy.target);
+ };
+ this.menu.appendChild(poweredBy);
+
+ if (options.change) {
+ // register onchange event
+ editor.on('change', function () {
options.change();
+ });
+ }
+ }
+ else {
+ // load a plain text textarea
+ var textarea = document.createElement('textarea');
+ textarea.className = 'text';
+ textarea.spellcheck = false;
+ this.content.appendChild(textarea);
+ this.textarea = textarea;
+
+ if (options.change) {
+ // register onchange event
+ if (this.textarea.oninput === null) {
+ this.textarea.oninput = function () {
+ options.change();
+ }
+ }
+ else {
+ // oninput is undefined. For IE8-
+ this.textarea.onchange = function () {
+ options.change();
+ }
}
}
}
- }
- // load initial json object or string
- if (typeof(json) == 'string') {
- this.setText(json);
- }
- else {
- this.set(json);
- }
-};
+ // load initial json object or string
+ if (typeof(json) == 'string') {
+ this.setText(json);
+ }
+ else {
+ this.set(json);
+ }
+ };
-/**
- * Detach the editor from the DOM
- * @private
- */
-TextEditor.prototype._delete = function () {
- if (this.frame && this.container && this.frame.parentNode == this.container) {
- this.container.removeChild(this.frame);
- }
-};
+ /**
+ * Detach the editor from the DOM
+ * @private
+ */
+ TextEditor.prototype._delete = function () {
+ if (this.frame && this.container && this.frame.parentNode == this.container) {
+ this.container.removeChild(this.frame);
+ }
+ };
-/**
- * Throw an error. If an error callback is configured in options.error, this
- * callback will be invoked. Else, a regular error is thrown.
- * @param {Error} err
- * @private
- */
-TextEditor.prototype._onError = function(err) {
- // TODO: onError is deprecated since version 2.2.0. cleanup some day
- if (typeof this.onError === 'function') {
- util.log('WARNING: JSONEditor.onError is deprecated. ' +
- 'Use options.error instead.');
- this.onError(err);
- }
+ /**
+ * Throw an error. If an error callback is configured in options.error, this
+ * callback will be invoked. Else, a regular error is thrown.
+ * @param {Error} err
+ * @private
+ */
+ TextEditor.prototype._onError = function(err) {
+ // TODO: onError is deprecated since version 2.2.0. cleanup some day
+ if (typeof this.onError === 'function') {
+ util.log('WARNING: JSONEditor.onError is deprecated. ' +
+ 'Use options.error instead.');
+ this.onError(err);
+ }
- if (this.options && typeof this.options.error === 'function') {
- this.options.error(err);
- }
- else {
- throw err;
- }
-};
+ if (this.options && typeof this.options.error === 'function') {
+ this.options.error(err);
+ }
+ else {
+ throw err;
+ }
+ };
-/**
- * Compact the code in the formatter
- */
-TextEditor.prototype.compact = function () {
- var json = util.parse(this.getText());
- this.setText(JSON.stringify(json));
-};
+ /**
+ * Compact the code in the formatter
+ */
+ TextEditor.prototype.compact = function () {
+ var json = util.parse(this.getText());
+ this.setText(JSON.stringify(json));
+ };
-/**
- * Format the code in the formatter
- */
-TextEditor.prototype.format = function () {
- var json = util.parse(this.getText());
- this.setText(JSON.stringify(json, null, this.indentation));
-};
+ /**
+ * Format the code in the formatter
+ */
+ TextEditor.prototype.format = function () {
+ var json = util.parse(this.getText());
+ this.setText(JSON.stringify(json, null, this.indentation));
+ };
-/**
- * Set focus to the formatter
- */
-TextEditor.prototype.focus = function () {
- if (this.textarea) {
- this.textarea.focus();
- }
- if (this.editor) {
- this.editor.focus();
- }
-};
+ /**
+ * Set focus to the formatter
+ */
+ TextEditor.prototype.focus = function () {
+ if (this.textarea) {
+ this.textarea.focus();
+ }
+ if (this.editor) {
+ this.editor.focus();
+ }
+ };
-/**
- * Resize the formatter
- */
-TextEditor.prototype.resize = function () {
- if (this.editor) {
- var force = false;
- this.editor.resize(force);
- }
-};
+ /**
+ * Resize the formatter
+ */
+ TextEditor.prototype.resize = function () {
+ if (this.editor) {
+ var force = false;
+ this.editor.resize(force);
+ }
+ };
-/**
- * Set json data in the formatter
- * @param {Object} json
- */
-TextEditor.prototype.set = function(json) {
- this.setText(JSON.stringify(json, null, this.indentation));
-};
+ /**
+ * Set json data in the formatter
+ * @param {Object} json
+ */
+ TextEditor.prototype.set = function(json) {
+ this.setText(JSON.stringify(json, null, this.indentation));
+ };
-/**
- * Get json data from the formatter
- * @return {Object} json
- */
-TextEditor.prototype.get = function() {
- return util.parse(this.getText());
-};
+ /**
+ * Get json data from the formatter
+ * @return {Object} json
+ */
+ TextEditor.prototype.get = function() {
+ return util.parse(this.getText());
+ };
-/**
- * Get the text contents of the TextEditor
- * @return {String} jsonText
- */
-TextEditor.prototype.getText = function() {
- if (this.textarea) {
- return this.textarea.value;
- }
- if (this.editor) {
- return this.editor.getValue();
- }
- return '';
-};
+ /**
+ * Get the text contents of the TextEditor
+ * @return {String} jsonText
+ */
+ TextEditor.prototype.getText = function() {
+ if (this.textarea) {
+ return this.textarea.value;
+ }
+ if (this.editor) {
+ return this.editor.getValue();
+ }
+ return '';
+ };
-/**
- * Set the text contents of the TextEditor
- * @param {String} jsonText
- */
-TextEditor.prototype.setText = function(jsonText) {
- if (this.textarea) {
- this.textarea.value = jsonText;
- }
- if (this.editor) {
- this.editor.setValue(jsonText, -1);
- }
-};
+ /**
+ * Set the text contents of the TextEditor
+ * @param {String} jsonText
+ */
+ TextEditor.prototype.setText = function(jsonText) {
+ if (this.textarea) {
+ this.textarea.value = jsonText;
+ }
+ if (this.editor) {
+ this.editor.setValue(jsonText, -1);
+ }
+ };
-// register modes at the JSONEditor
-JSONEditor.modes.text = {
- editor: TextEditor,
- data: 'text',
- load: TextEditor.prototype.format
-};
-JSONEditor.modes.code = {
- editor: TextEditor,
- data: 'text',
- load: TextEditor.prototype.format
-};
+ // define modes
+ TextEditor.modes = {
+ text: {
+ editor: TextEditor,
+ data: 'text',
+ load: TextEditor.prototype.format
+ },
+ code: {
+ editor: TextEditor,
+ data: 'text',
+ load: TextEditor.prototype.format
+ }
+ };
+
+ return TextEditor;
+});
diff --git a/src/js/TreeEditor.js b/src/js/TreeEditor.js
index a407278..933d4e3 100644
--- a/src/js/TreeEditor.js
+++ b/src/js/TreeEditor.js
@@ -1,754 +1,731 @@
-/**
- * @constructor TreeEditor
- * @param {Element} container Container element
- * @param {Object} [options] Object with options. available options:
- * {String} mode Editor mode. Available values:
- * 'tree' (default), 'view',
- * and 'form'.
- * {Boolean} search Enable search box.
- * True by default
- * {Boolean} history Enable history (undo/redo).
- * True by default
- * {function} change Callback method, triggered
- * on change of contents
- * {String} name Field name for the root node.
- * @param {Object | undefined} json JSON object
- */
-function TreeEditor(container, options, json) {
- if (!(this instanceof TreeEditor)) {
- throw new Error('TreeEditor constructor called without "new".');
+define(['./Highlighter', './History', './SearchBox', './Node', './modebox', './util'],
+ function (Highlighter, History, SearchBox, Node, modebox, util) {
+
+ /**
+ * @constructor TreeEditor
+ * @param {Element} container Container element
+ * @param {Object} [options] Object with options. available options:
+ * {String} mode Editor mode. Available values:
+ * 'tree' (default), 'view',
+ * and 'form'.
+ * {Boolean} search Enable search box.
+ * True by default
+ * {Boolean} history Enable history (undo/redo).
+ * True by default
+ * {function} change Callback method, triggered
+ * on change of contents
+ * {String} name Field name for the root node.
+ * @param {Object | undefined} json JSON object
+ */
+ function TreeEditor(container, options, json) {
+ if (!(this instanceof TreeEditor)) {
+ throw new Error('TreeEditor constructor called without "new".');
+ }
+
+ this._create(container, options, json);
}
- this._create(container, options, json);
-}
+ /**
+ * Create the TreeEditor
+ * @param {Element} container Container element
+ * @param {Object} [options] See description in constructor
+ * @param {Object | undefined} json JSON object
+ * @private
+ */
+ TreeEditor.prototype._create = function (container, options, json) {
+ if (!container) {
+ throw new Error('No container element provided.');
+ }
+ this.container = container;
+ this.dom = {};
+ this.highlighter = new Highlighter();
+ this.selection = undefined; // will hold the last input selection
-/**
- * Create the TreeEditor
- * @param {Element} container Container element
- * @param {Object} [options] See description in constructor
- * @param {Object | undefined} json JSON object
- * @private
- */
-TreeEditor.prototype._create = function (container, options, json) {
- if (!container) {
- throw new Error('No container element provided.');
- }
- this.container = container;
- this.dom = {};
- this.highlighter = new Highlighter();
- this.selection = undefined; // will hold the last input selection
+ this._setOptions(options);
- this._setOptions(options);
+ if (this.options.history && !this.mode.view) {
+ this.history = new History(this);
+ }
- if (this.options.history && !this.mode.view) {
- this.history = new History(this);
- }
+ this._createFrame();
+ this._createTable();
- this._createFrame();
- this._createTable();
-
- this.set(json || {});
-};
-
-/**
- * Detach the editor from the DOM
- * @private
- */
-TreeEditor.prototype._delete = function () {
- if (this.frame && this.container && this.frame.parentNode == this.container) {
- this.container.removeChild(this.frame);
- }
-};
-
-/**
- * Initialize and set default options
- * @param {Object} [options] See description in constructor
- * @private
- */
-TreeEditor.prototype._setOptions = function (options) {
- this.options = {
- search: true,
- history: true,
- mode: 'tree',
- name: undefined // field name of root node
+ this.set(json || {});
};
- // copy all options
- if (options) {
- for (var prop in options) {
- if (options.hasOwnProperty(prop)) {
- this.options[prop] = options[prop];
+ /**
+ * Detach the editor from the DOM
+ * @private
+ */
+ TreeEditor.prototype._delete = function () {
+ if (this.frame && this.container && this.frame.parentNode == this.container) {
+ this.container.removeChild(this.frame);
+ }
+ };
+
+ /**
+ * Initialize and set default options
+ * @param {Object} [options] See description in constructor
+ * @private
+ */
+ TreeEditor.prototype._setOptions = function (options) {
+ this.options = {
+ search: true,
+ history: true,
+ mode: 'tree',
+ name: undefined // field name of root node
+ };
+
+ // copy all options
+ if (options) {
+ for (var prop in options) {
+ if (options.hasOwnProperty(prop)) {
+ this.options[prop] = options[prop];
+ }
}
}
- // check for deprecated options
- if (options['enableSearch']) {
- // deprecated since version 1.6.0, 2012-11-03
- this.options.search = options['enableSearch'];
- util.log('WARNING: Option "enableSearch" is deprecated. Use "search" instead.');
- }
- if (options['enableHistory']) {
- // deprecated since version 1.6.0, 2012-11-03
- this.options.history = options['enableHistory'];
- util.log('WARNING: Option "enableHistory" is deprecated. Use "history" instead.');
- }
- if (options['mode'] == 'editor') {
- // deprecated since version 2.2.0, 2013-04-30
- this.options.mode = 'tree';
- util.log('WARNING: Mode "editor" is deprecated. Use "tree" instead.');
- }
- if (options['mode'] == 'viewer') {
- // deprecated since version 2.2.0, 2013-04-30
- this.options.mode = 'view';
- util.log('WARNING: Mode "viewer" is deprecated. Use "view" instead.');
- }
- }
-
- // interpret the mode options
- this.mode = {
- edit: (this.options.mode != 'view' && this.options.mode != 'form'),
- view: (this.options.mode == 'view'),
- form: (this.options.mode == 'form')
+ // interpret the mode options
+ this.mode = {
+ edit: (this.options.mode != 'view' && this.options.mode != 'form'),
+ view: (this.options.mode == 'view'),
+ form: (this.options.mode == 'form')
+ };
};
-};
// node currently being edited
-TreeEditor.focusNode = undefined;
+ TreeEditor.focusNode = undefined;
-/**
- * Set JSON object in editor
- * @param {Object | undefined} json JSON data
- * @param {String} [name] Optional field name for the root node.
- * Can also be set using setName(name).
- */
-TreeEditor.prototype.set = function (json, name) {
- // adjust field name for root node
- if (name) {
- // TODO: deprecated since version 2.2.0. Cleanup some day.
- util.log('Warning: second parameter "name" is deprecated. ' +
- 'Use setName(name) instead.');
- this.options.name = name;
- }
-
- // verify if json is valid JSON, ignore when a function
- if (json instanceof Function || (json === undefined)) {
- this.clear();
- }
- else {
- this.content.removeChild(this.table); // Take the table offline
-
- // replace the root node
- var params = {
- 'field': this.options.name,
- 'value': json
- };
- var node = new Node(this, params);
- this._setRoot(node);
-
- // expand
- var recurse = false;
- this.node.expand(recurse);
-
- this.content.appendChild(this.table); // Put the table online again
- }
-
- // TODO: maintain history, store last state and previous document
- if (this.history) {
- this.history.clear();
- }
-};
-
-/**
- * Get JSON object from editor
- * @return {Object | undefined} json
- */
-TreeEditor.prototype.get = function () {
- // remove focus from currently edited node
- if (TreeEditor.focusNode) {
- TreeEditor.focusNode.blur();
- }
-
- if (this.node) {
- return this.node.getValue();
- }
- else {
- return undefined;
- }
-};
-
-/**
- * Get the text contents of the TreeEditor
- * @return {String} jsonText
- */
-TreeEditor.prototype.getText = function() {
- return JSON.stringify(this.get());
-};
-
-/**
- * Set the text contents of the TreeEditor
- * @param {String} jsonText
- */
-TreeEditor.prototype.setText = function(jsonText) {
- this.set(util.parse(jsonText));
-};
-
-/**
- * Set a field name for the root node.
- * @param {String | undefined} name
- */
-TreeEditor.prototype.setName = function (name) {
- this.options.name = name;
- if (this.node) {
- this.node.updateField(this.options.name);
- }
-};
-
-/**
- * Get the field name for the root node.
- * @return {String | undefined} name
- */
-TreeEditor.prototype.getName = function () {
- return this.options.name;
-};
-
-/**
- * Remove the root node from the editor
- */
-TreeEditor.prototype.clear = function () {
- if (this.node) {
- this.node.collapse();
- this.tbody.removeChild(this.node.getDom());
- delete this.node;
- }
-};
-
-/**
- * Set the root node for the json editor
- * @param {Node} node
- * @private
- */
-TreeEditor.prototype._setRoot = function (node) {
- this.clear();
-
- this.node = node;
-
- // append to the dom
- this.tbody.appendChild(node.getDom());
-};
-
-/**
- * Search text in all nodes
- * The nodes will be expanded when the text is found one of its childs,
- * else it will be collapsed. Searches are case insensitive.
- * @param {String} text
- * @return {Object[]} results Array with nodes containing the search results
- * The result objects contains fields:
- * - {Node} node,
- * - {String} elem the dom element name where
- * the result is found ('field' or
- * 'value')
- */
-TreeEditor.prototype.search = function (text) {
- var results;
- if (this.node) {
- this.content.removeChild(this.table); // Take the table offline
- results = this.node.search(text);
- this.content.appendChild(this.table); // Put the table online again
- }
- else {
- results = [];
- }
-
- return results;
-};
-
-/**
- * Expand all nodes
- */
-TreeEditor.prototype.expandAll = function () {
- if (this.node) {
- this.content.removeChild(this.table); // Take the table offline
- this.node.expand();
- this.content.appendChild(this.table); // Put the table online again
- }
-};
-
-/**
- * Collapse all nodes
- */
-TreeEditor.prototype.collapseAll = function () {
- if (this.node) {
- this.content.removeChild(this.table); // Take the table offline
- this.node.collapse();
- this.content.appendChild(this.table); // Put the table online again
- }
-};
-
-/**
- * The method onChange is called whenever a field or value is changed, created,
- * deleted, duplicated, etc.
- * @param {String} action Change action. Available values: "editField",
- * "editValue", "changeType", "appendNode",
- * "removeNode", "duplicateNode", "moveNode", "expand",
- * "collapse".
- * @param {Object} params Object containing parameters describing the change.
- * The parameters in params depend on the action (for
- * example for "editValue" the Node, old value, and new
- * value are provided). params contains all information
- * needed to undo or redo the action.
- * @private
- */
-TreeEditor.prototype._onAction = function (action, params) {
- // add an action to the history
- if (this.history) {
- this.history.add(action, params);
- }
-
- // trigger the onChange callback
- if (this.options.change) {
- try {
- this.options.change();
+ /**
+ * Set JSON object in editor
+ * @param {Object | undefined} json JSON data
+ * @param {String} [name] Optional field name for the root node.
+ * Can also be set using setName(name).
+ */
+ TreeEditor.prototype.set = function (json, name) {
+ // adjust field name for root node
+ if (name) {
+ // TODO: deprecated since version 2.2.0. Cleanup some day.
+ util.log('Warning: second parameter "name" is deprecated. ' +
+ 'Use setName(name) instead.');
+ this.options.name = name;
}
- catch (err) {
- util.log('Error in change callback: ', err);
+
+ // verify if json is valid JSON, ignore when a function
+ if (json instanceof Function || (json === undefined)) {
+ this.clear();
}
- }
-};
+ else {
+ this.content.removeChild(this.table); // Take the table offline
-/**
- * Start autoscrolling when given mouse position is above the top of the
- * editor contents, or below the bottom.
- * @param {Number} mouseY Absolute mouse position in pixels
- */
-TreeEditor.prototype.startAutoScroll = function (mouseY) {
- var me = this;
- var content = this.content;
- var top = util.getAbsoluteTop(content);
- var height = content.clientHeight;
- var bottom = top + height;
- var margin = 24;
- var interval = 50; // ms
+ // replace the root node
+ var params = {
+ 'field': this.options.name,
+ 'value': json
+ };
+ var node = new Node(this, params);
+ this._setRoot(node);
- if ((mouseY < top + margin) && content.scrollTop > 0) {
- this.autoScrollStep = ((top + margin) - mouseY) / 3;
- }
- else if (mouseY > bottom - margin &&
- height + content.scrollTop < content.scrollHeight) {
- this.autoScrollStep = ((bottom - margin) - mouseY) / 3;
- }
- else {
- this.autoScrollStep = undefined;
- }
+ // expand
+ var recurse = false;
+ this.node.expand(recurse);
- if (this.autoScrollStep) {
- if (!this.autoScrollTimer) {
- this.autoScrollTimer = setInterval(function () {
- if (me.autoScrollStep) {
- content.scrollTop -= me.autoScrollStep;
- }
- else {
- me.stopAutoScroll();
- }
- }, interval);
+ this.content.appendChild(this.table); // Put the table online again
}
- }
- else {
- this.stopAutoScroll();
- }
-};
-/**
- * Stop auto scrolling. Only applicable when scrolling
- */
-TreeEditor.prototype.stopAutoScroll = function () {
- if (this.autoScrollTimer) {
- clearTimeout(this.autoScrollTimer);
- delete this.autoScrollTimer;
- }
- if (this.autoScrollStep) {
- delete this.autoScrollStep;
- }
-};
-
-
-/**
- * Set the focus to an element in the TreeEditor, set text selection, and
- * set scroll position.
- * @param {Object} selection An object containing fields:
- * {Element | undefined} dom The dom element
- * which has focus
- * {Range | TextRange} range A text selection
- * {Number} scrollTop Scroll position
- */
-TreeEditor.prototype.setSelection = function (selection) {
- if (!selection) {
- return;
- }
-
- if ('scrollTop' in selection && this.content) {
- // TODO: animated scroll
- this.content.scrollTop = selection.scrollTop;
- }
- if (selection.range) {
- util.setSelectionOffset(selection.range);
- }
- if (selection.dom) {
- selection.dom.focus();
- }
-};
-
-/**
- * Get the current focus
- * @return {Object} selection An object containing fields:
- * {Element | undefined} dom The dom element
- * which has focus
- * {Range | TextRange} range A text selection
- * {Number} scrollTop Scroll position
- */
-TreeEditor.prototype.getSelection = function () {
- return {
- dom: TreeEditor.domFocus,
- scrollTop: this.content ? this.content.scrollTop : 0,
- range: util.getSelectionOffset()
+ // TODO: maintain history, store last state and previous document
+ if (this.history) {
+ this.history.clear();
+ }
};
-};
-/**
- * Adjust the scroll position such that given top position is shown at 1/4
- * of the window height.
- * @param {Number} top
- * @param {function(boolean)} [callback] Callback, executed when animation is
- * finished. The callback returns true
- * when animation is finished, or false
- * when not.
- */
-TreeEditor.prototype.scrollTo = function (top, callback) {
- var content = this.content;
- if (content) {
- var editor = this;
- // cancel any running animation
- if (editor.animateTimeout) {
- clearTimeout(editor.animateTimeout);
- delete editor.animateTimeout;
- }
- if (editor.animateCallback) {
- editor.animateCallback(false);
- delete editor.animateCallback;
+ /**
+ * Get JSON object from editor
+ * @return {Object | undefined} json
+ */
+ TreeEditor.prototype.get = function () {
+ // remove focus from currently edited node
+ if (TreeEditor.focusNode) {
+ TreeEditor.focusNode.blur();
}
- // calculate final scroll position
- var height = content.clientHeight;
- var bottom = content.scrollHeight - height;
- var finalScrollTop = Math.min(Math.max(top - height / 4, 0), bottom);
+ if (this.node) {
+ return this.node.getValue();
+ }
+ else {
+ return undefined;
+ }
+ };
- // animate towards the new scroll position
- var animate = function () {
- var scrollTop = content.scrollTop;
- var diff = (finalScrollTop - scrollTop);
- if (Math.abs(diff) > 3) {
- content.scrollTop += diff / 3;
- editor.animateCallback = callback;
- editor.animateTimeout = setTimeout(animate, 50);
+ /**
+ * Get the text contents of the TreeEditor
+ * @return {String} jsonText
+ */
+ TreeEditor.prototype.getText = function() {
+ return JSON.stringify(this.get());
+ };
+
+ /**
+ * Set the text contents of the TreeEditor
+ * @param {String} jsonText
+ */
+ TreeEditor.prototype.setText = function(jsonText) {
+ this.set(util.parse(jsonText));
+ };
+
+ /**
+ * Set a field name for the root node.
+ * @param {String | undefined} name
+ */
+ TreeEditor.prototype.setName = function (name) {
+ this.options.name = name;
+ if (this.node) {
+ this.node.updateField(this.options.name);
+ }
+ };
+
+ /**
+ * Get the field name for the root node.
+ * @return {String | undefined} name
+ */
+ TreeEditor.prototype.getName = function () {
+ return this.options.name;
+ };
+
+ /**
+ * Remove the root node from the editor
+ */
+ TreeEditor.prototype.clear = function () {
+ if (this.node) {
+ this.node.collapse();
+ this.tbody.removeChild(this.node.getDom());
+ delete this.node;
+ }
+ };
+
+ /**
+ * Set the root node for the json editor
+ * @param {Node} node
+ * @private
+ */
+ TreeEditor.prototype._setRoot = function (node) {
+ this.clear();
+
+ this.node = node;
+
+ // append to the dom
+ this.tbody.appendChild(node.getDom());
+ };
+
+ /**
+ * Search text in all nodes
+ * The nodes will be expanded when the text is found one of its childs,
+ * else it will be collapsed. Searches are case insensitive.
+ * @param {String} text
+ * @return {Object[]} results Array with nodes containing the search results
+ * The result objects contains fields:
+ * - {Node} node,
+ * - {String} elem the dom element name where
+ * the result is found ('field' or
+ * 'value')
+ */
+ TreeEditor.prototype.search = function (text) {
+ var results;
+ if (this.node) {
+ this.content.removeChild(this.table); // Take the table offline
+ results = this.node.search(text);
+ this.content.appendChild(this.table); // Put the table online again
+ }
+ else {
+ results = [];
+ }
+
+ return results;
+ };
+
+ /**
+ * Expand all nodes
+ */
+ TreeEditor.prototype.expandAll = function () {
+ if (this.node) {
+ this.content.removeChild(this.table); // Take the table offline
+ this.node.expand();
+ this.content.appendChild(this.table); // Put the table online again
+ }
+ };
+
+ /**
+ * Collapse all nodes
+ */
+ TreeEditor.prototype.collapseAll = function () {
+ if (this.node) {
+ this.content.removeChild(this.table); // Take the table offline
+ this.node.collapse();
+ this.content.appendChild(this.table); // Put the table online again
+ }
+ };
+
+ /**
+ * The method onChange is called whenever a field or value is changed, created,
+ * deleted, duplicated, etc.
+ * @param {String} action Change action. Available values: "editField",
+ * "editValue", "changeType", "appendNode",
+ * "removeNode", "duplicateNode", "moveNode", "expand",
+ * "collapse".
+ * @param {Object} params Object containing parameters describing the change.
+ * The parameters in params depend on the action (for
+ * example for "editValue" the Node, old value, and new
+ * value are provided). params contains all information
+ * needed to undo or redo the action.
+ * @private
+ */
+ TreeEditor.prototype._onAction = function (action, params) {
+ // add an action to the history
+ if (this.history) {
+ this.history.add(action, params);
+ }
+
+ // trigger the onChange callback
+ if (this.options.change) {
+ try {
+ this.options.change();
}
- else {
- // finished
- if (callback) {
- callback(true);
- }
- content.scrollTop = finalScrollTop;
+ catch (err) {
+ util.log('Error in change callback: ', err);
+ }
+ }
+ };
+
+ /**
+ * Start autoscrolling when given mouse position is above the top of the
+ * editor contents, or below the bottom.
+ * @param {Number} mouseY Absolute mouse position in pixels
+ */
+ TreeEditor.prototype.startAutoScroll = function (mouseY) {
+ var me = this;
+ var content = this.content;
+ var top = util.getAbsoluteTop(content);
+ var height = content.clientHeight;
+ var bottom = top + height;
+ var margin = 24;
+ var interval = 50; // ms
+
+ if ((mouseY < top + margin) && content.scrollTop > 0) {
+ this.autoScrollStep = ((top + margin) - mouseY) / 3;
+ }
+ else if (mouseY > bottom - margin &&
+ height + content.scrollTop < content.scrollHeight) {
+ this.autoScrollStep = ((bottom - margin) - mouseY) / 3;
+ }
+ else {
+ this.autoScrollStep = undefined;
+ }
+
+ if (this.autoScrollStep) {
+ if (!this.autoScrollTimer) {
+ this.autoScrollTimer = setInterval(function () {
+ if (me.autoScrollStep) {
+ content.scrollTop -= me.autoScrollStep;
+ }
+ else {
+ me.stopAutoScroll();
+ }
+ }, interval);
+ }
+ }
+ else {
+ this.stopAutoScroll();
+ }
+ };
+
+ /**
+ * Stop auto scrolling. Only applicable when scrolling
+ */
+ TreeEditor.prototype.stopAutoScroll = function () {
+ if (this.autoScrollTimer) {
+ clearTimeout(this.autoScrollTimer);
+ delete this.autoScrollTimer;
+ }
+ if (this.autoScrollStep) {
+ delete this.autoScrollStep;
+ }
+ };
+
+
+ /**
+ * Set the focus to an element in the TreeEditor, set text selection, and
+ * set scroll position.
+ * @param {Object} selection An object containing fields:
+ * {Element | undefined} dom The dom element
+ * which has focus
+ * {Range | TextRange} range A text selection
+ * {Number} scrollTop Scroll position
+ */
+ TreeEditor.prototype.setSelection = function (selection) {
+ if (!selection) {
+ return;
+ }
+
+ if ('scrollTop' in selection && this.content) {
+ // TODO: animated scroll
+ this.content.scrollTop = selection.scrollTop;
+ }
+ if (selection.range) {
+ util.setSelectionOffset(selection.range);
+ }
+ if (selection.dom) {
+ selection.dom.focus();
+ }
+ };
+
+ /**
+ * Get the current focus
+ * @return {Object} selection An object containing fields:
+ * {Element | undefined} dom The dom element
+ * which has focus
+ * {Range | TextRange} range A text selection
+ * {Number} scrollTop Scroll position
+ */
+ TreeEditor.prototype.getSelection = function () {
+ return {
+ dom: TreeEditor.domFocus,
+ scrollTop: this.content ? this.content.scrollTop : 0,
+ range: util.getSelectionOffset()
+ };
+ };
+
+ /**
+ * Adjust the scroll position such that given top position is shown at 1/4
+ * of the window height.
+ * @param {Number} top
+ * @param {function(boolean)} [callback] Callback, executed when animation is
+ * finished. The callback returns true
+ * when animation is finished, or false
+ * when not.
+ */
+ TreeEditor.prototype.scrollTo = function (top, callback) {
+ var content = this.content;
+ if (content) {
+ var editor = this;
+ // cancel any running animation
+ if (editor.animateTimeout) {
+ clearTimeout(editor.animateTimeout);
delete editor.animateTimeout;
+ }
+ if (editor.animateCallback) {
+ editor.animateCallback(false);
delete editor.animateCallback;
}
- };
- animate();
- }
- else {
- if (callback) {
- callback(false);
+
+ // calculate final scroll position
+ var height = content.clientHeight;
+ var bottom = content.scrollHeight - height;
+ var finalScrollTop = Math.min(Math.max(top - height / 4, 0), bottom);
+
+ // animate towards the new scroll position
+ var animate = function () {
+ var scrollTop = content.scrollTop;
+ var diff = (finalScrollTop - scrollTop);
+ if (Math.abs(diff) > 3) {
+ content.scrollTop += diff / 3;
+ editor.animateCallback = callback;
+ editor.animateTimeout = setTimeout(animate, 50);
+ }
+ else {
+ // finished
+ if (callback) {
+ callback(true);
+ }
+ content.scrollTop = finalScrollTop;
+ delete editor.animateTimeout;
+ delete editor.animateCallback;
+ }
+ };
+ animate();
+ }
+ else {
+ if (callback) {
+ callback(false);
+ }
}
- }
-};
-
-/**
- * Create main frame
- * @private
- */
-TreeEditor.prototype._createFrame = function () {
- // create the frame
- this.frame = document.createElement('div');
- this.frame.className = 'jsoneditor';
- this.container.appendChild(this.frame);
-
- // create one global event listener to handle all events from all nodes
- var editor = this;
- var onEvent = function (event) {
- editor._onEvent(event);
};
- this.frame.onclick = function (event) {
- var target = event.target;// || event.srcElement;
- onEvent(event);
+ /**
+ * Create main frame
+ * @private
+ */
+ TreeEditor.prototype._createFrame = function () {
+ // create the frame
+ this.frame = document.createElement('div');
+ this.frame.className = 'jsoneditor';
+ this.container.appendChild(this.frame);
- // prevent default submit action of buttons when TreeEditor is located
- // inside a form
- if (target.nodeName == 'BUTTON') {
+ // create one global event listener to handle all events from all nodes
+ var editor = this;
+ function onEvent(event) {
+ editor._onEvent(event);
+ }
+ this.frame.onclick = function (event) {
+ var target = event.target;// || event.srcElement;
+
+ onEvent(event);
+
+ // prevent default submit action of buttons when TreeEditor is located
+ // inside a form
+ if (target.nodeName == 'BUTTON') {
+ event.preventDefault();
+ }
+ };
+ this.frame.oninput = onEvent;
+ this.frame.onchange = onEvent;
+ this.frame.onkeydown = onEvent;
+ this.frame.onkeyup = onEvent;
+ this.frame.oncut = onEvent;
+ this.frame.onpaste = onEvent;
+ this.frame.onmousedown = onEvent;
+ this.frame.onmouseup = onEvent;
+ this.frame.onmouseover = onEvent;
+ this.frame.onmouseout = onEvent;
+ // Note: focus and blur events do not propagate, therefore they defined
+ // using an eventListener with useCapture=true
+ // see http://www.quirksmode.org/blog/archives/2008/04/delegating_the.html
+ util.addEventListener(this.frame, 'focus', onEvent, true);
+ util.addEventListener(this.frame, 'blur', onEvent, true);
+ this.frame.onfocusin = onEvent; // for IE
+ this.frame.onfocusout = onEvent; // for IE
+
+ // create menu
+ this.menu = document.createElement('div');
+ this.menu.className = 'menu';
+ this.frame.appendChild(this.menu);
+
+ // create expand all button
+ var expandAll = document.createElement('button');
+ expandAll.className = 'expand-all';
+ expandAll.title = 'Expand all fields';
+ expandAll.onclick = function () {
+ editor.expandAll();
+ };
+ this.menu.appendChild(expandAll);
+
+ // create expand all button
+ var collapseAll = document.createElement('button');
+ collapseAll.title = 'Collapse all fields';
+ collapseAll.className = 'collapse-all';
+ collapseAll.onclick = function () {
+ editor.collapseAll();
+ };
+ this.menu.appendChild(collapseAll);
+
+ // create undo/redo buttons
+ if (this.history) {
+ // create undo button
+ var undo = document.createElement('button');
+ undo.className = 'undo separator';
+ undo.title = 'Undo last action (Ctrl+Z)';
+ undo.onclick = function () {
+ editor._onUndo();
+ };
+ this.menu.appendChild(undo);
+ this.dom.undo = undo;
+
+ // create redo button
+ var redo = document.createElement('button');
+ redo.className = 'redo';
+ redo.title = 'Redo (Ctrl+Shift+Z)';
+ redo.onclick = function () {
+ editor._onRedo();
+ };
+ this.menu.appendChild(redo);
+ this.dom.redo = redo;
+
+ // register handler for onchange of history
+ this.history.onChange = function () {
+ undo.disabled = !editor.history.canUndo();
+ redo.disabled = !editor.history.canRedo();
+ };
+ this.history.onChange();
+ }
+
+ // create mode box
+ if (this.options && this.options.modes && this.options.modes.length) {
+ var modeBox = modebox.create(this, this.options.modes, this.options.mode);
+ this.menu.appendChild(modeBox);
+ this.dom.modeBox = modeBox;
+ }
+
+ // create search box
+ if (this.options.search) {
+ this.searchBox = new SearchBox(this, this.menu);
+ }
+ };
+
+ /**
+ * Perform an undo action
+ * @private
+ */
+ TreeEditor.prototype._onUndo = function () {
+ if (this.history) {
+ // undo last action
+ this.history.undo();
+
+ // trigger change callback
+ if (this.options.change) {
+ this.options.change();
+ }
+ }
+ };
+
+ /**
+ * Perform a redo action
+ * @private
+ */
+ TreeEditor.prototype._onRedo = function () {
+ if (this.history) {
+ // redo last action
+ this.history.redo();
+
+ // trigger change callback
+ if (this.options.change) {
+ this.options.change();
+ }
+ }
+ };
+
+ /**
+ * Event handler
+ * @param event
+ * @private
+ */
+ TreeEditor.prototype._onEvent = function (event) {
+ var target = event.target;
+
+ if (event.type == 'keydown') {
+ this._onKeyDown(event);
+ }
+
+ if (event.type == 'focus') {
+ TreeEditor.domFocus = target;
+ }
+
+ var node = Node.getNodeFromTarget(target);
+ if (node) {
+ node.onEvent(event);
+ }
+ };
+
+ /**
+ * Event handler for keydown. Handles shortcut keys
+ * @param {Event} event
+ * @private
+ */
+ TreeEditor.prototype._onKeyDown = function (event) {
+ var keynum = event.which || event.keyCode;
+ var ctrlKey = event.ctrlKey;
+ var shiftKey = event.shiftKey;
+ var handled = false;
+
+ if (keynum == 9) { // Tab or Shift+Tab
+ setTimeout(function () {
+ // select all text when moving focus to an editable div
+ util.selectContentEditable(TreeEditor.domFocus);
+ }, 0);
+ }
+
+ if (this.searchBox) {
+ if (ctrlKey && keynum == 70) { // Ctrl+F
+ this.searchBox.dom.search.focus();
+ this.searchBox.dom.search.select();
+ handled = true;
+ }
+ else if (keynum == 114 || (ctrlKey && keynum == 71)) { // F3 or Ctrl+G
+ var focus = true;
+ if (!shiftKey) {
+ // select next search result (F3 or Ctrl+G)
+ this.searchBox.next(focus);
+ }
+ else {
+ // select previous search result (Shift+F3 or Ctrl+Shift+G)
+ this.searchBox.previous(focus);
+ }
+
+ handled = true;
+ }
+ }
+
+ if (this.history) {
+ if (ctrlKey && !shiftKey && keynum == 90) { // Ctrl+Z
+ // undo
+ this._onUndo();
+ handled = true;
+ }
+ else if (ctrlKey && shiftKey && keynum == 90) { // Ctrl+Shift+Z
+ // redo
+ this._onRedo();
+ handled = true;
+ }
+ }
+
+ if (handled) {
event.preventDefault();
+ event.stopPropagation();
}
};
- this.frame.oninput = onEvent;
- this.frame.onchange = onEvent;
- this.frame.onkeydown = onEvent;
- this.frame.onkeyup = onEvent;
- this.frame.oncut = onEvent;
- this.frame.onpaste = onEvent;
- this.frame.onmousedown = onEvent;
- this.frame.onmouseup = onEvent;
- this.frame.onmouseover = onEvent;
- this.frame.onmouseout = onEvent;
- // Note: focus and blur events do not propagate, therefore they defined
- // using an eventListener with useCapture=true
- // see http://www.quirksmode.org/blog/archives/2008/04/delegating_the.html
- util.addEventListener(this.frame, 'focus', onEvent, true);
- util.addEventListener(this.frame, 'blur', onEvent, true);
- this.frame.onfocusin = onEvent; // for IE
- this.frame.onfocusout = onEvent; // for IE
- // create menu
- this.menu = document.createElement('div');
- this.menu.className = 'menu';
- this.frame.appendChild(this.menu);
+ /**
+ * Create main table
+ * @private
+ */
+ TreeEditor.prototype._createTable = function () {
+ var contentOuter = document.createElement('div');
+ contentOuter.className = 'outer';
+ this.contentOuter = contentOuter;
- // create expand all button
- var expandAll = document.createElement('button');
- expandAll.className = 'expand-all';
- expandAll.title = 'Expand all fields';
- expandAll.onclick = function () {
- editor.expandAll();
- };
- this.menu.appendChild(expandAll);
+ this.content = document.createElement('div');
+ this.content.className = 'tree';
+ contentOuter.appendChild(this.content);
- // create expand all button
- var collapseAll = document.createElement('button');
- collapseAll.title = 'Collapse all fields';
- collapseAll.className = 'collapse-all';
- collapseAll.onclick = function () {
- editor.collapseAll();
- };
- this.menu.appendChild(collapseAll);
+ this.table = document.createElement('table');
+ this.table.className = 'tree';
+ this.content.appendChild(this.table);
- // create undo/redo buttons
- if (this.history) {
- // create undo button
- var undo = document.createElement('button');
- undo.className = 'undo separator';
- undo.title = 'Undo last action (Ctrl+Z)';
- undo.onclick = function () {
- editor._onUndo();
- };
- this.menu.appendChild(undo);
- this.dom.undo = undo;
-
- // create redo button
- var redo = document.createElement('button');
- redo.className = 'redo';
- redo.title = 'Redo (Ctrl+Shift+Z)';
- redo.onclick = function () {
- editor._onRedo();
- };
- this.menu.appendChild(redo);
- this.dom.redo = redo;
-
- // register handler for onchange of history
- this.history.onChange = function () {
- undo.disabled = !editor.history.canUndo();
- redo.disabled = !editor.history.canRedo();
- };
- this.history.onChange();
- }
-
- // create mode box
- if (this.options && this.options.modes && this.options.modes.length) {
- var modeBox = createModeBox(this, this.options.modes, this.options.mode);
- this.menu.appendChild(modeBox);
- this.dom.modeBox = modeBox;
- }
-
- // create search box
- if (this.options.search) {
- this.searchBox = new SearchBox(this, this.menu);
- }
-};
-
-/**
- * Perform an undo action
- * @private
- */
-TreeEditor.prototype._onUndo = function () {
- if (this.history) {
- // undo last action
- this.history.undo();
-
- // trigger change callback
- if (this.options.change) {
- this.options.change();
+ // create colgroup where the first two columns don't have a fixed
+ // width, and the edit columns do have a fixed width
+ var col;
+ this.colgroupContent = document.createElement('colgroup');
+ if (this.mode.edit) {
+ col = document.createElement('col');
+ col.width = "24px";
+ this.colgroupContent.appendChild(col);
}
- }
-};
-
-/**
- * Perform a redo action
- * @private
- */
-TreeEditor.prototype._onRedo = function () {
- if (this.history) {
- // redo last action
- this.history.redo();
-
- // trigger change callback
- if (this.options.change) {
- this.options.change();
- }
- }
-};
-
-/**
- * Event handler
- * @param event
- * @private
- */
-TreeEditor.prototype._onEvent = function (event) {
- var target = event.target;
-
- if (event.type == 'keydown') {
- this._onKeyDown(event);
- }
-
- if (event.type == 'focus') {
- TreeEditor.domFocus = target;
- }
-
- var node = Node.getNodeFromTarget(target);
- if (node) {
- node.onEvent(event);
- }
-};
-
-/**
- * Event handler for keydown. Handles shortcut keys
- * @param {Event} event
- * @private
- */
-TreeEditor.prototype._onKeyDown = function (event) {
- var keynum = event.which || event.keyCode;
- var ctrlKey = event.ctrlKey;
- var shiftKey = event.shiftKey;
- var handled = false;
-
- if (keynum == 9) { // Tab or Shift+Tab
- setTimeout(function () {
- // select all text when moving focus to an editable div
- util.selectContentEditable(TreeEditor.domFocus);
- }, 0);
- }
-
- if (this.searchBox) {
- if (ctrlKey && keynum == 70) { // Ctrl+F
- this.searchBox.dom.search.focus();
- this.searchBox.dom.search.select();
- handled = true;
- }
- else if (keynum == 114 || (ctrlKey && keynum == 71)) { // F3 or Ctrl+G
- var focus = true;
- if (!shiftKey) {
- // select next search result (F3 or Ctrl+G)
- this.searchBox.next(focus);
- }
- else {
- // select previous search result (Shift+F3 or Ctrl+Shift+G)
- this.searchBox.previous(focus);
- }
-
- handled = true;
- }
- }
-
- if (this.history) {
- if (ctrlKey && !shiftKey && keynum == 90) { // Ctrl+Z
- // undo
- this._onUndo();
- handled = true;
- }
- else if (ctrlKey && shiftKey && keynum == 90) { // Ctrl+Shift+Z
- // redo
- this._onRedo();
- handled = true;
- }
- }
-
- if (handled) {
- event.preventDefault();
- event.stopPropagation();
- }
-};
-
-/**
- * Create main table
- * @private
- */
-TreeEditor.prototype._createTable = function () {
- var contentOuter = document.createElement('div');
- contentOuter.className = 'outer';
- this.contentOuter = contentOuter;
-
- this.content = document.createElement('div');
- this.content.className = 'tree';
- contentOuter.appendChild(this.content);
-
- this.table = document.createElement('table');
- this.table.className = 'tree';
- this.content.appendChild(this.table);
-
- // create colgroup where the first two columns don't have a fixed
- // width, and the edit columns do have a fixed width
- var col;
- this.colgroupContent = document.createElement('colgroup');
- if (this.mode.edit) {
col = document.createElement('col');
col.width = "24px";
this.colgroupContent.appendChild(col);
- }
- col = document.createElement('col');
- col.width = "24px";
- this.colgroupContent.appendChild(col);
- col = document.createElement('col');
- this.colgroupContent.appendChild(col);
- this.table.appendChild(this.colgroupContent);
+ col = document.createElement('col');
+ this.colgroupContent.appendChild(col);
+ this.table.appendChild(this.colgroupContent);
- this.tbody = document.createElement('tbody');
- this.table.appendChild(this.tbody);
+ this.tbody = document.createElement('tbody');
+ this.table.appendChild(this.tbody);
- this.frame.appendChild(contentOuter);
-};
+ this.frame.appendChild(contentOuter);
+ };
-// register modes at the JSONEditor
-JSONEditor.modes.tree = {
- editor: TreeEditor,
- data: 'json'
-};
-JSONEditor.modes.view = {
- editor: TreeEditor,
- data: 'json'
-};
-JSONEditor.modes.form = {
- editor: TreeEditor,
- data: 'json'
-};
-// Deprecated modes (deprecated since version 2.2.0)
-JSONEditor.modes.editor = {
- editor: TreeEditor,
- data: 'json'
-};
-JSONEditor.modes.viewer = {
- editor: TreeEditor,
- data: 'json'
-};
+ // define modes
+ TreeEditor.modes = {
+ tree: {
+ editor: TreeEditor,
+ data: 'json'
+ },
+ view: {
+ editor: TreeEditor,
+ data: 'json'
+ },
+ form: {
+ editor: TreeEditor,
+ data: 'json'
+ }
+ };
+
+ return TreeEditor;
+});
diff --git a/src/js/appendNodeFactory.js b/src/js/appendNodeFactory.js
new file mode 100644
index 0000000..6c547b4
--- /dev/null
+++ b/src/js/appendNodeFactory.js
@@ -0,0 +1,225 @@
+define(['./util'], function (util) {
+
+ /**
+ * A factory function to create an AppendNode, which depends on a Node
+ * @param {Node} Node
+ */
+ function appendNodeFactory(Node) {
+ /**
+ * @constructor AppendNode
+ * @extends Node
+ * @param {TreeEditor} editor
+ * Create a new AppendNode. This is a special node which is created at the
+ * end of the list with childs for an object or array
+ */
+ function AppendNode (editor) {
+ /** @type {TreeEditor} */
+ this.editor = editor;
+ this.dom = {};
+ }
+
+ AppendNode.prototype = new Node();
+
+ /**
+ * Return a table row with an append button.
+ * @return {Element} dom TR element
+ */
+ AppendNode.prototype.getDom = function () {
+ // TODO: implement a new solution for the append node
+ var dom = this.dom;
+
+ if (dom.tr) {
+ return dom.tr;
+ }
+
+ // a row for the append button
+ var trAppend = document.createElement('tr');
+ trAppend.node = this;
+ dom.tr = trAppend;
+
+ // TODO: consistent naming
+
+ if (this.editor.mode.edit) {
+ // a cell for the dragarea column
+ dom.tdDrag = document.createElement('td');
+
+ // create context menu
+ var tdMenu = document.createElement('td');
+ dom.tdMenu = tdMenu;
+ var menu = document.createElement('button');
+ menu.className = 'contextmenu';
+ menu.title = 'Click to open the actions menu (Ctrl+M)';
+ dom.menu = menu;
+ tdMenu.appendChild(dom.menu);
+ }
+
+ // a cell for the contents (showing text 'empty')
+ var tdAppend = document.createElement('td');
+ var domText = document.createElement('div');
+ domText.innerHTML = '(empty)';
+ domText.className = 'readonly';
+ tdAppend.appendChild(domText);
+ dom.td = tdAppend;
+ dom.text = domText;
+
+ this.updateDom();
+
+ return trAppend;
+ };
+
+ /**
+ * Update the HTML dom of the Node
+ */
+ AppendNode.prototype.updateDom = function () {
+ var dom = this.dom;
+ var tdAppend = dom.td;
+ if (tdAppend) {
+ tdAppend.style.paddingLeft = (this.getLevel() * 24 + 26) + 'px';
+ // TODO: not so nice hard coded offset
+ }
+
+ var domText = dom.text;
+ if (domText) {
+ domText.innerHTML = '(empty ' + this.parent.type + ')';
+ }
+
+ // attach or detach the contents of the append node:
+ // hide when the parent has childs, show when the parent has no childs
+ var trAppend = dom.tr;
+ if (!this.isVisible()) {
+ if (dom.tr.firstChild) {
+ if (dom.tdDrag) {
+ trAppend.removeChild(dom.tdDrag);
+ }
+ if (dom.tdMenu) {
+ trAppend.removeChild(dom.tdMenu);
+ }
+ trAppend.removeChild(tdAppend);
+ }
+ }
+ else {
+ if (!dom.tr.firstChild) {
+ if (dom.tdDrag) {
+ trAppend.appendChild(dom.tdDrag);
+ }
+ if (dom.tdMenu) {
+ trAppend.appendChild(dom.tdMenu);
+ }
+ trAppend.appendChild(tdAppend);
+ }
+ }
+ };
+
+ /**
+ * Check whether the AppendNode is currently visible.
+ * the AppendNode is visible when its parent has no childs (i.e. is empty).
+ * @return {boolean} isVisible
+ */
+ AppendNode.prototype.isVisible = function () {
+ return (this.parent.childs.length == 0);
+ };
+
+ /**
+ * Show a contextmenu for this node
+ * @param {HTMLElement} anchor The element to attach the menu to.
+ * @param {function} [onClose] Callback method called when the context menu
+ * is being closed.
+ */
+ AppendNode.prototype.showContextMenu = function (anchor, onClose) {
+ var node = this;
+ var titles = Node.TYPE_TITLES;
+ var items = [
+ // create append button
+ {
+ 'text': 'Append',
+ 'title': 'Append a new field with type \'auto\' (Ctrl+Shift+Ins)',
+ 'submenuTitle': 'Select the type of the field to be appended',
+ 'className': 'insert',
+ 'click': function () {
+ node._onAppend('', '', 'auto');
+ },
+ 'submenu': [
+ {
+ 'text': 'Auto',
+ 'className': 'type-auto',
+ 'title': titles.auto,
+ 'click': function () {
+ node._onAppend('', '', 'auto');
+ }
+ },
+ {
+ 'text': 'Array',
+ 'className': 'type-array',
+ 'title': titles.array,
+ 'click': function () {
+ node._onAppend('', []);
+ }
+ },
+ {
+ 'text': 'Object',
+ 'className': 'type-object',
+ 'title': titles.object,
+ 'click': function () {
+ node._onAppend('', {});
+ }
+ },
+ {
+ 'text': 'String',
+ 'className': 'type-string',
+ 'title': titles.string,
+ 'click': function () {
+ node._onAppend('', '', 'string');
+ }
+ }
+ ]
+ }
+ ];
+
+ var menu = new ContextMenu(items, {close: onClose});
+ menu.show(anchor);
+ };
+
+ /**
+ * Handle an event. The event is catched centrally by the editor
+ * @param {Event} event
+ */
+ AppendNode.prototype.onEvent = function (event) {
+ var type = event.type;
+ var target = event.target || event.srcElement;
+ var dom = this.dom;
+
+ // highlight the append nodes parent
+ var menu = dom.menu;
+ if (target == menu) {
+ if (type == 'mouseover') {
+ this.editor.highlighter.highlight(this.parent);
+ }
+ else if (type == 'mouseout') {
+ this.editor.highlighter.unhighlight();
+ }
+ }
+
+ // context menu events
+ if (type == 'click' && target == dom.menu) {
+ var highlighter = this.editor.highlighter;
+ highlighter.highlight(this.parent);
+ highlighter.lock();
+ util.addClassName(dom.menu, 'selected');
+ this.showContextMenu(dom.menu, function () {
+ util.removeClassName(dom.menu, 'selected');
+ highlighter.unlock();
+ highlighter.unhighlight();
+ });
+ }
+
+ if (type == 'keydown') {
+ this.onKeyDown(event);
+ }
+ };
+
+ return AppendNode;
+ }
+
+ // return the factory function
+ return appendNodeFactory;
+});
diff --git a/src/js/header.js b/src/js/header.js
index fc4949d..0e0eb57 100644
--- a/src/js/header.js
+++ b/src/js/header.js
@@ -20,7 +20,7 @@
* License for the specific language governing permissions and limitations under
* the License.
*
- * Copyright (c) 2011-2013 Jos de Jong, http://jsoneditoronline.org
+ * Copyright (c) 2011-2014 Jos de Jong, http://jsoneditoronline.org
*
* @author Jos de Jong,
* @version @@version
diff --git a/src/js/modebox.js b/src/js/modebox.js
index 8b25ba6..2cf2bf2 100644
--- a/src/js/modebox.js
+++ b/src/js/modebox.js
@@ -1,94 +1,101 @@
-/**
- * create a mode box to be used in the editor menu's
- * @param {JSONEditor} editor
- * @param {String[]} modes Available modes: 'code', 'form', 'text', 'tree', 'view'
- * @param {String} current Available modes: 'code', 'form', 'text', 'tree', 'view'
- * @returns {HTMLElement} box
- */
-function createModeBox(editor, modes, current) {
+define(['./ContextMenu'], function (ContextMenu) {
+
/**
- * Switch the mode of the editor
- * @param {String} mode
+ * Create a mode box to be used in the editor menu's
+ * @param {JSONEditor} editor
+ * @param {String[]} modes Available modes: 'code', 'form', 'text', 'tree', 'view'
+ * @param {String} current Available modes: 'code', 'form', 'text', 'tree', 'view'
+ * @returns {HTMLElement} box
*/
- function switchMode(mode) {
- // switch mode
- editor.setMode(mode);
+ function createModeBox(editor, modes, current) {
+ /**
+ * 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
- var availableModes = {
- code: {
- 'text': 'Code',
- 'title': 'Switch to code highlighter',
- 'click': function () {
- switchMode('code')
- }
- },
- form: {
- 'text': 'Form',
- 'title': 'Switch to form editor',
- 'click': function () {
- switchMode('form');
- }
- },
- text: {
- 'text': 'Text',
- 'title': 'Switch to plain text editor',
- 'click': function () {
- switchMode('text');
- }
- },
- tree: {
- 'text': 'Tree',
- 'title': 'Switch to tree editor',
- 'click': function () {
- switchMode('tree');
- }
- },
- view: {
- 'text': 'View',
- 'title': 'Switch to tree view',
- 'click': function () {
- switchMode('view');
+ // restore focus on mode box
+ var modeBox = editor.dom && editor.dom.modeBox;
+ if (modeBox) {
+ modeBox.focus();
}
}
- };
- // list the selected modes
- var items = [];
- for (var i = 0; i < modes.length; i++) {
- var mode = modes[i];
- var item = availableModes[mode];
- if (!item) {
- throw new Error('Unknown mode "' + mode + '"');
+ // available modes
+ var availableModes = {
+ code: {
+ 'text': 'Code',
+ 'title': 'Switch to code highlighter',
+ 'click': function () {
+ switchMode('code')
+ }
+ },
+ form: {
+ 'text': 'Form',
+ 'title': 'Switch to form editor',
+ 'click': function () {
+ switchMode('form');
+ }
+ },
+ text: {
+ 'text': 'Text',
+ 'title': 'Switch to plain text editor',
+ 'click': function () {
+ switchMode('text');
+ }
+ },
+ tree: {
+ 'text': 'Tree',
+ 'title': 'Switch to tree editor',
+ 'click': function () {
+ switchMode('tree');
+ }
+ },
+ view: {
+ 'text': 'View',
+ 'title': 'Switch to tree view',
+ 'click': function () {
+ switchMode('view');
+ }
+ }
+ };
+
+ // list the selected modes
+ var items = [];
+ for (var i = 0; i < modes.length; i++) {
+ var mode = modes[i];
+ var item = availableModes[mode];
+ if (!item) {
+ throw new Error('Unknown mode "' + mode + '"');
+ }
+
+ item.className = 'type-modes' + ((current == mode) ? ' selected' : '');
+ items.push(item);
}
- item.className = 'type-modes' + ((current == mode) ? ' selected' : '');
- items.push(item);
+ // retrieve the title of current mode
+ var currentMode = availableModes[current];
+ if (!currentMode) {
+ throw new Error('Unknown mode "' + current + '"');
+ }
+ var currentTitle = currentMode.text;
+
+ // create the html element
+ var box = document.createElement('button');
+ box.className = 'modes separator';
+ box.innerHTML = currentTitle + ' ▾';
+ box.title = 'Switch editor mode';
+ box.onclick = function () {
+ var menu = new ContextMenu(items);
+ menu.show(box);
+ };
+
+ return box;
}
- // retrieve the title of current mode
- var currentMode = availableModes[current];
- if (!currentMode) {
- throw new Error('Unknown mode "' + current + '"');
+ return {
+ create: createModeBox
}
- var currentTitle = currentMode.text;
-
- // create the html element
- var box = document.createElement('button');
- box.className = 'modes separator';
- box.innerHTML = currentTitle + ' ▾';
- box.title = 'Switch editor mode';
- box.onclick = function () {
- var menu = new ContextMenu(items);
- menu.show(box);
- };
-
- return box;
-}
+});
diff --git a/src/js/module.js b/src/js/module.js
index 994282e..c524fb6 100644
--- a/src/js/module.js
+++ b/src/js/module.js
@@ -2,10 +2,6 @@
// module exports
var jsoneditor = {
'JSONEditor': JSONEditor,
- 'JSONFormatter': function () {
- throw new Error('JSONFormatter is deprecated. ' +
- 'Use JSONEditor with mode "text" or "code" instead');
- },
'util': util
};
diff --git a/src/js/util.js b/src/js/util.js
index 0cd15f6..404b009 100644
--- a/src/js/util.js
+++ b/src/js/util.js
@@ -1,474 +1,479 @@
-// create namespace
-util = {};
+define(function () {
-/**
- * Parse JSON using the parser built-in in the browser.
- * On exception, the jsonString is validated and a detailed error is thrown.
- * @param {String} jsonString
- */
-util.parse = function parse(jsonString) {
- try {
- return JSON.parse(jsonString);
- }
- catch (err) {
- // try to throw a more detailed error message using validate
- util.validate(jsonString);
- throw err;
- }
-};
+ // create namespace
+ var util = {};
-/**
- * Validate a string containing a JSON object
- * This method uses JSONLint to validate the String. If JSONLint is not
- * available, the built-in JSON parser of the browser is used.
- * @param {String} jsonString String with an (invalid) JSON object
- * @throws Error
- */
-util.validate = function validate(jsonString) {
- if (typeof(jsonlint) != 'undefined') {
- jsonlint.parse(jsonString);
- }
- else {
- JSON.parse(jsonString);
- }
-};
-
-/**
- * Extend object a with the properties of object b
- * @param {Object} a
- * @param {Object} b
- * @return {Object} a
- */
-util.extend = function extend(a, b) {
- for (var prop in b) {
- if (b.hasOwnProperty(prop)) {
- a[prop] = b[prop];
+ /**
+ * Parse JSON using the parser built-in in the browser.
+ * On exception, the jsonString is validated and a detailed error is thrown.
+ * @param {String} jsonString
+ */
+ util.parse = function parse(jsonString) {
+ try {
+ return JSON.parse(jsonString);
}
- }
- return a;
-};
-
-/**
- * Remove all properties from object a
- * @param {Object} a
- * @return {Object} a
- */
-util.clear = function clear (a) {
- for (var prop in a) {
- if (a.hasOwnProperty(prop)) {
- delete a[prop];
+ catch (err) {
+ // try to throw a more detailed error message using validate
+ util.validate(jsonString);
+ throw err;
}
- }
- return a;
-};
+ };
-/**
- * Output text to the console, if console is available
- * @param {...*} args
- */
-util.log = function log (args) {
- if (typeof console !== 'undefined' && typeof console.log === 'function') {
- console.log.apply(console, arguments);
- }
-};
-
-/**
- * Get the type of an object
- * @param {*} object
- * @return {String} type
- */
-util.type = function type (object) {
- if (object === null) {
- return 'null';
- }
- if (object === undefined) {
- return 'undefined';
- }
- if ((object instanceof Number) || (typeof object === 'number')) {
- return 'number';
- }
- if ((object instanceof String) || (typeof object === 'string')) {
- return 'string';
- }
- if ((object instanceof Boolean) || (typeof object === 'boolean')) {
- return 'boolean';
- }
- if ((object instanceof RegExp) || (typeof object === 'regexp')) {
- return 'regexp';
- }
- if (Array.isArray(object)) {
- return 'array';
- }
-
- return 'object';
-};
-
-/**
- * Test whether a text contains a url (matches when a string starts
- * with 'http://*' or 'https://*' and has no whitespace characters)
- * @param {String} text
- */
-var isUrlRegex = /^https?:\/\/\S+$/;
-util.isUrl = function isUrl (text) {
- return (typeof text == 'string' || text instanceof String) &&
- isUrlRegex.test(text);
-};
-
-/**
- * Retrieve the absolute left value of a DOM element
- * @param {Element} elem A dom element, for example a div
- * @return {Number} left The absolute left position of this element
- * in the browser page.
- */
-util.getAbsoluteLeft = function getAbsoluteLeft(elem) {
- var rect = elem.getBoundingClientRect();
- return rect.left + window.pageXOffset || document.scrollLeft || 0;
-};
-
-/**
- * Retrieve the absolute top value of a DOM element
- * @param {Element} elem A dom element, for example a div
- * @return {Number} top The absolute top position of this element
- * in the browser page.
- */
-util.getAbsoluteTop = function getAbsoluteTop(elem) {
- var rect = elem.getBoundingClientRect();
- return rect.top + window.pageYOffset || document.scrollTop || 0;
-};
-
-/**
- * add a className to the given elements style
- * @param {Element} elem
- * @param {String} className
- */
-util.addClassName = function addClassName(elem, className) {
- var classes = elem.className.split(' ');
- if (classes.indexOf(className) == -1) {
- classes.push(className); // add the class to the array
- elem.className = classes.join(' ');
- }
-};
-
-/**
- * add a className to the given elements style
- * @param {Element} elem
- * @param {String} className
- */
-util.removeClassName = function removeClassName(elem, className) {
- var classes = elem.className.split(' ');
- var index = classes.indexOf(className);
- if (index != -1) {
- classes.splice(index, 1); // remove the class from the array
- elem.className = classes.join(' ');
- }
-};
-
-/**
- * Strip the formatting from the contents of a div
- * the formatting from the div itself is not stripped, only from its childs.
- * @param {Element} divElement
- */
-util.stripFormatting = function stripFormatting(divElement) {
- var childs = divElement.childNodes;
- for (var i = 0, iMax = childs.length; i < iMax; i++) {
- var child = childs[i];
-
- // remove the style
- if (child.style) {
- // TODO: test if child.attributes does contain style
- child.removeAttribute('style');
+ /**
+ * Validate a string containing a JSON object
+ * This method uses JSONLint to validate the String. If JSONLint is not
+ * available, the built-in JSON parser of the browser is used.
+ * @param {String} jsonString String with an (invalid) JSON object
+ * @throws Error
+ */
+ util.validate = function validate(jsonString) {
+ if (typeof(jsonlint) != 'undefined') {
+ jsonlint.parse(jsonString);
}
+ else {
+ JSON.parse(jsonString);
+ }
+ };
- // remove all attributes
- var attributes = child.attributes;
- if (attributes) {
- for (var j = attributes.length - 1; j >= 0; j--) {
- var attribute = attributes[j];
- if (attribute.specified == true) {
- child.removeAttribute(attribute.name);
- }
+ /**
+ * Extend object a with the properties of object b
+ * @param {Object} a
+ * @param {Object} b
+ * @return {Object} a
+ */
+ util.extend = function extend(a, b) {
+ for (var prop in b) {
+ if (b.hasOwnProperty(prop)) {
+ a[prop] = b[prop];
}
}
+ return a;
+ };
- // recursively strip childs
- util.stripFormatting(child);
- }
-};
-
-/**
- * Set focus to the end of an editable div
- * code from Nico Burns
- * http://stackoverflow.com/users/140293/nico-burns
- * http://stackoverflow.com/questions/1125292/how-to-move-cursor-to-end-of-contenteditable-entity
- * @param {Element} contentEditableElement A content editable div
- */
-util.setEndOfContentEditable = function setEndOfContentEditable(contentEditableElement) {
- var range, selection;
- if(document.createRange) {
- range = document.createRange();//Create a range (a range is a like the selection but invisible)
- range.selectNodeContents(contentEditableElement);//Select the entire contents of the element with the range
- range.collapse(false);//collapse the range to the end point. false means collapse to end rather than the start
- selection = window.getSelection();//get the selection object (allows you to change selection)
- selection.removeAllRanges();//remove any selections already made
- selection.addRange(range);//make the range you have just created the visible selection
- }
-};
-
-/**
- * Select all text of a content editable div.
- * http://stackoverflow.com/a/3806004/1262753
- * @param {Element} contentEditableElement A content editable div
- */
-util.selectContentEditable = function selectContentEditable(contentEditableElement) {
- if (!contentEditableElement || contentEditableElement.nodeName != 'DIV') {
- return;
- }
-
- var sel, range;
- if (window.getSelection && document.createRange) {
- range = document.createRange();
- range.selectNodeContents(contentEditableElement);
- sel = window.getSelection();
- sel.removeAllRanges();
- sel.addRange(range);
- }
-};
-
-/**
- * Get text selection
- * http://stackoverflow.com/questions/4687808/contenteditable-selected-text-save-and-restore
- * @return {Range | TextRange | null} range
- */
-util.getSelection = function getSelection() {
- if (window.getSelection) {
- var sel = window.getSelection();
- if (sel.getRangeAt && sel.rangeCount) {
- return sel.getRangeAt(0);
+ /**
+ * Remove all properties from object a
+ * @param {Object} a
+ * @return {Object} a
+ */
+ util.clear = function clear (a) {
+ for (var prop in a) {
+ if (a.hasOwnProperty(prop)) {
+ delete a[prop];
+ }
}
- }
- return null;
-};
+ return a;
+ };
-/**
- * Set text selection
- * http://stackoverflow.com/questions/4687808/contenteditable-selected-text-save-and-restore
- * @param {Range | TextRange | null} range
- */
-util.setSelection = function setSelection(range) {
- if (range) {
- if (window.getSelection) {
- var sel = window.getSelection();
+ /**
+ * Output text to the console, if console is available
+ * @param {...*} args
+ */
+ util.log = function log (args) {
+ if (typeof console !== 'undefined' && typeof console.log === 'function') {
+ console.log.apply(console, arguments);
+ }
+ };
+
+ /**
+ * Get the type of an object
+ * @param {*} object
+ * @return {String} type
+ */
+ util.type = function type (object) {
+ if (object === null) {
+ return 'null';
+ }
+ if (object === undefined) {
+ return 'undefined';
+ }
+ if ((object instanceof Number) || (typeof object === 'number')) {
+ return 'number';
+ }
+ if ((object instanceof String) || (typeof object === 'string')) {
+ return 'string';
+ }
+ if ((object instanceof Boolean) || (typeof object === 'boolean')) {
+ return 'boolean';
+ }
+ if ((object instanceof RegExp) || (typeof object === 'regexp')) {
+ return 'regexp';
+ }
+ if (Array.isArray(object)) {
+ return 'array';
+ }
+
+ return 'object';
+ };
+
+ /**
+ * Test whether a text contains a url (matches when a string starts
+ * with 'http://*' or 'https://*' and has no whitespace characters)
+ * @param {String} text
+ */
+ var isUrlRegex = /^https?:\/\/\S+$/;
+ util.isUrl = function isUrl (text) {
+ return (typeof text == 'string' || text instanceof String) &&
+ isUrlRegex.test(text);
+ };
+
+ /**
+ * Retrieve the absolute left value of a DOM element
+ * @param {Element} elem A dom element, for example a div
+ * @return {Number} left The absolute left position of this element
+ * in the browser page.
+ */
+ util.getAbsoluteLeft = function getAbsoluteLeft(elem) {
+ var rect = elem.getBoundingClientRect();
+ return rect.left + window.pageXOffset || document.scrollLeft || 0;
+ };
+
+ /**
+ * Retrieve the absolute top value of a DOM element
+ * @param {Element} elem A dom element, for example a div
+ * @return {Number} top The absolute top position of this element
+ * in the browser page.
+ */
+ util.getAbsoluteTop = function getAbsoluteTop(elem) {
+ var rect = elem.getBoundingClientRect();
+ return rect.top + window.pageYOffset || document.scrollTop || 0;
+ };
+
+ /**
+ * add a className to the given elements style
+ * @param {Element} elem
+ * @param {String} className
+ */
+ util.addClassName = function addClassName(elem, className) {
+ var classes = elem.className.split(' ');
+ if (classes.indexOf(className) == -1) {
+ classes.push(className); // add the class to the array
+ elem.className = classes.join(' ');
+ }
+ };
+
+ /**
+ * add a className to the given elements style
+ * @param {Element} elem
+ * @param {String} className
+ */
+ util.removeClassName = function removeClassName(elem, className) {
+ var classes = elem.className.split(' ');
+ var index = classes.indexOf(className);
+ if (index != -1) {
+ classes.splice(index, 1); // remove the class from the array
+ elem.className = classes.join(' ');
+ }
+ };
+
+ /**
+ * Strip the formatting from the contents of a div
+ * the formatting from the div itself is not stripped, only from its childs.
+ * @param {Element} divElement
+ */
+ util.stripFormatting = function stripFormatting(divElement) {
+ var childs = divElement.childNodes;
+ for (var i = 0, iMax = childs.length; i < iMax; i++) {
+ var child = childs[i];
+
+ // remove the style
+ if (child.style) {
+ // TODO: test if child.attributes does contain style
+ child.removeAttribute('style');
+ }
+
+ // remove all attributes
+ var attributes = child.attributes;
+ if (attributes) {
+ for (var j = attributes.length - 1; j >= 0; j--) {
+ var attribute = attributes[j];
+ if (attribute.specified == true) {
+ child.removeAttribute(attribute.name);
+ }
+ }
+ }
+
+ // recursively strip childs
+ util.stripFormatting(child);
+ }
+ };
+
+ /**
+ * Set focus to the end of an editable div
+ * code from Nico Burns
+ * http://stackoverflow.com/users/140293/nico-burns
+ * http://stackoverflow.com/questions/1125292/how-to-move-cursor-to-end-of-contenteditable-entity
+ * @param {Element} contentEditableElement A content editable div
+ */
+ util.setEndOfContentEditable = function setEndOfContentEditable(contentEditableElement) {
+ var range, selection;
+ if(document.createRange) {
+ range = document.createRange();//Create a range (a range is a like the selection but invisible)
+ range.selectNodeContents(contentEditableElement);//Select the entire contents of the element with the range
+ range.collapse(false);//collapse the range to the end point. false means collapse to end rather than the start
+ selection = window.getSelection();//get the selection object (allows you to change selection)
+ selection.removeAllRanges();//remove any selections already made
+ selection.addRange(range);//make the range you have just created the visible selection
+ }
+ };
+
+ /**
+ * Select all text of a content editable div.
+ * http://stackoverflow.com/a/3806004/1262753
+ * @param {Element} contentEditableElement A content editable div
+ */
+ util.selectContentEditable = function selectContentEditable(contentEditableElement) {
+ if (!contentEditableElement || contentEditableElement.nodeName != 'DIV') {
+ return;
+ }
+
+ var sel, range;
+ if (window.getSelection && document.createRange) {
+ range = document.createRange();
+ range.selectNodeContents(contentEditableElement);
+ sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
}
- }
-};
+ };
-/**
- * Get selected text range
- * @return {Object} params object containing parameters:
- * {Number} startOffset
- * {Number} endOffset
- * {Element} container HTML element holding the
- * selected text element
- * Returns null if no text selection is found
- */
-util.getSelectionOffset = function getSelectionOffset() {
- var range = util.getSelection();
-
- if (range && 'startOffset' in range && 'endOffset' in range &&
- range.startContainer && (range.startContainer == range.endContainer)) {
- return {
- startOffset: range.startOffset,
- endOffset: range.endOffset,
- container: range.startContainer.parentNode
- };
- }
-
- return null;
-};
-
-/**
- * Set selected text range in given element
- * @param {Object} params An object containing:
- * {Element} container
- * {Number} startOffset
- * {Number} endOffset
- */
-util.setSelectionOffset = function setSelectionOffset(params) {
- if (document.createRange && window.getSelection) {
- var selection = window.getSelection();
- if(selection) {
- var range = document.createRange();
- // TODO: do not suppose that the first child of the container is a textnode,
- // but recursively find the textnodes
- range.setStart(params.container.firstChild, params.startOffset);
- range.setEnd(params.container.firstChild, params.endOffset);
-
- util.setSelection(range);
- }
- }
-};
-
-/**
- * Get the inner text of an HTML element (for example a div element)
- * @param {Element} element
- * @param {Object} [buffer]
- * @return {String} innerText
- */
-util.getInnerText = function getInnerText(element, buffer) {
- var first = (buffer == undefined);
- if (first) {
- buffer = {
- 'text': '',
- 'flush': function () {
- var text = this.text;
- this.text = '';
- return text;
- },
- 'set': function (text) {
- this.text = text;
+ /**
+ * Get text selection
+ * http://stackoverflow.com/questions/4687808/contenteditable-selected-text-save-and-restore
+ * @return {Range | TextRange | null} range
+ */
+ util.getSelection = function getSelection() {
+ if (window.getSelection) {
+ var sel = window.getSelection();
+ if (sel.getRangeAt && sel.rangeCount) {
+ return sel.getRangeAt(0);
}
- };
- }
+ }
+ return null;
+ };
- // text node
- if (element.nodeValue) {
- return buffer.flush() + element.nodeValue;
- }
+ /**
+ * Set text selection
+ * http://stackoverflow.com/questions/4687808/contenteditable-selected-text-save-and-restore
+ * @param {Range | TextRange | null} range
+ */
+ util.setSelection = function setSelection(range) {
+ if (range) {
+ if (window.getSelection) {
+ var sel = window.getSelection();
+ sel.removeAllRanges();
+ sel.addRange(range);
+ }
+ }
+ };
- // divs or other HTML elements
- if (element.hasChildNodes()) {
- var childNodes = element.childNodes;
- var innerText = '';
+ /**
+ * Get selected text range
+ * @return {Object} params object containing parameters:
+ * {Number} startOffset
+ * {Number} endOffset
+ * {Element} container HTML element holding the
+ * selected text element
+ * Returns null if no text selection is found
+ */
+ util.getSelectionOffset = function getSelectionOffset() {
+ var range = util.getSelection();
- for (var i = 0, iMax = childNodes.length; i < iMax; i++) {
- var child = childNodes[i];
+ if (range && 'startOffset' in range && 'endOffset' in range &&
+ range.startContainer && (range.startContainer == range.endContainer)) {
+ return {
+ startOffset: range.startOffset,
+ endOffset: range.endOffset,
+ container: range.startContainer.parentNode
+ };
+ }
- if (child.nodeName == 'DIV' || child.nodeName == 'P') {
- var prevChild = childNodes[i - 1];
- var prevName = prevChild ? prevChild.nodeName : undefined;
- if (prevName && prevName != 'DIV' && prevName != 'P' && prevName != 'BR') {
- innerText += '\n';
- buffer.flush();
+ return null;
+ };
+
+ /**
+ * Set selected text range in given element
+ * @param {Object} params An object containing:
+ * {Element} container
+ * {Number} startOffset
+ * {Number} endOffset
+ */
+ util.setSelectionOffset = function setSelectionOffset(params) {
+ if (document.createRange && window.getSelection) {
+ var selection = window.getSelection();
+ if(selection) {
+ var range = document.createRange();
+ // TODO: do not suppose that the first child of the container is a textnode,
+ // but recursively find the textnodes
+ range.setStart(params.container.firstChild, params.startOffset);
+ range.setEnd(params.container.firstChild, params.endOffset);
+
+ util.setSelection(range);
+ }
+ }
+ };
+
+ /**
+ * Get the inner text of an HTML element (for example a div element)
+ * @param {Element} element
+ * @param {Object} [buffer]
+ * @return {String} innerText
+ */
+ util.getInnerText = function getInnerText(element, buffer) {
+ var first = (buffer == undefined);
+ if (first) {
+ buffer = {
+ 'text': '',
+ 'flush': function () {
+ var text = this.text;
+ this.text = '';
+ return text;
+ },
+ 'set': function (text) {
+ this.text = text;
+ }
+ };
+ }
+
+ // text node
+ if (element.nodeValue) {
+ return buffer.flush() + element.nodeValue;
+ }
+
+ // divs or other HTML elements
+ if (element.hasChildNodes()) {
+ var childNodes = element.childNodes;
+ var innerText = '';
+
+ for (var i = 0, iMax = childNodes.length; i < iMax; i++) {
+ var child = childNodes[i];
+
+ if (child.nodeName == 'DIV' || child.nodeName == 'P') {
+ var prevChild = childNodes[i - 1];
+ var prevName = prevChild ? prevChild.nodeName : undefined;
+ if (prevName && prevName != 'DIV' && prevName != 'P' && prevName != 'BR') {
+ innerText += '\n';
+ buffer.flush();
+ }
+ innerText += util.getInnerText(child, buffer);
+ buffer.set('\n');
+ }
+ else if (child.nodeName == 'BR') {
+ innerText += buffer.flush();
+ buffer.set('\n');
+ }
+ else {
+ innerText += util.getInnerText(child, buffer);
}
- innerText += util.getInnerText(child, buffer);
- buffer.set('\n');
}
- else if (child.nodeName == 'BR') {
- innerText += buffer.flush();
- buffer.set('\n');
- }
- else {
- innerText += util.getInnerText(child, buffer);
+
+ return innerText;
+ }
+ else {
+ if (element.nodeName == 'P' && util.getInternetExplorerVersion() != -1) {
+ // On Internet Explorer, a
with hasChildNodes()==false is
+ // rendered with a new line. Note that a
with
+ // hasChildNodes()==true is rendered without a new line
+ // Other browsers always ensure there is a inside the
,
+ // and if not, the
does not render a new line
+ return buffer.flush();
}
}
- return innerText;
- }
- else {
- if (element.nodeName == 'P' && util.getInternetExplorerVersion() != -1) {
- // On Internet Explorer, a
with hasChildNodes()==false is
- // rendered with a new line. Note that a
with
- // hasChildNodes()==true is rendered without a new line
- // Other browsers always ensure there is a inside the
,
- // and if not, the
does not render a new line
- return buffer.flush();
- }
- }
+ // br or unknown
+ return '';
+ };
- // br or unknown
- return '';
-};
-
-/**
- * Returns the version of Internet Explorer or a -1
- * (indicating the use of another browser).
- * Source: http://msdn.microsoft.com/en-us/library/ms537509(v=vs.85).aspx
- * @return {Number} Internet Explorer version, or -1 in case of an other browser
- */
-util.getInternetExplorerVersion = function getInternetExplorerVersion() {
- if (_ieVersion == -1) {
- var rv = -1; // Return value assumes failure.
- if (navigator.appName == 'Microsoft Internet Explorer')
- {
- var ua = navigator.userAgent;
- var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
- if (re.exec(ua) != null) {
- rv = parseFloat( RegExp.$1 );
+ /**
+ * Returns the version of Internet Explorer or a -1
+ * (indicating the use of another browser).
+ * Source: http://msdn.microsoft.com/en-us/library/ms537509(v=vs.85).aspx
+ * @return {Number} Internet Explorer version, or -1 in case of an other browser
+ */
+ util.getInternetExplorerVersion = function getInternetExplorerVersion() {
+ if (_ieVersion == -1) {
+ var rv = -1; // Return value assumes failure.
+ if (navigator.appName == 'Microsoft Internet Explorer')
+ {
+ var ua = navigator.userAgent;
+ var re = new RegExp("MSIE ([0-9]{1,}[\.0-9]{0,})");
+ if (re.exec(ua) != null) {
+ rv = parseFloat( RegExp.$1 );
+ }
}
+
+ _ieVersion = rv;
}
- _ieVersion = rv;
- }
+ return _ieVersion;
+ };
- return _ieVersion;
-};
+ /**
+ * Test whether the current browser is Firefox
+ * @returns {boolean} isFirefox
+ */
+ util.isFirefox = function isFirefox () {
+ return (navigator.userAgent.indexOf("Firefox") != -1);
+ };
-/**
- * Test whether the current browser is Firefox
- * @returns {boolean} isFirefox
- */
-util.isFirefox = function isFirefox () {
- return (navigator.userAgent.indexOf("Firefox") != -1);
-};
+ /**
+ * cached internet explorer version
+ * @type {Number}
+ * @private
+ */
+ var _ieVersion = -1;
-/**
- * cached internet explorer version
- * @type {Number}
- * @private
- */
-var _ieVersion = -1;
+ /**
+ * Add and event listener. Works for all browsers
+ * @param {Element} element An html element
+ * @param {string} action The action, for example "click",
+ * without the prefix "on"
+ * @param {function} listener The callback function to be executed
+ * @param {boolean} [useCapture] false by default
+ * @return {function} the created event listener
+ */
+ util.addEventListener = function addEventListener(element, action, listener, useCapture) {
+ if (element.addEventListener) {
+ if (useCapture === undefined)
+ useCapture = false;
-/**
- * Add and event listener. Works for all browsers
- * @param {Element} element An html element
- * @param {string} action The action, for example "click",
- * without the prefix "on"
- * @param {function} listener The callback function to be executed
- * @param {boolean} [useCapture] false by default
- * @return {function} the created event listener
- */
-util.addEventListener = function addEventListener(element, action, listener, useCapture) {
- if (element.addEventListener) {
- if (useCapture === undefined)
- useCapture = false;
+ if (action === "mousewheel" && util.isFirefox()) {
+ action = "DOMMouseScroll"; // For Firefox
+ }
- if (action === "mousewheel" && util.isFirefox()) {
- action = "DOMMouseScroll"; // For Firefox
+ element.addEventListener(action, listener, useCapture);
+ return listener;
+ } else if (element.attachEvent) {
+ // Old IE browsers
+ var f = function () {
+ return listener.call(element, window.event);
+ };
+ element.attachEvent("on" + action, f);
+ return f;
}
+ };
- element.addEventListener(action, listener, useCapture);
- return listener;
- } else if (element.attachEvent) {
- // Old IE browsers
- var f = function () {
- return listener.call(element, window.event);
- };
- element.attachEvent("on" + action, f);
- return f;
- }
-};
+ /**
+ * Remove an event listener from an element
+ * @param {Element} element An html dom element
+ * @param {string} action The name of the event, for example "mousedown"
+ * @param {function} listener The listener function
+ * @param {boolean} [useCapture] false by default
+ */
+ util.removeEventListener = function removeEventListener(element, action, listener, useCapture) {
+ if (element.removeEventListener) {
+ if (useCapture === undefined)
+ useCapture = false;
-/**
- * Remove an event listener from an element
- * @param {Element} element An html dom element
- * @param {string} action The name of the event, for example "mousedown"
- * @param {function} listener The listener function
- * @param {boolean} [useCapture] false by default
- */
-util.removeEventListener = function removeEventListener(element, action, listener, useCapture) {
- if (element.removeEventListener) {
- if (useCapture === undefined)
- useCapture = false;
+ if (action === "mousewheel" && util.isFirefox()) {
+ action = "DOMMouseScroll"; // For Firefox
+ }
- if (action === "mousewheel" && util.isFirefox()) {
- action = "DOMMouseScroll"; // For Firefox
+ element.removeEventListener(action, listener, useCapture);
+ } else if (element.detachEvent) {
+ // Old IE browsers
+ element.detachEvent("on" + action, listener);
}
+ };
- element.removeEventListener(action, listener, useCapture);
- } else if (element.detachEvent) {
- // Old IE browsers
- element.detachEvent("on" + action, listener);
- }
-};
+ return util;
+});
\ No newline at end of file
diff --git a/test/couchdbeditor.html b/test/couchdbeditor.html
index eae44d5..d1680f1 100644
--- a/test/couchdbeditor.html
+++ b/test/couchdbeditor.html
@@ -37,7 +37,7 @@
function init() {
var container = document.getElementById('jsoneditor');
- editor = (container);
+ editor = new JSONEditor(container);
document.getElementById('url').focus();
}
diff --git a/test/require.js b/test/require.js
new file mode 100644
index 0000000..d65036f
--- /dev/null
+++ b/test/require.js
@@ -0,0 +1,36 @@
+/*
+ RequireJS 2.1.13 Copyright (c) 2010-2014, The Dojo Foundation All Rights Reserved.
+ Available via the MIT or new BSD license.
+ see: http://github.com/jrburke/requirejs for details
+*/
+var requirejs,require,define;
+(function(ba){function G(b){return"[object Function]"===K.call(b)}function H(b){return"[object Array]"===K.call(b)}function v(b,c){if(b){var d;for(d=0;dthis.depCount&&!this.defined){if(G(l)){if(this.events.error&&this.map.isDefine||g.onError!==ca)try{f=i.execCb(c,l,b,f)}catch(d){a=d}else f=i.execCb(c,l,b,f);this.map.isDefine&&void 0===f&&((b=this.module)?f=b.exports:this.usingExports&&
+(f=this.exports));if(a)return a.requireMap=this.map,a.requireModules=this.map.isDefine?[this.map.id]:null,a.requireType=this.map.isDefine?"define":"require",w(this.error=a)}else f=l;this.exports=f;if(this.map.isDefine&&!this.ignore&&(r[c]=f,g.onResourceLoad))g.onResourceLoad(i,this.map,this.depMaps);y(c);this.defined=!0}this.defining=!1;this.defined&&!this.defineEmitted&&(this.defineEmitted=!0,this.emit("defined",this.exports),this.defineEmitComplete=!0)}}else this.fetch()}},callPlugin:function(){var a=
+this.map,b=a.id,d=p(a.prefix);this.depMaps.push(d);q(d,"defined",u(this,function(f){var l,d;d=m(aa,this.map.id);var e=this.map.name,P=this.map.parentMap?this.map.parentMap.name:null,n=i.makeRequire(a.parentMap,{enableBuildCallback:!0});if(this.map.unnormalized){if(f.normalize&&(e=f.normalize(e,function(a){return c(a,P,!0)})||""),f=p(a.prefix+"!"+e,this.map.parentMap),q(f,"defined",u(this,function(a){this.init([],function(){return a},null,{enabled:!0,ignore:!0})})),d=m(h,f.id)){this.depMaps.push(f);
+if(this.events.error)d.on("error",u(this,function(a){this.emit("error",a)}));d.enable()}}else d?(this.map.url=i.nameToUrl(d),this.load()):(l=u(this,function(a){this.init([],function(){return a},null,{enabled:!0})}),l.error=u(this,function(a){this.inited=!0;this.error=a;a.requireModules=[b];B(h,function(a){0===a.map.id.indexOf(b+"_unnormalized")&&y(a.map.id)});w(a)}),l.fromText=u(this,function(f,c){var d=a.name,e=p(d),P=M;c&&(f=c);P&&(M=!1);s(e);t(j.config,b)&&(j.config[d]=j.config[b]);try{g.exec(f)}catch(h){return w(C("fromtexteval",
+"fromText eval for "+b+" failed: "+h,h,[b]))}P&&(M=!0);this.depMaps.push(e);i.completeLoad(d);n([d],l)}),f.load(a.name,n,l,j))}));i.enable(d,this);this.pluginMaps[d.id]=d},enable:function(){V[this.map.id]=this;this.enabling=this.enabled=!0;v(this.depMaps,u(this,function(a,b){var c,f;if("string"===typeof a){a=p(a,this.map.isDefine?this.map:this.map.parentMap,!1,!this.skipMap);this.depMaps[b]=a;if(c=m(L,a.id)){this.depExports[b]=c(this);return}this.depCount+=1;q(a,"defined",u(this,function(a){this.defineDep(b,
+a);this.check()}));this.errback&&q(a,"error",u(this,this.errback))}c=a.id;f=h[c];!t(L,c)&&(f&&!f.enabled)&&i.enable(a,this)}));B(this.pluginMaps,u(this,function(a){var b=m(h,a.id);b&&!b.enabled&&i.enable(a,this)}));this.enabling=!1;this.check()},on:function(a,b){var c=this.events[a];c||(c=this.events[a]=[]);c.push(b)},emit:function(a,b){v(this.events[a],function(a){a(b)});"error"===a&&delete this.events[a]}};i={config:j,contextName:b,registry:h,defined:r,urlFetched:S,defQueue:A,Module:Z,makeModuleMap:p,
+nextTick:g.nextTick,onError:w,configure:function(a){a.baseUrl&&"/"!==a.baseUrl.charAt(a.baseUrl.length-1)&&(a.baseUrl+="/");var b=j.shim,c={paths:!0,bundles:!0,config:!0,map:!0};B(a,function(a,b){c[b]?(j[b]||(j[b]={}),U(j[b],a,!0,!0)):j[b]=a});a.bundles&&B(a.bundles,function(a,b){v(a,function(a){a!==b&&(aa[a]=b)})});a.shim&&(B(a.shim,function(a,c){H(a)&&(a={deps:a});if((a.exports||a.init)&&!a.exportsFn)a.exportsFn=i.makeShimExports(a);b[c]=a}),j.shim=b);a.packages&&v(a.packages,function(a){var b,
+a="string"===typeof a?{name:a}:a;b=a.name;a.location&&(j.paths[b]=a.location);j.pkgs[b]=a.name+"/"+(a.main||"main").replace(ia,"").replace(Q,"")});B(h,function(a,b){!a.inited&&!a.map.unnormalized&&(a.map=p(b))});if(a.deps||a.callback)i.require(a.deps||[],a.callback)},makeShimExports:function(a){return function(){var b;a.init&&(b=a.init.apply(ba,arguments));return b||a.exports&&da(a.exports)}},makeRequire:function(a,e){function j(c,d,m){var n,q;e.enableBuildCallback&&(d&&G(d))&&(d.__requireJsBuild=
+!0);if("string"===typeof c){if(G(d))return w(C("requireargs","Invalid require call"),m);if(a&&t(L,c))return L[c](h[a.id]);if(g.get)return g.get(i,c,a,j);n=p(c,a,!1,!0);n=n.id;return!t(r,n)?w(C("notloaded",'Module name "'+n+'" has not been loaded yet for context: '+b+(a?"":". Use require([])"))):r[n]}J();i.nextTick(function(){J();q=s(p(null,a));q.skipMap=e.skipMap;q.init(c,d,m,{enabled:!0});D()});return j}e=e||{};U(j,{isBrowser:z,toUrl:function(b){var d,e=b.lastIndexOf("."),k=b.split("/")[0];if(-1!==
+e&&(!("."===k||".."===k)||1e.attachEvent.toString().indexOf("[native code"))&&!Y?(M=!0,e.attachEvent("onreadystatechange",b.onScriptLoad)):
+(e.addEventListener("load",b.onScriptLoad,!1),e.addEventListener("error",b.onScriptError,!1)),e.src=d,J=e,D?y.insertBefore(e,D):y.appendChild(e),J=null,e;if(ea)try{importScripts(d),b.completeLoad(c)}catch(m){b.onError(C("importscripts","importScripts failed for "+c+" at "+d,m,[c]))}};z&&!q.skipDataMain&&T(document.getElementsByTagName("script"),function(b){y||(y=b.parentNode);if(I=b.getAttribute("data-main"))return s=I,q.baseUrl||(E=s.split("/"),s=E.pop(),O=E.length?E.join("/")+"/":"./",q.baseUrl=
+O),s=s.replace(Q,""),g.jsExtRegExp.test(s)&&(s=I),q.deps=q.deps?q.deps.concat(s):[s],!0});define=function(b,c,d){var e,g;"string"!==typeof b&&(d=c,c=b,b=null);H(c)||(d=c,c=null);!c&&G(d)&&(c=[],d.length&&(d.toString().replace(ka,"").replace(la,function(b,d){c.push(d)}),c=(1===d.length?["require"]:["require","exports","module"]).concat(c)));if(M){if(!(e=J))N&&"interactive"===N.readyState||T(document.getElementsByTagName("script"),function(b){if("interactive"===b.readyState)return N=b}),e=N;e&&(b||
+(b=e.getAttribute("data-requiremodule")),g=F[e.getAttribute("data-requirecontext")])}(g?g.defQueue:R).push([b,c,d])};define.amd={jQuery:!0};g.exec=function(b){return eval(b)};g(q)}})(this);
diff --git a/test/test.html b/test/test.html
index 89c475b..eb5dd2c 100644
--- a/test/test.html
+++ b/test/test.html
@@ -3,9 +3,22 @@
+
+
+
+
-
@@ -43,26 +56,30 @@
diff --git a/test/test_build.html b/test/test_build.html
new file mode 100644
index 0000000..1bce0d3
--- /dev/null
+++ b/test/test_build.html
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Switch editor mode using the mode box.
+ Note that the mode can be changed programmatically as well using the method
+ editor.setMode(mode), try it in the console of your browser.
+