- seems to be working
- added some utility
This commit is contained in:
parent
11ebf22ec9
commit
0f2874e6df
@ -221,26 +221,21 @@ export class Component extends EventTarget {
|
||||
|
||||
// ─── Router init ───
|
||||
|
||||
export function initRouter(outletId, includeId, routes) {
|
||||
export function initRouter(outletId, routes) {
|
||||
const outletRef = document.getElementById(outletId);
|
||||
const includeRef = document.getElementById(includeId);
|
||||
if (!outletRef) {
|
||||
console.error(`Outlet element with id '${outletId}' not found`);
|
||||
return null;
|
||||
}
|
||||
if (!includeRef) {
|
||||
console.error(`Include element with id '${includeId}' not found`);
|
||||
return null;
|
||||
}
|
||||
return new Router(outletRef, includeRef, routes);
|
||||
return new Router(outletRef, routes);
|
||||
}
|
||||
|
||||
// ─── Router ───
|
||||
|
||||
export class Router {
|
||||
constructor(outletRef, includeRef, routes) {
|
||||
constructor(outletRef, routes) {
|
||||
this.outletRef = outletRef;
|
||||
this.includeRef = includeRef;
|
||||
this.shadowRoot = outletRef.attachShadow({ mode: "open" });
|
||||
this.routes = routes;
|
||||
this.templateCache = new Map();
|
||||
this.activeController = null;
|
||||
@ -248,8 +243,7 @@ export class Router {
|
||||
this._boundUpdateHandler = null;
|
||||
this._rendering = false;
|
||||
this._pendingUpdate = false;
|
||||
outletRef.textContent = "Loading...";
|
||||
includeRef.innerHTML = "";
|
||||
this.shadowRoot.textContent = "Loading...";
|
||||
navigation.addEventListener("navigate", this.handleNavigate.bind(this));
|
||||
}
|
||||
|
||||
@ -312,7 +306,10 @@ export class Router {
|
||||
}
|
||||
|
||||
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;
|
||||
if (!paths) return;
|
||||
const list = Array.isArray(paths) ? paths : [paths];
|
||||
@ -320,7 +317,8 @@ export class Router {
|
||||
const link = document.createElement("link");
|
||||
link.rel = "stylesheet";
|
||||
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);
|
||||
this.activeRenderResult = result;
|
||||
|
||||
// Clear outlet, append fragment
|
||||
this.outletRef.textContent = "";
|
||||
this.outletRef.appendChild(result.fragment);
|
||||
// Clear shadow root content (keep style links), append fragment
|
||||
for (const child of Array.from(this.shadowRoot.childNodes)) {
|
||||
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
|
||||
this.wireEvents(this.activeController, this.outletRef);
|
||||
this.wireViewChildren(this.activeController, this.outletRef);
|
||||
// Wire events + two-way bindings on the live shadow DOM
|
||||
this.wireEvents(this.activeController, this.shadowRoot);
|
||||
this.wireViewChildren(this.activeController, this.shadowRoot);
|
||||
|
||||
this.activeController.onAfterRender();
|
||||
this._rendering = false;
|
||||
|
||||
@ -1,11 +1,5 @@
|
||||
import {
|
||||
initRouter,
|
||||
Route,
|
||||
ViewChild,
|
||||
Component,
|
||||
ComponentInput,
|
||||
ComponentDefinition,
|
||||
} from "./node-pwa.js";
|
||||
import { initRouter, Route, ViewChild, Component, ComponentInput, ComponentDefinition } from "./node-pwa.js";
|
||||
import { IntInput } from "./utils.js";
|
||||
|
||||
class TestComponent extends Component {
|
||||
static get definition() {
|
||||
@ -21,14 +15,15 @@ class TestComponent extends Component {
|
||||
});
|
||||
}
|
||||
|
||||
count = new ComponentInput(0);
|
||||
count = new ComponentInput(0, IntInput);
|
||||
name = new ComponentInput("World");
|
||||
|
||||
incrementCount() {
|
||||
this.count++;
|
||||
const currentCount = this.count();
|
||||
this.count.set(currentCount + 1);
|
||||
}
|
||||
}
|
||||
|
||||
const route1 = new Route("counter", 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