naja
Version:
Modern AJAX library for Nette Framework
3 lines (2 loc) • 18.2 kB
JavaScript
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).naja=e()}(this,(function(){"use strict";const t=t=>{"loading"===document.readyState?document.addEventListener("DOMContentLoaded",t):t()};class e extends EventTarget{constructor(t){super(),this.naja=t,this.selector=".ajax",this.allowedOrigins=[window.location.origin],this.handler=this.handleUI.bind(this),t.addEventListener("init",this.initialize.bind(this))}initialize(){t((()=>this.bindUI(window.document.body))),this.naja.snippetHandler.addEventListener("afterUpdate",(t=>{const{snippet:e}=t.detail;this.bindUI(e)}))}bindUI(t){const e=`a${this.selector}`,i=t=>{t.removeEventListener("click",this.handler),t.addEventListener("click",this.handler)};if(t.matches(e))return i(t);t.querySelectorAll(e).forEach((t=>i(t)));const n=t=>{t.removeEventListener("submit",this.handler),t.addEventListener("submit",this.handler)};if(t instanceof HTMLFormElement)return n(t);t.querySelectorAll("form").forEach((t=>n(t)))}handleUI(t){const e=t.currentTarget,i=this.naja.prepareOptions(),n=()=>{};if(t instanceof MouseEvent){if(t.altKey||t.ctrlKey||t.shiftKey||t.metaKey||t.button)return;return void this.clickElement(e,i,t).catch(n)}const{submitter:s}=t;(""===this.selector||e.matches(this.selector)||s?.matches(this.selector))&&this.submitForm(s??e,i,t).catch(n)}async clickElement(t,e={},i){if(t instanceof HTMLAnchorElement)return this.processInteraction(t,"GET",t.href,null,e,i);if((t instanceof HTMLInputElement||t instanceof HTMLButtonElement)&&t.form)return this.submitForm(t,e,i);throw new Error("Unsupported element in clickElement(): element must be an anchor or a submitter element attached to a form.")}async submitForm(t,e={},i){let n,s=null;if((t instanceof HTMLInputElement||t instanceof HTMLButtonElement)&&t.form)n=t.form,s=t;else{if(!(t instanceof HTMLFormElement))throw new Error("Unsupported element in submitForm(): formOrSubmitter must be either a form or a submitter element attached to a form.");n=t,s=i instanceof SubmitEvent?i.submitter:null}const a=(s?.getAttribute("formmethod")??n.getAttribute("method")??"GET").toUpperCase(),r=s?.getAttribute("formaction")??n.getAttribute("action")??window.location.pathname+window.location.search,o=new FormData(n,s);return this.processInteraction(s??n,a,r,o,e,i)}async processInteraction(t,e,i,n=null,s={},a){if(!this.dispatchEvent(new CustomEvent("interaction",{cancelable:!0,detail:{element:t,originalEvent:a,options:s}})))return a?.preventDefault(),{};if(!this.isUrlAllowed(`${i}`))throw new Error(`Cannot dispatch async request, URL is not allowed: ${i}`);return a?.preventDefault(),this.naja.makeRequest(e,i,n,s)}isUrlAllowed(t){const e=new URL(t,location.href);return"null"!==e.origin&&this.allowedOrigins.includes(e.origin)}}class i{constructor(t){this.naja=t,t.addEventListener("init",this.initialize.bind(this)),t.uiHandler.addEventListener("interaction",this.processForm.bind(this))}initialize(){t((()=>this.initForms(window.document.body))),this.naja.snippetHandler.addEventListener("afterUpdate",(t=>{const{snippet:e}=t.detail;this.initForms(e)}))}initForms(t){const e=this.netteForms||window.Nette;if(!e)return;if(t instanceof HTMLFormElement)return void e.initForm(t);t.querySelectorAll("form").forEach((t=>e.initForm(t)))}processForm(t){const{element:e,originalEvent:i}=t.detail,n=e instanceof HTMLFormElement,s=(e instanceof HTMLInputElement||e instanceof HTMLButtonElement)&&e.form;s&&(e.form["nette-submittedBy"]=e);const a=this.netteForms||window.Nette;(n||s)&&a&&!a.validateForm(e)&&(i?.stopImmediatePropagation(),i?.preventDefault(),t.preventDefault())}}class n extends EventTarget{constructor(t){super(),this.naja=t,t.uiHandler.addEventListener("interaction",(t=>{const{element:e,options:i}=t.detail;if(e.hasAttribute("data-naja-force-redirect")||e.form?.hasAttribute("data-naja-force-redirect")){const t=e.getAttribute("data-naja-force-redirect")??e.form?.getAttribute("data-naja-force-redirect");i.forceRedirect="off"!==t}})),t.addEventListener("success",(t=>{const{payload:e,options:i}=t.detail;e.redirect&&(this.makeRedirect(e.redirect,i.forceRedirect??!1,i),t.stopImmediatePropagation())})),this.locationAdapter={assign:t=>window.location.assign(t)}}makeRedirect(t,e,i={}){t instanceof URL&&(t=t.href);let n=e||!this.naja.uiHandler.isUrlAllowed(t);this.dispatchEvent(new CustomEvent("redirect",{cancelable:!0,detail:{url:t,setUrl(e){t=e},isHardRedirect:n,setHardRedirect(t){n=!!t},options:i}}))&&(n?this.locationAdapter.assign(t):this.naja.makeRequest("GET",t,null,i))}}class s extends EventTarget{constructor(t){super(),this.op={replace:{updateElement(t,e){t.innerHTML=e},updateIndex:(t,e)=>e},prepend:{updateElement(t,e){t.insertAdjacentHTML("afterbegin",e)},updateIndex:(t,e)=>e+t},append:{updateElement(t,e){t.insertAdjacentHTML("beforeend",e)},updateIndex:(t,e)=>t+e}},t.addEventListener("success",(t=>{const{options:e,payload:i}=t.detail;i.snippets&&this.updateSnippets(i.snippets,!1,e)}))}static findSnippets(t,e=window.document){const i={};return e.querySelectorAll('[id^="snippet-"]').forEach((e=>{(t?.(e)??1)&&(i[e.id]=e.innerHTML)})),i}async updateSnippets(t,e=!1,i={}){await Promise.all(Object.keys(t).map((async n=>{const s=document.getElementById(n);s&&await this.updateSnippet(s,t[n],e,i)})))}async updateSnippet(t,e,i,n){let s=this.op.replace;!t.hasAttribute("data-naja-snippet-prepend")&&!t.hasAttribute("data-ajax-prepend")||i?!t.hasAttribute("data-naja-snippet-append")&&!t.hasAttribute("data-ajax-append")||i||(s=this.op.append):s=this.op.prepend;if(!this.dispatchEvent(new CustomEvent("beforeUpdate",{cancelable:!0,detail:{snippet:t,content:e,fromCache:i,operation:s,changeOperation(t){s=t},options:n}})))return;this.dispatchEvent(new CustomEvent("pendingUpdate",{detail:{snippet:t,content:e,fromCache:i,operation:s,options:n}}));const a="function"==typeof s?s:s.updateElement;await a(t,e),this.dispatchEvent(new CustomEvent("afterUpdate",{detail:{snippet:t,content:e,fromCache:i,operation:s,options:n}}))}}const a=Symbol();class r extends EventTarget{constructor(t){super(),this.naja=t,this.initialized=!1,this.cursor=0,this.popStateHandler=this.handlePopState.bind(this),t.addEventListener("init",this.initialize.bind(this)),t.addEventListener("before",this.saveUrl.bind(this)),t.addEventListener("before",this.saveOriginalTitle.bind(this)),t.addEventListener("before",this.replaceInitialState.bind(this)),t.addEventListener("success",this.pushNewState.bind(this)),t.redirectHandler.addEventListener("redirect",this.saveRedirectedUrl.bind(this)),t.uiHandler.addEventListener("interaction",this.configureMode.bind(this)),this.historyAdapter={replaceState:(t,e,i)=>window.history.replaceState(t,e,i),pushState:(t,e,i)=>window.history.pushState(t,e,i)}}set uiCache(t){console.warn("Naja: HistoryHandler.uiCache is deprecated, use options.snippetCache instead."),this.naja.defaultOptions.snippetCache=t}handlePopState(t){const{state:e}=t;if("naja"!==e?.source)return;const i=e.cursor-this.cursor;this.cursor=e.cursor;const n=this.naja.prepareOptions();this.dispatchEvent(new CustomEvent("restoreState",{detail:{state:e,direction:i,options:n}}))}initialize(){window.addEventListener("popstate",this.popStateHandler)}saveOriginalTitle(t){const{options:e}=t.detail;e[a]=window.document.title}saveUrl(t){const{url:e,options:i}=t.detail;i.href??=e}saveRedirectedUrl(t){const{url:e,options:i}=t.detail;i.href=e}replaceInitialState(e){const{options:i}=e.detail;!1===r.normalizeMode(i.history)||this.initialized||(t((()=>this.historyAdapter.replaceState(this.buildState(window.location.href,"replace",this.cursor,i),window.document.title,window.location.href))),this.initialized=!0)}configureMode(t){const{element:e,options:i}=t.detail;if(e.hasAttribute("data-naja-history")||e.form?.hasAttribute("data-naja-history")){const t=e.getAttribute("data-naja-history")??e.form?.getAttribute("data-naja-history");i.history=r.normalizeMode(t)}}static normalizeMode(t){return"off"!==t&&!1!==t&&("replace"!==t||"replace")}pushNewState(t){const{payload:e,options:i}=t.detail,n=r.normalizeMode(i.history);if(!1===n)return;e.postGet&&e.url&&(i.href=e.url);const s="replace"===n?"replaceState":"pushState",o="replace"===n?this.cursor:++this.cursor,d=this.buildState(i.href,n,o,i),c=window.document.title;window.document.title=i[a],this.historyAdapter[s](d,c,i.href),window.document.title=c}buildState(t,e,i,n){const s={source:"naja",cursor:i,href:t};return this.dispatchEvent(new CustomEvent("buildState",{detail:{state:s,operation:"replace"===e?"replaceState":"pushState",options:n}})),s}}class o extends EventTarget{constructor(t){super(),this.naja=t,this.currentSnippets=new Map,this.storages={off:new d(t),history:new c,session:new l},t.addEventListener("init",this.initializeIndex.bind(this)),t.snippetHandler.addEventListener("pendingUpdate",this.updateIndex.bind(this)),t.uiHandler.addEventListener("interaction",this.configureCache.bind(this)),t.historyHandler.addEventListener("buildState",this.buildHistoryState.bind(this)),t.historyHandler.addEventListener("restoreState",this.restoreHistoryState.bind(this))}resolveStorage(t){let e;return e=!0===t||void 0===t?"history":!1===t?"off":t,this.storages[e]}static shouldCacheSnippet(t){return!(t.hasAttribute("data-naja-history-nocache")||t.hasAttribute("data-history-nocache")||t.hasAttribute("data-naja-snippet-cache")&&"off"===t.getAttribute("data-naja-snippet-cache"))}initializeIndex(){t((()=>{const t=s.findSnippets(o.shouldCacheSnippet);this.currentSnippets=new Map(Object.entries(t))}))}updateIndex(t){const{snippet:e,content:i,operation:n}=t.detail;if(!o.shouldCacheSnippet(e))return;const a=this.currentSnippets.get(e.id)??"",r="object"==typeof n?n.updateIndex:()=>i;this.currentSnippets.set(e.id,r(a,i));const d=o.parser.parseFromString(i,"text/html"),c=s.findSnippets(o.shouldCacheSnippet,d);for(const[t,e]of Object.entries(c))this.currentSnippets.set(t,e)}configureCache(t){const{element:e,options:i}=t.detail;if(e&&(e.hasAttribute("data-naja-snippet-cache")||e.form?.hasAttribute("data-naja-snippet-cache")||e.hasAttribute("data-naja-history-cache")||e.form?.hasAttribute("data-naja-history-cache"))){const t=e.getAttribute("data-naja-snippet-cache")??e.form?.getAttribute("data-naja-snippet-cache")??e.getAttribute("data-naja-history-cache")??e.form?.getAttribute("data-naja-history-cache");i.snippetCache=t}}buildHistoryState(t){const{state:e,options:i}=t.detail;"historyUiCache"in i&&(console.warn("Naja: options.historyUiCache is deprecated, use options.snippetCache instead."),i.snippetCache=i.historyUiCache);const n=Object.keys(s.findSnippets(o.shouldCacheSnippet)),a=Object.fromEntries(Array.from(this.currentSnippets).filter((([t])=>n.includes(t))));if(!this.dispatchEvent(new CustomEvent("store",{cancelable:!0,detail:{snippets:a,state:e,options:i}})))return;const r=this.resolveStorage(i.snippetCache);e.snippets={storage:r.type,key:r.store(a)}}restoreHistoryState(t){const{state:e,options:i}=t.detail;if(void 0===e.snippets)return;if(i.snippetCache=e.snippets.storage,!this.dispatchEvent(new CustomEvent("fetch",{cancelable:!0,detail:{state:e,options:i}})))return;const n=this.resolveStorage(i.snippetCache).fetch(e.snippets.key,e,i);null!==n&&this.dispatchEvent(new CustomEvent("restore",{cancelable:!0,detail:{snippets:n,state:e,options:i}}))&&this.naja.snippetHandler.updateSnippets(n,!0,i)}}o.parser=new DOMParser;class d{constructor(t){this.naja=t,this.type="off"}store(){return null}fetch(t,e,i){return this.naja.makeRequest("GET",e.href,null,{...i,history:!1,snippetCache:!1}),null}}class c{constructor(){this.type="history"}store(t){return t}fetch(t){return t}}class l{constructor(){this.type="session"}store(t){const e=Math.random().toString(36).substring(2,8);return window.sessionStorage.setItem(e,JSON.stringify(t)),e}fetch(t){const e=window.sessionStorage.getItem(t);return null===e?null:JSON.parse(e)}}class p{constructor(t){this.naja=t,this.loadedScripts=new Set,t.addEventListener("init",this.initialize.bind(this))}initialize(){t((()=>{document.querySelectorAll("script[data-naja-script-id]").forEach((t=>{const e=t.getAttribute("data-naja-script-id");null!==e&&""!==e&&this.loadedScripts.add(e)}))})),this.naja.snippetHandler.addEventListener("afterUpdate",(t=>{const{content:e}=t.detail;this.loadScripts(e)}))}loadScripts(t){"string"!=typeof t?Object.keys(t).forEach((e=>{const i=t[e];this.loadScriptsInSnippet(i)})):this.loadScriptsInSnippet(t)}loadScriptsInSnippet(t){if(!/<script/i.test(t))return;p.parser.parseFromString(t,"text/html").querySelectorAll("script").forEach((t=>{const e=t.getAttribute("data-naja-script-id");if(null!==e&&""!==e&&this.loadedScripts.has(e))return;const i=window.document.createElement("script");if(i.innerHTML=t.innerHTML,t.hasAttributes())for(const e of t.attributes)i.setAttribute(e.name,e.value);window.document.head.appendChild(i).parentNode.removeChild(i),null!==e&&""!==e&&this.loadedScripts.add(e)}))}}p.parser=new DOMParser;class h extends EventTarget{constructor(t,a,d,c,l,h,u){super(),this.VERSION=3,this.initialized=!1,this.extensions=[],this.defaultOptions={},this.uiHandler=new(t??e)(this),this.redirectHandler=new(a??n)(this),this.snippetHandler=new(d??s)(this),this.formsHandler=new(c??i)(this),this.historyHandler=new(l??r)(this),this.snippetCache=new(h??o)(this),this.scriptLoader=new(u??p)(this)}registerExtension(t){this.initialized&&t.initialize(this),this.extensions.push(t)}initialize(t={}){if(this.initialized)throw new Error("Cannot initialize Naja, it is already initialized.");this.defaultOptions=this.prepareOptions(t),this.extensions.forEach((t=>t.initialize(this))),this.dispatchEvent(new CustomEvent("init",{detail:{defaultOptions:this.defaultOptions}})),this.initialized=!0}prepareOptions(t){return{...this.defaultOptions,...t,fetch:{...this.defaultOptions.fetch,...t?.fetch}}}async makeRequest(t,e,i=null,n={}){"string"==typeof e&&(e=new URL(e,location.href)),n=this.prepareOptions(n);const s=new Headers(n.fetch.headers||{}),a=this.transformData(e,t,i),r=new AbortController,o=new Request(e.toString(),{credentials:"same-origin",...n.fetch,method:t,headers:s,body:a,signal:r.signal});if(o.headers.set("X-Requested-With","XMLHttpRequest"),o.headers.set("Accept","application/json"),!this.dispatchEvent(new CustomEvent("before",{cancelable:!0,detail:{request:o,method:t,url:e.toString(),data:i,options:n}})))return{};const d=window.fetch(o);let c,l;this.dispatchEvent(new CustomEvent("start",{detail:{request:o,promise:d,abortController:r,options:n}}));try{if(c=await d,!c.ok)throw new u(c);l=await c.json()}catch(t){if("AbortError"===t.name)return this.dispatchEvent(new CustomEvent("abort",{detail:{request:o,error:t,options:n}})),this.dispatchEvent(new CustomEvent("complete",{detail:{request:o,response:c,payload:void 0,error:t,options:n}})),{};throw this.dispatchEvent(new CustomEvent("error",{detail:{request:o,response:c,error:t,options:n}})),this.dispatchEvent(new CustomEvent("complete",{detail:{request:o,response:c,payload:void 0,error:t,options:n}})),t}return this.dispatchEvent(new CustomEvent("payload",{detail:{request:o,response:c,payload:l,options:n}})),this.dispatchEvent(new CustomEvent("success",{detail:{request:o,response:c,payload:l,options:n}})),this.dispatchEvent(new CustomEvent("complete",{detail:{request:o,response:c,payload:l,error:void 0,options:n}})),l}appendToQueryString(t,e,i){if(null!=i)if(Array.isArray(i)||Object.getPrototypeOf(i)===Object.prototype)for(const[n,s]of Object.entries(i))this.appendToQueryString(t,`${e}[${n}]`,s);else t.append(e,String(i))}transformData(t,e,i){const n=["GET","HEAD"].includes(e.toUpperCase());if(n&&i instanceof FormData){for(const[e,n]of i)null!=n&&t.searchParams.append(e,String(n));return null}if(null!==i&&Object.getPrototypeOf(i)===Object.prototype||Array.isArray(i)){const e=n?t.searchParams:new URLSearchParams;for(const[t,n]of Object.entries(i))this.appendToQueryString(e,t,n);return n?null:e}return i}}class u extends Error{constructor(t){const e=`HTTP ${t.status}: ${t.statusText}`;super(e),this.name=this.constructor.name,this.stack=new Error(e).stack,this.response=t}}const f=new h;return f.registerExtension(new class{constructor(){this.abortControllers=new Set}initialize(t){t.uiHandler.addEventListener("interaction",this.checkAbortable.bind(this)),t.addEventListener("init",this.onInitialize.bind(this)),t.addEventListener("start",this.saveAbortController.bind(this)),t.addEventListener("complete",this.removeAbortController.bind(this))}onInitialize(){document.addEventListener("keydown",(t=>{if("Escape"===t.key&&!(t.ctrlKey||t.shiftKey||t.altKey||t.metaKey)){for(const t of this.abortControllers)t.abort();this.abortControllers.clear()}}))}checkAbortable(t){const{element:e,options:i}=t.detail;(e.hasAttribute("data-naja-abort")||e.form?.hasAttribute("data-naja-abort"))&&(i.abort="off"!==(e.getAttribute("data-naja-abort")??e.form?.getAttribute("data-naja-abort")))}saveAbortController(t){const{abortController:e,options:i}=t.detail;!1!==i.abort&&(this.abortControllers.add(e),i.clearAbortExtension=()=>this.abortControllers.delete(e))}removeAbortController(t){const{options:e}=t.detail;!1!==e.abort&&e.clearAbortExtension&&e.clearAbortExtension()}}),f.registerExtension(new class{constructor(){this.abortControllers=new Map}initialize(t){t.uiHandler.addEventListener("interaction",this.checkUniqueness.bind(this)),t.addEventListener("start",this.abortPreviousRequest.bind(this)),t.addEventListener("complete",this.clearRequest.bind(this))}checkUniqueness(t){const{element:e,options:i}=t.detail;if(e.hasAttribute("data-naja-unique")??e.form?.hasAttribute("data-naja-unique")){const t=e.getAttribute("data-naja-unique")??e.form?.getAttribute("data-naja-unique");i.unique="off"!==t&&(t??"default")}}abortPreviousRequest(t){const{abortController:e,options:i}=t.detail;!1!==i.unique&&(this.abortControllers.get(i.unique??"default")?.abort(),this.abortControllers.set(i.unique??"default",e))}clearRequest(t){const{request:e,options:i}=t.detail;e.signal.aborted||!1===i.unique||this.abortControllers.delete(i.unique??"default")}}),f.Naja=h,f.HttpError=u,f}));
//# sourceMappingURL=Naja.min.js.map