@gravitee/ui-components 3.24.3-migrate-codemirror-f56ff96 → 3.24.4-migrate-codemirror-0dec695

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,10 @@
1
+ ## [3.24.4](https://github.com/gravitee-io/gravitee-ui-components/compare/v3.24.3...v3.24.4) (2021-12-16)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * remove unnecessary perform update ([1b8fee6](https://github.com/gravitee-io/gravitee-ui-components/commit/1b8fee6711bf1af63bf40ca9aca2fac570590076))
7
+
1
8
  ## [3.24.3](https://github.com/gravitee-io/gravitee-ui-components/compare/v3.24.2...v3.24.3) (2021-12-15)
2
9
 
3
10
 
@@ -18,9 +18,6 @@
18
18
  "empty": "No data to display",
19
19
  "error": "An error has occurred"
20
20
  },
21
- "gv-code": {
22
- "copy": "Copy to clipboard"
23
- },
24
21
  "gv-confirm": {
25
22
  "cancel": "No",
26
23
  "ok": "Yes"
@@ -18,9 +18,6 @@
18
18
  "empty": "Aucune donnée à afficher ici",
19
19
  "error": "Une erreur est survenue pendant le chargement"
20
20
  },
21
- "gv-code": {
22
- "copy": "Copier dans le presse papier"
23
- },
24
21
  "gv-confirm": {
25
22
  "cancel": "Non",
26
23
  "ok": "Oui"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gravitee/ui-components",
3
- "version": "3.24.3-migrate-codemirror-f56ff96",
3
+ "version": "3.24.4-migrate-codemirror-0dec695",
4
4
  "description": "Gravitee.io UI Components library, based on Web Components",
5
5
  "repository": {
6
6
  "type": "git",
@@ -99,7 +99,6 @@
99
99
  "babel-plugin-template-html-minifier": "4.1.0",
100
100
  "bundle-phobia-cli": "0.14.13",
101
101
  "chromatic": "6.1.0",
102
- "codemirror-asciidoc": "1.0.4",
103
102
  "danger": "10.7.1",
104
103
  "del": "6.0.0",
105
104
  "eslint": "8.4.0",
@@ -146,7 +145,6 @@
146
145
  "@highcharts/map-collection": "^1.0.0",
147
146
  "asciidoctor": "^2.2.4",
148
147
  "asciidoctor-highlight.js": "^0.3.0 || ^0.4.0",
149
- "codemirror-asciidoc": "^1.0.4",
150
148
  "highcharts": "^8.0.0 || ^9.0.0",
151
149
  "highlight.js": "^10.7.0 || ^11.1.0"
152
150
  },
@@ -163,9 +161,6 @@
163
161
  "asciidoctor-highlight.js": {
164
162
  "optional": true
165
163
  },
166
- "codemirror-asciidoc": {
167
- "optional": true
168
- },
169
164
  "highcharts": {
170
165
  "optional": true
171
166
  },
@@ -322,14 +322,6 @@ export class GvButton extends LitElement {
322
322
  }
323
323
  }
324
324
 
