Merge branch 'develop' of https://github.com/josdejong/jsoneditor into develop

This commit is contained in:
Israel 2017-12-02 03:00:30 -05:00
commit 371a7b2d14
30 changed files with 4105 additions and 1422 deletions

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
.idea .idea
.vscode
build build
downloads downloads
node_modules node_modules

View File

@ -3,6 +3,57 @@
https://github.com/josdejong/jsoneditor https://github.com/josdejong/jsoneditor
## 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 ## 2017-07-24, version 5.9.3
- Fixed broken multi-selection (regression). - Fixed broken multi-selection (regression).

View File

@ -1,5 +1,11 @@
# JSON Editor # 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/2017.svg)
[![License](https://img.shields.io/github/license/josdejong/jsoneditor.svg)](https://github.com/josdejong/jsoneditor/blob/master/LICENSE)
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2Fjosdejong%2Fjsoneditor.svg?type=shield)](https://app.fossa.io/projects/git%2Bgithub.com%2Fjosdejong%2Fjsoneditor?ref=badge_shield)
JSON Editor is a web-based tool to view, edit, format, and validate JSON. 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 It has various modes such as a tree editor, a code editor, and a plain text
editor. editor.

View File

@ -11,7 +11,7 @@
height="144" height="144"
id="svg4136" id="svg4136"
version="1.1" version="1.1"
inkscape:version="0.91 r" inkscape:version="0.91 r13725"
sodipodi:docname="jsoneditor-icons.svg"> sodipodi:docname="jsoneditor-icons.svg">
<title <title
id="title6512">JSON Editor Icons</title> id="title6512">JSON Editor Icons</title>
@ -39,12 +39,12 @@
inkscape:pageopacity="0" inkscape:pageopacity="0"
inkscape:pageshadow="2" inkscape:pageshadow="2"
inkscape:window-width="1920" inkscape:window-width="1920"
inkscape:window-height="1028" inkscape:window-height="1027"
id="namedview4144" id="namedview4144"
showgrid="true" showgrid="true"
inkscape:zoom="4" inkscape:zoom="32"
inkscape:cx="97.217248" inkscape:cx="101.13921"
inkscape:cy="59.950227" inkscape:cy="34.512712"
inkscape:window-x="0" inkscape:window-x="0"
inkscape:window-y="0" inkscape:window-y="0"
inkscape:window-maximized="1" inkscape:window-maximized="1"
@ -710,49 +710,49 @@
width="15.99999" width="15.99999"
y="101" y="101"
x="76.000008" x="76.000008"
style="fill:#ffffff;fill-opacity:0.80000007;stroke:none;stroke-width:0" /> style="fill:#ffffff;fill-opacity:0.8;stroke:none;stroke-width:0" />
<rect <rect
id="rect4761-0" id="rect4761-0"
height="1.9999945" height="1.9999945"
width="15.99999" width="15.99999"
y="105" y="105"
x="76.000008" x="76.000008"
style="fill:#ffffff;fill-opacity:0.80000007;stroke:none;stroke-width:0" /> style="fill:#ffffff;fill-opacity:0.8;stroke:none;stroke-width:0" />
<rect <rect
id="rect4761-7" id="rect4761-7"
height="1.9999945" height="1.9999945"
width="9" width="9"
y="109" y="109"
x="76.000008" x="76.000008"
style="fill:#ffffff;fill-opacity:0.80000007;stroke:none;stroke-width:0" /> style="fill:#ffffff;fill-opacity:0.8;stroke:none;stroke-width:0" />
<rect <rect
id="rect4761-1-1" id="rect4761-1-1"
height="1.9999945" height="1.9999945"
width="12" width="12"
y="125" y="125"
x="76.000008" x="76.000008"
style="fill:#ffffff;fill-opacity:0.80000007;stroke:none;stroke-width:0" /> style="fill:#ffffff;fill-opacity:0.8;stroke:none;stroke-width:0" />
<rect <rect
id="rect4761-1-1-4" id="rect4761-1-1-4"
height="1.9999945" height="1.9999945"
width="10" width="10"
y="137" y="137"
x="76.000008" x="76.000008"
style="fill:#ffffff;fill-opacity:0.80000007;stroke:none;stroke-width:0" /> style="fill:#ffffff;fill-opacity:0.8;stroke:none;stroke-width:0" />
<rect <rect
id="rect4761-1-1-4-4" id="rect4761-1-1-4-4"
height="1.9999945" height="1.9999945"
width="10" width="10"
y="129" y="129"
x="82" x="82"
style="fill:#ffffff;fill-opacity:0.80000007;stroke:none;stroke-width:0" /> style="fill:#ffffff;fill-opacity:0.8;stroke:none;stroke-width:0" />
<rect <rect
id="rect4761-1-1-4-4-3" id="rect4761-1-1-4-4-3"
height="1.9999945" height="1.9999945"
width="9" width="9"
y="133" y="133"
x="82" x="82"
style="fill:#ffffff;fill-opacity:0.80000007;stroke:none;stroke-width:0" /> style="fill:#ffffff;fill-opacity:0.8;stroke:none;stroke-width:0" />
<path <path
inkscape:connector-curvature="0" inkscape:connector-curvature="0"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.8;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.66157866;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.8;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.66157866;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
@ -890,4 +890,10 @@
id="path4300-6" id="path4300-6"
inkscape:connector-curvature="0" inkscape:connector-curvature="0"
sodipodi:nodetypes="cccc" /> sodipodi:nodetypes="cccc" />
<path
style="fill:#ffffff;fill-opacity:0.8;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:0.80000001"
d="M 99.994369,113.0221 102,114.98353 l 7,-6.9558 3,0.97227 2,-1 1,-2 0,-3 -3,3 -3,-3 3,-3 -3,0 -2,1 -1,2 0.99437,3.0221 z"
id="path4268"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccccccccc" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 36 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

88
dist/jsoneditor.css vendored
View File

@ -228,6 +228,16 @@ div.jsoneditor-outer {
box-sizing: border-box; box-sizing: border-box;
} }
div.jsoneditor-outer.has-nav-bar {
margin: -61px 0 0 0;
padding: 61px 0 0 0;
}
div.jsoneditor-outer.has-status-bar {
margin: -35px 0 -26px 0;
padding: 35px 0 26px 0;
}
textarea.jsoneditor-text, textarea.jsoneditor-text,
.ace-jsoneditor { .ace-jsoneditor {
min-height: 150px; min-height: 150px;
@ -255,7 +265,7 @@ textarea.jsoneditor-text {
tr.jsoneditor-highlight, tr.jsoneditor-highlight,
tr.jsoneditor-selected { tr.jsoneditor-selected {
background-color: #e6e6e6; background-color: #d3d3d3;
} }
tr.jsoneditor-selected button.jsoneditor-dragarea, tr.jsoneditor-selected button.jsoneditor-dragarea,
@ -831,6 +841,10 @@ div.jsoneditor-menu > button.jsoneditor-format {
background-position: -72px -120px; background-position: -72px -120px;
} }
div.jsoneditor-menu > button.jsoneditor-repair {
background-position: -96px -96px;
}
div.jsoneditor-menu > div.jsoneditor-modes { div.jsoneditor-menu > div.jsoneditor-modes {
display: inline-block; display: inline-block;
float: left; float: left;
@ -977,3 +991,75 @@ div.jsoneditor div.autocomplete.hint {
top: 4px; top: 4px;
left: 4px; left: 4px;
} }
div.jsoneditor-treepath {
padding: 0 5px;
overflow: hidden;
}
div.jsoneditor-treepath div.jsoneditor-contextmenu-root {
position: absolute;
left: 0;
}
div.jsoneditor-treepath span.jsoneditor-treepath-element {
margin: 1px;
font-family: arial, sans-serif;
font-size: 10pt;
}
div.jsoneditor-treepath span.jsoneditor-treepath-seperator {
margin: 2px;
font-size: 9pt;
font-family: arial, sans-serif;
}
div.jsoneditor-treepath span.jsoneditor-treepath-element:hover,
div.jsoneditor-treepath span.jsoneditor-treepath-seperator:hover {
cursor: pointer;
text-decoration: underline;
}
div.jsoneditor-statusbar {
line-height: 26px;
height: 26px;
margin-top: -26px;
color: #808080;
background-color: #ebebeb;
border-top: 1px solid #d3d3d3;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
font-size: 10pt;
}
div.jsoneditor-statusbar > .jsoneditor-curserinfo-label {
margin: 0 2px 0 4px;
}
div.jsoneditor-statusbar > .jsoneditor-curserinfo-val {
margin-right: 12px;
}
div.jsoneditor-statusbar > .jsoneditor-curserinfo-count {
margin-left: 4px;
}
div.jsoneditor-navigation-bar {
width: 100%;
height: 26px;
line-height: 26px;
padding: 0;
margin: 0;
border-bottom: 1px solid #d3d3d3;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
color: #808080;
background-color: #ebebeb;
font-size: 10pt;
}
div.jsoneditor-navigation-bar.nav-bar-empty:after {
content: 'Select a node ...';
color: rgba(104, 104, 91, 0.56);
position: absolute;
margin-left: 5px;
}

3609
dist/jsoneditor.js vendored

File diff suppressed because one or more lines are too long

2
dist/jsoneditor.map vendored

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -180,6 +180,13 @@ Constructs a new JSONEditor.
- Can return an object `{startFrom: number, options: string[]}`. Here `startFrom` determines the start character from where the existing text will be replaced. `startFrom` is `0` by default, replacing the whole text. - Can return an object `{startFrom: number, options: string[]}`. Here `startFrom` determines the start character from where the existing text will be replaced. `startFrom` is `0` by default, replacing the whole text.
- Can return a `Promise` resolving one of the return types above to support asynchronously retrieving a list with options. - Can return a `Promise` resolving one of the return types above to support asynchronously retrieving a list with options.
- `{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 buttom of the editor - the status bar shows the cursor position (currently only for 'code' `mode`) and a count of the selected charcters. True by default. Only applicable when `mode` is 'code' or 'text'.
### Methods ### Methods

View File

@ -131,7 +131,10 @@ gulp.task('bundle-css', ['mkdir'], function () {
'src/css/contextmenu.css', 'src/css/contextmenu.css',
'src/css/menu.css', 'src/css/menu.css',
'src/css/searchbox.css', 'src/css/searchbox.css',
'src/css/autocomplete.css' 'src/css/autocomplete.css',
'src/css/treepath.css',
'src/css/statusbar.css',
'src/css/navigationbar.css'
]) ])
.pipe(concatCss(NAME + '.css')) .pipe(concatCss(NAME + '.css'))
.pipe(gulp.dest(DIST)) .pipe(gulp.dest(DIST))

View File

@ -1,6 +1,6 @@
{ {
"name": "jsoneditor", "name": "jsoneditor",
"version": "5.9.3", "version": "5.11.0",
"main": "./index", "main": "./index",
"description": "A web-based tool to view, edit, format, and validate JSON", "description": "A web-based tool to view, edit, format, and validate JSON",
"tags": [ "tags": [
@ -23,8 +23,8 @@
"test": "mocha test" "test": "mocha test"
}, },
"dependencies": { "dependencies": {
"ajv": "5.2.0", "ajv": "5.4.0",
"brace": "0.10.0", "brace": "0.11.0",
"javascript-natural-sort": "0.7.1" "javascript-natural-sort": "0.7.1"
}, },
"devDependencies": { "devDependencies": {

View File

@ -11,7 +11,7 @@
height="144" height="144"
id="svg4136" id="svg4136"
version="1.1" version="1.1"
inkscape:version="0.91 r" inkscape:version="0.91 r13725"
sodipodi:docname="jsoneditor-icons.svg"> sodipodi:docname="jsoneditor-icons.svg">
<title <title
id="title6512">JSON Editor Icons</title> id="title6512">JSON Editor Icons</title>
@ -39,12 +39,12 @@
inkscape:pageopacity="0" inkscape:pageopacity="0"
inkscape:pageshadow="2" inkscape:pageshadow="2"
inkscape:window-width="1920" inkscape:window-width="1920"
inkscape:window-height="1028" inkscape:window-height="1027"
id="namedview4144" id="namedview4144"
showgrid="true" showgrid="true"
inkscape:zoom="4" inkscape:zoom="32"
inkscape:cx="97.217248" inkscape:cx="101.13921"
inkscape:cy="59.950227" inkscape:cy="34.512712"
inkscape:window-x="0" inkscape:window-x="0"
inkscape:window-y="0" inkscape:window-y="0"
inkscape:window-maximized="1" inkscape:window-maximized="1"
@ -710,49 +710,49 @@
width="15.99999" width="15.99999"
y="101" y="101"
x="76.000008" x="76.000008"
style="fill:#ffffff;fill-opacity:0.80000007;stroke:none;stroke-width:0" /> style="fill:#ffffff;fill-opacity:0.8;stroke:none;stroke-width:0" />
<rect <rect
id="rect4761-0" id="rect4761-0"
height="1.9999945" height="1.9999945"
width="15.99999" width="15.99999"
y="105" y="105"
x="76.000008" x="76.000008"
style="fill:#ffffff;fill-opacity:0.80000007;stroke:none;stroke-width:0" /> style="fill:#ffffff;fill-opacity:0.8;stroke:none;stroke-width:0" />
<rect <rect
id="rect4761-7" id="rect4761-7"
height="1.9999945" height="1.9999945"
width="9" width="9"
y="109" y="109"
x="76.000008" x="76.000008"
style="fill:#ffffff;fill-opacity:0.80000007;stroke:none;stroke-width:0" /> style="fill:#ffffff;fill-opacity:0.8;stroke:none;stroke-width:0" />
<rect <rect
id="rect4761-1-1" id="rect4761-1-1"
height="1.9999945" height="1.9999945"
width="12" width="12"
y="125" y="125"
x="76.000008" x="76.000008"
style="fill:#ffffff;fill-opacity:0.80000007;stroke:none;stroke-width:0" /> style="fill:#ffffff;fill-opacity:0.8;stroke:none;stroke-width:0" />
<rect <rect
id="rect4761-1-1-4" id="rect4761-1-1-4"
height="1.9999945" height="1.9999945"
width="10" width="10"
y="137" y="137"
x="76.000008" x="76.000008"
style="fill:#ffffff;fill-opacity:0.80000007;stroke:none;stroke-width:0" /> style="fill:#ffffff;fill-opacity:0.8;stroke:none;stroke-width:0" />
<rect <rect
id="rect4761-1-1-4-4" id="rect4761-1-1-4-4"
height="1.9999945" height="1.9999945"
width="10" width="10"
y="129" y="129"
x="82" x="82"
style="fill:#ffffff;fill-opacity:0.80000007;stroke:none;stroke-width:0" /> style="fill:#ffffff;fill-opacity:0.8;stroke:none;stroke-width:0" />
<rect <rect
id="rect4761-1-1-4-4-3" id="rect4761-1-1-4-4-3"
height="1.9999945" height="1.9999945"
width="9" width="9"
y="133" y="133"
x="82" x="82"
style="fill:#ffffff;fill-opacity:0.80000007;stroke:none;stroke-width:0" /> style="fill:#ffffff;fill-opacity:0.8;stroke:none;stroke-width:0" />
<path <path
inkscape:connector-curvature="0" inkscape:connector-curvature="0"
style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.8;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.66157866;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate" style="color:#000000;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:medium;line-height:normal;font-family:sans-serif;text-indent:0;text-align:start;text-decoration:none;text-decoration-line:none;text-decoration-style:solid;text-decoration-color:#000000;letter-spacing:normal;word-spacing:normal;text-transform:none;direction:ltr;block-progression:tb;writing-mode:lr-tb;baseline-shift:baseline;text-anchor:start;white-space:normal;clip-rule:nonzero;display:inline;overflow:visible;visibility:visible;opacity:0.8;isolation:auto;mix-blend-mode:normal;color-interpolation:sRGB;color-interpolation-filters:linearRGB;solid-color:#000000;solid-opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2.66157866;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;color-rendering:auto;image-rendering:auto;shape-rendering:auto;text-rendering:auto;enable-background:accumulate"
@ -890,4 +890,10 @@
id="path4300-6" id="path4300-6"
inkscape:connector-curvature="0" inkscape:connector-curvature="0"
sodipodi:nodetypes="cccc" /> sodipodi:nodetypes="cccc" />
<path
style="fill:#ffffff;fill-opacity:0.8;fill-rule:evenodd;stroke:#ffffff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:0.80000001"
d="M 99.994369,113.0221 102,114.98353 l 7,-6.9558 3,0.97227 2,-1 1,-2 0,-3 -3,3 -3,-3 3,-3 -3,0 -2,1 -1,2 0.99437,3.0221 z"
id="path4268"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccccccccc" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -210,6 +210,16 @@ div.jsoneditor-outer {
box-sizing: border-box; box-sizing: border-box;
} }
div.jsoneditor-outer.has-nav-bar {
margin: -61px 0 0 0;
padding: 61px 0 0 0;
}
div.jsoneditor-outer.has-status-bar {
margin: -35px 0 -26px 0;
padding: 35px 0 26px 0;
}
textarea.jsoneditor-text, textarea.jsoneditor-text,
.ace-jsoneditor { .ace-jsoneditor {
min-height: 150px; min-height: 150px;
@ -239,7 +249,7 @@ textarea.jsoneditor-text {
tr.jsoneditor-highlight, tr.jsoneditor-highlight,
tr.jsoneditor-selected { tr.jsoneditor-selected {
background-color: #e6e6e6; background-color: #d3d3d3;
} }
tr.jsoneditor-selected button.jsoneditor-dragarea, tr.jsoneditor-selected button.jsoneditor-dragarea,

View File

@ -71,6 +71,9 @@ div.jsoneditor-menu > button.jsoneditor-compact {
div.jsoneditor-menu > button.jsoneditor-format { div.jsoneditor-menu > button.jsoneditor-format {
background-position: -72px -120px; background-position: -72px -120px;
} }
div.jsoneditor-menu > button.jsoneditor-repair {
background-position: -96px -96px;
}
div.jsoneditor-menu > div.jsoneditor-modes { div.jsoneditor-menu > div.jsoneditor-modes {
display: inline-block; display: inline-block;

21
src/css/navigationbar.css Normal file
View File

@ -0,0 +1,21 @@
div.jsoneditor-navigation-bar {
width: 100%;
height: 26px;
line-height: 26px;
padding: 0;
margin: 0;
border-bottom: 1px solid #d3d3d3;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
color: #808080;
background-color: #ebebeb;
font-size: 10pt;
}
div.jsoneditor-navigation-bar.nav-bar-empty:after {
content: 'Select a node ...';
color: rgba(104, 104, 91, 0.56);
position: absolute;
margin-left: 5px;
}

25
src/css/statusbar.css Normal file
View File

@ -0,0 +1,25 @@
div.jsoneditor-statusbar {
line-height: 26px;
height: 26px;
margin-top: -26px;
color: #808080;
background-color: #ebebeb;
border-top: 1px solid #d3d3d3;
-moz-box-sizing: border-box;
-webkit-box-sizing: border-box;
box-sizing: border-box;
font-size: 10pt;
}
div.jsoneditor-statusbar > .jsoneditor-curserinfo-label {
margin: 0 2px 0 4px;
}
div.jsoneditor-statusbar > .jsoneditor-curserinfo-val {
margin-right: 12px;
}
div.jsoneditor-statusbar > .jsoneditor-curserinfo-count {
margin-left: 4px;
}

27
src/css/treepath.css Normal file
View File

@ -0,0 +1,27 @@
div.jsoneditor-treepath {
padding: 0 5px;
overflow: hidden;
}
div.jsoneditor-treepath div.jsoneditor-contextmenu-root {
position: absolute;
left: 0;
}
div.jsoneditor-treepath span.jsoneditor-treepath-element{
margin: 1px;
font-family: arial, sans-serif;
font-size: 10pt;
}
div.jsoneditor-treepath span.jsoneditor-treepath-seperator {
margin: 2px;
font-size: 9pt;
font-family: arial, sans-serif;
}
div.jsoneditor-treepath span.jsoneditor-treepath-element:hover, div.jsoneditor-treepath span.jsoneditor-treepath-seperator:hover {
cursor:pointer;
text-decoration:underline;
}

View File

@ -2,6 +2,15 @@
var util = require('./util'); var util = require('./util');
/**
* Node.getRootNode shim
* @param {Node} node node to check
* @return {Node} node's rootNode or `window` if there is ShadowDOM is not supported.
*/
function getRootNode(node){
return node.getRootNode && node.getRootNode() || window;
}
/** /**
* A context menu * A context menu
* @param {Object[]} items Array containing the menu structure * @param {Object[]} items Array containing the menu structure
@ -202,8 +211,12 @@ ContextMenu.prototype.show = function (anchor, contentWindow) {
// determine whether to display the menu below or above the anchor // determine whether to display the menu below or above the anchor
var showBelow = true; var showBelow = true;
if (contentWindow) { var parent = anchor.parentNode;
var anchorRect = anchor.getBoundingClientRect(); var anchorRect = anchor.getBoundingClientRect();
var parentRect = parent.getBoundingClientRect()
if (contentWindow) {
var contentRect = contentWindow.getBoundingClientRect(); var contentRect = contentWindow.getBoundingClientRect();
if (anchorRect.bottom + this.maxHeight < contentRect.bottom) { if (anchorRect.bottom + this.maxHeight < contentRect.bottom) {
@ -218,29 +231,34 @@ ContextMenu.prototype.show = function (anchor, contentWindow) {
} }
} }
var leftGap = anchorRect.left - parentRect.left;
var topGap = anchorRect.top - parentRect.top;
// position the menu // position the menu
if (showBelow) { if (showBelow) {
// display the menu below the anchor // display the menu below the anchor
var anchorHeight = anchor.offsetHeight; var anchorHeight = anchor.offsetHeight;
this.dom.menu.style.left = '0px'; this.dom.menu.style.left = leftGap + 'px';
this.dom.menu.style.top = anchorHeight + 'px'; this.dom.menu.style.top = topGap + anchorHeight + 'px';
this.dom.menu.style.bottom = ''; this.dom.menu.style.bottom = '';
} }
else { else {
// display the menu above the anchor // display the menu above the anchor
this.dom.menu.style.left = '0px'; this.dom.menu.style.left = leftGap + 'px';
this.dom.menu.style.top = ''; this.dom.menu.style.top = topGap + 'px';
this.dom.menu.style.bottom = '0px'; this.dom.menu.style.bottom = '0px';
} }
// find the root node of the page (window, or a shadow dom root element)
this.rootNode = getRootNode(anchor);
// attach the menu to the parent of the anchor // attach the menu to the parent of the anchor
var parent = anchor.parentNode;
parent.insertBefore(this.dom.root, parent.firstChild); parent.insertBefore(this.dom.root, parent.firstChild);
// create and attach event listeners // create and attach event listeners
var me = this; var me = this;
var list = this.dom.list; var list = this.dom.list;
this.eventListeners.mousedown = util.addEventListener(window, 'mousedown', function (event) { this.eventListeners.mousedown = util.addEventListener(this.rootNode, 'mousedown', function (event) {
// hide menu on click outside of the menu // hide menu on click outside of the menu
var target = event.target; var target = event.target;
if ((target != list) && !me._isChildOf(target, list)) { if ((target != list) && !me._isChildOf(target, list)) {
@ -249,7 +267,7 @@ ContextMenu.prototype.show = function (anchor, contentWindow) {
event.preventDefault(); event.preventDefault();
} }
}); });
this.eventListeners.keydown = util.addEventListener(window, 'keydown', function (event) { this.eventListeners.keydown = util.addEventListener(this.rootNode, 'keydown', function (event) {
me._onKeyDown(event); me._onKeyDown(event);
}); });
@ -284,7 +302,7 @@ ContextMenu.prototype.hide = function () {
if (this.eventListeners.hasOwnProperty(name)) { if (this.eventListeners.hasOwnProperty(name)) {
var fn = this.eventListeners[name]; var fn = this.eventListeners[name];
if (fn) { if (fn) {
util.removeEventListener(window, name, fn); util.removeEventListener(this.rootNode, name, fn);
} }
delete this.eventListeners[name]; delete this.eventListeners[name];
} }

View File

@ -82,7 +82,8 @@ function JSONEditor (container, options, json) {
'ajv', 'schema', 'schemaRefs','templates', 'ajv', 'schema', 'schemaRefs','templates',
'ace', 'theme','autocomplete', 'ace', 'theme','autocomplete',
'onChange', 'onEditable', 'onError', 'onModeChange', 'onChange', 'onEditable', 'onError', 'onModeChange',
'escapeUnicode', 'history', 'search', 'mode', 'modes', 'name', 'indentation', 'sortObjectKeys' 'escapeUnicode', 'history', 'search', 'mode', 'modes', 'name', 'indentation',
'sortObjectKeys', 'navigationBar', 'statusBar'
]; ];
Object.keys(options).forEach(function (option) { Object.keys(options).forEach(function (option) {

View File

@ -539,6 +539,20 @@ Node.prototype.hideChilds = function() {
}; };
/**
* Goes through the path from the node to the root and ensures that it is expanded
*/
Node.prototype.expandTo = function() {
var currentNode = this.parent;
while (currentNode) {
if (!currentNode.expanded) {
currentNode.expand();
}
currentNode = currentNode.parent;
}
};
/** /**
* Add a new child to the node. * Add a new child to the node.
* Only applicable when Node value is of type array or object * Only applicable when Node value is of type array or object
@ -1998,7 +2012,9 @@ Node.prototype._updateSchema = function () {
//Locating the schema of the node and checking for any enum type //Locating the schema of the node and checking for any enum type
if(this.editor && this.editor.options) { if(this.editor && this.editor.options) {
// find the part of the json schema matching this nodes path // find the part of the json schema matching this nodes path
this.schema = Node._findSchema(this.editor.options.schema, this.getPath()); this.schema = this.editor.options.schema
? Node._findSchema(this.editor.options.schema, this.getPath())
: null;
if (this.schema) { if (this.schema) {
this.enum = Node._findEnum(this.schema); this.enum = Node._findEnum(this.schema);
} }
@ -2040,18 +2056,46 @@ Node._findEnum = function (schema) {
*/ */
Node._findSchema = function (schema, path) { Node._findSchema = function (schema, path) {
var childSchema = schema; var childSchema = schema;
var foundSchema = childSchema;
var allSchemas = schema.oneOf || schema.anyOf || schema.allOf;
if (!allSchemas) {
allSchemas = [schema];
}
for (var j = 0; j < allSchemas.length; j++) {
childSchema = allSchemas[j];
for (var i = 0; i < path.length && childSchema; i++) { for (var i = 0; i < path.length && childSchema; i++) {
var key = path[i]; var key = path[i];
if (typeof key === 'string' && childSchema.properties) {
childSchema = childSchema.properties[key] || null if (typeof key === 'string' && childSchema.patternProperties && i == path.length - 1) {
for (var prop in childSchema.patternProperties) {
foundSchema = Node._findSchema(childSchema.patternProperties[prop], path.slice(i, path.length));
}
}
else if (childSchema.items && childSchema.items.properties) {
childSchema = childSchema.items.properties[key];
if (childSchema) {
foundSchema = Node._findSchema(childSchema, path.slice(i, path.length));
}
}
else if (typeof key === 'string' && childSchema.properties) {
childSchema = childSchema.properties[key] || null;
if (childSchema) {
foundSchema = Node._findSchema(childSchema, path.slice(i, path.length));
}
} }
else if (typeof key === 'number' && childSchema.items) { else if (typeof key === 'number' && childSchema.items) {
childSchema = childSchema.items childSchema = childSchema.items;
if (childSchema) {
foundSchema = Node._findSchema(childSchema, path.slice(i, path.length));
}
} }
} }
return childSchema }
return foundSchema
}; };
/** /**
@ -2284,8 +2328,10 @@ Node.prototype.onEvent = function (event) {
break; break;
case 'click': case 'click':
if (event.ctrlKey || !this.editable.value) { if (event.ctrlKey && this.editable.value) {
// if read-only, we use the regular click behavior of an anchor
if (util.isUrl(this.value)) { if (util.isUrl(this.value)) {
event.preventDefault();
window.open(this.value, '_blank'); window.open(this.value, '_blank');
} }
} }

107
src/js/TreePath.js Normal file
View File

@ -0,0 +1,107 @@
'use strict';
var ContextMenu = require('./ContextMenu');
/**
* Creates a component that visualize path selection in tree based editors
* @param {HTMLElement} container
* @constructor
*/
function TreePath(container) {
if (container) {
this.path = document.createElement('div');
this.path.className = 'jsoneditor-treepath';
container.appendChild(this.path);
this.reset();
}
};
/**
* Reset component to initial status
*/
TreePath.prototype.reset = function () {
this.path.innerHTML = '';
}
/**
* Renders the component UI according to a given path objects
* @param {Array<name: String, childs: Array>} pathObjs a list of path objects
*
*/
TreePath.prototype.setPath = function (pathObjs) {
var me = this;
this.reset();
if (pathObjs && pathObjs.length) {
pathObjs.forEach(function (pathObj, idx) {
var pathEl = document.createElement('span');
var sepEl;
pathEl.className = 'jsoneditor-treepath-element';
pathEl.innerText = pathObj.name;
pathEl.onclick = _onSegmentClick.bind(me, pathObj);
me.path.appendChild(pathEl);
if (pathObj.children.length) {
sepEl = document.createElement('span');
sepEl.className = 'jsoneditor-treepath-seperator';
sepEl.innerHTML = '&#9658;';
sepEl.onclick = function () {
var items = [];
pathObj.children.forEach(function (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)
});
});
var menu = new ContextMenu(items);
menu.show(sepEl);
};
me.path.appendChild(sepEl, me.container);
}
if(idx === pathObjs.length - 1) {
var leftRectPos = (sepEl || pathEl).getBoundingClientRect().left;
if(me.path.offsetWidth < leftRectPos) {
me.path.scrollLeft = leftRectPos;
}
}
});
}
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
*/
TreePath.prototype.onSectionSelected = function (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
*/
TreePath.prototype.onContextMenuItemSelected = function (callback) {
if (typeof callback === 'function') {
this.contextMenuCallback = callback;
}
};
module.exports = TreePath;

View File

@ -1,4 +1,4 @@
'use strict'; 'use strict';
function completely(config) { function completely(config) {
config = config || {}; config = config || {};

View File

@ -36,6 +36,11 @@ var DEFAULT_THEME = 'ace/theme/jsoneditor';
textmode.create = function (container, options) { textmode.create = function (container, options) {
// read options // read options
options = options || {}; options = options || {};
if(typeof options.statusBar === 'undefined') {
options.statusBar = true;
}
this.options = options; this.options = options;
// indentation // indentation
@ -131,6 +136,22 @@ textmode.create = function (container, options) {
} }
}; };
// create repair button
var buttonRepair = document.createElement('button');
buttonRepair.type = 'button';
buttonRepair.className = 'jsoneditor-repair';
buttonRepair.title = 'Repair JSON: fix quotes and escape characters, remove comments and JSONP notation, turn JavaScript objects into JSON.';
this.menu.appendChild(buttonRepair);
buttonRepair.onclick = function () {
try {
me.repair();
me._onChange();
}
catch (err) {
me._onError(err);
}
};
// create mode box // create mode box
if (this.options && this.options.modes && this.options.modes.length) { 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) { this.modeSwitcher = new ModeSwitcher(this.menu, this.options.modes, this.options.mode, function onSwitch(mode) {
@ -200,6 +221,7 @@ textmode.create = function (container, options) {
// register onchange event // register onchange event
aceEditor.on('change', this._onChange.bind(this)); aceEditor.on('change', this._onChange.bind(this));
aceEditor.on('changeSelection', this._onSelect.bind(this));
} }
else { else {
// load a plain text textarea // load a plain text textarea
@ -218,6 +240,62 @@ textmode.create = function (container, options) {
// oninput is undefined. For IE8- // oninput is undefined. For IE8-
this.textarea.onchange = this._onChange.bind(this); 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);
}
if (options.statusBar) {
if (this.mode === 'code') {
util.addClassName(this.content, 'has-status-bar');
this.curserInfoElements = {};
var statusBar = document.createElement('div');
statusBar.className = 'jsoneditor-statusbar';
this.frame.appendChild(statusBar);
var lnLabel = document.createElement('span');
lnLabel.className = 'jsoneditor-curserinfo-label';
lnLabel.innerText = 'Ln:';
var lnVal = document.createElement('span');
lnVal.className = 'jsoneditor-curserinfo-val';
lnVal.innerText = 0;
statusBar.appendChild(lnLabel);
statusBar.appendChild(lnVal);
var colLabel = document.createElement('span');
colLabel.className = 'jsoneditor-curserinfo-label';
colLabel.innerText = 'Col:';
var colVal = document.createElement('span');
colVal.className = 'jsoneditor-curserinfo-val';
colVal.innerText = 0;
statusBar.appendChild(colLabel);
statusBar.appendChild(colVal);
this.curserInfoElements.colVal = colVal;
this.curserInfoElements.lnVal = lnVal;
var countLabel = document.createElement('span');
countLabel.className = 'jsoneditor-curserinfo-label';
countLabel.innerText = 'characters selected';
countLabel.style.display = 'none';
var 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);
}
} }
this.setSchema(this.options.schema, this.options.schemaRefs); this.setSchema(this.options.schema, this.options.schemaRefs);
@ -244,6 +322,29 @@ textmode._onChange = function () {
} }
}; };
/**
* Handle text selection
* Calculates the cursor position and selection range and updates menu
* @private
*/
textmode._onSelect = function () {
if(this.options.statusBar) {
if (this.textarea) {
var selectionRange = util.getInputSelection(this.textarea);
if (selectionRange.start !== selectionRange.end) {
this._setSelectionCountDisplay(Math.abs(selectionRange.end - selectionRange.start));
}
} else if (this.aceEditor && this.curserInfoElements) {
var curserPos = this.aceEditor.getCursorPosition();
var selectedText = this.aceEditor.getSelectedText();
this.curserInfoElements.lnVal.innerText = curserPos.row + 1;
this.curserInfoElements.colVal.innerText = curserPos.column + 1;
this._setSelectionCountDisplay(selectedText.length);
}
}
};
/** /**
* Event handler for keydown. Handles shortcut keys * Event handler for keydown. Handles shortcut keys
* @param {Event} event * @param {Event} event
@ -269,6 +370,39 @@ textmode._onKeyDown = function (event) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
} }
this._setSelectionCountDisplay();
};
/**
* Event handler for mousedown.
* @param {Event} event
* @private
*/
textmode._onMouseDown = function (event) {
this._setSelectionCountDisplay();
};
/**
* Event handler for blur.
* @param {Event} event
* @private
*/
textmode._onBlur = function (event) {
this._setSelectionCountDisplay();
};
textmode._setSelectionCountDisplay = function (value) {
if (this.options.statusBar && this.curserInfoElements) {
if (value && this.curserInfoElements && this.curserInfoElements.countVal) {
this.curserInfoElements.countVal.innerText = value;
this.curserInfoElements.countVal.style.display = 'inline';
this.curserInfoElements.countLabel.style.display = 'inline';
} else {
this.curserInfoElements.countVal.style.display = 'none';
this.curserInfoElements.countLabel.style.display = 'none';
}
}
}; };
/** /**
@ -296,7 +430,7 @@ textmode.destroy = function () {
}; };
/** /**
* Compact the code in the formatter * Compact the code in the text editor
*/ */
textmode.compact = function () { textmode.compact = function () {
var json = this.get(); var json = this.get();
@ -305,7 +439,7 @@ textmode.compact = function () {
}; };
/** /**
* Format the code in the formatter * Format the code in the text editor
*/ */
textmode.format = function () { textmode.format = function () {
var json = this.get(); var json = this.get();
@ -313,6 +447,15 @@ textmode.format = function () {
this.setText(text); this.setText(text);
}; };
/**
* Repair the code in the text editor
*/
textmode.repair = function () {
var text = this.getText();
var sanitizedText = util.sanitize(text);
this.setText(sanitizedText);
};
/** /**
* Set focus to the formatter * Set focus to the formatter
*/ */
@ -405,7 +548,6 @@ textmode.setText = function(jsonText) {
this.options.onChange = originalOnChange; this.options.onChange = originalOnChange;
} }
// validate JSON schema // validate JSON schema
this.validate(); this.validate();
}; };

View File

@ -5,6 +5,7 @@ var Highlighter = require('./Highlighter');
var History = require('./History'); var History = require('./History');
var SearchBox = require('./SearchBox'); var SearchBox = require('./SearchBox');
var ContextMenu = require('./ContextMenu'); var ContextMenu = require('./ContextMenu');
var TreePath = require('./TreePath');
var Node = require('./Node'); var Node = require('./Node');
var ModeSwitcher = require('./ModeSwitcher'); var ModeSwitcher = require('./ModeSwitcher');
var util = require('./util'); var util = require('./util');
@ -113,7 +114,8 @@ treemode._setOptions = function (options) {
name: undefined, // field name of root node name: undefined, // field name of root node
schema: null, schema: null,
schemaRefs: null, schemaRefs: null,
autocomplete: null autocomplete: null,
navigationBar : true
}; };
// copy all options // copy all options
@ -757,6 +759,17 @@ treemode._createFrame = function () {
if (this.options.search) { if (this.options.search) {
this.searchBox = new SearchBox(this, this.menu); 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.treePath.onSectionSelected(this._onTreePathSectionSelected.bind(this));
this.treePath.onContextMenuItemSelected(this._onTreePathMenuItemSelected.bind(this));
}
}; };
/** /**
@ -810,6 +823,10 @@ treemode._onEvent = function (event) {
var node = Node.getNodeFromTarget(event.target); var node = Node.getNodeFromTarget(event.target);
if (this.options && this.options.navigationBar && node && (event.type == 'keydown' || event.type == 'mousedown')) {
this._updateTreePath(node.getNodePath());
}
if (node && node.selected) { if (node && node.selected) {
if (event.type == 'click') { if (event.type == 'click') {
if (event.target == node.dom.menu) { if (event.target == node.dom.menu) {
@ -850,6 +867,73 @@ treemode._onEvent = function (event) {
} }
}; };
/**
* Update TreePath components
* @param {Array<Node>} pathNodes list of nodes in path from root to selection
* @private
*/
treemode._updateTreePath = function (pathNodes) {
if (pathNodes && pathNodes.length) {
util.removeClassName(this.navBar, 'nav-bar-empty');
var pathObjs = [];
pathNodes.forEach(function (node) {
var pathObj = {
name: getName(node),
node: node,
children: []
}
if (node.childs && node.childs.length) {
node.childs.forEach(function (childNode) {
pathObj.children.push({
name: getName(childNode),
node: childNode
});
});
}
pathObjs.push(pathObj);
});
this.treePath.setPath(pathObjs);
} else {
util.addClassName(this.navBar, 'nav-bar-empty');
}
function getName(node) {
return node.field || (isNaN(node.index) ? node.type : node.index);
}
};
/**
* 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 = function (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) {
var selectionObj = pathObj.children.find(function (obj) {
return obj.name === selection;
});
if(selectionObj && selectionObj.node) {
this._updateTreePath(selectionObj.node.getNodePath());
selectionObj.node.expandTo();
selectionObj.node.focus();
}
}
};
treemode._startDragDistance = function (event) { treemode._startDragDistance = function (event) {
this.dragDistanceEvent = { this.dragDistanceEvent = {
initialTarget: event.target, initialTarget: event.target,
@ -1162,6 +1246,9 @@ treemode._onKeyDown = function (event) {
treemode._createTable = function () { treemode._createTable = function () {
var contentOuter = document.createElement('div'); var contentOuter = document.createElement('div');
contentOuter.className = 'jsoneditor-outer'; contentOuter.className = 'jsoneditor-outer';
if(this.options.navigationBar) {
util.addClassName(contentOuter, 'has-nav-bar');
}
this.contentOuter = contentOuter; this.contentOuter = contentOuter;
this.content = document.createElement('div'); this.content = document.createElement('div');

View File

@ -50,6 +50,15 @@ exports.sanitize = function (jsString) {
'\t': '\\t' '\t': '\\t'
}; };
var quote = '\'';
var quoteDbl = '"';
var quoteLeft = '\u2018';
var quoteRight = '\u2019';
var quoteDblLeft = '\u201C';
var quoteDblRight = '\u201D';
var graveAccent = '\u0060';
var acuteAccent = '\u00B4';
// helper functions to get the current/prev/next character // helper functions to get the current/prev/next character
function curr () { return jsString.charAt(i); } function curr () { return jsString.charAt(i); }
function next() { return jsString.charAt(i + 1); } function next() { return jsString.charAt(i + 1); }
@ -88,11 +97,11 @@ exports.sanitize = function (jsString) {
} }
// parse single or double quoted string // parse single or double quoted string
function parseString(quote) { function parseString(endQuote) {
chars.push('"'); chars.push('"');
i++; i++;
var c = curr(); var c = curr();
while (i < jsString.length && c !== quote) { while (i < jsString.length && c !== endQuote) {
if (c === '"' && prev() !== '\\') { if (c === '"' && prev() !== '\\') {
// unescaped double quote, escape it // unescaped double quote, escape it
chars.push('\\"'); chars.push('\\"');
@ -118,7 +127,7 @@ exports.sanitize = function (jsString) {
i++; i++;
c = curr(); c = curr();
} }
if (c === quote) { if (c === endQuote) {
chars.push('"'); chars.push('"');
i++; i++;
} }
@ -154,8 +163,25 @@ exports.sanitize = function (jsString) {
else if (c === '/' && next() === '/') { else if (c === '/' && next() === '/') {
skipComment(); skipComment();
} }
else if (c === '\'' || c === '"') { else if (c === '\u00A0' || (c >= '\u2000' && c <= '\u200A') || c === '\u202F' || c === '\u205F' || c === '\u3000') {
parseString(c); // special white spaces (like non breaking space)
chars.push(' ')
i++
}
else if (c === quote) {
parseString(quote);
}
else if (c === quoteDbl) {
parseString(quoteDbl);
}
else if (c === graveAccent) {
parseString(acuteAccent);
}
else if (c === quoteLeft) {
parseString(quoteRight);
}
else if (c === quoteDblLeft) {
parseString(quoteDblRight);
} }
else if (/[a-zA-Z_$]/.test(c) && ['{', ','].indexOf(lastNonWhitespace()) !== -1) { else if (/[a-zA-Z_$]/.test(c) && ['{', ','].indexOf(lastNonWhitespace()) !== -1) {
// an unquoted object key (like a in '{a:2}') // an unquoted object key (like a in '{a:2}')
@ -790,10 +816,64 @@ exports.textDiff = function textDiff(oldText, newText) {
return {start: start, end: newEnd}; return {start: 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.
**/
exports.getInputSelection = function(el) {
var start = 0, end = 0, normalizedValue, range, textInputRange, len, endRange;
if (typeof el.selectionStart == "number" && typeof el.selectionEnd == "number") {
start = el.selectionStart;
end = 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 start and end 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) {
start = end = len;
} else {
start = -textInputRange.moveStart("character", -len);
start += normalizedValue.slice(0, start).split("\n").length - 1;
if (textInputRange.compareEndPoints("EndToEnd", endRange) > -1) {
end = len;
} else {
end = -textInputRange.moveEnd("character", -len);
end += normalizedValue.slice(0, end).split("\n").length - 1;
}
}
}
}
return {
start: start,
end: end
};
}
if (typeof Element !== 'undefined') { if (typeof Element !== 'undefined') {
// Polyfill for array remove // Polyfill for array remove
(function (arr) { (function () {
arr.forEach(function (item) { function polyfill (item) {
if (item.hasOwnProperty('remove')) { if (item.hasOwnProperty('remove')) {
return; return;
} }
@ -806,8 +886,12 @@ if (typeof Element !== 'undefined') {
this.parentNode.removeChild(this); this.parentNode.removeChild(this);
} }
}); });
}); }
})([Element.prototype, CharacterData.prototype, DocumentType.prototype]);
if (typeof Element !== 'undefined') { polyfill(Element.prototype); }
if (typeof CharacterData !== 'undefined') { polyfill(CharacterData.prototype); }
if (typeof DocumentType !== 'undefined') { polyfill(DocumentType.prototype); }
})();
} }
@ -818,3 +902,15 @@ if (!String.prototype.startsWith) {
return this.substr(position, searchString.length) === searchString; return this.substr(position, searchString.length) === searchString;
}; };
} }
// Polyfill for Array.find
if (!Array.prototype.find) {
Array.prototype.find = function(callback) {
for (var i = 0; i < this.length; i++) {
var element = this[i];
if ( callback.call(this, element, i, this) ) {
return element;
}
}
}
}

View File

@ -14,6 +14,8 @@ describe('util', function () {
it('should replace JavaScript with JSON', function () { it('should replace JavaScript with JSON', function () {
assert.equal(util.sanitize('{a:2}'), '{"a":2}'); assert.equal(util.sanitize('{a:2}'), '{"a":2}');
assert.equal(util.sanitize('{a: 2}'), '{"a": 2}');
assert.equal(util.sanitize('{\n a: 2\n}'), '{\n "a": 2\n}');
assert.equal(util.sanitize('{\'a\':2}'), '{"a":2}'); assert.equal(util.sanitize('{\'a\':2}'), '{"a":2}');
assert.equal(util.sanitize('{a:\'foo\'}'), '{"a":"foo"}'); assert.equal(util.sanitize('{a:\'foo\'}'), '{"a":"foo"}');
assert.equal(util.sanitize('{a:\'foo\',b:\'bar\'}'), '{"a":"foo","b":"bar"}'); assert.equal(util.sanitize('{a:\'foo\',b:\'bar\'}'), '{"a":"foo","b":"bar"}');
@ -30,6 +32,11 @@ describe('util', function () {
assert.equal(util.sanitize('"foo\\\'bar"'), '"foo\'bar"'); assert.equal(util.sanitize('"foo\\\'bar"'), '"foo\'bar"');
}); });
it('should replace special white characters', function () {
assert.equal(util.sanitize('{"a":\u00a0"foo\u00a0bar"}'), '{"a": "foo\u00a0bar"}');
assert.equal(util.sanitize('{"a":\u2009"foo"}'), '{"a": "foo"}');
});
it('should escape unescaped control characters', function () { it('should escape unescaped control characters', function () {
assert.equal(util.sanitize('"hello\bworld"'), '"hello\\bworld"') assert.equal(util.sanitize('"hello\bworld"'), '"hello\\bworld"')
assert.equal(util.sanitize('"hello\fworld"'), '"hello\\fworld"') assert.equal(util.sanitize('"hello\fworld"'), '"hello\\fworld"')
@ -39,6 +46,12 @@ describe('util', function () {
assert.equal(util.sanitize('{"value\n": "dc=hcm,dc=com"}'), '{"value\\n": "dc=hcm,dc=com"}') assert.equal(util.sanitize('{"value\n": "dc=hcm,dc=com"}'), '{"value\\n": "dc=hcm,dc=com"}')
}) })
it('should replace left/right quotes', function () {
assert.equal(util.sanitize('\u2018foo\u2019'), '"foo"')
assert.equal(util.sanitize('\u201Cfoo\u201D'), '"foo"')
assert.equal(util.sanitize('\u0060foo\u00B4'), '"foo"')
})
it('remove comments', function () { it('remove comments', function () {
assert.equal(util.sanitize('/* foo */ {}'), ' {}'); assert.equal(util.sanitize('/* foo */ {}'), ' {}');
assert.equal(util.sanitize('/* foo */ {}'), ' {}'); assert.equal(util.sanitize('/* foo */ {}'), ' {}');