tw-framework 1.0.6 → 1.0.7

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.
@@ -23,10 +23,18 @@
23
23
  try { var c = globalThis.__twClient; return (c && c.$page) || {}; } catch (e) { return {}; }
24
24
  };
25
25
 
26
+ var warnedExprs = {};
26
27
  function evalExpr(expr) {
27
28
  try {
28
29
  return (new Function("state", "tw", "with (tw) { with (state) { return (" + expr + "); } }"))(state, twClientScope());
29
30
  } catch (e) {
31
+ // Silent failures were a debugging nightmare: a typo in an
32
+ // expression just blanked every span it touched. Warn once per
33
+ // expression, then stay quiet.
34
+ if (!warnedExprs[expr]) {
35
+ warnedExprs[expr] = 1;
36
+ try { console.warn("[tw] expression failed:", expr, "-", e.message); } catch (we) { /* ignore */ }
37
+ }
30
38
  return undefined;
31
39
  }
32
40
  }
@@ -129,6 +137,11 @@
129
137
  }
130
138
  el.__twRefs = [];
131
139
  var html = "";
140
+ // The loop var is BORROWED state space: save the user's own value
141
+ // (if any) and restore it after -- the old code deleted it, so a
142
+ // loop over a var named like an existing state key destroyed it.
143
+ var __twPrevV = state[v];
144
+ var __twHadV = Object.prototype.hasOwnProperty.call(state, v);
132
145
  for (var k = 0; k < arr.length; k++) {
133
146
  var ref = "__tw_item_" + (++twItemSeq);
134
147
  state[ref] = arr[k];
@@ -136,7 +149,8 @@
136
149
  state[v] = arr[k];
137
150
  html += renderTemplateString(tpl, v, ref);
138
151
  }
139
- delete state[v];
152
+ if (__twHadV) state[v] = __twPrevV;
153
+ else delete state[v];
140
154
  if (el.__twLen !== arr.length + ":" + html.length || el.__twHtml !== html) {
141
155
  el.innerHTML = html;
142
156
  el.__twLen = arr.length + ":" + html.length;
@@ -157,7 +171,15 @@
157
171
  var key = inputs[j].getAttribute("data-tw-model");
158
172
  if (key in state) {
159
173
  var v = state[key];
160
- if (String(inputs[j].value) !== String(v)) inputs[j].value = v;
174
+ var isCheck = inputs[j].type === "checkbox" || inputs[j].type === "radio";
175
+ if (isCheck) {
176
+ // Boolean binding for checkboxes/radios (round 4: these used to
177
+ // read .value ("on") and never reflect boolean state).
178
+ if (inputs[j].type === "radio") inputs[j].checked = String(v) === inputs[j].value;
179
+ else inputs[j].checked = !!v;
180
+ } else if (String(inputs[j].value) !== String(v)) {
181
+ inputs[j].value = v;
182
+ }
161
183
  }
162
184
  }
163
185
  refreshLoops();
@@ -170,6 +192,11 @@
170
192
  "keyup", "keypress", "focus", "blur", "mouseenter", "mouseleave", "wheel"];
171
193
 
172
194
  EVENTS.forEach(function (ev) {
195
+ // Delegate on the BUBBLE phase so the innermost handler runs first
196
+ // (round 4: capture=true inverted the expected order for nested
197
+ // data-tw-event elements). focus/blur do not bubble -- those two
198
+ // keep the capture phase.
199
+ var useCapture = ev === "focus" || ev === "blur";
173
200
  document.addEventListener(ev, function (e) {
174
201
  var t = e.target;
175
202
  if (!t || !t.closest) return;
@@ -180,16 +207,28 @@
180
207
  if (host.getAttribute("data-tw-event-" + ev + "-prevent") === "true") e.preventDefault();
181
208
  if (host.getAttribute("data-tw-event-" + ev + "-stop") === "true") e.stopPropagation();
182
209
  window.__twEvent = { target: t, value: t.value, type: ev };
183
- runHandler(code);
210
+ try { runHandler(code); } finally {
211
+ // Event context is scoped to THIS handler only (round 4: the
212
+ // stale window.__twEvent leaked the previous event's target and
213
+ // value into later handlers that read it).
214
+ try { delete window.__twEvent; } catch (ee) { window.__twEvent = undefined; }
215
+ }
184
216
  refresh();
185
- }, true);
217
+ }, useCapture);
186
218
  });
187
219
 
188
220
  // Two-way: input with data-tw-model writes to state
189
221
  document.addEventListener("input", function (e) {
190
222
  var t = e.target;
191
223
  if (t && t.getAttribute && t.getAttribute("data-tw-model")) {
192
- state[t.getAttribute("data-tw-model")] = t.value;
224
+ var mk = t.getAttribute("data-tw-model");
225
+ if (t.type === "checkbox" || t.type === "radio") {
226
+ // checkbox -> boolean; radio -> its value when checked (round 4:
227
+ // the old code wrote the literal string "on").
228
+ state[mk] = t.type === "checkbox" ? !!t.checked : (t.checked ? t.value : state[mk]);
229
+ } else {
230
+ state[mk] = t.value;
231
+ }
193
232
  refresh();
194
233
  }
195
234
  });
@@ -260,6 +299,22 @@
260
299
  if (oldRoot.replaceWith) oldRoot.replaceWith(imported);
261
300
  else oldRoot.innerHTML = newRoot.innerHTML;
262
301
  }
302
+ // innerHTML-inserted <script> tags never execute (round 4: pages
303
+ // relying on scripts silently died after an SPA swap). Re-create
304
+ // each script node so the browser runs it.
305
+ try {
306
+ var liveRoot = document.querySelector("[data-tw-root]") || document.body;
307
+ var swapped = liveRoot.querySelectorAll("script");
308
+ for (var si = 0; si < swapped.length; si++) {
309
+ var os = swapped[si];
310
+ var ns = document.createElement("script");
311
+ for (var ai = 0; ai < os.attributes.length; ai++) {
312
+ ns.setAttribute(os.attributes[ai].name, os.attributes[ai].value);
313
+ }
314
+ ns.textContent = os.textContent;
315
+ if (os.parentNode) os.parentNode.replaceChild(ns, os);
316
+ }
317
+ } catch (e) { /* script re-run is best-effort */ }
263
318
 
264
319
  var t = newDoc ? newDoc.querySelector("title") : null;
265
320
  if (t) document.title = t.textContent;
@@ -329,9 +384,16 @@
329
384
  // data-tw-prefetch="always" -> fetched immediately on load
330
385
  // data-tw-prefetch="viewport" -> fetched when the link scrolls into view
331
386
  // data-tw-no-prefetch -> never fetched
387
+ var prefetchOrder = [];
332
388
  function twPrefetch(href) {
333
389
  if (!href || !sameOrigin(href) || prefetched[href]) return;
334
390
  prefetched[href] = true;
391
+ // Bound the cache (round 4: it grew forever on long SPA sessions).
392
+ prefetchOrder.push(href);
393
+ while (prefetchOrder.length > 60) {
394
+ var old2 = prefetchOrder.shift();
395
+ delete prefetched[old2];
396
+ }
335
397
  try { fetch(href).catch(function () {}); } catch (e) { /* ignore */ }
336
398
  }
337
399