mirror of
https://github.com/ankitects/anki.git
synced 2025-09-22 16:02:23 -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
|
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
|
||||||
|
|
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">
|
<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;
|
||||||
|
|
|
@ -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",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
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 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>
|
||||||
|
|
|
@ -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>
|
||||||
|
|
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">
|
<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>
|
||||||
|
|
|
@ -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
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)");
|
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
|
// 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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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(
|
||||||
|
|
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"
|
"@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"
|
||||||
|
|
Loading…
Reference in a new issue