ScrapHTMLEditor@0.2.0
実装
2023-01-16 00:02:17 2時間使って書いたのに、結局動かなかったので箒
code:mod.tsx
/// <reference no-default-lib="true"/>
/// <reference lib="esnext"/>
/// <reference lib="dom"/>
/** @jsx h */
import { render, h } from "../preact/mod.tsx";
import { App } from "./App.tsx";
export const setup = () => {
const app = document.createElement("div");
const shadowRoot = app.attachShadow({ mode: "open" });
document.body.append(app);
render(<App delay={500} />, shadowRoot);
return () => {
render(null, shadowRoot);
app.remove();
};
};
code:App.tsx
/// <reference no-default-lib="true"/>
/// <reference lib="esnext"/>
/// <reference lib="dom"/>
/** @jsx h */
/** @jsxFrag Fragment */
import { Fragment, h } from "../preact/mod.tsx";
import { useCallback, useEffect, useMemo, useRef, useState } from "../preact/hooks.ts";
import { useFiles } from "./useFiles.ts";
import { getLineDOM } from "../scrapbox-userscript-std/dom.ts";
interface File {
lineIds: string[];
tab: WindowProxy | null;
url: string;
content: string;
open: () => void;
};
export interface AppProps {
delay: number;
}
export const App = (props: AppProps) => {
const fileMap = useFiles(props.delay);
useEffect(() => {
setFiles((draft) => {
const content = file.blocks.flatMap((block) => block.lines).join("\n");
const prev = draft.get(fileName);
if (content === prev?.content) continue;
if (prev?.url) URL.revokeObjectURL(prev.url);
const url = URL.createObjectURL(
new Blob(content, { type: "text/html" }) );
let tab = prev?.tab ?? null;
if (tab) {
tab.location.href = url;
}
draft.set(fileName, {
lineIds: file.blocks.map((block) => block.startId),
url,
content,
tab,
open: () => {
tab?.focus?.();
tab ??= window.open(url);
},
});
fileNames.delete(fileName);
}
for (const fileName of fileNames) {
const file = draft.get(fileName);
if (!file) continue;
URL.revokeObjectURL(file.url);
file.tab?.close?.();
draft.delete(fileName);
}
return new Map(draft);
});
const buttonData = useMemo(
(lineId) => ({ fileName, lineId, open: file.open })
)
),
);
return (<>
<style>{
`.run {
position: absolute;
}
button {
background: unset;
color: unset;
border: unset;
cursor: pointer;
}`
}</style>
{buttonData.map((button) => <Button {...button} />)}
</>);
};
interface ButtonProps {
fileName: string;
lineId: string;
open: () => void;
};
const Button = ({ fileName, lineId, open }: ButtonProps) => {
const ref = useRef<HTMLDivElement>(null);
const position = useMemo(() => {
const lineDOM = getLineDOM(lineId);
if (!lineDOM || !ref.current) return "display: none;";
const rect = lineDOM.getBoundingClientRect();
const pRect = ref.current.parentElement?.getBoundingClientRect?.();
if (!pRect) return "display: none;";
return top: ${rect.top - pRect.top}px; left: ${rect.left + 10 - pRect.left}px;;
return (
<div ref={ref} className="run" style={position}>
<button onClick={open} title={fileName}><i className="kamon kamon-play" /></button>
</div>
);
};
code:useFiles.ts
import { useState, useEffect } from "../preact/hooks.ts";
import { extractCodeFiles, CodeFile } from "../scrapbox-userscript-std/dom.ts";
import { Line, Scrapbox } from "../scrapbox-jp%2Ftypes/userscript.ts";
declare const scrapbox: Scrapbox;
export const useFiles = (delay = 100): Map<string, CodeFile> => {
useEffect(() => {
const update = () => {
setFiles((draft) => {
if (
scrapbox.Layout !== "page" ||
) {
}
const files = extractCodeFiles(scrapbox.Page.lines);
if (file.lang === "html") continue;
files.delete(key);
}
return files;
});
};
let timer: number | undefined;
const callback = () => {
clearTimeout(timer);
timer = setTimeout(() => update(), delay);
};
update();
scrapbox.addListener("lines:changed", callback);
return () => scrapbox.removeListener("lines:changed", callback);
return files;
};
code:index.html
<!DOCTYPE html>
<html>
<head>
<title>Hello, world from Scrapbox!</title>
<meta charset="utf-8"></meta>
</head>
<body>
<h1>Heading 1</h1>
<p>
てすとてすと
</p>
</body>
</html>