@fusion.dev/helix-headless 0.219.0 → 0.221.0

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.
@@ -1,11 +1,11 @@
1
1
  import { __decorate } from "tslib";
2
- import { AriaForwardController, customElement, drawIcon, formatWords, HelixFormElement, ListedController, MIRROR_RULE, MixinControllerValue, MixinField, numberConverter, onApple, restoreValue, stopPress, words, } from '@fusion.dev/helix-core';
3
- import { asterisk, caretDown, caretRight, caretUp, info, magnifyingGlass, textAa, textAUnderline, warning, x, xCircleFill, } from '@fusion.dev/helix-icons';
2
+ import { AriaForwardController, customElement, drawIcon, findPattern, findRanges, formatWords, HelixFormElement, ListedController, MixinControllerValue, MixinField, numberConverter, onApple, restoreValue, stopPress, words, } from '@fusion.dev/helix-core';
3
+ import { info, magnifyingGlass, warning, xCircleFill } from '@fusion.dev/helix-icons';
4
4
  import { nothing } from 'lit';
5
5
  import { property, state } from 'lit/decorators.js';
6
- import { live } from 'lit/directives/live.js';
7
6
  import { html } from 'lit/static-html.js';
8
7
  import '../../button/index.js';
8
+ import '../../find/index.js';
9
9
  import '../../icon-button/index.js';
10
10
  import '../../toolbar/index.js';
11
11
  export const diagnosticLines = (value, diagnostics) => {
@@ -53,24 +53,7 @@ export const positionOf = (value, at) => {
53
53
  line: before.split('\n').length,
54
54
  };
55
55
  };
56
- const patternOf = (query) => {
57
- const source = query.regex ? query.search : query.search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
58
- try {
59
- return new RegExp(query.wholeWord ? `\\b(?:${source})\\b` : source, query.caseSensitive ? 'gu' : 'giu');
60
- }
61
- catch {
62
- return undefined;
63
- }
64
- };
65
- export const findRanges = (text, query) => {
66
- const pattern = query.search === '' ? undefined : patternOf(query);
67
- if (!pattern) {
68
- return [];
69
- }
70
- return [...text.matchAll(pattern)]
71
- .filter((match) => match[0] !== '')
72
- .map((match) => [match.index, match.index + match[0].length]);
73
- };
56
+ export { findRanges };
74
57
  const LANGUAGE_NAMES = {
75
58
  bash: 'Shell',
76
59
  css: 'CSS',
@@ -129,15 +112,6 @@ let HelixHeadlessCodeEditor = class HelixHeadlessCodeEditor extends MixinControl
129
112
  this.wrap = false;
130
113
  this.caret = 0;
131
114
  this.findOpen = false;
132
- this.found = undefined;
133
- this.replaceOpen = false;
134
- this.query = {
135
- caseSensitive: false,
136
- regex: false,
137
- replace: '',
138
- search: '',
139
- wholeWord: false,
140
- };
141
115
  this.escaped = false;
142
116
  this.engineEdited = false;
143
117
  this.handleViewFocusOut = (event) => {
@@ -147,6 +121,14 @@ let HelixHeadlessCodeEditor = class HelixHeadlessCodeEditor extends MixinControl
147
121
  this.announceChange();
148
122
  }
149
123
  };
124
+ this.finder = {
125
+ end: () => this.view?.findEnd?.(),
126
+ find: (query, direction) => this.findIn(query, direction),
127
+ replace: (query, all) => this.replaceIn(query, all),
128
+ };
129
+ this.handleFindToggle = (event) => {
130
+ this.findOpen = event.newState === 'open';
131
+ };
150
132
  this.handleKeys = (event) => {
151
133
  const mod = onApple() ? event.metaKey && !event.ctrlKey : event.ctrlKey && !event.metaKey;
152
134
  if (mod && !event.altKey && !event.shiftKey && event.key.toLowerCase() === 'f') {
@@ -155,28 +137,6 @@ let HelixHeadlessCodeEditor = class HelixHeadlessCodeEditor extends MixinControl
155
137
  void this.openFind();
156
138
  }
157
139
  };
