UNPKG

jsoneditor

Version:

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

1,124 lines (980 loc) 31.6 kB
'use strict' import { jsonrepair } from 'jsonrepair' import ace from './ace' import { DEFAULT_MODAL_ANCHOR } from './constants' import { ErrorTable } from './ErrorTable' import { FocusTracker } from './FocusTracker' import { setLanguage, setLanguages, translate } from './i18n' import { createQuery, executeQuery } from './jmespathQuery' import { ModeSwitcher } from './ModeSwitcher' import { showSortModal } from './showSortModal' import { showTransformModal } from './showTransformModal' import { tryRequireThemeJsonEditor } from './tryRequireThemeJsonEditor' import { SchemaTextCompleter } from './SchemaTextCompleter' import { addClassName, debounce, escapeUnicodeChars, getIndexForPosition, getInputSelection, getPositionForPath, improveSchemaError, isObject, isValidationErrorChanged, parse, sort, sortObjectKeys } from './util' import { validateCustom } from './validationUtils' // create a mixin with the functions for text mode const textmode = {} const DEFAULT_THEME = 'ace/theme/jsoneditor' /** * Create a text editor * @param {Element} container * @param {Object} [options] Object with options. See docs for details. * @private */ textmode.create = function (container, options = {}) { if (typeof options.statusBar === 'undefined') { options.statusBar = true } // setting default for textmode options.mainMenuBar = options.mainMenuBar !== false options.enableSort = options.enableSort !== false options.enableTransform = options.enableTransform !== false options.createQuery = options.createQuery || createQuery options.executeQuery = options.executeQuery || executeQuery options.showErrorTable = options.showErrorTable !== undefined ? options.showErrorTable : ['text', 'preview'] this.options = options // indentation if (typeof options.indentation === 'number') { this.indentation = Number(options.indentation) } else { this.indentation = 2 // number of spaces } // language setLanguages(this.options.languages) setLanguage(this.options.language) // grab ace from options if provided const _ace = options.ace ? options.ace : ace // TODO: make the option options.ace deprecated, it's not needed anymore (see #309) // determine mode this.mode = (options.mode === 'code') ? 'code' : 'text' if (this.mode === 'code') { // verify whether Ace editor is available and supported if (typeof _ace === 'undefined') { this.mode = 'text' console.warn('Failed to load Ace editor, falling back to plain text mode. Please use a JSONEditor bundle including Ace, or pass Ace as via the configuration option `ace`.') } } // determine theme this.theme = options.theme || DEFAULT_THEME if (this.theme === DEFAULT_THEME && _ace) { tryRequireThemeJsonEditor() } if (options.onTextSelectionChange) { this.onTextSelectionChange(options.onTextSelectionChange) } const me = this this.container = container this.dom = {} this.aceEditor = undefined // ace code editor this.textarea = undefined // plain text editor (fallback when Ace is not available) this.validateSchema = null this.annotations = [] this.lastSchemaErrors = undefined // create a debounced validate function this._debouncedValidate = debounce(this._validateAndCatch.bind(this), this.DEBOUNCE_INTERVAL) this.width = container.clientWidth this.height = container.clientHeight this.frame = document.createElement('div') this.frame.className = 'jsoneditor jsoneditor-mode-' + this.options.mode this.frame.onclick = event => { // prevent default submit action when the editor is located inside a form event.preventDefault() } this.frame.onkeydown = event => { me._onKeyDown(event) } // setting the FocusTracker on 'this.frame' to track the editor's focus event const focusTrackerConfig = { target: this.frame, onFocus: this.options.onFocus || null, onBlur: this.options.onBlur || null } this.frameFocusTracker = new FocusTracker(focusTrackerConfig) this.content = document.createElement('div') this.content.className = 'jsoneditor-outer' if (this.options.mainMenuBar) { addClassName(this.content, 'has-main-menu-bar') // create menu this.menu = document.createElement('div') this.menu.className = 'jsoneditor-menu' this.frame.appendChild(this.menu) // create format button const buttonFormat = document.createElement('button') buttonFormat.type = 'button' buttonFormat.className = 'jsoneditor-format' buttonFormat.title = translate('formatTitle') this.menu.appendChild(buttonFormat) buttonFormat.onclick = () => { try { me.format() me._onChange() } catch (err) { me._onError(err) } } // create compact button const buttonCompact = document.createElement('button') buttonCompact.type = 'button' buttonCompact.className = 'jsoneditor-compact' buttonCompact.title = translate('compactTitle') this.menu.appendChild(buttonCompact) buttonCompact.onclick = () => { try { me.compact() me._onChange() } catch (err) { me._onError(err) } } // create sort button if (this.options.enableSort) { const sort = document.createElement('button') sort.type = 'button' sort.className = 'jsoneditor-sort' sort.title = translate('sortTitleShort') sort.onclick = () => { me._showSortModal() } this.menu.appendChild(sort) } // create transform button if (this.options.enableTransform) { const transform = document.createElement('button') transform.type = 'button' transform.title = translate('transformTitleShort') transform.className = 'jsoneditor-transform' transform.onclick = () => { me._showTransformModal() } this.menu.appendChild(transform) } // create repair button const buttonRepair = document.createElement('button') buttonRepair.type = 'button' buttonRepair.className = 'jsoneditor-repair' buttonRepair.title = translate('repairTitle') this.menu.appendChild(buttonRepair) buttonRepair.onclick = () => { try { me.repair() me._onChange() } catch (err) { me._onError(err) } } // create undo/redo buttons if (this.mode === 'code') { // create undo button const undo = document.createElement('button') undo.type = 'button' undo.className = 'jsoneditor-undo jsoneditor-separator' undo.title = translate('undo') undo.onclick = () => { this.aceEditor.getSession().getUndoManager().undo() } this.menu.appendChild(undo) this.dom.undo = undo // create redo button const redo = document.createElement('button') redo.type = 'button' redo.className = 'jsoneditor-redo' redo.title = translate('redo') redo.onclick = () => { this.aceEditor.getSession().getUndoManager().redo() } this.menu.appendChild(redo) this.dom.redo = redo } // create mode box if (this.options && this.options.modes && this.options.modes.length) { this.modeSwitcher = new ModeSwitcher(this.menu, this.options.modes, this.options.mode, function onSwitch (mode) { // switch mode and restore focus try { me.setMode(mode) me.modeSwitcher.focus() } catch (err) { me._onError(err) } }) } if (this.mode === 'code') { const poweredBy = document.createElement('a') poweredBy.appendChild(document.createTextNode('powered by ace')) poweredBy.href = 'https://ace.c9.io/' poweredBy.target = '_blank' poweredBy.className = 'jsoneditor-poweredBy' poweredBy.onclick = () => { // 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, 'noreferrer') } this.menu.appendChild(poweredBy) } } const emptyNode = {} const isReadOnly = (this.options.onEditable && typeof (this.options.onEditable === 'function') && !this.options.onEditable(emptyNode)) 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) const aceEditor = _ace.edit(this.editorDom) const aceSession = aceEditor.getSession() aceEditor.$blockScrolling = Infinity aceEditor.setTheme(this.theme) aceEditor.setOptions({ readOnly: isReadOnly }) aceEditor.setShowPrintMargin(false) aceEditor.setFontSize('14px') aceSession.setMode('ace/mode/json') aceSession.setTabSize(this.indentation) aceSession.setUseSoftTabs(true) aceSession.setUseWrapMode(true) // replace ace setAnnotations with custom function that also covers jsoneditor annotations const originalSetAnnotations = aceSession.setAnnotations aceSession.setAnnotations = function (annotations) { originalSetAnnotations.call(this, annotations && annotations.length ? annotations : me.annotations) } // disable Ctrl+L quickkey of Ace (is used by the browser to select the address bar) aceEditor.commands.bindKey('Ctrl-L', null) aceEditor.commands.bindKey('Command-L', null) // disable the quickkeys we want to use for Format and Compact aceEditor.commands.bindKey('Ctrl-\\', null) aceEditor.commands.bindKey('Command-\\', null) aceEditor.commands.bindKey('Ctrl-Shift-\\', null) aceEditor.commands.bindKey('Command-Shift-\\', null) this.aceEditor = aceEditor // register onchange event aceEditor.on('change', this._onChange.bind(this)) aceEditor.on('changeSelection', this._onSelect.bind(this)) } else { // load a plain text textarea const textarea = document.createElement('textarea') textarea.className = 'jsoneditor-text' textarea.spellcheck = false this.content.appendChild(textarea) this.textarea = textarea this.textarea.readOnly = isReadOnly // register onchange event if (this.textarea.oninput === null) { this.textarea.oninput = this._onChange.bind(this) } else { // oninput is undefined. For IE8- this.textarea.onchange = this._onChange.bind(this) } textarea.onselect = this._onSelect.bind(this) textarea.onmousedown = this._onMouseDown.bind(this) textarea.onblur = this._onBlur.bind(this) } this._updateHistoryButtons() const errorTableVisible = Array.isArray(this.options.showErrorTable) ? this.options.showErrorTable.includes(this.mode) : this.options.showErrorTable === true this.errorTable = new ErrorTable({ errorTableVisible, onToggleVisibility: function () { me._validateAndCatch() }, onFocusLine: function (line) { me.isFocused = true if (!isNaN(line)) { me.setTextSelection({ row: line, column: 1 }, { row: line, column: 1000 }) } }, onChangeHeight: function (height) { // TODO: change CSS to using flex box, remove setting height using JavaScript const statusBarHeight = me.dom.statusBar ? me.dom.statusBar.clientHeight : 0 const totalHeight = height + statusBarHeight + 1 me.content.style.marginBottom = (-totalHeight) + 'px' me.content.style.paddingBottom = totalHeight + 'px' } }) this.frame.appendChild(this.errorTable.getErrorTable()) if (options.statusBar) { addClassName(this.content, 'has-status-bar') this.curserInfoElements = {} const statusBar = document.createElement('div') this.dom.statusBar = statusBar statusBar.className = 'jsoneditor-statusbar' this.frame.appendChild(statusBar) const lnLabel = document.createElement('span') lnLabel.className = 'jsoneditor-curserinfo-label' lnLabel.innerText = 'Ln:' const lnVal = document.createElement('span') lnVal.className = 'jsoneditor-curserinfo-val' lnVal.innerText = '1' statusBar.appendChild(lnLabel) statusBar.appendChild(lnVal) const colLabel = document.createElement('span') colLabel.className = 'jsoneditor-curserinfo-label' colLabel.innerText = 'Col:' const colVal = document.createElement('span') colVal.className = 'jsoneditor-curserinfo-val' colVal.innerText = '1' statusBar.appendChild(colLabel) statusBar.appendChild(colVal) this.curserInfoElements.colVal = colVal this.curserInfoElements.lnVal = lnVal const countLabel = document.createElement('span') countLabel.className = 'jsoneditor-curserinfo-label' countLabel.innerText = 'characters selected' countLabel.style.display = 'none' const countVal = document.createElement('span') countVal.className = 'jsoneditor-curserinfo-count' countVal.innerText = '0' countVal.style.display = 'none' this.curserInfoElements.countLabel = countLabel this.curserInfoElements.countVal = countVal statusBar.appendChild(countVal) statusBar.appendChild(countLabel) statusBar.appendChild(this.errorTable.getErrorCounter()) statusBar.appendChild(this.errorTable.getWarningIcon()) statusBar.appendChild(this.errorTable.getErrorIcon()) } this.setSchema(this.options.schema, this.options.schemaRefs) } textmode._onSchemaChange = function (schema, schemaRefs) { if (!this.aceEditor) { return } if (this.options.allowSchemaSuggestions && schema) { this.aceEditor.setOption('enableBasicAutocompletion', [new SchemaTextCompleter(schema, schemaRefs)]) this.aceEditor.setOption('enableLiveAutocompletion', true) } else { this.aceEditor.setOption('enableBasicAutocompletion', undefined) this.aceEditor.setOption('enableLiveAutocompletion', false) } } /** * Handle a change: * - Validate JSON schema * - Send a callback to the onChange listener if provided * @private */ textmode._onChange = function () { if (this.onChangeDisabled) { return } // enable/disable undo/redo buttons setTimeout(() => { if (this._updateHistoryButtons) { this._updateHistoryButtons() } }) // validate JSON schema (if configured) this._debouncedValidate() // trigger the onChange callback if (this.options.onChange) { try { this.options.onChange() } catch (err) { console.error('Error in onChange callback: ', err) } } // trigger the onChangeText callback if (this.options.onChangeText) { try { this.options.onChangeText(this.getText()) } catch (err) { console.error('Error in onChangeText callback: ', err) } } } textmode._updateHistoryButtons = function () { if (this.aceEditor && this.dom.undo && this.dom.redo) { const undoManager = this.aceEditor.getSession().getUndoManager() if (undoManager && undoManager.hasUndo && undoManager.hasRedo) { this.dom.undo.disabled = !undoManager.hasUndo() this.dom.redo.disabled = !undoManager.hasRedo() } } } /** * Open a sort modal * @private */ textmode._showSortModal = function () { try { const me = this const container = this.options.modalAnchor || DEFAULT_MODAL_ANCHOR const json = this.get() function onSort (sortedBy) { if (Array.isArray(json)) { const sortedJson = sort(json, sortedBy.path, sortedBy.direction) me.sortedBy = sortedBy me.update(sortedJson) } if (isObject(json)) { const sortedJson = sortObjectKeys(json, sortedBy.direction) me.sortedBy = sortedBy me.update(sortedJson) } } showSortModal(container, json, onSort, me.sortedBy) } catch (err) { this._onError(err) } } /** * Open a transform modal * @private */ textmode._showTransformModal = function () { try { const { modalAnchor, createQuery, executeQuery, queryDescription } = this.options const json = this.get() showTransformModal({ container: modalAnchor || DEFAULT_MODAL_ANCHOR, json, queryDescription, // can be undefined createQuery, executeQuery, onTransform: query => { const updatedJson = executeQuery(json, query) this.update(updatedJson) } }) } catch (err) { this._onError(err) } } /** * Handle text selection * Calculates the cursor position and selection range and updates menu * @private */ textmode._onSelect = function () { this._updateCursorInfo() this._emitSelectionChange() } /** * Event handler for keydown. Handles shortcut keys * @param {Event} event * @private */ textmode._onKeyDown = function (event) { const keynum = event.which || event.keyCode let handled = false if (keynum === 73 && event.ctrlKey) { if (event.shiftKey) { // Ctrl+Shift+I this.compact() this._onChange() } else { // Ctrl+I this.format() this._onChange() } handled = true } if (handled) { event.preventDefault() event.stopPropagation() } this._updateCursorInfo() this._emitSelectionChange() } /** * Event handler for mousedown. * @private */ textmode._onMouseDown = function () { this._updateCursorInfo() this._emitSelectionChange() } /** * Event handler for blur. * @private */ textmode._onBlur = function () { const me = this // this allows to avoid blur when clicking inner elements (like the errors panel) // just make sure to set the isFocused to true on the inner element onclick callback setTimeout(() => { if (!me.isFocused) { me._updateCursorInfo() me._emitSelectionChange() } me.isFocused = false }) } /** * Update the cursor info and the status bar, if presented */ textmode._updateCursorInfo = function () { const me = this let line, col, count if (this.textarea) { setTimeout(() => { // this to verify we get the most updated textarea cursor selection const selectionRange = getInputSelection(me.textarea) if (selectionRange.startIndex !== selectionRange.endIndex) { count = selectionRange.endIndex - selectionRange.startIndex } if (count && me.cursorInfo && me.cursorInfo.line === selectionRange.end.row && me.cursorInfo.column === selectionRange.end.column) { line = selectionRange.start.row col = selectionRange.start.column } else { line = selectionRange.end.row col = selectionRange.end.column } me.cursorInfo = { line, column: col, count } if (me.options.statusBar) { updateDisplay() } }, 0) } else if (this.aceEditor && this.curserInfoElements) { const curserPos = this.aceEditor.getCursorPosition() const selectedText = this.aceEditor.getSelectedText() line = curserPos.row + 1 col = curserPos.column + 1 count = selectedText.length me.cursorInfo = { line, column: col, count } if (this.options.statusBar) { updateDisplay() } } function updateDisplay () { if (me.curserInfoElements.countVal.innerText !== count) { me.curserInfoElements.countVal.innerText = count me.curserInfoElements.countVal.style.display = count ? 'inline' : 'none' me.curserInfoElements.countLabel.style.display = count ? 'inline' : 'none' } me.curserInfoElements.lnVal.innerText = line me.curserInfoElements.colVal.innerText = col } } /** * emits selection change callback, if given * @private */ textmode._emitSelectionChange = function () { if (this._selectionChangedHandler) { const currentSelection = this.getTextSelection() this._selectionChangedHandler(currentSelection.start, currentSelection.end, currentSelection.text) } } /** * refresh ERROR annotations state * error annotations are handled by the ace json mode (ace/mode/json) * validation annotations are handled by this mode * therefore in order to refresh we send only the annotations of error type in order to maintain its state * @private */ textmode._refreshAnnotations = function () { const session = this.aceEditor && this.aceEditor.getSession() if (session) { const errEnnotations = session.getAnnotations().filter(annotation => annotation.type === 'error') session.setAnnotations(errEnnotations) } } /** * Destroy the editor. Clean up DOM, event listeners, and web workers. */ textmode.destroy = function () { // remove old ace editor if (this.aceEditor) { this.aceEditor.destroy() this.aceEditor = null } if (this.frame && this.container && this.frame.parentNode === this.container) { this.container.removeChild(this.frame) } if (this.modeSwitcher) { this.modeSwitcher.destroy() this.modeSwitcher = null } this.textarea = null this._debouncedValidate = null // Removing the FocusTracker set to track the editor's focus event this.frameFocusTracker.destroy() } /** * Compact the code in the text editor */ textmode.compact = function () { const json = this.get() const text = JSON.stringify(json) this.updateText(text) } /** * Format the code in the text editor */ textmode.format = function () { const json = this.get() const text = JSON.stringify(json, null, this.indentation) this.updateText(text) } /** * Repair the code in the text editor */ textmode.repair = function () { const text = this.getText() try { const repairedText = jsonrepair(text) this.updateText(repairedText) } catch (err) { // repair was not successful, do nothing } } /** * Set focus to the formatter */ textmode.focus = function () { if (this.textarea) { this.textarea.focus() } if (this.aceEditor) { this.aceEditor.focus() } } /** * Resize the formatter */ textmode.resize = function () { if (this.aceEditor) { const force = false this.aceEditor.resize(force) } } /** * Set json data in the formatter * @param {*} json */ textmode.set = function (json) { this.setText(JSON.stringify(json, null, this.indentation)) } /** * Update data. Same as calling `set` in text/code mode. * @param {*} json */ textmode.update = function (json) { this.updateText(JSON.stringify(json, null, this.indentation)) } /** * Get json data from the formatter * @return {*} json */ textmode.get = function () { const text = this.getText() return parse(text) // this can throw an error } /** * Get the text contents of the editor * @return {String} jsonText */ textmode.getText = function () { if (this.textarea) { return this.textarea.value } if (this.aceEditor) { return this.aceEditor.getValue() } return '' } /** * Set the text contents of the editor and optionally clear the history * @param {String} jsonText * @param {boolean} clearHistory Only applicable for mode 'code' * @private */ textmode._setText = function (jsonText, clearHistory) { const text = (this.options.escapeUnicode === true) ? escapeUnicodeChars(jsonText) : jsonText if (this.textarea) { this.textarea.value = text } if (this.aceEditor) { // prevent emitting onChange events while setting new text this.onChangeDisabled = true this.aceEditor.setValue(text, -1) this.onChangeDisabled = false if (clearHistory) { // prevent initial undo action clearing the initial contents const me = this setTimeout(() => { if (me.aceEditor) { me.aceEditor.session.getUndoManager().reset() } }) } setTimeout(() => { if (this._updateHistoryButtons) { this._updateHistoryButtons() } }) } // validate JSON schema this._debouncedValidate() } /** * Set the text contents of the editor * @param {String} jsonText */ textmode.setText = function (jsonText) { this._setText(jsonText, true) } /** * Update the text contents * @param {string} jsonText */ textmode.updateText = function (jsonText) { // don't update if there are no changes if (this.getText() === jsonText) { return } this._setText(jsonText, false) } /** * Validate current JSON object against the configured JSON schema * Throws an exception when no JSON schema is configured */ textmode.validate = function () { let schemaErrors = [] let parseErrors = [] let json try { json = this.get() // this can fail when there is no valid json // execute JSON schema validation (ajv) if (this.validateSchema) { const valid = this.validateSchema(json) if (!valid) { schemaErrors = this.validateSchema.errors.map(error => { error.type = 'validation' return improveSchemaError(error) }) } } // execute custom validation and after than merge and render all errors // TODO: implement a better mechanism for only using the last validation action this.validationSequence = (this.validationSequence || 0) + 1 const me = this const seq = this.validationSequence return validateCustom(json, this.options.onValidate) .then(customValidationErrors => { // only apply when there was no other validation started whilst resolving async results if (seq === me.validationSequence) { const errors = schemaErrors.concat(parseErrors).concat(customValidationErrors) me._renderErrors(errors) if ( typeof this.options.onValidationError === 'function' && isValidationErrorChanged(errors, this.lastSchemaErrors) ) { this.options.onValidationError.call(this, errors) } this.lastSchemaErrors = errors } return this.lastSchemaErrors }) } catch (err) { if (this.getText()) { // try to extract the line number from the jsonlint error message const match = /\w*line\s*(\d+)\w*/g.exec(err.message) let line if (match) { line = +match[1] } parseErrors = [{ type: 'error', message: err.message.replace(/\n/g, '<br>'), line }] } this._renderErrors(parseErrors) if ( typeof this.options.onValidationError === 'function' && isValidationErrorChanged(parseErrors, this.lastSchemaErrors) ) { this.options.onValidationError.call(this, parseErrors) } this.lastSchemaErrors = parseErrors return Promise.resolve(this.lastSchemaErrors) } } textmode._validateAndCatch = function () { this.validate().catch(err => { console.error('Error running validation:', err) }) } textmode._renderErrors = function (errors) { const jsonText = this.getText() const errorPaths = [] errors.reduce((acc, curr) => { if (typeof curr.dataPath === 'string' && acc.indexOf(curr.dataPath) === -1) { acc.push(curr.dataPath) } return acc }, errorPaths) const errorLocations = getPositionForPath(jsonText, errorPaths) // render annotations in Ace Editor (if any) if (this.aceEditor) { this.annotations = errorLocations.map(errLoc => { const validationErrors = errors.filter(err => err.dataPath === errLoc.path) const message = validationErrors.map(err => err.message).join('\n') if (message) { return { row: errLoc.line, column: errLoc.column, text: 'Schema validation error' + (validationErrors.length !== 1 ? 's' : '') + ': \n' + message, type: 'warning', source: 'jsoneditor' } } return {} }) this._refreshAnnotations() } // render errors in the errors table (if any) this.errorTable.setErrors(errors, errorLocations) // update the height of the ace editor if (this.aceEditor) { const force = false this.aceEditor.resize(force) } } /** * Get the selection details * @returns {{start:{row:Number, column:Number},end:{row:Number, column:Number},text:String}} */ textmode.getTextSelection = function () { let selection = {} if (this.textarea) { const selectionRange = getInputSelection(this.textarea) if (this.cursorInfo && this.cursorInfo.line === selectionRange.end.row && this.cursorInfo.column === selectionRange.end.column) { // selection direction is bottom => up selection.start = selectionRange.end selection.end = selectionRange.start } else { selection = selectionRange } return { start: selection.start, end: selection.end, text: this.textarea.value.substring(selectionRange.startIndex, selectionRange.endIndex) } } if (this.aceEditor) { const aceSelection = this.aceEditor.getSelection() const selectedText = this.aceEditor.getSelectedText() const range = aceSelection.getRange() const lead = aceSelection.getSelectionLead() if (lead.row === range.end.row && lead.column === range.end.column) { selection = range } else { // selection direction is bottom => up selection.start = range.end selection.end = range.start } return { start: { row: selection.start.row + 1, column: selection.start.column + 1 }, end: { row: selection.end.row + 1, column: selection.end.column + 1 }, text: selectedText } } } /** * Callback registration for selection change * @param {selectionCallback} callback * * @callback selectionCallback */ textmode.onTextSelectionChange = function (callback) { if (typeof callback === 'function') { this._selectionChangedHandler = debounce(callback, this.DEBOUNCE_INTERVAL) } } /** * Set selection on editor's text * @param {{row:Number, column:Number}} startPos selection start position * @param {{row:Number, column:Number}} endPos selected end position */ textmode.setTextSelection = function (startPos, endPos) { if (!startPos || !endPos) return if (this.textarea) { const startIndex = getIndexForPosition(this.textarea, startPos.row, startPos.column) const endIndex = getIndexForPosition(this.textarea, endPos.row, endPos.column) if (startIndex > -1 && endIndex > -1) { if (this.textarea.setSelectionRange) { this.textarea.focus() this.textarea.setSelectionRange(startIndex, endIndex) } else if (this.textarea.createTextRange) { // IE < 9 const range = this.textarea.createTextRange() range.collapse(true) range.moveEnd('character', endIndex) range.moveStart('character', startIndex) range.select() } const rows = (this.textarea.value.match(/\n/g) || []).length + 1 const lineHeight = this.textarea.scrollHeight / rows const selectionScrollPos = (startPos.row * lineHeight) this.textarea.scrollTop = selectionScrollPos > this.textarea.clientHeight ? (selectionScrollPos - (this.textarea.clientHeight / 2)) : 0 } } else if (this.aceEditor) { const range = { start: { row: startPos.row - 1, column: startPos.column - 1 }, end: { row: endPos.row - 1, column: endPos.column - 1 } } this.aceEditor.selection.setRange(range) this.aceEditor.scrollToLine(startPos.row - 1, true) } } function load () { try { this.format() } catch (err) { // in case of an error, just move on, failing formatting is not a big deal } } // define modes export const textModeMixins = [ { mode: 'text', mixin: textmode, data: 'text', load }, { mode: 'code', mixin: textmode, data: 'text', load } ]