@digicreon/mujs 1.0.0 → 1.2.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.
Files changed (3) hide show
  1. package/README.md +165 -5
  2. package/dist/mu.min.js +482 -2
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -9,9 +9,12 @@ No build step required. No dependencies. No framework. Just a single `<script>`
9
9
  Inspired by [pjax](https://github.com/defunkt/jquery-pjax), [Turbo](https://turbo.hotwired.dev/) and [HTMX](https://htmx.org/), µJS aims to be simpler and lighter while covering the most common use cases.
10
10
 
11
11
  - šŸš€ **Fast** — Prefetch on hover, no full page reload, progress bar
12
- - 🪶 **Lightweight** — Single file, ~3 KB gzipped, zero dependencies
12
+ - 🪶 **Lightweight** — Single file, ~10 KB gzipped, zero dependencies
13
13
  - šŸ”Œ **Drop-in** — Works with any backend (PHP, Python, Ruby, Go…), no server-side changes needed
14
14
  - 🧩 **Patch mode** — Update multiple page fragments in a single request
15
+ - šŸŽÆ **Triggers** — Any element, any event: live search, polling, focus actions
16
+ - šŸ”„ **HTTP verbs** — GET, POST, PUT, PATCH, DELETE on links, buttons, and forms
17
+ - šŸ“” **SSE** — Real-time updates via Server-Sent Events
15
18
  - ✨ **Modern** — View Transitions, DOM morphing (via idiomorph), `fetch` API, event delegation
16
19
 
17
20
 
@@ -22,6 +25,9 @@ Inspired by [pjax](https://github.com/defunkt/jquery-pjax), [Turbo](https://turb
22
25
  - [Modes](#modes)
23
26
  - [Patch mode](#patch-mode)
24
27
  - [Forms](#forms)
28
+ - [HTTP methods](#http-methods)
29
+ - [Triggers](#triggers)
30
+ - [Server-Sent Events (SSE)](#server-sent-events-sse)
25
31
  - [Ghost mode](#ghost-mode)
26
32
  - [Scroll restoration](#scroll-restoration)
27
33
  - [Prefetch](#prefetch)
@@ -49,16 +55,16 @@ Inspired by [pjax](https://github.com/defunkt/jquery-pjax), [Turbo](https://turb
49
55
 
50
56
  ```html
51
57
  <!-- unpkg -->
52
- <script src="https://unpkg.com/mujs/dist/mu.min.js"></script>
58
+ <script src="https://unpkg.com/@digicreon/mujs/dist/mu.min.js"></script>
53
59
 
54
60
  <!-- jsDelivr -->
55
- <script src="https://cdn.jsdelivr.net/npm/mujs/dist/mu.min.js"></script>
61
+ <script src="https://cdn.jsdelivr.net/npm/@digicreon/mujs/dist/mu.min.js"></script>
56
62
  ```
57
63
 
58
64
  ### Via npm
59
65
 
60
66
  ```bash
61
- npm install mujs
67
+ npm install @digicreon/mujs
62
68
  ```
63
69
 
64
70
 
@@ -204,6 +210,23 @@ Data is sent as `FormData`. Ghost mode is enabled by default (POST responses sho
204
210
  </form>
205
211
  ```
206
212
 
213
+ ### PUT / PATCH / DELETE forms
214
+
215
+ Use `mu-method` to override the HTTP method. The form data is sent as `FormData`, like POST.
216
+
217
+ ```html
218
+ <!-- PUT form -->
219
+ <form action="/api/user/1" mu-method="put">
220
+ <input type="text" name="name">
221
+ <button type="submit">Update</button>
222
+ </form>
223
+
224
+ <!-- DELETE form (no data needed) -->
225
+ <form action="/api/user/1" mu-method="delete">
226
+ <button type="submit">Delete</button>
227
+ </form>
228
+ ```
229
+
207
230
  ### POST form with patch response
208
231
 
209
232
  ```html
@@ -251,6 +274,139 @@ Add `mu-confirm-quit` to a form. If any input is modified, the user is prompted
251
274
  ```
252
275
 
253
276
 
277
+ ## HTTP methods
278
+
279
+ By default, links use GET and forms use their `method` attribute. The `mu-method` attribute overrides the HTTP method for any element.
280
+
281
+ Supported values: `get`, `post`, `put`, `patch`, `delete`, `sse`.
282
+
283
+ ```html
284
+ <!-- DELETE button -->
285
+ <button mu-url="/api/item/42" mu-method="delete" mu-mode="remove" mu-target="#item-42">
286
+ Delete
287
+ </button>
288
+
289
+ <!-- PUT link -->
290
+ <a href="/api/publish/5" mu-method="put" mu-mode="none">Publish</a>
291
+ ```
292
+
293
+ Non-GET requests send an `X-Mu-Method` header with the HTTP method, allowing the server to distinguish between standard and µJS-initiated requests.
294
+
295
+ > **Note:** `mu-post` is deprecated. Use `mu-method="post"` instead.
296
+
297
+
298
+ ## Triggers
299
+
300
+ µJS supports custom event triggers via the `mu-trigger` attribute. This allows any element with a `mu-url` to initiate a fetch on events other than click or submit.
301
+
302
+ ### Default triggers
303
+
304
+ When `mu-trigger` is absent, the trigger depends on the element type:
305
+
306
+ | Element | Default trigger |
307
+ |---|---|
308
+ | `<a>` | `click` |
309
+ | `<form>` | `submit` |
310
+ | `<input>`, `<textarea>`, `<select>` | `change` |
311
+ | Any other element | `click` |
312
+
313
+ ### Available triggers
314
+
315
+ | Trigger | Browser event(s) | Typical elements |
316
+ |---|---|---|
317
+ | `click` | `click` | Any element (default for `<a>`, `<button>`, `<div>`...) |
318
+ | `submit` | `submit` | `<form>` |
319
+ | `change` | `input` | `<input>`, `<textarea>`, `<select>` |
320
+ | `blur` | `change` + `blur` (deduplicated) | `<input>`, `<textarea>`, `<select>` |
321
+ | `focus` | `focus` | `<input>`, `<textarea>`, `<select>` |
322
+ | `load` | *(fires immediately when rendered)* | Any element |
323
+
324
+ ### Examples
325
+
326
+ **Live search with debounce:**
327
+
328
+ ```html
329
+ <input type="text" name="q"
330
+ mu-trigger="change" mu-debounce="500"
331
+ mu-url="/search" mu-target="#results" mu-source="#results"
332
+ mu-mode="update">
333
+ ```
334
+
335
+ **Action on focus (e.g. load suggestions):**
336
+
337
+ ```html
338
+ <input type="text" mu-trigger="focus"
339
+ mu-url="/suggestions" mu-target="#suggestions" mu-mode="update">
340
+ ```
341
+
342
+ **Action on blur (save on field exit):**
343
+
344
+ ```html
345
+ <input type="text" name="title" mu-trigger="blur"
346
+ mu-url="/api/save" mu-method="put" mu-target="#status" mu-mode="update">
347
+ ```
348
+
349
+ **Load content immediately:**
350
+
351
+ ```html
352
+ <div mu-trigger="load"
353
+ mu-url="/sidebar" mu-target="#sidebar" mu-mode="update">
354
+ </div>
355
+ ```
356
+
357
+ ### Polling
358
+
359
+ Combine `mu-trigger="load"` with `mu-repeat` to poll a URL at regular intervals:
360
+
361
+ ```html
362
+ <div mu-trigger="load" mu-repeat="5000"
363
+ mu-url="/notifications" mu-target="#notifs" mu-mode="update">
364
+ </div>
365
+ ```
366
+
367
+ The first fetch fires immediately, then every 5 seconds. Polling intervals are automatically cleaned up when the element is removed from the DOM.
368
+
369
+ ### Debounce
370
+
371
+ Use `mu-debounce` to delay the fetch until the user stops interacting:
372
+
373
+ ```html
374
+ <input type="text" name="q" mu-debounce="300"
375
+ mu-url="/search" mu-target="#results" mu-mode="update">
376
+ ```
377
+
378
+ > **Note:** Triggers other than `click` and `submit` default to ghost mode (no browser history entry).
379
+
380
+
381
+ ## Server-Sent Events (SSE)
382
+
383
+ µJS supports real-time updates via Server-Sent Events. Set `mu-method="sse"` to open an `EventSource` connection instead of a one-shot fetch.
384
+
385
+ ```html
386
+ <div mu-trigger="load" mu-url="/chat/stream"
387
+ mu-mode="patch" mu-method="sse">
388
+ </div>
389
+ ```
390
+
391
+ Each incoming SSE message is treated as HTML and rendered according to the element's `mu-mode`. In patch mode, the server sends HTML fragments with `mu-patch-target` attributes, just like a regular patch response.
392
+
393
+ ### Server-side example
394
+
395
+ ```
396
+ event: message
397
+ data: <div mu-patch-target="#messages" mu-patch-mode="append"><p>New message!</p></div>
398
+
399
+ event: message
400
+ data: <span mu-patch-target="#online-count">42</span>
401
+ ```
402
+
403
+ ### Limitations
404
+
405
+ - **No custom headers**: `EventSource` does not support custom HTTP headers. Use query parameters for authentication (e.g. `mu-url="/stream?token=abc"`).
406
+ - **Connection limit**: Browsers allow ~6 SSE connections per domain in HTTP/1.1. Use HTTP/2 to avoid this limit.
407
+ - **Automatic cleanup**: SSE connections are closed when the element is removed from the DOM (e.g. when the page changes).
408
+
409
+
254
410
  ## Ghost mode
255
411
 
256
412
  Ghost mode prevents a navigation from being added to browser history and disables automatic scroll-to-top.
@@ -424,7 +580,11 @@ All attributes support both `mu-*` and `data-mu-*` syntax.
424
580
  | `mu-morph` | Disable morphing on this element (`false`). |
425
581
  | `mu-transition` | Disable view transitions on this element (`false`). |
426
582
  | `mu-prefetch` | Disable prefetch on hover for this link (`false`). |
427
- | `mu-post` | Force POST method on a link. |
583
+ | `mu-method` | HTTP method: `get`, `post`, `put`, `patch`, `delete`, or `sse`. |
584
+ | `mu-trigger` | Event trigger: `click`, `submit`, `change`, `blur`, `focus`, `load`. |
585
+ | `mu-debounce` | Debounce delay in milliseconds (e.g. `"500"`). |
586
+ | `mu-repeat` | Polling interval in milliseconds (e.g. `"5000"`). |
587
+ | `mu-post` | *(Deprecated)* Use `mu-method="post"` instead. |
428
588
  | `mu-confirm` | Show a confirmation dialog before loading. |
429
589
  | `mu-confirm-quit` | *(Forms)* Prompt before leaving if the form has been modified. |
430
590
  | `mu-validate` | *(Forms)* Name of a JS validation function. Must return `true`/`false`. |
package/dist/mu.min.js CHANGED
@@ -1,2 +1,482 @@
1
- /* µJS (muJS) v1.0 - mujs.org */
2
- var mu=new function(){this._VERSION="1.0",this._defaults={processLinks:!0,processForms:!0,ghost:!1,ghostRedirect:!1,mode:"replace",target:"body",source:"body",title:"title",scrollToTop:null,urlPrefix:null,progress:!0,prefetch:!0,morph:!0,transition:!0,confirmQuitText:"Are you sure you want to leave this page?"},this._cfg={},this._lastUrl=null,this._previousUrl=null,this._abortCtrl=null,this._progressEl=null,this._prefetchCache=new Map,this._confirmQuit=!1,this._jsIncludes={},this._morph=null,this.init=function(t){if(mu._cfg=Object.assign({},mu._defaults,t||{}),mu._cfg._titleAttr=null,mu._cfg.title){var e=mu._cfg.title.lastIndexOf("/");-1!==e&&(mu._cfg._titleAttr=mu._cfg.title.substring(e+1),mu._cfg.title=mu._cfg.title.substring(0,e))}mu._morph||void 0===window.Idiomorph||"function"!=typeof window.Idiomorph.morph||(mu._morph=function(t,e,r){window.Idiomorph.morph(t,e,r)}),document.addEventListener("click",mu._onClick),document.addEventListener("submit",mu._onSubmit),document.addEventListener("mouseover",mu._onMouseOver),document.addEventListener("input",mu._onInput),window.addEventListener("popstate",mu._onPopState),window.addEventListener("beforeunload",mu._onBeforeUnload),window.history.replaceState({mu:!0,url:location.pathname+location.search},""),mu._emit("mu:init",{url:location.pathname+location.search})},this.load=function(t,e){var r=Object.assign({},mu._cfg,e||{});mu._loadExec(t,r)},this.getLastUrl=function(){return mu._lastUrl},this.getPreviousUrl=function(){return mu._previousUrl},this.setConfirmQuit=function(t){mu._confirmQuit=!!t},this.setMorph=function(t){mu._morph=t},this._attr=function(t,e){return t.hasAttribute("mu-"+e)?t.getAttribute("mu-"+e):t.hasAttribute("data-mu-"+e)?t.getAttribute("data-mu-"+e):null},this._attrBool=function(t,e,r){var o=mu._attr(t,e);return null===o?r:""===o||"true"===o||"false"!==o&&r},this._shouldProcess=function(t){if("true"===mu._attr(t,"disabled")||""===mu._attr(t,"disabled"))return!1;if("false"===t.getAttribute("mu")||"false"===t.getAttribute("data-mu"))return!1;if(t.hasAttribute("target"))return!1;if("A"===t.tagName&&t.hasAttribute("onclick"))return!1;if("FORM"===t.tagName&&t.hasAttribute("onsubmit"))return!1;if("true"===t.getAttribute("mu")||"true"===t.getAttribute("data-mu"))return!0;var e=t.getAttribute("href")||t.getAttribute("action")||"";return!(!e.startsWith("/")||e.startsWith("//"))},this._elemCfg=function(t){var e,r=Object.assign({},mu._cfg);if(null!==(e=mu._attr(t,"mode"))&&(r.mode=e),null!==(e=mu._attr(t,"target"))&&(r.target=e),null!==(e=mu._attr(t,"source"))&&(r.source=e),null!==(e=mu._attr(t,"title"))){r.title=e,r._titleAttr=null;var o=e.lastIndexOf("/");-1!==o&&(r._titleAttr=e.substring(o+1),r.title=e.substring(0,o))}return null!==(e=mu._attr(t,"url"))&&(r._url=e),null!==(e=mu._attr(t,"prefix"))&&(r.urlPrefix=e),r.ghost=mu._attrBool(t,"ghost",r.ghost),r.ghostRedirect=mu._attrBool(t,"ghost-redirect",r.ghostRedirect),r.scrollToTop=mu._attrBool(t,"scroll-to-top",r.scrollToTop),r.morph=mu._attrBool(t,"morph",r.morph),r.transition=mu._attrBool(t,"transition",r.transition),r.post=mu._attrBool(t,"post",!1),r.confirm=mu._attr(t,"confirm"),r.patchGhost=mu._attrBool(t,"patch-ghost",!0),r},this._onClick=function(t){var e=t.target.closest("a");if(e&&mu._cfg.processLinks&&mu._shouldProcess(e)&&!(t.ctrlKey||t.metaKey||t.shiftKey||t.altKey)){t.preventDefault();var r=mu._elemCfg(e),o=r._url||e.getAttribute("href");if(r._sourceElement=e,!r.confirm||window.confirm(r.confirm)){if(mu._confirmQuit){if(!window.confirm(r.confirmQuitText))return;mu.setConfirmQuit(!1)}mu._loadExec(o,r)}}},this._onSubmit=function(t){var e=t.target.closest("form");if(e&&mu._cfg.processForms&&mu._shouldProcess(e)&&e.reportValidity()){var r=mu._attr(e,"validate");if(!r||"function"!=typeof window[r]||window[r](e)){var o=(e.getAttribute("method")||"get").toLowerCase(),u=mu._elemCfg(e),i=u._url||e.getAttribute("action");if(u._sourceElement=e,"post"===o)t.preventDefault(),u.postData=new FormData(e),e.hasAttribute("mu-ghost")||e.hasAttribute("data-mu-ghost")||(u.ghost=!0),null===u.scrollToTop&&"patch"!==u.mode&&(u.scrollToTop=!0);else{t.preventDefault();var n=new FormData(e);i=i+"?"+new URLSearchParams(n).toString()}mu.setConfirmQuit(!1),mu._loadExec(i,u)}}},this._onInput=function(t){t.target.closest("form[mu-confirm-quit], form[data-mu-confirm-quit]")&&mu.setConfirmQuit(!0)},this._onMouseOver=function(t){if(mu._cfg.prefetch){var e=t.target.closest("a");if(e&&mu._shouldProcess(e)&&!1!==mu._attrBool(e,"prefetch",!0)){var r=mu._attr(e,"url")||e.getAttribute("href");if(r&&!mu._prefetchCache.has(r)){mu._prefetchCache.set(r,null);var o=mu._cfg.urlPrefix?mu._cfg.urlPrefix+r:r;fetch(o,{headers:{"X-Requested-With":"mujs","X-Mu-Prefetch":"1"},priority:"low"}).then(function(t){return t.ok?t.text():null}).then(function(t){t?mu._prefetchCache.set(r,t):mu._prefetchCache.delete(r)}).catch(function(){mu._prefetchCache.delete(r)})}}}},this._onPopState=function(t){var e=t.state;if(e&&e.mu){var r=Object.assign({},mu._cfg);r.ghost=!0,r.scrollToTop=!1,r._restoreScroll=void 0!==e.scrollX?{x:e.scrollX,y:e.scrollY}:null,mu._loadExec(e.url,r)}else window.location.href=document.location},this._onBeforeUnload=function(t){mu._confirmQuit&&(t.preventDefault(),t.returnValue=mu._cfg.confirmQuitText)},this._loadExec=async function(t,e){var r=e.urlPrefix?e.urlPrefix+t:t;if(mu._saveScroll(),mu._emit("mu:before-fetch",{url:t,fetchUrl:r,config:e,sourceElement:e._sourceElement||null})){mu._abortCtrl&&mu._abortCtrl.abort(),mu._abortCtrl=new AbortController,mu._showProgress();try{var o=null,u=null,i=t;if(mu._prefetchCache.has(t)&&null!==mu._prefetchCache.get(t))o=mu._prefetchCache.get(t),mu._prefetchCache.delete(t);else{var n={signal:mu._abortCtrl.signal,headers:{"X-Requested-With":"mujs","X-Mu-Mode":e.mode}};if(e.postData?(n.method="POST",n.body=e.postData):e.post&&(n.method="POST"),!(u=await fetch(r,n)).ok)return void mu._emit("mu:fetch-error",{url:t,fetchUrl:r,status:u.status,response:u});u.redirected&&(i=new URL(u.url).pathname+new URL(u.url).search),o=await u.text()}var s,a={url:t,finalUrl:i,html:o,config:e};if(!mu._emit("mu:before-render",a))return;if(s="patch"===e.mode?function(){mu._renderPatch(a.html,e)}:function(){mu._renderPage(a.html,e)},e.transition&&document.startViewTransition?document.startViewTransition(s):s(),"patch"===e.mode)e.patchGhost||(mu._previousUrl=mu._lastUrl,mu._lastUrl=i,window.history.pushState({mu:!0,url:i},"",i));else e.ghost||u&&u.redirected&&e.ghostRedirect||e.postData?u&&u.redirected&&!e.ghostRedirect?(mu._previousUrl=mu._lastUrl,mu._lastUrl=i,window.history.pushState({mu:!0,url:i},"",i)):(mu._previousUrl=mu._lastUrl,mu._lastUrl=i):(mu._previousUrl=mu._lastUrl,mu._lastUrl=i,window.history.pushState({mu:!0,url:i},"",i));if("patch"!==e.mode)if(e._restoreScroll)window.scrollTo(e._restoreScroll.x,e._restoreScroll.y);else{var l=e.ghost||u&&u.redirected&&e.ghostRedirect;if(null!==e.scrollToTop?e.scrollToTop:!l){var m=t.indexOf("#");if(-1!==m){var c=document.getElementById(t.substring(m+1));c&&c.scrollIntoView({behavior:"smooth"})}else window.scrollTo(0,0)}}mu.setConfirmQuit(!1),mu._emit("mu:after-render",{url:t,finalUrl:i,mode:e.mode})}catch(e){if("AbortError"===e.name)return;mu._emit("mu:fetch-error",{url:t,fetchUrl:r,error:e})}finally{mu._hideProgress()}}},this._renderPage=function(t,e){var r=(new DOMParser).parseFromString(t,"text/html"),o=null;e.source&&(o=r.querySelector(e.source)),o||(o=r.body);var u=document.querySelector(e.target);if(u){mu._applyMode(e.mode,u,o,e),mu._updateTitle(r,e),mu._mergeHead(r);var i=document.querySelector(e.target)||document.body;mu._runScripts(i)}else console.warn("[µJS] Target element '"+e.target+"' not found.")},this._renderPatch=function(t,e){for(var r=(new DOMParser).parseFromString(t,"text/html").querySelectorAll("[mu-patch-target], [data-mu-patch-target]"),o=0;o<r.length;o++){var u=r[o],i=u.getAttribute("mu-patch-target")||u.getAttribute("data-mu-patch-target"),n=u.getAttribute("mu-patch-mode")||u.getAttribute("data-mu-patch-mode")||"replace",s=document.querySelector(i);s?(mu._applyMode(n,s,u,e),"remove"!==n&&mu._runScripts(u)):console.warn("[µJS] Patch target '"+i+"' not found.")}},this._applyMode=function(t,e,r,o){var u=o.morph&&mu._morph;switch(t){case"update":u?mu._morph(e,r.innerHTML,{morphStyle:"innerHTML"}):e.innerHTML=r.innerHTML;break;case"prepend":e.prepend(r);break;case"append":e.append(r);break;case"before":e.before(r);break;case"after":e.after(r);break;case"remove":e.remove();break;case"none":break;default:"BODY"===e.tagName&&"BODY"===r.tagName?u?mu._morph(e,r.innerHTML,{morphStyle:"innerHTML"}):e.innerHTML=r.innerHTML:u?mu._morph(e,r.outerHTML,{morphStyle:"outerHTML"}):e.replaceWith(r)}},this._updateTitle=function(t,e){if(e.title){var r=t.querySelector(e.title);if(r){var o=e._titleAttr?r.getAttribute(e._titleAttr):r.textContent;o&&(document.title=o)}}},this._mergeHead=function(t){for(var e="link[rel='stylesheet'], style, meta[name], meta[property]",r=document.head.querySelectorAll(e),o=t.head.querySelectorAll(e),u=new Map,i=0;i<r.length;i++)u.set(mu._elKey(r[i]),r[i]);var n=new Set;for(i=0;i<o.length;i++){var s=mu._elKey(o[i]);n.add(s),u.has(s)||document.head.appendChild(o[i].cloneNode(!0))}u.forEach(function(t,e){n.has(e)||t.remove()})},this._elKey=function(t){return"LINK"===t.tagName?"link:"+t.getAttribute("href"):"STYLE"===t.tagName?"style:"+t.textContent.substring(0,100):"META"===t.tagName?"meta:"+(t.getAttribute("name")||t.getAttribute("property")):t.outerHTML},this._runScripts=function(t){for(var e=t.querySelectorAll("script"),r=0;r<e.length;r++){var o=e[r];if("true"!==mu._attr(o,"disabled")&&""!==mu._attr(o,"disabled")){if(o.hasAttribute("src")){var u=o.getAttribute("src");if(mu._jsIncludes[u])continue;mu._jsIncludes[u]=!0}for(var i=document.createElement("script"),n=0;n<o.attributes.length;n++)i.setAttribute(o.attributes[n].name,o.attributes[n].value);i.textContent=o.textContent,o.parentNode.replaceChild(i,o)}}},this._showProgress=function(){if(mu._cfg.progress){if(!mu._progressEl){mu._progressEl=document.createElement("div"),mu._progressEl.id="mu-progress";var t=mu._progressEl.style;t.position="fixed",t.top="0",t.left="0",t.height="3px",t.background="#29d",t.zIndex="99999",t.transition="width .3s ease",t.width="0"}document.body.appendChild(mu._progressEl),mu._progressEl.offsetWidth,mu._progressEl.style.width="70%"}},this._hideProgress=function(){mu._progressEl&&(mu._progressEl.style.width="100%",setTimeout(function(){mu._progressEl&&(mu._progressEl.style.transition="none",mu._progressEl.style.width="0",mu._progressEl.offsetWidth,mu._progressEl.style.transition="width .3s ease",mu._progressEl.remove())},200))},this._saveScroll=function(){var t=window.history.state;t&&t.mu&&(t.scrollX=window.scrollX,t.scrollY=window.scrollY,window.history.replaceState(t,""))},this._emit=function(t,e){(e=e||{}).lastUrl=mu._lastUrl,e.previousUrl=mu._previousUrl;var r=new CustomEvent(t,{bubbles:!0,cancelable:!0,detail:e});return document.dispatchEvent(r)}};
1
+ /**
2
+ * µJS (muJS) - Lightweight AJAX navigation library
3
+ * https://mujs.org
4
+ * MIT License
5
+ *
6
+ * @file mu.js
7
+ * @version 1.2
8
+ * @license MIT
9
+ * @see https://mujs.org
10
+ *
11
+ *
12
+ * ============================================================================
13
+ * TABLE OF CONTENTS
14
+ * ============================================================================
15
+ *
16
+ * 1. Overview
17
+ * 2. Installation
18
+ * 3. Basic usage
19
+ * 4. Modes
20
+ * 5. Patch mode
21
+ * 6. Attributes reference
22
+ * 7. Configuration reference
23
+ * 8. Events
24
+ * 9. Morphing (idiomorph)
25
+ * 10. View Transitions
26
+ * 11. Forms
27
+ * 12. Ghost mode
28
+ * 13. Prefetch
29
+ * 14. Progress bar
30
+ * 15. Triggers
31
+ * 16. Server-Sent Events (SSE)
32
+ * 17. Programmatic usage
33
+ *
34
+ *
35
+ * ============================================================================
36
+ * 1. OVERVIEW
37
+ * ============================================================================
38
+ *
39
+ * µJS intercepts clicks on links and form submissions to load pages via AJAX
40
+ * instead of full browser navigation. The fetched content replaces part (or
41
+ * all) of the current page, making navigation faster and smoother.
42
+ *
43
+ * It is inspired by pjax, Turbo (Hotwire), and HTMX, but aims to be simpler
44
+ * and lighter, with zero dependencies.
45
+ *
46
+ *
47
+ * ============================================================================
48
+ * 2. INSTALLATION
49
+ * ============================================================================
50
+ *
51
+ * Include the script in your HTML page:
52
+ *
53
+ * <script src="/path/to/mu.js"><\/script>
54
+ * <script>mu.init();</script>
55
+ *
56
+ * Optionally, load idiomorph before mu.js for DOM morphing support:
57
+ *
58
+ * <script src="/path/to/idiomorph.js"></script>
59
+ * <script src="/path/to/mu.js"></script>
60
+ * <script>mu.init();</script>
61
+ *
62
+ * µJS auto-detects idiomorph. If present, morphing is enabled by default.
63
+ *
64
+ * Or use as an ES module:
65
+ *
66
+ * import mu from './mu.js';
67
+ * mu.init();
68
+ *
69
+ *
70
+ * ============================================================================
71
+ * 3. BASIC USAGE
72
+ * ============================================================================
73
+ *
74
+ * After calling mu.init(), all internal links (href starting with "/") are
75
+ * automatically intercepted. Clicking a link fetches the page via AJAX and
76
+ * replaces the current <body> with the fetched <body>.
77
+ *
78
+ * No modification of your HTML is needed for basic usage.
79
+ *
80
+ * Example — default behavior (full page replacement):
81
+ *
82
+ * <a href="/about">About</a>
83
+ *
84
+ * Example — replace only a fragment:
85
+ *
86
+ * <a href="/about" mu-target="#content" mu-source="#main">About</a>
87
+ *
88
+ * Example — disable µJS on a specific link:
89
+ *
90
+ * <a href="/file.pdf" mu-disabled>Download</a>
91
+ *
92
+ *
93
+ * ============================================================================
94
+ * 4. MODES
95
+ * ============================================================================
96
+ *
97
+ * The "mu-mode" attribute controls how fetched content is injected into the
98
+ * current page. Default mode is "replace".
99
+ *
100
+ * replace Replace the target node with the source node (default).
101
+ * update Replace the inner content of the target with the source's
102
+ * inner content.
103
+ * prepend Insert the source node at the beginning of the target.
104
+ * append Insert the source node at the end of the target.
105
+ * before Insert the source node before the target.
106
+ * after Insert the source node after the target.
107
+ * remove Remove the target node (source content is ignored).
108
+ * none Do nothing to the DOM (events are still fired).
109
+ * patch Process multiple targeted fragments (see section 5).
110
+ *
111
+ * Example:
112
+ *
113
+ * <a href="/notifications" mu-mode="update" mu-target="#notifs">
114
+ * Refresh notifications
115
+ * </a>
116
+ *
117
+ *
118
+ * ============================================================================
119
+ * 5. PATCH MODE
120
+ * ============================================================================
121
+ *
122
+ * In patch mode, the fetched HTML contains multiple fragments, each targeting
123
+ * a different part of the current page. Fragments are identified by the
124
+ * "mu-patch-target" attribute.
125
+ *
126
+ * Link triggering a patch:
127
+ *
128
+ * <a href="/api/comments/new" mu-mode="patch">Add comment</a>
129
+ *
130
+ * Server response (plain HTML):
131
+ *
132
+ * <!-- Replaces #comment-42 (default mode: replace) -->
133
+ * <div mu-patch-target="#comment-42" class="comment">
134
+ * Updated comment
135
+ * </div>
136
+ *
137
+ * <!-- Appends to #comments -->
138
+ * <div class="comment" mu-patch-target="#comments" mu-patch-mode="append">
139
+ * New comment
140
+ * </div>
141
+ *
142
+ * <!-- Updates the page title -->
143
+ * <title mu-patch-target="title">New title</title>
144
+ *
145
+ * <!-- Removes an element -->
146
+ * <div mu-patch-target="#old-banner" mu-patch-mode="remove"></div>
147
+ *
148
+ * <!-- Adds a stylesheet -->
149
+ * <link rel="stylesheet" href="/css/gallery.css"
150
+ * mu-patch-target="head" mu-patch-mode="append">
151
+ *
152
+ * Patch fragments are standard HTML elements. No special tags needed.
153
+ * The mu-patch-* attributes are kept on injected nodes for debugging.
154
+ *
155
+ * By default, patch mode does not modify browser history. To enable it:
156
+ *
157
+ * <a href="/products?cat=3" mu-mode="patch" mu-patch-ghost="false">
158
+ * Filter
159
+ * </a>
160
+ *
161
+ *
162
+ * ============================================================================
163
+ * 6. ATTRIBUTES REFERENCE
164
+ * ============================================================================
165
+ *
166
+ * All attributes support both "mu-*" and "data-mu-*" syntax.
167
+ *
168
+ * mu-disabled Disable µJS on this element.
169
+ * mu-mode Injection mode (see section 4).
170
+ * mu-target CSS selector for the target node in the current page.
171
+ * mu-source CSS selector for the source node in the fetched page.
172
+ * mu-url Override the URL to fetch (instead of href/action).
173
+ * mu-prefix URL prefix for the fetch request.
174
+ * mu-title Selector for the title node. Supports
175
+ * "selector/attribute" syntax. Empty string to disable
176
+ * title update.
177
+ * mu-ghost "true" to skip browser history and scroll-to-top.
178
+ * mu-ghost-redirect "true" to skip history for redirected responses.
179
+ * mu-scroll-to-top "true"/"false" to force or prevent scrolling.
180
+ * mu-morph "false" to disable morphing on this element.
181
+ * mu-transition "false" to disable view transitions on this element.
182
+ * mu-prefetch "false" to disable prefetch on hover for this link.
183
+ * mu-method HTTP method: "get", "post", "put", "patch",
184
+ * "delete", or "sse". Default: "get" for links,
185
+ * form's method attribute for forms.
186
+ * mu-trigger Event trigger: "click", "submit", "change",
187
+ * "blur", "focus", "load". Default depends on
188
+ * element type (see section 15).
189
+ * mu-debounce Debounce delay in milliseconds (e.g. "500").
190
+ * mu-repeat Repeat interval in ms for polling (e.g. "5000").
191
+ * mu-post (Deprecated) Use mu-method="post" instead.
192
+ * mu-confirm Confirmation message shown before loading.
193
+ * mu-confirm-quit (Forms) Enable quit-page confirmation when the form
194
+ * has been modified.
195
+ * mu-validate Name of a JS function for form validation. Receives
196
+ * the form element. Must return true/false.
197
+ * mu-patch-target (Patch fragments) CSS selector of the target node.
198
+ * mu-patch-mode (Patch fragments) Injection mode for this fragment.
199
+ * mu-patch-ghost "false" to add the URL to browser history in patch
200
+ * mode (default: "true", no history).
201
+ *
202
+ *
203
+ * ============================================================================
204
+ * 7. CONFIGURATION REFERENCE
205
+ * ============================================================================
206
+ *
207
+ * Pass an object to mu.init() to override defaults:
208
+ *
209
+ * mu.init({
210
+ * ghost: true,
211
+ * processForms: false,
212
+ * morph: false
213
+ * });
214
+ *
215
+ * processLinks (bool) Process <a> tags. Default: true.
216
+ * processForms (bool) Process <form> tags. Default: true.
217
+ * ghost (bool) Ghost mode for all links. Default: false.
218
+ * ghostRedirect (bool) Ghost mode for redirections. Default: false.
219
+ * mode (string) Default injection mode. Default: "replace".
220
+ * target (string) Default target selector. Default: "body".
221
+ * source (string) Default source selector. Default: "body".
222
+ * title (string) Title selector. Default: "title".
223
+ * scrollToTop (bool) Force scroll behavior. Default: null (auto).
224
+ * urlPrefix (string) Prefix added to fetched URLs. Default: null.
225
+ * progress (bool) Show progress bar. Default: true.
226
+ * prefetch (bool) Prefetch on hover. Default: true.
227
+ * morph (bool) Enable DOM morphing. Default: true.
228
+ * transition (bool) Enable View Transitions. Default: true.
229
+ * confirmQuitText (string) Quit-page confirmation message.
230
+ *
231
+ *
232
+ * ============================================================================
233
+ * 8. EVENTS
234
+ * ============================================================================
235
+ *
236
+ * µJS dispatches CustomEvents on `document`. All events carry a `detail`
237
+ * object with `lastUrl` and `previousUrl`.
238
+ *
239
+ * mu:init Fired after initialization.
240
+ * detail: {url}
241
+ *
242
+ * mu:before-fetch Fired before fetching. Cancelable (preventDefault()
243
+ * aborts the load).
244
+ * detail: {url, fetchUrl, config, sourceElement}
245
+ *
246
+ * mu:before-render Fired after fetch, before DOM injection. Cancelable.
247
+ * detail.html can be modified to alter injected content.
248
+ * detail: {url, finalUrl, html, config}
249
+ *
250
+ * mu:after-render Fired after DOM injection.
251
+ * detail: {url, finalUrl, mode}
252
+ *
253
+ * mu:fetch-error Fired on fetch failure or HTTP error.
254
+ * detail: {url, fetchUrl, status, response, error}
255
+ *
256
+ * Example:
257
+ *
258
+ * document.addEventListener("mu:after-render", function(e) {
259
+ * console.log("Loaded: " + e.detail.url);
260
+ * myApp.initWidgets();
261
+ * });
262
+ *
263
+ *
264
+ * ============================================================================
265
+ * 9. MORPHING (IDIOMORPH)
266
+ * ============================================================================
267
+ *
268
+ * When a morph library is available, µJS uses it for "replace" and "update"
269
+ * modes to preserve DOM state (focus, scroll position, video playback, etc.).
270
+ *
271
+ * µJS auto-detects idiomorph (https://github.com/bigskysoftware/idiomorph):
272
+ *
273
+ * <script src="/path/to/idiomorph.js"></script>
274
+ * <script src="/path/to/mu.js"></script>
275
+ *
276
+ * To use a different morph library:
277
+ *
278
+ * mu.init();
279
+ * mu.setMorph(function(target, html, opts) {
280
+ * myMorphLib.morph(target, html, opts);
281
+ * });
282
+ *
283
+ * Morphing can be disabled globally (morph: false in config) or per-element
284
+ * (mu-morph="false").
285
+ *
286
+ *
287
+ * ============================================================================
288
+ * 10. VIEW TRANSITIONS
289
+ * ============================================================================
290
+ *
291
+ * µJS uses the View Transitions API (document.startViewTransition) when
292
+ * supported by the browser. This provides smooth animated transitions between
293
+ * page states.
294
+ *
295
+ * Enabled by default. Falls back silently on unsupported browsers.
296
+ *
297
+ * Disable globally:
298
+ *
299
+ * mu.init({ transition: false });
300
+ *
301
+ * Disable per-element:
302
+ *
303
+ * <a href="/page" mu-transition="false">Link</a>
304
+ *
305
+ *
306
+ * ============================================================================
307
+ * 11. FORMS
308
+ * ============================================================================
309
+ *
310
+ * µJS intercepts form submissions. HTML5 validation (reportValidity) is
311
+ * checked before any fetch.
312
+ *
313
+ * GET forms: data is serialized as query string, behaves like a link.
314
+ * POST forms: data is sent as FormData. Ghost mode is enabled by default
315
+ * (POST responses should not be replayed via browser back button).
316
+ *
317
+ * Example — POST form in patch mode:
318
+ *
319
+ * <form action="/comment/create" method="post" mu-mode="patch">
320
+ * <textarea name="body"></textarea>
321
+ * <button type="submit">Send</button>
322
+ * </form>
323
+ *
324
+ * Quit-page confirmation: add mu-confirm-quit to a form. If any input is
325
+ * modified, the user will be prompted before navigating away.
326
+ *
327
+ * <form action="/save" method="post" mu-confirm-quit>...</form>
328
+ *
329
+ * Custom validation via mu-validate attribute:
330
+ *
331
+ * <form action="/save" method="post" mu-validate="myValidator">...</form>
332
+ * <script>
333
+ * function myValidator(form) {
334
+ * return form.querySelector('#name').value.length > 0;
335
+ * }
336
+ * </script>
337
+ *
338
+ *
339
+ * ============================================================================
340
+ * 12. GHOST MODE
341
+ * ============================================================================
342
+ *
343
+ * Ghost mode prevents a navigation from being added to browser history and
344
+ * disables automatic scroll-to-top.
345
+ *
346
+ * <a href="/panel" mu-ghost>Open panel</a>
347
+ *
348
+ * Global ghost mode:
349
+ *
350
+ * mu.init({ ghost: true });
351
+ *
352
+ * In patch mode, ghost is enabled by default (mu-patch-ghost="true").
353
+ * Use mu-patch-ghost="false" to add the URL to history.
354
+ *
355
+ *
356
+ * ============================================================================
357
+ * 13. PREFETCH
358
+ * ============================================================================
359
+ *
360
+ * When enabled (default), µJS fetches the page on mouse hover, before the
361
+ * user clicks. This saves ~100-300ms of perceived loading time.
362
+ *
363
+ * The prefetch cache stores one entry per URL. It is consumed and cleared
364
+ * when the link is clicked.
365
+ *
366
+ * Disable globally:
367
+ *
368
+ * mu.init({ prefetch: false });
369
+ *
370
+ * Disable per-link:
371
+ *
372
+ * <a href="/heavy-page" mu-prefetch="false">Link</a>
373
+ *
374
+ *
375
+ * ============================================================================
376
+ * 14. PROGRESS BAR
377
+ * ============================================================================
378
+ *
379
+ * A thin progress bar is displayed at the top of the page during fetch.
380
+ * It is styled with inline CSS (no external stylesheet needed).
381
+ *
382
+ * The bar element has id="mu-progress" and can be styled via CSS:
383
+ *
384
+ * #mu-progress { background: red !important; height: 5px !important; }
385
+ *
386
+ * Disable globally:
387
+ *
388
+ * mu.init({ progress: false });
389
+ *
390
+ *
391
+ * ============================================================================
392
+ * 15. TRIGGERS
393
+ * ============================================================================
394
+ *
395
+ * µJS supports custom event triggers via the mu-trigger attribute. This
396
+ * allows any element with a mu-url to initiate a fetch on events other
397
+ * than click or submit.
398
+ *
399
+ * Default triggers (when mu-trigger is absent):
400
+ * <a> click
401
+ * <form> submit
402
+ * <input>, <textarea>, <select> change
403
+ * Any other element click
404
+ *
405
+ * Trigger mapping to browser events:
406
+ * click → click (handled by event delegation)
407
+ * submit → submit (handled by event delegation)
408
+ * change → input (browser event)
409
+ * blur → change + blur (deduplicated)
410
+ * focus → focus
411
+ * load → fires immediately when rendered
412
+ *
413
+ * Example — live search with debounce:
414
+ *
415
+ * <input type="text" name="q"
416
+ * mu-trigger="change" mu-debounce="500"
417
+ * mu-url="/search" mu-target="#results" mu-source="#results"
418
+ * mu-mode="update">
419
+ *
420
+ * Example — polling every 5 seconds:
421
+ *
422
+ * <div mu-trigger="load" mu-repeat="5000"
423
+ * mu-url="/notifications" mu-target="#notifs" mu-mode="update">
424
+ * </div>
425
+ *
426
+ * Example — action on focus:
427
+ *
428
+ * <input type="text" mu-trigger="focus"
429
+ * mu-url="/suggestions" mu-target="#suggestions"
430
+ * mu-mode="update">
431
+ *
432
+ * Triggers other than click/submit default to ghost mode (no history).
433
+ *
434
+ *
435
+ * ============================================================================
436
+ * 16. SERVER-SENT EVENTS (SSE)
437
+ * ============================================================================
438
+ *
439
+ * µJS supports Server-Sent Events via mu-method="sse". This opens an
440
+ * EventSource connection to the given URL. Each incoming message is
441
+ * treated as HTML and rendered according to the element's mode.
442
+ *
443
+ * Example — real-time chat with patch mode:
444
+ *
445
+ * <div mu-trigger="load" mu-url="/chat/stream"
446
+ * mu-mode="patch" mu-method="sse">
447
+ * </div>
448
+ *
449
+ * The server sends SSE messages where each data payload is HTML containing
450
+ * patch fragments (with mu-patch-target attributes).
451
+ *
452
+ * Limitations:
453
+ * - EventSource does not support custom HTTP headers. Use query
454
+ * parameters for authentication or identification.
455
+ * - Browser limit: ~6 SSE connections per domain in HTTP/1.1.
456
+ * Use HTTP/2 to avoid this limit.
457
+ * - SSE connections are automatically closed when the element is
458
+ * removed from the DOM (via _cleanupTriggers).
459
+ *
460
+ *
461
+ * ============================================================================
462
+ * 17. PROGRAMMATIC USAGE
463
+ * ============================================================================
464
+ *
465
+ * // Load a page
466
+ * mu.load("/page", { ghost: true, target: "#content" });
467
+ *
468
+ * // Get last loaded URL
469
+ * mu.getLastUrl();
470
+ *
471
+ * // Get previous URL
472
+ * mu.getPreviousUrl();
473
+ *
474
+ * // Enable/disable quit-page confirmation
475
+ * mu.setConfirmQuit(true);
476
+ * mu.setConfirmQuit(false);
477
+ *
478
+ * // Register a custom morph function
479
+ * mu.setMorph(myMorphFunction);
480
+ *
481
+ */
482
+ var mu=mu||new function(){this._VERSION="1.2",this._defaults={processLinks:!0,processForms:!0,ghost:!1,ghostRedirect:!1,mode:"replace",target:"body",source:"body",title:"title",scrollToTop:null,urlPrefix:null,progress:!0,prefetch:!0,prefetchTtl:3e3,morph:!0,transition:!0,confirmQuitText:"Are you sure you want to leave this page?"},this._cfg={},this._lastUrl=null,this._previousUrl=null,this._abortCtrl=null,this._progressEl=null,this._prefetchCache=new Map,this._confirmQuit=!1,this._jsIncludes={},this._morph=null,this._initialized=!1,this.init=function(t){if(mu._cfg=Object.assign({},mu._defaults,t||{}),mu._cfg._titleAttr=null,mu._cfg.title){var e=mu._cfg.title.lastIndexOf("/");-1!==e&&(mu._cfg._titleAttr=mu._cfg.title.substring(e+1),mu._cfg.title=mu._cfg.title.substring(0,e))}mu._morph||void 0===window.Idiomorph||"function"!=typeof window.Idiomorph.morph||(mu._morph=function(t,e,r){window.Idiomorph.morph(t,e,r)});for(var r=document.querySelectorAll("script[src]"),u=0;u<r.length;u++)mu._jsIncludes[r[u].getAttribute("src")]=!0;mu._initialized||(document.addEventListener("click",mu._onClick),document.addEventListener("submit",mu._onSubmit),document.addEventListener("mouseover",mu._onMouseOver),document.addEventListener("mouseout",mu._onMouseOut),document.addEventListener("input",mu._onInput),window.addEventListener("popstate",mu._onPopState),window.addEventListener("beforeunload",mu._onBeforeUnload),mu._initialized=!0),window.history.replaceState({mu:!0,url:location.pathname+location.search},""),mu._initTriggers(document.body),mu._emit("mu:init",{url:location.pathname+location.search})},this.load=function(t,e){var r=Object.assign({},mu._cfg,e||{});mu._loadExec(t,r)},this.getLastUrl=function(){return mu._lastUrl},this.getPreviousUrl=function(){return mu._previousUrl},this.setConfirmQuit=function(t){mu._confirmQuit=!!t},this.setMorph=function(t){mu._morph=t},this._attr=function(t,e){return t.hasAttribute("mu-"+e)?t.getAttribute("mu-"+e):t.hasAttribute("data-mu-"+e)?t.getAttribute("data-mu-"+e):null},this._attrBool=function(t,e,r){var u=mu._attr(t,e);return null===u?r:""===u||"true"===u||"false"!==u&&r},this._shouldProcess=function(t){if("true"===mu._attr(t,"disabled")||""===mu._attr(t,"disabled"))return!1;if("false"===t.getAttribute("mu")||"false"===t.getAttribute("data-mu"))return!1;if(t.hasAttribute("target"))return!1;if("A"===t.tagName&&t.hasAttribute("onclick"))return!1;if("FORM"===t.tagName&&t.hasAttribute("onsubmit"))return!1;if("true"===t.getAttribute("mu")||"true"===t.getAttribute("data-mu"))return!0;var e=mu._attr(t,"url");if(null!==e)return!(!e.startsWith("/")||e.startsWith("//"));var r=t.getAttribute("href")||t.getAttribute("action")||"";return!(!r.startsWith("/")||r.startsWith("//"))},this._elemCfg=function(t){var e,r=Object.assign({},mu._cfg);if(null!==(e=mu._attr(t,"mode"))&&(r.mode=e),null!==(e=mu._attr(t,"target"))&&(r.target=e),null!==(e=mu._attr(t,"source"))&&(r.source=e),null!==(e=mu._attr(t,"title"))){r.title=e,r._titleAttr=null;var u=e.lastIndexOf("/");-1!==u&&(r._titleAttr=e.substring(u+1),r.title=e.substring(0,u))}return null!==(e=mu._attr(t,"url"))&&(r._url=e),null!==(e=mu._attr(t,"prefix"))&&(r.urlPrefix=e),r.ghost=mu._attrBool(t,"ghost",r.ghost),r.ghostRedirect=mu._attrBool(t,"ghost-redirect",r.ghostRedirect),r.scrollToTop=mu._attrBool(t,"scroll-to-top",r.scrollToTop),r.morph=mu._attrBool(t,"morph",r.morph),r.transition=mu._attrBool(t,"transition",r.transition),null!==(e=mu._attr(t,"method"))&&(r.method=e.toLowerCase()),!r.method&&mu._attrBool(t,"post",!1)&&(r.method="post"),r.confirm=mu._attr(t,"confirm"),r.patchGhost=mu._attrBool(t,"patch-ghost",!0),r},this._onClick=function(t){var e=t.target.closest("[mu-url], [data-mu-url], a");if(e&&mu._shouldProcess(e)&&"click"===mu._getTrigger(e)&&("A"!==e.tagName||mu._cfg.processLinks)&&!(t.ctrlKey||t.metaKey||t.shiftKey||t.altKey)){t.preventDefault();var r=mu._elemCfg(e),u=r._url||e.getAttribute("href");if(r._sourceElement=e,r.method||(r.method="get"),!r.confirm||window.confirm(r.confirm)){if(mu._confirmQuit){if(!window.confirm(r.confirmQuitText))return;mu.setConfirmQuit(!1)}"sse"!==r.method?mu._loadExec(u,r):mu._openSSE(u,e,r)}}},this._onSubmit=function(t){var e=t.target.closest("form");if(e&&mu._cfg.processForms&&mu._shouldProcess(e)&&"submit"===mu._getTrigger(e)&&e.reportValidity()){var r=mu._attr(e,"validate");if(!r||"function"!=typeof window[r]||window[r](e)){var u=mu._elemCfg(e),o=u.method||(e.getAttribute("method")||"get").toLowerCase();u.method=o;var i=u._url||e.getAttribute("action");if(u._sourceElement=e,"get"!==o)t.preventDefault(),u.postData=new FormData(e),e.hasAttribute("mu-ghost")||e.hasAttribute("data-mu-ghost")||(u.ghost=!0),null===u.scrollToTop&&"patch"!==u.mode&&(u.scrollToTop=!0);else{t.preventDefault();var n=new FormData(e);i=i+"?"+new URLSearchParams(n).toString()}mu.setConfirmQuit(!1),mu._loadExec(i,u)}}},this._onInput=function(t){t.target.closest("form[mu-confirm-quit], form[data-mu-confirm-quit]")&&mu.setConfirmQuit(!0)},this._onMouseOver=function(t){if(mu._cfg.prefetch){var e=t.target.closest("[mu-url], [data-mu-url], a");if(e&&mu._shouldProcess(e)&&!1!==mu._attrBool(e,"prefetch",!0)){var r=mu._attr(e,"url")||e.getAttribute("href");if(r&&!mu._prefetchCache.has(r)&&r!==location.pathname+location.search){mu._prefetchCache.set(r,null);var u=mu._cfg.urlPrefix?mu._cfg.urlPrefix+r:r;fetch(u,{headers:{"X-Requested-With":"mujs","X-Mu-Prefetch":"1"},priority:"low"}).then(function(t){return t.ok?t.text():null}).then(function(t){t?mu._prefetchCache.set(r,{html:t,ts:Date.now()}):mu._prefetchCache.delete(r)}).catch(function(){mu._prefetchCache.delete(r)})}}}},this._onMouseOut=function(t){if(mu._cfg.prefetch){var e=t.target.closest("[mu-url], [data-mu-url], a");if(e){var r=mu._attr(e,"url")||e.getAttribute("href");r&&mu._prefetchCache.delete(r)}}},this._onPopState=function(t){var e=t.state;if(e&&e.mu){var r=Object.assign({},mu._cfg);r.ghost=!0,r.scrollToTop=!1,r._restoreScroll=void 0!==e.scrollX?{x:e.scrollX,y:e.scrollY}:null,mu._loadExec(e.url,r)}else window.location.href=document.location},this._onBeforeUnload=function(t){mu._confirmQuit&&(t.preventDefault(),t.returnValue=mu._cfg.confirmQuitText)},this._getTrigger=function(t){var e=mu._attr(t,"trigger");if(e)return e;var r=t.tagName;return"FORM"===r?"submit":"INPUT"===r||"TEXTAREA"===r||"SELECT"===r?"change":"click"},this._debounce=function(t,e){var r=null;return function(){clearTimeout(r),r=setTimeout(t,e)}},this._triggerAction=function(t){if(mu._shouldProcess(t)){var e=mu._elemCfg(t),r=e._url||t.getAttribute("href")||t.getAttribute("action");if(r){if(e._sourceElement=t,e.method||(e.method="get"),t.hasAttribute("mu-ghost")||t.hasAttribute("data-mu-ghost")||(e.ghost=!0),"INPUT"===t.tagName||"TEXTAREA"===t.tagName||"SELECT"===t.tagName){var u=t.closest("form");if(u)if("get"===e.method){var o=new FormData(u),i=new URLSearchParams(o).toString();r=r.split("?")[0]+"?"+i}else e.postData=new FormData(u);else t.name&&"get"===e.method&&(r=r.split("?")[0]+"?"+encodeURIComponent(t.name)+"="+encodeURIComponent(t.value))}"sse"!==e.method?mu._loadExec(r,e):mu._openSSE(r,t,e)}}},this._initTriggers=function(t){for(var e=t.querySelectorAll("[mu-url], [data-mu-url], [mu-trigger], [data-mu-trigger]"),r=0;r<e.length;r++){var u=e[r];if(!u._mu_bound){var o=mu._getTrigger(u);if("click"!==o&&"submit"!==o)if(mu._attr(u,"url")||u.getAttribute("href")||u.getAttribute("action")){u._mu_bound=!0;var i=parseInt(mu._attr(u,"debounce"),10)||0,n=parseInt(mu._attr(u,"repeat"),10)||0,a=function(t){return function(){mu._triggerAction(t)}}(u);if(i>0&&(a=mu._debounce(a,i)),n>0&&(a=function(t,e,r){var u=!1;return function(){e(),u||(u=!0,t._mu_interval=setInterval(e,r))}}(u,a,n)),"change"===o)u.addEventListener("input",a);else if("blur"===o){var s=function(t){var e=0;return function(){var r=Date.now();r-e<50||(e=r,t())}}(a);u.addEventListener("change",s),u.addEventListener("blur",s)}else"focus"===o?u.addEventListener("focus",a):"load"===o&&a()}}}},this._cleanupTriggers=function(t){for(var e=t.querySelectorAll?t.querySelectorAll("*"):[],r=0;r<e.length;r++)e[r]._mu_interval&&(clearInterval(e[r]._mu_interval),e[r]._mu_interval=null),e[r]._mu_sse&&(e[r]._mu_sse.close(),e[r]._mu_sse=null);t._mu_interval&&(clearInterval(t._mu_interval),t._mu_interval=null),t._mu_sse&&(t._mu_sse.close(),t._mu_sse=null)},this._openSSE=function(t,e,r){e._mu_sse&&e._mu_sse.close();var u=r.urlPrefix?r.urlPrefix+t:t,o=new EventSource(u);e._mu_sse=o,o.onmessage=function(e){var u={url:t,html:e.data,config:r};mu._emit("mu:before-render",u)&&("patch"===r.mode?mu._renderPatch(u.html,r):mu._renderPage(u.html,r),mu._emit("mu:after-render",{url:t,finalUrl:t,mode:r.mode}))},o.onerror=function(){mu._emit("mu:fetch-error",{url:t,fetchUrl:u,error:new Error("SSE connection error")})}},this._loadExec=async function(t,e){var r=e.urlPrefix?e.urlPrefix+t:t;if(mu._saveScroll(),mu._emit("mu:before-fetch",{url:t,fetchUrl:r,config:e,sourceElement:e._sourceElement||null})){mu._abortCtrl&&mu._abortCtrl.abort(),mu._abortCtrl=new AbortController,mu._showProgress();try{var u=null,o=null,i=t,n=mu._prefetchCache.get(t);if(n&&n.html&&Date.now()-n.ts<mu._cfg.prefetchTtl)u=n.html,mu._prefetchCache.delete(t);else{var a={signal:mu._abortCtrl.signal,headers:{"X-Requested-With":"mujs","X-Mu-Mode":e.mode}},s=(e.method||"get").toUpperCase();if("GET"!==s&&(a.method=s,a.headers["X-Mu-Method"]=s,e.postData&&(a.body=e.postData)),!(o=await fetch(r,a)).ok)return void mu._emit("mu:fetch-error",{url:t,fetchUrl:r,status:o.status,response:o});o.redirected&&(i=new URL(o.url).pathname+new URL(o.url).search),u=await o.text()}var l,m={url:t,finalUrl:i,html:u,config:e};if(!mu._emit("mu:before-render",m))return;if(l="patch"===e.mode?function(){mu._renderPatch(m.html,e)}:function(){mu._renderPage(m.html,e)},e.transition&&document.startViewTransition?document.startViewTransition(l):l(),"patch"===e.mode)e.patchGhost||(mu._previousUrl=mu._lastUrl,mu._lastUrl=i,window.history.pushState({mu:!0,url:i},"",i));else e.ghost||o&&o.redirected&&e.ghostRedirect||e.postData?o&&o.redirected&&!e.ghostRedirect?(mu._previousUrl=mu._lastUrl,mu._lastUrl=i,window.history.pushState({mu:!0,url:i},"",i)):(mu._previousUrl=mu._lastUrl,mu._lastUrl=i):(mu._previousUrl=mu._lastUrl,mu._lastUrl=i,window.history.pushState({mu:!0,url:i},"",i));if("patch"!==e.mode)if(e._restoreScroll)window.scrollTo(e._restoreScroll.x,e._restoreScroll.y);else{var c=e.ghost||o&&o.redirected&&e.ghostRedirect;if(null!==e.scrollToTop?e.scrollToTop:!c){var h=t.indexOf("#");if(-1!==h){var d=document.getElementById(t.substring(h+1));d&&d.scrollIntoView({behavior:"smooth"})}else window.scrollTo(0,0)}}mu.setConfirmQuit(!1),mu._emit("mu:after-render",{url:t,finalUrl:i,mode:e.mode})}catch(e){if("AbortError"===e.name)return;mu._emit("mu:fetch-error",{url:t,fetchUrl:r,error:e})}finally{mu._hideProgress()}}},this._renderPage=function(t,e){var r=(new DOMParser).parseFromString(t,"text/html"),u=null;e.source&&(u=r.querySelector(e.source)),u||(u=r.body);var o=document.querySelector(e.target);if(o){mu._cleanupTriggers(o),mu._applyMode(e.mode,o,u,e),e.ghost||mu._updateTitle(r,e),mu._mergeHead(r);var i=document.querySelector(e.target)||document.body;mu._runScripts(i),mu._initTriggers(i)}else console.warn("[µJS] Target element '"+e.target+"' not found.")},this._renderPatch=function(t,e){for(var r=(new DOMParser).parseFromString(t,"text/html").querySelectorAll("[mu-patch-target], [data-mu-patch-target]"),u=[],o=0;o<r.length;o++){var i=r[o],n=i.getAttribute("mu-patch-target")||i.getAttribute("data-mu-patch-target"),a=i.getAttribute("mu-patch-mode")||i.getAttribute("data-mu-patch-mode")||"replace",s=document.querySelector(n);s?(mu._cleanupTriggers(s),mu._applyMode(a,s,i,e),"remove"!==a&&(mu._runScripts(i),u.push(n))):console.warn("[µJS] Patch target '"+n+"' not found.")}for(var l=0;l<u.length;l++){var m=document.querySelector(u[l]);m&&mu._initTriggers(m)}},this._applyMode=function(t,e,r,u){var o=u.morph&&mu._morph;switch(t){case"update":o?mu._morph(e,r.innerHTML,{morphStyle:"innerHTML"}):e.innerHTML=r.innerHTML;break;case"prepend":e.prepend(r);break;case"append":e.append(r);break;case"before":e.before(r);break;case"after":e.after(r);break;case"remove":e.remove();break;case"none":break;default:"BODY"===e.tagName&&"BODY"===r.tagName?o?mu._morph(e,r.innerHTML,{morphStyle:"innerHTML"}):e.innerHTML=r.innerHTML:o?mu._morph(e,r.outerHTML,{morphStyle:"outerHTML"}):e.replaceWith(r)}},this._updateTitle=function(t,e){if(e.title){var r=t.querySelector(e.title);if(r){var u=e._titleAttr?r.getAttribute(e._titleAttr):r.textContent;u&&(document.title=u)}}},this._mergeHead=function(t){for(var e="link[rel='stylesheet'], style, script",r=document.head.querySelectorAll(e),u=t.head.querySelectorAll(e),o=new Set,i=0;i<r.length;i++)o.add(mu._elKey(r[i]));for(i=0;i<u.length;i++)if(!o.has(mu._elKey(u[i])))if("SCRIPT"===u[i].tagName){for(var n=document.createElement("script"),a=0;a<u[i].attributes.length;a++)n.setAttribute(u[i].attributes[a].name,u[i].attributes[a].value);n.textContent=u[i].textContent,n.hasAttribute("src")&&(mu._jsIncludes[n.getAttribute("src")]=!0),document.head.appendChild(n)}else document.head.appendChild(u[i].cloneNode(!0))},this._elKey=function(t){return"LINK"===t.tagName?"link:"+t.getAttribute("href"):"STYLE"===t.tagName?"style:"+t.textContent.substring(0,100):"SCRIPT"===t.tagName?"script:"+(t.getAttribute("src")||t.textContent.substring(0,100)):t.outerHTML},this._runScripts=function(t){for(var e=t.querySelectorAll("script"),r=0;r<e.length;r++){var u=e[r];if("true"!==mu._attr(u,"disabled")&&""!==mu._attr(u,"disabled")){if(u.hasAttribute("src")){var o=u.getAttribute("src");if(mu._jsIncludes[o])continue;mu._jsIncludes[o]=!0}for(var i=document.createElement("script"),n=0;n<u.attributes.length;n++)i.setAttribute(u.attributes[n].name,u.attributes[n].value);i.textContent=u.textContent,u.parentNode.replaceChild(i,u)}}},this._showProgress=function(){if(mu._cfg.progress){if(!mu._progressEl){mu._progressEl=document.createElement("div"),mu._progressEl.id="mu-progress";var t=mu._progressEl.style;t.position="fixed",t.top="0",t.left="0",t.height="3px",t.background="#29d",t.zIndex="99999",t.transition="width .3s ease",t.width="0"}document.body.appendChild(mu._progressEl),mu._progressEl.offsetWidth,mu._progressEl.style.width="70%"}},this._hideProgress=function(){mu._progressEl&&(mu._progressEl.style.width="100%",setTimeout(function(){mu._progressEl&&(mu._progressEl.style.transition="none",mu._progressEl.style.width="0",mu._progressEl.offsetWidth,mu._progressEl.style.transition="width .3s ease",mu._progressEl.remove())},200))},this._saveScroll=function(){var t=window.history.state;t&&t.mu&&(t.scrollX=window.scrollX,t.scrollY=window.scrollY,window.history.replaceState(t,""))},this._emit=function(t,e){(e=e||{}).lastUrl=mu._lastUrl,e.previousUrl=mu._previousUrl;var r=new CustomEvent(t,{bubbles:!0,cancelable:!0,detail:e});return document.dispatchEvent(r)}};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@digicreon/mujs",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Lightweight AJAX navigation library",
5
5
  "main": "dist/mu.min.js",
6
6
  "files": [