158
- this.handleFindInput = (event) => {
159
- this.query = { ...this.query, search: event.target.value };
160
- this.search('first');
161
- };
162
- this.handleReplaceInput = (event) => {
163
- this.query = { ...this.query, replace: event.target.value };
164
- };
165
- this.handleFindKeys = (event) => {
166
- if (event.key === 'Escape') {
167
- event.preventDefault();
168
- this.closeFind();
169
- }
170
- else if (event.key === 'Enter') {
171
- event.preventDefault();
172
- if (event.target.getAttribute('part') === 'replace-field') {
173
- this.replace(event.metaKey || event.ctrlKey);
174
- }
175
- else {
176
- this.search(event.shiftKey ? 'previous' : 'next');
177
- }
178
- }
179
- };
180
140
  this.handleInput = (event) => {
181
141
  event.stopPropagation();
182
142
  const control = event.target;
@@ -203,7 +163,6 @@ let HelixHeadlessCodeEditor = class HelixHeadlessCodeEditor extends MixinControl
203
163
  this.escaped = false;
204
164
  };
205
165
  }
206
- static { this.markRules = [MIRROR_RULE]; }
207
166
  static { this.words = 'helix-code-editor'; }
208
167
  get control() {
209
168
  return this.partNamed('control');
@@ -220,9 +179,6 @@ let HelixHeadlessCodeEditor = class HelixHeadlessCodeEditor extends MixinControl
220
179
  wrap: this.wrap,
221
180
  };
222
181
  }
