Refactor text attribute button components (#3012)

* Factor out common code in bold/italic/underline/sub/sup buttons

* Update exclusiveNames' states on click

* Update exclusiveNames' states on keyboard shortcut
This commit is contained in:
Lucas Scharenbroch 2024-02-18 00:09:05 -06:00 committed by GitHub
parent f09fbea3b9
commit 97dcb776de
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 133 additions and 346 deletions

View file

@ -4,20 +4,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
--> -->
<script lang="ts"> <script lang="ts">
import * as tr from "@tslib/ftl"; import * as tr from "@tslib/ftl";
import { getPlatformString } from "@tslib/shortcuts";
import { removeStyleProperties } from "@tslib/styling"; import { removeStyleProperties } from "@tslib/styling";
import { singleCallback } from "@tslib/typing";
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 type { MatchType } from "../../domlib/surround";
import { surrounder } from "../rich-text-input";
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
import { boldIcon } from "./icons"; import { boldIcon } from "./icons";
import TextAttributeButton from "./TextAttributeButton.svelte";
const surroundElement = document.createElement("b");
function matcher(element: HTMLElement | SVGElement, match: MatchType): void { function matcher(element: HTMLElement | SVGElement, match: MatchType): void {
if (element.tagName === "B" || element.tagName === "STRONG") { if (element.tagName === "B" || element.tagName === "STRONG") {
@ -36,62 +27,14 @@ 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,
};
const { removeFormats } = editorToolbarContext.get();
removeFormats.update((formats) => [...formats, namedFormat]);
async function updateStateFromActiveInput(): Promise<boolean> {
return disabled ? false : surrounder.isSurrounded(key);
}
function makeBold(): void {
surrounder.surround(key);
}
const keyCombination = "Control+B";
let disabled: boolean;
onMount(() =>
singleCallback(
surrounder.active.subscribe((value) => (disabled = !value)),
surrounder.registerFormat(key, format),
),
);
</script> </script>
<WithState {key} update={updateStateFromActiveInput} let:state={active} let:updateState> <TextAttributeButton
<IconButton tagName="b"
tooltip="{tr.editingBoldText()} ({getPlatformString(keyCombination)})" {matcher}
{active} key="bold"
{disabled} tooltip={tr.editingBoldText()}
on:click={(event) => { keyCombination="Control+B"
makeBold(); >
updateState(event); {@html boldIcon}
}} </TextAttributeButton>
>
{@html boldIcon}
</IconButton>
<Shortcut
{keyCombination}
on:action={(event) => {
makeBold();
updateState(event);
}}
/>
</WithState>

View file

@ -4,20 +4,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
--> -->
<script lang="ts"> <script lang="ts">
import * as tr from "@tslib/ftl"; import * as tr from "@tslib/ftl";
import { getPlatformString } from "@tslib/shortcuts";
import { removeStyleProperties } from "@tslib/styling"; import { removeStyleProperties } from "@tslib/styling";
import { singleCallback } from "@tslib/typing";
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 type { MatchType } from "../../domlib/surround";
import { surrounder } from "../rich-text-input";
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
import { italicIcon } from "./icons"; import { italicIcon } from "./icons";
import TextAttributeButton from "./TextAttributeButton.svelte";
const surroundElement = document.createElement("i");
function matcher(element: HTMLElement | SVGElement, match: MatchType): void { function matcher(element: HTMLElement | SVGElement, match: MatchType): void {
if (element.tagName === "I" || element.tagName === "EM") { if (element.tagName === "I" || element.tagName === "EM") {
@ -35,62 +26,14 @@ 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,
};
const { removeFormats } = editorToolbarContext.get();
removeFormats.update((formats) => [...formats, namedFormat]);
async function updateStateFromActiveInput(): Promise<boolean> {
return disabled ? false : surrounder.isSurrounded(key);
}
function makeItalic(): void {
surrounder.surround(key);
}
const keyCombination = "Control+I";
let disabled: boolean;
onMount(() =>
singleCallback(
surrounder.active.subscribe((value) => (disabled = !value)),
surrounder.registerFormat(key, format),
),
);
</script> </script>
<WithState {key} update={updateStateFromActiveInput} let:state={active} let:updateState> <TextAttributeButton
<IconButton tagName="i"
tooltip="{tr.editingItalicText()} ({getPlatformString(keyCombination)})" {matcher}
{active} key="italic"
{disabled} tooltip={tr.editingItalicText()}
on:click={(event) => { keyCombination="Control+I"
makeItalic(); >
updateState(event); {@html italicIcon}
}} </TextAttributeButton>
>
{@html italicIcon}
</IconButton>
<Shortcut
{keyCombination}
on:action={(event) => {
makeItalic();
updateState(event);
}}
/>
</WithState>

View file

@ -4,21 +4,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
--> -->
<script lang="ts"> <script lang="ts">
import * as tr from "@tslib/ftl"; import * as tr from "@tslib/ftl";
import { getPlatformString } from "@tslib/shortcuts";
import { removeStyleProperties } from "@tslib/styling"; import { removeStyleProperties } from "@tslib/styling";
import { singleCallback } from "@tslib/typing";
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 { surrounder } from "../rich-text-input";
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
import { subscriptIcon } from "./icons"; import { subscriptIcon } from "./icons";
import TextAttributeButton from "./TextAttributeButton.svelte";
const surroundElement = document.createElement("sub");
export function matcher(element: HTMLElement | SVGElement, match: MatchType): void { export function matcher(element: HTMLElement | SVGElement, match: MatchType): void {
if (element.tagName === "SUB") { if (element.tagName === "SUB") {
@ -36,64 +26,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
}); });
} }
} }
const key = "subscript";
const format = {
surroundElement,
matcher,
};
const namedFormat = {
key,
name: tr.editingSubscript(),
show: true,
active: true,
};
const { removeFormats } = editorToolbarContext.get();
removeFormats.update((formats) => [...formats, namedFormat]);
async function updateStateFromActiveInput(): Promise<boolean> {
return disabled ? false : surrounder.isSurrounded(key);
}
function makeSub(): void {
surrounder.surround(key, ["superscript"]);
}
const keyCombination = "Control+Shift+=";
let disabled: boolean;
onMount(() =>
singleCallback(
surrounder.active.subscribe((value) => (disabled = !value)),
surrounder.registerFormat(key, format),
),
);
</script> </script>
<WithState {key} update={updateStateFromActiveInput} let:state={active} let:updateState> <TextAttributeButton
<IconButton tagName="sub"
tooltip="{tr.editingSubscript()} ({getPlatformString(keyCombination)})" {matcher}
{active} key="subscript"
{disabled} tooltip={tr.editingSubscript()}
on:click={(event) => { keyCombination="Control+Shift+="
makeSub(); exclusiveNames={["superscript"]}
updateState(event); >
updateStateByKey("superscript", event); {@html subscriptIcon}
}} </TextAttributeButton>
>
{@html subscriptIcon}
</IconButton>
<Shortcut
{keyCombination}
on:action={(event) => {
makeSub();
updateState(event);
updateStateByKey("superscript", event);
}}
/>
</WithState>

View file

@ -4,21 +4,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
--> -->
<script lang="ts"> <script lang="ts">
import * as tr from "@tslib/ftl"; import * as tr from "@tslib/ftl";
import { getPlatformString } from "@tslib/shortcuts";
import { removeStyleProperties } from "@tslib/styling"; import { removeStyleProperties } from "@tslib/styling";
import { singleCallback } from "@tslib/typing";
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 { surrounder } from "../rich-text-input";
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
import { superscriptIcon } from "./icons"; import { superscriptIcon } from "./icons";
import TextAttributeButton from "./TextAttributeButton.svelte";
const surroundElement = document.createElement("sup");
export function matcher(element: HTMLElement | SVGElement, match: MatchType): void { export function matcher(element: HTMLElement | SVGElement, match: MatchType): void {
if (element.tagName === "SUP") { if (element.tagName === "SUP") {
@ -36,64 +26,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
}); });
} }
} }
const key = "superscript";
export const format = {
surroundElement,
matcher,
};
const namedFormat = {
key,
name: tr.editingSuperscript(),
show: true,
active: true,
};
const { removeFormats } = editorToolbarContext.get();
removeFormats.update((formats) => [...formats, namedFormat]);
async function updateStateFromActiveInput(): Promise<boolean> {
return disabled ? false : surrounder.isSurrounded(key);
}
function makeSuper(): void {
surrounder.surround(key, ["subscript"]);
}
const keyCombination = "Control+=";
let disabled: boolean;
onMount(() =>
singleCallback(
surrounder.active.subscribe((value) => (disabled = !value)),
surrounder.registerFormat(key, format),
),
);
</script> </script>
<WithState {key} update={updateStateFromActiveInput} let:state={active} let:updateState> <TextAttributeButton
<IconButton tagName="sup"
tooltip="{tr.editingSuperscript()} ({getPlatformString(keyCombination)})" {matcher}
{active} key="superscript"
{disabled} tooltip={tr.editingSuperscript()}
on:click={(event) => { keyCombination="Control+="
makeSuper(); exclusiveNames={["subscript"]}
updateState(event); >
updateStateByKey("subscript", event); {@html superscriptIcon}
}} </TextAttributeButton>
>
{@html superscriptIcon}
</IconButton>
<Shortcut
{keyCombination}
on:action={(event) => {
makeSuper();
updateState(event);
updateStateByKey("subscript", event);
}}
/>
</WithState>

