Basic editor using preact

This commit is contained in:
jos 2016-07-12 13:56:40 +02:00
parent b4cf47b06f
commit 0d250cbdcd
9 changed files with 12942 additions and 1 deletions

View File

@ -20,7 +20,8 @@
"scripts": {
"build": "gulp",
"watch": "gulp watch",
"test": "mocha test"
"test": "mocha test",
"build-next": "rollup -c"
},
"dependencies": {
"ajv": "3.8.8",
@ -28,14 +29,26 @@
"javascript-natural-sort": "0.7.1"
},
"devDependencies": {
"babel-plugin-transform-react-jsx": "6.8.0",
"browserify": "13.0.1",
"buble": "0.12.5",
"debug": "2.2.0",
"gulp": "3.9.1",
"gulp-clean-css": "2.0.5",
"gulp-concat-css": "2.2.0",
"gulp-shell": "0.5.2",
"gulp-util": "3.0.7",
"json-loader": "0.5.4",
"mithril": "0.2.5",
"mkdirp": "0.5.1",
"mocha": "2.4.5",
"preact": "4.8.0",
"reify": "0.3.6",
"rollup": "0.34.1",
"rollup-plugin-buble": "0.12.1",
"rollup-plugin-commonjs": "3.1.0",
"rollup-plugin-node-resolve": "1.7.1",
"rollup-plugin-npm": "2.0.0",
"uglify-js": "2.6.2",
"webpack": "1.12.14"
}

12655
public/index.html Normal file

File diff suppressed because it is too large Load Diff

49
public/jsoneditor.css Normal file
View File

@ -0,0 +1,49 @@
.jsoneditor {
border: 1px solid #3883fa;
overflow: auto;
}
.jsoneditor-node {
/*border: 1px solid #555;*/
font: 14px Arial;
/* flexbox setup */
display: inline-flex;
flex-direction: row;
}
.jsoneditor-node > div {
flex: 1 1 auto;
padding: 2px 5px;
}
ul.jsoneditor-list {
list-style-type: none;
padding-left: 24px;
margin: 0;
}
.jsoneditor-key,
.jsoneditor-value {
min-width: 32px;
border: 1px solid transparent;
border-radius: 2px;
word-break: break-word;
}
.jsoneditor-key:focus,
.jsoneditor-value:focus,
.jsoneditor-key:hover,
.jsoneditor-value:hover {
background-color: #FFFFAB;
border-color: #ff0;
}
.jsoneditor-separator {
color: gray;
}
.jsoneditor-info {
color: gray;
}

92
src/JSONNode.js Normal file
View File

@ -0,0 +1,92 @@
import { h, Component } from 'preact'
import isObject from './utils/isObject'
export default class JSONNode extends Component {
constructor (props) {
super(props)
this.onValueInput = this.onValueInput.bind(this)
}
render (props) {
if (Array.isArray(props.value)) {
return this.renderArray(props)
}
else if (isObject(props.value)) {
return this.renderObject(props)
}
else {
return this.renderValue(props)
}
}
renderObject ({field, value, onChangeValue}) {
//console.log('JSONObject', field,value)
return h('li', {class: 'jsoneditor-object'}, [
h('div', {class: 'jsoneditor-node'}, [
h('div', {class: 'jsoneditor-field', contentEditable: true}, field),
h('div', {class: 'jsoneditor-separator'}, ':'),
h('div', {class: 'jsoneditor-info'}, '{' + Object.keys(value).length + '}')
]),
h('ul',
{class: 'jsoneditor-list'},
Object.keys(value).map(f => h(JSONNode, {parent: this, field: f, value: value[f], onChangeValue})))
])
}
renderArray ({field, value, onChangeValue}) {
return h('li', {}, [
h('div', {class: 'jsoneditor-node jsoneditor-array'}, [
h('div', {class: 'jsoneditor-field', contentEditable: true}, field),
h('div', {class: 'jsoneditor-separator'}, ':'),
h('div', {class: 'jsoneditor-info'}, '{' + value.length + '}')
]),
h('ul',
{class: 'jsoneditor-list'},
value.map((v, f) => h(JSONNode, {parent: this, field: f, value: v, onChangeValue})))
])
}
renderValue ({field, value}) {
//console.log('JSONValue', field, value)
return h('li', {}, [
h('div', {class: 'jsoneditor-node'}, [
h('div', {class: 'jsoneditor-field', contentEditable: true}, field),
h('div', {class: 'jsoneditor-separator'}, ':'),
h('div', {
class: 'jsoneditor-value',
contentEditable: true,
// 'data-path': JSON.stringify(this.getPath())
onInput: this.onValueInput
}, value)
])
])
}
shouldComponentUpdate(nextProps, nextState) {
return nextProps.field !== this.props.field || nextProps.value !== this.props.value
}
onValueInput (event) {
const path = this.getPath()
const value = event.target.innerHTML
this.props.onChangeValue(path, value)
}
getPath () {
const path = []
let node = this
while (node) {
path.unshift(node.props.field)
node = node.props.parent
}
path.shift() // remove the root node again (null)
return path
}
}

