Reworked the source code to commonjs modules
This commit is contained in:
parent
abc3ac3625
commit
c18145f503
11671
jsoneditor.js
11671
jsoneditor.js
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -1,444 +1,443 @@
|
|||
define(['./util'], function (util) {
|
||||
var util = require('./util');
|
||||
|
||||
/**
|
||||
* A context menu
|
||||
* @param {Object[]} items Array containing the menu structure
|
||||
* TODO: describe structure
|
||||
* @param {Object} [options] Object with options. Available options:
|
||||
* {function} close Callback called when the
|
||||
* context menu is being closed.
|
||||
* @constructor
|
||||
*/
|
||||
function ContextMenu (items, options) {
|
||||
this.dom = {};
|
||||
/**
|
||||
* A context menu
|
||||
* @param {Object[]} items Array containing the menu structure
|
||||
* TODO: describe structure
|
||||
* @param {Object} [options] Object with options. Available options:
|
||||
* {function} close Callback called when the
|
||||
* context menu is being closed.
|
||||
* @constructor
|
||||
*/
|
||||
function ContextMenu (items, options) {
|
||||
this.dom = {};
|
||||
|
||||
var me = this;
|
||||
var dom = this.dom;
|
||||
this.anchor = undefined;
|
||||
this.items = items;
|
||||
this.eventListeners = {};
|
||||
this.selection = undefined; // holds the selection before the menu was opened
|
||||
this.visibleSubmenu = undefined;
|
||||
this.onClose = options ? options.close : undefined;
|
||||
var me = this;
|
||||
var dom = this.dom;
|
||||
this.anchor = undefined;
|
||||
this.items = items;
|
||||
this.eventListeners = {};
|
||||
this.selection = undefined; // holds the selection before the menu was opened
|
||||
this.visibleSubmenu = undefined;
|
||||
this.onClose = options ? options.close : undefined;
|
||||
|
||||
// create a container element
|
||||
var menu = document.createElement('div');
|
||||
menu.className = 'jsoneditor-contextmenu';
|
||||
dom.menu = menu;
|
||||
// create a container element
|
||||
var menu = document.createElement('div');
|
||||
menu.className = 'jsoneditor-contextmenu';
|
||||
dom.menu = menu;
|
||||
|
||||
// create a list to hold the menu items
|
||||
var list = document.createElement('ul');
|
||||
list.className = 'menu';
|
||||
menu.appendChild(list);
|
||||
dom.list = list;
|
||||
dom.items = []; // list with all buttons
|
||||
// create a list to hold the menu items
|
||||
var list = document.createElement('ul');
|
||||
list.className = 'menu';
|
||||
menu.appendChild(list);
|
||||
dom.list = list;
|
||||
dom.items = []; // list with all buttons
|
||||
|
||||
// create a (non-visible) button to set the focus to the menu
|
||||
var focusButton = document.createElement('button');
|
||||
dom.focusButton = focusButton;
|
||||
var li = document.createElement('li');
|
||||
li.style.overflow = 'hidden';
|
||||
li.style.height = '0';
|
||||
li.appendChild(focusButton);
|
||||
list.appendChild(li);
|
||||
// create a (non-visible) button to set the focus to the menu
|
||||
var focusButton = document.createElement('button');
|
||||
dom.focusButton = focusButton;
|
||||
var li = document.createElement('li');
|
||||
li.style.overflow = 'hidden';
|
||||
li.style.height = '0';
|
||||
li.appendChild(focusButton);
|
||||
list.appendChild(li);
|
||||
|
||||
function createMenuItems (list, domItems, items) {
|
||||
items.forEach(function (item) {
|
||||
if (item.type == 'separator') {
|
||||
// create a separator
|
||||
var separator = document.createElement('div');
|
||||
separator.className = 'separator';
|
||||
li = document.createElement('li');
|
||||
li.appendChild(separator);
|
||||
list.appendChild(li);
|
||||
function createMenuItems (list, domItems, items) {
|
||||
items.forEach(function (item) {
|
||||
if (item.type == 'separator') {
|
||||
// create a separator
|
||||
var separator = document.createElement('div');
|
||||
separator.className = 'separator';
|
||||
li = document.createElement('li');
|
||||
li.appendChild(separator);
|
||||
list.appendChild(li);
|
||||
}
|
||||
else {
|
||||
var domItem = {};
|
||||
|
||||
// create a menu item
|
||||
var li = document.createElement('li');
|
||||
list.appendChild(li);
|
||||
|
||||
// create a button in the menu item
|
||||
var button = document.createElement('button');
|
||||
button.className = item.className;
|
||||
domItem.button = button;
|
||||
if (item.title) {
|
||||
button.title = item.title;
|
||||
}
|
||||
else {
|
||||
var domItem = {};
|
||||
if (item.click) {
|
||||
button.onclick = function () {
|
||||
me.hide();
|
||||
item.click();
|
||||
};
|
||||
}
|
||||
li.appendChild(button);
|
||||
|
||||
// create a menu item
|
||||
var li = document.createElement('li');
|
||||
list.appendChild(li);
|
||||
// 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));
|
||||
|
||||
// create a button in the menu item
|
||||
var button = document.createElement('button');
|
||||
button.className = item.className;
|
||||
domItem.button = button;
|
||||
if (item.title) {
|
||||
button.title = item.title;
|
||||
}
|
||||
var buttonSubmenu;
|
||||
if (item.click) {
|
||||
button.onclick = function () {
|
||||
me.hide();
|
||||
item.click();
|
||||
};
|
||||
}
|
||||
li.appendChild(button);
|
||||
// submenu and a button with a click handler
|
||||
button.className += ' default';
|
||||
|
||||
// create the contents of the button
|
||||
if (item.submenu) {
|
||||
// add the icon to the button
|
||||
var divIcon = document.createElement('div');
|
||||
divIcon.className = 'icon';
|
||||
button.appendChild(divIcon);
|
||||
button.appendChild(document.createTextNode(item.text));
|
||||
|
||||
var buttonSubmenu;
|
||||
if (item.click) {
|
||||
// submenu and a button with a click handler
|
||||
button.className += ' default';
|
||||
|
||||
var buttonExpand = document.createElement('button');
|
||||
domItem.buttonExpand = buttonExpand;
|
||||
buttonExpand.className = 'expand';
|
||||
buttonExpand.innerHTML = '<div class="expand"></div>';
|
||||
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;
|
||||
var buttonExpand = document.createElement('button');
|
||||
domItem.buttonExpand = buttonExpand;
|
||||
buttonExpand.className = 'expand';
|
||||
buttonExpand.innerHTML = '<div class="expand"></div>';
|
||||
li.appendChild(buttonExpand);
|
||||
if (item.submenuTitle) {
|
||||
buttonExpand.title = item.submenuTitle;
|
||||
}
|
||||
|
||||
// attach a handler to expand/collapse the submenu
|
||||
buttonSubmenu.onclick = function () {
|
||||
me._onExpandItem(domItem);
|
||||
buttonSubmenu.focus();
|
||||
};
|
||||
|
||||
// create the submenu
|
||||
var domSubItems = [];
|
||||
domItem.subItems = domSubItems;
|
||||
var ul = document.createElement('ul');
|
||||
domItem.ul = ul;
|
||||
ul.className = 'menu';
|
||||
ul.style.height = '0';
|
||||
li.appendChild(ul);
|
||||
createMenuItems(ul, domSubItems, item.submenu);
|
||||
buttonSubmenu = buttonExpand;
|
||||
}
|
||||
else {
|
||||
// no submenu, just a button with clickhandler
|
||||
button.innerHTML = '<div class="icon"></div>' + item.text;
|
||||
// submenu and a button without a click handler
|
||||
var divExpand = document.createElement('div');
|
||||
divExpand.className = 'expand';
|
||||
button.appendChild(divExpand);
|
||||
|
||||
buttonSubmenu = button;
|
||||
}
|
||||
|
||||
domItems.push(domItem);
|
||||
// attach a handler to expand/collapse the submenu
|
||||
buttonSubmenu.onclick = function () {
|
||||
me._onExpandItem(domItem);
|
||||
buttonSubmenu.focus();
|
||||
};
|
||||
|
||||
// create the submenu
|
||||
var domSubItems = [];
|
||||
domItem.subItems = domSubItems;
|
||||
var ul = document.createElement('ul');
|
||||
domItem.ul = ul;
|
||||
ul.className = 'menu';
|
||||
ul.style.height = '0';
|
||||
li.appendChild(ul);
|
||||
createMenuItems(ul, domSubItems, item.submenu);
|
||||
}
|
||||
else {
|
||||
// no submenu, just a button with clickhandler
|
||||
button.innerHTML = '<div class="icon"></div>' + item.text;
|
||||
}
|
||||
});
|
||||
}
|
||||
createMenuItems(list, this.dom.items, items);
|
||||
|
||||
// TODO: when the editor is small, show the submenu on the right instead of inline?
|
||||
|
||||
// calculate the max height of the menu with one submenu expanded
|
||||
this.maxHeight = 0; // height in pixels
|
||||
items.forEach(function (item) {
|
||||
var height = (items.length + (item.submenu ? item.submenu.length : 0)) * 24;
|
||||
me.maxHeight = Math.max(me.maxHeight, height);
|
||||
domItems.push(domItem);
|
||||
}
|
||||
});
|
||||
}
|
||||
createMenuItems(list, this.dom.items, items);
|
||||
|
||||
/**
|
||||
* Get the currently visible buttons
|
||||
* @return {Array.<HTMLElement>} buttons
|
||||
* @private
|
||||
*/
|
||||
ContextMenu.prototype._getVisibleButtons = function () {
|
||||
var buttons = [];
|
||||
var me = this;
|
||||
this.dom.items.forEach(function (item) {
|
||||
buttons.push(item.button);
|
||||
if (item.buttonExpand) {
|
||||
buttons.push(item.buttonExpand);
|
||||
}
|
||||
if (item.subItems && item == me.expandedItem) {
|
||||
item.subItems.forEach(function (subItem) {
|
||||
buttons.push(subItem.button);
|
||||
if (subItem.buttonExpand) {
|
||||
buttons.push(subItem.buttonExpand);
|
||||
}
|
||||
// TODO: change to fully recursive method
|
||||
});
|
||||
}
|
||||
});
|
||||
// TODO: when the editor is small, show the submenu on the right instead of inline?
|
||||
|
||||
return buttons;
|
||||
};
|
||||
// calculate the max height of the menu with one submenu expanded
|
||||
this.maxHeight = 0; // height in pixels
|
||||
items.forEach(function (item) {
|
||||
var height = (items.length + (item.submenu ? item.submenu.length : 0)) * 24;
|
||||
me.maxHeight = Math.max(me.maxHeight, height);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the currently visible buttons
|
||||
* @return {Array.<HTMLElement>} buttons
|
||||
* @private
|
||||
*/
|
||||
ContextMenu.prototype._getVisibleButtons = function () {
|
||||
var buttons = [];
|
||||
var me = this;
|
||||
this.dom.items.forEach(function (item) {
|
||||
buttons.push(item.button);
|
||||
if (item.buttonExpand) {
|
||||
buttons.push(item.buttonExpand);
|
||||
}
|
||||
if (item.subItems && item == me.expandedItem) {
|
||||
item.subItems.forEach(function (subItem) {
|
||||
buttons.push(subItem.button);
|
||||
if (subItem.buttonExpand) {
|
||||
buttons.push(subItem.buttonExpand);
|
||||
}
|
||||
// TODO: change to fully recursive method
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return buttons;
|
||||
};
|
||||
|
||||
// currently displayed context menu, a singleton. We may only have one visible context menu
|
||||
ContextMenu.visibleMenu = undefined;
|
||||
ContextMenu.visibleMenu = undefined;
|
||||
|
||||
/**
|
||||
* Attach the menu to an anchor
|
||||
* @param {HTMLElement} anchor
|
||||
*/
|
||||
ContextMenu.prototype.show = function (anchor) {
|
||||
this.hide();
|
||||
/**
|
||||
* Attach the menu to an anchor
|
||||
* @param {HTMLElement} anchor
|
||||
*/
|
||||
ContextMenu.prototype.show = function (anchor) {
|
||||
this.hide();
|
||||
|
||||
// calculate whether the menu fits below the anchor
|
||||
var windowHeight = window.innerHeight,
|
||||
windowScroll = (window.pageYOffset || document.scrollTop || 0),
|
||||
windowBottom = windowHeight + windowScroll,
|
||||
anchorHeight = anchor.offsetHeight,
|
||||
menuHeight = this.maxHeight;
|
||||
// calculate whether the menu fits below the anchor
|
||||
var windowHeight = window.innerHeight,
|
||||
windowScroll = (window.pageYOffset || document.scrollTop || 0),
|
||||
windowBottom = windowHeight + windowScroll,
|
||||
anchorHeight = anchor.offsetHeight,
|
||||
menuHeight = this.maxHeight;
|
||||
|
||||
// position the menu
|
||||
var left = util.getAbsoluteLeft(anchor);
|
||||
var top = util.getAbsoluteTop(anchor);
|
||||
if (top + anchorHeight + menuHeight < windowBottom) {
|
||||
// display the menu below the anchor
|
||||
this.dom.menu.style.left = left + 'px';
|
||||
this.dom.menu.style.top = (top + anchorHeight) + 'px';
|
||||
this.dom.menu.style.bottom = '';
|
||||
}
|
||||
else {
|
||||
// display the menu above the anchor
|
||||
this.dom.menu.style.left = left + 'px';
|
||||
this.dom.menu.style.top = '';
|
||||
this.dom.menu.style.bottom = (windowHeight - top) + 'px';
|
||||
}
|
||||
// position the menu
|
||||
var left = util.getAbsoluteLeft(anchor);
|
||||
var top = util.getAbsoluteTop(anchor);
|
||||
if (top + anchorHeight + menuHeight < windowBottom) {
|
||||
// display the menu below the anchor
|
||||
this.dom.menu.style.left = left + 'px';
|
||||
this.dom.menu.style.top = (top + anchorHeight) + 'px';
|
||||
this.dom.menu.style.bottom = '';
|
||||
}
|
||||
else {
|
||||
// display the menu above the anchor
|
||||
this.dom.menu.style.left = left + 'px';
|
||||
this.dom.menu.style.top = '';
|
||||
this.dom.menu.style.bottom = (windowHeight - top) + 'px';
|
||||
}
|
||||
|
||||
// attach the menu to the document
|
||||
document.body.appendChild(this.dom.menu);
|
||||
// attach the menu to the document
|
||||
document.body.appendChild(this.dom.menu);
|
||||
|
||||
// create and attach event listeners
|
||||
var me = this;
|
||||
var list = this.dom.list;
|
||||
this.eventListeners.mousedown = util.addEventListener(
|
||||
document, 'mousedown', function (event) {
|
||||
// hide menu on click outside of the menu
|
||||
var target = event.target;
|
||||
if ((target != list) && !me._isChildOf(target, list)) {
|
||||
me.hide();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
});
|
||||
this.eventListeners.mousewheel = util.addEventListener(
|
||||
document, 'mousewheel', function (event) {
|
||||
// block scrolling when context menu is visible
|
||||
// create and attach event listeners
|
||||
var me = this;
|
||||
var list = this.dom.list;
|
||||
this.eventListeners.mousedown = util.addEventListener(
|
||||
document, 'mousedown', function (event) {
|
||||
// hide menu on click outside of the menu
|
||||
var target = event.target;
|
||||
if ((target != list) && !me._isChildOf(target, list)) {
|
||||
me.hide();
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
});
|
||||
this.eventListeners.keydown = util.addEventListener(
|
||||
document, 'keydown', function (event) {
|
||||
me._onKeyDown(event);
|
||||
});
|
||||
}
|
||||
});
|
||||
this.eventListeners.mousewheel = util.addEventListener(
|
||||
document, 'mousewheel', function (event) {
|
||||
// block scrolling when context menu is visible
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
});
|
||||
this.eventListeners.keydown = util.addEventListener(
|
||||
document, 'keydown', function (event) {
|
||||
me._onKeyDown(event);
|
||||
});
|
||||
|
||||
// move focus to the first button in the context menu
|
||||
this.selection = util.getSelection();
|
||||
this.anchor = anchor;
|
||||
// move focus to the first button in the context menu
|
||||
this.selection = util.getSelection();
|
||||
this.anchor = anchor;
|
||||
setTimeout(function () {
|
||||
me.dom.focusButton.focus();
|
||||
}, 0);
|
||||
|
||||
if (ContextMenu.visibleMenu) {
|
||||
ContextMenu.visibleMenu.hide();
|
||||
}
|
||||
ContextMenu.visibleMenu = this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hide the context menu if visible
|
||||
*/
|
||||
ContextMenu.prototype.hide = function () {
|
||||
// remove the menu from the DOM
|
||||
if (this.dom.menu.parentNode) {
|
||||
this.dom.menu.parentNode.removeChild(this.dom.menu);
|
||||
if (this.onClose) {
|
||||
this.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
// remove all event listeners
|
||||
// all event listeners are supposed to be attached to document.
|
||||
for (var name in this.eventListeners) {
|
||||
if (this.eventListeners.hasOwnProperty(name)) {
|
||||
var fn = this.eventListeners[name];
|
||||
if (fn) {
|
||||
util.removeEventListener(document, name, fn);
|
||||
}
|
||||
delete this.eventListeners[name];
|
||||
}
|
||||
}
|
||||
|
||||
if (ContextMenu.visibleMenu == this) {
|
||||
ContextMenu.visibleMenu = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Expand a submenu
|
||||
* Any currently expanded submenu will be hided.
|
||||
* @param {Object} domItem
|
||||
* @private
|
||||
*/
|
||||
ContextMenu.prototype._onExpandItem = function (domItem) {
|
||||
var me = this;
|
||||
var alreadyVisible = (domItem == this.expandedItem);
|
||||
|
||||
// hide the currently visible submenu
|
||||
var expandedItem = this.expandedItem;
|
||||
if (expandedItem) {
|
||||
//var ul = expandedItem.ul;
|
||||
expandedItem.ul.style.height = '0';
|
||||
expandedItem.ul.style.padding = '';
|
||||
setTimeout(function () {
|
||||
me.dom.focusButton.focus();
|
||||
if (me.expandedItem != expandedItem) {
|
||||
expandedItem.ul.style.display = '';
|
||||
util.removeClassName(expandedItem.ul.parentNode, 'selected');
|
||||
}
|
||||
}, 300); // timeout duration must match the css transition duration
|
||||
this.expandedItem = undefined;
|
||||
}
|
||||
|
||||
if (!alreadyVisible) {
|
||||
var ul = domItem.ul;
|
||||
ul.style.display = 'block';
|
||||
var height = ul.clientHeight; // force a reflow in Firefox
|
||||
setTimeout(function () {
|
||||
if (me.expandedItem == domItem) {
|
||||
ul.style.height = (ul.childNodes.length * 24) + 'px';
|
||||
ul.style.padding = '5px 10px';
|
||||
}
|
||||
}, 0);
|
||||
util.addClassName(ul.parentNode, 'selected');
|
||||
this.expandedItem = domItem;
|
||||
}
|
||||
};
|
||||
|
||||
if (ContextMenu.visibleMenu) {
|
||||
ContextMenu.visibleMenu.hide();
|
||||
/**
|
||||
* Handle onkeydown event
|
||||
* @param {Event} event
|
||||
* @private
|
||||
*/
|
||||
ContextMenu.prototype._onKeyDown = function (event) {
|
||||
var target = event.target;
|
||||
var keynum = event.which;
|
||||
var handled = false;
|
||||
var buttons, targetIndex, prevButton, nextButton;
|
||||
|
||||
if (keynum == 27) { // ESC
|
||||
// hide the menu on ESC key
|
||||
|
||||
// restore previous selection and focus
|
||||
if (this.selection) {
|
||||
util.setSelection(this.selection);
|
||||
}
|
||||
if (this.anchor) {
|
||||
this.anchor.focus();
|
||||
}
|
||||
ContextMenu.visibleMenu = this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hide the context menu if visible
|
||||
*/
|
||||
ContextMenu.prototype.hide = function () {
|
||||
// remove the menu from the DOM
|
||||
if (this.dom.menu.parentNode) {
|
||||
this.dom.menu.parentNode.removeChild(this.dom.menu);
|
||||
if (this.onClose) {
|
||||
this.onClose();
|
||||
this.hide();
|
||||
|
||||
handled = true;
|
||||
}
|
||||
else if (keynum == 9) { // Tab
|
||||
if (!event.shiftKey) { // Tab
|
||||
buttons = this._getVisibleButtons();
|
||||
targetIndex = buttons.indexOf(target);
|
||||
if (targetIndex == buttons.length - 1) {
|
||||
// move to first button
|
||||
buttons[0].focus();
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// remove all event listeners
|
||||
// all event listeners are supposed to be attached to document.
|
||||
for (var name in this.eventListeners) {
|
||||
if (this.eventListeners.hasOwnProperty(name)) {
|
||||
var fn = this.eventListeners[name];
|
||||
if (fn) {
|
||||
util.removeEventListener(document, name, fn);
|
||||
}
|
||||
delete this.eventListeners[name];
|
||||
else { // Shift+Tab
|
||||
buttons = this._getVisibleButtons();
|
||||
targetIndex = buttons.indexOf(target);
|
||||
if (targetIndex == 0) {
|
||||
// move to last button
|
||||
buttons[buttons.length - 1].focus();
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (ContextMenu.visibleMenu == this) {
|
||||
ContextMenu.visibleMenu = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Expand a submenu
|
||||
* Any currently expanded submenu will be hided.
|
||||
* @param {Object} domItem
|
||||
* @private
|
||||
*/
|
||||
ContextMenu.prototype._onExpandItem = function (domItem) {
|
||||
var me = this;
|
||||
var alreadyVisible = (domItem == this.expandedItem);
|
||||
|
||||
// hide the currently visible submenu
|
||||
var expandedItem = this.expandedItem;
|
||||
if (expandedItem) {
|
||||
//var ul = expandedItem.ul;
|
||||
expandedItem.ul.style.height = '0';
|
||||
expandedItem.ul.style.padding = '';
|
||||
setTimeout(function () {
|
||||
if (me.expandedItem != expandedItem) {
|
||||
expandedItem.ul.style.display = '';
|
||||
util.removeClassName(expandedItem.ul.parentNode, 'selected');
|
||||
}
|
||||
}, 300); // timeout duration must match the css transition duration
|
||||
this.expandedItem = undefined;
|
||||
}
|
||||
|
||||
if (!alreadyVisible) {
|
||||
var ul = domItem.ul;
|
||||
ul.style.display = 'block';
|
||||
var height = ul.clientHeight; // force a reflow in Firefox
|
||||
setTimeout(function () {
|
||||
if (me.expandedItem == domItem) {
|
||||
ul.style.height = (ul.childNodes.length * 24) + 'px';
|
||||
ul.style.padding = '5px 10px';
|
||||
}
|
||||
}, 0);
|
||||
util.addClassName(ul.parentNode, 'selected');
|
||||
this.expandedItem = domItem;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle onkeydown event
|
||||
* @param {Event} event
|
||||
* @private
|
||||
*/
|
||||
ContextMenu.prototype._onKeyDown = function (event) {
|
||||
var target = event.target;
|
||||
var keynum = event.which;
|
||||
var handled = false;
|
||||
var buttons, targetIndex, prevButton, nextButton;
|
||||
|
||||
if (keynum == 27) { // ESC
|
||||
// hide the menu on ESC key
|
||||
|
||||
// restore previous selection and focus
|
||||
if (this.selection) {
|
||||
util.setSelection(this.selection);
|
||||
}
|
||||
if (this.anchor) {
|
||||
this.anchor.focus();
|
||||
}
|
||||
|
||||
this.hide();
|
||||
|
||||
handled = true;
|
||||
}
|
||||
else if (keynum == 9) { // Tab
|
||||
if (!event.shiftKey) { // Tab
|
||||
buttons = this._getVisibleButtons();
|
||||
targetIndex = buttons.indexOf(target);
|
||||
if (targetIndex == buttons.length - 1) {
|
||||
// move to first button
|
||||
buttons[0].focus();
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
else { // Shift+Tab
|
||||
buttons = this._getVisibleButtons();
|
||||
targetIndex = buttons.indexOf(target);
|
||||
if (targetIndex == 0) {
|
||||
// move to last button
|
||||
buttons[buttons.length - 1].focus();
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (keynum == 37) { // Arrow Left
|
||||
if (target.className == 'expand') {
|
||||
buttons = this._getVisibleButtons();
|
||||
targetIndex = buttons.indexOf(target);
|
||||
prevButton = buttons[targetIndex - 1];
|
||||
if (prevButton) {
|
||||
prevButton.focus();
|
||||
}
|
||||
}
|
||||
handled = true;
|
||||
}
|
||||
else if (keynum == 38) { // Arrow Up
|
||||
}
|
||||
else if (keynum == 37) { // Arrow Left
|
||||
if (target.className == 'expand') {
|
||||
buttons = this._getVisibleButtons();
|
||||
targetIndex = buttons.indexOf(target);
|
||||
prevButton = buttons[targetIndex - 1];
|
||||
if (prevButton && prevButton.className == 'expand') {
|
||||
// skip expand button
|
||||
prevButton = buttons[targetIndex - 2];
|
||||
}
|
||||
if (!prevButton) {
|
||||
// move to last button
|
||||
prevButton = buttons[buttons.length - 1];
|
||||
}
|
||||
if (prevButton) {
|
||||
prevButton.focus();
|
||||
}
|
||||
}
|
||||
handled = true;
|
||||
}
|
||||
else if (keynum == 38) { // Arrow Up
|
||||
buttons = this._getVisibleButtons();
|
||||
targetIndex = buttons.indexOf(target);
|
||||
prevButton = buttons[targetIndex - 1];
|
||||
if (prevButton && prevButton.className == 'expand') {
|
||||
// skip expand button
|
||||
prevButton = buttons[targetIndex - 2];
|
||||
}
|
||||
if (!prevButton) {
|
||||
// move to last button
|
||||
prevButton = buttons[buttons.length - 1];
|
||||
}
|
||||
if (prevButton) {
|
||||
prevButton.focus();
|
||||
}
|
||||
handled = true;
|
||||
}
|
||||
else if (keynum == 39) { // Arrow Right
|
||||
buttons = this._getVisibleButtons();
|
||||
targetIndex = buttons.indexOf(target);
|
||||
nextButton = buttons[targetIndex + 1];
|
||||
if (nextButton && nextButton.className == 'expand') {
|
||||
nextButton.focus();
|
||||
}
|
||||
handled = true;
|
||||
}
|
||||
else if (keynum == 40) { // Arrow Down
|
||||
buttons = this._getVisibleButtons();
|
||||
targetIndex = buttons.indexOf(target);
|
||||
nextButton = buttons[targetIndex + 1];
|
||||
if (nextButton && nextButton.className == 'expand') {
|
||||
// skip expand button
|
||||
nextButton = buttons[targetIndex + 2];
|
||||
}
|
||||
if (!nextButton) {
|
||||
// move to first button
|
||||
nextButton = buttons[0];
|
||||
}
|
||||
if (nextButton) {
|
||||
nextButton.focus();
|
||||
handled = true;
|
||||
}
|
||||
else if (keynum == 39) { // Arrow Right
|
||||
buttons = this._getVisibleButtons();
|
||||
targetIndex = buttons.indexOf(target);
|
||||
nextButton = buttons[targetIndex + 1];
|
||||
if (nextButton && nextButton.className == 'expand') {
|
||||
nextButton.focus();
|
||||
}
|
||||
handled = true;
|
||||
}
|
||||
else if (keynum == 40) { // Arrow Down
|
||||
buttons = this._getVisibleButtons();
|
||||
targetIndex = buttons.indexOf(target);
|
||||
nextButton = buttons[targetIndex + 1];
|
||||
if (nextButton && nextButton.className == 'expand') {
|
||||
// skip expand button
|
||||
nextButton = buttons[targetIndex + 2];
|
||||
}
|
||||
if (!nextButton) {
|
||||
// move to first button
|
||||
nextButton = buttons[0];
|
||||
}
|
||||
if (nextButton) {
|
||||
nextButton.focus();
|
||||
handled = true;
|
||||
}
|
||||
handled = true;
|
||||
}
|
||||
// TODO: arrow left and right
|
||||
handled = true;
|
||||
}
|
||||
// TODO: arrow left and right
|
||||
|
||||
if (handled) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
if (handled) {
|
||||
event.stopPropagation();
|
||||
event.preventDefault();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Test if an element is a child of a parent element.
|
||||
* @param {Element} child
|
||||
* @param {Element} parent
|
||||
* @return {boolean} isChild
|
||||
*/
|
||||
ContextMenu.prototype._isChildOf = function (child, parent) {
|
||||
var e = child.parentNode;
|
||||
while (e) {
|
||||
if (e == parent) {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
e = e.parentNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test if an element is a child of a parent element.
|
||||
* @param {Element} child
|
||||
* @param {Element} parent
|
||||
* @return {boolean} isChild
|
||||
*/
|
||||
ContextMenu.prototype._isChildOf = function (child, parent) {
|
||||
var e = child.parentNode;
|
||||
while (e) {
|
||||
if (e == parent) {
|
||||
return true;
|
||||
}
|
||||
e = e.parentNode;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
return ContextMenu;
|
||||
});
|
||||
module.exports = ContextMenu;
|
||||
|
|
|
@ -1,87 +1,84 @@
|
|||
define(function () {
|
||||
/**
|
||||
* The highlighter can highlight/unhighlight a node, and
|
||||
* animate the visibility of a context menu.
|
||||
* @constructor Highlighter
|
||||
*/
|
||||
function Highlighter () {
|
||||
this.locked = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* The highlighter can highlight/unhighlight a node, and
|
||||
* animate the visibility of a context menu.
|
||||
* @constructor Highlighter
|
||||
*/
|
||||
function Highlighter () {
|
||||
this.locked = false;
|
||||
/**
|
||||
* Hightlight given node and its childs
|
||||
* @param {Node} node
|
||||
*/
|
||||
Highlighter.prototype.highlight = function (node) {
|
||||
if (this.locked) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hightlight given node and its childs
|
||||
* @param {Node} node
|
||||
*/
|
||||
Highlighter.prototype.highlight = function (node) {
|
||||
if (this.locked) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.node != node) {
|
||||
// unhighlight current node
|
||||
if (this.node) {
|
||||
this.node.setHighlight(false);
|
||||
}
|
||||
|
||||
// highlight new node
|
||||
this.node = node;
|
||||
this.node.setHighlight(true);
|
||||
}
|
||||
|
||||
// cancel any current timeout
|
||||
this._cancelUnhighlight();
|
||||
};
|
||||
|
||||
/**
|
||||
* Unhighlight currently highlighted node.
|
||||
* Will be done after a delay
|
||||
*/
|
||||
Highlighter.prototype.unhighlight = function () {
|
||||
if (this.locked) {
|
||||
return;
|
||||
}
|
||||
|
||||
var me = this;
|
||||
if (this.node != node) {
|
||||
// unhighlight current node
|
||||
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);
|
||||
this.node.setHighlight(false);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Cancel an unhighlight action (if before the timeout of the unhighlight action)
|
||||
* @private
|
||||
*/
|
||||
Highlighter.prototype._cancelUnhighlight = function () {
|
||||
if (this.unhighlightTimer) {
|
||||
clearTimeout(this.unhighlightTimer);
|
||||
this.unhighlightTimer = undefined;
|
||||
}
|
||||
};
|
||||
// highlight new node
|
||||
this.node = node;
|
||||
this.node.setHighlight(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Lock highlighting or unhighlighting nodes.
|
||||
* methods highlight and unhighlight do not work while locked.
|
||||
*/
|
||||
Highlighter.prototype.lock = function () {
|
||||
this.locked = true;
|
||||
};
|
||||
// cancel any current timeout
|
||||
this._cancelUnhighlight();
|
||||
};
|
||||
|
||||
/**
|
||||
* Unlock highlighting or unhighlighting nodes
|
||||
*/
|
||||
Highlighter.prototype.unlock = function () {
|
||||
this.locked = false;
|
||||
};
|
||||
/**
|
||||
* Unhighlight currently highlighted node.
|
||||
* Will be done after a delay
|
||||
*/
|
||||
Highlighter.prototype.unhighlight = function () {
|
||||
if (this.locked) {
|
||||
return;
|
||||
}
|
||||
|
||||
return Highlighter;
|
||||
});
|
||||
var me = this;
|
||||
if (this.node) {
|
||||
this._cancelUnhighlight();
|
||||
|
||||
// do the unhighlighting after a small delay, to prevent re-highlighting
|
||||
// the same node when moving from the drag-icon to the contextmenu-icon
|
||||
// or vice versa.
|
||||
this.unhighlightTimer = setTimeout(function () {
|
||||
me.node.setHighlight(false);
|
||||
me.node = undefined;
|
||||
me.unhighlightTimer = undefined;
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Cancel an unhighlight action (if before the timeout of the unhighlight action)
|
||||
* @private
|
||||
*/
|
||||
Highlighter.prototype._cancelUnhighlight = function () {
|
||||
if (this.unhighlightTimer) {
|
||||
clearTimeout(this.unhighlightTimer);
|
||||
this.unhighlightTimer = undefined;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Lock highlighting or unhighlighting nodes.
|
||||
* methods highlight and unhighlight do not work while locked.
|
||||
*/
|
||||
Highlighter.prototype.lock = function () {
|
||||
this.locked = true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Unlock highlighting or unhighlighting nodes
|
||||
*/
|
||||
Highlighter.prototype.unlock = function () {
|
||||
this.locked = false;
|
||||
};
|
||||
|
||||
module.exports = Highlighter;
|
||||
|
|
|
@ -1,223 +1,222 @@
|
|||
define(['./util'], function (util) {
|
||||
var util = require('./util');
|
||||
|
||||
/**
|
||||
* @constructor History
|
||||
* Store action history, enables undo and redo
|
||||
* @param {JSONEditor} editor
|
||||
*/
|
||||
function History (editor) {
|
||||
this.editor = editor;
|
||||
this.clear();
|
||||
/**
|
||||
* @constructor History
|
||||
* Store action history, enables undo and redo
|
||||
* @param {JSONEditor} editor
|
||||
*/
|
||||
function History (editor) {
|
||||
this.editor = editor;
|
||||
this.clear();
|
||||
|
||||
// map with all supported actions
|
||||
this.actions = {
|
||||
'editField': {
|
||||
'undo': function (params) {
|
||||
params.node.updateField(params.oldValue);
|
||||
},
|
||||
'redo': function (params) {
|
||||
params.node.updateField(params.newValue);
|
||||
}
|
||||
// map with all supported actions
|
||||
this.actions = {
|
||||
'editField': {
|
||||
'undo': function (params) {
|
||||
params.node.updateField(params.oldValue);
|
||||
},
|
||||
'editValue': {
|
||||
'undo': function (params) {
|
||||
params.node.updateValue(params.oldValue);
|
||||
},
|
||||
'redo': function (params) {
|
||||
params.node.updateValue(params.newValue);
|
||||
}
|
||||
},
|
||||
'appendNode': {
|
||||
'undo': function (params) {
|
||||
params.parent.removeChild(params.node);
|
||||
},
|
||||
'redo': function (params) {
|
||||
params.parent.appendChild(params.node);
|
||||
}
|
||||
},
|
||||
'insertBeforeNode': {
|
||||
'undo': function (params) {
|
||||
params.parent.removeChild(params.node);
|
||||
},
|
||||
'redo': function (params) {
|
||||
params.parent.insertBefore(params.node, params.beforeNode);
|
||||
}
|
||||
},
|
||||
'insertAfterNode': {
|
||||
'undo': function (params) {
|
||||
params.parent.removeChild(params.node);
|
||||
},
|
||||
'redo': function (params) {
|
||||
params.parent.insertAfter(params.node, params.afterNode);
|
||||
}
|
||||
},
|
||||
'removeNode': {
|
||||
'undo': function (params) {
|
||||
var parent = params.parent;
|
||||
var beforeNode = parent.childs[params.index] || parent.append;
|
||||
parent.insertBefore(params.node, beforeNode);
|
||||
},
|
||||
'redo': function (params) {
|
||||
params.parent.removeChild(params.node);
|
||||
}
|
||||
},
|
||||
'duplicateNode': {
|
||||
'undo': function (params) {
|
||||
params.parent.removeChild(params.clone);
|
||||
},
|
||||
'redo': function (params) {
|
||||
params.parent.insertAfter(params.clone, params.node);
|
||||
}
|
||||
},
|
||||
'changeType': {
|
||||
'undo': function (params) {
|
||||
params.node.changeType(params.oldType);
|
||||
},
|
||||
'redo': function (params) {
|
||||
params.node.changeType(params.newType);
|
||||
}
|
||||
},
|
||||
'moveNode': {
|
||||
'undo': function (params) {
|
||||
params.startParent.moveTo(params.node, params.startIndex);
|
||||
},
|
||||
'redo': function (params) {
|
||||
params.endParent.moveTo(params.node, params.endIndex);
|
||||
}
|
||||
},
|
||||
'sort': {
|
||||
'undo': function (params) {
|
||||
var node = params.node;
|
||||
node.hideChilds();
|
||||
node.sort = params.oldSort;
|
||||
node.childs = params.oldChilds;
|
||||
node.showChilds();
|
||||
},
|
||||
'redo': function (params) {
|
||||
var node = params.node;
|
||||
node.hideChilds();
|
||||
node.sort = params.newSort;
|
||||
node.childs = params.newChilds;
|
||||
node.showChilds();
|
||||
}
|
||||
'redo': function (params) {
|
||||
params.node.updateField(params.newValue);
|
||||
}
|
||||
},
|
||||
'editValue': {
|
||||
'undo': function (params) {
|
||||
params.node.updateValue(params.oldValue);
|
||||
},
|
||||
'redo': function (params) {
|
||||
params.node.updateValue(params.newValue);
|
||||
}
|
||||
},
|
||||
'appendNode': {
|
||||
'undo': function (params) {
|
||||
params.parent.removeChild(params.node);
|
||||
},
|
||||
'redo': function (params) {
|
||||
params.parent.appendChild(params.node);
|
||||
}
|
||||
},
|
||||
'insertBeforeNode': {
|
||||
'undo': function (params) {
|
||||
params.parent.removeChild(params.node);
|
||||
},
|
||||
'redo': function (params) {
|
||||
params.parent.insertBefore(params.node, params.beforeNode);
|
||||
}
|
||||
},
|
||||
'insertAfterNode': {
|
||||
'undo': function (params) {
|
||||
params.parent.removeChild(params.node);
|
||||
},
|
||||
'redo': function (params) {
|
||||
params.parent.insertAfter(params.node, params.afterNode);
|
||||
}
|
||||
},
|
||||
'removeNode': {
|
||||
'undo': function (params) {
|
||||
var parent = params.parent;
|
||||
var beforeNode = parent.childs[params.index] || parent.append;
|
||||
parent.insertBefore(params.node, beforeNode);
|
||||
},
|
||||
'redo': function (params) {
|
||||
params.parent.removeChild(params.node);
|
||||
}
|
||||
},
|
||||
'duplicateNode': {
|
||||
'undo': function (params) {
|
||||
params.parent.removeChild(params.clone);
|
||||
},
|
||||
'redo': function (params) {
|
||||
params.parent.insertAfter(params.clone, params.node);
|
||||
}
|
||||
},
|
||||
'changeType': {
|
||||
'undo': function (params) {
|
||||
params.node.changeType(params.oldType);
|
||||
},
|
||||
'redo': function (params) {
|
||||
params.node.changeType(params.newType);
|
||||
}
|
||||
},
|
||||
'moveNode': {
|
||||
'undo': function (params) {
|
||||
params.startParent.moveTo(params.node, params.startIndex);
|
||||
},
|
||||
'redo': function (params) {
|
||||
params.endParent.moveTo(params.node, params.endIndex);
|
||||
}
|
||||
},
|
||||
'sort': {
|
||||
'undo': function (params) {
|
||||
var node = params.node;
|
||||
node.hideChilds();
|
||||
node.sort = params.oldSort;
|
||||
node.childs = params.oldChilds;
|
||||
node.showChilds();
|
||||
},
|
||||
'redo': function (params) {
|
||||
var node = params.node;
|
||||
node.hideChilds();
|
||||
node.sort = params.newSort;
|
||||
node.childs = params.newChilds;
|
||||
node.showChilds();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: restore the original caret position and selection with each undo
|
||||
// TODO: implement history for actions "expand", "collapse", "scroll", "setDocument"
|
||||
};
|
||||
// TODO: restore the original caret position and selection with each undo
|
||||
// TODO: implement history for actions "expand", "collapse", "scroll", "setDocument"
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* The method onChange is executed when the History is changed, and can
|
||||
* be overloaded.
|
||||
*/
|
||||
History.prototype.onChange = function () {};
|
||||
|
||||
/**
|
||||
* Add a new action to the history
|
||||
* @param {String} action The executed action. Available actions: "editField",
|
||||
* "editValue", "changeType", "appendNode",
|
||||
* "removeNode", "duplicateNode", "moveNode"
|
||||
* @param {Object} params Object containing parameters describing the change.
|
||||
* The parameters in params depend on the action (for
|
||||
* example for "editValue" the Node, old value, and new
|
||||
* value are provided). params contains all information
|
||||
* needed to undo or redo the action.
|
||||
*/
|
||||
History.prototype.add = function (action, params) {
|
||||
this.index++;
|
||||
this.history[this.index] = {
|
||||
'action': action,
|
||||
'params': params,
|
||||
'timestamp': new Date()
|
||||
};
|
||||
|
||||
// remove redo actions which are invalid now
|
||||
if (this.index < this.history.length - 1) {
|
||||
this.history.splice(this.index + 1, this.history.length - this.index - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* The method onChange is executed when the History is changed, and can
|
||||
* be overloaded.
|
||||
*/
|
||||
History.prototype.onChange = function () {};
|
||||
// fire onchange event
|
||||
this.onChange();
|
||||
};
|
||||
|
||||
/**
|
||||
* Add a new action to the history
|
||||
* @param {String} action The executed action. Available actions: "editField",
|
||||
* "editValue", "changeType", "appendNode",
|
||||
* "removeNode", "duplicateNode", "moveNode"
|
||||
* @param {Object} params Object containing parameters describing the change.
|
||||
* The parameters in params depend on the action (for
|
||||
* example for "editValue" the Node, old value, and new
|
||||
* value are provided). params contains all information
|
||||
* needed to undo or redo the action.
|
||||
*/
|
||||
History.prototype.add = function (action, params) {
|
||||
/**
|
||||
* Clear history
|
||||
*/
|
||||
History.prototype.clear = function () {
|
||||
this.history = [];
|
||||
this.index = -1;
|
||||
|
||||
// fire onchange event
|
||||
this.onChange();
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if there is an action available for undo
|
||||
* @return {Boolean} canUndo
|
||||
*/
|
||||
History.prototype.canUndo = function () {
|
||||
return (this.index >= 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if there is an action available for redo
|
||||
* @return {Boolean} canRedo
|
||||
*/
|
||||
History.prototype.canRedo = function () {
|
||||
return (this.index < this.history.length - 1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Undo the last action
|
||||
*/
|
||||
History.prototype.undo = function () {
|
||||
if (this.canUndo()) {
|
||||
var obj = this.history[this.index];
|
||||
if (obj) {
|
||||
var action = this.actions[obj.action];
|
||||
if (action && action.undo) {
|
||||
action.undo(obj.params);
|
||||
if (obj.params.oldSelection) {
|
||||
this.editor.setSelection(obj.params.oldSelection);
|
||||
}
|
||||
}
|
||||
else {
|
||||
util.log('Error: unknown action "' + obj.action + '"');
|
||||
}
|
||||
}
|
||||
this.index--;
|
||||
|
||||
// fire onchange event
|
||||
this.onChange();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Redo the last action
|
||||
*/
|
||||
History.prototype.redo = function () {
|
||||
if (this.canRedo()) {
|
||||
this.index++;
|
||||
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);
|
||||
var obj = this.history[this.index];
|
||||
if (obj) {
|
||||
var action = this.actions[obj.action];
|
||||
if (action && action.redo) {
|
||||
action.redo(obj.params);
|
||||
if (obj.params.newSelection) {
|
||||
this.editor.setSelection(obj.params.newSelection);
|
||||
}
|
||||
}
|
||||
else {
|
||||
util.log('Error: unknown action "' + obj.action + '"');
|
||||
}
|
||||
}
|
||||
|
||||
// fire onchange event
|
||||
this.onChange();
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Clear history
|
||||
*/
|
||||
History.prototype.clear = function () {
|
||||
this.history = [];
|
||||
this.index = -1;
|
||||
|
||||
// fire onchange event
|
||||
this.onChange();
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if there is an action available for undo
|
||||
* @return {Boolean} canUndo
|
||||
*/
|
||||
History.prototype.canUndo = function () {
|
||||
return (this.index >= 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if there is an action available for redo
|
||||
* @return {Boolean} canRedo
|
||||
*/
|
||||
History.prototype.canRedo = function () {
|
||||
return (this.index < this.history.length - 1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Undo the last action
|
||||
*/
|
||||
History.prototype.undo = function () {
|
||||
if (this.canUndo()) {
|
||||
var obj = this.history[this.index];
|
||||
if (obj) {
|
||||
var action = this.actions[obj.action];
|
||||
if (action && action.undo) {
|
||||
action.undo(obj.params);
|
||||
if (obj.params.oldSelection) {
|
||||
this.editor.setSelection(obj.params.oldSelection);
|
||||
}
|
||||
}
|
||||
else {
|
||||
util.log('Error: unknown action "' + obj.action + '"');
|
||||
}
|
||||
}
|
||||
this.index--;
|
||||
|
||||
// fire onchange event
|
||||
this.onChange();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Redo the last action
|
||||
*/
|
||||
History.prototype.redo = function () {
|
||||
if (this.canRedo()) {
|
||||
this.index++;
|
||||
|
||||
var obj = this.history[this.index];
|
||||
if (obj) {
|
||||
var action = this.actions[obj.action];
|
||||
if (action && action.redo) {
|
||||
action.redo(obj.params);
|
||||
if (obj.params.newSelection) {
|
||||
this.editor.setSelection(obj.params.newSelection);
|
||||
}
|
||||
}
|
||||
else {
|
||||
util.log('Error: unknown action "' + obj.action + '"');
|
||||
}
|
||||
}
|
||||
|
||||
// fire onchange event
|
||||
this.onChange();
|
||||
}
|
||||
};
|
||||
|
||||
return History;
|
||||
});
|
||||
module.exports = History;
|
||||
|
|
|
@ -1,261 +1,262 @@
|
|||
define(['./treemode', './textmode', './util'], function (treemode, textmode, util) {
|
||||
var treemode = require('./treemode');
|
||||
var textmode = require('./textmode');
|
||||
var util = require('./util');
|
||||
|
||||
/**
|
||||
* @constructor JSONEditor
|
||||
* @param {Element} container Container element
|
||||
* @param {Object} [options] Object with options. available options:
|
||||
* {String} mode Editor mode. Available values:
|
||||
* 'tree' (default), 'view',
|
||||
* 'form', 'text', and 'code'.
|
||||
* {function} change Callback method, triggered
|
||||
* on change of contents
|
||||
* {Boolean} search Enable search box.
|
||||
* True by default
|
||||
* Only applicable for modes
|
||||
* 'tree', 'view', and 'form'
|
||||
* {Boolean} history Enable history (undo/redo).
|
||||
* True by default
|
||||
* Only applicable for modes
|
||||
* 'tree', 'view', and 'form'
|
||||
* {String} name Field name for the root node.
|
||||
* Only applicable for modes
|
||||
* 'tree', 'view', and 'form'
|
||||
* {Number} indentation Number of indentation
|
||||
* spaces. 4 by default.
|
||||
* Only applicable for
|
||||
* modes 'text' and 'code'
|
||||
* @param {Object | undefined} json JSON object
|
||||
*/
|
||||
function JSONEditor (container, options, json) {
|
||||
if (!(this instanceof JSONEditor)) {
|
||||
throw new Error('JSONEditor constructor called without "new".');
|
||||
}
|
||||
|
||||
// check for unsupported browser (IE8 and older)
|
||||
var ieVersion = util.getInternetExplorerVersion();
|
||||
if (ieVersion != -1 && ieVersion < 9) {
|
||||
throw new Error('Unsupported browser, IE9 or newer required. ' +
|
||||
'Please install the newest version of your browser.');
|
||||
}
|
||||
|
||||
if (arguments.length) {
|
||||
this._create(container, options, json);
|
||||
}
|
||||
/**
|
||||
* @constructor JSONEditor
|
||||
* @param {Element} container Container element
|
||||
* @param {Object} [options] Object with options. available options:
|
||||
* {String} mode Editor mode. Available values:
|
||||
* 'tree' (default), 'view',
|
||||
* 'form', 'text', and 'code'.
|
||||
* {function} change Callback method, triggered
|
||||
* on change of contents
|
||||
* {Boolean} search Enable search box.
|
||||
* True by default
|
||||
* Only applicable for modes
|
||||
* 'tree', 'view', and 'form'
|
||||
* {Boolean} history Enable history (undo/redo).
|
||||
* True by default
|
||||
* Only applicable for modes
|
||||
* 'tree', 'view', and 'form'
|
||||
* {String} name Field name for the root node.
|
||||
* Only applicable for modes
|
||||
* 'tree', 'view', and 'form'
|
||||
* {Number} indentation Number of indentation
|
||||
* spaces. 4 by default.
|
||||
* Only applicable for
|
||||
* modes 'text' and 'code'
|
||||
* @param {Object | undefined} json JSON object
|
||||
*/
|
||||
function JSONEditor (container, options, json) {
|
||||
if (!(this instanceof JSONEditor)) {
|
||||
throw new Error('JSONEditor constructor called without "new".');
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for all registered modes. Example:
|
||||
* {
|
||||
* tree: {
|
||||
* mixin: TreeEditor,
|
||||
* data: 'json'
|
||||
* },
|
||||
* text: {
|
||||
* mixin: TextEditor,
|
||||
* data: 'text'
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @type { Object.<String, {mixin: Object, data: String} > }
|
||||
*/
|
||||
JSONEditor.modes = {};
|
||||
// check for unsupported browser (IE8 and older)
|
||||
var ieVersion = util.getInternetExplorerVersion();
|
||||
if (ieVersion != -1 && ieVersion < 9) {
|
||||
throw new Error('Unsupported browser, IE9 or newer required. ' +
|
||||
'Please install the newest version of your browser.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the JSONEditor
|
||||
* @param {Element} container Container element
|
||||
* @param {Object} [options] See description in constructor
|
||||
* @param {Object | undefined} json JSON object
|
||||
* @private
|
||||
*/
|
||||
JSONEditor.prototype._create = function (container, options, json) {
|
||||
this.container = container;
|
||||
this.options = options || {};
|
||||
this.json = json || {};
|
||||
if (arguments.length) {
|
||||
this._create(container, options, json);
|
||||
}
|
||||
}
|
||||
|
||||
var mode = this.options.mode || 'tree';
|
||||
this.setMode(mode);
|
||||
};
|
||||
/**
|
||||
* Configuration for all registered modes. Example:
|
||||
* {
|
||||
* tree: {
|
||||
* mixin: TreeEditor,
|
||||
* data: 'json'
|
||||
* },
|
||||
* text: {
|
||||
* mixin: TextEditor,
|
||||
* data: 'text'
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* @type { Object.<String, {mixin: Object, data: String} > }
|
||||
*/
|
||||
JSONEditor.modes = {};
|
||||
|
||||
/**
|
||||
* Detach the editor from the DOM
|
||||
* @private
|
||||
*/
|
||||
JSONEditor.prototype._delete = function () {};
|
||||
/**
|
||||
* Create the JSONEditor
|
||||
* @param {Element} container Container element
|
||||
* @param {Object} [options] See description in constructor
|
||||
* @param {Object | undefined} json JSON object
|
||||
* @private
|
||||
*/
|
||||
JSONEditor.prototype._create = function (container, options, json) {
|
||||
this.container = container;
|
||||
this.options = options || {};
|
||||
this.json = json || {};
|
||||
|
||||
/**
|
||||
* Set JSON object in editor
|
||||
* @param {Object | undefined} json JSON data
|
||||
*/
|
||||
JSONEditor.prototype.set = function (json) {
|
||||
this.json = json;
|
||||
};
|
||||
var mode = this.options.mode || 'tree';
|
||||
this.setMode(mode);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get JSON from the editor
|
||||
* @returns {Object} json
|
||||
*/
|
||||
JSONEditor.prototype.get = function () {
|
||||
return this.json;
|
||||
};
|
||||
/**
|
||||
* Detach the editor from the DOM
|
||||
* @private
|
||||
*/
|
||||
JSONEditor.prototype._delete = function () {};
|
||||
|
||||
/**
|
||||
* Set string containing JSON for the editor
|
||||
* @param {String | undefined} jsonText
|
||||
*/
|
||||
JSONEditor.prototype.setText = function (jsonText) {
|
||||
this.json = util.parse(jsonText);
|
||||
};
|
||||
/**
|
||||
* Set JSON object in editor
|
||||
* @param {Object | undefined} json JSON data
|
||||
*/
|
||||
JSONEditor.prototype.set = function (json) {
|
||||
this.json = json;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get stringified JSON contents from the editor
|
||||
* @returns {String} jsonText
|
||||
*/
|
||||
JSONEditor.prototype.getText = function () {
|
||||
return JSON.stringify(this.json);
|
||||
};
|
||||
/**
|
||||
* Get JSON from the editor
|
||||
* @returns {Object} json
|
||||
*/
|
||||
JSONEditor.prototype.get = function () {
|
||||
return this.json;
|
||||
};
|
||||
|
||||
/**
|
||||
* Set a field name for the root node.
|
||||
* @param {String | undefined} name
|
||||
*/
|
||||
JSONEditor.prototype.setName = function (name) {
|
||||
if (!this.options) {
|
||||
this.options = {};
|
||||
}
|
||||
this.options.name = name;
|
||||
};
|
||||
/**
|
||||
* Set string containing JSON for the editor
|
||||
* @param {String | undefined} jsonText
|
||||
*/
|
||||
JSONEditor.prototype.setText = function (jsonText) {
|
||||
this.json = util.parse(jsonText);
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the field name for the root node.
|
||||
* @return {String | undefined} name
|
||||
*/
|
||||
JSONEditor.prototype.getName = function () {
|
||||
return this.options && this.options.name;
|
||||
};
|
||||
/**
|
||||
* Get stringified JSON contents from the editor
|
||||
* @returns {String} jsonText
|
||||
*/
|
||||
JSONEditor.prototype.getText = function () {
|
||||
return JSON.stringify(this.json);
|
||||
};
|
||||
|
||||
/**
|
||||
* Change the mode of the editor.
|
||||
* JSONEditor will be extended with all methods needed for the chosen mode.
|
||||
* @param {String} mode Available modes: 'tree' (default), 'view', 'form',
|
||||
* 'text', and 'code'.
|
||||
*/
|
||||
JSONEditor.prototype.setMode = function (mode) {
|
||||
var container = this.container,
|
||||
options = util.extend({}, this.options),
|
||||
data,
|
||||
name;
|
||||
/**
|
||||
* Set a field name for the root node.
|
||||
* @param {String | undefined} name
|
||||
*/
|
||||
JSONEditor.prototype.setName = function (name) {
|
||||
if (!this.options) {
|
||||
this.options = {};
|
||||
}
|
||||
this.options.name = name;
|
||||
};
|
||||
|
||||
options.mode = mode;
|
||||
var config = JSONEditor.modes[mode];
|
||||
if (config) {
|
||||
try {
|
||||
var asText = (config.data == 'text');
|
||||
name = this.getName();
|
||||
data = this[asText ? 'getText' : 'get'](); // get text or json
|
||||
/**
|
||||
* Get the field name for the root node.
|
||||
* @return {String | undefined} name
|
||||
*/
|
||||
JSONEditor.prototype.getName = function () {
|
||||
return this.options && this.options.name;
|
||||
};
|
||||
|
||||
this._delete();
|
||||
util.clear(this);
|
||||
util.extend(this, config.mixin);
|
||||
this.create(container, options);
|
||||
/**
|
||||
* Change the mode of the editor.
|
||||
* JSONEditor will be extended with all methods needed for the chosen mode.
|
||||
* @param {String} mode Available modes: 'tree' (default), 'view', 'form',
|
||||
* 'text', and 'code'.
|
||||
*/
|
||||
JSONEditor.prototype.setMode = function (mode) {
|
||||
var container = this.container,
|
||||
options = util.extend({}, this.options),
|
||||
data,
|
||||
name;
|
||||
|
||||
this.setName(name);
|
||||
this[asText ? 'setText' : 'set'](data); // set text or json
|
||||
options.mode = mode;
|
||||
var config = JSONEditor.modes[mode];
|
||||
if (config) {
|
||||
try {
|
||||
var asText = (config.data == 'text');
|
||||
name = this.getName();
|
||||
data = this[asText ? 'getText' : 'get'](); // get text or json
|
||||
|
||||
if (typeof config.load === 'function') {
|
||||
try {
|
||||
config.load.call(this);
|
||||
}
|
||||
catch (err) {}
|
||||
this._delete();
|
||||
util.clear(this);
|
||||
util.extend(this, config.mixin);
|
||||
this.create(container, options);
|
||||
|
||||
this.setName(name);
|
||||
this[asText ? 'setText' : 'set'](data); // set text or json
|
||||
|
||||
if (typeof config.load === 'function') {
|
||||
try {
|
||||
config.load.call(this);
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
this._onError(err);
|
||||
catch (err) {}
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw new Error('Unknown mode "' + options.mode + '"');
|
||||
catch (err) {
|
||||
this._onError(err);
|
||||
}
|
||||
};
|
||||
}
|
||||
else {
|
||||
throw new Error('Unknown mode "' + options.mode + '"');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Throw an error. If an error callback is configured in options.error, this
|
||||
* callback will be invoked. Else, a regular error is thrown.
|
||||
* @param {Error} err
|
||||
* @private
|
||||
*/
|
||||
JSONEditor.prototype._onError = function(err) {
|
||||
// TODO: onError is deprecated since version 2.2.0. cleanup some day
|
||||
if (typeof this.onError === 'function') {
|
||||
util.log('WARNING: JSONEditor.onError is deprecated. ' +
|
||||
'Use options.error instead.');
|
||||
this.onError(err);
|
||||
/**
|
||||
* Throw an error. If an error callback is configured in options.error, this
|
||||
* callback will be invoked. Else, a regular error is thrown.
|
||||
* @param {Error} err
|
||||
* @private
|
||||
*/
|
||||
JSONEditor.prototype._onError = function(err) {
|
||||
// TODO: onError is deprecated since version 2.2.0. cleanup some day
|
||||
if (typeof this.onError === 'function') {
|
||||
util.log('WARNING: JSONEditor.onError is deprecated. ' +
|
||||
'Use options.error instead.');
|
||||
this.onError(err);
|
||||
}
|
||||
|
||||
if (this.options && typeof this.options.error === 'function') {
|
||||
this.options.error(err);
|
||||
}
|
||||
else {
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Register a plugin with one ore multiple modes for the JSON Editor.
|
||||
*
|
||||
* A mode is described as an object with properties:
|
||||
*
|
||||
* - `mode: String` The name of the mode.
|
||||
* - `mixin: Object` An object containing the mixin functions which
|
||||
* will be added to the JSONEditor. Must contain functions
|
||||
* create, get, getText, set, and setText. May have
|
||||
* additional functions.
|
||||
* When the JSONEditor switches to a mixin, all mixin
|
||||
* functions are added to the JSONEditor, and then
|
||||
* the function `create(container, options)` is executed.
|
||||
* - `data: 'text' | 'json'` The type of data that will be used to load the mixin.
|
||||
* - `[load: function]` An optional function called after the mixin
|
||||
* has been loaded.
|
||||
*
|
||||
* @param {Object | Array} mode A mode object or an array with multiple mode objects.
|
||||
*/
|
||||
JSONEditor.registerMode = function (mode) {
|
||||
var i, prop;
|
||||
|
||||
if (util.isArray(mode)) {
|
||||
// multiple modes
|
||||
for (i = 0; i < mode.length; i++) {
|
||||
JSONEditor.registerMode(mode[i]);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// validate the new mode
|
||||
if (!('mode' in mode)) throw new Error('Property "mode" missing');
|
||||
if (!('mixin' in mode)) throw new Error('Property "mixin" missing');
|
||||
if (!('data' in mode)) throw new Error('Property "data" missing');
|
||||
var name = mode.mode;
|
||||
if (name in JSONEditor.modes) {
|
||||
throw new Error('Mode "' + name + '" already registered');
|
||||
}
|
||||
|
||||
if (this.options && typeof this.options.error === 'function') {
|
||||
this.options.error(err);
|
||||
// validate the mixin
|
||||
if (typeof mode.mixin.create !== 'function') {
|
||||
throw new Error('Required function "create" missing on mixin');
|
||||
}
|
||||
else {
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Register a plugin with one ore multiple modes for the JSON Editor.
|
||||
*
|
||||
* A mode is described as an object with properties:
|
||||
*
|
||||
* - `mode: String` The name of the mode.
|
||||
* - `mixin: Object` An object containing the mixin functions which
|
||||
* will be added to the JSONEditor. Must contain functions
|
||||
* create, get, getText, set, and setText. May have
|
||||
* additional functions.
|
||||
* When the JSONEditor switches to a mixin, all mixin
|
||||
* functions are added to the JSONEditor, and then
|
||||
* the function `create(container, options)` is executed.
|
||||
* - `data: 'text' | 'json'` The type of data that will be used to load the mixin.
|
||||
* - `[load: function]` An optional function called after the mixin
|
||||
* has been loaded.
|
||||
*
|
||||
* @param {Object | Array} mode A mode object or an array with multiple mode objects.
|
||||
*/
|
||||
JSONEditor.registerMode = function (mode) {
|
||||
var i, prop;
|
||||
|
||||
if (util.isArray(mode)) {
|
||||
// multiple modes
|
||||
for (i = 0; i < mode.length; i++) {
|
||||
JSONEditor.registerMode(mode[i]);
|
||||
var reserved = ['setMode', 'registerMode', 'modes'];
|
||||
for (i = 0; i < reserved.length; i++) {
|
||||
prop = reserved[i];
|
||||
if (prop in mode.mixin) {
|
||||
throw new Error('Reserved property "' + prop + '" not allowed in mixin');
|
||||
}
|
||||
}
|
||||
else {
|
||||
// validate the new mode
|
||||
if (!('mode' in mode)) throw new Error('Property "mode" missing');
|
||||
if (!('mixin' in mode)) throw new Error('Property "mixin" missing');
|
||||
if (!('data' in mode)) throw new Error('Property "data" missing');
|
||||
var name = mode.mode;
|
||||
if (name in JSONEditor.modes) {
|
||||
throw new Error('Mode "' + name + '" already registered');
|
||||
}
|
||||
|
||||
// validate the mixin
|
||||
if (typeof mode.mixin.create !== 'function') {
|
||||
throw new Error('Required function "create" missing on mixin');
|
||||
}
|
||||
var reserved = ['setMode', 'registerMode', 'modes'];
|
||||
for (i = 0; i < reserved.length; i++) {
|
||||
prop = reserved[i];
|
||||
if (prop in mode.mixin) {
|
||||
throw new Error('Reserved property "' + prop + '" not allowed in mixin');
|
||||
}
|
||||
}
|
||||
JSONEditor.modes[name] = mode;
|
||||
}
|
||||
};
|
||||
|
||||
JSONEditor.modes[name] = mode;
|
||||
}
|
||||
};
|
||||
// register tree and text modes
|
||||
JSONEditor.registerMode(treemode);
|
||||
JSONEditor.registerMode(textmode);
|
||||
|
||||
// register tree and text modes
|
||||
JSONEditor.registerMode(treemode);
|
||||
JSONEditor.registerMode(textmode);
|
||||
|
||||
return JSONEditor;
|
||||
});
|
||||
module.exports = JSONEditor;
|
||||
|
|
5425
src/js/Node.js
5425
src/js/Node.js
File diff suppressed because it is too large
Load Diff
|
@ -1,293 +1,288 @@
|
|||
define(function () {
|
||||
/**
|
||||
* @constructor SearchBox
|
||||
* Create a search box in given HTML container
|
||||
* @param {JSONEditor} editor The JSON Editor to attach to
|
||||
* @param {Element} container HTML container element of where to
|
||||
* create the search box
|
||||
*/
|
||||
function SearchBox (editor, container) {
|
||||
var searchBox = this;
|
||||
|
||||
/**
|
||||
* @constructor SearchBox
|
||||
* Create a search box in given HTML container
|
||||
* @param {JSONEditor} editor The JSON Editor to attach to
|
||||
* @param {Element} container HTML container element of where to
|
||||
* create the search box
|
||||
*/
|
||||
function SearchBox (editor, container) {
|
||||
var searchBox = this;
|
||||
this.editor = editor;
|
||||
this.timeout = undefined;
|
||||
this.delay = 200; // ms
|
||||
this.lastText = undefined;
|
||||
|
||||
this.editor = editor;
|
||||
this.timeout = undefined;
|
||||
this.delay = 200; // ms
|
||||
this.lastText = undefined;
|
||||
this.dom = {};
|
||||
this.dom.container = container;
|
||||
|
||||
this.dom = {};
|
||||
this.dom.container = container;
|
||||
var table = document.createElement('table');
|
||||
this.dom.table = table;
|
||||
table.className = 'search';
|
||||
container.appendChild(table);
|
||||
var tbody = document.createElement('tbody');
|
||||
this.dom.tbody = tbody;
|
||||
table.appendChild(tbody);
|
||||
var tr = document.createElement('tr');
|
||||
tbody.appendChild(tr);
|
||||
|
||||
var table = document.createElement('table');
|
||||
this.dom.table = table;
|
||||
table.className = 'search';
|
||||
container.appendChild(table);
|
||||
var tbody = document.createElement('tbody');
|
||||
this.dom.tbody = tbody;
|
||||
table.appendChild(tbody);
|
||||
var tr = document.createElement('tr');
|
||||
tbody.appendChild(tr);
|
||||
var td = document.createElement('td');
|
||||
tr.appendChild(td);
|
||||
var results = document.createElement('div');
|
||||
this.dom.results = results;
|
||||
results.className = 'results';
|
||||
td.appendChild(results);
|
||||
|
||||
var td = document.createElement('td');
|
||||
tr.appendChild(td);
|
||||
var results = document.createElement('div');
|
||||
this.dom.results = results;
|
||||
results.className = 'results';
|
||||
td.appendChild(results);
|
||||
td = document.createElement('td');
|
||||
tr.appendChild(td);
|
||||
var divInput = document.createElement('div');
|
||||
this.dom.input = divInput;
|
||||
divInput.className = 'frame';
|
||||
divInput.title = 'Search fields and values';
|
||||
td.appendChild(divInput);
|
||||
|
||||
td = document.createElement('td');
|
||||
tr.appendChild(td);
|
||||
var divInput = document.createElement('div');
|
||||
this.dom.input = divInput;
|
||||
divInput.className = 'frame';
|
||||
divInput.title = 'Search fields and values';
|
||||
td.appendChild(divInput);
|
||||
// table to contain the text input and search button
|
||||
var tableInput = document.createElement('table');
|
||||
divInput.appendChild(tableInput);
|
||||
var tbodySearch = document.createElement('tbody');
|
||||
tableInput.appendChild(tbodySearch);
|
||||
tr = document.createElement('tr');
|
||||
tbodySearch.appendChild(tr);
|
||||
|
||||
// table to contain the text input and search button
|
||||
var tableInput = document.createElement('table');
|
||||
divInput.appendChild(tableInput);
|
||||
var tbodySearch = document.createElement('tbody');
|
||||
tableInput.appendChild(tbodySearch);
|
||||
tr = document.createElement('tr');
|
||||
tbodySearch.appendChild(tr);
|
||||
var refreshSearch = document.createElement('button');
|
||||
refreshSearch.className = 'refresh';
|
||||
td = document.createElement('td');
|
||||
td.appendChild(refreshSearch);
|
||||
tr.appendChild(td);
|
||||
|
||||
var refreshSearch = document.createElement('button');
|
||||
refreshSearch.className = 'refresh';
|
||||
td = document.createElement('td');
|
||||
td.appendChild(refreshSearch);
|
||||
tr.appendChild(td);
|
||||
var search = document.createElement('input');
|
||||
this.dom.search = search;
|
||||
search.oninput = function (event) {
|
||||
searchBox._onDelayedSearch(event);
|
||||
};
|
||||
search.onchange = function (event) { // For IE 9
|
||||
searchBox._onSearch(event);
|
||||
};
|
||||
search.onkeydown = function (event) {
|
||||
searchBox._onKeyDown(event);
|
||||
};
|
||||
search.onkeyup = function (event) {
|
||||
searchBox._onKeyUp(event);
|
||||
};
|
||||
refreshSearch.onclick = function (event) {
|
||||
search.select();
|
||||
};
|
||||
|
||||
var search = document.createElement('input');
|
||||
this.dom.search = search;
|
||||
search.oninput = function (event) {
|
||||
searchBox._onDelayedSearch(event);
|
||||
};
|
||||
search.onchange = function (event) { // For IE 9
|
||||
searchBox._onSearch(event);
|
||||
};
|
||||
search.onkeydown = function (event) {
|
||||
searchBox._onKeyDown(event);
|
||||
};
|
||||
search.onkeyup = function (event) {
|
||||
searchBox._onKeyUp(event);
|
||||
};
|
||||
refreshSearch.onclick = function (event) {
|
||||
search.select();
|
||||
};
|
||||
// TODO: ESC in FF restores the last input, is a FF bug, https://bugzilla.mozilla.org/show_bug.cgi?id=598819
|
||||
td = document.createElement('td');
|
||||
td.appendChild(search);
|
||||
tr.appendChild(td);
|
||||
|
||||
// TODO: ESC in FF restores the last input, is a FF bug, https://bugzilla.mozilla.org/show_bug.cgi?id=598819
|
||||
td = document.createElement('td');
|
||||
td.appendChild(search);
|
||||
tr.appendChild(td);
|
||||
var searchNext = document.createElement('button');
|
||||
searchNext.title = 'Next result (Enter)';
|
||||
searchNext.className = 'next';
|
||||
searchNext.onclick = function () {
|
||||
searchBox.next();
|
||||
};
|
||||
td = document.createElement('td');
|
||||
td.appendChild(searchNext);
|
||||
tr.appendChild(td);
|
||||
|
||||
var searchNext = document.createElement('button');
|
||||
searchNext.title = 'Next result (Enter)';
|
||||
searchNext.className = 'next';
|
||||
searchNext.onclick = function () {
|
||||
searchBox.next();
|
||||
};
|
||||
td = document.createElement('td');
|
||||
td.appendChild(searchNext);
|
||||
tr.appendChild(td);
|
||||
var searchPrevious = document.createElement('button');
|
||||
searchPrevious.title = 'Previous result (Shift+Enter)';
|
||||
searchPrevious.className = 'previous';
|
||||
searchPrevious.onclick = function () {
|
||||
searchBox.previous();
|
||||
};
|
||||
td = document.createElement('td');
|
||||
td.appendChild(searchPrevious);
|
||||
tr.appendChild(td);
|
||||
}
|
||||
|
||||
var searchPrevious = document.createElement('button');
|
||||
searchPrevious.title = 'Previous result (Shift+Enter)';
|
||||
searchPrevious.className = 'previous';
|
||||
searchPrevious.onclick = function () {
|
||||
searchBox.previous();
|
||||
};
|
||||
td = document.createElement('td');
|
||||
td.appendChild(searchPrevious);
|
||||
tr.appendChild(td);
|
||||
/**
|
||||
* Go to the next search result
|
||||
* @param {boolean} [focus] If true, focus will be set to the next result
|
||||
* focus is false by default.
|
||||
*/
|
||||
SearchBox.prototype.next = function(focus) {
|
||||
if (this.results != undefined) {
|
||||
var index = (this.resultIndex != undefined) ? this.resultIndex + 1 : 0;
|
||||
if (index > this.results.length - 1) {
|
||||
index = 0;
|
||||
}
|
||||
this._setActiveResult(index, focus);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Go to the next search result
|
||||
* @param {boolean} [focus] If true, focus will be set to the next result
|
||||
* focus is false by default.
|
||||
*/
|
||||
SearchBox.prototype.next = function(focus) {
|
||||
if (this.results != undefined) {
|
||||
var index = (this.resultIndex != undefined) ? this.resultIndex + 1 : 0;
|
||||
if (index > this.results.length - 1) {
|
||||
index = 0;
|
||||
}
|
||||
this._setActiveResult(index, focus);
|
||||
/**
|
||||
* Go to the prevous search result
|
||||
* @param {boolean} [focus] If true, focus will be set to the next result
|
||||
* focus is false by default.
|
||||
*/
|
||||
SearchBox.prototype.previous = function(focus) {
|
||||
if (this.results != undefined) {
|
||||
var max = this.results.length - 1;
|
||||
var index = (this.resultIndex != undefined) ? this.resultIndex - 1 : max;
|
||||
if (index < 0) {
|
||||
index = max;
|
||||
}
|
||||
};
|
||||
this._setActiveResult(index, focus);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Go to the prevous search result
|
||||
* @param {boolean} [focus] If true, focus will be set to the next result
|
||||
* focus is false by default.
|
||||
*/
|
||||
SearchBox.prototype.previous = function(focus) {
|
||||
if (this.results != undefined) {
|
||||
var max = this.results.length - 1;
|
||||
var index = (this.resultIndex != undefined) ? this.resultIndex - 1 : max;
|
||||
if (index < 0) {
|
||||
index = max;
|
||||
}
|
||||
this._setActiveResult(index, focus);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Set new value for the current active result
|
||||
* @param {Number} index
|
||||
* @param {boolean} [focus] If true, focus will be set to the next result.
|
||||
* focus is false by default.
|
||||
* @private
|
||||
*/
|
||||
SearchBox.prototype._setActiveResult = function(index, focus) {
|
||||
// de-activate current active result
|
||||
if (this.activeResult) {
|
||||
var prevNode = this.activeResult.node;
|
||||
var prevElem = this.activeResult.elem;
|
||||
if (prevElem == 'field') {
|
||||
delete prevNode.searchFieldActive;
|
||||
}
|
||||
else {
|
||||
delete prevNode.searchValueActive;
|
||||
}
|
||||
prevNode.updateDom();
|
||||
}
|
||||
|
||||
if (!this.results || !this.results[index]) {
|
||||
// out of range, set to undefined
|
||||
this.resultIndex = undefined;
|
||||
this.activeResult = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
this.resultIndex = index;
|
||||
|
||||
// set new node active
|
||||
var node = this.results[this.resultIndex].node;
|
||||
var elem = this.results[this.resultIndex].elem;
|
||||
if (elem == 'field') {
|
||||
node.searchFieldActive = true;
|
||||
/**
|
||||
* Set new value for the current active result
|
||||
* @param {Number} index
|
||||
* @param {boolean} [focus] If true, focus will be set to the next result.
|
||||
* focus is false by default.
|
||||
* @private
|
||||
*/
|
||||
SearchBox.prototype._setActiveResult = function(index, focus) {
|
||||
// de-activate current active result
|
||||
if (this.activeResult) {
|
||||
var prevNode = this.activeResult.node;
|
||||
var prevElem = this.activeResult.elem;
|
||||
if (prevElem == 'field') {
|
||||
delete prevNode.searchFieldActive;
|
||||
}
|
||||
else {
|
||||
node.searchValueActive = true;
|
||||
delete prevNode.searchValueActive;
|
||||
}
|
||||
this.activeResult = this.results[this.resultIndex];
|
||||
node.updateDom();
|
||||
prevNode.updateDom();
|
||||
}
|
||||
|
||||
// TODO: not so nice that the focus is only set after the animation is finished
|
||||
node.scrollTo(function () {
|
||||
if (focus) {
|
||||
node.focus(elem);
|
||||
}
|
||||
});
|
||||
};
|
||||
if (!this.results || !this.results[index]) {
|
||||
// out of range, set to undefined
|
||||
this.resultIndex = undefined;
|
||||
this.activeResult = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel any running onDelayedSearch.
|
||||
* @private
|
||||
*/
|
||||
SearchBox.prototype._clearDelay = function() {
|
||||
if (this.timeout != undefined) {
|
||||
clearTimeout(this.timeout);
|
||||
delete this.timeout;
|
||||
this.resultIndex = index;
|
||||
|
||||
// set new node active
|
||||
var node = this.results[this.resultIndex].node;
|
||||
var elem = this.results[this.resultIndex].elem;
|
||||
if (elem == 'field') {
|
||||
node.searchFieldActive = true;
|
||||
}
|
||||
else {
|
||||
node.searchValueActive = true;
|
||||
}
|
||||
this.activeResult = this.results[this.resultIndex];
|
||||
node.updateDom();
|
||||
|
||||
// TODO: not so nice that the focus is only set after the animation is finished
|
||||
node.scrollTo(function () {
|
||||
if (focus) {
|
||||
node.focus(elem);
|
||||
}
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Start a timer to execute a search after a short delay.
|
||||
* Used for reducing the number of searches while typing.
|
||||
* @param {Event} event
|
||||
* @private
|
||||
*/
|
||||
SearchBox.prototype._onDelayedSearch = function (event) {
|
||||
// execute the search after a short delay (reduces the number of
|
||||
// search actions while typing in the search text box)
|
||||
this._clearDelay();
|
||||
var searchBox = this;
|
||||
this.timeout = setTimeout(function (event) {
|
||||
searchBox._onSearch(event);
|
||||
},
|
||||
this.delay);
|
||||
};
|
||||
/**
|
||||
* Cancel any running onDelayedSearch.
|
||||
* @private
|
||||
*/
|
||||
SearchBox.prototype._clearDelay = function() {
|
||||
if (this.timeout != undefined) {
|
||||
clearTimeout(this.timeout);
|
||||
delete this.timeout;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle onSearch event
|
||||
* @param {Event} event
|
||||
* @param {boolean} [forceSearch] If true, search will be executed again even
|
||||
* when the search text is not changed.
|
||||
* Default is false.
|
||||
* @private
|
||||
*/
|
||||
SearchBox.prototype._onSearch = function (event, forceSearch) {
|
||||
this._clearDelay();
|
||||
/**
|
||||
* Start a timer to execute a search after a short delay.
|
||||
* Used for reducing the number of searches while typing.
|
||||
* @param {Event} event
|
||||
* @private
|
||||
*/
|
||||
SearchBox.prototype._onDelayedSearch = function (event) {
|
||||
// execute the search after a short delay (reduces the number of
|
||||
// search actions while typing in the search text box)
|
||||
this._clearDelay();
|
||||
var searchBox = this;
|
||||
this.timeout = setTimeout(function (event) {
|
||||
searchBox._onSearch(event);
|
||||
},
|
||||
this.delay);
|
||||
};
|
||||
|
||||
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);
|
||||
/**
|
||||
* Handle onSearch event
|
||||
* @param {Event} event
|
||||
* @param {boolean} [forceSearch] If true, search will be executed again even
|
||||
* when the search text is not changed.
|
||||
* Default is false.
|
||||
* @private
|
||||
*/
|
||||
SearchBox.prototype._onSearch = function (event, forceSearch) {
|
||||
this._clearDelay();
|
||||
|
||||
// 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 = '';
|
||||
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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle onKeyDown event in the input box
|
||||
* @param {Event} event
|
||||
* @private
|
||||
*/
|
||||
SearchBox.prototype._onKeyDown = function (event) {
|
||||
var keynum = event.which;
|
||||
if (keynum == 27) { // ESC
|
||||
this.dom.search.value = ''; // clear search
|
||||
this._onSearch(event);
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
else {
|
||||
this.dom.results.innerHTML = '';
|
||||
}
|
||||
else if (keynum == 13) { // Enter
|
||||
if (event.ctrlKey) {
|
||||
// force to search again
|
||||
this._onSearch(event, true);
|
||||
}
|
||||
else if (event.shiftKey) {
|
||||
// move to the previous search result
|
||||
this.previous();
|
||||
}
|
||||
else {
|
||||
// move to the next search result
|
||||
this.next();
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle onKeyDown event in the input box
|
||||
* @param {Event} event
|
||||
* @private
|
||||
*/
|
||||
SearchBox.prototype._onKeyDown = function (event) {
|
||||
var keynum = event.which;
|
||||
if (keynum == 27) { // ESC
|
||||
this.dom.search.value = ''; // clear search
|
||||
this._onSearch(event);
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
else if (keynum == 13) { // Enter
|
||||
if (event.ctrlKey) {
|
||||
// force to search again
|
||||
this._onSearch(event, true);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle onKeyUp event in the input box
|
||||
* @param {Event} event
|
||||
* @private
|
||||
*/
|
||||
SearchBox.prototype._onKeyUp = function (event) {
|
||||
var keynum = event.keyCode;
|
||||
if (keynum != 27 && keynum != 13) { // !show and !Enter
|
||||
this._onDelayedSearch(event); // For IE 9
|
||||
else if (event.shiftKey) {
|
||||
// move to the previous search result
|
||||
this.previous();
|
||||
}
|
||||
};
|
||||
|
||||
return SearchBox;
|
||||
});
|
||||
else {
|
||||
// move to the next search result
|
||||
this.next();
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle onKeyUp event in the input box
|
||||
* @param {Event} event
|
||||
* @private
|
||||
*/
|
||||
SearchBox.prototype._onKeyUp = function (event) {
|
||||
var keynum = event.keyCode;
|
||||
if (keynum != 27 && keynum != 13) { // !show and !Enter
|
||||
this._onDelayedSearch(event); // For IE 9
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = SearchBox;
|
||||
|
|
|
@ -1,227 +1,226 @@
|
|||
define(['./ContextMenu', './util'], function (ContextMenu, util) {
|
||||
var util = require('./util');
|
||||
var ContextMenu = require('./ContextMenu');
|
||||
|
||||
/**
|
||||
* A factory function to create an AppendNode, which depends on a Node
|
||||
* @param {Node} Node
|
||||
*/
|
||||
function appendNodeFactory(Node) {
|
||||
/**
|
||||
* A factory function to create an AppendNode, which depends on a Node
|
||||
* @param {Node} Node
|
||||
* @constructor AppendNode
|
||||
* @extends Node
|
||||
* @param {TreeEditor} editor
|
||||
* Create a new AppendNode. This is a special node which is created at the
|
||||
* end of the list with childs for an object or array
|
||||
*/
|
||||
function appendNodeFactory(Node) {
|
||||
/**
|
||||
* @constructor AppendNode
|
||||
* @extends Node
|
||||
* @param {TreeEditor} editor
|
||||
* Create a new AppendNode. This is a special node which is created at the
|
||||
* end of the list with childs for an object or array
|
||||
*/
|
||||
function AppendNode (editor) {
|
||||
/** @type {TreeEditor} */
|
||||
this.editor = editor;
|
||||
this.dom = {};
|
||||
}
|
||||
|
||||
AppendNode.prototype = new Node();
|
||||
|
||||
/**
|
||||
* Return a table row with an append button.
|
||||
* @return {Element} dom TR element
|
||||
*/
|
||||
AppendNode.prototype.getDom = function () {
|
||||
// TODO: implement a new solution for the append node
|
||||
var dom = this.dom;
|
||||
|
||||
if (dom.tr) {
|
||||
return dom.tr;
|
||||
}
|
||||
|
||||
this._updateEditability();
|
||||
|
||||
// a row for the append button
|
||||
var trAppend = document.createElement('tr');
|
||||
trAppend.node = this;
|
||||
dom.tr = trAppend;
|
||||
|
||||
// TODO: consistent naming
|
||||
|
||||
if (this.editable.field) {
|
||||
// a cell for the dragarea column
|
||||
dom.tdDrag = document.createElement('td');
|
||||
|
||||
// create context menu
|
||||
var tdMenu = document.createElement('td');
|
||||
dom.tdMenu = tdMenu;
|
||||
var menu = document.createElement('button');
|
||||
menu.className = 'contextmenu';
|
||||
menu.title = 'Click to open the actions menu (Ctrl+M)';
|
||||
dom.menu = menu;
|
||||
tdMenu.appendChild(dom.menu);
|
||||
}
|
||||
|
||||
// a cell for the contents (showing text 'empty')
|
||||
var tdAppend = document.createElement('td');
|
||||
var domText = document.createElement('div');
|
||||
domText.innerHTML = '(empty)';
|
||||
domText.className = 'readonly';
|
||||
tdAppend.appendChild(domText);
|
||||
dom.td = tdAppend;
|
||||
dom.text = domText;
|
||||
|
||||
this.updateDom();
|
||||
|
||||
return trAppend;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the HTML dom of the Node
|
||||
*/
|
||||
AppendNode.prototype.updateDom = function () {
|
||||
var dom = this.dom;
|
||||
var tdAppend = dom.td;
|
||||
if (tdAppend) {
|
||||
tdAppend.style.paddingLeft = (this.getLevel() * 24 + 26) + 'px';
|
||||
// TODO: not so nice hard coded offset
|
||||
}
|
||||
|
||||
var domText = dom.text;
|
||||
if (domText) {
|
||||
domText.innerHTML = '(empty ' + this.parent.type + ')';
|
||||
}
|
||||
|
||||
// attach or detach the contents of the append node:
|
||||
// hide when the parent has childs, show when the parent has no childs
|
||||
var trAppend = dom.tr;
|
||||
if (!this.isVisible()) {
|
||||
if (dom.tr.firstChild) {
|
||||
if (dom.tdDrag) {
|
||||
trAppend.removeChild(dom.tdDrag);
|
||||
}
|
||||
if (dom.tdMenu) {
|
||||
trAppend.removeChild(dom.tdMenu);
|
||||
}
|
||||
trAppend.removeChild(tdAppend);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!dom.tr.firstChild) {
|
||||
if (dom.tdDrag) {
|
||||
trAppend.appendChild(dom.tdDrag);
|
||||
}
|
||||
if (dom.tdMenu) {
|
||||
trAppend.appendChild(dom.tdMenu);
|
||||
}
|
||||
trAppend.appendChild(tdAppend);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check whether the AppendNode is currently visible.
|
||||
* the AppendNode is visible when its parent has no childs (i.e. is empty).
|
||||
* @return {boolean} isVisible
|
||||
*/
|
||||
AppendNode.prototype.isVisible = function () {
|
||||
return (this.parent.childs.length == 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* Show a contextmenu for this node
|
||||
* @param {HTMLElement} anchor The element to attach the menu to.
|
||||
* @param {function} [onClose] Callback method called when the context menu
|
||||
* is being closed.
|
||||
*/
|
||||
AppendNode.prototype.showContextMenu = function (anchor, onClose) {
|
||||
var node = this;
|
||||
var titles = Node.TYPE_TITLES;
|
||||
var items = [
|
||||
// create append button
|
||||
{
|
||||
'text': 'Append',
|
||||
'title': 'Append a new field with type \'auto\' (Ctrl+Shift+Ins)',
|
||||
'submenuTitle': 'Select the type of the field to be appended',
|
||||
'className': 'insert',
|
||||
'click': function () {
|
||||
node._onAppend('', '', 'auto');
|
||||
},
|
||||
'submenu': [
|
||||
{
|
||||
'text': 'Auto',
|
||||
'className': 'type-auto',
|
||||
'title': titles.auto,
|
||||
'click': function () {
|
||||
node._onAppend('', '', 'auto');
|
||||
}
|
||||
},
|
||||
{
|
||||
'text': 'Array',
|
||||
'className': 'type-array',
|
||||
'title': titles.array,
|
||||
'click': function () {
|
||||
node._onAppend('', []);
|
||||
}
|
||||
},
|
||||
{
|
||||
'text': 'Object',
|
||||
'className': 'type-object',
|
||||
'title': titles.object,
|
||||
'click': function () {
|
||||
node._onAppend('', {});
|
||||
}
|
||||
},
|
||||
{
|
||||
'text': 'String',
|
||||
'className': 'type-string',
|
||||
'title': titles.string,
|
||||
'click': function () {
|
||||
node._onAppend('', '', 'string');
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
var menu = new ContextMenu(items, {close: onClose});
|
||||
menu.show(anchor);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle an event. The event is catched centrally by the editor
|
||||
* @param {Event} event
|
||||
*/
|
||||
AppendNode.prototype.onEvent = function (event) {
|
||||
var type = event.type;
|
||||
var target = event.target || event.srcElement;
|
||||
var dom = this.dom;
|
||||
|
||||
// highlight the append nodes parent
|
||||
var menu = dom.menu;
|
||||
if (target == menu) {
|
||||
if (type == 'mouseover') {
|
||||
this.editor.highlighter.highlight(this.parent);
|
||||
}
|
||||
else if (type == 'mouseout') {
|
||||
this.editor.highlighter.unhighlight();
|
||||
}
|
||||
}
|
||||
|
||||
// context menu events
|
||||
if (type == 'click' && target == dom.menu) {
|
||||
var highlighter = this.editor.highlighter;
|
||||
highlighter.highlight(this.parent);
|
||||
highlighter.lock();
|
||||
util.addClassName(dom.menu, 'selected');
|
||||
this.showContextMenu(dom.menu, function () {
|
||||
util.removeClassName(dom.menu, 'selected');
|
||||
highlighter.unlock();
|
||||
highlighter.unhighlight();
|
||||
});
|
||||
}
|
||||
|
||||
if (type == 'keydown') {
|
||||
this.onKeyDown(event);
|
||||
}
|
||||
};
|
||||
|
||||
return AppendNode;
|
||||
function AppendNode (editor) {
|
||||
/** @type {TreeEditor} */
|
||||
this.editor = editor;
|
||||
this.dom = {};
|
||||
}
|
||||
|
||||
// return the factory function
|
||||
return appendNodeFactory;
|
||||
});
|
||||
AppendNode.prototype = new Node();
|
||||
|
||||
/**
|
||||
* Return a table row with an append button.
|
||||
* @return {Element} dom TR element
|
||||
*/
|
||||
AppendNode.prototype.getDom = function () {
|
||||
// TODO: implement a new solution for the append node
|
||||
var dom = this.dom;
|
||||
|
||||
if (dom.tr) {
|
||||
return dom.tr;
|
||||
}
|
||||
|
||||
this._updateEditability();
|
||||
|
||||
// a row for the append button
|
||||
var trAppend = document.createElement('tr');
|
||||
trAppend.node = this;
|
||||
dom.tr = trAppend;
|
||||
|
||||
// TODO: consistent naming
|
||||
|
||||
if (this.editable.field) {
|
||||
// a cell for the dragarea column
|
||||
dom.tdDrag = document.createElement('td');
|
||||
|
||||
// create context menu
|
||||
var tdMenu = document.createElement('td');
|
||||
dom.tdMenu = tdMenu;
|
||||
var menu = document.createElement('button');
|
||||
menu.className = 'contextmenu';
|
||||
menu.title = 'Click to open the actions menu (Ctrl+M)';
|
||||
dom.menu = menu;
|
||||
tdMenu.appendChild(dom.menu);
|
||||
}
|
||||
|
||||
// a cell for the contents (showing text 'empty')
|
||||
var tdAppend = document.createElement('td');
|
||||
var domText = document.createElement('div');
|
||||
domText.innerHTML = '(empty)';
|
||||
domText.className = 'readonly';
|
||||
tdAppend.appendChild(domText);
|
||||
dom.td = tdAppend;
|
||||
dom.text = domText;
|
||||
|
||||
this.updateDom();
|
||||
|
||||
return trAppend;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the HTML dom of the Node
|
||||
*/
|
||||
AppendNode.prototype.updateDom = function () {
|
||||
var dom = this.dom;
|
||||
var tdAppend = dom.td;
|
||||
if (tdAppend) {
|
||||
tdAppend.style.paddingLeft = (this.getLevel() * 24 + 26) + 'px';
|
||||
// TODO: not so nice hard coded offset
|
||||
}
|
||||
|
||||
var domText = dom.text;
|
||||
if (domText) {
|
||||
domText.innerHTML = '(empty ' + this.parent.type + ')';
|
||||
}
|
||||
|
||||
// attach or detach the contents of the append node:
|
||||
// hide when the parent has childs, show when the parent has no childs
|
||||
var trAppend = dom.tr;
|
||||
if (!this.isVisible()) {
|
||||
if (dom.tr.firstChild) {
|
||||
if (dom.tdDrag) {
|
||||
trAppend.removeChild(dom.tdDrag);
|
||||
}
|
||||
if (dom.tdMenu) {
|
||||
trAppend.removeChild(dom.tdMenu);
|
||||
}
|
||||
trAppend.removeChild(tdAppend);
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (!dom.tr.firstChild) {
|
||||
if (dom.tdDrag) {
|
||||
trAppend.appendChild(dom.tdDrag);
|
||||
}
|
||||
if (dom.tdMenu) {
|
||||
trAppend.appendChild(dom.tdMenu);
|
||||
}
|
||||
trAppend.appendChild(tdAppend);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check whether the AppendNode is currently visible.
|
||||
* the AppendNode is visible when its parent has no childs (i.e. is empty).
|
||||
* @return {boolean} isVisible
|
||||
*/
|
||||
AppendNode.prototype.isVisible = function () {
|
||||
return (this.parent.childs.length == 0);
|
||||
};
|
||||
|
||||
/**
|
||||
* Show a contextmenu for this node
|
||||
* @param {HTMLElement} anchor The element to attach the menu to.
|
||||
* @param {function} [onClose] Callback method called when the context menu
|
||||
* is being closed.
|
||||
*/
|
||||
AppendNode.prototype.showContextMenu = function (anchor, onClose) {
|
||||
var node = this;
|
||||
var titles = Node.TYPE_TITLES;
|
||||
var items = [
|
||||
// create append button
|
||||
{
|
||||
'text': 'Append',
|
||||
'title': 'Append a new field with type \'auto\' (Ctrl+Shift+Ins)',
|
||||
'submenuTitle': 'Select the type of the field to be appended',
|
||||
'className': 'insert',
|
||||
'click': function () {
|
||||
node._onAppend('', '', 'auto');
|
||||
},
|
||||
'submenu': [
|
||||
{
|
||||
'text': 'Auto',
|
||||
'className': 'type-auto',
|
||||
'title': titles.auto,
|
||||
'click': function () {
|
||||
node._onAppend('', '', 'auto');
|
||||
}
|
||||
},
|
||||
{
|
||||
'text': 'Array',
|
||||
'className': 'type-array',
|
||||
'title': titles.array,
|
||||
'click': function () {
|
||||
node._onAppend('', []);
|
||||
}
|
||||
},
|
||||
{
|
||||
'text': 'Object',
|
||||
'className': 'type-object',
|
||||
'title': titles.object,
|
||||
'click': function () {
|
||||
node._onAppend('', {});
|
||||
}
|
||||
},
|
||||
{
|
||||
'text': 'String',
|
||||
'className': 'type-string',
|
||||
'title': titles.string,
|
||||
'click': function () {
|
||||
node._onAppend('', '', 'string');
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
var menu = new ContextMenu(items, {close: onClose});
|
||||
menu.show(anchor);
|
||||
};
|
||||
|
||||
/**
|
||||
* Handle an event. The event is catched centrally by the editor
|
||||
* @param {Event} event
|
||||
*/
|
||||
AppendNode.prototype.onEvent = function (event) {
|
||||
var type = event.type;
|
||||
var target = event.target || event.srcElement;
|
||||
var dom = this.dom;
|
||||
|
||||
// highlight the append nodes parent
|
||||
var menu = dom.menu;
|
||||
if (target == menu) {
|
||||
if (type == 'mouseover') {
|
||||
this.editor.highlighter.highlight(this.parent);
|
||||
}
|
||||
else if (type == 'mouseout') {
|
||||
this.editor.highlighter.unhighlight();
|
||||
}
|
||||
}
|
||||
|
||||
// context menu events
|
||||
if (type == 'click' && target == dom.menu) {
|
||||
var highlighter = this.editor.highlighter;
|
||||
highlighter.highlight(this.parent);
|
||||
highlighter.lock();
|
||||
util.addClassName(dom.menu, 'selected');
|
||||
this.showContextMenu(dom.menu, function () {
|
||||
util.removeClassName(dom.menu, 'selected');
|
||||
highlighter.unlock();
|
||||
highlighter.unhighlight();
|
||||
});
|
||||
}
|
||||
|
||||
if (type == 'keydown') {
|
||||
this.onKeyDown(event);
|
||||
}
|
||||
};
|
||||
|
||||
return AppendNode;
|
||||
}
|
||||
|
||||
module.exports = appendNodeFactory;
|
||||
|
|
|
@ -1,103 +1,100 @@
|
|||
define(['./ContextMenu'], function (ContextMenu) {
|
||||
var ContextMenu = require('./ContextMenu');
|
||||
|
||||
/**
|
||||
* Create a select box to be used in the editor menu's, which allows to switch mode
|
||||
* @param {Object} editor
|
||||
* @param {String[]} modes Available modes: 'code', 'form', 'text', 'tree', 'view'
|
||||
* @param {String} current Available modes: 'code', 'form', 'text', 'tree', 'view'
|
||||
* @returns {HTMLElement} box
|
||||
*/
|
||||
function createModeSwitcher(editor, modes, current) {
|
||||
// TODO: decouple mode switcher from editor
|
||||
|
||||
/**
|
||||
* Create a select box to be used in the editor menu's, which allows to switch mode
|
||||
* @param {Object} editor
|
||||
* @param {String[]} modes Available modes: 'code', 'form', 'text', 'tree', 'view'
|
||||
* @param {String} current Available modes: 'code', 'form', 'text', 'tree', 'view'
|
||||
* @returns {HTMLElement} box
|
||||
* Switch the mode of the editor
|
||||
* @param {String} mode
|
||||
*/
|
||||
function createModeSwitcher(editor, modes, current) {
|
||||
// TODO: decouple mode switcher from editor
|
||||
function switchMode(mode) {
|
||||
// switch mode
|
||||
editor.setMode(mode);
|
||||
|
||||
/**
|
||||
* Switch the mode of the editor
|
||||
* @param {String} mode
|
||||
*/
|
||||
function switchMode(mode) {
|
||||
// switch mode
|
||||
editor.setMode(mode);
|
||||
|
||||
// restore focus on mode box
|
||||
var modeBox = editor.dom && editor.dom.modeBox;
|
||||
if (modeBox) {
|
||||
modeBox.focus();
|
||||
}
|
||||
// restore focus on mode box
|
||||
var modeBox = editor.dom && editor.dom.modeBox;
|
||||
if (modeBox) {
|
||||
modeBox.focus();
|
||||
}
|
||||
|
||||
// available modes
|
||||
var availableModes = {
|
||||
code: {
|
||||
'text': 'Code',
|
||||
'title': 'Switch to code highlighter',
|
||||
'click': function () {
|
||||
switchMode('code')
|
||||
}
|
||||
},
|
||||
form: {
|
||||
'text': 'Form',
|
||||
'title': 'Switch to form editor',
|
||||
'click': function () {
|
||||
switchMode('form');
|
||||
}
|
||||
},
|
||||
text: {
|
||||
'text': 'Text',
|
||||
'title': 'Switch to plain text editor',
|
||||
'click': function () {
|
||||
switchMode('text');
|
||||
}
|
||||
},
|
||||
tree: {
|
||||
'text': 'Tree',
|
||||
'title': 'Switch to tree editor',
|
||||
'click': function () {
|
||||
switchMode('tree');
|
||||
}
|
||||
},
|
||||
view: {
|
||||
'text': 'View',
|
||||
'title': 'Switch to tree view',
|
||||
'click': function () {
|
||||
switchMode('view');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// list the selected modes
|
||||
var items = [];
|
||||
for (var i = 0; i < modes.length; i++) {
|
||||
var mode = modes[i];
|
||||
var item = availableModes[mode];
|
||||
if (!item) {
|
||||
throw new Error('Unknown mode "' + mode + '"');
|
||||
}
|
||||
|
||||
item.className = 'type-modes' + ((current == mode) ? ' selected' : '');
|
||||
items.push(item);
|
||||
}
|
||||
|
||||
// retrieve the title of current mode
|
||||
var currentMode = availableModes[current];
|
||||
if (!currentMode) {
|
||||
throw new Error('Unknown mode "' + current + '"');
|
||||
}
|
||||
var currentTitle = currentMode.text;
|
||||
|
||||
// create the html element
|
||||
var box = document.createElement('button');
|
||||
box.className = 'modes separator';
|
||||
box.innerHTML = currentTitle + ' ▾';
|
||||
box.title = 'Switch editor mode';
|
||||
box.onclick = function () {
|
||||
var menu = new ContextMenu(items);
|
||||
menu.show(box);
|
||||
};
|
||||
|
||||
return box;
|
||||
}
|
||||
|
||||
return {
|
||||
create: createModeSwitcher
|
||||
// available modes
|
||||
var availableModes = {
|
||||
code: {
|
||||
'text': 'Code',
|
||||
'title': 'Switch to code highlighter',
|
||||
'click': function () {
|
||||
switchMode('code')
|
||||
}
|
||||
},
|
||||
form: {
|
||||
'text': 'Form',
|
||||
'title': 'Switch to form editor',
|
||||
'click': function () {
|
||||
switchMode('form');
|
||||
}
|
||||
},
|
||||
text: {
|
||||
'text': 'Text',
|
||||
'title': 'Switch to plain text editor',
|
||||
'click': function () {
|
||||
switchMode('text');
|
||||
}
|
||||
},
|
||||
tree: {
|
||||
'text': 'Tree',
|
||||
'title': 'Switch to tree editor',
|
||||
'click': function () {
|
||||
switchMode('tree');
|
||||
}
|
||||
},
|
||||
view: {
|
||||
'text': 'View',
|
||||
'title': 'Switch to tree view',
|
||||
'click': function () {
|
||||
switchMode('view');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// list the selected modes
|
||||
var items = [];
|
||||
for (var i = 0; i < modes.length; i++) {
|
||||
var mode = modes[i];
|
||||
var item = availableModes[mode];
|
||||
if (!item) {
|
||||
throw new Error('Unknown mode "' + mode + '"');
|
||||
}
|
||||
|
||||
item.className = 'type-modes' + ((current == mode) ? ' selected' : '');
|
||||
items.push(item);
|
||||
}
|
||||
});
|
||||
|
||||
// retrieve the title of current mode
|
||||
var currentMode = availableModes[current];
|
||||
if (!currentMode) {
|
||||
throw new Error('Unknown mode "' + current + '"');
|
||||
}
|
||||
var currentTitle = currentMode.text;
|
||||
|
||||
// create the html element
|
||||
var box = document.createElement('button');
|
||||
box.className = 'modes separator';
|
||||
box.innerHTML = currentTitle + ' ▾';
|
||||
box.title = 'Switch editor mode';
|
||||
box.onclick = function () {
|
||||
var menu = new ContextMenu(items);
|
||||
menu.show(box);
|
||||
};
|
||||
|
||||
return box;
|
||||
}
|
||||
|
||||
exports.create = createModeSwitcher;
|
||||
|
|
|
@ -1,54 +0,0 @@
|
|||
|
||||
// module exports
|
||||
var jsoneditor = {
|
||||
'JSONEditor': JSONEditor,
|
||||
'util': util
|
||||
};
|
||||
|
||||
/**
|
||||
* load jsoneditor.css
|
||||
*/
|
||||
var loadCss = function () {
|
||||
// find the script named 'jsoneditor.js' or 'jsoneditor.min.js' or
|
||||
// 'jsoneditor.min.js', and use its path to find the css file to be
|
||||
// loaded.
|
||||
var scripts = document.getElementsByTagName('script');
|
||||
for (var s = 0; s < scripts.length; s++) {
|
||||
var src = scripts[s].src;
|
||||
if (/(^|\/)jsoneditor([-\.]min)?.js$/.test(src)) {
|
||||
var jsFile = src.split('?')[0];
|
||||
var cssFile = jsFile.substring(0, jsFile.length - 2) + 'css';
|
||||
|
||||
// load css file
|
||||
var link = document.createElement('link');
|
||||
link.type = 'text/css';
|
||||
link.rel = 'stylesheet';
|
||||
link.href = cssFile;
|
||||
document.getElementsByTagName('head')[0].appendChild(link);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* CommonJS module exports
|
||||
*/
|
||||
if (typeof(module) != 'undefined' && typeof(exports) != 'undefined') {
|
||||
loadCss();
|
||||
module.exports = exports = jsoneditor;
|
||||
}
|
||||
|
||||
/**
|
||||
* AMD module exports
|
||||
*/
|
||||
if (typeof(require) != 'undefined' && typeof(define) != 'undefined') {
|
||||
loadCss();
|
||||
define(function () {
|
||||
return jsoneditor;
|
||||
});
|
||||
}
|
||||
else {
|
||||
// attach the module to the window, load as a regular javascript file
|
||||
window['jsoneditor'] = jsoneditor;
|
||||
}
|
|
@ -1,335 +1,335 @@
|
|||
define(['./modeswitcher', './util'], function (modeswitcher, util) {
|
||||
var modeswitcher = require('./modeswitcher');
|
||||
var util = require('./util');
|
||||
|
||||
// create a mixin with the functions for text mode
|
||||
var textmode = {};
|
||||
// create a mixin with the functions for text mode
|
||||
var textmode = {};
|
||||
|
||||
/**
|
||||
* Create a text editor
|
||||
* @param {Element} container
|
||||
* @param {Object} [options] Object with options. available options:
|
||||
* {String} mode Available values:
|
||||
* "text" (default)
|
||||
* or "code".
|
||||
* {Number} indentation Number of indentation
|
||||
* spaces. 2 by default.
|
||||
* {function} change Callback method
|
||||
* triggered on change
|
||||
* @private
|
||||
*/
|
||||
textmode.create = function (container, options) {
|
||||
// read options
|
||||
options = options || {};
|
||||
this.options = options;
|
||||
if (options.indentation) {
|
||||
this.indentation = Number(options.indentation);
|
||||
}
|
||||
else {
|
||||
this.indentation = 2; // number of spaces
|
||||
}
|
||||
this.mode = (options.mode == 'code') ? 'code' : 'text';
|
||||
if (this.mode == 'code') {
|
||||
// verify whether Ace editor is available and supported
|
||||
if (typeof ace === 'undefined') {
|
||||
this.mode = 'text';
|
||||
util.log('WARNING: Cannot load code editor, Ace library not loaded. ' +
|
||||
'Falling back to plain text editor');
|
||||
}
|
||||
/**
|
||||
* Create a text editor
|
||||
* @param {Element} container
|
||||
* @param {Object} [options] Object with options. available options:
|
||||
* {String} mode Available values:
|
||||
* "text" (default)
|
||||
* or "code".
|
||||
* {Number} indentation Number of indentation
|
||||
* spaces. 2 by default.
|
||||
* {function} change Callback method
|
||||
* triggered on change
|
||||
* @private
|
||||
*/
|
||||
textmode.create = function (container, options) {
|
||||
// read options
|
||||
options = options || {};
|
||||
this.options = options;
|
||||
if (options.indentation) {
|
||||
this.indentation = Number(options.indentation);
|
||||
}
|
||||
else {
|
||||
this.indentation = 2; // number of spaces
|
||||
}
|
||||
this.mode = (options.mode == 'code') ? 'code' : 'text';
|
||||
if (this.mode == 'code') {
|
||||
// verify whether Ace editor is available and supported
|
||||
if (typeof ace === 'undefined') {
|
||||
this.mode = 'text';
|
||||
util.log('WARNING: Cannot load code editor, Ace library not loaded. ' +
|
||||
'Falling back to plain text editor');
|
||||
}
|
||||
}
|
||||
|
||||
var me = this;
|
||||
this.container = container;
|
||||
this.dom = {};
|
||||
this.editor = undefined; // ace code editor
|
||||
this.textarea = undefined; // plain text editor (fallback when Ace is not available)
|
||||
var me = this;
|
||||
this.container = container;
|
||||
this.dom = {};
|
||||
this.editor = undefined; // ace code editor
|
||||
this.textarea = undefined; // plain text editor (fallback when Ace is not available)
|
||||
|
||||
this.width = container.clientWidth;
|
||||
this.height = container.clientHeight;
|
||||
this.width = container.clientWidth;
|
||||
this.height = container.clientHeight;
|
||||
|
||||
this.frame = document.createElement('div');
|
||||
this.frame.className = 'jsoneditor';
|
||||
this.frame.onclick = function (event) {
|
||||
// prevent default submit action when the editor is located inside a form
|
||||
event.preventDefault();
|
||||
};
|
||||
this.frame.onkeydown = function (event) {
|
||||
me._onKeyDown(event);
|
||||
};
|
||||
|
||||
// create menu
|
||||
this.menu = document.createElement('div');
|
||||
this.menu.className = 'menu';
|
||||
this.frame.appendChild(this.menu);
|
||||
|
||||
// create format button
|
||||
var buttonFormat = document.createElement('button');
|
||||
buttonFormat.className = 'format';
|
||||
buttonFormat.title = 'Format JSON data, with proper indentation and line feeds (Ctrl+\\)';
|
||||
this.menu.appendChild(buttonFormat);
|
||||
buttonFormat.onclick = function () {
|
||||
try {
|
||||
me.format();
|
||||
}
|
||||
catch (err) {
|
||||
me._onError(err);
|
||||
}
|
||||
};
|
||||
|
||||
// create compact button
|
||||
var buttonCompact = document.createElement('button');
|
||||
buttonCompact.className = 'compact';
|
||||
buttonCompact.title = 'Compact JSON data, remove all whitespaces (Ctrl+Shift+\\)';
|
||||
this.menu.appendChild(buttonCompact);
|
||||
buttonCompact.onclick = function () {
|
||||
try {
|
||||
me.compact();
|
||||
}
|
||||
catch (err) {
|
||||
me._onError(err);
|
||||
}
|
||||
};
|
||||
|
||||
// create mode box
|
||||
if (this.options && this.options.modes && this.options.modes.length) {
|
||||
var modeBox = modeswitcher.create(this, this.options.modes, this.options.mode);
|
||||
this.menu.appendChild(modeBox);
|
||||
this.dom.modeBox = modeBox;
|
||||
}
|
||||
|
||||
this.content = document.createElement('div');
|
||||
this.content.className = 'outer';
|
||||
this.frame.appendChild(this.content);
|
||||
|
||||
this.container.appendChild(this.frame);
|
||||
|
||||
if (this.mode == 'code') {
|
||||
this.editorDom = document.createElement('div');
|
||||
this.editorDom.style.height = '100%'; // TODO: move to css
|
||||
this.editorDom.style.width = '100%'; // TODO: move to css
|
||||
this.content.appendChild(this.editorDom);
|
||||
|
||||
var editor = ace.edit(this.editorDom);
|
||||
editor.setTheme('ace/theme/jsoneditor');
|
||||
editor.setShowPrintMargin(false);
|
||||
editor.setFontSize(13);
|
||||
editor.getSession().setMode('ace/mode/json');
|
||||
editor.getSession().setTabSize(this.indentation);
|
||||
editor.getSession().setUseSoftTabs(true);
|
||||
editor.getSession().setUseWrapMode(true);
|
||||
this.editor = editor;
|
||||
|
||||
var poweredBy = document.createElement('a');
|
||||
poweredBy.appendChild(document.createTextNode('powered by ace'));
|
||||
poweredBy.href = 'http://ace.ajax.org';
|
||||
poweredBy.target = '_blank';
|
||||
poweredBy.className = 'poweredBy';
|
||||
poweredBy.onclick = function () {
|
||||
// TODO: this anchor falls below the margin of the content,
|
||||
// therefore the normal a.href does not work. We use a click event
|
||||
// for now, but this should be fixed.
|
||||
window.open(poweredBy.href, poweredBy.target);
|
||||
};
|
||||
this.menu.appendChild(poweredBy);
|
||||
|
||||
if (options.change) {
|
||||
// register onchange event
|
||||
editor.on('change', function () {
|
||||
options.change();
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
// load a plain text textarea
|
||||
var textarea = document.createElement('textarea');
|
||||
textarea.className = 'text';
|
||||
textarea.spellcheck = false;
|
||||
this.content.appendChild(textarea);
|
||||
this.textarea = textarea;
|
||||
|
||||
if (options.change) {
|
||||
// register onchange event
|
||||
if (this.textarea.oninput === null) {
|
||||
this.textarea.oninput = function () {
|
||||
options.change();
|
||||
}
|
||||
}
|
||||
else {
|
||||
// oninput is undefined. For IE8-
|
||||
this.textarea.onchange = function () {
|
||||
options.change();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
this.frame = document.createElement('div');
|
||||
this.frame.className = 'jsoneditor';
|
||||
this.frame.onclick = function (event) {
|
||||
// prevent default submit action when the editor is located inside a form
|
||||
event.preventDefault();
|
||||
};
|
||||
this.frame.onkeydown = function (event) {
|
||||
me._onKeyDown(event);
|
||||
};
|
||||
|
||||
/**
|
||||
* Event handler for keydown. Handles shortcut keys
|
||||
* @param {Event} event
|
||||
* @private
|
||||
*/
|
||||
textmode._onKeyDown = function (event) {
|
||||
var keynum = event.which || event.keyCode;
|
||||
var handled = false;
|
||||
|
||||
if (keynum == 220 && event.ctrlKey) {
|
||||
if (event.shiftKey) { // Ctrl+Shift+\
|
||||
this.compact();
|
||||
}
|
||||
else { // Ctrl+\
|
||||
this.format();
|
||||
}
|
||||
handled = true;
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Detach the editor from the DOM
|
||||
* @private
|
||||
*/
|
||||
textmode._delete = function () {
|
||||
if (this.frame && this.container && this.frame.parentNode == this.container) {
|
||||
this.container.removeChild(this.frame);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Throw an error. If an error callback is configured in options.error, this
|
||||
* callback will be invoked. Else, a regular error is thrown.
|
||||
* @param {Error} err
|
||||
* @private
|
||||
*/
|
||||
textmode._onError = function(err) {
|
||||
// TODO: onError is deprecated since version 2.2.0. cleanup some day
|
||||
if (typeof this.onError === 'function') {
|
||||
util.log('WARNING: JSONEditor.onError is deprecated. ' +
|
||||
'Use options.error instead.');
|
||||
this.onError(err);
|
||||
}
|
||||
|
||||
if (this.options && typeof this.options.error === 'function') {
|
||||
this.options.error(err);
|
||||
}
|
||||
else {
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Compact the code in the formatter
|
||||
*/
|
||||
textmode.compact = function () {
|
||||
var json = this.get();
|
||||
var text = JSON.stringify(json);
|
||||
this.setText(text);
|
||||
};
|
||||
|
||||
/**
|
||||
* Format the code in the formatter
|
||||
*/
|
||||
textmode.format = function () {
|
||||
var json = this.get();
|
||||
var text = JSON.stringify(json, null, this.indentation);
|
||||
this.setText(text);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set focus to the formatter
|
||||
*/
|
||||
textmode.focus = function () {
|
||||
if (this.textarea) {
|
||||
this.textarea.focus();
|
||||
}
|
||||
if (this.editor) {
|
||||
this.editor.focus();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Resize the formatter
|
||||
*/
|
||||
textmode.resize = function () {
|
||||
if (this.editor) {
|
||||
var force = false;
|
||||
this.editor.resize(force);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Set json data in the formatter
|
||||
* @param {Object} json
|
||||
*/
|
||||
textmode.set = function(json) {
|
||||
this.setText(JSON.stringify(json, null, this.indentation));
|
||||
};
|
||||
|
||||
/**
|
||||
* Get json data from the formatter
|
||||
* @return {Object} json
|
||||
*/
|
||||
textmode.get = function() {
|
||||
var text = this.getText();
|
||||
var json;
|
||||
// create menu
|
||||
this.menu = document.createElement('div');
|
||||
this.menu.className = 'menu';
|
||||
this.frame.appendChild(this.menu);
|
||||
|
||||
// create format button
|
||||
var buttonFormat = document.createElement('button');
|
||||
buttonFormat.className = 'format';
|
||||
buttonFormat.title = 'Format JSON data, with proper indentation and line feeds (Ctrl+\\)';
|
||||
this.menu.appendChild(buttonFormat);
|
||||
buttonFormat.onclick = function () {
|
||||
try {
|
||||
json = util.parse(text); // this can throw an error
|
||||
me.format();
|
||||
}
|
||||
catch (err) {
|
||||
// try to sanitize json, replace JavaScript notation with JSON notation
|
||||
text = util.sanitize(text);
|
||||
this.setText(text);
|
||||
|
||||
// try to parse again
|
||||
json = util.parse(text); // this can throw an error
|
||||
}
|
||||
|
||||
return json;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the text contents of the editor
|
||||
* @return {String} jsonText
|
||||
*/
|
||||
textmode.getText = function() {
|
||||
if (this.textarea) {
|
||||
return this.textarea.value;
|
||||
}
|
||||
if (this.editor) {
|
||||
return this.editor.getValue();
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the text contents of the editor
|
||||
* @param {String} jsonText
|
||||
*/
|
||||
textmode.setText = function(jsonText) {
|
||||
if (this.textarea) {
|
||||
this.textarea.value = jsonText;
|
||||
}
|
||||
if (this.editor) {
|
||||
this.editor.setValue(jsonText, -1);
|
||||
me._onError(err);
|
||||
}
|
||||
};
|
||||
|
||||
// define modes
|
||||
return [
|
||||
{
|
||||
mode: 'text',
|
||||
mixin: textmode,
|
||||
data: 'text',
|
||||
load: textmode.format
|
||||
},
|
||||
{
|
||||
mode: 'code',
|
||||
mixin: textmode,
|
||||
data: 'text',
|
||||
load: textmode.format
|
||||
// create compact button
|
||||
var buttonCompact = document.createElement('button');
|
||||
buttonCompact.className = 'compact';
|
||||
buttonCompact.title = 'Compact JSON data, remove all whitespaces (Ctrl+Shift+\\)';
|
||||
this.menu.appendChild(buttonCompact);
|
||||
buttonCompact.onclick = function () {
|
||||
try {
|
||||
me.compact();
|
||||
}
|
||||
];
|
||||
});
|
||||
catch (err) {
|
||||
me._onError(err);
|
||||
}
|
||||
};
|
||||
|
||||
// create mode box
|
||||
if (this.options && this.options.modes && this.options.modes.length) {
|
||||
var modeBox = modeswitcher.create(this, this.options.modes, this.options.mode);
|
||||
this.menu.appendChild(modeBox);
|
||||
this.dom.modeBox = modeBox;
|
||||
}
|
||||
|
||||
this.content = document.createElement('div');
|
||||
this.content.className = 'outer';
|
||||
this.frame.appendChild(this.content);
|
||||
|
||||
this.container.appendChild(this.frame);
|
||||
|
||||
if (this.mode == 'code') {
|
||||
this.editorDom = document.createElement('div');
|
||||
this.editorDom.style.height = '100%'; // TODO: move to css
|
||||
this.editorDom.style.width = '100%'; // TODO: move to css
|
||||
this.content.appendChild(this.editorDom);
|
||||
|
||||
var editor = ace.edit(this.editorDom);
|
||||
editor.setTheme('ace/theme/jsoneditor');
|
||||
editor.setShowPrintMargin(false);
|
||||
editor.setFontSize(13);
|
||||
editor.getSession().setMode('ace/mode/json');
|
||||
editor.getSession().setTabSize(this.indentation);
|
||||
editor.getSession().setUseSoftTabs(true);
|
||||
editor.getSession().setUseWrapMode(true);
|
||||
this.editor = editor;
|
||||
|
||||
var poweredBy = document.createElement('a');
|
||||
poweredBy.appendChild(document.createTextNode('powered by ace'));
|
||||
poweredBy.href = 'http://ace.ajax.org';
|
||||
poweredBy.target = '_blank';
|
||||
poweredBy.className = 'poweredBy';
|
||||
poweredBy.onclick = function () {
|
||||
// TODO: this anchor falls below the margin of the content,
|
||||
// therefore the normal a.href does not work. We use a click event
|
||||
// for now, but this should be fixed.
|
||||
window.open(poweredBy.href, poweredBy.target);
|
||||
};
|
||||
this.menu.appendChild(poweredBy);
|
||||
|
||||
if (options.change) {
|
||||
// register onchange event
|
||||
editor.on('change', function () {
|
||||
options.change();
|
||||
});
|
||||
}
|
||||
}
|
||||
else {
|
||||
// load a plain text textarea
|
||||
var textarea = document.createElement('textarea');
|
||||
textarea.className = 'text';
|
||||
textarea.spellcheck = false;
|
||||
this.content.appendChild(textarea);
|
||||
this.textarea = textarea;
|
||||
|
||||
if (options.change) {
|
||||
// register onchange event
|
||||
if (this.textarea.oninput === null) {
|
||||
this.textarea.oninput = function () {
|
||||
options.change();
|
||||
}
|
||||
}
|
||||
else {
|
||||
// oninput is undefined. For IE8-
|
||||
this.textarea.onchange = function () {
|
||||
options.change();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Event handler for keydown. Handles shortcut keys
|
||||
* @param {Event} event
|
||||
* @private
|
||||
*/
|
||||
textmode._onKeyDown = function (event) {
|
||||
var keynum = event.which || event.keyCode;
|
||||
var handled = false;
|
||||
|
||||
if (keynum == 220 && event.ctrlKey) {
|
||||
if (event.shiftKey) { // Ctrl+Shift+\
|
||||
this.compact();
|
||||
}
|
||||
else { // Ctrl+\
|
||||
this.format();
|
||||
}
|
||||
handled = true;
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Detach the editor from the DOM
|
||||
* @private
|
||||
*/
|
||||
textmode._delete = function () {
|
||||
if (this.frame && this.container && this.frame.parentNode == this.container) {
|
||||
this.container.removeChild(this.frame);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Throw an error. If an error callback is configured in options.error, this
|
||||
* callback will be invoked. Else, a regular error is thrown.
|
||||
* @param {Error} err
|
||||
* @private
|
||||
*/
|
||||
textmode._onError = function(err) {
|
||||
// TODO: onError is deprecated since version 2.2.0. cleanup some day
|
||||
if (typeof this.onError === 'function') {
|
||||
util.log('WARNING: JSONEditor.onError is deprecated. ' +
|
||||
'Use options.error instead.');
|
||||
this.onError(err);
|
||||
}
|
||||
|
||||
if (this.options && typeof this.options.error === 'function') {
|
||||
this.options.error(err);
|
||||
}
|
||||
else {
|
||||
throw err;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Compact the code in the formatter
|
||||
*/
|
||||
textmode.compact = function () {
|
||||
var json = this.get();
|
||||
var text = JSON.stringify(json);
|
||||
this.setText(text);
|
||||
};
|
||||
|
||||
/**
|
||||
* Format the code in the formatter
|
||||
*/
|
||||
textmode.format = function () {
|
||||
var json = this.get();
|
||||
var text = JSON.stringify(json, null, this.indentation);
|
||||
this.setText(text);
|
||||
};
|
||||
|
||||
/**
|
||||
* Set focus to the formatter
|
||||
*/
|
||||
textmode.focus = function () {
|
||||
if (this.textarea) {
|
||||
this.textarea.focus();
|
||||
}
|
||||
if (this.editor) {
|
||||
this.editor.focus();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Resize the formatter
|
||||
*/
|
||||
textmode.resize = function () {
|
||||
if (this.editor) {
|
||||
var force = false;
|
||||
this.editor.resize(force);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Set json data in the formatter
|
||||
* @param {Object} json
|
||||
*/
|
||||
textmode.set = function(json) {
|
||||
this.setText(JSON.stringify(json, null, this.indentation));
|
||||
};
|
||||
|
||||
/**
|
||||
* Get json data from the formatter
|
||||
* @return {Object} json
|
||||
*/
|
||||
textmode.get = function() {
|
||||
var text = this.getText();
|
||||
var json;
|
||||
|
||||
try {
|
||||
json = util.parse(text); // this can throw an error
|
||||
}
|
||||
catch (err) {
|
||||
// try to sanitize json, replace JavaScript notation with JSON notation
|
||||
text = util.sanitize(text);
|
||||
this.setText(text);
|
||||
|
||||
// try to parse again
|
||||
json = util.parse(text); // this can throw an error
|
||||
}
|
||||
|
||||
return json;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get the text contents of the editor
|
||||
* @return {String} jsonText
|
||||
*/
|
||||
textmode.getText = function() {
|
||||
if (this.textarea) {
|
||||
return this.textarea.value;
|
||||
}
|
||||
if (this.editor) {
|
||||
return this.editor.getValue();
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
/**
|
||||
* Set the text contents of the editor
|
||||
* @param {String} jsonText
|
||||
*/
|
||||
textmode.setText = function(jsonText) {
|
||||
if (this.textarea) {
|
||||
this.textarea.value = jsonText;
|
||||
}
|
||||
if (this.editor) {
|
||||
this.editor.setValue(jsonText, -1);
|
||||
}
|
||||
};
|
||||
|
||||
// define modes
|
||||
module.exports = [
|
||||
{
|
||||
mode: 'text',
|
||||
mixin: textmode,
|
||||
data: 'text',
|
||||
load: textmode.format
|
||||
},
|
||||
{
|
||||
mode: 'code',
|
||||
mixin: textmode,
|
||||
data: 'text',
|
||||
load: textmode.format
|
||||
}
|
||||
];
|
||||
|
|
1351
src/js/treemode.js
1351
src/js/treemode.js
File diff suppressed because it is too large
Load Diff
986
src/js/util.js
986
src/js/util.js
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue