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
|
||||
*.iml
|
||||
.vscode
|
||||
build
|
||||
dist
|
||||
downloads
|
||||
node_modules
|
||||
*.zip
|
||||
npm-debug.log
|
||||
/.vs
|
||||
/node_modules/
|
||||
/public/dist/
|
||||
|
||||
.DS_Store
|
||||
.idea
|
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
|
78
HISTORY.md
78
HISTORY.md
|
@ -3,84 +3,8 @@
|
|||
https://github.com/josdejong/jsoneditor
|
||||
|
||||
|
||||
## not yet published, version 9.1.2
|
||||
## not yet published, version 8.6.7
|
||||
|
||||
- Log a clear error in the console when the returned value of `onEditable` is
|
||||
invalid. See #1112.
|
||||
|
||||
|
||||
## 2020-09-23, version 9.1.1
|
||||
|
||||
- Fix #1111: Enum dropdown not showing when using patternProperties for schema.
|
||||
Thanks @ziga-miklic.
|
||||
- Fixed JSONEditor not working when opened in a new window, see #1098.
|
||||
Thanks @joshkel.
|
||||
- Fix quick-key `Ctrl+D` (duplicate) not working.
|
||||
- Define "charset: utf-8" in all HTML examples.
|
||||
|
||||
|
||||
## 2020-09-15, version 9.1.0
|
||||
|
||||
- Implemented German translation (`de`). Thanks @s-a.
|
||||
- Fix quick-keys `Ctrl-\` (format) and `Ctrl-Shift-\` (compact) not working
|
||||
in `code` mode.
|
||||
- Updated dependencies to `ajv@6.12.5`.
|
||||
|
||||
|
||||
## 2020-09-09, version 9.0.5
|
||||
|
||||
- Fix #1090: autocomplete firing on dragging or clicking a node.
|
||||
- Fix #1096: editor crashing when passing an empty string as `name`.
|
||||
- Updated dependencies to `ajv@6.12.4`.
|
||||
|
||||
|
||||
## 2020-08-15, version 9.0.4
|
||||
|
||||
- Updated dependencies to `ace-builds@1.4.12`, `ajv@6.12.3`.
|
||||
- Fix #1077: change the `main` field in `package.json` to point to the actual
|
||||
bundled and minified file instead of a node.js index file.
|
||||
|
||||
|
||||
## 2020-07-02, version 9.0.3
|
||||
|
||||
- Fix regression introduced in `v9.0.2` in the select boxes in the
|
||||
Transform model not lighlighting the matches correctly.
|
||||
|
||||
|
||||
## 2020-07-01, version 9.0.2
|
||||
|
||||
- Fix #1029: XSS vulnerabilities. Thanks @onemoreflag for reporting.
|
||||
- Fix #1017: unable to style the color of a value containing a color.
|
||||
Thanks @p3x-robot.
|
||||
|
||||
|
||||
## 2020-06-24, version 9.0.1
|
||||
|
||||
- Fixed broken link to the Ace editor website (https://ace.c9.io/).
|
||||
Thanks @p3x-robot.
|
||||
- Fix #1027: create IE11 Array polyfills `find` and `findIndex` in such a way
|
||||
that they are not iterable.
|
||||
|
||||
|
||||
## 2020-05-24, version 9.0.0
|
||||
|
||||
- Implemented option `limitDragging`, see #962. This is a breaking change when
|
||||
using a JSON schema: dragging is more restrictive by default in that case.
|
||||
Set `limitDragging: false` to keep the old, non-restricted behavior.
|
||||
|
||||
|
||||
## 2020-05-13, version 8.6.8
|
||||
|
||||
- Fix #936: too many return characters inserted when pasting formatted text
|
||||
from OpenOffice.
|
||||
|
||||
|
||||
## 2020-05-10, version 8.6.7
|
||||
|
||||
- Fix #858: the `dist/jsoneditor.js` bundle containing a link to a
|
||||
non-existing source map.
|
||||
- Fix #978: in some special cases the caret was jumping to the beginning of the
|
||||
line whilst typing.
|
||||
- Update dependencies to `ajv@6.12.2`.
|
||||
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
[![Downloads](https://img.shields.io/npm/dm/jsoneditor.svg)](https://www.npmjs.com/package/jsoneditor)
|
||||
![Maintenance](https://img.shields.io/maintenance/yes/2020.svg)
|
||||
[![License](https://img.shields.io/github/license/josdejong/jsoneditor.svg)](https://github.com/josdejong/jsoneditor/blob/master/LICENSE)
|
||||
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fjosdejong%2Fjsoneditor.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fjosdejong%2Fjsoneditor?ref=badge_shield)
|
||||
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fjosdejong%2Fjsoneditor.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fjosdejong%2Fjsoneditor?ref=badge_shield) [![Greenkeeper badge](https://badges.greenkeeper.io/josdejong/jsoneditor.svg)](https://greenkeeper.io/)
|
||||
|
||||
JSON Editor 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
|
||||
|
@ -83,10 +83,10 @@ with npm (recommended):
|
|||
|
||||
```html
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<html>
|
||||
<head>
|
||||
<!-- when using the mode "code", it's important to specify charset utf-8 -->
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
|
||||
|
||||
<link href="jsoneditor/dist/jsoneditor.min.css" rel="stylesheet" type="text/css">
|
||||
<script src="jsoneditor/dist/jsoneditor.min.js"></script>
|
||||
|
|
20
docs/api.md
20
docs/api.md
|
@ -222,15 +222,11 @@ Constructs a new JSONEditor.
|
|||
|
||||
- `{boolean} escapeUnicode`
|
||||
|
||||
If `true`, unicode characters are escaped and displayed as their hexadecimal code (like `\u260E`) instead of of the character itself (like `☎`). `false` by default.
|
||||
If true, unicode characters are escaped and displayed as their hexadecimal code (like `\u260E`) instead of of the character itself (like `☎`). `false` by default.
|
||||
|
||||
- `{boolean} sortObjectKeys`
|
||||
|
||||
If `true`, object keys in 'tree', 'view' or 'form' mode list be listed alphabetically instead by their insertion order. Sorting is performed using a natural sort algorithm, which makes it easier to see objects that have string numbers as keys. `false` by default.
|
||||
|
||||
- `{boolean} limitDragging`
|
||||
|
||||
If `false`, nodes can be dragged from any parent node to any other parent node. If `true`, nodes can only be dragged inside the same parent node, which effectively only allows reordering of nodes. By default, `limitDragging` is `true` when no JSON `schema` is defined, and `false` otherwise.
|
||||
If true, object keys in 'tree', 'view' or 'form' mode list be listed alphabetically instead by their insertion order. Sorting is performed using a natural sort algorithm, which makes it easier to see objects that have string numbers as keys. `false` by default.
|
||||
|
||||
- `{boolean} history`
|
||||
|
||||
|
@ -442,14 +438,6 @@ Constructs a new JSONEditor.
|
|||
```
|
||||
Only applicable when `mode` is 'form', 'tree' or 'view'.
|
||||
|
||||
- `{function} onFocus({ type: 'focus', target })`
|
||||
Callback method, triggered when the editor comes into focus,
|
||||
passing an object `{type, target}`, Applicable for all modes.
|
||||
|
||||
- `{function} onBlur({ type: 'blur', target })`
|
||||
Callback method, triggered when the editor goes out of focus,
|
||||
passing an object `{type, target}`, Applicable for all modes.
|
||||
|
||||
- `{boolean} colorPicker`
|
||||
|
||||
If `true` (default), values containing a color name or color code will have a color picker rendered on their left side.
|
||||
|
@ -555,7 +543,7 @@ Constructs a new JSONEditor.
|
|||
|
||||
- `{string} language`
|
||||
|
||||
The default language comes from the browser navigator, but you can force a specific language. So use here string as 'en' or 'pt-BR'. Built-in languages: `en`, `zh-CN`, `pt-BR`, `tr`, `ja`, `fr-FR`, `de`. Other translations can be specified via the option `languages`.
|
||||
The default language comes from the browser navigator, but you can force a specific language. So use here string as 'en' or 'pt-BR'. Built-in languages: `en`, `pt-BR`, `zh-CN`, `tr`, `ja`, `fr-FR`. Other translations can be specified via the option `languages`.
|
||||
|
||||
- `{Object} languages`
|
||||
|
||||
|
@ -767,7 +755,7 @@ See also `JSONEditor.update(json)`.
|
|||
|
||||
#### `JSONEditor.setMode(mode)`
|
||||
|
||||
Switch mode. Mode `code` requires the [Ace editor](https://ace.c9.io/).
|
||||
Switch mode. Mode `code` requires the [Ace editor](http://ace.ajax.org/).
|
||||
|
||||
*Parameters:*
|
||||
|
||||
|
|
|
@ -13,7 +13,6 @@ customized with several classes that reflect its type and state.
|
|||
- `jsoneditor-undefined`
|
||||
- `jsoneditor-number`
|
||||
- `jsoneditor-string`
|
||||
- `jsoneditor-string jsoneditor-color-value`
|
||||
- `jsoneditor-boolean`
|
||||
- `jsoneditor-regexp`
|
||||
- `jsoneditor-array`
|
||||
|
|
|
@ -59,10 +59,10 @@ var json = editor.get();
|
|||
|
||||
```html
|
||||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<html>
|
||||
<head>
|
||||
<!-- when using the mode "code", it's important to specify charset utf-8 -->
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
|
||||
|
||||
<link href="jsoneditor/dist/jsoneditor.min.css" rel="stylesheet" type="text/css">
|
||||
<script src="jsoneditor/dist/jsoneditor.min.js"></script>
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<title>JSONEditor | Basic usage</title>
|
||||
|
||||
<link href="../dist/jsoneditor.css" rel="stylesheet" type="text/css">
|
||||
|
@ -38,8 +36,7 @@
|
|||
'number': 123,
|
||||
'object': {'a': 'b', 'c': 'd'},
|
||||
'time': 1575599819000,
|
||||
'string': 'Hello World',
|
||||
'onlineDemo': 'https://jsoneditoronline.org/'
|
||||
'string': 'Hello World'
|
||||
}
|
||||
editor.set(json)
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<title>JSONEditor | Viewer</title>
|
||||
|
||||
<link href="../dist/jsoneditor.css" rel="stylesheet" type="text/css">
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<html>
|
||||
<head>
|
||||
<!-- when using the mode "code", it's important to specify charset utf-8 -->
|
||||
<meta charset="utf-8">
|
||||
|
||||
<title>JSONEditor | Switch mode</title>
|
||||
|
||||
<!-- when using the mode "code", it's important to specify charset utf-8 -->
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
|
||||
|
||||
<link href="../dist/jsoneditor.css" rel="stylesheet" type="text/css">
|
||||
<script src="../dist/jsoneditor.js"></script>
|
||||
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<title>JSONEditor | Load and save</title>
|
||||
|
||||
<link href="../dist/jsoneditor.css" rel="stylesheet" type="text/css">
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<title>JSONEditor | Custom editable fields</title>
|
||||
|
||||
<link href="../dist/jsoneditor.css" rel="stylesheet" type="text/css">
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<title>JSONEditor | Custom styling</title>
|
||||
|
||||
<link href="../dist/jsoneditor.css" rel="stylesheet" type="text/css">
|
||||
|
@ -37,7 +35,7 @@
|
|||
// create the editor
|
||||
const container = document.getElementById('jsoneditor')
|
||||
const options = {
|
||||
modes: ['tree', 'text']
|
||||
modes: ['text', 'tree']
|
||||
}
|
||||
const json = {
|
||||
'array': [1, 2, 3],
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<title>JSONEditor | JSON schema validation</title>
|
||||
|
||||
<link href="../dist/jsoneditor.css" rel="stylesheet" type="text/css">
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<title>JSONEditor | Custom Ace</title>
|
||||
|
||||
<!-- load a custom version of Ace editor -->
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<html>
|
||||
<head>
|
||||
<!-- when using the mode "code", it's important to specify charset utf-8 -->
|
||||
<meta charset="utf-8">
|
||||
|
||||
<title>JSONEditor | Switch mode</title>
|
||||
|
||||
<!-- when using the mode "code", it's important to specify charset utf-8 -->
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
|
||||
|
||||
<link href="../dist/jsoneditor.css" rel="stylesheet" type="text/css">
|
||||
<script src="../dist/jsoneditor.js"></script>
|
||||
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<title>JSONEditor | Item templates</title>
|
||||
|
||||
<link href="../dist/jsoneditor.css" rel="stylesheet" type="text/css">
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<title>JSONEditor | Auto Complete</title>
|
||||
|
||||
<link href="../dist/jsoneditor.css" rel="stylesheet" type="text/css">
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<title>JSONEditor | Dynamic Auto Complete</title>
|
||||
|
||||
<link href="../dist/jsoneditor.css" rel="stylesheet" type="text/css">
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<title>JSONEditor | Advanced Auto Complete</title>
|
||||
|
||||
<meta charset="UTF-8">
|
||||
<link href="../dist/jsoneditor.css" rel="stylesheet" type="text/css">
|
||||
<script src="../dist/jsoneditor.js"></script>
|
||||
<script src="https://unpkg.com/jsonpath@0.2.11/jsonpath.min.js"></script>
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
|
||||
<title>JSONEditor | Translate</title>
|
||||
|
||||
<link href="../dist/jsoneditor.css" rel="stylesheet" type="text/css">
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
|
||||
|
||||
<link href="../dist/jsoneditor.css" rel="stylesheet" type="text/css">
|
||||
<script src="../dist/jsoneditor.js"></script>
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<title>JSONEditor | Synchronize two editors</title>
|
||||
|
||||
<link href="../dist/jsoneditor.css" rel="stylesheet" type="text/css">
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
|
||||
|
||||
<link href="../dist/jsoneditor.css" rel="stylesheet" type="text/css">
|
||||
<script src="../dist/jsoneditor.js"></script>
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<title>JSONEditor | Custom validation</title>
|
||||
|
||||
<link href="../dist/jsoneditor.css" rel="stylesheet" type="text/css">
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<title>JSONEditor | Custom validation (asynchronous)</title>
|
||||
|
||||
<link href="../dist/jsoneditor.css" rel="stylesheet" type="text/css">
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=utf-8">
|
||||
|
||||
<link href="../dist/jsoneditor.css" rel="stylesheet" type="text/css">
|
||||
<script src="../dist/jsoneditor.js"></script>
|
||||
|
@ -32,21 +32,12 @@
|
|||
height: 500px;
|
||||
}
|
||||
#containerRight .different_element {
|
||||
background-color: #acee61;
|
||||
background-color: greenyellow !important;
|
||||
}
|
||||
#containerRight .different_element div.jsoneditor-field,
|
||||
#containerRight .different_element div.jsoneditor-value {
|
||||
color: red;
|
||||
}
|
||||
|
||||
#containerLeft .different_element {
|
||||
background-color: pink;
|
||||
background-color: violet !important;
|
||||
}
|
||||
#containerLeft .different_element div.jsoneditor-field,
|
||||
#containerLeft .different_element div.jsoneditor-value {
|
||||
color: red;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<title>JSONEditor | Basic usage</title>
|
||||
|
||||
<link href="../dist/jsoneditor.css" rel="stylesheet" type="text/css">
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<title>JSONEditor | onValidationError</title>
|
||||
|
||||
<link href="../dist/jsoneditor.css" rel="stylesheet" type="text/css">
|
||||
|
|
|
@ -1,89 +0,0 @@
|
|||
<!DOCTYPE HTML>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<title>JSONEditor | New window</title>
|
||||
|
||||
<link href="../dist/jsoneditor.css" rel="stylesheet" type="text/css">
|
||||
<script src="../dist/jsoneditor.js"></script>
|
||||
|
||||
<style type="text/css">
|
||||
#jsoneditor {
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<p>
|
||||
<button id="openNewEditor">Open Editor in New Window</button>
|
||||
<button id="setJSON">Set JSON</button>
|
||||
<button id="getJSON">Get JSON</button>
|
||||
</p>
|
||||
|
||||
<script>
|
||||
let editor
|
||||
|
||||
function openNewEditor() {
|
||||
const child = window.open("", "_blank", "width=400,height=400")
|
||||
child.document.title = 'JSONEditor | New window'
|
||||
child.onunload = function () {
|
||||
editor = undefined
|
||||
}
|
||||
|
||||
// make the necessary styles available within the child window
|
||||
// for JSONEditor
|
||||
const baseUrl = window.location.href.slice(0, window.location.href.lastIndexOf('/'))
|
||||
const jsonEditorStyles = child.document.createElement("link")
|
||||
jsonEditorStyles.setAttribute("href", baseUrl + "/../dist/jsoneditor.css")
|
||||
jsonEditorStyles.setAttribute("rel", "stylesheet")
|
||||
child.document.head.append(jsonEditorStyles)
|
||||
// for vanilla-picker
|
||||
const colorPickerStyles = JSONEditor.VanillaPicker.StyleElement.cloneNode(true)
|
||||
child.document.head.append(colorPickerStyles)
|
||||
|
||||
const container = child.document.createElement("div")
|
||||
child.document.body.append(container)
|
||||
|
||||
// create the editor
|
||||
const options = {
|
||||
// Show sort and transform modals in the child window, not the parent.
|
||||
modalAnchor: child.document.body
|
||||
}
|
||||
editor = new JSONEditor(container, options)
|
||||
}
|
||||
|
||||
// create a new window
|
||||
document.getElementById('openNewEditor').onclick = openNewEditor
|
||||
|
||||
// set json
|
||||
document.getElementById('setJSON').onclick = function () {
|
||||
if (!editor) {
|
||||
openNewEditor()
|
||||
}
|
||||
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 () {
|
||||
if (!editor) {
|
||||
alert('No editor is open')
|
||||
} else {
|
||||
const json = editor.get()
|
||||
alert(JSON.stringify(json, null, 2))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
File diff suppressed because it is too large
Load Diff
|
@ -3,11 +3,11 @@
|
|||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"jsoneditor": "latest",
|
||||
"jsoneditor": "^8.5.3",
|
||||
"lodash": "4.17.15",
|
||||
"react": "16.13.1",
|
||||
"react-dom": "16.13.1",
|
||||
"react-scripts": "3.4.1"
|
||||
"react-scripts": "3.4.0"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -3,7 +3,7 @@
|
|||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"jsoneditor": "latest",
|
||||
"jsoneditor": "^8.5.3",
|
||||
"react": "16.13.1",
|
||||
"react-dom": "16.13.1",
|
||||
"react-scripts": "3.4.1"
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
{
|
||||
"groups": {
|
||||
"default": {
|
||||
"packages": [
|
||||
"examples/react_advanced_demo/package.json",
|
||||
"examples/react_demo/package.json",
|
||||
"package.json"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
243
gulpfile.js
243
gulpfile.js
|
@ -1,243 +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'
|
||||
}
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
use: ['source-map-loader'],
|
||||
enforce: 'pre'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// 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
104
package.json
104
package.json
|
@ -1,76 +1,52 @@
|
|||
{
|
||||
"name": "jsoneditor",
|
||||
"version": "9.1.1",
|
||||
"main": "./dist/jsoneditor.min.js",
|
||||
"description": "A web-based tool to view, edit, format, and validate JSON",
|
||||
"tags": [
|
||||
"json",
|
||||
"editor",
|
||||
"viewer",
|
||||
"formatter"
|
||||
],
|
||||
"author": "Jos de Jong <wjosdejong@gmail.com>",
|
||||
"license": "Apache-2.0",
|
||||
"homepage": "https://github.com/josdejong/jsoneditor",
|
||||
"name": "jsoneditor-svelte",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"module": "./public/dist/es/jsoneditor.js",
|
||||
"scripts": {
|
||||
"build": "rollup -c",
|
||||
"dev": "rollup -c -w",
|
||||
"start": "sirv public",
|
||||
"test": "mocha ./src/**/*.test.js ./src/**/*.test.mjs",
|
||||
"prepare": "node tools/fixLodashEs.cjs && node tools/generateAceWorker.mjs && node tools/generateAjvDrafts.mjs"
|
||||
},
|
||||
"license": "(MIT OR Apache-2.0)",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/josdejong/jsoneditor.git"
|
||||
},
|
||||
"bugs": "https://github.com/josdejong/jsoneditor/issues",
|
||||
"scripts": {
|
||||
"build": "gulp",
|
||||
"minify": "gulp minify",
|
||||
"start": "gulp watch",
|
||||
"test": "mocha test --require @babel/register",
|
||||
"lint": "standard --env=mocha",
|
||||
"prepublishOnly": "npm test && npm run build"
|
||||
},
|
||||
"dependencies": {
|
||||
"ace-builds": "^1.4.12",
|
||||
"ajv": "^6.12.5",
|
||||
"javascript-natural-sort": "^0.7.1",
|
||||
"jmespath": "^0.15.0",
|
||||
"json-source-map": "^0.6.1",
|
||||
"mobius1-selectr": "^2.4.13",
|
||||
"picomodal": "^3.0.0",
|
||||
"vanilla-picker": "^2.10.1"
|
||||
"@fortawesome/free-regular-svg-icons": "5.14.0",
|
||||
"@fortawesome/free-solid-svg-icons": "5.14.0",
|
||||
"ace-builds": "1.4.12",
|
||||
"ajv": "6.12.3",
|
||||
"classnames": "2.2.6",
|
||||
"lodash-es": "4.17.15",
|
||||
"natural-compare-lite": "1.4.0",
|
||||
"svelte-awesome": "2.3.0",
|
||||
"svelte-select": "3.11.1",
|
||||
"svelte-simple-modal": "0.6.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.11.6",
|
||||
"@babel/preset-env": "7.11.5",
|
||||
"@babel/register": "7.11.5",
|
||||
"babel-loader": "8.1.0",
|
||||
"@rollup/plugin-commonjs": "14.0.0",
|
||||
"@rollup/plugin-json": "4.1.0",
|
||||
"@rollup/plugin-node-resolve": "8.4.0",
|
||||
"btoa": "1.2.1",
|
||||
"date-format": "3.0.0",
|
||||
"fancy-log": "1.3.3",
|
||||
"gulp": "4.0.2",
|
||||
"gulp-clean-css": "4.3.0",
|
||||
"gulp-concat-css": "3.1.0",
|
||||
"gulp-sass": "4.1.0",
|
||||
"jsdom": "16.4.0",
|
||||
"json-loader": "0.5.7",
|
||||
"eslint": "7.7.0",
|
||||
"eslint-config-standard": "14.1.1",
|
||||
"eslint-plugin-import": "2.22.0",
|
||||
"eslint-plugin-node": "11.1.0",
|
||||
"eslint-plugin-promise": "4.2.1",
|
||||
"eslint-plugin-standard": "4.0.1",
|
||||
"mkdirp": "1.0.4",
|
||||
"mocha": "8.1.3",
|
||||
"source-map-loader": "1.1.0",
|
||||
"standard": "14.3.4",
|
||||
"uglify-js": "3.11.1",
|
||||
"webpack": "4.44.2"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"docs",
|
||||
"examples",
|
||||
"src",
|
||||
"HISTORY.md",
|
||||
"index.js",
|
||||
"LICENSE",
|
||||
"NOTICE",
|
||||
"README.md"
|
||||
],
|
||||
"standard": {
|
||||
"ignore": [
|
||||
"src/js/assets",
|
||||
"examples/react*"
|
||||
]
|
||||
"mocha": "8.1.1",
|
||||
"rollup": "2.23.0",
|
||||
"rollup-plugin-livereload": "1.3.0",
|
||||
"rollup-plugin-svelte": "5.2.3",
|
||||
"rollup-plugin-terser": "6.1.0",
|
||||
"sass": "1.26.10",
|
||||
"sirv-cli": "1.0.3",
|
||||
"svelte": "3.24.0",
|
||||
"svelte-preprocess": "4.0.8"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,435 +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'
|
||||
const buttonExpandInner = document.createElement('div')
|
||||
buttonExpandInner.className = 'jsoneditor-expand'
|
||||
buttonExpand.appendChild(buttonExpandInner)
|
||||
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
|
||||
const icon = document.createElement('div')
|
||||
icon.className = 'jsoneditor-icon'
|
||||
button.appendChild(icon)
|
||||
|
||||
const text = document.createElement('div')
|
||||
text.className = 'jsoneditor-text'
|
||||
text.appendChild(document.createTextNode(translate(item.text)))
|
||||
button.appendChild(text)
|
||||
}
|
||||
|
||||
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
|
|
@ -1,194 +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.textContent = 'Scroll for more \u25BF'
|
||||
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'
|
||||
|
||||
const table = document.createElement('table')
|
||||
table.className = 'jsoneditor-text-errors'
|
||||
validationErrors.appendChild(table)
|
||||
|
||||
const tbody = document.createElement('tbody')
|
||||
table.appendChild(tbody)
|
||||
|
||||
errors.forEach(error => {
|
||||
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'
|
||||
}
|
||||
|
||||
const td1 = document.createElement('td')
|
||||
const button = document.createElement('button')
|
||||
button.className = 'jsoneditor-schema-error'
|
||||
td1.appendChild(button)
|
||||
trEl.appendChild(td1)
|
||||
|
||||
const td2 = document.createElement('td')
|
||||
td2.style = 'white-space: nowrap;'
|
||||
td2.textContent = (!isNaN(line) ? ('Ln ' + line) : '')
|
||||
trEl.appendChild(td2)
|
||||
|
||||
if (typeof error === 'string') {
|
||||
const td34 = document.createElement('td')
|
||||
td34.colSpan = 2
|
||||
const pre = document.createElement('pre')
|
||||
pre.appendChild(document.createTextNode(error))
|
||||
td34.appendChild(pre)
|
||||
trEl.appendChild(td34)
|
||||
} else {
|
||||
const td3 = document.createElement('td')
|
||||
td3.appendChild(document.createTextNode(error.dataPath || ''))
|
||||
trEl.appendChild(td3)
|
||||
|
||||
const td4 = document.createElement('td')
|
||||
const pre = document.createElement('pre')
|
||||
pre.appendChild(document.createTextNode(error.message))
|
||||
td4.appendChild(pre)
|
||||
trEl.appendChild(td4)
|
||||
}
|
||||
|
||||
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,492 +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, getInnerText, 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', 'limitDragging',
|
||||
'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
|
||||
JSONEditor.getInnerText = getInnerText
|
||||
|
||||
// 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.textContent = currentTitle + ' \u25BE'
|
||||
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
|
||||
}
|
||||
}
|
4666
src/js/Node.js
4666
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.textContent = 'no\u00A0results'
|
||||
} else if (resultCount === 1) {
|
||||
this.dom.results.textContent = '1\u00A0result'
|
||||
} else if (resultCount > MAX_SEARCH_RESULTS) {
|
||||
this.dom.results.textContent = MAX_SEARCH_RESULTS + '+\u00A0results'
|
||||
} else {
|
||||
this.dom.results.textContent = resultCount + '\u00A0results'
|
||||
}
|
||||
} else {
|
||||
this.dom.results.textContent = ''
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.textContent = 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.textContent = ''
|
||||
|
||||
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.textContent = '\u25BA'
|
||||
|
||||
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.textContent = '...'
|
||||
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,25 +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/mode-text')
|
||||
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.appendChild(document.createTextNode('(' + 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.firstChild.nodeValue = '(' + 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,379 +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.textContent = ''
|
||||
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.textContent = ''
|
||||
divRow.appendChild(document.createTextNode(row.substring(0, token.length)))
|
||||
const b = document.createElement('b')
|
||||
b.appendChild(document.createTextNode(row.substring(token.length)))
|
||||
divRow.appendChild(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)
|
||||
}
|
||||
|
||||
spacer.textContent = text
|
||||
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)
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue