Implemented mode `code` (using ace editor)

This commit is contained in:
jos 2016-10-14 12:28:33 +02:00
parent a607d2c2f4
commit 5ff41f2d9d
14 changed files with 459 additions and 66 deletions

View File

@ -0,0 +1,61 @@
<!DOCTYPE HTML>
<html>
<head>
<title>JSONEditor | Custom Ace Editor</title>
<!-- we use the minimalist jsoneditor, which doesn't have Ace Editor included -->
<script src="../dist/jsoneditor-minimalist.js"></script>
<!-- load your own instance of Ace Editor and all plugins that you need -->
<!-- jsoneditor requires ext-searchbox and mode-json -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.5/ace.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.5/ext-searchbox.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.5/mode-json.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ace/1.2.5/theme-twilight.js"></script>
<style type="text/css">
#jsoneditor {
width: 500px;
height: 500px;
}
</style>
</head>
<body>
<p>
In this example, the we use the minimalist version of jsoneditor and load
and configure Ace editor our selves.
</p>
<div id="jsoneditor"></div>
<script>
// create the editor, set mode to 'code' (powered by ace editor)
var container = document.getElementById('jsoneditor')
var options = {
mode: 'code',
onLoadAce: function (aceEditor, container, options) {
// we can adjust configuration of the ace editor,
// or create a completely new instance of ace editor.
// let's set a custom theme and font size
aceEditor.setTheme('ace/theme/twilight')
aceEditor.setFontSize(16)
return aceEditor
}
}
var editor = jsoneditor(container, options)
var json = {
'array': [1, 2, 3],
'boolean': true,
'null': null,
'number': 123,
'object': {'a': 'b', 'c': 'd'},
'string': 'Hello World'
}
editor.set(json)
</script>
</body>
</html>

View File

