@limetech/lime-elements 39.44.5 → 39.45.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.
Files changed (69) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/dist/cjs/{keycodes-B75-uKOK.js → keycodes-k-EdzcMX.js} +4 -0
  3. package/dist/cjs/lime-elements.cjs.js +1 -1
  4. package/dist/cjs/limel-breadcrumbs_8.cjs.entry.js +20 -1
  5. package/dist/cjs/limel-chip-set.cjs.entry.js +1 -1
  6. package/dist/cjs/limel-chip_2.cjs.entry.js +1 -1
  7. package/dist/cjs/limel-file-viewer.cjs.entry.js +84 -8
  8. package/dist/cjs/limel-markdown.cjs.entry.js +1 -1
  9. package/dist/cjs/limel-picker.cjs.entry.js +1 -1
  10. package/dist/cjs/limel-popover_2.cjs.entry.js +1 -1
  11. package/dist/cjs/limel-prosemirror-adapter.cjs.entry.js +778 -395
  12. package/dist/cjs/limel-select.cjs.entry.js +377 -13
  13. package/dist/cjs/limel-text-editor-link-menu.cjs.entry.js +1 -1
  14. package/dist/cjs/loader.cjs.js +1 -1
  15. package/dist/cjs/{markdown-parser-ChnRAxpZ.js → markdown-parser-YH5HSSgF.js} +36 -56
  16. package/dist/collection/components/list/list.js +19 -0
  17. package/dist/collection/components/select/select.js +215 -12
  18. package/dist/collection/components/select/select.template.js +17 -3
  19. package/dist/collection/components/text-editor/prosemirror-adapter/editor-config.js +12 -1
  20. package/dist/collection/components/text-editor/prosemirror-adapter/menu/menu-command-utils/active-state-utils.js +36 -0
  21. package/dist/collection/components/text-editor/prosemirror-adapter/menu/menu-command-utils/list-utils.js +270 -0
  22. package/dist/collection/components/text-editor/prosemirror-adapter/menu/menu-command-utils/node-utils.js +15 -0
  23. package/dist/collection/components/text-editor/prosemirror-adapter/menu/menu-commands.js +81 -69
  24. package/dist/collection/components/text-editor/prosemirror-adapter/plugins/list-key-handler.js +44 -0
  25. package/dist/collection/components/text-editor/prosemirror-adapter/prosemirror-adapter.js +4 -3
  26. package/dist/collection/util/keycodes.js +3 -0
  27. package/dist/collection/util/typeahead.js +159 -0
  28. package/dist/esm/{keycodes-rI0IeKpx.js → keycodes-C4_9qUMP.js} +4 -1
  29. package/dist/esm/lime-elements.js +1 -1
  30. package/dist/esm/limel-breadcrumbs_8.entry.js +20 -1
  31. package/dist/esm/limel-chip-set.entry.js +1 -1
  32. package/dist/esm/limel-chip_2.entry.js +1 -1
  33. package/dist/esm/limel-file-viewer.entry.js +84 -8
  34. package/dist/esm/limel-markdown.entry.js +1 -1
  35. package/dist/esm/limel-picker.entry.js +1 -1
  36. package/dist/esm/limel-popover_2.entry.js +1 -1
  37. package/dist/esm/limel-prosemirror-adapter.entry.js +779 -396
  38. package/dist/esm/limel-select.entry.js +378 -14
  39. package/dist/esm/limel-text-editor-link-menu.entry.js +1 -1
  40. package/dist/esm/loader.js +1 -1
  41. package/dist/esm/{markdown-parser-BSQKleVw.js → markdown-parser-DrxxGLF5.js} +36 -56
  42. package/dist/lime-elements/lime-elements.esm.js +1 -1
  43. package/dist/lime-elements/{p-a089b580.entry.js → p-03fd2181.entry.js} +1 -1
  44. package/dist/lime-elements/{p-fc43fb46.entry.js → p-12fe8fe1.entry.js} +6 -6
  45. package/dist/lime-elements/p-1f29f482.entry.js +1 -0
  46. package/dist/lime-elements/{p-bf3c78a8.entry.js → p-2b01c1f2.entry.js} +1 -1
  47. package/dist/lime-elements/{p-758939be.entry.js → p-2e5d5e79.entry.js} +1 -1
  48. package/dist/lime-elements/{p-3ea159fd.entry.js → p-38393fb4.entry.js} +1 -1
  49. package/dist/lime-elements/p-9152d7e1.entry.js +23 -0
  50. package/dist/lime-elements/{p-1e3cdfa1.entry.js → p-9a04a686.entry.js} +1 -1
  51. package/dist/lime-elements/p-C4_9qUMP.js +1 -0
  52. package/dist/lime-elements/{p-B0qhUema.js → p-C9xHOCE_.js} +2 -2
  53. package/dist/lime-elements/{p-6fbf20c6.entry.js → p-e8df7a2e.entry.js} +1 -1
  54. package/dist/lime-elements/p-f51b5651.entry.js +1 -0
  55. package/dist/types/components/select/select.d.ts +76 -1
  56. package/dist/types/components/select/select.template.d.ts +21 -1
  57. package/dist/types/components/text-editor/prosemirror-adapter/menu/menu-command-utils/active-state-utils.d.ts +6 -0
  58. package/dist/types/components/text-editor/prosemirror-adapter/menu/menu-command-utils/list-utils.d.ts +68 -0
  59. package/dist/types/components/text-editor/prosemirror-adapter/menu/menu-command-utils/node-utils.d.ts +10 -0
  60. package/dist/types/components/text-editor/prosemirror-adapter/menu/menu-commands.d.ts +10 -0
  61. package/dist/types/components/text-editor/prosemirror-adapter/plugins/list-key-handler.d.ts +14 -0
  62. package/dist/types/components.d.ts +36 -0
  63. package/dist/types/util/keycodes.d.ts +3 -0
  64. package/dist/types/util/typeahead.d.ts +101 -0
  65. package/package.json +3 -3
  66. package/dist/lime-elements/p-552d0733.entry.js +0 -1
  67. package/dist/lime-elements/p-845cfda7.entry.js +0 -1
  68. package/dist/lime-elements/p-8f4273e4.entry.js +0 -23
  69. package/dist/lime-elements/p-rI0IeKpx.js +0 -1
