duto 0.0.0 → 0.260808.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/LICENSE +21 -0
- package/README.md +21 -12
- package/package.json +56 -2
- package/src/cli/commands/build.ts +74 -0
- package/src/cli/commands/dev-old.ts +213 -0
- package/src/cli/commands/dev.ts +87 -0
- package/src/cli/commands/preview.ts +33 -0
- package/src/cli/index.ts +28 -0
- package/src/cli/utils.ts +146 -0
- package/src/client/app.tsx +123 -0
- package/src/client/hmr.ts +13 -0
- package/src/client/island.ts +136 -0
- package/src/client/link.ts +70 -0
- package/src/client/router.ts +329 -0
- package/src/client/storage.ts +95 -0
- package/src/compiler/aliases.ts +36 -0
- package/src/compiler/assets.ts +43 -0
- package/src/compiler/ast.ts +764 -0
- package/src/compiler/constants.ts +24 -0
- package/src/compiler/file.ts +60 -0
- package/src/compiler/index.ts +307 -0
- package/src/compiler/island.ts +108 -0
- package/src/compiler/meta.ts +601 -0
- package/src/compiler/routes.ts +29 -0
- package/src/compiler/types.ts +209 -0
- package/src/compiler/utils.ts +5 -0
- package/src/compiler/vite-plugin.ts +324 -0
- package/src/components/link.tsx +38 -0
- package/src/i18n/client.ts +403 -0
- package/src/i18n/element.ts +44 -0
- package/src/i18n/server.ts +56 -0
- package/src/index.ts +28 -0
- package/src/preact/client-dev.ts +5 -0
- package/src/preact/client.ts +87 -0
- package/src/preact/context.ts +41 -0
- package/src/preact/server.ts +186 -0
- package/src/preact/signals.ts +89 -0
- package/src/preact/static-html.ts +45 -0
- package/src/preact/types.ts +26 -0
package/src/cli/utils.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { IMPORT, Datte } from 't0n'
|
|
2
|
+
import { join } from 'pathe'
|
|
3
|
+
import { gray, green, yellow, red, cyan } from 't0n/color'
|
|
4
|
+
import type { UserConfig } from 'vite'
|
|
5
|
+
|
|
6
|
+
import { resolveExtension } from '@/file'
|
|
7
|
+
import config from '@/vite-plugin'
|
|
8
|
+
import { _root } from '@/utils'
|
|
9
|
+
import { match, match404 } from '@/routes'
|
|
10
|
+
import type { Metadata, PluginOptions } from '@/types'
|
|
11
|
+
import { getMetadata } from '@/meta'
|
|
12
|
+
|
|
13
|
+
export async function getConfig(name = 'config'): Promise<UserConfig & PluginOptions> {
|
|
14
|
+
let opts = {} as UserConfig
|
|
15
|
+
const file = resolveExtension(name)
|
|
16
|
+
if (file) opts = await IMPORT(join(_root, file), 'default')
|
|
17
|
+
return config(opts)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// export const formatTime = (ms: number) => {
|
|
21
|
+
// if (ms < 1000) return `${ms}ms`
|
|
22
|
+
// return `${(ms / 1000).toFixed(2)}s`
|
|
23
|
+
// }
|
|
24
|
+
export const formatTime = (ms: number) => {
|
|
25
|
+
if (ms < 0.001) return `${(ms * 1_000_000).toFixed(2)}ns`
|
|
26
|
+
if (ms < 1) return `${(ms * 1000).toFixed(2)}µs`
|
|
27
|
+
if (ms < 1000) return `${ms.toFixed(2)}ms`
|
|
28
|
+
if (ms < 60000) return `${(ms / 1000).toFixed(2)}s`
|
|
29
|
+
return `${Math.floor(ms / 60000)}m ${((ms % 60000) / 1000).toFixed(1)}s`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const statusColorMap = new Map([
|
|
33
|
+
[5, red],
|
|
34
|
+
[4, yellow],
|
|
35
|
+
[3, cyan],
|
|
36
|
+
])
|
|
37
|
+
|
|
38
|
+
const getStatusColor = (status: number) => (statusColorMap.get(Math.floor(status / 100)) ?? green)(String(status))
|
|
39
|
+
|
|
40
|
+
const clients = new Set<Bun.ServerWebSocket<void>>()
|
|
41
|
+
export function reload() {
|
|
42
|
+
for (const ws of clients) {
|
|
43
|
+
if (ws.readyState === WebSocket.OPEN)
|
|
44
|
+
ws.send('reload')
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function preview(
|
|
49
|
+
meta: Metadata,
|
|
50
|
+
host: string = '0.0.0.0',
|
|
51
|
+
port: number = 4173
|
|
52
|
+
) {
|
|
53
|
+
return Bun.serve({
|
|
54
|
+
hostname: host,
|
|
55
|
+
port,
|
|
56
|
+
|
|
57
|
+
websocket: {
|
|
58
|
+
open(ws) {
|
|
59
|
+
clients.add(ws)
|
|
60
|
+
},
|
|
61
|
+
close(ws) {
|
|
62
|
+
clients.delete(ws)
|
|
63
|
+
},
|
|
64
|
+
message() {},
|
|
65
|
+
},
|
|
66
|
+
async fetch(req, server) {
|
|
67
|
+
const start = performance.now()
|
|
68
|
+
|
|
69
|
+
const url = new URL(req.url)
|
|
70
|
+
const path = decodeURIComponent(url.pathname)
|
|
71
|
+
const headers = {
|
|
72
|
+
'Access-Control-Allow-Origin': '*',
|
|
73
|
+
'Access-Control-Allow-Methods': 'GET',
|
|
74
|
+
'Access-Control-Allow-Headers': '*',
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (req.method === 'OPTIONS')
|
|
78
|
+
return new Response(null, { status: 204, headers })
|
|
79
|
+
|
|
80
|
+
if (req.headers.get('upgrade') === 'websocket'
|
|
81
|
+
&& req.headers.get('sec-websocket-protocol') === 'duto') {
|
|
82
|
+
server.upgrade(req)
|
|
83
|
+
return
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
let status = 500
|
|
87
|
+
let route: string | undefined
|
|
88
|
+
let matched: Bun.MatchedRoute | null | undefined
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
let res: Response
|
|
92
|
+
const dist = 'dist' // TODO: get from configs
|
|
93
|
+
|
|
94
|
+
let file = Bun.file(join(_root, dist, path))
|
|
95
|
+
|
|
96
|
+
if (await file.exists()) {
|
|
97
|
+
// route = path
|
|
98
|
+
res = new Response(file)
|
|
99
|
+
} else {
|
|
100
|
+
matched = match(path)
|
|
101
|
+
|
|
102
|
+
// console.log(matched)
|
|
103
|
+
route = matched?.name || match404(meta, path)
|
|
104
|
+
|
|
105
|
+
res = new Response(null, { status: 404 })
|
|
106
|
+
|
|
107
|
+
// for (const asset of path !== '/' && matched && matched.kind === 'exact' ? [
|
|
108
|
+
// path,
|
|
109
|
+
// join(path, 'index'),
|
|
110
|
+
// ] : ['index']) {
|
|
111
|
+
for (const asset of [
|
|
112
|
+
path,
|
|
113
|
+
join(path, 'index'),
|
|
114
|
+
'index',
|
|
115
|
+
]) {
|
|
116
|
+
file = Bun.file(join(_root, dist, asset +'.html'))
|
|
117
|
+
|
|
118
|
+
if (await file.exists()) {
|
|
119
|
+
// if (route === '/' || !route) route = asset
|
|
120
|
+
res = new Response(file)
|
|
121
|
+
break
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
status = res.status
|
|
127
|
+
return res
|
|
128
|
+
} catch (err) {
|
|
129
|
+
status = 500
|
|
130
|
+
throw err
|
|
131
|
+
} finally {
|
|
132
|
+
if (route || matched) {
|
|
133
|
+
const elapsed = `${Math.max(1, Math.ceil(performance.now() - start))}ms`
|
|
134
|
+
|
|
135
|
+
console.log([
|
|
136
|
+
gray(Datte.dateTime()),
|
|
137
|
+
getStatusColor(status),
|
|
138
|
+
path,
|
|
139
|
+
route && route !== path ? gray(route) : '',
|
|
140
|
+
gray(elapsed),
|
|
141
|
+
].filter(Boolean).join(' '))
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
})
|
|
146
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// @ts-ignore
|
|
2
|
+
import ':duto/client/hmr'
|
|
3
|
+
|
|
4
|
+
import { render, hydrate, h } from 'preact'
|
|
5
|
+
import { Switch, Route, Router } from 'wouter-preact'
|
|
6
|
+
import { useLocation, useParams, useSearchParams, } from 'wouter-preact'
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
fallbackLocale, locales, messages, routes, routeMap,
|
|
10
|
+
// @ts-ignore
|
|
11
|
+
} from ':duto/i18n'
|
|
12
|
+
// import i18n from '../i18n/client'
|
|
13
|
+
import i18n, { updatePickers } from '../i18n/client'
|
|
14
|
+
|
|
15
|
+
i18n(fallbackLocale, locales, messages, routes, routeMap)
|
|
16
|
+
|
|
17
|
+
import EI18n from '../i18n/element'
|
|
18
|
+
import Island from './island'
|
|
19
|
+
import Link from './link'
|
|
20
|
+
|
|
21
|
+
// import Router from './router'
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
views, SRoutes,
|
|
25
|
+
// @ts-ignore
|
|
26
|
+
} from ':duto'
|
|
27
|
+
// @ts-ignore
|
|
28
|
+
import ':root/styles'
|
|
29
|
+
|
|
30
|
+
window.$duto = {
|
|
31
|
+
sroutes: SRoutes,
|
|
32
|
+
location: '/',
|
|
33
|
+
navigate: {},
|
|
34
|
+
params: {},
|
|
35
|
+
search: {},
|
|
36
|
+
setSearch: {},
|
|
37
|
+
}
|
|
38
|
+
import { renderRoute } from './router'
|
|
39
|
+
// import { navigate } from 'wouter-preact/use-hash-location'
|
|
40
|
+
|
|
41
|
+
for (let [tag, el, opts] of [
|
|
42
|
+
['i-i', EI18n],
|
|
43
|
+
['i-l', Link, { extends: 'a' }],
|
|
44
|
+
['i-s', Island],
|
|
45
|
+
] as [string, new () => HTMLElement, ElementDefinitionOptions?][]) {
|
|
46
|
+
if (!customElements.get(tag))
|
|
47
|
+
customElements.define(tag, el, opts)
|
|
48
|
+
}
|
|
49
|
+
// Router.load(SRoutes).mount(views)
|
|
50
|
+
|
|
51
|
+
// render(h(Router, null,
|
|
52
|
+
// h(() => {
|
|
53
|
+
// // const __ = useRoute(location.pathname)
|
|
54
|
+
// document.body.removeAttribute('hidden')
|
|
55
|
+
// const [, navigate] = useLocation()
|
|
56
|
+
// // console.log(navigate)
|
|
57
|
+
// // this.navigate = navigate
|
|
58
|
+
|
|
59
|
+
// // TODO: talvez registrar todos os valores pelo id do elemento em uma fila e atualizar nesse tick...
|
|
60
|
+
// // input, select etc registra um update val e aqui popula..
|
|
61
|
+
// updatePickers()
|
|
62
|
+
// return null
|
|
63
|
+
// }, null),
|
|
64
|
+
// h(Switch, null,
|
|
65
|
+
// h(Route, { path: SRoutes.has(location.pathname) ? location.pathname : '/' }),
|
|
66
|
+
// ...views.map(([path, Component, defs, getProps]) =>
|
|
67
|
+
// h(Route, {
|
|
68
|
+
// path,
|
|
69
|
+
// component: ({ params }: { params: Record<string, string> }) => {
|
|
70
|
+
// // Trigger async render (updates container imperatively to avoid
|
|
71
|
+
// // React/Preact hook rules issues with async components)
|
|
72
|
+
|
|
73
|
+
// renderRoute(document.body, Component, params, defs, getProps)
|
|
74
|
+
// // Return null — renderRoute will call render() directly on the container
|
|
75
|
+
// return null
|
|
76
|
+
// },
|
|
77
|
+
// })
|
|
78
|
+
// ),
|
|
79
|
+
|
|
80
|
+
// ) ), document.body)
|
|
81
|
+
render(<Router>
|
|
82
|
+
{h(() => {
|
|
83
|
+
document.body.removeAttribute('hidden')
|
|
84
|
+
const [location, navigate] = useLocation()
|
|
85
|
+
window.$duto.location = location
|
|
86
|
+
window.$duto.navigate = navigate
|
|
87
|
+
window.$duto.params = useParams()
|
|
88
|
+
const [search, setSearch] = useSearchParams()
|
|
89
|
+
window.$duto.search = search
|
|
90
|
+
window.$duto.setSearch = setSearch
|
|
91
|
+
|
|
92
|
+
// TODO: talvez registrar todos os valores pelo id do elemento em uma fila e atualizar nesse tick...
|
|
93
|
+
// input, select etc registra um update val e aqui popula..
|
|
94
|
+
updatePickers()
|
|
95
|
+
|
|
96
|
+
// React.useEffect(() => {
|
|
97
|
+
// document.body.removeAttribute('hidden');
|
|
98
|
+
// navigateRef.current = navigate;
|
|
99
|
+
// updatePickers();
|
|
100
|
+
// }, [navigate]);
|
|
101
|
+
// return null;
|
|
102
|
+
}, null)}
|
|
103
|
+
<Switch>
|
|
104
|
+
<Route path={SRoutes.has(location.pathname) ? location.pathname : '/'} component={({ params }) => {
|
|
105
|
+
delete params[0]
|
|
106
|
+
window.$duto.route = { pattern: location.pathname, params, path: location.pathname }
|
|
107
|
+
}} />
|
|
108
|
+
{/* @ts-ignore */}
|
|
109
|
+
{...views.map(([path, Component, defs, getProps, layouts]) => (
|
|
110
|
+
<Route path={path} component={({ params }: { params: Record<string, string> }) => {
|
|
111
|
+
// Trigger async render (updates container imperatively to avoid
|
|
112
|
+
// React/Preact hook rules issues with async components)
|
|
113
|
+
|
|
114
|
+
delete params[0]
|
|
115
|
+
window.$duto.route = { pattern: path, params, path: location.pathname }
|
|
116
|
+
|
|
117
|
+
renderRoute(document.body, Component, params, defs, getProps, layouts)
|
|
118
|
+
// Return null — renderRoute will call render() directly on the container
|
|
119
|
+
return null
|
|
120
|
+
}} />
|
|
121
|
+
))}
|
|
122
|
+
</Switch>
|
|
123
|
+
</Router>, document.body)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
let hmr: WebSocket | null = null
|
|
2
|
+
const url = `ws${location.protocol === 'https:' ? 'ws' : ''}://${location.host}`
|
|
3
|
+
|
|
4
|
+
const connect = () => {
|
|
5
|
+
hmr = new WebSocket(url, 'duto')
|
|
6
|
+
hmr.onmessage = ({ data }) => data === 'reload' && location.reload()
|
|
7
|
+
hmr.onerror = () => hmr?.close()
|
|
8
|
+
}
|
|
9
|
+
const reconect = () => (!hmr || hmr?.readyState === WebSocket.CLOSED) && location.reload()
|
|
10
|
+
connect()
|
|
11
|
+
document.addEventListener('visibilitychange', () => document.visibilityState === 'visible' && reconect())
|
|
12
|
+
window.addEventListener('focus', reconect)
|
|
13
|
+
window.addEventListener('pageshow', reconect)
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import hydrator from '../preact/client'
|
|
2
|
+
|
|
3
|
+
const propTypes: { [k: string]: (value: any) => any } = {
|
|
4
|
+
0: (value) => reviveObject(value),
|
|
5
|
+
1: (value) => reviveArray(value),
|
|
6
|
+
2: (value) => new RegExp(value),
|
|
7
|
+
3: (value) => new Date(value),
|
|
8
|
+
4: (value) => new Map(reviveArray(value)),
|
|
9
|
+
5: (value) => new Set(reviveArray(value)),
|
|
10
|
+
6: (value) => BigInt(value),
|
|
11
|
+
7: (value) => new URL(value),
|
|
12
|
+
8: (value) => new Uint8Array(value),
|
|
13
|
+
9: (value) => new Uint16Array(value),
|
|
14
|
+
10: (value) => new Uint32Array(value),
|
|
15
|
+
11: (value) => Number.POSITIVE_INFINITY * value,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Not using JSON.parse reviver because it's bottom-up but we want top-down
|
|
19
|
+
const reviveTuple = (raw: any): any => {
|
|
20
|
+
const [type, value] = raw
|
|
21
|
+
return type in propTypes ? propTypes[type](value) : undefined
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const reviveArray = (raw: any): any => (raw as Array<any>).map(reviveTuple)
|
|
25
|
+
const reviveObject = (raw: any): any => {
|
|
26
|
+
if (typeof raw !== 'object' || raw === null) return raw
|
|
27
|
+
return Object.fromEntries(Object.entries(raw).map(([key, value]) => [key, reviveTuple(value)]))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 🌊🏝🌴
|
|
31
|
+
export default class Island extends HTMLElement {
|
|
32
|
+
public Component: any
|
|
33
|
+
private _hydrated = false
|
|
34
|
+
static observedAttributes = ['props']
|
|
35
|
+
|
|
36
|
+
connectedCallback() {
|
|
37
|
+
if (!this.hasAttribute('await-children') || document.readyState === 'interactive' || document.readyState === 'complete') {
|
|
38
|
+
this.build()
|
|
39
|
+
return
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
setTimeout(() => {
|
|
43
|
+
this.build()
|
|
44
|
+
}, 0)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
disconnectedCallback() {
|
|
48
|
+
// document.removeEventListener('d:after-swap', this.unmount);
|
|
49
|
+
// document.addEventListener('d:after-swap', this.unmount, { once: true });
|
|
50
|
+
|
|
51
|
+
this.unmount()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
attributeChangedCallback(name: string, oldValue: string, newValue: string) {
|
|
55
|
+
if (this.Component) {
|
|
56
|
+
// console.log(`[AttributeChanged] O atributo "${name}" mudou de "${oldValue}" para "${newValue}".`, this)
|
|
57
|
+
this._hydrated = false
|
|
58
|
+
this.hydrate()
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async build() {
|
|
63
|
+
const componentModule = await import(this.getAttribute('url')!)
|
|
64
|
+
const componentExport = this.getAttribute('export') || 'default'
|
|
65
|
+
if (!componentExport.includes('.')) {
|
|
66
|
+
this.Component = componentModule[componentExport]
|
|
67
|
+
} else {
|
|
68
|
+
this.Component = componentModule
|
|
69
|
+
for (const part of componentExport.split('.'))
|
|
70
|
+
this.Component = this.Component[part]
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
await this.hydrate()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// hydrate = async () => {
|
|
77
|
+
async hydrate() {
|
|
78
|
+
// Make sure the island is mounted on the DOM before hydrating. It could be unmounted
|
|
79
|
+
// when the parent island hydrates and re-creates this island.
|
|
80
|
+
if (!this.isConnected || this._hydrated) return
|
|
81
|
+
|
|
82
|
+
// Wait for parent island to hydrate first so we hydrate top-down. The `ssr` attribute
|
|
83
|
+
// represents that it has not completed hydration yet.
|
|
84
|
+
const parent = this.parentElement?.closest('i-s')
|
|
85
|
+
if (parent) {
|
|
86
|
+
parent.addEventListener('d:hydrate', this.hydrate, { once: true })
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const slotted = this.querySelectorAll('d-b')
|
|
91
|
+
const slots: Record<string, string> = {};
|
|
92
|
+
// Always check to see if there are templates.
|
|
93
|
+
// This happens if slots were passed but the client component did not render them
|
|
94
|
+
const templates = this.querySelectorAll('template[d-template]')
|
|
95
|
+
for (const template of templates) {
|
|
96
|
+
const closest = template.closest(this.tagName)
|
|
97
|
+
if (!closest?.isSameNode(this)) continue
|
|
98
|
+
slots[template.getAttribute('d-template') || 'default'] = template.innerHTML
|
|
99
|
+
template.remove()
|
|
100
|
+
}
|
|
101
|
+
for (const slot of slotted) {
|
|
102
|
+
const closest = slot.closest(this.tagName)
|
|
103
|
+
if (!closest?.isSameNode(this)) continue
|
|
104
|
+
slots[slot.getAttribute('name') || 'default'] = slot.innerHTML
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
let props: Record<string, unknown>
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
props = this.hasAttribute('props') ? reviveObject(JSON.parse(this.getAttribute('props')!)) : {}
|
|
111
|
+
} catch (e) {
|
|
112
|
+
let componentName: string = this.getAttribute('url') || '<unknown>'
|
|
113
|
+
const componentExport = this.getAttribute('export')
|
|
114
|
+
|
|
115
|
+
if (componentExport)
|
|
116
|
+
componentName += ` (export ${componentExport})`
|
|
117
|
+
|
|
118
|
+
console.error(
|
|
119
|
+
`[hydrate] Error parsing props for component ${componentName}`,
|
|
120
|
+
this.getAttribute('props'),
|
|
121
|
+
e,
|
|
122
|
+
)
|
|
123
|
+
throw e
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// @ts-ignore
|
|
127
|
+
await hydrator(this, this.Component, props, slots)
|
|
128
|
+
|
|
129
|
+
this._hydrated = true
|
|
130
|
+
this.dispatchEvent(new CustomEvent('d:hydrate'))
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
unmount() {
|
|
134
|
+
if (!this.isConnected) this.dispatchEvent(new CustomEvent('d:unmount'))
|
|
135
|
+
}
|
|
136
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export default class Link extends HTMLAnchorElement {
|
|
2
|
+
static observedAttributes = [
|
|
3
|
+
'href',
|
|
4
|
+
'target',
|
|
5
|
+
'download',
|
|
6
|
+
'rel',
|
|
7
|
+
'hreflang',
|
|
8
|
+
'referrerpolicy',
|
|
9
|
+
'ping',
|
|
10
|
+
'type',
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
constructor() {
|
|
14
|
+
super()
|
|
15
|
+
|
|
16
|
+
this.addEventListener('click', this.#onClick)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
disconnectedCallback() {
|
|
20
|
+
this.removeEventListener('click', this.#onClick)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
#onClick = (e: MouseEvent) => {
|
|
24
|
+
// Respeita comportamento padrão
|
|
25
|
+
if (
|
|
26
|
+
e.defaultPrevented
|
|
27
|
+
|| this.target === '_blank'
|
|
28
|
+
|| e.metaKey
|
|
29
|
+
|| e.ctrlKey
|
|
30
|
+
|| e.shiftKey
|
|
31
|
+
|| e.altKey
|
|
32
|
+
|| e.button !== 0
|
|
33
|
+
) {
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
// Allow ctrl/cmd+click to open in new tab normally
|
|
37
|
+
if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return
|
|
38
|
+
// e.preventDefault()
|
|
39
|
+
if ((e.target as HTMLAnchorElement).hasAttribute('href'))
|
|
40
|
+
window.dispatchEvent(new CustomEvent('d:navigate', { detail: e.target }))
|
|
41
|
+
|
|
42
|
+
// Exemplo: interceptar navegação SPA
|
|
43
|
+
// event.preventDefault()
|
|
44
|
+
// router.push(this.href)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Proxy opcional para facilitar typings
|
|
48
|
+
get url() {
|
|
49
|
+
return new URL(this.href)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
window.addEventListener('d:navigate', e => {
|
|
55
|
+
const el = (e as CustomEvent<HTMLAnchorElement>).detail
|
|
56
|
+
|
|
57
|
+
console.warn('d:navigate', el)
|
|
58
|
+
// if (href) _navigate?.(href)
|
|
59
|
+
// _navigate?.('/haha-2')
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// const links = document.querySelectorAll('a[d]')
|
|
63
|
+
// for (const link of links) { // @ts-ignore
|
|
64
|
+
// link.addEventListener('click', (e: MouseEvent) => {
|
|
65
|
+
// // Allow ctrl/cmd+click to open in new tab normally
|
|
66
|
+
// if (e.metaKey || e.ctrlKey || e.shiftKey || e.altKey) return
|
|
67
|
+
// e.preventDefault()
|
|
68
|
+
// window.dispatchEvent(new CustomEvent('d:navigate', { detail: e.target }))
|
|
69
|
+
// })
|
|
70
|
+
// }
|