Refactor how toolbar buttons get to surround within editor fields (#1931)
* Export surrounder directly from RichTextInput * Change wording in editor/surround * Remove empty line * Change wording * Fix interfaces * Add field description directly in NoteEditor * Strip description logic from ContentEditable * Make RichTextInput position: relative * Make attachToShadow an async function * Apply field styling to field description * Show FieldDescription only if content empty * Remove descriptionStore and descriptionKey * Revert "Make attachToShadow an async function" This reverts commit b62705eadf7335c7ee0c6c8797047e1f1ccdbf44. SvelteActionReturnType does not accept Promise<void> * Fix mess after merge commit * Require registering surround formats
This commit is contained in:
parent
d5945a213a
commit
9ca13ca3bc
@ -169,14 +169,15 @@ export class FormattingNode<T = never> extends TreeNode {
|
||||
* When surrounding "inside" with a bold format in the following case:
|
||||
* `<span class="myclass"><em>inside</em></span>`
|
||||
* The formatting node would sit above the span (it ascends above both
|
||||
* the span and the em tag), and both tags are extensions to this node.
|
||||
* the em and the span tag), and its extensions are the span tag and the
|
||||
* em tag (in this order).
|
||||
*
|
||||
* @example
|
||||
* When a format only wants to add a class, it would typically look for an
|
||||
* extension first. When applying class="myclass" to "inside" in the
|
||||
* following case:
|
||||
* `<em><span style="color: rgb(255, 0, 0)"><b>inside</b></span></em>`
|
||||
* It would typically become:
|
||||
* It should typically become:
|
||||
* `<em><span class="myclass" style="color: rgb(255, 0, 0)"><b>inside</b></span></em>`
|
||||
*/
|
||||
extensions: (HTMLElement | SVGElement)[] = [];
|
||||
|
@ -44,7 +44,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
import type { Writable } from "svelte/store";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
import { descriptionKey, directionKey } from "../lib/context-keys";
|
||||
import { directionKey } from "../lib/context-keys";
|
||||
import { promiseWithResolver } from "../lib/promise";
|
||||
import type { Destroyable } from "./destroyable";
|
||||
import EditingArea from "./EditingArea.svelte";
|
||||
@ -60,11 +60,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
$: $directionStore = field.direction;
|
||||
|
||||
const descriptionStore = writable<string>();
|
||||
setContext(descriptionKey, descriptionStore);
|
||||
|
||||
$: $descriptionStore = field.description;
|
||||
|
||||
const editingArea: Partial<EditingAreaAPI> = {};
|
||||
const [element, elementResolve] = promiseWithResolver<HTMLElement>();
|
||||
|
||||
|
48
ts/editor/FieldDescription.svelte
Normal file
48
ts/editor/FieldDescription.svelte
Normal file
@ -0,0 +1,48 @@
|
||||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { getContext } from "svelte";
|
||||
import type { Readable } from "svelte/store";
|
||||
|
||||
import { directionKey, fontFamilyKey, fontSizeKey } from "../lib/context-keys";
|
||||
import { context } from "./EditingArea.svelte";
|
||||
|
||||
const { content } = context.get();
|
||||
|
||||
const fontFamily = getContext<Readable<string>>(fontFamilyKey);
|
||||
const fontSize = getContext<Readable<number>>(fontSizeKey);
|
||||
const direction = getContext<Readable<"ltr" | "rtl">>(directionKey);
|
||||
|
||||
$: empty = $content.length === 0;
|
||||
</script>
|
||||
|
||||
{#if empty}
|
||||
<div
|
||||
class="field-description"
|
||||
style:font-family={$fontFamily}
|
||||
style:font-size="{$fontSize}px"
|
||||
style:direction={$direction}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.field-description {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
|
||||
cursor: text;
|
||||
opacity: 0.4;
|
||||
|
||||
/* same as in ContentEditable */
|
||||
padding: 6px;
|
||||
|
||||
/* stay a on single line */
|
||||
overflow-x: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
</style>
|
@ -46,20 +46,21 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
import DecoratedElements from "./DecoratedElements.svelte";
|
||||
import { clearableArray } from "./destroyable";
|
||||
import DuplicateLink from "./DuplicateLink.svelte";
|
||||
import { EditorToolbar } from "./editor-toolbar";
|
||||
import EditorToolbar from "./editor-toolbar";
|
||||
import type { FieldData } from "./EditorField.svelte";
|
||||
import EditorField from "./EditorField.svelte";
|
||||
import FieldDescription from "./FieldDescription.svelte";
|
||||
import Fields from "./Fields.svelte";
|
||||
import FieldsEditor from "./FieldsEditor.svelte";
|
||||
import FrameElement from "./FrameElement.svelte";
|
||||
import { alertIcon } from "./icons";
|
||||
import { ImageHandle } from "./image-overlay";
|
||||
import { MathjaxHandle } from "./mathjax-overlay";
|
||||
import ImageHandle from "./image-overlay";
|
||||
import MathjaxHandle from "./mathjax-overlay";
|
||||
import MathjaxElement from "./MathjaxElement.svelte";
|
||||
import Notification from "./Notification.svelte";
|
||||
import { PlainTextInput } from "./plain-text-input";
|
||||
import PlainTextInput from "./plain-text-input";
|
||||
import PlainTextBadge from "./PlainTextBadge.svelte";
|
||||
import { editingInputIsRichText, RichTextInput } from "./rich-text-input";
|
||||
import RichTextInput, { editingInputIsRichText } from "./rich-text-input";
|
||||
import RichTextBadge from "./RichTextBadge.svelte";
|
||||
|
||||
function quoteFontFamily(fontFamily: string): string {
|
||||
@ -302,9 +303,11 @@ the AddCards dialog) should be implemented in the user of this component.
|
||||
<Fields>
|
||||
<DecoratedElements>
|
||||
{#each fieldsData as field, index}
|
||||
{@const content = fieldStores[index]}
|
||||
|
||||
<EditorField
|
||||
{field}
|
||||
content={fieldStores[index]}
|
||||
{content}
|
||||
api={fields[index]}
|
||||
on:focusin={() => {
|
||||
$focusedField = fields[index];
|
||||
@ -313,9 +316,7 @@ the AddCards dialog) should be implemented in the user of this component.
|
||||
on:focusout={() => {
|
||||
$focusedField = null;
|
||||
bridgeCommand(
|
||||
`blur:${index}:${getNoteId()}:${get(
|
||||
fieldStores[index],
|
||||
)}`,
|
||||
`blur:${index}:${getNoteId()}:${get(content)}`,
|
||||
);
|
||||
}}
|
||||
--label-color={cols[index] === "dupe"
|
||||
@ -361,6 +362,9 @@ the AddCards dialog) should be implemented in the user of this component.
|
||||
>
|
||||
<ImageHandle maxWidth={250} maxHeight={125} />
|
||||
<MathjaxHandle />
|
||||
<FieldDescription>
|
||||
{field.description}
|
||||
</FieldDescription>
|
||||
</RichTextInput>
|
||||
|
||||
<PlainTextInput
|
||||
|
@ -3,6 +3,8 @@ Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
|
||||
import IconButton from "../../components/IconButton.svelte";
|
||||
import Shortcut from "../../components/Shortcut.svelte";
|
||||
import WithState from "../../components/WithState.svelte";
|
||||
@ -10,9 +12,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
import * as tr from "../../lib/ftl";
|
||||
import { getPlatformString } from "../../lib/shortcuts";
|
||||
import { removeStyleProperties } from "../../lib/styling";
|
||||
import { context as noteEditorContext } from "../NoteEditor.svelte";
|
||||
import { editingInputIsRichText } from "../rich-text-input";
|
||||
import { Surrounder } from "../surround";
|
||||
import { singleCallback } from "../../lib/typing";
|
||||
import { surrounder } from "../rich-text-input";
|
||||
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
|
||||
import { boldIcon } from "./icons";
|
||||
|
||||
@ -36,50 +37,44 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
}
|
||||
}
|
||||
|
||||
const key = "bold";
|
||||
|
||||
const format = {
|
||||
surroundElement,
|
||||
matcher,
|
||||
};
|
||||
|
||||
const namedFormat = {
|
||||
key,
|
||||
name: tr.editingBoldText(),
|
||||
show: true,
|
||||
active: true,
|
||||
format,
|
||||
};
|
||||
|
||||
const { removeFormats } = editorToolbarContext.get();
|
||||
removeFormats.update((formats) => [...formats, namedFormat]);
|
||||
|
||||
const { focusedInput } = noteEditorContext.get();
|
||||
const surrounder = Surrounder.make();
|
||||
let disabled: boolean;
|
||||
|
||||
$: if (editingInputIsRichText($focusedInput)) {
|
||||
surrounder.richText = $focusedInput;
|
||||
disabled = false;
|
||||
} else {
|
||||
surrounder.disable();
|
||||
disabled = true;
|
||||
}
|
||||
|
||||
function updateStateFromActiveInput(): Promise<boolean> {
|
||||
return disabled ? Promise.resolve(false) : surrounder.isSurrounded(format);
|
||||
async function updateStateFromActiveInput(): Promise<boolean> {
|
||||
return disabled ? false : surrounder.isSurrounded(key);
|
||||
}
|
||||
|
||||
function makeBold(): void {
|
||||
surrounder.surround(format);
|
||||
surrounder.surround(key);
|
||||
}
|
||||
|
||||
const keyCombination = "Control+B";
|
||||
|
||||
let disabled: boolean;
|
||||
|
||||
onMount(() =>
|
||||
singleCallback(
|
||||
surrounder.active.subscribe((value) => (disabled = !value)),
|
||||
surrounder.registerFormat(key, format),
|
||||
),
|
||||
);
|
||||
</script>
|
||||
|
||||
<WithState
|
||||
key="bold"
|
||||
update={updateStateFromActiveInput}
|
||||
let:state={active}
|
||||
let:updateState
|
||||
>
|
||||
<WithState {key} update={updateStateFromActiveInput} let:state={active} let:updateState>
|
||||
<IconButton
|
||||
tooltip="{tr.editingBoldText()} ({getPlatformString(keyCombination)})"
|
||||
{active}
|
||||
|
@ -6,7 +6,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
import type { Writable } from "svelte/store";
|
||||
|
||||
import { resetAllState, updateAllState } from "../../components/WithState.svelte";
|
||||
import type { SurroundFormat } from "../../domlib/surround";
|
||||
import type { DefaultSlotInterface } from "../../sveltelib/dynamic-slotting";
|
||||
|
||||
export function updateActiveButtons(event: Event) {
|
||||
@ -17,11 +16,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
resetAllState(false);
|
||||
}
|
||||
|
||||
export interface RemoveFormat<T> {
|
||||
export interface RemoveFormat {
|
||||
name: string;
|
||||
key: string;
|
||||
show: boolean;
|
||||
active: boolean;
|
||||
format: SurroundFormat<T>;
|
||||
}
|
||||
|
||||
export interface EditorToolbarAPI {
|
||||
@ -30,7 +29,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
inlineButtons: DefaultSlotInterface;
|
||||
blockButtons: DefaultSlotInterface;
|
||||
templateButtons: DefaultSlotInterface;
|
||||
removeFormats: Writable<RemoveFormat<any>[]>;
|
||||
removeFormats: Writable<RemoveFormat[]>;
|
||||
}
|
||||
|
||||
/* Our dynamic components */
|
||||
@ -69,7 +68,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
const inlineButtons = {} as DefaultSlotInterface;
|
||||
const blockButtons = {} as DefaultSlotInterface;
|
||||
const templateButtons = {} as DefaultSlotInterface;
|
||||
const removeFormats = writable<RemoveFormat<any>[]>([]);
|
||||
const removeFormats = writable<RemoveFormat[]>([]);
|
||||
|
||||
let apiPartial: Partial<EditorToolbarAPI> = {};
|
||||
export { apiPartial as api };
|
||||
|
@ -3,20 +3,16 @@ Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
|
||||
import IconButton from "../../components/IconButton.svelte";
|
||||
import type {
|
||||
FormattingNode,
|
||||
MatchType,
|
||||
SurroundFormat,
|
||||
} from "../../domlib/surround";
|
||||
import type { FormattingNode, MatchType } from "../../domlib/surround";
|
||||
import { bridgeCommand } from "../../lib/bridgecommand";
|
||||
import * as tr from "../../lib/ftl";
|
||||
import { removeStyleProperties } from "../../lib/styling";
|
||||
import { context as noteEditorContext } from "../NoteEditor.svelte";
|
||||
import { editingInputIsRichText } from "../rich-text-input";
|
||||
import { Surrounder } from "../surround";
|
||||
import { singleCallback } from "../../lib/typing";
|
||||
import { surrounder } from "../rich-text-input";
|
||||
import ColorPicker from "./ColorPicker.svelte";
|
||||
import type { RemoveFormat } from "./EditorToolbar.svelte";
|
||||
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
|
||||
import { arrowIcon, highlightColorIcon } from "./icons";
|
||||
import WithColorHelper from "./WithColorHelper.svelte";
|
||||
@ -79,37 +75,36 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
return true;
|
||||
}
|
||||
|
||||
const format: SurroundFormat<string> = {
|
||||
const key = "highlightColor";
|
||||
|
||||
const format = {
|
||||
matcher,
|
||||
merger,
|
||||
formatter,
|
||||
};
|
||||
|
||||
const namedFormat: RemoveFormat<string> = {
|
||||
const namedFormat = {
|
||||
key,
|
||||
name: tr.editingTextHighlightColor(),
|
||||
show: true,
|
||||
active: true,
|
||||
format,
|
||||
};
|
||||
|
||||
const { removeFormats } = editorToolbarContext.get();
|
||||
removeFormats.update((formats) => [...formats, namedFormat]);
|
||||
|
||||
const { focusedInput } = noteEditorContext.get();
|
||||
const surrounder = Surrounder.make();
|
||||
function setTextColor(): void {
|
||||
surrounder.overwriteSurround(key);
|
||||
}
|
||||
|
||||
let disabled: boolean;
|
||||
|
||||
$: if (editingInputIsRichText($focusedInput)) {
|
||||
disabled = false;
|
||||
surrounder.richText = $focusedInput;
|
||||
} else {
|
||||
disabled = true;
|
||||
surrounder.disable();
|
||||
}
|
||||
|
||||
function setTextColor(): void {
|
||||
surrounder.overwriteSurround(format);
|
||||
}
|
||||
onMount(() =>
|
||||
singleCallback(
|
||||
surrounder.active.subscribe((value) => (disabled = !value)),
|
||||
surrounder.registerFormat(key, format),
|
||||
),
|
||||
);
|
||||
</script>
|
||||
|
||||
<WithColorHelper {color} let:colorHelperIcon let:setColor>
|
||||
|
@ -3,6 +3,8 @@ Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
|
||||
import IconButton from "../../components/IconButton.svelte";
|
||||
import Shortcut from "../../components/Shortcut.svelte";
|
||||
import WithState from "../../components/WithState.svelte";
|
||||
@ -10,9 +12,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
import * as tr from "../../lib/ftl";
|
||||
import { getPlatformString } from "../../lib/shortcuts";
|
||||
import { removeStyleProperties } from "../../lib/styling";
|
||||
import { context as noteEditorContext } from "../NoteEditor.svelte";
|
||||
import { editingInputIsRichText } from "../rich-text-input";
|
||||
import { Surrounder } from "../surround";
|
||||
import { singleCallback } from "../../lib/typing";
|
||||
import { surrounder } from "../rich-text-input";
|
||||
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
|
||||
import { italicIcon } from "./icons";
|
||||
|
||||
@ -35,50 +36,44 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
}
|
||||
}
|
||||
|
||||
const key = "italic";
|
||||
|
||||
const format = {
|
||||
surroundElement,
|
||||
matcher,
|
||||
};
|
||||
|
||||
const namedFormat = {
|
||||
key,
|
||||
name: tr.editingItalicText(),
|
||||
show: true,
|
||||
active: true,
|
||||
format,
|
||||
};
|
||||
|
||||
const { removeFormats } = editorToolbarContext.get();
|
||||
removeFormats.update((formats) => [...formats, namedFormat]);
|
||||
|
||||
const { focusedInput } = noteEditorContext.get();
|
||||
const surrounder = Surrounder.make();
|
||||
let disabled: boolean;
|
||||
|
||||
$: if (editingInputIsRichText($focusedInput)) {
|
||||
surrounder.richText = $focusedInput;
|
||||
disabled = false;
|
||||
} else {
|
||||
surrounder.disable();
|
||||
disabled = true;
|
||||
}
|
||||
|
||||
function updateStateFromActiveInput(): Promise<boolean> {
|
||||
return disabled ? Promise.resolve(false) : surrounder!.isSurrounded(format);
|
||||
async function updateStateFromActiveInput(): Promise<boolean> {
|
||||
return disabled ? false : surrounder.isSurrounded(key);
|
||||
}
|
||||
|
||||
function makeItalic(): void {
|
||||
surrounder.surround(format);
|
||||
surrounder.surround(key);
|
||||
}
|
||||
|
||||
const keyCombination = "Control+I";
|
||||
|
||||
let disabled: boolean;
|
||||
|
||||
onMount(() =>
|
||||
singleCallback(
|
||||
surrounder.active.subscribe((value) => (disabled = !value)),
|
||||
surrounder.registerFormat(key, format),
|
||||
),
|
||||
);
|
||||
</script>
|
||||
|
||||
<WithState
|
||||
key="italic"
|
||||
update={updateStateFromActiveInput}
|
||||
let:state={active}
|
||||
let:updateState
|
||||
>
|
||||
<WithState {key} update={updateStateFromActiveInput} let:state={active} let:updateState>
|
||||
<IconButton
|
||||
tooltip="{tr.editingItalicText()} ({getPlatformString(keyCombination)})"
|
||||
{active}
|
||||
|
@ -3,6 +3,8 @@ Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
|
||||
import Checkbox from "../../components/CheckBox.svelte";
|
||||
import DropdownItem from "../../components/DropdownItem.svelte";
|
||||
import DropdownMenu from "../../components/DropdownMenu.svelte";
|
||||
@ -10,74 +12,64 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
import IconButton from "../../components/IconButton.svelte";
|
||||
import Shortcut from "../../components/Shortcut.svelte";
|
||||
import WithDropdown from "../../components/WithDropdown.svelte";
|
||||
import type { SurroundFormat } from "../../domlib/surround";
|
||||
import type { MatchType } from "../../domlib/surround";
|
||||
import * as tr from "../../lib/ftl";
|
||||
import { altPressed } from "../../lib/keys";
|
||||
import { getPlatformString } from "../../lib/shortcuts";
|
||||
import { context as noteEditorContext } from "../NoteEditor.svelte";
|
||||
import { editingInputIsRichText } from "../rich-text-input";
|
||||
import { Surrounder } from "../surround";
|
||||
import { singleCallback } from "../../lib/typing";
|
||||
import { surrounder } from "../rich-text-input";
|
||||
import type { RemoveFormat } from "./EditorToolbar.svelte";
|
||||
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
|
||||
import { eraserIcon } from "./icons";
|
||||
import { arrowIcon } from "./icons";
|
||||
|
||||
const { focusedInput } = noteEditorContext.get();
|
||||
const surrounder = Surrounder.make();
|
||||
let disabled: boolean;
|
||||
const { removeFormats } = editorToolbarContext.get();
|
||||
|
||||
$: if (editingInputIsRichText($focusedInput)) {
|
||||
surrounder.richText = $focusedInput;
|
||||
disabled = false;
|
||||
} else {
|
||||
surrounder.disable();
|
||||
disabled = true;
|
||||
const surroundElement = document.createElement("span");
|
||||
|
||||
function matcher(element: HTMLElement | SVGElement, match: MatchType<never>): void {
|
||||
if (
|
||||
element.tagName === "SPAN" &&
|
||||
element.className.length === 0 &&
|
||||
element.style.cssText.length === 0
|
||||
) {
|
||||
match.remove();
|
||||
}
|
||||
}
|
||||
|
||||
const { removeFormats } = editorToolbarContext.get();
|
||||
const key = "simple spans";
|
||||
const format = {
|
||||
matcher,
|
||||
surroundElement,
|
||||
};
|
||||
|
||||
removeFormats.update((formats) =>
|
||||
formats.concat({
|
||||
name: "simple spans",
|
||||
key,
|
||||
name: key,
|
||||
show: false,
|
||||
active: true,
|
||||
format: {
|
||||
matcher: (
|
||||
element: HTMLElement | SVGElement,
|
||||
match: MatchType<never>,
|
||||
): void => {
|
||||
if (
|
||||
element.tagName === "SPAN" &&
|
||||
element.className.length === 0 &&
|
||||
element.style.cssText.length === 0
|
||||
) {
|
||||
match.remove();
|
||||
}
|
||||
},
|
||||
surroundElement: document.createElement("span"),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
let activeFormats: SurroundFormat<any>[];
|
||||
$: activeFormats = $removeFormats
|
||||
let activeKeys: string[];
|
||||
$: activeKeys = $removeFormats
|
||||
.filter((format) => format.active)
|
||||
.map((format) => format.format);
|
||||
.map((format) => format.key);
|
||||
|
||||
let inactiveFormats: SurroundFormat<any>[];
|
||||
$: inactiveFormats = $removeFormats
|
||||
let inactiveKeys: string[];
|
||||
$: inactiveKeys = $removeFormats
|
||||
.filter((format) => !format.active)
|
||||
.map((format) => format.format);
|
||||
.map((format) => format.key);
|
||||
|
||||
let showFormats: RemoveFormat<any>[];
|
||||
let showFormats: RemoveFormat[];
|
||||
$: showFormats = $removeFormats.filter((format) => format.show);
|
||||
|
||||
function remove(): void {
|
||||
surrounder.remove(activeFormats, inactiveFormats);
|
||||
surrounder.remove(activeKeys, inactiveKeys);
|
||||
}
|
||||
|
||||
function onItemClick<T>(event: MouseEvent, format: RemoveFormat<T>): void {
|
||||
function onItemClick(event: MouseEvent, format: RemoveFormat): void {
|
||||
if (altPressed(event)) {
|
||||
for (const format of showFormats) {
|
||||
format.active = false;
|
||||
@ -89,6 +81,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
}
|
||||
|
||||
const keyCombination = "Control+R";
|
||||
|
||||
let disabled: boolean;
|
||||
|
||||
onMount(() =>
|
||||
singleCallback(
|
||||
surrounder.active.subscribe((value) => (disabled = !value)),
|
||||
surrounder.registerFormat(key, format),
|
||||
),
|
||||
);
|
||||
</script>
|
||||
|
||||
<IconButton
|
||||
|
@ -2,9 +2,21 @@
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script context="module" lang="ts">
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
|
||||
import IconButton from "../../components/IconButton.svelte";
|
||||
import Shortcut from "../../components/Shortcut.svelte";
|
||||
import WithState from "../../components/WithState.svelte";
|
||||
import { updateStateByKey } from "../../components/WithState.svelte";
|
||||
import type { MatchType } from "../../domlib/surround";
|
||||
import * as tr from "../../lib/ftl";
|
||||
import { getPlatformString } from "../../lib/shortcuts";
|
||||
import { removeStyleProperties } from "../../lib/styling";
|
||||
import { singleCallback } from "../../lib/typing";
|
||||
import { surrounder } from "../rich-text-input";
|
||||
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
|
||||
import { subscriptIcon } from "./icons";
|
||||
|
||||
const surroundElement = document.createElement("sub");
|
||||
|
||||
@ -25,65 +37,44 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
}
|
||||
}
|
||||
|
||||
export const format = {
|
||||
const key = "subscript";
|
||||
|
||||
const format = {
|
||||
surroundElement,
|
||||
matcher,
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import IconButton from "../../components/IconButton.svelte";
|
||||
import Shortcut from "../../components/Shortcut.svelte";
|
||||
import WithState from "../../components/WithState.svelte";
|
||||
import { updateStateByKey } from "../../components/WithState.svelte";
|
||||
import * as tr from "../../lib/ftl";
|
||||
import { getPlatformString } from "../../lib/shortcuts";
|
||||
import { context as noteEditorContext } from "../NoteEditor.svelte";
|
||||
import { editingInputIsRichText } from "../rich-text-input";
|
||||
import { Surrounder } from "../surround";
|
||||
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
|
||||
import { subscriptIcon } from "./icons";
|
||||
import { format as superscript } from "./SuperscriptButton.svelte";
|
||||
|
||||
const namedFormat = {
|
||||
key,
|
||||
name: tr.editingSubscript(),
|
||||
show: true,
|
||||
active: true,
|
||||
format,
|
||||
};
|
||||
|
||||
const { removeFormats } = editorToolbarContext.get();
|
||||
removeFormats.update((formats) => [...formats, namedFormat]);
|
||||
|
||||
const { focusedInput } = noteEditorContext.get();
|
||||
const surrounder = Surrounder.make();
|
||||
let disabled: boolean;
|
||||
|
||||
$: if (editingInputIsRichText($focusedInput)) {
|
||||
surrounder.richText = $focusedInput;
|
||||
disabled = false;
|
||||
} else {
|
||||
surrounder.disable();
|
||||
disabled = true;
|
||||
}
|
||||
|
||||
function updateStateFromActiveInput(): Promise<boolean> {
|
||||
return disabled ? Promise.resolve(false) : surrounder!.isSurrounded(format);
|
||||
async function updateStateFromActiveInput(): Promise<boolean> {
|
||||
return disabled ? false : surrounder.isSurrounded(key);
|
||||
}
|
||||
|
||||
function makeSub(): void {
|
||||
surrounder.surround(format, [superscript]);
|
||||
surrounder.surround(key, ["superscript"]);
|
||||
}
|
||||
|
||||
const keyCombination = "Control+Shift+=";
|
||||
|
||||
let disabled: boolean;
|
||||
|
||||
onMount(() =>
|
||||
singleCallback(
|
||||
surrounder.active.subscribe((value) => (disabled = !value)),
|
||||
surrounder.registerFormat(key, format),
|
||||
),
|
||||
);
|
||||
</script>
|
||||
|
||||
<WithState
|
||||
key="sub"
|
||||
update={updateStateFromActiveInput}
|
||||
let:state={active}
|
||||
let:updateState
|
||||
>
|
||||
<WithState {key} update={updateStateFromActiveInput} let:state={active} let:updateState>
|
||||
<IconButton
|
||||
tooltip="{tr.editingSubscript()} ({getPlatformString(keyCombination)})"
|
||||
{active}
|
||||
@ -91,7 +82,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
on:click={(event) => {
|
||||
makeSub();
|
||||
updateState(event);
|
||||
updateStateByKey("super", event);
|
||||
updateStateByKey("superscript", event);
|
||||
}}
|
||||
>
|
||||
{@html subscriptIcon}
|
||||
@ -102,7 +93,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
on:action={(event) => {
|
||||
makeSub();
|
||||
updateState(event);
|
||||
updateStateByKey("super", event);
|
||||
updateStateByKey("superscript", event);
|
||||
}}
|
||||
/>
|
||||
</WithState>
|
||||
|
@ -2,9 +2,21 @@
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script context="module" lang="ts">
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
|
||||
import IconButton from "../../components/IconButton.svelte";
|
||||
import Shortcut from "../../components/Shortcut.svelte";
|
||||
import WithState from "../../components/WithState.svelte";
|
||||
import { updateStateByKey } from "../../components/WithState.svelte";
|
||||
import type { MatchType } from "../../domlib/surround";
|
||||
import * as tr from "../../lib/ftl";
|
||||
import { getPlatformString } from "../../lib/shortcuts";
|
||||
import { removeStyleProperties } from "../../lib/styling";
|
||||
import { singleCallback } from "../../lib/typing";
|
||||
import { surrounder } from "../rich-text-input";
|
||||
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
|
||||
import { superscriptIcon } from "./icons";
|
||||
|
||||
const surroundElement = document.createElement("sup");
|
||||
|
||||
@ -25,65 +37,44 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
}
|
||||
}
|
||||
|
||||
const key = "superscript";
|
||||
|
||||
export const format = {
|
||||
surroundElement,
|
||||
matcher,
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import IconButton from "../../components/IconButton.svelte";
|
||||
import Shortcut from "../../components/Shortcut.svelte";
|
||||
import WithState from "../../components/WithState.svelte";
|
||||
import { updateStateByKey } from "../../components/WithState.svelte";
|
||||
import * as tr from "../../lib/ftl";
|
||||
import { getPlatformString } from "../../lib/shortcuts";
|
||||
import { context as noteEditorContext } from "../NoteEditor.svelte";
|
||||
import { editingInputIsRichText } from "../rich-text-input";
|
||||
import { Surrounder } from "../surround";
|
||||
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
|
||||
import { superscriptIcon } from "./icons";
|
||||
import { format as subscript } from "./SubscriptButton.svelte";
|
||||
|
||||
const namedFormat = {
|
||||
key,
|
||||
name: tr.editingSuperscript(),
|
||||
show: true,
|
||||
active: true,
|
||||
format,
|
||||
};
|
||||
|
||||
const { removeFormats } = editorToolbarContext.get();
|
||||
removeFormats.update((formats) => [...formats, namedFormat]);
|
||||
|
||||
const { focusedInput } = noteEditorContext.get();
|
||||
const surrounder = Surrounder.make();
|
||||
let disabled: boolean;
|
||||
|
||||
$: if (editingInputIsRichText($focusedInput)) {
|
||||
surrounder.richText = $focusedInput;
|
||||
disabled = false;
|
||||
} else {
|
||||
surrounder.disable();
|
||||
disabled = true;
|
||||
}
|
||||
|
||||
function updateStateFromActiveInput(): Promise<boolean> {
|
||||
return disabled ? Promise.resolve(false) : surrounder!.isSurrounded(format);
|
||||
async function updateStateFromActiveInput(): Promise<boolean> {
|
||||
return disabled ? false : surrounder.isSurrounded(key);
|
||||
}
|
||||
|
||||
function makeSuper(): void {
|
||||
surrounder.surround(format, [subscript]);
|
||||
surrounder.surround(key, ["subscript"]);
|
||||
}
|
||||
|
||||
const keyCombination = "Control+=";
|
||||
|
||||
let disabled: boolean;
|
||||
|
||||
onMount(() =>
|
||||
singleCallback(
|
||||
surrounder.active.subscribe((value) => (disabled = !value)),
|
||||
surrounder.registerFormat(key, format),
|
||||
),
|
||||
);
|
||||
</script>
|
||||
|
||||
<WithState
|
||||
key="super"
|
||||
update={updateStateFromActiveInput}
|
||||
let:state={active}
|
||||
let:updateState
|
||||
>
|
||||
<WithState {key} update={updateStateFromActiveInput} let:state={active} let:updateState>
|
||||
<IconButton
|
||||
tooltip="{tr.editingSuperscript()} ({getPlatformString(keyCombination)})"
|
||||
{active}
|
||||
@ -91,7 +82,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
on:click={(event) => {
|
||||
makeSuper();
|
||||
updateState(event);
|
||||
updateStateByKey("sub", event);
|
||||
updateStateByKey("subscript", event);
|
||||
}}
|
||||
>
|
||||
{@html superscriptIcon}
|
||||
@ -102,7 +93,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
on:action={(event) => {
|
||||
makeSuper();
|
||||
updateState(event);
|
||||
updateStateByKey("sub", event);
|
||||
updateStateByKey("subscript", event);
|
||||
}}
|
||||
/>
|
||||
</WithState>
|
||||
|
@ -3,23 +3,19 @@ Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
|
||||
import IconButton from "../../components/IconButton.svelte";
|
||||
import Shortcut from "../../components/Shortcut.svelte";
|
||||
import type {
|
||||
FormattingNode,
|
||||
MatchType,
|
||||
SurroundFormat,
|
||||
} from "../../domlib/surround";
|
||||
import type { FormattingNode, MatchType } from "../../domlib/surround";
|
||||
import { bridgeCommand } from "../../lib/bridgecommand";
|
||||
import * as tr from "../../lib/ftl";
|
||||
import { getPlatformString } from "../../lib/shortcuts";
|
||||
import { removeStyleProperties } from "../../lib/styling";
|
||||
import { singleCallback } from "../../lib/typing";
|
||||
import { withFontColor } from "../helpers";
|
||||
import { context as noteEditorContext } from "../NoteEditor.svelte";
|
||||
import { editingInputIsRichText } from "../rich-text-input";
|
||||
import { Surrounder } from "../surround";
|
||||
import { surrounder } from "../rich-text-input";
|
||||
import ColorPicker from "./ColorPicker.svelte";
|
||||
import type { RemoveFormat } from "./EditorToolbar.svelte";
|
||||
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
|
||||
import { arrowIcon, textColorIcon } from "./icons";
|
||||
import WithColorHelper from "./WithColorHelper.svelte";
|
||||
@ -93,40 +89,39 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
return true;
|
||||
}
|
||||
|
||||
const format: SurroundFormat<string> = {
|
||||
const key = "textColor";
|
||||
|
||||
const format = {
|
||||
matcher,
|
||||
merger,
|
||||
formatter,
|
||||
};
|
||||
|
||||
const namedFormat: RemoveFormat<string> = {
|
||||
const namedFormat = {
|
||||
key,
|
||||
name: tr.editingTextColor(),
|
||||
show: true,
|
||||
active: true,
|
||||
format,
|
||||
};
|
||||
|
||||
const { removeFormats } = editorToolbarContext.get();
|
||||
removeFormats.update((formats) => [...formats, namedFormat]);
|
||||
|
||||
const { focusedInput } = noteEditorContext.get();
|
||||
const surrounder = Surrounder.make();
|
||||
let disabled: boolean;
|
||||
|
||||
$: if (editingInputIsRichText($focusedInput)) {
|
||||
surrounder.richText = $focusedInput;
|
||||
disabled = false;
|
||||
} else {
|
||||
surrounder.disable();
|
||||
disabled = true;
|
||||
}
|
||||
|
||||
function setTextColor(): void {
|
||||
surrounder.overwriteSurround(format);
|
||||
surrounder.overwriteSurround(key);
|
||||
}
|
||||
|
||||
const setCombination = "F7";
|
||||
const pickCombination = "F8";
|
||||
|
||||
let disabled: boolean;
|
||||
|
||||
onMount(() =>
|
||||
singleCallback(
|
||||
surrounder.active.subscribe((value) => (disabled = !value)),
|
||||
surrounder.registerFormat(key, format),
|
||||
),
|
||||
);
|
||||
</script>
|
||||
|
||||
<WithColorHelper {color} let:colorHelperIcon let:setColor>
|
||||
|
@ -3,15 +3,16 @@ Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
|
||||
import IconButton from "../../components/IconButton.svelte";
|
||||
import Shortcut from "../../components/Shortcut.svelte";
|
||||
import WithState from "../../components/WithState.svelte";
|
||||
import type { MatchType } from "../../domlib/surround";
|
||||
import * as tr from "../../lib/ftl";
|
||||
import { getPlatformString } from "../../lib/shortcuts";
|
||||
import { context as noteEditorContext } from "../NoteEditor.svelte";
|
||||
import { editingInputIsRichText } from "../rich-text-input";
|
||||
import { Surrounder } from "../surround";
|
||||
import { singleCallback } from "../../lib/typing";
|
||||
import { surrounder } from "../rich-text-input";
|
||||
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
|
||||
import { underlineIcon } from "./icons";
|
||||
|
||||
@ -23,7 +24,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
}
|
||||
}
|
||||
|
||||
const clearer = () => false;
|
||||
function clearer() {
|
||||
return false;
|
||||
}
|
||||
|
||||
const key = "underline";
|
||||
|
||||
const format = {
|
||||
surroundElement,
|
||||
@ -32,44 +37,36 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
};
|
||||
|
||||
const namedFormat = {
|
||||
key,
|
||||
name: tr.editingUnderlineText(),
|
||||
show: true,
|
||||
active: true,
|
||||
format,
|
||||
};
|
||||
|
||||
const { removeFormats } = editorToolbarContext.get();
|
||||
removeFormats.update((formats) => [...formats, namedFormat]);
|
||||
|
||||
const { focusedInput } = noteEditorContext.get();
|
||||
const surrounder = Surrounder.make();
|
||||
let disabled: boolean;
|
||||
|
||||
$: if (editingInputIsRichText($focusedInput)) {
|
||||
surrounder.richText = $focusedInput;
|
||||
disabled = false;
|
||||
} else {
|
||||
surrounder.disable();
|
||||
disabled = true;
|
||||
}
|
||||
|
||||
function updateStateFromActiveInput(): Promise<boolean> {
|
||||
return disabled ? Promise.resolve(false) : surrounder!.isSurrounded(format);
|
||||
async function updateStateFromActiveInput(): Promise<boolean> {
|
||||
return disabled ? false : surrounder.isSurrounded(key);
|
||||
}
|
||||
|
||||
function makeUnderline(): void {
|
||||
surrounder.surround(format);
|
||||
surrounder.surround(key);
|
||||
}
|
||||
|
||||
const keyCombination = "Control+U";
|
||||
|
||||
let disabled: boolean;
|
||||
|
||||
onMount(() =>
|
||||
singleCallback(
|
||||
surrounder.active.subscribe((value) => (disabled = !value)),
|
||||
surrounder.registerFormat(key, format),
|
||||
),
|
||||
);
|
||||
</script>
|
||||
|
||||
<WithState
|
||||
key="underline"
|
||||
update={updateStateFromActiveInput}
|
||||
let:state={active}
|
||||
let:updateState
|
||||
>
|
||||
<WithState {key} update={updateStateFromActiveInput} let:state={active} let:updateState>
|
||||
<IconButton
|
||||
tooltip="{tr.editingUnderlineText()} ({getPlatformString(keyCombination)})"
|
||||
{active}
|
||||
|
@ -1,6 +1,8 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import EditorToolbar from "./EditorToolbar.svelte";
|
||||
|
||||
export type { EditorToolbarAPI } from "./EditorToolbar.svelte";
|
||||
export { default as EditorToolbar, editorToolbar } from "./EditorToolbar.svelte";
|
||||
export { default as ClozeButtons } from "./EditorToolbar.svelte";
|
||||
export default EditorToolbar;
|
||||
export { editorToolbar } from "./EditorToolbar.svelte";
|
||||
|
@ -1,4 +1,6 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
export { default as ImageHandle } from "./ImageHandle.svelte";
|
||||
import ImageHandle from "./ImageHandle.svelte";
|
||||
|
||||
export default ImageHandle;
|
||||
|
@ -1,4 +1,6 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
export { default as MathjaxHandle } from "./MathjaxHandle.svelte";
|
||||
import MathjaxHandle from "./MathjaxHandle.svelte";
|
||||
|
||||
export default MathjaxHandle;
|
||||
|
@ -1,8 +1,8 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import PlainTextInput from "./PlainTextInput.svelte";
|
||||
|
||||
export type { PlainTextInputAPI } from "./PlainTextInput.svelte";
|
||||
export {
|
||||
parsingInstructions,
|
||||
default as PlainTextInput,
|
||||
} from "./PlainTextInput.svelte";
|
||||
export default PlainTextInput;
|
||||
export * from "./PlainTextInput.svelte";
|
||||
|
@ -6,9 +6,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
import type { ContentEditableAPI } from "../../editable/ContentEditable.svelte";
|
||||
import type { InputHandlerAPI } from "../../sveltelib/input-handler";
|
||||
import type { EditingInputAPI, FocusableInputAPI } from "../EditingArea.svelte";
|
||||
import type { SurroundedAPI } from "../surround";
|
||||
import type CustomStyles from "./CustomStyles.svelte";
|
||||
|
||||
export interface RichTextInputAPI extends EditingInputAPI {
|
||||
export interface RichTextInputAPI extends EditingInputAPI, SurroundedAPI {
|
||||
name: "rich-text";
|
||||
/** This is the contentEditable anki-editable element */
|
||||
element: Promise<HTMLElement>;
|
||||
@ -21,7 +22,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
customStyles: Promise<CustomStyles>;
|
||||
}
|
||||
|
||||
export function editingInputIsRichText(
|
||||
function editingInputIsRichText(
|
||||
editingInput: EditingInputAPI | null,
|
||||
): editingInput is RichTextInputAPI {
|
||||
return editingInput?.name === "rich-text";
|
||||
@ -30,20 +31,28 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
import { registerPackage } from "../../lib/runtime-require";
|
||||
import contextProperty from "../../sveltelib/context-property";
|
||||
import lifecycleHooks from "../../sveltelib/lifecycle-hooks";
|
||||
import { Surrounder } from "../surround";
|
||||
|
||||
const key = Symbol("richText");
|
||||
const [context, setContextProperty] = contextProperty<RichTextInputAPI>(key);
|
||||
const [globalInputHandler, setupGlobalInputHandler] = useInputHandler();
|
||||
const [lifecycle, instances, setupLifecycleHooks] =
|
||||
lifecycleHooks<RichTextInputAPI>();
|
||||
const surrounder = Surrounder.make();
|
||||
|
||||
registerPackage("anki/RichTextInput", {
|
||||
context,
|
||||
surrounder,
|
||||
lifecycle,
|
||||
instances,
|
||||
});
|
||||
|
||||
export { context, globalInputHandler as inputHandler };
|
||||
export {
|
||||
context,
|
||||
editingInputIsRichText,
|
||||
globalInputHandler as inputHandler,
|
||||
surrounder,
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
@ -52,12 +61,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import { placeCaretAfterContent } from "../../domlib/place-caret";
|
||||
import ContentEditable from "../../editable/ContentEditable.svelte";
|
||||
import {
|
||||
descriptionKey,
|
||||
directionKey,
|
||||
fontFamilyKey,
|
||||
fontSizeKey,
|
||||
} from "../../lib/context-keys";
|
||||
import { directionKey, fontFamilyKey, fontSizeKey } from "../../lib/context-keys";
|
||||
import { promiseWithResolver } from "../../lib/promise";
|
||||
import { singleCallback } from "../../lib/typing";
|
||||
import useDOMMirror from "../../sveltelib/dom-mirror";
|
||||
@ -75,7 +79,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
const { focusedInput } = noteEditorContext.get();
|
||||
const { content, editingInputs } = editingAreaContext.get();
|
||||
|
||||
const description = getContext<Readable<string>>(descriptionKey);
|
||||
const fontFamily = getContext<Readable<string>>(fontFamilyKey);
|
||||
const fontSize = getContext<Readable<number>>(fontSizeKey);
|
||||
const direction = getContext<Readable<"ltr" | "rtl">>(directionKey);
|
||||
@ -173,6 +176,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
function setFocus(): void {
|
||||
$focusedInput = api;
|
||||
surrounder.enable(api);
|
||||
|
||||
// We do not unset focusedInput here.
|
||||
// If we did, UI components for the input would react the store
|
||||
@ -180,6 +184,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
// field right away.
|
||||
}
|
||||
|
||||
function removeFocus(): void {
|
||||
surrounder.disable();
|
||||
}
|
||||
|
||||
$: pushUpdate(!hidden);
|
||||
|
||||
onMount(() => {
|
||||
@ -200,18 +208,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
setupLifecycleHooks(api);
|
||||
</script>
|
||||
|
||||
<div class="rich-text-input" on:focusin={setFocus} {hidden}>
|
||||
{#if $content.length === 0}
|
||||
<div
|
||||
class="rich-text-placeholder"
|
||||
style:font-family={$fontFamily}
|
||||
style:font-size={$fontSize + "px"}
|
||||
style:direction={$direction}
|
||||
>
|
||||
{$description}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="rich-text-input" {hidden} on:focusin={setFocus} on:focusout={removeFocus}>
|
||||
<RichTextStyles
|
||||
color={$pageTheme.isDark ? "white" : "black"}
|
||||
fontFamily={$fontFamily}
|
||||
@ -243,19 +240,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
<style lang="scss">
|
||||
.rich-text-input {
|
||||
position: relative;
|
||||
margin: 6px;
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.rich-text-placeholder {
|
||||
position: absolute;
|
||||
color: var(--disabled);
|
||||
|
||||
/* Adopts same size as the content editable element */
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
/* Keep text on single line and hide overflow */
|
||||
white-space: nowrap;
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
@ -1,9 +1,8 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import { default as RichTextInput } from "./RichTextInput.svelte";
|
||||
|
||||
export type { RichTextInputAPI } from "./RichTextInput.svelte";
|
||||
export {
|
||||
context,
|
||||
editingInputIsRichText,
|
||||
default as RichTextInput,
|
||||
} from "./RichTextInput.svelte";
|
||||
export default RichTextInput;
|
||||
export * from "./RichTextInput.svelte";
|
||||
|
@ -1,7 +1,8 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import { get } from "svelte/store";
|
||||
import type { Writable } from "svelte/store";
|
||||
import { get, writable } from "svelte/store";
|
||||
|
||||
import type { Matcher } from "../domlib/find-above";
|
||||
import { findClosest } from "../domlib/find-above";
|
||||
@ -10,7 +11,11 @@ import { boolMatcher, reformat, surround, unsurround } from "../domlib/surround"
|
||||
import { getRange, getSelection } from "../lib/cross-browser";
|
||||
import { registerPackage } from "../lib/runtime-require";
|
||||
import type { TriggerItem } from "../sveltelib/handler-list";
|
||||
import type { RichTextInputAPI } from "./rich-text-input";
|
||||
import type { InputHandlerAPI } from "../sveltelib/input-handler";
|
||||
|
||||
function isValid<T>(value: T | undefined): value is T {
|
||||
return Boolean(value);
|
||||
}
|
||||
|
||||
function isSurroundedInner(
|
||||
range: AbstractRange,
|
||||
@ -57,35 +62,54 @@ function removeFormats(
|
||||
return surroundRange;
|
||||
}
|
||||
|
||||
export class Surrounder {
|
||||
static make(): Surrounder {
|
||||
export interface SurroundedAPI {
|
||||
element: Promise<HTMLElement>;
|
||||
inputHandler: InputHandlerAPI;
|
||||
}
|
||||
|
||||
export class Surrounder<T = unknown> {
|
||||
static make<T>(): Surrounder<T> {
|
||||
return new Surrounder();
|
||||
}
|
||||
|
||||
private api: RichTextInputAPI | null = null;
|
||||
private trigger: TriggerItem<{ event: InputEvent; text: Text }> | null = null;
|
||||
private api: SurroundedAPI | null = null;
|
||||
private triggers: Map<string, TriggerItem<{ event: InputEvent; text: Text }>> =
|
||||
new Map();
|
||||
|
||||
set richText(api: RichTextInputAPI) {
|
||||
active: Writable<boolean> = writable(false);
|
||||
|
||||
enable(api: SurroundedAPI): void {
|
||||
this.api = api;
|
||||
this.trigger = api.inputHandler.insertText.trigger({ once: true });
|
||||
this.active.set(true);
|
||||
|
||||
for (const key of this.formats.keys()) {
|
||||
this.triggers.set(
|
||||
key,
|
||||
this.api.inputHandler.insertText.trigger({ once: true }),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* After calling disable, using any of the surrounding methods will throw an
|
||||
* exception. Make sure to set the rich text before trying to use them again.
|
||||
* exception. Make sure to set the input before trying to use them again.
|
||||
*/
|
||||
disable(): void {
|
||||
this.api = null;
|
||||
this.trigger?.off();
|
||||
this.trigger = null;
|
||||
this.active.set(false);
|
||||
|
||||
for (const [key, trigger] of this.triggers) {
|
||||
trigger.off();
|
||||
this.triggers.delete(key);
|
||||
}
|
||||
}
|
||||
|
||||
private async _assert_base(): Promise<HTMLElement> {
|
||||
if (!this.api) {
|
||||
throw new Error("No rich text set");
|
||||
throw new Error("Surrounder: No input set");
|
||||
}
|
||||
|
||||
return await this.api.element;
|
||||
return this.api.element;
|
||||
}
|
||||
|
||||
private _toggleTrigger<T>(
|
||||
@ -93,12 +117,13 @@ export class Surrounder {
|
||||
selection: Selection,
|
||||
matcher: Matcher,
|
||||
format: SurroundFormat<T>,
|
||||
trigger: TriggerItem<{ event: InputEvent; text: Text }>,
|
||||
exclusive: SurroundFormat<T>[] = [],
|
||||
): void {
|
||||
if (get(this.trigger!.active)) {
|
||||
this.trigger!.off();
|
||||
if (get(trigger.active)) {
|
||||
trigger.off();
|
||||
} else {
|
||||
this.trigger!.on(async ({ text }) => {
|
||||
trigger.on(async ({ text }) => {
|
||||
const range = new Range();
|
||||
range.selectNode(text);
|
||||
|
||||
@ -114,9 +139,10 @@ export class Surrounder {
|
||||
base: HTMLElement,
|
||||
selection: Selection,
|
||||
format: SurroundFormat<T>,
|
||||
trigger: TriggerItem<{ event: InputEvent; text: Text }>,
|
||||
exclusive: SurroundFormat<T>[] = [],
|
||||
): void {
|
||||
this.trigger!.on(async ({ text }) => {
|
||||
trigger.on(async ({ text }) => {
|
||||
const range = new Range();
|
||||
range.selectNode(text);
|
||||
|
||||
@ -132,68 +158,121 @@ export class Surrounder {
|
||||
base: HTMLElement,
|
||||
selection: Selection,
|
||||
remove: SurroundFormat<T>[],
|
||||
triggers: TriggerItem<{ event: InputEvent; text: Text }>[],
|
||||
reformat: SurroundFormat<T>[] = [],
|
||||
): void {
|
||||
this.trigger!.on(async ({ text }) => {
|
||||
const range = new Range();
|
||||
range.selectNode(text);
|
||||
triggers.map((trigger) =>
|
||||
trigger.on(async ({ text }) => {
|
||||
const range = new Range();
|
||||
range.selectNode(text);
|
||||
|
||||
const clearedRange = removeFormats(range, base, remove, reformat);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(clearedRange);
|
||||
selection.collapseToEnd();
|
||||
});
|
||||
const clearedRange = removeFormats(range, base, remove, reformat);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(clearedRange);
|
||||
selection.collapseToEnd();
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
private formats: Map<string, SurroundFormat<T>> = new Map();
|
||||
|
||||
/**
|
||||
* Register a surround format under a certain name.
|
||||
* This name is then used with the surround functions to actually apply or
|
||||
* remove the given format
|
||||
*/
|
||||
registerFormat(key: string, format: SurroundFormat<T>): () => void {
|
||||
this.formats.set(key, format);
|
||||
|
||||
if (this.api) {
|
||||
this.triggers.set(
|
||||
key,
|
||||
this.api.inputHandler.insertText.trigger({ once: true }),
|
||||
);
|
||||
}
|
||||
|
||||
return () => this.formats.delete(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the surround command on the current range of the RichTextInput.
|
||||
* Check if a surround format under the given key is registered.
|
||||
*/
|
||||
hasFormat(key: string): boolean {
|
||||
return this.formats.has(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the surround command on the current range of the input.
|
||||
* If the range is already surrounded, it will unsurround instead.
|
||||
*/
|
||||
async surround<T>(
|
||||
format: SurroundFormat<T>,
|
||||
exclusive: SurroundFormat<T>[] = [],
|
||||
): Promise<void> {
|
||||
async surround(formatName: string, exclusiveNames: string[] = []): Promise<void> {
|
||||
const base = await this._assert_base();
|
||||
const selection = getSelection(base)!;
|
||||
const range = getRange(selection);
|
||||
const matcher = boolMatcher(format);
|
||||
const format = this.formats.get(formatName);
|
||||
const trigger = this.triggers.get(formatName);
|
||||
|
||||
if (!range) {
|
||||
if (!format || !range || !trigger) {
|
||||
return;
|
||||
}
|
||||
|
||||
const matcher = boolMatcher(format);
|
||||
|
||||
const exclusives = exclusiveNames
|
||||
.map((name) => this.formats.get(name))
|
||||
.filter(isValid);
|
||||
|
||||
if (range.collapsed) {
|
||||
return this._toggleTrigger(base, selection, matcher, format, exclusive);
|
||||
return this._toggleTrigger(
|
||||
base,
|
||||
selection,
|
||||
matcher,
|
||||
format,
|
||||
trigger,
|
||||
exclusives,
|
||||
);
|
||||
}
|
||||
|
||||
const clearedRange = removeFormats(range, base, exclusive);
|
||||
const clearedRange = removeFormats(range, base, exclusives);
|
||||
const matches = isSurroundedInner(clearedRange, base, matcher);
|
||||
surroundAndSelect(matches, clearedRange, base, format, selection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use the surround command on the current range of the RichTextInput.
|
||||
* Use the surround command on the current range of the input.
|
||||
* If the range is already surrounded, it will overwrite the format.
|
||||
* This might be better suited if the surrounding is parameterized (like
|
||||
* text color).
|
||||
*/
|
||||
async overwriteSurround<T>(
|
||||
format: SurroundFormat<T>,
|
||||
exclusive: SurroundFormat<T>[] = [],
|
||||
async overwriteSurround(
|
||||
formatName: string,
|
||||
exclusiveNames: string[] = [],
|
||||
): Promise<void> {
|
||||
const base = await this._assert_base();
|
||||
const selection = getSelection(base)!;
|
||||
const range = getRange(selection);
|
||||
const format = this.formats.get(formatName);
|
||||
const trigger = this.triggers.get(formatName);
|
||||
|
||||
if (!range) {
|
||||
if (!format || !range || !trigger) {
|
||||
return;
|
||||
}
|
||||
|
||||
const exclusives = exclusiveNames
|
||||
.map((name) => this.formats.get(name))
|
||||
.filter(isValid);
|
||||
|
||||
if (range.collapsed) {
|
||||
return this._toggleTriggerOverwrite(base, selection, format, exclusive);
|
||||
return this._toggleTriggerOverwrite(
|
||||
base,
|
||||
selection,
|
||||
format,
|
||||
trigger,
|
||||
exclusives,
|
||||
);
|
||||
}
|
||||
|
||||
const clearedRange = removeFormats(range, base, exclusive);
|
||||
const clearedRange = removeFormats(range, base, exclusives);
|
||||
const surroundedRange = surround(clearedRange, base, format);
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(surroundedRange);
|
||||
@ -205,26 +284,25 @@ export class Surrounder {
|
||||
* provided format, OR if a surround trigger is active (surround on next
|
||||
* text insert).
|
||||
*/
|
||||
async isSurrounded<T>(format: SurroundFormat<T>): Promise<boolean> {
|
||||
async isSurrounded(formatName: string): Promise<boolean> {
|
||||
const base = await this._assert_base();
|
||||
const selection = getSelection(base)!;
|
||||
const range = getRange(selection);
|
||||
const format = this.formats.get(formatName);
|
||||
const trigger = this.triggers.get(formatName);
|
||||
|
||||
if (!range) {
|
||||
if (!format || !range || !trigger) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isSurrounded = isSurroundedInner(range, base, boolMatcher(format));
|
||||
return get(this.trigger!.active) ? !isSurrounded : isSurrounded;
|
||||
return get(trigger.active) ? !isSurrounded : isSurrounded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear/Reformat the provided formats in the current range.
|
||||
*/
|
||||
async remove<T>(
|
||||
formats: SurroundFormat<T>[],
|
||||
reformats: SurroundFormat<T>[] = [],
|
||||
): Promise<void> {
|
||||
async remove(formatNames: string[], reformatNames: string[] = []): Promise<void> {
|
||||
const base = await this._assert_base();
|
||||
const selection = getSelection(base)!;
|
||||
const range = getRange(selection);
|
||||
@ -233,8 +311,26 @@ export class Surrounder {
|
||||
return;
|
||||
}
|
||||
|
||||
const formats = formatNames
|
||||
.map((name) => this.formats.get(name))
|
||||
.filter(isValid);
|
||||
|
||||
const triggers = formatNames
|
||||
.map((name) => this.triggers.get(name))
|
||||
.filter(isValid);
|
||||
|
||||
const reformats = reformatNames
|
||||
.map((name) => this.formats.get(name))
|
||||
.filter(isValid);
|
||||
|
||||
if (range.collapsed) {
|
||||
return this._toggleTriggerRemove(base, selection, formats, reformats);
|
||||
return this._toggleTriggerRemove(
|
||||
base,
|
||||
selection,
|
||||
formats,
|
||||
triggers,
|
||||
reformats,
|
||||
);
|
||||
}
|
||||
|
||||
const surroundedRange = removeFormats(range, base, formats, reformats);
|
||||
|
Loading…
Reference in New Issue
Block a user