@very-simple/ajax 0.7.1 → 1.0.0-beta.10

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 CHANGED
@@ -3,7 +3,7 @@
3
3
  A very simple turbolinks inspired library based on [Idiomorph](https://github.com/bigskysoftware/idiomorph).
4
4
  You can use it for progressive enhancements and to give your multi-page websites a SPA-like feel.
5
5
 
6
- 💾 only ~3kb (minify and gzip)
6
+ 💾 only ~6kb (minify and gzip)
7
7
 
8
8
  ## Installation
9
9
 
@@ -13,4 +13,246 @@ npm install @very-simple/ajax
13
13
 
14
14
  ## Usage
15
15
 
16
- Checkout the `/examples` folder for more examples. Better documentation coming soon...
16
+ ```ts
17
+ import { start } from '@very-simple/ajax'
18
+ start()
19
+ ```
20
+
21
+ ```html
22
+ <a href="/about">About</a>
23
+ ```
24
+
25
+ Clicking the link now fetches `/about`, morphs the browser history and
26
+ replaces the page content, without a full page reload.
27
+
28
+ Checkout the `/examples` folder for more examples.
29
+
30
+ ### Attributes
31
+
32
+ Add these attributes to a link or form to tweak how its visit behaves,
33
+ without writing any JavaScript:
34
+
35
+ | Attribute | Description |
36
+ | ------------------- | ------------------------------------------------------------------------------------ |
37
+ | `data-ajax-reload` | Opt out of interception entirely; the browser performs a normal navigation/submit. |
38
+ | `data-ajax-regions` | Comma separated list of selectors to swap instead of the whole `<body>`. |
39
+ | `data-ajax-history` | History mode to use: `push`, `replace` or `none`. |
40
+ | `data-ajax-merge` | Merge strategy for the swapped content, e.g. `morph`, `replace`, `before`, `append`. |
41
+ | `data-ajax-state` | JSON encoded state to store in `history.state` for this visit. |
42
+ | `data-ajax-prefetch` | Prefetch the link before it's clicked: `hover`, `mousedown` or `false` to opt out. Without a value the `prefetch` option is used, falling back to `hover`. |
43
+
44
+ ```html
45
+ <!-- only swap #content, preserving the merge strategy for that region -->
46
+ <a href="/about" data-ajax-regions="#content" data-ajax-merge="morph">About</a>
47
+
48
+ <!-- opt out of ajax handling for this link, e.g. to force a full reload -->
49
+ <a href="/logout" data-ajax-reload>Logout</a>
50
+ ```
51
+
52
+ > The `data-ajax-` prefix can be customized via the `prefix` option in `start()`.
53
+
54
+ ### `start(options?)`
55
+
56
+ Call this once when your app boots. It intercepts same-origin link clicks and
57
+ form submissions and turns them into ajax visits, and hooks into browser
58
+ back/forward navigation.
59
+
60
+ ```ts
61
+ import { start } from '@very-simple/ajax'
62
+
63
+ start({
64
+ // Strategy used to merge the swapped content into the current DOM.
65
+ merge: 'replace',
66
+ // How to update the `<head>`: 'merge', 'append', 'morph' or 'none'.
67
+ head: 'merge',
68
+ // Re-execute `<script>` tags found in the swapped content.
69
+ executeScripts: true,
70
+ // `'view'` wraps the swap in the View Transitions API (if supported), `'css'`
71
+ // keeps the old and new region in the DOM and sets `data-ajax-leaving` /
72
+ // `data-ajax-entering` for CSS transitions (only for the `replace` merge).
73
+ transitions: false,
74
+ // Delay (ms) before showing a loading indicator on slow visits.
75
+ loadingDelay: 500,
76
+ // Delay (ms) before hiding the loading indicator again.
77
+ progressHideDelay: 500,
78
+ // Prefix used for the `data-ajax-*` attributes.
79
+ prefix: 'data-ajax-',
80
+ // Default selectors to swap instead of the whole `<body>`, used for visits
81
+ // that don't specify their own `regions` (e.g. via `data-ajax-regions`).
82
+ regions: [],
83
+ // Default prefetch strategy for links: false, 'hover' (hover/focus) or
84
+ // 'mousedown' (press). Touch always prefetches on touchstart.
85
+ prefetch: false,
86
+ // How long (ms) a prefetched response stays usable.
87
+ prefetchTtl: 30000,
88
+ // Custom scroll handling, e.g. restoring position on back/forward visits.
89
+ scrollBehavior: ({ isBackForward, savedPosition }) =>
90
+ isBackForward ? savedPosition : { top: 0 },
91
+ // Called after an element is added to the DOM by a merge.
92
+ mount: (el) => {},
93
+ // Called before an element is removed from the DOM by a merge.
94
+ unmount: (el) => {},
95
+ })
96
+ ```
97
+
98
+ `head`, `executeScripts` and `transitions` also accept a callback
99
+ receiving `{ from, to, isBackForward }`. Combined with `data-ajax-state` this
100
+ allows deciding per link, e.g. to only add new `<head>` elements (scripts,
101
+ styles) without removing existing ones when loading a partial:
102
+
103
+ ```html
104
+ <a href="/partial" data-ajax-state='{"isPartial": true}'>Partial</a>
105
+ ```
106
+
107
+ ```ts
108
+ start({ head: ({ to }) => (to.state?.isPartial ? 'append' : 'merge') })
109
+ ```
110
+
111
+ ### `visit(url, options?)`
112
+
113
+ A lower-level API for triggering a visit programmatically, e.g. in
114
+ response to a custom event or timer instead of a link click:
115
+
116
+ ```ts
117
+ import { visit } from '@very-simple/ajax'
118
+
119
+ visit('/about', {
120
+ // History mode to use: `push`, `replace` or `none`.
121
+ history: 'push',
122
+ // Selectors to swap instead of the whole `<body>`.
123
+ regions: ['#content'],
124
+ // State to store in `history.state` for this visit.
125
+ state: {},
126
+ // Options passed through to the underlying `fetch()` call.
127
+ request: {},
128
+ // Focus the first element with `autofocus` in the swapped content.
129
+ autoFocus: true,
130
+ // ... You can also overwrite any option set in `start()` an a per-visit bases here.
131
+ })
132
+ ```
133
+
134
+ ### Events
135
+
136
+ Every visit fires a series of `CustomEvent`s on `document`, each namespaced
137
+ with `ajax:`:
138
+
139
+ | Event | Description |
140
+ | ------------------- | -------------------------------------------------------------------------- |
141
+ | `ajax:before-visit` | Fired before the new page is fetched. |
142
+ | `ajax:before-swap` | Fired after the new document has been loaded, right before it's merged in. |
143
+ | `ajax:after-swap` | Fired right after the new content has been merged in. |
144
+ | `ajax:load` | Fired once the whole visit (including scroll/focus handling) has finished. |
145
+
146
+ #### Async listeners
147
+
148
+ Use `event.detail.waitUntil(promise)` on those same events to delay the
149
+ visit until the given promise settles, e.g. to run an animation before
150
+ continuing:
151
+
152
+ ```ts
153
+ document.addEventListener('ajax:before-swap', (e) => {
154
+ e.detail.waitUntil(animateIn())
155
+ })
156
+ ```
157
+
158
+ #### Aborting
159
+
160
+ `before-visit`, `before-swap` and `after-swap` carry an `AbortSignal` in
161
+ `event.detail.signal`, which is aborted if the visit gets superseded by a
162
+ newer one (e.g. the user clicks another link while yours is still running).
163
+ If your listener does anything that isn't automatically cancelled, like a
164
+ manual animation loop, check `signal.aborted` (or listen for the signal's
165
+ `abort` event) to stop it early:
166
+
167
+ ```ts
168
+ document.addEventListener('ajax:before-swap', (e) => {
169
+ const { signal } = e.detail
170
+ const stopWhenAborted = () => (running = false)
171
+ signal.addEventListener('abort', stopWhenAborted)
172
+
173
+ let running = true
174
+ const loop = () => {
175
+ if (!running) return
176
+ // ...do some animation work...
177
+ requestAnimationFrame(loop)
178
+ }
179
+ loop()
180
+ })
181
+ ```
182
+
183
+ ## Common patterns
184
+
185
+ ### Load more
186
+
187
+ Turn a "load more" link into an infinite-scroll-like list by appending the
188
+ new region into the existing one and replacing the link itself with the next
189
+ page's link:
190
+
191
+ ```html
192
+ <div id="posts" data-ajax-merge="append">
193
+ <article>1. Lorem Ipsum</article>
194
+ <article>2. Lorem Ipsum</article>
195
+ </div>
196
+
197
+ <a
198
+ id="load-more"
199
+ href="/posts?page=2"
200
+ data-ajax-history="none"
201
+ data-ajax-regions="#posts, #load-more"
202
+ >
203
+ More
204
+ </a>
205
+ ```
206
+
207
+ Since `#load-more` is one of the swapped regions, the response's `#load-more`
208
+ link (pointing to page 3) replaces this one, so clicking it again keeps
209
+ loading further pages.
210
+
211
+ ### Loading content into a dialog
212
+
213
+ Regions aren't limited to the elements currently visible on the page. You can
214
+ target a hidden `<dialog>` and only swap its content, leaving the rest of the
215
+ page untouched:
216
+
217
+ ```html
218
+ <dialog id="preview"></dialog>
219
+
220
+ <a href="/posts/1" data-ajax-regions="#preview" data-ajax-history="none">
221
+ Preview
222
+ </a>
223
+ ```
224
+
225
+ The response for `/posts/1` provides its own `#preview` region and opens
226
+ itself via an inline script, since `executeScripts` re-runs `<script>` tags
227
+ found in the swapped content:
228
+
229
+ ```html
230
+ <dialog id="preview">
231
+ <h2>Post 1</h2>
232
+ <script>
233
+ document.getElementById('preview').showModal()
234
+ </script>
235
+ </dialog>
236
+ ```
237
+
238
+ ### Permanent elements
239
+
240
+ Mark an element with `data-ajax-permanent` (and give it a stable `id`) to
241
+ keep it alive across visits instead of replacing it, e.g. a currently
242
+ playing `<audio>`/`<video>` element or anything else with state that
243
+ shouldn't be reset:
244
+
245
+ ```html
246
+ <div id="player" data-ajax-permanent>
247
+ <audio src="/song.mp3" controls></audio>
248
+ </div>
249
+ ```
250
+
251
+ As long as an element with the same `id` exists in both the old and the new
252
+ document, it's kept instead of being replaced.
253
+
254
+ > With the default `replace`/`update` merge strategies the permanent element
255
+ > is still moved to wherever it appears in the new content, which can cause a
256
+ > visible flash for things like playing animations. Use `merge: 'morph'`
257
+ > instead (globally or via `data-ajax-merge`) so the element is truly left
258
+ > untouched in place.
package/dist/index.d.ts CHANGED
@@ -1,49 +1,123 @@
1
- interface VisitOptions {
2
- action?: 'push' | 'replace' | 'none';
3
- emitEvents?: boolean;
1
+ interface AjaxState extends Record<string, unknown> {
2
+ regions?: string[];
3
+ index?: number;
4
+ }
5
+ /** Also accepts all `Config` options, e.g. `merge`, `scrollBehavior`, etc. */
6
+ interface VisitOptions extends Config {
7
+ history?: 'push' | 'replace' | 'none';
4
8
  isBackForward?: boolean;
5
- merge?: MergeStrategy;
6
- morphHeads?: boolean;
7
9
  autoFocus?: boolean;
10
+ request?: RequestInit;
11
+ regions?: string[];
12
+ state?: Omit<AjaxState, 'regions' | 'index'>;
13
+ }
14
+ type StartOptions = Config;
15
+ interface LoadingOptions {
8
16
  loadingDelay?: number;
9
17
  progressHideDelay?: number;
10
18
  request?: RequestInit;
11
- regions?: string[];
12
19
  }
13
20
  type MergeStrategy = 'replace' | 'morph' | 'before' | 'after' | 'prepend' | 'append' | 'update';
21
+ /** `true` is `'merge'`, `false` is `'none'`. */
22
+ type HeadMode = 'merge' | 'append' | 'morph' | 'none' | boolean;
14
23
  interface Config {
15
- watchHistory?: boolean;
24
+ /** Strategy used to merge the swapped content into the current DOM. */
16
25
  merge?: MergeStrategy;
17
- morphHeads?: boolean;
26
+ /**
27
+ * How to update the `<head>`: `'merge'` adds new and removes stale elements,
28
+ * `'append'` only adds new ones, `'morph'` morphs it like any element and
29
+ * `'none'` leaves it untouched. A callback decides per visit.
30
+ */
31
+ head?: Resolvable<HeadMode>;
32
+ /** Re-execute `<script>` tags found in the swapped content. A callback decides per visit. */
33
+ executeScripts?: Resolvable<boolean>;
34
+ /**
35
+ * `'view'` wraps the swap in the View Transitions API (if supported), `'css'`
36
+ * keeps the old and new region in the DOM and sets `data-ajax-leaving` /
37
+ * `data-ajax-entering` so CSS can animate them (only for `replace`). A
38
+ * callback decides per visit.
39
+ */
40
+ transitions?: Resolvable<false | 'view' | 'css'>;
41
+ /** Custom scroll handling, e.g. restoring position on back/forward visits. */
42
+ scrollBehavior?: ScrollBehavior;
43
+ /** Delay (ms) before showing a loading indicator on slow visits. */
18
44
  loadingDelay?: number;
45
+ /** Delay (ms) before hiding the loading indicator again. */
19
46
  progressHideDelay?: number;
47
+ /** Prefix used for the `data-ajax-*` attributes. */
48
+ prefix?: string;
49
+ /** Default regions used for visits that don't specify their own. */
50
+ regions?: string[];
51
+ /**
52
+ * Default prefetch strategy for links: `'hover'` on hover/focus, `'mousedown'`
53
+ * on press. Links can override it with `data-ajax-prefetch`.
54
+ */
55
+ prefetch?: false | 'hover' | 'mousedown';
56
+ /** How long (ms) a prefetched response stays usable. */
57
+ prefetchTtl?: number;
58
+ /** Called after an element is added to the DOM by a merge. */
59
+ mount?: (el: Element) => void;
60
+ /** Called before an element is removed from the DOM by a merge. */
61
+ unmount?: (el: Element) => void;
20
62
  }
63
+ /** A value, or a callback resolving it right before the swap. */
64
+ type Resolvable<T> = T | ((info: {
65
+ from: PageState;
66
+ to: PageState;
67
+ isBackForward: boolean;
68
+ }) => T);
69
+ type ScrollPosition = {
70
+ top: number;
71
+ };
72
+ type ScrollBehavior = (info: {
73
+ from: PageState;
74
+ to: PageState;
75
+ isBackForward: boolean;
76
+ savedPosition?: ScrollPosition;
77
+ }) => ScrollPosition | false | undefined;
78
+ type WaitUntil = (promise: Promise<unknown>) => void;
79
+ type PageState = {
80
+ url: string;
81
+ state?: AjaxState;
82
+ document?: Document;
83
+ };
84
+ type Payload = {
85
+ from: PageState;
86
+ to: PageState;
87
+ isBackForward: boolean;
88
+ signal: AbortSignal;
89
+ waitUntil: WaitUntil;
90
+ };
21
91
  type EventMap = {
22
- visit: {
23
- url: string;
24
- prevUrl: string;
92
+ load: {
93
+ from: PageState;
94
+ to: PageState;
95
+ isBackForward: boolean;
25
96
  };
26
- 'before-visit': {
27
- url: string;
28
- prevUrl?: string;
29
- };
30
- 'before-render': {
31
- url: string;
32
- prevUrl?: string;
33
- newDocument: Document;
97
+ 'before-visit': Payload;
98
+ 'before-swap': Payload;
99
+ 'after-swap': Payload;
100
+ 'progress-start': Record<string, never>;
101
+ progress: {
102
+ progress: number;
34
103
  };
104
+ 'progress-end': Record<string, never>;
35
105
  };
36
106
  type DomEventMap = {
37
- [K in keyof EventMap as `simple-ajax:${K & string}`]: CustomEvent<EventMap[K]>;
107
+ [K in keyof EventMap as `ajax:${K & string}`]: CustomEvent<EventMap[K]>;
38
108
  };
39
109
  declare global {
40
110
  interface DocumentEventMap extends DomEventMap {
41
111
  }
42
112
  }
43
113
 
44
- declare const _default: {
45
- start: ({ watchHistory, merge, morphHeads, loadingDelay, }?: Config) => void;
46
- visit: (url: string, { action, emitEvents, isBackForward, morphHeads, merge: mergeStrategy, loadingDelay, progressHideDelay, autoFocus, request, regions, }?: VisitOptions) => Promise<void>;
47
- };
114
+ declare const start: (options?: StartOptions) => void;
115
+
116
+ declare const scrollPositions: Map<string, ScrollPosition>;
117
+ /**
118
+ * Load a new page, merge the regions/bodies and add a new history entry
119
+ * according to the history mode.
120
+ */
121
+ declare const visit: (url: string, { history: historyMode, isBackForward, autoFocus, request, state, regions, head, merge: mergeStrategy, executeScripts: executeScriptsOption, transitions, loadingDelay, progressHideDelay, }?: VisitOptions) => Promise<void>;
48
122
 
49
- export { _default as default };
123
+ export { type AjaxState, type Config, type EventMap, type HeadMode, type LoadingOptions, type MergeStrategy, type PageState, type Resolvable, type ScrollBehavior, type ScrollPosition, type StartOptions, type VisitOptions, scrollPositions, start, visit };
package/dist/index.js CHANGED
@@ -1,159 +1,337 @@
1
1
  // src/config.ts
2
- var globalConfig = {
3
- watchHistory: true,
4
- morphHeads: true,
2
+ var config = {
3
+ head: "merge",
5
4
  merge: "replace",
5
+ executeScripts: true,
6
+ transitions: false,
6
7
  loadingDelay: 500,
7
- progressHideDelay: 500
8
+ progressHideDelay: 500,
9
+ prefix: "data-ajax-",
10
+ regions: [],
11
+ prefetch: false,
12
+ prefetchTtl: 3e4
13
+ };
14
+
15
+ // src/prefetch.ts
16
+ var store = /* @__PURE__ */ new Map();
17
+ var keyOf = (url, regions) => `${url}
18
+ ${regions.join(" ")}`;
19
+ var isFresh = (time) => Date.now() - time < config.prefetchTtl;
20
+ var prefetch = (url, regions) => {
21
+ for (const [key2, entry] of store) if (!isFresh(entry.time)) store.delete(key2);
22
+ const key = keyOf(url, regions);
23
+ if (store.has(key)) return;
24
+ const promise = fetch(url, {
25
+ headers: {
26
+ "X-Very-Simple-Ajax": "true",
27
+ "X-Very-Simple-Ajax-Regions": regions.join(" ")
28
+ }
29
+ }).then(async (response) => {
30
+ if (!response.ok) return void 0;
31
+ return { html: await response.text(), response };
32
+ }).catch(() => void 0).then((result) => {
33
+ if (!result) store.delete(key);
34
+ return result;
35
+ });
36
+ store.set(key, { time: Date.now(), promise });
37
+ };
38
+ var takePrefetched = (url, regions) => {
39
+ const key = keyOf(url, regions);
40
+ const entry = store.get(key);
41
+ store.delete(key);
42
+ return entry && isFresh(entry.time) ? entry.promise : void 0;
8
43
  };
9
44
 
10
45
  // src/load.ts
11
46
  var parser = new DOMParser();
12
47
  var currentLoadController;
48
+ var progressDelayTimeout;
13
49
  var progress = 0;
14
50
  var trickleInterval;
51
+ var emit = (type, payload) => document.dispatchEvent(new CustomEvent(`ajax:${type}`, { detail: payload }));
52
+ var cache = /* @__PURE__ */ new Map();
15
53
  var load = async (url, regions, options) => {
16
- let progressDelayTimeout;
54
+ let aborted = false;
17
55
  try {
18
56
  currentLoadController?.abort();
57
+ clearTimeout(progressDelayTimeout);
19
58
  currentLoadController = new AbortController();
20
59
  setProgress(0);
21
60
  progressDelayTimeout = window.setTimeout(() => {
22
- toggleLoading(true);
23
- toggleProgress(true);
61
+ emit("progress-start", {});
24
62
  startTrickle();
25
63
  }, options.loadingDelay);
26
- const response = await fetch(url, {
64
+ const prefetched = await takePrefetched(url, regions);
65
+ const response = prefetched?.response ?? await fetch(url, {
27
66
  ...options.request,
28
67
  headers: {
29
68
  ...options.request?.headers,
30
- "X-Very-Simple-Request": "true",
31
- "X-Very-Simple-Regions": regions.join(" ")
69
+ "X-Very-Simple-Ajax": "true",
70
+ "X-Very-Simple-Ajax-Regions": regions.join(" ")
32
71
  },
33
72
  signal: currentLoadController.signal
34
73
  });
35
- const html = await response.text();
36
- const document2 = parser.parseFromString(html, "text/html");
74
+ const html = prefetched?.html ?? await response.text();
75
+ if (response.ok) cache.set(url, html);
76
+ const document2 = parseHtml(html);
37
77
  return { document: document2, response };
38
78
  } catch (e) {
39
- setProgress(0);
40
- const isAbortError = e instanceof DOMException && e.name === "AbortError";
41
- if (!isAbortError) {
79
+ aborted = e instanceof DOMException && e.name === "AbortError";
80
+ if (!aborted) {
81
+ setProgress(0);
42
82
  window.location.reload();
43
83
  }
44
84
  } finally {
45
- clearTimeout(progressDelayTimeout);
46
- stopTrickle();
47
- setProgress(1);
48
- toggleLoading(false);
49
- setTimeout(() => toggleProgress(false), options.progressHideDelay);
85
+ if (!aborted) {
86
+ clearTimeout(progressDelayTimeout);
87
+ stopTrickle();
88
+ setProgress(1);
89
+ setTimeout(() => emit("progress-end", {}), options.progressHideDelay);
90
+ }
50
91
  }
51
92
  };
93
+ var parseHtml = (html) => parser.parseFromString(html, "text/html");
52
94
  var trickle = () => {
53
- const amount = -0.095 * progress + 0.1;
54
- setProgress(progress + Math.random() * amount);
95
+ setProgress(progress + Math.random() * 0.1 * (1 - progress));
96
+ };
97
+ var startTrickle = () => {
98
+ stopTrickle();
99
+ trickleInterval = window.setInterval(trickle, 300);
100
+ };
101
+ var stopTrickle = () => {
102
+ clearInterval(trickleInterval);
103
+ trickleInterval = void 0;
55
104
  };
56
- var startTrickle = () => trickleInterval = window.setInterval(trickle, 300);
57
- var stopTrickle = () => clearInterval(trickleInterval);
58
105
  var setProgress = (value) => {
59
106
  progress = value;
60
- document.documentElement.style.setProperty(
61
- "--simple-progress",
62
- `${Math.round(value * 1e4) / 100}%`
63
- );
107
+ emit("progress", { progress: value });
64
108
  };
65
- var toggleProgress = (state) => document.documentElement.toggleAttribute("data-simple-progress", state);
66
- var toggleLoading = (state) => document.documentElement.toggleAttribute("data-simple-loading", state);
67
109
 
68
110
  // src/merge.ts
69
111
  import { Idiomorph } from "idiomorph/dist/idiomorph.esm.js";
70
- var merge = (region, newRegion, strategy) => {
112
+ var merge = async (region, newRegion, strategy, signal, useCssTransition = false) => {
113
+ if (useCssTransition && strategy === "replace") {
114
+ return replaceWithTransition(region, newRegion, signal);
115
+ }
116
+ return mergeBuiltIn(region, newRegion, strategy);
117
+ };
118
+ var replaceWithTransition = async (region, newRegion, signal) => {
119
+ const { prefix, mount, unmount } = config;
120
+ const escapedPrefix = CSS.escape(prefix);
121
+ newRegion.querySelectorAll(`[${escapedPrefix}permanent][id]`).forEach((el) => {
122
+ const original = region.querySelector(`#${CSS.escape(el.id)}`);
123
+ if (original) el.replaceWith(original);
124
+ });
125
+ newRegion.setAttribute(`${prefix}entering`, "");
126
+ region.before(newRegion);
127
+ mount?.(newRegion);
128
+ newRegion.getBoundingClientRect();
129
+ newRegion.removeAttribute(`${prefix}entering`);
130
+ region.setAttribute(`${prefix}leaving`, "");
131
+ region.inert = true;
132
+ const animations = [region, newRegion].flatMap(
133
+ (el) => el.getAnimations({ subtree: true })
134
+ );
135
+ const finish = () => animations.forEach((a) => a.finish());
136
+ signal.addEventListener("abort", finish, { once: true });
137
+ await Promise.allSettled(animations.map((a) => a.finished));
138
+ signal.removeEventListener("abort", finish);
139
+ unmount?.(region);
140
+ region.remove();
141
+ return { autoFocusEl: findAutoFocus(newRegion) };
142
+ };
143
+ var findAutoFocus = (newRegion) => {
144
+ const { prefix } = config;
145
+ const escapedPrefix = CSS.escape(prefix);
146
+ if (newRegion.hasAttribute(`${prefix}autofocus`) || newRegion.autofocus) {
147
+ return newRegion;
148
+ }
149
+ return newRegion.querySelector(`[${escapedPrefix}autofocus]`) ?? newRegion.querySelector("[autofocus]") ?? void 0;
150
+ };
151
+ var mergeBuiltIn = (region, newRegion, strategy) => {
71
152
  let autoFocusEl;
72
- if (strategy === "replace") {
73
- region.replaceWith(newRegion);
153
+ const { prefix, mount, unmount } = config;
154
+ const escapedPrefix = CSS.escape(prefix);
155
+ if (strategy === "replace" || strategy === "update") {
156
+ const permanents = /* @__PURE__ */ new Map();
157
+ Array.from(
158
+ newRegion.querySelectorAll(`[${escapedPrefix}permanent][id]`)
159
+ ).forEach((el) => {
160
+ const original = region.querySelector(`#${CSS.escape(el.id)}`);
161
+ if (original) {
162
+ permanents.set(el.id, original);
163
+ original.remove();
164
+ }
165
+ });
166
+ unmount?.(region);
167
+ if (strategy === "replace") region.replaceWith(newRegion);
168
+ else region.replaceChildren(...Array.from(newRegion.children));
169
+ const target = strategy === "replace" ? newRegion : region;
170
+ target.querySelectorAll(`[${escapedPrefix}permanent][id]`).forEach((el) => {
171
+ const original = permanents.get(el.id);
172
+ if (original) el.replaceWith(original);
173
+ });
174
+ mount?.(target);
74
175
  } else if (strategy === "before") {
75
176
  region.insertAdjacentElement("beforebegin", newRegion);
177
+ mount?.(newRegion);
76
178
  } else if (strategy === "after") {
77
179
  region.insertAdjacentElement("afterend", newRegion);
180
+ mount?.(newRegion);
78
181
  } else if (strategy === "prepend") {
79
182
  region.insertAdjacentElement("afterbegin", newRegion);
183
+ mount?.(newRegion);
80
184
  } else if (strategy === "append") {
81
185
  region.insertAdjacentElement("beforeend", newRegion);
82
- } else if (strategy === "update") {
83
- region.replaceChildren(...Array.from(newRegion.children));
186
+ mount?.(newRegion);
84
187
  } else if (strategy === "morph") {
85
188
  const result = morph(region, newRegion);
86
189
  autoFocusEl = result.autoFocusEl;
87
190
  }
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 };
191
+ if (strategy === "morph") return { autoFocusEl };
192
+ return { autoFocusEl: findAutoFocus(newRegion) };
99
193
  };
100
194
  var morph = (container, newContainer) => {
195
+ const { prefix, mount, unmount } = config;
101
196
  const manualReplacements = [];
102
- let autoFocusEls = [];
197
+ const autoFocusEls = [];
103
198
  let currentNodeKeepAttributes = [];
104
199
  Idiomorph.morph(container, newContainer, {
105
200
  callbacks: {
106
201
  beforeNodeMorphed(oldNode, newNode) {
107
- if (!(oldNode instanceof HTMLElement)) return true;
108
- if (!(newNode instanceof HTMLElement)) return true;
109
- currentNodeKeepAttributes = oldNode.dataset.simpleKeepAttributes?.split(" ") ?? [];
110
- if (oldNode.hasAttribute("data-simple-permanent") && oldNode.id === newNode.id)
202
+ if (!(oldNode instanceof Element)) return true;
203
+ if (!(newNode instanceof Element)) return true;
204
+ currentNodeKeepAttributes = oldNode.getAttribute(`${prefix}keep-attributes`)?.split(" ") ?? [];
205
+ if (oldNode.hasAttribute(`${prefix}permanent`) && oldNode.id === newNode.id)
111
206
  return false;
112
- const oldComponent = oldNode.dataset.simpleComponent;
113
- const newComponent = newNode.dataset.simpleComponent;
114
- if (!oldComponent || oldComponent === newComponent) return true;
207
+ const oldIsComponent = oldNode.hasAttribute(`${prefix}component`);
208
+ const newIsComponent = newNode.hasAttribute(`${prefix}component`);
209
+ const isSameComponent = oldIsComponent && newIsComponent && oldNode.getAttribute(`${prefix}component`) === newNode.getAttribute(`${prefix}component`);
210
+ if (!oldIsComponent || isSameComponent) return true;
115
211
  manualReplacements.push([oldNode, newNode]);
212
+ return false;
116
213
  },
117
214
  beforeAttributeUpdated: (name) => {
118
215
  return !currentNodeKeepAttributes.includes(name);
119
216
  },
120
- afterNodeMorphed(_, newNode) {
121
- if (newNode instanceof HTMLElement && (newNode.autofocus || newNode.dataset.simpleAutofocus)) {
122
- autoFocusEls.push(newNode);
217
+ beforeNodeRemoved(node) {
218
+ if (node instanceof Element && !node.hasAttribute(`${prefix}permanent`)) {
219
+ unmount?.(node);
220
+ }
221
+ return true;
222
+ },
223
+ afterNodeAdded(node) {
224
+ if (node instanceof Element && node.hasAttribute(`${prefix}component`)) {
225
+ mount?.(node);
226
+ }
227
+ },
228
+ afterNodeMorphed(oldNode) {
229
+ if (oldNode instanceof HTMLElement && (oldNode.autofocus || oldNode.hasAttribute(`${prefix}autofocus`))) {
230
+ autoFocusEls.push(oldNode);
123
231
  }
124
232
  }
125
233
  }
126
234
  });
127
- for (const [oldEl, newEl] of manualReplacements) oldEl.replaceWith(newEl);
128
- const autoFocusEl = autoFocusEls.find((el) => el.dataset.simpleAutofocus) ?? autoFocusEls.find((el) => el.autofocus);
235
+ for (const [oldEl, newEl] of manualReplacements) {
236
+ unmount?.(oldEl);
237
+ oldEl.replaceWith(newEl);
238
+ mount?.(newEl);
239
+ }
240
+ const autoFocusEl = autoFocusEls.find((el) => el.hasAttribute(`${prefix}autofocus`)) ?? autoFocusEls.find((el) => el.autofocus);
129
241
  return { autoFocusEl };
130
242
  };
131
243
 
244
+ // src/scripts.ts
245
+ var executeScripts = (container) => {
246
+ const marker = `${config.prefix}script-executed`;
247
+ const waiting = [];
248
+ for (const script of container.querySelectorAll("script")) {
249
+ if (script.hasAttribute(marker)) continue;
250
+ const type = script.getAttribute("type");
251
+ if (type && type !== "module" && type !== "text/javascript") continue;
252
+ const newScript = document.createElement("script");
253
+ newScript.textContent = script.textContent;
254
+ for (const attr of script.attributes) {
255
+ newScript.setAttribute(attr.name, attr.value);
256
+ }
257
+ newScript.setAttribute(marker, "");
258
+ if (newScript.src) {
259
+ waiting.push(
260
+ new Promise((resolve) => {
261
+ newScript.onload = newScript.onerror = resolve;
262
+ })
263
+ );
264
+ }
265
+ script.replaceWith(newScript);
266
+ }
267
+ return Promise.all(waiting);
268
+ };
269
+
132
270
  // src/visit.ts
133
271
  import { Idiomorph as Idiomorph2 } from "idiomorph/dist/idiomorph.esm.js";
134
- var cache = /* @__PURE__ */ new Map();
135
- var emit = async (type, payload) => {
136
- const event = new CustomEvent(`simple-ajax:${type}`, { detail: payload });
272
+ var normalizeUrl = (url) => {
273
+ try {
274
+ const urlObj = new URL(url, window.location.origin);
275
+ return urlObj.pathname + urlObj.search;
276
+ } catch {
277
+ return url;
278
+ }
279
+ };
280
+ var emit2 = (type, payload) => {
281
+ const waiting = [];
282
+ const waitUntil = (promise) => waiting.push(Promise.resolve(promise));
283
+ payload = { ...payload, waitUntil };
284
+ const event = new CustomEvent(`ajax:${type}`, { detail: payload });
137
285
  document.dispatchEvent(event);
286
+ if (waiting.length) return Promise.allSettled(waiting);
138
287
  };
139
- var currentUrl = window.location.pathname;
140
- var prevUrl;
288
+ var currentUrl = window.location.pathname + window.location.search;
289
+ var currentState = history.state;
290
+ var currentVisitController;
291
+ var scrollPositions = /* @__PURE__ */ new Map();
141
292
  var visit = async (url, {
142
- action = "none",
143
- emitEvents = true,
293
+ history: historyMode = "none",
144
294
  isBackForward = false,
145
- morphHeads = globalConfig.morphHeads,
146
- merge: mergeStrategy = globalConfig.merge,
147
- loadingDelay = globalConfig.loadingDelay,
148
- progressHideDelay = globalConfig.progressHideDelay,
149
295
  autoFocus = true,
150
296
  request,
151
- regions = []
297
+ state,
298
+ regions = config.regions,
299
+ head = config.head,
300
+ merge: mergeStrategy = config.merge,
301
+ executeScripts: executeScriptsOption = config.executeScripts,
302
+ transitions = config.transitions,
303
+ loadingDelay = config.loadingDelay,
304
+ progressHideDelay = config.progressHideDelay
152
305
  } = {}) => {
153
- if (emitEvents) emit("before-visit", { url, prevUrl });
306
+ url = normalizeUrl(url);
307
+ const fromUrl = currentUrl;
308
+ const fromState = currentState;
309
+ if (!(isBackForward && state)) {
310
+ const currentIndex = currentState?.index ?? 0;
311
+ const index = historyMode === "push" ? currentIndex + 1 : currentIndex;
312
+ state = { ...state, regions, index };
313
+ }
314
+ currentVisitController?.abort();
315
+ currentVisitController = new AbortController();
316
+ const { signal } = currentVisitController;
317
+ if (url === currentUrl) historyMode = "none";
318
+ await emit2("before-visit", {
319
+ from: { url: fromUrl, state: fromState, document },
320
+ to: { url, state },
321
+ signal,
322
+ isBackForward
323
+ });
324
+ if (signal.aborted) return;
154
325
  let newDocument;
155
326
  if (isBackForward) {
156
- newDocument = cache.get(url)?.cloneNode(true);
327
+ const html = cache.get(url);
328
+ if (html) {
329
+ const cached = parseHtml(html);
330
+ const isComplete = regions.every(
331
+ (selector) => !document.querySelector(selector) || cached.querySelector(selector)
332
+ );
333
+ if (isComplete) newDocument = cached;
334
+ }
157
335
  }
158
336
  if (!newDocument) {
159
337
  const options = { loadingDelay, progressHideDelay, request };
@@ -164,64 +342,251 @@ var visit = async (url, {
164
342
  }
165
343
  }
166
344
  if (!newDocument) return;
167
- prevUrl = currentUrl;
345
+ if (signal.aborted) return;
168
346
  currentUrl = url;
169
- if (emitEvents) emit("before-render", { url, prevUrl, newDocument });
170
- if (!isBackForward) {
171
- const cacheId = window.location.pathname + window.location.search;
172
- cache.set(cacheId, document.cloneNode(true));
173
- }
174
- if (morphHeads) Idiomorph2.morph(document.head, newDocument.head);
175
- const getMergeStrategy = (oldEl, newEl) => newEl.dataset.simpleMerge || oldEl.dataset.simpleMerge || mergeStrategy;
347
+ currentState = state;
348
+ if (historyMode === "replace") history.replaceState(state, "", url);
349
+ else if (historyMode === "push") history.pushState(state, "", url);
350
+ const from = { url: fromUrl, state: fromState, document };
351
+ const to = { url, state, document: newDocument };
352
+ await emit2("before-swap", { from, to, isBackForward, signal });
353
+ if (signal.aborted) return;
354
+ const resolve = (option) => typeof option === "function" ? option({ from, to, isBackForward }) : option;
355
+ const resolvedHead = resolve(head);
356
+ let headStyle = resolvedHead;
357
+ if (headStyle === true) headStyle = "merge";
358
+ else if (headStyle === false) headStyle = "none";
359
+ const updateHead = () => {
360
+ if (headStyle === "none") return;
361
+ Idiomorph2.morph(document.head, newDocument.head, {
362
+ head: { style: headStyle }
363
+ });
364
+ };
365
+ const shouldExecuteScripts = resolve(executeScriptsOption);
366
+ const transition = resolve(transitions);
367
+ const getMergeStrategy = (oldEl, newEl) => newEl.getAttribute(`${config.prefix}merge`) || oldEl.getAttribute(`${config.prefix}merge`) || mergeStrategy;
176
368
  let autoFocusEl;
177
369
  const hasMatchingRegions = regions?.some(
178
370
  (selector) => document.querySelector(selector) && newDocument.querySelector(selector)
179
371
  );
180
- if (hasMatchingRegions) {
181
- for (const id of regions) {
182
- const region = document.querySelector(id);
183
- const newRegion = newDocument.querySelector(id);
184
- if (!region || !newRegion) continue;
372
+ const mergeRegions = async () => {
373
+ const scriptPromises = [];
374
+ if (hasMatchingRegions) {
375
+ for (const id of regions) {
376
+ const region = document.querySelector(id);
377
+ const parsedRegion = newDocument.querySelector(id);
378
+ if (!region || !parsedRegion) continue;
379
+ const newRegion = document.importNode(parsedRegion, true);
380
+ const strategy = getMergeStrategy(region, newRegion);
381
+ const result = await merge(
382
+ region,
383
+ newRegion,
384
+ strategy,
385
+ signal,
386
+ transition === "css"
387
+ );
388
+ autoFocusEl ??= result.autoFocusEl;
389
+ if (shouldExecuteScripts) {
390
+ const finalRegion = document.querySelector(id);
391
+ if (finalRegion) scriptPromises.push(executeScripts(finalRegion));
392
+ }
393
+ }
394
+ } else {
395
+ const region = document.body;
396
+ const newRegion = document.importNode(newDocument.body, true);
185
397
  const strategy = getMergeStrategy(region, newRegion);
186
- const result = merge(region, newRegion, strategy);
187
- autoFocusEl ??= result.autoFocusEl;
398
+ const result = await merge(region, newRegion, strategy, signal);
399
+ autoFocusEl = result.autoFocusEl;
400
+ if (shouldExecuteScripts) {
401
+ scriptPromises.push(executeScripts(document.body));
402
+ }
188
403
  }
404
+ if (scriptPromises.length) await Promise.all(scriptPromises);
405
+ };
406
+ if (fromUrl) scrollPositions.set(fromUrl, { top: window.scrollY });
407
+ const applyScrollBehavior = () => {
408
+ if (!config.scrollBehavior) return;
409
+ const savedPosition = isBackForward ? scrollPositions.get(url) : void 0;
410
+ const position = config.scrollBehavior({
411
+ from,
412
+ to,
413
+ isBackForward,
414
+ savedPosition
415
+ });
416
+ if (position) window.scrollTo(position);
417
+ };
418
+ if (transition === "view" && document.startViewTransition) {
419
+ await document.startViewTransition(async () => {
420
+ updateHead();
421
+ await mergeRegions();
422
+ await emit2("after-swap", { from, to, isBackForward, signal });
423
+ applyScrollBehavior();
424
+ }).updateCallbackDone;
189
425
  } else {
190
- const region = document.body;
191
- const newRegion = newDocument.body;
192
- const strategy = getMergeStrategy(region, newRegion);
193
- const result = merge(region, newRegion, strategy);
194
- autoFocusEl = result.autoFocusEl;
426
+ updateHead();
427
+ await mergeRegions();
428
+ await emit2("after-swap", { from, to, isBackForward, signal });
429
+ applyScrollBehavior();
195
430
  }
196
- if (autoFocus) autoFocusEl?.focus();
197
- if (action === "replace") history.replaceState(null, "", url);
198
- else if (action === "push") history.pushState(null, "", url);
199
- if (emitEvents) emit("visit", { url, prevUrl });
431
+ if (signal.aborted) return;
432
+ if (autoFocus) autoFocusEl?.focus({ preventScroll: true });
433
+ emit2("load", { from, to, isBackForward });
200
434
  };
201
435
 
202
- // src/start.ts
203
- var start = ({
204
- watchHistory = true,
205
- merge: merge2 = "replace",
206
- morphHeads = true,
207
- loadingDelay = 500
208
- } = {}) => {
209
- globalConfig.merge = merge2;
210
- globalConfig.watchHistory = watchHistory;
211
- globalConfig.morphHeads = morphHeads;
212
- globalConfig.loadingDelay = loadingDelay;
213
- if (watchHistory) {
214
- window.addEventListener("popstate", () => {
215
- visit(window.location.pathname + window.location.search, {
216
- action: "none",
217
- isBackForward: true
218
- });
436
+ // src/intercept.ts
437
+ var interceptLinks = () => {
438
+ document.addEventListener("click", (e) => {
439
+ if (e.defaultPrevented || isModifiedClick(e)) return;
440
+ const target = e.composedPath ? e.composedPath()[0] : e.target;
441
+ const link = target?.closest?.("a");
442
+ if (!link || !isInterceptable(link)) return;
443
+ e.preventDefault();
444
+ visit(link.href, { history: "push", ...getVisitOptions(link) });
445
+ });
446
+ };
447
+ var isInterceptable = (link) => {
448
+ if (!link.href) return false;
449
+ const isExternalTarget = link.target && link.target !== "_self";
450
+ return !(link.hasAttribute(`${config.prefix}reload`) || link.hasAttribute("download") || isExternalTarget || !isSameOrigin(link.href));
451
+ };
452
+ var hoverDelay = 100;
453
+ var getPrefetchStrategy = (link) => {
454
+ const attr = link.getAttribute(`${config.prefix}prefetch`);
455
+ if (attr === null) return config.prefetch;
456
+ if (attr === "false") return false;
457
+ if (attr === "hover" || attr === "mousedown") return attr;
458
+ return config.prefetch || "hover";
459
+ };
460
+ var interceptPrefetch = () => {
461
+ let hoverTimeout;
462
+ let hoverLink;
463
+ const run = (link) => {
464
+ if (navigator.connection?.saveData) return;
465
+ const url = normalizeUrl(link.href);
466
+ if (url === currentUrl) return;
467
+ const regions = getVisitOptions(link).regions ?? config.regions;
468
+ prefetch(url, regions);
469
+ };
470
+ const findLink = (e) => {
471
+ const link = e.target?.closest?.("a");
472
+ if (!link || !getPrefetchStrategy(link) || !isInterceptable(link)) return;
473
+ return link;
474
+ };
475
+ const clearHover = () => {
476
+ clearTimeout(hoverTimeout);
477
+ hoverLink = void 0;
478
+ };
479
+ document.addEventListener("mouseover", (e) => {
480
+ const link = findLink(e);
481
+ if (!link || link === hoverLink || getPrefetchStrategy(link) !== "hover")
482
+ return;
483
+ clearHover();
484
+ hoverLink = link;
485
+ hoverTimeout = window.setTimeout(() => run(link), hoverDelay);
486
+ });
487
+ document.addEventListener("mouseout", (e) => {
488
+ const to = e.relatedTarget;
489
+ if (hoverLink && !(to && hoverLink.contains(to))) clearHover();
490
+ });
491
+ document.addEventListener("focusin", (e) => {
492
+ const link = findLink(e);
493
+ if (link && getPrefetchStrategy(link) === "hover") run(link);
494
+ });
495
+ document.addEventListener(
496
+ "touchstart",
497
+ (e) => {
498
+ const link = findLink(e);
499
+ if (link) run(link);
500
+ },
501
+ { passive: true }
502
+ );
503
+ document.addEventListener("mousedown", (e) => {
504
+ if (e.button !== 0) return;
505
+ const link = findLink(e);
506
+ if (link) run(link);
507
+ });
508
+ };
509
+ var interceptForms = () => {
510
+ document.addEventListener("submit", (e) => {
511
+ const form = e.target;
512
+ if (e.defaultPrevented || !(form instanceof HTMLFormElement) || form.hasAttribute(`${config.prefix}reload`))
513
+ return;
514
+ const submitter = e.submitter;
515
+ const formAction = typeof form.action === "string" ? form.action : form.getAttribute("action");
516
+ const formMethod = typeof form.method === "string" ? form.method : form.getAttribute("method");
517
+ const method = (submitter?.getAttribute("formmethod") ?? formMethod ?? "get").toLowerCase();
518
+ if (method === "dialog") return;
519
+ const action = submitter?.getAttribute("formaction") ?? formAction ?? location.href;
520
+ if (!isSameOrigin(action)) return;
521
+ const formData = new FormData(form, submitter ?? void 0);
522
+ let url = action;
523
+ const request = { method };
524
+ if (method === "get") {
525
+ const urlObj = new URL(action, location.href);
526
+ urlObj.search = new URLSearchParams(formData).toString();
527
+ url = urlObj.toString();
528
+ } else {
529
+ request.body = formData;
530
+ }
531
+ e.preventDefault();
532
+ visit(url, { history: "push", ...getVisitOptions(form), request });
533
+ });
534
+ };
535
+ var interceptHistory = () => {
536
+ window.addEventListener("popstate", (event) => {
537
+ if (normalizeUrl(window.location.href) === normalizeUrl(currentUrl)) return;
538
+ const isBack = (event.state?.index ?? 0) < (currentState?.index ?? 0);
539
+ const regions = isBack ? currentState?.regions : event.state?.regions;
540
+ visit(window.location.href, {
541
+ history: "none",
542
+ isBackForward: true,
543
+ regions: regions ?? config.regions,
544
+ state: event.state
219
545
  });
546
+ });
547
+ };
548
+ var isSameOrigin = (url) => new URL(url, location.href).origin === location.origin;
549
+ var isModifiedClick = (e) => e.button !== 0 || e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
550
+ var getVisitOptions = (el) => {
551
+ const prefix = config.prefix;
552
+ const options = {};
553
+ const historyAttr = el.getAttribute(`${prefix}history`);
554
+ if (historyAttr) options.history = historyAttr;
555
+ const regions = el.getAttribute(`${prefix}regions`);
556
+ if (regions) {
557
+ options.regions = regions.split(",").map((region) => region.trim()).filter(Boolean);
558
+ }
559
+ const state = el.getAttribute(`${prefix}state`);
560
+ if (state) {
561
+ try {
562
+ options.state = JSON.parse(state);
563
+ } catch {
564
+ console.error(`Invalid ${prefix}state on`, el);
565
+ }
220
566
  }
567
+ const merge2 = el.getAttribute(`${prefix}merge`);
568
+ if (merge2) options.merge = merge2;
569
+ return options;
221
570
  };
222
571
 
223
- // src/index.ts
224
- var index_default = { start, visit };
572
+ // src/start.ts
573
+ var start = (options = {}) => {
574
+ Object.assign(config, options);
575
+ const url = normalizeUrl(window.location.href);
576
+ if (!cache.has(url)) cache.set(url, document.documentElement.outerHTML);
577
+ if (config.scrollBehavior) history.scrollRestoration = "manual";
578
+ interceptLinks();
579
+ interceptForms();
580
+ interceptHistory();
581
+ interceptPrefetch();
582
+ if (import.meta.env.DEV) {
583
+ document.addEventListener("ajax:before-visit", () => {
584
+ document.head.querySelectorAll("style[data-vite-dev-id]").forEach((el) => el.setAttribute("im-preserve", "true"));
585
+ });
586
+ }
587
+ };
225
588
  export {
226
- index_default as default
589
+ scrollPositions,
590
+ start,
591
+ visit
227
592
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@very-simple/ajax",
3
- "version": "0.7.1",
4
- "description": "A very simple turbolinks inspired library",
3
+ "version": "1.0.0-beta.10",
4
+ "description": "A very simple turbolinks inspired ajax library",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "type": "module",
@@ -10,12 +10,14 @@
10
10
  ],
11
11
  "repository": {
12
12
  "type": "git",
13
- "url": "https://github.com/very-simple/ajax.git"
13
+ "url": "https://github.com/arnoson/very-simple-ajax.git"
14
14
  },
15
15
  "scripts": {
16
16
  "build": "tsup src/index.ts --dts --format esm",
17
17
  "dev": "vite",
18
18
  "test": "playwright test",
19
+ "test:types": "tsc --noEmit",
20
+ "size": "esbuild src/index.ts --bundle --minify --format=esm | gzip -c | wc -c",
19
21
  "release": "npm run build && npm run test && bumpp"
20
22
  },
21
23
  "author": "arnoson",
@@ -23,12 +25,11 @@
23
25
  "devDependencies": {
24
26
  "@picocss/pico": "^2.0.6",
25
27
  "@playwright/test": "^1.48.2",
26
- "@types/node": "^22.0",
27
- "@very-simple/components": "^0.6.0",
28
+ "@types/node": "^24",
28
29
  "bumpp": "^10.0.0",
29
30
  "tsup": "8",
30
- "typescript": "^5",
31
- "vite": "^6"
31
+ "typescript": "^6",
32
+ "vite": "^8"
32
33
  },
33
34
  "dependencies": {
34
35
  "idiomorph": "^0.7.0"