diff --git a/examples/06_custom_styling.html b/examples/06_custom_styling.html index bc17075..37d9add 100644 --- a/examples/06_custom_styling.html +++ b/examples/06_custom_styling.html @@ -1,37 +1,37 @@ - JSONEditor | Custom styling + JSONEditor | Custom styling - - + + - + p { + width: 500px; + font-family: "DejaVu Sans", sans-serif; + } + - + -

- This example demonstrates how to customize the look of JSONEditor, - the editor below has a dark theme. Note that the example isn't worked - out for the mode code. To do that, you can load and configure - a custom theme for the Ace editor. -

+

+ This example demonstrates how to customize the look of JSONEditor, + the editor below has a dark theme. Note that the example isn't worked + out for the mode code. To do that, you can load and configure + a custom theme for the Ace editor. +

-
+
- + diff --git a/examples/11_autocomplete.html b/examples/11_autocomplete.html new file mode 100644 index 0000000..e8f543b --- /dev/null +++ b/examples/11_autocomplete.html @@ -0,0 +1,105 @@ + + + + JSONEditor | Auto Complete + + + + + + + + + + + +

+ This example demonstrates how to autocomplete works with an ActivationChar option, press "*" in any value and continue with autocompletion. +

+ +
+ + + + diff --git a/examples/12_autocomplete_simple.html b/examples/12_autocomplete_simple.html new file mode 100644 index 0000000..20f55a6 --- /dev/null +++ b/examples/12_autocomplete_simple.html @@ -0,0 +1,92 @@ + + + + JSONEditor | Auto Complete + + + + + + + + + + + +

+ This example demonstrates how to autocomplete works, options available are dynamics and consist in all the strings found in the json +

+ +
+ + + + diff --git a/examples/13_autocomplete_simplest.html b/examples/13_autocomplete_simplest.html new file mode 100644 index 0000000..ef82482 --- /dev/null +++ b/examples/13_autocomplete_simplest.html @@ -0,0 +1,56 @@ + + + + JSONEditor | Auto Complete + + + + + + + + + + + +

+ This example demonstrates how to autocomplete works, options available are "apple","cranberry","raspberry","pie" +

