mypgs 1.1.5 → 1.3.3

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 (80) hide show
  1. package/AI_GUIDELINES.md +353 -0
  2. package/README.md +483 -53
  3. package/assets/javascript/_imports.js +26 -0
  4. package/assets/javascript/_pgs.js +53 -2
  5. package/assets/javascript/components/_accordion.js +114 -66
  6. package/assets/javascript/components/_dropdown.js +199 -98
  7. package/assets/javascript/components/_menu.js +33 -69
  8. package/assets/javascript/components/_modals.js +44 -3
  9. package/assets/javascript/components/_notifications.js +162 -0
  10. package/assets/javascript/components/_slides.js +11 -1
  11. package/assets/javascript/components/_stepTabs.js +16 -4
  12. package/assets/javascript/components/_steps.js +32 -6
  13. package/assets/javascript/functions/_formValidate.js +4 -5
  14. package/assets/javascript/index.js +8 -4
  15. package/assets/javascript/patterns/_cookieConsent.js +8 -2
  16. package/assets/javascript/patterns/_header.js +1 -1
  17. package/assets/javascript/pgs.d.ts +17 -2
  18. package/assets/scss/base/_html.scss +23 -1
  19. package/assets/scss/components/_dropdown.scss +24 -88
  20. package/assets/scss/components/_logo.scss +1 -1
  21. package/assets/scss/components/_menu.scss +129 -15
  22. package/assets/scss/components/_modals.scss +15 -3
  23. package/assets/scss/components/_notification.scss +11 -14
  24. package/assets/scss/components/_tooltip.scss +2 -9
  25. package/assets/scss/index.scss +0 -1
  26. package/assets/scss/layout/_gap.scss +7 -1
  27. package/assets/scss/layout/_layout.scss +0 -12
  28. package/assets/scss/layout/_pageShell.scss +102 -108
  29. package/assets/scss/mixin/_mx-form.scss +1 -3
  30. package/assets/scss/mixin/_mx-responsive.scss +2 -2
  31. package/assets/scss/mixin/_mx-semantic.scss +0 -60
  32. package/assets/scss/mixin/mixin.scss +0 -1
  33. package/assets/scss/patterns/_footer.scss +1 -0
  34. package/assets/scss/patterns/_header.scss +20 -18
  35. package/dist/css/index.css +325 -1161
  36. package/dist/css/index.css.map +1 -1
  37. package/dist/css/index.min.css +1 -1
  38. package/dist/index.d.ts +17 -2
  39. package/dist/javascript/index.js +985 -412
  40. package/dist/javascript/index.js.map +1 -1
  41. package/dist/javascript/index.min.js +1 -1
  42. package/package.json +7 -2
  43. package/templates/components/{md-accordion.html → accordion.html} +2 -2
  44. package/templates/components/dropdown.html +91 -0
  45. package/templates/components/menu.html +41 -0
  46. package/templates/components/modal.html +78 -0
  47. package/templates/components/notification.html +35 -0
  48. package/templates/components/{md-slides.html → slides.html} +5 -5
  49. package/templates/components/{md-steps.html → steps.html} +7 -7
  50. package/templates/components/{md-tooltip.html → tooltip.html} +4 -2
  51. package/templates/demo.css +18 -0
  52. package/templates/demo.html +4 -5
  53. package/templates/demo.js +53 -52
  54. package/templates/layout/flex.html +89 -0
  55. package/templates/layout/grid.html +89 -0
  56. package/templates/layout/pageShell.html +59 -0
  57. package/templates/layout/section.html +40 -0
  58. package/templates/{layout/md-footer.html → patterns/footer.html} +1 -0
  59. package/templates/{layout/md-header.html → patterns/header.html} +6 -9
  60. package/assets/javascript/components/_exeNotifications.js +0 -85
  61. package/assets/javascript/functions/_notifications.js +0 -74
  62. package/assets/javascript/functions/_sendForm.js +0 -100
  63. package/assets/scss/base/_reset.scss +0 -15
  64. package/assets/scss/mixin/_mx-menu.scss +0 -154
  65. package/templates/components/md-dropdown.html +0 -16
  66. package/templates/components/md-menu.html +0 -40
  67. package/templates/components/md-modal.html +0 -19
  68. package/templates/components/md-notification.html +0 -13
  69. package/templates/layout/md-pageShell.html +0 -22
  70. package/templates/layout/md-section.html +0 -25
  71. /package/templates/components/{md-breadcumbs.html → breadcumbs.html} +0 -0
  72. /package/templates/components/{md-button.html → button.html} +0 -0
  73. /package/templates/components/{md-card.html → card.html} +0 -0
  74. /package/templates/components/{md-form.html → form.html} +0 -0
  75. /package/templates/components/{md-logo.html → logo.html} +0 -0
  76. /package/templates/components/{md-searchbar.html → searchbar.html} +0 -0
  77. /package/templates/components/{md-stepTabs.html → stepTabs.html} +0 -0
  78. /package/templates/components/{md-table.html → table.html} +0 -0
  79. /package/templates/layout/{md-body.html → body.html} +0 -0
  80. /package/templates/{layout/md-cookieConsent.html → patterns/cookieConsent.html} +0 -0
