sprinkles-js 0.1.0 → 0.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/index.js +148 -78
- package/dist/index.js.map +1 -1
- package/dist/index.umd.cjs +1 -1
- package/dist/index.umd.cjs.map +1 -1
- package/package.json +8 -8
package/dist/index.js
CHANGED
|
@@ -1,98 +1,168 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
1
|
+
class a extends HTMLElement {
|
|
2
|
+
/**
|
|
3
|
+
* The default tagName that will be used to register this elements
|
|
4
|
+
*/
|
|
5
|
+
static tagName = "sprinkles-element";
|
|
6
|
+
/**
|
|
7
|
+
* An array of elements that will be referenced in the elements
|
|
8
|
+
*
|
|
9
|
+
* These refs are expected to match DOM-nodes following the pattern
|
|
10
|
+
* `[data-TAG-NAME-ref="refName"]`
|
|
11
|
+
*
|
|
12
|
+
* @type {string[]}
|
|
13
|
+
*/
|
|
14
|
+
static refs = [];
|
|
15
|
+
/**
|
|
16
|
+
* An object with events that should be observed.
|
|
17
|
+
*
|
|
18
|
+
* For each event a listener will be added when the elements connects (and removed when it disconnects)
|
|
19
|
+
*
|
|
20
|
+
* @type {{ [eventName: string]: string | { method: string; ref?: string, options?: boolean | AddEventListenerOptions; immediate?: boolean } | string[] | { method: string; ref?: string, options?: boolean | AddEventListenerOptions; immediate?: boolean }[] }}
|
|
21
|
+
*/
|
|
22
|
+
static events = {};
|
|
23
|
+
static register(t = this.tagName, e) {
|
|
24
|
+
"customElements" in window && (e || customElements).define(t, this);
|
|
23
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* The found refs, based on the class field `refs`
|
|
28
|
+
*
|
|
29
|
+
* @type { [name: string]: Element | null}
|
|
30
|
+
*/
|
|
31
|
+
refs = {};
|
|
24
32
|
connectedCallback() {
|
|
25
|
-
|
|
33
|
+
this.#e(), this.#r(), this.afterConnected();
|
|
26
34
|
}
|
|
27
35
|
disconnectedCallback() {
|
|
28
|
-
this.beforeDisconnect(), i(
|
|
36
|
+
this.beforeDisconnect(), this.#i();
|
|
29
37
|
}
|
|
30
38
|
// Callbacks
|
|
31
39
|
afterConnected() {
|
|
32
40
|
}
|
|
33
41
|
beforeDisconnect() {
|
|
34
42
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
`[data-${this.constructor.tagName}-ref=${e}]`
|
|
41
|
-
);
|
|
42
|
-
});
|
|
43
|
-
}, u = function() {
|
|
44
|
-
Object.entries(this.constructor.events).forEach(([e, c]) => {
|
|
45
|
-
i(this, o, h).call(this, c).forEach((a) => {
|
|
46
|
-
a.element && (a.element.addEventListener(
|
|
47
|
-
e,
|
|
48
|
-
a.callback,
|
|
49
|
-
a.options
|
|
50
|
-
), a.immediate && a.callback.call());
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
}, b = function() {
|
|
54
|
-
Object.entries(this.constructor.events).forEach(([e, c]) => {
|
|
55
|
-
i(this, o, h).call(this, c).forEach((a) => {
|
|
56
|
-
a.element && a.element.removeEventListener(
|
|
57
|
-
e,
|
|
58
|
-
a.callback,
|
|
59
|
-
a.options
|
|
43
|
+
// Setup
|
|
44
|
+
#e() {
|
|
45
|
+
this.refs = {}, this.constructor.refs.forEach((t) => {
|
|
46
|
+
this.refs[t] = this.querySelector(
|
|
47
|
+
`[data-${this.constructor.tagName}-ref=${t}]`
|
|
60
48
|
);
|
|
61
49
|
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
50
|
+
}
|
|
51
|
+
#r() {
|
|
52
|
+
Object.entries(this.constructor.events).forEach(([t, e]) => {
|
|
53
|
+
this.#t(e).forEach((r) => {
|
|
54
|
+
r.element && (r.element.addEventListener(
|
|
55
|
+
t,
|
|
56
|
+
r.callback,
|
|
57
|
+
r.options
|
|
58
|
+
), r.immediate && r.callback.call());
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
#i() {
|
|
63
|
+
Object.entries(this.constructor.events).forEach(([t, e]) => {
|
|
64
|
+
this.#t(e).forEach((r) => {
|
|
65
|
+
r.element && r.element.removeEventListener(
|
|
66
|
+
t,
|
|
67
|
+
r.callback,
|
|
68
|
+
r.options
|
|
69
|
+
);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
#t(t) {
|
|
74
|
+
return t = Array.isArray(t) ? t : [t], t.map((e) => (e = typeof e == "string" ? { method: e } : { ...e }, e.element ||= e.ref ? this.refs[e.ref] : this, e.immediate ||= !1, e.callback = this[e.method].bind(this), e));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
class b extends a {
|
|
78
|
+
static tagName = "character-counter";
|
|
79
|
+
static refs = ["input", "output", "error"];
|
|
80
|
+
static events = {
|
|
81
|
+
input: { method: "updateCounter", immediate: !0 },
|
|
82
|
+
// We detect that the limit is reached through `keydown`, since input isn't triggered (since the user can't input any more)
|
|
83
|
+
// and `keyup` fires after the input event and would be triggered on the Nth character instead of the N + 1 character
|
|
84
|
+
keydown: { method: "alertLimitReached" }
|
|
85
|
+
};
|
|
86
|
+
#e = !1;
|
|
87
|
+
get maxLength() {
|
|
88
|
+
const t = this.refs.input.maxLength;
|
|
89
|
+
return t === -1 ? 1 / 0 : t;
|
|
90
|
+
}
|
|
91
|
+
get valueLength() {
|
|
92
|
+
return this.refs.input.value.length;
|
|
93
|
+
}
|
|
94
|
+
get remainingCharacters() {
|
|
95
|
+
return this.maxLength - this.valueLength;
|
|
96
|
+
}
|
|
97
|
+
updateCounter() {
|
|
98
|
+
Number.isFinite(this.maxLength) && this.refs.output !== null && (this.refs.output.innerText = this.dataset.characterCounterTemplate.replace("{{current}}", this.valueLength).replace("{{total}}", this.maxLength).replace("{{remaining}}", this.remainingCharacters), this.#e && this.remainingCharacters > 0 && (this.refs.error.innerText = null));
|
|
99
|
+
}
|
|
100
|
+
alertLimitReached(t) {
|
|
101
|
+
this.remainingCharacters > 0 || this.dataset.characterCounterLimitMessage !== void 0 && (h(t.key) || d(t)) && (this.refs.error.hidden && (this.refs.error.hidden = !1), this.refs.error.innerText = this.dataset.characterCounterLimitMessage, this.#e = !0);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function h(i) {
|
|
105
|
+
return i.length === 1 && i.match(/\S| /);
|
|
106
|
+
}
|
|
107
|
+
const m = [
|
|
108
|
+
"Process",
|
|
109
|
+
// Firefox
|
|
110
|
+
"Unidentified"
|
|
111
|
+
// Chrome (but only 127+?)
|
|
112
|
+
];
|
|
113
|
+
function d(i) {
|
|
114
|
+
return m.includes(i.key) || i.keyCode == 229;
|
|
115
|
+
}
|
|
116
|
+
class p extends a {
|
|
117
|
+
static tagName = "clickable-card";
|
|
118
|
+
static refs = ["link"];
|
|
119
|
+
static events = {
|
|
120
|
+
click: "handleClick"
|
|
121
|
+
};
|
|
85
122
|
handleClick(t) {
|
|
86
|
-
if (t.target.closest(
|
|
123
|
+
if (t.target.closest(
|
|
124
|
+
`${this.constructor.tagName} :is(a,button,summary,input,select)`
|
|
125
|
+
))
|
|
87
126
|
return;
|
|
88
127
|
!window.getSelection().toString() && this.refs.link.click();
|
|
89
128
|
}
|
|
90
129
|
}
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
130
|
+
function f(i, t) {
|
|
131
|
+
let e, r, n, c = 0;
|
|
132
|
+
function l() {
|
|
133
|
+
i.apply(r, n), n = null, r = null;
|
|
134
|
+
}
|
|
135
|
+
function o() {
|
|
136
|
+
const s = Date.now() - c;
|
|
137
|
+
s < t && s >= 0 ? e = setTimeout(o, t - s) : (clearTimeout(e), e = null, l());
|
|
138
|
+
}
|
|
139
|
+
const u = () => {
|
|
140
|
+
r = this, n = arguments, c = Date.now(), e || (e = setTimeout(o, t));
|
|
141
|
+
};
|
|
142
|
+
return u.trigger = () => {
|
|
143
|
+
e && (clearTimeout(e), e = null), l();
|
|
144
|
+
}, u;
|
|
145
|
+
}
|
|
146
|
+
class C extends a {
|
|
147
|
+
static tagName = "form-autosubmit";
|
|
148
|
+
static refs = ["button"];
|
|
149
|
+
static events = {
|
|
150
|
+
change: "submit",
|
|
151
|
+
input: "debouncedSubmit"
|
|
152
|
+
};
|
|
153
|
+
debouncedSubmit = f(this.#e.bind(this), 250);
|
|
154
|
+
submit() {
|
|
155
|
+
this.debouncedSubmit.trigger();
|
|
156
|
+
}
|
|
157
|
+
#e() {
|
|
158
|
+
this.refs.button.click();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
94
161
|
export {
|
|
95
|
-
|
|
96
|
-
|
|
162
|
+
b as CharacterCounterElement,
|
|
163
|
+
p as ClickableCardElement,
|
|
164
|
+
C as FormAutosubmitElement,
|
|
165
|
+
a as SprinklesElement,
|
|
166
|
+
f as debounce
|
|
97
167
|
};
|
|
98
168
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/elements/sprinkles_element.js","../src/elements/clickable_card_element.js"],"sourcesContent":["/**\n * SprinklesElement is an abstract class to help us build custom elements\n *\n * By defining refs and events the element will automatically run some setup when connected.\n */\nexport default class SprinklesElement extends HTMLElement {\n /**\n * The default tagName that will be used to register this elements\n */\n static tagName = \"sprinkles-element\";\n\n /**\n * An array of elements that will be referenced in the elements\n *\n * These refs are expected to match DOM-nodes following the pattern\n * `[data-TAG-NAME-ref=\"refName\"]`\n *\n * @type {string[]}\n */\n static refs = [];\n\n /**\n * An object with events that should be observed.\n *\n * For each event a listener will be added when the elements connects (and removed when it disconnects)\n *\n * @type {{ [eventName: string]: string | { method: string; ref?: string, options?: boolean | AddEventListenerOptions; immediate?: boolean } | string[] | { method: string; ref?: string, options?: boolean | AddEventListenerOptions; immediate?: boolean }[] }}\n */\n static events = {};\n\n static register(tagName = this.tagName, registry) {\n if (\"customElements\" in window) {\n (registry || customElements).define(tagName, this);\n }\n }\n\n /**\n * The found refs, based on the class field `refs`\n *\n * @type { [name: string]: Element | null}\n */\n refs = {};\n\n connectedCallback() {\n this.#detectRefs();\n this.#bindEvents();\n this.afterConnected();\n }\n\n disconnectedCallback() {\n this.beforeDisconnect();\n this.#unbindEvents();\n }\n\n // Callbacks\n afterConnected() {}\n\n beforeDisconnect() {}\n\n // Setup\n #detectRefs() {\n this.refs = {};\n this.constructor.refs.forEach((refName) => {\n this.refs[refName] = this.querySelector(\n `[data-${this.constructor.tagName}-ref=${refName}]`,\n );\n });\n }\n\n #bindEvents() {\n Object.entries(this.constructor.events).forEach(([eventName, settings]) => {\n this.#normalizeEventSettings(settings).forEach((setting) => {\n // If we don't have an element, we simply return (this can happen when a ref could not be found)\n if (!setting.element) return;\n\n setting.element.addEventListener(\n eventName,\n setting.callback,\n setting.options,\n );\n if (setting.immediate) setting.callback.call();\n });\n });\n }\n\n #unbindEvents() {\n Object.entries(this.constructor.events).forEach(([eventName, settings]) => {\n this.#normalizeEventSettings(settings).forEach((setting) => {\n if (!setting.element) return;\n\n setting.element.removeEventListener(\n eventName,\n setting.callback,\n setting.options,\n );\n });\n });\n }\n\n #normalizeEventSettings(settings) {\n // Settings can be passed as an object or an array of objects, we always normalize this to an array\n settings = Array.isArray(settings) ? settings : [settings];\n return settings.map((setting) => {\n // We always clone settings, so we don't modify the value that is stored as a class field\n // Otherwise setting `element` will mess with other instances of the same element\n setting =\n typeof setting === \"string\" ? { method: setting } : { ...setting };\n setting.element ||= setting[\"ref\"] ? this.refs[setting[\"ref\"]] : this;\n setting.immediate ||= false;\n setting.callback = this[setting.method].bind(this);\n return setting;\n });\n }\n}\n","import SprinklesElement from \"./sprinkles_element\";\n\n/**\n * A card that can be clicked anywhere to follow a link inside it\n *\n * This ensures that a screen reader does not to read the whole card to announce the link, while a user can still click anywhere in the card.\n * You can include other links and buttons in the card, these will not trigger a navigation.\n *\n * This is based on the recipe and discussion from Web Accessibility Cookbook by Manuel Matuzović (O’Reilly). Copyright 2024 Manuel Matuzović, 978-1-098-14560-6.\n * and this article from Vikas Parashar: https://css-tricks.com/block-links-the-search-for-a-perfect-solution/#aa-method-4-sprinkle-javascript-on-the-second-method\n */\nexport default class ClickableCardElement extends SprinklesElement {\n static tagName = \"clickable-card\";\n static refs = [\"link\"];\n static events = {\n click: \"handleClick\",\n };\n\n handleClick(event) {\n // Don't handle clicks on buttons or anchors\n if (event.target.closest(`${this.constructor.tagName} :is(a,button)`)) {\n return;\n }\n\n // Check if text is selected and don't click if so\n const noTextSelected = !window.getSelection().toString();\n if (noTextSelected) this.refs.link.click();\n }\n}\n"],"names":["_SprinklesElement_instances","detectRefs_fn","bindEvents_fn","unbindEvents_fn","normalizeEventSettings_fn","SprinklesElement","__privateAdd","__publicField","tagName","registry","__privateMethod","refName","eventName","settings","setting","ClickableCardElement","event"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/elements/sprinkles_element.js","../src/elements/character_counter_element.js","../src/elements/clickable_card_element.js","../src/lib/debounce.js","../src/elements/form_autosubmit_element.js"],"sourcesContent":["/**\n * SprinklesElement is an abstract class to help us build custom elements\n *\n * By defining refs and events the element will automatically run some setup when connected.\n */\nexport default class SprinklesElement extends HTMLElement {\n /**\n * The default tagName that will be used to register this elements\n */\n static tagName = \"sprinkles-element\";\n\n /**\n * An array of elements that will be referenced in the elements\n *\n * These refs are expected to match DOM-nodes following the pattern\n * `[data-TAG-NAME-ref=\"refName\"]`\n *\n * @type {string[]}\n */\n static refs = [];\n\n /**\n * An object with events that should be observed.\n *\n * For each event a listener will be added when the elements connects (and removed when it disconnects)\n *\n * @type {{ [eventName: string]: string | { method: string; ref?: string, options?: boolean | AddEventListenerOptions; immediate?: boolean } | string[] | { method: string; ref?: string, options?: boolean | AddEventListenerOptions; immediate?: boolean }[] }}\n */\n static events = {};\n\n static register(tagName = this.tagName, registry) {\n if (\"customElements\" in window) {\n (registry || customElements).define(tagName, this);\n }\n }\n\n /**\n * The found refs, based on the class field `refs`\n *\n * @type { [name: string]: Element | null}\n */\n refs = {};\n\n connectedCallback() {\n this.#detectRefs();\n this.#bindEvents();\n this.afterConnected();\n }\n\n disconnectedCallback() {\n this.beforeDisconnect();\n this.#unbindEvents();\n }\n\n // Callbacks\n afterConnected() {}\n\n beforeDisconnect() {}\n\n // Setup\n #detectRefs() {\n this.refs = {};\n this.constructor.refs.forEach((refName) => {\n this.refs[refName] = this.querySelector(\n `[data-${this.constructor.tagName}-ref=${refName}]`,\n );\n });\n }\n\n #bindEvents() {\n Object.entries(this.constructor.events).forEach(([eventName, settings]) => {\n this.#normalizeEventSettings(settings).forEach((setting) => {\n // If we don't have an element, we simply return (this can happen when a ref could not be found)\n if (!setting.element) return;\n\n setting.element.addEventListener(\n eventName,\n setting.callback,\n setting.options,\n );\n if (setting.immediate) setting.callback.call();\n });\n });\n }\n\n #unbindEvents() {\n Object.entries(this.constructor.events).forEach(([eventName, settings]) => {\n this.#normalizeEventSettings(settings).forEach((setting) => {\n if (!setting.element) return;\n\n setting.element.removeEventListener(\n eventName,\n setting.callback,\n setting.options,\n );\n });\n });\n }\n\n #normalizeEventSettings(settings) {\n // Settings can be passed as an object or an array of objects, we always normalize this to an array\n settings = Array.isArray(settings) ? settings : [settings];\n return settings.map((setting) => {\n // We always clone settings, so we don't modify the value that is stored as a class field\n // Otherwise setting `element` will mess with other instances of the same element\n setting =\n typeof setting === \"string\" ? { method: setting } : { ...setting };\n setting.element ||= setting[\"ref\"] ? this.refs[setting[\"ref\"]] : this;\n setting.immediate ||= false;\n setting.callback = this[setting.method].bind(this);\n return setting;\n });\n }\n}\n","import SprinklesElement from \"./sprinkles_element\";\n\n/**\n * An element that wraps an input and an element to print the character counter\n *\n * On every keystroke the counter is updated. If the max length of the input is reached\n * an error can be added to alert the user.\n */\nexport default class CharacterCounterElement extends SprinklesElement {\n static tagName = \"character-counter\";\n static refs = [\"input\", \"output\", \"error\"];\n static events = {\n input: { method: \"updateCounter\", immediate: true },\n // We detect that the limit is reached through `keydown`, since input isn't triggered (since the user can't input any more)\n // and `keyup` fires after the input event and would be triggered on the Nth character instead of the N + 1 character\n keydown: { method: \"alertLimitReached\" },\n };\n\n #hasAlerted = false;\n\n get maxLength() {\n const maxLength = this.refs.input.maxLength;\n // If maxLength isn't set (and returns -1), we set this to infinity\n return maxLength === -1 ? Infinity : maxLength;\n }\n\n get valueLength() {\n return this.refs.input.value.length;\n }\n\n get remainingCharacters() {\n return this.maxLength - this.valueLength;\n }\n\n updateCounter() {\n if (!Number.isFinite(this.maxLength)) return;\n if (this.refs.output === null) return;\n\n this.refs.output.innerText = this.dataset.characterCounterTemplate\n .replace(\"{{current}}\", this.valueLength)\n .replace(\"{{total}}\", this.maxLength)\n .replace(\"{{remaining}}\", this.remainingCharacters);\n\n if (this.#hasAlerted && this.remainingCharacters > 0) {\n this.refs.error.innerText = null;\n }\n }\n\n alertLimitReached(event) {\n if (this.remainingCharacters > 0) return;\n if (this.dataset.characterCounterLimitMessage === undefined) return;\n\n if (isPrintableCharacter(event.key) || isAndroidKey(event)) {\n if (this.refs.error.hidden) this.refs.error.hidden = false;\n this.refs.error.innerText = this.dataset.characterCounterLimitMessage;\n this.#hasAlerted = true;\n }\n }\n}\n\nfunction isPrintableCharacter(str) {\n return str.length === 1 && str.match(/\\S| /);\n}\n\nconst ANDROID_KEYS = [\n \"Process\", // Firefox\n \"Unidentified\", // Chrome (but only 127+?)\n];\n\n// NOTE: Certain android devices always send an event with a special key and/or keyCode `229`\nfunction isAndroidKey(event) {\n return ANDROID_KEYS.includes(event.key) || event.keyCode == 229;\n}\n","import SprinklesElement from \"./sprinkles_element\";\n\n/**\n * A card that can be clicked anywhere to follow a link inside it\n *\n * This ensures that a screen reader does not to read the whole card to announce the link, while a user can still click anywhere in the card.\n * You can include other interactive elements in the card, these will not trigger a navigation.\n *\n * This is based on the recipe and discussion from Web Accessibility Cookbook by Manuel Matuzović (O’Reilly). Copyright 2024 Manuel Matuzović, 978-1-098-14560-6.\n * and this article from Vikas Parashar: https://css-tricks.com/block-links-the-search-for-a-perfect-solution/#aa-method-4-sprinkle-javascript-on-the-second-method\n */\nexport default class ClickableCardElement extends SprinklesElement {\n static tagName = \"clickable-card\";\n static refs = [\"link\"];\n static events = {\n click: \"handleClick\",\n };\n\n handleClick(event) {\n // Don't handle clicks on other interactive elements\n if (\n event.target.closest(\n `${this.constructor.tagName} :is(a,button,summary,input,select)`,\n )\n ) {\n return;\n }\n\n // Check if text is selected and don't click if so\n const noTextSelected = !window.getSelection().toString();\n if (noTextSelected) this.refs.link.click();\n }\n}\n","export function debounce(callback, wait) {\n let timeout, context, args;\n let lastCall = 0;\n\n function run() {\n callback.apply(context, args);\n args = null;\n context = null;\n }\n\n function later() {\n const delta = Date.now() - lastCall;\n\n if (delta < wait && delta >= 0) {\n timeout = setTimeout(later, wait - delta);\n } else {\n clearTimeout(timeout);\n timeout = null;\n run();\n }\n }\n\n const debounced = () => {\n context = this;\n args = arguments;\n lastCall = Date.now();\n\n if (!timeout) {\n timeout = setTimeout(later, wait);\n }\n };\n\n debounced.trigger = () => {\n if (timeout) {\n clearTimeout(timeout);\n timeout = null;\n }\n run();\n };\n\n return debounced;\n}\n","import SprinklesElement from \"./sprinkles_element\";\nimport { debounce } from \"../lib/debounce\";\n\n/**\n * This element submits a form automatically clicking on a hidden submit input/button\n *\n * The form gets submitted on each change, or on input with a debounce.\n * You still need to make sure that this is what the user expects and/or that the submit is captured\n * and only part of the DOM get's updated using a technique like turbo frames.\n * (We don't use `form.submit()` since that event isn't captured by turbo)\n */\nexport default class FormAutosubmitElement extends SprinklesElement {\n static tagName = \"form-autosubmit\";\n static refs = [\"button\"];\n static events = {\n change: \"submit\",\n input: \"debouncedSubmit\",\n };\n\n debouncedSubmit = debounce(this.#submit.bind(this), 250);\n\n submit() {\n this.debouncedSubmit.trigger();\n }\n\n #submit() {\n this.refs.button.click();\n }\n}\n"],"names":["SprinklesElement","tagName","registry","#detectRefs","#bindEvents","#unbindEvents","refName","eventName","settings","#normalizeEventSettings","setting","CharacterCounterElement","#hasAlerted","maxLength","event","isPrintableCharacter","isAndroidKey","str","ANDROID_KEYS","ClickableCardElement","debounce","callback","wait","timeout","context","args","lastCall","run","later","delta","debounced","FormAutosubmitElement","#submit"],"mappings":"AAKe,MAAMA,UAAyB,YAAY;AAAA;AAAA;AAAA;AAAA,EAIxD,OAAO,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUjB,OAAO,OAAO,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASd,OAAO,SAAS,CAAA;AAAA,EAEhB,OAAO,SAASC,IAAU,KAAK,SAASC,GAAU;AAChD,IAAI,oBAAoB,WACrBA,KAAY,gBAAgB,OAAOD,GAAS,IAAI;AAAA,EAErD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,OAAO,CAAA;AAAA,EAEP,oBAAoB;AAClB,SAAKE,GAAW,GAChB,KAAKC,GAAW,GAChB,KAAK,eAAc;AAAA,EACrB;AAAA,EAEA,uBAAuB;AACrB,SAAK,iBAAgB,GACrB,KAAKC,GAAa;AAAA,EACpB;AAAA;AAAA,EAGA,iBAAiB;AAAA,EAAC;AAAA,EAElB,mBAAmB;AAAA,EAAC;AAAA;AAAA,EAGpBF,KAAc;AACZ,SAAK,OAAO,CAAA,GACZ,KAAK,YAAY,KAAK,QAAQ,CAACG,MAAY;AACzC,WAAK,KAAKA,CAAO,IAAI,KAAK;AAAA,QACxB,SAAS,KAAK,YAAY,OAAO,QAAQA,CAAO;AAAA,MACxD;AAAA,IACI,CAAC;AAAA,EACH;AAAA,EAEAF,KAAc;AACZ,WAAO,QAAQ,KAAK,YAAY,MAAM,EAAE,QAAQ,CAAC,CAACG,GAAWC,CAAQ,MAAM;AACzE,WAAKC,GAAwBD,CAAQ,EAAE,QAAQ,CAACE,MAAY;AAE1D,QAAKA,EAAQ,YAEbA,EAAQ,QAAQ;AAAA,UACdH;AAAA,UACAG,EAAQ;AAAA,UACRA,EAAQ;AAAA,QAClB,GACYA,EAAQ,aAAWA,EAAQ,SAAS,KAAI;AAAA,MAC9C,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA,EAEAL,KAAgB;AACd,WAAO,QAAQ,KAAK,YAAY,MAAM,EAAE,QAAQ,CAAC,CAACE,GAAWC,CAAQ,MAAM;AACzE,WAAKC,GAAwBD,CAAQ,EAAE,QAAQ,CAACE,MAAY;AAC1D,QAAKA,EAAQ,WAEbA,EAAQ,QAAQ;AAAA,UACdH;AAAA,UACAG,EAAQ;AAAA,UACRA,EAAQ;AAAA,QAClB;AAAA,MACM,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA,EAEAD,GAAwBD,GAAU;AAEhC,WAAAA,IAAW,MAAM,QAAQA,CAAQ,IAAIA,IAAW,CAACA,CAAQ,GAClDA,EAAS,IAAI,CAACE,OAGnBA,IACE,OAAOA,KAAY,WAAW,EAAE,QAAQA,EAAO,IAAK,EAAE,GAAGA,EAAO,GAClEA,EAAQ,YAAYA,EAAQ,MAAS,KAAK,KAAKA,EAAQ,GAAM,IAAI,MACjEA,EAAQ,cAAc,IACtBA,EAAQ,WAAW,KAAKA,EAAQ,MAAM,EAAE,KAAK,IAAI,GAC1CA,EACR;AAAA,EACH;AACF;ACzGe,MAAMC,UAAgCX,EAAiB;AAAA,EACpE,OAAO,UAAU;AAAA,EACjB,OAAO,OAAO,CAAC,SAAS,UAAU,OAAO;AAAA,EACzC,OAAO,SAAS;AAAA,IACd,OAAO,EAAE,QAAQ,iBAAiB,WAAW,GAAI;AAAA;AAAA;AAAA,IAGjD,SAAS,EAAE,QAAQ,oBAAmB;AAAA,EAC1C;AAAA,EAEEY,KAAc;AAAA,EAEd,IAAI,YAAY;AACd,UAAMC,IAAY,KAAK,KAAK,MAAM;AAElC,WAAOA,MAAc,KAAK,QAAWA;AAAA,EACvC;AAAA,EAEA,IAAI,cAAc;AAChB,WAAO,KAAK,KAAK,MAAM,MAAM;AAAA,EAC/B;AAAA,EAEA,IAAI,sBAAsB;AACxB,WAAO,KAAK,YAAY,KAAK;AAAA,EAC/B;AAAA,EAEA,gBAAgB;AACd,IAAK,OAAO,SAAS,KAAK,SAAS,KAC/B,KAAK,KAAK,WAAW,SAEzB,KAAK,KAAK,OAAO,YAAY,KAAK,QAAQ,yBACvC,QAAQ,eAAe,KAAK,WAAW,EACvC,QAAQ,aAAa,KAAK,SAAS,EACnC,QAAQ,iBAAiB,KAAK,mBAAmB,GAEhD,KAAKD,MAAe,KAAK,sBAAsB,MACjD,KAAK,KAAK,MAAM,YAAY;AAAA,EAEhC;AAAA,EAEA,kBAAkBE,GAAO;AACvB,IAAI,KAAK,sBAAsB,KAC3B,KAAK,QAAQ,iCAAiC,WAE9CC,EAAqBD,EAAM,GAAG,KAAKE,EAAaF,CAAK,OACnD,KAAK,KAAK,MAAM,WAAQ,KAAK,KAAK,MAAM,SAAS,KACrD,KAAK,KAAK,MAAM,YAAY,KAAK,QAAQ,8BACzC,KAAKF,KAAc;AAAA,EAEvB;AACF;AAEA,SAASG,EAAqBE,GAAK;AACjC,SAAOA,EAAI,WAAW,KAAKA,EAAI,MAAM,MAAM;AAC7C;AAEA,MAAMC,IAAe;AAAA,EACnB;AAAA;AAAA,EACA;AAAA;AACF;AAGA,SAASF,EAAaF,GAAO;AAC3B,SAAOI,EAAa,SAASJ,EAAM,GAAG,KAAKA,EAAM,WAAW;AAC9D;AC7De,MAAMK,UAA6BnB,EAAiB;AAAA,EACjE,OAAO,UAAU;AAAA,EACjB,OAAO,OAAO,CAAC,MAAM;AAAA,EACrB,OAAO,SAAS;AAAA,IACd,OAAO;AAAA,EACX;AAAA,EAEE,YAAYc,GAAO;AAEjB,QACEA,EAAM,OAAO;AAAA,MACX,GAAG,KAAK,YAAY,OAAO;AAAA,IACnC;AAEM;AAKF,IADuB,CAAC,OAAO,aAAY,EAAG,SAAQ,KAClC,KAAK,KAAK,KAAK,MAAK;AAAA,EAC1C;AACF;AChCO,SAASM,EAASC,GAAUC,GAAM;AACvC,MAAIC,GAASC,GAASC,GAClBC,IAAW;AAEf,WAASC,IAAM;AACb,IAAAN,EAAS,MAAMG,GAASC,CAAI,GAC5BA,IAAO,MACPD,IAAU;AAAA,EACZ;AAEA,WAASI,IAAQ;AACf,UAAMC,IAAQ,KAAK,IAAG,IAAKH;AAE3B,IAAIG,IAAQP,KAAQO,KAAS,IAC3BN,IAAU,WAAWK,GAAON,IAAOO,CAAK,KAExC,aAAaN,CAAO,GACpBA,IAAU,MACVI,EAAG;AAAA,EAEP;AAEA,QAAMG,IAAY,MAAM;AACtB,IAAAN,IAAU,MACVC,IAAO,WACPC,IAAW,KAAK,IAAG,GAEdH,MACHA,IAAU,WAAWK,GAAON,CAAI;AAAA,EAEpC;AAEA,SAAAQ,EAAU,UAAU,MAAM;AACxB,IAAIP,MACF,aAAaA,CAAO,GACpBA,IAAU,OAEZI,EAAG;AAAA,EACL,GAEOG;AACT;AC9Be,MAAMC,UAA8B/B,EAAiB;AAAA,EAClE,OAAO,UAAU;AAAA,EACjB,OAAO,OAAO,CAAC,QAAQ;AAAA,EACvB,OAAO,SAAS;AAAA,IACd,QAAQ;AAAA,IACR,OAAO;AAAA,EACX;AAAA,EAEE,kBAAkBoB,EAAS,KAAKY,GAAQ,KAAK,IAAI,GAAG,GAAG;AAAA,EAEvD,SAAS;AACP,SAAK,gBAAgB,QAAO;AAAA,EAC9B;AAAA,EAEAA,KAAU;AACR,SAAK,KAAK,OAAO,MAAK;AAAA,EACxB;AACF;"}
|
package/dist/index.umd.cjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
(function(
|
|
1
|
+
(function(r,s){typeof exports=="object"&&typeof module<"u"?s(exports):typeof define=="function"&&define.amd?define(["exports"],s):(r=typeof globalThis<"u"?globalThis:r||self,s(r["sprinkles-js"]={}))})(this,function(r){"use strict";class s extends HTMLElement{static tagName="sprinkles-element";static refs=[];static events={};static register(t=this.tagName,e){"customElements"in window&&(e||customElements).define(t,this)}refs={};connectedCallback(){this.#e(),this.#i(),this.afterConnected()}disconnectedCallback(){this.beforeDisconnect(),this.#n()}afterConnected(){}beforeDisconnect(){}#e(){this.refs={},this.constructor.refs.forEach(t=>{this.refs[t]=this.querySelector(`[data-${this.constructor.tagName}-ref=${t}]`)})}#i(){Object.entries(this.constructor.events).forEach(([t,e])=>{this.#t(e).forEach(i=>{i.element&&(i.element.addEventListener(t,i.callback,i.options),i.immediate&&i.callback.call())})})}#n(){Object.entries(this.constructor.events).forEach(([t,e])=>{this.#t(e).forEach(i=>{i.element&&i.element.removeEventListener(t,i.callback,i.options)})})}#t(t){return t=Array.isArray(t)?t:[t],t.map(e=>(e=typeof e=="string"?{method:e}:{...e},e.element||=e.ref?this.refs[e.ref]:this,e.immediate||=!1,e.callback=this[e.method].bind(this),e))}}class d extends s{static tagName="character-counter";static refs=["input","output","error"];static events={input:{method:"updateCounter",immediate:!0},keydown:{method:"alertLimitReached"}};#e=!1;get maxLength(){const t=this.refs.input.maxLength;return t===-1?1/0:t}get valueLength(){return this.refs.input.value.length}get remainingCharacters(){return this.maxLength-this.valueLength}updateCounter(){Number.isFinite(this.maxLength)&&this.refs.output!==null&&(this.refs.output.innerText=this.dataset.characterCounterTemplate.replace("{{current}}",this.valueLength).replace("{{total}}",this.maxLength).replace("{{remaining}}",this.remainingCharacters),this.#e&&this.remainingCharacters>0&&(this.refs.error.innerText=null))}alertLimitReached(t){this.remainingCharacters>0||this.dataset.characterCounterLimitMessage!==void 0&&(f(t.key)||p(t))&&(this.refs.error.hidden&&(this.refs.error.hidden=!1),this.refs.error.innerText=this.dataset.characterCounterLimitMessage,this.#e=!0)}}function f(n){return n.length===1&&n.match(/\S| /)}const b=["Process","Unidentified"];function p(n){return b.includes(n.key)||n.keyCode==229}class C extends s{static tagName="clickable-card";static refs=["link"];static events={click:"handleClick"};handleClick(t){if(t.target.closest(`${this.constructor.tagName} :is(a,button,summary,input,select)`))return;!window.getSelection().toString()&&this.refs.link.click()}}function l(n,t){let e,i,a,o=0;function u(){n.apply(i,a),a=null,i=null}function h(){const c=Date.now()-o;c<t&&c>=0?e=setTimeout(h,t-c):(clearTimeout(e),e=null,u())}const m=()=>{i=this,a=arguments,o=Date.now(),e||(e=setTimeout(h,t))};return m.trigger=()=>{e&&(clearTimeout(e),e=null),u()},m}class k extends s{static tagName="form-autosubmit";static refs=["button"];static events={change:"submit",input:"debouncedSubmit"};debouncedSubmit=l(this.#e.bind(this),250);submit(){this.debouncedSubmit.trigger()}#e(){this.refs.button.click()}}r.CharacterCounterElement=d,r.ClickableCardElement=C,r.FormAutosubmitElement=k,r.SprinklesElement=s,r.debounce=l,Object.defineProperty(r,Symbol.toStringTag,{value:"Module"})});
|
|
2
2
|
//# sourceMappingURL=index.umd.cjs.map
|
package/dist/index.umd.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.umd.cjs","sources":["../src/elements/sprinkles_element.js","../src/elements/clickable_card_element.js"],"sourcesContent":["/**\n * SprinklesElement is an abstract class to help us build custom elements\n *\n * By defining refs and events the element will automatically run some setup when connected.\n */\nexport default class SprinklesElement extends HTMLElement {\n /**\n * The default tagName that will be used to register this elements\n */\n static tagName = \"sprinkles-element\";\n\n /**\n * An array of elements that will be referenced in the elements\n *\n * These refs are expected to match DOM-nodes following the pattern\n * `[data-TAG-NAME-ref=\"refName\"]`\n *\n * @type {string[]}\n */\n static refs = [];\n\n /**\n * An object with events that should be observed.\n *\n * For each event a listener will be added when the elements connects (and removed when it disconnects)\n *\n * @type {{ [eventName: string]: string | { method: string; ref?: string, options?: boolean | AddEventListenerOptions; immediate?: boolean } | string[] | { method: string; ref?: string, options?: boolean | AddEventListenerOptions; immediate?: boolean }[] }}\n */\n static events = {};\n\n static register(tagName = this.tagName, registry) {\n if (\"customElements\" in window) {\n (registry || customElements).define(tagName, this);\n }\n }\n\n /**\n * The found refs, based on the class field `refs`\n *\n * @type { [name: string]: Element | null}\n */\n refs = {};\n\n connectedCallback() {\n this.#detectRefs();\n this.#bindEvents();\n this.afterConnected();\n }\n\n disconnectedCallback() {\n this.beforeDisconnect();\n this.#unbindEvents();\n }\n\n // Callbacks\n afterConnected() {}\n\n beforeDisconnect() {}\n\n // Setup\n #detectRefs() {\n this.refs = {};\n this.constructor.refs.forEach((refName) => {\n this.refs[refName] = this.querySelector(\n `[data-${this.constructor.tagName}-ref=${refName}]`,\n );\n });\n }\n\n #bindEvents() {\n Object.entries(this.constructor.events).forEach(([eventName, settings]) => {\n this.#normalizeEventSettings(settings).forEach((setting) => {\n // If we don't have an element, we simply return (this can happen when a ref could not be found)\n if (!setting.element) return;\n\n setting.element.addEventListener(\n eventName,\n setting.callback,\n setting.options,\n );\n if (setting.immediate) setting.callback.call();\n });\n });\n }\n\n #unbindEvents() {\n Object.entries(this.constructor.events).forEach(([eventName, settings]) => {\n this.#normalizeEventSettings(settings).forEach((setting) => {\n if (!setting.element) return;\n\n setting.element.removeEventListener(\n eventName,\n setting.callback,\n setting.options,\n );\n });\n });\n }\n\n #normalizeEventSettings(settings) {\n // Settings can be passed as an object or an array of objects, we always normalize this to an array\n settings = Array.isArray(settings) ? settings : [settings];\n return settings.map((setting) => {\n // We always clone settings, so we don't modify the value that is stored as a class field\n // Otherwise setting `element` will mess with other instances of the same element\n setting =\n typeof setting === \"string\" ? { method: setting } : { ...setting };\n setting.element ||= setting[\"ref\"] ? this.refs[setting[\"ref\"]] : this;\n setting.immediate ||= false;\n setting.callback = this[setting.method].bind(this);\n return setting;\n });\n }\n}\n","import SprinklesElement from \"./sprinkles_element\";\n\n/**\n * A card that can be clicked anywhere to follow a link inside it\n *\n * This ensures that a screen reader does not to read the whole card to announce the link, while a user can still click anywhere in the card.\n * You can include other links and buttons in the card, these will not trigger a navigation.\n *\n * This is based on the recipe and discussion from Web Accessibility Cookbook by Manuel Matuzović (O’Reilly). Copyright 2024 Manuel Matuzović, 978-1-098-14560-6.\n * and this article from Vikas Parashar: https://css-tricks.com/block-links-the-search-for-a-perfect-solution/#aa-method-4-sprinkle-javascript-on-the-second-method\n */\nexport default class ClickableCardElement extends SprinklesElement {\n static tagName = \"clickable-card\";\n static refs = [\"link\"];\n static events = {\n click: \"handleClick\",\n };\n\n handleClick(event) {\n // Don't handle clicks on buttons or anchors\n if (event.target.closest(`${this.constructor.tagName} :is(a,button)`)) {\n return;\n }\n\n // Check if text is selected and don't click if so\n const noTextSelected = !window.getSelection().toString();\n if (noTextSelected) this.refs.link.click();\n }\n}\n"],"names":["SprinklesElement","__privateAdd","_SprinklesElement_instances","__publicField","tagName","registry","__privateMethod","detectRefs_fn","bindEvents_fn","unbindEvents_fn","refName","eventName","settings","normalizeEventSettings_fn","setting","ClickableCardElement","event"],"mappings":"qoBAKe,MAAMA,UAAyB,WAAY,CAA3C,kCAAAC,EAAA,KAAAC,GAoCbC,EAAA,YAAO,CAAE,GAXT,OAAO,SAASC,EAAU,KAAK,QAASC,EAAU,CAC5C,mBAAoB,SACrBA,GAAY,gBAAgB,OAAOD,EAAS,IAAI,CAEvD,CASE,mBAAoB,CAClBE,EAAA,KAAKJ,EAAAK,GAAL,WACAD,EAAA,KAAKJ,EAAAM,GAAL,WACA,KAAK,eAAgB,CACzB,CAEE,sBAAuB,CACrB,KAAK,iBAAkB,EACvBF,EAAA,KAAKJ,EAAAO,GAAL,UACJ,CAGE,gBAAiB,CAAA,CAEjB,kBAAmB,CAAA,CAwDrB,CA5GeP,EAAA,YAuDbK,EAAW,UAAG,CACZ,KAAK,KAAO,CAAE,EACd,KAAK,YAAY,KAAK,QAASG,GAAY,CACzC,KAAK,KAAKA,CAAO,EAAI,KAAK,cACxB,SAAS,KAAK,YAAY,OAAO,QAAQA,CAAO,GACjD,CACP,CAAK,CACL,EAEEF,EAAW,UAAG,CACZ,OAAO,QAAQ,KAAK,YAAY,MAAM,EAAE,QAAQ,CAAC,CAACG,EAAWC,CAAQ,IAAM,CACzEN,EAAA,KAAKJ,EAAAW,GAAL,UAA6BD,GAAU,QAASE,GAAY,CAErDA,EAAQ,UAEbA,EAAQ,QAAQ,iBACdH,EACAG,EAAQ,SACRA,EAAQ,OACT,EACGA,EAAQ,WAAWA,EAAQ,SAAS,KAAM,EACtD,CAAO,CACP,CAAK,CACL,EAEEL,EAAa,UAAG,CACd,OAAO,QAAQ,KAAK,YAAY,MAAM,EAAE,QAAQ,CAAC,CAACE,EAAWC,CAAQ,IAAM,CACzEN,EAAA,KAAKJ,EAAAW,GAAL,UAA6BD,GAAU,QAASE,GAAY,CACrDA,EAAQ,SAEbA,EAAQ,QAAQ,oBACdH,EACAG,EAAQ,SACRA,EAAQ,OACT,CACT,CAAO,CACP,CAAK,CACL,EAEED,EAAuB,SAACD,EAAU,CAEhC,OAAAA,EAAW,MAAM,QAAQA,CAAQ,EAAIA,EAAW,CAACA,CAAQ,EAClDA,EAAS,IAAKE,IAGnBA,EACE,OAAOA,GAAY,SAAW,CAAE,OAAQA,CAAS,EAAG,CAAE,GAAGA,CAAS,EACpEA,EAAQ,UAARA,EAAQ,QAAYA,EAAQ,IAAS,KAAK,KAAKA,EAAQ,GAAM,EAAI,MACjEA,EAAQ,YAARA,EAAQ,UAAc,IACtBA,EAAQ,SAAW,KAAKA,EAAQ,MAAM,EAAE,KAAK,IAAI,EAC1CA,EACR,CACL,EAvGEX,EAJmBH,EAIZ,UAAU,qBAUjBG,EAdmBH,EAcZ,OAAO,CAAE,GAShBG,EAvBmBH,EAuBZ,SAAS,CAAE,GCjBL,MAAMe,UAA6Bf,CAAiB,CAOjE,YAAYgB,EAAO,CAEjB,GAAIA,EAAM,OAAO,QAAQ,GAAG,KAAK,YAAY,OAAO,gBAAgB,EAClE,OAIqB,CAAC,OAAO,aAAY,EAAG,SAAU,GACpC,KAAK,KAAK,KAAK,MAAO,CAC9C,CACA,CAhBEb,EADmBY,EACZ,UAAU,kBACjBZ,EAFmBY,EAEZ,OAAO,CAAC,MAAM,GACrBZ,EAHmBY,EAGZ,SAAS,CACd,MAAO,aACR"}
|
|
1
|
+
{"version":3,"file":"index.umd.cjs","sources":["../src/elements/sprinkles_element.js","../src/elements/character_counter_element.js","../src/elements/clickable_card_element.js","../src/lib/debounce.js","../src/elements/form_autosubmit_element.js"],"sourcesContent":["/**\n * SprinklesElement is an abstract class to help us build custom elements\n *\n * By defining refs and events the element will automatically run some setup when connected.\n */\nexport default class SprinklesElement extends HTMLElement {\n /**\n * The default tagName that will be used to register this elements\n */\n static tagName = \"sprinkles-element\";\n\n /**\n * An array of elements that will be referenced in the elements\n *\n * These refs are expected to match DOM-nodes following the pattern\n * `[data-TAG-NAME-ref=\"refName\"]`\n *\n * @type {string[]}\n */\n static refs = [];\n\n /**\n * An object with events that should be observed.\n *\n * For each event a listener will be added when the elements connects (and removed when it disconnects)\n *\n * @type {{ [eventName: string]: string | { method: string; ref?: string, options?: boolean | AddEventListenerOptions; immediate?: boolean } | string[] | { method: string; ref?: string, options?: boolean | AddEventListenerOptions; immediate?: boolean }[] }}\n */\n static events = {};\n\n static register(tagName = this.tagName, registry) {\n if (\"customElements\" in window) {\n (registry || customElements).define(tagName, this);\n }\n }\n\n /**\n * The found refs, based on the class field `refs`\n *\n * @type { [name: string]: Element | null}\n */\n refs = {};\n\n connectedCallback() {\n this.#detectRefs();\n this.#bindEvents();\n this.afterConnected();\n }\n\n disconnectedCallback() {\n this.beforeDisconnect();\n this.#unbindEvents();\n }\n\n // Callbacks\n afterConnected() {}\n\n beforeDisconnect() {}\n\n // Setup\n #detectRefs() {\n this.refs = {};\n this.constructor.refs.forEach((refName) => {\n this.refs[refName] = this.querySelector(\n `[data-${this.constructor.tagName}-ref=${refName}]`,\n );\n });\n }\n\n #bindEvents() {\n Object.entries(this.constructor.events).forEach(([eventName, settings]) => {\n this.#normalizeEventSettings(settings).forEach((setting) => {\n // If we don't have an element, we simply return (this can happen when a ref could not be found)\n if (!setting.element) return;\n\n setting.element.addEventListener(\n eventName,\n setting.callback,\n setting.options,\n );\n if (setting.immediate) setting.callback.call();\n });\n });\n }\n\n #unbindEvents() {\n Object.entries(this.constructor.events).forEach(([eventName, settings]) => {\n this.#normalizeEventSettings(settings).forEach((setting) => {\n if (!setting.element) return;\n\n setting.element.removeEventListener(\n eventName,\n setting.callback,\n setting.options,\n );\n });\n });\n }\n\n #normalizeEventSettings(settings) {\n // Settings can be passed as an object or an array of objects, we always normalize this to an array\n settings = Array.isArray(settings) ? settings : [settings];\n return settings.map((setting) => {\n // We always clone settings, so we don't modify the value that is stored as a class field\n // Otherwise setting `element` will mess with other instances of the same element\n setting =\n typeof setting === \"string\" ? { method: setting } : { ...setting };\n setting.element ||= setting[\"ref\"] ? this.refs[setting[\"ref\"]] : this;\n setting.immediate ||= false;\n setting.callback = this[setting.method].bind(this);\n return setting;\n });\n }\n}\n","import SprinklesElement from \"./sprinkles_element\";\n\n/**\n * An element that wraps an input and an element to print the character counter\n *\n * On every keystroke the counter is updated. If the max length of the input is reached\n * an error can be added to alert the user.\n */\nexport default class CharacterCounterElement extends SprinklesElement {\n static tagName = \"character-counter\";\n static refs = [\"input\", \"output\", \"error\"];\n static events = {\n input: { method: \"updateCounter\", immediate: true },\n // We detect that the limit is reached through `keydown`, since input isn't triggered (since the user can't input any more)\n // and `keyup` fires after the input event and would be triggered on the Nth character instead of the N + 1 character\n keydown: { method: \"alertLimitReached\" },\n };\n\n #hasAlerted = false;\n\n get maxLength() {\n const maxLength = this.refs.input.maxLength;\n // If maxLength isn't set (and returns -1), we set this to infinity\n return maxLength === -1 ? Infinity : maxLength;\n }\n\n get valueLength() {\n return this.refs.input.value.length;\n }\n\n get remainingCharacters() {\n return this.maxLength - this.valueLength;\n }\n\n updateCounter() {\n if (!Number.isFinite(this.maxLength)) return;\n if (this.refs.output === null) return;\n\n this.refs.output.innerText = this.dataset.characterCounterTemplate\n .replace(\"{{current}}\", this.valueLength)\n .replace(\"{{total}}\", this.maxLength)\n .replace(\"{{remaining}}\", this.remainingCharacters);\n\n if (this.#hasAlerted && this.remainingCharacters > 0) {\n this.refs.error.innerText = null;\n }\n }\n\n alertLimitReached(event) {\n if (this.remainingCharacters > 0) return;\n if (this.dataset.characterCounterLimitMessage === undefined) return;\n\n if (isPrintableCharacter(event.key) || isAndroidKey(event)) {\n if (this.refs.error.hidden) this.refs.error.hidden = false;\n this.refs.error.innerText = this.dataset.characterCounterLimitMessage;\n this.#hasAlerted = true;\n }\n }\n}\n\nfunction isPrintableCharacter(str) {\n return str.length === 1 && str.match(/\\S| /);\n}\n\nconst ANDROID_KEYS = [\n \"Process\", // Firefox\n \"Unidentified\", // Chrome (but only 127+?)\n];\n\n// NOTE: Certain android devices always send an event with a special key and/or keyCode `229`\nfunction isAndroidKey(event) {\n return ANDROID_KEYS.includes(event.key) || event.keyCode == 229;\n}\n","import SprinklesElement from \"./sprinkles_element\";\n\n/**\n * A card that can be clicked anywhere to follow a link inside it\n *\n * This ensures that a screen reader does not to read the whole card to announce the link, while a user can still click anywhere in the card.\n * You can include other interactive elements in the card, these will not trigger a navigation.\n *\n * This is based on the recipe and discussion from Web Accessibility Cookbook by Manuel Matuzović (O’Reilly). Copyright 2024 Manuel Matuzović, 978-1-098-14560-6.\n * and this article from Vikas Parashar: https://css-tricks.com/block-links-the-search-for-a-perfect-solution/#aa-method-4-sprinkle-javascript-on-the-second-method\n */\nexport default class ClickableCardElement extends SprinklesElement {\n static tagName = \"clickable-card\";\n static refs = [\"link\"];\n static events = {\n click: \"handleClick\",\n };\n\n handleClick(event) {\n // Don't handle clicks on other interactive elements\n if (\n event.target.closest(\n `${this.constructor.tagName} :is(a,button,summary,input,select)`,\n )\n ) {\n return;\n }\n\n // Check if text is selected and don't click if so\n const noTextSelected = !window.getSelection().toString();\n if (noTextSelected) this.refs.link.click();\n }\n}\n","export function debounce(callback, wait) {\n let timeout, context, args;\n let lastCall = 0;\n\n function run() {\n callback.apply(context, args);\n args = null;\n context = null;\n }\n\n function later() {\n const delta = Date.now() - lastCall;\n\n if (delta < wait && delta >= 0) {\n timeout = setTimeout(later, wait - delta);\n } else {\n clearTimeout(timeout);\n timeout = null;\n run();\n }\n }\n\n const debounced = () => {\n context = this;\n args = arguments;\n lastCall = Date.now();\n\n if (!timeout) {\n timeout = setTimeout(later, wait);\n }\n };\n\n debounced.trigger = () => {\n if (timeout) {\n clearTimeout(timeout);\n timeout = null;\n }\n run();\n };\n\n return debounced;\n}\n","import SprinklesElement from \"./sprinkles_element\";\nimport { debounce } from \"../lib/debounce\";\n\n/**\n * This element submits a form automatically clicking on a hidden submit input/button\n *\n * The form gets submitted on each change, or on input with a debounce.\n * You still need to make sure that this is what the user expects and/or that the submit is captured\n * and only part of the DOM get's updated using a technique like turbo frames.\n * (We don't use `form.submit()` since that event isn't captured by turbo)\n */\nexport default class FormAutosubmitElement extends SprinklesElement {\n static tagName = \"form-autosubmit\";\n static refs = [\"button\"];\n static events = {\n change: \"submit\",\n input: \"debouncedSubmit\",\n };\n\n debouncedSubmit = debounce(this.#submit.bind(this), 250);\n\n submit() {\n this.debouncedSubmit.trigger();\n }\n\n #submit() {\n this.refs.button.click();\n }\n}\n"],"names":["SprinklesElement","tagName","registry","#detectRefs","#bindEvents","#unbindEvents","refName","eventName","settings","#normalizeEventSettings","setting","CharacterCounterElement","#hasAlerted","maxLength","event","isPrintableCharacter","isAndroidKey","str","ANDROID_KEYS","ClickableCardElement","debounce","callback","wait","timeout","context","args","lastCall","run","later","delta","debounced","FormAutosubmitElement","#submit"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sprinkles-js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"main": "index.js",
|
|
5
5
|
"repository": "https://github.com/tree-company/sprinkles-js",
|
|
6
6
|
"author": "Tree company <development@treecompany.be>",
|
|
@@ -27,12 +27,12 @@
|
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@tree-company/eslint-config": "^0.
|
|
31
|
-
"@tree-company/stylelint-config": "^0.
|
|
32
|
-
"eslint": "^9.
|
|
33
|
-
"playwright": "^1.
|
|
34
|
-
"prettier": "^3.
|
|
35
|
-
"stylelint": "^16.
|
|
36
|
-
"vite": "^
|
|
30
|
+
"@tree-company/eslint-config": "^0.5.0",
|
|
31
|
+
"@tree-company/stylelint-config": "^0.5.0",
|
|
32
|
+
"eslint": "^9.32.0",
|
|
33
|
+
"playwright": "^1.55.1",
|
|
34
|
+
"prettier": "^3.6.2",
|
|
35
|
+
"stylelint": "^16.24.0",
|
|
36
|
+
"vite": "^7.1.3"
|
|
37
37
|
}
|
|
38
38
|
}
|