@erickxavier/no-js 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +159 -0
- package/dist/cjs/no.js +25 -0
- package/dist/cjs/no.js.map +7 -0
- package/dist/esm/no.js +25 -0
- package/dist/esm/no.js.map +7 -0
- package/dist/iife/no.js +25 -0
- package/dist/iife/no.js.map +7 -0
- package/package.json +64 -0
- package/src/animations.js +113 -0
- package/src/cdn.js +16 -0
- package/src/context.js +104 -0
- package/src/directives/binding.js +118 -0
- package/src/directives/conditionals.js +283 -0
- package/src/directives/events.js +153 -0
- package/src/directives/http.js +288 -0
- package/src/directives/i18n.js +29 -0
- package/src/directives/loops.js +235 -0
- package/src/directives/refs.js +144 -0
- package/src/directives/state.js +102 -0
- package/src/directives/styling.js +88 -0
- package/src/directives/validation.js +216 -0
- package/src/dom.js +232 -0
- package/src/evaluate.js +298 -0
- package/src/fetch.js +173 -0
- package/src/filters.js +137 -0
- package/src/globals.js +58 -0
- package/src/i18n.js +36 -0
- package/src/index.js +189 -0
- package/src/registry.js +60 -0
- package/src/router.js +253 -0
package/src/filters.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2
|
+
// BUILT-IN FILTERS
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
4
|
+
|
|
5
|
+
import { _filters } from "./globals.js";
|
|
6
|
+
|
|
7
|
+
// Text
|
|
8
|
+
_filters.uppercase = (v) => String(v ?? "").toUpperCase();
|
|
9
|
+
_filters.lowercase = (v) => String(v ?? "").toLowerCase();
|
|
10
|
+
_filters.capitalize = (v) =>
|
|
11
|
+
String(v ?? "").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
12
|
+
_filters.truncate = (v, len = 100) => {
|
|
13
|
+
const s = String(v ?? "");
|
|
14
|
+
return s.length > len ? s.slice(0, len) + "..." : s;
|
|
15
|
+
};
|
|
16
|
+
_filters.trim = (v) => String(v ?? "").trim();
|
|
17
|
+
_filters.stripHtml = (v) => String(v ?? "").replace(/<[^>]*>/g, "");
|
|
18
|
+
_filters.slugify = (v) =>
|
|
19
|
+
String(v ?? "")
|
|
20
|
+
.toLowerCase()
|
|
21
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
22
|
+
.replace(/^-|-$/g, "");
|
|
23
|
+
_filters.nl2br = (v) => String(v ?? "").replace(/\n/g, "<br>");
|
|
24
|
+
_filters.encodeUri = (v) => encodeURIComponent(String(v ?? ""));
|
|
25
|
+
|
|
26
|
+
// Numbers
|
|
27
|
+
_filters.number = (v, decimals = 0) => {
|
|
28
|
+
const n = Number(v);
|
|
29
|
+
if (isNaN(n)) return v;
|
|
30
|
+
return n.toLocaleString(undefined, {
|
|
31
|
+
minimumFractionDigits: decimals,
|
|
32
|
+
maximumFractionDigits: decimals,
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
_filters.currency = (v, code = "USD") => {
|
|
36
|
+
const n = Number(v);
|
|
37
|
+
if (isNaN(n)) return v;
|
|
38
|
+
try {
|
|
39
|
+
return n.toLocaleString(undefined, { style: "currency", currency: code });
|
|
40
|
+
} catch {
|
|
41
|
+
return `${code} ${n.toFixed(2)}`;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
_filters.percent = (v, decimals = 0) => {
|
|
45
|
+
const n = Number(v);
|
|
46
|
+
if (isNaN(n)) return v;
|
|
47
|
+
return (n * 100).toFixed(decimals) + "%";
|
|
48
|
+
};
|
|
49
|
+
_filters.filesize = (v) => {
|
|
50
|
+
const n = Number(v);
|
|
51
|
+
if (isNaN(n)) return v;
|
|
52
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
53
|
+
let i = 0;
|
|
54
|
+
let size = n;
|
|
55
|
+
while (size >= 1024 && i < units.length - 1) {
|
|
56
|
+
size /= 1024;
|
|
57
|
+
i++;
|
|
58
|
+
}
|
|
59
|
+
return size.toFixed(i > 0 ? 1 : 0) + " " + units[i];
|
|
60
|
+
};
|
|
61
|
+
_filters.ordinal = (v) => {
|
|
62
|
+
const n = Number(v);
|
|
63
|
+
if (isNaN(n)) return v;
|
|
64
|
+
const s = ["th", "st", "nd", "rd"];
|
|
65
|
+
const mod = n % 100;
|
|
66
|
+
return n + (s[(mod - 20) % 10] || s[mod] || s[0]);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Arrays
|
|
70
|
+
_filters.count = (v) => (Array.isArray(v) ? v.length : 0);
|
|
71
|
+
_filters.first = (v) => (Array.isArray(v) ? v[0] : v);
|
|
72
|
+
_filters.last = (v) => (Array.isArray(v) ? v[v.length - 1] : v);
|
|
73
|
+
_filters.join = (v, sep = ", ") => (Array.isArray(v) ? v.join(sep) : v);
|
|
74
|
+
_filters.reverse = (v) => (Array.isArray(v) ? [...v].reverse() : v);
|
|
75
|
+
_filters.unique = (v) => (Array.isArray(v) ? [...new Set(v)] : v);
|
|
76
|
+
_filters.pluck = (v, key) => (Array.isArray(v) ? v.map((i) => i?.[key]) : v);
|
|
77
|
+
_filters.sortBy = (v, key) => {
|
|
78
|
+
if (!Array.isArray(v)) return v;
|
|
79
|
+
const desc = key?.startsWith("-");
|
|
80
|
+
const k = desc ? key.slice(1) : key;
|
|
81
|
+
return [...v].sort((a, b) => {
|
|
82
|
+
const va = a?.[k],
|
|
83
|
+
vb = b?.[k];
|
|
84
|
+
const r = va < vb ? -1 : va > vb ? 1 : 0;
|
|
85
|
+
return desc ? -r : r;
|
|
86
|
+
});
|
|
87
|
+
};
|
|
88
|
+
_filters.where = (v, key, val) =>
|
|
89
|
+
Array.isArray(v) ? v.filter((i) => i?.[key] === val) : v;
|
|
90
|
+
|
|
91
|
+
// Date
|
|
92
|
+
_filters.date = (v, fmt = "short") => {
|
|
93
|
+
const d = new Date(v);
|
|
94
|
+
if (isNaN(d)) return v;
|
|
95
|
+
const opts =
|
|
96
|
+
fmt === "long"
|
|
97
|
+
? { dateStyle: "long" }
|
|
98
|
+
: fmt === "full"
|
|
99
|
+
? { dateStyle: "full" }
|
|
100
|
+
: { dateStyle: "short" };
|
|
101
|
+
return d.toLocaleDateString(undefined, opts);
|
|
102
|
+
};
|
|
103
|
+
_filters.datetime = (v) => {
|
|
104
|
+
const d = new Date(v);
|
|
105
|
+
if (isNaN(d)) return v;
|
|
106
|
+
return d.toLocaleString();
|
|
107
|
+
};
|
|
108
|
+
_filters.relative = (v) => {
|
|
109
|
+
const d = new Date(v);
|
|
110
|
+
if (isNaN(d)) return v;
|
|
111
|
+
const diff = (Date.now() - d.getTime()) / 1000;
|
|
112
|
+
if (diff < 60) return "just now";
|
|
113
|
+
if (diff < 3600) return Math.floor(diff / 60) + "m ago";
|
|
114
|
+
if (diff < 86400) return Math.floor(diff / 3600) + "h ago";
|
|
115
|
+
if (diff < 2592000) return Math.floor(diff / 86400) + "d ago";
|
|
116
|
+
return d.toLocaleDateString();
|
|
117
|
+
};
|
|
118
|
+
_filters.fromNow = (v) => {
|
|
119
|
+
const d = new Date(v);
|
|
120
|
+
if (isNaN(d)) return v;
|
|
121
|
+
const diff = (d.getTime() - Date.now()) / 1000;
|
|
122
|
+
if (diff < 0) return _filters.relative(v);
|
|
123
|
+
if (diff < 60) return "in a moment";
|
|
124
|
+
if (diff < 3600) return "in " + Math.floor(diff / 60) + "m";
|
|
125
|
+
if (diff < 86400) return "in " + Math.floor(diff / 3600) + "h";
|
|
126
|
+
return "in " + Math.floor(diff / 86400) + "d";
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Utility
|
|
130
|
+
_filters.default = (v, def = "") => (v == null || v === "" ? def : v);
|
|
131
|
+
_filters.json = (v, indent = 2) => JSON.stringify(v, null, indent);
|
|
132
|
+
_filters.debug = (v) => {
|
|
133
|
+
console.log("[No.JS debug]", v);
|
|
134
|
+
return v;
|
|
135
|
+
};
|
|
136
|
+
_filters.keys = (v) => (v && typeof v === "object" ? Object.keys(v) : []);
|
|
137
|
+
_filters.values = (v) => (v && typeof v === "object" ? Object.values(v) : []);
|
package/src/globals.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2
|
+
// SHARED STATE & UTILITIES
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
4
|
+
|
|
5
|
+
export const _config = {
|
|
6
|
+
baseApiUrl: "",
|
|
7
|
+
headers: {},
|
|
8
|
+
timeout: 10000,
|
|
9
|
+
retries: 0,
|
|
10
|
+
retryDelay: 1000,
|
|
11
|
+
credentials: "same-origin",
|
|
12
|
+
csrf: null,
|
|
13
|
+
cache: { strategy: "none", ttl: 300000 },
|
|
14
|
+
templates: { cache: true },
|
|
15
|
+
router: { mode: "history", base: "/", scrollBehavior: "top" },
|
|
16
|
+
i18n: { defaultLocale: "en", fallbackLocale: "en", detectBrowser: false },
|
|
17
|
+
debug: false,
|
|
18
|
+
devtools: false,
|
|
19
|
+
csp: null,
|
|
20
|
+
sanitize: true,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const _interceptors = { request: [], response: [] };
|
|
24
|
+
export const _eventBus = {};
|
|
25
|
+
export const _stores = {};
|
|
26
|
+
export const _storeWatchers = new Set();
|
|
27
|
+
export const _filters = {};
|
|
28
|
+
export const _validators = {};
|
|
29
|
+
export const _cache = new Map();
|
|
30
|
+
export const _refs = {};
|
|
31
|
+
export let _routerInstance = null;
|
|
32
|
+
|
|
33
|
+
export function setRouterInstance(r) {
|
|
34
|
+
_routerInstance = r;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function _log(...args) {
|
|
38
|
+
if (_config.debug) console.log("[No.JS]", ...args);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function _warn(...args) {
|
|
42
|
+
console.warn("[No.JS]", ...args);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function _notifyStoreWatchers() {
|
|
46
|
+
for (const fn of _storeWatchers) fn();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function _watchExpr(expr, ctx, fn) {
|
|
50
|
+
ctx.$watch(fn);
|
|
51
|
+
if (typeof expr === "string" && expr.includes("$store")) {
|
|
52
|
+
_storeWatchers.add(fn);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function _emitEvent(name, data) {
|
|
57
|
+
(_eventBus[name] || []).forEach((fn) => fn(data));
|
|
58
|
+
}
|
package/src/i18n.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2
|
+
// i18n SYSTEM
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
4
|
+
|
|
5
|
+
import { _config } from "./globals.js";
|
|
6
|
+
|
|
7
|
+
export const _i18n = {
|
|
8
|
+
locale: "en",
|
|
9
|
+
locales: {},
|
|
10
|
+
t(key, params = {}) {
|
|
11
|
+
const messages =
|
|
12
|
+
_i18n.locales[_i18n.locale] ||
|
|
13
|
+
_i18n.locales[_config.i18n.fallbackLocale] ||
|
|
14
|
+
{};
|
|
15
|
+
let msg = key.split(".").reduce((o, k) => o?.[k], messages);
|
|
16
|
+
if (msg == null) return key;
|
|
17
|
+
|
|
18
|
+
// Pluralization: "one item | {count} items"
|
|
19
|
+
if (
|
|
20
|
+
typeof msg === "string" &&
|
|
21
|
+
msg.includes("|") &&
|
|
22
|
+
params.count != null
|
|
23
|
+
) {
|
|
24
|
+
const forms = msg.split("|").map((s) => s.trim());
|
|
25
|
+
msg = Number(params.count) === 1 ? forms[0] : forms[1] || forms[0];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Interpolation: {name}
|
|
29
|
+
if (typeof msg === "string") {
|
|
30
|
+
msg = msg.replace(/\{(\w+)\}/g, (_, k) =>
|
|
31
|
+
params[k] != null ? params[k] : "",
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
return msg;
|
|
35
|
+
},
|
|
36
|
+
};
|
package/src/index.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2
|
+
// No.JS — Module Entry Point
|
|
3
|
+
// For npm/ESM/CJS consumers: import NoJS from 'nojs'
|
|
4
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
5
|
+
|
|
6
|
+
// Core modules
|
|
7
|
+
import {
|
|
8
|
+
_config,
|
|
9
|
+
_filters,
|
|
10
|
+
_validators,
|
|
11
|
+
_interceptors,
|
|
12
|
+
_eventBus,
|
|
13
|
+
_stores,
|
|
14
|
+
_refs,
|
|
15
|
+
_routerInstance,
|
|
16
|
+
setRouterInstance,
|
|
17
|
+
_log,
|
|
18
|
+
} from "./globals.js";
|
|
19
|
+
import { _i18n } from "./i18n.js";
|
|
20
|
+
import { createContext } from "./context.js";
|
|
21
|
+
import { evaluate, resolve } from "./evaluate.js";
|
|
22
|
+
import { findContext, _loadRemoteTemplates, _loadRemoteTemplatesPhase1, _loadRemoteTemplatesPhase2, _processTemplateIncludes } from "./dom.js";
|
|
23
|
+
import { registerDirective, processTree } from "./registry.js";
|
|
24
|
+
import { _createRouter } from "./router.js";
|
|
25
|
+
|
|
26
|
+
// Side-effect imports: register built-in filters
|
|
27
|
+
import "./filters.js";
|
|
28
|
+
|
|
29
|
+
// Side-effect imports: register all built-in directives
|
|
30
|
+
import "./directives/state.js";
|
|
31
|
+
import "./directives/http.js";
|
|
32
|
+
import "./directives/binding.js";
|
|
33
|
+
import "./directives/conditionals.js";
|
|
34
|
+
import "./directives/loops.js";
|
|
35
|
+
import "./directives/styling.js";
|
|
36
|
+
import "./directives/events.js";
|
|
37
|
+
import "./directives/refs.js";
|
|
38
|
+
import "./directives/validation.js";
|
|
39
|
+
import "./directives/i18n.js";
|
|
40
|
+
|
|
41
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
42
|
+
// PUBLIC API
|
|
43
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
44
|
+
|
|
45
|
+
function _getDefaultRoutePath() {
|
|
46
|
+
if (typeof window === "undefined") return null;
|
|
47
|
+
const routerCfg = _config.router || {};
|
|
48
|
+
if (routerCfg.mode === "hash") {
|
|
49
|
+
return window.location.hash.slice(1) || "/";
|
|
50
|
+
}
|
|
51
|
+
const base = (routerCfg.base || "/").replace(/\/$/, "");
|
|
52
|
+
return window.location.pathname.replace(base, "") || "/";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const NoJS = {
|
|
56
|
+
get baseApiUrl() {
|
|
57
|
+
return _config.baseApiUrl;
|
|
58
|
+
},
|
|
59
|
+
set baseApiUrl(v) {
|
|
60
|
+
_config.baseApiUrl = v;
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
config(opts = {}) {
|
|
64
|
+
// Save nested objects before shallow assign overwrites them
|
|
65
|
+
const prevHeaders = { ..._config.headers };
|
|
66
|
+
const prevCache = { ..._config.cache };
|
|
67
|
+
const prevTemplates = { ..._config.templates };
|
|
68
|
+
const prevRouter = { ..._config.router };
|
|
69
|
+
const prevI18n = { ..._config.i18n };
|
|
70
|
+
Object.assign(_config, opts);
|
|
71
|
+
if (opts.headers)
|
|
72
|
+
_config.headers = { ...prevHeaders, ...opts.headers };
|
|
73
|
+
if (opts.csrf) _config.csrf = opts.csrf;
|
|
74
|
+
if (opts.cache) _config.cache = { ...prevCache, ...opts.cache };
|
|
75
|
+
if (opts.templates) _config.templates = { ...prevTemplates, ...opts.templates };
|
|
76
|
+
if (opts.router) _config.router = { ...prevRouter, ...opts.router };
|
|
77
|
+
if (opts.i18n) {
|
|
78
|
+
_config.i18n = { ...prevI18n, ...opts.i18n };
|
|
79
|
+
_i18n.locale = opts.i18n.defaultLocale || _i18n.locale;
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
async init(root) {
|
|
84
|
+
if (typeof document === "undefined") return;
|
|
85
|
+
root = root || document.body;
|
|
86
|
+
_log("Initializing...");
|
|
87
|
+
|
|
88
|
+
// Inline template includes (e.g. skeletons) — synchronous, before any fetch
|
|
89
|
+
_processTemplateIncludes(root);
|
|
90
|
+
|
|
91
|
+
// Determine active route path for phase 1 prioritization
|
|
92
|
+
const defaultRoutePath = _getDefaultRoutePath();
|
|
93
|
+
|
|
94
|
+
// Phase 1 (blocking): priority + non-route + default route templates
|
|
95
|
+
await _loadRemoteTemplatesPhase1(defaultRoutePath);
|
|
96
|
+
|
|
97
|
+
// Check for route-view outlets to activate router
|
|
98
|
+
if (document.querySelector("[route-view]")) {
|
|
99
|
+
setRouterInstance(_createRouter());
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
processTree(root); // ← first paint happens here
|
|
103
|
+
|
|
104
|
+
// Init router after tree is processed
|
|
105
|
+
if (_routerInstance) await _routerInstance.init();
|
|
106
|
+
|
|
107
|
+
_log("Initialized.");
|
|
108
|
+
|
|
109
|
+
// Phase 2 (non-blocking): background preload remaining route templates
|
|
110
|
+
_loadRemoteTemplatesPhase2();
|
|
111
|
+
|
|
112
|
+
// DevTools integration
|
|
113
|
+
if (_config.devtools && typeof window !== "undefined") {
|
|
114
|
+
window.__NOJS_DEVTOOLS__ = {
|
|
115
|
+
stores: _stores,
|
|
116
|
+
config: _config,
|
|
117
|
+
refs: _refs,
|
|
118
|
+
router: _routerInstance,
|
|
119
|
+
filters: Object.keys(_filters),
|
|
120
|
+
validators: Object.keys(_validators),
|
|
121
|
+
version: NoJS.version,
|
|
122
|
+
};
|
|
123
|
+
_log("DevTools enabled — access via window.__NOJS_DEVTOOLS__");
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
|
|
127
|
+
// Register custom directive
|
|
128
|
+
directive(name, handler) {
|
|
129
|
+
registerDirective(name, handler);
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
// Register custom filter
|
|
133
|
+
filter(name, fn) {
|
|
134
|
+
_filters[name] = fn;
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
// Register custom validator
|
|
138
|
+
validator(name, fn) {
|
|
139
|
+
_validators[name] = fn;
|
|
140
|
+
},
|
|
141
|
+
|
|
142
|
+
// i18n
|
|
143
|
+
i18n(opts) {
|
|
144
|
+
if (opts.locales) _i18n.locales = opts.locales;
|
|
145
|
+
if (opts.defaultLocale) _i18n.locale = opts.defaultLocale;
|
|
146
|
+
if (opts.fallbackLocale) _config.i18n.fallbackLocale = opts.fallbackLocale;
|
|
147
|
+
if (opts.detectBrowser) {
|
|
148
|
+
const browserLang =
|
|
149
|
+
typeof navigator !== "undefined" ? navigator.language : "en";
|
|
150
|
+
if (_i18n.locales[browserLang]) _i18n.locale = browserLang;
|
|
151
|
+
}
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
// Event bus
|
|
155
|
+
on(event, fn) {
|
|
156
|
+
if (!_eventBus[event]) _eventBus[event] = [];
|
|
157
|
+
_eventBus[event].push(fn);
|
|
158
|
+
return () => {
|
|
159
|
+
_eventBus[event] = _eventBus[event].filter((f) => f !== fn);
|
|
160
|
+
};
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
// Request interceptors
|
|
164
|
+
interceptor(type, fn) {
|
|
165
|
+
if (_interceptors[type]) _interceptors[type].push(fn);
|
|
166
|
+
},
|
|
167
|
+
|
|
168
|
+
// Access global stores
|
|
169
|
+
get store() {
|
|
170
|
+
return _stores;
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
// Access router
|
|
174
|
+
get router() {
|
|
175
|
+
return _routerInstance;
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
// Utilities (for custom directives)
|
|
179
|
+
createContext,
|
|
180
|
+
evaluate,
|
|
181
|
+
findContext,
|
|
182
|
+
processTree,
|
|
183
|
+
resolve,
|
|
184
|
+
|
|
185
|
+
// Version
|
|
186
|
+
version: "1.0.0",
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
export default NoJS;
|
package/src/registry.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
2
|
+
// DIRECTIVE REGISTRY & DOM PROCESSING
|
|
3
|
+
// ═══════════════════════════════════════════════════════════════════════
|
|
4
|
+
|
|
5
|
+
const _directives = new Map();
|
|
6
|
+
|
|
7
|
+
export function registerDirective(name, handler) {
|
|
8
|
+
_directives.set(name, {
|
|
9
|
+
priority: handler.priority ?? 50,
|
|
10
|
+
init: handler.init,
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function _matchDirective(attrName) {
|
|
15
|
+
if (_directives.has(attrName))
|
|
16
|
+
return { directive: _directives.get(attrName), match: attrName };
|
|
17
|
+
// Pattern matches
|
|
18
|
+
const patterns = ["class-*", "on:*", "style-*", "bind-*"];
|
|
19
|
+
for (const p of patterns) {
|
|
20
|
+
const prefix = p.replace("*", "");
|
|
21
|
+
if (attrName.startsWith(prefix) && _directives.has(p)) {
|
|
22
|
+
return { directive: _directives.get(p), match: p };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function processElement(el) {
|
|
29
|
+
if (el.__declared) return;
|
|
30
|
+
el.__declared = true;
|
|
31
|
+
|
|
32
|
+
const matched = [];
|
|
33
|
+
for (const attr of [...el.attributes]) {
|
|
34
|
+
const m = _matchDirective(attr.name);
|
|
35
|
+
if (m) {
|
|
36
|
+
matched.push({
|
|
37
|
+
name: attr.name,
|
|
38
|
+
value: attr.value,
|
|
39
|
+
priority: m.directive.priority,
|
|
40
|
+
init: m.directive.init,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
matched.sort((a, b) => a.priority - b.priority);
|
|
46
|
+
for (const m of matched) {
|
|
47
|
+
m.init(el, m.name, m.value);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function processTree(root) {
|
|
52
|
+
if (!root) return;
|
|
53
|
+
if (root.nodeType === 1 && !root.__declared) processElement(root);
|
|
54
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT);
|
|
55
|
+
while (walker.nextNode()) {
|
|
56
|
+
const node = walker.currentNode;
|
|
57
|
+
if (node.tagName === "TEMPLATE" || node.tagName === "SCRIPT") continue;
|
|
58
|
+
if (!node.__declared) processElement(node);
|
|
59
|
+
}
|
|
60
|
+
}
|