@lisergia/core 0.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/package.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@lisergia/core",
3
+ "version": "0.0.0",
4
+ "exports": {
5
+ "./*": "./src/*.ts"
6
+ },
7
+ "main": "./src/index.ts",
8
+ "files": [
9
+ "src"
10
+ ],
11
+ "dependencies": {
12
+ "auto-bind": "^5.0.1",
13
+ "lenis": "^1.3.1",
14
+ "mobx": "^6.13.7",
15
+ "nanoevents": "^9.1.0",
16
+ "tempus": "1.0.0-dev.10"
17
+ }
18
+ }
package/src/App.ts ADDED
@@ -0,0 +1,293 @@
1
+ import { autorun, computed, IArrayDidChange, IValueDidChange, makeObservable, observable, observe } from 'mobx'
2
+
3
+ import { Component, ComponentParameters } from './Component'
4
+ import { Links } from './Links'
5
+ import { Page, PageParameters } from './Page'
6
+
7
+ export interface ApplicationComponentData {
8
+ component: new (params?: ComponentParameters) => Component
9
+ }
10
+
11
+ export interface ApplicationComponentDatasetData extends ApplicationComponentData {
12
+ selector: string
13
+ }
14
+
15
+ export interface ApplicationRoute {
16
+ component: new (params?: PageParameters) => Page
17
+ template: string
18
+ }
19
+
20
+ export class ApplicationManager extends Component {
21
+ declare element: HTMLElement
22
+
23
+ template: string = document.documentElement.dataset.template ?? '404'
24
+
25
+ constructor() {
26
+ super({
27
+ autoListeners: false,
28
+ element: '.app',
29
+ })
30
+
31
+ makeObservable(this, {
32
+ // Application DOM Element.
33
+ element: observable,
34
+
35
+ // Components.
36
+ canvas: observable,
37
+ components: observable,
38
+ transition: observable,
39
+
40
+ // Page Information.
41
+ currentPage: observable,
42
+ nextPage: observable,
43
+
44
+ // Route Information.
45
+ route: observable,
46
+
47
+ // Template Information.
48
+ template: observable,
49
+
50
+ // Page Scroll.
51
+ scroll: computed,
52
+ })
53
+
54
+ observe(this.components, this.onComponentChange)
55
+ observe(this, 'route', this.onRouteChange)
56
+
57
+ autorun(this.onTitleUpdate)
58
+ autorun(this.onTemplateUpdate)
59
+
60
+ this.addEventListeners()
61
+ }
62
+
63
+ onTitleUpdate() {
64
+ const title = this.nextPage?.title
65
+
66
+ if (title) {
67
+ document.title = title
68
+ }
69
+ }
70
+
71
+ onTemplateUpdate() {
72
+ const template = this.nextPage?.template
73
+
74
+ if (template) {
75
+ document.documentElement.dataset.template = template
76
+
77
+ this.template = template
78
+ }
79
+ }
80
+
81
+ //
82
+ // Components.
83
+ //
84
+ canvas?: Component
85
+ components: Array<Component> = []
86
+ transition?: Component & { onTransition?: (args: any) => Promise<void> }
87
+
88
+ initComponents(components: Array<ApplicationComponentData>) {
89
+ const classes = components.map(
90
+ ({ component: Component }) =>
91
+ new Component({
92
+ application: this,
93
+ }),
94
+ )
95
+
96
+ this.canvas = classes.find((component) => component.id === 'canvas')
97
+ this.transition = classes.find((component) => component.id === 'transition')
98
+
99
+ classes.forEach((component) => {
100
+ this.addComponent(component)
101
+ })
102
+ }
103
+
104
+ addComponent(component: Component) {
105
+ this.components.push(component)
106
+ }
107
+
108
+ removeComponent(component: Component) {
109
+ component.destroy()
110
+
111
+ const index = this.components.indexOf(component)
112
+
113
+ if (index !== -1) {
114
+ this.components.splice(index, 1)
115
+ }
116
+ }
117
+
118
+ onComponentChange(event: IArrayDidChange<Component>) {
119
+ if (event.type === 'splice') {
120
+ const { added, removed } = event
121
+ }
122
+ }
123
+
124
+ //
125
+ // Datasets.
126
+ //
127
+ datasets: Array<ApplicationComponentDatasetData> = []
128
+
129
+ initDatasets(datasets: Array<ApplicationComponentDatasetData>) {
130
+ this.datasets = datasets
131
+ }
132
+
133
+ //
134
+ // Routes.
135
+ //
136
+ pages: Map<string, new (args: PageParameters) => Page> = new Map()
137
+
138
+ initRoutes(routes: Array<ApplicationRoute>) {
139
+ routes.forEach(({ component, template }) => {
140
+ this.pages.set(template, component)
141
+ })
142
+ }
143
+
144
+ //
145
+ // Sprites.
146
+ //
147
+ async initSprites(url = '/bundle.svg') {
148
+ const request = await window.fetch(url)
149
+ const response = await request.text()
150
+
151
+ const sprite = document.createElement('div')
152
+
153
+ sprite.innerHTML = response
154
+
155
+ sprite.style.left = '-999999px'
156
+ sprite.style.opacity = '0'
157
+ sprite.style.position = 'absolute'
158
+ sprite.style.top = '0'
159
+
160
+ document.body.appendChild(sprite)
161
+ }
162
+
163
+ //
164
+ // Initialization.
165
+ //
166
+ declare links: Links
167
+
168
+ initPage() {
169
+ this.createPage()
170
+ this.createLinks()
171
+ }
172
+
173
+ //
174
+ // Links.
175
+ //
176
+ createLinks() {
177
+ this.links = new Links(this)
178
+ }
179
+
180
+ //
181
+ // Page.
182
+ //
183
+ currentPage?: Page = undefined
184
+
185
+ createPage(template = this.template) {
186
+ const PageClass = this.pages.get(template)!
187
+
188
+ const page = new PageClass({
189
+ application: this,
190
+ datasets: this.datasets,
191
+ })
192
+
193
+ this.currentPage = page
194
+ this.currentPage.create()
195
+ }
196
+
197
+ destroyPage() {
198
+ if (this.currentPage) {
199
+ this.currentPage.destroy()
200
+ }
201
+ }
202
+
203
+ //
204
+ // Navigate.
205
+ //
206
+ route: string = window.location.pathname
207
+
208
+ onRouteChange({ oldValue, newValue }: IValueDidChange<string>) {
209
+ this.onRouteChangeRequest({
210
+ href: newValue,
211
+ pushState: true,
212
+ })
213
+ }
214
+
215
+ async onRouteChangeRequest({ href, pushState = true }: { href: string; pushState: boolean }) {
216
+ const request = await window.fetch(href)
217
+ const response = await request.text()
218
+
219
+ this.onRequest({
220
+ href,
221
+ response,
222
+ pushState,
223
+ })
224
+ }
225
+
226
+ //
227
+ // Request.
228
+ //
229
+ nextPage: {
230
+ element?: Element
231
+ template?: string
232
+ title?: string
233
+ } = {}
234
+
235
+ async onRequest({ href, response, pushState }: { href: string; response: string; pushState: boolean }) {
236
+ const domParser = new DOMParser()
237
+ const dom = domParser.parseFromString(response, 'text/html')
238
+
239
+ const html = dom.querySelector('html')!
240
+ const app = dom.querySelector('.app')!
241
+
242
+ this.nextPage = {
243
+ element: app,
244
+ template: html.dataset.template ?? this.template,
245
+ title: dom.title ?? document.title,
246
+ }
247
+
248
+ if (this.transition) {
249
+ await this.transition.onTransition?.(this)
250
+ } else {
251
+ this.currentPage!.element.remove()
252
+ this.currentPage!.destroy()
253
+
254
+ this.element.appendChild(this.nextPage.element!.firstElementChild!)
255
+
256
+ this.createPage(this.nextPage.template)
257
+ }
258
+
259
+ if (pushState) {
260
+ window.history.pushState({}, this.nextPage.title!, href)
261
+ }
262
+ }
263
+
264
+ //
265
+ // Pop State.
266
+ //
267
+ onPopState() {
268
+ this.onRouteChangeRequest({
269
+ href: document.location.pathname,
270
+ pushState: false,
271
+ })
272
+ }
273
+
274
+ //
275
+ // Scroll.
276
+ //
277
+ get scroll() {
278
+ return this.currentPage?.scroll ?? 0
279
+ }
280
+
281
+ //
282
+ // Listeners.
283
+ //
284
+ addEventListeners() {
285
+ window.addEventListener('popstate', this.onPopState)
286
+ }
287
+
288
+ removeEventListeners() {
289
+ window.removeEventListener('popstate', this.onPopState)
290
+ }
291
+ }
292
+
293
+ export const Application = new ApplicationManager()
@@ -0,0 +1,133 @@
1
+ import { ApplicationManager } from './App'
2
+ import { EventEmitter } from './EventEmitter'
3
+
4
+ export interface ComponentClasses {
5
+ [key: string]: string
6
+ }
7
+
8
+ export interface ComponentElements {
9
+ [key: string]: Array<any> | Element | Array<Element> | HTMLElement | Array<HTMLElement> | NodeList | Window | null
10
+ }
11
+
12
+ export type ComponentSelector = string | HTMLElement
13
+
14
+ export interface ComponentSelectors {
15
+ [key: string]: string | Element | Array<Element> | HTMLElement | Array<HTMLElement> | NodeList | Window
16
+ }
17
+
18
+ export interface ComponentParameters {
19
+ application?: ApplicationManager
20
+ autoListeners?: boolean
21
+ autoMount?: boolean
22
+ classes?: ComponentClasses
23
+ element?: ComponentSelector
24
+ elements?: ComponentSelectors
25
+ id?: string
26
+ }
27
+
28
+ export class Component extends EventEmitter {
29
+ application?: ApplicationManager
30
+ autoListeners: boolean
31
+ autoMount: boolean
32
+ classes?: ComponentClasses
33
+ selector?: ComponentSelector
34
+ selectors?: ComponentSelectors
35
+
36
+ id?: string
37
+
38
+ element?: HTMLElement
39
+ elements: ComponentElements = {}
40
+
41
+ constructor({
42
+ application,
43
+ autoListeners = true,
44
+ autoMount = true,
45
+ classes,
46
+ element,
47
+ elements,
48
+ id,
49
+ }: ComponentParameters) {
50
+ super()
51
+
52
+ this.application = application
53
+ this.autoListeners = autoListeners
54
+ this.autoMount = autoMount
55
+
56
+ this.classes = classes
57
+
58
+ this.selector = element
59
+ this.selectors = elements
60
+
61
+ this.id = id
62
+
63
+ if (this.autoMount) {
64
+ this.create()
65
+ }
66
+
67
+ if (this.autoListeners) {
68
+ this.addEventListeners()
69
+ }
70
+ }
71
+
72
+ create() {
73
+ if (this.selector) {
74
+ this.initElement(this.selector)
75
+ }
76
+
77
+ if (this.selectors) {
78
+ this.initElements(this.selectors)
79
+ }
80
+ }
81
+
82
+ initElement(selector: ComponentSelector) {
83
+ if (selector instanceof HTMLElement) {
84
+ this.element = selector
85
+ } else {
86
+ this.element = document.querySelector(selector)!
87
+ }
88
+ }
89
+
90
+ initElements(selectors?: ComponentSelectors) {
91
+ for (const key in selectors) {
92
+ const selector = selectors[key]
93
+
94
+ if (selector === window) {
95
+ this.elements[key] = window
96
+ } else if (selector instanceof HTMLElement) {
97
+ this.elements[key] = selector
98
+ } else if (selector instanceof NodeList) {
99
+ this.elements[key] = selector
100
+ } else if (Array.isArray(selector)) {
101
+ this.elements[key] = selector
102
+ } else {
103
+ const elements = this.element!.querySelectorAll(selector as string)
104
+
105
+ if (elements.length === 0) {
106
+ const elements = document.querySelectorAll(selector as string)
107
+
108
+ if (elements.length === 0) {
109
+ this.elements[key] = null
110
+ } else if (elements.length === 1) {
111
+ this.elements[key] = elements[0] as HTMLElement
112
+ } else {
113
+ this.elements[key] = elements
114
+ }
115
+ } else if (elements.length === 1) {
116
+ this.elements[key] = elements[0] as HTMLElement
117
+ } else {
118
+ this.elements[key] = elements
119
+ }
120
+ }
121
+ }
122
+ }
123
+
124
+ addEventListeners() {}
125
+
126
+ removeEventListeners() {}
127
+
128
+ destroy() {
129
+ super.destroy()
130
+
131
+ this.removeEventListeners()
132
+ }
133
+ }
@@ -0,0 +1,41 @@
1
+ import AutoBind from 'auto-bind'
2
+ import { Emitter, Unsubscribe, createNanoEvents } from 'nanoevents'
3
+
4
+ export class EventEmitter {
5
+ emitter: Emitter
6
+ entries: Map<Function, Unsubscribe> = new Map()
7
+
8
+ constructor() {
9
+ AutoBind(this)
10
+
11
+ this.emitter = createNanoEvents()
12
+ }
13
+
14
+ on(event: string, callback: (...args: any) => void) {
15
+ if (!callback) {
16
+ return console.trace('No callback provided')
17
+ }
18
+
19
+ const emitter = this.emitter.on(event, callback)
20
+
21
+ this.entries.set(callback, emitter)
22
+
23
+ return emitter
24
+ }
25
+
26
+ off(event: string, callback: (...args: any) => void) {
27
+ const unsubscribe = this.entries.get(callback)
28
+
29
+ if (unsubscribe) {
30
+ unsubscribe()
31
+ }
32
+ }
33
+
34
+ fire(event: string, ...args: any[]) {
35
+ this.emitter.emit(event, ...args)
36
+ }
37
+
38
+ destroy() {
39
+ this.entries.forEach((unsubscribe) => unsubscribe())
40
+ }
41
+ }
package/src/Link.ts ADDED
@@ -0,0 +1,48 @@
1
+ import { Component } from './Component'
2
+
3
+ export class Link extends Component {
4
+ declare element: HTMLLinkElement
5
+
6
+ constructor({ element }: { element: HTMLAnchorElement }) {
7
+ super({ element })
8
+ }
9
+
10
+ onClick(event: MouseEvent) {
11
+ event.preventDefault()
12
+
13
+ this.fire('click', this.element.href)
14
+ }
15
+
16
+ addEventListeners() {
17
+ const isLocal = this.element.href.includes(window.location.origin)
18
+ const isLocalPrevented = this.element.dataset.linkOverride === ''
19
+ const isNotEmail = !this.element.href.startsWith('mailto')
20
+ const isNotPhone = !this.element.href.startsWith('tel')
21
+ const isDownload = this.element.hasAttribute('download')
22
+ const isAnchor = this.element.href.includes('#')
23
+
24
+ if (isAnchor) {
25
+ const hash = this.element.href.split('#')[1]
26
+ const element = document.querySelector(`#${hash}`)
27
+
28
+ if (element) {
29
+ return
30
+ }
31
+ }
32
+
33
+ if (isLocalPrevented || isDownload) {
34
+ return
35
+ }
36
+
37
+ if (isLocal) {
38
+ this.element.onclick = this.onClick
39
+ } else if (isNotEmail && isNotPhone) {
40
+ this.element.rel = 'noopener'
41
+ this.element.setAttribute('target', '_blank')
42
+ }
43
+ }
44
+
45
+ removeEventListeners() {
46
+ this.element.onclick = null
47
+ }
48
+ }
package/src/Links.ts ADDED
@@ -0,0 +1,46 @@
1
+ import { reaction } from 'mobx'
2
+
3
+ import { ApplicationManager } from './App'
4
+ import { Link } from './Link'
5
+ import { EventEmitter } from './EventEmitter'
6
+
7
+ export class Links extends EventEmitter {
8
+ declare application: ApplicationManager
9
+ declare links: Array<Link>
10
+
11
+ constructor(application: ApplicationManager) {
12
+ super()
13
+
14
+ this.application = application
15
+
16
+ reaction(
17
+ () => application.currentPage,
18
+ () => this.refresh(),
19
+ { fireImmediately: true },
20
+ )
21
+ }
22
+
23
+ addEventListeners() {
24
+ this.links?.forEach((link) => link.destroy())
25
+
26
+ const links = document.querySelectorAll('a')
27
+
28
+ this.links = Array.from(links).map((element) => {
29
+ const link = new Link({
30
+ element,
31
+ })
32
+
33
+ link.on('click', this.onLinkClick)
34
+
35
+ return link
36
+ })
37
+ }
38
+
39
+ onLinkClick(href: string) {
40
+ this.application.route = href
41
+ }
42
+
43
+ refresh() {
44
+ this.addEventListeners()
45
+ }
46
+ }
package/src/Page.ts ADDED
@@ -0,0 +1,209 @@
1
+ import Lenis, { LenisOptions } from 'lenis'
2
+ import { makeObservable, observable } from 'mobx'
3
+ import Tempus from 'tempus'
4
+
5
+ import { ApplicationManager } from './App'
6
+ import { Component, ComponentClasses, ComponentSelectors } from './Component'
7
+
8
+ export interface PageParameters {
9
+ application: ApplicationManager
10
+ classes?: ComponentClasses
11
+ datasets: Array<{ component: typeof Component; selector: string }>
12
+ element?: HTMLElement | string
13
+ elements?: ComponentSelectors
14
+ scrollEnabled?: boolean
15
+ scrollOptions?: LenisOptions
16
+ }
17
+
18
+ export class Page extends Component {
19
+ declare element: HTMLElement
20
+ declare elements: {
21
+ wrapper: HTMLElement
22
+ }
23
+
24
+ declare application: ApplicationManager
25
+
26
+ datasets: Array<{
27
+ component: typeof Component
28
+ selector: string
29
+ }> = []
30
+
31
+ scroll: number = 0
32
+ scrollEnabled: boolean = true
33
+ scrollOptions: LenisOptions = {}
34
+
35
+ declare unsubscribeRaf: (() => void) | undefined
36
+
37
+ constructor({
38
+ application,
39
+ classes,
40
+ datasets,
41
+ element,
42
+ elements,
43
+ scrollEnabled = true,
44
+ scrollOptions = {},
45
+ }: PageParameters) {
46
+ super({
47
+ autoMount: false,
48
+ autoListeners: false,
49
+ classes,
50
+ element,
51
+ elements,
52
+ })
53
+
54
+ this.application = application
55
+ this.datasets = datasets
56
+
57
+ this.scrollEnabled = scrollEnabled
58
+ this.scrollOptions = scrollOptions
59
+
60
+ makeObservable(this, {
61
+ components: observable,
62
+ scroll: observable,
63
+ })
64
+ }
65
+
66
+ create() {
67
+ super.create()
68
+
69
+ this.createResizeObserver()
70
+ this.createComponents()
71
+ this.createScroll()
72
+
73
+ this.addEventListeners()
74
+ }
75
+
76
+ //
77
+ // Observer.
78
+ //
79
+ declare resizeObserver: ResizeObserver
80
+
81
+ createResizeObserver() {
82
+ this.resizeObserver = new ResizeObserver(() => {
83
+ this.onResize()
84
+ })
85
+
86
+ this.resizeObserver.observe(this.elements.wrapper)
87
+ }
88
+
89
+ destroyResizeObserver() {
90
+ this.resizeObserver.disconnect()
91
+ }
92
+
93
+ //
94
+ // Components.
95
+ //
96
+ components: Set<Component> = new Set()
97
+
98
+ createComponents() {
99
+ this.datasets.map(({ component: Component, selector }) => {
100
+ const elements = this.element.querySelectorAll(selector)
101
+
102
+ const components = Array.from(elements).map((element) => {
103
+ const component = new Component({
104
+ application: this.application,
105
+ element: element as HTMLElement,
106
+ })
107
+
108
+ return component
109
+ })
110
+
111
+ components.forEach((component) => {
112
+ this.application.addComponent(component)
113
+
114
+ this.components.add(component)
115
+ })
116
+ })
117
+ }
118
+
119
+ destroyComponents() {
120
+ this.components.forEach((component) => {
121
+ this.application.removeComponent(component)
122
+ })
123
+
124
+ this.components.clear()
125
+ }
126
+
127
+ //
128
+ // Scroll.
129
+ //
130
+ declare lenis: Lenis
131
+
132
+ createScroll() {
133
+ if (!this.scrollEnabled) {
134
+ this.element.addEventListener('scroll', this.onScrollFallback)
135
+
136
+ return
137
+ }
138
+
139
+ this.lenis = new Lenis({
140
+ content: this.elements.wrapper as HTMLElement,
141
+ wrapper: this.element,
142
+ ...this.scrollOptions,
143
+ })
144
+
145
+ this.lenis.on('scroll', (event) => {
146
+ this.onScroll(event.scroll)
147
+
148
+ this.emitter.emit('scroll', event)
149
+ })
150
+ }
151
+
152
+ destroyScroll() {
153
+ if (!this.scrollEnabled) {
154
+ this.element.removeEventListener('scroll', this.onScrollFallback)
155
+
156
+ return
157
+ }
158
+
159
+ this.lenis?.destroy()
160
+ }
161
+
162
+ //
163
+ // Events.
164
+ //
165
+ onResize() {
166
+ this.lenis?.resize()
167
+ }
168
+
169
+ onRAF(time: number) {
170
+ this.lenis?.raf(time)
171
+ }
172
+
173
+ onScroll(scroll: number) {
174
+ this.scroll = scroll
175
+ }
176
+
177
+ onScrollFallback() {
178
+ this.scroll = this.element.scrollTop
179
+
180
+ this.emitter.emit('scroll', { scroll: this.scroll })
181
+ }
182
+
183
+ //
184
+ // Events.
185
+ //
186
+ addEventListeners() {
187
+ super.addEventListeners()
188
+
189
+ this.unsubscribeRaf = Tempus.add(this.onRAF)
190
+ }
191
+
192
+ removeEventListeners() {
193
+ super.removeEventListeners()
194
+
195
+ this.unsubscribeRaf?.()
196
+ this.unsubscribeRaf = undefined
197
+ }
198
+
199
+ //
200
+ // Destroy.
201
+ //
202
+ destroy() {
203
+ super.destroy()
204
+
205
+ this.destroyResizeObserver()
206
+ this.destroyComponents()
207
+ this.destroyScroll()
208
+ }
209
+ }
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ export * from './App'
2
+ export * from './Link'
3
+ export * from './Links'
4
+ export * from './Component'
5
+ export * from './EventEmitter'
6
+ export * from './Page'
7
+
8
+ import Tempus from 'tempus'
9
+
10
+ export { Tempus }