Enable EarlyAssets and stimulus controllers support. Improve layout in component debugger
This commit is contained in:
parent
c654b092ee
commit
148c533b5b
29
package-lock.json
generated
29
package-lock.json
generated
@ -14,10 +14,11 @@
|
||||
"@hotwired/turbo": "^8.0.2",
|
||||
"@koa/router": "^12.0.1",
|
||||
"@playwright/test": "^1.36.1",
|
||||
"@sealcode/jdd": "^0.3.3",
|
||||
"@sealcode/sealgen": "^0.14.4",
|
||||
"@sealcode/jdd": "^0.3.4",
|
||||
"@sealcode/sealgen": "^0.14.5",
|
||||
"@sealcode/ts-predicates": "^0.4.3",
|
||||
"@types/kill-port": "^2.0.0",
|
||||
"@types/leaflet": "^1.9.8",
|
||||
"get-port": "^7.0.0",
|
||||
"js-convert-case": "^4.2.0",
|
||||
"koa-responsive-image-router": "^0.2.19",
|
||||
@ -791,9 +792,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sealcode/jdd": {
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@sealcode/jdd/-/jdd-0.3.3.tgz",
|
||||
"integrity": "sha512-mGOBlE/gQSSdXZfg5wP6mq8vw6q/qrxB6HIMGvctSZLAAFjiz/ExnOLN+5lIGmO5XpNRWcCHHlBmFKpBy4Vz3A==",
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmjs.org/@sealcode/jdd/-/jdd-0.3.4.tgz",
|
||||
"integrity": "sha512-lwkVoUguOsOxuKYURw/hl3XqFARY85CuqnwzgdHG+gC6x23y/PgljbPREAT5GSKYCIVWkwGdNAfbh0HglV4Z4Q==",
|
||||
"dependencies": {
|
||||
"@sealcode/ts-predicates": "^0.5.3",
|
||||
"koa-responsive-image-router": "^0.2.19",
|
||||
@ -820,8 +821,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sealcode/sealgen": {
|
||||
"version": "0.14.4",
|
||||
"license": "ISC",
|
||||
"version": "0.14.5",
|
||||
"resolved": "https://registry.npmjs.org/@sealcode/sealgen/-/sealgen-0.14.5.tgz",
|
||||
"integrity": "sha512-SE3dpS5KAsvpznKJla1VoqlnDNxeHJ2mTN2Kzdwg9Chleht/Mw/ojb7E58ZX/WxJNg+BRuCYy6knJkvkO7SfQg==",
|
||||
"dependencies": {
|
||||
"@koa/router": "^12.0.1",
|
||||
"@sealcode/ts-predicates": "^0.4.3",
|
||||
@ -1031,6 +1033,11 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/geojson": {
|
||||
"version": "7946.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.14.tgz",
|
||||
"integrity": "sha512-WCfD5Ht3ZesJUsONdhvm84dmzWOiOzOAqOncN0++w0lBw1o8OuDNJF2McvvCef/yBqb/HYRahp1BYtODFQ8bRg=="
|
||||
},
|
||||
"node_modules/@types/http-assert": {
|
||||
"version": "1.5.5",
|
||||
"license": "MIT"
|
||||
@ -1119,6 +1126,14 @@
|
||||
"@types/koa-send": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/leaflet": {
|
||||
"version": "1.9.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/leaflet/-/leaflet-1.9.8.tgz",
|
||||
"integrity": "sha512-EXdsL4EhoUtGm2GC2ZYtXn+Fzc6pluVgagvo2VC1RHWToLGlTRwVYoDpqS/7QXa01rmDyBjJk3Catpf60VMkwg==",
|
||||
"dependencies": {
|
||||
"@types/geojson": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/luxon": {
|
||||
"version": "3.3.8",
|
||||
"license": "MIT"
|
||||
|
@ -6,8 +6,8 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"start": "docker-compose up -d db && node .",
|
||||
"typecheck:back": "npx tsc --noEmit --target es6 --lib es2021,dom -p src/back",
|
||||
"typecheck:front": "npx tsc --noEmit --target es6 --lib es2015,dom -p src/front",
|
||||
"typecheck:back": "npx tsc --noEmit --target es6 --lib es2021,dom -p tsconfig-back.json",
|
||||
"typecheck:front": "npx tsc --noEmit --target es6 --lib es2015,dom -p tsconfig-front.json",
|
||||
"build": "sealgen build",
|
||||
"watch": "multiple-scripts-tmux -p watch",
|
||||
"reset-db": "docker-compose down && docker-compose up -d",
|
||||
@ -35,10 +35,11 @@
|
||||
"@hotwired/turbo": "^8.0.2",
|
||||
"@koa/router": "^12.0.1",
|
||||
"@playwright/test": "^1.36.1",
|
||||
"@sealcode/jdd": "^0.3.3",
|
||||
"@sealcode/sealgen": "^0.14.4",
|
||||
"@sealcode/jdd": "^0.3.4",
|
||||
"@sealcode/sealgen": "^0.14.5",
|
||||
"@sealcode/ts-predicates": "^0.4.3",
|
||||
"@types/kill-port": "^2.0.0",
|
||||
"@types/leaflet": "^1.9.8",
|
||||
"get-port": "^7.0.0",
|
||||
"js-convert-case": "^4.2.0",
|
||||
"koa-responsive-image-router": "^0.2.19",
|
||||
|
145
src/back/html.ts
145
src/back/html.ts
@ -46,6 +46,11 @@ export default function html(
|
||||
) => Templatable = defaultHead
|
||||
): Readable {
|
||||
ctx.set("content-type", "text/html;charset=utf-8");
|
||||
const controllers: string[] = [];
|
||||
if (htmlOptions.autoRefreshCSS) {
|
||||
controllers.push("refresh-styles");
|
||||
controllers.push("refresh-on-ts-changes");
|
||||
}
|
||||
return tempstream/* HTML */ ` <!DOCTYPE html>
|
||||
<html
|
||||
lang="${htmlOptions.language || DEFAULT_HTML_LANG}"
|
||||
@ -54,146 +59,8 @@ export default function html(
|
||||
<head>
|
||||
${makeHead(ctx, title, htmlOptions)}
|
||||
</head>
|
||||
<body>
|
||||
<body data-controller="${controllers.join(" ")}">
|
||||
${(htmlOptions.navbar || default_navbar)(ctx)} ${body}
|
||||
${htmlOptions.autoRefreshCSS
|
||||
? /* HTML */ `<script>
|
||||
function make_new_link() {
|
||||
const new_link = document.createElement("link");
|
||||
new_link.rel = "stylesheet";
|
||||
new_link.href = \`/dist/main.css?\${Math.random()}+\${Math.random()}\`;
|
||||
new_link.type = "text/css";
|
||||
return new_link;
|
||||
}
|
||||
|
||||
function getStyles() {
|
||||
return Array.from(
|
||||
document.querySelectorAll("head link")
|
||||
).filter(
|
||||
(e) => new URL(e.href).pathname == "/dist/main.css"
|
||||
);
|
||||
}
|
||||
|
||||
function cleanup_css() {
|
||||
console.log("clearing styles");
|
||||
getStyles()
|
||||
.slice(0, -1)
|
||||
.forEach((style) => {
|
||||
style.parentElement.removeChild(style);
|
||||
});
|
||||
}
|
||||
document.documentElement.addEventListener(
|
||||
"turbo:morph",
|
||||
cleanup_css
|
||||
);
|
||||
|
||||
const sleep = (time) =>
|
||||
new Promise((resolve) => {
|
||||
setTimeout(resolve, time);
|
||||
});
|
||||
|
||||
const APP_DOWN_ERROR_MESSAGE = "App is currently down";
|
||||
|
||||
function get_status() {
|
||||
return fetch("/status.json").then((r) => r.json());
|
||||
}
|
||||
|
||||
async function wait_for_run_id_to_change() {
|
||||
let first_timestamp;
|
||||
try {
|
||||
const { started_at, status } = await get_status();
|
||||
first_timestamp = started_at;
|
||||
} catch (e) {
|
||||
await wait_for_app_to_be_stable();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!first_timestamp) {
|
||||
throw new Error(APP_DOWN_ERROR_MESSAGE);
|
||||
}
|
||||
|
||||
while (true) {
|
||||
const { started_at, status } =
|
||||
await get_status().catch(() => ({
|
||||
started_at: first_timestamp,
|
||||
}));
|
||||
if (started_at !== first_timestamp) {
|
||||
return;
|
||||
}
|
||||
await sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
async function wait_for_app_to_be_stable(n = 3) {
|
||||
console.log("Waiting for app to be stable....");
|
||||
let counter = 0;
|
||||
while (true) {
|
||||
const { status } = await get_status().catch((e) => ({
|
||||
status: "down",
|
||||
}));
|
||||
if (status == "running") {
|
||||
console.log(counter);
|
||||
counter++;
|
||||
} else {
|
||||
counter = 0;
|
||||
}
|
||||
if (counter == n) {
|
||||
return;
|
||||
}
|
||||
await sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
async function wait_for_app_restart() {
|
||||
try {
|
||||
await wait_for_run_id_to_change();
|
||||
} catch (e) {
|
||||
if (e.message !== APP_DOWN_ERROR_MESSAGE) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
await wait_for_app_to_be_stable();
|
||||
}
|
||||
|
||||
(async function () {
|
||||
const { started_at, status } = await fetch(
|
||||
"/status.json"
|
||||
).then((r) => r.json());
|
||||
last_known_start_timestamp = started_at;
|
||||
const { port, watch } = await fetch(
|
||||
"/dist/notifier.json"
|
||||
).then((r) => r.json());
|
||||
if (!watch) {
|
||||
console.warning(
|
||||
"Not running auto refresh on watch because the build process is not running in watch mode"
|
||||
);
|
||||
return;
|
||||
}
|
||||
const socket = new WebSocket(\`ws://localhost:\${port}\`);
|
||||
socket.onmessage = async (message) => {
|
||||
if (message.data === "css") {
|
||||
const new_link = make_new_link();
|
||||
new_link.onload = cleanup_css;
|
||||
document
|
||||
.querySelector("head")
|
||||
.appendChild(new_link);
|
||||
}
|
||||
if (message.data === "ts") {
|
||||
document.documentElement.classList.add(
|
||||
"restarting"
|
||||
);
|
||||
await wait_for_app_restart();
|
||||
document.documentElement.dispatchEvent(
|
||||
new Event("ts-rebuilt")
|
||||
);
|
||||
document.documentElement.classList.remove(
|
||||
"restarting"
|
||||
);
|
||||
}
|
||||
};
|
||||
})();
|
||||
</script>`
|
||||
: ""}
|
||||
${htmlOptions.disableCopyEvent
|
||||
? /* HTML */ "<script>document.addEventListener('copy', (e) => e.preventDefault());</script>"
|
||||
: ""}
|
||||
|
96
src/back/html/refresh-on-ts-changes.stimulus.ts
Normal file
96
src/back/html/refresh-on-ts-changes.stimulus.ts
Normal file
@ -0,0 +1,96 @@
|
||||
import { Controller } from "stimulus";
|
||||
|
||||
const APP_DOWN_ERROR_MESSAGE = "App is currently down";
|
||||
|
||||
const sleep = (time: number) =>
|
||||
new Promise((resolve) => {
|
||||
setTimeout(resolve, time);
|
||||
});
|
||||
|
||||
async function get_status() {
|
||||
const r = await fetch("/status.json");
|
||||
return await r.json();
|
||||
}
|
||||
|
||||
async function wait_for_run_id_to_change() {
|
||||
let first_timestamp: number;
|
||||
try {
|
||||
const { started_at } = await get_status();
|
||||
first_timestamp = started_at;
|
||||
} catch (e) {
|
||||
await wait_for_app_to_be_stable();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!first_timestamp) {
|
||||
throw new Error(APP_DOWN_ERROR_MESSAGE);
|
||||
}
|
||||
|
||||
while (true) {
|
||||
const { started_at } = await get_status().catch(() => ({
|
||||
started_at: first_timestamp,
|
||||
}));
|
||||
if (started_at !== first_timestamp) {
|
||||
return;
|
||||
}
|
||||
await sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
async function wait_for_app_to_be_stable(n = 3) {
|
||||
console.log("Waiting for app to be stable....");
|
||||
let counter = 0;
|
||||
while (true) {
|
||||
const { status } = await get_status().catch(() => ({
|
||||
status: "down",
|
||||
}));
|
||||
if (status == "running") {
|
||||
console.log(counter);
|
||||
counter++;
|
||||
} else {
|
||||
counter = 0;
|
||||
}
|
||||
if (counter == n) {
|
||||
return;
|
||||
}
|
||||
await sleep(100);
|
||||
}
|
||||
}
|
||||
|
||||
async function wait_for_app_restart() {
|
||||
try {
|
||||
await wait_for_run_id_to_change();
|
||||
} catch (e) {
|
||||
if (e.message !== APP_DOWN_ERROR_MESSAGE) {
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
await wait_for_app_to_be_stable();
|
||||
}
|
||||
|
||||
export default class RefreshOnTSChanges extends Controller {
|
||||
socket: WebSocket;
|
||||
|
||||
async connect() {
|
||||
const { port, watch } = await fetch("/dist/notifier.json").then((r) => r.json());
|
||||
if (!watch) {
|
||||
console.warn(
|
||||
"Not running auto refresh on watch because the build process is not running in watch mode"
|
||||
);
|
||||
return;
|
||||
}
|
||||
const socket = new WebSocket(`ws://localhost:${port}`);
|
||||
socket.onmessage = async (message) => {
|
||||
if (message.data.endsWith("-ts")) {
|
||||
document.documentElement.classList.add("restarting");
|
||||
await wait_for_app_restart();
|
||||
document.documentElement.dispatchEvent(new Event("ts-rebuilt"));
|
||||
document.documentElement.classList.remove("restarting");
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async disconnect() {
|
||||
this.socket.close();
|
||||
}
|
||||
}
|
46
src/back/html/refresh-styles.stimulus.ts
Normal file
46
src/back/html/refresh-styles.stimulus.ts
Normal file
@ -0,0 +1,46 @@
|
||||
import { Controller } from "stimulus";
|
||||
|
||||
function make_new_link() {
|
||||
const new_link = document.createElement("link");
|
||||
new_link.rel = "stylesheet";
|
||||
new_link.href = `/dist/main.css?${Math.random()}+${Math.random()}`;
|
||||
new_link.type = "text/css";
|
||||
return new_link;
|
||||
}
|
||||
|
||||
function getStyles() {
|
||||
return Array.from(document.querySelectorAll("head link")).filter(
|
||||
(e: HTMLLinkElement) => new URL(e.href).pathname == "/dist/main.css"
|
||||
);
|
||||
}
|
||||
|
||||
function cleanup_css() {
|
||||
console.log("clearing styles");
|
||||
getStyles()
|
||||
.slice(0, -1)
|
||||
.forEach((style) => {
|
||||
style.parentElement.removeChild(style);
|
||||
});
|
||||
}
|
||||
|
||||
export default class RefreshStyles extends Controller {
|
||||
socket: WebSocket;
|
||||
|
||||
async connect() {
|
||||
const { port } = await fetch("/dist/notifier.json").then((r) => r.json());
|
||||
this.socket = new WebSocket(`ws://localhost:${port}`);
|
||||
this.socket.onmessage = async (message) => {
|
||||
if (message.data === "css") {
|
||||
const new_link = make_new_link();
|
||||
new_link.onload = cleanup_css;
|
||||
document.querySelector("head").appendChild(new_link);
|
||||
}
|
||||
};
|
||||
document.documentElement.addEventListener("turbo:morph", cleanup_css);
|
||||
}
|
||||
|
||||
async disconnect() {
|
||||
this.socket.close();
|
||||
document.documentElement.removeEventListener("turbo:morph", cleanup_css);
|
||||
}
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
.map-with-pins {
|
||||
#map {
|
||||
height: 534px;
|
||||
}
|
||||
height: 534px;
|
||||
|
||||
.popup {
|
||||
.leaflet-popup-content-wrapper,
|
||||
.leaflet-popup-tip {
|
||||
@ -32,6 +31,7 @@
|
||||
font-size: 12px;
|
||||
line-height: 21px;
|
||||
}
|
||||
|
||||
.button {
|
||||
display: inline-block;
|
||||
margin: 0 auto;
|
||||
|
@ -32,95 +32,36 @@ export class MapWithPins extends Component<typeof component_arguments> {
|
||||
return component_arguments;
|
||||
}
|
||||
|
||||
async getEarlyAssets() {
|
||||
return [
|
||||
{
|
||||
type: "script" as const,
|
||||
url: "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js",
|
||||
identity: "https://unpkg.com/leaflet@1.9.4/dist/leaflet.js",
|
||||
integrity: "sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=",
|
||||
},
|
||||
{
|
||||
type: "style" as const,
|
||||
url: "https://unpkg.com/leaflet@1.9.4/dist/leaflet.css",
|
||||
integrity: "sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=",
|
||||
identity: "https://unpkg.com/leaflet@1.9.4/dist/leaflet.css",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
toHTML({
|
||||
pins,
|
||||
}: ExtractStructuredComponentArgumentsValues<
|
||||
typeof component_arguments
|
||||
>): FlatTemplatable {
|
||||
return (
|
||||
<div class="map-with-pins">
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
||||
crossorigin=""
|
||||
/>
|
||||
<script
|
||||
onload="loadMap()"
|
||||
src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
||||
crossorigin=""
|
||||
></script>
|
||||
{
|
||||
/* HTML */ `<script>
|
||||
function loadMap() {
|
||||
const mapDiv = document.getElementById("map");
|
||||
const resizeObserver = new ResizeObserver(() => {
|
||||
map.invalidateSize();
|
||||
});
|
||||
|
||||
resizeObserver.observe(mapDiv);
|
||||
var map = L.map("map");
|
||||
L.tileLayer(
|
||||
"https://tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||
{
|
||||
maxZoom: 19,
|
||||
attribution:
|
||||
'© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
||||
}
|
||||
).addTo(map);
|
||||
|
||||
const pins = ${JSON.stringify(pins)};
|
||||
pins.forEach((pin) => addPin(pin, map));
|
||||
}
|
||||
|
||||
function addPin(pin, map) {
|
||||
var pinIcon = L.icon({
|
||||
iconUrl: "/pin-icon.svg",
|
||||
iconSize: [29, 41],
|
||||
iconAnchor: [14, 40],
|
||||
popupAnchor: [-3, 14],
|
||||
});
|
||||
|
||||
var marker = L.marker(
|
||||
pin.coordinates.split(", ").map((x) => parseFloat(x)),
|
||||
{
|
||||
icon: pinIcon,
|
||||
}
|
||||
).addTo(map);
|
||||
|
||||
var popup = L.popup({
|
||||
closeButton: false,
|
||||
autoClose: false,
|
||||
closeOnEscapeKey: false,
|
||||
closeOnClick: false,
|
||||
className: "popup",
|
||||
offset: [0, -32],
|
||||
maxWidth: "auto",
|
||||
})
|
||||
.setLatLng(
|
||||
pin.coordinates.split(", ").map((x) => parseFloat(x))
|
||||
)
|
||||
.setContent(
|
||||
/* HTML */ \`<div class="popup-content">
|
||||
<p class="title">\${pin.title}</p>
|
||||
<p class="address">\${pin.address}</p>
|
||||
<a class="button" href="\${pin.button.link}">
|
||||
\${pin.button.text}
|
||||
</a>
|
||||
</div> \`
|
||||
)
|
||||
.addTo(map);
|
||||
map.setView(
|
||||
pin.coordinates.split(", ").map((x) => parseFloat(x)),
|
||||
13
|
||||
);
|
||||
}
|
||||
</script>`
|
||||
}
|
||||
|
||||
<div id="map"></div>
|
||||
</div>
|
||||
<div
|
||||
class="map-with-pins"
|
||||
data-controller="map-with-pins"
|
||||
data-map-with-pins-pins-value={JSON.stringify(pins)
|
||||
.replaceAll("\n", "\\n")
|
||||
.replaceAll('"', """)}
|
||||
></div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
101
src/back/jdd-components/map-with-pins/map-with-pins.stimulus.ts
Normal file
101
src/back/jdd-components/map-with-pins/map-with-pins.stimulus.ts
Normal file
@ -0,0 +1,101 @@
|
||||
import { Controller } from "stimulus";
|
||||
declare const L: any;
|
||||
|
||||
export default class MapWithPins extends Controller {
|
||||
id: string;
|
||||
map: any;
|
||||
initiated: boolean = false;
|
||||
resizeObserver: ResizeObserver;
|
||||
|
||||
static values = {
|
||||
pins: String,
|
||||
};
|
||||
|
||||
connect() {
|
||||
if (this.initiated) {
|
||||
this.map.remove();
|
||||
}
|
||||
if (window.L) {
|
||||
this.initiateMap();
|
||||
} else {
|
||||
document.addEventListener(
|
||||
"loaded-https://unpkg.com/leaflet@1.9.4/dist/leaflet.js",
|
||||
() => {
|
||||
this.initiateMap();
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.map.remove();
|
||||
this.initiated = false;
|
||||
this.resizeObserver.disconnect();
|
||||
}
|
||||
|
||||
initiateMap() {
|
||||
this.map = L.map(this.element);
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
this.map.invalidateSize();
|
||||
});
|
||||
|
||||
this.resizeObserver.observe(this.element);
|
||||
|
||||
L.tileLayer("https://tile.openstreetmap.org/{z}/{x}/{y}.png", {
|
||||
maxZoom: 19,
|
||||
attribution:
|
||||
'© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
||||
}).addTo(this.map);
|
||||
|
||||
const pins = JSON.parse(
|
||||
this.element.attributes["data-map-with-pins-pins-value"].value
|
||||
);
|
||||
pins.forEach((pin) => this.addPin(pin));
|
||||
this.initiated = true;
|
||||
}
|
||||
|
||||
pinsValueChanged() {
|
||||
if (this.initiated) {
|
||||
this.connect();
|
||||
}
|
||||
}
|
||||
|
||||
addPin(pin) {
|
||||
var pinIcon = L.icon({
|
||||
iconUrl: "/pin-icon.svg",
|
||||
iconSize: [29, 41],
|
||||
iconAnchor: [14, 40],
|
||||
popupAnchor: [-3, 14],
|
||||
});
|
||||
|
||||
var marker = L.marker(
|
||||
pin.coordinates.split(", ").map((x) => parseFloat(x)),
|
||||
{
|
||||
icon: pinIcon,
|
||||
}
|
||||
).addTo(this.map);
|
||||
|
||||
var popup = L.popup({
|
||||
closeButton: false,
|
||||
autoClose: false,
|
||||
closeOnEscapeKey: false,
|
||||
closeOnClick: false,
|
||||
className: "popup",
|
||||
offset: [0, -32],
|
||||
maxWidth: "auto",
|
||||
})
|
||||
.setLatLng(pin.coordinates.split(", ").map((x) => parseFloat(x)))
|
||||
.setContent(
|
||||
/* HTML */ `<div class="popup-content">
|
||||
<p class="title">${pin.title}</p>
|
||||
<p class="address">${pin.address}</p>
|
||||
<a class="button" href="${pin.button.link}"> ${pin.button.text} </a>
|
||||
</div> `
|
||||
)
|
||||
.addTo(this.map);
|
||||
this.map.setView(
|
||||
pin.coordinates.split(", ").map((x) => parseFloat(x)),
|
||||
13
|
||||
);
|
||||
}
|
||||
}
|
3
src/back/jdd-components/nice-box/nice-box.css
Normal file
3
src/back/jdd-components/nice-box/nice-box.css
Normal file
@ -0,0 +1,3 @@
|
||||
.nice-box {
|
||||
background-color: white;
|
||||
}
|
@ -0,0 +1,60 @@
|
||||
import { Controller } from "stimulus";
|
||||
|
||||
export default class ComponentDebugger extends Controller {
|
||||
id: string;
|
||||
main_form: HTMLFormElement;
|
||||
is_resizing: boolean = false;
|
||||
origin_x: number;
|
||||
origin_width: number;
|
||||
|
||||
connect() {
|
||||
this.main_form = document.querySelector("#component-debugger").closest("form");
|
||||
document.documentElement.addEventListener("ts-rebuilt", () => {
|
||||
this.main_form.requestSubmit();
|
||||
});
|
||||
this.main_form.addEventListener("turbo:submit-end", () => {
|
||||
// this clears the values of file inputs, so they don't get unecessarily
|
||||
// re-uploaded on future submissions - the file is alreade there on the server
|
||||
this.main_form
|
||||
.querySelectorAll("input[type=file]")
|
||||
.forEach((input: HTMLInputElement) => (input.value = ""));
|
||||
});
|
||||
|
||||
window.addEventListener("load", () => {
|
||||
this.update_width_display();
|
||||
});
|
||||
document.addEventListener("turbo:render", () => this.update_width_display());
|
||||
|
||||
const gutter = this.targets.find("gutter") as HTMLDivElement;
|
||||
gutter.addEventListener("mousedown", (e) => {
|
||||
this.is_resizing = true;
|
||||
this.origin_x = e.clientX;
|
||||
const resizable = this.targets.find("preview") as HTMLSpanElement;
|
||||
this.origin_width = resizable.getBoundingClientRect().width;
|
||||
const handler = this.resizeHandler.bind(this);
|
||||
document.addEventListener("mousemove", handler);
|
||||
document.addEventListener("mouseup", () => {
|
||||
document.removeEventListener("mousemove", handler);
|
||||
});
|
||||
e.preventDefault();
|
||||
});
|
||||
}
|
||||
|
||||
update_width_display() {
|
||||
const component_width = (this.targets.find("preview") as HTMLSpanElement)
|
||||
.offsetWidth;
|
||||
(
|
||||
this.targets.find("component-width") as HTMLSpanElement
|
||||
).innerHTML = `(width: ${component_width}px)`;
|
||||
}
|
||||
|
||||
resizeHandler(e: MouseEvent) {
|
||||
const width_offset = this.origin_x - e.clientX;
|
||||
|
||||
const new_width = Math.max(this.origin_width + width_offset, 1);
|
||||
document
|
||||
.getElementById("component-debugger")
|
||||
.style.setProperty("--resizable-column-width", new_width + "px");
|
||||
this.update_width_display();
|
||||
}
|
||||
}
|
@ -40,11 +40,7 @@ export async function ComponentInputTable<
|
||||
return (
|
||||
<fieldset>
|
||||
<legend>{arg_path.at(-1)}</legend>
|
||||
<div
|
||||
style={`max-width: calc(var(--resizable-column-width) - ${
|
||||
arg_path.length + 3
|
||||
} * 14px); overflow-x: auto; max-height: 500px; overflow-y: auto;`}
|
||||
>
|
||||
<div>
|
||||
<table style="position: relative; /* necessary for sticky th*/">
|
||||
<tbody>
|
||||
<tr>
|
||||
|
@ -5,7 +5,7 @@
|
||||
|
||||
.two-column {
|
||||
display: grid;
|
||||
grid-template-columns: min-content 15px 1fr;
|
||||
grid-template-columns: 1fr 15px min-content;
|
||||
}
|
||||
|
||||
.resize-gutter {
|
||||
@ -15,7 +15,6 @@
|
||||
}
|
||||
|
||||
.resizable {
|
||||
width: var(--resizable-column-width);
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
@ -24,6 +23,20 @@
|
||||
transform: scale(0.99);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.component-preview {
|
||||
width: var(--resizable-column-width);
|
||||
|
||||
& > fieldset {
|
||||
min-width: 0; /* default is min-content and that causes overflow*/
|
||||
max-height: calc(100vh - 75px);
|
||||
overflow-x: auto;
|
||||
}
|
||||
}
|
||||
.component-arguments {
|
||||
max-height: calc(100vh - 80px);
|
||||
overflow-y: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.component-preview-parameters {
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { render } from "@sealcode/jdd";
|
||||
import { StateAndMetadata, StatefulPage, to_base64 } from "@sealcode/sealgen";
|
||||
import { hasFieldOfType, hasShape, predicates } from "@sealcode/ts-predicates";
|
||||
import { render, renderEarlyAssets } from "@sealcode/jdd";
|
||||
import { StatefulPage, to_base64 } from "@sealcode/sealgen";
|
||||
import { hasShape, predicates } from "@sealcode/ts-predicates";
|
||||
import { BaseContext } from "koa";
|
||||
import { Templatable, TempstreamJSX } from "tempstream";
|
||||
import html from "../html.js";
|
||||
import { Templatable, tempstream, TempstreamJSX } from "tempstream";
|
||||
import html, { defaultHead } from "../html.js";
|
||||
import { registry } from "../jdd-components/components.js";
|
||||
import { ComponentInput } from "./component-preview/component-input.js";
|
||||
import { ComponentPreviewActions } from "./component-preview/component-preview-actions.js";
|
||||
@ -31,13 +31,33 @@ export default new (class ComponentsPage extends StatefulPage<
|
||||
return initial_state;
|
||||
}
|
||||
|
||||
wrapInLayout(ctx: BaseContext, content: Templatable): Templatable {
|
||||
return html(ctx, "Components", content, {
|
||||
morphing: false,
|
||||
preserveScroll: true,
|
||||
autoRefreshCSS: true,
|
||||
navbar: () => ``,
|
||||
});
|
||||
wrapInLayout(
|
||||
ctx: BaseContext,
|
||||
content: Templatable,
|
||||
state: ComponentPreviewState
|
||||
): Templatable {
|
||||
return html(
|
||||
ctx,
|
||||
"Components",
|
||||
content,
|
||||
{
|
||||
morphing: true,
|
||||
preserveScroll: true,
|
||||
autoRefreshCSS: true,
|
||||
navbar: () => ``,
|
||||
},
|
||||
(...args) =>
|
||||
tempstream`${defaultHead(...args)}${renderEarlyAssets(
|
||||
registry,
|
||||
[
|
||||
{
|
||||
component_name: state.component,
|
||||
args: state.component_args,
|
||||
},
|
||||
],
|
||||
jdd_context
|
||||
)}`
|
||||
);
|
||||
}
|
||||
|
||||
wrapInForm(state: ComponentPreviewState, content: Templatable): Templatable {
|
||||
@ -96,8 +116,9 @@ export default new (class ComponentsPage extends StatefulPage<
|
||||
class="two-column"
|
||||
id="component-debugger"
|
||||
style="--resizable-column-width: 50vw"
|
||||
data-controller="component-debugger"
|
||||
>
|
||||
<div class="resizable">
|
||||
<div class="component-arguments">
|
||||
{/*The below button has to be here in order for it to be the default behavior */}
|
||||
<input type="submit" value="Preview" />
|
||||
<select
|
||||
@ -135,11 +156,12 @@ export default new (class ComponentsPage extends StatefulPage<
|
||||
</fieldset>
|
||||
<code>{JSON.stringify(state)}</code>
|
||||
</div>
|
||||
<div class="resize-gutter"></div>
|
||||
<div class="component-preview">
|
||||
<div class="resize-gutter" data-component-debugger-target="gutter"></div>
|
||||
<div class="component-preview" data-component-debugger-target="preview">
|
||||
<fieldset>
|
||||
<legend>
|
||||
Preview <span id="component_width_span"></span>
|
||||
Preview{" "}
|
||||
<span data-component-debugger-target="component-width"></span>
|
||||
</legend>
|
||||
{render(
|
||||
registry,
|
||||
@ -155,75 +177,11 @@ export default new (class ComponentsPage extends StatefulPage<
|
||||
{
|
||||
/* HTML */ `<script>
|
||||
(function () {
|
||||
function update_width_display() {
|
||||
const component_width =
|
||||
document.getElementsByClassName(
|
||||
"component-preview"
|
||||
)[0].offsetWidth;
|
||||
document.getElementById(
|
||||
"component_width_span"
|
||||
).innerHTML = \`(width: \${component_width}px)\`;
|
||||
}
|
||||
window.addEventListener("load", (event) => {
|
||||
update_width_display();
|
||||
});
|
||||
document.addEventListener(
|
||||
"turbo:render",
|
||||
update_width_display
|
||||
);
|
||||
let is_resizing = false;
|
||||
let origin_x;
|
||||
let origin_width;
|
||||
const gutter = document.querySelector(".resize-gutter");
|
||||
const resizable = document.querySelector(".resizable");
|
||||
const move_listener = (e) => {
|
||||
const new_width = Math.max(
|
||||
origin_width + (e.clientX - origin_x),
|
||||
1
|
||||
);
|
||||
document
|
||||
.getElementById("component-debugger")
|
||||
.style.setProperty(
|
||||
"--resizable-column-width",
|
||||
new_width + "px"
|
||||
);
|
||||
update_width_display();
|
||||
};
|
||||
gutter.addEventListener("mousedown", (e) => {
|
||||
is_resizing = true;
|
||||
origin_x = e.clientX;
|
||||
origin_width =
|
||||
resizable.getBoundingClientRect().width;
|
||||
document.addEventListener("mousemove", move_listener);
|
||||
document.addEventListener("mouseup", () => {
|
||||
document.removeEventListener(
|
||||
"mousemove",
|
||||
move_listener
|
||||
);
|
||||
});
|
||||
e.preventDefault();
|
||||
});
|
||||
})();
|
||||
</script>`
|
||||
}
|
||||
</div>
|
||||
{
|
||||
/* HTML */ `<script>
|
||||
const main_form = document
|
||||
.querySelector("#component-debugger")
|
||||
.closest("form");
|
||||
document.documentElement.addEventListener("ts-rebuilt", () => {
|
||||
main_form.requestSubmit();
|
||||
});
|
||||
main_form.addEventListener("turbo:submit-end", () => {
|
||||
// this clears the values of file inputs, so they don't get unecessarily
|
||||
// re-uploaded on future submissions - the file is alreade there on the server
|
||||
main_form
|
||||
.querySelectorAll("input[type=file]")
|
||||
.forEach((input) => (input.value = ""));
|
||||
});
|
||||
</script>`
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
22
src/front/controllers.ts
Normal file
22
src/front/controllers.ts
Normal file
@ -0,0 +1,22 @@
|
||||
// DO NOT EDIT! This file is generated automaticaly with 'npm run generate-stimulus'
|
||||
|
||||
import * as Turbo from "@hotwired/turbo";
|
||||
import { Application } from "stimulus";
|
||||
const application = Application.start();
|
||||
|
||||
import { default as RefreshOnTsChanges } from "./../back/html/refresh-on-ts-changes.stimulus.js";
|
||||
application.register("refresh-on-ts-changes", RefreshOnTsChanges);
|
||||
|
||||
import { default as RefreshStyles } from "./../back/html/refresh-styles.stimulus.js";
|
||||
application.register("refresh-styles", RefreshStyles);
|
||||
|
||||
import { default as MapWithPins } from "./../back/jdd-components/map-with-pins/map-with-pins.stimulus.js";
|
||||
application.register("map-with-pins", MapWithPins);
|
||||
|
||||
import { default as ComponentDebugger } from "./../back/routes/component-preview/component-debugger.stimulus.js";
|
||||
application.register("component-debugger", ComponentDebugger);
|
||||
|
||||
import { default as InputImagePreview } from "./../back/routes/component-preview/input-image-preview.stimulus.js";
|
||||
application.register("input-image-preview", InputImagePreview);
|
||||
|
||||
export { Turbo };
|
@ -1,10 +1 @@
|
||||
import * as Turbo from "@hotwired/turbo";
|
||||
import { Application } from "stimulus";
|
||||
import InputImagePreview from "./controllers/input-image-preview";
|
||||
import TaskController from "./controllers/task-controller";
|
||||
|
||||
export { Turbo };
|
||||
|
||||
const application = Application.start();
|
||||
application.register("task", TaskController);
|
||||
application.register("input-image-preview", InputImagePreview);
|
||||
export * from "./controllers.js";
|
||||
|
@ -3,6 +3,7 @@
|
||||
@import "../node_modules/@sealcode/sealgen/src/forms/forms.css";
|
||||
@import "back/jdd-components/image-demo/image-demo.css";
|
||||
@import "back/jdd-components/map-with-pins/map-with-pins.css";
|
||||
@import "back/jdd-components/nice-box/nice-box.css";
|
||||
@import "back/jdd-components/table/table.css";
|
||||
@import "back/routes/common/ui/input.css";
|
||||
@import "back/routes/components.css";
|
||||
|
@ -8,7 +8,7 @@
|
||||
"target": "ES2019",
|
||||
"esModuleInterop": true,
|
||||
"lib": ["es2021"],
|
||||
"outDir": "../../dist/back",
|
||||
"outDir": "./dist/back",
|
||||
"keyofStringsOnly": true,
|
||||
"jsx": "react",
|
||||
"reactNamespace": "TempstreamJSX",
|
||||
@ -19,6 +19,7 @@
|
||||
"skipLibCheck": true,
|
||||
"types": ["vitest/globals"]
|
||||
},
|
||||
"include": ["./**/*", "./*"],
|
||||
"include": ["./src/back/*", "./src/back/**/*"],
|
||||
"exclude": ["./src/front", "./src/**/*.stimulus.ts"],
|
||||
"ts-node": { "experimentalResolver": true, "esm": true }
|
||||
}
|
@ -5,5 +5,5 @@
|
||||
"lib": ["dom"],
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["./**/*", "./index.ts"]
|
||||
"include": ["./src/front", "./src/**/*.stimulus.ts"]
|
||||
}
|
6
tsconfig.json
Normal file
6
tsconfig.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"references": [
|
||||
{ "path": "./tsconfig-front.json" },
|
||||
{ "path": "./tsconfig-back.json" }
|
||||
]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user