mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 14:32:22 -04:00
Merge branch 'main' into color-palette
This commit is contained in:
parent
e036edd584
commit
ac4c88afdc
20 changed files with 337 additions and 205 deletions
|
@ -58,6 +58,7 @@ editing-toggle-visual-editor = Toggle Visual Editor
|
||||||
editing-underline-text = Underline text
|
editing-underline-text = Underline text
|
||||||
editing-unordered-list = Unordered list
|
editing-unordered-list = Unordered list
|
||||||
editing-warning-cloze-deletions-will-not-work = Warning, cloze deletions will not work until you switch the type at the top to Cloze.
|
editing-warning-cloze-deletions-will-not-work = Warning, cloze deletions will not work until you switch the type at the top to Cloze.
|
||||||
|
editing-toggle-mathjax-rendering = Toggle MathJax Rendering
|
||||||
|
|
||||||
## You don't need to translate these strings, as they will be replaced with different ones soon.
|
## You don't need to translate these strings, as they will be replaced with different ones soon.
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ fields-font = Font:
|
||||||
fields-new-position-1 = New position (1...{ $val }):
|
fields-new-position-1 = New position (1...{ $val }):
|
||||||
fields-notes-require-at-least-one-field = Notes require at least one field.
|
fields-notes-require-at-least-one-field = Notes require at least one field.
|
||||||
fields-reverse-text-direction-rtl = Reverse text direction (RTL)
|
fields-reverse-text-direction-rtl = Reverse text direction (RTL)
|
||||||
|
fields-html-by-default = Use HTML editor by default
|
||||||
fields-size = Size:
|
fields-size = Size:
|
||||||
fields-sort-by-this-field-in-the = Sort by this field in the browser
|
fields-sort-by-this-field-in-the = Sort by this field in the browser
|
||||||
fields-that-field-name-is-already-used = That field name is already used.
|
fields-that-field-name-is-already-used = That field name is already used.
|
||||||
|
|
|
@ -72,6 +72,7 @@ message Notetype {
|
||||||
string font_name = 3;
|
string font_name = 3;
|
||||||
uint32 font_size = 4;
|
uint32 font_size = 4;
|
||||||
string description = 5;
|
string description = 5;
|
||||||
|
bool plain_text = 6;
|
||||||
|
|
||||||
bytes other = 255;
|
bytes other = 255;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,8 +14,8 @@
|
||||||
body {
|
body {
|
||||||
color: var(--text-fg);
|
color: var(--text-fg);
|
||||||
background: var(--window-bg);
|
background: var(--window-bg);
|
||||||
margin: 1em;
|
|
||||||
transition: opacity 0.5s ease-out;
|
transition: opacity 0.5s ease-out;
|
||||||
|
margin: 2em;
|
||||||
overscroll-behavior: none;
|
overscroll-behavior: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,11 +24,6 @@ a {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
|
||||||
margin: 2em;
|
|
||||||
overscroll-behavior: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
margin-bottom: 0.2em;
|
margin-bottom: 0.2em;
|
||||||
}
|
}
|
||||||
|
|
|
@ -125,6 +125,7 @@ class Editor:
|
||||||
self.card: Card | None = None
|
self.card: Card | None = None
|
||||||
self._init_links()
|
self._init_links()
|
||||||
self.setupOuter()
|
self.setupOuter()
|
||||||
|
self.add_webview()
|
||||||
self.setupWeb()
|
self.setupWeb()
|
||||||
self.setupShortcuts()
|
self.setupShortcuts()
|
||||||
gui_hooks.editor_did_init(self)
|
gui_hooks.editor_did_init(self)
|
||||||
|
@ -139,11 +140,12 @@ class Editor:
|
||||||
self.widget.setLayout(l)
|
self.widget.setLayout(l)
|
||||||
self.outerLayout = l
|
self.outerLayout = l
|
||||||
|
|
||||||
def setupWeb(self) -> None:
|
def add_webview(self) -> None:
|
||||||
self.web = EditorWebView(self.widget, self)
|
self.web = EditorWebView(self.widget, self)
|
||||||
self.web.set_bridge_command(self.onBridgeCmd, self)
|
self.web.set_bridge_command(self.onBridgeCmd, self)
|
||||||
self.outerLayout.addWidget(self.web, 1)
|
self.outerLayout.addWidget(self.web, 1)
|
||||||
|
|
||||||
|
def setupWeb(self) -> None:
|
||||||
if self.editorMode == EditorMode.ADD_CARDS:
|
if self.editorMode == EditorMode.ADD_CARDS:
|
||||||
file = "note_creator"
|
file = "note_creator"
|
||||||
elif self.editorMode == EditorMode.BROWSER:
|
elif self.editorMode == EditorMode.BROWSER:
|
||||||
|
@ -499,6 +501,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
||||||
]
|
]
|
||||||
|
|
||||||
flds = self.note.note_type()["flds"]
|
flds = self.note.note_type()["flds"]
|
||||||
|
plain_texts = [fld.get("plainText", False) for fld in flds]
|
||||||
descriptions = [fld.get("description", "") for fld in flds]
|
descriptions = [fld.get("description", "") for fld in flds]
|
||||||
|
|
||||||
self.widget.show()
|
self.widget.show()
|
||||||
|
@ -519,14 +522,26 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
||||||
text_color = self.mw.pm.profile.get("lastTextColor", "#00f")
|
text_color = self.mw.pm.profile.get("lastTextColor", "#00f")
|
||||||
highlight_color = self.mw.pm.profile.get("lastHighlightColor", "#00f")
|
highlight_color = self.mw.pm.profile.get("lastHighlightColor", "#00f")
|
||||||
|
|
||||||
js = "setFields({}); setDescriptions({}); setFonts({}); focusField({}); setNoteId({}); setColorButtons({}); setTags({}); ".format(
|
js = """
|
||||||
|
setFields({});
|
||||||
|
setPlainTexts({});
|
||||||
|
setDescriptions({});
|
||||||
|
setFonts({});
|
||||||
|
focusField({});
|
||||||
|
setNoteId({});
|
||||||
|
setColorButtons({});
|
||||||
|
setTags({});
|
||||||
|
setMathjaxEnabled({});
|
||||||
|
""".format(
|
||||||
json.dumps(data),
|
json.dumps(data),
|
||||||
|
json.dumps(plain_texts),
|
||||||
json.dumps(descriptions),
|
json.dumps(descriptions),
|
||||||
json.dumps(self.fonts()),
|
json.dumps(self.fonts()),
|
||||||
json.dumps(focusTo),
|
json.dumps(focusTo),
|
||||||
json.dumps(self.note.id),
|
json.dumps(self.note.id),
|
||||||
json.dumps([text_color, highlight_color]),
|
json.dumps([text_color, highlight_color]),
|
||||||
json.dumps(self.note.tags),
|
json.dumps(self.note.tags),
|
||||||
|
json.dumps(self.mw.col.get_config("renderMathjax", True)),
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.addMode:
|
if self.addMode:
|
||||||
|
@ -1130,6 +1145,14 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
||||||
def insertMathjaxChemistry(self) -> None:
|
def insertMathjaxChemistry(self) -> None:
|
||||||
self.web.eval("wrap('\\\\(\\\\ce{', '}\\\\)');")
|
self.web.eval("wrap('\\\\(\\\\ce{', '}\\\\)');")
|
||||||
|
|
||||||
|
def toggleMathjax(self) -> None:
|
||||||
|
self.mw.col.set_config(
|
||||||
|
"renderMathjax", not self.mw.col.get_config("renderMathjax", False)
|
||||||
|
)
|
||||||
|
# hackily redraw the page
|
||||||
|
self.setupWeb()
|
||||||
|
self.loadNoteKeepingFocus()
|
||||||
|
|
||||||
# Links from HTML
|
# Links from HTML
|
||||||
######################################################################
|
######################################################################
|
||||||
|
|
||||||
|
@ -1156,6 +1179,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
||||||
mathjaxInline=Editor.insertMathjaxInline,
|
mathjaxInline=Editor.insertMathjaxInline,
|
||||||
mathjaxBlock=Editor.insertMathjaxBlock,
|
mathjaxBlock=Editor.insertMathjaxBlock,
|
||||||
mathjaxChemistry=Editor.insertMathjaxChemistry,
|
mathjaxChemistry=Editor.insertMathjaxChemistry,
|
||||||
|
toggleMathjax=Editor.toggleMathjax,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -243,6 +243,7 @@ class FieldDialog(QDialog):
|
||||||
f.fontSize.setValue(fld["size"])
|
f.fontSize.setValue(fld["size"])
|
||||||
f.sortField.setChecked(self.model["sortf"] == fld["ord"])
|
f.sortField.setChecked(self.model["sortf"] == fld["ord"])
|
||||||
f.rtl.setChecked(fld["rtl"])
|
f.rtl.setChecked(fld["rtl"])
|
||||||
|
f.plainTextByDefault.setChecked(fld["plainText"])
|
||||||
f.fieldDescription.setText(fld.get("description", ""))
|
f.fieldDescription.setText(fld.get("description", ""))
|
||||||
|
|
||||||
def saveField(self) -> None:
|
def saveField(self) -> None:
|
||||||
|
@ -264,6 +265,10 @@ class FieldDialog(QDialog):
|
||||||
if fld["rtl"] != rtl:
|
if fld["rtl"] != rtl:
|
||||||
fld["rtl"] = rtl
|
fld["rtl"] = rtl
|
||||||
self.change_tracker.mark_basic()
|
self.change_tracker.mark_basic()
|
||||||
|
plain_text = f.plainTextByDefault.isChecked()
|
||||||
|
if fld["plainText"] != plain_text:
|
||||||
|
fld["plainText"] = plain_text
|
||||||
|
self.change_tracker.mark_basic()
|
||||||
desc = f.fieldDescription.text()
|
desc = f.fieldDescription.text()
|
||||||
if fld.get("description", "") != desc:
|
if fld.get("description", "") != desc:
|
||||||
fld["description"] = desc
|
fld["description"] = desc
|
||||||
|
|
|
@ -6,8 +6,8 @@
|
||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>586</width>
|
<width>598</width>
|
||||||
<height>376</height>
|
<height>378</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
|
@ -84,13 +84,6 @@
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<layout class="QGridLayout" name="_2">
|
<layout class="QGridLayout" name="_2">
|
||||||
<item row="1" column="0">
|
|
||||||
<widget class="QLabel" name="label_font">
|
|
||||||
<property name="text">
|
|
||||||
<string>fields_editing_font</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="3" column="1">
|
<item row="3" column="1">
|
||||||
<widget class="QCheckBox" name="rtl">
|
<widget class="QCheckBox" name="rtl">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
@ -98,13 +91,27 @@
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="2">
|
<item row="1" column="1">
|
||||||
<widget class="QSpinBox" name="fontSize">
|
<widget class="QFontComboBox" name="fontFamily">
|
||||||
<property name="minimum">
|
<property name="minimumSize">
|
||||||
<number>5</number>
|
<size>
|
||||||
|
<width>0</width>
|
||||||
|
<height>25</height>
|
||||||
|
</size>
|
||||||
</property>
|
</property>
|
||||||
<property name="maximum">
|
</widget>
|
||||||
<number>300</number>
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QLabel" name="label_font">
|
||||||
|
<property name="text">
|
||||||
|
<string>fields_editing_font</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="1" colspan="2">
|
||||||
|
<widget class="QLineEdit" name="fieldDescription">
|
||||||
|
<property name="placeholderText">
|
||||||
|
<string>fields_description_placeholder</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
@ -115,13 +122,6 @@
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="1">
|
|
||||||
<widget class="QRadioButton" name="sortField">
|
|
||||||
<property name="text">
|
|
||||||
<string>fields_sort_by_this_field_in_the</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="0" column="0">
|
<item row="0" column="0">
|
||||||
<widget class="QLabel" name="label_description">
|
<widget class="QLabel" name="label_description">
|
||||||
<property name="sizePolicy">
|
<property name="sizePolicy">
|
||||||
|
@ -135,20 +135,30 @@
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="1">
|
<item row="1" column="2">
|
||||||
<widget class="QFontComboBox" name="fontFamily">
|
<widget class="QSpinBox" name="fontSize">
|
||||||
<property name="minimumSize">
|
<property name="minimum">
|
||||||
<size>
|
<number>5</number>
|
||||||
<width>0</width>
|
</property>
|
||||||
<height>25</height>
|
<property name="maximum">
|
||||||
</size>
|
<number>300</number>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="0" column="1" colspan="2">
|
<item row="2" column="1">
|
||||||
<widget class="QLineEdit" name="fieldDescription">
|
<widget class="QRadioButton" name="sortField">
|
||||||
<property name="placeholderText">
|
<property name="text">
|
||||||
<string>fields_description_placeholder</string>
|
<string>fields_sort_by_this_field_in_the</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="4" column="1">
|
||||||
|
<widget class="QCheckBox" name="plainTextByDefault">
|
||||||
|
<property name="enabled">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>fields_html_by_default</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
|
|
@ -42,6 +42,7 @@ impl NoteField {
|
||||||
config: NoteFieldConfig {
|
config: NoteFieldConfig {
|
||||||
sticky: false,
|
sticky: false,
|
||||||
rtl: false,
|
rtl: false,
|
||||||
|
plain_text: false,
|
||||||
font_name: "Arial".into(),
|
font_name: "Arial".into(),
|
||||||
font_size: 20,
|
font_size: 20,
|
||||||
description: "".into(),
|
description: "".into(),
|
||||||
|
|
|
@ -161,7 +161,7 @@ impl From<Notetype> for NotetypeSchema11 {
|
||||||
|
|
||||||
/// See [crate::deckconfig::schema11::clear_other_duplicates()].
|
/// See [crate::deckconfig::schema11::clear_other_duplicates()].
|
||||||
fn clear_other_field_duplicates(other: &mut HashMap<String, Value>) {
|
fn clear_other_field_duplicates(other: &mut HashMap<String, Value>) {
|
||||||
for key in &["description"] {
|
for key in &["description", "plainText"] {
|
||||||
other.remove(*key);
|
other.remove(*key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -195,6 +195,7 @@ impl From<CardRequirement> for CardRequirementSchema11 {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct NoteFieldSchema11 {
|
pub struct NoteFieldSchema11 {
|
||||||
pub(crate) name: String,
|
pub(crate) name: String,
|
||||||
pub(crate) ord: Option<u16>,
|
pub(crate) ord: Option<u16>,
|
||||||
|
@ -211,6 +212,9 @@ pub struct NoteFieldSchema11 {
|
||||||
#[serde(default, deserialize_with = "default_on_invalid")]
|
#[serde(default, deserialize_with = "default_on_invalid")]
|
||||||
pub(crate) description: String,
|
pub(crate) description: String,
|
||||||
|
|
||||||
|
#[serde(default, deserialize_with = "default_on_invalid")]
|
||||||
|
pub(crate) plain_text: bool,
|
||||||
|
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub(crate) other: HashMap<String, Value>,
|
pub(crate) other: HashMap<String, Value>,
|
||||||
}
|
}
|
||||||
|
@ -222,6 +226,7 @@ impl Default for NoteFieldSchema11 {
|
||||||
ord: None,
|
ord: None,
|
||||||
sticky: false,
|
sticky: false,
|
||||||
rtl: false,
|
rtl: false,
|
||||||
|
plain_text: false,
|
||||||
font: "Arial".to_string(),
|
font: "Arial".to_string(),
|
||||||
size: 20,
|
size: 20,
|
||||||
description: String::new(),
|
description: String::new(),
|
||||||
|
@ -238,6 +243,7 @@ impl From<NoteFieldSchema11> for NoteField {
|
||||||
config: NoteFieldConfig {
|
config: NoteFieldConfig {
|
||||||
sticky: f.sticky,
|
sticky: f.sticky,
|
||||||
rtl: f.rtl,
|
rtl: f.rtl,
|
||||||
|
plain_text: f.plain_text,
|
||||||
font_name: f.font,
|
font_name: f.font,
|
||||||
font_size: f.size as u32,
|
font_size: f.size as u32,
|
||||||
description: f.description,
|
description: f.description,
|
||||||
|
@ -259,6 +265,7 @@ impl From<NoteField> for NoteFieldSchema11 {
|
||||||
ord: p.ord.map(|o| o as u16),
|
ord: p.ord.map(|o| o as u16),
|
||||||
sticky: conf.sticky,
|
sticky: conf.sticky,
|
||||||
rtl: conf.rtl,
|
rtl: conf.rtl,
|
||||||
|
plain_text: conf.plain_text,
|
||||||
font: conf.font_name,
|
font: conf.font_name,
|
||||||
size: conf.font_size as u16,
|
size: conf.font_size as u16,
|
||||||
description: conf.description,
|
description: conf.description,
|
||||||
|
|
|
@ -32,7 +32,7 @@ $utilities: (
|
||||||
flex-basis: 75%;
|
flex-basis: 75%;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
body {
|
||||||
overscroll-behavior: none;
|
overscroll-behavior: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -38,17 +38,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
onDestroy(() => tooltipObject?.dispose());
|
onDestroy(() => tooltipObject?.dispose());
|
||||||
|
|
||||||
// hack to update field description tooltips
|
|
||||||
let previousTooltip: string = tooltip;
|
|
||||||
$: if (tooltip !== previousTooltip) {
|
|
||||||
previousTooltip = tooltip;
|
|
||||||
if (tooltipObject !== undefined) {
|
|
||||||
const element: HTMLElement = tooltipObject["_element"];
|
|
||||||
tooltipObject.dispose();
|
|
||||||
createTooltip(element);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<slot {createTooltip} {tooltipObject} />
|
<slot {createTooltip} {tooltipObject} />
|
||||||
|
|
|
@ -20,6 +20,10 @@ function trimBreaks(text: string): string {
|
||||||
.replace(/\n*$/, "");
|
.replace(/\n*$/, "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const mathjaxConfig = {
|
||||||
|
enabled: true,
|
||||||
|
};
|
||||||
|
|
||||||
export const Mathjax: DecoratedElementConstructor = class Mathjax
|
export const Mathjax: DecoratedElementConstructor = class Mathjax
|
||||||
extends HTMLElement
|
extends HTMLElement
|
||||||
implements DecoratedElement
|
implements DecoratedElement
|
||||||
|
@ -41,6 +45,9 @@ export const Mathjax: DecoratedElementConstructor = class Mathjax
|
||||||
}
|
}
|
||||||
|
|
||||||
static toUndecorated(stored: string): string {
|
static toUndecorated(stored: string): string {
|
||||||
|
if (!mathjaxConfig.enabled) {
|
||||||
|
return stored;
|
||||||
|
}
|
||||||
return stored
|
return stored
|
||||||
.replace(mathjaxBlockDelimiterPattern, (_match: string, text: string) => {
|
.replace(mathjaxBlockDelimiterPattern, (_match: string, text: string) => {
|
||||||
const trimmed = trimBreaks(text);
|
const trimmed = trimBreaks(text);
|
||||||
|
|
|
@ -12,6 +12,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
fontFamily: string;
|
fontFamily: string;
|
||||||
fontSize: number;
|
fontSize: number;
|
||||||
direction: "ltr" | "rtl";
|
direction: "ltr" | "rtl";
|
||||||
|
plainText: boolean;
|
||||||
description: string;
|
description: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,10 +32,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
<style>
|
<style>
|
||||||
.field-description {
|
.field-description {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
inset: 0;
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
|
||||||
cursor: text;
|
|
||||||
opacity: 0.4;
|
opacity: 0.4;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
/* same as in ContentEditable */
|
/* same as in ContentEditable */
|
||||||
padding: 6px;
|
padding: 6px;
|
||||||
|
|
|
@ -109,21 +109,29 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
fieldNames = newFieldNames;
|
fieldNames = newFieldNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let plainTexts: boolean[] = [];
|
||||||
|
let richTextsHidden: boolean[] = [];
|
||||||
|
let plainTextsHidden: boolean[] = [];
|
||||||
|
|
||||||
|
export function setPlainTexts(fs: boolean[]): void {
|
||||||
|
richTextsHidden = plainTexts = fs;
|
||||||
|
plainTextsHidden = Array.from(fs, (v) => !v);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setMathjaxEnabled(enabled: boolean): void {
|
||||||
|
mathjaxConfig.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
let fieldDescriptions: string[] = [];
|
let fieldDescriptions: string[] = [];
|
||||||
export function setDescriptions(fs: string[]): void {
|
export function setDescriptions(fs: string[]): void {
|
||||||
fieldDescriptions = fs;
|
fieldDescriptions = fs;
|
||||||
}
|
}
|
||||||
|
|
||||||
let fonts: [string, number, boolean][] = [];
|
let fonts: [string, number, boolean][] = [];
|
||||||
let richTextsHidden: boolean[] = [];
|
|
||||||
let plainTextsHidden: boolean[] = [];
|
|
||||||
const fields = clearableArray<EditorFieldAPI>();
|
const fields = clearableArray<EditorFieldAPI>();
|
||||||
|
|
||||||
export function setFonts(fs: [string, number, boolean][]): void {
|
export function setFonts(fs: [string, number, boolean][]): void {
|
||||||
fonts = fs;
|
fonts = fs;
|
||||||
|
|
||||||
richTextsHidden = fonts.map((_, index) => richTextsHidden[index] ?? false);
|
|
||||||
plainTextsHidden = fonts.map((_, index) => plainTextsHidden[index] ?? true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function focusField(index: number | null): void {
|
export function focusField(index: number | null): void {
|
||||||
|
@ -171,6 +179,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
$: fieldsData = fieldNames.map((name, index) => ({
|
$: fieldsData = fieldNames.map((name, index) => ({
|
||||||
name,
|
name,
|
||||||
|
plainText: plainTexts[index],
|
||||||
description: fieldDescriptions[index],
|
description: fieldDescriptions[index],
|
||||||
fontFamily: quoteFontFamily(fonts[index][0]),
|
fontFamily: quoteFontFamily(fonts[index][0]),
|
||||||
fontSize: fonts[index][1],
|
fontSize: fonts[index][1],
|
||||||
|
@ -223,6 +232,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
const toolbar: Partial<EditorToolbarAPI> = {};
|
const toolbar: Partial<EditorToolbarAPI> = {};
|
||||||
|
|
||||||
|
import { mathjaxConfig } from "../editable/mathjax-element";
|
||||||
import { wrapInternal } from "../lib/wrap";
|
import { wrapInternal } from "../lib/wrap";
|
||||||
import * as oldEditorAdapter from "./old-editor-adapter";
|
import * as oldEditorAdapter from "./old-editor-adapter";
|
||||||
|
|
||||||
|
@ -239,6 +249,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
Object.assign(globalThis, {
|
Object.assign(globalThis, {
|
||||||
setFields,
|
setFields,
|
||||||
|
setPlainTexts,
|
||||||
setDescriptions,
|
setDescriptions,
|
||||||
setFonts,
|
setFonts,
|
||||||
focusField,
|
focusField,
|
||||||
|
@ -250,6 +261,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
getNoteId,
|
getNoteId,
|
||||||
setNoteId,
|
setNoteId,
|
||||||
wrap,
|
wrap,
|
||||||
|
setMathjaxEnabled,
|
||||||
...oldEditorAdapter,
|
...oldEditorAdapter,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import Popover from "../../components/Popover.svelte";
|
import Popover from "../../components/Popover.svelte";
|
||||||
import Shortcut from "../../components/Shortcut.svelte";
|
import Shortcut from "../../components/Shortcut.svelte";
|
||||||
import WithFloating from "../../components/WithFloating.svelte";
|
import WithFloating from "../../components/WithFloating.svelte";
|
||||||
|
import { mathjaxConfig } from "../../editable/mathjax-element";
|
||||||
|
import { bridgeCommand } from "../../lib/bridgecommand";
|
||||||
import * as tr from "../../lib/ftl";
|
import * as tr from "../../lib/ftl";
|
||||||
import { getPlatformString } from "../../lib/shortcuts";
|
import { getPlatformString } from "../../lib/shortcuts";
|
||||||
import { wrapInternal } from "../../lib/wrap";
|
import { wrapInternal } from "../../lib/wrap";
|
||||||
|
@ -50,6 +52,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
surround("[$$]", "[/$$]");
|
surround("[$$]", "[/$$]");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleShowMathjax(): void {
|
||||||
|
mathjaxConfig.enabled = !mathjaxConfig.enabled;
|
||||||
|
bridgeCommand("toggleMathjax");
|
||||||
|
}
|
||||||
|
|
||||||
type LatexItem = [() => void, string, string];
|
type LatexItem = [() => void, string, string];
|
||||||
|
|
||||||
const dropdownItems: LatexItem[] = [
|
const dropdownItems: LatexItem[] = [
|
||||||
|
@ -93,6 +100,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<Shortcut {keyCombination} on:action={callback} />
|
<Shortcut {keyCombination} on:action={callback} />
|
||||||
{/each}
|
{/each}
|
||||||
|
<DropdownItem on:click={toggleShowMathjax}>
|
||||||
|
<span>{tr.editingToggleMathjaxRendering()}</span>
|
||||||
|
</DropdownItem>
|
||||||
</Popover>
|
</Popover>
|
||||||
</WithFloating>
|
</WithFloating>
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
import Checkbox from "../../components/CheckBox.svelte";
|
import CheckBox from "../../components/CheckBox.svelte";
|
||||||
import DropdownItem from "../../components/DropdownItem.svelte";
|
import DropdownItem from "../../components/DropdownItem.svelte";
|
||||||
import DropdownMenu from "../../components/DropdownMenu.svelte";
|
import DropdownMenu from "../../components/DropdownMenu.svelte";
|
||||||
import { withButton } from "../../components/helpers";
|
import { withButton } from "../../components/helpers";
|
||||||
|
@ -14,7 +14,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import WithDropdown from "../../components/WithDropdown.svelte";
|
import WithDropdown from "../../components/WithDropdown.svelte";
|
||||||
import type { MatchType } from "../../domlib/surround";
|
import type { MatchType } from "../../domlib/surround";
|
||||||
import * as tr from "../../lib/ftl";
|
import * as tr from "../../lib/ftl";
|
||||||
import { altPressed } from "../../lib/keys";
|
import { altPressed, shiftPressed } from "../../lib/keys";
|
||||||
import { getPlatformString } from "../../lib/shortcuts";
|
import { getPlatformString } from "../../lib/shortcuts";
|
||||||
import { singleCallback } from "../../lib/typing";
|
import { singleCallback } from "../../lib/typing";
|
||||||
import { surrounder } from "../rich-text-input";
|
import { surrounder } from "../rich-text-input";
|
||||||
|
@ -25,45 +25,22 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
const { removeFormats } = editorToolbarContext.get();
|
const { removeFormats } = editorToolbarContext.get();
|
||||||
|
|
||||||
const surroundElement = document.createElement("span");
|
function filterForKeys(formats: RemoveFormat[], value: boolean): string[] {
|
||||||
|
return formats
|
||||||
function matcher(element: HTMLElement | SVGElement, match: MatchType<never>): void {
|
.filter((format) => format.active === value)
|
||||||
if (
|
.map((format) => format.key);
|
||||||
element.tagName === "SPAN" &&
|
|
||||||
element.className.length === 0 &&
|
|
||||||
element.style.cssText.length === 0
|
|
||||||
) {
|
|
||||||
match.remove();
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
const key = "simple spans";
|
|
||||||
const format = {
|
|
||||||
matcher,
|
|
||||||
surroundElement,
|
|
||||||
};
|
|
||||||
|
|
||||||
removeFormats.update((formats) =>
|
|
||||||
formats.concat({
|
|
||||||
key,
|
|
||||||
name: key,
|
|
||||||
show: false,
|
|
||||||
active: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
let activeKeys: string[];
|
let activeKeys: string[];
|
||||||
$: activeKeys = $removeFormats
|
$: activeKeys = filterForKeys($removeFormats, true);
|
||||||
.filter((format) => format.active)
|
|
||||||
.map((format) => format.key);
|
|
||||||
|
|
||||||
let inactiveKeys: string[];
|
let inactiveKeys: string[];
|
||||||
$: inactiveKeys = $removeFormats
|
$: inactiveKeys = filterForKeys($removeFormats, false);
|
||||||
.filter((format) => !format.active)
|
|
||||||
.map((format) => format.key);
|
|
||||||
|
|
||||||
let showFormats: RemoveFormat[];
|
let showFormats: RemoveFormat[];
|
||||||
$: showFormats = $removeFormats.filter((format) => format.show);
|
$: showFormats = $removeFormats.filter(
|
||||||
|
(format: RemoveFormat): boolean => format.show,
|
||||||
|
);
|
||||||
|
|
||||||
function remove(): void {
|
function remove(): void {
|
||||||
surrounder.remove(activeKeys, inactiveKeys);
|
surrounder.remove(activeKeys, inactiveKeys);
|
||||||
|
@ -71,8 +48,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
function onItemClick(event: MouseEvent, format: RemoveFormat): void {
|
function onItemClick(event: MouseEvent, format: RemoveFormat): void {
|
||||||
if (altPressed(event)) {
|
if (altPressed(event)) {
|
||||||
|
const value = shiftPressed(event);
|
||||||
|
|
||||||
for (const format of showFormats) {
|
for (const format of showFormats) {
|
||||||
format.active = false;
|
format.active = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -84,12 +63,44 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
let disabled: boolean;
|
let disabled: boolean;
|
||||||
|
|
||||||
onMount(() =>
|
onMount(() => {
|
||||||
singleCallback(
|
const surroundElement = document.createElement("span");
|
||||||
|
|
||||||
|
function matcher(
|
||||||
|
element: HTMLElement | SVGElement,
|
||||||
|
match: MatchType<never>,
|
||||||
|
): void {
|
||||||
|
if (
|
||||||
|
element.tagName === "SPAN" &&
|
||||||
|
element.className.length === 0 &&
|
||||||
|
element.style.cssText.length === 0
|
||||||
|
) {
|
||||||
|
match.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const simpleSpans = {
|
||||||
|
matcher,
|
||||||
|
surroundElement,
|
||||||
|
};
|
||||||
|
|
||||||
|
const key = "simple spans";
|
||||||
|
|
||||||
|
removeFormats.update((formats: RemoveFormat[]): RemoveFormat[] => [
|
||||||
|
...formats,
|
||||||
|
{
|
||||||
|
key,
|
||||||
|
name: key,
|
||||||
|
show: false,
|
||||||
|
active: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
return singleCallback(
|
||||||
surrounder.active.subscribe((value) => (disabled = !value)),
|
surrounder.active.subscribe((value) => (disabled = !value)),
|
||||||
surrounder.registerFormat(key, format),
|
surrounder.registerFormat(key, simpleSpans),
|
||||||
),
|
|
||||||
);
|
);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
|
@ -117,7 +128,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
<DropdownMenu on:mousedown={(event) => event.preventDefault()}>
|
<DropdownMenu on:mousedown={(event) => event.preventDefault()}>
|
||||||
{#each showFormats as format (format.name)}
|
{#each showFormats as format (format.name)}
|
||||||
<DropdownItem on:click={(event) => onItemClick(event, format)}>
|
<DropdownItem on:click={(event) => onItemClick(event, format)}>
|
||||||
<Checkbox bind:value={format.active} />
|
<CheckBox bind:value={format.active} />
|
||||||
<span class="d-flex-inline ps-3">{format.name}</span>
|
<span class="d-flex-inline ps-3">{format.name}</span>
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
|
@ -3,6 +3,8 @@ 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
|
||||||
-->
|
-->
|
||||||
<script context="module" lang="ts">
|
<script context="module" lang="ts">
|
||||||
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
import type { ContentEditableAPI } from "../../editable/ContentEditable.svelte";
|
import type { ContentEditableAPI } from "../../editable/ContentEditable.svelte";
|
||||||
import type { InputHandlerAPI } from "../../sveltelib/input-handler";
|
import type { InputHandlerAPI } from "../../sveltelib/input-handler";
|
||||||
import type { EditingInputAPI, FocusableInputAPI } from "../EditingArea.svelte";
|
import type { EditingInputAPI, FocusableInputAPI } from "../EditingArea.svelte";
|
||||||
|
@ -38,7 +40,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
const [globalInputHandler, setupGlobalInputHandler] = useInputHandler();
|
const [globalInputHandler, setupGlobalInputHandler] = useInputHandler();
|
||||||
const [lifecycle, instances, setupLifecycleHooks] =
|
const [lifecycle, instances, setupLifecycleHooks] =
|
||||||
lifecycleHooks<RichTextInputAPI>();
|
lifecycleHooks<RichTextInputAPI>();
|
||||||
const surrounder = Surrounder.make();
|
const apiStore = writable<SurroundedAPI | null>(null);
|
||||||
|
const surrounder = Surrounder.make(apiStore);
|
||||||
|
|
||||||
registerPackage("anki/RichTextInput", {
|
registerPackage("anki/RichTextInput", {
|
||||||
context,
|
context,
|
||||||
|
@ -176,16 +179,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
function setFocus(): void {
|
function setFocus(): void {
|
||||||
$focusedInput = api;
|
$focusedInput = api;
|
||||||
surrounder.enable(api);
|
$apiStore = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeFocus(): void {
|
||||||
// We do not unset focusedInput here.
|
// We do not unset focusedInput here.
|
||||||
// If we did, UI components for the input would react the store
|
// If we did, UI components for the input would react the store
|
||||||
// being unset, even though most likely it will be set to some other
|
// being unset, even though most likely it will be set to some other
|
||||||
// field right away.
|
// field right away.
|
||||||
}
|
|
||||||
|
|
||||||
function removeFocus(): void {
|
$apiStore = null;
|
||||||
surrounder.disable();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$: pushUpdate(!hidden);
|
$: pushUpdate(!hidden);
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
// 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
|
||||||
|
|
||||||
import type { Writable } from "svelte/store";
|
import type { Readable } from "svelte/store";
|
||||||
import { get, writable } from "svelte/store";
|
import { derived, get } from "svelte/store";
|
||||||
|
|
||||||
import type { Matcher } from "../domlib/find-above";
|
import type { Matcher } from "../domlib/find-above";
|
||||||
import { findClosest } from "../domlib/find-above";
|
import { findClosest } from "../domlib/find-above";
|
||||||
import type { SurroundFormat } from "../domlib/surround";
|
import type { SurroundFormat } from "../domlib/surround";
|
||||||
import { boolMatcher, reformat, surround, unsurround } from "../domlib/surround";
|
import { boolMatcher, reformat, surround, unsurround } from "../domlib/surround";
|
||||||
import { getRange, getSelection } from "../lib/cross-browser";
|
import { getRange, getSelection } from "../lib/cross-browser";
|
||||||
|
import { asyncNoop } from "../lib/functional";
|
||||||
import { registerPackage } from "../lib/runtime-require";
|
import { registerPackage } from "../lib/runtime-require";
|
||||||
import type { TriggerItem } from "../sveltelib/handler-list";
|
import type { TriggerItem } from "../sveltelib/handler-list";
|
||||||
import type { InputHandlerAPI } from "../sveltelib/input-handler";
|
import type { InputHandlerAPI } from "../sveltelib/input-handler";
|
||||||
|
@ -67,52 +68,55 @@ export interface SurroundedAPI {
|
||||||
inputHandler: InputHandlerAPI;
|
inputHandler: InputHandlerAPI;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Surrounder<T = unknown> {
|
/**
|
||||||
static make<T>(): Surrounder<T> {
|
|
||||||
return new Surrounder();
|
|
||||||
}
|
|
||||||
|
|
||||||
private api: SurroundedAPI | null = null;
|
|
||||||
private triggers: Map<string, TriggerItem<{ event: InputEvent; text: Text }>> =
|
|
||||||
new Map();
|
|
||||||
|
|
||||||
active: Writable<boolean> = writable(false);
|
|
||||||
|
|
||||||
enable(api: SurroundedAPI): void {
|
|
||||||
this.api = api;
|
|
||||||
this.active.set(true);
|
|
||||||
|
|
||||||
for (const key of this.formats.keys()) {
|
|
||||||
this.triggers.set(
|
|
||||||
key,
|
|
||||||
this.api.inputHandler.insertText.trigger({ once: true }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* After calling disable, using any of the surrounding methods will throw an
|
* After calling disable, using any of the surrounding methods will throw an
|
||||||
* exception. Make sure to set the input before trying to use them again.
|
* exception. Make sure to set the input before trying to use them again.
|
||||||
*/
|
*/
|
||||||
disable(): void {
|
export class Surrounder<T = unknown> {
|
||||||
this.api = null;
|
#api?: SurroundedAPI;
|
||||||
this.active.set(false);
|
|
||||||
|
|
||||||
for (const [key, trigger] of this.triggers) {
|
#triggers: Map<string, TriggerItem<{ event: InputEvent; text: Text }>> = new Map();
|
||||||
|
#formats: Map<string, SurroundFormat<T>> = new Map();
|
||||||
|
|
||||||
|
active: Readable<boolean>;
|
||||||
|
|
||||||
|
private constructor(apiStore: Readable<SurroundedAPI | null>) {
|
||||||
|
this.active = derived(apiStore, (api) => Boolean(api));
|
||||||
|
|
||||||
|
apiStore.subscribe((api: SurroundedAPI | null): void => {
|
||||||
|
if (api) {
|
||||||
|
this.#api = api;
|
||||||
|
|
||||||
|
for (const key of this.#formats.keys()) {
|
||||||
|
this.#triggers.set(
|
||||||
|
key,
|
||||||
|
api.inputHandler.insertText.trigger({ once: true }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.#api = undefined;
|
||||||
|
|
||||||
|
for (const [key, trigger] of this.#triggers) {
|
||||||
trigger.off();
|
trigger.off();
|
||||||
this.triggers.delete(key);
|
this.#triggers.delete(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private async _assert_base(): Promise<HTMLElement> {
|
static make<T>(apiStore: Readable<SurroundedAPI | null>): Surrounder<T> {
|
||||||
if (!this.api) {
|
return new Surrounder(apiStore);
|
||||||
throw new Error("Surrounder: No input set");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.api.element;
|
#getBaseElement(): Promise<HTMLElement> {
|
||||||
|
if (!this.#api) {
|
||||||
|
throw new Error("Surrounder: No api set");
|
||||||
}
|
}
|
||||||
|
|
||||||
private _toggleTrigger<T>(
|
return this.#api.element;
|
||||||
|
}
|
||||||
|
|
||||||
|
#toggleTrigger<T>(
|
||||||
base: HTMLElement,
|
base: HTMLElement,
|
||||||
selection: Selection,
|
selection: Selection,
|
||||||
matcher: Matcher,
|
matcher: Matcher,
|
||||||
|
@ -135,7 +139,7 @@ export class Surrounder<T = unknown> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _toggleTriggerOverwrite<T>(
|
#toggleTriggerOverwrite<T>(
|
||||||
base: HTMLElement,
|
base: HTMLElement,
|
||||||
selection: Selection,
|
selection: Selection,
|
||||||
format: SurroundFormat<T>,
|
format: SurroundFormat<T>,
|
||||||
|
@ -154,51 +158,84 @@ export class Surrounder<T = unknown> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _toggleTriggerRemove<T>(
|
#toggleTriggerRemove<T>(
|
||||||
base: HTMLElement,
|
base: HTMLElement,
|
||||||
selection: Selection,
|
selection: Selection,
|
||||||
remove: SurroundFormat<T>[],
|
formats: {
|
||||||
triggers: TriggerItem<{ event: InputEvent; text: Text }>[],
|
format: SurroundFormat<T>;
|
||||||
|
trigger: TriggerItem<{ event: InputEvent; text: Text }>;
|
||||||
|
}[],
|
||||||
reformat: SurroundFormat<T>[] = [],
|
reformat: SurroundFormat<T>[] = [],
|
||||||
): void {
|
): void {
|
||||||
triggers.map((trigger) =>
|
const remainingFormats = formats
|
||||||
trigger.on(async ({ text }) => {
|
.filter(({ trigger }) => {
|
||||||
|
if (get(trigger.active)) {
|
||||||
|
// Deactivate active triggers for active formats.
|
||||||
|
trigger.off();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise you are within the format. This is why we activate
|
||||||
|
// the trigger, so that the active button is set to inactive.
|
||||||
|
// We still need to remove the format however.
|
||||||
|
trigger.on(asyncNoop);
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map(({ format }) => format);
|
||||||
|
|
||||||
|
// Use an anonymous insertText handler instead of some trigger associated with a name
|
||||||
|
this.#api!.inputHandler.insertText.on(
|
||||||
|
async ({ text }) => {
|
||||||
const range = new Range();
|
const range = new Range();
|
||||||
range.selectNode(text);
|
range.selectNode(text);
|
||||||
|
|
||||||
const clearedRange = removeFormats(range, base, remove, reformat);
|
const clearedRange = removeFormats(
|
||||||
|
range,
|
||||||
|
base,
|
||||||
|
remainingFormats,
|
||||||
|
reformat,
|
||||||
|
);
|
||||||
selection.removeAllRanges();
|
selection.removeAllRanges();
|
||||||
selection.addRange(clearedRange);
|
selection.addRange(clearedRange);
|
||||||
selection.collapseToEnd();
|
selection.collapseToEnd();
|
||||||
}),
|
},
|
||||||
|
{ once: true },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private formats: Map<string, SurroundFormat<T>> = new Map();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a surround format under a certain name.
|
|
||||||
* This name is then used with the surround functions to actually apply or
|
|
||||||
* remove the given format
|
|
||||||
*/
|
|
||||||
registerFormat(key: string, format: SurroundFormat<T>): () => void {
|
|
||||||
this.formats.set(key, format);
|
|
||||||
|
|
||||||
if (this.api) {
|
|
||||||
this.triggers.set(
|
|
||||||
key,
|
|
||||||
this.api.inputHandler.insertText.trigger({ once: true }),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => this.formats.delete(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a surround format under the given key is registered.
|
* Check if a surround format under the given key is registered.
|
||||||
*/
|
*/
|
||||||
hasFormat(key: string): boolean {
|
hasFormat(key: string): boolean {
|
||||||
return this.formats.has(key);
|
return this.#formats.has(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register a surround format under a certain key.
|
||||||
|
* This name is then used with the surround functions to actually apply or
|
||||||
|
* remove the given format.
|
||||||
|
*/
|
||||||
|
registerFormat(key: string, format: SurroundFormat<T>): () => void {
|
||||||
|
this.#formats.set(key, format);
|
||||||
|
|
||||||
|
if (this.#api) {
|
||||||
|
this.#triggers.set(
|
||||||
|
key,
|
||||||
|
this.#api.inputHandler.insertText.trigger({ once: true }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => this.#formats.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a surround format under a specific key.
|
||||||
|
*/
|
||||||
|
updateFormat(
|
||||||
|
key: string,
|
||||||
|
update: (format: SurroundFormat<T>) => SurroundFormat<T>,
|
||||||
|
): void {
|
||||||
|
this.#formats.set(key, update(this.#formats.get(key)!));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -206,11 +243,11 @@ export class Surrounder<T = unknown> {
|
||||||
* If the range is already surrounded, it will unsurround instead.
|
* If the range is already surrounded, it will unsurround instead.
|
||||||
*/
|
*/
|
||||||
async surround(formatName: string, exclusiveNames: string[] = []): Promise<void> {
|
async surround(formatName: string, exclusiveNames: string[] = []): Promise<void> {
|
||||||
const base = await this._assert_base();
|
const base = await this.#getBaseElement();
|
||||||
const selection = getSelection(base)!;
|
const selection = getSelection(base)!;
|
||||||
const range = getRange(selection);
|
const range = getRange(selection);
|
||||||
const format = this.formats.get(formatName);
|
const format = this.#formats.get(formatName);
|
||||||
const trigger = this.triggers.get(formatName);
|
const trigger = this.#triggers.get(formatName);
|
||||||
|
|
||||||
if (!format || !range || !trigger) {
|
if (!format || !range || !trigger) {
|
||||||
return;
|
return;
|
||||||
|
@ -219,11 +256,11 @@ export class Surrounder<T = unknown> {
|
||||||
const matcher = boolMatcher(format);
|
const matcher = boolMatcher(format);
|
||||||
|
|
||||||
const exclusives = exclusiveNames
|
const exclusives = exclusiveNames
|
||||||
.map((name) => this.formats.get(name))
|
.map((name) => this.#formats.get(name))
|
||||||
.filter(isValid);
|
.filter(isValid);
|
||||||
|
|
||||||
if (range.collapsed) {
|
if (range.collapsed) {
|
||||||
return this._toggleTrigger(
|
return this.#toggleTrigger(
|
||||||
base,
|
base,
|
||||||
selection,
|
selection,
|
||||||
matcher,
|
matcher,
|
||||||
|
@ -248,22 +285,22 @@ export class Surrounder<T = unknown> {
|
||||||
formatName: string,
|
formatName: string,
|
||||||
exclusiveNames: string[] = [],
|
exclusiveNames: string[] = [],
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const base = await this._assert_base();
|
const base = await this.#getBaseElement();
|
||||||
const selection = getSelection(base)!;
|
const selection = getSelection(base)!;
|
||||||
const range = getRange(selection);
|
const range = getRange(selection);
|
||||||
const format = this.formats.get(formatName);
|
const format = this.#formats.get(formatName);
|
||||||
const trigger = this.triggers.get(formatName);
|
const trigger = this.#triggers.get(formatName);
|
||||||
|
|
||||||
if (!format || !range || !trigger) {
|
if (!format || !range || !trigger) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const exclusives = exclusiveNames
|
const exclusives = exclusiveNames
|
||||||
.map((name) => this.formats.get(name))
|
.map((name) => this.#formats.get(name))
|
||||||
.filter(isValid);
|
.filter(isValid);
|
||||||
|
|
||||||
if (range.collapsed) {
|
if (range.collapsed) {
|
||||||
return this._toggleTriggerOverwrite(
|
return this.#toggleTriggerOverwrite(
|
||||||
base,
|
base,
|
||||||
selection,
|
selection,
|
||||||
format,
|
format,
|
||||||
|
@ -285,13 +322,13 @@ export class Surrounder<T = unknown> {
|
||||||
* text insert).
|
* text insert).
|
||||||
*/
|
*/
|
||||||
async isSurrounded(formatName: string): Promise<boolean> {
|
async isSurrounded(formatName: string): Promise<boolean> {
|
||||||
const base = await this._assert_base();
|
const base = await this.#getBaseElement();
|
||||||
const selection = getSelection(base)!;
|
const selection = getSelection(base)!;
|
||||||
const range = getRange(selection);
|
const range = getRange(selection);
|
||||||
const format = this.formats.get(formatName);
|
const format = this.#formats.get(formatName);
|
||||||
const trigger = this.triggers.get(formatName);
|
const trigger = this.#triggers.get(formatName);
|
||||||
|
|
||||||
if (!format || !range || !trigger) {
|
if (!range || !format || !trigger) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -303,7 +340,7 @@ export class Surrounder<T = unknown> {
|
||||||
* Clear/Reformat the provided formats in the current range.
|
* Clear/Reformat the provided formats in the current range.
|
||||||
*/
|
*/
|
||||||
async remove(formatNames: string[], reformatNames: string[] = []): Promise<void> {
|
async remove(formatNames: string[], reformatNames: string[] = []): Promise<void> {
|
||||||
const base = await this._assert_base();
|
const base = await this.#getBaseElement();
|
||||||
const selection = getSelection(base)!;
|
const selection = getSelection(base)!;
|
||||||
const range = getRange(selection);
|
const range = getRange(selection);
|
||||||
|
|
||||||
|
@ -311,29 +348,39 @@ export class Surrounder<T = unknown> {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const formats = formatNames
|
const activeFormats = formatNames
|
||||||
.map((name) => this.formats.get(name))
|
.map((name: string) => ({
|
||||||
.filter(isValid);
|
name,
|
||||||
|
format: this.#formats.get(name)!,
|
||||||
|
trigger: this.#triggers.get(name)!,
|
||||||
|
}))
|
||||||
|
.filter(({ format, trigger }): boolean => {
|
||||||
|
if (!format || !trigger) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const triggers = formatNames
|
const isSurrounded = isSurroundedInner(
|
||||||
.map((name) => this.triggers.get(name))
|
range,
|
||||||
.filter(isValid);
|
base,
|
||||||
|
boolMatcher(format),
|
||||||
|
);
|
||||||
|
return get(trigger.active) ? !isSurrounded : isSurrounded;
|
||||||
|
});
|
||||||
|
|
||||||
const reformats = reformatNames
|
const reformats = reformatNames
|
||||||
.map((name) => this.formats.get(name))
|
.map((name) => this.#formats.get(name))
|
||||||
.filter(isValid);
|
.filter(isValid);
|
||||||
|
|
||||||
if (range.collapsed) {
|
if (range.collapsed) {
|
||||||
return this._toggleTriggerRemove(
|
return this.#toggleTriggerRemove(base, selection, activeFormats, reformats);
|
||||||
base,
|
|
||||||
selection,
|
|
||||||
formats,
|
|
||||||
triggers,
|
|
||||||
reformats,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const surroundedRange = removeFormats(range, base, formats, reformats);
|
const surroundedRange = removeFormats(
|
||||||
|
range,
|
||||||
|
base,
|
||||||
|
activeFormats.map(({ format }) => format),
|
||||||
|
reformats,
|
||||||
|
);
|
||||||
selection.removeAllRanges();
|
selection.removeAllRanges();
|
||||||
selection.addRange(surroundedRange);
|
selection.addRange(surroundedRange);
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,10 @@ export function noop(): void {
|
||||||
/* noop */
|
/* noop */
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function asyncNoop(): Promise<void> {
|
||||||
|
/* noop */
|
||||||
|
}
|
||||||
|
|
||||||
export function id<T>(t: T): T {
|
export function id<T>(t: T): T {
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue