naja
Version:
Modern AJAX library for Nette Framework
860 lines (847 loc) • 34 kB
JavaScript
/*
* Naja.js
* 3.2.1
*
* by Jiří Pudil <https://jiripudil.cz>
*/
// ready
const onDomReady = (callback) => {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', callback);
}
else {
callback();
}
};
class UIHandler extends EventTarget {
constructor(naja) {
super();
this.naja = naja;
this.selector = '.ajax';
this.allowedOrigins = [window.location.origin];
this.handler = this.handleUI.bind(this);
naja.addEventListener('init', this.initialize.bind(this));
}
initialize() {
onDomReady(() => this.bindUI(window.document.body));
this.naja.snippetHandler.addEventListener('afterUpdate', (event) => {
const { snippet } = event.detail;
this.bindUI(snippet);
});
}
bindUI(element) {
const selector = `a${this.selector}`;
const bindElement = (element) => {
element.removeEventListener('click', this.handler);
element.addEventListener('click', this.handler);
};
if (element.matches(selector)) {
return bindElement(element);
}
const elements = element.querySelectorAll(selector);
elements.forEach((element) => bindElement(element));
const bindForm = (form) => {
form.removeEventListener('submit', this.handler);
form.addEventListener('submit', this.handler);
};
if (element instanceof HTMLFormElement) {
return bindForm(element);
}
const forms = element.querySelectorAll('form');
forms.forEach((form) => bindForm(form));
}
handleUI(event) {
const element = event.currentTarget;
const options = this.naja.prepareOptions();
const ignoreErrors = () => {
// don't reject the promise in case of an error as developers have no way of handling the rejection
// in this situation; errors should be handled in `naja.addEventListener('error', errorHandler)`
};
if (event instanceof MouseEvent) {
if (event.altKey || event.ctrlKey || event.shiftKey || event.metaKey || event.button) {
return;
}
this.clickElement(element, options, event).catch(ignoreErrors);
return;
}
const { submitter } = event;
if (this.selector === '' || element.matches(this.selector) || submitter?.matches(this.selector)) {
this.submitForm(submitter ?? element, options, event).catch(ignoreErrors);
}
}
async clickElement(element, options = {}, event) {
if (element instanceof HTMLAnchorElement) {
return this.processInteraction(element, 'GET', element.href, null, options, event);
}
if ((element instanceof HTMLInputElement || element instanceof HTMLButtonElement) && element.form) {
return this.submitForm(element, options, event);
}
throw new Error('Unsupported element in clickElement(): element must be an anchor or a submitter element attached to a form.');
}
async submitForm(formOrSubmitter, options = {}, event) {
let form;
let submitter = null;
if ((formOrSubmitter instanceof HTMLInputElement || formOrSubmitter instanceof HTMLButtonElement) && formOrSubmitter.form) {
// eslint-disable-next-line prefer-destructuring
form = formOrSubmitter.form;
submitter = formOrSubmitter;
}
else if (formOrSubmitter instanceof HTMLFormElement) {
form = formOrSubmitter;
submitter = event instanceof SubmitEvent ? event.submitter : null;
}
else {
throw new Error('Unsupported element in submitForm(): formOrSubmitter must be either a form or a submitter element attached to a form.');
}
const method = (submitter?.getAttribute('formmethod') ?? form.getAttribute('method') ?? 'GET').toUpperCase();
const url = submitter?.getAttribute('formaction') ?? form.getAttribute('action') ?? window.location.pathname + window.location.search;
const data = new FormData(form, submitter);
return this.processInteraction(submitter ?? form, method, url, data, options, event);
}
async processInteraction(element, method, url, data = null, options = {}, event) {
if (!this.dispatchEvent(new CustomEvent('interaction', { cancelable: true, detail: { element, originalEvent: event, options } }))) {
event?.preventDefault();
return {};
}
if (!this.isUrlAllowed(`${url}`)) {
throw new Error(`Cannot dispatch async request, URL is not allowed: ${url}`);
}
event?.preventDefault();
return this.naja.makeRequest(method, url, data, options);
}
isUrlAllowed(url) {
const urlObject = new URL(url, location.href);
// ignore non-URL URIs (javascript:, data:, mailto:, ...)
if (urlObject.origin === 'null') {
return false;
}
return this.allowedOrigins.includes(urlObject.origin);
}
}
class FormsHandler {
constructor(naja) {
this.naja = naja;
naja.addEventListener('init', this.initialize.bind(this));
naja.uiHandler.addEventListener('interaction', this.processForm.bind(this));
}
initialize() {
onDomReady(() => this.initForms(window.document.body));
this.naja.snippetHandler.addEventListener('afterUpdate', (event) => {
const { snippet } = event.detail;
this.initForms(snippet);
});
}
initForms(element) {
const netteForms = this.netteForms || window.Nette;
if (!netteForms) {
return;
}
if (element instanceof HTMLFormElement) {
netteForms.initForm(element);
return;
}
const forms = element.querySelectorAll('form');
forms.forEach((form) => netteForms.initForm(form));
}
processForm(event) {
const { element, originalEvent } = event.detail;
const isForm = element instanceof HTMLFormElement;
const isSubmitter = (element instanceof HTMLInputElement || element instanceof HTMLButtonElement) && element.form;
if (isSubmitter) {
element.form['nette-submittedBy'] = element;
}
const netteForms = this.netteForms || window.Nette;
if ((isForm || isSubmitter) && netteForms && !netteForms.validateForm(element)) {
originalEvent?.stopImmediatePropagation();
originalEvent?.preventDefault();
event.preventDefault();
}
}
}
class RedirectHandler extends EventTarget {
constructor(naja) {
super();
this.naja = naja;
naja.uiHandler.addEventListener('interaction', (event) => {
const { element, options } = event.detail;
if (element.hasAttribute('data-naja-force-redirect') || element.form?.hasAttribute('data-naja-force-redirect')) {
const value = element.getAttribute('data-naja-force-redirect') ?? element.form?.getAttribute('data-naja-force-redirect');
options.forceRedirect = value !== 'off';
}
});
naja.addEventListener('success', (event) => {
const { payload, options } = event.detail;
if (!payload.redirect) {
return;
}
this.makeRedirect(payload.redirect, options.forceRedirect ?? false, options);
event.stopImmediatePropagation();
});
this.locationAdapter = {
assign: (url) => window.location.assign(url),
};
}
makeRedirect(url, force, options = {}) {
if (url instanceof URL) {
url = url.href;
}
let isHardRedirect = force || !this.naja.uiHandler.isUrlAllowed(url);
const canRedirect = this.dispatchEvent(new CustomEvent('redirect', {
cancelable: true,
detail: {
url,
setUrl(value) {
url = value;
},
isHardRedirect,
setHardRedirect(value) {
isHardRedirect = !!value;
},
options,
},
}));
if (!canRedirect) {
return;
}
if (isHardRedirect) {
this.locationAdapter.assign(url);
}
else {
this.naja.makeRequest('GET', url, null, options);
}
}
}
class SnippetHandler extends EventTarget {
constructor(naja) {
super();
this.op = {
replace: {
updateElement(snippet, content) {
snippet.innerHTML = content;
},
updateIndex(_, newContent) {
return newContent;
},
},
prepend: {
updateElement(snippet, content) {
snippet.insertAdjacentHTML('afterbegin', content);
},
updateIndex(currentContent, newContent) {
return newContent + currentContent;
},
},
append: {
updateElement(snippet, content) {
snippet.insertAdjacentHTML('beforeend', content);
},
updateIndex(currentContent, newContent) {
return currentContent + newContent;
},
},
};
naja.addEventListener('success', (event) => {
const { options, payload } = event.detail;
if (!payload.snippets) {
return;
}
this.updateSnippets(payload.snippets, false, options);
});
}
static findSnippets(predicate, document = window.document) {
const result = {};
const snippets = document.querySelectorAll('[id^="snippet-"]');
snippets.forEach((snippet) => {
if (predicate?.(snippet) ?? true) {
result[snippet.id] = snippet.innerHTML;
}
});
return result;
}
async updateSnippets(snippets, fromCache = false, options = {}) {
await Promise.all(Object.keys(snippets).map(async (id) => {
const snippet = document.getElementById(id);
if (snippet) {
await this.updateSnippet(snippet, snippets[id], fromCache, options);
}
}));
}
async updateSnippet(snippet, content, fromCache, options) {
let operation = this.op.replace;
if ((snippet.hasAttribute('data-naja-snippet-prepend') || snippet.hasAttribute('data-ajax-prepend')) && !fromCache) {
operation = this.op.prepend;
}
else if ((snippet.hasAttribute('data-naja-snippet-append') || snippet.hasAttribute('data-ajax-append')) && !fromCache) {
operation = this.op.append;
}
const canUpdate = this.dispatchEvent(new CustomEvent('beforeUpdate', {
cancelable: true,
detail: {
snippet,
content,
fromCache,
operation,
changeOperation(value) {
operation = value;
},
options,
},
}));
if (!canUpdate) {
return;
}
this.dispatchEvent(new CustomEvent('pendingUpdate', {
detail: {
snippet,
content,
fromCache,
operation,
options,
},
}));
const updateElement = typeof operation === 'function' ? operation : operation.updateElement;
await updateElement(snippet, content);
this.dispatchEvent(new CustomEvent('afterUpdate', {
detail: {
snippet,
content,
fromCache,
operation,
options,
},
}));
}
}
const originalTitleKey = Symbol();
class HistoryHandler extends EventTarget {
constructor(naja) {
super();
this.naja = naja;
this.initialized = false;
this.cursor = 0;
this.popStateHandler = this.handlePopState.bind(this);
naja.addEventListener('init', this.initialize.bind(this));
naja.addEventListener('before', this.saveUrl.bind(this));
naja.addEventListener('before', this.saveOriginalTitle.bind(this));
naja.addEventListener('before', this.replaceInitialState.bind(this));
naja.addEventListener('success', this.pushNewState.bind(this));
naja.redirectHandler.addEventListener('redirect', this.saveRedirectedUrl.bind(this));
naja.uiHandler.addEventListener('interaction', this.configureMode.bind(this));
this.historyAdapter = {
replaceState: (state, title, url) => window.history.replaceState(state, title, url),
pushState: (state, title, url) => window.history.pushState(state, title, url),
};
}
set uiCache(value) {
console.warn('Naja: HistoryHandler.uiCache is deprecated, use options.snippetCache instead.');
this.naja.defaultOptions.snippetCache = value;
}
handlePopState(event) {
const { state } = event;
if (state?.source !== 'naja') {
return;
}
const direction = state.cursor - this.cursor;
this.cursor = state.cursor;
const options = this.naja.prepareOptions();
this.dispatchEvent(new CustomEvent('restoreState', { detail: { state, direction, options } }));
}
initialize() {
window.addEventListener('popstate', this.popStateHandler);
}
saveOriginalTitle(event) {
const { options } = event.detail;
options[originalTitleKey] = window.document.title;
}
saveUrl(event) {
const { url, options } = event.detail;
options.href ??= url;
}
saveRedirectedUrl(event) {
const { url, options } = event.detail;
options.href = url;
}
replaceInitialState(event) {
const { options } = event.detail;
const mode = HistoryHandler.normalizeMode(options.history);
if (mode !== false && !this.initialized) {
onDomReady(() => this.historyAdapter.replaceState(this.buildState(window.location.href, 'replace', this.cursor, options), window.document.title, window.location.href));
this.initialized = true;
}
}
configureMode(event) {
const { element, options } = event.detail;
if (element.hasAttribute('data-naja-history') || element.form?.hasAttribute('data-naja-history')) {
const value = element.getAttribute('data-naja-history') ?? element.form?.getAttribute('data-naja-history');
options.history = HistoryHandler.normalizeMode(value);
}
}
static normalizeMode(mode) {
if (mode === 'off' || mode === false) {
return false;
}
else if (mode === 'replace') {
return 'replace';
}
return true;
}
pushNewState(event) {
const { payload, options } = event.detail;
const mode = HistoryHandler.normalizeMode(options.history);
if (mode === false) {
return;
}
if (payload.postGet && payload.url) {
options.href = payload.url;
}
const method = mode === 'replace' ? 'replaceState' : 'pushState';
const cursor = mode === 'replace' ? this.cursor : ++this.cursor;
const state = this.buildState(options.href, mode, cursor, options);
// before the state is pushed into history, revert to the original title
const newTitle = window.document.title;
window.document.title = options[originalTitleKey];
this.historyAdapter[method](state, newTitle, options.href);
// after the state is pushed into history, update back to the new title
window.document.title = newTitle;
}
buildState(href, mode, cursor, options) {
const state = {
source: 'naja',
cursor,
href,
};
this.dispatchEvent(new CustomEvent('buildState', {
detail: {
state,
operation: mode === 'replace' ? 'replaceState' : 'pushState',
options,
},
}));
return state;
}
}
class SnippetCache extends EventTarget {
constructor(naja) {
super();
this.naja = naja;
this.currentSnippets = new Map();
this.storages = {
off: new OffCacheStorage(naja),
history: new HistoryCacheStorage(),
session: new SessionCacheStorage(),
};
naja.addEventListener('init', this.initializeIndex.bind(this));
naja.snippetHandler.addEventListener('pendingUpdate', this.updateIndex.bind(this));
naja.uiHandler.addEventListener('interaction', this.configureCache.bind(this));
naja.historyHandler.addEventListener('buildState', this.buildHistoryState.bind(this));
naja.historyHandler.addEventListener('restoreState', this.restoreHistoryState.bind(this));
}
resolveStorage(option) {
let storageType;
if (option === true || option === undefined) {
storageType = 'history';
}
else if (option === false) {
storageType = 'off';
}
else {
storageType = option;
}
return this.storages[storageType];
}
static shouldCacheSnippet(snippet) {
return !snippet.hasAttribute('data-naja-history-nocache')
&& !snippet.hasAttribute('data-history-nocache')
&& (!snippet.hasAttribute('data-naja-snippet-cache')
|| snippet.getAttribute('data-naja-snippet-cache') !== 'off');
}
initializeIndex() {
onDomReady(() => {
const currentSnippets = SnippetHandler.findSnippets(SnippetCache.shouldCacheSnippet);
this.currentSnippets = new Map(Object.entries(currentSnippets));
});
}
updateIndex(event) {
const { snippet, content, operation } = event.detail;
if (!SnippetCache.shouldCacheSnippet(snippet)) {
return;
}
const currentContent = this.currentSnippets.get(snippet.id) ?? '';
const updateIndex = typeof operation === 'object'
? operation.updateIndex
: () => content;
this.currentSnippets.set(snippet.id, updateIndex(currentContent, content));
// update nested snippets
const snippetContent = SnippetCache.parser.parseFromString(content, 'text/html');
const nestedSnippets = SnippetHandler.findSnippets(SnippetCache.shouldCacheSnippet, snippetContent);
for (const [id, content] of Object.entries(nestedSnippets)) {
this.currentSnippets.set(id, content);
}
}
configureCache(event) {
const { element, options } = event.detail;
if (!element) {
return;
}
if (element.hasAttribute('data-naja-snippet-cache') || element.form?.hasAttribute('data-naja-snippet-cache')
|| element.hasAttribute('data-naja-history-cache') || element.form?.hasAttribute('data-naja-history-cache')) {
const value = element.getAttribute('data-naja-snippet-cache')
?? element.form?.getAttribute('data-naja-snippet-cache')
?? element.getAttribute('data-naja-history-cache')
?? element.form?.getAttribute('data-naja-history-cache');
options.snippetCache = value;
}
}
buildHistoryState(event) {
const { state, options } = event.detail;
if ('historyUiCache' in options) {
console.warn('Naja: options.historyUiCache is deprecated, use options.snippetCache instead.');
options.snippetCache = options.historyUiCache;
}
const presentSnippetIds = Object.keys(SnippetHandler.findSnippets(SnippetCache.shouldCacheSnippet));
const snippets = Object.fromEntries(Array.from(this.currentSnippets).filter(([id]) => presentSnippetIds.includes(id)));
if (!this.dispatchEvent(new CustomEvent('store', { cancelable: true, detail: { snippets, state, options } }))) {
return;
}
const storage = this.resolveStorage(options.snippetCache);
state.snippets = {
storage: storage.type,
key: storage.store(snippets),
};
}
restoreHistoryState(event) {
const { state, options } = event.detail;
if (state.snippets === undefined) {
return;
}
options.snippetCache = state.snippets.storage;
if (!this.dispatchEvent(new CustomEvent('fetch', { cancelable: true, detail: { state, options } }))) {
return;
}
const storage = this.resolveStorage(options.snippetCache);
const snippets = storage.fetch(state.snippets.key, state, options);
if (snippets === null) {
return;
}
if (!this.dispatchEvent(new CustomEvent('restore', { cancelable: true, detail: { snippets, state, options } }))) {
return;
}
this.naja.snippetHandler.updateSnippets(snippets, true, options);
}
}
SnippetCache.parser = new DOMParser();
class OffCacheStorage {
constructor(naja) {
this.naja = naja;
this.type = 'off';
} // eslint-disable-line no-empty-function
store() {
return null;
}
fetch(key, state, options) {
this.naja.makeRequest('GET', state.href, null, {
...options,
history: false,
snippetCache: false,
});
return null;
}
}
class HistoryCacheStorage {
constructor() {
this.type = 'history';
}
store(data) {
return data;
}
fetch(key) {
return key;
}
}
class SessionCacheStorage {
constructor() {
this.type = 'session';
}
store(data) {
const key = Math.random().toString(36).substring(2, 8);
window.sessionStorage.setItem(key, JSON.stringify(data));
return key;
}
fetch(key) {
const data = window.sessionStorage.getItem(key);
if (data === null) {
return null;
}
return JSON.parse(data);
}
}
class ScriptLoader {
constructor(naja) {
this.naja = naja;
this.loadedScripts = new Set();
naja.addEventListener('init', this.initialize.bind(this));
}
initialize() {
onDomReady(() => {
document.querySelectorAll('script[data-naja-script-id]').forEach((script) => {
const scriptId = script.getAttribute('data-naja-script-id');
if (scriptId !== null && scriptId !== '') {
this.loadedScripts.add(scriptId);
}
});
});
this.naja.snippetHandler.addEventListener('afterUpdate', (event) => {
const { content } = event.detail;
this.loadScripts(content);
});
}
loadScripts(snippetsOrSnippet) {
if (typeof snippetsOrSnippet === 'string') {
this.loadScriptsInSnippet(snippetsOrSnippet);
return;
}
Object.keys(snippetsOrSnippet).forEach((id) => {
const content = snippetsOrSnippet[id];
this.loadScriptsInSnippet(content);
});
}
loadScriptsInSnippet(content) {
if (!/<script/i.test(content)) {
return;
}
const snippetContent = ScriptLoader.parser.parseFromString(content, 'text/html');
const scripts = snippetContent.querySelectorAll('script');
scripts.forEach((script) => {
const scriptId = script.getAttribute('data-naja-script-id');
if (scriptId !== null && scriptId !== '' && this.loadedScripts.has(scriptId)) {
return;
}
const scriptEl = window.document.createElement('script');
scriptEl.innerHTML = script.innerHTML;
if (script.hasAttributes()) {
for (const attribute of script.attributes) {
scriptEl.setAttribute(attribute.name, attribute.value);
}
}
window.document.head.appendChild(scriptEl)
.parentNode.removeChild(scriptEl);
if (scriptId !== null && scriptId !== '') {
this.loadedScripts.add(scriptId);
}
});
}
}
ScriptLoader.parser = new DOMParser();
class Naja extends EventTarget {
constructor(uiHandler, redirectHandler, snippetHandler, formsHandler, historyHandler, snippetCache, scriptLoader) {
super();
this.VERSION = 3;
this.initialized = false;
this.extensions = [];
this.defaultOptions = {};
this.uiHandler = new (uiHandler ?? UIHandler)(this);
this.redirectHandler = new (redirectHandler ?? RedirectHandler)(this);
this.snippetHandler = new (snippetHandler ?? SnippetHandler)(this);
this.formsHandler = new (formsHandler ?? FormsHandler)(this);
this.historyHandler = new (historyHandler ?? HistoryHandler)(this);
this.snippetCache = new (snippetCache ?? SnippetCache)(this);
this.scriptLoader = new (scriptLoader ?? ScriptLoader)(this);
}
registerExtension(extension) {
if (this.initialized) {
extension.initialize(this);
}
this.extensions.push(extension);
}
initialize(defaultOptions = {}) {
if (this.initialized) {
throw new Error('Cannot initialize Naja, it is already initialized.');
}
this.defaultOptions = this.prepareOptions(defaultOptions);
this.extensions.forEach((extension) => extension.initialize(this));
this.dispatchEvent(new CustomEvent('init', { detail: { defaultOptions: this.defaultOptions } }));
this.initialized = true;
}
prepareOptions(options) {
return {
...this.defaultOptions,
...options,
fetch: {
...this.defaultOptions.fetch,
...options?.fetch,
},
};
}
async makeRequest(method, url, data = null, options = {}) {
// normalize url to instanceof URL
if (typeof url === 'string') {
url = new URL(url, location.href);
}
options = this.prepareOptions(options);
const headers = new Headers(options.fetch.headers || {});
const body = this.transformData(url, method, data);
const abortController = new AbortController();
const request = new Request(url.toString(), {
credentials: 'same-origin',
...options.fetch,
method,
headers,
body,
signal: abortController.signal,
});
// impersonate XHR so that Nette can detect isAjax()
request.headers.set('X-Requested-With', 'XMLHttpRequest');
// hint the server that Naja expects response to be JSON
request.headers.set('Accept', 'application/json');
if (!this.dispatchEvent(new CustomEvent('before', { cancelable: true, detail: { request, method, url: url.toString(), data, options } }))) {
return {};
}
const promise = window.fetch(request);
this.dispatchEvent(new CustomEvent('start', { detail: { request, promise, abortController, options } }));
let response, payload;
try {
response = await promise;
if (!response.ok) {
throw new HttpError(response);
}
payload = await response.json();
}
catch (error) {
if (error.name === 'AbortError') {
this.dispatchEvent(new CustomEvent('abort', { detail: { request, error, options } }));
this.dispatchEvent(new CustomEvent('complete', { detail: { request, response, payload: undefined, error, options } }));
return {};
}
this.dispatchEvent(new CustomEvent('error', { detail: { request, response, error, options } }));
this.dispatchEvent(new CustomEvent('complete', { detail: { request, response, payload: undefined, error, options } }));
throw error;
}
this.dispatchEvent(new CustomEvent('payload', { detail: { request, response, payload, options } }));
this.dispatchEvent(new CustomEvent('success', { detail: { request, response, payload, options } }));
this.dispatchEvent(new CustomEvent('complete', { detail: { request, response, payload, error: undefined, options } }));
return payload;
}
appendToQueryString(searchParams, key, value) {
if (value === null || value === undefined) {
return;
}
if (Array.isArray(value) || Object.getPrototypeOf(value) === Object.prototype) {
for (const [subkey, subvalue] of Object.entries(value)) {
this.appendToQueryString(searchParams, `${key}[${subkey}]`, subvalue);
}
}
else {
searchParams.append(key, String(value));
}
}
transformData(url, method, data) {
const isGet = ['GET', 'HEAD'].includes(method.toUpperCase());
// sending a form via GET -> serialize FormData into URL and return empty request body
if (isGet && data instanceof FormData) {
for (const [key, value] of data) {
if (value !== null && value !== undefined) {
url.searchParams.append(key, String(value));
}
}
return null;
}
// sending a POJO -> serialize it recursively into URLSearchParams
const isDataPojo = data !== null && Object.getPrototypeOf(data) === Object.prototype;
if (isDataPojo || Array.isArray(data)) {
// for GET requests, append values to URL and return empty request body
// otherwise build `new URLSearchParams()` to act as the request body
const transformedData = isGet ? url.searchParams : new URLSearchParams();
for (const [key, value] of Object.entries(data)) {
this.appendToQueryString(transformedData, key, value);
}
return isGet
? null
: transformedData;
}
return data;
}
}
class HttpError extends Error {
constructor(response) {
const message = `HTTP ${response.status}: ${response.statusText}`;
super(message);
this.name = this.constructor.name;
this.stack = new Error(message).stack;
this.response = response;
}
}
class AbortExtension {
constructor() {
this.abortControllers = new Set();
}
initialize(naja) {
naja.uiHandler.addEventListener('interaction', this.checkAbortable.bind(this));
naja.addEventListener('init', this.onInitialize.bind(this));
naja.addEventListener('start', this.saveAbortController.bind(this));
naja.addEventListener('complete', this.removeAbortController.bind(this));
}
onInitialize() {
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape' && !(event.ctrlKey || event.shiftKey || event.altKey || event.metaKey)) {
for (const controller of this.abortControllers) {
controller.abort();
}
this.abortControllers.clear();
}
});
}
checkAbortable(event) {
const { element, options } = event.detail;
if (element.hasAttribute('data-naja-abort') || element.form?.hasAttribute('data-naja-abort')) {
options.abort = (element.getAttribute('data-naja-abort') ?? element.form?.getAttribute('data-naja-abort')) !== 'off';
}
}
saveAbortController(event) {
const { abortController, options } = event.detail;
if (options.abort !== false) {
this.abortControllers.add(abortController);
options.clearAbortExtension = () => this.abortControllers.delete(abortController);
}
}
removeAbortController(event) {
const { options } = event.detail;
if (options.abort !== false && !!options.clearAbortExtension) {
options.clearAbortExtension();
}
}
}
class UniqueExtension {
constructor() {
this.abortControllers = new Map();
}
initialize(naja) {
naja.uiHandler.addEventListener('interaction', this.checkUniqueness.bind(this));
naja.addEventListener('start', this.abortPreviousRequest.bind(this));
naja.addEventListener('complete', this.clearRequest.bind(this));
}
checkUniqueness(event) {
const { element, options } = event.detail;
if (element.hasAttribute('data-naja-unique') ?? element.form?.hasAttribute('data-naja-unique')) {
const unique = element.getAttribute('data-naja-unique') ?? element.form?.getAttribute('data-naja-unique');
options.unique = unique === 'off' ? false : unique ?? 'default';
}
}
abortPreviousRequest(event) {
const { abortController, options } = event.detail;
if (options.unique !== false) {
this.abortControllers.get(options.unique ?? 'default')?.abort();
this.abortControllers.set(options.unique ?? 'default', abortController);
}
}
clearRequest(event) {
const { request, options } = event.detail;
if (!request.signal.aborted && options.unique !== false) {
this.abortControllers.delete(options.unique ?? 'default');
}
}
}
const naja = new Naja();
naja.registerExtension(new AbortExtension());
naja.registerExtension(new UniqueExtension());
export { HttpError, Naja, naja as default };
//# sourceMappingURL=Naja.esm.js.map