Compare commits

...

171 Commits

Author SHA1 Message Date
Jos de Jong 8cec2df3c7 Implement flexible expansion of parts of a large array 2020-10-14 17:52:51 +02:00
Jos de Jong 002453cc92 Expand first item of an array by default 2020-10-10 14:22:00 +02:00
Jos de Jong cc99284052 Refactor API of asyncSearch 2020-10-10 14:11:38 +02:00
Jos de Jong adda5836af Limit number of search results 2020-10-10 14:08:01 +02:00
Jos de Jong 3f9a7def75 throttle search progress again 2020-09-30 11:59:21 +02:00
Jos de Jong a20359c6a8 Remove throttling, report onProgress less often 2020-09-30 11:54:48 +02:00
Jos de Jong df853f6370 Fix not being able to paste inside SearchBox input using Ctrl+V 2020-09-30 11:49:47 +02:00
Jos de Jong b87286e002 Integrate async search (WIP) 2020-09-30 11:44:05 +02:00
Jos de Jong d88936b9dc Silence eslint 2020-09-23 08:20:26 +02:00
Jos de Jong 6fb6bc68e6 Rename onProgress callback 2020-09-23 08:18:32 +02:00
Jos de Jong fd673a4ea0 Implement asyncSearch (WIP) 2020-09-16 21:19:51 +02:00
Jos de Jong ac078e445b Create empty array on inserted structure containing an array 2020-09-16 13:33:56 +02:00
Jos de Jong 34534bba0c Improve responsiveness of modal 2020-09-16 12:15:31 +02:00
Jos de Jong 1bf3f7e1c2 Remember wizard state 2020-09-16 10:27:47 +02:00
Jos de Jong 78411371e6 Collapsable wizard 2020-09-16 10:10:46 +02:00
Jos de Jong 5033194313 Fix styling of select boxes in wizard 2020-09-16 09:53:02 +02:00
Jos de Jong 08d49388ed Fix orderBy query generation 2020-09-02 20:30:57 +02:00
Jos de Jong 22f429e01b Implement transform wizard (WIP) 2020-09-02 20:24:11 +02:00
Jos de Jong d67fffc7d5 Cleanup logging 2020-09-02 16:06:26 +02:00
Jos de Jong 45f8881e96 Allow using lodash functions in transform query 2020-09-02 12:14:48 +02:00
Jos de Jong 023c57f492 Linting fixes 2020-09-02 11:39:50 +02:00
Jos de Jong d3c3d47ec0 Debounce and limit transform preview 2020-09-02 11:29:09 +02:00
Jos de Jong 01579dc115 Remember previous query when opening transform modal again 2020-09-02 11:08:05 +02:00
Jos de Jong 350969638c Keep the expanded state of the transformed node 2020-09-02 10:59:19 +02:00
Jos de Jong 00d0099355 Implement Transform modal (WIP) 2020-09-02 10:25:29 +02:00
Jos de Jong ad35e6fc16 Do not expand sorted array 2020-09-02 09:28:41 +02:00
Jos de Jong 3cf43de54c Remember state of SortModal properties 2020-09-02 09:23:16 +02:00
Jos de Jong a24045cb42 Refactor sorting to return a JSONPatchDocument 2020-08-30 14:38:12 +02:00
Jos de Jong a28335fe57 Add eslint to the project 2020-08-30 14:11:24 +02:00
Jos de Jong 763b0b7c3c Refactor sortMoveOperations a bit 2020-08-23 18:14:51 +02:00
Jos de Jong dff1fc0811 Implement first simple version of sortMoveOperations 2020-08-23 18:12:07 +02:00
Jos de Jong a848358815 Clean up console.log 2020-08-23 18:11:52 +02:00
Jos de Jong ec581ad674 Add unit tests for sorting functions 2020-08-23 17:30:05 +02:00
Jos de Jong aeb3374406 Move sortArray and sortObjectKeys into a separate file 2020-08-23 16:47:11 +02:00
Jos de Jong 82c81e7b4c Get undo/redo working when sorting 2020-08-23 16:39:06 +02:00
Jos de Jong 41d5ab5786 Fix immutableJSONPatch not handling root path 2020-08-23 16:27:29 +02:00
Jos de Jong 058cf6e104 CSS tweak 2020-08-23 14:56:28 +02:00
Jos de Jong 89eaf2f264 Implement sort a nested path if provided 2020-08-23 14:53:42 +02:00
Jos de Jong 456f1912fd Implement some logic to sort object keys 2020-08-19 21:37:15 +02:00
Jos de Jong bf4d0907f0 Styling tweak 2020-08-19 16:50:28 +02:00
Jos de Jong 1ad834cf31 Improve styling of the modal and select boxes 2020-08-19 16:45:12 +02:00
Jos de Jong 37fcdf85e2 Implement SortModal and TransformModal (WIP) 2020-08-16 21:43:21 +02:00
Jos de Jong 8256bd637c Small Fix in JSONEditor 2020-08-16 11:54:03 +02:00
Jos de Jong 8b99527604 Implement option mode 2020-08-16 11:44:28 +02:00
Jos de Jong f2b2769727 Fix indentation 2020-08-16 11:34:50 +02:00
Jos de Jong baa336981f Create main component JSONEditor and mode component TreeMode. Integrate svelte-simple-modal 2020-08-16 11:34:02 +02:00
Jos de Jong a3c0da7d95 Change validation-errors constistently to buttons 2020-08-13 15:35:13 +02:00
Jos de Jong 735702ba99 Styling tweak 2020-08-13 15:33:12 +02:00
Jos de Jong 1477aaaa4b Highlight object when hovering, select node only when clicking inside one of the selectable areas 2020-08-13 15:29:51 +02:00
Jos de Jong 22845a0cc8 Minor css tweaks 2020-08-06 15:52:55 +02:00
Jos de Jong e36ba7a384 Fix indentation of append-node-selector 2020-07-30 16:14:18 +02:00
Jos de Jong a29c196733 Only show validation error on parent when collapsed 2020-07-30 14:55:49 +02:00
Jos de Jong bb2dcf0039 Add Ace to the project (not yet used) 2020-07-30 11:42:11 +02:00
Jos de Jong 98f7cacb55 Implement support for JSON Schema validation 2020-07-30 11:41:35 +02:00
Jos de Jong b30aac082b Minor tweaks 2020-07-29 22:03:30 +02:00
Jos de Jong 9356f44c95 Implement option `validate` and method `setValidator` (WIP) 2020-07-29 21:52:46 +02:00
Jos de Jong 300e46b149 Update all dependencies 2020-07-27 16:49:31 +02:00
Jos de Jong a830dab67f Fix typo 2020-07-27 16:21:54 +02:00
Jos de Jong a54e5b6f08 Implement menu item "Remove" 2020-07-27 16:21:09 +02:00
Jos de Jong 0c418ac846 Refactoring in duplicate 2020-07-27 16:15:16 +02:00
Jos de Jong eea6e09bd8 Implement insert 2020-07-27 16:05:51 +02:00
Jos de Jong a6bb790f5e Create dropdown menu (WIP) 2020-07-27 13:45:23 +02:00
Jos de Jong c441663528 Move singleton object into JSONNode 2020-07-27 09:44:09 +02:00
Jos de Jong b99d4b4d5d Reorganize files in folders /components and /logic 2020-07-27 09:42:15 +02:00
Jos de Jong e629b404a6 Move Menu into a separate component 2020-07-26 12:00:14 +02:00
Jos de Jong e23e4a82dd Make searchResult reactive 2020-07-26 11:31:38 +02:00
Jos de Jong ad3ac339cf Fix clearing search result when closing search box 2020-07-26 11:11:46 +02:00
Jos de Jong bd73739343 Fix existsIn not working for symbols attached to arrays 2020-07-26 11:09:29 +02:00
Jos de Jong 23067b4638 Fix expanding nested search results 2020-07-26 10:56:04 +02:00
Jos de Jong ad4572d21e Extend `setIn` with optional support for creating missing path 2020-07-26 10:45:56 +02:00
Jos de Jong b9ceec09e3 Change dashed-line color to gray 2020-07-24 20:30:23 +02:00
Jos de Jong 2961e0d910 Move expandPath to stateUtils.js 2020-07-22 17:28:33 +02:00
Jos de Jong da2f912d6d Rename file to `stateUtils.js` 2020-07-22 16:57:30 +02:00
Jos de Jong 7d67ecc4bc Move logic of search into `search.js` 2020-07-22 16:53:18 +02:00
Jos de Jong be87b1e4cd Move `doSearch` to `search.js` 2020-07-22 13:46:50 +02:00
Jos de Jong 35983df136 Fix selected before-node-selector not yet visible whilst mouse is down 2020-07-22 13:40:51 +02:00
Jos de Jong 9ac6ca95c4 Implement Duplicate 2020-07-22 11:55:11 +02:00
Jos de Jong 401a6e19fd Fix a bug in `append` and a bit of refactoring 2020-07-22 11:09:33 +02:00
Jos de Jong ce8cf93170 hide before and append selector line when dragging 2020-07-22 09:49:54 +02:00
Jos de Jong 174f1194ef Cleanup a TODO, it's not working out nicely 2020-07-15 10:32:40 +02:00
Jos de Jong f3459430b0 Unify selection on mousedown 2020-07-15 10:24:39 +02:00
Jos de Jong 3919585db4 Clear selection on Escape 2020-07-15 09:05:54 +02:00
Jos de Jong 404f623215 Swap to first/last search result when end is reached 2020-07-12 14:56:53 +02:00
Jos de Jong c773e5bfa9 Be able to select a single node by clicking 2020-07-12 14:49:12 +02:00
Jos de Jong 7ad262a7b1 Group lodash imports 2020-07-12 11:17:19 +02:00
Jos de Jong 89fc4070a2 Fix appending `... (copy)` suffix when replacing keys with new keys having the same name(s) 2020-07-12 11:13:06 +02:00
Jos de Jong fb3a9cdf36 Rename action to operation 2020-07-12 10:58:55 +02:00
Jos de Jong bdc9f3ba34 Fix cut/copy/paste shortcuts not working when menu button has focus 2020-07-12 10:53:05 +02:00
Jos de Jong 65c38f7b05 Implement restoring selection on undo/redo (not yet for cursor) 2020-07-08 16:15:27 +02:00
Jos de Jong 2fb95c3951 Select pasted content after pasting 2020-07-08 15:22:34 +02:00
Jos de Jong 2a7d4828cb Make quick keys for cut/copy/paste working 2020-07-08 14:58:18 +02:00
Jos de Jong 0e5dabed89 Fix `insertBefore` and `replace` relying on object key order 2020-07-08 14:41:38 +02:00
Jos de Jong 16d3092670 Ensure property key to be unique 2020-07-08 13:19:40 +02:00
Jos de Jong 2f393e5948 Fix pasting properties inline in an object instead of at the bottom 2020-07-08 12:49:12 +02:00
Jos de Jong e44284df90 Fix pasting clipboard in empty object 2020-07-08 10:04:09 +02:00
Jos de Jong bbf0543d85 Fix array collapsing when pasting clipboard 2020-07-08 09:48:40 +02:00
Jos de Jong fa2a23a83d Cut/copy/paste (WIP) 2020-07-05 14:18:34 +02:00
Jos de Jong 823b445e94 Implement selecting before or after a node (WIP) 2020-07-04 20:34:35 +02:00
josdejong 9a23799d5f Implement selecting one or multiple nodes by dragging 2020-06-24 14:20:25 +02:00
josdejong 20a067508d Styling tweaks 2020-06-03 16:00:47 +02:00
josdejong 6b63d623ec Update package.json 2020-06-03 14:29:48 +02:00
josdejong 7ffbd39ae2 Update dependencies 2020-06-03 14:08:11 +02:00
josdejong 9565660ae5 Some refactoring in immutabilityHelpers.js 2020-06-03 14:06:39 +02:00
josdejong 1bdb00dc75 Rewrite all unit tests to plain mocha and assert 2020-06-03 13:53:31 +02:00
josdejong 91125408a9 Fix having two versions of lodash packaged (ugly workaround) 2020-06-03 11:11:41 +02:00
josdejong f17c575c60 Reset array limit when not expanded, and create a "show more" button 2020-06-03 08:43:33 +02:00
josdejong 44eb417e4f Reset array limit when not expanded 2020-06-03 08:24:06 +02:00
josdejong 0da0d14b3d Fix collapse method 2020-06-02 22:40:23 +02:00
josdejong 3ea589a126 Show title on expand button 2020-06-02 22:38:46 +02:00
josdejong 2a885ac5ef Change to `expand` and `collapse` methods which accept a callback 2020-06-02 21:58:40 +02:00
josdejong 637aa62762 Create expand/collapse all buttons 2020-06-02 21:53:36 +02:00
josdejong 61d91c2f9c Implement methods `expandAll`, `collapseAll`, and Ctrl+Click on expand button 2020-06-02 21:41:59 +02:00
josdejong 40accb3f39 Refactor `setIn` again to throw an exception when path doesn't exist 2020-06-02 21:21:14 +02:00
josdejong c293bed8e5 Replace strings with symbols to solve the issue with extending Array with specific properties 2020-06-02 21:14:06 +02:00
josdejong 1c06300443 Remove `onChangeKey`, handle in child itself 2020-06-02 18:10:33 +02:00
josdejong d2b8470e2e Rename `json` to `doc` 2020-06-02 18:02:48 +02:00
josdejong 596d868cc5 Implement `syncState` (WIP) 2020-06-02 17:58:13 +02:00
josdejong 41c633e124 Store object props in global state experiment 2020-06-02 14:48:01 +02:00
josdejong 55ee6361d6 Rename state constant names 2020-06-02 14:13:33 +02:00
josdejong e5ec845d4f Expand array limit when search result is beyond the visible items 2020-05-31 10:54:34 +02:00
josdejong 34e45a04c4 Pass `path` 2020-05-24 12:02:13 +02:00
josdejong d8c059c9b0 Implement scroll to active element via querySelector (WIP) 2020-05-24 11:57:51 +02:00
josdejong d04c94a1c6 Create global state containing expanded state 2020-05-23 20:45:28 +02:00
josdejong d2a84b29fe Implement SearchBox highlight active result (WIP) 2020-05-23 13:43:12 +02:00
josdejong 9c8febb1ff Implement SearchBox (WIP) 2020-05-22 14:13:51 +02:00
josdejong b66a1cf0fa Update devDependencies 2020-05-22 11:53:57 +02:00
josdejong e412628b89 Remove support for Symbol from immutabilityHelpers 2020-05-22 10:59:04 +02:00
josdejong 8392c57a2d Implement history (undo/redo) 2020-05-22 10:53:15 +02:00
josdejong d88120d41c Handle prevention of duplicate keys 2020-05-22 09:35:18 +02:00
josdejong dfe7c06ec7 Let `handleChangeKey` fire the change event 2020-05-22 09:20:44 +02:00
josdejong a0aaf206fa Remove use of Symbol (it's not serializable) 2020-05-20 10:16:13 +02:00
josdejong 93f3375c55 Fix bottom padding 2020-05-13 20:44:03 +02:00
josdejong 0da297c5d3 Fix height jumping when switching from empty to non-empty and vice versa 2020-05-13 20:33:43 +02:00
josdejong 5cc3665c47 Implement functions `getPlainText` and `setPlainText` 2020-05-13 20:21:44 +02:00
josdejong fce4c7910a Fix file extension of sass files 2020-05-13 10:16:59 +02:00
josdejong bcd4553adb Fix indentation 2020-05-08 21:10:59 +02:00
josdejong 324e5053d9 Cleanup outdated TODO 2020-05-08 21:09:00 +02:00
josdejong b3ef0ad6a4 Remove `options` from immutableJSONPatch.js 2020-05-08 21:07:35 +02:00
josdejong 4774a50799 Fix module url in package.json 2020-05-06 20:32:23 +02:00
josdejong 5ac813f091 Create dist/es folder for output bundle 2020-05-06 20:22:49 +02:00
josdejong f33b715391 Fix broken unit test 2020-05-06 20:17:47 +02:00
josdejong 8cb22b3480 Export jsoneditor factory function 2020-05-06 20:11:59 +02:00
josdejong 0d8854c5ea Implementing debouncing of inputs 2020-05-06 14:47:30 +02:00
josdejong 488d7bde49 Move styling into separate files 2020-05-06 14:18:31 +02:00
josdejong 8d48d16858 Fix highlighting of search results broken for values 2020-05-06 14:07:21 +02:00
josdejong d4b02e8d00 Fix highlighting of search results broken for values 2020-05-06 14:07:03 +02:00
josdejong 4d2eb28eb3 Handle changes via JSONPatch 2020-05-06 13:57:55 +02:00
josdejong e66ff70ac4 Remove some old code from getInnerText 2020-05-06 11:51:15 +02:00
josdejong e9116cc36c Fix controlling contenteditable div innerText 2020-05-06 10:47:48 +02:00
josdejong 2b73c6e6bf Some fixes in styling empty key/value 2020-05-05 21:25:54 +02:00
josdejong 3517d185f9 Add lorum ipsum text for testing 2020-05-05 12:10:59 +02:00
josdejong ed263726dc Fix styling of rows, and fields with long text 2020-05-05 12:09:50 +02:00
josdejong 375a268531 Fix vertical alignment 2020-05-05 12:01:06 +02:00
josdejong 88fb95a522 Implement loading a (large) file from disk 2020-05-04 22:30:09 +02:00
josdejong f495974598 Fix overflow of contents 2020-05-04 21:58:04 +02:00
josdejong 9b5891d5cb Split App into JSONEditor and App 2020-05-04 21:50:32 +02:00
josdejong a4c58cfa15 Calculate props only once instead of twice on creation 2020-05-03 19:49:55 +02:00
josdejong 3a9a409fcb Fix changing property name not working for arrays and objects 2020-05-03 19:44:55 +02:00
josdejong 7646c1756b Disable spell checking 2020-04-29 09:46:45 +02:00
josdejong b22e5fae81 Make url clickable 2020-04-27 17:41:11 +02:00
josdejong 4fdf90fb38 Unescape HTML input text 2020-04-27 13:44:43 +02:00
josdejong c03353845e Styling fixes 2020-04-27 13:38:55 +02:00
josdejong 581e64bd73 Improve styling of node header 2020-04-27 12:20:47 +02:00
josdejong 86ac2c52c1 Implement object/array tag when collapsed, and delimiters [] and {} around the contents 2020-04-27 12:08:18 +02:00
josdejong 6bb9e9460a Cleanup console.log 2020-04-27 11:26:38 +02:00
josdejong cf027db855 Props working properly now (though solution is half mutable) 2020-04-27 11:26:17 +02:00
josdejong 80c7b2814f Make editing properties workable (not fully solved yet) 2020-04-27 10:28:09 +02:00
josdejong 5fe4be081f Implement handleChangeKey (WIP) 2020-04-26 22:48:17 +02:00
josdejong 0ee1ab5cca Colorzzz 2020-04-26 22:16:33 +02:00
josdejong 635616ac1c Add JSON Patch functions and immutability helpers 2020-04-26 21:33:34 +02:00
josdejong 1bc28041d3 Initial commit with a new Svelte setup 2020-04-25 22:32:20 +02:00
161 changed files with 9706 additions and 31095 deletions

View File

@ -1,5 +0,0 @@
{
"presets": [
["@babel/preset-env"]
]
}

16
.eslintrc.js Normal file
View File

@ -0,0 +1,16 @@
module.exports = {
env: {
browser: true,
es2020: true,
mocha: true
},
extends: [
'standard'
],
parserOptions: {
ecmaVersion: 12,
sourceType: 'module'
},
rules: {
}
}

13
.gitignore vendored
View File

@ -1,10 +1,5 @@
/node_modules/
/public/dist/
.DS_Store
.idea
*.iml
.vscode
build
dist
downloads
node_modules
*.zip
npm-debug.log
/.vs

View File

@ -1,13 +0,0 @@
bower.json
CONTRIBUTING.md
downloads
misc
node_modules
test
tools
.idea
component.json
.npmignore
.gitignore
*.zip
npm-debug.log

View File

@ -1,5 +0,0 @@
language: node_js
node_js:
- "lts/*"
script: npm test && npm run lint

View File

@ -1,11 +0,0 @@
{
"groups": {
"default": {
"packages": [
"examples/react_advanced_demo/package.json",
"examples/react_demo/package.json",
"package.json"
]
}
}
}

View File

@ -1,238 +0,0 @@
const fs = require('fs')
const path = require('path')
const gulp = require('gulp')
const log = require('fancy-log')
const format = require('date-format')
const concatCss = require('gulp-concat-css')
const minifyCSS = require('gulp-clean-css')
const sass = require('gulp-sass')
const mkdirp = require('mkdirp')
const webpack = require('webpack')
const uglify = require('uglify-js')
const btoa = require('btoa')
const NAME = 'jsoneditor'
const NAME_MINIMALIST = 'jsoneditor-minimalist'
const ENTRY = './src/js/JSONEditor.js'
const HEADER = './src/js/header.js'
const IMAGE = './src/scss/img/jsoneditor-icons.svg'
const DOCS = './src/docs/*'
const DIST = path.join(__dirname, 'dist')
// generate banner with today's date and correct version
function createBanner () {
const today = format.asString('yyyy-MM-dd', new Date()) // today, formatted as yyyy-MM-dd
const version = require('./package.json').version // math.js version
return String(fs.readFileSync(HEADER))
.replace('@@date', today)
.replace('@@version', version)
}
const bannerPlugin = new webpack.BannerPlugin({
banner: createBanner(),
entryOnly: true,
raw: true
})
const webpackConfigModule = {
rules: [
{
test: /\.m?js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader'
}
}
]
}
// create a single instance of the compiler to allow caching
const compiler = webpack({
entry: ENTRY,
output: {
library: 'JSONEditor',
libraryTarget: 'umd',
path: DIST,
filename: NAME + '.js'
},
plugins: [bannerPlugin],
optimization: {
// We no not want to minimize our code.
minimize: false
},
module: webpackConfigModule,
resolve: {
extensions: ['.js'],
mainFields: ['main'] // pick ES5 version of vanilla-picker
},
cache: true
})
// create a single instance of the compiler to allow caching
const compilerMinimalist = webpack({
entry: ENTRY,
output: {
library: 'JSONEditor',
libraryTarget: 'umd',
path: DIST,
filename: NAME_MINIMALIST + '.js'
},
module: webpackConfigModule,
plugins: [
bannerPlugin,
new webpack.IgnorePlugin(new RegExp('^ace-builds')),
new webpack.IgnorePlugin(new RegExp('worker-json-data-url')),
new webpack.IgnorePlugin(new RegExp('^ajv')),
new webpack.IgnorePlugin(new RegExp('^vanilla-picker'))
],
optimization: {
// We no not want to minimize our code.
minimize: false
},
cache: true
})
function minify (name) {
const code = String(fs.readFileSync(DIST + '/' + name + '.js'))
const result = uglify.minify(code, {
sourceMap: {
url: name + '.map'
},
output: {
comments: /@license/,
max_line_len: 64000 // extra large because we have embedded code for workers
}
})
if (result.error) {
throw result.error
}
const fileMin = DIST + '/' + name + '.min.js'
const fileMap = DIST + '/' + name + '.map'
fs.writeFileSync(fileMin, result.code)
fs.writeFileSync(fileMap, result.map)
log('Minified ' + fileMin)
log('Mapped ' + fileMap)
}
// make dist folder structure
gulp.task('mkdir', function (done) {
mkdirp.sync(DIST)
mkdirp.sync(DIST + '/img')
done()
})
// Create an embedded version of the json worker code: a data url
gulp.task('embed-json-worker', function (done) {
const workerBundleFile = './node_modules/ace-builds/src-noconflict/worker-json.js'
const workerEmbeddedFile = './src/js/generated/worker-json-data-url.js'
const workerScript = String(fs.readFileSync(workerBundleFile))
const workerDataUrl = 'data:application/javascript;base64,' + btoa(workerScript)
fs.writeFileSync(workerEmbeddedFile, 'module.exports = \'' + workerDataUrl + '\'\n')
done()
})
// bundle javascript
gulp.task('bundle', function (done) {
// update the banner contents (has a date in it which should stay up to date)
bannerPlugin.banner = createBanner()
compiler.run(function (err, stats) {
if (err) {
log(err)
}
log('bundled ' + NAME + '.js')
done()
})
})
// bundle minimalist version of javascript
gulp.task('bundle-minimalist', function (done) {
// update the banner contents (has a date in it which should stay up to date)
bannerPlugin.banner = createBanner()
compilerMinimalist.run(function (err, stats) {
if (err) {
log(err)
}
log('bundled ' + NAME_MINIMALIST + '.js')
done()
})
})
// bundle css
gulp.task('bundle-css', function (done) {
gulp
.src(['src/scss/jsoneditor.scss'])
.pipe(
sass({
// importer: tildeImporter
})
)
.pipe(concatCss(NAME + '.css'))
.pipe(gulp.dest(DIST))
.pipe(concatCss(NAME + '.min.css'))
.pipe(minifyCSS())
.pipe(gulp.dest(DIST))
done()
})
// create a folder img and copy the icons
gulp.task('copy-img', function (done) {
gulp.src(IMAGE).pipe(gulp.dest(DIST + '/img'))
log('Copied images')
done()
})
// create a folder img and copy the icons
gulp.task('copy-docs', function (done) {
gulp.src(DOCS).pipe(gulp.dest(DIST))
log('Copied doc')
done()
})
gulp.task('minify', function (done) {
minify(NAME)
done()
})
gulp.task('minify-minimalist', function (done) {
minify(NAME_MINIMALIST)
done()
})
// The watch task (to automatically rebuild when the source code changes)
// Does only generate jsoneditor.js and jsoneditor.css, and copy the image
// Does NOT minify the code and does NOT generate the minimalist version
gulp.task('watch', gulp.series('bundle', 'bundle-css', 'copy-img', function () {
gulp.watch(['src/**/*'], gulp.series('bundle', 'bundle-css', 'copy-img'))
}))
// The default task (called when you run `gulp`)
gulp.task('default', gulp.series(
'mkdir',
'embed-json-worker',
gulp.parallel(
'copy-img',
'copy-docs',
'bundle-css',
gulp.series('bundle', 'minify'),
gulp.series('bundle-minimalist', 'minify-minimalist')
)
))

View File

@ -1 +0,0 @@
module.exports = require('./dist/jsoneditor')

14
misc/architecture.md Normal file
View File

@ -0,0 +1,14 @@
# JSONEditor Svelte architecture choices
- Immutable -> one-way data binding
- All Actions based on JSONPatch
- It must be possible to persist all state, including expanded/collapsed and
selection.
- State with search results, and, expanded state, and selection is separate
from the JSON document itself, and are JSON objectswith the same structure,
using symbols.
- Must be able to open huge JSON files
- Must work directly on the JSON object itself, not on a wrapped object model
- Display only the first 100 items of an array etc. Or show items in groups
of 100 items.
- Search must not crash on large files. Stop at 999 results or something.

9714
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,75 +1,52 @@
{
"name": "jsoneditor",
"version": "8.6.6",
"main": "./index",
"description": "A web-based tool to view, edit, format, and validate JSON",
"tags": [
"json",
"editor",
"viewer",
"formatter"
],
"author": "Jos de Jong <wjosdejong@gmail.com>",
"license": "Apache-2.0",
"homepage": "https://github.com/josdejong/jsoneditor",
"name": "jsoneditor-svelte",
"version": "0.0.1",
"type": "module",
"module": "./public/dist/es/jsoneditor.js",
"scripts": {
"build": "rollup -c",
"dev": "rollup -c -w",
"start": "sirv public",
"test": "mocha ./src/**/*.test.js ./src/**/*.test.mjs",
"prepare": "node tools/fixLodashEs.cjs && node tools/generateAceWorker.mjs && node tools/generateAjvDrafts.mjs"
},
"license": "(MIT OR Apache-2.0)",
"repository": {
"type": "git",
"url": "https://github.com/josdejong/jsoneditor.git"
},
"bugs": "https://github.com/josdejong/jsoneditor/issues",
"scripts": {
"build": "gulp",
"minify": "gulp minify",
"start": "gulp watch",
"test": "mocha test --require @babel/register",
"lint": "standard --env=mocha",
"prepublishOnly": "npm test && npm run build"
},
"dependencies": {
"ace-builds": "^1.4.11",
"ajv": "^6.12.2",
"javascript-natural-sort": "^0.7.1",
"jmespath": "^0.15.0",
"json-source-map": "^0.6.1",
"mobius1-selectr": "^2.4.13",
"picomodal": "^3.0.0",
"vanilla-picker": "^2.10.1"
"@fortawesome/free-regular-svg-icons": "5.14.0",
"@fortawesome/free-solid-svg-icons": "5.14.0",
"ace-builds": "1.4.12",
"ajv": "6.12.3",
"classnames": "2.2.6",
"lodash-es": "4.17.15",
"natural-compare-lite": "1.4.0",
"svelte-awesome": "2.3.0",
"svelte-select": "3.11.1",
"svelte-simple-modal": "0.6.0"
},
"devDependencies": {
"@babel/core": "7.9.0",
"@babel/preset-env": "7.9.5",
"@babel/register": "7.9.0",
"babel-loader": "8.1.0",
"@rollup/plugin-commonjs": "14.0.0",
"@rollup/plugin-json": "4.1.0",
"@rollup/plugin-node-resolve": "8.4.0",
"btoa": "1.2.1",
"date-format": "3.0.0",
"fancy-log": "1.3.3",
"gulp": "4.0.2",
"gulp-clean-css": "4.3.0",
"gulp-concat-css": "3.1.0",
"gulp-sass": "4.0.2",
"jsdom": "16.2.2",
"json-loader": "0.5.7",
"eslint": "7.7.0",
"eslint-config-standard": "14.1.1",
"eslint-plugin-import": "2.22.0",
"eslint-plugin-node": "11.1.0",
"eslint-plugin-promise": "4.2.1",
"eslint-plugin-standard": "4.0.1",
"mkdirp": "1.0.4",
"mocha": "7.1.1",
"standard": "14.3.3",
"uglify-js": "3.9.1",
"webpack": "4.43.0"
},
"files": [
"dist",
"docs",
"examples",
"src",
"HISTORY.md",
"index.js",
"LICENSE",
"NOTICE",
"README.md"
],
"standard": {
"ignore": [
"src/js/assets",
"examples/react*"
]
"mocha": "8.1.1",
"rollup": "2.23.0",
"rollup-plugin-livereload": "1.3.0",
"rollup-plugin-svelte": "5.2.3",
"rollup-plugin-terser": "6.1.0",
"sass": "1.26.10",
"sirv-cli": "1.0.3",
"svelte": "3.24.0",
"svelte-preprocess": "4.0.8"
}
}

View File

@ -0,0 +1,52 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset='utf-8'>
<title>JSONEditor | Basic usage</title>
<style type="text/css">
#jsoneditor {
width: 500px;
height: 500px;
}
</style>
</head>
<body>
<p>
<button id="setJSON">Set JSON</button>
<button id="getJSON">Get JSON</button>
</p>
<div id="jsoneditor"></div>
<script type="module">
import jsoneditor from '../dist/es/jsoneditor.js'
// create the editor
const editor = jsoneditor({
target: document.getElementById('jsoneditor')
})
// set json
document.getElementById('setJSON').onclick = function () {
const json = {
'array': [1, 2, 3],
'boolean': true,
'color': '#82b92c',
'null': null,
'number': 123,
'object': {'a': 'b', 'c': 'd'},
'time': 1575599819000,
'string': 'Hello World'
}
editor.set(json)
}
// get json
document.getElementById('getJSON').onclick = function () {
const json = editor.get()
alert(JSON.stringify(json, null, 2))
}
</script>
</body>
</html>

View File

@ -0,0 +1,136 @@
<!DOCTYPE HTML>
<html>
<head>
<meta charset='utf-8'>
<title>JSONEditor | JSON schema validation</title>
<style type="text/css">
body {
width: 600px;
font: 11pt sans-serif;
}
#jsoneditor {
width: 100%;
height: 500px;
}
/* custom bold styling for non-default JSON schema values */
.jsoneditor-is-not-default {
font-weight: bold;
}
</style>
</head>
<body>
<h1>JSON schema validation</h1>
<p>
This example demonstrates JSON schema validation. The JSON object in this example must contain properties like <code>firstName</code> and <code>lastName</code>, can can optionally have a property <code>age</code> which must be a positive integer.
</p>
<p>
See <a href="http://json-schema.org/" target="_blank">http://json-schema.org/</a> for more information.
</p>
<div id="jsoneditor"></div>
<script type="module">
import jsoneditor, { createAjvValidator } from "../dist/es/jsoneditor.js"
const schema = {
"title": "Employee",
"description": "Object containing employee details",
"type": "object",
"properties": {
"firstName": {
"title": "First Name",
"description": "The given name.",
"examples": [
"John"
],
"type": "string"
},
"lastName": {
"title": "Last Name",
"description": "The family name.",
"examples": [
"Smith"
],
"type": "string"
},
"gender": {
"title": "Gender",
"enum": ["male", "female"]
},
"availableToHire": {
"type": "boolean",
"default": false
},
"age": {
"description": "Age in years",
"type": "integer",
"minimum": 0,
"examples": [28, 32]
},
"job": {
"$ref": "job"
}
},
"required": ["firstName", "lastName"]
}
const schemaRefs = {
job: {
"title": "Job description",
"type": "object",
"required": ["address"],
"properties": {
"company": {
"type": "string",
"examples": [
"ACME",
"Dexter Industries"
]
},
"role": {
"description": "Job title.",
"type": "string",
"examples": [
"Human Resources Coordinator",
"Software Developer"
],
"default": "Software Developer"
},
"address": {
"type": "string"
},
"salary": {
"type": "number",
"minimum": 120,
"examples": [100, 110, 120]
}
}
}
}
const doc = {
firstName: 'John',
lastName: 'Doe',
gender: null,
age: "28",
availableToHire: true,
job: {
company: 'freelance',
role: 'developer',
salary: 100
}
}
// create the editor
const editor = jsoneditor({
target: document.getElementById('jsoneditor'),
doc,
onChangeJson: doc => console.log('onChangeJson', doc),
validate: createAjvValidator(schema, schemaRefs)
})
</script>
</body>
</html>

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

192
public/index.html Normal file
View File

@ -0,0 +1,192 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset='utf-8'>
<meta name='viewport' content='width=device-width,initial-scale=1'>
<title>JSON Editor (Svelte)</title>
<link rel='icon' type='image/png' href='favicon.png'>
<style>
html, body {
position: relative;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
body {
color: #333;
margin: 0;
padding: 8px;
box-sizing: border-box;
}
h1 {
color: purple;
}
#testEditorContainer {
width: 800px;
height: 500px;
max-width: 100%;
}
</style>
</head>
<body>
<div id="testEditorContainer"></div>
<p>
<button id="loadLargeJson">load large json</button>
<button id="clearJson">clear json</button>
<button id="patchJson">patch json</button>
<input id="loadFile" type="file">
</p>
<p>
<button id="expandAll">expand all</button>
<button id="expand2">expand 2 levels</button>
<button id="collapseAll">collapse all</button>
</p>
<script type="module">
import jsoneditor, { TreeMode } from './dist/es/jsoneditor.js'
const doc = {
'array': [1, 2, 3, {
name: 'Item ' + 2,
id: String(2),
index: 2,
time: new Date().toISOString(),
location: {
latitude: 1.23,
longitude: 23.44,
coordinates: [23.44, 1.23]
}
}],
'emptyArray': [],
'boolean': true,
'color': '#82b92c',
'null': null,
'number': 123,
'object': {
'a': 'b', 'c': 'd', nested: {
name: 'Item ' + 2,
id: String(2),
index: 2,
time: new Date().toISOString(),
location: {
latitude: 1.23,
longitude: 23.44,
coordinates: [23.44, 1.23]
}
}
},
'emptyObject': {},
'': '',
'string': 'Hello World',
'url': 'https://jsoneditoronline.org',
'Lorem Ipsum': 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'
}
const testEditor = jsoneditor({
target: document.getElementById('testEditorContainer'),
mode: TreeMode,
doc,
onChangeJson: doc => console.log('onChangeJson', doc),
validate: doc => {
if (
doc && typeof doc === 'object' &&
doc.object && typeof doc.object === 'object' &&
doc.object.a === 'b') {
return [
{
path: ['object', 'a'],
message: '"a" should not be "b" ;)'
}
]
}
return []
}
})
window.testEditor = testEditor // expose to window for debugging
document.getElementById('loadLargeJson').onclick = function handleLoadLargeJson() {
const count = 500
console.log('create large json', {count})
console.time('create large json')
const largeJson = {}
largeJson.numbers = []
largeJson.randomNumbers = []
largeJson.array = []
for (let i = 0; i < count; i++) {
const longitude = 4 + i / count
const latitude = 51 + i / count
largeJson.numbers.push(i)
largeJson.randomNumbers.push(Math.round(Math.random() * 1000))
largeJson.array.push({
name: 'Item ' + i,
id: String(i),
index: i,
time: new Date().toISOString(),
location: {
latitude,
longitude,
coordinates: [longitude, latitude]
},
random: Math.random()
})
}
console.timeEnd('create large json')
// const stringifiedSize = JSON.stringify(largeJson).length
// console.log(`large json stringified size: ${filesize(stringifiedSize)}`)
testEditor.set(largeJson)
}
document.getElementById('clearJson').onclick = function handleClearJson() {
testEditor.set({})
}
document.getElementById('patchJson').onclick = function handleClearJson() {
const operations = [{
op: 'replace',
path: '/object/c',
value: 'd2'
}]
testEditor.patch(operations)
}
document.getElementById('loadFile').onchange = function loadFile(event) {
console.log('loadFile', event.target.files)
const reader = new FileReader()
const file = event.target.files[0]
reader.onload = function (event) {
const text = event.target.result
const json = JSON.parse(text)
testEditor.set(json)
}
reader.readAsText(file)
}
document.getElementById('expandAll').onclick = function expandAll () {
testEditor.expand(() => true)
}
document.getElementById('expand2').onclick = function expandAll () {
testEditor.expand(path => path.length < 2)
}
document.getElementById('collapseAll').onclick = function collapseAll () {
testEditor.collapse(() => false)
}
</script>
</body>
</html>

77
rollup.config.js Normal file
View File

@ -0,0 +1,77 @@
import svelte from 'rollup-plugin-svelte';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import json from '@rollup/plugin-json';
import livereload from 'rollup-plugin-livereload';
import { terser } from 'rollup-plugin-terser';
import autoPreprocess from 'svelte-preprocess';
const production = !process.env.ROLLUP_WATCH;
export default {
input: 'src/main.js',
output: {
sourcemap: true,
format: 'esm', // esm, umd, cjs, iife
name: 'JSONEditor',
file: 'public/dist/es/jsoneditor.js'
},
plugins: [
svelte({
// enable run-time checks when not in production
dev: !production,
// // we'll extract any component CSS out into
// // a separate file - better for performance
// css: css => {
// css.write('public/build/bundle.css');
// },
preprocess: autoPreprocess()
}),
// If you have external dependencies installed from
// npm, you'll most likely need these plugins. In
// some cases you'll need additional configuration -
// consult the documentation for details:
// https://github.com/rollup/plugins/tree/master/packages/commonjs
resolve({
browser: true,
dedupe: ['svelte', 'svelte/transition', 'svelte/internal']
}),
commonjs(),
json(),
// In dev mode, call `npm run start` once
// the bundle has been generated
!production && serve(),
// Watch the `public` directory and refresh the
// browser on changes when not in production
!production && livereload('public'),
// If we're building for production (npm run build
// instead of npm run dev), minify
production && terser()
],
watch: {
clearScreen: false
}
};
function serve() {
let started = false;
return {
writeBundle() {
if (!started) {
started = true;
require('child_process').spawn('npm', ['run', 'start', '--', '--dev'], {
stdio: ['ignore', 'inherit', 'inherit'],
shell: true
});
}
}
};
}

View File

@ -0,0 +1,204 @@
# Jump.js
[![Jump.js on NPM](https://img.shields.io/npm/v/jump.js.svg?style=flat-square)](https://www.npmjs.com/package/jump.js)
A small, modern, dependency-free smooth scrolling library.
* [Demo Page](http://callmecavs.github.io/jump.js/) (Click the arrows!)
## Usage
Jump was developed with a modern JavaScript workflow in mind. To use it, it's recommended you have a build system in place that can transpile ES6, and bundle modules. For a minimal boilerplate that fulfills those requirements, check out [outset](https://github.com/callmecavs/outset).
Follow these steps to get started:
1. [Install](#install)
2. [Import](#import)
3. [Call](#call)
4. [Review Options](#options)
### Install
Using NPM, install Jump, and save it to your `package.json` dependencies.
```bash
$ npm install jump.js --save
```
### Import
Import Jump, naming it according to your preference.
```es6
// import Jump
import jump from 'jump.js'
```
### Call
Jump exports a _singleton_, so there's no need to create an instance. Just call it, passing a [target](#target).
```es6
// call Jump, passing a target
jump('.target')
```
Note that the singleton can make an infinite number of jumps.
## Options
All options, **except [target](#target)**, are optional, and have sensible defaults. The defaults are shown below:
```es6
jump('.target', {
duration: 1000,
offset: 0,
callback: undefined,
easing: easeInOutQuad,
a11y: false
})
```
Explanation of each option follows:
* [target](#target)
* [duration](#duration)
* [offset](#offset)
* [callback](#callback)
* [easing](#easing)
* [a11y](#a11y)
### target
Scroll _from the current position_ by passing a number of pixels.
```es6
// scroll down 100px
jump(100)
// scroll up 100px
jump(-100)
```
Or, scroll _to an element_, by passing either:
* a node, or
* a CSS selector
```es6
// passing a node
const node = document.querySelector('.target')
jump(node)
// passing a CSS selector
// the element referenced by the selector is determined using document.querySelector
jump('.target')
```
### duration
Pass the time the `jump()` takes, in milliseconds.
```es6
jump('.target', {
duration: 1000
})
```
Or, pass a function that returns the duration of the `jump()` in milliseconds. This function is passed the `jump()` `distance`, in `px`, as a parameter.
```es6
jump('.target', {
duration: distance => Math.abs(distance)
})
```
### offset
Offset a `jump()`, _only if to an element_, by a number of pixels.
```es6
// stop 10px before the top of the element
jump('.target', {
offset: -10
})
// stop 10px after the top of the element
jump('.target', {
offset: 10
})
```
Note that this option is useful for accommodating `position: fixed` elements.
### callback
Pass a function that will be called after the `jump()` has been completed.
```es6
// in both regular and arrow functions, this === window
jump('.target', {
callback: () => console.log('Jump completed!')
})
```
### easing
Easing function used to transition the `jump()`.
```es6
jump('.target', {
easing: easeInOutQuad
})
```
See [easing.js](https://github.com/callmecavs/jump.js/blob/master/src/easing.js) for the definition of `easeInOutQuad`, the default easing function. Credit for this function goes to Robert Penner.
### a11y
If enabled, _and scrolling to an element_:
* add a [`tabindex`](https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex) to, and
* [`focus`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/focus) the element
```es6
jump('.target', {
a11y: true
})
```
Note that this option is disabled by default because it has _visual implications_ in many browsers. Focusing an element triggers the `:focus` CSS state selector, and is often accompanied by an `outline`.
## Browser Support
Jump depends on the following browser APIs:
* [requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame)
Consequently, it supports the following natively:
* Chrome 24+
* Firefox 23+
* Safari 6.1+
* Opera 15+
* IE 10+
* iOS Safari 7.1+
* Android Browser 4.4+
To add support for older browsers, consider including polyfills/shims for the APIs listed above. There are no plans to include any in the library, in the interest of file size.
## License
[MIT](https://opensource.org/licenses/MIT). © 2016 Michael Cavalea
[![Built With Love](http://forthebadge.com/images/badges/built-with-love.svg)](http://forthebadge.com)

View File

@ -0,0 +1,11 @@
// Robert Penner's easeInOutQuad
// find the rest of his easing functions here: http://robertpenner.com/easing/
// find them exported for ES6 consumption here: https://github.com/jaxgeller/ez.js
export default (t, b, c, d) => {
t /= d / 2
if(t < 1) return c / 2 * t * t + b
t--
return -c / 2 * (t * (t - 2) - 1) + b
}

View File

@ -0,0 +1,196 @@
import easeInOutQuad from './easing.js'
const jumper = () => {
// private variable cache
// no variables are created during a jump, preventing memory leaks
let container // container element to be scrolled (node)
let element // element to scroll to (node)
let start // where scroll starts (px)
let stop // where scroll stops (px)
let offset // adjustment from the stop position (px)
let easing // easing function (function)
let a11y // accessibility support flag (boolean)
let distance // distance of scroll (px)
let duration // scroll duration (ms)
let timeStart // time scroll started (ms)
let timeElapsed // time spent scrolling thus far (ms)
let next // next scroll position (px)
let callback // to call when done scrolling (function)
let scrolling // true whilst scrolling (boolean)
// scroll position helper
function location() {
return container.scrollY || container.pageYOffset || container.scrollTop
}
// element offset helper
function top(element) {
const elementTop = element.getBoundingClientRect().top
const containerTop = container.getBoundingClientRect
? container.getBoundingClientRect().top
: 0
return elementTop - containerTop + start
}
// scrollTo helper
function scrollTo(top) {
container.scrollTo
? container.scrollTo(0, top) // window
: container.scrollTop = top // custom container
}
// rAF loop helper
function loop(timeCurrent) {
// store time scroll started, if not started already
if(!timeStart) {
timeStart = timeCurrent
}
// determine time spent scrolling so far
timeElapsed = timeCurrent - timeStart
// calculate next scroll position
next = easing(timeElapsed, start, distance, duration)
// scroll to it
scrollTo(next)
scrolling = true
// check progress
timeElapsed < duration
? requestAnimationFrame(loop) // continue scroll loop
: done() // scrolling is done
}
// scroll finished helper
function done() {
// account for rAF time rounding inaccuracies
scrollTo(start + distance)
// if scrolling to an element, and accessibility is enabled
if(element && a11y) {
// add tabindex indicating programmatic focus
element.setAttribute('tabindex', '-1')
// focus the element
element.focus()
}
// if it exists, fire the callback
if(typeof callback === 'function') {
callback()
}
// reset time for next jump
timeStart = false
// we're done scrolling
scrolling = false
}
// API
function jump(target, options = {}) {
// resolve options, or use defaults
duration = options.duration || 1000
offset = options.offset || 0
callback = options.callback // "undefined" is a suitable default, and won't be called
easing = options.easing || easeInOutQuad
a11y = options.a11y || false
// resolve container
switch(typeof options.container) {
case 'object':
// we assume container is an HTML element (Node)
container = options.container
break
case 'string':
container = document.querySelector(options.container)
break
default:
container = window
}
// cache starting position
start = location()
// resolve target
switch(typeof target) {
// scroll from current position
case 'number':
element = undefined // no element to scroll to
a11y = false // make sure accessibility is off
stop = start + target
break
// scroll to element (node)
// bounding rect is relative to the viewport
case 'object':
element = target
stop = top(element)
break
// scroll to element (selector)
// bounding rect is relative to the viewport
case 'string':
element = document.querySelector(target)
stop = top(element)
break
default:
}
// resolve scroll distance, accounting for offset
distance = stop - start + offset
// resolve duration
switch(typeof options.duration) {
// number in ms
case 'number':
duration = options.duration
break
// function passed the distance of the scroll
case 'function':
duration = options.duration(distance)
break
default:
}
// start the loop if we're not already scrolling
if (!scrolling) {
requestAnimationFrame(loop)
}
else {
// reset time for next jump
timeStart = false
}
}
// expose only the jump method
return jump
}
// export singleton
const singleton = jumper()
export default singleton

View File

@ -0,0 +1,61 @@
<svelte:options immutable={true} />
<script>
import Modal from 'svelte-simple-modal'
import TreeMode from './treemode/TreeMode.svelte'
const DefaultMode = TreeMode
export let config = {}
let ref
export function set (json) {
// TODO: check if the method exists for this mode, if not, throw a clear error
ref.set(json)
}
export function get () {
// TODO: check if the method exists for this mode, if not, throw a clear error
return ref.get()
}
export function expand (callback) {
// TODO: check if the method exists for this mode, if not, throw a clear error
return ref.expand(callback)
}
export function collapse (callback) {
// TODO: check if the method exists for this mode, if not, throw a clear error
return ref.collapse(callback)
}
export function setValidator (newValidate) {
// TODO: check if the method exists for this mode, if not, throw a clear error
ref.setValidator(newValidate)
}
export function getValidator () {
// TODO: check if the method exists for this mode, if not, throw a clear error
return ref.getValidator()
}
export function patch(operations, newSelection) {
// TODO: check if the method exists for this mode, if not, throw a clear error
return ref.patch(operations, newSelection)
}
function getRestConfig (config) {
let { mode, ...restConfig } = config
return restConfig
}
</script>
<Modal>
<!-- TODO: pass the config options explicitly here? -->
<svelte:component
this={config.mode || DefaultMode}
bind:this={ref}
{...getRestConfig(config)}
/>
</Modal>

View File

@ -0,0 +1,46 @@
@import '../../styles.scss';
.menu-dropdown {
position: relative;
overflow: visible;
ul {
margin: 0;
padding: 0;
li {
margin: 0;
padding: 0;
list-style-type : none;
}
}
.items {
display: none;
position: absolute;
top: 100%;
left: 0;
background: white;
z-index: 2;
color: $black;
box-shadow: $box-shadow;
&.visible {
display: block;
}
button {
width: 100%;
text-align: left;
&:hover {
background: $background-gray;
}
&:disabled {
color: $gray;
background: unset;
}
}
}
}

View File

@ -0,0 +1,66 @@
<script>
import Icon from 'svelte-awesome'
import { faCaretDown } from '@fortawesome/free-solid-svg-icons'
import { onDestroy, onMount } from 'svelte'
import { keyComboFromEvent} from '../../utils/keyBindings.js'
/** @type {MenuDropdownItem[]} */
export let items = []
export let title = null
export let width = '120px'
export let visible = false
function toggleShow (event) {
event.stopPropagation()
visible = !visible
}
function handleClick () {
visible = false
}
function handleKeyDown (event) {
const combo = keyComboFromEvent(event)
if (combo === 'Escape') {
event.preventDefault()
visible = false
}
}
onMount(() => {
document.addEventListener('click', handleClick)
document.addEventListener('keydown', handleKeyDown)
})
onDestroy(() => {
document.removeEventListener('click', handleClick)
document.removeEventListener('keydown', handleKeyDown)
})
</script>
<div class="menu-dropdown" title={title} on:click={handleClick}>
<slot name="defaultItem"></slot>
<button on:click={toggleShow}>
<Icon data={faCaretDown} />
</button>
<div class="items" class:visible style="width: {width};">
<ul>
{#each items as item}
<li>
<button
on:click={() => item.onClick()}
title={item.title}
disabled={item.disabled}
>
{item.text}
</button>
</li>
{/each}
</ul>
</div>
</div>
<style src="./DropdownMenu.scss"></style>

View File

@ -0,0 +1,21 @@
@import '../../styles.scss';
.header {
display: flex;
background: $theme-color;
color: $white;
.title {
flex: 1;
padding: $input-padding;
vertical-align: middle;
}
button.close {
min-width: 32px;
&:hover {
background: rgba(255, 255, 255, 0.1);
}
}
}

View File

@ -0,0 +1,20 @@
<script>
import { getContext } from 'svelte'
import Icon from 'svelte-awesome'
import { faTimes } from '@fortawesome/free-solid-svg-icons'
export let title = 'Modal'
const {close} = getContext('simple-modal')
</script>
<div class="header">
<div class="title">
{title}
</div>
<button class="close" on:click={close}>
<Icon data={faTimes} />
</button>
</div>
<style src="./Header.scss"></style>

View File

@ -0,0 +1,51 @@
@import '../../styles.scss';
.jsoneditor-modal {
// styling for the select box, svelte-select
// see docs: https://github.com/rob-balfre/svelte-select#styling
--height: 36px;
--multiItemHeight: 28px;
--multiItemMargin: 2px;
--multiItemPadding: 2px 8px;
--multiClearTop: 5px;
--multiItemBorderRadius: 6px;
--clearSelectTop: 2px;
--clearSelectBottom: 2px;
--itemIsActiveBG: #3883fa; // theme-color
font-family: $font-family;
font-size: $font-size;
color: $black;
.contents {
padding: 20px;
overflow: hidden;
.actions {
display: flex;
flex-direction: row;
justify-content: flex-end;
padding-top: $padding;
}
}
}
// custom styling for the modal.
// FIXME: not neat to override global styles!
:global(.bg) {
width: 100%;
height: 100%;
}
:global(.bg .window-wrap) {
margin: 0;
}
:global(.bg .window) {
max-width: 80%;
margin: 4rem auto 2rem auto;
overflow: auto;
}
:global(.bg .content) {
max-height: calc(100vh - 6rem);
}

View File

@ -0,0 +1,34 @@
@import '../../styles.scss';
@import './Modal.scss';
.jsoneditor-modal.sort {
table {
width: 100%;
border-collapse: collapse;
border-spacing: none;
th, td {
text-align: left;
vertical-align: middle;
font-weight: normal;
padding-bottom: $padding;
input.path {
width: 100%;
box-sizing: border-box;
padding: 6px 16px; // TODO: define variables for those props
border: 1px solid $border-gray;
border-radius: $border-radius;
font-family: $font-family;
font-size: $font-size;
color: $black;
outline: none;
&:read-only {
border: 1px solid $background-gray;
}
}
}
}
}

View File

@ -0,0 +1,145 @@
<svelte:options immutable={true} />
<script>
import { getContext } from 'svelte'
import Select from 'svelte-select'
import Header from './Header.svelte'
import { getNestedPaths } from '../../utils/arrayUtils.js'
import { isObject } from '../../utils/typeUtils.js'
import { stringifyPath } from '../../utils/pathUtils.js'
import { sortArray, sortObjectKeys } from '../../logic/sort.js'
import { sortModalState } from './sortModalState.js'
import { compileJSONPointer } from '../../utils/jsonPointer.js'
import { get } from 'lodash-es'
import { getIn } from '../../utils/immutabilityHelpers.js'
export let id
export let json
export let rootPath
export let onSort
const {close} = getContext('simple-modal')
let stateId = `${id}:${compileJSONPointer(rootPath)}`
$: json
$: jsonIsArray = Array.isArray(json)
$: paths = jsonIsArray ? getNestedPaths(json) : undefined
$: properties = paths ? paths.map(pathToOption) : undefined
const asc = {
value: 1,
label: 'ascending'
}
const desc = {
value: -1,
label: 'descending'
}
const directions = [asc, desc]
let selectedProperty = (sortModalState[stateId] && sortModalState[stateId].selectedProperty) || undefined
let selectedDirection = (sortModalState[stateId] && sortModalState[stateId].selectedDirection) || asc
$: {
// if there is only one option, select it and do not render the select box
if (selectedProperty === undefined && properties && properties.length === 1) {
selectedProperty = properties[0]
}
}
function pathToOption (path) {
return {
value: path,
label: stringifyPath(path)
}
}
function handleSort () {
// remember the selected values for the next time we open the SortModal
// just in memory, not persisted
sortModalState[stateId] = {
selectedProperty,
selectedDirection
}
if (jsonIsArray) {
if (!selectedProperty) {
return
}
const property = selectedProperty.value
const direction = selectedDirection.value
const operations = sortArray(json, rootPath, property, direction)
onSort(operations)
} else if (isObject(json)) {
const direction = selectedDirection.value
const operations = sortObjectKeys(json, rootPath, direction)
onSort(operations)
} else {
console.error('Cannot sort: no array or object')
}
close()
}
</script>
<div class="jsoneditor-modal sort">
<Header title={jsonIsArray ? 'Sort array items' : 'Sort object keys'} />
<div class="contents">
<table>
<colgroup>
<col width="25%">
<col width="75%">
</colgroup>
<tbody>
<tr>
<th>Path</th>
<td>
<input
class="path"
type="text"
readonly
value={rootPath.length > 0 ? stringifyPath(rootPath) : '(whole document)'}
/>
</td>
</tr>
{#if jsonIsArray && (properties.length > 1 || selectedProperty === undefined) }
<tr>
<th>Property</th>
<td>
<Select
items={properties}
bind:selectedValue={selectedProperty}
/>
</td>
</tr>
{/if}
<tr>
<th>Direction</th>
<td>
<Select
items={directions}
containerClasses='test-class'
bind:selectedValue={selectedDirection}
isClearable={false}
/>
</td>
</tr>
</tbody>
</table>
<div class="actions">
<button
class="primary"
on:click={handleSort}
disabled={jsonIsArray ? !selectedProperty : false}
>
Sort
</button>
</div>
</div>
</div>
<style src="./SortModal.scss"></style>

View File

@ -0,0 +1,53 @@
@import '../../styles.scss';
@import './Modal.scss';
.jsoneditor-modal.transform {
.description {
color: $dark-gray;
code {
background: $background-gray;
font-family: $font-family-mono;
font-size: $font-size-mono;
}
}
.contents {
color: $dark-gray;
}
.label {
font-weight: bold;
padding-top: $padding * 2;
padding-bottom: $padding / 2;
display: block;
button {
font-weight: bold;
}
}
textarea.query,
textarea.preview {
border: 1px solid $border-gray;
border-radius: $border-radius;
outline: none;
width: 100%;
height: 100px;
resize: vertical; // prevent resizing horizontally
box-sizing: border-box;
padding: $padding / 2;
font-family: $font-family-mono;
font-size: $font-size-mono;
color: $black;
}
textarea.preview {
height: 200px;
&.error {
color: $red;
}
}
}

View File

@ -0,0 +1,178 @@
<svelte:options immutable={true} />
<script>
import { getContext } from 'svelte'
import Icon from 'svelte-awesome'
import { debounce } from 'lodash-es'
import { compileJSONPointer } from '../../utils/jsonPointer.js'
import Header from './Header.svelte'
import { transformModalState } from './transformModalState.js'
import { DEBOUNCE_DELAY, MAX_PREVIEW_CHARACTERS } from '../../constants.js'
import { truncate } from '../../utils/stringUtils.js'
import TransformWizard from './TransformWizard.svelte'
import * as _ from 'lodash-es'
import { getIn } from '../../utils/immutabilityHelpers.js'
import { faCaretDown, faCaretRight } from '@fortawesome/free-solid-svg-icons'
export let id
export let json
export let rootPath
export let onTransform
const DEFAULT_QUERY = 'function query (data) {\n return data\n}'
const {close} = getContext('simple-modal')
let stateId = `${id}:${compileJSONPointer(rootPath)}`
const state = transformModalState[stateId] || {}
let query = state.query || DEFAULT_QUERY
let previewHasError = false
let preview = ''
// showWizard is not stored inside a stateId
let showWizard = transformModalState.showWizard !== false
let filterField = state.filterField
let filterRelation = state.filterRelation
let filterValue = state.filterValue
let sortField = state.sortField
let sortDirection = state.sortDirection
let pickFields = state.pickFields
function evalTransform(json, query) {
// FIXME: replace unsafe new Function with a JS based query language
// As long as we don't persist or fetch queries, there is no security risk.
// TODO: only import the most relevant subset of lodash instead of the full library?
const queryFn = new Function('_', `'use strict'; return (${query})`)(_)
return queryFn(json)
}
function updateQuery (newQuery) {
console.log('updated query by wizard', newQuery)
query = newQuery
}
function previewTransform(json, query) {
try {
const jsonTransformed = evalTransform(json, query)
preview = truncate(JSON.stringify(jsonTransformed, null, 2), MAX_PREVIEW_CHARACTERS)
previewHasError = false
} catch (err) {
preview = err.toString()
previewHasError = true
}
}
const previewTransformDebounced = debounce(previewTransform, DEBOUNCE_DELAY)
$: {
previewTransformDebounced(json, query)
}
function handleTransform () {
try {
const jsonTransformed = evalTransform(json, query)
onTransform([
{
op: 'replace',
path: compileJSONPointer(rootPath),
value: jsonTransformed
}
])
// remember the selected values for the next time we open the SortModal
// just in memory, not persisted
transformModalState[stateId] = {
query,
filterField,
filterRelation,
filterValue,
sortField,
sortDirection,
pickFields
}
close()
} catch (err) {
// this should never occur since we can only press the Transform
// button when creating a preview was succesful
console.error(err)
preview = err.toString()
previewHasError = true
}
}
function toggleShowWizard () {
showWizard = !showWizard
// not stored inside a stateId
transformModalState.showWizard = showWizard
}
</script>
<div class="jsoneditor-modal transform">
<Header title='Transform' />
<div class="contents">
<div class='description'>
Enter a JavaScript function to filter, sort, or transform the data.
</div>
<div class='description'>
You can use <a href='https://lodash.com' target='_blank' rel='noopener noreferrer'>Lodash</a>
functions like <code>_.map</code>, <code>_.filter</code>,
<code>_.orderBy</code>, <code>_.sortBy</code>, <code>_.groupBy</code>,
<code>_.pick</code>, <code>_.uniq</code>, <code>_.get</code>, etcetera.
</div>
<div class="label">
<button on:click={toggleShowWizard}>
<Icon data={showWizard ? faCaretDown : faCaretRight} />
Wizard
</button>
</div>
{#if showWizard}
{#if Array.isArray(json)}
<TransformWizard
bind:filterField
bind:filterRelation
bind:filterValue
bind:sortField
bind:sortDirection
bind:pickFields
json={json}
onQuery={updateQuery}
/>
{:else}
(Only available for arrays, not for objects)
{/if}
{/if}
<div class="label">
Query
</div>
<textarea class="query" bind:value={query} />
<div class="label">Preview</div>
<textarea
class="preview"
class:error={previewHasError}
bind:value={preview}
readonly
/>
<div class="actions">
<button
class="primary"
on:click={handleTransform}
disabled={previewHasError}
>
Transform
</button>
</div>
</div>
</div>
<style src="./TransformModal.scss"></style>

View File

@ -0,0 +1,61 @@
@import '../../styles.scss';
table.transform-wizard {
border-collapse: collapse;
border-spacing: 0;
width: 100%;
tr {
th {
font-weight: normal;
text-align: left;
width: 60px;
}
td {
.horizontal {
width: 100%;
display: flex;
flex-direction: row;
margin-bottom: $padding/2;
:global(.selectContainer) {
&.filter-field {
flex: 4;
margin-right: $padding/2;
}
&.filter-relation {
flex: 1;
margin-right: $padding/2;
}
&.sort-field {
flex: 3;
margin-right: $padding/2;
}
&.sort-direction {
flex: 1;
}
&.pick-fields {
flex: 1;
}
}
.filter-value {
flex: 4;
padding: $padding;
border: 1px solid $border-gray;
border-radius: $border-radius;
outline: none;
&:focus {
border-color: $theme-color;
}
}
}
}
}
}

View File

@ -0,0 +1,133 @@
<svelte:options immutable={true} />
<script>
import Select from 'svelte-select'
import { getNestedPaths } from '../../utils/arrayUtils.js'
import { stringifyPath } from '../../utils/pathUtils.js'
import { createQuery } from '../../logic/jsCreateQuery.js'
import { isEqual } from 'lodash-es'
export let json
export let onQuery
// fields
export let filterField = undefined
export let filterRelation = undefined
export let filterValue = undefined
export let sortField = undefined
export let sortDirection = undefined
export let pickFields = undefined
// options
$: jsonIsArray = Array.isArray(json)
$: paths = jsonIsArray ? getNestedPaths(json) : undefined
$: fieldOptions = paths ? paths.map(pathToOption) : undefined
const filterRelationOptions = ['==', '!=', '<', '<=', '>', '>='].map(relation => ({
value: relation,
label: relation
}))
const sortDirectionOptions = [
{ value: 'asc', label: 'ascending' },
{ value: 'desc', label: 'descending' },
]
function pathToOption (path) {
return {
value: path,
label: stringifyPath(path)
}
}
let queryOptions = {}
$: {
const newQueryOptions = {}
if (filterField && filterRelation && filterValue) {
newQueryOptions.filter = {
field: filterField.value,
relation: filterRelation.value,
value: filterValue
}
}
if (sortField && sortDirection) {
newQueryOptions.sort = {
field: sortField.value,
direction: sortDirection.value
}
}
if (pickFields) {
newQueryOptions.projection = {
fields: pickFields.map(item => item.value)
}
}
if (!isEqual(newQueryOptions, queryOptions)) {
queryOptions = newQueryOptions
const query = createQuery(json, queryOptions)
// console.log('query updated', query, queryOptions)
onQuery(query)
}
}
</script>
<table class="transform-wizard">
<tr>
<th>Filter</th>
<td>
<div class='horizontal'>
<Select
containerClasses='filter-field'
items={fieldOptions}
bind:selectedValue={filterField}
/>
<Select
containerClasses='filter-relation'
items={filterRelationOptions}
bind:selectedValue={filterRelation}
/>
<input
class='filter-value'
bind:value={filterValue}
/>
</div>
</td>
</tr>
<tr>
<th>Sort</th>
<td>
<div class='horizontal'>
<Select
containerClasses='sort-field'
items={fieldOptions}
bind:selectedValue={sortField}
/>
<Select
containerClasses='sort-direction'
items={sortDirectionOptions}
bind:selectedValue={sortDirection}
/>
</div>
</td>
</tr>
<tr>
<th>Pick</th>
<td>
<div class='horizontal'>
<Select
containerClasses='pick-fields'
items={fieldOptions}
isMulti
bind:selectedValue={pickFields}
/>
</div>
</td>
</tr>
</table>
<style src="./TransformWizard.scss"></style>

View File

@ -0,0 +1 @@
export const sortModalState = {}

View File

@ -0,0 +1 @@
export const transformModalState = {}

View File

@ -0,0 +1,57 @@
@import '../../styles.scss';
$color: $gray;
$background-color: $background-gray;
div.collapsed-items {
font-family: $font-family;
font-size: $font-size;
color: $color;
// https://sharkcoder.com/visual/borders
$size: 8px;
padding: $padding / 2;
border: $size solid transparent;
border-width: $size 0;
background-color: $background-color;
background-color: hsla(0, 0%, 0%, 0);
background-image:
linear-gradient($background-color, $background-color),
linear-gradient(to bottom right, transparent 50.5%, $background-color 50.5%),
linear-gradient(to bottom left, transparent 50.5%, $background-color 50.5%),
linear-gradient(to top right, transparent 50.5%, $background-color 50.5%),
linear-gradient(to top left, transparent 50.5%, $background-color 50.5%);
background-repeat: repeat, repeat-x, repeat-x, repeat-x, repeat-x;
background-position: 0 0, $size 0, $size 0, $size 100%,$size 100%;
background-size: auto auto, 2*$size 2*$size, 2*$size 2*$size, 2*$size 2*$size, 2*$size 2*$size;
background-clip: padding-box, border-box, border-box, border-box, border-box;
background-origin: padding-box, border-box, border-box, border-box, border-box;
display: flex;
div.text,
button.expand-items {
margin: 0 $padding / 2;
}
div.text {
display: inline;
}
button.expand-items {
font-family: $font-family;
font-size: $font-size;
color: $color;
background: none;
border: none;
padding: 0;
text-decoration: underline;
cursor: pointer;
&:hover,
&:focus {
color: $red;
}
}
}

View File

@ -0,0 +1,41 @@
<script>
import {
INDENTATION_WIDTH
} from '../../constants.js'
import { getExpandItemsSections } from '../../logic/expandItemsSections.js'
export let visibleSections
export let sectionIndex
export let total
export let path
/** @type {function (path: Path, section: Section)} */
export let onExpandSection
$: visibleSection = visibleSections[sectionIndex]
$: startIndex = visibleSection.end
$: endIndex = visibleSections[sectionIndex + 1]
? visibleSections[sectionIndex + 1].start
: total
$: expandItemsSections = getExpandItemsSections(startIndex, endIndex)
// TODO: this is duplicated from the same function in JSONNode
function getIndentationStyle(level) {
return `margin-left: ${level * INDENTATION_WIDTH}px`
}
</script>
<div class="collapsed-items" style={getIndentationStyle(path.length + 2)}>
<div>
<div class="text">Items {startIndex}-{endIndex}</div
>{#each expandItemsSections as expandItemsSection
}<button class="expand-items" on:click={() => onExpandSection(path, expandItemsSection)}>
show {expandItemsSection.start}-{expandItemsSection.end}
</button>
{/each}
</div>
</div>
<style src="./CollapsedItems.scss"></style>

View File

@ -0,0 +1,249 @@
@import '../../styles.scss';
.json-node {
position: relative;
font-family: $font-family-mono;
font-size: $font-size-mono;
color: $black;
&.root {
min-height: 100%;
padding-bottom: $input-padding;
box-sizing: border-box;
}
.header,
.contents,
.footer {
transition: background-color 0.1s ease-in-out;
}
&.hovered {
.header,
.contents,
.footer {
background-color: $hovered-background;
}
}
&.selected {
.header,
.contents,
.footer {
background-color: $selection-background;
}
}
$selector-height: 8px; // must be about half a line height
.props,
.items {
position: relative;
}
.before-node-selector,
.append-node-selector {
position: absolute;
left: 0;
right: $input-padding;
height: $selector-height;
box-sizing: border-box;
padding-left: $indentation-width;
z-index: 1;
.selector {
margin-top: $selector-height / 2;
}
&:hover {
.selector {
border: 1px dashed $light-gray;
}
}
&.selected {
.selector {
border: 1px dashed $gray;
}
}
}
// selector must not be visible whilst dragging (mouse down)
&:active {
.before-node-selector,
.append-node-selector {
&:not(.selected) {
.selector {
border: none;
}
}
}
}
.before-node-selector {
top: -$selector-height/2 - 1px;
}
.append-node-selector {
bottom: -$selector-height/2 - 1px;
.selector {
margin-top: $selector-height / 2 - 2px;
}
}
.header {
position: relative;
}
.header,
.contents {
display: table;
flex-direction: row;
line-height: $line-height;
> * {
display: table-cell;
}
}
.contents {
padding-left: $line-height ; // must be the same as the width of the expand button
padding-right: 5px;
}
.footer {
display: inline-block;
padding-left: $line-height + $input-padding; // must be the same as the width of the expand button
}
}
.expand {
position: relative;
top: 2px;
width: $line-height;
height: $line-height;
padding: 0;
margin: 0;
border: none;
cursor: pointer;
background: transparent;
color: $gray-icon;
font-size: $font-size-mono;
line-height: $line-height;
}
.key,
.value {
line-height: $line-height;
min-width: 16px;
word-break: normal;
padding: 0 $input-padding;
outline: none;
border-radius: 1px;
vertical-align: top;
&:focus {
box-shadow: 0 0 3px 1px #008fd5;
z-index: 1;
}
}
.separator,
.delimiter {
vertical-align: top;
color: $gray;
}
.tag {
vertical-align: top;
border: none;
font-size: $font-size-small;
font-family: $font-family;
color: white;
background: $light-gray;
border-radius: 2px;
padding: 1px 4px;
margin: 0 5px;
cursor: pointer;
position: relative;
&:hover {
background: lighten($light-gray, 5%);
}
}
.value {
&.string {
color: #008000;
}
&.object,
&.array {
min-width: 16px;
color: $gray;
}
&.number {
color: #ee422e;
}
&.boolean {
color: #ff8c00;
}
&.null {
color: #004ED0;
}
&.invalid {
color: #000000;
}
&.url {
color: green;
text-decoration: underline;
}
}
div.empty {
&:not(:focus) {
outline: 1px dotted lightgray;
-moz-outline-radius: 2px;
}
&::after {
pointer-events: none;
color: lightgray;
}
&.key::after {
content: 'key';
}
&.value::after {
content: 'value';
}
}
.key.search,
.value.search {
background-color: $highlight-color;
&.active {
background-color: $highlight-active-color;
}
}
// TODO: create a class shared by .expand and .validation-error buttons
.validation-error {
color: $warning-color;
padding: 0 $input-padding;
height: $line-height;
line-height: $line-height;
font-size: $font-size;
position: relative;
top: 2px;
}

View File

@ -0,0 +1,554 @@
<svelte:options immutable={true} />
<script>
import { debounce, isEqual } from 'lodash-es'
import { rename } from '../../logic/operations.js'
import { singleton } from './singleton.js'
import {
DEBOUNCE_DELAY,
STATE_EXPANDED,
STATE_PROPS,
STATE_SEARCH_PROPERTY,
STATE_SEARCH_VALUE,
STATE_VISIBLE_SECTIONS,
INDENTATION_WIDTH,
VALIDATION_ERROR
} from '../../constants.js'
import {
getPlainText,
isChildOfAttribute,
isChildOfNodeName,
isContentEditableDiv,
setPlainText
} from '../../utils/domUtils.js'
import Icon from 'svelte-awesome'
import { faCaretDown, faCaretRight, faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'
import classnames from 'classnames'
import { findUniqueName } from '../../utils/stringUtils.js'
import { isUrl, stringConvert, valueType } from '../../utils/typeUtils'
import { compileJSONPointer } from '../../utils/jsonPointer'
import { getNextKeys } from '../../logic/documentState.js'
import CollapsedItems from './CollapsedItems.svelte'
export let key = undefined // only applicable for object properties
export let value
export let path
export let state
export let searchResult
export let validationErrors
export let onPatch
export let onUpdateKey
export let onExpand
export let onSelect
/** @type {function (path: Path, section: Section)} */
export let onExpandSection
export let selection
$: expanded = state && state[STATE_EXPANDED]
$: visibleSections = state && state[STATE_VISIBLE_SECTIONS]
$: props = state && state[STATE_PROPS]
$: validationError = validationErrors && validationErrors[VALIDATION_ERROR]
const escapeUnicode = false // TODO: pass via options
let domKey
let domValue
let hovered = false
$: type = valueType (value)
$: limit = visibleSections && visibleSections[0].end // FIXME: make dynamic
$: limited = type === 'array' && value.length > limit
$: items = type === 'array'
? limited ? value.slice(0, limit) : value
: undefined
$: valueIsUrl = isUrl(value)
let keyClass
$: keyClass = getKeyClass(key, searchResult)
let valueClass
$: valueClass = getValueClass(value, searchResult)
$: if (domKey) {
if (document.activeElement !== domKey) {
// synchronize the innerText of the editable div with the escaped value,
// but only when the domValue does not have focus else we will ruin
// the cursor position.
setPlainText(domKey, key)
}
}
$: if (domValue) {
if (document.activeElement !== domValue) {
// synchronize the innerText of the editable div with the escaped value,
// but only when the domValue does not have focus else we will ruin
// the cursor position.
setPlainText(domValue, value)
}
}
function getIndentationStyle(level) {
return `margin-left: ${level * INDENTATION_WIDTH}px`
}
function getValueClass (value, searchResult) {
const type = valueType (value)
return classnames('value', type, searchResult && searchResult[STATE_SEARCH_VALUE], {
url: isUrl(value),
empty: typeof value === 'string' && value.length === 0,
})
}
function getKeyClass(key, searchResult) {
return classnames('key', searchResult && searchResult[STATE_SEARCH_PROPERTY], {
empty: key === ''
})
}
function toggleExpand (event) {
event.stopPropagation()
const recursive = event.ctrlKey
onExpand(path, !expanded, recursive)
}
function handleExpand (event) {
event.stopPropagation()
onExpand(path, true)
}
function updateKey () {
const newKey = getPlainText(domKey)
// must be handled by the parent which has knowledge about the other keys
onUpdateKey(key, newKey)
}
const updateKeyDebounced = debounce(updateKey, DEBOUNCE_DELAY)
function handleUpdateKey (oldKey, newKey) {
const newKeyUnique = findUniqueName(newKey, value)
const nextKeys = getNextKeys(props, key, false)
onPatch(rename(path, oldKey, newKeyUnique, nextKeys))
}
function handleKeyInput (event) {
const newKey = getPlainText(event.target)
keyClass = getKeyClass(newKey, searchResult)
if (newKey === '') {
// immediately update to cleanup any left over <br/>
setPlainText(domKey, '')
}
// fire a change event only after a delay
updateKeyDebounced()
}
function handleKeyBlur (event) {
// handle any pending changes still waiting in the debounce function
updateKeyDebounced.flush()
// make sure differences in escaped text like with new lines is updated
setPlainText(domKey, key)
}
// get the value from the DOM
function getValue () {
const valueText = getPlainText(domValue)
return stringConvert(valueText) // TODO: implement support for type "string"
}
function updateValue () {
const newValue = getValue()
onPatch([{
op: 'replace',
path: compileJSONPointer(path),
value: newValue
}])
}
const debouncedUpdateValue = debounce(updateValue, DEBOUNCE_DELAY)
function handleValueInput () {
// do not await the debounced update to apply styles
const newValue = getValue()
valueClass = getValueClass(newValue, searchResult)
if (newValue === '') {
// immediately update to cleanup any left over <br/>
setPlainText(domValue, '')
}
// fire a change event only after a delay
debouncedUpdateValue()
}
function handleValueBlur () {
// handle any pending changes still waiting in the debounce function
debouncedUpdateValue.flush()
// make sure differences in escaped text like with new lines is updated
setPlainText(domValue, value)
}
function handleValueClick (event) {
if (valueIsUrl && event.ctrlKey) {
event.preventDefault()
event.stopPropagation()
window.open(value, '_blank')
}
}
function handleValueKeyDown (event) {
if (event.ctrlKey && event.key === 'Enter') {
event.preventDefault()
event.stopPropagation()
window.open(value, '_blank')
}
}
function handleMouseDown (event) {
// unselect existing selection on mouse down if any
if (selection) {
onSelect(null)
}
// check if the mouse down is not happening in the key or value input fields or on a button
if (isContentEditableDiv(event.target) || isChildOfNodeName(event.target, 'BUTTON')) {
return
}
if (isChildOfAttribute(event.target, 'data-type', 'before-node-selector')) {
singleton.mousedown = true
singleton.selectionAnchor = path
singleton.selectionFocus = null
onSelect({
beforePath: path
})
} else if (isChildOfAttribute(event.target, 'data-type', 'append-node-selector')) {
singleton.mousedown = true
singleton.selectionAnchor = path
singleton.selectionFocus = null
onSelect({
appendPath: path
})
} else {
// initialize dragging a selection
singleton.mousedown = true
singleton.selectionAnchor = path
singleton.selectionFocus = null
if (isChildOfAttribute(event.target, 'data-type', 'selectable-area')) {
// select current node
onSelect({
anchorPath: path,
focusPath: path
})
}
}
event.stopPropagation()
// IMPORTANT: do not use event.preventDefault() here,
// else the :active style doesn't work!
// we attache the mouse up event listener to the global document,
// so we will not miss if the mouse up is happening outside of the editor
document.addEventListener('mouseup', handleMouseUp)
}
function handleMouseMove (event) {
if (singleton.mousedown) {
event.preventDefault()
event.stopPropagation()
if (singleton.selectionFocus == null) {
// First move event, no selection yet.
// Clear the default selection of the browser
if (window.getSelection) {
window.getSelection().empty()
}
}
if (!isEqual(path, singleton.selectionFocus)) {
singleton.selectionFocus = path
onSelect({
anchorPath: singleton.selectionAnchor,
focusPath: singleton.selectionFocus
})
}
}
}
function handleMouseUp (event) {
if (singleton.mousedown) {
event.stopPropagation()
singleton.mousedown = false
}
document.removeEventListener('mouseup', handleMouseUp)
}
function handleMouseOver (event) {
event.stopPropagation()
if (
isChildOfAttribute(event.target, 'data-type', 'selectable-area') &&
!isContentEditableDiv(event.target)
) {
hovered = true
}
}
function handleMouseOut (event) {
event.stopPropagation()
hovered = false
}
// FIXME: this is not efficient. Create a nested object with the selection and pass that
$: selected = (selection && selection.pathsMap)
? selection.pathsMap[compileJSONPointer(path)] === true
: false
$: selectedBefore = (selection && selection.beforePath)
? isEqual(selection.beforePath, path)
: false
$: selectedAppend = (selection && selection.appendPath)
? isEqual(selection.appendPath, path)
: false
$: indentationStyle = getIndentationStyle(path.length)
</script>
<div
class='json-node'
class:root={path.length === 0}
class:selected={selected}
class:hovered={hovered}
on:mousedown={handleMouseDown}
on:mousemove={handleMouseMove}
on:mouseover={handleMouseOver}
on:mouseout={handleMouseOut}
>
<div
data-type="before-node-selector"
class="before-node-selector"
class:selected={selectedBefore}
style={indentationStyle}
>
<div class="selector"></div>
</div>
{#if type === 'array'}
<div
data-type="selectable-area" class='header' style={indentationStyle} >
<button
class='expand'
on:click={toggleExpand}
title='Expand or collapse this array (Ctrl+Click to expand/collapse recursively)'
>
{#if expanded}
<Icon data={faCaretDown} />
{:else}
<Icon data={faCaretRight} />
{/if}
</button>
{#if typeof key === 'string'}
<div
class={keyClass}
data-path={compileJSONPointer(path.concat(STATE_SEARCH_PROPERTY))}
contenteditable="true"
spellcheck="false"
on:input={handleKeyInput}
on:blur={handleKeyBlur}
bind:this={domKey}
></div>
<div class="separator">:</div>
{/if}
{#if expanded}
<div class="delimiter">[</div>
{:else}
<div class="delimiter">[</div>
<button class="tag" on:click={handleExpand}>{value.length} items</button>
<div class="delimiter">]</div>
{#if validationError}
<!-- FIXME: implement proper tooltip -->
<button
class='validation-error'
title={validationError.isChildError ? 'Contains invalid items' : validationError.message}
on:click={handleExpand}
>
<Icon data={faExclamationTriangle} />
</button>
{/if}
{/if}
</div>
{#if expanded}
<div class="items">
{#each visibleSections as visibleSection, sectionIndex (sectionIndex)}
{#each value.slice(visibleSection.start, Math.min(visibleSection.end, value.length)) as item, itemIndex (itemIndex)}
<svelte:self
key={visibleSection.start + itemIndex}
value={item}
path={path.concat(visibleSection.start + itemIndex)}
state={state && state[visibleSection.start + itemIndex]}
searchResult={searchResult ? searchResult[visibleSection.start + itemIndex] : undefined}
validationErrors={validationErrors ? validationErrors[visibleSection.start + itemIndex] : undefined}
onPatch={onPatch}
onUpdateKey={handleUpdateKey}
onExpand={onExpand}
onSelect={onSelect}
onExpandSection={onExpandSection}
selection={selection}
/>
{/each}
{#if visibleSection.end < value.length}
<CollapsedItems
visibleSections={visibleSections}
sectionIndex={sectionIndex}
total={value.length}
path={path}
onExpandSection={onExpandSection}
/>
{/if}
{/each}
<div
data-type="append-node-selector"
class="append-node-selector"
class:selected={selectedAppend}
style={getIndentationStyle(path.length + 1)}
>
<div class="selector"></div>
</div>
<!-- {#if limited}
<div class="limit" style={getIndentationStyle(path.length + 2)}>
(showing {limit} of {value.length} items <button on:click={handleShowMore}>show more</button> <button on:click={handleShowAll}>show all</button>)
</div>
{/if} -->
</div>
<div data-type="selectable-area" class="footer" style={indentationStyle} >
<span class="delimiter">]</span>
</div>
{/if}
{:else if type === 'object'}
<div data-type="selectable-area" class="header" style={indentationStyle} >
<button
class='expand'
on:click={toggleExpand}
title='Expand or collapse this object (Ctrl+Click to expand/collapse recursively)'
>
{#if expanded}
<Icon data={faCaretDown} />
{:else}
<Icon data={faCaretRight} />
{/if}
</button>
{#if typeof key === 'string'}
<div
class={keyClass}
data-path={compileJSONPointer(path.concat(STATE_SEARCH_PROPERTY))}
contenteditable="true"
spellcheck="false"
on:input={handleKeyInput}
on:blur={handleKeyBlur}
bind:this={domKey}
></div>
<span class="separator">:</span>
{/if}
{#if expanded}
<span class="delimiter">&#123;</span>
{:else}
<span class="delimiter"> &#123;</span>
<button class="tag" on:click={handleExpand}>{Object.keys(value).length} props</button>
<span class="delimiter">&rbrace;</span>
{#if validationError}
<!-- FIXME: implement proper tooltip -->
<button
class='validation-error'
title={validationError.isChildError ? 'Contains invalid properties' : validationError.message}
on:click={handleExpand}
>
<Icon data={faExclamationTriangle} />
</button>
{/if}
{/if}
</div>
{#if expanded}
<div class="props">
{#each props as prop (prop.id)}
<svelte:self
key={prop.key}
value={value[prop.key]}
path={path.concat(prop.key)}
state={state && state[prop.key]}
searchResult={searchResult ? searchResult[prop.key] : undefined}
validationErrors={validationErrors ? validationErrors[prop.key] : undefined}
onPatch={onPatch}
onUpdateKey={handleUpdateKey}
onExpand={onExpand}
onSelect={onSelect}
onExpandSection={onExpandSection}
selection={selection}
/>
{/each}
<div
data-type="append-node-selector"
class="append-node-selector"
class:selected={selectedAppend}
style={getIndentationStyle(path.length + 1)}
>
<div class="selector"></div>
</div>
</div>
<div data-type="selectable-area" class="footer" style={indentationStyle} >
<span class="delimiter">&rbrace;</span>
</div>
{/if}
{:else}
<div data-type="selectable-area" class="contents" style={indentationStyle} >
{#if typeof key === 'string'}
<div
class={keyClass}
data-path={compileJSONPointer(path.concat(STATE_SEARCH_PROPERTY))}
contenteditable="true"
spellcheck="false"
on:input={handleKeyInput}
on:blur={handleKeyBlur}
bind:this={domKey}
></div>
<span class="separator">:</span>
{/if}
<div
class={valueClass}
data-path={compileJSONPointer(path.concat(STATE_SEARCH_VALUE))}
contenteditable="true"
spellcheck="false"
on:input={handleValueInput}
on:blur={handleValueBlur}
on:click={handleValueClick}
on:keydown={handleValueKeyDown}
bind:this={domValue}
title={valueIsUrl ? 'Ctrl+Click or Ctrl+Enter to open url in new window' : null}
></div>
{#if validationError}
<!-- FIXME: implement proper tooltip -->
<button class='validation-error' title={validationError.message}>
<Icon data={faExclamationTriangle} />
</button>
{/if}
</div>
{/if}
</div>
<style src="./JSONNode.scss"></style>

View File

@ -0,0 +1,54 @@
@import '../../styles.scss';
.menu {
font-family: $font-family;
font-size: $font-size;
background: $theme-color;
color: $white;
display: flex;
align-items: center;
position: relative;
// FIXME: should utilize the generic styling in styles.scss
.button {
width: $menu-button-size;
height: $menu-button-size;
border: none;
background: transparent;
color: white;
cursor: pointer;
padding: 0;
&:hover,
&:focus {
background: rgba(255, 255, 255, 0.3);
}
&:disabled {
color: rgba(255, 255, 255, 0.5);
background: transparent;
}
}
.space {
flex: 1;
}
.separator {
$margin: 3px;
background: rgba(255, 255, 255, 0.15);
box-sizing: border-box;
width: 1px;
height: $menu-button-size - 2 * $margin;
margin: $margin;
}
.search-box-container {
position: absolute;
top: 100%;
right: $search-box-offset + 20px; // keep space for scrollbar
margin-top: $search-box-offset;
z-index: 2;
}
}

View File

@ -0,0 +1,203 @@
<svelte:options immutable={true} />
<script>
import Icon from 'svelte-awesome'
import { faCut, faClone, faCopy, faPaste, faSearch, faUndo, faRedo, faPlus, faTimes, faFilter, faSortAmountDownAlt } from '@fortawesome/free-solid-svg-icons'
import SearchBox from './SearchBox.svelte'
import DropdownMenu from '../controls/DropdownMenu.svelte'
export let searchText
export let searchResult
export let searching
export let showSearch = false
export let selection
export let clipboard
export let historyState
export let onCut
export let onCopy
export let onPaste
export let onRemove
export let onDuplicate
export let onInsert
export let onUndo
export let onRedo
export let onSort
export let onTransform
export let onSearchText
export let onNextSearchResult
export let onPreviousSearchResult
$: hasSelection = selection != null
$: hasSelectionContents = selection != null && selection.paths != null
$: hasSelectionWithoutContents = selection != null && selection.paths == null
$: hasClipboardContents = clipboard != null && selection != null
function handleToggleSearch() {
showSearch = !showSearch
}
function clearSearchResult () {
showSearch = false
onSearchText('')
}
function handleInsertStructure () {
onInsert('structure')
}
/** @type {MenuDropdownItem[]} */
$: insertItems = [
{
text: 'Insert value',
title: 'Insert a new value',
onClick: () => onInsert('value'),
disabled: !hasSelectionWithoutContents,
default: true
},
{
text: 'Insert object',
title: 'Insert a new object',
onClick: () => onInsert('object'),
disabled: !hasSelectionWithoutContents
},
{
text: 'Insert array',
title: 'Insert a new array',
onClick: () => onInsert('array'),
disabled: !hasSelectionWithoutContents
},
{
text: 'Insert structure',
title: 'Insert a new item with the same structure as other items. ' +
'Only applicable inside an array',
onClick: handleInsertStructure,
disabled: !hasSelectionWithoutContents
}
]
</script>
<div class="menu">
<button
class="button cut"
on:click={onCut}
disabled={!hasSelectionContents}
title="Cut (Ctrl+X)"
>
<Icon data={faCut} />
</button>
<button
class="button copy"
on:click={onCopy}
disabled={!hasSelectionContents}
title="Copy (Ctrl+C)"
>
<Icon data={faCopy} />
</button>
<button
class="button paste"
on:click={onPaste}
disabled={!hasClipboardContents}
title="Paste (Ctrl+V)"
>
<Icon data={faPaste} />
</button>
<div class="separator"></div>
<button
class="button remove"
on:click={onRemove}
disabled={!hasSelectionContents}
title="Remove (Delete)"
>
<Icon data={faTimes} />
</button>
<button
class="button duplicate"
on:click={onDuplicate}
disabled={!hasSelectionContents}
title="Duplicate (Ctrl+D)"
>
<Icon data={faClone} />
</button>
<DropdownMenu
items={insertItems}
title="Insert new structure (Insert)"
>
<button
class="button insert"
slot="defaultItem"
on:click={handleInsertStructure}
disabled={!hasSelectionWithoutContents}
>
<Icon data={faPlus} />
</button>
</DropdownMenu>
<div class="separator"></div>
<button
class="button sort"
on:click={onSort}
title="Sort"
>
<Icon data={faSortAmountDownAlt} />
</button>
<button
class="button transform"
on:click={onTransform}
title="Transform contents (filter, sort, project)"
>
<Icon data={faFilter} />
</button>
<div class="separator"></div>
<button
class="button search"
on:click={handleToggleSearch}
title="Search (Ctrl+F)"
>
<Icon data={faSearch} />
</button>
<div class="separator"></div>
<button
class="button undo"
disabled={!historyState.canUndo}
on:click={onUndo}
title="Undo (Ctrl+Z)"
>
<Icon data={faUndo} />
</button>
<button
class="button redo"
disabled={!historyState.canRedo}
on:click={onRedo}
title="Redo (Ctrl+Shift+Z)"
>
<Icon data={faRedo} />
</button>
<div class="space"></div>
{#if showSearch}
<div class="search-box-container">
<SearchBox
text={searchText}
resultCount={searchResult ? searchResult.count : 0}
activeIndex={searchResult ? searchResult.activeIndex : 0}
searching={searching}
onChange={onSearchText}
onNext={onNextSearchResult}
onPrevious={onPreviousSearchResult}
onClose={clearSearchResult}
/>
</div>
{/if}
</div>
<style src="./Menu.scss"></style>

View File

@ -0,0 +1,70 @@
@import '../../styles.scss';
$search-size: 24px;
$button-width: 20px;
.search-box {
border: 2px solid $theme-color;
border-radius: $border-radius;
background: $white;
box-shadow: $box-shadow;
.search-form {
display: flex;
align-items: center;
padding: 0 5px;
button {
color: $light-gray;
display: block;
width: $search-size;
height: $search-size;
text-align: center;
background: transparent;
border: none;
padding: 0;
margin: 0;
}
button {
width: $button-width;
cursor: pointer;
&:hover,
&:active {
color: $gray;
&.search-icon {
color: $light-gray;
cursor: inherit;
}
}
}
.search-input {
border: none;
height: $search-size;
padding: 0 $input-padding;
margin: 0;
flex: 1;
width: 100px;
outline: none;
}
.searching {
width: $button-width;
color: $light-gray;
}
.search-count {
color: $light-gray;
font-size: $font-size-small;
visibility: hidden;
padding: 0 $input-padding;
&.visible {
visibility: visible;
}
}
}
}

View File

@ -0,0 +1,98 @@
<svelte:options immutable={true} />
<script>
import { debounce } from 'lodash-es'
import Icon from 'svelte-awesome'
import { faCircleNotch, faSearch, faChevronDown, faChevronUp, faTimes } from '@fortawesome/free-solid-svg-icons'
import { DEBOUNCE_DELAY, MAX_SEARCH_RESULTS } from '../../constants.js'
import { keyComboFromEvent } from '../../utils/keyBindings.js'
export let text = ''
export let searching
let inputText = ''
export let resultCount = 0
export let activeIndex = 0
export let onChange = () => {}
export let onPrevious = () => {}
export let onNext = () => {}
export let onClose = () => {}
$: formattedResultCount = resultCount >= MAX_SEARCH_RESULTS
? `${MAX_SEARCH_RESULTS - 1}+`
: String(resultCount)
$: onChangeDebounced = debounce(onChange, DEBOUNCE_DELAY)
function handleSubmit (event) {
event.preventDefault()
const pendingChanges = text !== inputText
if (pendingChanges) {
onChangeDebounced.cancel()
onChange(inputText)
} else {
onNext()
}
}
function handleInput (event) {
inputText = event.target.value
onChangeDebounced(inputText)
// TODO: fire debounced onChange
}
function handleKeyDown (event) {
const combo = keyComboFromEvent(event)
if (combo === 'Ctrl+Enter' || combo === 'Command+Enter') {
event.preventDefault()
// TODO: move focus to the active element
}
if (combo === 'Escape') {
event.preventDefault()
onClose()
}
}
function initSearchInput (element) {
element.select()
}
</script>
<div class="search-box">
<form class="search-form" on:submit={handleSubmit} on:keydown={handleKeyDown}>
<button class="search-icon">
{#if searching}
<Icon data={faCircleNotch} spin />
{:else}
<Icon data={faSearch} />
{/if}
</button>
<label about="search input">
<input
class="search-input"
type="text"
value={text}
on:input={handleInput}
use:initSearchInput
/>
</label>
<div class="search-count" class:visible={text !== ''}>
{activeIndex !== -1 ? `${activeIndex + 1}/` : ''}{formattedResultCount}
</div>
<button class="search-next" on:click={onNext} type="button">
<Icon data={faChevronDown} />
</button>
<button class="search-previous" on:click={onPrevious} type="button">
<Icon data={faChevronUp} />
</button>
<button class="search-clear" on:click={onClose} type="button">
<Icon data={faTimes} />
</button>
</form>
</div>
<style src="./SearchBox.scss"></style>

View File

@ -0,0 +1,41 @@
@import '../../styles.scss';
.jsoneditor {
border: 1px solid $theme-color;
border-top: none; // menu already gives a border
width: 100%;
height: 100%;
min-height: 150px;
display: flex;
flex-direction: column;
position: relative;
.hidden-input-label {
position: absolute;
right: 0;
top: 0;
width: 0;
height: 0;
.hidden-input {
width: 0;
height: 0;
padding: 0;
border: 0;
outline: none;
visibility: hidden;
&.visible {
visibility: visible;
}
}
}
.contents {
flex: 1;
overflow: auto;
//display: flex;
//flex-direction: column;
}
}

View File

@ -0,0 +1,586 @@
<svelte:options immutable={true} />
<script>
import { getContext, tick } from 'svelte'
import {
duplicate,
insert,
createNewValue,
removeAll
} from '../../logic/operations.js'
import {
STATE_EXPANDED,
STATE_LIMIT,
SCROLL_DURATION,
SIMPLE_MODAL_OPTIONS,
SEARCH_PROGRESS_THROTTLE,
MAX_SEARCH_RESULTS
} from '../../constants.js'
import { createHistory } from '../../logic/history.js'
import JSONNode from './JSONNode.svelte'
import {
createPathsMap,
createSelectionFromOperations,
expandSelection,
findRootPath
} from '../../logic/selection.js'
import { isContentEditableDiv, isTextInput } from '../../utils/domUtils.js'
import {
getIn,
setIn,
updateIn
} from '../../utils/immutabilityHelpers.js'
import { compileJSONPointer, parseJSONPointer } from '../../utils/jsonPointer.js'
import { keyComboFromEvent } from '../../utils/keyBindings.js'
import { searchAsync, searchNext, searchPrevious, updateSearchResult } from '../../logic/search.js'
import { immutableJSONPatch } from '../../utils/immutableJSONPatch'
import { last, initial, cloneDeep, uniqueId, throttle } from 'lodash-es'
import jump from '../../assets/jump.js/src/jump.js'
import { expandPath, expandSection, syncState, patchProps } from '../../logic/documentState.js'
import Menu from './Menu.svelte'
import { isObjectOrArray } from '../../utils/typeUtils.js'
import { mapValidationErrors } from '../../logic/validation.js'
import SortModal from '../modals/SortModal.svelte'
import TransformModal from '../modals/TransformModal.svelte'
const { open } = getContext('simple-modal')
const sortModalId = uniqueId()
const transformModalId = uniqueId()
let divContents
let domHiddenInput
export let validate = null
export let onChangeJson = () => {}
export function setValidator (newValidate) {
validate = newValidate
}
export function getValidator () {
return validate
}
export let doc = {}
let state = undefined
let selection = null
let clipboard = null
$: state = syncState(doc, state, [], (path) => {
return path.length < 1
? true
: (path.length === 1 && path[0] === 0) // first item of an array?
? true
: false
})
$: validationErrorsList = validate ? validate(doc) : []
$: validationErrors = mapValidationErrors(validationErrorsList)
let showSearch = false
let searching = false
let searchText = ''
let searchResult = undefined
let searchHandler = undefined
function handleSearchProgress (results) {
searchResult = updateSearchResult(doc, results, searchResult)
}
const handleSearchProgressDebounced = throttle(handleSearchProgress, SEARCH_PROGRESS_THROTTLE)
function handleSearchDone (results) {
searchResult = updateSearchResult(doc, results, searchResult)
searching = false
console.log('finished search')
}
async function handleSearchText (text) {
searchText = text
await tick() // await for the search results to be updated
focusActiveSearchResult(searchResult && searchResult.activeItem)
}
async function handleNextSearchResult () {
searchResult = searchNext(searchResult)
focusActiveSearchResult(searchResult && searchResult.activeItem)
}
function handlePreviousSearchResult () {
searchResult = searchPrevious(searchResult)
focusActiveSearchResult(searchResult && searchResult.activeItem)
}
async function focusActiveSearchResult (activeItem) {
if (activeItem) {
state = expandPath(state, initial(activeItem))
await tick()
scrollTo(activeItem)
}
}
$: {
// cancel previous search when still running
if (searchHandler && searchHandler.cancel) {
console.log('cancel previous search')
searchHandler.cancel()
}
console.log('start search', searchText)
searching = true
searchHandler = searchAsync(searchText, doc, {
onProgress: handleSearchProgressDebounced,
onDone: handleSearchDone,
maxResults: MAX_SEARCH_RESULTS
})
}
const history = createHistory({
onChange: (state) => {
historyState = state
}
})
let historyState = history.getState()
export function expand (callback = () => true) {
state = syncState(doc, state, [], callback, true)
}
export function collapse (callback = () => false) {
state = syncState(doc, state, [], callback, true)
}
export function get() {
return doc
}
export function set(newDocument) {
doc = newDocument
searchResult = undefined
state = undefined
history.clear()
}
/**
* @param {JSONPatchDocument} operations
* @param {Selection} [newSelection]
*/
export function patch(operations, newSelection) {
const prevState = state
const prevSelection = selection
console.log('operations', operations)
const documentPatchResult = immutableJSONPatch(doc, operations)
const statePatchResult = immutableJSONPatch(state, operations)
// TODO: only apply operations to state for relevant operations: move, copy, delete? Figure out
doc = documentPatchResult.json
state = patchProps(statePatchResult.json, operations)
if (newSelection) {
selection = newSelection
}
history.add({
undo: documentPatchResult.revert,
redo: operations,
prevState,
state,
prevSelection,
selection: newSelection
})
return {
doc,
error: documentPatchResult.error,
undo: documentPatchResult.revert,
redo: operations
}
}
function selectionToClipboard (selection) {
if (!selection || !selection.paths) {
return null
}
return selection.paths.map(path => {
return {
key: String(last(path)),
value: cloneDeep(getIn(doc, path))
}
})
}
function handleCut() {
if (selection && selection.paths) {
console.log('cut', { selection, clipboard })
clipboard = selectionToClipboard(selection)
const operations = removeAll(selection.paths)
handlePatch(operations)
selection = null
}
}
function handleCopy() {
if (selection && selection.paths) {
clipboard = selectionToClipboard(selection)
console.log('copy', { clipboard })
}
}
function handlePaste() {
if (selection && clipboard) {
console.log('paste', { clipboard, selection })
const operations = insert(doc, state, selection, clipboard)
const newSelection = createSelectionFromOperations(operations)
handlePatch(operations, newSelection)
}
}
function handleRemove() {
if (selection && selection.paths) {
console.log('remove', { selection })
const operations = removeAll(selection.paths)
handlePatch(operations)
selection = null
}
}
function handleDuplicate() {
if (selection && selection.paths) {
console.log('duplicate', { selection })
const operations = duplicate(doc, state, selection.paths)
const newSelection = createSelectionFromOperations(operations)
handlePatch(operations, newSelection)
}
}
/**
* @param {'value' | 'object' | 'array' | 'structure'} type
*/
function handleInsert(type) {
if (selection != null) {
console.log('insert', { type, selection })
const value = createNewValue(doc, selection, type)
const values = [
{
key: 'new',
value
}
]
const operations = insert(doc, state, selection, values)
const newSelection = createSelectionFromOperations(operations)
handlePatch(operations, newSelection)
if (isObjectOrArray(value)) {
// expand the new object/array in case of inserting a structure
operations
.filter(operation => operation.op === 'add')
.forEach(operation => handleExpand(parseJSONPointer(operation.path), true, true))
}
}
}
function handleUndo() {
if (history.getState().canUndo) {
const item = history.undo()
if (item) {
doc = immutableJSONPatch(doc, item.undo).json
state = item.prevState
selection = item.prevSelection
console.log('undo', { item, doc, state, selection })
emitOnChange()
}
}
}
function handleRedo() {
if (history.getState().canRedo) {
const item = history.redo()
if (item) {
doc = immutableJSONPatch(doc, item.redo).json
state = item.state
selection = item.selection
console.log('redo', { item, doc, state, selection })
emitOnChange()
}
}
}
function handleSort () {
const rootPath = findRootPath(selection)
open(SortModal, {
id: sortModalId,
json: getIn(doc, rootPath),
rootPath,
onSort: async (operations) => {
console.log('onSort', rootPath, operations)
patch(operations, selection)
}
}, {
...SIMPLE_MODAL_OPTIONS,
styleWindow: {
...SIMPLE_MODAL_OPTIONS.styleWindow,
width: '400px'
}
})
}
function handleTransform () {
const rootPath = findRootPath(selection)
open(TransformModal, {
id: transformModalId,
json: getIn(doc, rootPath),
rootPath,
onTransform: async (operations) => {
console.log('onTransform', rootPath, operations)
const expanded = getIn(state, rootPath.concat(STATE_EXPANDED))
patch(operations, selection)
// keep the root nodes expanded state
await tick()
state = setIn(state, rootPath.concat(STATE_EXPANDED), expanded)
}
}, {
...SIMPLE_MODAL_OPTIONS,
styleWindow: {
...SIMPLE_MODAL_OPTIONS.styleWindow,
width: '600px'
}
})
}
/**
* Scroll the window vertically to the node with given path
* @param {Path} path
*/
function scrollTo (path) {
const elem = divContents.querySelector(`div[data-path="${compileJSONPointer(path)}"]`)
const offset = -(divContents.getBoundingClientRect().height / 4)
if (elem) {
jump(elem, {
container: divContents,
offset,
duration: SCROLL_DURATION
})
}
}
function emitOnChange() {
// TODO: add more logic here to emit onChange, onChangeJson, onChangeText, etc.
onChangeJson(doc)
}
/**
* @param {JSONPatchDocument} operations
* @param {Selection} [newSelection]
*/
function handlePatch(operations, newSelection) {
// console.log('handlePatch', operations)
const patchResult = patch(operations, newSelection)
emitOnChange()
return patchResult
}
function handleUpdateKey (oldKey, newKey) {
// should never be called on the root
}
/**
* Toggle expanded state of a node
* @param {Path} path
* @param {boolean} expanded
* @param {boolean} [recursive=false]
*/
function handleExpand (path, expanded, recursive = false) {
if (recursive) {
state = updateIn(state, path, (childState) => {
return syncState(getIn(doc, path), childState, [], () => expanded, true)
})
} else {
state = setIn(state, path.concat(STATE_EXPANDED), expanded, true)
}
}
/**
* @param {SelectionSchema} selectionSchema
*/
function handleSelect (selectionSchema) {
if (selectionSchema) {
const { anchorPath, focusPath, beforePath, appendPath } = selectionSchema
if (beforePath) {
selection = { beforePath }
} else if (appendPath) {
selection = { appendPath }
} else if (anchorPath && focusPath) {
const paths = expandSelection(doc, state, anchorPath, focusPath)
selection = {
paths,
pathsMap: createPathsMap(paths)
}
} else {
console.error('Unknown type of selection', selectionSchema)
}
// set focus to the hidden input, so we can capture quick keys like Ctrl+X, Ctrl+C, Ctrl+V
setTimeout(() => domHiddenInput.focus())
} else {
selection = null
}
}
function handleExpandSection (path, section) {
console.log('handleExpandSection', path, section)
state = expandSection(state, path, section)
}
function handleKeyDown (event) {
const combo = keyComboFromEvent(event)
if (!isContentEditableDiv(event.target) && !isTextInput(event.target)) {
if (combo === 'Ctrl+X' || combo === 'Command+X') {
event.preventDefault()
handleCut()
}
if (combo === 'Ctrl+C' || combo === 'Command+C') {
event.preventDefault()
handleCopy()
}
if (combo === 'Ctrl+V' || combo === 'Command+V') {
event.preventDefault()
handlePaste()
}
if (combo === 'Ctrl+D' || combo === 'Command+D') {
event.preventDefault()
handleDuplicate()
}
if (combo === 'Delete') {
event.preventDefault()
handleRemove()
}
if (combo === 'Insert' || combo === 'Insert') {
event.preventDefault()
handleInsert('structure')
}
if (combo === 'Escape') {
event.preventDefault()
selection = null
}
}
if (combo === 'Ctrl+F' || combo === 'Command+F') {
event.preventDefault()
showSearch = true
}
if (combo === 'Ctrl+Z' || combo === 'Command+Z') {
event.preventDefault()
// TODO: find a better way to restore focus
const activeElement = document.activeElement
if (activeElement && activeElement.blur && activeElement.focus) {
activeElement.blur()
setTimeout(() => {
handleUndo()
setTimeout(() => activeElement.focus())
})
} else {
handleUndo()
}
}
if (combo === 'Ctrl+Shift+Z' || combo === 'Command+Shift+Z') {
event.preventDefault()
// TODO: find a better way to restore focus
const activeElement = document.activeElement
if (activeElement && activeElement.blur && activeElement.focus) {
activeElement.blur()
setTimeout(() => {
handleRedo()
setTimeout(() => activeElement.focus())
})
} else {
handleRedo()
}
}
}
</script>
<div class="jsoneditor" on:keydown={handleKeyDown}>
<Menu
historyState={historyState}
searchText={searchText}
searching={searching}
searchResult={searchResult}
bind:showSearch
selection={selection}
clipboard={clipboard}
onCut={handleCut}
onCopy={handleCopy}
onPaste={handlePaste}
onRemove={handleRemove}
onDuplicate={handleDuplicate}
onInsert={handleInsert}
onUndo={handleUndo}
onRedo={handleRedo}
onSort={handleSort}
onTransform={handleTransform}
onSearchText={handleSearchText}
onNextSearchResult={handleNextSearchResult}
onPreviousSearchResult={handlePreviousSearchResult}
/>
<label class="hidden-input-label">
<input
class="hidden-input"
class:visible={!!selection}
bind:this={domHiddenInput}
/>
</label>
<div class="contents" bind:this={divContents}>
<JSONNode
value={doc}
path={[]}
state={state}
searchResult={searchResult && searchResult.itemsWithActive}
validationErrors={validationErrors}
onPatch={handlePatch}
onUpdateKey={handleUpdateKey}
onExpand={handleExpand}
onSelect={handleSelect}
onExpandSection={handleExpandSection}
selection={selection}
/>
</div>
</div>
<style src="./TreeMode.scss"></style>

View File

@ -0,0 +1,6 @@
// used by JSONNode during dragging
export const singleton = {
mousedown: false,
selectionAnchor: null, // Path
selectionFocus: null // Path
}

35
src/constants.js Normal file
View File

@ -0,0 +1,35 @@
export const STATE_EXPANDED = Symbol('expanded')
export const STATE_LIMIT = Symbol('limit')
export const STATE_VISIBLE_SECTIONS = Symbol('visible sections')
export const STATE_PROPS = Symbol('props')
export const STATE_SEARCH_PROPERTY = Symbol('search:property')
export const STATE_SEARCH_VALUE = Symbol('search:value')
export const VALIDATION_ERROR = Symbol('validation:error')
export const SCROLL_DURATION = 300 // ms
export const DEBOUNCE_DELAY = 300
export const SEARCH_PROGRESS_THROTTLE = 300 // ms
export const MAX_SEARCH_RESULTS = 1000
export const ARRAY_SECTION_SIZE = 100
export const DEFAULT_VISIBLE_SECTIONS = [{ start: 0, end: ARRAY_SECTION_SIZE }]
export const MAX_PREVIEW_CHARACTERS = 20e3 // characters
export const INDENTATION_WIDTH = 18 // pixels IMPORTANT: keep in sync with sass constant $indentation-width
export const SIMPLE_MODAL_OPTIONS = {
closeButton: false,
styleBg: {
top: 0,
left: 0,
background: 'rgba(0, 0, 0, 0.3)',
justifyContent: 'normal'
},
styleWindow: {
borderRadius: '2px'
},
styleContent: {
padding: '0px',
overflow: 'visible' // needed for select box dropdowns which are larger than the modal
}
}

View File

@ -1,44 +0,0 @@
# Which files do I need?
Ehhh, that's quite some files in this dist folder. Which files do I need?
## Full version
If you're not sure which version to use, use the full version.
Which files are needed when using the full version?
- jsoneditor.min.js
- jsoneditor.map (optional, for debugging purposes only)
- jsoneditor.min.css
- img/jsoneditor-icons.svg
## Minimalist version
The minimalist version has excluded the following libraries:
- `ace` (via `brace`), used for the code editor.
- `ajv`, used for JSON schema validation.
- `vanilla-picker`, used as color picker.
This reduces the the size of the minified and gzipped JavaScript file
from about 210 kB to about 70 kB (one third).
When to use the minimalist version?
- If you don't need the mode "code" and don't need JSON schema validation.
- Or if you want to provide `ace` and/or `ajv` yourself via the configuration
options, for example when you already use Ace in other parts of your
web application too and don't want to bundle the library twice.
- You don't need the color picker, or want to provide your own
color picker using `onColorPicker`.
Which files are needed when using the minimalist version?
- jsoneditor-minimalist.min.js
- jsoneditor-minimalist.map (optional, for debugging purposes only)
- jsoneditor.min.css
- img/jsoneditor-icons.svg

View File

@ -1,429 +0,0 @@
'use strict'
import { createAbsoluteAnchor } from './createAbsoluteAnchor'
import { addClassName, getSelection, removeClassName, setSelection } from './util'
import { translate } from './i18n'
/**
* A context menu
* @param {Object[]} items Array containing the menu structure
* TODO: describe structure
* @param {Object} [options] Object with options. Available options:
* {function} close Callback called when the
* context menu is being closed.
* @constructor
*/
export class ContextMenu {
constructor (items, options) {
this.dom = {}
const me = this
const dom = this.dom
this.anchor = undefined
this.items = items
this.eventListeners = {}
this.selection = undefined // holds the selection before the menu was opened
this.onClose = options ? options.close : undefined
// create root element
const root = document.createElement('div')
root.className = 'jsoneditor-contextmenu-root'
dom.root = root
// create a container element
const menu = document.createElement('div')
menu.className = 'jsoneditor-contextmenu'
dom.menu = menu
root.appendChild(menu)
// create a list to hold the menu items
const list = document.createElement('ul')
list.className = 'jsoneditor-menu'
menu.appendChild(list)
dom.list = list
dom.items = [] // list with all buttons
// create a (non-visible) button to set the focus to the menu
const focusButton = document.createElement('button')
focusButton.type = 'button'
dom.focusButton = focusButton
const li = document.createElement('li')
li.style.overflow = 'hidden'
li.style.height = '0'
li.appendChild(focusButton)
list.appendChild(li)
function createMenuItems (list, domItems, items) {
items.forEach(item => {
if (item.type === 'separator') {
// create a separator
const separator = document.createElement('div')
separator.className = 'jsoneditor-separator'
const li = document.createElement('li')
li.appendChild(separator)
list.appendChild(li)
} else {
const domItem = {}
// create a menu item
const li = document.createElement('li')
list.appendChild(li)
// create a button in the menu item
const button = document.createElement('button')
button.type = 'button'
button.className = item.className
domItem.button = button
if (item.title) {
button.title = item.title
}
if (item.click) {
button.onclick = event => {
event.preventDefault()
me.hide()
item.click()
}
}
li.appendChild(button)
// create the contents of the button
if (item.submenu) {
// add the icon to the button
const divIcon = document.createElement('div')
divIcon.className = 'jsoneditor-icon'
button.appendChild(divIcon)
const divText = document.createElement('div')
divText.className = 'jsoneditor-text' +
(item.click ? '' : ' jsoneditor-right-margin')
divText.appendChild(document.createTextNode(item.text))
button.appendChild(divText)
let buttonSubmenu
if (item.click) {
// submenu and a button with a click handler
button.className += ' jsoneditor-default'
const buttonExpand = document.createElement('button')
buttonExpand.type = 'button'
domItem.buttonExpand = buttonExpand
buttonExpand.className = 'jsoneditor-expand'
buttonExpand.innerHTML = '<div class="jsoneditor-expand"></div>'
li.appendChild(buttonExpand)
if (item.submenuTitle) {
buttonExpand.title = item.submenuTitle
}
buttonSubmenu = buttonExpand
} else {
// submenu and a button without a click handler
const divExpand = document.createElement('div')
divExpand.className = 'jsoneditor-expand'
button.appendChild(divExpand)
buttonSubmenu = button
}
// attach a handler to expand/collapse the submenu
buttonSubmenu.onclick = event => {
event.preventDefault()
me._onExpandItem(domItem)
buttonSubmenu.focus()
}
// create the submenu
const domSubItems = []
domItem.subItems = domSubItems
const ul = document.createElement('ul')
domItem.ul = ul
ul.className = 'jsoneditor-menu'
ul.style.height = '0'
li.appendChild(ul)
createMenuItems(ul, domSubItems, item.submenu)
} else {
// no submenu, just a button with clickhandler
button.innerHTML = '<div class="jsoneditor-icon"></div>' +
'<div class="jsoneditor-text">' + translate(item.text) + '</div>'
}
domItems.push(domItem)
}
})
}
createMenuItems(list, this.dom.items, items)
// TODO: when the editor is small, show the submenu on the right instead of inline?
// calculate the max height of the menu with one submenu expanded
this.maxHeight = 0 // height in pixels
items.forEach(item => {
const height = (items.length + (item.submenu ? item.submenu.length : 0)) * 24
me.maxHeight = Math.max(me.maxHeight, height)
})
}
/**
* Get the currently visible buttons
* @return {Array.<HTMLElement>} buttons
* @private
*/
_getVisibleButtons () {
const buttons = []
const me = this
this.dom.items.forEach(item => {
buttons.push(item.button)
if (item.buttonExpand) {
buttons.push(item.buttonExpand)
}
if (item.subItems && item === me.expandedItem) {
item.subItems.forEach(subItem => {
buttons.push(subItem.button)
if (subItem.buttonExpand) {
buttons.push(subItem.buttonExpand)
}
// TODO: change to fully recursive method
})
}
})
return buttons
}
/**
* Attach the menu to an anchor
* @param {HTMLElement} anchor Anchor where the menu will be attached as sibling.
* @param {HTMLElement} frame The root of the JSONEditor window
* @param {Boolean=} ignoreParent ignore anchor parent in regard to the calculation of the position, needed when the parent position is absolute
*/
show (anchor, frame, ignoreParent) {
this.hide()
// determine whether to display the menu below or above the anchor
let showBelow = true
const parent = anchor.parentNode
const anchorRect = anchor.getBoundingClientRect()
const parentRect = parent.getBoundingClientRect()
const frameRect = frame.getBoundingClientRect()
const me = this
this.dom.absoluteAnchor = createAbsoluteAnchor(anchor, frame, () => {
me.hide()
})
if (anchorRect.bottom + this.maxHeight < frameRect.bottom) {
// fits below -> show below
} else if (anchorRect.top - this.maxHeight > frameRect.top) {
// fits above -> show above
showBelow = false
} else {
// doesn't fit above nor below -> show below
}
const topGap = ignoreParent ? 0 : (anchorRect.top - parentRect.top)
// position the menu
if (showBelow) {
// display the menu below the anchor
const anchorHeight = anchor.offsetHeight
this.dom.menu.style.left = '0'
this.dom.menu.style.top = topGap + anchorHeight + 'px'
this.dom.menu.style.bottom = ''
} else {
// display the menu above the anchor
this.dom.menu.style.left = '0'
this.dom.menu.style.top = ''
this.dom.menu.style.bottom = '0px'
}
// attach the menu to the temporary, absolute anchor
// parent.insertBefore(this.dom.root, anchor);
this.dom.absoluteAnchor.appendChild(this.dom.root)
// move focus to the first button in the context menu
this.selection = getSelection()
this.anchor = anchor
setTimeout(() => {
me.dom.focusButton.focus()
}, 0)
if (ContextMenu.visibleMenu) {
ContextMenu.visibleMenu.hide()
}
ContextMenu.visibleMenu = this
}
/**
* Hide the context menu if visible
*/
hide () {
// remove temporary absolutely positioned anchor
if (this.dom.absoluteAnchor) {
this.dom.absoluteAnchor.destroy()
delete this.dom.absoluteAnchor
}
// remove the menu from the DOM
if (this.dom.root.parentNode) {
this.dom.root.parentNode.removeChild(this.dom.root)
if (this.onClose) {
this.onClose()
}
}
if (ContextMenu.visibleMenu === this) {
ContextMenu.visibleMenu = undefined
}
}
/**
* Expand a submenu
* Any currently expanded submenu will be hided.
* @param {Object} domItem
* @private
*/
_onExpandItem (domItem) {
const me = this
const alreadyVisible = (domItem === this.expandedItem)
// hide the currently visible submenu
const expandedItem = this.expandedItem
if (expandedItem) {
// var ul = expandedItem.ul;
expandedItem.ul.style.height = '0'
expandedItem.ul.style.padding = ''
setTimeout(() => {
if (me.expandedItem !== expandedItem) {
expandedItem.ul.style.display = ''
removeClassName(expandedItem.ul.parentNode, 'jsoneditor-selected')
}
}, 300) // timeout duration must match the css transition duration
this.expandedItem = undefined
}
if (!alreadyVisible) {
const ul = domItem.ul
ul.style.display = 'block'
// eslint-disable-next-line no-unused-expressions
ul.clientHeight // force a reflow in Firefox
setTimeout(() => {
if (me.expandedItem === domItem) {
let childsHeight = 0
for (let i = 0; i < ul.childNodes.length; i++) {
childsHeight += ul.childNodes[i].clientHeight
}
ul.style.height = childsHeight + 'px'
ul.style.padding = '5px 10px'
}
}, 0)
addClassName(ul.parentNode, 'jsoneditor-selected')
this.expandedItem = domItem
}
}
/**
* Handle onkeydown event
* @param {Event} event
* @private
*/
_onKeyDown (event) {
const target = event.target
const keynum = event.which
let handled = false
let buttons, targetIndex, prevButton, nextButton
if (keynum === 27) { // ESC
// hide the menu on ESC key
// restore previous selection and focus
if (this.selection) {
setSelection(this.selection)
}
if (this.anchor) {
this.anchor.focus()
}
this.hide()
handled = true
} else if (keynum === 9) { // Tab
if (!event.shiftKey) { // Tab
buttons = this._getVisibleButtons()
targetIndex = buttons.indexOf(target)
if (targetIndex === buttons.length - 1) {
// move to first button
buttons[0].focus()
handled = true
}
} else { // Shift+Tab
buttons = this._getVisibleButtons()
targetIndex = buttons.indexOf(target)
if (targetIndex === 0) {
// move to last button
buttons[buttons.length - 1].focus()
handled = true
}
}
} else if (keynum === 37) { // Arrow Left
if (target.className === 'jsoneditor-expand') {
buttons = this._getVisibleButtons()
targetIndex = buttons.indexOf(target)
prevButton = buttons[targetIndex - 1]
if (prevButton) {
prevButton.focus()
}
}
handled = true
} else if (keynum === 38) { // Arrow Up
buttons = this._getVisibleButtons()
targetIndex = buttons.indexOf(target)
prevButton = buttons[targetIndex - 1]
if (prevButton && prevButton.className === 'jsoneditor-expand') {
// skip expand button
prevButton = buttons[targetIndex - 2]
}
if (!prevButton) {
// move to last button
prevButton = buttons[buttons.length - 1]
}
if (prevButton) {
prevButton.focus()
}
handled = true
} else if (keynum === 39) { // Arrow Right
buttons = this._getVisibleButtons()
targetIndex = buttons.indexOf(target)
nextButton = buttons[targetIndex + 1]
if (nextButton && nextButton.className === 'jsoneditor-expand') {
nextButton.focus()
}
handled = true
} else if (keynum === 40) { // Arrow Down
buttons = this._getVisibleButtons()
targetIndex = buttons.indexOf(target)
nextButton = buttons[targetIndex + 1]
if (nextButton && nextButton.className === 'jsoneditor-expand') {
// skip expand button
nextButton = buttons[targetIndex + 2]
}
if (!nextButton) {
// move to first button
nextButton = buttons[0]
}
if (nextButton) {
nextButton.focus()
handled = true
}
handled = true
}
// TODO: arrow left and right
if (handled) {
event.stopPropagation()
event.preventDefault()
}
}
}
// currently displayed context menu, a singleton. We may only have one visible context menu
ContextMenu.visibleMenu = undefined
export default ContextMenu

View File

@ -1,169 +0,0 @@
/**
* Show errors and schema warnings in a clickable table view
* @param {Object} config
* @property {boolean} errorTableVisible
* @property {function (boolean) : void} onToggleVisibility
* @property {function (number)} [onFocusLine]
* @property {function (number)} onChangeHeight
* @constructor
*/
export class ErrorTable {
constructor (config) {
this.errorTableVisible = config.errorTableVisible
this.onToggleVisibility = config.onToggleVisibility
this.onFocusLine = config.onFocusLine || (() => {})
this.onChangeHeight = config.onChangeHeight
this.dom = {}
const validationErrorsContainer = document.createElement('div')
validationErrorsContainer.className = 'jsoneditor-validation-errors-container'
this.dom.validationErrorsContainer = validationErrorsContainer
const additionalErrorsIndication = document.createElement('div')
additionalErrorsIndication.style.display = 'none'
additionalErrorsIndication.className = 'jsoneditor-additional-errors fadein'
additionalErrorsIndication.innerHTML = 'Scroll for more &#9663;'
this.dom.additionalErrorsIndication = additionalErrorsIndication
validationErrorsContainer.appendChild(additionalErrorsIndication)
const validationErrorIcon = document.createElement('span')
validationErrorIcon.className = 'jsoneditor-validation-error-icon'
validationErrorIcon.style.display = 'none'
this.dom.validationErrorIcon = validationErrorIcon
const validationErrorCount = document.createElement('span')
validationErrorCount.className = 'jsoneditor-validation-error-count'
validationErrorCount.style.display = 'none'
this.dom.validationErrorCount = validationErrorCount
this.dom.parseErrorIndication = document.createElement('span')
this.dom.parseErrorIndication.className = 'jsoneditor-parse-error-icon'
this.dom.parseErrorIndication.style.display = 'none'
}
getErrorTable () {
return this.dom.validationErrorsContainer
}
getErrorCounter () {
return this.dom.validationErrorCount
}
getWarningIcon () {
return this.dom.validationErrorIcon
}
getErrorIcon () {
return this.dom.parseErrorIndication
}
toggleTableVisibility () {
this.errorTableVisible = !this.errorTableVisible
this.onToggleVisibility(this.errorTableVisible)
}
setErrors (errors, errorLocations) {
// clear any previous errors
if (this.dom.validationErrors) {
this.dom.validationErrors.parentNode.removeChild(this.dom.validationErrors)
this.dom.validationErrors = null
this.dom.additionalErrorsIndication.style.display = 'none'
}
// create the table with errors
// keep default behavior for parse errors
if (this.errorTableVisible && errors.length > 0) {
const validationErrors = document.createElement('div')
validationErrors.className = 'jsoneditor-validation-errors'
validationErrors.innerHTML = '<table class="jsoneditor-text-errors"><tbody></tbody></table>'
const tbody = validationErrors.getElementsByTagName('tbody')[0]
errors.forEach(error => {
let message
if (typeof error === 'string') {
message = '<td colspan="2"><pre>' + error + '</pre></td>'
} else {
message =
'<td>' + (error.dataPath || '') + '</td>' +
'<td><pre>' + error.message + '</pre></td>'
}
let line
if (!isNaN(error.line)) {
line = error.line
} else if (error.dataPath) {
const errLoc = errorLocations.find(loc => loc.path === error.dataPath)
if (errLoc) {
line = errLoc.line + 1
}
}
const trEl = document.createElement('tr')
trEl.className = !isNaN(line) ? 'jump-to-line' : ''
if (error.type === 'error') {
trEl.className += ' parse-error'
} else {
trEl.className += ' validation-error'
}
trEl.innerHTML = ('<td><button class="jsoneditor-schema-error"></button></td><td style="white-space:nowrap;">' + (!isNaN(line) ? ('Ln ' + line) : '') + '</td>' + message)
trEl.onclick = () => {
this.onFocusLine(line)
}
tbody.appendChild(trEl)
})
this.dom.validationErrors = validationErrors
this.dom.validationErrorsContainer.appendChild(validationErrors)
this.dom.additionalErrorsIndication.title = errors.length + ' errors total'
if (this.dom.validationErrorsContainer.clientHeight < this.dom.validationErrorsContainer.scrollHeight) {
this.dom.additionalErrorsIndication.style.display = 'block'
this.dom.validationErrorsContainer.onscroll = () => {
this.dom.additionalErrorsIndication.style.display =
(this.dom.validationErrorsContainer.clientHeight > 0 && this.dom.validationErrorsContainer.scrollTop === 0) ? 'block' : 'none'
}
} else {
this.dom.validationErrorsContainer.onscroll = undefined
}
const height = this.dom.validationErrorsContainer.clientHeight + (this.dom.statusBar ? this.dom.statusBar.clientHeight : 0)
// this.content.style.marginBottom = (-height) + 'px';
// this.content.style.paddingBottom = height + 'px';
this.onChangeHeight(height)
} else {
this.onChangeHeight(0)
}
// update the status bar
const validationErrorsCount = errors.filter(error => error.type !== 'error').length
if (validationErrorsCount > 0) {
this.dom.validationErrorCount.style.display = 'inline'
this.dom.validationErrorCount.innerText = validationErrorsCount
this.dom.validationErrorCount.onclick = this.toggleTableVisibility.bind(this)
this.dom.validationErrorIcon.style.display = 'inline'
this.dom.validationErrorIcon.title = validationErrorsCount + ' schema validation error(s) found'
this.dom.validationErrorIcon.onclick = this.toggleTableVisibility.bind(this)
} else {
this.dom.validationErrorCount.style.display = 'none'
this.dom.validationErrorIcon.style.display = 'none'
}
// update the parse error icon
const hasParseErrors = errors.some(error => error.type === 'error')
if (hasParseErrors) {
const line = errors[0].line
this.dom.parseErrorIndication.style.display = 'block'
this.dom.parseErrorIndication.title = !isNaN(line)
? ('parse error on line ' + line)
: 'parse error - check that the json is valid'
this.dom.parseErrorIndication.onclick = this.toggleTableVisibility.bind(this)
} else {
this.dom.parseErrorIndication.style.display = 'none'
}
}
}

View File

@ -1,100 +0,0 @@
'use strict'
/**
* @constructor FocusTracker
* A custom focus tracker for a DOM element with complex internal DOM structure
* @param {[Object]} config A set of configurations for the FocusTracker
* {DOM Object} target * The DOM object to track (required)
* {Function} onFocus onFocus callback
* {Function} onBlur onBlur callback
*
* @return
*/
export class FocusTracker {
constructor (config) {
this.target = config.target || null
if (!this.target) {
throw new Error('FocusTracker constructor called without a "target" to track.')
}
this.onFocus = (typeof config.onFocus === 'function') ? config.onFocus : null
this.onBlur = (typeof config.onBlur === 'function') ? config.onBlur : null
this._onClick = this._onEvent.bind(this)
this._onKeyUp = function (event) {
if (event.which === 9 || event.keyCode === 9) {
this._onEvent(event)
}
}.bind(this)
this.focusFlag = false
this.firstEventFlag = true
/*
Adds required (click and keyup) event listeners to the 'document' object
to track the focus of the given 'target'
*/
if (this.onFocus || this.onBlur) {
document.addEventListener('click', this._onClick)
document.addEventListener('keyup', this._onKeyUp)
}
}
/**
* Removes the event listeners on the 'document' object
* that were added to track the focus of the given 'target'
*/
destroy () {
document.removeEventListener('click', this._onClick)
document.removeEventListener('keyup', this._onKeyUp)
this._onEvent({ target: document.body }) // calling _onEvent with body element in the hope that the FocusTracker is added to an element inside the body tag
}
/**
* Tracks the focus of the target and calls the onFocus and onBlur
* event callbacks if available.
* @param {Event} [event] The 'click' or 'keyup' event object,
* from the respective events set on
* document object
* @private
*/
_onEvent (event) {
const target = event.target
let focusFlag
if (target === this.target) {
focusFlag = true
} else if (this.target.contains(target) || this.target.contains(document.activeElement)) {
focusFlag = true
} else {
focusFlag = false
}
if (focusFlag) {
if (!this.focusFlag) {
// trigger the onFocus callback
if (this.onFocus) {
this.onFocus({ type: 'focus', target: this.target })
}
this.focusFlag = true
}
} else {
if (this.focusFlag || this.firstEventFlag) {
// trigger the onBlur callback
if (this.onBlur) {
this.onBlur({ type: 'blur', target: this.target })
}
this.focusFlag = false
/*
When switching from one mode to another in the editor, the FocusTracker gets recreated.
At that time, this.focusFlag will be init to 'false' and will fail the above if condition, when blur occurs
this.firstEventFlag is added to overcome that issue
*/
if (this.firstEventFlag) {
this.firstEventFlag = false
}
}
}
}
}

View File

@ -1,86 +0,0 @@
'use strict'
/**
* The highlighter can highlight/unhighlight a node, and
* animate the visibility of a context menu.
* @constructor Highlighter
*/
export class Highlighter {
constructor () {
this.locked = false
}
/**
* Hightlight given node and its childs
* @param {Node} node
*/
highlight (node) {
if (this.locked) {
return
}
if (this.node !== node) {
// unhighlight current node
if (this.node) {
this.node.setHighlight(false)
}
// highlight new node
this.node = node
this.node.setHighlight(true)
}
// cancel any current timeout
this._cancelUnhighlight()
}
/**
* Unhighlight currently highlighted node.
* Will be done after a delay
*/
unhighlight () {
if (this.locked) {
return
}
const me = this
if (this.node) {
this._cancelUnhighlight()
// do the unhighlighting after a small delay, to prevent re-highlighting
// the same node when moving from the drag-icon to the contextmenu-icon
// or vice versa.
this.unhighlightTimer = setTimeout(() => {
me.node.setHighlight(false)
me.node = undefined
me.unhighlightTimer = undefined
}, 0)
}
}
/**
* Cancel an unhighlight action (if before the timeout of the unhighlight action)
* @private
*/
_cancelUnhighlight () {
if (this.unhighlightTimer) {
clearTimeout(this.unhighlightTimer)
this.unhighlightTimer = undefined
}
}
/**
* Lock highlighting or unhighlighting nodes.
* methods highlight and unhighlight do not work while locked.
*/
lock () {
this.locked = true
}
/**
* Unlock highlighting or unhighlighting nodes
*/
unlock () {
this.locked = false
}
}

View File

@ -1,85 +0,0 @@
/**
* Keep track on any history, be able
* @param {function} onChange
* @param {function} calculateItemSize
* @param {number} limit Maximum size of all items in history
* @constructor
*/
export class History {
constructor (onChange, calculateItemSize, limit) {
this.onChange = onChange
this.calculateItemSize = calculateItemSize || (() => 1)
this.limit = limit
this.items = []
this.index = -1
}
add (item) {
// limit number of items in history so that the total size doesn't
// always keep at least one item in memory
while (this._calculateHistorySize() > this.limit && this.items.length > 1) {
this.items.shift()
this.index--
}
// cleanup any redo action that are not valid anymore
this.items = this.items.slice(0, this.index + 1)
this.items.push(item)
this.index++
this.onChange()
}
_calculateHistorySize () {
const calculateItemSize = this.calculateItemSize
let totalSize = 0
this.items.forEach(item => {
totalSize += calculateItemSize(item)
})
return totalSize
}
undo () {
if (!this.canUndo()) {
return
}
this.index--
this.onChange()
return this.items[this.index]
}
redo () {
if (!this.canRedo()) {
return
}
this.index++
this.onChange()
return this.items[this.index]
}
canUndo () {
return this.index > 0
}
canRedo () {
return this.index < this.items.length - 1
}
clear () {
this.items = []
this.index = -1
this.onChange()
}
}

View File

@ -1,491 +0,0 @@
'use strict'
const ace = require('./ace') // may be undefined in case of minimalist bundle
const VanillaPicker = require('./vanilla-picker') // may be undefined in case of minimalist bundle
const { treeModeMixins } = require('./treemode')
const { textModeMixins } = require('./textmode')
const { previewModeMixins } = require('./previewmode')
const { clear, extend, getInternetExplorerVersion, parse } = require('./util')
const { tryRequireAjv } = require('./tryRequireAjv')
const { showTransformModal } = require('./showTransformModal')
const { showSortModal } = require('./showSortModal')
const Ajv = tryRequireAjv()
if (typeof Promise === 'undefined') {
console.error('Promise undefined. Please load a Promise polyfill in the browser in order to use JSONEditor')
}
/**
* @constructor JSONEditor
* @param {Element} container Container element
* @param {Object} [options] Object with options. available options:
* {String} mode Editor mode. Available values:
* 'tree' (default), 'view',
* 'form', 'text', and 'code'.
* {function} onChange Callback method, triggered
* on change of contents.
* Does not pass the contents itself.
* See also `onChangeJSON` and
* `onChangeText`.
* {function} onChangeJSON Callback method, triggered
* in modes on change of contents,
* passing the changed contents
* as JSON.
* Only applicable for modes
* 'tree', 'view', and 'form'.
* {function} onChangeText Callback method, triggered
* in modes on change of contents,
* passing the changed contents
* as stringified JSON.
* {function} onError Callback method, triggered
* when an error occurs
* {Boolean} search Enable search box.
* True by default
* Only applicable for modes
* 'tree', 'view', and 'form'
* {Boolean} history Enable history (undo/redo).
* True by default
* Only applicable for modes
* 'tree', 'view', and 'form'
* {String} name Field name for the root node.
* Only applicable for modes
* 'tree', 'view', and 'form'
* {Number} indentation Number of indentation
* spaces. 4 by default.
* Only applicable for
* modes 'text' and 'code'
* {boolean} escapeUnicode If true, unicode
* characters are escaped.
* false by default.
* {boolean} sortObjectKeys If true, object keys are
* sorted before display.
* false by default.
* {function} onSelectionChange Callback method,
* triggered on node selection change
* Only applicable for modes
* 'tree', 'view', and 'form'
* {function} onTextSelectionChange Callback method,
* triggered on text selection change
* Only applicable for modes
* {HTMLElement} modalAnchor The anchor element to apply an
* overlay and display the modals in a
* centered location.
* Defaults to document.body
* 'text' and 'code'
* {function} onEvent Callback method, triggered
* when an event occurs in
* a JSON field or value.
* Only applicable for
* modes 'form', 'tree' and
* 'view'
* {function} onFocus Callback method, triggered
* when the editor comes into focus,
* passing an object {type, target},
* Applicable for all modes
* {function} onBlur Callback method, triggered
* when the editor goes out of focus,
* passing an object {type, target},
* Applicable for all modes
* {function} onClassName Callback method, triggered
* when a Node DOM is rendered. Function returns
* a css class name to be set on a node.
* Only applicable for
* modes 'form', 'tree' and
* 'view'
* {Number} maxVisibleChilds Number of children allowed for a node
* in 'tree', 'view', or 'form' mode before
* the "show more/show all" buttons appear.
* 100 by default.
*
* @param {Object | undefined} json JSON object
*/
function JSONEditor (container, options, json) {
if (!(this instanceof JSONEditor)) {
throw new Error('JSONEditor constructor called without "new".')
}
// check for unsupported browser (IE8 and older)
const ieVersion = getInternetExplorerVersion()
if (ieVersion !== -1 && ieVersion < 9) {
throw new Error('Unsupported browser, IE9 or newer required. ' +
'Please install the newest version of your browser.')
}
if (options) {
// check for deprecated options
if (options.error) {
console.warn('Option "error" has been renamed to "onError"')
options.onError = options.error
delete options.error
}
if (options.change) {
console.warn('Option "change" has been renamed to "onChange"')
options.onChange = options.change
delete options.change
}
if (options.editable) {
console.warn('Option "editable" has been renamed to "onEditable"')
options.onEditable = options.editable
delete options.editable
}
// warn if onChangeJSON is used when mode can be `text` or `code`
if (options.onChangeJSON) {
if (options.mode === 'text' || options.mode === 'code' ||
(options.modes && (options.modes.indexOf('text') !== -1 || options.modes.indexOf('code') !== -1))) {
console.warn('Option "onChangeJSON" is not applicable to modes "text" and "code". ' +
'Use "onChangeText" or "onChange" instead.')
}
}
// validate options
if (options) {
Object.keys(options).forEach(option => {
if (JSONEditor.VALID_OPTIONS.indexOf(option) === -1) {
console.warn('Unknown option "' + option + '". This option will be ignored')
}
})
}
}
if (arguments.length) {
this._create(container, options, json)
}
}
/**
* Configuration for all registered modes. Example:
* {
* tree: {
* mixin: TreeEditor,
* data: 'json'
* },
* text: {
* mixin: TextEditor,
* data: 'text'
* }
* }
*
* @type { Object.<String, {mixin: Object, data: String} > }
*/
JSONEditor.modes = {}
// debounce interval for JSON schema validation in milliseconds
JSONEditor.prototype.DEBOUNCE_INTERVAL = 150
JSONEditor.VALID_OPTIONS = [
'ajv', 'schema', 'schemaRefs', 'templates',
'ace', 'theme', 'autocomplete',
'onChange', 'onChangeJSON', 'onChangeText',
'onEditable', 'onError', 'onEvent', 'onModeChange', 'onNodeName', 'onValidate', 'onCreateMenu',
'onSelectionChange', 'onTextSelectionChange', 'onClassName',
'onFocus', 'onBlur',
'colorPicker', 'onColorPicker',
'timestampTag', 'timestampFormat',
'escapeUnicode', 'history', 'search', 'mode', 'modes', 'name', 'indentation',
'sortObjectKeys', 'navigationBar', 'statusBar', 'mainMenuBar', 'languages', 'language', 'enableSort', 'enableTransform',
'maxVisibleChilds', 'onValidationError',
'modalAnchor', 'popupAnchor',
'createQuery', 'executeQuery', 'queryDescription'
]
/**
* Create the JSONEditor
* @param {Element} container Container element
* @param {Object} [options] See description in constructor
* @param {Object | undefined} json JSON object
* @private
*/
JSONEditor.prototype._create = function (container, options, json) {
this.container = container
this.options = options || {}
this.json = json || {}
const mode = this.options.mode || (this.options.modes && this.options.modes[0]) || 'tree'
this.setMode(mode)
}
/**
* Destroy the editor. Clean up DOM, event listeners, and web workers.
*/
JSONEditor.prototype.destroy = () => {}
/**
* Set JSON object in editor
* @param {Object | undefined} json JSON data
*/
JSONEditor.prototype.set = function (json) {
this.json = json
}
/**
* Get JSON from the editor
* @returns {Object} json
*/
JSONEditor.prototype.get = function () {
return this.json
}
/**
* Set string containing JSON for the editor
* @param {String | undefined} jsonText
*/
JSONEditor.prototype.setText = function (jsonText) {
this.json = parse(jsonText)
}
/**
* Get stringified JSON contents from the editor
* @returns {String} jsonText
*/
JSONEditor.prototype.getText = function () {
return JSON.stringify(this.json)
}
/**
* Set a field name for the root node.
* @param {String | undefined} name
*/
JSONEditor.prototype.setName = function (name) {
if (!this.options) {
this.options = {}
}
this.options.name = name
}
/**
* Get the field name for the root node.
* @return {String | undefined} name
*/
JSONEditor.prototype.getName = function () {
return this.options && this.options.name
}
/**
* Change the mode of the editor.
* JSONEditor will be extended with all methods needed for the chosen mode.
* @param {String} mode Available modes: 'tree' (default), 'view', 'form',
* 'text', and 'code'.
*/
JSONEditor.prototype.setMode = function (mode) {
// if the mode is the same as current mode (and it's not the first time), do nothing.
if (mode === this.options.mode && this.create) {
return
}
const container = this.container
const options = extend({}, this.options)
const oldMode = options.mode
let data
let name
options.mode = mode
const config = JSONEditor.modes[mode]
if (config) {
try {
const asText = (config.data === 'text')
name = this.getName()
data = this[asText ? 'getText' : 'get']() // get text or json
this.destroy()
clear(this)
extend(this, config.mixin)
this.create(container, options)
this.setName(name)
this[asText ? 'setText' : 'set'](data) // set text or json
if (typeof config.load === 'function') {
try {
config.load.call(this)
} catch (err) {
console.error(err)
}
}
if (typeof options.onModeChange === 'function' && mode !== oldMode) {
try {
options.onModeChange(mode, oldMode)
} catch (err) {
console.error(err)
}
}
} catch (err) {
this._onError(err)
}
} else {
throw new Error('Unknown mode "' + options.mode + '"')
}
}
/**
* Get the current mode
* @return {string}
*/
JSONEditor.prototype.getMode = function () {
return this.options.mode
}
/**
* Throw an error. If an error callback is configured in options.error, this
* callback will be invoked. Else, a regular error is thrown.
* @param {Error} err
* @private
*/
JSONEditor.prototype._onError = function (err) {
if (this.options && typeof this.options.onError === 'function') {
this.options.onError(err)
} else {
throw err
}
}
/**
* Set a JSON schema for validation of the JSON object.
* To remove the schema, call JSONEditor.setSchema(null)
* @param {Object | null} schema
* @param {Object.<string, Object>=} schemaRefs Schemas that are referenced using the `$ref` property from the JSON schema that are set in the `schema` option,
+ the object structure in the form of `{reference_key: schemaObject}`
*/
JSONEditor.prototype.setSchema = function (schema, schemaRefs) {
// compile a JSON schema validator if a JSON schema is provided
if (schema) {
let ajv
try {
// grab ajv from options if provided, else create a new instance
if (this.options.ajv) {
ajv = this.options.ajv
} else {
ajv = Ajv({
allErrors: true,
verbose: true,
schemaId: 'auto',
$data: true
})
// support both draft-04 and draft-06 alongside the latest draft-07
ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-04.json'))
ajv.addMetaSchema(require('ajv/lib/refs/json-schema-draft-06.json'))
}
} catch (err) {
console.warn('Failed to create an instance of Ajv, JSON Schema validation is not available. Please use a JSONEditor bundle including Ajv, or pass an instance of Ajv as via the configuration option `ajv`.')
}
if (ajv) {
if (schemaRefs) {
for (const ref in schemaRefs) {
ajv.removeSchema(ref) // When updating a schema - old refs has to be removed first
if (schemaRefs[ref]) {
ajv.addSchema(schemaRefs[ref], ref)
}
}
this.options.schemaRefs = schemaRefs
}
this.validateSchema = ajv.compile(schema)
// add schema to the options, so that when switching to an other mode,
// the set schema is not lost
this.options.schema = schema
// validate now
this.validate()
}
this.refresh() // update DOM
} else {
// remove current schema
this.validateSchema = null
this.options.schema = null
this.options.schemaRefs = null
this.validate() // to clear current error messages
this.refresh() // update DOM
}
}
/**
* Validate current JSON object against the configured JSON schema
* Throws an exception when no JSON schema is configured
*/
JSONEditor.prototype.validate = () => {
// must be implemented by treemode and textmode
}
/**
* Refresh the rendered contents
*/
JSONEditor.prototype.refresh = () => {
// can be implemented by treemode and textmode
}
/**
* Register a plugin with one ore multiple modes for the JSON Editor.
*
* A mode is described as an object with properties:
*
* - `mode: String` The name of the mode.
* - `mixin: Object` An object containing the mixin functions which
* will be added to the JSONEditor. Must contain functions
* create, get, getText, set, and setText. May have
* additional functions.
* When the JSONEditor switches to a mixin, all mixin
* functions are added to the JSONEditor, and then
* the function `create(container, options)` is executed.
* - `data: 'text' | 'json'` The type of data that will be used to load the mixin.
* - `[load: function]` An optional function called after the mixin
* has been loaded.
*
* @param {Object | Array} mode A mode object or an array with multiple mode objects.
*/
JSONEditor.registerMode = mode => {
let i, prop
if (Array.isArray(mode)) {
// multiple modes
for (i = 0; i < mode.length; i++) {
JSONEditor.registerMode(mode[i])
}
} else {
// validate the new mode
if (!('mode' in mode)) throw new Error('Property "mode" missing')
if (!('mixin' in mode)) throw new Error('Property "mixin" missing')
if (!('data' in mode)) throw new Error('Property "data" missing')
const name = mode.mode
if (name in JSONEditor.modes) {
throw new Error('Mode "' + name + '" already registered')
}
// validate the mixin
if (typeof mode.mixin.create !== 'function') {
throw new Error('Required function "create" missing on mixin')
}
const reserved = ['setMode', 'registerMode', 'modes']
for (i = 0; i < reserved.length; i++) {
prop = reserved[i]
if (prop in mode.mixin) {
throw new Error('Reserved property "' + prop + '" not allowed in mixin')
}
}
JSONEditor.modes[name] = mode
}
}
// register tree, text, and preview modes
JSONEditor.registerMode(treeModeMixins)
JSONEditor.registerMode(textModeMixins)
JSONEditor.registerMode(previewModeMixins)
// expose some of the libraries that can be used customized
JSONEditor.ace = ace
JSONEditor.Ajv = Ajv
JSONEditor.VanillaPicker = VanillaPicker
// expose some utils (this is undocumented, unofficial)
JSONEditor.showTransformModal = showTransformModal
JSONEditor.showSortModal = showSortModal
// default export for TypeScript ES6 projects
JSONEditor.default = JSONEditor
module.exports = JSONEditor

View File

@ -1,123 +0,0 @@
'use strict'
import { ContextMenu } from './ContextMenu'
import { translate } from './i18n'
/**
* Create a select box to be used in the editor menu's, which allows to switch mode
* @param {HTMLElement} container
* @param {String[]} modes Available modes: 'code', 'form', 'text', 'tree', 'view', 'preview'
* @param {String} current Available modes: 'code', 'form', 'text', 'tree', 'view', 'preview'
* @param {function(mode: string)} onSwitch Callback invoked on switch
* @constructor
*/
export class ModeSwitcher {
constructor (container, modes, current, onSwitch) {
// available modes
const availableModes = {
code: {
text: translate('modeCodeText'),
title: translate('modeCodeTitle'),
click: function () {
onSwitch('code')
}
},
form: {
text: translate('modeFormText'),
title: translate('modeFormTitle'),
click: function () {
onSwitch('form')
}
},
text: {
text: translate('modeTextText'),
title: translate('modeTextTitle'),
click: function () {
onSwitch('text')
}
},
tree: {
text: translate('modeTreeText'),
title: translate('modeTreeTitle'),
click: function () {
onSwitch('tree')
}
},
view: {
text: translate('modeViewText'),
title: translate('modeViewTitle'),
click: function () {
onSwitch('view')
}
},
preview: {
text: translate('modePreviewText'),
title: translate('modePreviewTitle'),
click: function () {
onSwitch('preview')
}
}
}
// list the selected modes
const items = []
for (let i = 0; i < modes.length; i++) {
const mode = modes[i]
const item = availableModes[mode]
if (!item) {
throw new Error('Unknown mode "' + mode + '"')
}
item.className = 'jsoneditor-type-modes' + ((current === mode) ? ' jsoneditor-selected' : '')
items.push(item)
}
// retrieve the title of current mode
const currentMode = availableModes[current]
if (!currentMode) {
throw new Error('Unknown mode "' + current + '"')
}
const currentTitle = currentMode.text
// create the html element
const box = document.createElement('button')
box.type = 'button'
box.className = 'jsoneditor-modes jsoneditor-separator'
box.innerHTML = currentTitle + ' &#x25BE;'
box.title = translate('modeEditorTitle')
box.onclick = () => {
const menu = new ContextMenu(items)
menu.show(box, container)
}
const frame = document.createElement('div')
frame.className = 'jsoneditor-modes'
frame.style.position = 'relative'
frame.appendChild(box)
container.appendChild(frame)
this.dom = {
container: container,
box: box,
frame: frame
}
}
/**
* Set focus to switcher
*/
focus () {
this.dom.box.focus()
}
/**
* Destroy the ModeSwitcher, remove from DOM
*/
destroy () {
if (this.dom && this.dom.frame && this.dom.frame.parentNode) {
this.dom.frame.parentNode.removeChild(this.dom.frame)
}
this.dom = null
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,333 +0,0 @@
'use strict'
import { findUniqueName } from './util'
/**
* @constructor History
* Store action history, enables undo and redo
* @param {JSONEditor} editor
*/
export class NodeHistory {
constructor (editor) {
this.editor = editor
this.history = []
this.index = -1
this.clear()
// helper function to find a Node from a path
function findNode (path) {
return editor.node.findNodeByInternalPath(path)
}
// map with all supported actions
this.actions = {
editField: {
undo: function (params) {
const parentNode = findNode(params.parentPath)
const node = parentNode.childs[params.index]
node.updateField(params.oldValue)
},
redo: function (params) {
const parentNode = findNode(params.parentPath)
const node = parentNode.childs[params.index]
node.updateField(params.newValue)
}
},
editValue: {
undo: function (params) {
findNode(params.path).updateValue(params.oldValue)
},
redo: function (params) {
findNode(params.path).updateValue(params.newValue)
}
},
changeType: {
undo: function (params) {
findNode(params.path).changeType(params.oldType)
},
redo: function (params) {
findNode(params.path).changeType(params.newType)
}
},
appendNodes: {
undo: function (params) {
const parentNode = findNode(params.parentPath)
params.paths.map(findNode).forEach(node => {
parentNode.removeChild(node)
})
},
redo: function (params) {
const parentNode = findNode(params.parentPath)
params.nodes.forEach(node => {
parentNode.appendChild(node)
})
}
},
insertBeforeNodes: {
undo: function (params) {
const parentNode = findNode(params.parentPath)
params.paths.map(findNode).forEach(node => {
parentNode.removeChild(node)
})
},
redo: function (params) {
const parentNode = findNode(params.parentPath)
const beforeNode = findNode(params.beforePath)
params.nodes.forEach(node => {
parentNode.insertBefore(node, beforeNode)
})
}
},
insertAfterNodes: {
undo: function (params) {
const parentNode = findNode(params.parentPath)
params.paths.map(findNode).forEach(node => {
parentNode.removeChild(node)
})
},
redo: function (params) {
const parentNode = findNode(params.parentPath)
let afterNode = findNode(params.afterPath)
params.nodes.forEach(node => {
parentNode.insertAfter(node, afterNode)
afterNode = node
})
}
},
removeNodes: {
undo: function (params) {
const parentNode = findNode(params.parentPath)
const beforeNode = parentNode.childs[params.index] || parentNode.append
params.nodes.forEach(node => {
parentNode.insertBefore(node, beforeNode)
})
},
redo: function (params) {
const parentNode = findNode(params.parentPath)
params.paths.map(findNode).forEach(node => {
parentNode.removeChild(node)
})
}
},
duplicateNodes: {
undo: function (params) {
const parentNode = findNode(params.parentPath)
params.clonePaths.map(findNode).forEach(node => {
parentNode.removeChild(node)
})
},
redo: function (params) {
const parentNode = findNode(params.parentPath)
let afterNode = findNode(params.afterPath)
const nodes = params.paths.map(findNode)
nodes.forEach(node => {
const clone = node.clone()
if (parentNode.type === 'object') {
const existingFieldNames = parentNode.getFieldNames()
clone.field = findUniqueName(node.field, existingFieldNames)
}
parentNode.insertAfter(clone, afterNode)
afterNode = clone
})
}
},
moveNodes: {
undo: function (params) {
const oldParentNode = findNode(params.oldParentPath)
const newParentNode = findNode(params.newParentPath)
const oldBeforeNode = oldParentNode.childs[params.oldIndex] || oldParentNode.append
// first copy the nodes, then move them
const nodes = newParentNode.childs.slice(params.newIndex, params.newIndex + params.count)
nodes.forEach((node, index) => {
node.field = params.fieldNames[index]
oldParentNode.moveBefore(node, oldBeforeNode)
})
// This is a hack to work around an issue that we don't know tha original
// path of the new parent after dragging, as the node is already moved at that time.
if (params.newParentPathRedo === null) {
params.newParentPathRedo = newParentNode.getInternalPath()
}
},
redo: function (params) {
const oldParentNode = findNode(params.oldParentPathRedo)
const newParentNode = findNode(params.newParentPathRedo)
const newBeforeNode = newParentNode.childs[params.newIndexRedo] || newParentNode.append
// first copy the nodes, then move them
const nodes = oldParentNode.childs.slice(params.oldIndexRedo, params.oldIndexRedo + params.count)
nodes.forEach((node, index) => {
node.field = params.fieldNames[index]
newParentNode.moveBefore(node, newBeforeNode)
})
}
},
sort: {
undo: function (params) {
const node = findNode(params.path)
node.hideChilds()
node.childs = params.oldChilds
node.updateDom({ updateIndexes: true })
node.showChilds()
},
redo: function (params) {
const node = findNode(params.path)
node.hideChilds()
node.childs = params.newChilds
node.updateDom({ updateIndexes: true })
node.showChilds()
}
},
transform: {
undo: function (params) {
findNode(params.path).setInternalValue(params.oldValue)
// TODO: would be nice to restore the state of the node and childs
},
redo: function (params) {
findNode(params.path).setInternalValue(params.newValue)
// TODO: would be nice to restore the state of the node and childs
}
}
// TODO: restore the original caret position and selection with each undo
// TODO: implement history for actions "expand", "collapse", "scroll", "setDocument"
}
}
/**
* The method onChange is executed when the History is changed, and can
* be overloaded.
*/
onChange () {}
/**
* Add a new action to the history
* @param {String} action The executed action. Available actions: "editField",
* "editValue", "changeType", "appendNode",
* "removeNode", "duplicateNode", "moveNode"
* @param {Object} params Object containing parameters describing the change.
* The parameters in params depend on the action (for
* example for "editValue" the Node, old value, and new
* value are provided). params contains all information
* needed to undo or redo the action.
*/
add (action, params) {
this.index++
this.history[this.index] = {
action: action,
params: params,
timestamp: new Date()
}
// remove redo actions which are invalid now
if (this.index < this.history.length - 1) {
this.history.splice(this.index + 1, this.history.length - this.index - 1)
}
// fire onchange event
this.onChange()
}
/**
* Clear history
*/
clear () {
this.history = []
this.index = -1
// fire onchange event
this.onChange()
}
/**
* Check if there is an action available for undo
* @return {Boolean} canUndo
*/
canUndo () {
return (this.index >= 0)
}
/**
* Check if there is an action available for redo
* @return {Boolean} canRedo
*/
canRedo () {
return (this.index < this.history.length - 1)
}
/**
* Undo the last action
*/
undo () {
if (this.canUndo()) {
const obj = this.history[this.index]
if (obj) {
const action = this.actions[obj.action]
if (action && action.undo) {
action.undo(obj.params)
if (obj.params.oldSelection) {
try {
this.editor.setDomSelection(obj.params.oldSelection)
} catch (err) {
console.error(err)
}
}
} else {
console.error(new Error('unknown action "' + obj.action + '"'))
}
}
this.index--
// fire onchange event
this.onChange()
}
}
/**
* Redo the last action
*/
redo () {
if (this.canRedo()) {
this.index++
const obj = this.history[this.index]
if (obj) {
const action = this.actions[obj.action]
if (action && action.redo) {
action.redo(obj.params)
if (obj.params.newSelection) {
try {
this.editor.setDomSelection(obj.params.newSelection)
} catch (err) {
console.error(err)
}
}
} else {
console.error(new Error('unknown action "' + obj.action + '"'))
}
}
// fire onchange event
this.onChange()
}
}
/**
* Destroy history
*/
destroy () {
this.editor = null
this.history = []
this.index = -1
}
}

View File

@ -1,325 +0,0 @@
'use strict'
import { translate } from './i18n'
/**
* @constructor SearchBox
* Create a search box in given HTML container
* @param {JSONEditor} editor The JSON Editor to attach to
* @param {Element} container HTML container element of where to
* create the search box
*/
export class SearchBox {
constructor (editor, container) {
const searchBox = this
this.editor = editor
this.timeout = undefined
this.delay = 200 // ms
this.lastText = undefined
this.results = null
this.dom = {}
this.dom.container = container
const wrapper = document.createElement('div')
this.dom.wrapper = wrapper
wrapper.className = 'jsoneditor-search'
container.appendChild(wrapper)
const results = document.createElement('div')
this.dom.results = results
results.className = 'jsoneditor-results'
wrapper.appendChild(results)
const divInput = document.createElement('div')
this.dom.input = divInput
divInput.className = 'jsoneditor-frame'
divInput.title = translate('searchTitle')
wrapper.appendChild(divInput)
const refreshSearch = document.createElement('button')
refreshSearch.type = 'button'
refreshSearch.className = 'jsoneditor-refresh'
divInput.appendChild(refreshSearch)
const search = document.createElement('input')
search.type = 'text'
this.dom.search = search
search.oninput = event => {
searchBox._onDelayedSearch(event)
}
search.onchange = event => {
// For IE 9
searchBox._onSearch()
}
search.onkeydown = event => {
searchBox._onKeyDown(event)
}
search.onkeyup = event => {
searchBox._onKeyUp(event)
}
refreshSearch.onclick = event => {
search.select()
}
// TODO: ESC in FF restores the last input, is a FF bug, https://bugzilla.mozilla.org/show_bug.cgi?id=598819
divInput.appendChild(search)
const searchNext = document.createElement('button')
searchNext.type = 'button'
searchNext.title = translate('searchNextResultTitle')
searchNext.className = 'jsoneditor-next'
searchNext.onclick = () => {
searchBox.next()
}
divInput.appendChild(searchNext)
const searchPrevious = document.createElement('button')
searchPrevious.type = 'button'
searchPrevious.title = translate('searchPreviousResultTitle')
searchPrevious.className = 'jsoneditor-previous'
searchPrevious.onclick = () => {
searchBox.previous()
}
divInput.appendChild(searchPrevious)
}
/**
* Go to the next search result
* @param {boolean} [focus] If true, focus will be set to the next result
* focus is false by default.
*/
next (focus) {
if (this.results) {
let index = this.resultIndex !== null ? this.resultIndex + 1 : 0
if (index > this.results.length - 1) {
index = 0
}
this._setActiveResult(index, focus)
}
}
/**
* Go to the prevous search result
* @param {boolean} [focus] If true, focus will be set to the next result
* focus is false by default.
*/
previous (focus) {
if (this.results) {
const max = this.results.length - 1
let index = this.resultIndex !== null ? this.resultIndex - 1 : max
if (index < 0) {
index = max
}
this._setActiveResult(index, focus)
}
}
/**
* Set new value for the current active result
* @param {Number} index
* @param {boolean} [focus] If true, focus will be set to the next result.
* focus is false by default.
* @private
*/
_setActiveResult (index, focus) {
// de-activate current active result
if (this.activeResult) {
const prevNode = this.activeResult.node
const prevElem = this.activeResult.elem
if (prevElem === 'field') {
delete prevNode.searchFieldActive
} else {
delete prevNode.searchValueActive
}
prevNode.updateDom()
}
if (!this.results || !this.results[index]) {
// out of range, set to undefined
this.resultIndex = undefined
this.activeResult = undefined
return
}
this.resultIndex = index
// set new node active
const node = this.results[this.resultIndex].node
const elem = this.results[this.resultIndex].elem
if (elem === 'field') {
node.searchFieldActive = true
} else {
node.searchValueActive = true
}
this.activeResult = this.results[this.resultIndex]
node.updateDom()
// TODO: not so nice that the focus is only set after the animation is finished
node.scrollTo(() => {
if (focus) {
node.focus(elem)
}
})
}
/**
* Cancel any running onDelayedSearch.
* @private
*/
_clearDelay () {
if (this.timeout !== undefined) {
clearTimeout(this.timeout)
delete this.timeout
}
}
/**
* Start a timer to execute a search after a short delay.
* Used for reducing the number of searches while typing.
* @param {Event} event
* @private
*/
_onDelayedSearch (event) {
// execute the search after a short delay (reduces the number of
// search actions while typing in the search text box)
this._clearDelay()
const searchBox = this
this.timeout = setTimeout(event => {
searchBox._onSearch()
}, this.delay)
}
/**
* Handle onSearch event
* @param {boolean} [forceSearch] If true, search will be executed again even
* when the search text is not changed.
* Default is false.
* @private
*/
_onSearch (forceSearch) {
this._clearDelay()
const value = this.dom.search.value
const text = value.length > 0 ? value : undefined
if (text !== this.lastText || forceSearch) {
// only search again when changed
this.lastText = text
this.results = this.editor.search(text)
const MAX_SEARCH_RESULTS = this.results[0]
? this.results[0].node.MAX_SEARCH_RESULTS
: Infinity
// try to maintain the current active result if this is still part of the new search results
let activeResultIndex = 0
if (this.activeResult) {
for (let i = 0; i < this.results.length; i++) {
if (this.results[i].node === this.activeResult.node) {
activeResultIndex = i
break
}
}
}
this._setActiveResult(activeResultIndex, false)
// display search results
if (text !== undefined) {
const resultCount = this.results.length
if (resultCount === 0) {
this.dom.results.innerHTML = 'no&nbsp;results'
} else if (resultCount === 1) {
this.dom.results.innerHTML = '1&nbsp;result'
} else if (resultCount > MAX_SEARCH_RESULTS) {
this.dom.results.innerHTML = MAX_SEARCH_RESULTS + '+&nbsp;results'
} else {
this.dom.results.innerHTML = resultCount + '&nbsp;results'
}
} else {
this.dom.results.innerHTML = ''
}
}
}
/**
* Handle onKeyDown event in the input box
* @param {Event} event
* @private
*/
_onKeyDown (event) {
const keynum = event.which
if (keynum === 27) {
// ESC
this.dom.search.value = '' // clear search
this._onSearch()
event.preventDefault()
event.stopPropagation()
} else if (keynum === 13) {
// Enter
if (event.ctrlKey) {
// force to search again
this._onSearch(true)
} else if (event.shiftKey) {
// move to the previous search result
this.previous()
} else {
// move to the next search result
this.next()
}
event.preventDefault()
event.stopPropagation()
}
}
/**
* Handle onKeyUp event in the input box
* @param {Event} event
* @private
*/
_onKeyUp (event) {
const keynum = event.keyCode
if (keynum !== 27 && keynum !== 13) {
// !show and !Enter
this._onDelayedSearch(event) // For IE 9
}
}
/**
* Clear the search results
*/
clear () {
this.dom.search.value = ''
this._onSearch()
}
/**
* Refresh searchResults if there is a search value
*/
forceSearch () {
this._onSearch(true)
}
/**
* Test whether the search box value is empty
* @returns {boolean} Returns true when empty.
*/
isEmpty () {
return this.dom.search.value === ''
}
/**
* Destroy the search box
*/
destroy () {
this.editor = null
this.dom.container.removeChild(this.dom.wrapper)
this.dom = null
this.results = null
this.activeResult = null
this._clearDelay()
}
}

View File

@ -1,142 +0,0 @@
'use strict'
import { ContextMenu } from './ContextMenu'
import { translate } from './i18n'
import { addClassName, removeClassName } from './util'
/**
* Creates a component that visualize path selection in tree based editors
* @param {HTMLElement} container
* @param {HTMLElement} root
* @constructor
*/
export class TreePath {
constructor (container, root) {
if (container) {
this.root = root
this.path = document.createElement('div')
this.path.className = 'jsoneditor-treepath'
this.path.setAttribute('tabindex', 0)
this.contentMenuClicked = false
container.appendChild(this.path)
this.reset()
}
}
/**
* Reset component to initial status
*/
reset () {
this.path.innerHTML = translate('selectNode')
}
/**
* Renders the component UI according to a given path objects
* @param {Array<{name: String, childs: Array}>} pathObjs a list of path objects
*
*/
setPath (pathObjs) {
const me = this
this.path.innerHTML = ''
if (pathObjs && pathObjs.length) {
pathObjs.forEach((pathObj, idx) => {
const pathEl = document.createElement('span')
let sepEl
pathEl.className = 'jsoneditor-treepath-element'
pathEl.innerText = pathObj.name
pathEl.onclick = _onSegmentClick.bind(me, pathObj)
me.path.appendChild(pathEl)
if (pathObj.children.length) {
sepEl = document.createElement('span')
sepEl.className = 'jsoneditor-treepath-seperator'
sepEl.innerHTML = '&#9658;'
sepEl.onclick = () => {
me.contentMenuClicked = true
const items = []
pathObj.children.forEach(child => {
items.push({
text: child.name,
className: 'jsoneditor-type-modes' + (pathObjs[idx + 1] + 1 && pathObjs[idx + 1].name === child.name ? ' jsoneditor-selected' : ''),
click: _onContextMenuItemClick.bind(me, pathObj, child.name)
})
})
const menu = new ContextMenu(items)
menu.show(sepEl, me.root, true)
}
me.path.appendChild(sepEl)
}
if (idx === pathObjs.length - 1) {
const leftRectPos = (sepEl || pathEl).getBoundingClientRect().right
if (me.path.offsetWidth < leftRectPos) {
me.path.scrollLeft = leftRectPos
}
if (me.path.scrollLeft) {
const showAllBtn = document.createElement('span')
showAllBtn.className = 'jsoneditor-treepath-show-all-btn'
showAllBtn.title = 'show all path'
showAllBtn.innerHTML = '...'
showAllBtn.onclick = _onShowAllClick.bind(me, pathObjs)
me.path.insertBefore(showAllBtn, me.path.firstChild)
}
}
})
}
function _onShowAllClick (pathObjs) {
me.contentMenuClicked = false
addClassName(me.path, 'show-all')
me.path.style.width = me.path.parentNode.getBoundingClientRect().width - 10 + 'px'
me.path.onblur = () => {
if (me.contentMenuClicked) {
me.contentMenuClicked = false
me.path.focus()
return
}
removeClassName(me.path, 'show-all')
me.path.onblur = undefined
me.path.style.width = ''
me.setPath(pathObjs)
}
}
function _onSegmentClick (pathObj) {
if (this.selectionCallback) {
this.selectionCallback(pathObj)
}
}
function _onContextMenuItemClick (pathObj, selection) {
if (this.contextMenuCallback) {
this.contextMenuCallback(pathObj, selection)
}
}
}
/**
* set a callback function for selection of path section
* @param {Function} callback function to invoke when section is selected
*/
onSectionSelected (callback) {
if (typeof callback === 'function') {
this.selectionCallback = callback
}
}
/**
* set a callback function for selection of path section
* @param {Function} callback function to invoke when section is selected
*/
onContextMenuItemSelected (callback) {
if (typeof callback === 'function') {
this.contextMenuCallback = callback
}
}
}

View File

@ -1,24 +0,0 @@
let ace
if (window.ace) {
// use the already loaded instance of Ace
ace = window.ace
} else {
try {
// load Ace editor
ace = require('ace-builds/src-noconflict/ace')
// load required Ace plugins
require('ace-builds/src-noconflict/mode-json')
require('ace-builds/src-noconflict/ext-searchbox')
// embed Ace json worker
// https://github.com/ajaxorg/ace/issues/3913
const jsonWorkerDataUrl = require('../generated/worker-json-data-url')
ace.config.setModuleUrl('ace/mode/json_worker', jsonWorkerDataUrl)
} catch (err) {
// failed to load Ace (can be minimalist bundle).
// No worries, the editor will fall back to plain text if needed.
}
}
module.exports = ace

View File

@ -1,144 +0,0 @@
/* ***** 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 ***** */
window.ace.define('ace/theme/jsoneditor', ['require', 'exports', 'module', 'ace/lib/dom'], (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: "dejavu sans mono", "droid sans mono", consolas, monaco, "lucida console", "courier new", courier, monospace, sans-serif;
line-height: 1.3;
background-color: #fff;
}
.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
}`
const dom = acequire('../lib/dom')
dom.importCssString(exports.cssText, exports.cssClass)
})

View File

@ -1,251 +0,0 @@
'use strict'
import { ContextMenu } from './ContextMenu'
import { translate } from './i18n'
import { addClassName, removeClassName } from './util'
/**
* A factory function to create an AppendNode, which depends on a Node
* @param {Node} Node
*/
export function appendNodeFactory (Node) {
/**
* @constructor AppendNode
* @extends Node
* @param {TreeEditor} editor
* Create a new AppendNode. This is a special node which is created at the
* end of the list with childs for an object or array
*/
function AppendNode (editor) {
/** @type {TreeEditor} */
this.editor = editor
this.dom = {}
}
AppendNode.prototype = new Node()
/**
* Return a table row with an append button.
* @return {Element} dom TR element
*/
AppendNode.prototype.getDom = function () {
// TODO: implement a new solution for the append node
const dom = this.dom
if (dom.tr) {
return dom.tr
}
this._updateEditability()
// a row for the append button
const trAppend = document.createElement('tr')
trAppend.className = 'jsoneditor-append'
trAppend.node = this
dom.tr = trAppend
// TODO: consistent naming
if (this.editor.options.mode === 'tree') {
// a cell for the dragarea column
dom.tdDrag = document.createElement('td')
// create context menu
const tdMenu = document.createElement('td')
dom.tdMenu = tdMenu
const menu = document.createElement('button')
menu.type = 'button'
menu.className = 'jsoneditor-button jsoneditor-contextmenu-button'
menu.title = 'Click to open the actions menu (Ctrl+M)'
dom.menu = menu
tdMenu.appendChild(dom.menu)
}
// a cell for the contents (showing text 'empty')
const tdAppend = document.createElement('td')
const domText = document.createElement('div')
domText.innerHTML = '(' + translate('empty') + ')'
domText.className = 'jsoneditor-readonly'
tdAppend.appendChild(domText)
dom.td = tdAppend
dom.text = domText
this.updateDom()
return trAppend
}
/**
* Append node doesn't have a path
* @returns {null}
*/
AppendNode.prototype.getPath = () => null
/**
* Append node doesn't have an index
* @returns {null}
*/
AppendNode.prototype.getIndex = () => null
/**
* Update the HTML dom of the Node
*/
AppendNode.prototype.updateDom = function (options) {
const dom = this.dom
const tdAppend = dom.td
if (tdAppend) {
tdAppend.style.paddingLeft = (this.getLevel() * 24 + 26) + 'px'
// TODO: not so nice hard coded offset
}
const domText = dom.text
if (domText) {
domText.innerHTML = '(' + translate('empty') + ' ' + this.parent.type + ')'
}
// attach or detach the contents of the append node:
// hide when the parent has childs, show when the parent has no childs
const trAppend = dom.tr
if (!this.isVisible()) {
if (dom.tr.firstChild) {
if (dom.tdDrag) {
trAppend.removeChild(dom.tdDrag)
}
if (dom.tdMenu) {
trAppend.removeChild(dom.tdMenu)
}
trAppend.removeChild(tdAppend)
}
} else {
if (!dom.tr.firstChild) {
if (dom.tdDrag) {
trAppend.appendChild(dom.tdDrag)
}
if (dom.tdMenu) {
trAppend.appendChild(dom.tdMenu)
}
trAppend.appendChild(tdAppend)
}
}
}
/**
* Check whether the AppendNode is currently visible.
* the AppendNode is visible when its parent has no childs (i.e. is empty).
* @return {boolean} isVisible
*/
AppendNode.prototype.isVisible = function () {
return (this.parent.childs.length === 0)
}
/**
* Show a contextmenu for this node
* @param {HTMLElement} anchor The element to attach the menu to.
* @param {function} [onClose] Callback method called when the context menu
* is being closed.
*/
AppendNode.prototype.showContextMenu = function (anchor, onClose) {
const node = this
const appendSubmenu = [
{
text: translate('auto'),
className: 'jsoneditor-type-auto',
title: translate('autoType'),
click: function () {
node._onAppend('', '', 'auto')
}
},
{
text: translate('array'),
className: 'jsoneditor-type-array',
title: translate('arrayType'),
click: function () {
node._onAppend('', [])
}
},
{
text: translate('object'),
className: 'jsoneditor-type-object',
title: translate('objectType'),
click: function () {
node._onAppend('', {})
}
},
{
text: translate('string'),
className: 'jsoneditor-type-string',
title: translate('stringType'),
click: function () {
node._onAppend('', '', 'string')
}
}
]
node.addTemplates(appendSubmenu, true)
let items = [
// create append button
{
text: translate('appendText'),
title: translate('appendTitleAuto'),
submenuTitle: translate('appendSubmenuTitle'),
className: 'jsoneditor-insert',
click: function () {
node._onAppend('', '', 'auto')
},
submenu: appendSubmenu
}
]
if (this.editor.options.onCreateMenu) {
const path = node.parent.getPath()
items = this.editor.options.onCreateMenu(items, {
type: 'append',
path: path,
paths: [path]
})
}
const menu = new ContextMenu(items, { close: onClose })
menu.show(anchor, this.editor.getPopupAnchor())
}
/**
* Handle an event. The event is caught centrally by the editor
* @param {Event} event
*/
AppendNode.prototype.onEvent = function (event) {
const type = event.type
const target = event.target || event.srcElement
const dom = this.dom
// highlight the append nodes parent
const menu = dom.menu
if (target === menu) {
if (type === 'mouseover') {
this.editor.highlighter.highlight(this.parent)
} else if (type === 'mouseout') {
this.editor.highlighter.unhighlight()
}
}
// context menu events
if (type === 'click' && target === dom.menu) {
const highlighter = this.editor.highlighter
highlighter.highlight(this.parent)
highlighter.lock()
addClassName(dom.menu, 'jsoneditor-selected')
this.showContextMenu(dom.menu, () => {
removeClassName(dom.menu, 'jsoneditor-selected')
highlighter.unlock()
highlighter.unhighlight()
})
}
if (type === 'keydown') {
this.onKeyDown(event)
}
}
return AppendNode
}

View File

@ -1,15 +0,0 @@
The file jsonlint.js is copied from the following project:
https://github.com/josdejong/jsonlint at 85a19d7
which is a fork of the (currently not maintained) project:
https://github.com/zaach/jsonlint
The forked project contains some fixes to allow the file to be bundled with
browserify. The file is copied in this project to prevent issues with linking
to a github project from package.json, which is for example not supported
by jspm.
As soon as zaach/jsonlint is being maintained again we can push the fix
to the original library and use it as dependency again.

View File

@ -1,418 +0,0 @@
/* Jison generated parser */
var jsonlint = (function(){
var parser = {trace: function trace() { },
yy: {},
symbols_: {"error":2,"JSONString":3,"STRING":4,"JSONNumber":5,"NUMBER":6,"JSONNullLiteral":7,"NULL":8,"JSONBooleanLiteral":9,"TRUE":10,"FALSE":11,"JSONText":12,"JSONValue":13,"EOF":14,"JSONObject":15,"JSONArray":16,"{":17,"}":18,"JSONMemberList":19,"JSONMember":20,":":21,",":22,"[":23,"]":24,"JSONElementList":25,"$accept":0,"$end":1},
terminals_: {2:"error",4:"STRING",6:"NUMBER",8:"NULL",10:"TRUE",11:"FALSE",14:"EOF",17:"{",18:"}",21:":",22:",",23:"[",24:"]"},
productions_: [0,[3,1],[5,1],[7,1],[9,1],[9,1],[12,2],[13,1],[13,1],[13,1],[13,1],[13,1],[13,1],[15,2],[15,3],[20,3],[19,1],[19,3],[16,2],[16,3],[25,1],[25,3]],
performAction: function anonymous(yytext,yyleng,yylineno,yy,yystate,$$,_$) {
var $0 = $$.length - 1;
switch (yystate) {
case 1: // replace escaped characters with actual character
this.$ = yytext.replace(/\\(\\|")/g, "$"+"1")
.replace(/\\n/g,'\n')
.replace(/\\r/g,'\r')
.replace(/\\t/g,'\t')
.replace(/\\v/g,'\v')
.replace(/\\f/g,'\f')
.replace(/\\b/g,'\b');
break;
case 2:this.$ = Number(yytext);
break;
case 3:this.$ = null;
break;
case 4:this.$ = true;
break;
case 5:this.$ = false;
break;
case 6:return this.$ = $$[$0-1];
break;
case 13:this.$ = {};
break;
case 14:this.$ = $$[$0-1];
break;
case 15:this.$ = [$$[$0-2], $$[$0]];
break;
case 16:this.$ = {}; this.$[$$[$0][0]] = $$[$0][1];
break;
case 17:this.$ = $$[$0-2]; $$[$0-2][$$[$0][0]] = $$[$0][1];
break;
case 18:this.$ = [];
break;
case 19:this.$ = $$[$0-1];
break;
case 20:this.$ = [$$[$0]];
break;
case 21:this.$ = $$[$0-2]; $$[$0-2].push($$[$0]);
break;
}
},
table: [{3:5,4:[1,12],5:6,6:[1,13],7:3,8:[1,9],9:4,10:[1,10],11:[1,11],12:1,13:2,15:7,16:8,17:[1,14],23:[1,15]},{1:[3]},{14:[1,16]},{14:[2,7],18:[2,7],22:[2,7],24:[2,7]},{14:[2,8],18:[2,8],22:[2,8],24:[2,8]},{14:[2,9],18:[2,9],22:[2,9],24:[2,9]},{14:[2,10],18:[2,10],22:[2,10],24:[2,10]},{14:[2,11],18:[2,11],22:[2,11],24:[2,11]},{14:[2,12],18:[2,12],22:[2,12],24:[2,12]},{14:[2,3],18:[2,3],22:[2,3],24:[2,3]},{14:[2,4],18:[2,4],22:[2,4],24:[2,4]},{14:[2,5],18:[2,5],22:[2,5],24:[2,5]},{14:[2,1],18:[2,1],21:[2,1],22:[2,1],24:[2,1]},{14:[2,2],18:[2,2],22:[2,2],24:[2,2]},{3:20,4:[1,12],18:[1,17],19:18,20:19},{3:5,4:[1,12],5:6,6:[1,13],7:3,8:[1,9],9:4,10:[1,10],11:[1,11],13:23,15:7,16:8,17:[1,14],23:[1,15],24:[1,21],25:22},{1:[2,6]},{14:[2,13],18:[2,13],22:[2,13],24:[2,13]},{18:[1,24],22:[1,25]},{18:[2,16],22:[2,16]},{21:[1,26]},{14:[2,18],18:[2,18],22:[2,18],24:[2,18]},{22:[1,28],24:[1,27]},{22:[2,20],24:[2,20]},{14:[2,14],18:[2,14],22:[2,14],24:[2,14]},{3:20,4:[1,12],20:29},{3:5,4:[1,12],5:6,6:[1,13],7:3,8:[1,9],9:4,10:[1,10],11:[1,11],13:30,15:7,16:8,17:[1,14],23:[1,15]},{14:[2,19],18:[2,19],22:[2,19],24:[2,19]},{3:5,4:[1,12],5:6,6:[1,13],7:3,8:[1,9],9:4,10:[1,10],11:[1,11],13:31,15:7,16:8,17:[1,14],23:[1,15]},{18:[2,17],22:[2,17]},{18:[2,15],22:[2,15]},{22:[2,21],24:[2,21]}],
defaultActions: {16:[2,6]},
parseError: function parseError(str, hash) {
throw new Error(str);
},
parse: function parse(input) {
var self = this,
stack = [0],
vstack = [null], // semantic value stack
lstack = [], // location stack
table = this.table,
yytext = '',
yylineno = 0,
yyleng = 0,
recovering = 0,
TERROR = 2,
EOF = 1;
//this.reductionCount = this.shiftCount = 0;
this.lexer.setInput(input);
this.lexer.yy = this.yy;
this.yy.lexer = this.lexer;
if (typeof this.lexer.yylloc == 'undefined')
this.lexer.yylloc = {};
var yyloc = this.lexer.yylloc;
lstack.push(yyloc);
if (typeof this.yy.parseError === 'function')
this.parseError = this.yy.parseError;
function popStack (n) {
stack.length = stack.length - 2*n;
vstack.length = vstack.length - n;
lstack.length = lstack.length - n;
}
function lex() {
var token;
token = self.lexer.lex() || 1; // $end = 1
// if token isn't its numeric value, convert
if (typeof token !== 'number') {
token = self.symbols_[token] || token;
}
return token;
}
var symbol, preErrorSymbol, state, action, a, r, yyval={},p,len,newState, expected;
while (true) {
// retreive state number from top of stack
state = stack[stack.length-1];
// use default actions if available
if (this.defaultActions[state]) {
action = this.defaultActions[state];
} else {
if (symbol == null)
symbol = lex();
// read action for current state and first input
action = table[state] && table[state][symbol];
}
// handle parse error
_handle_error:
if (typeof action === 'undefined' || !action.length || !action[0]) {
if (!recovering) {
// Report error
expected = [];
for (p in table[state]) if (this.terminals_[p] && p > 2) {
expected.push("'"+this.terminals_[p]+"'");
}
var errStr = '';
if (this.lexer.showPosition) {
errStr = 'Parse error on line '+(yylineno+1)+":\n"+this.lexer.showPosition()+"\nExpecting "+expected.join(', ') + ", got '" + this.terminals_[symbol]+ "'";
} else {
errStr = 'Parse error on line '+(yylineno+1)+": Unexpected " +
(symbol == 1 /*EOF*/ ? "end of input" :
("'"+(this.terminals_[symbol] || symbol)+"'"));
}
this.parseError(errStr,
{text: this.lexer.match, token: this.terminals_[symbol] || symbol, line: this.lexer.yylineno, loc: yyloc, expected: expected});
}
// just recovered from another error
if (recovering == 3) {
if (symbol == EOF) {
throw new Error(errStr || 'Parsing halted.');
}
// discard current lookahead and grab another
yyleng = this.lexer.yyleng;
yytext = this.lexer.yytext;
yylineno = this.lexer.yylineno;
yyloc = this.lexer.yylloc;
symbol = lex();
}
// try to recover from error
while (1) {
// check for error recovery rule in this state
if ((TERROR.toString()) in table[state]) {
break;
}
if (state == 0) {
throw new Error(errStr || 'Parsing halted.');
}
popStack(1);
state = stack[stack.length-1];
}
preErrorSymbol = symbol; // save the lookahead token
symbol = TERROR; // insert generic error symbol as new lookahead
state = stack[stack.length-1];
action = table[state] && table[state][TERROR];
recovering = 3; // allow 3 real symbols to be shifted before reporting a new error
}
// this shouldn't happen, unless resolve defaults are off
if (action[0] instanceof Array && action.length > 1) {
throw new Error('Parse Error: multiple actions possible at state: '+state+', token: '+symbol);
}
switch (action[0]) {
case 1: // shift
//this.shiftCount++;
stack.push(symbol);
vstack.push(this.lexer.yytext);
lstack.push(this.lexer.yylloc);
stack.push(action[1]); // push state
symbol = null;
if (!preErrorSymbol) { // normal execution/no error
yyleng = this.lexer.yyleng;
yytext = this.lexer.yytext;
yylineno = this.lexer.yylineno;
yyloc = this.lexer.yylloc;
if (recovering > 0)
recovering--;
} else { // error just occurred, resume old lookahead f/ before error
symbol = preErrorSymbol;
preErrorSymbol = null;
}
break;
case 2: // reduce
//this.reductionCount++;
len = this.productions_[action[1]][1];
// perform semantic action
yyval.$ = vstack[vstack.length-len]; // default to $$ = $1
// default location, uses first token for firsts, last for lasts
yyval._$ = {
first_line: lstack[lstack.length-(len||1)].first_line,
last_line: lstack[lstack.length-1].last_line,
first_column: lstack[lstack.length-(len||1)].first_column,
last_column: lstack[lstack.length-1].last_column
};
r = this.performAction.call(yyval, yytext, yyleng, yylineno, this.yy, action[1], vstack, lstack);
if (typeof r !== 'undefined') {
return r;
}
// pop off stack
if (len) {
stack = stack.slice(0,-1*len*2);
vstack = vstack.slice(0, -1*len);
lstack = lstack.slice(0, -1*len);
}
stack.push(this.productions_[action[1]][0]); // push nonterminal (reduce)
vstack.push(yyval.$);
lstack.push(yyval._$);
// goto new state = table[STATE][NONTERMINAL]
newState = table[stack[stack.length-2]][stack[stack.length-1]];
stack.push(newState);
break;
case 3: // accept
return true;
}
}
return true;
}};
/* Jison generated lexer */
var lexer = (function(){
var lexer = ({EOF:1,
parseError:function parseError(str, hash) {
if (this.yy.parseError) {
this.yy.parseError(str, hash);
} else {
throw new Error(str);
}
},
setInput:function (input) {
this._input = input;
this._more = this._less = this.done = false;
this.yylineno = this.yyleng = 0;
this.yytext = this.matched = this.match = '';
this.conditionStack = ['INITIAL'];
this.yylloc = {first_line:1,first_column:0,last_line:1,last_column:0};
return this;
},
input:function () {
var ch = this._input[0];
this.yytext+=ch;
this.yyleng++;
this.match+=ch;
this.matched+=ch;
var lines = ch.match(/\n/);
if (lines) this.yylineno++;
this._input = this._input.slice(1);
return ch;
},
unput:function (ch) {
this._input = ch + this._input;
return this;
},
more:function () {
this._more = true;
return this;
},
less:function (n) {
this._input = this.match.slice(n) + this._input;
},
pastInput:function () {
var past = this.matched.substr(0, this.matched.length - this.match.length);
return (past.length > 20 ? '...':'') + past.substr(-20).replace(/\n/g, "");
},
upcomingInput:function () {
var next = this.match;
if (next.length < 20) {
next += this._input.substr(0, 20-next.length);
}
return (next.substr(0,20)+(next.length > 20 ? '...':'')).replace(/\n/g, "");
},
showPosition:function () {
var pre = this.pastInput();
var c = new Array(pre.length + 1).join("-");
return pre + this.upcomingInput() + "\n" + c+"^";
},
next:function () {
if (this.done) {
return this.EOF;
}
if (!this._input) this.done = true;
var token,
match,
tempMatch,
index,
col,
lines;
if (!this._more) {
this.yytext = '';
this.match = '';
}
var rules = this._currentRules();
for (var i=0;i < rules.length; i++) {
tempMatch = this._input.match(this.rules[rules[i]]);
if (tempMatch && (!match || tempMatch[0].length > match[0].length)) {
match = tempMatch;
index = i;
if (!this.options.flex) break;
}
}
if (match) {
lines = match[0].match(/\n.*/g);
if (lines) this.yylineno += lines.length;
this.yylloc = {first_line: this.yylloc.last_line,
last_line: this.yylineno+1,
first_column: this.yylloc.last_column,
last_column: lines ? lines[lines.length-1].length-1 : this.yylloc.last_column + match[0].length}
this.yytext += match[0];
this.match += match[0];
this.yyleng = this.yytext.length;
this._more = false;
this._input = this._input.slice(match[0].length);
this.matched += match[0];
token = this.performAction.call(this, this.yy, this, rules[index],this.conditionStack[this.conditionStack.length-1]);
if (this.done && this._input) this.done = false;
if (token) return token;
else return;
}
if (this._input === "") {
return this.EOF;
} else {
this.parseError('Lexical error on line '+(this.yylineno+1)+'. Unrecognized text.\n'+this.showPosition(),
{text: "", token: null, line: this.yylineno});
}
},
lex:function lex() {
var r = this.next();
if (typeof r !== 'undefined') {
return r;
} else {
return this.lex();
}
},
begin:function begin(condition) {
this.conditionStack.push(condition);
},
popState:function popState() {
return this.conditionStack.pop();
},
_currentRules:function _currentRules() {
return this.conditions[this.conditionStack[this.conditionStack.length-1]].rules;
},
topState:function () {
return this.conditionStack[this.conditionStack.length-2];
},
pushState:function begin(condition) {
this.begin(condition);
}});
lexer.options = {};
lexer.performAction = function anonymous(yy,yy_,$avoiding_name_collisions,YY_START) {
var YYSTATE=YY_START
switch($avoiding_name_collisions) {
case 0:/* skip whitespace */
break;
case 1:return 6
break;
case 2:yy_.yytext = yy_.yytext.substr(1,yy_.yyleng-2); return 4
break;
case 3:return 17
break;
case 4:return 18
break;
case 5:return 23
break;
case 6:return 24
break;
case 7:return 22
break;
case 8:return 21
break;
case 9:return 10
break;
case 10:return 11
break;
case 11:return 8
break;
case 12:return 14
break;
case 13:return 'INVALID'
break;
}
};
lexer.rules = [/^(?:\s+)/,/^(?:(-?([0-9]|[1-9][0-9]+))(\.[0-9]+)?([eE][-+]?[0-9]+)?\b)/,/^(?:"(?:\\[\\"bfnrt/]|\\u[a-fA-F0-9]{4}|[^\\\0-\x09\x0a-\x1f"])*")/,/^(?:\{)/,/^(?:\})/,/^(?:\[)/,/^(?:\])/,/^(?:,)/,/^(?::)/,/^(?:true\b)/,/^(?:false\b)/,/^(?:null\b)/,/^(?:$)/,/^(?:.)/];
lexer.conditions = {"INITIAL":{"rules":[0,1,2,3,4,5,6,7,8,9,10,11,12,13],"inclusive":true}};
;
return lexer;})()
parser.lexer = lexer;
return parser;
})();
if (typeof require !== 'undefined' && typeof exports !== 'undefined') {
exports.parser = jsonlint;
exports.parse = jsonlint.parse.bind(jsonlint);
}

View File

@ -1,6 +0,0 @@
This is a copy of the Selectr project
https://github.com/Mobius1/Selectr
Reason is that the project is not maintained and has some issues
loading it via `require` in a webpack project.

File diff suppressed because it is too large Load Diff

View File

@ -1,472 +0,0 @@
/*!
* Selectr 2.4.0
* https://github.com/Mobius1/Selectr
*
* Released under the MIT license
*/
.selectr-container {
position: relative;
}
.selectr-container li {
list-style: none;
}
.selectr-hidden {
position: absolute;
overflow: hidden;
clip: rect(0px, 0px, 0px, 0px);
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
border: 0 none;
}
.selectr-visible {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
opacity: 0;
z-index: 11;
}
.selectr-desktop.multiple .selectr-visible {
display: none;
}
.selectr-desktop.multiple.native-open .selectr-visible {
top: 100%;
min-height: 200px !important;
height: auto;
opacity: 1;
display: block;
}
.selectr-container.multiple.selectr-mobile .selectr-selected {
z-index: 0;
}
.selectr-selected {
position: relative;
z-index: 1;
box-sizing: border-box;
width: 100%;
padding: 7px 28px 7px 14px;
cursor: pointer;
border: 1px solid $jse-grey;
border-radius: 3px;
background-color: $jse-white;
}
.selectr-selected::before {
position: absolute;
top: 50%;
right: 10px;
width: 0;
height: 0;
content: '';
-o-transform: rotate(0deg) translate3d(0px, -50%, 0px);
-ms-transform: rotate(0deg) translate3d(0px, -50%, 0px);
-moz-transform: rotate(0deg) translate3d(0px, -50%, 0px);
-webkit-transform: rotate(0deg) translate3d(0px, -50%, 0px);
transform: rotate(0deg) translate3d(0px, -50%, 0px);
border-width: 4px 4px 0 4px;
border-style: solid;
border-color: #6c7a86 transparent transparent;
}
.selectr-container.open .selectr-selected::before,
.selectr-container.native-open .selectr-selected::before {
border-width: 0 4px 4px 4px;
border-style: solid;
border-color: transparent transparent #6c7a86;
}
.selectr-label {
display: none;
overflow: hidden;
width: 100%;
white-space: nowrap;
text-overflow: ellipsis;
}
.selectr-placeholder {
color: #6c7a86;
}
.selectr-tags {
margin: 0;
padding: 0;
white-space: normal;
}
.has-selected .selectr-tags {
margin: 0 0 -2px;
}
.selectr-tag {
list-style: none;
position: relative;
float: left;
padding: 2px 25px 2px 8px;
margin: 0 2px 2px 0;
cursor: default;
color: $jse-white;
border: medium none;
border-radius: 10px;
background: #acb7bf none repeat scroll 0 0;
}
.selectr-container.multiple.has-selected .selectr-selected {
padding: 5px 28px 5px 5px;
}
.selectr-options-container {
position: absolute;
z-index: 10000;
top: calc(100% - 1px);
left: 0;
display: none;
box-sizing: border-box;
width: 100%;
border-width: 0 1px 1px;
border-style: solid;
border-color: transparent $jse-grey $jse-grey;
border-radius: 0 0 3px 3px;
background-color: $jse-white;
}
.selectr-container.open .selectr-options-container {
display: block;
}
.selectr-input-container {
position: relative;
display: none;
}
.selectr-clear,
.selectr-input-clear,
.selectr-tag-remove {
position: absolute;
top: 50%;
right: 22px;
width: 20px;
height: 20px;
padding: 0;
cursor: pointer;
-o-transform: translate3d(0px, -50%, 0px);
-ms-transform: translate3d(0px, -50%, 0px);
-moz-transform: translate3d(0px, -50%, 0px);
-webkit-transform: translate3d(0px, -50%, 0px);
transform: translate3d(0px, -50%, 0px);
border: medium none;
background-color: transparent;
z-index: 11;
}
.selectr-clear,
.selectr-input-clear {
display: none;
}
.selectr-container.has-selected .selectr-clear,
.selectr-input-container.active .selectr-input-clear {
display: block;
}
.selectr-selected .selectr-tag-remove {
right: 2px;
}
.selectr-clear::before,
.selectr-clear::after,
.selectr-input-clear::before,
.selectr-input-clear::after,
.selectr-tag-remove::before,
.selectr-tag-remove::after {
position: absolute;
top: 5px;
left: 9px;
width: 2px;
height: 10px;
content: ' ';
background-color: #6c7a86;
}
.selectr-tag-remove::before,
.selectr-tag-remove::after {
top: 4px;
width: 3px;
height: 12px;
background-color: $jse-white;
}
.selectr-clear:before,
.selectr-input-clear::before,
.selectr-tag-remove::before {
-o-transform: rotate(45deg);
-ms-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-webkit-transform: rotate(45deg);
transform: rotate(45deg);
}
.selectr-clear:after,
.selectr-input-clear::after,
.selectr-tag-remove::after {
-o-transform: rotate(-45deg);
-ms-transform: rotate(-45deg);
-moz-transform: rotate(-45deg);
-webkit-transform: rotate(-45deg);
transform: rotate(-45deg);
}
.selectr-input-container.active,
.selectr-input-container.active .selectr-clear {
display: block;
}
.selectr-input {
top: 5px;
left: 5px;
box-sizing: border-box;
width: calc(100% - 30px);
margin: 10px 15px;
padding: 7px 30px 7px 9px;
border: 1px solid $jse-grey;
border-radius: 3px;
}
.selectr-notice {
display: none;
box-sizing: border-box;
width: 100%;
padding: 8px 16px;
border-top: 1px solid $jse-grey;
border-radius: 0 0 3px 3px;
background-color: $jse-white;
}
.selectr-container.notice .selectr-notice {
display: block;
}
.selectr-container.notice .selectr-selected {
border-radius: 3px 3px 0 0;
}
.selectr-options {
position: relative;
top: calc(100% + 2px);
display: none;
overflow-x: auto;
overflow-y: scroll;
max-height: 200px;
margin: 0;
padding: 0;
}
.selectr-container.open .selectr-options,
.selectr-container.open .selectr-input-container,
.selectr-container.notice .selectr-options-container {
display: block;
}
.selectr-option {
position: relative;
display: block;
padding: 5px 20px;
list-style: outside none none;
cursor: pointer;
font-weight: normal;
}
.selectr-options.optgroups > .selectr-option {
padding-left: 25px;
}
.selectr-optgroup {
font-weight: bold;
padding: 0;
}
.selectr-optgroup--label {
font-weight: bold;
margin-top: 10px;
padding: 5px 15px;
}
.selectr-match {
text-decoration: underline;
}
.selectr-option.selected {
background-color: #ddd;
}
.selectr-option.active {
color: $jse-white;
background-color: #5897fb;
}
.selectr-option.disabled {
opacity: 0.4;
}
.selectr-option.excluded {
display: none;
}
.selectr-container.open .selectr-selected {
border-color: $jse-grey $jse-grey transparent $jse-grey;
border-radius: 3px 3px 0 0;
}
.selectr-container.open .selectr-selected::after {
-o-transform: rotate(180deg) translate3d(0px, 50%, 0px);
-ms-transform: rotate(180deg) translate3d(0px, 50%, 0px);
-moz-transform: rotate(180deg) translate3d(0px, 50%, 0px);
-webkit-transform: rotate(180deg) translate3d(0px, 50%, 0px);
transform: rotate(180deg) translate3d(0px, 50%, 0px);
}
.selectr-disabled {
opacity: .6;
}
.selectr-empty,
.has-selected .selectr-placeholder {
display: none;
}
.has-selected .selectr-label {
display: block;
}
/* TAGGABLE */
.taggable .selectr-selected {
padding: 4px 28px 4px 4px;
}
.taggable .selectr-selected::after {
display: table;
content: " ";
clear: both;
}
.taggable .selectr-label {
width: auto;
}
.taggable .selectr-tags {
float: left;
display: block;
}
.taggable .selectr-placeholder {
display: none;
}
.input-tag {
float: left;
min-width: 90px;
width: auto;
}
.selectr-tag-input {
border: medium none;
padding: 3px 10px;
width: 100%;
font-family: inherit;
font-weight: inherit;
font-size: inherit;
}
.selectr-input-container.loading::after {
position: absolute;
top: 50%;
right: 20px;
width: 20px;
height: 20px;
content: '';
-o-transform: translate3d(0px, -50%, 0px);
-ms-transform: translate3d(0px, -50%, 0px);
-moz-transform: translate3d(0px, -50%, 0px);
-webkit-transform: translate3d(0px, -50%, 0px);
transform: translate3d(0px, -50%, 0px);
-o-transform-origin: 50% 0 0;
-ms-transform-origin: 50% 0 0;
-moz-transform-origin: 50% 0 0;
-webkit-transform-origin: 50% 0 0;
transform-origin: 50% 0 0;
-moz-animation: 500ms linear 0s normal forwards infinite running spin;
-webkit-animation: 500ms linear 0s normal forwards infinite running spin;
animation: 500ms linear 0s normal forwards infinite running spin;
border-width: 3px;
border-style: solid;
border-color: #aaa #ddd #ddd;
border-radius: 50%;
}
@-webkit-keyframes spin {
0% {
-webkit-transform: rotate(0deg) translate3d(0px, -50%, 0px);
transform: rotate(0deg) translate3d(0px, -50%, 0px);
}
100% {
-webkit-transform: rotate(360deg) translate3d(0px, -50%, 0px);
transform: rotate(360deg) translate3d(0px, -50%, 0px);
}
}
@keyframes spin {
0% {
-webkit-transform: rotate(0deg) translate3d(0px, -50%, 0px);
transform: rotate(0deg) translate3d(0px, -50%, 0px);
}
100% {
-webkit-transform: rotate(360deg) translate3d(0px, -50%, 0px);
transform: rotate(360deg) translate3d(0px, -50%, 0px);
}
}
.selectr-container.open.inverted .selectr-selected {
border-color: transparent $jse-grey $jse-grey;
border-radius: 0 0 3px 3px;
}
.selectr-container.inverted .selectr-options-container {
border-width: 1px 1px 0;
border-color: $jse-grey $jse-grey transparent;
border-radius: 3px 3px 0 0;
background-color: $jse-white;
}
.selectr-container.inverted .selectr-options-container {
top: auto;
bottom: calc(100% - 1px);
}
.selectr-container ::-webkit-input-placeholder {
color: #6c7a86;
opacity: 1;
}
.selectr-container ::-moz-placeholder {
color: #6c7a86;
opacity: 1;
}
.selectr-container :-ms-input-placeholder {
color: #6c7a86;
opacity: 1;
}
.selectr-container ::placeholder {
color: #6c7a86;
opacity: 1;
}

View File

@ -1,381 +0,0 @@
'use strict'
const defaultFilterFunction = {
start: function (token, match, config) {
return match.indexOf(token) === 0
},
contain: function (token, match, config) {
return match.indexOf(token) > -1
}
}
export function autocomplete (config) {
config = config || {}
config.filter = config.filter || 'start'
config.trigger = config.trigger || 'keydown'
config.confirmKeys = config.confirmKeys || [39, 35, 9] // right, end, tab
config.caseSensitive = config.caseSensitive || false // autocomplete case sensitive
let fontSize = ''
let fontFamily = ''
const wrapper = document.createElement('div')
wrapper.style.position = 'relative'
wrapper.style.outline = '0'
wrapper.style.border = '0'
wrapper.style.margin = '0'
wrapper.style.padding = '0'
const dropDown = document.createElement('div')
dropDown.className = 'autocomplete dropdown'
dropDown.style.position = 'absolute'
dropDown.style.visibility = 'hidden'
let spacer
let leftSide // <-- it will contain the leftSide part of the textfield (the bit that was already autocompleted)
const createDropDownController = (elem, rs) => {
let rows = []
let ix = 0
let oldIndex = -1
// TODO: move this styling in JS to SCSS
const onMouseOver = function () { this.style.backgroundColor = '#ddd' }
const onMouseOut = function () { this.style.backgroundColor = '' }
const onMouseDown = function () { p.hide(); p.onmouseselection(this.__hint, p.rs) }
var p = {
rs: rs,
hide: function () {
elem.style.visibility = 'hidden'
// rs.hideDropDown();
},
refresh: function (token, array) {
elem.style.visibility = 'hidden'
ix = 0
elem.innerHTML = ''
const vph = (window.innerHeight || document.documentElement.clientHeight)
const rect = elem.parentNode.getBoundingClientRect()
const distanceToTop = rect.top - 6 // heuristic give 6px
const distanceToBottom = vph - rect.bottom - 6 // distance from the browser border.
rows = []
const filterFn = typeof config.filter === 'function' ? config.filter : defaultFilterFunction[config.filter]
const filtered = !filterFn ? [] : array.filter(match => filterFn(config.caseSensitive ? token : token.toLowerCase(), config.caseSensitive ? match : match.toLowerCase(), config))
rows = filtered.map(row => {
const divRow = document.createElement('div')
divRow.className = 'item'
// divRow.style.color = config.color;
divRow.onmouseover = onMouseOver
divRow.onmouseout = onMouseOut
divRow.onmousedown = onMouseDown
divRow.__hint = row
divRow.innerHTML = row.substring(0, token.length) + '<b>' + row.substring(token.length) + '</b>'
elem.appendChild(divRow)
return divRow
})
if (rows.length === 0) {
return // nothing to show.
}
if (rows.length === 1 && ((token.toLowerCase() === rows[0].__hint.toLowerCase() && !config.caseSensitive) ||
(token === rows[0].__hint && config.caseSensitive))) {
return // do not show the dropDown if it has only one element which matches what we have just displayed.
}
if (rows.length < 2) return
p.highlight(0)
if (distanceToTop > distanceToBottom * 3) { // Heuristic (only when the distance to the to top is 4 times more than distance to the bottom
elem.style.maxHeight = distanceToTop + 'px' // we display the dropDown on the top of the input text
elem.style.top = ''
elem.style.bottom = '100%'
} else {
elem.style.top = '100%'
elem.style.bottom = ''
elem.style.maxHeight = distanceToBottom + 'px'
}
elem.style.visibility = 'visible'
},
highlight: function (index) {
if (oldIndex !== -1 && rows[oldIndex]) {
rows[oldIndex].className = 'item'
}
rows[index].className = 'item hover'
oldIndex = index
},
move: function (step) { // moves the selection either up or down (unless it's not possible) step is either +1 or -1.
if (elem.style.visibility === 'hidden') return '' // nothing to move if there is no dropDown. (this happens if the user hits escape and then down or up)
if (ix + step === -1 || ix + step === rows.length) return rows[ix].__hint // NO CIRCULAR SCROLLING.
ix += step
p.highlight(ix)
return rows[ix].__hint// txtShadow.value = uRows[uIndex].__hint ;
},
onmouseselection: function () { } // it will be overwritten.
}
return p
}
function setEndOfContenteditable (contentEditableElement) {
let range, selection
if (document.createRange) {
// Firefox, Chrome, Opera, Safari, IE 9+
range = document.createRange()// Create a range (a range is a like the selection but invisible)
range.selectNodeContents(contentEditableElement)// Select the entire contents of the element with the range
range.collapse(false)// collapse the range to the end point. false means collapse to end rather than the start
selection = window.getSelection()// get the selection object (allows you to change selection)
selection.removeAllRanges()// remove any selections already made
selection.addRange(range)// make the range you have just created the visible selection
} else if (document.selection) {
// IE 8 and lower
range = document.body.createTextRange()// Create a range (a range is a like the selection but invisible)
range.moveToElementText(contentEditableElement)// Select the entire contents of the element with the range
range.collapse(false)// collapse the range to the end point. false means collapse to end rather than the start
range.select()// Select the range (make it the visible selection
}
}
function calculateWidthForText (text) {
if (spacer === undefined) { // on first call only.
spacer = document.createElement('span')
spacer.style.visibility = 'hidden'
spacer.style.position = 'fixed'
spacer.style.outline = '0'
spacer.style.margin = '0'
spacer.style.padding = '0'
spacer.style.border = '0'
spacer.style.left = '0'
spacer.style.whiteSpace = 'pre'
spacer.style.fontSize = fontSize
spacer.style.fontFamily = fontFamily
spacer.style.fontWeight = 'normal'
document.body.appendChild(spacer)
}
// Used to encode an HTML string into a plain text.
// taken from http://stackoverflow.com/questions/1219860/javascript-jquery-html-encoding
spacer.innerHTML = String(text).replace(/&/g, '&amp;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
return spacer.getBoundingClientRect().right
}
const rs = {
onArrowDown: function () { }, // defaults to no action.
onArrowUp: function () { }, // defaults to no action.
onEnter: function () { }, // defaults to no action.
onTab: function () { }, // defaults to no action.
startFrom: 0,
options: [],
element: null,
elementHint: null,
elementStyle: null,
wrapper: wrapper, // Only to allow easy access to the HTML elements to the final user (possibly for minor customizations)
show: function (element, startPos, options) {
this.startFrom = startPos
this.wrapper.remove()
if (this.elementHint) {
this.elementHint.remove()
this.elementHint = null
}
if (fontSize === '') {
fontSize = window.getComputedStyle(element).getPropertyValue('font-size')
}
if (fontFamily === '') {
fontFamily = window.getComputedStyle(element).getPropertyValue('font-family')
}
dropDown.style.marginLeft = '0'
dropDown.style.marginTop = element.getBoundingClientRect().height + 'px'
this.options = options.map(String)
if (this.element !== element) {
this.element = element
this.elementStyle = {
zIndex: this.element.style.zIndex,
position: this.element.style.position,
backgroundColor: this.element.style.backgroundColor,
borderColor: this.element.style.borderColor
}
}
this.element.style.zIndex = 3
this.element.style.position = 'relative'
this.element.style.backgroundColor = 'transparent'
this.element.style.borderColor = 'transparent'
this.elementHint = element.cloneNode()
this.elementHint.className = 'autocomplete hint'
this.elementHint.style.zIndex = 2
this.elementHint.style.position = 'absolute'
this.elementHint.onfocus = () => { this.element.focus() }
if (this.element.addEventListener) {
this.element.removeEventListener('keydown', keyDownHandler)
this.element.addEventListener('keydown', keyDownHandler, false)
this.element.removeEventListener('blur', onBlurHandler)
this.element.addEventListener('blur', onBlurHandler, false)
}
wrapper.appendChild(this.elementHint)
wrapper.appendChild(dropDown)
element.parentElement.appendChild(wrapper)
this.repaint(element)
},
setText: function (text) {
this.element.innerText = text
},
getText: function () {
return this.element.innerText
},
hideDropDown: function () {
this.wrapper.remove()
if (this.elementHint) {
this.elementHint.remove()
this.elementHint = null
dropDownController.hide()
this.element.style.zIndex = this.elementStyle.zIndex
this.element.style.position = this.elementStyle.position
this.element.style.backgroundColor = this.elementStyle.backgroundColor
this.element.style.borderColor = this.elementStyle.borderColor
}
},
repaint: function (element) {
let text = element.innerText
text = text.replace('\n', '')
const optionsLength = this.options.length
// breaking text in leftSide and token.
const token = text.substring(this.startFrom)
leftSide = text.substring(0, this.startFrom)
for (let i = 0; i < optionsLength; i++) {
const opt = this.options[i]
if ((!config.caseSensitive && opt.toLowerCase().indexOf(token.toLowerCase()) === 0) ||
(config.caseSensitive && opt.indexOf(token) === 0)) { // <-- how about upperCase vs. lowercase
this.elementHint.innerText = leftSide + token + opt.substring(token.length)
this.elementHint.realInnerText = leftSide + opt
break
}
}
// moving the dropDown and refreshing it.
dropDown.style.left = calculateWidthForText(leftSide) + 'px'
dropDownController.refresh(token, this.options)
this.elementHint.style.width = calculateWidthForText(this.elementHint.innerText) + 10 + 'px'
const wasDropDownHidden = (dropDown.style.visibility === 'hidden')
if (!wasDropDownHidden) { this.elementHint.style.width = calculateWidthForText(this.elementHint.innerText) + dropDown.clientWidth + 'px' }
}
}
var dropDownController = createDropDownController(dropDown, rs)
var keyDownHandler = function (e) {
// console.log("Keydown:" + e.keyCode);
e = e || window.event
const keyCode = e.keyCode
if (this.elementHint == null) return
if (keyCode === 33) { return } // page up (do nothing)
if (keyCode === 34) { return } // page down (do nothing);
if (keyCode === 27) { // escape
rs.hideDropDown()
rs.element.focus()
e.preventDefault()
e.stopPropagation()
return
}
let text = this.element.innerText
text = text.replace('\n', '')
if (config.confirmKeys.indexOf(keyCode) >= 0) { // (autocomplete triggered)
if (keyCode === 9) {
if (this.elementHint.innerText.length === 0) {
rs.onTab()
}
}
if (this.elementHint.innerText.length > 0) { // if there is a hint
if (this.element.innerText !== this.elementHint.realInnerText) {
this.element.innerText = this.elementHint.realInnerText
rs.hideDropDown()
setEndOfContenteditable(this.element)
if (keyCode === 9) {
rs.element.focus()
e.preventDefault()
e.stopPropagation()
}
}
}
return
}
if (keyCode === 13) { // enter (autocomplete triggered)
if (this.elementHint.innerText.length === 0) { // if there is a hint
rs.onEnter()
} else {
const wasDropDownHidden = (dropDown.style.visibility === 'hidden')
dropDownController.hide()
if (wasDropDownHidden) {
rs.hideDropDown()
rs.element.focus()
rs.onEnter()
return
}
this.element.innerText = this.elementHint.realInnerText
rs.hideDropDown()
setEndOfContenteditable(this.element)
e.preventDefault()
e.stopPropagation()
}
return
}
if (keyCode === 40) { // down
const token = text.substring(this.startFrom)
const m = dropDownController.move(+1)
if (m === '') { rs.onArrowDown() }
this.elementHint.innerText = leftSide + token + m.substring(token.length)
this.elementHint.realInnerText = leftSide + m
e.preventDefault()
e.stopPropagation()
return
}
if (keyCode === 38) { // up
const token = text.substring(this.startFrom)
const m = dropDownController.move(-1)
if (m === '') { rs.onArrowUp() }
this.elementHint.innerText = leftSide + token + m.substring(token.length)
this.elementHint.realInnerText = leftSide + m
e.preventDefault()
e.stopPropagation()
}
}.bind(rs)
var onBlurHandler = e => {
rs.hideDropDown()
// console.log("Lost focus.");
}
dropDownController.onmouseselection = (text, rs) => {
rs.element.innerText = rs.elementHint.innerText = leftSide + text
rs.hideDropDown()
window.setTimeout(() => {
rs.element.focus()
setEndOfContenteditable(rs.element)
}, 1)
}
return rs
}

View File

@ -1,5 +0,0 @@
export const DEFAULT_MODAL_ANCHOR = document.body
export const SIZE_LARGE = 10 * 1024 * 1024 // 10 MB
export const MAX_PREVIEW_CHARACTERS = 20000
export const PREVIEW_HISTORY_LIMIT = 2 * 1024 * 1024 * 1024 // 2 GB

View File

@ -1,99 +0,0 @@
import { isChildOf, removeEventListener, addEventListener } from './util'
/**
* Create an anchor element absolutely positioned in the `parent`
* element.
* @param {HTMLElement} anchor
* @param {HTMLElement} parent
* @param {function(HTMLElement)} [onDestroy] Callback when the anchor is destroyed
* @param {boolean} [destroyOnMouseOut=false] If true, anchor will be removed on mouse out
* @returns {HTMLElement}
*/
export function createAbsoluteAnchor (anchor, parent, onDestroy, destroyOnMouseOut = false) {
const root = getRootNode(anchor)
const eventListeners = {}
const anchorRect = anchor.getBoundingClientRect()
const parentRect = parent.getBoundingClientRect()
const absoluteAnchor = document.createElement('div')
absoluteAnchor.className = 'jsoneditor-anchor'
absoluteAnchor.style.position = 'absolute'
absoluteAnchor.style.left = (anchorRect.left - parentRect.left) + 'px'
absoluteAnchor.style.top = (anchorRect.top - parentRect.top) + 'px'
absoluteAnchor.style.width = (anchorRect.width - 2) + 'px'
absoluteAnchor.style.height = (anchorRect.height - 2) + 'px'
absoluteAnchor.style.boxSizing = 'border-box'
parent.appendChild(absoluteAnchor)
function destroy () {
// remove temporary absolutely positioned anchor
if (absoluteAnchor && absoluteAnchor.parentNode) {
absoluteAnchor.parentNode.removeChild(absoluteAnchor)
// remove all event listeners
// all event listeners are supposed to be attached to document.
for (const name in eventListeners) {
if (hasOwnProperty(eventListeners, name)) {
const fn = eventListeners[name]
if (fn) {
removeEventListener(root, name, fn)
}
delete eventListeners[name]
}
}
if (typeof onDestroy === 'function') {
onDestroy(anchor)
}
}
}
function isOutside (target) {
return (target !== absoluteAnchor) && !isChildOf(target, absoluteAnchor)
}
// create and attach event listeners
function destroyIfOutside (event) {
if (isOutside(event.target)) {
destroy()
}
}
eventListeners.mousedown = addEventListener(root, 'mousedown', destroyIfOutside)
eventListeners.mousewheel = addEventListener(root, 'mousewheel', destroyIfOutside)
if (destroyOnMouseOut) {
let destroyTimer = null
absoluteAnchor.onmouseover = () => {
clearTimeout(destroyTimer)
destroyTimer = null
}
absoluteAnchor.onmouseout = () => {
if (!destroyTimer) {
destroyTimer = setTimeout(destroy, 200)
}
}
}
absoluteAnchor.destroy = destroy
return absoluteAnchor
}
/**
* Node.getRootNode shim
* @param {HTMLElement} node node to check
* @return {HTMLElement} node's rootNode or `window` if there is ShadowDOM is not supported.
*/
function getRootNode (node) {
return (typeof node.getRootNode === 'function')
? node.getRootNode()
: window
}
function hasOwnProperty (object, key) {
return Object.prototype.hasOwnProperty.call(object, key)
}

View File

@ -1 +0,0 @@
*

View File

@ -1,29 +0,0 @@
/*!
* jsoneditor.js
*
* @brief
* JSONEditor is a web-based tool to view, edit, format, and validate JSON.
* It has various modes such as a tree editor, a code editor, and a plain text
* editor.
*
* Supported browsers: Chrome, Firefox, Safari, Opera, Internet Explorer 8+
*
* @license
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy
* of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*
* Copyright (c) 2011-2020 Jos de Jong, http://jsoneditoronline.org
*
* @author Jos de Jong, <wjosdejong@gmail.com>
* @version @@version
* @date @@date
*/

View File

@ -1,598 +0,0 @@
'use strict'
/* eslint-disable no-template-curly-in-string */
import './polyfills'
const _locales = ['en', 'pt-BR', 'zh-CN', 'tr', 'ja', 'fr-FR']
const _defs = {
en: {
array: 'Array',
auto: 'Auto',
appendText: 'Append',
appendTitle: 'Append a new field with type \'auto\' after this field (Ctrl+Shift+Ins)',
appendSubmenuTitle: 'Select the type of the field to be appended',
appendTitleAuto: 'Append a new field with type \'auto\' (Ctrl+Shift+Ins)',
ascending: 'Ascending',
ascendingTitle: 'Sort the childs of this ${type} in ascending order',
actionsMenu: 'Click to open the actions menu (Ctrl+M)',
cannotParseFieldError: 'Cannot parse field into JSON',
cannotParseValueError: 'Cannot parse value into JSON',
collapseAll: 'Collapse all fields',
compactTitle: 'Compact JSON data, remove all whitespaces (Ctrl+Shift+\\)',
descending: 'Descending',
descendingTitle: 'Sort the childs of this ${type} in descending order',
drag: 'Drag to move this field (Alt+Shift+Arrows)',
duplicateKey: 'duplicate key',
duplicateText: 'Duplicate',
duplicateTitle: 'Duplicate selected fields (Ctrl+D)',
duplicateField: 'Duplicate this field (Ctrl+D)',
duplicateFieldError: 'Duplicate field name',
empty: 'empty',
expandAll: 'Expand all fields',
expandTitle: 'Click to expand/collapse this field (Ctrl+E). \n' +
'Ctrl+Click to expand/collapse including all childs.',
formatTitle: 'Format JSON data, with proper indentation and line feeds (Ctrl+\\)',
insert: 'Insert',
insertTitle: 'Insert a new field with type \'auto\' before this field (Ctrl+Ins)',
insertSub: 'Select the type of the field to be inserted',
object: 'Object',
ok: 'Ok',
redo: 'Redo (Ctrl+Shift+Z)',
removeText: 'Remove',
removeTitle: 'Remove selected fields (Ctrl+Del)',
removeField: 'Remove this field (Ctrl+Del)',
repairTitle: 'Repair JSON: fix quotes and escape characters, remove comments and JSONP notation, turn JavaScript objects into JSON.',
searchTitle: 'Search fields and values',
searchNextResultTitle: 'Next result (Enter)',
searchPreviousResultTitle: 'Previous result (Shift + Enter)',
selectNode: 'Select a node...',
showAll: 'show all',
showMore: 'show more',
showMoreStatus: 'displaying ${visibleChilds} of ${totalChilds} items.',
sort: 'Sort',
sortTitle: 'Sort the childs of this ${type}',
sortTitleShort: 'Sort contents',
sortFieldLabel: 'Field:',
sortDirectionLabel: 'Direction:',
sortFieldTitle: 'Select the nested field by which to sort the array or object',
sortAscending: 'Ascending',
sortAscendingTitle: 'Sort the selected field in ascending order',
sortDescending: 'Descending',
sortDescendingTitle: 'Sort the selected field in descending order',
string: 'String',
transform: 'Transform',
transformTitle: 'Filter, sort, or transform the childs of this ${type}',
transformTitleShort: 'Filter, sort, or transform contents',
extract: 'Extract',
extractTitle: 'Extract this ${type}',
transformQueryTitle: 'Enter a JMESPath query',
transformWizardLabel: 'Wizard',
transformWizardFilter: 'Filter',
transformWizardSortBy: 'Sort by',
transformWizardSelectFields: 'Select fields',
transformQueryLabel: 'Query',
transformPreviewLabel: 'Preview',
type: 'Type',
typeTitle: 'Change the type of this field',
openUrl: 'Ctrl+Click or Ctrl+Enter to open url in new window',
undo: 'Undo last action (Ctrl+Z)',
validationCannotMove: 'Cannot move a field into a child of itself',
autoType: 'Field type "auto". ' +
'The field type is automatically determined from the value ' +
'and can be a string, number, boolean, or null.',
objectType: 'Field type "object". ' +
'An object contains an unordered set of key/value pairs.',
arrayType: 'Field type "array". ' +
'An array contains an ordered collection of values.',
stringType: 'Field type "string". ' +
'Field type is not determined from the value, ' +
'but always returned as string.',
modeEditorTitle: 'Switch Editor Mode',
modeCodeText: 'Code',
modeCodeTitle: 'Switch to code highlighter',
modeFormText: 'Form',
modeFormTitle: 'Switch to form editor',
modeTextText: 'Text',
modeTextTitle: 'Switch to plain text editor',
modeTreeText: 'Tree',
modeTreeTitle: 'Switch to tree editor',
modeViewText: 'View',
modeViewTitle: 'Switch to tree view',
modePreviewText: 'Preview',
modePreviewTitle: 'Switch to preview mode',
examples: 'Examples',
default: 'Default'
},
'zh-CN': {
array: '数组',
auto: '自动',
appendText: '追加',
appendTitle: '在此字段后追加一个类型为“auto”的新字段 (Ctrl+Shift+Ins)',
appendSubmenuTitle: '选择要追加的字段类型',
appendTitleAuto: '追加类型为“auto”的新字段 (Ctrl+Shift+Ins)',
ascending: '升序',
ascendingTitle: '升序排列${type}的子节点',
actionsMenu: '点击打开动作菜单(Ctrl+M)',
cannotParseFieldError: '无法将字段解析为JSON',
cannotParseValueError: '无法将值解析为JSON',
collapseAll: '缩进所有字段',
compactTitle: '压缩JSON数据删除所有空格 (Ctrl+Shift+\\)',
descending: '降序',
descendingTitle: '降序排列${type}的子节点',
drag: '拖拽移动该节点(Alt+Shift+Arrows)',
duplicateKey: '重复键',
duplicateText: '复制',
duplicateTitle: '复制选中字段(Ctrl+D)',
duplicateField: '复制该字段(Ctrl+D)',
duplicateFieldError: '重复的字段名称',
empty: '清空',
expandAll: '展开所有字段',
expandTitle: '点击 展开/收缩 该字段(Ctrl+E). \n' +
'Ctrl+Click 展开/收缩 包含所有子节点.',
formatTitle: '使用适当的缩进和换行符格式化JSON数据 (Ctrl+\\)',
insert: '插入',
insertTitle: '在此字段前插入类型为“auto”的新字段 (Ctrl+Ins)',
insertSub: '选择要插入的字段类型',
object: '对象',
ok: 'Ok',
redo: '重做 (Ctrl+Shift+Z)',
removeText: '移除',
removeTitle: '移除选中字段 (Ctrl+Del)',
removeField: '移除该字段 (Ctrl+Del)',
repairTitle: '修复JSON修复引号和转义符删除注释和JSONP表示法将JavaScript对象转换为JSON。',
selectNode: '选择一个节点...',
showAll: '展示全部',
showMore: '展示更多',
showMoreStatus: '显示${totalChilds}的${visibleChilds}项目.',
sort: '排序',
sortTitle: '排序${type}的子节点',
sortTitleShort: '内容排序',
sortFieldLabel: '字段:',
sortDirectionLabel: '方向:',
sortFieldTitle: '选择用于对数组或对象排序的嵌套字段',
sortAscending: '升序排序',
sortAscendingTitle: '按照该字段升序排序',
sortDescending: '降序排序',
sortDescendingTitle: '按照该字段降序排序',
string: '字符串',
transform: '变换',
transformTitle: '筛选,排序,或者转换${type}的子节点',
transformTitleShort: '筛选,排序,或者转换内容',
extract: '提取',
extractTitle: '提取这个 ${type}',
transformQueryTitle: '输入JMESPath查询',
transformWizardLabel: '向导',
transformWizardFilter: '筛选',
transformWizardSortBy: '排序',
transformWizardSelectFields: '选择字段',
transformQueryLabel: '查询',
transformPreviewLabel: '预览',
type: '类型',
typeTitle: '更改字段类型',
openUrl: 'Ctrl+Click 或者 Ctrl+Enter 在新窗口打开链接',
undo: '撤销上次动作 (Ctrl+Z)',
validationCannotMove: '无法将字段移入其子节点',
autoType: '字段类型 "auto". ' +
'字段类型由值自动确定 ' +
'可以为 stringnumberboolean或者 null.',
objectType: '字段类型 "object". ' +
'对象包含一组无序的键/值对.',
arrayType: '字段类型 "array". ' +
'数组包含值的有序集合.',
stringType: '字段类型 "string". ' +
'字段类型由值自动确定,' +
'但始终作为字符串返回.',
modeCodeText: '代码',
modeCodeTitle: '切换至代码高亮',
modeFormText: '表单',
modeFormTitle: '切换至表单编辑',
modeTextText: '文本',
modeTextTitle: '切换至文本编辑',
modeTreeText: '树',
modeTreeTitle: '切换至树编辑',
modeViewText: '视图',
modeViewTitle: '切换至树视图',
modePreviewText: '预览',
modePreviewTitle: '切换至预览模式',
examples: '例子',
default: '缺省'
},
'pt-BR': {
array: 'Lista',
auto: 'Automatico',
appendText: 'Adicionar',
appendTitle: 'Adicionar novo campo com tipo \'auto\' depois deste campo (Ctrl+Shift+Ins)',
appendSubmenuTitle: 'Selecione o tipo do campo a ser adicionado',
appendTitleAuto: 'Adicionar novo campo com tipo \'auto\' (Ctrl+Shift+Ins)',
ascending: 'Ascendente',
ascendingTitle: 'Organizar filhor do tipo ${type} em crescente',
actionsMenu: 'Clique para abrir o menu de ações (Ctrl+M)',
cannotParseFieldError: 'Não é possível analisar o campo no JSON',
cannotParseValueError: 'Não é possível analisar o valor em JSON',
collapseAll: 'Fechar todos campos',
compactTitle: 'Dados JSON compactos, remova todos os espaços em branco (Ctrl+Shift+\\)',
descending: 'Descendente',
descendingTitle: 'Organizar o filhos do tipo ${type} em decrescente',
duplicateKey: 'chave duplicada',
drag: 'Arraste para mover este campo (Alt+Shift+Arrows)',
duplicateText: 'Duplicar',
duplicateTitle: 'Duplicar campos selecionados (Ctrl+D)',
duplicateField: 'Duplicar este campo (Ctrl+D)',
duplicateFieldError: 'Nome do campo duplicado',
empty: 'vazio',
expandAll: 'Expandir todos campos',
expandTitle: 'Clique para expandir/encolher este campo (Ctrl+E). \n' +
'Ctrl+Click para expandir/encolher incluindo todos os filhos.',
formatTitle: 'Formate dados JSON, com recuo e feeds de linha adequados (Ctrl+\\)',
insert: 'Inserir',
insertTitle: 'Inserir um novo campo do tipo \'auto\' antes deste campo (Ctrl+Ins)',
insertSub: 'Selecionar o tipo de campo a ser inserido',
object: 'Objeto',
ok: 'Ok',
redo: 'Refazer (Ctrl+Shift+Z)',
removeText: 'Remover',
removeTitle: 'Remover campos selecionados (Ctrl+Del)',
removeField: 'Remover este campo (Ctrl+Del)',
repairTitle: 'Repare JSON: corrija aspas e caracteres de escape, remova comentários e notação JSONP, transforme objetos JavaScript em JSON.',
selectNode: 'Selecione um nódulo...',
showAll: 'mostrar todos',
showMore: 'mostrar mais',
showMoreStatus: 'exibindo ${visibleChilds} de ${totalChilds} itens.',
sort: 'Organizar',
sortTitle: 'Organizar os filhos deste ${type}',
sortTitleShort: 'Organizar os filhos',
sortFieldLabel: 'Campo:',
sortDirectionLabel: 'Direção:',
sortFieldTitle: 'Selecione um campo filho pelo qual ordenar o array ou objeto',
sortAscending: 'Ascendente',
sortAscendingTitle: 'Ordenar o campo selecionado por ordem ascendente',
sortDescending: 'Descendente',
sortDescendingTitle: 'Ordenar o campo selecionado por ordem descendente',
string: 'Texto',
transform: 'Transformar',
transformTitle: 'Filtrar, ordenar ou transformar os filhos deste ${type}',
transformTitleShort: 'Filtrar, ordenar ou transformar conteúdos',
transformQueryTitle: 'Insira uma expressão JMESPath',
transformWizardLabel: 'Assistente',
transformWizardFilter: 'Filtro',
transformWizardSortBy: 'Ordenar por',
transformWizardSelectFields: 'Selecionar campos',
transformQueryLabel: 'Expressão',
transformPreviewLabel: 'Visualizar',
type: 'Tipo',
typeTitle: 'Mudar o tipo deste campo',
openUrl: 'Ctrl+Click ou Ctrl+Enter para abrir link em nova janela',
undo: 'Desfazer último ação (Ctrl+Z)',
validationCannotMove: 'Não pode mover um campo como filho dele mesmo',
autoType: 'Campo do tipo "auto". ' +
'O tipo do campo é determinao automaticamente a partir do seu valor ' +
'e pode ser texto, número, verdade/falso ou nulo.',
objectType: 'Campo do tipo "objeto". ' +
'Um objeto contém uma lista de pares com chave e valor.',
arrayType: 'Campo do tipo "lista". ' +
'Uma lista contem uma coleção de valores ordenados.',
stringType: 'Campo do tipo "string". ' +
'Campo do tipo nao é determinado através do seu valor, ' +
'mas sempre retornara um texto.',
examples: 'Exemplos',
default: 'Revelia'
},
tr: {
array: 'Dizin',
auto: 'Otomatik',
appendText: 'Ekle',
appendTitle: 'Bu alanın altına \'otomatik\' tipinde yeni bir alan ekle (Ctrl+Shift+Ins)',
appendSubmenuTitle: 'Eklenecek alanın tipini seç',
appendTitleAuto: '\'Otomatik\' tipinde yeni bir alan ekle (Ctrl+Shift+Ins)',
ascending: 'Artan',
ascendingTitle: '${type}\'ın alt tiplerini artan düzende sırala',
actionsMenu: 'Aksiyon menüsünü açmak için tıklayın (Ctrl+M)',
collapseAll: 'Tüm alanları kapat',
descending: 'Azalan',
descendingTitle: '${type}\'ın alt tiplerini azalan düzende sırala',
drag: 'Bu alanı taşımak için sürükleyin (Alt+Shift+Arrows)',
duplicateKey: 'Var olan anahtar',
duplicateText: 'Aşağıya kopyala',
duplicateTitle: 'Seçili alanlardan bir daha oluştur (Ctrl+D)',
duplicateField: 'Bu alandan bir daha oluştur (Ctrl+D)',
duplicateFieldError: 'Duplicate field name',
cannotParseFieldError: 'Alan JSON\'a ayrıştırılamıyor',
cannotParseValueError: 'JSON\'a değer ayrıştırılamıyor',
empty: 'boş',
expandAll: 'Tüm alanları aç',
expandTitle: 'Bu alanı açmak/kapatmak için tıkla (Ctrl+E). \n' +
'Alt alanlarda dahil tüm alanları açmak için Ctrl+Click ',
insert: 'Ekle',
insertTitle: 'Bu alanın üstüne \'otomatik\' tipinde yeni bir alan ekle (Ctrl+Ins)',
insertSub: 'Araya eklenecek alanın tipini seç',
object: 'Nesne',
ok: 'Tamam',
redo: 'Yeniden yap (Ctrl+Shift+Z)',
removeText: 'Kaldır',
removeTitle: 'Seçilen alanları kaldır (Ctrl+Del)',
removeField: 'Bu alanı kaldır (Ctrl+Del)',
selectNode: 'Bir nesne seç...',
showAll: 'tümünü göster',
showMore: 'daha fazla göster',
showMoreStatus: '${totalChilds} alanın ${visibleChilds} alt alanları gösteriliyor',
sort: 'Sırala',
sortTitle: '${type}\'ın alt alanlarını sırala',
sortTitleShort: 'İçerikleri sırala',
sortFieldLabel: 'Alan:',
sortDirectionLabel: 'Yön:',
sortFieldTitle: 'Diziyi veya nesneyi sıralamak için iç içe geçmiş alanı seçin',
sortAscending: 'Artan',
sortAscendingTitle: 'Seçili alanı artan düzende sırala',
sortDescending: 'Azalan',
sortDescendingTitle: 'Seçili alanı azalan düzende sırala',
string: 'Karakter Dizisi',
transform: 'Dönüştür',
transformTitle: '${type}\'ın alt alanlarını filtrele, sırala veya dönüştür',
transformTitleShort: 'İçerikleri filterele, sırala veya dönüştür',
transformQueryTitle: 'JMESPath sorgusu gir',
transformWizardLabel: 'Sihirbaz',
transformWizardFilter: 'Filtre',
transformWizardSortBy: 'Sırala',
transformWizardSelectFields: 'Alanları seç',
transformQueryLabel: 'Sorgu',
transformPreviewLabel: 'Önizleme',
type: 'Tip',
typeTitle: 'Bu alanın tipini değiştir',
openUrl: 'URL\'i yeni bir pencerede açmak için Ctrl+Click veya Ctrl+Enter',
undo: 'Son değişikliği geri al (Ctrl+Z)',
validationCannotMove: 'Alt alan olarak taşınamıyor',
autoType: 'Alan tipi "otomatik". ' +
'Alan türü otomatik olarak değerden belirlenir' +
've bir dize, sayı, boolean veya null olabilir.',
objectType: 'Alan tipi "nesne". ' +
'Bir nesne, sıralanmamış bir anahtar / değer çifti kümesi içerir.',
arrayType: 'Alan tipi "dizi". ' +
'Bir dizi, düzenli değerler koleksiyonu içerir.',
stringType: 'Alan tipi "karakter dizisi". ' +
'Alan türü değerden belirlenmez,' +
'ancak her zaman karakter dizisi olarak döndürülür.',
modeCodeText: 'Kod',
modeCodeTitle: 'Kod vurgulayıcıya geç',
modeFormText: 'Form',
modeFormTitle: 'Form düzenleyiciye geç',
modeTextText: 'Metin',
modeTextTitle: 'Düz metin düzenleyiciye geç',
modeTreeText: 'Ağaç',
modeTreeTitle: 'Ağaç düzenleyiciye geç',
modeViewText: 'Görünüm',
modeViewTitle: 'Ağaç görünümüne geç',
examples: 'Örnekler',
default: 'Varsayılan'
},
ja: {
array: '配列',
auto: 'オート',
appendText: '追加',
appendTitle: '次のフィールドに"オート"のフィールドを追加 (Ctrl+Shift+Ins)',
appendSubmenuTitle: '追加するフィールドの型を選択してください',
appendTitleAuto: '"オート"のフィールドを追加 (Ctrl+Shift+Ins)',
ascending: '昇順',
ascendingTitle: '${type}の子要素を昇順に並べ替え',
actionsMenu: 'クリックしてアクションメニューを開く (Ctrl+M)',
collapseAll: 'すべてを折りたたむ',
descending: '降順',
descendingTitle: '${type}の子要素を降順に並べ替え',
drag: 'ドラッグして選択中のフィールドを移動 (Alt+Shift+Arrows)',
duplicateKey: '複製キー',
duplicateText: '複製',
duplicateTitle: '選択中のフィールドを複製 (Ctrl+D)',
duplicateField: '選択中のフィールドを複製 (Ctrl+D)',
duplicateFieldError: 'フィールド名が重複しています',
cannotParseFieldError: 'JSONのフィールドを解析できません',
cannotParseValueError: 'JSONの値を解析できません',
empty: '空',
expandAll: 'すべてを展開',
expandTitle: 'クリックしてフィールドを展開/折りたたむ (Ctrl+E). \n' +
'Ctrl+Click ですべての子要素を展開/折りたたむ',
insert: '挿入',
insertTitle: '選択中のフィールドの前に新しいフィールドを挿入 (Ctrl+Ins)',
insertSub: '挿入するフィールドの型を選択',
object: 'オブジェクト',
ok: '実行',
redo: 'やり直す (Ctrl+Shift+Z)',
removeText: '削除',
removeTitle: '選択中のフィールドを削除 (Ctrl+Del)',
removeField: '選択中のフィールドを削除 (Ctrl+Del)',
selectNode: 'ノードを選択...',
showAll: 'すべてを表示',
showMore: 'もっと見る',
showMoreStatus: '${totalChilds}個のアイテムのうち ${visibleChilds}個を表示しています。',
sort: '並べ替え',
sortTitle: '${type}の子要素を並べ替え',
sortTitleShort: '並べ替え',
sortFieldLabel: 'フィールド:',
sortDirectionLabel: '順序:',
sortFieldTitle: '配列またはオブジェクトを並び替えるためのフィールドを選択',
sortAscending: '昇順',
sortAscendingTitle: '選択中のフィールドを昇順に並び替え',
sortDescending: '降順',
sortDescendingTitle: '選択中のフィールドを降順に並び替え',
string: '文字列',
transform: '変換',
transformTitle: '${type}の子要素をフィルター・並び替え・変換する',
transformTitleShort: '内容をフィルター・並び替え・変換する',
extract: '抽出',
extractTitle: '${type}を抽出',
transformQueryTitle: 'JMESPathクエリを入力',
transformWizardLabel: 'ウィザード',
transformWizardFilter: 'フィルター',
transformWizardSortBy: '並び替え',
transformWizardSelectFields: 'フィールドを選択',
transformQueryLabel: 'クエリ',
transformPreviewLabel: 'プレビュー',
type: '型',
typeTitle: '選択中のフィールドの型を変更',
openUrl: 'Ctrl+Click または Ctrl+Enter で 新規ウィンドウでURLを開く',
undo: '元に戻す (Ctrl+Z)',
validationCannotMove: '子要素に移動できません ',
autoType: 'オート: ' +
'フィールドの型は値から自動的に決定されます。 ' +
'(文字列・数値・ブール・null)',
objectType: 'オブジェクト: ' +
'オブジェクトは順序が決まっていないキーと値のペア組み合わせです。',
arrayType: '配列: ' +
'配列は順序が決まっている値の集合体です。',
stringType: '文字列: ' +
'フィールド型は値から決定されませんが、' +
'常に文字列として返されます。',
modeCodeText: 'コードモード',
modeCodeTitle: 'ハイライトモードに切り替え',
modeFormText: 'フォームモード',
modeFormTitle: 'フォームモードに切り替え',
modeTextText: 'テキストモード',
modeTextTitle: 'テキストモードに切り替え',
modeTreeText: 'ツリーモード',
modeTreeTitle: 'ツリーモードに切り替え',
modeViewText: 'ビューモード',
modeViewTitle: 'ビューモードに切り替え',
modePreviewText: 'プレビュー',
modePreviewTitle: 'プレビューに切り替え',
examples: '例',
default: 'デフォルト'
},
'fr-FR': {
array: 'Liste',
auto: 'Auto',
appendText: 'Ajouter',
appendTitle: 'Ajouter un champ de type \'auto\' après ce champ (Ctrl+Shift+Ins)',
appendSubmenuTitle: 'Sélectionner le type du champ à ajouter',
appendTitleAuto: 'Ajouter un champ de type \'auto\' (Ctrl+Shift+Ins)',
ascending: 'Ascendant',
ascendingTitle: 'Trier les enfants de ce ${type} par ordre ascendant',
actionsMenu: 'Ouvrir le menu des actions (Ctrl+M)',
collapseAll: 'Regrouper',
descending: 'Descendant',
descendingTitle: 'Trier les enfants de ce ${type} par ordre descendant',
drag: 'Déplacer (Alt+Shift+Arrows)',
duplicateKey: 'Dupliquer la clé',
duplicateText: 'Dupliquer',
duplicateTitle: 'Dupliquer les champs sélectionnés (Ctrl+D)',
duplicateField: 'Dupliquer ce champ (Ctrl+D)',
duplicateFieldError: 'Dupliquer le nom de champ',
cannotParseFieldError: 'Champ impossible à parser en JSON',
cannotParseValueError: 'Valeur impossible à parser en JSON',
empty: 'vide',
expandAll: 'Étendre',
expandTitle: 'Étendre/regrouper ce champ (Ctrl+E). \n' +
'Ctrl+Click pour étendre/regrouper avec tous les champs.',
insert: 'Insérer',
insertTitle: 'Insérer un champ de type \'auto\' avant ce champ (Ctrl+Ins)',
insertSub: 'Sélectionner le type de champ à insérer',
object: 'Objet',
ok: 'Ok',
redo: 'Rejouer (Ctrl+Shift+Z)',
removeText: 'Supprimer',
removeTitle: 'Supprimer les champs sélectionnés (Ctrl+Del)',
removeField: 'Supprimer ce champ (Ctrl+Del)',
searchTitle: 'Rechercher champs et valeurs',
searchNextResultTitle: 'Résultat suivant (Enter)',
searchPreviousResultTitle: 'Résultat précédent (Shift + Enter)',
selectNode: 'Sélectionner un nœud...',
showAll: 'voir tout',
showMore: 'voir plus',
showMoreStatus: '${visibleChilds} éléments affichés de ${totalChilds}.',
sort: 'Trier',
sortTitle: 'Trier les champs de ce ${type}',
sortTitleShort: 'Trier',
sortFieldLabel: 'Champ:',
sortDirectionLabel: 'Direction:',
sortFieldTitle: 'Sélectionner les champs permettant de trier les listes et objet',
sortAscending: 'Ascendant',
sortAscendingTitle: 'Trier les champs sélectionnés par ordre ascendant',
sortDescending: 'Descendant',
sortDescendingTitle: 'Trier les champs sélectionnés par ordre descendant',
string: 'Chaîne',
transform: 'Transformer',
transformTitle: 'Filtrer, trier, or transformer les enfants de ce ${type}',
transformTitleShort: 'Filtrer, trier ou transformer le contenu',
extract: 'Extraire',
extractTitle: 'Extraire ce ${type}',
transformQueryTitle: 'Saisir une requête JMESPath',
transformWizardLabel: 'Assistant',
transformWizardFilter: 'Filtrer',
transformWizardSortBy: 'Trier par',
transformWizardSelectFields: 'Sélectionner les champs',
transformQueryLabel: 'Requête',
transformPreviewLabel: 'Prévisualisation',
type: 'Type',
typeTitle: 'Changer le type de ce champ',
openUrl: 'Ctrl+Click ou Ctrl+Enter pour ouvrir l\'url dans une autre fenêtre',
undo: 'Annuler la dernière action (Ctrl+Z)',
validationCannotMove: 'Cannot move a field into a child of itself',
autoType: 'Champe de type "auto". ' +
'Ce type de champ est automatiquement déterminé en fonction de la valeur ' +
'et peut être de type "chaîne", "nombre", "booléen" ou null.',
objectType: 'Champ de type "objet". ' +
'Un objet contient un ensemble non ordonné de paires clé/valeur.',
arrayType: 'Champ de type "liste". ' +
'Une liste contient une collection ordonnée de valeurs.',
stringType: 'Champ de type "chaîne". ' +
'Ce type de champ n\'est pas déterminé en fonction de la valeur, ' +
'mais retourne systématiquement une chaîne de caractères.',
modeEditorTitle: 'Changer mode d\'édition',
modeCodeText: 'Code',
modeCodeTitle: 'Activer surlignage code',
modeFormText: 'Formulaire',
modeFormTitle: 'Activer formulaire',
modeTextText: 'Texte',
modeTextTitle: 'Activer éditeur texte',
modeTreeText: 'Arbre',
modeTreeTitle: 'Activer éditeur arbre',
modeViewText: 'Lecture seule',
modeViewTitle: 'Activer vue arbre',
modePreviewText: 'Prévisualisation',
modePreviewTitle: 'Activer mode prévisualiser',
examples: 'Exemples',
default: 'Défaut'
}
}
const _defaultLang = 'en'
const userLang = typeof navigator !== 'undefined'
? navigator.language || navigator.userLanguage
: undefined
let _lang = _locales.find(l => l === userLang) || _defaultLang
export function setLanguage (lang) {
if (!lang) {
return
}
const langFound = _locales.find(l => l === lang)
if (langFound) {
_lang = langFound
} else {
console.error('Language not found')
}
}
export function setLanguages (languages) {
if (!languages) {
return
}
for (const language in languages) {
const langFound = _locales.find(l => l === language)
if (!langFound) {
_locales.push(language)
}
_defs[language] = Object.assign({}, _defs[_defaultLang], _defs[language], languages[language])
}
}
export function translate (key, data, lang) {
if (!lang) {
lang = _lang
}
let text = _defs[lang][key] || _defs[_defaultLang][key] || key
if (data) {
for (const dataKey in data) {
text = text.replace('${' + dataKey + '}', data[dataKey])
}
}
return text
}

View File

@ -1,75 +0,0 @@
import jmespath from 'jmespath'
import { get, parsePath, parseString } from './util'
/**
* Build a JMESPath query based on query options coming from the wizard
* @param {JSON} json The JSON document for which to build the query.
* Used for context information like determining
* the type of values (string or number)
* @param {QueryOptions} queryOptions
* @return {string} Returns a query (as string)
*/
export function createQuery (json, queryOptions) {
const { sort, filter, projection } = queryOptions
let query = ''
if (filter) {
const examplePath = filter.field !== '@'
? ['0'].concat(parsePath('.' + filter.field))
: ['0']
const exampleValue = get(json, examplePath)
const value1 = typeof exampleValue === 'string'
? filter.value
: parseString(filter.value)
query += '[? ' +
filter.field + ' ' +
filter.relation + ' ' +
'`' + JSON.stringify(value1) + '`' +
']'
} else {
query += Array.isArray(json)
? '[*]'
: '@'
}
if (sort) {
if (sort.direction === 'desc') {
query += ' | reverse(sort_by(@, &' + sort.field + '))'
} else {
query += ' | sort_by(@, &' + sort.field + ')'
}
}
if (projection) {
if (query[query.length - 1] !== ']') {
query += ' | [*]'
}
if (projection.fields.length === 1) {
query += '.' + projection.fields[0]
} else if (projection.fields.length > 1) {
query += '.{' +
projection.fields.map(value => {
const parts = value.split('.')
const last = parts[parts.length - 1]
return last + ': ' + value
}).join(', ') +
'}'
} else { // values.length === 0
// ignore
}
}
return query
}
/**
* Execute a JMESPath query
* @param {JSON} json
* @param {string} query
* @return {JSON} Returns the transformed JSON
*/
export function executeQuery (json, query) {
return jmespath.search(json, query)
}

View File

@ -1,197 +0,0 @@
'use strict'
/**
* Convert part of a JSON object to a JSON string.
* Use case is to stringify a small part of a large JSON object so you can see
* a preview.
*
* @param {*} value
* The value to convert to a JSON string.
*
* @param {number | string | null} [space]
* A String or Number object that's used to insert white space into the output
* JSON string for readability purposes. If this is a Number, it indicates the
* number of space characters to use as white space; this number is capped at 10
* if it's larger than that. Values less than 1 indicate that no space should be
* used. If this is a String, the string (or the first 10 characters of the string,
* if it's longer than that) is used as white space. If this parameter is not
* provided (or is null), no white space is used.
*
* @param {number} [limit] Maximum size of the string output.
*
* @returns {string | undefined} Returns the string representation of the JSON object.
*/
export function stringifyPartial (value, space, limit) {
let _space // undefined by default
if (typeof space === 'number') {
if (space > 10) {
_space = repeat(' ', 10)
} else if (space >= 1) {
_space = repeat(' ', space)
}
// else ignore
} else if (typeof space === 'string' && space !== '') {
_space = space
}
const output = stringifyValue(value, _space, '', limit)
return output.length > limit
? (slice(output, limit) + '...')
: output
}
/**
* Stringify a value
* @param {*} value
* @param {string} space
* @param {string} indent
* @param {number} limit
* @return {string | undefined}
*/
function stringifyValue (value, space, indent, limit) {
// boolean, null, number, string, or date
if (typeof value === 'boolean' || value instanceof Boolean ||
value === null ||
typeof value === 'number' || value instanceof Number ||
typeof value === 'string' || value instanceof String ||
value instanceof Date) {
return JSON.stringify(value)
}
// array
if (Array.isArray(value)) {
return stringifyArray(value, space, indent, limit)
}
// object (test lastly!)
if (value && typeof value === 'object') {
return stringifyObject(value, space, indent, limit)
}
return undefined
}
/**
* Stringify an array
* @param {Array} array
* @param {string} space
* @param {string} indent
* @param {number} limit
* @return {string}
*/
function stringifyArray (array, space, indent, limit) {
const childIndent = space ? (indent + space) : undefined
let str = space ? '[\n' : '['
for (let i = 0; i < array.length; i++) {
const item = array[i]
if (space) {
str += childIndent
}
if (typeof item !== 'undefined' && typeof item !== 'function') {
str += stringifyValue(item, space, childIndent, limit)
} else {
str += 'null'
}
if (i < array.length - 1) {
str += space ? ',\n' : ','
}
// stop as soon as we're exceeding the limit
if (str.length > limit) {
return str + '...'
}
}
str += space ? ('\n' + indent + ']') : ']'
return str
}
/**
* Stringify an object
* @param {Object} object
* @param {string} space
* @param {string} indent
* @param {number} limit
* @return {string}
*/
function stringifyObject (object, space, indent, limit) {
const childIndent = space ? (indent + space) : undefined
let first = true
let str = space ? '{\n' : '{'
if (typeof object.toJSON === 'function') {
return stringifyValue(object.toJSON(), space, indent, limit)
}
for (const key in object) {
if (hasOwnProperty(object, key)) {
const value = object[key]
if (first) {
first = false
} else {
str += space ? ',\n' : ','
}
str += space
? (childIndent + '"' + key + '": ')
: ('"' + key + '":')
str += stringifyValue(value, space, childIndent, limit)
// stop as soon as we're exceeding the limit
if (str.length > limit) {
return str + '...'
}
}
}
str += space ? ('\n' + indent + '}') : '}'
return str
}
/**
* Repeat a string a number of times.
* Simple linear solution, we only need up to 10 iterations in practice
* @param {string} text
* @param {number} times
* @return {string}
*/
function repeat (text, times) {
let res = ''
while (times-- > 0) {
res += text
}
return res
}
/**
* Limit the length of text
* @param {string} text
* @param {number} [limit]
* @return {string}
*/
function slice (text, limit) {
return typeof limit === 'number'
? text.slice(0, limit)
: text
}
/**
* Test whether some text contains a JSON array, i.e. the first
* non-white space character is a [
* @param {string} jsonText
* @return {boolean}
*/
export function containsArray (jsonText) {
return /^\s*\[/.test(jsonText)
}
function hasOwnProperty (object, key) {
return Object.prototype.hasOwnProperty.call(object, key)
}

View File

@ -1,54 +0,0 @@
if (typeof Element !== 'undefined') {
// Polyfill for array remove
(() => {
function polyfill (item) {
if ('remove' in item) {
return
}
Object.defineProperty(item, 'remove', {
configurable: true,
enumerable: true,
writable: true,
value: function remove () {
if (this.parentNode !== undefined) { this.parentNode.removeChild(this) }
}
})
}
if (typeof window.Element !== 'undefined') { polyfill(window.Element.prototype) }
if (typeof window.CharacterData !== 'undefined') { polyfill(window.CharacterData.prototype) }
if (typeof window.DocumentType !== 'undefined') { polyfill(window.DocumentType.prototype) }
})()
}
// simple polyfill for Array.findIndex
if (!Array.prototype.findIndex) {
// eslint-disable-next-line no-extend-native
Array.prototype.findIndex = function (predicate) {
for (let i = 0; i < this.length; i++) {
const element = this[i]
if (predicate.call(this, element, i, this)) {
return i
}
}
return -1
}
}
// Polyfill for Array.find
if (!Array.prototype.find) {
// eslint-disable-next-line no-extend-native
Array.prototype.find = function (predicate) {
const i = this.findIndex(predicate)
return this[i]
}
}
// Polyfill for String.trim
if (!String.prototype.trim) {
// eslint-disable-next-line no-extend-native
String.prototype.trim = function () {
return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '')
}
}

View File

@ -1,689 +0,0 @@
'use strict'
import { setLanguage, setLanguages, translate } from './i18n'
import { ModeSwitcher } from './ModeSwitcher'
import { ErrorTable } from './ErrorTable'
import { showSortModal } from './showSortModal'
import { showTransformModal } from './showTransformModal'
import { textModeMixins } from './textmode'
import { DEFAULT_MODAL_ANCHOR, MAX_PREVIEW_CHARACTERS, PREVIEW_HISTORY_LIMIT, SIZE_LARGE } from './constants'
import { FocusTracker } from './FocusTracker'
import {
addClassName,
debounce,
escapeUnicodeChars,
formatSize,
isObject,
limitCharacters,
parse,
removeClassName,
repair,
sort,
sortObjectKeys
} from './util'
import { History } from './History'
import { createQuery, executeQuery } from './jmespathQuery'
const textmode = textModeMixins[0].mixin
// create a mixin with the functions for text mode
const previewmode = {}
/**
* Create a JSON document preview, suitable for processing of large documents
* @param {Element} container
* @param {Object} [options] Object with options. See docs for details.
* @private
*/
previewmode.create = function (container, options = {}) {
if (typeof options.statusBar === 'undefined') {
options.statusBar = true
}
// setting default for previewmode
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
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)
// determine mode
this.mode = 'preview'
const me = this
this.container = container
this.dom = {}
this.json = undefined
this.text = ''
// TODO: JSON Schema support
// create a debounced validate function
this._debouncedValidate = debounce(this.validate.bind(this), this.DEBOUNCE_INTERVAL)
this.width = container.clientWidth
this.height = container.clientHeight
this.frame = document.createElement('div')
this.frame.className = 'jsoneditor jsoneditor-mode-preview'
this.frame.onclick = event => {
// prevent default submit action when the editor is located inside a form
event.preventDefault()
}
// 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'
this.dom.busy = document.createElement('div')
this.dom.busy.className = 'jsoneditor-busy'
this.dom.busyContent = document.createElement('span')
this.dom.busyContent.innerHTML = 'busy...'
this.dom.busy.appendChild(this.dom.busyContent)
this.content.appendChild(this.dom.busy)
this.dom.previewContent = document.createElement('pre')
this.dom.previewContent.className = 'jsoneditor-preview'
this.dom.previewText = document.createTextNode('')
this.dom.previewContent.appendChild(this.dom.previewText)
this.content.appendChild(this.dom.previewContent)
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 = function handleFormat () {
me.executeWithBusyMessage(() => {
try {
me.format()
} catch (err) {
me._onError(err)
}
}, 'formatting...')
}
// 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 = function handleCompact () {
me.executeWithBusyMessage(() => {
try {
me.compact()
} catch (err) {
me._onError(err)
}
}, 'compacting...')
}
// 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.dom.transform = transform
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 = () => {
if (me.json === undefined) { // only repair if we don't have valid JSON
me.executeWithBusyMessage(() => {
try {
me.repair()
} catch (err) {
me._onError(err)
}
}, 'repairing...')
}
}
// create history and undo/redo buttons
if (this.options.history !== false) { // default option value is true
const onHistoryChange = () => {
me.dom.undo.disabled = !me.history.canUndo()
me.dom.redo.disabled = !me.history.canRedo()
}
const calculateItemSize = item => // times two to account for the json object
item.text.length * 2
this.history = new History(onHistoryChange, calculateItemSize, PREVIEW_HISTORY_LIMIT)
// create undo button
const undo = document.createElement('button')
undo.type = 'button'
undo.className = 'jsoneditor-undo jsoneditor-separator'
undo.title = translate('undo')
undo.onclick = () => {
const action = me.history.undo()
if (action) {
me._applyHistory(action)
}
}
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 = () => {
const action = me.history.redo()
if (action) {
me._applyHistory(action)
}
}
this.menu.appendChild(redo)
this.dom.redo = redo
// force enabling/disabling the undo/redo button
this.history.onChange()
}
// 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
me.setMode(mode)
me.modeSwitcher.focus()
})
}
}
this.errorTable = new ErrorTable({
errorTableVisible: true,
onToggleVisibility: function () {
me.validate()
},
onFocusLine: null,
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.content)
this.frame.appendChild(this.errorTable.getErrorTable())
this.container.appendChild(this.frame)
if (options.statusBar) {
addClassName(this.content, 'has-status-bar')
const statusBar = document.createElement('div')
this.dom.statusBar = statusBar
statusBar.className = 'jsoneditor-statusbar'
this.frame.appendChild(statusBar)
this.dom.fileSizeInfo = document.createElement('span')
this.dom.fileSizeInfo.className = 'jsoneditor-size-info'
this.dom.fileSizeInfo.innerText = ''
statusBar.appendChild(this.dom.fileSizeInfo)
this.dom.arrayInfo = document.createElement('span')
this.dom.arrayInfo.className = 'jsoneditor-size-info'
this.dom.arrayInfo.innerText = ''
statusBar.appendChild(this.dom.arrayInfo)
statusBar.appendChild(this.errorTable.getErrorCounter())
statusBar.appendChild(this.errorTable.getWarningIcon())
statusBar.appendChild(this.errorTable.getErrorIcon())
}
this._renderPreview()
this.setSchema(this.options.schema, this.options.schemaRefs)
}
previewmode._renderPreview = function () {
const text = this.getText()
this.dom.previewText.nodeValue = limitCharacters(text, MAX_PREVIEW_CHARACTERS)
if (this.dom.fileSizeInfo) {
this.dom.fileSizeInfo.innerText = 'Size: ' + formatSize(text.length)
}
if (this.dom.arrayInfo) {
if (Array.isArray(this.json)) {
this.dom.arrayInfo.innerText = ('Array: ' + this.json.length + ' items')
} else {
this.dom.arrayInfo.innerText = ''
}
}
}
/**
* Handle a change:
* - Validate JSON schema
* - Send a callback to the onChange listener if provided
* @private
*/
previewmode._onChange = function () {
// 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 onChangeJSON callback
if (this.options.onChangeJSON) {
try {
this.options.onChangeJSON(this.get())
} catch (err) {
console.error('Error in onChangeJSON 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)
}
}
}
/**
* Open a sort modal
* @private
*/
previewmode._showSortModal = function () {
const me = this
function onSort (json, sortedBy) {
if (Array.isArray(json)) {
const sortedArray = sort(json, sortedBy.path, sortedBy.direction)
me.sortedBy = sortedBy
me._setAndFireOnChange(sortedArray)
}
if (isObject(json)) {
const sortedObject = sortObjectKeys(json, sortedBy.direction)
me.sortedBy = sortedBy
me._setAndFireOnChange(sortedObject)
}
}
this.executeWithBusyMessage(() => {
const container = me.options.modalAnchor || DEFAULT_MODAL_ANCHOR
const json = me.get()
me._renderPreview() // update array count
showSortModal(container, json, sortedBy => {
me.executeWithBusyMessage(() => {
onSort(json, sortedBy)
}, 'sorting...')
}, me.sortedBy)
}, 'parsing...')
}
/**
* Open a transform modal
* @private
*/
previewmode._showTransformModal = function () {
this.executeWithBusyMessage(() => {
const { createQuery, executeQuery, modalAnchor, queryDescription } = this.options
const json = this.get()
this._renderPreview() // update array count
showTransformModal({
anchor: modalAnchor || DEFAULT_MODAL_ANCHOR,
json,
queryDescription, // can be undefined
createQuery,
executeQuery,
onTransform: query => {
this.executeWithBusyMessage(() => {
const updatedJson = executeQuery(json, query)
this._setAndFireOnChange(updatedJson)
}, 'transforming...')
}
})
}, 'parsing...')
}
/**
* Destroy the editor. Clean up DOM, event listeners, and web workers.
*/
previewmode.destroy = function () {
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._debouncedValidate = null
if (this.history) {
this.history.clear()
this.history = null
}
// Removing the FocusTracker set to track the editor's focus event
this.frameFocusTracker.destroy()
}
/**
* Compact the code in the text editor
*/
previewmode.compact = function () {
const json = this.get()
const text = JSON.stringify(json)
// we know that in this case the json is still the same, so we pass json too
this._setTextAndFireOnChange(text, json)
}
/**
* Format the code in the text editor
*/
previewmode.format = function () {
const json = this.get()
const text = JSON.stringify(json, null, this.indentation)
// we know that in this case the json is still the same, so we pass json too
this._setTextAndFireOnChange(text, json)
}
/**
* Repair the code in the text editor
*/
previewmode.repair = function () {
const text = this.getText()
const repairedText = repair(text)
this._setTextAndFireOnChange(repairedText)
}
/**
* Set focus to the editor
*/
previewmode.focus = function () {
// we don't really have a place to focus,
// let's focus on the transform button
this.dom.transform.focus()
}
/**
* Set json data in the editor
* @param {*} json
*/
previewmode.set = function (json) {
if (this.history) {
this.history.clear()
}
this._set(json)
}
/**
* Update data. Same as calling `set` in text/code mode.
* @param {*} json
*/
previewmode.update = function (json) {
this._set(json)
}
/**
* Set json data
* @param {*} json
*/
previewmode._set = function (json) {
this.text = undefined
this.json = json
this._renderPreview()
this._pushHistory()
// validate JSON schema
this._debouncedValidate()
}
previewmode._setAndFireOnChange = function (json) {
this._set(json)
this._onChange()
}
/**
* Get json data
* @return {*} json
*/
previewmode.get = function () {
if (this.json === undefined) {
const text = this.getText()
this.json = parse(text) // this can throw an error
}
return this.json
}
/**
* Get the text contents of the editor
* @return {String} jsonText
*/
previewmode.getText = function () {
if (this.text === undefined) {
this.text = JSON.stringify(this.json, null, this.indentation)
if (this.options.escapeUnicode === true) {
this.text = escapeUnicodeChars(this.text)
}
}
return this.text
}
/**
* Set the text contents of the editor
* @param {String} jsonText
*/
previewmode.setText = function (jsonText) {
if (this.history) {
this.history.clear()
}
this._setText(jsonText)
}
/**
* Update the text contents
* @param {string} jsonText
*/
previewmode.updateText = function (jsonText) {
// don't update if there are no changes
if (this.getText() === jsonText) {
return
}
this._setText(jsonText)
}
/**
* Set the text contents of the editor
* @param {string} jsonText
* @param {*} [json] Optional JSON instance of the text
* @private
*/
previewmode._setText = function (jsonText, json) {
if (this.options.escapeUnicode === true) {
this.text = escapeUnicodeChars(jsonText)
} else {
this.text = jsonText
}
this.json = json
this._renderPreview()
if (this.json === undefined) {
const me = this
this.executeWithBusyMessage(() => {
try {
// force parsing the json now, else it will be done in validate without feedback
me.json = me.get()
me._renderPreview()
me._pushHistory()
} catch (err) {
// no need to throw an error, validation will show an error
}
}, 'parsing...')
} else {
this._pushHistory()
}
this._debouncedValidate()
}
/**
* Set text and fire onChange callback
* @param {string} jsonText
* @param {*} [json] Optional JSON instance of the text
* @private
*/
previewmode._setTextAndFireOnChange = function (jsonText, json) {
this._setText(jsonText, json)
this._onChange()
}
/**
* Apply history to the current state
* @param {{json?: JSON, text?: string}} action
* @private
*/
previewmode._applyHistory = function (action) {
this.json = action.json
this.text = action.text
this._renderPreview()
this._debouncedValidate()
}
/**
* Push the current state to history
* @private
*/
previewmode._pushHistory = function () {
if (!this.history) {
return
}
const action = {
text: this.text,
json: this.json
}
this.history.add(action)
}
/**
* Execute a heavy, blocking action.
* Before starting the action, show a message on screen like "parsing..."
* @param {function} fn
* @param {string} message
*/
previewmode.executeWithBusyMessage = function (fn, message) {
const size = this.getText().length
if (size > SIZE_LARGE) {
const me = this
addClassName(me.frame, 'busy')
me.dom.busyContent.innerText = message
setTimeout(() => {
fn()
removeClassName(me.frame, 'busy')
me.dom.busyContent.innerText = ''
}, 100)
} else {
fn()
}
}
// TODO: refactor into composable functions instead of this shaky mixin-like structure
previewmode.validate = textmode.validate
previewmode._renderErrors = textmode._renderErrors
// define modes
export const previewModeMixins = [
{
mode: 'preview',
mixin: previewmode,
data: 'json'
}
]

View File

@ -1,154 +0,0 @@
'use strict'
import { translate } from './i18n'
/**
* A factory function to create an ShowMoreNode, which depends on a Node
* @param {function} Node
*/
export function showMoreNodeFactory (Node) {
/**
* @constructor ShowMoreNode
* @extends Node
* @param {TreeEditor} editor
* @param {Node} parent
* Create a new ShowMoreNode. This is a special node which is created
* for arrays or objects having more than 100 items
*/
function ShowMoreNode (editor, parent) {
/** @type {TreeEditor} */
this.editor = editor
this.parent = parent
this.dom = {}
}
ShowMoreNode.prototype = new Node()
/**
* Return a table row with an append button.
* @return {Element} dom TR element
*/
ShowMoreNode.prototype.getDom = function () {
if (this.dom.tr) {
return this.dom.tr
}
this._updateEditability()
// display "show more"
if (!this.dom.tr) {
const me = this
const parent = this.parent
const showMoreButton = document.createElement('a')
showMoreButton.appendChild(document.createTextNode(translate('showMore')))
showMoreButton.href = '#'
showMoreButton.onclick = event => {
// TODO: use callback instead of accessing a method of the parent
parent.visibleChilds = Math.floor(parent.visibleChilds / parent.getMaxVisibleChilds() + 1) *
parent.getMaxVisibleChilds()
me.updateDom()
parent.showChilds()
event.preventDefault()
return false
}
const showAllButton = document.createElement('a')
showAllButton.appendChild(document.createTextNode(translate('showAll')))
showAllButton.href = '#'
showAllButton.onclick = event => {
// TODO: use callback instead of accessing a method of the parent
parent.visibleChilds = Infinity
me.updateDom()
parent.showChilds()
event.preventDefault()
return false
}
const moreContents = document.createElement('div')
const moreText = document.createTextNode(this._getShowMoreText())
moreContents.className = 'jsoneditor-show-more'
moreContents.appendChild(moreText)
moreContents.appendChild(showMoreButton)
moreContents.appendChild(document.createTextNode('. '))
moreContents.appendChild(showAllButton)
moreContents.appendChild(document.createTextNode('. '))
const tdContents = document.createElement('td')
tdContents.appendChild(moreContents)
const moreTr = document.createElement('tr')
if (this.editor.options.mode === 'tree') {
moreTr.appendChild(document.createElement('td'))
moreTr.appendChild(document.createElement('td'))
}
moreTr.appendChild(tdContents)
moreTr.className = 'jsoneditor-show-more'
this.dom.tr = moreTr
this.dom.moreContents = moreContents
this.dom.moreText = moreText
}
this.updateDom()
return this.dom.tr
}
/**
* Update the HTML dom of the Node
*/
ShowMoreNode.prototype.updateDom = function (options) {
if (this.isVisible()) {
// attach to the right child node (the first non-visible child)
this.dom.tr.node = this.parent.childs[this.parent.visibleChilds]
if (!this.dom.tr.parentNode) {
const nextTr = this.parent._getNextTr()
if (nextTr) {
nextTr.parentNode.insertBefore(this.dom.tr, nextTr)
}
}
// update the counts in the text
this.dom.moreText.nodeValue = this._getShowMoreText()
// update left margin
this.dom.moreContents.style.marginLeft = (this.getLevel() + 1) * 24 + 'px'
} else {
if (this.dom.tr && this.dom.tr.parentNode) {
this.dom.tr.parentNode.removeChild(this.dom.tr)
}
}
}
ShowMoreNode.prototype._getShowMoreText = function () {
return translate('showMoreStatus', {
visibleChilds: this.parent.visibleChilds,
totalChilds: this.parent.childs.length
}) + ' '
}
/**
* Check whether the ShowMoreNode is currently visible.
* the ShowMoreNode is visible when it's parent has more childs than
* the current visibleChilds
* @return {boolean} isVisible
*/
ShowMoreNode.prototype.isVisible = function () {
return this.parent.expanded && this.parent.childs.length > this.parent.visibleChilds
}
/**
* Handle an event. The event is caught centrally by the editor
* @param {Event} event
*/
ShowMoreNode.prototype.onEvent = function (event) {
const type = event.type
if (type === 'keydown') {
this.onKeyDown(event)
}
}
return ShowMoreNode
}

View File

@ -1,131 +0,0 @@
import picoModal from 'picomodal'
import { translate } from './i18n'
import { contains, getChildPaths } from './util'
/**
* Show advanced sorting modal
* @param {HTMLElement} container The container where to center
* the modal and create an overlay
* @param {JSON} json The JSON data to be sorted.
* @param {function} onSort Callback function, invoked with
* an object containing the selected
* path and direction
* @param {Object} options
* Available options:
* - {string} path The selected path
* - {'asc' | 'desc'} direction The selected direction
*/
export function showSortModal (container, json, onSort, options) {
const paths = Array.isArray(json)
? getChildPaths(json)
: ['']
const selectedPath = options && options.path && contains(paths, options.path)
? options.path
: paths[0]
const selectedDirection = (options && options.direction) || 'asc'
const content = '<div class="pico-modal-contents">' +
'<div class="pico-modal-header">' + translate('sort') + '</div>' +
'<form>' +
'<table>' +
'<tbody>' +
'<tr>' +
' <td>' + translate('sortFieldLabel') + ' </td>' +
' <td class="jsoneditor-modal-input">' +
' <div class="jsoneditor-select-wrapper">' +
' <select id="field" title="' + translate('sortFieldTitle') + '">' +
' </select>' +
' </div>' +
' </td>' +
'</tr>' +
'<tr>' +
' <td>' + translate('sortDirectionLabel') + ' </td>' +
' <td class="jsoneditor-modal-input">' +
' <div id="direction" class="jsoneditor-button-group">' +
'<input type="button" ' +
'value="' + translate('sortAscending') + '" ' +
'title="' + translate('sortAscendingTitle') + '" ' +
'data-value="asc" ' +
'class="jsoneditor-button-first jsoneditor-button-asc"/>' +
'<input type="button" ' +
'value="' + translate('sortDescending') + '" ' +
'title="' + translate('sortDescendingTitle') + '" ' +
'data-value="desc" ' +
'class="jsoneditor-button-last jsoneditor-button-desc"/>' +
' </div>' +
' </td>' +
'</tr>' +
'<tr>' +
'<td colspan="2" class="jsoneditor-modal-input jsoneditor-modal-actions">' +
' <input type="submit" id="ok" value="' + translate('ok') + '" />' +
'</td>' +
'</tr>' +
'</tbody>' +
'</table>' +
'</form>' +
'</div>'
picoModal({
parent: container,
content: content,
overlayClass: 'jsoneditor-modal-overlay',
overlayStyles: {
backgroundColor: 'rgb(1,1,1)',
opacity: 0.3
},
modalClass: 'jsoneditor-modal jsoneditor-modal-sort'
})
.afterCreate(modal => {
const form = modal.modalElem().querySelector('form')
const ok = modal.modalElem().querySelector('#ok')
const field = modal.modalElem().querySelector('#field')
const direction = modal.modalElem().querySelector('#direction')
function preprocessPath (path) {
return (path === '')
? '@'
: (path[0] === '.')
? path.slice(1)
: path
}
paths.forEach(path => {
const option = document.createElement('option')
option.text = preprocessPath(path)
option.value = path
field.appendChild(option)
})
function setDirection (value) {
direction.value = value
direction.className = 'jsoneditor-button-group jsoneditor-button-group-value-' + direction.value
}
field.value = selectedPath || paths[0]
setDirection(selectedDirection || 'asc')
direction.onclick = event => {
setDirection(event.target.getAttribute('data-value'))
}
ok.onclick = event => {
event.preventDefault()
event.stopPropagation()
modal.close()
onSort({
path: field.value,
direction: direction.value
})
}
if (form) { // form is not available when JSONEditor is created inside a form
form.onsubmit = ok.onclick
}
})
.afterClose(modal => {
modal.destroy()
})
.show()
}

View File

@ -1,296 +0,0 @@
import picoModal from 'picomodal'
import Selectr from './assets/selectr/selectr'
import { translate } from './i18n'
import { stringifyPartial } from './jsonUtils'
import { getChildPaths, debounce } from './util'
import { MAX_PREVIEW_CHARACTERS } from './constants'
const DEFAULT_DESCRIPTION =
'Enter a <a href="http://jmespath.org" target="_blank">JMESPath</a> query to filter, sort, or transform the JSON data.<br/>' +
'To learn JMESPath, go to <a href="http://jmespath.org/tutorial.html" target="_blank">the interactive tutorial</a>.'
/**
* Show advanced filter and transform modal using JMESPath
* @param {Object} params
* @property {HTMLElement} container The container where to center
* the modal and create an overlay
* @property {JSON} json The json data to be transformed
* @property {string} [queryDescription] Optional custom description explaining
* the transform functionality
* @property {function} createQuery Function called to create a query
* from the wizard form
* @property {function} executeQuery Execute a query for the preview pane
* @property {function} onTransform Callback invoked with the created
* query as callback
*/
export function showTransformModal (
{
container,
json,
queryDescription = DEFAULT_DESCRIPTION,
createQuery,
executeQuery,
onTransform
}
) {
const value = json
const content = '<label class="pico-modal-contents">' +
'<div class="pico-modal-header">' + translate('transform') + '</div>' +
'<p>' + queryDescription + '</p>' +
'<div class="jsoneditor-jmespath-label">' + translate('transformWizardLabel') + ' </div>' +
'<div id="wizard" class="jsoneditor-jmespath-block jsoneditor-jmespath-wizard">' +
' <table class="jsoneditor-jmespath-wizard-table">' +
' <tbody>' +
' <tr>' +
' <th>' + translate('transformWizardFilter') + '</th>' +
' <td class="jsoneditor-jmespath-filter">' +
' <div class="jsoneditor-inline jsoneditor-jmespath-filter-field" >' +
' <select id="filterField">' +
' </select>' +
' </div>' +
' <div class="jsoneditor-inline jsoneditor-jmespath-filter-relation" >' +
' <select id="filterRelation">' +
' <option value="==">==</option>' +
' <option value="!=">!=</option>' +
' <option value="<">&lt;</option>' +
' <option value="<=">&lt;=</option>' +
' <option value=">">&gt;</option>' +
' <option value=">=">&gt;=</option>' +
' </select>' +
' </div>' +
' <div class="jsoneditor-inline jsoneditor-jmespath-filter-value" >' +
' <input type="text" class="value" placeholder="value..." id="filterValue" />' +
' </div>' +
' </td>' +
' </tr>' +
' <tr>' +
' <th>' + translate('transformWizardSortBy') + '</th>' +
' <td class="jsoneditor-jmespath-filter">' +
' <div class="jsoneditor-inline jsoneditor-jmespath-sort-field">' +
' <select id="sortField">' +
' </select>' +
' </div>' +
' <div class="jsoneditor-inline jsoneditor-jmespath-sort-order" >' +
' <select id="sortOrder">' +
' <option value="asc">Ascending</option>' +
' <option value="desc">Descending</option>' +
' </select>' +
' </div>' +
' </td>' +
' </tr>' +
' <tr id="selectFieldsPart">' +
' <th>' + translate('transformWizardSelectFields') + '</th>' +
' <td class="jsoneditor-jmespath-filter">' +
' <select class="jsoneditor-jmespath-select-fields" id="selectFields" multiple></select>' +
' </td>' +
' </tr>' +
' </tbody>' +
' </table>' +
'</div>' +
'<div class="jsoneditor-jmespath-label">' + translate('transformQueryLabel') + ' </div>' +
'<div class="jsoneditor-jmespath-block">' +
' <textarea id="query" ' +
' rows="4" ' +
' autocomplete="off" ' +
' autocorrect="off" ' +
' autocapitalize="off" ' +
' spellcheck="false"' +
' title="' + translate('transformQueryTitle') + '">[*]</textarea>' +
'</div>' +
'<div class="jsoneditor-jmespath-label">' + translate('transformPreviewLabel') + ' </div>' +
'<div class="jsoneditor-jmespath-block">' +
' <textarea id="preview" ' +
' class="jsoneditor-transform-preview"' +
' readonly> </textarea>' +
'</div>' +
'<div class="jsoneditor-jmespath-block jsoneditor-modal-actions">' +
' <input type="submit" id="ok" value="' + translate('ok') + '" autofocus />' +
'</div>' +
'</div>'
picoModal({
parent: container,
content: content,
overlayClass: 'jsoneditor-modal-overlay',
overlayStyles: {
backgroundColor: 'rgb(1,1,1)',
opacity: 0.3
},
modalClass: 'jsoneditor-modal jsoneditor-modal-transform',
focus: false
})
.afterCreate(modal => {
const elem = modal.modalElem()
const wizard = elem.querySelector('#wizard')
const ok = elem.querySelector('#ok')
const filterField = elem.querySelector('#filterField')
const filterRelation = elem.querySelector('#filterRelation')
const filterValue = elem.querySelector('#filterValue')
const sortField = elem.querySelector('#sortField')
const sortOrder = elem.querySelector('#sortOrder')
const selectFields = elem.querySelector('#selectFields')
const query = elem.querySelector('#query')
const preview = elem.querySelector('#preview')
if (!Array.isArray(value)) {
wizard.style.fontStyle = 'italic'
wizard.innerHTML = '(wizard not available for objects, only for arrays)'
}
const sortablePaths = getChildPaths(json)
sortablePaths.forEach(path => {
const formattedPath = preprocessPath(path)
const filterOption = document.createElement('option')
filterOption.text = formattedPath
filterOption.value = formattedPath
filterField.appendChild(filterOption)
const sortOption = document.createElement('option')
sortOption.text = formattedPath
sortOption.value = formattedPath
sortField.appendChild(sortOption)
})
const selectablePaths = getChildPaths(json, true).filter(path => path !== '')
if (selectablePaths.length > 0) {
selectablePaths.forEach(path => {
const formattedPath = preprocessPath(path)
const option = document.createElement('option')
option.text = formattedPath
option.value = formattedPath
selectFields.appendChild(option)
})
} else {
const selectFieldsPart = elem.querySelector('#selectFieldsPart')
if (selectFieldsPart) {
selectFieldsPart.style.display = 'none'
}
}
const selectrFilterField = new Selectr(filterField, { defaultSelected: false, clearable: true, allowDeselect: true, placeholder: 'field...' })
const selectrFilterRelation = new Selectr(filterRelation, { defaultSelected: false, clearable: true, allowDeselect: true, placeholder: 'compare...' })
const selectrSortField = new Selectr(sortField, { defaultSelected: false, clearable: true, allowDeselect: true, placeholder: 'field...' })
const selectrSortOrder = new Selectr(sortOrder, { defaultSelected: false, clearable: true, allowDeselect: true, placeholder: 'order...' })
const selectrSelectFields = new Selectr(selectFields, { multiple: true, clearable: true, defaultSelected: false, placeholder: 'select fields...' })
selectrFilterField.on('selectr.change', generateQueryFromWizard)
selectrFilterRelation.on('selectr.change', generateQueryFromWizard)
filterValue.oninput = generateQueryFromWizard
selectrSortField.on('selectr.change', generateQueryFromWizard)
selectrSortOrder.on('selectr.change', generateQueryFromWizard)
selectrSelectFields.on('selectr.change', generateQueryFromWizard)
elem.querySelector('.pico-modal-contents').onclick = event => {
// prevent the first clear button (in any select box) from getting
// focus when clicking anywhere in the modal. Only allow clicking links.
if (event.target.nodeName !== 'A') {
event.preventDefault()
}
}
function preprocessPath (path) {
return (path === '')
? '@'
: (path[0] === '.')
? path.slice(1)
: path
}
function updatePreview () {
try {
const transformed = executeQuery(value, query.value)
preview.className = 'jsoneditor-transform-preview'
preview.value = stringifyPartial(transformed, 2, MAX_PREVIEW_CHARACTERS)
ok.disabled = false
} catch (err) {
preview.className = 'jsoneditor-transform-preview jsoneditor-error'
preview.value = err.toString()
ok.disabled = true
}
}
const debouncedUpdatePreview = debounce(updatePreview, 300)
function tryCreateQuery (json, queryOptions) {
try {
query.value = createQuery(json, queryOptions)
ok.disabled = false
debouncedUpdatePreview()
} catch (err) {
const message = 'Error: an error happened when executing "createQuery": ' + (err.message || err.toString())
query.value = ''
ok.disabled = true
preview.className = 'jsoneditor-transform-preview jsoneditor-error'
preview.value = message
}
}
function generateQueryFromWizard () {
const queryOptions = {}
if (filterField.value && filterRelation.value && filterValue.value) {
queryOptions.filter = {
field: filterField.value,
relation: filterRelation.value,
value: filterValue.value
}
}
if (sortField.value && sortOrder.value) {
queryOptions.sort = {
field: sortField.value,
direction: sortOrder.value
}
}
if (selectFields.value) {
const fields = []
for (let i = 0; i < selectFields.options.length; i++) {
if (selectFields.options[i].selected) {
const selectedField = selectFields.options[i].value
fields.push(selectedField)
}
}
queryOptions.projection = {
fields
}
}
tryCreateQuery(json, queryOptions)
}
query.oninput = debouncedUpdatePreview
ok.onclick = event => {
event.preventDefault()
event.stopPropagation()
modal.close()
onTransform(query.value)
}
// initialize with empty query
tryCreateQuery(json, {})
setTimeout(() => {
query.select()
query.focus()
query.selectionStart = 3
query.selectionEnd = 3
})
})
.afterClose(modal => {
modal.destroy()
})
.show()
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +0,0 @@
exports.tryRequireAjv = function () {
try {
return require('ajv')
} catch (err) {
// no problem... when we need Ajv we will throw a neat exception
}
}

View File

@ -1,7 +0,0 @@
exports.tryRequireThemeJsonEditor = function () {
try {
require('./ace/theme-jsoneditor')
} catch (err) {
console.error(err)
}
}

View File

@ -1,25 +0,0 @@
/**
* @typedef {object} QueryOptions
* @property {FilterOptions} [filter]
* @property {SortOptions} [sort]
* @property {ProjectionOptions} [projection]
*/
/**
* @typedef {object} FilterOptions
* @property {string} field
* @property {string} relation Can be '==', '<', etc
* @property {string} value
*/
/**
* @typedef {object} SortOptions
* @property {string} field
* @property {string} direction Can be 'asc' or 'desc'
*/
/**
* @typedef {object} ProjectionOptions
* @property {string[]} fields
*/

File diff suppressed because it is too large Load Diff

View File

@ -1,47 +0,0 @@
import { isPromise, isValidValidationError, stringifyPath } from './util'
/**
* Execute custom validation if configured.
*
* Returns a promise resolving with the custom errors (or an empty array).
*/
export function validateCustom (json, onValidate) {
if (!onValidate) {
return Promise.resolve([])
}
try {
const customValidateResults = onValidate(json)
const resultPromise = isPromise(customValidateResults)
? customValidateResults
: Promise.resolve(customValidateResults)
return resultPromise.then(customValidationPathErrors => {
if (Array.isArray(customValidationPathErrors)) {
return customValidationPathErrors
.filter(error => {
const valid = isValidValidationError(error)
if (!valid) {
console.warn('Ignoring a custom validation error with invalid structure. ' +
'Expected structure: {path: [...], message: "..."}. ' +
'Actual error:', error)
}
return valid
})
.map(error => // change data structure into the structure matching the JSON schema errors
({
dataPath: stringifyPath(error.path),
message: error.message,
type: 'customValidation'
}))
} else {
return []
}
})
} catch (err) {
return Promise.reject(err)
}
}

View File

@ -1,15 +0,0 @@
let VanillaPicker
if (window.Picker) {
// use the already loaded instance of VanillaPicker
VanillaPicker = window.Picker
} else {
try {
// load color picker
VanillaPicker = require('vanilla-picker')
} catch (err) {
// probably running the minimalist bundle
}
}
module.exports = VanillaPicker

14
src/logic/aceJson.mjs Normal file
View File

@ -0,0 +1,14 @@
// load Ace editor
import ace from 'ace-builds/src-noconflict/ace'
// load required Ace plugins
import 'ace-builds/src-noconflict/mode-json'
import 'ace-builds/src-noconflict/ext-searchbox'
// embed Ace json worker
// generated via tools/generateWorkerJSONDataUrl.mjs
// https://github.com/ajaxorg/ace/issues/3913
import jsonWorkerDataUrl from '../generated/worker-json-data-url'
ace.config.setModuleUrl('ace/mode/json_worker', jsonWorkerDataUrl)
export const aceJson = ace

231
src/logic/documentState.js Normal file
View File

@ -0,0 +1,231 @@
import { initial, isEqual, isNumber, last, merge, uniqueId } from 'lodash-es'
import {
DEFAULT_VISIBLE_SECTIONS,
STATE_EXPANDED,
STATE_PROPS,
STATE_VISIBLE_SECTIONS
} from '../constants.js'
import { deleteIn, getIn, insertAt, setIn, updateIn } from '../utils/immutabilityHelpers.js'
import { parseJSONPointer } from '../utils/jsonPointer.js'
import { isObject, isObjectOrArray } from '../utils/typeUtils.js'
import { mergeSections, inVisibleSection, previousRoundNumber, nextRoundNumber } from './expandItemsSections.js'
/**
* Sync a state object with the doc it belongs to: update props, limit, and expanded state
*
* @param {JSON} doc
* @param {JSON | undefined} state
* @param {Path} path
* @param {function (path: Path) : boolean} expand
* @param {boolean} [forceRefresh=false] if true, force refreshing the expanded state
* @returns {JSON | undefined}
*/
export function syncState (doc, state = undefined, path, expand, forceRefresh = false) {
// TODO: this function can be made way more efficient if we pass prevState:
// when immutable, we can simply be done already when the state === prevState
if (isObject(doc)) {
const updatedState = {}
updatedState[STATE_PROPS] = updateProps(doc, state && state[STATE_PROPS])
updatedState[STATE_EXPANDED] = (state && !forceRefresh)
? state[STATE_EXPANDED]
: expand(path)
if (updatedState[STATE_EXPANDED]) {
Object.keys(doc).forEach(key => {
const childDocument = doc[key]
if (isObjectOrArray(childDocument)) {
const childState = state && state[key]
updatedState[key] = syncState(childDocument, childState, path.concat(key), expand, forceRefresh)
}
})
}
return updatedState
}
if (Array.isArray(doc)) {
const updatedState = []
updatedState[STATE_EXPANDED] = (state && !forceRefresh)
? state[STATE_EXPANDED]
: expand(path)
// note that we reset the visible items when the state is not expanded
updatedState[STATE_VISIBLE_SECTIONS] = (state && updatedState[STATE_EXPANDED])
? state[STATE_VISIBLE_SECTIONS]
: DEFAULT_VISIBLE_SECTIONS
if (updatedState[STATE_EXPANDED]) {
updatedState[STATE_VISIBLE_SECTIONS].forEach(({ start, end }) => {
for (let i = start; i < Math.min(doc.length, end); i++) {
const childDocument = doc[i]
if (isObjectOrArray(childDocument)) {
const childState = state && state[i]
updatedState[i] = syncState(childDocument, childState, path.concat(i), expand, forceRefresh)
}
}
})
}
return updatedState
}
// primitive values have no state
return undefined
}
/**
* Expand all nodes on given path
* @param {JSON} state
* @param {Path} path
* @return {JSON} returns the updated state
*/
// TODO: write unit tests for expandPath
export function expandPath (state, path) {
let updatedState = state
for (let i = 1; i < path.length; i++) {
const partialPath = path.slice(0, i)
// FIXME: setIn has to create object first
updatedState = setIn(updatedState, partialPath.concat(STATE_EXPANDED), true, true)
// if needed, enlarge the expanded sections such that the search result becomes visible in the array
const key = path[i]
if (isNumber(key)) {
const sectionsPath = partialPath.concat(STATE_VISIBLE_SECTIONS)
const sections = getIn(updatedState, sectionsPath) || DEFAULT_VISIBLE_SECTIONS
if (!inVisibleSection(sections, key)) {
const start = previousRoundNumber(key)
const end = nextRoundNumber(start)
const newSection = { start, end }
const updatedSections = mergeSections(sections.concat(newSection))
updatedState = setIn(updatedState, sectionsPath, updatedSections)
}
}
}
return updatedState
}
/**
* Expand a section of items in an array
* @param {JSON} state
* @param {Path} path
* @param {Section} section
* @return {JSON} returns the updated state
*/
// TODO: write unit test
export function expandSection (state, path, section) {
return updateIn(state, path.concat(STATE_VISIBLE_SECTIONS), (sections = DEFAULT_VISIBLE_SECTIONS) => {
return mergeSections(sections.concat(section))
})
}
export function updateProps (value, prevProps) {
if (!isObject(value)) {
return undefined
}
// copy the props that still exist
const props = prevProps
? prevProps.filter(item => value[item.key] !== undefined)
: []
// add new props
const prevKeys = new Set(props.map(item => item.key))
Object.keys(value).forEach(key => {
if (!prevKeys.has(key)) {
props.push({
id: uniqueId(),
key
})
}
})
return props
}
// TODO: write unit tests
// TODO: split this function in smaller functions
export function patchProps (state, operations) {
let updatedState = state
operations.map(operation => {
if (operation.op === 'move') {
if (isEqual(
initial(parseJSONPointer(operation.from)),
initial(parseJSONPointer(operation.path))
)) {
// move inside the same object
const pathFrom = parseJSONPointer(operation.from)
const pathTo = parseJSONPointer(operation.path)
const parentPath = initial(pathFrom)
const props = getIn(updatedState, parentPath.concat(STATE_PROPS))
if (props) {
const oldKey = last(pathFrom)
const newKey = last(pathTo)
const oldIndex = props.findIndex(item => item.key === oldKey)
if (oldIndex !== -1) {
if (oldKey !== newKey) {
// A property is renamed.
// in case the new key shadows an existing key, remove the existing key
const newIndex = props.findIndex(item => item.key === newKey)
if (newIndex !== -1) {
const updatedProps = deleteIn(props, [newIndex])
updatedState = setIn(updatedState, parentPath.concat([STATE_PROPS]), updatedProps, true)
}
// Rename the key in the object's props so it maintains its identity and hence its index
updatedState = setIn(updatedState, parentPath.concat([STATE_PROPS, oldIndex, 'key']), newKey, true)
} else {
// operation.from and operation.path are the same:
// property is moved but stays the same -> move it to the end of the props
const oldProp = props[oldIndex]
const updatedProps = insertAt(deleteIn(props, [oldIndex]), [props.length - 1], oldProp)
updatedState = setIn(updatedState, parentPath.concat([STATE_PROPS]), updatedProps, true)
}
}
}
}
}
if (operation.op === 'add' || operation.op === 'copy') {
const pathTo = parseJSONPointer(operation.path)
const parentPath = initial(pathTo)
const key = last(pathTo)
const props = getIn(updatedState, parentPath.concat(STATE_PROPS))
if (props) {
const index = props.findIndex(item => item.key === key)
if (index === -1) {
const newProp = {
id: uniqueId(),
key
}
const updatedProps = insertAt(props, [props.length], newProp)
updatedState = setIn(updatedState, parentPath.concat([STATE_PROPS]), updatedProps, true)
}
}
}
})
return updatedState
}
export function getNextKeys (props, key, includeKey = false) {
if (props) {
const index = props.findIndex(prop => prop.key === key)
if (index !== -1) {
return props.slice(index + (includeKey ? 0 : 1)).map(prop => prop.key)
}
}
return []
}

View File

@ -0,0 +1,65 @@
import assert from 'assert'
import {
DEFAULT_VISIBLE_SECTIONS,
STATE_EXPANDED,
STATE_PROPS,
STATE_VISIBLE_SECTIONS
} from '../constants.js'
import { syncState, updateProps } from './documentState.js'
describe('documentState', () => {
it('syncState', () => {
const document = {
array: [1, 2, { c: 6 }],
object: { a: 4, b: 5 },
value: 'hello'
}
function expand (path) {
return path.length <= 1
}
const state = syncState(document, undefined, [], expand)
const expectedState = {}
expectedState[STATE_EXPANDED] = true
expectedState[STATE_PROPS] = [
{ id: state[STATE_PROPS][0].id, key: 'array' },
{ id: state[STATE_PROPS][1].id, key: 'object' },
{ id: state[STATE_PROPS][2].id, key: 'value' }
]
expectedState.array = []
expectedState.array[STATE_EXPANDED] = true
expectedState.array[STATE_VISIBLE_SECTIONS] = DEFAULT_VISIBLE_SECTIONS
expectedState.array[2] = {}
expectedState.array[2][STATE_EXPANDED] = false
expectedState.array[2][STATE_PROPS] = [
{ id: state.array[2][STATE_PROPS][0].id, key: 'c' } // FIXME: props should not be created because node is not expanded
]
expectedState.object = {}
expectedState.object[STATE_EXPANDED] = true
expectedState.object[STATE_PROPS] = [
{ id: state.object[STATE_PROPS][0].id, key: 'a' },
{ id: state.object[STATE_PROPS][1].id, key: 'b' }
]
assert.deepStrictEqual(state, expectedState)
})
it('updateProps (1)', () => {
const props1 = updateProps({ b: 2 })
assert.deepStrictEqual(props1.map(item => item.key), ['b'])
const props2 = updateProps({ a: 1, b: 2 }, props1)
assert.deepStrictEqual(props2.map(item => item.key), ['b', 'a'])
assert.deepStrictEqual(props2[0], props1[0]) // b must still have the same id
})
it('updateProps (2)', () => {
const props1 = updateProps({ a: 1, b: 2 })
const props2 = updateProps({ a: 1, b: 2 }, props1)
assert.deepStrictEqual(props2, props1)
})
// TODO: write more unit tests
})

View File

@ -0,0 +1,94 @@
import { sortBy } from 'lodash-es'
import { ARRAY_SECTION_SIZE } from '../constants.js'
/**
* Create sections that can be expanded.
* Used to display a button like "Show items 100-200"
*
* @param {number} startIndex
* @param {number} endIndex
* @return {Section[]}
*/
export function getExpandItemsSections (startIndex, endIndex) {
// expand the start of the section
const section1 = {
start: startIndex,
end: Math.min(nextRoundNumber(startIndex), endIndex)
}
// expand the middle of the section
const start2 = Math.max(previousRoundNumber((startIndex + endIndex) / 2), startIndex)
const section2 = {
start: start2,
end: Math.min(nextRoundNumber(start2), endIndex)
}
// expand the end of the section
const section3 = {
start: Math.max(previousRoundNumber(endIndex), startIndex),
end: endIndex
}
const sections = [
section1
]
const showSection2 = section2.start >= section1.end && section2.end <= section3.end
if (showSection2) {
sections.push(section2)
}
const showSection3 = section3.start >= (showSection2 ? section2.end : section1.end)
if (showSection3) {
sections.push(section3)
}
return sections
}
/**
* Sort and merge a list with sections
* @param {Section[]} sections
* @return {Section[]}
*/
export function mergeSections (sections) {
const sortedSections = sortBy(sections, section => section.start)
const mergedSections = [
sortedSections[0]
]
for (let sortedIndex = 0; sortedIndex < sortedSections.length; sortedIndex++) {
const mergedIndex = mergedSections.length - 1
const previous = mergedSections[mergedIndex]
const current = sortedSections[sortedIndex]
if (current.start <= previous.end) {
// there is overlap -> replace the previous item
mergedSections[mergedIndex] = {
start: Math.min(previous.start, current.start),
end: Math.max(previous.end, current.end)
}
} else {
// no overlap, just add the item
mergedSections.push(current)
}
}
return mergedSections
}
// TODO: write unit test
export function inVisibleSection (sections, index) {
return sections.some(section => {
return index >= section.start && index < section.end
})
}
export function nextRoundNumber (index) {
return Math.floor((index + ARRAY_SECTION_SIZE) / ARRAY_SECTION_SIZE) * ARRAY_SECTION_SIZE
}
export function previousRoundNumber (index) {
return Math.ceil((index - ARRAY_SECTION_SIZE) / ARRAY_SECTION_SIZE) * ARRAY_SECTION_SIZE
}

View File

@ -0,0 +1,113 @@
import assert from 'assert'
import {
mergeSections,
getExpandItemsSections,
nextRoundNumber,
previousRoundNumber
} from './expandItemsSections.js'
describe('expandItemsSections', () => {
it('should find the next round number', () => {
assert.strictEqual(nextRoundNumber(5), 100)
assert.strictEqual(nextRoundNumber(99), 100)
assert.strictEqual(nextRoundNumber(100), 200)
})
it('should find the previous round number', () => {
assert.strictEqual(previousRoundNumber(100), 0)
assert.strictEqual(previousRoundNumber(199), 100)
assert.strictEqual(previousRoundNumber(200), 100)
assert.strictEqual(previousRoundNumber(101), 100)
assert.strictEqual(previousRoundNumber(500), 400)
})
it('should calculate expandable sections (start, middle, end)', () => {
assert.deepStrictEqual(getExpandItemsSections(0, 1000), [
{ start: 0, end: 100 },
{ start: 400, end: 500 },
{ start: 900, end: 1000 }
])
assert.deepStrictEqual(getExpandItemsSections(30, 510), [
{ start: 30, end: 100 },
{ start: 200, end: 300 },
{ start: 500, end: 510 }
])
assert.deepStrictEqual(getExpandItemsSections(30, 250), [
{ start: 30, end: 100 },
{ start: 100, end: 200 },
{ start: 200, end: 250 }
])
assert.deepStrictEqual(getExpandItemsSections(30, 200), [
{ start: 30, end: 100 },
{ start: 100, end: 200 }
])
assert.deepStrictEqual(getExpandItemsSections(30, 170), [
{ start: 30, end: 100 },
{ start: 100, end: 170 }
])
assert.deepStrictEqual(getExpandItemsSections(30, 100), [
{ start: 30, end: 100 }
])
assert.deepStrictEqual(getExpandItemsSections(30, 70), [
{ start: 30, end: 70 }
])
})
it('should apply expanding a new piece of selection', () => {
// merge
assert.deepStrictEqual(mergeSections([
{ start: 0, end: 100 },
{ start: 100, end: 200 }
]), [
{ start: 0, end: 200 }
])
// sort correctly
assert.deepStrictEqual(mergeSections([
{ start: 0, end: 100 },
{ start: 400, end: 500 },
{ start: 200, end: 300 }
]), [
{ start: 0, end: 100 },
{ start: 200, end: 300 },
{ start: 400, end: 500 }
])
// merge partial overlapping
assert.deepStrictEqual(mergeSections([
{ start: 0, end: 30 },
{ start: 20, end: 100 }
]), [
{ start: 0, end: 100 }
])
// merge full overlapping
assert.deepStrictEqual(mergeSections([
{ start: 100, end: 200 },
{ start: 0, end: 300 }
]), [
{ start: 0, end: 300 }
])
assert.deepStrictEqual(mergeSections([
{ start: 0, end: 300 },
{ start: 100, end: 200 }
]), [
{ start: 0, end: 300 }
])
// merge overlapping with two
assert.deepStrictEqual(mergeSections([
{ start: 0, end: 100 },
{ start: 200, end: 300 },
{ start: 100, end: 200 }
]), [
{ start: 0, end: 300 }
])
})
})

116
src/logic/history.js Normal file
View File

@ -0,0 +1,116 @@
const MAX_HISTORY_ITEMS = 1000
/**
* @typedef {*} HistoryItem
* @property {Object} undo
* @property {Object} redo
*/
/**
* @param {Object} [options]
* @property {number} [maxItems]
* @property {onChange} [({canUndo: boolean, canRedo: boolean, length: number}) => void]
* @returns {Object}
*/
export function createHistory (options = {}) {
const maxItems = options.maxItems || MAX_HISTORY_ITEMS
/**
* items in history are sorted from newest first to oldest last
* @type {HistoryItem[]}
*/
let items = []
/**
* @type {number}
*/
let index = 0
/**
* @return {boolean}
*/
function canUndo () {
return index < items.length
}
/**
* @return {boolean}
*/
function canRedo () {
return index > 0
}
function getState () {
return {
canUndo: canUndo(),
canRedo: canRedo(),
length: items.length
}
}
function handleChange () {
if (options.onChange) {
options.onChange(getState())
}
}
/**
* @param {HistoryItem} item
*/
function add (item) {
items = [item]
.concat(items.slice(index))
.slice(0, maxItems)
index = 0
handleChange()
}
function clear () {
items = []
index = 0
handleChange()
}
/**
* @return {HistoryItem | undefined}
*/
function undo () {
if (canUndo()) {
const item = items[index]
index += 1
handleChange()
return item
}
return undefined
}
/**
* @return {HistoryItem | undefined}
*/
function redo () {
if (canRedo()) {
index -= 1
handleChange()
return items[index]
}
return undefined
}
return {
add,
clear,
getState,
undo,
redo
}
}

View File

@ -0,0 +1,46 @@
import { last } from 'lodash-es'
export function createQuery (json, queryOptions) {
console.log('createQuery', queryOptions)
const { filter, sort, projection } = queryOptions
const queryParts = []
if (filter) {
// Note that the comparisons embrace type coercion,
// so a filter value like '5' (text) will match numbers like 5 too.
const getActualValue = filter.field.length > 0
? `item => _.get(item, ${JSON.stringify(filter.field)})`
: 'item => item'
queryParts.push(` data = data.filter(${getActualValue} ${filter.relation} '${filter.value}')\n`)
}
if (sort) {
// Empty field array means that the field itself is selected.
// For example when we have an array containing numbers.
if (sort.field.length > 0) {
queryParts.push(` data = _.orderBy(data, ${JSON.stringify(sort.field)}, '${sort.direction}')\n`)
} else {
queryParts.push(` data = _.sortBy(data, '${sort.direction}')\n`)
}
}
if (projection) {
// It is possible to make a util function "pickFlat"
// and use that when building the query to make it more readable.
if (projection.fields.length > 1) {
const fields = projection.fields.map(field => {
const name = last(field)
return ` ${JSON.stringify(name)}: _.get(item, ${JSON.stringify(field)})`
})
queryParts.push(` data = data.map(item => ({\n${fields.join(',\n')}})\n )\n`)
} else {
const field = projection.fields[0]
queryParts.push(` data = data.map(item => _.get(item, ${JSON.stringify(field)}))\n`)
}
}
queryParts.push(' return data\n')
return `function query (data) {\n${queryParts.join('')}}`
}

333
src/logic/operations.js Normal file
View File

@ -0,0 +1,333 @@
import { first, initial, isEmpty, last, pickBy, cloneDeepWith } from 'lodash-es'
import { STATE_PROPS } from '../constants.js'
import { getNextKeys } from '../logic/documentState.js'
import { getParentPath } from '../logic/selection.js'
import { getIn } from '../utils/immutabilityHelpers.js'
import { compileJSONPointer } from '../utils/jsonPointer.js'
import { findUniqueName } from '../utils/stringUtils.js'
import { isObject } from '../utils/typeUtils.js'
/**
* Create a JSONPatch for an insert operation.
*
* This function needs the current data in order to be able to determine
* a unique property name for the inserted node in case of duplicating
* and object property
*
* @param {JSON} json
* @param {Path} path
* @param {Array.<{key?: string, value: JSON}>} values
* @param {string[]} nextKeys A list with all keys *after* the renamed key,
* these keys will be moved down, so the renamed
* key will maintain it's position above these keys
* @return {JSONPatchDocument}
*/
export function insertBefore (json, path, values, nextKeys) { // TODO: find a better name and define datastructure for values
const parentPath = initial(path)
const parent = getIn(json, parentPath)
if (Array.isArray(parent)) {
const offset = parseInt(last(path), 10)
return values.map((entry, index) => ({
op: 'add',
path: compileJSONPointer(parentPath.concat(offset + index)),
value: entry.value
}))
} else { // 'object'
return [
// insert new values
...values.map(entry => {
const newProp = findUniqueName(entry.key, parent)
return {
op: 'add',
path: compileJSONPointer(parentPath.concat(newProp)),
value: entry.value
}
}),
// move all lower down keys so the inserted key will maintain it's position
...nextKeys.map(key => moveDown(parentPath, key))
]
}
}
/**
* Create a JSONPatch for an append operation. The values will be appended
* to the end of the array or object.
*
* This function needs the current data in order to be able to determine
* a unique property name for the inserted node in case of duplicating
* and object property
*
* @param {JSON} json
* @param {Path} path
* @param {Array.<{key?: string, value: JSON}>} values
* @return {JSONPatchDocument}
*/
export function append (json, path, values) { // TODO: find a better name and define datastructure for values
const parent = getIn(json, path)
if (Array.isArray(parent)) {
const offset = parent.length
return values.map((entry, index) => ({
op: 'add',
path: compileJSONPointer(path.concat(offset + index)),
value: entry.value
}))
} else { // 'object'
return values.map(entry => {
const newProp = findUniqueName(entry.key, parent)
return {
op: 'add',
path: compileJSONPointer(path.concat(newProp)),
value: entry.value
}
})
}
}
/**
* Rename an object key
* Not applicable to arrays
*
* @param {Path} parentPath
* @param {string} oldKey
* @param {string} newKey
* @param {string[]} nextKeys A list with all keys *after* the renamed key,
* these keys will be moved down, so the renamed
* key will maintain it's position above these keys
* @returns {JSONPatchDocument}
*/
export function rename (parentPath, oldKey, newKey, nextKeys) {
return [
// rename a key
{
op: 'move',
from: compileJSONPointer(parentPath.concat(oldKey)),
path: compileJSONPointer(parentPath.concat(newKey))
},
// move all lower down keys so the renamed key will maintain it's position
...nextKeys.map(key => moveDown(parentPath, key))
]
}
/**
* Create a JSONPatch for an insert operation.
*
* This function needs the current data in order to be able to determine
* a unique property name for the inserted node in case of duplicating
* and object property
*
* @param {JSON} json
* @param {Path[]} paths
* @param {Array.<{key?: string, value: JSON}>} values
* @param {string[]} nextKeys A list with all keys *after* the renamed key,
* these keys will be moved down, so the renamed
* key will maintain it's position above these keys
* @return {JSONPatchDocument}
*/
export function replace (json, paths, values, nextKeys) { // TODO: find a better name and define datastructure for values
const firstPath = first(paths)
const parentPath = initial(firstPath)
const parent = getIn(json, parentPath)
if (Array.isArray(parent)) {
const firstPath = first(paths)
const offset = firstPath ? parseInt(last(firstPath), 10) : 0
return [
// remove operations
...removeAll(paths),
// insert operations
values.map((entry, index) => ({
op: 'add',
path: compileJSONPointer(parentPath.concat(index + offset)),
value: entry.value
}))
]
} else { // parent is Object
// if we're going to replace an existing object with key "a" with a new
// key "a", we must not create a new unique name "a (copy)".
const removeKeys = new Set(paths.map(path => last(path)))
const parentWithoutRemovedKeys = pickBy(parent, (value, key) => !removeKeys.has(key))
return [
// remove operations
...removeAll(paths),
// insert operations
...values.map(entry => {
const newProp = findUniqueName(entry.key, parentWithoutRemovedKeys)
return {
op: 'add',
path: compileJSONPointer(parentPath.concat(newProp)),
value: entry.value
}
}),
// move down operations
// move all lower down keys so the renamed key will maintain it's position
...nextKeys.map(key => moveDown(parentPath, key))
]
}
}
/**
* Create a JSONPatch for a duplicate action.
*
* This function needs the current data in order to be able to determine
* a unique property name for the duplicated node in case of duplicating
* and object property
*
* @param {JSON} json
* @param {JSON} doc
* @param {Path[]} paths
* @return {JSONPatchDocument}
*/
export function duplicate (doc, state, paths) {
// FIXME: here we assume selection.paths is sorted correctly, that's a dangerous assumption
const lastPath = last(paths)
const parentPath = initial(lastPath)
const beforeKey = last(lastPath)
const props = getIn(state, parentPath.concat(STATE_PROPS))
const nextKeys = getNextKeys(props, beforeKey, false)
const parent = getIn(doc, parentPath)
if (Array.isArray(parent)) {
const lastPath = last(paths)
const offset = lastPath ? (parseInt(last(lastPath), 10) + 1) : 0
return [
// copy operations
...paths.map((path, index) => ({
op: 'copy',
from: compileJSONPointer(path),
path: compileJSONPointer(parentPath.concat(index + offset))
})),
// move down operations
// move all lower down keys so the renamed key will maintain it's position
...nextKeys.map(key => moveDown(parentPath, key))
]
} else { // 'object'
return [
// copy operations
...paths.map(path => {
const prop = last(path)
const newProp = findUniqueName(prop, parent)
return {
op: 'copy',
from: compileJSONPointer(path),
path: compileJSONPointer(parentPath.concat(newProp))
}
}),
// move down operations
// move all lower down keys so the renamed key will maintain it's position
...nextKeys.map(key => moveDown(parentPath, key))
]
}
}
export function insert (doc, state, selection, values) {
if (selection.beforePath) {
const parentPath = initial(selection.beforePath)
const beforeKey = last(selection.beforePath)
const props = getIn(state, parentPath.concat(STATE_PROPS))
const nextKeys = getNextKeys(props, beforeKey, true)
const operations = insertBefore(doc, selection.beforePath, values, nextKeys)
// TODO: move calculation of nextKeys inside insertBefore?
return operations
} else if (selection.appendPath) {
const operations = append(doc, selection.appendPath, values)
return operations
} else if (selection.paths) {
const lastPath = last(selection.paths) // FIXME: here we assume selection.paths is sorted correctly, that's a dangerous assumption
const parentPath = initial(lastPath)
const beforeKey = last(lastPath)
const props = getIn(state, parentPath.concat(STATE_PROPS))
const nextKeys = getNextKeys(props, beforeKey, true)
const operations = replace(doc, selection.paths, values, nextKeys)
// TODO: move calculation of nextKeys inside replace?
return operations
}
}
export function createNewValue (doc, selection, type) {
switch (type) {
case 'value':
return ''
case 'object':
return {}
case 'array':
return []
case 'structure': {
const parentPath = getParentPath(selection)
const parent = getIn(doc, parentPath)
if (Array.isArray(parent) && !isEmpty(parent)) {
const jsonExample = first(parent)
const structure = cloneDeepWith(jsonExample, (value) => {
return Array.isArray(value)
? []
: isObject(value)
? undefined // leave object as is, will recurse into it
: ''
})
return structure
} else {
// no example structure
return ''
}
}
default:
return ''
}
}
/**
* Create a JSONPatch for a remove operation
* @param {Path} path
* @return {JSONPatchDocument}
*/
export function remove (path) {
return [{
op: 'remove',
path: compileJSONPointer(path)
}]
}
/**
* Create a JSONPatch for a multiple remove operation
* @param {Path[]} paths
* @return {JSONPatchDocument}
*/
export function removeAll (paths) {
return paths
.map(path => ({
op: 'remove',
path: compileJSONPointer(path)
}))
.reverse() // reverse is needed for arrays: delete the last index first
}
// helper function to move a key down in an object,
// so another key can get positioned before the moved down keys
function moveDown (parentPath, key) {
return {
op: 'move',
from: compileJSONPointer(parentPath.concat(key)),
path: compileJSONPointer(parentPath.concat(key))
}
}

View File

@ -0,0 +1,42 @@
import assert from 'assert'
import { createNewValue } from './operations.js'
describe('operations', () => {
describe('createNewValue', () => {
it('should create a value of type "value"', () => {
assert.strictEqual(createNewValue({}, null, 'value'), '')
})
it('should create a value of type "object"', () => {
assert.deepStrictEqual(createNewValue({}, null, 'object'), {})
})
it('should create a value of type "array"', () => {
assert.deepStrictEqual(createNewValue({}, null, 'array'), [])
})
it('should create a simple value via type "structure"', () => {
assert.deepStrictEqual(createNewValue([1, 2, 3], { paths: [[0]] }, 'structure'), '')
})
it('should create a nested object via type "structure"', () => {
const doc = [
{
a: 2,
b: {
c: 3
},
d: [1, 2, 3]
}
]
assert.deepStrictEqual(createNewValue(doc, { paths: [[0]] }, 'structure'), {
a: '',
b: {
c: ''
},
d: []
})
})
})
})

215
src/logic/search.js Normal file
View File

@ -0,0 +1,215 @@
import { isEqual, initial } from 'lodash-es'
import { STATE_SEARCH_PROPERTY, STATE_SEARCH_VALUE } from '../constants.js'
import { existsIn, getIn, setIn } from '../utils/immutabilityHelpers.js'
import { valueType } from '../utils/typeUtils.js'
/**
* @typedef {Object} SearchResult
* @property {Object} items
* @property {Object} itemsWithActive
* @property {Path[]} flatItems
* @property {Path} activeItem
* @property {number} activeIndex
* @property {number} count
*/
// TODO: comment
export function updateSearchResult (doc, flatResults, previousResult) {
const flatItems = flatResults
const items = createRecursiveSearchResults(doc, flatItems)
const activeItem = (previousResult && previousResult.activeItem &&
existsIn(items, previousResult.activeItem))
? previousResult.activeItem
: flatItems[0]
const activeIndex = flatItems.findIndex(item => isEqual(item, activeItem))
const itemsWithActive = (items && activeItem && activeIndex !== -1)
? setIn(items, activeItem, 'search active')
: items
return {
items,
itemsWithActive,
flatItems,
count: flatItems.length,
activeItem,
activeIndex: activeIndex
}
}
// TODO: comment
export function createRecursiveSearchResults (referenceDoc, flatResults) {
// TODO: smart update result based on previous results to make the results immutable when there is no actual change
let result = {}
flatResults.forEach(path => {
const parentPath = initial(path)
if (!existsIn(result, parentPath)) {
const item = getIn(referenceDoc, parentPath)
result = setIn(result, parentPath, Array.isArray(item) ? [] : {}, true)
}
result = setIn(result, path, 'search')
})
return result
}
/**
* @param {SearchResult} searchResult
* @return {SearchResult}
*/
export function searchNext (searchResult) {
const nextActiveIndex = searchResult.activeIndex < searchResult.flatItems.length - 1
? searchResult.activeIndex + 1
: 0
const nextActiveItem = searchResult.flatItems[nextActiveIndex]
const itemsWithActive = nextActiveItem
? setIn(searchResult.items, nextActiveItem, 'search active', true)
: searchResult.items
return {
...searchResult,
itemsWithActive,
activeItem: nextActiveItem,
activeIndex: nextActiveIndex
}
}
/**
* @param {SearchResult} searchResult
* @return {SearchResult}
*/
export function searchPrevious (searchResult) {
const previousActiveIndex = searchResult.activeIndex > 0
? searchResult.activeIndex - 1
: searchResult.flatItems.length - 1
const previousActiveItem = searchResult.flatItems[previousActiveIndex]
const itemsWithActive = previousActiveItem
? setIn(searchResult.items, previousActiveItem, 'search active', true)
: searchResult.items
return {
...searchResult,
itemsWithActive,
activeItem: previousActiveItem,
activeIndex: previousActiveIndex
}
}
async function tick () {
return new Promise(setTimeout)
}
// TODO: comment
export function searchAsync (searchText, doc, { onProgress, onDone, maxResults = Infinity, yieldAfterItemCount = 10_000 }) {
// TODO: what is a good value for yieldAfterItemCount? (larger means faster results but also less responsive during search)
const search = searchGenerator(searchText, doc, yieldAfterItemCount)
// TODO: implement pause after having found x results (like 999)?
let cancelled = false
const results = []
let newResults = false
async function executeSearch () {
if (!searchText || searchText === '') {
onDone(results)
return
}
let next
do {
next = search.next()
if (next.value) {
if (results.length < maxResults) {
results.push(next.value) // TODO: make this immutable?
newResults = true
} else {
// max results limit reached
cancelled = true
onDone(results)
}
} else {
// time for a small break, give the browser space to do stuff
if (newResults) {
newResults = false
onProgress(results)
}
await tick()
}
// eslint-disable-next-line no-unmodified-loop-condition
} while (!cancelled && !next.done)
if (next.done) {
onDone(results)
} // else: cancelled
}
// start searching on the next tick
setTimeout(executeSearch)
return {
cancel: () => {
cancelled = true
}
}
}
// TODO: comment
export function * searchGenerator (searchText, doc, yieldAfterItemCount = undefined) {
let count = 0
function * incrementCounter () {
count++
if (typeof yieldAfterItemCount === 'number' && count % yieldAfterItemCount === 0) {
// pause every x items
yield null
}
}
function * searchRecursiveAsync (searchText, doc, path) {
const type = valueType(doc)
if (type === 'array') {
for (let i = 0; i < doc.length; i++) {
yield * searchRecursiveAsync(searchText, doc[i], path.concat([i]))
}
} else if (type === 'object') {
for (const prop of Object.keys(doc)) {
if (typeof prop === 'string' && containsCaseInsensitive(prop, searchText)) {
yield path.concat([prop, STATE_SEARCH_PROPERTY])
}
yield * incrementCounter()
yield * searchRecursiveAsync(searchText, doc[prop], path.concat([prop]))
}
} else { // type is a value
if (containsCaseInsensitive(doc, searchText)) {
yield path.concat([STATE_SEARCH_VALUE])
}
yield * incrementCounter()
}
}
return yield * searchRecursiveAsync(searchText, doc, [])
}
/**
* Do a case insensitive search for a search text in a text
* @param {String} text
* @param {String} searchText
* @return {boolean} Returns true if `search` is found in `text`
*/
export function containsCaseInsensitive (text, searchText) {
return String(text).toLowerCase().indexOf(searchText.toLowerCase()) !== -1
}

152
src/logic/search.test.js Normal file
View File

@ -0,0 +1,152 @@
import assert from 'assert'
import { times } from 'lodash-es'
import { searchAsync, searchGenerator, createRecursiveSearchResults } from './search.js'
import { STATE_SEARCH_PROPERTY, STATE_SEARCH_VALUE } from '../constants.js'
describe('search', () => {
it('should search with generator', () => {
const doc = {
b: { c: 'a' },
a: [
{ a: 'b', c: 'a' },
'e',
'a'
]
}
const search = searchGenerator('a', doc)
assert.deepStrictEqual(search.next(), { done: false, value: ['b', 'c', STATE_SEARCH_VALUE] })
assert.deepStrictEqual(search.next(), { done: false, value: ['a', STATE_SEARCH_PROPERTY] })
assert.deepStrictEqual(search.next(), { done: false, value: ['a', 0, 'a', STATE_SEARCH_PROPERTY] })
assert.deepStrictEqual(search.next(), { done: false, value: ['a', 0, 'c', STATE_SEARCH_VALUE] })
assert.deepStrictEqual(search.next(), { done: false, value: ['a', 2, STATE_SEARCH_VALUE] })
assert.deepStrictEqual(search.next(), { done: true, value: undefined })
})
it('should yield every x items during search', () => {
const doc = times(30, index => String(index))
const search = searchGenerator('4', doc, 10)
assert.deepStrictEqual(search.next(), { done: false, value: [4, STATE_SEARCH_VALUE] })
assert.deepStrictEqual(search.next(), { done: false, value: null }) // at 10
assert.deepStrictEqual(search.next(), { done: false, value: [14, STATE_SEARCH_VALUE] })
assert.deepStrictEqual(search.next(), { done: false, value: null }) // at 20
assert.deepStrictEqual(search.next(), { done: false, value: [24, STATE_SEARCH_VALUE] })
assert.deepStrictEqual(search.next(), { done: false, value: null }) // at 30
assert.deepStrictEqual(search.next(), { done: true, value: undefined })
})
it('should search async', (done) => {
const doc = times(30, index => String(index))
const callbacks = []
function onProgress (results) {
callbacks.push(results.slice(0))
}
function onDone (results) {
assert.deepStrictEqual(results, [
[4, STATE_SEARCH_VALUE],
[14, STATE_SEARCH_VALUE],
[24, STATE_SEARCH_VALUE]
])
assert.deepStrictEqual(callbacks, [
results.slice(0, 1),
results.slice(0, 2),
results.slice(0, 3)
])
done()
}
const yieldAfterItemCount = 1
searchAsync('4', doc, { onProgress, onDone, yieldAfterItemCount })
// should not have results right after creation, but only on the first next tick
assert.deepStrictEqual(callbacks, [])
})
it('should cancel async search', (done) => {
const doc = times(30, index => String(index))
const callbacks = []
function onProgress (results) {
callbacks.push(results.slice(0))
}
function onDone () {
throw new Error('onDone should not be invoked')
}
const yieldAfterItemCount = 1 // very low so we can see whether actually cancelled
const { cancel } = searchAsync('4', doc, { onProgress, onDone, yieldAfterItemCount })
setTimeout(() => {
cancel()
setTimeout(() => {
assert.deepStrictEqual(callbacks, [])
done()
}, 100) // FIXME: this is tricky, relying on a delay to test whether actually cancelled
})
})
it('should limit async search results', (done) => {
const doc = times(30, index => 'item ' + index)
function onDone (results) {
assert.strictEqual(results.length, 10)
done()
}
const maxResults = 10
searchAsync('item', doc, { onDone, maxResults })
})
it('should generate recursive search results from flat results', () => {
// Based on document:
const doc = {
b: { c: 'a' },
a: [
{ a: 'b', c: 'a' },
'e',
'a'
]
}
// search results for 'a':
const flatResults = [
['b', 'c', STATE_SEARCH_VALUE],
['a', STATE_SEARCH_PROPERTY], // This is a tricky one: we can't guarantee creating a as Array without having the reference document
['a', 0, 'a', STATE_SEARCH_PROPERTY],
['a', 0, 'c', STATE_SEARCH_VALUE],
['a', 2, STATE_SEARCH_VALUE]
]
const actual = createRecursiveSearchResults(doc, flatResults)
const expected = {}
expected.b = {}
expected.b.c = {}
expected.b.c[STATE_SEARCH_VALUE] = 'search'
expected.a = []
expected.a[STATE_SEARCH_PROPERTY] = 'search'
expected.a[0] = {}
expected.a[0].a = {}
expected.a[0].a[STATE_SEARCH_PROPERTY] = 'search'
expected.a[0].c = {}
expected.a[0].c[STATE_SEARCH_VALUE] = 'search'
expected.a[2] = {}
expected.a[2][STATE_SEARCH_VALUE] = 'search'
assert.deepStrictEqual(actual, expected)
})
// TODO: test searchNext
// TODO: test searchPrevious
})

142
src/logic/selection.js Normal file
View File

@ -0,0 +1,142 @@
import { first, initial, isEqual } from 'lodash-es'
import { STATE_PROPS } from '../constants.js'
import { getIn } from '../utils/immutabilityHelpers.js'
import { compileJSONPointer, parseJSONPointer } from '../utils/jsonPointer.js'
import { isObject } from '../utils/typeUtils.js'
/**
* Expand a selection start and end into an array containing all paths
* between (and including) start and end
*
* @param {JSON} doc
* @param {JSON} state
* @param {Path} anchorPath
* @param {Path} focusPath
* @return {Path[]} paths
*/
export function expandSelection (doc, state, anchorPath, focusPath) {
if (isEqual(anchorPath, focusPath)) {
// just a single node
return [anchorPath]
} else {
// multiple nodes
const sharedPath = findSharedPath(anchorPath, focusPath)
if (anchorPath.length === sharedPath.length || focusPath.length === sharedPath.length) {
// a parent and a child, like ['arr', 1] and ['arr']
return [sharedPath]
}
const anchorKey = anchorPath[sharedPath.length]
const focusKey = focusPath[sharedPath.length]
const value = getIn(doc, sharedPath)
if (isObject(value)) {
const props = getIn(state, sharedPath.concat(STATE_PROPS))
const anchorIndex = props.findIndex(prop => prop.key === anchorKey)
const focusIndex = props.findIndex(prop => prop.key === focusKey)
if (anchorIndex !== -1 && focusIndex !== -1) {
const startIndex = Math.min(anchorIndex, focusIndex)
const endIndex = Math.max(anchorIndex, focusIndex)
const paths = []
for (let i = startIndex; i <= endIndex; i++) {
paths.push(sharedPath.concat(props[i].key))
}
return paths
}
}
if (Array.isArray(value)) {
const startIndex = Math.min(anchorKey, focusKey)
const endIndex = Math.max(anchorKey, focusKey)
const paths = []
for (let i = startIndex; i <= endIndex; i++) {
paths.push(sharedPath.concat(i))
}
return paths
}
}
throw new Error('Failed to create selection')
}
/**
* @param {Selection} selection
* @return {Path} Returns parent path
*/
export function getParentPath (selection) {
if (selection.beforePath) {
return initial(selection.beforePath)
}
if (selection.appendPath) {
return selection.appendPath
}
if (selection.paths) {
const firstPath = first(selection.paths)
return initial(firstPath)
}
}
/**
* @param {JSONPatchDocument} operations
* @returns {MultiSelection}
*/
export function createSelectionFromOperations (operations) {
const paths = operations
.filter(operation => operation.op === 'add' || operation.op === 'copy')
.map(operation => parseJSONPointer(operation.path))
return {
paths,
pathsMap: createPathsMap(paths)
}
}
/**
* @param {Path[]} paths
* @returns {Object}
*/
export function createPathsMap (paths) {
const pathsMap = {}
paths.forEach(path => {
pathsMap[compileJSONPointer(path)] = true
})
return pathsMap
}
/**
* Find the common path of two paths.
* For example findCommonRoot(['arr', '1', 'name'], ['arr', '1', 'address', 'contact']) returns ['arr', '1']
* @param {Path} path1
* @param {Path} path2
* @return {Path}
*/
// TODO: write unit tests for findSharedPath
export function findSharedPath (path1, path2) {
let i = 0
while (i < path1.length && path1[i] === path2[i]) {
i++
}
return path1.slice(0, i)
}
/**
* @param {Selection} selection
*/
export function findRootPath (selection) {
return selection && selection.paths
? selection.paths.length > 1
? initial(first(selection.paths)) // the parent path of the paths
: first(selection.paths) // the first and only path
: []
}

View File

@ -0,0 +1,93 @@
import assert from 'assert'
import { expandSelection, getParentPath, findRootPath } from './selection.js'
import { syncState } from './documentState.js'
describe('selection', () => {
const doc = {
obj: {
arr: [1, 2, { first: 3, last: 4 }]
},
str: 'hello world',
nill: null,
bool: false
}
const state = syncState(doc, undefined, [], () => true)
it('should expand a selection (object)', () => {
const start = ['obj', 'arr', '2', 'last']
const end = ['nill']
const actual = expandSelection(doc, state, start, end)
assert.deepStrictEqual(actual, [
['obj'],
['str'],
['nill']
])
})
it('should expand a selection (array)', () => {
const start = ['obj', 'arr', 1]
const end = ['obj', 'arr', 0] // note the "wrong" order of start and end
const actual = expandSelection(doc, state, start, end)
assert.deepStrictEqual(actual, [
['obj', 'arr', 0],
['obj', 'arr', 1]
])
})
it('should expand a selection (array) (2)', () => {
const start = ['obj', 'arr', 1] // child
const end = ['obj', 'arr'] // parent
const actual = expandSelection(doc, state, start, end)
assert.deepStrictEqual(actual, [
['obj', 'arr']
])
})
it('should expand a selection (value)', () => {
const start = ['obj', 'arr', 2, 'first']
const end = ['obj', 'arr', 2, 'first']
const actual = expandSelection(doc, state, start, end)
assert.deepStrictEqual(actual, [
['obj', 'arr', 2, 'first']
])
})
it('should expand a selection (value)', () => {
const start = ['obj', 'arr']
const end = ['obj', 'arr']
const actual = expandSelection(doc, state, start, end)
assert.deepStrictEqual(actual, [
['obj', 'arr']
])
})
it('should get parent path from a selection', () => {
assert.deepStrictEqual(getParentPath({ beforePath: ['a', 'b'] }), ['a'])
assert.deepStrictEqual(getParentPath({ appendPath: ['a', 'b'] }), ['a', 'b'])
assert.deepStrictEqual(getParentPath({
paths: [
['a', 'b'],
['a', 'c'],
['a', 'd']
]
}), ['a'])
})
it('should find the root path from a selection', () => {
assert.deepStrictEqual(findRootPath({
paths: [
['a', 'b'],
['a', 'c'],
['a', 'd']
]
}), ['a'])
assert.deepStrictEqual(findRootPath({ beforePath: ['a', 'b'] }), [])
assert.deepStrictEqual(findRootPath({ appendPath: ['a', 'b'] }), [])
assert.deepStrictEqual(findRootPath(), [])
})
})

128
src/logic/sort.js Normal file
View File

@ -0,0 +1,128 @@
import naturalCompare from 'natural-compare-lite'
import { getIn } from '../utils/immutabilityHelpers.js'
import { compileJSONPointer } from '../utils/jsonPointer.js'
function caseInsensitiveNaturalCompare (a, b) {
const aLower = typeof a === 'string' ? a.toLowerCase() : a
const bLower = typeof b === 'string' ? b.toLowerCase() : b
return naturalCompare(aLower, bLower)
}
/**
* Sort the keys of an object
* @param {Object} object The object to be sorted
* @param {Path} [rootPath=[]] Relative path when the array was located
* @param {1 | -1} [direction=1] Pass 1 to sort ascending, -1 to sort descending
* @return {JSONPatchDocument} Returns a JSONPatch document with move operation
* to get the array sorted.
*/
export function sortObjectKeys (object, rootPath = [], direction = 1) {
const keys = Object.keys(object)
const sortedKeys = keys.slice()
sortedKeys.sort((keyA, keyB) => {
return direction * caseInsensitiveNaturalCompare(keyA, keyB)
})
// TODO: can we make this more efficient? check if the first couple of keys are already in order and if so ignore them
const operations = []
for (let i = 0; i < sortedKeys.length; i++) {
const key = sortedKeys[i]
const path = compileJSONPointer(rootPath.concat(key))
operations.push({
op: 'move',
from: path,
path
})
}
return operations
}
/**
* Sort the items of an array
* @param {Array} array The array to be sorted
* @param {Path} [rootPath=[]] Relative path when the array was located
* @param {Path} [propertyPath=[]] Nested path to the property on which to sort the contents
* @param {1 | -1} [direction=1] Pass 1 to sort ascending, -1 to sort descending
* @return {JSONPatchDocument} Returns a JSONPatch document with move operation
* to get the array sorted.
*/
export function sortArray (array, rootPath = [], propertyPath = [], direction = 1) {
const comparator = createObjectComparator(propertyPath, direction)
return getSortingMoves(array, comparator).map(({ fromIndex, toIndex }) => {
return {
op: 'move',
from: compileJSONPointer(rootPath.concat(fromIndex)),
path: compileJSONPointer(rootPath.concat(toIndex))
}
})
}
/**
* Create a comparator function to compare nested properties in an array
* @param {Path} propertyPath
* @param {1 | -1} direction
*/
function createObjectComparator (propertyPath, direction) {
return function comparator (a, b) {
const valueA = getIn(a, propertyPath)
const valueB = getIn(b, propertyPath)
if (valueA === undefined) {
return direction
}
if (valueB === undefined) {
return -direction
}
if (typeof valueA !== 'string' && typeof valueB !== 'string') {
// both values are a number, boolean, or null -> use simple, fast sorting
return valueA > valueB
? direction
: valueA < valueB
? -direction
: 0
}
return direction * caseInsensitiveNaturalCompare(valueA, valueB)
}
}
/**
* Create an array containing all move operations
* needed to sort the array contents.
* @param {Array} array
* @param {function (a, b) => number} comparator
* @param {Array.<{fromIndex: number, toIndex: number}>}
*/
export function getSortingMoves (array, comparator) {
const operations = []
const sorted = []
// TODO: rewrite the function to pass a callback instead of returning an array?
for (let i = 0; i < array.length; i++) {
// TODO: implement a faster way to sort. Something with longest increasing subsequence?
// TODO: can we simplify the following code?
const item = array[i]
if (i > 0 && comparator(sorted[i - 1], item) > 0) {
let j = i - 1
while (j > 0 && comparator(sorted[j - 1], item) > 0) {
j--
}
operations.push({
fromIndex: i,
toIndex: j
})
sorted.splice(j, 0, item)
} else {
sorted.push(item)
}
}
return operations
}

Some files were not shown because too many files have changed in this diff Show More