diff --git a/ts/components/IconButton.svelte b/ts/components/IconButton.svelte index 6763def8f..7c02c44fe 100644 --- a/ts/components/IconButton.svelte +++ b/ts/components/IconButton.svelte @@ -3,9 +3,8 @@ Copyright: Ankitects Pty Ltd and contributors License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html --> + + diff --git a/ts/components/WithState.svelte b/ts/components/WithState.svelte index 97d22ee4c..14657087f 100644 --- a/ts/components/WithState.svelte +++ b/ts/components/WithState.svelte @@ -5,14 +5,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - - {@html bracketsIcon} - + + + {@html bracketsIcon} + + diff --git a/ts/editor/ColorButtons.svelte b/ts/editor/ColorButtons.svelte index 388c000ed..eac0cad46 100644 --- a/ts/editor/ColorButtons.svelte +++ b/ts/editor/ColorButtons.svelte @@ -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 WithShortcut from "components/WithShortcut.svelte"; import WithColorHelper from "./WithColorHelper.svelte"; + import OnlyEditable from "./OnlyEditable.svelte"; import { textColorIcon, highlightColorIcon, arrowIcon } from "./icons"; import { appendInParentheses } from "./helpers"; @@ -28,51 +29,61 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - - - - {@html textColorIcon} - {@html colorHelperIcon} - - - + + + + + {@html textColorIcon} + {@html colorHelperIcon} + + + - - - - {@html arrowIcon} - - - - + + + + {@html arrowIcon} + + + + + - - - {@html highlightColorIcon} - {@html colorHelperIcon} - - + + + + {@html highlightColorIcon} + {@html colorHelperIcon} + + - - - {@html arrowIcon} - - - + + + {@html arrowIcon} + + + + diff --git a/ts/editor/CommandIconButton.svelte b/ts/editor/CommandIconButton.svelte new file mode 100644 index 000000000..4781d2ccc --- /dev/null +++ b/ts/editor/CommandIconButton.svelte @@ -0,0 +1,79 @@ + + + + + {#if withoutShortcut && withoutState} + document.execCommand(key)}> + + + {:else if withoutShortcut} + document.queryCommandState(key)} + let:state={active} + let:updateState + > + { + document.execCommand(key); + updateState(event); + }} + > + + + + {:else if withoutState} + + document.execCommand(key)} + on:mount={createShortcut} + > + + + + {:else} + + document.queryCommandState(key)} + let:state={active} + let:updateState + > + { + document.execCommand(key); + updateState(event); + }} + on:mount={createShortcut} + > + + + + + {/if} + diff --git a/ts/editor/FormatBlockButtons.svelte b/ts/editor/FormatBlockButtons.svelte index 358b02ad5..f1d4376ea 100644 --- a/ts/editor/FormatBlockButtons.svelte +++ b/ts/editor/FormatBlockButtons.svelte @@ -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 ButtonDropdown from "components/ButtonDropdown.svelte"; import ButtonToolbarItem from "components/ButtonToolbarItem.svelte"; - import WithState from "components/WithState.svelte"; import WithDropdownMenu from "components/WithDropdownMenu.svelte"; + import OnlyEditable from "./OnlyEditable.svelte"; + import CommandIconButton from "./CommandIconButton.svelte"; import { getListItem } from "./helpers"; import { @@ -46,134 +47,66 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - document.queryCommandState("insertUnorderedList")} - let:state={active} - let:updateState + tooltip={tr.editingUnorderedList()} + withoutShortcut>{@html ulIcon} - { - document.execCommand("insertUnorderedList"); - updateState(event); - }} - > - {@html ulIcon} - - - document.queryCommandState("insertOrderedList")} - let:state={active} - let:updateState + tooltip={tr.editingOrderedList()} + withoutShortcut>{@html olIcon} - { - document.execCommand("insertOrderedList"); - updateState(event); - }} - > - {@html olIcon} - - - - {@html listOptionsIcon} - + + + {@html listOptionsIcon} + + - document.queryCommandState("justifyLeft")} - let:state={active} - let:updateState + tooltip={tr.editingAlignLeft()} + withoutShortcut + >{@html justifyLeftIcon} - { - document.execCommand("justifyLeft"); - updateState(event); - }} - > - {@html justifyLeftIcon} - - - - document.queryCommandState("justifyCenter")} - let:state={active} - let:updateState + tooltip={tr.editingCenter()} + withoutShortcut + >{@html justifyCenterIcon} - { - document.execCommand("justifyCenter"); - updateState(event); - }} - > - {@html justifyCenterIcon} - - - - document.queryCommandState("justifyRight")} - let:state={active} - let:updateState + tooltip={tr.editingAlignRight()} + withoutShortcut + >{@html justifyRightIcon} - { - document.execCommand("justifyRight"); - updateState(event); - }} - > - {@html justifyRightIcon} - - - document.queryCommandState("justifyFull")} - let:state={active} - let:updateState + tooltip={tr.editingJustify()} + withoutShortcut + >{@html justifyFullIcon} - { - document.execCommand("justifyFull"); - updateState(event); - }} - > - {@html justifyFullIcon} - - @@ -181,21 +114,27 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - - {@html outdentIcon} - + + + {@html outdentIcon} + + - - {@html indentIcon} - + + + {@html indentIcon} + + diff --git a/ts/editor/FormatInlineButtons.svelte b/ts/editor/FormatInlineButtons.svelte index 156317a20..47032b74e 100644 --- a/ts/editor/FormatInlineButtons.svelte +++ b/ts/editor/FormatInlineButtons.svelte @@ -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 ButtonGroupItem from "components/ButtonGroupItem.svelte"; - import IconButton from "components/IconButton.svelte"; - import WithState from "components/WithState.svelte"; - import WithShortcut from "components/WithShortcut.svelte"; + import CommandIconButton from "./CommandIconButton.svelte"; import { boldIcon, @@ -19,147 +17,57 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html subscriptIcon, eraserIcon, } from "./icons"; - import { appendInParentheses } from "./helpers"; export let api = {}; - - document.queryCommandState("bold")} - let:state={active} - let:updateState - > - { - document.execCommand("bold"); - updateState(event); - }} - on:mount={createShortcut} - > - {@html boldIcon} - - - + {@html boldIcon} - - document.queryCommandState("italic")} - let:state={active} - let:updateState - > - { - document.execCommand("italic"); - updateState(event); - }} - on:mount={createShortcut} - > - {@html italicIcon} - - - + {@html italicIcon} - - document.queryCommandState("underline")} - let:state={active} - let:updateState - > - { - document.execCommand("underline"); - updateState(event); - }} - on:mount={createShortcut} - > - {@html underlineIcon} - - - + {@html underlineIcon} - - document.queryCommandState("superscript")} - let:state={active} - let:updateState - > - { - document.execCommand("superscript"); - updateState(event); - }} - on:mount={createShortcut} - > - {@html superscriptIcon} - - - + {@html superscriptIcon} - - document.queryCommandState("subscript")} - let:state={active} - let:updateState - > - { - document.execCommand("subscript"); - updateState(event); - }} - on:mount={createShortcut} - > - {@html subscriptIcon} - - - + {@html subscriptIcon} - - { - document.execCommand("removeFormat"); - }} - on:mount={createShortcut} - > - {@html eraserIcon} - - + {@html eraserIcon} diff --git a/ts/editor/OnlyEditable.svelte b/ts/editor/OnlyEditable.svelte new file mode 100644 index 000000000..c36bfa86d --- /dev/null +++ b/ts/editor/OnlyEditable.svelte @@ -0,0 +1,15 @@ + + + + + + + + diff --git a/ts/editor/TemplateButtons.svelte b/ts/editor/TemplateButtons.svelte index 3ee854e77..9de8256ba 100644 --- a/ts/editor/TemplateButtons.svelte +++ b/ts/editor/TemplateButtons.svelte @@ -5,6 +5,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - - {@html paperclipIcon} - + + + {@html paperclipIcon} + + - - {@html micIcon} - + + + {@html micIcon} + + @@ -70,9 +87,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - - {@html functionIcon} - + + + {@html functionIcon} + + wrap("\\(", "\\)")} + on:click={() => wrapCurrent("\\(", "\\)")} on:mount={createShortcut} > {tr.editingMathjaxInline()} @@ -95,7 +114,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html let:shortcutLabel > wrap("\\[", "\\]")} + on:click={() => wrapCurrent("\\[", "\\]")} on:mount={createShortcut} > {tr.editingMathjaxBlock()} @@ -109,7 +128,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html let:shortcutLabel > wrap("\\(\\ce{", "}\\)")} + on:click={() => wrapCurrent("\\(\\ce{", "}\\)")} on:mount={createShortcut} > {tr.editingMathjaxChemistry()} @@ -123,7 +142,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html let:shortcutLabel > wrap("[latex]", "[/latex]")} + on:click={() => wrapCurrent("[latex]", "[/latex]")} on:mount={createShortcut} > {tr.editingLatex()} @@ -137,7 +156,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html let:shortcutLabel > wrap("[$]", "[/$]")} + on:click={() => wrapCurrent("[$]", "[/$]")} on:mount={createShortcut} > {tr.editingLatexEquation()} @@ -151,7 +170,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html let:shortcutLabel > wrap("[$$]", "[/$$]")} + on:click={() => wrapCurrent("[$$]", "[/$$]")} on:mount={createShortcut} > {tr.editingLatexMathEnv()} @@ -163,15 +182,28 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html - - - {@html xmlIcon} - - + + + + + {@html xmlIcon} + + + + diff --git a/ts/editor/changeTimer.ts b/ts/editor/changeTimer.ts index 30e442e5d..577cc3c82 100644 --- a/ts/editor/changeTimer.ts +++ b/ts/editor/changeTimer.ts @@ -41,6 +41,6 @@ export function saveNow(keepFocus: boolean): void { saveField(currentField, "key"); } else { // triggers onBlur, which saves - currentField.blurEditable(); + currentField.blur(); } } diff --git a/ts/editor/codable.ts b/ts/editor/codable.ts new file mode 100644 index 000000000..568ef6aaf --- /dev/null +++ b/ts/editor/codable.ts @@ -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 */ + } +} diff --git a/ts/editor/contextKeys.ts b/ts/editor/contextKeys.ts new file mode 100644 index 000000000..552cb9188 --- /dev/null +++ b/ts/editor/contextKeys.ts @@ -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"); diff --git a/ts/editor/editable.scss b/ts/editor/editable.scss index 7489a48a4..c502f7793 100644 --- a/ts/editor/editable.scss +++ b/ts/editor/editable.scss @@ -37,3 +37,16 @@ img.drawing { 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; +} diff --git a/ts/editor/editable.ts b/ts/editor/editable.ts index 680bdf35c..03bad6d82 100644 --- a/ts/editor/editable.ts +++ b/ts/editor/editable.ts @@ -1,7 +1,10 @@ // Copyright: Ankitects Pty Ltd and contributors // 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 { if (field.childNodes.length === 0) { @@ -36,4 +39,32 @@ export class Editable extends HTMLElement { connectedCallback(): void { 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(); + } } diff --git a/ts/editor/editingArea.ts b/ts/editor/editingArea.ts index b5345a630..35ac59fe7 100644 --- a/ts/editor/editingArea.ts +++ b/ts/editor/editingArea.ts @@ -6,23 +6,20 @@ */ import type { Editable } from "./editable"; +import type { Codable } from "./codable"; import { updateActiveButtons } from "./toolbar"; import { bridgeCommand } from "./lib"; import { onInput, onKey, onKeyUp } from "./inputHandlers"; import { onFocus, onBlur } from "./focusHandlers"; -function onPaste(evt: ClipboardEvent): void { - bridgeCommand("paste"); - evt.preventDefault(); -} - function onCutOrCopy(): void { bridgeCommand("cutOrCopy"); } export class EditingArea extends HTMLDivElement { editable: Editable; + codable: Codable; baseStyle: HTMLStyleElement; constructor() { @@ -41,6 +38,17 @@ export class EditingArea extends HTMLDivElement { this.editable = document.createElement("anki-editable") as 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 { @@ -48,11 +56,11 @@ export class EditingArea extends HTMLDivElement { } set fieldHTML(content: string) { - this.editable.fieldHTML = content; + this.activeInput.fieldHTML = content; } get fieldHTML(): string { - return this.editable.fieldHTML; + return this.activeInput.fieldHTML; } connectedCallback(): void { @@ -61,7 +69,7 @@ export class EditingArea extends HTMLDivElement { this.addEventListener("input", onInput); this.addEventListener("focus", onFocus); this.addEventListener("blur", onBlur); - this.addEventListener("paste", onPaste); + this.addEventListener("paste", this.onPaste); this.addEventListener("copy", onCutOrCopy); this.addEventListener("oncut", onCutOrCopy); this.addEventListener("mouseup", updateActiveButtons); @@ -76,7 +84,7 @@ export class EditingArea extends HTMLDivElement { this.removeEventListener("input", onInput); this.removeEventListener("focus", onFocus); this.removeEventListener("blur", onBlur); - this.removeEventListener("paste", onPaste); + this.removeEventListener("paste", this.onPaste); this.removeEventListener("copy", onCutOrCopy); this.removeEventListener("oncut", onCutOrCopy); this.removeEventListener("mouseup", updateActiveButtons); @@ -107,15 +115,65 @@ export class EditingArea extends HTMLDivElement { 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 { return this.shadowRoot!.getSelection()!; } - focusEditable(): void { - this.editable.focus(); + surroundSelection(before: string, after: string): void { + 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 { - this.editable.blur(); + blur(); } } diff --git a/ts/editor/focusHandlers.ts b/ts/editor/focusHandlers.ts index 1c748f4fb..6d782cd9a 100644 --- a/ts/editor/focusHandlers.ts +++ b/ts/editor/focusHandlers.ts @@ -9,7 +9,7 @@ import { bridgeCommand } from "./lib"; export function onFocus(evt: FocusEvent): void { const currentField = evt.currentTarget as EditingArea; - currentField.focusEditable(); + currentField.focus(); bridgeCommand(`focus:${currentField.ord}`); enableButtons(); } diff --git a/ts/editor/helpers.ts b/ts/editor/helpers.ts index 871feb1c7..afee4a781 100644 --- a/ts/editor/helpers.ts +++ b/ts/editor/helpers.ts @@ -1,7 +1,9 @@ // Copyright: Ankitects Pty Ltd and contributors // 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 { return node.nodeType === Node.ELEMENT_NODE; @@ -69,11 +71,11 @@ export function nodeIsInline(node: Node): boolean { return !nodeIsElement(node) || INLINE_TAGS.includes(node.tagName); } -export function caretToEnd(currentField: EditingArea): void { +export function caretToEnd(node: Node): void { const range = document.createRange(); - range.selectNodeContents(currentField.editable); + range.selectNodeContents(node); range.collapse(false); - const selection = currentField.getSelection(); + const selection = (node.getRootNode() as Document | ShadowRoot).getSelection()!; selection.removeAllRanges(); selection.addRange(range); } diff --git a/ts/editor/index.ts b/ts/editor/index.ts index 6056047cc..a3325c550 100644 --- a/ts/editor/index.ts +++ b/ts/editor/index.ts @@ -11,13 +11,13 @@ import { setupI18n, ModuleName } from "lib/i18n"; import "./fields.css"; -import { caretToEnd } from "./helpers"; import { saveField } from "./changeTimer"; import { EditorField } from "./editorField"; import { LabelContainer } from "./labelContainer"; import { EditingArea } from "./editingArea"; import { Editable } from "./editable"; +import { Codable } from "./codable"; import { initToolbar } from "./toolbar"; export { setNoteId, getNoteId } from "./noteId"; @@ -35,6 +35,7 @@ declare global { } customElements.define("anki-editable", Editable); +customElements.define("anki-codable", Codable, { extends: "textarea" }); customElements.define("anki-editing-area", EditingArea, { extends: "div" }); customElements.define("anki-label-container", LabelContainer, { extends: "div" }); customElements.define("anki-editor-field", EditorField, { extends: "div" }); @@ -49,8 +50,8 @@ export function focusField(n: number): void { const field = getEditorField(n); if (field) { - field.editingArea.focusEditable(); - caretToEnd(field.editingArea); + field.editingArea.focus(); + field.editingArea.caretToEnd(); updateActiveButtons(new Event("manualfocus")); } } @@ -60,7 +61,7 @@ export function focusIfField(x: number, y: number): boolean { for (let i = 0; i < elements.length; i++) { const elem = elements[i] as EditingArea; if (elem instanceof EditingArea) { - elem.focusEditable(); + elem.focus(); return true; } } diff --git a/ts/editor/inputHandlers.ts b/ts/editor/inputHandlers.ts index f80011537..ae2c8c44f 100644 --- a/ts/editor/inputHandlers.ts +++ b/ts/editor/inputHandlers.ts @@ -7,7 +7,7 @@ import { updateActiveButtons } from "./toolbar"; import { EditingArea } from "./editingArea"; -import { caretToEnd, nodeIsElement, getBlockElement } from "./helpers"; +import { nodeIsElement } from "./helpers"; import { triggerChangeTimer } from "./changeTimer"; import { registerShortcut } from "lib/shortcuts"; @@ -22,17 +22,12 @@ export function onKey(evt: KeyboardEvent): void { // esc clears focus, allowing dialog to close if (evt.code === "Escape") { - currentField.blurEditable(); - return; + return currentField.blur(); } // prefer
instead of
- if ( - evt.code === "Enter" && - !getBlockElement(currentField.shadowRoot!) !== evt.shiftKey - ) { - evt.preventDefault(); - document.execCommand("insertLineBreak"); + if (evt.code === "Enter") { + return currentField.onEnter(evt); } // // fix Ctrl+right/left handling in RTL fields @@ -59,7 +54,7 @@ export function onKey(evt: KeyboardEvent): void { function updateFocus(evt: FocusEvent) { const newFocusTarget = evt.target; if (newFocusTarget instanceof EditingArea) { - caretToEnd(newFocusTarget); + newFocusTarget.caretToEnd(); updateActiveButtons(evt); } } diff --git a/ts/editor/toolbar.ts b/ts/editor/toolbar.ts index 856d69014..ac6c18b42 100644 --- a/ts/editor/toolbar.ts +++ b/ts/editor/toolbar.ts @@ -7,12 +7,14 @@ */ import { disabledKey, nightModeKey } from "components/contextKeys"; +import { inCodableKey } from "./contextKeys"; import { writable } from "svelte/store"; import EditorToolbar from "./EditorToolbar.svelte"; import "./bootstrap.css"; const disabled = writable(false); +const inCodable = writable(false); export function initToolbar(i18n: Promise): Promise { let toolbarResolve: (value: EditorToolbar) => void; @@ -27,6 +29,7 @@ export function initToolbar(i18n: Promise): Promise { const context = new Map(); context.set(disabledKey, disabled); + context.set(inCodableKey, inCodable); context.set( nightModeKey, document.documentElement.classList.contains("night-mode") @@ -47,6 +50,14 @@ export function disableButtons(): void { disabled.set(true); } +export function setCodableButtons(): void { + inCodable.set(true); +} + +export function setEditableButtons(): void { + inCodable.set(false); +} + export { updateActiveButtons, clearActiveButtons, diff --git a/ts/editor/wrap.ts b/ts/editor/wrap.ts index 4255d4d80..8b67d776b 100644 --- a/ts/editor/wrap.ts +++ b/ts/editor/wrap.ts @@ -45,6 +45,11 @@ export function wrap(front: string, back: string): void { wrapInternal(front, back, false); } +export function wrapCurrent(front: string, back: string): void { + const currentField = getCurrentField()!; + currentField.surroundSelection(front, back); +} + /* currently unused */ export function wrapIntoText(front: string, back: string): void { wrapInternal(front, back, true); diff --git a/ts/licenses.json b/ts/licenses.json index 54792d39a..971abdcc7 100644 --- a/ts/licenses.json +++ b/ts/licenses.json @@ -147,6 +147,15 @@ "path": "node_modules/bootstrap", "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": { "licenses": "MIT", "repository": "https://github.com/tj/commander.js", diff --git a/ts/package.json b/ts/package.json index 8aa61d415..580d5f772 100644 --- a/ts/package.json +++ b/ts/package.json @@ -12,6 +12,7 @@ "@sqltools/formatter": "^1.2.2", "@tsconfig/svelte": "^1.0.10", "@types/bootstrap": "^5.0.12", + "@types/codemirror": "^5.60.0", "@types/d3": "^6.3.0", "@types/diff": "^5.0.0", "@types/jest": "^26.0.22", @@ -60,6 +61,7 @@ "@types/marked": "^2.0.2", "bootstrap": "=5.0.0-beta3", "bootstrap-icons": "^1.4.0", + "codemirror": "^5.61.1", "css-browser-selector": "^0.6.5", "d3": "^7.0.0", "intl-pluralrules": "^1.2.2", diff --git a/ts/sass/BUILD.bazel b/ts/sass/BUILD.bazel index f78576633..9c61c0b79 100644 --- a/ts/sass/BUILD.bazel +++ b/ts/sass/BUILD.bazel @@ -8,7 +8,7 @@ sass_library( "bootstrap-dark.scss", ], visibility = ["//visibility:public"], - deps = ["//ts/sass/bootstrap"], + deps = ["//ts/sass/bootstrap", "//ts/sass/codemirror"], ) sass_library( diff --git a/ts/sass/codemirror/BUILD.bazel b/ts/sass/codemirror/BUILD.bazel new file mode 100644 index 000000000..0ef981a1e --- /dev/null +++ b/ts/sass/codemirror/BUILD.bazel @@ -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"], +) diff --git a/ts/yarn.lock b/ts/yarn.lock index 5fb4bd20b..53048b413 100644 --- a/ts/yarn.lock +++ b/ts/yarn.lock @@ -701,6 +701,13 @@ "@popperjs/core" "^2.9.2" "@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@*": version "2.12.1" 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" 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@*": version "7946.0.7" 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" 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@*": version "20.2.0" 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" 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: version "1.0.1" resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59"