autocomplete integrated.

This commit is contained in:
Israel Garcia 2017-05-26 03:58:16 -04:00
commit 5411ad19ac
7 changed files with 721 additions and 25 deletions

View File

@ -0,0 +1,105 @@
<!DOCTYPE HTML>
<html>
<head>
<title>JSONEditor | Auto Complete</title>
<link href="../dist/jsoneditor.css" rel="stylesheet" type="text/css">
<script src="../dist/jsoneditor.js"></script>
<style type="text/css">
#jsoneditor {
width: 500px;
height: 500px;
}
p {
width: 500px;
font-family: "DejaVu Sans", sans-serif;
}
</style>
<!--<link href="./css/darktheme.css" rel="stylesheet" type="text/css">-->
</head>
<body>
<p>
This example demonstrates how to autocomplete works with an ActivationChar option, press "*" in any value and continue with autocompletion.
</p>
<div id="jsoneditor"></div>
<script>
// create the editor
var container = document.getElementById('jsoneditor');
var options = {
modes: ['text', 'tree'],
autocomplete: {
Config: {
ConfirmKeys: [39, 35, 9, 190] // Confirm Autocomplete Keys: [right, end, tab, '.'] // By default are only [right, end, tab]
},
ActivationChar: '*', // ActivationChar indicate if the value starts with this Char then autocompletion will be activated. ('' will be ignored).
ApplyTo:['value'],
GetOptions: function (autocomplete, node, text) {
var YaskON = {
// Return first level json paths by the node 'o'
stringify: function (o, prefix, activationChar) {
prefix = prefix || '';
switch (typeof o) {
case 'object':
var output = "";
if (Array.isArray(o)) {
o.forEach(function (e, index) {
output += activationChar + prefix + '[' + index + ']' + '\n';
}.bind(this));
return output;
}
output = "";
for (var k in o) {
if (o.hasOwnProperty(k)) {
if (prefix == "") output += this.stringify(o[k], k, activationChar);
}
}
if (prefix != "") output += activationChar + prefix + '\n'
return output;
case 'function':
return "";
default:
return prefix + '\n';
}
}
};
var data = {};
autocomplete.startFrom = 0;
var lastPoint = text.lastIndexOf('.');
if ((lastPoint > 0) && (text.length > 1)) {
var fnode = node.editor.node.findNode('.' + text.substring(this.ActivationChar.length, lastPoint));
if (fnode && typeof fnode.getValue() == 'object') {
data = fnode.getValue();
// Indicate that autocompletion should start after the . (ignoring the first part)
autocomplete.startFrom = text.lastIndexOf('.') + 1;
}
}
else
data = node.editor.get();
var optionsStr = YaskON.stringify(data, null, this.ActivationChar);
var options = optionsStr.split("\n");
return options;
}
}
};
var json = {
'array': [{'field1':'v1', 'field2':'v2'}, 2, 3],
'boolean': true,
'null': null,
'number': 123,
'object': {'a': 'b', 'c': 'd'},
'string': 'Hello World'
};
var editor = new JSONEditor(container, options, json);
</script>
</body>
</html>

View File

@ -0,0 +1,92 @@
<!DOCTYPE HTML>
<html>
<head>
<title>JSONEditor | Auto Complete</title>
<link href="../dist/jsoneditor.css" rel="stylesheet" type="text/css">
<script src="../dist/jsoneditor.js"></script>
<style type="text/css">
#jsoneditor {
width: 500px;
height: 500px;
}
p {
width: 500px;
font-family: "DejaVu Sans", sans-serif;
}
</style>
<!--<link href="./css/darktheme.css" rel="stylesheet" type="text/css">-->
</head>
<body>
<p>
This example demonstrates how to autocomplete works, options available are dynamics and consist in all the strings found in the json
</p>
<div id="jsoneditor"></div>
<script>
// create the editor
var container = document.getElementById('jsoneditor');
var options = {
modes: ['text', 'tree'],
autocomplete: {
ApplyTo:['value'],
GetOptions: function (autocomplete, node, text) {
// Return all strings in json
function stringify(o, prefix) {
prefix = prefix || '';
switch (typeof o) {
case 'object':
var output = "";
if (Array.isArray(o)) {
if ((prefix != "") && (prefix != text)) output += prefix + '\n';
o.forEach(function (e, index) {
output += stringify(e, null);
}.bind(this));
return output;
}
output = "";
for (var k in o) {
if (o.hasOwnProperty(k)) {
if (prefix == "") output += stringify(o[k], k);
}
}
if ((prefix != "") && (prefix != text)) output += prefix + '\n';
return output;
case 'function':
return "";
default:
var output = "";
if ((prefix != "") && (prefix != text)) output += prefix + '\n';
if ((o != "") && (o != text)) output += o + '\n';
return output;
}
}
var data = node.editor.get();
var optionsStr = stringify(data, null);
var options = optionsStr.split("\n");
return options;
}
}
};
var json = {
'array': [{'field1':'v1', 'field2':'v2'}, 2, 3],
'boolean': true,
'null': null,
'number': 123,
'object': {'a': 'b', 'c': 'd'},
'string': 'Hello World'
};
var editor = new JSONEditor(container, options, json);
</script>
</body>
</html>

View File

@ -0,0 +1,56 @@
<!DOCTYPE HTML>
<html>
<head>
<title>JSONEditor | Auto Complete</title>
<link href="../dist/jsoneditor.css" rel="stylesheet" type="text/css">
<script src="../dist/jsoneditor.js"></script>
<style type="text/css">
#jsoneditor {
width: 500px;
height: 500px;
}
p {
width: 500px;
font-family: "DejaVu Sans", sans-serif;
}
</style>
<!--<link href="./css/darktheme.css" rel="stylesheet" type="text/css">-->
</head>
<body>
<p>
This example demonstrates how to autocomplete works, options available are "apple","cranberry","raspberry","pie"
</p>
<div id="jsoneditor"></div>
<script>
// create the editor
var container = document.getElementById('jsoneditor');
var options = {
modes: ['text', 'tree'],
autocomplete: {
ApplyTo:['name','value'], // This indicates the autocomplete is going to be applied to json properties names and values.
GetOptions: function () {
return ["apple","cranberry","raspberry","pie"];
}
}
};
var json = {
'array': [{'field1':'v1', 'field2':'v2'}, 2, 3],
'boolean': true,
'null': null,
'number': 123,
'object': {'a': 'b', 'c': 'd'},
'string': 'Hello World'
};
var editor = new JSONEditor(container, options, json);
</script>
</body>
</html>

View File

@ -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'
];

414
src/js/autocomplete.js Normal file
View File

@ -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 + '<b>' + array[i].substring(token.length) + '</b>';
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, '&amp;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
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;

View File

@ -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();