Repository: josdejong/jsoneditor Branch: develop Commit: b14dd44a5075 Files: 152 Total size: 863.6 KB Directory structure: gitextract_p3pjbjxq/ ├── .babelrc ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ └── build.yaml ├── .gitignore ├── .npmignore ├── CONTRIBUTING.md ├── HISTORY.md ├── LICENSE ├── NOTICE ├── README.md ├── SECURITY.md ├── docs/ │ ├── api.md │ ├── shortcut_keys.md │ ├── styling.md │ └── usage.md ├── examples/ │ ├── 01_basic_usage.html │ ├── 02_viewer.html │ ├── 03_switch_mode.html │ ├── 04_load_and_save.html │ ├── 05_custom_fields_editable.html │ ├── 06_custom_styling.html │ ├── 07_json_schema_validation.html │ ├── 08_custom_ace.html │ ├── 09_readonly_text_mode.html │ ├── 10_templates.html │ ├── 11_autocomplete_basic.html │ ├── 12_autocomplete_dynamic.html │ ├── 13_autocomplete_advanced.html │ ├── 14_translate.html │ ├── 15_selection_api.html │ ├── 16_synchronize_editors.html │ ├── 17_on_event_api.html │ ├── 18_custom_validation.html │ ├── 19_custom_validation_async.html │ ├── 20_custom_css_style_for_nodes.html │ ├── 21_customize_context_menu.html │ ├── 22_on_validation_event.html │ ├── 23_custom_query_language.html │ ├── 24_new_window.html │ ├── 25_sync_node_expand.html │ ├── 26_autocomplete_by_schema.html │ ├── 27_autocomplete_by_schema_recursive_refs.html │ ├── 28_autocomplete_text_value_objects.html │ ├── 29_autocomplete_multiple_fields.html │ ├── css/ │ │ └── darktheme.css │ ├── react_advanced_demo/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package.json │ │ ├── public/ │ │ │ └── index.html │ │ └── src/ │ │ ├── App.css │ │ ├── App.js │ │ ├── App.test.js │ │ ├── JSONEditorReact.css │ │ ├── JSONEditorReact.js │ │ ├── index.css │ │ └── index.js │ ├── react_demo/ │ │ ├── .gitignore │ │ ├── README.md │ │ ├── package.json │ │ ├── public/ │ │ │ └── index.html │ │ └── src/ │ │ ├── App.css │ │ ├── App.js │ │ ├── App.test.js │ │ ├── JSONEditorDemo.css │ │ ├── JSONEditorDemo.js │ │ ├── index.css │ │ └── index.js │ └── requirejs_demo/ │ ├── requirejs_demo.html │ └── scripts/ │ └── main.js ├── greenkeeper.json ├── gulpfile.js ├── index.js ├── misc/ │ └── how_to_publish.md ├── package.json ├── src/ │ ├── docs/ │ │ └── which files do I need.md │ ├── js/ │ │ ├── ContextMenu.js │ │ ├── ErrorTable.js │ │ ├── FocusTracker.js │ │ ├── Highlighter.js │ │ ├── History.js │ │ ├── JSONEditor.js │ │ ├── ModeSwitcher.js │ │ ├── Node.js │ │ ├── NodeHistory.js │ │ ├── SchemaTextCompleter.js │ │ ├── SearchBox.js │ │ ├── TreePath.js │ │ ├── ace/ │ │ │ ├── index.js │ │ │ └── theme-jsoneditor.js │ │ ├── appendNodeFactory.js │ │ ├── assets/ │ │ │ ├── jsonlint/ │ │ │ │ ├── README.md │ │ │ │ └── jsonlint.js │ │ │ └── selectr/ │ │ │ ├── README.md │ │ │ ├── selectr.js │ │ │ └── selectr.scss │ │ ├── autocomplete.js │ │ ├── constants.js │ │ ├── createAbsoluteAnchor.js │ │ ├── header.js │ │ ├── i18n.js │ │ ├── jmespathQuery.js │ │ ├── jsonUtils.js │ │ ├── polyfills.js │ │ ├── previewmode.js │ │ ├── showMoreNodeFactory.js │ │ ├── showSortModal.js │ │ ├── showTransformModal.js │ │ ├── textmode.js │ │ ├── treemode.js │ │ ├── tryRequireAjv.js │ │ ├── tryRequireThemeJsonEditor.js │ │ ├── types.js │ │ ├── util.js │ │ ├── validationUtils.js │ │ └── vanilla-picker/ │ │ └── index.js │ └── scss/ │ ├── img/ │ │ └── description.txt │ ├── jsoneditor/ │ │ ├── _autocomplete.scss │ │ ├── _contextmenu.scss │ │ ├── _editor.scss │ │ ├── _menu.scss │ │ ├── _navigationbar.scss │ │ ├── _reset.scss │ │ ├── _searchbox.scss │ │ ├── _statusbar.scss │ │ ├── _treepath.scss │ │ └── _variables.scss │ └── jsoneditor.scss └── test/ ├── Node.test.js ├── SchemaTextCompleter.test.js ├── couchdbeditor.html ├── data/ │ ├── jsons.js │ └── schemas.js ├── jsonUtils.test.js ├── setup.js ├── test_bootstrap.html ├── test_build.html ├── test_build_min.html ├── test_code_mode.html ├── test_color_picker.html ├── test_destroy.html ├── test_enum.html ├── test_enum_2.html ├── test_focus_tracker.html ├── test_get_inner_text.html ├── test_large_array.html ├── test_materialize.html ├── test_minimalist_min.html ├── test_popup_anchor.html ├── test_preview_load_save.html ├── test_schema.html ├── test_update.html └── util.test.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .babelrc ================================================ { "presets": [ ["@babel/preset-env"] ] } ================================================ FILE: .github/FUNDING.yml ================================================ github: josdejong ================================================ FILE: .github/workflows/build.yaml ================================================ name: Node.js CI on: [push, pull_request] jobs: build: runs-on: ubuntu-latest strategy: matrix: node-version: [20.x, 22.x, 24.x] steps: - uses: actions/checkout@v4 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v4 with: node-version: ${{ matrix.node-version }} - run: npm install - run: npm run test env: CI: true lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 24.x - run: npm ci - run: npm run lint env: CI: true ================================================ FILE: .gitignore ================================================ .idea *.iml .vscode build dist downloads node_modules *.zip npm-debug.log /.vs ================================================ FILE: .npmignore ================================================ bower.json CONTRIBUTING.md downloads misc node_modules test tools .idea component.json .npmignore .gitignore *.zip npm-debug.log ================================================ FILE: CONTRIBUTING.md ================================================ ## Contributing Contributions to the `jsoneditor` library are very welcome! We can't do this alone. You can contribute in different ways: spread the word, report bugs, come up with ideas and suggestions, and contribute to the code. There are a few preferences regarding code contributions: - Send pull requests to the `develop` branch, not the `master` branch. - You can use modern JavaScript features, the code is transpiled using Babel. - `jsoneditor` follows the https://standardjs.com/ code style. To test: ``` npm run lint ``` - If possible, create a unit test for any new functionality. To run tests: ``` npm test ``` Thanks! ================================================ FILE: HISTORY.md ================================================ # JSON Editor - History ## 2025-10-15, version 10.4.2 - Fix: #1680 ensure that method `.validate()` always returns a `Promise`. ## 2025-08-28, version 10.4.1 - Fix: show autocompletion dropdown for single element when `option.text` is available (#1676). Thanks @ahmed-saber. ## 2025-08-22, version 10.4.0 - Feat: implemented a new option `withPath` for the `expand` method, see #1671. - Feat: update dependencies `jsonrepair` and `ace-builds`. - Fix: #1673 refinements in the autocomplete text highlighting and dropdown logic (#1674). Thanks @ahmed-saber. - Fix: some class names for highlighting search results in the `darktheme.css` example file (#1672). Thanks @epeleh. ## 2025-08-01, version 10.3.0 - Feat: add support for autocompletion options with separate `text` and `value` (#1669). Thanks @ahmed-saber. - Fix: update dependencies (like `jsonrepair` and `ace-builds`). ## 2025-03-28, version 10.2.0 - Feat: stringify integer numbers bigger than max safe integer (#1646). Thanks @pawfrolow. ## 2025-02-17, version 10.1.3 - Fix: #1643 handle `true` property value in a JSON Schema (#1644). Thanks @joshkel. ## 2024-12-18, version 10.1.2 - Fix: #1637 remove `mobius1-selectr` from the list with dependencies to get rid of a security vulnerability in its docs. ## 2024-11-13, version 10.1.1 - Fix: when editing a key giving it a ` (copy)` suffix, the suffix was removed. - Fix: #1627 `onValidationError` did not always trigger. - Fix: upgrade to the latest version of `ace`. - Fix: upgrade to the latest version of `sass`. ## 2024-06-19, version 10.1.0 - Feat: upgrade to the latest version of `ace` and `jsonrepair` - Fix: #1601 do not convert hexadecimal values into regular numbers. ## 2024-05-09, version 10.0.4 - Fix: #1591 select contents in tree mode only when using the left mouse button, not the middle button or right mouse button. ## 2024-04-25, version 10.0.3 - Update dependencies `ace-builds`, `jsonrepair`, and `vanilla-picker`. - Fix: fix HTML open tag wrongly being `label` instead of `div`. Thanks @ppetkow. ## 2024-03-18, version 10.0.2 - Fix: autocompletion sometimes throwing an error when there are no suggestions. Thanks @jpage-godaddy. - Update dependencies `ace` and `jsonrepair`. ## 2024-02-09, version 10.0.1 - Fix #1570: Spanish grammar corrections (#1571). Thanks @gonchesan. - Update dependencies `ace` and `jsonrepair`. ## 2023-12-21, version 10.0.0 - BREAKING CHANGE: dropped support for Internet Explorer 11. - Update dependencies `ace` and `jsonrepair`. ## 2023-12-21, version 9.10.5 - Revert dependency `jsonrepair` to `v3.1.0` to stay compatible with Internet Explorer 11. Regression introduced in `v9.10.3`. See #1563. ## 2023-11-08, version 9.10.4 - Fix #1558: url not wrapping on Chrome in mode `view`, font-size increasing on mobile Webview. - Upgraded to the latest version of `ace` and `jsonrepair`. ## 2023-09-21, version 9.10.3 - Fix #1550: trimming zero in front of a number (#1549). Thanks @DarkFisk. - Updated dependencies, most notable `ace`, `jsonrepair`, and `vanilla-picker`. ## 2023-05-12, version 9.10.2 - Fix option `showErrorTable` (see #1515): - a console warning was logged when using the option `showErrorTable` - `showErrorTable: false` was not working - `showErrorTable` was not working for `preview` mode - Upgraded to the latest version of `ace` and `jsonrepair`. ## 2023-05-12, version 9.10.1 - Fix #1515: a console warning is logged when using the option `showErrorTable`. ## 2023-02-20, version 9.10.0 - Implement option `showErrorTable` to customize in which modes the error table shows up initially (#1497). Thanks @magedhennawy. - Upgrade to the latest version of Ace editor, `v1.15.2`. - Upgrade to the latest version of jsonrepair, `v3.0.2`. - Fix #1208: source map issue "Could not load source file "0" in source map" (#1499). Thanks @joshkel. ## 2022-09-20, version 9.9.2 - Fix #1470: update the code of Selectr from `2.4.0` to `2.4.13`. ## 2022-09-19, version 9.9.1 - Upgrade to the latest version of Ace editor, `v1.10.1`. ## 2022-06-13, version 9.9.0 - Implemented #968: support for auto-completion based on JSON Schema (#1435). Thanks @meirotstein. - Upgrade to the latest version of Ace editor, `v1.6.0`. ## 2022-05-31, version 9.8.0 - Implemented method `editor.expand({ path, isExpand, recursive })` and callback `onExpand({ path, isExpand, recursive })`. Thanks @himanshu520. - Upgrade to the latest version of Ace editor, `v1.5.3`. ## 2022-03-15, version 9.7.4 - Fix #1421: fix `onBlur` event not firing when focus goes to an `iframe`. ## 2022-03-04, version 9.7.3 - Fix #1422: fix `setSchema` not working. ## 2022-02-09, version 9.7.2 - Fix #1419: blurry text preview query on Chrome. ## 2022-02-09, version 9.7.1 - Fix #1419: blurry text preview on Chrome. ## 2022-01-30, version 9.7.0 - Implement #1413: show color indicator on readonly fields and in mode `view`. ## 2022-01-13, version 9.6.0 - Pass a new property `value` along with the `onNodeName` callback, see #1409. Thanks @brianpos. - Implement the `value` property of the callbacks `onNodeName`, `onEditable`, and `onClassName` for objects and arrays too (was `undefined` before). Since this can be a heavy recursive operation, the property is changed into a lazy getter. ## 2021-12-29, version 9.5.11 - Fix the font on Ubuntu for real by add the "ubuntu mono" font. See #1405. ## 2021-12-27, version 9.5.10 - Fix the font on Ubuntu: add "dejavu sans mono". See #1405. ## 2021-12-22, version 9.5.9 - More tweaking of the font because the 13px Cascadia Mono font on Windows gave issues in Ace editor. Changed to 14px Consolas on Windows, 14px Menlo on Mac, see #1392, #1403. ## 2021-12-19, version 9.5.8 - Fix vertical centering of text and buttons, see #1392. - Improve font: 13px Cascadia Mono on Windows, 13px Monaco on Mac, see #1392. ## 2021-11-06, version 9.5.7 - More robust polyfill for `Element.remove`, `window.CharacterData.remove`, and `window.DocumentType.remove`. Thanks @caok2709. - Update dependencies: `ace-builds@1.4.13`, `vanilla-picker@2.12.1`. ## 2021-09-22, version 9.5.6 - Fix inefficient regex to replace return characters. ## 2021-09-01, version 9.5.5 - Fix `setMode` not throwing an exception anymore in case of a parse error (regression since `9.5.4`). ## 2021-08-25, version 9.5.4 - Use `noreferrer` for window.open, see #1365. Thanks @rajitbanerjee. - Fix #1363: parsing error contains html characters. - Fix opening the Transform or Sort modal in code mode with invalid JSON contents not triggering the `onError` callback (see #1364). - Change the default behavior of error handling to open a basic alert instead of logging the error in the console (see #1364). ## 2021-07-28, version 9.5.3 - Fix #1356: background of tree mode is transparent instead of white. - Fix #473: enum dropdown not working on referenced schemas and templates, see #1355. Thanks @mpccolorado. ## 2021-07-22, version 9.5.2 - Fix #675: Relative image urls in CSS replaced with absolute urls by build script, see #1354. Thanks @esulu. ## 2021-06-30, version 9.5.1 - Upgrade to `jsonrepair@2.2.1`. ## 2021-06-05, version 9.5.0 - Implemented new method `JSONEditor.validate(): Promise`. Thanks @ChrisAcrobat. ## 2021-06-02, version 9.4.2 - Fix #1311: exception being thrown under certain conditions when switching from `code` mode to `preview` mode. - Rename spin animation of `selectr` to prevent conflicts with tailwind, see #1333. Thanks @mdix. ## 2021-04-25, version 9.4.1 - Improvements in the Korean translation. Thanks @luasenvy. ## 2021-04-17, version 9.4.0 - Added Korean translation. Thanks @luasenvy. - Added Spanish translation. Thanks @joabac. - Fix #1282: JSON schema enum dropdown not working for conditionals like `oneOf`, `anyOf`, `allOf`. Thanks @maufl. - Fix #1307: losing caret position when calling `refresh()` during `onChange` callback. ## 2021-04-10, version 9.3.1 - Introduced a new sass variable `$jse-icons-url`, see #1268. Thanks @ppetkow. ## 2021-04-01, version 9.3.0 - Improved Russian translation. Thanks @PunKHS. - Upgraded dependencies to `jsonrepair@2.1.0`. ## 2021-02-24, version 9.2.0 - Added Russian translation. Thanks @PunKHS. - Changed shortcut keys for Format and Compact in code mode from `Ctrl+\` and `Ctrl+Shift+\` to `Ctrl+I` and `Ctrl+Shift+I` respectively, because not all browsers and operating systems support this key combination. ## 2021-02-14, version 9.1.10 - Fixed resolving a JSON schema reference linking to an other schema, see #1239. Thanks @Hagartinger. - Upgraded to latest dependencies (`vanilla-picker@2.11.2`). ## 2021-01-23, version 9.1.9 - Fix `jsoneditor-minimalist` bundle being too large. Regression since `v9.1.5` (caused by a recent upgrade to Webpack 5). Thanks @cbmgit. ## 2021-01-16, version 9.1.8 - Replaced `simple-json-repair` with `jsonrepair` (library was renamed). ## 2020-12-30, version 9.1.7 - Fix #1206: library bundle broken on IE 11, regression introduced in v9.1.6. ## 2020-12-23, version 9.1.6 - Fix #1192: enum dropdown from a JSON schema not rendered when using `additionalProperties`. Thanks @maufl. - Fix #1191: clarify docs about configuration option `ajv`. - Fix #1193: simplify and fix example 20_custom_css_style_for_nodes.html. ## 2020-12-14, version 9.1.5 - Fix #1185: enum dropdown not selecting actual value when this is not a string. - Fix selected value of enum dropdown not updated when changed programmatically. ## 2020-11-23, version 9.1.4 - Fix #1119: list of keys in navigation bar missing a scroll bar. Thanks @tanmayrajani. ## 2020-11-19, version 9.1.3 - Fix #1158: JSON schema_findSchema not found if using internal references. Thanks @maufl. - Update dependencies: `vanilla-picker@2.11.0`. ## 2020-11-07, version 9.1.2 - Fix #1126: fire `onEvent` for boolean checkbox and enum selectbox too. - Log a clear error in the console when the returned value of `onEditable` is invalid. See #1112. - Updated dependency to `ajv@6.12.6`. - Extract the JSON repair functionality into a separate, library `simple-json-repair` with many improvements. ## 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 (). 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`. ## 2020-04-21, version 8.6.6 - Fix #969: adding a new property to an empty object or array is broken. Regression introduced in `v8.6.5`. ## 2020-04-19, version 8.6.5 - Fix #964: translation of titles of some context menu items not working. - Update dependencies to `ace-builds@1.4.11`, `ajv@6.12.1`. ## 2020-03-29, version 8.6.4 - Fix #921: `sortObjectKeys` emits `onChange` events. - Fix #946: `language` not working in modes `text`, `code`, and `preview`. - Revert reckoning with the order of object properties when updating an object (introduced in `v8.6.2`). See #917. - Implement support for repairing line separate JSON. ## 2020-03-18, version 8.6.3 - Fix #932: `JSONEditor.update` broken, did not always recognize when the input changed. Regression introduced in `v8.6.2`. ## 2020-03-18, version 8.6.2 - Fixed #917, #926: Keep order of properties when updating an object. - Fixed #928: Custom root name not reflected in path of navigation bar. - Upgraded to `ajv@6.12.0` ## 2020-02-17, version 8.6.1 - Fixed #908: editor throwing an exception when switching from `'preview'` to `'code'` mode. ## 2020-02-16, version 8.6.0 - Fixed #906: Implemented turning Python objects containing `True`, `False` and `None` into valid JSON using repair. ## 2020-02-06, version 8.5.3 - Fix #892: the undo/redo buttons in mode `code` being broken when custom loading an old version of Ace Editor. ## 2020-02-05, version 8.5.2 - Fix undo/redo buttons in mode `code` not always updating. ## 2020-02-05, version 8.5.1 - Fix broken build. ## 2020-02-05, version 8.5.0 - Implemented support for customizing the query language used in the Transform modal. New options `createQuery`, `executeQuery`, and `queryDescription` are available for this now. An example is available in `examples/23_custom_query_language.html`. See #857, #871. - Implement undo/redo buttons in `code` mode. - Fix history (undo/redo) being cleared in mode `code` and `text` after transforming or sorting. ## 2020-01-25, version 8.4.1 - Fix `console.log` in production code. Oopsie. ## 2020-01-25, version 8.4.0 - Added CSS classes `jsoneditor-expanded` and `jsoneditor-collapsed` on array and object nodes reflecting there state. ## 2020-01-18, version 8.3.0 - Update dependency `ajv` to `v6.11.0`. - Fix #790: editor breaking when missing a translation containing a placeholder. ## 2020-01-16, version 8.2.0 - Make it easy to create custom styling by overriding default SASS variable values, see #881. Thanks @petermanders89. - Update `ace` to `v1.4.8`. ## 2020-01-06, version 8.1.2 - Fix #873: buttons Format, Compact, and Repair not supporting internationalization. - Fix #877: Some CSS styling issues when used in combination with Materialize. - Updated dependency `vanilla-picker` to `v2.10.1`. ## 2019-12-28, version 8.1.1 - Fixed the file size reported in `preview` mode show `KB` and `MB` instead of `KiB` and `MiB` in order to match the size reported by filesystems. ## 2019-12-18, version 8.1.0 - Implemented `popupAnchor` allowing to select a custom anchor element. See #869 and #870. - Fixed #502: CSS rule `* { font-family: ... }` resulting in Ace editor (`code` mode) not having a mono-space font anymore. ## 2019-12-11, version 8.0.0 - Implemented option `timestampFormat` which allows customizing the formatting of timestamp tags. See also option `timestampTag`. Thanks @smallp. - Changed the behavior of `timestampTag` to fallback on the built-in rules when the function does not return a boolean. See #856. - Reverted the heuristics introduced in `v7.3.0` to check whether some field contains a timestamp based on the field name, because they can give wrong timestamps in case of values in seconds instead of the assumed milliseconds (see #847, #856). ## 2019-12-08, version 7.5.0 - Extended the callback `onValidationError` to also report parse errors, and distinguish between JSON schema validation errors and custom errors. See #861 and #612. Thanks @meirotstein. ## 2019-12-01, version 7.4.0 - Implemented callback function `onValidationError`, see #612, #854. Thanks @meirotstein. - Fixed #850: make autocomplete options robust against non-string inputs like `null`, `123`, `true`, `false`. ## 2019-12-01, version 7.3.1 - Fixed #855: `onFocus` and `onBlur` not working in modes `text` and `code` when editor was created without main menu bar, and `editor.destroy()` throwing an exception. ## 2019-11-27, version 7.3.0 - Implemented callbacks `onFocus` and `onBlur` (PR #809, issue #727). Thanks @123survesh. - Fixed #847: allow customizing the in rules determining whether a value is a timestamp or not by passing a callback function to `timestampTag`. ## 2019-10-27, version 7.2.1 - Fixed #826: editor not allowing indentation `0`. - Fixed #828: do not expand/collapse when clicking the text of a node in modes `view` or `form`. - Fixed #829: z-index issue of context-menu button and conflicting css names. ## 2019-10-23, version 7.2.0 - Implemented Japanese translation (`ja`). Thanks @yutakiyama. - Implemented French translation (`fr-FR`), and some improvements in the translation. Thanks @yannickyvin. - Upgraded to the latest version of Ace editor, 1.4.7. - Fixed #824: Parse errors not displayed with bottom right error icon in modes `code` and `text`. ## 2019-10-13, version 7.1.0 - Upgraded to the latest version of Ace editor 1.4.6. Changed implementation to use `ace-builds` directly instead of `brace` (still using Ace 1.2.9). - Improved Portuguese translation. Thanks @victorananias. ## 2019-10-06, version 7.0.5 - Upgraded dependencies: `vanilla-picker@2.10.0`. - Minor documentation improvements. Thanks @slash-arun. - Minor styling fixes. ## 2019-09-11, version 7.0.4 - Fixed #723: schema error popup and color picker not always fully visible. - Fixed wrong text color in search box when using JSONEditor in combination with bootstrap. See #791. Thanks @dmitry-kulikov. - Fixed react examples not working out of the box when cloning or downloading the git repository of JSONEditor. See #787, #788. Thanks @vishwasnavadak. ## 2019-09-04, version 7.0.3 - Fixed `index.js` pointing to non-transpiled code. See #783. - Fixed absolute url of images in SASS. Thanks @moonbreezee. ## 2019-09-02, version 7.0.2 - Fix #781: race condition when destroying the editor right after setting data. ## 2019-09-01, version 7.0.1 - Fix npm package missing `dist` folder. ## 2019-09-01, version 7.0.0 - Converted the code largely to ES6, put Babel transpiler in place. - Dropped support for bower, removed the `dist` folder from the git repository. - Fixed #586: caret position lost when switching browser tabs. ## 2019-08-28, version 6.4.1 - Fix styling of autocompletion dropdown broken. Regression since `v6.4.0`. ## 2019-08-28, version 6.4.0 - Replaces CSS with SASS internally, improvements in styling. Thanks @ppetkow. - Fixed #761: JSON schema errors not rendered in the gutter for mode `code` when the path contained a property with a forward slash, and errors not clickable in the error table. - Fixed #777: option `sortObjectKeys` broken. ## 2019-08-15, version 6.3.0 - Fixed #755: JSONEditor throwing an exception in mode `code`, `text`, and `preview` when `statusBar: false`. - When duplicating an object property, move focus to the field and do not immediately add the `(copy)` suffix. See #766. - Fixed #769: option `name` not working anymore. Regression since `v6.1.0`. - Fixed #763: `autocomplete.trigger: 'focus'` throws an error when opening the context menu. Thanks @Thaina. - Updated dependencies `json-source-map@0.6.1` ## 2019-08-01, version 6.2.1 - Updated Chinese translation. Thanks @SargerasWang. ## 2019-07-28, version 6.2.0 - Implemented new mode `preview`, capable of working with large JSON documents up to 500 MiB. - Repair button is now capable of turning MongoDB documents into valid JSON. - Fixed #730: in `code` mode, there was an initial undo action which clears the content. - Upgraded dependencies `vanilla-picker@2.9.2`, `mobius1-selectr@2.4.13`, `ajv@6.10.2`. ## 2019-06-22, version 6.1.0 - Implemented menu options `sort` and `transform` for modes `code` and `text`. - Implemented new context menu item `extract`. - Minor tweaks in the way paths are displayed in the sort and transform modals. ## 2019-06-12, version 6.0.0 - Breaking change: upgraded dependency `ajv@6.10.0`, supporting JSON schema draft-07 alongside draft-06 and draft-04. - Upgraded dependency `vanilla-picker@2.8.1`. - Use JSON schema title as name for the root object if defined (see #635). ## 2019-06-08, version 5.34.0 - Extended the autocomplete feature with new options `filter` and `trigger`. Thanks @Gcaufy. - Removed :hover style on disabled buttons. Thanks @Gcaufy. - Upgraded dependency `mobius1-selectr@2.4.12`. ## 2019-05-29, version 5.33.0 - Fixed #697: JSON Schema enum dropdown not working inside an array. - Fixed #698: When using `onCreateMenu`, `node.path` is null when clicking on an append node or when multiple nodes are selected. - Upgraded dependencies to `mobius1-selectr@2.4.10`, `vanilla-picker@2.8.0`. - Remove :hover style on disabled buttons. Thanks @Gcaufy. ## 2019-04-27, version 5.32.5 - Fixed a bug in the JMESPath query wizard which didn't correctly handle selecting multiple fields. - Fixed context menu not working when multiple nodes are selected. ## 2019-04-10, version 5.32.4 - Fixed #682 and #687: JSONEditor not being able to handle JSON schema validation errors when the root of the document is an Array. Thanks @DusuWen. ## 2019-04-04, version 5.32.3 - Fixed #684: `const` used in bundled library. ## 2019-04-03, version 5.32.2 - Fixed #416: Clipped action menu for append nodes. - Improve detection of value type in transform modal. - Styling improvements in the transform modal. - Fix CSS class for default/non-default schema values not applied to enums, see (#666). - Fixed #671: Improved handling of duplicate property names, which could cause values to be cleared when used as a controlled component in for example React. ## 2019-03-28, version 5.32.1 - Fixed a regression in parsing JSON paths: numbers where parsed as strings instead of a numeric value. See #679. Thanks @AdamVig. - Fixed using hyphens in the path of custom validation errors (see #665). Thanks @tobiasfriden. ## 2019-03-20, version 5.32.0 - Implemented support for reckoning with JSON schema default values: custom styling can be applied for default and non-default values. Thanks @AdamVig. - Fixed #667: resolving JSON Schema examples and descriptions did not always work for referenced schemas. Thanks @AdamVig. - Fixed #676: JSON Paths containing array properties with a `]` not parsed correctly. ## 2019-03-14, version 5.31.1 - Fix IE11 issue. - Some fixes in the Simplified Chinese translation. Thanks @@adf0001 and @yuxizhe. ## 2019-03-10, version 5.31.0 - Display JSON schema examples in tooltip (#664). Thanks @AdamVig. ## 2019-03-02, version 5.30.0 - Implemented a new option `onCreateMenu` to customize the action menu. Thanks @RobAley. ## 2019-02-20, version 5.29.1 - Fixed #661: JSONEditor broken on IE11 caused by duplicate JSON entries in a translation. ## 2019-02-16, version 5.29.0 - Added Simplified Chinese localization. Thanks @long2ice. - Added Turkish localization. Thanks @beratpostalci. - Improved JSON schema titles on fields. Fixes #321. Thanks @AdamVig. - Fixes in resolving JSON schemas, see #651. Thanks @AdamVig. - Fix #657: `onClassName` throwing an error when a node is removed. ## 2019-01-23, version 5.28.2 - Fix #639: Occurrence of non-ES5 `const` declaration in published code. Regression introduced in `v5.28.0`. ## 2019-01-22, version 5.28.1 - Fix #637: Vertical white border left/right from the main menu in some specific circumstances. - Fix #638: Cannot expand after collapse. Regression introduced in v5.28.0. ## 2019-01-21, version 5.28.0 - Implemented new option `maxVisibleChilds` to customize the maximum number childs that is rendered by default. Thanks @20goto10. - Implemented new option `onClassName`, allowing customized and dynamic styling of nodes. See 20_custom_css_style_for_nodes.html for a demo. Thanks @maestr0. - Make the method `refresh()` public. ## 2019-01-16, version 5.27.1 - Improved navigating deeply nested paths via the navigation bar, see #619. Thanks @meirotstein. - Sdd title from schema description to show the tips for user input. Thanks @tylerchen. - Fix JSON Schema not resolving refs `$ref`, and not creating enum dropdowns. Thanks @tylerchen. ## 2019-01-05, version 5.27.0 - Implemented customizing object and array names via a new option `onNodeName`. Thanks @bnanchen. - Visibility of schema validation errors at the bottom of mode code and text are now toggleable. Thanks @meirotstein. - Fixed text of the mode switcher not being translated. Thanks @antfu. ## 2018-12-06, version 5.26.3 - Fixed #610: JSON Repair now removes trailing commas. - Upgraded devDependency `gulp` to v4. Thanks @maestr0. ## 2018-11-13, version 5.26.2 - Fixed dragging and selecting multiple nodes not working (regression introduced in `v5.26.1`). ## 2018-11-13, version 5.26.1 - Fixed `.update()` throwing an exception when replacing a JSON object with `null`. Thanks @DullReferenceException. - Fixed #598: Search field can't be focused in object view. ## 2018-11-12, version 5.26.0 - Implemented option `mainMenuBar` to enable/disable the main menu bar. Thanks @tanmayrajani. ## 2018-10-29, version 5.25.0 - Implemented options `enableSort` and `enableTransform` so you can turn off these features. Thanks @tanmayrajani. - Fixed #590: validation failing in code and text mode when status bar is disabled. - Fixed #589: the path in the navigation bar is not updated when duplicating or removing a node, and neither after an undo/redo action. - Fixed duplicate and remove of the action menu of multiple selected nodes not working. - Fixed not preventing default selection of text when selecting nodes. - Fixed #595: navigation bar path link not working. ## 2018-10-08, version 5.24.7 - Fix #582: parse error annotations not always up to date in code editor. Thanks @meirotstein. ## 2018-09-12, version 5.24.6 - Fix #548: `import JSONEditor from 'jsoneditor'` not working in TypeScript projects (gave a constructor is undefined error). ## 2018-09-06, version 5.24.5 - Fixed a bug in textmode on IE 11, not loading the editor when `Promise` is undefined. ## 2018-09-06, version 5.24.4 - Fixed #576: Visualization in mode `view` when an array with more than 100 items is rendered. - Fixed JSONEditor not working on IE11: continue and throw console errors when `Promise` is undefined. Regression since `v5.23.0`. - Fixed `onClose` of color picker not being fired when clicking outside the picker to close it. - Upgraded dependencies `brace`, `mobius1-selectr`, `vanilla-picker`. - Upgraded devDependency `mocha`. ## 2018-08-29, version 5.24.3 - Fixed color picker not working in ES6 projects. - Fixed color picker closing immediately after the first `onChange` event, and `onChange` events are now debounced like all text inputs. ## 2018-08-27, version 5.24.2 - Improved error and validation messaging in `text` mode. Thanks @meirotstein. - Clicking a message now selects the line where the error occurs. - Icon bottom right showing when there are warnings or errors. - Fixed field still editable after moving a node from an object to an array, changing the field from a property into an index. ## 2018-08-26, version 5.24.1 - Context menu and color picker are now absolutely positioned, and can overflow the borders of the editor. - Fixed #568: mode switcher disappearing when selecting the current mode again. - Fixed `transform` not creating/removing expand button when the type of a node changed. ## 2018-08-22, version 5.24.0 - Implemented a color picker, and allow hooking in a custom color picker. new options are `colorPicker` and `onColorPicker`. - Implemented a timestamp tag displayed right from timestamps, with corresponding option `timestampTag`. ## 2018-08-17, version 5.23.1 - Fixed #566: transform function broken, regression since `v5.20.0`. ## 2018-08-15, version 5.23.0 - Implemented support for custom validation using a new `onValidate` callback. - In tree mode, nodes containing a validation error now have a className `jsoneditor-validation-error` which can be used for custom styling. ## 2018-08-13, version 5.22.0 - Implemented `onEvent` callback triggered when an event occurs in a JSON field or value. Thanks @cristinabarrantes. ## 2018-08-12, version 5.21.0 - Show validation errors inline instead of at the bottom when in code mode. Thanks @meirotstein. - Fix #562: allow `$` character in property names of of a JSON schema. ## 2018-08-10, version 5.20.0 _Good news: JSONEditor is finally framework friendly and can now be easily integrated in React, Vue, and Angular!_ - Implemented new methods `update` and `updateText`, which maintain the state of the editor (expanded nodes, search, selection). This makes it easy to integrate in frameworks like React. - Implemented options `onChangeJSON(json)` and `onChangeText(jsonString)`. - Added two React examples to the `examples` folder. - Fixed menu buttons "Sort" and "Transform" being available in modes `view` and `form`. ## 2018-08-02, version 5.19.2 - Fixed #558: scrolling to search results and automatically scrolling up/down when dragging an item broken (regression since v5.19.1). ## 2018-07-28, version 5.19.1 - Fixed #557: inner contents of the scrollable area being displayed outside of the editor (on Chrome only). ## 2018-07-11, version 5.19.0 - No more grayed out icons of the context menu, see #532. - Added Sort and Transform buttons to the main menu. - Fixes and improvements in the Transform dialog. ## 2018-06-27, version 5.18.0 - Implemented JMESPath support for advanced filtering, sorting, and transforming of JSON documents. - Implemented a new option `modalAnchor` to control at which part of the screen the modals are displayed. - Fixed #544: JSON Schema errors sometimes not being displayed in the editor. ## 2018-06-03, version 5.17.1 - Fixed a bug in a translation text. ## 2018-06-03, version 5.17.0 - Implemented advanced sorting for arrays. ## 2018-05-23, version 5.16.0 - Better handling of JSON documents containing large arrays: - Only displays the first 100 items of large arrays, with buttons "show more" and "show all" to render more items. - Search results are now limited to max 1000 matches, and search does no longer expand the paths to all matches but only expands the path of the current search result. - Fixed index numbers of Array items not being updated after sorting. ## 2018-05-02, version 5.15.0 - Implemented selection API: `onSelectionChanged`, `onTextSelectionChanged`, `getSelection`, `getTextSelection`, `setSelection`, `setTextSelection`, and `getNodesByRange`. Thanks @meirotstein. ## 2018-03-21, version 5.14.1 - Fixed absolute path of css image `jsoneditor-icons.svg`, which could. give issues with webpack plugin "file-loader". Thanks @landru29. ## 2018-02-25, version 5.14.0 - Implemented support for translations. Thanks @mariohmol. - Fixed a bug sometimes occurring when dragging items from array to object, see #509. Thanks @43081j. - Fixed autocomplete not accepting returned `null` values, see #512. Thanks @43081j. - Fixed memory inefficiency when working with large JSON Schema's generating many errors. Thanks @43081j. ## 2018-02-07, version 5.13.3 - Fixed a positioning issue with JSON Schema errors in text/code mode. ## 2018-01-18, version 5.13.2 - Fixed view mode opening links in a new tab instead of current tab when Ctrl key is not down. Thanks @LEW21. - Fixed #502: code editor not showing a monospaced font some cases. ## 2017-12-28, version 5.13.1 - Fixed another occurrence of #494: properties not escaped in the navigation bar. ## 2017-12-28, version 5.13.0 - Implemented cursor position in text mode. Thanks @meirotstein. - Fixed #494: properties not escaped in the navigation bar. Thanks @meirotstein. ## 2017-12-18, version 5.12.0 - Implemented #482: Include `caseSensitive` option for autocomplete. Thanks @israelito3000. - Upgraded dependencies - `ajv@5.5.2` ## 2017-11-22, version 5.11.0 - Upgraded dependencies - `ajv@5.4.0` - `brace@0.11.0` - Fixed dropdown for JSON Schema enums when defined inside pattern properties. Thanks @alquist. - Fixed code containing a non UTF-8 character. Thanks @alshakero. ## 2017-11-15, version 5.10.1 - Some styling tweaks in the navigation bar and status bar. - Don't display status bar in `text` mode (which doesn't yet support row and col counts). ## 2017-11-15, version 5.10.0 - Implemented a navigation bar showing the path. Thanks @meirotstein. - Implemented a status bar showing cursor location. Thanks @meirotstein. - Implemented repairing JSON objects containing left and right single and double quotes (which you get when typing a JSON object in Word) in `text` and `code` mode. - Implemented repairing JSON objects containing special white space characters like non-breaking space. - Upgraded dependency `ajv` to version `5.3.0`. - Fixed #481: A polyfill required `DocumentType` which is not defined in all environments. ## 2017-09-16, version 5.9.6 - Fixed displaying a dropdown for enums inside composite schemas. Thanks @hachichaud. - Fixed #461: Urls opening twice on Firefox and Safari. ## 2017-08-26, version 5.9.5 - Fixed a regression introduced in `v5.9.4`: after using the context menu once, it was not possible to set focus to an other input field anymore. ## 2017-08-20, version 5.9.4 - Fixed #447: context menus not working in Shadow DOM. Thanks @tomalec. ## 2017-07-24, version 5.9.3 - Fixed broken multi-selection (regression). ## 2017-07-13, version 5.9.2 - Fixed a bug in the JSON sanitizer. ## 2017-07-13, version 5.9.1 - `setText` method of tree mode now automatically sanitizes JSON input when needed. - Fixed #430: automatically fix unescaped control characters in JSON input. ## 2017-07-10, version 5.9.0 - Implemented support for JSON schema references `$ref`, see #302. Thanks @meirotstein. - Fixed #429: JSONEditor no longer accepting an empty array for option `modes`. Thanks @trystan2k. - Fixed JSONEditor picking the first entry of `modes` as initial mode instead of option `mode`. ## 2017-07-08, version 5.8.2 - Select first option from `modes` instead of `tree` when `mode` is not configured. Thanks @bag-man. - Some fixes and improvements in the API of autocompletion. Thanks @israelito3000. ## 2017-07-03, version 5.8.1 - Fixed broken minified bundles in folder `dist` (again...). ## 2017-07-02, version 5.8.0 - Implemented support for autocompletion. Thanks @israelito3000. ## 2017-06-27, version 5.7.2 - Fixed broken minified bundles in folder `dist` (reverted to `uglify-js@2.8.22` for now). ## 2017-06-25, version 5.7.1 - Upgraded dependency `ajv` to version `5.2.0`. Resolves warnings in Webpack build processes. ## 2017-05-26, version 5.7.0 - Implemented support for template items. Thanks @israelito3000. - Upgraded dependencies to the latest versions. Thanks @andreykaipov. ## 2017-04-15, version 5.6.0 - Implemented readonly option for modes `text` and `code.` Thanks @walkerrandolphsmith. - Upgraded dependencies (`brance` and `ajv`) to the latest versions. - Fixed not being able to move focus to enum select box when clicking a JSON Schema warning. - Fixed #309: already loaded version of Ace being overwritten by the embedded version of JSONEditor. - Fixed #368: Mode selection drop down not fully visible on small screen. - Fixed #253: Optimize the input experience of Chinese IME. Thanks @chinesedfan. ## 2017-01-06, version 5.5.11 - Fixed embedded version of jsoneditor ace theme not being loaded in minimalist version (see #55). - Fixed a styling issue in the SearchBox of Ace editor (mode `code`). - Fixed #347: CSS more robust against global settings of div position. - Added docs and example on how to use a custom version of Ace editor. ## 2016-11-02, version 5.5.10 - Fixed #85: pressing enter in an input in a form containing a JSONEditor too breaks submitting the form. ## 2016-10-17, version 5.5.9 - Fixed #329: Editor showing duplicate key warnings for keys defined on the Object prototype, like `toString` and `watch`. ## 2016-09-27, version 5.5.8 - Fixed #314: JSON schema validation throwing an error "Unexpected token ' in JSON at position 0" in specific cases. Thanks @apostrophest ## 2016-08-17, version 5.5.7 - Fixed #308: wrong positioning of label "empty array" when `onEditable` returns false. ## 2016-06-15, version 5.5.6 - Fixed #303: editor contents collapsed when the parent div of the JSONEditor has no height set. - Improved example 04_load_and_save.html. Thanks @RDCH106. ## 2016-05-24, version 5.5.5 - Fixed #298: Switch mode button disappears when switching from text/code to tree/form/view mode when the JSON contained errors. - Fixed enum drop downs not working when the JSONEditor is configured with a name. ## 2016-05-22, version 5.5.4 - Fixed #285: an issue with the enum drop down when having defined multiple enums in a JSON schema. - Fixed a (harmless) error in the console when clicking right from an enum drop down. ## 2016-05-22, version 5.5.3 - Fixed #299: reverted the fix of #268 by trimming text in fields and values. ## 2016-04-18, version 5.5.2 - Fixed #294: Fields reset their caret location on every key press in Firefox. ## 2016-04-16, version 5.5.1 - Fixed enum select boxes not being rendered/removed when setting or removing a JSON schema via `editor.setSchema(schema)`. ## 2016-04-16, version 5.5.0 - Implemented a dropdown for values having an JSON Schema enum. Thanks @tdakanalis. - Fixed #291, #292: Some CSS broken when using the editor in combination with bootstrap. Thanks @nucleartide. ## 2016-04-09, version 5.4.0 - Upgraded all dependencies (`ajv`, `brace`, etc). - Fixed #289: Some CSS breaking when using the editor in combination with materialize.css or bootstrap. - Fixed #290: `setText()` not working in mode text or code. ## 2016-04-06, version 5.3.0 - Implemented support for sorting object keys naturally. Thanks @edufelipe. - Sorting object keys or array items via the context menu is now also naturally sorted. - Fixed #283: improved JSON schema error message in case of no additionalProperties. - Fixed #286: Calling `get()` or `getText()` caused the editor to lose focus. A regression introduced in v5.2.0. ## 2016-03-20, version 5.2.0 - Implemented method `editor.destroy()` to properly cleanup the editor (#278). - Fixed #268: JSONEditor now trims text in fields and values. - Fixed #280: Some CSS issues when used in combination with bootstrap. ## 2016-02-15, version 5.1.5 - Fixed #272: Checkbox for boolean values visible in view mode. ## 2016-02-13, version 5.1.4 - Fixed broken example 04_load_and_save.html. See #265. ## 2016-02-03, version 5.1.3 - Fixed #264: Clicking items in the context menu not working on Firefox. ## 2016-01-21, version 5.1.2 - Improvements in sanitizing invalid JSON. - Updated dependencies to the latest version. - Fixed clicking format/compact not triggering an onChange event. - Fixed #259: when having a JSONEditor inside an HTML form, clicking an entry in the context menu did submit the form. - Fixed browserify build, see #260. Thanks @onip. ## 2016-01-16, version 5.1.1 - Fixed #257: Improving error messages for enum errors failed when the schema contains references. - Fixed #255: Removed wrong console warning about the option `search`. - Fixed error thrown when option `search` is false (see #256). Thanks @MiroHibler. ## 2016-01-14, version 5.1.0 - Implemented support for JSON schema validation, powered by `ajv`. - Implemented #197: display an error in case of duplicate keys in an object. - Implemented #183: display a checkbox left from boolean values, so you can easily switch between true/false. - Implemented debouncing of keyboard input, resulting in much less history actions whilst typing. - Added a minimalist bundle to the `dist` folder, excluding `ace` and `ajv`. - Fixed #222: editor throwing `onChange` events when switching mode. - Fixed an error throw when switching to mode "code" via the menu. - Fixed interfering shortcut keys: changed quick keys to select multiple fields from `Shift+Arrow Up/Down` to `Ctrl+Shift+Arrow Up/Down`. ## 2015-12-31, version 5.0.1 - Fixed a bug in positioning of the context menu for multiple selected nodes. - Fixed #130: option `onEditable` not available in mode `form`. - Fixed #202: removed `version` field from bower.json. ## 2015-12-31, version 5.0.0 - New design. - Implemented selection of multiple nodes, allowing to move/duplicate/remove multiple nodes at once (See #106). - Implemented a new option `escapeUnicode`, which will show the hexadecimal unicode instead of the character itself. (See #93 and #230). - Implemented method `getMode`. - Implemented option `onModeChange(oldMode, newMode)`. - Implemented #203: Objects and arrays in mode `form` and `view` are now expandable by clicking the field names too. - Replaced the PNG icon images with SVG. Thanks @1j01. - Renamed all CSS classes They now have prefixes `.jsoneditor-` to prevent name collisions with css frameworks like bootstrap. - Renamed options `change`, `editable`, `error` to respectively `onChange`, `onEditable`, and `onError`. Old options are still working and give a deprecation warning. - Colors of values are now customizable using CSS. - JSONEditor new throws a warning in the console in case of unknown options. - Fixed #93 and #227: html codes like `&` not escaped. - Fixed #149: Memory leak when switching mode from/to `code` mode, web worker of Ace editor wasn't cleaned up. - Fixed #234: Remove dependency on a fork of the `jsonlint` project on github. - Fixed: disabled `Ctrl+L` quick key to go to a line, instead use the default browser behavior of selecting the address bar. - Fixed #38: clear search results after a new JSON object is set. - Fixed #242: row stays highlighted when dragging outside editor. - Fixed quick-keys Shift+Alt+Arrows not registering actions in history. - Fixed #104: context menus are now positioned relative to the elements of the editor instead of an absolute position in the window. ## 2015-06-13, version 4.2.1 - Fixed #161: Cannot select text in Ace editor on systems using Chinese fonts. ## 2015-05-14, version 4.2.0 - Implemented option `theme`, allowing to set a custom theme for the Ace editor. Thanks @nfvs. - Implemented option `ace`, which allows to pass a custom instance of the Ace instead of the embedded version. - Fixed #186: binding issue to `jsonlint.parse`. - Fixed `editor.get()` manipulating the code when containing an error. ## 2015-03-15, version 4.1.1 - Added missing file `index.js` to the bower package. ## 2015-03-15, version 4.1.0 - Implemented a function `focus()` for modes tree, view, and form. - Added `./src` folder to the distributed package, needed for usage via node.js/browserify. ## 2015-02-28, version 4.0.0 - Ace editor and jsonlint are now packed with jsoneditor.js by default. This makes the library about 4 times larger. If Ace is not needed, a custom build of the library can be done. - The distribution files are now moved from the root to the `/dist` folder. - Reworked the source code to CommonJS modules, using `brace` to load Ace. - JSONP is now automatically stripped from JSON. Thanks @yanivefraim. - Fixed bugs in the JSON sanitizer, no longer manipulating JSON-like structures inside strings. ## 2015-01-25, version 3.2.0 - Implemented shortcut keys `Ctrl+\` to format and `Ctrl+Shift+\` to compact JSON when in mode `text` or `code`. - Before an error is thrown because of invalid text, the editor first tries to sanitize the text (replace JavaScript notation with JSON notation), and only after that throws the error. - Fixed Node.path() not working for a JSON Object `""`. Thanks @tomalec. - Minor styling improvements. - Fixed configured indentation not being applied to Ace editor. ## 2014-09-03, version 3.1.2 - Some fixes/improvements in `parseJS` (to parse a JSON object from a JavaScript object). - Fixed the lack of a semi colon at end of the bundled files. ## 2014-08-01, version 3.1.1 - Replaced parsing of JavaScript objects into JSON from `eval` to a dedicated `parseJS` function. ## 2014-07-28, version 3.1.0 - JSONEditor now accepts JavaScript objects as input, and can turn them into valid JSON. For example `{a:2,b:'str'}` can be turned into `{"a":2,"b":"str"}`. - Implemented an option `editable`, a callback function, which allows to set individual nodes (their field and/or value) editable or read-only. - Fixed: shortcut keys to manipulate the nodes are now disabled when mode is `form` or `view`. ## 2014-05-31, version 3.0.0 - Large code reorganization. - Editor must be loaded as `new JSONEditor(...)` instead of `new jsoneditor.JSONEditor(...)`. - Css is not automatically loaded anymore when using AMD. - Web application has been moved to another project. ## 2014-01-03, version 2.3.6 - Fixed positioning issue of the action menu. ## 2013-12-09, version 2.3.5 - Fixed a positioning issue of the action menu again. - Fixed an issue with non-breaking space characters. ## 2013-11-19, version 2.3.4 - Dropped support for IE8, cleaned up legacy code for old browsers. - Disabled saving files using HTML5 on Firefox to prevent a Firefox bug blocking cut/paste functionality in editable divs after using a.download. ## 2013-10-17, version 2.3.3 - Added support for search (Ctrl+F) in the code editor Ace. - Fixed a positioning issue of the action menu when in bootstrap modal. (thanks tsash). ## 2013-09-26, version 2.3.2 - The web application is now available offline. Thanks ayanamist. ## 2013-09-24, version 2.3.1 - Fixed non-working action menu when in bootstrap modal (z-index issue). - Fixed missing main field in package.json. ## 2013-09-13, version 2.3.0 - Implemented an option `modes`, which creates a menu in the editor where the user can switch between the selected editor modes. - Fixed wrong title on fields with value `null`. - Fixed buggy loading of files in the web application. ## 2013-08-01, version 2.2.2 - Fixed non working option `indentation`. - Fixed css not being loaded with AMD in case of multiple scripts. - Fixed a security error in the server side file retriever script of the web application. ## 2013-05-27, version 2.2.1 - Fixed undefined options in TextEditor. Thanks Wiseon3. - Fixed non-working save function on Firefox 21. Thanks youxiachai. ## 2013-05-04, version 2.2.0 - Unified JSONFormatter and JSONEditor in one editor with a switchable mode. - Urls are navigable now. - Improved error and log handling. - Added jsoneditor to package managers npm and bower. ## 2013-03-11, version 2.1.1 - Fixed an issue with console outputs on IE8, causing the editor not to work at all on IE8. ## 2013-03-08, version 2.1.0 - Replaced the plain text editor with code editor Ace, which brings in syntax highlighting and code inspection. - Improved the splitter between the two panels. Panels can be hided. ## 2013-02-26, version 2.0.2 - Fixed: dragarea of the root node was wrongly visible is removed now. ## 2013-02-21, version 2.0.1 - Fixed undefined variable in the redo method. - Removed the "hide ads" button. Not allowed by Google AdSense, sorry. ## 2013-02-09, version 2.0.0 - Implemented a context menu, replacing the action buttons on the right side of the editor and the inline action buttons. This gives a cleaner interface, more space for the actual contents, and more room for new controls (like insert and sort). - Implemented shortcut keys. The JSON Editor can be used with just a keyboard. - Implemented sort action, which sorts the childs of an array or object. - Implemented auto scrolling up and down when dragging a node and reaching the top or bottom of the editor. - Added support for CommonJS and RequireJS. - Added more examples. - Improved performance and memory usage. - Implemented a new mode 'form', in which only values are editable and the fields are fixed. - Minor improvements and bug fixes. ## 2012-12-08, version 1.7.0 - Implemented two modes: 'editor' (default), and 'viewer'. In viewer mode, the data and datastructure is read-only. - Implemented methods set(json, name), setName(name), and getName(), which allows for setting and getting the field name of the root node. - Fixed an issue where the search bar does not work when there is no global window.editor object. ## 2012-11-26, version 1.6.2 - Fixed a bug in the change callback handler, resulting in an infinite loop when requesting the contents of the editor inside the callback (issue #19). ## 2012-11-21, version 1.6.1 - Added a request header "Accept: application/json" when loading files and urls. ## 2012-11-03, version 1.6.0 - Added feature to the web application to load and save files from disk and url. - Improved error messages in the web application using JSONLint. - Made the web application pass the W3C markup validation service. - Added option 'change' to both editor and formatter, which allows to set a callback which is triggered when the contents of the editor or formatter changes. - Changed the default indentation of the JSONFormatter to 4 spaces. - Renamed options 'enableSearch' and 'enableHistory' to 'search' and 'history' respectively. - Added parameter 'json' to the JSONFormatter constructor. - Added option 'indentation' to the JSONFormatter. ## 2012-10-08, version 1.5.1 - Replaced the paid Chrome App with a free, hosted Chrome App (with ads). ## 2012-10-02, version 1.5.0 - Implemented history: undo/redo all actions. - Created menu icons (instead of text buttons). - Cleaned up the code (removed unused params, improved comments, etc). - Minor performance improvements. ## 2012-08-31, version 1.4.4 - Changed: description of advertisement now gives information about the Chrome App (without ads). - Changed: Chrome App is now configured to be available offline. - Fixed: When zooming your browser window, the fields/values did get wrapped on Chrome (thanks Henri Gourvest), and on Firefox sometimes the jsoneditor disappeared due to wrapping of the interface contents. ## 2012-08-25, version 1.4.3 - Changed: changed code for the buttons to copy from formatter to editor and vice versa, no inline javascript (gives security policy errors in chrome app). ## 2012-08-25, version 1.4.2 - Changed: other bootstrapping mechanism for the chrome app, in a separate javascript file, as inline javascript is not allowed (security policy). - Fixed: drop down menu for changing the field type did throw javascript errors (did not break any functionality though). ## 2012-08-23, version 1.4.1 - New: Chrome app created. ## 2012-08-23, version 1.4.0 - New: Improved icon, logo, and interface header. ## 2012-08-19, version 1.3.0 - New: Added buttons next and previous to the search box in the upper right. - New: Escape characters are automatically inserted before " and \ missing and escape character, making the string contents valid JSON. New lines are automatically replaced with \n. (Thanks Steve Clay) - Changed: all icons have been put in a single sprite. This will improve page load times as there are much less server requests needed to load the editor. ## 2012-08-12, version 1.2.0 - New: Added search functionality. Search results are expanded and highlighted. Quickkeys in the search box: Enter (next), Shift+Enter (previous), Ctrl+Enter (search again). - New: The position of the vertical separator between left and right panel is stored. - New: Link to the sourcecode on github added at the bottom of the page. - Changed: Refinements in the layout: fonts, colors, icons. - Fixed: leading an trailing spaces not being displayed in the editor. - Fixed: wrapping of long words and urls in Chrome. - Fixed: ignoring functions and undefined values in the loaded JSON object (they where interpreted as empty object and string instead of being ignored). ## 2012-07-01, version 1.1.1 - Fixed global event listener for the focus/blur events, causing changes in fields and values not always being registered. - Fixed a css issue with Firefox (box-sizing of the editor). ## 2012-04-24, version 1.1 - Fixed a bug. Dragging an object down which has been expanded and collapsed again did not work. - Using a minified version of jsoneditor.js, to improve page load time and save bandwidth. ## 2012-04-21, version 1.0 - Values are no longer aligned in one global column, but placed directly right from the field. Having field and value close together improves readability, especially in case of deeply nested data. - Values are colorized by their type: strings are green, values read, booleans blue, and null is purple. - Font is changed to a monotype font for better readability. - Special characters like \t are now handled nicely. - Overall performance and memory usage improved. - When clicking on whitespace, focus is set to the closest field or value. - some other small interface tweaks. - Fixed a bug with casting a value from type auto to string and vice versa (the value was not casted at all). ## 2012-03-01, version 0.9.10 - Nicer looking select box for the field types, with icons. - Improved drag and drop: better visualized, and now working in all browsers. - Previous values will be restored after changing the type of a field. When changing the type back, the previous value or childs will be restored. - When hovering buttons (fieldtype, duplicate, delete, add) or when dragging a field, corresponding field including its childs is highlighted. This makes it easier to see what part of the data will be edited. - Errors are now displayed in a message window on top of the page instead of an alert which pops up. - Fixed a bug with displaying enters in fields. - Fixed a bug where the last trailing enter was removed when setting json in the editor. - Added a fix to get around Internet Explorer 8 issues with vertical scrollbars. ## 2012-01-29, version 0.9.9 - Fields can be duplicated - Support for drag and drop: - fields in the editor itself can be moved via drag and drop - fields can be exported from the editor as JSON - external JSON can be dropped inside the editor - When changing type from array to object and vice versa, childs will be maintained instead of removed. - Updated interface. Works now in IE8 too. ## 2012-01-16, version 0.9.8 - Improved the performance of expanding a node with all its childs. ## 2012-01-09, version 0.9.7 - Added functionality to expand/collapse a node and all its childs. Click the expand button of a node while holding Ctrl down. - Small interface improvements ## 2011-11-28, version 0.9.6 - First fully usable version of the JSON editor ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS ================================================ FILE: NOTICE ================================================ JSON Editor https://github.com/josdejong/jsoneditor Copyright (C) 2011-2026 Jos de Jong Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # JSON Editor [![Version](https://img.shields.io/npm/v/jsoneditor.svg)](https://www.npmjs.com/package/jsoneditor) [![Downloads](https://img.shields.io/npm/dm/jsoneditor.svg)](https://www.npmjs.com/package/jsoneditor) [![Maintenance](https://img.shields.io/maintenance/yes/2026.svg)](https://github.com/josdejong/jsoneditor/pulse) [![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) 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 editor. The editor can be used as a component in your own web application. It can be loaded as CommonJS module, AMD module, or as a regular javascript file. The library was originally developed as core component of the popular web application https://jsoneditoronline.org and has been open sourced since then. Supported browsers: Chrome, Firefox, Safari, Edge. json editor   code editor Continuous integration tests are run on [GitHub Actions](https://github.com/josdejong/mathjs/actions), and [LambdaTest](https://www.lambdatest.com) is used to test on all major browsers. [![LambdaTest](https://raw.github.com/josdejong/mathjs/master/misc/lambdatest.svg)](https://www.lambdatest.com) Thanks, GitHub Actions and LambdaTest for the generous support for this open source project! ## Successor: svelte-jsoneditor This library [`jsoneditor`](https://github.com/josdejong/jsoneditor) has a successor: [`svelte-jsoneditor`](https://github.com/josdejong/svelte-jsoneditor). The new editor is not a one-to-one replacement, so there may be reasons to stick with `jsoneditor`. The main differences between the two [are described here](https://github.com/josdejong/svelte-jsoneditor#differences-between-josdejongsvelte-jsoneditor-and-josdejongjsoneditor). ## Features JSONEditor has various modes, with the following features. ### Tree mode - Change, add, move, remove, and duplicate fields and values. - Sort arrays and objects. - Transform JSON using [JMESPath](http://jmespath.org/) queries. - Colorized code. - Color picker. - Search & highlight text in the tree view. - Undo and redo all actions. - JSON schema validation (powered by [ajv](https://github.com/epoberezkin/ajv)). ### Code mode - Colorized code (powered by [Ace](https://ace.c9.io)). - Inspect JSON (powered by [Ace](https://ace.c9.io)). - Format and compact JSON. - Repair JSON. - JSON schema validation (powered by [ajv](https://github.com/epoberezkin/ajv)). ### Text mode - Format and compact JSON. - Repair JSON. - JSON schema validation (powered by [ajv](https://github.com/epoberezkin/ajv)). ### Preview mode - Handle large JSON documents up to 500 MiB. - Transform JSON using [JMESPath](http://jmespath.org/) queries. - Format and compact JSON. - Repair JSON. - JSON schema validation (powered by [ajv](https://github.com/epoberezkin/ajv)). ## Documentation - Documentation: - [API](https://github.com/josdejong/jsoneditor/tree/master/docs/api.md) - [Usage](https://github.com/josdejong/jsoneditor/tree/master/docs/usage.md) - [Shortcut keys](https://github.com/josdejong/jsoneditor/tree/master/docs/shortcut_keys.md) - [Examples](https://github.com/josdejong/jsoneditor/tree/master/examples) - [Source](https://github.com/josdejong/jsoneditor) - [History](https://github.com/josdejong/jsoneditor/blob/master/HISTORY.md) ## Install with npm (recommended): npm install jsoneditor Alternatively, you can use another JavaScript package manager like https://yarnpkg.com/, or a CDN such as https://cdnjs.com/ or https://www.jsdelivr.com/. ## Use > Note that in the following example, you'll have to change the urls `jsoneditor/dist/jsoneditor.min.js` and `jsoneditor/dist/jsoneditor.min.css` to match the place where you've downloaded the library, or fill in the URL of the CDN you're using. ```html
``` ## Build The code of the JSON Editor is located in the folder `./src`. To build jsoneditor: - Install dependencies: ``` npm install ``` - Build JSON Editor: ``` npm run build ``` This will generate the files `./jsoneditor.js`, `./jsoneditor.css`, and minified versions in the dist of the project. - To automatically build when a source file has changed: ``` npm start ``` This will update `./jsoneditor.js` and `./jsoneditor.css` in the dist folder on every change, but it will **NOT** update the minified versions as that's an expensive operation. ## Test Run unit tests: ``` npm test ``` Run code linting ([JavaScript Standard Style](https://standardjs.com/)): ``` npm run lint ``` ## License `jsoneditor` is released as open source under the permissive the [Apache 2.0 license](LICENSE.md). **If you are using jsoneditor commercially, there is a _social_ (but no legal) expectation that you help fund its maintenance. [Start here](https://github.com/sponsors/josdejong).** ================================================ FILE: SECURITY.md ================================================ # Security Policy ## Reporting a Vulnerability Please report (suspected) security vulnerabilities privately to one of the maintainers of the library, for example to Jos de Jong: https://github.com/josdejong. ================================================ FILE: docs/api.md ================================================ # API Reference ## JSONEditor ### Constructor #### `JSONEditor(container [, options [, json]])` Constructs a new JSONEditor. *Parameters:* - `{Element} container` An HTML DIV element. The JSONEditor will be created inside this container element. - `{Object} options` Optional object with options. The available options are described under [Configuration options](#configuration-options). - `{JSON} json` Initial JSON data to be loaded into the JSONEditor. Alternatively, the method `JSONEditor.set(json)` can be used to load JSON data into the editor. *Returns:* - `{JSONEditor} editor` New instance of a JSONEditor. ### Configuration options - `{Object} ace` Provide a custom version of the [Ace editor](http://ace.c9.io/) and use this instead of the version that comes embedded with JSONEditor. Only applicable when `mode` is `code`. When providing your own instance of Ace editor, be aware that JSONEditor assumes the following Ace plugins to be loaded: `mode-json`, `worker-json`, `ext-searchbox`, `ext-language_tools`. Note that when using the minimalist version of JSONEditor (which has Ace excluded), JSONEditor will try to load the Ace plugins `ace/mode/json` and `ace/ext/searchbox`. These plugins must be loaded beforehand or be available in the folder of the Ace editor. - `{Object} ajv` Provide a custom instance of [ajv](https://github.com/epoberezkin/ajv), the library used for JSON schema validation. Example: ```js var options = { ajv: Ajv({ allErrors: true, verbose: true, jsonPointers: false, $data: true }) } ``` > IMPORTANT: JSONEditor relies on some specific configuration of Ajv. > Providing different configuration (like `jsonPointers: true` instead of `false`) > results in JSONEditor breaking because the format of the Ajv errors differs > from what is expected. - `{function} onChange()` Set a callback function triggered when the contents of the JSONEditor change. This callback does not pass the changed contents, use `get()` or `getText()` for that. Note that `get()` can throw an exception in mode `text`, `code`, or `preview`, when the editor contains invalid JSON. Will only be triggered on changes made by the user, not in case of programmatic changes via the functions `set`, `setText`, `update`, or `updateText`. See also callback functions `onChangeJSON(json)` and `onChangeText(jsonString)`. - `{function} onChangeJSON(json)` Set a callback function triggered when the contents of the JSONEditor change. Passes the changed JSON document. Only applicable when option `mode` is `tree`, `form`, or `view`. The callback will only be triggered on changes made by the user, not in case of programmatic changes via the functions `set`, `setText`, `update`, or `updateText`. Also see the callback function `onChangeText(jsonString)`. - `{function} onChangeText(jsonString)` Set a callback function triggered when the contents of the JSONEditor change. Passes the changed JSON document inside a string (stringified). The callback will only be triggered on changes made by the user, not in case of programmatic changes via the functions `set`, `setText`, `update`, or `updateText`. Also see the callback function `onChangeJSON(json)`. - `{function} onClassName({ path, field, value })` Set a callback function to add custom CSS classes to the rendered nodes. Only applicable when option `mode` is `tree`, `form`, or `view`. The callback is invoked with an object containing `path`, `field` and `value`: ``` { path: string[], field: string, value: string } ``` The function must either return a string containing CSS class names, or return `undefined` in order to do nothing for a specific node. In order to update css classes when they depend on external state, you can call `editor.refresh()`. - `{function} onExpand({ path, isExpand, recursive })` Set a callback function to be invoked when a node is expanded/collapsed (not programtically via APIs). Only applicable when option `mode` is `tree`, `form`, or `view`. The callback is invoked with an object containing `path`, `isExpand` and `recursive`: ``` { path: string[], isExpand: boolean, recursive: boolean } ``` - `{function} onEditable({ path, field, value })` Set a callback function to determine whether individual nodes are editable or read-only. Only applicable when option `mode` is `tree`, `text`, or `code`. In case of mode `tree`, the callback is invoked as `editable(node)`, where the first parameter is an object: ``` { field: string, value: string, path: string[] } ``` The function must either return a boolean value to set both the nodes field and value editable or read-only, or return an object `{field: boolean, value: boolean}` to set set the read-only attribute for field and value individually. In modes `text` and `code`, the callback is invoked as `editable(node)` where `node` is an empty object (no field, value, or path). In that case the function can return false to make the text or code editor completely read-only. - `{function} onError(error)` Set a callback function triggered when an error occurs. Invoked with the error as first argument. The callback is only invoked for errors triggered by a users action, like switching from code mode to tree mode or clicking the Format button whilst the editor doesn't contain valid JSON. When not defined, a basic alert with the error message will be opened. - `{function} onModeChange(newMode, oldMode)` Set a callback function triggered right after the mode is changed by the user. Only applicable when the mode can be changed by the user (i.e. when option `modes` is set). - `{function} onNodeName({ path, type, size, value })` Customize the name of object and array nodes. By default the names are brackets with the number of childs inside, like `{5}` and `[32]`. The number inside can be customized. using `onNodeName`. The first parameter is an object containing the following properties: ``` { path: string[], type: 'object' | 'array', size: number, value: object } ``` The `onNodeName` function should return a string containing the name for the node. If nothing is returned, the size (number of childs) will be displayed. - `{function} onValidate(json)` Set a callback function for custom validation. Available in all modes. On a change of the JSON, the callback function is invoked with the changed data. The function should return an array with errors or null if there are no errors. The function can also return a `Promise` resolving with the errors retrieved via an asynchronous validation (like sending a request to a server for validation). The returned errors must have the following structure: `{path: Array., message: string}`. Example: ```js var options = { onValidate: function (json) { var errors = []; if (json && json.customer && !json.customer.address) { errors.push({ path: ['customer'], message: 'Required property "address" missing.' }); } return errors; } } ``` Also see the option `schema` for JSON schema validation. - `{function} onValidationError(errors: ValidationError[])` Set a callback function for validation and parse errors. Available in all modes. The `ValidationError` contains a `type`, and an `error` object. On validation of the json, if errors of any kind were found this callback is invoked with the errors data. On change, the callback will be invoked only if errors were changed. See also method `JSONEditor.validate()`. Example: ```js var options = { /** * @param {Array} errors validation errors */ onValidationError: function (errors) { errors.forEach((error) => { switch (error.type) { case 'validation': // schema validation error ... break; case 'customValidation': // custom validation error ... break; case 'error': // json parse error ... break; ... } }); ... } } ``` - `{function} onCreateMenu(items, node)` Customize context menus in tree mode. Sets a callback function to customize the context menu in tree mode. Each time the user clicks on the context menu button, an array of menu items is created. If this callback is configured, the array with menu items is passed to this function. The menu items can be customized in this function in any aspect of these menu items, including deleting them and/or adding new items. The function should return the final array of menu items to be displayed to the user. Each menu item is represented by an object, which may also contain a submenu array of items. See the source code of example 21 in the examples folder for more info on the format of the items and submenu objects. The second argument `node` is an object containing the following properties: ``` { type: 'single' | 'multiple' | 'append' path: Array, paths: Array with paths } ``` The property `path` containing the path of the node, and `paths` contains the same path or in case there are multiple selected nodes it contains the paths of all selected nodes. When the user opens the context menu of an append node (in an empty object or array), the `type` will be `'append'` and the `path` will contain the path of the parent node. - `{boolean} escapeUnicode` If `true`, unicode characters are escaped and displayed as their hexadecimal code (like `\u260E`) instead 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. - `{boolean} history` Enables history, adds a button Undo and Redo to the menu of the JSONEditor. `true` by default. Only applicable when `mode` is `'tree'`, `'form'`, or `'preview'`. - `{String} mode` Set the editor mode. Available values: 'tree' (default), 'view', 'form', 'code', 'text', 'preview'. In 'view' mode, the data and datastructure is read-only. In 'form' mode, only the value can be changed, the data structure is read-only. Mode 'code' requires the Ace editor to be loaded on the page. Mode 'text' shows the data as plain text. The 'preview' mode can handle large JSON documents up to 500 MiB. It shows a preview of the data, and allows to transform, sort, filter, format, or compact the data. - `{String[]} modes` Create a box in the editor menu where the user can switch between the specified modes. Available values: see option `mode`. - `{String} name` Initial field name for the root node, is `undefined` by default. Can also be set using `JSONEditor.setName(name)`. Only applicable when `mode` is 'tree', 'view', or 'form'. - `{Object} schema` Validate the JSON object against a JSON schema. A JSON schema describes the structure that a JSON object must have, like required properties or the type that a value must have. See [http://json-schema.org/](http://json-schema.org/) for more information. Also see the option `onValidate` for custom validation. - `{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}` - `{boolean} allowSchemaSuggestions` Enables autocomplete suggestions based on the JSON schema. `false` by default. when enabled and schema is configured, the editor will suggest text completions based on the schema properties, examples and enums. **limitation**: the completions will be presented only for a valid json. Only applicable when `mode` is 'code'. - `{boolean} search` Enables a search box in the upper right corner of the JSONEditor. `true` by default. Only applicable when `mode` is 'tree', 'view', or 'form'. - `{Number} indentation` Number of indentation spaces. `2` by default. Only applicable when `mode` is 'code', 'text', or 'preview'. - `{String} theme` Set the Ace editor theme, uses included 'ace/theme/jsoneditor' by default. Please note that only the default theme is included with JSONEditor, so if you specify another one you need to make sure it is loaded. - `{Object} templates` Array of templates that will appear in the context menu, Each template is a json object precreated that can be added as a object value to any node in your document. The following example allow you can create a "Person" node and a "Address" node, each one will appear in your context menu, once you selected the whole json object will be created. ```js var options = { templates: [ { text: 'Person', title: 'Insert a Person Node', className: 'jsoneditor-type-object', field: 'PersonTemplate', value: { 'firstName': 'John', 'lastName': 'Do', 'age': 28 } }, { text: 'Address', title: 'Insert a Address Node', field: 'AddressTemplate', value: { 'street': "", 'city': "", 'state': "", 'ZIP code': "" } } ] } ``` - `{Object} autocomplete` *autocomplete* will enable this feature in your editor in tree mode, the object have the following **subelements**: - `{string} filter` - `{Function} filter` Indicate the filter method of the autocomplete. Default to `start`. - `start` : Match your input from the start, e.g. `ap` match `apple` but `pl` does not. - `contain` : Contain your input or not, e.g. `pl` match `apple` too. - Custom Function : Define custom filter rule with signature `function(token, match, config)`. The `match` parameter can be either a string or an object with `text` and `value` properties. Return `true` if the option matches the input token. - `{string} trigger` Indicate the way to trigger autocomplete menu. Default to `keydown` - `keydown` : When you type something in the field or value, it will trigger autocomplete. - `focus` : When you focus in the field or value, it will trigger the autocomplete. - `{number[]} confirmKeys` Indicate the KeyCodes for trigger confirm completion, by default those keys are: `[39, 35, 9]` which are the code for [right, end, tab] - `{boolean} caseSensitive` Indicate if the autocomplete is going to be strict case-sensitive to match the options. - `{Function} getOptions (text: string, path: string[], input: string, editor: JSONEditor)` This function will return your possible options for create the autocomplete selection, you can control dynamically which options you want to display according to the current active editing node. *Parameters:* - `text` : The text in the current node part. (basically the text that the user is editing) - `path` : The path of the node that is being edited as an array with strings. - `input` : Can be "field" or "value" depending if the user is editing a field name or a value of a node. - `editor` : The editor instance object that is being edited. *Returns:* - Can return an array with autocomplete options. Options can be either strings (e.g. `['apple','cranberry','raspberry','pie']`) or objects with `text` and `value` properties (e.g. `[{text: 'Apple', value: 'apple'}, {text: 'Cranberry', value: 'cranberry'}]`) - Can return `null` when there are no autocomplete options. - Can return an object `{startFrom: number, options: (string|object)[]}`. Here `startFrom` determines the start character from where the existing text will be replaced. `startFrom` is `0` by default, replacing the whole text. Options can be strings or objects as described above. - Can return a `Promise` resolving one of the return types above to support asynchronously retrieving a list with options. - `{boolean} mainMenuBar` Adds main menu bar - Contains format, sort, transform, search etc. functionality. `true` by default. Applicable in all types of `mode`. - `{boolean} navigationBar` Adds navigation bar to the menu - the navigation bar visualize the current position on the tree structure as well as allows breadcrumbs navigation. `true` by default. Only applicable when `mode` is 'tree', 'form' or 'view'. - `{boolean} statusBar` Adds status bar to the bottom of the editor - the status bar shows the cursor position and a count of the selected characters. `true` by default. Only applicable when `mode` is 'code', 'text', or 'preview'. - `{boolean} | {Array} showErrorTable` Automatically expand error table above the status bar on error or validation error if `mode` matches an array item. Alternatively used as a boolean value. Default value is `['text', 'preview']`. - `{function} onTextSelectionChange(start, end, text)` Set a callback function triggered when a text is selected in the JSONEditor. callback signature should be: ```js /** * @param {{row:Number, column:Number}} start Selection start position * @param {{row:Number, column:Number}} end Selected end position * @param {String} text selected text */ function onTextSelectionChange(start, end, text) { ... } ``` Only applicable when `mode` is 'code' or 'text'. - `{function} onSelectionChange(start, end)` Set a callback function triggered when Nodes are selected in the JSONEditor. callback signature should be: ```js /** * @typedef {{value: String|Object|Number|Boolean, path: Array.}} SerializableNode * * @param {SerializableNode=} start * @param {SerializableNode=} end */ function onSelectionChange(start, end) { ... } ``` Only applicable when `mode` is 'tree'. - `{function} onEvent({ field, path, value? }, event)` Set a callback function that will be triggered when an event will occur in a JSON field or value. In case of field event, node information will be ``` { field: string, path: {string|number}[] } ``` In case of value event, node information will be ``` { field: string, path: {string|number}[], value: string } ``` signature should be: ```js /** * @param {Node} the Node where event has been triggered identified by {field: string, path: {string|number}[] [, value: string]}` * @param {event} the event fired */ function onEvent(node, event) { ... } ``` 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. - `{function} onColorPicker(parent, color, onChange)` Callback function triggered when the user clicks a color. Can be used to implement a custom color picker. The callback is invoked with three arguments: `parent` is an HTML element where the color picker can be attached, `color` is the current color, `onChange(newColor)` is a callback which has to be invoked with the new color selected in the color picker. JSONEditor comes with a built-in color picker, powered by [vanilla-picker](https://github.com/Sphinxxxx/vanilla-picker). A simple example of `onColorPicker` using `vanilla-picker`: ```js var options = { onColorPicker: function (parent, color, onChange) { new VanillaPicker({ parent: parent, color: color, onDone: function (color) { onChange(color.hex) } }).show(); } } ``` - `{boolean | function({field, value, path}) -> boolean} timestampTag` If `true` (default), a tag with the date/time of a timestamp is displayed right from values containing a timestamp. By default, a value is considered a timestamp when it is an integer number with a value larger than Jan 1th 2000, `946684800000`. When `timestampTag` a is a function, a timestamp tag will be displayed when this function returns `true`, and no timestamp is displayed when the function returns `false`. When the function returns a non-boolean value like `null` or `undefined`, JSONEditor will fallback on the built-in rules to determine whether or not to show a timestamp. The function is invoked with an object as first parameter: ``` { field: string, value: string, path: string[] } ``` Whether a value is a timestamp can be determined implicitly based on the `value`, or explicitly based on `field` or `path`. You can for example test whether a field name contains a string like: `'date'` or `'time'`. Example: ```js var options = { timestampTag: function ({ field, value, path }) { if (field === 'dateCreated') { return true } return false } } ``` Only applicable for modes `tree`, `form`, and `view`. - `{ function({field, value, path}) -> string|null } timestampFormat` Customizing the way formating the timestamp. Called when a value is timestamp after `timestampTag`. If it returns null, the timestamp would be formatted with default setting (`new Date(value).toISOString()`). parameter: ``` { field: string, value: string, path: string[] } ``` Example: ```js var options = { timestampFormat: function ({ field, value, path }) { if (field === 'customTime') { return new Date(value*1000).toString() } return null } } ``` Only applicable for modes `tree`, `form`, and `view`. - `{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`, `es` `zh-CN`, `pt-BR`, `tr`, `ja`, `fr-FR`, `de`, `ru`, `ko`. Other translations can be specified via the option `languages`. - `{Object} languages` You can override existing translations or provide a new translation for a specific language. To do it provide an object at languages with language and the keys/values to be inserted. For example: ``` 'languages': { 'pt-BR': { 'auto': 'Automático testing' }, 'en': { 'auto': 'Auto testing' } } ``` All available fields for translation can be found in the source file `src/js/i18n.js`. - `{HTMLElement} modalAnchor` The container element where modals (like for sorting and filtering) are attached: an overlay will be created on top of this container, and the modal will be created in the center of this container. - `{HTMLElement} popupAnchor` The container element where popups (for example drop down menus, for JSON Schema error tooltips, and color pickers) will be absolutely positioned. By default, this is the root DIV element of the editor itself. When the JSONEditor is inside a DIV element which hides overflowing contents (CSS `overflow: auto` or `overflow: hidden`), tooltips will be visible only partly. In this case, a `popupAnchor` outside of the element without hidden overflow will allow the tooltips to be visible when overflowing the DIV element of the JSONEditor. - `{boolean} enableSort` Enable sorting of arrays and object properties. Only applicable for mode 'tree'. `true` by default. - `{boolean} enableTransform` Enable filtering, sorting, and transforming JSON using a [JMESPath](http://jmespath.org/) query. Only applicable for mode 'tree'. `true` by default. - `{Number} maxVisibleChilds` Number of children allowed for a given node before the "show more / show all" message appears (in 'tree', 'view', or 'form' modes). `100` by default. - `{ function(json: JSON, queryOptions: QueryOptions) -> string } createQuery` Create a query string based on query options filled in the Transform Wizard in the Transform modal. Normally used in combination with `executeQuery`. The input for the function are the entered query options and the current JSON, and the output must be a string containing the query. This query will be executed using `executeQuery`. The query options have the following structure: ``` interface QueryOptions { filter?: { field: string | '@' relation: '==' | '!=' | '<' | '<=' | '>' | '>=' value: string } sort?: { field: string | '@' direction: 'asc' | 'desc' } projection?: { fields: string[] } } ``` Note that there is a special case `'@'` for `filter.field` and `sort.field`. It means that the field itself is selected, for example when having an array containing numbers. A usage example can be found in `examples/23_custom_query_language.html`. - `{ function(json: JSON, query: string) -> JSON } executeQuery` Replace the build-in query language used in the Transform modal with a custom language. Normally used in combination with `createQuery`. The input for the function is the current JSON and a query string, and output must be the transformed JSON. A usage example can be found in `examples/23_custom_query_language.html`. - `{string} queryDescription` A text description displayed on top of the Transform modal. Can be used to explain a custom query language implemented via `createQuery` and `executeQuery`. The text can contain HTML code like a link to a web page. A usage example can be found in `examples/23_custom_query_language.html`. ### Methods #### `JSONEditor.collapseAll()` Collapse all fields. Only applicable for mode 'tree', 'view', and 'form'. #### `JSONEditor.destroy()` Destroy the editor. Clean up DOM, event listeners, and web workers. #### `JSONEditor.expandAll()` Expand all fields. Only applicable for mode 'tree', 'view', and 'form'. #### `JSONEditor.expand(options)` Expand/collapse a given JSON node. Only applicable for mode 'tree', 'view' and 'form'. *`options` fields:* - `{Array.} path` Path for the node to expand/collapse. Required. - `{Boolean} isExpand` When true, expand the node. Else collapse it. Required. - `{Boolean} recursive` When true, expand/collapse child nodes recursively. Optional. - `{Boolean} withPath` When true, expand/collapse all nodes of `path` itself. Optional. #### `JSONEditor.focus()` Set focus to the JSONEditor. #### `JSONEditor.get()` Get JSON data. This method throws an exception when the editor does not contain valid JSON, which can be the case when the editor is in mode `code`, `text`, or `preview`. *Returns:* - `{JSON} json` JSON data from the JSONEditor. #### `JSONEditor.getMode()` Retrieve the current mode of the editor. *Returns:* - `{String} mode` Current mode of the editor, for example `tree` or `code`. #### `JSONEditor.getName()` Retrieve the current field name of the root node. *Returns:* - `{String | undefined} name` Current field name of the root node, or undefined if not set. #### `JSONEditor.getNodesByRange(start, end)` A utility function for getting a list of `SerializableNode` under certain range. This function can be used as complementary to `getSelection` and `onSelectionChange` if a list of __all__ the selected nodes is required. *Parameters:* - `{path: Array.} start` Path for the first node in range - `{path: Array.} end` Path for the last node in range #### `JSONEditor.getSelection()` Get the current selected nodes, Only applicable for mode 'tree'. *Returns:* - `{start:SerializableNode, end: SerializableNode}` #### `JSONEditor.getText()` Get JSON data as string. *Returns:* - `{String} jsonString` Contents of the editor as string. When the editor is in code `text`, `code` or `preview`, the returned text is returned as-is. For the other modes, the returned text is a compacted string. In order to get the JSON formatted with a certain number of spaces, use `JSON.stringify(JSONEditor.get(), null, 2)`. #### `JSONEditor.getTextSelection()` Get the current selected text with the selection range, Only applicable for mode 'text' and 'code'. *Returns:* - `{start:{row:Number, column:Number},end:{row:Number, column:Number},text:String} selection` #### `JSONEditor.refresh()` Force the editor to refresh the user interface and update all rendered HTML. This can be useful for example when using `onClassName` and the returned class name depends on external factors. #### `JSONEditor.set(json)` Set JSON data. Resets the state of the editor (expanded nodes, search, selection). See also `JSONEditor.update(json)`. *Parameters:* - `{JSON} json` JSON data to be displayed in the JSONEditor. #### `JSONEditor.setMode(mode)` Switch mode. Mode `code` requires the [Ace editor](https://ace.c9.io/). *Parameters:* - `{String} mode` Available values: `tree`, `view`, `form`, `code`, `text`, `preview`. #### `JSONEditor.setName(name)` Set a field name for the root node. *Parameters:* - `{String | undefined} name` Field name of the root node. If undefined, the current name will be removed. #### `JSONEditor.setSchema(schema [,schemaRefs])` Set a JSON schema for validation of the JSON object. See also option `schema`. See [http://json-schema.org/](http://json-schema.org/) for more information on the JSON schema definition. *Parameters:* - `{Object} schema` A JSON schema. - `{Object} schemaRefs` Optional, Schemas that are referenced using the `$ref` property from the JSON schema, the object structure in the form of `{reference_key: schemaObject}` #### `JSONEditor.setSelection(start, end)` Set selection for a range of nodes, Only applicable for mode 'tree'. - If no parameters sent - the current selection will be removed, if exists. - For single node selecion send only the `start` parameter. - If the nodes are not from the same level the first common parent will be selected *Parameters:* - `{path: Array.} start` Path for the start node - `{path: Array.} end` Path for the end node #### `JSONEditor.setText(jsonString)` Set text data in the editor. This method throws an exception when the provided jsonString does not contain valid JSON and the editor is in mode `tree`, `view`, or `form`. *Parameters:* - `{String} jsonString` Contents of the editor as string. #### `JSONEditor.setTextSelection(startPos, endPos)` Set text selection for a range, Only applicable for mode 'text' and 'code'. *Parameters:* - `{row:Number, column:Number} startPos` Position for selection start - `{row:Number, column:Number} endPos` Position for selection end #### `JSONEditor.update(json)` Replace JSON data when the new data contains changes. In modes `tree`, `form`, and `view`, the state of the editor will be maintained (expanded nodes, search, selection). See also `JSONEditor.set(json)`. *Parameters:* - `{JSON} json` JSON data to be displayed in the JSONEditor. #### `JSONEditor.updateText (json)` Replace text data when the new data contains changes. In modes `tree`, `form`, and `view`, the state of the editor will be maintained (expanded nodes, search, selection). Also see `JSONEditor.setText(jsonString)`. This method throws an exception when the provided jsonString does not contain valid JSON and the editor is in mode `tree`, `view`, or `form`. *Parameters:* - `{String} jsonString` Contents of the editor as string. #### `JSONEditor.validate()` Validate the JSON document against the configured JSON schema or custom validator. See also the `onValidationError` callback. *Returns:* - `{Promise} errorsPromise` Returns a promise which resolves with the current validation errors, or an empty list when there are no errors. The `ValidationError` contains a `type`, `path`, and `message`. ### Static properties - `{string[]} JSONEditor.VALID_OPTIONS` An array with the names of all known options. - `{object} ace` Access to the bundled Ace editor, via the [`brace` library](https://github.com/thlorenz/brace). Ace is used in code mode. Same as `var ace = require('brace');`. - `{function} Ajv` Access to the bundled [`ajv` library](https://github.com/epoberezkin/ajv), used for JSON schema validation. Same as `var Ajv = require('ajv');`. - `{function} VanillaPicker` Access to the bundled [`vanilla-picker` library](https://github.com/Sphinxxxx/vanilla-picker), used as color picker. Same as `var VanillaPicker = require('vanilla-picker');`. ### Examples A tree editor: ```js var options = { "mode": "tree", "search": true }; var editor = new JSONEditor(container, options); var json = { "Array": [1, 2, 3], "Boolean": true, "Null": null, "Number": 123, "Object": {"a": "b", "c": "d"}, "String": "Hello World" }; editor.set(json); editor.expandAll(); var json = editor.get(json); ``` A text editor: ```js var options = { "mode": "text", "indentation": 2 }; var editor = new JSONEditor(container, options); var json = { "Array": [1, 2, 3], "Boolean": true, "Null": null, "Number": 123, "Object": {"a": "b", "c": "d"}, "String": "Hello World" }; editor.set(json); var json = editor.get(); ``` ## JSON parsing and stringification In general, to parse or stringify JSON data, the browsers built in JSON parser can be used. To create a formatted string from a JSON object, use: ```js var formattedString = JSON.stringify(json, null, 2); ``` to create a compacted string from a JSON object, use: ```js var compactString = JSON.stringify(json); ``` To parse a String to a JSON object, use: ```js var json = JSON.parse(string); ``` ================================================ FILE: docs/shortcut_keys.md ================================================ # Shortcut keys ## Tree Editor Key | Description ----------------------- | ------------------------------------------------ Alt+Arrows | Move the caret up/down/left/right between fields Ctrl+Shift+Arrow Up/Down| Select multiple fields Shift+Alt+Arrows | Move current field or selected fields up/down/left/right Ctrl+D | Duplicate field Ctrl+Del | Remove field Ctrl+Enter | Open link when on a field containing an url Ctrl+Ins | Insert a new field with type auto Ctrl+Shift+Ins | Append a new field with type auto Ctrl+E | Expand or collapse field Alt+End | Move the caret to the last field Ctrl+F | Find F3, Ctrl+G | Find next Shift+F3, Ctrl+Shift+G | Find previous Alt+Home | Move the caret to the first field Ctrl+M | Show actions menu Ctrl+Z | Undo last action Ctrl+Shift+Z | Redo ## Code Editor The code editor is powered by [Ace Editor](http://ace.c9.io/). This editor's shortcut keys are described here: https://github.com/ajaxorg/ace/wiki/Default-Keyboard-Shortcuts Additionally, there are shortcut keys to format/compact the code: Key | Description ----------------------- | ------------------------------------------------ Ctrl+I | Format JSON data, set proper indentation Ctrl+Shift+I | Compact JSON data, remove all whitespace ================================================ FILE: docs/styling.md ================================================ # Styling Reference Documentation for writing custom JSON Editor styles. ## Node Node is the fundamental unit that makes up the hierarchical JSON display in the Form, Tree, and View modes. It can be customized with several classes that reflect its type and state. - `jsoneditor-field`: the property name - `jsoneditor-value`: the value of the property - The value element will have one of the following classes depending on its type: - `jsoneditor-null` - `jsoneditor-undefined` - `jsoneditor-number` - `jsoneditor-string` - `jsoneditor-string jsoneditor-color-value` - `jsoneditor-boolean` - `jsoneditor-regexp` - `jsoneditor-array` - `jsoneditor-object` - `jsoneditor-url` - `jsoneditor-is-default`: applied to the value element when the value matches the default from the schema - `jsoneditor-is-not-default`: applied to the value element when the value does not match the default from the schema - `jsoneditor-schema-error`: the warning icon that appears when the Node has a schema validation error - `jsoneditor-popover`: the popover that appears when hovering over the schema validation error warning icon ================================================ FILE: docs/usage.md ================================================ # Usage ### Install Install via npm: npm install jsoneditor Alternatively, you can use another JavaScript package manager like https://yarnpkg.com/, or a CDN such as https://cdnjs.com/ or https://www.jsdelivr.com/. ## Load To implement JSONEditor in a web application, load the javascript and css file in the head of the HTML page: ```html ``` Here you'll have to change the urls `jsoneditor/dist/jsoneditor.min.js` and `jsoneditor/dist/jsoneditor.min.css` to match the place where you've downloaded the library, or fill in the URL of the CDN you're using. ## Use In the body, create a div element with an id and a size: ```html
``` After the page is loaded, load the editor with javascript: ```js var container = document.getElementById("jsoneditor"); var options = { mode: 'tree' }; var editor = new JSONEditor(container, options); ``` To set JSON data in the editor: ```js var json = { "Array": [1, 2, 3], "Boolean": true, "Null": null, "Number": 123, "Object": {"a": "b", "c": "d"}, "String": "Hello World" }; editor.set(json); ``` To get JSON data from the editor: ```js var json = editor.get(); ``` ## Full Example ```html

``` For more examples, see the [examples section](https://github.com/josdejong/jsoneditor/tree/master/examples). ================================================ FILE: examples/01_basic_usage.html ================================================ JSONEditor | Basic usage

================================================ FILE: examples/02_viewer.html ================================================ JSONEditor | Viewer

This editor is read-only (mode='viewer').

================================================ FILE: examples/03_switch_mode.html ================================================ JSONEditor | Switch mode

Switch editor mode using the mode box. Note that the mode can be changed programmatically as well using the method editor.setMode(mode), try it in the console of your browser.

================================================ FILE: examples/04_load_and_save.html ================================================ JSONEditor | Load and save

Load and save JSON documents

This examples uses HTML5 to load/save local files. Powered by FileReader.js and FileSaver.js.
Only supported on modern browsers (Chrome, FireFox, IE10+, Safari 6.1+, Opera 15+).

Load a JSON document:

Save a JSON document:

================================================ FILE: examples/05_custom_fields_editable.html ================================================ JSONEditor | Custom editable fields

In this example:

  • the field _id and its value are read-only
  • the field name is read-only but has an editable value
  • the field age and its value are editable
================================================ FILE: examples/06_custom_styling.html ================================================ JSONEditor | Custom styling

This example demonstrates how to customize the look of JSONEditor, the editor below has a dark theme. Note that the example isn't worked out for the mode code. To do that, you can load and configure a custom theme for the Ace editor.

================================================ FILE: examples/07_json_schema_validation.html ================================================ JSONEditor | JSON schema validation

JSON schema validation

This example demonstrates JSON schema validation. The JSON object in this example must contain properties like firstName and lastName, can can optionally have a property age which must be a positive integer.

See http://json-schema.org/ for more information.

================================================ FILE: examples/08_custom_ace.html ================================================ JSONEditor | Custom Ace

Custom Ace editor

This example demonstrates how to load a custom version of Ace editor into JSONEditor.

By default, JSONEditor code mode loads the following Ace plugins:

  • ace/mode/json
  • ace/ext/searchbox
  • ace/theme/jsoneditor

The jsoneditor theme comes embedded with JSONEditor. The other two plugins (json and searchbox) must be available in the folder of the custom Ace editor, or already be loaded via a script tag.

================================================ FILE: examples/09_readonly_text_mode.html ================================================ JSONEditor | Switch mode

Switch editor mode using the mode box. Note that the mode can be changed programmatically as well using the method editor.setMode(mode), try it in the console of your browser.

================================================ FILE: examples/10_templates.html ================================================ JSONEditor | Item templates

Item templates

Using item templates, the options in the context menu under "insert" and "append" can be extended with extra options, containing a domain specific template like a "Person", "Contact", "Order", "Address", etc.

================================================ FILE: examples/11_autocomplete_basic.html ================================================ JSONEditor | Auto Complete

This example demonstrates how to autocomplete works, options available are: 'apple','cranberry','raspberry','pie', 'mango', 'mandarine', 'melon', 'appleton'.

================================================ FILE: examples/12_autocomplete_dynamic.html ================================================ JSONEditor | Dynamic Auto Complete

This example demonstrates how to autocomplete works, options available are dynamics and consist in all the strings found in the json

================================================ FILE: examples/13_autocomplete_advanced.html ================================================  JSONEditor | Advanced Auto Complete

This example demonstrates how to autocomplete works with an ActivationChar option, press "*" in any value and continue with autocompletion. The autocomplete returns the posible jsonpaths of the existing json document, for example *object.a.

================================================ FILE: examples/14_translate.html ================================================ JSONEditor | Translate

JSONEditor has support for multiple languages (i18n), in this case uses pt-BR.

================================================ FILE: examples/15_selection_api.html ================================================

Selection indication was done using the on[Text]SelectionChange listeners.
you can try the following calls in the console of your browser:
// text and code modes: editor.getTextSelection() editor.setTextSelection(startPos, endPos) // tree mode: editor.getSelection() editor.setSelection(startNode, endNode)

Selection:
================================================ FILE: examples/16_synchronize_editors.html ================================================ JSONEditor | Synchronize two editors

Keep two editors synchronized using onChangeText and updateText.

This can be done too with onChangeJSON and update, which can only be used in modes tree, form (and view).

================================================ FILE: examples/17_on_event_api.html ================================================

When clicking on a JSON field or value, a log message will be shown in console.

================================================ FILE: examples/18_custom_validation.html ================================================ JSONEditor | Custom validation

Custom validation

This example demonstrates how to run custom validation on a JSON object. The validation is available in all modes.

================================================ FILE: examples/19_custom_validation_async.html ================================================ JSONEditor | Custom validation (asynchronous)

Asynchronous custom validation

This example demonstrates how to run asynchronous custom validation on a JSON object. The names are checked asynchronously and the results "come in" half a second later. Known names in this example are 'Joe', 'Harry', 'Megan'. For other names, a validation error will be displayed.

================================================ FILE: examples/20_custom_css_style_for_nodes.html ================================================

Custom class names

This example highlights the differences between two JSON objects using the option onClassName. Make a change in the left or right editor to see the changes update accordingly.

Please note that this is not a full-fledged, performant JSON diffing solution, it's just a small example to demonstrate onClassName.

================================================ FILE: examples/21_customize_context_menu.html ================================================ JSONEditor | Basic usage

Context Menu Customization

This example demonstrates the use of the onCreateMenu callback option, which allows you to customise context menus after they are created but before they are shown to the user. You can alter/delete existing items as well as adding new menu items. See the source code for this example for more information.

================================================ FILE: examples/22_on_validation_event.html ================================================ JSONEditor | onValidationError

JSON schema validation

This example demonstrates onValidationError callback.

================================================ FILE: examples/23_custom_query_language.html ================================================ JSONEditor | Custom query language

This demo shows how to configure a custom query language. Click on the "Transform" button and try it out.

This basic example uses lodash functions filter, sort, and pick, but you can run any JavaScript code.

WARNING: this example uses new Function() which can be dangerous when executed with arbitrary code. Don't use it in production.

================================================ FILE: examples/24_new_window.html ================================================ JSONEditor | New window

================================================ FILE: examples/25_sync_node_expand.html ================================================ JSONEditor | Sync Node Expand
================================================ FILE: examples/26_autocomplete_by_schema.html ================================================ JSONEditor | Auto-completion by schema

JSON autocompletion by schema

This example demonstrates JSON autocompletion by schema. try to change the JSON properties and values and you'll get a suggestions that are based on the schema properties, examples and enums.

See http://json-schema.org/ for more information.

================================================ FILE: examples/27_autocomplete_by_schema_recursive_refs.html ================================================ JSONEditor | Auto-completion by schema

JSON autocompletion by schema (recursive schema)

This example demonstrates JSON autocompletion by schema. try to change the JSON properties and values and you'll get a suggestions that are based on the schema properties, examples and enums. In this example the schema that in use is actually a recursive schema, meaninng it has a referance of a sub-schema that refer the same sub-schema again.

See http://json-schema.org/ for more information.

================================================ FILE: examples/28_autocomplete_text_value_objects.html ================================================ JSONEditor | Auto Complete with Text/Value Objects

Auto Complete with Text/Value Objects

This example demonstrates the enhanced autocomplete functionality using objects with separate text (display) and value (actual value) properties. You can search by either the company name or stock symbol. Try typing "Apple", "Microsoft", "Google", "AAPL", "MSFT", or "GOOGL". The dropdown shows company names, but the selected value will be the stock symbol.

================================================ FILE: examples/29_autocomplete_multiple_fields.html ================================================ JSONEditor | Auto Complete with Multiple Searchable Fields

Auto Complete with Multiple Searchable Fields

This example demonstrates advanced autocomplete functionality with custom filtering that searches across multiple fields within each option. Countries with multiple searchable fields - you can search by country name, capital city, or country code. Try typing "United States", "Washington", "US", "Germany", "Berlin", "DE", "Japan", "Tokyo", or "JP".

================================================ FILE: examples/css/darktheme.css ================================================ /* dark styling of the editor */ div.jsoneditor, div.jsoneditor-menu { border-color: #4b4b4b; } div.jsoneditor-menu { background-color: #4b4b4b; } div.jsoneditor-tree, div.jsoneditor textarea.jsoneditor-text { background-color: #666666; color: #ffffff; } div.jsoneditor-field, div.jsoneditor-value { color: #ffffff; } table.jsoneditor-search div.jsoneditor-frame { background: #808080; } tr.jsoneditor-highlight, tr.jsoneditor-selected { background-color: #808080; } div.jsoneditor-field[contenteditable=true]:focus, div.jsoneditor-field[contenteditable=true]:hover, div.jsoneditor-value[contenteditable=true]:focus, div.jsoneditor-value[contenteditable=true]:hover, div.jsoneditor-field.jsoneditor-highlight, div.jsoneditor-value.jsoneditor-highlight { background-color: #808080; border-color: #808080; } div.jsoneditor-field.jsoneditor-highlight-active, div.jsoneditor-field.jsoneditor-highlight-active:focus, div.jsoneditor-field.jsoneditor-highlight-active:hover, div.jsoneditor-value.jsoneditor-highlight-active, div.jsoneditor-value.jsoneditor-highlight-active:focus, div.jsoneditor-value.jsoneditor-highlight-active:hover { background-color: #b1b1b1; border-color: #b1b1b1; } div.jsoneditor-tree button:focus { background-color: #868686; } /* coloring of JSON in tree mode */ div.jsoneditor-readonly { color: #acacac; } div.jsoneditor td.jsoneditor-separator { color: #acacac; } div.jsoneditor-value.jsoneditor-string { color: #00ff88; } div.jsoneditor-value.jsoneditor-object, div.jsoneditor-value.jsoneditor-array { color: #bababa; } div.jsoneditor-value.jsoneditor-number { color: #ff4040; } div.jsoneditor-value.jsoneditor-boolean { color: #ff8048; } div.jsoneditor-value.jsoneditor-null { color: #49a7fc; } div.jsoneditor-value.jsoneditor-invalid { color: white; } ================================================ FILE: examples/react_advanced_demo/.gitignore ================================================ # See https://help.github.com/ignore-files/ for more about ignoring files. # dependencies /node_modules # testing /coverage # production /build # misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* ================================================ FILE: examples/react_advanced_demo/README.md ================================================ # JSONEditor React advanced demo This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app). ## Install Install dependencies once: ``` npm install ``` ## Run To run the demo: ``` npm start ``` This will open a development server at http://localhost:3000 ================================================ FILE: examples/react_advanced_demo/package.json ================================================ { "name": "react_advanced_demo", "version": "0.1.0", "private": true, "dependencies": { "jsoneditor": "latest", "lodash": "4.17.23", "react": "18.2.0", "react-dom": "18.2.0", "react-scripts": "5.0.1" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] } } ================================================ FILE: examples/react_advanced_demo/public/index.html ================================================ JSONEditor | React advanced demo
================================================ FILE: examples/react_advanced_demo/src/App.css ================================================ .app .contents { width: 500px; height: 400px; } .app .contents .mode { padding: 10px 0; } .app .contents .code { background: #f5f5f5; overflow: auto; } ================================================ FILE: examples/react_advanced_demo/src/App.js ================================================ import React, { Component } from 'react'; import JSONEditorReact from './JSONEditorReact'; import './App.css'; const schema = { title: 'Example Schema', type: 'object', properties: { array: { type: 'array', items: { type: 'number' } }, boolean: { type: 'boolean' }, number: { type: 'number' } }, required: ['array', 'string', 'boolean'] }; const json = { 'array': [1, 2, 3], 'boolean': true, 'null': null, 'number': 'four', 'object': {'a': 'b', 'c': 'd'}, 'string': 'Hello World' }; const modes = ['tree', 'form', 'view', 'code', 'text']; class App extends Component { state = { schema, text: JSON.stringify(json, null, 2), mode: 'tree' }; render() { return (

JSONEditor React advanced demo

mode:
              
                {this.state.text}
              
            
); } onChangeText = (text) => { this.setState({ text }); }; onModeChangeSelect = (event) => { this.setState({ mode: event.target.value }); }; onModeChange = (mode) => { this.setState({ mode }); }; } export default App; ================================================ FILE: examples/react_advanced_demo/src/App.test.js ================================================ import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; it('renders without crashing', () => { const div = document.createElement('div'); ReactDOM.render(, div); ReactDOM.unmountComponentAtNode(div); }); ================================================ FILE: examples/react_advanced_demo/src/JSONEditorReact.css ================================================ .jsoneditor-react-container { width: 100%; height: 100%; } ================================================ FILE: examples/react_advanced_demo/src/JSONEditorReact.js ================================================ import React, {Component} from 'react'; import isEqual from 'lodash/isEqual'; import cloneDeep from 'lodash/cloneDeep'; import JSONEditor from 'jsoneditor'; import 'jsoneditor/dist/jsoneditor.css'; import './JSONEditorReact.css'; export default class JSONEditorReact extends Component { componentDidMount () { // copy all properties into options for the editor // (except the properties for the JSONEditorReact component itself) const options = Object.assign({}, this.props); delete options.json; delete options.text; this.jsoneditor = new JSONEditor(this.container, options); if ('json' in this.props) { this.jsoneditor.set(this.props.json); } if ('text' in this.props) { this.jsoneditor.setText(this.props.text); } this.schema = cloneDeep(this.props.schema); this.schemaRefs = cloneDeep(this.props.schemaRefs); } componentDidUpdate() { if ('json' in this.props) { this.jsoneditor.update(this.props.json); } if ('text' in this.props) { this.jsoneditor.updateText(this.props.text); } if ('mode' in this.props) { this.jsoneditor.setMode(this.props.mode); } // store a clone of the schema to keep track on when it actually changes. // (When using a PureComponent all of this would be redundant) const schemaChanged = !isEqual(this.props.schema, this.schema); const schemaRefsChanged = !isEqual(this.props.schemaRefs, this.schemaRefs); if (schemaChanged || schemaRefsChanged) { this.schema = cloneDeep(this.props.schema); this.schemaRefs = cloneDeep(this.props.schemaRefs); this.jsoneditor.setSchema(this.props.schema, this.props.schemaRefs); } } componentWillUnmount () { if (this.jsoneditor) { this.jsoneditor.destroy(); } } render() { return (
this.container = elem} /> ); } } ================================================ FILE: examples/react_advanced_demo/src/index.css ================================================ body { font-family: sans-serif; } ================================================ FILE: examples/react_advanced_demo/src/index.js ================================================ import React from 'react' import { createRoot } from 'react-dom/client' import App from './App' import './index.css' const container = document.getElementById('root') const root = createRoot(container) root.render(); ================================================ FILE: examples/react_demo/.gitignore ================================================ # See https://help.github.com/ignore-files/ for more about ignoring files. # dependencies /node_modules # testing /coverage # production /build # misc .DS_Store .env.local .env.development.local .env.test.local .env.production.local npm-debug.log* yarn-debug.log* yarn-error.log* ================================================ FILE: examples/react_demo/README.md ================================================ # JSONEditor React demo This project was bootstrapped with [Create React App](https://github.com/facebookincubator/create-react-app). ## Install Install dependencies once: ``` npm install ``` ## Run To run the demo: ``` npm start ``` This will open a development server at http://localhost:3000 ================================================ FILE: examples/react_demo/package.json ================================================ { "name": "react_demo", "version": "0.1.0", "private": true, "dependencies": { "jsoneditor": "latest", "react": "18.2.0", "react-dom": "18.2.0", "react-scripts": "5.0.1" }, "scripts": { "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test --env=jsdom", "eject": "react-scripts eject" }, "browserslist": { "production": [ ">0.2%", "not dead", "not op_mini all" ], "development": [ "last 1 chrome version", "last 1 firefox version", "last 1 safari version" ] } } ================================================ FILE: examples/react_demo/public/index.html ================================================ JSONEditor | React demo
================================================ FILE: examples/react_demo/src/App.css ================================================ .app .contents { width: 500px; height: 400px; } .app .contents .menu { padding: 10px 0; } .app .contents .code { background: #f5f5f5; } ================================================ FILE: examples/react_demo/src/App.js ================================================ import React, { Component } from 'react'; import JSONEditorDemo from './JSONEditorDemo'; import './App.css'; class App extends Component { state = { json: { 'array': [1, 2, 3], 'boolean': true, 'null': null, 'number': 123, 'object': {'a': 'b', 'c': 'd'}, 'string': 'Hello World' } }; render() { return (

JSONEditor React demo

              
                {JSON.stringify(this.state.json, null, 2)}
              
            
); } onChangeJSON = (json) => { this.setState({ json }); }; updateTime = () => { const time = new Date().toISOString(); this.setState({ json: Object.assign({}, this.state.json, { time }) }) }; } export default App; ================================================ FILE: examples/react_demo/src/App.test.js ================================================ import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; it('renders without crashing', () => { const div = document.createElement('div'); ReactDOM.render(, div); ReactDOM.unmountComponentAtNode(div); }); ================================================ FILE: examples/react_demo/src/JSONEditorDemo.css ================================================ .jsoneditor-react-container { width: 100%; height: 100%; } ================================================ FILE: examples/react_demo/src/JSONEditorDemo.js ================================================ import React, {Component} from 'react'; import JSONEditor from 'jsoneditor'; import 'jsoneditor/dist/jsoneditor.css'; import './JSONEditorDemo.css'; export default class JSONEditorDemo extends Component { componentDidMount () { const options = { mode: 'tree', onChangeJSON: this.props.onChangeJSON }; this.jsoneditor = new JSONEditor(this.container, options); this.jsoneditor.set(this.props.json); } componentWillUnmount () { if (this.jsoneditor) { this.jsoneditor.destroy(); } } componentDidUpdate() { this.jsoneditor.update(this.props.json); } render() { return (
this.container = elem} /> ); } } ================================================ FILE: examples/react_demo/src/index.css ================================================ body { font-family: sans-serif; } ================================================ FILE: examples/react_demo/src/index.js ================================================ import React from 'react' import { createRoot } from 'react-dom/client' import App from './App' import './index.css' const container = document.getElementById('root') const root = createRoot(container) root.render(); ================================================ FILE: examples/requirejs_demo/requirejs_demo.html ================================================ JSONEditor | Require.js demo

================================================ FILE: examples/requirejs_demo/scripts/main.js ================================================ const module = '../../../dist/jsoneditor' require([module], function (JSONEditor) { // create the editor const container = document.getElementById('jsoneditor') const editor = new JSONEditor(container) // set json document.getElementById('setJSON').onclick = function () { const json = { array: [1, 2, 3], boolean: true, null: null, number: 123, object: { a: 'b', c: 'd' }, string: 'Hello World' } editor.set(json) } // get json document.getElementById('getJSON').onclick = function () { const json = editor.get() window.alert(JSON.stringify(json, null, 2)) } }) ================================================ FILE: greenkeeper.json ================================================ { "groups": { "default": { "packages": [ "examples/react_advanced_demo/package.json", "examples/react_demo/package.json", "package.json" ] } } } ================================================ FILE: gulpfile.js ================================================ 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')(require('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, target: ['web', 'es5'], 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, target: ['web', 'es5'], output: { library: 'JSONEditor', libraryTarget: 'umd', path: DIST, filename: NAME_MINIMALIST + '.js' }, module: webpackConfigModule, plugins: [ bannerPlugin, new webpack.IgnorePlugin({ resourceRegExp: /^ace-builds/ }), new webpack.IgnorePlugin({ resourceRegExp: /worker-json-data-url/ }), new webpack.IgnorePlugin({ resourceRegExp: /^ajv/ }), new webpack.IgnorePlugin({ resourceRegExp: /^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({ [name + '.js']: 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) { const concatOptions = { rebaseUrls: false } const minifyOptions = { rebase: false } gulp .src(['src/scss/jsoneditor.scss']) .pipe( sass({ // importer: tildeImporter }) ) .pipe(concatCss(NAME + '.css', concatOptions)) .pipe(gulp.dest(DIST)) .pipe(concatCss(NAME + '.min.css', concatOptions)) .pipe(minifyCSS(minifyOptions)) .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') ) )) ================================================ FILE: index.js ================================================ module.exports = require('./dist/jsoneditor') ================================================ FILE: misc/how_to_publish.md ================================================ # How to publish jsoneditor This document describes the steps required to publish a new version of jsoneditor. ## Update version number Update the version number in package.json. Update package-lock.json: npm install ## Update history Update the date and version number in the file HISTORY.md. Verify whether all changes in the new version are described. ## Test the library Run the unit tests and validate whether all tests pass: npm test ## Build library Build the build (jsoneditor.js, jsoneditor.css, ...) files by running: npm run build-and-test After the build is complete, verify if the files are updated and contain the correct date and version number in the header. ## Test Test whether the npm library is ok by opening some examples, and check whether the files under `dists` are created and have contents. ## Commit - Commit the final code. - Merge the develop branch into the master branch. - Push to github. If everything is well, create a tag for the new version, like: git tag v1.2.4 git push --tags ## Publish Publish to npm: npm publish ## Test published library Install the libraries locally and test whether they work correctly: cd tmp-folder npm install jsoneditor ## Done Congrats, be proud. ================================================ FILE: package.json ================================================ { "name": "jsoneditor", "version": "10.4.2", "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 ", "license": "Apache-2.0", "homepage": "https://jsoneditoronline.org", "repository": { "type": "git", "url": "https://github.com/josdejong/jsoneditor.git" }, "bugs": "https://github.com/josdejong/jsoneditor/issues", "scripts": { "build": "gulp", "build-and-test": "npm run build && npm test && npm run lint", "minify": "gulp minify", "start": "gulp watch", "test": "mocha test --require @babel/register", "lint": "standard --env=mocha", "format": "standard --env=mocha --fix", "prepublishOnly": "npm run build-and-test" }, "dependencies": { "ace-builds": "^1.36.2", "ajv": "^6.12.6", "javascript-natural-sort": "^0.7.1", "jmespath": "^0.16.0", "json-source-map": "^0.6.1", "jsonrepair": "^3.8.1", "picomodal": "^3.0.0", "vanilla-picker": "^2.12.3" }, "devDependencies": { "@babel/core": "7.28.4", "@babel/preset-env": "7.28.3", "@babel/register": "7.28.3", "babel-loader": "10.0.0", "btoa": "1.2.1", "date-format": "4.0.14", "fancy-log": "2.0.0", "gulp": "5.0.1", "gulp-clean-css": "4.3.0", "gulp-concat-css": "3.1.0", "gulp-sass": "6.0.1", "jsdom": "27.0.0", "json-loader": "0.5.7", "mkdirp": "3.0.1", "mocha": "11.7.4", "sass": "1.93.2", "source-map-loader": "5.0.0", "standard": "17.1.2", "uglify-js": "3.19.3", "webpack": "5.105.0" }, "files": [ "dist", "docs", "examples", "src", "HISTORY.md", "index.js", "LICENSE", "NOTICE", "README.md" ], "standard": { "ignore": [ "src/js/assets", "examples/react*" ] } } ================================================ FILE: src/docs/which files do I need.md ================================================ # 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 ================================================ FILE: src/js/ContextMenu.js ================================================ '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. * {boolean} limitHeight Whether ContextMenu height should be * limited or not. * @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 this.limitHeight = options ? options.limitHeight : false // 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.} 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' } if (this.limitHeight) { const margin = 10 // make sure there is a little margin left const maxPossibleMenuHeight = showBelow ? frameRect.bottom - anchorRect.bottom - margin : anchorRect.top - frameRect.top - margin this.dom.list.style.maxHeight = maxPossibleMenuHeight + 'px' this.dom.list.style.overflowY = 'auto' } // 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 ================================================ FILE: src/js/ErrorTable.js ================================================ /** * 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.replace(/
/gi, '\n'))) 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' } } } ================================================ FILE: src/js/FocusTracker.js ================================================ '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._onBlur = this._onEvent.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) document.addEventListener('blur', this._onBlur) } } /** * 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) document.removeEventListener('blur', this._onBlur) 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 } } } } } ================================================ FILE: src/js/Highlighter.js ================================================ '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 } } ================================================ FILE: src/js/History.js ================================================ /** * 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() } } ================================================ FILE: src/js/JSONEditor.js ================================================ '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. } */ 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', 'onExpand', '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', 'allowSchemaSuggestions', 'showErrorTable' ] /** * 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 options.mode = mode const config = JSONEditor.modes[mode] if (!config) { throw new Error('Unknown mode "' + options.mode + '"') } const asText = (config.data === 'text') const name = this.getName() const 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) } } } /** * 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 basic alert window with the error message * will be shown to the user. * @param {Error} err * @private */ JSONEditor.prototype._onError = function (err) { if (this.options && typeof this.options.onError === 'function') { this.options.onError(err) } else { window.alert(err.toString()) } } /** * Set a JSON schema for validation of the JSON object. * To remove the schema, call JSONEditor.setSchema(null) * @param {Object | null} schema * @param {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 this.options.schemaRefs = schemaRefs // 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 } if (typeof this._onSchemaChange === 'function') { this._onSchemaChange(schema, schemaRefs) } } /** * 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 return Promise.resolve([]) } /** * 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 ================================================ FILE: src/js/ModeSwitcher.js ================================================ '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, box, frame } } /** * Set focus to switcher */ focus () { this.dom.box.focus() } /** * Destroy the ModeSwitcher, remove from DOM */ destroy () { if (this.dom && this.dom.frame && this.dom.frame.parentNode) { this.dom.frame.parentNode.removeChild(this.dom.frame) } this.dom = null } } ================================================ FILE: src/js/Node.js ================================================ 'use strict' import naturalSort from 'javascript-natural-sort' import { createAbsoluteAnchor } from './createAbsoluteAnchor' import { ContextMenu } from './ContextMenu' import { appendNodeFactory } from './appendNodeFactory' import { showMoreNodeFactory } from './showMoreNodeFactory' import { showSortModal } from './showSortModal' import { showTransformModal } from './showTransformModal' import { addClassName, addEventListener, debounce, escapeUnicodeChars, findUniqueName, getAbsoluteLeft, getAbsoluteTop, getInnerText, getType, isTimestamp, isUrl, isValidColor, makeFieldTooltip, parse, parsePath, parseString, removeAllClassNames, removeClassName, removeEventListener, selectContentEditable, setEndOfContentEditable, stripFormatting, textDiff } from './util' import { translate } from './i18n' import { DEFAULT_MODAL_ANCHOR } from './constants' /** * @constructor Node * Create a new Node * @param {./treemode} editor * @param {Object} [params] Can contain parameters: * {string} field * {boolean} fieldEditable * {*} value * {String} type Can have values 'auto', 'array', * 'object', or 'string'. */ export class Node { constructor (editor, params) { /** @type {./treemode} */ this.editor = editor this.dom = {} this.expanded = false if (params && (params instanceof Object)) { this.setField(params.field, params.fieldEditable) if ('value' in params) { this.setValue(params.value, params.type) } if ('internalValue' in params) { this.setInternalValue(params.internalValue) } } else { this.setField('') this.setValue(null) } this._debouncedOnChangeValue = debounce(this._onChangeValue.bind(this), Node.prototype.DEBOUNCE_INTERVAL) this._debouncedOnChangeField = debounce(this._onChangeField.bind(this), Node.prototype.DEBOUNCE_INTERVAL) // starting value for visible children this.visibleChilds = this.getMaxVisibleChilds() } getMaxVisibleChilds () { return (this.editor && this.editor.options && this.editor.options.maxVisibleChilds) ? this.editor.options.maxVisibleChilds : DEFAULT_MAX_VISIBLE_CHILDS } /** * Determine whether the field and/or value of this node are editable * @private */ _updateEditability () { this.editable = { field: true, value: true } if (this.editor) { this.editable.field = this.editor.options.mode === 'tree' this.editable.value = this.editor.options.mode !== 'view' if ((this.editor.options.mode === 'tree' || this.editor.options.mode === 'form') && (typeof this.editor.options.onEditable === 'function')) { const getValue = this.getValue.bind(this) const editable = this.editor.options.onEditable({ field: this.field, get value () { return getValue() }, path: this.getPath() }) if (typeof editable === 'boolean') { this.editable.field = editable this.editable.value = editable } else if (typeof editable === 'object' && editable !== null) { if (typeof editable.field === 'boolean') this.editable.field = editable.field if (typeof editable.value === 'boolean') this.editable.value = editable.value } else { console.error( 'Invalid return value for function onEditable.', 'Actual value:', editable, '.', 'Either a boolean or object { field: boolean, value: boolean } expected.') this.editable.field = false this.editable.value = false } } } } /** * Get the path of this node * @return {{string|number}[]} Array containing the path to this node. * Element is a number if is the index of an array, a string otherwise. */ getPath () { let node = this const path = [] while (node) { const field = node.getName() if (field !== undefined) { path.unshift(field) } node = node.parent } return path } /** * Get the internal path of this node, a list with the child indexes. * @return {String[]} Array containing the internal path to this node */ getInternalPath () { let node = this const internalPath = [] while (node) { if (node.parent) { internalPath.unshift(node.getIndex()) } node = node.parent } return internalPath } /** * Get node serializable name * @returns {String|Number} */ getName () { return !this.parent ? undefined // do not add an (optional) field name of the root node : (this.parent.type !== 'array') ? this.field : this.index } /** * Find child node by serializable path * @param {Array} path */ findNodeByPath (path) { if (!path) { return } if (path.length === 0) { return this } if (path.length && this.childs && this.childs.length) { for (let i = 0; i < this.childs.length; ++i) { if (('' + path[0]) === ('' + this.childs[i].getName())) { return this.childs[i].findNodeByPath(path.slice(1)) } } } } /** * Find child node by an internal path: the indexes of the childs nodes * @param {Array} internalPath * @return {Node | undefined} Returns the node if the path exists. * Returns undefined otherwise. */ findNodeByInternalPath (internalPath) { if (!internalPath) { return undefined } let node = this for (let i = 0; i < internalPath.length && node; i++) { const childIndex = internalPath[i] node = node.childs[childIndex] } return node } /** * @typedef {{value: String|Object|Number|Boolean, path: Array.}} SerializableNode * * Returns serializable representation for the node * @return {SerializableNode} */ serialize () { return { value: this.getValue(), path: this.getPath() } } /** * Find a Node from a JSON path like '.items[3].name' * @param {string} jsonPath * @return {Node | null} Returns the Node when found, returns null if not found */ findNode (jsonPath) { const path = parsePath(jsonPath) let node = this while (node && path.length > 0) { const prop = path.shift() if (typeof prop === 'number') { if (node.type !== 'array') { throw new Error('Cannot get child node at index ' + prop + ': node is no array') } node = node.childs[prop] } else { // string if (node.type !== 'object') { throw new Error('Cannot get child node ' + prop + ': node is no object') } node = node.childs.filter(child => child.field === prop)[0] } } return node } /** * Find all parents of this node. The parents are ordered from root node towards * the original node. * @return {Array.} */ findParents () { const parents = [] let parent = this.parent while (parent) { parents.unshift(parent) parent = parent.parent } return parents } /** * * @param {{dataPath: string, keyword: string, message: string, params: Object, schemaPath: string} | null} error * @param {Node} [child] When this is the error of a parent node, pointing * to an invalid child node, the child node itself * can be provided. If provided, clicking the error * icon will set focus to the invalid child node. */ setError (error, child) { this.error = error this.errorChild = child if (this.dom && this.dom.tr) { this.updateError() } } /** * Render the error */ updateError () { const error = this.fieldError || this.valueError || this.error let tdError = this.dom.tdError if (error && this.dom && this.dom.tr) { addClassName(this.dom.tr, 'jsoneditor-validation-error') if (!tdError) { tdError = document.createElement('td') this.dom.tdError = tdError this.dom.tdValue.parentNode.appendChild(tdError) } const button = document.createElement('button') button.type = 'button' button.className = 'jsoneditor-button jsoneditor-schema-error' const destroy = () => { if (this.dom.popupAnchor) { this.dom.popupAnchor.destroy() // this will trigger the onDestroy callback } } const onDestroy = () => { delete this.dom.popupAnchor } const createPopup = (destroyOnMouseOut) => { const frame = this.editor.frame this.dom.popupAnchor = createAbsoluteAnchor(button, this.editor.getPopupAnchor(), onDestroy, destroyOnMouseOut) const popupWidth = 200 // must correspond to what's configured in the CSS const buttonRect = button.getBoundingClientRect() const frameRect = frame.getBoundingClientRect() const position = (frameRect.width - buttonRect.x > (popupWidth / 2 + 20)) ? 'jsoneditor-above' : 'jsoneditor-left' const popover = document.createElement('div') popover.className = 'jsoneditor-popover ' + position popover.appendChild(document.createTextNode(error.message)) this.dom.popupAnchor.appendChild(popover) } button.onmouseover = () => { if (!this.dom.popupAnchor) { createPopup(true) } } button.onfocus = () => { destroy() createPopup(false) } button.onblur = () => { destroy() } // when clicking the error icon, expand all nodes towards the invalid // child node, and set focus to the child node const child = this.errorChild if (child) { button.onclick = function showInvalidNode () { child.findParents().forEach(parent => { parent.expand(false) }) child.scrollTo(() => { child.focus() }) } } // apply the error message to the node while (tdError.firstChild) { tdError.removeChild(tdError.firstChild) } tdError.appendChild(button) } else { if (this.dom.tr) { removeClassName(this.dom.tr, 'jsoneditor-validation-error') } if (tdError) { this.dom.tdError.parentNode.removeChild(this.dom.tdError) delete this.dom.tdError } } } /** * Get the index of this node: the index in the list of childs where this * node is part of * @return {number | null} Returns the index, or null if this is the root node */ getIndex () { if (this.parent) { const index = this.parent.childs.indexOf(this) return index !== -1 ? index : null } else { return -1 } } /** * Set parent node * @param {Node} parent */ setParent (parent) { this.parent = parent } /** * Set field * @param {String} field * @param {boolean} [fieldEditable] */ setField (field, fieldEditable) { this.field = field this.previousField = field this.fieldEditable = (fieldEditable === true) } /** * Get field * @return {String} */ getField () { if (this.field === undefined) { this._getDomField() } return this.field } /** * Set value. Value is a JSON structure or an element String, Boolean, etc. * @param {*} value * @param {String} [type] Specify the type of the value. Can be 'auto', * 'array', 'object', or 'string' */ setValue (value, type) { let childValue, child let i, j const updateDom = false const previousChilds = this.childs this.type = this._getType(value) // check if type corresponds with the provided type if (type && type !== this.type) { if (type === 'string' && this.type === 'auto') { this.type = type } else { throw new Error('Type mismatch: ' + 'cannot cast value of type "' + this.type + ' to the specified type "' + type + '"') } } if (this.type === 'array') { // array if (!this.childs) { this.childs = [] } for (i = 0; i < value.length; i++) { childValue = value[i] if (childValue !== undefined && !(childValue instanceof Function)) { if (i < this.childs.length) { // reuse existing child, keep its state child = this.childs[i] child.fieldEditable = false child.index = i child.setValue(childValue) } else { // create a new child child = new Node(this.editor, { value: childValue }) const visible = i < this.getMaxVisibleChilds() this.appendChild(child, visible, updateDom) } } } // cleanup redundant childs // we loop backward to prevent issues with shifting index numbers for (j = this.childs.length; j >= value.length; j--) { this.removeChild(this.childs[j], updateDom) } } else if (this.type === 'object') { // object if (!this.childs) { this.childs = [] } // cleanup redundant childs // we loop backward to prevent issues with shifting index numbers for (j = this.childs.length - 1; j >= 0; j--) { if (!hasOwnProperty(value, this.childs[j].field)) { this.removeChild(this.childs[j], updateDom) } } i = 0 for (const childField in value) { if (hasOwnProperty(value, childField)) { childValue = value[childField] if (childValue !== undefined && !(childValue instanceof Function)) { const child = this.findChildByProperty(childField) if (child) { // reuse existing child, keep its state child.setField(childField, true) child.setValue(childValue) } else { // create a new child, append to the end const newChild = new Node(this.editor, { field: childField, value: childValue }) const visible = i < this.getMaxVisibleChilds() this.appendChild(newChild, visible, updateDom) } } i++ } } this.value = '' // sort object keys during initialization. Must not trigger an onChange action if (this.editor.options.sortObjectKeys === true) { const triggerAction = false this.sort([], 'asc', triggerAction) } } else { // value this.hideChilds() delete this.append delete this.showMore delete this.expanded delete this.childs this.value = value } // recreate the DOM if switching from an object/array to auto/string or vice versa // needed to recreated the expand button for example if (Array.isArray(previousChilds) !== Array.isArray(this.childs)) { this.recreateDom() } this.updateDom({ updateIndexes: true }) this.previousValue = this.value // used only to check for changes in DOM vs JS model } /** * Set internal value * @param {*} internalValue Internal value structure keeping type, * order and duplicates in objects */ setInternalValue (internalValue) { let childValue, child, visible let i, j const notUpdateDom = false const previousChilds = this.childs this.type = internalValue.type if (internalValue.type === 'array') { // array if (!this.childs) { this.childs = [] } for (i = 0; i < internalValue.childs.length; i++) { childValue = internalValue.childs[i] if (childValue !== undefined && !(childValue instanceof Function)) { if (i < this.childs.length) { // reuse existing child, keep its state child = this.childs[i] child.fieldEditable = false child.index = i child.setInternalValue(childValue) } else { // create a new child child = new Node(this.editor, { internalValue: childValue }) visible = i < this.getMaxVisibleChilds() this.appendChild(child, visible, notUpdateDom) } } } // cleanup redundant childs // we loop backward to prevent issues with shifting index numbers for (j = this.childs.length; j >= internalValue.childs.length; j--) { this.removeChild(this.childs[j], notUpdateDom) } } else if (internalValue.type === 'object') { // object if (!this.childs) { this.childs = [] } for (i = 0; i < internalValue.childs.length; i++) { childValue = internalValue.childs[i] if (childValue !== undefined && !(childValue instanceof Function)) { if (i < this.childs.length) { // reuse existing child, keep its state child = this.childs[i] delete child.index child.setField(childValue.field, true) child.setInternalValue(childValue.value) } else { // create a new child child = new Node(this.editor, { field: childValue.field, internalValue: childValue.value }) visible = i < this.getMaxVisibleChilds() this.appendChild(child, visible, notUpdateDom) } } } // cleanup redundant childs // we loop backward to prevent issues with shifting index numbers for (j = this.childs.length; j >= internalValue.childs.length; j--) { this.removeChild(this.childs[j], notUpdateDom) } } else { // value this.hideChilds() delete this.append delete this.showMore delete this.expanded delete this.childs this.value = internalValue.value } // recreate the DOM if switching from an object/array to auto/string or vice versa // needed to recreated the expand button for example if (Array.isArray(previousChilds) !== Array.isArray(this.childs)) { this.recreateDom() } this.updateDom({ updateIndexes: true }) this.previousValue = this.value // used only to check for changes in DOM vs JS model } /** * Remove the DOM of this node and it's childs and recreate it again */ recreateDom () { if (this.dom && this.dom.tr && this.dom.tr.parentNode) { const domAnchor = this._detachFromDom() this.clearDom() this._attachToDom(domAnchor) } else { this.clearDom() } } /** * Get value. Value is a JSON structure * @return {*} value */ getValue () { if (this.type === 'array') { const arr = [] this.childs.forEach(child => { arr.push(child.getValue()) }) return arr } else if (this.type === 'object') { const obj = {} this.childs.forEach(child => { obj[child.getField()] = child.getValue() }) return obj } else { if (this.value === undefined) { this._getDomValue() } return this.value } } /** * Get internal value, a structure which maintains ordering and duplicates in objects * @return {*} value */ getInternalValue () { if (this.type === 'array') { return { type: this.type, childs: this.childs.map(child => child.getInternalValue()) } } else if (this.type === 'object') { return { type: this.type, childs: this.childs.map(child => ({ field: child.getField(), value: child.getInternalValue() })) } } else { if (this.value === undefined) { this._getDomValue() } return { type: this.type, value: this.value } } } /** * Get the nesting level of this node * @return {Number} level */ getLevel () { return (this.parent ? this.parent.getLevel() + 1 : 0) } /** * Get jsonpath of the current node * @return {Node[]} Returns an array with nodes */ getNodePath () { const path = this.parent ? this.parent.getNodePath() : [] path.push(this) return path } /** * Create a clone of a node * The complete state of a clone is copied, including whether it is expanded or * not. The DOM elements are not cloned. * @return {Node} clone */ clone () { const clone = new Node(this.editor) clone.type = this.type clone.field = this.field clone.fieldInnerText = this.fieldInnerText clone.fieldEditable = this.fieldEditable clone.previousField = this.previousField clone.value = this.value clone.valueInnerText = this.valueInnerText clone.previousValue = this.previousValue clone.expanded = this.expanded clone.visibleChilds = this.visibleChilds if (this.childs) { // an object or array const cloneChilds = [] this.childs.forEach(child => { const childClone = child.clone() childClone.setParent(clone) cloneChilds.push(childClone) }) clone.childs = cloneChilds } else { // a value clone.childs = undefined } return clone } /** * Expand this node and optionally its childs. * @param {boolean} [recurse] Optional recursion, true by default. When * true, all childs will be expanded recursively */ expand (recurse) { if (!this.childs) { return } // set this node expanded this.expanded = true if (this.dom.expand) { this.dom.expand.className = 'jsoneditor-button jsoneditor-expanded' } this.showChilds() if (recurse !== false) { this.childs.forEach(child => { child.expand(recurse) }) } // update the css classes of table row, and fire onClassName etc this.updateDom({ recurse: false }) } /** * Collapse this node and optionally its childs. * @param {boolean} [recurse] Optional recursion, true by default. When * true, all childs will be collapsed recursively */ collapse (recurse) { if (!this.childs) { return } this.hideChilds() // collapse childs in case of recurse if (recurse !== false) { this.childs.forEach(child => { child.collapse(recurse) }) } // make this node collapsed if (this.dom.expand) { this.dom.expand.className = 'jsoneditor-button jsoneditor-collapsed' } this.expanded = false // update the css classes of table row, and fire onClassName etc this.updateDom({ recurse: false }) } /** * Recursively show all childs when they are expanded */ showChilds () { const childs = this.childs if (!childs) { return } if (!this.expanded) { return } const tr = this.dom.tr let nextTr const table = tr ? tr.parentNode : undefined if (table) { // show row with append button const append = this.getAppendDom() if (!append.parentNode) { nextTr = tr.nextSibling if (nextTr) { table.insertBefore(append, nextTr) } else { table.appendChild(append) } } // show childs const iMax = Math.min(this.childs.length, this.visibleChilds) nextTr = this._getNextTr() for (let i = 0; i < iMax; i++) { const child = this.childs[i] if (!child.getDom().parentNode) { table.insertBefore(child.getDom(), nextTr) } child.showChilds() } // show "show more childs" if limited const showMore = this.getShowMoreDom() nextTr = this._getNextTr() if (!showMore.parentNode) { table.insertBefore(showMore, nextTr) } this.showMore.updateDom() // to update the counter } } _getNextTr () { if (this.showMore && this.showMore.getDom().parentNode) { return this.showMore.getDom() } if (this.append && this.append.getDom().parentNode) { return this.append.getDom() } } /** * Hide the node with all its childs * @param {{resetVisibleChilds: boolean}} [options] */ hide (options) { const tr = this.dom.tr const table = tr ? tr.parentNode : undefined if (table) { table.removeChild(tr) } if (this.dom.popupAnchor) { this.dom.popupAnchor.destroy() } this.hideChilds(options) } /** * Recursively hide all childs * @param {{resetVisibleChilds: boolean}} [options] */ hideChilds (options) { const childs = this.childs if (!childs) { return } if (!this.expanded) { return } // hide append row const append = this.getAppendDom() if (append.parentNode) { append.parentNode.removeChild(append) } // hide childs this.childs.forEach(child => { child.hide() }) // hide "show more" row const showMore = this.getShowMoreDom() if (showMore.parentNode) { showMore.parentNode.removeChild(showMore) } // reset max visible childs if (!options || options.resetVisibleChilds) { this.visibleChilds = this.getMaxVisibleChilds() } } /** * set custom css classes on a node */ _updateCssClassName () { if (this.dom.field && this.editor && this.editor.options && typeof this.editor.options.onClassName === 'function' && this.dom.tree) { removeAllClassNames(this.dom.tree) const getValue = this.getValue.bind(this) const addClasses = this.editor.options.onClassName({ path: this.getPath(), field: this.field, get value () { return getValue() } }) || '' addClassName(this.dom.tree, 'jsoneditor-values ' + addClasses) } } recursivelyUpdateCssClassesOnNodes () { this._updateCssClassName() if (Array.isArray(this.childs)) { for (let i = 0; i < this.childs.length; i++) { this.childs[i].recursivelyUpdateCssClassesOnNodes() } } } /** * Goes through the path from the node to the root and ensures that it is expanded */ expandTo () { let currentNode = this.parent while (currentNode) { if (!currentNode.expanded) { currentNode.expand() } currentNode = currentNode.parent } } /** * Add a new child to the node. * Only applicable when Node value is of type array or object * @param {Node} node * @param {boolean} [visible] If true (default), the child will be rendered * @param {boolean} [updateDom] If true (default), the DOM of both parent * node and appended node will be updated * (child count, indexes) */ appendChild (node, visible, updateDom) { if (this._hasChilds()) { // adjust the link to the parent node.setParent(this) node.fieldEditable = (this.type === 'object') if (this.type === 'array') { node.index = this.childs.length } if (this.type === 'object' && node.field === undefined) { // initialize field value if needed node.setField('') } this.childs.push(node) if (this.expanded && visible !== false) { // insert into the DOM, before the appendRow const newTr = node.getDom() const nextTr = this._getNextTr() const table = nextTr ? nextTr.parentNode : undefined if (nextTr && table) { table.insertBefore(newTr, nextTr) } node.showChilds() this.visibleChilds++ } if (updateDom !== false) { this.updateDom({ updateIndexes: true }) node.updateDom({ recurse: true }) } } } /** * Move a node from its current parent to this node * Only applicable when Node value is of type array or object * @param {Node} node * @param {Node} beforeNode * @param {boolean} [updateDom] If true (default), the DOM of both parent * node and appended node will be updated * (child count, indexes) */ moveBefore (node, beforeNode, updateDom) { if (this._hasChilds()) { // create a temporary row, to prevent the scroll position from jumping // when removing the node const tbody = (this.dom.tr) ? this.dom.tr.parentNode : undefined let trTemp if (tbody) { trTemp = document.createElement('tr') trTemp.style.height = tbody.clientHeight + 'px' tbody.appendChild(trTemp) } if (node.parent) { node.parent.removeChild(node) } if (beforeNode instanceof AppendNode || !beforeNode) { // the this.childs.length + 1 is to reckon with the node that we're about to add if (this.childs.length + 1 > this.visibleChilds) { const lastVisibleNode = this.childs[this.visibleChilds - 1] this.insertBefore(node, lastVisibleNode, updateDom) } else { const visible = true this.appendChild(node, visible, updateDom) } } else { this.insertBefore(node, beforeNode, updateDom) } if (tbody && trTemp) { tbody.removeChild(trTemp) } } } /** * Insert a new child before a given node * Only applicable when Node value is of type array or object * @param {Node} node * @param {Node} beforeNode * @param {boolean} [updateDom] If true (default), the DOM of both parent * node and appended node will be updated * (child count, indexes) */ insertBefore (node, beforeNode, updateDom) { if (this._hasChilds()) { this.visibleChilds++ // initialize field value if needed if (this.type === 'object' && node.field === undefined) { node.setField('') } if (beforeNode === this.append) { // append to the child nodes // adjust the link to the parent node.setParent(this) node.fieldEditable = (this.type === 'object') this.childs.push(node) } else { // insert before a child node const index = this.childs.indexOf(beforeNode) if (index === -1) { throw new Error('Node not found') } // adjust the link to the parent node.setParent(this) node.fieldEditable = (this.type === 'object') this.childs.splice(index, 0, node) } if (this.expanded) { // insert into the DOM const newTr = node.getDom() const nextTr = beforeNode.getDom() const table = nextTr ? nextTr.parentNode : undefined if (nextTr && table) { table.insertBefore(newTr, nextTr) } node.showChilds() this.showChilds() } if (updateDom !== false) { this.updateDom({ updateIndexes: true }) node.updateDom({ recurse: true }) } } } /** * Insert a new child before a given node * Only applicable when Node value is of type array or object * @param {Node} node * @param {Node} afterNode */ insertAfter (node, afterNode) { if (this._hasChilds()) { const index = this.childs.indexOf(afterNode) const beforeNode = this.childs[index + 1] if (beforeNode) { this.insertBefore(node, beforeNode) } else { this.appendChild(node) } } } /** * Search in this node * Searches are case insensitive. * @param {String} text * @param {Node[]} [results] Array where search results will be added * used to count and limit the results whilst iterating * @return {Node[]} results Array with nodes containing the search text */ search (text, results) { if (!Array.isArray(results)) { results = [] } let index const search = text ? text.toLowerCase() : undefined // delete old search data delete this.searchField delete this.searchValue // search in field if (this.field !== undefined && results.length <= this.MAX_SEARCH_RESULTS) { const field = String(this.field).toLowerCase() index = field.indexOf(search) if (index !== -1) { this.searchField = true results.push({ node: this, elem: 'field' }) } // update dom this._updateDomField() } // search in value if (this._hasChilds()) { // array, object // search the nodes childs if (this.childs) { this.childs.forEach(child => { child.search(text, results) }) } } else { // string, auto if (this.value !== undefined && results.length <= this.MAX_SEARCH_RESULTS) { const value = String(this.value).toLowerCase() index = value.indexOf(search) if (index !== -1) { this.searchValue = true results.push({ node: this, elem: 'value' }) } // update dom this._updateDomValue() } } return results } /** * Move the scroll position such that this node is in the visible area. * The node will not get the focus * @param {function(boolean)} [callback] */ scrollTo (callback) { this.expandPathToNode() if (this.dom.tr && this.dom.tr.parentNode) { this.editor.scrollTo(this.dom.tr.offsetTop, callback) } } /** * if the node is not visible, expand its parents */ expandPathToNode () { let node = this const recurse = false while (node && node.parent) { // expand visible childs of the parent if needed const index = node.parent.type === 'array' ? node.index : node.parent.childs.indexOf(node) while (node.parent.visibleChilds < index + 1) { node.parent.visibleChilds += this.getMaxVisibleChilds() } // expand the parent itself node.parent.expand(recurse) node = node.parent } } /** * Set focus to this node * @param {String} [elementName] The field name of the element to get the * focus available values: 'drag', 'menu', * 'expand', 'field', 'value' (default) */ focus (elementName) { Node.focusElement = elementName if (this.dom.tr && this.dom.tr.parentNode) { const dom = this.dom switch (elementName) { case 'drag': if (dom.drag) { dom.drag.focus() } else { dom.menu.focus() } break case 'menu': dom.menu.focus() break case 'expand': if (this._hasChilds()) { dom.expand.focus() } else if (dom.field && this.fieldEditable) { dom.field.focus() selectContentEditable(dom.field) } else if (dom.value && !this._hasChilds()) { dom.value.focus() selectContentEditable(dom.value) } else { dom.menu.focus() } break case 'field': if (dom.field && this.fieldEditable) { dom.field.focus() selectContentEditable(dom.field) } else if (dom.value && !this._hasChilds()) { dom.value.focus() selectContentEditable(dom.value) } else if (this._hasChilds()) { dom.expand.focus() } else { dom.menu.focus() } break case 'value': default: if (dom.select) { // enum select box dom.select.focus() } else if (dom.value && !this._hasChilds()) { dom.value.focus() selectContentEditable(dom.value) } else if (dom.field && this.fieldEditable) { dom.field.focus() selectContentEditable(dom.field) } else if (this._hasChilds()) { dom.expand.focus() } else { dom.menu.focus() } break } } } /** * Check if given node is a child. The method will check recursively to find * this node. * @param {Node} node * @return {boolean} containsNode */ containsNode (node) { if (this === node) { return true } const childs = this.childs if (childs) { // TODO: use the js5 Array.some() here? for (let i = 0, iMax = childs.length; i < iMax; i++) { if (childs[i].containsNode(node)) { return true } } } return false } /** * Remove a child from the node. * Only applicable when Node value is of type array or object * @param {Node} node The child node to be removed; * @param {boolean} [updateDom] If true (default), the DOM of the parent * node will be updated (like child count) * @return {Node | undefined} node The removed node on success, * else undefined */ removeChild (node, updateDom) { if (this.childs) { const index = this.childs.indexOf(node) if (index !== -1) { if (index < this.visibleChilds && this.expanded) { this.visibleChilds-- } node.hide() // delete old search results delete node.searchField delete node.searchValue const removedNode = this.childs.splice(index, 1)[0] removedNode.parent = null if (updateDom !== false) { this.updateDom({ updateIndexes: true }) } return removedNode } } return undefined } /** * Remove a child node node from this node * This method is equal to Node.removeChild, except that _remove fire an * onChange event. * @param {Node} node * @private */ _remove (node) { this.removeChild(node) } /** * Change the type of the value of this Node * @param {String} newType */ changeType (newType) { const oldType = this.type if (oldType === newType) { // type is not changed return } if ((newType === 'string' || newType === 'auto') && (oldType === 'string' || oldType === 'auto')) { // this is an easy change this.type = newType } else { // change from array to object, or from string/auto to object/array const domAnchor = this._detachFromDom() // delete the old DOM this.clearDom() // adjust the field and the value this.type = newType // adjust childs if (newType === 'object') { if (!this.childs) { this.childs = [] } this.childs.forEach(child => { child.clearDom() delete child.index child.fieldEditable = true if (child.field === undefined) { child.field = '' } }) if (oldType === 'string' || oldType === 'auto') { this.expanded = true } } else if (newType === 'array') { if (!this.childs) { this.childs = [] } this.childs.forEach((child, index) => { child.clearDom() child.fieldEditable = false child.index = index }) if (oldType === 'string' || oldType === 'auto') { this.expanded = true } } else { this.expanded = false } this._attachToDom(domAnchor) } if (newType === 'auto' || newType === 'string') { // cast value to the correct type if (newType === 'string') { this.value = String(this.value) } else { this.value = parseString(String(this.value)) } this.focus() } this.updateDom({ updateIndexes: true }) } /** * Test whether the JSON contents of this node are deep equal to provided JSON object. * @param {*} json */ deepEqual (json) { let i if (this.type === 'array') { if (!Array.isArray(json)) { return false } if (this.childs.length !== json.length) { return false } for (i = 0; i < this.childs.length; i++) { if (!this.childs[i].deepEqual(json[i])) { return false } } } else if (this.type === 'object') { if (typeof json !== 'object' || !json) { return false } // we reckon with the order of the properties too. const props = Object.keys(json) if (this.childs.length !== props.length) { return false } for (i = 0; i < props.length; i++) { const child = this.childs[i] if (child.field !== props[i] || !child.deepEqual(json[child.field])) { return false } } } else { if (this.value !== json) { return false } } return true } /** * Retrieve value from DOM * @private */ _getDomValue () { this._clearValueError() if (this.dom.value && this.type !== 'array' && this.type !== 'object') { this.valueInnerText = getInnerText(this.dom.value) if (this.valueInnerText === '' && this.dom.value.innerHTML !== '') { // When clearing the contents, often a
remains, messing up the // styling of the empty text box. Therefore we remove the
this.dom.value.textContent = '' } } if (this.valueInnerText !== undefined) { try { // retrieve the value let value if (this.type === 'string') { value = this._unescapeHTML(this.valueInnerText) } else { const str = this._unescapeHTML(this.valueInnerText) value = parseString(str) } if (value !== this.value) { this.value = value this._debouncedOnChangeValue() } } catch (err) { // keep the previous value this._setValueError(translate('cannotParseValueError')) } } } /** * Show a local error in case of invalid value * @param {string} message * @private */ _setValueError (message) { this.valueError = { message } this.updateError() } _clearValueError () { if (this.valueError) { this.valueError = null this.updateError() } } /** * Show a local error in case of invalid or duplicate field * @param {string} message * @private */ _setFieldError (message) { this.fieldError = { message } this.updateError() } _clearFieldError () { if (this.fieldError) { this.fieldError = null this.updateError() } } /** * Handle a changed value * @private */ _onChangeValue () { // get current selection, then override the range such that we can select // the added/removed text on undo/redo const oldSelection = this.editor.getDomSelection() if (oldSelection.range) { const undoDiff = textDiff(String(this.value), String(this.previousValue)) oldSelection.range.startOffset = undoDiff.start oldSelection.range.endOffset = undoDiff.end } const newSelection = this.editor.getDomSelection() if (newSelection.range) { const redoDiff = textDiff(String(this.previousValue), String(this.value)) newSelection.range.startOffset = redoDiff.start newSelection.range.endOffset = redoDiff.end } this.editor._onAction('editValue', { path: this.getInternalPath(), oldValue: this.previousValue, newValue: this.value, oldSelection, newSelection }) this.previousValue = this.value } /** * Handle a changed field * @private */ _onChangeField () { // get current selection, then override the range such that we can select // the added/removed text on undo/redo const oldSelection = this.editor.getDomSelection() const previous = this.previousField || '' if (oldSelection.range) { const undoDiff = textDiff(this.field, previous) oldSelection.range.startOffset = undoDiff.start oldSelection.range.endOffset = undoDiff.end } const newSelection = this.editor.getDomSelection() if (newSelection.range) { const redoDiff = textDiff(previous, this.field) newSelection.range.startOffset = redoDiff.start newSelection.range.endOffset = redoDiff.end } this.editor._onAction('editField', { parentPath: this.parent.getInternalPath(), index: this.getIndex(), oldValue: this.previousField, newValue: this.field, oldSelection, newSelection }) this.previousField = this.field } /** * Update dom value: * - the text color of the value, depending on the type of the value * - the height of the field, depending on the width * - background color in case it is empty * @private */ _updateDomValue () { const domValue = this.dom.value if (domValue) { const classNames = ['jsoneditor-value'] // set text color depending on value type const value = this.value const valueType = (this.type === 'auto') ? getType(value) : this.type const valueIsUrl = valueType === 'string' && isUrl(value) classNames.push('jsoneditor-' + valueType) if (valueIsUrl) { classNames.push('jsoneditor-url') } // visual styling when empty const isEmpty = (String(this.value) === '' && this.type !== 'array' && this.type !== 'object') if (isEmpty) { classNames.push('jsoneditor-empty') } // highlight when there is a search result if (this.searchValueActive) { classNames.push('jsoneditor-highlight-active') } if (this.searchValue) { classNames.push('jsoneditor-highlight') } domValue.className = classNames.join(' ') // update title if (valueType === 'array' || valueType === 'object') { const count = this.childs ? this.childs.length : 0 domValue.title = this.type + ' containing ' + count + ' items' } else if (valueIsUrl && this.editable.value) { domValue.title = translate('openUrl') } else { domValue.title = '' } // show checkbox when the value is a boolean if (valueType === 'boolean' && this.editable.value) { if (!this.dom.checkbox) { this.dom.checkbox = document.createElement('input') this.dom.checkbox.type = 'checkbox' this.dom.tdCheckbox = document.createElement('td') this.dom.tdCheckbox.className = 'jsoneditor-tree' this.dom.tdCheckbox.appendChild(this.dom.checkbox) this.dom.tdValue.parentNode.insertBefore(this.dom.tdCheckbox, this.dom.tdValue) } this.dom.checkbox.checked = this.value } else { // cleanup checkbox when displayed if (this.dom.tdCheckbox) { this.dom.tdCheckbox.parentNode.removeChild(this.dom.tdCheckbox) delete this.dom.tdCheckbox delete this.dom.checkbox } } // create select box when this node has an enum object if (this.enum && this.editable.value) { if (!this.dom.select) { this.dom.select = document.createElement('select') this.id = this.field + '_' + new Date().getUTCMilliseconds() this.dom.select.id = this.id this.dom.select.name = this.dom.select.id // Create the default empty option const defaultOption = document.createElement('option') defaultOption.value = '' defaultOption.textContent = '--' this.dom.select.appendChild(defaultOption) // Iterate all enum values and add them as options this._updateEnumOptions() this.dom.tdSelect = document.createElement('td') this.dom.tdSelect.className = 'jsoneditor-tree' this.dom.tdSelect.appendChild(this.dom.select) this.dom.tdValue.parentNode.insertBefore(this.dom.tdSelect, this.dom.tdValue) } // Select the matching value this.dom.select.value = (this.enum.indexOf(this.value) !== -1) ? this.value : '' // default // If the enum is inside a composite type display // both the simple input and the dropdown field if (this.schema && ( !hasOwnProperty(this.schema, 'oneOf') && !hasOwnProperty(this.schema, 'anyOf') && !hasOwnProperty(this.schema, 'allOf')) ) { this.valueFieldHTML = this.dom.tdValue.innerHTML this.dom.tdValue.style.visibility = 'hidden' this.dom.tdValue.textContent = '' } else { delete this.valueFieldHTML } } else { // cleanup select box when displayed, and attach the editable div instead if (this.dom.tdSelect) { this.dom.tdSelect.parentNode.removeChild(this.dom.tdSelect) delete this.dom.tdSelect delete this.dom.select this.dom.tdValue.innerHTML = this.valueFieldHTML this.dom.tdValue.style.visibility = '' delete this.valueFieldHTML this.dom.tdValue.appendChild(this.dom.value) } } // show color picker when value is a color if (this.editor.options.colorPicker && typeof value === 'string' && isValidColor(value)) { if (!this.dom.color) { this.dom.color = document.createElement('div') this.dom.color.className = 'jsoneditor-color' this.dom.tdColor = document.createElement('td') this.dom.tdColor.className = 'jsoneditor-tree' this.dom.tdColor.appendChild(this.dom.color) this.dom.tdValue.parentNode.insertBefore(this.dom.tdColor, this.dom.tdValue) } // update styling of value and color background addClassName(this.dom.value, 'jsoneditor-color-value') if (!this.editable.value) { addClassName(this.dom.color, 'jsoneditor-color-readonly') } else { removeClassName(this.dom.color, 'jsoneditor-color-readonly') } this.dom.color.style.backgroundColor = value } else { // cleanup color picker when displayed this._deleteDomColor() } // show date tag when value is a timestamp in milliseconds if (this._showTimestampTag()) { if (!this.dom.date) { this.dom.date = document.createElement('div') this.dom.date.className = 'jsoneditor-date' this.dom.value.parentNode.appendChild(this.dom.date) } let title = null if (typeof this.editor.options.timestampFormat === 'function') { title = this.editor.options.timestampFormat({ field: this.field, value: this.value, path: this.getPath() }) } if (!title) { this.dom.date.textContent = new Date(value).toISOString() } else { while (this.dom.date.firstChild) { this.dom.date.removeChild(this.dom.date.firstChild) } this.dom.date.appendChild(document.createTextNode(title)) } this.dom.date.title = new Date(value).toString() } else { // cleanup date tag if (this.dom.date) { this.dom.date.parentNode.removeChild(this.dom.date) delete this.dom.date } } // strip formatting from the contents of the editable div stripFormatting(domValue) this._updateDomDefault() } } _updateEnumOptions () { if (!this.enum || !this.dom.select) { return } // clear the existing options this.dom.select.innerHTML = '' // Iterate all enum values and add them as options for (let i = 0; i < this.enum.length; i++) { const option = document.createElement('option') option.value = this.enum[i] option.textContent = this.enum[i] this.dom.select.appendChild(option) } } _deleteDomColor () { if (this.dom.color) { this.dom.tdColor.parentNode.removeChild(this.dom.tdColor) delete this.dom.tdColor delete this.dom.color removeClassName(this.dom.value, 'jsoneditor-color-value') } } /** * Update dom field: * - the text color of the field, depending on the text * - the height of the field, depending on the width * - background color in case it is empty * @private */ _updateDomField () { const domField = this.dom.field if (domField) { const tooltip = makeFieldTooltip(this.schema, this.editor.options.language) if (tooltip) { domField.title = tooltip } // make background color lightgray when empty const isEmpty = (String(this.field) === '' && this.parent && this.parent.type !== 'array') if (isEmpty) { addClassName(domField, 'jsoneditor-empty') } else { removeClassName(domField, 'jsoneditor-empty') } // highlight when there is a search result if (this.searchFieldActive) { addClassName(domField, 'jsoneditor-highlight-active') } else { removeClassName(domField, 'jsoneditor-highlight-active') } if (this.searchField) { addClassName(domField, 'jsoneditor-highlight') } else { removeClassName(domField, 'jsoneditor-highlight') } // strip formatting from the contents of the editable div stripFormatting(domField) } } /** * Retrieve field from DOM * @param {boolean} [forceUnique] If true, the field name will be changed * into a unique name in case it is a duplicate. * @private */ _getDomField (forceUnique) { this._clearFieldError() if (this.dom.field && this.fieldEditable) { this.fieldInnerText = getInnerText(this.dom.field) if (this.fieldInnerText === '' && this.dom.field.innerHTML !== '') { // When clearing the contents, often a
remains, messing up the // styling of the empty text box. Therefore we remove the
this.dom.field.textContent = '' } } if (this.fieldInnerText !== undefined) { try { let field = this._unescapeHTML(this.fieldInnerText) const existingFieldNames = this.parent.getFieldNames(this) const isDuplicate = existingFieldNames.indexOf(field) !== -1 if (!isDuplicate) { if (field !== this.field) { this.field = field this._debouncedOnChangeField() } } else { if (forceUnique) { // fix duplicate field: change it into a unique name field = findUniqueName(field, existingFieldNames) if (field !== this.field) { this.field = field // TODO: don't debounce but resolve right away, and cancel current debounce this._debouncedOnChangeField() } } else { this._setFieldError(translate('duplicateFieldError')) } } } catch (err) { // keep the previous field value this._setFieldError(translate('cannotParseFieldError')) } } } /** * Update the value of the schema default element in the DOM. * @private * @returns {undefined} */ _updateDomDefault () { // Short-circuit if schema is missing, has no default, or if Node has children if (!this.schema || this.schema.default === undefined || this._hasChilds()) { return } // select either enum dropdown (select) or input value const inputElement = this.dom.select ? this.dom.select : this.dom.value if (!inputElement) { return } if (this.value === this.schema.default) { inputElement.title = translate('default') addClassName(inputElement, 'jsoneditor-is-default') removeClassName(inputElement, 'jsoneditor-is-not-default') } else { inputElement.removeAttribute('title') removeClassName(inputElement, 'jsoneditor-is-default') addClassName(inputElement, 'jsoneditor-is-not-default') } } /** * Test whether to show a timestamp tag or not * @return {boolean} Returns true when the value is a timestamp */ _showTimestampTag () { if (typeof this.value !== 'number') { return false } const timestampTag = this.editor.options.timestampTag if (typeof timestampTag === 'function') { const result = timestampTag({ field: this.field, value: this.value, path: this.getPath() }) if (typeof result === 'boolean') { return result } else { return isTimestamp(this.field, this.value) } } else if (timestampTag === true) { return isTimestamp(this.field, this.value) } else { return false } } /** * Clear the dom of the node */ clearDom () { // TODO: hide the node first? // this.hide(); // TODO: recursively clear dom? this.dom = {} } /** * Get the HTML DOM TR element of the node. * The dom will be generated when not yet created * @return {Element} tr HTML DOM TR Element */ getDom () { const dom = this.dom if (dom.tr) { return dom.tr } this._updateEditability() // create row dom.tr = document.createElement('tr') dom.tr.node = this if (this.editor.options.mode === 'tree') { // note: we take here the global setting const tdDrag = document.createElement('td') if (this.editable.field) { // create draggable area if (this.parent) { const domDrag = document.createElement('button') domDrag.type = 'button' dom.drag = domDrag domDrag.className = 'jsoneditor-button jsoneditor-dragarea' domDrag.title = translate('drag') tdDrag.appendChild(domDrag) } } dom.tr.appendChild(tdDrag) // create context menu const tdMenu = document.createElement('td') const menu = document.createElement('button') menu.type = 'button' dom.menu = menu menu.className = 'jsoneditor-button jsoneditor-contextmenu-button' menu.title = translate('actionsMenu') tdMenu.appendChild(dom.menu) dom.tr.appendChild(tdMenu) } // create tree and field const tdField = document.createElement('td') dom.tr.appendChild(tdField) dom.tree = this._createDomTree() tdField.appendChild(dom.tree) this.updateDom({ updateIndexes: true }) return dom.tr } /** * Test whether a Node is rendered and visible * @returns {boolean} */ isVisible () { return (this.dom && this.dom.tr && this.dom.tr.parentNode) || false } /** * Test if this node is a sescendant of an other node * @param {Node} node * @return {boolean} isDescendant * @private */ isDescendantOf (node) { let n = this.parent while (n) { if (n === node) { return true } n = n.parent } return false } /** * Create an editable field * @return {Element} domField * @private */ _createDomField () { return document.createElement('div') } /** * Set highlighting for this node and all its childs. * Only applied to the currently visible (expanded childs) * @param {boolean} highlight */ setHighlight (highlight) { if (this.dom.tr) { if (highlight) { addClassName(this.dom.tr, 'jsoneditor-highlight') } else { removeClassName(this.dom.tr, 'jsoneditor-highlight') } if (this.append) { this.append.setHighlight(highlight) } if (this.childs) { this.childs.forEach(child => { child.setHighlight(highlight) }) } } } /** * Select or deselect a node * @param {boolean} selected * @param {boolean} [isFirst] */ setSelected (selected, isFirst) { this.selected = selected if (this.dom.tr) { if (selected) { addClassName(this.dom.tr, 'jsoneditor-selected') } else { removeClassName(this.dom.tr, 'jsoneditor-selected') } if (isFirst) { addClassName(this.dom.tr, 'jsoneditor-first') } else { removeClassName(this.dom.tr, 'jsoneditor-first') } if (this.append) { this.append.setSelected(selected) } if (this.showMore) { this.showMore.setSelected(selected) } if (this.childs) { this.childs.forEach(child => { child.setSelected(selected) }) } } } /** * Update the value of the node. Only primitive types are allowed, no Object * or Array is allowed. * @param {String | Number | Boolean | null} value */ updateValue (value) { this.value = value this.previousValue = value this.valueError = undefined this.updateDom() } /** * Update the field of the node. * @param {String} field */ updateField (field) { this.field = field this.previousField = field this.fieldError = undefined this.updateDom() } /** * Update the HTML DOM, optionally recursing through the childs * @param {Object} [options] Available parameters: * {boolean} [recurse] If true, the * DOM of the childs will be updated recursively. * False by default. * {boolean} [updateIndexes] If true, the childs * indexes of the node will be updated too. False by * default. */ updateDom (options) { // update level indentation const domTree = this.dom.tree if (domTree) { domTree.style.marginLeft = this.getLevel() * 24 + 'px' } // apply field to DOM const domField = this.dom.field if (domField) { if (this.fieldEditable) { // parent is an object domField.contentEditable = this.editable.field domField.spellcheck = false domField.className = 'jsoneditor-field' } else { // parent is an array this is the root node domField.contentEditable = false domField.className = 'jsoneditor-readonly' } let fieldText if (this.index !== undefined) { fieldText = this.index } else if (this.field !== undefined) { fieldText = this.field } else { const schema = this.editor.options.schema ? Node._findSchema(this.editor.options.schema, this.editor.options.schemaRefs || {}, this.getPath()) : undefined if (schema && schema.title) { fieldText = schema.title } else if (this._hasChilds()) { fieldText = this.type } else { fieldText = '' } } const escapedField = this._escapeHTML(fieldText) if ( document.activeElement !== domField && escapedField !== this._unescapeHTML(getInnerText(domField)) ) { // only update if it not has the focus or when there is an actual change, // else you would needlessly loose the caret position when changing tabs // or whilst typing domField.innerHTML = escapedField } this._updateSchema() this._updateEnumOptions() } // apply value to DOM const domValue = this.dom.value if (domValue) { if (this.type === 'array' || this.type === 'object') { this.updateNodeName() } else { const escapedValue = this._escapeHTML(this.value) if ( document.activeElement !== domValue && escapedValue !== this._unescapeHTML(getInnerText(domValue)) ) { // only update if it not has the focus or when there is an actual change, // else you would needlessly loose the caret position when changing tabs // or whilst typing domValue.innerHTML = escapedValue } } } // apply styling to the table row const tr = this.dom.tr if (tr) { if (this.type === 'array' || this.type === 'object') { addClassName(tr, 'jsoneditor-expandable') if (this.expanded) { addClassName(tr, 'jsoneditor-expanded') removeClassName(tr, 'jsoneditor-collapsed') } else { addClassName(tr, 'jsoneditor-collapsed') removeClassName(tr, 'jsoneditor-expanded') } } else { removeClassName(tr, 'jsoneditor-expandable') removeClassName(tr, 'jsoneditor-expanded') removeClassName(tr, 'jsoneditor-collapsed') } } // update field and value this._updateDomField() this._updateDomValue() // update childs indexes if (options && options.updateIndexes === true) { // updateIndexes is true or undefined this._updateDomIndexes() } // update childs recursively if (options && options.recurse === true) { if (this.childs) { this.childs.forEach(child => { child.updateDom(options) }) } } // update rendering of error if (this.error) { this.updateError() } // update row with append button if (this.append) { this.append.updateDom() } // update "show more" text at the bottom of large arrays if (this.showMore) { this.showMore.updateDom() } // fire onClassName this._updateCssClassName() } /** * Locate the JSON schema of the node and check for any enum type * @private */ _updateSchema () { // Locating the schema of the node and checking for any enum type if (this.editor && this.editor.options) { // find the part of the json schema matching this nodes path this.schema = this.editor.options.schema // fix childSchema with $ref, and not display the select element on the child schema because of not found enum ? Node._findSchema(this.editor.options.schema, this.editor.options.schemaRefs || {}, this.getPath()) : null if (this.schema) { this.enum = Node._findEnum(this.schema) } else { delete this.enum } } } /** * Update the DOM of the childs of a node: update indexes and undefined field * names. * Only applicable when structure is an array or object * @private */ _updateDomIndexes () { const domValue = this.dom.value const childs = this.childs if (domValue && childs) { if (this.type === 'array') { childs.forEach((child, index) => { child.index = index const childField = child.dom.field if (childField) { childField.textContent = index } }) } else if (this.type === 'object') { childs.forEach(child => { if (child.index !== undefined) { delete child.index if (child.field === undefined) { child.field = '' } } }) } } } /** * Create an editable value * @private */ _createDomValue () { let domValue if (this.type === 'array') { domValue = document.createElement('div') domValue.textContent = '[...]' } else if (this.type === 'object') { domValue = document.createElement('div') domValue.textContent = '{...}' } else { if (!this.editable.value && isUrl(this.value)) { // create a link in case of read-only editor and value containing an url domValue = document.createElement('a') domValue.href = this.value domValue.innerHTML = this._escapeHTML(this.value) } else { // create an editable or read-only div domValue = document.createElement('div') domValue.contentEditable = this.editable.value domValue.spellcheck = false domValue.innerHTML = this._escapeHTML(this.value) } } return domValue } /** * Create an expand/collapse button * @return {Element} expand * @private */ _createDomExpandButton () { // create expand button const expand = document.createElement('button') expand.type = 'button' if (this._hasChilds()) { expand.className = this.expanded ? 'jsoneditor-button jsoneditor-expanded' : 'jsoneditor-button jsoneditor-collapsed' expand.title = translate('expandTitle') } else { expand.className = 'jsoneditor-button jsoneditor-invisible' expand.title = '' } return expand } /** * Create a DOM tree element, containing the expand/collapse button * @return {Element} domTree * @private */ _createDomTree () { const dom = this.dom const domTree = document.createElement('table') const tbody = document.createElement('tbody') domTree.style.borderCollapse = 'collapse' // TODO: put in css domTree.className = 'jsoneditor-values' domTree.appendChild(tbody) const tr = document.createElement('tr') tbody.appendChild(tr) // create expand button const tdExpand = document.createElement('td') tdExpand.className = 'jsoneditor-tree' tr.appendChild(tdExpand) dom.expand = this._createDomExpandButton() tdExpand.appendChild(dom.expand) dom.tdExpand = tdExpand // create the field const tdField = document.createElement('td') tdField.className = 'jsoneditor-tree' tr.appendChild(tdField) dom.field = this._createDomField() tdField.appendChild(dom.field) dom.tdField = tdField // create a separator const tdSeparator = document.createElement('td') tdSeparator.className = 'jsoneditor-tree' tr.appendChild(tdSeparator) if (this.type !== 'object' && this.type !== 'array') { tdSeparator.appendChild(document.createTextNode(':')) tdSeparator.className = 'jsoneditor-separator' } dom.tdSeparator = tdSeparator // create the value const tdValue = document.createElement('td') tdValue.className = 'jsoneditor-tree' tr.appendChild(tdValue) dom.value = this._createDomValue() tdValue.appendChild(dom.value) dom.tdValue = tdValue return domTree } /** * Handle an event. The event is caught centrally by the editor * @param {Event} event */ onEvent (event) { const type = event.type const target = event.target || event.srcElement const dom = this.dom const node = this const expandable = this._hasChilds() // check if mouse is on menu or on dragarea. // If so, highlight current row and its childs if (target === dom.drag || target === dom.menu) { if (type === 'mouseover') { this.editor.highlighter.highlight(this) } else if (type === 'mouseout') { this.editor.highlighter.unhighlight() } } // context menu events if (type === 'click' && target === dom.menu) { const highlighter = node.editor.highlighter highlighter.highlight(node) highlighter.lock() addClassName(dom.menu, 'jsoneditor-selected') this.showContextMenu(dom.menu, () => { removeClassName(dom.menu, 'jsoneditor-selected') highlighter.unlock() highlighter.unhighlight() }) } // expand events if (type === 'click') { if (target === dom.expand) { if (expandable) { const recurse = event.ctrlKey // with ctrl-key, expand/collapse all this._onExpand(recurse) } } } if ( type === 'click' && (event.target === node.dom.tdColor || event.target === node.dom.color) && this.editable.value ) { this._showColorPicker() } // swap the value of a boolean when the checkbox displayed left is clicked if (type === 'change' && target === dom.checkbox) { this.dom.value.textContent = String(!this.value) this._getDomValue() this._updateDomDefault() } // update the value of the node based on the selected option if (type === 'change' && target === dom.select) { this.dom.value.innerHTML = this._escapeHTML(dom.select.value) this._getDomValue() this._updateDomValue() } // value events const domValue = dom.value if (target === domValue) { // noinspection FallthroughInSwitchStatementJS switch (type) { case 'blur': case 'change': { this._getDomValue() this._clearValueError() this._updateDomValue() const escapedValue = this._escapeHTML(this.value) if (escapedValue !== this._unescapeHTML(getInnerText(domValue))) { // only update when there is an actual change, else you loose the // caret position when changing tabs or whilst typing domValue.innerHTML = escapedValue } break } case 'input': // this._debouncedGetDomValue(true); // TODO this._getDomValue() this._updateDomValue() break case 'keydown': case 'mousedown': // TODO: cleanup this.editor.selection = this.editor.getDomSelection() break case 'click': if (event.ctrlKey && this.editable.value) { // if read-only, we use the regular click behavior of an anchor if (isUrl(this.value)) { event.preventDefault() window.open(this.value, '_blank', 'noreferrer') } } break case 'keyup': // this._debouncedGetDomValue(true); // TODO this._getDomValue() this._updateDomValue() break case 'cut': case 'paste': setTimeout(() => { node._getDomValue() node._updateDomValue() }, 1) break } } // field events const domField = dom.field if (target === domField) { switch (type) { case 'blur': { this._getDomField(true) this._updateDomField() const escapedField = this._escapeHTML(this.field) if (escapedField !== this._unescapeHTML(getInnerText(domField))) { // only update when there is an actual change, else you loose the // caret position when changing tabs or whilst typing domField.innerHTML = escapedField } break } case 'input': this._getDomField() this._updateSchema() this._updateDomField() this._updateDomValue() break case 'keydown': case 'mousedown': this.editor.selection = this.editor.getDomSelection() break case 'keyup': this._getDomField() this._updateDomField() break case 'cut': case 'paste': setTimeout(() => { node._getDomField() node._updateDomField() }, 1) break } } // focus // when clicked in whitespace left or right from the field or value, set focus const domTree = dom.tree if (domTree && target === domTree.parentNode && type === 'click' && !event.hasMoved) { const left = (event.offsetX !== undefined) ? (event.offsetX < (this.getLevel() + 1) * 24) : (event.pageX < getAbsoluteLeft(dom.tdSeparator))// for FF if (left || expandable) { // node is expandable when it is an object or array if (domField) { setEndOfContentEditable(domField) domField.focus() } } else { if (domValue && !this.enum) { setEndOfContentEditable(domValue) domValue.focus() } } } if (((target === dom.tdExpand && !expandable) || target === dom.tdField || target === dom.tdSeparator) && (type === 'click' && !event.hasMoved)) { if (domField) { setEndOfContentEditable(domField) domField.focus() } } if (type === 'keydown') { this.onKeyDown(event) } // fire after applying for example a change by clicking a checkbox if (typeof this.editor.options.onEvent === 'function') { this._onEvent(event) } } /** * Trigger external onEvent provided in options if node is a JSON field or * value. * Information provided depends on the element, value is only included if * event occurs in a JSON value: * {field: string, path: {string|number}[] [, value: string]} * @param {Event} event * @private */ _onEvent (event) { const element = event.target const isField = element === this.dom.field const isValue = ( element === this.dom.value || element === this.dom.checkbox || element === this.dom.select) if (isField || isValue) { const info = { field: this.getField(), path: this.getPath() } // For leaf values, include value if (isValue && !this._hasChilds()) { info.value = this.getValue() } this.editor.options.onEvent(info, event) } } /** * Key down event handler * @param {Event} event */ onKeyDown (event) { const keynum = event.which || event.keyCode const target = event.target || event.srcElement const ctrlKey = event.ctrlKey const shiftKey = event.shiftKey const altKey = event.altKey let handled = false let prevNode, nextNode, nextDom, nextDom2 const editable = this.editor.options.mode === 'tree' let oldSelection let oldNextNode let oldParent let oldIndexRedo let newIndexRedo let oldParentPathRedo let newParentPathRedo let nodes let multiselection const selectedNodes = this.editor.multiselection.nodes.length > 0 ? this.editor.multiselection.nodes : [this] const firstNode = selectedNodes[0] const lastNode = selectedNodes[selectedNodes.length - 1] // console.log(ctrlKey, keynum, event.charCode); // TODO: cleanup if (keynum === 13) { // Enter if (target === this.dom.value) { if (!this.editable.value || event.ctrlKey) { if (isUrl(this.value)) { window.open(this.value, '_blank', 'noreferrer') handled = true } } } else if (target === this.dom.expand) { const expandable = this._hasChilds() if (expandable) { const recurse = event.ctrlKey // with ctrl-key, expand/collapse all this._onExpand(recurse) target.focus() handled = true } } } else if (keynum === 68) { // D if (ctrlKey && editable) { // Ctrl+D Node.onDuplicate(selectedNodes) handled = true } } else if (keynum === 69) { // E if (ctrlKey) { // Ctrl+E and Ctrl+Shift+E this._onExpand(shiftKey) // recurse = shiftKey target.focus() // TODO: should restore focus in case of recursing expand (which takes DOM offline) handled = true } } else if (keynum === 77 && editable) { // M if (ctrlKey) { // Ctrl+M this.showContextMenu(target) handled = true } } else if (keynum === 46 && editable) { // Del if (ctrlKey) { // Ctrl+Del Node.onRemove(selectedNodes) handled = true } } else if (keynum === 45 && editable) { // Ins if (ctrlKey && !shiftKey) { // Ctrl+Ins this._onInsertBefore() handled = true } else if (ctrlKey && shiftKey) { // Ctrl+Shift+Ins this._onInsertAfter() handled = true } } else if (keynum === 35) { // End if (altKey) { // Alt+End // find the last node const endNode = this._lastNode() if (endNode) { endNode.focus(Node.focusElement || this._getElementName(target)) } handled = true } } else if (keynum === 36) { // Home if (altKey) { // Alt+Home // find the first node const homeNode = this._firstNode() if (homeNode) { homeNode.focus(Node.focusElement || this._getElementName(target)) } handled = true } } else if (keynum === 37) { // Arrow Left if (altKey && !shiftKey) { // Alt + Arrow Left // move to left element const prevElement = this._previousElement(target) if (prevElement) { this.focus(this._getElementName(prevElement)) } handled = true } else if (altKey && shiftKey && editable) { // Alt + Shift + Arrow left if (lastNode.expanded) { const appendDom = lastNode.getAppendDom() nextDom = appendDom ? appendDom.nextSibling : undefined } else { const dom = lastNode.getDom() nextDom = dom.nextSibling } if (nextDom) { nextNode = Node.getNodeFromTarget(nextDom) nextDom2 = nextDom.nextSibling const nextNode2 = Node.getNodeFromTarget(nextDom2) if (nextNode && nextNode instanceof AppendNode && !(lastNode.parent.childs.length === 1) && nextNode2 && nextNode2.parent) { oldSelection = this.editor.getDomSelection() oldParent = firstNode.parent oldNextNode = oldParent.childs[lastNode.getIndex() + 1] || oldParent.append oldIndexRedo = firstNode.getIndex() newIndexRedo = nextNode2.getIndex() oldParentPathRedo = oldParent.getInternalPath() newParentPathRedo = nextNode2.parent.getInternalPath() selectedNodes.forEach(node => { nextNode2.parent.moveBefore(node, nextNode2) }) this.focus(Node.focusElement || this._getElementName(target)) this.editor._onAction('moveNodes', { count: selectedNodes.length, fieldNames: selectedNodes.map(getField), oldParentPath: oldParent.getInternalPath(), newParentPath: firstNode.parent.getInternalPath(), oldIndex: oldNextNode.getIndex(), newIndex: firstNode.getIndex(), oldIndexRedo, newIndexRedo, oldParentPathRedo, newParentPathRedo, oldSelection, newSelection: this.editor.getDomSelection() }) } } } } else if (keynum === 38) { // Arrow Up if (altKey && !shiftKey) { // Alt + Arrow Up // find the previous node prevNode = this._previousNode() if (prevNode) { this.editor.deselect(true) prevNode.focus(Node.focusElement || this._getElementName(target)) } handled = true } else if (!altKey && ctrlKey && shiftKey && editable) { // Ctrl + Shift + Arrow Up // select multiple nodes prevNode = this._previousNode() if (prevNode) { multiselection = this.editor.multiselection multiselection.start = multiselection.start || this multiselection.end = prevNode nodes = this.editor._findTopLevelNodes(multiselection.start, multiselection.end) this.editor.select(nodes) prevNode.focus('field') // select field as we know this always exists } handled = true } else if (altKey && shiftKey && editable) { // Alt + Shift + Arrow Up // find the previous node prevNode = firstNode._previousNode() if (prevNode && prevNode.parent) { oldSelection = this.editor.getDomSelection() oldParent = firstNode.parent oldNextNode = oldParent.childs[lastNode.getIndex() + 1] || oldParent.append oldIndexRedo = firstNode.getIndex() newIndexRedo = prevNode.getIndex() oldParentPathRedo = oldParent.getInternalPath() newParentPathRedo = prevNode.parent.getInternalPath() selectedNodes.forEach(node => { prevNode.parent.moveBefore(node, prevNode) }) this.focus(Node.focusElement || this._getElementName(target)) this.editor._onAction('moveNodes', { count: selectedNodes.length, fieldNames: selectedNodes.map(getField), oldParentPath: oldParent.getInternalPath(), newParentPath: firstNode.parent.getInternalPath(), oldIndex: oldNextNode.getIndex(), newIndex: firstNode.getIndex(), oldIndexRedo, newIndexRedo, oldParentPathRedo, newParentPathRedo, oldSelection, newSelection: this.editor.getDomSelection() }) } handled = true } } else if (keynum === 39) { // Arrow Right if (altKey && !shiftKey) { // Alt + Arrow Right // move to right element const nextElement = this._nextElement(target) if (nextElement) { this.focus(this._getElementName(nextElement)) } handled = true } else if (altKey && shiftKey && editable) { // Alt + Shift + Arrow Right const dom = firstNode.getDom() const prevDom = dom.previousSibling if (prevDom) { prevNode = Node.getNodeFromTarget(prevDom) if (prevNode && prevNode.parent && !prevNode.isVisible()) { oldSelection = this.editor.getDomSelection() oldParent = firstNode.parent oldNextNode = oldParent.childs[lastNode.getIndex() + 1] || oldParent.append oldIndexRedo = firstNode.getIndex() newIndexRedo = prevNode.getIndex() oldParentPathRedo = oldParent.getInternalPath() newParentPathRedo = prevNode.parent.getInternalPath() selectedNodes.forEach(node => { prevNode.parent.moveBefore(node, prevNode) }) this.focus(Node.focusElement || this._getElementName(target)) this.editor._onAction('moveNodes', { count: selectedNodes.length, fieldNames: selectedNodes.map(getField), oldParentPath: oldParent.getInternalPath(), newParentPath: firstNode.parent.getInternalPath(), oldIndex: oldNextNode.getIndex(), newIndex: firstNode.getIndex(), oldIndexRedo, newIndexRedo, oldParentPathRedo, newParentPathRedo, oldSelection, newSelection: this.editor.getDomSelection() }) } } } } else if (keynum === 40) { // Arrow Down if (altKey && !shiftKey) { // Alt + Arrow Down // find the next node nextNode = this._nextNode() if (nextNode) { this.editor.deselect(true) nextNode.focus(Node.focusElement || this._getElementName(target)) } handled = true } else if (!altKey && ctrlKey && shiftKey && editable) { // Ctrl + Shift + Arrow Down // select multiple nodes nextNode = this._nextNode() if (nextNode) { multiselection = this.editor.multiselection multiselection.start = multiselection.start || this multiselection.end = nextNode nodes = this.editor._findTopLevelNodes(multiselection.start, multiselection.end) this.editor.select(nodes) nextNode.focus('field') // select field as we know this always exists } handled = true } else if (altKey && shiftKey && editable) { // Alt + Shift + Arrow Down // find the 2nd next node and move before that one if (lastNode.expanded) { nextNode = lastNode.append ? lastNode.append._nextNode() : undefined } else { nextNode = lastNode._nextNode() } // when the next node is not visible, we've reached the "showMore" buttons if (nextNode && !nextNode.isVisible()) { nextNode = nextNode.parent.showMore } if (nextNode && nextNode instanceof AppendNode) { nextNode = lastNode } const nextNode2 = nextNode && (nextNode._nextNode() || nextNode.parent.append) if (nextNode2 && nextNode2.parent) { oldSelection = this.editor.getDomSelection() oldParent = firstNode.parent oldNextNode = oldParent.childs[lastNode.getIndex() + 1] || oldParent.append oldIndexRedo = firstNode.getIndex() newIndexRedo = nextNode2.getIndex() oldParentPathRedo = oldParent.getInternalPath() newParentPathRedo = nextNode2.parent.getInternalPath() selectedNodes.forEach(node => { nextNode2.parent.moveBefore(node, nextNode2) }) this.focus(Node.focusElement || this._getElementName(target)) this.editor._onAction('moveNodes', { count: selectedNodes.length, fieldNames: selectedNodes.map(getField), oldParentPath: oldParent.getInternalPath(), newParentPath: firstNode.parent.getInternalPath(), oldParentPathRedo, newParentPathRedo, oldIndexRedo, newIndexRedo, oldIndex: oldNextNode.getIndex(), newIndex: firstNode.getIndex(), oldSelection, newSelection: this.editor.getDomSelection() }) } handled = true } } if (handled) { event.preventDefault() event.stopPropagation() } } /** * Handle the expand event, when clicked on the expand button * @param {boolean} recurse If true, child nodes will be expanded too * @private */ _onExpand (recurse) { let table let frame let scrollTop if (recurse) { // Take the table offline table = this.dom.tr.parentNode // TODO: not nice to access the main table like this frame = table.parentNode scrollTop = frame.scrollTop frame.removeChild(table) } if (this.expanded) { this.collapse(recurse) } else { this.expand(recurse) } if (recurse) { // Put the table online again frame.appendChild(table) frame.scrollTop = scrollTop } if (typeof this.editor.options.onExpand === 'function') { this.editor.options.onExpand({ path: this.getPath(), isExpand: this.expanded, recursive: recurse }) } } /** * Open a color picker to select a new color * @private */ _showColorPicker () { if (typeof this.editor.options.onColorPicker === 'function' && this.dom.color) { const node = this // force deleting current color picker (if any) node._deleteDomColor() node.updateDom() const colorAnchor = createAbsoluteAnchor(this.dom.color, this.editor.getPopupAnchor()) this.editor.options.onColorPicker(colorAnchor, this.value, function onChange (value) { if (typeof value === 'string' && value !== node.value) { // force recreating the color block, to cleanup any attached color picker node._deleteDomColor() node.value = value node.updateDom() node._debouncedOnChangeValue() } }) } } /** * Get all field names of an object * @param {Node} [excludeNode] Optional node to be excluded from the returned field names * @return {string[]} */ getFieldNames (excludeNode) { if (this.type === 'object') { return this.childs .filter(child => child !== excludeNode) .map(child => child.field) } return [] } /** * Handle insert before event * @param {String} [field] * @param {*} [value] * @param {String} [type] Can be 'auto', 'array', 'object', or 'string' * @private */ _onInsertBefore (field, value, type) { const oldSelection = this.editor.getDomSelection() const newNode = new Node(this.editor, { field: (field !== undefined) ? field : '', value: (value !== undefined) ? value : '', type }) newNode.expand(true) const beforePath = this.getInternalPath() this.parent.insertBefore(newNode, this) this.editor.highlighter.unhighlight() newNode.focus('field') const newSelection = this.editor.getDomSelection() this.editor._onAction('insertBeforeNodes', { nodes: [newNode], paths: [newNode.getInternalPath()], beforePath, parentPath: this.parent.getInternalPath(), oldSelection, newSelection }) } /** * Handle insert after event * @param {String} [field] * @param {*} [value] * @param {String} [type] Can be 'auto', 'array', 'object', or 'string' * @private */ _onInsertAfter (field, value, type) { const oldSelection = this.editor.getDomSelection() const newNode = new Node(this.editor, { field: (field !== undefined) ? field : '', value: (value !== undefined) ? value : '', type }) newNode.expand(true) this.parent.insertAfter(newNode, this) this.editor.highlighter.unhighlight() newNode.focus('field') const newSelection = this.editor.getDomSelection() this.editor._onAction('insertAfterNodes', { nodes: [newNode], paths: [newNode.getInternalPath()], afterPath: this.getInternalPath(), parentPath: this.parent.getInternalPath(), oldSelection, newSelection }) } /** * Handle append event * @param {String} [field] * @param {*} [value] * @param {String} [type] Can be 'auto', 'array', 'object', or 'string' * @private */ _onAppend (field, value, type) { const oldSelection = this.editor.getDomSelection() const newNode = new Node(this.editor, { field: (field !== undefined) ? field : '', value: (value !== undefined) ? value : '', type }) newNode.expand(true) this.parent.appendChild(newNode) this.editor.highlighter.unhighlight() newNode.focus('field') const newSelection = this.editor.getDomSelection() this.editor._onAction('appendNodes', { nodes: [newNode], paths: [newNode.getInternalPath()], parentPath: this.parent.getInternalPath(), oldSelection, newSelection }) } /** * Change the type of the node's value * @param {String} newType * @private */ _onChangeType (newType) { const oldType = this.type if (newType !== oldType) { const oldSelection = this.editor.getDomSelection() this.changeType(newType) const newSelection = this.editor.getDomSelection() this.editor._onAction('changeType', { path: this.getInternalPath(), oldType, newType, oldSelection, newSelection }) } } /** * Sort the child's of the node. Only applicable when the node has type 'object' * or 'array'. * @param {String[] | string} path Path of the child value to be compared * @param {String} direction Sorting direction. Available values: "asc", "desc" * @param {boolean} [triggerAction=true] If true (default), a user action will be * triggered, creating an entry in history * and invoking onChange. * @private */ sort (path, direction, triggerAction = true) { if (typeof path === 'string') { path = parsePath(path) } if (!this._hasChilds()) { return } this.hideChilds() // sorting is faster when the childs are not attached to the dom // copy the childs array (the old one will be kept for an undo action const oldChilds = this.childs this.childs = this.childs.concat() // sort the childs array const order = (direction === 'desc') ? -1 : 1 if (this.type === 'object') { this.childs.sort((a, b) => order * naturalSort(a.field, b.field)) } else { // this.type === 'array' this.childs.sort((a, b) => { const nodeA = a.getNestedChild(path) const nodeB = b.getNestedChild(path) if (!nodeA) { return order } if (!nodeB) { return -order } const valueA = nodeA.value const valueB = nodeB.value if (typeof valueA !== 'string' && typeof valueB !== 'string') { // both values are a number, boolean, or null -> use simple, fast sorting return valueA > valueB ? order : valueA < valueB ? -order : 0 } return order * naturalSort(valueA, valueB) }) } // update the index numbering this._updateDomIndexes() this.showChilds() if (triggerAction === true) { this.editor._onAction('sort', { path: this.getInternalPath(), oldChilds, newChilds: this.childs }) } } /** * Replace the value of the node, keep it's state * @param {*} newValue */ update (newValue) { const oldValue = this.getInternalValue() this.setValue(newValue) this.editor._onAction('transform', { path: this.getInternalPath(), oldValue, newValue: this.getInternalValue() }) } /** * Remove this node from the DOM * @returns {{table: Element, nextTr?: Element}} * Returns the DOM elements that which be used to attach the node * to the DOM again, see _attachToDom. * @private */ _detachFromDom () { const table = this.dom.tr ? this.dom.tr.parentNode : undefined let lastTr if (this.expanded) { lastTr = this.getAppendDom() } else { lastTr = this.getDom() } const nextTr = (lastTr && lastTr.parentNode) ? lastTr.nextSibling : undefined this.hide({ resetVisibleChilds: false }) return { table, nextTr } } /** * Attach this node to the DOM again * @param {{table: Element, nextTr?: Element}} domAnchor * The DOM elements returned by _detachFromDom. * @private */ _attachToDom (domAnchor) { if (domAnchor.table) { if (domAnchor.nextTr) { domAnchor.table.insertBefore(this.getDom(), domAnchor.nextTr) } else { domAnchor.table.appendChild(this.getDom()) } } if (this.expanded) { this.showChilds() } } /** * Transform the node given a JMESPath query. * @param {String} query JMESPath query to apply * @private */ transform (query) { if (!this._hasChilds()) { return } this.hideChilds() // sorting is faster when the childs are not attached to the dom try { const oldInternalValue = this.getInternalValue() // apply the JMESPath query const oldValue = this.getValue() const newValue = this.editor.options.executeQuery(oldValue, query) this.setValue(newValue) const newInternalValue = this.getInternalValue() this.editor._onAction('transform', { path: this.getInternalPath(), oldValue: oldInternalValue, newValue: newInternalValue }) this.showChilds() } catch (err) { this.showChilds() this.editor._onError(err) } } /** * Make this object the root object of the ditor */ extract () { this.editor.node.hideChilds() this.hideChilds() try { const oldInternalValue = this.editor.node.getInternalValue() this.editor._setRoot(this) const newInternalValue = this.editor.node.getInternalValue() this.editor._onAction('transform', { path: this.editor.node.getInternalPath(), oldValue: oldInternalValue, newValue: newInternalValue }) } catch (err) { this.editor._onError(err) } finally { this.updateDom({ recurse: true }) this.showChilds() } } /** * Get a nested child given a path with properties * @param {String[]} path * @returns {Node} */ getNestedChild (path) { let i = 0 let child = this while (child && i < path.length) { child = child.findChildByProperty(path[i]) i++ } return child } /** * Find a child by property name * @param {string} prop * @return {Node | undefined} Returns the child node when found, or undefined otherwise */ findChildByProperty (prop) { if (this.type !== 'object') { return undefined } return this.childs.find(child => child.field === prop) } /** * Create a table row with an append button. * @return {HTMLElement | undefined} tr with the AppendNode contents */ getAppendDom () { if (!this.append) { this.append = new AppendNode(this.editor) this.append.setParent(this) } return this.append.getDom() } /** * Create a table row with an showMore button and text * @return {HTMLElement | undefined} tr with the AppendNode contents */ getShowMoreDom () { if (!this.showMore) { this.showMore = new ShowMoreNode(this.editor, this) } return this.showMore.getDom() } /** * Get the next sibling of current node * @return {Node} nextSibling */ nextSibling () { const index = this.parent.childs.indexOf(this) return this.parent.childs[index + 1] || this.parent.append } /** * Get the previously rendered node * @return {Node | null} previousNode */ _previousNode () { let prevNode = null const dom = this.getDom() if (dom && dom.parentNode) { // find the previous field let prevDom = dom do { prevDom = prevDom.previousSibling prevNode = Node.getNodeFromTarget(prevDom) } while (prevDom && prevNode && (prevNode instanceof AppendNode && !prevNode.isVisible())) } return prevNode } /** * Get the next rendered node * @return {Node | null} nextNode * @private */ _nextNode () { let nextNode = null const dom = this.getDom() if (dom && dom.parentNode) { // find the previous field let nextDom = dom do { nextDom = nextDom.nextSibling nextNode = Node.getNodeFromTarget(nextDom) } while (nextDom && nextNode && (nextNode instanceof AppendNode && !nextNode.isVisible())) } return nextNode } /** * Get the first rendered node * @return {Node | null} firstNode * @private */ _firstNode () { let firstNode = null const dom = this.getDom() if (dom && dom.parentNode) { const firstDom = dom.parentNode.firstChild firstNode = Node.getNodeFromTarget(firstDom) } return firstNode } /** * Get the last rendered node * @return {Node | null} lastNode * @private */ _lastNode () { let lastNode = null const dom = this.getDom() if (dom && dom.parentNode) { let lastDom = dom.parentNode.lastChild lastNode = Node.getNodeFromTarget(lastDom) while (lastDom && lastNode && !lastNode.isVisible()) { lastDom = lastDom.previousSibling lastNode = Node.getNodeFromTarget(lastDom) } } return lastNode } /** * Get the next element which can have focus. * @param {Element} elem * @return {Element | null} nextElem * @private */ _previousElement (elem) { const dom = this.dom // noinspection FallthroughInSwitchStatementJS switch (elem) { case dom.value: if (this.fieldEditable) { return dom.field } // intentional fall through case dom.field: if (this._hasChilds()) { return dom.expand } // intentional fall through case dom.expand: return dom.menu case dom.menu: if (dom.drag) { return dom.drag } // intentional fall through default: return null } } /** * Get the next element which can have focus. * @param {Element} elem * @return {Element | null} nextElem * @private */ _nextElement (elem) { const dom = this.dom // noinspection FallthroughInSwitchStatementJS switch (elem) { case dom.drag: return dom.menu case dom.menu: if (this._hasChilds()) { return dom.expand } // intentional fall through case dom.expand: if (this.fieldEditable) { return dom.field } // intentional fall through case dom.field: if (!this._hasChilds()) { return dom.value } // intentional fall through default: return null } } /** * Get the dom name of given element. returns null if not found. * For example when element === dom.field, "field" is returned. * @param {Element} element * @return {String | null} elementName Available elements with name: 'drag', * 'menu', 'expand', 'field', 'value' * @private */ _getElementName (element) { return Object.keys(this.dom) .find(name => this.dom[name] === element) } /** * Test if this node has childs. This is the case when the node is an object * or array. * @return {boolean} hasChilds * @private */ _hasChilds () { return this.type === 'array' || this.type === 'object' } addTemplates (menu, append) { const node = this const templates = node.editor.options.templates if (templates == null) return if (templates.length) { // create a separator menu.push({ type: 'separator' }) } const appendData = (name, data) => { node._onAppend(name, data) } const insertData = (name, data) => { node._onInsertBefore(name, data) } templates.forEach(function (template) { menu.push({ text: template.text, className: (template.className || 'jsoneditor-type-object'), title: template.title, click: (append ? appendData.bind(this, template.field, template.value) : insertData.bind(this, template.field, template.value)) }) }) } /** * Show a contextmenu for this node * @param {HTMLElement} anchor Anchor element to attach the context menu to * as sibling. * @param {function} [onClose] Callback method called when the context menu * is being closed. */ showContextMenu (anchor, onClose) { const node = this let items = [] if (this.editable.value) { items.push({ text: translate('type'), title: translate('typeTitle'), className: 'jsoneditor-type-' + this.type, submenu: [ { text: translate('auto'), className: 'jsoneditor-type-auto' + (this.type === 'auto' ? ' jsoneditor-selected' : ''), title: translate('autoType'), click: function () { node._onChangeType('auto') } }, { text: translate('array'), className: 'jsoneditor-type-array' + (this.type === 'array' ? ' jsoneditor-selected' : ''), title: translate('arrayType'), click: function () { node._onChangeType('array') } }, { text: translate('object'), className: 'jsoneditor-type-object' + (this.type === 'object' ? ' jsoneditor-selected' : ''), title: translate('objectType'), click: function () { node._onChangeType('object') } }, { text: translate('string'), className: 'jsoneditor-type-string' + (this.type === 'string' ? ' jsoneditor-selected' : ''), title: translate('stringType'), click: function () { node._onChangeType('string') } } ] }) } if (this._hasChilds()) { if (this.editor.options.enableSort) { items.push({ text: translate('sort'), title: translate('sortTitle', { type: this.type }), className: 'jsoneditor-sort-asc', click: function () { node.showSortModal() } }) } if (this.editor.options.enableTransform) { items.push({ text: translate('transform'), title: translate('transformTitle', { type: this.type }), className: 'jsoneditor-transform', click: function () { node.showTransformModal() } }) } if (this.parent) { items.push({ text: translate('extract'), title: translate('extractTitle', { type: this.type }), className: 'jsoneditor-extract', click: function () { node.extract() } }) } } if (this.parent && this.parent._hasChilds()) { if (items.length) { // create a separator items.push({ type: 'separator' }) } // create append button (for last child node only) const childs = node.parent.childs if (node === childs[childs.length - 1]) { 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) items.push({ text: translate('appendText'), title: translate('appendTitle'), submenuTitle: translate('appendSubmenuTitle'), className: 'jsoneditor-append', click: function () { node._onAppend('', '', 'auto') }, submenu: appendSubmenu }) } // create insert button const insertSubmenu = [ { text: translate('auto'), className: 'jsoneditor-type-auto', title: translate('autoType'), click: function () { node._onInsertBefore('', '', 'auto') } }, { text: translate('array'), className: 'jsoneditor-type-array', title: translate('arrayType'), click: function () { node._onInsertBefore('', []) } }, { text: translate('object'), className: 'jsoneditor-type-object', title: translate('objectType'), click: function () { node._onInsertBefore('', {}) } }, { text: translate('string'), className: 'jsoneditor-type-string', title: translate('stringType'), click: function () { node._onInsertBefore('', '', 'string') } } ] node.addTemplates(insertSubmenu, false) items.push({ text: translate('insert'), title: translate('insertTitle'), submenuTitle: translate('insertSub'), className: 'jsoneditor-insert', click: function () { node._onInsertBefore('', '', 'auto') }, submenu: insertSubmenu }) if (this.editable.field) { // create duplicate button items.push({ text: translate('duplicateText'), title: translate('duplicateField'), className: 'jsoneditor-duplicate', click: function () { Node.onDuplicate(node) } }) // create remove button items.push({ text: translate('removeText'), title: translate('removeField'), className: 'jsoneditor-remove', click: function () { Node.onRemove(node) } }) } } if (this.editor.options.onCreateMenu) { const path = node.getPath() items = this.editor.options.onCreateMenu(items, { type: 'single', path, paths: [path] }) } const menu = new ContextMenu(items, { close: onClose }) menu.show(anchor, this.editor.getPopupAnchor()) } /** * Show sorting modal */ showSortModal () { const node = this const container = this.editor.options.modalAnchor || DEFAULT_MODAL_ANCHOR const json = this.getValue() function onSort (sortedBy) { const path = sortedBy.path const pathArray = parsePath(path) node.sortedBy = sortedBy node.sort(pathArray, sortedBy.direction) } showSortModal(container, json, onSort, node.sortedBy) } /** * Show transform modal */ showTransformModal () { const { modalAnchor, createQuery, executeQuery, queryDescription } = this.editor.options const json = this.getValue() showTransformModal({ container: modalAnchor || DEFAULT_MODAL_ANCHOR, json, queryDescription, // can be undefined createQuery, executeQuery, onTransform: query => { this.transform(query) } }) } /** * get the type of a value * @param {*} value * @return {String} type Can be 'object', 'array', 'string', 'auto' * @private */ _getType (value) { if (value instanceof Array) { return 'array' } if (value instanceof Object) { return 'object' } if (typeof (value) === 'string' && typeof (parseString(value)) !== 'string') { return 'string' } return 'auto' } /** * escape a text, such that it can be displayed safely in an HTML element * @param {String} text * @return {String} escapedText * @private */ _escapeHTML (text) { if (typeof text !== 'string') { return String(text) } else { const htmlEscaped = String(text) .replace(/&/g, '&') // must be replaced first! .replace(//g, '>') .replace(/ {2}/g, '  ') // replace double space with an nbsp and space .replace(/^ /, ' ') // space at start .replace(/ $/, ' ') // space at end const json = JSON.stringify(htmlEscaped) let html = json.substring(1, json.length - 1) if (this.editor.options.escapeUnicode === true) { html = escapeUnicodeChars(html) } return html } } /** * unescape a string. * @param {String} escapedText * @return {String} text * @private */ _unescapeHTML (escapedText) { const json = '"' + this._escapeJSON(escapedText) + '"' const htmlEscaped = parse(json) return htmlEscaped .replace(/</g, '<') .replace(/>/g, '>') .replace(/ |\u00A0/g, ' ') .replace(/&/g, '&') // must be replaced last } /** * escape a text to make it a valid JSON string. The method will: * - replace unescaped double quotes with '\"' * - replace unescaped backslash with '\\' * - replace returns with '\n' * @param {String} text * @return {String} escapedText * @private */ _escapeJSON (text) { // TODO: replace with some smart regex (only when a new solution is faster!) let escaped = '' let i = 0 while (i < text.length) { let c = text.charAt(i) if (c === '\n') { escaped += '\\n' } else if (c === '\\') { escaped += c i++ c = text.charAt(i) if (c === '' || '"\\/bfnrtu'.indexOf(c) === -1) { escaped += '\\' // no valid escape character } escaped += c } else if (c === '"') { escaped += '\\"' } else { escaped += c } i++ } return escaped } /** * update the object name according to the callback onNodeName * @private */ updateNodeName () { const count = this.childs ? this.childs.length : 0 let nodeName if (this.type === 'object' || this.type === 'array') { if (this.editor.options.onNodeName) { try { const getValue = this.getValue.bind(this) nodeName = this.editor.options.onNodeName({ path: this.getPath(), size: count, type: this.type, get value () { return getValue() } }) } catch (err) { console.error('Error in onNodeName callback: ', err) } } this.dom.value.textContent = (this.type === 'object') ? ('{' + (nodeName || count) + '}') : ('[' + (nodeName || count) + ']') } } /** * update recursively the object's and its children's name. * @private */ recursivelyUpdateNodeName () { if (this.expanded) { this.updateNodeName() if (this.childs !== 'undefined') { let i for (i in this.childs) { this.childs[i].recursivelyUpdateNodeName() } } } } } // debounce interval for keyboard input in milliseconds Node.prototype.DEBOUNCE_INTERVAL = 150 // search will stop iterating as soon as the max is reached Node.prototype.MAX_SEARCH_RESULTS = 999 // default number of child nodes to display const DEFAULT_MAX_VISIBLE_CHILDS = 100 // stores the element name currently having the focus Node.focusElement = undefined /** * Select all text in an editable div after a delay of 0 ms * @param {Element} editableDiv */ Node.select = editableDiv => { setTimeout(() => { selectContentEditable(editableDiv) }, 0) } /** * DragStart event, fired on mousedown on the dragarea at the left side of a Node * @param {Node[] | Node} nodes * @param {Event} event */ Node.onDragStart = (nodes, event) => { if (!Array.isArray(nodes)) { return Node.onDragStart([nodes], event) } if (nodes.length === 0) { return } const firstNode = nodes[0] const lastNode = nodes[nodes.length - 1] const parent = firstNode.parent const draggedNode = Node.getNodeFromTarget(event.target) const editor = firstNode.editor // in case of multiple selected nodes, offsetY prevents the selection from // jumping when you start dragging one of the lower down nodes in the selection const offsetY = getAbsoluteTop(draggedNode.dom.tr) - getAbsoluteTop(firstNode.dom.tr) if (!editor.mousemove) { editor.mousemove = addEventListener(event.view, 'mousemove', event => { Node.onDrag(nodes, event) }) } if (!editor.mouseup) { editor.mouseup = addEventListener(event.view, 'mouseup', event => { Node.onDragEnd(nodes, event) }) } editor.highlighter.lock() editor.drag = { oldCursor: document.body.style.cursor, oldSelection: editor.getDomSelection(), oldPaths: nodes.map(getInternalPath), oldParent: parent, oldNextNode: parent.childs[lastNode.getIndex() + 1] || parent.append, oldParentPathRedo: parent.getInternalPath(), oldIndexRedo: firstNode.getIndex(), mouseX: event.pageX, offsetY, level: firstNode.getLevel() } document.body.style.cursor = 'move' event.preventDefault() } /** * Drag event, fired when moving the mouse while dragging a Node * @param {Node[] | Node} nodes * @param {Event} event */ Node.onDrag = (nodes, event) => { if (!Array.isArray(nodes)) { return Node.onDrag([nodes], event) } if (nodes.length === 0) { return } // TODO: this method has grown too large. Split it in a number of methods const editor = nodes[0].editor const mouseY = event.pageY - editor.drag.offsetY const mouseX = event.pageX let trPrev, trNext, trFirst, trLast, trRoot let nodePrev, nodeNext let topPrev, topFirst, bottomNext, heightNext let moved = false // TODO: add an ESC option, which resets to the original position // move up/down const firstNode = nodes[0] const trThis = firstNode.dom.tr let topThis = getAbsoluteTop(trThis) const heightThis = trThis.offsetHeight if (mouseY < topThis) { // move up trPrev = trThis do { trPrev = trPrev.previousSibling nodePrev = Node.getNodeFromTarget(trPrev) topPrev = trPrev ? getAbsoluteTop(trPrev) : 0 } while (trPrev && mouseY < topPrev) if (nodePrev && !nodePrev.parent) { nodePrev = undefined } if (!nodePrev) { // move to the first node trRoot = trThis.parentNode.firstChild trPrev = trRoot ? trRoot.nextSibling : undefined nodePrev = Node.getNodeFromTarget(trPrev) if (nodePrev === firstNode) { nodePrev = undefined } } if (nodePrev && nodePrev.isVisible()) { // check if mouseY is really inside the found node trPrev = nodePrev.dom.tr topPrev = trPrev ? getAbsoluteTop(trPrev) : 0 if (mouseY > topPrev + heightThis) { nodePrev = undefined } } if ( nodePrev && (editor.options.limitDragging === false || nodePrev.parent === nodes[0].parent) ) { nodes.forEach(node => { nodePrev.parent.moveBefore(node, nodePrev) }) moved = true } } else { // move down const lastNode = nodes[nodes.length - 1] trLast = (lastNode.expanded && lastNode.append) ? lastNode.append.getDom() : lastNode.dom.tr trFirst = trLast ? trLast.nextSibling : undefined if (trFirst) { topFirst = getAbsoluteTop(trFirst) trNext = trFirst do { nodeNext = Node.getNodeFromTarget(trNext) if (trNext) { bottomNext = trNext.nextSibling ? getAbsoluteTop(trNext.nextSibling) : 0 heightNext = trNext ? (bottomNext - topFirst) : 0 if (nodeNext && nodeNext.parent.childs.length === nodes.length && nodeNext.parent.childs[nodes.length - 1] === lastNode) { // We are about to remove the last child of this parent, // which will make the parents appendNode visible. topThis += 27 // TODO: dangerous to suppose the height of the appendNode a constant of 27 px. } trNext = trNext.nextSibling } } while (trNext && mouseY > topThis + heightNext) if (nodeNext && nodeNext.parent) { // calculate the desired level const diffX = (mouseX - editor.drag.mouseX) const diffLevel = Math.round(diffX / 24 / 2) const level = editor.drag.level + diffLevel // desired level let levelNext = nodeNext.getLevel() // level to be // find the best fitting level (move upwards over the append nodes) trPrev = nodeNext.dom.tr && nodeNext.dom.tr.previousSibling while (levelNext < level && trPrev) { nodePrev = Node.getNodeFromTarget(trPrev) const isDraggedNode = nodes.some(node => node === nodePrev || nodePrev.isDescendantOf(node)) if (isDraggedNode) { // neglect the dragged nodes themselves and their childs } else if (nodePrev instanceof AppendNode) { const childs = nodePrev.parent.childs if (childs.length !== nodes.length || childs[nodes.length - 1] !== lastNode) { // non-visible append node of a list of childs // consisting of not only this node (else the // append node will change into a visible "empty" // text when removing this node). nodeNext = Node.getNodeFromTarget(trPrev) levelNext = nodeNext.getLevel() } else { break } } else { break } trPrev = trPrev.previousSibling } if (nodeNext instanceof AppendNode && !nodeNext.isVisible() && nodeNext.parent.showMore.isVisible()) { nodeNext = nodeNext._nextNode() } // move the node when its position is changed if ( nodeNext && (editor.options.limitDragging === false || nodeNext.parent === nodes[0].parent) && nodeNext.dom.tr && nodeNext.dom.tr !== trLast.nextSibling ) { nodes.forEach(node => { nodeNext.parent.moveBefore(node, nodeNext) }) moved = true } } } } if (moved) { // update the dragging parameters when moved editor.drag.mouseX = mouseX editor.drag.level = firstNode.getLevel() } // auto scroll when hovering around the top of the editor editor.startAutoScroll(mouseY) event.preventDefault() } /** * Drag event, fired on mouseup after having dragged a node * @param {Node[] | Node} nodes * @param {Event} event */ Node.onDragEnd = (nodes, event) => { if (!Array.isArray(nodes)) { return Node.onDrag([nodes], event) } if (nodes.length === 0) { return } const firstNode = nodes[0] const editor = firstNode.editor // set focus to the context menu button of the first node if (firstNode && firstNode.dom.menu) { firstNode.dom.menu.focus() } const oldParentPath = editor.drag.oldParent.getInternalPath() const newParentPath = firstNode.parent.getInternalPath() const sameParent = editor.drag.oldParent === firstNode.parent const oldIndex = editor.drag.oldNextNode.getIndex() const newIndex = firstNode.getIndex() const oldParentPathRedo = editor.drag.oldParentPathRedo const oldIndexRedo = editor.drag.oldIndexRedo const newIndexRedo = (sameParent && oldIndexRedo < newIndex) ? (newIndex + nodes.length) : newIndex if (!sameParent || oldIndexRedo !== newIndex) { // only register this action if the node is actually moved to another place editor._onAction('moveNodes', { count: nodes.length, fieldNames: nodes.map(getField), oldParentPath, newParentPath, oldIndex, newIndex, oldIndexRedo, newIndexRedo, oldParentPathRedo, newParentPathRedo: null, // This is a hack, value will be filled in during undo oldSelection: editor.drag.oldSelection, newSelection: editor.getDomSelection() }) } document.body.style.cursor = editor.drag.oldCursor editor.highlighter.unlock() nodes.forEach(node => { node.updateDom() if (event.target !== node.dom.drag && event.target !== node.dom.menu) { editor.highlighter.unhighlight() } }) delete editor.drag if (editor.mousemove) { removeEventListener(event.view, 'mousemove', editor.mousemove) delete editor.mousemove } if (editor.mouseup) { removeEventListener(event.view, 'mouseup', editor.mouseup) delete editor.mouseup } // Stop any running auto scroll editor.stopAutoScroll() event.preventDefault() } /** * find an enum definition in a JSON schema, as property `enum` or inside * one of the schemas composites (`oneOf`, `anyOf`, `allOf`) * @param {Object} schema * @return {Array | null} Returns the enum when found, null otherwise. * @private */ Node._findEnum = schema => { if (schema.enum) { return schema.enum } const composite = schema.oneOf || schema.anyOf || schema.allOf if (composite) { const match = composite.filter(entry => entry.enum) if (match.length > 0) { return match[0].enum } } return null } /** * Implementation for _findSchema * @param {Object} topLevelSchema * @param {Object} schemaRefs * @param {Array.} path * @param {Object} currentSchema * @return {Object | boolean | null} * @private */ Node._findOneSchema = (topLevelSchema, schemaRefs, path, currentSchema) => { const nextPath = path.slice(1, path.length) const nextKey = path[0] if (typeof currentSchema === 'object' && '$ref' in currentSchema && typeof currentSchema.$ref === 'string') { const ref = currentSchema.$ref if (ref in schemaRefs) { currentSchema = schemaRefs[ref] } else if (ref.startsWith('#/')) { const refPath = ref.substring(2).split('/') currentSchema = topLevelSchema for (const segment of refPath) { if (segment in currentSchema) { currentSchema = currentSchema[segment] } else { throw Error(`Unable to resolve reference ${ref}`) } } } else if (ref.match(/#\//g)?.length === 1) { const [schemaUrl, relativePath] = ref.split('#/') if (schemaUrl in schemaRefs) { const referencedSchema = schemaRefs[schemaUrl] const reference = { $ref: '#/'.concat(relativePath) } const auxNextPath = [] auxNextPath.push(nextKey) if (nextPath.length > 0) { auxNextPath.push(...nextPath) } return Node._findSchema(referencedSchema, schemaRefs, auxNextPath, reference) } else { throw Error(`Unable to resolve reference ${ref}`) } } else { throw Error(`Unable to resolve reference ${ref}`) } } // We have no more path segments to resolve, return the currently found schema // We do this here, after resolving references, in case of the leaf schema beeing a reference if (nextKey === undefined) { return currentSchema } if (typeof nextKey === 'string') { if (typeof currentSchema.properties === 'object' && currentSchema.properties !== null && nextKey in currentSchema.properties) { currentSchema = currentSchema.properties[nextKey] return Node._findSchema(topLevelSchema, schemaRefs, nextPath, currentSchema) } if (typeof currentSchema.patternProperties === 'object' && currentSchema.patternProperties !== null) { for (const prop in currentSchema.patternProperties) { if (nextKey.match(prop)) { currentSchema = currentSchema.patternProperties[prop] return Node._findSchema(topLevelSchema, schemaRefs, nextPath, currentSchema) } } } if (typeof currentSchema.additionalProperties === 'object') { currentSchema = currentSchema.additionalProperties return Node._findSchema(topLevelSchema, schemaRefs, nextPath, currentSchema) } return null } if (typeof nextKey === 'number' && typeof currentSchema.items === 'object' && currentSchema.items !== null) { currentSchema = currentSchema.items return Node._findSchema(topLevelSchema, schemaRefs, nextPath, currentSchema) } return null } /** * Return the part of a JSON schema matching given path. * * Note that this attempts to find *a* schema matching the path, not necessarily * the best / most appropriate. For example, oneOf vs. anyOf vs. allOf may * result in different schemas being applied in practice. * * @param {Object} topLevelSchema * @param {Object} schemaRefs * @param {Array.} path * @param {Object} currentSchema * @return {Object | null} * @private */ Node._findSchema = (topLevelSchema, schemaRefs, path, currentSchema = topLevelSchema) => { let possibleSchemas = [currentSchema] for (const subSchemas of [currentSchema.oneOf, currentSchema.anyOf, currentSchema.allOf]) { if (Array.isArray(subSchemas)) { possibleSchemas = possibleSchemas.concat(subSchemas) } } let fallback = null for (const schema of possibleSchemas) { const result = Node._findOneSchema(topLevelSchema, schemaRefs, path, schema) // Although we don't attempt to find the best / most appropriate schema, we // can at least attempt to find something more specific than `true`. if (result === true) { fallback = true continue } else if (result !== null) { return result } } return fallback } /** * Remove nodes * @param {Node[] | Node} nodes */ Node.onRemove = nodes => { if (!Array.isArray(nodes)) { return Node.onRemove([nodes]) } if (nodes && nodes.length > 0) { const firstNode = nodes[0] const parent = firstNode.parent const editor = firstNode.editor const firstIndex = firstNode.getIndex() editor.highlighter.unhighlight() // adjust the focus const oldSelection = editor.getDomSelection() Node.blurNodes(nodes) const newSelection = editor.getDomSelection() // store the paths before removing them (needed for history) const paths = nodes.map(getInternalPath) // remove the nodes nodes.forEach(node => { node.parent._remove(node) }) // store history action editor._onAction('removeNodes', { nodes, paths, parentPath: parent.getInternalPath(), index: firstIndex, oldSelection, newSelection }) } } /** * Duplicate nodes * duplicated nodes will be added right after the original nodes * @param {Node[] | Node} nodes */ Node.onDuplicate = nodes => { if (!Array.isArray(nodes)) { return Node.onDuplicate([nodes]) } if (nodes && nodes.length > 0) { const lastNode = nodes[nodes.length - 1] const parent = lastNode.parent const editor = lastNode.editor editor.deselect(editor.multiselection.nodes) // duplicate the nodes const oldSelection = editor.getDomSelection() let afterNode = lastNode const clones = nodes.map(node => { const clone = node.clone() if (node.parent.type === 'object') { const existingFieldNames = node.parent.getFieldNames() clone.field = findUniqueName(node.field, existingFieldNames) } parent.insertAfter(clone, afterNode) afterNode = clone return clone }) // set selection to the duplicated nodes if (nodes.length === 1) { if (clones[0].parent.type === 'object') { // when duplicating a single object property, // set focus to the field and keep the original field name clones[0].dom.field.innerHTML = nodes[0]._escapeHTML(nodes[0].field) clones[0].focus('field') } else { clones[0].focus() } } else { editor.select(clones) } const newSelection = editor.getDomSelection() editor._onAction('duplicateNodes', { paths: nodes.map(getInternalPath), clonePaths: clones.map(getInternalPath), afterPath: lastNode.getInternalPath(), parentPath: parent.getInternalPath(), oldSelection, newSelection }) } } /** * Find the node from an event target * @param {HTMLElement} target * @return {Node | undefined} node or undefined when not found * @static */ Node.getNodeFromTarget = target => { while (target) { if (target.node) { return target.node } target = target.parentNode } return undefined } /** * Test whether target is a child of the color DOM of a node * @param {HTMLElement} target * @returns {boolean} */ Node.targetIsColorPicker = target => { const node = Node.getNodeFromTarget(target) if (node) { let parent = target && target.parentNode while (parent) { if (parent === node.dom.color) { return true } parent = parent.parentNode } } return false } /** * Remove the focus of given nodes, and move the focus to the (a) node before, * (b) the node after, or (c) the parent node. * @param {Array. | Node} nodes */ Node.blurNodes = nodes => { if (!Array.isArray(nodes)) { Node.blurNodes([nodes]) return } const firstNode = nodes[0] const parent = firstNode.parent const firstIndex = firstNode.getIndex() if (parent.childs[firstIndex + nodes.length]) { parent.childs[firstIndex + nodes.length].focus() } else if (parent.childs[firstIndex - 1]) { parent.childs[firstIndex - 1].focus() } else { parent.focus() } } // helper function to get the internal path of a node function getInternalPath (node) { return node.getInternalPath() } // helper function to get the field of a node function getField (node) { return node.getField() } function hasOwnProperty (object, key) { return Object.prototype.hasOwnProperty.call(object, key) } // TODO: find a nicer solution to resolve this circular dependency between Node and AppendNode // idea: introduce properties .isAppendNode and .isNode and use that instead of instanceof AppendNode checks const AppendNode = appendNodeFactory(Node) const ShowMoreNode = showMoreNodeFactory(Node) ================================================ FILE: src/js/NodeHistory.js ================================================ '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, 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 } } ================================================ FILE: src/js/SchemaTextCompleter.js ================================================ 'use strict' import * as jsonMap from 'json-source-map' import { isArray, isObject, uniqueMergeArrays, asyncExec } from './util' /** * SchemaTextCompleter class implements the ace ext-language_tools completer API, * and suggests completions for the text editor that are relative * to the cursor position and the json schema */ export class SchemaTextCompleter { constructor (schema, schemaRefs) { this.schema = schema this.schemaRefs = schemaRefs || {} this.suggestions = {} this.suggestionsRefs = {} this._buildSuggestions() } _buildSuggestions () { this._handleSchemaEntry('', this.schema, this.suggestions) for (const refName in this.schemaRefs) { this.suggestionsRefs[refName] = {} this._handleSchemaEntry('', this.schemaRefs[refName], this.suggestionsRefs[refName]) } } _handleRef (currectPath, refName, suggestionsObj) { suggestionsObj[currectPath] = suggestionsObj[currectPath] || {} suggestionsObj[currectPath].refs = suggestionsObj[currectPath].refs || [] suggestionsObj[currectPath].refs = uniqueMergeArrays(suggestionsObj[currectPath].refs, [refName]) } _handleSchemaEntry (currectPath, schemaNode, suggestionsObj) { if (!schemaNode) { console.error('SchemaTextCompleter: schema node is missing for path', currectPath) return } if (schemaNode.$ref) { this._handleRef(currectPath, schemaNode.$ref, suggestionsObj) return } const ofConditionEntry = this._checkOfConditon(schemaNode) if (ofConditionEntry) { this._handleOfCondition(currectPath, schemaNode[ofConditionEntry], suggestionsObj) return } switch (schemaNode.type) { case 'object': this._handleObject(currectPath, schemaNode, suggestionsObj) break case 'string': case 'number': case 'integer': this._handlePrimitive(currectPath, schemaNode, suggestionsObj) break case 'boolean': this._handleBoolean(currectPath, schemaNode, suggestionsObj) break case 'array': this._handleArray(currectPath, schemaNode, suggestionsObj) } } _handleObject (currectPath, schemaNode, suggestionsObj) { if (isObject(schemaNode.properties)) { const props = Object.keys(schemaNode.properties) suggestionsObj[currectPath] = suggestionsObj[currectPath] || {} suggestionsObj[currectPath].props = suggestionsObj[currectPath].props || [] suggestionsObj[currectPath].props = uniqueMergeArrays(suggestionsObj[currectPath].props, props) props.forEach((prop) => { asyncExec(() => { this._handleSchemaEntry(`${currectPath}/${prop}`, schemaNode.properties[prop], suggestionsObj) }) }) } } _handlePrimitive (currectPath, schemaNode, suggestionsObj) { suggestionsObj[currectPath] = suggestionsObj[currectPath] || {} if (isArray(schemaNode.examples)) { suggestionsObj[currectPath].examples = suggestionsObj[currectPath].examples || [] suggestionsObj[currectPath].examples = uniqueMergeArrays(suggestionsObj[currectPath].examples, schemaNode.examples) } if (isArray(schemaNode.enum)) { suggestionsObj[currectPath].enum = suggestionsObj[currectPath].enum || [] suggestionsObj[currectPath].enum = uniqueMergeArrays(suggestionsObj[currectPath].enum, schemaNode.enum) } } _handleBoolean (currectPath, schemaNode, suggestionsObj) { if (!suggestionsObj[currectPath]) { suggestionsObj[currectPath] = { bool: [true, false] } } } _handleArray (currectPath, schemaNode, suggestionsObj) { if (schemaNode.items) { asyncExec(() => { this._handleSchemaEntry(`${currectPath}/\\d+`, schemaNode.items, suggestionsObj) }) } } _handleOfCondition (currectPath, schemaNode, suggestionsObj) { if (schemaNode && schemaNode.length) { schemaNode.forEach(schemaEntry => { asyncExec(() => { this._handleSchemaEntry(currectPath, schemaEntry, suggestionsObj) }) }) } } _checkOfConditon (entry) { if (!entry) { return } if (entry.oneOf) { return 'oneOf' } if (entry.anyOf) { return 'anyOf' } if (entry.allOf) { return 'allOf' } } getCompletions (editor, session, pos, prefix, callback) { try { const map = jsonMap.parse(session.getValue()) const pointers = map.pointers || {} const processCompletionsCallback = (suggestions) => { let completions = [] let score = 0 const appendSuggesions = (type) => { const typeTitle = { props: 'property', enum: 'enum', bool: 'boolean', examples: 'examples' } if (suggestions && suggestions[type]?.length) { completions = completions.concat(suggestions[type].map(term => { return { caption: term + '', meta: `schema [${typeTitle[type]}]`, score: score++, value: term + '' } })) } } appendSuggesions('props') appendSuggesions('enum') appendSuggesions('bool') appendSuggesions('examples') if (completions.length) { callback(null, completions) } } Object.keys(pointers).forEach((ptr) => { asyncExec(() => { const matchPointersToPath = (pointer, currentSuggestions, path) => { const option = Object.keys(currentSuggestions).reduce((last, key) => { if (new RegExp(`^${path}${key}`).test(pointer)) { if (!last || last.length < key.length) { return key } } return last }, null) if (typeof option === 'string') { if (currentSuggestions[option]?.refs?.length) { const mergedSuggestions = {} for (const idx in currentSuggestions[option].refs) { const refName = currentSuggestions[option].refs[idx] if (this.suggestionsRefs[refName]) { const refSuggestion = matchPointersToPath(pointer, this.suggestionsRefs[refName], `${path}${option}`) if (refSuggestion?.enum) { mergedSuggestions.enum = uniqueMergeArrays(mergedSuggestions.enum, refSuggestion.enum) } if (refSuggestion?.examples) { mergedSuggestions.examples = uniqueMergeArrays(mergedSuggestions.examples, refSuggestion.examples) } if (refSuggestion?.bool) { mergedSuggestions.bool = uniqueMergeArrays(mergedSuggestions.bool, refSuggestion.bool) } if (refSuggestion?.props) { mergedSuggestions.props = uniqueMergeArrays(mergedSuggestions.props, refSuggestion.props) } } } return mergedSuggestions } else if (new RegExp(`^${path}${option}$`).test(pointer)) { // console.log('SchemaTextCompleter: Text suggestion match', { path: pointer, schemaPath: `${path}${option}`, suggestions: currentSuggestions[option] }) return currentSuggestions[option] } } } let selectedPtr if (pointers[ptr].key?.line === pos.row) { if (pos.column >= pointers[ptr].key.column && pos.column <= pointers[ptr].keyEnd.column) { selectedPtr = ptr.slice(0, ptr.lastIndexOf('/')) } } if (pointers[ptr].value?.line === pos.row && pointers[ptr].value?.line === pointers[ptr].valueEnd?.line) { // multiline values are objects if (pos.column >= pointers[ptr].value.column && pos.column <= pointers[ptr].valueEnd.column) { selectedPtr = ptr } } if (selectedPtr) { const chosenCompletions = matchPointersToPath(selectedPtr, this.suggestions, '') processCompletionsCallback(chosenCompletions) } }) }) } catch (e) { // probably not valid json, ignore. } } } ================================================ FILE: src/js/SearchBox.js ================================================ '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() } } ================================================ FILE: src/js/TreePath.js ================================================ '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, { limitHeight: true }) 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 } } } ================================================ FILE: src/js/ace/index.js ================================================ let ace if (window.ace) { // use the already loaded instance of Ace ace = window.ace } else { try { // load Ace editor ace = require('ace-builds/src-noconflict/ace') // load required Ace plugins require('ace-builds/src-noconflict/mode-json') require('ace-builds/src-noconflict/ext-searchbox') require('ace-builds/src-noconflict/ext-language_tools') // 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 ================================================ FILE: src/js/ace/theme-jsoneditor.js ================================================ /* ***** 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 { 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) }) ================================================ FILE: src/js/appendNodeFactory.js ================================================ '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, 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 } ================================================ FILE: src/js/assets/jsonlint/README.md ================================================ 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. ================================================ FILE: src/js/assets/jsonlint/jsonlint.js ================================================ /* 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); } ================================================ FILE: src/js/assets/selectr/README.md ================================================ 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: src/js/assets/selectr/selectr.js ================================================ /*! * Selectr 2.4.13 * http://mobius.ovh/docs/selectr * * Released under the MIT license */ 'use strict'; /** * Event Emitter */ var Events = function() {}; /** * Event Prototype * @type {Object} */ Events.prototype = { /** * Add custom event listener * @param {String} event Event type * @param {Function} func Callback * @return {Void} */ on: function(event, func) { this._events = this._events || {}; this._events[event] = this._events[event] || []; this._events[event].push(func); }, /** * Remove custom event listener * @param {String} event Event type * @param {Function} func Callback * @return {Void} */ off: function(event, func) { this._events = this._events || {}; if (event in this._events === false) return; this._events[event].splice(this._events[event].indexOf(func), 1); }, /** * Fire a custom event * @param {String} event Event type * @return {Void} */ emit: function(event /* , args... */ ) { this._events = this._events || {}; if (event in this._events === false) return; for (var i = 0; i < this._events[event].length; i++) { this._events[event][i].apply(this, Array.prototype.slice.call(arguments, 1)); } } }; /** * Event mixin * @param {Object} obj * @return {Object} */ Events.mixin = function(obj) { var props = ['on', 'off', 'emit']; for (var i = 0; i < props.length; i++) { if (typeof obj === 'function') { obj.prototype[props[i]] = Events.prototype[props[i]]; } else { obj[props[i]] = Events.prototype[props[i]]; } } return obj; }; /** * Helpers * @type {Object} */ var util = { escapeRegExp: function(str) { // source from lodash 3.0.0 var _reRegExpChar = /[\\^$.*+?()[\]{}|]/g; var _reHasRegExpChar = new RegExp(_reRegExpChar.source); return (str && _reHasRegExpChar.test(str)) ? str.replace(_reRegExpChar, '\\$&') : str; }, extend: function(src, props) { for (var prop in props) { if (props.hasOwnProperty(prop)) { var val = props[prop]; if (val && Object.prototype.toString.call(val) === "[object Object]") { src[prop] = src[prop] || {}; util.extend(src[prop], val); } else { src[prop] = val; } } } return src; }, each: function(a, b, c) { if ("[object Object]" === Object.prototype.toString.call(a)) { for (var d in a) { if (Object.prototype.hasOwnProperty.call(a, d)) { b.call(c, d, a[d], a); } } } else { for (var e = 0, f = a.length; e < f; e++) { b.call(c, e, a[e], a); } } }, createElement: function(e, a) { var d = document, el = d.createElement(e); if (a && "[object Object]" === Object.prototype.toString.call(a)) { var i; for (i in a) if (i in el) el[i] = a[i]; else if ("html" === i) el.innerHTML = a[i]; else el.setAttribute(i, a[i]); } return el; }, hasClass: function(a, b) { if (a) return a.classList ? a.classList.contains(b) : !!a.className && !!a.className.match(new RegExp("(\\s|^)" + b + "(\\s|$)")); }, addClass: function(a, b) { if (!util.hasClass(a, b)) { if (a.classList) { a.classList.add(b); } else { a.className = a.className.trim() + " " + b; } } }, removeClass: function(a, b) { if (util.hasClass(a, b)) { if (a.classList) { a.classList.remove(b); } else { a.className = a.className.replace(new RegExp("(^|\\s)" + b.split(" ").join("|") + "(\\s|$)", "gi"), " "); } } }, closest: function(el, fn) { return el && el !== document.body && (fn(el) ? el : util.closest(el.parentNode, fn)); }, isInt: function(val) { return typeof val === 'number' && isFinite(val) && Math.floor(val) === val; }, debounce: function(a, b, c) { var d; return function() { var e = this, f = arguments, g = function() { d = null; if (!c) a.apply(e, f); }, h = c && !d; clearTimeout(d); d = setTimeout(g, b); if (h) { a.apply(e, f); } }; }, rect: function(el, abs) { var w = window; var r = el.getBoundingClientRect(); var x = abs ? w.pageXOffset : 0; var y = abs ? w.pageYOffset : 0; return { bottom: r.bottom + y, height: r.height, left: r.left + x, right: r.right + x, top: r.top + y, width: r.width }; }, includes: function(a, b) { return a.indexOf(b) > -1; }, startsWith: function(a, b) { return a.substr( 0, b.length ) === b; }, truncate: function(el) { while (el.firstChild) { el.removeChild(el.firstChild); } } }; function isset(obj, prop) { return obj.hasOwnProperty(prop) && (obj[prop] === true || obj[prop].length); } /** * Append an item to the list * @param {Object} item * @param {Object} custom * @return {Void} */ function appendItem(item, parent, custom) { if (item.parentNode) { if (!item.parentNode.parentNode) { parent.appendChild(item.parentNode); } } else { parent.appendChild(item); } util.removeClass(item, "excluded"); if (!custom) { // remove any highlighting, without xss item.textContent = item.textContent; } } /** * Render the item list * @return {Void} */ var render = function() { if (this.items.length) { var f = document.createDocumentFragment(); if (this.config.pagination) { var pages = this.pages.slice(0, this.pageIndex); util.each(pages, function(i, items) { util.each(items, function(j, item) { appendItem(item, f, this.customOption); }, this); }, this); } else { util.each(this.items, function(i, item) { appendItem(item, f, this.customOption); }, this); } // highlight first selected option if any; first option otherwise if (f.childElementCount) { util.removeClass(this.items[this.navIndex], "active"); this.navIndex = ( f.querySelector(".selectr-option.selected") || f.querySelector(".selectr-option") ).idx; util.addClass(this.items[this.navIndex], "active"); } this.tree.appendChild(f); } }; /** * Dismiss / close the dropdown * @param {obj} e * @return {void} */ var dismiss = function(e) { var target = e.target; if (!this.container.contains(target) && (this.opened || util.hasClass(this.container, "notice"))) { this.close(); } }; /** * Build a list item from the HTMLOptionElement * @param {int} i HTMLOptionElement index * @param {HTMLOptionElement} option * @param {bool} group Has parent optgroup * @return {void} */ var createItem = function(option, data) { data = data || option; var elementData = { class: "selectr-option", role: "treeitem", "aria-selected": false }; if(this.customOption){ elementData.html = this.config.renderOption(data); // asume xss prevention in custom render function } else{ elementData.textContent = option.textContent; // treat all as plain text } var opt = util.createElement("li",elementData); opt.idx = option.idx; this.items.push(opt); if (option.defaultSelected) { this.defaultSelected.push(option.idx); } if (option.disabled) { opt.disabled = true; util.addClass(opt, "disabled"); } return opt; }; /** * Build the container * @return {Void} */ var build = function() { this.requiresPagination = this.config.pagination && this.config.pagination > 0; // Set width if (isset(this.config, "width")) { if (util.isInt(this.config.width)) { this.width = this.config.width + "px"; } else { if (this.config.width === "auto") { this.width = "100%"; } else if (util.includes(this.config.width, "%")) { this.width = this.config.width; } } } this.container = util.createElement("div", { class: "selectr-container" }); // Custom className if (this.config.customClass) { util.addClass(this.container, this.config.customClass); } // Mobile device if (this.mobileDevice) { util.addClass(this.container, "selectr-mobile"); } else { util.addClass(this.container, "selectr-desktop"); } // Hide the HTMLSelectElement and prevent focus this.el.tabIndex = -1; // Native dropdown if (this.config.nativeDropdown || this.mobileDevice) { util.addClass(this.el, "selectr-visible"); } else { util.addClass(this.el, "selectr-hidden"); } this.selected = util.createElement("div", { class: "selectr-selected", disabled: this.disabled, tabIndex: 0, "aria-expanded": false }); this.label = util.createElement(this.el.multiple ? "ul" : "span", { class: "selectr-label" }); var dropdown = util.createElement("div", { class: "selectr-options-container" }); this.tree = util.createElement("ul", { class: "selectr-options", role: "tree", "aria-hidden": true, "aria-expanded": false }); this.notice = util.createElement("div", { class: "selectr-notice" }); this.el.setAttribute("aria-hidden", true); if (this.disabled) { this.el.disabled = true; } if (this.el.multiple) { util.addClass(this.label, "selectr-tags"); util.addClass(this.container, "multiple"); // Collection of tags this.tags = []; // Collection of selected values // #93 defaultSelected = false did not work as expected this.selectedValues = (this.config.defaultSelected) ? this.getSelectedProperties('value') : []; // Collection of selected indexes this.selectedIndexes = this.getSelectedProperties('idx'); } else { // #93 defaultSelected = false did not work as expected // these values were undefined this.selectedValue = null; this.selectedIndex = -1; } this.selected.appendChild(this.label); if (this.config.clearable) { this.selectClear = util.createElement("button", { class: "selectr-clear", type: "button" }); this.container.appendChild(this.selectClear); util.addClass(this.container, "clearable"); } if (this.config.taggable) { var li = util.createElement('li', { class: 'input-tag' }); this.input = util.createElement("input", { class: "selectr-tag-input", placeholder: this.config.tagPlaceholder, tagIndex: 0, autocomplete: "off", autocorrect: "off", autocapitalize: "off", spellcheck: "false", role: "textbox", type: "search" }); li.appendChild(this.input); this.label.appendChild(li); util.addClass(this.container, "taggable"); this.tagSeperators = [","]; if (this.config.tagSeperators) { this.tagSeperators = this.tagSeperators.concat(this.config.tagSeperators); var _aTempEscapedSeperators = []; for(var _nTagSeperatorStepCount = 0; _nTagSeperatorStepCount < this.tagSeperators.length; _nTagSeperatorStepCount++){ _aTempEscapedSeperators.push(util.escapeRegExp(this.tagSeperators[_nTagSeperatorStepCount])); } this.tagSeperatorsRegex = new RegExp(_aTempEscapedSeperators.join('|'),'i'); } else { this.tagSeperatorsRegex = new RegExp(',','i'); } } if (this.config.searchable) { this.input = util.createElement("input", { class: "selectr-input", tagIndex: -1, autocomplete: "off", autocorrect: "off", autocapitalize: "off", spellcheck: "false", role: "textbox", type: "search", placeholder: this.config.messages.searchPlaceholder }); this.inputClear = util.createElement("button", { class: "selectr-input-clear", type: "button" }); this.inputContainer = util.createElement("div", { class: "selectr-input-container" }); this.inputContainer.appendChild(this.input); this.inputContainer.appendChild(this.inputClear); dropdown.appendChild(this.inputContainer); } dropdown.appendChild(this.notice); dropdown.appendChild(this.tree); // List of items for the dropdown this.items = []; // Establish options this.options = []; // Check for options in the element if (this.el.options.length) { this.options = [].slice.call(this.el.options); } // Element may have optgroups so // iterate element.children instead of element.options var group = false, j = 0; if (this.el.children.length) { util.each(this.el.children, function(i, element) { if (element.nodeName === "OPTGROUP") { group = util.createElement("ul", { class: "selectr-optgroup", role: "group", html: "
  • " + element.label + "
  • " }); util.each(element.children, function(x, el) { el.idx = j; group.appendChild(createItem.call(this, el, group)); j++; }, this); } else { element.idx = j; createItem.call(this, element); j++; } }, this); } // Options defined by the data option if (this.config.data && Array.isArray(this.config.data)) { this.data = []; var optgroup = false, option; group = false; j = 0; util.each(this.config.data, function(i, opt) { // Check for group options if (isset(opt, "children")) { optgroup = util.createElement("optgroup", { label: opt.text }); group = util.createElement("ul", { class: "selectr-optgroup", role: "group", html: "
  • " + opt.text + "
  • " }); util.each(opt.children, function(x, data) { option = new Option(data.text, data.value, false, data.hasOwnProperty("selected") && data.selected === true); option.disabled = isset(data, "disabled"); this.options.push(option); optgroup.appendChild(option); option.idx = j; group.appendChild(createItem.call(this, option, data)); this.data[j] = data; j++; }, this); this.el.appendChild(optgroup); } else { option = new Option(opt.text, opt.value, false, opt.hasOwnProperty("selected") && opt.selected === true); option.disabled = isset(opt, "disabled"); this.options.push(option); option.idx = j; createItem.call(this, option, opt); this.data[j] = opt; j++; } }, this); } this.setSelected(true); var first; this.navIndex = 0; for (var i = 0; i < this.items.length; i++) { first = this.items[i]; if (!util.hasClass(first, "disabled")) { util.addClass(first, "active"); this.navIndex = i; break; } } // Check for pagination / infinite scroll if (this.requiresPagination) { this.pageIndex = 1; // Create the pages this.paginate(); } this.container.appendChild(this.selected); this.container.appendChild(dropdown); this.placeEl = util.createElement("div", { class: "selectr-placeholder" }); // Set the placeholder this.setPlaceholder(); this.selected.appendChild(this.placeEl); // Disable if required if (this.disabled) { this.disable(); } this.el.parentNode.insertBefore(this.container, this.el); this.container.appendChild(this.el); }; /** * Navigate through the dropdown * @param {obj} e * @return {void} */ var navigate = function(e) { e = e || window.event; // Filter out the keys we don"t want if (!this.items.length || !this.opened || !util.includes([13, 38, 40], e.which)) { this.navigating = false; return; } e.preventDefault(); if (e.which === 13) { if ( this.noResults || (this.config.taggable && this.input.value.length > 0) ) { return false; } return this.change(this.navIndex); } var direction, prevEl = this.items[this.navIndex]; var lastIndex = this.navIndex; switch (e.which) { case 38: direction = 0; if (this.navIndex > 0) { this.navIndex--; } break; case 40: direction = 1; if (this.navIndex < this.items.length - 1) { this.navIndex++; } } this.navigating = true; // Instead of wasting memory holding a copy of this.items // with disabled / excluded options omitted, skip them instead while (util.hasClass(this.items[this.navIndex], "disabled") || util.hasClass(this.items[this.navIndex], "excluded")) { if (this.navIndex > 0 && this.navIndex < this.items.length -1) { if (direction) { this.navIndex++; } else { this.navIndex--; } } else { this.navIndex = lastIndex; break; } if (this.searching) { if (this.navIndex > this.tree.lastElementChild.idx) { this.navIndex = this.tree.lastElementChild.idx; break; } else if (this.navIndex < this.tree.firstElementChild.idx) { this.navIndex = this.tree.firstElementChild.idx; break; } } } // Autoscroll the dropdown during navigation var r = util.rect(this.items[this.navIndex]); if (!direction) { if (this.navIndex === 0) { this.tree.scrollTop = 0; } else if (r.top - this.optsRect.top < 0) { this.tree.scrollTop = this.tree.scrollTop + (r.top - this.optsRect.top); } } else { if (this.navIndex === 0) { this.tree.scrollTop = 0; } else if ((r.top + r.height) > (this.optsRect.top + this.optsRect.height)) { this.tree.scrollTop = this.tree.scrollTop + ((r.top + r.height) - (this.optsRect.top + this.optsRect.height)); } // Load another page if needed if (this.navIndex === this.tree.childElementCount - 1 && this.requiresPagination) { load.call(this); } } if (prevEl) { util.removeClass(prevEl, "active"); } util.addClass(this.items[this.navIndex], "active"); }; /** * Add a tag * @param {HTMLElement} item */ var addTag = function(item) { var that = this, r; var docFrag = document.createDocumentFragment(); var option = this.options[item.idx]; var data = this.data ? this.data[item.idx] : option; var elementData = { class: "selectr-tag" }; if (this.customSelected){ elementData.html = this.config.renderSelection(data); // asume xss prevention in custom render function } else { elementData.textContent = option.textContent; } var tag = util.createElement("li", elementData); var btn = util.createElement("button", { class: "selectr-tag-remove", type: "button" }); tag.appendChild(btn); // Set property to check against later tag.idx = item.idx; tag.tag = option.value; this.tags.push(tag); if (this.config.sortSelected) { var tags = this.tags.slice(); // Deal with values that contain numbers r = function(val, arr) { val.replace(/(\d+)|(\D+)/g, function(that, $1, $2) { arr.push([$1 || Infinity, $2 || ""]); }); }; tags.sort(function(a, b) { var x = [], y = [], ac, bc; if (that.config.sortSelected === true) { ac = a.tag; bc = b.tag; } else if (that.config.sortSelected === 'text') { ac = a.textContent; bc = b.textContent; } r(ac, x); r(bc, y); while (x.length && y.length) { var ax = x.shift(); var by = y.shift(); var nn = (ax[0] - by[0]) || ax[1].localeCompare(by[1]); if (nn) return nn; } return x.length - y.length; }); util.each(tags, function(i, tg) { docFrag.appendChild(tg); }); this.label.innerHTML = ""; } else { docFrag.appendChild(tag); } if (this.config.taggable) { this.label.insertBefore(docFrag, this.input.parentNode); } else { this.label.appendChild(docFrag); } }; /** * Remove a tag * @param {HTMLElement} item * @return {void} */ var removeTag = function(item) { var tag = false; util.each(this.tags, function(i, t) { if (t.idx === item.idx) { tag = t; } }, this); if (tag) { this.label.removeChild(tag); this.tags.splice(this.tags.indexOf(tag), 1); } }; /** * Load the next page of items * @return {void} */ var load = function() { var tree = this.tree; var scrollTop = tree.scrollTop; var scrollHeight = tree.scrollHeight; var offsetHeight = tree.offsetHeight; var atBottom = scrollTop >= (scrollHeight - offsetHeight); if ((atBottom && this.pageIndex < this.pages.length)) { var f = document.createDocumentFragment(); util.each(this.pages[this.pageIndex], function(i, item) { appendItem(item, f, this.customOption); }, this); tree.appendChild(f); this.pageIndex++; this.emit("selectr.paginate", { items: this.items.length, total: this.data.length, page: this.pageIndex, pages: this.pages.length }); } }; /** * Clear a search * @return {void} */ var clearSearch = function() { if (this.config.searchable || this.config.taggable) { this.input.value = null; this.searching = false; if (this.config.searchable) { util.removeClass(this.inputContainer, "active"); } if (util.hasClass(this.container, "notice")) { util.removeClass(this.container, "notice"); util.addClass(this.container, "open"); this.input.focus(); } util.each(this.items, function(i, item) { // Items that didn't match need the class // removing to make them visible again util.removeClass(item, "excluded"); // Remove the span element for underlining matched items if (!this.customOption) { // without xss item.textContent = item.textContent; } }, this); } }; /** * Query matching for searches. * Wraps matching text in a span.selectr-match. * * @param {string} query * @param {HTMLOptionElement} option element * @return {bool} true if matched; false otherwise */ var match = function(query, option) { var text = option.textContent; var RX = new RegExp( query, "ig" ); var result = RX.exec( text ); if (result) { // #102 stop xss option.innerHTML = ""; var span = document.createElement( "span" ); span.classList.add( "selectr-match" ); span.textContent = result[0]; option.appendChild( document.createTextNode( text.substring( 0, result.index ) ) ); option.appendChild( span ); option.appendChild( document.createTextNode( text.substring( RX.lastIndex ) ) ); return true; } return false; }; // Main Lib var Selectr = function(el, config) { if (!el) { throw new Error("You must supply either a HTMLSelectElement or a CSS3 selector string."); } this.el = el; // CSS3 selector string if (typeof el === "string") { this.el = document.querySelector(el); } if (this.el === null) { throw new Error("The element you passed to Selectr can not be found."); } if (this.el.nodeName.toLowerCase() !== "select") { throw new Error("The element you passed to Selectr is not a HTMLSelectElement."); } this.render(config); }; /** * Render the instance * @param {object} config * @return {void} */ Selectr.prototype.render = function(config) { if (this.rendered) return; /** * Default configuration options * @type {Object} */ var defaultConfig = { /** * Emulates browser behaviour by selecting the first option by default * @type {Boolean} */ defaultSelected: true, /** * Sets the width of the container * @type {String} */ width: "auto", /** * Enables/ disables the container * @type {Boolean} */ disabled: false, /** * Enables/ disables logic for mobile * @type {Boolean} */ disabledMobile: false, /** * Enables / disables the search function * @type {Boolean} */ searchable: true, /** * Enable disable the clear button * @type {Boolean} */ clearable: false, /** * Sort the tags / multiselect options * @type {Boolean} */ sortSelected: false, /** * Allow deselecting of select-one options * @type {Boolean} */ allowDeselect: false, /** * Close the dropdown when scrolling (@AlexanderReiswich, #11) * @type {Boolean} */ closeOnScroll: false, /** * Allow the use of the native dropdown (@jonnyscholes, #14) * @type {Boolean} */ nativeDropdown: false, /** * Allow the use of native typing behavior for toggling, searching, selecting * @type {boolean} */ nativeKeyboard: false, /** * Set the main placeholder * @type {String} */ placeholder: "Select an option...", /** * Allow the tagging feature * @type {Boolean} */ taggable: false, /** * Set the tag input placeholder (@labikmartin, #21, #22) * @type {String} */ tagPlaceholder: "Enter a tag...", messages: { noResults: "No results.", noOptions: "No options available.", maxSelections: "A maximum of {max} items can be selected.", tagDuplicate: "That tag is already in use.", searchPlaceholder: "Search options..." } }; // add instance reference (#87) this.el.selectr = this; // Merge defaults with user set config this.config = util.extend(defaultConfig, config); // Store type this.originalType = this.el.type; // Store tabIndex this.originalIndex = this.el.tabIndex; // Store defaultSelected options for form reset this.defaultSelected = []; // Store the original option count this.originalOptionCount = this.el.options.length; if (this.config.multiple || this.config.taggable) { this.el.multiple = true; } // Disabled? this.disabled = isset(this.config, "disabled"); this.opened = false; if (this.config.taggable) { this.config.searchable = false; } this.navigating = false; this.mobileDevice = false; if (!this.config.disabledMobile && /Android|webOS|iPhone|iPad|BlackBerry|Windows Phone|Opera Mini|IEMobile|Mobile/i.test(navigator.userAgent)) { this.mobileDevice = true; } this.customOption = this.config.hasOwnProperty("renderOption") && typeof this.config.renderOption === "function"; this.customSelected = this.config.hasOwnProperty("renderSelection") && typeof this.config.renderSelection === "function"; this.supportsEventPassiveOption = this.detectEventPassiveOption(); // Enable event emitter Events.mixin(this); build.call(this); this.bindEvents(); this.update(); this.optsRect = util.rect(this.tree); this.rendered = true; // Fixes macOS Safari bug #28 if (!this.el.multiple) { this.el.selectedIndex = this.selectedIndex; } var that = this; setTimeout(function() { that.emit("selectr.init"); }, 20); }; Selectr.prototype.getSelected = function () { var selected = this.el.querySelectorAll('option:checked'); return selected; }; Selectr.prototype.getSelectedProperties = function (prop) { var selected = this.getSelected(); var values = [].slice.call(selected) .map(function(option) { return option[prop]; }) .filter(function(i) { return i!==null && i!==undefined; }); return values; }; /** * Feature detection: addEventListener passive option * https://dom.spec.whatwg.org/#dom-addeventlisteneroptions-passive * https://github.com/WICG/EventListenerOptions/blob/gh-pages/explainer.md */ Selectr.prototype.detectEventPassiveOption = function () { var supportsPassiveOption = false; try { var opts = Object.defineProperty({}, 'passive', { get: function() { supportsPassiveOption = true; } }); window.addEventListener('test', null, opts); } catch (e) {} return supportsPassiveOption; }; /** * Attach the required event listeners */ Selectr.prototype.bindEvents = function() { var that = this; this.events = {}; this.events.dismiss = dismiss.bind(this); this.events.navigate = navigate.bind(this); this.events.reset = this.reset.bind(this); if (this.config.nativeDropdown || this.mobileDevice) { this.container.addEventListener("touchstart", function(e) { if (e.changedTouches[0].target === that.el) { that.toggle(); } }, this.supportsEventPassiveOption ? { passive: true } : false); this.container.addEventListener("click", function(e) { if (e.target === that.el) { that.toggle(); } }); var getChangedOptions = function(last, current) { var added=[], removed=last.slice(0); var idx; for (var i=0; i -1) removed.splice(idx, 1); else added.push(current[i]); } return [added, removed]; }; // Listen for the change on the native select // and update accordingly this.el.addEventListener("change", function(e) { if (e.__selfTriggered) { return; } if (that.el.multiple) { var indexes = that.getSelectedProperties('idx'); var changes = getChangedOptions(that.selectedIndexes, indexes); util.each(changes[0], function(i, idx) { that.select(idx); }, that); util.each(changes[1], function(i, idx) { that.deselect(idx); }, that); } else { if (that.el.selectedIndex > -1) { that.select(that.el.selectedIndex); } } }); } // Open the dropdown with Enter key if focused if ( this.config.nativeDropdown ) { this.container.addEventListener("keydown", function(e) { if (e.key === "Enter" && that.selected === document.activeElement) { // show native dropdown that.toggle(); // focus on it setTimeout(function() { that.el.focus(); }, 200); } }); } // Non-native dropdown this.selected.addEventListener("click", function(e) { if (!that.disabled) { that.toggle(); } e.preventDefault(); }); if ( this.config.nativeKeyboard ) { var typing = ''; var typingTimeout = null; this.selected.addEventListener("keydown", function (e) { // Do nothing if disabled, not focused, or modifier keys are pressed if ( that.disabled || that.selected !== document.activeElement || (e.altKey || e.ctrlKey || e.metaKey) ) { return; } // Open the dropdown on [enter], [ ], [↓], and [↑] keys if ( e.key === " " || (! that.opened && ["Enter", "ArrowUp", "ArrowDown"].indexOf(e.key) > -1) ) { that.toggle(); e.preventDefault(); e.stopPropagation(); return; } // Type to search if multiple; type to select otherwise // make sure e.key is a single, printable character // .length check is a short-circut to skip checking keys like "ArrowDown", etc. // prefer "codePoint" methods; they work with the full range of unicode if ( e.key.length <= 2 && String[String.fromCodePoint ? "fromCodePoint" : "fromCharCode"]( e.key[String.codePointAt ? "codePointAt" : "charCodeAt"]( 0 ) ) === e.key ) { if ( that.config.multiple ) { that.open(); if ( that.config.searchable ) { that.input.value = e.key; that.input.focus(); that.search( null, true ); } } else { if ( typingTimeout ) { clearTimeout( typingTimeout ); } typing += e.key; var found = that.search( typing, true ); if ( found && found.length ) { that.clear(); that.setValue( found[0].value ); } setTimeout(function () { typing = ''; }, 1000); } e.preventDefault(); e.stopPropagation(); return; } }); // Close the dropdown on [esc] key this.container.addEventListener("keyup", function (e) { if ( that.opened && e.key === "Escape" ) { that.close(); e.stopPropagation(); // keep focus so we can re-open easily if desired that.selected.focus(); } }); } // Remove tag this.label.addEventListener("click", function(e) { if (util.hasClass(e.target, "selectr-tag-remove")) { that.deselect(e.target.parentNode.idx); } }); // Clear input if (this.selectClear) { this.selectClear.addEventListener("click", this.clear.bind(this)); } // Prevent text selection this.tree.addEventListener("mousedown", function(e) { e.preventDefault(); }); // Select / deselect items this.tree.addEventListener("click", function(e) { var item = util.closest(e.target, function(el) { return el && util.hasClass(el, "selectr-option"); }); if (item) { if (!util.hasClass(item, "disabled")) { if (util.hasClass(item, "selected")) { if (that.el.multiple || !that.el.multiple && that.config.allowDeselect) { that.deselect(item.idx); } } else { that.select(item.idx); } if (that.opened && !that.el.multiple) { that.close(); } } } e.preventDefault(); e.stopPropagation(); }); // Mouseover list items this.tree.addEventListener("mouseover", function(e) { if (util.hasClass(e.target, "selectr-option")) { if (!util.hasClass(e.target, "disabled")) { util.removeClass(that.items[that.navIndex], "active"); util.addClass(e.target, "active"); that.navIndex = [].slice.call(that.items).indexOf(e.target); } } }); // Searchable if (this.config.searchable) { // Show / hide the search input clear button this.input.addEventListener("focus", function(e) { that.searching = true; }); this.input.addEventListener("blur", function(e) { that.searching = false; }); this.input.addEventListener("keyup", function(e) { that.search(); if (!that.config.taggable) { // Show / hide the search input clear button if (this.value.length) { util.addClass(this.parentNode, "active"); } else { util.removeClass(this.parentNode, "active"); } } }); // Clear the search input this.inputClear.addEventListener("click", function(e) { that.input.value = null; clearSearch.call(that); if (!that.tree.childElementCount) { render.call(that); } }); } if (this.config.taggable) { this.input.addEventListener("keyup", function(e) { that.search(); if (that.config.taggable && this.value.length) { var _sVal = this.value.trim(); if (_sVal.length && (e.which === 13 || that.tagSeperatorsRegex.test(_sVal) )) { var _sGrabbedTagValue = _sVal.replace(that.tagSeperatorsRegex, ''); _sGrabbedTagValue = util.escapeRegExp(_sGrabbedTagValue); _sGrabbedTagValue = _sGrabbedTagValue.trim(); var _oOption; if(_sGrabbedTagValue.length){ _oOption = that.add({ value: _sGrabbedTagValue, textContent: _sGrabbedTagValue, selected: true }, true); } if(_oOption){ that.close(); clearSearch.call(that); } else { this.value = ''; that.setMessage(that.config.messages.tagDuplicate); } } } }); } this.update = util.debounce(function() { // Optionally close dropdown on scroll / resize (#11) if (that.opened && that.config.closeOnScroll) { that.close(); } if (that.width) { that.container.style.width = that.width; } that.invert(); }, 50); if (this.requiresPagination) { this.paginateItems = util.debounce(function() { load.call(this); }, 50); this.tree.addEventListener("scroll", this.paginateItems.bind(this)); } // Dismiss when clicking outside the container document.addEventListener("click", this.events.dismiss); window.addEventListener("keydown", this.events.navigate); window.addEventListener("resize", this.update); window.addEventListener("scroll", this.update); // remove event listeners on destroy() this.on('selectr.destroy', function () { document.removeEventListener("click", this.events.dismiss); window.removeEventListener("keydown", this.events.navigate); window.removeEventListener("resize", this.update); window.removeEventListener("scroll", this.update); }); // Listen for form.reset() (@ambrooks, #13) if (this.el.form) { this.el.form.addEventListener("reset", this.events.reset); // remove listener on destroy() this.on('selectr.destroy', function () { this.el.form.removeEventListener("reset", this.events.reset); }); } }; /** * Check for selected options * @param {bool} reset */ Selectr.prototype.setSelected = function(reset) { // Select first option as with a native select-one element - #21, #24 if (!this.config.data && !this.el.multiple && this.el.options.length) { // Browser has selected the first option by default if (this.el.selectedIndex === 0) { if (!this.el.options[0].defaultSelected && !this.config.defaultSelected) { this.el.selectedIndex = -1; } } this.selectedIndex = this.el.selectedIndex; if (this.selectedIndex > -1) { this.select(this.selectedIndex); } } // If we're changing a select-one to select-multiple via the config // and there are no selected options, the first option will be selected by the browser // Let's prevent that here. if (this.config.multiple && this.originalType === "select-one" && !this.config.data) { if (this.el.options[0].selected && !this.el.options[0].defaultSelected) { this.el.options[0].selected = false; } } util.each(this.options, function(i, option) { if (option.selected && option.defaultSelected) { this.select(option.idx); } }, this); if (this.config.selectedValue) { this.setValue(this.config.selectedValue); } if (this.config.data) { if (!this.el.multiple && this.config.defaultSelected && this.el.selectedIndex < 0 && this.config.data.length > 0) { this.select(0); } var j = 0; util.each(this.config.data, function(i, opt) { // Check for group options if (isset(opt, "children")) { util.each(opt.children, function(x, item) { if (item.hasOwnProperty("selected") && item.selected === true) { this.select(j); } j++; }, this); } else { if (opt.hasOwnProperty("selected") && opt.selected === true) { this.select(j); } j++; } }, this); } }; /** * Destroy the instance * @return {void} */ Selectr.prototype.destroy = function() { if (!this.rendered) return; this.emit("selectr.destroy"); // Revert to select-single if programtically set to multiple if (this.originalType === 'select-one') { this.el.multiple = false; } if (this.config.data) { this.el.innerHTML = ""; } // Remove the className from select element util.removeClass(this.el, 'selectr-hidden'); // Replace the container with the original select element this.container.parentNode.replaceChild(this.el, this.container); this.rendered = false; // remove reference delete this.el.selectr; }; /** * Change an options state * @param {Number} index * @return {void} */ Selectr.prototype.change = function(index) { var item = this.items[index], option = this.options[index]; if (option.disabled) { return; } if (option.selected && util.hasClass(item, "selected")) { this.deselect(index); } else { this.select(index); } if (this.opened && !this.el.multiple) { this.close(); } }; /** * Select an option * @param {Number} index * @return {void} */ Selectr.prototype.select = function(index) { var item = this.items[index], options = [].slice.call(this.el.options), option = this.options[index]; if (this.el.multiple) { if (util.includes(this.selectedIndexes, index)) { return false; } if (this.config.maxSelections && this.tags.length === this.config.maxSelections) { this.setMessage(this.config.messages.maxSelections.replace("{max}", this.config.maxSelections), true); return false; } this.selectedValues.push(option.value); this.selectedIndexes.push(index); addTag.call(this, item); } else { var data = this.data ? this.data[index] : option; if (this.customSelected) { this.label.innerHTML = this.config.renderSelection(data); } else { // no xss this.label.textContent = option.textContent; } this.selectedValue = option.value; this.selectedIndex = index; util.each(this.options, function(i, o) { var opt = this.items[i]; if (i !== index) { if (opt) { util.removeClass(opt, "selected"); } o.selected = false; o.removeAttribute("selected"); } }, this); } if (!util.includes(options, option)) { this.el.add(option); } item.setAttribute("aria-selected", true); util.addClass(item, "selected"); util.addClass(this.container, "has-selected"); option.selected = true; option.setAttribute("selected", ""); this.emit("selectr.change", option); this.emit("selectr.select", option); // fire native change event if ("createEvent" in document) { var evt = document.createEvent("HTMLEvents"); evt.initEvent("change", true, true); evt.__selfTriggered = true; this.el.dispatchEvent(evt); } else { this.el.fireEvent("onchange"); } }; /** * Deselect an option * @param {Number} index * @return {void} */ Selectr.prototype.deselect = function(index, force) { var item = this.items[index], option = this.options[index]; if (this.el.multiple) { var selIndex = this.selectedIndexes.indexOf(index); this.selectedIndexes.splice(selIndex, 1); var valIndex = this.selectedValues.indexOf(option.value); this.selectedValues.splice(valIndex, 1); removeTag.call(this, item); if (!this.tags.length) { util.removeClass(this.container, "has-selected"); } } else { if (!force && !this.config.clearable && !this.config.allowDeselect) { return false; } this.label.innerHTML = ""; this.selectedValue = null; this.el.selectedIndex = this.selectedIndex = -1; util.removeClass(this.container, "has-selected"); } this.items[index].setAttribute("aria-selected", false); util.removeClass(this.items[index], "selected"); option.selected = false; option.removeAttribute("selected"); this.emit("selectr.change", null); this.emit("selectr.deselect", option); // fire native change event if ("createEvent" in document) { var evt = document.createEvent("HTMLEvents"); evt.initEvent("change", true, true); evt.__selfTriggered = true; this.el.dispatchEvent(evt); } else { this.el.fireEvent("onchange"); } }; /** * Programmatically set selected values * @param {String|Array} value - A string or an array of strings */ Selectr.prototype.setValue = function(value) { var isArray = Array.isArray(value); if (!isArray) { value = value.toString().trim(); } // Can't pass array to select-one if (!this.el.multiple && isArray) { return false; } util.each(this.options, function(i, option) { if (isArray && (value.indexOf(option.value) > -1) || option.value === value) { this.change(option.idx); } }, this); }; /** * Set the selected value(s) * @param {bool} toObject Return only the raw values or an object * @param {bool} toJson Return the object as a JSON string * @return {mixed} Array or String */ Selectr.prototype.getValue = function(toObject, toJson) { var value; if (this.el.multiple) { if (toObject) { if (this.selectedIndexes.length) { value = {}; value.values = []; util.each(this.selectedIndexes, function(i, index) { var option = this.options[index]; value.values[i] = { value: option.value, text: option.textContent }; }, this); } } else { value = this.selectedValues.slice(); } } else { if (toObject) { var option = this.options[this.selectedIndex]; value = { value: option.value, text: option.textContent }; } else { value = this.selectedValue; } } if (toObject && toJson) { value = JSON.stringify(value); } return value; }; /** * Add a new option or options * @param {object} data */ Selectr.prototype.add = function(data, checkDuplicate) { if (data) { this.data = this.data || []; this.items = this.items || []; this.options = this.options || []; if (Array.isArray(data)) { // We have an array on items util.each(data, function(i, obj) { this.add(obj, checkDuplicate); }, this); } // User passed a single object to the method // or Selectr passed an object from an array else if ("[object Object]" === Object.prototype.toString.call(data)) { if (checkDuplicate) { var dupe = false; util.each(this.options, function(i, option) { if (option.value.toLowerCase() === data.value.toLowerCase()) { dupe = true; } }); if (dupe) { return false; } } var option = util.createElement('option', data); this.data.push(data); // fix for native iOS dropdown otherwise the native dropdown will be empty if (this.mobileDevice) { this.el.add(option); } // Add the new option to the list this.options.push(option); // Add the index for later use option.idx = this.options.length > 0 ? this.options.length - 1 : 0; // Create a new item createItem.call(this, option); // Select the item if required if (data.selected) { this.select(option.idx); } // We may have had an empty select so update // the placeholder to reflect the changes. this.setPlaceholder(); return option; } // Recount the pages if (this.config.pagination) { this.paginate(); } return true; } }; /** * Remove an option or options * @param {Mixed} o Array, integer (index) or string (value) * @return {Void} */ Selectr.prototype.remove = function(o) { var options = []; if (Array.isArray(o)) { util.each(o, function(i, opt) { if (util.isInt(opt)) { options.push(this.getOptionByIndex(opt)); } else if (typeof opt === "string") { options.push(this.getOptionByValue(opt)); } }, this); } else if (util.isInt(o)) { options.push(this.getOptionByIndex(o)); } else if (typeof o === "string") { options.push(this.getOptionByValue(o)); } if (options.length) { var index; util.each(options, function(i, option) { index = option.idx; // Remove the HTMLOptionElement this.el.remove(option); // Remove the reference from the option array this.options.splice(index, 1); // If the item has a parentNode (group element) it needs to be removed // otherwise the render function will still append it to the dropdown var parentNode = this.items[index].parentNode; if (parentNode) { parentNode.removeChild(this.items[index]); } // Remove reference from the items array this.items.splice(index, 1); // Reset the indexes util.each(this.options, function(i, opt) { opt.idx = i; this.items[i].idx = i; }, this); }, this); // We may have had an empty select now so update // the placeholder to reflect the changes. this.setPlaceholder(); // Recount the pages if (this.config.pagination) { this.paginate(); } } }; /** * Remove all options */ Selectr.prototype.removeAll = function() { // Clear any selected options this.clear(true); // Remove the HTMLOptionElements util.each(this.el.options, function(i, option) { this.el.remove(option); }, this); // Empty the dropdown util.truncate(this.tree); // Reset variables this.items = []; this.options = []; this.data = []; this.navIndex = 0; if (this.requiresPagination) { this.requiresPagination = false; this.pageIndex = 1; this.pages = []; } // Update the placeholder this.setPlaceholder(); }; /** * Perform a search * @param {string}|{null} query The query string (taken from user input if null) * @param {boolean} anchor Anchor search to beginning of strings (defaults to false)? * @return {Array} Search results, as an array of {text, value} objects */ Selectr.prototype.search = function( string, anchor ) { if ( this.navigating ) { return; } // we're only going to alter the DOM for "live" searches var live = false; if ( ! string ) { string = this.input.value; live = true; // Remove message and clear dropdown this.removeMessage(); util.truncate(this.tree); } var results = []; var f = document.createDocumentFragment(); string = string.trim().toLowerCase(); if ( string.length > 0 ) { var compare = anchor ? util.startsWith : util.includes; util.each( this.options, function ( i, option ) { var item = this.items[option.idx]; var matches = compare( option.textContent.trim().toLowerCase(), string ); if ( matches && !option.disabled ) { results.push( { text: option.textContent, value: option.value } ); if ( live ) { appendItem( item, f, this.customOption ); util.removeClass( item, "excluded" ); // Underline the matching results if ( !this.customOption ) { match( string, option ); } } } else if ( live ) { util.addClass( item, "excluded" ); } }, this); if ( live ) { // Append results if ( !f.childElementCount ) { if ( !this.config.taggable ) { this.noResults = true; this.setMessage( this.config.messages.noResults ); } } else { // Highlight top result (@binary-koan #26) var prevEl = this.items[this.navIndex]; var firstEl = f.querySelector(".selectr-option:not(.excluded)"); this.noResults = false; util.removeClass( prevEl, "active" ); this.navIndex = firstEl.idx; util.addClass( firstEl, "active" ); } this.tree.appendChild( f ); } } else { render.call(this); } return results; }; /** * Toggle the dropdown * @return {void} */ Selectr.prototype.toggle = function() { if (!this.disabled) { if (this.opened) { this.close(); } else { this.open(); } } }; /** * Open the dropdown * @return {void} */ Selectr.prototype.open = function() { var that = this; if (!this.options.length) { return false; } if (!this.opened) { this.emit("selectr.open"); } this.opened = true; if (this.mobileDevice || this.config.nativeDropdown) { util.addClass(this.container, "native-open"); if (this.config.data) { // Dump the options into the select // otherwise the native dropdown will be empty util.each(this.options, function(i, option) { this.el.add(option); }, this); } return; } util.addClass(this.container, "open"); render.call(this); this.invert(); this.tree.scrollTop = 0; util.removeClass(this.container, "notice"); this.selected.setAttribute("aria-expanded", true); this.tree.setAttribute("aria-hidden", false); this.tree.setAttribute("aria-expanded", true); if (this.config.searchable && !this.config.taggable) { setTimeout(function() { that.input.focus(); // Allow tab focus that.input.tabIndex = 0; }, 10); } }; /** * Close the dropdown * @return {void} */ Selectr.prototype.close = function() { if (this.opened) { this.emit("selectr.close"); } this.opened = false; this.navigating = false; if (this.mobileDevice || this.config.nativeDropdown) { util.removeClass(this.container, "native-open"); return; } var notice = util.hasClass(this.container, "notice"); if (this.config.searchable && !notice) { this.input.blur(); // Disable tab focus this.input.tabIndex = -1; this.searching = false; } if (notice) { util.removeClass(this.container, "notice"); this.notice.textContent = ""; } util.removeClass(this.container, "open"); util.removeClass(this.container, "native-open"); this.selected.setAttribute("aria-expanded", false); this.tree.setAttribute("aria-hidden", true); this.tree.setAttribute("aria-expanded", false); util.truncate(this.tree); clearSearch.call(this); }; /** * Enable the element * @return {void} */ Selectr.prototype.enable = function() { this.disabled = false; this.el.disabled = false; this.selected.tabIndex = this.originalIndex; if (this.el.multiple) { util.each(this.tags, function(i, t) { t.lastElementChild.tabIndex = 0; }); } util.removeClass(this.container, "selectr-disabled"); }; /** * Disable the element * @param {boolean} container Disable the container only (allow value submit with form) * @return {void} */ Selectr.prototype.disable = function(container) { if (!container) { this.el.disabled = true; } this.selected.tabIndex = -1; if (this.el.multiple) { util.each(this.tags, function(i, t) { t.lastElementChild.tabIndex = -1; }); } this.disabled = true; util.addClass(this.container, "selectr-disabled"); }; /** * Reset to initial state * @return {void} */ Selectr.prototype.reset = function() { if (!this.disabled) { this.clear(); this.setSelected(true); util.each(this.defaultSelected, function(i, idx) { this.select(idx); }, this); this.emit("selectr.reset"); } }; /** * Clear all selections * @return {void} */ Selectr.prototype.clear = function(force, isClearLast) { if (this.el.multiple) { // Loop over the selectedIndexes so we don't have to loop over all the options // which can be costly if there are a lot of them if (this.selectedIndexes.length) { // Copy the array or we'll get an error var indexes = this.selectedIndexes.slice(); if (isClearLast) { this.deselect(indexes.slice(-1)[0]); } else { util.each(indexes, function(i, idx) { this.deselect(idx); }, this); } } } else { if (this.selectedIndex > -1) { this.deselect(this.selectedIndex, force); } } this.emit("selectr.clear"); }; /** * Return serialised data * @param {boolean} toJson * @return {mixed} Returns either an object or JSON string */ Selectr.prototype.serialise = function(toJson) { var data = []; util.each(this.options, function(i, option) { var obj = { value: option.value, text: option.textContent }; if (option.selected) { obj.selected = true; } if (option.disabled) { obj.disabled = true; } data[i] = obj; }); return toJson ? JSON.stringify(data) : data; }; /** * Localised version of serialise() method */ Selectr.prototype.serialize = function(toJson) { return this.serialise(toJson); }; /** * Sets the placeholder * @param {String} placeholder */ Selectr.prototype.setPlaceholder = function(placeholder) { // Set the placeholder placeholder = placeholder || this.config.placeholder || this.el.getAttribute("placeholder"); if (!this.options.length) { placeholder = this.config.messages.noOptions; } this.placeEl.innerHTML = placeholder; }; /** * Paginate the option list * @return {Array} */ Selectr.prototype.paginate = function() { if (this.items.length) { var that = this; this.pages = this.items.map(function(v, i) { return i % that.config.pagination === 0 ? that.items.slice(i, i + that.config.pagination) : null; }).filter(function(pages) { return pages; }); return this.pages; } }; /** * Display a message * @param {String} message The message */ Selectr.prototype.setMessage = function(message, close) { if (close) { this.close(); } util.addClass(this.container, "notice"); this.notice.textContent = message; }; /** * Dismiss the current message */ Selectr.prototype.removeMessage = function() { util.removeClass(this.container, "notice"); this.notice.innerHTML = ""; }; /** * Keep the dropdown within the window * @return {void} */ Selectr.prototype.invert = function() { var rt = util.rect(this.selected), oh = this.tree.parentNode.offsetHeight, wh = window.innerHeight, doInvert = rt.top + rt.height + oh > wh; if (doInvert) { util.addClass(this.container, "inverted"); this.isInverted = true; } else { util.removeClass(this.container, "inverted"); this.isInverted = false; } this.optsRect = util.rect(this.tree); }; /** * Get an option via it's index * @param {Integer} index The index of the HTMLOptionElement required * @return {HTMLOptionElement} */ Selectr.prototype.getOptionByIndex = function(index) { return this.options[index]; }; /** * Get an option via it's value * @param {String} value The value of the HTMLOptionElement required * @return {HTMLOptionElement} */ Selectr.prototype.getOptionByValue = function(value) { var option = false; for (var i = 0, l = this.options.length; i < l; i++) { if (this.options[i].value.trim() === value.toString().trim()) { option = this.options[i]; break; } } return option; }; module.exports = Selectr; ================================================ FILE: src/js/assets/selectr/selectr.scss ================================================ @use "../../../scss/jsoneditor/variables"; /*! * Selectr 2.4.13 * http://mobius.ovh/docs/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 variables.$jse-grey; border-radius: 3px; background-color: variables.$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: variables.$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 variables.$jse-grey variables.$jse-grey; border-radius: 0 0 3px 3px; background-color: variables.$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: variables.$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 variables.$jse-grey; border-radius: 3px; } .selectr-notice { display: none; box-sizing: border-box; width: 100%; padding: 8px 16px; border-top: 1px solid variables.$jse-grey; border-radius: 0 0 3px 3px; background-color: variables.$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: variables.$jse-white; background-color: #5897fb; } .selectr-option.disabled { opacity: 0.4; } .selectr-option.excluded { display: none; } .selectr-container.open .selectr-selected { border-color: variables.$jse-grey variables.$jse-grey transparent variables.$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 selectr-spin; -webkit-animation: 500ms linear 0s normal forwards infinite running selectr-spin; animation: 500ms linear 0s normal forwards infinite running selectr-spin; border-width: 3px; border-style: solid; border-color: #aaa #ddd #ddd; border-radius: 50%; } @-webkit-keyframes selectr-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 selectr-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 variables.$jse-grey variables.$jse-grey; border-radius: 0 0 3px 3px; } .selectr-container.inverted .selectr-options-container { border-width: 1px 1px 0; border-color: variables.$jse-grey variables.$jse-grey transparent; border-radius: 3px 3px 0 0; background-color: variables.$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; } ================================================ FILE: src/js/autocomplete.js ================================================ 'use strict' // Helper functions for handling both string and object option formats const getOptionText = (option) => { if (option == null) return '' return typeof option === 'string' ? option : (option.text || '') } const getOptionValue = (option) => { if (option == null) return '' return typeof option === 'string' ? option : (option.value || option.text || '') } const isObject = (value) => { return value !== null && typeof value === 'object' } const normalizeCase = (text = '', config) => { return config.caseSensitive ? text : text.toLowerCase() } const ensureStringOption = (option) => { // Keep objects as-is, but convert primitives to strings to prevent breaking changes return isObject(option) ? option : String(option) } const getHighlightedTextParts = (token, row, config) => { const rowText = getOptionText(row) const rowValue = getOptionValue(row) const tokenLower = normalizeCase(token, config) const rowTextLower = normalizeCase(rowText, config) const rowValueLower = normalizeCase(rowValue, config) // Find the best match position for highlighting let matchIndex = -1 const matchLength = token.length let displayText = rowText // Prefer text matches over value matches for display if (rowTextLower.indexOf(tokenLower) > -1) { matchIndex = rowTextLower.indexOf(tokenLower) displayText = rowText } else if (rowValueLower.indexOf(tokenLower) > -1) { matchIndex = rowValueLower.indexOf(tokenLower) displayText = rowText } if (matchIndex > -1) { return { beforeText: displayText.substring(0, matchIndex), matchText: displayText.substring(matchIndex, matchIndex + matchLength), afterText: displayText.substring(matchIndex + matchLength), displayText } } else { // No match found, return the display text as-is return { beforeText: '', matchText: '', afterText: displayText, displayText } } } // Helper function to reduce duplication in filter functions const matchesFilter = (token, match, config, matchFunction) => { const normalizedToken = normalizeCase(token, config) if (isObject(match)) { // Check both text and value properties for object matches const matchText = getOptionText(match) const matchValue = getOptionValue(match) const normalizedText = normalizeCase(matchText, config) const normalizedValue = normalizeCase(matchValue, config) return matchFunction(normalizedText, normalizedToken) || matchFunction(normalizedValue, normalizedToken) } else { // Handle simple string matches const normalizedMatch = normalizeCase(String(match), config) return matchFunction(normalizedMatch, normalizedToken) } } const defaultFilterFunction = { start: function (token, match, config) { return matchesFilter(token, match, config, (normalizedText, normalizedToken) => normalizedText.indexOf(normalizedToken) === 0 ) }, contain: function (token, match, config) { return matchesFilter(token, match, config, (normalizedText, normalizedToken) => normalizedText.indexOf(normalizedToken) > -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) } const p = { 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(token, match, 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 = '' const { beforeText, matchText, afterText } = getHighlightedTextParts(token, row, config) // Add text before match (if any) if (beforeText) { divRow.appendChild(document.createTextNode(beforeText)) } // Add highlighted match (if any) if (matchText) { const b = document.createElement('b') b.appendChild(document.createTextNode(matchText)) divRow.appendChild(b) } // Add text after match if (afterText) { divRow.appendChild(document.createTextNode(afterText)) } elem.appendChild(divRow) return divRow }) if (rows.length === 0) { return // nothing to show. } const firstRowValue = getOptionValue(rows[0].__hint) const hasOptionText = isObject(rows[0].__hint) && 'text' in rows[0].__hint if (rows.length === 1 && normalizeCase(token, config) === normalizeCase(firstRowValue, config) && !hasOptionText) { return // do not show the dropDown if it has only one element which matches what we have just displayed and has no option.text property. } 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, // 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(ensureStringOption) 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) // Use the same filter logic as the dropdown for consistency const filterFn = typeof config.filter === 'function' ? config.filter : defaultFilterFunction[config.filter] for (let i = 0; i < optionsLength; i++) { const opt = this.options[i] if (filterFn && filterFn(token, opt, config)) { const optText = getOptionText(opt) const optValue = getOptionValue(opt) // For hints, prioritize matches that start with the token for better UX let hintText = '' const normalizedToken = normalizeCase(token, config) const normalizedOptText = normalizeCase(optText, config) const normalizedOptValue = normalizeCase(optValue, config) if (normalizedOptText.indexOf(normalizedToken) === 0) { // Text starts with token - show completion hintText = leftSide + token + optText.substring(token.length) } else if (normalizedOptValue.indexOf(normalizedToken) === 0) { // Value starts with token - show completion hintText = leftSide + token + optValue.substring(token.length) } else { // Contains match but doesn't start with token - just show the token hintText = leftSide + token } this.elementHint.innerText = hintText this.elementHint.realInnerText = leftSide + optValue 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' } } } const dropDownController = createDropDownController(dropDown, rs) const 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 + getOptionText(m).substring(token.length) this.elementHint.realInnerText = leftSide + getOptionValue(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 + getOptionText(m).substring(token.length) this.elementHint.realInnerText = leftSide + getOptionValue(m) e.preventDefault() e.stopPropagation() } }.bind(rs) const onBlurHandler = e => { rs.hideDropDown() // console.log("Lost focus."); } dropDownController.onmouseselection = (option, rs) => { const optionValue = getOptionValue(option) rs.element.innerText = rs.elementHint.innerText = leftSide + optionValue rs.hideDropDown() window.setTimeout(() => { rs.element.focus() setEndOfContenteditable(rs.element) }, 1) } return rs } ================================================ FILE: src/js/constants.js ================================================ 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 ================================================ FILE: src/js/createAbsoluteAnchor.js ================================================ 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) } ================================================ FILE: src/js/header.js ================================================ /*! * jsoneditor.js * * @brief * JSONEditor is a web-based tool to view, edit, format, and validate JSON. * It has various modes such as a tree editor, a code editor, and a plain text * editor. * * Supported browsers: Chrome, Firefox, Safari, Edge * * @license * Licensed under the Apache License, Version 2.0 (the "License"); you may not * use this file except in compliance with the License. You may obtain a copy * of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the * License for the specific language governing permissions and limitations under * the License. * * Copyright (c) 2011-2026 Jos de Jong, http://jsoneditoronline.org * * @author Jos de Jong, * @version @@version * @date @@date */ ================================================ FILE: src/js/i18n.js ================================================ 'use strict' /* eslint-disable no-template-curly-in-string */ import './polyfills' const _defs = { en: { array: 'Array', auto: 'Auto', appendText: 'Append', appendTitle: 'Append a new field with type \'auto\' after this field (Ctrl+Shift+Ins)', appendSubmenuTitle: 'Select the type of the field to be appended', appendTitleAuto: 'Append a new field with type \'auto\' (Ctrl+Shift+Ins)', ascending: 'Ascending', ascendingTitle: 'Sort the childs of this ${type} in ascending order', actionsMenu: 'Click to open the actions menu (Ctrl+M)', cannotParseFieldError: 'Cannot parse field into JSON', cannotParseValueError: 'Cannot parse value into JSON', collapseAll: 'Collapse all fields', compactTitle: 'Compact JSON data, remove all whitespaces (Ctrl+Shift+I)', descending: 'Descending', descendingTitle: 'Sort the childs of this ${type} in descending order', drag: 'Drag to move this field (Alt+Shift+Arrows)', duplicateKey: 'duplicate key', duplicateText: 'Duplicate', duplicateTitle: 'Duplicate selected fields (Ctrl+D)', duplicateField: 'Duplicate this field (Ctrl+D)', duplicateFieldError: 'Duplicate field name', empty: 'empty', expandAll: 'Expand all fields', expandTitle: 'Click to expand/collapse this field (Ctrl+E). \n' + 'Ctrl+Click to expand/collapse including all childs.', formatTitle: 'Format JSON data, with proper indentation and line feeds (Ctrl+I)', insert: 'Insert', insertTitle: 'Insert a new field with type \'auto\' before this field (Ctrl+Ins)', insertSub: 'Select the type of the field to be inserted', object: 'Object', ok: 'Ok', redo: 'Redo (Ctrl+Shift+Z)', removeText: 'Remove', removeTitle: 'Remove selected fields (Ctrl+Del)', removeField: 'Remove this field (Ctrl+Del)', repairTitle: 'Repair JSON: fix quotes and escape characters, remove comments and JSONP notation, turn JavaScript objects into JSON.', searchTitle: 'Search fields and values', searchNextResultTitle: 'Next result (Enter)', searchPreviousResultTitle: 'Previous result (Shift + Enter)', selectNode: 'Select a node...', showAll: 'show all', showMore: 'show more', showMoreStatus: 'displaying ${visibleChilds} of ${totalChilds} items.', sort: 'Sort', sortTitle: 'Sort the childs of this ${type}', sortTitleShort: 'Sort contents', sortFieldLabel: 'Field:', sortDirectionLabel: 'Direction:', sortFieldTitle: 'Select the nested field by which to sort the array or object', sortAscending: 'Ascending', sortAscendingTitle: 'Sort the selected field in ascending order', sortDescending: 'Descending', sortDescendingTitle: 'Sort the selected field in descending order', string: 'String', transform: 'Transform', transformTitle: 'Filter, sort, or transform the childs of this ${type}', transformTitleShort: 'Filter, sort, or transform contents', extract: 'Extract', extractTitle: 'Extract this ${type}', transformQueryTitle: 'Enter a JMESPath query', transformWizardLabel: 'Wizard', transformWizardFilter: 'Filter', transformWizardSortBy: 'Sort by', transformWizardSelectFields: 'Select fields', transformQueryLabel: 'Query', transformPreviewLabel: 'Preview', type: 'Type', typeTitle: 'Change the type of this field', openUrl: 'Ctrl+Click or Ctrl+Enter to open url in new window', undo: 'Undo last action (Ctrl+Z)', validationCannotMove: 'Cannot move a field into a child of itself', autoType: 'Field type "auto". ' + 'The field type is automatically determined from the value ' + 'and can be a string, number, boolean, or null.', objectType: 'Field type "object". ' + 'An object contains an unordered set of key/value pairs.', arrayType: 'Field type "array". ' + 'An array contains an ordered collection of values.', stringType: 'Field type "string". ' + 'Field type is not determined from the value, ' + 'but always returned as string.', modeEditorTitle: 'Switch Editor Mode', modeCodeText: 'Code', modeCodeTitle: 'Switch to code highlighter', modeFormText: 'Form', modeFormTitle: 'Switch to form editor', modeTextText: 'Text', modeTextTitle: 'Switch to plain text editor', modeTreeText: 'Tree', modeTreeTitle: 'Switch to tree editor', modeViewText: 'View', modeViewTitle: 'Switch to tree view', modePreviewText: 'Preview', modePreviewTitle: 'Switch to preview mode', examples: 'Examples', default: 'Default', containsInvalidProperties: 'Contains invalid properties', containsInvalidItems: 'Contains invalid items' }, es: { array: 'Matriz', auto: 'Auto', appendText: 'Agregar', appendTitle: 'Agregue un nuevo campo con el tipo \'auto\' después de este campo (Ctrl + Shift + Ins)', appendSubmenuTitle: 'Seleccione el tipo de campo que se agregará', appendTitleAuto: 'Agregue un nuevo campo con el tipo \'auto\' (Ctrl + Shift + Ins)', ascending: 'Ascendente', ascendingTitle: 'Ordene los elementos secundarios de este ${type} en orden ascendente', actionsMenu: 'Haga clic para abrir el menú de acciones (Ctrl + M)', cannotParseFieldError: 'No se puede parsear el campo en JSON', cannotParseValueError: 'No se puede parsear el valor en JSON', collapseAll: 'Contraer todos los campos', compactTitle: 'Compactar datos JSON, eliminar todos los espacios en blanco (Ctrl + Shift + I)', descending: 'Descendente', descendingTitle: 'Ordene los hijos de este ${type} en orden descendente', drag: 'Arrastre para mover este campo (Alt + Mayús + Flechas)', duplicateKey: 'llave duplicada', duplicateText: 'Duplicar', duplicateTitle: 'Duplicar campos seleccionados (Ctrl + D)', duplicateField: 'Duplicar este campo (Ctrl + D)', duplicateFieldError: 'Nombre de campo duplicado', empty: 'vacio', expandAll: 'Expandir todos los campos', expandTitle: 'Haga clic para expandir/contraer este campo (Ctrl + E). \n ' + ' Ctrl+Clic para expandir/contraer incluyendo todos los niños.', formatTitle: 'Formatee los datos JSON, con la sangría y los avances de línea adecuados (Ctrl + I)', insert: 'Insertar', insertTitle: 'Inserte un nuevo campo con el tipo \'auto\' antes de este campo (Ctrl + Ins)', insertSub: 'Seleccione el tipo de campo a insertar', object: 'Objeto', ok: 'Ok', redo: 'Rehacer (Ctrl+Mayús+Z)', removeText: 'Eliminar', removeTitle: 'Eliminar campos seleccionados (Ctrl+Supr)', removeField: 'Eliminar este campo (Ctrl+Supr)', repairTitle: 'Reparar JSON: corrija comillas y caracteres de escape, elimine comentarios y notación JSONP, convierta objetos JavaScript en JSON.', searchTitle: 'Campos de búsqueda y valores', searchNextResultTitle: 'Siguiente resultado (Entrar)', searchPreviousResultTitle: 'Resultado anterior (Shift + Enter)', selectNode: 'Seleccione un nodo...', showAll: 'mostrar todo', showMore: 'mostrar más', showMoreStatus: 'mostrando ${visibleChilds} de ${totalChilds} elementos.', sort: 'Ordenar', sortTitle: 'Ordene los hijos de este ${type}', sortTitleShort: 'Ordenar contenidos', sortFieldLabel: 'Campo:', sortDirectionLabel: 'Dirección:', sortFieldTitle: 'Seleccione el campo anidado por el cual ordenar la matriz u objeto', sortAscending: 'Ascendente', sortAscendingTitle: 'Ordenar el campo seleccionado en orden ascendente', sortDescending: 'Descendente', sortDescendingTitle: 'Ordenar por el campo seleccionado, en orden descendente', string: 'Texto', transform: 'Transformar', transformTitle: 'Filtrar, ordenar o transformar los hijos de este ${type}', transformTitleShort: 'Filtrar, ordenar o transformar contenidos', extract: 'Extraer', extractTitle: 'Extrae este ${type}', transformQueryTitle: 'Ingrese una consulta JMESPath', transformWizardLabel: 'Wizard', transformWizardFilter: 'Filtro', transformWizardSortBy: 'Ordenar por', transformWizardSelectFields: 'Seleccione un campo', transformQueryLabel: 'Consulta', transformPreviewLabel: 'Vista Previa', type: 'Tipo', typeTitle: 'Cambiar el tipo de campo', openUrl: 'Ctrl+Click o Ctrl+Enter para abrir la URL en una nueva ventana', undo: 'Deshacer la última acción (Ctrl+Z)', validationCannotMove: 'No se puede mover un campo a un hijo de sí mismo.', autoType: 'Tipo de campo "auto". ' + 'El tipo de campo se determina automáticamente a partir del valor ' + 'y puede ser una cadena, un número, un booleano o un valor nulo.', objectType: 'Tipo de campo "objeto". ' + ' Un objeto contiene un conjunto desordenado de pares clave/valor.', arrayType: 'Tipo de campo "matriz". ' + ' Una matriz contiene una colección ordenada de valores.', stringType: 'Tipo de campo "cadena". ' + ' El tipo de campo no se determina a partir del valor, ' + ' pero siempre se devuelve como una cadena.', modeEditorTitle: 'Cambiar modo de editor', modeCodeText: 'Código', modeCodeTitle: 'Cambiar al resaltador de código', modeFormText: 'Formulario', modeFormTitle: 'Cambiar al editor de formularios', modeTextText: 'Texto', modeTextTitle: 'Cambiar al editor de texto sin formato', modeTreeText: 'Árbol', modeTreeTitle: 'Cambiar al editor de árbol', modeViewText: 'Vista', modeViewTitle: 'Cambiar a la vista de árbol', modePreviewText: 'Vista Previa', modePreviewTitle: 'Cambiar al modo de vista previa', examples: 'Ejemplos', default: 'Predeterminado', containsInvalidProperties: 'Contiene propiedades no válidas', containsInvalidItems: 'Contiene ítems no válidos' }, 'zh-CN': { array: '数组', auto: '自动', appendText: '追加', appendTitle: '在此字段后追加一个类型为“auto”的新字段 (Ctrl+Shift+Ins)', appendSubmenuTitle: '选择要追加的字段类型', appendTitleAuto: '追加类型为“auto”的新字段 (Ctrl+Shift+Ins)', ascending: '升序', ascendingTitle: '升序排列${type}的子节点', actionsMenu: '点击打开动作菜单(Ctrl+M)', cannotParseFieldError: '无法将字段解析为JSON', cannotParseValueError: '无法将值解析为JSON', collapseAll: '缩进所有字段', compactTitle: '压缩JSON数据,删除所有空格 (Ctrl+Shift+I)', descending: '降序', descendingTitle: '降序排列${type}的子节点', drag: '拖拽移动该节点(Alt+Shift+Arrows)', duplicateKey: '重复键', duplicateText: '复制', duplicateTitle: '复制选中字段(Ctrl+D)', duplicateField: '复制该字段(Ctrl+D)', duplicateFieldError: '重复的字段名称', empty: '清空', expandAll: '展开所有字段', expandTitle: '点击 展开/收缩 该字段(Ctrl+E). \n' + 'Ctrl+Click 展开/收缩 包含所有子节点.', formatTitle: '使用适当的缩进和换行符格式化JSON数据 (Ctrl+I)', insert: '插入', insertTitle: '在此字段前插入类型为“auto”的新字段 (Ctrl+Ins)', insertSub: '选择要插入的字段类型', object: '对象', ok: 'Ok', redo: '重做 (Ctrl+Shift+Z)', removeText: '移除', removeTitle: '移除选中字段 (Ctrl+Del)', removeField: '移除该字段 (Ctrl+Del)', repairTitle: '修复JSON:修复引号和转义符,删除注释和JSONP表示法,将JavaScript对象转换为JSON。', selectNode: '选择一个节点...', showAll: '展示全部', showMore: '展示更多', showMoreStatus: '显示${totalChilds}的${visibleChilds}项目.', sort: '排序', sortTitle: '排序${type}的子节点', sortTitleShort: '内容排序', sortFieldLabel: '字段:', sortDirectionLabel: '方向:', sortFieldTitle: '选择用于对数组或对象排序的嵌套字段', sortAscending: '升序排序', sortAscendingTitle: '按照该字段升序排序', sortDescending: '降序排序', sortDescendingTitle: '按照该字段降序排序', string: '字符串', transform: '变换', transformTitle: '筛选,排序,或者转换${type}的子节点', transformTitleShort: '筛选,排序,或者转换内容', extract: '提取', extractTitle: '提取这个 ${type}', transformQueryTitle: '输入JMESPath查询', transformWizardLabel: '向导', transformWizardFilter: '筛选', transformWizardSortBy: '排序', transformWizardSelectFields: '选择字段', transformQueryLabel: '查询', transformPreviewLabel: '预览', type: '类型', typeTitle: '更改字段类型', openUrl: 'Ctrl+Click 或者 Ctrl+Enter 在新窗口打开链接', undo: '撤销上次动作 (Ctrl+Z)', validationCannotMove: '无法将字段移入其子节点', autoType: '字段类型 "auto". ' + '字段类型由值自动确定 ' + '可以为 string,number,boolean,或者 null.', objectType: '字段类型 "object". ' + '对象包含一组无序的键/值对.', arrayType: '字段类型 "array". ' + '数组包含值的有序集合.', stringType: '字段类型 "string". ' + '字段类型由值自动确定,' + '但始终作为字符串返回.', modeCodeText: '代码', modeCodeTitle: '切换至代码高亮', modeFormText: '表单', modeFormTitle: '切换至表单编辑', modeTextText: '文本', modeTextTitle: '切换至文本编辑', modeTreeText: '树', modeTreeTitle: '切换至树编辑', modeViewText: '视图', modeViewTitle: '切换至树视图', modePreviewText: '预览', modePreviewTitle: '切换至预览模式', examples: '例子', default: '缺省', containsInvalidProperties: '包含无效的属性', containsInvalidItems: '包含无效项目' }, 'pt-BR': { array: 'Lista', auto: 'Automatico', appendText: 'Adicionar', appendTitle: 'Adicionar novo campo com tipo \'auto\' depois deste campo (Ctrl+Shift+Ins)', appendSubmenuTitle: 'Selecione o tipo do campo a ser adicionado', appendTitleAuto: 'Adicionar novo campo com tipo \'auto\' (Ctrl+Shift+Ins)', ascending: 'Ascendente', ascendingTitle: 'Organizar filhor do tipo ${type} em crescente', actionsMenu: 'Clique para abrir o menu de ações (Ctrl+M)', cannotParseFieldError: 'Não é possível analisar o campo no JSON', cannotParseValueError: 'Não é possível analisar o valor em JSON', collapseAll: 'Fechar todos campos', compactTitle: 'Dados JSON compactos, remova todos os espaços em branco (Ctrl+Shift+I)', descending: 'Descendente', descendingTitle: 'Organizar o filhos do tipo ${type} em decrescente', duplicateKey: 'chave duplicada', drag: 'Arraste para mover este campo (Alt+Shift+Arrows)', duplicateText: 'Duplicar', duplicateTitle: 'Duplicar campos selecionados (Ctrl+D)', duplicateField: 'Duplicar este campo (Ctrl+D)', duplicateFieldError: 'Nome do campo duplicado', empty: 'vazio', expandAll: 'Expandir todos campos', expandTitle: 'Clique para expandir/encolher este campo (Ctrl+E). \n' + 'Ctrl+Click para expandir/encolher incluindo todos os filhos.', formatTitle: 'Formate dados JSON, com recuo e feeds de linha adequados (Ctrl+I)', insert: 'Inserir', insertTitle: 'Inserir um novo campo do tipo \'auto\' antes deste campo (Ctrl+Ins)', insertSub: 'Selecionar o tipo de campo a ser inserido', object: 'Objeto', ok: 'Ok', redo: 'Refazer (Ctrl+Shift+Z)', removeText: 'Remover', removeTitle: 'Remover campos selecionados (Ctrl+Del)', removeField: 'Remover este campo (Ctrl+Del)', repairTitle: 'Repare JSON: corrija aspas e caracteres de escape, remova comentários e notação JSONP, transforme objetos JavaScript em JSON.', selectNode: 'Selecione um nódulo...', showAll: 'mostrar todos', showMore: 'mostrar mais', showMoreStatus: 'exibindo ${visibleChilds} de ${totalChilds} itens.', sort: 'Organizar', sortTitle: 'Organizar os filhos deste ${type}', sortTitleShort: 'Organizar os filhos', sortFieldLabel: 'Campo:', sortDirectionLabel: 'Direção:', sortFieldTitle: 'Selecione um campo filho pelo qual ordenar o array ou objeto', sortAscending: 'Ascendente', sortAscendingTitle: 'Ordenar o campo selecionado por ordem ascendente', sortDescending: 'Descendente', sortDescendingTitle: 'Ordenar o campo selecionado por ordem descendente', string: 'Texto', transform: 'Transformar', transformTitle: 'Filtrar, ordenar ou transformar os filhos deste ${type}', transformTitleShort: 'Filtrar, ordenar ou transformar conteúdos', transformQueryTitle: 'Insira uma expressão JMESPath', transformWizardLabel: 'Assistente', transformWizardFilter: 'Filtro', transformWizardSortBy: 'Ordenar por', transformWizardSelectFields: 'Selecionar campos', transformQueryLabel: 'Expressão', transformPreviewLabel: 'Visualizar', type: 'Tipo', typeTitle: 'Mudar o tipo deste campo', openUrl: 'Ctrl+Click ou Ctrl+Enter para abrir link em nova janela', undo: 'Desfazer último ação (Ctrl+Z)', validationCannotMove: 'Não pode mover um campo como filho dele mesmo', autoType: 'Campo do tipo "auto". ' + 'O tipo do campo é determinao automaticamente a partir do seu valor ' + 'e pode ser texto, número, verdade/falso ou nulo.', objectType: 'Campo do tipo "objeto". ' + 'Um objeto contém uma lista de pares com chave e valor.', arrayType: 'Campo do tipo "lista". ' + 'Uma lista contem uma coleção de valores ordenados.', stringType: 'Campo do tipo "string". ' + 'Campo do tipo nao é determinado através do seu valor, ' + 'mas sempre retornara um texto.', examples: 'Exemplos', default: 'Revelia', containsInvalidProperties: 'Contém propriedades inválidas', containsInvalidItems: 'Contém itens inválidos' }, tr: { array: 'Dizin', auto: 'Otomatik', appendText: 'Ekle', appendTitle: 'Bu alanın altına \'otomatik\' tipinde yeni bir alan ekle (Ctrl+Shift+Ins)', appendSubmenuTitle: 'Eklenecek alanın tipini seç', appendTitleAuto: '\'Otomatik\' tipinde yeni bir alan ekle (Ctrl+Shift+Ins)', ascending: 'Artan', ascendingTitle: '${type}\'ın alt tiplerini artan düzende sırala', actionsMenu: 'Aksiyon menüsünü açmak için tıklayın (Ctrl+M)', collapseAll: 'Tüm alanları kapat', descending: 'Azalan', descendingTitle: '${type}\'ın alt tiplerini azalan düzende sırala', drag: 'Bu alanı taşımak için sürükleyin (Alt+Shift+Arrows)', duplicateKey: 'Var olan anahtar', duplicateText: 'Aşağıya kopyala', duplicateTitle: 'Seçili alanlardan bir daha oluştur (Ctrl+D)', duplicateField: 'Bu alandan bir daha oluştur (Ctrl+D)', duplicateFieldError: 'Duplicate field name', cannotParseFieldError: 'Alan JSON\'a ayrıştırılamıyor', cannotParseValueError: 'JSON\'a değer ayrıştırılamıyor', empty: 'boş', expandAll: 'Tüm alanları aç', expandTitle: 'Bu alanı açmak/kapatmak için tıkla (Ctrl+E). \n' + 'Alt alanlarda dahil tüm alanları açmak için Ctrl+Click ', insert: 'Ekle', insertTitle: 'Bu alanın üstüne \'otomatik\' tipinde yeni bir alan ekle (Ctrl+Ins)', insertSub: 'Araya eklenecek alanın tipini seç', object: 'Nesne', ok: 'Tamam', redo: 'Yeniden yap (Ctrl+Shift+Z)', removeText: 'Kaldır', removeTitle: 'Seçilen alanları kaldır (Ctrl+Del)', removeField: 'Bu alanı kaldır (Ctrl+Del)', selectNode: 'Bir nesne seç...', showAll: 'tümünü göster', showMore: 'daha fazla göster', showMoreStatus: '${totalChilds} alanın ${visibleChilds} alt alanları gösteriliyor', sort: 'Sırala', sortTitle: '${type}\'ın alt alanlarını sırala', sortTitleShort: 'İçerikleri sırala', sortFieldLabel: 'Alan:', sortDirectionLabel: 'Yön:', sortFieldTitle: 'Diziyi veya nesneyi sıralamak için iç içe geçmiş alanı seçin', sortAscending: 'Artan', sortAscendingTitle: 'Seçili alanı artan düzende sırala', sortDescending: 'Azalan', sortDescendingTitle: 'Seçili alanı azalan düzende sırala', string: 'Karakter Dizisi', transform: 'Dönüştür', transformTitle: '${type}\'ın alt alanlarını filtrele, sırala veya dönüştür', transformTitleShort: 'İçerikleri filterele, sırala veya dönüştür', transformQueryTitle: 'JMESPath sorgusu gir', transformWizardLabel: 'Sihirbaz', transformWizardFilter: 'Filtre', transformWizardSortBy: 'Sırala', transformWizardSelectFields: 'Alanları seç', transformQueryLabel: 'Sorgu', transformPreviewLabel: 'Önizleme', type: 'Tip', typeTitle: 'Bu alanın tipini değiştir', openUrl: 'URL\'i yeni bir pencerede açmak için Ctrl+Click veya Ctrl+Enter', undo: 'Son değişikliği geri al (Ctrl+Z)', validationCannotMove: 'Alt alan olarak taşınamıyor', autoType: 'Alan tipi "otomatik". ' + 'Alan türü otomatik olarak değerden belirlenir' + 've bir dize, sayı, boolean veya null olabilir.', objectType: 'Alan tipi "nesne". ' + 'Bir nesne, sıralanmamış bir anahtar / değer çifti kümesi içerir.', arrayType: 'Alan tipi "dizi". ' + 'Bir dizi, düzenli değerler koleksiyonu içerir.', stringType: 'Alan tipi "karakter dizisi". ' + 'Alan türü değerden belirlenmez,' + 'ancak her zaman karakter dizisi olarak döndürülür.', modeCodeText: 'Kod', modeCodeTitle: 'Kod vurgulayıcıya geç', modeFormText: 'Form', modeFormTitle: 'Form düzenleyiciye geç', modeTextText: 'Metin', modeTextTitle: 'Düz metin düzenleyiciye geç', modeTreeText: 'Ağaç', modeTreeTitle: 'Ağaç düzenleyiciye geç', modeViewText: 'Görünüm', modeViewTitle: 'Ağaç görünümüne geç', examples: 'Örnekler', default: 'Varsayılan', containsInvalidProperties: 'Geçersiz özellikler içeriyor', containsInvalidItems: 'Geçersiz öğeler içeriyor' }, ja: { array: '配列', auto: 'オート', appendText: '追加', appendTitle: '次のフィールドに"オート"のフィールドを追加 (Ctrl+Shift+Ins)', appendSubmenuTitle: '追加するフィールドの型を選択してください', appendTitleAuto: '"オート"のフィールドを追加 (Ctrl+Shift+Ins)', ascending: '昇順', ascendingTitle: '${type}の子要素を昇順に並べ替え', actionsMenu: 'クリックしてアクションメニューを開く (Ctrl+M)', collapseAll: 'すべてを折りたたむ', descending: '降順', descendingTitle: '${type}の子要素を降順に並べ替え', drag: 'ドラッグして選択中のフィールドを移動 (Alt+Shift+Arrows)', duplicateKey: '複製キー', duplicateText: '複製', duplicateTitle: '選択中のフィールドを複製 (Ctrl+D)', duplicateField: '選択中のフィールドを複製 (Ctrl+D)', duplicateFieldError: 'フィールド名が重複しています', cannotParseFieldError: 'JSONのフィールドを解析できません', cannotParseValueError: 'JSONの値を解析できません', empty: '空', expandAll: 'すべてを展開', expandTitle: 'クリックしてフィールドを展開/折りたたむ (Ctrl+E). \n' + 'Ctrl+Click ですべての子要素を展開/折りたたむ', insert: '挿入', insertTitle: '選択中のフィールドの前に新しいフィールドを挿入 (Ctrl+Ins)', insertSub: '挿入するフィールドの型を選択', object: 'オブジェクト', ok: '実行', redo: 'やり直す (Ctrl+Shift+Z)', removeText: '削除', removeTitle: '選択中のフィールドを削除 (Ctrl+Del)', removeField: '選択中のフィールドを削除 (Ctrl+Del)', selectNode: 'ノードを選択...', showAll: 'すべてを表示', showMore: 'もっと見る', showMoreStatus: '${totalChilds}個のアイテムのうち ${visibleChilds}個を表示しています。', sort: '並べ替え', sortTitle: '${type}の子要素を並べ替え', sortTitleShort: '並べ替え', sortFieldLabel: 'フィールド:', sortDirectionLabel: '順序:', sortFieldTitle: '配列またはオブジェクトを並び替えるためのフィールドを選択', sortAscending: '昇順', sortAscendingTitle: '選択中のフィールドを昇順に並び替え', sortDescending: '降順', sortDescendingTitle: '選択中のフィールドを降順に並び替え', string: '文字列', transform: '変換', transformTitle: '${type}の子要素をフィルター・並び替え・変換する', transformTitleShort: '内容をフィルター・並び替え・変換する', extract: '抽出', extractTitle: '${type}を抽出', transformQueryTitle: 'JMESPathクエリを入力', transformWizardLabel: 'ウィザード', transformWizardFilter: 'フィルター', transformWizardSortBy: '並び替え', transformWizardSelectFields: 'フィールドを選択', transformQueryLabel: 'クエリ', transformPreviewLabel: 'プレビュー', type: '型', typeTitle: '選択中のフィールドの型を変更', openUrl: 'Ctrl+Click または Ctrl+Enter で 新規ウィンドウでURLを開く', undo: '元に戻す (Ctrl+Z)', validationCannotMove: '子要素に移動できません ', autoType: 'オート: ' + 'フィールドの型は値から自動的に決定されます。 ' + '(文字列・数値・ブール・null)', objectType: 'オブジェクト: ' + 'オブジェクトは順序が決まっていないキーと値のペア組み合わせです。', arrayType: '配列: ' + '配列は順序が決まっている値の集合体です。', stringType: '文字列: ' + 'フィールド型は値から決定されませんが、' + '常に文字列として返されます。', modeCodeText: 'コードモード', modeCodeTitle: 'ハイライトモードに切り替え', modeFormText: 'フォームモード', modeFormTitle: 'フォームモードに切り替え', modeTextText: 'テキストモード', modeTextTitle: 'テキストモードに切り替え', modeTreeText: 'ツリーモード', modeTreeTitle: 'ツリーモードに切り替え', modeViewText: 'ビューモード', modeViewTitle: 'ビューモードに切り替え', modePreviewText: 'プレビュー', modePreviewTitle: 'プレビューに切り替え', examples: '例', default: 'デフォルト', containsInvalidProperties: '無効なプロパティが含まれています', containsInvalidItems: '無効なアイテムが含まれています' }, 'fr-FR': { array: 'Liste', auto: 'Auto', appendText: 'Ajouter', appendTitle: 'Ajouter un champ de type \'auto\' après ce champ (Ctrl+Shift+Ins)', appendSubmenuTitle: 'Sélectionner le type du champ à ajouter', appendTitleAuto: 'Ajouter un champ de type \'auto\' (Ctrl+Shift+Ins)', ascending: 'Ascendant', ascendingTitle: 'Trier les enfants de ce ${type} par ordre ascendant', actionsMenu: 'Ouvrir le menu des actions (Ctrl+M)', collapseAll: 'Regrouper', descending: 'Descendant', descendingTitle: 'Trier les enfants de ce ${type} par ordre descendant', drag: 'Déplacer (Alt+Shift+Arrows)', duplicateKey: 'Dupliquer la clé', duplicateText: 'Dupliquer', duplicateTitle: 'Dupliquer les champs sélectionnés (Ctrl+D)', duplicateField: 'Dupliquer ce champ (Ctrl+D)', duplicateFieldError: 'Dupliquer le nom de champ', cannotParseFieldError: 'Champ impossible à parser en JSON', cannotParseValueError: 'Valeur impossible à parser en JSON', empty: 'vide', expandAll: 'Étendre', expandTitle: 'Étendre/regrouper ce champ (Ctrl+E). \n' + 'Ctrl+Click pour étendre/regrouper avec tous les champs.', insert: 'Insérer', insertTitle: 'Insérer un champ de type \'auto\' avant ce champ (Ctrl+Ins)', insertSub: 'Sélectionner le type de champ à insérer', object: 'Objet', ok: 'Ok', redo: 'Rejouer (Ctrl+Shift+Z)', removeText: 'Supprimer', removeTitle: 'Supprimer les champs sélectionnés (Ctrl+Del)', removeField: 'Supprimer ce champ (Ctrl+Del)', searchTitle: 'Rechercher champs et valeurs', searchNextResultTitle: 'Résultat suivant (Enter)', searchPreviousResultTitle: 'Résultat précédent (Shift + Enter)', selectNode: 'Sélectionner un nœud...', showAll: 'voir tout', showMore: 'voir plus', showMoreStatus: '${visibleChilds} éléments affichés de ${totalChilds}.', sort: 'Trier', sortTitle: 'Trier les champs de ce ${type}', sortTitleShort: 'Trier', sortFieldLabel: 'Champ:', sortDirectionLabel: 'Direction:', sortFieldTitle: 'Sélectionner les champs permettant de trier les listes et objet', sortAscending: 'Ascendant', sortAscendingTitle: 'Trier les champs sélectionnés par ordre ascendant', sortDescending: 'Descendant', sortDescendingTitle: 'Trier les champs sélectionnés par ordre descendant', string: 'Chaîne', transform: 'Transformer', transformTitle: 'Filtrer, trier, or transformer les enfants de ce ${type}', transformTitleShort: 'Filtrer, trier ou transformer le contenu', extract: 'Extraire', extractTitle: 'Extraire ce ${type}', transformQueryTitle: 'Saisir une requête JMESPath', transformWizardLabel: 'Assistant', transformWizardFilter: 'Filtrer', transformWizardSortBy: 'Trier par', transformWizardSelectFields: 'Sélectionner les champs', transformQueryLabel: 'Requête', transformPreviewLabel: 'Prévisualisation', type: 'Type', typeTitle: 'Changer le type de ce champ', openUrl: 'Ctrl+Click ou Ctrl+Enter pour ouvrir l\'url dans une autre fenêtre', undo: 'Annuler la dernière action (Ctrl+Z)', validationCannotMove: 'Cannot move a field into a child of itself', autoType: 'Champe de type "auto". ' + 'Ce type de champ est automatiquement déterminé en fonction de la valeur ' + 'et peut être de type "chaîne", "nombre", "booléen" ou null.', objectType: 'Champ de type "objet". ' + 'Un objet contient un ensemble non ordonné de paires clé/valeur.', arrayType: 'Champ de type "liste". ' + 'Une liste contient une collection ordonnée de valeurs.', stringType: 'Champ de type "chaîne". ' + 'Ce type de champ n\'est pas déterminé en fonction de la valeur, ' + 'mais retourne systématiquement une chaîne de caractères.', modeEditorTitle: 'Changer mode d\'édition', modeCodeText: 'Code', modeCodeTitle: 'Activer surlignage code', modeFormText: 'Formulaire', modeFormTitle: 'Activer formulaire', modeTextText: 'Texte', modeTextTitle: 'Activer éditeur texte', modeTreeText: 'Arbre', modeTreeTitle: 'Activer éditeur arbre', modeViewText: 'Lecture seule', modeViewTitle: 'Activer vue arbre', modePreviewText: 'Prévisualisation', modePreviewTitle: 'Activer mode prévisualiser', examples: 'Exemples', default: 'Défaut', containsInvalidProperties: 'Contient des propriétés non valides', containsInvalidItems: 'Contient des éléments invalides' }, de: { array: 'Auflistung', auto: 'Auto', appendText: 'anhängen', appendTitle: 'Fügen Sie nach diesem Feld ein neues Feld mit dem Typ \'auto\' ein (Strg+Umschalt+Ein)', appendSubmenuTitle: 'Wählen Sie den Typ des neuen Feldes', appendTitleAuto: 'Ein neues Feld vom Typ \'auto\' hinzufügen (Strg+Umschalt+Ein)', ascending: 'Aufsteigend', ascendingTitle: 'Sortieren Sie die Elemente dieses ${type} in aufsteigender Reihenfolge', actionsMenu: 'Klicken Sie zum Öffnen des Aktionsmenüs (Strg+M)', cannotParseFieldError: 'Feld kann nicht in JSON geparst werden', cannotParseValueError: 'Wert kann nicht in JSON geparst werden', collapseAll: 'Alle Felder zuklappen', compactTitle: 'JSON-Daten verdichten, alle Leerzeichen entfernen (Strg+Umschalt+\\)', descending: 'Absteigend', descendingTitle: 'Sortieren Sie die Elemente dieses ${type} in absteigender Reihenfolge', drag: 'Ziehen, um dieses Feld zu verschieben (Alt+Umschalt+Pfeile)', duplicateKey: 'Doppelter Schlüssel', duplicateText: 'Duplikat', duplicateTitle: 'Ausgewählte Felder duplizieren (Strg+D)', duplicateField: 'Dieses Feld duplizieren (Strg+D)', duplicateFieldError: 'Doppelter Feldname', empty: 'leer', expandAll: 'Alle Felder anzeigen', expandTitle: 'Klicken Sie, um dieses Feld zu erweitern/zu kollabieren (Strg+E). \nStrg+Klicken Sie, um dieses Feld einschließlich aller Elemente zu erweitern/zu kollabieren.', formatTitle: 'JSON-Daten mit korrekter Einrückung und Zeilenvorschüben formatieren (Strg+\\)', insert: 'einfügen', insertTitle: 'Fügen Sie vor diesem Feld ein neues Feld mit dem Typ \'auto\' ein (Strg+Einfg)', insertSub: 'Wählen Sie den Typ des neuen Feldes', object: 'Objekt', ok: 'Ok', redo: 'Wiederholen (Strg+Umschalt+Z)', removeText: 'entfernen', removeTitle: 'Ausgewählte Felder entfernen (Strg+Entf)', removeField: 'Dieses Feld entfernen (Strg+Entf)', repairTitle: 'JSON reparieren: Anführungszeichen und Escape-Zeichen korrigieren, Kommentare und JSONP-Notation entfernen, JavaScript-Objekte in JSON umwandeln.', searchTitle: 'Suchfelder und Werte', searchNextResultTitle: 'Nächstes Ergebnis (Enter)', searchPreviousResultTitle: 'Vorheriges Ergebnis (Umschalt + Eingabe)', selectNode: 'Wählen Sie einen Knoten aus...', showAll: 'alle anzeigen', showMore: 'mehr anzeigen', showMoreStatus: 'Anzeige von ${visibleChilds} von ${totalChilds}-Elementen.', sort: 'Sortieren', sortTitle: 'Sortieren Sie die Elemente dieses ${type}', sortTitleShort: 'Inhalt sortieren', sortFieldLabel: 'Feld:', sortDirectionLabel: 'Richtung:', sortFieldTitle: 'Wählen Sie das verschachtelte Feld, nach dem das Array oder Objekt sortiert werden soll.', sortAscending: 'Aufsteigend', sortAscendingTitle: 'Sortieren Sie das ausgewählte Feld in aufsteigender Reihenfolge', sortDescending: 'Absteigend', sortDescendingTitle: 'Sortieren Sie das ausgewählte Feld in absteigender Reihenfolge', string: 'Zeichenfolge', transform: 'Verwandeln', transformTitle: 'Die Elemente dieses ${type} filtern, sortieren oder transformieren', transformTitleShort: 'Inhalte filtern, sortieren oder transformieren', extract: 'Auszug', extractTitle: 'Extrahieren Sie diesen ${type}', transformQueryTitle: 'Eine JMESPath-Abfrage eingeben', transformWizardLabel: 'Zauberer', transformWizardFilter: 'Filter', transformWizardSortBy: 'Sortieren nach', transformWizardSelectFields: 'Felder auswählen', transformQueryLabel: 'Anfrage', transformPreviewLabel: 'Vorschau', type: 'Geben Sie ein.', typeTitle: 'Ändern Sie den Typ dieses Feldes', openUrl: 'Strg+Klicken oder Strg+Eingabe, um die URL in einem neuen Fenster zu öffnen', undo: 'Letzte Aktion rückgängig machen (Strg+Z)', validationCannotMove: 'Kann ein Feld nicht in ein Kind seiner selbst verschieben', autoType: 'Feldtyp "auto". Der Feldtyp wird automatisch aus dem Wert bestimmt und kann ein String, eine Zahl, boolesch oder null sein.', objectType: 'Feldtyp "Objekt". Ein Objekt enthält eine ungeordnete Menge von Schlüssel/Wert-Paaren.', arrayType: 'Feldtyp "Array". Ein Array enthält eine geordnete Sammlung von Werten.', stringType: 'Feldtyp "Zeichenfolge". Der Feldtyp wird nicht aus dem Wert bestimmt, sondern immer als Zeichenfolge zurückgegeben.', modeEditorTitle: 'Editor-Modus umschalten', modeCodeText: 'Code', modeCodeTitle: 'Umschalten auf Code-Highlighter', modeFormText: 'Formular', modeFormTitle: 'Zum Formular-Editor wechseln', modeTextText: 'Text', modeTextTitle: 'Zum Editor für einfachen Text wechseln', modeTreeText: 'Baum', modeTreeTitle: 'Zum Baum-Editor wechseln', modeViewText: 'Siehe', modeViewTitle: 'Zur Baumansicht wechseln', modePreviewText: 'Vorschau', modePreviewTitle: 'In den Vorschau-Modus wechseln', examples: 'Beispiele', default: 'Standardmäßig', containsInvalidProperties: 'Enthält ungültige Eigenschaften', containsInvalidItems: 'Enthält ungültige Elemente' }, ru: { array: 'Массив', auto: 'Авто', appendText: 'Добавить', appendTitle: 'Добавить новое поле с типом \'авто\' после этого поля (Ctrl+Shift+Ins)', appendSubmenuTitle: 'Выбрать тип поля для добавления', appendTitleAuto: 'Добавить новое поле с типом \'авто\' (Ctrl+Shift+Ins)', ascending: 'По возрастанию', ascendingTitle: 'Сортировать ${type} по возрастанию', actionsMenu: 'Нажмите для открытия меню действий (Ctrl+M)', cannotParseFieldError: 'Невозможно преобразовать поле в JSON', cannotParseValueError: 'Невозможно преобразовать значение в JSON', collapseAll: 'Свернуть все', compactTitle: 'Минификация JSON (Ctrl+Shift+I)', descending: 'По убыванию', descendingTitle: 'Сортировать ${type} по убыванию', drag: 'Потяните для перемещения этого поля (Alt+Shift+Arrows)', duplicateKey: 'повторяющийся ключ', duplicateText: 'Дублировать', duplicateTitle: 'Дублирование полей (Ctrl+D)', duplicateField: 'Дублировать поле (Ctrl+D)', duplicateFieldError: 'Дублирование названия поля', empty: 'пустой', expandAll: 'Развернуть все', expandTitle: 'Нажмите для раскрытия/скрытия поля (Ctrl+E)\n' + 'или Ctrl+Click для раскрытия/скрытия всех потомков.', formatTitle: 'Форматирование JSON (Ctrl+I)', insert: 'Вставить', insertTitle: 'Вставить новое поле с типом \'авто\' перед этим полем (Ctrl+Ins)', insertSub: 'Выбрать тип поля для вставки', object: 'Объект', ok: 'ОК', redo: 'Повторить (Ctrl+Shift+Z)', removeText: 'Удалить', removeTitle: 'Удалить выбранные поля (Ctrl+Del)', removeField: 'Удалить поле (Ctrl+Del)', repairTitle: 'Восстановите JSON: исправьте кавычки и escape-символы, удалите комментарии и нотацию JSONP, модифицируйте объекты JavaScript в JSON.', searchTitle: 'Поиск', searchNextResultTitle: 'Следующий результат (Enter)', searchPreviousResultTitle: 'Предыдущий результат (Shift + Enter)', selectNode: 'Выбор узла...', showAll: 'показать все', showMore: 'больше', showMoreStatus: '${visibleChilds} из ${totalChilds}', sort: 'Сортировка', sortTitle: 'Сортировка потомков типа ${type}', sortTitleShort: 'Сортировка содержимого', sortFieldLabel: 'Поле:', sortDirectionLabel: 'Направление:', sortFieldTitle: 'Выберите поле для сортировки массива или объекта', sortAscending: 'По возрастанию', sortAscendingTitle: 'Сортировка выбранного поря по возрастанию', sortDescending: 'По убыванию', sortDescendingTitle: 'Сортировка выбранного поря по убыванию', string: 'Строка', transform: 'Модификация', transformTitle: 'Фильтрация, сортировка или модификация данных типа ${type}', transformTitleShort: 'Фильтрация, сортировка или модификация данных', extract: 'Извлечение', extractTitle: 'Извлечь тип ${type}', transformQueryTitle: 'Введите JMESpath запрос', transformWizardLabel: 'Мастер', transformWizardFilter: 'Фильтр', transformWizardSortBy: 'Сортировка', transformWizardSelectFields: 'Поля', transformQueryLabel: 'Запрос', transformPreviewLabel: 'Просмотр', type: 'Тип', typeTitle: 'Изменить тип этого поля', openUrl: 'Ctrl+Click или Ctrl+Enter для открытия url в новом окне', undo: 'Отменить (Ctrl+Z)', validationCannotMove: 'Поле не может быть перемещено в потомка', autoType: 'Тип поля автоматически определяется по значению ' + 'и может быть строкой, числом, логическим значением или null.', objectType: 'Объект содержит неупорядоченный набор пар ключ/значение.', arrayType: 'Массив содержит упорядоченный набор значений.', stringType: 'Тип поля не определяется из значения, ' + 'но всегда возвращается как строка.', modeEditorTitle: 'Переключение режима редактора', modeCodeText: 'Код', modeCodeTitle: 'Переключить в режим редактора кода', modeFormText: 'Форма', modeFormTitle: 'Переключить в режим формы', modeTextText: 'Текст', modeTextTitle: 'Переключить в режим редактора текста', modeTreeText: 'Дерево', modeTreeTitle: 'Переключить в режим редактора дерева', modeViewText: 'Просмотр дерева', modeViewTitle: 'Переключить в режим просмотра дерева', modePreviewText: 'Просмотр', modePreviewTitle: 'Переключить в режим просмотра', examples: 'Примеры', default: 'По умолчанию', containsInvalidProperties: 'Содержит недопустимые свойства', containsInvalidItems: 'Содержит недопустимые элементы' }, ko: { array: '배열', auto: '자동', appendText: '추가', appendTitle: '선택한 요소 아래에 "자동" 요소를 추가합니다. (Ctrl + Shift + Ins)', appendSubmenuTitle: '추가할 요소의 유형을 선택해주세요.', appendTitleAuto: '"자동" 요소를 추가합니다. (Ctrl + Shift + Ins)', ascending: '오름차순', ascendingTitle: '선택한 ${type}의 하위 요소를 오름차순 정렬합니다.', actionsMenu: '메뉴 열기 (Ctrl + M)', cannotParseFieldError: 'JSON의 요소를 해석할 수 없습니다.', cannotParseValueError: 'JSON의 값을 해석할 수 없습니다.', collapseAll: '모두 접기', compactTitle: '모든 공백을 제거하여 JSON 데이터를 작게 만듭니다. (Ctrl + Shift + I)', descending: '내림차순', descendingTitle: '선택한 ${type}의 하위 요소를 내림차순으로 정렬', drag: '드래그하여 요소를 이동합니다. (Alt + Shift + Arrows)', duplicateKey: '복제키', duplicateText: '복제', duplicateTitle: '선택한 요소를 복제합니다. (Ctrl + D)', duplicateField: '선택한 요소를 복제합니다. (Ctrl + D)', duplicateFieldError: '요소 이름이 중복되었습니다.', empty: '비어있음', expandAll: '모두 열기', expandTitle: '클릭하여 요소를 열거나 닫습니다. (Ctrl + E) \nCtrl + Click으로 모든 하위 요소를 열거나 닫습니다.', formatTitle: '적절한 들여쓰기 및 줄바꿈으로 JSON 데이터를 정형화합니다. (Ctrl + I)', insert: '삽입', insertTitle: '선택한 요소 위에 새요소를 삽입합니다. (Ctrl + Ins)', insertSub: '삽입할 요소의 유형을 선택해주세요.', object: '객체', ok: '확인', redo: '다시 실행 (Ctrl + Shift + Z)', removeText: '삭제', removeTitle: '선택한 요소를 삭제합니다. (Ctrl + Del)', removeField: '선택한 요소를 삭제합니다. (Ctrl + Del)', repairTitle: 'JSON 교정: JSON 내의 주석과 JSONP 표기법을 지우고 따옴표와 이스케이프 문자를 수정합니다.', searchTitle: '요소 또는 값 찾기', searchNextResultTitle: '다음으로 찾기 (Enter)', searchPreviousResultTitle: '이전으로 찾기 (Shift + Enter)', selectNode: '요소를 선택해주세요...', showAll: '모두보기', showMore: '더보기', showMoreStatus: '${totalChilds} 개의 항목 중 ${visibleChilds} 개를 표시합니다.', sort: '정렬', sortTitle: '선택한 ${type}의 하위 요소를 정렬합니다.', sortTitleShort: '정렬', sortFieldLabel: '요소:', sortDirectionLabel: '순서:', sortFieldTitle: '배열이나 객체를 정렬하는 요소를 선택해주세요.', sortAscending: '오름차순', sortAscendingTitle: '선택한 요소를 오름차순으로 정렬합니다.', sortDescending: '내림차순', sortDescendingTitle: '선택한 요소를 내림차순으로 정렬합니다.', string: '문자', transform: '변환', transformTitle: '선택한 ${type}의 하위 요소를 필터하거나 정렬 또는 변환합니다.', transformTitleShort: '내용을 필터하거나 정렬 또는 변환합니다.', extract: '추출', extractTitle: '선택한 ${type}의 값을 최상위에 위치시킵니다.', transformQueryTitle: 'JMESPath 쿼리를 입력해주세요.', transformWizardLabel: '마법사', transformWizardFilter: '필터', transformWizardSortBy: '정렬', transformWizardSelectFields: '요소를 선택해주세요.', transformQueryLabel: '쿼리', transformPreviewLabel: '미리보기', type: '유형', typeTitle: '선택한 요소의 유형을 변경합니다.', openUrl: 'Ctrl + Click 또는 Ctrl + Enter로 새 창에서 URL 열기', undo: '실행 취소 (Ctrl + Z)', validationCannotMove: '하위 요소로 이동할 수 없습니다.', autoType: '자동: 요소의 형식이 값의 유형으로 결정됩니다. 문자, 숫자, 부울, 또는 null만 허용됩니다.', objectType: '객체: 순서대로 나열되지 않은 이름/값 쌍으로 이루어진 집합입니다.', arrayType: '배열: 순서대로 나열된 값의 집합입니다.', stringType: '문자: 요소의 유형이 값에서 결정되지 않지만 항상 문자로 반환됩니다.', modeEditorTitle: '편집기 유형 변경', modeCodeText: '코드', modeCodeTitle: '형식 교정을 도와주는 기능이 포함된 문자 편집기', modeFormText: '입력 양식', modeFormTitle: '정해진 요소에 값을 입력하는 편집기', modeTextText: '문자', modeTextTitle: '단순 문자 편집기', modeTreeText: '트리', modeTreeTitle: '트리 구조로 표시되는 편집기', modeViewText: '보기', modeViewTitle: '읽기전용 트리 구조로 JSON을 표시', modePreviewText: '미리보기', modePreviewTitle: '읽기전용 문자로 JSON을 표시', examples: '예제', default: '기본값', containsInvalidProperties: '잘못된 속성이 포함되어 있습니다.', containsInvalidItems: '잘못된 항목이 포함되어 있습니다' } } const _locales = Object.keys(_defs) const _defaultLang = 'en' const userLang = typeof navigator !== 'undefined' ? navigator.language || navigator.userLanguage : undefined let _lang = _locales.find(l => l === userLang) || _defaultLang export function setLanguage (lang) { if (!lang) { return } const langFound = _locales.find(l => l === lang) if (langFound) { _lang = langFound } else { console.error('Language not found') } } export function setLanguages (languages) { if (!languages) { return } for (const language in languages) { const langFound = _locales.find(l => l === language) if (!langFound) { _locales.push(language) } _defs[language] = Object.assign({}, _defs[_defaultLang], _defs[language], languages[language]) } } export function translate (key, data, lang) { if (!lang) { lang = _lang } let text = _defs[lang][key] || _defs[_defaultLang][key] || key if (data) { for (const dataKey in data) { text = text.replace('${' + dataKey + '}', data[dataKey]) } } return text } ================================================ FILE: src/js/jmespathQuery.js ================================================ import jmespath from 'jmespath' import { get, parsePath, parseString } from './util' /** * Build a JMESPath query based on query options coming from the wizard * @param {JSON} json The JSON document for which to build the query. * Used for context information like determining * the type of values (string or number) * @param {QueryOptions} queryOptions * @return {string} Returns a query (as string) */ export function createQuery (json, queryOptions) { const { sort, filter, projection } = queryOptions let query = '' if (filter) { const examplePath = filter.field !== '@' ? ['0'].concat(parsePath('.' + filter.field)) : ['0'] const exampleValue = get(json, examplePath) const value1 = typeof exampleValue === 'string' ? filter.value : parseString(filter.value) query += '[? ' + filter.field + ' ' + filter.relation + ' ' + '`' + JSON.stringify(value1) + '`' + ']' } else { query += Array.isArray(json) ? '[*]' : '@' } if (sort) { if (sort.direction === 'desc') { query += ' | reverse(sort_by(@, &' + sort.field + '))' } else { query += ' | sort_by(@, &' + sort.field + ')' } } if (projection) { if (query[query.length - 1] !== ']') { query += ' | [*]' } if (projection.fields.length === 1) { query += '.' + projection.fields[0] } else if (projection.fields.length > 1) { query += '.{' + projection.fields.map(value => { const parts = value.split('.') const last = parts[parts.length - 1] return last + ': ' + value }).join(', ') + '}' } else { // values.length === 0 // ignore } } return query } /** * Execute a JMESPath query * @param {JSON} json * @param {string} query * @return {JSON} Returns the transformed JSON */ export function executeQuery (json, query) { return jmespath.search(json, query) } ================================================ FILE: src/js/jsonUtils.js ================================================ 'use strict' /** * Convert part of a JSON object to a JSON string. * Use case is to stringify a small part of a large JSON object so you can see * a preview. * * @param {*} value * The value to convert to a JSON string. * * @param {number | string | null} [space] * A String or Number object that's used to insert white space into the output * JSON string for readability purposes. If this is a Number, it indicates the * number of space characters to use as white space; this number is capped at 10 * if it's larger than that. Values less than 1 indicate that no space should be * used. If this is a String, the string (or the first 10 characters of the string, * if it's longer than that) is used as white space. If this parameter is not * provided (or is null), no white space is used. * * @param {number} [limit] Maximum size of the string output. * * @returns {string | undefined} Returns the string representation of the JSON object. */ export function stringifyPartial (value, space, limit) { let _space // undefined by default if (typeof space === 'number') { if (space > 10) { _space = repeat(' ', 10) } else if (space >= 1) { _space = repeat(' ', space) } // else ignore } else if (typeof space === 'string' && space !== '') { _space = space } const output = stringifyValue(value, _space, '', limit) return output.length > limit ? (slice(output, limit) + '...') : output } /** * Stringify a value * @param {*} value * @param {string} space * @param {string} indent * @param {number} limit * @return {string | undefined} */ function stringifyValue (value, space, indent, limit) { // boolean, null, number, string, or date if (typeof value === 'boolean' || value instanceof Boolean || value === null || typeof value === 'number' || value instanceof Number || typeof value === 'string' || value instanceof String || value instanceof Date) { return JSON.stringify(value) } // array if (Array.isArray(value)) { return stringifyArray(value, space, indent, limit) } // object (test lastly!) if (value && typeof value === 'object') { return stringifyObject(value, space, indent, limit) } return undefined } /** * Stringify an array * @param {Array} array * @param {string} space * @param {string} indent * @param {number} limit * @return {string} */ function stringifyArray (array, space, indent, limit) { const childIndent = space ? (indent + space) : undefined let str = space ? '[\n' : '[' for (let i = 0; i < array.length; i++) { const item = array[i] if (space) { str += childIndent } if (typeof item !== 'undefined' && typeof item !== 'function') { str += stringifyValue(item, space, childIndent, limit) } else { str += 'null' } if (i < array.length - 1) { str += space ? ',\n' : ',' } // stop as soon as we're exceeding the limit if (str.length > limit) { return str + '...' } } str += space ? ('\n' + indent + ']') : ']' return str } /** * Stringify an object * @param {Object} object * @param {string} space * @param {string} indent * @param {number} limit * @return {string} */ function stringifyObject (object, space, indent, limit) { const childIndent = space ? (indent + space) : undefined let first = true let str = space ? '{\n' : '{' if (typeof object.toJSON === 'function') { return stringifyValue(object.toJSON(), space, indent, limit) } for (const key in object) { if (hasOwnProperty(object, key)) { const value = object[key] if (first) { first = false } else { str += space ? ',\n' : ',' } str += space ? (childIndent + '"' + key + '": ') : ('"' + key + '":') str += stringifyValue(value, space, childIndent, limit) // stop as soon as we're exceeding the limit if (str.length > limit) { return str + '...' } } } str += space ? ('\n' + indent + '}') : '}' return str } /** * Repeat a string a number of times. * Simple linear solution, we only need up to 10 iterations in practice * @param {string} text * @param {number} times * @return {string} */ function repeat (text, times) { let res = '' while (times-- > 0) { res += text } return res } /** * Limit the length of text * @param {string} text * @param {number} [limit] * @return {string} */ function slice (text, limit) { return typeof limit === 'number' ? text.slice(0, limit) : text } /** * Test whether some text contains a JSON array, i.e. the first * non-white space character is a [ * @param {string} jsonText * @return {boolean} */ export function containsArray (jsonText) { return /^\s*\[/.test(jsonText) } function hasOwnProperty (object, key) { return Object.prototype.hasOwnProperty.call(object, key) } ================================================ FILE: src/js/polyfills.js ================================================ if (typeof Element !== 'undefined') { // Polyfill for array remove (() => { function polyfill (item) { if (typeof item !== 'undefined') { if ('remove' in item) { return } Object.defineProperty(item, 'remove', { configurable: true, enumerable: true, writable: true, value: function remove () { if (this.parentNode !== undefined) { this.parentNode.removeChild(this) } } }) } } if (typeof window.Element !== 'undefined') { polyfill(window.Element.prototype) } if (typeof window.CharacterData !== 'undefined') { polyfill(window.CharacterData.prototype) } if (typeof window.DocumentType !== 'undefined') { polyfill(window.DocumentType.prototype) } })() } // simple polyfill for Array.findIndex if (!Array.prototype.findIndex) { // eslint-disable-next-line no-extend-native Object.defineProperty(Array.prototype, 'findIndex', { value: function (predicate) { for (let i = 0; i < this.length; i++) { const element = this[i] if (predicate.call(this, element, i, this)) { return i } } return -1 }, configurable: true, writable: true }) } // Polyfill for Array.find if (!Array.prototype.find) { // eslint-disable-next-line no-extend-native Object.defineProperty(Array.prototype, 'find', { value: function (predicate) { const i = this.findIndex(predicate) return this[i] }, configurable: true, writable: true }) } // Polyfill for String.trim if (!String.prototype.trim) { // eslint-disable-next-line no-extend-native String.prototype.trim = function () { return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '') } } ================================================ FILE: src/js/previewmode.js ================================================ 'use strict' import { jsonrepair } from 'jsonrepair' import { DEFAULT_MODAL_ANCHOR, MAX_PREVIEW_CHARACTERS, PREVIEW_HISTORY_LIMIT, SIZE_LARGE } from './constants' import { ErrorTable } from './ErrorTable' import { FocusTracker } from './FocusTracker' import { History } from './History' import { setLanguage, setLanguages, translate } from './i18n' import { createQuery, executeQuery } from './jmespathQuery' import { ModeSwitcher } from './ModeSwitcher' import { showSortModal } from './showSortModal' import { showTransformModal } from './showTransformModal' import { textModeMixins } from './textmode' import { addClassName, debounce, escapeUnicodeChars, formatSize, isObject, limitCharacters, parse, removeClassName, sort, sortObjectKeys } from './util' const textmode = textModeMixins[0].mixin // create a mixin with the functions for text mode const previewmode = {} /** * Create a JSON document preview, suitable for processing of large documents * @param {Element} container * @param {Object} [options] Object with options. See docs for details. * @private */ previewmode.create = function (container, options = {}) { if (typeof options.statusBar === 'undefined') { options.statusBar = true } // setting default for previewmode options.mainMenuBar = options.mainMenuBar !== false options.enableSort = options.enableSort !== false options.enableTransform = options.enableTransform !== false options.createQuery = options.createQuery || createQuery options.executeQuery = options.executeQuery || executeQuery this.options = options // indentation if (typeof options.indentation === 'number') { this.indentation = Number(options.indentation) } else { this.indentation = 2 // number of spaces } // language setLanguages(this.options.languages) setLanguage(this.options.language) // determine mode this.mode = 'preview' const me = this this.container = container this.dom = {} this.json = undefined this.text = '' // TODO: JSON Schema support // create a debounced validate function this._debouncedValidate = debounce(this.validate.bind(this), this.DEBOUNCE_INTERVAL) this.width = container.clientWidth this.height = container.clientHeight this.frame = document.createElement('div') this.frame.className = 'jsoneditor jsoneditor-mode-preview' this.frame.onclick = event => { // prevent default submit action when the editor is located inside a form event.preventDefault() } // setting the FocusTracker on 'this.frame' to track the editor's focus event const focusTrackerConfig = { target: this.frame, onFocus: this.options.onFocus || null, onBlur: this.options.onBlur || null } this.frameFocusTracker = new FocusTracker(focusTrackerConfig) this.content = document.createElement('div') this.content.className = 'jsoneditor-outer' this.dom.busy = document.createElement('div') this.dom.busy.className = 'jsoneditor-busy' this.dom.busyContent = document.createElement('span') this.dom.busyContent.textContent = 'busy...' this.dom.busy.appendChild(this.dom.busyContent) this.content.appendChild(this.dom.busy) this.dom.previewContent = document.createElement('pre') this.dom.previewContent.className = 'jsoneditor-preview' this.dom.previewText = document.createTextNode('') this.dom.previewContent.appendChild(this.dom.previewText) this.content.appendChild(this.dom.previewContent) if (this.options.mainMenuBar) { addClassName(this.content, 'has-main-menu-bar') // create menu this.menu = document.createElement('div') this.menu.className = 'jsoneditor-menu' this.frame.appendChild(this.menu) // create format button const buttonFormat = document.createElement('button') buttonFormat.type = 'button' buttonFormat.className = 'jsoneditor-format' buttonFormat.title = translate('formatTitle') this.menu.appendChild(buttonFormat) buttonFormat.onclick = function handleFormat () { me.executeWithBusyMessage(() => { try { me.format() } catch (err) { me._onError(err) } }, 'formatting...') } // create compact button const buttonCompact = document.createElement('button') buttonCompact.type = 'button' buttonCompact.className = 'jsoneditor-compact' buttonCompact.title = translate('compactTitle') this.menu.appendChild(buttonCompact) buttonCompact.onclick = function handleCompact () { me.executeWithBusyMessage(() => { try { me.compact() } catch (err) { me._onError(err) } }, 'compacting...') } // create sort button if (this.options.enableSort) { const sort = document.createElement('button') sort.type = 'button' sort.className = 'jsoneditor-sort' sort.title = translate('sortTitleShort') sort.onclick = () => { me._showSortModal() } this.menu.appendChild(sort) } // create transform button if (this.options.enableTransform) { const transform = document.createElement('button') transform.type = 'button' transform.title = translate('transformTitleShort') transform.className = 'jsoneditor-transform' transform.onclick = () => { me._showTransformModal() } this.dom.transform = transform this.menu.appendChild(transform) } // create repair button const buttonRepair = document.createElement('button') buttonRepair.type = 'button' buttonRepair.className = 'jsoneditor-repair' buttonRepair.title = translate('repairTitle') this.menu.appendChild(buttonRepair) buttonRepair.onclick = () => { if (me.json === undefined) { // only repair if we don't have valid JSON me.executeWithBusyMessage(() => { try { me.repair() } catch (err) { me._onError(err) } }, 'repairing...') } } // create history and undo/redo buttons if (this.options.history !== false) { // default option value is true const onHistoryChange = () => { me.dom.undo.disabled = !me.history.canUndo() me.dom.redo.disabled = !me.history.canRedo() } const calculateItemSize = item => // times two to account for the json object item.text.length * 2 this.history = new History(onHistoryChange, calculateItemSize, PREVIEW_HISTORY_LIMIT) // create undo button const undo = document.createElement('button') undo.type = 'button' undo.className = 'jsoneditor-undo jsoneditor-separator' undo.title = translate('undo') undo.onclick = () => { const action = me.history.undo() if (action) { me._applyHistory(action) } } this.menu.appendChild(undo) this.dom.undo = undo // create redo button const redo = document.createElement('button') redo.type = 'button' redo.className = 'jsoneditor-redo' redo.title = translate('redo') redo.onclick = () => { const action = me.history.redo() if (action) { me._applyHistory(action) } } this.menu.appendChild(redo) this.dom.redo = redo // force enabling/disabling the undo/redo button this.history.onChange() } // create mode box if (this.options && this.options.modes && this.options.modes.length) { this.modeSwitcher = new ModeSwitcher(this.menu, this.options.modes, this.options.mode, function onSwitch (mode) { // switch mode and restore focus try { me.setMode(mode) me.modeSwitcher.focus() } catch (err) { me._onError(err) } }) } } const errorTableVisible = Array.isArray(this.options.showErrorTable) ? this.options.showErrorTable.includes(this.mode) : this.options.showErrorTable === true this.errorTable = new ErrorTable({ errorTableVisible, onToggleVisibility: function () { me.validate() }, onFocusLine: null, onChangeHeight: function (height) { // TODO: change CSS to using flex box, remove setting height using JavaScript const statusBarHeight = me.dom.statusBar ? me.dom.statusBar.clientHeight : 0 const totalHeight = height + statusBarHeight + 1 me.content.style.marginBottom = (-totalHeight) + 'px' me.content.style.paddingBottom = totalHeight + 'px' } }) this.frame.appendChild(this.content) this.frame.appendChild(this.errorTable.getErrorTable()) this.container.appendChild(this.frame) if (options.statusBar) { addClassName(this.content, 'has-status-bar') const statusBar = document.createElement('div') this.dom.statusBar = statusBar statusBar.className = 'jsoneditor-statusbar' this.frame.appendChild(statusBar) this.dom.fileSizeInfo = document.createElement('span') this.dom.fileSizeInfo.className = 'jsoneditor-size-info' this.dom.fileSizeInfo.innerText = '' statusBar.appendChild(this.dom.fileSizeInfo) this.dom.arrayInfo = document.createElement('span') this.dom.arrayInfo.className = 'jsoneditor-size-info' this.dom.arrayInfo.innerText = '' statusBar.appendChild(this.dom.arrayInfo) statusBar.appendChild(this.errorTable.getErrorCounter()) statusBar.appendChild(this.errorTable.getWarningIcon()) statusBar.appendChild(this.errorTable.getErrorIcon()) } this._renderPreview() this.setSchema(this.options.schema, this.options.schemaRefs) } previewmode._renderPreview = function () { const text = this.getText() this.dom.previewText.nodeValue = limitCharacters(text, MAX_PREVIEW_CHARACTERS) if (this.dom.fileSizeInfo) { this.dom.fileSizeInfo.innerText = 'Size: ' + formatSize(text.length) } if (this.dom.arrayInfo) { if (Array.isArray(this.json)) { this.dom.arrayInfo.innerText = ('Array: ' + this.json.length + ' items') } else { this.dom.arrayInfo.innerText = '' } } } /** * Handle a change: * - Validate JSON schema * - Send a callback to the onChange listener if provided * @private */ previewmode._onChange = function () { // validate JSON schema (if configured) this._debouncedValidate() // trigger the onChange callback if (this.options.onChange) { try { this.options.onChange() } catch (err) { console.error('Error in onChange callback: ', err) } } // trigger the onChangeJSON callback if (this.options.onChangeJSON) { try { this.options.onChangeJSON(this.get()) } catch (err) { console.error('Error in onChangeJSON callback: ', err) } } // trigger the onChangeText callback if (this.options.onChangeText) { try { this.options.onChangeText(this.getText()) } catch (err) { console.error('Error in onChangeText callback: ', err) } } } /** * Open a sort modal * @private */ previewmode._showSortModal = function () { const me = this function onSort (json, sortedBy) { if (Array.isArray(json)) { const sortedArray = sort(json, sortedBy.path, sortedBy.direction) me.sortedBy = sortedBy me._setAndFireOnChange(sortedArray) } if (isObject(json)) { const sortedObject = sortObjectKeys(json, sortedBy.direction) me.sortedBy = sortedBy me._setAndFireOnChange(sortedObject) } } this.executeWithBusyMessage(() => { const container = me.options.modalAnchor || DEFAULT_MODAL_ANCHOR const json = me.get() me._renderPreview() // update array count showSortModal(container, json, sortedBy => { me.executeWithBusyMessage(() => { onSort(json, sortedBy) }, 'sorting...') }, me.sortedBy) }, 'parsing...') } /** * Open a transform modal * @private */ previewmode._showTransformModal = function () { this.executeWithBusyMessage(() => { const { createQuery, executeQuery, modalAnchor, queryDescription } = this.options const json = this.get() this._renderPreview() // update array count showTransformModal({ container: modalAnchor || DEFAULT_MODAL_ANCHOR, json, queryDescription, // can be undefined createQuery, executeQuery, onTransform: query => { this.executeWithBusyMessage(() => { const updatedJson = executeQuery(json, query) this._setAndFireOnChange(updatedJson) }, 'transforming...') } }) }, 'parsing...') } /** * Destroy the editor. Clean up DOM, event listeners, and web workers. */ previewmode.destroy = function () { if (this.frame && this.container && this.frame.parentNode === this.container) { this.container.removeChild(this.frame) } if (this.modeSwitcher) { this.modeSwitcher.destroy() this.modeSwitcher = null } this._debouncedValidate = null if (this.history) { this.history.clear() this.history = null } // Removing the FocusTracker set to track the editor's focus event this.frameFocusTracker.destroy() } /** * Compact the code in the text editor */ previewmode.compact = function () { const json = this.get() const text = JSON.stringify(json) // we know that in this case the json is still the same, so we pass json too this._setTextAndFireOnChange(text, json) } /** * Format the code in the text editor */ previewmode.format = function () { const json = this.get() const text = JSON.stringify(json, null, this.indentation) // we know that in this case the json is still the same, so we pass json too this._setTextAndFireOnChange(text, json) } /** * Repair the code in the text editor */ previewmode.repair = function () { const text = this.getText() try { const repairedText = jsonrepair(text) this._setTextAndFireOnChange(repairedText) } catch (err) { // repair was not successful, do nothing } } /** * Set focus to the editor */ previewmode.focus = function () { // we don't really have a place to focus, // let's focus on the transform button this.dom.transform.focus() } /** * Set json data in the editor * @param {*} json */ previewmode.set = function (json) { if (this.history) { this.history.clear() } this._set(json) } /** * Update data. Same as calling `set` in text/code mode. * @param {*} json */ previewmode.update = function (json) { this._set(json) } /** * Set json data * @param {*} json */ previewmode._set = function (json) { this.text = undefined this.json = json this._renderPreview() this._pushHistory() // validate JSON schema this._debouncedValidate() } previewmode._setAndFireOnChange = function (json) { this._set(json) this._onChange() } /** * Get json data * @return {*} json */ previewmode.get = function () { if (this.json === undefined) { const text = this.getText() this.json = parse(text) // this can throw an error } return this.json } /** * Get the text contents of the editor * @return {String} jsonText */ previewmode.getText = function () { if (this.text === undefined) { this.text = JSON.stringify(this.json, null, this.indentation) if (this.options.escapeUnicode === true) { this.text = escapeUnicodeChars(this.text) } } return this.text } /** * Set the text contents of the editor * @param {String} jsonText */ previewmode.setText = function (jsonText) { if (this.history) { this.history.clear() } this._setText(jsonText) } /** * Update the text contents * @param {string} jsonText */ previewmode.updateText = function (jsonText) { // don't update if there are no changes if (this.getText() === jsonText) { return } this._setText(jsonText) } /** * Set the text contents of the editor * @param {string} jsonText * @param {*} [json] Optional JSON instance of the text * @private */ previewmode._setText = function (jsonText, json) { if (this.options.escapeUnicode === true) { this.text = escapeUnicodeChars(jsonText) } else { this.text = jsonText } this.json = json this._renderPreview() if (this.json === undefined) { const me = this this.executeWithBusyMessage(() => { try { // force parsing the json now, else it will be done in validate without feedback me.json = me.get() me._renderPreview() me._pushHistory() } catch (err) { // no need to throw an error, validation will show an error } }, 'parsing...') } else { this._pushHistory() } this._debouncedValidate() } /** * Set text and fire onChange callback * @param {string} jsonText * @param {*} [json] Optional JSON instance of the text * @private */ previewmode._setTextAndFireOnChange = function (jsonText, json) { this._setText(jsonText, json) this._onChange() } /** * Apply history to the current state * @param {{json?: JSON, text?: string}} action * @private */ previewmode._applyHistory = function (action) { this.json = action.json this.text = action.text this._renderPreview() this._debouncedValidate() } /** * Push the current state to history * @private */ previewmode._pushHistory = function () { if (!this.history) { return } const action = { text: this.text, json: this.json } this.history.add(action) } /** * Execute a heavy, blocking action. * Before starting the action, show a message on screen like "parsing..." * @param {function} fn * @param {string} message */ previewmode.executeWithBusyMessage = function (fn, message) { const size = this.getText().length if (size > SIZE_LARGE) { const me = this addClassName(me.frame, 'busy') me.dom.busyContent.innerText = message setTimeout(() => { fn() removeClassName(me.frame, 'busy') me.dom.busyContent.innerText = '' }, 100) } else { fn() } } // TODO: refactor into composable functions instead of this shaky mixin-like structure previewmode.validate = textmode.validate previewmode._renderErrors = textmode._renderErrors // define modes export const previewModeMixins = [ { mode: 'preview', mixin: previewmode, data: 'json' } ] ================================================ FILE: src/js/showMoreNodeFactory.js ================================================ 'use strict' import { translate } from './i18n' /** * A factory function to create an ShowMoreNode, which depends on a Node * @param {function} Node */ export function showMoreNodeFactory (Node) { /** * @constructor ShowMoreNode * @extends Node * @param {TreeEditor} editor * @param {Node} parent * Create a new ShowMoreNode. This is a special node which is created * for arrays or objects having more than 100 items */ function ShowMoreNode (editor, parent) { /** @type {TreeEditor} */ this.editor = editor this.parent = parent this.dom = {} } ShowMoreNode.prototype = new Node() /** * Return a table row with an append button. * @return {Element} dom TR element */ ShowMoreNode.prototype.getDom = function () { if (this.dom.tr) { return this.dom.tr } this._updateEditability() // display "show more" if (!this.dom.tr) { const me = this const parent = this.parent const showMoreButton = document.createElement('a') showMoreButton.appendChild(document.createTextNode(translate('showMore'))) showMoreButton.href = '#' showMoreButton.onclick = event => { // TODO: use callback instead of accessing a method of the parent parent.visibleChilds = Math.floor(parent.visibleChilds / parent.getMaxVisibleChilds() + 1) * parent.getMaxVisibleChilds() me.updateDom() parent.showChilds() event.preventDefault() return false } const showAllButton = document.createElement('a') showAllButton.appendChild(document.createTextNode(translate('showAll'))) showAllButton.href = '#' showAllButton.onclick = event => { // TODO: use callback instead of accessing a method of the parent parent.visibleChilds = Infinity me.updateDom() parent.showChilds() event.preventDefault() return false } const moreContents = document.createElement('div') const moreText = document.createTextNode(this._getShowMoreText()) moreContents.className = 'jsoneditor-show-more' moreContents.appendChild(moreText) moreContents.appendChild(showMoreButton) moreContents.appendChild(document.createTextNode('. ')) moreContents.appendChild(showAllButton) moreContents.appendChild(document.createTextNode('. ')) const tdContents = document.createElement('td') tdContents.appendChild(moreContents) const moreTr = document.createElement('tr') if (this.editor.options.mode === 'tree') { moreTr.appendChild(document.createElement('td')) moreTr.appendChild(document.createElement('td')) } moreTr.appendChild(tdContents) moreTr.className = 'jsoneditor-show-more' this.dom.tr = moreTr this.dom.moreContents = moreContents this.dom.moreText = moreText } this.updateDom() return this.dom.tr } /** * Update the HTML dom of the Node */ ShowMoreNode.prototype.updateDom = function (options) { if (this.isVisible()) { // attach to the right child node (the first non-visible child) this.dom.tr.node = this.parent.childs[this.parent.visibleChilds] if (!this.dom.tr.parentNode) { const nextTr = this.parent._getNextTr() if (nextTr) { nextTr.parentNode.insertBefore(this.dom.tr, nextTr) } } // update the counts in the text this.dom.moreText.nodeValue = this._getShowMoreText() // update left margin this.dom.moreContents.style.marginLeft = (this.getLevel() + 1) * 24 + 'px' } else { if (this.dom.tr && this.dom.tr.parentNode) { this.dom.tr.parentNode.removeChild(this.dom.tr) } } } ShowMoreNode.prototype._getShowMoreText = function () { return translate('showMoreStatus', { visibleChilds: this.parent.visibleChilds, totalChilds: this.parent.childs.length }) + ' ' } /** * Check whether the ShowMoreNode is currently visible. * the ShowMoreNode is visible when it's parent has more childs than * the current visibleChilds * @return {boolean} isVisible */ ShowMoreNode.prototype.isVisible = function () { return this.parent.expanded && this.parent.childs.length > this.parent.visibleChilds } /** * Handle an event. The event is caught centrally by the editor * @param {Event} event */ ShowMoreNode.prototype.onEvent = function (event) { const type = event.type if (type === 'keydown') { this.onKeyDown(event) } } return ShowMoreNode } ================================================ FILE: src/js/showSortModal.js ================================================ import picoModal from 'picomodal' import { translate } from './i18n' import { contains, getChildPaths } from './util' /** * Show advanced sorting modal * @param {HTMLElement} container The container where to center * the modal and create an overlay * @param {JSON} json The JSON data to be sorted. * @param {function} onSort Callback function, invoked with * an object containing the selected * path and direction * @param {Object} options * Available options: * - {string} path The selected path * - {'asc' | 'desc'} direction The selected direction */ export function showSortModal (container, json, onSort, options) { const paths = Array.isArray(json) ? getChildPaths(json) : [''] const selectedPath = options && options.path && contains(paths, options.path) ? options.path : paths[0] const selectedDirection = (options && options.direction) || 'asc' const content = '
    ' + '
    ' + translate('sort') + '
    ' + '
    ' + '' + '' + '' + ' ' + ' ' + '' + '' + ' ' + ' ' + '' + '' + '' + '' + '' + '
    ' + translate('sortFieldLabel') + ' ' + '
    ' + ' ' + '
    ' + '
    ' + translate('sortDirectionLabel') + ' ' + '
    ' + '' + '' + '
    ' + '
    ' + ' ' + '
    ' + '
    ' + '
    ' picoModal({ parent: container, content, overlayClass: 'jsoneditor-modal-overlay', overlayStyles: { backgroundColor: 'rgb(1,1,1)', opacity: 0.3 }, modalClass: 'jsoneditor-modal jsoneditor-modal-sort' }) .afterCreate(modal => { const form = modal.modalElem().querySelector('form') const ok = modal.modalElem().querySelector('#ok') const field = modal.modalElem().querySelector('#field') const direction = modal.modalElem().querySelector('#direction') function preprocessPath (path) { return (path === '') ? '@' : (path[0] === '.') ? path.slice(1) : path } paths.forEach(path => { const option = document.createElement('option') option.text = preprocessPath(path) option.value = path field.appendChild(option) }) function setDirection (value) { direction.value = value direction.className = 'jsoneditor-button-group jsoneditor-button-group-value-' + direction.value } field.value = selectedPath || paths[0] setDirection(selectedDirection || 'asc') direction.onclick = event => { setDirection(event.target.getAttribute('data-value')) } ok.onclick = event => { event.preventDefault() event.stopPropagation() modal.close() onSort({ path: field.value, direction: direction.value }) } if (form) { // form is not available when JSONEditor is created inside a form form.onsubmit = ok.onclick } }) .afterClose(modal => { modal.destroy() }) .show() } ================================================ FILE: src/js/showTransformModal.js ================================================ import picoModal from 'picomodal' import Selectr from './assets/selectr/selectr' import { translate } from './i18n' import { stringifyPartial } from './jsonUtils' import { getChildPaths, debounce } from './util' import { MAX_PREVIEW_CHARACTERS } from './constants' const DEFAULT_DESCRIPTION = 'Enter a JMESPath query to filter, sort, or transform the JSON data.
    ' + 'To learn JMESPath, go to the interactive tutorial.' /** * Show advanced filter and transform modal using JMESPath * @param {Object} params * @property {HTMLElement} container The container where to center * the modal and create an overlay * @property {JSON} json The json data to be transformed * @property {string} [queryDescription] Optional custom description explaining * the transform functionality * @property {function} createQuery Function called to create a query * from the wizard form * @property {function} executeQuery Execute a query for the preview pane * @property {function} onTransform Callback invoked with the created * query as callback */ export function showTransformModal ( { container, json, queryDescription = DEFAULT_DESCRIPTION, createQuery, executeQuery, onTransform } ) { const value = json const content = '
    ' + '
    ' + translate('transform') + '
    ' + '

    ' + queryDescription + '

    ' + '
    ' + translate('transformWizardLabel') + '
    ' + '
    ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + ' ' + '
    ' + translate('transformWizardFilter') + '' + '
    ' + ' ' + '
    ' + '
    ' + ' ' + '
    ' + '
    ' + ' ' + '
    ' + '
    ' + translate('transformWizardSortBy') + '' + '
    ' + ' ' + '
    ' + '
    ' + ' ' + '
    ' + '
    ' + translate('transformWizardSelectFields') + '' + ' ' + '
    ' + '
    ' + '
    ' + translate('transformQueryLabel') + '
    ' + '
    ' + ' ' + '
    ' + '
    ' + translate('transformPreviewLabel') + '
    ' + '
    ' + ' ' + '
    ' + '
    ' + ' ' + '
    ' + '
    ' picoModal({ parent: container, content, overlayClass: 'jsoneditor-modal-overlay', overlayStyles: { backgroundColor: 'rgb(1,1,1)', opacity: 0.3 }, modalClass: 'jsoneditor-modal jsoneditor-modal-transform', focus: false }) .afterCreate(modal => { const elem = modal.modalElem() const wizard = elem.querySelector('#wizard') const ok = elem.querySelector('#ok') const filterField = elem.querySelector('#filterField') const filterRelation = elem.querySelector('#filterRelation') const filterValue = elem.querySelector('#filterValue') const sortField = elem.querySelector('#sortField') const sortOrder = elem.querySelector('#sortOrder') const selectFields = elem.querySelector('#selectFields') const query = elem.querySelector('#query') const preview = elem.querySelector('#preview') if (!Array.isArray(value)) { wizard.style.fontStyle = 'italic' wizard.textContent = '(wizard not available for objects, only for arrays)' } const sortablePaths = getChildPaths(json) sortablePaths.forEach(path => { const formattedPath = preprocessPath(path) const filterOption = document.createElement('option') filterOption.text = formattedPath filterOption.value = formattedPath filterField.appendChild(filterOption) const sortOption = document.createElement('option') sortOption.text = formattedPath sortOption.value = formattedPath sortField.appendChild(sortOption) }) const selectablePaths = getChildPaths(json, true).filter(path => path !== '') if (selectablePaths.length > 0) { selectablePaths.forEach(path => { const formattedPath = preprocessPath(path) const option = document.createElement('option') option.text = formattedPath option.value = formattedPath selectFields.appendChild(option) }) } else { const selectFieldsPart = elem.querySelector('#selectFieldsPart') if (selectFieldsPart) { selectFieldsPart.style.display = 'none' } } const selectrFilterField = new Selectr(filterField, { defaultSelected: false, clearable: true, allowDeselect: true, placeholder: 'field...' }) const selectrFilterRelation = new Selectr(filterRelation, { defaultSelected: false, clearable: true, allowDeselect: true, placeholder: 'compare...' }) const selectrSortField = new Selectr(sortField, { defaultSelected: false, clearable: true, allowDeselect: true, placeholder: 'field...' }) const selectrSortOrder = new Selectr(sortOrder, { defaultSelected: false, clearable: true, allowDeselect: true, placeholder: 'order...' }) const selectrSelectFields = new Selectr(selectFields, { multiple: true, clearable: true, defaultSelected: false, placeholder: 'select fields...' }) selectrFilterField.on('selectr.change', generateQueryFromWizard) selectrFilterRelation.on('selectr.change', generateQueryFromWizard) filterValue.oninput = generateQueryFromWizard selectrSortField.on('selectr.change', generateQueryFromWizard) selectrSortOrder.on('selectr.change', generateQueryFromWizard) selectrSelectFields.on('selectr.change', generateQueryFromWizard) elem.querySelector('.pico-modal-contents').onclick = event => { // prevent the first clear button (in any select box) from getting // focus when clicking anywhere in the modal. Only allow clicking links. if (event.target.nodeName !== 'A') { event.preventDefault() } } function preprocessPath (path) { return (path === '') ? '@' : (path[0] === '.') ? path.slice(1) : path } function updatePreview () { try { const transformed = executeQuery(value, query.value) preview.className = 'jsoneditor-transform-preview' preview.value = stringifyPartial(transformed, 2, MAX_PREVIEW_CHARACTERS) ok.disabled = false } catch (err) { preview.className = 'jsoneditor-transform-preview jsoneditor-error' preview.value = err.toString() ok.disabled = true } } const debouncedUpdatePreview = debounce(updatePreview, 300) function tryCreateQuery (json, queryOptions) { try { query.value = createQuery(json, queryOptions) ok.disabled = false debouncedUpdatePreview() } catch (err) { const message = 'Error: an error happened when executing "createQuery": ' + (err.message || err.toString()) query.value = '' ok.disabled = true preview.className = 'jsoneditor-transform-preview jsoneditor-error' preview.value = message } } function generateQueryFromWizard () { const queryOptions = {} if (filterField.value && filterRelation.value && filterValue.value) { queryOptions.filter = { field: filterField.value, relation: filterRelation.value, value: filterValue.value } } if (sortField.value && sortOrder.value) { queryOptions.sort = { field: sortField.value, direction: sortOrder.value } } if (selectFields.value) { const fields = [] for (let i = 0; i < selectFields.options.length; i++) { if (selectFields.options[i].selected) { const selectedField = selectFields.options[i].value fields.push(selectedField) } } queryOptions.projection = { fields } } tryCreateQuery(json, queryOptions) } query.oninput = debouncedUpdatePreview ok.onclick = event => { event.preventDefault() event.stopPropagation() modal.close() onTransform(query.value) } // initialize with empty query tryCreateQuery(json, {}) setTimeout(() => { query.select() query.focus() query.selectionStart = 3 query.selectionEnd = 3 }) }) .afterClose(modal => { modal.destroy() }) .show() } ================================================ FILE: src/js/textmode.js ================================================ 'use strict' import { jsonrepair } from 'jsonrepair' import ace from './ace' import { DEFAULT_MODAL_ANCHOR } from './constants' import { ErrorTable } from './ErrorTable' import { FocusTracker } from './FocusTracker' import { setLanguage, setLanguages, translate } from './i18n' import { createQuery, executeQuery } from './jmespathQuery' import { ModeSwitcher } from './ModeSwitcher' import { showSortModal } from './showSortModal' import { showTransformModal } from './showTransformModal' import { tryRequireThemeJsonEditor } from './tryRequireThemeJsonEditor' import { SchemaTextCompleter } from './SchemaTextCompleter' import { addClassName, debounce, escapeUnicodeChars, getIndexForPosition, getInputSelection, getPositionForPath, improveSchemaError, isObject, isValidationErrorChanged, parse, sort, sortObjectKeys } from './util' import { validateCustom } from './validationUtils' // create a mixin with the functions for text mode const textmode = {} const DEFAULT_THEME = 'ace/theme/jsoneditor' /** * Create a text editor * @param {Element} container * @param {Object} [options] Object with options. See docs for details. * @private */ textmode.create = function (container, options = {}) { if (typeof options.statusBar === 'undefined') { options.statusBar = true } // setting default for textmode options.mainMenuBar = options.mainMenuBar !== false options.enableSort = options.enableSort !== false options.enableTransform = options.enableTransform !== false options.createQuery = options.createQuery || createQuery options.executeQuery = options.executeQuery || executeQuery options.showErrorTable = options.showErrorTable !== undefined ? options.showErrorTable : ['text', 'preview'] this.options = options // indentation if (typeof options.indentation === 'number') { this.indentation = Number(options.indentation) } else { this.indentation = 2 // number of spaces } // language setLanguages(this.options.languages) setLanguage(this.options.language) // grab ace from options if provided const _ace = options.ace ? options.ace : ace // TODO: make the option options.ace deprecated, it's not needed anymore (see #309) // determine mode this.mode = (options.mode === 'code') ? 'code' : 'text' if (this.mode === 'code') { // verify whether Ace editor is available and supported if (typeof _ace === 'undefined') { this.mode = 'text' console.warn('Failed to load Ace editor, falling back to plain text mode. Please use a JSONEditor bundle including Ace, or pass Ace as via the configuration option `ace`.') } } // determine theme this.theme = options.theme || DEFAULT_THEME if (this.theme === DEFAULT_THEME && _ace) { tryRequireThemeJsonEditor() } if (options.onTextSelectionChange) { this.onTextSelectionChange(options.onTextSelectionChange) } const me = this this.container = container this.dom = {} this.aceEditor = undefined // ace code editor this.textarea = undefined // plain text editor (fallback when Ace is not available) this.validateSchema = null this.annotations = [] this.lastSchemaErrors = undefined // create a debounced validate function this._debouncedValidate = debounce(this._validateAndCatch.bind(this), this.DEBOUNCE_INTERVAL) this.width = container.clientWidth this.height = container.clientHeight this.frame = document.createElement('div') this.frame.className = 'jsoneditor jsoneditor-mode-' + this.options.mode this.frame.onclick = event => { // prevent default submit action when the editor is located inside a form event.preventDefault() } this.frame.onkeydown = event => { me._onKeyDown(event) } // setting the FocusTracker on 'this.frame' to track the editor's focus event const focusTrackerConfig = { target: this.frame, onFocus: this.options.onFocus || null, onBlur: this.options.onBlur || null } this.frameFocusTracker = new FocusTracker(focusTrackerConfig) this.content = document.createElement('div') this.content.className = 'jsoneditor-outer' if (this.options.mainMenuBar) { addClassName(this.content, 'has-main-menu-bar') // create menu this.menu = document.createElement('div') this.menu.className = 'jsoneditor-menu' this.frame.appendChild(this.menu) // create format button const buttonFormat = document.createElement('button') buttonFormat.type = 'button' buttonFormat.className = 'jsoneditor-format' buttonFormat.title = translate('formatTitle') this.menu.appendChild(buttonFormat) buttonFormat.onclick = () => { try { me.format() me._onChange() } catch (err) { me._onError(err) } } // create compact button const buttonCompact = document.createElement('button') buttonCompact.type = 'button' buttonCompact.className = 'jsoneditor-compact' buttonCompact.title = translate('compactTitle') this.menu.appendChild(buttonCompact) buttonCompact.onclick = () => { try { me.compact() me._onChange() } catch (err) { me._onError(err) } } // create sort button if (this.options.enableSort) { const sort = document.createElement('button') sort.type = 'button' sort.className = 'jsoneditor-sort' sort.title = translate('sortTitleShort') sort.onclick = () => { me._showSortModal() } this.menu.appendChild(sort) } // create transform button if (this.options.enableTransform) { const transform = document.createElement('button') transform.type = 'button' transform.title = translate('transformTitleShort') transform.className = 'jsoneditor-transform' transform.onclick = () => { me._showTransformModal() } this.menu.appendChild(transform) } // create repair button const buttonRepair = document.createElement('button') buttonRepair.type = 'button' buttonRepair.className = 'jsoneditor-repair' buttonRepair.title = translate('repairTitle') this.menu.appendChild(buttonRepair) buttonRepair.onclick = () => { try { me.repair() me._onChange() } catch (err) { me._onError(err) } } // create undo/redo buttons if (this.mode === 'code') { // create undo button const undo = document.createElement('button') undo.type = 'button' undo.className = 'jsoneditor-undo jsoneditor-separator' undo.title = translate('undo') undo.onclick = () => { this.aceEditor.getSession().getUndoManager().undo() } this.menu.appendChild(undo) this.dom.undo = undo // create redo button const redo = document.createElement('button') redo.type = 'button' redo.className = 'jsoneditor-redo' redo.title = translate('redo') redo.onclick = () => { this.aceEditor.getSession().getUndoManager().redo() } this.menu.appendChild(redo) this.dom.redo = redo } // create mode box if (this.options && this.options.modes && this.options.modes.length) { this.modeSwitcher = new ModeSwitcher(this.menu, this.options.modes, this.options.mode, function onSwitch (mode) { // switch mode and restore focus try { me.setMode(mode) me.modeSwitcher.focus() } catch (err) { me._onError(err) } }) } if (this.mode === 'code') { const poweredBy = document.createElement('a') poweredBy.appendChild(document.createTextNode('powered by ace')) poweredBy.href = 'https://ace.c9.io/' poweredBy.target = '_blank' poweredBy.className = 'jsoneditor-poweredBy' poweredBy.onclick = () => { // TODO: this anchor falls below the margin of the content, // therefore the normal a.href does not work. We use a click event // for now, but this should be fixed. window.open(poweredBy.href, poweredBy.target, 'noreferrer') } this.menu.appendChild(poweredBy) } } const emptyNode = {} const isReadOnly = (this.options.onEditable && typeof (this.options.onEditable === 'function') && !this.options.onEditable(emptyNode)) this.frame.appendChild(this.content) this.container.appendChild(this.frame) if (this.mode === 'code') { this.editorDom = document.createElement('div') this.editorDom.style.height = '100%' // TODO: move to css this.editorDom.style.width = '100%' // TODO: move to css this.content.appendChild(this.editorDom) const aceEditor = _ace.edit(this.editorDom) const aceSession = aceEditor.getSession() aceEditor.$blockScrolling = Infinity aceEditor.setTheme(this.theme) aceEditor.setOptions({ readOnly: isReadOnly }) aceEditor.setShowPrintMargin(false) aceEditor.setFontSize('14px') aceSession.setMode('ace/mode/json') aceSession.setTabSize(this.indentation) aceSession.setUseSoftTabs(true) aceSession.setUseWrapMode(true) // replace ace setAnnotations with custom function that also covers jsoneditor annotations const originalSetAnnotations = aceSession.setAnnotations aceSession.setAnnotations = function (annotations) { originalSetAnnotations.call(this, annotations && annotations.length ? annotations : me.annotations) } // disable Ctrl+L quickkey of Ace (is used by the browser to select the address bar) aceEditor.commands.bindKey('Ctrl-L', null) aceEditor.commands.bindKey('Command-L', null) // disable the quickkeys we want to use for Format and Compact aceEditor.commands.bindKey('Ctrl-\\', null) aceEditor.commands.bindKey('Command-\\', null) aceEditor.commands.bindKey('Ctrl-Shift-\\', null) aceEditor.commands.bindKey('Command-Shift-\\', null) this.aceEditor = aceEditor // register onchange event aceEditor.on('change', this._onChange.bind(this)) aceEditor.on('changeSelection', this._onSelect.bind(this)) } else { // load a plain text textarea const textarea = document.createElement('textarea') textarea.className = 'jsoneditor-text' textarea.spellcheck = false this.content.appendChild(textarea) this.textarea = textarea this.textarea.readOnly = isReadOnly // register onchange event if (this.textarea.oninput === null) { this.textarea.oninput = this._onChange.bind(this) } else { // oninput is undefined. For IE8- this.textarea.onchange = this._onChange.bind(this) } textarea.onselect = this._onSelect.bind(this) textarea.onmousedown = this._onMouseDown.bind(this) textarea.onblur = this._onBlur.bind(this) } this._updateHistoryButtons() const errorTableVisible = Array.isArray(this.options.showErrorTable) ? this.options.showErrorTable.includes(this.mode) : this.options.showErrorTable === true this.errorTable = new ErrorTable({ errorTableVisible, onToggleVisibility: function () { me._validateAndCatch() }, onFocusLine: function (line) { me.isFocused = true if (!isNaN(line)) { me.setTextSelection({ row: line, column: 1 }, { row: line, column: 1000 }) } }, onChangeHeight: function (height) { // TODO: change CSS to using flex box, remove setting height using JavaScript const statusBarHeight = me.dom.statusBar ? me.dom.statusBar.clientHeight : 0 const totalHeight = height + statusBarHeight + 1 me.content.style.marginBottom = (-totalHeight) + 'px' me.content.style.paddingBottom = totalHeight + 'px' } }) this.frame.appendChild(this.errorTable.getErrorTable()) if (options.statusBar) { addClassName(this.content, 'has-status-bar') this.curserInfoElements = {} const statusBar = document.createElement('div') this.dom.statusBar = statusBar statusBar.className = 'jsoneditor-statusbar' this.frame.appendChild(statusBar) const lnLabel = document.createElement('span') lnLabel.className = 'jsoneditor-curserinfo-label' lnLabel.innerText = 'Ln:' const lnVal = document.createElement('span') lnVal.className = 'jsoneditor-curserinfo-val' lnVal.innerText = '1' statusBar.appendChild(lnLabel) statusBar.appendChild(lnVal) const colLabel = document.createElement('span') colLabel.className = 'jsoneditor-curserinfo-label' colLabel.innerText = 'Col:' const colVal = document.createElement('span') colVal.className = 'jsoneditor-curserinfo-val' colVal.innerText = '1' statusBar.appendChild(colLabel) statusBar.appendChild(colVal) this.curserInfoElements.colVal = colVal this.curserInfoElements.lnVal = lnVal const countLabel = document.createElement('span') countLabel.className = 'jsoneditor-curserinfo-label' countLabel.innerText = 'characters selected' countLabel.style.display = 'none' const countVal = document.createElement('span') countVal.className = 'jsoneditor-curserinfo-count' countVal.innerText = '0' countVal.style.display = 'none' this.curserInfoElements.countLabel = countLabel this.curserInfoElements.countVal = countVal statusBar.appendChild(countVal) statusBar.appendChild(countLabel) statusBar.appendChild(this.errorTable.getErrorCounter()) statusBar.appendChild(this.errorTable.getWarningIcon()) statusBar.appendChild(this.errorTable.getErrorIcon()) } this.setSchema(this.options.schema, this.options.schemaRefs) } textmode._onSchemaChange = function (schema, schemaRefs) { if (!this.aceEditor) { return } if (this.options.allowSchemaSuggestions && schema) { this.aceEditor.setOption('enableBasicAutocompletion', [new SchemaTextCompleter(schema, schemaRefs)]) this.aceEditor.setOption('enableLiveAutocompletion', true) } else { this.aceEditor.setOption('enableBasicAutocompletion', undefined) this.aceEditor.setOption('enableLiveAutocompletion', false) } } /** * Handle a change: * - Validate JSON schema * - Send a callback to the onChange listener if provided * @private */ textmode._onChange = function () { if (this.onChangeDisabled) { return } // enable/disable undo/redo buttons setTimeout(() => { if (this._updateHistoryButtons) { this._updateHistoryButtons() } }) // validate JSON schema (if configured) this._debouncedValidate() // trigger the onChange callback if (this.options.onChange) { try { this.options.onChange() } catch (err) { console.error('Error in onChange callback: ', err) } } // trigger the onChangeText callback if (this.options.onChangeText) { try { this.options.onChangeText(this.getText()) } catch (err) { console.error('Error in onChangeText callback: ', err) } } } textmode._updateHistoryButtons = function () { if (this.aceEditor && this.dom.undo && this.dom.redo) { const undoManager = this.aceEditor.getSession().getUndoManager() if (undoManager && undoManager.hasUndo && undoManager.hasRedo) { this.dom.undo.disabled = !undoManager.hasUndo() this.dom.redo.disabled = !undoManager.hasRedo() } } } /** * Open a sort modal * @private */ textmode._showSortModal = function () { try { const me = this const container = this.options.modalAnchor || DEFAULT_MODAL_ANCHOR const json = this.get() function onSort (sortedBy) { if (Array.isArray(json)) { const sortedJson = sort(json, sortedBy.path, sortedBy.direction) me.sortedBy = sortedBy me.update(sortedJson) } if (isObject(json)) { const sortedJson = sortObjectKeys(json, sortedBy.direction) me.sortedBy = sortedBy me.update(sortedJson) } } showSortModal(container, json, onSort, me.sortedBy) } catch (err) { this._onError(err) } } /** * Open a transform modal * @private */ textmode._showTransformModal = function () { try { const { modalAnchor, createQuery, executeQuery, queryDescription } = this.options const json = this.get() showTransformModal({ container: modalAnchor || DEFAULT_MODAL_ANCHOR, json, queryDescription, // can be undefined createQuery, executeQuery, onTransform: query => { const updatedJson = executeQuery(json, query) this.update(updatedJson) } }) } catch (err) { this._onError(err) } } /** * Handle text selection * Calculates the cursor position and selection range and updates menu * @private */ textmode._onSelect = function () { this._updateCursorInfo() this._emitSelectionChange() } /** * Event handler for keydown. Handles shortcut keys * @param {Event} event * @private */ textmode._onKeyDown = function (event) { const keynum = event.which || event.keyCode let handled = false if (keynum === 73 && event.ctrlKey) { if (event.shiftKey) { // Ctrl+Shift+I this.compact() this._onChange() } else { // Ctrl+I this.format() this._onChange() } handled = true } if (handled) { event.preventDefault() event.stopPropagation() } this._updateCursorInfo() this._emitSelectionChange() } /** * Event handler for mousedown. * @private */ textmode._onMouseDown = function () { this._updateCursorInfo() this._emitSelectionChange() } /** * Event handler for blur. * @private */ textmode._onBlur = function () { const me = this // this allows to avoid blur when clicking inner elements (like the errors panel) // just make sure to set the isFocused to true on the inner element onclick callback setTimeout(() => { if (!me.isFocused) { me._updateCursorInfo() me._emitSelectionChange() } me.isFocused = false }) } /** * Update the cursor info and the status bar, if presented */ textmode._updateCursorInfo = function () { const me = this let line, col, count if (this.textarea) { setTimeout(() => { // this to verify we get the most updated textarea cursor selection const selectionRange = getInputSelection(me.textarea) if (selectionRange.startIndex !== selectionRange.endIndex) { count = selectionRange.endIndex - selectionRange.startIndex } if (count && me.cursorInfo && me.cursorInfo.line === selectionRange.end.row && me.cursorInfo.column === selectionRange.end.column) { line = selectionRange.start.row col = selectionRange.start.column } else { line = selectionRange.end.row col = selectionRange.end.column } me.cursorInfo = { line, column: col, count } if (me.options.statusBar) { updateDisplay() } }, 0) } else if (this.aceEditor && this.curserInfoElements) { const curserPos = this.aceEditor.getCursorPosition() const selectedText = this.aceEditor.getSelectedText() line = curserPos.row + 1 col = curserPos.column + 1 count = selectedText.length me.cursorInfo = { line, column: col, count } if (this.options.statusBar) { updateDisplay() } } function updateDisplay () { if (me.curserInfoElements.countVal.innerText !== count) { me.curserInfoElements.countVal.innerText = count me.curserInfoElements.countVal.style.display = count ? 'inline' : 'none' me.curserInfoElements.countLabel.style.display = count ? 'inline' : 'none' } me.curserInfoElements.lnVal.innerText = line me.curserInfoElements.colVal.innerText = col } } /** * emits selection change callback, if given * @private */ textmode._emitSelectionChange = function () { if (this._selectionChangedHandler) { const currentSelection = this.getTextSelection() this._selectionChangedHandler(currentSelection.start, currentSelection.end, currentSelection.text) } } /** * refresh ERROR annotations state * error annotations are handled by the ace json mode (ace/mode/json) * validation annotations are handled by this mode * therefore in order to refresh we send only the annotations of error type in order to maintain its state * @private */ textmode._refreshAnnotations = function () { const session = this.aceEditor && this.aceEditor.getSession() if (session) { const errEnnotations = session.getAnnotations().filter(annotation => annotation.type === 'error') session.setAnnotations(errEnnotations) } } /** * Destroy the editor. Clean up DOM, event listeners, and web workers. */ textmode.destroy = function () { // remove old ace editor if (this.aceEditor) { this.aceEditor.destroy() this.aceEditor = null } if (this.frame && this.container && this.frame.parentNode === this.container) { this.container.removeChild(this.frame) } if (this.modeSwitcher) { this.modeSwitcher.destroy() this.modeSwitcher = null } this.textarea = null this._debouncedValidate = null // Removing the FocusTracker set to track the editor's focus event this.frameFocusTracker.destroy() } /** * Compact the code in the text editor */ textmode.compact = function () { const json = this.get() const text = JSON.stringify(json) this.updateText(text) } /** * Format the code in the text editor */ textmode.format = function () { const json = this.get() const text = JSON.stringify(json, null, this.indentation) this.updateText(text) } /** * Repair the code in the text editor */ textmode.repair = function () { const text = this.getText() try { const repairedText = jsonrepair(text) this.updateText(repairedText) } catch (err) { // repair was not successful, do nothing } } /** * Set focus to the formatter */ textmode.focus = function () { if (this.textarea) { this.textarea.focus() } if (this.aceEditor) { this.aceEditor.focus() } } /** * Resize the formatter */ textmode.resize = function () { if (this.aceEditor) { const force = false this.aceEditor.resize(force) } } /** * Set json data in the formatter * @param {*} json */ textmode.set = function (json) { this.setText(JSON.stringify(json, null, this.indentation)) } /** * Update data. Same as calling `set` in text/code mode. * @param {*} json */ textmode.update = function (json) { this.updateText(JSON.stringify(json, null, this.indentation)) } /** * Get json data from the formatter * @return {*} json */ textmode.get = function () { const text = this.getText() return parse(text) // this can throw an error } /** * Get the text contents of the editor * @return {String} jsonText */ textmode.getText = function () { if (this.textarea) { return this.textarea.value } if (this.aceEditor) { return this.aceEditor.getValue() } return '' } /** * Set the text contents of the editor and optionally clear the history * @param {String} jsonText * @param {boolean} clearHistory Only applicable for mode 'code' * @private */ textmode._setText = function (jsonText, clearHistory) { const text = (this.options.escapeUnicode === true) ? escapeUnicodeChars(jsonText) : jsonText if (this.textarea) { this.textarea.value = text } if (this.aceEditor) { // prevent emitting onChange events while setting new text this.onChangeDisabled = true this.aceEditor.setValue(text, -1) this.onChangeDisabled = false if (clearHistory) { // prevent initial undo action clearing the initial contents const me = this setTimeout(() => { if (me.aceEditor) { me.aceEditor.session.getUndoManager().reset() } }) } setTimeout(() => { if (this._updateHistoryButtons) { this._updateHistoryButtons() } }) } // validate JSON schema this._debouncedValidate() } /** * Set the text contents of the editor * @param {String} jsonText */ textmode.setText = function (jsonText) { this._setText(jsonText, true) } /** * Update the text contents * @param {string} jsonText */ textmode.updateText = function (jsonText) { // don't update if there are no changes if (this.getText() === jsonText) { return } this._setText(jsonText, false) } /** * Validate current JSON object against the configured JSON schema * Throws an exception when no JSON schema is configured */ textmode.validate = function () { let schemaErrors = [] let parseErrors = [] let json try { json = this.get() // this can fail when there is no valid json // execute JSON schema validation (ajv) if (this.validateSchema) { const valid = this.validateSchema(json) if (!valid) { schemaErrors = this.validateSchema.errors.map(error => { error.type = 'validation' return improveSchemaError(error) }) } } // execute custom validation and after than merge and render all errors // TODO: implement a better mechanism for only using the last validation action this.validationSequence = (this.validationSequence || 0) + 1 const me = this const seq = this.validationSequence return validateCustom(json, this.options.onValidate) .then(customValidationErrors => { // only apply when there was no other validation started whilst resolving async results if (seq === me.validationSequence) { const errors = schemaErrors.concat(parseErrors).concat(customValidationErrors) me._renderErrors(errors) if ( typeof this.options.onValidationError === 'function' && isValidationErrorChanged(errors, this.lastSchemaErrors) ) { this.options.onValidationError.call(this, errors) } this.lastSchemaErrors = errors } return this.lastSchemaErrors }) } catch (err) { if (this.getText()) { // try to extract the line number from the jsonlint error message const match = /\w*line\s*(\d+)\w*/g.exec(err.message) let line if (match) { line = +match[1] } parseErrors = [{ type: 'error', message: err.message.replace(/\n/g, '
    '), line }] } this._renderErrors(parseErrors) if ( typeof this.options.onValidationError === 'function' && isValidationErrorChanged(parseErrors, this.lastSchemaErrors) ) { this.options.onValidationError.call(this, parseErrors) } this.lastSchemaErrors = parseErrors return Promise.resolve(this.lastSchemaErrors) } } textmode._validateAndCatch = function () { this.validate().catch(err => { console.error('Error running validation:', err) }) } textmode._renderErrors = function (errors) { const jsonText = this.getText() const errorPaths = [] errors.reduce((acc, curr) => { if (typeof curr.dataPath === 'string' && acc.indexOf(curr.dataPath) === -1) { acc.push(curr.dataPath) } return acc }, errorPaths) const errorLocations = getPositionForPath(jsonText, errorPaths) // render annotations in Ace Editor (if any) if (this.aceEditor) { this.annotations = errorLocations.map(errLoc => { const validationErrors = errors.filter(err => err.dataPath === errLoc.path) const message = validationErrors.map(err => err.message).join('\n') if (message) { return { row: errLoc.line, column: errLoc.column, text: 'Schema validation error' + (validationErrors.length !== 1 ? 's' : '') + ': \n' + message, type: 'warning', source: 'jsoneditor' } } return {} }) this._refreshAnnotations() } // render errors in the errors table (if any) this.errorTable.setErrors(errors, errorLocations) // update the height of the ace editor if (this.aceEditor) { const force = false this.aceEditor.resize(force) } } /** * Get the selection details * @returns {{start:{row:Number, column:Number},end:{row:Number, column:Number},text:String}} */ textmode.getTextSelection = function () { let selection = {} if (this.textarea) { const selectionRange = getInputSelection(this.textarea) if (this.cursorInfo && this.cursorInfo.line === selectionRange.end.row && this.cursorInfo.column === selectionRange.end.column) { // selection direction is bottom => up selection.start = selectionRange.end selection.end = selectionRange.start } else { selection = selectionRange } return { start: selection.start, end: selection.end, text: this.textarea.value.substring(selectionRange.startIndex, selectionRange.endIndex) } } if (this.aceEditor) { const aceSelection = this.aceEditor.getSelection() const selectedText = this.aceEditor.getSelectedText() const range = aceSelection.getRange() const lead = aceSelection.getSelectionLead() if (lead.row === range.end.row && lead.column === range.end.column) { selection = range } else { // selection direction is bottom => up selection.start = range.end selection.end = range.start } return { start: { row: selection.start.row + 1, column: selection.start.column + 1 }, end: { row: selection.end.row + 1, column: selection.end.column + 1 }, text: selectedText } } } /** * Callback registration for selection change * @param {selectionCallback} callback * * @callback selectionCallback */ textmode.onTextSelectionChange = function (callback) { if (typeof callback === 'function') { this._selectionChangedHandler = debounce(callback, this.DEBOUNCE_INTERVAL) } } /** * Set selection on editor's text * @param {{row:Number, column:Number}} startPos selection start position * @param {{row:Number, column:Number}} endPos selected end position */ textmode.setTextSelection = function (startPos, endPos) { if (!startPos || !endPos) return if (this.textarea) { const startIndex = getIndexForPosition(this.textarea, startPos.row, startPos.column) const endIndex = getIndexForPosition(this.textarea, endPos.row, endPos.column) if (startIndex > -1 && endIndex > -1) { if (this.textarea.setSelectionRange) { this.textarea.focus() this.textarea.setSelectionRange(startIndex, endIndex) } else if (this.textarea.createTextRange) { // IE < 9 const range = this.textarea.createTextRange() range.collapse(true) range.moveEnd('character', endIndex) range.moveStart('character', startIndex) range.select() } const rows = (this.textarea.value.match(/\n/g) || []).length + 1 const lineHeight = this.textarea.scrollHeight / rows const selectionScrollPos = (startPos.row * lineHeight) this.textarea.scrollTop = selectionScrollPos > this.textarea.clientHeight ? (selectionScrollPos - (this.textarea.clientHeight / 2)) : 0 } } else if (this.aceEditor) { const range = { start: { row: startPos.row - 1, column: startPos.column - 1 }, end: { row: endPos.row - 1, column: endPos.column - 1 } } this.aceEditor.selection.setRange(range) this.aceEditor.scrollToLine(startPos.row - 1, true) } } function load () { try { this.format() } catch (err) { // in case of an error, just move on, failing formatting is not a big deal } } // define modes export const textModeMixins = [ { mode: 'text', mixin: textmode, data: 'text', load }, { mode: 'code', mixin: textmode, data: 'text', load } ] ================================================ FILE: src/js/treemode.js ================================================ 'use strict' import { autocomplete } from './autocomplete' import { ContextMenu } from './ContextMenu' import { FocusTracker } from './FocusTracker' import { Highlighter } from './Highlighter' import { setLanguage, setLanguages, translate } from './i18n' import { createQuery, executeQuery } from './jmespathQuery' import { ModeSwitcher } from './ModeSwitcher' import { Node } from './Node' import { NodeHistory } from './NodeHistory' import { SearchBox } from './SearchBox' import { TreePath } from './TreePath' import { addClassName, addEventListener, debounce, getAbsoluteTop, getSelectionOffset, getWindow, hasParentNode, improveSchemaError, isPromise, isValidationErrorChanged, isValidValidationError, parse, removeClassName, removeEventListener, selectContentEditable, setSelectionOffset, tryJsonRepair } from './util' import VanillaPicker from './vanilla-picker' // create a mixin with the functions for tree mode const treemode = {} /** * Create a tree editor * @param {Element} container Container element * @param {Object} [options] Object with options. See docs for details. * @private */ treemode.create = function (container, options) { if (!container) { throw new Error('No container element provided.') } this.container = container this.dom = {} this.highlighter = new Highlighter() this.selection = undefined // will hold the last input selection this.multiselection = { nodes: [] } this.validateSchema = null // will be set in .setSchema(schema) this.validationSequence = 0 this.errorNodes = [] this.lastSchemaErrors = undefined this.node = null this.focusTarget = null this._setOptions(options) if (options.autocomplete) { this.autocomplete = autocomplete(options.autocomplete) } if (this.options.history && this.options.mode !== 'view') { this.history = new NodeHistory(this) } this._createFrame() this._createTable() } /** * Destroy the editor. Clean up DOM, event listeners, and web workers. */ treemode.destroy = function () { if (this.frame && this.container && this.frame.parentNode === this.container) { this.container.removeChild(this.frame) this.frame = null } this.container = null this.dom = null this.clear() this.node = null this.focusTarget = null this.selection = null this.multiselection = null this.errorNodes = null this.validateSchema = null this._debouncedValidate = null if (this.history) { this.history.destroy() this.history = null } if (this.searchBox) { this.searchBox.destroy() this.searchBox = null } if (this.modeSwitcher) { this.modeSwitcher.destroy() this.modeSwitcher = null } // Removing the FocusTracker set to track the editor's focus event this.frameFocusTracker.destroy() } /** * Initialize and set default options * @param {Object} [options] See description in constructor * @private */ treemode._setOptions = function (options) { this.options = { search: true, history: true, mode: 'tree', name: undefined, // field name of root node schema: null, schemaRefs: null, autocomplete: null, navigationBar: true, mainMenuBar: true, limitDragging: false, onSelectionChange: null, colorPicker: true, onColorPicker: function (parent, color, onChange) { if (VanillaPicker) { // we'll render the color picker on top // when there is not enough space below, and there is enough space above const pickerHeight = 300 // estimated height of the color picker const top = parent.getBoundingClientRect().top const windowHeight = getWindow(parent).innerHeight const showOnTop = ((windowHeight - top) < pickerHeight && top > pickerHeight) new VanillaPicker({ parent, color, popup: showOnTop ? 'top' : 'bottom', onDone: function (color) { const alpha = color.rgba[3] const hex = (alpha === 1) ? color.hex.substr(0, 7) // return #RRGGBB : color.hex // return #RRGGBBAA onChange(hex) } }).show() } else { console.warn('Cannot open color picker: the `vanilla-picker` library is not included in the bundle. ' + 'Either use the full bundle or implement your own color picker using `onColorPicker`.') } }, timestampTag: true, timestampFormat: null, createQuery, executeQuery, onEvent: null, enableSort: true, enableTransform: true } // copy all options if (options) { Object.keys(options).forEach(prop => { this.options[prop] = options[prop] }) // default limitDragging to true when a JSON schema is defined if (options.limitDragging == null && options.schema != null) { this.options.limitDragging = true } } // compile a JSON schema validator if a JSON schema is provided this.setSchema(this.options.schema, this.options.schemaRefs) // create a debounced validate function this._debouncedValidate = debounce(this._validateAndCatch.bind(this), this.DEBOUNCE_INTERVAL) if (options.onSelectionChange) { this.onSelectionChange(options.onSelectionChange) } setLanguages(this.options.languages) setLanguage(this.options.language) } /** * Set new JSON object in editor. * Resets the state of the editor (expanded nodes, search, selection). * * @param {*} json */ treemode.set = function (json) { // verify if json is valid JSON, ignore when a function if (json instanceof Function || (json === undefined)) { this.clear() } else { this.content.removeChild(this.table) // Take the table offline // replace the root node const params = { field: this.options.name, value: json } const node = new Node(this, params) this._setRoot(node) // validate JSON schema (if configured) this._validateAndCatch() // expand const recurse = false this.node.expand(recurse) this.content.appendChild(this.table) // Put the table online again } // TODO: maintain history, store last state and previous document if (this.history) { this.history.clear() } // clear search if (this.searchBox) { this.searchBox.clear() } } /** * Update JSON object in editor. * Maintains the state of the editor (expanded nodes, search, selection). * * @param {*} json */ treemode.update = function (json) { // don't update if there are no changes if (this.node.deepEqual(json)) { return } const selection = this.getSelection() // apply the changed json this.onChangeDisabled = true // don't fire an onChange event this.node.update(json) this.onChangeDisabled = false // validate JSON schema this._validateAndCatch() // update search result if any if (this.searchBox && !this.searchBox.isEmpty()) { this.searchBox.forceSearch() } // update selection if any if (selection && selection.start && selection.end) { // only keep/update the selection if both start and end node still exists, // else we clear the selection const startNode = this.node.findNodeByPath(selection.start.path) const endNode = this.node.findNodeByPath(selection.end.path) if (startNode && endNode) { this.setSelection(selection.start, selection.end) } else { this.setSelection({}, {}) // clear selection } } else { this.setSelection({}, {}) // clear selection } } /** * Get JSON object from editor * @return {Object | undefined} json */ treemode.get = function () { // TODO: resolve pending debounced input changes if any, but do not resolve invalid inputs if (this.node) { return this.node.getValue() } else { return undefined } } /** * Get the text contents of the editor * @return {String} jsonText */ treemode.getText = function () { return JSON.stringify(this.get()) } /** * Set the text contents of the editor. * Resets the state of the editor (expanded nodes, search, selection). * @param {String} jsonText */ treemode.setText = function (jsonText) { try { this.set(parse(jsonText)) // this can throw an error } catch (err) { // try to repair json, replace JavaScript notation with JSON notation const repairedJsonText = tryJsonRepair(jsonText) // try to parse again this.set(parse(repairedJsonText)) // this can throw an error } } /** * Update the text contents of the editor. * Maintains the state of the editor (expanded nodes, search, selection). * @param {String} jsonText */ treemode.updateText = function (jsonText) { try { this.update(parse(jsonText)) // this can throw an error } catch (err) { // try to repair json, replace JavaScript notation with JSON notation const repairJsonText = tryJsonRepair(jsonText) // try to parse again this.update(parse(repairJsonText)) // this can throw an error } } /** * Set a field name for the root node. * @param {String | undefined} name */ treemode.setName = function (name) { this.options.name = name if (this.node) { this.node.updateField(this.options.name) } } /** * Get the field name for the root node. * @return {String | undefined} name */ treemode.getName = function () { return this.options.name } /** * Set focus to the editor. Focus will be set to: * - the first editable field or value, or else * - to the expand button of the root node, or else * - to the context menu button of the root node, or else * - to the first button in the top menu */ treemode.focus = function () { let input = this.scrollableContent.querySelector('[contenteditable=true]') if (input) { input.focus() } else if (this.node.dom.expand) { this.node.dom.expand.focus() } else if (this.node.dom.menu) { this.node.dom.menu.focus() } else { // focus to the first button in the menu input = this.frame.querySelector('button') if (input) { input.focus() } } } /** * Remove the root node from the editor */ treemode.clear = function () { if (this.node) { this.node.hide() delete this.node } if (this.treePath) { this.treePath.reset() } } /** * Set the root node for the json editor * @param {Node} node * @private */ treemode._setRoot = function (node) { this.clear() this.node = node node.setParent(null) node.setField(this.getName(), false) delete node.index // append to the dom this.tbody.appendChild(node.getDom()) } /** * Search text in all nodes * The nodes will be expanded when the text is found one of its childs, * else it will be collapsed. Searches are case insensitive. * @param {String} text * @return {Object[]} results Array with nodes containing the search results * The result objects contains fields: * - {Node} node, * - {String} elem the dom element name where * the result is found ('field' or * 'value') */ treemode.search = function (text) { let results if (this.node) { this.content.removeChild(this.table) // Take the table offline results = this.node.search(text) this.content.appendChild(this.table) // Put the table online again } else { results = [] } return results } /** * Expand all nodes */ treemode.expandAll = function () { if (this.node) { this.content.removeChild(this.table) // Take the table offline this.node.expand() this.content.appendChild(this.table) // Put the table online again } } /** * Collapse all nodes */ treemode.collapseAll = function () { if (this.node) { this.content.removeChild(this.table) // Take the table offline this.node.collapse() this.content.appendChild(this.table) // Put the table online again } } /** * Expand/collapse a given JSON node. * @param {Object} [options] Available parameters: * {Array} [path] Path for the node to expand/collapse. * {Boolean} [isExpand] When true, expand the node. Else collapse it. * {Boolean} [recursive] When true, expand/collapse child nodes recursively. * {Boolean} [withPath] When true, expand/collapse all nodes of `path` itself. */ treemode.expand = function (options) { if (!options || !this.node) return const node = this.node.findNodeByPath(options.path) if (!node) return if (options.withPath) { for (let i = 0; i < options.path.length; i++) { const parentNode = this.node.findNodeByPath(options.path.slice(0, i)) if (parentNode) { if (options.isExpand) { parentNode.expand(false) } else { parentNode.collapse(false) } } } } if (options.isExpand) { node.expand(options.recursive) } else { node.collapse(options.recursive) } } /** * The method onChange is called whenever a field or value is changed, created, * deleted, duplicated, etc. * @param {String} action Change action. Available values: "editField", * "editValue", "changeType", "appendNode", * "removeNode", "duplicateNode", "moveNode", "expand", * "collapse". * @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. * @private */ treemode._onAction = function (action, params) { // add an action to the history if (this.history) { this.history.add(action, params) } this._onChange() } /** * Handle a change: * - Validate JSON schema * - Send a callback to the onChange listener if provided * @private */ treemode._onChange = function () { if (this.onChangeDisabled) { return } // selection can be changed after undo/redo this.selection = this.getDomSelection() // validate JSON schema (if configured) this._debouncedValidate() if (this.treePath) { const selectedNode = (this.node && this.selection) ? this.node.findNodeByInternalPath(this.selection.path) : this.multiselection ? this.multiselection.nodes[0] : undefined if (selectedNode) { this._updateTreePath(selectedNode.getNodePath()) } else { this.treePath.reset() } } // trigger the onChange callback if (this.options.onChange) { try { this.options.onChange() } catch (err) { console.error('Error in onChange callback: ', err) } } // trigger the onChangeJSON callback if (this.options.onChangeJSON) { try { this.options.onChangeJSON(this.get()) } catch (err) { console.error('Error in onChangeJSON callback: ', err) } } // trigger the onChangeText callback if (this.options.onChangeText) { try { this.options.onChangeText(this.getText()) } catch (err) { console.error('Error in onChangeText callback: ', err) } } // trigger the onClassName callback if (this.options.onClassName) { this.node.recursivelyUpdateCssClassesOnNodes() } // trigger the onNodeName callback if (this.options.onNodeName && this.node.childs) { try { this.node.recursivelyUpdateNodeName() } catch (err) { console.error('Error in onNodeName callback: ', err) } } } /** * Validate current JSON object against the configured JSON schema * Throws an exception when no JSON schema is configured */ treemode.validate = function () { const root = this.node if (!root) { // TODO: this should be redundant but is needed on mode switch return Promise.resolve([]) } const json = root.getValue() // execute JSON schema validation let schemaErrors = [] if (this.validateSchema) { const valid = this.validateSchema(json) if (!valid) { // apply all new errors schemaErrors = this.validateSchema.errors .map(error => improveSchemaError(error)) .map(function findNode (error) { return { node: root.findNode(error.dataPath), error, type: 'validation' } }) .filter(function hasNode (entry) { return entry.node != null }) } } // execute custom validation and after than merge and render all errors try { this.validationSequence++ const me = this const seq = this.validationSequence return this._validateCustom(json) .then(customValidationErrors => { // only apply when there was no other validation started whilst resolving async results if (seq === me.validationSequence) { const errorNodes = [].concat(schemaErrors, customValidationErrors || []) me._renderValidationErrors(errorNodes) if ( typeof this.options.onValidationError === 'function' && isValidationErrorChanged(errorNodes, this.lastSchemaErrors) ) { this.options.onValidationError.call(this, errorNodes) } this.lastSchemaErrors = errorNodes } return this.lastSchemaErrors }) } catch (err) { return Promise.reject(err) } } treemode._validateAndCatch = function () { this.validate().catch(err => { console.error('Error running validation:', err) }) } treemode._renderValidationErrors = function (errorNodes) { // clear all current errors if (this.errorNodes) { this.errorNodes.forEach(node => { node.setError(null) }) } // render the new errors const parentPairs = errorNodes .reduce((all, entry) => entry.node .findParents() .filter(parent => !all.some(pair => pair[0] === parent)) .map(parent => [parent, entry.node]) .concat(all), []) this.errorNodes = parentPairs .map(pair => ({ node: pair[0], child: pair[1], error: { message: pair[0].type === 'object' ? translate('containsInvalidProperties') // object : translate('containsInvalidItems') // array } })) .concat(errorNodes) .map(function setError (entry) { entry.node.setError(entry.error, entry.child) return entry.node }) } /** * Execute custom validation if configured. * * Returns a promise resolving with the custom errors (or nothing). */ treemode._validateCustom = function (json) { try { if (this.options.onValidate) { const root = this.node const customValidateResults = this.options.onValidate(json) const resultPromise = isPromise(customValidateResults) ? customValidateResults : Promise.resolve(customValidateResults) return resultPromise.then(customValidationPathErrors => { if (Array.isArray(customValidationPathErrors)) { return customValidationPathErrors .filter(error => { const valid = isValidValidationError(error) if (!valid) { console.warn('Ignoring a custom validation error with invalid structure. ' + 'Expected structure: {path: [...], message: "..."}. ' + 'Actual error:', error) } return valid }) .map(error => { let node try { node = (error && error.path) ? root.findNodeByPath(error.path) : null } catch (err) { // stay silent here, we throw a generic warning if no node is found } if (!node) { console.warn('Ignoring validation error: node not found. Path:', error.path, 'Error:', error) } return { node, error, type: 'customValidation' } }) .filter(entry => entry && entry.node && entry.error && entry.error.message) } else { return null } }) } } catch (err) { return Promise.reject(err) } return Promise.resolve(null) } /** * Refresh the rendered contents */ treemode.refresh = function () { if (this.node) { this.node.updateDom({ recurse: true }) } } /** * Start autoscrolling when given mouse position is above the top of the * editor contents, or below the bottom. * @param {Number} mouseY Absolute mouse position in pixels */ treemode.startAutoScroll = function (mouseY) { const me = this const content = this.scrollableContent const top = getAbsoluteTop(content) const height = content.clientHeight const bottom = top + height const margin = 24 const interval = 50 // ms if ((mouseY < top + margin) && content.scrollTop > 0) { this.autoScrollStep = ((top + margin) - mouseY) / 3 } else if (mouseY > bottom - margin && height + content.scrollTop < content.scrollHeight) { this.autoScrollStep = ((bottom - margin) - mouseY) / 3 } else { this.autoScrollStep = undefined } if (this.autoScrollStep) { if (!this.autoScrollTimer) { this.autoScrollTimer = setInterval(() => { if (me.autoScrollStep) { content.scrollTop -= me.autoScrollStep } else { me.stopAutoScroll() } }, interval) } } else { this.stopAutoScroll() } } /** * Stop auto scrolling. Only applicable when scrolling */ treemode.stopAutoScroll = function () { if (this.autoScrollTimer) { clearTimeout(this.autoScrollTimer) delete this.autoScrollTimer } if (this.autoScrollStep) { delete this.autoScrollStep } } /** * Set the focus to an element in the editor, set text selection, and * set scroll position. * @param {Object} selection An object containing fields: * {Element | undefined} dom The dom element * which has focus * {Range | TextRange} range A text selection * {Node[]} nodes Nodes in case of multi selection * {Number} scrollTop Scroll position */ treemode.setDomSelection = function (selection) { if (!selection) { return } if ('scrollTop' in selection && this.scrollableContent) { // TODO: animated scroll this.scrollableContent.scrollTop = selection.scrollTop } if (selection.paths) { // multi-select const me = this const nodes = selection.paths.map(path => me.node.findNodeByInternalPath(path)) this.select(nodes) } else { // find the actual DOM element where to apply the focus const node = selection.path ? this.node.findNodeByInternalPath(selection.path) : null const container = (node && selection.domName) ? node.dom[selection.domName] : null if (selection.range && container) { const range = Object.assign({}, selection.range, { container }) setSelectionOffset(range) } else if (node) { // just a fallback node.focus() } } } /** * Get the current focus * @return {Object} selection An object containing fields: * {Element | undefined} dom The dom element * which has focus * {Range | TextRange} range A text selection * {Node[]} nodes Nodes in case of multi selection * {Number} scrollTop Scroll position */ treemode.getDomSelection = function () { // find the node and field name of the current target, // so we can store the current selection in a serializable // way (internal node path and domName) const node = Node.getNodeFromTarget(this.focusTarget) const focusTarget = this.focusTarget const domName = node ? Object.keys(node.dom).find(domName => node.dom[domName] === focusTarget) : null let range = getSelectionOffset() if (range && range.container.nodeName !== 'DIV') { // filter on (editable) divs) range = null } if (range && range.container !== focusTarget) { range = null } if (range) { // we cannot rely on the current instance of the container, // we need to store the internal node path and field and // find the actual DOM field when applying the selection delete range.container } return { path: node ? node.getInternalPath() : null, domName, range, paths: this.multiselection.length > 0 ? this.multiselection.nodes.map(node => node.getInternalPath()) : null, scrollTop: this.scrollableContent ? this.scrollableContent.scrollTop : 0 } } /** * Adjust the scroll position such that given top position is shown at 1/4 * of the window height. * @param {Number} top * @param {function(boolean)} [animateCallback] Callback, executed when animation is * finished. The callback returns true * when animation is finished, or false * when not. */ treemode.scrollTo = function (top, animateCallback) { const content = this.scrollableContent if (content) { const editor = this // cancel any running animation if (editor.animateTimeout) { clearTimeout(editor.animateTimeout) delete editor.animateTimeout } if (editor.animateCallback) { editor.animateCallback(false) delete editor.animateCallback } // calculate final scroll position const height = content.clientHeight const bottom = content.scrollHeight - height const finalScrollTop = Math.min(Math.max(top - height / 4, 0), bottom) // animate towards the new scroll position const animate = () => { const scrollTop = content.scrollTop const diff = (finalScrollTop - scrollTop) if (Math.abs(diff) > 3) { content.scrollTop += diff / 3 editor.animateCallback = animateCallback editor.animateTimeout = setTimeout(animate, 50) } else { // finished if (animateCallback) { animateCallback(true) } content.scrollTop = finalScrollTop delete editor.animateTimeout delete editor.animateCallback } } animate() } else { if (animateCallback) { animateCallback(false) } } } /** * Create main frame * @private */ treemode._createFrame = function () { // create the frame this.frame = document.createElement('div') this.frame.className = 'jsoneditor jsoneditor-mode-' + this.options.mode // this.frame.setAttribute("tabindex","0"); this.container.appendChild(this.frame) this.contentOuter = document.createElement('div') this.contentOuter.className = 'jsoneditor-outer' // create one global event listener to handle all events from all nodes const editor = this function onEvent (event) { // when switching to mode "code" or "text" via the menu, some events // are still fired whilst the _onEvent methods is already removed. if (editor._onEvent) { editor._onEvent(event) } } // setting the FocusTracker on 'this.frame' to track the editor's focus event const focusTrackerConfig = { target: this.frame, onFocus: this.options.onFocus || null, onBlur: this.options.onBlur || null } this.frameFocusTracker = new FocusTracker(focusTrackerConfig) this.frame.onclick = event => { const target = event.target// || event.srcElement; onEvent(event) // prevent default submit action of buttons when editor is located // inside a form if (target.nodeName === 'BUTTON') { event.preventDefault() } } this.frame.oninput = onEvent this.frame.onchange = onEvent this.frame.onkeydown = onEvent this.frame.onkeyup = onEvent this.frame.oncut = onEvent this.frame.onpaste = onEvent this.frame.onmousedown = onEvent this.frame.onmouseup = onEvent this.frame.onmouseover = onEvent this.frame.onmouseout = onEvent // Note: focus and blur events do not propagate, therefore they defined // using an eventListener with useCapture=true // see http://www.quirksmode.org/blog/archives/2008/04/delegating_the.html addEventListener(this.frame, 'focus', onEvent, true) addEventListener(this.frame, 'blur', onEvent, true) this.frame.onfocusin = onEvent // for IE this.frame.onfocusout = onEvent // for IE if (this.options.mainMenuBar) { addClassName(this.contentOuter, 'has-main-menu-bar') // create menu this.menu = document.createElement('div') this.menu.className = 'jsoneditor-menu' this.frame.appendChild(this.menu) // create expand all button const expandAll = document.createElement('button') expandAll.type = 'button' expandAll.className = 'jsoneditor-expand-all' expandAll.title = translate('expandAll') expandAll.onclick = () => { editor.expandAll() if (typeof this.options.onExpand === 'function') { this.options.onExpand({ path: [], isExpand: true, recursive: true }) } } this.menu.appendChild(expandAll) // create collapse all button const collapseAll = document.createElement('button') collapseAll.type = 'button' collapseAll.title = translate('collapseAll') collapseAll.className = 'jsoneditor-collapse-all' collapseAll.onclick = () => { editor.collapseAll() if (typeof this.options.onExpand === 'function') { this.options.onExpand({ path: [], isExpand: false, recursive: true }) } } this.menu.appendChild(collapseAll) // create sort button if (this.options.enableSort) { const sort = document.createElement('button') sort.type = 'button' sort.className = 'jsoneditor-sort' sort.title = translate('sortTitleShort') sort.onclick = () => { editor.node.showSortModal() } this.menu.appendChild(sort) } // create transform button if (this.options.enableTransform) { const transform = document.createElement('button') transform.type = 'button' transform.title = translate('transformTitleShort') transform.className = 'jsoneditor-transform' transform.onclick = () => { editor.node.showTransformModal() } this.menu.appendChild(transform) } // create undo/redo buttons if (this.history) { // create undo button const undo = document.createElement('button') undo.type = 'button' undo.className = 'jsoneditor-undo jsoneditor-separator' undo.title = translate('undo') undo.onclick = () => { editor._onUndo() } this.menu.appendChild(undo) this.dom.undo = undo // create redo button const redo = document.createElement('button') redo.type = 'button' redo.className = 'jsoneditor-redo' redo.title = translate('redo') redo.onclick = () => { editor._onRedo() } this.menu.appendChild(redo) this.dom.redo = redo // register handler for onchange of history this.history.onChange = () => { undo.disabled = !editor.history.canUndo() redo.disabled = !editor.history.canRedo() } this.history.onChange() } // create mode box if (this.options && this.options.modes && this.options.modes.length) { const me = this this.modeSwitcher = new ModeSwitcher(this.menu, this.options.modes, this.options.mode, function onSwitch (mode) { // switch mode and restore focus try { me.setMode(mode) me.modeSwitcher.focus() } catch (err) { me._onError(err) } }) } // create search box if (this.options.search) { this.searchBox = new SearchBox(this, this.menu) } } if (this.options.navigationBar) { // create second menu row for treepath this.navBar = document.createElement('div') this.navBar.className = 'jsoneditor-navigation-bar nav-bar-empty' this.frame.appendChild(this.navBar) this.treePath = new TreePath(this.navBar, this.getPopupAnchor()) this.treePath.onSectionSelected(this._onTreePathSectionSelected.bind(this)) this.treePath.onContextMenuItemSelected(this._onTreePathMenuItemSelected.bind(this)) } } /** * Perform an undo action * @private */ treemode._onUndo = function () { if (this.history) { // undo last action this.history.undo() // fire change event this._onChange() } } /** * Perform a redo action * @private */ treemode._onRedo = function () { if (this.history) { // redo last action this.history.redo() // fire change event this._onChange() } } /** * Event handler * @param event * @private */ treemode._onEvent = function (event) { // don't process events when coming from the color picker if (Node.targetIsColorPicker(event.target)) { return } const node = Node.getNodeFromTarget(event.target) if (event.type === 'keydown') { this._onKeyDown(event) } if (node && event.type === 'focus') { this.focusTarget = event.target if (this.options.autocomplete && this.options.autocomplete.trigger === 'focus') { this._showAutoComplete(event.target) } } if (event.type === 'mousedown') { this._startDragDistance(event) } if (event.type === 'mousemove' || event.type === 'mouseup' || event.type === 'click') { this._updateDragDistance(event) } if (node && this.options && this.options.navigationBar && node && (event.type === 'keydown' || event.type === 'mousedown')) { // apply on next tick, right after the new key press is applied const me = this setTimeout(() => { me._updateTreePath(node.getNodePath()) }) } if (node && node.selected) { if (event.type === 'click') { if (event.target === node.dom.menu) { this.showContextMenu(event.target) // stop propagation (else we will open the context menu of a single node) return } // deselect a multi selection if (!event.hasMoved) { this.deselect() } } if (event.type === 'mousedown') { // drag multiple nodes Node.onDragStart(this.multiselection.nodes, event) } } else { // filter mouse events in the contents part of the editor (not the main menu) if (event.type === 'mousedown' && hasParentNode(event.target, this.content)) { this.deselect() if (node && event.target === node.dom.drag) { // drag a singe node Node.onDragStart(node, event) } else if (!node || (event.target !== node.dom.field && event.target !== node.dom.value && event.target !== node.dom.select)) { // select multiple nodes this._onMultiSelectStart(event) } } } if (node) { node.onEvent(event) } } /** * Update TreePath components * @param {Array} pathNodes list of nodes in path from root to selection * @private */ treemode._updateTreePath = function (pathNodes) { if (pathNodes && pathNodes.length) { removeClassName(this.navBar, 'nav-bar-empty') const pathObjs = [] pathNodes.forEach(node => { const pathObj = { name: getName(node), node, children: [] } if (node.childs && node.childs.length) { node.childs.forEach(childNode => { pathObj.children.push({ name: getName(childNode), node: childNode }) }) } pathObjs.push(pathObj) }) this.treePath.setPath(pathObjs) } else { addClassName(this.navBar, 'nav-bar-empty') } function getName (node) { return node.parent ? ((node.parent.type === 'array') ? node.index : node.field) : (node.field || node.type) } } /** * Callback for tree path section selection - focus the selected node in the tree * @param {Object} pathObj path object that was represents the selected section node * @private */ treemode._onTreePathSectionSelected = pathObj => { if (pathObj && pathObj.node) { pathObj.node.expandTo() pathObj.node.focus() } } /** * Callback for tree path menu item selection - rebuild the path accrding to the new selection and focus the selected node in the tree * @param {Object} pathObj path object that was represents the parent section node * @param {String} selection selected section child * @private */ treemode._onTreePathMenuItemSelected = function (pathObj, selection) { if (pathObj && pathObj.children.length) { const selectionObj = pathObj.children.find(obj => obj.name === selection) if (selectionObj && selectionObj.node) { this._updateTreePath(selectionObj.node.getNodePath()) selectionObj.node.expandTo() selectionObj.node.focus() } } } treemode._startDragDistance = function (event) { this.dragDistanceEvent = { initialTarget: event.target, initialPageX: event.pageX, initialPageY: event.pageY, dragDistance: 0, hasMoved: false } } treemode._updateDragDistance = function (event) { if (!this.dragDistanceEvent) { this._startDragDistance(event) } const diffX = event.pageX - this.dragDistanceEvent.initialPageX const diffY = event.pageY - this.dragDistanceEvent.initialPageY this.dragDistanceEvent.dragDistance = Math.sqrt(diffX * diffX + diffY * diffY) this.dragDistanceEvent.hasMoved = this.dragDistanceEvent.hasMoved || this.dragDistanceEvent.dragDistance > 10 event.dragDistance = this.dragDistanceEvent.dragDistance event.hasMoved = this.dragDistanceEvent.hasMoved return event.dragDistance } /** * Start multi selection of nodes by dragging the mouse * @param {MouseEvent} event * @private */ treemode._onMultiSelectStart = function (event) { const node = Node.getNodeFromTarget(event.target) if (this.options.mode !== 'tree' || this.options.onEditable !== undefined) { // dragging not allowed in modes 'view' and 'form' // TODO: allow multiselection of items when option onEditable is specified return } this.multiselection = { start: node || null, end: null, nodes: [] } this._startDragDistance(event) const editor = this if (!this.mousemove) { this.mousemove = addEventListener(event.view, 'mousemove', event => { editor._onMultiSelect(event) }) } if (!this.mouseup) { this.mouseup = addEventListener(event.view, 'mouseup', event => { editor._onMultiSelectEnd(event) }) } event.preventDefault() } /** * Multiselect nodes by dragging * @param {MouseEvent} event * @private */ treemode._onMultiSelect = function (event) { event.preventDefault() this._updateDragDistance(event) if (!event.hasMoved) { return } const node = Node.getNodeFromTarget(event.target) if (node) { if (this.multiselection.start == null) { this.multiselection.start = node } this.multiselection.end = node } // deselect previous selection this.deselect() // find the selected nodes in the range from first to last const start = this.multiselection.start const end = this.multiselection.end || this.multiselection.start if (start && end) { // find the top level childs, all having the same parent this.multiselection.nodes = this._findTopLevelNodes(start, end) if (this.multiselection.nodes && this.multiselection.nodes.length) { const firstNode = this.multiselection.nodes[0] if (this.multiselection.start === firstNode || this.multiselection.start.isDescendantOf(firstNode)) { this.multiselection.direction = 'down' } else { this.multiselection.direction = 'up' } } this.select(this.multiselection.nodes) } } /** * End of multiselect nodes by dragging * @param {MouseEvent} event * @private */ treemode._onMultiSelectEnd = function (event) { // set focus to the context menu button of the first node const firstNode = this.multiselection.nodes[0] if (firstNode && firstNode.dom.menu) { firstNode.dom.menu.focus() } this.multiselection.start = null this.multiselection.end = null // cleanup global event listeners if (this.mousemove) { removeEventListener(event.view, 'mousemove', this.mousemove) delete this.mousemove } if (this.mouseup) { removeEventListener(event.view, 'mouseup', this.mouseup) delete this.mouseup } } /** * deselect currently selected nodes * @param {boolean} [clearStartAndEnd=false] If true, the `start` and `end` * state is cleared too. */ treemode.deselect = function (clearStartAndEnd) { const selectionChanged = !!this.multiselection.nodes.length this.multiselection.nodes.forEach(node => { node.setSelected(false) }) this.multiselection.nodes = [] if (clearStartAndEnd) { this.multiselection.start = null this.multiselection.end = null } if (selectionChanged) { if (this._selectionChangedHandler) { this._selectionChangedHandler() } } } /** * select nodes * @param {Node[] | Node} nodes */ treemode.select = function (nodes) { if (!Array.isArray(nodes)) { return this.select([nodes]) } if (nodes) { this.deselect() this.multiselection.nodes = nodes.slice(0) const first = nodes[0] nodes.forEach(node => { node.expandPathToNode() node.setSelected(true, node === first) }) if (this._selectionChangedHandler) { const selection = this.getSelection() this._selectionChangedHandler(selection.start, selection.end) } } } /** * From two arbitrary selected nodes, find their shared parent node. * From that parent node, select the two child nodes in the brances going to * nodes `start` and `end`, and select all childs in between. * @param {Node} start * @param {Node} end * @return {Array.} Returns an ordered list with child nodes * @private */ treemode._findTopLevelNodes = (start, end) => { const startPath = start.getNodePath() const endPath = end.getNodePath() let i = 0 while (i < startPath.length && startPath[i] === endPath[i]) { i++ } let root = startPath[i - 1] let startChild = startPath[i] let endChild = endPath[i] if (!startChild || !endChild) { if (root.parent) { // startChild is a parent of endChild or vice versa startChild = root endChild = root root = root.parent } else { // we have selected the root node (which doesn't have a parent) startChild = root.childs[0] endChild = root.childs[root.childs.length - 1] } } if (root && startChild && endChild) { const startIndex = root.childs.indexOf(startChild) const endIndex = root.childs.indexOf(endChild) const firstIndex = Math.min(startIndex, endIndex) const lastIndex = Math.max(startIndex, endIndex) return root.childs.slice(firstIndex, lastIndex + 1) } else { return [] } } /** * Show autocomplete menu * @param {HTMLElement} element * @private */ treemode._showAutoComplete = function (element) { const node = Node.getNodeFromTarget(element) let jsonElementType = '' if (element.className.indexOf('jsoneditor-value') >= 0) jsonElementType = 'value' if (element.className.indexOf('jsoneditor-field') >= 0) jsonElementType = 'field' if (jsonElementType === '') { // Unknown element field. Could be a button or something else return } const self = this setTimeout(() => { if (node && (self.options.autocomplete.trigger === 'focus' || element.innerText.length > 0)) { const result = self.options.autocomplete.getOptions(element.innerText, node.getPath(), jsonElementType, node.editor) if (result === null) { self.autocomplete.hideDropDown() } else if (typeof result.then === 'function') { // probably a promise result .then(obj => { if (obj === null) { self.autocomplete.hideDropDown() } else if (obj.options) { self.autocomplete.show(element, obj.startFrom, obj.options) } else { self.autocomplete.show(element, 0, obj) } }) .catch(err => { console.error(err) }) } else { // definitely not a promise if (result.options) { self.autocomplete.show(element, result.startFrom, result.options) } else { self.autocomplete.show(element, 0, result) } } } else { self.autocomplete.hideDropDown() } }, 50) } /** * Event handler for keydown. Handles shortcut keys * @param {Event} event * @private */ treemode._onKeyDown = function (event) { const keynum = event.which || event.keyCode const altKey = event.altKey const ctrlKey = event.ctrlKey const metaKey = event.metaKey const shiftKey = event.shiftKey let handled = false const currentTarget = this.focusTarget if (keynum === 9) { // Tab or Shift+Tab const me = this setTimeout(() => { /* - Checking for change in focusTarget - Without the check, pressing tab after reaching the final DOM element in the editor will set the focus back to it than passing focus outside the editor */ if (me.focusTarget !== currentTarget) { // select all text when moving focus to an editable div selectContentEditable(me.focusTarget) } }, 0) } if (this.searchBox) { if (ctrlKey && keynum === 70) { // Ctrl+F this.searchBox.dom.search.focus() this.searchBox.dom.search.select() handled = true } else if (keynum === 114 || (ctrlKey && keynum === 71)) { // F3 or Ctrl+G const focus = true if (!shiftKey) { // select next search result (F3 or Ctrl+G) this.searchBox.next(focus) } else { // select previous search result (Shift+F3 or Ctrl+Shift+G) this.searchBox.previous(focus) } handled = true } } if (this.history) { if (ctrlKey && !shiftKey && keynum === 90) { // Ctrl+Z // undo this._onUndo() handled = true } else if (ctrlKey && shiftKey && keynum === 90) { // Ctrl+Shift+Z // redo this._onRedo() handled = true } } if ((this.options.autocomplete) && (!handled)) { if (!ctrlKey && !altKey && !metaKey && (event.key.length === 1 || keynum === 8 || keynum === 46)) { handled = false // Activate autocomplete this._showAutoComplete(event.target) } } if (handled) { event.preventDefault() event.stopPropagation() } } /** * Create main table * @private */ treemode._createTable = function () { if (this.options.navigationBar) { addClassName(this.contentOuter, 'has-nav-bar') } this.scrollableContent = document.createElement('div') this.scrollableContent.className = 'jsoneditor-tree' this.contentOuter.appendChild(this.scrollableContent) // the jsoneditor-tree-inner div with bottom padding is here to // keep space for the action menu dropdown. It's created as a // separate div instead of using scrollableContent to work around // and issue in the Chrome browser showing scrollable contents outside of the div // see https://github.com/josdejong/jsoneditor/issues/557 this.content = document.createElement('div') this.content.className = 'jsoneditor-tree-inner' this.scrollableContent.appendChild(this.content) this.table = document.createElement('table') this.table.className = 'jsoneditor-tree' this.content.appendChild(this.table) // create colgroup where the first two columns don't have a fixed // width, and the edit columns do have a fixed width let col this.colgroupContent = document.createElement('colgroup') if (this.options.mode === 'tree') { col = document.createElement('col') col.width = '24px' this.colgroupContent.appendChild(col) } col = document.createElement('col') col.width = '24px' this.colgroupContent.appendChild(col) col = document.createElement('col') this.colgroupContent.appendChild(col) this.table.appendChild(this.colgroupContent) this.tbody = document.createElement('tbody') this.table.appendChild(this.tbody) this.frame.appendChild(this.contentOuter) } /** * Show a contextmenu for this node. * Used for multiselection * @param {HTMLElement} anchor Anchor element to attach the context menu to. * @param {function} [onClose] Callback method called when the context menu * is being closed. */ treemode.showContextMenu = function (anchor, onClose) { let items = [] const selectedNodes = this.multiselection.nodes.slice() // create duplicate button items.push({ text: translate('duplicateText'), title: translate('duplicateTitle'), className: 'jsoneditor-duplicate', click: function () { Node.onDuplicate(selectedNodes) } }) // create remove button items.push({ text: translate('remove'), title: translate('removeTitle'), className: 'jsoneditor-remove', click: function () { Node.onRemove(selectedNodes) } }) if (this.options.onCreateMenu) { const paths = selectedNodes.map(node => node.getPath()) items = this.options.onCreateMenu(items, { type: 'multiple', path: paths[0], paths }) } const menu = new ContextMenu(items, { close: onClose }) menu.show(anchor, this.getPopupAnchor()) } treemode.getPopupAnchor = function () { return this.options.popupAnchor || this.frame } /** * Get current selected nodes * @return {{start:SerializableNode, end: SerializableNode}} */ treemode.getSelection = function () { const selection = { start: null, end: null } if (this.multiselection.nodes && this.multiselection.nodes.length) { if (this.multiselection.nodes.length) { const selection1 = this.multiselection.nodes[0] const selection2 = this.multiselection.nodes[this.multiselection.nodes.length - 1] if (this.multiselection.direction === 'down') { selection.start = selection1.serialize() selection.end = selection2.serialize() } else { selection.start = selection2.serialize() selection.end = selection1.serialize() } } } return selection } /** * Callback registration for selection change * @param {selectionCallback} callback * * @callback selectionCallback */ treemode.onSelectionChange = function (callback) { if (typeof callback === 'function') { this._selectionChangedHandler = debounce(callback, this.DEBOUNCE_INTERVAL) } } /** * Select range of nodes. * For selecting single node send only the start parameter * For clear the selection do not send any parameter * If the nodes are not from the same level the first common parent will be selected * @param {{path: Array.}} start object contains the path for selection start * @param {{path: Array.}} end object contains the path for selection end */ treemode.setSelection = function (start, end) { // check for old usage if (start && start.dom && start.range) { console.warn('setSelection/getSelection usage for text selection is deprecated and should not be used, see documentation for supported selection options') this.setDomSelection(start) } const nodes = this._getNodeInstancesByRange(start, end) nodes.forEach(node => { node.expandTo() }) this.select(nodes) } /** * Returns a set of Nodes according to a range of selection * @param {{path: Array.}} start object contains the path for range start * @param {{path: Array.}=} end object contains the path for range end * @return {Array.} Node instances on the given range * @private */ treemode._getNodeInstancesByRange = function (start, end) { let startNode, endNode if (start && start.path) { startNode = this.node.findNodeByPath(start.path) if (end && end.path) { endNode = this.node.findNodeByPath(end.path) } } let nodes = [] if (startNode instanceof Node) { if (endNode instanceof Node && endNode !== startNode) { if (startNode.parent === endNode.parent) { if (startNode.getIndex() < endNode.getIndex()) { start = startNode end = endNode } else { start = endNode end = startNode } let current = start nodes.push(current) do { current = current.nextSibling() nodes.push(current) } while (current && current !== end) } else { nodes = this._findTopLevelNodes(startNode, endNode) } } else { nodes.push(startNode) } } return nodes } treemode.getNodesByRange = function (start, end) { const nodes = this._getNodeInstancesByRange(start, end) const serializableNodes = [] nodes.forEach(node => { serializableNodes.push(node.serialize()) }) return serializableNodes } // define modes export const treeModeMixins = [ { mode: 'tree', mixin: treemode, data: 'json' }, { mode: 'view', mixin: treemode, data: 'json' }, { mode: 'form', mixin: treemode, data: 'json' } ] ================================================ FILE: src/js/tryRequireAjv.js ================================================ exports.tryRequireAjv = function () { try { return require('ajv') } catch (err) { // no problem... when we need Ajv we will throw a neat exception } } ================================================ FILE: src/js/tryRequireThemeJsonEditor.js ================================================ exports.tryRequireThemeJsonEditor = function () { try { require('./ace/theme-jsoneditor') } catch (err) { console.error(err) } } ================================================ FILE: src/js/types.js ================================================ /** * @typedef {object} QueryOptions * @property {FilterOptions} [filter] * @property {SortOptions} [sort] * @property {ProjectionOptions} [projection] */ /** * @typedef {object} FilterOptions * @property {string} field * @property {string} relation Can be '==', '<', etc * @property {string} value */ /** * @typedef {object} SortOptions * @property {string} field * @property {string} direction Can be 'asc' or 'desc' */ /** * @typedef {object} ProjectionOptions * @property {string[]} fields */ ================================================ FILE: src/js/util.js ================================================ 'use strict' import './polyfills' import naturalSort from 'javascript-natural-sort' import { jsonrepair } from 'jsonrepair' import jsonlint from './assets/jsonlint/jsonlint' import jsonMap from 'json-source-map' import { translate } from './i18n' const MAX_ITEMS_FIELDS_COLLECTION = 10000 const YEAR_2000 = 946684800000 /** * Parse JSON using the parser built-in in the browser. * On exception, the jsonString is validated and a detailed error is thrown. * @param {String} jsonString * @return {JSON} json */ export function parse (jsonString) { try { return JSON.parse(jsonString) } catch (err) { // try to throw a more detailed error message using validate validate(jsonString) // rethrow the original error throw err } } /** * Try to fix the JSON string. If not successful, return the original string * @param {string} jsonString */ export function tryJsonRepair (jsonString) { try { return jsonrepair(jsonString) } catch (err) { // repair was not successful, return original text return jsonString } } /** * Escape unicode characters. * For example input '\u2661' (length 1) will output '\\u2661' (length 5). * @param {string} text * @return {string} */ export function escapeUnicodeChars ( // see https://www.wikiwand.com/en/UTF-16 text ) { return ( // note: we leave surrogate pairs as two individual chars, // as JSON doesn't interpret them as a single unicode char. text.replace( /[\u007F-\uFFFF]/g, c => '\\u' + ('0000' + c.charCodeAt(0).toString(16)).slice(-4) ) ) } /** * Validate a string containing a JSON object * This method uses JSONLint to validate the String. If JSONLint is not * available, the built-in JSON parser of the browser is used. * @param {String} jsonString String with an (invalid) JSON object * @throws Error */ export function validate (jsonString) { if (typeof (jsonlint) !== 'undefined') { jsonlint.parse(jsonString) } else { JSON.parse(jsonString) } } /** * Extend object a with the properties of object b * @param {Object} a * @param {Object} b * @return {Object} a */ export function extend (a, b) { for (const prop in b) { if (hasOwnProperty(b, prop)) { a[prop] = b[prop] } } return a } /** * Remove all properties from object a * @param {Object} a * @return {Object} a */ export function clear (a) { for (const prop in a) { if (hasOwnProperty(a, prop)) { delete a[prop] } } return a } /** * Get the type of an object * @param {*} object * @return {String} type */ export function getType (object) { if (object === null) { return 'null' } if (object === undefined) { return 'undefined' } if ((object instanceof Number) || (typeof object === 'number')) { return 'number' } if ((object instanceof String) || (typeof object === 'string')) { return 'string' } if ((object instanceof Boolean) || (typeof object === 'boolean')) { return 'boolean' } if (object instanceof RegExp) { return 'regexp' } if (isArray(object)) { return 'array' } return 'object' } /** * Test whether a text contains a url (matches when a string starts * with 'http://*' or 'https://*' and has no whitespace characters) * @param {String} text */ const isUrlRegex = /^https?:\/\/\S+$/ export function isUrl (text) { return (typeof text === 'string' || text instanceof String) && isUrlRegex.test(text) } /** * Tes whether given object is an Array * @param {*} obj * @returns {boolean} returns true when obj is an array */ export function isArray (obj) { return Object.prototype.toString.call(obj) === '[object Array]' } /** * Gets a DOM element's Window. This is normally just the global `window` * variable, but if we opened a child window, it may be different. * @param {HTMLElement} element * @return {Window} */ export function getWindow (element) { return element.ownerDocument.defaultView } /** * Retrieve the absolute left value of a DOM element * @param {Element} elem A dom element, for example a div * @return {Number} left The absolute left position of this element * in the browser page. */ export function getAbsoluteLeft (elem) { const rect = elem.getBoundingClientRect() return rect.left + window.pageXOffset || document.scrollLeft || 0 } /** * Retrieve the absolute top value of a DOM element * @param {Element} elem A dom element, for example a div * @return {Number} top The absolute top position of this element * in the browser page. */ export function getAbsoluteTop (elem) { const rect = elem.getBoundingClientRect() return rect.top + window.pageYOffset || document.scrollTop || 0 } /** * add a className to the given elements style * @param {Element} elem * @param {String} className */ export function addClassName (elem, className) { const classes = elem.className.split(' ') if (classes.indexOf(className) === -1) { classes.push(className) // add the class to the array elem.className = classes.join(' ') } } /** * remove all classes from the given elements style * @param {Element} elem */ export function removeAllClassNames (elem) { elem.className = '' } /** * add a className to the given elements style * @param {Element} elem * @param {String} className */ export function removeClassName (elem, className) { const classes = elem.className.split(' ') const index = classes.indexOf(className) if (index !== -1) { classes.splice(index, 1) // remove the class from the array elem.className = classes.join(' ') } } /** * Strip the formatting from the contents of a div * the formatting from the div itself is not stripped, only from its childs. * @param {Element} divElement */ export function stripFormatting (divElement) { const childs = divElement.childNodes for (let i = 0, iMax = childs.length; i < iMax; i++) { const child = childs[i] // remove the style if (child.style) { // TODO: test if child.attributes does contain style child.removeAttribute('style') } // remove all attributes const attributes = child.attributes if (attributes) { for (let j = attributes.length - 1; j >= 0; j--) { const attribute = attributes[j] if (attribute.specified === true) { child.removeAttribute(attribute.name) } } } // recursively strip childs stripFormatting(child) } } /** * Set focus to the end of an editable div * code from Nico Burns * http://stackoverflow.com/users/140293/nico-burns * http://stackoverflow.com/questions/1125292/how-to-move-cursor-to-end-of-contenteditable-entity * @param {Element} contentEditableElement A content editable div */ export function setEndOfContentEditable (contentEditableElement) { let range, selection if (document.createRange) { 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 } } /** * Select all text of a content editable div. * http://stackoverflow.com/a/3806004/1262753 * @param {Element} contentEditableElement A content editable div */ export function selectContentEditable (contentEditableElement) { if (!contentEditableElement || contentEditableElement.nodeName !== 'DIV') { return } let sel, range if (window.getSelection && document.createRange) { range = document.createRange() range.selectNodeContents(contentEditableElement) sel = window.getSelection() sel.removeAllRanges() sel.addRange(range) } } /** * Get text selection * http://stackoverflow.com/questions/4687808/contenteditable-selected-text-save-and-restore * @return {Range | TextRange | null} range */ export function getSelection () { if (window.getSelection) { const sel = window.getSelection() if (sel.getRangeAt && sel.rangeCount) { return sel.getRangeAt(0) } } return null } /** * Set text selection * http://stackoverflow.com/questions/4687808/contenteditable-selected-text-save-and-restore * @param {Range | TextRange | null} range */ export function setSelection (range) { if (range) { if (window.getSelection) { const sel = window.getSelection() sel.removeAllRanges() sel.addRange(range) } } } /** * Get selected text range * @return {Object} params object containing parameters: * {Number} startOffset * {Number} endOffset * {Element} container HTML element holding the * selected text element * Returns null if no text selection is found */ export function getSelectionOffset () { const range = getSelection() if (range && 'startOffset' in range && 'endOffset' in range && range.startContainer && (range.startContainer === range.endContainer)) { return { startOffset: range.startOffset, endOffset: range.endOffset, container: range.startContainer.parentNode } } return null } /** * Set selected text range in given element * @param {Object} params An object containing: * {Element} container * {Number} startOffset * {Number} endOffset */ export function setSelectionOffset (params) { if (document.createRange && window.getSelection) { const selection = window.getSelection() if (selection) { const range = document.createRange() if (!params.container.firstChild) { params.container.appendChild(document.createTextNode('')) } // TODO: do not suppose that the first child of the container is a textnode, // but recursively find the textnodes range.setStart(params.container.firstChild, params.startOffset) range.setEnd(params.container.firstChild, params.endOffset) setSelection(range) } } } /** * Get the inner text of an HTML element (for example a div element) * @param {Element} element * @param {Object} [buffer] * @return {String} innerText */ export function getInnerText (element, buffer) { const first = (buffer === undefined) if (first) { buffer = { _text: '', flush: function () { const text = this._text this._text = '' return text }, set: function (text) { this._text = text } } } // text node if (element.nodeValue) { // remove return characters and the whitespaces surrounding those return characters const trimmedValue = removeReturnsAndSurroundingWhitespace(element.nodeValue) if (trimmedValue !== '') { return buffer.flush() + trimmedValue } else { // ignore empty text return '' } } // divs or other HTML elements if (element.hasChildNodes()) { const childNodes = element.childNodes let innerText = '' for (let i = 0, iMax = childNodes.length; i < iMax; i++) { const child = childNodes[i] if (child.nodeName === 'DIV' || child.nodeName === 'P') { const prevChild = childNodes[i - 1] const prevName = prevChild ? prevChild.nodeName : undefined if (prevName && prevName !== 'DIV' && prevName !== 'P' && prevName !== 'BR') { if (innerText !== '') { innerText += '\n' } buffer.flush() } innerText += getInnerText(child, buffer) buffer.set('\n') } else if (child.nodeName === 'BR') { innerText += buffer.flush() buffer.set('\n') } else { innerText += getInnerText(child, buffer) } } return innerText } // br or unknown return '' } // regular expression matching one or multiple return characters with all their // enclosing white spaces export function removeReturnsAndSurroundingWhitespace (text) { return text.replace(/(\b|^)\s*(\b|$)/g, (match) => { return /\n/.exec(match) ? '' : match }) } /** * Test whether an element has the provided parent node somewhere up the node tree. * @param {Element} elem * @param {Element} parent * @return {boolean} */ export function hasParentNode (elem, parent) { let e = elem ? elem.parentNode : undefined while (e) { if (e === parent) { return true } e = e.parentNode } return false } /** * Returns the version of Internet Explorer or a -1 * (indicating the use of another browser). * Source: http://msdn.microsoft.com/en-us/library/ms537509(v=vs.85).aspx * @return {Number} Internet Explorer version, or -1 in case of an other browser */ export function getInternetExplorerVersion () { if (_ieVersion === -1) { let rv = -1 // Return value assumes failure. if (typeof navigator !== 'undefined' && navigator.appName === 'Microsoft Internet Explorer') { const ua = navigator.userAgent const re = /MSIE ([0-9]+[.0-9]+)/ if (re.exec(ua) != null) { rv = parseFloat(RegExp.$1) } } _ieVersion = rv } return _ieVersion } /** * cached internet explorer version * @type {Number} * @private */ let _ieVersion = -1 /** * Test whether the current browser is Firefox * @returns {boolean} isFirefox */ export function isFirefox () { return (typeof navigator !== 'undefined' && navigator.userAgent.indexOf('Firefox') !== -1) } /** * Add an event listener. Works for all browsers * @param {Element} element An html element * @param {string} action The action, for example "click", * without the prefix "on" * @param {function} listener The callback function to be executed * @param {boolean} [useCapture] false by default * @return {function} the created event listener */ export function addEventListener (element, action, listener, useCapture) { if (element.addEventListener) { if (useCapture === undefined) { useCapture = false } if (action === 'mousewheel' && isFirefox()) { action = 'DOMMouseScroll' // For Firefox } element.addEventListener(action, listener, useCapture) return listener } else if (element.attachEvent) { // Old IE browsers const f = () => listener.call(element, window.event) element.attachEvent('on' + action, f) return f } } /** * Remove an event listener from an element * @param {Element} element An html dom element * @param {string} action The name of the event, for example "mousedown" * @param {function} listener The listener function * @param {boolean} [useCapture] false by default */ export function removeEventListener (element, action, listener, useCapture) { if (element.removeEventListener) { if (useCapture === undefined) { useCapture = false } if (action === 'mousewheel' && isFirefox()) { action = 'DOMMouseScroll' // For Firefox } element.removeEventListener(action, listener, useCapture) } else if (element.detachEvent) { // Old IE browsers element.detachEvent('on' + action, listener) } } /** * Test if an element is a child of a parent element. * @param {Element} elem * @param {Element} parent * @return {boolean} returns true if elem is a child of the parent */ export function isChildOf (elem, parent) { let e = elem.parentNode while (e) { if (e === parent) { return true } e = e.parentNode } return false } /** * Parse a JSON path like '.items[3].name' into an array * @param {string} jsonPath * @return {Array} */ export function parsePath (jsonPath) { const path = [] let i = 0 function parseProperty () { let prop = '' while (jsonPath[i] !== undefined && /[\w$]/.test(jsonPath[i])) { prop += jsonPath[i] i++ } if (prop === '') { throw new Error('Invalid JSON path: property name expected at index ' + i) } return prop } function parseIndex (end) { let name = '' while (jsonPath[i] !== undefined && jsonPath[i] !== end) { name += jsonPath[i] i++ } if (jsonPath[i] !== end) { throw new Error('Invalid JSON path: unexpected end, character ' + end + ' expected') } return name } while (jsonPath[i] !== undefined) { if (jsonPath[i] === '.') { i++ path.push(parseProperty()) } else if (jsonPath[i] === '[') { i++ if (jsonPath[i] === '\'' || jsonPath[i] === '"') { const end = jsonPath[i] i++ path.push(parseIndex(end)) if (jsonPath[i] !== end) { throw new Error('Invalid JSON path: closing quote \' expected at index ' + i) } i++ } else { let index = parseIndex(']').trim() if (index.length === 0) { throw new Error('Invalid JSON path: array value expected at index ' + i) } // Coerce numeric indices to numbers, but ignore star index = index === '*' ? index : JSON.parse(index) path.push(index) } if (jsonPath[i] !== ']') { throw new Error('Invalid JSON path: closing bracket ] expected at index ' + i) } i++ } else { throw new Error('Invalid JSON path: unexpected character "' + jsonPath[i] + '" at index ' + i) } } return path } /** * Stringify an array with a path in a JSON path like '.items[3].name' * @param {Array.} path * @returns {string} */ export function stringifyPath (path) { return path .map(p => { if (typeof p === 'number') { return ('[' + p + ']') } else if (typeof p === 'string' && p.match(/^[A-Za-z0-9_$]+$/)) { return '.' + p } else { return '["' + p + '"]' } }) .join('') } /** * Improve the error message of a JSON schema error * @param {Object} error * @return {Object} The error */ export function improveSchemaError (error) { if (error.keyword === 'enum' && Array.isArray(error.schema)) { let enums = error.schema if (enums) { enums = enums.map(value => JSON.stringify(value)) if (enums.length > 5) { const more = ['(' + (enums.length - 5) + ' more...)'] enums = enums.slice(0, 5) enums.push(more) } error.message = 'should be equal to one of: ' + enums.join(', ') } } if (error.keyword === 'additionalProperties') { error.message = 'should NOT have additional property: ' + error.params.additionalProperty } return error } /** * Test whether something is a Promise * @param {*} object * @returns {boolean} Returns true when object is a promise, false otherwise */ export function isPromise (object) { return object && typeof object.then === 'function' && typeof object.catch === 'function' } /** * Test whether a custom validation error has the correct structure * @param {*} validationError The error to be checked. * @returns {boolean} Returns true if the structure is ok, false otherwise */ export function isValidValidationError (validationError) { return typeof validationError === 'object' && Array.isArray(validationError.path) && typeof validationError.message === 'string' } /** * Test whether the child rect fits completely inside the parent rect. * @param {ClientRect} parent * @param {ClientRect} child * @param {number} margin */ export function insideRect (parent, child, margin) { const _margin = margin !== undefined ? margin : 0 return child.left - _margin >= parent.left && child.right + _margin <= parent.right && child.top - _margin >= parent.top && child.bottom + _margin <= parent.bottom } /** * Returns a function, that, as long as it continues to be invoked, will not * be triggered. The function will be called after it stops being called for * N milliseconds. * * Source: https://davidwalsh.name/javascript-debounce-function * * @param {function} func * @param {number} wait Number in milliseconds * @param {boolean} [immediate=false] If `immediate` is passed, trigger the * function on the leading edge, instead * of the trailing. * @return {function} Return the debounced function */ export function debounce (func, wait, immediate) { let timeout return function () { const context = this; const args = arguments const later = () => { timeout = null if (!immediate) func.apply(context, args) } const callNow = immediate && !timeout clearTimeout(timeout) timeout = setTimeout(later, wait) if (callNow) func.apply(context, args) } } /** * Determines the difference between two texts. * Can only detect one removed or inserted block of characters. * @param {string} oldText * @param {string} newText * @return {{start: number, end: number}} Returns the start and end * of the changed part in newText. */ export function textDiff (oldText, newText) { const len = newText.length let start = 0 let oldEnd = oldText.length let newEnd = newText.length while (newText.charAt(start) === oldText.charAt(start) && start < len) { start++ } while (newText.charAt(newEnd - 1) === oldText.charAt(oldEnd - 1) && newEnd > start && oldEnd > 0) { newEnd-- oldEnd-- } return { start, end: newEnd } } /** * Return an object with the selection range or cursor position (if both have the same value) * Support also old browsers (IE8-) * Source: http://ourcodeworld.com/articles/read/282/how-to-get-the-current-cursor-position-and-selection-within-a-text-input-or-textarea-in-javascript * @param {DOMElement} el A dom element of a textarea or input text. * @return {Object} reference Object with 2 properties (start and end) with the identifier of the location of the cursor and selected text. **/ export function getInputSelection (el) { let startIndex = 0; let endIndex = 0; let normalizedValue; let range; let textInputRange; let len; let endRange if (typeof el.selectionStart === 'number' && typeof el.selectionEnd === 'number') { startIndex = el.selectionStart endIndex = el.selectionEnd } else { range = document.selection.createRange() if (range && range.parentElement() === el) { len = el.value.length normalizedValue = el.value.replace(/\r\n/g, '\n') // Create a working TextRange that lives only in the input textInputRange = el.createTextRange() textInputRange.moveToBookmark(range.getBookmark()) // Check if the startIndex and endIndex of the selection are at the very end // of the input, since moveStart/moveEnd doesn't return what we want // in those cases endRange = el.createTextRange() endRange.collapse(false) if (textInputRange.compareEndPoints('StartToEnd', endRange) > -1) { startIndex = endIndex = len } else { startIndex = -textInputRange.moveStart('character', -len) startIndex += normalizedValue.slice(0, startIndex).split('\n').length - 1 if (textInputRange.compareEndPoints('EndToEnd', endRange) > -1) { endIndex = len } else { endIndex = -textInputRange.moveEnd('character', -len) endIndex += normalizedValue.slice(0, endIndex).split('\n').length - 1 } } } } return { startIndex, endIndex, start: _positionForIndex(startIndex), end: _positionForIndex(endIndex) } /** * Returns textarea row and column position for certain index * @param {Number} index text index * @returns {{row: Number, column: Number}} */ function _positionForIndex (index) { const textTillIndex = el.value.substring(0, index) const row = (textTillIndex.match(/\n/g) || []).length + 1 const col = textTillIndex.length - textTillIndex.lastIndexOf('\n') return { row, column: col } } } /** * Returns the index for certain position in text element * @param {DOMElement} el A dom element of a textarea or input text. * @param {Number} row row value, > 0, if exceeds rows number - last row will be returned * @param {Number} column column value, > 0, if exceeds column length - end of column will be returned * @returns {Number} index of position in text, -1 if not found */ export function getIndexForPosition (el, row, column) { const text = el.value || '' if (row > 0 && column > 0) { const rows = text.split('\n', row) row = Math.min(rows.length, row) column = Math.min(rows[row - 1].length, column - 1) const columnCount = (row === 1 ? column : column + 1) // count new line on multiple rows return rows.slice(0, row - 1).join('\n').length + columnCount } return -1 } /** * Returns location of json paths in certain json string * @param {String} text json string * @param {Array} paths array of json paths * @returns {Array<{path: String, line: Number, row: Number}>} */ export function getPositionForPath (text, paths) { const result = [] let jsmap if (!paths || !paths.length) { return result } try { jsmap = jsonMap.parse(text) } catch (err) { return result } paths.forEach(path => { const pathArr = parsePath(path) const pointerName = compileJSONPointer(pathArr) const pointer = jsmap.pointers[pointerName] if (pointer) { result.push({ path, line: pointer.key ? pointer.key.line : (pointer.value ? pointer.value.line : 0), column: pointer.key ? pointer.key.column : (pointer.value ? pointer.value.column : 0) }) } }) return result } /** * Compile a JSON Pointer * WARNING: this is an incomplete implementation * @param {Array.} path * @return {string} */ export function compileJSONPointer (path) { return path .map(p => ('/' + String(p) .replace(/~/g, '~0') .replace(/\//g, '~1') )) .join('') } /** * Get the applied color given a color name or code * Source: https://stackoverflow.com/questions/6386090/validating-css-color-names/33184805 * @param {string} color * @returns {string | null} returns the color if the input is a valid * color, and returns null otherwise. Example output: * 'rgba(255,0,0,0.7)' or 'rgb(255,0,0)' */ export function getColorCSS (color) { const ele = document.createElement('div') ele.style.color = color return ele.style.color.split(/\s+/).join('').toLowerCase() || null } /** * Test if a string contains a valid color name or code. * @param {string} color * @returns {boolean} returns true if a valid color, false otherwise */ export function isValidColor (color) { return !!getColorCSS(color) } /** * Make a tooltip for a field based on the field's schema. * @param {object} schema JSON schema * @param {string} [locale] Locale code (for example, zh-CN) * @returns {string} Field tooltip, may be empty string if all relevant schema properties are missing */ export function makeFieldTooltip (schema, locale) { if (!schema) { return '' } let tooltip = '' if (schema.title) { tooltip += schema.title } if (schema.description) { if (tooltip.length > 0) { tooltip += '\n' } tooltip += schema.description } if (schema.default) { if (tooltip.length > 0) { tooltip += '\n\n' } tooltip += translate('default', undefined, locale) + '\n' tooltip += JSON.stringify(schema.default, null, 2) } if (Array.isArray(schema.examples) && schema.examples.length > 0) { if (tooltip.length > 0) { tooltip += '\n\n' } tooltip += translate('examples', undefined, locale) + '\n' schema.examples.forEach((example, index) => { tooltip += JSON.stringify(example, null, 2) if (index !== schema.examples.length - 1) { tooltip += '\n' } }) } return tooltip } /** * Get a nested property from an object. * Returns undefined when the property does not exist. * @param {Object} object * @param {string[]} path * @return {*} */ export function get (object, path) { let value = object for (let i = 0; i < path.length && value !== undefined && value !== null; i++) { value = value[path[i]] } return value } /** * Find a unique name. Suffix the name with ' (copy)', '(copy 2)', etc * until a unique name is found * @param {string} name * @param {Array} existingPropNames Array with existing prop names */ export function findUniqueName (name, existingPropNames) { if (existingPropNames.indexOf(name) === -1) { return name } const strippedName = name.replace(/ \(copy( \d+)?\)$/, '') let validName = strippedName let i = 1 while (existingPropNames.indexOf(validName) !== -1) { const copy = 'copy' + (i > 1 ? (' ' + i) : '') validName = strippedName + ' (' + copy + ')' i++ } return validName } /** * Get the child paths of an array * @param {JSON} json * @param {boolean} [includeObjects=false] If true, object and array paths are returned as well * @return {string[]} */ export function getChildPaths (json, includeObjects) { const pathsMap = {} function getObjectChildPaths (json, pathsMap, rootPath, includeObjects) { const isValue = !Array.isArray(json) && !isObject(json) if (isValue || includeObjects) { pathsMap[rootPath || ''] = true } if (isObject(json)) { Object.keys(json).forEach(field => { getObjectChildPaths(json[field], pathsMap, rootPath + '.' + field, includeObjects) }) } } if (Array.isArray(json)) { const max = Math.min(json.length, MAX_ITEMS_FIELDS_COLLECTION) for (let i = 0; i < max; i++) { const item = json[i] getObjectChildPaths(item, pathsMap, '', includeObjects) } } else { pathsMap[''] = true } return Object.keys(pathsMap).sort() } /** * Sort object keys using natural sort * @param {Array} array * @param {String} [path] JSON pointer * @param {'asc' | 'desc'} [direction] */ export function sort (array, path, direction) { const parsedPath = path && path !== '.' ? parsePath(path) : [] const sign = direction === 'desc' ? -1 : 1 const sortedArray = array.slice() sortedArray.sort((a, b) => { const aValue = get(a, parsedPath) const bValue = get(b, parsedPath) return sign * (aValue > bValue ? 1 : aValue < bValue ? -1 : 0) }) return sortedArray } /** * Sort object keys using natural sort * @param {Object} object * @param {'asc' | 'desc'} [direction] */ export function sortObjectKeys (object, direction) { const sign = (direction === 'desc') ? -1 : 1 const sortedFields = Object.keys(object).sort((a, b) => sign * naturalSort(a, b)) const sortedObject = {} sortedFields.forEach(field => { sortedObject[field] = object[field] }) return sortedObject } /** * Cast contents of a string to the correct type. * This can be a string, a number, a boolean, etc * @param {String} str * @return {*} castedStr * @private */ export function parseString (str) { if (str === '') { return '' } const lower = str.toLowerCase() if (lower === 'null') { return null } if (lower === 'true') { return true } if (lower === 'false') { return false } const containsLeadingZero = /^0\d+$/ const startsWithZeroPrefix = /^0[xbo]/i // hex, binary, octal numbers if (containsLeadingZero.test(str) || startsWithZeroPrefix.test(str)) { // treat '001', '0x1A', '0b1101', and '0o3700' as a string return str } const num = Number(str) // will nicely fail with '123ab' const numFloat = parseFloat(str) // will nicely fail with ' ' const isFiniteNumber = !isNaN(num) && !isNaN(numFloat) && isFinite(num) const isInSafeRange = num <= Number.MAX_SAFE_INTEGER && num >= Number.MIN_SAFE_INTEGER const isInteger = /^\d+$/.test(str) if (isFiniteNumber && (isInSafeRange || !isInteger)) { return num } return str } /** * Test whether some field contains a timestamp in milliseconds after the year 2000. * @param {string} field * @param {number} value * @return {boolean} */ export function isTimestamp (field, value) { return typeof value === 'number' && value > YEAR_2000 && isFinite(value) && Math.floor(value) === value && !isNaN(new Date(value).valueOf()) } /** * Return a human readable document size * For example formatSize(7570718) outputs '7.6 MB' * @param {number} size * @return {string} Returns a human readable size */ export function formatSize (size) { if (size < 900) { return size.toFixed() + ' B' } const KB = size / 1000 if (KB < 900) { return KB.toFixed(1) + ' KB' } const MB = KB / 1000 if (MB < 900) { return MB.toFixed(1) + ' MB' } const GB = MB / 1000 if (GB < 900) { return GB.toFixed(1) + ' GB' } const TB = GB / 1000 return TB.toFixed(1) + ' TB' } /** * Limit text to a maximum number of characters * @param {string} text * @param {number} maxCharacterCount * @return {string} Returns the limited text, * ending with '...' if the max was exceeded */ export function limitCharacters (text, maxCharacterCount) { if (text.length <= maxCharacterCount) { return text } return text.slice(0, maxCharacterCount) + '...' } /** * Test whether a value is an Object * @param {*} value * @return {boolean} */ export function isObject (value) { return typeof value === 'object' && value !== null && !Array.isArray(value) } /** * Helper function to test whether an array contains an item * @param {Array} array * @param {*} item * @return {boolean} Returns true if `item` is in `array`, returns false otherwise. */ export function contains (array, item) { return array.indexOf(item) !== -1 } /** * Checks if validation has changed from the previous execution * @param {Array} currErr current validation errors * @param {Array} prevErr previous validation errors */ export function isValidationErrorChanged (currErr, prevErr) { if (!currErr && !prevErr) { return false } if (!Array.isArray(currErr) || !Array.isArray(prevErr) || prevErr.length !== currErr.length) { return true } for (let i = 0; i < currErr.length; i++) { const currItem = currErr[i] const prevItem = prevErr[i] if ( currItem.type !== prevItem.type || JSON.stringify(currItem.error) !== JSON.stringify(prevItem.error) ) { return true } } return false } /** * Uniquely merge array of elements * @param {Array} inputArray1 * @param {Array} inputArray2 * @returns {Array} an array with unique merged elements */ export function uniqueMergeArrays (inputArray1, inputArray2) { const arr1 = inputArray1?.length ? inputArray1 : [] const arr2 = inputArray2?.length ? inputArray2 : [] return [...new Set(arr1.concat(arr2))] } export function asyncExec (callback) { setTimeout(callback) } function hasOwnProperty (object, key) { return Object.prototype.hasOwnProperty.call(object, key) } ================================================ FILE: src/js/validationUtils.js ================================================ import { isPromise, isValidValidationError, stringifyPath } from './util' /** * Execute custom validation if configured. * * Returns a promise resolving with the custom errors (or an empty array). */ export function validateCustom (json, onValidate) { if (!onValidate) { return Promise.resolve([]) } try { const customValidateResults = onValidate(json) const resultPromise = isPromise(customValidateResults) ? customValidateResults : Promise.resolve(customValidateResults) return resultPromise.then(customValidationPathErrors => { if (Array.isArray(customValidationPathErrors)) { return customValidationPathErrors .filter(error => { const valid = isValidValidationError(error) if (!valid) { console.warn('Ignoring a custom validation error with invalid structure. ' + 'Expected structure: {path: [...], message: "..."}. ' + 'Actual error:', error) } return valid }) .map(error => // change data structure into the structure matching the JSON schema errors ({ dataPath: stringifyPath(error.path), message: error.message, type: 'customValidation' })) } else { return [] } }) } catch (err) { return Promise.reject(err) } } ================================================ FILE: src/js/vanilla-picker/index.js ================================================ let VanillaPicker if (window.Picker) { // use the already loaded instance of VanillaPicker VanillaPicker = window.Picker } else { try { // load color picker VanillaPicker = require('vanilla-picker') } catch (err) { // probably running the minimalist bundle } } module.exports = VanillaPicker ================================================ FILE: src/scss/img/description.txt ================================================ JSON Editor Icons size: outer: 24x24 px inner: 16x16 px blue background: RGBA 97b0f8ff gray background: RGBA 4d4d4dff grey background: RGBA d3d3d3ff red foreground: RGBA ff3300ff green foreground: RGBA 13ae00ff characters are based on the Arial font ================================================ FILE: src/scss/jsoneditor/_autocomplete.scss ================================================ @use "variables"; .jsoneditor { .autocomplete { &.dropdown { position: absolute; background: variables.$jse-white; box-shadow: variables.$jse-box-shadow; border: 1px solid variables.$jse-bar-border; overflow-x: hidden; overflow-y: auto; cursor: default; margin: 0; padding: 5px; text-align: left; outline: 0; font-family: variables.$jse-font-mono; font-size: variables.$jse-font-size; .item { color: variables.$jse-content-color; &.hover { background-color: variables.$jse-light-bg; } } } &.hint { color: variables.$jse-date; top: 4px; left: 4px; } } } ================================================ FILE: src/scss/jsoneditor/_contextmenu.scss ================================================ @use "variables"; .jsoneditor-contextmenu-root { position: relative; width: 0; height: 0; } .jsoneditor-contextmenu { position: absolute; box-sizing: content-box; z-index: 2; .jsoneditor-menu { position: relative; left: 0; top: 0; width: 128px; height: auto; background: variables.$jse-white; border: 1px solid variables.$jse-bar-border; box-shadow: variables.$jse-box-shadow; list-style: none; margin: 0; padding: 0; button { position: relative; padding: 0 8px 0 0; margin: 0; width: 128px; height: auto; border: none; cursor: pointer; color: variables.$jse-contextmenu-color; background: transparent; font-size: variables.$jse-font-size; font-family: variables.$jse-font; box-sizing: border-box; text-align: left; &::-moz-focus-inner { padding: 0; border: 0; } &.jsoneditor-default { width: 96px; } &.jsoneditor-expand { float: right; width: 32px; height: 24px; border-left: 1px solid variables.$jse-separator; } } li { overflow: hidden; ul { display: none; position: relative; left: -10px; top: 0; border: none; box-shadow: variables.$jse-box-shadow-inner; padding: 0 10px; -webkit-transition: all 0.3s ease-out; -moz-transition: all 0.3s ease-out; -o-transition: all 0.3s ease-out; transition: all 0.3s ease-out; .jsoneditor-icon { margin-left: 24px; } li { button { padding-left: 24px; animation: all ease-in-out 1s; } } } button { .jsoneditor-expand { position: absolute; top: 0; right: 0; width: 24px; height: 24px; padding: 0; margin: 0 4px 0 0; background-image: variables.$jse-icons-url; background-position: 0 -72px; } } } } .jsoneditor-icon { position: absolute; top: 0; left: 0; width: 24px; height: 24px; border: none; padding: 0; margin: 0; background-image: variables.$jse-icons-url; } .jsoneditor-text { padding: 4px 0 4px 24px; word-wrap: break-word; &.jsoneditor-right-margin { padding-right: 24px; } } .jsoneditor-separator { height: 0; border-top: 1px solid variables.$jse-separator; padding-top: 5px; margin-top: 5px; } button { &.jsoneditor-remove { .jsoneditor-icon { background-position: -24px 0; } } &.jsoneditor-append { .jsoneditor-icon { background-position: 0 0; } } &.jsoneditor-insert { .jsoneditor-icon { background-position: 0 0; } } &.jsoneditor-duplicate { .jsoneditor-icon { background-position: -48px 0; } } &.jsoneditor-sort-asc { .jsoneditor-icon { background-position: -168px 0; } } &.jsoneditor-sort-desc { .jsoneditor-icon { background-position: -192px 0; } } &.jsoneditor-transform { .jsoneditor-icon { background-position: -216px 0; } } &.jsoneditor-extract { .jsoneditor-icon { background-position: 0 -24px; } } &.jsoneditor-type-string { .jsoneditor-icon { background-position: -144px 0; } } &.jsoneditor-type-auto { .jsoneditor-icon { background-position: -120px 0; } } &.jsoneditor-type-object { .jsoneditor-icon { background-position: -72px 0; } } &.jsoneditor-type-array { .jsoneditor-icon { background-position: -96px 0; } } &.jsoneditor-type-modes { .jsoneditor-icon { background-image: none; width: 6px; } } } } .jsoneditor-contextmenu ul, .jsoneditor-contextmenu li { box-sizing: content-box; position: relative; } .jsoneditor-contextmenu .jsoneditor-menu button:hover, .jsoneditor-contextmenu .jsoneditor-menu button:focus { color: variables.$jse-content-color; background-color: variables.$jse-preview; outline: none; } .jsoneditor-contextmenu .jsoneditor-menu li button.jsoneditor-selected, .jsoneditor-contextmenu .jsoneditor-menu li button.jsoneditor-selected:hover, .jsoneditor-contextmenu .jsoneditor-menu li button.jsoneditor-selected:focus { color: variables.$jse-white; background-color: variables.$jse-number; } .jsoneditor-contextmenu .jsoneditor-menu li ul li button:hover, .jsoneditor-contextmenu .jsoneditor-menu li ul li button:focus { background-color: variables.$jse-preview; } .jsoneditor-modal { max-width: 95%; border-radius: 2px !important; padding: 45px 15px 15px 15px !important; box-shadow: variables.$jse-box-shadow; color: variables.$jse-contextmenu-color; line-height: 1.3em; &.jsoneditor-modal-transform { width: 600px !important; } .pico-modal-header { position: absolute; box-sizing: border-box; top: 0; left: 0; width: 100%; padding: 0 10px; height: 30px; line-height: 30px; font-family: variables.$jse-font; font-size: 11pt; background: variables.$jse-blue; color: variables.$jse-white; } table { width: 100%; td { padding: 3px 0; &.jsoneditor-modal-input { text-align: right; padding-right: 0; white-space: nowrap; } &.jsoneditor-modal-actions { padding-top: 15px; } } th { vertical-align: middle; } } p { &:first-child { margin-top: 0; } } a { color: variables.$jse-blue; } .jsoneditor-jmespath-block { margin-bottom: 10px; } .pico-close { background: none !important; font-size: 24px !important; top: 7px !important; right: 7px !important; color: variables.$jse-white; } input { padding: 4px; } input[type="text"] { cursor: inherit; } input[disabled] { background: variables.$jse-empty; color: variables.$jse-readonly; } .jsoneditor-select-wrapper { position: relative; display: inline-block; &:after { content: ""; width: 0; height: 0; border-left: 5px solid transparent; border-right: 5px solid transparent; border-top: 6px solid #666; position: absolute; right: 8px; top: 14px; pointer-events: none; } } select { padding: 3px 24px 3px 10px; min-width: 180px; max-width: 350px; -webkit-appearance: none; -moz-appearance: none; appearance: none; text-indent: 0; text-overflow: ""; font-size: variables.$jse-font-size; line-height: 1.5em; &::-ms-expand { display: none; } } .jsoneditor-button-group { input { padding: 4px 10px; margin: 0; border-radius: 0; border-left-style: none; &.jsoneditor-button-first { border-top-left-radius: 3px; border-bottom-left-radius: 3px; border-left-style: solid; } &.jsoneditor-button-last { border-top-right-radius: 3px; border-bottom-right-radius: 3px; } } } .jsoneditor-transform-preview { background: variables.$jse-preview; height: 200px; &.jsoneditor-error { color: variables.$jse-number; } } .jsoneditor-jmespath-wizard { line-height: 1.2em; width: 100%; padding: 0; border-radius: 3px; } .jsoneditor-jmespath-label { font-weight: bold; color: dodgerblue; margin-top: 20px; margin-bottom: 5px; } .jsoneditor-jmespath-wizard-table { width: 100%; border-collapse: collapse; } .jsoneditor-jmespath-wizard-label { font-style: italic; margin: 4px 0 2px 0; } .jsoneditor-inline { position: relative; display: inline-block; width: 100%; padding-top: 2px; padding-bottom: 2px; &:not(:last-child) { padding-right: 2px; } } .jsoneditor-jmespath-filter { display: flex; flex-wrap: wrap; } .jsoneditor-jmespath-filter-field { width: 180px; } .jsoneditor-jmespath-filter-relation { width: 100px; } .jsoneditor-jmespath-filter-value { min-width: 180px; flex: 1; } .jsoneditor-jmespath-sort-field { width: 170px; } .jsoneditor-jmespath-sort-order { width: 150px; } .jsoneditor-jmespath-select-fields { width: 100%; } .selectr-selected { border-color: variables.$jse-bar-border; padding: 4px 28px 4px 8px; .selectr-tag { background-color: variables.$jse-blue; border-radius: 5px; } } } .jsoneditor-modal table th, .jsoneditor-modal table td { text-align: left; vertical-align: middle; font-weight: normal; color: variables.$jse-contextmenu-color; border-spacing: 0; border-collapse: collapse; } .jsoneditor-modal select, .jsoneditor-modal textarea, .jsoneditor-modal input, .jsoneditor-modal input[type="text"], .jsoneditor-modal input[type="text"]:focus, .jsoneditor-modal #query { background: #ffffff; border: 1px solid variables.$jse-bar-border; color: variables.$jse-contextmenu-color; border-radius: 3px; padding: 4px; } .jsoneditor-modal textarea, .jsoneditor-modal #query { // workaround for a bug on Chrome resulting in blurry text, // see https://github.com/josdejong/jsoneditor/issues/1419 border-radius: unset; } .jsoneditor-modal, .jsoneditor-modal table td, .jsoneditor-modal table th, .jsoneditor-modal select, .jsoneditor-modal option, .jsoneditor-modal textarea, .jsoneditor-modal input, .jsoneditor-modal input[type="text"], .jsoneditor-modal #query { font-size: 10.5pt; font-family: variables.$jse-font; } .jsoneditor-modal #query, .jsoneditor-modal .jsoneditor-transform-preview { font-family: variables.$jse-font-mono; font-size: variables.$jse-font-size; width: 100%; box-sizing: border-box; } .jsoneditor-modal input[type="button"], .jsoneditor-modal input[type="submit"] { background: variables.$jse-preview; padding: 4px 20px; } .jsoneditor-modal select, .jsoneditor-modal input { cursor: pointer; } .jsoneditor-modal .jsoneditor-button-group.jsoneditor-button-group-value-asc input.jsoneditor-button-asc, .jsoneditor-modal .jsoneditor-button-group.jsoneditor-button-group-value-desc input.jsoneditor-button-desc { background: variables.$jse-blue; border-color: variables.$jse-blue; color: variables.$jse-white; } ================================================ FILE: src/scss/jsoneditor/_editor.scss ================================================ @use "variables"; .jsoneditor { color: variables.$jse-content-color; border: thin solid variables.$jse-blue; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; width: 100%; height: 100%; position: relative; padding: 0; line-height: 100%; } div.jsoneditor-field, div.jsoneditor-value, a.jsoneditor-value, div.jsoneditor-readonly, div.jsoneditor-default { border: 1px solid transparent; min-height: 16px; min-width: 32px; line-height: 16px; padding: 2px; margin: 1px; word-wrap: break-word; word-break: break-word; overflow-wrap: break-word; float: left; } div.jsoneditor-field p, div.jsoneditor-value p { margin: 0; } div { &.jsoneditor-value { &.jsoneditor-empty { &::after { content: "value"; } } &.jsoneditor-string { color: variables.$jse-string; } &.jsoneditor-number { color: variables.$jse-number; } &.jsoneditor-boolean { color: variables.$jse-boolean; } &.jsoneditor-null { color: variables.$jse-null; } &.jsoneditor-color-value { color: variables.$jse-color-value; } &.jsoneditor-invalid { color: variables.$jse-invalid; } } &.jsoneditor-readonly { min-width: 16px; color: variables.$jse-readonly; } &.jsoneditor-empty { border-color: variables.$jse-bar-border; border-style: dashed; border-radius: 2px; } &.jsoneditor-field { &.jsoneditor-empty { &::after { content: "field"; } } } &.jsoneditor { td { vertical-align: top; &.jsoneditor-separator { padding: 3px 0; vertical-align: top; color: variables.$jse-readonly; } &.jsoneditor-tree { vertical-align: top; } } &.busy { pre { &.jsoneditor-preview { background: variables.$jse-preview; color: variables.$jse-readonly; } } div { &.jsoneditor-busy { display: inherit; } } } code { &.jsoneditor-preview { background: none; } } &.jsoneditor-mode-preview { pre { &.jsoneditor-preview { width: 100%; height: 100%; box-sizing: border-box; overflow: auto; padding: 2px; margin: 0; white-space: pre-wrap; word-break: break-all; } } } } &.jsoneditor-default { color: variables.$jse-readonly; padding-left: 10px; } &.jsoneditor-tree { width: 100%; height: 100%; position: relative; overflow: auto; background: variables.$jse-white; button { &.jsoneditor-button { width: 24px; height: 24px; padding: 0; margin: 0; border: none; cursor: pointer; background-color: transparent; background-image: variables.$jse-icons-url; &:focus { background-color: variables.$jse-preview; outline: #e5e5e5 solid 1px; } } &.jsoneditor-collapsed { background-position: 0 -48px; } &.jsoneditor-expanded { background-position: 0 -72px; } &.jsoneditor-contextmenu-button { background-position: -48px -72px; } &.jsoneditor-invisible { visibility: hidden; background: none; } &.jsoneditor-dragarea { background-image: variables.$jse-icons-url; background-position: -72px -72px; cursor: move; } } *:focus { outline: none; } div { &.jsoneditor-show-more { display: inline-block; padding: 3px 4px; margin: 2px 0; background-color: variables.$jse-separator; border-radius: 3px; color: variables.$jse-readonly; font-family: variables.$jse-font; font-size: variables.$jse-font-size; a { display: inline-block; color: variables.$jse-readonly; } } &.jsoneditor-color { display: inline-block; width: 12px; height: 12px; margin: 4px; border: 1px solid variables.$jse-readonly; cursor: pointer; &.jsoneditor-color-readonly { cursor: inherit; } } &.jsoneditor-date { background: variables.$jse-date; color: variables.$jse-white; font-family: variables.$jse-font; border-radius: 3px; display: inline-block; padding: 3px; margin: 0 3px; } } table { &.jsoneditor-tree { border-collapse: collapse; border-spacing: 0; width: 100%; } } .jsoneditor-button { display: block; &.jsoneditor-schema-error { width: 24px; height: 24px; padding: 0; margin: 0 4px 0 0; background-image: variables.$jse-icons-url; background-position: -168px -48px; background-color: transparent; } } } &.jsoneditor-outer { position: static; width: 100%; height: 100%; margin: 0; padding: 0; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; &.has-nav-bar { margin-top: -26px; padding-top: 26px; &.has-main-menu-bar { margin-top: -61px; padding-top: 61px; } } &.has-status-bar { margin-bottom: -26px; padding-bottom: 26px; } &.has-main-menu-bar { margin-top: -35px; padding-top: 35px; } } &.jsoneditor-busy { position: absolute; top: 15%; left: 0; box-sizing: border-box; width: 100%; text-align: center; display: none; span { background-color: variables.$jse-busy; border: 1px solid variables.$jse-busy-border-color; border-radius: 3px; padding: 5px 15px; box-shadow: variables.$jse-box-shadow-sm; } } } div.jsoneditor-field.jsoneditor-empty::after, div.jsoneditor-value.jsoneditor-empty::after { pointer-events: none; color: variables.$jse-empty; font-size: 8pt; } div.jsoneditor-value.jsoneditor-url, a.jsoneditor-value.jsoneditor-url { color: variables.$jse-string; text-decoration: underline; } a { &.jsoneditor-value { &.jsoneditor-url { display: inline-block; padding: 2px; margin: 2px; } } } a.jsoneditor-value.jsoneditor-url:hover, a.jsoneditor-value.jsoneditor-url:focus { color: variables.$jse-number; } div.jsoneditor-field[contenteditable="true"]:focus, div.jsoneditor-field[contenteditable="true"]:hover, div.jsoneditor-value[contenteditable="true"]:focus, div.jsoneditor-value[contenteditable="true"]:hover, div.jsoneditor-field.jsoneditor-highlight, div.jsoneditor-value.jsoneditor-highlight { background-color: variables.$jse-busy; border: 1px solid variables.$jse-busy-border-color; border-radius: 2px; } div.jsoneditor-field.jsoneditor-highlight-active, div.jsoneditor-field.jsoneditor-highlight-active:focus, div.jsoneditor-field.jsoneditor-highlight-active:hover, div.jsoneditor-value.jsoneditor-highlight-active, div.jsoneditor-value.jsoneditor-highlight-active:focus, div.jsoneditor-value.jsoneditor-highlight-active:hover { background-color: variables.$jse-highlight-bg; border: 1px solid variables.$jse-highlight-border-color; border-radius: 2px; } div.jsoneditor-value.jsoneditor-object, div.jsoneditor-value.jsoneditor-array { min-width: 16px; } div.jsoneditor-tree button.jsoneditor-contextmenu-button:hover, div.jsoneditor-tree button.jsoneditor-contextmenu-button:focus, div.jsoneditor-tree button.jsoneditor-contextmenu-button.jsoneditor-selected, tr.jsoneditor-selected.jsoneditor-first button.jsoneditor-contextmenu-button { background-position: -48px -48px; } div.jsoneditor-tree div.jsoneditor-show-more a:hover, div.jsoneditor-tree div.jsoneditor-show-more a:focus { color: variables.$jse-number; } textarea.jsoneditor-text, .ace-jsoneditor { min-height: 150px; &.ace_editor { font-family: variables.$jse-font-mono; } } textarea { &.jsoneditor-text { width: 100%; height: 100%; margin: 0; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; outline-width: 0; border: none; background-color: variables.$jse-white; resize: none; } } tr.jsoneditor-highlight, tr.jsoneditor-selected { background-color: variables.$jse-empty; } tr.jsoneditor-selected button.jsoneditor-dragarea, tr.jsoneditor-selected button.jsoneditor-contextmenu-button { visibility: hidden; } tr.jsoneditor-selected.jsoneditor-first button.jsoneditor-dragarea, tr.jsoneditor-selected.jsoneditor-first button.jsoneditor-contextmenu-button { visibility: visible; } div.jsoneditor-tree button.jsoneditor-dragarea:hover, div.jsoneditor-tree button.jsoneditor-dragarea:focus, tr.jsoneditor-selected.jsoneditor-first button.jsoneditor-dragarea { background-position: -72px -48px; } div.jsoneditor tr, div.jsoneditor th, div.jsoneditor td { padding: 0; margin: 0; } div.jsoneditor-field, div.jsoneditor-value, div.jsoneditor td, div.jsoneditor th, div.jsoneditor textarea, pre.jsoneditor-preview, .jsoneditor-schema-error, .jsoneditor-popover { font-family: variables.$jse-font-mono; font-size: variables.$jse-font-size; color: variables.$jse-content-color; } .jsoneditor-schema-error { cursor: default; display: inline-block; height: 24px; line-height: 24px; position: relative; text-align: center; width: 24px; } .jsoneditor-popover { background-color: variables.$jse-popover-bg; border-radius: 3px; box-shadow: variables.$jse-box-shadow-sm; color: variables.$jse-white; padding: 7px 10px; position: absolute; cursor: auto; width: 200px; &.jsoneditor-above { bottom: 32px; left: -98px; &:before { border-top: 7px solid variables.$jse-popover-bg; bottom: -7px; } } &.jsoneditor-below { top: 32px; left: -98px; &:before { border-bottom: 7px solid variables.$jse-popover-bg; top: -7px; } } &.jsoneditor-left { top: -7px; right: 32px; &:before { border-left: 7px solid variables.$jse-popover-bg; border-top: 7px solid transparent; border-bottom: 7px solid transparent; content: ""; top: 19px; right: -14px; left: inherit; margin-left: inherit; margin-top: -7px; position: absolute; } } &.jsoneditor-right { top: -7px; left: 32px; &:before { border-right: 7px solid variables.$jse-popover-bg; border-top: 7px solid transparent; border-bottom: 7px solid transparent; content: ""; top: 19px; left: -14px; margin-left: inherit; margin-top: -7px; position: absolute; } } &:before { border-right: 7px solid transparent; border-left: 7px solid transparent; content: ""; display: block; left: 50%; margin-left: -7px; position: absolute; } } .jsoneditor-text-errors { tr { &.jump-to-line { &:hover { text-decoration: underline; cursor: pointer; } } } } .jsoneditor-schema-error:hover .jsoneditor-popover, .jsoneditor-schema-error:focus .jsoneditor-popover { display: block; animation: fade-in 0.3s linear 1, move-up 0.3s linear 1; } @keyframes fade-in { from { opacity: 0; } to { opacity: 1; } } /* JSON schema errors displayed at the bottom of the editor in mode text and code */ .jsoneditor { .jsoneditor-validation-errors-container { max-height: 130px; overflow-y: auto; } .jsoneditor-validation-errors { width: 100%; overflow: hidden; } .jsoneditor-additional-errors { position: absolute; margin: auto; bottom: 31px; left: calc(50% - 92px); color: variables.$jse-readonly; background-color: variables.$jse-light-bg; padding: 7px 15px; border-radius: 8px; &.visible { visibility: visible; opacity: 1; transition: opacity 2s linear; } &.hidden { visibility: hidden; opacity: 0; transition: visibility 0s 2s, opacity 2s linear; } } .jsoneditor-text-errors { width: 100%; border-collapse: collapse; border-top: 1px solid variables.$jse-highlight-border-color; td { padding: 3px 6px; vertical-align: middle; pre { margin: 0; white-space: pre-wrap; } } tr { background-color: variables.$jse-busy; &.parse-error { background-color: variables.$jse-error; } } } } .jsoneditor-text-errors { .jsoneditor-schema-error { border: none; width: 24px; height: 24px; padding: 0; margin: 0 4px 0 0; cursor: pointer; } tr { .jsoneditor-schema-error { background-image: variables.$jse-icons-url; background-position: -168px -48px; background-color: transparent; } &.parse-error { .jsoneditor-schema-error { background-image: variables.$jse-icons-url; background-position: -25px 0px; background-color: transparent; } } } } .jsoneditor-anchor { cursor: pointer; .picker_wrapper { &.popup { &.popup_bottom { top: 28px; left: -10px; } } } } .fadein { -webkit-animation: fadein 0.3s; animation: fadein 0.3s; -moz-animation: fadein 0.3s; -o-animation: fadein 0.3s; } @keyframes fadein { 0% { opacity: 0; } 100% { opacity: 1; } } // override some styles which where cleared in reset.scss .jsoneditor-modal { input[type="search"].selectr-input { border: 1px solid #d3d3d3; width: calc(100% - 4px); margin: 2px; padding: 4px; box-sizing: border-box; } button.selectr-input-clear { right: 8px; } } ================================================ FILE: src/scss/jsoneditor/_menu.scss ================================================ @use "variables"; .jsoneditor-menu { width: 100%; height: 35px; padding: 2px; margin: 0; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; color: variables.$jse-white; background-color: variables.$jse-blue; border-bottom: 1px solid variables.$jse-blue; } .jsoneditor-menu > button, .jsoneditor-menu > .jsoneditor-modes > button { width: 26px; height: 26px; margin: 2px; padding: 0; border-radius: 2px; border: 1px solid transparent; background-color: transparent; background-image: variables.$jse-icons-url; color: variables.$jse-white; opacity: 0.8; font-family: variables.$jse-font; font-size: variables.$jse-font-size; float: left; } .jsoneditor-menu > button:hover, .jsoneditor-menu > .jsoneditor-modes > button:hover { background-color: rgba(255, 255, 255, 0.2); border: 1px solid rgba(255, 255, 255, 0.4); } .jsoneditor-menu > button:focus, .jsoneditor-menu > button:active, .jsoneditor-menu > .jsoneditor-modes > button:focus, .jsoneditor-menu > .jsoneditor-modes > button:active { background-color: rgba(255, 255, 255, 0.3); } .jsoneditor-menu > button:disabled, .jsoneditor-menu > .jsoneditor-modes > button:disabled { opacity: 0.5; background-color: transparent; border: none; } .jsoneditor-menu > button.jsoneditor-collapse-all { background-position: 0 -96px; } .jsoneditor-menu > button.jsoneditor-expand-all { background-position: 0 -120px; } .jsoneditor-menu > button.jsoneditor-sort { background-position: -120px -96px; } .jsoneditor-menu > button.jsoneditor-transform { background-position: -144px -96px; } .jsoneditor.jsoneditor-mode-view > .jsoneditor-menu > button.jsoneditor-sort, .jsoneditor.jsoneditor-mode-form > .jsoneditor-menu > button.jsoneditor-sort, .jsoneditor.jsoneditor-mode-view > .jsoneditor-menu > button.jsoneditor-transform, .jsoneditor.jsoneditor-mode-form > .jsoneditor-menu > button.jsoneditor-transform { display: none; } .jsoneditor-menu > button.jsoneditor-undo { background-position: -24px -96px; } .jsoneditor-menu > button.jsoneditor-undo:disabled { background-position: -24px -120px; } .jsoneditor-menu > button.jsoneditor-redo { background-position: -48px -96px; } .jsoneditor-menu > button.jsoneditor-redo:disabled { background-position: -48px -120px; } .jsoneditor-menu > button.jsoneditor-compact { background-position: -72px -96px; } .jsoneditor-menu > button.jsoneditor-format { background-position: -72px -120px; } .jsoneditor-menu > button.jsoneditor-repair { background-position: -96px -96px; } .jsoneditor-menu > .jsoneditor-modes { display: inline-block; float: left; } .jsoneditor-menu > .jsoneditor-modes > button { background-image: none; width: auto; padding-left: 6px; padding-right: 6px; } .jsoneditor-menu > button.jsoneditor-separator, .jsoneditor-menu > .jsoneditor-modes > button.jsoneditor-separator { margin-left: 10px; } .jsoneditor-menu a { font-family: variables.$jse-font; font-size: variables.$jse-font-size; color: variables.$jse-white; opacity: 0.8; vertical-align: middle; } .jsoneditor-menu a:hover { opacity: 1; } .jsoneditor-menu a.jsoneditor-poweredBy { font-size: 8pt; position: absolute; right: 0; top: 0; padding: 10px; } ================================================ FILE: src/scss/jsoneditor/_navigationbar.scss ================================================ @use "variables"; .jsoneditor-navigation-bar { width: 100%; height: 26px; line-height: 26px; padding: 0; margin: 0; border-bottom: 1px solid variables.$jse-bar-border; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; color: variables.$jse-readonly; background-color: variables.$jse-light-bg; overflow: hidden; font-family: variables.$jse-font; font-size: variables.$jse-font-size; } ================================================ FILE: src/scss/jsoneditor/_reset.scss ================================================ .jsoneditor, .jsoneditor-modal { -webkit-text-size-adjust: none; text-size-adjust: none; input, input:not([type]), input[type="text"], input[type="search"], { height: auto; border: inherit; box-shadow: none; font-size: inherit; box-sizing: inherit; padding: inherit; font-family: inherit; transition: none; line-height: inherit; &:focus { border: inherit; box-shadow: inherit; } } textarea { height: inherit; } select { display: inherit; height: inherit; } label { font-size: inherit; font-weight: inherit; color: inherit; } table { border-collapse: collapse; width: auto; } td, th { padding: 0; display: table-cell; text-align: left; vertical-align: inherit; border-radius: inherit; } } ================================================ FILE: src/scss/jsoneditor/_searchbox.scss ================================================ @use "variables"; .jsoneditor { &-search { font-family: variables.$jse-font; position: absolute; right: 4px; top: 4px; border-collapse: collapse; border-spacing: 0; display: flex; input { color: variables.$jse-content-color; width: 120px; border: none; outline: none; margin: 1px; line-height: 20px; font-family: variables.$jse-font; } button { width: 16px; height: 24px; padding: 0; margin: 0; border: none; background: variables.$jse-icons-url; vertical-align: top; &:hover { background-color: transparent; } &.jsoneditor-refresh { width: 18px; background-position: -99px -73px; } &.jsoneditor-next { cursor: pointer; background-position: -124px -73px; &:hover { background-position: -124px -49px; } } &.jsoneditor-previous { cursor: pointer; background-position: -148px -73px; margin-right: 2px; &:hover { background-position: -148px -49px; } } } } &-results { font-family: variables.$jse-font; color: variables.$jse-white; padding-right: 5px; line-height: 26px; } &-frame { border: 1px solid transparent; background-color: variables.$jse-white; padding: 0 2px; margin: 0; } } ================================================ FILE: src/scss/jsoneditor/_statusbar.scss ================================================ @use "variables"; .jsoneditor-statusbar { line-height: 26px; height: 26px; color: variables.$jse-readonly; background-color: variables.$jse-bar-bg; border-top: 1px solid variables.$jse-bar-border; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; box-sizing: border-box; font-size: variables.$jse-font-size; & > .jsoneditor-curserinfo-val { margin-right: 12px; } & > .jsoneditor-curserinfo-count { margin-left: 4px; } & > .jsoneditor-validation-error-icon { float: right; width: 24px; height: 24px; padding: 0; margin-top: 1px; background-image: variables.$jse-icons-url; background-position: -168px -48px; cursor: pointer; } & > .jsoneditor-validation-error-count { float: right; margin: 0 4px 0 0; cursor: pointer; } & > .jsoneditor-parse-error-icon { float: right; width: 24px; height: 24px; padding: 0; margin: 1px; background-image: variables.$jse-icons-url; background-position: -25px 0px; } .jsoneditor-array-info { a { color: inherit; } } } div.jsoneditor-statusbar > .jsoneditor-curserinfo-label, div.jsoneditor-statusbar > .jsoneditor-size-info { margin: 0 4px; } ================================================ FILE: src/scss/jsoneditor/_treepath.scss ================================================ @use "variables"; .jsoneditor-treepath { padding: 0 5px; overflow: hidden; white-space: nowrap; outline: none; &.show-all { word-wrap: break-word; white-space: normal; position: absolute; background-color: variables.$jse-light-bg; z-index: 1; box-shadow: variables.$jse-box-shadow; span { &.jsoneditor-treepath-show-all-btn { display: none; } } } div { &.jsoneditor-contextmenu-root { position: absolute; left: 0; } } .jsoneditor-treepath-show-all-btn { position: absolute; background-color: variables.$jse-light-bg; left: 0; height: 20px; padding: 0 3px; cursor: pointer; } .jsoneditor-treepath-element { margin: 1px; font-family: variables.$jse-font; font-size: variables.$jse-font-size; } .jsoneditor-treepath-seperator { margin: 2px; font-size: 9pt; font-family: variables.$jse-font; } } .jsoneditor-treepath span.jsoneditor-treepath-element:hover, .jsoneditor-treepath span.jsoneditor-treepath-seperator:hover { cursor: pointer; text-decoration: underline; } ================================================ FILE: src/scss/jsoneditor/_variables.scss ================================================ $jse-white: #ffffff !default; $jse-grey: #999999 !default; $jse-light-bg: #ebebeb !default; $jse-blue: #3883fa !default; $jse-content-color: #1a1a1a !default; $jse-string: #006000 !default; $jse-number: #ee422e !default; $jse-boolean: #ff8c00 !default; $jse-null: #004ed0 !default; $jse-color-value: $jse-content-color !default; $jse-invalid: $jse-content-color !default; $jse-readonly: #808080 !default; $jse-empty: #d3d3d3 !default; $jse-preview: #f5f5f5 !default; $jse-busy: #ffffab !default; $jse-busy-border-color: #ffee00 !default; $jse-error: #ee2e2e70 !default; $jse-separator: #e5e5e5 !default; $jse-highlight-bg: #ffee00 !default; $jse-highlight-border-color: #ffc700 !default; $jse-popover-bg: #4c4c4c !default; $jse-bar-bg: $jse-light-bg !default; $jse-bar-border: $jse-empty !default; $jse-menu-color: $jse-empty !default; $jse-contextmenu-color: #4d4d4d !default; $jse-box-shadow: 2px 2px 12px rgba(128, 128, 128, 0.3) !default; $jse-box-shadow-sm: 0 0 5px rgba(0, 0, 0, 0.4) !default; $jse-box-shadow-inner: inset 0 0 10px rgba(128, 128, 128, 0.5) !default; $jse-date: #a1a1a1 !default; $jse-font: arial, sans-serif !default; // "consolas" for Windows, "menlo" for Mac with fallback to "monaco", 'Ubuntu Mono' for Ubuntu // (at Mac this font looks too large at 14px, but 13px is too small for the font on Windows) $jse-font-mono: consolas, menlo, monaco, 'Ubuntu Mono', 'source-code-pro', monospace !default; $jse-font-size: 14px !default; $jse-icons: "./img/jsoneditor-icons.svg" !default; $jse-icons-url: url($jse-icons) !default; ================================================ FILE: src/scss/jsoneditor.scss ================================================ @use "jsoneditor/reset"; @use "jsoneditor/variables"; @use "jsoneditor/autocomplete"; @use "jsoneditor/contextmenu"; @use "jsoneditor/editor"; @use "jsoneditor/menu"; @use "jsoneditor/navigationbar"; @use "jsoneditor/searchbox"; @use "jsoneditor/statusbar"; @use "jsoneditor/treepath"; @use "../js/assets/selectr/selectr"; ================================================ FILE: test/Node.test.js ================================================ import assert from 'assert' import './setup' import { Node } from '../src/js/Node' describe('Node', () => { describe('_findSchema', () => { it('should find schema', () => { const schema = { type: 'object', properties: { child: { type: 'string' } } } const path = ['child'] assert.strictEqual(Node._findSchema(schema, {}, path), schema.properties.child) }) it('should find schema inside an array item', () => { const schema = { properties: { job: { type: 'array', items: { type: 'object', properties: { company: { enum: ['test1', 'test2'] } } } } } } assert.strictEqual(Node._findSchema(schema, {}, []), schema) assert.strictEqual(Node._findSchema(schema, {}, ['job']), schema.properties.job) assert.strictEqual(Node._findSchema(schema, {}, ['job', 0]), schema.properties.job.items) assert.strictEqual(Node._findSchema(schema, {}, ['job', 0, 'company']), schema.properties.job.items.properties.company) }) it('should find schema within multi-level object properties', () => { const schema = { type: 'object', properties: { levelTwo: { type: 'object', properties: { levelThree: { type: 'object', properties: { bool: { type: 'boolean' } } } } } } } let path = [] assert.strictEqual(Node._findSchema(schema, {}, path), schema) path = ['levelTwo'] assert.strictEqual(Node._findSchema(schema, {}, path), schema.properties.levelTwo) path = ['levelTwo', 'levelThree'] assert.strictEqual(Node._findSchema(schema, {}, path), schema.properties.levelTwo.properties.levelThree) path = ['levelTwo', 'levelThree', 'bool'] assert.strictEqual( Node._findSchema(schema, {}, path), schema.properties.levelTwo.properties.levelThree.properties.bool ) }) it('should find referenced schema within multi-level object properties', () => { const schema = { type: 'object', properties: { aProperty: { $ref: 'second_schema#/definitions/some_def' } } } const schemaRefs = { second_schema: { definitions: { some_def: { type: 'object', properties: { enumProp: { enum: [1, 2, 3] } } } } } } const path = ['aProperty', 'enumProp'] const expectedSchema = { enum: [1, 2, 3] } assert.deepStrictEqual(Node._findSchema(schema, schemaRefs, path), expectedSchema) }) it('should find array referenced schema within multi-level object properties', () => { const schema = { type: 'object', properties: { aProperty: { type: 'array', items: { $ref: 'second_schema#/definitions/some_def' } } } } const schemaRefs = { second_schema: { definitions: { some_def: { type: 'object', properties: { enumProp: { enum: [1, 2, 3] } } } } } } const path = ['aProperty', 0, 'enumProp'] const expectedSchema = { enum: [1, 2, 3] } assert.deepStrictEqual(Node._findSchema(schema, schemaRefs, path), expectedSchema) }) it('should return null for path that has no schema', () => { const schema = { type: 'object', properties: { foo: { type: 'object', properties: { baz: { type: 'number' } } } } } let path = ['bar'] assert.strictEqual(Node._findSchema(schema, {}, path), null) path = ['foo', 'bar'] assert.strictEqual(Node._findSchema(schema, {}, path), null) }) it('should find one of required properties', () => { const schema = { properties: { company: { type: 'string', enum: ['1', '2'] }, worker: { type: 'string', enum: ['a', 'b'] }, manager: { type: 'string', enum: ['c', 'd'] } }, additionalProperties: false, oneOf: [ { required: ['worker'] }, { required: ['manager'] } ] } let path = ['company'] assert.deepStrictEqual(Node._findSchema(schema, {}, path), { type: 'string', enum: ['1', '2'] }) path = ['worker'] assert.deepStrictEqual(Node._findSchema(schema, {}, path), { type: 'string', enum: ['a', 'b'] }) }) // https://json-schema.org/understanding-json-schema/reference/object#extending it('works with extending schemas', () => { const schema = { properties: { name: true, manager: { type: 'string', enum: ['c', 'd'] } }, additionalProperties: false, allOf: [ { properties: { name: { type: 'string', enum: ['a', 'b'] } } } ] } let path = ['name'] assert.deepStrictEqual(Node._findSchema(schema, {}, path), { type: 'string', enum: ['a', 'b'] }) path = ['manager'] assert.deepStrictEqual(Node._findSchema(schema, {}, path), { type: 'string', enum: ['c', 'd'] }) }) describe('with $ref', () => { it('should find a referenced schema', () => { const schema = { type: 'object', properties: { foo: { $ref: 'foo' } } } const fooSchema = { type: 'number', title: 'Foo' } const path = ['foo'] assert.strictEqual(Node._findSchema(schema, { foo: fooSchema }, path), fooSchema) }) it('should find a referenced schema property', () => { const schema = { type: 'object', properties: { foo: { $ref: 'foo' } } } const fooSchema = { type: 'object', properties: { levelTwo: { type: 'string' } } } const path = ['foo', 'levelTwo'] assert.strictEqual(Node._findSchema(schema, { foo: fooSchema }, path), fooSchema.properties.levelTwo) }) it('should find a referenced schema definition', () => { const schema = { type: 'object', properties: { foo: { type: 'array', items: { $ref: 'foo#/definitions/some_def' } } } } const fooSchema = { definitions: { some_def: { type: 'object', properties: { propA: { type: 'string' }, propB: { type: 'string' } } } } } const path = ['foo', 0] assert.strictEqual(Node._findSchema(schema, { foo: fooSchema }, path), fooSchema.definitions.some_def) }) it('should find a referenced schema definition 2', () => { const schema = { type: 'object', properties: { foo: { type: 'array', items: { $ref: 'foo#/definitions/some_def' } } } } const fooSchema = { definitions: { some_def: { type: 'object', properties: { propA: { type: 'string' }, propB: { type: 'string' } } } } } const path = ['foo', 0, 'propA'] assert.strictEqual(Node._findSchema(schema, { foo: fooSchema }, path), fooSchema.definitions.some_def.properties.propA) }) it('should find a referenced schema definition 3', () => { const schema = { type: 'object', properties: { foo: { type: 'array', items: { $ref: 'foo#/definitions/some_def' } } } } const fooSchema = { definitions: { some_def: { type: 'object', properties: { propA: { type: 'object', properties: { propA1: { type: 'boolean' } } }, propB: { type: 'string' } } } } } const path = ['foo', 0, 'propA', 'propA1'] assert.strictEqual(Node._findSchema(schema, { foo: fooSchema }, path), fooSchema.definitions.some_def.properties.propA.properties.propA1) }) }) describe('with $ref to internal definition', () => { it('should find a referenced schema', () => { const schema = { $schema: 'http://json-schema.org/draft-07/schema#', type: 'object', patternProperties: { '^/[a-z0-9]*$': { $ref: '#/definitions/component' } }, definitions: { component: { type: 'object', properties: { type: { type: 'string', minLength: 1 }, config: { type: 'object' }, children: { type: 'object', patternProperties: { '^/[a-z0-9]+$': { $ref: '#/definitions/component' } } } } } } } const path = ['/status', 'children', '/bus', 'config'] const foundSchema = { type: 'object' } assert.notStrictEqual(Node._findSchema(schema, {}, path), foundSchema) }) }) describe('with $ref to external definition', () => { it('should find a referenced schema', () => { const schema = { type: 'object', properties: { address: { $ref: 'definitions.json#/address' } } } const definitions = { address: { type: 'object', properties: { country: { type: 'string' }, city: { type: 'string' } } } } const path = ['address', 'city'] const foundSchema = { type: 'string' } assert.notStrictEqual(Node._findSchema(schema, { 'definitions.json': definitions }, path), foundSchema) }) }) describe('with pattern properties', () => { it('should find schema', () => { const schema = { type: 'object', properties: { str: { title: 'str', type: 'boolean' } }, patternProperties: { '^foo[0-9]': { title: 'foo[0-] pattern property', type: 'string' } } } let path = [] assert.strictEqual(Node._findSchema(schema, {}, path), schema, 'top level') path = ['str'] assert.strictEqual(Node._findSchema(schema, {}, path), schema.properties.str, 'normal property') }) it('should find schema within multi-level object properties', () => { const schema = { type: 'object', properties: { levelTwo: { type: 'object', properties: { levelThree: { type: 'object', properties: { bool: { title: 'bool', type: 'boolean' } } } } } }, patternProperties: { '^foo[0-9]': { title: 'foo[0-9] pattern property', type: 'string' } } } let path = [] assert.strictEqual(Node._findSchema(schema, {}, path), schema, 'top level') path = ['levelTwo'] assert.strictEqual(Node._findSchema(schema, {}, path), schema.properties.levelTwo, 'level two') path = ['levelTwo', 'levelThree'] assert.strictEqual(Node._findSchema(schema, {}, path), schema.properties.levelTwo.properties.levelThree, 'level three') path = ['levelTwo', 'levelThree', 'bool'] assert.strictEqual( Node._findSchema(schema, {}, path), schema.properties.levelTwo.properties.levelThree.properties.bool, 'normal property' ) }) it('should find schema for pattern properties', () => { const schema = { type: 'object', patternProperties: { '^foo[0-9]': { title: 'foo[0-9] pattern property', type: 'string' }, '^bar[0-9]': { title: 'bar[0-9] pattern property', type: 'string' } } } let path = ['foo1'] assert.strictEqual( Node._findSchema(schema, {}, path), schema.patternProperties['^foo[0-9]'], 'first pattern property' ) path = ['bar5'] assert.strictEqual( Node._findSchema(schema, {}, path), schema.patternProperties['^bar[0-9]'], 'second pattern property' ) }) it('should find schema for multi-level pattern properties', () => { const schema = { type: 'object', patternProperties: { '^foo[0-9]': { title: 'foo[0-9] pattern property', type: 'object', properties: { fooChild: { type: 'object', properties: { fooChild2: { type: 'string' } } } } }, '^bar[0-9]': { title: 'bar[0-9] pattern property', type: 'object', properties: { barChild: { type: 'string' } } } } } let path = ['foo1', 'fooChild', 'fooChild2'] assert.strictEqual( Node._findSchema(schema, {}, path), schema.patternProperties['^foo[0-9]'].properties.fooChild.properties.fooChild2, 'first pattern property child of child' ) path = ['bar5', 'barChild'] assert.strictEqual( Node._findSchema(schema, {}, path), schema.patternProperties['^bar[0-9]'].properties.barChild, 'second pattern property child' ) }) it('should return null for path that has no schema', () => { const schema = { type: 'object', properties: { levelTwo: { type: 'object', properties: { levelThree: { type: 'number' } } } }, patternProperties: { '^foo[0-9]': { title: 'foo[0-9] pattern property', type: 'string' }, '^bar[0-9]': { title: 'bar[0-9] pattern property', type: 'string' } } } let path = ['not-in-schema'] assert.strictEqual(Node._findSchema(schema, {}, path), null) path = ['levelOne', 'not-in-schema'] assert.strictEqual(Node._findSchema(schema, {}, path), null) }) it('should return additionalProperties schema', () => { const schema = { type: 'object', properties: { company: { type: 'string', enum: ['1', '2'] }, nested: { type: 'object', additionalProperties: { type: 'number' } } }, additionalProperties: { type: 'string', enum: ['1', '2'] } } let path = ['company2'] assert.strictEqual( Node._findSchema(schema, {}, path), schema.additionalProperties, 'additionalProperties schema' ) path = ['nested', 'virtual'] assert.strictEqual( Node._findSchema(schema, {}, path), schema.properties.nested.additionalProperties, 'additionalProperties schema' ) }) }) }) describe('_findEnum', () => { it('should find enum', () => { const schema = { type: 'object', enum: [1, 2, 3] } assert.strictEqual(Node._findEnum(schema), schema.enum) }) }) }) ================================================ FILE: test/SchemaTextCompleter.test.js ================================================ import assert from 'assert' import { schema, schemaRefs } from './data/schemas' import { autocompleteJsonStr } from './data/jsons' import { SchemaTextCompleter } from '../src/js/SchemaTextCompleter' const sessionMock = { getValue: () => autocompleteJsonStr } describe('SchemaTextCompleter tests', () => { let schemaTextCompleter before(() => { schemaTextCompleter = new SchemaTextCompleter(schema, schemaRefs) }) it('should initiate and expose getCompletions function', () => { assert.strictEqual(typeof schemaTextCompleter.getCompletions, 'function') }) it('should validate completions of single schema ref', (done) => { schemaTextCompleter.getCompletions( undefined, sessionMock, { row: 2, column: 18 }, '', (a, completions) => { assert.strictEqual(completions.length, 1) assert.strictEqual(completions[0].caption, 'John') assert.strictEqual(completions[0].meta, 'schema [examples]') assert.strictEqual(completions[0].score, 0) assert.strictEqual(completions[0].value, 'John') done() } ) }) it('should validate completions of triple schema refs', (done) => { schemaTextCompleter.getCompletions( undefined, sessionMock, { row: 15, column: 14 }, '', (a, completions) => { assert.strictEqual(completions.length, 3) assert.strictEqual(completions[0].caption, 'junior') assert.strictEqual(completions[0].meta, 'schema [enum]') assert.strictEqual(completions[0].score, 0) assert.strictEqual(completions[0].value, 'junior') assert.strictEqual(completions[1].caption, 'experienced') assert.strictEqual(completions[1].meta, 'schema [enum]') assert.strictEqual(completions[1].score, 1) assert.strictEqual(completions[1].value, 'experienced') assert.strictEqual(completions[2].caption, 'senior') assert.strictEqual(completions[2].meta, 'schema [enum]') assert.strictEqual(completions[2].score, 2) assert.strictEqual(completions[2].value, 'senior') done() } ) }) it('should validate completions of recursive schema refs', (done) => { schemaTextCompleter.getCompletions( undefined, sessionMock, { row: 22, column: 21 }, '', (a, completions) => { assert.strictEqual(completions.length, 1) assert.strictEqual(completions[0].caption, 'Smith') assert.strictEqual(completions[0].meta, 'schema [examples]') assert.strictEqual(completions[0].score, 0) assert.strictEqual(completions[0].value, 'Smith') done() } ) }) }) ================================================ FILE: test/couchdbeditor.html ================================================ CouchDB Document Editor

    CouchDB Document Editor

    Document Url:
    ================================================ FILE: test/data/jsons.js ================================================ export const autocompleteJsonStr = `{ "personalDetails": { "firstName": "John", "lastName": "Doe", "gender": "male", "age": 32 }, "availableToHire": true, "job": { "company": "freelance", "role": "Human Resources Coordinator", "salary": 140, "address": "Jerusalem" }, "profession": { "level": "senior", "experience": 10 }, "reporters": [ { "personalDetails": { "firstName": "John", "lastName": "Doe", "gender": "male", "age": 28 }, "job": { "company": "freelance", "role": "developer", "salary": 120, "address": "New York" }, "profession": { "level": "junior", "experience": 2 } } ], "publications": [ { "type": "academic", "journal": "MIT today" }, { "type": "professional", "journal": "stack overflow" } ] }` ================================================ FILE: test/data/schemas.js ================================================ const employeeSchema = { title: 'Employee', description: 'Object containing employee details', type: 'object', additonalProperties: false, properties: { personalDetails: { $ref: 'personal' }, availableToHire: { type: 'boolean', default: false }, job: { $ref: 'job' }, profession: { oneOf: [ { $ref: 'junior' }, { $ref: 'experienced' }, { $ref: 'senior' } ] }, reporters: { type: 'array', items: { $ref: 'employeeSchema' } }, publications: { type: 'array', items: { type: 'object', properties: { type: { type: 'string', enum: ['academic', 'professional'] }, journal: { type: 'string' } } } } }, required: ['personalDetails'] } const personal = { title: 'Personal Details', type: 'object', required: ['firstName', 'lastName'], 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', type: 'string', enum: ['male', 'female'], examples: ['male', 'female'] }, age: { description: 'Age in years', type: 'integer', minimum: 0, examples: [28, 32] } } } const 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 junior = { type: 'object', properties: { level: { type: 'string', enum: ['junior'] }, experience: { description: 'years of experience', type: 'number', minimum: 0, maximum: 3, examples: [0, 1, 2, 3] } } } const experienced = { type: 'object', properties: { level: { type: 'string', enum: ['experienced'] }, experience: { description: 'years of experience', type: 'number', minimum: 3, maximum: 7, examples: [3, 4, 5, 6, 7] } } } const senior = { type: 'object', properties: { level: { type: 'string', enum: ['senior'] }, experience: { description: 'years of experience', type: 'number', minimum: 7, examples: [7, 8, 9, 10, 11] } } } export const schema = { oneOf: [ { $ref: 'employeeSchema' } ] } export const schemaRefs = { employeeSchema, personal, job, junior, experienced, senior } ================================================ FILE: test/jsonUtils.test.js ================================================ import assert from 'assert' import { stringifyPartial, containsArray } from '../src/js/jsonUtils' describe('jsonUtils', () => { it('should stringify a small object', () => { const json = { a: 2, b: 'foo', c: null, d: false, e: [1, 2, 3], f: { g: 'h' } } assert.strictEqual(stringifyPartial(json), '{"a":2,"b":"foo","c":null,"d":false,"e":[1,2,3],"f":{"g":"h"}}') }) it('should stringify a small object with formatting', () => { const json = { a: 2, b: 'foo', c: null, d: false, e: [1, 2, 3], f: { g: 'h' } } assert.strictEqual(stringifyPartial(json, 2), '{\n' + ' "a": 2,\n' + ' "b": "foo",\n' + ' "c": null,\n' + ' "d": false,\n' + ' "e": [\n' + ' 1,\n' + ' 2,\n' + ' 3\n' + ' ],\n' + ' "f": {\n' + ' "g": "h"\n' + ' }\n' + '}') assert.strictEqual(stringifyPartial(json, ' '), '{\n' + ' "a": 2,\n' + ' "b": "foo",\n' + ' "c": null,\n' + ' "d": false,\n' + ' "e": [\n' + ' 1,\n' + ' 2,\n' + ' 3\n' + ' ],\n' + ' "f": {\n' + ' "g": "h"\n' + ' }\n' + '}') }) it('should limit stringified output', () => { const json = { a: 2, b: 'foo', c: null, d: false, e: [1, 2, 3], f: { g: 'h' } } const all = '{"a":2,"b":"foo","c":null,"d":false,"e":[1,2,3],"f":{"g":"h"}}' const limit = 20 assert.strictEqual(stringifyPartial(json, undefined, limit), all.slice(0, limit) + '...') assert.strictEqual(stringifyPartial(json, undefined, all.length), all) assert.strictEqual(stringifyPartial([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], undefined, 10), '[1,2,3,4,5...') assert.strictEqual(stringifyPartial(12345678, undefined, 4), '1234...') }) it('should count array items', () => { // assert.strictEqual(countArrayItems('[1,2,3]'), 3) assert.strictEqual(containsArray('[]'), true) assert.strictEqual(containsArray(' []'), true) assert.strictEqual(containsArray(' \t []'), true) assert.strictEqual(containsArray(' \t\n []'), true) assert.strictEqual(containsArray('"["'), false) assert.strictEqual(containsArray('2'), false) assert.strictEqual(containsArray('null'), false) assert.strictEqual(containsArray('{}'), false) }) }) ================================================ FILE: test/setup.js ================================================ import { JSDOM } from 'jsdom' /** * Set up the test environment by simulating browser globals. * @param {string} [locale=en] A locale to set in navigator.language * @return {void} */ function setUpTestEnvironment (locale) { if (!locale) { locale = 'en' } const dom = new JSDOM('...') global.window = dom.window global.document = dom.window.document if (typeof global.navigator === 'undefined') { global.navigator = dom.window.navigator } // JSDom has no setter defined for navigator.language, so defineProperty is necessary in order to override it Object.defineProperty(navigator, 'language', { value: locale }) }; setUpTestEnvironment() ================================================ FILE: test/test_bootstrap.html ================================================

    Test with bootstrap

    ================================================ FILE: test/test_build.html ================================================

    Switch editor mode using the mode box. Note that the mode can be changed programmatically as well using the method editor.setMode(mode), try it in the console of your browser.

    ================================================ FILE: test/test_build_min.html ================================================

    Switch editor mode using the mode box. Note that the mode can be changed programmatically as well using the method editor.setMode(mode), try it in the console of your browser.

    ================================================ FILE: test/test_code_mode.html ================================================

    Switch editor mode using the mode box. Note that the mode can be changed programmatically as well using the method editor.setMode(mode), try it in the console of your browser.

    ================================================ FILE: test/test_color_picker.html ================================================

    Test color picker firing onChange on every change instead of onDone.

    ================================================ FILE: test/test_destroy.html ================================================

    Switch editor mode using the mode box. Note that the mode can be changed programmatically as well using the method editor.setMode(mode), try it in the console of your browser.

    ================================================ FILE: test/test_enum.html ================================================ JSONEditor | template + enums
    ================================================ FILE: test/test_enum_2.html ================================================ JSONEditor | template + enums

    Demonstrates a template with JSON schema validation. To use: click the context menu of the first or second employee, click insert or append, click "Employee".

    See github issue #473

    ================================================ FILE: test/test_focus_tracker.html ================================================

    Switch editor mode using the mode box. Note that the mode can be changed programmatically as well using the method editor.setMode(mode), try it in the console of your browser.

    ================================================ FILE: test/test_get_inner_text.html ================================================ JSONEditor test getInnerHtml contenteditable div:

    Hello world

    test paste from OpenOffice

    a


    test

    innerText:

    getInnerText:

    inner text (stringified):

    text content (stringified):

    ================================================ FILE: test/test_large_array.html ================================================

    Switch editor mode using the mode box. Note that the mode can be changed programmatically as well using the method editor.setMode(mode), try it in the console of your browser.

    ================================================ FILE: test/test_materialize.html ================================================

    Switch editor mode using the mode box. Note that the mode can be changed programmatically as well using the method editor.setMode(mode), try it in the console of your browser.

    ================================================ FILE: test/test_minimalist_min.html ================================================

    Switch editor mode using the mode box. Note that the mode can be changed programmatically as well using the method editor.setMode(mode), try it in the console of your browser.

    ================================================ FILE: test/test_popup_anchor.html ================================================ JSONEditor | JSON schema validation

    Test custom tooltip anchor

    The JSON Schema error tooltips and the color picker should have correct placing and overflow the editor, also in combination with scrolling.

    ================================================ FILE: test/test_preview_load_save.html ================================================ JSONEditor | Preview mode load and save

    Load and save JSON documents in Preview mode

    This examples uses HTML5 to load/save local files. Powered by FileReader.js and FileSaver.js.
    Only supported on modern browsers (Chrome, FireFox, IE10+, Safari 6.1+, Opera 15+).

    Load a JSON document:

    Save a JSON document:

    ================================================ FILE: test/test_schema.html ================================================

    Switch editor mode using the mode box. Note that the mode can be changed programmatically as well using the method editor.setMode(mode), try it in the console of your browser.

    ================================================ FILE: test/test_update.html ================================================ JSONEditor | Update JSON
    ================================================ FILE: test/util.test.js ================================================ import assert from 'assert' import { compileJSONPointer, findUniqueName, formatSize, get, getChildPaths, getIndexForPosition, isObject, isTimestamp, isValidationErrorChanged, limitCharacters, makeFieldTooltip, parsePath, parseString, removeReturnsAndSurroundingWhitespace, sort, sortObjectKeys, stringifyPath, uniqueMergeArrays } from '../src/js/util' describe('util', () => { describe('jsonPath', () => { it('should stringify an array of paths', () => { assert.deepStrictEqual(stringifyPath([]), '') assert.deepStrictEqual(stringifyPath(['foo']), '.foo') assert.deepStrictEqual(stringifyPath(['foo', 'bar']), '.foo.bar') assert.deepStrictEqual(stringifyPath(['foo', 2]), '.foo[2]') assert.deepStrictEqual(stringifyPath(['foo', 2, 'bar']), '.foo[2].bar') assert.deepStrictEqual(stringifyPath(['foo', 2, 'bar_baz']), '.foo[2].bar_baz') assert.deepStrictEqual(stringifyPath([2]), '[2]') assert.deepStrictEqual(stringifyPath(['foo', 'prop-with-hyphens']), '.foo["prop-with-hyphens"]') assert.deepStrictEqual(stringifyPath(['foo', 'prop with spaces']), '.foo["prop with spaces"]') }) it('should parse a json path', () => { assert.deepStrictEqual(parsePath(''), []) assert.deepStrictEqual(parsePath('.foo'), ['foo']) assert.deepStrictEqual(parsePath('.foo.bar'), ['foo', 'bar']) assert.deepStrictEqual(parsePath('.foo[2]'), ['foo', 2]) assert.deepStrictEqual(parsePath('.foo[2].bar'), ['foo', 2, 'bar']) assert.deepStrictEqual(parsePath('.foo["prop with spaces"]'), ['foo', 'prop with spaces']) assert.deepStrictEqual(parsePath('.foo[\'prop with single quotes as outputted by ajv library\']'), ['foo', 'prop with single quotes as outputted by ajv library']) assert.deepStrictEqual(parsePath('.foo["prop with . dot"]'), ['foo', 'prop with . dot']) assert.deepStrictEqual(parsePath('.foo["prop with ] character"]'), ['foo', 'prop with ] character']) assert.deepStrictEqual(parsePath('.foo[*].bar'), ['foo', '*', 'bar']) assert.deepStrictEqual(parsePath('[2]'), [2]) }) it('should throw an exception in case of an invalid path', () => { assert.throws(() => { parsePath('.') }, /Invalid JSON path: property name expected at index 1/) assert.throws(() => { parsePath('[') }, /Invalid JSON path: unexpected end, character ] expected/) assert.throws(() => { parsePath('[]') }, /Invalid JSON path: array value expected at index 1/) assert.throws(() => { parsePath('.foo[ ]') }, /Invalid JSON path: array value expected at index 7/) assert.throws(() => { parsePath('.[]') }, /Invalid JSON path: property name expected at index 1/) assert.throws(() => { parsePath('["23]') }, /Invalid JSON path: unexpected end, character " expected/) assert.throws(() => { parsePath('.foo bar') }, /Invalid JSON path: unexpected character " " at index 4/) }) }) describe('getIndexForPosition', () => { const el = { value: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\nUt enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.' } it('happy flows - row and column in range', () => { assert.strictEqual(getIndexForPosition(el, 1, 1), 0) assert.strictEqual(getIndexForPosition(el, 2, 1), 124) assert.strictEqual(getIndexForPosition(el, 3, 8), 239) assert.strictEqual(getIndexForPosition(el, 4, 22), 356) }) it('if range exceeds it should be considered as if it is last row or column length', () => { assert.strictEqual(getIndexForPosition(el, 1, 100000), 123) assert.strictEqual(getIndexForPosition(el, 100000, 1), 335) assert.strictEqual(getIndexForPosition(el, 100000, 100000), 445) }) it('missing or wrong input sould return -1', () => { assert.strictEqual(getIndexForPosition(el), -1) assert.strictEqual(getIndexForPosition(el, undefined, 1), -1) assert.strictEqual(getIndexForPosition(el, 1, undefined), -1) assert.strictEqual(getIndexForPosition(el, -2, -2), -1) }) }) describe('isValidationErrorChanged', () => { const err1 = { type: 'validation', error: { keyword: 'enum', dataPath: '.gender', schemaPath: '#/properties/gender/enum', params: { allowedValues: ['male', 'female'] }, message: 'should be equal to one of: "male", "female"', schema: ['male', 'female'], parentSchema: { title: 'Gender', enum: ['male', 'female'] }, data: null, type: 'validation' } } const err2 = { type: 'validation', error: { keyword: 'type', dataPath: '.age', schemaPath: '#/properties/age/type', params: { type: 'integer' }, message: 'should be integer', schema: 'integer', parentSchema: { description: 'Age in years', type: 'integer', minimum: 0, examples: [28, 32] }, data: '28', type: 'validation' } } const err3 = { type: 'validation', error: { dataPath: '.gender', message: 'Member must be an object with properties "name" and "age"' } } const err3b = { type: 'validation', error: { dataPath: '.gender', message: 'Must be an object' } } it('empty value for both current and previous error should return false', () => { assert.strictEqual(isValidationErrorChanged(), false) }) it('empty value for one of current and previous error should return true', () => { assert.strictEqual(isValidationErrorChanged([err1]), true) assert.strictEqual(isValidationErrorChanged(undefined, [err1]), true) }) it('different length of current and previous errors should return true', () => { assert.strictEqual(isValidationErrorChanged([err1], []), true) assert.strictEqual(isValidationErrorChanged([err1], [err1, err2]), true) }) it('different values for current and previous errors should return true', () => { assert.strictEqual(isValidationErrorChanged([err1, err2], [err3, err1]), true) }) it('different message', () => { assert.strictEqual(isValidationErrorChanged([err3], [err3b]), true) }) }) describe('get', () => { it('should get a nested property from an object', () => { const obj = { a: { b: 2 }, c: 3, d: null, e: undefined } assert.strictEqual(get(obj, ['a', 'b']), 2) assert.strictEqual(get(obj, ['c']), 3) assert.deepStrictEqual(get(obj, ['a']), { b: 2 }) assert.strictEqual(get(obj, ['a', 'foo']), undefined) assert.strictEqual(get(obj, ['a', 'foo', 'bar']), undefined) assert.strictEqual(get(obj, ['d']), null) assert.strictEqual(get(obj, ['d', 'foo', 'bar']), null) assert.strictEqual(get(obj, ['e']), undefined) }) }) describe('makeFieldTooltip', () => { it('should return empty string when the schema is missing all relevant fields', () => { assert.strictEqual(makeFieldTooltip({}), '') assert.strictEqual(makeFieldTooltip({ additionalProperties: false }), '') assert.strictEqual(makeFieldTooltip(), '') }) it('should make tooltips with only title', () => { assert.strictEqual(makeFieldTooltip({ title: 'foo' }), 'foo') }) it('should make tooltips with only description', () => { assert.strictEqual(makeFieldTooltip({ description: 'foo' }), 'foo') }) it('should make tooltips with only default', () => { assert.strictEqual(makeFieldTooltip({ default: 'foo' }), 'Default\n"foo"') }) it('should make tooltips with only examples', () => { assert.strictEqual(makeFieldTooltip({ examples: ['foo', 'bar'] }), 'Examples\n"foo"\n"bar"') }) it('should make tooltips with title and description', () => { assert.strictEqual(makeFieldTooltip({ title: 'foo', description: 'bar' }), 'foo\nbar') const longTitle = 'Lorem Ipsum Dolor' const longDescription = 'Duis id elit non ante gravida vestibulum non nec est. ' + 'Proin vitae ligula at elit dapibus tempor. ' + 'Etiam lacinia augue vel condimentum interdum. ' assert.strictEqual( makeFieldTooltip({ title: longTitle, description: longDescription }), longTitle + '\n' + longDescription ) }) it('should make tooltips with title, description, and examples', () => { assert.strictEqual( makeFieldTooltip({ title: 'foo', description: 'bar', examples: ['baz'] }), 'foo\nbar\n\nExamples\n"baz"' ) }) it('should make tooltips with title, description, default, and examples', () => { assert.strictEqual( makeFieldTooltip({ title: 'foo', description: 'bar', default: 'bat', examples: ['baz'] }), 'foo\nbar\n\nDefault\n"bat"\n\nExamples\n"baz"' ) }) it('should handle empty fields', () => { assert.strictEqual(makeFieldTooltip({ title: '', description: 'bar' }), 'bar') assert.strictEqual(makeFieldTooltip({ title: 'foo', description: '' }), 'foo') assert.strictEqual(makeFieldTooltip({ description: 'bar', examples: [] }), 'bar') assert.strictEqual(makeFieldTooltip({ description: 'bar', examples: [''] }), 'bar\n\nExamples\n""') }) it('should internationalize "Defaults" correctly', () => { assert.strictEqual(makeFieldTooltip({ default: 'foo' }, 'pt-BR'), 'Revelia\n"foo"') }) it('should internationalize "Examples" correctly', () => { assert.strictEqual(makeFieldTooltip({ examples: ['foo'] }, 'pt-BR'), 'Exemplos\n"foo"') }) }) describe('getChildPaths', () => { it('should extract all child paths of an array containing objects', () => { const json = [ { name: 'A', location: { latitude: 1, longitude: 2 } }, { name: 'B', location: { latitude: 1, longitude: 2 } }, { name: 'C', timestamp: 0 } ] assert.deepStrictEqual(getChildPaths(json), [ '.location.latitude', '.location.longitude', '.name', '.timestamp' ]) }) it('should extract all child paths of an array containing objects, including objects', () => { const json = [ { name: 'A', location: { latitude: 1, longitude: 2 } }, { name: 'B', location: { latitude: 1, longitude: 2 } }, { name: 'C', timestamp: 0 } ] assert.deepStrictEqual(getChildPaths(json, true), [ '', '.location', '.location.latitude', '.location.longitude', '.name', '.timestamp' ]) }) it('should extract all child paths of an array containing values', () => { const json = [1, 2, 3] assert.deepStrictEqual(getChildPaths(json), [ '' ]) }) it('should extract all child paths of a non-array', () => { assert.deepStrictEqual(getChildPaths({ a: 2, b: { c: 3 } }), ['']) assert.deepStrictEqual(getChildPaths('foo'), ['']) assert.deepStrictEqual(getChildPaths(123), ['']) }) }) it('should test whether something is an object', () => { assert.strictEqual(isObject({}), true) assert.strictEqual(isObject(new Date()), true) assert.strictEqual(isObject([]), false) assert.strictEqual(isObject(2), false) assert.strictEqual(isObject(null), false) assert.strictEqual(isObject(undefined), false) assert.strictEqual(isObject(), false) }) describe('sort', () => { it('should sort an array', () => { const array = [4, 1, 10, 2] assert.deepStrictEqual(sort(array), [1, 2, 4, 10]) assert.deepStrictEqual(sort(array, '.', 'desc'), [10, 4, 2, 1]) }) it('should sort an array containing objects', () => { const array = [ { value: 4 }, { value: 1 }, { value: 10 }, { value: 2 } ] assert.deepStrictEqual(sort(array, '.value'), [ { value: 1 }, { value: 2 }, { value: 4 }, { value: 10 } ]) assert.deepStrictEqual(sort(array, '.value', 'desc'), [ { value: 10 }, { value: 4 }, { value: 2 }, { value: 1 } ]) }) }) describe('sortObjectKeys', () => { it('should sort the keys of an object', () => { const object = { c: 'c', a: 'a', b: 'b' } assert.strictEqual(JSON.stringify(object), '{"c":"c","a":"a","b":"b"}') assert.strictEqual(JSON.stringify(sortObjectKeys(object)), '{"a":"a","b":"b","c":"c"}') assert.strictEqual(JSON.stringify(sortObjectKeys(object, 'asc')), '{"a":"a","b":"b","c":"c"}') assert.strictEqual(JSON.stringify(sortObjectKeys(object, 'desc')), '{"c":"c","b":"b","a":"a"}') }) }) describe('parseString', () => { it('should parse a string', () => { assert.strictEqual(parseString('foo'), 'foo') assert.strictEqual(parseString('234foo'), '234foo') assert.strictEqual(parseString(' 234'), 234) assert.strictEqual(parseString('234 '), 234) assert.strictEqual(parseString('2.3'), 2.3) assert.strictEqual(parseString('null'), null) assert.strictEqual(parseString('true'), true) assert.strictEqual(parseString('false'), false) assert.strictEqual(parseString('+1'), 1) assert.strictEqual(parseString('01'), '01') assert.strictEqual(parseString('001'), '001') assert.strictEqual(parseString('0.3'), 0.3) assert.strictEqual(parseString('0e3'), 0) assert.strictEqual(parseString(' '), ' ') assert.strictEqual(parseString(''), '') assert.strictEqual(parseString('"foo"'), '"foo"') assert.strictEqual(parseString('"2"'), '"2"') assert.strictEqual(parseString('\'foo\''), '\'foo\'') assert.strictEqual(parseString('0x1A'), '0x1A') assert.strictEqual(parseString('0x1F'), '0x1F') assert.strictEqual(parseString('0x1a'), '0x1a') assert.strictEqual(parseString('0b1101'), '0b1101') assert.strictEqual(parseString('0o3700'), '0o3700') assert.strictEqual(parseString('0X1a'), '0X1a') assert.strictEqual(parseString('0B1101'), '0B1101') assert.strictEqual(parseString('0O3700'), '0O3700') assert.strictEqual(parseString('7405242042266046865'), '7405242042266046865') assert.strictEqual(parseString('9007199254740991'), 9007199254740991) assert.strictEqual(parseString('9007199254740991'), 9007199254740991) assert.strictEqual(parseString('-9007199254740991'), -9007199254740991) assert.strictEqual(parseString('1e25'), 1e25) assert.strictEqual(parseString('1e308'), 1e308) assert.strictEqual(parseString('1e309'), '1e309') }) }) it('should find a unique name', () => { assert.strictEqual(findUniqueName('other', [ 'a', 'b', 'c' ]), 'other') assert.strictEqual(findUniqueName('b', [ 'a', 'b', 'c' ]), 'b (copy)') assert.strictEqual(findUniqueName('b', [ 'a', 'b', 'c', 'b (copy)' ]), 'b (copy 2)') assert.strictEqual(findUniqueName('b', [ 'a', 'b', 'c', 'b (copy)', 'b (copy 2)' ]), 'b (copy 3)') assert.strictEqual(findUniqueName('b (copy)', [ 'a', 'b', 'b (copy)', 'b (copy 2)', 'c' ]), 'b (copy 3)') assert.strictEqual(findUniqueName('b (copy 2)', [ 'a', 'b', 'b (copy)', 'b (copy 2)', 'c' ]), 'b (copy 3)') assert.strictEqual(findUniqueName('b (copy)', [ 'a', 'c' ]), 'b (copy)') }) it('should format a document size in a human readable way', () => { assert.strictEqual(formatSize(500), '500 B') assert.strictEqual(formatSize(900), '0.9 KB') assert.strictEqual(formatSize(77.89 * 1000), '77.9 KB') assert.strictEqual(formatSize(950 * 1000), '0.9 MB') assert.strictEqual(formatSize(7.22 * 1000 * 1000), '7.2 MB') assert.strictEqual(formatSize(945.4 * 1000 * 1000), '0.9 GB') assert.strictEqual(formatSize(22.37 * 1000 * 1000 * 1000), '22.4 GB') assert.strictEqual(formatSize(1000 * 1000 * 1000 * 1000), '1.0 TB') }) it('should limit characters', () => { assert.strictEqual(limitCharacters('hello world', 11), 'hello world') assert.strictEqual(limitCharacters('hello world', 5), 'hello...') assert.strictEqual(limitCharacters('hello world', 100), 'hello world') }) it('should compile a JSON pointer', () => { assert.strictEqual(compileJSONPointer(['foo', 'bar']), '/foo/bar') assert.strictEqual(compileJSONPointer(['foo', '/~ ~/']), '/foo/~1~0 ~0~1') assert.strictEqual(compileJSONPointer(['']), '/') assert.strictEqual(compileJSONPointer([]), '') }) it('should test whether a field is a timestamp', () => { assert.strictEqual(isTimestamp('foo', 1574809200000), true) assert.strictEqual(isTimestamp('foo', 1574809200000.2), false) }) it('regex should match whitespace and surrounding whitespace', () => { assert.strictEqual( removeReturnsAndSurroundingWhitespace(' \n A\nB \nC \n D \n\n E F\n '), 'ABCDE F') }) describe('uniqueMergeArrays', () => { it('should merge arrays with unique values', () => { const arr1 = ['a', 'b', 'c', 'd', 'e'] const arr2 = ['c', 'd', 'f', 'g'] assert.deepStrictEqual(uniqueMergeArrays(arr1, arr2), ['a', 'b', 'c', 'd', 'e', 'f', 'g']) }) }) // TODO: thoroughly test all util methods })