38
src/Main.js Normal file
View File

@ -0,0 +1,38 @@
import { h, Component } from 'preact'
import setIn from './utils/setIn'
import JSONNode from './JSONNode'
export default class Main extends Component {
constructor (props) {
super(props)
this.state = {
json: props.json || {}
}
this.onChangeValue = this.onChangeValue.bind(this)
}
render(props, state) {
return h('div', {class: 'jsoneditor', onInput: this.onInput}, [
h('ul', {class: 'jsoneditor-list'}, [
h(JSONNode, {parent: null, field: null, value: state.json, onChangeValue: this.onChangeValue})
])
])
}
onChangeValue (path, value) {
console.log('onChangeValue', path, value)
this.setState({
json: setIn(this.state.json, path, value)
})
}
get () {
return this.state.json
}
set (json) {
this.setState({json})
}
}

21
src/index.js Normal file
View File

@ -0,0 +1,21 @@
import { h, render } from 'preact'
import Main from './Main'
/**
* Factory function to create a new JSONEditor
* @param container
* @return {*}
* @constructor
*/
export default function jsoneditor (container) {
const elem = render(h(Main), container)
return elem._component
}
// TODO: UMD export
window.jsoneditor = jsoneditor
// export JSONEditor

27
src/utils/clone.js Normal file
View File

@ -0,0 +1,27 @@
import isObject from './isObject'
// TODO: unit test clone
/**
* Flat clone the properties of an object or array
* @param {Object | Array} value
* @return {Object | Array} Returns a flat clone of the object or Array
*/
export default function clone (value) {
if (Array.isArray(value)) {
return value.slice(0)
}
else if (isObject(value)) {
const cloned = {}
Object.keys(value).forEach(key => {
cloned[key] = value[key]
})
return cloned
}
else {
// a primitive value
return value
}
}

12
src/utils/isObject.js Normal file
View File

@ -0,0 +1,12 @@
/**
* Test whether a value is an object (and not an Array or Date or primitive value)
*
* @param {*} value
* @return {boolean}
*/
export default function isObject (value) {
return typeof value === 'object' &&
value && // not null
!Array.isArray(value) &&
value.toString() === '[object Object]'
}

34
src/utils/setIn.js Normal file
View File

@ -0,0 +1,34 @@
import isObject from './isObject'
import clone from './clone'
// TODO: unit test setIn
/**
* helper function to replace a nested property in an object with a new value
* without mutating the object itself.
*
* @param {Object | Array} object
* @param {Array.<string | number>} path
* @param {*} value
* @return {Object | Array} Returns a new, updated object or array
*/
export default function setIn (object, path, value) {
if (path.length === 0) {
return value
}
// TODO: change array into object and vice versa when key is a number/string
const key = path[0]
const child = (Array.isArray(object[key]) || isObject(object[key]))
? object[key]
: (typeof path[1] === 'number' ? [] : {})
const updated = clone(object)
updated[key] = setIn(child, path.slice(1), value)
return updated
}
window.setIn = setIn // TODO: cleanup