@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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Stick.js — declarative behavior for HTML elements
3
- * https://github.com/kaiquekandykoga/Stickjs
3
+ * https://github.com/ktfth/Stickjs
4
4
  */
5
5
 
6
6
  /**
package/stick.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /*!
2
2
  * Stick.js — declarative behavior for HTML elements
3
- * https://github.com/kaiquekandykoga/Stickjs
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.0.0';
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 {{token}} placeholders using properties of el
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
- return param.replace(/\{\{([\w-]+)\}\}/g, (_, key) => {
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 = (el.dataset.stickMethod || 'GET').toUpperCase();
604
- const swapMode = el.dataset.stickSwap || 'innerHTML';
605
- const loading = el.dataset.stickLoading || '';
606
- const errorSel = el.dataset.stickError;
607
- const errorEl = errorSel ? document.querySelector(errorSel) : null;
608
- const prev = el.textContent;
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 = await fetch(param, { method, body, headers });
725
+ const res = await fetch(param, { method, body, headers });
629
726
  if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
630
- const text = await res.text();
631
- swap(target, text, swapMode);
632
- Stick.init(target);
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 ─────────────────────────────────────────────────────