+ +
+ + + + diff --git a/src/js/JSONEditor.js b/src/js/JSONEditor.js index fd2c2c3..1cc6788 100644 --- a/src/js/JSONEditor.js +++ b/src/js/JSONEditor.js @@ -79,8 +79,8 @@ function JSONEditor (container, options, json) { // validate options if (options) { var VALID_OPTIONS = [ - 'ace', 'theme', 'ajv', 'schema','templates', + 'ace', 'theme','autocomplete', 'onChange', 'onEditable', 'onError', 'onModeChange', 'escapeUnicode', 'history', 'search', 'mode', 'modes', 'name', 'indentation', 'sortObjectKeys' ]; diff --git a/src/js/autocomplete.js b/src/js/autocomplete.js new file mode 100644 index 0000000..501db81 --- /dev/null +++ b/src/js/autocomplete.js @@ -0,0 +1,414 @@ +'use strict'; + +(function (arr) { + arr.forEach(function (item) { + if (item.hasOwnProperty('remove')) { + return; + } + Object.defineProperty(item, 'remove', { + configurable: true, + enumerable: true, + writable: true, + value: function remove() { + if (this.parentNode != null) + this.parentNode.removeChild(this); + } + }); + }); +})([Element.prototype, CharacterData.prototype, DocumentType.prototype]); + +if (!String.prototype.startsWith) { + String.prototype.startsWith = function (searchString, position) { + position = position || 0; + return this.substr(position, searchString.length) === searchString; + }; +} + +function completely(config) { + config = config || {}; + config.ConfirmKeys = config.ConfirmKeys || [39, 35, 9] // right, end, tab + config.fontSize = config.fontSize || ''; + config.fontFamily = config.fontFamily || ''; + config.promptInnerHTML = config.promptInnerHTML || ''; + config.color = config.color || '#333'; + config.hintColor = config.hintColor || '#aaa'; + config.backgroundColor = config.backgroundColor || '#fff'; + config.dropDownBorderColor = config.dropDownBorderColor || '#aaa'; + config.dropDownZIndex = config.dropDownZIndex || '100'; // to ensure we are in front of everybody + config.dropDownOnHoverBackgroundColor = config.dropDownOnHoverBackgroundColor || '#ddd'; + + var wrapper = document.createElement('div'); + wrapper.style.position = 'relative'; + wrapper.style.outline = '0'; + wrapper.style.border = '0'; + wrapper.style.margin = '0'; + wrapper.style.padding = '0'; + + var dropDown = document.createElement('div'); + dropDown.style.position = 'absolute'; + dropDown.style.visibility = 'hidden'; + dropDown.style.outline = '0'; + dropDown.style.margin = '0'; + dropDown.style.paddingLeft = '2pt'; + dropDown.style.paddingRight = '10pt'; + dropDown.style.textAlign = 'left'; + dropDown.style.fontSize = config.fontSize; + dropDown.style.fontFamily = config.fontFamily; + dropDown.style.backgroundColor = config.backgroundColor; + dropDown.style.zIndex = config.dropDownZIndex; + dropDown.style.cursor = 'default'; + dropDown.style.borderStyle = 'solid'; + dropDown.style.borderWidth = '1px'; + dropDown.style.borderColor = config.dropDownBorderColor; + dropDown.style.overflowX = 'hidden'; + dropDown.style.whiteSpace = 'pre'; + dropDown.style.overflowY = 'scroll'; // note: this might be ugly when the scrollbar is not required. however in this way the width of the dropDown takes into account + + + var spacer; + var leftSide; // <-- it will contain the leftSide part of the textfield (the bit that was already autocompleted) + var createDropDownController = function (elem, rs) { + var rows = []; + var ix = 0; + var oldIndex = -1; + + var onMouseOver = function () { this.style.outline = '1px solid #ddd'; } + var onMouseOut = function () { this.style.outline = '0'; } + var onMouseDown = function () { p.hide(); p.onmouseselection(this.__hint, p.rs); } + + var p = { + rs: rs, + hide: function () { + elem.style.visibility = 'hidden'; + //rs.hideDropDown(); + }, + refresh: function (token, array) { + elem.style.visibility = 'hidden'; + ix = 0; + elem.innerHTML = ''; + var vph = (window.innerHeight || document.documentElement.clientHeight); + var rect = elem.parentNode.getBoundingClientRect(); + var distanceToTop = rect.top - 6; // heuristic give 6px + var distanceToBottom = vph - rect.bottom - 6; // distance from the browser border. + + rows = []; + for (var i = 0; i < array.length; i++) { + if (array[i].indexOf(token) !== 0) { continue; } + var divRow = document.createElement('div'); + divRow.style.color = config.color; + divRow.onmouseover = onMouseOver; + divRow.onmouseout = onMouseOut; + divRow.onmousedown = onMouseDown; + divRow.__hint = array[i]; + divRow.innerHTML = token + '' + array[i].substring(token.length) + ''; + rows.push(divRow); + elem.appendChild(divRow); + } + if (rows.length === 0) { + return; // nothing to show. + } + if (rows.length === 1 && token === rows[0].__hint) { + return; // do not show the dropDown if it has only one element which matches what we have just displayed. + } + + if (rows.length < 2) return; + p.highlight(0); + + if (distanceToTop > distanceToBottom * 3) { // Heuristic (only when the distance to the to top is 4 times more than distance to the bottom + elem.style.maxHeight = distanceToTop + 'px'; // we display the dropDown on the top of the input text + elem.style.top = ''; + elem.style.bottom = '100%'; + } else { + elem.style.top = '100%'; + elem.style.bottom = ''; + elem.style.maxHeight = distanceToBottom + 'px'; + } + elem.style.visibility = 'visible'; + }, + highlight: function (index) { + if (oldIndex != -1 && rows[oldIndex]) { + rows[oldIndex].style.backgroundColor = config.backgroundColor; + } + rows[index].style.backgroundColor = config.dropDownOnHoverBackgroundColor; // <-- should be config + oldIndex = index; + }, + move: function (step) { // moves the selection either up or down (unless it's not possible) step is either +1 or -1. + if (elem.style.visibility === 'hidden') return ''; // nothing to move if there is no dropDown. (this happens if the user hits escape and then down or up) + if (ix + step === -1 || ix + step === rows.length) return rows[ix].__hint; // NO CIRCULAR SCROLLING. + ix += step; + p.highlight(ix); + return rows[ix].__hint;//txtShadow.value = uRows[uIndex].__hint ; + }, + onmouseselection: function () { } // it will be overwritten. + }; + return p; + } + + function setEndOfContenteditable(contentEditableElement) { + var range, selection; + if (document.createRange)//Firefox, Chrome, Opera, Safari, IE 9+ + { + range = document.createRange();//Create a range (a range is a like the selection but invisible) + range.selectNodeContents(contentEditableElement);//Select the entire contents of the element with the range + range.collapse(false);//collapse the range to the end point. false means collapse to end rather than the start + selection = window.getSelection();//get the selection object (allows you to change selection) + selection.removeAllRanges();//remove any selections already made + selection.addRange(range);//make the range you have just created the visible selection + } + else if (document.selection)//IE 8 and lower + { + range = document.body.createTextRange();//Create a range (a range is a like the selection but invisible) + range.moveToElementText(contentEditableElement);//Select the entire contents of the element with the range + range.collapse(false);//collapse the range to the end point. false means collapse to end rather than the start + range.select();//Select the range (make it the visible selection + } + } + + function calculateWidthForText(text) { + if (spacer === undefined) { // on first call only. + spacer = document.createElement('span'); + spacer.style.visibility = 'hidden'; + spacer.style.position = 'fixed'; + spacer.style.outline = '0'; + spacer.style.margin = '0'; + spacer.style.padding = '0'; + spacer.style.border = '0'; + spacer.style.left = '0'; + spacer.style.whiteSpace = 'pre'; + spacer.style.fontSize = config.fontSize; + spacer.style.fontFamily = config.fontFamily; + spacer.style.fontWeight = 'normal'; + document.body.appendChild(spacer); + } + + // Used to encode an HTML string into a plain text. + // taken from http://stackoverflow.com/questions/1219860/javascript-jquery-html-encoding + spacer.innerHTML = String(text).replace(/&/g, '&') + .replace(/"/g, '"') + .replace(/'/g, ''') + .replace(//g, '>'); + return spacer.getBoundingClientRect().right; + } + + var rs = { + onArrowDown: function () { }, // defaults to no action. + onArrowUp: function () { }, // defaults to no action. + onEnter: function () { }, // defaults to no action. + onTab: function () { }, // defaults to no action. + startFrom: 0, + options: [], + element: null, + elementHint: null, + elementStyle: null, + wrapper: wrapper, // Only to allow easy access to the HTML elements to the final user (possibly for minor customizations) + Show: function (element, options) { + this.wrapper.remove(); + if (this.elementHint) { + this.elementHint.remove(); + this.elementHint = null; + } + + if (config.fontSize == '') { + config.fontSize = window.getComputedStyle(element).getPropertyValue('font-size'); + dropDown.style.fontSize = config.fontSize; + } + if (config.fontFamily == '') { + config.fontFamily = window.getComputedStyle(element).getPropertyValue('font-family'); + dropDown.style.fontFamily = config.fontFamily; + } + + var w = element.getBoundingClientRect().right - element.getBoundingClientRect().left; + dropDown.style.marginLeft = '0'; + dropDown.style.marginTop = element.getBoundingClientRect().height + 'px'; + this.options = options; + + if (this.element != element) { + this.element = element; + this.elementStyle = { + zIndex: this.element.style.zIndex, + position: this.element.style.position, + backgroundColor: this.element.style.backgroundColor, + borderColor: this.element.style.borderColor + } + } + + this.element.style.zIndex = 3; + this.element.style.position = 'relative'; + this.element.style.backgroundColor = 'transparent'; + this.element.style.borderColor = 'transparent'; + + + this.elementHint = element.cloneNode(); + this.elementHint.style.zIndex = 2; + this.elementHint.style.position = 'absolute'; + this.elementHint.style.top = '0'; + this.elementHint.style.left = '0'; + this.elementHint.style.color = config.hintColor; + this.elementHint.onfocus = function () { this.element.focus(); }.bind(this); + + if (this.element.addEventListener) { + this.element.removeEventListener("keydown", keyDownHandler); + this.element.addEventListener("keydown", keyDownHandler, false); + this.element.removeEventListener("blur", onBlurHandler); + this.element.addEventListener("blur", onBlurHandler, false); + } + + wrapper.appendChild(this.elementHint); + wrapper.appendChild(dropDown); + element.parentElement.appendChild(wrapper); + + + this.repaint(element); + }, + setText: function (text) { + this.element.innerText = text; + }, + getText: function () { + return this.element.innerText; + }, + hideDropDown: function () { + this.wrapper.remove(); + if (this.elementHint) { + this.elementHint.remove(); + this.elementHint = null; + dropDownController.hide(); + this.element.style.zIndex = this.elementStyle.zIndex; + this.element.style.position = this.elementStyle.position; + this.element.style.backgroundColor = this.elementStyle.backgroundColor; + this.element.style.borderColor = this.elementStyle.borderColor; + } + + }, + repaint: function (element) { + var text = element.innerText; + text = text.replace('\n', ''); + + var startFrom = this.startFrom; + var options = this.options; + var optionsLength = this.options.length; + + // breaking text in leftSide and token. + + var token = text.substring(this.startFrom); + leftSide = text.substring(0, this.startFrom); + + for (var i = 0; i < optionsLength; i++) { + var opt = this.options[i]; + if (opt.indexOf(token) === 0) { // <-- how about upperCase vs. lowercase + this.elementHint.innerText = leftSide + opt; + break; + } + } + // moving the dropDown and refreshing it. + dropDown.style.left = calculateWidthForText(leftSide) + 'px'; + dropDownController.refresh(token, this.options); + this.elementHint.style.width = calculateWidthForText(this.elementHint.innerText) + 10 + 'px' + var wasDropDownHidden = (dropDown.style.visibility == 'hidden'); + if (!wasDropDownHidden) + this.elementHint.style.width = calculateWidthForText(this.elementHint.innerText) + dropDown.clientWidth + 'px'; + } + }; + + var dropDownController = createDropDownController(dropDown, rs); + + var keyDownHandler = function (e) { + //console.log("Keydown:" + e.keyCode); + e = e || window.event; + var keyCode = e.keyCode; + + if (this.elementHint == null) return; + + if (keyCode == 33) { return; } // page up (do nothing) + if (keyCode == 34) { return; } // page down (do nothing); + + if (keyCode == 27) { //escape + rs.hideDropDown(); + rs.element.focus(); + e.preventDefault(); + e.stopPropagation(); + return; + } + config.ConfirmKeys + if (config.ConfirmKeys.indexOf(keyCode) >= 0) { // (autocomplete triggered) + if (keyCode == 9) { + if (this.elementHint.innerText.length == 0) { + rs.onTab(); + } + } + if (this.elementHint.innerText.length > 0) { // if there is a hint + if (this.element.innerText != this.elementHint.innerText) { + this.element.innerText = this.elementHint.innerText; + rs.hideDropDown(); + setEndOfContenteditable(this.element); + if (keyCode == 9) { + rs.element.focus(); + e.preventDefault(); + e.stopPropagation(); + } + } + } + return; + } + + if (keyCode == 13) { // enter (autocomplete triggered) + if (this.elementHint.innerText.length == 0) { // if there is a hint + rs.onEnter(); + } else { + var wasDropDownHidden = (dropDown.style.visibility == 'hidden'); + dropDownController.hide(); + + if (wasDropDownHidden) { + rs.hideDropDown(); + rs.element.focus(); + rs.onEnter(); + return; + } + + this.element.innerText = this.elementHint.innerText; + rs.hideDropDown(); + setEndOfContenteditable(this.element); + e.preventDefault(); + e.stopPropagation(); + } + return; + } + + if (keyCode == 40) { // down + var m = dropDownController.move(+1); + if (m == '') { rs.onArrowDown(); } + this.elementHint.innerText = leftSide + m; + e.preventDefault(); + e.stopPropagation(); + return; + } + + if (keyCode == 38) { // up + var m = dropDownController.move(-1); + if (m == '') { rs.onArrowUp(); } + this.elementHint.innerText = leftSide + m; + e.preventDefault(); + e.stopPropagation(); + return; + } + + }.bind(rs); + + var onBlurHandler = function (e) { + rs.hideDropDown(); + //console.log("Lost focus."); + }.bind(rs); + + dropDownController.onmouseselection = function (text, rs) { + rs.element.innerText = rs.elementHint.innerText = leftSide + text; + rs.hideDropDown(); + window.setTimeout(function () { + rs.element.focus(); + setEndOfContenteditable(rs.element); + }, 1); + }; + + return rs; +} + +module.exports = completely; \ No newline at end of file diff --git a/src/js/treemode.js b/src/js/treemode.js index 1fc243e..c32db01 100644 --- a/src/js/treemode.js +++ b/src/js/treemode.js @@ -8,6 +8,7 @@ var ContextMenu = require('./ContextMenu'); var Node = require('./Node'); var ModeSwitcher = require('./ModeSwitcher'); var util = require('./util'); +var autocomplete = require('./autocomplete'); // create a mixin with the functions for tree mode var treemode = {}; @@ -51,6 +52,9 @@ treemode.create = function (container, options) { this._setOptions(options); + if (options.autocomplete) + this.autocomplete = new autocomplete(options.autocomplete.Config); + if (this.options.history && this.options.mode !== 'view') { this.history = new History(this); } @@ -107,7 +111,8 @@ treemode._setOptions = function (options) { history: true, mode: 'tree', name: undefined, // field name of root node - schema: null + schema: null, + autocomplete: null }; // copy all options @@ -1051,7 +1056,9 @@ treemode._findTopLevelNodes = function (start, end) { */ treemode._onKeyDown = function (event) { var keynum = event.which || event.keyCode; + var altKey = event.altKey; var ctrlKey = event.ctrlKey; + var metaKey = event.metaKey; var shiftKey = event.shiftKey; var handled = false; @@ -1097,6 +1104,28 @@ treemode._onKeyDown = function (event) { } } + if ((this.options.autocomplete) && (!handled)) { + if (!ctrlKey && !altKey && !metaKey && (event.key.length == 1 || keynum == 8 || keynum == 46)) { + handled = false; + if ((this.options.autocomplete.ApplyTo.indexOf('value') >= 0 && event.target.className.indexOf("jsoneditor-value") >= 0) || + (this.options.autocomplete.ApplyTo.indexOf('name') >= 0 && event.target.className.indexOf("jsoneditor-field") >= 0)) { + var node = Node.getNodeFromTarget(event.target); + if (this.options.autocomplete.ActivationChar == null || event.target.innerText.startsWith(this.options.autocomplete.ActivationChar)) { // Activate autocomplete + setTimeout(function (hnode, element) { + if (element.innerText.length > 0) { + var options = this.options.autocomplete.GetOptions(this.autocomplete, hnode, element.innerText); + if (options.length > 0) + this.autocomplete.Show(element, options); + } + else + this.autocomplete.hideDropDown(); + + }.bind(this, node, event.target), 100); + } + } + } + } + if (handled) { event.preventDefault(); event.stopPropagation();