amateras 0.3.0 → 0.4.1
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 +6 -4
- package/ext/css/README.md +19 -0
- package/ext/css/src/index.ts +347 -331
- package/ext/css/src/lib/colorAssign.ts +1 -1
- package/ext/css/src/structure/$CSSContainerRule.ts +13 -0
- package/ext/css/src/structure/$CSSRule.ts +1 -1
- package/ext/css/src/structure/$CSSStyleRule.ts +0 -7
- package/ext/css/src/structure/$CSSVariable.ts +3 -3
- package/ext/html/html.ts +1 -13
- 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/router/index.ts +13 -6
- package/ext/router/node/Page.ts +3 -3
- package/ext/router/node/Route.ts +2 -1
- package/ext/router/node/Router.ts +33 -22
- package/ext/ssr/index.ts +4 -2
- package/package.json +4 -7
- package/src/core.ts +33 -23
- package/src/lib/assign.ts +11 -12
- package/src/lib/assignHelper.ts +9 -8
- package/src/lib/chain.ts +13 -0
- package/src/lib/debounce.ts +7 -0
- package/src/lib/env.ts +2 -0
- package/src/lib/native.ts +25 -35
- package/src/lib/randomId.ts +2 -4
- package/src/lib/sleep.ts +1 -3
- package/src/node/$Element.ts +183 -24
- package/src/node/$HTMLElement.ts +23 -0
- package/src/node/$Node.ts +81 -57
- package/src/{node/node.ts → node.ts} +2 -5
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { _Object_assign } from "../../../../src/lib/native";
|
|
2
2
|
|
|
3
|
-
export
|
|
3
|
+
export const colorAssign = (key: string, colors: {[key: number]: string}) => {
|
|
4
4
|
if (!$.color) _Object_assign($, {color: {}});
|
|
5
5
|
_Object_assign($.color, {[key]: colors})
|
|
6
6
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { _Array_from, _instanceof } from "amateras/lib/native";
|
|
2
|
+
import { $CSSRule } from "#structure/$CSSRule";
|
|
3
|
+
|
|
4
|
+
export class $CSSContainerRule extends $CSSRule {
|
|
5
|
+
condition: string;
|
|
6
|
+
name: string;
|
|
7
|
+
constructor(selector: string) {
|
|
8
|
+
super(selector);
|
|
9
|
+
const [_, name, condition] = selector.match(/@container (.+?) (.+)/) as [string, string, string]
|
|
10
|
+
this.name = name;
|
|
11
|
+
this.condition = condition;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -8,6 +8,6 @@ export abstract class $CSSRule {
|
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
get options(): {[key: string]: any} {
|
|
11
|
-
return _Object_fromEntries(_Array_from(this.rules).map(rule => [rule.selector, rule]))
|
|
11
|
+
return _Object_fromEntries(_Array_from(this.rules).map(rule => [rule.selector, rule.options]))
|
|
12
12
|
}
|
|
13
13
|
}
|
|
@@ -8,13 +8,6 @@ export class $CSSStyleRule extends $CSSRule {
|
|
|
8
8
|
super(selector);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
-
clone(selector: string) {
|
|
12
|
-
const rule = new $CSSStyleRule(selector)
|
|
13
|
-
rule.declarations = this.declarations;
|
|
14
|
-
rule.rules = this.rules;
|
|
15
|
-
return rule
|
|
16
|
-
}
|
|
17
|
-
|
|
18
11
|
get options(): {[key: string]: any} {
|
|
19
12
|
return {..._Object_fromEntries(_Array_from(this.declarations).map(([_, dec]) => [dec.key, dec])), ...super.options}
|
|
20
13
|
}
|
package/ext/html/html.ts
CHANGED
|
@@ -10,16 +10,4 @@ import './node/$Media';
|
|
|
10
10
|
import './node/$OptGroup';
|
|
11
11
|
import './node/$Option';
|
|
12
12
|
import './node/$Select';
|
|
13
|
-
import './node/$TextArea';
|
|
14
|
-
export * from './node/$Anchor';
|
|
15
|
-
export * from './node/$Canvas';
|
|
16
|
-
export * from './node/$Dialog';
|
|
17
|
-
export * from './node/$Form';
|
|
18
|
-
export * from './node/$Image';
|
|
19
|
-
export * from './node/$Input';
|
|
20
|
-
export * from './node/$Label';
|
|
21
|
-
export * from './node/$Media';
|
|
22
|
-
export * from './node/$OptGroup';
|
|
23
|
-
export * from './node/$Option';
|
|
24
|
-
export * from './node/$Select';
|
|
25
|
-
export * from './node/$TextArea';
|
|
13
|
+
import './node/$TextArea';
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# amateras/i18n
|
|
2
|
+
|
|
3
|
+
## Usage
|
|
4
|
+
```ts
|
|
5
|
+
import 'amateras';
|
|
6
|
+
import 'amateras/i18n';
|
|
7
|
+
|
|
8
|
+
const $t = $.i18n()
|
|
9
|
+
// add 'en' locale dictionary context
|
|
10
|
+
.add('en', {
|
|
11
|
+
homepage: {
|
|
12
|
+
_: 'Home',
|
|
13
|
+
hello: 'Hello, $name$!',
|
|
14
|
+
}
|
|
15
|
+
})
|
|
16
|
+
// set 'en' as locale language
|
|
17
|
+
.locale('en')
|
|
18
|
+
|
|
19
|
+
$(document.body).content([
|
|
20
|
+
$('h1').content( $t('homepage') )
|
|
21
|
+
// <h1><text>Home</text></h1>
|
|
22
|
+
$t('homepage.hello', {name: 'Amateras'})
|
|
23
|
+
// <text>Hello, Amateras!</text>
|
|
24
|
+
])
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Change Language
|
|
28
|
+
```ts
|
|
29
|
+
$t.locale('zh')
|
|
30
|
+
// all translation text will be updated
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Import Dictionary Context
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
// ./i18n/en.ts
|
|
37
|
+
export default {
|
|
38
|
+
hello: 'Hello, $name$!'
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ./i18n/zh.ts
|
|
42
|
+
export default {
|
|
43
|
+
hello: '您好,$name$!'
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ./entry_file.ts
|
|
47
|
+
const $t = $.i18n()
|
|
48
|
+
.add('en', () => import('./i18n/en.ts'))
|
|
49
|
+
.add('zh', () => import('./i18n/zh.ts'))
|
|
50
|
+
// set 'zh' as locale language
|
|
51
|
+
// and fetch file automatically from path
|
|
52
|
+
.locale('zh');
|
|
53
|
+
```
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { _Array_from, _instanceof, _Object_assign } from "amateras/lib/native"
|
|
2
|
+
import { $ } from "amateras/core"
|
|
3
|
+
import { I18n } from "#structure/I18n"
|
|
4
|
+
import type { I18nText as _I18nText, I18nTextOptions } from "#node/I18nText";
|
|
5
|
+
import { I18nDictionary, type I18nDictionaryContext, type I18nDictionaryContextImporter } from "#structure/I18nDictionary";
|
|
6
|
+
|
|
7
|
+
_Object_assign($, {
|
|
8
|
+
i18n(defaultLocale: string) {
|
|
9
|
+
const i18n = new I18n(defaultLocale);
|
|
10
|
+
const i18nFn = (key: string, options?: I18nTextOptions) => i18n.translate(key, options);
|
|
11
|
+
_Object_assign(i18nFn, {
|
|
12
|
+
i18n,
|
|
13
|
+
locale(locale: string) {
|
|
14
|
+
if (!arguments.length) return i18n.locale();
|
|
15
|
+
i18n.locale(locale);
|
|
16
|
+
return this;
|
|
17
|
+
},
|
|
18
|
+
add(lang: string, context: I18nDictionaryContext | I18nDictionaryContextImporter) {
|
|
19
|
+
i18n.map.set(lang, new I18nDictionary(context));
|
|
20
|
+
return this;
|
|
21
|
+
},
|
|
22
|
+
delete(lang: string) {
|
|
23
|
+
i18n.map.delete(lang);
|
|
24
|
+
return this;
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
return i18nFn
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
type ResolvedAsyncDictionary<F extends I18nDictionaryContextImporter> = Awaited<ReturnType<F>>['default'];
|
|
32
|
+
|
|
33
|
+
type DeepKeys<T> = T extends I18nDictionaryContext
|
|
34
|
+
? {
|
|
35
|
+
[K in keyof T]: K extends string
|
|
36
|
+
? K extends '_' ? never : `${K}` | `${K}.${DeepKeys<T[K]>}`
|
|
37
|
+
: never;
|
|
38
|
+
}[keyof T]
|
|
39
|
+
: never;
|
|
40
|
+
|
|
41
|
+
declare module "amateras/core" {
|
|
42
|
+
export namespace $ {
|
|
43
|
+
export interface I18nFunction<D extends I18nDictionaryContext = {}> {
|
|
44
|
+
(path: DeepKeys<D>, ...args: any[]): I18nText;
|
|
45
|
+
i18n: I18n;
|
|
46
|
+
locale(): string;
|
|
47
|
+
locale(lang?: $Parameter<string>): this;
|
|
48
|
+
add<F extends I18nDictionaryContext | I18nDictionaryContextImporter>(lang: string, dictionary: F): I18nFunction<D | (F extends I18nDictionaryContextImporter ? ResolvedAsyncDictionary<F> : F)>;
|
|
49
|
+
delete(lang: string): this;
|
|
50
|
+
}
|
|
51
|
+
export function i18n(defaultLocale: string): I18nFunction;
|
|
52
|
+
export type I18nText = _I18nText;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { _Array_from, isUndefined } from "amateras/lib/native";
|
|
2
|
+
import { $HTMLElement } from "amateras/node/$HTMLElement";
|
|
3
|
+
import type { I18n } from "#structure/I18n";
|
|
4
|
+
|
|
5
|
+
export class I18nText extends $HTMLElement<HTMLElement, { i18nupdate: Event }> {
|
|
6
|
+
i18n: I18n;
|
|
7
|
+
key: string;
|
|
8
|
+
options: I18nTextOptions | undefined;
|
|
9
|
+
constructor(i18n: I18n, key: string, options?: I18nTextOptions) {
|
|
10
|
+
super('text');
|
|
11
|
+
this.i18n = i18n;
|
|
12
|
+
this.key = key;
|
|
13
|
+
this.options = options;
|
|
14
|
+
i18n.locale$.signal.subscribe(() => this.update())
|
|
15
|
+
this.update();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async update() {
|
|
19
|
+
update: {
|
|
20
|
+
const {key, i18n} = this;
|
|
21
|
+
const dictionary = i18n.dictionary();
|
|
22
|
+
if (!dictionary) {this.content(key); break update}
|
|
23
|
+
const target = await dictionary.find(key);
|
|
24
|
+
if (isUndefined(target)) break update;
|
|
25
|
+
const snippets = target.split(/\$[a-zA-Z0-9_]+\$/);
|
|
26
|
+
if (snippets.length === 1 || !this.options) {this.content(target); break update}
|
|
27
|
+
const matches = target.matchAll(/(\$([a-zA-Z0-9_]+)\$)/g);
|
|
28
|
+
this.content(snippets.map(text => [text, this.options?.[matches.next().value?.at(2)!] ?? null]));
|
|
29
|
+
}
|
|
30
|
+
this.dispatchEvent(new Event('i18nupdate'));
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type I18nTextOptions = {[key: string]: any}
|
|
@@ -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}>
|
package/ext/router/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AnchorTarget } from "#html/$Anchor";
|
|
2
|
-
import { _Object_assign, forEach } from "#lib/native";
|
|
2
|
+
import { _bind, _Object_assign, forEach } from "#lib/native";
|
|
3
3
|
import type { $NodeContentResolver } from "#node/$Node";
|
|
4
4
|
import type { Page } from "./node/Page";
|
|
5
5
|
import { Route } from "./node/Route";
|
|
@@ -21,18 +21,25 @@ declare module 'amateras/core' {
|
|
|
21
21
|
export function forward(): typeof Router;
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
+
|
|
25
|
+
declare global {
|
|
26
|
+
interface GlobalEventHandlersEventMap {
|
|
27
|
+
'routeopen': Event;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
24
31
|
// assign methods
|
|
25
32
|
_Object_assign($, {
|
|
26
|
-
open: Router.open
|
|
27
|
-
replace: Router.replace
|
|
28
|
-
back: Router.back
|
|
29
|
-
forward: Router.forward
|
|
33
|
+
open: _bind(Router.open, Router),
|
|
34
|
+
replace: _bind(Router.replace, Router),
|
|
35
|
+
back: _bind(Router.back, Router),
|
|
36
|
+
forward: _bind(Router.forward, Router)
|
|
30
37
|
});
|
|
31
38
|
// define styles
|
|
32
39
|
forEach([
|
|
33
40
|
`router{display:block}`,
|
|
34
41
|
`page{display:block}`
|
|
35
|
-
],
|
|
42
|
+
], $.style);
|
|
36
43
|
// assign nodes
|
|
37
44
|
$.assign([
|
|
38
45
|
['router', Router],
|
package/ext/router/node/Page.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { chain } from "#lib/chain";
|
|
1
2
|
import { isUndefined } from "#lib/native";
|
|
2
3
|
import { $HTMLElement } from "#node/$HTMLElement";
|
|
3
4
|
import type { RouteData } from "..";
|
|
@@ -9,6 +10,7 @@ export class Page<R extends Route<any> = any, Data extends RouteData = any> exte
|
|
|
9
10
|
params: Data['params'];
|
|
10
11
|
query: Data['query'];
|
|
11
12
|
#pageTitle: null | string = null;
|
|
13
|
+
initial = false;
|
|
12
14
|
constructor(route: R, data?: {params: any, query: any}) {
|
|
13
15
|
super('page');
|
|
14
16
|
this.route = route;
|
|
@@ -20,8 +22,6 @@ export class Page<R extends Route<any> = any, Data extends RouteData = any> exte
|
|
|
20
22
|
pageTitle(): string | null;
|
|
21
23
|
pageTitle(title: string | null): this;
|
|
22
24
|
pageTitle(title?: string | null) {
|
|
23
|
-
|
|
24
|
-
if (!isUndefined(title)) this.#pageTitle = title;
|
|
25
|
-
return this;
|
|
25
|
+
return chain(this, arguments, () => this.#pageTitle, title, title => this.#pageTitle = title)
|
|
26
26
|
}
|
|
27
27
|
}
|
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
|
|
|
@@ -37,6 +37,7 @@ export class Route<Path extends string = string> extends BaseRouteNode<Path> {
|
|
|
37
37
|
async build(data: {params: any, query: any} = {params: {}, query: {}}, page?: Page) {
|
|
38
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,24 +1,32 @@
|
|
|
1
1
|
import type { AnchorTarget } from "#html/$Anchor";
|
|
2
|
-
import {
|
|
2
|
+
import { _document } from "#lib/env";
|
|
3
|
+
import { _Array_from, _instanceof, _JSON_parse, _JSON_stringify, _Object_entries, _Object_fromEntries, forEach, startsWith } from "#lib/native";
|
|
3
4
|
import { Page } from "./Page";
|
|
4
5
|
import { BaseRouteNode, Route } from "./Route";
|
|
5
6
|
|
|
6
7
|
// history index
|
|
7
8
|
let index = 0;
|
|
9
|
+
const _addEventListener = addEventListener;
|
|
8
10
|
const _location = location;
|
|
9
11
|
const {origin} = _location;
|
|
10
12
|
const _history = history;
|
|
13
|
+
const _sessionStorage = sessionStorage;
|
|
11
14
|
const documentElement = _document.documentElement;
|
|
12
15
|
const [PUSH, REPLACE] = [1, 2] as const;
|
|
13
16
|
const [FORWARD, BACK] = ['forward', 'back'] as const;
|
|
14
|
-
|
|
15
|
-
// disable browser scroll restoration
|
|
16
|
-
_history.scrollRestoration = 'manual';
|
|
17
|
-
|
|
17
|
+
const scrollStorageKey = '__scroll__';
|
|
18
18
|
/** convert path string to URL object */
|
|
19
19
|
const toURL = (path: string | URL) =>
|
|
20
|
-
_instanceof(path, URL) ? path :
|
|
20
|
+
_instanceof(path, URL) ? path : startsWith(path, 'http') ? new URL(path) : new URL(startsWith(path, origin) ? path : origin + path);
|
|
21
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
|
+
}
|
|
22
30
|
/** handle history state with push and replace state. */
|
|
23
31
|
const historyHandler = async (path: string | URL | Nullish, mode: 1 | 2, target?: AnchorTarget) => {
|
|
24
32
|
if (!path) return;
|
|
@@ -26,16 +34,14 @@ const historyHandler = async (path: string | URL | Nullish, mode: 1 | 2, target?
|
|
|
26
34
|
if (url.href === _location.href) return;
|
|
27
35
|
if (target && target !== '_self') return open(url, target);
|
|
28
36
|
if (url.origin !== origin) return open(url, target);
|
|
29
|
-
|
|
30
|
-
index: index,
|
|
31
|
-
x: documentElement.scrollLeft,
|
|
32
|
-
y: documentElement.scrollTop
|
|
33
|
-
}, '', _location.href);
|
|
37
|
+
scrollRecord();
|
|
34
38
|
if (mode === PUSH) index += 1;
|
|
35
39
|
Router.direction = FORWARD;
|
|
36
|
-
|
|
37
|
-
for (let router of Router.routers) router.routes.size && await router.resolve(path)
|
|
40
|
+
_history[mode === PUSH ? 'pushState' : 'replaceState']({index}, '' , url);
|
|
41
|
+
for (let router of Router.routers) router.routes.size && await router.resolve(path);
|
|
38
42
|
}
|
|
43
|
+
// disable browser scroll restoration
|
|
44
|
+
_history.scrollRestoration = 'manual';
|
|
39
45
|
|
|
40
46
|
export class Router extends BaseRouteNode<''> {
|
|
41
47
|
static pageRouters = new Map<Page, Router>();
|
|
@@ -88,7 +94,7 @@ export class Router extends BaseRouteNode<''> {
|
|
|
88
94
|
if (routeSnippet.includes(':')) {
|
|
89
95
|
if (targetSnippet === '/') continue routeLoop;
|
|
90
96
|
const [prefix, paramName] = routeSnippet.split(':') as [string, string];
|
|
91
|
-
if (!
|
|
97
|
+
if (!startsWith(targetSnippet, prefix)) continue routeLoop;
|
|
92
98
|
routeData.params[paramName] = targetSnippet.replace(`${prefix}`, '');
|
|
93
99
|
pass();
|
|
94
100
|
continue splitLoop;
|
|
@@ -105,21 +111,20 @@ export class Router extends BaseRouteNode<''> {
|
|
|
105
111
|
const targetRoutes = determineRoute(this, pathname + '/', hash);
|
|
106
112
|
// build pages
|
|
107
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
|
+
|
|
108
116
|
for (const [route, pathId] of targetRoutes) {
|
|
109
117
|
const page = this.pageMap.get(pathId) ?? new Page(route ?? prevRoute.routes.get('404') ?? new Route('404', () => null), routeData);
|
|
110
|
-
await route?.build(routeData, page);
|
|
118
|
+
if (!page.initial) await route?.build(routeData, page);
|
|
111
119
|
_document && (_document.title = page.pageTitle() ?? _document.title);
|
|
112
120
|
this.pageMap.set(pathId, page);
|
|
113
|
-
|
|
114
|
-
if (href === _location.href) {
|
|
115
|
-
if (prevPage) Router.pageRouters.get(prevPage)?.content(page);
|
|
116
|
-
else this.content(page);
|
|
117
|
-
}
|
|
121
|
+
if (href === _location.href) appendPage(prevPage ? Router.pageRouters.get(prevPage) : this, page);
|
|
118
122
|
prevPage = page;
|
|
119
123
|
if (route) prevRoute = route;
|
|
120
124
|
}
|
|
121
|
-
let { x, y } =
|
|
125
|
+
let { x, y } = Router.scroll ?? {x: 0, y: 0};
|
|
122
126
|
scrollTo(x, y);
|
|
127
|
+
this.dispatchEvent(new Event('routeopen', {bubbles: true}));
|
|
123
128
|
return this;
|
|
124
129
|
}
|
|
125
130
|
|
|
@@ -131,8 +136,14 @@ export class Router extends BaseRouteNode<''> {
|
|
|
131
136
|
index = stateIndex;
|
|
132
137
|
this.resolve(_location.href);
|
|
133
138
|
}
|
|
134
|
-
|
|
139
|
+
_addEventListener('popstate', resolve);
|
|
140
|
+
_addEventListener('beforeunload', scrollRecord);
|
|
141
|
+
_addEventListener('scroll', scrollRecord, false);
|
|
135
142
|
resolve();
|
|
136
143
|
return this;
|
|
137
144
|
}
|
|
145
|
+
|
|
146
|
+
static get scroll(): ScrollData[number] {
|
|
147
|
+
return _JSON_parse(_sessionStorage.getItem(scrollStorageKey) ?? '{}')[index] ?? {x: 0, y: 0}
|
|
148
|
+
}
|
|
138
149
|
}
|
package/ext/ssr/index.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import './env';
|
|
2
2
|
import 'amateras';
|
|
3
|
-
import { _Array_from,
|
|
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 $ {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "amateras",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Amateras is a DOM Utility library.",
|
|
5
5
|
"module": "index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -25,7 +25,6 @@
|
|
|
25
25
|
"exports": {
|
|
26
26
|
".": "./src/index.ts",
|
|
27
27
|
"./core": "./src/core.ts",
|
|
28
|
-
"./node": "./src/node/node.ts",
|
|
29
28
|
"./node/*": "./src/node/*.ts",
|
|
30
29
|
"./lib/*": "./src/lib/*.ts",
|
|
31
30
|
"./structure/*": "./src/structure/*.ts",
|
|
@@ -35,12 +34,10 @@
|
|
|
35
34
|
"./css/colors": "./ext/css/src/lib/colors.ts",
|
|
36
35
|
"./css/color/*": "./ext/css/src/lib/colors/*.ts",
|
|
37
36
|
"./router": "./ext/router/index.ts",
|
|
38
|
-
"./ssr": "./ext/ssr/index.ts"
|
|
37
|
+
"./ssr": "./ext/ssr/index.ts",
|
|
38
|
+
"./i18n": "./ext/i18n/src/index.ts"
|
|
39
39
|
},
|
|
40
40
|
"workspaces": [
|
|
41
41
|
"./ext/*"
|
|
42
|
-
]
|
|
43
|
-
"peerDependencies": {
|
|
44
|
-
"@amateras/css": "./ext/css"
|
|
45
|
-
}
|
|
42
|
+
]
|
|
46
43
|
}
|
package/src/core.ts
CHANGED
|
@@ -1,27 +1,36 @@
|
|
|
1
1
|
import './global';
|
|
2
|
+
import './node';
|
|
2
3
|
import { Signal } from "#structure/Signal";
|
|
3
4
|
import { $Element, type $Event } from "#node/$Element";
|
|
4
5
|
import { $Node, type $NodeContentResolver, type $NodeContentTypes } from '#node/$Node';
|
|
5
|
-
import '#
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
6
|
+
import { _instanceof, isString, isFunction, _Object_assign, isObject, isNull, _Object_entries, _Object_defineProperty, forEach, isNumber, _Array_from, isUndefined, _bind, _null } from '#lib/native';
|
|
7
|
+
import { $HTMLElement } from '#node/$HTMLElement';
|
|
8
|
+
import { _document } from '#lib/env';
|
|
8
9
|
|
|
9
10
|
const nodeNameMap: {[key: string]: Constructor<$Node>} = {}
|
|
11
|
+
const _stylesheet = new CSSStyleSheet();
|
|
10
12
|
|
|
11
13
|
export function $<F extends (...args: any[]) => $NodeContentResolver<$Node>, N extends number>(number: N, fn: F, ...args: Parameters<F>): Repeat<ReturnType<F>, N>;
|
|
12
14
|
export function $<F extends (...args: any[]) => $NodeContentResolver<$Node>>(fn: F, ...args: Parameters<F>): ReturnType<F>;
|
|
13
15
|
export function $<T extends Constructor<$Node>, P extends ConstructorParameters<T>, N extends number>(number: N, construct: T, ...args: P): Repeat<InstanceType<T>, N>;
|
|
14
16
|
export function $<T extends Constructor<$Node>, P extends ConstructorParameters<T>>(construct: T, ...args: P): InstanceType<T>;
|
|
17
|
+
export function $(nodes: NodeListOf<Node | ChildNode>): $Node[];
|
|
15
18
|
export function $<N extends $Node>($node: N, ...args: any[]): N;
|
|
19
|
+
export function $<N extends $Node>($node: N | null | undefined, ...args: any[]): N | null | undefined;
|
|
16
20
|
export function $<H extends HTMLElement>(element: H, ...args: any[]): $HTMLElement<H>;
|
|
21
|
+
export function $<H extends HTMLElement>(element: H | null | undefined, ...args: any[]): $HTMLElement<H> | null | undefined;
|
|
17
22
|
export function $<E extends Element>(element: E, ...args: any[]): $Element<E>;
|
|
23
|
+
export function $<E extends Element>(element: E | null | undefined, ...args: any[]): $Element<E> | null | undefined;
|
|
24
|
+
export function $<N extends Node | EventTarget>(node: N, ...args: any[]): $Node;
|
|
25
|
+
export function $<N extends Node | EventTarget>(node: N | null | undefined, ...args: any[]): $Node | null | undefined;
|
|
18
26
|
export function $<K extends TemplateStringsArray>(string: K, ...values: any[]): $NodeContentTypes[];
|
|
19
27
|
export function $<K extends keyof HTMLElementTagNameMap, N extends number>(number: N, tagname: K): Repeat<$HTMLElement<HTMLElementTagNameMap[K]>, N>;
|
|
20
28
|
export function $<K extends keyof HTMLElementTagNameMap>(tagname: K): $HTMLElement<HTMLElementTagNameMap[K]>;
|
|
21
|
-
export function $<Ev extends $Event<$Element, Event>>(event: Ev): Ev['
|
|
29
|
+
export function $<Ev extends $Event<$Element, Event>>(event: Ev): Ev['currentTarget']['$'];
|
|
22
30
|
export function $<N extends number>(number: N, tagname: string): Repeat<$HTMLElement<HTMLElement>, N>;
|
|
23
31
|
export function $(tagname: string): $HTMLElement<HTMLElement>
|
|
24
|
-
export function $(resolver: string | number | Element | $Node | Function | TemplateStringsArray | Event
|
|
32
|
+
export function $(resolver: string | number | null | undefined | Element | HTMLElement | $Node | Function | TemplateStringsArray | Event | NodeListOf<Node | ChildNode>, ...args: any[]) {
|
|
33
|
+
if (isNull(resolver) || isUndefined(resolver)) return null;
|
|
25
34
|
if (_instanceof(resolver, $Node)) return resolver;
|
|
26
35
|
if (isString(resolver) && nodeNameMap[resolver]) return new nodeNameMap[resolver](...args);
|
|
27
36
|
if (isFunction(resolver))
|
|
@@ -34,16 +43,19 @@ export function $(resolver: string | number | Element | $Node | Function | Templ
|
|
|
34
43
|
if (_instanceof(resolver, Node) && _instanceof(resolver.$, $Node)) return resolver.$;
|
|
35
44
|
if (_instanceof(resolver, Event)) return $(resolver.currentTarget as Element);
|
|
36
45
|
if (isNumber(resolver)) return _Array_from({length: resolver}).map(_ => $(args[0], ...args.slice(1)));
|
|
37
|
-
return new $
|
|
46
|
+
if (_instanceof(resolver, HTMLElement)) return new $HTMLElement(resolver);
|
|
47
|
+
if (_instanceof(resolver, Element)) return new $Element(resolver);
|
|
48
|
+
if (_instanceof(resolver, NodeList)) return _Array_from(resolver).map($)
|
|
49
|
+
return new $HTMLElement(resolver);
|
|
38
50
|
}
|
|
39
51
|
|
|
40
52
|
export namespace $ {
|
|
41
|
-
export const stylesheet =
|
|
42
|
-
_document.adoptedStyleSheets.push(
|
|
53
|
+
export const stylesheet = _stylesheet;
|
|
54
|
+
_document.adoptedStyleSheets.push(_stylesheet);
|
|
55
|
+
export const style = _bind(_stylesheet.insertRule, _stylesheet);
|
|
43
56
|
type SignalProcess<T> = T extends Array<any> ? {} : T extends object ? { [key in keyof T as `${string & key}$`]: SignalFunction<T[key]> } : {};
|
|
44
57
|
export type SignalFunction<T> = {signal: Signal<T>, set: (newValue: T | ((oldValue: T) => T)) => SignalFunction<T>} & (() => T) & SignalProcess<T>;
|
|
45
|
-
export
|
|
46
|
-
export function signal<T>(value: T) {
|
|
58
|
+
export const signal = <T>(value: T): SignalFunction<T> => {
|
|
47
59
|
const signal = new Signal<T>(value);
|
|
48
60
|
const signalFn = function () { return signal.value(); }
|
|
49
61
|
_Object_assign(signalFn, {
|
|
@@ -61,14 +73,14 @@ export namespace $ {
|
|
|
61
73
|
}
|
|
62
74
|
|
|
63
75
|
export type ComputeFunction<T> = ({(): T}) & { signal: Signal<T> };
|
|
64
|
-
export
|
|
76
|
+
export const compute = <T>(process: () => T): ComputeFunction<T> => {
|
|
65
77
|
let subscribed = false;
|
|
66
|
-
const signalFn: SignalFunction<any> = signal(
|
|
67
|
-
|
|
78
|
+
const signalFn: SignalFunction<any> = signal(_null);
|
|
79
|
+
const computeFn = () => {
|
|
68
80
|
if (!subscribed) return signalFn.set(subscribe())();
|
|
69
81
|
else return signalFn.set(process())();
|
|
70
82
|
}
|
|
71
|
-
|
|
83
|
+
const subscribe = () => {
|
|
72
84
|
const signalHandler = (signal: Signal<any>) => {
|
|
73
85
|
signal.subscribe(() => signalFn.set(process()))
|
|
74
86
|
}
|
|
@@ -82,21 +94,19 @@ export namespace $ {
|
|
|
82
94
|
return computeFn as ComputeFunction<T>
|
|
83
95
|
}
|
|
84
96
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
97
|
+
type assign = {
|
|
98
|
+
(resolver: [nodeName: string, $node: Constructor<$Node>][]): $;
|
|
99
|
+
(nodeName: string, $node: Constructor<$Node>): $;
|
|
100
|
+
}
|
|
101
|
+
export const assign: assign = (resolver: string | [nodeName: string, $node: Constructor<$Node>][], $node?: Constructor<$Node>) => {
|
|
88
102
|
if (isString(resolver)) $node && (nodeNameMap[resolver] = $node);
|
|
89
103
|
else forEach(resolver, ([nodeName, $node]) => nodeNameMap[nodeName] = $node);
|
|
90
104
|
return $;
|
|
91
105
|
}
|
|
92
106
|
|
|
93
|
-
export
|
|
94
|
-
return _instanceof(item, Array) ? item : [item];
|
|
95
|
-
}
|
|
107
|
+
export const toArray = <T>(item: OrArray<T>): T[] => _instanceof(item, Array) ? item : [item];
|
|
96
108
|
|
|
97
|
-
export
|
|
98
|
-
return $('span').content(content);
|
|
99
|
-
}
|
|
109
|
+
export const span = (content: string) => $('span').content(content);
|
|
100
110
|
}
|
|
101
111
|
export type $ = typeof $;
|
|
102
112
|
globalThis.$ = $;
|