@ktfth/stickjs 3.0.0 → 3.0.2
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/bin/registry.json +15 -15
- package/bin/stickjs.js +42 -4
- package/package.json +14 -3
- package/stick-ui/stick-ui.css +276 -41
- package/stick-ui/templates/accordion.html +49 -0
- package/stick-ui/templates/autocomplete.html +63 -0
- package/stick-ui/templates/command-palette.html +38 -0
- package/stick-ui/templates/copy-button.html +42 -0
- package/stick-ui/templates/data-table.html +84 -0
- package/stick-ui/templates/dialog.html +65 -0
- package/stick-ui/templates/dropdown.html +49 -0
- package/stick-ui/templates/notification.html +46 -0
- package/stick-ui/templates/skeleton.html +38 -0
- package/stick-ui/templates/stepper.html +95 -0
- package/stick-ui/templates/tabs.html +53 -0
- package/stick-ui/templates/toast.html +35 -0
- package/stick-ui/templates/toggle-group.html +42 -0
- package/stick-ui/templates/toggle.html +36 -0
- package/stick-ui/templates/tooltip.html +55 -0
- package/stick.d.ts +1 -1
- package/stick.js +169 -16
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Tooltip — Stick.js Template</title>
|
|
7
|
+
<link rel="stylesheet" href="https://unpkg.com/@ktfth/stickjs/stick-ui/stick-ui.css">
|
|
8
|
+
<script src="https://unpkg.com/@ktfth/stickjs/stick.js"></script>
|
|
9
|
+
<script src="https://unpkg.com/@ktfth/stickjs/stick-ui/plugins/tooltip.js"></script>
|
|
10
|
+
</head>
|
|
11
|
+
<body style="padding: 4rem 2rem; max-width: 600px; margin: 0 auto;">
|
|
12
|
+
|
|
13
|
+
<h1>Tooltip</h1>
|
|
14
|
+
<p>Hover or focus buttons to see tooltips in different positions.</p>
|
|
15
|
+
|
|
16
|
+
<div style="display: flex; gap: 1.5rem; flex-wrap: wrap; margin-top: 2rem;">
|
|
17
|
+
<button class="stk-btn"
|
|
18
|
+
data-stick="mouseenter:tooltip:Tooltip on top"
|
|
19
|
+
data-stick-2="mouseleave:tooltip-hide:"
|
|
20
|
+
data-stick-3="focus:tooltip:Tooltip on top"
|
|
21
|
+
data-stick-4="blur:tooltip-hide:"
|
|
22
|
+
data-stk-tooltip-pos="top">
|
|
23
|
+
Top
|
|
24
|
+
</button>
|
|
25
|
+
|
|
26
|
+
<button class="stk-btn"
|
|
27
|
+
data-stick="mouseenter:tooltip:Tooltip on bottom"
|
|
28
|
+
data-stick-2="mouseleave:tooltip-hide:"
|
|
29
|
+
data-stick-3="focus:tooltip:Tooltip on bottom"
|
|
30
|
+
data-stick-4="blur:tooltip-hide:"
|
|
31
|
+
data-stk-tooltip-pos="bottom">
|
|
32
|
+
Bottom
|
|
33
|
+
</button>
|
|
34
|
+
|
|
35
|
+
<button class="stk-btn"
|
|
36
|
+
data-stick="mouseenter:tooltip:Tooltip on left"
|
|
37
|
+
data-stick-2="mouseleave:tooltip-hide:"
|
|
38
|
+
data-stick-3="focus:tooltip:Tooltip on left"
|
|
39
|
+
data-stick-4="blur:tooltip-hide:"
|
|
40
|
+
data-stk-tooltip-pos="left">
|
|
41
|
+
Left
|
|
42
|
+
</button>
|
|
43
|
+
|
|
44
|
+
<button class="stk-btn"
|
|
45
|
+
data-stick="mouseenter:tooltip:Tooltip on right"
|
|
46
|
+
data-stick-2="mouseleave:tooltip-hide:"
|
|
47
|
+
data-stick-3="focus:tooltip:Tooltip on right"
|
|
48
|
+
data-stick-4="blur:tooltip-hide:"
|
|
49
|
+
data-stk-tooltip-pos="right">
|
|
50
|
+
Right
|
|
51
|
+
</button>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
</body>
|
|
55
|
+
</html>
|
package/stick.d.ts
CHANGED
package/stick.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*!
|
|
2
2
|
* Stick.js — declarative behavior for HTML elements
|
|
3
|
-
* https://github.com/
|
|
3
|
+
* https://github.com/ktfth/Stickjs
|
|
4
4
|
*
|
|
5
5
|
* Syntax:
|
|
6
6
|
* data-stick="event:handler:param"
|
|
@@ -22,6 +22,13 @@
|
|
|
22
22
|
* data-stick-loading="Loading…" button label while fetch is in-flight
|
|
23
23
|
* data-stick-headers='{"Authorization":"Bearer token"}' fetch headers (JSON)
|
|
24
24
|
* data-stick-json='{"key":"{{value}}"}' send JSON body instead of FormData
|
|
25
|
+
* data-stick-map="#sel:Field, …" JSON field → DOM mapping for fetch-json handler
|
|
26
|
+
* data-stick-then="handler:tgt:p, …" post-fetch action chain (sequential)
|
|
27
|
+
* data-stick-accept="json" parse fetch response as JSON array
|
|
28
|
+
* data-stick-template="#tpl" <template> for JSON array rendering (with accept=json)
|
|
29
|
+
* data-stick-state-loading="…" state actions when fetch starts
|
|
30
|
+
* data-stick-state-done="…" state actions when fetch succeeds
|
|
31
|
+
* data-stick-state-error="…" state actions when fetch fails
|
|
25
32
|
*
|
|
26
33
|
* Target selectors (data-stick-target):
|
|
27
34
|
* "#id" querySelector (default)
|
|
@@ -45,6 +52,7 @@
|
|
|
45
52
|
* {{url:key}} URLSearchParams value from current page URL (e.g. {{url:q}} → ?q=value)
|
|
46
53
|
* {{data-*}} any data attribute, e.g. {{data-id}}
|
|
47
54
|
* {{attr}} any attribute, e.g. {{href}}, {{src}}
|
|
55
|
+
* {{#sel.prop}} cross-element: querySelector(sel).prop (e.g. {{#myInput.value}})
|
|
48
56
|
*
|
|
49
57
|
* Example: data-stick="input:fetch:/api/search?q={{value}}"
|
|
50
58
|
* data-stick="click:dispatch:item-selected:{{data-id}}"
|
|
@@ -80,7 +88,7 @@
|
|
|
80
88
|
}(typeof window !== 'undefined' ? window : this, function () {
|
|
81
89
|
'use strict';
|
|
82
90
|
|
|
83
|
-
const VERSION = '3.
|
|
91
|
+
const VERSION = '3.1.0';
|
|
84
92
|
const handlers = {};
|
|
85
93
|
const listenerMap = new WeakMap(); // el → [{event, fn, options}]
|
|
86
94
|
let _debug = false;
|
|
@@ -135,9 +143,44 @@
|
|
|
135
143
|
}
|
|
136
144
|
}
|
|
137
145
|
|
|
138
|
-
// Resolve {{
|
|
146
|
+
// Resolve {{key}} tokens from a plain object (for JSON template rendering)
|
|
147
|
+
function interpolateFromObj(text, obj) {
|
|
148
|
+
return text.replace(/\{\{([\w-]+)\}\}/g, (_, key) => obj[key] !== undefined ? String(obj[key]) : '');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Walk a cloned fragment and interpolate {{tokens}} from a plain object
|
|
152
|
+
function interpolateCloneFromObj(fragment, obj) {
|
|
153
|
+
var walk = document.createTreeWalker(fragment, NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT);
|
|
154
|
+
var node;
|
|
155
|
+
while ((node = walk.nextNode())) {
|
|
156
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
157
|
+
if (node.nodeValue.includes('{{')) node.nodeValue = interpolateFromObj(node.nodeValue, obj);
|
|
158
|
+
} else {
|
|
159
|
+
[...node.attributes].forEach(function(attr) {
|
|
160
|
+
if (attr.value.includes('{{')) attr.value = interpolateFromObj(attr.value, obj);
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Resolve {{token}} placeholders — supports cross-element {{#selector.prop}}
|
|
139
167
|
function interpolate(param, el) {
|
|
140
|
-
|
|
168
|
+
// Cross-element interpolation: {{#selector.prop}} resolves from another DOM element
|
|
169
|
+
param = param.replace(/\{\{(#[^}]+)\}\}/g, function(_, ref) {
|
|
170
|
+
var lastDot = ref.lastIndexOf('.');
|
|
171
|
+
if (lastDot <= 0) return '';
|
|
172
|
+
var sel = ref.slice(0, lastDot);
|
|
173
|
+
var prop = ref.slice(lastDot + 1);
|
|
174
|
+
var found = (typeof document !== 'undefined') ? document.querySelector(sel) : null;
|
|
175
|
+
if (!found) return '';
|
|
176
|
+
if (prop === 'value') return found.value ?? '';
|
|
177
|
+
if (prop === 'text' || prop === 'textContent') return found.textContent?.trim() ?? '';
|
|
178
|
+
if (prop === 'checked') return String(found.checked ?? false);
|
|
179
|
+
if (prop.startsWith('data-')) return found.dataset[toCamel(prop.slice(5))] ?? '';
|
|
180
|
+
return found[prop] !== undefined ? String(found[prop]) : (found.getAttribute(prop) ?? '');
|
|
181
|
+
});
|
|
182
|
+
// Local element interpolation
|
|
183
|
+
return param.replace(/\{\{([\w:-]+)\}\}/g, (_, key) => {
|
|
141
184
|
if (key === 'value') return el.value ?? '';
|
|
142
185
|
if (key === 'text') return el.textContent.trim();
|
|
143
186
|
if (key === 'id') return el.id ?? '';
|
|
@@ -224,6 +267,57 @@
|
|
|
224
267
|
});
|
|
225
268
|
}
|
|
226
269
|
|
|
270
|
+
// ── state & then helpers ────────────────────────────────────────
|
|
271
|
+
|
|
272
|
+
// Parse state action: "handler:param:#target" or "handler:#target"
|
|
273
|
+
function parseStateAction(str) {
|
|
274
|
+
str = str.trim();
|
|
275
|
+
if (!str) return null;
|
|
276
|
+
var parts = str.split(':');
|
|
277
|
+
if (parts.length < 2) return null;
|
|
278
|
+
return { handler: parts[0], targetSel: parts[parts.length - 1].trim(), param: parts.length > 2 ? parts.slice(1, -1).join(':').trim() : '' };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Apply state actions (Feature 5) — e.g. "add-class:loading:#container, disable:#btn"
|
|
282
|
+
function applyStateActions(actionsStr) {
|
|
283
|
+
if (!actionsStr) return;
|
|
284
|
+
actionsStr.split(',').forEach(function(raw) {
|
|
285
|
+
var parsed = parseStateAction(raw);
|
|
286
|
+
if (!parsed) return;
|
|
287
|
+
var target = document.querySelector(parsed.targetSel);
|
|
288
|
+
if (!target || typeof handlers[parsed.handler] !== 'function') return;
|
|
289
|
+
try { handlers[parsed.handler](target, parsed.param, { type: 'state' }, target); }
|
|
290
|
+
catch (err) { console.error('[Stick:state]', err); }
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Parse then action: "handler:target:param" or "handler:target"
|
|
295
|
+
function parseThenEntry(str) {
|
|
296
|
+
str = str.trim();
|
|
297
|
+
if (!str) return null;
|
|
298
|
+
var i1 = str.indexOf(':');
|
|
299
|
+
if (i1 === -1) return { handler: str, targetSel: null, param: '' };
|
|
300
|
+
var rest = str.slice(i1 + 1);
|
|
301
|
+
var i2 = rest.indexOf(':');
|
|
302
|
+
if (i2 === -1) return { handler: str.slice(0, i1), targetSel: rest.trim(), param: '' };
|
|
303
|
+
return { handler: str.slice(0, i1), targetSel: rest.slice(0, i2).trim(), param: rest.slice(i2 + 1).trim() };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Execute post-fetch then chain (Feature 2) — "handler:target:param, …"
|
|
307
|
+
async function executeThenChain(actionsStr, sourceEl) {
|
|
308
|
+
if (!actionsStr) return;
|
|
309
|
+
var actions = actionsStr.split(',');
|
|
310
|
+
for (var i = 0; i < actions.length; i++) {
|
|
311
|
+
var parsed = parseThenEntry(actions[i]);
|
|
312
|
+
if (!parsed) continue;
|
|
313
|
+
var target = parsed.targetSel ? document.querySelector(parsed.targetSel) : sourceEl;
|
|
314
|
+
if (!target) { console.warn('[Stick:then] target not found:', parsed.targetSel); continue; }
|
|
315
|
+
if (typeof handlers[parsed.handler] !== 'function') { console.warn('[Stick:then] unknown handler:', parsed.handler); continue; }
|
|
316
|
+
try { await handlers[parsed.handler](sourceEl, parsed.param, { type: 'then' }, target); }
|
|
317
|
+
catch (err) { console.error('[Stick:then] handler "' + parsed.handler + '" threw:', err); }
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
227
321
|
// ── core ──────────────────────────────────────────────────────────
|
|
228
322
|
|
|
229
323
|
const Stick = {
|
|
@@ -597,15 +691,17 @@
|
|
|
597
691
|
const val = localStorage.getItem(p);
|
|
598
692
|
if (val !== null) t.value !== undefined ? (t.value = val) : (t.textContent = val);
|
|
599
693
|
})
|
|
600
|
-
// Network
|
|
694
|
+
// Network — supports accept=json (F4), state lifecycle (F5), then chain (F2)
|
|
601
695
|
.add('fetch', async (el, param, e, target) => {
|
|
602
696
|
e?.preventDefault?.();
|
|
603
|
-
const method
|
|
604
|
-
const swapMode
|
|
605
|
-
const loading
|
|
606
|
-
const errorSel
|
|
607
|
-
const errorEl
|
|
608
|
-
const prev
|
|
697
|
+
const method = (el.dataset.stickMethod || 'GET').toUpperCase();
|
|
698
|
+
const swapMode = el.dataset.stickSwap || 'innerHTML';
|
|
699
|
+
const loading = el.dataset.stickLoading || '';
|
|
700
|
+
const errorSel = el.dataset.stickError;
|
|
701
|
+
const errorEl = errorSel ? document.querySelector(errorSel) : null;
|
|
702
|
+
const prev = el.textContent;
|
|
703
|
+
const acceptJson = el.dataset.stickAccept === 'json';
|
|
704
|
+
const templateSel = el.dataset.stickTemplate;
|
|
609
705
|
|
|
610
706
|
let headers = {};
|
|
611
707
|
try { headers = JSON.parse(el.dataset.stickHeaders || '{}'); } catch (_) {}
|
|
@@ -613,6 +709,7 @@
|
|
|
613
709
|
if (loading) el.textContent = loading;
|
|
614
710
|
el.setAttribute('aria-busy', 'true');
|
|
615
711
|
if (errorEl) errorEl.hidden = true;
|
|
712
|
+
applyStateActions(el.dataset.stickStateLoading);
|
|
616
713
|
|
|
617
714
|
try {
|
|
618
715
|
let body;
|
|
@@ -625,19 +722,75 @@
|
|
|
625
722
|
body = new FormData(el.closest('form') || el);
|
|
626
723
|
}
|
|
627
724
|
}
|
|
628
|
-
const res
|
|
725
|
+
const res = await fetch(param, { method, body, headers });
|
|
629
726
|
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
727
|
+
|
|
728
|
+
if (acceptJson) {
|
|
729
|
+
// Feature 4: JSON response → clone template per item into target
|
|
730
|
+
const json = await res.json();
|
|
731
|
+
const tpl = templateSel ? document.querySelector(templateSel) : null;
|
|
732
|
+
if (tpl && tpl.tagName === 'TEMPLATE') {
|
|
733
|
+
target.innerHTML = '';
|
|
734
|
+
var items = Array.isArray(json) ? json : [json];
|
|
735
|
+
items.forEach(function(item) {
|
|
736
|
+
var clone = tpl.content.cloneNode(true);
|
|
737
|
+
interpolateCloneFromObj(clone, item);
|
|
738
|
+
target.appendChild(clone);
|
|
739
|
+
});
|
|
740
|
+
Stick.init(target);
|
|
741
|
+
} else {
|
|
742
|
+
console.warn('[Stick:fetch] accept=json requires data-stick-template pointing to a <template>');
|
|
743
|
+
}
|
|
744
|
+
} else {
|
|
745
|
+
const text = await res.text();
|
|
746
|
+
swap(target, text, swapMode);
|
|
747
|
+
Stick.init(target);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
applyStateActions(el.dataset.stickStateDone);
|
|
751
|
+
await executeThenChain(el.dataset.stickThen, el);
|
|
633
752
|
} catch (err) {
|
|
634
753
|
console.error('[Stick:fetch]', err);
|
|
754
|
+
applyStateActions(el.dataset.stickStateError);
|
|
635
755
|
if (errorEl) { errorEl.textContent = err.message; errorEl.hidden = false; }
|
|
636
|
-
else target.textContent = `Error: ${err.message}`;
|
|
756
|
+
else if (!el.dataset.stickStateError) target.textContent = `Error: ${err.message}`;
|
|
637
757
|
} finally {
|
|
638
758
|
if (loading) el.textContent = prev;
|
|
639
759
|
el.removeAttribute('aria-busy');
|
|
640
760
|
}
|
|
761
|
+
})
|
|
762
|
+
// Feature 1: fetch-json — GET URL, map JSON fields to DOM elements via data-stick-map
|
|
763
|
+
.add('fetch-json', async (el, param, e, target) => {
|
|
764
|
+
var mapStr = el.dataset.stickMap;
|
|
765
|
+
el.setAttribute('aria-busy', 'true');
|
|
766
|
+
applyStateActions(el.dataset.stickStateLoading);
|
|
767
|
+
|
|
768
|
+
try {
|
|
769
|
+
var res = await fetch(param, { method: 'GET' });
|
|
770
|
+
if (!res.ok) throw new Error(res.status + ' ' + res.statusText);
|
|
771
|
+
var json = await res.json();
|
|
772
|
+
|
|
773
|
+
if (mapStr) {
|
|
774
|
+
mapStr.split(',').forEach(function(entry) {
|
|
775
|
+
entry = entry.trim();
|
|
776
|
+
if (!entry) return;
|
|
777
|
+
var idx = entry.indexOf(':');
|
|
778
|
+
if (idx === -1) return;
|
|
779
|
+
var sel = entry.slice(0, idx).trim();
|
|
780
|
+
var field = entry.slice(idx + 1).trim();
|
|
781
|
+
var found = document.querySelector(sel);
|
|
782
|
+
if (found && json[field] !== undefined) found.textContent = String(json[field]);
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
applyStateActions(el.dataset.stickStateDone);
|
|
787
|
+
await executeThenChain(el.dataset.stickThen, el);
|
|
788
|
+
} catch (err) {
|
|
789
|
+
console.error('[Stick:fetch-json]', err);
|
|
790
|
+
applyStateActions(el.dataset.stickStateError);
|
|
791
|
+
} finally {
|
|
792
|
+
el.removeAttribute('aria-busy');
|
|
793
|
+
}
|
|
641
794
|
});
|
|
642
795
|
|
|
643
796
|
// ── auto-init ─────────────────────────────────────────────────────
|