@@ -33093,7 +33093,7 @@ function rehypeRaw(options) {
33093
33093
 
33094
33094
  // http://www.w3.org/TR/CSS21/grammar.html
33095
33095
  // https://github.com/visionmedia/css-parse/pull/49#issuecomment-30088027
33096
- var COMMENT_REGEX = /\/\*[^*]*\*+([^/*][^*]*\*+)*\//g;
33096
+ var COMMENT_REGEX = /\/\*(?:[^*]|\*(?!\/))*\*\//g;
33097
33097
 
33098
33098
  var NEWLINE_REGEX = /\n/g;
33099
33099
  var WHITESPACE_REGEX = /^\s*/;
@@ -33101,12 +33101,10 @@ var WHITESPACE_REGEX = /^\s*/;
33101
33101
  // declaration
33102
33102
  var PROPERTY_REGEX = /^(\*?[-#/*\\\w]+(\[[0-9a-z_-]+\])?)\s*/;
33103
33103
  var COLON_REGEX = /^:\s*/;
33104
- var VALUE_REGEX = /^((?:'(?:\\'|.)*?'|"(?:\\"|.)*?"|\([^)]*?\)|[^};])+)/;
33104
+ var VALUE_REGEX =
33105
+ /^((?:'(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*"|url\((?:'(?:\\.|[^'\\])*'|"(?:\\.|[^"\\])*"|[^)]*)\)|[^};])+)/;
33105
33106
  var SEMICOLON_REGEX = /^[;\s]*/;
33106
33107
 
33107
- // https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String/Trim#Polyfill
33108
- var TRIM_REGEX = /^\s+|\s+$/g;
33109
-
33110
33108
  // strings
33111
33109
  var NEWLINE = '\n';
33112
33110
  var FORWARD_SLASH = '/';
@@ -33230,16 +33228,13 @@ function index (style, options) {
33230
33228
  /**
33231
33229
  * Parse comments.
33232
33230
  *
33233
- * @param {Object[]} [rules]
33231
+ * @param {Object[]} rules
33234
33232
  * @return {Object[]}
33235
33233
  */
33236
33234
  function comments(rules) {
33237
33235
  var c;
33238
- rules = rules || [];
33239
33236
  while ((c = comment())) {
33240
- if (c !== false) {
33241
- rules.push(c);
33242
- }
33237
+ rules.push(c);
33243
33238
  }
33244
33239
  return rules;
33245
33240
  }
@@ -33301,9 +33296,9 @@ function index (style, options) {
33301
33296
 
33302
33297
  var ret = pos({
33303
33298
  type: TYPE_DECLARATION,
33304
- property: trim(prop[0].replace(COMMENT_REGEX, EMPTY_STRING)),
33299
+ property: prop[0].replace(COMMENT_REGEX, EMPTY_STRING).trim(),
33305
33300
  value: val
33306
- ? trim(val[0].replace(COMMENT_REGEX, EMPTY_STRING))
33301
+ ? val[0].replace(COMMENT_REGEX, EMPTY_STRING).trim()
33307
33302
  : EMPTY_STRING
33308
33303
  });
33309
33304
 
@@ -33326,10 +33321,8 @@ function index (style, options) {
33326
33321
  // declarations
33327
33322
  var decl;
33328
33323
  while ((decl = declaration())) {
33329
- if (decl !== false) {
33330
- decls.push(decl);
33331
- comments(decls);
33332
- }
33324
+ decls.push(decl);
33325
+ comments(decls);
33333
33326
  }
33334
33327
 
33335
33328
  return decls;
@@ -33339,47 +33332,34 @@ function index (style, options) {
33339
33332
  return declarations();
33340
33333
  }
33341
33334
 
33342
- /**
33343
- * Trim `str`.
33344
- *
33345
- * @param {String} str
33346
- * @return {String}
33347
- */
33348
- function trim(str) {
33349
- return str ? str.replace(TRIM_REGEX, EMPTY_STRING) : EMPTY_STRING;
33350
- }
33351
-
33352
- /**
33353
- * Parses inline style to object.
33354
- *
33355
- * @param style - Inline style.
33356
- * @param iterator - Iterator.
33357
- * @returns - Style object or null.
33358
- *
33359
- * @example Parsing inline style to object:
33360
- *
33361
- * ```js
33362
- * import parse from 'style-to-object';
33363
- * parse('line-height: 42;'); // { 'line-height': '42' }
33364
- * ```
33365
- */
33335
+ //#region src/index.ts
33336
+ /**
33337
+ * Parses inline style to object.
33338
+ *
33339
+ * @param style - Inline style.
33340
+ * @param iterator - Iterator.
33341
+ * @returns - Style object or null.
33342
+ *
33343
+ * @example Parsing inline style to object:
33344
+ *
33345
+ * ```js
33346
+ * import parse from 'style-to-object';
33347
+ * parse('line-height: 42;'); // { 'line-height': '42' }
33348
+ * ```
33349
+ */
33366
33350
  function StyleToObject(style, iterator) {
33367
- let styleObject = null;
33368
- if (!style || typeof style !== 'string') {
33369
- return styleObject;
33370
- }
33371
- const declarations = index(style);
33372
- declarations.forEach((declaration) => {
33373
- if (declaration.type !== 'declaration') {
33374
- return;
33375
- }
33376
- const { property, value } = declaration;
33377
- if (value) {
33378
- styleObject = styleObject || {};
33379
- styleObject[property] = value;
33380
- }
33381
- });
33382
- return styleObject;
33351
+ let styleObject = null;
33352
+ if (!style || typeof style !== "string") return styleObject;
33353
+ const declarations = index(style);
33354
+ declarations.forEach((declaration) => {
33355
+ if (declaration.type !== "declaration") return;
33356
+ const { property, value } = declaration;
33357
+ if (value) {
33358
+ styleObject = styleObject ?? {};
33359
+ styleObject[property] = value;
33360
+ }
33361
+ });
33362
+ return styleObject;
33383
33363
  }
33384
33364
 
33385
33365
  var colorName$1;
@@ -50,6 +50,25 @@ export class List {
50
50
  this.listElement.removeEventListener('mousedown', this.handleItemMouseDown);
51
51
  this.listElement.addEventListener('mousedown', this.handleItemMouseDown);
52
52
  this.mdcList = new MDCList(element);
53
+ // NOTE: This has no effect. It is kept as a marker rather than
54
+ // removed, so that its absence isn't mistaken for an oversight.
55
+ //
56
+ // MDC indexes each row for typeahead by looking for
57
+ // `.mdc-deprecated-list-item__primary-text` inside it, while
58
+ // `limel-list-item` renders its label as `<span class="label">`. MDC
59
+ // therefore builds an empty index, and typing does nothing.
60
+ //
61
+ // Do not "fix" this by adding MDC's class name to `limel-list-item`.
62
+ // That revives MDC's typeahead along with three behaviors that cannot
63
+ // be configured from the outside: a hard coded 300ms buffer, `Enter`
64
+ // being ignored for as long as that buffer lives (its `notifyAction`
65
+ // is guarded by `isTypeaheadInProgress`), and the space bar being
66
+ // excluded from matches. It would also collide with `limel-menu`,
67
+ // where single characters are used as item hotkeys.
68
+ //
69
+ // `limel-select` implements its own typeahead instead, see
70
+ // `src/util/typeahead.ts`. It intercepts typed characters in the
71
+ // capture phase on this very element, so they never reach MDC.
53
72
  this.mdcList.hasTypeahead = true;
54
73
  };
55
74
  /**
@@ -2,11 +2,23 @@ import { MDCFloatingLabel } from "@material/floating-label";
2
2
  import { MDCSelectHelperText } from "@material/select/helper-text";
3
3
  import { h, } from "@stencil/core";
4
4
  import { isMobileDevice } from "../../util/device";
5
- import { ENTER, SPACE } from "../../util/keycodes";
5
+ import { ENTER, SPACEBAR } from "../../util/keycodes";
6
6
  import { isMultiple } from "../../util/multiple";
7
7
  import { createRandomString } from "../../util/random-string";
8
- import { SelectTemplate, triggerIconColorWarning } from "./select.template";
8
+ import { findTypeaheadMatch, isTypeaheadKey, NO_TYPEAHEAD_MATCH, TypeaheadBuffer, } from "../../util/typeahead";
9
+ import { createMenuItems, SelectTemplate, triggerIconColorWarning, } from "./select.template";
9
10
  /**
11
+ * ## Keyboard
12
+ *
13
+ * Typing characters jumps to the option whose text starts with them, the way
14
+ * a native `<select>` does. Characters accumulate for a short while, so typing
15
+ * `n`, `e` finds "Netherlands" rather than the next option starting with `n`.
16
+ * Pressing the same character repeatedly cycles through all options starting
17
+ * with it. This works both while the dropdown is open, and while the closed
18
+ * component has focus — in which case the dropdown opens with the match
19
+ * highlighted. Typing only moves the highlight; the value is not changed until
20
+ * the option is picked with `Enter` or a click.
21
+ *
10
22
  * @exampleComponent limel-example-select-basic
11
23
  * @exampleComponent limel-example-select-with-icons
12
24
  * @exampleComponent limel-example-select-with-separators
@@ -26,6 +38,15 @@ export class Select {
26
38
  updateHasPrimaryComponent() {
27
39
  this.hasPrimaryComponentMemo = this.computeHasPrimaryComponent();
28
40
  }
41
+ /**
42
+ * `options` and `required` are the inputs that decide which rows the
43
+ * dropdown renders, and therefore which row an index refers to. When
44
+ * either changes, a buffered typeahead match no longer points at what the
45
+ * user was aiming for.
46
+ */
47
+ resetTypeaheadOnItemsChange() {
48
+ this.resetTypeahead();
49
+ }
29
50
  constructor() {
30
51
  /**
31
52
  * Set to `true` to make the field disabled.
@@ -57,10 +78,51 @@ export class Select {
57
78
  this.hasChanged = false;
58
79
  this.hasPrimaryComponentMemo = false;
59
80
  this.checkValid = false;
81
+ this.typeaheadBuffer = new TypeaheadBuffer();
82
+ this.handleMenuTriggerKeyDown = (event) => {
83
+ // Typeahead runs first, so that a space extends an ongoing typeahead
84
+ // instead of just opening the dropdown. A *bare* space is declined by
85
+ // `handleTypeaheadKey`, and keeps its usual meaning below.
86
+ if (this.handleTypeaheadKey(event, NO_TYPEAHEAD_MATCH)) {
87
+ return;
88
+ }
89
+ const isEnter = event.key === ENTER;
90
+ const isSpace = event.key === SPACEBAR;
91
+ if (!this.menuOpen && (isSpace || isEnter)) {
92
+ event.stopPropagation();
93
+ event.preventDefault();
94
+ // `preventDefault` cancels the activation click that the trigger
95
+ // `button` would otherwise synthesize, so the menu has to be
96
+ // opened here rather than through the click handler.
97
+ this.openMenu();
98
+ }
99
+ };
100
+ this.setListElement = (element) => {
101
+ var _a, _b;
102
+ if (this.list === element) {
103
+ return;
104
+ }
105
+ (_a = this.list) === null || _a === void 0 ? void 0 : _a.removeEventListener('keydown', this.handleListKeyDownCapture, true);
106
+ this.list = element;
107
+ (_b = this.list) === null || _b === void 0 ? void 0 : _b.addEventListener('keydown', this.handleListKeyDownCapture, true);
108
+ };
109
+ /**
110
+ * Key handler for the dropdown, in the capture phase.
111
+ *
112
+ * `MDCList` listens for `keydown` on the `ul` inside `limel-list`'s shadow
113
+ * root, so capturing on the `limel-list` element itself is the only place
114
+ * a typed character can be stopped before MDC sees it. It has to be
115
+ * stopped: MDC would add it to its own typeahead buffer, which suppresses
116
+ * selection with `Enter` for as long as that buffer lives, and it treats
117
+ * the space bar as a selection.
118
+ *
119
+ * @param event - the key that was pressed
120
+ */
121
+ this.handleListKeyDownCapture = (event) => {
122
+ this.handleTypeaheadKey(event, this.getFocusedMenuItemIndex());
123
+ };
60
124
  this.handleMenuChange = this.handleMenuChange.bind(this);
61
125
  this.handleNativeChange = this.handleNativeChange.bind(this);
62
- this.handleMenuTriggerKeyPress =
63
- this.handleMenuTriggerKeyPress.bind(this);
64
126
  this.openMenu = this.openMenu.bind(this);
65
127
  this.closeMenu = this.closeMenu.bind(this);
66
128
  this.portalId = createRandomString();
@@ -96,6 +158,7 @@ export class Select {
96
158
  }
97
159
  disconnectedCallback() {
98
160
  this.cancelPendingFocus();
161
+ this.resetTypeahead();
99
162
  if (this.mdcFloatingLabel) {
100
163
  this.mdcFloatingLabel.destroy();
101
164
  }
@@ -110,7 +173,7 @@ export class Select {
110
173
  }
111
174
  render() {
112
175
  const dropdownZIndex = getComputedStyle(this.host).getPropertyValue('--dropdown-z-index');
113
- return (h(SelectTemplate, { key: '8f65bfbadfa797644b4d458452e2eceac2dbcda8', id: this.portalId, disabled: this.disabled || this.readonly, readonly: this.readonly, required: this.required, invalid: this.invalid, label: this.label, helperText: this.helperText, value: this.value, options: this.options, onMenuChange: this.handleMenuChange, onNativeChange: this.handleNativeChange, onTriggerPress: this.handleMenuTriggerKeyPress, multiple: this.multiple, isOpen: this.menuOpen, open: this.openMenu, close: this.closeMenu, checkValid: this.checkValid, native: this.shouldRenderNative(), dropdownZIndex: dropdownZIndex, anchor: this.getAnchorElement() }));
176
+ return (h(SelectTemplate, { key: '97e6b0b033a487e26ab7e4abe7165837ebe08911', id: this.portalId, disabled: this.disabled || this.readonly, readonly: this.readonly, required: this.required, invalid: this.invalid, label: this.label, helperText: this.helperText, value: this.value, options: this.options, onMenuChange: this.handleMenuChange, onNativeChange: this.handleNativeChange, onTriggerKeyDown: this.handleMenuTriggerKeyDown, listRef: this.setListElement, multiple: this.multiple, isOpen: this.menuOpen, open: this.openMenu, close: this.closeMenu, checkValid: this.checkValid, native: this.shouldRenderNative(), dropdownZIndex: dropdownZIndex, anchor: this.getAnchorElement() }));
114
177
  }
115
178
  watchOpen(newValue, oldValue) {
116
179
  if (this.checkValid) {
@@ -145,6 +208,21 @@ export class Select {
145
208
  if (!this.menuOpen) {
146
209
  return;
147
210
  }
211
+ // Consumed on read, so that a later, unrelated re-render
212
+ // cannot resurrect a stale index and yank focus.
213
+ const typeaheadIndex = this.pendingTypeaheadIndex;
214
+ this.pendingTypeaheadIndex = undefined;
215
+ if (typeaheadIndex !== undefined &&
216
+ this.focusMenuItemAtIndex(list, typeaheadIndex)) {
217
+ return;
218
+ }
219
+ // Picking an option while `multiple` re-renders the dropdown
220
+ // without closing it, which brings us back here with a row
221
+ // already focused. Moving to the first option then would lose
222
+ // the user's place in the list after every pick.
223
+ if (this.getFocusedMenuItemIndex() !== NO_TYPEAHEAD_MATCH) {
224
+ return;
225
+ }
148
226
  this.focusFirstMenuItem(list);
149
227
  });
150
228
  this.focusObserver.observe(list);
@@ -167,6 +245,30 @@ export class Select {
167
245
  firstItem.focus({ preventScroll: true });
168
246
  }
169
247
  }
248
+ /**
249
+ * Focus the row at a given index in the dropdown.
250
+ *
251
+ * The rows only become focusable once `limel-list` has set up `MDCList`,
252
+ * which is what gives them a `tabindex`. Callers use the return value to
253
+ * fall back to `pendingTypeaheadIndex` when that has not happened yet.
254
+ *
255
+ * @param list - the `limel-list` of the dropdown
256
+ * @param index - the index of the row, as rendered in its `data-index`
257
+ * @returns whether the row could be focused
258
+ */
259
+ focusMenuItemAtIndex(list, index) {
260
+ var _a;
261
+ const item = (_a = list === null || list === void 0 ? void 0 : list.shadowRoot) === null || _a === void 0 ? void 0 : _a.querySelector(`[data-index="${index}"]`);
262
+ if (!item) {
263
+ return false;
264
+ }
265
+ // Scrolled explicitly, and only within the dropdown: letting `focus()`
266
+ // scroll would scroll the page, since the dropdown is rendered into a
267
+ // portal that is absolutely positioned on the `body`.
268
+ item.focus({ preventScroll: true });
269
+ item.scrollIntoView({ block: 'nearest' });
270
+ return list.shadowRoot.activeElement === item;
271
+ }
170
272
  setTriggerFocus() {
171
273
  const trigger = this.host.shadowRoot.querySelector('.limel-select-trigger');
172
274
  trigger.focus();
@@ -218,6 +320,7 @@ export class Select {
218
320
  this.change.emit(option);
219
321
  this.menuOpen = false;
220
322
  this.cancelPendingFocus();
323
+ this.resetTypeahead();
221
324
  this.setTriggerFocus();
222
325
  }
223
326
  openMenu() {
@@ -251,17 +354,111 @@ export class Select {
251
354
  closeMenu() {
252
355
  this.menuOpen = false;
253
356
  this.cancelPendingFocus();
357
+ this.resetTypeahead();
254
358
  this.setTriggerFocus();
255
359
  }
256
- handleMenuTriggerKeyPress(event) {
257
- const isEnter = event.key === ENTER;
258
- const isSpace = event.key === SPACE;
259
- if (!this.menuOpen && (isSpace || isEnter)) {
260
- event.stopPropagation();
261
- event.preventDefault();
262
- this.menuOpen = true;
360
+ /**
361
+ * Move the highlight to the option matching the characters typed so far,
362
+ * the way a native `<select>` does. Never changes the value — the option
363
+ * still has to be picked with `Enter` or a click.
364
+ *
365
+ * @param event - the key that was pressed
366
+ * @param focusedIndex - the row that currently has focus, if any
367
+ * @returns whether the key was handled as typeahead
368
+ */
369
+ handleTypeaheadKey(event, focusedIndex) {
370
+ // The native dropdown on mobile devices does its own typeahead. Note
371
+ // that `setMenuFocus` bails out on mobile too, so a pending index
372
+ // would never be consumed there.
373
+ if (this.isMobileDevice || !isTypeaheadKey(event)) {
374
+ return false;
375
+ }
376
+ // A space only continues an ongoing typeahead. On its own it keeps its
377
+ // usual meaning of opening the dropdown, or selecting the focused
378
+ // option.
379
+ if (event.key === SPACEBAR && this.typeaheadBuffer.isEmpty) {
380
+ return false;
381
+ }
382
+ event.preventDefault();
383
+ event.stopPropagation();
384
+ // Derived per key press rather than cached, so that the indices always
385
+ // refer to the rows the dropdown renders right now.
386
+ const items = createMenuItems(this.options, this.value, this.required);
387
+ const candidates = items.map((item) => ('separator' in item ? null : item));
388
+ const buffer = this.typeaheadBuffer.append(event.key);
389
+ const currentIndex = this.resolveTypeaheadIndex(items, focusedIndex);
390
+ const index = findTypeaheadMatch(candidates, buffer, currentIndex);
391
+ if (index !== NO_TYPEAHEAD_MATCH) {
392
+ this.focusTypeaheadMatch(index);
393
+ }
394
+ return true;
395
+ }
396
+ /**
397
+ * @param items - the items of the dropdown
398
+ * @param focusedIndex - the row that currently has focus, if any
399
+ * @returns the index a typeahead search should start from
400
+ */
401
+ resolveTypeaheadIndex(items, focusedIndex) {
402
+ if (focusedIndex !== NO_TYPEAHEAD_MATCH) {
403
+ return focusedIndex;
404
+ }
405
+ // Typed again before focus had time to land in the dropdown.
406
+ if (this.pendingTypeaheadIndex !== undefined) {
407
+ return this.pendingTypeaheadIndex;
408
+ }
409
+ // Falls back to the selected option, so that typing continues from
410
+ // wherever the highlight already is. `findIndex` returning `-1` for a
411
+ // select without a value is exactly the "no current row" that
412
+ // `findTypeaheadMatch` expects.
413
+ return items.findIndex((item) => !('separator' in item) && item.selected);
414
+ }
415
+ focusTypeaheadMatch(index) {
416
+ // Overwrites any index still pending from an earlier character, even
417
+ // when this one can be focused right away. Opening the dropdown queues
418
+ // a `setMenuFocus` that waits for it to become visible, and someone
419
+ // typing quickly gets the next character in before that resolves;
420
+ // leaving the earlier index in place would let the queued focus apply
421
+ // it on top of this newer match.
422
+ this.pendingTypeaheadIndex = index;
423
+ if (this.menuOpen && this.focusMenuItemAtIndex(this.list, index)) {
424
+ // Focus landed synchronously, so this index has already done its
425
+ // job. Clearing it — rather than leaving it for `setMenuFocus` to
426
+ // consume later — stops it from being replayed by some unrelated
427
+ // future re-render, such as picking an option in a `multiple`
428
+ // select, after the user has since moved focus elsewhere with the
429
+ // arrow keys. A `setMenuFocus` still queued from opening the
430
+ // dropdown a moment ago is unaffected: it finds this row already
431
+ // focused and leaves it alone, below.
432
+ this.pendingTypeaheadIndex = undefined;
433
+ return;
434
+ }
435
+ if (this.menuOpen) {
436
+ // Already open, so no re-render is coming that would trigger
437
+ // `componentDidUpdate`. Schedule the focus explicitly.
438
+ this.setMenuFocus();
439
+ }
440
+ else {
441
+ this.openMenu();
263
442
  }
264
443
  }
444
+ /**
445
+ * @returns the index of the focused row of the dropdown, or
446
+ * `NO_TYPEAHEAD_MATCH` when no row has focus
447
+ */
448
+ getFocusedMenuItemIndex() {
449
+ var _a, _b, _c;
450
+ // Deliberately not `event.target`: a listener on the `limel-list`
451
+ // element sees events from inside its shadow root retargeted to the
452
+ // element itself, never to the row that was actually focused.
453
+ const focused = (_b = (_a = this.list) === null || _a === void 0 ? void 0 : _a.shadowRoot) === null || _b === void 0 ? void 0 : _b.activeElement;
454
+ const row = focused === null || focused === void 0 ? void 0 : focused.closest('[data-index]');
455
+ const index = Number.parseInt((_c = row === null || row === void 0 ? void 0 : row.dataset.index) !== null && _c !== void 0 ? _c : '', 10);
456
+ return Number.isNaN(index) ? NO_TYPEAHEAD_MATCH : index;
457
+ }
458
+ resetTypeahead() {
459
+ this.typeaheadBuffer.clear();
460
+ this.pendingTypeaheadIndex = undefined;
461
+ }
265
462
  handleNativeChange(event) {
266
463
  event.stopPropagation();
267
464
  const element = this.host.shadowRoot.querySelector('select.limel-select__native-control');
@@ -543,6 +740,12 @@ export class Select {
543
740
  }, {
544
741
  "propName": "options",
545
742
  "methodName": "updateHasPrimaryComponent"
743
+ }, {
744
+ "propName": "options",
745
+ "methodName": "resetTypeaheadOnItemsChange"
746
+ }, {
747
+ "propName": "required",
748
+ "methodName": "resetTypeaheadOnItemsChange"
546
749
  }, {
547
750
  "propName": "menuOpen",
548
751
  "methodName": "watchOpen"
@@ -52,7 +52,7 @@ const SelectValue = (props) => {
52
52
  'limel-select-trigger': true,
53
53
  'limel-select--focused': props.isOpen,
54
54
  };
55
- return (h("button", { slot: "content", class: anchorClassList, onClick: props.open, onKeyPress: props.onTriggerPress, "aria-haspopup": "listbox", "aria-expanded": props.isOpen, "aria-controls": props.id, "aria-labelledby": "s-label s-selected-text", "aria-required": props.required, disabled: props.disabled || props.readonly }, h("span", { class: "mdc-select__selected-text-container limel-select__selected-option" }, getSelectedIcon(props.value), getSelectedPrimaryComponent(props.value), h("span", { id: "s-selected-text", class: "mdc-select__selected-text limel-select__selected-option__text" }, getSelectedText(props.value))), h(ShowIcon, Object.assign({}, props, { isValid: props.isValid })), h("span", { class: "mdc-select__dropdown-icon" }, h("svg", { class: "mdc-select__dropdown-icon-graphic", viewBox: "7 10 10 5", focusable: "false" }, h("polygon", { stroke: "none", "fill-rule": "evenodd", points: "7 10 12 15 17 10" })))));
55
+ return (h("button", { slot: "content", class: anchorClassList, onClick: props.open, onKeyDown: props.onTriggerKeyDown, "aria-haspopup": "listbox", "aria-expanded": props.isOpen, "aria-controls": props.id, "aria-labelledby": "s-label s-selected-text", "aria-required": props.required, disabled: props.disabled || props.readonly }, h("span", { class: "mdc-select__selected-text-container limel-select__selected-option" }, getSelectedIcon(props.value), getSelectedPrimaryComponent(props.value), h("span", { id: "s-selected-text", class: "mdc-select__selected-text limel-select__selected-option__text" }, getSelectedText(props.value))), h(ShowIcon, Object.assign({}, props, { isValid: props.isValid })), h("span", { class: "mdc-select__dropdown-icon" }, h("svg", { class: "mdc-select__dropdown-icon-graphic", viewBox: "7 10 10 5", focusable: "false" }, h("polygon", { stroke: "none", "fill-rule": "evenodd", points: "7 10 12 15 17 10" })))));
56
56
  };
57
57
  const ShowIcon = (props) => {
58
58
  if (props.isValid) {
@@ -80,7 +80,7 @@ const MenuDropdown = (props) => {
80
80
  display: 'flex',
81
81
  'min-width': '100%',
82
82
  width: 'fit-content',
83
- } }, h("limel-list", { items: items, type: props.multiple ? 'checkbox' : 'selectable', onChange: props.onMenuChange }))));
83
+ } }, h("limel-list", { ref: props.listRef, items: items, type: props.multiple ? 'checkbox' : 'selectable', onChange: props.onMenuChange }))));
84
84
  };
85
85
  const NativeDropdown = (props) => {
86
86
  const options = props.options
@@ -103,7 +103,21 @@ function isSelected(option, value) {
103
103
  }
104
104
  return option.value === value.value;
105
105
  }
106
- function createMenuItems(options, value, selectIsRequired = false) {
106
+ /**
107
+ * Create the items for the dropdown.
108
+ *
109
+ * The returned array is index-aligned with the `data-index` attribute that
110
+ * `limel-list` renders on each row. Separators are included, and occupy an
111
+ * index. Anything that needs to address a row by index — such as the
112
+ * typeahead in `select.tsx` — must derive its indices from this same array.
113
+ *
114
+ * @param options - the options of the select
115
+ * @param value - the currently selected option or options
116
+ * @param selectIsRequired - whether the select requires a value, in which
117
+ * case the "empty" option is left out
118
+ * @returns the items to render in the dropdown
119
+ */
120
+ export function createMenuItems(options, value, selectIsRequired = false) {
107
121
  const menuOptionFilter = getMenuOptionFilter(selectIsRequired);
108
122
  return options.filter(menuOptionFilter).map((option) => {
109
123
  if ('separator' in option) {
@@ -11,6 +11,7 @@ import { createImageInserterPlugin } from "./plugins/image/inserter";
11
11
  import { createImageViewPlugin } from "./plugins/image/view";
12
12
  import { createMenuStateTrackingPlugin } from "./plugins/menu-state-tracking-plugin";
13
13
  import { createActionBarInteractionPlugin } from "./plugins/menu-action-interaction-plugin";
14
+ import { createListKeyHandlerPlugin } from "./plugins/list-key-handler";
14
15
  import { createTriggerPlugin } from "./plugins/trigger/factory";
15
16
  import { getTableNodes, getTableEditingPlugins } from "./plugins/table-plugin";
16
17
  import { getImageNode } from "./plugins/image/node";
@@ -62,7 +63,16 @@ export function buildEditorSchema(options) {
62
63
  export function buildEditorPlugins(options) {
63
64
  const { schema, menuCommandFactory, contentConverter, language, contentType, triggerCharacters, inlineImages, onNewLinkSelection, onImagePasted, onActiveItemsChange, } = options;
64
65
  return [
65
- ...exampleSetup({ schema: schema, menuBar: false }),
66
+ // exampleSetup binds Shift-Ctrl-8/9 to a plain wrapInList. On
67
+ // Windows/Linux those are the same physical keys as our
68
+ // Mod-Shift-8/7 list commands, and exampleSetup's keymap runs
69
+ // first, so its bindings are suppressed to let the context-aware
70
+ // list commands own the shortcuts on every platform.
71
+ ...exampleSetup({
72
+ schema: schema,
73
+ menuBar: false,
74
+ mapKeys: { 'Shift-Ctrl-8': false, 'Shift-Ctrl-9': false },
75
+ }),
66
76
  keymap(menuCommandFactory.buildKeymap()),
67
77
  createTriggerPlugin(triggerCharacters, contentConverter),
68
78
  createLinkPlugin(onNewLinkSelection),
@@ -70,6 +80,7 @@ export function buildEditorPlugins(options) {
70
80
  createImageViewPlugin(language),
71
81
  createMenuStateTrackingPlugin(editorMenuTypesArray, menuCommandFactory, onActiveItemsChange),
72
82
  createActionBarInteractionPlugin(menuCommandFactory),
83
+ createListKeyHandlerPlugin(schema),
73
84
  ...getTableEditingPlugins(contentType === 'html'),
74
85
  ];
75
86
  }
@@ -0,0 +1,36 @@
1
+ import { LevelMapping } from "../types";
2
+ export const setActiveMethodForMark = (command, markType) => {
3
+ command.active = (state) => {
4
+ const { from, $from, to, empty } = state.selection;
5
+ if (empty) {
6
+ return !!markType.isInSet(state.storedMarks || $from.marks());
7
+ }
8
+ return state.doc.rangeHasMark(from, to, markType);
9
+ };
10
+ };
11
+ export const setActiveMethodForNode = (command, nodeType, level) => {
12
+ command.active = (state) => {
13
+ const { $from } = state.selection;
14
+ const node = $from.node($from.depth);
15
+ if (node && node.type.name === nodeType.name) {
16
+ if (nodeType.name === LevelMapping.Heading && level) {
17
+ return node.attrs.level === level;
18
+ }
19
+ return true;
20
+ }
21
+ return false;
22
+ };
23
+ };
24
+ export const setActiveMethodForWrap = (command, nodeType) => {
25
+ command.active = (state) => {
26
+ const { from, to } = state.selection;
27
+ let found = false;
28
+ state.doc.nodesBetween(from, to, (node) => {
29
+ if (node.type === nodeType) {
30
+ found = true;
31
+ }
32
+ return !found;
33
+ });
34
+ return found;
35
+ };
36
+ };