Anki/ts/lib/tslib/dom.ts
Damien Elmes 9f55cf26fc
Switch to SvelteKit (#3077)
* Update to latest Node LTS

* Add sveltekit

* Split tslib into separate @generated and @tslib components

SvelteKit's path aliases don't support multiple locations, so our old
approach of using @tslib to refer to both ts/lib and out/ts/lib will no
longer work. Instead, all generated sources and their includes are
placed in a separate out/ts/generated folder, and imported via @generated
instead. This also allows us to generate .ts files, instead of needing
to output separate .d.ts and .js files.

* Switch package.json to module type

* Avoid usage of baseUrl

Incompatible with SvelteKit

* Move sass into ts; use relative links

SvelteKit's default sass support doesn't allow overriding loadPaths

* jest->vitest, graphs example working with yarn dev

* most pages working in dev mode

* Some fixes after rebasing

* Fix/silence some svelte-check errors

* Get image-occlusion working with Fabric types

* Post-rebase lock changes

* Editor is now checked

* SvelteKit build integrated into ninja

* Use the new SvelteKit entrypoint for pages like congrats/deck options/etc

* Run eslint once for ts/**; fix some tests

* Fix a bunch of issues introduced when rebasing over latest main

* Run eslint fix

* Fix remaining eslint+pylint issues; tests now all pass

* Fix some issues with a clean build

* Latest bufbuild no longer requires @__PURE__ hack

* Add a few missed dependencies

* Add yarn.bat to fix Windows build

* Fix pages failing to show when ANKI_API_PORT not defined

* Fix svelte-check and vitest on Windows

* Set node path in ./yarn

* Move svelte-kit output to ts/.svelte-kit

Sadly, I couldn't figure out a way to store it in out/ if out/ is
a symlink, as it breaks module resolution when SvelteKit is run.

* Allow HMR inside Anki

* Skip SvelteKit build when HMR is defined

* Fix some post-rebase issues

I should have done a normal merge instead.
2024-03-31 09:16:31 +01:00

152 lines
3.8 KiB
TypeScript

// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { getSelection } from "./cross-browser";
export function nodeIsElement(node: Node): node is Element {
return node.nodeType === Node.ELEMENT_NODE;
}
/**
* In the web this is probably equivalent to `nodeIsElement`, but this is
* convenient to convince Typescript.
*/
export function nodeIsCommonElement(node: Node): node is HTMLElement | SVGElement {
return node instanceof HTMLElement || node instanceof SVGElement;
}
export function nodeIsText(node: Node): node is Text {
return node.nodeType === Node.TEXT_NODE;
}
export function nodeIsComment(node: Node): node is Comment {
return node.nodeType === Node.COMMENT_NODE;
}
// https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements
export const BLOCK_ELEMENTS = [
"ADDRESS",
"ARTICLE",
"ASIDE",
"BLOCKQUOTE",
"DETAILS",
"DIALOG",
"DD",
"DIV",
"DL",
"DT",
"FIELDSET",
"FIGCAPTION",
"FIGURE",
"FOOTER",
"FORM",
"H1",
"H2",
"H3",
"H4",
"H5",
"H6",
"HEADER",
"HGROUP",
"HR",
"LI",
"MAIN",
"NAV",
"OL",
"P",
"PRE",
"SECTION",
"TABLE",
"UL",
];
export function hasBlockAttribute(element: Element): boolean {
return element.hasAttribute("block") && element.getAttribute("block") !== "false";
}
export function elementIsBlock(element: Element): boolean {
return BLOCK_ELEMENTS.includes(element.tagName) || hasBlockAttribute(element);
}
export const NO_SPLIT_TAGS = ["RUBY"];
export function elementShouldNotBeSplit(element: Element): boolean {
return elementIsBlock(element) || NO_SPLIT_TAGS.includes(element.tagName);
}
// https://developer.mozilla.org/en-US/docs/Glossary/Empty_element
export const EMPTY_ELEMENTS = [
"AREA",
"BASE",
"BR",
"COL",
"EMBED",
"HR",
"IMG",
"INPUT",
"LINK",
"META",
"PARAM",
"SOURCE",
"TRACK",
"WBR",
];
export function elementIsEmpty(element: Element): boolean {
return EMPTY_ELEMENTS.includes(element.tagName);
}
export function nodeContainsInlineContent(node: Node): boolean {
for (const child of node.childNodes) {
if (
(nodeIsElement(child) && elementIsBlock(child))
|| !nodeContainsInlineContent(child)
) {
return false;
}
}
// empty node is trivially inline
return true;
}
/**
* Consumes the input fragment.
*/
export function fragmentToString(fragment: DocumentFragment): string {
const fragmentDiv = document.createElement("div");
fragmentDiv.appendChild(fragment);
const html = fragmentDiv.innerHTML;
return html;
}
const getAnchorParent =
<T extends Element>(predicate: (element: Element) => element is T) => (root: Node): T | null => {
const anchor = getSelection(root)?.anchorNode;
if (!anchor) {
return null;
}
let anchorParent: T | null = null;
let element = nodeIsElement(anchor) ? anchor : anchor.parentElement;
while (element) {
anchorParent = anchorParent || (predicate(element) ? element : null);
element = element.parentElement;
}
return anchorParent;
};
const isListItem = (element: Element): element is HTMLLIElement =>
window.getComputedStyle(element).display === "list-item";
const isParagraph = (element: Element): element is HTMLParamElement => element.tagName === "P";
const isBlockElement = (
element: Element,
): element is HTMLLIElement & HTMLParamElement => isListItem(element) || isParagraph(element);
export const getListItem = getAnchorParent(isListItem);
export const getParagraph = getAnchorParent(isParagraph);
export const getBlockElement = getAnchorParent(isBlockElement);