primate 0.0.1 → 0.3.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 +27 -0
- package/README.md +50 -0
- package/package.json +15 -1
- package/source/client/Action.js +159 -0
- package/source/client/App.js +16 -0
- package/source/client/Base.js +5 -0
- package/source/client/Client.js +65 -0
- package/source/client/Context.js +53 -0
- package/source/client/Element.js +245 -0
- package/source/client/Node.js +13 -0
- package/source/client/View.js +90 -0
- package/source/client/document.js +6 -0
- package/source/client/exports.js +15 -0
- package/source/preset/client/Element.js +2 -0
- package/source/preset/client/app.js +2 -0
- package/source/preset/data/stores/default.js +2 -0
- package/source/preset/primate.json +31 -0
- package/source/preset/static/index.html +10 -0
- package/source/server/Action.js +118 -0
- package/source/server/App.js +42 -0
- package/source/server/Base.js +35 -0
- package/source/server/Bundler.js +180 -0
- package/source/server/Context.js +90 -0
- package/source/server/Crypto.js +8 -0
- package/source/server/Directory.js +35 -0
- package/source/server/File.js +117 -0
- package/source/server/Router.js +61 -0
- package/source/server/Session.js +69 -0
- package/source/server/attributes.js +11 -0
- package/source/server/cache.js +17 -0
- package/source/server/conf.js +33 -0
- package/source/server/domain/Domain.js +277 -0
- package/source/server/domain/Field.js +115 -0
- package/source/server/domain/domains.js +15 -0
- package/source/server/errors/Fallback.js +1 -0
- package/source/server/errors/InternalServer.js +1 -0
- package/source/server/errors/Predicate.js +1 -0
- package/source/server/errors.js +3 -0
- package/source/server/exports.js +27 -0
- package/source/server/fallback.js +11 -0
- package/source/server/invariants.js +19 -0
- package/source/server/log.js +22 -0
- package/source/server/promises/Eager.js +49 -0
- package/source/server/promises/Meta.js +42 -0
- package/source/server/promises.js +2 -0
- package/source/server/servers/Dynamic.js +51 -0
- package/source/server/servers/Server.js +5 -0
- package/source/server/servers/Static.js +113 -0
- package/source/server/servers/content-security-policy.json +8 -0
- package/source/server/servers/http-codes.json +5 -0
- package/source/server/servers/mimes.json +12 -0
- package/source/server/store/Memory.js +60 -0
- package/source/server/store/Store.js +17 -0
- package/source/server/types/Array.js +33 -0
- package/source/server/types/Boolean.js +29 -0
- package/source/server/types/Date.js +20 -0
- package/source/server/types/Domain.js +16 -0
- package/source/server/types/File.js +20 -0
- package/source/server/types/Instance.js +8 -0
- package/source/server/types/Number.js +45 -0
- package/source/server/types/Object.js +12 -0
- package/source/server/types/Primitive.js +7 -0
- package/source/server/types/Storeable.js +51 -0
- package/source/server/types/String.js +49 -0
- package/source/server/types/errors/Array.json +7 -0
- package/source/server/types/errors/Boolean.json +5 -0
- package/source/server/types/errors/Date.json +3 -0
- package/source/server/types/errors/Number.json +9 -0
- package/source/server/types/errors/Object.json +3 -0
- package/source/server/types/errors/String.json +11 -0
- package/source/server/types.js +7 -0
- package/source/server/utils/extend_object.js +10 -0
- package/source/server/view/Parser.js +122 -0
- package/source/server/view/TreeNode.js +195 -0
- package/source/server/view/View.js +30 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
Copyright (c) 2022, Primate core team <core@primatejs.com>. All rights
|
|
2
|
+
reserved.
|
|
3
|
+
|
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
|
6
|
+
|
|
7
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
8
|
+
list of conditions and the following disclaimer.
|
|
9
|
+
|
|
10
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
11
|
+
this list of conditions and the following disclaimer in the documentation
|
|
12
|
+
and/or other materials provided with the distribution.
|
|
13
|
+
|
|
14
|
+
3. Neither the name of the copyright holder nor the names of its contributors
|
|
15
|
+
may be used to endorse or promote products derived from this software without
|
|
16
|
+
specific prior written permission.
|
|
17
|
+
|
|
18
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
19
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
20
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
21
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
22
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
23
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
24
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
25
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
26
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
27
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Primate: a JavaScript framework
|
|
2
|
+
|
|
3
|
+
⚠️ **This software is currently its initial development phase. It is not
|
|
4
|
+
considered production-ready. Expect breakage.** ⚠️
|
|
5
|
+
|
|
6
|
+
Primate is a server-client JavaScript framework for web applications aimed at
|
|
7
|
+
relieving you of dealing with repetitive, error-prone tasks and letting you
|
|
8
|
+
concentrate on writing effective, expressive code.
|
|
9
|
+
|
|
10
|
+
## Installing
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
yarn add primate
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Concepts
|
|
17
|
+
|
|
18
|
+
* **Minimal** just one dependency ([ws][])
|
|
19
|
+
* **Simple** only native web technologies (JavaScript, HTML)
|
|
20
|
+
* **Full-stack** server and client, both in JavaScript
|
|
21
|
+
* **Layered** separation of data (domains), logic (actions) and
|
|
22
|
+
presentation (views)
|
|
23
|
+
* **Correct** data verification using field definitions in domains
|
|
24
|
+
* **Secure** serving hash-verified scripts on secure HTTP with a strong CSP
|
|
25
|
+
* **Roles** access control with contexts
|
|
26
|
+
* **Sessions** using cookies that are `HttpOnly`, `Secure` and `Path`-limited
|
|
27
|
+
* **Sane defaults** minimally opinionated with good, overrideable defaults
|
|
28
|
+
for most features
|
|
29
|
+
* **Data linking** modeling `1:1`, `1:n` and `n:m` relationships on domains via
|
|
30
|
+
ad-hoc getters
|
|
31
|
+
|
|
32
|
+
## Getting started
|
|
33
|
+
|
|
34
|
+
Coming soon.
|
|
35
|
+
|
|
36
|
+
## Resources
|
|
37
|
+
|
|
38
|
+
Coming soon.
|
|
39
|
+
|
|
40
|
+
## Versioning
|
|
41
|
+
|
|
42
|
+
This software is still in its initial development phase and is not considered
|
|
43
|
+
stable or recommended for production. Once we release a stable version `1`, we
|
|
44
|
+
will move over to semantic versioning.
|
|
45
|
+
|
|
46
|
+
## License
|
|
47
|
+
|
|
48
|
+
BSD-3-Clause
|
|
49
|
+
|
|
50
|
+
[ws]: https://github.com/websockets/ws
|
package/package.json
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "primate",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"author": "Primate core team <core@primatejs.com>",
|
|
5
5
|
"homepage": "https://primatejs.com",
|
|
6
|
+
"description": "Server-client framework",
|
|
6
7
|
"license": "BSD-3-Clause",
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"ws": "^8.4.0"
|
|
10
|
+
},
|
|
11
|
+
"scripts": {
|
|
12
|
+
"test": "node --experimental-json-modules node_modules/stick stick.json"
|
|
13
|
+
},
|
|
14
|
+
"devDependencies": {
|
|
15
|
+
"eslint": "^8.6.0",
|
|
16
|
+
"eslint-plugin-json": "^3.1.0",
|
|
17
|
+
"stick": "../stick"
|
|
18
|
+
},
|
|
19
|
+
"type": "module",
|
|
20
|
+
"exports": "./source/server/exports.js",
|
|
7
21
|
"engines": {
|
|
8
22
|
"node": ">=17.3.0"
|
|
9
23
|
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import Element from "../Element.js";
|
|
2
|
+
import View from "./View.js";
|
|
3
|
+
import Base from "./Base.js";
|
|
4
|
+
import {origin_base} from "./document.js";
|
|
5
|
+
|
|
6
|
+
export default class Action extends Base {
|
|
7
|
+
constructor(name, context) {
|
|
8
|
+
super();
|
|
9
|
+
this.name = name;
|
|
10
|
+
this.context = context;
|
|
11
|
+
this.listeners = [];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
before() {}
|
|
15
|
+
|
|
16
|
+
async enter(data) {
|
|
17
|
+
if (data.location) {
|
|
18
|
+
location.href = data.location;
|
|
19
|
+
} else {
|
|
20
|
+
this.create_view(data);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
create_view(data, root, save_history = true) {
|
|
25
|
+
if (save_history) {
|
|
26
|
+
const pathname = data.pathname === "" ? "/" : data.pathname;
|
|
27
|
+
window.location.pathname !== pathname
|
|
28
|
+
&& history.pushState({}, "", pathname);
|
|
29
|
+
}
|
|
30
|
+
this.view = new View(data, this, root);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
create_and_return_view(data, root, save_history) {
|
|
34
|
+
return new View(data, this, root);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get_form_data(target) {
|
|
38
|
+
const payload = {};
|
|
39
|
+
const elements = target.elements;
|
|
40
|
+
for (let i = 0; i < elements.length; i++) {
|
|
41
|
+
const element = elements[i];
|
|
42
|
+
const type = Element.value(element, "type");
|
|
43
|
+
const data_value = Element.value(element, "data-value");
|
|
44
|
+
switch (element.localName) {
|
|
45
|
+
case "input":
|
|
46
|
+
case "textarea":
|
|
47
|
+
if (type === "checkbox") {
|
|
48
|
+
payload[data_value] = element.checked;
|
|
49
|
+
} else if (type === "file") {
|
|
50
|
+
payload[data_value] = element.getAttribute("base64");
|
|
51
|
+
} else if (type === "submit") {
|
|
52
|
+
// do nothing
|
|
53
|
+
} else {
|
|
54
|
+
payload[data_value] = element.value;
|
|
55
|
+
}
|
|
56
|
+
break;
|
|
57
|
+
case "select":
|
|
58
|
+
payload[data_value] = Element
|
|
59
|
+
.value(element.children[element.selectedIndex], "value");
|
|
60
|
+
break;
|
|
61
|
+
default:
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return payload;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async onsubmit(event) {
|
|
69
|
+
const target = event.target;
|
|
70
|
+
const stop = this.fire("submit", event);
|
|
71
|
+
if (stop) {
|
|
72
|
+
return event.preventDefault();
|
|
73
|
+
}
|
|
74
|
+
event.preventDefault();
|
|
75
|
+
const data = await this.write(target.action, this.get_form_data(target));
|
|
76
|
+
return data.pathname === document.location.href.replace(origin_base, "")
|
|
77
|
+
? this.view.update(data.payload)
|
|
78
|
+
: this.context.run(data);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async onclick(event) {
|
|
82
|
+
const stop = await this.fire("click", event);
|
|
83
|
+
if (stop) {
|
|
84
|
+
return event.preventDefault();
|
|
85
|
+
}
|
|
86
|
+
let target;
|
|
87
|
+
if (event.target.shadowRoot !== null &&
|
|
88
|
+
event.target.shadowRoot.activeElement !== null) {
|
|
89
|
+
target = event.target.shadowRoot.activeElement.closest("a");
|
|
90
|
+
} else {
|
|
91
|
+
target = event.target.closest("a");
|
|
92
|
+
}
|
|
93
|
+
if (target !== null) {
|
|
94
|
+
const href = Element.value(target, "href");
|
|
95
|
+
const url = new URL(href, origin_base);
|
|
96
|
+
if (event.button === 0 && url.href.startsWith(origin_base)) {
|
|
97
|
+
event.preventDefault();
|
|
98
|
+
if (href !== "") {
|
|
99
|
+
const data = await this.read(href);
|
|
100
|
+
await this.context.run(data);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
onchange() {
|
|
107
|
+
const stop = this.fire("change", event);
|
|
108
|
+
if (stop) {
|
|
109
|
+
event.preventDefault();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
oninput() {
|
|
114
|
+
const stop = this.fire("input", event);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
fire(type, event) {
|
|
118
|
+
const listeners = this.listeners[type];
|
|
119
|
+
if (listeners !== undefined) {
|
|
120
|
+
const target = event.target;
|
|
121
|
+
for (const listener of listeners) {
|
|
122
|
+
if (event.target.closest(listener.selector)) {
|
|
123
|
+
listener.listener(event, target);
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
on(selector, event, listener) {
|
|
133
|
+
this.listeners[event] = this.listeners[event] ?? [];
|
|
134
|
+
const index = this.listeners[event]
|
|
135
|
+
.findIndex(listener => listener.selector === selector);
|
|
136
|
+
const object = {selector, listener};
|
|
137
|
+
if (index === -1) {
|
|
138
|
+
this.listeners[event].push(object);
|
|
139
|
+
} else {
|
|
140
|
+
this.listeners[event].splice(index, 1, object);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
_(selector) {
|
|
145
|
+
return new Element(selector, this);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
get selector() {
|
|
149
|
+
return selector => new Element(selector, this);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
read(pathname) {
|
|
153
|
+
return this.context.read(pathname);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
write(pathname, payload) {
|
|
157
|
+
return this.context.write(pathname, payload);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import Client from "./Client.js";
|
|
2
|
+
|
|
3
|
+
export default class App {
|
|
4
|
+
constructor() {
|
|
5
|
+
this.definitions = {};
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
run() {
|
|
9
|
+
this.client = new Client();
|
|
10
|
+
this.client.open();
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
define(name, predicate) {
|
|
14
|
+
this.definitions[name] = predicate;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import Context from "./Context.js";
|
|
2
|
+
import {contexts} from "./exports.js";
|
|
3
|
+
import {base, host, origin_base} from "./document.js";
|
|
4
|
+
|
|
5
|
+
const events = ["submit", "click", "change", "input"];
|
|
6
|
+
let back = undefined;
|
|
7
|
+
const location = `wss://${host}${base}`;
|
|
8
|
+
|
|
9
|
+
export default class Client {
|
|
10
|
+
constructor(conf) {
|
|
11
|
+
this.conf = conf;
|
|
12
|
+
|
|
13
|
+
window.onpopstate = async event => {
|
|
14
|
+
const {pathname, search} = event.target.location;
|
|
15
|
+
return this.execute(await this.read(pathname + search));
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
for (const key of events) {
|
|
19
|
+
window.addEventListener(key, event => this.context.on(key, event));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
open() {
|
|
24
|
+
return this.connection?.readyState === 1 ? this
|
|
25
|
+
: new Promise(resolve => {
|
|
26
|
+
const connection = new WebSocket(location);
|
|
27
|
+
connection.addEventListener("message", ({data}) => data === "open"
|
|
28
|
+
? resolve(this)
|
|
29
|
+
: this.receive(JSON.parse(data)));
|
|
30
|
+
this.connection = connection;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async receive(data) {
|
|
35
|
+
if (back !== undefined) {
|
|
36
|
+
back(data);
|
|
37
|
+
back = undefined;
|
|
38
|
+
} else {
|
|
39
|
+
await this.execute(data);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async execute(data) {
|
|
44
|
+
const {context} = data;
|
|
45
|
+
this.context = new (contexts[context] ?? Context)(context, this);
|
|
46
|
+
await this.context.run(data);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
read(url) {
|
|
50
|
+
return this.send("read", url);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
write(url, payload) {
|
|
54
|
+
return this.send("write", url, payload);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async send(type, url, payload = {}) {
|
|
58
|
+
await this.open();
|
|
59
|
+
const {pathname, search} = new URL(url, origin_base);
|
|
60
|
+
return new Promise(resolve => {
|
|
61
|
+
back = data => resolve(data);
|
|
62
|
+
this.connection.send(JSON.stringify({type, pathname, search, payload}));
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import Base from "./Base.js";
|
|
2
|
+
import Action from "./Action.js";
|
|
3
|
+
import {routes} from "./exports.js";
|
|
4
|
+
|
|
5
|
+
export default class Context extends Base {
|
|
6
|
+
constructor(name, client) {
|
|
7
|
+
super();
|
|
8
|
+
this.name = name;
|
|
9
|
+
this.actions = {};
|
|
10
|
+
this.used_actions = [];
|
|
11
|
+
this.client = client;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async run(data) {
|
|
15
|
+
const {namespace, action} = data;
|
|
16
|
+
const route = `${namespace}/${action}`;
|
|
17
|
+
const module = routes?.[namespace]?.[action] ?? Action;
|
|
18
|
+
this.actions[route] = new module(action, this);
|
|
19
|
+
const nextaction = this.actions[route];
|
|
20
|
+
nextaction.enter(data);
|
|
21
|
+
await this.Class().before(nextaction);
|
|
22
|
+
nextaction.before();
|
|
23
|
+
this.action = nextaction;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static before(action) {
|
|
27
|
+
return action;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static prepare(action, data) {
|
|
31
|
+
return data;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
read(pathname) {
|
|
35
|
+
return this.client.read(pathname);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
write(pathname, payload) {
|
|
39
|
+
return this.client.write(pathname, payload);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
on(key, event) {
|
|
43
|
+
this.action[`on${key}`](event);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
push_action() {
|
|
47
|
+
this.used_actions.push(this.action);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
pop_action() {
|
|
51
|
+
this.action = this.used_actions.pop();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
const file_listener = event => reader.readAsDataURL(event.target.files[0]);
|
|
2
|
+
const reader = new FileReader();
|
|
3
|
+
|
|
4
|
+
const replace = (attribute, source) => {
|
|
5
|
+
if (attribute.includes(".")) {
|
|
6
|
+
const index = attribute.indexOf(".");
|
|
7
|
+
const left = attribute.slice(0, index);
|
|
8
|
+
const rest = attribute.slice(index+1);
|
|
9
|
+
if (source[left] !== undefined) {
|
|
10
|
+
return replace(rest, source[left]);
|
|
11
|
+
}
|
|
12
|
+
} else {
|
|
13
|
+
return source[attribute];
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const data_prefix_length = 5;
|
|
18
|
+
const data_regex = /\${([^}]*)}/g;
|
|
19
|
+
const fulfill = (attribute, source) => {
|
|
20
|
+
if (source === undefined) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
let value = attribute.value;
|
|
24
|
+
const matches = [...value.matchAll(data_regex)];
|
|
25
|
+
if (matches.length > 0) {
|
|
26
|
+
for (const match of matches) {
|
|
27
|
+
const key = match[0];
|
|
28
|
+
const new_value = replace(match[1], source);
|
|
29
|
+
value = value.replace(key, new_value);
|
|
30
|
+
}
|
|
31
|
+
} else {
|
|
32
|
+
value = replace(value, source);
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
const $selector = Symbol("$selector");
|
|
39
|
+
const $element = Symbol("$element");
|
|
40
|
+
const $action = Symbol("$action");
|
|
41
|
+
|
|
42
|
+
const proxy_handler = {
|
|
43
|
+
"get": (target, property, receiver) => {
|
|
44
|
+
if (property === "element") {
|
|
45
|
+
return target.element;
|
|
46
|
+
}
|
|
47
|
+
if (property === "on" || property === "insert") {
|
|
48
|
+
return target[property].bind(target);
|
|
49
|
+
}
|
|
50
|
+
if (target.element !== null) {
|
|
51
|
+
return (...args) => target[property] !== undefined
|
|
52
|
+
? target[property](...args)
|
|
53
|
+
: receiver;
|
|
54
|
+
} else {
|
|
55
|
+
return () => receiver;
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export default class Element {
|
|
61
|
+
constructor(selector, action) {
|
|
62
|
+
this[$action] = action;
|
|
63
|
+
if (typeof selector === "string") {
|
|
64
|
+
this.construct_by_selector(selector);
|
|
65
|
+
} else {
|
|
66
|
+
this.construct_by_element(selector);
|
|
67
|
+
}
|
|
68
|
+
return new Proxy(this, proxy_handler);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
construct_by_selector(selector) {
|
|
72
|
+
this[$selector] = selector;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
construct_by_element(element) {
|
|
76
|
+
this[$element] = element;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
static value(element, name) {
|
|
80
|
+
const attribute = element.attributes.getNamedItem(name);
|
|
81
|
+
return attribute === null ? null : attribute.value;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
static evaluate(attribute, source, data) {
|
|
85
|
+
const element = new this(source);
|
|
86
|
+
const attribute_name = attribute.name.slice(data_prefix_length);
|
|
87
|
+
element.resolve(attribute_name)(fulfill(attribute, data), attribute_name);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
resolve(name) {
|
|
91
|
+
return (this[name] ?? this.default).bind(this);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
get element() {
|
|
95
|
+
return this[$element] ?? document.querySelector(this[$selector]);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
each(callback) {
|
|
99
|
+
document.querySelectorAll(this[$selector]).forEach(callback);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
get(name) {
|
|
103
|
+
return this.element.getAttribute(name);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
has(attribute) {
|
|
107
|
+
return this.element[attribute] !== undefined;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
show() {
|
|
111
|
+
return this.class("show");
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
hide() {
|
|
115
|
+
return this.class("show", false);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
insert() {
|
|
119
|
+
const element = document.createElement("div");
|
|
120
|
+
if (this[$selector].startsWith(".")) {
|
|
121
|
+
element.className = this[$selector].slice(1);
|
|
122
|
+
}
|
|
123
|
+
document.body.append(element);
|
|
124
|
+
return this;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
replace(child) {
|
|
128
|
+
this.element.innerHTML = "";
|
|
129
|
+
return this.append(child);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
append(child) {
|
|
133
|
+
if (child instanceof HTMLElement) {
|
|
134
|
+
this.element.appendChild(child);
|
|
135
|
+
} else {
|
|
136
|
+
this.element.innerHTML += child;
|
|
137
|
+
}
|
|
138
|
+
return this;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
set(name, value) {
|
|
142
|
+
this.element.setAttribute(name, value);
|
|
143
|
+
return this;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
class(classes, set) {
|
|
147
|
+
classes.split(" ").forEach(name => {
|
|
148
|
+
this.element.classList.toggle(name, set !== false);
|
|
149
|
+
})
|
|
150
|
+
return this;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
remove() {
|
|
154
|
+
this.element.remove();
|
|
155
|
+
return this;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
remove_attribute(name) {
|
|
159
|
+
this.element.removeAttribute(name);
|
|
160
|
+
return this;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
click() {
|
|
164
|
+
this.element.click();
|
|
165
|
+
return this;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
on(event, handler) {
|
|
169
|
+
this[$action].on(this[$selector], event, handler);
|
|
170
|
+
return this;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
defined(value) {
|
|
174
|
+
return value === undefined && this.remove();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
html(value) {
|
|
178
|
+
if (value !== undefined) {
|
|
179
|
+
this.element.innerHTML = value;
|
|
180
|
+
}
|
|
181
|
+
return this;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
options(value) {
|
|
185
|
+
value instanceof Array && value.forEach(option => {
|
|
186
|
+
if (this.element.children[option._id] === undefined) {
|
|
187
|
+
const newElement = document.createElement("option");
|
|
188
|
+
newElement.setAttribute("value", option._id);
|
|
189
|
+
newElement.innerHTML = option.name;
|
|
190
|
+
this.append(newElement);
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if(value) {
|
|
196
|
+
return value !== true && this.remove();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
value(value) {
|
|
200
|
+
const name = `${this.element.localName}_value`;
|
|
201
|
+
this[name]?.(value) ?? this.default_value(value);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
select_value(value) {
|
|
205
|
+
this.element.value = value === undefined ? "" : value;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
input_value(value) {
|
|
209
|
+
const type = Element.value(this.element, "type");
|
|
210
|
+
this[`input_${type}_value`]?.(value) ?? this.input_input_value(value);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
textarea_value(value) {
|
|
214
|
+
return this.input_value(value);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
input_checkbox_value(value) {
|
|
218
|
+
if (value === true) {
|
|
219
|
+
this.element.checked = true;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
input_file_value(value) {
|
|
224
|
+
const element = this.element;
|
|
225
|
+
if (value !== undefined) {
|
|
226
|
+
element.setAttribute("base64", value);
|
|
227
|
+
}
|
|
228
|
+
reader.onload = ({target}) => element.setAttribute("base64", target.result);
|
|
229
|
+
element.addEventListener("change", file_listener);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
input_input_value(value) {
|
|
233
|
+
if (value !== undefined) {
|
|
234
|
+
this.element.value = value;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
default_value(value) {
|
|
239
|
+
this.element.innerText = value ?? "";
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
default(value, attribute) {
|
|
243
|
+
value !== undefined && this.element.setAttribute(attribute, value);
|
|
244
|
+
}
|
|
245
|
+
}
|