@optionfactory/fml 9.0.0-rc11 → 9.0.0-rc13

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/dist/ful.mjs CHANGED
@@ -1412,15 +1412,22 @@ const INPUT_MODES = { numeric: 'numeric', decimal: 'decimal' };
1412
1412
 
1413
1413
  const signed = (v, digits) => (v.startsWith('-') ? '-' : '') + digits(v);
1414
1414
 
1415
- const NUMERIC_FILTERS = {
1416
- numeric: (v) => signed(v, (t) => t.replace(/\D/g, '')),
1415
+ const NUMERIC_DIGITS = {
1416
+ numeric: (t) => t.replace(/\D/g, ''),
1417
1417
  //one separator, the first: the rest are a slip while typing, not a value
1418
- decimal: (v) =>
1419
- signed(v, (t) => {
1420
- const kept = t.replace(/[^\d.,]/g, '');
1421
- const at = kept.search(/[.,]/);
1422
- return at === -1 ? kept : kept.slice(0, at + 1) + kept.slice(at + 1).replace(/[.,]/g, '');
1423
- }),
1418
+ decimal: (t) => {
1419
+ const kept = t.replace(/[^\d.,]/g, '');
1420
+ const at = kept.search(/[.,]/);
1421
+ return at === -1 ? kept : kept.slice(0, at + 1) + kept.slice(at + 1).replace(/[.,]/g, '');
1422
+ },
1423
+ };
1424
+
1425
+ const numericFilter = (type, unsigned) => {
1426
+ const digits = NUMERIC_DIGITS[type];
1427
+ if (!digits) {
1428
+ return null;
1429
+ }
1430
+ return unsigned ? digits : (v) => signed(v, digits);
1424
1431
  };
1425
1432
 
1426
1433
  const warnedBoth = new WeakSet();
@@ -1442,7 +1449,7 @@ const filterOf = (el) => {
1442
1449
  const re = compiled('reject', reject);
1443
1450
  return re && ((v) => v.replace(re, ''));
1444
1451
  }
1445
- return NUMERIC_FILTERS[el._type()] ?? null;
1452
+ return numericFilter(el._type(), el.declared('unsigned'));
1446
1453
  };
1447
1454
 
1448
1455
  /** A labelled text input over any native type or textarea; the temporal inputs are its subclasses. */
@@ -1457,6 +1464,7 @@ class Input extends Field {
1457
1464
  'reject',
1458
1465
  'uppercase:presence',
1459
1466
  'trim:presence',
1467
+ 'unsigned:presence',
1460
1468
  'autocomplete',
1461
1469
  ];
1462
1470
  static slots = true;
