Introduce editable module
This commit is contained in:
parent
9daf037c0b
commit
9b2378c3d2
@ -21,11 +21,19 @@ copy_files_into_group(
|
|||||||
name = "editor",
|
name = "editor",
|
||||||
srcs = [
|
srcs = [
|
||||||
"editor.css",
|
"editor.css",
|
||||||
"editable.css",
|
|
||||||
],
|
],
|
||||||
package = "//ts/editor",
|
package = "//ts/editor",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
copy_files_into_group(
|
||||||
|
name = "editable",
|
||||||
|
srcs = [
|
||||||
|
"editable-build.css",
|
||||||
|
],
|
||||||
|
package = "//ts/editable",
|
||||||
|
)
|
||||||
|
|
||||||
copy_files_into_group(
|
copy_files_into_group(
|
||||||
name = "reviewer",
|
name = "reviewer",
|
||||||
srcs = [
|
srcs = [
|
||||||
|
102
ts/editable/BUILD.bazel
Normal file
102
ts/editable/BUILD.bazel
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
load("@npm//@bazel/typescript:index.bzl", "ts_library")
|
||||||
|
load("//ts/svelte:svelte.bzl", "compile_svelte", "svelte_check")
|
||||||
|
load("//ts:prettier.bzl", "prettier_test")
|
||||||
|
load("//ts:eslint.bzl", "eslint_test")
|
||||||
|
load("//ts:esbuild.bzl", "esbuild")
|
||||||
|
load("//ts:vendor.bzl", "copy_bootstrap_icons", "copy_mdi_icons")
|
||||||
|
load("//ts:compile_sass.bzl", "compile_sass")
|
||||||
|
|
||||||
|
svelte_files = glob(["*.svelte"])
|
||||||
|
|
||||||
|
svelte_names = [f.replace(".svelte", "") for f in svelte_files]
|
||||||
|
|
||||||
|
filegroup(
|
||||||
|
name = "svelte_components",
|
||||||
|
srcs = svelte_names,
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
|
|
||||||
|
compile_svelte(
|
||||||
|
name = "svelte",
|
||||||
|
srcs = svelte_files,
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
deps = [
|
||||||
|
"//ts/components",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
compile_sass(
|
||||||
|
srcs = [
|
||||||
|
"editable-base.scss",
|
||||||
|
],
|
||||||
|
group = "editable_scss",
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
deps = [
|
||||||
|
"//ts/sass:scrollbar_lib",
|
||||||
|
"//ts/sass/codemirror",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
ts_library(
|
||||||
|
name = "editable",
|
||||||
|
srcs = glob(["*.ts"]),
|
||||||
|
module_name = "editable",
|
||||||
|
tsconfig = "//ts:tsconfig.json",
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
deps = [
|
||||||
|
"//ts/lib",
|
||||||
|
"//ts/sveltelib",
|
||||||
|
"//ts/components",
|
||||||
|
"//ts:image_module_support",
|
||||||
|
"@npm//svelte",
|
||||||
|
] + svelte_names,
|
||||||
|
)
|
||||||
|
|
||||||
|
esbuild(
|
||||||
|
name = "editable-build",
|
||||||
|
args = [
|
||||||
|
"--loader:.svg=text",
|
||||||
|
"--resolve-extensions=.mjs,.js",
|
||||||
|
"--log-level=warning",
|
||||||
|
],
|
||||||
|
entry_point = "index.ts",
|
||||||
|
output_css = "editable-build.css",
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
deps = [
|
||||||
|
"editable_ts",
|
||||||
|
"svelte_components",
|
||||||
|
"//ts/components",
|
||||||
|
"//ts/components:svelte_components",
|
||||||
|
"@npm//protobufjs",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
################
|
||||||
|
|
||||||
|
prettier_test(
|
||||||
|
name = "format_check",
|
||||||
|
srcs = glob([
|
||||||
|
"*.ts",
|
||||||
|
"*.svelte",
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
|
||||||
|
eslint_test(
|
||||||
|
name = "eslint",
|
||||||
|
srcs = glob(
|
||||||
|
[
|
||||||
|
"*.ts",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
svelte_check(
|
||||||
|
name = "svelte_check",
|
||||||
|
srcs = glob([
|
||||||
|
"*.ts",
|
||||||
|
"*.svelte",
|
||||||
|
]) + [
|
||||||
|
"//ts/components",
|
||||||
|
],
|
||||||
|
)
|
@ -57,3 +57,5 @@ export class EditableContainer extends HTMLDivElement {
|
|||||||
return this.baseRule!.style.direction === "rtl";
|
return this.baseRule!.style.direction === "rtl";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customElements.define("anki-editable-container", EditableContainer, { extends: "div" });
|
@ -1,10 +1,19 @@
|
|||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
import { bridgeCommand } from "./lib";
|
import { bridgeCommand } from "lib/bridgecommand";
|
||||||
import { elementIsBlock, caretToEnd, getBlockElement } from "./helpers";
|
import { elementIsBlock, getBlockElement } from "lib/dom";
|
||||||
import { inCodable } from "./toolbar";
|
// import { inCodable } from "./toolbar";
|
||||||
import { wrap } from "./wrap";
|
// import { wrap } from "./wrap";
|
||||||
|
|
||||||
|
export function caretToEnd(node: Node): void {
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(node);
|
||||||
|
range.collapse(false);
|
||||||
|
const selection = (node.getRootNode() as Document | ShadowRoot).getSelection()!;
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(range);
|
||||||
|
}
|
||||||
|
|
||||||
function containsInlineContent(element: Element): boolean {
|
function containsInlineContent(element: Element): boolean {
|
||||||
for (const child of element.children) {
|
for (const child of element.children) {
|
||||||
@ -37,7 +46,8 @@ export class Editable extends HTMLElement {
|
|||||||
|
|
||||||
focus(): void {
|
focus(): void {
|
||||||
super.focus();
|
super.focus();
|
||||||
inCodable.set(false);
|
// TODO
|
||||||
|
// inCodable.set(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
caretToEnd(): void {
|
caretToEnd(): void {
|
||||||
@ -45,7 +55,8 @@ export class Editable extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
surroundSelection(before: string, after: string): void {
|
surroundSelection(before: string, after: string): void {
|
||||||
wrap(before, after);
|
// TODO
|
||||||
|
// wrap(before, after);
|
||||||
}
|
}
|
||||||
|
|
||||||
onEnter(event: KeyboardEvent): void {
|
onEnter(event: KeyboardEvent): void {
|
||||||
@ -63,3 +74,5 @@ export class Editable extends HTMLElement {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customElements.define("anki-editable", Editable);
|
5
ts/editable/index.ts
Normal file
5
ts/editable/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
import "./editable-container";
|
||||||
|
import "./editable";
|
@ -25,18 +25,6 @@ compile_svelte(
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
compile_sass(
|
|
||||||
srcs = [
|
|
||||||
"editable.scss",
|
|
||||||
],
|
|
||||||
group = "editable_scss",
|
|
||||||
visibility = ["//visibility:public"],
|
|
||||||
deps = [
|
|
||||||
"//ts/sass:scrollbar_lib",
|
|
||||||
"//ts/sass/codemirror",
|
|
||||||
],
|
|
||||||
)
|
|
||||||
|
|
||||||
compile_sass(
|
compile_sass(
|
||||||
srcs = [
|
srcs = [
|
||||||
"fields.scss",
|
"fields.scss",
|
||||||
@ -71,6 +59,7 @@ ts_library(
|
|||||||
"//ts/lib",
|
"//ts/lib",
|
||||||
"//ts/sveltelib",
|
"//ts/sveltelib",
|
||||||
"//ts/components",
|
"//ts/components",
|
||||||
|
"//ts/editable",
|
||||||
"//ts/html-filter",
|
"//ts/html-filter",
|
||||||
"//ts:image_module_support",
|
"//ts:image_module_support",
|
||||||
"@npm//svelte",
|
"@npm//svelte",
|
||||||
@ -156,6 +145,7 @@ esbuild(
|
|||||||
"bootstrap-icons",
|
"bootstrap-icons",
|
||||||
"mdi-icons",
|
"mdi-icons",
|
||||||
"svelte_components",
|
"svelte_components",
|
||||||
|
"//ts/editable",
|
||||||
"//ts/components",
|
"//ts/components",
|
||||||
"//ts/components:svelte_components",
|
"//ts/components:svelte_components",
|
||||||
"@npm//protobufjs",
|
"@npm//protobufjs",
|
||||||
|
@ -14,7 +14,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
import OnlyEditable from "./OnlyEditable.svelte";
|
import OnlyEditable from "./OnlyEditable.svelte";
|
||||||
import CommandIconButton from "./CommandIconButton.svelte";
|
import CommandIconButton from "./CommandIconButton.svelte";
|
||||||
|
|
||||||
import { getCurrentField, getListItem } from "./helpers";
|
import { getListItem } from "lib/dom";
|
||||||
|
import { getCurrentField } from "./helpers";
|
||||||
import {
|
import {
|
||||||
ulIcon,
|
ulIcon,
|
||||||
olIcon,
|
olIcon,
|
||||||
@ -31,7 +32,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
|
|
||||||
function outdentListItem() {
|
function outdentListItem() {
|
||||||
const currentField = getCurrentField();
|
const currentField = getCurrentField();
|
||||||
if (getListItem(currentField.editableContainer.shadowRoot!)) {
|
if (getListItem(currentField!.editableContainer.shadowRoot!)) {
|
||||||
document.execCommand("outdent");
|
document.execCommand("outdent");
|
||||||
} else {
|
} else {
|
||||||
alert("Indent/unindent currently only works with lists.");
|
alert("Indent/unindent currently only works with lists.");
|
||||||
@ -40,7 +41,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
|
|
||||||
function indentListItem() {
|
function indentListItem() {
|
||||||
const currentField = getCurrentField();
|
const currentField = getCurrentField();
|
||||||
if (getListItem(currentField.editableContainer.shadowRoot!)) {
|
if (getListItem(currentField!.editableContainer.shadowRoot!)) {
|
||||||
document.execCommand("indent");
|
document.execCommand("indent");
|
||||||
} else {
|
} else {
|
||||||
alert("Indent/unindent currently only works with lists.");
|
alert("Indent/unindent currently only works with lists.");
|
||||||
|
@ -4,12 +4,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
-->
|
-->
|
||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
import { createEventDispatcher, onDestroy } from "svelte";
|
import { createEventDispatcher, onDestroy } from "svelte";
|
||||||
import { nodeIsElement } from "./helpers";
|
import { nodeIsElement } from "lib/dom";
|
||||||
|
|
||||||
|
export let activeImage: HTMLImageElement | null;
|
||||||
export let container: HTMLElement;
|
export let container: HTMLElement;
|
||||||
export let sheet: CSSStyleSheet;
|
export let sheet: CSSStyleSheet;
|
||||||
|
|
||||||
export let activeImage: HTMLImageElement | null;
|
|
||||||
let active: boolean = false;
|
let active: boolean = false;
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
|
@ -88,3 +88,5 @@ export class Codable extends HTMLTextAreaElement {
|
|||||||
/* default */
|
/* default */
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customElements.define("anki-codable", Codable, { extends: "textarea" });
|
||||||
|
@ -8,8 +8,8 @@
|
|||||||
|
|
||||||
import ImageHandle from "./ImageHandle.svelte";
|
import ImageHandle from "./ImageHandle.svelte";
|
||||||
|
|
||||||
import type { EditableContainer } from "./editable-container";
|
import type { EditableContainer } from "editable/editable-container";
|
||||||
import type { Editable } from "./editable";
|
import type { Editable } from "editable/editable";
|
||||||
import type { Codable } from "./codable";
|
import type { Codable } from "./codable";
|
||||||
|
|
||||||
import { updateActiveButtons } from "./toolbar";
|
import { updateActiveButtons } from "./toolbar";
|
||||||
@ -254,3 +254,5 @@ export class EditingArea extends HTMLDivElement {
|
|||||||
blur();
|
blur();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customElements.define("anki-editing-area", EditingArea, { extends: "div" });
|
||||||
|
@ -65,3 +65,5 @@ export class EditorField extends HTMLDivElement {
|
|||||||
this.editingArea.setBaseStyling(fontFamily, fontSize, direction);
|
this.editingArea.setBaseStyling(fontFamily, fontSize, direction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customElements.define("anki-editor-field", EditorField, { extends: "div" });
|
||||||
|
@ -1,103 +1,12 @@
|
|||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
/* eslint
|
|
||||||
@typescript-eslint/no-non-null-assertion: "off",
|
|
||||||
*/
|
|
||||||
|
|
||||||
import type { EditingArea } from "./editing-area";
|
import type { EditingArea } from "./editing-area";
|
||||||
|
|
||||||
export function getCurrentField(): EditingArea | null {
|
export function getCurrentField(): EditingArea | null {
|
||||||
return document.activeElement?.closest(".field") ?? null;
|
return document.activeElement?.closest(".field") ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function nodeIsElement(node: Node): node is Element {
|
|
||||||
return node.nodeType === Node.ELEMENT_NODE;
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements
|
|
||||||
const BLOCK_TAGS = [
|
|
||||||
"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 elementIsBlock(element: Element): boolean {
|
|
||||||
return BLOCK_TAGS.includes(element.tagName);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function caretToEnd(node: Node): void {
|
|
||||||
const range = document.createRange();
|
|
||||||
range.selectNodeContents(node);
|
|
||||||
range.collapse(false);
|
|
||||||
const selection = (node.getRootNode() as Document | ShadowRoot).getSelection()!;
|
|
||||||
selection.removeAllRanges();
|
|
||||||
selection.addRange(range);
|
|
||||||
}
|
|
||||||
|
|
||||||
const getAnchorParent =
|
|
||||||
<T extends Element>(predicate: (element: Element) => element is T) =>
|
|
||||||
(currentField: DocumentOrShadowRoot): T | null => {
|
|
||||||
const anchor = currentField.getSelection()?.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);
|
|
||||||
|
|
||||||
export function appendInParentheses(text: string, appendix: string): string {
|
export function appendInParentheses(text: string, appendix: string): string {
|
||||||
return `${text} (${appendix})`;
|
return `${text} (${appendix})`;
|
||||||
}
|
}
|
||||||
|
@ -13,22 +13,23 @@ import type EditorToolbar from "./EditorToolbar.svelte";
|
|||||||
import type TagEditor from "./TagEditor.svelte";
|
import type TagEditor from "./TagEditor.svelte";
|
||||||
|
|
||||||
import { filterHTML } from "html-filter";
|
import { filterHTML } from "html-filter";
|
||||||
import { updateActiveButtons } from "./toolbar";
|
|
||||||
import { setupI18n, ModuleName } from "lib/i18n";
|
import { setupI18n, ModuleName } from "lib/i18n";
|
||||||
import { isApplePlatform } from "lib/platform";
|
import { isApplePlatform } from "lib/platform";
|
||||||
import { registerShortcut } from "lib/shortcuts";
|
import { registerShortcut } from "lib/shortcuts";
|
||||||
import { bridgeCommand } from "lib/bridgecommand";
|
import { bridgeCommand } from "lib/bridgecommand";
|
||||||
|
import { updateActiveButtons } from "./toolbar";
|
||||||
|
import { saveField } from "./change-timer";
|
||||||
|
|
||||||
import "./fields.css";
|
import "./fields.css";
|
||||||
|
|
||||||
import { saveField } from "./change-timer";
|
import "editable/editable";
|
||||||
|
import "editable/editable-container";
|
||||||
import { EditorField } from "./editor-field";
|
import "./label-container";
|
||||||
import { LabelContainer } from "./label-container";
|
import "./codable";
|
||||||
|
import "./editor-field";
|
||||||
|
import type { EditorField } from "./editor-field";
|
||||||
import { EditingArea } from "./editing-area";
|
import { EditingArea } from "./editing-area";
|
||||||
import { EditableContainer } from "./editable-container";
|
|
||||||
import { Editable } from "./editable";
|
|
||||||
import { Codable } from "./codable";
|
|
||||||
import { initToolbar, fieldFocused } from "./toolbar";
|
import { initToolbar, fieldFocused } from "./toolbar";
|
||||||
import { initTagEditor } from "./tag-editor";
|
import { initTagEditor } from "./tag-editor";
|
||||||
import { getCurrentField } from "./helpers";
|
import { getCurrentField } from "./helpers";
|
||||||
@ -50,13 +51,6 @@ declare global {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define("anki-editable", Editable);
|
|
||||||
customElements.define("anki-editable-container", EditableContainer, { extends: "div" });
|
|
||||||
customElements.define("anki-codable", Codable, { extends: "textarea" });
|
|
||||||
customElements.define("anki-editing-area", EditingArea, { extends: "div" });
|
|
||||||
customElements.define("anki-label-container", LabelContainer, { extends: "div" });
|
|
||||||
customElements.define("anki-editor-field", EditorField, { extends: "div" });
|
|
||||||
|
|
||||||
if (isApplePlatform()) {
|
if (isApplePlatform()) {
|
||||||
registerShortcut(() => bridgeCommand("paste"), "Control+Shift+V");
|
registerShortcut(() => bridgeCommand("paste"), "Control+Shift+V");
|
||||||
}
|
}
|
||||||
|
@ -5,9 +5,9 @@
|
|||||||
@typescript-eslint/no-non-null-assertion: "off",
|
@typescript-eslint/no-non-null-assertion: "off",
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { nodeIsElement } from "lib/dom";
|
||||||
import { updateActiveButtons } from "./toolbar";
|
import { updateActiveButtons } from "./toolbar";
|
||||||
import { EditingArea } from "./editing-area";
|
import { EditingArea } from "./editing-area";
|
||||||
import { nodeIsElement } from "./helpers";
|
|
||||||
import { triggerChangeTimer } from "./change-timer";
|
import { triggerChangeTimer } from "./change-timer";
|
||||||
import { registerShortcut } from "lib/shortcuts";
|
import { registerShortcut } from "lib/shortcuts";
|
||||||
|
|
||||||
|
@ -127,3 +127,5 @@ export class LabelContainer extends HTMLDivElement {
|
|||||||
this.toggleSticky();
|
this.toggleSticky();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customElements.define("anki-label-container", LabelContainer, { extends: "div" });
|
||||||
|
89
ts/lib/dom.ts
Normal file
89
ts/lib/dom.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
export function nodeIsElement(node: Node): node is Element {
|
||||||
|
return node.nodeType === Node.ELEMENT_NODE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/HTML/Block-level_elements
|
||||||
|
const BLOCK_TAGS = [
|
||||||
|
"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 elementIsBlock(element: Element): boolean {
|
||||||
|
return BLOCK_TAGS.includes(element.tagName);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function caretToEnd(node: Node): void {
|
||||||
|
const range = document.createRange();
|
||||||
|
range.selectNodeContents(node);
|
||||||
|
range.collapse(false);
|
||||||
|
const selection = (node.getRootNode() as Document | ShadowRoot).getSelection()!;
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(range);
|
||||||
|
}
|
||||||
|
|
||||||
|
const getAnchorParent =
|
||||||
|
<T extends Element>(predicate: (element: Element) => element is T) =>
|
||||||
|
(currentField: DocumentOrShadowRoot): T | null => {
|
||||||
|
const anchor = currentField.getSelection()?.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);
|
@ -14,6 +14,7 @@
|
|||||||
],
|
],
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
|
"editable/*": ["../bazel-bin/ts/editable/*"],
|
||||||
"lib/*": ["../bazel-bin/ts/lib/*"],
|
"lib/*": ["../bazel-bin/ts/lib/*"],
|
||||||
"html-filter/*": ["../bazel-bin/ts/html-filter/*"]
|
"html-filter/*": ["../bazel-bin/ts/html-filter/*"]
|
||||||
/* "sveltelib/*": ["../bazel-bin/ts/sveltelib/*"], */
|
/* "sveltelib/*": ["../bazel-bin/ts/sveltelib/*"], */
|
||||||
|
Loading…
Reference in New Issue
Block a user