diff --git a/ts/editor/changeTimer.ts b/ts/editor/changeTimer.ts
new file mode 100644
index 000000000..0b6566c4c
--- /dev/null
+++ b/ts/editor/changeTimer.ts
@@ -0,0 +1,47 @@
+import type { EditingArea } from ".";
+
+import { getCurrentField } from ".";
+import { bridgeCommand } from "./lib";
+import { getNoteId } from "./noteId";
+import { updateButtonState } from "./toolbar";
+
+let changeTimer: number | null = null;
+
+export function triggerChangeTimer(currentField: EditingArea): void {
+ clearChangeTimer();
+ changeTimer = setTimeout(function () {
+ updateButtonState();
+ saveField(currentField, "key");
+ }, 600);
+}
+
+function clearChangeTimer(): void {
+ if (changeTimer) {
+ clearTimeout(changeTimer);
+ changeTimer = null;
+ }
+}
+
+export function saveField(currentField: EditingArea, type: "blur" | "key"): void {
+ clearChangeTimer();
+ bridgeCommand(
+ `${type}:${currentField.ord}:${getNoteId()}:${currentField.fieldHTML}`
+ );
+}
+
+export function saveNow(keepFocus: boolean): void {
+ const currentField = getCurrentField();
+
+ if (!currentField) {
+ return;
+ }
+
+ clearChangeTimer();
+
+ if (keepFocus) {
+ saveField(currentField, "key");
+ } else {
+ // triggers onBlur, which saves
+ currentField.blurEditable();
+ }
+}
diff --git a/ts/editor/editor.scss b/ts/editor/editor.scss
index 32bea7b6b..30b0af96b 100644
--- a/ts/editor/editor.scss
+++ b/ts/editor/editor.scss
@@ -10,7 +10,8 @@ html {
flex-direction: column;
margin: 5px;
- & > *, & > * > * {
+ & > *,
+ & > * > * {
margin: 1px 0;
&:first-child {
@@ -55,15 +56,19 @@ body {
background: var(--bg-color);
}
-.topbuts > * {
- margin: 0 1px;
+.topbuts {
+ margin-bottom: 2px;
- &:first-child {
- margin-left: 0;
- }
+ & > * {
+ margin: 0 1px;
- &:last-child {
- margin-right: 0;
+ &:first-child {
+ margin-left: 0;
+ }
+
+ &:last-child {
+ margin-right: 0;
+ }
}
}
diff --git a/ts/editor/focusHandlers.ts b/ts/editor/focusHandlers.ts
new file mode 100644
index 000000000..df5d0d16b
--- /dev/null
+++ b/ts/editor/focusHandlers.ts
@@ -0,0 +1,76 @@
+import type { EditingArea, EditorField } from ".";
+
+import { bridgeCommand } from "./lib";
+import { enableButtons, disableButtons } from "./toolbar";
+import { saveField } from "./changeTimer";
+
+enum ViewportRelativePosition {
+ Contained,
+ ExceedTop,
+ ExceedBottom,
+}
+
+function isFieldInViewport(
+ element: Element,
+ toolbarHeight: number
+): ViewportRelativePosition {
+ const rect = element.getBoundingClientRect();
+
+ return rect.top <= toolbarHeight
+ ? ViewportRelativePosition.ExceedTop
+ : rect.bottom >= (window.innerHeight || document.documentElement.clientHeight)
+ ? ViewportRelativePosition.ExceedBottom
+ : ViewportRelativePosition.Contained;
+}
+
+function caretToEnd(currentField: EditingArea): void {
+ const range = document.createRange();
+ range.selectNodeContents(currentField.editable);
+ range.collapse(false);
+ const selection = currentField.getSelection();
+ selection.removeAllRanges();
+ selection.addRange(range);
+}
+
+// For distinguishing focus by refocusing window from deliberate focus
+let previousActiveElement: EditingArea | null = null;
+
+export function onFocus(evt: FocusEvent): void {
+ const currentField = evt.currentTarget as EditingArea;
+
+ if (currentField === previousActiveElement) {
+ return;
+ }
+
+ const editorField = currentField.parentElement! as EditorField;
+ const toolbarHeight = document.getElementById("topbutsOuter")!.clientHeight;
+ switch (isFieldInViewport(editorField, toolbarHeight)) {
+ case ViewportRelativePosition.ExceedBottom:
+ editorField.scrollIntoView(false);
+ break;
+ case ViewportRelativePosition.ExceedTop:
+ editorField.scrollIntoView(true);
+ window.scrollBy(0, -toolbarHeight);
+ break;
+ }
+
+ currentField.focusEditable();
+ bridgeCommand(`focus:${currentField.ord}`);
+ enableButtons();
+ // do this twice so that there's no flicker on newer versions
+ caretToEnd(currentField);
+}
+
+export function onBlur(evt: FocusEvent): void {
+ const currentField = evt.currentTarget as EditingArea;
+
+ if (currentField === document.activeElement) {
+ // other widget or window focused; current field unchanged
+ saveField(currentField, "key");
+ previousActiveElement = currentField;
+ } else {
+ saveField(currentField, "blur");
+ disableButtons();
+ previousActiveElement = null;
+ }
+}
diff --git a/ts/editor/filterHtml.ts b/ts/editor/htmlFilter.ts
similarity index 99%
rename from ts/editor/filterHtml.ts
rename to ts/editor/htmlFilter.ts
index 0b142932d..bb995cbd9 100644
--- a/ts/editor/filterHtml.ts
+++ b/ts/editor/htmlFilter.ts
@@ -1,6 +1,6 @@
import { nodeIsElement } from "./helpers";
-export let filterHTML = function (
+export function filterHTML(
html: string,
internal: boolean,
extendedMode: boolean
@@ -21,7 +21,7 @@ export let filterHTML = function (
}
outHtml = outHtml.trim();
return outHtml;
-};
+}
let allowedTagsBasic = {};
let allowedTagsExtended = {};
diff --git a/ts/editor/index.ts b/ts/editor/index.ts
index 46cf52524..2abc66412 100644
--- a/ts/editor/index.ts
+++ b/ts/editor/index.ts
@@ -1,13 +1,18 @@
/* Copyright: Ankitects Pty Ltd and contributors
* License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */
-import { filterHTML } from "./filterHtml";
-import { nodeIsElement, nodeIsInline } from "./helpers";
+import { nodeIsInline } from "./helpers";
import { bridgeCommand } from "./lib";
+import { saveField } from "./changeTimer";
+import { filterHTML } from "./htmlFilter";
+import { updateButtonState, maybeDisableButtons } from "./toolbar";
+import { onInput, onKey, onKeyUp } from "./inputHandlers";
+import { onFocus, onBlur } from "./focusHandlers";
-let currentField: EditingArea | null = null;
-let changeTimer: number | null = null;
-let currentNoteId: number | null = null;
+export { setNoteId, getNoteId } from "./noteId";
+export { preventButtonFocus, toggleEditorButton, setFGButton } from "./toolbar";
+export { saveNow } from "./changeTimer";
+export { wrap, wrapIntoText } from "./wrap";
declare global {
interface Selection {
@@ -18,162 +23,10 @@ declare global {
}
}
-export function setFGButton(col: string): void {
- document.getElementById("forecolor").style.backgroundColor = col;
-}
-
-export function saveNow(keepFocus: boolean): void {
- if (!currentField) {
- return;
- }
-
- clearChangeTimer();
-
- if (keepFocus) {
- saveField("key");
- } else {
- // triggers onBlur, which saves
- currentField.blurEditable();
- }
-}
-
-function triggerKeyTimer(): void {
- clearChangeTimer();
- changeTimer = setTimeout(function () {
- updateButtonState();
- saveField("key");
- }, 600);
-}
-
-function onKey(evt: KeyboardEvent): void {
- // esc clears focus, allowing dialog to close
- if (evt.code === "Escape") {
- currentField.blurEditable();
- return;
- }
-
- // prefer
instead of
- if (evt.code === "Enter" && !inListItem()) {
- evt.preventDefault();
- document.execCommand("insertLineBreak");
- }
-
- // // fix Ctrl+right/left handling in RTL fields
- if (currentField.isRightToLeft()) {
- const selection = currentField.getSelection();
- const granularity = evt.ctrlKey ? "word" : "character";
- const alter = evt.shiftKey ? "extend" : "move";
-
- switch (evt.code) {
- case "ArrowRight":
- selection.modify(alter, "right", granularity);
- evt.preventDefault();
- return;
- case "ArrowLeft":
- selection.modify(alter, "left", granularity);
- evt.preventDefault();
- return;
- }
- }
-
- triggerKeyTimer();
-}
-
-function onKeyUp(evt: KeyboardEvent): void {
- // Avoid div element on remove
- if (evt.code === "Enter" || evt.code === "Backspace") {
- const anchor = currentField.getSelection().anchorNode;
-
- if (
- nodeIsElement(anchor) &&
- anchor.tagName === "DIV" &&
- !(anchor instanceof EditingArea) &&
- anchor.childElementCount === 1 &&
- anchor.children[0].tagName === "BR"
- ) {
- anchor.replaceWith(anchor.children[0]);
- }
- }
-}
-
-function inListItem(): boolean {
- const anchor = currentField.getSelection().anchorNode;
-
- let inList = false;
- let n = nodeIsElement(anchor) ? anchor : anchor.parentElement;
- while (n) {
- inList = inList || window.getComputedStyle(n).display == "list-item";
- n = n.parentElement;
- }
-
- return inList;
-}
-
-function onInput(): void {
- // make sure IME changes get saved
- triggerKeyTimer();
-}
-
-function updateButtonState(): void {
- const buts = ["bold", "italic", "underline", "superscript", "subscript"];
- for (const name of buts) {
- const elem = document.querySelector(`#${name}`) as HTMLElement;
- elem.classList.toggle("highlighted", document.queryCommandState(name));
- }
-
- // fixme: forecolor
- // 'col': document.queryCommandValue("forecolor")
-}
-
-export function toggleEditorButton(buttonid: string): void {
- const button = $(buttonid)[0];
- button.classList.toggle("highlighted");
-}
-
-export function setFormat(cmd: string, arg?: any, nosave: boolean = false): void {
- document.execCommand(cmd, false, arg);
- if (!nosave) {
- saveField("key");
- updateButtonState();
- }
-}
-
-function clearChangeTimer(): void {
- if (changeTimer) {
- clearTimeout(changeTimer);
- changeTimer = null;
- }
-}
-
-function onFocus(evt: FocusEvent): void {
- const elem = evt.currentTarget as EditingArea;
- if (currentField === elem) {
- // anki window refocused; current element unchanged
- return;
- }
- elem.focusEditable();
- currentField = elem;
- bridgeCommand(`focus:${currentField.ord}`);
- enableButtons();
- // do this twice so that there's no flicker on newer versions
- caretToEnd();
- // scroll if bottom of element off the screen
- function pos(elem: HTMLElement): number {
- let cur = 0;
- do {
- cur += elem.offsetTop;
- elem = elem.offsetParent as HTMLElement;
- } while (elem);
- return cur;
- }
-
- const y = pos(elem);
- if (
- window.pageYOffset + window.innerHeight < y + elem.offsetHeight ||
- window.pageYOffset > y
- ) {
- window.scroll(0, y + elem.offsetHeight - window.innerHeight);
- }
+export function getCurrentField(): EditingArea | null {
+ return document.activeElement instanceof EditingArea
+ ? document.activeElement
+ : null;
}
export function focusField(n: number): void {
@@ -190,42 +43,32 @@ export function focusIfField(x: number, y: number): boolean {
let elem = elements[i] as EditingArea;
if (elem instanceof EditingArea) {
elem.focusEditable();
- // the focus event may not fire if the window is not active, so make sure
- // the current field is set
- currentField = elem;
return true;
}
}
return false;
}
-function onPaste(event: ClipboardEvent): void {
+export function pasteHTML(
+ html: string,
+ internal: boolean,
+ extendedMode: boolean
+): void {
+ html = filterHTML(html, internal, extendedMode);
+
+ if (html !== "") {
+ setFormat("inserthtml", html);
+ }
+}
+
+function onPaste(evt: ClipboardEvent): void {
bridgeCommand("paste");
- event.preventDefault();
+ evt.preventDefault();
}
-function caretToEnd(): void {
- const range = document.createRange();
- range.selectNodeContents(currentField.editable);
- range.collapse(false);
- const selection = currentField.getSelection();
- selection.removeAllRanges();
- selection.addRange(range);
-}
-
-function onBlur(): void {
- if (!currentField) {
- return;
- }
-
- if (document.activeElement === currentField) {
- // other widget or window focused; current field unchanged
- saveField("key");
- } else {
- saveField("blur");
- currentField = null;
- disableButtons();
- }
+function onCutOrCopy(): boolean {
+ bridgeCommand("cutOrCopy");
+ return true;
}
function containsInlineContent(field: Element): boolean {
@@ -243,85 +86,6 @@ function containsInlineContent(field: Element): boolean {
return true;
}
-function saveField(type: "blur" | "key"): void {
- clearChangeTimer();
- if (!currentField) {
- // no field has been focused yet
- return;
- }
-
- bridgeCommand(
- `${type}:${currentField.ord}:${currentNoteId}:${currentField.fieldHTML}`
- );
-}
-
-function wrappedExceptForWhitespace(text: string, front: string, back: string): string {
- const match = text.match(/^(\s*)([^]*?)(\s*)$/);
- return match[1] + front + match[2] + back + match[3];
-}
-
-export function preventButtonFocus(): void {
- for (const element of document.querySelectorAll("button.linkb")) {
- element.addEventListener("mousedown", (evt: Event) => {
- evt.preventDefault();
- });
- }
-}
-
-function disableButtons(): void {
- $("button.linkb:not(.perm)").prop("disabled", true);
-}
-
-function enableButtons(): void {
- $("button.linkb").prop("disabled", false);
-}
-
-// disable the buttons if a field is not currently focused
-function maybeDisableButtons(): void {
- if (document.activeElement instanceof EditingArea) {
- enableButtons();
- } else {
- disableButtons();
- }
-}
-
-export function wrap(front: string, back: string): void {
- wrapInternal(front, back, false);
-}
-
-/* currently unused */
-export function wrapIntoText(front: string, back: string): void {
- wrapInternal(front, back, true);
-}
-
-function wrapInternal(front: string, back: string, plainText: boolean): void {
- const s = currentField.getSelection();
- let r = s.getRangeAt(0);
- const content = r.cloneContents();
- const span = document.createElement("span");
- span.appendChild(content);
- if (plainText) {
- const new_ = wrappedExceptForWhitespace(span.innerText, front, back);
- setFormat("inserttext", new_);
- } else {
- const new_ = wrappedExceptForWhitespace(span.innerHTML, front, back);
- setFormat("inserthtml", new_);
- }
- if (!span.innerHTML) {
- // run with an empty selection; move cursor back past postfix
- r = s.getRangeAt(0);
- r.setStart(r.startContainer, r.startOffset - back.length);
- r.collapse(true);
- s.removeAllRanges();
- s.addRange(r);
- }
-}
-
-function onCutOrCopy(): boolean {
- bridgeCommand("cutOrCopy");
- return true;
-}
-
class Editable extends HTMLElement {
set fieldHTML(content: string) {
this.innerHTML = content;
@@ -344,7 +108,7 @@ class Editable extends HTMLElement {
customElements.define("anki-editable", Editable);
-class EditingArea extends HTMLDivElement {
+export class EditingArea extends HTMLDivElement {
editable: Editable;
baseStyle: HTMLStyleElement;
@@ -356,14 +120,14 @@ class EditingArea extends HTMLDivElement {
const rootStyle = document.createElement("link");
rootStyle.setAttribute("rel", "stylesheet");
rootStyle.setAttribute("href", "./_anki/css/editable.css");
- this.shadowRoot.appendChild(rootStyle);
+ this.shadowRoot!.appendChild(rootStyle);
this.baseStyle = document.createElement("style");
this.baseStyle.setAttribute("rel", "stylesheet");
- this.shadowRoot.appendChild(this.baseStyle);
+ this.shadowRoot!.appendChild(this.baseStyle);
this.editable = document.createElement("anki-editable") as Editable;
- this.shadowRoot.appendChild(this.editable);
+ this.shadowRoot!.appendChild(this.editable);
}
get ord(): number {
@@ -387,6 +151,7 @@ class EditingArea extends HTMLDivElement {
this.addEventListener("paste", onPaste);
this.addEventListener("copy", onCutOrCopy);
this.addEventListener("oncut", onCutOrCopy);
+ this.addEventListener("mouseup", updateButtonState);
const baseStyleSheet = this.baseStyle.sheet as CSSStyleSheet;
baseStyleSheet.insertRule("anki-editable {}", 0);
@@ -401,6 +166,7 @@ class EditingArea extends HTMLDivElement {
this.removeEventListener("paste", onPaste);
this.removeEventListener("copy", onCutOrCopy);
this.removeEventListener("oncut", onCutOrCopy);
+ this.removeEventListener("mouseup", updateButtonState);
}
initialize(color: string, content: string): void {
@@ -427,7 +193,7 @@ class EditingArea extends HTMLDivElement {
}
getSelection(): Selection {
- return this.shadowRoot.getSelection();
+ return this.shadowRoot!.getSelection()!;
}
focusEditable(): void {
@@ -441,7 +207,7 @@ class EditingArea extends HTMLDivElement {
customElements.define("anki-editing-area", EditingArea, { extends: "div" });
-class EditorField extends HTMLDivElement {
+export class EditorField extends HTMLDivElement {
labelContainer: HTMLDivElement;
label: HTMLSpanElement;
editingArea: EditingArea;
@@ -490,7 +256,7 @@ class EditorField extends HTMLDivElement {
customElements.define("anki-editor-field", EditorField, { extends: "div" });
function adjustFieldAmount(amount: number): void {
- const fieldsContainer = document.getElementById("fields");
+ const fieldsContainer = document.getElementById("fields")!;
while (fieldsContainer.childElementCount < amount) {
const newField = document.createElement("div", {
@@ -501,12 +267,12 @@ function adjustFieldAmount(amount: number): void {
}
while (fieldsContainer.childElementCount > amount) {
- fieldsContainer.removeChild(fieldsContainer.lastElementChild);
+ fieldsContainer.removeChild(fieldsContainer.lastElementChild as Node);
}
}
export function getEditorField(n: number): EditorField | null {
- const fields = document.getElementById("fields").children;
+ const fields = document.getElementById("fields")!.children;
return (fields[n] as EditorField) ?? null;
}
@@ -514,7 +280,7 @@ export function forEditorField(
values: T[],
func: (field: EditorField, value: T) => void
): void {
- const fields = document.getElementById("fields").children;
+ const fields = document.getElementById("fields")!.children;
for (let i = 0; i < fields.length; i++) {
const field = fields[i] as EditorField;
func(field, values[i]);
@@ -541,7 +307,7 @@ export function setBackgrounds(cols: ("dupe" | "")[]) {
field.editingArea.classList.toggle("dupe", value === "dupe")
);
document
- .querySelector("#dupes")
+ .getElementById("dupes")!
.classList.toggle("is-inactive", !cols.includes("dupe"));
}
@@ -551,18 +317,10 @@ export function setFonts(fonts: [string, number, boolean][]): void {
});
}
-export function setNoteId(id: number): void {
- currentNoteId = id;
-}
-
-export let pasteHTML = function (
- html: string,
- internal: boolean,
- extendedMode: boolean
-): void {
- html = filterHTML(html, internal, extendedMode);
-
- if (html !== "") {
- setFormat("inserthtml", html);
+export function setFormat(cmd: string, arg?: any, nosave: boolean = false): void {
+ document.execCommand(cmd, false, arg);
+ if (!nosave) {
+ saveField(getCurrentField() as EditingArea, "key");
+ updateButtonState();
}
-};
+}
diff --git a/ts/editor/inputHandlers.ts b/ts/editor/inputHandlers.ts
new file mode 100644
index 000000000..d60c64165
--- /dev/null
+++ b/ts/editor/inputHandlers.ts
@@ -0,0 +1,76 @@
+import { EditingArea } from ".";
+import { nodeIsElement } from "./helpers";
+import { triggerChangeTimer } from "./changeTimer";
+
+function inListItem(currentField: EditingArea): boolean {
+ const anchor = currentField.getSelection()!.anchorNode!;
+
+ let inList = false;
+ let n = nodeIsElement(anchor) ? anchor : anchor.parentElement;
+ while (n) {
+ inList = inList || window.getComputedStyle(n).display == "list-item";
+ n = n.parentElement;
+ }
+
+ return inList;
+}
+
+export function onInput(event: Event): void {
+ // make sure IME changes get saved
+ triggerChangeTimer(event.currentTarget as EditingArea);
+}
+
+export function onKey(evt: KeyboardEvent): void {
+ const currentField = evt.currentTarget as EditingArea;
+
+ // esc clears focus, allowing dialog to close
+ if (evt.code === "Escape") {
+ currentField.blurEditable();
+ return;
+ }
+
+ // prefer
instead of
+ if (evt.code === "Enter" && !inListItem(currentField)) {
+ evt.preventDefault();
+ document.execCommand("insertLineBreak");
+ }
+
+ // // fix Ctrl+right/left handling in RTL fields
+ if (currentField.isRightToLeft()) {
+ const selection = currentField.getSelection();
+ const granularity = evt.ctrlKey ? "word" : "character";
+ const alter = evt.shiftKey ? "extend" : "move";
+
+ switch (evt.code) {
+ case "ArrowRight":
+ selection.modify(alter, "right", granularity);
+ evt.preventDefault();
+ return;
+ case "ArrowLeft":
+ selection.modify(alter, "left", granularity);
+ evt.preventDefault();
+ return;
+ }
+ }
+
+ triggerChangeTimer(currentField);
+}
+
+export function onKeyUp(evt: KeyboardEvent): void {
+ const currentField = evt.currentTarget as EditingArea;
+
+ // Avoid div element on remove
+ if (evt.code === "Enter" || evt.code === "Backspace") {
+ const anchor = currentField.getSelection().anchorNode as Node;
+
+ if (
+ nodeIsElement(anchor) &&
+ anchor.tagName === "DIV" &&
+ !(anchor instanceof EditingArea) &&
+ anchor.childElementCount === 1 &&
+ anchor.children[0].tagName === "BR"
+ ) {
+ anchor.replaceWith(anchor.children[0]);
+ }
+ }
+}
diff --git a/ts/editor/noteId.ts b/ts/editor/noteId.ts
new file mode 100644
index 000000000..e787a77d6
--- /dev/null
+++ b/ts/editor/noteId.ts
@@ -0,0 +1,9 @@
+let currentNoteId: number | null = null;
+
+export function setNoteId(id: number): void {
+ currentNoteId = id;
+}
+
+export function getNoteId(): number | null {
+ return currentNoteId;
+}
diff --git a/ts/editor/toolbar.ts b/ts/editor/toolbar.ts
new file mode 100644
index 000000000..f0410d78f
--- /dev/null
+++ b/ts/editor/toolbar.ts
@@ -0,0 +1,46 @@
+import { EditingArea } from ".";
+
+export function updateButtonState(): void {
+ const buts = ["bold", "italic", "underline", "superscript", "subscript"];
+ for (const name of buts) {
+ const elem = document.querySelector(`#${name}`) as HTMLElement;
+ elem.classList.toggle("highlighted", document.queryCommandState(name));
+ }
+
+ // fixme: forecolor
+ // 'col': document.queryCommandValue("forecolor")
+}
+
+export function preventButtonFocus(): void {
+ for (const element of document.querySelectorAll("button.linkb")) {
+ element.addEventListener("mousedown", (evt: Event) => {
+ evt.preventDefault();
+ });
+ }
+}
+
+export function disableButtons(): void {
+ $("button.linkb:not(.perm)").prop("disabled", true);
+}
+
+export function enableButtons(): void {
+ $("button.linkb").prop("disabled", false);
+}
+
+// disable the buttons if a field is not currently focused
+export function maybeDisableButtons(): void {
+ if (document.activeElement instanceof EditingArea) {
+ enableButtons();
+ } else {
+ disableButtons();
+ }
+}
+
+export function setFGButton(col: string): void {
+ document.getElementById("forecolor")!.style.backgroundColor = col;
+}
+
+export function toggleEditorButton(buttonid: string): void {
+ const button = $(buttonid)[0];
+ button.classList.toggle("highlighted");
+}
diff --git a/ts/editor/wrap.ts b/ts/editor/wrap.ts
new file mode 100644
index 000000000..2408fe0f5
--- /dev/null
+++ b/ts/editor/wrap.ts
@@ -0,0 +1,44 @@
+import { getCurrentField, setFormat } from ".";
+
+function wrappedExceptForWhitespace(text: string, front: string, back: string): string {
+ const match = text.match(/^(\s*)([^]*?)(\s*)$/)!;
+ return match[1] + front + match[2] + back + match[3];
+}
+
+function moveCursorPastPostfix(selection: Selection, postfix: string): void {
+ const range = selection.getRangeAt(0);
+ range.setStart(range.startContainer, range.startOffset - postfix.length);
+ range.collapse(true);
+ selection.removeAllRanges();
+ selection.addRange(range);
+}
+
+function wrapInternal(front: string, back: string, plainText: boolean): void {
+ const currentField = getCurrentField()!;
+ const selection = currentField.getSelection();
+ const range = selection.getRangeAt(0);
+ const content = range.cloneContents();
+ const span = document.createElement("span");
+ span.appendChild(content);
+
+ if (plainText) {
+ const new_ = wrappedExceptForWhitespace(span.innerText, front, back);
+ setFormat("inserttext", new_);
+ } else {
+ const new_ = wrappedExceptForWhitespace(span.innerHTML, front, back);
+ setFormat("inserthtml", new_);
+ }
+
+ if (!span.innerHTML) {
+ moveCursorPastPostfix(selection, back);
+ }
+}
+
+export function wrap(front: string, back: string): void {
+ wrapInternal(front, back, false);
+}
+
+/* currently unused */
+export function wrapIntoText(front: string, back: string): void {
+ wrapInternal(front, back, true);
+}