@lisergia/core 24.0.0 → 26.0.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.
package/dist/App.d.ts ADDED
@@ -0,0 +1,66 @@
1
+ import { Component, type ComponentParameters } from './Component.js';
2
+ import { Links } from './Links.js';
3
+ import type { Page, PageParameters } from './Page.js';
4
+ export interface ApplicationComponentData {
5
+ component: new (params?: ComponentParameters) => Component;
6
+ }
7
+ export interface ApplicationComponentDatasetData extends ApplicationComponentData {
8
+ selector: string;
9
+ }
10
+ export interface ApplicationRoute {
11
+ component: new (params?: PageParameters) => Page;
12
+ template: string;
13
+ }
14
+ export declare class ApplicationManager extends Component {
15
+ element: HTMLElement;
16
+ template: string;
17
+ constructor();
18
+ onTitleUpdate(): void;
19
+ onTemplateUpdate(): void;
20
+ canvas?: Component;
21
+ components: Array<Component>;
22
+ transition?: Component & {
23
+ onTransition?: (application: ApplicationManager) => void | Promise<void>;
24
+ };
25
+ initComponents(components: Array<ApplicationComponentData>): void;
26
+ addComponent(component: Component): void;
27
+ removeComponent(component: Component): void;
28
+ datasets: Array<ApplicationComponentDatasetData>;
29
+ initDatasets(datasets: Array<ApplicationComponentDatasetData>): void;
30
+ pages: Map<string, new (args: PageParameters) => Page>;
31
+ initRoutes(routes: Array<ApplicationRoute>): void;
32
+ initSprites(url?: string): Promise<void>;
33
+ IS_LINKS_ENABLED: boolean;
34
+ links: Links;
35
+ initPage(): void;
36
+ createLinks(): void;
37
+ currentPage?: Page;
38
+ createPage(template?: string): void;
39
+ destroyPage(): void;
40
+ route: string;
41
+ routeHistory: Array<string>;
42
+ setRoute(route: string): boolean;
43
+ navigate(href: string, { pushState }?: {
44
+ pushState?: boolean;
45
+ }): void;
46
+ onRouteChangeRequest({ href, pushState }: {
47
+ href: string;
48
+ pushState: boolean;
49
+ }): Promise<void>;
50
+ nextPage: {
51
+ element?: HTMLElement;
52
+ template?: string;
53
+ title?: string;
54
+ };
55
+ onRequest({ href, response, pushState }: {
56
+ href: string;
57
+ response: string;
58
+ pushState: boolean;
59
+ }): Promise<void>;
60
+ onPopState(): void;
61
+ get scroll(): number;
62
+ onResize(): void;
63
+ addEventListeners(): void;
64
+ removeEventListeners(): void;
65
+ }
66
+ export declare const Application: ApplicationManager;
package/dist/App.js ADDED
@@ -0,0 +1,241 @@
1
+ import { Component } from './Component.js';
2
+ import { Links } from './Links.js';
3
+ export class ApplicationManager extends Component {
4
+ template = document.documentElement.dataset.template ?? '404';
5
+ constructor() {
6
+ super({
7
+ autoListeners: false,
8
+ element: '.app',
9
+ });
10
+ this.addEventListeners();
11
+ }
12
+ onTitleUpdate() {
13
+ const title = this.nextPage?.title;
14
+ if (title) {
15
+ document.title = title;
16
+ }
17
+ }
18
+ onTemplateUpdate() {
19
+ const template = this.nextPage?.template;
20
+ if (template) {
21
+ document.documentElement.dataset.template = template;
22
+ this.template = template;
23
+ }
24
+ }
25
+ //
26
+ // Components.
27
+ //
28
+ canvas;
29
+ components = [];
30
+ transition;
31
+ initComponents(components) {
32
+ const classes = components.map(({ component: Component }) => new Component({
33
+ application: this,
34
+ }));
35
+ this.canvas = classes.find((component) => component.id === 'canvas');
36
+ this.transition = classes.find((component) => component.id === 'transition');
37
+ classes.forEach((component) => {
38
+ this.addComponent(component);
39
+ });
40
+ }
41
+ addComponent(component) {
42
+ this.components.push(component);
43
+ }
44
+ removeComponent(component) {
45
+ component.destroy();
46
+ const index = this.components.indexOf(component);
47
+ if (index !== -1) {
48
+ this.components.splice(index, 1);
49
+ }
50
+ }
51
+ //
52
+ // Datasets.
53
+ //
54
+ datasets = [];
55
+ initDatasets(datasets) {
56
+ this.datasets = datasets;
57
+ }
58
+ //
59
+ // Routes.
60
+ //
61
+ pages = new Map();
62
+ initRoutes(routes) {
63
+ routes.forEach(({ component, template }) => {
64
+ this.pages.set(template, component);
65
+ });
66
+ }
67
+ //
68
+ // Sprites.
69
+ //
70
+ async initSprites(url = '/bundle.svg') {
71
+ const request = await window.fetch(url);
72
+ const response = await request.text();
73
+ const sprite = document.createElement('div');
74
+ sprite.innerHTML = response;
75
+ sprite.style.left = '-999999px';
76
+ sprite.style.opacity = '0';
77
+ sprite.style.position = 'absolute';
78
+ sprite.style.top = '0';
79
+ document.body.appendChild(sprite);
80
+ }
81
+ //
82
+ // Initialization.
83
+ //
84
+ IS_LINKS_ENABLED = true;
85
+ initPage() {
86
+ this.createPage();
87
+ this.createLinks();
88
+ }
89
+ //
90
+ // Links.
91
+ //
92
+ createLinks() {
93
+ if (!this.IS_LINKS_ENABLED) {
94
+ return;
95
+ }
96
+ this.links = new Links(this);
97
+ }
98
+ //
99
+ // Page.
100
+ //
101
+ currentPage = undefined;
102
+ createPage(template = this.template) {
103
+ const PageClass = this.pages.get(template);
104
+ const page = new PageClass({
105
+ application: this,
106
+ datasets: this.datasets,
107
+ });
108
+ this.currentPage = page;
109
+ this.currentPage.create();
110
+ this.fire('page', page);
111
+ }
112
+ destroyPage() {
113
+ if (this.currentPage) {
114
+ this.currentPage.destroy();
115
+ }
116
+ }
117
+ //
118
+ // Navigate.
119
+ //
120
+ route = `${window.location.pathname}${window.location.search}${window.location.hash}`;
121
+ routeHistory = [this.route];
122
+ setRoute(route) {
123
+ if (route === this.route) {
124
+ return false;
125
+ }
126
+ this.route = route;
127
+ this.fire('route', route);
128
+ return true;
129
+ }
130
+ navigate(href, { pushState = true } = {}) {
131
+ const url = new URL(href, window.location.href);
132
+ if (url.origin !== window.location.origin) {
133
+ window.location.assign(url.href);
134
+ return;
135
+ }
136
+ const route = `${url.pathname}${url.search}${url.hash}`;
137
+ if (!this.setRoute(route)) {
138
+ return;
139
+ }
140
+ void this.onRouteChangeRequest({
141
+ href: route,
142
+ pushState,
143
+ });
144
+ }
145
+ async onRouteChangeRequest({ href, pushState = true }) {
146
+ try {
147
+ const request = await window.fetch(href);
148
+ if (request.redirected) {
149
+ window.location.assign(request.url || href);
150
+ return;
151
+ }
152
+ const contentType = request.headers.get('content-type');
153
+ if (contentType && !contentType.toLowerCase().includes('text/html')) {
154
+ window.location.assign(href);
155
+ return;
156
+ }
157
+ const response = await request.text();
158
+ await this.onRequest({
159
+ href,
160
+ response,
161
+ pushState,
162
+ });
163
+ }
164
+ catch {
165
+ window.location.assign(href);
166
+ }
167
+ }
168
+ //
169
+ // Request.
170
+ //
171
+ nextPage = {};
172
+ async onRequest({ href, response, pushState }) {
173
+ const dom = new DOMParser().parseFromString(response, 'text/html');
174
+ const html = dom.documentElement;
175
+ const app = dom.querySelector('.app');
176
+ const page = app?.firstElementChild;
177
+ const template = html.dataset.template ?? this.template;
178
+ if (!app || !(page instanceof HTMLElement) || !this.pages.has(template)) {
179
+ window.location.assign(href);
180
+ return;
181
+ }
182
+ this.nextPage = {
183
+ element: app,
184
+ template,
185
+ title: dom.title || document.title,
186
+ };
187
+ this.onTitleUpdate();
188
+ this.onTemplateUpdate();
189
+ if (this.transition) {
190
+ await this.transition.onTransition?.(this);
191
+ }
192
+ else {
193
+ this.currentPage.element.remove();
194
+ this.currentPage.destroy();
195
+ this.element.appendChild(this.nextPage.element.firstElementChild);
196
+ this.createPage(this.nextPage.template);
197
+ }
198
+ if (pushState) {
199
+ window.history.pushState({}, this.nextPage.title, href);
200
+ }
201
+ this.routeHistory.push(href);
202
+ }
203
+ //
204
+ // Pop State.
205
+ //
206
+ onPopState() {
207
+ const route = `${document.location.pathname}${document.location.search}${document.location.hash}`;
208
+ const currentUrl = new URL(this.route, window.location.origin);
209
+ const nextUrl = new URL(route, window.location.origin);
210
+ // Hash-only change: update the route without requesting a new page.
211
+ if (currentUrl.pathname === nextUrl.pathname && currentUrl.search === nextUrl.search) {
212
+ this.setRoute(route);
213
+ return;
214
+ }
215
+ this.navigate(route, { pushState: false });
216
+ }
217
+ //
218
+ // Scroll.
219
+ //
220
+ get scroll() {
221
+ return this.currentPage.scroll ?? 0;
222
+ }
223
+ //
224
+ // Resize.
225
+ //
226
+ onResize() {
227
+ this.fire('resize');
228
+ }
229
+ //
230
+ // Listeners.
231
+ //
232
+ addEventListeners() {
233
+ window.addEventListener('popstate', this.onPopState);
234
+ window.addEventListener('resize', this.onResize);
235
+ }
236
+ removeEventListeners() {
237
+ window.removeEventListener('popstate', this.onPopState);
238
+ window.removeEventListener('resize', this.onResize);
239
+ }
240
+ }
241
+ export const Application = new ApplicationManager();
@@ -0,0 +1,46 @@
1
+ import type { ApplicationManager } from './App.js';
2
+ import { EventEmitter } from './EventEmitter.js';
3
+ export interface ComponentClasses {
4
+ [key: string]: string;
5
+ }
6
+ export interface ComponentElements {
7
+ [key: string]: unknown[] | Element | Array<Element> | HTMLElement | Array<HTMLElement> | NodeList | Window | null;
8
+ }
9
+ export type ComponentSelector = string | HTMLElement;
10
+ export interface ComponentSelectors {
11
+ [key: string]: string | Element | Array<Element> | HTMLElement | Array<HTMLElement> | NodeList | Window;
12
+ }
13
+ export interface ComponentParameters {
14
+ application?: ApplicationManager;
15
+ autoListeners?: boolean;
16
+ autoMount?: boolean;
17
+ classes?: ComponentClasses;
18
+ element?: ComponentSelector;
19
+ elements?: ComponentSelectors;
20
+ id?: string;
21
+ }
22
+ export declare class Component extends EventEmitter {
23
+ application?: ApplicationManager;
24
+ autoListeners: boolean;
25
+ autoMount: boolean;
26
+ classes?: ComponentClasses;
27
+ disposers: Set<() => void>;
28
+ selector?: ComponentSelector;
29
+ selectors?: ComponentSelectors;
30
+ id?: string;
31
+ element?: HTMLElement;
32
+ elements: ComponentElements;
33
+ constructor({ application, autoListeners, autoMount, classes, element, elements, id, }: ComponentParameters);
34
+ create(): void;
35
+ initElement(selector: ComponentSelector): void;
36
+ destroyElement(): void;
37
+ initElements(selectors?: ComponentSelectors): void;
38
+ destroyElements(): void;
39
+ addDisposer(disposer: () => void): () => void;
40
+ destroyDisposers(): void;
41
+ addEventListeners(): void;
42
+ removeEventListeners(): void;
43
+ onResize(): void;
44
+ onScroll(_scroll: number): void;
45
+ destroy(): void;
46
+ }
@@ -0,0 +1,124 @@
1
+ import { EventEmitter } from './EventEmitter.js';
2
+ export class Component extends EventEmitter {
3
+ application;
4
+ autoListeners;
5
+ autoMount;
6
+ classes;
7
+ disposers = new Set();
8
+ selector;
9
+ selectors;
10
+ id;
11
+ element;
12
+ elements = {};
13
+ constructor({ application, autoListeners = true, autoMount = true, classes, element, elements, id, }) {
14
+ super();
15
+ this.application = application;
16
+ this.autoListeners = autoListeners;
17
+ this.autoMount = autoMount;
18
+ this.classes = classes;
19
+ this.selector = element;
20
+ this.selectors = elements;
21
+ this.id = id;
22
+ if (this.application) {
23
+ this.application.on('scroll', this.onScroll);
24
+ this.application.on('resize', this.onResize);
25
+ this.addDisposer(() => {
26
+ this.application?.off('scroll', this.onScroll);
27
+ this.application?.off('resize', this.onResize);
28
+ });
29
+ }
30
+ if (this.autoMount) {
31
+ this.create();
32
+ }
33
+ if (this.autoListeners) {
34
+ this.addEventListeners();
35
+ }
36
+ }
37
+ create() {
38
+ if (this.selector) {
39
+ this.initElement(this.selector);
40
+ }
41
+ if (this.selectors) {
42
+ this.initElements(this.selectors);
43
+ }
44
+ }
45
+ initElement(selector) {
46
+ if (selector instanceof HTMLElement) {
47
+ this.element = selector;
48
+ }
49
+ else {
50
+ this.element = document.querySelector(selector);
51
+ }
52
+ }
53
+ destroyElement() {
54
+ this.element = undefined;
55
+ }
56
+ initElements(selectors) {
57
+ for (const key in selectors) {
58
+ const selector = selectors[key];
59
+ if (selector === window) {
60
+ this.elements[key] = window;
61
+ }
62
+ else if (selector instanceof HTMLElement) {
63
+ this.elements[key] = selector;
64
+ }
65
+ else if (selector instanceof NodeList) {
66
+ this.elements[key] = selector;
67
+ }
68
+ else if (Array.isArray(selector)) {
69
+ this.elements[key] = selector;
70
+ }
71
+ else {
72
+ const elements = this.element.querySelectorAll(selector);
73
+ if (elements.length === 0) {
74
+ const elements = document.querySelectorAll(selector);
75
+ if (elements.length === 0) {
76
+ this.elements[key] = null;
77
+ }
78
+ else if (elements.length === 1) {
79
+ this.elements[key] = elements[0];
80
+ }
81
+ else {
82
+ this.elements[key] = elements;
83
+ }
84
+ }
85
+ else if (elements.length === 1) {
86
+ this.elements[key] = elements[0];
87
+ }
88
+ else {
89
+ this.elements[key] = elements;
90
+ }
91
+ }
92
+ }
93
+ }
94
+ destroyElements() {
95
+ this.elements = {};
96
+ }
97
+ addDisposer(disposer) {
98
+ this.disposers.add(disposer);
99
+ return disposer;
100
+ }
101
+ destroyDisposers() {
102
+ this.disposers.forEach((dispose) => {
103
+ dispose();
104
+ });
105
+ this.disposers.clear();
106
+ }
107
+ addEventListeners() { }
108
+ removeEventListeners() { }
109
+ //
110
+ // Hooks.
111
+ //
112
+ // Called whenever the application page scrolls or resizes. Wired automatically
113
+ // when the component receives an `application`; override in subclasses.
114
+ //
115
+ onResize() { }
116
+ onScroll(_scroll) { }
117
+ destroy() {
118
+ this.destroyDisposers();
119
+ super.destroy();
120
+ this.removeEventListeners();
121
+ this.destroyElements();
122
+ this.destroyElement();
123
+ }
124
+ }
@@ -0,0 +1,10 @@
1
+ import { type Emitter, type Unsubscribe } from 'nanoevents';
2
+ export declare class EventEmitter {
3
+ emitter: Emitter;
4
+ entries: Map<unknown, Unsubscribe>;
5
+ constructor();
6
+ on<Arguments extends unknown[]>(event: string, callback: (...args: Arguments) => void): void | Unsubscribe;
7
+ off<Arguments extends unknown[]>(_event: string, callback: (...args: Arguments) => void): void;
8
+ fire(event: string, ...args: unknown[]): void;
9
+ destroy(): void;
10
+ }
@@ -0,0 +1,34 @@
1
+ import AutoBind from 'auto-bind';
2
+ import { createNanoEvents } from 'nanoevents';
3
+ export class EventEmitter {
4
+ emitter;
5
+ entries = new Map();
6
+ constructor() {
7
+ AutoBind(this);
8
+ this.emitter = createNanoEvents();
9
+ }
10
+ on(event, callback) {
11
+ if (!callback) {
12
+ return console.trace('No callback provided');
13
+ }
14
+ const emitter = this.emitter.on(event, callback);
15
+ this.entries.set(callback, emitter);
16
+ return emitter;
17
+ }
18
+ off(_event, callback) {
19
+ const unsubscribe = this.entries.get(callback);
20
+ if (unsubscribe) {
21
+ unsubscribe();
22
+ }
23
+ this.entries.delete(callback);
24
+ }
25
+ fire(event, ...args) {
26
+ this.emitter.emit(event, ...args);
27
+ }
28
+ destroy() {
29
+ this.entries.forEach((unsubscribe) => {
30
+ unsubscribe();
31
+ });
32
+ this.entries.clear();
33
+ }
34
+ }
package/dist/Link.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { Component } from './Component.js';
2
+ export declare class Link extends Component {
3
+ element: HTMLAnchorElement;
4
+ constructor({ element }: {
5
+ element: HTMLAnchorElement;
6
+ });
7
+ onClick(event: MouseEvent): void;
8
+ shouldHandleClick(event: MouseEvent): boolean;
9
+ addEventListeners(): void;
10
+ removeEventListeners(): void;
11
+ }
package/dist/Link.js ADDED
@@ -0,0 +1,57 @@
1
+ import { Component } from './Component.js';
2
+ export class Link extends Component {
3
+ constructor({ element }) {
4
+ super({ element });
5
+ }
6
+ onClick(event) {
7
+ if (!this.shouldHandleClick(event)) {
8
+ return;
9
+ }
10
+ event.preventDefault();
11
+ this.fire('click', new URL(this.element.href, window.location.href).href);
12
+ }
13
+ shouldHandleClick(event) {
14
+ if (event.defaultPrevented ||
15
+ event.button !== 0 ||
16
+ event.metaKey ||
17
+ event.ctrlKey ||
18
+ event.shiftKey ||
19
+ event.altKey ||
20
+ this.element.hasAttribute('download')) {
21
+ return false;
22
+ }
23
+ const target = this.element.getAttribute('target');
24
+ if (target && target.toLowerCase() !== '_self') {
25
+ return false;
26
+ }
27
+ const url = new URL(this.element.href, window.location.href);
28
+ const isHttp = url.protocol === 'http:' || url.protocol === 'https:';
29
+ const isLocal = isHttp && url.origin === window.location.origin;
30
+ const hasHashReference = this.element.getAttribute('href')?.includes('#') ?? false;
31
+ const isSameDocumentHash = hasHashReference && url.pathname === window.location.pathname && url.search === window.location.search;
32
+ return isLocal && !isSameDocumentHash;
33
+ }
34
+ addEventListeners() {
35
+ const url = new URL(this.element.href, window.location.href);
36
+ const isHttp = url.protocol === 'http:' || url.protocol === 'https:';
37
+ const isLocal = isHttp && url.origin === window.location.origin;
38
+ if (isLocal) {
39
+ this.element.onclick = this.onClick;
40
+ }
41
+ else if (isHttp) {
42
+ if (!this.element.hasAttribute('target')) {
43
+ this.element.setAttribute('target', '_blank');
44
+ }
45
+ if (this.element.target === '_blank') {
46
+ const rel = new Set(this.element.rel.split(/\s+/).filter(Boolean));
47
+ rel.add('noopener');
48
+ this.element.rel = Array.from(rel).join(' ');
49
+ }
50
+ }
51
+ }
52
+ removeEventListeners() {
53
+ if (this.element.onclick === this.onClick) {
54
+ this.element.onclick = null;
55
+ }
56
+ }
57
+ }
@@ -0,0 +1,12 @@
1
+ import type { ApplicationManager } from './App.js';
2
+ import { EventEmitter } from './EventEmitter.js';
3
+ import { Link } from './Link.js';
4
+ export declare class Links extends EventEmitter {
5
+ application: ApplicationManager;
6
+ links: Array<Link>;
7
+ constructor(application: ApplicationManager);
8
+ addEventListeners(): void;
9
+ onLinkClick(href: string): void;
10
+ refresh(): void;
11
+ destroy(): void;
12
+ }
package/dist/Links.js ADDED
@@ -0,0 +1,36 @@
1
+ import { EventEmitter } from './EventEmitter.js';
2
+ import { Link } from './Link.js';
3
+ export class Links extends EventEmitter {
4
+ constructor(application) {
5
+ super();
6
+ this.application = application;
7
+ this.application.on('page', this.refresh);
8
+ this.refresh();
9
+ }
10
+ addEventListeners() {
11
+ this.links?.forEach((link) => {
12
+ link.destroy();
13
+ });
14
+ const links = document.querySelectorAll('a');
15
+ this.links = Array.from(links).map((element) => {
16
+ const link = new Link({
17
+ element,
18
+ });
19
+ link.on('click', this.onLinkClick);
20
+ return link;
21
+ });
22
+ }
23
+ onLinkClick(href) {
24
+ this.application.navigate(href);
25
+ }
26
+ refresh() {
27
+ this.addEventListeners();
28
+ }
29
+ destroy() {
30
+ this.application.off('page', this.refresh);
31
+ this.links?.forEach((link) => {
32
+ link.destroy();
33
+ });
34
+ super.destroy();
35
+ }
36
+ }
package/dist/Page.d.ts ADDED
@@ -0,0 +1,52 @@
1
+ import Lenis, { type LenisOptions } from 'lenis';
2
+ import { type TempusState } from 'tempus';
3
+ import type { ApplicationManager } from './App.js';
4
+ import { Component, type ComponentClasses, type ComponentSelectors } from './Component.js';
5
+ export interface PageParameters {
6
+ application: ApplicationManager;
7
+ classes?: ComponentClasses;
8
+ datasets: Array<{
9
+ component: typeof Component;
10
+ selector: string;
11
+ }>;
12
+ element?: HTMLElement | string;
13
+ elements?: ComponentSelectors;
14
+ scrollEnabled?: boolean;
15
+ scrollOptions?: LenisOptions;
16
+ }
17
+ export declare class Page extends Component {
18
+ element: HTMLElement;
19
+ elements: {
20
+ wrapper: HTMLElement;
21
+ };
22
+ application: ApplicationManager;
23
+ datasets: Array<{
24
+ component: typeof Component;
25
+ selector: string;
26
+ }>;
27
+ scroll: number;
28
+ scrollEnabled: boolean;
29
+ scrollOptions: LenisOptions;
30
+ unsubscribeRaf: (() => void) | undefined;
31
+ constructor({ application, classes, datasets, element, elements, scrollEnabled, scrollOptions, }: PageParameters);
32
+ create(): void;
33
+ resizeObserver: ResizeObserver;
34
+ createResizeObserver(): void;
35
+ destroyResizeObserver(): void;
36
+ components: Set<Component>;
37
+ createComponents(): void;
38
+ destroyComponents(): void;
39
+ lenis: Lenis;
40
+ createScroll(): void;
41
+ onLenisScroll(event: {
42
+ scroll: number;
43
+ }): void;
44
+ destroyScroll(): void;
45
+ onResize(): void;
46
+ onRAF({ time }: TempusState): void;
47
+ onScroll(scroll: number): void;
48
+ onScrollFallback(): void;
49
+ addEventListeners(): void;
50
+ removeEventListeners(): void;
51
+ destroy(): void;
52
+ }