@@ -220,10 +220,61 @@ export function pgs(root) {
220
220
  const api = createBasePgs();
221
221
  api.state = createState("pgs-state");
222
222
  api.option = createOption("pgs-option");
223
- api.modules = createOption("pgs-modules");
224
223
  return api;
225
224
  }
226
225
 
227
- globalThis.pgs ??= pgs;
226
+ const PGS_IMPORTS = {};
227
+
228
+ function registerImportModule(name, module) {
229
+ const key = String(name || "").trim().replace(/^pgs[_-\s]*/i, "").toLowerCase();
230
+
231
+ if (!key) throw new TypeError("pgs.registerImport(...modules): ogni modulo deve avere name o PGS_name");
232
+
233
+ PGS_IMPORTS[key] = {
234
+ name,
235
+ module
236
+ };
237
+ }
238
+
239
+ pgs.registerImport = function (...modules) {
240
+ modules.flat().forEach(item => {
241
+ if (item && typeof item === "object" && !item.PGS_name && !item.name) {
242
+ Object.entries(item).forEach(([name, module]) => registerImportModule(name, module));
243
+ return;
244
+ }
245
+
246
+ registerImportModule(item?.PGS_name || item?.name, item);
247
+ });
228
248
 
249
+ return pgs;
250
+ };
229
251
 
