Reworked the source code to commonjs modules

This commit is contained in:
jos 2015-02-27 21:17:19 +01:00
parent abc3ac3625
commit c18145f503
15 changed files with 11502 additions and 11587 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

7
jsoneditor.min.js vendored

File diff suppressed because one or more lines are too long

View File

@ -1,444 +1,443 @@
define(['./util'], function (util) { var util = require('./util');
/** /**
* A context menu * A context menu
* @param {Object[]} items Array containing the menu structure * @param {Object[]} items Array containing the menu structure
* TODO: describe structure * TODO: describe structure
* @param {Object} [options] Object with options. Available options: * @param {Object} [options] Object with options. Available options:
* {function} close Callback called when the * {function} close Callback called when the
* context menu is being closed. * context menu is being closed.
* @constructor * @constructor
*/ */
function ContextMenu (items, options) { function ContextMenu (items, options) {
this.dom = {}; this.dom = {};
var me = this; var me = this;
var dom = this.dom; var dom = this.dom;
this.anchor = undefined; this.anchor = undefined;
this.items = items; this.items = items;
this.eventListeners = {}; this.eventListeners = {};
this.selection = undefined; // holds the selection before the menu was opened this.selection = undefined; // holds the selection before the menu was opened
this.visibleSubmenu = undefined; this.visibleSubmenu = undefined;
this.onClose = options ? options.close : undefined; this.onClose = options ? options.close : undefined;
// create a container element // create a container element
var menu = document.createElement('div'); var menu = document.createElement('div');
menu.className = 'jsoneditor-contextmenu'; menu.className = 'jsoneditor-contextmenu';
dom.menu = menu; dom.menu = menu;
// create a list to hold the menu items // create a list to hold the menu items
var list = document.createElement('ul'); var list = document.createElement('ul');
list.className = 'menu'; list.className = 'menu';
menu.appendChild(list); menu.appendChild(list);
dom.list = list; dom.list = list;
dom.items = []; // list with all buttons dom.items = []; // list with all buttons
// create a (non-visible) button to set the focus to the menu // create a (non-visible) button to set the focus to the menu
var focusButton = document.createElement('button'); var focusButton = document.createElement('button');
dom.focusButton = focusButton; dom.focusButton = focusButton;
var li = document.createElement('li'); var li = document.createElement('li');
li.style.overflow = 'hidden'; li.style.overflow = 'hidden';
li.style.height = '0'; li.style.height = '0';
li.appendChild(focusButton); li.appendChild(focusButton);
list.appendChild(li); list.appendChild(li);
function createMenuItems (list, domItems, items) { function createMenuItems (list, domItems, items) {
items.forEach(function (item) { items.forEach(function (item) {
if (item.type == 'separator') { if (item.type == 'separator') {
// create a separator // create a separator
var separator = document.createElement('div'); var separator = document.createElement('div');
separator.className = 'separator'; separator.className = 'separator';
li = document.createElement('li'); li = document.createElement('li');
li.appendChild(separator); li.appendChild(separator);
list.appendChild(li); 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 { if (item.click) {
var domItem = {}; button.onclick = function () {
me.hide();
item.click();
};
}
li.appendChild(button);
// create a menu item // create the contents of the button
var li = document.createElement('li'); if (item.submenu) {
list.appendChild(li); // 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 buttonSubmenu;
var button = document.createElement('button');
button.className = item.className;
domItem.button = button;
if (item.title) {
button.title = item.title;
}
if (item.click) { if (item.click) {
button.onclick = function () { // submenu and a button with a click handler
me.hide(); button.className += ' default';
item.click();
};
}
li.appendChild(button);
// create the contents of the button var buttonExpand = document.createElement('button');
if (item.submenu) { domItem.buttonExpand = buttonExpand;
// add the icon to the button buttonExpand.className = 'expand';
var divIcon = document.createElement('div'); buttonExpand.innerHTML = '<div class="expand"></div>';
divIcon.className = 'icon'; li.appendChild(buttonExpand);
button.appendChild(divIcon); if (item.submenuTitle) {
button.appendChild(document.createTextNode(item.text)); buttonExpand.title = item.submenuTitle;
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;
} }
// attach a handler to expand/collapse the submenu buttonSubmenu = buttonExpand;
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 { else {
// no submenu, just a button with clickhandler // submenu and a button without a click handler
button.innerHTML = '<div class="icon"></div>' + item.text; 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? domItems.push(domItem);
}
// calculate the max height of the menu with one submenu expanded
this.maxHeight = 0; // height in pixels
items.forEach(function (item) {
var height = (items.length + (item.submenu ? item.submenu.length : 0)) * 24;
me.maxHeight = Math.max(me.maxHeight, height);
}); });
} }
createMenuItems(list, this.dom.items, items);
/** // TODO: when the editor is small, show the submenu on the right instead of inline?
* Get the currently visible buttons
* @return {Array.<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; // 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 // 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 * Attach the menu to an anchor
* @param {HTMLElement} anchor * @param {HTMLElement} anchor
*/ */
ContextMenu.prototype.show = function (anchor) { ContextMenu.prototype.show = function (anchor) {
this.hide(); this.hide();
// calculate whether the menu fits below the anchor // calculate whether the menu fits below the anchor
var windowHeight = window.innerHeight, var windowHeight = window.innerHeight,
windowScroll = (window.pageYOffset || document.scrollTop || 0), windowScroll = (window.pageYOffset || document.scrollTop || 0),
windowBottom = windowHeight + windowScroll, windowBottom = windowHeight + windowScroll,
anchorHeight = anchor.offsetHeight, anchorHeight = anchor.offsetHeight,
menuHeight = this.maxHeight; menuHeight = this.maxHeight;
// position the menu // position the menu
var left = util.getAbsoluteLeft(anchor); var left = util.getAbsoluteLeft(anchor);
var top = util.getAbsoluteTop(anchor); var top = util.getAbsoluteTop(anchor);
if (top + anchorHeight + menuHeight < windowBottom) { if (top + anchorHeight + menuHeight < windowBottom) {
// display the menu below the anchor // display the menu below the anchor
this.dom.menu.style.left = left + 'px'; this.dom.menu.style.left = left + 'px';
this.dom.menu.style.top = (top + anchorHeight) + 'px'; this.dom.menu.style.top = (top + anchorHeight) + 'px';
this.dom.menu.style.bottom = ''; this.dom.menu.style.bottom = '';
} }
else { else {
// display the menu above the anchor // display the menu above the anchor
this.dom.menu.style.left = left + 'px'; this.dom.menu.style.left = left + 'px';
this.dom.menu.style.top = ''; this.dom.menu.style.top = '';
this.dom.menu.style.bottom = (windowHeight - top) + 'px'; this.dom.menu.style.bottom = (windowHeight - top) + 'px';
} }
// attach the menu to the document // attach the menu to the document
document.body.appendChild(this.dom.menu); document.body.appendChild(this.dom.menu);
// create and attach event listeners // create and attach event listeners
var me = this; var me = this;
var list = this.dom.list; var list = this.dom.list;
this.eventListeners.mousedown = util.addEventListener( this.eventListeners.mousedown = util.addEventListener(
document, 'mousedown', function (event) { document, 'mousedown', function (event) {
// hide menu on click outside of the menu // hide menu on click outside of the menu
var target = event.target; var target = event.target;
if ((target != list) && !me._isChildOf(target, list)) { if ((target != list) && !me._isChildOf(target, list)) {
me.hide(); me.hide();
event.stopPropagation();
event.preventDefault();
}
});
this.eventListeners.mousewheel = util.addEventListener(
document, 'mousewheel', function (event) {
// block scrolling when context menu is visible
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
}); }
this.eventListeners.keydown = util.addEventListener( });
document, 'keydown', function (event) { this.eventListeners.mousewheel = util.addEventListener(
me._onKeyDown(event); 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 // move focus to the first button in the context menu
this.selection = util.getSelection(); this.selection = util.getSelection();
this.anchor = anchor; 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 () { 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); }, 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;
};
/** this.hide();
* Hide the context menu if visible
*/ handled = true;
ContextMenu.prototype.hide = function () { }
// remove the menu from the DOM else if (keynum == 9) { // Tab
if (this.dom.menu.parentNode) { if (!event.shiftKey) { // Tab
this.dom.menu.parentNode.removeChild(this.dom.menu); buttons = this._getVisibleButtons();
if (this.onClose) { targetIndex = buttons.indexOf(target);
this.onClose(); if (targetIndex == buttons.length - 1) {
// move to first button
buttons[0].focus();
handled = true;
} }
} }
else { // Shift+Tab
// remove all event listeners buttons = this._getVisibleButtons();
// all event listeners are supposed to be attached to document. targetIndex = buttons.indexOf(target);
for (var name in this.eventListeners) { if (targetIndex == 0) {
if (this.eventListeners.hasOwnProperty(name)) { // move to last button
var fn = this.eventListeners[name]; buttons[buttons.length - 1].focus();
if (fn) { handled = true;
util.removeEventListener(document, name, fn);
}
delete this.eventListeners[name];
} }
} }
}
if (ContextMenu.visibleMenu == this) { else if (keynum == 37) { // Arrow Left
ContextMenu.visibleMenu = undefined; if (target.className == 'expand') {
}
};
/**
* Expand a submenu
* Any currently expanded submenu will be hided.
* @param {Object} domItem
* @private
*/
ContextMenu.prototype._onExpandItem = function (domItem) {
var me = this;
var alreadyVisible = (domItem == this.expandedItem);
// hide the currently visible submenu
var expandedItem = this.expandedItem;
if (expandedItem) {
//var ul = expandedItem.ul;
expandedItem.ul.style.height = '0';
expandedItem.ul.style.padding = '';
setTimeout(function () {
if (me.expandedItem != expandedItem) {
expandedItem.ul.style.display = '';
util.removeClassName(expandedItem.ul.parentNode, 'selected');
}
}, 300); // timeout duration must match the css transition duration
this.expandedItem = undefined;
}
if (!alreadyVisible) {
var ul = domItem.ul;
ul.style.display = 'block';
var height = ul.clientHeight; // force a reflow in Firefox
setTimeout(function () {
if (me.expandedItem == domItem) {
ul.style.height = (ul.childNodes.length * 24) + 'px';
ul.style.padding = '5px 10px';
}
}, 0);
util.addClassName(ul.parentNode, 'selected');
this.expandedItem = domItem;
}
};
/**
* Handle onkeydown event
* @param {Event} event
* @private
*/
ContextMenu.prototype._onKeyDown = function (event) {
var target = event.target;
var keynum = event.which;
var handled = false;
var buttons, targetIndex, prevButton, nextButton;
if (keynum == 27) { // ESC
// hide the menu on ESC key
// restore previous selection and focus
if (this.selection) {
util.setSelection(this.selection);
}
if (this.anchor) {
this.anchor.focus();
}
this.hide();
handled = true;
}
else if (keynum == 9) { // Tab
if (!event.shiftKey) { // Tab
buttons = this._getVisibleButtons();
targetIndex = buttons.indexOf(target);
if (targetIndex == buttons.length - 1) {
// move to first button
buttons[0].focus();
handled = true;
}
}
else { // Shift+Tab
buttons = this._getVisibleButtons();
targetIndex = buttons.indexOf(target);
if (targetIndex == 0) {
// move to last button
buttons[buttons.length - 1].focus();
handled = true;
}
}
}
else if (keynum == 37) { // Arrow Left
if (target.className == 'expand') {
buttons = this._getVisibleButtons();
targetIndex = buttons.indexOf(target);
prevButton = buttons[targetIndex - 1];
if (prevButton) {
prevButton.focus();
}
}
handled = true;
}
else if (keynum == 38) { // Arrow Up
buttons = this._getVisibleButtons(); buttons = this._getVisibleButtons();
targetIndex = buttons.indexOf(target); targetIndex = buttons.indexOf(target);
prevButton = buttons[targetIndex - 1]; 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) { if (prevButton) {
prevButton.focus(); prevButton.focus();
} }
}
handled = true;
}
else if (keynum == 38) { // Arrow Up
buttons = this._getVisibleButtons();
targetIndex = buttons.indexOf(target);
prevButton = buttons[targetIndex - 1];
if (prevButton && prevButton.className == 'expand') {
// skip expand button
prevButton = buttons[targetIndex - 2];
}
if (!prevButton) {
// move to last button
prevButton = buttons[buttons.length - 1];
}
if (prevButton) {
prevButton.focus();
}
handled = true;
}
else if (keynum == 39) { // Arrow Right
buttons = this._getVisibleButtons();
targetIndex = buttons.indexOf(target);
nextButton = buttons[targetIndex + 1];
if (nextButton && nextButton.className == 'expand') {
nextButton.focus();
}
handled = true;
}
else if (keynum == 40) { // Arrow Down
buttons = this._getVisibleButtons();
targetIndex = buttons.indexOf(target);
nextButton = buttons[targetIndex + 1];
if (nextButton && nextButton.className == 'expand') {
// skip expand button
nextButton = buttons[targetIndex + 2];
}
if (!nextButton) {
// move to first button
nextButton = buttons[0];
}
if (nextButton) {
nextButton.focus();
handled = true; handled = true;
} }
else if (keynum == 39) { // Arrow Right handled = true;
buttons = this._getVisibleButtons(); }
targetIndex = buttons.indexOf(target); // TODO: arrow left and right
nextButton = buttons[targetIndex + 1];
if (nextButton && nextButton.className == 'expand') {
nextButton.focus();
}
handled = true;
}
else if (keynum == 40) { // Arrow Down
buttons = this._getVisibleButtons();
targetIndex = buttons.indexOf(target);
nextButton = buttons[targetIndex + 1];
if (nextButton && nextButton.className == 'expand') {
// skip expand button
nextButton = buttons[targetIndex + 2];
}
if (!nextButton) {
// move to first button
nextButton = buttons[0];
}
if (nextButton) {
nextButton.focus();
handled = true;
}
handled = true;
}
// TODO: arrow left and right
if (handled) { if (handled) {
event.stopPropagation(); event.stopPropagation();
event.preventDefault(); event.preventDefault();
}
};
/**
* Test if an element is a child of a parent element.
* @param {Element} child
* @param {Element} parent
* @return {boolean} isChild
*/
ContextMenu.prototype._isChildOf = function (child, parent) {
var e = child.parentNode;
while (e) {
if (e == parent) {
return true;
} }
}; e = e.parentNode;
}
/** return false;
* 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; module.exports = ContextMenu;
};
return ContextMenu;
});

View File

@ -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 * Hightlight given node and its childs
* animate the visibility of a context menu. * @param {Node} node
* @constructor Highlighter */
*/ Highlighter.prototype.highlight = function (node) {
function Highlighter () { if (this.locked) {
this.locked = false; return;
} }
/** if (this.node != node) {
* Hightlight given node and its childs // unhighlight current node
* @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) { if (this.node) {
this._cancelUnhighlight(); this.node.setHighlight(false);
// 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);
} }
};
/** // highlight new node
* Cancel an unhighlight action (if before the timeout of the unhighlight action) this.node = node;
* @private this.node.setHighlight(true);
*/ }
Highlighter.prototype._cancelUnhighlight = function () {
if (this.unhighlightTimer) {
clearTimeout(this.unhighlightTimer);
this.unhighlightTimer = undefined;
}
};
/** // cancel any current timeout
* Lock highlighting or unhighlighting nodes. this._cancelUnhighlight();
* methods highlight and unhighlight do not work while locked. };
*/
Highlighter.prototype.lock = function () {
this.locked = true;
};
/** /**
* Unlock highlighting or unhighlighting nodes * Unhighlight currently highlighted node.
*/ * Will be done after a delay
Highlighter.prototype.unlock = function () { */
this.locked = false; 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;

View File

@ -1,223 +1,222 @@
define(['./util'], function (util) { var util = require('./util');
/** /**
* @constructor History * @constructor History
* Store action history, enables undo and redo * Store action history, enables undo and redo
* @param {JSONEditor} editor * @param {JSONEditor} editor
*/ */
function History (editor) { function History (editor) {
this.editor = editor; this.editor = editor;
this.clear(); this.clear();
// map with all supported actions // map with all supported actions
this.actions = { this.actions = {
'editField': { 'editField': {
'undo': function (params) { 'undo': function (params) {
params.node.updateField(params.oldValue); params.node.updateField(params.oldValue);
},
'redo': function (params) {
params.node.updateField(params.newValue);
}
}, },
'editValue': { 'redo': function (params) {
'undo': function (params) { params.node.updateField(params.newValue);
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();
}
} }
},
'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: restore the original caret position and selection with each undo
// TODO: implement history for actions "expand", "collapse", "scroll", "setDocument" // TODO: implement history for actions "expand", "collapse", "scroll", "setDocument"
}; };
}
/**
* The method onChange is executed when the History is changed, and can
* be overloaded.
*/
History.prototype.onChange = function () {};
/**
* Add a new action to the history
* @param {String} action The executed action. Available actions: "editField",
* "editValue", "changeType", "appendNode",
* "removeNode", "duplicateNode", "moveNode"
* @param {Object} params Object containing parameters describing the change.
* The parameters in params depend on the action (for
* example for "editValue" the Node, old value, and new
* value are provided). params contains all information
* needed to undo or redo the action.
*/
History.prototype.add = function (action, params) {
this.index++;
this.history[this.index] = {
'action': action,
'params': params,
'timestamp': new Date()
};
// remove redo actions which are invalid now
if (this.index < this.history.length - 1) {
this.history.splice(this.index + 1, this.history.length - this.index - 1);
} }
/** // fire onchange event
* The method onChange is executed when the History is changed, and can this.onChange();
* be overloaded. };
*/
History.prototype.onChange = function () {};
/** /**
* Add a new action to the history * Clear history
* @param {String} action The executed action. Available actions: "editField", */
* "editValue", "changeType", "appendNode", History.prototype.clear = function () {
* "removeNode", "duplicateNode", "moveNode" this.history = [];
* @param {Object} params Object containing parameters describing the change. this.index = -1;
* The parameters in params depend on the action (for
* example for "editValue" the Node, old value, and new // fire onchange event
* value are provided). params contains all information this.onChange();
* needed to undo or redo the action. };
*/
History.prototype.add = function (action, params) { /**
* 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.index++;
this.history[this.index] = {
'action': action,
'params': params,
'timestamp': new Date()
};
// remove redo actions which are invalid now var obj = this.history[this.index];
if (this.index < this.history.length - 1) { if (obj) {
this.history.splice(this.index + 1, this.history.length - this.index - 1); 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 // fire onchange event
this.onChange(); this.onChange();
}; }
};
/** module.exports = History;
* 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;
});

View File

@ -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 * @constructor JSONEditor
* @param {Element} container Container element * @param {Element} container Container element
* @param {Object} [options] Object with options. available options: * @param {Object} [options] Object with options. available options:
* {String} mode Editor mode. Available values: * {String} mode Editor mode. Available values:
* 'tree' (default), 'view', * 'tree' (default), 'view',
* 'form', 'text', and 'code'. * 'form', 'text', and 'code'.
* {function} change Callback method, triggered * {function} change Callback method, triggered
* on change of contents * on change of contents
* {Boolean} search Enable search box. * {Boolean} search Enable search box.
* True by default * True by default
* Only applicable for modes * Only applicable for modes
* 'tree', 'view', and 'form' * 'tree', 'view', and 'form'
* {Boolean} history Enable history (undo/redo). * {Boolean} history Enable history (undo/redo).
* True by default * True by default
* Only applicable for modes * Only applicable for modes
* 'tree', 'view', and 'form' * 'tree', 'view', and 'form'
* {String} name Field name for the root node. * {String} name Field name for the root node.
* Only applicable for modes * Only applicable for modes
* 'tree', 'view', and 'form' * 'tree', 'view', and 'form'
* {Number} indentation Number of indentation * {Number} indentation Number of indentation
* spaces. 4 by default. * spaces. 4 by default.
* Only applicable for * Only applicable for
* modes 'text' and 'code' * modes 'text' and 'code'
* @param {Object | undefined} json JSON object * @param {Object | undefined} json JSON object
*/ */
function JSONEditor (container, options, json) { function JSONEditor (container, options, json) {
if (!(this instanceof JSONEditor)) { if (!(this instanceof JSONEditor)) {
throw new Error('JSONEditor constructor called without "new".'); throw new Error('JSONEditor constructor called without "new".');
}
// check for unsupported browser (IE8 and older)
var ieVersion = util.getInternetExplorerVersion();
if (ieVersion != -1 && ieVersion < 9) {
throw new Error('Unsupported browser, IE9 or newer required. ' +
'Please install the newest version of your browser.');
}
if (arguments.length) {
this._create(container, options, json);
}
} }
/** // check for unsupported browser (IE8 and older)
* Configuration for all registered modes. Example: var ieVersion = util.getInternetExplorerVersion();
* { if (ieVersion != -1 && ieVersion < 9) {
* tree: { throw new Error('Unsupported browser, IE9 or newer required. ' +
* mixin: TreeEditor, 'Please install the newest version of your browser.');
* data: 'json' }
* },
* text: {
* mixin: TextEditor,
* data: 'text'
* }
* }
*
* @type { Object.<String, {mixin: Object, data: String} > }
*/
JSONEditor.modes = {};
/** if (arguments.length) {
* Create the JSONEditor this._create(container, options, json);
* @param {Element} container Container element }
* @param {Object} [options] See description in constructor }
* @param {Object | undefined} json JSON object
* @private
*/
JSONEditor.prototype._create = function (container, options, json) {
this.container = container;
this.options = options || {};
this.json = json || {};
var mode = this.options.mode || 'tree'; /**
this.setMode(mode); * 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 * Create the JSONEditor
* @private * @param {Element} container Container element
*/ * @param {Object} [options] See description in constructor
JSONEditor.prototype._delete = function () {}; * @param {Object | undefined} json JSON object
* @private
*/
JSONEditor.prototype._create = function (container, options, json) {
this.container = container;
this.options = options || {};
this.json = json || {};
/** var mode = this.options.mode || 'tree';
* Set JSON object in editor this.setMode(mode);
* @param {Object | undefined} json JSON data };
*/
JSONEditor.prototype.set = function (json) {
this.json = json;
};
/** /**
* Get JSON from the editor * Detach the editor from the DOM
* @returns {Object} json * @private
*/ */
JSONEditor.prototype.get = function () { JSONEditor.prototype._delete = function () {};
return this.json;
};
/** /**
* Set string containing JSON for the editor * Set JSON object in editor
* @param {String | undefined} jsonText * @param {Object | undefined} json JSON data
*/ */
JSONEditor.prototype.setText = function (jsonText) { JSONEditor.prototype.set = function (json) {
this.json = util.parse(jsonText); this.json = json;
}; };
/** /**
* Get stringified JSON contents from the editor * Get JSON from the editor
* @returns {String} jsonText * @returns {Object} json
*/ */
JSONEditor.prototype.getText = function () { JSONEditor.prototype.get = function () {
return JSON.stringify(this.json); return this.json;
}; };
/** /**
* Set a field name for the root node. * Set string containing JSON for the editor
* @param {String | undefined} name * @param {String | undefined} jsonText
*/ */
JSONEditor.prototype.setName = function (name) { JSONEditor.prototype.setText = function (jsonText) {
if (!this.options) { this.json = util.parse(jsonText);
this.options = {}; };
}
this.options.name = name;
};
/** /**
* Get the field name for the root node. * Get stringified JSON contents from the editor
* @return {String | undefined} name * @returns {String} jsonText
*/ */
JSONEditor.prototype.getName = function () { JSONEditor.prototype.getText = function () {
return this.options && this.options.name; return JSON.stringify(this.json);
}; };
/** /**
* Change the mode of the editor. * Set a field name for the root node.
* JSONEditor will be extended with all methods needed for the chosen mode. * @param {String | undefined} name
* @param {String} mode Available modes: 'tree' (default), 'view', 'form', */
* 'text', and 'code'. JSONEditor.prototype.setName = function (name) {
*/ if (!this.options) {
JSONEditor.prototype.setMode = function (mode) { this.options = {};
var container = this.container, }
options = util.extend({}, this.options), this.options.name = name;
data, };
name;
options.mode = mode; /**
var config = JSONEditor.modes[mode]; * Get the field name for the root node.
if (config) { * @return {String | undefined} name
try { */
var asText = (config.data == 'text'); JSONEditor.prototype.getName = function () {
name = this.getName(); return this.options && this.options.name;
data = this[asText ? 'getText' : 'get'](); // get text or json };
this._delete(); /**
util.clear(this); * Change the mode of the editor.
util.extend(this, config.mixin); * JSONEditor will be extended with all methods needed for the chosen mode.
this.create(container, options); * @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); options.mode = mode;
this[asText ? 'setText' : 'set'](data); // set text or json 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') { this._delete();
try { util.clear(this);
config.load.call(this); util.extend(this, config.mixin);
} this.create(container, options);
catch (err) {}
this.setName(name);
this[asText ? 'setText' : 'set'](data); // set text or json
if (typeof config.load === 'function') {
try {
config.load.call(this);
} }
} catch (err) {}
catch (err) {
this._onError(err);
} }
} }
else { catch (err) {
throw new Error('Unknown mode "' + options.mode + '"'); this._onError(err);
} }
}; }
else {
throw new Error('Unknown mode "' + options.mode + '"');
}
};
/** /**
* Throw an error. If an error callback is configured in options.error, this * Throw an error. If an error callback is configured in options.error, this
* callback will be invoked. Else, a regular error is thrown. * callback will be invoked. Else, a regular error is thrown.
* @param {Error} err * @param {Error} err
* @private * @private
*/ */
JSONEditor.prototype._onError = function(err) { JSONEditor.prototype._onError = function(err) {
// TODO: onError is deprecated since version 2.2.0. cleanup some day // TODO: onError is deprecated since version 2.2.0. cleanup some day
if (typeof this.onError === 'function') { if (typeof this.onError === 'function') {
util.log('WARNING: JSONEditor.onError is deprecated. ' + util.log('WARNING: JSONEditor.onError is deprecated. ' +
'Use options.error instead.'); 'Use options.error instead.');
this.onError(err); 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') { // validate the mixin
this.options.error(err); if (typeof mode.mixin.create !== 'function') {
throw new Error('Required function "create" missing on mixin');
} }
else { var reserved = ['setMode', 'registerMode', 'modes'];
throw err; 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');
/**
* 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');
}
// validate the mixin JSONEditor.modes[name] = mode;
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; // register tree and text modes
} JSONEditor.registerMode(treemode);
}; JSONEditor.registerMode(textmode);
// register tree and text modes module.exports = JSONEditor;
JSONEditor.registerMode(treemode);
JSONEditor.registerMode(textmode);
return JSONEditor;
});

File diff suppressed because it is too large Load Diff

View File

@ -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;
/** this.editor = editor;
* @constructor SearchBox this.timeout = undefined;
* Create a search box in given HTML container this.delay = 200; // ms
* @param {JSONEditor} editor The JSON Editor to attach to this.lastText = undefined;
* @param {Element} container HTML container element of where to
* create the search box
*/
function SearchBox (editor, container) {
var searchBox = this;
this.editor = editor; this.dom = {};
this.timeout = undefined; this.dom.container = container;
this.delay = 200; // ms
this.lastText = undefined;
this.dom = {}; var table = document.createElement('table');
this.dom.container = container; 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'); var td = document.createElement('td');
this.dom.table = table; tr.appendChild(td);
table.className = 'search'; var results = document.createElement('div');
container.appendChild(table); this.dom.results = results;
var tbody = document.createElement('tbody'); results.className = 'results';
this.dom.tbody = tbody; td.appendChild(results);
table.appendChild(tbody);
var tr = document.createElement('tr');
tbody.appendChild(tr);
var td = document.createElement('td'); td = document.createElement('td');
tr.appendChild(td); tr.appendChild(td);
var results = document.createElement('div'); var divInput = document.createElement('div');
this.dom.results = results; this.dom.input = divInput;
results.className = 'results'; divInput.className = 'frame';
td.appendChild(results); divInput.title = 'Search fields and values';
td.appendChild(divInput);
td = document.createElement('td'); // table to contain the text input and search button
tr.appendChild(td); var tableInput = document.createElement('table');
var divInput = document.createElement('div'); divInput.appendChild(tableInput);
this.dom.input = divInput; var tbodySearch = document.createElement('tbody');
divInput.className = 'frame'; tableInput.appendChild(tbodySearch);
divInput.title = 'Search fields and values'; tr = document.createElement('tr');
td.appendChild(divInput); tbodySearch.appendChild(tr);
// table to contain the text input and search button var refreshSearch = document.createElement('button');
var tableInput = document.createElement('table'); refreshSearch.className = 'refresh';
divInput.appendChild(tableInput); td = document.createElement('td');
var tbodySearch = document.createElement('tbody'); td.appendChild(refreshSearch);
tableInput.appendChild(tbodySearch); tr.appendChild(td);
tr = document.createElement('tr');
tbodySearch.appendChild(tr);
var refreshSearch = document.createElement('button'); var search = document.createElement('input');
refreshSearch.className = 'refresh'; this.dom.search = search;
td = document.createElement('td'); search.oninput = function (event) {
td.appendChild(refreshSearch); searchBox._onDelayedSearch(event);
tr.appendChild(td); };
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'); // TODO: ESC in FF restores the last input, is a FF bug, https://bugzilla.mozilla.org/show_bug.cgi?id=598819
this.dom.search = search; td = document.createElement('td');
search.oninput = function (event) { td.appendChild(search);
searchBox._onDelayedSearch(event); tr.appendChild(td);
};
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 var searchNext = document.createElement('button');
td = document.createElement('td'); searchNext.title = 'Next result (Enter)';
td.appendChild(search); searchNext.className = 'next';
tr.appendChild(td); searchNext.onclick = function () {
searchBox.next();
};
td = document.createElement('td');
td.appendChild(searchNext);
tr.appendChild(td);
var searchNext = document.createElement('button'); var searchPrevious = document.createElement('button');
searchNext.title = 'Next result (Enter)'; searchPrevious.title = 'Previous result (Shift+Enter)';
searchNext.className = 'next'; searchPrevious.className = 'previous';
searchNext.onclick = function () { searchPrevious.onclick = function () {
searchBox.next(); searchBox.previous();
}; };
td = document.createElement('td'); td = document.createElement('td');
td.appendChild(searchNext); td.appendChild(searchPrevious);
tr.appendChild(td); tr.appendChild(td);
}
var searchPrevious = document.createElement('button'); /**
searchPrevious.title = 'Previous result (Shift+Enter)'; * Go to the next search result
searchPrevious.className = 'previous'; * @param {boolean} [focus] If true, focus will be set to the next result
searchPrevious.onclick = function () { * focus is false by default.
searchBox.previous(); */
}; SearchBox.prototype.next = function(focus) {
td = document.createElement('td'); if (this.results != undefined) {
td.appendChild(searchPrevious); var index = (this.resultIndex != undefined) ? this.resultIndex + 1 : 0;
tr.appendChild(td); if (index > this.results.length - 1) {
index = 0;
}
this._setActiveResult(index, focus);
} }
};
/** /**
* Go to the next search result * Go to the prevous search result
* @param {boolean} [focus] If true, focus will be set to the next result * @param {boolean} [focus] If true, focus will be set to the next result
* focus is false by default. * focus is false by default.
*/ */
SearchBox.prototype.next = function(focus) { SearchBox.prototype.previous = function(focus) {
if (this.results != undefined) { if (this.results != undefined) {
var index = (this.resultIndex != undefined) ? this.resultIndex + 1 : 0; var max = this.results.length - 1;
if (index > this.results.length - 1) { var index = (this.resultIndex != undefined) ? this.resultIndex - 1 : max;
index = 0; if (index < 0) {
} index = max;
this._setActiveResult(index, focus);
} }
}; this._setActiveResult(index, focus);
}
};
/** /**
* Go to the prevous search result * Set new value for the current active result
* @param {boolean} [focus] If true, focus will be set to the next result * @param {Number} index
* focus is false by default. * @param {boolean} [focus] If true, focus will be set to the next result.
*/ * focus is false by default.
SearchBox.prototype.previous = function(focus) { * @private
if (this.results != undefined) { */
var max = this.results.length - 1; SearchBox.prototype._setActiveResult = function(index, focus) {
var index = (this.resultIndex != undefined) ? this.resultIndex - 1 : max; // de-activate current active result
if (index < 0) { if (this.activeResult) {
index = max; var prevNode = this.activeResult.node;
} var prevElem = this.activeResult.elem;
this._setActiveResult(index, focus); if (prevElem == 'field') {
} delete prevNode.searchFieldActive;
};
/**
* Set new value for the current active result
* @param {Number} index
* @param {boolean} [focus] If true, focus will be set to the next result.
* focus is false by default.
* @private
*/
SearchBox.prototype._setActiveResult = function(index, focus) {
// de-activate current active result
if (this.activeResult) {
var prevNode = this.activeResult.node;
var prevElem = this.activeResult.elem;
if (prevElem == 'field') {
delete prevNode.searchFieldActive;
}
else {
delete prevNode.searchValueActive;
}
prevNode.updateDom();
}
if (!this.results || !this.results[index]) {
// out of range, set to undefined
this.resultIndex = undefined;
this.activeResult = undefined;
return;
}
this.resultIndex = index;
// set new node active
var node = this.results[this.resultIndex].node;
var elem = this.results[this.resultIndex].elem;
if (elem == 'field') {
node.searchFieldActive = true;
} }
else { else {
node.searchValueActive = true; delete prevNode.searchValueActive;
} }
this.activeResult = this.results[this.resultIndex]; prevNode.updateDom();
node.updateDom(); }
// TODO: not so nice that the focus is only set after the animation is finished if (!this.results || !this.results[index]) {
node.scrollTo(function () { // out of range, set to undefined
if (focus) { this.resultIndex = undefined;
node.focus(elem); this.activeResult = undefined;
} return;
}); }
};
/** this.resultIndex = index;
* Cancel any running onDelayedSearch.
* @private // set new node active
*/ var node = this.results[this.resultIndex].node;
SearchBox.prototype._clearDelay = function() { var elem = this.results[this.resultIndex].elem;
if (this.timeout != undefined) { if (elem == 'field') {
clearTimeout(this.timeout); node.searchFieldActive = true;
delete this.timeout; }
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. * Cancel any running onDelayedSearch.
* Used for reducing the number of searches while typing. * @private
* @param {Event} event */
* @private SearchBox.prototype._clearDelay = function() {
*/ if (this.timeout != undefined) {
SearchBox.prototype._onDelayedSearch = function (event) { clearTimeout(this.timeout);
// execute the search after a short delay (reduces the number of delete this.timeout;
// search actions while typing in the search text box) }
this._clearDelay(); };
var searchBox = this;
this.timeout = setTimeout(function (event) {
searchBox._onSearch(event);
},
this.delay);
};
/** /**
* Handle onSearch event * Start a timer to execute a search after a short delay.
* @param {Event} event * Used for reducing the number of searches while typing.
* @param {boolean} [forceSearch] If true, search will be executed again even * @param {Event} event
* when the search text is not changed. * @private
* Default is false. */
* @private SearchBox.prototype._onDelayedSearch = function (event) {
*/ // execute the search after a short delay (reduces the number of
SearchBox.prototype._onSearch = function (event, forceSearch) { // search actions while typing in the search text box)
this._clearDelay(); 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; * Handle onSearch event
if (text != this.lastText || forceSearch) { * @param {Event} event
// only search again when changed * @param {boolean} [forceSearch] If true, search will be executed again even
this.lastText = text; * when the search text is not changed.
this.results = this.editor.search(text); * Default is false.
this._setActiveResult(undefined); * @private
*/
SearchBox.prototype._onSearch = function (event, forceSearch) {
this._clearDelay();
// display search results var value = this.dom.search.value;
if (text != undefined) { var text = (value.length > 0) ? value : undefined;
var resultCount = this.results.length; if (text != this.lastText || forceSearch) {
switch (resultCount) { // only search again when changed
case 0: this.dom.results.innerHTML = 'no&nbsp;results'; break; this.lastText = text;
case 1: this.dom.results.innerHTML = '1&nbsp;result'; break; this.results = this.editor.search(text);
default: this.dom.results.innerHTML = resultCount + '&nbsp;results'; break; this._setActiveResult(undefined);
}
} // display search results
else { if (text != undefined) {
this.dom.results.innerHTML = ''; var resultCount = this.results.length;
switch (resultCount) {
case 0: this.dom.results.innerHTML = 'no&nbsp;results'; break;
case 1: this.dom.results.innerHTML = '1&nbsp;result'; break;
default: this.dom.results.innerHTML = resultCount + '&nbsp;results'; break;
} }
} }
}; else {
this.dom.results.innerHTML = '';
/**
* Handle onKeyDown event in the input box
* @param {Event} event
* @private
*/
SearchBox.prototype._onKeyDown = function (event) {
var keynum = event.which;
if (keynum == 27) { // ESC
this.dom.search.value = ''; // clear search
this._onSearch(event);
event.preventDefault();
event.stopPropagation();
} }
else if (keynum == 13) { // Enter }
if (event.ctrlKey) { };
// force to search again
this._onSearch(event, true); /**
} * Handle onKeyDown event in the input box
else if (event.shiftKey) { * @param {Event} event
// move to the previous search result * @private
this.previous(); */
} SearchBox.prototype._onKeyDown = function (event) {
else { var keynum = event.which;
// move to the next search result if (keynum == 27) { // ESC
this.next(); this.dom.search.value = ''; // clear search
} this._onSearch(event);
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
}
else if (keynum == 13) { // Enter
if (event.ctrlKey) {
// force to search again
this._onSearch(event, true);
} }
}; else if (event.shiftKey) {
// move to the previous search result
/** this.previous();
* 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 {
// move to the next search result
return SearchBox; 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;

View File

@ -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 * @constructor AppendNode
* @param {Node} Node * @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) { function AppendNode (editor) {
/** /** @type {TreeEditor} */
* @constructor AppendNode this.editor = editor;
* @extends Node this.dom = {};
* @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;
} }
// return the factory function AppendNode.prototype = new Node();
return appendNodeFactory;
}); /**
* 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;

View File

@ -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 * Switch the mode of the editor
* @param {Object} editor * @param {String} mode
* @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) { function switchMode(mode) {
// TODO: decouple mode switcher from editor // switch mode
editor.setMode(mode);
/** // restore focus on mode box
* Switch the mode of the editor var modeBox = editor.dom && editor.dom.modeBox;
* @param {String} mode if (modeBox) {
*/ modeBox.focus();
function switchMode(mode) {
// switch mode
editor.setMode(mode);
// restore focus on mode box
var modeBox = editor.dom && editor.dom.modeBox;
if (modeBox) {
modeBox.focus();
}
} }
// available modes
var availableModes = {
code: {
'text': 'Code',
'title': 'Switch to code highlighter',
'click': function () {
switchMode('code')
}
},
form: {
'text': 'Form',
'title': 'Switch to form editor',
'click': function () {
switchMode('form');
}
},
text: {
'text': 'Text',
'title': 'Switch to plain text editor',
'click': function () {
switchMode('text');
}
},
tree: {
'text': 'Tree',
'title': 'Switch to tree editor',
'click': function () {
switchMode('tree');
}
},
view: {
'text': 'View',
'title': 'Switch to tree view',
'click': function () {
switchMode('view');
}
}
};
// list the selected modes
var items = [];
for (var i = 0; i < modes.length; i++) {
var mode = modes[i];
var item = availableModes[mode];
if (!item) {
throw new Error('Unknown mode "' + mode + '"');
}
item.className = 'type-modes' + ((current == mode) ? ' selected' : '');
items.push(item);
}
// retrieve the title of current mode
var currentMode = availableModes[current];
if (!currentMode) {
throw new Error('Unknown mode "' + current + '"');
}
var currentTitle = currentMode.text;
// create the html element
var box = document.createElement('button');
box.className = 'modes separator';
box.innerHTML = currentTitle + ' &#x25BE;';
box.title = 'Switch editor mode';
box.onclick = function () {
var menu = new ContextMenu(items);
menu.show(box);
};
return box;
} }
return { // available modes
create: createModeSwitcher 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 + ' &#x25BE;';
box.title = 'Switch editor mode';
box.onclick = function () {
var menu = new ContextMenu(items);
menu.show(box);
};
return box;
}
exports.create = createModeSwitcher;

View File

@ -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;
}

View File

@ -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 // create a mixin with the functions for text mode
var textmode = {}; var textmode = {};
/** /**
* Create a text editor * Create a text editor
* @param {Element} container * @param {Element} container
* @param {Object} [options] Object with options. available options: * @param {Object} [options] Object with options. available options:
* {String} mode Available values: * {String} mode Available values:
* "text" (default) * "text" (default)
* or "code". * or "code".
* {Number} indentation Number of indentation * {Number} indentation Number of indentation
* spaces. 2 by default. * spaces. 2 by default.
* {function} change Callback method * {function} change Callback method
* triggered on change * triggered on change
* @private * @private
*/ */
textmode.create = function (container, options) { textmode.create = function (container, options) {
// read options // read options
options = options || {}; options = options || {};
this.options = options; this.options = options;
if (options.indentation) { if (options.indentation) {
this.indentation = Number(options.indentation); this.indentation = Number(options.indentation);
} }
else { else {
this.indentation = 2; // number of spaces this.indentation = 2; // number of spaces
} }
this.mode = (options.mode == 'code') ? 'code' : 'text'; this.mode = (options.mode == 'code') ? 'code' : 'text';
if (this.mode == 'code') { if (this.mode == 'code') {
// verify whether Ace editor is available and supported // verify whether Ace editor is available and supported
if (typeof ace === 'undefined') { if (typeof ace === 'undefined') {
this.mode = 'text'; this.mode = 'text';
util.log('WARNING: Cannot load code editor, Ace library not loaded. ' + util.log('WARNING: Cannot load code editor, Ace library not loaded. ' +
'Falling back to plain text editor'); 'Falling back to plain text editor');
}
} }
}
var me = this; var me = this;
this.container = container; this.container = container;
this.dom = {}; this.dom = {};
this.editor = undefined; // ace code editor this.editor = undefined; // ace code editor
this.textarea = undefined; // plain text editor (fallback when Ace is not available) this.textarea = undefined; // plain text editor (fallback when Ace is not available)
this.width = container.clientWidth; this.width = container.clientWidth;
this.height = container.clientHeight; this.height = container.clientHeight;
this.frame = document.createElement('div'); this.frame = document.createElement('div');
this.frame.className = 'jsoneditor'; this.frame.className = 'jsoneditor';
this.frame.onclick = function (event) { this.frame.onclick = function (event) {
// prevent default submit action when the editor is located inside a form // prevent default submit action when the editor is located inside a form
event.preventDefault(); event.preventDefault();
}; };
this.frame.onkeydown = function (event) { this.frame.onkeydown = function (event) {
me._onKeyDown(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();
}
}
}
}
}; };
/** // create menu
* Event handler for keydown. Handles shortcut keys this.menu = document.createElement('div');
* @param {Event} event this.menu.className = 'menu';
* @private this.frame.appendChild(this.menu);
*/
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 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 { try {
json = util.parse(text); // this can throw an error me.format();
} }
catch (err) { catch (err) {
// try to sanitize json, replace JavaScript notation with JSON notation me._onError(err);
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 // create compact button
return [ var buttonCompact = document.createElement('button');
{ buttonCompact.className = 'compact';
mode: 'text', buttonCompact.title = 'Compact JSON data, remove all whitespaces (Ctrl+Shift+\\)';
mixin: textmode, this.menu.appendChild(buttonCompact);
data: 'text', buttonCompact.onclick = function () {
load: textmode.format try {
}, me.compact();
{
mode: 'code',
mixin: textmode,
data: 'text',
load: textmode.format
} }
]; 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
}
];

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff