naja
Version:
Modern AJAX library for Nette Framework
240 lines (192 loc) • 7.83 kB
text/typescript
import {Naja, Options} from '../Naja';
import {InteractionEvent} from './UIHandler';
import {BuildStateEvent, HistoryState, RestoreStateEvent} from './HistoryHandler';
import {PendingUpdateEvent, SnippetHandler} from './SnippetHandler';
import {onDomReady, TypedEventListener} from '../utils';
declare module '../Naja' {
interface Options {
snippetCache?: boolean | SnippetCacheStorageType;
}
}
declare module './HistoryHandler' {
interface HistoryState {
snippets?: {
readonly storage: SnippetCacheStorageType;
readonly key: SnippetCacheKey;
};
}
}
export class SnippetCache extends EventTarget {
private readonly storages: Record<SnippetCacheStorageType, SnippetCacheStorage>;
private currentSnippets: Map<string, string> = new Map();
private static parser = new DOMParser();
public constructor(private readonly naja: Naja) {
super();
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));
}
private resolveStorage(option?: boolean | SnippetCacheStorageType): SnippetCacheStorage {
let storageType: SnippetCacheStorageType;
if (option === true || option === undefined) {
storageType = 'history';
} else if (option === false) {
storageType = 'off';
} else {
storageType = option;
}
return this.storages[storageType];
}
private static shouldCacheSnippet(snippet: Element): boolean {
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');
}
private initializeIndex(): void {
onDomReady(() => {
const currentSnippets = SnippetHandler.findSnippets(SnippetCache.shouldCacheSnippet);
this.currentSnippets = new Map(Object.entries(currentSnippets));
});
}
private updateIndex(event: PendingUpdateEvent): void {
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);
}
}
private configureCache(event: InteractionEvent): void {
const {element, options} = event.detail;
if ( ! element) {
return;
}
if (element.hasAttribute('data-naja-snippet-cache') || (element as HTMLInputElement).form?.hasAttribute('data-naja-snippet-cache')
|| element.hasAttribute('data-naja-history-cache') || (element as HTMLInputElement).form?.hasAttribute('data-naja-history-cache')
) {
const value = element.getAttribute('data-naja-snippet-cache')
?? (element as HTMLInputElement).form?.getAttribute('data-naja-snippet-cache')
?? element.getAttribute('data-naja-history-cache')
?? (element as HTMLInputElement).form?.getAttribute('data-naja-history-cache');
options.snippetCache = value as SnippetCacheStorageType;
}
}
private buildHistoryState(event: BuildStateEvent): void {
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),
};
}
private restoreHistoryState(event: RestoreStateEvent): void {
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);
}
declare public addEventListener: <K extends keyof SnippetCacheEventMap | string>(type: K, listener: TypedEventListener<SnippetHandler, K extends keyof SnippetCacheEventMap ? SnippetCacheEventMap[K] : CustomEvent>, options?: boolean | AddEventListenerOptions) => void;
declare public removeEventListener: <K extends keyof SnippetCacheEventMap | string>(type: K, listener: TypedEventListener<SnippetHandler, K extends keyof SnippetCacheEventMap ? SnippetCacheEventMap[K] : CustomEvent>, options?: boolean | AddEventListenerOptions) => void;
}
export type StoreEvent = CustomEvent<{snippets: CachedSnippets, state: HistoryState, options: Options}>;
export type FetchEvent = CustomEvent<{state: HistoryState, options: Options}>;
export type RestoreEvent = CustomEvent<{snippets: CachedSnippets, state: HistoryState, options: Options}>;
interface SnippetCacheEventMap {
store: StoreEvent;
fetch: FetchEvent;
restore: RestoreEvent;
}
type CachedSnippets = Record<string, string>;
type SnippetCacheKey = CachedSnippets | string | null;
type SnippetCacheStorageType = 'off' | 'history' | 'session';
interface SnippetCacheStorage {
readonly type: SnippetCacheStorageType;
store(data: CachedSnippets): SnippetCacheKey;
fetch(key: SnippetCacheKey, state: HistoryState, options: Options): CachedSnippets | null;
}
class OffCacheStorage implements SnippetCacheStorage {
public readonly type = 'off';
public constructor(private readonly naja: Naja) {} // eslint-disable-line no-empty-function
public store(): null {
return null;
}
public fetch(key: null, state: HistoryState, options: Options): CachedSnippets | null {
this.naja.makeRequest(
'GET',
state.href,
null,
{
...options,
history: false,
snippetCache: false,
},
);
return null;
}
}
class HistoryCacheStorage implements SnippetCacheStorage {
public readonly type = 'history';
public store(data: CachedSnippets): CachedSnippets {
return data;
}
public fetch(key: CachedSnippets): CachedSnippets | null {
return key;
}
}
class SessionCacheStorage implements SnippetCacheStorage {
public readonly type = 'session';
public store(data: CachedSnippets): string {
const key = Math.random().toString(36).substring(2, 8);
window.sessionStorage.setItem(key, JSON.stringify(data));
return key;
}
public fetch(key: string): CachedSnippets | null {
const data = window.sessionStorage.getItem(key);
if (data === null) {
return null;
}
return JSON.parse(data);
}
}