diff --git a/ftl/core/editing.ftl b/ftl/core/editing.ftl index 89e876b98..e6fcc47cc 100644 --- a/ftl/core/editing.ftl +++ b/ftl/core/editing.ftl @@ -1,3 +1,4 @@ +editing-actual-size = Toggle actual size editing-add-media = Add Media editing-align-left = Align left editing-align-right = Align right @@ -15,6 +16,9 @@ editing-cut = Cut editing-edit-current = Edit Current editing-edit-html = Edit HTML editing-fields = Fields +editing-float-left = Float left +editing-float-right = Float right +editing-float-none = No float editing-html-editor = HTML Editor editing-indent = Increase indent editing-italic-text = Italic text diff --git a/ts/components/ButtonGroup.svelte b/ts/components/ButtonGroup.svelte index 6e2ef5989..6a5be8357 100644 --- a/ts/components/ButtonGroup.svelte +++ b/ts/components/ButtonGroup.svelte @@ -19,6 +19,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html export { className as class }; export let size: number | undefined = undefined; + export let wrap: boolean | undefined = undefined; $: buttonSize = size ? `--buttons-size: ${size}rem; ` : ""; @@ -45,9 +46,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html if ($items.length === 1) { return ButtonPosition.Standalone; } else if (index === 0) { - return ButtonPosition.Leftmost; + return ButtonPosition.InlineStart; } else if (index === $items.length - 1) { - return ButtonPosition.Rightmost; + return ButtonPosition.InlineEnd; } else { return ButtonPosition.Center; } @@ -95,7 +96,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
div { + flex-direction: row; flex-wrap: var(--buttons-wrap); padding: calc(var(--buttons-size) / 10); margin: 0; diff --git a/ts/components/ButtonGroupItem.svelte b/ts/components/ButtonGroupItem.svelte index 50a9938bf..79d214469 100644 --- a/ts/components/ButtonGroupItem.svelte +++ b/ts/components/ButtonGroupItem.svelte @@ -20,21 +20,24 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html let position_: ButtonPosition; let style: string; - const radius = "calc(var(--buttons-size) / 7.5)"; + const radius = "5px"; + + const leftStyle = `--border-left-radius: ${radius}; --border-right-radius: 0; `; + const rightStyle = `--border-left-radius: 0; --border-right-radius: ${radius}; `; $: { switch (position_) { case ButtonPosition.Standalone: style = `--border-left-radius: ${radius}; --border-right-radius: ${radius}; `; break; - case ButtonPosition.Leftmost: - style = `--border-left-radius: ${radius}; --border-right-radius: 0; `; + case ButtonPosition.InlineStart: + style = leftStyle; break; case ButtonPosition.Center: style = "--border-left-radius: 0; --border-right-radius: 0; "; break; - case ButtonPosition.Rightmost: - style = `--border-left-radius: 0; --border-right-radius: ${radius}; `; + case ButtonPosition.InlineEnd: + style = rightStyle; break; } } diff --git a/ts/components/IconButton.svelte b/ts/components/IconButton.svelte index e23a4d5d3..2f9d7b61a 100644 --- a/ts/components/IconButton.svelte +++ b/ts/components/IconButton.svelte @@ -18,6 +18,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html export let iconSize: number = 75; export let widthMultiplier: number = 1; + export let flipX: boolean = false; let buttonRef: HTMLButtonElement; @@ -44,7 +45,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html on:click on:mousedown|preventDefault > - + + + diff --git a/ts/editor/HandleControl.svelte b/ts/editor/HandleControl.svelte new file mode 100644 index 000000000..6e8f0e429 --- /dev/null +++ b/ts/editor/HandleControl.svelte @@ -0,0 +1,139 @@ + + + +
+
+
+
+
+
+
+ + diff --git a/ts/editor/HandleLabel.svelte b/ts/editor/HandleLabel.svelte new file mode 100644 index 000000000..d4e36b16f --- /dev/null +++ b/ts/editor/HandleLabel.svelte @@ -0,0 +1,69 @@ + + + +
+ +
+ + diff --git a/ts/editor/HandleSelection.svelte b/ts/editor/HandleSelection.svelte new file mode 100644 index 000000000..e0ef0f46e --- /dev/null +++ b/ts/editor/HandleSelection.svelte @@ -0,0 +1,56 @@ + + + +
+ /* prevent triggering Bootstrap dropdown */ event.stopImmediatePropagation()} + style="--left: {left}px; --top: {top}px; --width: {width}px; --height: {height}px; --offsetX: {offsetX}px; --offsetY: {offsetY}px;" +> + +
+ + diff --git a/ts/editor/ImageHandle.svelte b/ts/editor/ImageHandle.svelte new file mode 100644 index 000000000..1fdee5374 --- /dev/null +++ b/ts/editor/ImageHandle.svelte @@ -0,0 +1,210 @@ + + + +{#if sheet} + + { + updateSizesWithDimensions(); + dropdownObject.update(); + }} + let:toggleActualSize + let:active + > + {#if activeImage} + createDropdown(event.detail.selection)} + > + + + + {actualWidth}×{actualHeight} + {#if customDimensions} + (Original: {naturalWidth}×{naturalHeight}) + {/if} + + + { + if (active) { + setPointerCapture(event); + } + }} + on:pointermove={(event) => { + resize(event); + updateSizesWithDimensions(); + dropdownObject.update(); + }} + /> + + +
+ + + + + + +
+
+ {/if} +
+
+{/if} diff --git a/ts/editor/ImageHandleFloatButtons.svelte b/ts/editor/ImageHandleFloatButtons.svelte new file mode 100644 index 000000000..2d221f0a6 --- /dev/null +++ b/ts/editor/ImageHandleFloatButtons.svelte @@ -0,0 +1,61 @@ + + + + + + { + image.style.float = "left"; + setTimeout(() => dispatch("update")); + }}>{@html inlineStartIcon} + + + + { + image.style.float = ""; + setTimeout(() => dispatch("update")); + }}>{@html floatNoneIcon} + + + + { + image.style.float = "right"; + setTimeout(() => dispatch("update")); + }}>{@html inlineEndIcon} + + diff --git a/ts/editor/ImageHandleSizeSelect.svelte b/ts/editor/ImageHandleSizeSelect.svelte new file mode 100644 index 000000000..bed0d71b2 --- /dev/null +++ b/ts/editor/ImageHandleSizeSelect.svelte @@ -0,0 +1,26 @@ + + + + + + {@html icon} + + diff --git a/ts/editor/TemplateButtons.svelte b/ts/editor/TemplateButtons.svelte index cb224de5b..b9766e0c9 100644 --- a/ts/editor/TemplateButtons.svelte +++ b/ts/editor/TemplateButtons.svelte @@ -18,8 +18,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html import OnlyEditable from "./OnlyEditable.svelte"; import ClozeButton from "./ClozeButton.svelte"; - import { getCurrentField } from "."; - import { appendInParentheses } from "./helpers"; + import { getCurrentField, appendInParentheses } from "./helpers"; import { wrapCurrent } from "./wrap"; import { paperclipIcon, micIcon, functionIcon, xmlIcon } from "./icons"; diff --git a/ts/editor/WithImageConstrained.svelte b/ts/editor/WithImageConstrained.svelte new file mode 100644 index 000000000..a9e303b2d --- /dev/null +++ b/ts/editor/WithImageConstrained.svelte @@ -0,0 +1,199 @@ + + + +{#if activeImage} + +{/if} diff --git a/ts/editor/change-timer.ts b/ts/editor/change-timer.ts index fb580fb84..033e9f9ed 100644 --- a/ts/editor/change-timer.ts +++ b/ts/editor/change-timer.ts @@ -3,7 +3,7 @@ import type { EditingArea } from "./editing-area"; -import { getCurrentField } from "."; +import { getCurrentField } from "./helpers"; import { bridgeCommand } from "./lib"; import { getNoteId } from "./note-id"; @@ -23,9 +23,10 @@ function clearChangeTimer(): void { export function saveField(currentField: EditingArea, type: "blur" | "key"): void { clearChangeTimer(); - bridgeCommand( - `${type}:${currentField.ord}:${getNoteId()}:${currentField.fieldHTML}` - ); + const command = `${type}:${currentField.ord}:${getNoteId()}:${ + currentField.fieldHTML + }`; + bridgeCommand(command); } export function saveNow(keepFocus: boolean): void { diff --git a/ts/editor/editable-container.ts b/ts/editor/editable-container.ts new file mode 100644 index 000000000..3448e662e --- /dev/null +++ b/ts/editor/editable-container.ts @@ -0,0 +1,59 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +/* eslint +@typescript-eslint/no-non-null-assertion: "off", + */ + +export class EditableContainer extends HTMLDivElement { + baseStyle: HTMLStyleElement; + baseRule?: CSSStyleRule; + imageStyle?: HTMLStyleElement; + + constructor() { + super(); + const shadow = this.attachShadow({ mode: "open" }); + + if (document.documentElement.classList.contains("night-mode")) { + this.classList.add("night-mode"); + } + + const rootStyle = document.createElement("link"); + rootStyle.setAttribute("rel", "stylesheet"); + rootStyle.setAttribute("href", "./_anki/css/editable.css"); + shadow.appendChild(rootStyle); + + this.baseStyle = document.createElement("style"); + this.baseStyle.setAttribute("rel", "stylesheet"); + this.baseStyle.id = "baseStyle"; + shadow.appendChild(this.baseStyle); + } + + connectedCallback(): void { + const sheet = this.baseStyle.sheet as CSSStyleSheet; + const baseIndex = sheet.insertRule("anki-editable {}"); + this.baseRule = sheet.cssRules[baseIndex] as CSSStyleRule; + } + + initialize(color: string): void { + this.setBaseColor(color); + } + + setBaseColor(color: string): void { + if (this.baseRule) { + this.baseRule.style.color = color; + } + } + + setBaseStyling(fontFamily: string, fontSize: string, direction: string): void { + if (this.baseRule) { + this.baseRule.style.fontFamily = fontFamily; + this.baseRule.style.fontSize = fontSize; + this.baseRule.style.direction = direction; + } + } + + isRightToLeft(): boolean { + return this.baseRule!.style.direction === "rtl"; + } +} diff --git a/ts/editor/editable.scss b/ts/editor/editable.scss index a13e719e6..360c9d83c 100644 --- a/ts/editor/editable.scss +++ b/ts/editor/editable.scss @@ -6,6 +6,10 @@ anki-editable { overflow: auto; padding: 6px; + &:focus { + outline: none; + } + * { max-width: 100%; } diff --git a/ts/editor/editing-area.ts b/ts/editor/editing-area.ts index 2309a2e1e..c1578d76f 100644 --- a/ts/editor/editing-area.ts +++ b/ts/editor/editing-area.ts @@ -3,8 +3,12 @@ /* eslint @typescript-eslint/no-non-null-assertion: "off", +@typescript-eslint/no-explicit-any: "off", */ +import ImageHandle from "./ImageHandle.svelte"; + +import type { EditableContainer } from "./editable-container"; import type { Editable } from "./editable"; import type { Codable } from "./codable"; @@ -12,43 +16,71 @@ import { updateActiveButtons } from "./toolbar"; import { bridgeCommand } from "./lib"; import { onInput, onKey, onKeyUp } from "./input-handlers"; import { onFocus, onBlur } from "./focus-handlers"; +import { nightModeKey } from "components/context-keys"; function onCutOrCopy(): void { bridgeCommand("cutOrCopy"); } export class EditingArea extends HTMLDivElement { + imageHandle: Promise; + editableContainer: EditableContainer; editable: Editable; codable: Codable; - baseStyle: HTMLStyleElement; constructor() { super(); - this.attachShadow({ mode: "open" }); this.className = "field"; - if (document.documentElement.classList.contains("night-mode")) { - this.classList.add("night-mode"); - } + this.editableContainer = document.createElement("div", { + is: "anki-editable-container", + }) as EditableContainer; - const rootStyle = document.createElement("link"); - rootStyle.setAttribute("rel", "stylesheet"); - rootStyle.setAttribute("href", "./_anki/css/editable.css"); - this.shadowRoot!.appendChild(rootStyle); - - this.baseStyle = document.createElement("style"); - this.baseStyle.setAttribute("rel", "stylesheet"); - this.shadowRoot!.appendChild(this.baseStyle); + const imageStyle = document.createElement("style"); + imageStyle.setAttribute("rel", "stylesheet"); + imageStyle.id = "imageHandleStyle"; this.editable = document.createElement("anki-editable") as Editable; - this.shadowRoot!.appendChild(this.editable); + + const context = new Map(); + context.set( + nightModeKey, + document.documentElement.classList.contains("night-mode") + ); + + let imageHandleResolve: (value: ImageHandle) => void; + this.imageHandle = new Promise((resolve) => { + imageHandleResolve = resolve; + }); + + imageStyle.addEventListener("load", () => + imageHandleResolve( + new ImageHandle({ + target: this, + anchor: this.editableContainer, + props: { + container: this.editable, + sheet: imageStyle.sheet, + }, + context, + } as any) + ) + ); + + this.editableContainer.shadowRoot!.appendChild(imageStyle); + this.editableContainer.shadowRoot!.appendChild(this.editable); + this.appendChild(this.editableContainer); this.codable = document.createElement("textarea", { is: "anki-codable", }) as Codable; - this.shadowRoot!.appendChild(this.codable); + this.appendChild(this.codable); + this.onFocus = this.onFocus.bind(this); + this.onBlur = this.onBlur.bind(this); + this.onKey = this.onKey.bind(this); this.onPaste = this.onPaste.bind(this); + this.showImageHandle = this.showImageHandle.bind(this); } get activeInput(): Editable | Codable { @@ -60,7 +92,7 @@ export class EditingArea extends HTMLDivElement { } set fieldHTML(content: string) { - this.activeInput.fieldHTML = content; + this.imageHandle.then(() => (this.activeInput.fieldHTML = content)); } get fieldHTML(): string { @@ -68,30 +100,29 @@ export class EditingArea extends HTMLDivElement { } connectedCallback(): void { - this.addEventListener("keydown", onKey); + this.addEventListener("keydown", this.onKey); this.addEventListener("keyup", onKeyUp); this.addEventListener("input", onInput); - this.addEventListener("focus", onFocus); - this.addEventListener("blur", onBlur); + this.addEventListener("focusin", this.onFocus); + this.addEventListener("focusout", this.onBlur); this.addEventListener("paste", this.onPaste); this.addEventListener("copy", onCutOrCopy); this.addEventListener("oncut", onCutOrCopy); this.addEventListener("mouseup", updateActiveButtons); - - const baseStyleSheet = this.baseStyle.sheet as CSSStyleSheet; - baseStyleSheet.insertRule("anki-editable {}", 0); + this.editable.addEventListener("click", this.showImageHandle); } disconnectedCallback(): void { - this.removeEventListener("keydown", onKey); + this.removeEventListener("keydown", this.onKey); this.removeEventListener("keyup", onKeyUp); this.removeEventListener("input", onInput); - this.removeEventListener("focus", onFocus); - this.removeEventListener("blur", onBlur); + this.removeEventListener("focusin", this.onFocus); + this.removeEventListener("focusout", this.onBlur); this.removeEventListener("paste", this.onPaste); this.removeEventListener("copy", onCutOrCopy); this.removeEventListener("oncut", onCutOrCopy); this.removeEventListener("mouseup", updateActiveButtons); + this.editable.removeEventListener("click", this.showImageHandle); } initialize(color: string, content: string): void { @@ -100,9 +131,7 @@ export class EditingArea extends HTMLDivElement { } setBaseColor(color: string): void { - const styleSheet = this.baseStyle.sheet as CSSStyleSheet; - const firstRule = styleSheet.cssRules[0] as CSSStyleRule; - firstRule.style.color = color; + this.editableContainer.setBaseColor(color); } quoteFontFamily(fontFamily: string): string { @@ -114,17 +143,15 @@ export class EditingArea extends HTMLDivElement { } setBaseStyling(fontFamily: string, fontSize: string, direction: string): void { - const styleSheet = this.baseStyle.sheet as CSSStyleSheet; - const firstRule = styleSheet.cssRules[0] as CSSStyleRule; - firstRule.style.fontFamily = this.quoteFontFamily(fontFamily); - firstRule.style.fontSize = fontSize; - firstRule.style.direction = direction; + this.editableContainer.setBaseStyling( + this.quoteFontFamily(fontFamily), + fontSize, + direction + ); } isRightToLeft(): boolean { - const styleSheet = this.baseStyle.sheet as CSSStyleSheet; - const firstRule = styleSheet.cssRules[0] as CSSStyleRule; - return firstRule.style.direction === "rtl"; + return this.editableContainer.isRightToLeft(); } focus(): void { @@ -140,25 +167,62 @@ export class EditingArea extends HTMLDivElement { } hasFocus(): boolean { - return document.activeElement === this; + return document.activeElement?.closest(".field") === this; } getSelection(): Selection { - return this.shadowRoot!.getSelection()!; + const root = this.activeInput.getRootNode() as Document | ShadowRoot; + return root.getSelection()!; } surroundSelection(before: string, after: string): void { this.activeInput.surroundSelection(before, after); } + onFocus(event: FocusEvent): void { + onFocus(event); + } + + onBlur(event: FocusEvent): void { + this.resetImageHandle(); + onBlur(event); + } + onEnter(event: KeyboardEvent): void { this.activeInput.onEnter(event); } + onKey(event: KeyboardEvent): void { + this.resetImageHandle(); + onKey(event); + } + onPaste(event: ClipboardEvent): void { + this.resetImageHandle(); this.activeInput.onPaste(event); } + resetImageHandle(): void { + this.imageHandle.then((imageHandle) => + (imageHandle as any).$set({ + activeImage: null, + }) + ); + } + + showImageHandle(event: MouseEvent): void { + if (event.target instanceof HTMLImageElement) { + this.imageHandle.then((imageHandle) => + (imageHandle as any).$set({ + activeImage: event.target, + isRtl: this.isRightToLeft(), + }) + ); + } else { + this.resetImageHandle(); + } + } + toggleHtmlEdit(): void { const hadFocus = this.hasFocus(); @@ -166,6 +230,7 @@ export class EditingArea extends HTMLDivElement { this.fieldHTML = this.codable.teardown(); this.editable.hidden = false; } else { + this.resetImageHandle(); this.editable.hidden = true; this.codable.setup(this.editable.fieldHTML); } diff --git a/ts/editor/editor-field.ts b/ts/editor/editor-field.ts index 0c1cca362..08b85df71 100644 --- a/ts/editor/editor-field.ts +++ b/ts/editor/editor-field.ts @@ -10,6 +10,8 @@ export class EditorField extends HTMLDivElement { constructor() { super(); + this.classList.add("editor-field"); + this.labelContainer = document.createElement("div", { is: "anki-label-container", }) as LabelContainer; @@ -19,6 +21,8 @@ export class EditorField extends HTMLDivElement { is: "anki-editing-area", }) as EditingArea; this.appendChild(this.editingArea); + + this.focusIfNotFocused = this.focusIfNotFocused.bind(this); } static get observedAttributes(): string[] { @@ -29,6 +33,21 @@ export class EditorField extends HTMLDivElement { this.setAttribute("ord", String(n)); } + focusIfNotFocused(): void { + if (!this.editingArea.hasFocus()) { + this.editingArea.focus(); + this.editingArea.caretToEnd(); + } + } + + connectedCallback(): void { + this.labelContainer.addEventListener("mousedown", this.focusIfNotFocused); + } + + disconnectedCallback(): void { + this.labelContainer.removeEventListener("mousedown", this.focusIfNotFocused); + } + attributeChangedCallback(name: string, _oldValue: string, newValue: string): void { switch (name) { case "ord": diff --git a/ts/editor/fields.scss b/ts/editor/fields.scss index 1d0e34920..727959aa8 100644 --- a/ts/editor/fields.scss +++ b/ts/editor/fields.scss @@ -10,13 +10,30 @@ #fields { display: flex; + overflow-x: hidden; flex-direction: column; - margin: 5px; + margin: 3px 0; +} + +.editor-field { + margin: 3px; + border-radius: 5px; + border: 1px solid var(--border-color); + + --border-color: var(--border); + + &:focus-within { + box-shadow: 0 0 0 3px var(--focus-shadow); + + --border-color: var(--focus-border); + } } .field { - border: 1px solid var(--border); + position: relative; + background: var(--frame-bg); + border-radius: 0 0 5px 5px; &.dupe { // this works around the background colour persisting in copy+paste @@ -26,8 +43,16 @@ } .fname { - vertical-align: middle; - padding: 0; + border-width: 0 0 1px; + border-style: dashed; + border-color: var(--border-color); + border-radius: 5px 5px 0 0; + + padding: 0px 6px; +} + +.fieldname { + user-select: none; } #dupes, @@ -62,3 +87,13 @@ opacity: 0.5; } } + +@import "ts/sass/codemirror/lib/codemirror"; +@import "ts/sass/codemirror/theme/monokai"; +@import "ts/sass/codemirror/addon/fold/foldgutter"; + +.CodeMirror { + height: auto; + border-radius: 0 0 5px 5px; + padding: 6px 0; +} diff --git a/ts/editor/focus-handlers.ts b/ts/editor/focus-handlers.ts index f15f9df50..11c317ead 100644 --- a/ts/editor/focus-handlers.ts +++ b/ts/editor/focus-handlers.ts @@ -10,15 +10,12 @@ import type { EditingArea } from "./editing-area"; import { saveField } from "./change-timer"; import { bridgeCommand } from "./lib"; +import { getCurrentField } from "./helpers"; export function onFocus(evt: FocusEvent): void { const currentField = evt.currentTarget as EditingArea; currentField.focus(); - - if (currentField.shadowRoot!.getSelection()!.anchorNode === null) { - // selection is not inside editable after focusing - currentField.caretToEnd(); - } + currentField.caretToEnd(); bridgeCommand(`focus:${currentField.ord}`); fieldFocused.set(true); @@ -26,7 +23,7 @@ export function onFocus(evt: FocusEvent): void { export function onBlur(evt: FocusEvent): void { const previousFocus = evt.currentTarget as EditingArea; - const currentFieldUnchanged = previousFocus === document.activeElement; + const currentFieldUnchanged = previousFocus === getCurrentField(); saveField(previousFocus, currentFieldUnchanged ? "key" : "blur"); fieldFocused.set(false); diff --git a/ts/editor/helpers.ts b/ts/editor/helpers.ts index 12a13657e..174260152 100644 --- a/ts/editor/helpers.ts +++ b/ts/editor/helpers.ts @@ -5,6 +5,12 @@ @typescript-eslint/no-non-null-assertion: "off", */ +import type { EditingArea } from "./editing-area"; + +export function getCurrentField(): EditingArea | null { + return document.activeElement?.closest(".field") ?? null; +} + export function nodeIsElement(node: Node): node is Element { return node.nodeType === Node.ELEMENT_NODE; } diff --git a/ts/editor/icons.ts b/ts/editor/icons.ts index 003cdcd01..00ae6776c 100644 --- a/ts/editor/icons.ts +++ b/ts/editor/icons.ts @@ -33,3 +33,11 @@ export { default as xmlIcon } from "./xml.svg"; export const arrowIcon = ''; + +// image handle +export { default as floatNoneIcon } from "./format-float-none.svg"; +export { default as floatLeftIcon } from "./format-float-left.svg"; +export { default as floatRightIcon } from "./format-float-right.svg"; + +export { default as sizeActual } from "./image-size-select-actual.svg"; +export { default as sizeMinimized } from "./image-size-select-large.svg"; diff --git a/ts/editor/index.ts b/ts/editor/index.ts index 09ec6d01f..bdb0c7f03 100644 --- a/ts/editor/index.ts +++ b/ts/editor/index.ts @@ -23,15 +23,18 @@ import { saveField } from "./change-timer"; import { EditorField } from "./editor-field"; import { LabelContainer } from "./label-container"; import { EditingArea } from "./editing-area"; +import { EditableContainer } from "./editable-container"; import { Editable } from "./editable"; import { Codable } from "./codable"; import { initToolbar, fieldFocused } from "./toolbar"; +import { getCurrentField } from "./helpers"; export { setNoteId, getNoteId } from "./note-id"; export { saveNow } from "./change-timer"; export { wrap, wrapIntoText } from "./wrap"; export { editorToolbar } from "./toolbar"; export { activateStickyShortcuts } from "./label-container"; +export { getCurrentField } from "./helpers"; export { components } from "./Components.svelte"; declare global { @@ -44,6 +47,7 @@ declare global { } customElements.define("anki-editable", Editable); +customElements.define("anki-editable-container", EditableContainer, { extends: "div" }); customElements.define("anki-codable", Codable, { extends: "textarea" }); customElements.define("anki-editing-area", EditingArea, { extends: "div" }); customElements.define("anki-label-container", LabelContainer, { extends: "div" }); @@ -53,12 +57,6 @@ if (isApplePlatform()) { registerShortcut(() => bridgeCommand("paste"), "Control+Shift+V"); } -export function getCurrentField(): EditingArea | null { - return document.activeElement instanceof EditingArea - ? document.activeElement - : null; -} - export function focusField(n: number): void { const field = getEditorField(n); diff --git a/ts/editor/label-container.ts b/ts/editor/label-container.ts index bc6816e54..2bbdeffdf 100644 --- a/ts/editor/label-container.ts +++ b/ts/editor/label-container.ts @@ -32,12 +32,14 @@ export function activateStickyShortcuts(): void { } export class LabelContainer extends HTMLDivElement { - sticky: HTMLSpanElement; label: HTMLSpanElement; + fieldState: HTMLSpanElement; + + sticky: HTMLSpanElement; constructor() { super(); - this.className = "d-flex justify-content-between"; + this.className = "fname d-flex justify-content-between"; i18n.then(() => { this.title = appendInParentheses(tr.editingToggleSticky(), "F9"); @@ -47,25 +49,36 @@ export class LabelContainer extends HTMLDivElement { this.label.className = "fieldname"; this.appendChild(this.label); + this.fieldState = document.createElement("span"); + this.fieldState.className = "field-state d-flex justify-content-between"; + this.appendChild(this.fieldState); + this.sticky = document.createElement("span"); - this.sticky.className = "icon pin-icon me-1"; + this.sticky.className = "icon pin-icon"; this.sticky.innerHTML = pinIcon; this.sticky.hidden = true; - this.appendChild(this.sticky); + this.fieldState.appendChild(this.sticky); this.setSticky = this.setSticky.bind(this); this.hoverIcon = this.hoverIcon.bind(this); this.removeHoverIcon = this.removeHoverIcon.bind(this); this.toggleSticky = this.toggleSticky.bind(this); + this.keepFocus = this.keepFocus.bind(this); + } + + keepFocus(event: Event): void { + event.preventDefault(); } connectedCallback(): void { + this.addEventListener("mousedown", this.keepFocus); this.sticky.addEventListener("click", this.toggleSticky); this.sticky.addEventListener("mouseenter", this.hoverIcon); this.sticky.addEventListener("mouseleave", this.removeHoverIcon); } disconnectedCallback(): void { + this.removeEventListener("mousedown", this.keepFocus); this.sticky.removeEventListener("click", this.toggleSticky); this.sticky.removeEventListener("mouseenter", this.hoverIcon); this.sticky.removeEventListener("mouseleave", this.removeHoverIcon); diff --git a/ts/editor/wrap.ts b/ts/editor/wrap.ts index 8b67d776b..f7c2114a6 100644 --- a/ts/editor/wrap.ts +++ b/ts/editor/wrap.ts @@ -5,7 +5,8 @@ @typescript-eslint/no-non-null-assertion: "off", */ -import { getCurrentField, setFormat } from "."; +import { getCurrentField } from "./helpers"; +import { setFormat } from "."; function wrappedExceptForWhitespace(text: string, front: string, back: string): string { const match = text.match(/^(\s*)([^]*?)(\s*)$/)!; diff --git a/ts/reviewer/reviewer.scss b/ts/reviewer/reviewer.scss index a39fca0fa..24f24a3cf 100644 --- a/ts/reviewer/reviewer.scss +++ b/ts/reviewer/reviewer.scss @@ -19,7 +19,7 @@ body.nightMode { } img { - max-width: 95%; + max-width: 100%; max-height: 95vh; } diff --git a/ts/sass/_vars.scss b/ts/sass/_vars.scss index ec8b2d1bb..bac3d4816 100644 --- a/ts/sass/_vars.scss +++ b/ts/sass/_vars.scss @@ -36,6 +36,8 @@ --suspended-bg: #ffffb2; --marked-bg: #cce; --tooltip-bg: #fcfcfc; + --focus-border: #0969da; + --focus-shadow: rgba(9 105 218 / 0.3); } :root[class*="night-mode"] { @@ -73,4 +75,6 @@ --suspended-bg: #aaaa33; --marked-bg: #77c; --tooltip-bg: #272727; + --focus-border: #316dca; + --focus-shadow: #143d79; } diff --git a/ts/sass/buttons.scss b/ts/sass/buttons.scss index c7b29f544..2246e9f6a 100644 --- a/ts/sass/buttons.scss +++ b/ts/sass/buttons.scss @@ -17,7 +17,7 @@ font-size: 14px; -webkit-appearance: none; - border-radius: 3px; + border-radius: 5px; padding: 5px; border: 1px solid var(--border); } @@ -37,7 +37,7 @@ box-shadow: 0 0 3px fusion-vars.$button-outline; border: 1px solid fusion-vars.$button-border; - border-radius: 2px; + border-radius: 5px; padding: 10px; padding-top: 3px; padding-bottom: 3px;