@ -81,8 +81,8 @@ var compilerMinimalist = webpack({
},
plugins: [
bannerPlugin,
new webpack.NormalModuleReplacementPlugin(new RegExp('^brace$'), EMPTY),
new webpack.NormalModuleReplacementPlugin(new RegExp('^ajv'), EMPTY),
new webpack.NormalModuleReplacementPlugin(new RegExp('^./assets/ace$'), EMPTY),
new webpack.NormalModuleReplacementPlugin(new RegExp('^ajv$'), EMPTY),
new webpack.optimize.UglifyJsPlugin()
],
module: {
@ -165,4 +165,4 @@ gulp.task(WATCH, ['bundle'], function() {
})
// The default task (called when you run `gulp`)
gulp.task('default', [ 'bundle', 'bundle-minimalist' ])
gulp.task('default', [ 'bundle', 'bundle-minimalist', 'copy' ])

View File

@ -23,18 +23,18 @@
"test": "ava test/*.test.js test/**/*.test.js --verbose"
},
"dependencies": {
"ajv": "4.7.5",
"ajv": "4.7.7",
"brace": "0.8.0",
"javascript-natural-sort": "0.7.1",
"lodash": "4.16.2",
"preact": "6.1.0"
"lodash": "4.16.4",
"preact": "6.3.0"
},
"devDependencies": {
"ava": "0.16.0",
"babel-core": "6.16.0",
"babel-core": "6.17.0",
"babel-loader": "6.2.5",
"babel-preset-stage-2": "6.16.0",
"babel-preset-stage-3": "6.16.0",
"babel-preset-stage-2": "6.17.0",
"babel-preset-stage-3": "6.17.0",
"browser-sync": "2.17.3",
"css-loader": "0.25.0",
"gulp": "3.9.1",

127
src/CodeMode.js Normal file
View File

@ -0,0 +1,127 @@
import { h } from 'preact'
import TextMode from './TextMode'
import ace from './assets/ace'
/**
* CodeMode (powered by Ace editor)
*
* Usage:
*
* <CodeMode
* options={Object}
* onChange={function(text: string)}
* onChangeMode={function(mode: string)}
* onLoadAce={function(aceEditor: Object, container: Element, options: Object) : Object}
* />
*
* Methods:
*
* setText(text)
* getText() : text
* set(json : JSON)
* get() : JSON
* patch(actions: JSONPatch)
* format()
* compact()
* destroy()
*
*/
export default class CodeMode extends TextMode {
constructor (props) {
super(props)
this.state = {}
this.id = 'id' + Math.round(Math.random() * 1e6) // unique enough id within the JSONEditor
this.aceEditor = null
}
render (props, state) {
return h('div', {class: 'jsoneditor jsoneditor-mode-code'}, [
this.renderMenu(),
h('div', {class: 'jsoneditor-contents', id: this.id})
])
}
componentDidMount () {
const options = this.props.options || {}
const container = this.base.querySelector('#' + this.id)
// use ace from bundle, and if not available try to use from global
const _ace = ace || window['ace']
let aceEditor = null
if (_ace && _ace.edit) {
// create ace editor
aceEditor = _ace.edit(container)
// bundle and load jsoneditor theme for ace editor
require('./assets/ace/theme-jsoneditor')
// configure ace editor
aceEditor.$blockScrolling = Infinity
aceEditor.setTheme('ace/theme/jsoneditor')
aceEditor.setShowPrintMargin(false)
aceEditor.setFontSize(13)
aceEditor.getSession().setMode('ace/mode/json')
aceEditor.getSession().setTabSize(options.indentation)
aceEditor.getSession().setUseSoftTabs(true)
aceEditor.getSession().setUseWrapMode(true)
aceEditor.commands.bindKey('Ctrl-L', null) // disable Ctrl+L (is used by the browser to select the address bar)
aceEditor.commands.bindKey('Command-L', null) // disable Ctrl+L (is used by the browser to select the address bar)
}
else {
// ace is excluded from the bundle.
}
// allow changing the config or completely replacing aceEditor
this.aceEditor = options.onLoadAce
? options.onLoadAce(aceEditor, container, options) || aceEditor
: aceEditor
// register onchange event
this.aceEditor.on('change', this.handleChange)
// set initial text
this.setText('{}')
}
componentWillUnmount () {
this.destroy()
}
/**
* Destroy the editor
*/
destroy () {
// neatly destroy ace editor
this.aceEditor.destroy()
}
componentDidUpdate () {
// TODO: handle changes in props
}
handleChange = () => {
// TODO: handle changes
// console.log('ace editor changed')
}
/**
* Set a string containing a JSON document
* @param {string} text
*/
setText (text) {
this.aceEditor.setValue(text, -1)
}
/**
* Get the JSON document as text
* @return {string} text
*/
getText () {
return this.aceEditor.getValue()
}
}

View File

@ -3,8 +3,30 @@ import { parseJSON } from './utils/jsonUtils'
import { jsonToData, dataToJson, patchData } from './jsonData'
import ModeButton from './menu/ModeButton'
/**
* TextMode
*
* Usage:
*
* <TextMode
* options={Object}
* onChange={function(text: string)}
* onChangeMode={function(mode: string)}
* />
*
* Methods:
*
* setText(text)
* getText() : text
* set(json : JSON)
* get() : JSON
* patch(actions: JSONPatch)
* format()
* compact()
* destroy()
*
*/
export default class TextMode extends Component {
// TODO: define propTypes
constructor (props) {
super(props)
@ -16,29 +38,7 @@ export default class TextMode extends Component {
render (props, state) {
return h('div', {class: 'jsoneditor jsoneditor-mode-text'}, [
h('div', {class: 'jsoneditor-menu'}, [
h('button', {
class: 'jsoneditor-format',
title: 'Format the JSON document',
onClick: this.handleFormat
}),
h('button', {
class: 'jsoneditor-compact',
title: 'Compact the JSON document',
onClick: this.handleCompact
}),
// TODO: implement a button "Fix JSON"
h('div', {class: 'jsoneditor-vertical-menu-separator'}),
this.props.options.modes && h(ModeButton, {
modes: this.props.options.modes,
mode: this.props.mode,
onMode: this.props.onMode,
onError: this.handleError
})
]),
this.renderMenu(),
h('div', {class: 'jsoneditor-contents'}, [
h('textarea', {
@ -50,10 +50,37 @@ export default class TextMode extends Component {
])
}
/** @protected */
renderMenu () {
return h('div', {class: 'jsoneditor-menu'}, [
h('button', {
class: 'jsoneditor-format',
title: 'Format the JSON document',
onClick: this.handleFormat
}),
h('button', {
class: 'jsoneditor-compact',
title: 'Compact the JSON document',
onClick: this.handleCompact
}),
// TODO: implement a button "Repair"
h('div', {class: 'jsoneditor-vertical-menu-separator'}),
this.props.options.modes && h(ModeButton, {
modes: this.props.options.modes,
mode: this.props.mode,
onChangeMode: this.props.onChangeMode,
onError: this.handleError
})
])
}
/**
* Get the configured indentation
* @return {number}
* @private
* @protected
*/
getIndentation () {
return this.props.options && this.props.options.indentation || 2
@ -62,15 +89,13 @@ export default class TextMode extends Component {
/**
* handle changed text input in the textarea
* @param {Event} event
* @private
* @protected
*/
handleChange = (event) => {
this.setState({
text: event.target.value
})
this.setText(event.target.value)
}
/** @private */
/** @protected */
handleFormat = () => {
try {
this.format()
@ -80,7 +105,7 @@ export default class TextMode extends Component {
}
}
/** @private */
/** @protected */
handleCompact = () => {
try {
this.compact()
@ -90,7 +115,7 @@ export default class TextMode extends Component {
}
}
/** @private */
/** @protected */
handleError = (err) => {
if (this.props.options && this.props.options.onError) {
this.props.options.onError(err)
@ -145,9 +170,7 @@ export default class TextMode extends Component {
* @param {Object | Array | string | number | boolean | null} json JSON data
*/
set (json) {
this.setState({
text: JSON.stringify(json, null, this.getIndentation())
})
this.setText(JSON.stringify(json, null, this.getIndentation()))
}
/**
@ -155,7 +178,7 @@ export default class TextMode extends Component {
* @returns {Object | Array | string | number | boolean | null} json
*/
get () {
return parseJSON(this.state.text)
return parseJSON(this.getText())
}
/**
@ -173,4 +196,11 @@ export default class TextMode extends Component {
getText () {
return this.state.text
}
/**
* Destroy the editor
*/
destroy () {
}
}

View File

@ -48,7 +48,7 @@ export default class TreeMode extends Component {
}
render (props, state) {
// TODO: make mode tree dynamic
// TODO: make mode tree dynamic: can be 'tree', 'form', 'view'
return h('div', {
class: 'jsoneditor jsoneditor-mode-tree',
'data-jsoneditor': 'true'
@ -104,7 +104,7 @@ export default class TreeMode extends Component {
this.props.options.modes && h(ModeButton, {
modes: this.props.options.modes,
mode: this.props.mode,
onMode: this.props.onMode,
onChangeMode: this.props.onChangeMode,
onError: this.handleError
})
])
@ -365,6 +365,13 @@ export default class TreeMode extends Component {
})
}
/**
* Destroy the editor
*/
destroy () {
}
/**
* Default function to determine whether or not to expand a node initially
*

8
src/assets/ace/index.js Normal file
View File

@ -0,0 +1,8 @@
// load brace
import ace from 'brace'
// load required ace plugins
import 'brace/mode/json'
import 'brace/ext/searchbox'
export default ace

View File

@ -0,0 +1,144 @@
/* ***** BEGIN LICENSE BLOCK *****
* Distributed under the BSD license:
*
* Copyright (c) 2010, Ajax.org B.V.
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
* * Neither the name of Ajax.org B.V. nor the
* names of its contributors may be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* ***** END LICENSE BLOCK ***** */
ace.define('ace/theme/jsoneditor', ['require', 'exports', 'module', 'ace/lib/dom'], function(acequire, exports, module) {
exports.isDark = false;
exports.cssClass = "ace-jsoneditor";
exports.cssText = ".ace-jsoneditor .ace_gutter {\
background: #ebebeb;\
color: #333\
}\
\
.ace-jsoneditor.ace_editor {\
font-family: droid sans mono, consolas, monospace, courier new, courier, sans-serif;\
line-height: 1.3;\
}\
.ace-jsoneditor .ace_print-margin {\
width: 1px;\
background: #e8e8e8\
}\
.ace-jsoneditor .ace_scroller {\
background-color: #FFFFFF\
}\
.ace-jsoneditor .ace_text-layer {\
color: gray\
}\
.ace-jsoneditor .ace_variable {\
color: #1a1a1a\
}\
.ace-jsoneditor .ace_cursor {\
border-left: 2px solid #000000\
}\
.ace-jsoneditor .ace_overwrite-cursors .ace_cursor {\
border-left: 0px;\
border-bottom: 1px solid #000000\
}\
.ace-jsoneditor .ace_marker-layer .ace_selection {\
background: lightgray\
}\
.ace-jsoneditor.ace_multiselect .ace_selection.ace_start {\
box-shadow: 0 0 3px 0px #FFFFFF;\
border-radius: 2px\
}\
.ace-jsoneditor .ace_marker-layer .ace_step {\
background: rgb(255, 255, 0)\
}\
.ace-jsoneditor .ace_marker-layer .ace_bracket {\
margin: -1px 0 0 -1px;\
border: 1px solid #BFBFBF\
}\
.ace-jsoneditor .ace_marker-layer .ace_active-line {\
background: #FFFBD1\
}\
.ace-jsoneditor .ace_gutter-active-line {\
background-color : #dcdcdc\
}\
.ace-jsoneditor .ace_marker-layer .ace_selected-word {\
border: 1px solid lightgray\
}\
.ace-jsoneditor .ace_invisible {\
color: #BFBFBF\
}\
.ace-jsoneditor .ace_keyword,\
.ace-jsoneditor .ace_meta,\
.ace-jsoneditor .ace_support.ace_constant.ace_property-value {\
color: #AF956F\
}\
.ace-jsoneditor .ace_keyword.ace_operator {\
color: #484848\
}\
.ace-jsoneditor .ace_keyword.ace_other.ace_unit {\
color: #96DC5F\
}\
.ace-jsoneditor .ace_constant.ace_language {\
color: darkorange\
}\
.ace-jsoneditor .ace_constant.ace_numeric {\
color: red\
}\
.ace-jsoneditor .ace_constant.ace_character.ace_entity {\
color: #BF78CC\
}\
.ace-jsoneditor .ace_invalid {\
color: #FFFFFF;\
background-color: #FF002A;\
}\
.ace-jsoneditor .ace_fold {\
background-color: #AF956F;\
border-color: #000000\
}\
.ace-jsoneditor .ace_storage,\
.ace-jsoneditor .ace_support.ace_class,\
.ace-jsoneditor .ace_support.ace_function,\
.ace-jsoneditor .ace_support.ace_other,\
.ace-jsoneditor .ace_support.ace_type {\
color: #C52727\
}\
.ace-jsoneditor .ace_string {\
color: green\
}\
.ace-jsoneditor .ace_comment {\
color: #BCC8BA\
}\
.ace-jsoneditor .ace_entity.ace_name.ace_tag,\
.ace-jsoneditor .ace_entity.ace_other.ace_attribute-name {\
color: #606060\
}\
.ace-jsoneditor .ace_markup.ace_underline {\
text-decoration: underline\
}\
.ace-jsoneditor .ace_indent-guide {\
background: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bLly//BwAmVgd1/w11/gAAAABJRU5ErkJggg==\") right repeat-y\
}";
var dom = acequire("../lib/dom");
dom.importCssString(exports.cssText, exports.cssClass);
});

View File

@ -7,7 +7,7 @@
<!-- For IE and Edge -->
<!--<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-shim/0.35.1/es6-shim.min.js"></script>-->
<script src="../dist/jsoneditor-minimalist.js"></script>
<script src="../dist/jsoneditor.js"></script>
<style>
#container {
height: 300px;
@ -21,6 +21,7 @@
<label for="mode">mode:
<select id="mode">
<option value="code">code</option>
<option value="text">text</option>
<option value="tree" selected>tree</option>
</select>
@ -30,6 +31,7 @@
<script>
// create the editor
const mode = document.getElementById('mode').value
const container = document.getElementById('container')
const options = {
onChange: function (patch, revert) {
@ -39,12 +41,14 @@
},
onChangeMode: function (mode, prevMode) {
console.log('switched mode from', prevMode, 'to', mode)
document.getElementById('mode').value = mode
},
onError: function (err) {
console.error(err)
alert(err)
},
modes: ['text', 'tree'],
mode: mode,
modes: ['text', 'code', 'tree'],
indentation: 4
}
const editor = jsoneditor(container, options)

View File

@ -1,13 +1,15 @@
import { h, render } from 'preact'
import TreeMode from './TreeMode'
import CodeMode from './CodeMode'
import TextMode from './TextMode'
import TreeMode from './TreeMode'
import '!style!css!less!./jsoneditor.less'
// TODO: allow adding new modes
const modes = {
tree: TreeMode,
text: TextMode
code: CodeMode,
text: TextMode,
tree: TreeMode
}
/**
@ -121,6 +123,7 @@ function jsoneditor (container, options = {}) {
}
let success = false
let initialChildCount = editor._container.children.length
let element
try {
// find the constructor for the selected mode
@ -130,12 +133,22 @@ function jsoneditor (container, options = {}) {
'Choose from: ' + Object.keys(modes).join(', '))
}
function handleChangeMode (mode) {
const prevMode = editor._mode
editor.setMode(mode)
if (editor._options.onChangeMode) {
editor._options.onChangeMode(mode, prevMode)
}
}
// create new component
element = render(
h(constructor, {
mode,
options: editor._options,
onMode: editor.setMode
onChangeMode: handleChangeMode
}),
editor._container)
@ -150,22 +163,22 @@ function jsoneditor (container, options = {}) {
if (success) {
// destroy previous component
if (editor._element) {
// TODO: call editor._component.destroy() instead
editor._element._component.destroy()
editor._element.parentNode.removeChild(editor._element)
}
const prevMode = editor._mode
editor._mode = mode
editor._element = element
editor._component = element._component
if (editor._options.onChangeMode && prevMode) {
editor._options.onChangeMode(mode, prevMode)
}
}
else {
// remove the just created component (where setText failed)
element.parentNode.removeChild(element)
// TODO: fall back to text mode when loading code mode failed?
// remove the just created component if any (where construction or setText failed)
const childCount = editor._container.children.length
if (childCount !== initialChildCount) {
editor._container.removeChild(editor._container.lastChild)
}
}
}
}

View File

@ -12,7 +12,7 @@ export default class ModeButton extends Component {
}
/**
* @param {{modes: string[], mode: string, onMode: function, onError: function}} props
* @param {{modes: string[], mode: string, onChangeMode: function, onError: function}} props
* @param state
* @return {*}
*/

View File

@ -4,7 +4,7 @@ import { findParentNode } from '../utils/domUtils'
export default class ModeMenu extends Component {
/**
* @param {{open, modes, mode, onMode, onRequestClose, onError}} props
* @param {{open, modes, mode, onChangeMode, onRequestClose, onError}} props
* @param {Object} state
* @return {JSX.Element}
*/
@ -17,7 +17,7 @@ export default class ModeMenu extends Component {
((mode === props.mode) ? ' jsoneditor-selected' : ''),
onClick: () => {
try {
props.onMode(mode)
props.onChangeMode(mode)
props.onRequestClose()
}
catch (err) {

View File

@ -31,7 +31,7 @@
* }} JSONPatchResult
*
* @typedef {{
* mode: 'tree' | 'text',
* mode: 'tree' | 'text' | 'code',
* modes: string[],
* indentation: number | string,
* onChange: function (patch: JSONPatch, revert: JSONPatch),

View File

@ -19,7 +19,6 @@ export function parseJSON(jsonString) {
}
}
/**
* Validate a string containing a JSON object
* This method uses JSONLint to validate the String. If JSONLint is not