View file

@ -0,0 +1,81 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import { getPlatformString } from "@tslib/shortcuts";
import { singleCallback } from "@tslib/typing";
import { onMount } from "svelte";
import IconButton from "../../components/IconButton.svelte";
import Shortcut from "../../components/Shortcut.svelte";
import WithState, { updateStateByKey } from "../../components/WithState.svelte";
import type { MatchType } from "../../domlib/surround";
import { surrounder } from "../rich-text-input";
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
export let tagName;
export let matcher: (element: HTMLElement | SVGElement, match: MatchType) => void;
export let key: string;
export let tooltip: string;
export let keyCombination: string;
export let exclusiveNames: string[] = [];
const surroundElement = document.createElement(tagName);
const format = {
surroundElement,
matcher,
};
const namedFormat = {
key,
name: tooltip,
show: true,
active: true,
};
const { removeFormats } = editorToolbarContext.get();
removeFormats.update((formats) => [...formats, namedFormat]);
async function updateStateFromActiveInput(): Promise<boolean> {
return disabled ? false : surrounder.isSurrounded(key);
}
function applyAttribute(): void {
surrounder.surround(key, exclusiveNames);
}
let disabled: boolean;
onMount(() =>
singleCallback(
surrounder.active.subscribe((value) => (disabled = !value)),
surrounder.registerFormat(key, format),
),
);
</script>
<WithState {key} update={updateStateFromActiveInput} let:state={active} let:updateState>
<IconButton
tooltip="{tooltip} ({getPlatformString(keyCombination)})"
{active}
{disabled}
on:click={(event) => {
applyAttribute();
updateState(event);
exclusiveNames.map((name) => updateStateByKey(name, event));
}}
>
<slot />
</IconButton>
<Shortcut
{keyCombination}
on:action={(event) => {
applyAttribute();
updateState(event);
exclusiveNames.map((name) => updateStateByKey(name, event));
}}
/>
</WithState>

