@wpmoo/ui 2.2.0 → 2.3.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.
package/dist/js/moo-ui.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /*!
2
- * Moo UI moo-ui.js v2.2.0 (https://wpmoo.org/)
2
+ * Moo UI moo-ui.js v2.3.0 (https://wpmoo.org/)
3
3
  * Copyright 2026 WPMoo Authors
4
4
  * Licensed under MIT (https://github.com/wpmoo-org/ui/blob/main/LICENSE)
5
5
  */
@@ -364,9 +364,9 @@ var Combobox = class _Combobox {
364
364
  const needle = normalize(this._input.value);
365
365
  let count = 0;
366
366
  this._options.forEach((option) => {
367
- const matches = !needle || normalize(option.textContent).includes(needle);
368
- option.hidden = !matches;
369
- count += matches ? 1 : 0;
367
+ const matches2 = !needle || normalize(option.textContent).includes(needle);
368
+ option.hidden = !matches2;
369
+ count += matches2 ? 1 : 0;
370
370
  });
371
371
  this._syncGroupVisibility();
372
372
  this._empty.hidden = count !== 0;
@@ -4832,6 +4832,324 @@ var ImageGallery = class _ImageGallery {
4832
4832
  }
4833
4833
  };
4834
4834
 
4835
+ // src/js/components/consent.js
4836
+ var instances10 = /* @__PURE__ */ new WeakMap();
4837
+ var documents = /* @__PURE__ */ new WeakMap();
4838
+ var RECORD_VERSION = 1;
4839
+ var DAY_SECONDS = 86400;
4840
+ var FOCUSABLE = "button, a[href], input:not([type='hidden']), select, textarea, [tabindex]:not([tabindex='-1'])";
4841
+ function webSource(value, base) {
4842
+ if (!value) return "";
4843
+ try {
4844
+ const url = new URL(value, base);
4845
+ return url.protocol === "https:" || url.protocol === "http:" ? url.href : "";
4846
+ } catch {
4847
+ return "";
4848
+ }
4849
+ }
4850
+ function matches(pattern, name) {
4851
+ return pattern.endsWith("*") ? name.startsWith(pattern.slice(0, -1)) : name === pattern;
4852
+ }
4853
+ var ConsentManager = class _ConsentManager {
4854
+ /** The manager bound to a [data-consent] element or to a document. */
4855
+ static getInstance(target) {
4856
+ if (target?.nodeType === 9) return documents.get(target) || null;
4857
+ return target?.nodeType === 1 ? instances10.get(target) || null : null;
4858
+ }
4859
+ static getOrCreateInstance(element, options) {
4860
+ return _ConsentManager.getInstance(element) || new _ConsentManager(element, options);
4861
+ }
4862
+ constructor(element, { reload } = {}) {
4863
+ if (element?.nodeType !== 1 || !element.matches("[data-consent]") || !element.id) {
4864
+ throw new TypeError("ConsentManager requires a named [data-consent] element.");
4865
+ }
4866
+ const existing = instances10.get(element);
4867
+ if (existing) return existing;
4868
+ this._element = element;
4869
+ this._document = element.ownerDocument;
4870
+ if (documents.has(this._document)) {
4871
+ throw new TypeError("ConsentManager allows one consent card per document.");
4872
+ }
4873
+ this._window = this._document.defaultView;
4874
+ const source = element.querySelector("script[data-consent-config]");
4875
+ if (!source) throw new TypeError("ConsentManager requires a [data-consent-config] script.");
4876
+ this._config = JSON.parse(source.textContent);
4877
+ this._cookie = { name: "moo_consent", days: 365, ...this._config.cookie || {} };
4878
+ this._categories = this._config.categories || [];
4879
+ this._optional = this._categories.filter((category) => !category.required).map((category) => category.id);
4880
+ this._reload = reload || (() => this._window.location.reload());
4881
+ this._waiting = /* @__PURE__ */ new Map();
4882
+ this._listeners = [];
4883
+ this._trapping = false;
4884
+ this._returnFocus = null;
4885
+ const record = this._read();
4886
+ this._valid = Boolean(record);
4887
+ this._choices = this._normalize(record ? record.c : {});
4888
+ const control = (suffix) => this._document.getElementById(`${element.id}-${suffix}`);
4889
+ const actions = [
4890
+ [control("accept"), () => this.acceptAll()],
4891
+ [control("decline"), () => this.declineAll()],
4892
+ [control("save"), () => this.save(this._switchChoices(), "save")]
4893
+ ];
4894
+ for (const [button, action] of actions) {
4895
+ if (button) this._listen(button, "click", action);
4896
+ }
4897
+ this._listen(this._document, "click", (event) => this._onDocumentClick(event));
4898
+ this._listen(element, "keydown", (event) => this._onKeydown(event));
4899
+ instances10.set(element, this);
4900
+ documents.set(this._document, this);
4901
+ this.activate(this._document);
4902
+ if (!this._valid) this._show();
4903
+ this.ready = Promise.resolve(this);
4904
+ queueMicrotask(() => this._emit("ready", { choices: this.choices, valid: this._valid, revision: this.revision }));
4905
+ }
4906
+ get revision() {
4907
+ return String(this._config.revision);
4908
+ }
4909
+ get valid() {
4910
+ return this._valid;
4911
+ }
4912
+ get choices() {
4913
+ return { ...this._choices };
4914
+ }
4915
+ get isOpen() {
4916
+ return !this._element.hidden;
4917
+ }
4918
+ granted(id) {
4919
+ const category = this._categories.find((entry) => entry.id === id);
4920
+ if (!category) return false;
4921
+ return category.required ? true : this._valid && this._choices[id] === true;
4922
+ }
4923
+ /** Runs callback now if granted, otherwise on grant. Returns an unsubscribe function. */
4924
+ whenGranted(id, callback) {
4925
+ if (this.granted(id)) {
4926
+ this._run(callback);
4927
+ return () => {
4928
+ };
4929
+ }
4930
+ if (!this._waiting.has(id)) this._waiting.set(id, /* @__PURE__ */ new Set());
4931
+ this._waiting.get(id).add(callback);
4932
+ return () => this._waiting.get(id)?.delete(callback);
4933
+ }
4934
+ acceptAll() {
4935
+ return this.save(Object.fromEntries(this._optional.map((id) => [id, true])), "accept-all");
4936
+ }
4937
+ declineAll() {
4938
+ return this.save({}, "decline");
4939
+ }
4940
+ grant(id) {
4941
+ return this.save({ ...this._choices, [id]: true }, "grant");
4942
+ }
4943
+ deny(id) {
4944
+ return this.save({ ...this._choices, [id]: false }, "deny");
4945
+ }
4946
+ /** Stores the choice; optional categories not named become false. */
4947
+ save(choices, source = "save") {
4948
+ const previous = this._valid ? this.choices : this._normalize({});
4949
+ const wasValid = this._valid;
4950
+ const next = this._normalize(choices);
4951
+ this._write(next);
4952
+ this._valid = true;
4953
+ this._choices = next;
4954
+ const granted = this._optional.filter((id) => next[id] && !previous[id]);
4955
+ const revoked = wasValid ? this._optional.filter((id) => previous[id] && !next[id]) : [];
4956
+ this.close();
4957
+ this.activate(this._document);
4958
+ for (const id of granted) {
4959
+ const waiting = this._waiting.get(id);
4960
+ this._waiting.delete(id);
4961
+ waiting?.forEach((callback) => this._run(callback));
4962
+ }
4963
+ this._emit("change", { choices: this.choices, previous, granted, revoked, source });
4964
+ if (revoked.length) {
4965
+ revoked.forEach((id) => this._forget(id));
4966
+ this._reload();
4967
+ }
4968
+ return this.choices;
4969
+ }
4970
+ /** Opens the card with the stored choices; traps focus until closed. */
4971
+ open(trigger = null) {
4972
+ this._returnFocus = trigger;
4973
+ this._trapping = true;
4974
+ this._show();
4975
+ this._document.getElementById(`${this._element.id}-title`)?.focus({ preventScroll: true });
4976
+ this._emit("open", {});
4977
+ }
4978
+ /** Hides the card; it stays open while no valid choice exists. */
4979
+ close() {
4980
+ if (!this._valid) return false;
4981
+ const wasOpen = this.isOpen;
4982
+ const returnFocus = this._trapping ? this._returnFocus : null;
4983
+ this._element.hidden = true;
4984
+ this._trapping = false;
4985
+ this._returnFocus = null;
4986
+ if (returnFocus?.isConnected) returnFocus.focus({ preventScroll: true });
4987
+ if (wasOpen) this._emit("close", {});
4988
+ return true;
4989
+ }
4990
+ /** Activates gated elements under root whose category is granted. */
4991
+ activate(root = this._document) {
4992
+ this._activateWhere(root, (id) => this.granted(id));
4993
+ }
4994
+ dispose() {
4995
+ if (instances10.get(this._element) !== this) return;
4996
+ this._listeners.forEach(([target, type, handler]) => target.removeEventListener(type, handler));
4997
+ this._listeners = [];
4998
+ this._waiting.clear();
4999
+ instances10.delete(this._element);
5000
+ documents.delete(this._document);
5001
+ }
5002
+ // A failing site callback must not stop the rest of a save, such as the
5003
+ // cleanup and reload after a withdrawal; the error is still reported.
5004
+ _run(callback) {
5005
+ try {
5006
+ callback(this);
5007
+ } catch (error) {
5008
+ if (typeof this._window.reportError === "function") this._window.reportError(error);
5009
+ else this._window.console?.error(error);
5010
+ }
5011
+ }
5012
+ _activateWhere(root, allowed) {
5013
+ for (const element of root.querySelectorAll("[data-consent-category]")) {
5014
+ if (allowed(element.getAttribute("data-consent-category"))) this._activateElement(element);
5015
+ }
5016
+ for (const placeholder of root.querySelectorAll("[data-consent-placeholder]")) {
5017
+ if (allowed(placeholder.getAttribute("data-consent-placeholder"))) placeholder.hidden = true;
5018
+ }
5019
+ }
5020
+ _listen(target, type, handler) {
5021
+ target.addEventListener(type, handler);
5022
+ this._listeners.push([target, type, handler]);
5023
+ }
5024
+ _emit(name, detail) {
5025
+ this._document.dispatchEvent(new this._window.CustomEvent(`moo:consent:${name}`, { bubbles: true, detail }));
5026
+ }
5027
+ _normalize(choices) {
5028
+ return Object.fromEntries(this._optional.map((id) => [id, choices?.[id] === true || choices?.[id] === 1]));
5029
+ }
5030
+ _switch(id) {
5031
+ return this._document.getElementById(`${this._element.id}-category-${id}`);
5032
+ }
5033
+ _switchChoices() {
5034
+ return Object.fromEntries(this._optional.map((id) => [id, Boolean(this._switch(id)?.checked)]));
5035
+ }
5036
+ _show() {
5037
+ for (const id of this._optional) {
5038
+ const input = this._switch(id);
5039
+ if (input) input.checked = this.granted(id);
5040
+ }
5041
+ this._element.hidden = false;
5042
+ }
5043
+ _read() {
5044
+ const prefix = `${this._cookie.name}=`;
5045
+ const raw = this._document.cookie.split("; ").find((part) => part.startsWith(prefix));
5046
+ if (!raw) return null;
5047
+ try {
5048
+ const record = JSON.parse(decodeURIComponent(raw.slice(prefix.length)));
5049
+ const age = Date.now() / 1e3 - Number(record.t);
5050
+ if (record.v !== RECORD_VERSION || record.r !== this.revision) return null;
5051
+ if (!(age >= 0 && age < this._cookie.days * DAY_SECONDS)) return null;
5052
+ return record;
5053
+ } catch {
5054
+ return null;
5055
+ }
5056
+ }
5057
+ _write(choices) {
5058
+ const record = {
5059
+ v: RECORD_VERSION,
5060
+ r: this.revision,
5061
+ t: Math.floor(Date.now() / 1e3),
5062
+ c: Object.fromEntries(Object.entries(choices).map(([id, value]) => [id, value ? 1 : 0]))
5063
+ };
5064
+ const secure = this._window.location.protocol === "https:" ? "; Secure" : "";
5065
+ this._document.cookie = `${this._cookie.name}=${encodeURIComponent(JSON.stringify(record))}; Path=/; Max-Age=${this._cookie.days * DAY_SECONDS}; SameSite=Lax${secure}`;
5066
+ }
5067
+ _forget(id) {
5068
+ const category = this._categories.find((entry) => entry.id === id);
5069
+ const host = this._window.location.hostname;
5070
+ const parts = host.split(".");
5071
+ const domains = [""];
5072
+ for (let index = 0; index < parts.length - 1; index += 1) domains.push(`; Domain=${parts.slice(index).join(".")}`);
5073
+ const cookieNames = this._document.cookie.split("; ").map((part) => part.split("=")[0]).filter(Boolean);
5074
+ for (const item of category?.storage || []) {
5075
+ if (item.type === "cookie") {
5076
+ for (const name of cookieNames.filter((candidate) => matches(item.name, candidate))) {
5077
+ for (const domain of domains) this._document.cookie = `${name}=; Path=/; Max-Age=0${domain}`;
5078
+ }
5079
+ } else {
5080
+ try {
5081
+ const storage = item.type === "local" ? this._window.localStorage : this._window.sessionStorage;
5082
+ Object.keys(storage).filter((key) => matches(item.name, key)).forEach((key) => storage.removeItem(key));
5083
+ } catch {
5084
+ }
5085
+ }
5086
+ }
5087
+ }
5088
+ _activateElement(element) {
5089
+ if (element.hasAttribute("data-consent-active")) return;
5090
+ const declared = element.getAttribute("data-consent-src");
5091
+ const src = webSource(declared, this._document.baseURI);
5092
+ if (declared && !src) return;
5093
+ if (element.localName === "script") {
5094
+ const script = this._document.createElement("script");
5095
+ for (const { name, value } of element.attributes) {
5096
+ if (name !== "type" && !name.startsWith("data-consent-")) script.setAttribute(name, value);
5097
+ }
5098
+ const type = element.getAttribute("data-consent-type");
5099
+ if (type) script.type = type;
5100
+ if (src) script.src = src;
5101
+ else script.textContent = element.textContent;
5102
+ script.setAttribute("data-consent-category", element.getAttribute("data-consent-category"));
5103
+ script.setAttribute("data-consent-active", "");
5104
+ element.replaceWith(script);
5105
+ return;
5106
+ }
5107
+ element.setAttribute("data-consent-active", "");
5108
+ if (src) element.setAttribute("src", src);
5109
+ element.hidden = false;
5110
+ }
5111
+ _onDocumentClick(event) {
5112
+ const control = event.target.closest?.("[data-consent-open], [data-consent-grant]");
5113
+ if (!control || control.matches(":disabled")) return;
5114
+ if (control.hasAttribute("data-consent-open")) {
5115
+ if (control.getAttribute("data-consent-open") !== this._element.id) return;
5116
+ event.preventDefault();
5117
+ this.open(control);
5118
+ return;
5119
+ }
5120
+ const id = control.getAttribute("data-consent-grant");
5121
+ if (!this._categories.some((category) => category.id === id)) return;
5122
+ event.preventDefault();
5123
+ const placeholder = control.closest("[data-consent-placeholder]");
5124
+ const scope = control.closest("[data-consent-embed]") || placeholder?.parentElement;
5125
+ if (placeholder?.querySelector("[data-consent-remember]")?.checked || !scope) {
5126
+ this.grant(id);
5127
+ } else {
5128
+ this._activateWhere(scope, (category) => category === id);
5129
+ }
5130
+ }
5131
+ _onKeydown(event) {
5132
+ if (!this._trapping) return;
5133
+ if (event.key === "Escape") {
5134
+ if (this.close()) event.preventDefault();
5135
+ return;
5136
+ }
5137
+ if (event.key !== "Tab") return;
5138
+ const focusable = Array.from(this._element.querySelectorAll(FOCUSABLE)).filter((control) => !control.matches(":disabled") && control.getClientRects().length > 0);
5139
+ if (!focusable.length) return;
5140
+ const first = focusable[0];
5141
+ const last = focusable[focusable.length - 1];
5142
+ const active = this._document.activeElement;
5143
+ if (event.shiftKey && (active === first || !this._element.contains(active) || active.id === `${this._element.id}-title`)) {
5144
+ event.preventDefault();
5145
+ last.focus();
5146
+ } else if (!event.shiftKey && active === last) {
5147
+ event.preventDefault();
5148
+ first.focus();
5149
+ }
5150
+ }
5151
+ };
5152
+
4835
5153
  // src/js/moo-ui.js
4836
5154
  import PasswordRequirements from "./password-requirements.js";
4837
5155
  async function loadChart() {
@@ -4842,6 +5160,7 @@ var MooUI = {
4842
5160
  PasswordRequirements,
4843
5161
  FloatingPanel,
4844
5162
  ImageGallery,
5163
+ ConsentManager,
4845
5164
  loadChart,
4846
5165
  Combobox,
4847
5166
  ContextMenu,
@@ -4858,6 +5177,7 @@ var MooUI = {
4858
5177
  var moo_ui_default = MooUI;
4859
5178
  export {
4860
5179
  Combobox,
5180
+ ConsentManager,
4861
5181
  ContextMenu,
4862
5182
  DataTable,
4863
5183
  MooDatepicker as Datepicker,