@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.
@@ -0,0 +1,113 @@
1
+ // ═══════════════════════════════════════════════════════════════════════
2
+ // ANIMATIONS
3
+ // ═══════════════════════════════════════════════════════════════════════
4
+
5
+ let _stylesInjected = false;
6
+
7
+ function _injectBuiltInStyles() {
8
+ if (_stylesInjected || typeof document === "undefined") return;
9
+ _stylesInjected = true;
10
+
11
+ const css = `
12
+ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
13
+ @keyframes fadeOut { from { opacity: 1; } to { opacity: 0; } }
14
+ @keyframes fadeInUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
15
+ @keyframes fadeInDown { from { opacity: 0; transform: translateY(-20px); } to { opacity: 1; transform: translateY(0); } }
16
+ @keyframes fadeOutUp { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(-20px); } }
17
+ @keyframes fadeOutDown { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(20px); } }
18
+ @keyframes slideInLeft { from { transform: translateX(-100%); } to { transform: translateX(0); } }
19
+ @keyframes slideInRight { from { transform: translateX(100%); } to { transform: translateX(0); } }
20
+ @keyframes slideOutLeft { from { transform: translateX(0); } to { transform: translateX(-100%); } }
21
+ @keyframes slideOutRight { from { transform: translateX(0); } to { transform: translateX(100%); } }
22
+ @keyframes zoomIn { from { opacity: 0; transform: scale(0.5); } to { opacity: 1; transform: scale(1); } }
23
+ @keyframes zoomOut { from { opacity: 1; transform: scale(1); } to { opacity: 0; transform: scale(0.5); } }
24
+ @keyframes bounceIn { 0% { opacity: 0; transform: scale(0.3); } 50% { opacity: 1; transform: scale(1.05); } 70% { transform: scale(0.9); } 100% { opacity: 1; transform: scale(1); } }
25
+ @keyframes bounceOut { 0% { opacity: 1; transform: scale(1); } 20% { transform: scale(0.9); } 50%,55% { opacity: 1; transform: scale(1.1); } 100% { opacity: 0; transform: scale(0.3); } }
26
+ `.trim();
27
+
28
+ const style = document.createElement("style");
29
+ style.setAttribute("data-nojs-animations", "");
30
+ style.textContent = css;
31
+ document.head.appendChild(style);
32
+ }
33
+
34
+ export function _animateIn(el, animName, transitionName, durationMs) {
35
+ _injectBuiltInStyles();
36
+ const fallback = durationMs || 1000;
37
+ if (animName) {
38
+ const target = el.firstElementChild || el;
39
+ target.classList.add(animName);
40
+ if (durationMs) target.style.animationDuration = durationMs + "ms";
41
+ target.addEventListener(
42
+ "animationend",
43
+ () => target.classList.remove(animName),
44
+ { once: true },
45
+ );
46
+ }
47
+ if (transitionName) {
48
+ const target = el.firstElementChild || el;
49
+ target.classList.add(
50
+ transitionName + "-enter",
51
+ transitionName + "-enter-active",
52
+ );
53
+ requestAnimationFrame(() => {
54
+ target.classList.remove(transitionName + "-enter");
55
+ target.classList.add(transitionName + "-enter-to");
56
+ const done = () => {
57
+ target.classList.remove(
58
+ transitionName + "-enter-active",
59
+ transitionName + "-enter-to",
60
+ );
61
+ };
62
+ target.addEventListener("transitionend", done, { once: true });
63
+ // Fallback
64
+ setTimeout(done, fallback);
65
+ });
66
+ }
67
+ }
68
+
69
+ export function _animateOut(el, animName, transitionName, callback, durationMs) {
70
+ _injectBuiltInStyles();
71
+ const fallback = durationMs || 2000;
72
+ if (!el.firstElementChild && !el.childNodes.length) {
73
+ callback();
74
+ return;
75
+ }
76
+ if (animName) {
77
+ const target = el.firstElementChild || el;
78
+ target.classList.add(animName);
79
+ if (durationMs) target.style.animationDuration = durationMs + "ms";
80
+ target.addEventListener(
81
+ "animationend",
82
+ () => {
83
+ target.classList.remove(animName);
84
+ callback();
85
+ },
86
+ { once: true },
87
+ );
88
+ setTimeout(callback, fallback); // Fallback
89
+ return;
90
+ }
91
+ if (transitionName) {
92
+ const target = el.firstElementChild || el;
93
+ target.classList.add(
94
+ transitionName + "-leave",
95
+ transitionName + "-leave-active",
96
+ );
97
+ requestAnimationFrame(() => {
98
+ target.classList.remove(transitionName + "-leave");
99
+ target.classList.add(transitionName + "-leave-to");
100
+ const done = () => {
101
+ target.classList.remove(
102
+ transitionName + "-leave-active",
103
+ transitionName + "-leave-to",
104
+ );
105
+ callback();
106
+ };
107
+ target.addEventListener("transitionend", done, { once: true });
108
+ setTimeout(done, fallback);
109
+ });
110
+ return;
111
+ }
112
+ callback();
113
+ }
package/src/cdn.js ADDED
@@ -0,0 +1,16 @@
1
+ // ═══════════════════════════════════════════════════════════════════════
2
+ // No.JS — CDN Entry Point
3
+ // For <script> tag usage: auto-initializes, sets window.NoJS
4
+ // ═══════════════════════════════════════════════════════════════════════
5
+
6
+ import NoJS from "./index.js";
7
+
8
+ // Expose globally
9
+ window.NoJS = NoJS;
10
+
11
+ // Auto-init on DOM ready
12
+ if (document.readyState === "loading") {
13
+ document.addEventListener("DOMContentLoaded", () => NoJS.init());
14
+ } else {
15
+ NoJS.init();
16
+ }
package/src/context.js ADDED
@@ -0,0 +1,104 @@
1
+ // ═══════════════════════════════════════════════════════════════════════
2
+ // REACTIVE CONTEXT
3
+ // ═══════════════════════════════════════════════════════════════════════
4
+
5
+ import { _stores, _refs, _routerInstance } from "./globals.js";
6
+ import { _i18n } from "./i18n.js";
7
+
8
+ let _batchDepth = 0;
9
+ const _batchQueue = new Set();
10
+
11
+ export function _startBatch() {
12
+ _batchDepth++;
13
+ }
14
+
15
+ export function _endBatch() {
16
+ _batchDepth--;
17
+ if (_batchDepth === 0 && _batchQueue.size > 0) {
18
+ const fns = [..._batchQueue];
19
+ _batchQueue.clear();
20
+ fns.forEach((fn) => fn());
21
+ }
22
+ }
23
+
24
+ export function createContext(data = {}, parent = null) {
25
+ const listeners = new Set();
26
+ const raw = {};
27
+ Object.assign(raw, data);
28
+ let notifying = false;
29
+
30
+ function notify() {
31
+ if (notifying) return;
32
+ notifying = true;
33
+ try {
34
+ if (_batchDepth > 0) {
35
+ listeners.forEach((fn) => _batchQueue.add(fn));
36
+ } else {
37
+ listeners.forEach((fn) => fn());
38
+ }
39
+ } finally {
40
+ notifying = false;
41
+ }
42
+ }
43
+
44
+ const handler = {
45
+ get(target, key) {
46
+ if (key === "__isProxy") return true;
47
+ if (key === "__raw") return target;
48
+ if (key === "__listeners") return listeners;
49
+ if (key === "$watch")
50
+ return (fn) => {
51
+ listeners.add(fn);
52
+ return () => listeners.delete(fn);
53
+ };
54
+ if (key === "$notify") return notify;
55
+ if (key === "$set")
56
+ return (k, v) => {
57
+ proxy[k] = v;
58
+ };
59
+ if (key === "$parent") return parent;
60
+ if (key === "$refs") return _refs;
61
+ if (key === "$store") return _stores;
62
+ if (key === "$route")
63
+ return _routerInstance ? _routerInstance.current : {};
64
+ if (key === "$router") return _routerInstance;
65
+ if (key === "$i18n") return _i18n;
66
+ if (key === "$form") return target.$form || null;
67
+ if (key in target) return target[key];
68
+ if (parent && parent.__isProxy) return parent[key];
69
+ return undefined;
70
+ },
71
+ set(target, key, value) {
72
+ const old = target[key];
73
+ target[key] = value;
74
+ if (old !== value) notify();
75
+ return true;
76
+ },
77
+ has(target, key) {
78
+ if (key in target) return true;
79
+ if (parent && parent.__isProxy) return key in parent;
80
+ return false;
81
+ },
82
+ };
83
+
84
+ const proxy = new Proxy(raw, handler);
85
+ return proxy;
86
+ }
87
+
88
+ // Collect all keys from a context + its parent chain
89
+ export function _collectKeys(ctx) {
90
+ const allKeys = new Set();
91
+ const allVals = {};
92
+ let c = ctx;
93
+ while (c && c.__isProxy) {
94
+ const raw = c.__raw;
95
+ for (const k of Object.keys(raw)) {
96
+ if (!allKeys.has(k)) {
97
+ allKeys.add(k);
98
+ allVals[k] = raw[k];
99
+ }
100
+ }
101
+ c = c.$parent;
102
+ }
103
+ return { keys: [...allKeys], vals: allVals };
104
+ }
@@ -0,0 +1,118 @@
1
+ // ═══════════════════════════════════════════════════════════════════════
2
+ // DIRECTIVES: bind, bind-html, bind-*, model
3
+ // ═══════════════════════════════════════════════════════════════════════
4
+
5
+ import { _watchExpr } from "../globals.js";
6
+ import { evaluate, _execStatement } from "../evaluate.js";
7
+ import { findContext, _sanitizeHtml } from "../dom.js";
8
+ import { registerDirective } from "../registry.js";
9
+
10
+ registerDirective("bind", {
11
+ priority: 20,
12
+ init(el, name, expr) {
13
+ const ctx = findContext(el);
14
+ function update() {
15
+ const val = evaluate(expr, ctx);
16
+ if (val !== undefined && val !== null) el.textContent = String(val);
17
+ }
18
+ _watchExpr(expr, ctx, update);
19
+ update();
20
+ },
21
+ });
22
+
23
+ registerDirective("bind-html", {
24
+ priority: 20,
25
+ init(el, name, expr) {
26
+ const ctx = findContext(el);
27
+ function update() {
28
+ const val = evaluate(expr, ctx);
29
+ if (val != null) el.innerHTML = _sanitizeHtml(String(val));
30
+ }
31
+ _watchExpr(expr, ctx, update);
32
+ update();
33
+ },
34
+ });
35
+
36
+ registerDirective("bind-*", {
37
+ priority: 20,
38
+ init(el, name, expr) {
39
+ const attrName = name.replace("bind-", "");
40
+ const ctx = findContext(el);
41
+
42
+ // Two-way binding for value
43
+ if (
44
+ attrName === "value" &&
45
+ (el.tagName === "INPUT" ||
46
+ el.tagName === "TEXTAREA" ||
47
+ el.tagName === "SELECT")
48
+ ) {
49
+ el.addEventListener("input", () => {
50
+ const val = el.type === "number" ? Number(el.value) : el.value;
51
+ _execStatement(`${expr} = ${JSON.stringify(val)}`, ctx);
52
+ });
53
+ }
54
+
55
+ function update() {
56
+ const val = evaluate(expr, ctx);
57
+ // Boolean attributes
58
+ if (
59
+ [
60
+ "disabled",
61
+ "readonly",
62
+ "checked",
63
+ "selected",
64
+ "hidden",
65
+ "required",
66
+ ].includes(attrName)
67
+ ) {
68
+ if (val) el.setAttribute(attrName, "");
69
+ else el.removeAttribute(attrName);
70
+ if (attrName in el) el[attrName] = !!val;
71
+ return;
72
+ }
73
+ if (val != null) el.setAttribute(attrName, String(val));
74
+ else el.removeAttribute(attrName);
75
+ }
76
+ _watchExpr(expr, ctx, update);
77
+ update();
78
+ },
79
+ });
80
+
81
+ registerDirective("model", {
82
+ priority: 20,
83
+ init(el, name, expr) {
84
+ const ctx = findContext(el);
85
+ const tag = el.tagName;
86
+ const type = el.type;
87
+
88
+ // Model → DOM
89
+ function update() {
90
+ const val = evaluate(expr, ctx);
91
+ if (tag === "INPUT" && type === "checkbox") {
92
+ el.checked = !!val;
93
+ } else if (tag === "INPUT" && type === "radio") {
94
+ el.checked = el.value === String(val);
95
+ } else if (tag === "SELECT") {
96
+ el.value = val != null ? String(val) : "";
97
+ } else {
98
+ el.value = val != null ? String(val) : "";
99
+ }
100
+ }
101
+
102
+ // DOM → Model
103
+ const event =
104
+ tag === "SELECT" || type === "checkbox" || type === "radio"
105
+ ? "change"
106
+ : "input";
107
+ el.addEventListener(event, () => {
108
+ let val;
109
+ if (type === "checkbox") val = el.checked;
110
+ else if (type === "number" || type === "range") val = Number(el.value);
111
+ else val = el.value;
112
+ _execStatement(`${expr} = __val`, ctx, { __val: val });
113
+ });
114
+
115
+ ctx.$watch(update);
116
+ update();
117
+ },
118
+ });
@@ -0,0 +1,283 @@
1
+ // ═══════════════════════════════════════════════════════════════════════
2
+ // DIRECTIVES: if, else-if, else, show, hide, switch
3
+ // ═══════════════════════════════════════════════════════════════════════
4
+
5
+ import { _watchExpr } from "../globals.js";
6
+ import { evaluate } from "../evaluate.js";
7
+ import { findContext, _clearDeclared, _cloneTemplate } from "../dom.js";
8
+ import { registerDirective, processTree } from "../registry.js";
9
+ import { _animateIn, _animateOut } from "../animations.js";
10
+
11
+ registerDirective("if", {
12
+ priority: 10,
13
+ init(el, name, expr) {
14
+ const ctx = findContext(el);
15
+ const thenId = el.getAttribute("then");
16
+ const elseId = el.getAttribute("else");
17
+ const animEnter =
18
+ el.getAttribute("animate-enter") || el.getAttribute("animate");
19
+ const animLeave = el.getAttribute("animate-leave");
20
+ const transition = el.getAttribute("transition");
21
+ const animDuration = parseInt(el.getAttribute("animate-duration")) || 0;
22
+ const originalChildren = [...el.childNodes].map((n) => n.cloneNode(true));
23
+ let currentState = undefined;
24
+
25
+ function update() {
26
+ const result = !!evaluate(expr, ctx);
27
+ if (result === currentState) return;
28
+ currentState = result;
29
+
30
+ // Animation leave
31
+ if (animLeave || transition) {
32
+ _animateOut(el, animLeave, transition, () => render(result), animDuration);
33
+ } else {
34
+ render(result);
35
+ }
36
+ }
37
+
38
+ function render(result) {
39
+ if (result) {
40
+ if (thenId) {
41
+ const clone = _cloneTemplate(thenId);
42
+ if (clone) {
43
+ el.innerHTML = "";
44
+ el.appendChild(clone);
45
+ }
46
+ } else {
47
+ el.innerHTML = "";
48
+ for (const child of originalChildren)
49
+ el.appendChild(child.cloneNode(true));
50
+ }
51
+ } else {
52
+ if (elseId) {
53
+ const clone = _cloneTemplate(elseId);
54
+ if (clone) {
55
+ el.innerHTML = "";
56
+ el.appendChild(clone);
57
+ }
58
+ } else if (thenId) {
59
+ el.innerHTML = "";
60
+ } else {
61
+ el.innerHTML = "";
62
+ }
63
+ }
64
+
65
+ _clearDeclared(el);
66
+ processTree(el);
67
+
68
+ // Animation enter
69
+ if (animEnter || transition) {
70
+ _animateIn(el, animEnter, transition, animDuration);
71
+ }
72
+ }
73
+
74
+ ctx.$watch(update);
75
+ update();
76
+ },
77
+ });
78
+
79
+ registerDirective("else-if", {
80
+ priority: 10,
81
+ init(el, name, expr) {
82
+ // Works like `if` but checks previous sibling's condition
83
+ const ctx = findContext(el);
84
+ const thenId = el.getAttribute("then");
85
+
86
+ function update() {
87
+ // Check if any preceding if/else-if was true
88
+ let prev = el.previousElementSibling;
89
+ while (prev) {
90
+ const prevExpr =
91
+ prev.getAttribute("if") || prev.getAttribute("else-if");
92
+ if (prevExpr) {
93
+ if (evaluate(prevExpr, ctx)) {
94
+ el.innerHTML = "";
95
+ el.style.display = "none";
96
+ return;
97
+ }
98
+ } else break;
99
+ prev = prev.previousElementSibling;
100
+ }
101
+
102
+ const result = !!evaluate(expr, ctx);
103
+ el.style.display = "";
104
+ if (result) {
105
+ if (thenId) {
106
+ const clone = _cloneTemplate(thenId);
107
+ if (clone) {
108
+ el.innerHTML = "";
109
+ el.appendChild(clone);
110
+ }
111
+ }
112
+ _clearDeclared(el);
113
+ processTree(el);
114
+ } else {
115
+ el.innerHTML = "";
116
+ }
117
+ }
118
+ ctx.$watch(update);
119
+ update();
120
+ },
121
+ });
122
+
123
+ registerDirective("else", {
124
+ priority: 10,
125
+ init(el) {
126
+ // Skip if this element also has an "if" directive (else is used as an attribute of if)
127
+ if (el.hasAttribute("if")) return;
128
+ const ctx = findContext(el);
129
+ const thenId = el.getAttribute("then");
130
+ const originalChildren = [...el.childNodes].map((n) => n.cloneNode(true));
131
+
132
+ function update() {
133
+ // Check if any preceding if/else-if was true
134
+ let prev = el.previousElementSibling;
135
+ while (prev) {
136
+ const prevExpr =
137
+ prev.getAttribute("if") || prev.getAttribute("else-if");
138
+ if (prevExpr) {
139
+ if (evaluate(prevExpr, ctx)) {
140
+ el.innerHTML = "";
141
+ el.style.display = "none";
142
+ return;
143
+ }
144
+ } else break;
145
+ prev = prev.previousElementSibling;
146
+ }
147
+
148
+ // No preceding condition was true — show else content
149
+ el.style.display = "";
150
+ if (thenId) {
151
+ const clone = _cloneTemplate(thenId);
152
+ if (clone) {
153
+ el.innerHTML = "";
154
+ el.appendChild(clone);
155
+ }
156
+ } else {
157
+ el.innerHTML = "";
158
+ for (const child of originalChildren)
159
+ el.appendChild(child.cloneNode(true));
160
+ }
161
+ _clearDeclared(el);
162
+ processTree(el);
163
+ }
164
+ ctx.$watch(update);
165
+ update();
166
+ },
167
+ });
168
+
169
+ registerDirective("show", {
170
+ priority: 20,
171
+ init(el, name, expr) {
172
+ const ctx = findContext(el);
173
+ const animEnter = el.getAttribute("animate-enter") || el.getAttribute("animate");
174
+ const animLeave = el.getAttribute("animate-leave");
175
+ const transition = el.getAttribute("transition");
176
+ const animDuration = parseInt(el.getAttribute("animate-duration")) || 0;
177
+ let currentState = undefined;
178
+
179
+ function update() {
180
+ const result = !!evaluate(expr, ctx);
181
+ if (result === currentState) return;
182
+ currentState = result;
183
+
184
+ if (result) {
185
+ el.style.display = "";
186
+ if (animEnter || transition) _animateIn(el, animEnter, transition, animDuration);
187
+ } else {
188
+ if (animLeave || transition) {
189
+ _animateOut(el, animLeave, transition, () => { el.style.display = "none"; }, animDuration);
190
+ } else {
191
+ el.style.display = "none";
192
+ }
193
+ }
194
+ }
195
+ _watchExpr(expr, ctx, update);
196
+ update();
197
+ },
198
+ });
199
+
200
+ registerDirective("hide", {
201
+ priority: 20,
202
+ init(el, name, expr) {
203
+ const ctx = findContext(el);
204
+ const animEnter = el.getAttribute("animate-enter") || el.getAttribute("animate");
205
+ const animLeave = el.getAttribute("animate-leave");
206
+ const transition = el.getAttribute("transition");
207
+ const animDuration = parseInt(el.getAttribute("animate-duration")) || 0;
208
+ let currentState = undefined;
209
+
210
+ function update() {
211
+ const result = !evaluate(expr, ctx);
212
+ if (result === currentState) return;
213
+ currentState = result;
214
+
215
+ if (result) {
216
+ el.style.display = "";
217
+ if (animEnter || transition) _animateIn(el, animEnter, transition, animDuration);
218
+ } else {
219
+ if (animLeave || transition) {
220
+ _animateOut(el, animLeave, transition, () => { el.style.display = "none"; }, animDuration);
221
+ } else {
222
+ el.style.display = "none";
223
+ }
224
+ }
225
+ }
226
+ _watchExpr(expr, ctx, update);
227
+ update();
228
+ },
229
+ });
230
+
231
+ registerDirective("switch", {
232
+ priority: 10,
233
+ init(el, name, expr) {
234
+ const ctx = findContext(el);
235
+
236
+ function update() {
237
+ const val = evaluate(expr, ctx);
238
+ let matched = false;
239
+
240
+ for (const child of [...el.children]) {
241
+ const caseVal = child.getAttribute("case");
242
+ const isDefault = child.hasAttribute("default");
243
+ const thenTpl = child.getAttribute("then");
244
+
245
+ if (caseVal) {
246
+ // Support multi-value: case="'a','b'"
247
+ const values = caseVal
248
+ .split(",")
249
+ .map((v) => evaluate(v.trim(), ctx));
250
+ if (!matched && values.includes(val)) {
251
+ matched = true;
252
+ child.style.display = "";
253
+ if (thenTpl) {
254
+ const clone = _cloneTemplate(thenTpl);
255
+ if (clone) {
256
+ child.innerHTML = "";
257
+ child.appendChild(clone);
258
+ }
259
+ child.__declared = false;
260
+ processTree(child);
261
+ }
262
+ } else {
263
+ child.style.display = "none";
264
+ }
265
+ } else if (isDefault) {
266
+ child.style.display = matched ? "none" : "";
267
+ if (!matched && thenTpl) {
268
+ const clone = _cloneTemplate(thenTpl);
269
+ if (clone) {
270
+ child.innerHTML = "";
271
+ child.appendChild(clone);
272
+ }
273
+ child.__declared = false;
274
+ processTree(child);
275
+ }
276
+ }
277
+ }
278
+ }
279
+
280
+ ctx.$watch(update);
281
+ update();
282
+ },
283
+ });