mirror of
https://github.com/ankitects/anki.git
synced 2025-09-22 07:52:24 -04:00
Merge pull request #1242 from hgiesel/codable
In-line HTML-Editing for Editor
This commit is contained in:
commit
039be57499
28 changed files with 645 additions and 372 deletions
|
@ -3,9 +3,8 @@ Copyright: Ankitects Pty Ltd and contributors
|
|||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
import type { Readable } from "svelte/store";
|
||||
import { getContext, onMount, createEventDispatcher } from "svelte";
|
||||
import { disabledKey, nightModeKey, dropdownKey } from "./contextKeys";
|
||||
import { nightModeKey, dropdownKey } from "./contextKeys";
|
||||
import type { DropdownProps } from "./dropdown";
|
||||
|
||||
export let id: string | undefined = undefined;
|
||||
|
@ -14,7 +13,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
export let tooltip: string | undefined = undefined;
|
||||
export let active = false;
|
||||
export let disables = true;
|
||||
export let disabled = false;
|
||||
export const disables = false; /* unused */
|
||||
export let tabbable = false;
|
||||
|
||||
export let iconSize: number = 75;
|
||||
|
@ -22,9 +22,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
let buttonRef: HTMLButtonElement;
|
||||
|
||||
const disabled = getContext<Readable<boolean>>(disabledKey);
|
||||
$: _disabled = disables && $disabled;
|
||||
|
||||
const nightMode = getContext<boolean>(nightModeKey);
|
||||
const dropdownProps = getContext<DropdownProps>(dropdownKey) ?? { dropdown: false };
|
||||
|
||||
|
@ -43,7 +40,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
style={`--icon-size: ${iconSize}%`}
|
||||
title={tooltip}
|
||||
{...dropdownProps}
|
||||
disabled={_disabled}
|
||||
{disabled}
|
||||
tabindex={tabbable ? 0 : -1}
|
||||
on:click
|
||||
on:mousedown|preventDefault
|
||||
|
|
16
ts/components/WithContext.svelte
Normal file
16
ts/components/WithContext.svelte
Normal file
|
@ -0,0 +1,16 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
import type { Readable } from "svelte/store";
|
||||
import { getContext } from "svelte";
|
||||
|
||||
type T = boolean;
|
||||
|
||||
export let key: Symbol | string;
|
||||
|
||||
const store = getContext<Readable<T>>(key);
|
||||
</script>
|
||||
|
||||
<slot context={$store} />
|
|
@ -5,14 +5,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<script lang="typescript" context="module">
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
type UpdaterMap = Map<string, (event: Event) => boolean>;
|
||||
type StateMap = Map<string, boolean>;
|
||||
type KeyType = Symbol | string;
|
||||
type UpdaterMap = Map<KeyType, (event: Event) => boolean>;
|
||||
type StateMap = Map<KeyType, boolean>;
|
||||
|
||||
const updaterMap = new Map() as UpdaterMap;
|
||||
const stateMap = new Map() as StateMap;
|
||||
const stateStore = writable(stateMap);
|
||||
|
||||
function updateAllStateWithCallback(callback: (key: string) => boolean): void {
|
||||
function updateAllStateWithCallback(callback: (key: KeyType) => boolean): void {
|
||||
stateStore.update((map: StateMap): StateMap => {
|
||||
const newMap = new Map() as StateMap;
|
||||
|
||||
|
@ -25,7 +26,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
}
|
||||
|
||||
export function updateAllState(event: Event): void {
|
||||
updateAllStateWithCallback((key: string): boolean =>
|
||||
updateAllStateWithCallback((key: KeyType): boolean =>
|
||||
updaterMap.get(key)!(event)
|
||||
);
|
||||
}
|
||||
|
@ -34,7 +35,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
updateAllStateWithCallback((): boolean => state);
|
||||
}
|
||||
|
||||
function updateStateByKey(key: string, event: Event): void {
|
||||
function updateStateByKey(key: KeyType, event: Event): void {
|
||||
stateStore.update((map: StateMap): StateMap => {
|
||||
map.set(key, updaterMap.get(key)!(event));
|
||||
return map;
|
||||
|
@ -43,7 +44,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
</script>
|
||||
|
||||
<script lang="typescript">
|
||||
export let key: string;
|
||||
export let key: KeyType;
|
||||
export let update: (event: Event) => boolean;
|
||||
|
||||
let state: boolean = false;
|
||||
|
|
|
@ -73,6 +73,8 @@ ts_library(
|
|||
"//ts/html-filter",
|
||||
"//ts:image_module_support",
|
||||
"@npm//svelte",
|
||||
"@npm//@types/codemirror",
|
||||
"@npm//codemirror",
|
||||
] + svelte_names,
|
||||
)
|
||||
|
||||
|
@ -177,7 +179,7 @@ svelte_check(
|
|||
]) + [
|
||||
"//ts/sass:button_mixins_lib",
|
||||
"//ts/sass/bootstrap",
|
||||
"//ts/components:svelte_components",
|
||||
"@npm//@types/bootstrap",
|
||||
"//ts/components",
|
||||
],
|
||||
)
|
||||
|
|
|
@ -4,20 +4,22 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
-->
|
||||
<script lang="typescript">
|
||||
import * as tr from "lib/i18n";
|
||||
import { disabledKey } from "components/contextKeys";
|
||||
|
||||
import IconButton from "components/IconButton.svelte";
|
||||
import WithShortcut from "components/WithShortcut.svelte";
|
||||
import WithContext from "components/WithContext.svelte";
|
||||
|
||||
import { bracketsIcon } from "./icons";
|
||||
import { forEditorField } from ".";
|
||||
import { wrap } from "./wrap";
|
||||
import { wrapCurrent } from "./wrap";
|
||||
|
||||
const clozePattern = /\{\{c(\d+)::/gu;
|
||||
function getCurrentHighestCloze(increment: boolean): number {
|
||||
let highest = 0;
|
||||
|
||||
forEditorField([], (field) => {
|
||||
const fieldHTML = field.editingArea.editable.fieldHTML;
|
||||
const fieldHTML = field.editingArea.fieldHTML;
|
||||
const matches: number[] = [];
|
||||
let match: RegExpMatchArray | null = null;
|
||||
|
||||
|
@ -37,16 +39,19 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
function onCloze(event: KeyboardEvent | MouseEvent): void {
|
||||
const highestCloze = getCurrentHighestCloze(!event.getModifierState("Alt"));
|
||||
wrap(`{{c${highestCloze}::`, "}}");
|
||||
wrapCurrent(`{{c${highestCloze}::`, "}}");
|
||||
}
|
||||
</script>
|
||||
|
||||
<WithShortcut shortcut={"Control+Alt?+Shift+C"} let:createShortcut let:shortcutLabel>
|
||||
<IconButton
|
||||
tooltip={`${tr.editingClozeDeletion()} (${shortcutLabel})`}
|
||||
on:click={onCloze}
|
||||
on:mount={createShortcut}
|
||||
>
|
||||
{@html bracketsIcon}
|
||||
</IconButton>
|
||||
<WithContext key={disabledKey} let:context={disabled}>
|
||||
<IconButton
|
||||
tooltip={`${tr.editingClozeDeletion()} (${shortcutLabel})`}
|
||||
{disabled}
|
||||
on:click={onCloze}
|
||||
on:mount={createShortcut}
|
||||
>
|
||||
{@html bracketsIcon}
|
||||
</IconButton>
|
||||
</WithContext>
|
||||
</WithShortcut>
|
||||
|
|
|
@ -11,6 +11,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
import ColorPicker from "components/ColorPicker.svelte";
|
||||
import WithShortcut from "components/WithShortcut.svelte";
|
||||
import WithColorHelper from "./WithColorHelper.svelte";
|
||||
import OnlyEditable from "./OnlyEditable.svelte";
|
||||
|
||||
import { textColorIcon, highlightColorIcon, arrowIcon } from "./icons";
|
||||
import { appendInParentheses } from "./helpers";
|
||||
|
@ -28,51 +29,61 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
<ButtonGroup {api}>
|
||||
<WithColorHelper let:colorHelperIcon let:color let:setColor>
|
||||
<ButtonGroupItem>
|
||||
<WithShortcut shortcut={"F7"} let:createShortcut let:shortcutLabel>
|
||||
<IconButton
|
||||
tooltip={appendInParentheses(
|
||||
tr.editingSetForegroundColor(),
|
||||
shortcutLabel
|
||||
)}
|
||||
on:click={wrapWithForecolor(color)}
|
||||
on:mount={createShortcut}
|
||||
>
|
||||
{@html textColorIcon}
|
||||
{@html colorHelperIcon}
|
||||
</IconButton>
|
||||
</WithShortcut>
|
||||
</ButtonGroupItem>
|
||||
<OnlyEditable let:disabled>
|
||||
<ButtonGroupItem>
|
||||
<WithShortcut shortcut={"F7"} let:createShortcut let:shortcutLabel>
|
||||
<IconButton
|
||||
tooltip={appendInParentheses(
|
||||
tr.editingSetForegroundColor(),
|
||||
shortcutLabel
|
||||
)}
|
||||
{disabled}
|
||||
on:click={wrapWithForecolor(color)}
|
||||
on:mount={createShortcut}
|
||||
>
|
||||
{@html textColorIcon}
|
||||
{@html colorHelperIcon}
|
||||
</IconButton>
|
||||
</WithShortcut>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<WithShortcut shortcut={"F8"} let:createShortcut let:shortcutLabel>
|
||||
<IconButton
|
||||
tooltip={appendInParentheses(
|
||||
tr.editingChangeColor(),
|
||||
shortcutLabel
|
||||
)}
|
||||
widthMultiplier={0.5}
|
||||
>
|
||||
{@html arrowIcon}
|
||||
<ColorPicker on:change={setColor} on:mount={createShortcut} />
|
||||
</IconButton>
|
||||
</WithShortcut>
|
||||
</ButtonGroupItem>
|
||||
<ButtonGroupItem>
|
||||
<WithShortcut shortcut={"F8"} let:createShortcut let:shortcutLabel>
|
||||
<IconButton
|
||||
tooltip={appendInParentheses(
|
||||
tr.editingChangeColor(),
|
||||
shortcutLabel
|
||||
)}
|
||||
{disabled}
|
||||
widthMultiplier={0.5}
|
||||
>
|
||||
{@html arrowIcon}
|
||||
<ColorPicker on:change={setColor} on:mount={createShortcut} />
|
||||
</IconButton>
|
||||
</WithShortcut>
|
||||
</ButtonGroupItem>
|
||||
</OnlyEditable>
|
||||
</WithColorHelper>
|
||||
|
||||
<WithColorHelper let:colorHelperIcon let:color let:setColor>
|
||||
<ButtonGroupItem>
|
||||
<IconButton on:click={wrapWithBackcolor(color)}>
|
||||
{@html highlightColorIcon}
|
||||
{@html colorHelperIcon}
|
||||
</IconButton>
|
||||
</ButtonGroupItem>
|
||||
<OnlyEditable let:disabled>
|
||||
<ButtonGroupItem>
|
||||
<IconButton on:click={wrapWithBackcolor(color)} {disabled}>
|
||||
{@html highlightColorIcon}
|
||||
{@html colorHelperIcon}
|
||||
</IconButton>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<IconButton tooltip={tr.editingChangeColor()} widthMultiplier={0.5}>
|
||||
{@html arrowIcon}
|
||||
<ColorPicker on:change={setColor} />
|
||||
</IconButton>
|
||||
</ButtonGroupItem>
|
||||
<ButtonGroupItem>
|
||||
<IconButton
|
||||
tooltip={tr.editingChangeColor()}
|
||||
widthMultiplier={0.5}
|
||||
{disabled}
|
||||
>
|
||||
{@html arrowIcon}
|
||||
<ColorPicker on:change={setColor} />
|
||||
</IconButton>
|
||||
</ButtonGroupItem>
|
||||
</OnlyEditable>
|
||||
</WithColorHelper>
|
||||
</ButtonGroup>
|
||||
|
|
79
ts/editor/CommandIconButton.svelte
Normal file
79
ts/editor/CommandIconButton.svelte
Normal file
|
@ -0,0 +1,79 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
import IconButton from "components/IconButton.svelte";
|
||||
import WithShortcut from "components/WithShortcut.svelte";
|
||||
import WithState from "components/WithState.svelte";
|
||||
import OnlyEditable from "./OnlyEditable.svelte";
|
||||
|
||||
import { appendInParentheses } from "./helpers";
|
||||
|
||||
export let key: string;
|
||||
export let tooltip: string;
|
||||
export let shortcut: string = "";
|
||||
|
||||
export let withoutShortcut = false;
|
||||
export let withoutState = false;
|
||||
</script>
|
||||
|
||||
<OnlyEditable let:disabled>
|
||||
{#if withoutShortcut && withoutState}
|
||||
<IconButton {tooltip} {disabled} on:click={() => document.execCommand(key)}>
|
||||
<slot />
|
||||
</IconButton>
|
||||
{:else if withoutShortcut}
|
||||
<WithState
|
||||
{key}
|
||||
update={() => document.queryCommandState(key)}
|
||||
let:state={active}
|
||||
let:updateState
|
||||
>
|
||||
<IconButton
|
||||
{tooltip}
|
||||
{active}
|
||||
{disabled}
|
||||
on:click={(event) => {
|
||||
document.execCommand(key);
|
||||
updateState(event);
|
||||
}}
|
||||
>
|
||||
<slot />
|
||||
</IconButton>
|
||||
</WithState>
|
||||
{:else if withoutState}
|
||||
<WithShortcut {shortcut} let:createShortcut let:shortcutLabel>
|
||||
<IconButton
|
||||
tooltip={appendInParentheses(tooltip, shortcutLabel)}
|
||||
{disabled}
|
||||
on:click={() => document.execCommand(key)}
|
||||
on:mount={createShortcut}
|
||||
>
|
||||
<slot />
|
||||
</IconButton>
|
||||
</WithShortcut>
|
||||
{:else}
|
||||
<WithShortcut {shortcut} let:createShortcut let:shortcutLabel>
|
||||
<WithState
|
||||
{key}
|
||||
update={() => document.queryCommandState(key)}
|
||||
let:state={active}
|
||||
let:updateState
|
||||
>
|
||||
<IconButton
|
||||
tooltip={appendInParentheses(tooltip, shortcutLabel)}
|
||||
{active}
|
||||
{disabled}
|
||||
on:click={(event) => {
|
||||
document.execCommand(key);
|
||||
updateState(event);
|
||||
}}
|
||||
on:mount={createShortcut}
|
||||
>
|
||||
<slot />
|
||||
</IconButton>
|
||||
</WithState>
|
||||
</WithShortcut>
|
||||
{/if}
|
||||
</OnlyEditable>
|
|
@ -11,8 +11,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
import IconButton from "components/IconButton.svelte";
|
||||
import ButtonDropdown from "components/ButtonDropdown.svelte";
|
||||
import ButtonToolbarItem from "components/ButtonToolbarItem.svelte";
|
||||
import WithState from "components/WithState.svelte";
|
||||
import WithDropdownMenu from "components/WithDropdownMenu.svelte";
|
||||
import OnlyEditable from "./OnlyEditable.svelte";
|
||||
import CommandIconButton from "./CommandIconButton.svelte";
|
||||
|
||||
import { getListItem } from "./helpers";
|
||||
import {
|
||||
|
@ -46,134 +47,66 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
<ButtonGroup {api}>
|
||||
<ButtonGroupItem>
|
||||
<WithState
|
||||
<CommandIconButton
|
||||
key="insertUnorderedList"
|
||||
update={() => document.queryCommandState("insertUnorderedList")}
|
||||
let:state={active}
|
||||
let:updateState
|
||||
tooltip={tr.editingUnorderedList()}
|
||||
withoutShortcut>{@html ulIcon}</CommandIconButton
|
||||
>
|
||||
<IconButton
|
||||
tooltip={tr.editingUnorderedList()}
|
||||
{active}
|
||||
on:click={(event) => {
|
||||
document.execCommand("insertUnorderedList");
|
||||
updateState(event);
|
||||
}}
|
||||
>
|
||||
{@html ulIcon}
|
||||
</IconButton>
|
||||
</WithState>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<WithState
|
||||
<CommandIconButton
|
||||
key="insertOrderedList"
|
||||
update={() => document.queryCommandState("insertOrderedList")}
|
||||
let:state={active}
|
||||
let:updateState
|
||||
tooltip={tr.editingOrderedList()}
|
||||
withoutShortcut>{@html olIcon}</CommandIconButton
|
||||
>
|
||||
<IconButton
|
||||
tooltip={tr.editingOrderedList()}
|
||||
{active}
|
||||
on:click={(event) => {
|
||||
document.execCommand("insertOrderedList");
|
||||
updateState(event);
|
||||
}}
|
||||
>
|
||||
{@html olIcon}
|
||||
</IconButton>
|
||||
</WithState>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<WithDropdownMenu let:createDropdown let:menuId>
|
||||
<IconButton on:mount={createDropdown}>
|
||||
{@html listOptionsIcon}
|
||||
</IconButton>
|
||||
<OnlyEditable let:disabled>
|
||||
<IconButton {disabled} on:mount={createDropdown}>
|
||||
{@html listOptionsIcon}
|
||||
</IconButton>
|
||||
</OnlyEditable>
|
||||
|
||||
<ButtonDropdown id={menuId}>
|
||||
<ButtonToolbarItem id="justify">
|
||||
<ButtonGroup>
|
||||
<ButtonGroupItem>
|
||||
<WithState
|
||||
<CommandIconButton
|
||||
key="justifyLeft"
|
||||
update={() => document.queryCommandState("justifyLeft")}
|
||||
let:state={active}
|
||||
let:updateState
|
||||
tooltip={tr.editingAlignLeft()}
|
||||
withoutShortcut
|
||||
>{@html justifyLeftIcon}</CommandIconButton
|
||||
>
|
||||
<IconButton
|
||||
tooltip={tr.editingAlignLeft()}
|
||||
{active}
|
||||
on:click={(event) => {
|
||||
document.execCommand("justifyLeft");
|
||||
updateState(event);
|
||||
}}
|
||||
>
|
||||
{@html justifyLeftIcon}
|
||||
</IconButton>
|
||||
</WithState>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<WithState
|
||||
<CommandIconButton
|
||||
key="justifyCenter"
|
||||
update={() =>
|
||||
document.queryCommandState("justifyCenter")}
|
||||
let:state={active}
|
||||
let:updateState
|
||||
tooltip={tr.editingCenter()}
|
||||
withoutShortcut
|
||||
>{@html justifyCenterIcon}</CommandIconButton
|
||||
>
|
||||
<IconButton
|
||||
tooltip={tr.editingCenter()}
|
||||
{active}
|
||||
on:click={(event) => {
|
||||
document.execCommand("justifyCenter");
|
||||
updateState(event);
|
||||
}}
|
||||
>
|
||||
{@html justifyCenterIcon}
|
||||
</IconButton>
|
||||
</WithState>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<WithState
|
||||
<CommandIconButton
|
||||
key="justifyRight"
|
||||
update={() =>
|
||||
document.queryCommandState("justifyRight")}
|
||||
let:state={active}
|
||||
let:updateState
|
||||
tooltip={tr.editingAlignRight()}
|
||||
withoutShortcut
|
||||
>{@html justifyRightIcon}</CommandIconButton
|
||||
>
|
||||
<IconButton
|
||||
tooltip={tr.editingAlignRight()}
|
||||
{active}
|
||||
on:click={(event) => {
|
||||
document.execCommand("justifyRight");
|
||||
updateState(event);
|
||||
}}
|
||||
>
|
||||
{@html justifyRightIcon}
|
||||
</IconButton>
|
||||
</WithState>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<WithState
|
||||
<CommandIconButton
|
||||
key="justifyFull"
|
||||
update={() => document.queryCommandState("justifyFull")}
|
||||
let:state={active}
|
||||
let:updateState
|
||||
tooltip={tr.editingJustify()}
|
||||
withoutShortcut
|
||||
>{@html justifyFullIcon}</CommandIconButton
|
||||
>
|
||||
<IconButton
|
||||
tooltip={tr.editingJustify()}
|
||||
{active}
|
||||
on:click={(event) => {
|
||||
document.execCommand("justifyFull");
|
||||
updateState(event);
|
||||
}}
|
||||
>
|
||||
{@html justifyFullIcon}
|
||||
</IconButton>
|
||||
</WithState>
|
||||
</ButtonGroupItem>
|
||||
</ButtonGroup>
|
||||
</ButtonToolbarItem>
|
||||
|
@ -181,21 +114,27 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<ButtonToolbarItem id="indentation">
|
||||
<ButtonGroup>
|
||||
<ButtonGroupItem>
|
||||
<IconButton
|
||||
on:click={outdentListItem}
|
||||
tooltip={tr.editingOutdent()}
|
||||
>
|
||||
{@html outdentIcon}
|
||||
</IconButton>
|
||||
<OnlyEditable let:disabled>
|
||||
<IconButton
|
||||
on:click={outdentListItem}
|
||||
tooltip={tr.editingOutdent()}
|
||||
{disabled}
|
||||
>
|
||||
{@html outdentIcon}
|
||||
</IconButton>
|
||||
</OnlyEditable>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<IconButton
|
||||
on:click={indentListItem}
|
||||
tooltip={tr.editingIndent()}
|
||||
>
|
||||
{@html indentIcon}
|
||||
</IconButton>
|
||||
<OnlyEditable let:disabled>
|
||||
<IconButton
|
||||
on:click={indentListItem}
|
||||
tooltip={tr.editingIndent()}
|
||||
{disabled}
|
||||
>
|
||||
{@html indentIcon}
|
||||
</IconButton>
|
||||
</OnlyEditable>
|
||||
</ButtonGroupItem>
|
||||
</ButtonGroup>
|
||||
</ButtonToolbarItem>
|
||||
|
|
|
@ -7,9 +7,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
import ButtonGroup from "components/ButtonGroup.svelte";
|
||||
import ButtonGroupItem from "components/ButtonGroupItem.svelte";
|
||||
import IconButton from "components/IconButton.svelte";
|
||||
import WithState from "components/WithState.svelte";
|
||||
import WithShortcut from "components/WithShortcut.svelte";
|
||||
import CommandIconButton from "./CommandIconButton.svelte";
|
||||
|
||||
import {
|
||||
boldIcon,
|
||||
|
@ -19,147 +17,57 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
subscriptIcon,
|
||||
eraserIcon,
|
||||
} from "./icons";
|
||||
import { appendInParentheses } from "./helpers";
|
||||
|
||||
export let api = {};
|
||||
</script>
|
||||
|
||||
<ButtonGroup {api}>
|
||||
<ButtonGroupItem>
|
||||
<WithShortcut shortcut={"Control+B"} let:createShortcut let:shortcutLabel>
|
||||
<WithState
|
||||
key="bold"
|
||||
update={() => document.queryCommandState("bold")}
|
||||
let:state={active}
|
||||
let:updateState
|
||||
>
|
||||
<IconButton
|
||||
tooltip={appendInParentheses(tr.editingBoldText(), shortcutLabel)}
|
||||
{active}
|
||||
on:click={(event) => {
|
||||
document.execCommand("bold");
|
||||
updateState(event);
|
||||
}}
|
||||
on:mount={createShortcut}
|
||||
>
|
||||
{@html boldIcon}
|
||||
</IconButton>
|
||||
</WithState>
|
||||
</WithShortcut>
|
||||
<CommandIconButton
|
||||
key="bold"
|
||||
shortcut={"Control+B"}
|
||||
tooltip={tr.editingBoldText()}>{@html boldIcon}</CommandIconButton
|
||||
>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<WithShortcut shortcut={"Control+I"} let:createShortcut let:shortcutLabel>
|
||||
<WithState
|
||||
key="italic"
|
||||
update={() => document.queryCommandState("italic")}
|
||||
let:state={active}
|
||||
let:updateState
|
||||
>
|
||||
<IconButton
|
||||
tooltip={appendInParentheses(tr.editingItalicText(), shortcutLabel)}
|
||||
{active}
|
||||
on:click={(event) => {
|
||||
document.execCommand("italic");
|
||||
updateState(event);
|
||||
}}
|
||||
on:mount={createShortcut}
|
||||
>
|
||||
{@html italicIcon}
|
||||
</IconButton>
|
||||
</WithState>
|
||||
</WithShortcut>
|
||||
<CommandIconButton
|
||||
key="italic"
|
||||
shortcut={"Control+I"}
|
||||
tooltip={tr.editingItalicText()}>{@html italicIcon}</CommandIconButton
|
||||
>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<WithShortcut shortcut={"Control+U"} let:createShortcut let:shortcutLabel>
|
||||
<WithState
|
||||
key="underline"
|
||||
update={() => document.queryCommandState("underline")}
|
||||
let:state={active}
|
||||
let:updateState
|
||||
>
|
||||
<IconButton
|
||||
tooltip={appendInParentheses(
|
||||
tr.editingUnderlineText(),
|
||||
shortcutLabel
|
||||
)}
|
||||
{active}
|
||||
on:click={(event) => {
|
||||
document.execCommand("underline");
|
||||
updateState(event);
|
||||
}}
|
||||
on:mount={createShortcut}
|
||||
>
|
||||
{@html underlineIcon}
|
||||
</IconButton>
|
||||
</WithState>
|
||||
</WithShortcut>
|
||||
<CommandIconButton
|
||||
key="underline"
|
||||
shortcut={"Control+U"}
|
||||
tooltip={tr.editingUnderlineText()}>{@html underlineIcon}</CommandIconButton
|
||||
>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<WithShortcut shortcut={"Control+="} let:createShortcut let:shortcutLabel>
|
||||
<WithState
|
||||
key="superscript"
|
||||
update={() => document.queryCommandState("superscript")}
|
||||
let:state={active}
|
||||
let:updateState
|
||||
>
|
||||
<IconButton
|
||||
tooltip={appendInParentheses(
|
||||
tr.editingSuperscript(),
|
||||
shortcutLabel
|
||||
)}
|
||||
{active}
|
||||
on:click={(event) => {
|
||||
document.execCommand("superscript");
|
||||
updateState(event);
|
||||
}}
|
||||
on:mount={createShortcut}
|
||||
>
|
||||
{@html superscriptIcon}
|
||||
</IconButton>
|
||||
</WithState>
|
||||
</WithShortcut>
|
||||
<CommandIconButton
|
||||
key="superscript"
|
||||
shortcut={"Control+="}
|
||||
tooltip={tr.editingSuperscript()}>{@html superscriptIcon}</CommandIconButton
|
||||
>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<WithShortcut shortcut={"Control+Shift+="} let:createShortcut let:shortcutLabel>
|
||||
<WithState
|
||||
key="subscript"
|
||||
update={() => document.queryCommandState("subscript")}
|
||||
let:state={active}
|
||||
let:updateState
|
||||
>
|
||||
<IconButton
|
||||
tooltip={appendInParentheses(tr.editingSubscript(), shortcutLabel)}
|
||||
{active}
|
||||
on:click={(event) => {
|
||||
document.execCommand("subscript");
|
||||
updateState(event);
|
||||
}}
|
||||
on:mount={createShortcut}
|
||||
>
|
||||
{@html subscriptIcon}
|
||||
</IconButton>
|
||||
</WithState>
|
||||
</WithShortcut>
|
||||
<CommandIconButton
|
||||
key="subscript"
|
||||
shortcut={"Control+Shift+="}
|
||||
tooltip={tr.editingSubscript()}>{@html subscriptIcon}</CommandIconButton
|
||||
>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<WithShortcut shortcut={"Control+R"} let:createShortcut let:shortcutLabel>
|
||||
<IconButton
|
||||
tooltip={appendInParentheses(
|
||||
tr.editingRemoveFormatting(),
|
||||
shortcutLabel
|
||||
)}
|
||||
on:click={() => {
|
||||
document.execCommand("removeFormat");
|
||||
}}
|
||||
on:mount={createShortcut}
|
||||
>
|
||||
{@html eraserIcon}
|
||||
</IconButton>
|
||||
</WithShortcut>
|
||||
<CommandIconButton
|
||||
key="removeFormat"
|
||||
shortcut={"Control+R"}
|
||||
tooltip={tr.editingRemoveFormatting()}
|
||||
withoutState>{@html eraserIcon}</CommandIconButton
|
||||
>
|
||||
</ButtonGroupItem>
|
||||
</ButtonGroup>
|
||||
|
|
15
ts/editor/OnlyEditable.svelte
Normal file
15
ts/editor/OnlyEditable.svelte
Normal file
|
@ -0,0 +1,15 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
import WithContext from "components/WithContext.svelte";
|
||||
import { disabledKey } from "components/contextKeys";
|
||||
import { inCodableKey } from "./contextKeys";
|
||||
</script>
|
||||
|
||||
<WithContext key={disabledKey} let:context={disabled}>
|
||||
<WithContext key={inCodableKey} let:context={inCodable}>
|
||||
<slot disabled={disabled || inCodable} />
|
||||
</WithContext>
|
||||
</WithContext>
|
|
@ -5,6 +5,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<script lang="typescript">
|
||||
import * as tr from "lib/i18n";
|
||||
import { bridgeCommand } from "lib/bridgecommand";
|
||||
import { disabledKey } from "components/contextKeys";
|
||||
import { inCodableKey } from "./contextKeys";
|
||||
|
||||
import ButtonGroup from "components/ButtonGroup.svelte";
|
||||
import ButtonGroupItem from "components/ButtonGroupItem.svelte";
|
||||
|
@ -13,10 +15,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
import DropdownItem from "components/DropdownItem.svelte";
|
||||
import WithDropdownMenu from "components/WithDropdownMenu.svelte";
|
||||
import WithShortcut from "components/WithShortcut.svelte";
|
||||
import WithContext from "components/WithContext.svelte";
|
||||
import OnlyEditable from "./OnlyEditable.svelte";
|
||||
import ClozeButton from "./ClozeButton.svelte";
|
||||
|
||||
import { wrap } from "./wrap";
|
||||
import { getCurrentField } from ".";
|
||||
import { appendInParentheses } from "./helpers";
|
||||
import { wrapCurrent } from "./wrap";
|
||||
import { paperclipIcon, micIcon, functionIcon, xmlIcon } from "./icons";
|
||||
|
||||
export let api = {};
|
||||
|
@ -29,38 +34,50 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
bridgeCommand("record");
|
||||
}
|
||||
|
||||
function onHtmlEdit(): void {
|
||||
bridgeCommand("htmlEdit");
|
||||
function onHtmlEdit() {
|
||||
const currentField = getCurrentField();
|
||||
if (currentField) {
|
||||
currentField.toggleHtmlEdit();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ButtonGroup {api}>
|
||||
<ButtonGroupItem>
|
||||
<WithShortcut shortcut={"F3"} let:createShortcut let:shortcutLabel>
|
||||
<IconButton
|
||||
tooltip={appendInParentheses(
|
||||
tr.editingAttachPicturesaudiovideo(),
|
||||
shortcutLabel
|
||||
)}
|
||||
iconSize={70}
|
||||
on:click={onAttachment}
|
||||
on:mount={createShortcut}
|
||||
>
|
||||
{@html paperclipIcon}
|
||||
</IconButton>
|
||||
<OnlyEditable let:disabled>
|
||||
<IconButton
|
||||
tooltip={appendInParentheses(
|
||||
tr.editingAttachPicturesaudiovideo(),
|
||||
shortcutLabel
|
||||
)}
|
||||
iconSize={70}
|
||||
{disabled}
|
||||
on:click={onAttachment}
|
||||
on:mount={createShortcut}
|
||||
>
|
||||
{@html paperclipIcon}
|
||||
</IconButton>
|
||||
</OnlyEditable>
|
||||
</WithShortcut>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<WithShortcut shortcut={"F5"} let:createShortcut let:shortcutLabel>
|
||||
<IconButton
|
||||
tooltip={appendInParentheses(tr.editingRecordAudio(), shortcutLabel)}
|
||||
iconSize={70}
|
||||
on:click={onRecord}
|
||||
on:mount={createShortcut}
|
||||
>
|
||||
{@html micIcon}
|
||||
</IconButton>
|
||||
<OnlyEditable let:disabled>
|
||||
<IconButton
|
||||
tooltip={appendInParentheses(
|
||||
tr.editingRecordAudio(),
|
||||
shortcutLabel
|
||||
)}
|
||||
iconSize={70}
|
||||
{disabled}
|
||||
on:click={onRecord}
|
||||
on:mount={createShortcut}
|
||||
>
|
||||
{@html micIcon}
|
||||
</IconButton>
|
||||
</OnlyEditable>
|
||||
</WithShortcut>
|
||||
</ButtonGroupItem>
|
||||
|
||||
|
@ -70,9 +87,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
<ButtonGroupItem>
|
||||
<WithDropdownMenu let:createDropdown let:menuId>
|
||||
<IconButton on:mount={createDropdown}>
|
||||
{@html functionIcon}
|
||||
</IconButton>
|
||||
<WithContext key={disabledKey} let:context={disabled}>
|
||||
<IconButton {disabled} on:mount={createDropdown}>
|
||||
{@html functionIcon}
|
||||
</IconButton>
|
||||
</WithContext>
|
||||
|
||||
<DropdownMenu id={menuId}>
|
||||
<WithShortcut
|
||||
|
@ -81,7 +100,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
let:shortcutLabel
|
||||
>
|
||||
<DropdownItem
|
||||
on:click={() => wrap("\\(", "\\)")}
|
||||
on:click={() => wrapCurrent("\\(", "\\)")}
|
||||
on:mount={createShortcut}
|
||||
>
|
||||
{tr.editingMathjaxInline()}
|
||||
|
@ -95,7 +114,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
let:shortcutLabel
|
||||
>
|
||||
<DropdownItem
|
||||
on:click={() => wrap("\\[", "\\]")}
|
||||
on:click={() => wrapCurrent("\\[", "\\]")}
|
||||
on:mount={createShortcut}
|
||||
>
|
||||
{tr.editingMathjaxBlock()}
|
||||
|
@ -109,7 +128,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
let:shortcutLabel
|
||||
>
|
||||
<DropdownItem
|
||||
on:click={() => wrap("\\(\\ce{", "}\\)")}
|
||||
on:click={() => wrapCurrent("\\(\\ce{", "}\\)")}
|
||||
on:mount={createShortcut}
|
||||
>
|
||||
{tr.editingMathjaxChemistry()}
|
||||
|
@ -123,7 +142,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
let:shortcutLabel
|
||||
>
|
||||
<DropdownItem
|
||||
on:click={() => wrap("[latex]", "[/latex]")}
|
||||
on:click={() => wrapCurrent("[latex]", "[/latex]")}
|
||||
on:mount={createShortcut}
|
||||
>
|
||||
{tr.editingLatex()}
|
||||
|
@ -137,7 +156,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
let:shortcutLabel
|
||||
>
|
||||
<DropdownItem
|
||||
on:click={() => wrap("[$]", "[/$]")}
|
||||
on:click={() => wrapCurrent("[$]", "[/$]")}
|
||||
on:mount={createShortcut}
|
||||
>
|
||||
{tr.editingLatexEquation()}
|
||||
|
@ -151,7 +170,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
let:shortcutLabel
|
||||
>
|
||||
<DropdownItem
|
||||
on:click={() => wrap("[$$]", "[/$$]")}
|
||||
on:click={() => wrapCurrent("[$$]", "[/$$]")}
|
||||
on:mount={createShortcut}
|
||||
>
|
||||
{tr.editingLatexMathEnv()}
|
||||
|
@ -163,15 +182,28 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<WithShortcut shortcut={"Control+Shift+X"} let:createShortcut let:shortcutLabel>
|
||||
<IconButton
|
||||
tooltip={appendInParentheses(tr.editingHtmlEditor(), shortcutLabel)}
|
||||
iconSize={70}
|
||||
on:click={onHtmlEdit}
|
||||
on:mount={createShortcut}
|
||||
>
|
||||
{@html xmlIcon}
|
||||
</IconButton>
|
||||
</WithShortcut>
|
||||
<WithContext key={disabledKey} let:context={disabled}>
|
||||
<WithContext key={inCodableKey} let:context={inCodable}>
|
||||
<WithShortcut
|
||||
shortcut={"Control+Shift+X"}
|
||||
let:createShortcut
|
||||
let:shortcutLabel
|
||||
>
|
||||
<IconButton
|
||||
tooltip={appendInParentheses(
|
||||
tr.editingHtmlEditor(),
|
||||
shortcutLabel
|
||||
)}
|
||||
iconSize={70}
|
||||
active={!disabled && inCodable}
|
||||
{disabled}
|
||||
on:click={onHtmlEdit}
|
||||
on:mount={createShortcut}
|
||||
>
|
||||
{@html xmlIcon}
|
||||
</IconButton>
|
||||
</WithShortcut>
|
||||
</WithContext>
|
||||
</WithContext>
|
||||
</ButtonGroupItem>
|
||||
</ButtonGroup>
|
||||
|
|
|
@ -41,6 +41,6 @@ export function saveNow(keepFocus: boolean): void {
|
|||
saveField(currentField, "key");
|
||||
} else {
|
||||
// triggers onBlur, which saves
|
||||
currentField.blurEditable();
|
||||
currentField.blur();
|
||||
}
|
||||
}
|
||||
|
|
89
ts/editor/codable.ts
Normal file
89
ts/editor/codable.ts
Normal file
|
@ -0,0 +1,89 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import * as CodeMirror from "codemirror/lib/codemirror";
|
||||
import "codemirror/mode/htmlmixed/htmlmixed";
|
||||
import "codemirror/addon/fold/foldcode";
|
||||
import "codemirror/addon/fold/foldgutter";
|
||||
import "codemirror/addon/fold/xml-fold";
|
||||
import "codemirror/addon/edit/matchtags.js";
|
||||
import "codemirror/addon/edit/closetag.js";
|
||||
|
||||
import { setCodableButtons } from "./toolbar";
|
||||
|
||||
const codeMirrorOptions = {
|
||||
mode: "htmlmixed",
|
||||
theme: "monokai",
|
||||
lineNumbers: true,
|
||||
lineWrapping: true,
|
||||
foldGutter: true,
|
||||
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
|
||||
matchTags: { bothTags: true },
|
||||
autoCloseTags: true,
|
||||
extraKeys: { Tab: false, "Shift-Tab": false },
|
||||
viewportMargin: Infinity,
|
||||
};
|
||||
|
||||
const parser = new DOMParser();
|
||||
|
||||
function parseHTML(html: string): string {
|
||||
const doc = parser.parseFromString(html, "text/html");
|
||||
return doc.documentElement.innerHTML;
|
||||
}
|
||||
|
||||
export class Codable extends HTMLTextAreaElement {
|
||||
codeMirror: CodeMirror | undefined;
|
||||
active: boolean;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.active = false;
|
||||
}
|
||||
|
||||
set fieldHTML(content: string) {
|
||||
this.value = content;
|
||||
}
|
||||
|
||||
get fieldHTML(): string {
|
||||
return parseHTML(this.codeMirror.getValue());
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
this.setAttribute("hidden", "");
|
||||
}
|
||||
|
||||
setup(html: string): void {
|
||||
this.active = true;
|
||||
this.fieldHTML = html;
|
||||
this.codeMirror = CodeMirror.fromTextArea(this, codeMirrorOptions);
|
||||
}
|
||||
|
||||
teardown(): string {
|
||||
this.active = false;
|
||||
this.codeMirror.toTextArea();
|
||||
this.codeMirror = undefined;
|
||||
return parseHTML(this.value);
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
this.codeMirror.focus();
|
||||
setCodableButtons();
|
||||
}
|
||||
|
||||
caretToEnd(): void {
|
||||
this.codeMirror.setCursor(this.codeMirror.lineCount(), 0);
|
||||
}
|
||||
|
||||
surroundSelection(before: string, after: string): void {
|
||||
const selection = this.codeMirror.getSelection();
|
||||
this.codeMirror.replaceSelection(before + selection + after);
|
||||
}
|
||||
|
||||
onEnter(): void {
|
||||
/* default */
|
||||
}
|
||||
|
||||
onPaste(): void {
|
||||
/* default */
|
||||
}
|
||||
}
|
4
ts/editor/contextKeys.ts
Normal file
4
ts/editor/contextKeys.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
export const inCodableKey = Symbol("inCodable");
|
|
@ -37,3 +37,16 @@ img.drawing {
|
|||
filter: unquote("invert() hue-rotate(180deg)");
|
||||
}
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@import "ts/sass/codemirror/lib/codemirror";
|
||||
@import "ts/sass/codemirror/theme/monokai";
|
||||
@import "ts/sass/codemirror/addon/fold/foldgutter";
|
||||
|
||||
.CodeMirror {
|
||||
height: auto;
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import { nodeIsInline } from "./helpers";
|
||||
import { bridgeCommand } from "./lib";
|
||||
import { nodeIsInline, caretToEnd, getBlockElement } from "./helpers";
|
||||
import { setEditableButtons } from "./toolbar";
|
||||
import { wrap } from "./wrap";
|
||||
|
||||
function containsInlineContent(field: Element): boolean {
|
||||
if (field.childNodes.length === 0) {
|
||||
|
@ -36,4 +39,32 @@ export class Editable extends HTMLElement {
|
|||
connectedCallback(): void {
|
||||
this.setAttribute("contenteditable", "");
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
super.focus();
|
||||
setEditableButtons();
|
||||
}
|
||||
|
||||
caretToEnd(): void {
|
||||
caretToEnd(this);
|
||||
}
|
||||
|
||||
surroundSelection(before: string, after: string): void {
|
||||
wrap(before, after);
|
||||
}
|
||||
|
||||
onEnter(event: KeyboardEvent): void {
|
||||
if (
|
||||
!getBlockElement(this.getRootNode() as Document | ShadowRoot) !==
|
||||
event.shiftKey
|
||||
) {
|
||||
event.preventDefault();
|
||||
document.execCommand("insertLineBreak");
|
||||
}
|
||||
}
|
||||
|
||||
onPaste(event: ClipboardEvent): void {
|
||||
bridgeCommand("paste");
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,23 +6,20 @@
|
|||
*/
|
||||
|
||||
import type { Editable } from "./editable";
|
||||
import type { Codable } from "./codable";
|
||||
|
||||
import { updateActiveButtons } from "./toolbar";
|
||||
import { bridgeCommand } from "./lib";
|
||||
import { onInput, onKey, onKeyUp } from "./inputHandlers";
|
||||
import { onFocus, onBlur } from "./focusHandlers";
|
||||
|
||||
function onPaste(evt: ClipboardEvent): void {
|
||||
bridgeCommand("paste");
|
||||
evt.preventDefault();
|
||||
}
|
||||
|
||||
function onCutOrCopy(): void {
|
||||
bridgeCommand("cutOrCopy");
|
||||
}
|
||||
|
||||
export class EditingArea extends HTMLDivElement {
|
||||
editable: Editable;
|
||||
codable: Codable;
|
||||
baseStyle: HTMLStyleElement;
|
||||
|
||||
constructor() {
|
||||
|
@ -41,6 +38,17 @@ export class EditingArea extends HTMLDivElement {
|
|||
|
||||
this.editable = document.createElement("anki-editable") as Editable;
|
||||
this.shadowRoot!.appendChild(this.editable);
|
||||
|
||||
this.codable = document.createElement("textarea", {
|
||||
is: "anki-codable",
|
||||
}) as Codable;
|
||||
this.shadowRoot!.appendChild(this.codable);
|
||||
|
||||
this.onPaste = this.onPaste.bind(this);
|
||||
}
|
||||
|
||||
get activeInput(): Editable | Codable {
|
||||
return this.codable.active ? this.codable : this.editable;
|
||||
}
|
||||
|
||||
get ord(): number {
|
||||
|
@ -48,11 +56,11 @@ export class EditingArea extends HTMLDivElement {
|
|||
}
|
||||
|
||||
set fieldHTML(content: string) {
|
||||
this.editable.fieldHTML = content;
|
||||
this.activeInput.fieldHTML = content;
|
||||
}
|
||||
|
||||
get fieldHTML(): string {
|
||||
return this.editable.fieldHTML;
|
||||
return this.activeInput.fieldHTML;
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
|
@ -61,7 +69,7 @@ export class EditingArea extends HTMLDivElement {
|
|||
this.addEventListener("input", onInput);
|
||||
this.addEventListener("focus", onFocus);
|
||||
this.addEventListener("blur", onBlur);
|
||||
this.addEventListener("paste", onPaste);
|
||||
this.addEventListener("paste", this.onPaste);
|
||||
this.addEventListener("copy", onCutOrCopy);
|
||||
this.addEventListener("oncut", onCutOrCopy);
|
||||
this.addEventListener("mouseup", updateActiveButtons);
|
||||
|
@ -76,7 +84,7 @@ export class EditingArea extends HTMLDivElement {
|
|||
this.removeEventListener("input", onInput);
|
||||
this.removeEventListener("focus", onFocus);
|
||||
this.removeEventListener("blur", onBlur);
|
||||
this.removeEventListener("paste", onPaste);
|
||||
this.removeEventListener("paste", this.onPaste);
|
||||
this.removeEventListener("copy", onCutOrCopy);
|
||||
this.removeEventListener("oncut", onCutOrCopy);
|
||||
this.removeEventListener("mouseup", updateActiveButtons);
|
||||
|
@ -107,15 +115,65 @@ export class EditingArea extends HTMLDivElement {
|
|||
return firstRule.style.direction === "rtl";
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
this.activeInput.focus();
|
||||
}
|
||||
|
||||
blur(): void {
|
||||
this.activeInput.blur();
|
||||
}
|
||||
|
||||
caretToEnd(): void {
|
||||
this.activeInput.caretToEnd();
|
||||
}
|
||||
|
||||
hasFocus(): boolean {
|
||||
return document.activeElement === this;
|
||||
}
|
||||
|
||||
getSelection(): Selection {
|
||||
return this.shadowRoot!.getSelection()!;
|
||||
}
|
||||
|
||||
focusEditable(): void {
|
||||
this.editable.focus();
|
||||
surroundSelection(before: string, after: string): void {
|
||||
this.activeInput.surroundSelection(before, after);
|
||||
}
|
||||
|
||||
onEnter(event: KeyboardEvent): void {
|
||||
this.activeInput.onEnter(event);
|
||||
}
|
||||
|
||||
onPaste(event: ClipboardEvent): void {
|
||||
this.activeInput.onPaste(event);
|
||||
}
|
||||
|
||||
toggleHtmlEdit(): void {
|
||||
const hadFocus = this.hasFocus();
|
||||
|
||||
if (this.codable.active) {
|
||||
this.fieldHTML = this.codable.teardown();
|
||||
this.editable.hidden = false;
|
||||
} else {
|
||||
this.editable.hidden = true;
|
||||
this.codable.setup(this.fieldHTML);
|
||||
}
|
||||
|
||||
if (hadFocus) {
|
||||
this.focus();
|
||||
this.caretToEnd();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use focus instead
|
||||
*/
|
||||
focusEditable(): void {
|
||||
focus();
|
||||
}
|
||||
/**
|
||||
* @deprecated Use blur instead
|
||||
*/
|
||||
blurEditable(): void {
|
||||
this.editable.blur();
|
||||
blur();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import { bridgeCommand } from "./lib";
|
|||
|
||||
export function onFocus(evt: FocusEvent): void {
|
||||
const currentField = evt.currentTarget as EditingArea;
|
||||
currentField.focusEditable();
|
||||
currentField.focus();
|
||||
bridgeCommand(`focus:${currentField.ord}`);
|
||||
enableButtons();
|
||||
}
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import type { EditingArea } from "./editingArea";
|
||||
/* eslint
|
||||
@typescript-eslint/no-non-null-assertion: "off",
|
||||
*/
|
||||
|
||||
export function nodeIsElement(node: Node): node is Element {
|
||||
return node.nodeType === Node.ELEMENT_NODE;
|
||||
|
@ -69,11 +71,11 @@ export function nodeIsInline(node: Node): boolean {
|
|||
return !nodeIsElement(node) || INLINE_TAGS.includes(node.tagName);
|
||||
}
|
||||
|
||||
export function caretToEnd(currentField: EditingArea): void {
|
||||
export function caretToEnd(node: Node): void {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(currentField.editable);
|
||||
range.selectNodeContents(node);
|
||||
range.collapse(false);
|
||||
const selection = currentField.getSelection();
|
||||
const selection = (node.getRootNode() as Document | ShadowRoot).getSelection()!;
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
|
|
|
@ -11,13 +11,13 @@ import { setupI18n, ModuleName } from "lib/i18n";
|
|||
|
||||
import "./fields.css";
|
||||
|
||||
import { caretToEnd } from "./helpers";
|
||||
import { saveField } from "./changeTimer";
|
||||
|
||||
import { EditorField } from "./editorField";
|
||||
import { LabelContainer } from "./labelContainer";
|
||||
import { EditingArea } from "./editingArea";
|
||||
import { Editable } from "./editable";
|
||||
import { Codable } from "./codable";
|
||||
import { initToolbar } from "./toolbar";
|
||||
|
||||
export { setNoteId, getNoteId } from "./noteId";
|
||||
|
@ -35,6 +35,7 @@ declare global {
|
|||
}
|
||||
|
||||
customElements.define("anki-editable", Editable);
|
||||
customElements.define("anki-codable", Codable, { extends: "textarea" });
|
||||
customElements.define("anki-editing-area", EditingArea, { extends: "div" });
|
||||
customElements.define("anki-label-container", LabelContainer, { extends: "div" });
|
||||
customElements.define("anki-editor-field", EditorField, { extends: "div" });
|
||||
|
@ -49,8 +50,8 @@ export function focusField(n: number): void {
|
|||
const field = getEditorField(n);
|
||||
|
||||
if (field) {
|
||||
field.editingArea.focusEditable();
|
||||
caretToEnd(field.editingArea);
|
||||
field.editingArea.focus();
|
||||
field.editingArea.caretToEnd();
|
||||
updateActiveButtons(new Event("manualfocus"));
|
||||
}
|
||||
}
|
||||
|
@ -60,7 +61,7 @@ export function focusIfField(x: number, y: number): boolean {
|
|||
for (let i = 0; i < elements.length; i++) {
|
||||
const elem = elements[i] as EditingArea;
|
||||
if (elem instanceof EditingArea) {
|
||||
elem.focusEditable();
|
||||
elem.focus();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { updateActiveButtons } from "./toolbar";
|
||||
import { EditingArea } from "./editingArea";
|
||||
import { caretToEnd, nodeIsElement, getBlockElement } from "./helpers";
|
||||
import { nodeIsElement } from "./helpers";
|
||||
import { triggerChangeTimer } from "./changeTimer";
|
||||
import { registerShortcut } from "lib/shortcuts";
|
||||
|
||||
|
@ -22,17 +22,12 @@ export function onKey(evt: KeyboardEvent): void {
|
|||
|
||||
// esc clears focus, allowing dialog to close
|
||||
if (evt.code === "Escape") {
|
||||
currentField.blurEditable();
|
||||
return;
|
||||
return currentField.blur();
|
||||
}
|
||||
|
||||
// prefer <br> instead of <div></div>
|
||||
if (
|
||||
evt.code === "Enter" &&
|
||||
!getBlockElement(currentField.shadowRoot!) !== evt.shiftKey
|
||||
) {
|
||||
evt.preventDefault();
|
||||
document.execCommand("insertLineBreak");
|
||||
if (evt.code === "Enter") {
|
||||
return currentField.onEnter(evt);
|
||||
}
|
||||
|
||||
// // fix Ctrl+right/left handling in RTL fields
|
||||
|
@ -59,7 +54,7 @@ export function onKey(evt: KeyboardEvent): void {
|
|||
function updateFocus(evt: FocusEvent) {
|
||||
const newFocusTarget = evt.target;
|
||||
if (newFocusTarget instanceof EditingArea) {
|
||||
caretToEnd(newFocusTarget);
|
||||
newFocusTarget.caretToEnd();
|
||||
updateActiveButtons(evt);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,12 +7,14 @@
|
|||
*/
|
||||
|
||||
import { disabledKey, nightModeKey } from "components/contextKeys";
|
||||
import { inCodableKey } from "./contextKeys";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
import EditorToolbar from "./EditorToolbar.svelte";
|
||||
import "./bootstrap.css";
|
||||
|
||||
const disabled = writable(false);
|
||||
const inCodable = writable(false);
|
||||
|
||||
export function initToolbar(i18n: Promise<void>): Promise<EditorToolbar> {
|
||||
let toolbarResolve: (value: EditorToolbar) => void;
|
||||
|
@ -27,6 +29,7 @@ export function initToolbar(i18n: Promise<void>): Promise<EditorToolbar> {
|
|||
|
||||
const context = new Map();
|
||||
context.set(disabledKey, disabled);
|
||||
context.set(inCodableKey, inCodable);
|
||||
context.set(
|
||||
nightModeKey,
|
||||
document.documentElement.classList.contains("night-mode")
|
||||
|
@ -47,6 +50,14 @@ export function disableButtons(): void {
|
|||
disabled.set(true);
|
||||
}
|
||||
|
||||
export function setCodableButtons(): void {
|
||||
inCodable.set(true);
|
||||
}
|
||||
|
||||
export function setEditableButtons(): void {
|
||||
inCodable.set(false);
|
||||
}
|
||||
|
||||
export {
|
||||
updateActiveButtons,
|
||||
clearActiveButtons,
|
||||
|
|
|
@ -45,6 +45,11 @@ export function wrap(front: string, back: string): void {
|
|||
wrapInternal(front, back, false);
|
||||
}
|
||||
|
||||
export function wrapCurrent(front: string, back: string): void {
|
||||
const currentField = getCurrentField()!;
|
||||
currentField.surroundSelection(front, back);
|
||||
}
|
||||
|
||||
/* currently unused */
|
||||
export function wrapIntoText(front: string, back: string): void {
|
||||
wrapInternal(front, back, true);
|
||||
|
|
|
@ -147,6 +147,15 @@
|
|||
"path": "node_modules/bootstrap",
|
||||
"licenseFile": "node_modules/bootstrap/LICENSE"
|
||||
},
|
||||
"codemirror@5.61.1": {
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/codemirror/CodeMirror",
|
||||
"publisher": "Marijn Haverbeke",
|
||||
"email": "marijnh@gmail.com",
|
||||
"url": "http://marijnhaverbeke.nl",
|
||||
"path": "node_modules/codemirror",
|
||||
"licenseFile": "node_modules/codemirror/LICENSE"
|
||||
},
|
||||
"commander@7.2.0": {
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/tj/commander.js",
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
"@sqltools/formatter": "^1.2.2",
|
||||
"@tsconfig/svelte": "^1.0.10",
|
||||
"@types/bootstrap": "^5.0.12",
|
||||
"@types/codemirror": "^5.60.0",
|
||||
"@types/d3": "^6.3.0",
|
||||
"@types/diff": "^5.0.0",
|
||||
"@types/jest": "^26.0.22",
|
||||
|
@ -60,6 +61,7 @@
|
|||
"@types/marked": "^2.0.2",
|
||||
"bootstrap": "=5.0.0-beta3",
|
||||
"bootstrap-icons": "^1.4.0",
|
||||
"codemirror": "^5.61.1",
|
||||
"css-browser-selector": "^0.6.5",
|
||||
"d3": "^7.0.0",
|
||||
"intl-pluralrules": "^1.2.2",
|
||||
|
|
|
@ -8,7 +8,7 @@ sass_library(
|
|||
"bootstrap-dark.scss",
|
||||
],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = ["//ts/sass/bootstrap"],
|
||||
deps = ["//ts/sass/bootstrap", "//ts/sass/codemirror"],
|
||||
)
|
||||
|
||||
sass_library(
|
||||
|
|
24
ts/sass/codemirror/BUILD.bazel
Normal file
24
ts/sass/codemirror/BUILD.bazel
Normal file
|
@ -0,0 +1,24 @@
|
|||
load("//ts:vendor.bzl", "pkg_from_name", "vendor_js_lib")
|
||||
load("@io_bazel_rules_sass//:defs.bzl", "sass_library")
|
||||
|
||||
# copy codemirror sass files in
|
||||
vendor_js_lib(
|
||||
name = "sass-sources",
|
||||
include = [
|
||||
"lib/codemirror.css",
|
||||
"theme",
|
||||
"addon/fold/foldgutter.css",
|
||||
],
|
||||
base = "external/npm/node_modules/codemirror/",
|
||||
pkg = pkg_from_name("codemirror"),
|
||||
visibility = ["//visibility:private"],
|
||||
)
|
||||
|
||||
# wrap them in a library
|
||||
sass_library(
|
||||
name = "codemirror",
|
||||
srcs = [
|
||||
":sass-sources",
|
||||
],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
24
ts/yarn.lock
24
ts/yarn.lock
|
@ -701,6 +701,13 @@
|
|||
"@popperjs/core" "^2.9.2"
|
||||
"@types/jquery" "*"
|
||||
|
||||
"@types/codemirror@^5.60.0":
|
||||
version "5.60.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/codemirror/-/codemirror-5.60.0.tgz#bf14b728449ebd355c17054262a083639a995710"
|
||||
integrity sha512-xgzXZyCzedLRNC67/Nn8rpBtTFnAsX2C+Q/LGoH6zgcpF/LqdNHJMHEOhqT1bwUcSp6kQdOIuKzRbeW9DYhEhg==
|
||||
dependencies:
|
||||
"@types/tern" "*"
|
||||
|
||||
"@types/d3-array@*":
|
||||
version "2.12.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-2.12.1.tgz#bee6857b812f1ecfd5e6832fd67f617b667dd024"
|
||||
|
@ -921,6 +928,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/diff/-/diff-5.0.0.tgz#eb71e94feae62548282c4889308a3dfb57e36020"
|
||||
integrity sha512-jrm2K65CokCCX4NmowtA+MfXyuprZC13jbRuwprs6/04z/EcFg/MCwYdsHn+zgV4CQBiATiI7AEq7y1sZCtWKA==
|
||||
|
||||
"@types/estree@*":
|
||||
version "0.0.48"
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.48.tgz#18dc8091b285df90db2f25aa7d906cfc394b7f74"
|
||||
integrity sha512-LfZwXoGUDo0C3me81HXgkBg5CTQYb6xzEl+fNmbO4JdRiSKQ8A0GD1OBBvKAIsbCUgoyAty7m99GqqMQe784ew==
|
||||
|
||||
"@types/geojson@*":
|
||||
version "7946.0.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.7.tgz#c8fa532b60a0042219cdf173ca21a975ef0666ad"
|
||||
|
@ -1069,6 +1081,13 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.0.tgz#7036640b4e21cc2f259ae826ce843d277dad8cff"
|
||||
integrity sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw==
|
||||
|
||||
"@types/tern@*":
|
||||
version "0.23.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/tern/-/tern-0.23.3.tgz#4b54538f04a88c9ff79de1f6f94f575a7f339460"
|
||||
integrity sha512-imDtS4TAoTcXk0g7u4kkWqedB3E4qpjXzCpD2LU5M5NAXHzCDsypyvXSaG7mM8DKYkCRa7tFp4tS/lp/Wo7Q3w==
|
||||
dependencies:
|
||||
"@types/estree" "*"
|
||||
|
||||
"@types/yargs-parser@*":
|
||||
version "20.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.0.tgz#dd3e6699ba3237f0348cd085e4698780204842f9"
|
||||
|
@ -1516,6 +1535,11 @@ co@^4.6.0:
|
|||
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
|
||||
integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=
|
||||
|
||||
codemirror@^5.61.1:
|
||||
version "5.61.1"
|
||||
resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.61.1.tgz#ccfc8a43b8fcfb8b12e8e75b5ffde48d541406e0"
|
||||
integrity sha512-+D1NZjAucuzE93vJGbAaXzvoBHwp9nJZWWWF9utjv25+5AZUiah6CIlfb4ikG4MoDsFsCG8niiJH5++OO2LgIQ==
|
||||
|
||||
collect-v8-coverage@^1.0.0:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59"
|
||||
|
|
Loading…
Reference in a new issue