UNPKG

jsoneditor

Version:

A web-based tool to view, edit, format, and validate JSON

380 lines (334 loc) 14.1 kB
'use strict' const defaultFilterFunction = { start: function (token, match, config) { return match.indexOf(token) === 0 }, contain: function (token, match, config) { return match.indexOf(token) > -1 } } export function autocomplete (config) { config = config || {} config.filter = config.filter || 'start' config.trigger = config.trigger || 'keydown' config.confirmKeys = config.confirmKeys || [39, 35, 9] // right, end, tab config.caseSensitive = config.caseSensitive || false // autocomplete case sensitive let fontSize = '' let fontFamily = '' const wrapper = document.createElement('div') wrapper.style.position = 'relative' wrapper.style.outline = '0' wrapper.style.border = '0' wrapper.style.margin = '0' wrapper.style.padding = '0' const dropDown = document.createElement('div') dropDown.className = 'autocomplete dropdown' dropDown.style.position = 'absolute' dropDown.style.visibility = 'hidden' let spacer let leftSide // <-- it will contain the leftSide part of the textfield (the bit that was already autocompleted) const createDropDownController = (elem, rs) => { let rows = [] let ix = 0 let oldIndex = -1 // TODO: move this styling in JS to SCSS const onMouseOver = function () { this.style.backgroundColor = '#ddd' } const onMouseOut = function () { this.style.backgroundColor = '' } const onMouseDown = function () { p.hide(); p.onmouseselection(this.__hint, p.rs) } const p = { rs, hide: function () { elem.style.visibility = 'hidden' // rs.hideDropDown(); }, refresh: function (token, array) { elem.style.visibility = 'hidden' ix = 0 elem.textContent = '' const vph = (window.innerHeight || document.documentElement.clientHeight) const rect = elem.parentNode.getBoundingClientRect() const distanceToTop = rect.top - 6 // heuristic give 6px const distanceToBottom = vph - rect.bottom - 6 // distance from the browser border. rows = [] const filterFn = typeof config.filter === 'function' ? config.filter : defaultFilterFunction[config.filter] const filtered = !filterFn ? [] : array.filter(match => filterFn(config.caseSensitive ? token : token.toLowerCase(), config.caseSensitive ? match : match.toLowerCase(), config)) rows = filtered.map(row => { const divRow = document.createElement('div') divRow.className = 'item' // divRow.style.color = config.color; divRow.onmouseover = onMouseOver divRow.onmouseout = onMouseOut divRow.onmousedown = onMouseDown divRow.__hint = row divRow.textContent = '' divRow.appendChild(document.createTextNode(row.substring(0, token.length))) const b = document.createElement('b') b.appendChild(document.createTextNode(row.substring(token.length))) divRow.appendChild(b) elem.appendChild(divRow) return divRow }) if (rows.length === 0) { return // nothing to show. } if (rows.length === 1 && ((token.toLowerCase() === rows[0].__hint.toLowerCase() && !config.caseSensitive) || (token === rows[0].__hint && config.caseSensitive))) { 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].className = 'item' } rows[index].className = 'item hover' 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) { let 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 = fontSize spacer.style.fontFamily = fontFamily spacer.style.fontWeight = 'normal' document.body.appendChild(spacer) } spacer.textContent = text return spacer.getBoundingClientRect().right } const 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, // Only to allow easy access to the HTML elements to the final user (possibly for minor customizations) show: function (element, startPos, options) { this.startFrom = startPos this.wrapper.remove() if (this.elementHint) { this.elementHint.remove() this.elementHint = null } if (fontSize === '') { fontSize = window.getComputedStyle(element).getPropertyValue('font-size') } if (fontFamily === '') { fontFamily = window.getComputedStyle(element).getPropertyValue('font-family') } dropDown.style.marginLeft = '0' dropDown.style.marginTop = element.getBoundingClientRect().height + 'px' this.options = options.map(String) 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.className = 'autocomplete hint' this.elementHint.style.zIndex = 2 this.elementHint.style.position = 'absolute' this.elementHint.onfocus = () => { this.element.focus() } 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) { let text = element.innerText text = text.replace('\n', '') const optionsLength = this.options.length // breaking text in leftSide and token. const token = text.substring(this.startFrom) leftSide = text.substring(0, this.startFrom) for (let i = 0; i < optionsLength; i++) { const opt = this.options[i] if ((!config.caseSensitive && opt.toLowerCase().indexOf(token.toLowerCase()) === 0) || (config.caseSensitive && opt.indexOf(token) === 0)) { // <-- how about upperCase vs. lowercase this.elementHint.innerText = leftSide + token + opt.substring(token.length) this.elementHint.realInnerText = 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' const wasDropDownHidden = (dropDown.style.visibility === 'hidden') if (!wasDropDownHidden) { this.elementHint.style.width = calculateWidthForText(this.elementHint.innerText) + dropDown.clientWidth + 'px' } } } const dropDownController = createDropDownController(dropDown, rs) const keyDownHandler = function (e) { // console.log("Keydown:" + e.keyCode); e = e || window.event const 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 } let text = this.element.innerText text = text.replace('\n', '') 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.realInnerText) { this.element.innerText = this.elementHint.realInnerText 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 { const wasDropDownHidden = (dropDown.style.visibility === 'hidden') dropDownController.hide() if (wasDropDownHidden) { rs.hideDropDown() rs.element.focus() rs.onEnter() return } this.element.innerText = this.elementHint.realInnerText rs.hideDropDown() setEndOfContenteditable(this.element) e.preventDefault() e.stopPropagation() } return } if (keyCode === 40) { // down const token = text.substring(this.startFrom) const m = dropDownController.move(+1) if (m === '') { rs.onArrowDown() } this.elementHint.innerText = leftSide + token + m.substring(token.length) this.elementHint.realInnerText = leftSide + m e.preventDefault() e.stopPropagation() return } if (keyCode === 38) { // up const token = text.substring(this.startFrom) const m = dropDownController.move(-1) if (m === '') { rs.onArrowUp() } this.elementHint.innerText = leftSide + token + m.substring(token.length) this.elementHint.realInnerText = leftSide + m e.preventDefault() e.stopPropagation() } }.bind(rs) const onBlurHandler = e => { rs.hideDropDown() // console.log("Lost focus."); } dropDownController.onmouseselection = (text, rs) => { rs.element.innerText = rs.elementHint.innerText = leftSide + text rs.hideDropDown() window.setTimeout(() => { rs.element.focus() setEndOfContenteditable(rs.element) }, 1) } return rs }