amateras 0.2.0 → 0.4.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/README.md +25 -7
- package/ext/css/README.md +19 -0
- package/ext/css/src/index.ts +395 -322
- package/ext/css/src/lib/colorAssign.ts +6 -0
- package/ext/css/src/lib/colors/amber.ts +25 -0
- package/ext/css/src/lib/colors/blackwhite.ts +13 -0
- package/ext/css/src/lib/colors/blue.ts +25 -0
- package/ext/css/src/lib/colors/cyan.ts +25 -0
- package/ext/css/src/lib/colors/emerald.ts +25 -0
- package/ext/css/src/lib/colors/fuchsia.ts +25 -0
- package/ext/css/src/lib/colors/gray.ts +25 -0
- package/ext/css/src/lib/colors/green.ts +25 -0
- package/ext/css/src/lib/colors/indigo.ts +25 -0
- package/ext/css/src/lib/colors/lime.ts +25 -0
- package/ext/css/src/lib/colors/neutral.ts +25 -0
- package/ext/css/src/lib/colors/orange.ts +25 -0
- package/ext/css/src/lib/colors/pink.ts +25 -0
- package/ext/css/src/lib/colors/purple.ts +25 -0
- package/ext/css/src/lib/colors/red.ts +25 -0
- package/ext/css/src/lib/colors/rose.ts +25 -0
- package/ext/css/src/lib/colors/sky.ts +25 -0
- package/ext/css/src/lib/colors/slate.ts +25 -0
- package/ext/css/src/lib/colors/stone.ts +25 -0
- package/ext/css/src/lib/colors/teal.ts +25 -0
- package/ext/css/src/lib/colors/violet.ts +25 -0
- package/ext/css/src/lib/colors/yellow.ts +25 -0
- package/ext/css/src/lib/colors/zinc.ts +25 -0
- package/ext/css/src/lib/colors.ts +23 -0
- package/ext/css/src/structure/$CSSContainerRule.ts +13 -0
- package/ext/css/src/structure/$CSSKeyframesRule.ts +1 -5
- package/ext/css/src/structure/$CSSMediaRule.ts +3 -23
- package/ext/css/src/structure/$CSSRule.ts +6 -18
- package/ext/css/src/structure/$CSSStyleRule.ts +5 -14
- package/ext/css/src/structure/$CSSVariable.ts +3 -3
- package/ext/html/html.ts +1 -13
- package/ext/html/node/$Anchor.ts +31 -1
- package/ext/html/node/$Image.ts +54 -1
- package/ext/html/node/$Input.ts +154 -1
- package/ext/html/node/$OptGroup.ts +8 -1
- package/ext/html/node/$Option.ts +25 -1
- package/ext/html/node/$Select.ts +61 -1
- package/ext/i18n/README.md +53 -0
- package/ext/i18n/package.json +10 -0
- package/ext/i18n/src/index.ts +54 -0
- package/ext/i18n/src/node/I18nText.ts +35 -0
- package/ext/i18n/src/structure/I18n.ts +40 -0
- package/ext/i18n/src/structure/I18nDictionary.ts +31 -0
- package/ext/markdown/index.ts +123 -0
- package/ext/router/index.ts +13 -4
- package/ext/router/node/Page.ts +1 -0
- package/ext/router/node/Route.ts +4 -3
- package/ext/router/node/Router.ts +62 -17
- package/ext/router/node/RouterAnchor.ts +1 -1
- package/ext/ssr/index.ts +7 -5
- package/ext/ui/lib/VirtualScroll.ts +24 -0
- package/ext/ui/node/Accordian.ts +97 -0
- package/ext/ui/node/Tabs.ts +114 -0
- package/ext/ui/node/Toast.ts +16 -0
- package/ext/ui/node/Waterfall.ts +73 -0
- package/ext/ui/package.json +11 -0
- package/package.json +6 -7
- package/src/core.ts +36 -19
- package/src/global.ts +4 -0
- package/src/lib/assign.ts +12 -12
- package/src/lib/assignHelper.ts +2 -2
- package/src/lib/chain.ts +3 -0
- package/src/lib/debounce.ts +7 -0
- package/src/lib/env.ts +2 -0
- package/src/lib/native.ts +22 -24
- package/src/lib/randomId.ts +1 -1
- package/src/lib/sleep.ts +1 -1
- package/src/node/$Element.ts +301 -35
- package/src/node/$HTMLElement.ts +94 -1
- package/src/node/$Node.ts +148 -54
- package/src/node/$Virtual.ts +58 -0
- package/src/{node/node.ts → node.ts} +2 -4
- package/src/structure/Signal.ts +3 -3
- package/ext/css/src/structure/$CSSKeyframeRule.ts +0 -14
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { _instanceof } from "amateras/lib/native";
|
|
2
|
+
import { I18nText, type I18nTextOptions } from "#node/I18nText";
|
|
3
|
+
import { I18nDictionary } from "#structure/I18nDictionary";
|
|
4
|
+
|
|
5
|
+
export class I18n {
|
|
6
|
+
locale$ = $.signal<string>('');
|
|
7
|
+
map = new Map<string, I18nDictionary>();
|
|
8
|
+
#defaultLocale: string;
|
|
9
|
+
constructor(defaultLocale: string) {
|
|
10
|
+
this.#defaultLocale = defaultLocale;
|
|
11
|
+
this.locale$.set(defaultLocale);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
defaultLocale(): string;
|
|
15
|
+
defaultLocale(locale: string): this;
|
|
16
|
+
defaultLocale(locale?: string) {
|
|
17
|
+
if (!arguments.length) return this.#defaultLocale;
|
|
18
|
+
if (locale) this.locale$.set(locale);
|
|
19
|
+
return this;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
locale(): string;
|
|
23
|
+
locale(locale: string): this;
|
|
24
|
+
locale(locale?: string) {
|
|
25
|
+
if (!arguments.length) return this.locale$();
|
|
26
|
+
if (locale) this.locale$.set(locale)
|
|
27
|
+
return this;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
dictionary(locale = this.locale$()) {
|
|
31
|
+
if (!locale) return null;
|
|
32
|
+
const dictionary = this.map.get(locale);
|
|
33
|
+
return dictionary;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
translate(key: string, options?: I18nTextOptions) {
|
|
37
|
+
return new I18nText(this, key, options);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { _instanceof, isObject } from "amateras/lib/native";
|
|
2
|
+
|
|
3
|
+
export class I18nDictionary {
|
|
4
|
+
#context: I18nDictionaryContext | Promise<I18nDictionaryContext> | null = null;
|
|
5
|
+
#fetch: I18nDictionaryContextImporter | null = null;
|
|
6
|
+
constructor(resolver: I18nDictionaryContext | I18nDictionaryContextImporter) {
|
|
7
|
+
if (_instanceof(resolver, Function)) this.#fetch = resolver;
|
|
8
|
+
else this.#context = resolver;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async context(): Promise<I18nDictionaryContext> {
|
|
12
|
+
if (this.#context) return await this.#context;
|
|
13
|
+
if (!this.#fetch) throw 'I18n Context Fetch Error';
|
|
14
|
+
return this.#context = this.#fetch().then((module) => module.default);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async find(path: string, context?: I18nDictionaryContext): Promise<string | undefined> {
|
|
18
|
+
if (!context) context = await this.context();
|
|
19
|
+
const [snippet, ...rest] = path.split('.') as [string, ...string[]];
|
|
20
|
+
const target = context[snippet];
|
|
21
|
+
if (isObject(target)) {
|
|
22
|
+
if (rest.length) return this.find(rest.join('.'), target);
|
|
23
|
+
else return target['_'] as string;
|
|
24
|
+
}
|
|
25
|
+
if (rest.length) return path;
|
|
26
|
+
else return target;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export type I18nDictionaryContext = {[key: string]: string | I18nDictionaryContext}
|
|
31
|
+
export type I18nDictionaryContextImporter = () => Promise<{default: I18nDictionaryContext}>
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { _Array_from, forEach } from "#lib/native";
|
|
2
|
+
|
|
3
|
+
const blockProcesses = new Set<MarkdownBlockProcessOptions>();
|
|
4
|
+
const inlineProcesses = new Set<MarkdownProcessFunction>();
|
|
5
|
+
|
|
6
|
+
export class Markdown {
|
|
7
|
+
blockProcessSet = new Set(blockProcesses);
|
|
8
|
+
inlineProcessSet = new Set(inlineProcesses);
|
|
9
|
+
constructor() {}
|
|
10
|
+
|
|
11
|
+
blockProcess(options: MarkdownBlockProcessOptions) {
|
|
12
|
+
this.blockProcessSet.add(options);
|
|
13
|
+
return this;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
inlineProcess(handle: MarkdownProcessFunction) {
|
|
17
|
+
this.inlineProcessSet.add(handle);
|
|
18
|
+
return this;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
toHTML(text: string) {
|
|
22
|
+
const blocks = _Array_from(text.matchAll(/(?:.+?\n?)+/gm));
|
|
23
|
+
return blocks.map(block => {
|
|
24
|
+
let matched, blockText = block[0]
|
|
25
|
+
for (const blockProcess of blockProcesses) {
|
|
26
|
+
matched = blockText.match(blockProcess.regexp);
|
|
27
|
+
if (!matched) continue;
|
|
28
|
+
blockText = blockProcess.handle(blockText);
|
|
29
|
+
const removeHTML = blockText.replaceAll(/<.+>[^<]+?<\/.+>/gm, '');
|
|
30
|
+
if (!removeHTML) break;
|
|
31
|
+
}
|
|
32
|
+
if (!matched) blockText = paragraph(blockText);
|
|
33
|
+
inlineProcesses.forEach(fn => blockText = fn(blockText))
|
|
34
|
+
return blockText;
|
|
35
|
+
}).join('')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
toDOM(text: string) {
|
|
39
|
+
return $('article').innerHTML(this.toHTML(text))
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type MarkdownProcessFunction = (text: string) => string;
|
|
44
|
+
export interface MarkdownBlockProcessOptions {
|
|
45
|
+
regexp: RegExp,
|
|
46
|
+
handle: (text: string) => string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const blockProcess = (options: MarkdownBlockProcessOptions) => blockProcesses.add(options);
|
|
50
|
+
const inlineProcess = (handle: MarkdownProcessFunction) => inlineProcesses.add(handle);
|
|
51
|
+
const replaceAll = (str: string, searchValue: string | RegExp, replacer: ((substring: string, ...args: any[]) => string) | string): string => str.replaceAll(searchValue, replacer as any);
|
|
52
|
+
const trim = (str: string) => str.trim();
|
|
53
|
+
const paragraph = (str: string) => {
|
|
54
|
+
return replaceAll(str, /(?:.+?\n?)+/gm, $0 => `<p>${trim($0)}</p>`)
|
|
55
|
+
}
|
|
56
|
+
// Headings
|
|
57
|
+
blockProcess({
|
|
58
|
+
regexp: /^(#+) (.+)/gm,
|
|
59
|
+
handle: text => replaceAll(text, /^(#+) (.+)/gm, (_, $1: string, $2) => `<h${$1.length}>${$2}</h${$1.length}>`)
|
|
60
|
+
});
|
|
61
|
+
blockProcess({
|
|
62
|
+
regexp: /^(.+)\n==+$/gm,
|
|
63
|
+
handle: text => replaceAll(text, /^(.+)\n==+$/gm, (_, $1) => `<h1>${$1}</h1>`)
|
|
64
|
+
});
|
|
65
|
+
blockProcess({
|
|
66
|
+
regexp: /^(.+)\n--+$/gm,
|
|
67
|
+
handle: text => replaceAll(text, /^(.+)\n--+$/gm, (_, $1) => `<h2>${$1}</h2>`)
|
|
68
|
+
});
|
|
69
|
+
// Blockquote
|
|
70
|
+
blockProcess({
|
|
71
|
+
regexp: /(?:^> ?.*(?:\n|$))+/gm,
|
|
72
|
+
handle: text => {
|
|
73
|
+
const fn = (str: string) => {
|
|
74
|
+
const blocks = _Array_from(str.matchAll(/(?:^> ?.*(?:\n|$))+/gm));
|
|
75
|
+
forEach(blocks, block => {
|
|
76
|
+
const blocked = fn(replaceAll(block[0], /^> ?/gm, ''));
|
|
77
|
+
str = str.replace(block[0], `<blockquote>\n${paragraph(blocked)}\n</blockquote>`);
|
|
78
|
+
})
|
|
79
|
+
return str;
|
|
80
|
+
}
|
|
81
|
+
return fn(text);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
// List
|
|
85
|
+
blockProcess({
|
|
86
|
+
regexp: /(?:^(?:\t|(?: )+)?(?:-|[0-9]+\.) (?:.+\n?))+/gm,
|
|
87
|
+
handle: text => {
|
|
88
|
+
const fn = (str: string) => {
|
|
89
|
+
const blocks = _Array_from(str.matchAll(/(?:^(?:\t|(?: )+)?(?:-|[0-9]+\.) (?:.+\n?))+/gm));
|
|
90
|
+
forEach(blocks, block => {
|
|
91
|
+
let haveList = false // check this loop have list
|
|
92
|
+
const type = block[0].match(/^(-|[0-9]+\.) /)?.[1] === '-' ? 'ul' : 'ol';
|
|
93
|
+
const listed = replaceAll(block[0], /^(?:-|[0-9]+\.) (.+)/gm, (_, $1: string) => (haveList = true, `<li>\n${trim($1)}\n</li>`));
|
|
94
|
+
const clearTabbed = replaceAll(listed, /^(?:\t|(?: ))/gm, '');
|
|
95
|
+
const convertedList = fn(clearTabbed);
|
|
96
|
+
str = str.replace(block[0], haveList ? `<${type}>\n${trim(convertedList)}\n</${type}>` : convertedList);
|
|
97
|
+
})
|
|
98
|
+
return str;
|
|
99
|
+
}
|
|
100
|
+
return fn(text);
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
// Codeblock
|
|
104
|
+
blockProcess({
|
|
105
|
+
regexp: /^```([^`\n]+)\n([^`]+)?```/gm,
|
|
106
|
+
handle: text => replaceAll(text, /^```([^`\n]+)\n([^`]+)?```/gm, (_, $1, $2: string) => `<pre><code>\n${trim($2)}\n</code></pre>`)
|
|
107
|
+
})
|
|
108
|
+
// Horizontal Rule
|
|
109
|
+
blockProcess({
|
|
110
|
+
regexp: /^(?:---|\*\*\*|___)(\s+)?$/gm,
|
|
111
|
+
handle: text => replaceAll(text, /^(?:---|\*\*\*|___)(\s+)?$/gm, _ => `<hr>`)
|
|
112
|
+
})
|
|
113
|
+
// Bold
|
|
114
|
+
inlineProcess(text => replaceAll(text, /\*\*([^*]+?)\*\*/g, (_, $1) => `<b>${$1}</b>`));
|
|
115
|
+
// Italic
|
|
116
|
+
inlineProcess(text => replaceAll(text, /\*([^*]+?)\*/g, (_, $1) => `<i>${$1}</i>`));
|
|
117
|
+
// Image
|
|
118
|
+
inlineProcess(text => replaceAll(text, /!\[(.+?)\]\((.+?)(?: "(.+?)?")?\)/g, (_, alt, src, title) => `<img src="${src}" alt="${alt}"${title ? ` title="${title}"` : ''}>`));
|
|
119
|
+
// Link
|
|
120
|
+
inlineProcess(text => replaceAll(text, /\[(.+?)\]\((?:(\w\w+?:[^\s]+?)(?: "(.+?)?")?)\)/g, (_, content, href, title) => `<a href="${href}"${title ? ` title="${title}"` : ''}>${content}</a>`));
|
|
121
|
+
inlineProcess(text => replaceAll(text, /\[(.+?)\]\((?:(\w+?@(?:\w|\.\w)+?)(?: "(.+)?")?)\)/g, (_, content, mail, title) => `<a href="mailto:${mail}"${title ? ` title="${title}"` : ''}>${content}</a>`));
|
|
122
|
+
inlineProcess(text => replaceAll(text, /<(\w\w+?:[^\s]+?)>/g, (_, href) => `<a href="${href}">${href}</a>`));
|
|
123
|
+
inlineProcess(text => replaceAll(text, /<(\w+?@(?:\w|\.\w)+?)>/g, (_, mail) => `<a href="mailto:${mail}">${mail}</a>`));
|
package/ext/router/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { AnchorTarget } from "#html/$Anchor";
|
|
2
|
+
import { _Object_assign, forEach } from "#lib/native";
|
|
2
3
|
import type { $NodeContentResolver } from "#node/$Node";
|
|
3
4
|
import type { Page } from "./node/Page";
|
|
4
5
|
import { Route } from "./node/Route";
|
|
@@ -7,18 +8,26 @@ import { RouterAnchor } from "./node/RouterAnchor";
|
|
|
7
8
|
export * from "./node/Route";
|
|
8
9
|
export * from "./node/Router";
|
|
9
10
|
export * from "./node/Page";
|
|
11
|
+
export * from "./node/RouterAnchor";
|
|
10
12
|
|
|
11
13
|
declare module 'amateras/core' {
|
|
12
14
|
export function $<P extends string>(nodeName: 'route', path: P, builder: RouteBuilder<Route<P>, RouteDataResolver<P>>): Route<P>;
|
|
13
15
|
export function $(nodeName: 'router', page?: Page<any>): Router;
|
|
14
16
|
export function $(nodeName: 'ra'): RouterAnchor;
|
|
15
17
|
export namespace $ {
|
|
16
|
-
export function open(url: string | URL | Nullish): typeof Router;
|
|
18
|
+
export function open(url: string | URL | Nullish, target: AnchorTarget): typeof Router;
|
|
17
19
|
export function replace(url: string | URL | Nullish): typeof Router;
|
|
18
20
|
export function back(): typeof Router;
|
|
19
21
|
export function forward(): typeof Router;
|
|
20
22
|
}
|
|
21
23
|
}
|
|
24
|
+
|
|
25
|
+
declare global {
|
|
26
|
+
interface GlobalEventHandlersEventMap {
|
|
27
|
+
'routeopen': Event;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
22
31
|
// assign methods
|
|
23
32
|
_Object_assign($, {
|
|
24
33
|
open: Router.open.bind(Router),
|
|
@@ -27,10 +36,10 @@ _Object_assign($, {
|
|
|
27
36
|
forward: Router.forward.bind(Router)
|
|
28
37
|
});
|
|
29
38
|
// define styles
|
|
30
|
-
[
|
|
39
|
+
forEach([
|
|
31
40
|
`router{display:block}`,
|
|
32
41
|
`page{display:block}`
|
|
33
|
-
]
|
|
42
|
+
], $.style);
|
|
34
43
|
// assign nodes
|
|
35
44
|
$.assign([
|
|
36
45
|
['router', Router],
|
package/ext/router/node/Page.ts
CHANGED
|
@@ -9,6 +9,7 @@ export class Page<R extends Route<any> = any, Data extends RouteData = any> exte
|
|
|
9
9
|
params: Data['params'];
|
|
10
10
|
query: Data['query'];
|
|
11
11
|
#pageTitle: null | string = null;
|
|
12
|
+
initial = false;
|
|
12
13
|
constructor(route: R, data?: {params: any, query: any}) {
|
|
13
14
|
super('page');
|
|
14
15
|
this.route = route;
|
package/ext/router/node/Route.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { _instanceof, _Object_fromEntries, _Array_from } from "#lib/native";
|
|
2
|
-
import { $Element } from "#node
|
|
2
|
+
import { $Element } from "#node/$Element";
|
|
3
3
|
import type { AsyncRoute, RouteBuilder, RouteDataResolver } from "..";
|
|
4
4
|
import { Page } from "./Page";
|
|
5
5
|
|
|
@@ -34,9 +34,10 @@ export class Route<Path extends string = string> extends BaseRouteNode<Path> {
|
|
|
34
34
|
super(path, builder, 'route');
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
async build(data: {params: any, query: any} = {params: {}, query: {}}) {
|
|
38
|
-
|
|
37
|
+
async build(data: {params: any, query: any} = {params: {}, query: {}}, page?: Page) {
|
|
38
|
+
page = page ?? new Page(this, data);
|
|
39
39
|
page.params = data.params;
|
|
40
|
+
page.initial = true;
|
|
40
41
|
let resolver: any = this.builder(page);
|
|
41
42
|
if (_instanceof(resolver, Promise)) {
|
|
42
43
|
const result = await resolver as any;
|
|
@@ -1,32 +1,61 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { AnchorTarget } from "#html/$Anchor";
|
|
2
|
+
import { _document } from "#lib/env";
|
|
3
|
+
import { _Array_from, _instanceof, _JSON_parse, _JSON_stringify, _Object_entries, _Object_fromEntries, forEach, startsWith } from "#lib/native";
|
|
2
4
|
import { Page } from "./Page";
|
|
3
5
|
import { BaseRouteNode, Route } from "./Route";
|
|
4
6
|
|
|
7
|
+
// history index
|
|
8
|
+
let index = 0;
|
|
9
|
+
const _addEventListener = addEventListener;
|
|
5
10
|
const _location = location;
|
|
6
11
|
const {origin} = _location;
|
|
7
12
|
const _history = history;
|
|
8
|
-
const
|
|
9
|
-
|
|
13
|
+
const _sessionStorage = sessionStorage;
|
|
14
|
+
const documentElement = _document.documentElement;
|
|
10
15
|
const [PUSH, REPLACE] = [1, 2] as const;
|
|
11
|
-
const
|
|
16
|
+
const [FORWARD, BACK] = ['forward', 'back'] as const;
|
|
17
|
+
const scrollStorageKey = '__scroll__';
|
|
18
|
+
/** convert path string to URL object */
|
|
19
|
+
const toURL = (path: string | URL) =>
|
|
20
|
+
_instanceof(path, URL) ? path : startsWith(path, 'http') ? new URL(path) : new URL(startsWith(path, origin) ? path : origin + path);
|
|
21
|
+
|
|
22
|
+
type ScrollData = {[key: number]: {x: number, y: number}};
|
|
23
|
+
const scrollRecord = (e?: Event) => {
|
|
24
|
+
const data = _JSON_parse(_sessionStorage.getItem(scrollStorageKey) ?? '{}') as ScrollData;
|
|
25
|
+
data[index] = { x: documentElement.scrollLeft, y: documentElement.scrollTop };
|
|
26
|
+
// e is Event when called from scroll or beforeload
|
|
27
|
+
if (!e) forEach(_Object_entries(data), ([i]) => +i > index && delete data[+i])
|
|
28
|
+
_sessionStorage.setItem(scrollStorageKey, _JSON_stringify(data));
|
|
29
|
+
}
|
|
30
|
+
/** handle history state with push and replace state. */
|
|
31
|
+
const historyHandler = async (path: string | URL | Nullish, mode: 1 | 2, target?: AnchorTarget) => {
|
|
12
32
|
if (!path) return;
|
|
13
33
|
const url = toURL(path);
|
|
14
|
-
if (url.
|
|
15
|
-
|
|
16
|
-
|
|
34
|
+
if (url.href === _location.href) return;
|
|
35
|
+
if (target && target !== '_self') return open(url, target);
|
|
36
|
+
if (url.origin !== origin) return open(url, target);
|
|
37
|
+
scrollRecord();
|
|
38
|
+
if (mode === PUSH) index += 1;
|
|
39
|
+
Router.direction = FORWARD;
|
|
40
|
+
_history[mode === PUSH ? 'pushState' : 'replaceState']({index}, '' , url);
|
|
41
|
+
for (let router of Router.routers) router.routes.size && await router.resolve(path);
|
|
17
42
|
}
|
|
43
|
+
// disable browser scroll restoration
|
|
44
|
+
_history.scrollRestoration = 'manual';
|
|
45
|
+
|
|
18
46
|
export class Router extends BaseRouteNode<''> {
|
|
19
47
|
static pageRouters = new Map<Page, Router>();
|
|
20
48
|
static routers = new Set<Router>();
|
|
21
49
|
pageMap = new Map<string, Page>();
|
|
50
|
+
static direction: 'back' | 'forward' = FORWARD;
|
|
22
51
|
constructor(page?: Page) {
|
|
23
52
|
super('', () => [], 'router')
|
|
24
53
|
Router.routers.add(this);
|
|
25
54
|
if (page) Router.pageRouters.set(page, this);
|
|
26
55
|
}
|
|
27
56
|
|
|
28
|
-
static open(path: string | URL | Nullish) {
|
|
29
|
-
historyHandler(path, PUSH);
|
|
57
|
+
static open(path: string | URL | Nullish, target?: AnchorTarget) {
|
|
58
|
+
historyHandler(path, PUSH, target);
|
|
30
59
|
return this;
|
|
31
60
|
}
|
|
32
61
|
|
|
@@ -46,7 +75,7 @@ export class Router extends BaseRouteNode<''> {
|
|
|
46
75
|
}
|
|
47
76
|
|
|
48
77
|
async resolve(path: string | URL) {
|
|
49
|
-
const {pathname, searchParams, hash} = toURL(path);
|
|
78
|
+
const {pathname, searchParams, hash, href} = toURL(path);
|
|
50
79
|
const routeData = { params: {} as {[key: string]: string}, query: _Object_fromEntries(searchParams) }
|
|
51
80
|
const split = (p: string) => p.replaceAll(/\/+/g, '/').split('/').map(path => `/${path}`);
|
|
52
81
|
|
|
@@ -65,7 +94,7 @@ export class Router extends BaseRouteNode<''> {
|
|
|
65
94
|
if (routeSnippet.includes(':')) {
|
|
66
95
|
if (targetSnippet === '/') continue routeLoop;
|
|
67
96
|
const [prefix, paramName] = routeSnippet.split(':') as [string, string];
|
|
68
|
-
if (!
|
|
97
|
+
if (!startsWith(targetSnippet, prefix)) continue routeLoop;
|
|
69
98
|
routeData.params[paramName] = targetSnippet.replace(`${prefix}`, '');
|
|
70
99
|
pass();
|
|
71
100
|
continue splitLoop;
|
|
@@ -82,23 +111,39 @@ export class Router extends BaseRouteNode<''> {
|
|
|
82
111
|
const targetRoutes = determineRoute(this, pathname + '/', hash);
|
|
83
112
|
// build pages
|
|
84
113
|
let prevPage: null | Page = null, prevRoute: BaseRouteNode<any> = this;
|
|
114
|
+
const appendPage = (prevRouter: Router | undefined, page: Page) => page.parentNode !== prevRouter?.node && prevRouter?.content(page);
|
|
115
|
+
|
|
85
116
|
for (const [route, pathId] of targetRoutes) {
|
|
86
|
-
const page =
|
|
87
|
-
|
|
117
|
+
const page = this.pageMap.get(pathId) ?? new Page(route ?? prevRoute.routes.get('404') ?? new Route('404', () => null), routeData);
|
|
118
|
+
if (!page.initial) await route?.build(routeData, page);
|
|
88
119
|
_document && (_document.title = page.pageTitle() ?? _document.title);
|
|
89
120
|
this.pageMap.set(pathId, page);
|
|
90
|
-
if (
|
|
91
|
-
else this.content(page);
|
|
121
|
+
if (href === _location.href) appendPage(prevPage ? Router.pageRouters.get(prevPage) : this, page);
|
|
92
122
|
prevPage = page;
|
|
93
123
|
if (route) prevRoute = route;
|
|
94
124
|
}
|
|
125
|
+
let { x, y } = Router.scroll ?? {x: 0, y: 0};
|
|
126
|
+
scrollTo(x, y);
|
|
127
|
+
this.dispatchEvent(new Event('routeopen', {bubbles: true}));
|
|
95
128
|
return this;
|
|
96
129
|
}
|
|
97
130
|
|
|
98
131
|
listen() {
|
|
99
|
-
const resolve = () =>
|
|
100
|
-
|
|
132
|
+
const resolve = () => {
|
|
133
|
+
const stateIndex = _history.state?.index ?? 0;
|
|
134
|
+
if (index > stateIndex) Router.direction = BACK;
|
|
135
|
+
if (index < stateIndex) Router.direction = FORWARD;
|
|
136
|
+
index = stateIndex;
|
|
137
|
+
this.resolve(_location.href);
|
|
138
|
+
}
|
|
139
|
+
_addEventListener('popstate', resolve);
|
|
140
|
+
_addEventListener('beforeunload', scrollRecord);
|
|
141
|
+
_addEventListener('scroll', scrollRecord, false);
|
|
101
142
|
resolve();
|
|
102
143
|
return this;
|
|
103
144
|
}
|
|
145
|
+
|
|
146
|
+
static get scroll(): ScrollData[number] {
|
|
147
|
+
return _JSON_parse(_sessionStorage.getItem(scrollStorageKey) ?? '{}')[index] ?? {x: 0, y: 0}
|
|
148
|
+
}
|
|
104
149
|
}
|
|
@@ -3,6 +3,6 @@ import { $Anchor } from "#html/$Anchor";
|
|
|
3
3
|
export class RouterAnchor extends $Anchor {
|
|
4
4
|
constructor() {
|
|
5
5
|
super();
|
|
6
|
-
this.on('click', e => { e.preventDefault(); $.open(this.href()) })
|
|
6
|
+
this.on('click', e => { e.preventDefault(); $.open(this.href(), this.target()) })
|
|
7
7
|
}
|
|
8
8
|
}
|
package/ext/ssr/index.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import './env';
|
|
2
2
|
import 'amateras';
|
|
3
|
-
import { _Array_from, _instanceof, _Object_assign, _Object_defineProperty } from "amateras/lib/native";
|
|
4
|
-
import { $Element
|
|
3
|
+
import { _Array_from, _instanceof, _Object_assign, _Object_defineProperty, forEach } from "amateras/lib/native";
|
|
4
|
+
import { $Element } from 'amateras/node/$Element';
|
|
5
5
|
import { BROWSER, NODE } from 'esm-env';
|
|
6
|
+
import { $Node, $Text } from 'amateras/node/$Node';
|
|
7
|
+
import { _document } from '../../src/lib/env';
|
|
6
8
|
|
|
7
9
|
declare module 'amateras/core' {
|
|
8
10
|
export namespace $ {
|
|
@@ -24,7 +26,7 @@ export function onclient<T>(cb: () => T): T | undefined {
|
|
|
24
26
|
_Object_assign($, {
|
|
25
27
|
mount(id: string, $node: $Element) {
|
|
26
28
|
if (!BROWSER) return;
|
|
27
|
-
const node =
|
|
29
|
+
const node = _document.querySelector(`#${id}`);
|
|
28
30
|
if (!node) throw 'Target node of mounting not found';
|
|
29
31
|
getData(node, $node);
|
|
30
32
|
node.replaceWith($node.node);
|
|
@@ -32,12 +34,12 @@ _Object_assign($, {
|
|
|
32
34
|
function getData(node: Node, $node: $Node) {
|
|
33
35
|
if (node.nodeName === 'SIGNAL' && _instanceof(node, Element) && _instanceof($node, $Text)) {
|
|
34
36
|
const type = $(node).attr()['type'];
|
|
35
|
-
return $node.signals
|
|
37
|
+
return forEach($node.signals, signal => signal.value(type === 'number' ? Number(node.textContent) : type === 'boolean' ? node.textContent == 'true' ? true : false : node.textContent));
|
|
36
38
|
}
|
|
37
39
|
if (_instanceof(node, Text)) return $node.textContent(node.textContent);
|
|
38
40
|
if (_instanceof(node, Element) && _instanceof($node, $Element)) $node.attr($(node).attr());
|
|
39
41
|
const arr = _Array_from($node.childNodes);
|
|
40
|
-
node.childNodes
|
|
42
|
+
forEach(node.childNodes, (_node, i) => {
|
|
41
43
|
const targetChildNode = arr.at(i);
|
|
42
44
|
if (!targetChildNode) throw 'Target DOM tree not matched';
|
|
43
45
|
getData(_node, targetChildNode.$)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { _Array_from, _instanceof, forEach } from "amateras/lib/native";
|
|
2
|
+
import type { $Virtual } from "amateras/node/$Virtual";
|
|
3
|
+
import { $HTMLElement } from "amateras/node/$HTMLElement";
|
|
4
|
+
import { _document } from "../../../src/lib/env";
|
|
5
|
+
|
|
6
|
+
export function VirtualScroll($parent: $Virtual, scroller: Node = _document) {
|
|
7
|
+
scroller.addEventListener('scroll', render, true);
|
|
8
|
+
$parent.on('layout', render);
|
|
9
|
+
function render() {
|
|
10
|
+
const getRect = ($node: $HTMLElement) => $node.getBoundingClientRect();
|
|
11
|
+
const number = (str: string) => parseInt(str);
|
|
12
|
+
const parentRect = getRect($parent);
|
|
13
|
+
const children = _Array_from($parent.nodes);
|
|
14
|
+
forEach(children, $child => {
|
|
15
|
+
if (!_instanceof($child, $HTMLElement)) return;
|
|
16
|
+
const { top, height } = $child.style();
|
|
17
|
+
const topPos = parentRect.top + number(top);
|
|
18
|
+
const bottomPos = topPos + number(height);
|
|
19
|
+
if (bottomPos < 0 || topPos > innerHeight) $parent.hide($child);
|
|
20
|
+
else $parent.show($child);
|
|
21
|
+
})
|
|
22
|
+
$parent.render();
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { _Array_from, _instanceof, forEach, isNull } from "amateras/lib/native";
|
|
2
|
+
import { $HTMLElement } from "amateras/node/$HTMLElement";
|
|
3
|
+
import type { $Node, $NodeContentResolver } from "amateras/node/$Node";
|
|
4
|
+
import { chain } from "../../../src/lib/chain";
|
|
5
|
+
|
|
6
|
+
const [ACCORDIAN, ACCORDIAN_ITEM, ACCORDIAN_TRIGGER, ACCORDIAN_CONTENT, ACCORDIAN_CONTAINER] = ['accordian', 'accordian-item', 'accordian-trigger', 'accordian-content', 'accordian-container'] as const;
|
|
7
|
+
forEach([
|
|
8
|
+
`${ACCORDIAN},${ACCORDIAN_ITEM},${ACCORDIAN_TRIGGER}{display:block}`,
|
|
9
|
+
`${ACCORDIAN_CONTENT}{display:grid;grid-template-rows:0fr}`,
|
|
10
|
+
`${ACCORDIAN_CONTENT}[opened]{grid-template-rows:1fr}`,
|
|
11
|
+
`${ACCORDIAN_CONTAINER}{overflow:hidden}`,
|
|
12
|
+
], $.style)
|
|
13
|
+
|
|
14
|
+
export class Accordian extends $HTMLElement {
|
|
15
|
+
#autoclose = false;
|
|
16
|
+
constructor() {
|
|
17
|
+
super(ACCORDIAN);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
autoclose(): boolean;
|
|
21
|
+
autoclose(autoclose: boolean): this;
|
|
22
|
+
autoclose(autoclose?: boolean) {
|
|
23
|
+
return chain(this, arguments, () => this.#autoclose, autoclose, autoclose => this.#autoclose = autoclose);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
get items() {
|
|
27
|
+
return _Array_from($(this.childNodes)).filter($child => _instanceof($child, AccordianItem))
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class AccordianItem extends $HTMLElement {
|
|
32
|
+
$content: null | AccordianContent = null;
|
|
33
|
+
$trigger: null | AccordianTrigger = null;
|
|
34
|
+
$root: null | Accordian = null;
|
|
35
|
+
constructor() {
|
|
36
|
+
super(ACCORDIAN_ITEM);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
mounted($parent: $Node): this {
|
|
40
|
+
if (_instanceof($parent, Accordian)) this.$root = $parent;
|
|
41
|
+
forEach($(this.childNodes), $c => {
|
|
42
|
+
if (_instanceof($c, AccordianTrigger)) this.$trigger = $c;
|
|
43
|
+
if (_instanceof($c, AccordianContent)) this.$content = $c;
|
|
44
|
+
})
|
|
45
|
+
return this;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class AccordianTrigger extends $HTMLElement {
|
|
50
|
+
$item: null | AccordianItem = null;
|
|
51
|
+
constructor() {
|
|
52
|
+
super(ACCORDIAN_TRIGGER);
|
|
53
|
+
this.on('click', _ => {
|
|
54
|
+
const $item = this.$item;
|
|
55
|
+
const $root = $item?.$root;
|
|
56
|
+
this.$item?.$content?.use($content => isNull($content.attr('opened')) ? $content.open() : $content.close());
|
|
57
|
+
$root?.autoclose() && $root.items.forEach($i => $i !== $item && $i.$content?.close())
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
mounted($parent: $Node): this {
|
|
62
|
+
if (_instanceof($parent, AccordianItem)) this.$item = $parent;
|
|
63
|
+
return this;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class AccordianContent extends $HTMLElement {
|
|
68
|
+
$container = $(AccordianContainer);
|
|
69
|
+
constructor() {
|
|
70
|
+
super(ACCORDIAN_CONTENT);
|
|
71
|
+
super.insert(this.$container);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
content(children: $NodeContentResolver<AccordianContainer>): this {
|
|
75
|
+
this.$container.content(children);
|
|
76
|
+
return this;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
insert(resolver: $NodeContentResolver<AccordianContainer>, position?: number): this {
|
|
80
|
+
this.$container.insert(resolver, position);
|
|
81
|
+
return this;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
open() {
|
|
85
|
+
return this.attr({opened: ''})
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
close() {
|
|
89
|
+
return this.attr({opened: null});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export class AccordianContainer extends $HTMLElement {
|
|
94
|
+
constructor() {
|
|
95
|
+
super(ACCORDIAN_CONTAINER);
|
|
96
|
+
}
|
|
97
|
+
}
|