325
- async performUpdate() {
326
- super.performUpdate();
327
- const icon = this.shadowRoot.querySelector('gv-icon');
328
- if (icon) {
329
- icon.performUpdate();
330
- }
331
- }
332
-
333
325
  render() {
334
326
  const classes = {
335
327
  button: true,
@@ -272,19 +272,7 @@ export class GvInput extends InputElement(LitElement) {
272
272
  firstUpdated(changedProperties) {
273
273
  super.firstUpdated(changedProperties);
274
274
 
275
- const defaultInputElement = this.getInputElement();
276
- for (const node of this.childNodes) {
277
- if (node.nodeType === 1) {
278
- const child = node.nodeName.toLowerCase() === 'input' ? node : node.querySelector('input');
279
- if (child) {
280
- this._input = child;
281
- break;
282
- }
283
- }
284
- }
285
- if (this._input) {
286
- defaultInputElement.remove();
287
- }
275
+ this.catchSlot();
288
276
 
289
277
  if (this.clipboard) {
290
278
  import('clipboard-copy').then(
@@ -304,13 +292,6 @@ export class GvInput extends InputElement(LitElement) {
304
292
 
305
293
  setTimeout(() => {
306
294
  const clipboardPopover = this.shadowRoot.querySelector('.clipboard__popover');
307
- if (this.readonly) {
308
- this.getInputElement().addEventListener('click', (e) => {
309
- e.preventDefault();
310
- e.stopPropagation();
311
- clipboardPopover.click();
312
- });
313
- }
314
295
  clipboardPopover.addEventListener('click', (e) => {
315
296
  e.preventDefault();
316
297
  e.stopPropagation();
@@ -325,11 +306,32 @@ export class GvInput extends InputElement(LitElement) {
325
306
  }
326
307
 
327
308
  this.getInputElement().id = this._id;
309
+
310
+ this.bindInputEvents();
311
+ }
312
+
313
+ bindInputEvents() {
328
314
  this.getInputElement().addEventListener('input', this._onInput.bind(this));
329
315
  this.getInputElement().addEventListener('keyup', this._onKeyUp.bind(this));
330
316
  this.getInputElement().addEventListener('keypress', this._onKeyPress.bind(this));
331
317
  }
332
318
 
319
+ catchSlot() {
320
+ const defaultInputElement = this.getInputElement();
321
+ for (const node of this.childNodes) {
322
+ if (node.nodeType === 1) {
323
+ const child = node.nodeName.toLowerCase() === 'input' ? node : node.querySelector('input');
324
+ if (child) {
325
+ this._input = child;
326
+ break;
327
+ }
328
+ }
329
+ }
330
+ if (this._input) {
331
+ defaultInputElement.remove();
332
+ }
333
+ }
334
+
333
335
  updateState(value) {
334
336
  super.updateState(value);
335
337
  if (this.valid === true && !this.readonly) {
@@ -481,7 +483,7 @@ export class GvInput extends InputElement(LitElement) {
481
483
  return !this.loading;
482
484
  }
483
485
 
484
- _renderIcon() {
486
+ renderIcon() {
485
487
  let shape = null;
486
488
  if ((!this.isPassword && this.icon) || this.iconLeft) {
487
489
  shape = (!this.isPassword && this.icon) || this.iconLeft;
@@ -545,7 +547,7 @@ export class GvInput extends InputElement(LitElement) {
545
547
  ${this.renderLabel()}
546
548
  <input />
547
549
  <slot></slot>
548
- ${this._renderClearIcon()} ${this._renderIcon()} ${this._renderPasswordIcon()}
550
+ ${this._renderClearIcon()} ${this.renderIcon()} ${this._renderPasswordIcon()}
549
551
  </div>
550
552
  ${this.description != null ? html`<div class="description" .innerHTML="${this.description}"></div>` : ''}
551
553
  `;
@@ -52,13 +52,16 @@ export function InputElement(ParentClass) {
52
52
  :host([invalid]) :not(.clipboard) ::slotted(.input),
53
53
  :host([invalid]) :not(.clipboard) .box-icon-left,
54
54
  :host([invalid]) :not(.clipboard) textarea,
55
- :host([invalid]) :not(.clipboard) select {
55
+ :host([invalid]) :not(.clipboard) select,
56
+ :host([invalid]) :not(.clipboard) .input {
56
57
  box-shadow: inset 3px 0 0 var(--gv-input-invalid--bxshc, var(--gv-theme-color-error, #da1a1b));
57
58
  }
58
59
 
59
60
  :host([readonly]) :not(.clipboard) input,
60
61
  :host([readonly]) :not(.clipboard) ::slotted(.input),
61
- :host([readonly]) .textarea :host([readonly]) :not(.clipboard) select {
62
+ :host([readonly]) .textarea,
63
+ :host([readonly]) :not(.clipboard) select,
64
+ :host([readonly]) .input {
62
65
  border-left: 1px solid var(--gv-input--bdc, var(--gv-theme-neutral-color-dark, #d9d9d9));
63
66
  }
64
67
 
@@ -86,7 +89,7 @@ export function InputElement(ParentClass) {
86
89
 
87
90
  setValidity(isInvalid = false, violation = '') {
88
91
  const element = this.getInputElement();
89
- if (element) {
92
+ if (element && element.setCustomValidity) {
90
93
  if (isInvalid) {
91
94
  if (violation == null || violation.trim() === '') {
92
95
  violation = 'field is not valid';
@@ -95,6 +98,7 @@ export function InputElement(ParentClass) {
95
98
  } else {
96
99
  element.setCustomValidity('');
97
100
  }
101
+ element.reportValidity();
98
102
  }
99
103
  this.invalid = isInvalid;
100
104
  this.valid = !this.invalid;
@@ -13,22 +13,20 @@
13
13
  * See the License for the specific language governing permissions and
14
14
  * limitations under the License.
15
15
  */
16
- import { css, LitElement, html } from 'lit';
17
-
18
- import { shapeClipboard, shapeCopied } from '../styles/shapes';
19
- import { dispatchCustomEvent } from '../lib/events';
20
- import { uuid } from '../lib/utils';
21
- import { classMap } from 'lit-html/directives/class-map';
16
+ import { css, html } from 'lit';
22
17
 
23
18
  import { EditorView, basicSetup } from '@codemirror/basic-setup';
24
- import { EditorState, Compartment, Prec, EditorSelection } from '@codemirror/state';
25
- import { autocompletion, startCompletion, pickedCompletion } from '@codemirror/autocomplete';
19
+ import { EditorState, Compartment } from '@codemirror/state';
20
+ import { autocompletion } from '@codemirror/autocomplete';
26
21
  import { json } from '@codemirror/lang-json';
27
22
  import { languages } from '@codemirror/language-data';
28
- import { placeholder, keymap } from '@codemirror/view';
29
- import { get } from 'object-path';
30
- import { InputElement } from '../mixins/input-element';
31
- import { i18n } from '../lib/i18n';
23
+ import { placeholder } from '@codemirror/view';
24
+
25
+ import { shapeClipboard } from '../styles/shapes';
26
+ import { dispatchCustomEvent } from '../lib/events';
27
+ import { uuid } from '../lib/utils';
28
+ import { classMap } from 'lit-html/directives/class-map';
29
+ import { GvInput } from '../atoms/gv-input';
32
30
  import { skeleton } from '../styles/skeleton';
33
31
  import { empty } from '../styles/empty';
34
32
  import { input } from '../styles/input';
@@ -37,8 +35,20 @@ const languageCompartment = new Compartment();
37
35
  const readonlyCompartment = new Compartment();
38
36
  const placeholderCompartment = new Compartment();
39
37
 
38
+ const overrideTheme = EditorView.theme({
39
+ '&.cm-editor.cm-focused': {
40
+ outline: 'none',
41
+ },
42
+ '.cm-gutters': {
43
+ backgroundColor: 'var(--gv-input-icon--bgc, var(--gv-theme-neutral-color, #f5f5f5))',
44
+ color: 'var(--gv-theme-font-color-dark, #262626)',
45
+ borderLeft: '5px solid',
46
+ borderColor: 'var(--gv-input--bdc, var(--gv-theme-neutral-color-dark, #d9d9d9))',
47
+ },
48
+ });
49
+
40
50
  /**
41
- * Code editor component
51
+ * Code component
42
52
  *
43
53
  * ## Details
44
54
  * * has @theme facet
@@ -49,8 +59,7 @@ const placeholderCompartment = new Compartment();
49
59
  *
50
60
  * @attr {String} label - code language
51
61
  * @attr {String} value - code content to be highlighted
52
- * @attr {options} Object - options based on codemirror https://codemirror.net/doc/manual.html#config
53
- * @attr {Object} grammar - The grammar for Expression Language support
62
+ * @attr {options} Object - options based on codemirror
54
63
  * @attr {String} placeholder - an example value to display in the input when empty
55
64
  * @attr {Number} rows - number of rows of the text element
56
65
  * @attr {Boolean} large - for a large input (only if the field has one row)
@@ -61,17 +70,11 @@ const placeholderCompartment = new Compartment();
61
70
  * @attr {Boolean} [autofocus=false] - true to put the focus on the input
62
71
  * @attr {Boolean} [readonly=false] - true if field is readonly mode
63
72
  */
64
- export class GvCode extends InputElement(LitElement) {
73
+ export class GvCode extends GvInput {
65
74
  static get properties() {
66
75
  return {
67
76
  ...super.properties,
68
77
  options: { type: Object },
69
- large: { type: Boolean },
70
- medium: { type: Boolean },
71
- small: { type: Boolean },
72
- clipboard: { type: Boolean },
73
- _clipboardIcon: { type: String },
74
- _codeMirror: { type: Object },
75
78
  rows: { type: Number },
76
79
  _error: { type: String, attribute: false },
77
80
  };
@@ -97,352 +100,165 @@ export class GvCode extends InputElement(LitElement) {
97
100
  };
98
101
  const inputClasses = {
99
102
  content: true,
100
- input: this.singleLine,
103
+ code: !this.hasInputStyle,
104
+ input: this.hasInputStyle,
101
105
  };
102
106
 
103
107
  return html`
104
108
  <div class="${classMap(classes)}">
105
109
  ${this.label ? html`<label for="code">${this.label}</label>` : ''}
106
- <div id="${this._id}" class="${classMap(inputClasses)}">
107
- ${this.clipboard
108
- ? html`<gv-button
109
- title="${i18n('gv-code.copy')}"
110
- ?outlined="${!this._copied}"
111
- ?primary="${this._copied}"
112
- small
113
- icon="${this._clipboardIcon}"
114
- ></gv-button>`
115
- : ''}
116
- </div>
110
+ <div id="${this._id}" class="${classMap(inputClasses)}">${this.renderIcon()}</div>
117
111
  </div>
118
112
  ${this.description != null ? html`<div class="description" .innerHTML="${this.description}"></div>` : ''}
119
113
  `;
120
114
  }
121
115
 
116
+ bindInputEvents() {
117
+ dispatchCustomEvent(this, 'ready', { currentTarget: this });
118
+ }
119
+
122
120
  _onChange() {
123
121
  dispatchCustomEvent(this, 'input', this.value);
124
122
  }
125
123
 
126
- get singleLine() {
124
+ get hasInputStyle() {
127
125
  return this.rows === 1;
128
126
  }
129
127
 
130
- _getModelType(typeId) {
131
- if (this.grammar != null) {
132
- return get(this.grammar, `_types.${typeId}`);
133
- }
134
- return null;
128
+ _autocomplete(completionContext) {
129
+ const { handler, match } = this.findBestAutocompleteHandler(completionContext);
130
+ return handler.run.call(this, completionContext, match);
135
131
  }
136
132
 
137
- _isMap(typeId) {
138
- return ['Map', 'HttpHeaders', 'MultiValueMap'].includes(typeId);
139
- }
140
-
141
- _getEnum(typeId) {
142
- if (this.grammar != null) {
143
- return get(this.grammar, `_enums.${typeId}`);
144
- }
145
- return [''];
146
- }
147
-
148
- _convertType(type) {
149
- if (type) {
150
- const lowerType = type.toLowerCase();
151
- if (lowerType === 'string') {
152
- // class, constant, enum, function, interface, keyword, method, namespace, property, text, type, and variable.
153
- return 'text';
154
- } else if (lowerType === 'httpheaders' || lowerType === 'multivaluemap') {
155
- return 'type';
156
- } else if (lowerType === 'int' || lowerType === 'long') {
157
- return 'variable';
158
- } else if (lowerType.includes('[]')) {
159
- return 'enum';
160
- }
161
- }
162
- return 'variable';
133
+ findBestAutocompleteHandler(completionContext) {
134
+ return { handler: this.defaultCompletionHandler, match: true };
163
135
  }
164
136
 
165
- _autocomplete(ctx) {
166
- const { handler, match } = this._findAutocompleteHandler(ctx);
167
- return handler.run.call(this, ctx, match);
168
- }
169
-
170
- _buildMethods(type) {
171
- const modelType = this._getModelType(type);
172
- return modelType.methods.map(({ name, params = [], returnType }) => {
173
- const command = `${name}()`;
174
- const displayParams = params.map((p) => `${p.type} ${p.name}`);
175
- const label = `${name}(${displayParams.join(', ')})`;
176
- const detail = `return ${returnType}`;
177
- return {
178
- type: 'method',
179
- command,
180
- apply: (view, completion, from, to) => {
181
- const anchor = from + name.length + 1;
182
- view.dispatch({
183
- changes: { from, to, insert: completion.command },
184
- selection: { anchor },
185
- userEvent: 'input.complete',
186
- annotations: pickedCompletion.of(view),
187
- });
188
- },
189
- label,
190
- detail,
191
- };
192
- });
193
- }
194
-
195
- _findAutocompleteHandler(ctx) {
196
- return this._completionHandlers
197
- .filter((handler) => (this.grammar != null && handler.supportEL === true) || handler.supportEL !== true)
198
- .map((handler) => {
199
- const match = handler.expr ? ctx.matchBefore(handler.expr) : true;
200
- return { handler, match };
201
- })
202
- .find(({ match, expr }) => match);
203
- }
204
-
205
- get _completionHandlers() {
206
- return [
207
- {
208
- expr: /{#[a-z]*/,
209
- supportEL: true,
210
- run(ctx, match) {
211
- const prefix = match.text.replaceAll('{#', '');
212
- const from = ctx.pos - prefix.length;
213
- const options = Object.keys(this.grammar)
214
- .filter((command) => !command.startsWith('_') && command.startsWith(prefix))
215
- .map((command) => {
216
- return {
217
- // class, constant, enum, function, interface, keyword, method, namespace, property, text, type, and variable.
218
- type: 'variable',
219
- command,
220
- apply: command,
221
- label: command,
222
- };
223
- });
224
- return { from, options };
225
- },
226
- },
227
- {
228
- expr: /{#[a-z]*.[a-zA-Z]*/,
229
- supportEL: true,
230
- run(ctx, match) {
231
- const tokens = match.text.split('.');
232
- const key = tokens[0].replaceAll('{#', '');
233
- const prefix = tokens[1];
234
- const candidate = this.grammar[key][prefix];
235
- let options = [];
236
- let from = ctx.pos;
237
- if (candidate) {
238
- // const modelType = this._getModelType(candidate._type);
239
- if (this._isMap(candidate._type)) {
240
- options = this._getEnum(candidate._type).map((value) => {
241
- const type = this._convertType(candidate._type);
242
- const command = `['${value}'][0]`;
243
- return {
244
- type,
245
- command,
246
- apply: command,
247
- label: value,
248
- };
249
- });
250
- }
251
- } else {
252
- from -= prefix.length;
253
- const candidate = this.grammar[key];
254
- if (candidate._type) {
255
- options = this._buildMethods(candidate._type);
256
- } else {
257
- options = Object.keys(candidate)
258
- .filter((command) => !command.startsWith('_') && command.startsWith(prefix))
259
- .map((command) => ({
260
- type: this._convertType(candidate[command]._type),
261
- command,
262
- apply: command,
263
- label: command,
264
- }));
265
- }
266
- }
267
- return { from, options };
268
- },
269
- },
270
- {
271
- expr: /{#[a-z]*.[a-zA-Z]*.[a-zA-Z]*/,
272
- supportEL: true,
273
- run(ctx, match) {
274
- const tokens = match.text.split('.');
275
- const key = tokens[0].replaceAll('{#', '');
276
- const word = tokens[1];
277
- const prefix = tokens[2];
278
- const candidate = this.grammar[key][word];
279
- let options = [];
280
- let from = ctx.pos;
281
- if (candidate) {
282
- if (this._isMap(candidate._type)) {
283
- options = this._getEnum(candidate._type)
284
- .filter((command) => !command.startsWith('_') && command.startsWith(prefix))
285
- .map((label) => {
286
- const type = this._convertType(candidate._type);
287
- return {
288
- type,
289
- command: `['${label}'][0]`,
290
- apply(view, completion, _from, to) {
291
- const from = _from - prefix.length - 1;
292
- view.dispatch({
293
- changes: { from, to, insert: completion.command },
294
- selection: { anchor: from + completion.command.length },
295
- userEvent: 'input.complete',
296
- annotations: pickedCompletion.of(view),
297
- });
298
- },
299
- label,
300
- };
301
- });
302
- } else {
303
- if (candidate._type) {
304
- options = this._buildMethods(candidate._type);
305
- } else {
306
- from -= prefix.length;
307
- options = Object.keys(candidate)
308
- .filter((command) => command.startsWith(prefix))
309
- .map((command) => {
310
- return {
311
- type: this._convertType(candidate[command]._type),
312
- command,
313
- apply: command,
314
- label: command,
315
- };
316
- });
317
- }
318
- }
319
- }
320
- return { from, options };
321
- },
322
- },
323
- {
324
- run(ctx) {
325
- const autocompleteLanguage = ctx.state.languageDataAt('autocomplete', ctx.pos);
326
- if (autocompleteLanguage.length > 0) {
327
- return autocompleteLanguage[0](ctx);
328
- }
329
- return { from: ctx.pos, options: [] };
330
- },
137
+ get defaultCompletionHandler() {
138
+ return {
139
+ run(completionContext) {
140
+ const language = completionContext.state.languageDataAt('autocomplete', completionContext.pos);
141
+ if (language.length > 0) {
142
+ return language[0](completionContext);
143
+ }
144
+ return { from: completionContext.pos, options: [] };
331
145
  },
332
- ];
146
+ };
333
147
  }
334
148
 
335
- getExtensions() {
149
+ _getExtensions() {
336
150
  return [
337
151
  basicSetup,
152
+ overrideTheme,
338
153
  languageCompartment.of(json()),
339
154
  placeholderCompartment.of(placeholder(this.placeholder || '')),
340
- EditorState.transactionFilter.of((tr) => (this.singleLine && tr.newDoc.lines > 1 ? [] : tr)),
341
- readonlyCompartment.of(EditorView.editable.of(!this.readonly)),
342
- EditorView.updateListener.of((update) => {
343
- if (update.view.contentDOM.innerText) {
344
- this.value = update.view.contentDOM.innerText;
155
+ EditorState.transactionFilter.of((tr) => (this.hasInputStyle && tr.newDoc.lines > 1 ? [] : tr)),
156
+ readonlyCompartment.of(EditorView.editable.of(!this.readonly && !this.disabled)),
157
+ EditorView.updateListener.of((viewUpdate) => {
158
+ if (viewUpdate.docChanged) {
159
+ this.value = this._editorView.state.doc.toString();
345
160
  }
346
161
  }),
347
162
  autocompletion({
348
163
  activateOnTyping: true,
349
164
  override: [this._autocomplete.bind(this)],
350
165
  }),
351
- Prec.high(this.insertELKeymap()),
166
+ ...this.getExtensions(),
352
167
  ];
353
168
  }
354
169
 
355
- insertELKeymap() {
356
- return keymap.of([
357
- {
358
- key: 'Ctrl-Shift-e',
359
- mac: 'Cmd-Shift-e',
360
- run(view) {
361
- view.dispatch(
362
- view.state.changeByRange((range) => ({
363
- changes: [{ from: range.from, to: range.to, insert: '{#}' }],
364
- range: EditorSelection.range(range.from + 2, range.to + 2),
365
- })),
366
- );
367
- startCompletion(view);
368
- return true;
369
- },
370
- },
371
- ]);
170
+ getExtensions() {
171
+ return [];
172
+ }
173
+
174
+ getInputElement() {
175
+ return this.shadowRoot.querySelector(`[id=${this._id}]`);
372
176
  }
373
177
 
374
178
  firstUpdated() {
375
179
  super.firstUpdated();
376
- const parent = this.shadowRoot.querySelector(`[id=${this._id}]`);
180
+
181
+ const parent = this.getInputElement();
182
+
377
183
  this._editorState = EditorState.create({
378
184
  doc: this.value,
379
- extensions: this.getExtensions(),
185
+ extensions: this._getExtensions(),
380
186
  });
381
187
  this._editorView = new EditorView({
382
188
  root: this.shadowRoot,
383
189
  parent,
384
190
  state: this._editorState,
385
191
  });
386
- parent.appendChild(this._editorView.dom);
387
192
 
388
- if (this.clipboard) {
389
- import('clipboard-copy').then((mod) => {
390
- const copy = mod.default;
391
- this.shadowRoot.querySelector('gv-button').addEventListener('gv-button:click', () => {
392
- copy(this.value);
393
- this._copied = true;
394
- this._clipboardIcon = shapeCopied;
395
- dispatchCustomEvent(this, 'clipboard-copy');
396
- setTimeout(() => {
397
- this._copied = false;
398
- this._clipboardIcon = shapeClipboard;
399
- }, 1000);
400
- });
401
- });
402
- }
403
- dispatchCustomEvent(this, 'ready');
193
+ parent.appendChild(this._editorView.dom);
404
194
  }
405
195
 
406
196
  async updated(properties) {
197
+ super.updated(properties);
407
198
  if (properties.has('placeholder')) {
408
- this._editorView.dispatch({
409
- effects: placeholderCompartment.reconfigure(placeholder(this.placeholder)),
410
- });
199
+ this._reflectPlaceholder();
411
200
  }
412
201
  if (properties.has('value')) {
413
- const transaction = this._editorState.update({ changes: { from: 0, to: this._editorState.doc.length, insert: this.value } });
414
- this._editorState = transaction.state;
202
+ this._reflectValue();
415
203
  }
416
- if (properties.has('readonly')) {
417
- this._editorView.dispatch({
418
- effects: readonlyCompartment.reconfigure(EditorView.editable.of(!this.readonly)),
419
- });
204
+ if (properties.has('readonly') || properties.has('disabled')) {
205
+ this._reflectReadonlyDisabled();
420
206
  }
421
-
422
207
  if (properties.has('options') && this.options) {
423
- if (this.options.mode) {
424
- const language = languages.find((language) => {
425
- return language.name.toUpperCase() === this.options.mode.toUpperCase() || language.alias.includes(this.options.mode);
426
- });
427
- if (language != null) {
428
- if (language.support == null) {
429
- await language.load();
430
- }
431
- this._editorView.dispatch({
432
- effects: languageCompartment.reconfigure(language.support.extension),
433
- });
434
- } else {
435
- console.warn(
436
- `Cannot find language ${this.options.mode}, please use language supported by CodeMirror 6`,
437
- languages.map((l) => l.name),
438
- );
208
+ await this._reflectOptions();
209
+ }
210
+ }
211
+
212
+ _reflectPlaceholder() {
213
+ this._editorView.dispatch({
214
+ effects: placeholderCompartment.reconfigure(placeholder(this.placeholder)),
215
+ });
216
+ }
217
+
218
+ _reflectValue() {
219
+ const transaction = this._editorState.update({
220
+ changes: {
221
+ from: 0,
222
+ to: this._editorState.doc.length,
223
+ insert: this.value,
224
+ },
225
+ });
226
+ this._editorState = transaction.state;
227
+ }
228
+
229
+ _reflectReadonlyDisabled() {
230
+ this._editorView.dispatch({
231
+ effects: readonlyCompartment.reconfigure(EditorView.editable.of(!this.readonly && !this.disabled)),
232
+ });
233
+ }
234
+
235
+ async _reflectOptions() {
236
+ if (this.options.mode) {
237
+ const language = languages.find((languageDescription) => {
238
+ return (
239
+ languageDescription.name.toUpperCase() === this.options.mode.toUpperCase() ||
240
+ languageDescription.alias.includes(this.options.mode)
241
+ );
242
+ });
243
+ if (language != null) {
244
+ if (language.support == null) {
245
+ await language.load();
439
246
  }
247
+ this._editorView.dispatch({
248
+ effects: languageCompartment.reconfigure(language.support.extension),
249
+ });
250
+ } else {
251
+ console.warn(
252
+ `Cannot find language ${this.options.mode}, please use language supported by CodeMirror 6`,
253
+ languages.map((l) => l.name),
254
+ );
440
255
  }
441
256
  }
442
257
  }
443
258
 
444
259
  static get styles() {
445
260
  return [
261
+ ...super.styles,
446
262
  skeleton,
447
263
  empty,
448
264
  input,
@@ -458,6 +274,18 @@ export class GvCode extends InputElement(LitElement) {
458
274
  position: relative;
459
275
  }
460
276
 
277
+ .box-icon {
278
+ bottom: -1px;
279
+ right: -1px;
280
+ z-index: 1;
281
+ }
282
+
283
+ .code .box-icon {
284
+ border-radius: 0;
285
+ top: 0;
286
+ }
287
+
288
+ /** Override Codemirror style */
461
289
  .input .cm-gutters {
462
290
  display: none;
463
291
  }
@@ -474,15 +302,8 @@ export class GvCode extends InputElement(LitElement) {
474
302
  padding: 0;
475
303
  }
476
304
 
477
- .ͼ1.cm-editor.cm-focused {
478
- outline: none;
479
- }
480
-
481
- gv-button {
482
- position: absolute;
483
- right: 0;
484
- top: 0;
485
- z-index: 10;
305
+ :host([invalid]) .cm-gutters {
306
+ border-color: var(--gv-theme-color-error, #da1a1b);
486
307
  }
487
308
  `,
488
309
  ];
@@ -15,15 +15,255 @@
15
15
  */
16
16
  import { GvCode } from '../molecules/gv-code';
17
17
  import { dispatchCustomEvent } from '../lib/events';
18
+ import { pickedCompletion, startCompletion } from '@codemirror/autocomplete';
19
+ import { keymap } from '@codemirror/view';
20
+ import { EditorSelection, Prec } from '@codemirror/state';
21
+ import { get } from 'object-path';
18
22
 
19
23
  /**
20
24
  * Expression Language component
21
25
  *
26
+ * @fires gv-expression-language:input - input events with the `value` on `detail`
27
+ * @fires gv-expression-language:ready - event dispatch when component is ready
28
+ * @fires gv-expression-language:clipboard-copy - event dispatch when component the `value` has been copied to clipboard
29
+ *
30
+ * @attr {String} label - code language
31
+ * @attr {String} value - code content to be highlighted
32
+ * @attr {options} Object - options based on codemirror
33
+ * @attr {String} placeholder - an example value to display in the input when empty
34
+ * @attr {Number} rows - number of rows of the text element
35
+ * @attr {Boolean} large - for a large input (only if the field has one row)
36
+ * @attr {Boolean} medium - for a medium input (only if the field has one row) (Default)
37
+ * @attr {Boolean} small - for a small input (only if the field has one row)
38
+ * @attr {Object} grammar - The grammar for Expression Language support
39
+ *
40
+ * @attr {Boolean} [clipboard=false]- true if field has clipboard button
41
+ * @attr {Boolean} [autofocus=false] - true to put the focus on the input
42
+ * @attr {Boolean} [readonly=false] - true if field is readonly mode
22
43
  */
23
44
  export class GvExpressionLanguage extends GvCode {
45
+ static get properties() {
46
+ return {
47
+ ...super.properties,
48
+ grammar: { type: Object },
49
+ };
50
+ }
51
+
24
52
  firstUpdated() {
25
53
  super.firstUpdated();
26
- dispatchCustomEvent(this, 'ready');
54
+ dispatchCustomEvent(this, 'ready', { currentTarget: this });
55
+ }
56
+
57
+ getExtensions() {
58
+ return [Prec.high(this.insertELKeymap())];
59
+ }
60
+
61
+ insertELKeymap() {
62
+ return keymap.of([
63
+ {
64
+ key: 'Ctrl-Shift-e',
65
+ mac: 'Cmd-Shift-e',
66
+ run(view) {
67
+ view.dispatch(
68
+ view.state.changeByRange((range) => ({
69
+ changes: [{ from: range.from, to: range.to, insert: '{#}' }],
70
+ range: EditorSelection.range(range.from + 2, range.to + 2),
71
+ })),
72
+ );
73
+ startCompletion(view);
74
+ return true;
75
+ },
76
+ },
77
+ ]);
78
+ }
79
+
80
+ buildMethods(type) {
81
+ const modelType = this.getModelType(type);
82
+ return modelType.methods.map(({ name, params = [], returnType }) => {
83
+ const command = `${name}()`;
84
+ const displayParams = params.map((p) => `${p.type} ${p.name}`);
85
+ const label = `${name}(${displayParams.join(', ')})`;
86
+ const detail = `return ${returnType}`;
87
+ return {
88
+ type: 'method',
89
+ command,
90
+ apply: (view, completion, from, to) => {
91
+ const anchor = from + name.length + 1;
92
+ view.dispatch({
93
+ changes: { from, to, insert: completion.command },
94
+ selection: { anchor },
95
+ userEvent: 'input.complete',
96
+ annotations: pickedCompletion.of(view),
97
+ });
98
+ },
99
+ label,
100
+ detail,
101
+ };
102
+ });
103
+ }
104
+
105
+ findBestAutocompleteHandler(completionContext) {
106
+ return this.expressionLanguageCompletionHandlers
107
+ .filter((handler) => (this.grammar != null && handler.supportEL === true) || handler.supportEL !== true)
108
+ .map((handler) => {
109
+ const match = handler.expr ? completionContext.matchBefore(handler.expr) : true;
110
+ return { handler, match };
111
+ })
112
+ .find(({ match, expr }) => match);
113
+ }
114
+
115
+ getModelType(typeId) {
116
+ if (this.grammar != null) {
117
+ return get(this.grammar, `_types.${typeId}`);
118
+ }
119
+ return null;
120
+ }
121
+
122
+ isMap(typeId) {
123
+ return ['Map', 'HttpHeaders', 'MultiValueMap'].includes(typeId);
124
+ }
125
+
126
+ getEnum(typeId) {
127
+ if (this.grammar != null) {
128
+ return get(this.grammar, `_enums.${typeId}`);
129
+ }
130
+ return [''];
131
+ }
132
+
133
+ convertType(type) {
134
+ if (type) {
135
+ const lowerType = type.toLowerCase();
136
+ if (lowerType === 'string') {
137
+ return 'text';
138
+ } else if (lowerType === 'httpheaders' || lowerType === 'multivaluemap') {
139
+ return 'type';
140
+ } else if (lowerType === 'int' || lowerType === 'long') {
141
+ return 'variable';
142
+ } else if (lowerType.includes('[]')) {
143
+ return 'enum';
144
+ }
145
+ }
146
+ return 'variable';
147
+ }
148
+
149
+ get expressionLanguageCompletionHandlers() {
150
+ return [
151
+ {
152
+ expr: /{#[a-z]*/,
153
+ supportEL: true,
154
+ run(completionContext, match) {
155
+ const prefix = match.text.replaceAll('{#', '');
156
+ const from = completionContext.pos - prefix.length;
157
+ const options = Object.keys(this.grammar)
158
+ .filter((command) => !command.startsWith('_') && command.startsWith(prefix))
159
+ .map((command) => {
160
+ return {
161
+ type: 'variable',
162
+ command,
163
+ apply: command,
164
+ label: command,
165
+ };
166
+ });
167
+ return { from, options };
168
+ },
169
+ },
170
+ {
171
+ expr: /{#[a-z]*.[a-zA-Z]*/,
172
+ supportEL: true,
173
+ run(completionContext, match) {
174
+ const tokens = match.text.split('.');
175
+ const key = tokens[0].replaceAll('{#', '');
176
+ const prefix = tokens[1];
177
+ let candidate = this.grammar[key][prefix];
178
+ let options = [];
179
+ let from = completionContext.pos;
180
+ if (candidate) {
181
+ if (this.isMap(candidate._type)) {
182
+ options = this.getEnum(candidate._type).map((value) => {
183
+ const type = this.convertType(candidate._type);
184
+ const command = `['${value}'][0]`;
185
+ return {
186
+ type,
187
+ command,
188
+ apply: command,
189
+ label: value,
190
+ };
191
+ });
192
+ }
193
+ } else {
194
+ from -= prefix.length;
195
+ candidate = this.grammar[key];
196
+ if (candidate._type) {
197
+ options = this.buildMethods(candidate._type);
198
+ } else {
199
+ options = Object.keys(candidate)
200
+ .filter((command) => !command.startsWith('_') && command.startsWith(prefix))
201
+ .map((command) => ({
202
+ type: this.convertType(candidate[command]._type),
203
+ command,
204
+ apply: command,
205
+ label: command,
206
+ }));
207
+ }
208
+ }
209
+ return { from, options };
210
+ },
211
+ },
212
+ {
213
+ expr: /{#[a-z]*.[a-zA-Z]*.[a-zA-Z]*/,
214
+ supportEL: true,
215
+ run(completionContext, match) {
216
+ const tokens = match.text.split('.');
217
+ const key = tokens[0].replaceAll('{#', '');
218
+ const word = tokens[1];
219
+ const prefix = tokens[2];
220
+ const candidate = this.grammar[key][word];
221
+ let options = [];
222
+ let from = completionContext.pos;
223
+ if (candidate) {
224
+ if (this.isMap(candidate._type)) {
225
+ options = this.getEnum(candidate._type)
226
+ .filter((command) => !command.startsWith('_') && command.startsWith(prefix))
227
+ .map((label) => {
228
+ const type = this.convertType(candidate._type);
229
+ return {
230
+ type,
231
+ command: `['${label}'][0]`,
232
+ apply(view, completion, _from, to) {
233
+ from = _from - prefix.length - 1;
234
+ view.dispatch({
235
+ changes: { from, to, insert: completion.command },
236
+ selection: { anchor: from + completion.command.length },
237
+ userEvent: 'input.complete',
238
+ annotations: pickedCompletion.of(view),
239
+ });
240
+ },
241
+ label,
242
+ };
243
+ });
244
+ } else {
245
+ if (candidate._type) {
246
+ options = this.buildMethods(candidate._type);
247
+ } else {
248
+ from -= prefix.length;
249
+ options = Object.keys(candidate)
250
+ .filter((command) => command.startsWith(prefix))
251
+ .map((command) => {
252
+ return {
253
+ type: this.convertType(candidate[command]._type),
254
+ command,
255
+ apply: command,
256
+ label: command,
257
+ };
258
+ });
259
+ }
260
+ }
261
+ }
262
+ return { from, options };
263
+ },
264
+ },
265
+ this.defaultCompletionHandler,
266
+ ];
27
267
  }
28
268
  }
29
269
 
@@ -427,10 +427,6 @@ export class GvPolicyStudio extends KeyboardElement(LitElement) {
427
427
  search.focus();
428
428
  }
429
429
  }
430
- if (this.isPressed(KEYS.Esc)) {
431
- this._closeFlowStepForm();
432
- this._onCloseDocumentation();
433
- }
434
430
  }
435
431
  }
436
432
 
@@ -1114,7 +1110,7 @@ export class GvPolicyStudio extends KeyboardElement(LitElement) {
1114
1110
  outlined
1115
1111
  small
1116
1112
  @gv-button:click="${this._onCloseFlowStepForm}"
1117
- title="Close (esc)"
1113
+ title="Close"
1118
1114
  ></gv-button>
1119
1115
  <gv-button
1120
1116
  slot="header-left"
@@ -232,6 +232,7 @@ export const input = css`
232
232
  }
233
233
 
234
234
  :host([disabled]) input,
235
+ :host([disabled]) .input,
235
236
  :host([disabled]) ::slotted(input) {
236
237
  cursor: default;
237
238
  opacity: 0.5;