@pacem/pacem-core 2.0.0-grothendieck → 2.0.0-hadamard

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 (40) hide show
  1. package/dist/browser/pacem-core.js +308 -89
  2. package/dist/browser/pacem-core.js.map +1 -1
  3. package/dist/browser/pacem-core.min.js +2 -2
  4. package/dist/bundle/pacem-core.min.mjs +5 -5
  5. package/dist/bundle/pacem-core.mjs +193 -67
  6. package/dist/bundle/pacem-core.mjs.map +3 -3
  7. package/dist/docs/pacem-core.json +54 -24
  8. package/dist/esm/adapter.js +1 -1
  9. package/dist/esm/binding-parser.js +3 -3
  10. package/dist/esm/command.js +1 -1
  11. package/dist/esm/comparers.js +1 -1
  12. package/dist/esm/converters.js +1 -1
  13. package/dist/esm/decorators-next.js +1 -1
  14. package/dist/esm/decorators.js +1 -1
  15. package/dist/esm/defaults.js +1 -1
  16. package/dist/esm/events.js +1 -1
  17. package/dist/esm/eventtarget.js +1 -1
  18. package/dist/esm/expression.js +1 -1
  19. package/dist/esm/index-animations.js +1 -1
  20. package/dist/esm/index-behaviors.js +1 -1
  21. package/dist/esm/index-components.js +1 -1
  22. package/dist/esm/index-iife.js +1 -1
  23. package/dist/esm/index-logging.js +1 -1
  24. package/dist/esm/index-net.js +1 -1
  25. package/dist/esm/index-root.js +1 -1
  26. package/dist/esm/index-ui.js +1 -1
  27. package/dist/esm/index.js +1 -1
  28. package/dist/esm/logger.js +1 -1
  29. package/dist/esm/prefix.js +1 -1
  30. package/dist/esm/promise.js +1 -1
  31. package/dist/esm/regex.js +1 -1
  32. package/dist/esm/storage.js +1 -1
  33. package/dist/esm/template.js +1 -1
  34. package/dist/esm/transforms.js +1 -1
  35. package/dist/esm/types.js +1 -1
  36. package/dist/esm/utils-customelement.js +1 -1
  37. package/dist/esm/utils-customevent.js +1 -1
  38. package/dist/esm/utils.js +2 -2
  39. package/package.json +1 -1
  40. package/typings/index.d.ts +114 -20
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * @pacem/pacem-core v2.0.0-grothendieck (https://js.pacem.it)
2
+ * @pacem/pacem-core v2.0.0-hadamard (https://js.pacem.it)
3
3
  * Pacem (https://pacem.it)
4
4
  * Licensed under Apache-2.0
5
5
  */
@@ -409,7 +409,7 @@
409
409
  },
410
410
  /** Sets a CSS custom property's value on the document root (`:root`/`html`). */
411
411
  setVariable(name, value) {
412
- getComputedStyle(document.documentElement).setProperty(name, value);
412
+ document.documentElement.style.setProperty(name, value);
413
413
  },
414
414
  /**
415
415
  * Naive O(n^2) implementation for an actual css class presence (definition) in the current DOM.
@@ -1555,12 +1555,12 @@
1555
1555
  * Parses a binding expression against a scope.
1556
1556
  * @param expression The bare expression text (no `{{ }}` delimiters, no `, twoway`/`, once` modifier).
1557
1557
  * @param scope What the references resolve to.
1558
- * @throws When the expression is unsafe (see {@link analyze}).
1558
+ * @throws An `Error` when the expression is unsafe (see {@link analyze}).
1559
1559
  */
1560
1560
  static parse(expression, scope) {
1561
1561
  const analysis = BindingParser.analyze(expression);
1562
1562
  if (analysis.unsafe != null) {
1563
- throw `Unsafe expression detected: "${analysis.unsafe}" in "${expression}".`;
1563
+ throw new Error(`Unsafe expression detected: "${analysis.unsafe}" in "${expression}".`);
1564
1564
  }
1565
1565
  if (!analysis.hasReferences) {
1566
1566
  return { kind: 'constant', functionBody: `return ${expression};`, voidBody: expression, dependencies: [] };
@@ -5009,6 +5009,14 @@
5009
5009
  get element() {
5010
5010
  return this._builder.element;
5011
5011
  }
5012
+ /**
5013
+ * @readonly Gets the snap the element is going to rest at, relative to where the gesture started: `0` means it
5014
+ * bounces back home (the release didn't make it). Known on release (`swipe`, `swipe<direction>` and the
5015
+ * animation-end events); `undefined` while panning.
5016
+ */
5017
+ get snap() {
5018
+ return this._builder.snap;
5019
+ }
5012
5020
  /** Builds a new {@link SwipeEventArgsClass} out of a plain {@link SwipeEventArgs} builder. */
5013
5021
  static fromArgs(builder) {
5014
5022
  return new SwipeEventArgsClass(Utils.extend({}, builder));
@@ -5402,34 +5410,58 @@
5402
5410
  return html;
5403
5411
  }
5404
5412
  }
5405
- /** {@link PacemContentElement} variant that strips out `<script>` blocks (and, for `Node` content, `<script>` elements) before rendering `content`. */
5413
+ /** Attributes holding a URL, checked for `javascript:` ones by {@link sanitizeNode}. */
5414
+ const URL_ATTRIBUTES = ['href', 'src', 'action', 'formaction', 'xlink:href'];
5415
+ /** Whether an attribute value is a `javascript:` URL (whitespace and control characters ignored, as browsers do). */
5416
+ function isScriptUrl(value) {
5417
+ const normalized = (value ?? '').replace(/[\u0000- ]/g, '').toLowerCase();
5418
+ return normalized.startsWith('javascript:');
5419
+ }
5420
+ /**
5421
+ * Strips the script-capable parts of a DOM subtree, in place: `<script>` elements, `on*` event-handler attributes,
5422
+ * `javascript:` URLs.
5423
+ * @param node The subtree's root.
5424
+ */
5425
+ function sanitizeNode(node) {
5426
+ for (let i = node.childNodes.length - 1; i >= 0; i--) {
5427
+ const child = node.childNodes.item(i);
5428
+ if (child instanceof Element && child.localName === 'script') {
5429
+ child.remove();
5430
+ continue;
5431
+ }
5432
+ sanitizeNode(child);
5433
+ }
5434
+ if (!(node instanceof Element)) {
5435
+ return;
5436
+ }
5437
+ for (const attribute of Array.from(node.attributes)) {
5438
+ const name = attribute.name.toLowerCase();
5439
+ const isHandler = name.startsWith('on');
5440
+ const isScriptLink = URL_ATTRIBUTES.indexOf(name) >= 0 && isScriptUrl(attribute.value);
5441
+ if (isHandler || isScriptLink) {
5442
+ node.removeAttribute(attribute.name);
5443
+ }
5444
+ }
5445
+ }
5446
+ /**
5447
+ * {@link PacemContentElement} variant that renders `content` without what could run script: `<script>` elements,
5448
+ * `on*` event-handler attributes and `javascript:` URLs get removed (HTML strings are parsed, never executed, first).
5449
+ */
5406
5450
  class PacemSafeContentElement extends PacemContentElement {
5407
- _sanitizeLegacy(html) {
5451
+ sanitize(html) {
5408
5452
  if (Utils.isNullOrEmpty(html)) {
5409
5453
  return html;
5410
5454
  }
5411
5455
  if (html instanceof Node) {
5412
- for (let i = html.childNodes.length - 1; i >= 0; i--) {
5413
- const node = html.childNodes.item(i);
5414
- if (node instanceof HTMLScriptElement) {
5415
- node.remove();
5416
- }
5417
- else {
5418
- this.sanitize(node);
5419
- }
5420
- }
5456
+ sanitizeNode(html);
5421
5457
  return html;
5422
5458
  }
5423
- else {
5424
- const scriptBlockPattern = /<script.+<\/script>/g;
5425
- // TODO: remove onclick, onload, onkey*, ... attributes
5426
- return (html || '').toString().replace(scriptBlockPattern, '');
5427
- }
5428
- }
5429
- sanitize(html) {
5430
- // TODO: implement by using (when available in dom library)
5431
- // https://developer.mozilla.org/en-US/docs/Web/API/Sanitizer/Sanitizer
5432
- return this._sanitizeLegacy(html);
5459
+ // a template parses without running anything (scripts, image loads with their handlers…)
5460
+ const template = document.createElement('template');
5461
+ template.innerHTML = html.toString();
5462
+ const fragment = template.content;
5463
+ sanitizeNode(fragment);
5464
+ return fragment;
5433
5465
  }
5434
5466
  }
5435
5467
  //}
@@ -5658,7 +5690,11 @@
5658
5690
  return c > 3 && r && Object.defineProperty(target, key, r), r;
5659
5691
  };
5660
5692
  //namespace Pacem.Components {
5661
- /** Wraps a {@link BroadcastChannel}, sending/receiving messages to/from other same-origin browsing contexts. */
5693
+ /**
5694
+ * Wraps a {@link BroadcastChannel}, sending/receiving messages to/from other same-origin browsing contexts.
5695
+ * @fires receive {@link CustomEvent} - when a message arrives from another channel of the same name: `detail` is the message (also in `result`).
5696
+ * @fires error {@link CustomEvent} - when an arrived message couldn't be deserialized.
5697
+ */
5662
5698
  let PacemBroadcastChannelProxyElement = class PacemBroadcastChannelProxyElement extends PacemEventTarget {
5663
5699
  constructor() {
5664
5700
  super(...arguments);
@@ -5680,7 +5716,7 @@
5680
5716
  super.propertyChangedCallback(name, old, val, first);
5681
5717
  if (!first) {
5682
5718
  switch (name) {
5683
- case 'src':
5719
+ case 'channelName':
5684
5720
  this._disposeChannel();
5685
5721
  this._initializeChannel(val);
5686
5722
  break;
@@ -5728,6 +5764,10 @@
5728
5764
  channel.removeEventListener('message', this._messageHandler, false);
5729
5765
  channel.close();
5730
5766
  }
5767
+ // (a closed channel can't post anymore)
5768
+ if (channel === this.#channel) {
5769
+ this.#channel = null;
5770
+ }
5731
5771
  }
5732
5772
  };
5733
5773
  __decorate$D([
@@ -5804,13 +5844,32 @@
5804
5844
  return c > 3 && r && Object.defineProperty(target, key, r), r;
5805
5845
  };
5806
5846
  //namespace Pacem.Components {
5807
- /** Holds an arbitrary data model, optionally persisting it to local storage and debouncing/throttling change notifications. */
5847
+ /** Converts the `debounce` attribute: `true`/`false`, or else a number of milliseconds. */
5848
+ const DebouncePropertyConverter = {
5849
+ convert: (attr) => {
5850
+ switch (attr) {
5851
+ case 'true':
5852
+ return true;
5853
+ case 'false':
5854
+ return false;
5855
+ default:
5856
+ return parseFloat(attr);
5857
+ }
5858
+ },
5859
+ convertBack: (prop) => `${prop}`
5860
+ };
5861
+ /**
5862
+ * Holds an arbitrary data model, optionally persisting it to local storage and debouncing/throttling change notifications.
5863
+ * @fires propertychange {@link PropertyChangeEvent} - when `model` changes (debounced or throttled as set by `debounce` and `throttle`).
5864
+ */
5808
5865
  let PacemDataElement = class PacemDataElement extends PacemEventTarget {
5809
5866
  constructor(storage = new Storage()) {
5810
5867
  super();
5811
5868
  this.storage = storage;
5812
- this._handle = 0;
5813
- this._flag = false;
5869
+ /** Cancels the pending (debounced) notification, if any. */
5870
+ this._cancelPending = null;
5871
+ /** Whether a throttling window is open (changes are dropped meanwhile). */
5872
+ this._throttling = false;
5814
5873
  }
5815
5874
  propertyChangedCallback(name, old, val, first) {
5816
5875
  super.propertyChangedCallback(name, old, val, first);
@@ -5824,35 +5883,63 @@
5824
5883
  }
5825
5884
  break;
5826
5885
  case 'model':
5827
- const debounce = this.debounce, throttle = this.throttle, emit = () => {
5828
- this.dispatchEvent(new PropertyChangeEvent({ propertyName: name, oldValue: old, currentValue: val, firstChange: first }));
5829
- };
5830
- if (debounce > 0 && !first) {
5831
- if (throttle) {
5832
- if (this._flag)
5833
- return;
5834
- this._flag = true;
5835
- emit();
5836
- this._handle = window.setTimeout(() => { this._flag = false; }, debounce);
5837
- }
5838
- else {
5839
- clearTimeout(this._handle);
5840
- this._handle = window.setTimeout(emit, debounce);
5841
- }
5842
- }
5843
- else {
5844
- emit();
5845
- }
5846
- // persist
5847
- if (!first && !Utils.isNullOrEmpty(this.persistAs)) {
5848
- if (Utils.isNull(val))
5849
- this.storage.removeProperty(this.persistAs);
5850
- else
5851
- this.storage.setPropertyValue(this.persistAs, val, true);
5852
- }
5886
+ this._notifyModelChange(old, val, first);
5887
+ this._persist(val, first);
5853
5888
  break;
5854
5889
  }
5855
5890
  }
5891
+ /** Notifies a model change: right away, debounced or throttled (see {@link debounce} and {@link throttle}). */
5892
+ _notifyModelChange(old, val, first) {
5893
+ const emit = () => {
5894
+ this.dispatchEvent(new PropertyChangeEvent({ propertyName: 'model', oldValue: old, currentValue: val, firstChange: first }));
5895
+ };
5896
+ const debounce = this.debounce;
5897
+ const perFrame = debounce === true;
5898
+ const delay = typeof debounce === 'number' && debounce > 0 ? debounce : 0;
5899
+ if (first || (!perFrame && delay === 0)) {
5900
+ emit();
5901
+ return;
5902
+ }
5903
+ if (this.throttle) {
5904
+ this._throttle(emit, perFrame, delay);
5905
+ return;
5906
+ }
5907
+ this._cancelPending?.();
5908
+ this._cancelPending = this._schedule(emit, perFrame, delay);
5909
+ }
5910
+ /** Notifies the first change of a window, and drops the others until the window closes. */
5911
+ _throttle(emit, perFrame, delay) {
5912
+ if (this._throttling) {
5913
+ return;
5914
+ }
5915
+ this._throttling = true;
5916
+ emit();
5917
+ this._schedule(() => { this._throttling = false; }, perFrame, delay);
5918
+ }
5919
+ /**
5920
+ * Runs a callback on the next animation frame, or after a delay.
5921
+ * @returns A function cancelling it.
5922
+ */
5923
+ _schedule(callback, perFrame, delay) {
5924
+ if (perFrame) {
5925
+ const frame = requestAnimationFrame(callback);
5926
+ return () => cancelAnimationFrame(frame);
5927
+ }
5928
+ const timeout = window.setTimeout(callback, delay);
5929
+ return () => clearTimeout(timeout);
5930
+ }
5931
+ /** Persists the model under {@link persistAs}, if set (a `null` model removes it). */
5932
+ _persist(val, first) {
5933
+ const key = this.persistAs;
5934
+ if (first || Utils.isNullOrEmpty(key)) {
5935
+ return;
5936
+ }
5937
+ if (Utils.isNull(val)) {
5938
+ this.storage.removeProperty(key);
5939
+ return;
5940
+ }
5941
+ this.storage.setPropertyValue(key, val, true);
5942
+ }
5856
5943
  };
5857
5944
  __decorate$B([
5858
5945
  Watch({ emit: false /* in order to debounce the propertychange emit */, converter: PropertyConverters.Eval })
@@ -5861,7 +5948,7 @@
5861
5948
  Watch({ emit: false, converter: PropertyConverters.String })
5862
5949
  ], PacemDataElement.prototype, "persistAs", void 0);
5863
5950
  __decorate$B([
5864
- Watch({ emit: false, converter: PropertyConverters.Number })
5951
+ Watch({ emit: false, converter: DebouncePropertyConverter })
5865
5952
  ], PacemDataElement.prototype, "debounce", void 0);
5866
5953
  __decorate$B([
5867
5954
  Watch({ emit: false, converter: PropertyConverters.BooleanStrict })
@@ -6106,7 +6193,10 @@
6106
6193
  return PropertyConverters.Element.convertBack(prop, element);
6107
6194
  }
6108
6195
  };
6109
- /** Listens for an arbitrary DOM event on a `target` element (or `window`) and re-dispatches it as its own `emit` event. */
6196
+ /**
6197
+ * Listens for an arbitrary DOM event on a `target` element (or `window`) and re-dispatches it as its own `emit` event.
6198
+ * @fires emit {@link Event} - for every `event` caught on `target`: the caught one is `originalEvent`.
6199
+ */
6110
6200
  let PacemEventProxyElement = class PacemEventProxyElement extends PacemEventTarget {
6111
6201
  constructor() {
6112
6202
  super(...arguments);
@@ -6179,7 +6269,12 @@
6179
6269
  };
6180
6270
  //namespace Pacem.Components {
6181
6271
  const ABORT_MESSAGE = "New incoming request overrides the ongoing one. Aborting.";
6182
- /** Declarative wrapper around the `fetch()` API, debouncing/aborting requests as its properties change and exposing the parsed `result`. */
6272
+ /**
6273
+ * Declarative wrapper around the `fetch()` API, debouncing/aborting requests as its properties change and exposing the parsed `result`.
6274
+ * @fires success {@link CustomEvent} - when a response is OK (2xx), before its body gets parsed: `detail` is the `Response`.
6275
+ * @fires fetchresult {@link CustomEvent} - when the body got parsed (as set by `as`): `detail` is the new `result`.
6276
+ * @fires error {@link CustomEvent} - when a response is not OK: `detail` is the `Response` (network failures reject `fetch()` instead).
6277
+ */
6183
6278
  let PacemFetchElement = class PacemFetchElement extends PacemEventTarget {
6184
6279
  constructor() {
6185
6280
  super(...arguments);
@@ -6447,7 +6542,7 @@
6447
6542
  Watch({ reflectBack: true, converter: PropertyConverters.Number })
6448
6543
  ], PacemFetchElement.prototype, "debounce", void 0);
6449
6544
  __decorate$x([
6450
- Watch({ emit: false, reflectBack: true, converter: PropertyConverters.Number })
6545
+ Watch({ emit: false, reflectBack: true, converter: PropertyConverters.Boolean })
6451
6546
  ], PacemFetchElement.prototype, "throttle", void 0);
6452
6547
  __decorate$x([
6453
6548
  Watch({ emit: false, reflectBack: true, converter: PropertyConverters.Boolean })
@@ -6471,7 +6566,13 @@
6471
6566
  super(type, { detail });
6472
6567
  }
6473
6568
  }
6474
- /** Keeps track of an undo/redo-capable history of an arbitrary state. */
6569
+ /**
6570
+ * Keeps track of an undo/redo-capable history of an arbitrary state.
6571
+ * @fires push {@link StateEvent} - when a new state got recorded: `detail` has `state`, `canUndo` and `canRedo`.
6572
+ * @fires undo {@link StateEvent} - after a step back.
6573
+ * @fires redo {@link StateEvent} - after a step forward.
6574
+ * @fires reset {@link StateEvent} - when the history got cleared (the current state stays).
6575
+ */
6475
6576
  let PacemHistoryElement = class PacemHistoryElement extends PacemEventTarget {
6476
6577
  #history;
6477
6578
  propertyChangedCallback(name, old, val, first) {
@@ -6974,7 +7075,10 @@
6974
7075
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6975
7076
  };
6976
7077
  //namespace Pacem.Components {
6977
- /** Tracks the browser's network connectivity (`navigator.onLine`), updating `online` on `online`/`offline` events. */
7078
+ /**
7079
+ * Tracks the browser's network connectivity (`navigator.onLine`), updating `online` on `online`/`offline` events.
7080
+ * @fires propertychange {@link PropertyChangeEvent} - when `online` changes (the connection dropped or came back).
7081
+ */
6978
7082
  let PacemOnlineStatusElement = class PacemOnlineStatusElement extends PacemEventTarget {
6979
7083
  constructor() {
6980
7084
  super(...arguments);
@@ -7088,6 +7192,10 @@
7088
7192
  //
7089
7193
  /**
7090
7194
  * Repeats its `<template>` content once per `datasource` item, keeping the rendered items in sync as the datasource changes.
7195
+ * @fires repeateritemcreate {@link RepeaterItemCreateEvent} - when an item got rendered (and is in the DOM): `detail` has its `index`, `item` and `dom`.
7196
+ * @fires repeateritemremove {@link RepeaterItemRemoveEvent} - when an item got removed (the datasource shrank).
7197
+ * @fires itemcommand {@link CustomEvent} - when an element of an item dispatches a command (e.g. a `<pacem-button command-name>`): `detail` has `commandName` and `commandArgument`.
7198
+ * @fires item{command} {@link CustomEvent} - right after `itemcommand`, named after the command (e.g. `itemdelete` for `command-name="delete"`): `detail` is the `commandArgument`.
7091
7199
  * @example
7092
7200
  * <pacem-repeater datasource="[{id:'first', items:['a','b','c']}, ...]">
7093
7201
  * <ol>
@@ -7492,7 +7600,12 @@
7492
7600
  };
7493
7601
  //namespace Pacem.Components {
7494
7602
  const CHECK_PATTERN = /^([\w\.]:)?\/\/[^\/]+/;
7495
- /** Client-side router: parses/generates paths against a template and drives the browser History API. */
7603
+ /**
7604
+ * Client-side router: parses/generates paths against a template and drives the browser History API.
7605
+ * @fires propertychange {@link PropertyChangeEvent} - when `state` changes (every committed navigation).
7606
+ * @fires navigating {@link RouterNavigatingEvent} - on `window`, right before a navigation: cancelable (`preventDefault()` aborts it), `detail` is the target path.
7607
+ * @fires navigate {@link RouterNavigateEvent} - on `window`, once a navigation is committed: `detail` has `path`, `referrer` and `title`.
7608
+ */
7496
7609
  let PacemRouterElement = class PacemRouterElement extends PacemEventTarget {
7497
7610
  constructor() {
7498
7611
  super(...arguments);
@@ -7777,7 +7890,13 @@
7777
7890
  }
7778
7891
  return output;
7779
7892
  }
7780
- /** Registers a service worker and manages its (optional) Push API subscription. */
7893
+ /**
7894
+ * Registers a service worker and manages its (optional) Push API subscription.
7895
+ * @fires register {@link CustomEvent} - once the service worker got registered: `detail` is the `ServiceWorkerRegistration`.
7896
+ * @fires unregister {@link CustomEvent} - when a previous registration got replaced: `detail` is the old one.
7897
+ * @fires subscribe {@link CustomEvent} - when a push subscription is in place (found at start, or just made): `detail` is the `PushSubscription`.
7898
+ * @fires unsubscribe {@link CustomEvent} - when the push subscription went away: `detail` is the old `PushSubscription`.
7899
+ */
7781
7900
  let PacemServiceWorkerProxyElement = class PacemServiceWorkerProxyElement extends PacemEventTarget {
7782
7901
  viewActivatedCallback() {
7783
7902
  super.viewActivatedCallback();
@@ -7844,6 +7963,8 @@
7844
7963
  this.pushSubscription = null;
7845
7964
  }
7846
7965
  resolve(result);
7966
+ }, reason => {
7967
+ reject(reason);
7847
7968
  });
7848
7969
  }
7849
7970
  else {
@@ -7854,6 +7975,8 @@
7854
7975
  /**
7855
7976
  * Subscribes to push notifications through the registered service worker.
7856
7977
  * @param publicKey VAPID public key (base64url or raw bytes); defaults to `publicKey`
7978
+ * @returns The subscription (`null` without a push manager, or when notifications are denied); rejects when the
7979
+ * browser refuses to subscribe (no push service, an invalid key…).
7857
7980
  */
7858
7981
  subscribe(publicKey = this.publicKey) {
7859
7982
  return new Promise((resolve, reject) => {
@@ -7872,6 +7995,9 @@
7872
7995
  else {
7873
7996
  resolve(/* triggers 'subscribe' event */ this.pushSubscription = subscription);
7874
7997
  }
7998
+ }, reason => {
7999
+ // refused (permission denied, no push service, a bad key…): the caller gets to know
8000
+ reject(reason);
7875
8001
  });
7876
8002
  }
7877
8003
  else {
@@ -7942,12 +8068,20 @@
7942
8068
  || (/^arrow/.test(key) && (piece === key.substr(5)));
7943
8069
  }
7944
8070
  const KeyboardShortcutExecuteEventName = "execute";
7945
- /** Listens for a keyboard shortcut (optionally chorded, e.g. `"ctrl+k, s"`) on a target and fires an `execute` event when matched. */
8071
+ /**
8072
+ * Listens for a keyboard shortcut (optionally chorded, e.g. `"ctrl+k, s"`) on a target and fires an `execute` event when matched.
8073
+ * @fires execute {@link Event} - when the whole combination got typed (the matching key's default action is prevented).
8074
+ */
7946
8075
  let PacemShortcutElement = class PacemShortcutElement extends PacemEventTarget {
7947
8076
  constructor() {
7948
8077
  super(...arguments);
7949
8078
  this._accumulator = [];
7950
8079
  this._shortcutHandler = (evt) => {
8080
+ // a disabled shortcut leaves the keys alone (no execute, and no prevented default either)
8081
+ if (this.disabled) {
8082
+ this._accumulator.splice(0);
8083
+ return;
8084
+ }
7951
8085
  const result = (matchesKeyCombination(evt, this.combination, this._accumulator));
7952
8086
  switch (result) {
7953
8087
  case true:
@@ -8411,7 +8545,11 @@
8411
8545
  return c > 3 && r && Object.defineProperty(target, key, r), r;
8412
8546
  };
8413
8547
  //namespace Pacem.Components {
8414
- /** Wraps a {@link Worker}, sending/receiving messages to/from a web worker script. */
8548
+ /**
8549
+ * Wraps a {@link Worker}, sending/receiving messages to/from a web worker script.
8550
+ * @fires receive {@link CustomEvent} - when the worker posts a message back: `detail` is the message (also in `result`).
8551
+ * @fires error {@link CustomEvent} - when the worker script fails: `detail` is the error, when the browser can pass it over.
8552
+ */
8415
8553
  let PacemWebWorkerProxyElement = class PacemWebWorkerProxyElement extends PacemEventTarget {
8416
8554
  constructor() {
8417
8555
  super(...arguments);
@@ -8480,6 +8618,10 @@
8480
8618
  worker.removeEventListener('message', this._messageHandler, false);
8481
8619
  worker.terminate();
8482
8620
  }
8621
+ // (a terminated worker takes no more messages)
8622
+ if (worker === this._worker) {
8623
+ this._worker = null;
8624
+ }
8483
8625
  }
8484
8626
  };
8485
8627
  __decorate$9([
@@ -8916,6 +9058,13 @@
8916
9058
  /**
8917
9059
  * Pacem Drag & Drop element adapter: implements {@link DragDropper} on top of {@link PacemBehavior} to make
8918
9060
  * registered elements draggable and, optionally, sortable/droppable onto {@link dropTargets}.
9061
+ * @fires draginit {@link DragDropEvent} - on press, before anything moves: cancelable (`preventDefault()` refuses the drag).
9062
+ * @fires dragstart {@link DragDropEvent} - when the drag actually starts (the press lasted `lockTimeout`).
9063
+ * @fires drag {@link DragDropEvent} - on every move: cancelable.
9064
+ * @fires dragover {@link DragDropEvent} - when hovering one of the `dropTargets`: cancelable (`preventDefault()` refuses that target).
9065
+ * @fires dragout {@link DragDropEvent} - when leaving a drop target.
9066
+ * @fires drop {@link DragDropEvent} - on release over a drop target: `detail.target` is the target, `detail.element` the dragged element.
9067
+ * @fires dragend {@link DragDropEvent} - when it's all over (dropped or not).
8919
9068
  */
8920
9069
  let PacemDragDropElement = class PacemDragDropElement extends PacemBehavior {
8921
9070
  constructor() {
@@ -9041,7 +9190,42 @@
9041
9190
  else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
9042
9191
  return c > 3 && r && Object.defineProperty(target, key, r), r;
9043
9192
  };
9044
- //namespace Pacem.Components {
9193
+ /** Clamps `value` into `[min, max]`. */
9194
+ function clamp(value, min, max) {
9195
+ if (value < min) {
9196
+ return min;
9197
+ }
9198
+ return Math.min(value, max);
9199
+ }
9200
+ /**
9201
+ * Clamps a desired rect to the size constraints, keeping the edges opposite to the dragged handle in place
9202
+ * (a top/left handle moves the rect's origin, a bottom/right one doesn't).
9203
+ * @param rect The desired rect.
9204
+ * @param handle The dragged handle (`top`, `bottomleft`…).
9205
+ * @param constraints Minimum and maximum width and height.
9206
+ * @returns The clamped rect.
9207
+ */
9208
+ function clampToConstraints(rect, handle, constraints) {
9209
+ const width = clamp(rect.width, constraints.minWidth, constraints.maxWidth);
9210
+ const height = clamp(rect.height, constraints.minHeight, constraints.maxHeight);
9211
+ const x = handle.endsWith('left') ? rect.x + rect.width - width : rect.x;
9212
+ const y = handle.startsWith('top') ? rect.y + rect.height - height : rect.y;
9213
+ return { x, y, width, height };
9214
+ }
9215
+ /**
9216
+ * What an element's `width`/`height` leave out of its border box: nothing for `border-box`, padding and border for
9217
+ * `content-box` (so that a border-box size can be written as `width`/`height` whatever the box sizing).
9218
+ * @param computed The element's computed style.
9219
+ */
9220
+ function boxChrome(computed) {
9221
+ if (computed.boxSizing === 'border-box') {
9222
+ return { width: 0, height: 0 };
9223
+ }
9224
+ const px = (value) => parseFloat(value) || 0;
9225
+ const width = px(computed.paddingLeft) + px(computed.paddingRight) + px(computed.borderLeftWidth) + px(computed.borderRightWidth);
9226
+ const height = px(computed.paddingTop) + px(computed.paddingBottom) + px(computed.borderTopWidth) + px(computed.borderBottomWidth);
9227
+ return { width, height };
9228
+ }
9045
9229
  class RescaleElementDelegate {
9046
9230
  constructor(_element, _type, _rescaler, _logFn) {
9047
9231
  this._element = _element;
@@ -9109,9 +9293,13 @@
9109
9293
  this._logFn(LogLevel.Log, `handle: ${handle}, origin: ${JSON.stringify(origin)}, currentPosition: ${JSON.stringify(currentPosition)}`, `Rescaling`);
9110
9294
  this._logFn(LogLevel.Log, `delta: ${JSON.stringify(delta)}, position: ${JSON.stringify(position)}`, `Rescaling`);
9111
9295
  this._logFn(LogLevel.Log, `from: ${JSON.stringify(this._rect)}, to: ${JSON.stringify(desiredRect)}`, `Rescaling`);
9296
+ // free rescaling: a move beyond the constraints is clamped to them (a fast drag still reaches the limit)
9297
+ if (!keepProportions) {
9298
+ desiredRect = clampToConstraints(desiredRect, handle, { minWidth, maxWidth, minHeight, maxHeight });
9299
+ }
9112
9300
  // check constraints
9113
9301
  let horizontally = desiredRect.width >= minWidth && desiredRect.width <= maxWidth, vertically = desiredRect.height >= minHeight && desiredRect.height <= maxHeight;
9114
- // if proportions are constrained and one fails, then both fail
9302
+ // if proportions are constrained and one fails, then both fail (clamping one side would break the ratio)
9115
9303
  if (keepProportions && (!horizontally || !vertically)) {
9116
9304
  horizontally = vertically = false;
9117
9305
  }
@@ -9137,8 +9325,10 @@
9137
9325
  // render resize
9138
9326
  const m = this._originalTransform;
9139
9327
  el.style.transform = `matrix(${m.a},${m.b},${m.c},${m.d},${(m.e + desiredRect.x - this._rect.x)},${(m.f + desiredRect.y - this._rect.y)})`;
9140
- el.style.width = desiredRect.width + 'px';
9141
- el.style.height = desiredRect.height + 'px';
9328
+ // (the rect is a border box: a content-box element gets it minus its padding and border)
9329
+ const chrome = this._chrome;
9330
+ el.style.width = Math.max(desiredRect.width - chrome.width, 0) + 'px';
9331
+ el.style.height = Math.max(desiredRect.height - chrome.height, 0) + 'px';
9142
9332
  }
9143
9333
  // store targetRect
9144
9334
  this._targetRect = desiredRect;
@@ -9171,7 +9361,9 @@
9171
9361
  this._targetRect =
9172
9362
  Utils.offsetRect(_element);
9173
9363
  //
9174
- this._originalTransform = Utils.deserializeTransform(getComputedStyle(_element));
9364
+ const computed = getComputedStyle(_element);
9365
+ this._originalTransform = Utils.deserializeTransform(computed);
9366
+ this._chrome = boxChrome(computed);
9175
9367
  // body
9176
9368
  Utils.addClass(document.body, PCSS + '-rescaling rescale-' + _type);
9177
9369
  }
@@ -9186,6 +9378,11 @@
9186
9378
  /**
9187
9379
  * Pacem Rescale element adapter: implements {@link Rescaler} on top of {@link PacemBehavior}, decorating
9188
9380
  * registered elements with draggable resize handles (see {@link RescaleHandle}).
9381
+ * A move beyond the size constraints (`minWidth`, `maxWidth`, `minHeight`, `maxHeight`) is clamped to them; with
9382
+ * `keepProportions`, such a move is ignored instead (clamping one side would break the ratio).
9383
+ * @fires rescalestart {@link RescaleEvent} - when a handle gets grabbed: `detail.handle` tells which.
9384
+ * @fires rescale {@link RescaleEvent} - on every move: `detail.targetRect` is the size about to be applied; cancelable (`preventDefault()` applies nothing, e.g. to apply it your way).
9385
+ * @fires rescaleend {@link RescaleEvent} - when the handle gets released: `detail.targetRect` is the final size.
9189
9386
  */
9190
9387
  let PacemRescaleElement = class PacemRescaleElement extends PacemBehavior {
9191
9388
  constructor() {
@@ -9407,6 +9604,9 @@
9407
9604
  /**
9408
9605
  * Pacem Rotate element adapter: implements {@link Rotator} on top of {@link PacemBehavior} to make
9409
9606
  * registered elements rotatable around a pivot point via mouse/touch drag.
9607
+ * @fires rotatestart {@link RotateEvent} - on press: cancelable (`preventDefault()` refuses the gesture).
9608
+ * @fires rotate {@link RotateEvent} - whenever the (stepped) angle changes: `detail.rotation`, in radians; cancelable (nothing gets rendered).
9609
+ * @fires rotateend {@link RotateEvent} - on release: `detail.rotation` is the final angle.
9410
9610
  */
9411
9611
  let PacemRotateElement = class PacemRotateElement extends PacemBehavior {
9412
9612
  constructor() {
@@ -9512,9 +9712,10 @@
9512
9712
  swiper.dispatchEvent(swipeEvt);
9513
9713
  if (!swipeEvt.defaultPrevented) {
9514
9714
  let { delta } = this._delta(this._init, state);
9515
- if (!swiper.options.bouncy) {
9715
+ // (the merged options: the swiper's own `options` may well be unset, defaults apply)
9716
+ if (!this.#options.bouncy) {
9516
9717
  // clamp delta just in case
9517
- const snaps = swiper.snaps.slice().sort();
9718
+ const snaps = swiper.snaps.slice().sort((a, b) => a - b);
9518
9719
  const snapFirst = snaps[0], snapLast = snaps[snaps.length - 1];
9519
9720
  const dir = this._swiper.direction;
9520
9721
  const { x, y } = delta;
@@ -9534,25 +9735,26 @@
9534
9735
  console.log('How come?!');
9535
9736
  }
9536
9737
  else {
9537
- const args = this._buildSwipeArgsFromState();
9538
- const swipeEvt = new SwipeEvent(SwipeEventType.Swipe, args, { cancelable: true, bubbles: false });
9539
9738
  const swiper = this._swiper;
9739
+ // where the element is going to rest: the snap nearest to where its momentum carries it (known before
9740
+ // dispatching, so that every event of the release tells it: `detail.snap`, 0 meaning back home)
9741
+ const estimate = this._buildSwipeArgsFromState();
9742
+ // (a numeric copy: the swiper's own array stays as it is)
9743
+ const snaps = (swiper.snaps ?? [0.0]).slice().sort((a, b) => a - b);
9744
+ const target = this.#options.bouncy ? estimate.predictedDelta : estimate.predictedDelta.clamp(snaps[0], snaps[snaps.length - 1]);
9745
+ const snap = snaps.reduce((acc, v) => {
9746
+ const distance = Math.abs(v - target);
9747
+ const current = Math.abs(acc - target);
9748
+ return distance < current ? v : acc;
9749
+ }, 0);
9750
+ const args = this._buildSwipeArgsFromState(this.#state, snap);
9751
+ const swipeEvt = new SwipeEvent(SwipeEventType.Swipe, args, { cancelable: true, bubbles: false });
9540
9752
  swiper.dispatchEvent(swipeEvt);
9541
9753
  if (!swipeEvt.defaultPrevented) {
9542
9754
  const swipe1Evt = new SwipeEvent(("swipe" + args.direction), args, { cancelable: true, bubbles: false });
9543
9755
  swiper.dispatchEvent(swipe1Evt);
9544
9756
  if (!swipe1Evt.defaultPrevented) {
9545
- // if the predicted distance is greater than the original position + half the distance to the nearest snap,
9546
- const snaps = swiper.snaps ?? [0.0];
9547
- snaps.sort();
9548
- const target = swiper.options.bouncy ? args.predictedDelta : args.predictedDelta.clamp(snaps[0], snaps[snaps.length - 1]);
9549
- // then animate toward that target,
9550
- // otherwise bounce back
9551
- const snap = snaps.reduce((acc, v) => {
9552
- const distance = Math.abs(v - target);
9553
- const current = Math.abs(acc - target);
9554
- return distance < current ? v : acc;
9555
- }, 0);
9757
+ // animate toward the target, then settle on the snap (or bounce back home)
9556
9758
  const vertical = swiper.direction === 'vertical';
9557
9759
  const duration = args.predictedDuration;
9558
9760
  const delta0 = vertical ? { x: 0, y: target } : { x: target, y: 0 };
@@ -9566,7 +9768,11 @@
9566
9768
  };
9567
9769
  if (snap.isCloseTo(0)) {
9568
9770
  this._animateOffsetBy(delta0, duration).then(() => {
9569
- this._animateOffsetBy({ x: 0, y: 0 }, .3, 'cubic-bezier(.37,-0.01,.34,1.43)').then(() => swipeAnimationEnd());
9771
+ this._animateOffsetBy({ x: 0, y: 0 }, .3, 'cubic-bezier(.37,-0.01,.34,1.43)').then(() => {
9772
+ // back home exactly: the committed (overshooting) animation leaves float noise behind
9773
+ this._element.style.transform = this.#originalInlineTransform;
9774
+ swipeAnimationEnd();
9775
+ });
9570
9776
  });
9571
9777
  }
9572
9778
  else {
@@ -9596,6 +9802,7 @@
9596
9802
  const el = _element;
9597
9803
  this.#options = Utils.extend({}, DEFAULT_OPTIONS, _swiper.options ?? {});
9598
9804
  this.#originalTransform = Utils.deserializeTransform(getComputedStyle(el));
9805
+ this.#originalInlineTransform = el.style.transform;
9599
9806
  window.addEventListener('pointermove', this._moveHandler, false);
9600
9807
  window.addEventListener('pointerup', this._endHandler, false);
9601
9808
  window.addEventListener('pointercancel', this._endHandler, false);
@@ -9633,7 +9840,12 @@
9633
9840
  };
9634
9841
  return state;
9635
9842
  }
9636
- _buildSwipeArgsFromState(state = this.#state) {
9843
+ /**
9844
+ * Builds the event args of the current state.
9845
+ * @param state The gesture state.
9846
+ * @param snap The snap the element is going to rest at, once known (on release).
9847
+ */
9848
+ _buildSwipeArgsFromState(state = this.#state, snap) {
9637
9849
  const options = this.#options;
9638
9850
  const speed = this.speed;
9639
9851
  const dir = this._swiper.direction;
@@ -9648,7 +9860,7 @@
9648
9860
  // console.log(`v speed: ${verticalspeed}`);
9649
9861
  const args = SwipeEventArgsClass.fromArgs({
9650
9862
  position: state.position, element: this._element, t0: this._init, timestamp: state.timestamp, inertia: options.inertia, fling: options.fling,
9651
- horizontalspeed, verticalspeed, vertical
9863
+ horizontalspeed, verticalspeed, vertical, snap
9652
9864
  });
9653
9865
  //console.log(args);
9654
9866
  return args;
@@ -9677,6 +9889,8 @@
9677
9889
  #options;
9678
9890
  #samples;
9679
9891
  #originalTransform;
9892
+ /** The element's inline `transform` before the gesture, restored as is on a bounce-back home. */
9893
+ #originalInlineTransform;
9680
9894
  #state;
9681
9895
  dispose() {
9682
9896
  const el = this._element;
@@ -9688,6 +9902,11 @@
9688
9902
  /**
9689
9903
  * Pacem Swipe element adapter: implements {@link Swiper} on top of {@link PacemBehavior} to make
9690
9904
  * registered elements draggable along one axis, with snap points and fling/bounce-back detection.
9905
+ * @fires pan {@link SwipeEvent} - while the element follows the pointer: cancelable.
9906
+ * @fires swipe {@link SwipeEvent} - on release, once a swipe is detected (dragged past a snap, or flung): cancelable (stops the chain).
9907
+ * @fires swipe{direction} {@link SwipeEvent} - right after `swipe`, named after the direction (`swipeleft`, `swiperight`, `swipeup`, `swipedown`): cancelable.
9908
+ * @fires swipeanimationend {@link SwipeEvent} - when the element reached its snap: cancelable (`preventDefault()` keeps it there instead of returning).
9909
+ * @fires swipe{direction}animationend {@link SwipeEvent} - right after `swipeanimationend`, named after the direction.
9691
9910
  */
9692
9911
  let PacemSwipeElement = class PacemSwipeElement extends PacemBehavior {
9693
9912
  constructor() {