From b796de21285139dc574795ef87b1a3c48198a45b Mon Sep 17 00:00:00 2001 From: jos Date: Thu, 14 Jul 2016 15:15:50 +0200 Subject: [PATCH] Implemented expand/collapse --- src/JSONNode.js | 126 ++-- src/favicon.ico | Bin 0 -> 1150 bytes src/img/description.txt | 14 + src/img/jsoneditor-icons.svg | 893 +++++++++++++++++++++++++++++ src/jsoneditor.css | 66 ++- src/utils/shouldComponentUpdate.js | 27 + 6 files changed, 1075 insertions(+), 51 deletions(-) create mode 100644 src/favicon.ico create mode 100644 src/img/description.txt create mode 100644 src/img/jsoneditor-icons.svg create mode 100644 src/utils/shouldComponentUpdate.js diff --git a/src/JSONNode.js b/src/JSONNode.js index 344904f..df22689 100644 --- a/src/JSONNode.js +++ b/src/JSONNode.js @@ -7,10 +7,15 @@ export default class JSONNode extends Component { constructor (props) { super(props) - this.onChangeField = this.onChangeField.bind(this) - this.onChangeValue = this.onChangeValue.bind(this) - this.onClickValue = this.onClickValue.bind(this) - this.onKeyDownValue = this.onKeyDownValue.bind(this) + this.state = { + expanded: false + } + + this.handleChangeField = this.handleChangeField.bind(this) + this.handleChangeValue = this.handleChangeValue.bind(this) + this.handleClickValue = this.handleClickValue.bind(this) + this.handleKeyDownValue = this.handleKeyDownValue.bind(this) + this.handleExpand = this.handleExpand.bind(this) } render (props) { @@ -26,50 +31,65 @@ export default class JSONNode extends Component { } renderJSONObject ({parent, field, value, onChangeValue, onChangeField}) { - const childs = Object.keys(value).map(f => { - return h(JSONNode, { - parent: this, - field: f, - value: value[f], - onChangeValue, - onChangeField - }) - }) - - return h('li', {}, [ + const childCount = Object.keys(value).length + const contents = [ h('div', {class: 'jsoneditor-node jsoneditor-object'}, [ + this.renderExpandButton(), this.renderField(field, value, parent), this.renderSeparator(), - this.renderReadonly('{' + Object.keys(value).length + '}') - ]), - h('ul', {class: 'jsoneditor-list'}, childs) - ]) + this.renderReadonly(`{${childCount}}`, `Array containing ${childCount} items`) + ]) + ] + + if (this.state.expanded) { + const childs = this.state.expanded && Object.keys(value).map(f => { + return h(JSONNode, { + parent: this, + field: f, + value: value[f], + onChangeValue, + onChangeField + }) + }) + + contents.push(h('ul', {class: 'jsoneditor-list'}, childs)) + } + + return h('li', {}, contents) } renderJSONArray ({parent, field, value, onChangeValue, onChangeField}) { - const childs = value.map((v, i) => { - return h(JSONNode, { - parent: this, - index: i, - value: v, - onChangeValue, - onChangeField - }) - }) - - return h('li', {}, [ + const childCount = value.length + const contents = [ h('div', {class: 'jsoneditor-node jsoneditor-array'}, [ + this.renderExpandButton(), this.renderField(field, value, parent), this.renderSeparator(), - this.renderReadonly('{' + value.length + '}') - ]), - h('ul', {class: 'jsoneditor-list'}, childs) - ]) + this.renderReadonly(`[${childCount}]`, `Array containing ${childCount} items`) + ]) + ] + + if (this.state.expanded) { + const childs = this.state.expanded && value.map((v, i) => { + return h(JSONNode, { + parent: this, + index: i, + value: v, + onChangeValue, + onChangeField + }) + }) + + contents.push(h('ul', {class: 'jsoneditor-list'}, childs)) + } + + return h('li', {}, contents) } renderJSONValue ({parent, index, field, value}) { return h('li', {}, [ h('div', {class: 'jsoneditor-node'}, [ + h('div', {class: 'jsoneditor-button-placeholder'}), index !== undefined ? this.renderReadonly(index) : this.renderField(field, value, parent), @@ -79,8 +99,8 @@ export default class JSONNode extends Component { ]) } - renderReadonly (text) { - return h('div', {class: 'jsoneditor-readonly', contentEditable: false}, text) + renderReadonly (text, title = null) { + return h('div', {class: 'jsoneditor-readonly', contentEditable: false, title}, text) } renderField (field, value, parent) { @@ -91,7 +111,7 @@ export default class JSONNode extends Component { class: 'jsoneditor-field' + (hasParent ? '' : ' jsoneditor-readonly'), contentEditable: hasParent, spellCheck: 'false', - onBlur: this.onChangeField + onBlur: this.handleChangeField }, content) } @@ -108,18 +128,26 @@ export default class JSONNode extends Component { class: valueClass, contentEditable: true, spellCheck: 'false', - onBlur: this.onChangeValue, - onClick: this.onClickValue, - onKeyDown: this.onKeyDownValue, + onBlur: this.handleChangeValue, + onClick: this.handleClickValue, + onKeyDown: this.handleKeyDownValue, title: _isUrl ? 'Ctrl+Click or ctrl+Enter to open url' : null }, escapeHTML(value)) } - shouldComponentUpdate(nextProps, nextState) { - return nextProps.field !== this.props.field || nextProps.value !== this.props.value + renderExpandButton () { + const className = `jsoneditor-button jsoneditor-${this.state.expanded ? 'expanded' : 'collapsed'}` + return h('div', {class: 'jsoneditor-button-container'}, + h('button', {class: className, onClick: this.handleExpand}) + ) } - onChangeField (event) { + shouldComponentUpdate(nextProps, nextState) { + return Object.keys(nextProps).some(prop => this.props[prop] !== nextProps[prop]) || + (this.state && Object.keys(nextState).some(prop => this.state[prop] !== nextState[prop])) + } + + handleChangeField (event) { const path = this.props.parent.getPath() const newField = unescapeHTML(getInnerText(event.target)) const oldField = this.props.field @@ -128,7 +156,7 @@ export default class JSONNode extends Component { } } - onChangeValue (event) { + handleChangeValue (event) { const path = this.getPath() const value = this._getValueFromEvent(event) if (value !== this.props.value) { @@ -136,18 +164,24 @@ export default class JSONNode extends Component { } } - onClickValue (event) { + handleClickValue (event) { if (event.ctrlKey && event.button === 0) { // Ctrl+Left click this._openLinkIfUrl(event) } } - onKeyDownValue (event) { + handleKeyDownValue (event) { if (event.ctrlKey && event.which === 13) { // Ctrl+Enter this._openLinkIfUrl(event) } } + handleExpand (event) { + this.setState({ + expanded: !this.state.expanded + }) + } + _openLinkIfUrl (event) { const value = this._getValueFromEvent(event) diff --git a/src/favicon.ico b/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..e5350759bffc629d0fcc12e383023913dfa8d244 GIT binary patch literal 1150 zcmbVMyNUua6wShZfYsJp?$7u_RBTifVHy!zYYVGo5Jvm~8$m=6v9huev=Ko>(M}Mt z&_=|8V{VoVx$A7ulS$@=hibB$G*|qw}TKZnsqpv>&8NBod%L)is;Va5|mXJ)pZnUJ5$< z(P-p-Yh35eW`o^s=kcbOG;}|v)2YX6ALVQ|3%Vb4ekN}A>3pcsJSI;2qxY@X>oLyM t6^li5Ivw=;eWocC3Tj=gRtx!jo^jIenq$1!cjdaAe~zKNzCVQ*zz55cQ+@yd literal 0 HcmV?d00001 diff --git a/src/img/description.txt b/src/img/description.txt new file mode 100644 index 0000000..fe410a9 --- /dev/null +++ b/src/img/description.txt @@ -0,0 +1,14 @@ +JSON Editor Icons + +size: outer: 24x24 px + inner: 16x16 px + +blue background: RGBA 97b0f8ff +gray background: RGBA 4d4d4dff +grey background: RGBA d3d3d3ff + +red foreground: RGBA ff3300ff +green foreground: RGBA 13ae00ff + +characters are based on the Arial font + diff --git a/src/img/jsoneditor-icons.svg b/src/img/jsoneditor-icons.svg new file mode 100644 index 0000000..1b40068 --- /dev/null +++ b/src/img/jsoneditor-icons.svg @@ -0,0 +1,893 @@ + + + JSON Editor Icons + + + + image/svg+xml + + JSON Editor Icons + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/jsoneditor.css b/src/jsoneditor.css index 13dfea5..7ed7af1 100644 --- a/src/jsoneditor.css +++ b/src/jsoneditor.css @@ -17,14 +17,29 @@ .jsoneditor-node > div { flex: 1 1 auto; - - padding: 2px 5px; } ul.jsoneditor-list { list-style-type: none; padding-left: 24px; margin: 0; + font-size: 0; +} + +/* no left padding for the root ul element */ +.jsoneditor > ul.jsoneditor-list { + padding-left: 2px; +} + +.jsoneditor-field, +.jsoneditor-value, +.jsoneditor-readonly, +.jsoneditor-separator { + flex: 1 1 auto; + line-height: 20px; + + font-family: droid sans mono, consolas, monospace, courier new, courier, sans-serif; + font-size: 10pt; } .jsoneditor-field, @@ -33,12 +48,16 @@ ul.jsoneditor-list { min-width: 24px; word-break: break-word; - font-family: droid sans mono, consolas, monospace, courier new, courier, sans-serif; - font-size: 10pt; + padding: 0 5px; + color: #1A1A1A; outline: none; } +.jsoneditor-button-container { + font-size: 0; +} + .jsoneditor-field, .jsoneditor-value { border-radius: 1px; @@ -98,4 +117,41 @@ ul.jsoneditor-list { div.jsoneditor-value.jsoneditor-url { color: green; text-decoration: underline; -} \ No newline at end of file +} + +.jsoneditor-button-placeholder { + width: 20px; + padding: 0; + margin: 0; + + line-height: 20px; +} + +button.jsoneditor-button { + width: 20px; + height: 20px; + padding: 0; + margin: 0; + border: none; + cursor: pointer; + background: transparent url('img/jsoneditor-icons.svg'); +} + +button.jsoneditor-button:focus { + /* TODO: nice outline for buttons with focus + outline: #97B0F8 solid 2px; + box-shadow: 0 0 8px #97B0F8; + */ + background-color: #f5f5f5; + outline: #e5e5e5 solid 1px; +} + +/* FIXME: change icons from size 24x24 to 20x20 */ + +button.jsoneditor-button.jsoneditor-collapsed { + background-position: -2px -50px; +} + +button.jsoneditor-button.jsoneditor-expanded { + background-position: -2px -74px; +} diff --git a/src/utils/shouldComponentUpdate.js b/src/utils/shouldComponentUpdate.js new file mode 100644 index 0000000..799f511 --- /dev/null +++ b/src/utils/shouldComponentUpdate.js @@ -0,0 +1,27 @@ +/** + * Compares all current props and state with previous props and state, + * returns true if there are differences. Does do a flat comparison. + * + * Usage: add this function as property of a React.Component class in the constructor: + * + * import shouldComponentUpdate from './shouldComponentUpdate' + * + * export default class MyComponent extends React.Component { + * constructor (props) { + * super(props) + * + * // update only when props or state are changed + * this.shouldComponentUpdate = shouldComponentUpdate + * } + * + * render () { ...} + * } + * + * @param nextProps + * @param nextState + * @return {boolean} + */ +export default function shouldComponentUpdate (nextProps, nextState) { + return Object.keys(nextProps).some(prop => this.props[prop] !== nextProps[prop]) || + (this.state && Object.keys(nextState).some(prop => this.state[prop] !== nextState[prop])) +}