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. +

+ +
+ + + +