@lokascript/htmx-adapter 3.0.0 → 3.1.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/CHANGELOG.md +183 -3
- package/README.md +44 -16
- package/dist/htmx-i18n.global.js +1 -1
- package/dist/htmx-i18n.global.js.map +1 -1
- package/dist/index.cjs +207 -54
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +141 -31
- package/dist/index.d.ts +141 -31
- package/dist/index.js +198 -53
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/browser.ts +20 -5
- package/src/canonicalize.ts +131 -45
- package/src/extension.ts +175 -26
- package/src/hx-on.ts +153 -43
- package/src/index.ts +15 -1
package/dist/index.d.ts
CHANGED
|
@@ -86,11 +86,13 @@ declare function resetRegistry(): void;
|
|
|
86
86
|
* allows):
|
|
87
87
|
*
|
|
88
88
|
* - **The authored attribute is never removed or rewritten** — devtools
|
|
89
|
-
* keeps showing what the author wrote.
|
|
90
|
-
* author-written canonical `hx-trigger` whose *value* uses localized
|
|
91
|
-
* event names (`hx-trigger="clic"`)
|
|
92
|
-
*
|
|
93
|
-
*
|
|
89
|
+
* keeps showing what the author wrote. Two documented exceptions:
|
|
90
|
+
* an author-written canonical `hx-trigger` whose *value* uses localized
|
|
91
|
+
* event names (`hx-trigger="clic"`) has no separate canonical target,
|
|
92
|
+
* so the value is translated in place (idempotent — the maps are
|
|
93
|
+
* localized → canonical, so a second pass is a no-op); and in executor
|
|
94
|
+
* mode a canonical-named `hx-on:*` attribute may be removed by the
|
|
95
|
+
* claim when no htmx hook exists to cancel htmx's binding (hx-on.ts).
|
|
94
96
|
* - **An existing canonical attribute always wins.** If the element
|
|
95
97
|
* already has `hx-get`, a localized `hx-obtener` never overwrites it.
|
|
96
98
|
* - **No vocab, no work.** With no languages registered every function
|
|
@@ -101,17 +103,40 @@ declare function resetRegistry(): void;
|
|
|
101
103
|
* convention: Spanish writes `hx-obtener`, not `xx-obtener`), so the
|
|
102
104
|
* prefix doubles as the discovery anchor.
|
|
103
105
|
*/
|
|
106
|
+
/**
|
|
107
|
+
* Byte-mirror of htmx v4's `HCON.split` top-level-comma regex (vendored
|
|
108
|
+
* 4.0.0, `HCON.split`; pinned against the vendored file by
|
|
109
|
+
* test/vendor-mirror.test.ts). Spec boundaries must match core's grammar:
|
|
110
|
+
* commas inside `[filters]`, `(calls)`, and quoted strings — e.g.
|
|
111
|
+
* `from:".a, .b"` — are NOT separators.
|
|
112
|
+
*
|
|
113
|
+
* On htmx 4.0.0 the extension's `init(internalAPI)` swaps in htmx's own
|
|
114
|
+
* `HCON.split` (see setTriggerSpecSplitter), so the mirror is the
|
|
115
|
+
* fallback for v2 and for pages where the extension is not registered.
|
|
116
|
+
*/
|
|
117
|
+
declare const TOP_LEVEL_COMMA_RE: RegExp;
|
|
118
|
+
type TriggerSpecSplitter = (value: string) => string[];
|
|
119
|
+
/**
|
|
120
|
+
* Use htmx's own top-level splitter (`internalAPI.HCON.split` on 4.0.0)
|
|
121
|
+
* instead of the mirror. Pass `null` to restore the mirror.
|
|
122
|
+
*/
|
|
123
|
+
declare function setTriggerSpecSplitter(fn: TriggerSpecSplitter | null): void;
|
|
104
124
|
/**
|
|
105
125
|
* Translate localized event names inside an `hx-trigger` value.
|
|
106
126
|
*
|
|
107
127
|
* hx-trigger grammar: comma-separated specs, each `eventName[filter]
|
|
108
128
|
* modifier…`. Only the leading event token of each spec is translated
|
|
109
|
-
* (
|
|
110
|
-
* `from:body
|
|
111
|
-
*
|
|
112
|
-
*
|
|
129
|
+
* (an attached `[...]` filter, all modifiers like `delay:500ms` /
|
|
130
|
+
* `from:body` / `once`, and the authored spacing stay byte-identical —
|
|
131
|
+
* the split removes exactly the commas the join restores, so an
|
|
132
|
+
* all-canonical value comes back verbatim). Unknown tokens pass
|
|
133
|
+
* through untouched, which also makes translation idempotent (the maps
|
|
134
|
+
* are localized → canonical only). Only the map's OWN keys translate:
|
|
135
|
+
* `constructor` is not an event.
|
|
113
136
|
*/
|
|
114
137
|
declare function translateTriggerValue(value: string, events: Record<string, string>): string;
|
|
138
|
+
/** True when `name` on `elt` was written by canonicalization, not authored. */
|
|
139
|
+
declare function wasCreatedByAdapter(elt: Element, name: string): boolean;
|
|
115
140
|
/**
|
|
116
141
|
* Canonicalize one element's localized htmx attributes in place.
|
|
117
142
|
* Returns true if any attribute was added or updated.
|
|
@@ -131,18 +156,25 @@ declare function canonicalizeTree(root: Element | Document | DocumentFragment |
|
|
|
131
156
|
* Primary target is **htmx v4**, whose extensions register via
|
|
132
157
|
* `htmx.registerExtension(name, ext)` and hook lifecycle events through
|
|
133
158
|
* underscore-named methods (event name with `:` → `_`), each receiving
|
|
134
|
-
* `(elt, detail)`. Verified against htmx 4.0.0
|
|
159
|
+
* `(elt, detail)`. Verified against htmx 4.0.0 (see
|
|
135
160
|
* test/browser/vendor/README.md): `process(root)` fires
|
|
136
161
|
* `htmx:before:process` on the processed root — `document.body`
|
|
137
162
|
* initially, each swapped-in subtree afterwards — BEFORE any element
|
|
138
163
|
* init or hx-on binding, so canonicalizing in `htmx_before_process`
|
|
139
164
|
* covers everything htmx will read. `htmx_before_process_node` is kept
|
|
140
165
|
* as a defensive alias for other v4 prereleases that used per-node
|
|
141
|
-
* naming; an unmatched key is inert.
|
|
166
|
+
* naming; an unmatched key is inert. The cancelable
|
|
167
|
+
* `htmx_before_on_init` hook (fires per hx-on-carrying node, after
|
|
168
|
+
* before:process; documented as cancelable in htmx 4.0.0's `htmx.d.ts`)
|
|
169
|
+
* lets executor mode keep claimed `hx-on:*` attributes in the DOM
|
|
170
|
+
* instead of removing them — see createExtension.
|
|
142
171
|
*
|
|
143
172
|
* A v2 fallback (`htmx.defineExtension` + `onEvent('htmx:beforeProcessNode')`)
|
|
144
173
|
* is included because the localized attribute names are version-agnostic
|
|
145
|
-
* data — but v2 support is best-effort, not a tested target.
|
|
174
|
+
* data — but v2 support is best-effort, not a tested target. Measured on
|
|
175
|
+
* 2.0.10: `processNode` binds `hx-on` (`processHxOnWildcard`) BEFORE it
|
|
176
|
+
* fires `htmx:beforeProcessNode` per node, so v2 has no pre-bind seam and
|
|
177
|
+
* executor-mode claims keep removing canonical `hx-on:*` attrs there.
|
|
146
178
|
*
|
|
147
179
|
* The extension hook alone is not enough for the *initial* page: script
|
|
148
180
|
* order decides whether our sweep beats htmx's own DOMContentLoaded scan.
|
|
@@ -154,25 +186,69 @@ declare function canonicalizeTree(root: Element | Document | DocumentFragment |
|
|
|
154
186
|
declare const EXTENSION_NAME = "lokascript-i18n";
|
|
155
187
|
/** Minimal shape of the htmx global we interact with. */
|
|
156
188
|
interface HtmxLike {
|
|
157
|
-
/**
|
|
158
|
-
|
|
189
|
+
/**
|
|
190
|
+
* htmx v4 registration entry point. Returns `false` when the name is
|
|
191
|
+
* rejected — `htmx.config.extensions` is a non-empty allowlist that
|
|
192
|
+
* omits it, or it was already registered. (`htmx.d.ts` types the
|
|
193
|
+
* return as `void`; the runtime, verified on 4.0.0, returns `false`.)
|
|
194
|
+
*/
|
|
195
|
+
registerExtension?(name: string, extension: object): boolean | void;
|
|
159
196
|
/** htmx v1/v2 registration entry point. */
|
|
160
197
|
defineExtension?(name: string, extension: object): void;
|
|
198
|
+
/** htmx v4 config — the parts that decide which hx-on spellings it binds. */
|
|
199
|
+
config?: {
|
|
200
|
+
/** Alternate attribute prefix (`data-hx-` by default; `''` disables it). */
|
|
201
|
+
prefix?: string;
|
|
202
|
+
/** Separator between `hx-on` and the event name (`:` by default). */
|
|
203
|
+
metaCharacter?: string;
|
|
204
|
+
};
|
|
161
205
|
}
|
|
162
206
|
/**
|
|
163
207
|
* Build the extension object. v4 hooks and the v2 `onEvent` callback are
|
|
164
|
-
* both present — each API only reads the members it knows about.
|
|
208
|
+
* both present — each API only reads the members it knows about. Pass
|
|
209
|
+
* the htmx instance so the v4 hook can read `config.prefix` /
|
|
210
|
+
* `config.metaCharacter` (defaults are assumed otherwise).
|
|
211
|
+
*
|
|
212
|
+
* `init(internalAPI)` (v4 only) takes exactly one thing from the API:
|
|
213
|
+
* `HCON.split`, htmx's own top-level trigger-spec splitter, which
|
|
214
|
+
* replaces the regex mirror in canonicalize.ts for the lifetime of the
|
|
215
|
+
* page. The rest of the 4.0.0 surface — `attributeValue`,
|
|
216
|
+
* `parseTriggerSpecs`, `getAttributeObject`, `insertContent`, `morph`,
|
|
217
|
+
* `initSecurity`, `onTrigger`, `htmxProp`, `triggerHtmxEvent`,
|
|
218
|
+
* `executeJavaScript` — was evaluated and not used: `parseTriggerSpecs`
|
|
219
|
+
* is parse-only (no serializer, so translating through it would mean a
|
|
220
|
+
* hand-written spec serializer); `attributeValue`'s inheritance-aware
|
|
221
|
+
* reads buy nothing for a sweep that visits every element anyway; and
|
|
222
|
+
* `htmxProp(node).onInitialized = true` WOULD keep htmx's binder off a
|
|
223
|
+
* node, but couples to a private flag where `htmx:before:on:init` is a
|
|
224
|
+
* typed, documented, cancelable event — the hook below uses that.
|
|
165
225
|
*/
|
|
166
|
-
declare function createExtension(): object;
|
|
226
|
+
declare function createExtension(htmx?: HtmxLike | null): object;
|
|
167
227
|
/**
|
|
168
228
|
* Register the extension with an htmx global. Returns which API accepted
|
|
169
|
-
* it (`'v4'` / `'v2'`) or `null` if the object exposes neither
|
|
229
|
+
* it (`'v4'` / `'v2'`) or `null` if the object exposes neither, or if
|
|
230
|
+
* v4 rejected the registration.
|
|
231
|
+
*
|
|
232
|
+
* Only an ACCEPTED v4 registration turns claim-time neutralization off:
|
|
233
|
+
* that is the one runtime whose per-node `htmx:before:on:init` hook can
|
|
234
|
+
* keep htmx off a preserved `hx-on:*` attribute. Everything else keeps
|
|
235
|
+
* the hook-independent removal guard.
|
|
170
236
|
*/
|
|
171
237
|
declare function registerWith(htmx: HtmxLike | undefined | null): 'v4' | 'v2' | null;
|
|
172
238
|
/**
|
|
173
|
-
* Sweep the whole document
|
|
174
|
-
*
|
|
175
|
-
* (e.g. a vocab <script> below htmx,
|
|
239
|
+
* Sweep the whole document once the DOM is parsed, and re-sweep whenever
|
|
240
|
+
* a vocab module registers or the body executor changes after that
|
|
241
|
+
* (e.g. a vocab <script> below htmx, a lazily loaded _hyperscript, or
|
|
242
|
+
* dynamic registration).
|
|
243
|
+
*
|
|
244
|
+
* "Parsed" means DOMContentLoaded has FIRED (or `readyState` is already
|
|
245
|
+
* `'complete'`) — not merely `readyState !== 'loading'`. During
|
|
246
|
+
* DOMContentLoaded dispatch `readyState` is already `'interactive'`, and
|
|
247
|
+
* a `defer`/module adapter runs at `'interactive'` before htmx exists;
|
|
248
|
+
* sweeping then would claim in the default remove mode before
|
|
249
|
+
* `registerWith` had a chance to turn neutralization off. A `load`
|
|
250
|
+
* listener covers a script injected after DOMContentLoaded but before
|
|
251
|
+
* `'complete'` (DOMContentLoaded will not fire again for it).
|
|
176
252
|
*
|
|
177
253
|
* Returns a cleanup function (mainly for tests).
|
|
178
254
|
*/
|
|
@@ -253,11 +329,21 @@ declare function installResolverMode(htmx: ResolverConfigurableHtmx): () => void
|
|
|
253
329
|
* - Localized-named attrs (`hx-en:clic`) stay verbatim in the DOM — htmx
|
|
254
330
|
* never recognized them anyway — and NO canonical `hx-on:*` sibling is
|
|
255
331
|
* created.
|
|
256
|
-
* - Canonical-named attrs (`hx-on:click`)
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
*
|
|
332
|
+
* - Canonical-named attrs (`hx-on:click`) must be kept away from htmx's
|
|
333
|
+
* own binder: if htmx bound them, it would eval the hyperscript body
|
|
334
|
+
* as JS — a console error plus a double-execution attempt on every
|
|
335
|
+
* fire. A claim RECORDS the attribute (name + body) on the element;
|
|
336
|
+
* what keeps htmx off it is decided by whoever owns the runtime:
|
|
337
|
+
* - htmx v4, extension accepted: the extension's cancelable
|
|
338
|
+
* `htmx:before:on:init` hook consults the record per node and
|
|
339
|
+
* cancels htmx's binding — the authored attribute stays in the DOM
|
|
340
|
+
* (see extension.ts). `registerWith` turns claim-time removal OFF
|
|
341
|
+
* only after the registration actually succeeded.
|
|
342
|
+
* - everywhere else (htmx v2, a rejected v4 registration, no htmx):
|
|
343
|
+
* `neutralizeOnClaim` is ON (the default) and the canonical-named
|
|
344
|
+
* attribute is removed at claim time — the only guard that does not
|
|
345
|
+
* depend on a hook being installed, documented as the executor-mode
|
|
346
|
+
* exception in the README.
|
|
261
347
|
*
|
|
262
348
|
* With no executor set (the default), none of this runs and bodies keep
|
|
263
349
|
* upstream JS semantics — the behavior-preservation invariant.
|
|
@@ -266,10 +352,13 @@ declare function installResolverMode(htmx: ResolverConfigurableHtmx): () => void
|
|
|
266
352
|
* loads after the initial sweep still applies. Both auto-detection
|
|
267
353
|
* (`autoDetectBodyHooks`) and manual configuration are supported; the
|
|
268
354
|
* executor is also re-read at fire time so replacing it takes effect on
|
|
269
|
-
* live listeners.
|
|
355
|
+
* live listeners. The body is re-read on every re-claim, so
|
|
356
|
+
* `htmx.process(elt, true)` after editing the attribute runs the new body.
|
|
270
357
|
*/
|
|
271
358
|
type BodyExecutor = (code: string, elt: Element, evt: Event) => unknown;
|
|
272
359
|
type BodyTranslator = (body: string, lang: string) => string;
|
|
360
|
+
declare function setNeutralizeOnClaim(enabled: boolean): void;
|
|
361
|
+
declare function neutralizesOnClaim(): boolean;
|
|
273
362
|
/** Configure the body executor. Setting/clearing it notifies subscribers. */
|
|
274
363
|
declare function setBodyExecutor(fn: BodyExecutor | null): void;
|
|
275
364
|
/** Configure the body translator (localized body → English hyperscript). */
|
|
@@ -278,16 +367,37 @@ declare function hasBodyExecutor(): boolean;
|
|
|
278
367
|
declare function hasBodyTranslator(): boolean;
|
|
279
368
|
/** Subscribe to executor changes. Returns an unsubscribe fn. */
|
|
280
369
|
declare function onBodyHooksChanged(listener: () => void): () => void;
|
|
370
|
+
/** True when the adapter has claimed at least one hx-on attribute on `elt`. */
|
|
371
|
+
declare function isClaimed(elt: Element): boolean;
|
|
372
|
+
/** Names of the attributes claimed on `elt` (canonical and localized). */
|
|
373
|
+
declare function claimedAttrNames(elt: Element): Set<string>;
|
|
374
|
+
/**
|
|
375
|
+
* Remove the claimed CANONICAL-named (`hx-on:*`) attributes from `elt`.
|
|
376
|
+
* The per-node fallback for runtimes that will bind the node anyway
|
|
377
|
+
* (v4 mixed nodes). Localized names are invisible to htmx and stay.
|
|
378
|
+
* Returns how many were removed.
|
|
379
|
+
*/
|
|
380
|
+
declare function removeClaimedCanonicalAttrs(elt: Element): number;
|
|
381
|
+
interface ClaimOptions {
|
|
382
|
+
/**
|
|
383
|
+
* The attribute was written by the adapter's own canonicalization (a
|
|
384
|
+
* no-executor sweep's `hx-on:*` copy of a localized attr), not by the
|
|
385
|
+
* author. Such a duplicate is always removed on claim.
|
|
386
|
+
*/
|
|
387
|
+
adapterCreated?: boolean;
|
|
388
|
+
}
|
|
281
389
|
/**
|
|
282
|
-
* Claim one hx-on-family attribute on an element: install the
|
|
283
|
-
* listener and
|
|
284
|
-
*
|
|
390
|
+
* Claim one hx-on-family attribute on an element: record it, install the
|
|
391
|
+
* executor listener, and — when claim-time neutralization is on — remove
|
|
392
|
+
* a canonical-named attribute so htmx never JS-evals it. Returns true if
|
|
393
|
+
* the element changed or a listener was installed now (false when no
|
|
394
|
+
* executor, already claimed for that event, or malformed).
|
|
285
395
|
*
|
|
286
396
|
* `lang` is the element's language at claim time — used for lazy body
|
|
287
397
|
* translation. `events` is the language's event-name map (for the
|
|
288
398
|
* listener's event name; the shorthand/unknown cases pass through).
|
|
289
399
|
*/
|
|
290
|
-
declare function claimHxOnAttribute(elt: Element, attrName: string, lang: string, events: Record<string, string
|
|
400
|
+
declare function claimHxOnAttribute(elt: Element, attrName: string, lang: string, events: Record<string, string>, options?: ClaimOptions): boolean;
|
|
291
401
|
/**
|
|
292
402
|
* Auto-detect body hooks from page globals — called by the browser entry
|
|
293
403
|
* at load and again at DOMContentLoaded. Never overwrites hooks that
|
|
@@ -302,4 +412,4 @@ declare function autoDetectBodyHooks(win: object): void;
|
|
|
302
412
|
/** Reset all body-hook state. Mainly for tests. */
|
|
303
413
|
declare function resetBodyHooks(): void;
|
|
304
414
|
|
|
305
|
-
export { type BodyExecutor, type BodyTranslator, EXTENSION_NAME, type HtmxLike, type HtmxVocab, type VocabPayload, additionalAttributeSelectors, attributeResolver, autoDetectBodyHooks, canonicalizeElement, canonicalizeTree, claimHxOnAttribute, createExtension, hasAnyVocab, hasBodyExecutor, hasBodyTranslator, installAutoSweep, installResolverMode, isLangRegistered, isResolverMode, langOf, normLang, onBodyHooksChanged, onVocabUpdate, register, registerWith, resetBodyHooks, resetRegistry, setBodyExecutor, setBodyTranslator, setResolverMode, translateTriggerValue, vocabFor };
|
|
415
|
+
export { type BodyExecutor, type BodyTranslator, type ClaimOptions, EXTENSION_NAME, type HtmxLike, type HtmxVocab, TOP_LEVEL_COMMA_RE, type TriggerSpecSplitter, type VocabPayload, additionalAttributeSelectors, attributeResolver, autoDetectBodyHooks, canonicalizeElement, canonicalizeTree, claimHxOnAttribute, claimedAttrNames, createExtension, hasAnyVocab, hasBodyExecutor, hasBodyTranslator, installAutoSweep, installResolverMode, isClaimed, isLangRegistered, isResolverMode, langOf, neutralizesOnClaim, normLang, onBodyHooksChanged, onVocabUpdate, register, registerWith, removeClaimedCanonicalAttrs, resetBodyHooks, resetRegistry, setBodyExecutor, setBodyTranslator, setNeutralizeOnClaim, setResolverMode, setTriggerSpecSplitter, translateTriggerValue, vocabFor, wasCreatedByAdapter };
|
package/dist/index.js
CHANGED
|
@@ -57,10 +57,18 @@ function resetRegistry() {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
// src/hx-on.ts
|
|
60
|
+
var hasOwn = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
|
|
60
61
|
var executor = null;
|
|
61
62
|
var translator = null;
|
|
62
63
|
var hookChangeListeners = /* @__PURE__ */ new Set();
|
|
63
64
|
var claimed = /* @__PURE__ */ new WeakMap();
|
|
65
|
+
var neutralizeOnClaim = true;
|
|
66
|
+
function setNeutralizeOnClaim(enabled) {
|
|
67
|
+
neutralizeOnClaim = enabled;
|
|
68
|
+
}
|
|
69
|
+
function neutralizesOnClaim() {
|
|
70
|
+
return neutralizeOnClaim;
|
|
71
|
+
}
|
|
64
72
|
function setBodyExecutor(fn) {
|
|
65
73
|
executor = fn;
|
|
66
74
|
for (const listener of hookChangeListeners) listener();
|
|
@@ -80,44 +88,76 @@ function onBodyHooksChanged(listener) {
|
|
|
80
88
|
hookChangeListeners.delete(listener);
|
|
81
89
|
};
|
|
82
90
|
}
|
|
91
|
+
function isClaimed(elt) {
|
|
92
|
+
return (claimed.get(elt)?.size ?? 0) > 0;
|
|
93
|
+
}
|
|
94
|
+
function claimedAttrNames(elt) {
|
|
95
|
+
const names = /* @__PURE__ */ new Set();
|
|
96
|
+
const claims = claimed.get(elt);
|
|
97
|
+
if (claims) for (const c of claims.values()) names.add(c.attrName);
|
|
98
|
+
return names;
|
|
99
|
+
}
|
|
100
|
+
function removeClaimedCanonicalAttrs(elt) {
|
|
101
|
+
let n = 0;
|
|
102
|
+
for (const name of claimedAttrNames(elt)) {
|
|
103
|
+
if (name.startsWith("hx-on:") && elt.hasAttribute(name)) {
|
|
104
|
+
elt.removeAttribute(name);
|
|
105
|
+
n++;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return n;
|
|
109
|
+
}
|
|
83
110
|
function eventNameForSuffix(rawSuffix, events) {
|
|
84
111
|
if (rawSuffix.startsWith(":")) return `htmx${rawSuffix}`;
|
|
85
|
-
return events[rawSuffix]
|
|
112
|
+
return hasOwn(events, rawSuffix) ? events[rawSuffix] : rawSuffix;
|
|
86
113
|
}
|
|
87
|
-
function claimHxOnAttribute(elt, attrName, lang, events) {
|
|
114
|
+
function claimHxOnAttribute(elt, attrName, lang, events, options = {}) {
|
|
88
115
|
if (!executor) return false;
|
|
89
116
|
const colon = attrName.indexOf(":");
|
|
90
117
|
if (colon <= 0) return false;
|
|
91
118
|
const eventName = eventNameForSuffix(attrName.slice(colon + 1), events);
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
119
|
+
const canonical = attrName.startsWith("hx-on:");
|
|
120
|
+
const body = elt.getAttribute(attrName) ?? "";
|
|
121
|
+
let claims = claimed.get(elt);
|
|
122
|
+
const existing = claims?.get(eventName);
|
|
123
|
+
if (existing) {
|
|
124
|
+
if (existing.attrName === attrName) {
|
|
125
|
+
if (existing.body !== body) {
|
|
126
|
+
existing.body = body;
|
|
127
|
+
existing.translated = null;
|
|
128
|
+
}
|
|
129
|
+
if (canonical && neutralizeOnClaim && elt.hasAttribute(attrName)) {
|
|
130
|
+
elt.removeAttribute(attrName);
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
if (canonical && (options.adapterCreated || neutralizeOnClaim) && elt.hasAttribute(attrName)) {
|
|
95
136
|
elt.removeAttribute(attrName);
|
|
96
137
|
return true;
|
|
97
138
|
}
|
|
98
139
|
return false;
|
|
99
140
|
}
|
|
100
|
-
const
|
|
101
|
-
let translatedBody = null;
|
|
141
|
+
const claim = { attrName, lang, body, translated: null };
|
|
102
142
|
elt.addEventListener(eventName, (evt) => {
|
|
103
143
|
if (!executor) return;
|
|
104
|
-
if (
|
|
105
|
-
|
|
144
|
+
if (claim.translated === null) {
|
|
145
|
+
claim.translated = claim.lang !== "en" && translator ? translator(claim.body, claim.lang) : claim.body;
|
|
106
146
|
}
|
|
107
147
|
try {
|
|
108
|
-
executor(
|
|
148
|
+
executor(claim.translated, elt, evt);
|
|
109
149
|
} catch (err) {
|
|
110
150
|
if (typeof console !== "undefined") {
|
|
111
151
|
console.error(`[htmx-i18n] hx-on body execution failed (${attrName} \u2192 ${eventName})`, err);
|
|
112
152
|
}
|
|
113
153
|
}
|
|
114
154
|
});
|
|
115
|
-
if (
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
} else {
|
|
119
|
-
claimed.set(elt, /* @__PURE__ */ new Set([eventName]));
|
|
155
|
+
if (!claims) {
|
|
156
|
+
claims = /* @__PURE__ */ new Map();
|
|
157
|
+
claimed.set(elt, claims);
|
|
120
158
|
}
|
|
159
|
+
claims.set(eventName, claim);
|
|
160
|
+
if (canonical && neutralizeOnClaim) elt.removeAttribute(attrName);
|
|
121
161
|
return true;
|
|
122
162
|
}
|
|
123
163
|
function autoDetectBodyHooks(win) {
|
|
@@ -138,6 +178,7 @@ function resetBodyHooks() {
|
|
|
138
178
|
translator = null;
|
|
139
179
|
hookChangeListeners.clear();
|
|
140
180
|
claimed = /* @__PURE__ */ new WeakMap();
|
|
181
|
+
neutralizeOnClaim = true;
|
|
141
182
|
}
|
|
142
183
|
|
|
143
184
|
// src/resolver.ts
|
|
@@ -189,19 +230,36 @@ function installResolverMode(htmx) {
|
|
|
189
230
|
}
|
|
190
231
|
|
|
191
232
|
// src/canonicalize.ts
|
|
233
|
+
var hasOwn2 = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
|
|
192
234
|
var NS_RE = /^(?:hx|sse|ws)-/;
|
|
235
|
+
var TOP_LEVEL_COMMA_RE = /,(?![^\[]*\])(?![^(]*\))(?![^<]*\/>)(?=(?:[^"']|"[^"]*"|'[^']*')*$)/;
|
|
236
|
+
var mirrorSplit = (value) => value.split(TOP_LEVEL_COMMA_RE);
|
|
237
|
+
var splitSpecs = mirrorSplit;
|
|
238
|
+
function setTriggerSpecSplitter(fn) {
|
|
239
|
+
splitSpecs = fn ?? mirrorSplit;
|
|
240
|
+
}
|
|
241
|
+
function isTriggerAttr(name) {
|
|
242
|
+
return name === "hx-trigger" || name.startsWith("hx-trigger:");
|
|
243
|
+
}
|
|
193
244
|
function translateTriggerValue(value, events) {
|
|
194
|
-
return value
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
245
|
+
return splitSpecs(value).map(
|
|
246
|
+
(spec) => spec.replace(
|
|
247
|
+
/^(\s*)([^[\s]+)/,
|
|
248
|
+
(whole, ws, evt) => hasOwn2(events, evt) ? ws + events[evt] : whole
|
|
249
|
+
)
|
|
250
|
+
).join(",");
|
|
251
|
+
}
|
|
252
|
+
var created = /* @__PURE__ */ new WeakMap();
|
|
253
|
+
function markCreated(elt, name) {
|
|
254
|
+
let names = created.get(elt);
|
|
255
|
+
if (!names) {
|
|
256
|
+
names = /* @__PURE__ */ new Set();
|
|
257
|
+
created.set(elt, names);
|
|
258
|
+
}
|
|
259
|
+
names.add(name);
|
|
260
|
+
}
|
|
261
|
+
function wasCreatedByAdapter(elt, name) {
|
|
262
|
+
return created.get(elt)?.has(name) ?? false;
|
|
205
263
|
}
|
|
206
264
|
function canonicalizeElement(elt) {
|
|
207
265
|
if (!elt.attributes || elt.attributes.length === 0) return false;
|
|
@@ -223,23 +281,43 @@ function canonicalizeElement(elt) {
|
|
|
223
281
|
}
|
|
224
282
|
const attrs = vocab?.attrs ?? {};
|
|
225
283
|
const events = vocab?.events ?? {};
|
|
284
|
+
let hasEvents = false;
|
|
285
|
+
for (const _ in events) {
|
|
286
|
+
hasEvents = true;
|
|
287
|
+
break;
|
|
288
|
+
}
|
|
226
289
|
let changed = false;
|
|
227
290
|
for (const attr of Array.from(elt.attributes)) {
|
|
228
291
|
const name = attr.name;
|
|
229
292
|
if (!NS_RE.test(name)) continue;
|
|
230
293
|
if (executorMode && name.startsWith("hx-on:")) {
|
|
231
|
-
if (claimHxOnAttribute(elt, name, lang, events
|
|
294
|
+
if (claimHxOnAttribute(elt, name, lang, events, {
|
|
295
|
+
adapterCreated: wasCreatedByAdapter(elt, name)
|
|
296
|
+
})) {
|
|
297
|
+
changed = true;
|
|
298
|
+
}
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
if (isTriggerAttr(name)) {
|
|
302
|
+
if (hasEvents) {
|
|
303
|
+
const translated = translateTriggerValue(attr.value, events);
|
|
304
|
+
if (translated !== attr.value) {
|
|
305
|
+
elt.setAttribute(name, translated);
|
|
306
|
+
changed = true;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
232
309
|
continue;
|
|
233
310
|
}
|
|
234
|
-
let canonical = attrs[name];
|
|
311
|
+
let canonical = hasOwn2(attrs, name) ? attrs[name] : void 0;
|
|
235
312
|
let colonBase;
|
|
236
313
|
if (!canonical) {
|
|
237
314
|
const colon = name.indexOf(":");
|
|
238
315
|
if (colon > 0) {
|
|
239
|
-
|
|
316
|
+
const base = name.slice(0, colon);
|
|
317
|
+
colonBase = hasOwn2(attrs, base) ? attrs[base] : void 0;
|
|
240
318
|
if (colonBase) {
|
|
241
319
|
const suffix = name.slice(colon + 1);
|
|
242
|
-
canonical = `${colonBase}:${events[suffix]
|
|
320
|
+
canonical = colonBase === "hx-on" ? `${colonBase}:${hasOwn2(events, suffix) ? events[suffix] : suffix}` : `${colonBase}:${suffix}`;
|
|
243
321
|
}
|
|
244
322
|
}
|
|
245
323
|
}
|
|
@@ -250,18 +328,11 @@ function canonicalizeElement(elt) {
|
|
|
250
328
|
}
|
|
251
329
|
if (canonical === name || elt.hasAttribute(canonical)) continue;
|
|
252
330
|
let value = attr.value;
|
|
253
|
-
if (canonical
|
|
331
|
+
if (isTriggerAttr(canonical)) value = translateTriggerValue(value, events);
|
|
254
332
|
elt.setAttribute(canonical, value);
|
|
333
|
+
markCreated(elt, canonical);
|
|
255
334
|
changed = true;
|
|
256
335
|
}
|
|
257
|
-
const trigger = elt.getAttribute("hx-trigger");
|
|
258
|
-
if (trigger !== null && Object.keys(events).length > 0) {
|
|
259
|
-
const translated = translateTriggerValue(trigger, events);
|
|
260
|
-
if (translated !== trigger) {
|
|
261
|
-
elt.setAttribute("hx-trigger", translated);
|
|
262
|
-
changed = true;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
336
|
return changed;
|
|
266
337
|
}
|
|
267
338
|
function canonicalizeTree(root) {
|
|
@@ -280,9 +351,24 @@ function canonicalizeTree(root) {
|
|
|
280
351
|
|
|
281
352
|
// src/extension.ts
|
|
282
353
|
var EXTENSION_NAME = "lokascript-i18n";
|
|
283
|
-
function
|
|
354
|
+
function isHtmxBindableHxOn(name, prefixes, mc) {
|
|
355
|
+
for (const p of prefixes) {
|
|
356
|
+
if (name === p) return true;
|
|
357
|
+
if (name.startsWith(p) && name.charAt(p.length) === mc) return true;
|
|
358
|
+
}
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
function createExtension(htmx) {
|
|
362
|
+
const cfg = htmx?.config;
|
|
363
|
+
const prefix = cfg && "prefix" in cfg ? cfg.prefix ?? "" : "data-hx-";
|
|
364
|
+
const mc = cfg?.metaCharacter || ":";
|
|
365
|
+
const hxOnPrefixes = prefix ? ["hx-on", prefix + "on"] : ["hx-on"];
|
|
284
366
|
return {
|
|
285
|
-
|
|
367
|
+
init(internalAPI) {
|
|
368
|
+
const split = internalAPI?.HCON?.split;
|
|
369
|
+
if (typeof split === "function") setTriggerSpecSplitter(split.bind(internalAPI.HCON));
|
|
370
|
+
},
|
|
371
|
+
// htmx v4 (verified on 4.0.0): fires on each process() root
|
|
286
372
|
// before element init and hx-on binding.
|
|
287
373
|
htmx_before_process(elt) {
|
|
288
374
|
canonicalizeTree(elt);
|
|
@@ -291,6 +377,36 @@ function createExtension() {
|
|
|
291
377
|
htmx_before_process_node(elt) {
|
|
292
378
|
canonicalizeTree(elt);
|
|
293
379
|
},
|
|
380
|
+
// htmx v4 (verified on 4.0.0): fires per node carrying an
|
|
381
|
+
// hx-on-family attribute, cancelable — returning false makes htmx
|
|
382
|
+
// skip JS-binding that node entirely. This is executor mode's
|
|
383
|
+
// zero-mutation double-execution guard, decided from what the sweep
|
|
384
|
+
// RECORDED on the node (not from whether an executor is set now):
|
|
385
|
+
//
|
|
386
|
+
// - nothing claimed on this node → htmx proceeds as usual;
|
|
387
|
+
// - every hx-on attribute htmx would bind here is claimed → cancel,
|
|
388
|
+
// the authored attributes stay in the DOM;
|
|
389
|
+
// - the node also carries forms htmx must bind and the adapter never
|
|
390
|
+
// claims (the legacy composite `hx-on="event -> code"`, the
|
|
391
|
+
// `config.prefix` spelling, a custom meta character) → per-node
|
|
392
|
+
// cancellation would kill those too, so fall back to removing the
|
|
393
|
+
// claimed canonical attrs and let htmx bind the rest.
|
|
394
|
+
htmx_before_on_init(elt) {
|
|
395
|
+
const claimedNames = claimedAttrNames(elt);
|
|
396
|
+
if (claimedNames.size === 0) return void 0;
|
|
397
|
+
let unclaimedBindable = false;
|
|
398
|
+
for (const attr of Array.from(elt.attributes)) {
|
|
399
|
+
const name = attr.name;
|
|
400
|
+
if (claimedNames.has(name)) continue;
|
|
401
|
+
if (isHtmxBindableHxOn(name, hxOnPrefixes, mc)) {
|
|
402
|
+
unclaimedBindable = true;
|
|
403
|
+
break;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
if (!unclaimedBindable) return false;
|
|
407
|
+
removeClaimedCanonicalAttrs(elt);
|
|
408
|
+
return void 0;
|
|
409
|
+
},
|
|
294
410
|
// htmx v1/v2 fallback: single event dispatcher.
|
|
295
411
|
onEvent(name, evt) {
|
|
296
412
|
if (name !== "htmx:beforeProcessNode") return;
|
|
@@ -302,13 +418,23 @@ function createExtension() {
|
|
|
302
418
|
}
|
|
303
419
|
function registerWith(htmx) {
|
|
304
420
|
if (!htmx) return null;
|
|
305
|
-
const ext = createExtension();
|
|
421
|
+
const ext = createExtension(htmx);
|
|
306
422
|
if (typeof htmx.registerExtension === "function") {
|
|
307
|
-
htmx.registerExtension(EXTENSION_NAME, ext)
|
|
423
|
+
if (htmx.registerExtension(EXTENSION_NAME, ext) === false) {
|
|
424
|
+
if (typeof console !== "undefined") {
|
|
425
|
+
console.warn(
|
|
426
|
+
`[htmx-i18n] htmx.registerExtension("${EXTENSION_NAME}") returned false \u2014 the extension is not registered. If htmx.config.extensions (the <meta name="htmx-config"> allowlist) is set, add "${EXTENSION_NAME}" to it. Swapped-in content will not be canonicalized; executor-mode claims fall back to removing canonical hx-on:* attrs.`
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
setNeutralizeOnClaim(true);
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
setNeutralizeOnClaim(false);
|
|
308
433
|
return "v4";
|
|
309
434
|
}
|
|
310
435
|
if (typeof htmx.defineExtension === "function") {
|
|
311
436
|
htmx.defineExtension(EXTENSION_NAME, ext);
|
|
437
|
+
setNeutralizeOnClaim(true);
|
|
312
438
|
return "v2";
|
|
313
439
|
}
|
|
314
440
|
return null;
|
|
@@ -317,54 +443,73 @@ function installAutoSweep(doc = document) {
|
|
|
317
443
|
const sweep = () => {
|
|
318
444
|
canonicalizeTree(doc.body ?? doc.documentElement);
|
|
319
445
|
};
|
|
320
|
-
let
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
doc.addEventListener("DOMContentLoaded", onReady, { once: true });
|
|
324
|
-
removeDomListener = () => doc.removeEventListener("DOMContentLoaded", onReady);
|
|
325
|
-
} else {
|
|
446
|
+
let ready = doc.readyState === "complete";
|
|
447
|
+
let removeReadyListeners = null;
|
|
448
|
+
if (ready) {
|
|
326
449
|
sweep();
|
|
450
|
+
} else {
|
|
451
|
+
const onReady = () => {
|
|
452
|
+
if (ready) return;
|
|
453
|
+
ready = true;
|
|
454
|
+
removeReadyListeners?.();
|
|
455
|
+
sweep();
|
|
456
|
+
};
|
|
457
|
+
const win = doc.defaultView;
|
|
458
|
+
doc.addEventListener("DOMContentLoaded", onReady);
|
|
459
|
+
win?.addEventListener("load", onReady);
|
|
460
|
+
removeReadyListeners = () => {
|
|
461
|
+
doc.removeEventListener("DOMContentLoaded", onReady);
|
|
462
|
+
win?.removeEventListener("load", onReady);
|
|
463
|
+
};
|
|
327
464
|
}
|
|
328
465
|
const unsubscribeVocab = onVocabUpdate(() => {
|
|
329
|
-
if (
|
|
466
|
+
if (ready) sweep();
|
|
330
467
|
});
|
|
331
468
|
const unsubscribeBodyHooks = onBodyHooksChanged(() => {
|
|
332
|
-
if (
|
|
469
|
+
if (ready) sweep();
|
|
333
470
|
});
|
|
334
471
|
return () => {
|
|
335
|
-
|
|
472
|
+
removeReadyListeners?.();
|
|
336
473
|
unsubscribeVocab();
|
|
337
474
|
unsubscribeBodyHooks();
|
|
338
475
|
};
|
|
339
476
|
}
|
|
340
477
|
export {
|
|
341
478
|
EXTENSION_NAME,
|
|
479
|
+
TOP_LEVEL_COMMA_RE,
|
|
342
480
|
additionalAttributeSelectors,
|
|
343
481
|
attributeResolver,
|
|
344
482
|
autoDetectBodyHooks,
|
|
345
483
|
canonicalizeElement,
|
|
346
484
|
canonicalizeTree,
|
|
347
485
|
claimHxOnAttribute,
|
|
486
|
+
claimedAttrNames,
|
|
348
487
|
createExtension,
|
|
349
488
|
hasAnyVocab,
|
|
350
489
|
hasBodyExecutor,
|
|
351
490
|
hasBodyTranslator,
|
|
352
491
|
installAutoSweep,
|
|
353
492
|
installResolverMode,
|
|
493
|
+
isClaimed,
|
|
354
494
|
isLangRegistered,
|
|
355
495
|
isResolverMode,
|
|
356
496
|
langOf,
|
|
497
|
+
neutralizesOnClaim,
|
|
357
498
|
normLang,
|
|
358
499
|
onBodyHooksChanged,
|
|
359
500
|
onVocabUpdate,
|
|
360
501
|
register,
|
|
361
502
|
registerWith,
|
|
503
|
+
removeClaimedCanonicalAttrs,
|
|
362
504
|
resetBodyHooks,
|
|
363
505
|
resetRegistry,
|
|
364
506
|
setBodyExecutor,
|
|
365
507
|
setBodyTranslator,
|
|
508
|
+
setNeutralizeOnClaim,
|
|
366
509
|
setResolverMode,
|
|
510
|
+
setTriggerSpecSplitter,
|
|
367
511
|
translateTriggerValue,
|
|
368
|
-
vocabFor
|
|
512
|
+
vocabFor,
|
|
513
|
+
wasCreatedByAdapter
|
|
369
514
|
};
|
|
370
515
|
//# sourceMappingURL=index.js.map
|