View file

@ -4,86 +4,24 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
--> -->
<script lang="ts"> <script lang="ts">
import * as tr from "@tslib/ftl"; import * as tr from "@tslib/ftl";
import { getPlatformString } from "@tslib/shortcuts";
import { singleCallback } from "@tslib/typing";
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 type { MatchType } from "../../domlib/surround";
import { surrounder } from "../rich-text-input";
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
import { underlineIcon } from "./icons"; import { underlineIcon } from "./icons";
import TextAttributeButton from "./TextAttributeButton.svelte";
const surroundElement = document.createElement("u");
function matcher(element: HTMLElement | SVGElement, match: MatchType): void { function matcher(element: HTMLElement | SVGElement, match: MatchType): void {
if (element.tagName === "U") { if (element.tagName === "U") {
return match.remove(); return match.remove();
} }
} }
function clearer() {
return false;
}
const key = "underline";
const format = {
surroundElement,
matcher,
clearer,
};
const namedFormat = {
key,
name: tr.editingUnderlineText(),
show: true,
active: true,
};
const { removeFormats } = editorToolbarContext.get();
removeFormats.update((formats) => [...formats, namedFormat]);
async function updateStateFromActiveInput(): Promise<boolean> {
return disabled ? false : surrounder.isSurrounded(key);
}
function makeUnderline(): void {
surrounder.surround(key);
}
const keyCombination = "Control+U";
let disabled: boolean;
onMount(() =>
singleCallback(
surrounder.active.subscribe((value) => (disabled = !value)),
surrounder.registerFormat(key, format),
),
);
</script> </script>
<WithState {key} update={updateStateFromActiveInput} let:state={active} let:updateState> <TextAttributeButton
<IconButton tagName="u"
tooltip="{tr.editingUnderlineText()} ({getPlatformString(keyCombination)})" {matcher}
{active} key="underline"
{disabled} tooltip={tr.editingUnderlineText()}
on:click={(event) => { keyCombination="Control+U"
makeUnderline(); >
updateState(event); {@html underlineIcon}
}} </TextAttributeButton>
>
{@html underlineIcon}
</IconButton>
<Shortcut
{keyCombination}
on:action={(event) => {
makeUnderline();
updateState(event);
}}
/>
</WithState>