diff --git a/NOTICE b/NOTICE index f6698b2..53ca4b0 100644 --- a/NOTICE +++ b/NOTICE @@ -1,7 +1,7 @@ JSON Editor Online http://jsoneditoronline.org -Copyright (C) 2011-2012 Jos de Jong +Copyright (C) 2011-2013 Jos de Jong Licensed under the Apache License, Version 2.0 (the "License"); diff --git a/app/web/app.js b/app/web/app.js index 8902d3a..c2b43c3 100644 --- a/app/web/app.js +++ b/app/web/app.js @@ -23,10 +23,10 @@ * License for the specific language governing permissions and limitations under * the License. * - * Copyright (C) 2011-2012 Jos de Jong, http://jsoneditoronline.org + * Copyright (C) 2011-2013 Jos de Jong, http://jsoneditoronline.org * * @author Jos de Jong, - * @date 2012-11-03 + * @date 2013-01-01 */ diff --git a/app/web/fileretriever.js b/app/web/fileretriever.js index f3ef1a2..723a941 100644 --- a/app/web/fileretriever.js +++ b/app/web/fileretriever.js @@ -57,10 +57,10 @@ * License for the specific language governing permissions and limitations under * the License. * - * Copyright (c) 2012 Jos de Jong, http://jsoneditoronline.org + * Copyright (c) 2013 Jos de Jong, http://jsoneditoronline.org * * @author Jos de Jong, - * @date 2012-11-03 + * @date 2013-01-01 */ var FileRetriever = function (options) { // set options and variables diff --git a/app/web/index.html b/app/web/index.html index 9655546..56c1bc0 100644 --- a/app/web/index.html +++ b/app/web/index.html @@ -34,10 +34,10 @@ License for the specific language governing permissions and limitations under the License. - Copyright (C) 2011-2012 Jos de Jong, http://jsoneditoronline.org + Copyright (C) 2011-2013 Jos de Jong, http://jsoneditoronline.org @author Jos de Jong, - @date 2012-11-03 + @date 2013-01-01 --> @@ -165,7 +165,7 @@ • Data policy • - Copyright 2011-2012 Jos de Jong + Copyright 2011-2013 Jos de Jong diff --git a/app/web/test.html b/app/web/test.html index 3c5c00f..192c73c 100644 --- a/app/web/test.html +++ b/app/web/test.html @@ -33,10 +33,10 @@ License for the specific language governing permissions and limitations under the License. - Copyright (C) 2011-2012 Jos de Jong, http://jsoneditoronline.org + Copyright (C) 2011-2013 Jos de Jong, http://jsoneditoronline.org @author Jos de Jong, - @date 2012-11-03 + @date 2013-01-01 --> @@ -47,12 +47,23 @@ - + + + + - + + + + + + + + + @@ -63,10 +74,10 @@ @@ -191,7 +202,7 @@ • Data policy • - Copyright 2011-2012 Jos de Jong + Copyright 2011-2013 Jos de Jong diff --git a/build.xml b/build.xml index f508471..0d0ed8d 100644 --- a/build.xml +++ b/build.xml @@ -9,11 +9,14 @@ - - - - - + + + + + + + + @@ -22,43 +25,72 @@ - - - - - - - - + + + + + + + + - - - - - - + + + + + + + + + + jsoneditor.js + + + + jsoneditor.css + + + + + + + + + + + + + + + + + + + + + - + - + - + - + - + - + @@ -94,9 +126,9 @@ - - - + + + diff --git a/jsoneditor/css/contextmenu.css b/jsoneditor/css/contextmenu.css new file mode 100644 index 0000000..87f1c21 --- /dev/null +++ b/jsoneditor/css/contextmenu.css @@ -0,0 +1,219 @@ + +/* _______________________________ MAIN MENU ________________________________ */ + +div.jsoneditor-contextmenu { + position: absolute; +} + +div.jsoneditor-contextmenu ul { + position: relative; + left: 0; + top: 0; + width: 124px; + + background: white; + border: 1px solid #d3d3d3; + box-shadow: 2px 2px 12px rgba(128, 128, 128, 0.3); + + z-index: 1; + list-style: none; + margin: 0; + padding: 0; +} + +div.jsoneditor-contextmenu ul li button { + padding: 0; + margin: 0; + width: 124px; + height: 24px; + border: none; + cursor: pointer; + color: #4d4d4d; + background: transparent; + + line-height: 24px; + text-align: left; +} + +/* Fix button padding in firefox */ +div.jsoneditor-contextmenu ul li button::-moz-focus-inner { + padding: 0; + border: 0; +} + +div.jsoneditor-contextmenu ul li button:hover { + color: #1a1a1a; + background-color: #f5f5f5; +} + +div.jsoneditor-contextmenu ul li button.default { + width: 92px; +} + +div.jsoneditor-contextmenu ul li button.expand { + float: right; + width: 32px; + height: 24px; + border-left: 1px solid #e5e5e5; +} + +div.jsoneditor-contextmenu div.icon { + float: left; + width: 24px; + height: 24px; + border: none; + padding: 0; + margin: 0; + background-image: url('img/jsoneditor-icons.png'); +} + +div.jsoneditor-contextmenu ul li button div.expand { + float: right; + width: 24px; + height: 24px; + padding: 0; + margin: 0 4px 0 0; + background: url('img/jsoneditor-icons.png') 0 -72px; + opacity: 0.4; +} + +div.jsoneditor-contextmenu ul li button:hover div.expand, +div.jsoneditor-contextmenu ul li.selected div.expand, +div.jsoneditor-contextmenu ul li button.expand:hover div.expand { + opacity: 1; +} + +div.jsoneditor-contextmenu .separator { + height: 0; + border-top: 1px solid #e5e5e5; + padding-top: 5px; + margin-top: 5px; +} + +button.jsoneditor-remove > .icon { + background-position: -24px -24px; +} +button.jsoneditor-remove:hover > .icon { + background-position: -24px 0; +} + +button.jsoneditor-append > .icon { + background-position: 0 -24px; +} +button.jsoneditor-append:hover > .icon { + background-position: 0 0; +} + +button.jsoneditor-insert > .icon { + background-position: 0 -24px; +} +button.jsoneditor-insert:hover > .icon { + background-position: 0 0; +} + +button.jsoneditor-insert-above > .icon { + background-position: -24px -24px; +} +button.jsoneditor-insert-above:hover > .icon { + background-position: -24px 0; +} + +button.jsoneditor-insert-below > .icon { + background-position: -48px -24px; +} +button.jsoneditor-insert-below:hover > .icon { + background-position: -48px 0; +} + +button.jsoneditor-duplicate > .icon { + background-position: -48px -24px; +} +button.jsoneditor-duplicate:hover > .icon { + background-position: -48px 0; +} + +button.jsoneditor-sort-asc > .icon { + background-position: -168px -24px; +} +button.jsoneditor-sort-asc:hover > .icon { + background-position: -168px 0; +} + +button.jsoneditor-sort-desc > .icon { + background-position: -192px -24px; +} +button.jsoneditor-sort-desc:hover > .icon { + background-position: -192px 0; +} + +/* ____________________________ CONTEXT SUB MENU ____________________________ */ + + +div.jsoneditor-contextmenu ul li ul li .selected { + background-color: #D5DDF6; +} + +div.jsoneditor-contextmenu ul li { + overflow: hidden; +} + +div.jsoneditor-contextmenu ul li ul { + display: none; + position: relative; + left: -10px; + top: 0; + + border: none; + box-shadow: inset 0 0 10px rgba(128, 128, 128, 0.5); + padding: 0 10px; + + /* TODO: transition is not supported on IE8-9 */ + -webkit-transition: all 0.3s ease-out; + -moz-transition: all 0.3s ease-out; + -o-transition: all 0.3s ease-out; + transition: all 0.3s ease-out; +} + +div.jsoneditor-contextmenu ul li.selected ul { +} + +div.jsoneditor-contextmenu ul li ul li button { + padding-left: 24px; +} + +div.jsoneditor-contextmenu ul li ul li button:hover { + background-color: #f5f5f5; + +} + +button.jsoneditor-type-string > .icon { + background-position: -144px -24px; +} +button.jsoneditor-type-string:hover > .icon, +button.jsoneditor-type-string.selected > .icon{ + background-position: -144px 0; +} + +button.jsoneditor-type-auto > .icon { + background-position: -120px -24px; +} +button.jsoneditor-type-auto:hover > .icon, +button.jsoneditor-type-auto.selected > .icon { + background-position: -120px 0; +} + +button.jsoneditor-type-object > .icon { + background-position: -72px -24px; +} +button.jsoneditor-type-object:hover > .icon, +button.jsoneditor-type-object.selected > .icon{ + background-position: -72px 0; +} + +button.jsoneditor-type-array > .icon { + background-position: -96px -24px; +} +button.jsoneditor-type-array:hover > .icon, +button.jsoneditor-type-array.selected > .icon{ + background-position: -96px 0; +} diff --git a/jsoneditor/img/description.txt b/jsoneditor/css/img/description.txt similarity index 100% rename from jsoneditor/img/description.txt rename to jsoneditor/css/img/description.txt diff --git a/jsoneditor/img/export.sh b/jsoneditor/css/img/export.sh similarity index 100% rename from jsoneditor/img/export.sh rename to jsoneditor/css/img/export.sh diff --git a/jsoneditor/img/jsoneditor-icons.png b/jsoneditor/css/img/jsoneditor-icons.png similarity index 100% rename from jsoneditor/img/jsoneditor-icons.png rename to jsoneditor/css/img/jsoneditor-icons.png diff --git a/jsoneditor/img/jsoneditor-icons.svg b/jsoneditor/css/img/jsoneditor-icons.svg similarity index 100% rename from jsoneditor/img/jsoneditor-icons.svg rename to jsoneditor/css/img/jsoneditor-icons.svg diff --git a/jsoneditor/css/jsoneditor.css b/jsoneditor/css/jsoneditor.css new file mode 100644 index 0000000..1bbff5a --- /dev/null +++ b/jsoneditor/css/jsoneditor.css @@ -0,0 +1,235 @@ + +.jsoneditor-field, .jsoneditor-value, .jsoneditor-field-readonly, .jsoneditor-readonly { + border: 1px solid transparent; + min-height: 16px; + min-width: 24px; + padding: 2px; + margin: 1px; + outline: none; + word-wrap: break-word; + float: left; +} + +/* adjust margin of p elements inside editable divs, needed for Opera, IE */ +.jsoneditor-field p, .jsoneditor-value p { + margin: 0; +} + +.jsoneditor-value { + word-break: break-word; +} + +.jsoneditor-empty { + background-color: #E5E5E5; + border-radius: 2px; +} + +.jsoneditor-separator { + padding: 3px 0; + vertical-align: top; +} + +.jsoneditor-value:focus, .jsoneditor-field:focus, + .jsoneditor-value:hover, .jsoneditor-field:hover, + .jsoneditor-search-highlight { + background-color: #FFFFAB; + border: 1px solid yellow; + border-radius: 2px; +} + +.jsoneditor-search-highlight-active, + .jsoneditor-search-highlight-active:focus, + .jsoneditor-search-highlight-active:hover { + background-color: #ffee00; + border: 1px solid #ffc700; + border-radius: 2px; +} + +.jsoneditor-field-readonly:hover { + border: 1px solid white; +} + +.jsoneditor-readonly { + color: gray; +} + +button.jsoneditor-collapsed, button.jsoneditor-expanded, + button.jsoneditor-invisible, button.jsoneditor-dragarea, + button.jsoneditor-contextmenu, button.jsoneditor-append { + width: 24px; + height: 24px; + padding: 0; + margin: 0; + border: none; + cursor: pointer; + background-color: transparent; + background-image: url('img/jsoneditor-icons.png'); +} + +/* TODO: no global settings +button:disabled { + color: #808080; +} +*/ + +td.jsoneditor-nochilds { + color: gray; +} + +button.jsoneditor-collapsed { + background-position: 0 -48px; +} + +button.jsoneditor-expanded { + background-position: 0 -72px; +} + +.jsoneditor-contextmenu { + position: relative; + background-position: -48px -72px; +} + +.jsoneditor-contextmenu:hover, .jsoneditor-contextmenu:focus { + background-position: -48px -48px; +} + +button.jsoneditor-invisible { + visibility: hidden; + background: none; +} + +button.jsoneditor-collapsed, button.jsoneditor-expanded, +button.jsoneditor-invisible { + float: left; +} + +div.jsoneditor-frame { + color: #1A1A1A; + border: 1px solid #97B0F8; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; + width: 100%; + height: 100%; + overflow: auto; + position: relative; + padding: 0; +} + +table.jsoneditor-table { + border-collapse: collapse; + border-spacing: 0; + width: 100%; + margin: 0; +} + +div.jsoneditor-content-outer, div.jsonformatter-content { + width: 100%; + height: 100%; + margin: -35px 0 0 0; + padding: 35px 0 0 0; + + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; + + overflow: hidden; +} + +div.jsoneditor-content { + width: 100%; + height: 100%; + position: relative; + overflow: auto; +} + +textarea.jsonformatter-textarea { + width: 100%; + height: 100%; + margin: 0; + + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; + + border: none; + background-color: white; + resize: none; +} + +tr.jsoneditor-tr-highlight { + background-color: #FFFFAB; +} + +button.jsoneditor-dragarea { + width: 24px; + height: 24px; + /* + margin: 3px 0; + background: url('img/dots_gray.gif') top center; + background-repeat: repeat-y; + */ + background: url('img/jsoneditor-icons.png') -72px -72px; + + display: block; + cursor: move; +} + +button.jsoneditor-dragarea:hover, .jsoneditor-dragarea:focus { + background-position: -72px -48px; +} + +/* TODO: do not change global tr, th, td */ +tr, th, td { + padding: 0; + margin: 0; +} + +td.jsoneditor-td { + vertical-align: top; +} + +td.jsoneditor-td { + padding: 0; +} + +td.jsoneditor-td-edit { + background-color: #F5F5F5; + padding: 0; +} + +td.jsoneditor-td-tree { + vertical-align: top; +} + +td.jsoneditor-droparea { + height: 24px; + + border-top: 1px dashed gray; + border-bottom: 1px dashed gray; + background-color: #FFFF80; +} + +.jsoneditor-field, .jsoneditor-value, .jsoneditor-td, .jsoneditor-th, + .jsoneditor-type, .jsonformatter-textarea { + font-family: droid sans mono, monospace, courier new, courier, sans-serif; + font-size: 10pt; + color: #1A1A1A; +} + +div.jsoneditor-contextmenu button, + input.jsoneditor-search, div.jsoneditor-search-results { + font-family: arial, sans-serif; + font-size: 10pt; + color: #1A1A1A; +} + +.jsoneditor-hidden-focus { + position: absolute; + left: -1000px; + top: -1000px; + border: none; + outline: none; +} + +/* TODO: drastically cleanup the css, improve/simplify naming and cascading */ \ No newline at end of file diff --git a/jsoneditor/css/menu.css b/jsoneditor/css/menu.css new file mode 100644 index 0000000..68a26b8 --- /dev/null +++ b/jsoneditor/css/menu.css @@ -0,0 +1,64 @@ + +/* __________________________ JSONEDITOR TOP MENU ___________________________ */ + + +div.jsoneditor-menu { + width: 100%; + height: 35px; + padding: 2px; + margin: 0; + overflow: hidden; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; + + color: #1A1A1A; + background-color: #D5DDF6; + border-bottom: 1px solid #97B0F8; +} + +button.jsoneditor-menu { + width: 26px; + height: 26px; + margin: 2px; + padding: 2px; + border-radius: 2px; + border: 1px solid #aec0f8; + background: #e3eaf6 url('img/jsoneditor-icons.png'); +} + +button.jsoneditor-menu:hover { + background-color: #f0f2f5; +} +button.jsoneditor-menu:active { + background-color: #ffffff; +} +button.jsoneditor-menu:disabled { + background-color: #e3eaf6; +} + +button.jsoneditor-collapse-all { + background-position: 0 -96px; +} +button.jsoneditor-expand-all { + background-position: 0 -120px; +} +button.jsoneditor-undo { + background-position: -24px -96px; +} +button.jsoneditor-undo:disabled { + background-position: -24px -120px; +} +button.jsoneditor-redo { + background-position: -48px -96px; +} +button.jsoneditor-redo:disabled { + background-position: -48px -120px; +} +/* TODO: css for button:disabled is not supported by IE8 */ +button.jsoneditor-compact { + background-position: -72px -96px; +} +button.jsoneditor-format { + background-position: -72px -120px; +} diff --git a/jsoneditor/css/searchbox.css b/jsoneditor/css/searchbox.css new file mode 100644 index 0000000..02cf9d0 --- /dev/null +++ b/jsoneditor/css/searchbox.css @@ -0,0 +1,62 @@ + +table.jsoneditor-search { + position: absolute; + right: 2px; + top: 2px; +} + +table.jsoneditor-search-input { + border-collapse: collapse; +} + +div.jsoneditor-search { + border: 1px solid #97B0F8; + background-color: white; + padding: 0 2px; + margin: 0; +} + +input.jsoneditor-search { + width: 120px; + border: none; + outline: none; + margin: 1px; +} + +div.jsoneditor-search-results { + color: #4d4d4d; + padding-right: 5px; +} + +button.jsoneditor-search-refresh, button.jsoneditor-search-next, +button.jsoneditor-search-previous { + width: 16px; + height: 24px; + padding: 0; + margin: 0; + border: none; + background: url('img/jsoneditor-icons.png'); + vertical-align: top; +} + +button.jsoneditor-search-refresh { + width: 18px; + background-position: -99px -73px; +} + +button.jsoneditor-search-next { + cursor: pointer; + background-position: -124px -73px; +} +button.jsoneditor-search-next:hover { + background-position: -124px -49px; +} + +button.jsoneditor-search-previous { + cursor: pointer; + background-position: -148px -73px; + margin-right: 2px; +} +button.jsoneditor-search-previous:hover { + background-position: -148px -49px; +} diff --git a/demo.html b/jsoneditor/examples/demo.html similarity index 86% rename from demo.html rename to jsoneditor/examples/demo.html index 931e582..f342663 100644 --- a/demo.html +++ b/jsoneditor/examples/demo.html @@ -1,8 +1,8 @@ - - + + diff --git a/jsoneditor/js/appendnode.js b/jsoneditor/js/appendnode.js new file mode 100644 index 0000000..db4b78c --- /dev/null +++ b/jsoneditor/js/appendnode.js @@ -0,0 +1,224 @@ +/** + * @license + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * Copyright (c) 2011-2013 Jos de Jong, http://jsoneditoronline.org + * + * @author Jos de Jong, + */ + + +/** + * @constructor JSONEditor.AppendNode + * @extends JSONEditor.Node + * @param {JSONEditor} 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 + */ +JSONEditor.AppendNode = function (editor) { + this.editor = editor; + this.dom = {}; +}; + +JSONEditor.AppendNode.prototype = new JSONEditor.Node(); + +/** + * Return a table row with an append button. + * @return {Element} dom TR element + */ +JSONEditor.AppendNode.prototype.getDom = function () { + // TODO: do not create the DOM for the appendNode when in viewer mode + // 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; + + // when in viewer mode, don't create the contents for the append node + // but return here. + if (!this.editor.editable) { + return trAppend; + } + + // TODO: consistent naming + + // a cell for the dragarea column + var tdDrag = document.createElement('td'); + tdDrag.className = 'jsoneditor-td'; + dom.tdDrag = tdDrag; + + // create context menu + var tdMenu = document.createElement('td'); + tdMenu.className = 'jsoneditor-td'; + var menu = document.createElement('button'); + menu.className = 'jsoneditor-contextmenu'; + dom.menu = menu; + dom.tdMenu = tdMenu; + 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 = 'jsoneditor-readonly'; + tdAppend.appendChild(domText); + tdAppend.className = 'jsoneditor-td'; + dom.td = tdAppend; + dom.text = domText; + + this.updateDom(); + + return trAppend; +}; + +/** + * Update the HTML dom of the Node + */ +JSONEditor.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) { + trAppend.removeChild(dom.tdDrag); + trAppend.removeChild(dom.tdMenu); + trAppend.removeChild(tdAppend); + } + } + else { + if (!dom.tr.firstChild) { + trAppend.appendChild(dom.tdDrag); + 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 + */ +JSONEditor.AppendNode.prototype.isVisible = function () { + return (this.parent.childs.length == 0); +}; + +/** + * Show a contextmenu for this node + * @param {function} [onClose] Callback method called when the context menu + * is being closed. + */ +JSONEditor.AppendNode.prototype.showContextMenu = function (onClose) { + var node = this; + var titles = JSONEditor.Node.TYPE_TITLES; + var items = [ + // create append button + { + 'text': 'Append', + 'title': 'Append a new node with type \'auto\'', + 'submenuTitle': 'Select the type of the node to be appended', + 'className': 'jsoneditor-insert', + 'click': function () { + node._onAppend('field', 'value', 'auto'); + }, + 'submenu': [ + { + 'text': 'Auto', + 'className': 'jsoneditor-type-auto', + 'title': titles.auto, + 'click': function () { + node._onAppend('field', 'value', 'auto'); + } + }, + { + 'text': 'Array', + 'className': 'jsoneditor-type-array', + 'title': titles.array, + 'click': function () { + node._onAppend('field', []); + } + }, + { + 'text': 'Object', + 'className': 'jsoneditor-type-object', + 'title': titles.object, + 'click': function () { + node._onAppend('field', {}); + } + }, + { + 'text': 'String', + 'className': 'jsoneditor-type-string', + 'title': titles.string, + 'click': function () { + // TODO: settings type string does not work, will become auto + node._onAppend('field', 'value', 'string'); + } + } + ] + } + ]; + + var menu = new JSONEditor.ContextMenu(items, {close: onClose}); + menu.show(this.dom.menu); +}; + +/** + * Handle an event. The event is catched centrally by the editor + * @param {Event} event + */ +JSONEditor.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(); + this.showContextMenu(function () { + highlighter.unlock(); + highlighter.unhighlight(); + }); + } +}; diff --git a/jsoneditor/js/contextmenu.js b/jsoneditor/js/contextmenu.js new file mode 100644 index 0000000..ba9fdc6 --- /dev/null +++ b/jsoneditor/js/contextmenu.js @@ -0,0 +1,272 @@ +/** + * @license + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * Copyright (c) 2011-2013 Jos de Jong, http://jsoneditoronline.org + * + * @author Jos de Jong, + */ + +/** + * 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 + */ +JSONEditor.ContextMenu = function (items, options) { + var me = this; + this.items = items; + this.eventListeners = {}; + this.visibleSubmenu = undefined; + this.onClose = options ? options.close : undefined; + + // create a container element + var menu = document.createElement('div'); + menu.className = 'jsoneditor-contextmenu'; + this.menu = menu; + + // create a list to hold the menu items + var list = document.createElement('ul'); + list.className = 'menu'; + menu.appendChild(list); + this.list = list; + + function createMenuItems (list, 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 { + // 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; + 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'); + 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 + var selected = false; + buttonSubmenu.onclick = function () { + me._onShowSubmenu(submenu); + }; + + // create the submenu + var submenu = document.createElement('ul'); + submenu.className = 'menu'; + submenu.style.height = '0'; + li.appendChild(submenu); + createMenuItems(submenu, item.submenu); + } + else { + // no submenu, just a button with clickhandler + button.innerHTML = '
' + item.text; + } + } + }); + } + createMenuItems(list, 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); + }); +}; + +// currently displayed context menu, a singleton. We may only have one visible context menu +JSONEditor.ContextMenu.visibleMenu = undefined; + +/** + * Attach the menu to an anchor + * @param {Element} anchor + */ +JSONEditor.ContextMenu.prototype.show = function (anchor) { + this.hide(); + + // calculate whether the menu fits below the anchor + var windowHeight = JSONEditor.util.getWindowHeight(); + var anchorHeight = anchor.offsetHeight; + var menuHeight = this.maxHeight; + + // position the menu + var left = JSONEditor.util.getAbsoluteLeft(anchor); + var top = JSONEditor.util.getAbsoluteTop(anchor); + if (top + anchorHeight + menuHeight < windowHeight) { + // display the menu below the anchor + this.menu.style.left = left + 'px'; + this.menu.style.top = (top + anchorHeight) + 'px'; + this.menu.style.bottom = ''; + } + else { + // display the menu above the anchor + this.menu.style.left = left + 'px'; + this.menu.style.top = ''; + this.menu.style.bottom = (windowHeight - top) + 'px'; + } + + // attach the menu to the document + document.body.appendChild(this.menu); + + // create and attach event listeners + var me = this; + var list = this.list; + this.eventListeners.mousedown = JSONEditor.util.addEventListener( + document, 'mousedown', function (event) { + // hide menu on click outside of the menu + event = event || window.event; + var target = event.target || event.srcElement; + if (!JSONEditor.isChildOf(target, list)) { + me.hide(); + } + }); + this.eventListeners.mousewheel = JSONEditor.util.addEventListener( + document, 'mousewheel', function () { + // hide the menu on mouse scroll + me.hide(); + }); + this.eventListeners.keydown = JSONEditor.util.addEventListener( + document, 'keydown', function (event) { + // hide the menu on ESC key + event = event || window.event; + var keynum = event.which || event.keyCode; + if (keynum == 27) { // ESC + me.hide(); + JSONEditor.util.stopPropagation(event); + JSONEditor.util.preventDefault(event); + } + }); + + // TODO: focus to the first button in the context menu + + if (JSONEditor.ContextMenu.visibleMenu) { + JSONEditor.ContextMenu.visibleMenu.hide(); + } + JSONEditor.ContextMenu.visibleMenu = this; +}; + +/** + * Hide the context menu if visible + */ +JSONEditor.ContextMenu.prototype.hide = function () { + // remove the menu from the DOM + if (this.menu.parentNode) { + this.menu.parentNode.removeChild(this.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) { + JSONEditor.util.removeEventListener(document, name, fn); + } + delete this.eventListeners[name]; + } + } +}; + +/** + * Show or hide a submenu. + * Any currently visible submenu will be hided. + * @param {Element} submenu + * @private + */ +JSONEditor.ContextMenu.prototype._onShowSubmenu = function (submenu) { + var me = this; + var alreadyVisible = (submenu == this.visibleSubmenu); + + // hide the currently visible submenu + var visibleSubmenu = this.visibleSubmenu; + if (visibleSubmenu) { + visibleSubmenu.style.height = '0'; + visibleSubmenu.style.padding = ''; + setTimeout(function () { + if (me.visibleSubmenu != visibleSubmenu) { + visibleSubmenu.style.display = ''; + JSONEditor.util.removeClassName(visibleSubmenu.parentNode, 'selected'); + } + }, 300); // timeout duration must match the css transition duration + this.visibleSubmenu = undefined; + } + + if (!alreadyVisible) { + submenu.style.display = 'block'; + var height = submenu.clientHeight; // force a reflow in Firefox + setTimeout(function () { + if (me.visibleSubmenu == submenu) { + submenu.style.height = (submenu.childNodes.length * 24) + 'px'; + submenu.style.padding = '5px 10px'; + } + }, 0); + JSONEditor.util.addClassName(submenu.parentNode, 'selected'); + this.visibleSubmenu = submenu; + } +}; diff --git a/jsoneditor/js/highlighter.js b/jsoneditor/js/highlighter.js new file mode 100644 index 0000000..011041c --- /dev/null +++ b/jsoneditor/js/highlighter.js @@ -0,0 +1,102 @@ +/** + * @license + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * Copyright (c) 2011-2013 Jos de Jong, http://jsoneditoronline.org + * + * @author Jos de Jong, + */ + + +/** + * The highlighter can highlight/unhighlight a node, and + * animate the visibility of a context menu. + * @constructor JSONEditor.Highlighter + */ +JSONEditor.Highlighter = function () { + this.locked = false; +}; + +/** + * Hightlight given node and its childs + * @param {JSONEditor.Node} node + */ +JSONEditor.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 + */ +JSONEditor.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 + */ +JSONEditor.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. + */ +JSONEditor.Highlighter.prototype.lock = function () { + this.locked = true; +}; + +/** + * Unlock highlighting or unhighlighting nodes + */ +JSONEditor.Highlighter.prototype.unlock = function () { + this.locked = false; +}; diff --git a/jsoneditor/js/history.js b/jsoneditor/js/history.js new file mode 100644 index 0000000..4573a61 --- /dev/null +++ b/jsoneditor/js/history.js @@ -0,0 +1,233 @@ +/** + * @license + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * Copyright (c) 2011-2013 Jos de Jong, http://jsoneditoronline.org + * + * @author Jos de Jong, + */ + +/** + * @constructor JSONEditor.History + * Store action history, enables undo and redo + * @param {JSONEditor} editor + */ +JSONEditor.History = function (editor) { + this.editor = editor; + this.clear(); + + // map with all supported actions + this.actions = { + 'editField': { + 'undo': function (obj) { + obj.params.node.updateField(obj.params.oldValue); + }, + 'redo': function (obj) { + obj.params.node.updateField(obj.params.newValue); + } + }, + 'editValue': { + 'undo': function (obj) { + obj.params.node.updateValue(obj.params.oldValue); + }, + 'redo': function (obj) { + obj.params.node.updateValue(obj.params.newValue); + } + }, + 'appendNode': { + 'undo': function (obj) { + obj.params.parent.removeChild(obj.params.node); + }, + 'redo': function (obj) { + obj.params.parent.appendChild(obj.params.node); + } + }, + 'insertBeforeNode': { + 'undo': function (obj) { + obj.params.parent.removeChild(obj.params.node); + }, + 'redo': function (obj) { + obj.params.parent.insertBefore(obj.params.node, obj.params.beforeNode); + } + }, + 'insertAfterNode': { + 'undo': function (obj) { + obj.params.parent.removeChild(obj.params.node); + }, + 'redo': function (obj) { + obj.params.parent.insertAfter(obj.params.node, obj.params.afterNode); + } + }, + 'removeNode': { + 'undo': function (obj) { + var parent = obj.params.parent; + var beforeNode = parent.childs[obj.params.index] || parent.append; + parent.insertBefore(obj.params.node, beforeNode); + }, + 'redo': function (obj) { + obj.params.parent.removeChild(obj.params.node); + } + }, + 'duplicateNode': { + 'undo': function (obj) { + obj.params.parent.removeChild(obj.params.clone); + }, + 'redo': function (obj) { + obj.params.parent.insertAfter(obj.params.clone, obj.params.node); + } + }, + 'changeType': { + 'undo': function (obj) { + obj.params.node.changeType(obj.params.oldType); + }, + 'redo': function (obj) { + obj.params.node.changeType(obj.params.newType); + } + }, + 'moveNode': { + 'undo': function (obj) { + obj.params.startParent.moveTo(obj.params.node, obj.params.startIndex); + }, + 'redo': function (obj) { + obj.params.endParent.moveTo(obj.params.node, obj.params.endIndex); + } + }, + 'sort': { + 'undo': function (obj) { + var node = obj.params.node; + node.hideChilds(); + node.sort = obj.params.oldSort; + node.childs = obj.params.oldChilds; + node.showChilds(); + }, + 'redo': function (obj) { + var node = obj.params.node; + node.hideChilds(); + node.sort = obj.params.newSort; + node.childs = obj.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. + */ +JSONEditor.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. + */ +JSONEditor.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 + */ +JSONEditor.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 + */ +JSONEditor.History.prototype.canUndo = function () { + return (this.index >= 0); +}; + +/** + * Check if there is an action available for redo + * @return {Boolean} canRedo + */ +JSONEditor.History.prototype.canRedo = function () { + return (this.index < this.history.length - 1); +}; + +/** + * Undo the last action + */ +JSONEditor.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); + } + else { + console.log('Error: unknown action "' + obj.action + '"'); + } + } + this.index--; + + // fire onchange event + this.onChange(); + } +}; + +/** + * Redo the last action + */ +JSONEditor.History.prototype.redo = function () { + if (this.canRedo()) { + this.index++; + + var obj = this.history[this.index]; + if (obj) { + if (obj) { + var action = this.actions[obj.action]; + if (action && action.redo) { + action.redo(obj); + } + else { + console.log('Error: unknown action "' + obj.action + '"'); + } + } + } + + // fire onchange event + this.onChange(); + } +}; diff --git a/jsoneditor/js/jsoneditor.js b/jsoneditor/js/jsoneditor.js new file mode 100644 index 0000000..aa61068 --- /dev/null +++ b/jsoneditor/js/jsoneditor.js @@ -0,0 +1,786 @@ +/*! + * @file jsoneditor.js + * + * @brief + * JSONEditor is a web-based tool to view, edit, and format JSON. + * It shows data a clear, editable treeview. + * + * Supported browsers: Chrome, Firefox, Safari, Opera, Internet Explorer 8+ + * + * @license + * This json editor is open sourced with the intention to use the editor as + * a component in your own application. Not to just copy and monetize the editor + * as it is. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * Copyright (c) 2011-2013 Jos de Jong, http://jsoneditoronline.org + * + * @author Jos de Jong, + * @date 2013-01-01 + */ + + +// Internet Explorer 8 and older does not support Array.indexOf, +// so we define it here in that case +// http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/ +if(!Array.prototype.indexOf) { + Array.prototype.indexOf = function(obj){ + for(var i = 0; i < this.length; i++){ + if(this[i] == obj){ + return i; + } + } + return -1; + } +} + +// Internet Explorer 8 and older does not support Array.forEach, +// so we define it here in that case +// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach +if (!Array.prototype.forEach) { + Array.prototype.forEach = function(fn, scope) { + for(var i = 0, len = this.length; i < len; ++i) { + fn.call(scope || this, this[i], i, this); + } + } +} + +// define variable JSON, needed for correct error handling on IE7 and older +var JSON; + +/** + * JSONEditor + * @param {Element} container Container element + * @param {Object} [options] Object with options. available options: + * {String} mode Editor mode. Available values: + * 'editor' (default), 'viewer'. + * {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 + */ +var JSONEditor = function (container, options, json) { + // check availability of JSON parser (not available in IE7 and older) + if (!JSON) { + throw new Error ('Your browser does not support JSON. \n\n' + + 'Please install the newest version of your browser.\n' + + '(all modern browsers support JSON).'); + } + + if (!container) { + throw new Error('No container element provided.'); + } + this.container = container; + this.dom = {}; + this.highlighter = new JSONEditor.Highlighter(); + + this._setOptions(options); + + if (this.options.history && this.editable) { + this.history = new JSONEditor.History(this); + } + + this._createFrame(); + this._createTable(); + + this.set(json || {}); +}; + +/** + * Initialize and set default options + * @param {Object} [options] Object with options. available options: + * {String} mode Editor mode. Available values: + * 'editor' (default), 'viewer'. + * {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. + * @private + */ +JSONEditor.prototype._setOptions = function (options) { + this.options = { + search: true, + history: true, + mode: 'editor', + 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']; + console.log('WARNING: Option "enableSearch" is deprecated. Use "search" instead.'); + } + if (options['enableSearch']) { + // deprecated since version 1.6.0, 2012-11-03 + this.options.search = options['enableSearch']; + console.log('WARNING: Option "enableHistory" is deprecated. Use "history" instead.'); + } + } + + // interpret the options + this.editable = (this.options.mode != 'viewer'); +}; + +// node currently being edited +JSONEditor.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). + */ +JSONEditor.prototype.set = function (json, name) { + // adjust field name for root node + if (name) { + 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 JSONEditor.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 + */ +JSONEditor.prototype.get = function () { + // remove focus from currently edited node + if (JSONEditor.focusNode) { + JSONEditor.focusNode.blur(); + } + + if (this.node) { + return this.node.getValue(); + } + else { + return undefined; + } +}; + +/** + * Set a field name for the root node. + * @param {String | undefined} name + */ +JSONEditor.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 + */ +JSONEditor.prototype.getName = function () { + return this.options.name; +}; + +/** + * Remove the root node from the editor + */ +JSONEditor.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 {JSONEditor.Node} node + * @private + */ +JSONEditor.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: + * - {JSONEditor.Node} node, + * - {String} elem the dom element name where + * the result is found ('field' or + * 'value') + */ +JSONEditor.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 + */ +JSONEditor.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 + */ +JSONEditor.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. + */ +JSONEditor.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) { + console.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 + */ +JSONEditor.prototype.startAutoScroll = function (mouseY) { + var me = this; + var content = this.content; + var top = JSONEditor.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 + */ +JSONEditor.prototype.stopAutoScroll = function () { + if (this.autoScrollTimer) { + clearTimeout(this.autoScrollTimer); + delete this.autoScrollTimer; + } + if (this.autoScrollStep) { + delete this.autoScrollStep; + } +}; + + +/** + * Set the focus to the JSONEditor. A hidden input field will be created + * which captures key events + */ +// TODO: use the focus method? +JSONEditor.prototype.focus = function () { + /* + if (!this.dom.focus) { + this.dom.focus = document.createElement('input'); + this.dom.focus.className = 'jsoneditor-hidden-focus'; + + var editor = this; + this.dom.focus.onblur = function () { + // remove itself + if (editor.dom.focus) { + var focus = editor.dom.focus; + delete editor.dom.focus; + editor.frame.removeChild(focus); + } + }; + + // attach the hidden input box to the DOM + if (this.frame.firstChild) { + this.frame.insertBefore(this.dom.focus, this.frame.firstChild); + } + else { + this.frame.appendChild(this.dom.focus); + } + } + this.dom.focus.focus(); + */ +}; + +/** + * Adjust the scroll position such that given top position is shown at 1/4 + * of the window height. + * @param {Number} top + */ +JSONEditor.prototype.scrollTo = function (top) { + var content = this.content; + if (content) { + // cancel any running animation + var editor = this; + if (editor.animateTimeout) { + clearTimeout(editor.animateTimeout); + delete editor.animateTimeout; + } + + // 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.animateTimeout = setTimeout(animate, 50); + } + }; + animate(); + } +}; + +/** + * Test if an element is a child of a parent element. + * @param {Element} child + * @param {Element} parent + * @param {boolean} [includeParent] if true (default), the method will return + * true too when the child is the parent. + * @return {boolean} isChild + */ +JSONEditor.isChildOf = function (child, parent, includeParent) { + var e = child; + if (includeParent != false && e == parent) { + return true; + } + + e = e.parentNode; + while (e) { + if (e == parent) { + return true; + } + e = e.parentNode; + } + + return false; +}; + +/** + * Create main frame + * @private + */ +JSONEditor.prototype._createFrame = function () { + // create the frame + this.container.innerHTML = ''; + this.frame = document.createElement('div'); + this.frame.className = 'jsoneditor-frame'; + this.container.appendChild(this.frame); + + // create one global event listener to handle all events from all nodes + var editor = this; + // TODO: move this onEvent to JSONEditor.prototype.onEvent + var onEvent = function (event) { + event = event || window.event; + var target = event.target || event.srcElement; + + if (event.type == 'keydown') { + editor.onKeyDown(event); + } + + var node = JSONEditor.getNodeFromTarget(target); + if (node) { + node.onEvent(event); + } + }; + this.frame.onclick = function (event) { + onEvent(event); + + // prevent default submit action when JSONEditor is located inside a form + JSONEditor.util.preventDefault(event); + }; + 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 + JSONEditor.util.addEventListener(this.frame, 'focus', onEvent, true); + JSONEditor.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 = 'jsoneditor-menu'; + this.frame.appendChild(this.menu); + + // create expand all button + var expandAll = document.createElement('button'); + expandAll.className = 'jsoneditor-menu jsoneditor-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 = 'jsoneditor-menu jsoneditor-collapse-all'; + collapseAll.onclick = function () { + editor.collapseAll(); + }; + this.menu.appendChild(collapseAll); + + // create undo/redo buttons + if (this.history) { + // create separator + var separator = document.createElement('span'); + separator.innerHTML = ' '; + this.menu.appendChild(separator); + + // create undo button + var undo = document.createElement('button'); + undo.className = 'jsoneditor-menu jsoneditor-undo'; + undo.title = 'Undo last action'; + undo.onclick = function () { + editor._onUndo(); + }; + this.menu.appendChild(undo); + this.dom.undo = undo; + + // create redo button + var redo = document.createElement('button'); + redo.className = 'jsoneditor-menu jsoneditor-redo'; + redo.title = 'Redo'; + 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 search box + if (this.options.search) { + this.searchBox = new JSONEditor.SearchBox(this, this.menu); + } +}; + +/** + * Perform an undo action + * @private + */ +JSONEditor.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 + */ +JSONEditor.prototype._onRedo = function () { + if (this.history) { + // redo last action + editor.history.redo(); + + // trigger change callback + if (editor.options.change) { + editor.options.change(); + } + } +}; + +/** + * Event handler for keydown. Handles shortcut keys + * @param {Event} event + */ +JSONEditor.prototype.onKeyDown = function (event) { + var keynum = event.which || event.keyCode; + var ctrlKey = event.ctrlKey; + var shiftKey = event.shiftKey; + var handled = false; + + 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 + if (!shiftKey) { + // select next search result (F3 or Ctrl+G) + this.searchBox.next(); + } + else { + // select previous search result (Shift+F3 or Ctrl+Shift+G) + this.searchBox.previous(); + } + + // set selection to the current + this.searchBox.focusActiveResult(); + + 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) { + JSONEditor.util.preventDefault(event); + JSONEditor.util.stopPropagation(event); + } +}; + +/** + * Create main table + * @private + */ +JSONEditor.prototype._createTable = function () { + var contentOuter = document.createElement('div'); + contentOuter.className = 'jsoneditor-content-outer'; + this.contentOuter = contentOuter; + + this.content = document.createElement('div'); + this.content.className = 'jsoneditor-content'; + contentOuter.appendChild(this.content); + + this.table = document.createElement('table'); + this.table.className = 'jsoneditor-table'; + this.content.appendChild(this.table); + + // IE8 does not handle overflow='auto' correctly. + // Therefore, set overflow to 'scroll' + var ieVersion = JSONEditor.util.getInternetExplorerVersion(); + if (ieVersion == 8) { + this.content.style.overflow = 'scroll'; + } + + // 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'); + 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); +}; + +/** + * Find the node from an event target + * @param {Node} target + * @return {JSONEditor.Node | undefined} node or undefined when not found + */ +JSONEditor.getNodeFromTarget = function (target) { + while (target) { + if (target.node) { + return target.node; + } + target = target.parentNode; + } + + return undefined; +}; + +/** + * 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 + */ +JSONEditor.parse = function (jsonString) { + try { + return JSON.parse(jsonString); + } + catch (err) { + // get a detailed error message using validate + var message = JSONEditor.validate(jsonString) || err; + throw new Error(message); + } +}; + +/** + * 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 + * @return {String | undefined} Returns undefined when the string is valid JSON, + * returns a string with an error message when + * the data is invalid + */ +JSONEditor.validate = function (jsonString) { + var message = undefined; + + try { + if (window.jsonlint) { + window.jsonlint.parse(jsonString); + } + else { + JSON.parse(jsonString); + } + } + catch (err) { + message = '
' + err.toString() + '
'; + if (window.jsonlint) { + message += + '' + + 'validated by jsonlint' + + ''; + } + } + + return message; +}; diff --git a/jsoneditor/js/jsonformatter.js b/jsoneditor/js/jsonformatter.js new file mode 100644 index 0000000..3e0665e --- /dev/null +++ b/jsoneditor/js/jsonformatter.js @@ -0,0 +1,175 @@ +/** + * @license + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * Copyright (c) 2011-2013 Jos de Jong, http://jsoneditoronline.org + * + * @author Jos de Jong, + */ + +/** + * Create a JSONFormatter and attach it to given container + * @constructor JSONFormatter + * @param {Element} container + * @param {Object} [options] Object with options. available options: + * {Number} indentation Number of indentation + * spaces. 4 by default. + * {function} change Callback method + * triggered on change + * @param {JSON | String} [json] initial contents of the formatter + */ +JSONFormatter = function (container, options, json) { + // check availability of JSON parser (not available in IE7 and older) + if (!JSON) { + throw new Error('Your browser does not support JSON. \n\n' + + 'Please install the newest version of your browser.\n' + + '(all modern browsers support JSON).'); + } + + this.container = container; + this.indentation = 4; // number of spaces + + this.width = container.clientWidth; + this.height = container.clientHeight; + + this.frame = document.createElement('div'); + this.frame.className = "jsoneditor-frame"; + this.frame.onclick = function (event) { + // prevent default submit action when JSONFormatter is located inside a form + JSONEditor.util.preventDefault(event); + }; + + // create menu + this.menu = document.createElement('div'); + this.menu.className = 'jsoneditor-menu'; + this.frame.appendChild(this.menu); + + // create format button + var buttonFormat = document.createElement('button'); + //buttonFormat.innerHTML = 'Format'; + buttonFormat.className = 'jsoneditor-menu jsoneditor-format'; + buttonFormat.title = 'Format JSON data, with proper indentation and line feeds'; + //buttonFormat.className = 'jsoneditor-button'; + this.menu.appendChild(buttonFormat); + + // create compact button + var buttonCompact = document.createElement('button'); + //buttonCompact.innerHTML = 'Compact'; + buttonCompact.className = 'jsoneditor-menu jsoneditor-compact'; + buttonCompact.title = 'Compact JSON data, remove all whitespaces'; + //buttonCompact.className = 'jsoneditor-button'; + this.menu.appendChild(buttonCompact); + + this.content = document.createElement('div'); + this.content.className = 'jsonformatter-content'; + this.frame.appendChild(this.content); + + this.textarea = document.createElement('textarea'); + this.textarea.className = "jsonformatter-textarea"; + this.textarea.spellcheck = false; + this.content.appendChild(this.textarea); + + var textarea = this.textarea; + + // read the options + if (options) { + if (options.change) { + // register on change event + if (this.textarea.oninput === null) { + this.textarea.oninput = function () { + options.change(); + } + } + else { + // oninput is undefined. For IE8- + this.textarea.onchange = function () { + options.change(); + } + } + } + if (options.indentation) { + this.indentation = Number(options.indentation); + } + } + + var me = this; + buttonFormat.onclick = function () { + try { + var json = JSONEditor.parse(textarea.value); + textarea.value = JSON.stringify(json, null, me.indentation); + } + catch (err) { + me.onError(err); + } + }; + buttonCompact.onclick = function () { + try { + var json = JSONEditor.parse(textarea.value); + textarea.value = JSON.stringify(json); + } + catch (err) { + me.onError(err); + } + }; + + this.container.appendChild(this.frame); + + // load initial json object or string + if (typeof(json) == 'string') { + this.setText(json); + } + else { + this.set(json); + } +}; + +/** + * This method is executed on error. + * It can be overwritten for each instance of the JSONFormatter + * @param {String} err + */ +JSONFormatter.prototype.onError = function(err) { + // action should be implemented for the instance +}; + +/** + * Set json data in the formatter + * @param {Object} json + */ +JSONFormatter.prototype.set = function(json) { + this.textarea.value = JSON.stringify(json, null, this.indentation); +}; + +/** + * Get json data from the formatter + * @return {Object} json + */ +JSONFormatter.prototype.get = function() { + return JSONEditor.parse(this.textarea.value); +}; + +/** + * Get the text contents of the JSONFormatter + * @return {String} text + */ +JSONFormatter.prototype.getText = function() { + return this.textarea.value; +}; + +/** + * Set the text contents of the JSONFormatter + * @param {String} text + */ +JSONFormatter.prototype.setText = function(text) { + this.textarea.value = text; +}; diff --git a/jsoneditor/js/node.js b/jsoneditor/js/node.js new file mode 100644 index 0000000..84f1b5f --- /dev/null +++ b/jsoneditor/js/node.js @@ -0,0 +1,2400 @@ +/** + * @license + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * Copyright (c) 2011-2013 Jos de Jong, http://jsoneditoronline.org + * + * @author Jos de Jong, + */ + +/** + * @constructor JSONEditor.Node + * Create a new Node + * @param {JSONEditor} editor + * @param {Object} [params] Can contain parameters: + * {string} field + * {boolean} fieldEditable + * {*} value + * {String} type Can have values 'auto', 'array', + * 'object', or 'string'. + */ +JSONEditor.Node = function (editor, params) { + 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 {JSONEditor.Node} parent + */ +JSONEditor.Node.prototype.setParent = function(parent) { + this.parent = parent; +}; + +/** + * Set field + * @param {String} field + * @param {boolean} [fieldEditable] + */ +JSONEditor.Node.prototype.setField = function(field, fieldEditable) { + this.field = field; + this.fieldEditable = (fieldEditable == true); +}; + +/** + * Get field + * @return {String} + */ +JSONEditor.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' + */ +JSONEditor.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 JSONEditor.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 JSONEditor.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); + console.log('check', value, this.value); + } + else { + this.value = value; + } + */ + } +}; + +/** + * Get value. Value is a JSON structure + * @return {*} value + */ +JSONEditor.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 + */ +JSONEditor.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 {JSONEditor.Node} clone + */ +JSONEditor.Node.prototype.clone = function() { + var clone = new JSONEditor.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 + */ +JSONEditor.Node.prototype.expand = function(recurse) { + if (!this.childs) { + return; + } + + // set this node expanded + this.expanded = true; + if (this.dom.expand) { + this.dom.expand.className = 'jsoneditor-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 + */ +JSONEditor.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 = 'jsoneditor-collapsed'; + } + this.expanded = false; +}; + +/** + * Recursively show all childs when they are expanded + */ +JSONEditor.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 + */ +JSONEditor.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 + */ +JSONEditor.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 {JSONEditor.Node} node + */ +JSONEditor.Node.prototype.appendChild = function(node) { + if (this.type == 'array' || this.type == 'object') { + // 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 {JSONEditor.Node} node + * @param {JSONEditor.Node} beforeNode + */ +JSONEditor.Node.prototype.moveBefore = function(node, beforeNode) { + if (this.type == 'array' || this.type == 'object') { + // 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 JSONEditor.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 {JSONEditor.Node} node + * @param {Number} index + */ +JSONEditor.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 {JSONEditor.Node} node + * @param {JSONEditor.Node} beforeNode + */ +JSONEditor.Node.prototype.insertBefore = function(node, beforeNode) { + if (this.type == 'array' || this.type == 'object') { + 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 {JSONEditor.Node} node + * @param {JSONEditor.Node} afterNode + */ +JSONEditor.Node.prototype.insertAfter = function(node, afterNode) { + if (this.type == 'array' || this.type == 'object') { + 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 {JSONEditor.Node[]} results Array with nodes containing the search text + */ +JSONEditor.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.type == 'array' || this.type == 'object') { + // 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 + */ +JSONEditor.Node.prototype.scrollTo = function() { + 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); + } +}; + +/** + * Set focus to the value of this node + * @param {String} [field] The field name of the element to get the focus + * available values: 'field', 'value' + */ +JSONEditor.Node.prototype.focus = function(field) { + if (this.dom.tr && this.dom.tr.parentNode) { + if (field != 'value' && this.fieldEditable) { + var domField = this.dom.field; + if (domField) { + domField.focus(); + } + } + else { + var domValue = this.dom.value; + if (domValue) { + domValue.focus(); + } + } + } +}; + +/** + * Update the values from the DOM field and value of this node + */ +JSONEditor.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 {JSONEditor.Node} node the childNode to be duplicated + * @return {JSONEditor.Node} clone the clone of the node + * @private + */ +JSONEditor.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 {JSONEditor.Node} node + * @return {boolean} containsNode + */ +JSONEditor.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 {JSONEditor.Node} node the childNode to be moved + * @param {JSONEditor.Node} beforeNode node will be inserted before given + * node. If no beforeNode is given, + * the node is appended at the end + * @private + */ +JSONEditor.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 {JSONEditor.Node} node The child node to be removed; + * @return {JSONEditor.Node | undefined} node The removed node on success, + * else undefined + */ +JSONEditor.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 {JSONEditor.Node} node + * @private + */ +JSONEditor.Node.prototype._remove = function (node) { + this.removeChild(node); +}; + +/** + * Change the type of the value of this Node + * @param {String} newType + */ +JSONEditor.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 = index; + } + }); + + 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 + */ +JSONEditor.Node.prototype._getDomValue = function(silent) { + if (this.dom.value && this.type != 'array' && this.type != 'object') { + this.valueInnerText = JSONEditor.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 + }); + } + } + 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 + */ +JSONEditor.Node.prototype._updateDomValue = function () { + var domValue = this.dom.value; + if (domValue) { + // set text color depending on value type + var v = this.value; + var t = (this.type == 'auto') ? typeof(v) : this.type; + var color = ''; + if (t == 'string') { + color = 'green'; + } + else if (t == 'number') { + color = 'red'; + } + else if (t == 'boolean') { + color = 'blue'; + } + else if (this.type == 'object' || this.type == 'array') { + // note: typeof(null)=="object", therefore check this.type instead of t + color = ''; + } + else if (v === null) { + color = 'purple'; + } + else { + // invalid value + color = 'black'; + } + domValue.style.color = color; + + // make backgound color lightgray when empty + var isEmpty = (String(this.value) == '' && this.type != 'array' && this.type != 'object'); + if (isEmpty) { + JSONEditor.util.addClassName(domValue, 'jsoneditor-empty'); + } + else { + JSONEditor.util.removeClassName(domValue, 'jsoneditor-empty'); + } + + // highlight when there is a search result + if (this.searchValueActive) { + JSONEditor.util.addClassName(domValue, 'jsoneditor-search-highlight-active'); + } + else { + JSONEditor.util.removeClassName(domValue, 'jsoneditor-search-highlight-active'); + } + if (this.searchValue) { + JSONEditor.util.addClassName(domValue, 'jsoneditor-search-highlight'); + } + else { + JSONEditor.util.removeClassName(domValue, 'jsoneditor-search-highlight'); + } + + // strip formatting from the contents of the editable div + JSONEditor.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 + */ +JSONEditor.Node.prototype._updateDomField = function () { + var domField = this.dom.field; + if (domField) { + // make backgound color lightgray when empty + var isEmpty = (String(this.field) == ''); + if (isEmpty) { + JSONEditor.util.addClassName(domField, 'jsoneditor-empty'); + } + else { + JSONEditor.util.removeClassName(domField, 'jsoneditor-empty'); + } + + // highlight when there is a search result + if (this.searchFieldActive) { + JSONEditor.util.addClassName(domField, 'jsoneditor-search-highlight-active'); + } + else { + JSONEditor.util.removeClassName(domField, 'jsoneditor-search-highlight-active'); + } + if (this.searchField) { + JSONEditor.util.addClassName(domField, 'jsoneditor-search-highlight'); + } + else { + JSONEditor.util.removeClassName(domField, 'jsoneditor-search-highlight'); + } + + // strip formatting from the contents of the editable div + JSONEditor.util.stripFormatting(domField); + } +}; + +/** + * Retrieve field from DOM + * @param {boolean} [silent] If true (default), no errors will be thrown in + * case of invalid data + * @private + */ +JSONEditor.Node.prototype._getDomField = function(silent) { + if (this.dom.field && this.fieldEditable) { + this.fieldInnerText = JSONEditor.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 + }); + } + } + 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 + */ +JSONEditor.Node.prototype.clearDom = function() { + // TODO: hide the node first? + //this.hide(); + // TOOD: 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 + */ +JSONEditor.Node.prototype.getDom = function() { + var dom = this.dom; + if (dom.tr) { + return dom.tr; + } + + // create row + dom.tr = document.createElement('tr'); + dom.tr.className = 'jsoneditor-tr'; + dom.tr.node = this; + + if (this.editor.editable) { + // create draggable area + var tdDrag = document.createElement('td'); + tdDrag.className = 'jsoneditor-td'; + dom.drag = this._createDomDragArea(); + if (dom.drag) { + tdDrag.appendChild(dom.drag); + } + dom.tr.appendChild(tdDrag); + + // create context menu + var tdMenu = document.createElement('td'); + tdMenu.className = 'jsoneditor-td'; + var menu = document.createElement('button'); + menu.className = 'jsoneditor-contextmenu'; + dom.menu = menu; + tdMenu.appendChild(dom.menu); + dom.tr.appendChild(tdMenu); + } + + // create tree and field + var tdField = document.createElement('td'); + tdField.className = 'jsoneditor-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 + */ +JSONEditor.Node.prototype._onDragStart = function (event) { + event = event || window.event; + + var node = this; + if (!this.mousemove) { + this.mousemove = JSONEditor.util.addEventListener(document, 'mousemove', + function (event) { + node._onDrag(event); + }); + } + + if (!this.mouseup) { + this.mouseup = JSONEditor.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': JSONEditor.util.getMouseX(event), + 'level': this.getLevel() + }; + document.body.style.cursor = 'move'; + + JSONEditor.util.preventDefault(event); +}; + +/** + * Drag event, fired when moving the mouse while dragging a Node + * @param {Event} event + * @private + */ +JSONEditor.Node.prototype._onDrag = function (event) { + // TODO: this method has grown to large. Split it in a number of methods + event = event || window.event; + // TODO: make a separate function to get the absolute mouseY and mouseX + var mouseY = JSONEditor.util.getMouseY(event); + var mouseX = JSONEditor.util.getMouseX(event); + + 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 = JSONEditor.util.getAbsoluteTop(trThis); + heightThis = trThis.offsetHeight; + if (mouseY < topThis) { + // move up + trPrev = trThis; + do { + trPrev = trPrev.previousSibling; + nodePrev = JSONEditor.getNodeFromTarget(trPrev); + topPrev = trPrev ? JSONEditor.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 = JSONEditor.getNodeFromTarget(trPrev); + if (nodePrev == this) { + nodePrev = undefined; + } + } + + if (nodePrev) { + // check if mouseY is really inside the found node + trPrev = nodePrev.dom.tr; + topPrev = trPrev ? JSONEditor.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 = JSONEditor.util.getAbsoluteTop(trFirst); + trNext = trFirst; + do { + nodeNext = JSONEditor.getNodeFromTarget(trNext); + if (trNext) { + bottomNext = trNext.nextSibling ? + JSONEditor.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 = JSONEditor.getNodeFromTarget(trPrev); + if (nodePrev instanceof JSONEditor.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 = JSONEditor.getNodeFromTarget(trPrev); + levelNext = nodeNext.getLevel(); + } + else { + break; + } + } + else if (nodePrev == this) { + // neglect itself + } + else { + break; + } + + trPrev = trPrev.previousSibling; + } + + // move the node when its position is changed + if (this.dom.tr.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); + + JSONEditor.util.preventDefault(event); +}; + +/** + * Drag event, fired on mouseup after having dragged a node + * @param {Event} event + * @private + */ +JSONEditor.Node.prototype._onDragEnd = function (event) { + event = event || window.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) { + JSONEditor.util.removeEventListener(document, 'mousemove', this.mousemove); + delete this.mousemove; + } + if (this.mouseup) { + JSONEditor.util.removeEventListener(document, 'mouseup', this.mouseup); + delete this.mouseup; + } + + // Stop any running auto scroll + this.editor.stopAutoScroll(); + + JSONEditor.util.preventDefault(event); +}; + +/** + * Create a drag area, displayed at the left side of the node + * @return {Element | undefined} domDrag + * @private + */ +JSONEditor.Node.prototype._createDomDragArea = function () { + if (!this.parent) { + return undefined; + } + + var domDrag = document.createElement('button'); + domDrag.className = 'jsoneditor-dragarea'; + domDrag.title = 'Move field (drag and drop)'; + + return domDrag; +}; + +/** + * Create an editable field + * @return {Element} domField + * @private + */ +JSONEditor.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 + */ +JSONEditor.Node.prototype.setHighlight = function (highlight) { + if (this.dom.tr) { + this.dom.tr.className = 'jsoneditor-tr' + (highlight ? ' jsoneditor-tr-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 + */ +JSONEditor.Node.prototype.updateValue = function (value) { + this.value = value; + this.updateDom(); +}; + +/** + * Update the field of the node. + * @param {String} field + */ +JSONEditor.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. + */ +JSONEditor.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.editable; + domField.spellcheck = false; + domField.className = 'jsoneditor-field'; + } + else { + // parent is an array this is the root node + domField.className = 'jsoneditor-readonly'; + } + + var field; + if (this.index != undefined) { + field = this.index; + } + else if (this.field != undefined) { + field = this.field; + } + else if (this.type == 'array' || this.type == 'object') { + field = this.type; + } + else { + field = '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 + ']'; + domValue.title = this.type + ' containing ' + count + ' items'; + } + else if (this.type == 'object') { + domValue.innerHTML = '{' + count + '}'; + domValue.title = this.type + ' containing ' + count + ' items'; + } + else { + domValue.innerHTML = this._escapeHTML(this.value); + delete domValue.title; + } + } + + // 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 + */ +JSONEditor.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 = 'field'; + } + } + }); + } + } +}; + +/** + * Create an editable value + * @private + */ +JSONEditor.Node.prototype._createDomValue = function () { + var domValue; + + if (this.type == 'array') { + domValue = document.createElement('div'); + domValue.className = 'jsoneditor-readonly'; + domValue.innerHTML = '[...]'; + } + else if (this.type == 'object') { + domValue = document.createElement('div'); + domValue.className = 'jsoneditor-readonly'; + domValue.innerHTML = '{...}'; + } + else if (this.type == 'string') { + domValue = document.createElement('div'); + domValue.contentEditable = this.editor.editable; + domValue.spellcheck = false; + domValue.className = 'jsoneditor-value'; + domValue.innerHTML = this._escapeHTML(this.value); + } + else { + domValue = document.createElement('div'); + domValue.contentEditable = this.editor.editable; + domValue.spellcheck = false; + domValue.className = 'jsoneditor-value'; + domValue.innerHTML = this._escapeHTML(this.value); + } + + // TODO: in FF spel/check of editable divs is done via the body. quite ugly + // document.body.spellcheck = false; + + return domValue; +}; + +/** + * Create an expand/collapse button + * @return {Element} expand + * @private + */ +JSONEditor.Node.prototype._createDomExpandButton = function () { + // create expand button + var expand = document.createElement('button'); + var expandable = (this.type == 'array' || this.type == 'object'); + if (expandable) { + expand.className = this.expanded ? 'jsoneditor-expanded' : 'jsoneditor-collapsed'; + expand.title = + 'Click to expand/collapse this field. \n' + + 'Ctrl+Click to expand/collapse including all childs.'; + } + else { + expand.className = 'jsoneditor-invisible'; + expand.title = ''; + } + + return expand; +}; + + +/** + * Create a DOM tree element, containing the expand/collapse button + * @return {Element} domTree + * @private + */ +JSONEditor.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.appendChild(tbody); + var tr = document.createElement('tr'); + tbody.appendChild(tr); + + // create expand button + var tdExpand = document.createElement('td'); + tdExpand.className = 'jsoneditor-td-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 = 'jsoneditor-td-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 = 'jsoneditor-td-tree'; + tr.appendChild(tdSeparator); + if (this.type != 'object' && this.type != 'array') { + tdSeparator.appendChild(document.createTextNode(':')); + tdSeparator.className = 'jsoneditor-separator'; + } + dom.tdSeparator = tdSeparator; + + // create the value + var tdValue = document.createElement('td'); + tdValue.className = 'jsoneditor-td-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 + */ +JSONEditor.Node.prototype.onEvent = function (event) { + var type = event.type; + var target = event.target || event.srcElement; + var dom = this.dom; + var node = this; + var expandable = (this.type == 'array' || this.type == 'object'); + + // check if mouse is on menu or on dragarea + var isChildOf = JSONEditor.isChildOf; + var isOnDragArea = isChildOf(target, dom.drag); + var isOnMenu = isChildOf(target, dom.menu); + + // highlight current row and its childs + if (isOnMenu || isOnDragArea) { + if (type == 'mouseover') { + this.editor.highlighter.highlight(this); + } + else if (type == 'mouseout') { + // TODO: onmouseout of menu must only execute unhighlight when no contextmenu is visible + 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(); + this.showContextMenu(function () { + highlighter.unlock(); + highlighter.unhighlight(); + }); + } + + // expand events + var domExpand = dom.expand; + if (type == 'click' && target == dom.expand) { + if (expandable) { + this._onExpand(event); + } + } + + // value events + var domValue = dom.value; + if (target == domValue) { + switch (type) { + case 'focus': + JSONEditor.focusNode = this; + break; + + case 'blur': + case 'change': + this._getDomValue(true); + this._updateDomValue(); + if (this.value) { + domValue.innerHTML = this._escapeHTML(this.value); + } + 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': + JSONEditor.focusNode = this; + break; + + case 'change': + case 'blur': + this._getDomField(true); + this._updateDomField(); + if (this.field) { + domField.innerHTML = this._escapeHTML(this.field); + } + 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) : + (JSONEditor.util.getMouseX(event) < JSONEditor.util.getAbsoluteLeft(dom.tdSeparator));// for FF + if (left || expandable) { + // node is expandable when it is an object or array + if (domField) { + JSONEditor.util.setEndOfContentEditable(domField); + domField.focus(); + } + } + else { + if (domValue) { + JSONEditor.util.setEndOfContentEditable(domValue); + domValue.focus(); + } + } + break; + } + } + if ((target == dom.tdExpand && !expandable) || target == dom.tdField || + target == dom.tdSeparator) { + switch (type) { + case 'click': + if (domField) { + JSONEditor.util.setEndOfContentEditable(domField); + domField.focus(); + } + break; + } + } + + if (type == 'keydown') { + this.onKeyDown(event); + } +}; + +/** + * Key down event handler + * @param {Event} event + */ +JSONEditor.Node.prototype.onKeyDown = function (event) { + var keynum = event.which || event.keyCode; + var ctrlKey = event.ctrlKey; + var shiftKey = event.shiftKey; + var handled = false; + + // console.log(ctrlKey, keynum, event.charCode); // TODO: cleanup + if (ctrlKey && keynum == 68) { // ctrl+D + this._onDuplicate(); + handled = true; + } + /* TODO: implement shortcut keys + else if (ctrlKey && keynum == 46) { // Ctrl+Del + this._onRemove(); + handled = true; + // TODO: focus to the next node + } + else if (ctrlKey && !shiftKey && keynum == 45) { // Ctrl+Ins + this._onInsertBefore(); // Ctrl+Ins + handled = true; + // TODO: focus to the next node + } + else if (ctrlKey && shiftKey && keynum == 45) { // Ctrl+Shift+Ins + this._onInsertAfter(); + handled = true; + // TODO: focus to the next node + } + */ + + if (handled) { + JSONEditor.util.preventDefault(event); + JSONEditor.util.stopPropagation(event); + } +}; + +/** + * Handle the expand event, when clicked on the expand button + * @param {Event} event + * @private + */ +JSONEditor.Node.prototype._onExpand = function (event) { + event = event || window.event; + var recurse = event.ctrlKey; // with ctrl-key, expand/collapse all + + 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; + } +}; + +JSONEditor.Node.types = [ + { + 'value': 'array', + 'className': 'jsoneditor-option-array', + 'title': 'Field type "array". ' + + 'An array contains an ordered collection of values.' + }, + { + 'value': 'auto', + 'className': 'jsoneditor-option-auto', + 'title': 'Field type "auto". ' + + 'The field type is automatically determined from the value ' + + 'and can be a string, number, boolean, or null.' + }, + { + 'value': 'object', + 'className': 'jsoneditor-option-object', + 'title': 'Field type "object". ' + + 'An object contains an unordered set of key/value pairs.' + }, + { + 'value': 'string', + 'className': 'jsoneditor-option-string', + 'title': 'Field type "string". ' + + 'Field type is not determined from the value, ' + + 'but always returned as string.' + } +]; + +/** + * Remove this node + * @private + */ +JSONEditor.Node.prototype._onRemove = function() { + this.editor.highlighter.unhighlight(); + var index = this.parent.childs.indexOf(this); + + this.parent._remove(this); + + this.editor.onAction('removeNode', { + 'node': this, + 'parent': this.parent, + 'index': index + }); +}; + +/** + * Duplicate this node + * @private + */ +JSONEditor.Node.prototype._onDuplicate = function() { + var clone = this.parent._duplicate(this); + + this.editor.onAction('duplicateNode', { + 'node': this, + 'clone': clone, + 'parent': this.parent + }); +}; + +/** + * Handle insert before event + * @param {String} [field] + * @param {*} [value] + * @param {String} [type] Can be 'auto', 'array', 'object', or 'string' + * @private + */ +JSONEditor.Node.prototype._onInsertBefore = function (field, value, type) { + var newNode = new JSONEditor.Node(this.editor, { + 'field': (value != undefined) ? field : 'field', + 'value': (value != undefined) ? value : 'value', + 'type': type + }); + newNode.expand(true); + this.parent.insertBefore(newNode, this); + this.editor.highlighter.unhighlight(); + newNode.focus(); + + this.editor.onAction('insertBeforeNode', { + 'node': newNode, + 'beforeNode': this, + 'parent': this.parent + }); +}; + +/** + * Handle insert after event + * @param {String} [field] + * @param {*} [value] + * @param {String} [type] Can be 'auto', 'array', 'object', or 'string' + * @private + */ +JSONEditor.Node.prototype._onInsertAfter = function (field, value, type) { + var newNode = new JSONEditor.Node(this.editor, { + 'field': (value != undefined) ? field : 'field', + 'value': (value != undefined) ? value : 'value', + 'type': type + }); + newNode.expand(true); + this.parent.insertAfter(newNode, this); + this.editor.highlighter.unhighlight(); + newNode.focus(); + + this.editor.onAction('insertAfterNode', { + 'node': newNode, + 'afterNode': this, + 'parent': this.parent + }); +}; + +/** + * Handle append event + * @param {String} [field] + * @param {*} [value] + * @param {String} [type] Can be 'auto', 'array', 'object', or 'string' + * @private + */ +JSONEditor.Node.prototype._onAppend = function (field, value, type) { + var newNode = new JSONEditor.Node(this.editor, { + 'field': (value != undefined) ? field : 'field', + 'value': (value != undefined) ? value : 'value', + 'type': type + }); + newNode.expand(true); + this.parent.appendChild(newNode); + this.editor.highlighter.unhighlight(); + newNode.focus(); + + this.editor.onAction('appendNode', { + 'node': newNode, + 'parent': this.parent + }); +}; + +/** + * Change the type of the node's value + * @param {String} newType + * @private + */ +JSONEditor.Node.prototype._onChangeType = function (newType) { + var oldType = this.type; + if (newType != oldType) { + this.changeType(newType); + + this.editor.onAction('changeType', { + 'node': this, + 'oldType': oldType, + 'newType': newType + }); + } +}; + +/** + * 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 + */ +JSONEditor.Node.prototype._onSort = function (direction) { + if (this.childs && (this.type == 'array' || this.type == 'object')) { + 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 {Node | undefined} buttonAppend or undefined when inapplicable + */ +JSONEditor.Node.prototype.getAppend = function () { + if (!this.append) { + this.append = new JSONEditor.AppendNode(this.editor); + this.append.setParent(this); + } + return this.append.getDom(); +}; + +// titles with explanation for the different types +JSONEditor.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 {function} [onClose] Callback method called when the context menu + * is being closed. + */ +JSONEditor.Node.prototype.showContextMenu = function (onClose) { + var node = this; + var titles = JSONEditor.Node.TYPE_TITLES; + var items = []; + + // TODO: add titles for all context menu items + items.push({ + 'text': 'Type', + 'title': 'Change the type of this node', + 'className': 'jsoneditor-type-' + this.type, + 'submenu': [ + { + 'text': 'Auto', + 'className': 'jsoneditor-type-auto' + + (this.type == 'auto' ? ' selected' : ''), + 'title': titles.auto, + 'click': function () { + node._onChangeType('auto'); + } + }, + { + 'text': 'Array', + 'className': 'jsoneditor-type-array' + + (this.type == 'array' ? ' selected' : ''), + 'title': titles.array, + 'click': function () { + node._onChangeType('array'); + } + }, + { + 'text': 'Object', + 'className': 'jsoneditor-type-object' + + (this.type == 'object' ? ' selected' : ''), + 'title': titles.object, + 'click': function () { + node._onChangeType('object'); + } + }, + { + 'text': 'String', + 'className': 'jsoneditor-type-string' + + (this.type == 'string' ? ' selected' : ''), + 'title': titles.string, + 'click': function () { + node._onChangeType('string'); + } + } + ] + }); + + if (this.type == 'array' || this.type == 'object') { + var direction = ((this.sort == 'asc') ? 'desc': 'asc'); + items.push({ + 'text': 'Sort', + 'title': 'Sort the childs of this node', + 'className': 'jsoneditor-sort-' + direction, + 'click': function () { + node._onSort(direction); + }, + 'submenu': [ + { + 'text': 'Ascending', + 'className': 'jsoneditor-sort-asc', + 'title': 'Sort the childs of this node in ascending order', + 'click': function () { + node._onSort('asc'); + } + }, + { + 'text': 'Descending', + 'className': 'jsoneditor-sort-desc', + 'title': 'Sort the childs of this node in descending order', + 'click': function () { + node._onSort('desc'); + } + } + ] + }); + } + + if (this.parent && (this.parent.type == 'array' || this.parent.type == 'object')) { + // 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 node with type \'auto\' after this node', + 'submenuTitle': 'Select the type of the node to be appended', + 'className': 'jsoneditor-append', + 'click': function () { + node._onAppend('field', 'value', 'auto'); + }, + 'submenu': [ + { + 'text': 'Auto', + 'className': 'jsoneditor-type-auto', + 'title': titles.auto, + 'click': function () { + node._onAppend('field', 'value', 'auto'); + } + }, + { + 'text': 'Array', + 'className': 'jsoneditor-type-array', + 'title': titles.array, + 'click': function () { + node._onAppend('field', []); + } + }, + { + 'text': 'Object', + 'className': 'jsoneditor-type-object', + 'title': titles.object, + 'click': function () { + node._onAppend('field', {}); + } + }, + { + 'text': 'String', + 'className': 'jsoneditor-type-string', + 'title': titles.string, + 'click': function () { + // TODO: settings type string does not work, will become auto + node._onAppend('field', 'value', 'string'); + } + } + ] + }); + } + + // create insert button + items.push({ + 'text': 'Insert', + 'title': 'Insert a new node with type \'auto\' before this node', + 'submenuTitle': 'Select the type of the node to be inserted', + 'className': 'jsoneditor-insert', + 'click': function () { + node._onInsertBefore('field', 'value', 'auto'); + }, + 'submenu': [ + { + 'text': 'Auto', + 'className': 'jsoneditor-type-auto', + 'title': titles.auto, + 'click': function () { + node._onInsertBefore('field', 'value', 'auto'); + } + }, + { + 'text': 'Array', + 'className': 'jsoneditor-type-array', + 'title': titles.array, + 'click': function () { + node._onInsertBefore('field', []); + } + }, + { + 'text': 'Object', + 'className': 'jsoneditor-type-object', + 'title': titles.object, + 'click': function () { + node._onInsertBefore('field', {}); + } + }, + { + 'text': 'String', + 'className': 'jsoneditor-type-string', + 'title': titles.string, + 'click': function () { + // TODO: settings type string does not work, will become auto + node._onInsertBefore('field', 'value', 'string'); + } + } + ] + }); + + // create duplicate button + items.push({ + 'text': 'Duplicate', + 'title': 'Duplicate this node', + 'className': 'jsoneditor-duplicate', + 'click': function () { + node._onDuplicate(); + } + }); + + // create remove button + items.push({ + 'text': 'Remove', + 'title': 'Remove this node', + 'className': 'jsoneditor-remove', + 'click': function () { + node._onRemove(); + } + }); + } + + var menu = new JSONEditor.ContextMenu(items, {close: onClose}); + menu.show(this.dom.menu); +}; + +/** + * get the type of a value + * @param {*} value + * @return {String} type Can be 'object', 'array', 'string', 'auto' + * @private + */ +JSONEditor.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 + */ +JSONEditor.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 + */ +JSONEditor.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 + */ +JSONEditor.Node.prototype._unescapeHTML = function (escapedText) { + var json = '"' + this._escapeJSON(escapedText) + '"'; + var htmlEscaped = JSONEditor.parse(json); + return htmlEscaped + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/ /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 + */ +JSONEditor.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; +}; diff --git a/jsoneditor/js/searchbox.js b/jsoneditor/js/searchbox.js new file mode 100644 index 0000000..2482237 --- /dev/null +++ b/jsoneditor/js/searchbox.js @@ -0,0 +1,309 @@ +/** + * @license + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * Copyright (c) 2011-2013 Jos de Jong, http://jsoneditoronline.org + * + * @author Jos de Jong, + */ + +/** + * @constructor JSONEditor.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 + */ +JSONEditor.SearchBox = function(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 = 'jsoneditor-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'); + td.className = 'jsoneditor-search'; + tr.appendChild(td); + var results = document.createElement('div'); + this.dom.results = results; + results.className = 'jsoneditor-search-results'; + td.appendChild(results); + + td = document.createElement('td'); + td.className = 'jsoneditor-search'; + tr.appendChild(td); + var divInput = document.createElement('div'); + this.dom.input = divInput; + divInput.className = 'jsoneditor-search'; + divInput.title = 'Search fields and values'; + td.appendChild(divInput); + + // table to contain the text input and search button + var tableInput = document.createElement('table'); + tableInput.className = 'jsoneditor-search-input'; + 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 = 'jsoneditor-search-refresh'; + td = document.createElement('td'); + td.appendChild(refreshSearch); + tr.appendChild(td); + + var search = document.createElement('input'); + this.dom.search = search; + search.className = 'jsoneditor-search'; + search.oninput = function (event) { + searchBox.onDelayedSearch(event); + }; + search.onchange = function (event) { // For IE 8 + 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 = 'jsoneditor-search-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 = 'jsoneditor-search-previous'; + searchPrevious.onclick = function () { + searchBox.previous(); + }; + td = document.createElement('td'); + td.appendChild(searchPrevious); + tr.appendChild(td); + +}; + +/** + * Go to the next search result + */ +JSONEditor.SearchBox.prototype.next = function() { + if (this.results != undefined) { + var index = (this.resultIndex != undefined) ? this.resultIndex + 1 : 0; + if (index > this.results.length - 1) { + index = 0; + } + this.setActiveResult(index); + } +}; + +/** + * Go to the prevous search result + */ +JSONEditor.SearchBox.prototype.previous = function() { + 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); + } +}; + +/** + * Set new value for the current active result + * @param {Number} index + */ +JSONEditor.SearchBox.prototype.setActiveResult = function(index) { + // 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(); + + node.scrollTo(); +}; + +/** + * Set the focus to the currently active result. If there is no currently + * active result, the next search result will get focus + */ +JSONEditor.SearchBox.prototype.focusActiveResult = function() { + if (!this.activeResult) { + this.next(); + } + + if (this.activeResult) { + this.activeResult.node.focus(this.activeResult.elem); + } +}; + +/** + * Cancel any running onDelayedSearch. + */ +JSONEditor.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 + */ +JSONEditor.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. + */ +JSONEditor.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 + */ +JSONEditor.SearchBox.prototype.onKeyDown = function (event) { + event = event || window.event; + var keynum = event.which || event.keyCode; + if (keynum == 27) { // ESC + this.dom.search.value = ''; // clear search + this.onSearch(event); + JSONEditor.util.preventDefault(event); + JSONEditor.util.stopPropagation(event); + } + 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(); + } + JSONEditor.util.preventDefault(event); + JSONEditor.util.stopPropagation(event); + } +}; + +/** + * Handle onKeyUp event in the input box + * @param {Event} event + */ +JSONEditor.SearchBox.prototype.onKeyUp = function (event) { + event = event || window.event; + var keynum = event.which || event.keyCode; + if (keynum != 27 && keynum != 13) { // !show and !Enter + this.onDelayedSearch(event); // For IE 8 + } +}; diff --git a/jsoneditor/js/util.js b/jsoneditor/js/util.js new file mode 100644 index 0000000..a6f3f4c --- /dev/null +++ b/jsoneditor/js/util.js @@ -0,0 +1,380 @@ +/** + * @license + * Licensed under the Apache License, Version 2.0 (the "License"); you may not + * use this file except in compliance with the License. You may obtain a copy + * of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations under + * the License. + * + * Copyright (c) 2011-2013 Jos de Jong, http://jsoneditoronline.org + * + * @author Jos de Jong, + */ + +// create namespace for util methods +JSONEditor.util = {}; + + + +/** + * 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. + */ +JSONEditor.util.getAbsoluteLeft = function (elem) { + var left = elem.offsetLeft; + var body = document.body; + var e = elem.offsetParent; + while (e != null && elem != body) { + left += e.offsetLeft; + left -= e.scrollLeft; + e = e.offsetParent; + } + return left; +}; + +/** + * 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. + */ +JSONEditor.util.getAbsoluteTop = function (elem) { + var top = elem.offsetTop; + var body = document.body; + var e = elem.offsetParent; + while (e != null && e != body) { + top += e.offsetTop; + top -= e.scrollTop; + e = e.offsetParent; + } + return top; +}; + +/** + * Get the absolute, vertical mouse position from an event. + * @param {Event} event + * @return {Number} mouseY + */ +JSONEditor.util.getMouseY = function (event) { + var mouseY; + if ('pageY' in event) { + mouseY = event.pageY; + } + else { + // for IE8 and older + mouseY = (event.clientY + document.documentElement.scrollTop); + } + + return mouseY; +}; + +/** + * Get the absolute, horizontal mouse position from an event. + * @param {Event} event + * @return {Number} mouseX + */ +JSONEditor.util.getMouseX = function (event) { + var mouseX; + if ('pageX' in event) { + mouseX = event.pageX; + } + else { + // for IE8 and older + mouseX = (event.clientX + document.documentElement.scrollLeft); + } + + return mouseX; +}; + +/** + * Get the window height + * @return {Number} windowHeight + */ +JSONEditor.util.getWindowHeight = function () { + if ('innerHeight' in window) { + return window.innerHeight; + } + else { + // for IE8 and older + return Math.max(document.body.clientHeight, + document.documentElement.clientHeight); + } +}; + +/** + * add a className to the given elements style + * @param {Element} elem + * @param {String} className + */ +JSONEditor.util.addClassName = function(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 + */ +JSONEditor.util.removeClassName = function(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 + */ +JSONEditor.util.stripFormatting = function (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 + JSONEditor.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 + */ +JSONEditor.util.setEndOfContentEditable = function (contentEditableElement) { + var range, selection; + if(document.createRange) {//Firefox, Chrome, Opera, Safari, IE 9+ + 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 + } + else if(document.selection) {//IE 8 and lower + range = document.body.createTextRange();//Create a range (a range is a like the selection but invisible) + range.moveToElementText(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 + range.select();//Select the range (make it the visible selection + } +}; + +/** + * Get the inner text of an HTML element (for example a div element) + * @param {Element} element + * @param {Object} [buffer] + * @return {String} innerText + */ +JSONEditor.util.getInnerText = function (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 += JSONEditor.util.getInnerText(child, buffer); + buffer.set('\n'); + } + else if (child.nodeName == 'BR') { + innerText += buffer.flush(); + buffer.set('\n'); + } + else { + innerText += JSONEditor.util.getInnerText(child, buffer); + } + } + + return innerText; + } + else { + if (element.nodeName == 'P' && JSONEditor.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 + */ +JSONEditor.util._ieVersion = undefined; +JSONEditor.util.getInternetExplorerVersion = function() { + if (JSONEditor.util._ieVersion == undefined) { + 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 ); + } + } + + JSONEditor.util._ieVersion = rv; + } + + return JSONEditor.util._ieVersion; +}; + +/** + * 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 + */ +JSONEditor.util.addEventListener = function (element, action, listener, useCapture) { + if (element.addEventListener) { + if (useCapture === undefined) + useCapture = false; + + if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) { + action = "DOMMouseScroll"; // For Firefox + } + + element.addEventListener(action, listener, useCapture); + return listener; + } else { + // 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 + */ +JSONEditor.util.removeEventListener = function(element, action, listener, useCapture) { + if (element.removeEventListener) { + // non-IE browsers + if (useCapture === undefined) + useCapture = false; + + if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) { + action = "DOMMouseScroll"; // For Firefox + } + + element.removeEventListener(action, listener, useCapture); + } else { + // IE browsers + element.detachEvent("on" + action, listener); + } +}; + + +/** + * Stop event propagation + * @param {Event} event + */ +JSONEditor.util.stopPropagation = function (event) { + if (!event) { + event = window.event; + } + + if (event.stopPropagation) { + event.stopPropagation(); // non-IE browsers + } + else { + event.cancelBubble = true; // IE browsers + } +}; + + +/** + * Cancels the event if it is cancelable, without stopping further propagation of the event. + * @param {Event} event + */ +JSONEditor.util.preventDefault = function (event) { + if (!event) { + event = window.event; + } + + if (event.preventDefault) { + event.preventDefault(); // non-IE browsers + } + else { + event.returnValue = false; // IE browsers + } +}; diff --git a/jsoneditor/jsoneditor.css b/jsoneditor/jsoneditor.css deleted file mode 100644 index 5400b10..0000000 --- a/jsoneditor/jsoneditor.css +++ /dev/null @@ -1,585 +0,0 @@ - -.jsoneditor-field, .jsoneditor-value, .jsoneditor-field-readonly, .jsoneditor-readonly { - border: 1px solid transparent; - min-height: 16px; - min-width: 24px; - padding: 2px; - margin: 1px; - outline: none; - word-wrap: break-word; - float: left; -} - -/* adjust margin of p elements inside editable divs, needed for Opera, IE */ -.jsoneditor-field p, .jsoneditor-value p { - margin: 0; -} - -.jsoneditor-value { - word-break: break-word; -} - -.jsoneditor-empty { - background-color: #E5E5E5; - border-radius: 2px; -} - -.jsoneditor-separator { - padding: 3px 0; - vertical-align: top; -} - -.jsoneditor-value:focus, .jsoneditor-field:focus, - .jsoneditor-value:hover, .jsoneditor-field:hover, - .jsoneditor-search-highlight { - background-color: #FFFFAB; - border: 1px solid yellow; - border-radius: 2px; -} - -.jsoneditor-search-highlight-active, - .jsoneditor-search-highlight-active:focus, - .jsoneditor-search-highlight-active:hover { - background-color: #ffee00; - border: 1px solid #ffc700; - border-radius: 2px; -} - -.jsoneditor-field-readonly:hover { - border: 1px solid white; -} - -.jsoneditor-readonly { - color: gray; -} - -button.jsoneditor-collapsed, button.jsoneditor-expanded, - button.jsoneditor-invisible, button.jsoneditor-dragarea, - button.jsoneditor-contextmenu, button.jsoneditor-append { - width: 24px; - height: 24px; - padding: 0; - margin: 0; - border: none; - cursor: pointer; - background-color: transparent; - background-image: url('img/jsoneditor-icons.png'); -} - -/* TODO: no global settings -button:disabled { - color: #808080; -} -*/ - -td.jsoneditor-nochilds { - color: gray; -} - -button.jsoneditor-collapsed { - background-position: 0 -48px; -} - -button.jsoneditor-expanded { - background-position: 0 -72px; -} - -.jsoneditor-contextmenu { - position: relative; - background-position: -48px -72px; -} - -.jsoneditor-contextmenu:hover, .jsoneditor-contextmenu:focus { - background-position: -48px -48px; -} - -button.jsoneditor-invisible { - visibility: hidden; - background: none; -} - -button.jsoneditor-collapsed, button.jsoneditor-expanded, -button.jsoneditor-invisible { - float: left; -} - -div.jsoneditor-frame { - color: #1A1A1A; - border: 1px solid #97B0F8; - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - box-sizing: border-box; - width: 100%; - height: 100%; - overflow: auto; - position: relative; - padding: 0; -} - -table.jsoneditor-table { - border-collapse: collapse; - border-spacing: 0; - width: 100%; - margin: 0; -} - -div.jsoneditor-content-outer, div.jsonformatter-content { - width: 100%; - height: 100%; - margin: -35px 0 0 0; - padding: 35px 0 0 0; - - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - box-sizing: border-box; - - overflow: hidden; -} - -div.jsoneditor-content { - width: 100%; - height: 100%; - position: relative; - overflow: auto; -} - -textarea.jsonformatter-textarea { - width: 100%; - height: 100%; - margin: 0; - - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - box-sizing: border-box; - - border: none; - background-color: white; - resize: none; -} - -tr.jsoneditor-tr-highlight { - background-color: #FFFFAB; -} - -button.jsoneditor-dragarea { - width: 24px; - height: 24px; - /* - margin: 3px 0; - background: url('img/dots_gray.gif') top center; - background-repeat: repeat-y; - */ - background: url('img/jsoneditor-icons.png') -72px -72px; - - display: block; - cursor: move; -} - -button.jsoneditor-dragarea:hover, .jsoneditor-dragarea:focus { - background-position: -72px -48px; -} - -/* ___________________________ COMPONENT TOP MENU ___________________________ */ - - -div.jsoneditor-menu { - width: 100%; - height: 35px; - padding: 2px; - margin: 0; - overflow: hidden; - -moz-box-sizing: border-box; - -webkit-box-sizing: border-box; - box-sizing: border-box; - - color: #1A1A1A; - background-color: #D5DDF6; - border-bottom: 1px solid #97B0F8; -} - -button.jsoneditor-menu { - width: 26px; - height: 26px; - margin: 2px; - padding: 2px; - border-radius: 2px; - border: 1px solid #aec0f8; - background: #e3eaf6 url('img/jsoneditor-icons.png'); -} - -button.jsoneditor-menu:hover { - background-color: #f0f2f5; -} -button.jsoneditor-menu:active { - background-color: #ffffff; -} -button.jsoneditor-menu:disabled { - background-color: #e3eaf6; -} - -button.jsoneditor-collapse-all { - background-position: 0 -96px; -} -button.jsoneditor-expand-all { - background-position: 0 -120px; -} -button.jsoneditor-undo { - background-position: -24px -96px; -} -button.jsoneditor-undo:disabled { - background-position: -24px -120px; -} -button.jsoneditor-redo { - background-position: -48px -96px; -} -button.jsoneditor-redo:disabled { - background-position: -48px -120px; -} -/* TODO: css for button:disabled is not supported by IE8 */ -button.jsoneditor-compact { - background-position: -72px -96px; -} -button.jsoneditor-format { - background-position: -72px -120px; -} - -/* TODO: do not change global tr, th, td */ -tr, th, td { - padding: 0; - margin: 0; -} - -td.jsoneditor-td { - vertical-align: top; -} - -td.jsoneditor-td { - padding: 0; -} - -td.jsoneditor-td-edit { - background-color: #F5F5F5; - padding: 0; -} - -td.jsoneditor-td-tree { - vertical-align: top; -} - -td.jsoneditor-droparea { - height: 24px; - - border-top: 1px dashed gray; - border-bottom: 1px dashed gray; - background-color: #FFFF80; -} - -.jsoneditor-field, .jsoneditor-value, .jsoneditor-td, .jsoneditor-th, - .jsoneditor-type, .jsonformatter-textarea { - font-family: droid sans mono, monospace, courier new, courier, sans-serif; - font-size: 10pt; - color: #1A1A1A; -} - -div.jsoneditor-contextmenu button, - input.jsoneditor-search, div.jsoneditor-search-results { - font-family: arial, sans-serif; - font-size: 10pt; - color: #1A1A1A; -} - -.jsoneditor-hidden-focus { - position: absolute; - left: -1000px; - top: -1000px; - border: none; - outline: none; -} - -/* _______________________________ SEARCH BAR _______________________________ */ - -table.jsoneditor-search { - position: absolute; - right: 2px; - top: 2px; -} - -table.jsoneditor-search-input { - border-collapse: collapse; -} - -div.jsoneditor-search { - border: 1px solid #97B0F8; - background-color: white; - padding: 0 2px; - margin: 0; -} - -input.jsoneditor-search { - width: 120px; - border: none; - outline: none; - margin: 1px; -} - -div.jsoneditor-search-results { - color: #4d4d4d; - padding-right: 5px; -} - -button.jsoneditor-search-refresh, button.jsoneditor-search-next, -button.jsoneditor-search-previous { - width: 16px; - height: 24px; - padding: 0; - margin: 0; - border: none; - background: url('img/jsoneditor-icons.png'); - vertical-align: top; -} - -button.jsoneditor-search-refresh { - width: 18px; - background-position: -99px -73px; -} - -button.jsoneditor-search-next { - cursor: pointer; - background-position: -124px -73px; -} -button.jsoneditor-search-next:hover { - background-position: -124px -49px; -} - -button.jsoneditor-search-previous { - cursor: pointer; - background-position: -148px -73px; - margin-right: 2px; -} -button.jsoneditor-search-previous:hover { - background-position: -148px -49px; -} - - - -/* ______________________________ CONTEXT MENU ______________________________ */ - -div.jsoneditor-contextmenu { - position: absolute; -} - -div.jsoneditor-contextmenu ul { - position: relative; - left: 0; - top: 0; - width: 124px; - - background: white; - border: 1px solid #d3d3d3; - box-shadow: 2px 2px 12px rgba(128, 128, 128, 0.3); - - z-index: 1; - list-style: none; - margin: 0; - padding: 0; -} - -div.jsoneditor-contextmenu ul li button { - padding: 0; - margin: 0; - width: 124px; - height: 24px; - border: none; - cursor: pointer; - color: #4d4d4d; - background: transparent; - - line-height: 24px; - text-align: left; -} - -/* Fix button padding in firefox */ -div.jsoneditor-contextmenu ul li button::-moz-focus-inner { - padding: 0; - border: 0; -} - -div.jsoneditor-contextmenu ul li button:hover { - color: #1a1a1a; - background-color: #f5f5f5; -} - -div.jsoneditor-contextmenu ul li button.default { - width: 92px; -} - -div.jsoneditor-contextmenu ul li button.expand { - float: right; - width: 32px; - height: 24px; - border-left: 1px solid #e5e5e5; -} - -div.jsoneditor-contextmenu div.icon { - float: left; - width: 24px; - height: 24px; - border: none; - padding: 0; - margin: 0; - background-image: url('img/jsoneditor-icons.png'); -} - -div.jsoneditor-contextmenu ul li button div.expand { - float: right; - width: 24px; - height: 24px; - padding: 0; - margin: 0 4px 0 0; - background: url('img/jsoneditor-icons.png') 0 -72px; - opacity: 0.4; -} - -div.jsoneditor-contextmenu ul li button:hover div.expand, - div.jsoneditor-contextmenu ul li.selected div.expand, - div.jsoneditor-contextmenu ul li button.expand:hover div.expand { - opacity: 1; -} - -div.jsoneditor-contextmenu .separator { - height: 0; - border-top: 1px solid #e5e5e5; - padding-top: 5px; - margin-top: 5px; -} - -button.jsoneditor-remove > .icon { - background-position: -24px -24px; -} -button.jsoneditor-remove:hover > .icon { - background-position: -24px 0; -} - -button.jsoneditor-append > .icon { - background-position: 0 -24px; -} -button.jsoneditor-append:hover > .icon { - background-position: 0 0; -} - -button.jsoneditor-insert > .icon { - background-position: 0 -24px; -} -button.jsoneditor-insert:hover > .icon { - background-position: 0 0; -} - -button.jsoneditor-insert-above > .icon { - background-position: -24px -24px; -} -button.jsoneditor-insert-above:hover > .icon { - background-position: -24px 0; -} - -button.jsoneditor-insert-below > .icon { - background-position: -48px -24px; -} -button.jsoneditor-insert-below:hover > .icon { - background-position: -48px 0; -} - -button.jsoneditor-duplicate > .icon { - background-position: -48px -24px; -} -button.jsoneditor-duplicate:hover > .icon { - background-position: -48px 0; -} - -button.jsoneditor-sort-asc > .icon { - background-position: -168px -24px; -} -button.jsoneditor-sort-asc:hover > .icon { - background-position: -168px 0; -} - -button.jsoneditor-sort-desc > .icon { - background-position: -192px -24px; -} -button.jsoneditor-sort-desc:hover > .icon { - background-position: -192px 0; -} - -/* ____________________________ CONTEXT SUB MENU ____________________________ */ - - -div.jsoneditor-contextmenu ul li ul li .selected { - background-color: #D5DDF6; -} - -div.jsoneditor-contextmenu ul li { - overflow: hidden; -} - -div.jsoneditor-contextmenu ul li ul { - display: none; - position: relative; - left: -10px; - top: 0; - - border: none; - box-shadow: inset 0 0 10px rgba(128, 128, 128, 0.5); - padding: 0 10px; - - /* TODO: transition is not supported on IE8-9 */ - -webkit-transition: all 0.3s ease-out; - -moz-transition: all 0.3s ease-out; - -o-transition: all 0.3s ease-out; - transition: all 0.3s ease-out; -} - -div.jsoneditor-contextmenu ul li.selected ul { -} - -div.jsoneditor-contextmenu ul li ul li button { - padding-left: 24px; -} - -div.jsoneditor-contextmenu ul li ul li button:hover { - background-color: #f5f5f5; - -} - -button.jsoneditor-type-string > .icon { - background-position: -144px -24px; -} -button.jsoneditor-type-string:hover > .icon, - button.jsoneditor-type-string.selected > .icon{ - background-position: -144px 0; -} - -button.jsoneditor-type-auto > .icon { - background-position: -120px -24px; -} -button.jsoneditor-type-auto:hover > .icon, - button.jsoneditor-type-auto.selected > .icon { - background-position: -120px 0; -} - -button.jsoneditor-type-object > .icon { - background-position: -72px -24px; -} -button.jsoneditor-type-object:hover > .icon, - button.jsoneditor-type-object.selected > .icon{ - background-position: -72px 0; -} - -button.jsoneditor-type-array > .icon { - background-position: -96px -24px; -} -button.jsoneditor-type-array:hover > .icon, - button.jsoneditor-type-array.selected > .icon{ - background-position: -96px 0; -} - - -/* TODO: drastically cleanup the css, improve/simplify naming and cascading */ \ No newline at end of file diff --git a/jsoneditor/jsoneditor.js b/jsoneditor/jsoneditor.js deleted file mode 100644 index 028fdce..0000000 --- a/jsoneditor/jsoneditor.js +++ /dev/null @@ -1,4739 +0,0 @@ -/*! - * @file jsoneditor.js - * - * @brief - * JSONEditor is a web-based tool to view, edit, and format JSON. - * It shows data a clear, editable treeview. - * - * Supported browsers: Chrome, Firefox, Safari, Opera, Internet Explorer 8+ - * - * @license - * This json editor is open sourced with the intention to use the editor as - * a component in your own application. Not to just copy and monetize the editor - * as it is. - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy - * of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - * - * Copyright (c) 2011-2012 Jos de Jong, http://jsoneditoronline.org - * - * @author Jos de Jong, - * @date 2012-12-26 - */ - - -// Internet Explorer 8 and older does not support Array.indexOf, -// so we define it here in that case -// http://soledadpenades.com/2007/05/17/arrayindexof-in-internet-explorer/ -if(!Array.prototype.indexOf) { - Array.prototype.indexOf = function(obj){ - for(var i = 0; i < this.length; i++){ - if(this[i] == obj){ - return i; - } - } - return -1; - } -} - -// Internet Explorer 8 and older does not support Array.forEach, -// so we define it here in that case -// https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/forEach -if (!Array.prototype.forEach) { - Array.prototype.forEach = function(fn, scope) { - for(var i = 0, len = this.length; i < len; ++i) { - fn.call(scope || this, this[i], i, this); - } - } -} - -// define variable JSON, needed for correct error handling on IE7 and older -var JSON; - -/** - * JSONEditor - * @param {Element} container Container element - * @param {Object} [options] Object with options. available options: - * {String} mode Editor mode. Available values: - * 'editor' (default), 'viewer'. - * {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 - */ -JSONEditor = function (container, options, json) { - // check availability of JSON parser (not available in IE7 and older) - if (!JSON) { - throw new Error ('Your browser does not support JSON. \n\n' + - 'Please install the newest version of your browser.\n' + - '(all modern browsers support JSON).'); - } - - if (!container) { - throw new Error('No container element provided.'); - } - this.container = container; - this.dom = {}; - this.highlighter = new JSONEditor.Highlighter(); - - this._setOptions(options); - - if (this.options.history && this.editable) { - this.history = new JSONEditor.History(this); - } - - this._createFrame(); - this._createTable(); - - this.set(json || {}); -}; - -/** - * Initialize and set default options - * @param {Object} [options] Object with options. available options: - * {String} mode Editor mode. Available values: - * 'editor' (default), 'viewer'. - * {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. - * @private - */ -JSONEditor.prototype._setOptions = function (options) { - this.options = { - search: true, - history: true, - mode: 'editor', - 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']; - console.log('WARNING: Option "enableSearch" is deprecated. Use "search" instead.'); - } - if (options['enableSearch']) { - // deprecated since version 1.6.0, 2012-11-03 - this.options.search = options['enableSearch']; - console.log('WARNING: Option "enableHistory" is deprecated. Use "history" instead.'); - } - } - - // interpret the options - this.editable = (this.options.mode != 'viewer'); -}; - -// node currently being edited -JSONEditor.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). - */ -JSONEditor.prototype.set = function (json, name) { - // adjust field name for root node - if (name) { - 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 JSONEditor.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 - */ -JSONEditor.prototype.get = function () { - // remove focus from currently edited node - if (JSONEditor.focusNode) { - JSONEditor.focusNode.blur(); - } - - if (this.node) { - return this.node.getValue(); - } - else { - return undefined; - } -}; - -/** - * Set a field name for the root node. - * @param {String | undefined} name - */ -JSONEditor.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 - */ -JSONEditor.prototype.getName = function () { - return this.options.name; -}; - -/** - * Remove the root node from the editor - */ -JSONEditor.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 {JSONEditor.Node} node - * @private - */ -JSONEditor.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: - * - {JSONEditor.Node} node, - * - {String} elem the dom element name where - * the result is found ('field' or - * 'value') - */ -JSONEditor.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 - */ -JSONEditor.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 - */ -JSONEditor.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. - */ -JSONEditor.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) { - console.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 - */ -JSONEditor.prototype.startAutoScroll = function (mouseY) { - var me = this; - var content = this.content; - var top = JSONEditor.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 - */ -JSONEditor.prototype.stopAutoScroll = function () { - if (this.autoScrollTimer) { - clearTimeout(this.autoScrollTimer); - delete this.autoScrollTimer; - } - if (this.autoScrollStep) { - delete this.autoScrollStep; - } -}; - - -/** - * Set the focus to the JSONEditor. A hidden input field will be created - * which captures key events - */ -// TODO: use the focus method? -JSONEditor.prototype.focus = function () { - /* - if (!this.dom.focus) { - this.dom.focus = document.createElement('input'); - this.dom.focus.className = 'jsoneditor-hidden-focus'; - - var editor = this; - this.dom.focus.onblur = function () { - // remove itself - if (editor.dom.focus) { - var focus = editor.dom.focus; - delete editor.dom.focus; - editor.frame.removeChild(focus); - } - }; - - // attach the hidden input box to the DOM - if (this.frame.firstChild) { - this.frame.insertBefore(this.dom.focus, this.frame.firstChild); - } - else { - this.frame.appendChild(this.dom.focus); - } - } - this.dom.focus.focus(); - */ -}; - -/** - * Adjust the scroll position such that given top position is shown at 1/4 - * of the window height. - * @param {Number} top - */ -JSONEditor.prototype.scrollTo = function (top) { - var content = this.content; - if (content) { - // cancel any running animation - var editor = this; - if (editor.animateTimeout) { - clearTimeout(editor.animateTimeout); - delete editor.animateTimeout; - } - - // 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.animateTimeout = setTimeout(animate, 50); - } - }; - animate(); - } -}; - - -/** - * @constructor JSONEditor.History - * Store action history, enables undo and redo - * @param {JSONEditor} editor - */ -JSONEditor.History = function (editor) { - this.editor = editor; - this.clear(); - - // map with all supported actions - this.actions = { - 'editField': { - 'undo': function (obj) { - obj.params.node.updateField(obj.params.oldValue); - }, - 'redo': function (obj) { - obj.params.node.updateField(obj.params.newValue); - } - }, - 'editValue': { - 'undo': function (obj) { - obj.params.node.updateValue(obj.params.oldValue); - }, - 'redo': function (obj) { - obj.params.node.updateValue(obj.params.newValue); - } - }, - 'appendNode': { - 'undo': function (obj) { - obj.params.parent.removeChild(obj.params.node); - }, - 'redo': function (obj) { - obj.params.parent.appendChild(obj.params.node); - } - }, - 'insertBeforeNode': { - 'undo': function (obj) { - obj.params.parent.removeChild(obj.params.node); - }, - 'redo': function (obj) { - obj.params.parent.insertBefore(obj.params.node, obj.params.beforeNode); - } - }, - 'insertAfterNode': { - 'undo': function (obj) { - obj.params.parent.removeChild(obj.params.node); - }, - 'redo': function (obj) { - obj.params.parent.insertAfter(obj.params.node, obj.params.afterNode); - } - }, - 'removeNode': { - 'undo': function (obj) { - var parent = obj.params.parent; - var beforeNode = parent.childs[obj.params.index] || parent.append; - parent.insertBefore(obj.params.node, beforeNode); - }, - 'redo': function (obj) { - obj.params.parent.removeChild(obj.params.node); - } - }, - 'duplicateNode': { - 'undo': function (obj) { - obj.params.parent.removeChild(obj.params.clone); - }, - 'redo': function (obj) { - obj.params.parent.insertAfter(obj.params.clone, obj.params.node); - } - }, - 'changeType': { - 'undo': function (obj) { - obj.params.node.changeType(obj.params.oldType); - }, - 'redo': function (obj) { - obj.params.node.changeType(obj.params.newType); - } - }, - 'moveNode': { - 'undo': function (obj) { - obj.params.startParent.moveTo(obj.params.node, obj.params.startIndex); - }, - 'redo': function (obj) { - obj.params.endParent.moveTo(obj.params.node, obj.params.endIndex); - } - }, - 'sort': { - 'undo': function (obj) { - var node = obj.params.node; - node.hideChilds(); - node.sort = obj.params.oldSort; - node.childs = obj.params.oldChilds; - node.showChilds(); - }, - 'redo': function (obj) { - var node = obj.params.node; - node.hideChilds(); - node.sort = obj.params.newSort; - node.childs = obj.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. - */ -JSONEditor.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. - */ -JSONEditor.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 - */ -JSONEditor.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 - */ -JSONEditor.History.prototype.canUndo = function () { - return (this.index >= 0); -}; - -/** - * Check if there is an action available for redo - * @return {Boolean} canRedo - */ -JSONEditor.History.prototype.canRedo = function () { - return (this.index < this.history.length - 1); -}; - -/** - * Undo the last action - */ -JSONEditor.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); - } - else { - console.log('Error: unknown action "' + obj.action + '"'); - } - } - this.index--; - - // fire onchange event - this.onChange(); - } -}; - -/** - * Redo the last action - */ -JSONEditor.History.prototype.redo = function () { - if (this.canRedo()) { - this.index++; - - var obj = this.history[this.index]; - if (obj) { - if (obj) { - var action = this.actions[obj.action]; - if (action && action.redo) { - action.redo(obj); - } - else { - console.log('Error: unknown action "' + obj.action + '"'); - } - } - } - - // fire onchange event - this.onChange(); - } -}; - - -/** - * @constructor JSONEditor.Node - * Create a new Node - * @param {JSONEditor} editor - * @param {Object} [params] Can contain parameters: - * {string} field - * {boolean} fieldEditable - * {*} value - * {String} type Can have values 'auto', 'array', - * 'object', or 'string'. - */ -JSONEditor.Node = function (editor, params) { - 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 {JSONEditor.Node} parent - */ -JSONEditor.Node.prototype.setParent = function(parent) { - this.parent = parent; -}; - -/** - * Set field - * @param {String} field - * @param {boolean} [fieldEditable] - */ -JSONEditor.Node.prototype.setField = function(field, fieldEditable) { - this.field = field; - this.fieldEditable = (fieldEditable == true); -}; - -/** - * Get field - * @return {String} - */ -JSONEditor.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' - */ -JSONEditor.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 JSONEditor.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 JSONEditor.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); - console.log('check', value, this.value); - } - else { - this.value = value; - } - */ - } -}; - -/** - * Get value. Value is a JSON structure - * @return {*} value - */ -JSONEditor.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 - */ -JSONEditor.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 {JSONEditor.Node} clone - */ -JSONEditor.Node.prototype.clone = function() { - var clone = new JSONEditor.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 - */ -JSONEditor.Node.prototype.expand = function(recurse) { - if (!this.childs) { - return; - } - - // set this node expanded - this.expanded = true; - if (this.dom.expand) { - this.dom.expand.className = 'jsoneditor-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 - */ -JSONEditor.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 = 'jsoneditor-collapsed'; - } - this.expanded = false; -}; - -/** - * Recursively show all childs when they are expanded - */ -JSONEditor.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 - */ -JSONEditor.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 - */ -JSONEditor.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 {JSONEditor.Node} node - */ -JSONEditor.Node.prototype.appendChild = function(node) { - if (this.type == 'array' || this.type == 'object') { - // 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 {JSONEditor.Node} node - * @param {JSONEditor.Node} beforeNode - */ -JSONEditor.Node.prototype.moveBefore = function(node, beforeNode) { - if (this.type == 'array' || this.type == 'object') { - // 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 JSONEditor.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 {JSONEditor.Node} node - * @param {Number} index - */ -JSONEditor.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 {JSONEditor.Node} node - * @param {JSONEditor.Node} beforeNode - */ -JSONEditor.Node.prototype.insertBefore = function(node, beforeNode) { - if (this.type == 'array' || this.type == 'object') { - 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 {JSONEditor.Node} node - * @param {JSONEditor.Node} afterNode - */ -JSONEditor.Node.prototype.insertAfter = function(node, afterNode) { - if (this.type == 'array' || this.type == 'object') { - 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 {JSONEditor.Node[]} results Array with nodes containing the search text - */ -JSONEditor.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.type == 'array' || this.type == 'object') { - // 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 - */ -JSONEditor.Node.prototype.scrollTo = function() { - 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); - } -}; - -/** - * Set focus to the value of this node - * @param {String} [field] The field name of the element to get the focus - * available values: 'field', 'value' - */ -JSONEditor.Node.prototype.focus = function(field) { - if (this.dom.tr && this.dom.tr.parentNode) { - if (field != 'value' && this.fieldEditable) { - var domField = this.dom.field; - if (domField) { - domField.focus(); - } - } - else { - var domValue = this.dom.value; - if (domValue) { - domValue.focus(); - } - } - } -}; - -/** - * Update the values from the DOM field and value of this node - */ -JSONEditor.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 {JSONEditor.Node} node the childNode to be duplicated - * @return {JSONEditor.Node} clone the clone of the node - * @private - */ -JSONEditor.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 {JSONEditor.Node} node - * @return {boolean} containsNode - */ -JSONEditor.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 {JSONEditor.Node} node the childNode to be moved - * @param {JSONEditor.Node} beforeNode node will be inserted before given - * node. If no beforeNode is given, - * the node is appended at the end - * @private - */ -JSONEditor.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 {JSONEditor.Node} node The child node to be removed; - * @return {JSONEditor.Node | undefined} node The removed node on success, - * else undefined - */ -JSONEditor.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 {JSONEditor.Node} node - * @private - */ -JSONEditor.Node.prototype._remove = function (node) { - this.removeChild(node); -}; - -/** - * Change the type of the value of this Node - * @param {String} newType - */ -JSONEditor.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 = index; - } - }); - - 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 - */ -JSONEditor.Node.prototype._getDomValue = function(silent) { - if (this.dom.value && this.type != 'array' && this.type != 'object') { - this.valueInnerText = JSONEditor.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 - }); - } - } - 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 - */ -JSONEditor.Node.prototype._updateDomValue = function () { - var domValue = this.dom.value; - if (domValue) { - // set text color depending on value type - var v = this.value; - var t = (this.type == 'auto') ? typeof(v) : this.type; - var color = ''; - if (t == 'string') { - color = 'green'; - } - else if (t == 'number') { - color = 'red'; - } - else if (t == 'boolean') { - color = 'blue'; - } - else if (this.type == 'object' || this.type == 'array') { - // note: typeof(null)=="object", therefore check this.type instead of t - color = ''; - } - else if (v === null) { - color = 'purple'; - } - else { - // invalid value - color = 'black'; - } - domValue.style.color = color; - - // make backgound color lightgray when empty - var isEmpty = (String(this.value) == '' && this.type != 'array' && this.type != 'object'); - if (isEmpty) { - JSONEditor.util.addClassName(domValue, 'jsoneditor-empty'); - } - else { - JSONEditor.util.removeClassName(domValue, 'jsoneditor-empty'); - } - - // highlight when there is a search result - if (this.searchValueActive) { - JSONEditor.util.addClassName(domValue, 'jsoneditor-search-highlight-active'); - } - else { - JSONEditor.util.removeClassName(domValue, 'jsoneditor-search-highlight-active'); - } - if (this.searchValue) { - JSONEditor.util.addClassName(domValue, 'jsoneditor-search-highlight'); - } - else { - JSONEditor.util.removeClassName(domValue, 'jsoneditor-search-highlight'); - } - - // strip formatting from the contents of the editable div - JSONEditor.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 - */ -JSONEditor.Node.prototype._updateDomField = function () { - var domField = this.dom.field; - if (domField) { - // make backgound color lightgray when empty - var isEmpty = (String(this.field) == ''); - if (isEmpty) { - JSONEditor.util.addClassName(domField, 'jsoneditor-empty'); - } - else { - JSONEditor.util.removeClassName(domField, 'jsoneditor-empty'); - } - - // highlight when there is a search result - if (this.searchFieldActive) { - JSONEditor.util.addClassName(domField, 'jsoneditor-search-highlight-active'); - } - else { - JSONEditor.util.removeClassName(domField, 'jsoneditor-search-highlight-active'); - } - if (this.searchField) { - JSONEditor.util.addClassName(domField, 'jsoneditor-search-highlight'); - } - else { - JSONEditor.util.removeClassName(domField, 'jsoneditor-search-highlight'); - } - - // strip formatting from the contents of the editable div - JSONEditor.util.stripFormatting(domField); - } -}; - -/** - * Retrieve field from DOM - * @param {boolean} [silent] If true (default), no errors will be thrown in - * case of invalid data - * @private - */ -JSONEditor.Node.prototype._getDomField = function(silent) { - if (this.dom.field && this.fieldEditable) { - this.fieldInnerText = JSONEditor.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 - }); - } - } - 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 - */ -JSONEditor.Node.prototype.clearDom = function() { - // TODO: hide the node first? - //this.hide(); - // TOOD: 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 - */ -JSONEditor.Node.prototype.getDom = function() { - var dom = this.dom; - if (dom.tr) { - return dom.tr; - } - - // create row - dom.tr = document.createElement('tr'); - dom.tr.className = 'jsoneditor-tr'; - dom.tr.node = this; - - if (this.editor.editable) { - // create draggable area - var tdDrag = document.createElement('td'); - tdDrag.className = 'jsoneditor-td'; - dom.drag = this._createDomDragArea(); - if (dom.drag) { - tdDrag.appendChild(dom.drag); - } - dom.tr.appendChild(tdDrag); - - // create context menu - var tdMenu = document.createElement('td'); - tdMenu.className = 'jsoneditor-td'; - var menu = document.createElement('button'); - menu.className = 'jsoneditor-contextmenu'; - dom.menu = menu; - tdMenu.appendChild(dom.menu); - dom.tr.appendChild(tdMenu); - } - - // create tree and field - var tdField = document.createElement('td'); - tdField.className = 'jsoneditor-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 - */ -JSONEditor.Node.prototype._onDragStart = function (event) { - event = event || window.event; - - var node = this; - if (!this.mousemove) { - this.mousemove = JSONEditor.util.addEventListener(document, 'mousemove', - function (event) { - node._onDrag(event); - }); - } - - if (!this.mouseup) { - this.mouseup = JSONEditor.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': JSONEditor.util.getMouseX(event), - 'level': this.getLevel() - }; - document.body.style.cursor = 'move'; - - JSONEditor.util.preventDefault(event); -}; - -/** - * Drag event, fired when moving the mouse while dragging a Node - * @param {Event} event - * @private - */ -JSONEditor.Node.prototype._onDrag = function (event) { - // TODO: this method has grown to large. Split it in a number of methods - event = event || window.event; - // TODO: make a separate function to get the absolute mouseY and mouseX - var mouseY = JSONEditor.util.getMouseY(event); - var mouseX = JSONEditor.util.getMouseX(event); - - 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 = JSONEditor.util.getAbsoluteTop(trThis); - heightThis = trThis.offsetHeight; - if (mouseY < topThis) { - // move up - trPrev = trThis; - do { - trPrev = trPrev.previousSibling; - nodePrev = JSONEditor.getNodeFromTarget(trPrev); - topPrev = trPrev ? JSONEditor.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 = JSONEditor.getNodeFromTarget(trPrev); - if (nodePrev == this) { - nodePrev = undefined; - } - } - - if (nodePrev) { - // check if mouseY is really inside the found node - trPrev = nodePrev.dom.tr; - topPrev = trPrev ? JSONEditor.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 = JSONEditor.util.getAbsoluteTop(trFirst); - trNext = trFirst; - do { - nodeNext = JSONEditor.getNodeFromTarget(trNext); - if (trNext) { - bottomNext = trNext.nextSibling ? - JSONEditor.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 = JSONEditor.getNodeFromTarget(trPrev); - if (nodePrev instanceof JSONEditor.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 = JSONEditor.getNodeFromTarget(trPrev); - levelNext = nodeNext.getLevel(); - } - else { - break; - } - } - else if (nodePrev == this) { - // neglect itself - } - else { - break; - } - - trPrev = trPrev.previousSibling; - } - - // move the node when its position is changed - if (this.dom.tr.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); - - JSONEditor.util.preventDefault(event); -}; - -/** - * Drag event, fired on mouseup after having dragged a node - * @param {Event} event - * @private - */ -JSONEditor.Node.prototype._onDragEnd = function (event) { - event = event || window.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) { - JSONEditor.util.removeEventListener(document, 'mousemove', this.mousemove); - delete this.mousemove; - } - if (this.mouseup) { - JSONEditor.util.removeEventListener(document, 'mouseup', this.mouseup); - delete this.mouseup; - } - - // Stop any running auto scroll - this.editor.stopAutoScroll(); - - JSONEditor.util.preventDefault(event); -}; - -/** - * Create a drag area, displayed at the left side of the node - * @return {Element | undefined} domDrag - * @private - */ -JSONEditor.Node.prototype._createDomDragArea = function () { - if (!this.parent) { - return undefined; - } - - var domDrag = document.createElement('button'); - domDrag.className = 'jsoneditor-dragarea'; - domDrag.title = 'Move field (drag and drop)'; - - return domDrag; -}; - -/** - * Create an editable field - * @return {Element} domField - * @private - */ -JSONEditor.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 - */ -JSONEditor.Node.prototype.setHighlight = function (highlight) { - if (this.dom.tr) { - this.dom.tr.className = 'jsoneditor-tr' + (highlight ? ' jsoneditor-tr-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 - */ -JSONEditor.Node.prototype.updateValue = function (value) { - this.value = value; - this.updateDom(); -}; - -/** - * Update the field of the node. - * @param {String} field - */ -JSONEditor.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. - */ -JSONEditor.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.editable; - domField.spellcheck = false; - domField.className = 'jsoneditor-field'; - } - else { - // parent is an array this is the root node - domField.className = 'jsoneditor-readonly'; - } - - var field; - if (this.index != undefined) { - field = this.index; - } - else if (this.field != undefined) { - field = this.field; - } - else if (this.type == 'array' || this.type == 'object') { - field = this.type; - } - else { - field = '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 + ']'; - domValue.title = this.type + ' containing ' + count + ' items'; - } - else if (this.type == 'object') { - domValue.innerHTML = '{' + count + '}'; - domValue.title = this.type + ' containing ' + count + ' items'; - } - else { - domValue.innerHTML = this._escapeHTML(this.value); - delete domValue.title; - } - } - - // 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 - */ -JSONEditor.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 = 'field'; - } - } - }); - } - } -}; - -/** - * Create an editable value - * @private - */ -JSONEditor.Node.prototype._createDomValue = function () { - var domValue; - - if (this.type == 'array') { - domValue = document.createElement('div'); - domValue.className = 'jsoneditor-readonly'; - domValue.innerHTML = '[...]'; - } - else if (this.type == 'object') { - domValue = document.createElement('div'); - domValue.className = 'jsoneditor-readonly'; - domValue.innerHTML = '{...}'; - } - else if (this.type == 'string') { - domValue = document.createElement('div'); - domValue.contentEditable = this.editor.editable; - domValue.spellcheck = false; - domValue.className = 'jsoneditor-value'; - domValue.innerHTML = this._escapeHTML(this.value); - } - else { - domValue = document.createElement('div'); - domValue.contentEditable = this.editor.editable; - domValue.spellcheck = false; - domValue.className = 'jsoneditor-value'; - domValue.innerHTML = this._escapeHTML(this.value); - } - - // TODO: in FF spel/check of editable divs is done via the body. quite ugly - // document.body.spellcheck = false; - - return domValue; -}; - -/** - * Create an expand/collapse button - * @return {Element} expand - * @private - */ -JSONEditor.Node.prototype._createDomExpandButton = function () { - // create expand button - var expand = document.createElement('button'); - var expandable = (this.type == 'array' || this.type == 'object'); - if (expandable) { - expand.className = this.expanded ? 'jsoneditor-expanded' : 'jsoneditor-collapsed'; - expand.title = - 'Click to expand/collapse this field. \n' + - 'Ctrl+Click to expand/collapse including all childs.'; - } - else { - expand.className = 'jsoneditor-invisible'; - expand.title = ''; - } - - return expand; -}; - - -/** - * Create a DOM tree element, containing the expand/collapse button - * @return {Element} domTree - * @private - */ -JSONEditor.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.appendChild(tbody); - var tr = document.createElement('tr'); - tbody.appendChild(tr); - - // create expand button - var tdExpand = document.createElement('td'); - tdExpand.className = 'jsoneditor-td-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 = 'jsoneditor-td-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 = 'jsoneditor-td-tree'; - tr.appendChild(tdSeparator); - if (this.type != 'object' && this.type != 'array') { - tdSeparator.appendChild(document.createTextNode(':')); - tdSeparator.className = 'jsoneditor-separator'; - } - dom.tdSeparator = tdSeparator; - - // create the value - var tdValue = document.createElement('td'); - tdValue.className = 'jsoneditor-td-tree'; - tr.appendChild(tdValue); - dom.value = this._createDomValue(); - tdValue.appendChild(dom.value); - dom.tdValue = tdValue; - - return domTree; -}; - -/** - * The highlighter can highlight/unhighlight a node, and - * animate the visibility of a context menu. - * @constructor JSONEditor.Highlighter - */ -JSONEditor.Highlighter = function () { - this.locked = false; -}; - -/** - * Hightlight given node and its childs - * @param {JSONEditor.Node} node - */ -JSONEditor.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 - */ -JSONEditor.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 - */ -JSONEditor.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. - */ -JSONEditor.Highlighter.prototype.lock = function () { - this.locked = true; -}; - -/** - * Unlock highlighting or unhighlighting nodes - */ -JSONEditor.Highlighter.prototype.unlock = function () { - this.locked = false; -}; - -/** - * Test if an element is a child of a parent element. - * @param {Element} child - * @param {Element} parent - * @param {boolean} [includeParent] if true (default), the method will return - * true too when the child is the parent. - * @return {boolean} isChild - */ -JSONEditor.isChildOf = function (child, parent, includeParent) { - var e = child; - if (includeParent != false && e == parent) { - return true; - } - - e = e.parentNode; - while (e) { - if (e == parent) { - return true; - } - e = e.parentNode; - } - - return false; -}; - -/** - * Handle an event. The event is catched centrally by the editor - * @param {Event} event - */ -JSONEditor.Node.prototype.onEvent = function (event) { - var type = event.type; - var target = event.target || event.srcElement; - var dom = this.dom; - var node = this; - var expandable = (this.type == 'array' || this.type == 'object'); - - // check if mouse is on menu or on dragarea - var isChildOf = JSONEditor.isChildOf; - var isOnDragArea = isChildOf(target, dom.drag); - var isOnMenu = isChildOf(target, dom.menu); - - // highlight current row and its childs - if (isOnMenu || isOnDragArea) { - if (type == 'mouseover') { - this.editor.highlighter.highlight(this); - } - else if (type == 'mouseout') { - // TODO: onmouseout of menu must only execute unhighlight when no contextmenu is visible - 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(); - this.showContextMenu(function () { - highlighter.unlock(); - highlighter.unhighlight(); - }); - } - - // expand events - var domExpand = dom.expand; - if (type == 'click' && target == dom.expand) { - if (expandable) { - this._onExpand(event); - } - } - - // value events - var domValue = dom.value; - if (target == domValue) { - switch (type) { - case 'focus': - JSONEditor.focusNode = this; - break; - - case 'blur': - case 'change': - this._getDomValue(true); - this._updateDomValue(); - if (this.value) { - domValue.innerHTML = this._escapeHTML(this.value); - } - 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': - JSONEditor.focusNode = this; - break; - - case 'change': - case 'blur': - this._getDomField(true); - this._updateDomField(); - if (this.field) { - domField.innerHTML = this._escapeHTML(this.field); - } - 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) : - (JSONEditor.util.getMouseX(event) < JSONEditor.util.getAbsoluteLeft(dom.tdSeparator));// for FF - if (left || expandable) { - // node is expandable when it is an object or array - if (domField) { - JSONEditor.util.setEndOfContentEditable(domField); - domField.focus(); - } - } - else { - if (domValue) { - JSONEditor.util.setEndOfContentEditable(domValue); - domValue.focus(); - } - } - break; - } - } - if ((target == dom.tdExpand && !expandable) || target == dom.tdField || - target == dom.tdSeparator) { - switch (type) { - case 'click': - if (domField) { - JSONEditor.util.setEndOfContentEditable(domField); - domField.focus(); - } - break; - } - } - - if (type == 'keydown') { - this.onKeyDown(event); - } -}; - -/** - * Key down event handler - * @param {Event} event - */ -JSONEditor.Node.prototype.onKeyDown = function (event) { - var keynum = event.which || event.keyCode; - var ctrlKey = event.ctrlKey; - var shiftKey = event.shiftKey; - var handled = false; - - // console.log(ctrlKey, keynum, event.charCode); // TODO: cleanup - if (ctrlKey && keynum == 68) { // ctrl+D - this._onDuplicate(); - handled = true; - } - /* TODO: implement shortcut keys - else if (ctrlKey && keynum == 46) { // Ctrl+Del - this._onRemove(); - handled = true; - // TODO: focus to the next node - } - else if (ctrlKey && !shiftKey && keynum == 45) { // Ctrl+Ins - this._onInsertBefore(); // Ctrl+Ins - handled = true; - // TODO: focus to the next node - } - else if (ctrlKey && shiftKey && keynum == 45) { // Ctrl+Shift+Ins - this._onInsertAfter(); - handled = true; - // TODO: focus to the next node - } - */ - - if (handled) { - JSONEditor.util.preventDefault(event); - JSONEditor.util.stopPropagation(event); - } -}; - -/** - * Handle the expand event, when clicked on the expand button - * @param {Event} event - * @private - */ -JSONEditor.Node.prototype._onExpand = function (event) { - event = event || window.event; - var recurse = event.ctrlKey; // with ctrl-key, expand/collapse all - - 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; - } -}; - -JSONEditor.Node.types = [ - { - 'value': 'array', - 'className': 'jsoneditor-option-array', - 'title': 'Field type "array". ' + - 'An array contains an ordered collection of values.' - }, - { - 'value': 'auto', - 'className': 'jsoneditor-option-auto', - 'title': 'Field type "auto". ' + - 'The field type is automatically determined from the value ' + - 'and can be a string, number, boolean, or null.' - }, - { - 'value': 'object', - 'className': 'jsoneditor-option-object', - 'title': 'Field type "object". ' + - 'An object contains an unordered set of key/value pairs.' - }, - { - 'value': 'string', - 'className': 'jsoneditor-option-string', - 'title': 'Field type "string". ' + - 'Field type is not determined from the value, ' + - 'but always returned as string.' - } -]; - -/** - * Remove this node - * @private - */ -JSONEditor.Node.prototype._onRemove = function() { - this.editor.highlighter.unhighlight(); - var index = this.parent.childs.indexOf(this); - - this.parent._remove(this); - - this.editor.onAction('removeNode', { - 'node': this, - 'parent': this.parent, - 'index': index - }); -}; - -/** - * Duplicate this node - * @private - */ -JSONEditor.Node.prototype._onDuplicate = function() { - var clone = this.parent._duplicate(this); - - this.editor.onAction('duplicateNode', { - 'node': this, - 'clone': clone, - 'parent': this.parent - }); -}; - -/** - * Handle insert before event - * @param {String} [field] - * @param {*} [value] - * @param {String} [type] Can be 'auto', 'array', 'object', or 'string' - * @private - */ -JSONEditor.Node.prototype._onInsertBefore = function (field, value, type) { - var newNode = new JSONEditor.Node(this.editor, { - 'field': (value != undefined) ? field : 'field', - 'value': (value != undefined) ? value : 'value', - 'type': type - }); - newNode.expand(true); - this.parent.insertBefore(newNode, this); - this.editor.highlighter.unhighlight(); - newNode.focus(); - - this.editor.onAction('insertBeforeNode', { - 'node': newNode, - 'beforeNode': this, - 'parent': this.parent - }); -}; - -/** - * Handle insert after event - * @param {String} [field] - * @param {*} [value] - * @param {String} [type] Can be 'auto', 'array', 'object', or 'string' - * @private - */ -JSONEditor.Node.prototype._onInsertAfter = function (field, value, type) { - var newNode = new JSONEditor.Node(this.editor, { - 'field': (value != undefined) ? field : 'field', - 'value': (value != undefined) ? value : 'value', - 'type': type - }); - newNode.expand(true); - this.parent.insertAfter(newNode, this); - this.editor.highlighter.unhighlight(); - newNode.focus(); - - this.editor.onAction('insertAfterNode', { - 'node': newNode, - 'afterNode': this, - 'parent': this.parent - }); -}; - -/** - * Handle append event - * @param {String} [field] - * @param {*} [value] - * @param {String} [type] Can be 'auto', 'array', 'object', or 'string' - * @private - */ -JSONEditor.Node.prototype._onAppend = function (field, value, type) { - var newNode = new JSONEditor.Node(this.editor, { - 'field': (value != undefined) ? field : 'field', - 'value': (value != undefined) ? value : 'value', - 'type': type - }); - newNode.expand(true); - this.parent.appendChild(newNode); - this.editor.highlighter.unhighlight(); - newNode.focus(); - - this.editor.onAction('appendNode', { - 'node': newNode, - 'parent': this.parent - }); -}; - -/** - * Change the type of the node's value - * @param {String} newType - * @private - */ -JSONEditor.Node.prototype._onChangeType = function (newType) { - var oldType = this.type; - if (newType != oldType) { - this.changeType(newType); - - this.editor.onAction('changeType', { - 'node': this, - 'oldType': oldType, - 'newType': newType - }); - } -}; - -/** - * 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 - */ -JSONEditor.Node.prototype._onSort = function (direction) { - if (this.childs && (this.type == 'array' || this.type == 'object')) { - 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 {Node | undefined} buttonAppend or undefined when inapplicable - */ -JSONEditor.Node.prototype.getAppend = function () { - if (!this.append) { - this.append = new JSONEditor.AppendNode(this.editor); - this.append.setParent(this); - } - return this.append.getDom(); -}; - -// titles with explanation for the different types -JSONEditor.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 {function} [onClose] Callback method called when the context menu - * is being closed. - */ -JSONEditor.Node.prototype.showContextMenu = function (onClose) { - var node = this; - var titles = JSONEditor.TYPE_TITLES; - var items = []; - - // TODO: add titles for all context menu items - items.push({ - 'text': 'Type', - 'title': 'Change the type of this node', - 'className': 'jsoneditor-type-' + this.type, - 'submenu': [ - { - 'text': 'Auto', - 'className': 'jsoneditor-type-auto' + - (this.type == 'auto' ? ' selected' : ''), - 'title': titles.auto, - 'click': function () { - node._onChangeType('auto'); - } - }, - { - 'text': 'Array', - 'className': 'jsoneditor-type-array' + - (this.type == 'array' ? ' selected' : ''), - 'title': titles.array, - 'click': function () { - node._onChangeType('array'); - } - }, - { - 'text': 'Object', - 'className': 'jsoneditor-type-object' + - (this.type == 'object' ? ' selected' : ''), - 'title': titles.object, - 'click': function () { - node._onChangeType('object'); - } - }, - { - 'text': 'String', - 'className': 'jsoneditor-type-string' + - (this.type == 'string' ? ' selected' : ''), - 'title': titles.string, - 'click': function () { - node._onChangeType('string'); - } - } - ] - }); - - if (this.type == 'array' || this.type == 'object') { - var direction = ((this.sort == 'asc') ? 'desc': 'asc'); - items.push({ - 'text': 'Sort', - 'title': 'Sort the childs of this node', - 'className': 'jsoneditor-sort-' + direction, - 'click': function () { - node._onSort(direction); - }, - 'submenu': [ - { - 'text': 'Ascending', - 'className': 'jsoneditor-sort-asc', - 'title': 'Sort the childs of this node in ascending order', - 'click': function () { - node._onSort('asc'); - } - }, - { - 'text': 'Descending', - 'className': 'jsoneditor-sort-desc', - 'title': 'Sort the childs of this node in descending order', - 'click': function () { - node._onSort('desc'); - } - } - ] - }); - } - - if (this.parent && (this.parent.type == 'array' || this.parent.type == 'object')) { - // 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 node with type \'auto\' after this node', - 'submenuTitle': 'Select the type of the node to be appended', - 'className': 'jsoneditor-append', - 'click': function () { - node._onAppend('field', 'value', 'auto'); - }, - 'submenu': [ - { - 'text': 'Auto', - 'className': 'jsoneditor-type-auto', - 'title': titles.auto, - 'click': function () { - node._onAppend('field', 'value', 'auto'); - } - }, - { - 'text': 'Array', - 'className': 'jsoneditor-type-array', - 'title': titles.array, - 'click': function () { - node._onAppend('field', []); - } - }, - { - 'text': 'Object', - 'className': 'jsoneditor-type-object', - 'title': titles.object, - 'click': function () { - node._onAppend('field', {}); - } - }, - { - 'text': 'String', - 'className': 'jsoneditor-type-string', - 'title': titles.string, - 'click': function () { - // TODO: settings type string does not work, will become auto - node._onAppend('field', 'value', 'string'); - } - } - ] - }); - } - - // create insert button - items.push({ - 'text': 'Insert', - 'title': 'Insert a new node with type \'auto\' before this node', - 'submenuTitle': 'Select the type of the node to be inserted', - 'className': 'jsoneditor-insert', - 'click': function () { - node._onInsertBefore('field', 'value', 'auto'); - }, - 'submenu': [ - { - 'text': 'Auto', - 'className': 'jsoneditor-type-auto', - 'title': titles.auto, - 'click': function () { - node._onInsertBefore('field', 'value', 'auto'); - } - }, - { - 'text': 'Array', - 'className': 'jsoneditor-type-array', - 'title': titles.array, - 'click': function () { - node._onInsertBefore('field', []); - } - }, - { - 'text': 'Object', - 'className': 'jsoneditor-type-object', - 'title': titles.object, - 'click': function () { - node._onInsertBefore('field', {}); - } - }, - { - 'text': 'String', - 'className': 'jsoneditor-type-string', - 'title': titles.string, - 'click': function () { - // TODO: settings type string does not work, will become auto - node._onInsertBefore('field', 'value', 'string'); - } - } - ] - }); - - // create duplicate button - items.push({ - 'text': 'Duplicate', - 'title': 'Duplicate this node', - 'className': 'jsoneditor-duplicate', - 'click': function () { - node._onDuplicate(); - } - }); - - // create remove button - items.push({ - 'text': 'Remove', - 'title': 'Remove this node', - 'className': 'jsoneditor-remove', - 'click': function () { - node._onRemove(); - } - }); - } - - var menu = new JSONEditor.ContextMenu(items, {close: onClose}); - menu.show(this.dom.menu); -}; - -/** - * get the type of a value - * @param {*} value - * @return {String} type Can be 'object', 'array', 'string', 'auto' - * @private - */ -JSONEditor.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 - */ -JSONEditor.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 - */ -JSONEditor.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 - */ -JSONEditor.Node.prototype._unescapeHTML = function (escapedText) { - var json = '"' + this._escapeJSON(escapedText) + '"'; - var htmlEscaped = JSONEditor.parse(json); - return htmlEscaped - .replace(/</g, '<') - .replace(/>/g, '>') - .replace(/ /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 - */ -JSONEditor.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 JSONEditor.AppendNode - * @extends JSONEditor.Node - * @param {JSONEditor} 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 - */ -JSONEditor.AppendNode = function (editor) { - this.editor = editor; - this.dom = {}; -}; - -JSONEditor.AppendNode.prototype = new JSONEditor.Node(); - -/** - * Return a table row with an append button. - * @return {Element} dom TR element - */ -JSONEditor.AppendNode.prototype.getDom = function () { - // TODO: do not create the DOM for the appendNode when in viewer mode - // 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; - - // when in viewer mode, don't create the contents for the append node - // but return here. - if (!this.editor.editable) { - return trAppend; - } - - // TODO: consistent naming - - // a cell for the dragarea column - var tdDrag = document.createElement('td'); - tdDrag.className = 'jsoneditor-td'; - dom.tdDrag = tdDrag; - - // create context menu - var tdMenu = document.createElement('td'); - tdMenu.className = 'jsoneditor-td'; - var menu = document.createElement('button'); - menu.className = 'jsoneditor-contextmenu'; - dom.menu = menu; - dom.tdMenu = tdMenu; - 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 = 'jsoneditor-readonly'; - tdAppend.appendChild(domText); - tdAppend.className = 'jsoneditor-td'; - dom.td = tdAppend; - dom.text = domText; - - this.updateDom(); - - return trAppend; -}; - -/** - * Update the HTML dom of the Node - */ -JSONEditor.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) { - trAppend.removeChild(dom.tdDrag); - trAppend.removeChild(dom.tdMenu); - trAppend.removeChild(tdAppend); - } - } - else { - if (!dom.tr.firstChild) { - trAppend.appendChild(dom.tdDrag); - 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 - */ -JSONEditor.AppendNode.prototype.isVisible = function () { - return (this.parent.childs.length == 0); -}; - -/** - * Show a contextmenu for this node - * @param {function} [onClose] Callback method called when the context menu - * is being closed. - */ -JSONEditor.AppendNode.prototype.showContextMenu = function (onClose) { - var node = this; - var titles = JSONEditor.TYPE_TITLES; - var items = [ - // create append button - { - 'text': 'Append', - 'title': 'Append a new node with type \'auto\'', - 'submenuTitle': 'Select the type of the node to be appended', - 'className': 'jsoneditor-insert', - 'click': function () { - node._onAppend('field', 'value', 'auto'); - }, - 'submenu': [ - { - 'text': 'Auto', - 'className': 'jsoneditor-type-auto', - 'title': titles.auto, - 'click': function () { - node._onAppend('field', 'value', 'auto'); - } - }, - { - 'text': 'Array', - 'className': 'jsoneditor-type-array', - 'title': titles.array, - 'click': function () { - node._onAppend('field', []); - } - }, - { - 'text': 'Object', - 'className': 'jsoneditor-type-object', - 'title': titles.object, - 'click': function () { - node._onAppend('field', {}); - } - }, - { - 'text': 'String', - 'className': 'jsoneditor-type-string', - 'title': titles.string, - 'click': function () { - // TODO: settings type string does not work, will become auto - node._onAppend('field', 'value', 'string'); - } - } - ] - } - ]; - - var menu = new JSONEditor.ContextMenu(items, {close: onClose}); - menu.show(this.dom.menu); -}; - -/** - * Handle an event. The event is catched centrally by the editor - * @param {Event} event - */ -JSONEditor.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(); - this.showContextMenu(function () { - highlighter.unlock(); - highlighter.unhighlight(); - }); - } -}; - -/** - * 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 - */ -JSONEditor.ContextMenu = function (items, options) { - var me = this; - this.items = items; - this.eventListeners = {}; - this.visibleSubmenu = undefined; - this.onClose = options ? options.close : undefined; - - // create a container element - var menu = document.createElement('div'); - menu.className = 'jsoneditor-contextmenu'; - this.menu = menu; - - // create a list to hold the menu items - var list = document.createElement('ul'); - list.className = 'menu'; - menu.appendChild(list); - this.list = list; - - function createMenuItems (list, 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 { - // 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; - 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'); - 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 - var selected = false; - buttonSubmenu.onclick = function () { - me._onShowSubmenu(submenu); - }; - - // create the submenu - var submenu = document.createElement('ul'); - submenu.className = 'menu'; - submenu.style.height = '0'; - li.appendChild(submenu); - createMenuItems(submenu, item.submenu); - } - else { - // no submenu, just a button with clickhandler - button.innerHTML = '
' + item.text; - } - } - }); - } - createMenuItems(list, 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); - }); -}; - -// currently displayed context menu, a singleton. We may only have one visible context menu -JSONEditor.ContextMenu.visibleMenu = undefined; - -/** - * Attach the menu to an anchor - * @param {Element} anchor - */ -JSONEditor.ContextMenu.prototype.show = function (anchor) { - this.hide(); - - // calculate whether the menu fits below the anchor - var windowHeight = JSONEditor.util.getWindowHeight(); - var anchorHeight = anchor.offsetHeight; - var menuHeight = this.maxHeight; - - // position the menu - var left = JSONEditor.util.getAbsoluteLeft(anchor); - var top = JSONEditor.util.getAbsoluteTop(anchor); - if (top + anchorHeight + menuHeight < windowHeight) { - // display the menu below the anchor - this.menu.style.left = left + 'px'; - this.menu.style.top = (top + anchorHeight) + 'px'; - this.menu.style.bottom = ''; - } - else { - // display the menu above the anchor - this.menu.style.left = left + 'px'; - this.menu.style.top = ''; - this.menu.style.bottom = (windowHeight - top) + 'px'; - } - - // attach the menu to the document - document.body.appendChild(this.menu); - - // create and attach event listeners - var me = this; - var list = this.list; - this.eventListeners.mousedown = JSONEditor.util.addEventListener( - document, 'mousedown', function (event) { - // hide menu on click outside of the menu - event = event || window.event; - var target = event.target || event.srcElement; - if (!JSONEditor.isChildOf(target, list)) { - me.hide(); - } - }); - this.eventListeners.mousewheel = JSONEditor.util.addEventListener( - document, 'mousewheel', function () { - // hide the menu on mouse scroll - me.hide(); - }); - this.eventListeners.keydown = JSONEditor.util.addEventListener( - document, 'keydown', function (event) { - // hide the menu on ESC key - event = event || window.event; - var keynum = event.which || event.keyCode; - if (keynum == 27) { // ESC - me.hide(); - JSONEditor.util.stopPropagation(event); - JSONEditor.util.preventDefault(event); - } - }); - - // TODO: focus to the first button in the context menu - - if (JSONEditor.ContextMenu.visibleMenu) { - JSONEditor.ContextMenu.visibleMenu.hide(); - } - JSONEditor.ContextMenu.visibleMenu = this; -}; - -/** - * Hide the context menu if visible - */ -JSONEditor.ContextMenu.prototype.hide = function () { - // remove the menu from the DOM - if (this.menu.parentNode) { - this.menu.parentNode.removeChild(this.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) { - JSONEditor.util.removeEventListener(document, name, fn); - } - delete this.eventListeners[name]; - } - } -}; - -/** - * Show or hide a submenu. - * Any currently visible submenu will be hided. - * @param {Element} submenu - * @private - */ -JSONEditor.ContextMenu.prototype._onShowSubmenu = function (submenu) { - var me = this; - var alreadyVisible = (submenu == this.visibleSubmenu); - - // hide the currently visible submenu - var visibleSubmenu = this.visibleSubmenu; - if (visibleSubmenu) { - visibleSubmenu.style.height = '0'; - visibleSubmenu.style.padding = ''; - setTimeout(function () { - if (me.visibleSubmenu != visibleSubmenu) { - visibleSubmenu.style.display = ''; - JSONEditor.util.removeClassName(visibleSubmenu.parentNode, 'selected'); - } - }, 300); // timeout duration must match the css transition duration - this.visibleSubmenu = undefined; - } - - if (!alreadyVisible) { - submenu.style.display = 'block'; - var height = submenu.clientHeight; // force a reflow in Firefox - setTimeout(function () { - if (me.visibleSubmenu == submenu) { - submenu.style.height = (submenu.childNodes.length * 24) + 'px'; - submenu.style.padding = '5px 10px'; - } - }, 0); - JSONEditor.util.addClassName(submenu.parentNode, 'selected'); - this.visibleSubmenu = submenu; - } -}; - -/** - * Create main frame - * @private - */ -JSONEditor.prototype._createFrame = function () { - // create the frame - this.container.innerHTML = ''; - this.frame = document.createElement('div'); - this.frame.className = 'jsoneditor-frame'; - this.container.appendChild(this.frame); - - // create one global event listener to handle all events from all nodes - var editor = this; - // TODO: move this onEvent to JSONEditor.prototype.onEvent - var onEvent = function (event) { - event = event || window.event; - var target = event.target || event.srcElement; - - if (event.type == 'keydown') { - editor.onKeyDown(event); - } - - var node = JSONEditor.getNodeFromTarget(target); - if (node) { - node.onEvent(event); - } - }; - this.frame.onclick = function (event) { - onEvent(event); - - // prevent default submit action when JSONEditor is located inside a form - JSONEditor.util.preventDefault(event); - }; - 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 - JSONEditor.util.addEventListener(this.frame, 'focus', onEvent, true); - JSONEditor.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 = 'jsoneditor-menu'; - this.frame.appendChild(this.menu); - - // create expand all button - var expandAll = document.createElement('button'); - expandAll.className = 'jsoneditor-menu jsoneditor-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 = 'jsoneditor-menu jsoneditor-collapse-all'; - collapseAll.onclick = function () { - editor.collapseAll(); - }; - this.menu.appendChild(collapseAll); - - // create undo/redo buttons - if (this.history) { - // create separator - var separator = document.createElement('span'); - separator.innerHTML = ' '; - this.menu.appendChild(separator); - - // create undo button - var undo = document.createElement('button'); - undo.className = 'jsoneditor-menu jsoneditor-undo'; - undo.title = 'Undo last action'; - undo.onclick = function () { - editor._onUndo(); - }; - this.menu.appendChild(undo); - this.dom.undo = undo; - - // create redo button - var redo = document.createElement('button'); - redo.className = 'jsoneditor-menu jsoneditor-redo'; - redo.title = 'Redo'; - 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 search box - if (this.options.search) { - this.searchBox = new JSONEditor.SearchBox(this, this.menu); - } -}; - -/** - * Perform an undo action - * @private - */ -JSONEditor.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 - */ -JSONEditor.prototype._onRedo = function () { - if (this.history) { - // redo last action - editor.history.redo(); - - // trigger change callback - if (editor.options.change) { - editor.options.change(); - } - } -}; - -/** - * Event handler for keydown. Handles shortcut keys - * @param {Event} event - */ -JSONEditor.prototype.onKeyDown = function (event) { - var keynum = event.which || event.keyCode; - var ctrlKey = event.ctrlKey; - var shiftKey = event.shiftKey; - var handled = false; - - 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 - if (!shiftKey) { - // select next search result (F3 or Ctrl+G) - this.searchBox.next(); - } - else { - // select previous search result (Shift+F3 or Ctrl+Shift+G) - this.searchBox.previous(); - } - - // set selection to the current - this.searchBox.focusActiveResult(); - - 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) { - JSONEditor.util.preventDefault(event); - JSONEditor.util.stopPropagation(event); - } -}; - -/** - * Create main table - * @private - */ -JSONEditor.prototype._createTable = function () { - var contentOuter = document.createElement('div'); - contentOuter.className = 'jsoneditor-content-outer'; - this.contentOuter = contentOuter; - - this.content = document.createElement('div'); - this.content.className = 'jsoneditor-content'; - contentOuter.appendChild(this.content); - - this.table = document.createElement('table'); - this.table.className = 'jsoneditor-table'; - this.content.appendChild(this.table); - - // IE8 does not handle overflow='auto' correctly. - // Therefore, set overflow to 'scroll' - var ieVersion = JSONEditor.util.getInternetExplorerVersion(); - if (ieVersion == 8) { - this.content.style.overflow = 'scroll'; - } - - // 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'); - 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); -}; - -/** - * Find the node from an event target - * @param {Node} target - * @return {JSONEditor.Node | undefined} node or undefined when not found - */ -JSONEditor.getNodeFromTarget = function (target) { - while (target) { - if (target.node) { - return target.node; - } - target = target.parentNode; - } - - return undefined; -}; - -/** - * Create a JSONFormatter and attach it to given container - * @constructor JSONFormatter - * @param {Element} container - * @param {Object} [options] Object with options. available options: - * {Number} indentation Number of indentation - * spaces. 4 by default. - * {function} change Callback method - * triggered on change - * @param {JSON | String} [json] initial contents of the formatter - */ -JSONFormatter = function (container, options, json) { - // check availability of JSON parser (not available in IE7 and older) - if (!JSON) { - throw new Error('Your browser does not support JSON. \n\n' + - 'Please install the newest version of your browser.\n' + - '(all modern browsers support JSON).'); - } - - this.container = container; - this.indentation = 4; // number of spaces - - this.width = container.clientWidth; - this.height = container.clientHeight; - - this.frame = document.createElement('div'); - this.frame.className = "jsoneditor-frame"; - this.frame.onclick = function (event) { - // prevent default submit action when JSONFormatter is located inside a form - JSONEditor.util.preventDefault(event); - }; - - // create menu - this.menu = document.createElement('div'); - this.menu.className = 'jsoneditor-menu'; - this.frame.appendChild(this.menu); - - // create format button - var buttonFormat = document.createElement('button'); - //buttonFormat.innerHTML = 'Format'; - buttonFormat.className = 'jsoneditor-menu jsoneditor-format'; - buttonFormat.title = 'Format JSON data, with proper indentation and line feeds'; - //buttonFormat.className = 'jsoneditor-button'; - this.menu.appendChild(buttonFormat); - - // create compact button - var buttonCompact = document.createElement('button'); - //buttonCompact.innerHTML = 'Compact'; - buttonCompact.className = 'jsoneditor-menu jsoneditor-compact'; - buttonCompact.title = 'Compact JSON data, remove all whitespaces'; - //buttonCompact.className = 'jsoneditor-button'; - this.menu.appendChild(buttonCompact); - - this.content = document.createElement('div'); - this.content.className = 'jsonformatter-content'; - this.frame.appendChild(this.content); - - this.textarea = document.createElement('textarea'); - this.textarea.className = "jsonformatter-textarea"; - this.textarea.spellcheck = false; - this.content.appendChild(this.textarea); - - var textarea = this.textarea; - - // read the options - if (options) { - if (options.change) { - // register on change event - if (this.textarea.oninput === null) { - this.textarea.oninput = function () { - options.change(); - } - } - else { - // oninput is undefined. For IE8- - this.textarea.onchange = function () { - options.change(); - } - } - } - if (options.indentation) { - this.indentation = Number(options.indentation); - } - } - - var me = this; - buttonFormat.onclick = function () { - try { - var json = JSONEditor.parse(textarea.value); - textarea.value = JSON.stringify(json, null, me.indentation); - } - catch (err) { - me.onError(err); - } - }; - buttonCompact.onclick = function () { - try { - var json = JSONEditor.parse(textarea.value); - textarea.value = JSON.stringify(json); - } - catch (err) { - me.onError(err); - } - }; - - this.container.appendChild(this.frame); - - // load initial json object or string - if (typeof(json) == 'string') { - this.setText(json); - } - else { - this.set(json); - } -}; - -/** - * This method is executed on error. - * It can be overwritten for each instance of the JSONFormatter - * @param {String} err - */ -JSONFormatter.prototype.onError = function(err) { - // action should be implemented for the instance -}; - -/** - * Set json data in the formatter - * @param {Object} json - */ -JSONFormatter.prototype.set = function(json) { - this.textarea.value = JSON.stringify(json, null, this.indentation); -}; - -/** - * Get json data from the formatter - * @return {Object} json - */ -JSONFormatter.prototype.get = function() { - return JSONEditor.parse(this.textarea.value); -}; - -/** - * Get the text contents of the JSONFormatter - * @return {String} text - */ -JSONFormatter.prototype.getText = function() { - return this.textarea.value; -}; - -/** - * Set the text contents of the JSONFormatter - * @param {String} text - */ -JSONFormatter.prototype.setText = function(text) { - this.textarea.value = text; -}; - -/** - * @constructor JSONEditor.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 - */ -JSONEditor.SearchBox = function(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 = 'jsoneditor-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'); - td.className = 'jsoneditor-search'; - tr.appendChild(td); - var results = document.createElement('div'); - this.dom.results = results; - results.className = 'jsoneditor-search-results'; - td.appendChild(results); - - td = document.createElement('td'); - td.className = 'jsoneditor-search'; - tr.appendChild(td); - var divInput = document.createElement('div'); - this.dom.input = divInput; - divInput.className = 'jsoneditor-search'; - divInput.title = 'Search fields and values'; - td.appendChild(divInput); - - // table to contain the text input and search button - var tableInput = document.createElement('table'); - tableInput.className = 'jsoneditor-search-input'; - 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 = 'jsoneditor-search-refresh'; - td = document.createElement('td'); - td.appendChild(refreshSearch); - tr.appendChild(td); - - var search = document.createElement('input'); - this.dom.search = search; - search.className = 'jsoneditor-search'; - search.oninput = function (event) { - searchBox.onDelayedSearch(event); - }; - search.onchange = function (event) { // For IE 8 - 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 = 'jsoneditor-search-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 = 'jsoneditor-search-previous'; - searchPrevious.onclick = function () { - searchBox.previous(); - }; - td = document.createElement('td'); - td.appendChild(searchPrevious); - tr.appendChild(td); - -}; - -/** - * Go to the next search result - */ -JSONEditor.SearchBox.prototype.next = function() { - if (this.results != undefined) { - var index = (this.resultIndex != undefined) ? this.resultIndex + 1 : 0; - if (index > this.results.length - 1) { - index = 0; - } - this.setActiveResult(index); - } -}; - -/** - * Go to the prevous search result - */ -JSONEditor.SearchBox.prototype.previous = function() { - 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); - } -}; - -/** - * Set new value for the current active result - * @param {Number} index - */ -JSONEditor.SearchBox.prototype.setActiveResult = function(index) { - // 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(); - - node.scrollTo(); -}; - -/** - * Set the focus to the currently active result. If there is no currently - * active result, the next search result will get focus - */ -JSONEditor.SearchBox.prototype.focusActiveResult = function() { - if (!this.activeResult) { - this.next(); - } - - if (this.activeResult) { - this.activeResult.node.focus(this.activeResult.elem); - } -}; - -/** - * Cancel any running onDelayedSearch. - */ -JSONEditor.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 - */ -JSONEditor.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. - */ -JSONEditor.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 - */ -JSONEditor.SearchBox.prototype.onKeyDown = function (event) { - event = event || window.event; - var keynum = event.which || event.keyCode; - if (keynum == 27) { // ESC - this.dom.search.value = ''; // clear search - this.onSearch(event); - JSONEditor.util.preventDefault(event); - JSONEditor.util.stopPropagation(event); - } - 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(); - } - JSONEditor.util.preventDefault(event); - JSONEditor.util.stopPropagation(event); - } -}; - -/** - * Handle onKeyUp event in the input box - * @param {Event} event - */ -JSONEditor.SearchBox.prototype.onKeyUp = function (event) { - event = event || window.event; - var keynum = event.which || event.keyCode; - if (keynum != 27 && keynum != 13) { // !show and !Enter - this.onDelayedSearch(event); // For IE 8 - } -}; - -/** - * 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 - */ -JSONEditor.parse = function (jsonString) { - try { - return JSON.parse(jsonString); - } - catch (err) { - // get a detailed error message using validate - var message = JSONEditor.validate(jsonString) || err; - throw new Error(message); - } -}; - -/** - * 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 - * @return {String | undefined} Returns undefined when the string is valid JSON, - * returns a string with an error message when - * the data is invalid - */ -JSONEditor.validate = function (jsonString) { - var message = undefined; - - try { - if (window.jsonlint) { - window.jsonlint.parse(jsonString); - } - else { - JSON.parse(jsonString); - } - } - catch (err) { - message = '
' + err.toString() + '
'; - if (window.jsonlint) { - message += - '' + - 'validated by jsonlint' + - ''; - } - } - - return message; -}; - - -// create namespace for util methods -JSONEditor.util = {}; - - - -/** - * 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. - */ -JSONEditor.util.getAbsoluteLeft = function (elem) { - var left = elem.offsetLeft; - var body = document.body; - var e = elem.offsetParent; - while (e != null && elem != body) { - left += e.offsetLeft; - left -= e.scrollLeft; - e = e.offsetParent; - } - return left; -}; - -/** - * 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. - */ -JSONEditor.util.getAbsoluteTop = function (elem) { - var top = elem.offsetTop; - var body = document.body; - var e = elem.offsetParent; - while (e != null && e != body) { - top += e.offsetTop; - top -= e.scrollTop; - e = e.offsetParent; - } - return top; -}; - -/** - * Get the absolute, vertical mouse position from an event. - * @param {Event} event - * @return {Number} mouseY - */ -JSONEditor.util.getMouseY = function (event) { - var mouseY; - if ('pageY' in event) { - mouseY = event.pageY; - } - else { - // for IE8 and older - mouseY = (event.clientY + document.documentElement.scrollTop); - } - - return mouseY; -}; - -/** - * Get the absolute, horizontal mouse position from an event. - * @param {Event} event - * @return {Number} mouseX - */ -JSONEditor.util.getMouseX = function (event) { - var mouseX; - if ('pageX' in event) { - mouseX = event.pageX; - } - else { - // for IE8 and older - mouseX = (event.clientX + document.documentElement.scrollLeft); - } - - return mouseX; -}; - -/** - * Get the window height - * @return {Number} windowHeight - */ -JSONEditor.util.getWindowHeight = function () { - if ('innerHeight' in window) { - return window.innerHeight; - } - else { - // for IE8 and older - return Math.max(document.body.clientHeight, - document.documentElement.clientHeight); - } -}; - -/** - * add a className to the given elements style - * @param {Element} elem - * @param {String} className - */ -JSONEditor.util.addClassName = function(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 - */ -JSONEditor.util.removeClassName = function(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 - */ -JSONEditor.util.stripFormatting = function (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 - JSONEditor.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 - */ -JSONEditor.util.setEndOfContentEditable = function (contentEditableElement) { - var range, selection; - if(document.createRange) {//Firefox, Chrome, Opera, Safari, IE 9+ - 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 - } - else if(document.selection) {//IE 8 and lower - range = document.body.createTextRange();//Create a range (a range is a like the selection but invisible) - range.moveToElementText(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 - range.select();//Select the range (make it the visible selection - } -}; - -/** - * Get the inner text of an HTML element (for example a div element) - * @param {Element} element - * @param {Object} [buffer] - * @return {String} innerText - */ -JSONEditor.util.getInnerText = function (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 += JSONEditor.util.getInnerText(child, buffer); - buffer.set('\n'); - } - else if (child.nodeName == 'BR') { - innerText += buffer.flush(); - buffer.set('\n'); - } - else { - innerText += JSONEditor.util.getInnerText(child, buffer); - } - } - - return innerText; - } - else { - if (element.nodeName == 'P' && JSONEditor.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 - */ -JSONEditor.util._ieVersion = undefined; -JSONEditor.util.getInternetExplorerVersion = function() { - if (JSONEditor.util._ieVersion == undefined) { - 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 ); - } - } - - JSONEditor.util._ieVersion = rv; - } - - return JSONEditor.util._ieVersion; -}; - -/** - * 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 - */ -JSONEditor.util.addEventListener = function (element, action, listener, useCapture) { - if (element.addEventListener) { - if (useCapture === undefined) - useCapture = false; - - if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) { - action = "DOMMouseScroll"; // For Firefox - } - - element.addEventListener(action, listener, useCapture); - return listener; - } else { - // 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 - */ -JSONEditor.util.removeEventListener = function(element, action, listener, useCapture) { - if (element.removeEventListener) { - // non-IE browsers - if (useCapture === undefined) - useCapture = false; - - if (action === "mousewheel" && navigator.userAgent.indexOf("Firefox") >= 0) { - action = "DOMMouseScroll"; // For Firefox - } - - element.removeEventListener(action, listener, useCapture); - } else { - // IE browsers - element.detachEvent("on" + action, listener); - } -}; - - -/** - * Stop event propagation - * @param {Event} event - */ -JSONEditor.util.stopPropagation = function (event) { - if (!event) { - event = window.event; - } - - if (event.stopPropagation) { - event.stopPropagation(); // non-IE browsers - } - else { - event.cancelBubble = true; // IE browsers - } -}; - - -/** - * Cancels the event if it is cancelable, without stopping further propagation of the event. - * @param {Event} event - */ -JSONEditor.util.preventDefault = function (event) { - if (!event) { - event = window.event; - } - - if (event.preventDefault) { - event.preventDefault(); // non-IE browsers - } - else { - event.returnValue = false; // IE browsers - } -}; - diff --git a/test/couchdbeditor.html b/test/couchdbeditor.html index 172414c..d1680f1 100644 --- a/test/couchdbeditor.html +++ b/test/couchdbeditor.html @@ -10,8 +10,8 @@ - - + +