mirror of
https://github.com/ankitects/anki.git
synced 2025-09-24 16:56:36 -04:00
Merge pull request #957 from hgiesel/fieldsshadow
Implement editor as Web Component
This commit is contained in:
commit
b66bedbc9f
4 changed files with 304 additions and 158 deletions
19
qt/aqt/data/web/css/editable.scss
Normal file
19
qt/aqt/data/web/css/editable.scss
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
anki-editable {
|
||||||
|
display: block;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 5px;
|
||||||
|
|
||||||
|
&:empty::after {
|
||||||
|
content: "\a";
|
||||||
|
white-space: pre;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
img.drawing {
|
||||||
|
zoom: 50%;
|
||||||
|
|
||||||
|
.nightMode & {
|
||||||
|
filter: unquote("invert() hue-rotate(180deg)");
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,12 +1,16 @@
|
||||||
/* Copyright: Ankitects Pty Ltd and contributors
|
/* 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 */
|
||||||
|
|
||||||
|
html {
|
||||||
|
background: var(--bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
#fields {
|
#fields {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin: 5px;
|
margin: 5px;
|
||||||
|
|
||||||
& > * {
|
& > *, & > * > * {
|
||||||
margin: 1px 0;
|
margin: 1px 0;
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
|
@ -22,13 +26,9 @@
|
||||||
.field {
|
.field {
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
background: var(--frame-bg);
|
background: var(--frame-bg);
|
||||||
padding: 5px;
|
|
||||||
overflow-wrap: break-word;
|
|
||||||
overflow: auto;
|
|
||||||
|
|
||||||
&:empty::after {
|
&.dupe {
|
||||||
content: "\A";
|
background: var(--flag1-bg);
|
||||||
white-space: pre;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,10 +37,6 @@
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
|
||||||
max-width: 90%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
@ -55,6 +51,8 @@ body {
|
||||||
left: 0;
|
left: 0;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
|
|
||||||
|
background: var(--bg-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.topbuts > * {
|
.topbuts > * {
|
||||||
|
@ -118,21 +116,26 @@ button.highlighted {
|
||||||
}
|
}
|
||||||
|
|
||||||
#topbutsright & {
|
#topbutsright & {
|
||||||
border-bottom: 3px solid #000;
|
border-bottom: 3px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nightMode #topbutsright & {
|
||||||
|
border-bottom: 3px solid white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dupe {
|
#dupes {
|
||||||
background: var(--flag1-bg);
|
position: sticky;
|
||||||
}
|
bottom: 0;
|
||||||
|
|
||||||
#dupes a {
|
text-align: center;
|
||||||
color: var(--link);
|
background-color: var(--bg-color);
|
||||||
}
|
|
||||||
|
|
||||||
.drawing {
|
&.is-inactive {
|
||||||
zoom: 50%;
|
display: none;
|
||||||
}
|
}
|
||||||
.nightMode img.drawing {
|
|
||||||
filter: unquote("invert() hue-rotate(180deg)");
|
a {
|
||||||
|
color: var(--link);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
/* Copyright: Ankitects Pty Ltd and contributors
|
/* 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 */
|
||||||
|
|
||||||
let currentField = null;
|
let currentField: EditingArea | null = null;
|
||||||
let changeTimer = null;
|
let changeTimer: number | null = null;
|
||||||
let currentNoteId = null;
|
let currentNoteId: number | null = null;
|
||||||
|
|
||||||
declare interface String {
|
declare interface String {
|
||||||
format(...args: string[]): string;
|
format(...args: string[]): string;
|
||||||
|
@ -19,7 +19,7 @@ String.prototype.format = function (...args: string[]): string {
|
||||||
};
|
};
|
||||||
|
|
||||||
function setFGButton(col: string): void {
|
function setFGButton(col: string): void {
|
||||||
$("#forecolor")[0].style.backgroundColor = col;
|
document.getElementById("forecolor").style.backgroundColor = col;
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveNow(keepFocus: boolean): void {
|
function saveNow(keepFocus: boolean): void {
|
||||||
|
@ -33,7 +33,7 @@ function saveNow(keepFocus: boolean): void {
|
||||||
saveField("key");
|
saveField("key");
|
||||||
} else {
|
} else {
|
||||||
// triggers onBlur, which saves
|
// triggers onBlur, which saves
|
||||||
currentField.blur();
|
currentField.blurEditable();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -52,7 +52,7 @@ interface Selection {
|
||||||
function onKey(evt: KeyboardEvent): void {
|
function onKey(evt: KeyboardEvent): void {
|
||||||
// esc clears focus, allowing dialog to close
|
// esc clears focus, allowing dialog to close
|
||||||
if (evt.code === "Escape") {
|
if (evt.code === "Escape") {
|
||||||
currentField.blur();
|
currentField.blurEditable();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,28 +60,23 @@ function onKey(evt: KeyboardEvent): void {
|
||||||
if (evt.code === "Enter" && !inListItem()) {
|
if (evt.code === "Enter" && !inListItem()) {
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
document.execCommand("insertLineBreak");
|
document.execCommand("insertLineBreak");
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// fix Ctrl+right/left handling in RTL fields
|
// // fix Ctrl+right/left handling in RTL fields
|
||||||
if (currentField.dir === "rtl") {
|
if (currentField.isRightToLeft()) {
|
||||||
const selection = window.getSelection();
|
const selection = currentField.getSelection();
|
||||||
let granularity = "character";
|
const granularity = evt.ctrlKey ? "word" : "character";
|
||||||
let alter = "move";
|
const alter = evt.shiftKey ? "extend" : "move";
|
||||||
if (evt.ctrlKey) {
|
|
||||||
granularity = "word";
|
switch (evt.code) {
|
||||||
}
|
case "ArrowRight":
|
||||||
if (evt.shiftKey) {
|
selection.modify(alter, "right", granularity);
|
||||||
alter = "extend";
|
evt.preventDefault();
|
||||||
}
|
return;
|
||||||
if (evt.code === "ArrowRight") {
|
case "ArrowLeft":
|
||||||
selection.modify(alter, "right", granularity);
|
selection.modify(alter, "left", granularity);
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
return;
|
return;
|
||||||
} else if (evt.code === "ArrowLeft") {
|
|
||||||
selection.modify(alter, "left", granularity);
|
|
||||||
evt.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,12 +86,12 @@ function onKey(evt: KeyboardEvent): void {
|
||||||
function onKeyUp(evt: KeyboardEvent): void {
|
function onKeyUp(evt: KeyboardEvent): void {
|
||||||
// Avoid div element on remove
|
// Avoid div element on remove
|
||||||
if (evt.code === "Enter" || evt.code === "Backspace") {
|
if (evt.code === "Enter" || evt.code === "Backspace") {
|
||||||
const anchor = window.getSelection().anchorNode;
|
const anchor = currentField.getSelection().anchorNode;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
nodeIsElement(anchor) &&
|
nodeIsElement(anchor) &&
|
||||||
anchor.tagName === "DIV" &&
|
anchor.tagName === "DIV" &&
|
||||||
!anchor.classList.contains("field") &&
|
!(anchor instanceof EditingArea) &&
|
||||||
anchor.childElementCount === 1 &&
|
anchor.childElementCount === 1 &&
|
||||||
anchor.children[0].tagName === "BR"
|
anchor.children[0].tagName === "BR"
|
||||||
) {
|
) {
|
||||||
|
@ -172,7 +167,7 @@ function nodeIsInline(node: Node): boolean {
|
||||||
}
|
}
|
||||||
|
|
||||||
function inListItem(): boolean {
|
function inListItem(): boolean {
|
||||||
const anchor = window.getSelection().anchorNode;
|
const anchor = currentField.getSelection().anchorNode;
|
||||||
|
|
||||||
let inList = false;
|
let inList = false;
|
||||||
let n = nodeIsElement(anchor) ? anchor : anchor.parentElement;
|
let n = nodeIsElement(anchor) ? anchor : anchor.parentElement;
|
||||||
|
@ -195,7 +190,7 @@ function insertNewline(): void {
|
||||||
// differently. so in such cases we note the height has not
|
// differently. so in such cases we note the height has not
|
||||||
// changed and insert an extra newline.
|
// changed and insert an extra newline.
|
||||||
|
|
||||||
const r = window.getSelection().getRangeAt(0);
|
const r = currentField.getSelection().getRangeAt(0);
|
||||||
if (!r.collapsed) {
|
if (!r.collapsed) {
|
||||||
// delete any currently selected text first, making
|
// delete any currently selected text first, making
|
||||||
// sure the delete is undoable
|
// sure the delete is undoable
|
||||||
|
@ -211,7 +206,7 @@ function insertNewline(): void {
|
||||||
|
|
||||||
// is the cursor in an environment that respects whitespace?
|
// is the cursor in an environment that respects whitespace?
|
||||||
function inPreEnvironment(): boolean {
|
function inPreEnvironment(): boolean {
|
||||||
const anchor = window.getSelection().anchorNode;
|
const anchor = currentField.getSelection().anchorNode;
|
||||||
const n = nodeIsElement(anchor) ? anchor : anchor.parentElement;
|
const n = nodeIsElement(anchor) ? anchor : anchor.parentElement;
|
||||||
|
|
||||||
return window.getComputedStyle(n).whiteSpace.startsWith("pre");
|
return window.getComputedStyle(n).whiteSpace.startsWith("pre");
|
||||||
|
@ -254,13 +249,14 @@ function clearChangeTimer(): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
function onFocus(evt: FocusEvent): void {
|
function onFocus(evt: FocusEvent): void {
|
||||||
const elem = evt.currentTarget as HTMLElement;
|
const elem = evt.currentTarget as EditingArea;
|
||||||
if (currentField === elem) {
|
if (currentField === elem) {
|
||||||
// anki window refocused; current element unchanged
|
// anki window refocused; current element unchanged
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
elem.focusEditable();
|
||||||
currentField = elem;
|
currentField = elem;
|
||||||
pycmd(`focus:${currentFieldOrdinal()}`);
|
pycmd(`focus:${currentField.ord}`);
|
||||||
enableButtons();
|
enableButtons();
|
||||||
// do this twice so that there's no flicker on newer versions
|
// do this twice so that there's no flicker on newer versions
|
||||||
caretToEnd();
|
caretToEnd();
|
||||||
|
@ -284,18 +280,19 @@ function onFocus(evt: FocusEvent): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
function focusField(n: number): void {
|
function focusField(n: number): void {
|
||||||
if (n === null) {
|
const field = document.getElementById(`f${n}`) as EditingArea;
|
||||||
return;
|
|
||||||
|
if (field) {
|
||||||
|
field.focusEditable();
|
||||||
}
|
}
|
||||||
$(`#f${n}`).focus();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function focusIfField(x: number, y: number): boolean {
|
function focusIfField(x: number, y: number): boolean {
|
||||||
const elements = document.elementsFromPoint(x, y);
|
const elements = document.elementsFromPoint(x, y);
|
||||||
for (let i = 0; i < elements.length; i++) {
|
for (let i = 0; i < elements.length; i++) {
|
||||||
let elem = elements[i] as HTMLElement;
|
let elem = elements[i] as EditingArea;
|
||||||
if (elem.classList.contains("field")) {
|
if (elem instanceof EditingArea) {
|
||||||
elem.focus();
|
elem.focusEditable();
|
||||||
// the focus event may not fire if the window is not active, so make sure
|
// the focus event may not fire if the window is not active, so make sure
|
||||||
// the current field is set
|
// the current field is set
|
||||||
currentField = elem;
|
currentField = elem;
|
||||||
|
@ -311,12 +308,12 @@ function onPaste(): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
function caretToEnd(): void {
|
function caretToEnd(): void {
|
||||||
const r = document.createRange();
|
const range = document.createRange();
|
||||||
r.selectNodeContents(currentField);
|
range.selectNodeContents(currentField.editable);
|
||||||
r.collapse(false);
|
range.collapse(false);
|
||||||
const s = document.getSelection();
|
const selection = currentField.getSelection();
|
||||||
s.removeAllRanges();
|
selection.removeAllRanges();
|
||||||
s.addRange(r);
|
selection.addRange(range);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onBlur(): void {
|
function onBlur(): void {
|
||||||
|
@ -334,7 +331,7 @@ function onBlur(): void {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function fieldContainsInlineContent(field: HTMLDivElement): boolean {
|
function containsInlineContent(field: Element): boolean {
|
||||||
if (field.childNodes.length === 0) {
|
if (field.childNodes.length === 0) {
|
||||||
// for now, for all practical purposes, empty fields are in block mode
|
// for now, for all practical purposes, empty fields are in block mode
|
||||||
return false;
|
return false;
|
||||||
|
@ -356,18 +353,7 @@ function saveField(type: "blur" | "key"): void {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fieldText =
|
pycmd(`${type}:${currentField.ord}:${currentNoteId}:${currentField.fieldHTML}`);
|
||||||
fieldContainsInlineContent(currentField) &&
|
|
||||||
currentField.innerHTML.endsWith("<br>")
|
|
||||||
? // trim trailing <br>
|
|
||||||
currentField.innerHTML.slice(0, -4)
|
|
||||||
: currentField.innerHTML;
|
|
||||||
|
|
||||||
pycmd(`${type}:${currentFieldOrdinal()}:${currentNoteId}:${fieldText}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function currentFieldOrdinal(): string {
|
|
||||||
return currentField.id.substring(1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function wrappedExceptForWhitespace(text: string, front: string, back: string): string {
|
function wrappedExceptForWhitespace(text: string, front: string, back: string): string {
|
||||||
|
@ -393,10 +379,10 @@ function enableButtons(): void {
|
||||||
|
|
||||||
// disable the buttons if a field is not currently focused
|
// disable the buttons if a field is not currently focused
|
||||||
function maybeDisableButtons(): void {
|
function maybeDisableButtons(): void {
|
||||||
if (!document.activeElement || document.activeElement.className !== "field") {
|
if (document.activeElement instanceof EditingArea) {
|
||||||
disableButtons();
|
|
||||||
} else {
|
|
||||||
enableButtons();
|
enableButtons();
|
||||||
|
} else {
|
||||||
|
disableButtons();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -410,7 +396,7 @@ function wrapIntoText(front: string, back: string): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
function wrapInternal(front: string, back: string, plainText: boolean): void {
|
function wrapInternal(front: string, back: string, plainText: boolean): void {
|
||||||
const s = window.getSelection();
|
const s = currentField.getSelection();
|
||||||
let r = s.getRangeAt(0);
|
let r = s.getRangeAt(0);
|
||||||
const content = r.cloneContents();
|
const content = r.cloneContents();
|
||||||
const span = document.createElement("span");
|
const span = document.createElement("span");
|
||||||
|
@ -437,41 +423,198 @@ function onCutOrCopy(): boolean {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createField(
|
class Editable extends HTMLElement {
|
||||||
index: number,
|
set fieldHTML(content: string) {
|
||||||
label: string,
|
this.innerHTML = content;
|
||||||
color: string,
|
|
||||||
content: string
|
|
||||||
): [HTMLDivElement, HTMLDivElement] {
|
|
||||||
const name = document.createElement("div");
|
|
||||||
name.id = `name${index}`;
|
|
||||||
name.className = "fname";
|
|
||||||
|
|
||||||
const fieldname = document.createElement("span");
|
if (containsInlineContent(this)) {
|
||||||
fieldname.className = "fieldname";
|
this.appendChild(document.createElement("br"));
|
||||||
fieldname.innerText = label;
|
}
|
||||||
name.appendChild(fieldname);
|
|
||||||
|
|
||||||
const field = document.createElement("div");
|
|
||||||
field.id = `f${index}`;
|
|
||||||
field.className = "field";
|
|
||||||
field.setAttribute("contenteditable", "true");
|
|
||||||
field.style.color = color;
|
|
||||||
field.addEventListener("keydown", onKey);
|
|
||||||
field.addEventListener("keyup", onKeyUp);
|
|
||||||
field.addEventListener("input", onInput);
|
|
||||||
field.addEventListener("focus", onFocus);
|
|
||||||
field.addEventListener("blur", onBlur);
|
|
||||||
field.addEventListener("paste", onPaste);
|
|
||||||
field.addEventListener("copy", onCutOrCopy);
|
|
||||||
field.addEventListener("oncut", onCutOrCopy);
|
|
||||||
field.innerHTML = content;
|
|
||||||
|
|
||||||
if (fieldContainsInlineContent(field)) {
|
|
||||||
field.appendChild(document.createElement("br"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return [name, field];
|
get fieldHTML(): string {
|
||||||
|
return containsInlineContent(this) && this.innerHTML.endsWith("<br>")
|
||||||
|
? this.innerHTML.slice(0, -4) // trim trailing <br>
|
||||||
|
: this.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.setAttribute("contenteditable", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("anki-editable", Editable);
|
||||||
|
|
||||||
|
class EditingArea extends HTMLDivElement {
|
||||||
|
editable: Editable;
|
||||||
|
baseStyle: HTMLStyleElement;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.attachShadow({ mode: "open" });
|
||||||
|
this.className = "field";
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
this.editable = document.createElement("anki-editable") as Editable;
|
||||||
|
this.shadowRoot.appendChild(this.editable);
|
||||||
|
}
|
||||||
|
|
||||||
|
get ord(): number {
|
||||||
|
return Number(this.getAttribute("ord"));
|
||||||
|
}
|
||||||
|
|
||||||
|
set fieldHTML(content: string) {
|
||||||
|
this.editable.fieldHTML = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
get fieldHTML(): string {
|
||||||
|
return this.editable.fieldHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
connectedCallback(): void {
|
||||||
|
this.addEventListener("keydown", onKey);
|
||||||
|
this.addEventListener("keyup", onKeyUp);
|
||||||
|
this.addEventListener("input", onInput);
|
||||||
|
this.addEventListener("focus", onFocus);
|
||||||
|
this.addEventListener("blur", onBlur);
|
||||||
|
this.addEventListener("paste", onPaste);
|
||||||
|
this.addEventListener("copy", onCutOrCopy);
|
||||||
|
this.addEventListener("oncut", onCutOrCopy);
|
||||||
|
|
||||||
|
const baseStyleSheet = this.baseStyle.sheet as CSSStyleSheet;
|
||||||
|
baseStyleSheet.insertRule("anki-editable {}", 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback(): void {
|
||||||
|
this.removeEventListener("keydown", onKey);
|
||||||
|
this.removeEventListener("keyup", onKeyUp);
|
||||||
|
this.removeEventListener("input", onInput);
|
||||||
|
this.removeEventListener("focus", onFocus);
|
||||||
|
this.removeEventListener("blur", onBlur);
|
||||||
|
this.removeEventListener("paste", onPaste);
|
||||||
|
this.removeEventListener("copy", onCutOrCopy);
|
||||||
|
this.removeEventListener("oncut", onCutOrCopy);
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize(color: string, content: string): void {
|
||||||
|
this.setBaseColor(color);
|
||||||
|
this.editable.fieldHTML = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
setBaseColor(color: string): void {
|
||||||
|
const styleSheet = this.baseStyle.sheet as CSSStyleSheet;
|
||||||
|
const firstRule = styleSheet.cssRules[0] as CSSStyleRule;
|
||||||
|
firstRule.style.color = color;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = fontFamily;
|
||||||
|
firstRule.style.fontSize = fontSize;
|
||||||
|
firstRule.style.direction = direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
isRightToLeft(): boolean {
|
||||||
|
return this.editable.style.direction === "rtl";
|
||||||
|
}
|
||||||
|
|
||||||
|
getSelection(): Selection {
|
||||||
|
return this.shadowRoot.getSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
focusEditable(): void {
|
||||||
|
this.editable.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
blurEditable(): void {
|
||||||
|
this.editable.blur();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("anki-editing-area", EditingArea, { extends: "div" });
|
||||||
|
|
||||||
|
class EditorField extends HTMLDivElement {
|
||||||
|
labelContainer: HTMLDivElement;
|
||||||
|
label: HTMLSpanElement;
|
||||||
|
editingArea: EditingArea;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this.labelContainer = document.createElement("div");
|
||||||
|
this.labelContainer.className = "fname";
|
||||||
|
this.appendChild(this.labelContainer);
|
||||||
|
|
||||||
|
this.label = document.createElement("span");
|
||||||
|
this.label.className = "fieldname";
|
||||||
|
this.labelContainer.appendChild(this.label);
|
||||||
|
|
||||||
|
this.editingArea = document.createElement("div", {
|
||||||
|
is: "anki-editing-area",
|
||||||
|
}) as EditingArea;
|
||||||
|
this.appendChild(this.editingArea);
|
||||||
|
}
|
||||||
|
|
||||||
|
static get observedAttributes(): string[] {
|
||||||
|
return ["ord"];
|
||||||
|
}
|
||||||
|
|
||||||
|
set ord(n: number) {
|
||||||
|
this.setAttribute("ord", String(n));
|
||||||
|
}
|
||||||
|
|
||||||
|
attributeChangedCallback(name: string, _oldValue: string, newValue: string): void {
|
||||||
|
switch (name) {
|
||||||
|
case "ord":
|
||||||
|
this.editingArea.setAttribute("ord", newValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initialize(label: string, color: string, content: string): void {
|
||||||
|
this.label.innerText = label;
|
||||||
|
this.editingArea.initialize(color, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
setBaseStyling(fontFamily: string, fontSize: string, direction: string): void {
|
||||||
|
this.editingArea.setBaseStyling(fontFamily, fontSize, direction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("anki-editor-field", EditorField, { extends: "div" });
|
||||||
|
|
||||||
|
function adjustFieldAmount(amount: number): void {
|
||||||
|
const fieldsContainer = document.getElementById("fields");
|
||||||
|
|
||||||
|
while (fieldsContainer.childElementCount < amount) {
|
||||||
|
const newField = document.createElement("div", {
|
||||||
|
is: "anki-editor-field",
|
||||||
|
}) as EditorField;
|
||||||
|
newField.ord = fieldsContainer.childElementCount;
|
||||||
|
fieldsContainer.appendChild(newField);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (fieldsContainer.childElementCount > amount) {
|
||||||
|
fieldsContainer.removeChild(fieldsContainer.lastElementChild);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function forEditorField<T>(
|
||||||
|
values: T[],
|
||||||
|
func: (field: EditorField, value: T) => void
|
||||||
|
): void {
|
||||||
|
const fields = document.getElementById("fields").children;
|
||||||
|
for (let i = 0; i < fields.length; i++) {
|
||||||
|
const field = fields[i] as EditorField;
|
||||||
|
func(field, values[i]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setFields(fields: [string, string][]): void {
|
function setFields(fields: [string, string][]): void {
|
||||||
|
@ -481,49 +624,33 @@ function setFields(fields: [string, string][]): void {
|
||||||
.getComputedStyle(document.documentElement)
|
.getComputedStyle(document.documentElement)
|
||||||
.getPropertyValue("--text-fg");
|
.getPropertyValue("--text-fg");
|
||||||
|
|
||||||
const elements = fields.flatMap(([name, fieldcontent], index: number) =>
|
adjustFieldAmount(fields.length);
|
||||||
createField(index, name, color, fieldcontent)
|
forEditorField(fields, (field, [name, fieldContent]) =>
|
||||||
|
field.initialize(name, color, fieldContent)
|
||||||
);
|
);
|
||||||
|
|
||||||
const fieldsContainer = document.getElementById("fields");
|
|
||||||
// can be replaced with ParentNode.replaceChildren in Chrome 86+
|
|
||||||
while (fieldsContainer.firstChild) {
|
|
||||||
fieldsContainer.removeChild(fieldsContainer.firstChild);
|
|
||||||
}
|
|
||||||
for (const element of elements) {
|
|
||||||
fieldsContainer.appendChild(element);
|
|
||||||
}
|
|
||||||
|
|
||||||
maybeDisableButtons();
|
maybeDisableButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
function setBackgrounds(cols: "dupe"[]) {
|
function setBackgrounds(cols: ("dupe" | "")[]) {
|
||||||
for (let i = 0; i < cols.length; i++) {
|
forEditorField(cols, (field, value) =>
|
||||||
const element = document.querySelector(`#f${i}`);
|
field.editingArea.classList.toggle("dupe", value === "dupe")
|
||||||
element.classList.toggle("dupe", cols[i] === "dupe");
|
);
|
||||||
}
|
document
|
||||||
|
.querySelector("#dupes")
|
||||||
|
.classList.toggle("is-inactive", !cols.includes("dupe"));
|
||||||
}
|
}
|
||||||
|
|
||||||
function setFonts(fonts: [string, number, boolean][]): void {
|
function setFonts(fonts: [string, number, boolean][]): void {
|
||||||
for (let i = 0; i < fonts.length; i++) {
|
forEditorField(fonts, (field, [fontFamily, fontSize, isRtl]) => {
|
||||||
const n = $(`#f${i}`);
|
field.setBaseStyling(fontFamily, `${fontSize}px`, isRtl ? "rtl" : "ltr");
|
||||||
n.css("font-family", fonts[i][0]).css("font-size", fonts[i][1]);
|
});
|
||||||
n[0].dir = fonts[i][2] ? "rtl" : "ltr";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function setNoteId(id: number): void {
|
function setNoteId(id: number): void {
|
||||||
currentNoteId = id;
|
currentNoteId = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
function showDupes(): void {
|
|
||||||
$("#dupes").show();
|
|
||||||
}
|
|
||||||
|
|
||||||
function hideDupes(): void {
|
|
||||||
$("#dupes").hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
let pasteHTML = function (
|
let pasteHTML = function (
|
||||||
html: string,
|
html: string,
|
||||||
internal: boolean,
|
internal: boolean,
|
||||||
|
|
|
@ -73,8 +73,9 @@ audio = (
|
||||||
|
|
||||||
_html = """
|
_html = """
|
||||||
<style>
|
<style>
|
||||||
html { background: %s; }
|
:root {
|
||||||
#topbutsOuter { background: %s; }
|
--bg-color: %s;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
<div>
|
<div>
|
||||||
<div id="topbutsOuter">
|
<div id="topbutsOuter">
|
||||||
|
@ -82,11 +83,9 @@ html { background: %s; }
|
||||||
</div>
|
</div>
|
||||||
<div id="fields">
|
<div id="fields">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div id="dupes" class="is-inactive">
|
||||||
<div id="dupes" style="display:none;">
|
<a href="#" onclick="pycmd('dupes');return false;">%s</a>
|
||||||
<a href="#" onclick="pycmd('dupes');return false;">
|
</div>
|
||||||
%s
|
|
||||||
</a>
|
|
||||||
</div>
|
</div>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -219,7 +218,7 @@ class Editor:
|
||||||
bgcol = self.mw.app.palette().window().color().name() # type: ignore
|
bgcol = self.mw.app.palette().window().color().name() # type: ignore
|
||||||
# then load page
|
# then load page
|
||||||
self.web.stdHtml(
|
self.web.stdHtml(
|
||||||
_html % (bgcol, bgcol, topbuts, tr(TR.EDITING_SHOW_DUPLICATES)),
|
_html % (bgcol, topbuts, tr(TR.EDITING_SHOW_DUPLICATES)),
|
||||||
css=["css/editor.css"],
|
css=["css/editor.css"],
|
||||||
js=["js/vendor/jquery.min.js", "js/editor.js"],
|
js=["js/vendor/jquery.min.js", "js/editor.js"],
|
||||||
context=self,
|
context=self,
|
||||||
|
@ -505,7 +504,7 @@ class Editor:
|
||||||
self.web.setFocus()
|
self.web.setFocus()
|
||||||
gui_hooks.editor_did_load_note(self)
|
gui_hooks.editor_did_load_note(self)
|
||||||
|
|
||||||
js = "setFields(%s); setFonts(%s); focusField(%s); setNoteId(%s)" % (
|
js = "setFields(%s); setFonts(%s); focusField(%s); setNoteId(%s);" % (
|
||||||
json.dumps(data),
|
json.dumps(data),
|
||||||
json.dumps(self.fonts()),
|
json.dumps(self.fonts()),
|
||||||
json.dumps(focusTo),
|
json.dumps(focusTo),
|
||||||
|
@ -534,9 +533,7 @@ class Editor:
|
||||||
err = self.note.dupeOrEmpty()
|
err = self.note.dupeOrEmpty()
|
||||||
if err == 2:
|
if err == 2:
|
||||||
cols[0] = "dupe"
|
cols[0] = "dupe"
|
||||||
self.web.eval("showDupes();")
|
|
||||||
else:
|
|
||||||
self.web.eval("hideDupes();")
|
|
||||||
self.web.eval("setBackgrounds(%s);" % json.dumps(cols))
|
self.web.eval("setBackgrounds(%s);" % json.dumps(cols))
|
||||||
|
|
||||||
def showDupes(self):
|
def showDupes(self):
|
||||||
|
|
Loading…
Reference in a new issue