- seems to be working
- added some utility
This commit is contained in:
parent
11ebf22ec9
commit
0f2874e6df
@ -211,9 +211,9 @@ export class Component extends EventTarget {
|
|||||||
throw new Error("Component definition is not defined");
|
throw new Error("Component definition is not defined");
|
||||||
}
|
}
|
||||||
|
|
||||||
onInit() {}
|
onInit() { }
|
||||||
onBeforeRender() {}
|
onBeforeRender() { }
|
||||||
onAfterRender() {}
|
onAfterRender() { }
|
||||||
onDestroy() {
|
onDestroy() {
|
||||||
this._eventBindings.forEach((b) => b.detach());
|
this._eventBindings.forEach((b) => b.detach());
|
||||||
}
|
}
|
||||||
@ -221,26 +221,21 @@ export class Component extends EventTarget {
|
|||||||
|
|
||||||
// ─── Router init ───
|
// ─── Router init ───
|
||||||
|
|
||||||
export function initRouter(outletId, includeId, routes) {
|
export function initRouter(outletId, routes) {
|
||||||
const outletRef = document.getElementById(outletId);
|
const outletRef = document.getElementById(outletId);
|
||||||
const includeRef = document.getElementById(includeId);
|
|
||||||
if (!outletRef) {
|
if (!outletRef) {
|
||||||
console.error(`Outlet element with id '${outletId}' not found`);
|
console.error(`Outlet element with id '${outletId}' not found`);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (!includeRef) {
|
return new Router(outletRef, routes);
|
||||||
console.error(`Include element with id '${includeId}' not found`);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return new Router(outletRef, includeRef, routes);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Router ───
|
// ─── Router ───
|
||||||
|
|
||||||
export class Router {
|
export class Router {
|
||||||
constructor(outletRef, includeRef, routes) {
|
constructor(outletRef, routes) {
|
||||||
this.outletRef = outletRef;
|
this.outletRef = outletRef;
|
||||||
this.includeRef = includeRef;
|
this.shadowRoot = outletRef.attachShadow({ mode: "open" });
|
||||||
this.routes = routes;
|
this.routes = routes;
|
||||||
this.templateCache = new Map();
|
this.templateCache = new Map();
|
||||||
this.activeController = null;
|
this.activeController = null;
|
||||||
@ -248,8 +243,7 @@ export class Router {
|
|||||||
this._boundUpdateHandler = null;
|
this._boundUpdateHandler = null;
|
||||||
this._rendering = false;
|
this._rendering = false;
|
||||||
this._pendingUpdate = false;
|
this._pendingUpdate = false;
|
||||||
outletRef.textContent = "Loading...";
|
this.shadowRoot.textContent = "Loading...";
|
||||||
includeRef.innerHTML = "";
|
|
||||||
navigation.addEventListener("navigate", this.handleNavigate.bind(this));
|
navigation.addEventListener("navigate", this.handleNavigate.bind(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -312,7 +306,10 @@ export class Router {
|
|||||||
}
|
}
|
||||||
|
|
||||||
insertIncludes(route) {
|
insertIncludes(route) {
|
||||||
this.includeRef.innerHTML = "";
|
// Remove previous component styles from shadow root
|
||||||
|
this.shadowRoot
|
||||||
|
.querySelectorAll("link[data-component-style]")
|
||||||
|
.forEach((l) => l.remove());
|
||||||
const paths = route.ComponentDefinition.stylesPath;
|
const paths = route.ComponentDefinition.stylesPath;
|
||||||
if (!paths) return;
|
if (!paths) return;
|
||||||
const list = Array.isArray(paths) ? paths : [paths];
|
const list = Array.isArray(paths) ? paths : [paths];
|
||||||
@ -320,7 +317,8 @@ export class Router {
|
|||||||
const link = document.createElement("link");
|
const link = document.createElement("link");
|
||||||
link.rel = "stylesheet";
|
link.rel = "stylesheet";
|
||||||
link.href = p;
|
link.href = p;
|
||||||
this.includeRef.appendChild(link);
|
link.setAttribute("data-component-style", "");
|
||||||
|
this.shadowRoot.appendChild(link);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -335,13 +333,20 @@ export class Router {
|
|||||||
const result = this.activeTemplate.render(this.activeController);
|
const result = this.activeTemplate.render(this.activeController);
|
||||||
this.activeRenderResult = result;
|
this.activeRenderResult = result;
|
||||||
|
|
||||||
// Clear outlet, append fragment
|
// Clear shadow root content (keep style links), append fragment
|
||||||
this.outletRef.textContent = "";
|
for (const child of Array.from(this.shadowRoot.childNodes)) {
|
||||||
this.outletRef.appendChild(result.fragment);
|
if (
|
||||||
|
child.nodeType !== Node.ELEMENT_NODE ||
|
||||||
|
!child.hasAttribute("data-component-style")
|
||||||
|
) {
|
||||||
|
child.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.shadowRoot.appendChild(result.fragment);
|
||||||
|
|
||||||
// Wire events + two-way bindings on the live DOM
|
// Wire events + two-way bindings on the live shadow DOM
|
||||||
this.wireEvents(this.activeController, this.outletRef);
|
this.wireEvents(this.activeController, this.shadowRoot);
|
||||||
this.wireViewChildren(this.activeController, this.outletRef);
|
this.wireViewChildren(this.activeController, this.shadowRoot);
|
||||||
|
|
||||||
this.activeController.onAfterRender();
|
this.activeController.onAfterRender();
|
||||||
this._rendering = false;
|
this._rendering = false;
|
||||||
|
|||||||
@ -1,11 +1,5 @@
|
|||||||
import {
|
import { initRouter, Route, ViewChild, Component, ComponentInput, ComponentDefinition } from "./node-pwa.js";
|
||||||
initRouter,
|
import { IntInput } from "./utils.js";
|
||||||
Route,
|
|
||||||
ViewChild,
|
|
||||||
Component,
|
|
||||||
ComponentInput,
|
|
||||||
ComponentDefinition,
|
|
||||||
} from "./node-pwa.js";
|
|
||||||
|
|
||||||
class TestComponent extends Component {
|
class TestComponent extends Component {
|
||||||
static get definition() {
|
static get definition() {
|
||||||
@ -21,14 +15,15 @@ class TestComponent extends Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
count = new ComponentInput(0);
|
count = new ComponentInput(0, IntInput);
|
||||||
name = new ComponentInput("World");
|
name = new ComponentInput("World");
|
||||||
|
|
||||||
incrementCount() {
|
incrementCount() {
|
||||||
this.count++;
|
const currentCount = this.count();
|
||||||
|
this.count.set(currentCount + 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const route1 = new Route("counter", TestComponent);
|
const route1 = new Route("counter", TestComponent);
|
||||||
const route2 = new Route("counter/:count", TestComponent);
|
const route2 = new Route("counter/:count", TestComponent);
|
||||||
initRouter("routerOutlet", "styleOutlet", [route1, route2]);
|
initRouter("routerOutlet", [route1, route2]);
|
||||||
|
|||||||
176
OnlyPrompt.Frontend/js/utils.js
Normal file
176
OnlyPrompt.Frontend/js/utils.js
Normal file
@ -0,0 +1,176 @@
|
|||||||
|
// ─── parseDate (C# DateTime.ParseExact style) ───
|
||||||
|
// Parses a date string using an exact format pattern.
|
||||||
|
// Supported tokens:
|
||||||
|
// yyyy — 4-digit year
|
||||||
|
// yy — 2-digit year (assumes 2000+)
|
||||||
|
// MM — 2-digit month (01–12)
|
||||||
|
// M — 1 or 2-digit month
|
||||||
|
// dd — 2-digit day (01–31)
|
||||||
|
// d — 1 or 2-digit day
|
||||||
|
// HH — 2-digit hour 24h (00–23)
|
||||||
|
// H — 1 or 2-digit hour 24h
|
||||||
|
// hh — 2-digit hour 12h (01–12)
|
||||||
|
// h — 1 or 2-digit hour 12h
|
||||||
|
// mm — 2-digit minute (00–59)
|
||||||
|
// m — 1 or 2-digit minute
|
||||||
|
// ss — 2-digit second (00–59)
|
||||||
|
// s — 1 or 2-digit second
|
||||||
|
// fff — 3-digit milliseconds
|
||||||
|
// ff — 2-digit (tenths + hundredths)
|
||||||
|
// f — 1-digit (tenths)
|
||||||
|
// tt — AM/PM
|
||||||
|
// t — A/P
|
||||||
|
// Z — literal 'Z' (UTC marker)
|
||||||
|
// K — timezone offset (+HH:mm, -HH:mm, or Z)
|
||||||
|
//
|
||||||
|
// All other characters are matched literally.
|
||||||
|
// Returns Date on success, null on failure.
|
||||||
|
|
||||||
|
const TOKEN_ORDER = [
|
||||||
|
"yyyy", "yy",
|
||||||
|
"MM", "M",
|
||||||
|
"dd", "d",
|
||||||
|
"HH", "H", "hh", "h",
|
||||||
|
"mm", "m",
|
||||||
|
"ss", "s",
|
||||||
|
"fff", "ff", "f",
|
||||||
|
"tt", "t",
|
||||||
|
"K", "Z",
|
||||||
|
];
|
||||||
|
|
||||||
|
const TOKEN_PATTERNS = {
|
||||||
|
yyyy: "(\\d{4})",
|
||||||
|
yy: "(\\d{2})",
|
||||||
|
MM: "(\\d{2})",
|
||||||
|
M: "(\\d{1,2})",
|
||||||
|
dd: "(\\d{2})",
|
||||||
|
d: "(\\d{1,2})",
|
||||||
|
HH: "(\\d{2})",
|
||||||
|
H: "(\\d{1,2})",
|
||||||
|
hh: "(\\d{2})",
|
||||||
|
h: "(\\d{1,2})",
|
||||||
|
mm: "(\\d{2})",
|
||||||
|
m: "(\\d{1,2})",
|
||||||
|
ss: "(\\d{2})",
|
||||||
|
s: "(\\d{1,2})",
|
||||||
|
fff: "(\\d{3})",
|
||||||
|
ff: "(\\d{2})",
|
||||||
|
f: "(\\d{1})",
|
||||||
|
tt: "(AM|PM|am|pm)",
|
||||||
|
t: "([AaPp])",
|
||||||
|
K: "(Z|[+-]\\d{2}:\\d{2})",
|
||||||
|
Z: "(Z)",
|
||||||
|
};
|
||||||
|
|
||||||
|
function buildFormatRegex(format) {
|
||||||
|
const groups = [];
|
||||||
|
let regexStr = "";
|
||||||
|
let i = 0;
|
||||||
|
|
||||||
|
while (i < format.length) {
|
||||||
|
let matched = false;
|
||||||
|
for (const token of TOKEN_ORDER) {
|
||||||
|
if (format.startsWith(token, i)) {
|
||||||
|
groups.push(token);
|
||||||
|
regexStr += TOKEN_PATTERNS[token];
|
||||||
|
i += token.length;
|
||||||
|
matched = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!matched) {
|
||||||
|
// Escape literal character
|
||||||
|
regexStr += format[i].replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { regex: new RegExp("^" + regexStr + "$"), groups };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseDate(input, format) {
|
||||||
|
if (input == null || format == null) return null;
|
||||||
|
if (input instanceof Date) return input;
|
||||||
|
|
||||||
|
const str = String(input);
|
||||||
|
const { regex, groups } = buildFormatRegex(format);
|
||||||
|
const match = str.match(regex);
|
||||||
|
if (!match) return null;
|
||||||
|
|
||||||
|
let year = 0, month = 1, day = 1;
|
||||||
|
let hour = 0, minute = 0, second = 0, ms = 0;
|
||||||
|
let isPM = false, isAM = false;
|
||||||
|
let tzOffset = null; // minutes
|
||||||
|
|
||||||
|
for (let g = 0; g < groups.length; g++) {
|
||||||
|
const token = groups[g];
|
||||||
|
const val = match[g + 1];
|
||||||
|
|
||||||
|
switch (token) {
|
||||||
|
case "yyyy": year = parseInt(val, 10); break;
|
||||||
|
case "yy": year = 2000 + parseInt(val, 10); break;
|
||||||
|
case "MM":
|
||||||
|
case "M": month = parseInt(val, 10); break;
|
||||||
|
case "dd":
|
||||||
|
case "d": day = parseInt(val, 10); break;
|
||||||
|
case "HH":
|
||||||
|
case "H": hour = parseInt(val, 10); break;
|
||||||
|
case "hh":
|
||||||
|
case "h": hour = parseInt(val, 10); break;
|
||||||
|
case "mm":
|
||||||
|
case "m": minute = parseInt(val, 10); break;
|
||||||
|
case "ss":
|
||||||
|
case "s": second = parseInt(val, 10); break;
|
||||||
|
case "fff": ms = parseInt(val, 10); break;
|
||||||
|
case "ff": ms = parseInt(val, 10) * 10; break;
|
||||||
|
case "f": ms = parseInt(val, 10) * 100; break;
|
||||||
|
case "tt": isPM = val.toUpperCase() === "PM"; isAM = val.toUpperCase() === "AM"; break;
|
||||||
|
case "t": isPM = val.toUpperCase() === "P"; isAM = val.toUpperCase() === "A"; break;
|
||||||
|
case "Z": tzOffset = 0; break;
|
||||||
|
case "K":
|
||||||
|
if (val === "Z") {
|
||||||
|
tzOffset = 0;
|
||||||
|
} else {
|
||||||
|
const sign = val[0] === "+" ? 1 : -1;
|
||||||
|
const [h, m] = val.substring(1).split(":").map(Number);
|
||||||
|
tzOffset = sign * (h * 60 + m);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 12h → 24h conversion
|
||||||
|
if (isPM && hour < 12) hour += 12;
|
||||||
|
if (isAM && hour === 12) hour = 0;
|
||||||
|
|
||||||
|
if (tzOffset !== null) {
|
||||||
|
// Build UTC date, apply offset
|
||||||
|
const utcMs = Date.UTC(year, month - 1, day, hour, minute, second, ms);
|
||||||
|
return new Date(utcMs - tzOffset * 60000);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Date(year, month - 1, day, hour, minute, second, ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Input transform presets ───
|
||||||
|
|
||||||
|
export const IntInput = { transform: parseInt };
|
||||||
|
export const FloatInput = { transform: parseFloat };
|
||||||
|
export const BoolInput = {
|
||||||
|
transform: (v) => {
|
||||||
|
if (typeof v === "boolean") return v;
|
||||||
|
if (typeof v === "string") {
|
||||||
|
if (v.toLowerCase() === "true") return true;
|
||||||
|
if (v.toLowerCase() === "false") return false;
|
||||||
|
}
|
||||||
|
return Boolean(v);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export function DateInput(format) {
|
||||||
|
return {
|
||||||
|
transform: (v) => parseDate(v, format),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IsoDateInput = DateInput("yyyy-MM-ddTHH:mm:ssK");
|
||||||
Loading…
x
Reference in New Issue
Block a user