Replace svg sprite with fontawesome icons. Extend menu (WIP)

This commit is contained in:
jos 2018-09-12 17:54:20 +02:00
parent 0fb816d074
commit 081cde4489
14 changed files with 949 additions and 1370 deletions

1434
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -19,6 +19,8 @@
"bugs": "https://github.com/josdejong/jsoneditor/issues", "bugs": "https://github.com/josdejong/jsoneditor/issues",
"private": false, "private": false,
"dependencies": { "dependencies": {
"@fortawesome/fontawesome": "1.1.8",
"@fortawesome/fontawesome-free-solid": "5.0.13",
"ajv": "6.5.3", "ajv": "6.5.3",
"brace": "0.11.1", "brace": "0.11.1",
"javascript-natural-sort": "0.7.1", "javascript-natural-sort": "0.7.1",

View File

@ -15,6 +15,13 @@ import {
import { compileJSONPointer } from '../jsonPointer' import { compileJSONPointer } from '../jsonPointer'
import { ERROR, EXPANDED, ID, SEARCH_PROPERTY, SEARCH_VALUE, SELECTION, TYPE, VALUE } from '../eson' import { ERROR, EXPANDED, ID, SEARCH_PROPERTY, SEARCH_VALUE, SELECTION, TYPE, VALUE } from '../eson'
import fontawesome from '@fortawesome/fontawesome'
import faExclamationTriangle from '@fortawesome/fontawesome-free-solid/faExclamationTriangle'
import faCaretRight from '@fortawesome/fontawesome-free-solid/faCaretRight'
import faCaretDown from '@fortawesome/fontawesome-free-solid/faCaretDown'
fontawesome.library.add(faExclamationTriangle, faCaretRight, faCaretDown)
export default class JSONNode extends PureComponent { export default class JSONNode extends PureComponent {
static URL_TITLE = 'Ctrl+Click or Ctrl+Enter to open url' static URL_TITLE = 'Ctrl+Click or Ctrl+Enter to open url'
@ -96,12 +103,9 @@ export default class JSONNode extends PureComponent {
? [ ? [
this.renderTag(`${jsonPropsCount} ${jsonPropsCount === 1 ? 'prop' : 'props'}`, this.renderTag(`${jsonPropsCount} ${jsonPropsCount === 1 ? 'prop' : 'props'}`,
`Object containing ${jsonPropsCount} ${jsonPropsCount === 1 ? 'property' : 'properties'}`), `Object containing ${jsonPropsCount} ${jsonPropsCount === 1 ? 'property' : 'properties'}`),
this.renderDelimiter('}', 'jsoneditor-delimiter-end jsoneditor-delimiter-collapsed'), this.renderDelimiter('}', 'jsoneditor-delimiter-end jsoneditor-delimiter-collapsed')
this.renderInsertAfter()
] ]
: [ : null,
this.renderInsertBefore()
],
this.renderError(this.props.eson[ERROR]) this.renderError(this.props.eson[ERROR])
]) ])
@ -131,8 +135,7 @@ export default class JSONNode extends PureComponent {
const floatingMenu = this.renderFloatingMenu('object', this.props.eson[SELECTION]) const floatingMenu = this.renderFloatingMenu('object', this.props.eson[SELECTION])
const nodeEnd = this.props.eson[EXPANDED] const nodeEnd = this.props.eson[EXPANDED]
? h('div', {key: 'node-end', className: 'jsoneditor-node-end', 'data-area': 'empty'}, [ ? h('div', {key: 'node-end', className: 'jsoneditor-node-end', 'data-area': 'empty'}, [
this.renderDelimiter('}', 'jsoneditor-delimiter-end'), this.renderDelimiter('}', 'jsoneditor-delimiter-end')
this.renderInsertAfter()
]) ])
: null : null
@ -162,11 +165,8 @@ export default class JSONNode extends PureComponent {
this.renderTag(`${count} ${count === 1 ? 'item' : 'items'}`, this.renderTag(`${count} ${count === 1 ? 'item' : 'items'}`,
`Array containing ${count} ${count === 1 ? 'item' : 'items'}`), `Array containing ${count} ${count === 1 ? 'item' : 'items'}`),
this.renderDelimiter(']', 'jsoneditor-delimiter-end jsoneditor-delimiter-collapsed'), this.renderDelimiter(']', 'jsoneditor-delimiter-end jsoneditor-delimiter-collapsed'),
this.renderInsertAfter(),
] ]
: [ : null,
this.renderInsertBefore()
],
this.renderError(this.props.eson[ERROR]) this.renderError(this.props.eson[ERROR])
]) ])
@ -195,8 +195,7 @@ export default class JSONNode extends PureComponent {
const floatingMenu = this.renderFloatingMenu('array', this.props.eson[SELECTION]) const floatingMenu = this.renderFloatingMenu('array', this.props.eson[SELECTION])
const nodeEnd = this.props.eson[EXPANDED] const nodeEnd = this.props.eson[EXPANDED]
? h('div', {key: 'node-end', className: 'jsoneditor-node-end', 'data-area': 'empty'}, [ ? h('div', {key: 'node-end', className: 'jsoneditor-node-end', 'data-area': 'empty'}, [
this.renderDelimiter(']', 'jsoneditor-delimiter-end'), this.renderDelimiter(']', 'jsoneditor-delimiter-end')
this.renderInsertAfter()
]) ])
: null : null
@ -219,7 +218,6 @@ export default class JSONNode extends PureComponent {
this.renderProperty(), this.renderProperty(),
this.renderSeparator(), this.renderSeparator(),
this.renderValue(this.props.eson[VALUE], this.props.eson[SEARCH_VALUE], this.props.options), // FIXME this.renderValue(this.props.eson[VALUE], this.props.eson[SEARCH_VALUE], this.props.options), // FIXME
this.renderInsertAfter(),
this.renderError(this.props.eson[ERROR]) this.renderError(this.props.eson[ERROR])
]) ])
@ -236,24 +234,6 @@ export default class JSONNode extends PureComponent {
}, [node, floatingMenu]) }, [node, floatingMenu])
} }
renderInsertBefore () {
return h('div', {
key: 'insert',
className: 'jsoneditor-insert jsoneditor-insert-before',
title: 'Insert a new item or paste clipboard',
'data-area': 'inside'
})
}
renderInsertAfter () {
return h('div', {
key: 'insert',
className: 'jsoneditor-insert jsoneditor-insert-after',
title: 'Insert a new item or paste clipboard after this line',
'data-area': 'after'
})
}
/** /**
* Render contents for an empty object or array * Render contents for an empty object or array
* @param {string} text * @param {string} text
@ -398,7 +378,10 @@ export default class JSONNode extends PureComponent {
onFocus: this.updatePopoverDirection, onFocus: this.updatePopoverDirection,
onMouseOver: this.updatePopoverDirection onMouseOver: this.updatePopoverDirection
}, },
h('div', {className: 'jsoneditor-popover jsoneditor-right'}, error.message) [
h('i', {className: 'fa fa-exclamation-triangle'}),
h('div', {className: 'jsoneditor-popover jsoneditor-right'}, error.message)
]
) )
} }
else { else {
@ -445,7 +428,7 @@ export default class JSONNode extends PureComponent {
*/ */
updatePopoverDirection = (event) => { updatePopoverDirection = (event) => {
if (event.target.nodeName === 'BUTTON') { if (event.target.nodeName === 'BUTTON') {
const popover = event.target.firstChild const popover = event.target.lastChild
const directions = ['right', 'above', 'below', 'left'] const directions = ['right', 'above', 'below', 'left']
for (let i = 0; i < directions.length; i++) { for (let i = 0; i < directions.length; i++) {
@ -537,16 +520,19 @@ export default class JSONNode extends PureComponent {
} }
renderExpandButton () { renderExpandButton () {
const className = `jsoneditor-button jsoneditor-${this.props.eson[EXPANDED] ? 'expanded' : 'collapsed'}` const expanded = this.props.eson[EXPANDED]
const className = `jsoneditor-button jsoneditor-${expanded ? 'expanded' : 'collapsed'}`
return h('div', {key: 'expand', className: 'jsoneditor-button-container'}, // unique key depending on expanded state is to force the fontawesome icon to update
return h('div', {key: expanded, className: 'jsoneditor-button-container'},
h('button', { h('button', {
className: className, className: className,
onClick: this.handleExpand, onClick: this.handleExpand,
title: title:
'Click to expand/collapse this field. \n' + 'Click to expand/collapse this field. \n' +
'Ctrl+Click to expand/collapse including all childs.' 'Ctrl+Click to expand/collapse including all childs.'
}) }, h('i', {
className: expanded ? 'fa fa-caret-down' : 'fa fa-caret-right'}))
) )
} }

View File

@ -1,13 +1,17 @@
import { createElement as h, Component } from 'react' import { Component, createElement as h } from 'react'
import Ajv from 'ajv' import Ajv from 'ajv'
import { parseJSON } from '../utils/jsonUtils' import { parseJSON } from '../utils/jsonUtils'
import { escapeUnicodeChars } from '../utils/stringUtils' import { escapeUnicodeChars } from '../utils/stringUtils'
import { enrichSchemaError, limitErrors } from '../utils/schemaUtils' import { enrichSchemaError, limitErrors } from '../utils/schemaUtils'
import { createFindKeyBinding } from '../utils/keyBindings' import { createFindKeyBinding } from '../utils/keyBindings'
import { KEY_BINDINGS } from '../constants' import { KEY_BINDINGS } from '../constants'
import ModeButton from './menu/ModeButton'
import { immutableJSONPatch } from '../immutableJSONPatch' import { immutableJSONPatch } from '../immutableJSONPatch'
import TextModeMenu from './menu/TextModeMenu'
import fontawesome from '@fortawesome/fontawesome'
import faExclamationTriangle from '@fortawesome/fontawesome-free-solid/faExclamationTriangle'
fontawesome.library.add(faExclamationTriangle)
const AJV_OPTIONS = { const AJV_OPTIONS = {
allErrors: true, allErrors: true,
@ -112,37 +116,15 @@ export default class TextMode extends Component {
/** @protected */ /** @protected */
renderMenu () { renderMenu () {
// TODO: move Menu into a separate Component return h(TextModeMenu, {
return h('div', {key: 'menu', className: 'jsoneditor-menu'}, [ mode: this.props.mode,
h('button', { modes: this.props.modes,
key: 'format', onChangeMode: this.props.onChangeMode,
className: 'jsoneditor-format',
title: 'Format the JSON document',
onClick: this.handleFormat
}),
h('button', {
key: 'compact',
className: 'jsoneditor-compact',
title: 'Compact the JSON document',
onClick: this.handleCompact
}),
// TODO: implement a button "Repair" onFormat: this.handleFormat,
onCompact: this.handleCompact,
h('div', { onRepair: this.handleRepair
key: 'separator', })
className: 'jsoneditor-vertical-menu-separator'
}),
this.props.modes && h(ModeButton, {
key: 'mode',
// TODO: simply pass all options?
modes: this.props.modes,
mode: this.props.mode,
onChangeMode: this.props.onChangeMode,
onError: this.props.onError
})
])
} }
/** @protected */ /** @protected */
@ -188,7 +170,8 @@ export default class TextMode extends Component {
* @return {JSX.Element} * @return {JSX.Element}
*/ */
static renderSchemaError (error, index) { static renderSchemaError (error, index) {
const icon = h('input', {type: 'button', className: 'jsoneditor-schema-error'}) const icon = h('button', {className: 'jsoneditor-schema-error'},
h('i', {className: 'fa fa-exclamation-triangle'}))
if (error && error.schema && error.schemaPath) { if (error && error.schema && error.schemaPath) {
// this is an ajv error message // this is an ajv error message
@ -301,6 +284,13 @@ export default class TextMode extends Component {
} }
} }
/** @protected */
handleRepair = () => {
// FIXME: implement repair button
console.log('handleRepair not yet implemented')
alert('sorry, not yet implemented...')
}
/** /**
* Apply new text to the state, and emit an onChangeText event if there is a change * Apply new text to the state, and emit an onChangeText event if there is a change
* @param {string} text * @param {string} text

View File

@ -30,8 +30,6 @@ import {
import JSONNode from './JSONNode' import JSONNode from './JSONNode'
import JSONNodeView from './JSONNodeView' import JSONNodeView from './JSONNodeView'
import JSONNodeForm from './JSONNodeForm' import JSONNodeForm from './JSONNodeForm'
import ModeButton from './menu/ModeButton'
import Search from './menu/Search'
import { import {
findBaseNode, findBaseNode,
findNode, findNode,
@ -64,6 +62,7 @@ import {
syncEson syncEson
} from '../eson' } from '../eson'
import TreeModeMenu from './menu/TreeModeMenu' import TreeModeMenu from './menu/TreeModeMenu'
import Search from './menu/Search'
const AJV_OPTIONS = { const AJV_OPTIONS = {
allErrors: true, allErrors: true,
@ -73,6 +72,7 @@ const AJV_OPTIONS = {
const MAX_HISTORY_ITEMS = 1000 // maximum number of undo/redo items to be kept in memory const MAX_HISTORY_ITEMS = 1000 // maximum number of undo/redo items to be kept in memory
const SCROLL_DURATION = 400 // milliseconds const SCROLL_DURATION = 400 // milliseconds
const SEARCH_DEBOUNCE = 300 // milliseconds
export default class TreeMode extends PureComponent { export default class TreeMode extends PureComponent {
id = Math.round(Math.random() * 1e5) // TODO: create a uuid here? id = Math.round(Math.random() * 1e5) // TODO: create a uuid here?
@ -136,6 +136,7 @@ export default class TreeMode extends PureComponent {
options: {}, options: {},
showSearch: false,
searchResult: { searchResult: {
text: '', text: '',
matches: null, matches: null,
@ -238,6 +239,8 @@ export default class TreeMode extends PureComponent {
}, [ }, [
this.renderMenu(), this.renderMenu(),
this.renderSearch(),
h('div', { h('div', {
key: 'contents', key: 'contents',
ref: 'contents', ref: 'contents',
@ -273,22 +276,30 @@ export default class TreeMode extends PureComponent {
modes: this.props.modes, modes: this.props.modes,
onChangeMode: this.props.onChangeMode, onChangeMode: this.props.onChangeMode,
onExpandAll: this.handleExpandAll,
onCollapseAll: this.handleCollapseAll,
enableHistory: this.props.history, enableHistory: this.props.history,
canUndo: this.canUndo(), canUndo: this.canUndo(),
canRedo: this.canRedo(), canRedo: this.canRedo(),
onUndo: this.undo, onUndo: this.undo,
onRedo: this.redo, onRedo: this.redo,
enableSearch: this.props.search, onToggleSearch: this.toggleSearch
searchResult: this.state.searchResult, })
onSearch: this.handleSearch, }
onSearchNext: this.handleNext,
onSearchPrevious: this.handlePrevious,
findKeyBinding: this.findKeyBinding, renderSearch () {
if (!this.state.showSearch) {
return null
}
return h(Search, {
text: this.state.searchResult.text,
resultCount: this.state.searchResult.matches
? this.state.searchResult.matches.length : 0,
onChange: this.handleSearch,
onNext: this.handleNext,
onPrevious: this.handlePrevious,
findKeyBinding: this.props.findKeyBinding,
delay: SEARCH_DEBOUNCE
}) })
} }
@ -602,6 +613,12 @@ export default class TreeMode extends PureComponent {
}) })
} }
toggleSearch = () => {
this.setState({
showSearch: !this.state.showSearch
})
}
handleSearch = (text) => { handleSearch = (text) => {
// FIXME // FIXME
// FIXME: also apply search when eson is changed // FIXME: also apply search when eson is changed

View File

@ -17,74 +17,6 @@ $insert-area-height: 6px;
line-height: normal; line-height: normal;
} }
.jsoneditor-menu {
width: 100%;
box-sizing: border-box;
color: white;
background-color: $theme-color;
flex: 0 0 auto;
button {
width: 26px;
height: 26px;
margin: 2px;
padding: 0;
border-radius: 2px;
border: 1px solid transparent;
background: transparent url('img/jsoneditor-icons.svg');
color: white;
opacity: 0.8;
font-family: arial, sans-serif;
font-size: 10pt;
}
button:hover {
background-color: rgba(255,255,255,0.2);
border: 1px solid rgba(255,255,255,0.4);
}
button:focus,
button:active {
background-color: rgba(255,255,255,0.3);
}
button:disabled {
opacity: 0.5;
}
.jsoneditor-vertical-menu-separator {
width: 8px;
display: inline-block;
}
button.jsoneditor-expand-all {
background-position: 0 -120px;
}
button.jsoneditor-collapse-all {
background-position: 0 -96px;
}
button.jsoneditor-undo {
background-position: -24px -96px;
}
button.jsoneditor-undo:disabled {
background-position: -24px -120px;
}
button.jsoneditor-redo {
background-position: -48px -96px;
}
button.jsoneditor-redo:disabled {
background-position: -48px -120px;
}
button.jsoneditor-compact {
background-position: -72px -96px;
}
button.jsoneditor-format {
background-position: -72px -120px;
}
}
.jsoneditor-contents { .jsoneditor-contents {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -337,7 +269,9 @@ button.jsoneditor-button {
margin: 0; margin: 0;
border: none; border: none;
cursor: pointer; cursor: pointer;
background: transparent url('img/jsoneditor-icons.svg'); background: transparent;
color: $gray-icon;
font-size: 16px;
} }
button.jsoneditor-button:focus { button.jsoneditor-button:focus {
@ -349,31 +283,6 @@ button.jsoneditor-button:focus {
outline: #e5e5e5 solid 1px; 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;
}
button.jsoneditor-button.jsoneditor-drag {
background-position: -74px -74px;
cursor: move;
}
button.jsoneditor-button.jsoneditor-actionmenu {
background-position: -50px -74px;
}
button.jsoneditor-button.jsoneditor-actionmenu:hover,
button.jsoneditor-button.jsoneditor-actionmenu:focus,
button.jsoneditor-button.jsoneditor-actionmenu.jsoneditor-visible {
background-position: -50px -50px;
}
/*********************************** Menu *************************************/ /*********************************** Menu *************************************/
div.jsoneditor-menu-panel-right { div.jsoneditor-menu-panel-right {
@ -445,159 +354,6 @@ button.jsoneditor-menu-button.jsoneditor-selected {
button.jsoneditor-menu-default { button.jsoneditor-menu-default {
width: 104px; /* 136px - 32px */ width: 104px; /* 136px - 32px */
} }
//
//button.jsoneditor-menu-expand {
// width: 32px;
// float: right;
// border-left: 1px solid #e5e5e5;
//}
//span.jsoneditor-icon {
// float: left;
// width: 24px;
// height: 24px;
// border: none;
// padding: 0;
// margin: 0;
// background-image: url('img/jsoneditor-icons.svg');
//}
//
//span.jsoneditor-icon.jsoneditor-icon-expand {
// float: right;
// width: 24px;
// margin: 0 4px;
//
// background-position: 0 -72px !important;
// opacity: 0.4;
//}
//
//div.jsoneditor-menu-item button.jsoneditor-menu-button:hover span.jsoneditor-icon-expand,
//div.jsoneditor-menu-item button:focus span.jsoneditor-icon-expand {
// opacity: 1;
//}
//
//span.jsoneditor-text {
// display: inline-block;
// line-height: 24px;
//}
//
//div.jsoneditor-menu-separator {
// height: 0;
// border-top: 1px solid #e5e5e5;
// padding-top: 5px;
// margin-top: 5px;
//}
//
//button.jsoneditor-remove span.jsoneditor-icon {
// background-position: -24px -24px;
//}
//button.jsoneditor-remove:hover span.jsoneditor-icon,
//button.jsoneditor-remove:focus span.jsoneditor-icon {
// background-position: -24px 0;
//}
//
//button.jsoneditor-insert span.jsoneditor-icon {
// background-position: 0 -24px;
//}
//button.jsoneditor-insert:hover span.jsoneditor-icon,
//button.jsoneditor-insert:focus span.jsoneditor-icon {
// background-position: 0 0;
//}
//
//button.jsoneditor-duplicate span.jsoneditor-icon {
// background-position: -48px -24px;
//}
//button.jsoneditor-duplicate:hover span.jsoneditor-icon,
//button.jsoneditor-duplicate:focus span.jsoneditor-icon {
// background-position: -48px 0;
//}
//
//button.jsoneditor-sort-asc span.jsoneditor-icon {
// background-position: -168px -24px;
//}
//button.jsoneditor-sort-asc:hover span.jsoneditor-icon,
//button.jsoneditor-sort-asc:focus span.jsoneditor-icon {
// background-position: -168px 0;
//}
//
//button.jsoneditor-sort-desc span.jsoneditor-icon {
// background-position: -192px -24px;
//}
//button.jsoneditor-sort-desc:hover span.jsoneditor-icon,
//button.jsoneditor-sort-desc:focus span.jsoneditor-icon {
// background-position: -192px 0;
//}
//
//div.jsoneditor-submenu {
// visibility: hidden;
// max-height: 0;
//
// overflow: hidden;
//
// transition: max-height 0.3s ease-out;
//
// box-shadow: inset 0 10px 10px -10px rgba(128, 128, 128, 0.5),
// inset 0 -10px 10px -10px rgba(128, 128, 128, 0.5)
//}
//
//div.jsoneditor-submenu.jsoneditor-expanded {
// visibility: visible;
// max-height: 104px; /* 4 * 24px + 2 * 5px */
// /* FIXME: shouldn't rely on max-height equal to 4 items, should be flexible */
//}
//
//div.jsoneditor-submenu.jsoneditor-collapsing {
// visibility: visible;
// max-height: 0;
//}
//
//div.jsoneditor-submenu button {
// padding-left: 24px;
//}
//
//div.jsoneditor-submenu div.jsoneditor-menu-item:first-child {
// margin-top: 5px;
//}
//
//div.jsoneditor-submenu div.jsoneditor-menu-item:last-child {
// margin-bottom: 5px;
//}
//
//button.jsoneditor-type-string span.jsoneditor-icon {
// background-position: -144px -24px;
//}
//button.jsoneditor-type-string:hover span.jsoneditor-icon,
//button.jsoneditor-type-string:focus span.jsoneditor-icon,
//button.jsoneditor-type-string.jsoneditor-selected span.jsoneditor-icon {
// background-position: -144px 0;
//}
//
//button.jsoneditor-type-value span.jsoneditor-icon {
// background-position: -120px -24px;
//}
//button.jsoneditor-type-value:hover span.jsoneditor-icon,
//button.jsoneditor-type-value:focus span.jsoneditor-icon,
//button.jsoneditor-type-value.jsoneditor-selected span.jsoneditor-icon {
// background-position: -120px 0;
//}
//
//button.jsoneditor-type-Object span.jsoneditor-icon {
// background-position: -72px -24px;
//}
//button.jsoneditor-type-Object:hover span.jsoneditor-icon,
//button.jsoneditor-type-Object:focus span.jsoneditor-icon,
//button.jsoneditor-type-Object.jsoneditor-selected span.jsoneditor-icon {
// background-position: -72px 0;
//}
//
//button.jsoneditor-type-Array span.jsoneditor-icon {
// background-position: -96px -24px;
//}
//button.jsoneditor-type-Array:hover span.jsoneditor-icon,
//button.jsoneditor-type-Array:focus span.jsoneditor-icon,
//button.jsoneditor-type-Array.jsoneditor-selected span.jsoneditor-icon {
// background-position: -96px 0;
//}
/******************************* Floating Menu **********************************/ /******************************* Floating Menu **********************************/
@ -665,39 +421,6 @@ div.jsoneditor-node-container {
} }
} }
div.jsoneditor-insert {
width: $line-height;
height: $line-height;
&:hover {
background: url('img/jsoneditor-icons.svg') -2px -26px;
}
}
&.jsoneditor-selected-insert-after {
border-bottom: 1px dashed $light-gray;
margin-bottom: -1px;
> .jsoneditor-node > .jsoneditor-insert-after,
> .jsoneditor-node-end > .jsoneditor-insert-after {
background-image: url('img/jsoneditor-icons.svg');
background-position: -2px -2px !important;
}
}
&.jsoneditor-selected-insert-before {
> .jsoneditor-node > .jsoneditor-insert-before,
> .jsoneditor-node-end > .jsoneditor-insert-before {
background-image: url('img/jsoneditor-icons.svg');
background-position: -2px -2px !important;
}
> .jsoneditor-list {
border-top: 1px dashed $light-gray;
margin-top: -1px;
}
}
&.jsoneditor-hover { &.jsoneditor-hover {
> .jsoneditor-node > .jsoneditor-button-container, > .jsoneditor-node > .jsoneditor-button-container,
> .jsoneditor-node > .jsoneditor-button-placeholder { > .jsoneditor-node > .jsoneditor-button-placeholder {
@ -712,10 +435,8 @@ div.jsoneditor-node-container {
//padding-left: $line-height + $input-padding + 2px; //padding-left: $line-height + $input-padding + 2px;
border-left-color: $hoverColor; border-left-color: $hoverColor;
} }
} }
div.jsoneditor-floating-menu { div.jsoneditor-floating-menu {
position: absolute; position: absolute;
bottom: 100%; bottom: 100%;
@ -782,15 +503,16 @@ div.jsoneditor-node-container {
} }
/******************************* **********************************/ /******************************* **********************************/
div.jsoneditor-modes { div.jsoneditor-modes {
position: relative; position: relative;
display: inline-block; display: inline-block;
vertical-align: top;
button { button {
background: none; background: none;
width: auto; width: auto;
padding: 2px 6px; padding: 2px 6px;
font-size: $fontSize;
} }
button.jsoneditor-type-modes { button.jsoneditor-type-modes {
@ -855,7 +577,7 @@ div.jsoneditor-code {
} }
.jsoneditor-schema-error { button.jsoneditor-schema-error {
//user-select: none; //user-select: none;
outline: none; outline: none;
border: none; border: none;
@ -863,6 +585,7 @@ div.jsoneditor-code {
height: 20px; height: 20px;
padding: 0; padding: 0;
margin: 0 4px; margin: 0 4px;
background: url('img/jsoneditor-icons.svg') -171px -49px; background: transparent;
color: $warning-color;
cursor: pointer; cursor: pointer;
} }

View File

@ -0,0 +1,16 @@
@import '../style.scss';
@import './MenuButton.scss';
.jsoneditor-menu {
// TODO: there is also css for this menu in jsoneditor.scss, move this here
width: 100%;
box-sizing: border-box;
color: white;
background-color: $theme-color;
flex: 0 0 auto;
.jsoneditor-menu-group {
display: inline-block;
}
}

View File

@ -0,0 +1,36 @@
.jsoneditor-menu {
.jsoneditor-menu-group:not(:first-child) {
border-left: 1px solid rgba(255,255,255, 0.1);
margin-left: 5px;
padding-left: 5px;
}
button {
width: 26px;
height: 26px;
margin: 2px;
padding: 0;
border-radius: 2px;
border: 1px solid transparent;
background: transparent;
color: white;
opacity: 0.8;
font-family: arial, sans-serif;
font-size: 16px;
}
button:hover {
background-color: rgba(255,255,255,0.2);
border: 1px solid rgba(255,255,255,0.4);
}
button:focus,
button:active {
background-color: rgba(255,255,255,0.3);
}
button:disabled {
opacity: 0.5;
}
}

View File

@ -3,8 +3,17 @@ import PropTypes from 'prop-types'
import { keyComboFromEvent } from '../../utils/keyBindings' import { keyComboFromEvent } from '../../utils/keyBindings'
import { findEditorContainer, setSelection } from '../utils/domSelector' import { findEditorContainer, setSelection } from '../utils/domSelector'
import fontawesome from '@fortawesome/fontawesome'
import faSearch from '@fortawesome/fontawesome-free-solid/faSearch'
import faCaretUp from '@fortawesome/fontawesome-free-solid/faCaretUp'
import faCaretDown from '@fortawesome/fontawesome-free-solid/faCaretDown'
import './Menu.css'
import './Search.css' import './Search.css'
fontawesome.library.add(faSearch, faCaretUp, faCaretDown)
export default class Search extends Component { export default class Search extends Component {
constructor (props) { constructor (props) {
super (props) super (props)
@ -16,12 +25,14 @@ export default class Search extends Component {
render () { render () {
return h('div', {className: 'jsoneditor-search'}, [ return h('div', {className: 'jsoneditor-search'}, [
this.renderResultsCount(this.props.resultCount),
h('form', { h('form', {
key: 'box', key: 'box',
className: 'jsoneditor-search-box', className: 'jsoneditor-search-box',
onSubmit: this.handleSubmit onSubmit: this.handleSubmit
}, [ }, [
h('div', { className: 'jsoneditor-search-icon' }, [
h('i', {className: 'fa fa-search'})
]),
h('input', { h('input', {
key: 'input', key: 'input',
type: 'text', type: 'text',
@ -30,21 +41,22 @@ export default class Search extends Component {
onInput: this.handleChange, onInput: this.handleChange,
onKeyDown: this.handleKeyDown onKeyDown: this.handleKeyDown
}), }),
h('input', { h('button', {
key: 'next', key: 'next',
type: 'button', type: 'button',
className: 'jsoneditor-search-next', className: 'jsoneditor-search-next',
title: 'Next result', title: 'Next result',
onClick: this.props.onNext onClick: this.props.onNext
}), }, h('i', {className: 'fa fa-caret-down'})),
h('input', { h('button', {
key: 'previous', key: 'previous',
type: 'button', type: 'button',
className: 'jsoneditor-search-previous', className: 'jsoneditor-search-previous',
title: 'Previous result', title: 'Previous result',
onClick: this.props.onPrevious onClick: this.props.onPrevious
}) }, h('i', {className: 'fa fa-caret-up'}))
]) ]),
this.renderResultsCount(this.props.resultCount)
]) ])
} }
@ -54,13 +66,13 @@ export default class Search extends Component {
} }
if (resultCount === 0) { if (resultCount === 0) {
return h('div', {key: 'count', className: 'jsoneditor-results'}, '(no results)') return h('div', {key: 'count', className: 'jsoneditor-search-results'}, '(no results)')
} }
if (resultCount > 0) { if (resultCount > 0) {
const suffix = resultCount === 1 ? ' result' : ' results' const suffix = resultCount === 1 ? ' result' : ' results'
return h('div', {key: 'count', className: 'jsoneditor-results'}, resultCount + suffix) return h('div', {key: 'count', className: 'jsoneditor-search-results'}, resultCount + suffix)
} }
return null return null

View File

@ -1,13 +1,12 @@
@import '../style.scss';
$theme-color: #3883fa; $theme-color: #3883fa;
div.jsoneditor-search { div.jsoneditor-search {
font-family: arial, sans-serif; position: relative;
font-size: 10pt; background: $theme-color;
font-family: $fontFamilyMenu;
div.jsoneditor-results { font-size: $fontSize;
display: inline-block;
margin-right: 5px;
}
form.jsoneditor-search-box { form.jsoneditor-search-box {
display: inline-flex; display: inline-flex;
@ -15,18 +14,22 @@ div.jsoneditor-search {
max-width: 100%; max-width: 100%;
background-color: white; background-color: white;
border: 2px solid $theme-color; border: 1px solid $theme-color;
border-left-width: 0;
box-sizing: border-box; box-sizing: border-box;
$search-icon-width: 22px; $search-icon-width: 28px;
$search-icon-padding: 4px;
&::before { div.jsoneditor-search-icon {
position: absolute; position: absolute;
display: inline-block;
width: $search-icon-width; width: $search-icon-width;
height: 100%; height: $search-icon-width;
background: transparent url('../img/jsoneditor-icons.svg') -97px -71px; color: $light-gray;
content: ''; font-size: 16px;
padding: $search-icon-padding;
line-height: $search-icon-width - 2 * $search-icon-padding;
box-sizing: border-box;
} }
input.jsoneditor-search-text { input.jsoneditor-search-text {
@ -43,42 +46,28 @@ div.jsoneditor-search {
background: transparent; background: transparent;
} }
input[type=button] { button {
display: inline-block; display: inline-block;
position: relative; position: relative;
cursor: pointer;
width: 16px; width: 16px;
height: 100%; height: 100%;
line-height: 22px; line-height: 28px;
margin: 2px 0; background: transparent;
padding: 0;
border: none; border: none;
background: transparent url('../img/jsoneditor-icons.svg'); outline: none;
opacity: 0.8; color: $light-gray;
font-family: arial, sans-serif; font-family: arial, sans-serif;
font-size: 10pt; font-size: 16px;
} padding: 0;
input[type=button]:hover {
background-color: transparent;
}
input.jsoneditor-search-next {
cursor: pointer;
background-position: -124px -73px;
}
input.jsoneditor-search-next:hover {
background-position: -124px -49px;
}
input.jsoneditor-search-previous {
cursor: pointer;
background-position: -148px -73px;
margin-right: 2px;
}
input.jsoneditor-search-previous:hover {
background-position: -148px -49px;
} }
} }
div.jsoneditor-search-results {
display: inline-block;
margin-left: 5px;
color: white;
}
} }

View File

@ -0,0 +1,68 @@
import { createElement as h, PureComponent } from 'react'
import ModeButton from './ModeButton'
import PropTypes from 'prop-types'
import fontawesome from '@fortawesome/fontawesome'
import faAlignLeft from '@fortawesome/fontawesome-free-solid/faAlignLeft'
import faAlignJustify from '@fortawesome/fontawesome-free-solid/faAlignJustify'
import faScrewdriver from '@fortawesome/fontawesome-free-solid/faScrewdriver'
import './Menu.css'
fontawesome.library.add(faAlignLeft, faAlignJustify, faScrewdriver)
export default class TreeModeMenu extends PureComponent {
static propTypes = {
mode: PropTypes.string.isRequired,
modes: PropTypes.arrayOf(PropTypes.string),
onChangeMode: PropTypes.func.isRequired,
onFormat: PropTypes.func.isRequired,
onCompact: PropTypes.func.isRequired,
onRepair: PropTypes.func.isRequired
}
render () {
let items = []
// mode
if (this.props.modes ) {
items = items.concat([
h('div', {className: 'jsoneditor-menu-group'}, [
h(ModeButton, {
key: 'mode',
modes: this.props.modes,
mode: this.props.mode,
onChangeMode: this.props.onChangeMode,
onError: this.props.onError
})
])
])
}
// format / compact / repair
items = items.concat([
h('button', {
key: 'format',
className: 'jsoneditor-format',
title: 'Format the JSON document',
onClick: this.props.onFormat
}, h('i', {className: 'fa fa-align-left'})),
h('button', {
key: 'compact',
className: 'jsoneditor-compact',
title: 'Compact the JSON document',
onClick: this.props.onCompact
}, h('i', {className: 'fa fa-align-justify'})),
h('button', {
key: 'repair',
className: 'jsoneditor-repair',
title: 'Repair the JSON document',
onClick: this.props.onRepair
}, h('i', {className: 'fa fa-screwdriver'})),
])
return h('div', {key: 'menu', className: 'jsoneditor-menu'}, items)
}
}

View File

@ -1,9 +1,31 @@
import { createElement as h, PureComponent } from 'react' import { createElement as h, PureComponent } from 'react'
import ModeButton from './ModeButton' import ModeButton from './ModeButton'
import Search from './Search'
import PropTypes from 'prop-types' import PropTypes from 'prop-types'
const SEARCH_DEBOUNCE = 300 // milliseconds import fontawesome from '@fortawesome/fontawesome'
import faPlusSquare from '@fortawesome/fontawesome-free-solid/faPlusSquare'
import faMinusSquare from '@fortawesome/fontawesome-free-solid/faMinusSquare'
import faCut from '@fortawesome/fontawesome-free-solid/faCut'
import faCopy from '@fortawesome/fontawesome-free-solid/faCopy'
import faPaste from '@fortawesome/fontawesome-free-solid/faPaste'
import faPlus from '@fortawesome/fontawesome-free-solid/faPlus'
import faClone from '@fortawesome/fontawesome-free-solid/faClone'
import faTimes from '@fortawesome/fontawesome-free-solid/faTimes'
import faUndo from '@fortawesome/fontawesome-free-solid/faUndo'
import faRedo from '@fortawesome/fontawesome-free-solid/faRedo'
import faSortAmountDown from '@fortawesome/fontawesome-free-solid/faSortAmountDown'
import faFilter from '@fortawesome/fontawesome-free-solid/faFilter'
import faSearch from '@fortawesome/fontawesome-free-solid/faSearch'
import './Menu.css'
fontawesome.library.add(
faPlusSquare, faMinusSquare,
faCut, faCopy, faPaste,
faPlus, faClone, faTimes,
faUndo, faRedo,
faSortAmountDown, faFilter, faSearch
)
export default class TreeModeMenu extends PureComponent { export default class TreeModeMenu extends PureComponent {
@ -12,8 +34,12 @@ export default class TreeModeMenu extends PureComponent {
modes: PropTypes.arrayOf(PropTypes.string), modes: PropTypes.arrayOf(PropTypes.string),
onChangeMode: PropTypes.func.isRequired, onChangeMode: PropTypes.func.isRequired,
onExpandAll: PropTypes.func.isRequired, canCut: PropTypes.bool.isRequired,
onCollapseAll: PropTypes.func.isRequired, canCopy: PropTypes.bool.isRequired,
canPaste: PropTypes.bool.isRequired,
onCut: PropTypes.func.isRequired,
onCopy: PropTypes.func.isRequired,
onPaste: PropTypes.func.isRequired,
enableHistory: PropTypes.bool, enableHistory: PropTypes.bool,
canUndo: PropTypes.bool, canUndo: PropTypes.bool,
@ -21,80 +47,124 @@ export default class TreeModeMenu extends PureComponent {
onUndo: PropTypes.func, onUndo: PropTypes.func,
onRedo: PropTypes.func, onRedo: PropTypes.func,
enableSearch: PropTypes.bool, onToggleSearch: PropTypes.func
searchResult: PropTypes.string,
onSearch: PropTypes.func,
onSearchNext: PropTypes.func,
onSearchPrevious: PropTypes.func,
findKeyBinding: PropTypes.func.isRequired,
} }
render () { render () {
let items = [ let items = []
h('button', {
key: 'expand-all',
className: 'jsoneditor-expand-all',
title: 'Expand all objects and arrays',
onClick: this.props.onExpandAll
}),
h('button', {
key: 'collapse-all',
className: 'jsoneditor-collapse-all',
title: 'Collapse all objects and arrays',
onClick: this.props.onCollapseAll
})
]
if (this.props.mode !== 'view' && this.props.enableHistory !== false) {
items = items.concat([
h('div', {key: 'history-separator', className: 'jsoneditor-vertical-menu-separator'}),
h('button', {
key: 'undo',
className: 'jsoneditor-undo',
title: 'Undo last action',
disabled: !this.props.canUndo,
onClick: this.props.onUndo
}),
h('button', {
key: 'redo',
className: 'jsoneditor-redo',
title: 'Redo',
disabled: !this.props.canRedo,
onClick: this.props.onRedo
})
])
}
// mode
if (this.props.modes ) { if (this.props.modes ) {
items = items.concat([ items = items.concat([
h('div', {key: 'mode-separator', className: 'jsoneditor-vertical-menu-separator'}), h('div', {className: 'jsoneditor-menu-group'}, [
h(ModeButton, {
h(ModeButton, { key: 'mode',
key: 'mode', modes: this.props.modes,
modes: this.props.modes, mode: this.props.mode,
mode: this.props.mode, onChangeMode: this.props.onChangeMode,
onChangeMode: this.props.onChangeMode, onError: this.props.onError
onError: this.props.onError })
}) ])
]) ])
} }
if (this.props.enableSearch !== false) { // cut / copy / paste
// option search is true or undefined items = items.concat([
h('div', {className: 'jsoneditor-menu-group'}, [
h('button', {
key: 'cut',
className: 'jsoneditor-cut',
title: 'Cut current selection',
disabled: !this.props.canCut,
onClick: this.props.onCut
}, h('i', {className: 'fa fa-cut'})),
h('button', {
key: 'copy',
className: 'jsoneditor-copy',
title: 'Copy current selection',
// disabled: !this.props.canPaste,
onClick: this.props.onPaste
}, h('i', {className: 'fa fa-copy'})),
h('button', {
key: 'paste',
className: 'jsoneditor-paste',
title: 'Paste copied selection',
// disabled: !this.props.canPaste,
onClick: this.props.onPaste
}, h('i', {className: 'fa fa-paste'}))
])
])
// TODO: [insert structure / insert value / insert array / insert object] / duplicate / remove
items = items.concat([
h('div', {className: 'jsoneditor-menu-group'}, [
h('button', {
key: 'insert',
className: 'jsoneditor-insert',
title: 'Insert new contents',
onClick: this.props.onInsert
}, h('i', {className: 'fa fa-plus'})),
h('button', {
key: 'duplicate',
className: 'jsoneditor-duplicate',
title: 'Duplicate current selection',
disabled: !this.props.canDuplicate,
onClick: this.props.onDuplicate
}, h('i', {className: 'fa fa-clone'})),
h('button', {
key: 'remove',
className: 'jsoneditor-remove',
title: 'Remove selection',
disabled: !this.props.canRemove,
onClick: this.props.onRemove
}, h('i', {className: 'fa fa-times'}))
])
])
// sort / transform
items = items.concat([
h('div', {className: 'jsoneditor-menu-group'}, [
h('button', {
key: 'sort',
className: 'jsoneditor-sort',
title: 'Sort contents',
onClick: this.props.onSort // TODO: implement onSort
}, h('i', {className: 'fa fa-sort-amount-down'})),
h('button', {
key: 'transform',
className: 'jsoneditor-transform',
title: 'Transform contents',
// disabled: !this.props.canPaste,
onClick: this.props.onTransform
}, h('i', {className: 'fa fa-filter'})),
h('button', {
key: 'search',
className: 'jsoneditor-search',
title: 'Search and replace',
onClick: this.props.onToggleSearch
}, h('i', {className: 'fa fa-search'}))
])
])
// undo / redo
if (this.props.mode !== 'view' && this.props.enableHistory !== false) {
items = items.concat([ items = items.concat([
h('div', {key: 'search', className: 'jsoneditor-menu-panel-right'}, h('div', {className: 'jsoneditor-menu-group'}, [
h(Search, { h('button', {
text: this.props.searchResult.text, key: 'undo',
resultCount: this.props.searchResult.matches ? this.props.searchResult.matches.length : 0, className: 'jsoneditor-undo',
onChange: this.props.onSearch, title: 'Undo last action',
onNext: this.props.onSearchNext, disabled: !this.props.canUndo,
onPrevious: this.props.onSearchPrevious, onClick: this.props.onUndo
findKeyBinding: this.props.findKeyBinding, }, h('i', {className: 'fa fa-undo'})),
delay: SEARCH_DEBOUNCE h('button', {
}) key: 'redo',
) className: 'jsoneditor-redo',
title: 'Redo',
disabled: !this.props.canRedo,
onClick: this.props.onRedo
}, h('i', {className: 'fa fa-redo'}))
])
]) ])
} }

View File

@ -1,6 +1,8 @@
$fontFamily: "dejavu sans mono", "droid sans mono", consolas, monaco, "lucida console", "courier new", courier, monospace, sans-serif; $fontFamily: "dejavu sans mono", "droid sans mono", consolas, monaco, "lucida console", "courier new", courier, monospace, sans-serif;
$fontSize: 10pt; $fontSize: 10pt;
$fontFamilyMenu: arial, "sans-serif";
$black: #1A1A1A; $black: #1A1A1A;
$contentsMinHeight: 150px; $contentsMinHeight: 150px;
$theme-color: #3883fa; $theme-color: #3883fa;
@ -10,6 +12,8 @@ $floating-menu-color: #fff;
$selectedColor: #ffed99; $selectedColor: #ffed99;
$hoverColor: #d3d3d3; $hoverColor: #d3d3d3;
$hoverAndSelectedColor: #ffdb80; $hoverAndSelectedColor: #ffdb80;
$warning-color: #FBB917;
$gray: #9d9d9d; $gray: #9d9d9d;
$gray-icon: #5e5e5e;
$light-gray: #c0c0c0; $light-gray: #c0c0c0;
$input-padding: 5px; $input-padding: 5px;

View File

@ -158,7 +158,7 @@ export function findParentWithClassName (element, className) {
* @return {boolean} * @return {boolean}
*/ */
export function hasClassName (element, className) { export function hasClassName (element, className) {
return element && element.className return element && element.className && element.className.split
? element.className.split(' ').indexOf(className) !== -1 ? element.className.split(' ').indexOf(className) !== -1
: false : false
} }