@@ -2294,7 +2302,18 @@ class Anchors {
2294
2302
  if (invoke) {
2295
2303
  //popovertarget needs a target that can be named
2296
2304
  popover.id = popover.id || uid;
2297
- invoker.setAttribute('popovertarget', popover.id);
2305
+ if (invoker.localName === 'button' || invoker.localName === 'input') {
2306
+ invoker.setAttribute('popovertarget', popover.id);
2307
+ } else {
2308
+ invoker.addEventListener('click', () => popover.togglePopover());
2309
+ invoker.addEventListener('keydown', (/** @type any */ evt) => {
2310
+ if (evt.key !== 'Enter' && evt.key !== ' ') {
2311
+ return;
2312
+ }
2313
+ evt.preventDefault();
2314
+ popover.togglePopover();
2315
+ });
2316
+ }
2298
2317
  }
2299
2318
  const anchor = `--${uid}`;
2300
2319
  invoker.style.anchorName = anchor;
@@ -2906,7 +2925,7 @@ class Select extends Field {
2906
2925
  //the select's: a tooltip marker slotted into `info`, a button a page
2907
2926
  //put in an affix. Without this, reading the note beside a select
2908
2927
  //also stole the focus and dropped the dropdown over the note
2909
- const elsewhere = e.target.closest('button, a[href], input, select, textarea');
2928
+ const elsewhere = e.target.closest('button, [role="button"], a[href], input, select, textarea');
2910
2929
  if (elsewhere && elsewhere !== this.#input) {
2911
2930
  return;
2912
2931
  }
@@ -4836,27 +4855,38 @@ class BooleanFilter extends Field {
4836
4855
  * operator, differing only in what the server converts them to.
4837
4856
  */
4838
4857
  class InFilter extends Select {
4839
- async upgrade() {
4840
- //the snapshot is taken inside super.upgrade(), so an attribute this
4841
- //element does not let an author turn off has to be written before it
4842
- this.toggleAttribute('multiple', true);
4843
- await super.upgrade();
4858
+ static attributes = ['operator'];
4859
+ #keys() {
4860
+ const chosen = super.value;
4861
+ if (this.multiple) {
4862
+ return chosen;
4863
+ }
4864
+ return chosen === null || chosen === undefined ? [] : [chosen];
4844
4865
  }
4845
4866
  get value() {
4846
- const keys = super.value;
4867
+ const keys = this.#keys();
4847
4868
  //an empty set is no filter: the table drops a falsy value, where an
4848
4869
  //empty array would travel and match nothing
4849
- return keys.length === 0 ? null : keys;
4870
+ if (keys.length === 0) {
4871
+ return null;
4872
+ }
4873
+ const operator = this.declared('operator');
4874
+ return operator ? [operator, ...keys] : keys;
4850
4875
  }
4851
4876
  set value(v) {
4852
- super.value = v ?? [];
4877
+ const operator = this.declared('operator');
4878
+ let keys = v === null || v === undefined ? [] : Array.isArray(v) ? v : [v];
4879
+ if (operator && keys[0] === operator) {
4880
+ keys = keys.slice(1);
4881
+ }
4882
+ super.value = this.multiple ? keys : (keys[0] ?? null);
4853
4883
  }
4854
4884
  /** @returns {FilterCriterion|null} */
4855
4885
  get criterion() {
4856
4886
  return asCriterion(
4857
4887
  labelTextOf(this),
4858
- null,
4859
- (super.entry ?? []).map((e) => e.label),
4888
+ this.declared('operator'),
4889
+ [super.entry].flat().filter((e) => e).map((e) => e.label),
4860
4890
  );
4861
4891
  }
4862
4892
  }
@@ -4992,7 +5022,7 @@ class Tooltip extends ParsedElement {
4992
5022
  icon: 'info-circle-fill',
4993
5023
  };
4994
5024
  static template = `
4995
- <button type="button" class="ful-tip" data-ref="trigger" data-tpl-aria-label="#l10n:t('info.tooltip')"><ful-icon data-tpl-name="icon ?? config.icon" aria-hidden="true"></ful-icon></button>
5025
+ <span role="button" tabindex="0" class="ful-tip" data-ref="trigger" data-tpl-aria-label="#l10n:t('info.tooltip')"><ful-icon data-tpl-name="icon ?? config.icon" aria-hidden="true"></ful-icon></span>
4996
5026
  <ful-note popover data-ref="content">{{{{ slots.default }}}}</ful-note>
4997
5027
  `;
4998
5028
  render({ slots }) {
@@ -5101,10 +5131,18 @@ class Dialog extends ParsedElement {
5101
5131
  this.dispatchEvent(new CustomEvent('close', { detail: outcome }));
5102
5132
  this.#settle(outcome);
5103
5133
  });
5134
+ let pressedOutside = false;
5135
+ this.#dialog.addEventListener('mousedown', (/** @type any */ e) => {
5136
+ pressedOutside = e.target === this.#dialog;
5137
+ });
5104
5138
  this.#dialog.addEventListener('click', (/** @type any */ e) => {
5105
5139
  const result = e.target.closest('button[data-result]')?.dataset.result;
5106
5140
  if (result !== undefined) {
5107
5141
  this.#dialog.close(result);
5142
+ return;
5143
+ }
5144
+ if (!requiresAnswer && pressedOutside && e.target === this.#dialog) {
5145
+ this.#dialog.close('');
5108
5146
  }
5109
5147
  });
5110
5148
  //dismissal, not an answer: the waiters are settled with a dismissal, as
@@ -5306,6 +5344,15 @@ class Drawer extends ParsedElement {
5306
5344
  this.#dialog.setAttribute('placement', placement);
5307
5345
  }
5308
5346
  fragment.querySelector('[data-ref=close]').addEventListener('click', () => this.close());
5347
+ let pressedOutside = false;
5348
+ this.#dialog.addEventListener('mousedown', (e) => {
5349
+ pressedOutside = e.target === this.#dialog;
5350
+ });
5351
+ this.#dialog.addEventListener('click', (e) => {
5352
+ if (pressedOutside && e.target === this.#dialog) {
5353
+ this.close();
5354
+ }
5355
+ });
5309
5356
  this.#dialog.addEventListener('close', () => {
5310
5357
  this.dispatchEvent(
5311
5358
  new CustomEvent('close', { detail: this.#answer ?? { dismissed: true, response: null } }),