mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
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 b62705eadf
.
SvelteActionReturnType does not accept Promise<void>
* Fix mess after merge commit
* Require registering surround formats
This commit is contained in:
parent
d5945a213a
commit
9ca13ca3bc
20 changed files with 460 additions and 364 deletions
|
@ -169,14 +169,15 @@ export class FormattingNode<T = never> extends TreeNode {
|
||||||
* When surrounding "inside" with a bold format in the following case:
|
* When surrounding "inside" with a bold format in the following case:
|
||||||
* `<span class="myclass"><em>inside</em></span>`
|
* `<span class="myclass"><em>inside</em></span>`
|
||||||
* The formatting node would sit above the span (it ascends above both
|
* 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
|
* @example
|
||||||
* When a format only wants to add a class, it would typically look for an
|
* 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
|
* extension first. When applying class="myclass" to "inside" in the
|
||||||
* following case:
|
* following case:
|
||||||
* `<em><span style="color: rgb(255, 0, 0)"><b>inside</b></span></em>`
|
* `<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>`
|
* `<em><span class="myclass" style="color: rgb(255, 0, 0)"><b>inside</b></span></em>`
|
||||||
*/
|
*/
|
||||||
extensions: (HTMLElement | SVGElement)[] = [];
|
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 type { Writable } from "svelte/store";
|
||||||
import { 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 { promiseWithResolver } from "../lib/promise";
|
||||||
import type { Destroyable } from "./destroyable";
|
import type { Destroyable } from "./destroyable";
|
||||||
import EditingArea from "./EditingArea.svelte";
|
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;
|
$: $directionStore = field.direction;
|
||||||
|
|
||||||
const descriptionStore = writable<string>();
|
|
||||||
setContext(descriptionKey, descriptionStore);
|
|
||||||
|
|
||||||
$: $descriptionStore = field.description;
|
|
||||||
|
|
||||||
const editingArea: Partial<EditingAreaAPI> = {};
|
const editingArea: Partial<EditingAreaAPI> = {};
|
||||||
const [element, elementResolve] = promiseWithResolver<HTMLElement>();
|
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 DecoratedElements from "./DecoratedElements.svelte";
|
||||||
import { clearableArray } from "./destroyable";
|
import { clearableArray } from "./destroyable";
|
||||||
import DuplicateLink from "./DuplicateLink.svelte";
|
import DuplicateLink from "./DuplicateLink.svelte";
|
||||||
import { EditorToolbar } from "./editor-toolbar";
|
import EditorToolbar from "./editor-toolbar";
|
||||||
import type { FieldData } from "./EditorField.svelte";
|
import type { FieldData } from "./EditorField.svelte";
|
||||||
import EditorField from "./EditorField.svelte";
|
import EditorField from "./EditorField.svelte";
|
||||||
|
import FieldDescription from "./FieldDescription.svelte";
|
||||||
import Fields from "./Fields.svelte";
|
import Fields from "./Fields.svelte";
|
||||||
import FieldsEditor from "./FieldsEditor.svelte";
|
import FieldsEditor from "./FieldsEditor.svelte";
|
||||||
import FrameElement from "./FrameElement.svelte";
|
import FrameElement from "./FrameElement.svelte";
|
||||||
import { alertIcon } from "./icons";
|
import { alertIcon } from "./icons";
|
||||||
import { ImageHandle } from "./image-overlay";
|
import ImageHandle from "./image-overlay";
|
||||||
import { MathjaxHandle } from "./mathjax-overlay";
|
import MathjaxHandle from "./mathjax-overlay";
|
||||||
import MathjaxElement from "./MathjaxElement.svelte";
|
import MathjaxElement from "./MathjaxElement.svelte";
|
||||||
import Notification from "./Notification.svelte";
|
import Notification from "./Notification.svelte";
|
||||||
import { PlainTextInput } from "./plain-text-input";
|
import PlainTextInput from "./plain-text-input";
|
||||||
import PlainTextBadge from "./PlainTextBadge.svelte";
|
import PlainTextBadge from "./PlainTextBadge.svelte";
|
||||||
import { editingInputIsRichText, RichTextInput } from "./rich-text-input";
|
import RichTextInput, { editingInputIsRichText } from "./rich-text-input";
|
||||||
import RichTextBadge from "./RichTextBadge.svelte";
|
import RichTextBadge from "./RichTextBadge.svelte";
|
||||||
|
|
||||||
function quoteFontFamily(fontFamily: string): string {
|
function quoteFontFamily(fontFamily: string): string {
|
||||||
|
@ -302,9 +303,11 @@ the AddCards dialog) should be implemented in the user of this component.
|
||||||
<Fields>
|
<Fields>
|
||||||
<DecoratedElements>
|
<DecoratedElements>
|
||||||
{#each fieldsData as field, index}
|
{#each fieldsData as field, index}
|
||||||
|
{@const content = fieldStores[index]}
|
||||||
|
|
||||||
<EditorField
|
<EditorField
|
||||||
{field}
|
{field}
|
||||||
content={fieldStores[index]}
|
{content}
|
||||||
api={fields[index]}
|
api={fields[index]}
|
||||||
on:focusin={() => {
|
on:focusin={() => {
|
||||||
$focusedField = fields[index];
|
$focusedField = fields[index];
|
||||||
|
@ -313,9 +316,7 @@ the AddCards dialog) should be implemented in the user of this component.
|
||||||
on:focusout={() => {
|
on:focusout={() => {
|
||||||
$focusedField = null;
|
$focusedField = null;
|
||||||
bridgeCommand(
|
bridgeCommand(
|
||||||
`blur:${index}:${getNoteId()}:${get(
|
`blur:${index}:${getNoteId()}:${get(content)}`,
|
||||||
fieldStores[index],
|
|
||||||
)}`,
|
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
--label-color={cols[index] === "dupe"
|
--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} />
|
<ImageHandle maxWidth={250} maxHeight={125} />
|
||||||
<MathjaxHandle />
|
<MathjaxHandle />
|
||||||
|
<FieldDescription>
|
||||||
|
{field.description}
|
||||||
|
</FieldDescription>
|
||||||
</RichTextInput>
|
</RichTextInput>
|
||||||
|
|
||||||
<PlainTextInput
|
<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
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
import IconButton from "../../components/IconButton.svelte";
|
import IconButton from "../../components/IconButton.svelte";
|
||||||
import Shortcut from "../../components/Shortcut.svelte";
|
import Shortcut from "../../components/Shortcut.svelte";
|
||||||
import WithState from "../../components/WithState.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 * as tr from "../../lib/ftl";
|
||||||
import { getPlatformString } from "../../lib/shortcuts";
|
import { getPlatformString } from "../../lib/shortcuts";
|
||||||
import { removeStyleProperties } from "../../lib/styling";
|
import { removeStyleProperties } from "../../lib/styling";
|
||||||
import { context as noteEditorContext } from "../NoteEditor.svelte";
|
import { singleCallback } from "../../lib/typing";
|
||||||
import { editingInputIsRichText } from "../rich-text-input";
|
import { surrounder } from "../rich-text-input";
|
||||||
import { Surrounder } from "../surround";
|
|
||||||
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
|
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
|
||||||
import { boldIcon } from "./icons";
|
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 = {
|
const format = {
|
||||||
surroundElement,
|
surroundElement,
|
||||||
matcher,
|
matcher,
|
||||||
};
|
};
|
||||||
|
|
||||||
const namedFormat = {
|
const namedFormat = {
|
||||||
|
key,
|
||||||
name: tr.editingBoldText(),
|
name: tr.editingBoldText(),
|
||||||
show: true,
|
show: true,
|
||||||
active: true,
|
active: true,
|
||||||
format,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { removeFormats } = editorToolbarContext.get();
|
const { removeFormats } = editorToolbarContext.get();
|
||||||
removeFormats.update((formats) => [...formats, namedFormat]);
|
removeFormats.update((formats) => [...formats, namedFormat]);
|
||||||
|
|
||||||
const { focusedInput } = noteEditorContext.get();
|
async function updateStateFromActiveInput(): Promise<boolean> {
|
||||||
const surrounder = Surrounder.make();
|
return disabled ? false : surrounder.isSurrounded(key);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeBold(): void {
|
function makeBold(): void {
|
||||||
surrounder.surround(format);
|
surrounder.surround(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyCombination = "Control+B";
|
const keyCombination = "Control+B";
|
||||||
|
|
||||||
|
let disabled: boolean;
|
||||||
|
|
||||||
|
onMount(() =>
|
||||||
|
singleCallback(
|
||||||
|
surrounder.active.subscribe((value) => (disabled = !value)),
|
||||||
|
surrounder.registerFormat(key, format),
|
||||||
|
),
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<WithState
|
<WithState {key} update={updateStateFromActiveInput} let:state={active} let:updateState>
|
||||||
key="bold"
|
|
||||||
update={updateStateFromActiveInput}
|
|
||||||
let:state={active}
|
|
||||||
let:updateState
|
|
||||||
>
|
|
||||||
<IconButton
|
<IconButton
|
||||||
tooltip="{tr.editingBoldText()} ({getPlatformString(keyCombination)})"
|
tooltip="{tr.editingBoldText()} ({getPlatformString(keyCombination)})"
|
||||||
{active}
|
{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 type { Writable } from "svelte/store";
|
||||||
|
|
||||||
import { resetAllState, updateAllState } from "../../components/WithState.svelte";
|
import { resetAllState, updateAllState } from "../../components/WithState.svelte";
|
||||||
import type { SurroundFormat } from "../../domlib/surround";
|
|
||||||
import type { DefaultSlotInterface } from "../../sveltelib/dynamic-slotting";
|
import type { DefaultSlotInterface } from "../../sveltelib/dynamic-slotting";
|
||||||
|
|
||||||
export function updateActiveButtons(event: Event) {
|
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);
|
resetAllState(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RemoveFormat<T> {
|
export interface RemoveFormat {
|
||||||
name: string;
|
name: string;
|
||||||
|
key: string;
|
||||||
show: boolean;
|
show: boolean;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
format: SurroundFormat<T>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EditorToolbarAPI {
|
export interface EditorToolbarAPI {
|
||||||
|
@ -30,7 +29,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
inlineButtons: DefaultSlotInterface;
|
inlineButtons: DefaultSlotInterface;
|
||||||
blockButtons: DefaultSlotInterface;
|
blockButtons: DefaultSlotInterface;
|
||||||
templateButtons: DefaultSlotInterface;
|
templateButtons: DefaultSlotInterface;
|
||||||
removeFormats: Writable<RemoveFormat<any>[]>;
|
removeFormats: Writable<RemoveFormat[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Our dynamic components */
|
/* 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 inlineButtons = {} as DefaultSlotInterface;
|
||||||
const blockButtons = {} as DefaultSlotInterface;
|
const blockButtons = {} as DefaultSlotInterface;
|
||||||
const templateButtons = {} as DefaultSlotInterface;
|
const templateButtons = {} as DefaultSlotInterface;
|
||||||
const removeFormats = writable<RemoveFormat<any>[]>([]);
|
const removeFormats = writable<RemoveFormat[]>([]);
|
||||||
|
|
||||||
let apiPartial: Partial<EditorToolbarAPI> = {};
|
let apiPartial: Partial<EditorToolbarAPI> = {};
|
||||||
export { apiPartial as api };
|
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
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
import IconButton from "../../components/IconButton.svelte";
|
import IconButton from "../../components/IconButton.svelte";
|
||||||
import type {
|
import type { FormattingNode, MatchType } from "../../domlib/surround";
|
||||||
FormattingNode,
|
|
||||||
MatchType,
|
|
||||||
SurroundFormat,
|
|
||||||
} from "../../domlib/surround";
|
|
||||||
import { bridgeCommand } from "../../lib/bridgecommand";
|
import { bridgeCommand } from "../../lib/bridgecommand";
|
||||||
import * as tr from "../../lib/ftl";
|
import * as tr from "../../lib/ftl";
|
||||||
import { removeStyleProperties } from "../../lib/styling";
|
import { removeStyleProperties } from "../../lib/styling";
|
||||||
import { context as noteEditorContext } from "../NoteEditor.svelte";
|
import { singleCallback } from "../../lib/typing";
|
||||||
import { editingInputIsRichText } from "../rich-text-input";
|
import { surrounder } from "../rich-text-input";
|
||||||
import { Surrounder } from "../surround";
|
|
||||||
import ColorPicker from "./ColorPicker.svelte";
|
import ColorPicker from "./ColorPicker.svelte";
|
||||||
import type { RemoveFormat } from "./EditorToolbar.svelte";
|
|
||||||
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
|
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
|
||||||
import { arrowIcon, highlightColorIcon } from "./icons";
|
import { arrowIcon, highlightColorIcon } from "./icons";
|
||||||
import WithColorHelper from "./WithColorHelper.svelte";
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const format: SurroundFormat<string> = {
|
const key = "highlightColor";
|
||||||
|
|
||||||
|
const format = {
|
||||||
matcher,
|
matcher,
|
||||||
merger,
|
merger,
|
||||||
formatter,
|
formatter,
|
||||||
};
|
};
|
||||||
|
|
||||||
const namedFormat: RemoveFormat<string> = {
|
const namedFormat = {
|
||||||
|
key,
|
||||||
name: tr.editingTextHighlightColor(),
|
name: tr.editingTextHighlightColor(),
|
||||||
show: true,
|
show: true,
|
||||||
active: true,
|
active: true,
|
||||||
format,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { removeFormats } = editorToolbarContext.get();
|
const { removeFormats } = editorToolbarContext.get();
|
||||||
removeFormats.update((formats) => [...formats, namedFormat]);
|
removeFormats.update((formats) => [...formats, namedFormat]);
|
||||||
|
|
||||||
const { focusedInput } = noteEditorContext.get();
|
function setTextColor(): void {
|
||||||
const surrounder = Surrounder.make();
|
surrounder.overwriteSurround(key);
|
||||||
|
}
|
||||||
|
|
||||||
let disabled: boolean;
|
let disabled: boolean;
|
||||||
|
|
||||||
$: if (editingInputIsRichText($focusedInput)) {
|
onMount(() =>
|
||||||
disabled = false;
|
singleCallback(
|
||||||
surrounder.richText = $focusedInput;
|
surrounder.active.subscribe((value) => (disabled = !value)),
|
||||||
} else {
|
surrounder.registerFormat(key, format),
|
||||||
disabled = true;
|
),
|
||||||
surrounder.disable();
|
);
|
||||||
}
|
|
||||||
|
|
||||||
function setTextColor(): void {
|
|
||||||
surrounder.overwriteSurround(format);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<WithColorHelper {color} let:colorHelperIcon let:setColor>
|
<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
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
import IconButton from "../../components/IconButton.svelte";
|
import IconButton from "../../components/IconButton.svelte";
|
||||||
import Shortcut from "../../components/Shortcut.svelte";
|
import Shortcut from "../../components/Shortcut.svelte";
|
||||||
import WithState from "../../components/WithState.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 * as tr from "../../lib/ftl";
|
||||||
import { getPlatformString } from "../../lib/shortcuts";
|
import { getPlatformString } from "../../lib/shortcuts";
|
||||||
import { removeStyleProperties } from "../../lib/styling";
|
import { removeStyleProperties } from "../../lib/styling";
|
||||||
import { context as noteEditorContext } from "../NoteEditor.svelte";
|
import { singleCallback } from "../../lib/typing";
|
||||||
import { editingInputIsRichText } from "../rich-text-input";
|
import { surrounder } from "../rich-text-input";
|
||||||
import { Surrounder } from "../surround";
|
|
||||||
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
|
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
|
||||||
import { italicIcon } from "./icons";
|
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 = {
|
const format = {
|
||||||
surroundElement,
|
surroundElement,
|
||||||
matcher,
|
matcher,
|
||||||
};
|
};
|
||||||
|
|
||||||
const namedFormat = {
|
const namedFormat = {
|
||||||
|
key,
|
||||||
name: tr.editingItalicText(),
|
name: tr.editingItalicText(),
|
||||||
show: true,
|
show: true,
|
||||||
active: true,
|
active: true,
|
||||||
format,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { removeFormats } = editorToolbarContext.get();
|
const { removeFormats } = editorToolbarContext.get();
|
||||||
removeFormats.update((formats) => [...formats, namedFormat]);
|
removeFormats.update((formats) => [...formats, namedFormat]);
|
||||||
|
|
||||||
const { focusedInput } = noteEditorContext.get();
|
async function updateStateFromActiveInput(): Promise<boolean> {
|
||||||
const surrounder = Surrounder.make();
|
return disabled ? false : surrounder.isSurrounded(key);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeItalic(): void {
|
function makeItalic(): void {
|
||||||
surrounder.surround(format);
|
surrounder.surround(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyCombination = "Control+I";
|
const keyCombination = "Control+I";
|
||||||
|
|
||||||
|
let disabled: boolean;
|
||||||
|
|
||||||
|
onMount(() =>
|
||||||
|
singleCallback(
|
||||||
|
surrounder.active.subscribe((value) => (disabled = !value)),
|
||||||
|
surrounder.registerFormat(key, format),
|
||||||
|
),
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<WithState
|
<WithState {key} update={updateStateFromActiveInput} let:state={active} let:updateState>
|
||||||
key="italic"
|
|
||||||
update={updateStateFromActiveInput}
|
|
||||||
let:state={active}
|
|
||||||
let:updateState
|
|
||||||
>
|
|
||||||
<IconButton
|
<IconButton
|
||||||
tooltip="{tr.editingItalicText()} ({getPlatformString(keyCombination)})"
|
tooltip="{tr.editingItalicText()} ({getPlatformString(keyCombination)})"
|
||||||
{active}
|
{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
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
import Checkbox from "../../components/CheckBox.svelte";
|
import Checkbox from "../../components/CheckBox.svelte";
|
||||||
import DropdownItem from "../../components/DropdownItem.svelte";
|
import DropdownItem from "../../components/DropdownItem.svelte";
|
||||||
import DropdownMenu from "../../components/DropdownMenu.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 IconButton from "../../components/IconButton.svelte";
|
||||||
import Shortcut from "../../components/Shortcut.svelte";
|
import Shortcut from "../../components/Shortcut.svelte";
|
||||||
import WithDropdown from "../../components/WithDropdown.svelte";
|
import WithDropdown from "../../components/WithDropdown.svelte";
|
||||||
import type { SurroundFormat } from "../../domlib/surround";
|
|
||||||
import type { MatchType } from "../../domlib/surround";
|
import type { MatchType } from "../../domlib/surround";
|
||||||
import * as tr from "../../lib/ftl";
|
import * as tr from "../../lib/ftl";
|
||||||
import { altPressed } from "../../lib/keys";
|
import { altPressed } from "../../lib/keys";
|
||||||
import { getPlatformString } from "../../lib/shortcuts";
|
import { getPlatformString } from "../../lib/shortcuts";
|
||||||
import { context as noteEditorContext } from "../NoteEditor.svelte";
|
import { singleCallback } from "../../lib/typing";
|
||||||
import { editingInputIsRichText } from "../rich-text-input";
|
import { surrounder } from "../rich-text-input";
|
||||||
import { Surrounder } from "../surround";
|
|
||||||
import type { RemoveFormat } from "./EditorToolbar.svelte";
|
import type { RemoveFormat } from "./EditorToolbar.svelte";
|
||||||
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
|
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
|
||||||
import { eraserIcon } from "./icons";
|
import { eraserIcon } from "./icons";
|
||||||
import { arrowIcon } from "./icons";
|
import { arrowIcon } from "./icons";
|
||||||
|
|
||||||
const { focusedInput } = noteEditorContext.get();
|
const { removeFormats } = editorToolbarContext.get();
|
||||||
const surrounder = Surrounder.make();
|
|
||||||
let disabled: boolean;
|
|
||||||
|
|
||||||
$: if (editingInputIsRichText($focusedInput)) {
|
const surroundElement = document.createElement("span");
|
||||||
surrounder.richText = $focusedInput;
|
|
||||||
disabled = false;
|
function matcher(element: HTMLElement | SVGElement, match: MatchType<never>): void {
|
||||||
} else {
|
if (
|
||||||
surrounder.disable();
|
element.tagName === "SPAN" &&
|
||||||
disabled = true;
|
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) =>
|
removeFormats.update((formats) =>
|
||||||
formats.concat({
|
formats.concat({
|
||||||
name: "simple spans",
|
key,
|
||||||
|
name: key,
|
||||||
show: false,
|
show: false,
|
||||||
active: true,
|
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>[];
|
let activeKeys: string[];
|
||||||
$: activeFormats = $removeFormats
|
$: activeKeys = $removeFormats
|
||||||
.filter((format) => format.active)
|
.filter((format) => format.active)
|
||||||
.map((format) => format.format);
|
.map((format) => format.key);
|
||||||
|
|
||||||
let inactiveFormats: SurroundFormat<any>[];
|
let inactiveKeys: string[];
|
||||||
$: inactiveFormats = $removeFormats
|
$: inactiveKeys = $removeFormats
|
||||||
.filter((format) => !format.active)
|
.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);
|
$: showFormats = $removeFormats.filter((format) => format.show);
|
||||||
|
|
||||||
function remove(): void {
|
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)) {
|
if (altPressed(event)) {
|
||||||
for (const format of showFormats) {
|
for (const format of showFormats) {
|
||||||
format.active = false;
|
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";
|
const keyCombination = "Control+R";
|
||||||
|
|
||||||
|
let disabled: boolean;
|
||||||
|
|
||||||
|
onMount(() =>
|
||||||
|
singleCallback(
|
||||||
|
surrounder.active.subscribe((value) => (disabled = !value)),
|
||||||
|
surrounder.registerFormat(key, format),
|
||||||
|
),
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
|
|
|
@ -2,9 +2,21 @@
|
||||||
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
|
||||||
-->
|
-->
|
||||||
<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 type { MatchType } from "../../domlib/surround";
|
||||||
|
import * as tr from "../../lib/ftl";
|
||||||
|
import { getPlatformString } from "../../lib/shortcuts";
|
||||||
import { removeStyleProperties } from "../../lib/styling";
|
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");
|
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,
|
surroundElement,
|
||||||
matcher,
|
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 = {
|
const namedFormat = {
|
||||||
|
key,
|
||||||
name: tr.editingSubscript(),
|
name: tr.editingSubscript(),
|
||||||
show: true,
|
show: true,
|
||||||
active: true,
|
active: true,
|
||||||
format,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { removeFormats } = editorToolbarContext.get();
|
const { removeFormats } = editorToolbarContext.get();
|
||||||
removeFormats.update((formats) => [...formats, namedFormat]);
|
removeFormats.update((formats) => [...formats, namedFormat]);
|
||||||
|
|
||||||
const { focusedInput } = noteEditorContext.get();
|
async function updateStateFromActiveInput(): Promise<boolean> {
|
||||||
const surrounder = Surrounder.make();
|
return disabled ? false : surrounder.isSurrounded(key);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeSub(): void {
|
function makeSub(): void {
|
||||||
surrounder.surround(format, [superscript]);
|
surrounder.surround(key, ["superscript"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyCombination = "Control+Shift+=";
|
const keyCombination = "Control+Shift+=";
|
||||||
|
|
||||||
|
let disabled: boolean;
|
||||||
|
|
||||||
|
onMount(() =>
|
||||||
|
singleCallback(
|
||||||
|
surrounder.active.subscribe((value) => (disabled = !value)),
|
||||||
|
surrounder.registerFormat(key, format),
|
||||||
|
),
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<WithState
|
<WithState {key} update={updateStateFromActiveInput} let:state={active} let:updateState>
|
||||||
key="sub"
|
|
||||||
update={updateStateFromActiveInput}
|
|
||||||
let:state={active}
|
|
||||||
let:updateState
|
|
||||||
>
|
|
||||||
<IconButton
|
<IconButton
|
||||||
tooltip="{tr.editingSubscript()} ({getPlatformString(keyCombination)})"
|
tooltip="{tr.editingSubscript()} ({getPlatformString(keyCombination)})"
|
||||||
{active}
|
{active}
|
||||||
|
@ -91,7 +82,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
on:click={(event) => {
|
on:click={(event) => {
|
||||||
makeSub();
|
makeSub();
|
||||||
updateState(event);
|
updateState(event);
|
||||||
updateStateByKey("super", event);
|
updateStateByKey("superscript", event);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{@html subscriptIcon}
|
{@html subscriptIcon}
|
||||||
|
@ -102,7 +93,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
on:action={(event) => {
|
on:action={(event) => {
|
||||||
makeSub();
|
makeSub();
|
||||||
updateState(event);
|
updateState(event);
|
||||||
updateStateByKey("super", event);
|
updateStateByKey("superscript", event);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</WithState>
|
</WithState>
|
||||||
|
|
|
@ -2,9 +2,21 @@
|
||||||
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
|
||||||
-->
|
-->
|
||||||
<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 type { MatchType } from "../../domlib/surround";
|
||||||
|
import * as tr from "../../lib/ftl";
|
||||||
|
import { getPlatformString } from "../../lib/shortcuts";
|
||||||
import { removeStyleProperties } from "../../lib/styling";
|
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");
|
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 = {
|
export const format = {
|
||||||
surroundElement,
|
surroundElement,
|
||||||
matcher,
|
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 = {
|
const namedFormat = {
|
||||||
|
key,
|
||||||
name: tr.editingSuperscript(),
|
name: tr.editingSuperscript(),
|
||||||
show: true,
|
show: true,
|
||||||
active: true,
|
active: true,
|
||||||
format,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { removeFormats } = editorToolbarContext.get();
|
const { removeFormats } = editorToolbarContext.get();
|
||||||
removeFormats.update((formats) => [...formats, namedFormat]);
|
removeFormats.update((formats) => [...formats, namedFormat]);
|
||||||
|
|
||||||
const { focusedInput } = noteEditorContext.get();
|
async function updateStateFromActiveInput(): Promise<boolean> {
|
||||||
const surrounder = Surrounder.make();
|
return disabled ? false : surrounder.isSurrounded(key);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeSuper(): void {
|
function makeSuper(): void {
|
||||||
surrounder.surround(format, [subscript]);
|
surrounder.surround(key, ["subscript"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyCombination = "Control+=";
|
const keyCombination = "Control+=";
|
||||||
|
|
||||||
|
let disabled: boolean;
|
||||||
|
|
||||||
|
onMount(() =>
|
||||||
|
singleCallback(
|
||||||
|
surrounder.active.subscribe((value) => (disabled = !value)),
|
||||||
|
surrounder.registerFormat(key, format),
|
||||||
|
),
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<WithState
|
<WithState {key} update={updateStateFromActiveInput} let:state={active} let:updateState>
|
||||||
key="super"
|
|
||||||
update={updateStateFromActiveInput}
|
|
||||||
let:state={active}
|
|
||||||
let:updateState
|
|
||||||
>
|
|
||||||
<IconButton
|
<IconButton
|
||||||
tooltip="{tr.editingSuperscript()} ({getPlatformString(keyCombination)})"
|
tooltip="{tr.editingSuperscript()} ({getPlatformString(keyCombination)})"
|
||||||
{active}
|
{active}
|
||||||
|
@ -91,7 +82,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
on:click={(event) => {
|
on:click={(event) => {
|
||||||
makeSuper();
|
makeSuper();
|
||||||
updateState(event);
|
updateState(event);
|
||||||
updateStateByKey("sub", event);
|
updateStateByKey("subscript", event);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{@html superscriptIcon}
|
{@html superscriptIcon}
|
||||||
|
@ -102,7 +93,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
on:action={(event) => {
|
on:action={(event) => {
|
||||||
makeSuper();
|
makeSuper();
|
||||||
updateState(event);
|
updateState(event);
|
||||||
updateStateByKey("sub", event);
|
updateStateByKey("subscript", event);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</WithState>
|
</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
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
import IconButton from "../../components/IconButton.svelte";
|
import IconButton from "../../components/IconButton.svelte";
|
||||||
import Shortcut from "../../components/Shortcut.svelte";
|
import Shortcut from "../../components/Shortcut.svelte";
|
||||||
import type {
|
import type { FormattingNode, MatchType } from "../../domlib/surround";
|
||||||
FormattingNode,
|
|
||||||
MatchType,
|
|
||||||
SurroundFormat,
|
|
||||||
} from "../../domlib/surround";
|
|
||||||
import { bridgeCommand } from "../../lib/bridgecommand";
|
import { bridgeCommand } from "../../lib/bridgecommand";
|
||||||
import * as tr from "../../lib/ftl";
|
import * as tr from "../../lib/ftl";
|
||||||
import { getPlatformString } from "../../lib/shortcuts";
|
import { getPlatformString } from "../../lib/shortcuts";
|
||||||
import { removeStyleProperties } from "../../lib/styling";
|
import { removeStyleProperties } from "../../lib/styling";
|
||||||
|
import { singleCallback } from "../../lib/typing";
|
||||||
import { withFontColor } from "../helpers";
|
import { withFontColor } from "../helpers";
|
||||||
import { context as noteEditorContext } from "../NoteEditor.svelte";
|
import { surrounder } from "../rich-text-input";
|
||||||
import { editingInputIsRichText } from "../rich-text-input";
|
|
||||||
import { Surrounder } from "../surround";
|
|
||||||
import ColorPicker from "./ColorPicker.svelte";
|
import ColorPicker from "./ColorPicker.svelte";
|
||||||
import type { RemoveFormat } from "./EditorToolbar.svelte";
|
|
||||||
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
|
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
|
||||||
import { arrowIcon, textColorIcon } from "./icons";
|
import { arrowIcon, textColorIcon } from "./icons";
|
||||||
import WithColorHelper from "./WithColorHelper.svelte";
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
const format: SurroundFormat<string> = {
|
const key = "textColor";
|
||||||
|
|
||||||
|
const format = {
|
||||||
matcher,
|
matcher,
|
||||||
merger,
|
merger,
|
||||||
formatter,
|
formatter,
|
||||||
};
|
};
|
||||||
|
|
||||||
const namedFormat: RemoveFormat<string> = {
|
const namedFormat = {
|
||||||
|
key,
|
||||||
name: tr.editingTextColor(),
|
name: tr.editingTextColor(),
|
||||||
show: true,
|
show: true,
|
||||||
active: true,
|
active: true,
|
||||||
format,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { removeFormats } = editorToolbarContext.get();
|
const { removeFormats } = editorToolbarContext.get();
|
||||||
removeFormats.update((formats) => [...formats, namedFormat]);
|
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 {
|
function setTextColor(): void {
|
||||||
surrounder.overwriteSurround(format);
|
surrounder.overwriteSurround(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
const setCombination = "F7";
|
const setCombination = "F7";
|
||||||
const pickCombination = "F8";
|
const pickCombination = "F8";
|
||||||
|
|
||||||
|
let disabled: boolean;
|
||||||
|
|
||||||
|
onMount(() =>
|
||||||
|
singleCallback(
|
||||||
|
surrounder.active.subscribe((value) => (disabled = !value)),
|
||||||
|
surrounder.registerFormat(key, format),
|
||||||
|
),
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<WithColorHelper {color} let:colorHelperIcon let:setColor>
|
<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
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
import IconButton from "../../components/IconButton.svelte";
|
import IconButton from "../../components/IconButton.svelte";
|
||||||
import Shortcut from "../../components/Shortcut.svelte";
|
import Shortcut from "../../components/Shortcut.svelte";
|
||||||
import WithState from "../../components/WithState.svelte";
|
import WithState from "../../components/WithState.svelte";
|
||||||
import type { MatchType } from "../../domlib/surround";
|
import type { MatchType } from "../../domlib/surround";
|
||||||
import * as tr from "../../lib/ftl";
|
import * as tr from "../../lib/ftl";
|
||||||
import { getPlatformString } from "../../lib/shortcuts";
|
import { getPlatformString } from "../../lib/shortcuts";
|
||||||
import { context as noteEditorContext } from "../NoteEditor.svelte";
|
import { singleCallback } from "../../lib/typing";
|
||||||
import { editingInputIsRichText } from "../rich-text-input";
|
import { surrounder } from "../rich-text-input";
|
||||||
import { Surrounder } from "../surround";
|
|
||||||
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
|
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
|
||||||
import { underlineIcon } from "./icons";
|
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 = {
|
const format = {
|
||||||
surroundElement,
|
surroundElement,
|
||||||
|
@ -32,44 +37,36 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
};
|
};
|
||||||
|
|
||||||
const namedFormat = {
|
const namedFormat = {
|
||||||
|
key,
|
||||||
name: tr.editingUnderlineText(),
|
name: tr.editingUnderlineText(),
|
||||||
show: true,
|
show: true,
|
||||||
active: true,
|
active: true,
|
||||||
format,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { removeFormats } = editorToolbarContext.get();
|
const { removeFormats } = editorToolbarContext.get();
|
||||||
removeFormats.update((formats) => [...formats, namedFormat]);
|
removeFormats.update((formats) => [...formats, namedFormat]);
|
||||||
|
|
||||||
const { focusedInput } = noteEditorContext.get();
|
async function updateStateFromActiveInput(): Promise<boolean> {
|
||||||
const surrounder = Surrounder.make();
|
return disabled ? false : surrounder.isSurrounded(key);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeUnderline(): void {
|
function makeUnderline(): void {
|
||||||
surrounder.surround(format);
|
surrounder.surround(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyCombination = "Control+U";
|
const keyCombination = "Control+U";
|
||||||
|
|
||||||
|
let disabled: boolean;
|
||||||
|
|
||||||
|
onMount(() =>
|
||||||
|
singleCallback(
|
||||||
|
surrounder.active.subscribe((value) => (disabled = !value)),
|
||||||
|
surrounder.registerFormat(key, format),
|
||||||
|
),
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<WithState
|
<WithState {key} update={updateStateFromActiveInput} let:state={active} let:updateState>
|
||||||
key="underline"
|
|
||||||
update={updateStateFromActiveInput}
|
|
||||||
let:state={active}
|
|
||||||
let:updateState
|
|
||||||
>
|
|
||||||
<IconButton
|
<IconButton
|
||||||
tooltip="{tr.editingUnderlineText()} ({getPlatformString(keyCombination)})"
|
tooltip="{tr.editingUnderlineText()} ({getPlatformString(keyCombination)})"
|
||||||
{active}
|
{active}
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
// 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 EditorToolbar from "./EditorToolbar.svelte";
|
||||||
|
|
||||||
export type { EditorToolbarAPI } from "./EditorToolbar.svelte";
|
export type { EditorToolbarAPI } from "./EditorToolbar.svelte";
|
||||||
export { default as EditorToolbar, editorToolbar } from "./EditorToolbar.svelte";
|
export default EditorToolbar;
|
||||||
export { default as ClozeButtons } from "./EditorToolbar.svelte";
|
export { editorToolbar } from "./EditorToolbar.svelte";
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
// 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
|
||||||
|
|
||||||
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
|
// 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
|
||||||
|
|
||||||
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
|
// 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 PlainTextInput from "./PlainTextInput.svelte";
|
||||||
|
|
||||||
export type { PlainTextInputAPI } from "./PlainTextInput.svelte";
|
export type { PlainTextInputAPI } from "./PlainTextInput.svelte";
|
||||||
export {
|
export default PlainTextInput;
|
||||||
parsingInstructions,
|
export * from "./PlainTextInput.svelte";
|
||||||
default as PlainTextInput,
|
|
||||||
} 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 { ContentEditableAPI } from "../../editable/ContentEditable.svelte";
|
||||||
import type { InputHandlerAPI } from "../../sveltelib/input-handler";
|
import type { InputHandlerAPI } from "../../sveltelib/input-handler";
|
||||||
import type { EditingInputAPI, FocusableInputAPI } from "../EditingArea.svelte";
|
import type { EditingInputAPI, FocusableInputAPI } from "../EditingArea.svelte";
|
||||||
|
import type { SurroundedAPI } from "../surround";
|
||||||
import type CustomStyles from "./CustomStyles.svelte";
|
import type CustomStyles from "./CustomStyles.svelte";
|
||||||
|
|
||||||
export interface RichTextInputAPI extends EditingInputAPI {
|
export interface RichTextInputAPI extends EditingInputAPI, SurroundedAPI {
|
||||||
name: "rich-text";
|
name: "rich-text";
|
||||||
/** This is the contentEditable anki-editable element */
|
/** This is the contentEditable anki-editable element */
|
||||||
element: Promise<HTMLElement>;
|
element: Promise<HTMLElement>;
|
||||||
|
@ -21,7 +22,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
customStyles: Promise<CustomStyles>;
|
customStyles: Promise<CustomStyles>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function editingInputIsRichText(
|
function editingInputIsRichText(
|
||||||
editingInput: EditingInputAPI | null,
|
editingInput: EditingInputAPI | null,
|
||||||
): editingInput is RichTextInputAPI {
|
): editingInput is RichTextInputAPI {
|
||||||
return editingInput?.name === "rich-text";
|
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 { registerPackage } from "../../lib/runtime-require";
|
||||||
import contextProperty from "../../sveltelib/context-property";
|
import contextProperty from "../../sveltelib/context-property";
|
||||||
import lifecycleHooks from "../../sveltelib/lifecycle-hooks";
|
import lifecycleHooks from "../../sveltelib/lifecycle-hooks";
|
||||||
|
import { Surrounder } from "../surround";
|
||||||
|
|
||||||
const key = Symbol("richText");
|
const key = Symbol("richText");
|
||||||
const [context, setContextProperty] = contextProperty<RichTextInputAPI>(key);
|
const [context, setContextProperty] = contextProperty<RichTextInputAPI>(key);
|
||||||
const [globalInputHandler, setupGlobalInputHandler] = useInputHandler();
|
const [globalInputHandler, setupGlobalInputHandler] = useInputHandler();
|
||||||
const [lifecycle, instances, setupLifecycleHooks] =
|
const [lifecycle, instances, setupLifecycleHooks] =
|
||||||
lifecycleHooks<RichTextInputAPI>();
|
lifecycleHooks<RichTextInputAPI>();
|
||||||
|
const surrounder = Surrounder.make();
|
||||||
|
|
||||||
registerPackage("anki/RichTextInput", {
|
registerPackage("anki/RichTextInput", {
|
||||||
context,
|
context,
|
||||||
|
surrounder,
|
||||||
lifecycle,
|
lifecycle,
|
||||||
instances,
|
instances,
|
||||||
});
|
});
|
||||||
|
|
||||||
export { context, globalInputHandler as inputHandler };
|
export {
|
||||||
|
context,
|
||||||
|
editingInputIsRichText,
|
||||||
|
globalInputHandler as inputHandler,
|
||||||
|
surrounder,
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<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 { placeCaretAfterContent } from "../../domlib/place-caret";
|
||||||
import ContentEditable from "../../editable/ContentEditable.svelte";
|
import ContentEditable from "../../editable/ContentEditable.svelte";
|
||||||
import {
|
import { directionKey, fontFamilyKey, fontSizeKey } from "../../lib/context-keys";
|
||||||
descriptionKey,
|
|
||||||
directionKey,
|
|
||||||
fontFamilyKey,
|
|
||||||
fontSizeKey,
|
|
||||||
} from "../../lib/context-keys";
|
|
||||||
import { promiseWithResolver } from "../../lib/promise";
|
import { promiseWithResolver } from "../../lib/promise";
|
||||||
import { singleCallback } from "../../lib/typing";
|
import { singleCallback } from "../../lib/typing";
|
||||||
import useDOMMirror from "../../sveltelib/dom-mirror";
|
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 { focusedInput } = noteEditorContext.get();
|
||||||
const { content, editingInputs } = editingAreaContext.get();
|
const { content, editingInputs } = editingAreaContext.get();
|
||||||
|
|
||||||
const description = getContext<Readable<string>>(descriptionKey);
|
|
||||||
const fontFamily = getContext<Readable<string>>(fontFamilyKey);
|
const fontFamily = getContext<Readable<string>>(fontFamilyKey);
|
||||||
const fontSize = getContext<Readable<number>>(fontSizeKey);
|
const fontSize = getContext<Readable<number>>(fontSizeKey);
|
||||||
const direction = getContext<Readable<"ltr" | "rtl">>(directionKey);
|
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 {
|
function setFocus(): void {
|
||||||
$focusedInput = api;
|
$focusedInput = api;
|
||||||
|
surrounder.enable(api);
|
||||||
|
|
||||||
// We do not unset focusedInput here.
|
// We do not unset focusedInput here.
|
||||||
// If we did, UI components for the input would react the store
|
// 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.
|
// field right away.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function removeFocus(): void {
|
||||||
|
surrounder.disable();
|
||||||
|
}
|
||||||
|
|
||||||
$: pushUpdate(!hidden);
|
$: pushUpdate(!hidden);
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
@ -200,18 +208,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
setupLifecycleHooks(api);
|
setupLifecycleHooks(api);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="rich-text-input" on:focusin={setFocus} {hidden}>
|
<div class="rich-text-input" {hidden} on:focusin={setFocus} on:focusout={removeFocus}>
|
||||||
{#if $content.length === 0}
|
|
||||||
<div
|
|
||||||
class="rich-text-placeholder"
|
|
||||||
style:font-family={$fontFamily}
|
|
||||||
style:font-size={$fontSize + "px"}
|
|
||||||
style:direction={$direction}
|
|
||||||
>
|
|
||||||
{$description}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<RichTextStyles
|
<RichTextStyles
|
||||||
color={$pageTheme.isDark ? "white" : "black"}
|
color={$pageTheme.isDark ? "white" : "black"}
|
||||||
fontFamily={$fontFamily}
|
fontFamily={$fontFamily}
|
||||||
|
@ -243,19 +240,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.rich-text-input {
|
.rich-text-input {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 6px;
|
padding: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rich-text-placeholder {
|
.hidden {
|
||||||
position: absolute;
|
display: none;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
// 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 { default as RichTextInput } from "./RichTextInput.svelte";
|
||||||
|
|
||||||
export type { RichTextInputAPI } from "./RichTextInput.svelte";
|
export type { RichTextInputAPI } from "./RichTextInput.svelte";
|
||||||
export {
|
export default RichTextInput;
|
||||||
context,
|
export * from "./RichTextInput.svelte";
|
||||||
editingInputIsRichText,
|
|
||||||
default as RichTextInput,
|
|
||||||
} from "./RichTextInput.svelte";
|
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
// 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 { get } from "svelte/store";
|
import type { Writable } from "svelte/store";
|
||||||
|
import { get, writable } from "svelte/store";
|
||||||
|
|
||||||
import type { Matcher } from "../domlib/find-above";
|
import type { Matcher } from "../domlib/find-above";
|
||||||
import { findClosest } 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 { getRange, getSelection } from "../lib/cross-browser";
|
||||||
import { registerPackage } from "../lib/runtime-require";
|
import { registerPackage } from "../lib/runtime-require";
|
||||||
import type { TriggerItem } from "../sveltelib/handler-list";
|
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(
|
function isSurroundedInner(
|
||||||
range: AbstractRange,
|
range: AbstractRange,
|
||||||
|
@ -57,35 +62,54 @@ function removeFormats(
|
||||||
return surroundRange;
|
return surroundRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Surrounder {
|
export interface SurroundedAPI {
|
||||||
static make(): Surrounder {
|
element: Promise<HTMLElement>;
|
||||||
|
inputHandler: InputHandlerAPI;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class Surrounder<T = unknown> {
|
||||||
|
static make<T>(): Surrounder<T> {
|
||||||
return new Surrounder();
|
return new Surrounder();
|
||||||
}
|
}
|
||||||
|
|
||||||
private api: RichTextInputAPI | null = null;
|
private api: SurroundedAPI | null = null;
|
||||||
private trigger: TriggerItem<{ event: InputEvent; text: Text }> | 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.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
|
* 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 {
|
disable(): void {
|
||||||
this.api = null;
|
this.api = null;
|
||||||
this.trigger?.off();
|
this.active.set(false);
|
||||||
this.trigger = null;
|
|
||||||
|
for (const [key, trigger] of this.triggers) {
|
||||||
|
trigger.off();
|
||||||
|
this.triggers.delete(key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _assert_base(): Promise<HTMLElement> {
|
private async _assert_base(): Promise<HTMLElement> {
|
||||||
if (!this.api) {
|
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>(
|
private _toggleTrigger<T>(
|
||||||
|
@ -93,12 +117,13 @@ export class Surrounder {
|
||||||
selection: Selection,
|
selection: Selection,
|
||||||
matcher: Matcher,
|
matcher: Matcher,
|
||||||
format: SurroundFormat<T>,
|
format: SurroundFormat<T>,
|
||||||
|
trigger: TriggerItem<{ event: InputEvent; text: Text }>,
|
||||||
exclusive: SurroundFormat<T>[] = [],
|
exclusive: SurroundFormat<T>[] = [],
|
||||||
): void {
|
): void {
|
||||||
if (get(this.trigger!.active)) {
|
if (get(trigger.active)) {
|
||||||
this.trigger!.off();
|
trigger.off();
|
||||||
} else {
|
} else {
|
||||||
this.trigger!.on(async ({ text }) => {
|
trigger.on(async ({ text }) => {
|
||||||
const range = new Range();
|
const range = new Range();
|
||||||
range.selectNode(text);
|
range.selectNode(text);
|
||||||
|
|
||||||
|
@ -114,9 +139,10 @@ export class Surrounder {
|
||||||
base: HTMLElement,
|
base: HTMLElement,
|
||||||
selection: Selection,
|
selection: Selection,
|
||||||
format: SurroundFormat<T>,
|
format: SurroundFormat<T>,
|
||||||
|
trigger: TriggerItem<{ event: InputEvent; text: Text }>,
|
||||||
exclusive: SurroundFormat<T>[] = [],
|
exclusive: SurroundFormat<T>[] = [],
|
||||||
): void {
|
): void {
|
||||||
this.trigger!.on(async ({ text }) => {
|
trigger.on(async ({ text }) => {
|
||||||
const range = new Range();
|
const range = new Range();
|
||||||
range.selectNode(text);
|
range.selectNode(text);
|
||||||
|
|
||||||
|
@ -132,68 +158,121 @@ export class Surrounder {
|
||||||
base: HTMLElement,
|
base: HTMLElement,
|
||||||
selection: Selection,
|
selection: Selection,
|
||||||
remove: SurroundFormat<T>[],
|
remove: SurroundFormat<T>[],
|
||||||
|
triggers: TriggerItem<{ event: InputEvent; text: Text }>[],
|
||||||
reformat: SurroundFormat<T>[] = [],
|
reformat: SurroundFormat<T>[] = [],
|
||||||
): void {
|
): void {
|
||||||
this.trigger!.on(async ({ text }) => {
|
triggers.map((trigger) =>
|
||||||
const range = new Range();
|
trigger.on(async ({ text }) => {
|
||||||
range.selectNode(text);
|
const range = new Range();
|
||||||
|
range.selectNode(text);
|
||||||
|
|
||||||
const clearedRange = removeFormats(range, base, remove, reformat);
|
const clearedRange = removeFormats(range, base, remove, reformat);
|
||||||
selection.removeAllRanges();
|
selection.removeAllRanges();
|
||||||
selection.addRange(clearedRange);
|
selection.addRange(clearedRange);
|
||||||
selection.collapseToEnd();
|
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.
|
* If the range is already surrounded, it will unsurround instead.
|
||||||
*/
|
*/
|
||||||
async surround<T>(
|
async surround(formatName: string, exclusiveNames: string[] = []): Promise<void> {
|
||||||
format: SurroundFormat<T>,
|
|
||||||
exclusive: SurroundFormat<T>[] = [],
|
|
||||||
): Promise<void> {
|
|
||||||
const base = await this._assert_base();
|
const base = await this._assert_base();
|
||||||
const selection = getSelection(base)!;
|
const selection = getSelection(base)!;
|
||||||
const range = getRange(selection);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const matcher = boolMatcher(format);
|
||||||
|
|
||||||
|
const exclusives = exclusiveNames
|
||||||
|
.map((name) => this.formats.get(name))
|
||||||
|
.filter(isValid);
|
||||||
|
|
||||||
if (range.collapsed) {
|
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);
|
const matches = isSurroundedInner(clearedRange, base, matcher);
|
||||||
surroundAndSelect(matches, clearedRange, base, format, selection);
|
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.
|
* If the range is already surrounded, it will overwrite the format.
|
||||||
* This might be better suited if the surrounding is parameterized (like
|
* This might be better suited if the surrounding is parameterized (like
|
||||||
* text color).
|
* text color).
|
||||||
*/
|
*/
|
||||||
async overwriteSurround<T>(
|
async overwriteSurround(
|
||||||
format: SurroundFormat<T>,
|
formatName: string,
|
||||||
exclusive: SurroundFormat<T>[] = [],
|
exclusiveNames: string[] = [],
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const base = await this._assert_base();
|
const base = await this._assert_base();
|
||||||
const selection = getSelection(base)!;
|
const selection = getSelection(base)!;
|
||||||
const range = getRange(selection);
|
const range = getRange(selection);
|
||||||
|
const format = this.formats.get(formatName);
|
||||||
|
const trigger = this.triggers.get(formatName);
|
||||||
|
|
||||||
if (!range) {
|
if (!format || !range || !trigger) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const exclusives = exclusiveNames
|
||||||
|
.map((name) => this.formats.get(name))
|
||||||
|
.filter(isValid);
|
||||||
|
|
||||||
if (range.collapsed) {
|
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);
|
const surroundedRange = surround(clearedRange, base, format);
|
||||||
selection.removeAllRanges();
|
selection.removeAllRanges();
|
||||||
selection.addRange(surroundedRange);
|
selection.addRange(surroundedRange);
|
||||||
|
@ -205,26 +284,25 @@ export class Surrounder {
|
||||||
* provided format, OR if a surround trigger is active (surround on next
|
* provided format, OR if a surround trigger is active (surround on next
|
||||||
* text insert).
|
* text insert).
|
||||||
*/
|
*/
|
||||||
async isSurrounded<T>(format: SurroundFormat<T>): Promise<boolean> {
|
async isSurrounded(formatName: string): Promise<boolean> {
|
||||||
const base = await this._assert_base();
|
const base = await this._assert_base();
|
||||||
const selection = getSelection(base)!;
|
const selection = getSelection(base)!;
|
||||||
const range = getRange(selection);
|
const range = getRange(selection);
|
||||||
|
const format = this.formats.get(formatName);
|
||||||
|
const trigger = this.triggers.get(formatName);
|
||||||
|
|
||||||
if (!range) {
|
if (!format || !range || !trigger) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const isSurrounded = isSurroundedInner(range, base, boolMatcher(format));
|
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.
|
* Clear/Reformat the provided formats in the current range.
|
||||||
*/
|
*/
|
||||||
async remove<T>(
|
async remove(formatNames: string[], reformatNames: string[] = []): Promise<void> {
|
||||||
formats: SurroundFormat<T>[],
|
|
||||||
reformats: SurroundFormat<T>[] = [],
|
|
||||||
): Promise<void> {
|
|
||||||
const base = await this._assert_base();
|
const base = await this._assert_base();
|
||||||
const selection = getSelection(base)!;
|
const selection = getSelection(base)!;
|
||||||
const range = getRange(selection);
|
const range = getRange(selection);
|
||||||
|
@ -233,8 +311,26 @@ export class Surrounder {
|
||||||
return;
|
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) {
|
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);
|
const surroundedRange = removeFormats(range, base, formats, reformats);
|
||||||
|
|
Loading…
Reference in a new issue