@torpor/view 1.1.0 → 1.1.1

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/compile.cjs CHANGED
@@ -2201,12 +2201,15 @@ function headElementSelector(node) {
2201
2201
  function buildElementAttributes$1(node, varName, status, b) {
2202
2202
  let fragment = status.fragmentStack.at(-1)?.fragment;
2203
2203
  if (!fragment) throw new Error("No fragment on stack");
2204
- for (let { name, value, reactive, fullyReactive, span } of node.attributes) if (name === "self" && node.tagName === "@element") {} else if (name === "&ref") {} else if (value != null && fullyReactive) {
2205
- if (name === "&group") buildBindGroupAttribute(node, varName, value, status, b);
2206
- else if (name === "&value" || name === "&checked") buildBindAttribute(node, varName, name, value, status, b);
2204
+ const combinedHandlers = combinedEventHandlers(node, status);
2205
+ const consumedHandlers = new Set(Array.from(combinedHandlers.values(), (c) => c.index));
2206
+ for (let [index, { name, value, reactive, fullyReactive, span }] of node.attributes.entries()) if (name === "self" && node.tagName === "@element") {} else if (name === "&ref") {} else if (value != null && fullyReactive) {
2207
+ if (name === "&group") buildBindGroupAttribute(node, varName, value, status, b, combinedHandlers.get(index));
2208
+ else if (name === "&value" || name === "&checked") buildBindAttribute(node, varName, name, value, status, b, combinedHandlers.get(index));
2207
2209
  else if (name === "onmount") buildMount("elMount", `return (${trimEnd(value.trim(), ";")})(${varName});`, status, b);
2208
- else if (name.startsWith("on")) buildEventAttribute(varName, name, value, span, status, b);
2209
- else if (name.startsWith("transition")) buildTransitionAttribute(node, varName, name, value, status, b);
2210
+ else if (name.startsWith("on")) {
2211
+ if (!consumedHandlers.has(index)) buildEventAttribute(varName, name, value, span, status, b);
2212
+ } else if (name.startsWith("transition")) buildTransitionAttribute(node, varName, name, value, status, b);
2210
2213
  else if (name === "class") {
2211
2214
  status.imports.add("t_class");
2212
2215
  const params = [value];
@@ -2244,21 +2247,20 @@ function buildElementAttributes$1(node, varName, status, b) {
2244
2247
  }
2245
2248
  }
2246
2249
  }
2247
- function buildBindGroupAttribute(node, varName, value, status, b) {
2250
+ function buildBindGroupAttribute(node, varName, value, status, b, combined) {
2248
2251
  value = replaceForVarNames(value, status);
2249
2252
  let eventName = "change";
2250
2253
  let inputValue = node.attributes.find((a) => a.name === "value")?.value;
2251
2254
  let set = `${value} == ${inputValue}`;
2252
2255
  let propName = "checked";
2253
2256
  buildRun("setBinding", `${`${varName}.${propName} = ${set}`};`, status, b);
2254
- status.imports.add("t_event");
2255
- b.append(`t_event(${varName}, "${eventName}", (e) => {
2256
- if (e.target.${propName}) ${value} = ${inputValue};
2257
- });`);
2257
+ emitBindEvent(varName, eventName, `(e) => {
2258
+ if (e.target.${propName}) ${value} = ${inputValue};
2259
+ }`, `if (e.target.${propName}) ${value} = ${inputValue};`, combined, status, b);
2258
2260
  }
2259
- function buildBindAttribute(node, varName, name, value, status, b) {
2261
+ function buildBindAttribute(node, varName, name, value, status, b, combined) {
2260
2262
  value = replaceForVarNames(value, status);
2261
- let eventName = "input";
2263
+ let eventName = bindEventName(node, name);
2262
2264
  let defaultValue = "\"\"";
2263
2265
  let inputValue = "e.target.value";
2264
2266
  if (node.tagName === "input") {
@@ -2271,25 +2273,83 @@ function buildBindAttribute(node, varName, name, value, status, b) {
2271
2273
  case "checkbox":
2272
2274
  defaultValue = "false";
2273
2275
  inputValue = "e.target.checked";
2274
- break;
2275
- case "radio":
2276
- eventName = "change";
2277
- inputValue = "e.target.value";
2278
2276
  }
2279
2277
  } else if (node.tagName === "select") {
2280
- eventName = "change";
2281
2278
  if (node.attributes.find((a) => a.name === "multiple")) {
2282
2279
  buildRun("setBinding", `Array.from(${varName}.options).forEach((opt) => opt.selected = Array.isArray(${value}) && ${value}.includes(opt.value));`, status, b);
2283
- status.imports.add("t_event");
2284
- b.append(`t_event(${varName}, "change", (e) => ${value} = Array.from(e.target.selectedOptions).map((opt) => opt.value));`);
2280
+ emitBindEvent(varName, eventName, `(e) => ${value} = Array.from(e.target.selectedOptions).map((opt) => opt.value)`, `${value} = Array.from(e.target.selectedOptions).map((opt) => opt.value);`, combined, status, b);
2285
2281
  return;
2286
2282
  }
2287
2283
  }
2288
2284
  let set = `${value} || ${defaultValue}`;
2289
2285
  const propName = name.substring(1);
2290
2286
  buildRun("setBinding", `${propName === "value" ? `${varName}.${propName} = String(${set})` : `${varName}.${propName} = ${set}`};`, status, b);
2287
+ emitBindEvent(varName, eventName, `(e) => ${value} = ${inputValue}`, `${value} = ${inputValue};`, combined, status, b);
2288
+ }
2289
+ /**
2290
+ * Gets the name of the event that a &-binding attribute uses to write the
2291
+ * element's state back to the bound value
2292
+ */
2293
+ function bindEventName(node, name) {
2294
+ if (name === "&group") return "change";
2295
+ if (node.tagName === "select") return "change";
2296
+ if (node.tagName === "input") {
2297
+ let typeAttribute = node.attributes.find((a) => a.name === "type");
2298
+ if (typeAttribute?.value && trimQuotes(typeAttribute.value) === "radio") return "change";
2299
+ }
2300
+ return "input";
2301
+ }
2302
+ /**
2303
+ * Pairs &-binding attributes (&value, &checked, &group) with user `on<event>`
2304
+ * handler attributes listening to the same event. Both would emit their own
2305
+ * t_event call for the element, but the second registration would clobber
2306
+ * the first -- typically the binding's state write -- so the pair is emitted
2307
+ * as one t_event that calls the binding write and the user handler in source
2308
+ * order. Returns a map from binding attribute index to the combined handler.
2309
+ */
2310
+ function combinedEventHandlers(node, status) {
2311
+ const bindings = /* @__PURE__ */ new Map();
2312
+ const handlers = /* @__PURE__ */ new Map();
2313
+ for (let [index, { name, value, fullyReactive }] of node.attributes.entries()) {
2314
+ if (value == null || !fullyReactive) continue;
2315
+ if (name === "&value" || name === "&checked" || name === "&group") bindings.set(bindEventName(node, name), index);
2316
+ else if (name.startsWith("on")) handlers.set(name.substring(2), index);
2317
+ }
2318
+ const combined = /* @__PURE__ */ new Map();
2319
+ for (let [eventName, bindingIndex] of bindings) {
2320
+ const handlerIndex = handlers.get(eventName);
2321
+ if (handlerIndex === void 0) continue;
2322
+ const handler = node.attributes[handlerIndex];
2323
+ combined.set(bindingIndex, {
2324
+ index: handlerIndex,
2325
+ value: replaceForVarNames(handler.value, status),
2326
+ span: handler.span,
2327
+ first: handlerIndex < bindingIndex
2328
+ });
2329
+ }
2330
+ return combined;
2331
+ }
2332
+ /**
2333
+ * Emits the t_event call for a &-binding, optionally combined with a user
2334
+ * `on<event>` handler for the same event (which is called after -- or, when
2335
+ * it appeared first in source, before -- the binding write)
2336
+ */
2337
+ function emitBindEvent(varName, eventName, listener, body, combined, status, b) {
2291
2338
  status.imports.add("t_event");
2292
- b.append(`t_event(${varName}, "${eventName}", (e) => ${value} = ${inputValue});`);
2339
+ if (!combined) {
2340
+ b.append(`t_event(${varName}, "${eventName}", ${listener});`);
2341
+ return;
2342
+ }
2343
+ b.append(`t_event(${varName}, "${eventName}", (e) => {`);
2344
+ const handlerCall = `((${combined.value}) as ((e: any) => any) | undefined)?.(e);`;
2345
+ if (combined.first) {
2346
+ addMappedText("", handlerCall, "", combined.span, status, b);
2347
+ b.append(body);
2348
+ } else {
2349
+ b.append(body);
2350
+ addMappedText("", handlerCall, "", combined.span, status, b);
2351
+ }
2352
+ b.append("});");
2293
2353
  }
2294
2354
  function buildEventAttribute(varName, name, value, span, status, b) {
2295
2355
  value = replaceForVarNames(value, status);
package/dist/compile.mjs CHANGED
@@ -2200,12 +2200,15 @@ function headElementSelector(node) {
2200
2200
  function buildElementAttributes$1(node, varName, status, b) {
2201
2201
  let fragment = status.fragmentStack.at(-1)?.fragment;
2202
2202
  if (!fragment) throw new Error("No fragment on stack");
2203
- for (let { name, value, reactive, fullyReactive, span } of node.attributes) if (name === "self" && node.tagName === "@element") {} else if (name === "&ref") {} else if (value != null && fullyReactive) {
2204
- if (name === "&group") buildBindGroupAttribute(node, varName, value, status, b);
2205
- else if (name === "&value" || name === "&checked") buildBindAttribute(node, varName, name, value, status, b);
2203
+ const combinedHandlers = combinedEventHandlers(node, status);
2204
+ const consumedHandlers = new Set(Array.from(combinedHandlers.values(), (c) => c.index));
2205
+ for (let [index, { name, value, reactive, fullyReactive, span }] of node.attributes.entries()) if (name === "self" && node.tagName === "@element") {} else if (name === "&ref") {} else if (value != null && fullyReactive) {
2206
+ if (name === "&group") buildBindGroupAttribute(node, varName, value, status, b, combinedHandlers.get(index));
2207
+ else if (name === "&value" || name === "&checked") buildBindAttribute(node, varName, name, value, status, b, combinedHandlers.get(index));
2206
2208
  else if (name === "onmount") buildMount("elMount", `return (${trimEnd(value.trim(), ";")})(${varName});`, status, b);
2207
- else if (name.startsWith("on")) buildEventAttribute(varName, name, value, span, status, b);
2208
- else if (name.startsWith("transition")) buildTransitionAttribute(node, varName, name, value, status, b);
2209
+ else if (name.startsWith("on")) {
2210
+ if (!consumedHandlers.has(index)) buildEventAttribute(varName, name, value, span, status, b);
2211
+ } else if (name.startsWith("transition")) buildTransitionAttribute(node, varName, name, value, status, b);
2209
2212
  else if (name === "class") {
2210
2213
  status.imports.add("t_class");
2211
2214
  const params = [value];
@@ -2243,21 +2246,20 @@ function buildElementAttributes$1(node, varName, status, b) {
2243
2246
  }
2244
2247
  }
2245
2248
  }
2246
- function buildBindGroupAttribute(node, varName, value, status, b) {
2249
+ function buildBindGroupAttribute(node, varName, value, status, b, combined) {
2247
2250
  value = replaceForVarNames(value, status);
2248
2251
  let eventName = "change";
2249
2252
  let inputValue = node.attributes.find((a) => a.name === "value")?.value;
2250
2253
  let set = `${value} == ${inputValue}`;
2251
2254
  let propName = "checked";
2252
2255
  buildRun("setBinding", `${`${varName}.${propName} = ${set}`};`, status, b);
2253
- status.imports.add("t_event");
2254
- b.append(`t_event(${varName}, "${eventName}", (e) => {
2255
- if (e.target.${propName}) ${value} = ${inputValue};
2256
- });`);
2256
+ emitBindEvent(varName, eventName, `(e) => {
2257
+ if (e.target.${propName}) ${value} = ${inputValue};
2258
+ }`, `if (e.target.${propName}) ${value} = ${inputValue};`, combined, status, b);
2257
2259
  }
2258
- function buildBindAttribute(node, varName, name, value, status, b) {
2260
+ function buildBindAttribute(node, varName, name, value, status, b, combined) {
2259
2261
  value = replaceForVarNames(value, status);
2260
- let eventName = "input";
2262
+ let eventName = bindEventName(node, name);
2261
2263
  let defaultValue = "\"\"";
2262
2264
  let inputValue = "e.target.value";
2263
2265
  if (node.tagName === "input") {
@@ -2270,25 +2272,83 @@ function buildBindAttribute(node, varName, name, value, status, b) {
2270
2272
  case "checkbox":
2271
2273
  defaultValue = "false";
2272
2274
  inputValue = "e.target.checked";
2273
- break;
2274
- case "radio":
2275
- eventName = "change";
2276
- inputValue = "e.target.value";
2277
2275
  }
2278
2276
  } else if (node.tagName === "select") {
2279
- eventName = "change";
2280
2277
  if (node.attributes.find((a) => a.name === "multiple")) {
2281
2278
  buildRun("setBinding", `Array.from(${varName}.options).forEach((opt) => opt.selected = Array.isArray(${value}) && ${value}.includes(opt.value));`, status, b);
2282
- status.imports.add("t_event");
2283
- b.append(`t_event(${varName}, "change", (e) => ${value} = Array.from(e.target.selectedOptions).map((opt) => opt.value));`);
2279
+ emitBindEvent(varName, eventName, `(e) => ${value} = Array.from(e.target.selectedOptions).map((opt) => opt.value)`, `${value} = Array.from(e.target.selectedOptions).map((opt) => opt.value);`, combined, status, b);
2284
2280
  return;
2285
2281
  }
2286
2282
  }
2287
2283
  let set = `${value} || ${defaultValue}`;
2288
2284
  const propName = name.substring(1);
2289
2285
  buildRun("setBinding", `${propName === "value" ? `${varName}.${propName} = String(${set})` : `${varName}.${propName} = ${set}`};`, status, b);
2286
+ emitBindEvent(varName, eventName, `(e) => ${value} = ${inputValue}`, `${value} = ${inputValue};`, combined, status, b);
2287
+ }
2288
+ /**
2289
+ * Gets the name of the event that a &-binding attribute uses to write the
2290
+ * element's state back to the bound value
2291
+ */
2292
+ function bindEventName(node, name) {
2293
+ if (name === "&group") return "change";
2294
+ if (node.tagName === "select") return "change";
2295
+ if (node.tagName === "input") {
2296
+ let typeAttribute = node.attributes.find((a) => a.name === "type");
2297
+ if (typeAttribute?.value && trimQuotes(typeAttribute.value) === "radio") return "change";
2298
+ }
2299
+ return "input";
2300
+ }
2301
+ /**
2302
+ * Pairs &-binding attributes (&value, &checked, &group) with user `on<event>`
2303
+ * handler attributes listening to the same event. Both would emit their own
2304
+ * t_event call for the element, but the second registration would clobber
2305
+ * the first -- typically the binding's state write -- so the pair is emitted
2306
+ * as one t_event that calls the binding write and the user handler in source
2307
+ * order. Returns a map from binding attribute index to the combined handler.
2308
+ */
2309
+ function combinedEventHandlers(node, status) {
2310
+ const bindings = /* @__PURE__ */ new Map();
2311
+ const handlers = /* @__PURE__ */ new Map();
2312
+ for (let [index, { name, value, fullyReactive }] of node.attributes.entries()) {
2313
+ if (value == null || !fullyReactive) continue;
2314
+ if (name === "&value" || name === "&checked" || name === "&group") bindings.set(bindEventName(node, name), index);
2315
+ else if (name.startsWith("on")) handlers.set(name.substring(2), index);
2316
+ }
2317
+ const combined = /* @__PURE__ */ new Map();
2318
+ for (let [eventName, bindingIndex] of bindings) {
2319
+ const handlerIndex = handlers.get(eventName);
2320
+ if (handlerIndex === void 0) continue;
2321
+ const handler = node.attributes[handlerIndex];
2322
+ combined.set(bindingIndex, {
2323
+ index: handlerIndex,
2324
+ value: replaceForVarNames(handler.value, status),
2325
+ span: handler.span,
2326
+ first: handlerIndex < bindingIndex
2327
+ });
2328
+ }
2329
+ return combined;
2330
+ }
2331
+ /**
2332
+ * Emits the t_event call for a &-binding, optionally combined with a user
2333
+ * `on<event>` handler for the same event (which is called after -- or, when
2334
+ * it appeared first in source, before -- the binding write)
2335
+ */
2336
+ function emitBindEvent(varName, eventName, listener, body, combined, status, b) {
2290
2337
  status.imports.add("t_event");
2291
- b.append(`t_event(${varName}, "${eventName}", (e) => ${value} = ${inputValue});`);
2338
+ if (!combined) {
2339
+ b.append(`t_event(${varName}, "${eventName}", ${listener});`);
2340
+ return;
2341
+ }
2342
+ b.append(`t_event(${varName}, "${eventName}", (e) => {`);
2343
+ const handlerCall = `((${combined.value}) as ((e: any) => any) | undefined)?.(e);`;
2344
+ if (combined.first) {
2345
+ addMappedText("", handlerCall, "", combined.span, status, b);
2346
+ b.append(body);
2347
+ } else {
2348
+ b.append(body);
2349
+ addMappedText("", handlerCall, "", combined.span, status, b);
2350
+ }
2351
+ b.append("});");
2292
2352
  }
2293
2353
  function buildEventAttribute(varName, name, value, span, status, b) {
2294
2354
  value = replaceForVarNames(value, status);