Merge pull request #1242 from hgiesel/codable

In-line HTML-Editing for Editor
This commit is contained in:
Damien Elmes 2021-06-18 17:18:35 +10:00 committed by GitHub
commit 039be57499
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 645 additions and 372 deletions

View file

@ -3,9 +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="typescript"> <script lang="typescript">
import type { Readable } from "svelte/store";
import { getContext, onMount, createEventDispatcher } from "svelte"; import { getContext, onMount, createEventDispatcher } from "svelte";
import { disabledKey, nightModeKey, dropdownKey } from "./contextKeys"; import { nightModeKey, dropdownKey } from "./contextKeys";
import type { DropdownProps } from "./dropdown"; import type { DropdownProps } from "./dropdown";
export let id: string | undefined = undefined; 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 tooltip: string | undefined = undefined;
export let active = false; export let active = false;
export let disables = true; export let disabled = false;
export const disables = false; /* unused */
export let tabbable = false; export let tabbable = false;
export let iconSize: number = 75; 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; let buttonRef: HTMLButtonElement;
const disabled = getContext<Readable<boolean>>(disabledKey);
$: _disabled = disables && $disabled;
const nightMode = getContext<boolean>(nightModeKey); const nightMode = getContext<boolean>(nightModeKey);
const dropdownProps = getContext<DropdownProps>(dropdownKey) ?? { dropdown: false }; 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}%`} style={`--icon-size: ${iconSize}%`}
title={tooltip} title={tooltip}
{...dropdownProps} {...dropdownProps}
disabled={_disabled} {disabled}
tabindex={tabbable ? 0 : -1} tabindex={tabbable ? 0 : -1}
on:click on:click
on:mousedown|preventDefault on:mousedown|preventDefault

View 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} />

View file

@ -5,14 +5,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<script lang="typescript" context="module"> <script lang="typescript" context="module">
import { writable } from "svelte/store"; import { writable } from "svelte/store";
type UpdaterMap = Map<string, (event: Event) => boolean>; type KeyType = Symbol | string;
type StateMap = Map<string, boolean>; type UpdaterMap = Map<KeyType, (event: Event) => boolean>;
type StateMap = Map<KeyType, boolean>;
const updaterMap = new Map() as UpdaterMap; const updaterMap = new Map() as UpdaterMap;
const stateMap = new Map() as StateMap; const stateMap = new Map() as StateMap;
const stateStore = writable(stateMap); const stateStore = writable(stateMap);
function updateAllStateWithCallback(callback: (key: string) => boolean): void { function updateAllStateWithCallback(callback: (key: KeyType) => boolean): void {
stateStore.update((map: StateMap): StateMap => { stateStore.update((map: StateMap): StateMap => {
const newMap = new Map() as 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 { export function updateAllState(event: Event): void {
updateAllStateWithCallback((key: string): boolean => updateAllStateWithCallback((key: KeyType): boolean =>
updaterMap.get(key)!(event) 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); updateAllStateWithCallback((): boolean => state);
} }
function updateStateByKey(key: string, event: Event): void { function updateStateByKey(key: KeyType, event: Event): void {
stateStore.update((map: StateMap): StateMap => { stateStore.update((map: StateMap): StateMap => {
map.set(key, updaterMap.get(key)!(event)); map.set(key, updaterMap.get(key)!(event));
return map; return map;
@ -43,7 +44,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
</script> </script>
<script lang="typescript"> <script lang="typescript">
export let key: string; export let key: KeyType;
export let update: (event: Event) => boolean; export let update: (event: Event) => boolean;
let state: boolean = false; let state: boolean = false;

View file

@ -73,6 +73,8 @@ ts_library(
"//ts/html-filter", "//ts/html-filter",
"//ts:image_module_support", "//ts:image_module_support",
"@npm//svelte", "@npm//svelte",
"@npm//@types/codemirror",
"@npm//codemirror",
] + svelte_names, ] + svelte_names,
) )
@ -177,7 +179,7 @@ svelte_check(
]) + [ ]) + [
"//ts/sass:button_mixins_lib", "//ts/sass:button_mixins_lib",
"//ts/sass/bootstrap", "//ts/sass/bootstrap",
"//ts/components:svelte_components",
"@npm//@types/bootstrap", "@npm//@types/bootstrap",
"//ts/components",
], ],
) )

View file

@ -4,20 +4,22 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
--> -->
<script lang="typescript"> <script lang="typescript">
import * as tr from "lib/i18n"; import * as tr from "lib/i18n";
import { disabledKey } from "components/contextKeys";
import IconButton from "components/IconButton.svelte"; import IconButton from "components/IconButton.svelte";
import WithShortcut from "components/WithShortcut.svelte"; import WithShortcut from "components/WithShortcut.svelte";
import WithContext from "components/WithContext.svelte";
import { bracketsIcon } from "./icons"; import { bracketsIcon } from "./icons";
import { forEditorField } from "."; import { forEditorField } from ".";
import { wrap } from "./wrap"; import { wrapCurrent } from "./wrap";
const clozePattern = /\{\{c(\d+)::/gu; const clozePattern = /\{\{c(\d+)::/gu;
function getCurrentHighestCloze(increment: boolean): number { function getCurrentHighestCloze(increment: boolean): number {
let highest = 0; let highest = 0;
forEditorField([], (field) => { forEditorField([], (field) => {
const fieldHTML = field.editingArea.editable.fieldHTML; const fieldHTML = field.editingArea.fieldHTML;
const matches: number[] = []; const matches: number[] = [];
let match: RegExpMatchArray | null = null; 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 { function onCloze(event: KeyboardEvent | MouseEvent): void {
const highestCloze = getCurrentHighestCloze(!event.getModifierState("Alt")); const highestCloze = getCurrentHighestCloze(!event.getModifierState("Alt"));
wrap(`{{c${highestCloze}::`, "}}"); wrapCurrent(`{{c${highestCloze}::`, "}}");
} }
</script> </script>
<WithShortcut shortcut={"Control+Alt?+Shift+C"} let:createShortcut let:shortcutLabel> <WithShortcut shortcut={"Control+Alt?+Shift+C"} let:createShortcut let:shortcutLabel>
<IconButton <WithContext key={disabledKey} let:context={disabled}>
tooltip={`${tr.editingClozeDeletion()} (${shortcutLabel})`} <IconButton
on:click={onCloze} tooltip={`${tr.editingClozeDeletion()} (${shortcutLabel})`}
on:mount={createShortcut} {disabled}
> on:click={onCloze}
{@html bracketsIcon} on:mount={createShortcut}
</IconButton> >
{@html bracketsIcon}
</IconButton>
</WithContext>
</WithShortcut> </WithShortcut>

View file

@ -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 ColorPicker from "components/ColorPicker.svelte";
import WithShortcut from "components/WithShortcut.svelte"; import WithShortcut from "components/WithShortcut.svelte";
import WithColorHelper from "./WithColorHelper.svelte"; import WithColorHelper from "./WithColorHelper.svelte";
import OnlyEditable from "./OnlyEditable.svelte";
import { textColorIcon, highlightColorIcon, arrowIcon } from "./icons"; import { textColorIcon, highlightColorIcon, arrowIcon } from "./icons";
import { appendInParentheses } from "./helpers"; import { appendInParentheses } from "./helpers";
@ -28,51 +29,61 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<ButtonGroup {api}> <ButtonGroup {api}>
<WithColorHelper let:colorHelperIcon let:color let:setColor> <WithColorHelper let:colorHelperIcon let:color let:setColor>
<ButtonGroupItem> <OnlyEditable let:disabled>
<WithShortcut shortcut={"F7"} let:createShortcut let:shortcutLabel> <ButtonGroupItem>
<IconButton <WithShortcut shortcut={"F7"} let:createShortcut let:shortcutLabel>
tooltip={appendInParentheses( <IconButton
tr.editingSetForegroundColor(), tooltip={appendInParentheses(
shortcutLabel tr.editingSetForegroundColor(),
)} shortcutLabel
on:click={wrapWithForecolor(color)} )}
on:mount={createShortcut} {disabled}
> on:click={wrapWithForecolor(color)}
{@html textColorIcon} on:mount={createShortcut}
{@html colorHelperIcon} >
</IconButton> {@html textColorIcon}
</WithShortcut> {@html colorHelperIcon}
</ButtonGroupItem> </IconButton>
</WithShortcut>
</ButtonGroupItem>
<ButtonGroupItem> <ButtonGroupItem>
<WithShortcut shortcut={"F8"} let:createShortcut let:shortcutLabel> <WithShortcut shortcut={"F8"} let:createShortcut let:shortcutLabel>
<IconButton <IconButton
tooltip={appendInParentheses( tooltip={appendInParentheses(
tr.editingChangeColor(), tr.editingChangeColor(),
shortcutLabel shortcutLabel
)} )}
widthMultiplier={0.5} {disabled}
> widthMultiplier={0.5}
{@html arrowIcon} >
<ColorPicker on:change={setColor} on:mount={createShortcut} /> {@html arrowIcon}
</IconButton> <ColorPicker on:change={setColor} on:mount={createShortcut} />
</WithShortcut> </IconButton>
</ButtonGroupItem> </WithShortcut>
</ButtonGroupItem>
</OnlyEditable>
</WithColorHelper> </WithColorHelper>
<WithColorHelper let:colorHelperIcon let:color let:setColor> <WithColorHelper let:colorHelperIcon let:color let:setColor>
<ButtonGroupItem> <OnlyEditable let:disabled>
<IconButton on:click={wrapWithBackcolor(color)}> <ButtonGroupItem>
{@html highlightColorIcon} <IconButton on:click={wrapWithBackcolor(color)} {disabled}>
{@html colorHelperIcon} {@html highlightColorIcon}
</IconButton> {@html colorHelperIcon}
</ButtonGroupItem> </IconButton>
</ButtonGroupItem>
<ButtonGroupItem> <ButtonGroupItem>
<IconButton tooltip={tr.editingChangeColor()} widthMultiplier={0.5}> <IconButton
{@html arrowIcon} tooltip={tr.editingChangeColor()}
<ColorPicker on:change={setColor} /> widthMultiplier={0.5}
</IconButton> {disabled}
</ButtonGroupItem> >
{@html arrowIcon}
<ColorPicker on:change={setColor} />
</IconButton>
</ButtonGroupItem>
</OnlyEditable>
</WithColorHelper> </WithColorHelper>
</ButtonGroup> </ButtonGroup>

View 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>

View file

@ -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 IconButton from "components/IconButton.svelte";
import ButtonDropdown from "components/ButtonDropdown.svelte"; import ButtonDropdown from "components/ButtonDropdown.svelte";
import ButtonToolbarItem from "components/ButtonToolbarItem.svelte"; import ButtonToolbarItem from "components/ButtonToolbarItem.svelte";
import WithState from "components/WithState.svelte";
import WithDropdownMenu from "components/WithDropdownMenu.svelte"; import WithDropdownMenu from "components/WithDropdownMenu.svelte";
import OnlyEditable from "./OnlyEditable.svelte";
import CommandIconButton from "./CommandIconButton.svelte";
import { getListItem } from "./helpers"; import { getListItem } from "./helpers";
import { import {
@ -46,134 +47,66 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<ButtonGroup {api}> <ButtonGroup {api}>
<ButtonGroupItem> <ButtonGroupItem>
<WithState <CommandIconButton
key="insertUnorderedList" key="insertUnorderedList"
update={() => document.queryCommandState("insertUnorderedList")} tooltip={tr.editingUnorderedList()}
let:state={active} withoutShortcut>{@html ulIcon}</CommandIconButton
let:updateState
> >
<IconButton
tooltip={tr.editingUnorderedList()}
{active}
on:click={(event) => {
document.execCommand("insertUnorderedList");
updateState(event);
}}
>
{@html ulIcon}
</IconButton>
</WithState>
</ButtonGroupItem> </ButtonGroupItem>
<ButtonGroupItem> <ButtonGroupItem>
<WithState <CommandIconButton
key="insertOrderedList" key="insertOrderedList"
update={() => document.queryCommandState("insertOrderedList")} tooltip={tr.editingOrderedList()}
let:state={active} withoutShortcut>{@html olIcon}</CommandIconButton
let:updateState
> >
<IconButton
tooltip={tr.editingOrderedList()}
{active}
on:click={(event) => {
document.execCommand("insertOrderedList");
updateState(event);
}}
>
{@html olIcon}
</IconButton>
</WithState>
</ButtonGroupItem> </ButtonGroupItem>
<ButtonGroupItem> <ButtonGroupItem>
<WithDropdownMenu let:createDropdown let:menuId> <WithDropdownMenu let:createDropdown let:menuId>
<IconButton on:mount={createDropdown}> <OnlyEditable let:disabled>
{@html listOptionsIcon} <IconButton {disabled} on:mount={createDropdown}>
</IconButton> {@html listOptionsIcon}
</IconButton>
</OnlyEditable>
<ButtonDropdown id={menuId}> <ButtonDropdown id={menuId}>
<ButtonToolbarItem id="justify"> <ButtonToolbarItem id="justify">
<ButtonGroup> <ButtonGroup>
<ButtonGroupItem> <ButtonGroupItem>
<WithState <CommandIconButton
key="justifyLeft" key="justifyLeft"
update={() => document.queryCommandState("justifyLeft")} tooltip={tr.editingAlignLeft()}
let:state={active} withoutShortcut
let:updateState >{@html justifyLeftIcon}</CommandIconButton
> >
<IconButton
tooltip={tr.editingAlignLeft()}
{active}
on:click={(event) => {
document.execCommand("justifyLeft");
updateState(event);
}}
>
{@html justifyLeftIcon}
</IconButton>
</WithState>
</ButtonGroupItem> </ButtonGroupItem>
<ButtonGroupItem> <ButtonGroupItem>
<WithState <CommandIconButton
key="justifyCenter" key="justifyCenter"
update={() => tooltip={tr.editingCenter()}
document.queryCommandState("justifyCenter")} withoutShortcut
let:state={active} >{@html justifyCenterIcon}</CommandIconButton
let:updateState
> >
<IconButton
tooltip={tr.editingCenter()}
{active}
on:click={(event) => {
document.execCommand("justifyCenter");
updateState(event);
}}
>
{@html justifyCenterIcon}
</IconButton>
</WithState>
</ButtonGroupItem> </ButtonGroupItem>
<ButtonGroupItem> <ButtonGroupItem>
<WithState <CommandIconButton
key="justifyRight" key="justifyRight"
update={() => tooltip={tr.editingAlignRight()}
document.queryCommandState("justifyRight")} withoutShortcut
let:state={active} >{@html justifyRightIcon}</CommandIconButton
let:updateState
> >
<IconButton
tooltip={tr.editingAlignRight()}
{active}
on:click={(event) => {
document.execCommand("justifyRight");
updateState(event);
}}
>
{@html justifyRightIcon}
</IconButton>
</WithState>
</ButtonGroupItem> </ButtonGroupItem>
<ButtonGroupItem> <ButtonGroupItem>
<WithState <CommandIconButton
key="justifyFull" key="justifyFull"
update={() => document.queryCommandState("justifyFull")} tooltip={tr.editingJustify()}
let:state={active} withoutShortcut
let:updateState >{@html justifyFullIcon}</CommandIconButton
> >
<IconButton
tooltip={tr.editingJustify()}
{active}
on:click={(event) => {
document.execCommand("justifyFull");
updateState(event);
}}
>
{@html justifyFullIcon}
</IconButton>
</WithState>
</ButtonGroupItem> </ButtonGroupItem>
</ButtonGroup> </ButtonGroup>
</ButtonToolbarItem> </ButtonToolbarItem>
@ -181,21 +114,27 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<ButtonToolbarItem id="indentation"> <ButtonToolbarItem id="indentation">
<ButtonGroup> <ButtonGroup>
<ButtonGroupItem> <ButtonGroupItem>
<IconButton <OnlyEditable let:disabled>
on:click={outdentListItem} <IconButton
tooltip={tr.editingOutdent()} on:click={outdentListItem}
> tooltip={tr.editingOutdent()}
{@html outdentIcon} {disabled}
</IconButton> >
{@html outdentIcon}
</IconButton>
</OnlyEditable>
</ButtonGroupItem> </ButtonGroupItem>
<ButtonGroupItem> <ButtonGroupItem>
<IconButton <OnlyEditable let:disabled>
on:click={indentListItem} <IconButton
tooltip={tr.editingIndent()} on:click={indentListItem}
> tooltip={tr.editingIndent()}
{@html indentIcon} {disabled}
</IconButton> >
{@html indentIcon}
</IconButton>
</OnlyEditable>
</ButtonGroupItem> </ButtonGroupItem>
</ButtonGroup> </ButtonGroup>
</ButtonToolbarItem> </ButtonToolbarItem>

View file

@ -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 ButtonGroup from "components/ButtonGroup.svelte";
import ButtonGroupItem from "components/ButtonGroupItem.svelte"; import ButtonGroupItem from "components/ButtonGroupItem.svelte";
import IconButton from "components/IconButton.svelte"; import CommandIconButton from "./CommandIconButton.svelte";
import WithState from "components/WithState.svelte";
import WithShortcut from "components/WithShortcut.svelte";
import { import {
boldIcon, boldIcon,
@ -19,147 +17,57 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
subscriptIcon, subscriptIcon,
eraserIcon, eraserIcon,
} from "./icons"; } from "./icons";
import { appendInParentheses } from "./helpers";
export let api = {}; export let api = {};
</script> </script>
<ButtonGroup {api}> <ButtonGroup {api}>
<ButtonGroupItem> <ButtonGroupItem>
<WithShortcut shortcut={"Control+B"} let:createShortcut let:shortcutLabel> <CommandIconButton
<WithState key="bold"
key="bold" shortcut={"Control+B"}
update={() => document.queryCommandState("bold")} tooltip={tr.editingBoldText()}>{@html boldIcon}</CommandIconButton
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>
</ButtonGroupItem> </ButtonGroupItem>
<ButtonGroupItem> <ButtonGroupItem>
<WithShortcut shortcut={"Control+I"} let:createShortcut let:shortcutLabel> <CommandIconButton
<WithState key="italic"
key="italic" shortcut={"Control+I"}
update={() => document.queryCommandState("italic")} tooltip={tr.editingItalicText()}>{@html italicIcon}</CommandIconButton
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>
</ButtonGroupItem> </ButtonGroupItem>
<ButtonGroupItem> <ButtonGroupItem>
<WithShortcut shortcut={"Control+U"} let:createShortcut let:shortcutLabel> <CommandIconButton
<WithState key="underline"
key="underline" shortcut={"Control+U"}
update={() => document.queryCommandState("underline")} tooltip={tr.editingUnderlineText()}>{@html underlineIcon}</CommandIconButton
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>
</ButtonGroupItem> </ButtonGroupItem>
<ButtonGroupItem> <ButtonGroupItem>
<WithShortcut shortcut={"Control+="} let:createShortcut let:shortcutLabel> <CommandIconButton
<WithState key="superscript"
key="superscript" shortcut={"Control+="}
update={() => document.queryCommandState("superscript")} tooltip={tr.editingSuperscript()}>{@html superscriptIcon}</CommandIconButton
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>
</ButtonGroupItem> </ButtonGroupItem>
<ButtonGroupItem> <ButtonGroupItem>
<WithShortcut shortcut={"Control+Shift+="} let:createShortcut let:shortcutLabel> <CommandIconButton
<WithState key="subscript"
key="subscript" shortcut={"Control+Shift+="}
update={() => document.queryCommandState("subscript")} tooltip={tr.editingSubscript()}>{@html subscriptIcon}</CommandIconButton
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>
</ButtonGroupItem> </ButtonGroupItem>
<ButtonGroupItem> <ButtonGroupItem>
<WithShortcut shortcut={"Control+R"} let:createShortcut let:shortcutLabel> <CommandIconButton
<IconButton key="removeFormat"
tooltip={appendInParentheses( shortcut={"Control+R"}
tr.editingRemoveFormatting(), tooltip={tr.editingRemoveFormatting()}
shortcutLabel withoutState>{@html eraserIcon}</CommandIconButton
)} >
on:click={() => {
document.execCommand("removeFormat");
}}
on:mount={createShortcut}
>
{@html eraserIcon}
</IconButton>
</WithShortcut>
</ButtonGroupItem> </ButtonGroupItem>
</ButtonGroup> </ButtonGroup>

View 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>

View file

@ -5,6 +5,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<script lang="typescript"> <script lang="typescript">
import * as tr from "lib/i18n"; import * as tr from "lib/i18n";
import { bridgeCommand } from "lib/bridgecommand"; import { bridgeCommand } from "lib/bridgecommand";
import { disabledKey } from "components/contextKeys";
import { inCodableKey } from "./contextKeys";
import ButtonGroup from "components/ButtonGroup.svelte"; import ButtonGroup from "components/ButtonGroup.svelte";
import ButtonGroupItem from "components/ButtonGroupItem.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 DropdownItem from "components/DropdownItem.svelte";
import WithDropdownMenu from "components/WithDropdownMenu.svelte"; import WithDropdownMenu from "components/WithDropdownMenu.svelte";
import WithShortcut from "components/WithShortcut.svelte"; import WithShortcut from "components/WithShortcut.svelte";
import WithContext from "components/WithContext.svelte";
import OnlyEditable from "./OnlyEditable.svelte";
import ClozeButton from "./ClozeButton.svelte"; import ClozeButton from "./ClozeButton.svelte";
import { wrap } from "./wrap"; import { getCurrentField } from ".";
import { appendInParentheses } from "./helpers"; import { appendInParentheses } from "./helpers";
import { wrapCurrent } from "./wrap";
import { paperclipIcon, micIcon, functionIcon, xmlIcon } from "./icons"; import { paperclipIcon, micIcon, functionIcon, xmlIcon } from "./icons";
export let api = {}; export let api = {};
@ -29,38 +34,50 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
bridgeCommand("record"); bridgeCommand("record");
} }
function onHtmlEdit(): void { function onHtmlEdit() {
bridgeCommand("htmlEdit"); const currentField = getCurrentField();
if (currentField) {
currentField.toggleHtmlEdit();
}
} }
</script> </script>
<ButtonGroup {api}> <ButtonGroup {api}>
<ButtonGroupItem> <ButtonGroupItem>
<WithShortcut shortcut={"F3"} let:createShortcut let:shortcutLabel> <WithShortcut shortcut={"F3"} let:createShortcut let:shortcutLabel>
<IconButton <OnlyEditable let:disabled>
tooltip={appendInParentheses( <IconButton
tr.editingAttachPicturesaudiovideo(), tooltip={appendInParentheses(
shortcutLabel tr.editingAttachPicturesaudiovideo(),
)} shortcutLabel
iconSize={70} )}
on:click={onAttachment} iconSize={70}
on:mount={createShortcut} {disabled}
> on:click={onAttachment}
{@html paperclipIcon} on:mount={createShortcut}
</IconButton> >
{@html paperclipIcon}
</IconButton>
</OnlyEditable>
</WithShortcut> </WithShortcut>
</ButtonGroupItem> </ButtonGroupItem>
<ButtonGroupItem> <ButtonGroupItem>
<WithShortcut shortcut={"F5"} let:createShortcut let:shortcutLabel> <WithShortcut shortcut={"F5"} let:createShortcut let:shortcutLabel>
<IconButton <OnlyEditable let:disabled>
tooltip={appendInParentheses(tr.editingRecordAudio(), shortcutLabel)} <IconButton
iconSize={70} tooltip={appendInParentheses(
on:click={onRecord} tr.editingRecordAudio(),
on:mount={createShortcut} shortcutLabel
> )}
{@html micIcon} iconSize={70}
</IconButton> {disabled}
on:click={onRecord}
on:mount={createShortcut}
>
{@html micIcon}
</IconButton>
</OnlyEditable>
</WithShortcut> </WithShortcut>
</ButtonGroupItem> </ButtonGroupItem>
@ -70,9 +87,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<ButtonGroupItem> <ButtonGroupItem>
<WithDropdownMenu let:createDropdown let:menuId> <WithDropdownMenu let:createDropdown let:menuId>
<IconButton on:mount={createDropdown}> <WithContext key={disabledKey} let:context={disabled}>
{@html functionIcon} <IconButton {disabled} on:mount={createDropdown}>
</IconButton> {@html functionIcon}
</IconButton>
</WithContext>
<DropdownMenu id={menuId}> <DropdownMenu id={menuId}>
<WithShortcut <WithShortcut
@ -81,7 +100,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
let:shortcutLabel let:shortcutLabel
> >
<DropdownItem <DropdownItem
on:click={() => wrap("\\(", "\\)")} on:click={() => wrapCurrent("\\(", "\\)")}
on:mount={createShortcut} on:mount={createShortcut}
> >
{tr.editingMathjaxInline()} {tr.editingMathjaxInline()}
@ -95,7 +114,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
let:shortcutLabel let:shortcutLabel
> >
<DropdownItem <DropdownItem
on:click={() => wrap("\\[", "\\]")} on:click={() => wrapCurrent("\\[", "\\]")}
on:mount={createShortcut} on:mount={createShortcut}
> >
{tr.editingMathjaxBlock()} {tr.editingMathjaxBlock()}
@ -109,7 +128,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
let:shortcutLabel let:shortcutLabel
> >
<DropdownItem <DropdownItem
on:click={() => wrap("\\(\\ce{", "}\\)")} on:click={() => wrapCurrent("\\(\\ce{", "}\\)")}
on:mount={createShortcut} on:mount={createShortcut}
> >
{tr.editingMathjaxChemistry()} {tr.editingMathjaxChemistry()}
@ -123,7 +142,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
let:shortcutLabel let:shortcutLabel
> >
<DropdownItem <DropdownItem
on:click={() => wrap("[latex]", "[/latex]")} on:click={() => wrapCurrent("[latex]", "[/latex]")}
on:mount={createShortcut} on:mount={createShortcut}
> >
{tr.editingLatex()} {tr.editingLatex()}
@ -137,7 +156,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
let:shortcutLabel let:shortcutLabel
> >
<DropdownItem <DropdownItem
on:click={() => wrap("[$]", "[/$]")} on:click={() => wrapCurrent("[$]", "[/$]")}
on:mount={createShortcut} on:mount={createShortcut}
> >
{tr.editingLatexEquation()} {tr.editingLatexEquation()}
@ -151,7 +170,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
let:shortcutLabel let:shortcutLabel
> >
<DropdownItem <DropdownItem
on:click={() => wrap("[$$]", "[/$$]")} on:click={() => wrapCurrent("[$$]", "[/$$]")}
on:mount={createShortcut} on:mount={createShortcut}
> >
{tr.editingLatexMathEnv()} {tr.editingLatexMathEnv()}
@ -163,15 +182,28 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
</ButtonGroupItem> </ButtonGroupItem>
<ButtonGroupItem> <ButtonGroupItem>
<WithShortcut shortcut={"Control+Shift+X"} let:createShortcut let:shortcutLabel> <WithContext key={disabledKey} let:context={disabled}>
<IconButton <WithContext key={inCodableKey} let:context={inCodable}>
tooltip={appendInParentheses(tr.editingHtmlEditor(), shortcutLabel)} <WithShortcut
iconSize={70} shortcut={"Control+Shift+X"}
on:click={onHtmlEdit} let:createShortcut
on:mount={createShortcut} let:shortcutLabel
> >
{@html xmlIcon} <IconButton
</IconButton> tooltip={appendInParentheses(
</WithShortcut> tr.editingHtmlEditor(),
shortcutLabel
)}
iconSize={70}
active={!disabled && inCodable}
{disabled}
on:click={onHtmlEdit}
on:mount={createShortcut}
>
{@html xmlIcon}
</IconButton>
</WithShortcut>
</WithContext>
</WithContext>
</ButtonGroupItem> </ButtonGroupItem>
</ButtonGroup> </ButtonGroup>

View file

@ -41,6 +41,6 @@ export function saveNow(keepFocus: boolean): void {
saveField(currentField, "key"); saveField(currentField, "key");
} else { } else {
// triggers onBlur, which saves // triggers onBlur, which saves
currentField.blurEditable(); currentField.blur();
} }
} }

89
ts/editor/codable.ts Normal file
View file

@ -0,0 +1,89 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
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
View 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");

View file

@ -37,3 +37,16 @@ img.drawing {
filter: unquote("invert() hue-rotate(180deg)"); 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;
}

View file

@ -1,7 +1,10 @@
// 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 { 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 { function containsInlineContent(field: Element): boolean {
if (field.childNodes.length === 0) { if (field.childNodes.length === 0) {
@ -36,4 +39,32 @@ export class Editable extends HTMLElement {
connectedCallback(): void { connectedCallback(): void {
this.setAttribute("contenteditable", ""); 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();
}
} }

View file

@ -6,23 +6,20 @@
*/ */
import type { Editable } from "./editable"; import type { Editable } from "./editable";
import type { Codable } from "./codable";
import { updateActiveButtons } from "./toolbar"; import { updateActiveButtons } from "./toolbar";
import { bridgeCommand } from "./lib"; import { bridgeCommand } from "./lib";
import { onInput, onKey, onKeyUp } from "./inputHandlers"; import { onInput, onKey, onKeyUp } from "./inputHandlers";
import { onFocus, onBlur } from "./focusHandlers"; import { onFocus, onBlur } from "./focusHandlers";
function onPaste(evt: ClipboardEvent): void {
bridgeCommand("paste");
evt.preventDefault();
}
function onCutOrCopy(): void { function onCutOrCopy(): void {
bridgeCommand("cutOrCopy"); bridgeCommand("cutOrCopy");
} }
export class EditingArea extends HTMLDivElement { export class EditingArea extends HTMLDivElement {
editable: Editable; editable: Editable;
codable: Codable;
baseStyle: HTMLStyleElement; baseStyle: HTMLStyleElement;
constructor() { constructor() {
@ -41,6 +38,17 @@ export class EditingArea extends HTMLDivElement {
this.editable = document.createElement("anki-editable") as Editable; this.editable = document.createElement("anki-editable") as Editable;
this.shadowRoot!.appendChild(this.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 { get ord(): number {
@ -48,11 +56,11 @@ export class EditingArea extends HTMLDivElement {
} }
set fieldHTML(content: string) { set fieldHTML(content: string) {
this.editable.fieldHTML = content; this.activeInput.fieldHTML = content;
} }
get fieldHTML(): string { get fieldHTML(): string {
return this.editable.fieldHTML; return this.activeInput.fieldHTML;
} }
connectedCallback(): void { connectedCallback(): void {
@ -61,7 +69,7 @@ export class EditingArea extends HTMLDivElement {
this.addEventListener("input", onInput); this.addEventListener("input", onInput);
this.addEventListener("focus", onFocus); this.addEventListener("focus", onFocus);
this.addEventListener("blur", onBlur); this.addEventListener("blur", onBlur);
this.addEventListener("paste", onPaste); this.addEventListener("paste", this.onPaste);
this.addEventListener("copy", onCutOrCopy); this.addEventListener("copy", onCutOrCopy);
this.addEventListener("oncut", onCutOrCopy); this.addEventListener("oncut", onCutOrCopy);
this.addEventListener("mouseup", updateActiveButtons); this.addEventListener("mouseup", updateActiveButtons);
@ -76,7 +84,7 @@ export class EditingArea extends HTMLDivElement {
this.removeEventListener("input", onInput); this.removeEventListener("input", onInput);
this.removeEventListener("focus", onFocus); this.removeEventListener("focus", onFocus);
this.removeEventListener("blur", onBlur); this.removeEventListener("blur", onBlur);
this.removeEventListener("paste", onPaste); this.removeEventListener("paste", this.onPaste);
this.removeEventListener("copy", onCutOrCopy); this.removeEventListener("copy", onCutOrCopy);
this.removeEventListener("oncut", onCutOrCopy); this.removeEventListener("oncut", onCutOrCopy);
this.removeEventListener("mouseup", updateActiveButtons); this.removeEventListener("mouseup", updateActiveButtons);
@ -107,15 +115,65 @@ export class EditingArea extends HTMLDivElement {
return firstRule.style.direction === "rtl"; 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 { getSelection(): Selection {
return this.shadowRoot!.getSelection()!; return this.shadowRoot!.getSelection()!;
} }
focusEditable(): void { surroundSelection(before: string, after: string): void {
this.editable.focus(); 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 { blurEditable(): void {
this.editable.blur(); blur();
} }
} }

View file

@ -9,7 +9,7 @@ import { bridgeCommand } from "./lib";
export function onFocus(evt: FocusEvent): void { export function onFocus(evt: FocusEvent): void {
const currentField = evt.currentTarget as EditingArea; const currentField = evt.currentTarget as EditingArea;
currentField.focusEditable(); currentField.focus();
bridgeCommand(`focus:${currentField.ord}`); bridgeCommand(`focus:${currentField.ord}`);
enableButtons(); enableButtons();
} }

View file

@ -1,7 +1,9 @@
// 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 type { EditingArea } from "./editingArea"; /* eslint
@typescript-eslint/no-non-null-assertion: "off",
*/
export function nodeIsElement(node: Node): node is Element { export function nodeIsElement(node: Node): node is Element {
return node.nodeType === Node.ELEMENT_NODE; return node.nodeType === Node.ELEMENT_NODE;
@ -69,11 +71,11 @@ export function nodeIsInline(node: Node): boolean {
return !nodeIsElement(node) || INLINE_TAGS.includes(node.tagName); return !nodeIsElement(node) || INLINE_TAGS.includes(node.tagName);
} }
export function caretToEnd(currentField: EditingArea): void { export function caretToEnd(node: Node): void {
const range = document.createRange(); const range = document.createRange();
range.selectNodeContents(currentField.editable); range.selectNodeContents(node);
range.collapse(false); range.collapse(false);
const selection = currentField.getSelection(); const selection = (node.getRootNode() as Document | ShadowRoot).getSelection()!;
selection.removeAllRanges(); selection.removeAllRanges();
selection.addRange(range); selection.addRange(range);
} }

View file

@ -11,13 +11,13 @@ import { setupI18n, ModuleName } from "lib/i18n";
import "./fields.css"; import "./fields.css";
import { caretToEnd } from "./helpers";
import { saveField } from "./changeTimer"; import { saveField } from "./changeTimer";
import { EditorField } from "./editorField"; import { EditorField } from "./editorField";
import { LabelContainer } from "./labelContainer"; import { LabelContainer } from "./labelContainer";
import { EditingArea } from "./editingArea"; import { EditingArea } from "./editingArea";
import { Editable } from "./editable"; import { Editable } from "./editable";
import { Codable } from "./codable";
import { initToolbar } from "./toolbar"; import { initToolbar } from "./toolbar";
export { setNoteId, getNoteId } from "./noteId"; export { setNoteId, getNoteId } from "./noteId";
@ -35,6 +35,7 @@ declare global {
} }
customElements.define("anki-editable", Editable); customElements.define("anki-editable", Editable);
customElements.define("anki-codable", Codable, { extends: "textarea" });
customElements.define("anki-editing-area", EditingArea, { extends: "div" }); customElements.define("anki-editing-area", EditingArea, { extends: "div" });
customElements.define("anki-label-container", LabelContainer, { extends: "div" }); customElements.define("anki-label-container", LabelContainer, { extends: "div" });
customElements.define("anki-editor-field", EditorField, { extends: "div" }); customElements.define("anki-editor-field", EditorField, { extends: "div" });
@ -49,8 +50,8 @@ export function focusField(n: number): void {
const field = getEditorField(n); const field = getEditorField(n);
if (field) { if (field) {
field.editingArea.focusEditable(); field.editingArea.focus();
caretToEnd(field.editingArea); field.editingArea.caretToEnd();
updateActiveButtons(new Event("manualfocus")); updateActiveButtons(new Event("manualfocus"));
} }
} }
@ -60,7 +61,7 @@ export function focusIfField(x: number, y: number): boolean {
for (let i = 0; i < elements.length; i++) { for (let i = 0; i < elements.length; i++) {
const elem = elements[i] as EditingArea; const elem = elements[i] as EditingArea;
if (elem instanceof EditingArea) { if (elem instanceof EditingArea) {
elem.focusEditable(); elem.focus();
return true; return true;
} }
} }

View file

@ -7,7 +7,7 @@
import { updateActiveButtons } from "./toolbar"; import { updateActiveButtons } from "./toolbar";
import { EditingArea } from "./editingArea"; import { EditingArea } from "./editingArea";
import { caretToEnd, nodeIsElement, getBlockElement } from "./helpers"; import { nodeIsElement } from "./helpers";
import { triggerChangeTimer } from "./changeTimer"; import { triggerChangeTimer } from "./changeTimer";
import { registerShortcut } from "lib/shortcuts"; import { registerShortcut } from "lib/shortcuts";
@ -22,17 +22,12 @@ export function onKey(evt: KeyboardEvent): void {
// esc clears focus, allowing dialog to close // esc clears focus, allowing dialog to close
if (evt.code === "Escape") { if (evt.code === "Escape") {
currentField.blurEditable(); return currentField.blur();
return;
} }
// prefer <br> instead of <div></div> // prefer <br> instead of <div></div>
if ( if (evt.code === "Enter") {
evt.code === "Enter" && return currentField.onEnter(evt);
!getBlockElement(currentField.shadowRoot!) !== evt.shiftKey
) {
evt.preventDefault();
document.execCommand("insertLineBreak");
} }
// // fix Ctrl+right/left handling in RTL fields // // fix Ctrl+right/left handling in RTL fields
@ -59,7 +54,7 @@ export function onKey(evt: KeyboardEvent): void {
function updateFocus(evt: FocusEvent) { function updateFocus(evt: FocusEvent) {
const newFocusTarget = evt.target; const newFocusTarget = evt.target;
if (newFocusTarget instanceof EditingArea) { if (newFocusTarget instanceof EditingArea) {
caretToEnd(newFocusTarget); newFocusTarget.caretToEnd();
updateActiveButtons(evt); updateActiveButtons(evt);
} }
} }

View file

@ -7,12 +7,14 @@
*/ */
import { disabledKey, nightModeKey } from "components/contextKeys"; import { disabledKey, nightModeKey } from "components/contextKeys";
import { inCodableKey } from "./contextKeys";
import { writable } from "svelte/store"; import { writable } from "svelte/store";
import EditorToolbar from "./EditorToolbar.svelte"; import EditorToolbar from "./EditorToolbar.svelte";
import "./bootstrap.css"; import "./bootstrap.css";
const disabled = writable(false); const disabled = writable(false);
const inCodable = writable(false);
export function initToolbar(i18n: Promise<void>): Promise<EditorToolbar> { export function initToolbar(i18n: Promise<void>): Promise<EditorToolbar> {
let toolbarResolve: (value: EditorToolbar) => void; let toolbarResolve: (value: EditorToolbar) => void;
@ -27,6 +29,7 @@ export function initToolbar(i18n: Promise<void>): Promise<EditorToolbar> {
const context = new Map(); const context = new Map();
context.set(disabledKey, disabled); context.set(disabledKey, disabled);
context.set(inCodableKey, inCodable);
context.set( context.set(
nightModeKey, nightModeKey,
document.documentElement.classList.contains("night-mode") document.documentElement.classList.contains("night-mode")
@ -47,6 +50,14 @@ export function disableButtons(): void {
disabled.set(true); disabled.set(true);
} }
export function setCodableButtons(): void {
inCodable.set(true);
}
export function setEditableButtons(): void {
inCodable.set(false);
}
export { export {
updateActiveButtons, updateActiveButtons,
clearActiveButtons, clearActiveButtons,

View file

@ -45,6 +45,11 @@ export function wrap(front: string, back: string): void {
wrapInternal(front, back, false); wrapInternal(front, back, false);
} }
export function wrapCurrent(front: string, back: string): void {
const currentField = getCurrentField()!;
currentField.surroundSelection(front, back);
}
/* currently unused */ /* currently unused */
export function wrapIntoText(front: string, back: string): void { export function wrapIntoText(front: string, back: string): void {
wrapInternal(front, back, true); wrapInternal(front, back, true);

View file

@ -147,6 +147,15 @@
"path": "node_modules/bootstrap", "path": "node_modules/bootstrap",
"licenseFile": "node_modules/bootstrap/LICENSE" "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": { "commander@7.2.0": {
"licenses": "MIT", "licenses": "MIT",
"repository": "https://github.com/tj/commander.js", "repository": "https://github.com/tj/commander.js",

View file

@ -12,6 +12,7 @@
"@sqltools/formatter": "^1.2.2", "@sqltools/formatter": "^1.2.2",
"@tsconfig/svelte": "^1.0.10", "@tsconfig/svelte": "^1.0.10",
"@types/bootstrap": "^5.0.12", "@types/bootstrap": "^5.0.12",
"@types/codemirror": "^5.60.0",
"@types/d3": "^6.3.0", "@types/d3": "^6.3.0",
"@types/diff": "^5.0.0", "@types/diff": "^5.0.0",
"@types/jest": "^26.0.22", "@types/jest": "^26.0.22",
@ -60,6 +61,7 @@
"@types/marked": "^2.0.2", "@types/marked": "^2.0.2",
"bootstrap": "=5.0.0-beta3", "bootstrap": "=5.0.0-beta3",
"bootstrap-icons": "^1.4.0", "bootstrap-icons": "^1.4.0",
"codemirror": "^5.61.1",
"css-browser-selector": "^0.6.5", "css-browser-selector": "^0.6.5",
"d3": "^7.0.0", "d3": "^7.0.0",
"intl-pluralrules": "^1.2.2", "intl-pluralrules": "^1.2.2",

View file

@ -8,7 +8,7 @@ sass_library(
"bootstrap-dark.scss", "bootstrap-dark.scss",
], ],
visibility = ["//visibility:public"], visibility = ["//visibility:public"],
deps = ["//ts/sass/bootstrap"], deps = ["//ts/sass/bootstrap", "//ts/sass/codemirror"],
) )
sass_library( sass_library(

View 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"],
)

View file

@ -701,6 +701,13 @@
"@popperjs/core" "^2.9.2" "@popperjs/core" "^2.9.2"
"@types/jquery" "*" "@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@*": "@types/d3-array@*":
version "2.12.1" version "2.12.1"
resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-2.12.1.tgz#bee6857b812f1ecfd5e6832fd67f617b667dd024" 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" resolved "https://registry.yarnpkg.com/@types/diff/-/diff-5.0.0.tgz#eb71e94feae62548282c4889308a3dfb57e36020"
integrity sha512-jrm2K65CokCCX4NmowtA+MfXyuprZC13jbRuwprs6/04z/EcFg/MCwYdsHn+zgV4CQBiATiI7AEq7y1sZCtWKA== 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@*": "@types/geojson@*":
version "7946.0.7" version "7946.0.7"
resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.7.tgz#c8fa532b60a0042219cdf173ca21a975ef0666ad" 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" resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.0.tgz#7036640b4e21cc2f259ae826ce843d277dad8cff"
integrity sha512-RJJrrySY7A8havqpGObOB4W92QXKJo63/jFLLgpvOtsGUqbQZ9Sbgl35KMm1DjC6j7AvmmU2bIno+3IyEaemaw== 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@*": "@types/yargs-parser@*":
version "20.2.0" version "20.2.0"
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.0.tgz#dd3e6699ba3237f0348cd085e4698780204842f9" 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" resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
integrity sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ= 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: collect-v8-coverage@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59" resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59"