fix(lit-components): fix rendering and interaction issues in force-graph, html-table, and draggable-dom
Build and Deploy / build-and-deploy (push) Has been cancelled
Build and Deploy / build-and-deploy (push) Has been cancelled
- lit-force-graph: Refactored for instance reuse, better lifecycle management, and added dynamic imports for AR/VR modes. - lit-html-table: Improved visual design, readability of headers, and fixed reactivity in editable mode. - lit-draggable-dom: Fixed clipping issues by switching to relative/absolute positioning and resolved a panning speed bug caused by CSS variable inheritance.
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
import { html, css, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
|
||||
import "@material/mwc-icon-button";
|
||||
import "@material/mwc-icon";
|
||||
|
||||
@customElement("rich-action")
|
||||
export class RichAction extends LitElement {
|
||||
static styles = css`
|
||||
* {
|
||||
--mdc-icon-size: var(--icon-size);
|
||||
--mdc-icon-button-size: var(--icon-size);
|
||||
}
|
||||
section {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
section * {
|
||||
margin: 2px;
|
||||
}
|
||||
mwc-icon-button[active] {
|
||||
color: var(--rich-action-active-color);
|
||||
}
|
||||
mwc-icon {
|
||||
cursor: pointer;
|
||||
}
|
||||
`;
|
||||
|
||||
@property({ type: String }) command = "";
|
||||
@property({ type: String }) value?: string;
|
||||
@property({ type: String }) icon = "info";
|
||||
@property({ type: Boolean }) active = false;
|
||||
@property({ type: Array, hasChanged: () => true }) values: Option[] = [];
|
||||
|
||||
render() {
|
||||
const { icon, command, value, active, values } = this;
|
||||
const hasItems = values.length > 0;
|
||||
return html`<section>
|
||||
${hasItems
|
||||
? html` <mwc-icon>${icon}</mwc-icon>
|
||||
<select
|
||||
@change=${(e: Event) => {
|
||||
const event = e as CustomEvent;
|
||||
const select = event.target as HTMLSelectElement;
|
||||
const selectedValue = select.value;
|
||||
if (selectedValue === "--") {
|
||||
editorCommand("removeFormat", undefined);
|
||||
} else {
|
||||
editorCommand(command, selectedValue);
|
||||
}
|
||||
}}
|
||||
>
|
||||
${values.map(
|
||||
(v) =>
|
||||
html`<option value=${v.value} ?selected=${v.value === value}>
|
||||
${v.name}
|
||||
</option>`
|
||||
)}
|
||||
</select>`
|
||||
: html`<mwc-icon-button
|
||||
?active="${active}"
|
||||
icon=${icon}
|
||||
@click=${() => {
|
||||
if (command) {
|
||||
editorCommand(command, value);
|
||||
} else {
|
||||
this.dispatchEvent(
|
||||
new Event("action", {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
}}
|
||||
></mwc-icon-button>`}
|
||||
<div><slot></slot></div>
|
||||
</section>`;
|
||||
}
|
||||
}
|
||||
|
||||
interface Option {
|
||||
name: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export function editorCommand(command: string, value?: string) {
|
||||
document.execCommand(command, true, value);
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"rich-action": RichAction;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import { html, css, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators.js";
|
||||
import { live } from "../utils/live";
|
||||
|
||||
import "./rich-toolbar";
|
||||
import "./rich-viewer";
|
||||
|
||||
@customElement("rich-text")
|
||||
export class RichText extends LitElement {
|
||||
static styles = css`
|
||||
:host {
|
||||
--rich-color: black;
|
||||
--rich-background: white;
|
||||
--rich-action-active-color: red;
|
||||
--icon-size: 24px;
|
||||
--rich-padding: 8px;
|
||||
}
|
||||
main {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-rows: 1fr auto;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-areas:
|
||||
"viewer"
|
||||
"toolbar";
|
||||
}
|
||||
|
||||
rich-toolbar {
|
||||
grid-area: toolbar;
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
background-color: var(--rich-background);
|
||||
color: var(--rich-color);
|
||||
border-top: 1px solid var(--rich-color);
|
||||
}
|
||||
|
||||
rich-viewer {
|
||||
grid-area: viewer;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
overflow-y: auto;
|
||||
background-color: var(--rich-background);
|
||||
color: var(--rich-color);
|
||||
}
|
||||
|
||||
/* @media (hover: hover) and (pointer: fine) {
|
||||
main {
|
||||
grid-template-rows: auto 1fr;
|
||||
grid-template-areas:
|
||||
"toolbar"
|
||||
"viewer";
|
||||
}
|
||||
rich-toolbar {
|
||||
border-top: none;
|
||||
border-bottom: 1px solid var(--rich-color);
|
||||
}
|
||||
} */
|
||||
main {
|
||||
grid-template-rows: auto 1fr;
|
||||
grid-template-areas:
|
||||
"toolbar"
|
||||
"viewer";
|
||||
}
|
||||
rich-toolbar {
|
||||
border-top: none;
|
||||
border-bottom: 1px solid var(--rich-color);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:host {
|
||||
--rich-background: black;
|
||||
--rich-color: white;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@live selection: Selection | null = null;
|
||||
@property({ type: Boolean }) readonly = false;
|
||||
@property({ type: Object, hasChanged: () => true }) node: Element =
|
||||
document.createElement("div");
|
||||
|
||||
render() {
|
||||
const { selection, readonly, node } = this;
|
||||
return html`<main>
|
||||
<rich-toolbar
|
||||
.selection=${selection}
|
||||
.node=${node}
|
||||
@set-content=${(e: Event) => {
|
||||
const event = e as CustomEvent<string>;
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(event.detail, "text/html");
|
||||
const root = doc.querySelector("body");
|
||||
this.node.innerHTML = root?.innerHTML ?? "";
|
||||
this.requestUpdate();
|
||||
}}
|
||||
></rich-toolbar>
|
||||
<rich-viewer
|
||||
?readonly=${readonly}
|
||||
@selection=${(e: Event) => {
|
||||
const event = e as CustomEvent;
|
||||
this.selection = event.detail;
|
||||
}}
|
||||
.node=${node}
|
||||
>
|
||||
</rich-viewer>
|
||||
</main>`;
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
const children = this.children;
|
||||
if (children.length > 0) {
|
||||
// Check if <template> is the first child
|
||||
const template = children[0];
|
||||
if (template.tagName === "TEMPLATE") {
|
||||
const content = template.innerHTML.trim();
|
||||
if (content.length > 0) {
|
||||
this.node.innerHTML = content;
|
||||
this.requestUpdate();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"rich-text": RichText;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
import { html, css, LitElement } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators.js";
|
||||
import { checkFonts } from "../utils/check-fonts";
|
||||
import { live } from "../utils/live";
|
||||
|
||||
import "./rich-action";
|
||||
import { editorCommand } from "./rich-action";
|
||||
|
||||
@customElement("rich-toolbar")
|
||||
export class RichToolbar extends LitElement {
|
||||
static styles = css`
|
||||
header {
|
||||
width: 100%;
|
||||
color: var(--rich-color);
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
input[type="color"] {
|
||||
-webkit-appearance: none;
|
||||
border: none;
|
||||
width: var(--icon-size);
|
||||
height: var(--icon-size);
|
||||
}
|
||||
input[type="color"]::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
input[type="color"]::-webkit-color-swatch {
|
||||
border: none;
|
||||
}
|
||||
`;
|
||||
|
||||
@live selection: Selection | null = null;
|
||||
@query("#fg-color") fgColorInput!: HTMLInputElement;
|
||||
@query("#bd-color") bdColorInput!: HTMLInputElement;
|
||||
@state() fileHandle?: any;
|
||||
@property({ type: Object, hasChanged: () => true }) node!: Element;
|
||||
|
||||
render() {
|
||||
const tags = this.getTags();
|
||||
return html`<header>
|
||||
<rich-action icon="format_clear" command="removeFormat"></rich-action>
|
||||
<rich-action
|
||||
icon="format_bold"
|
||||
command="bold"
|
||||
?active=${tags.includes("b")}
|
||||
></rich-action>
|
||||
<rich-action
|
||||
icon="format_italic"
|
||||
command="italic"
|
||||
?active=${tags.includes("i")}
|
||||
></rich-action>
|
||||
<rich-action
|
||||
icon="format_underlined"
|
||||
command="underline"
|
||||
?active=${tags.includes("u")}
|
||||
></rich-action>
|
||||
<rich-action icon="format_align_left" command="justifyleft"></rich-action>
|
||||
<rich-action
|
||||
icon="format_align_center"
|
||||
command="justifycenter"
|
||||
></rich-action>
|
||||
<rich-action
|
||||
icon="format_align_right"
|
||||
command="justifyright"
|
||||
></rich-action>
|
||||
<rich-action
|
||||
icon="format_list_numbered"
|
||||
command="insertorderedlist"
|
||||
?active=${tags.includes("ol")}
|
||||
></rich-action>
|
||||
<rich-action
|
||||
icon="format_list_bulleted"
|
||||
command="insertunorderedlist"
|
||||
?active=${tags.includes("ul")}
|
||||
></rich-action>
|
||||
<rich-action
|
||||
icon="format_quote"
|
||||
command="formatblock"
|
||||
value="blockquote"
|
||||
></rich-action>
|
||||
<rich-action
|
||||
icon="format_indent_decrease"
|
||||
command="outdent"
|
||||
></rich-action>
|
||||
<rich-action icon="format_indent_increase" command="indent"></rich-action>
|
||||
<rich-action
|
||||
icon="add_link"
|
||||
?active=${tags.includes("a")}
|
||||
@action=${() => {
|
||||
const newLink = prompt("Write the URL here", "https://");
|
||||
// Check if valid url
|
||||
if (newLink && newLink.match(/^(http|https):\/\/[^ "]+$/)) {
|
||||
editorCommand("createlink", newLink);
|
||||
}
|
||||
}}
|
||||
>
|
||||
</rich-action>
|
||||
<rich-action
|
||||
icon="link_off"
|
||||
?active=${tags.includes("a")}
|
||||
command="unlink"
|
||||
>
|
||||
</rich-action>
|
||||
<rich-action
|
||||
icon="format_color_text"
|
||||
@action=${() => this.fgColorInput.click()}
|
||||
>
|
||||
<input
|
||||
type="color"
|
||||
id="fg-color"
|
||||
@input=${(e: Event) => {
|
||||
const input = e.target as HTMLInputElement;
|
||||
editorCommand("forecolor", input.value);
|
||||
}}
|
||||
/>
|
||||
</rich-action>
|
||||
<rich-action
|
||||
icon="border_color"
|
||||
@action=${() => this.bdColorInput.click()}
|
||||
>
|
||||
<input
|
||||
type="color"
|
||||
id="bd-color"
|
||||
@input=${(e: Event) => {
|
||||
const input = e.target as HTMLInputElement;
|
||||
editorCommand("backcolor", input.value);
|
||||
}}
|
||||
/>
|
||||
</rich-action>
|
||||
<rich-action
|
||||
icon="title"
|
||||
command="formatblock"
|
||||
.values=${[
|
||||
{ name: "Normal Text", value: "--" },
|
||||
{ name: "Heading 1", value: "h1" },
|
||||
{ name: "Heading 2", value: "h2" },
|
||||
{ name: "Heading 3", value: "h3" },
|
||||
{ name: "Heading 4", value: "h4" },
|
||||
{ name: "Heading 5", value: "h5" },
|
||||
{ name: "Heading 6", value: "h6" },
|
||||
{ name: "Paragraph", value: "p" },
|
||||
{ name: "Pre-Formatted", value: "pre" },
|
||||
]}
|
||||
></rich-action>
|
||||
<rich-action
|
||||
icon="text_format"
|
||||
command="fontname"
|
||||
.values=${[
|
||||
{ name: "Font Name", value: "--" },
|
||||
...Array.from(checkFonts()).map((font) => ({
|
||||
name: font,
|
||||
value: font,
|
||||
})),
|
||||
]}
|
||||
></rich-action>
|
||||
<rich-action
|
||||
icon="format_size"
|
||||
command="fontsize"
|
||||
.values=${[
|
||||
{ name: "Font Size", value: "--" },
|
||||
{ name: "Very Small", value: "1" },
|
||||
{ name: "Small", value: "2" },
|
||||
{ name: "Normal", value: "3" },
|
||||
{ name: "Medium Large", value: "4" },
|
||||
{ name: "Large", value: "5" },
|
||||
{ name: "Very Large", value: "6" },
|
||||
{ name: "Maximum", value: "7" },
|
||||
]}
|
||||
></rich-action>
|
||||
<rich-action icon="undo" command="undo"></rich-action>
|
||||
<rich-action icon="redo" command="redo"></rich-action>
|
||||
<rich-action icon="content_cut" command="cut"></rich-action>
|
||||
<rich-action icon="content_copy" command="copy"></rich-action>
|
||||
<rich-action icon="content_paste" command="paste"></rich-action>
|
||||
<rich-action
|
||||
icon="file_upload"
|
||||
@action=${async () => {
|
||||
if ("showOpenFilePicker" in window) {
|
||||
// File system api
|
||||
// @ts-ignore
|
||||
const [fileHandle] = await window.showOpenFilePicker();
|
||||
this.fileHandle = fileHandle;
|
||||
if (fileHandle) {
|
||||
const file = await fileHandle.getFile();
|
||||
const contents = await file.text();
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("set-content", {
|
||||
detail: contents,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Fallback to input
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.click();
|
||||
input.onchange = async () => {
|
||||
const file = input.files![0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const data = reader.result as string;
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("set-content", {
|
||||
detail: data,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
};
|
||||
}
|
||||
}}
|
||||
></rich-action>
|
||||
<rich-action
|
||||
icon="file_download"
|
||||
@action=${async () => {
|
||||
const contents = this.node.innerHTML;
|
||||
if (this.fileHandle) {
|
||||
const writable = await this.fileHandle.createWritable();
|
||||
await writable.write(
|
||||
[
|
||||
`<!DOCTYPE html>`,
|
||||
`<html lang="en">`,
|
||||
` <head> </head>`,
|
||||
` <body>${contents}</body>`,
|
||||
`</html>`,
|
||||
].join("\n")
|
||||
);
|
||||
await writable.close();
|
||||
} else {
|
||||
// Download file
|
||||
const url = window.URL.createObjectURL(
|
||||
new Blob([contents], { type: "text/html" })
|
||||
);
|
||||
const link = document.createElement("a");
|
||||
link.href = url;
|
||||
link.download = "index.html";
|
||||
link.click();
|
||||
}
|
||||
}}
|
||||
></rich-action>
|
||||
</header>`;
|
||||
}
|
||||
|
||||
getTags() {
|
||||
const { selection } = this;
|
||||
let tags: string[] = [];
|
||||
if (selection) {
|
||||
if (selection.type === "Range") {
|
||||
// @ts-ignore
|
||||
let parentNode = selection?.baseNode;
|
||||
if (parentNode) {
|
||||
const checkNode = () => {
|
||||
const tag = parentNode?.tagName?.toLowerCase()?.trim();
|
||||
if (tag) tags.push(tag);
|
||||
};
|
||||
while (parentNode != null) {
|
||||
checkNode();
|
||||
parentNode = parentNode?.parentNode;
|
||||
}
|
||||
}
|
||||
// Remove root tag
|
||||
tags.pop();
|
||||
} else {
|
||||
const content = this.selection?.toString() || "";
|
||||
tags = (content.match(/<[^>]+>/g) || [])
|
||||
.filter((tag) => !tag.startsWith("</"))
|
||||
.map((tag) => tag.replace(/<|>/g, ""));
|
||||
}
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"rich-toolbar": RichToolbar;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import { html, css, LitElement } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators.js";
|
||||
|
||||
@customElement("rich-viewer")
|
||||
export class RichViewer extends LitElement {
|
||||
static styles = css`
|
||||
article {
|
||||
padding: var(--rich-padding);
|
||||
width: calc(100% - var(--rich-padding) * 2);
|
||||
height: calc(100% - var(--rich-padding) * 2);
|
||||
}
|
||||
|
||||
article[contenteditable="true"] {
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
`;
|
||||
|
||||
@query("#content") content!: HTMLDivElement;
|
||||
@property({ type: Boolean }) readonly = false;
|
||||
@property({ type: Object, hasChanged: () => true }) node!: Element;
|
||||
|
||||
render() {
|
||||
const { readonly, node } = this;
|
||||
return html`<article
|
||||
id="content"
|
||||
contenteditable=${readonly ? "false" : "true"}
|
||||
@input=${() => this.updateSelection()}
|
||||
>
|
||||
${node}
|
||||
</article>`;
|
||||
}
|
||||
|
||||
updateSelection() {
|
||||
// @ts-ignore
|
||||
const shadowSelection = this.shadowRoot?.getSelection
|
||||
? // @ts-ignore
|
||||
this.shadowRoot!.getSelection()
|
||||
: null;
|
||||
const selection =
|
||||
shadowSelection || document.getSelection() || window.getSelection();
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("selection", {
|
||||
detail: selection,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
firstUpdated() {
|
||||
document.execCommand("defaultParagraphSeparator", false, "br");
|
||||
document.addEventListener("selectionchange", () => {
|
||||
this.updateSelection();
|
||||
});
|
||||
window.addEventListener("selectionchange", () => {
|
||||
this.updateSelection();
|
||||
});
|
||||
document.addEventListener("keydown", () => {
|
||||
this.updateSelection();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"rich-viewer": RichViewer;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import "./components/rich-text";
|
||||
import "./components/rich-viewer";
|
||||
|
||||
describe("Lit HTML Editor Components", () => {
|
||||
test("registers rich markdown/HTML custom elements successfully", () => {
|
||||
expect(customElements.get("rich-text")).toBeDefined();
|
||||
expect(customElements.get("rich-viewer")).toBeDefined();
|
||||
});
|
||||
|
||||
test("mounts rich-viewer correctly and accepts markdown content", async () => {
|
||||
document.body.innerHTML = '<rich-viewer value="# Hello World"></rich-viewer>';
|
||||
await window.happyDOM.whenAsyncComplete();
|
||||
|
||||
const element = document.body.querySelector("rich-viewer");
|
||||
expect(element).toBeDefined();
|
||||
expect(element?.getAttribute("value")).toBe("# Hello World");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './components/rich-text';
|
||||
export * from './components/rich-action';
|
||||
export * from './components/rich-toolbar';
|
||||
export * from './components/rich-viewer';
|
||||
@@ -0,0 +1,138 @@
|
||||
export function checkFonts(): string[] {
|
||||
const fontCheck = new Set(
|
||||
[
|
||||
// Windows 10
|
||||
"Arial",
|
||||
"Arial Black",
|
||||
"Bahnschrift",
|
||||
"Calibri",
|
||||
"Cambria",
|
||||
"Cambria Math",
|
||||
"Candara",
|
||||
"Comic Sans MS",
|
||||
"Consolas",
|
||||
"Constantia",
|
||||
"Corbel",
|
||||
"Courier New",
|
||||
"Ebrima",
|
||||
"Franklin Gothic Medium",
|
||||
"Gabriola",
|
||||
"Gadugi",
|
||||
"Georgia",
|
||||
"HoloLens MDL2 Assets",
|
||||
"Impact",
|
||||
"Ink Free",
|
||||
"Javanese Text",
|
||||
"Leelawadee UI",
|
||||
"Lucida Console",
|
||||
"Lucida Sans Unicode",
|
||||
"Malgun Gothic",
|
||||
"Marlett",
|
||||
"Microsoft Himalaya",
|
||||
"Microsoft JhengHei",
|
||||
"Microsoft New Tai Lue",
|
||||
"Microsoft PhagsPa",
|
||||
"Microsoft Sans Serif",
|
||||
"Microsoft Tai Le",
|
||||
"Microsoft YaHei",
|
||||
"Microsoft Yi Baiti",
|
||||
"MingLiU-ExtB",
|
||||
"Mongolian Baiti",
|
||||
"MS Gothic",
|
||||
"MV Boli",
|
||||
"Myanmar Text",
|
||||
"Nirmala UI",
|
||||
"Palatino Linotype",
|
||||
"Segoe MDL2 Assets",
|
||||
"Segoe Print",
|
||||
"Segoe Script",
|
||||
"Segoe UI",
|
||||
"Segoe UI Historic",
|
||||
"Segoe UI Emoji",
|
||||
"Segoe UI Symbol",
|
||||
"SimSun",
|
||||
"Sitka",
|
||||
"Sylfaen",
|
||||
"Symbol",
|
||||
"Tahoma",
|
||||
"Times New Roman",
|
||||
"Trebuchet MS",
|
||||
"Verdana",
|
||||
"Webdings",
|
||||
"Wingdings",
|
||||
"Yu Gothic",
|
||||
// macOS
|
||||
"American Typewriter",
|
||||
"Andale Mono",
|
||||
"Arial",
|
||||
"Arial Black",
|
||||
"Arial Narrow",
|
||||
"Arial Rounded MT Bold",
|
||||
"Arial Unicode MS",
|
||||
"Avenir",
|
||||
"Avenir Next",
|
||||
"Avenir Next Condensed",
|
||||
"Baskerville",
|
||||
"Big Caslon",
|
||||
"Bodoni 72",
|
||||
"Bodoni 72 Oldstyle",
|
||||
"Bodoni 72 Smallcaps",
|
||||
"Bradley Hand",
|
||||
"Brush Script MT",
|
||||
"Chalkboard",
|
||||
"Chalkboard SE",
|
||||
"Chalkduster",
|
||||
"Charter",
|
||||
"Cochin",
|
||||
"Comic Sans MS",
|
||||
"Copperplate",
|
||||
"Courier",
|
||||
"Courier New",
|
||||
"Didot",
|
||||
"DIN Alternate",
|
||||
"DIN Condensed",
|
||||
"Futura",
|
||||
"Geneva",
|
||||
"Georgia",
|
||||
"Gill Sans",
|
||||
"Helvetica",
|
||||
"Helvetica Neue",
|
||||
"Herculanum",
|
||||
"Hoefler Text",
|
||||
"Impact",
|
||||
"Lucida Grande",
|
||||
"Luminari",
|
||||
"Marker Felt",
|
||||
"Menlo",
|
||||
"Microsoft Sans Serif",
|
||||
"Monaco",
|
||||
"Noteworthy",
|
||||
"Optima",
|
||||
"Palatino",
|
||||
"Papyrus",
|
||||
"Phosphate",
|
||||
"Rockwell",
|
||||
"Savoye LET",
|
||||
"SignPainter",
|
||||
"Skia",
|
||||
"Snell Roundhand",
|
||||
"Tahoma",
|
||||
"Times",
|
||||
"Times New Roman",
|
||||
"Trattatello",
|
||||
"Trebuchet MS",
|
||||
"Verdana",
|
||||
"Zapfino",
|
||||
].sort()
|
||||
);
|
||||
const fontAvailable = new Set<string>();
|
||||
// @ts-ignore
|
||||
for (const font of fontCheck.values()) {
|
||||
// @ts-ignore
|
||||
if (document.fonts.check(`12px "${font}"`)) {
|
||||
fontAvailable.add(font);
|
||||
}
|
||||
}
|
||||
// @ts-ignore
|
||||
return fontAvailable.values();
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { property } from "lit/decorators.js";
|
||||
|
||||
export const live = property({ type: Object, hasChanged: () => true });
|
||||
Reference in New Issue
Block a user