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
@ -7,7 +7,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import contextProperty from "../sveltelib/context-property";
|
||||
|
||||
export interface EditingInputAPI {
|
||||
export interface FocusableInputAPI {
|
||||
readonly name: string;
|
||||
focusable: boolean;
|
||||
/**
|
||||
@ -22,6 +22,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
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 {
|
||||
content: Writable<string>;
|
||||
editingInputs: Writable<EditingInputAPI[]>;
|
||||
@ -36,7 +46,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { setContext as svelteSetContext } from "svelte";
|
||||
import { setContext as svelteSetContext, tick } from "svelte";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
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
|
||||
// closing active field
|
||||
function trapFocusOnBlurOut(event: FocusEvent): void {
|
||||
if (!event.relatedTarget && editingInputs.every((input) => !input.focusable)) {
|
||||
// Prevents editor field being entirely deselected when
|
||||
// closing active field.
|
||||
async function trapFocusOnBlurOut(event: FocusEvent): Promise<void> {
|
||||
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();
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -324,8 +324,26 @@ the AddCards dialog) should be implemented in the user of this component.
|
||||
{#if cols[index] === "dupe"}
|
||||
<DuplicateLink />
|
||||
{/if}
|
||||
<RichTextBadge bind:off={richTextsHidden[index]} />
|
||||
<PlainTextBadge bind:off={plainTextsHidden[index]} />
|
||||
<RichTextBadge
|
||||
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} />
|
||||
</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
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { createEventDispatcher, onMount } from "svelte";
|
||||
|
||||
import Badge from "../components/Badge.svelte";
|
||||
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 keyCombination = "Control+Shift+X";
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
export let off = false;
|
||||
|
||||
$: icon = off ? htmlOff : htmlOn;
|
||||
|
||||
function toggle() {
|
||||
off = !off;
|
||||
dispatch("toggle");
|
||||
}
|
||||
|
||||
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
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
||||
import Badge from "../components/Badge.svelte";
|
||||
import * as tr from "../lib/ftl";
|
||||
import { richTextOff, richTextOn } from "./icons";
|
||||
|
||||
export let off: boolean;
|
||||
|
||||
function toggle(): void {
|
||||
off = !off;
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function toggle() {
|
||||
dispatch("toggle");
|
||||
}
|
||||
|
||||
$: 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 lifecycleHooks from "../../sveltelib/lifecycle-hooks";
|
||||
import type { CodeMirrorAPI } from "../CodeMirror.svelte";
|
||||
import type { EditingInputAPI } from "../EditingArea.svelte";
|
||||
import type { EditingInputAPI, FocusableInputAPI } from "../EditingArea.svelte";
|
||||
|
||||
export interface PlainTextInputAPI extends EditingInputAPI {
|
||||
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 { storedToUndecorated, undecoratedToStored } from "./transform";
|
||||
|
||||
export let hidden: boolean;
|
||||
|
||||
const configuration = {
|
||||
mode: htmlanki,
|
||||
...baseOptions,
|
||||
@ -46,7 +48,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
};
|
||||
|
||||
const { focusedInput } = noteEditorContext.get();
|
||||
|
||||
const { editingInputs, content } = editingAreaContext.get();
|
||||
const code = writable($content);
|
||||
|
||||
@ -70,13 +71,21 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
moveCaretToEnd();
|
||||
}
|
||||
|
||||
export let hidden = false;
|
||||
|
||||
function toggle(): boolean {
|
||||
hidden = !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 = {
|
||||
name: "plain-text",
|
||||
focus,
|
||||
@ -84,6 +93,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
moveCaretToEnd,
|
||||
refocus,
|
||||
toggle,
|
||||
getInputAPI,
|
||||
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 useInputHandler from "../../sveltelib/input-handler";
|
||||
import { pageTheme } from "../../sveltelib/theme";
|
||||
import type { EditingInputAPI } from "../EditingArea.svelte";
|
||||
import type { EditingInputAPI, FocusableInputAPI } from "../EditingArea.svelte";
|
||||
import type CustomStyles from "./CustomStyles.svelte";
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 = {
|
||||
name: "rich-text",
|
||||
element: richTextPromise,
|
||||
@ -99,6 +110,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
refocus,
|
||||
focusable: !hidden,
|
||||
toggle,
|
||||
getInputAPI,
|
||||
moveCaretToEnd,
|
||||
preventResubscription,
|
||||
inputHandler,
|
||||
@ -155,7 +167,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
let:stylesDidLoad
|
||||
>
|
||||
<div
|
||||
class="rich-text-editable"
|
||||
bind:this={richTextDiv}
|
||||
class={className}
|
||||
class:hidden
|
||||
class:night-mode={$pageTheme.isDark}
|
||||
use:attachShadow
|
||||
|
@ -346,7 +346,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
}
|
||||
|
||||
function selectAllTags() {
|
||||
tagTypes.forEach((tag) => (tag.selected = true));
|
||||
for (const tag of tagTypes) {
|
||||
tag.selected = true;
|
||||
}
|
||||
|
||||
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)}
|
||||
on:tagadd={() => insertTagKeepFocus(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:tagjoinnext={() => joinWithNextTag(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;
|
||||
}
|
||||
|
||||
async function onSelectAll(event: KeyboardEvent) {
|
||||
function onSelectAll(event: KeyboardEvent) {
|
||||
if (name.length === 0) {
|
||||
input.blur();
|
||||
await tick(); // ensure blur events are processed before tagselectall
|
||||
dispatch("tagselectall");
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
dispatch("tagselectall");
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user