@studiometa/ui 1.4.0 → 1.5.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/Action/Action.js +4 -2
- package/Action/Action.js.map +2 -2
- package/Action/ActionEvent.d.ts +6 -1
- package/Action/ActionEvent.js +42 -9
- package/Action/ActionEvent.js.map +2 -2
- package/Fetch/Fetch.d.ts +10 -0
- package/Fetch/Fetch.js +44 -24
- package/Fetch/Fetch.js.map +2 -2
- package/package.json +1 -1
package/Action/Action.js
CHANGED
|
@@ -30,8 +30,10 @@ class Action extends Base {
|
|
|
30
30
|
}
|
|
31
31
|
if (on) {
|
|
32
32
|
const { target, effect } = this.$options;
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
if (effect) {
|
|
34
|
+
const effectDefinition = target ? `${target}${ActionEvent.effectSeparator}${effect}` : effect;
|
|
35
|
+
this.__actionEvents.add(new ActionEvent(this, on, effectDefinition));
|
|
36
|
+
}
|
|
35
37
|
}
|
|
36
38
|
return this.__actionEvents;
|
|
37
39
|
}
|
package/Action/Action.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../packages/ui/Action/Action.ts"],
|
|
4
|
-
"sourcesContent": ["import { Base } from '@studiometa/js-toolkit';\nimport type { BaseProps, BaseConfig } from '@studiometa/js-toolkit';\nimport { ActionEvent } from './ActionEvent.js';\n\nexport interface ActionProps extends BaseProps {\n $options: {\n on: string;\n target: string;\n selector: string;\n effect: string;\n };\n}\n\n/**\n * Action class.\n * @link https://ui.studiometa.dev/-/components/Action/\n */\nexport class Action<T extends BaseProps = BaseProps> extends Base<ActionProps & T> {\n static config: BaseConfig = {\n name: 'Action',\n options: {\n on: {\n type: String,\n default: 'click',\n },\n target: String,\n effect: String,\n },\n };\n\n /**\n * @private\n */\n __actionEvents: Set<ActionEvent<Action>>;\n\n get actionEvents() {\n if (this.__actionEvents) {\n return this.__actionEvents;\n }\n\n const { on } = this.$options;\n this.__actionEvents = new Set();\n\n // @ts-ignore\n for (const attribute of this.$el.attributes) {\n if (attribute.name.includes('on:')) {\n const name = attribute.name.split('on:').pop();\n this.__actionEvents.add(new ActionEvent(this, name, attribute.value));\n }\n }\n\n if (on) {\n const { target, effect } = this.$options;\n const effectDefinition = target ? `${target}${ActionEvent.effectSeparator}${effect}` : effect;\n
|
|
5
|
-
"mappings": "AAAA,SAAS,YAAY;AAErB,SAAS,mBAAmB;AAerB,MAAM,eAAgD,KAAsB;AAAA,EACjF,OAAO,SAAqB;AAAA,IAC1B,MAAM;AAAA,IACN,SAAS;AAAA,MACP,IAAI;AAAA,QACF,MAAM;AAAA,QACN,SAAS;AAAA,MACX;AAAA,MACA,QAAQ;AAAA,MACR,QAAQ;AAAA,IACV;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA;AAAA,EAEA,IAAI,eAAe;AACjB,QAAI,KAAK,gBAAgB;AACvB,aAAO,KAAK;AAAA,IACd;AAEA,UAAM,EAAE,GAAG,IAAI,KAAK;AACpB,SAAK,iBAAiB,oBAAI,IAAI;AAG9B,eAAW,aAAa,KAAK,IAAI,YAAY;AAC3C,UAAI,UAAU,KAAK,SAAS,KAAK,GAAG;AAClC,cAAM,OAAO,UAAU,KAAK,MAAM,KAAK,EAAE,IAAI;AAC7C,aAAK,eAAe,IAAI,IAAI,YAAY,MAAM,MAAM,UAAU,KAAK,CAAC;AAAA,MACtE;AAAA,IACF;AAEA,QAAI,IAAI;AACN,YAAM,EAAE,QAAQ,OAAO,IAAI,KAAK;AAChC,
|
|
4
|
+
"sourcesContent": ["import { Base } from '@studiometa/js-toolkit';\nimport type { BaseProps, BaseConfig } from '@studiometa/js-toolkit';\nimport { ActionEvent } from './ActionEvent.js';\n\nexport interface ActionProps extends BaseProps {\n $options: {\n on: string;\n target: string;\n selector: string;\n effect: string;\n };\n}\n\n/**\n * Action class.\n * @link https://ui.studiometa.dev/-/components/Action/\n */\nexport class Action<T extends BaseProps = BaseProps> extends Base<ActionProps & T> {\n static config: BaseConfig = {\n name: 'Action',\n options: {\n on: {\n type: String,\n default: 'click',\n },\n target: String,\n effect: String,\n },\n };\n\n /**\n * @private\n */\n __actionEvents: Set<ActionEvent<Action>>;\n\n get actionEvents() {\n if (this.__actionEvents) {\n return this.__actionEvents;\n }\n\n const { on } = this.$options;\n this.__actionEvents = new Set();\n\n // @ts-ignore\n for (const attribute of this.$el.attributes) {\n if (attribute.name.includes('on:')) {\n const name = attribute.name.split('on:').pop();\n this.__actionEvents.add(new ActionEvent(this, name, attribute.value));\n }\n }\n\n if (on) {\n const { target, effect } = this.$options;\n if (effect) {\n const effectDefinition = target ? `${target}${ActionEvent.effectSeparator}${effect}` : effect;\n this.__actionEvents.add(new ActionEvent(this, on, effectDefinition));\n }\n }\n\n return this.__actionEvents;\n }\n\n /**\n * Mounted\n */\n mounted() {\n for (const actionEvent of this.actionEvents) {\n actionEvent.attachEvent();\n }\n }\n\n /**\n * Destroyed\n */\n destroyed() {\n for (const actionEvent of this.actionEvents) {\n actionEvent.detachEvent();\n }\n }\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,YAAY;AAErB,SAAS,mBAAmB;AAerB,MAAM,eAAgD,KAAsB;AAAA,EACjF,OAAO,SAAqB;AAAA,IAC1B,MAAM;AAAA,IACN,SAAS;AAAA,MACP,IAAI;AAAA,QACF,MAAM;AAAA,QACN,SAAS;AAAA,MACX;AAAA,MACA,QAAQ;AAAA,MACR,QAAQ;AAAA,IACV;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA;AAAA,EAEA,IAAI,eAAe;AACjB,QAAI,KAAK,gBAAgB;AACvB,aAAO,KAAK;AAAA,IACd;AAEA,UAAM,EAAE,GAAG,IAAI,KAAK;AACpB,SAAK,iBAAiB,oBAAI,IAAI;AAG9B,eAAW,aAAa,KAAK,IAAI,YAAY;AAC3C,UAAI,UAAU,KAAK,SAAS,KAAK,GAAG;AAClC,cAAM,OAAO,UAAU,KAAK,MAAM,KAAK,EAAE,IAAI;AAC7C,aAAK,eAAe,IAAI,IAAI,YAAY,MAAM,MAAM,UAAU,KAAK,CAAC;AAAA,MACtE;AAAA,IACF;AAEA,QAAI,IAAI;AACN,YAAM,EAAE,QAAQ,OAAO,IAAI,KAAK;AAChC,UAAI,QAAQ;AACV,cAAM,mBAAmB,SAAS,GAAG,MAAM,GAAG,YAAY,eAAe,GAAG,MAAM,KAAK;AACvF,aAAK,eAAe,IAAI,IAAI,YAAY,MAAM,IAAI,gBAAgB,CAAC;AAAA,MACrE;AAAA,IACF;AAEA,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAKA,UAAU;AACR,eAAW,eAAe,KAAK,cAAc;AAC3C,kBAAY,YAAY;AAAA,IAC1B;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,YAAY;AACV,eAAW,eAAe,KAAK,cAAc;AAC3C,kBAAY,YAAY;AAAA,IAC1B;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/Action/ActionEvent.d.ts
CHANGED
|
@@ -48,6 +48,11 @@ export declare class ActionEvent<T extends Base> {
|
|
|
48
48
|
* Get the targets object for the defined targets string.
|
|
49
49
|
*/
|
|
50
50
|
get targets(): Record<string, Base<import("@studiometa/js-toolkit").BaseProps>>[];
|
|
51
|
+
/**
|
|
52
|
+
* Get instances mounted on the action element.
|
|
53
|
+
* @internal
|
|
54
|
+
*/
|
|
55
|
+
get instances(): Map<string, Base<import("@studiometa/js-toolkit").BaseProps>>;
|
|
51
56
|
/**
|
|
52
57
|
* Handle the defined event and trigger the effect for each defined target.
|
|
53
58
|
*/
|
|
@@ -55,7 +60,7 @@ export declare class ActionEvent<T extends Base> {
|
|
|
55
60
|
/**
|
|
56
61
|
* Execute the effect for all targets.
|
|
57
62
|
*/
|
|
58
|
-
|
|
63
|
+
executeEffect(targets: Array<Record<string, Base>>, effect: Function, event: Event): void;
|
|
59
64
|
/**
|
|
60
65
|
* Bind the defined event to the given Action instance root element.
|
|
61
66
|
*/
|
package/Action/ActionEvent.js
CHANGED
|
@@ -68,13 +68,22 @@ class ActionEvent {
|
|
|
68
68
|
*/
|
|
69
69
|
get effect() {
|
|
70
70
|
const { effectDefinition } = this;
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
71
|
+
const keys = Array.from(this.instances.keys());
|
|
72
|
+
const cacheKey = effectDefinition + keys.join("");
|
|
73
|
+
if (!effectCache.has(cacheKey)) {
|
|
74
|
+
const args = [
|
|
75
|
+
"ctx",
|
|
76
|
+
"event",
|
|
77
|
+
"target",
|
|
78
|
+
"action",
|
|
79
|
+
"self",
|
|
80
|
+
"$el",
|
|
81
|
+
...keys,
|
|
82
|
+
`return ${effectDefinition}`
|
|
83
|
+
];
|
|
84
|
+
effectCache.set(cacheKey, new Function(...args));
|
|
76
85
|
}
|
|
77
|
-
return effectCache.get(
|
|
86
|
+
return effectCache.get(cacheKey);
|
|
78
87
|
}
|
|
79
88
|
/**
|
|
80
89
|
* Get the targets object for the defined targets string.
|
|
@@ -100,6 +109,20 @@ class ActionEvent {
|
|
|
100
109
|
}
|
|
101
110
|
return targets;
|
|
102
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Get instances mounted on the action element.
|
|
114
|
+
* @internal
|
|
115
|
+
*/
|
|
116
|
+
get instances() {
|
|
117
|
+
const { $el } = this.action;
|
|
118
|
+
const instances = /* @__PURE__ */ new Map();
|
|
119
|
+
for (const instance of getInstances()) {
|
|
120
|
+
if (instance.$el === $el) {
|
|
121
|
+
instances.set(instance.$config.name, instance);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return instances;
|
|
125
|
+
}
|
|
103
126
|
/**
|
|
104
127
|
* Handle the defined event and trigger the effect for each defined target.
|
|
105
128
|
*/
|
|
@@ -124,15 +147,25 @@ class ActionEvent {
|
|
|
124
147
|
* Execute the effect for all targets.
|
|
125
148
|
*/
|
|
126
149
|
executeEffect(targets, effect, event) {
|
|
150
|
+
const { action } = this;
|
|
127
151
|
for (const target of targets) {
|
|
128
152
|
try {
|
|
129
153
|
const [currentTarget] = Object.values(target).flat();
|
|
130
|
-
const
|
|
154
|
+
const args = [
|
|
155
|
+
target,
|
|
156
|
+
event,
|
|
157
|
+
currentTarget,
|
|
158
|
+
action,
|
|
159
|
+
action,
|
|
160
|
+
currentTarget.$el,
|
|
161
|
+
...this.instances.values()
|
|
162
|
+
];
|
|
163
|
+
const value = effect.apply(action.$el, args);
|
|
131
164
|
if (isFunction(value)) {
|
|
132
|
-
value(
|
|
165
|
+
value.apply(action.$el, args);
|
|
133
166
|
}
|
|
134
167
|
} catch (err) {
|
|
135
|
-
|
|
168
|
+
action.$warn(err);
|
|
136
169
|
}
|
|
137
170
|
}
|
|
138
171
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../packages/ui/Action/ActionEvent.ts"],
|
|
4
|
-
"sourcesContent": ["import { getInstances } from '@studiometa/js-toolkit';\nimport type { Base } from '@studiometa/js-toolkit';\nimport { isFunction } from '@studiometa/js-toolkit/utils';\n\n/**\n * Extract component name and an optional additional selector from a string.\n
|
|
5
|
-
"mappings": "AAAA,SAAS,oBAAoB;AAE7B,SAAS,kBAAkB;
|
|
4
|
+
"sourcesContent": ["import { getInstances } from '@studiometa/js-toolkit';\nimport type { Base } from '@studiometa/js-toolkit';\nimport { isFunction } from '@studiometa/js-toolkit/utils';\n\n/**\n * Extract component name and an optional additional selector from a string.\n */\nconst TARGET_REGEX = /([a-zA-Z]+)(\\((.*)\\))?/;\n\nconst effectCache = new Map<string, Function>();\n\nexport type Modifiers = 'prevent' | 'stop' | 'once' | 'passive' | 'capture' | 'debounce';\n\nexport class ActionEvent<T extends Base> {\n static modifierSeparator = '.';\n static targetSeparator = ' ';\n static effectSeparator = '->';\n\n /**\n * Timer for debouncing event handling.\n */\n private debounceTimer?: number;\n\n /**\n * The Action instance.\n */\n action: T;\n\n /**\n * The event to listen to.\n */\n event: string;\n\n /**\n * The modifiers to apply to the event.\n */\n modifiers: Modifiers[];\n\n /**\n * The debounce delay in milliseconds.\n */\n debounceDelay: number = 100;\n\n /**\n * Target definition.\n * Ex: `Target Target(.selector)`.\n */\n targetDefinition: string;\n\n /**\n * The content of the effect callback function.\n */\n effectDefinition: string;\n\n /**\n * Class constructor.\n * @param {T} action The parent Action instance.\n * @param {string} eventDefinition The event with its modifiers: `click.prevent.stop`\n * @param {string} effectDefinition The target and effect definition: `Target(.selector)->target.$destroy()`\n */\n constructor(action: T, eventDefinition: string, effectDefinition: string) {\n this.action = action;\n const [event, ...modifiers] = eventDefinition.split(ActionEvent.modifierSeparator);\n this.event = event;\n\n // Process modifiers and extract debounce delay if present\n const processedModifiers: Modifiers[] = [];\n for (const modifier of modifiers) {\n if (modifier.startsWith('debounce')) {\n processedModifiers.push('debounce');\n this.debounceDelay = parseInt(modifier.replace('debounce', '') || '100');\n } else {\n processedModifiers.push(modifier as Modifiers);\n }\n }\n\n this.modifiers = processedModifiers;\n\n let effect = effectDefinition;\n let targetDefinition = '';\n\n if (effect.includes(ActionEvent.effectSeparator)) {\n [targetDefinition, effect] = effect.split(ActionEvent.effectSeparator);\n }\n\n this.targetDefinition = targetDefinition.trim();\n this.effectDefinition = effect.trim();\n }\n\n /**\n * Get the generated function for the defined effect.\n */\n get effect() {\n const { effectDefinition } = this;\n const keys = Array.from(this.instances.keys());\n const cacheKey = effectDefinition + keys.join('');\n\n if (!effectCache.has(cacheKey)) {\n const args = [\n 'ctx',\n 'event',\n 'target',\n 'action',\n 'self',\n '$el',\n ...keys,\n `return ${effectDefinition}`,\n ];\n effectCache.set(cacheKey, new Function(...args));\n }\n\n return effectCache.get(cacheKey) as Function;\n }\n\n /**\n * Get the targets object for the defined targets string.\n */\n get targets() {\n const { targetDefinition } = this;\n\n if (!targetDefinition) {\n return [{ Action: this.action }];\n }\n\n // Extract component's names and selectors.\n const parts = targetDefinition.split(ActionEvent.targetSeparator).map((part) => {\n const [, name, , selector] = part.match(TARGET_REGEX) ?? [];\n return [name, selector];\n });\n\n const targets = [] as Array<Record<string, Base>>;\n\n for (const instance of getInstances()) {\n const { name } = instance.__config;\n\n for (const part of parts) {\n const shouldPush =\n part[0] === name && (!part[1] || (part[1] && instance.$el.matches(part[1])));\n if (shouldPush) {\n targets.push({ [instance.__config.name]: instance });\n }\n }\n }\n\n return targets;\n }\n\n /**\n * Get instances mounted on the action element.\n * @internal\n */\n get instances() {\n const { $el } = this.action;\n const instances = new Map<string, Base>();\n for (const instance of getInstances()) {\n if (instance.$el === $el) {\n instances.set(instance.$config.name, instance);\n }\n }\n\n return instances;\n }\n\n /**\n * Handle the defined event and trigger the effect for each defined target.\n */\n handleEvent(event: Event) {\n const { targets, effect, modifiers } = this;\n\n if (modifiers.includes('prevent')) {\n event.preventDefault();\n }\n\n if (modifiers.includes('stop')) {\n event.stopPropagation();\n }\n\n if (modifiers.includes('debounce')) {\n clearTimeout(this.debounceTimer);\n this.debounceTimer = window.setTimeout(() => {\n this.executeEffect(targets, effect, event);\n }, this.debounceDelay);\n } else {\n this.executeEffect(targets, effect, event);\n }\n }\n\n /**\n * Execute the effect for all targets.\n */\n executeEffect(targets: Array<Record<string, Base>>, effect: Function, event: Event) {\n const { action } = this;\n\n for (const target of targets) {\n try {\n const [currentTarget] = Object.values(target).flat();\n const args = [\n target,\n event,\n currentTarget,\n action,\n action,\n currentTarget.$el,\n ...this.instances.values(),\n ];\n const value = effect.apply(action.$el, args);\n if (isFunction(value)) {\n value.apply(action.$el, args);\n }\n } catch (err) {\n action.$warn(err);\n }\n }\n }\n\n /**\n * Bind the defined event to the given Action instance root element.\n */\n attachEvent() {\n const { event, modifiers } = this;\n this.action.$el.addEventListener(event, this, {\n capture: modifiers.includes('capture'),\n once: modifiers.includes('once'),\n passive: modifiers.includes('passive'),\n });\n }\n\n /**\n * Unbind the event from the given Action instance root element.\n */\n detachEvent() {\n clearTimeout(this.debounceTimer);\n this.action.$el.removeEventListener(this.event, this);\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/Fetch/Fetch.d.ts
CHANGED
|
@@ -37,6 +37,7 @@ export declare class Fetch<T extends BaseProps = BaseProps> extends Base<T & Fet
|
|
|
37
37
|
readonly UPDATE: "fetch-update";
|
|
38
38
|
readonly AFTER_UPDATE: "fetch-update-after";
|
|
39
39
|
readonly ERROR: "fetch-error";
|
|
40
|
+
readonly ABORT: "fetch-abort";
|
|
40
41
|
};
|
|
41
42
|
/**
|
|
42
43
|
* Fetch modes enum.
|
|
@@ -71,6 +72,11 @@ export declare class Fetch<T extends BaseProps = BaseProps> extends Base<T & Fet
|
|
|
71
72
|
* @internal
|
|
72
73
|
*/
|
|
73
74
|
__abortController: AbortController;
|
|
75
|
+
/**
|
|
76
|
+
* Client.
|
|
77
|
+
* @internal
|
|
78
|
+
*/
|
|
79
|
+
__client: typeof fetch;
|
|
74
80
|
/**
|
|
75
81
|
* The client used for the fetch request.
|
|
76
82
|
*/
|
|
@@ -130,4 +136,8 @@ export declare class Fetch<T extends BaseProps = BaseProps> extends Base<T & Fet
|
|
|
130
136
|
* Handle errors.
|
|
131
137
|
*/
|
|
132
138
|
error(url: URL, requestInit: RequestInit, error: Error): void;
|
|
139
|
+
/**
|
|
140
|
+
* Abort the current request.
|
|
141
|
+
*/
|
|
142
|
+
abort(reason?: any): void;
|
|
133
143
|
}
|
package/Fetch/Fetch.js
CHANGED
|
@@ -13,7 +13,8 @@ class Fetch extends Base {
|
|
|
13
13
|
BEFORE_UPDATE: "fetch-update-before",
|
|
14
14
|
UPDATE: "fetch-update",
|
|
15
15
|
AFTER_UPDATE: "fetch-update-after",
|
|
16
|
-
ERROR: "fetch-error"
|
|
16
|
+
ERROR: "fetch-error",
|
|
17
|
+
ABORT: "fetch-abort"
|
|
17
18
|
};
|
|
18
19
|
/**
|
|
19
20
|
* Fetch modes enum.
|
|
@@ -65,11 +66,16 @@ class Fetch extends Base {
|
|
|
65
66
|
* @internal
|
|
66
67
|
*/
|
|
67
68
|
__abortController = new AbortController();
|
|
69
|
+
/**
|
|
70
|
+
* Client.
|
|
71
|
+
* @internal
|
|
72
|
+
*/
|
|
73
|
+
__client;
|
|
68
74
|
/**
|
|
69
75
|
* The client used for the fetch request.
|
|
70
76
|
*/
|
|
71
77
|
get client() {
|
|
72
|
-
return window.fetch.bind(window);
|
|
78
|
+
return this.__client ??= window.fetch.bind(window);
|
|
73
79
|
}
|
|
74
80
|
/**
|
|
75
81
|
* The URL to use for the request.
|
|
@@ -102,17 +108,18 @@ class Fetch extends Base {
|
|
|
102
108
|
...headers
|
|
103
109
|
}
|
|
104
110
|
};
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
normalizedRequestInit.headers[header.dataset.name] = header.value;
|
|
109
|
-
}
|
|
111
|
+
for (const header of headerRefs) {
|
|
112
|
+
if (header.dataset.name && header.value) {
|
|
113
|
+
normalizedRequestInit.headers[header.dataset.name] = header.value;
|
|
110
114
|
}
|
|
111
115
|
}
|
|
112
116
|
if (isForm) {
|
|
113
117
|
const form = $el;
|
|
114
|
-
|
|
115
|
-
normalizedRequestInit.
|
|
118
|
+
const method = form.method.toLowerCase();
|
|
119
|
+
normalizedRequestInit.method = method;
|
|
120
|
+
if (method === "post") {
|
|
121
|
+
normalizedRequestInit.body = new FormData(form);
|
|
122
|
+
}
|
|
116
123
|
}
|
|
117
124
|
return normalizedRequestInit;
|
|
118
125
|
}
|
|
@@ -173,9 +180,13 @@ class Fetch extends Base {
|
|
|
173
180
|
*/
|
|
174
181
|
async fetch(url, requestInit = {}) {
|
|
175
182
|
const { FETCH_EVENTS } = this.constructor;
|
|
176
|
-
this.$emit(FETCH_EVENTS.BEFORE_FETCH, this, url, requestInit);
|
|
183
|
+
this.$emit(FETCH_EVENTS.BEFORE_FETCH, { instance: this, url, requestInit });
|
|
177
184
|
this.__abortController.abort();
|
|
178
|
-
|
|
185
|
+
const newController = new AbortController();
|
|
186
|
+
newController.signal.addEventListener("abort", () => {
|
|
187
|
+
this.$emit(FETCH_EVENTS.ABORT, { instance: this, url, requestInit, reason: newController.signal.reason });
|
|
188
|
+
});
|
|
189
|
+
this.__abortController = newController;
|
|
179
190
|
const init = {
|
|
180
191
|
...this.requestInit,
|
|
181
192
|
...requestInit,
|
|
@@ -183,16 +194,20 @@ class Fetch extends Base {
|
|
|
183
194
|
...this.requestInit.headers,
|
|
184
195
|
...requestInit.headers
|
|
185
196
|
},
|
|
186
|
-
signal:
|
|
197
|
+
signal: newController.signal
|
|
187
198
|
};
|
|
188
199
|
this.$log("fetch", url, init);
|
|
189
|
-
this.$emit(FETCH_EVENTS.FETCH, this, url, requestInit);
|
|
200
|
+
this.$emit(FETCH_EVENTS.FETCH, { instance: this, url, requestInit: init });
|
|
190
201
|
try {
|
|
191
|
-
const
|
|
192
|
-
|
|
202
|
+
const response = await this.client(url, init);
|
|
203
|
+
if (!response.ok) {
|
|
204
|
+
throw new Error(`Fetch failed with status ${response.status}`);
|
|
205
|
+
}
|
|
206
|
+
const content = await response.text();
|
|
207
|
+
this.$emit(FETCH_EVENTS.AFTER_FETCH, { instance: this, url, requestInit, content });
|
|
193
208
|
this.update(url, init, content);
|
|
194
209
|
} catch (error) {
|
|
195
|
-
this.$emit(FETCH_EVENTS.AFTER_FETCH, this, url, requestInit);
|
|
210
|
+
this.$emit(FETCH_EVENTS.AFTER_FETCH, { instance: this, url, requestInit, error });
|
|
196
211
|
this.error(url, init, error);
|
|
197
212
|
}
|
|
198
213
|
}
|
|
@@ -204,10 +219,8 @@ class Fetch extends Base {
|
|
|
204
219
|
const { FETCH_MODES } = this.constructor;
|
|
205
220
|
const { mode, selector } = this.$options;
|
|
206
221
|
for (const newElement of fragment.querySelectorAll(selector)) {
|
|
207
|
-
const oldElement = document.
|
|
208
|
-
|
|
209
|
-
);
|
|
210
|
-
if (!oldElement) {
|
|
222
|
+
const oldElement = newElement.id && document.getElementById(newElement.id);
|
|
223
|
+
if (!oldElement || oldElement === newElement) {
|
|
211
224
|
continue;
|
|
212
225
|
}
|
|
213
226
|
const oldScripts = getScripts(oldElement);
|
|
@@ -239,7 +252,7 @@ class Fetch extends Base {
|
|
|
239
252
|
const { FETCH_EVENTS } = this.constructor;
|
|
240
253
|
const { history } = this.$options;
|
|
241
254
|
this.$log("content", url, content);
|
|
242
|
-
this.$emit(FETCH_EVENTS.BEFORE_UPDATE, this, url, requestInit, content);
|
|
255
|
+
this.$emit(FETCH_EVENTS.BEFORE_UPDATE, { instance: this, url, requestInit, content });
|
|
243
256
|
const fragment = this.__domParser.parseFromString(content, "text/html");
|
|
244
257
|
if (history) {
|
|
245
258
|
if (requestInit?.headers?.[this.__headerNames.X_TRIGGERED_BY] !== "popstate") {
|
|
@@ -251,7 +264,7 @@ class Fetch extends Base {
|
|
|
251
264
|
}
|
|
252
265
|
});
|
|
253
266
|
}
|
|
254
|
-
this.$emit(FETCH_EVENTS.UPDATE, this, url, requestInit, fragment);
|
|
267
|
+
this.$emit(FETCH_EVENTS.UPDATE, { instance: this, url, requestInit, fragment });
|
|
255
268
|
if (isFunction(document.startViewTransition)) {
|
|
256
269
|
await document.startViewTransition(() => {
|
|
257
270
|
this.__updateDOM(fragment);
|
|
@@ -259,14 +272,21 @@ class Fetch extends Base {
|
|
|
259
272
|
} else {
|
|
260
273
|
this.__updateDOM(fragment);
|
|
261
274
|
}
|
|
262
|
-
this.$emit(FETCH_EVENTS.AFTER_UPDATE, this, url, requestInit, fragment);
|
|
275
|
+
this.$emit(FETCH_EVENTS.AFTER_UPDATE, { instance: this, url, requestInit, fragment });
|
|
263
276
|
}
|
|
264
277
|
/**
|
|
265
278
|
* Handle errors.
|
|
266
279
|
*/
|
|
267
280
|
error(url, requestInit, error) {
|
|
281
|
+
if (error.name === "AbortError") return;
|
|
268
282
|
this.$log("error", url, requestInit, error);
|
|
269
|
-
this.$emit(this.constructor.FETCH_EVENTS.ERROR, this, url, requestInit, error);
|
|
283
|
+
this.$emit(this.constructor.FETCH_EVENTS.ERROR, { instance: this, url, requestInit, error });
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Abort the current request.
|
|
287
|
+
*/
|
|
288
|
+
abort(reason) {
|
|
289
|
+
this.__abortController.abort(reason);
|
|
270
290
|
}
|
|
271
291
|
}
|
|
272
292
|
export {
|
package/Fetch/Fetch.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../packages/ui/Fetch/Fetch.ts"],
|
|
4
|
-
"sourcesContent": ["import { Base, type BaseConfig, type BaseProps } from '@studiometa/js-toolkit';\nimport { domScheduler, historyPush, isFunction } from '@studiometa/js-toolkit/utils';\nimport morphdom from 'morphdom';\nimport { adoptNewScripts, getScripts } from './utils.js';\n\nexport interface FetchProps extends BaseProps {\n $el: HTMLAnchorElement | HTMLFormElement;\n $refs: {\n headers: HTMLInputElement[];\n };\n $options: {\n history: boolean;\n requestInit: RequestInit;\n headers: Record<string, string>;\n mode: 'replace' | 'prepend' | 'append' | 'morph';\n selector: string;\n };\n}\n\nexport type FetchConstructor<T extends Fetch = Fetch> = {\n new (...args: any[]): T;\n prototype: Fetch;\n} & Pick<typeof Fetch, keyof typeof Fetch>;\n\n/**\n * Fetch class.\n * @link https://ui.studiometa.dev/-/components/Fetch/\n */\nexport class Fetch<T extends BaseProps = BaseProps> extends Base<T & FetchProps> {\n /**\n * Declare the `this.constructor` type\n * @link https://github.com/microsoft/TypeScript/issues/3841#issuecomment-2381594311\n */\n declare ['constructor']: FetchConstructor;\n\n /**\n * Fetch events enum.\n */\n static FETCH_EVENTS = {\n BEFORE_FETCH: 'fetch-before',\n FETCH: 'fetch-fetch',\n AFTER_FETCH: 'fetch-after',\n BEFORE_UPDATE: 'fetch-update-before',\n UPDATE: 'fetch-update',\n AFTER_UPDATE: 'fetch-update-after',\n ERROR: 'fetch-error',\n } as const;\n\n /**\n * Fetch modes enum.\n */\n static FETCH_MODES = {\n REPLACE: 'replace',\n PREPEND: 'prepend',\n APPEND: 'append',\n MORPH: 'morph',\n } as const;\n\n /**\n * Config.\n */\n static config: BaseConfig = {\n name: 'Fetch',\n emits: Object.values(this.FETCH_EVENTS),\n refs: ['headers[]'],\n options: {\n history: Boolean,\n mode: {\n type: String,\n default: this.FETCH_MODES.REPLACE,\n },\n requestInit: Object,\n headers: Object,\n selector: {\n type: String,\n default: '[id]',\n },\n },\n };\n\n /**\n * Header names used by the requestInit property.\n * @internal\n */\n __headerNames = {\n ACCEPT: 'accept',\n X_REQUESTED_BY: 'x-requested-by',\n X_TRIGGERED_BY: 'x-triggered-by',\n USER_AGENT: 'user-agent',\n } as const;\n\n /**\n * DOM Parser to parse the new content to be injected.\n * @internal\n */\n __domParser = new DOMParser();\n\n /**\n * Abort controller to prevent multiple simultaneous fetches.\n * @internal\n */\n __abortController = new AbortController();\n\n /**\n * The client used for the fetch request.\n */\n get client(): typeof fetch {\n return window.fetch.bind(window);\n }\n\n /**\n * The URL to use for the request.\n */\n get url(): URL {\n const { $el, isForm } = this;\n\n if (isForm) {\n const { action, method } = this.$el as HTMLFormElement;\n const url = new URL(action);\n\n if (method.toLowerCase() === 'get') {\n // @ts-expect-error URLSearchParams accepts FormData as parameter in the browser.\n url.search = new URLSearchParams(new FormData($el)).toString();\n }\n\n return url;\n }\n\n return new URL($el.href);\n }\n\n /**\n * Option for the fetch request.\n */\n get requestInit(): RequestInit {\n const { __headerNames: headerNames, isForm, $el, $options, $refs } = this;\n const { requestInit, headers } = $options;\n const { headers: headerRefs } = $refs;\n const requestedBy = '@studiometa/ui/Fetch';\n\n const normalizedRequestInit = {\n ...requestInit,\n headers: {\n [headerNames.USER_AGENT]: `${navigator.userAgent} ${requestedBy}`,\n ...requestInit.headers,\n ...headers,\n },\n };\n\n if (headerRefs) {\n for (const header of headerRefs) {\n if (header.dataset.name && header.value) {\n normalizedRequestInit.headers[header.dataset.name] = header.value;\n }\n }\n }\n\n if (isForm) {\n const form = $el as HTMLFormElement;\n normalizedRequestInit.method = form.method;\n normalizedRequestInit.body = new FormData(form);\n }\n\n return normalizedRequestInit;\n }\n\n /**\n * Is the root element a link?\n */\n get isLink() {\n return this.$el instanceof HTMLAnchorElement;\n }\n\n /**\n * Is the root element a form?\n */\n get isForm() {\n return this.$el instanceof HTMLFormElement;\n }\n\n /**\n * Emit bubbling events.\n * @inheritdoc\n */\n $emit(event: string, ...args: unknown[]) {\n const e = new CustomEvent(event, { detail: args, bubbles: true });\n return super.$emit(e, ...args);\n }\n\n /**\n * If root element is a link, prevent its default behavior and fetch its URL.\n */\n onClick({ event }: { event: MouseEvent }) {\n if (!this.isLink) return;\n\n if (\n !event.ctrlKey &&\n !event.shiftKey &&\n !event.altKey &&\n !event.metaKey &&\n event.button === 0 &&\n this.$el.target !== '_blank'\n ) {\n event.preventDefault();\n this.fetch(this.url, this.requestInit);\n }\n }\n\n /**\n * If root element is a form, prevent its default behavior on submit and fetch its action\n * following the `method` attribute and with the form's data.\n */\n onSubmit({ event }: { event: SubmitEvent }) {\n if (!this.isForm) return;\n\n if (this.$el.target !== '_blank') {\n event.preventDefault();\n this.fetch(this.url, this.requestInit);\n }\n }\n\n /**\n * Update content on history back/forward navigation.\n */\n onWindowPopstate() {\n if (!this.$options.history) return;\n\n this.fetch(new URL(window.location.href), {\n headers: {\n [this.__headerNames.X_TRIGGERED_BY]: 'popstate',\n },\n });\n }\n\n /**\n * Fetch given url.\n */\n async fetch(url: URL, requestInit: RequestInit = {}) {\n const { FETCH_EVENTS } = this.constructor;\n this.$emit(FETCH_EVENTS.BEFORE_FETCH, this, url, requestInit);\n\n this.__abortController.abort();\n this.__abortController = new AbortController();\n const init = {\n ...this.requestInit,\n ...requestInit,\n headers: {\n ...this.requestInit.headers,\n ...requestInit.headers,\n },\n signal: this.__abortController.signal,\n };\n\n this.$log('fetch', url, init);\n this.$emit(FETCH_EVENTS.FETCH, this, url, requestInit);\n\n try {\n const content = await this.client(url, init).then((response) => response.text());\n this.$emit(FETCH_EVENTS.AFTER_FETCH, this, url, requestInit, content);\n this.update(url, init, content);\n } catch (error) {\n this.$emit(FETCH_EVENTS.AFTER_FETCH, this, url, requestInit);\n this.error(url, init, error);\n }\n }\n\n /**\n * Update the DOM with new content from the fetched HTML.\n * @internal\n */\n __updateDOM(fragment: Document) {\n const { FETCH_MODES } = this.constructor;\n const { mode, selector } = this.$options;\n\n // @ts-expect-error querySelectorAll is iterable in the browser\n for (const newElement of fragment.querySelectorAll<HTMLElement>(selector)) {\n const oldElement: HTMLElement = document.querySelector<HTMLElement>(\n `[id=\"${newElement.id}\"]`,\n );\n\n if (!oldElement) {\n continue;\n }\n\n const oldScripts = getScripts(oldElement);\n\n switch (mode) {\n case FETCH_MODES.APPEND:\n oldElement.append(...newElement.childNodes);\n adoptNewScripts(getScripts(oldElement), oldScripts);\n break;\n case FETCH_MODES.PREPEND:\n oldElement.prepend(...newElement.childNodes);\n adoptNewScripts(getScripts(oldElement), oldScripts);\n break;\n case FETCH_MODES.MORPH:\n morphdom(oldElement, newElement);\n adoptNewScripts(getScripts(oldElement), oldScripts);\n break;\n case FETCH_MODES.REPLACE:\n default:\n oldElement.replaceWith(newElement);\n adoptNewScripts(getScripts(newElement), oldScripts);\n break;\n }\n }\n }\n\n /**\n * Dispatch the contents to update to their matching FrameTarget.\n */\n async update(url: URL, requestInit: RequestInit, content: string) {\n const { FETCH_EVENTS } = this.constructor;\n const { history } = this.$options;\n\n this.$log('content', url, content);\n this.$emit(FETCH_EVENTS.BEFORE_UPDATE, this, url, requestInit, content);\n\n const fragment = this.__domParser.parseFromString(content, 'text/html');\n\n if (history) {\n if (requestInit?.headers?.[this.__headerNames.X_TRIGGERED_BY] !== 'popstate') {\n historyPush({ path: url.pathname, search: url.searchParams });\n }\n domScheduler.write(() => {\n if (fragment.title) {\n document.title = fragment.title;\n }\n });\n }\n\n this.$emit(FETCH_EVENTS.UPDATE, this, url, requestInit, fragment);\n\n if (isFunction(document.startViewTransition)) {\n await document.startViewTransition(() => {\n this.__updateDOM(fragment);\n }).ready;\n } else {\n this.__updateDOM(fragment);\n }\n\n this.$emit(FETCH_EVENTS.AFTER_UPDATE, this, url, requestInit, fragment);\n }\n\n /**\n * Handle errors.\n */\n error(url: URL, requestInit: RequestInit, error: Error) {\n this.$log('error', url, requestInit, error);\n this.$emit(this.constructor.FETCH_EVENTS.ERROR, this, url, requestInit, error);\n }\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,YAA6C;AACtD,SAAS,cAAc,aAAa,kBAAkB;AACtD,OAAO,cAAc;AACrB,SAAS,iBAAiB,kBAAkB;AAyBrC,MAAM,cAA+C,KAAqB;AAAA;AAAA;AAAA;AAAA,EAU/E,OAAO,eAAe;AAAA,IACpB,cAAc;AAAA,IACd,OAAO;AAAA,IACP,aAAa;AAAA,IACb,eAAe;AAAA,IACf,QAAQ;AAAA,IACR,cAAc;AAAA,IACd,OAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,OAAO,cAAc;AAAA,IACnB,SAAS;AAAA,IACT,SAAS;AAAA,IACT,QAAQ;AAAA,IACR,OAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,OAAO,SAAqB;AAAA,IAC1B,MAAM;AAAA,IACN,OAAO,OAAO,OAAO,KAAK,YAAY;AAAA,IACtC,MAAM,CAAC,WAAW;AAAA,IAClB,SAAS;AAAA,MACP,SAAS;AAAA,MACT,MAAM;AAAA,QACJ,MAAM;AAAA,QACN,SAAS,KAAK,YAAY;AAAA,MAC5B;AAAA,MACA,aAAa;AAAA,MACb,SAAS;AAAA,MACT,UAAU;AAAA,QACR,MAAM;AAAA,QACN,SAAS;AAAA,MACX;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,gBAAgB;AAAA,IACd,QAAQ;AAAA,IACR,gBAAgB;AAAA,IAChB,gBAAgB;AAAA,IAChB,YAAY;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,cAAc,IAAI,UAAU;AAAA;AAAA;AAAA;AAAA;AAAA,EAM5B,oBAAoB,IAAI,gBAAgB;AAAA;AAAA;AAAA;AAAA,
|
|
4
|
+
"sourcesContent": ["import { Base, type BaseConfig, type BaseProps } from '@studiometa/js-toolkit';\nimport { domScheduler, historyPush, isFunction } from '@studiometa/js-toolkit/utils';\nimport morphdom from 'morphdom';\nimport { adoptNewScripts, getScripts } from './utils.js';\n\nexport interface FetchProps extends BaseProps {\n $el: HTMLAnchorElement | HTMLFormElement;\n $refs: {\n headers: HTMLInputElement[];\n };\n $options: {\n history: boolean;\n requestInit: RequestInit;\n headers: Record<string, string>;\n mode: 'replace' | 'prepend' | 'append' | 'morph';\n selector: string;\n };\n}\n\nexport type FetchConstructor<T extends Fetch = Fetch> = {\n new (...args: any[]): T;\n prototype: Fetch;\n} & Pick<typeof Fetch, keyof typeof Fetch>;\n\n/**\n * Fetch class.\n * @link https://ui.studiometa.dev/-/components/Fetch/\n */\nexport class Fetch<T extends BaseProps = BaseProps> extends Base<T & FetchProps> {\n /**\n * Declare the `this.constructor` type\n * @link https://github.com/microsoft/TypeScript/issues/3841#issuecomment-2381594311\n */\n declare ['constructor']: FetchConstructor;\n\n /**\n * Fetch events enum.\n */\n static FETCH_EVENTS = {\n BEFORE_FETCH: 'fetch-before',\n FETCH: 'fetch-fetch',\n AFTER_FETCH: 'fetch-after',\n BEFORE_UPDATE: 'fetch-update-before',\n UPDATE: 'fetch-update',\n AFTER_UPDATE: 'fetch-update-after',\n ERROR: 'fetch-error',\n ABORT: 'fetch-abort',\n } as const;\n\n /**\n * Fetch modes enum.\n */\n static FETCH_MODES = {\n REPLACE: 'replace',\n PREPEND: 'prepend',\n APPEND: 'append',\n MORPH: 'morph',\n } as const;\n\n /**\n * Config.\n */\n static config: BaseConfig = {\n name: 'Fetch',\n emits: Object.values(this.FETCH_EVENTS),\n refs: ['headers[]'],\n options: {\n history: Boolean,\n mode: {\n type: String,\n default: this.FETCH_MODES.REPLACE,\n },\n requestInit: Object,\n headers: Object,\n selector: {\n type: String,\n default: '[id]',\n },\n },\n };\n\n /**\n * Header names used by the requestInit property.\n * @internal\n */\n __headerNames = {\n ACCEPT: 'accept',\n X_REQUESTED_BY: 'x-requested-by',\n X_TRIGGERED_BY: 'x-triggered-by',\n USER_AGENT: 'user-agent',\n } as const;\n\n /**\n * DOM Parser to parse the new content to be injected.\n * @internal\n */\n __domParser = new DOMParser();\n\n /**\n * Abort controller to prevent multiple simultaneous fetches.\n * @internal\n */\n __abortController = new AbortController();\n\n /**\n * Client.\n * @internal\n */\n __client: typeof fetch;\n\n /**\n * The client used for the fetch request.\n */\n get client(): typeof fetch {\n return (this.__client ??= window.fetch.bind(window));\n }\n\n /**\n * The URL to use for the request.\n */\n get url(): URL {\n const { $el, isForm } = this;\n\n if (isForm) {\n const { action, method } = this.$el as HTMLFormElement;\n const url = new URL(action);\n\n if (method.toLowerCase() === 'get') {\n // @ts-expect-error URLSearchParams accepts FormData as parameter in the browser.\n url.search = new URLSearchParams(new FormData($el)).toString();\n }\n\n return url;\n }\n\n return new URL($el.href);\n }\n\n /**\n * Option for the fetch request.\n */\n get requestInit(): RequestInit {\n const { __headerNames: headerNames, isForm, $el, $options, $refs } = this;\n const { requestInit, headers } = $options;\n const { headers: headerRefs } = $refs;\n const requestedBy = '@studiometa/ui/Fetch';\n\n const normalizedRequestInit = {\n ...requestInit,\n headers: {\n [headerNames.USER_AGENT]: `${navigator.userAgent} ${requestedBy}`,\n ...requestInit.headers,\n ...headers,\n },\n };\n\n for (const header of headerRefs) {\n if (header.dataset.name && header.value) {\n normalizedRequestInit.headers[header.dataset.name] = header.value;\n }\n }\n\n if (isForm) {\n const form = $el as HTMLFormElement;\n const method = form.method.toLowerCase();\n normalizedRequestInit.method = method;\n if (method === 'post') {\n normalizedRequestInit.body = new FormData(form);\n }\n }\n\n return normalizedRequestInit;\n }\n\n /**\n * Is the root element a link?\n */\n get isLink() {\n return this.$el instanceof HTMLAnchorElement;\n }\n\n /**\n * Is the root element a form?\n */\n get isForm() {\n return this.$el instanceof HTMLFormElement;\n }\n\n /**\n * Emit bubbling events.\n * @inheritdoc\n */\n $emit(event: string, ...args: unknown[]) {\n const e = new CustomEvent(event, { detail: args, bubbles: true });\n return super.$emit(e, ...args);\n }\n\n /**\n * If root element is a link, prevent its default behavior and fetch its URL.\n */\n onClick({ event }: { event: MouseEvent }) {\n if (!this.isLink) return;\n\n if (\n !event.ctrlKey &&\n !event.shiftKey &&\n !event.altKey &&\n !event.metaKey &&\n event.button === 0 &&\n this.$el.target !== '_blank'\n ) {\n event.preventDefault();\n this.fetch(this.url, this.requestInit);\n }\n }\n\n /**\n * If root element is a form, prevent its default behavior on submit and fetch its action\n * following the `method` attribute and with the form's data.\n */\n onSubmit({ event }: { event: SubmitEvent }) {\n if (!this.isForm) return;\n\n if (this.$el.target !== '_blank') {\n event.preventDefault();\n this.fetch(this.url, this.requestInit);\n }\n }\n\n /**\n * Update content on history back/forward navigation.\n */\n onWindowPopstate() {\n if (!this.$options.history) return;\n\n this.fetch(new URL(window.location.href), {\n headers: {\n [this.__headerNames.X_TRIGGERED_BY]: 'popstate',\n },\n });\n }\n\n /**\n * Fetch given url.\n */\n async fetch(url: URL, requestInit: RequestInit = {}) {\n const { FETCH_EVENTS } = this.constructor;\n this.$emit(FETCH_EVENTS.BEFORE_FETCH, { instance: this, url, requestInit });\n\n this.__abortController.abort();\n const newController = new AbortController();\n newController.signal.addEventListener('abort', () => {\n this.$emit(FETCH_EVENTS.ABORT, { instance: this, url, requestInit, reason: newController.signal.reason })\n });\n this.__abortController = newController;\n const init = {\n ...this.requestInit,\n ...requestInit,\n headers: {\n ...this.requestInit.headers,\n ...requestInit.headers,\n },\n signal: newController.signal,\n };\n\n this.$log('fetch', url, init);\n this.$emit(FETCH_EVENTS.FETCH, { instance: this, url, requestInit: init });\n\n try {\n const response = await this.client(url, init);\n\n if (!response.ok) {\n throw new Error(`Fetch failed with status ${response.status}`);\n }\n\n const content = await response.text();\n this.$emit(FETCH_EVENTS.AFTER_FETCH, { instance: this, url, requestInit, content });\n this.update(url, init, content);\n } catch (error) {\n this.$emit(FETCH_EVENTS.AFTER_FETCH, { instance: this, url, requestInit, error });\n this.error(url, init, error);\n }\n }\n\n /**\n * Update the DOM with new content from the fetched HTML.\n * @internal\n */\n __updateDOM(fragment: Document) {\n const { FETCH_MODES } = this.constructor;\n const { mode, selector } = this.$options;\n\n // @ts-expect-error querySelectorAll is iterable in the browser\n for (const newElement of fragment.querySelectorAll<HTMLElement>(selector)) {\n const oldElement = newElement.id && document.getElementById(newElement.id);\n\n if (!oldElement || oldElement === newElement) {\n continue;\n }\n\n const oldScripts = getScripts(oldElement);\n\n switch (mode) {\n case FETCH_MODES.APPEND:\n oldElement.append(...newElement.childNodes);\n adoptNewScripts(getScripts(oldElement), oldScripts);\n break;\n case FETCH_MODES.PREPEND:\n oldElement.prepend(...newElement.childNodes);\n adoptNewScripts(getScripts(oldElement), oldScripts);\n break;\n case FETCH_MODES.MORPH:\n morphdom(oldElement, newElement);\n adoptNewScripts(getScripts(oldElement), oldScripts);\n break;\n case FETCH_MODES.REPLACE:\n default:\n oldElement.replaceWith(newElement);\n adoptNewScripts(getScripts(newElement), oldScripts);\n break;\n }\n }\n }\n\n /**\n * Dispatch the contents to update to their matching FrameTarget.\n */\n async update(url: URL, requestInit: RequestInit, content: string) {\n const { FETCH_EVENTS } = this.constructor;\n const { history } = this.$options;\n\n this.$log('content', url, content);\n this.$emit(FETCH_EVENTS.BEFORE_UPDATE, { instance: this, url, requestInit, content });\n\n const fragment = this.__domParser.parseFromString(content, 'text/html');\n\n if (history) {\n if (requestInit?.headers?.[this.__headerNames.X_TRIGGERED_BY] !== 'popstate') {\n historyPush({ path: url.pathname, search: url.searchParams });\n }\n domScheduler.write(() => {\n if (fragment.title) {\n document.title = fragment.title;\n }\n });\n }\n\n this.$emit(FETCH_EVENTS.UPDATE, { instance: this, url, requestInit, fragment });\n\n if (isFunction(document.startViewTransition)) {\n await document.startViewTransition(() => {\n this.__updateDOM(fragment);\n }).ready;\n } else {\n this.__updateDOM(fragment);\n }\n\n this.$emit(FETCH_EVENTS.AFTER_UPDATE, { instance: this, url, requestInit, fragment });\n }\n\n /**\n * Handle errors.\n */\n error(url: URL, requestInit: RequestInit, error: Error) {\n if (error.name === 'AbortError') return;\n\n this.$log('error', url, requestInit, error);\n this.$emit(this.constructor.FETCH_EVENTS.ERROR, { instance: this, url, requestInit, error });\n }\n\n /**\n * Abort the current request.\n */\n abort(reason?: any) {\n this.__abortController.abort(reason);\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|