Compare commits
171 Commits
Author | SHA1 | Date |
---|---|---|
Jos de Jong | 8cec2df3c7 | |
Jos de Jong | 002453cc92 | |
Jos de Jong | cc99284052 | |
Jos de Jong | adda5836af | |
Jos de Jong | 3f9a7def75 | |
Jos de Jong | a20359c6a8 | |
Jos de Jong | df853f6370 | |
Jos de Jong | b87286e002 | |
Jos de Jong | d88936b9dc | |
Jos de Jong | 6fb6bc68e6 | |
Jos de Jong | fd673a4ea0 | |
Jos de Jong | ac078e445b | |
Jos de Jong | 34534bba0c | |
Jos de Jong | 1bf3f7e1c2 | |
Jos de Jong | 78411371e6 | |
Jos de Jong | 5033194313 | |
Jos de Jong | 08d49388ed | |
Jos de Jong | 22f429e01b | |
Jos de Jong | d67fffc7d5 | |
Jos de Jong | 45f8881e96 | |
Jos de Jong | 023c57f492 | |
Jos de Jong | d3c3d47ec0 | |
Jos de Jong | 01579dc115 | |
Jos de Jong | 350969638c | |
Jos de Jong | 00d0099355 | |
Jos de Jong | ad35e6fc16 | |
Jos de Jong | 3cf43de54c | |
Jos de Jong | a24045cb42 | |
Jos de Jong | a28335fe57 | |
Jos de Jong | 763b0b7c3c | |
Jos de Jong | dff1fc0811 | |
Jos de Jong | a848358815 | |
Jos de Jong | ec581ad674 | |
Jos de Jong | aeb3374406 | |
Jos de Jong | 82c81e7b4c | |
Jos de Jong | 41d5ab5786 | |
Jos de Jong | 058cf6e104 | |
Jos de Jong | 89eaf2f264 | |
Jos de Jong | 456f1912fd | |
Jos de Jong | bf4d0907f0 | |
Jos de Jong | 1ad834cf31 | |
Jos de Jong | 37fcdf85e2 | |
Jos de Jong | 8256bd637c | |
Jos de Jong | 8b99527604 | |
Jos de Jong | f2b2769727 | |
Jos de Jong | baa336981f | |
Jos de Jong | a3c0da7d95 | |
Jos de Jong | 735702ba99 | |
Jos de Jong | 1477aaaa4b | |
Jos de Jong | 22845a0cc8 | |
Jos de Jong | e36ba7a384 | |
Jos de Jong | a29c196733 | |
Jos de Jong | bb2dcf0039 | |
Jos de Jong | 98f7cacb55 | |
Jos de Jong | b30aac082b | |
Jos de Jong | 9356f44c95 | |
Jos de Jong | 300e46b149 | |
Jos de Jong | a830dab67f | |
Jos de Jong | a54e5b6f08 | |
Jos de Jong | 0c418ac846 | |
Jos de Jong | eea6e09bd8 | |
Jos de Jong | a6bb790f5e | |
Jos de Jong | c441663528 | |
Jos de Jong | b99d4b4d5d | |
Jos de Jong | e629b404a6 | |
Jos de Jong | e23e4a82dd | |
Jos de Jong | ad3ac339cf | |
Jos de Jong | bd73739343 | |
Jos de Jong | 23067b4638 | |
Jos de Jong | ad4572d21e | |
Jos de Jong | b9ceec09e3 | |
Jos de Jong | 2961e0d910 | |
Jos de Jong | da2f912d6d | |
Jos de Jong | 7d67ecc4bc | |
Jos de Jong | be87b1e4cd | |
Jos de Jong | 35983df136 | |
Jos de Jong | 9ac6ca95c4 | |
Jos de Jong | 401a6e19fd | |
Jos de Jong | ce8cf93170 | |
Jos de Jong | 174f1194ef | |
Jos de Jong | f3459430b0 | |
Jos de Jong | 3919585db4 | |
Jos de Jong | 404f623215 | |
Jos de Jong | c773e5bfa9 | |
Jos de Jong | 7ad262a7b1 | |
Jos de Jong | 89fc4070a2 | |
Jos de Jong | fb3a9cdf36 | |
Jos de Jong | bdc9f3ba34 | |
Jos de Jong | 65c38f7b05 | |
Jos de Jong | 2fb95c3951 | |
Jos de Jong | 2a7d4828cb | |
Jos de Jong | 0e5dabed89 | |
Jos de Jong | 16d3092670 | |
Jos de Jong | 2f393e5948 | |
Jos de Jong | e44284df90 | |
Jos de Jong | bbf0543d85 | |
Jos de Jong | fa2a23a83d | |
Jos de Jong | 823b445e94 | |
josdejong | 9a23799d5f | |
josdejong | 20a067508d | |
josdejong | 6b63d623ec | |
josdejong | 7ffbd39ae2 | |
josdejong | 9565660ae5 | |
josdejong | 1bdb00dc75 | |
josdejong | 91125408a9 | |
josdejong | f17c575c60 | |
josdejong | 44eb417e4f | |
josdejong | 0da0d14b3d | |
josdejong | 3ea589a126 | |
josdejong | 2a885ac5ef | |
josdejong | 637aa62762 | |
josdejong | 61d91c2f9c | |
josdejong | 40accb3f39 | |
josdejong | c293bed8e5 | |
josdejong | 1c06300443 | |
josdejong | d2b8470e2e | |
josdejong | 596d868cc5 | |
josdejong | 41c633e124 | |
josdejong | 55ee6361d6 | |
josdejong | e5ec845d4f | |
josdejong | 34e45a04c4 | |
josdejong | d8c059c9b0 | |
josdejong | d04c94a1c6 | |
josdejong | d2a84b29fe | |
josdejong | 9c8febb1ff | |
josdejong | b66a1cf0fa | |
josdejong | e412628b89 | |
josdejong | 8392c57a2d | |
josdejong | d88120d41c | |
josdejong | dfe7c06ec7 | |
josdejong | a0aaf206fa | |
josdejong | 93f3375c55 | |
josdejong | 0da297c5d3 | |
josdejong | 5cc3665c47 | |
josdejong | fce4c7910a | |
josdejong | bcd4553adb | |
josdejong | 324e5053d9 | |
josdejong | b3ef0ad6a4 | |
josdejong | 4774a50799 | |
josdejong | 5ac813f091 | |
josdejong | f33b715391 | |
josdejong | 8cb22b3480 | |
josdejong | 0d8854c5ea | |
josdejong | 488d7bde49 | |
josdejong | 8d48d16858 | |
josdejong | d4b02e8d00 | |
josdejong | 4d2eb28eb3 | |
josdejong | e66ff70ac4 | |
josdejong | e9116cc36c | |
josdejong | 2b73c6e6bf | |
josdejong | 3517d185f9 | |
josdejong | ed263726dc | |
josdejong | 375a268531 | |
josdejong | 88fb95a522 | |
josdejong | f495974598 | |
josdejong | 9b5891d5cb | |
josdejong | a4c58cfa15 | |
josdejong | 3a9a409fcb | |
josdejong | 7646c1756b | |
josdejong | b22e5fae81 | |
josdejong | 4fdf90fb38 | |
josdejong | c03353845e | |
josdejong | 581e64bd73 | |
josdejong | 86ac2c52c1 | |
josdejong | 6bb9e9460a | |
josdejong | cf027db855 | |
josdejong | 80c7b2814f | |
josdejong | 5fe4be081f | |
josdejong | 0ee1ab5cca | |
josdejong | 635616ac1c | |
josdejong | 1bc28041d3 |
|
@ -0,0 +1,16 @@
|
||||||
|
module.exports = {
|
||||||
|
env: {
|
||||||
|
browser: true,
|
||||||
|
es2020: true,
|
||||||
|
mocha: true
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
'standard'
|
||||||
|
],
|
||||||
|
parserOptions: {
|
||||||
|
ecmaVersion: 12,
|
||||||
|
sourceType: 'module'
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +1,5 @@
|
||||||
.idea
|
/node_modules/
|
||||||
*.iml
|
/public/dist/
|
||||||
.vscode
|
|
||||||
build
|
.DS_Store
|
||||||
dist
|
.idea
|
||||||
downloads
|
|
||||||
node_modules
|
|
||||||
*.zip
|
|
||||||
npm-debug.log
|
|
||||||
/.vs
|
|
13
.npmignore
13
.npmignore
|
@ -1,13 +0,0 @@
|
||||||
bower.json
|
|
||||||
CONTRIBUTING.md
|
|
||||||
downloads
|
|
||||||
misc
|
|
||||||
node_modules
|
|
||||||
test
|
|
||||||
tools
|
|
||||||
.idea
|
|
||||||
component.json
|
|
||||||
.npmignore
|
|
||||||
.gitignore
|
|
||||||
*.zip
|
|
||||||
npm-debug.log
|
|
|
@ -1,5 +0,0 @@
|
||||||
language: node_js
|
|
||||||
node_js:
|
|
||||||
- "lts/*"
|
|
||||||
|
|
||||||
script: npm test && npm run lint
|
|
|
@ -1,11 +0,0 @@
|
||||||
{
|
|
||||||
"groups": {
|
|
||||||
"default": {
|
|
||||||
"packages": [
|
|
||||||
"examples/react_advanced_demo/package.json",
|
|
||||||
"examples/react_demo/package.json",
|
|
||||||
"package.json"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
238
gulpfile.js
238
gulpfile.js
|
@ -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')
|
|
||||||
)
|
|
||||||
))
|
|
|
@ -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.
|
File diff suppressed because it is too large
Load Diff
103
package.json
103
package.json
|
@ -1,75 +1,52 @@
|
||||||
{
|
{
|
||||||
"name": "jsoneditor",
|
"name": "jsoneditor-svelte",
|
||||||
"version": "8.6.6",
|
"version": "0.0.1",
|
||||||
"main": "./index",
|
"type": "module",
|
||||||
"description": "A web-based tool to view, edit, format, and validate JSON",
|
"module": "./public/dist/es/jsoneditor.js",
|
||||||
"tags": [
|
"scripts": {
|
||||||
"json",
|
"build": "rollup -c",
|
||||||
"editor",
|
"dev": "rollup -c -w",
|
||||||
"viewer",
|
"start": "sirv public",
|
||||||
"formatter"
|
"test": "mocha ./src/**/*.test.js ./src/**/*.test.mjs",
|
||||||
],
|
"prepare": "node tools/fixLodashEs.cjs && node tools/generateAceWorker.mjs && node tools/generateAjvDrafts.mjs"
|
||||||
"author": "Jos de Jong <wjosdejong@gmail.com>",
|
},
|
||||||
"license": "Apache-2.0",
|
"license": "(MIT OR Apache-2.0)",
|
||||||
"homepage": "https://github.com/josdejong/jsoneditor",
|
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/josdejong/jsoneditor.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": {
|
"dependencies": {
|
||||||
"ace-builds": "^1.4.11",
|
"@fortawesome/free-regular-svg-icons": "5.14.0",
|
||||||
"ajv": "^6.12.2",
|
"@fortawesome/free-solid-svg-icons": "5.14.0",
|
||||||
"javascript-natural-sort": "^0.7.1",
|
"ace-builds": "1.4.12",
|
||||||
"jmespath": "^0.15.0",
|
"ajv": "6.12.3",
|
||||||
"json-source-map": "^0.6.1",
|
"classnames": "2.2.6",
|
||||||
"mobius1-selectr": "^2.4.13",
|
"lodash-es": "4.17.15",
|
||||||
"picomodal": "^3.0.0",
|
"natural-compare-lite": "1.4.0",
|
||||||
"vanilla-picker": "^2.10.1"
|
"svelte-awesome": "2.3.0",
|
||||||
|
"svelte-select": "3.11.1",
|
||||||
|
"svelte-simple-modal": "0.6.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "7.9.0",
|
"@rollup/plugin-commonjs": "14.0.0",
|
||||||
"@babel/preset-env": "7.9.5",
|
"@rollup/plugin-json": "4.1.0",
|
||||||
"@babel/register": "7.9.0",
|
"@rollup/plugin-node-resolve": "8.4.0",
|
||||||
"babel-loader": "8.1.0",
|
|
||||||
"btoa": "1.2.1",
|
"btoa": "1.2.1",
|
||||||
"date-format": "3.0.0",
|
"eslint": "7.7.0",
|
||||||
"fancy-log": "1.3.3",
|
"eslint-config-standard": "14.1.1",
|
||||||
"gulp": "4.0.2",
|
"eslint-plugin-import": "2.22.0",
|
||||||
"gulp-clean-css": "4.3.0",
|
"eslint-plugin-node": "11.1.0",
|
||||||
"gulp-concat-css": "3.1.0",
|
"eslint-plugin-promise": "4.2.1",
|
||||||
"gulp-sass": "4.0.2",
|
"eslint-plugin-standard": "4.0.1",
|
||||||
"jsdom": "16.2.2",
|
|
||||||
"json-loader": "0.5.7",
|
|
||||||
"mkdirp": "1.0.4",
|
"mkdirp": "1.0.4",
|
||||||
"mocha": "7.1.1",
|
"mocha": "8.1.1",
|
||||||
"standard": "14.3.3",
|
"rollup": "2.23.0",
|
||||||
"uglify-js": "3.9.1",
|
"rollup-plugin-livereload": "1.3.0",
|
||||||
"webpack": "4.43.0"
|
"rollup-plugin-svelte": "5.2.3",
|
||||||
},
|
"rollup-plugin-terser": "6.1.0",
|
||||||
"files": [
|
"sass": "1.26.10",
|
||||||
"dist",
|
"sirv-cli": "1.0.3",
|
||||||
"docs",
|
"svelte": "3.24.0",
|
||||||
"examples",
|
"svelte-preprocess": "4.0.8"
|
||||||
"src",
|
|
||||||
"HISTORY.md",
|
|
||||||
"index.js",
|
|
||||||
"LICENSE",
|
|
||||||
"NOTICE",
|
|
||||||
"README.md"
|
|
||||||
],
|
|
||||||
"standard": {
|
|
||||||
"ignore": [
|
|
||||||
"src/js/assets",
|
|
||||||
"examples/react*"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
|
@ -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>
|
Binary file not shown.
After Width: | Height: | Size: 3.1 KiB |
|
@ -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>
|
|
@ -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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
|
@ -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)
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
|
@ -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>
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -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);
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -0,0 +1 @@
|
||||||
|
export const sortModalState = {}
|
|
@ -0,0 +1 @@
|
||||||
|
export const transformModalState = {}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -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;
|
||||||
|
}
|
|
@ -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">{</span>
|
||||||
|
{:else}
|
||||||
|
<span class="delimiter"> {</span>
|
||||||
|
<button class="tag" on:click={handleExpand}>{Object.keys(value).length} props</button>
|
||||||
|
<span class="delimiter">}</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">}</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>
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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>
|
|
@ -0,0 +1,6 @@
|
||||||
|
// used by JSONNode during dragging
|
||||||
|
export const singleton = {
|
||||||
|
mousedown: false,
|
||||||
|
selectionAnchor: null, // Path
|
||||||
|
selectionFocus: null // Path
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
|
||||||
|
|
|
@ -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
|
|
|
@ -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 ▿'
|
|
||||||
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'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
|
@ -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 + ' ▾'
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
4619
src/js/Node.js
4619
src/js/Node.js
File diff suppressed because it is too large
Load Diff
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 results'
|
|
||||||
} else if (resultCount === 1) {
|
|
||||||
this.dom.results.innerHTML = '1 result'
|
|
||||||
} else if (resultCount > MAX_SEARCH_RESULTS) {
|
|
||||||
this.dom.results.innerHTML = MAX_SEARCH_RESULTS + '+ results'
|
|
||||||
} else {
|
|
||||||
this.dom.results.innerHTML = resultCount + ' 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()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 = '►'
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
|
@ -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)
|
|
||||||
})
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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.
|
|
|
@ -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);
|
|
||||||
}
|
|
|
@ -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
|
@ -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;
|
|
||||||
}
|
|
|
@ -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, '&')
|
|
||||||
.replace(/"/g, '"')
|
|
||||||
.replace(/'/g, ''')
|
|
||||||
.replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>')
|
|
||||||
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
|
|
||||||
}
|
|
|
@ -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
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -1 +0,0 @@
|
||||||
*
|
|
|
@ -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
|
|
||||||
*/
|
|
598
src/js/i18n.js
598
src/js/i18n.js
|
@ -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". ' +
|
|
||||||
'字段类型由值自动确定 ' +
|
|
||||||
'可以为 string,number,boolean,或者 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
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
|
@ -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, '')
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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'
|
|
||||||
}
|
|
||||||
]
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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()
|
|
||||||
}
|
|
|
@ -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="<"><</option>' +
|
|
||||||
' <option value="<="><=</option>' +
|
|
||||||
' <option value=">">></option>' +
|
|
||||||
' <option value=">=">>=</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()
|
|
||||||
}
|
|
1059
src/js/textmode.js
1059
src/js/textmode.js
File diff suppressed because it is too large
Load Diff
1826
src/js/treemode.js
1826
src/js/treemode.js
File diff suppressed because it is too large
Load Diff
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
exports.tryRequireThemeJsonEditor = function () {
|
|
||||||
try {
|
|
||||||
require('./ace/theme-jsoneditor')
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
*/
|
|
1529
src/js/util.js
1529
src/js/util.js
File diff suppressed because it is too large
Load Diff
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
|
@ -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
|
|
@ -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 []
|
||||||
|
}
|
|
@ -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
|
||||||
|
})
|
|
@ -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
|
||||||
|
}
|
|
@ -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 }
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -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('')}}`
|
||||||
|
}
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
|
@ -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: []
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
})
|
|
@ -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
|
||||||
|
: []
|
||||||
|
}
|
|
@ -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(), [])
|
||||||
|
})
|
||||||
|
})
|
|
@ -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
Loading…
Reference in New Issue