252
+ pgs.registerModules = function (modules = {}) {
253
+ Object.entries(modules).forEach(([name, module]) => {
254
+ const key = String(name || "").trim();
255
+ if (!key) return;
256
+
257
+ const hasOwn = Object.prototype.hasOwnProperty.call(pgs, key);
258
+ if (hasOwn && pgs[key] !== module) {
259
+ throw new Error(`pgs.registerModules(): "${key}" e' gia' definito su pgs`);
260
+ }
261
+
262
+ pgs[key] = module;
263
+ });
264
+
265
+ return pgs;
266
+ };
267
+
268
+ pgs.import = function (...names) {
269
+ return names.flat().reduce((imports, name) => {
270
+ const key = String(name || "").trim().replace(/^pgs[_-\s]*/i, "").toLowerCase();
271
+ const item = PGS_IMPORTS[key];
272
+
273
+ if (!item) throw new Error(`pgs.import(): modulo "${name}" non registrato`);
274
+
275
+ imports[item.name] = item.module;
276
+ return imports;
277
+ }, {});
278
+ };
279
+
280
+ globalThis.pgs ??= pgs;
@@ -1,77 +1,125 @@
1
- //= ACCORDION
2
- const allAccordion = pgs(document).querySelectorAll("accordion")
3
-
4
- allAccordion.forEach((accordion, index) => {
5
-
6
- const BUTTON = pgs(accordion).querySelector("accordion-button");
7
- const CONTENT = pgs(accordion).querySelector("accordion-content");
8
-
9
- //== ID univoci per aria-controls / aria-labelledby
10
- const ID = index + 1;
11
- const btnId = `acc-btn-${ID}`;
12
- const panelId = `acc-panel-${ID}`;
13
-
14
- //== Stato iniziale
15
- const isOpenInit = pgs(accordion).state.contains("open");
16
-
17
- //== Accessibilità (setup una volta)
18
- BUTTON.setAttribute("role", "button");
19
- BUTTON.setAttribute("tabindex", "0");
20
- BUTTON.setAttribute("id", btnId);
21
- BUTTON.setAttribute("aria-controls", panelId);
22
-
23
- CONTENT.setAttribute("id", panelId);
24
- CONTENT.setAttribute("role", "region");
25
- CONTENT.setAttribute("aria-labelledby", btnId);
26
-
27
- //+ Accessibility (applica stato aperto/chiuso)
28
- function accordionAccessibility(isOpen, button, content) {
29
- const text = (button?.textContent || "").trim().replace(/\s+/g, " ");
30
- button.setAttribute("aria-label", `${isOpen ? "Chiudi" : "Apri"} ${text || "sezione"}`);
31
- button.setAttribute("aria-expanded", String(isOpen));
32
- content.hidden = !isOpen;
33
- }
34
-
35
- //+ Chiudi tutti gli altri
36
- function closeOltherAccordion() {
37
- for (const otherLi of allAccordion) {
38
- if (otherLi === accordion) continue;
39
-
40
- const otherBtn = pgs(otherLi).querySelector("accordion-button");
41
- const otherContent = pgs(otherLi).querySelector("accordion-content");
42
- if (!otherBtn || !otherContent) continue;
43
-
44
- pgs(otherLi).state().remove("open");
45
- accordionAccessibility(false, otherBtn, otherContent);
1
+ //= ACCORDION
2
+ const API = new WeakMap();
3
+ let accordionId = 0;
4
+
5
+ function nextAccordionId() {
6
+ accordionId += 1;
7
+ return accordionId;
8
+ }
9
+
10
+ function directPgsChild(element, token) {
11
+ return Array.from(element.children).find(child => pgs(child).contains(token));
12
+ }
13
+
14
+ export function PGS_accordion_init(root = document) {
15
+ pgs(root).querySelectorAll("accordion").forEach((accordion) => {
16
+ if (API.has(accordion)) return;
17
+
18
+ const BUTTON = directPgsChild(accordion, "accordion-button");
19
+ const CONTENT = directPgsChild(accordion, "accordion-content");
20
+ if (!BUTTON || !CONTENT) return;
21
+
22
+ //== ID univoci per aria-controls / aria-labelledby
23
+ const ID = nextAccordionId();
24
+ const btnId = `acc-btn-${ID}`;
25
+ const panelId = `acc-panel-${ID}`;
26
+
27
+ //== Stato iniziale
28
+ const isOpenInit = pgs(accordion).state.contains("open");
29
+
30
+ //== Accessibilità (setup una volta)
31
+ BUTTON.setAttribute("role", "button");
32
+ BUTTON.setAttribute("tabindex", "0");
33
+ if (!BUTTON.id) BUTTON.setAttribute("id", btnId);
34
+
35
+ if (!CONTENT.id) CONTENT.setAttribute("id", panelId);
36
+ BUTTON.setAttribute("aria-controls", CONTENT.id);
37
+ CONTENT.setAttribute("role", "region");
38
+ CONTENT.setAttribute("aria-labelledby", BUTTON.id);
39
+
40
+ //+ Accessibility (applica stato aperto/chiuso)
41
+ function accordionAccessibility(isOpen, button, content) {
42
+ const text = (button?.textContent || "").trim().replace(/\s+/g, " ");
43
+ button.setAttribute("aria-label", `${isOpen ? "Chiudi" : "Apri"} ${text || "sezione"}`);
44
+ button.setAttribute("aria-expanded", String(isOpen));
45
+ content.hidden = !isOpen;
46
46
  }
47
- }
48
47
 
49
- //+ FN ACCORDION
50
- function accordionFunction() {
51
- const isOpen = pgs(accordion).state.contains("open");
52
- const nowOpen = !isOpen;
48
+ //+ Chiudi tutti gli altri
49
+ function closeOltherAccordion() {
50
+ for (const otherLi of pgs(document).querySelectorAll("accordion")) {
51
+ if (otherLi === accordion) continue;
53
52
 
54
- pgs(accordion).state.toggle("open", nowOpen);
55
- accordionAccessibility(nowOpen, BUTTON, CONTENT);
53
+ const otherBtn = pgs(otherLi).querySelector("accordion-button");
54
+ const otherContent = pgs(otherLi).querySelector("accordion-content");
55
+ if (!otherBtn || !otherContent) continue;
56
56
 
57
- closeOltherAccordion();
57
+ pgs(otherLi).state().remove("open");
58
+ accordionAccessibility(false, otherBtn, otherContent);
59
+ }
60
+ }
61
+
62
+ //+ FN ACCORDION
63
+ function accordionFunction() {
64
+ const isOpen = pgs(accordion).state.contains("open");
65
+ const nowOpen = !isOpen;
58
66
 
59
- //== scroll to view
60
- if (nowOpen) setTimeout(() => accordion.scrollIntoView({ block: "nearest", inline: "nearest" }), 100);
61
- }
67
+ pgs(accordion).state.toggle("open", nowOpen);
68
+ accordionAccessibility(nowOpen, BUTTON, CONTENT);
62
69
 
63
- // applica stato iniziale
64
- accordionAccessibility(isOpenInit, BUTTON, CONTENT);
70
+ closeOltherAccordion();
71
+
72
+ //== scroll to view
73
+ if (nowOpen) setTimeout(() => accordion.scrollIntoView({ block: "nearest", inline: "nearest" }), 100);
74
+ }
65
75
 
66
- //- Eventi
67
- BUTTON.addEventListener("click", accordionFunction);
76
+ function open() {
77
+ if (!pgs(accordion).state.contains("open")) accordionFunction();
78
+ }
68
79
 
69
- //- Tastiera: Enter / Space
70
- BUTTON.addEventListener("keydown", (e) => {
71
- if (e.key === "Enter" || e.key === " ") {
72
- e.preventDefault();
73
- accordionFunction();
80
+ function close() {
81
+ if (pgs(accordion).state.contains("open")) accordionFunction();
74
82
  }
83
+
84
+ // applica stato iniziale
85
+ accordionAccessibility(isOpenInit, BUTTON, CONTENT);
86
+
87
+ //- Eventi
88
+ BUTTON.addEventListener("click", accordionFunction);
89
+
90
+ //- Tastiera: Enter / Space
91
+ BUTTON.addEventListener("keydown", (e) => {
92
+ if (e.key === "Enter" || e.key === " ") {
93
+ e.preventDefault();
94
+ accordionFunction();
95
+ }
96
+ });
97
+
98
+ API.set(accordion, {
99
+ element: accordion,
100
+ button: BUTTON,
101
+ content: CONTENT,
102
+ open,
103
+ close,
104
+ toggle: accordionFunction,
105
+ refresh: () => {
106
+ PGS_accordion_init(accordion.parentNode || document);
107
+ return API.get(accordion);
108
+ },
109
+ isOpen: () => pgs(accordion).state.contains("open"),
110
+ });
75
111
  });
112
+ }
113
+
114
+ //# INIT
115
+ PGS_accordion_init();
116
+
117
+ //# API
118
+ export function PGS_accordion_api(selector) {
119
+ return API.get(selector);
120
+ }
76
121
 
77
- });
122
+ export const PGS_accordion = {
123
+ init: PGS_accordion_init,
124
+ api: PGS_accordion_api
125
+ };
@@ -1,108 +1,209 @@
1
- // + dropdown (Popover API)
2
- export function pgs_dropdown() {
3
- pgs(document).querySelectorAll("dropdown").forEach((DROPDOWN, index) => {
4
- const BUTTON = pgs(DROPDOWN).querySelector("dropdown-button");
5
- const CONTENT = pgs(DROPDOWN).querySelector("dropdown-content");
6
-
7
- if (!BUTTON || !CONTENT) return;
8
-
9
- // = INIT
10
- if (DROPDOWN.getAttribute("data-initialize") === "true") return;
11
- DROPDOWN.setAttribute("data-initialize", "true");
12
-
13
- // == CSS dropdown-anchor
14
- const ANCHOR_NAME = `--dropdown-anchor-${index}`;
15
- DROPDOWN.style.setProperty("--dropdown-anchor", ANCHOR_NAME);
16
-
17
- // == IDs + ACCESSIBILITY
18
- if (!BUTTON.id) BUTTON.id = `dropdown-btn-${index}`;
19
- if (!CONTENT.id) CONTENT.id = `dropdown-panel-${index}`;
20
- BUTTON.setAttribute("type", "button");
21
- BUTTON.setAttribute("aria-haspopup", "true");
22
- BUTTON.setAttribute("aria-controls", CONTENT.id);
23
- BUTTON.setAttribute("aria-expanded", "false");
24
- CONTENT.setAttribute("aria-labelledby", BUTTON.id);
25
-
26
- // == POPVER SETUP
27
- if (!CONTENT.hasAttribute("popover")) CONTENT.setAttribute("popover", "auto");
28
- BUTTON.setAttribute("popovertarget", CONTENT.id);
29
- BUTTON.setAttribute("popovertargetaction", "toggle");
30
-
31
- //-( Safari / legacy fallback: popover is in the top layer, so fixed coords are viewport-based.
32
- const HAS_ANCHOR_POSITIONING =
33
- CSS.supports("anchor-name: --dropdown-anchor") &&
34
- CSS.supports("position-anchor: --dropdown-anchor") &&
35
- CSS.supports("position-area: bottom") &&
36
- CSS.supports("top: anchor(bottom)");
37
- const USE_FALLBACK_POSITIONING = pgs(DROPDOWN).contains("tooltip") || !HAS_ANCHOR_POSITIONING;
38
-
39
- const updatePopoverPosition = () => {
40
- if (!USE_FALLBACK_POSITIONING) return;
41
-
42
- const buttonRect = BUTTON.getBoundingClientRect();
43
- const style = getComputedStyle(DROPDOWN);
44
- const offset = parseFloat(style.getPropertyValue("--dropdown-offset")) || 10;
45
- const padding = parseFloat(style.getPropertyValue("--dropdown-padding")) || 0;
46
- const arrowSize = parseFloat(style.getPropertyValue("--dropdown-arrow-size")) || 12;
47
- const viewportGap = 8;
48
- const viewportWidth = window.innerWidth;
49
- const maxWidth = viewportWidth - viewportGap * 2;
50
- const contentStyle = getComputedStyle(CONTENT);
51
- const cssMaxWidth = parseFloat(contentStyle.maxWidth);
52
- const dropdownMaxWidth = Number.isFinite(cssMaxWidth) ? Math.min(cssMaxWidth, maxWidth) : maxWidth;
53
- const contentWidth = Math.min(
54
- Math.max(CONTENT.scrollWidth + padding * 2, buttonRect.width),
55
- dropdownMaxWidth
56
- );
57
- const top = buttonRect.bottom + offset;
58
- const centeredLeft = buttonRect.left + buttonRect.width / 2 - contentWidth / 2;
59
- const left = Math.min(
60
- Math.max(centeredLeft, viewportGap),
61
- viewportWidth - contentWidth - viewportGap
62
- );
63
- const buttonCenter = buttonRect.left + buttonRect.width / 2;
64
- const arrowLeft = Math.min(
65
- Math.max(buttonCenter - left, padding + arrowSize),
66
- contentWidth - padding - arrowSize
67
- );
68
-
69
- DROPDOWN.style.setProperty("--dropdown-fallback-top", `${top}px`);
70
- DROPDOWN.style.setProperty("--dropdown-fallback-left", `${left}px`);
71
- DROPDOWN.style.setProperty("--dropdown-arrow-left", `${arrowLeft}px`);
72
- };
1
+ // + dropdown
2
+ const API = new WeakMap();
3
+ const OPEN_DROPDOWNS = new Set();
4
+ const VIEWPORT_GAP = 8;
5
+ let dropdownId = 0;
73
6
 
74
- BUTTON.addEventListener("click", e => {
75
- if (!USE_FALLBACK_POSITIONING) return;
7
+ function nextDropdownId() {
8
+ dropdownId += 1;
9
+ return dropdownId;
10
+ }
76
11
 
77
- e.preventDefault();
78
- if (CONTENT.matches(":popover-open")) {
79
- CONTENT.hidePopover();
80
- return;
81
- }
12
+ function isDropdownContent(element) {
13
+ return element instanceof Element && pgs(element).contains("dropdown-content");
14
+ }
82
15
 
83
- updatePopoverPosition();
84
- CONTENT.showPopover();
85
- });
16
+ function getDropdownTrigger(dropdown, content) {
17
+ const children = Array.from(dropdown.children).filter(child => child !== content);
18
+ const dropdownButton = children.find(child => pgs(child).contains("dropdown-button"));
86
19
 
87
- // == sync ARIA + data-open quando apre/chiude
88
- CONTENT.addEventListener("toggle", e => {
89
- const open = CONTENT.matches(":popover-open");
90
- BUTTON.setAttribute("aria-expanded", open ? "true" : "false");
91
- pgs(DROPDOWN).state.toggle("open", open);
92
- if (open) {
93
- updatePopoverPosition();
94
- CONTENT.querySelector('a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])')?.focus();
95
- }
96
- });
20
+ return dropdownButton || children.find(child => !isDropdownContent(child)) || dropdown;
21
+ }
22
+
23
+ function getDropdownContent(dropdown) {
24
+ return Array.from(dropdown.children).find(isDropdownContent) || pgs(dropdown).querySelector("dropdown-content");
25
+ }
26
+
27
+ function getDropdowns(root) {
28
+ const dropdowns = root instanceof Element && pgs(root).contains("dropdown") ? [root] : [];
29
+ dropdowns.push(...pgs(root).querySelectorAll("dropdown"));
30
+ return dropdowns;
31
+ }
32
+
33
+ function getposition(dropdown) {
34
+ const option = pgs(dropdown).option;
35
+ const optionValue = option.getValueBrackets("position");
36
+ const raw = (optionValue || "bottom center").trim().toLowerCase();
37
+ const parts = raw.split(/\s+/).filter(Boolean);
38
+ const side = parts.find(part => ["top", "right", "bottom", "left"].includes(part)) || "bottom";
39
+ const align = parts.find(part => ["top", "right", "bottom", "left", "center"].includes(part) && part !== side) || "center";
40
+
41
+ return { side, align };
42
+ }
43
+
44
+ function clamp(value, min, max) {
45
+ return Math.min(Math.max(value, min), max);
46
+ }
47
+
48
+ function updateposition(dropdown) {
49
+ const data = API.get(dropdown);
50
+ if (!data || !data.isOpen()) return;
51
+
52
+ const { trigger, content } = data;
53
+ const { side, align } = getposition(dropdown);
54
+ const triggerRect = trigger.getBoundingClientRect();
55
+ const contentRect = content.getBoundingClientRect();
56
+ const viewportWidth = document.documentElement.clientWidth;
57
+ const maxLeft = Math.max(VIEWPORT_GAP, viewportWidth - contentRect.width - VIEWPORT_GAP);
58
+ let left = triggerRect.left + (triggerRect.width - contentRect.width) / 2;
59
+ let top = triggerRect.bottom + VIEWPORT_GAP;
60
+
61
+ if (side === "top" || side === "bottom") {
62
+ top = side === "top"
63
+ ? triggerRect.top - contentRect.height - VIEWPORT_GAP
64
+ : triggerRect.bottom + VIEWPORT_GAP;
65
+
66
+ if (align === "left") left = triggerRect.left;
67
+ if (align === "right") left = triggerRect.right - contentRect.width;
68
+ }
69
+
70
+ if (side === "left" || side === "right") {
71
+ left = side === "left"
72
+ ? triggerRect.left - contentRect.width - VIEWPORT_GAP
73
+ : triggerRect.right + VIEWPORT_GAP;
74
+ top = triggerRect.top + (triggerRect.height - contentRect.height) / 2;
75
+
76
+ if (align === "top") top = triggerRect.top;
77
+ if (align === "bottom") top = triggerRect.bottom - contentRect.height;
78
+ }
79
+
80
+ if (side === "left" && left < VIEWPORT_GAP) {
81
+ left = triggerRect.right + VIEWPORT_GAP;
82
+ }
83
+
84
+ if (side === "right" && left + contentRect.width > viewportWidth - VIEWPORT_GAP) {
85
+ left = triggerRect.left - contentRect.width - VIEWPORT_GAP;
86
+ }
87
+
88
+ left = clamp(left, VIEWPORT_GAP, maxLeft);
89
+
90
+ content.style.setProperty("--dropdown-left", `${Math.round(left)}px`);
91
+ content.style.setProperty("--dropdown-top", `${Math.round(top)}px`);
92
+ }
93
+
94
+ function updateOpenDropdowns() {
95
+ OPEN_DROPDOWNS.forEach(updateposition);
96
+ }
97
+
98
+ function closeDropdown(dropdown) {
99
+ const data = API.get(dropdown);
100
+ if (!data || !data.isOpen()) return;
97
101
 
98
- window.addEventListener("resize", () => {
99
- if (CONTENT.matches(":popover-open")) updatePopoverPosition();
102
+ pgs(dropdown).state.remove("open");
103
+ data.trigger.setAttribute("aria-expanded", "false");
104
+ OPEN_DROPDOWNS.delete(dropdown);
105
+ }
106
+
107
+ function openDropdown(dropdown) {
108
+ const data = API.get(dropdown);
109
+ if (!data || data.isOpen()) return;
110
+
111
+ OPEN_DROPDOWNS.forEach(item => {
112
+ if (item !== dropdown) closeDropdown(item);
113
+ });
114
+
115
+ pgs(dropdown).state.add("open");
116
+ data.trigger.setAttribute("aria-expanded", "true");
117
+ OPEN_DROPDOWNS.add(dropdown);
118
+ updateposition(dropdown);
119
+ }
120
+
121
+ function toggleDropdown(dropdown) {
122
+ const data = API.get(dropdown);
123
+ if (!data) return;
124
+
125
+ if (data.isOpen()) closeDropdown(dropdown);
126
+ else openDropdown(dropdown);
127
+ }
128
+
129
+ function isInsideAnyDropdown(target) {
130
+ return Array.from(OPEN_DROPDOWNS).some(dropdown => dropdown.contains(target));
131
+ }
132
+
133
+ function PGS_dropdown_init(root = document) {
134
+ getDropdowns(root).forEach((DROPDOWN) => {
135
+ if (API.has(DROPDOWN)) return;
136
+
137
+ const CONTENT = getDropdownContent(DROPDOWN);
138
+ if (!CONTENT) return;
139
+
140
+ const TRIGGER = getDropdownTrigger(DROPDOWN, CONTENT);
141
+ const id = nextDropdownId();
142
+
143
+ if (!TRIGGER.id) TRIGGER.id = `dropdown-btn-${id}`;
144
+ if (!CONTENT.id) CONTENT.id = `dropdown-panel-${id}`;
145
+
146
+ if (TRIGGER.matches("button") && !TRIGGER.hasAttribute("type")) {
147
+ TRIGGER.setAttribute("type", "button");
148
+ }
149
+
150
+ TRIGGER.setAttribute("aria-haspopup", "true");
151
+ TRIGGER.setAttribute("aria-controls", CONTENT.id);
152
+ TRIGGER.setAttribute("aria-expanded", String(pgs(DROPDOWN).state.contains("open")));
153
+ CONTENT.setAttribute("aria-labelledby", TRIGGER.id);
154
+
155
+ const data = {
156
+ element: DROPDOWN,
157
+ trigger: TRIGGER,
158
+ content: CONTENT,
159
+ open: () => openDropdown(DROPDOWN),
160
+ close: () => closeDropdown(DROPDOWN),
161
+ toggle: () => toggleDropdown(DROPDOWN),
162
+ refresh: () => {
163
+ PGS_dropdown_init(DROPDOWN.parentNode || document);
164
+ updateposition(DROPDOWN);
165
+ return API.get(DROPDOWN);
166
+ },
167
+ isOpen: () => pgs(DROPDOWN).state.contains("open")
168
+ };
169
+
170
+ TRIGGER.addEventListener("click", (event) => {
171
+ if (isDropdownContent(event.target)) return;
172
+ event.preventDefault();
173
+ event.stopPropagation();
174
+ toggleDropdown(DROPDOWN);
100
175
  });
101
- window.addEventListener("scroll", () => {
102
- if (CONTENT.matches(":popover-open")) updatePopoverPosition();
103
- }, true);
176
+
177
+ CONTENT.addEventListener("click", event => event.stopPropagation());
178
+ API.set(DROPDOWN, data);
179
+
180
+ if (data.isOpen()) OPEN_DROPDOWNS.add(DROPDOWN);
181
+ updateposition(DROPDOWN);
104
182
  });
105
183
  }
106
184
 
185
+ document.addEventListener("click", (event) => {
186
+ if (isInsideAnyDropdown(event.target)) return;
187
+ OPEN_DROPDOWNS.forEach(closeDropdown);
188
+ });
189
+
190
+ document.addEventListener("keydown", (event) => {
191
+ if (event.key !== "Escape") return;
192
+ OPEN_DROPDOWNS.forEach(closeDropdown);
193
+ });
194
+
195
+ window.addEventListener("resize", updateOpenDropdowns);
196
+ window.addEventListener("scroll", updateOpenDropdowns, true);
197
+
107
198
  // # INIT
108
- pgs_dropdown();
199
+ PGS_dropdown_init();
200
+
201
+ // # API
202
+ function PGS_dropdown_api(selector) {
203
+ return API.get(selector);
204
+ }
205
+
206
+ export const PGS_dropdown = {
207
+ init: PGS_dropdown_init,
208
+ api: PGS_dropdown_api
209
+ };