Introduce editable module

This commit is contained in:
Henrik Giesel 2021-08-04 00:32:30 +02:00
parent 9daf037c0b
commit 9b2378c3d2
18 changed files with 255 additions and 133 deletions

View File

@ -21,11 +21,19 @@ copy_files_into_group(
name = "editor",
srcs = [
"editor.css",
"editable.css",
],
package = "//ts/editor",
)
copy_files_into_group(
name = "editable",
srcs = [
"editable-build.css",
],
package = "//ts/editable",
)
copy_files_into_group(
name = "reviewer",
srcs = [

102
ts/editable/BUILD.bazel Normal file
View 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",
],
)

View File

@ -57,3 +57,5 @@ export class EditableContainer extends HTMLDivElement {
return this.baseRule!.style.direction === "rtl";
}
}
customElements.define("anki-editable-container", EditableContainer, { extends: "div" });

View File

@ -1,10 +1,19 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import { bridgeCommand } from "./lib";
import { elementIsBlock, caretToEnd, getBlockElement } from "./helpers";
import { inCodable } from "./toolbar";
import { wrap } from "./wrap";
import { bridgeCommand } from "lib/bridgecommand";
import { elementIsBlock, getBlockElement } from "lib/dom";
// import { inCodable } from "./toolbar";
// 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 {
for (const child of element.children) {
@ -37,7 +46,8 @@ export class Editable extends HTMLElement {
focus(): void {
super.focus();
inCodable.set(false);
// TODO
// inCodable.set(false);
}
caretToEnd(): void {
@ -45,7 +55,8 @@ export class Editable extends HTMLElement {
}
surroundSelection(before: string, after: string): void {
wrap(before, after);
// TODO
// wrap(before, after);
}
onEnter(event: KeyboardEvent): void {
@ -63,3 +74,5 @@ export class Editable extends HTMLElement {
event.preventDefault();
}
}
customElements.define("anki-editable", Editable);

5
ts/editable/index.ts Normal file
View 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";

View File

@ -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(
srcs = [
"fields.scss",
@ -71,6 +59,7 @@ ts_library(
"//ts/lib",
"//ts/sveltelib",
"//ts/components",
"//ts/editable",
"//ts/html-filter",
"//ts:image_module_support",
"@npm//svelte",
@ -156,6 +145,7 @@ esbuild(
"bootstrap-icons",
"mdi-icons",
"svelte_components",
"//ts/editable",
"//ts/components",
"//ts/components:svelte_components",
"@npm//protobufjs",

View File

@ -14,7 +14,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import OnlyEditable from "./OnlyEditable.svelte";
import CommandIconButton from "./CommandIconButton.svelte";
import { getCurrentField, getListItem } from "./helpers";
import { getListItem } from "lib/dom";
import { getCurrentField } from "./helpers";
import {
ulIcon,
olIcon,
@ -31,7 +32,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
function outdentListItem() {
const currentField = getCurrentField();
if (getListItem(currentField.editableContainer.shadowRoot!)) {
if (getListItem(currentField!.editableContainer.shadowRoot!)) {
document.execCommand("outdent");
} else {
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() {
const currentField = getCurrentField();
if (getListItem(currentField.editableContainer.shadowRoot!)) {
if (getListItem(currentField!.editableContainer.shadowRoot!)) {
document.execCommand("indent");
} else {
alert("Indent/unindent currently only works with lists.");

View File

@ -4,12 +4,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="typescript">
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 sheet: CSSStyleSheet;
export let activeImage: HTMLImageElement | null;
let active: boolean = false;
$: {

View File

@ -88,3 +88,5 @@ export class Codable extends HTMLTextAreaElement {
/* default */
}
}
customElements.define("anki-codable", Codable, { extends: "textarea" });

View File

@ -8,8 +8,8 @@
import ImageHandle from "./ImageHandle.svelte";
import type { EditableContainer } from "./editable-container";
import type { Editable } from "./editable";
import type { EditableContainer } from "editable/editable-container";
import type { Editable } from "editable/editable";
import type { Codable } from "./codable";
import { updateActiveButtons } from "./toolbar";
@ -254,3 +254,5 @@ export class EditingArea extends HTMLDivElement {
blur();
}
}
customElements.define("anki-editing-area", EditingArea, { extends: "div" });

View File

@ -65,3 +65,5 @@ export class EditorField extends HTMLDivElement {
this.editingArea.setBaseStyling(fontFamily, fontSize, direction);
}
}
customElements.define("anki-editor-field", EditorField, { extends: "div" });

View File

@ -1,103 +1,12 @@
// Copyright: Ankitects Pty Ltd and contributors
// 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";
export function getCurrentField(): EditingArea | 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 {
return `${text} (${appendix})`;
}

View File

@ -13,22 +13,23 @@ import type EditorToolbar from "./EditorToolbar.svelte";
import type TagEditor from "./TagEditor.svelte";
import { filterHTML } from "html-filter";
import { updateActiveButtons } from "./toolbar";
import { setupI18n, ModuleName } from "lib/i18n";
import { isApplePlatform } from "lib/platform";
import { registerShortcut } from "lib/shortcuts";
import { bridgeCommand } from "lib/bridgecommand";
import { updateActiveButtons } from "./toolbar";
import { saveField } from "./change-timer";
import "./fields.css";
import { saveField } from "./change-timer";
import { EditorField } from "./editor-field";
import { LabelContainer } from "./label-container";
import "editable/editable";
import "editable/editable-container";
import "./label-container";
import "./codable";
import "./editor-field";
import type { EditorField } from "./editor-field";
import { EditingArea } from "./editing-area";
import { EditableContainer } from "./editable-container";
import { Editable } from "./editable";
import { Codable } from "./codable";
import { initToolbar, fieldFocused } from "./toolbar";
import { initTagEditor } from "./tag-editor";
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()) {
registerShortcut(() => bridgeCommand("paste"), "Control+Shift+V");
}

View File

@ -5,9 +5,9 @@
@typescript-eslint/no-non-null-assertion: "off",
*/
import { nodeIsElement } from "lib/dom";
import { updateActiveButtons } from "./toolbar";
import { EditingArea } from "./editing-area";
import { nodeIsElement } from "./helpers";
import { triggerChangeTimer } from "./change-timer";
import { registerShortcut } from "lib/shortcuts";

View File

@ -127,3 +127,5 @@ export class LabelContainer extends HTMLDivElement {
this.toggleSticky();
}
}
customElements.define("anki-label-container", LabelContainer, { extends: "div" });

89
ts/lib/dom.ts Normal file
View 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);

View File

@ -14,6 +14,7 @@
],
"baseUrl": ".",
"paths": {
"editable/*": ["../bazel-bin/ts/editable/*"],
"lib/*": ["../bazel-bin/ts/lib/*"],
"html-filter/*": ["../bazel-bin/ts/html-filter/*"]
/* "sveltelib/*": ["../bazel-bin/ts/sveltelib/*"], */