mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
Move focus into HTML editor when shown (#1861)
* Move focus into input field, when input is shown * Change trapFocusOut to move focus into available inputs - This means that e.g. closing the HTML editor with focus in it will focus the visual editor in turn * Prevent Control+A unselecting tag editor when no tags exist
This commit is contained in:
parent
de2cc20c59
commit
52438fe4c9
8 changed files with 120 additions and 25 deletions
|
@ -7,7 +7,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
import contextProperty from "../sveltelib/context-property";
|
import contextProperty from "../sveltelib/context-property";
|
||||||
|
|
||||||
export interface EditingInputAPI {
|
export interface FocusableInputAPI {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
focusable: boolean;
|
focusable: boolean;
|
||||||
/**
|
/**
|
||||||
|
@ -22,6 +22,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
refocus(): void;
|
refocus(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface EditingInputAPI extends FocusableInputAPI {
|
||||||
|
/**
|
||||||
|
* Check whether blurred target belongs to an editing input.
|
||||||
|
* The editing area can then restore focus to this input.
|
||||||
|
*
|
||||||
|
* @returns An editing input api that is associated with the event target.
|
||||||
|
*/
|
||||||
|
getInputAPI(target: EventTarget): Promise<FocusableInputAPI | null>;
|
||||||
|
}
|
||||||
|
|
||||||
export interface EditingAreaAPI {
|
export interface EditingAreaAPI {
|
||||||
content: Writable<string>;
|
content: Writable<string>;
|
||||||
editingInputs: Writable<EditingInputAPI[]>;
|
editingInputs: Writable<EditingInputAPI[]>;
|
||||||
|
@ -36,7 +46,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { setContext as svelteSetContext } from "svelte";
|
import { setContext as svelteSetContext, tick } from "svelte";
|
||||||
import { writable } from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
import { fontFamilyKey, fontSizeKey } from "../lib/context-keys";
|
import { fontFamilyKey, fontSizeKey } from "../lib/context-keys";
|
||||||
|
@ -116,12 +126,39 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// prevents editor field being entirely deselected when
|
// Prevents editor field being entirely deselected when
|
||||||
// closing active field
|
// closing active field.
|
||||||
function trapFocusOnBlurOut(event: FocusEvent): void {
|
async function trapFocusOnBlurOut(event: FocusEvent): Promise<void> {
|
||||||
if (!event.relatedTarget && editingInputs.every((input) => !input.focusable)) {
|
if (event.relatedTarget) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const oldInputElement = event.target;
|
||||||
|
|
||||||
|
await tick();
|
||||||
|
|
||||||
|
let focusableInput: FocusableInputAPI | null = null;
|
||||||
|
|
||||||
|
const focusableInputs = editingInputs.filter(
|
||||||
|
(input: EditingInputAPI): boolean => input.focusable,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (oldInputElement) {
|
||||||
|
for (const input of focusableInputs) {
|
||||||
|
focusableInput = await input.getInputAPI(oldInputElement);
|
||||||
|
|
||||||
|
if (focusableInput) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (focusableInput || (focusableInput = focusableInputs[0])) {
|
||||||
|
focusableInput.focus();
|
||||||
|
} else {
|
||||||
focusTrap.focus();
|
focusTrap.focus();
|
||||||
event.preventDefault();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -324,8 +324,26 @@ the AddCards dialog) should be implemented in the user of this component.
|
||||||
{#if cols[index] === "dupe"}
|
{#if cols[index] === "dupe"}
|
||||||
<DuplicateLink />
|
<DuplicateLink />
|
||||||
{/if}
|
{/if}
|
||||||
<RichTextBadge bind:off={richTextsHidden[index]} />
|
<RichTextBadge
|
||||||
<PlainTextBadge bind:off={plainTextsHidden[index]} />
|
bind:off={richTextsHidden[index]}
|
||||||
|
on:toggle={() => {
|
||||||
|
richTextsHidden[index] = !richTextsHidden[index];
|
||||||
|
|
||||||
|
if (!richTextsHidden[index]) {
|
||||||
|
richTextInputs[index].api.refocus();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<PlainTextBadge
|
||||||
|
bind:off={plainTextsHidden[index]}
|
||||||
|
on:toggle={() => {
|
||||||
|
plainTextsHidden[index] = !plainTextsHidden[index];
|
||||||
|
|
||||||
|
if (!plainTextsHidden[index]) {
|
||||||
|
plainTextInputs[index].api.refocus();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<slot name="field-state" {field} {index} />
|
<slot name="field-state" {field} {index} />
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
|
|
|
@ -3,7 +3,7 @@ Copyright: Ankitects Pty Ltd and contributors
|
||||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { createEventDispatcher, onMount } from "svelte";
|
||||||
|
|
||||||
import Badge from "../components/Badge.svelte";
|
import Badge from "../components/Badge.svelte";
|
||||||
import * as tr from "../lib/ftl";
|
import * as tr from "../lib/ftl";
|
||||||
|
@ -13,13 +13,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
const editorField = editorFieldContext.get();
|
const editorField = editorFieldContext.get();
|
||||||
const keyCombination = "Control+Shift+X";
|
const keyCombination = "Control+Shift+X";
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
export let off = false;
|
export let off = false;
|
||||||
|
|
||||||
$: icon = off ? htmlOff : htmlOn;
|
$: icon = off ? htmlOff : htmlOn;
|
||||||
|
|
||||||
function toggle() {
|
function toggle() {
|
||||||
off = !off;
|
dispatch("toggle");
|
||||||
}
|
}
|
||||||
|
|
||||||
function shortcut(target: HTMLElement): () => void {
|
function shortcut(target: HTMLElement): () => void {
|
||||||
|
|
|
@ -3,14 +3,18 @@ Copyright: Ankitects Pty Ltd and contributors
|
||||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
import Badge from "../components/Badge.svelte";
|
import Badge from "../components/Badge.svelte";
|
||||||
import * as tr from "../lib/ftl";
|
import * as tr from "../lib/ftl";
|
||||||
import { richTextOff, richTextOn } from "./icons";
|
import { richTextOff, richTextOn } from "./icons";
|
||||||
|
|
||||||
export let off: boolean;
|
export let off: boolean;
|
||||||
|
|
||||||
function toggle(): void {
|
const dispatch = createEventDispatcher();
|
||||||
off = !off;
|
|
||||||
|
function toggle() {
|
||||||
|
dispatch("toggle");
|
||||||
}
|
}
|
||||||
|
|
||||||
$: icon = off ? richTextOff : richTextOn;
|
$: icon = off ? richTextOff : richTextOn;
|
||||||
|
|
|
@ -6,7 +6,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import { registerPackage } from "../../lib/runtime-require";
|
import { registerPackage } from "../../lib/runtime-require";
|
||||||
import lifecycleHooks from "../../sveltelib/lifecycle-hooks";
|
import lifecycleHooks from "../../sveltelib/lifecycle-hooks";
|
||||||
import type { CodeMirrorAPI } from "../CodeMirror.svelte";
|
import type { CodeMirrorAPI } from "../CodeMirror.svelte";
|
||||||
import type { EditingInputAPI } from "../EditingArea.svelte";
|
import type { EditingInputAPI, FocusableInputAPI } from "../EditingArea.svelte";
|
||||||
|
|
||||||
export interface PlainTextInputAPI extends EditingInputAPI {
|
export interface PlainTextInputAPI extends EditingInputAPI {
|
||||||
name: "plain-text";
|
name: "plain-text";
|
||||||
|
@ -39,6 +39,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import removeProhibitedTags from "./remove-prohibited";
|
import removeProhibitedTags from "./remove-prohibited";
|
||||||
import { storedToUndecorated, undecoratedToStored } from "./transform";
|
import { storedToUndecorated, undecoratedToStored } from "./transform";
|
||||||
|
|
||||||
|
export let hidden: boolean;
|
||||||
|
|
||||||
const configuration = {
|
const configuration = {
|
||||||
mode: htmlanki,
|
mode: htmlanki,
|
||||||
...baseOptions,
|
...baseOptions,
|
||||||
|
@ -46,7 +48,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
};
|
};
|
||||||
|
|
||||||
const { focusedInput } = noteEditorContext.get();
|
const { focusedInput } = noteEditorContext.get();
|
||||||
|
|
||||||
const { editingInputs, content } = editingAreaContext.get();
|
const { editingInputs, content } = editingAreaContext.get();
|
||||||
const code = writable($content);
|
const code = writable($content);
|
||||||
|
|
||||||
|
@ -70,13 +71,21 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
moveCaretToEnd();
|
moveCaretToEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
export let hidden = false;
|
|
||||||
|
|
||||||
function toggle(): boolean {
|
function toggle(): boolean {
|
||||||
hidden = !hidden;
|
hidden = !hidden;
|
||||||
return hidden;
|
return hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getInputAPI(target: EventTarget): Promise<FocusableInputAPI | null> {
|
||||||
|
const editor = (await codeMirror.editor) as any;
|
||||||
|
|
||||||
|
if (target === editor.display.input.textarea) {
|
||||||
|
return api;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export const api: PlainTextInputAPI = {
|
export const api: PlainTextInputAPI = {
|
||||||
name: "plain-text",
|
name: "plain-text",
|
||||||
focus,
|
focus,
|
||||||
|
@ -84,6 +93,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
moveCaretToEnd,
|
moveCaretToEnd,
|
||||||
refocus,
|
refocus,
|
||||||
toggle,
|
toggle,
|
||||||
|
getInputAPI,
|
||||||
codeMirror,
|
codeMirror,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -10,7 +10,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import type { InputHandlerAPI } from "../../sveltelib/input-handler";
|
import type { InputHandlerAPI } from "../../sveltelib/input-handler";
|
||||||
import useInputHandler from "../../sveltelib/input-handler";
|
import useInputHandler from "../../sveltelib/input-handler";
|
||||||
import { pageTheme } from "../../sveltelib/theme";
|
import { pageTheme } from "../../sveltelib/theme";
|
||||||
import type { EditingInputAPI } from "../EditingArea.svelte";
|
import type { EditingInputAPI, FocusableInputAPI } from "../EditingArea.svelte";
|
||||||
import type CustomStyles from "./CustomStyles.svelte";
|
import type CustomStyles from "./CustomStyles.svelte";
|
||||||
|
|
||||||
export interface RichTextInputAPI extends EditingInputAPI {
|
export interface RichTextInputAPI extends EditingInputAPI {
|
||||||
|
@ -92,6 +92,17 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
return hidden;
|
return hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const className = "rich-text-editable";
|
||||||
|
let richTextDiv: HTMLElement;
|
||||||
|
|
||||||
|
async function getInputAPI(target: EventTarget): Promise<FocusableInputAPI | null> {
|
||||||
|
if (target === richTextDiv) {
|
||||||
|
return api;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
export const api: RichTextInputAPI = {
|
export const api: RichTextInputAPI = {
|
||||||
name: "rich-text",
|
name: "rich-text",
|
||||||
element: richTextPromise,
|
element: richTextPromise,
|
||||||
|
@ -99,6 +110,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
refocus,
|
refocus,
|
||||||
focusable: !hidden,
|
focusable: !hidden,
|
||||||
toggle,
|
toggle,
|
||||||
|
getInputAPI,
|
||||||
moveCaretToEnd,
|
moveCaretToEnd,
|
||||||
preventResubscription,
|
preventResubscription,
|
||||||
inputHandler,
|
inputHandler,
|
||||||
|
@ -155,7 +167,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
let:stylesDidLoad
|
let:stylesDidLoad
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="rich-text-editable"
|
bind:this={richTextDiv}
|
||||||
|
class={className}
|
||||||
class:hidden
|
class:hidden
|
||||||
class:night-mode={$pageTheme.isDark}
|
class:night-mode={$pageTheme.isDark}
|
||||||
use:attachShadow
|
use:attachShadow
|
||||||
|
|
|
@ -346,7 +346,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectAllTags() {
|
function selectAllTags() {
|
||||||
tagTypes.forEach((tag) => (tag.selected = true));
|
for (const tag of tagTypes) {
|
||||||
|
tag.selected = true;
|
||||||
|
}
|
||||||
|
|
||||||
tagTypes = tagTypes;
|
tagTypes = tagTypes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -451,7 +454,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
splitTag(index, detail.start, detail.end)}
|
splitTag(index, detail.start, detail.end)}
|
||||||
on:tagadd={() => insertTagKeepFocus(index)}
|
on:tagadd={() => insertTagKeepFocus(index)}
|
||||||
on:tagdelete={() => deleteTagAt(index)}
|
on:tagdelete={() => deleteTagAt(index)}
|
||||||
on:tagselectall={selectAllTags}
|
on:tagselectall={async () => {
|
||||||
|
if (tagTypes.length <= 1) {
|
||||||
|
// Noop if no other tags exist
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
activeInput.blur();
|
||||||
|
// Ensure blur events are processed first
|
||||||
|
await tick();
|
||||||
|
|
||||||
|
selectAllTags();
|
||||||
|
}}
|
||||||
on:tagjoinprevious={() => joinWithPreviousTag(index)}
|
on:tagjoinprevious={() => joinWithPreviousTag(index)}
|
||||||
on:tagjoinnext={() => joinWithNextTag(index)}
|
on:tagjoinnext={() => joinWithNextTag(index)}
|
||||||
on:tagmoveprevious={() => moveToPreviousTag(index)}
|
on:tagmoveprevious={() => moveToPreviousTag(index)}
|
||||||
|
|
|
@ -231,13 +231,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
name = last;
|
name = last;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onSelectAll(event: KeyboardEvent) {
|
function onSelectAll(event: KeyboardEvent) {
|
||||||
if (name.length === 0) {
|
if (name.length === 0) {
|
||||||
input.blur();
|
|
||||||
await tick(); // ensure blur events are processed before tagselectall
|
|
||||||
dispatch("tagselectall");
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
dispatch("tagselectall");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue