Implement color picker. Expose `VanillaPicker`, `ace`, and `Ajv`.
This commit is contained in:
parent
4f72c5e113
commit
b853ec3f64
|
@ -3,6 +3,12 @@
|
|||
https://github.com/josdejong/jsoneditor
|
||||
|
||||
|
||||
## not yet released, version 5.24.0
|
||||
|
||||
- Implemented a color picker, and allow hooking in a custom color
|
||||
picker. new options are `colorPicker` and `onColorPicker`.
|
||||
|
||||
|
||||
## 2018-08-17, version 5.23.1
|
||||
|
||||
- Fixed #566: transform function broken, regression since `v5.20.0`.
|
||||
|
|
21
README.md
21
README.md
|
@ -24,10 +24,11 @@ Cross browser testing for JSONEditor is generously provided by <a href="https://
|
|||
## Features
|
||||
|
||||
### Tree editor
|
||||
- Edit, add, move, remove, and duplicate fields and values.
|
||||
- Change type of values.
|
||||
- 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)).
|
||||
|
@ -36,10 +37,12 @@ Cross browser testing for JSONEditor is generously provided by <a href="https://
|
|||
- 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 editor
|
||||
- Format and compact JSON.
|
||||
- Repair JSON.
|
||||
- JSON schema validation (powered by [ajv](https://github.com/epoberezkin/ajv)).
|
||||
|
||||
|
||||
|
@ -65,18 +68,6 @@ with bower:
|
|||
bower install jsoneditor
|
||||
|
||||
|
||||
#### More
|
||||
|
||||
|
||||
There is a directive available for using JSONEditor in AngularJS:
|
||||
|
||||
[https://github.com/isonet/angular-jsoneditor](https://github.com/isonet/angular-jsoneditor)
|
||||
|
||||
Directive for Angular 5.x as well:
|
||||
|
||||
[https://github.com/mariohmol/ang-jsoneditor](https://github.com/mariohmol/ang-jsoneditor)
|
||||
|
||||
|
||||
## Use
|
||||
|
||||
```html
|
||||
|
@ -158,7 +149,7 @@ To create a custom bundle of the source code using browserify:
|
|||
|
||||
browserify ./index.js -o ./jsoneditor.custom.js -s JSONEditor
|
||||
|
||||
The Ace editor, used in mode `code`, accounts for about 75% of the total
|
||||
The Ace editor, used in mode `code`, accounts for about one third of the total
|
||||
size of the library. To exclude the Ace editor from the bundle:
|
||||
|
||||
browserify ./index.js -o ./jsoneditor.custom.js -s JSONEditor -x brace -x brace/mode/json -x brace/ext/searchbox
|
||||
|
|
52
docs/api.md
52
docs/api.md
|
@ -300,6 +300,39 @@ Constructs a new JSONEditor.
|
|||
```
|
||||
Only applicable when `mode` is 'form', 'tree' or 'view'.
|
||||
|
||||
- `{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();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
- `{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`, `pt-BR`. Other translations can be specified via the option `languages`.
|
||||
|
@ -543,12 +576,29 @@ valid JSON and the editor is in mode `tree`, `view`, or `form`.
|
|||
|
||||
Contents of the editor as string.
|
||||
|
||||
### Constants
|
||||
### 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:
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
var json = {
|
||||
'array': [1, 2, 3],
|
||||
'boolean': true,
|
||||
'color': '#82B92C',
|
||||
'color': '#82b92c',
|
||||
'null': null,
|
||||
'number': 123,
|
||||
'object': {'a': 'b', 'c': 'd'},
|
||||
|
|
|
@ -61,7 +61,8 @@ var compilerMinimalist = webpack({
|
|||
plugins: [
|
||||
bannerPlugin,
|
||||
new webpack.IgnorePlugin(new RegExp('^brace$')),
|
||||
new webpack.IgnorePlugin(new RegExp('^ajv'))
|
||||
new webpack.IgnorePlugin(new RegExp('^ajv')),
|
||||
new webpack.IgnorePlugin(new RegExp('^vanilla-picker$'))
|
||||
],
|
||||
cache: true
|
||||
});
|
||||
|
|
|
@ -4,6 +4,11 @@
|
|||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
"@sphinxxxx/color-conversion": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@sphinxxxx/color-conversion/-/color-conversion-2.1.1.tgz",
|
||||
"integrity": "sha1-2igalkHrP2mZeUMv5TG7pSDj/7s="
|
||||
},
|
||||
"Base64": {
|
||||
"version": "0.2.1",
|
||||
"resolved": "https://registry.npmjs.org/Base64/-/Base64-0.2.1.tgz",
|
||||
|
@ -810,6 +815,11 @@
|
|||
"integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==",
|
||||
"dev": true
|
||||
},
|
||||
"drag-tracker": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/drag-tracker/-/drag-tracker-1.0.0.tgz",
|
||||
"integrity": "sha1-m9M9OAvDBW22m9Wzz24GL+xYvWQ="
|
||||
},
|
||||
"duplexer2": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.0.2.tgz",
|
||||
|
@ -4220,6 +4230,15 @@
|
|||
"user-home": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"vanilla-picker": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/vanilla-picker/-/vanilla-picker-2.3.0.tgz",
|
||||
"integrity": "sha512-vteG997jlsJSNoPHOLoxThv6R1N8ZCbVTk5K3i5SXUolUQ5O26fG4NhxjSvpZeXbeiG0aVlg/cyQvGD7irJr0Q==",
|
||||
"requires": {
|
||||
"@sphinxxxx/color-conversion": "^2.1.1",
|
||||
"drag-tracker": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"vinyl": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/vinyl/-/vinyl-0.5.3.tgz",
|
||||
|
|
|
@ -29,7 +29,8 @@
|
|||
"jmespath": "0.15.0",
|
||||
"json-source-map": "^0.4.0",
|
||||
"mobius1-selectr": "2.4.1",
|
||||
"picomodal": "3.0.0"
|
||||
"picomodal": "3.0.0",
|
||||
"vanilla-picker": "2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"gulp": "3.9.1",
|
||||
|
|
|
@ -124,7 +124,7 @@ div.jsoneditor-value.jsoneditor-invalid {
|
|||
|
||||
|
||||
|
||||
div.jsoneditor-tree button {
|
||||
div.jsoneditor-tree button.jsoneditor-button {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
|
@ -162,7 +162,7 @@ div.jsoneditor-tree *:focus {
|
|||
outline: none;
|
||||
}
|
||||
|
||||
div.jsoneditor-tree button:focus {
|
||||
div.jsoneditor-tree button.jsoneditor-button:focus {
|
||||
/* TODO: nice outline for buttons with focus
|
||||
outline: #97B0F8 solid 2px;
|
||||
box-shadow: 0 0 8px #97B0F8;
|
||||
|
@ -206,6 +206,12 @@ div.jsoneditor-tree div.jsoneditor-color {
|
|||
margin: 4px;
|
||||
|
||||
border: 1px solid #808080;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
div.jsoneditor-tree div.jsoneditor-color .picker_wrapper.popup.popup_bottom {
|
||||
top: 28px;
|
||||
left: -10px;
|
||||
}
|
||||
|
||||
div.jsoneditor {
|
||||
|
|
|
@ -21,9 +21,10 @@ 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 160 kB to about 40 kB.
|
||||
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?
|
||||
|
||||
|
@ -31,6 +32,8 @@ When to use the minimalist version?
|
|||
- 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?
|
||||
|
||||
|
|
|
@ -8,6 +8,9 @@ catch (err) {
|
|||
// no problem... when we need Ajv we will throw a neat exception
|
||||
}
|
||||
|
||||
var ace = require('./ace'); // may be undefined in case of minimalist bundle
|
||||
var VanillaPicker = require('./vanilla-picker'); // may be undefined in case of minimalist bundle
|
||||
|
||||
var treemode = require('./treemode');
|
||||
var textmode = require('./textmode');
|
||||
var util = require('./util');
|
||||
|
@ -442,4 +445,9 @@ JSONEditor.registerMode = function (mode) {
|
|||
JSONEditor.registerMode(treemode);
|
||||
JSONEditor.registerMode(textmode);
|
||||
|
||||
// expose some of the libraries that can be used customized
|
||||
JSONEditor.ace = ace;
|
||||
JSONEditor.Ajv = Ajv;
|
||||
JSONEditor.VanillaPicker = VanillaPicker;
|
||||
|
||||
module.exports = JSONEditor;
|
||||
|
|
|
@ -758,7 +758,7 @@ Node.prototype.expand = function(recurse) {
|
|||
// set this node expanded
|
||||
this.expanded = true;
|
||||
if (this.dom.expand) {
|
||||
this.dom.expand.className = 'jsoneditor-expanded';
|
||||
this.dom.expand.className = 'jsoneditor-button jsoneditor-expanded';
|
||||
}
|
||||
|
||||
this.showChilds();
|
||||
|
@ -792,7 +792,7 @@ Node.prototype.collapse = function(recurse) {
|
|||
|
||||
// make this node collapsed
|
||||
if (this.dom.expand) {
|
||||
this.dom.expand.className = 'jsoneditor-collapsed';
|
||||
this.dom.expand.className = 'jsoneditor-button jsoneditor-collapsed';
|
||||
}
|
||||
this.expanded = false;
|
||||
};
|
||||
|
@ -1737,7 +1737,11 @@ Node.prototype._updateDomValue = function () {
|
|||
}
|
||||
|
||||
// show color picker when value is a color
|
||||
if (this.editable.value && typeof value === 'string' && util.isValidColor(value)) {
|
||||
if (this.editable.value &&
|
||||
this.editor.options.colorPicker &&
|
||||
typeof value === 'string' &&
|
||||
util.isValidColor(value)) {
|
||||
|
||||
if (!this.dom.color) {
|
||||
this.dom.color = document.createElement('div');
|
||||
this.dom.color.className = 'jsoneditor-color';
|
||||
|
@ -1754,11 +1758,7 @@ Node.prototype._updateDomValue = function () {
|
|||
}
|
||||
else {
|
||||
// cleanup color picker when displayed
|
||||
if (this.dom.color) {
|
||||
this.dom.tdColor.parentNode.removeChild(this.dom.tdColor);
|
||||
delete this.dom.tdColor;
|
||||
delete this.dom.color;
|
||||
}
|
||||
this._deleteDomColor();
|
||||
}
|
||||
|
||||
// strip formatting from the contents of the editable div
|
||||
|
@ -1766,6 +1766,14 @@ Node.prototype._updateDomValue = function () {
|
|||
}
|
||||
};
|
||||
|
||||
Node.prototype._deleteDomColor = function () {
|
||||
if (this.dom.color) {
|
||||
this.dom.tdColor.parentNode.removeChild(this.dom.tdColor);
|
||||
delete this.dom.tdColor;
|
||||
delete this.dom.color;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update dom field:
|
||||
* - the text color of the field, depending on the text
|
||||
|
@ -1918,7 +1926,7 @@ Node.prototype.getDom = function() {
|
|||
var domDrag = document.createElement('button');
|
||||
domDrag.type = 'button';
|
||||
dom.drag = domDrag;
|
||||
domDrag.className = 'jsoneditor-dragarea';
|
||||
domDrag.className = 'jsoneditor-button jsoneditor-dragarea';
|
||||
domDrag.title = translate('drag');
|
||||
tdDrag.appendChild(domDrag);
|
||||
}
|
||||
|
@ -1930,7 +1938,7 @@ Node.prototype.getDom = function() {
|
|||
var menu = document.createElement('button');
|
||||
menu.type = 'button';
|
||||
dom.menu = menu;
|
||||
menu.className = 'jsoneditor-contextmenu';
|
||||
menu.className = 'jsoneditor-button jsoneditor-contextmenu';
|
||||
menu.title = translate('actionsMenu');
|
||||
tdMenu.appendChild(dom.menu);
|
||||
dom.tr.appendChild(tdMenu);
|
||||
|
@ -2638,11 +2646,13 @@ Node.prototype._createDomExpandButton = function () {
|
|||
var expand = document.createElement('button');
|
||||
expand.type = 'button';
|
||||
if (this._hasChilds()) {
|
||||
expand.className = this.expanded ? 'jsoneditor-expanded' : 'jsoneditor-collapsed';
|
||||
expand.className = this.expanded
|
||||
? 'jsoneditor-button jsoneditor-expanded'
|
||||
: 'jsoneditor-button jsoneditor-collapsed';
|
||||
expand.title = translate('expandTitle');
|
||||
}
|
||||
else {
|
||||
expand.className = 'jsoneditor-invisible';
|
||||
expand.className = 'jsoneditor-button jsoneditor-invisible';
|
||||
expand.title = '';
|
||||
}
|
||||
|
||||
|
@ -2753,6 +2763,10 @@ Node.prototype.onEvent = function (event) {
|
|||
}
|
||||
}
|
||||
|
||||
if (type === 'click' && (event.target === node.dom.tdColor || event.target === node.dom.color)) {
|
||||
this._showColorPicker();
|
||||
}
|
||||
|
||||
// swap the value of a boolean when the checkbox displayed left is clicked
|
||||
if (type == 'change' && target == dom.checkbox) {
|
||||
this.dom.value.innerHTML = !this.value;
|
||||
|
@ -3300,6 +3314,31 @@ Node.prototype._onExpand = function (recurse) {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Open a color picker to select a new color
|
||||
* @private
|
||||
*/
|
||||
Node.prototype._showColorPicker = function () {
|
||||
if (typeof this.editor.options.onColorPicker === 'function' && this.dom.color) {
|
||||
var node = this;
|
||||
|
||||
// force deleting current color picker (if any)
|
||||
node._deleteDomColor();
|
||||
node.updateDom();
|
||||
|
||||
this.editor.options.onColorPicker(this.dom.color, 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._onChangeValue();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove nodes
|
||||
* @param {Node[] | Node} nodes
|
||||
|
@ -3762,7 +3801,7 @@ Node.prototype.getShowMoreDom = function () {
|
|||
|
||||
/**
|
||||
* Find the node from an event target
|
||||
* @param {Node} target
|
||||
* @param {HTMLElement} target
|
||||
* @return {Node | undefined} node or undefined when not found
|
||||
* @static
|
||||
*/
|
||||
|
@ -3777,6 +3816,27 @@ Node.getNodeFromTarget = function (target) {
|
|||
return undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Test whether target is a child of the color DOM of a node
|
||||
* @param {HTMLElement} target
|
||||
* @returns {boolean}
|
||||
*/
|
||||
Node.targetIsColorPicker = function (target) {
|
||||
var node = Node.getNodeFromTarget(target);
|
||||
|
||||
if (node) {
|
||||
var 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.
|
||||
|
|
|
@ -55,7 +55,7 @@ function appendNodeFactory(Node) {
|
|||
dom.tdMenu = tdMenu;
|
||||
var menu = document.createElement('button');
|
||||
menu.type = 'button';
|
||||
menu.className = 'jsoneditor-contextmenu';
|
||||
menu.className = 'jsoneditor-button jsoneditor-contextmenu';
|
||||
menu.title = 'Click to open the actions menu (Ctrl+M)';
|
||||
dom.menu = menu;
|
||||
tdMenu.appendChild(dom.menu);
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
'use strict';
|
||||
|
||||
|
||||
var VanillaPicker = require('./vanilla-picker');
|
||||
var Highlighter = require('./Highlighter');
|
||||
var History = require('./History');
|
||||
var SearchBox = require('./SearchBox');
|
||||
|
@ -139,6 +139,27 @@ treemode._setOptions = function (options) {
|
|||
autocomplete: null,
|
||||
navigationBar : true,
|
||||
onSelectionChange: null,
|
||||
colorPicker: true,
|
||||
onColorPicker: function (parent, color, onChange) {
|
||||
if (VanillaPicker) {
|
||||
new VanillaPicker({
|
||||
parent: parent,
|
||||
color: color,
|
||||
popup: 'bottom',
|
||||
onDone: function (color) {
|
||||
var alpha = color.rgba[3]
|
||||
var 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`.')
|
||||
}
|
||||
},
|
||||
onEvent: null
|
||||
};
|
||||
|
||||
|
@ -1073,6 +1094,11 @@ treemode._onRedo = function () {
|
|||
* @private
|
||||
*/
|
||||
treemode._onEvent = function (event) {
|
||||
// don't process events when coming from the color picker
|
||||
if (Node.targetIsColorPicker(event.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.type === 'keydown') {
|
||||
this._onKeyDown(event);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
var VanillaPicker
|
||||
|
||||
if (window.Picker) {
|
||||
// use the already loaded instance of VanillaPicker
|
||||
VanillaPicker = window.Picker
|
||||
}
|
||||
else {
|
||||
try {
|
||||
// load brace
|
||||
VanillaPicker = require('vanilla-picker');
|
||||
}
|
||||
catch (err) {
|
||||
// probably running the minimalist bundle
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = VanillaPicker;
|
|
@ -64,7 +64,7 @@
|
|||
json = {
|
||||
"array": [1, 2, [3,4,5]],
|
||||
"boolean": true,
|
||||
"color": "#82B92C",
|
||||
"color": "#82b92c",
|
||||
"htmlcode": '"',
|
||||
"escaped_unicode": '\\u20b9',
|
||||
"unicode": '\u20b9,\uD83D\uDCA9',
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
json = {
|
||||
"array": [1, 2, 3],
|
||||
"boolean": true,
|
||||
"color": "#82B92C",
|
||||
"color": "#82b92c",
|
||||
"null": null,
|
||||
"number": 123,
|
||||
"object": {"a": "b", "c": "d"},
|
||||
|
|
|
@ -50,7 +50,7 @@
|
|||
json = {
|
||||
"array": [1, 2, 3],
|
||||
"boolean": true,
|
||||
"color": "#82B92C",
|
||||
"color": "#82b92c",
|
||||
"null": null,
|
||||
"number": 123,
|
||||
"object": {"a": "b", "c": "d"},
|
||||
|
|
Loading…
Reference in New Issue