@very-simple/ajax 0.4.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/README.md ADDED
@@ -0,0 +1,16 @@
1
+ # 🧩 Very Simple Ajax
2
+
3
+ A very simple turbolinks inspired library based on [Idiomorph](https://github.com/bigskysoftware/idiomorph).
4
+ You can use it for progressive enhancements and to give your multi-page websites a SPA-like feel.
5
+
6
+ 💾 only ~3kb (minify and gzip)
7
+
8
+ ## Installation
9
+
10
+ ```
11
+ npm install @very-simple/ajax
12
+ ```
13
+
14
+ ## Usage
15
+
16
+ Checkout the `/examples` folder for more examples. Better documentation coming soon...
@@ -0,0 +1,20 @@
1
+ interface VisitOptions {
2
+ action?: 'push' | 'replace' | 'none';
3
+ emitEvents?: boolean;
4
+ isBackForward?: boolean;
5
+ merge?: MergeStrategy;
6
+ morphHeads?: boolean;
7
+ autoFocus?: boolean;
8
+ }
9
+ type MergeStrategy = 'replace' | 'morph' | 'before' | 'after' | 'prepend' | 'append' | 'update';
10
+ interface Config {
11
+ watchHistory?: boolean;
12
+ merge?: MergeStrategy;
13
+ }
14
+
15
+ declare const _default: {
16
+ start: ({ watchHistory, merge, }?: Config) => void;
17
+ visit: (url: string, { action, emitEvents, isBackForward, morphHeads, merge: mergeStrategy, autoFocus, }?: VisitOptions) => Promise<void>;
18
+ };
19
+
20
+ export { _default as default };
package/dist/index.js ADDED
@@ -0,0 +1,212 @@
1
+ // src/config.ts
2
+ var globalConfig = {
3
+ watchHistory: true,
4
+ merge: "replace"
5
+ };
6
+
7
+ // src/load.ts
8
+ var parser = new DOMParser();
9
+ var progress = 0;
10
+ var currentLoadController;
11
+ var load = async (url, regionIds) => {
12
+ let progressDelayTimeout;
13
+ try {
14
+ currentLoadController?.abort();
15
+ currentLoadController = new AbortController();
16
+ setProgress(0);
17
+ progressDelayTimeout = window.setTimeout(() => toggleLoading(true), 500);
18
+ const response = await fetch(url, {
19
+ headers: {
20
+ "X-Very-Simple-Request": "true",
21
+ "X-Very-Simple-Regions": regionIds.join(" ")
22
+ },
23
+ signal: currentLoadController.signal
24
+ });
25
+ const reader = response.body?.getReader();
26
+ const length = parseInt(response.headers.get("Content-Length") ?? "0");
27
+ let receivedBytes = 0;
28
+ const stream = new ReadableStream({
29
+ start(controller) {
30
+ const read = async () => {
31
+ const { done, value } = await reader.read();
32
+ if (done) {
33
+ controller.close();
34
+ return;
35
+ }
36
+ receivedBytes += value.length;
37
+ setProgress(receivedBytes / length);
38
+ controller.enqueue(value);
39
+ read();
40
+ };
41
+ read();
42
+ }
43
+ });
44
+ const html = await new Response(stream, {
45
+ headers: { "Content-Type": "text/html" }
46
+ }).text();
47
+ return parser.parseFromString(html, "text/html");
48
+ } catch (e) {
49
+ setProgress(0);
50
+ const isAbortError = e instanceof DOMException && e.name === "AbortError";
51
+ if (isAbortError || currentLoadController?.signal.aborted) {
52
+ window.location.reload();
53
+ }
54
+ } finally {
55
+ clearTimeout(progressDelayTimeout);
56
+ toggleLoading(false);
57
+ }
58
+ };
59
+ var setProgress = (value) => {
60
+ progress = value;
61
+ document.documentElement.style.setProperty(
62
+ "--simple-progress",
63
+ `${value * 100}%`
64
+ );
65
+ };
66
+ var toggleLoading = (state) => document.documentElement.toggleAttribute("data-simple-loading", state);
67
+
68
+ // src/merge.ts
69
+ import { Idiomorph } from "idiomorph/dist/idiomorph.esm.js";
70
+ var merge = (region, newRegion, strategy) => {
71
+ let autoFocusEl;
72
+ if (strategy === "replace") {
73
+ region.replaceWith(newRegion);
74
+ } else if (strategy === "before") {
75
+ region.insertAdjacentElement("beforebegin", newRegion);
76
+ } else if (strategy === "after") {
77
+ region.insertAdjacentElement("afterend", newRegion);
78
+ } else if (strategy === "prepend") {
79
+ region.insertAdjacentElement("afterbegin", newRegion);
80
+ } else if (strategy === "append") {
81
+ region.insertAdjacentElement("beforeend", newRegion);
82
+ } else if (strategy === "update") {
83
+ region.replaceChildren(...Array.from(newRegion.children));
84
+ } else if (strategy === "morph") {
85
+ const result = morph(region, newRegion);
86
+ autoFocusEl = result.autoFocusEl;
87
+ }
88
+ if (strategy !== "morph") {
89
+ newRegion.querySelectorAll("[data-simple-permanent][id]").forEach((el) => {
90
+ const originalEL = region.querySelector(`#${el.id}`);
91
+ if (originalEL) el.replaceWith(originalEL);
92
+ });
93
+ }
94
+ if (newRegion.hasAttribute("data-simple-autofocus") || newRegion.autofocus) {
95
+ autoFocusEl ??= newRegion;
96
+ }
97
+ autoFocusEl ??= newRegion.querySelector("[data-simple-autofocus]") ?? newRegion.querySelector("[autofocus]") ?? void 0;
98
+ return { autoFocusEl };
99
+ };
100
+ var morph = (container, newContainer) => {
101
+ const manualReplacements = [];
102
+ let autoFocusEls = [];
103
+ Idiomorph.morph(container, newContainer, {
104
+ callbacks: {
105
+ afterNodeMorphed(_, newNode) {
106
+ if (newNode instanceof HTMLElement && (newNode.autofocus || newNode.dataset.simpleAutofocus)) {
107
+ autoFocusEls.push(newNode);
108
+ }
109
+ },
110
+ beforeNodeMorphed(oldNode, newNode) {
111
+ if (!(oldNode instanceof HTMLElement)) return true;
112
+ if (!(newNode instanceof HTMLElement)) return true;
113
+ if (oldNode.hasAttribute("data-simple-permanent") && oldNode.id === newNode.id)
114
+ return false;
115
+ const oldComponent = oldNode.dataset.simpleComponent;
116
+ const newComponent = newNode.dataset.simpleComponent;
117
+ if (!oldComponent || oldComponent === newComponent) return true;
118
+ manualReplacements.push([oldNode, newNode]);
119
+ }
120
+ }
121
+ });
122
+ for (const [oldEl, newEl] of manualReplacements) oldEl.replaceWith(newEl);
123
+ const autoFocusEl = autoFocusEls.find((el) => el.dataset.simpleAutofocus) ?? autoFocusEls.find((el) => el.autofocus);
124
+ return { autoFocusEl };
125
+ };
126
+
127
+ // src/visit.ts
128
+ import { Idiomorph as Idiomorph2 } from "idiomorph/dist/idiomorph.esm.js";
129
+ var cache = /* @__PURE__ */ new Map();
130
+ var emit = async (type, payload) => {
131
+ const event = new CustomEvent(`simple-ajax:${type}`, { detail: payload });
132
+ document.dispatchEvent(event);
133
+ };
134
+ var visit = async (url, {
135
+ action = "none",
136
+ emitEvents = true,
137
+ isBackForward = false,
138
+ morphHeads = false,
139
+ merge: mergeStrategy = globalConfig.merge,
140
+ autoFocus = true
141
+ } = {}) => {
142
+ if (emitEvents) emit("before-visit", { url });
143
+ let newDocument;
144
+ if (isBackForward) {
145
+ newDocument = cache.get(url)?.cloneNode(true);
146
+ } else {
147
+ const cacheId = window.location.pathname + window.location.search;
148
+ cache.set(cacheId, document.cloneNode(true));
149
+ }
150
+ const regions = findRegions(document);
151
+ newDocument ??= await load(url, Array.from(regions.keys()));
152
+ if (!newDocument) return;
153
+ if (emitEvents) emit("before-render", { url, newDocument });
154
+ if (morphHeads) Idiomorph2.morph(document.head, newDocument.head);
155
+ const getMergeStrategy = (oldEl, newEl) => newEl.dataset.simpleAjax || oldEl.dataset.simpleAjax || mergeStrategy;
156
+ let autoFocusEl;
157
+ const newRegions = findRegions(newDocument);
158
+ const regionKeys = new Set(regions.keys());
159
+ const newRegionKeys = new Set(newRegions.keys());
160
+ const commonRegionKeys = regionKeys.intersection(newRegionKeys);
161
+ const hasCommonRegions = commonRegionKeys.size > 0;
162
+ if (hasCommonRegions) {
163
+ for (const id of commonRegionKeys) {
164
+ const region = regions.get(id);
165
+ const newRegion = newRegions.get(id);
166
+ const strategy = getMergeStrategy(region, newRegion);
167
+ const result = merge(region, newRegion, strategy);
168
+ autoFocusEl ??= result.autoFocusEl;
169
+ }
170
+ } else {
171
+ const region = document.body;
172
+ const newRegion = newDocument.body;
173
+ const strategy = getMergeStrategy(region, newRegion);
174
+ const result = merge(region, newRegion, strategy);
175
+ autoFocusEl = result.autoFocusEl;
176
+ }
177
+ if (autoFocus) autoFocusEl?.focus();
178
+ if (action === "replace") history.replaceState(null, "", url);
179
+ else if (action === "push") history.pushState(null, "", url);
180
+ if (emitEvents) emit("visit", { url });
181
+ };
182
+ var findRegions = (doc) => {
183
+ let regions = /* @__PURE__ */ new Map();
184
+ doc.querySelectorAll("[data-simple-ajax]").forEach((el) => {
185
+ if (!el.id) console.warn(`Ajax region is missing an id:`, el);
186
+ else regions.set(el.id, el);
187
+ });
188
+ return regions;
189
+ };
190
+
191
+ // src/start.ts
192
+ var start = ({
193
+ watchHistory = true,
194
+ merge: merge2 = "replace"
195
+ } = {}) => {
196
+ globalConfig.merge = merge2;
197
+ globalConfig.watchHistory = watchHistory;
198
+ if (watchHistory) {
199
+ window.addEventListener("popstate", async () => {
200
+ await visit(window.location.pathname + window.location.search, {
201
+ action: "none",
202
+ isBackForward: true
203
+ });
204
+ });
205
+ }
206
+ };
207
+
208
+ // src/index.ts
209
+ var src_default = { start, visit };
210
+ export {
211
+ src_default as default
212
+ };
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "@very-simple/ajax",
3
+ "version": "0.4.0",
4
+ "description": "A very simple turbolinks inspired library",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "type": "module",
8
+ "files": [
9
+ "dist/**"
10
+ ],
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/very-simple/ajax.git"
14
+ },
15
+ "scripts": {
16
+ "build": "rm -rf ./dist && tsup src/index.ts --dts --format esm",
17
+ "dev": "vite",
18
+ "test": "playwright test",
19
+ "release": "npm run build && npm run test && bumpp"
20
+ },
21
+ "author": "arnoson",
22
+ "license": "MIT",
23
+ "devDependencies": {
24
+ "@picocss/pico": "^2.0.6",
25
+ "@playwright/test": "^1.48.2",
26
+ "@types/node": "^18.19.64",
27
+ "@very-simple/components": "^0.6.0",
28
+ "bumpp": "^9.8.1",
29
+ "tsup": "8",
30
+ "typescript": "^5",
31
+ "vite": "^5"
32
+ },
33
+ "dependencies": {
34
+ "idiomorph": "^0.3.0"
35
+ }
36
+ }