223
- get readable() {
224
- return this.query.search === '' || patternOf(this.query) !== undefined;
225
- }
226
182
  focus(options) {
227
183
  if (this.view?.focus) {
228
184
  this.view.focus();
@@ -273,7 +229,7 @@ let HelixHeadlessCodeEditor = class HelixHeadlessCodeEditor extends MixinControl
273
229
  this.refreshFeedback();
274
230
  }
275
231
  this.toggleState('find-open', this.findOpen);
276
- this.toggleState('replace-open', this.findOpen && this.replaceOpen && !this.readOnly);
232
+ this.attachFind();
277
233
  const mount = this.partNamed('view');
278
234
  if (changed.has('engine') || (this.engine && !this.view && mount)) {
279
235
  this.view?.destroy();
@@ -308,43 +264,35 @@ let HelixHeadlessCodeEditor = class HelixHeadlessCodeEditor extends MixinControl
308
264
  return true;
309
265
  }
310
266
  async openFind() {
267
+ const find = this.find?.element;
268
+ if (!find) {
269
+ return;
270
+ }
311
271
  const control = this.control;
312
272
  if (!this.view && control && control.selectionEnd > control.selectionStart) {
313
273
  const selected = control.value.slice(control.selectionStart, control.selectionEnd);
314
274
  if (!selected.includes('\n')) {
315
- this.query = { ...this.query, search: selected };
275
+ find.query = selected;
316
276
  }
317
277
  }
318
- this.findOpen = true;
319
- await this.updateComplete;
320
- const field = this.partNamed('find-field');
321
- field?.focus();
322
- field?.select();
323
- if (this.query.search !== '') {
324
- this.search('first');
325
- }
326
- }
327
- closeFind() {
328
- this.findOpen = false;
329
- this.found = undefined;
330
- this.view?.findEnd?.();
331
- this.focus();
278
+ await find.show();
332
279
  }
333
- search(direction) {
334
- if (this.query.search === '' || !this.readable) {
335
- this.found = undefined;
336
- this.view?.findEnd?.();
280
+ attachFind() {
281
+ const element = this.partNamed('find') ?? undefined;
282
+ if (element === this.find?.element) {
337
283
  return;
338
284
  }
285
+ this.find?.sink.detach();
286
+ this.find = element ? { element, sink: element.attach(this.finder) } : undefined;
287
+ }
288
+ findIn(query, direction) {
339
289
  if (this.view?.find) {
340
- this.found = this.view.find(this.query, direction);
341
- return;
290
+ return this.view.find(query, direction);
342
291
  }
343
292
  const control = this.control;
344
- const ranges = findRanges(this.value, this.query);
293
+ const ranges = findRanges(this.value, query);
345
294
  if (!control || ranges.length === 0) {
346
- this.found = { count: ranges.length, index: -1 };
347
- return;
295
+ return { count: ranges.length, index: -1 };
348
296
  }
349
297
  const at = direction === 'first' ? control.selectionStart : control.selectionEnd;
350
298
  let index = direction === 'previous'
@@ -356,54 +304,37 @@ let HelixHeadlessCodeEditor = class HelixHeadlessCodeEditor extends MixinControl
356
304
  const [from, to] = ranges[index] ?? [0, 0];
357
305
  control.setSelectionRange(from, to);
358
306
  this.caret = from;
359
- this.found = { count: ranges.length, index };
307
+ return { count: ranges.length, index };
360
308
  }
361
- replace(all) {
362
- if (this.readOnly || this.query.search === '' || !this.readable) {
363
- return;
309
+ replaceIn(query, all) {
310
+ if (this.readOnly) {
311
+ return undefined;
364
312
  }
365
313
  if (this.view?.replace) {
366
- this.found = this.view.replace(this.query, all);
367
- return;
314
+ return this.view.replace(query, all);
368
315
  }
369
316
  const control = this.control;
370
- const pattern = patternOf(this.query);
317
+ const pattern = findPattern(query);
371
318
  if (!control || !pattern) {
372
- return;
319
+ return undefined;
373
320
  }
374
321
  if (all) {
375
- this.requestValue(this.value.replace(pattern, this.query.replace));
322
+ this.requestValue(this.value.replace(pattern, query.replace));
376
323
  }
377
324
  else {
378
- const selected = findRanges(this.value, this.query).find(([from, to]) => from === control.selectionStart && to === control.selectionEnd);
325
+ const selected = findRanges(this.value, query).find(([from, to]) => from === control.selectionStart && to === control.selectionEnd);
379
326
  if (!selected) {
380
- this.search('next');
381
- return;
327
+ return this.findIn(query, 'next');
382
328
  }
383
329
  const [from, to] = selected;
384
330
  const one = new RegExp(pattern.source, pattern.flags.replace('g', ''));
385
- const replaced = this.value.slice(from, to).replace(one, this.query.replace);
331
+ const replaced = this.value.slice(from, to).replace(one, query.replace);
386
332
  this.requestValue(this.value.slice(0, from) + replaced + this.value.slice(to));
387
333
  restoreValue(control, this.value);
388
334
  control.setSelectionRange(from + replaced.length, from + replaced.length);
389
335
  }
390
336
  restoreValue(control, this.value);
391
- this.search('next');
392
- }
393
- toggleOption(option) {
394
- this.query = { ...this.query, [option]: !this.query[option] };
395
- this.search('first');
396
- }
397
- get countText() {
398
- if (!this.readable) {
399
- return this.findInvalidLabel;
400
- }
401
- return this.found === undefined
402
- ? ''
403
- : formatWords(this, 'matchesLabel', {
404
- count: this.found.count,
405
- index: Math.max(this.found.index, 0) + 1,
406
- });
337
+ return this.findIn(query, 'next');
407
338
  }
408
339
  renderHeader() {
409
340
  const toolbar = this.tag('toolbar');
@@ -412,14 +343,14 @@ let HelixHeadlessCodeEditor = class HelixHeadlessCodeEditor extends MixinControl
412
343
  return html `<div part="header">
413
344
  ${this.renderLabel()}
414
345
  ${language ? html `<span part="language">${language}</span>` : nothing}
415
- <${toolbar} part="controls" exportparts="toolbar: controls-toolbar" exportparts="toolbar: controls-toolbar" label=${this.controlsLabel}>
346
+ <${toolbar} part="controls" exportparts="toolbar: controls-toolbar" label=${this.controlsLabel}>
416
347
  <slot name="actions"></slot>
417
348
  <${button}
418
- part="find"
419
- exportparts="button: find-button, live: find-live, spinner: find-spinner, text: find-text"
349
+ part="find-toggle"
350
+ exportparts="button: find-toggle-button, live: find-toggle-live, spinner: find-toggle-spinner, text: find-toggle-text"
420
351
  .expanded=${this.findOpen}
421
352
  label=${this.findLabel}
422
- @click=${() => (this.findOpen ? this.closeFind() : void this.openFind())}
353
+ @click=${() => (this.findOpen ? this.find?.element.hide() : void this.openFind())}
423
354
  @helix-press=${stopPress}
424
355
  >
425
356
  ${drawIcon(magnifyingGlass)}
@@ -428,132 +359,28 @@ let HelixHeadlessCodeEditor = class HelixHeadlessCodeEditor extends MixinControl
428
359
  </div>`;
429
360
  }
430
361
  renderFind() {
431
- const toolbar = this.tag('toolbar');
432
- const button = this.tag('icon-button');
433
- const textButton = this.tag('button');
434
- const searchable = this.readable && (this.found?.count ?? 0) > 0;
435
- const replacing = this.replaceOpen && !this.readOnly;
436
- return html `<div part="find-bar" role="search" aria-label=${this.findLabel} ?hidden=${!this.findOpen}>
437
- ${this.readOnly
438
- ? nothing
439
- : html `<${button}
440
- part="find-replace"
441
- exportparts="button: find-replace-button, live: find-replace-live, spinner: find-replace-spinner, text: find-replace-text"
442
- .expanded=${replacing}
443
- label=${this.findReplaceLabel}
444
- @click=${() => {
445
- this.replaceOpen = !this.replaceOpen;
446
- }}
447
- @helix-press=${stopPress}
448
- >
449
- ${replacing ? drawIcon(caretDown) : html `<span data-mirror>${drawIcon(caretRight)}</span>`}
450
- </${button}>`}
451
- <input
452
- part="find-field"
453
- aria-label=${this.findLabel}
454
- aria-invalid=${this.readable ? nothing : 'true'}
455
- autocomplete="off"
456
- placeholder=${this.findLabel}
457
- spellcheck="false"
458
- type="search"
459
- .value=${live(this.query.search)}
460
- @input=${this.handleFindInput}
461
- @keydown=${this.handleFindKeys}
462
- />
463
- <span part="find-count" role="status">${this.countText}</span>
464
- <${toolbar} part="find-controls" exportparts="toolbar: find-controls-toolbar" exportparts="toolbar: find-controls-toolbar" label=${this.findControlsLabel}>
465
- <${button}
466
- part="find-match-case"
467
- exportparts="button: find-match-case-button, live: find-match-case-live, spinner: find-match-case-spinner, text: find-match-case-text"
468
- aria-pressed=${this.query.caseSensitive ? 'true' : 'false'}
469
- label=${this.findMatchCaseLabel}
470
- @click=${() => this.toggleOption('caseSensitive')}
471
- @helix-press=${stopPress}
472
- >
473
- ${drawIcon(textAa)}
474
- </${button}>
475
- <${button}
476
- part="find-whole-word"
477
- exportparts="button: find-whole-word-button, live: find-whole-word-live, spinner: find-whole-word-spinner, text: find-whole-word-text"
478
- aria-pressed=${this.query.wholeWord ? 'true' : 'false'}
479
- label=${this.findWholeWordLabel}
480
- @click=${() => this.toggleOption('wholeWord')}
481
- @helix-press=${stopPress}
482
- >
483
- ${drawIcon(textAUnderline)}
484
- </${button}>
485
- <${button}
486
- part="find-regex"
487
- exportparts="button: find-regex-button, live: find-regex-live, spinner: find-regex-spinner, text: find-regex-text"
488
- aria-pressed=${this.query.regex ? 'true' : 'false'}
489
- label=${this.findRegexLabel}
490
- @click=${() => this.toggleOption('regex')}
491
- @helix-press=${stopPress}
492
- >
493
- ${drawIcon(asterisk)}
494
- </${button}>
495
- <${button}
496
- part="find-previous"
497
- exportparts="button: find-previous-button, live: find-previous-live, spinner: find-previous-spinner, text: find-previous-text"
498
- ?disabled=${!searchable}
499
- label=${this.findPreviousLabel}
500
- @click=${() => this.search('previous')}
501
- @helix-press=${stopPress}
502
- >
503
- ${drawIcon(caretUp)}
504
- </${button}>
505
- <${button}
506
- part="find-next"
507
- exportparts="button: find-next-button, live: find-next-live, spinner: find-next-spinner, text: find-next-text"
508
- ?disabled=${!searchable}
509
- label=${this.findNextLabel}
510
- @click=${() => this.search('next')}
511
- @helix-press=${stopPress}
512
- >
513
- ${drawIcon(caretDown)}
514
- </${button}>
515
- <${button}
516
- part="find-close"
517
- exportparts="button: find-close-button, live: find-close-live, spinner: find-close-spinner, text: find-close-text"
518
- label=${this.findCloseLabel}
519
- @click=${() => this.closeFind()}
520
- @helix-press=${stopPress}
521
- >
522
- ${drawIcon(x)}
523
- </${button}>
524
- </${toolbar}>
525
- ${!replacing
526
- ? nothing
527
- : html `<input
528
- part="replace-field"
529
- aria-label=${this.replaceWithLabel}
530
- autocomplete="off"
531
- placeholder=${this.replaceWithLabel}
532
- spellcheck="false"
533
- type="text"
534
- .value=${live(this.query.replace)}
535
- @input=${this.handleReplaceInput}
536
- @keydown=${this.handleFindKeys}
537
- />
538
- <${textButton}
539
- part="replace"
540
- exportparts="button: replace-button, live: replace-live, spinner: replace-spinner, text: replace-text"
541
- ?disabled=${!searchable}
542
- @click=${() => this.replace(false)}
543
- @helix-press=${stopPress}
544
- >
545
- ${this.replaceLabel}
546
- </${textButton}>
547
- <${textButton}
548
- part="replace-all"
549
- exportparts="button: replace-all-button, live: replace-all-live, spinner: replace-all-spinner, text: replace-all-text"
550
- ?disabled=${!searchable}
551
- @click=${() => this.replace(true)}
552
- @helix-press=${stopPress}
553
- >
554
- ${this.replaceAllLabel}
555
- </${textButton}>`}
556
- </div>`;
362
+ const find = this.tag('find');
363
+ return html `<${find}
364
+ part="find"
365
+ exportparts="actions: find-actions, bar: find-bar, close: find-close, close-button: find-close-button, close-live: find-close-live, close-spinner: find-close-spinner, close-text: find-close-text, controls: find-controls, controls-toolbar: find-controls-toolbar, count: find-count, field: find-field, live: find-live, match-case: find-match-case, match-case-button: find-match-case-button, match-case-live: find-match-case-live, match-case-pressed: find-match-case-pressed, match-case-spinner: find-match-case-spinner, match-case-text: find-match-case-text, next: find-next, next-button: find-next-button, next-live: find-next-live, next-spinner: find-next-spinner, next-text: find-next-text, previous: find-previous, previous-button: find-previous-button, previous-live: find-previous-live, previous-spinner: find-previous-spinner, previous-text: find-previous-text, regex: find-regex, regex-button: find-regex-button, regex-live: find-regex-live, regex-pressed: find-regex-pressed, regex-spinner: find-regex-spinner, regex-text: find-regex-text, replace: find-replace, replace-all: find-replace-all, replace-all-button: find-replace-all-button, replace-all-live: find-replace-all-live, replace-all-spinner: find-replace-all-spinner, replace-all-text: find-replace-all-text, replace-button: find-replace-button, replace-field: find-replace-field, replace-live: find-replace-live, replace-spinner: find-replace-spinner, replace-text: find-replace-text, replace-toggle: find-replace-toggle, replace-toggle-button: find-replace-toggle-button, replace-toggle-live: find-replace-toggle-live, replace-toggle-spinner: find-replace-toggle-spinner, replace-toggle-text: find-replace-toggle-text, whole-word: find-whole-word, whole-word-button: find-whole-word-button, whole-word-live: find-whole-word-live, whole-word-pressed: find-whole-word-pressed, whole-word-spinner: find-whole-word-spinner, whole-word-text: find-whole-word-text"
366
+ close-label=${this.findCloseLabel}
367
+ controls-label=${this.findControlsLabel}
368
+ invalid-label=${this.findInvalidLabel}
369
+ label=${this.findLabel}
370
+ match-case-label=${this.findMatchCaseLabel}
371
+ matches-label=${this.matchesLabel}
372
+ next-label=${this.findNextLabel}
373
+ previous-label=${this.findPreviousLabel}
374
+ ?readonly=${this.readOnly}
375
+ regex-label=${this.findRegexLabel}
376
+ replace-all-label=${this.replaceAllLabel}
377
+ replace-field-label=${this.replaceWithLabel}
378
+ replace-label=${this.replaceLabel}
379
+ replace-toggle-label=${this.findReplaceLabel}
380
+ shortcut=""
381
+ whole-word-label=${this.findWholeWordLabel}
382
+ @toggle=${this.handleFindToggle}
383
+ ></${find}>`;
557
384
  }
558
385
  renderDiagnostics() {
559
386
  if (this.diagnostics.length === 0) {
@@ -731,15 +558,6 @@ __decorate([
731
558
  __decorate([
732
559
  state()
733
560
  ], HelixHeadlessCodeEditor.prototype, "findOpen", void 0);
734
- __decorate([
735
- state()
736
- ], HelixHeadlessCodeEditor.prototype, "found", void 0);
737
- __decorate([
738
- state()
739
- ], HelixHeadlessCodeEditor.prototype, "replaceOpen", void 0);
740
- __decorate([
741
- state()
742
- ], HelixHeadlessCodeEditor.prototype, "query", void 0);
743
561
  HelixHeadlessCodeEditor = __decorate([
744
562
  customElement('helix-headless-code-editor')
745
563
  ], HelixHeadlessCodeEditor);
@@ -0,0 +1 @@
1
+ export * from './root/editor-root.js';
@@ -0,0 +1 @@
1
+ export * from './root/editor-root.js';