commit
db716b92f9
@ -380,29 +380,30 @@ class Browser(QMainWindow):
|
|||||||
self.form.gridLayout.addWidget(switch, 0, 0)
|
self.form.gridLayout.addWidget(switch, 0, 0)
|
||||||
|
|
||||||
def setupEditor(self) -> None:
|
def setupEditor(self) -> None:
|
||||||
def add_preview_button(leftbuttons: List[str], editor: Editor) -> None:
|
def add_preview_button(editor: Editor) -> None:
|
||||||
preview_shortcut = "Ctrl+Shift+P"
|
preview_shortcut = "Ctrl+Shift+P" # TODO
|
||||||
leftbuttons.insert(
|
|
||||||
0,
|
editor._links["preview"] = lambda _editor: self.onTogglePreview()
|
||||||
editor.addButton(
|
editor.web.eval(
|
||||||
None,
|
f"""
|
||||||
"preview",
|
$editorToolbar.addButton({{
|
||||||
lambda _editor: self.onTogglePreview(),
|
component: editorToolbar.LabelButton,
|
||||||
tr.browsing_preview_selected_card(
|
label: `{tr.actions_preview()}`,
|
||||||
val=shortcut(preview_shortcut),
|
tooltip: `{tr.browsing_preview_selected_card(val=shortcut(preview_shortcut))}`,
|
||||||
),
|
onClick: () => bridgeCommand("preview"),
|
||||||
tr.actions_preview(),
|
disables: false,
|
||||||
id="previewButton",
|
}}, "notetype");
|
||||||
keys=preview_shortcut,
|
"""
|
||||||
disables=False,
|
|
||||||
rightside=False,
|
|
||||||
toggleable=True,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
gui_hooks.editor_did_init_left_buttons.append(add_preview_button)
|
def add_preview_shortcut(cuts: List[Tuple], editor: Editor) -> None:
|
||||||
|
cuts.append(("Ctrl+Shift+P", self.onTogglePreview, True))
|
||||||
|
|
||||||
|
gui_hooks.editor_did_init.append(add_preview_button)
|
||||||
|
gui_hooks.editor_did_init_shortcuts.append(add_preview_shortcut)
|
||||||
self.editor = aqt.editor.Editor(self.mw, self.form.fieldsArea, self)
|
self.editor = aqt.editor.Editor(self.mw, self.form.fieldsArea, self)
|
||||||
gui_hooks.editor_did_init_left_buttons.remove(add_preview_button)
|
gui_hooks.editor_did_init_shortcuts.remove(add_preview_shortcut)
|
||||||
|
gui_hooks.editor_did_init.remove(add_preview_button)
|
||||||
|
|
||||||
@ensure_editor_saved
|
@ensure_editor_saved
|
||||||
def onRowChanged(
|
def onRowChanged(
|
||||||
|
@ -26,11 +26,20 @@ copy_files_into_group(
|
|||||||
package = "//ts/editor",
|
package = "//ts/editor",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
copy_files_into_group(
|
||||||
|
name = "editor-toolbar",
|
||||||
|
srcs = [
|
||||||
|
"editor-toolbar.css",
|
||||||
|
],
|
||||||
|
package = "//ts/editor-toolbar",
|
||||||
|
)
|
||||||
|
|
||||||
filegroup(
|
filegroup(
|
||||||
name = "css",
|
name = "css",
|
||||||
srcs = [
|
srcs = [
|
||||||
"css_local",
|
"css_local",
|
||||||
"editor",
|
"editor",
|
||||||
|
"editor-toolbar",
|
||||||
],
|
],
|
||||||
visibility = ["//qt:__subpackages__"],
|
visibility = ["//qt:__subpackages__"],
|
||||||
)
|
)
|
||||||
|
@ -37,11 +37,20 @@ copy_files_into_group(
|
|||||||
package = "//ts/editor",
|
package = "//ts/editor",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
copy_files_into_group(
|
||||||
|
name = "editor-toolbar",
|
||||||
|
srcs = [
|
||||||
|
"editor-toolbar.js",
|
||||||
|
],
|
||||||
|
package = "//ts/editor-toolbar",
|
||||||
|
)
|
||||||
|
|
||||||
filegroup(
|
filegroup(
|
||||||
name = "js",
|
name = "js",
|
||||||
srcs = [
|
srcs = [
|
||||||
"aqt_es5",
|
"aqt_es5",
|
||||||
"editor",
|
"editor",
|
||||||
|
"editor-toolbar",
|
||||||
"mathjax.js",
|
"mathjax.js",
|
||||||
"//qt/aqt/data/web/js/vendor",
|
"//qt/aqt/data/web/js/vendor",
|
||||||
],
|
],
|
||||||
|
161
qt/aqt/editor.py
161
qt/aqt/editor.py
@ -82,9 +82,7 @@ _html = """
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<div>
|
<div>
|
||||||
<div id="topbutsOuter">
|
<anki-editor-toolbar id="editorToolbar"></anki-editor-toolbar>
|
||||||
%s
|
|
||||||
</div>
|
|
||||||
<div id="fields">
|
<div id="fields">
|
||||||
</div>
|
</div>
|
||||||
<div id="dupes" class="is-inactive">
|
<div id="dupes" class="is-inactive">
|
||||||
@ -137,111 +135,62 @@ class Editor:
|
|||||||
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)
|
||||||
|
|
||||||
lefttopbtns: List[str] = [
|
|
||||||
self._addButton(
|
|
||||||
None,
|
|
||||||
"fields",
|
|
||||||
tr.editing_customize_fields(),
|
|
||||||
f"{tr.editing_fields()}...",
|
|
||||||
disables=False,
|
|
||||||
rightside=False,
|
|
||||||
),
|
|
||||||
self._addButton(
|
|
||||||
None,
|
|
||||||
"cards",
|
|
||||||
tr.editing_customize_card_templates_ctrlandl(),
|
|
||||||
f"{tr.editing_cards()}...",
|
|
||||||
disables=False,
|
|
||||||
rightside=False,
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
gui_hooks.editor_did_init_left_buttons(lefttopbtns, self)
|
|
||||||
|
|
||||||
righttopbtns: List[str] = [
|
|
||||||
self._addButton(
|
|
||||||
"text_bold", "bold", tr.editing_bold_text_ctrlandb(), id="bold"
|
|
||||||
),
|
|
||||||
self._addButton(
|
|
||||||
"text_italic",
|
|
||||||
"italic",
|
|
||||||
tr.editing_italic_text_ctrlandi(),
|
|
||||||
id="italic",
|
|
||||||
),
|
|
||||||
self._addButton(
|
|
||||||
"text_under",
|
|
||||||
"underline",
|
|
||||||
tr.editing_underline_text_ctrlandu(),
|
|
||||||
id="underline",
|
|
||||||
),
|
|
||||||
self._addButton(
|
|
||||||
"text_super",
|
|
||||||
"super",
|
|
||||||
tr.editing_superscript_ctrlandand(),
|
|
||||||
id="superscript",
|
|
||||||
),
|
|
||||||
self._addButton(
|
|
||||||
"text_sub", "sub", tr.editing_subscript_ctrland(), id="subscript"
|
|
||||||
),
|
|
||||||
self._addButton(
|
|
||||||
"text_clear", "clear", tr.editing_remove_formatting_ctrlandr()
|
|
||||||
),
|
|
||||||
self._addButton(
|
|
||||||
None,
|
|
||||||
"colour",
|
|
||||||
tr.editing_set_foreground_colour_f7(),
|
|
||||||
"""
|
|
||||||
<span id="forecolor" class="topbut rounded" style="background: #000"></span>
|
|
||||||
""",
|
|
||||||
),
|
|
||||||
self._addButton(
|
|
||||||
None,
|
|
||||||
"changeCol",
|
|
||||||
tr.editing_change_colour_f8(),
|
|
||||||
"""
|
|
||||||
<span class="topbut rounded rainbow"></span>
|
|
||||||
""",
|
|
||||||
),
|
|
||||||
self._addButton(
|
|
||||||
"text_cloze", "cloze", tr.editing_cloze_deletion_ctrlandshiftandc()
|
|
||||||
),
|
|
||||||
self._addButton(
|
|
||||||
"paperclip", "attach", tr.editing_attach_picturesaudiovideo_f3()
|
|
||||||
),
|
|
||||||
self._addButton("media-record", "record", tr.editing_record_audio_f5()),
|
|
||||||
self._addButton("more", "more"),
|
|
||||||
]
|
|
||||||
|
|
||||||
gui_hooks.editor_did_init_buttons(righttopbtns, self)
|
|
||||||
# legacy filter
|
|
||||||
righttopbtns = runFilter("setupEditorButtons", righttopbtns, self)
|
|
||||||
|
|
||||||
topbuts = """
|
|
||||||
<div id="topbutsleft" class="topbuts">
|
|
||||||
%(leftbts)s
|
|
||||||
</div>
|
|
||||||
<div id="topbutsright" class="topbuts">
|
|
||||||
%(rightbts)s
|
|
||||||
</div>
|
|
||||||
""" % dict(
|
|
||||||
leftbts="".join(lefttopbtns),
|
|
||||||
rightbts="".join(righttopbtns),
|
|
||||||
)
|
|
||||||
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, topbuts, tr.editing_show_duplicates()),
|
_html % (bgcol, tr.editing_show_duplicates()),
|
||||||
css=[
|
css=[
|
||||||
"css/editor.css",
|
"css/editor.css",
|
||||||
|
"css/editor-toolbar.css",
|
||||||
],
|
],
|
||||||
js=[
|
js=[
|
||||||
"js/vendor/jquery.min.js",
|
"js/vendor/jquery.min.js",
|
||||||
|
"js/vendor/protobuf.min.js",
|
||||||
"js/editor.js",
|
"js/editor.js",
|
||||||
|
"js/editor-toolbar.js",
|
||||||
],
|
],
|
||||||
context=self,
|
context=self,
|
||||||
default_css=False,
|
default_css=False,
|
||||||
)
|
)
|
||||||
self.web.eval("preventButtonFocus();")
|
|
||||||
|
lefttopbtns: List[str] = []
|
||||||
|
gui_hooks.editor_did_init_left_buttons(lefttopbtns, self)
|
||||||
|
|
||||||
|
lefttopbtns_defs = [
|
||||||
|
f"$editorToolbar.addButton({{ component: editorToolbar.RawButton, html: `{button}` }}, 'notetype');"
|
||||||
|
for button in lefttopbtns
|
||||||
|
]
|
||||||
|
lefttopbtns_js = "\n".join(lefttopbtns_defs)
|
||||||
|
|
||||||
|
righttopbtns: List[str] = []
|
||||||
|
gui_hooks.editor_did_init_buttons(righttopbtns, self)
|
||||||
|
# legacy filter
|
||||||
|
righttopbtns = runFilter("setupEditorButtons", righttopbtns, self)
|
||||||
|
|
||||||
|
righttopbtns_defs = "\n".join(
|
||||||
|
[
|
||||||
|
f"{{ component: editorToolbar.RawButton, html: `{button}` }},"
|
||||||
|
for button in righttopbtns
|
||||||
|
]
|
||||||
|
)
|
||||||
|
righttopbtns_js = (
|
||||||
|
f"""
|
||||||
|
$editorToolbar.addButtonGroup({{
|
||||||
|
id: "addons",
|
||||||
|
buttons: [ {righttopbtns_defs} ]
|
||||||
|
}});
|
||||||
|
"""
|
||||||
|
if righttopbtns_defs
|
||||||
|
else ""
|
||||||
|
)
|
||||||
|
|
||||||
|
self.web.eval(
|
||||||
|
f"""
|
||||||
|
$editorToolbar = document.getElementById("editorToolbar");
|
||||||
|
{lefttopbtns_js}
|
||||||
|
{righttopbtns_js}
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
# Top buttons
|
# Top buttons
|
||||||
######################################################################
|
######################################################################
|
||||||
@ -353,6 +302,7 @@ class Editor:
|
|||||||
type="button"
|
type="button"
|
||||||
title="{tip}"
|
title="{tip}"
|
||||||
onclick="pycmd('{cmd}');{togglesc}return false;"
|
onclick="pycmd('{cmd}');{togglesc}return false;"
|
||||||
|
onmousedown="window.event.preventDefault();"
|
||||||
>
|
>
|
||||||
{imgelm}
|
{imgelm}
|
||||||
{labelelm}
|
{labelelm}
|
||||||
@ -801,7 +751,8 @@ class Editor:
|
|||||||
self._wrapWithColour(self.fcolour)
|
self._wrapWithColour(self.fcolour)
|
||||||
|
|
||||||
def _updateForegroundButton(self) -> None:
|
def _updateForegroundButton(self) -> None:
|
||||||
self.web.eval(f"setFGButton('{self.fcolour}')")
|
# self.web.eval(f"setFGButton('{self.fcolour}')")
|
||||||
|
pass
|
||||||
|
|
||||||
def onColourChanged(self) -> None:
|
def onColourChanged(self) -> None:
|
||||||
self._updateForegroundButton()
|
self._updateForegroundButton()
|
||||||
@ -1112,6 +1063,10 @@ class Editor:
|
|||||||
dupes=showDupes,
|
dupes=showDupes,
|
||||||
paste=onPaste,
|
paste=onPaste,
|
||||||
cutOrCopy=onCutOrCopy,
|
cutOrCopy=onCutOrCopy,
|
||||||
|
htmlEdit=onHtmlEdit,
|
||||||
|
mathjaxInline=insertMathjaxInline,
|
||||||
|
mathjaxBlock=insertMathjaxBlock,
|
||||||
|
mathjaxChemistry=insertMathjaxChemistry,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1346,3 +1301,17 @@ gui_hooks.editor_will_use_font_for_field.append(fontMungeHack)
|
|||||||
gui_hooks.editor_will_munge_html.append(munge_html)
|
gui_hooks.editor_will_munge_html.append(munge_html)
|
||||||
gui_hooks.editor_will_munge_html.append(remove_null_bytes)
|
gui_hooks.editor_will_munge_html.append(remove_null_bytes)
|
||||||
gui_hooks.editor_will_munge_html.append(reverse_url_quoting)
|
gui_hooks.editor_will_munge_html.append(reverse_url_quoting)
|
||||||
|
|
||||||
|
|
||||||
|
def set_cloze_button(editor: Editor) -> None:
|
||||||
|
if editor.note.model()["type"] == MODEL_CLOZE:
|
||||||
|
editor.web.eval(
|
||||||
|
'document.getElementById("editorToolbar").showButton("template", "cloze"); '
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
editor.web.eval(
|
||||||
|
'document.getElementById("editorToolbar").hideButton("template", "cloze"); '
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
gui_hooks.editor_did_load_note.append(set_cloze_button)
|
||||||
|
@ -7,8 +7,8 @@ load("//ts:vendor.bzl", "copy_bootstrap_icons")
|
|||||||
load("//ts:compile_sass.bzl", "compile_sass")
|
load("//ts:compile_sass.bzl", "compile_sass")
|
||||||
|
|
||||||
compile_sass(
|
compile_sass(
|
||||||
srcs = ["deckconfig-base.scss"],
|
|
||||||
group = "base_css",
|
group = "base_css",
|
||||||
|
srcs = ["deckconfig-base.scss"],
|
||||||
visibility = ["//visibility:public"],
|
visibility = ["//visibility:public"],
|
||||||
deps = [
|
deps = [
|
||||||
"//ts/sass:base_lib",
|
"//ts/sass:base_lib",
|
||||||
|
150
ts/editor-toolbar/BUILD.bazel
Normal file
150
ts/editor-toolbar/BUILD.bazel
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
load("@npm//@bazel/typescript:index.bzl", "ts_library")
|
||||||
|
load("//ts/svelte:svelte.bzl", "compile_svelte", "svelte_check")
|
||||||
|
load("//ts:prettier.bzl", "prettier_test")
|
||||||
|
load("//ts:eslint.bzl", "eslint_test")
|
||||||
|
load("//ts:esbuild.bzl", "esbuild")
|
||||||
|
load("//ts:compile_sass.bzl", "compile_sass")
|
||||||
|
load("//ts:vendor.bzl", "copy_bootstrap_icons", "copy_mdi_icons")
|
||||||
|
|
||||||
|
svelte_files = glob(["*.svelte"])
|
||||||
|
|
||||||
|
svelte_names = [f.replace(".svelte", "") for f in svelte_files]
|
||||||
|
|
||||||
|
compile_svelte(
|
||||||
|
name = "svelte",
|
||||||
|
srcs = svelte_files,
|
||||||
|
deps = [
|
||||||
|
"//ts/sass:button_mixins_lib",
|
||||||
|
"//ts/sass/bootstrap",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
compile_sass(
|
||||||
|
group = "local_css",
|
||||||
|
srcs = [
|
||||||
|
"color.scss",
|
||||||
|
"legacy.scss",
|
||||||
|
"bootstrap.scss",
|
||||||
|
],
|
||||||
|
deps = [
|
||||||
|
"//ts/sass:button_mixins_lib",
|
||||||
|
"//ts/sass/bootstrap",
|
||||||
|
],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
|
|
||||||
|
ts_library(
|
||||||
|
name = "index",
|
||||||
|
srcs = ["index.ts"],
|
||||||
|
deps = [
|
||||||
|
"EditorToolbar",
|
||||||
|
"lib",
|
||||||
|
"//ts/lib",
|
||||||
|
"//ts/sveltelib",
|
||||||
|
"@npm//svelte",
|
||||||
|
"@npm//svelte2tsx",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
ts_library(
|
||||||
|
name = "lib",
|
||||||
|
srcs = glob(
|
||||||
|
["*.ts"],
|
||||||
|
exclude = ["index.ts"],
|
||||||
|
),
|
||||||
|
deps = [
|
||||||
|
"//ts/lib",
|
||||||
|
"//ts/lib:backend_proto",
|
||||||
|
"//ts/sveltelib",
|
||||||
|
"//ts:image_module_support",
|
||||||
|
"@npm//svelte",
|
||||||
|
"@npm//bootstrap",
|
||||||
|
"@npm//@popperjs/core",
|
||||||
|
"@npm//@types/bootstrap",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
copy_bootstrap_icons(
|
||||||
|
name = "bootstrap-icons",
|
||||||
|
icons = [
|
||||||
|
"type-bold.svg",
|
||||||
|
"type-italic.svg",
|
||||||
|
"type-underline.svg",
|
||||||
|
"eraser.svg",
|
||||||
|
"square-fill.svg",
|
||||||
|
"paperclip.svg",
|
||||||
|
"mic.svg",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
copy_mdi_icons(
|
||||||
|
name = "mdi-icons",
|
||||||
|
icons = [
|
||||||
|
"format-superscript.svg",
|
||||||
|
"format-subscript.svg",
|
||||||
|
"function-variant.svg",
|
||||||
|
"code-brackets.svg",
|
||||||
|
"xml.svg",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
esbuild(
|
||||||
|
name = "editor-toolbar",
|
||||||
|
srcs = [
|
||||||
|
"//ts:protobuf-shim.js",
|
||||||
|
],
|
||||||
|
args = [
|
||||||
|
"--global-name=editorToolbar",
|
||||||
|
"--inject:$(location //ts:protobuf-shim.js)",
|
||||||
|
"--resolve-extensions=.mjs,.js",
|
||||||
|
"--loader:.svg=text",
|
||||||
|
],
|
||||||
|
entry_point = "index.ts",
|
||||||
|
external = [
|
||||||
|
"protobufjs/light",
|
||||||
|
],
|
||||||
|
output_css = "editor-toolbar.css",
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
deps = [
|
||||||
|
"//ts/lib",
|
||||||
|
"//ts/lib:backend_proto",
|
||||||
|
"//ts:image_module_support",
|
||||||
|
"index",
|
||||||
|
"bootstrap-icons",
|
||||||
|
"mdi-icons",
|
||||||
|
"local_css",
|
||||||
|
] + svelte_names,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Tests
|
||||||
|
################
|
||||||
|
|
||||||
|
prettier_test(
|
||||||
|
name = "format_check",
|
||||||
|
srcs = glob([
|
||||||
|
"*.ts",
|
||||||
|
"*.svelte",
|
||||||
|
]),
|
||||||
|
)
|
||||||
|
|
||||||
|
eslint_test(
|
||||||
|
name = "eslint",
|
||||||
|
srcs = glob(
|
||||||
|
[
|
||||||
|
"*.ts",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
svelte_check(
|
||||||
|
name = "svelte_check",
|
||||||
|
srcs = glob([
|
||||||
|
"*.ts",
|
||||||
|
"*.svelte",
|
||||||
|
]) + [
|
||||||
|
"//ts/sass:button_mixins_lib",
|
||||||
|
"//ts/sass/bootstrap",
|
||||||
|
"@npm//@types/bootstrap",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
19
ts/editor-toolbar/ButtonDropdown.svelte
Normal file
19
ts/editor-toolbar/ButtonDropdown.svelte
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="typescript">
|
||||||
|
import type { ToolbarItem } from "./types";
|
||||||
|
import ButtonGroup from "./ButtonGroup.svelte";
|
||||||
|
|
||||||
|
export let id: string;
|
||||||
|
export let className = "";
|
||||||
|
|
||||||
|
function extendClassName(className: string): string {
|
||||||
|
return `dropdown-menu bg-transparent border-0 ${className}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export let buttons: ToolbarItem[];
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ButtonGroup {id} className={extendClassName(className)} {buttons} />
|
9
ts/editor-toolbar/ButtonGroup.d.ts
vendored
Normal file
9
ts/editor-toolbar/ButtonGroup.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
import type { ToolbarItem } from "./types";
|
||||||
|
|
||||||
|
export interface ButtonGroupProps {
|
||||||
|
id: string;
|
||||||
|
className?: string;
|
||||||
|
buttons: ToolbarItem[];
|
||||||
|
}
|
85
ts/editor-toolbar/ButtonGroup.svelte
Normal file
85
ts/editor-toolbar/ButtonGroup.svelte
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="typescript">
|
||||||
|
import type { ToolbarItem } from "./types";
|
||||||
|
import { getContext } from "svelte";
|
||||||
|
import { nightModeKey } from "./contextKeys";
|
||||||
|
|
||||||
|
export let id: string;
|
||||||
|
export let className = "";
|
||||||
|
export let buttons: ToolbarItem[];
|
||||||
|
|
||||||
|
function filterHidden({ hidden = false, ...props }) {
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nightMode = getContext(nightModeKey);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
ul {
|
||||||
|
display: inline-flex;
|
||||||
|
justify-items: start;
|
||||||
|
|
||||||
|
flex-wrap: var(--toolbar-wrap);
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
padding-inline-start: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-group {
|
||||||
|
/* buttons' borders exactly overlap each other */
|
||||||
|
:global(button),
|
||||||
|
:global(select) {
|
||||||
|
margin-left: -2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
li {
|
||||||
|
display: inline-block;
|
||||||
|
margin-bottom: calc(var(--toolbar-size) / 15);
|
||||||
|
|
||||||
|
> :global(button),
|
||||||
|
> :global(select) {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(1) {
|
||||||
|
margin-left: calc(var(--toolbar-size) / 7.5);
|
||||||
|
|
||||||
|
> :global(button),
|
||||||
|
> :global(select) {
|
||||||
|
/* default 0.25rem */
|
||||||
|
border-top-left-radius: calc(var(--toolbar-size) / 7.5);
|
||||||
|
border-bottom-left-radius: calc(var(--toolbar-size) / 7.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-last-child(1) {
|
||||||
|
margin-right: calc(var(--toolbar-size) / 7.5);
|
||||||
|
|
||||||
|
> :global(button),
|
||||||
|
> :global(select) {
|
||||||
|
border-top-right-radius: calc(var(--toolbar-size) / 7.5);
|
||||||
|
border-bottom-right-radius: calc(var(--toolbar-size) / 7.5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:nth-child(1)) {
|
||||||
|
margin-left: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<ul {id} class={className} class:border-group={!nightMode}>
|
||||||
|
{#each buttons as button}
|
||||||
|
{#if !button.hidden}
|
||||||
|
<li>
|
||||||
|
<svelte:component this={button.component} {...filterHidden(button)} />
|
||||||
|
</li>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</ul>
|
8
ts/editor-toolbar/ColorPicker.d.ts
vendored
Normal file
8
ts/editor-toolbar/ColorPicker.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
export interface ColorPickerProps {
|
||||||
|
id?: string;
|
||||||
|
className?: string;
|
||||||
|
tooltip: string;
|
||||||
|
onChange: (event: Event) => void;
|
||||||
|
}
|
69
ts/editor-toolbar/ColorPicker.svelte
Normal file
69
ts/editor-toolbar/ColorPicker.svelte
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="typescript">
|
||||||
|
import { getContext } from "svelte";
|
||||||
|
import { nightModeKey } from "./contextKeys";
|
||||||
|
|
||||||
|
export let id: string;
|
||||||
|
export let className = "";
|
||||||
|
export let tooltip: string;
|
||||||
|
|
||||||
|
export let onChange: (event: Event) => void;
|
||||||
|
|
||||||
|
function extendClassName(className: string): string {
|
||||||
|
return `btn ${className}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nightMode = getContext(nightModeKey);
|
||||||
|
|
||||||
|
let inputRef: HTMLInputElement;
|
||||||
|
|
||||||
|
function delegateToInput() {
|
||||||
|
inputRef.click();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use "ts/sass/button_mixins" as button;
|
||||||
|
|
||||||
|
@import "ts/sass/bootstrap/functions";
|
||||||
|
@import "ts/sass/bootstrap/variables";
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
width: calc(var(--toolbar-size) - 0px);
|
||||||
|
height: calc(var(--toolbar-size) - 0px);
|
||||||
|
|
||||||
|
padding: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include button.btn-day($with-disabled: false) using ($base) {
|
||||||
|
@include button.rainbow($base);
|
||||||
|
}
|
||||||
|
|
||||||
|
@include button.btn-night($with-disabled: false) using ($base) {
|
||||||
|
@include button.rainbow($base);
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
display: inline-block;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<button
|
||||||
|
tabindex="-1"
|
||||||
|
{id}
|
||||||
|
class={extendClassName(className)}
|
||||||
|
class:btn-day={!nightMode}
|
||||||
|
class:btn-night={nightMode}
|
||||||
|
title={tooltip}
|
||||||
|
on:click={delegateToInput}
|
||||||
|
on:mousedown|preventDefault>
|
||||||
|
<input bind:this={inputRef} type="color" on:change={onChange} />
|
||||||
|
</button>
|
10
ts/editor-toolbar/CommandIconButton.d.ts
vendored
Normal file
10
ts/editor-toolbar/CommandIconButton.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
export interface CommandIconButtonProps {
|
||||||
|
id?: string;
|
||||||
|
className?: string;
|
||||||
|
tooltip: string;
|
||||||
|
icon: string;
|
||||||
|
command: string;
|
||||||
|
activatable?: boolean;
|
||||||
|
}
|
80
ts/editor-toolbar/CommandIconButton.svelte
Normal file
80
ts/editor-toolbar/CommandIconButton.svelte
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="typescript" context="module">
|
||||||
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
|
const commandMap = writable(new Map<string, boolean>());
|
||||||
|
|
||||||
|
function updateButton(key: string): void {
|
||||||
|
commandMap.update(
|
||||||
|
(map: Map<string, boolean>): Map<string, boolean> =>
|
||||||
|
new Map([...map, [key, document.queryCommandState(key)]])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateButtons(callback: (key: string) => boolean): void {
|
||||||
|
commandMap.update(
|
||||||
|
(map: Map<string, boolean>): Map<string, boolean> => {
|
||||||
|
const newMap = new Map<string, boolean>();
|
||||||
|
|
||||||
|
for (const key of map.keys()) {
|
||||||
|
newMap.set(key, callback(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
return newMap;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateActiveButtons() {
|
||||||
|
updateButtons((key: string): boolean => document.queryCommandState(key));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearActiveButtons() {
|
||||||
|
updateButtons((): boolean => false);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="typescript">
|
||||||
|
import SquareButton from "./SquareButton.svelte";
|
||||||
|
|
||||||
|
export let id: string;
|
||||||
|
export let className = "";
|
||||||
|
export let tooltip: string;
|
||||||
|
|
||||||
|
export let icon: string;
|
||||||
|
export let command: string;
|
||||||
|
export let activatable = true;
|
||||||
|
export let disables = true;
|
||||||
|
export let dropdownToggle = false;
|
||||||
|
|
||||||
|
let active = false;
|
||||||
|
|
||||||
|
if (activatable) {
|
||||||
|
updateButton(command);
|
||||||
|
|
||||||
|
commandMap.subscribe((map: Map<string, boolean>): (() => void) => {
|
||||||
|
active = map.get(command);
|
||||||
|
return () => map.delete(command);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onClick(): void {
|
||||||
|
document.execCommand(command);
|
||||||
|
updateButton(command);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SquareButton
|
||||||
|
{id}
|
||||||
|
{className}
|
||||||
|
{tooltip}
|
||||||
|
{active}
|
||||||
|
{disables}
|
||||||
|
{dropdownToggle}
|
||||||
|
{onClick}
|
||||||
|
on:mount>
|
||||||
|
{@html icon}
|
||||||
|
</SquareButton>
|
11
ts/editor-toolbar/DropdownItem.d.ts
vendored
Normal file
11
ts/editor-toolbar/DropdownItem.d.ts
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
export interface DropdownItemProps {
|
||||||
|
id?: string;
|
||||||
|
className?: string;
|
||||||
|
tooltip: string;
|
||||||
|
|
||||||
|
onClick: (event: MouseEvent) => void;
|
||||||
|
label: string;
|
||||||
|
endLabel: string;
|
||||||
|
}
|
67
ts/editor-toolbar/DropdownItem.svelte
Normal file
67
ts/editor-toolbar/DropdownItem.svelte
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="typescript">
|
||||||
|
import { getContext } from "svelte";
|
||||||
|
import { nightModeKey } from "./contextKeys";
|
||||||
|
|
||||||
|
export let id: string;
|
||||||
|
export let className = "";
|
||||||
|
export let tooltip: string;
|
||||||
|
|
||||||
|
export let onClick: (event: MouseEvent) => void;
|
||||||
|
export let label: string;
|
||||||
|
export let endLabel: string;
|
||||||
|
|
||||||
|
const nightMode = getContext(nightModeKey);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "ts/sass/bootstrap/functions";
|
||||||
|
@import "ts/sass/bootstrap/variables";
|
||||||
|
|
||||||
|
button {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
color: black;
|
||||||
|
|
||||||
|
&.nightMode {
|
||||||
|
color: white;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus {
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
font-size: calc(var(--toolbar-size) / 2.3);
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.monospace {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<button
|
||||||
|
{id}
|
||||||
|
class={`btn dropdown-item ${className}`}
|
||||||
|
class:nightMode
|
||||||
|
title={tooltip}
|
||||||
|
on:click={onClick}
|
||||||
|
on:mousedown|preventDefault>
|
||||||
|
<span class:me-3={endLabel}>{label}</span>
|
||||||
|
{#if endLabel}<span class="monospace">{endLabel}</span>{/if}
|
||||||
|
</button>
|
8
ts/editor-toolbar/DropdownMenu.d.ts
vendored
Normal file
8
ts/editor-toolbar/DropdownMenu.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
import type { ToolbarItem } from "./types";
|
||||||
|
|
||||||
|
export interface DropdownMenuProps {
|
||||||
|
id: string;
|
||||||
|
menuItems: ToolbarItem[];
|
||||||
|
}
|
35
ts/editor-toolbar/DropdownMenu.svelte
Normal file
35
ts/editor-toolbar/DropdownMenu.svelte
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="typescript">
|
||||||
|
import type { DynamicSvelteComponent } from "sveltelib/dynamicComponent";
|
||||||
|
import { getContext } from "svelte";
|
||||||
|
import { nightModeKey } from "./contextKeys";
|
||||||
|
|
||||||
|
export let id: string;
|
||||||
|
export let menuItems: DynamicSvelteComponent[];
|
||||||
|
|
||||||
|
const nightMode = getContext<boolean>(nightModeKey);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import "ts/sass/bootstrap/functions";
|
||||||
|
@import "ts/sass/bootstrap/variables";
|
||||||
|
|
||||||
|
ul {
|
||||||
|
background-color: $light;
|
||||||
|
|
||||||
|
&.nightMode {
|
||||||
|
background-color: $secondary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<ul {id} class="dropdown-menu" class:nightMode>
|
||||||
|
{#each menuItems as menuItem}
|
||||||
|
<li class:nightMode>
|
||||||
|
<svelte:component this={menuItem.component} {...menuItem} />
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
78
ts/editor-toolbar/EditorToolbar.svelte
Normal file
78
ts/editor-toolbar/EditorToolbar.svelte
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script context="module" lang="typescript">
|
||||||
|
import "./legacy.css";
|
||||||
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
|
const disabled = writable(false);
|
||||||
|
|
||||||
|
export function enableButtons(): void {
|
||||||
|
disabled.set(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function disableButtons(): void {
|
||||||
|
disabled.set(true);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="typescript">
|
||||||
|
import type { Readable } from "svelte/store";
|
||||||
|
import type { ToolbarItem } from "./types";
|
||||||
|
import { setContext } from "svelte";
|
||||||
|
import { disabledKey, nightModeKey } from "./contextKeys";
|
||||||
|
|
||||||
|
import ButtonGroup from "./ButtonGroup.svelte";
|
||||||
|
|
||||||
|
export let buttons: Readable<ToolbarItem[]>;
|
||||||
|
export let menus: Readable<ToolbarItem[]>;
|
||||||
|
|
||||||
|
$: _buttons = $buttons;
|
||||||
|
$: _menus = $menus;
|
||||||
|
|
||||||
|
export let nightMode: boolean;
|
||||||
|
|
||||||
|
setContext(nightModeKey, nightMode);
|
||||||
|
setContext(disabledKey, disabled);
|
||||||
|
|
||||||
|
export let size: number = 30;
|
||||||
|
export let wraps: boolean = true;
|
||||||
|
|
||||||
|
$: style = `--toolbar-size: ${size}px; --toolbar-wrap: ${
|
||||||
|
wraps ? "wrap" : "nowrap"
|
||||||
|
}`;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
nav {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 10;
|
||||||
|
|
||||||
|
background: var(--bg-color);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
|
||||||
|
/* Remove outermost marigns */
|
||||||
|
& > :global(ul) {
|
||||||
|
& > :global(li:nth-child(1)) {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > :global(li:nth-last-child(1)) {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div {style}>
|
||||||
|
{#each _menus as menu}
|
||||||
|
<svelte:component this={menu.component} {...menu} />
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav {style}>
|
||||||
|
<ButtonGroup buttons={_buttons} />
|
||||||
|
</nav>
|
9
ts/editor-toolbar/IconButton.d.ts
vendored
Normal file
9
ts/editor-toolbar/IconButton.d.ts
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
export interface IconButtonProps {
|
||||||
|
id?: string;
|
||||||
|
className?: string;
|
||||||
|
tooltip: string;
|
||||||
|
icon: string;
|
||||||
|
onClick: (event: MouseEvent) => void;
|
||||||
|
}
|
20
ts/editor-toolbar/IconButton.svelte
Normal file
20
ts/editor-toolbar/IconButton.svelte
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="typescript">
|
||||||
|
import SquareButton from "./SquareButton.svelte";
|
||||||
|
|
||||||
|
export let id: string;
|
||||||
|
export let className = "";
|
||||||
|
export let tooltip: string;
|
||||||
|
export let disables = true;
|
||||||
|
export let dropdownToggle = false;
|
||||||
|
|
||||||
|
export let icon = "";
|
||||||
|
export let onClick: (event: MouseEvent) => void;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<SquareButton {id} {className} {tooltip} {onClick} {disables} {dropdownToggle} on:mount>
|
||||||
|
{@html icon}
|
||||||
|
</SquareButton>
|
11
ts/editor-toolbar/LabelButton.d.ts
vendored
Normal file
11
ts/editor-toolbar/LabelButton.d.ts
vendored
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
export interface LabelButtonProps {
|
||||||
|
id?: string;
|
||||||
|
className?: string;
|
||||||
|
|
||||||
|
label: string;
|
||||||
|
tooltip: string;
|
||||||
|
onClick: (event: MouseEvent) => void;
|
||||||
|
disables?: boolean;
|
||||||
|
}
|
69
ts/editor-toolbar/LabelButton.svelte
Normal file
69
ts/editor-toolbar/LabelButton.svelte
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="typescript">
|
||||||
|
import type { Readable } from "svelte/store";
|
||||||
|
import { onMount, createEventDispatcher, getContext } from "svelte";
|
||||||
|
import { disabledKey, nightModeKey } from "./contextKeys";
|
||||||
|
|
||||||
|
export let id: string;
|
||||||
|
export let className = "";
|
||||||
|
|
||||||
|
export let label: string;
|
||||||
|
export let tooltip: string;
|
||||||
|
export let onClick: (event: MouseEvent) => void;
|
||||||
|
export let disables = true;
|
||||||
|
export let dropdownToggle = false;
|
||||||
|
|
||||||
|
$: extraProps = dropdownToggle
|
||||||
|
? {
|
||||||
|
"data-bs-toggle": "dropdown",
|
||||||
|
"aria-expanded": "false",
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
|
||||||
|
let buttonRef: HTMLButtonElement;
|
||||||
|
|
||||||
|
function extendClassName(className: string): string {
|
||||||
|
return `btn ${className}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const disabled = getContext<Readable<boolean>>(disabledKey);
|
||||||
|
$: _disabled = disables && $disabled;
|
||||||
|
|
||||||
|
const nightMode = getContext<boolean>(nightModeKey);
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
onMount(() => dispatch("mount", { button: buttonRef }));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use "ts/sass/button_mixins" as button;
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 0 calc(var(--toolbar-size) / 3);
|
||||||
|
font-size: calc(var(--toolbar-size) / 2.3);
|
||||||
|
width: auto;
|
||||||
|
height: var(--toolbar-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
@include button.btn-day;
|
||||||
|
@include button.btn-night;
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<button
|
||||||
|
bind:this={buttonRef}
|
||||||
|
{id}
|
||||||
|
class={extendClassName(className)}
|
||||||
|
class:dropdown-toggle={dropdownToggle}
|
||||||
|
class:btn-day={!nightMode}
|
||||||
|
class:btn-night={nightMode}
|
||||||
|
tabindex="-1"
|
||||||
|
disabled={_disabled}
|
||||||
|
title={tooltip}
|
||||||
|
{...extraProps}
|
||||||
|
on:click={onClick}
|
||||||
|
on:mousedown|preventDefault>
|
||||||
|
{label}
|
||||||
|
</button>
|
14
ts/editor-toolbar/RawButton.svelte
Normal file
14
ts/editor-toolbar/RawButton.svelte
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="typescript">
|
||||||
|
import { onMount, createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
|
export let html: string;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
onMount(() => dispatch("mount", { button: null }));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{@html html}
|
70
ts/editor-toolbar/SelectButton.svelte
Normal file
70
ts/editor-toolbar/SelectButton.svelte
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="typescript">
|
||||||
|
import type { Readable } from "svelte/store";
|
||||||
|
import { onMount, createEventDispatcher, getContext } from "svelte";
|
||||||
|
import { disabledKey } from "./contextKeys";
|
||||||
|
import SelectOption from "./SelectOption.svelte";
|
||||||
|
|
||||||
|
interface Option {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
selected: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export let id: string;
|
||||||
|
export let className = "";
|
||||||
|
export let tooltip: string;
|
||||||
|
|
||||||
|
function extendClassName(classes: string) {
|
||||||
|
return `form-select ${classes}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export let disables = true;
|
||||||
|
export let options: Option[];
|
||||||
|
|
||||||
|
let buttonRef: HTMLSelectElement;
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
onMount(() => dispatch("mount", { button: buttonRef }));
|
||||||
|
|
||||||
|
const disabled = getContext<Readable<boolean>>(disabledKey);
|
||||||
|
$: _disabled = disables && $disabled;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
select {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
|
||||||
|
height: var(--toolbar-size);
|
||||||
|
width: auto;
|
||||||
|
|
||||||
|
font-size: calc(var(--toolbar-size) / 2.3);
|
||||||
|
user-select: none;
|
||||||
|
box-shadow: none;
|
||||||
|
border-radius: 0;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<select
|
||||||
|
tabindex="-1"
|
||||||
|
bind:this={buttonRef}
|
||||||
|
disabled={_disabled}
|
||||||
|
{id}
|
||||||
|
class={extendClassName(className)}
|
||||||
|
title={tooltip}>
|
||||||
|
{#each options as option}
|
||||||
|
<SelectOption {...option} />
|
||||||
|
{/each}
|
||||||
|
</select>
|
11
ts/editor-toolbar/SelectOption.svelte
Normal file
11
ts/editor-toolbar/SelectOption.svelte
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="typescript">
|
||||||
|
export let label: string;
|
||||||
|
export let value: string;
|
||||||
|
export let selected = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<option {selected} {value}>{label}</option>
|
88
ts/editor-toolbar/SquareButton.svelte
Normal file
88
ts/editor-toolbar/SquareButton.svelte
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="typescript">
|
||||||
|
import type { Readable } from "svelte/store";
|
||||||
|
import { getContext, onMount, createEventDispatcher } from "svelte";
|
||||||
|
import { disabledKey, nightModeKey } from "./contextKeys";
|
||||||
|
|
||||||
|
export let id: string;
|
||||||
|
export let className = "";
|
||||||
|
export let tooltip: string;
|
||||||
|
|
||||||
|
export let onClick: (event: MouseEvent) => void;
|
||||||
|
export let active = false;
|
||||||
|
export let disables = true;
|
||||||
|
export let dropdownToggle = false;
|
||||||
|
|
||||||
|
$: extraProps = dropdownToggle
|
||||||
|
? {
|
||||||
|
"data-bs-toggle": "dropdown",
|
||||||
|
"aria-expanded": "false",
|
||||||
|
}
|
||||||
|
: {};
|
||||||
|
|
||||||
|
let buttonRef: HTMLButtonElement;
|
||||||
|
|
||||||
|
function extendClassName(className: string): string {
|
||||||
|
return `btn ${className}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const disabled = getContext<Readable<boolean>>(disabledKey);
|
||||||
|
$: _disabled = disables && $disabled;
|
||||||
|
|
||||||
|
const nightMode = getContext<boolean>(nightModeKey);
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
onMount(() => dispatch("mount", { button: buttonRef }));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@use "ts/sass/button_mixins" as button;
|
||||||
|
|
||||||
|
button {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include button.btn-day;
|
||||||
|
@include button.btn-night;
|
||||||
|
|
||||||
|
span {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
|
||||||
|
/* constrain icon */
|
||||||
|
width: calc(var(--toolbar-size) - 2px);
|
||||||
|
height: calc(var(--toolbar-size) - 2px);
|
||||||
|
|
||||||
|
& > :global(svg),
|
||||||
|
& > :global(img) {
|
||||||
|
fill: currentColor;
|
||||||
|
vertical-align: unset;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-toggle::after {
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<button
|
||||||
|
bind:this={buttonRef}
|
||||||
|
{id}
|
||||||
|
class={extendClassName(className)}
|
||||||
|
class:active
|
||||||
|
class:dropdown-toggle={dropdownToggle}
|
||||||
|
class:btn-day={!nightMode}
|
||||||
|
class:btn-night={nightMode}
|
||||||
|
tabindex="-1"
|
||||||
|
title={tooltip}
|
||||||
|
disabled={_disabled}
|
||||||
|
{...extraProps}
|
||||||
|
on:click={onClick}
|
||||||
|
on:mousedown|preventDefault>
|
||||||
|
<span class="p-1"><slot /></span>
|
||||||
|
</button>
|
8
ts/editor-toolbar/WithDropdownMenu.d.ts
vendored
Normal file
8
ts/editor-toolbar/WithDropdownMenu.d.ts
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
import type { ToolbarItem } from "./types";
|
||||||
|
|
||||||
|
export interface WithDropdownMenuProps {
|
||||||
|
button: ToolbarItem;
|
||||||
|
menuId: string;
|
||||||
|
}
|
53
ts/editor-toolbar/WithDropdownMenu.svelte
Normal file
53
ts/editor-toolbar/WithDropdownMenu.svelte
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="typescript">
|
||||||
|
import type { DynamicSvelteComponent } from "sveltelib/dynamicComponent";
|
||||||
|
import type { ToolbarItem } from "./types";
|
||||||
|
|
||||||
|
import Dropdown from "bootstrap/js/dist/dropdown";
|
||||||
|
|
||||||
|
/* Bootstrap dropdown are normally declared alongside the associated button
|
||||||
|
* However we cannot do that, as the menus cannot be declared in sticky-positioned elements
|
||||||
|
*/
|
||||||
|
export let button: ToolbarItem;
|
||||||
|
export let menuId: string;
|
||||||
|
|
||||||
|
function extend({
|
||||||
|
className,
|
||||||
|
...rest
|
||||||
|
}: DynamicSvelteComponent): DynamicSvelteComponent {
|
||||||
|
return {
|
||||||
|
dropdownToggle: true,
|
||||||
|
...rest,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDropdown({ detail }: CustomEvent): void {
|
||||||
|
const button: HTMLButtonElement = detail.button;
|
||||||
|
|
||||||
|
/* Prevent focus on menu activation */
|
||||||
|
const noop = () => {};
|
||||||
|
Object.defineProperty(button, "focus", { value: noop });
|
||||||
|
|
||||||
|
/* Set custom menu without using .dropdown
|
||||||
|
* Rendering the menu here would cause the menu to
|
||||||
|
* be displayed outside of the visible area
|
||||||
|
*/
|
||||||
|
const dropdown = new Dropdown(button);
|
||||||
|
const menu = (button.getRootNode() as Document) /* or shadow root */
|
||||||
|
.getElementById(menuId);
|
||||||
|
|
||||||
|
if (!menu) {
|
||||||
|
console.log(`Could not find menu "${menuId}" for dropdown menu.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
(dropdown as any)._menu = menu;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:component
|
||||||
|
this={button.component}
|
||||||
|
{...extend(button)}
|
||||||
|
on:mount={createDropdown} />
|
8
ts/editor-toolbar/bootstrap.scss
vendored
Normal file
8
ts/editor-toolbar/bootstrap.scss
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
@import "ts/sass/bootstrap/functions";
|
||||||
|
@import "ts/sass/bootstrap/variables";
|
||||||
|
@import "ts/sass/bootstrap/mixins";
|
||||||
|
|
||||||
|
$btn-disabled-opacity: 0.4;
|
||||||
|
|
||||||
|
@import "ts/sass/bootstrap/buttons";
|
||||||
|
@import "ts/sass/bootstrap/dropdown";
|
48
ts/editor-toolbar/cloze.ts
Normal file
48
ts/editor-toolbar/cloze.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
import IconButton from "./IconButton.svelte";
|
||||||
|
import type { IconButtonProps } from "./IconButton";
|
||||||
|
|
||||||
|
import { DynamicSvelteComponent, dynamicComponent } from "sveltelib/dynamicComponent";
|
||||||
|
import * as tr from "anki/i18n";
|
||||||
|
|
||||||
|
import bracketsIcon from "./code-brackets.svg";
|
||||||
|
|
||||||
|
const clozePattern = /\{\{c(\d+)::/gu;
|
||||||
|
function getCurrentHighestCloze(increment: boolean): number {
|
||||||
|
let highest = 0;
|
||||||
|
|
||||||
|
// @ts-expect-error
|
||||||
|
forEditorField([], (field) => {
|
||||||
|
const matches = field.editingArea.editable.fieldHTML.matchAll(clozePattern);
|
||||||
|
highest = Math.max(
|
||||||
|
highest,
|
||||||
|
...[...matches].map((match: RegExpMatchArray): number => Number(match[1]))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (increment) {
|
||||||
|
highest++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.max(1, highest);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCloze(event: MouseEvent): void {
|
||||||
|
const highestCloze = getCurrentHighestCloze(!event.altKey);
|
||||||
|
|
||||||
|
// @ts-expect-error
|
||||||
|
wrap(`{{c${highestCloze}::`, "}}");
|
||||||
|
}
|
||||||
|
|
||||||
|
const iconButton = dynamicComponent<typeof IconButton, IconButtonProps>(IconButton);
|
||||||
|
|
||||||
|
export function getClozeButton(): DynamicSvelteComponent<typeof IconButton> &
|
||||||
|
IconButtonProps {
|
||||||
|
return iconButton({
|
||||||
|
id: "cloze",
|
||||||
|
icon: bracketsIcon,
|
||||||
|
onClick: onCloze,
|
||||||
|
tooltip: tr.editingClozeDeletionCtrlandshiftandc(),
|
||||||
|
});
|
||||||
|
}
|
7
ts/editor-toolbar/color.scss
Normal file
7
ts/editor-toolbar/color.scss
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
:root {
|
||||||
|
--foreground-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
.forecolor {
|
||||||
|
color: var(--foreground-color) !important;
|
||||||
|
}
|
53
ts/editor-toolbar/color.ts
Normal file
53
ts/editor-toolbar/color.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
import IconButton from "./IconButton.svelte";
|
||||||
|
import type { IconButtonProps } from "./IconButton";
|
||||||
|
import ColorPicker from "./ColorPicker.svelte";
|
||||||
|
import type { ColorPickerProps } from "./ColorPicker";
|
||||||
|
import ButtonGroup from "./ButtonGroup.svelte";
|
||||||
|
import type { ButtonGroupProps } from "./ButtonGroup";
|
||||||
|
|
||||||
|
import { DynamicSvelteComponent, dynamicComponent } from "sveltelib/dynamicComponent";
|
||||||
|
import * as tr from "anki/i18n";
|
||||||
|
|
||||||
|
import squareFillIcon from "./square-fill.svg";
|
||||||
|
import "./color.css";
|
||||||
|
|
||||||
|
const foregroundColorKeyword = "--foreground-color";
|
||||||
|
|
||||||
|
function setForegroundColor(color: string): void {
|
||||||
|
document.documentElement.style.setProperty(foregroundColorKeyword, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getForecolor(): string {
|
||||||
|
return document.documentElement.style.getPropertyValue(foregroundColorKeyword);
|
||||||
|
}
|
||||||
|
|
||||||
|
function wrapWithForecolor(color: string): void {
|
||||||
|
document.execCommand("forecolor", false, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
const iconButton = dynamicComponent<typeof IconButton, IconButtonProps>(IconButton);
|
||||||
|
const colorPicker = dynamicComponent<typeof ColorPicker, ColorPickerProps>(ColorPicker);
|
||||||
|
const buttonGroup = dynamicComponent<typeof ButtonGroup, ButtonGroupProps>(ButtonGroup);
|
||||||
|
|
||||||
|
export function getColorGroup(): DynamicSvelteComponent<typeof ButtonGroup> &
|
||||||
|
ButtonGroupProps {
|
||||||
|
const forecolorButton = iconButton({
|
||||||
|
icon: squareFillIcon,
|
||||||
|
className: "forecolor",
|
||||||
|
onClick: () => wrapWithForecolor(getForecolor()),
|
||||||
|
tooltip: tr.editingSetForegroundColourF7(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const colorpickerButton = colorPicker({
|
||||||
|
onChange: ({ currentTarget }) =>
|
||||||
|
setForegroundColor((currentTarget as HTMLInputElement).value),
|
||||||
|
tooltip: tr.editingChangeColourF8(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return buttonGroup({
|
||||||
|
id: "color",
|
||||||
|
buttons: [forecolorButton, colorpickerButton],
|
||||||
|
});
|
||||||
|
}
|
4
ts/editor-toolbar/contextKeys.ts
Normal file
4
ts/editor-toolbar/contextKeys.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
export const nightModeKey = Symbol("nightMode");
|
||||||
|
export const disabledKey = Symbol("disabled");
|
74
ts/editor-toolbar/format.ts
Normal file
74
ts/editor-toolbar/format.ts
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
import CommandIconButton from "./CommandIconButton.svelte";
|
||||||
|
import type { CommandIconButtonProps } from "./CommandIconButton";
|
||||||
|
import ButtonGroup from "./ButtonGroup.svelte";
|
||||||
|
import type { ButtonGroupProps } from "./ButtonGroup";
|
||||||
|
|
||||||
|
import { DynamicSvelteComponent, dynamicComponent } from "sveltelib/dynamicComponent";
|
||||||
|
import * as tr from "anki/i18n";
|
||||||
|
|
||||||
|
import boldIcon from "./type-bold.svg";
|
||||||
|
import italicIcon from "./type-italic.svg";
|
||||||
|
import underlineIcon from "./type-underline.svg";
|
||||||
|
import superscriptIcon from "./format-superscript.svg";
|
||||||
|
import subscriptIcon from "./format-subscript.svg";
|
||||||
|
import eraserIcon from "./eraser.svg";
|
||||||
|
|
||||||
|
const commandIconButton = dynamicComponent<
|
||||||
|
typeof CommandIconButton,
|
||||||
|
CommandIconButtonProps
|
||||||
|
>(CommandIconButton);
|
||||||
|
const buttonGroup = dynamicComponent<typeof ButtonGroup, ButtonGroupProps>(ButtonGroup);
|
||||||
|
|
||||||
|
export function getFormatGroup(): DynamicSvelteComponent<typeof ButtonGroup> &
|
||||||
|
ButtonGroupProps {
|
||||||
|
const boldButton = commandIconButton({
|
||||||
|
icon: boldIcon,
|
||||||
|
command: "bold",
|
||||||
|
tooltip: tr.editingBoldTextCtrlandb(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const italicButton = commandIconButton({
|
||||||
|
icon: italicIcon,
|
||||||
|
command: "italic",
|
||||||
|
tooltip: tr.editingItalicTextCtrlandi(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const underlineButton = commandIconButton({
|
||||||
|
icon: underlineIcon,
|
||||||
|
command: "underline",
|
||||||
|
tooltip: tr.editingUnderlineTextCtrlandu(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const superscriptButton = commandIconButton({
|
||||||
|
icon: superscriptIcon,
|
||||||
|
command: "superscript",
|
||||||
|
tooltip: tr.editingSuperscriptCtrlandand(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const subscriptButton = commandIconButton({
|
||||||
|
icon: subscriptIcon,
|
||||||
|
command: "subscript",
|
||||||
|
tooltip: tr.editingSubscriptCtrland(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const removeFormatButton = commandIconButton({
|
||||||
|
icon: eraserIcon,
|
||||||
|
command: "removeFormat",
|
||||||
|
activatable: false,
|
||||||
|
tooltip: tr.editingRemoveFormattingCtrlandr(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return buttonGroup({
|
||||||
|
id: "format",
|
||||||
|
buttons: [
|
||||||
|
boldButton,
|
||||||
|
italicButton,
|
||||||
|
underlineButton,
|
||||||
|
superscriptButton,
|
||||||
|
subscriptButton,
|
||||||
|
removeFormatButton,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
49
ts/editor-toolbar/identifiable.ts
Normal file
49
ts/editor-toolbar/identifiable.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
export interface Identifiable {
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalize<T extends Identifiable>(
|
||||||
|
values: T[],
|
||||||
|
idOrIndex: string | number
|
||||||
|
): number {
|
||||||
|
const normalizedIndex =
|
||||||
|
typeof idOrIndex === "string"
|
||||||
|
? values.findIndex((value) => value.id === idOrIndex)
|
||||||
|
: idOrIndex >= 0
|
||||||
|
? idOrIndex
|
||||||
|
: values.length + idOrIndex;
|
||||||
|
|
||||||
|
return normalizedIndex >= values.length ? -1 : normalizedIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function search<T extends Identifiable>(
|
||||||
|
values: T[],
|
||||||
|
idOrIndex: string | number
|
||||||
|
): T | null {
|
||||||
|
const index = normalize(values, idOrIndex);
|
||||||
|
return index >= 0 ? values[index] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function insert<T extends Identifiable>(
|
||||||
|
values: T[],
|
||||||
|
value: T,
|
||||||
|
idOrIndex: string | number
|
||||||
|
): T[] {
|
||||||
|
const index = normalize(values, idOrIndex);
|
||||||
|
return index >= 0
|
||||||
|
? [...values.slice(0, index), value, ...values.slice(index)]
|
||||||
|
: values;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function add<T extends Identifiable>(
|
||||||
|
values: T[],
|
||||||
|
value: T,
|
||||||
|
idOrIndex: string | number
|
||||||
|
): T[] {
|
||||||
|
const index = normalize(values, idOrIndex);
|
||||||
|
return index >= 0
|
||||||
|
? [...values.slice(0, index + 1), value, ...values.slice(index + 1)]
|
||||||
|
: values;
|
||||||
|
}
|
216
ts/editor-toolbar/index.ts
Normal file
216
ts/editor-toolbar/index.ts
Normal file
@ -0,0 +1,216 @@
|
|||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
import type { SvelteComponentDev } from "svelte/internal";
|
||||||
|
import type { ToolbarItem } from "./types";
|
||||||
|
|
||||||
|
import ButtonGroup from "./ButtonGroup.svelte";
|
||||||
|
import type { ButtonGroupProps } from "./ButtonGroup";
|
||||||
|
|
||||||
|
import { dynamicComponent } from "sveltelib/dynamicComponent";
|
||||||
|
import { Writable, writable } from "svelte/store";
|
||||||
|
|
||||||
|
import EditorToolbarSvelte from "./EditorToolbar.svelte";
|
||||||
|
|
||||||
|
import { setupI18n, ModuleName } from "anki/i18n";
|
||||||
|
|
||||||
|
import "./bootstrap.css";
|
||||||
|
|
||||||
|
import { getNotetypeGroup } from "./notetype";
|
||||||
|
import { getFormatGroup } from "./format";
|
||||||
|
import { getColorGroup } from "./color";
|
||||||
|
import { getTemplateGroup, getTemplateMenus } from "./template";
|
||||||
|
import { Identifiable, search, add, insert } from "./identifiable";
|
||||||
|
|
||||||
|
interface Hideable {
|
||||||
|
hidden?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function showComponent(component: Hideable): void {
|
||||||
|
component.hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideComponent(component: Hideable): void {
|
||||||
|
component.hidden = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleComponent(component: Hideable): void {
|
||||||
|
component.hidden = !component.hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
const buttonGroup = dynamicComponent<typeof ButtonGroup, ButtonGroupProps>(ButtonGroup);
|
||||||
|
|
||||||
|
let buttonsResolve: (
|
||||||
|
value: Writable<(ToolbarItem<typeof ButtonGroup> & ButtonGroupProps)[]>
|
||||||
|
) => void;
|
||||||
|
let menusResolve: (value: Writable<ToolbarItem[]>) => void;
|
||||||
|
|
||||||
|
class EditorToolbar extends HTMLElement {
|
||||||
|
component?: SvelteComponentDev;
|
||||||
|
|
||||||
|
buttonsPromise: Promise<
|
||||||
|
Writable<(ToolbarItem<typeof ButtonGroup> & ButtonGroupProps)[]>
|
||||||
|
> = new Promise((resolve) => {
|
||||||
|
buttonsResolve = resolve;
|
||||||
|
});
|
||||||
|
menusPromise: Promise<Writable<ToolbarItem[]>> = new Promise((resolve): void => {
|
||||||
|
menusResolve = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
connectedCallback(): void {
|
||||||
|
setupI18n({ modules: [ModuleName.EDITING] }).then(() => {
|
||||||
|
const buttons = writable([
|
||||||
|
getNotetypeGroup(),
|
||||||
|
getFormatGroup(),
|
||||||
|
getColorGroup(),
|
||||||
|
getTemplateGroup(),
|
||||||
|
]);
|
||||||
|
const menus = writable([...getTemplateMenus()]);
|
||||||
|
|
||||||
|
this.component = new EditorToolbarSvelte({
|
||||||
|
target: this,
|
||||||
|
props: {
|
||||||
|
buttons,
|
||||||
|
menus,
|
||||||
|
nightMode: document.documentElement.classList.contains(
|
||||||
|
"night-mode"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
buttonsResolve(buttons);
|
||||||
|
menusResolve(menus);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateButtonGroup<T>(
|
||||||
|
update: (
|
||||||
|
component: ToolbarItem<typeof ButtonGroup> & ButtonGroupProps & T
|
||||||
|
) => void,
|
||||||
|
group: string | number
|
||||||
|
): void {
|
||||||
|
this.buttonsPromise.then((buttons) => {
|
||||||
|
buttons.update((buttonGroups) => {
|
||||||
|
const foundGroup = search(buttonGroups, group);
|
||||||
|
|
||||||
|
if (foundGroup) {
|
||||||
|
update(
|
||||||
|
foundGroup as ToolbarItem<typeof ButtonGroup> &
|
||||||
|
ButtonGroupProps &
|
||||||
|
T
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return buttonGroups;
|
||||||
|
});
|
||||||
|
|
||||||
|
return buttons;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showButtonGroup(group: string | number): void {
|
||||||
|
this.updateButtonGroup<Hideable>(showComponent, group);
|
||||||
|
}
|
||||||
|
|
||||||
|
hideButtonGroup(group: string | number): void {
|
||||||
|
this.updateButtonGroup<Hideable>(hideComponent, group);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleButtonGroup(group: string | number): void {
|
||||||
|
this.updateButtonGroup<Hideable>(toggleComponent, group);
|
||||||
|
}
|
||||||
|
|
||||||
|
insertButtonGroup(newGroup: ButtonGroupProps, group: string | number = 0): void {
|
||||||
|
this.buttonsPromise.then((buttons) => {
|
||||||
|
buttons.update((buttonGroups) => {
|
||||||
|
const newButtonGroup = buttonGroup(newGroup);
|
||||||
|
return insert(buttonGroups, newButtonGroup, group);
|
||||||
|
});
|
||||||
|
|
||||||
|
return buttons;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
addButtonGroup(newGroup: ButtonGroupProps, group: string | number = -1): void {
|
||||||
|
this.buttonsPromise.then((buttons) => {
|
||||||
|
buttons.update((buttonGroups) => {
|
||||||
|
const newButtonGroup = buttonGroup(newGroup);
|
||||||
|
return add(buttonGroups, newButtonGroup, group);
|
||||||
|
});
|
||||||
|
|
||||||
|
return buttons;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateButton(
|
||||||
|
update: (component: ToolbarItem) => void,
|
||||||
|
group: string | number,
|
||||||
|
button: string | number
|
||||||
|
): void {
|
||||||
|
this.updateButtonGroup((foundGroup) => {
|
||||||
|
const foundButton = search(foundGroup.buttons, button);
|
||||||
|
|
||||||
|
if (foundButton) {
|
||||||
|
update(foundButton);
|
||||||
|
}
|
||||||
|
}, group);
|
||||||
|
}
|
||||||
|
|
||||||
|
showButton(group: string | number, button: string | number): void {
|
||||||
|
this.updateButton(showComponent, group, button);
|
||||||
|
}
|
||||||
|
|
||||||
|
hideButton(group: string | number, button: string | number): void {
|
||||||
|
this.updateButton(hideComponent, group, button);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleButton(group: string | number, button: string | number): void {
|
||||||
|
this.updateButton(toggleComponent, group, button);
|
||||||
|
}
|
||||||
|
|
||||||
|
insertButton(
|
||||||
|
newButton: ToolbarItem & Identifiable,
|
||||||
|
group: string | number,
|
||||||
|
button: string | number = 0
|
||||||
|
): void {
|
||||||
|
this.updateButtonGroup((component) => {
|
||||||
|
component.buttons = insert(
|
||||||
|
component.buttons as (ToolbarItem & Identifiable)[],
|
||||||
|
newButton,
|
||||||
|
button
|
||||||
|
);
|
||||||
|
}, group);
|
||||||
|
}
|
||||||
|
|
||||||
|
addButton(
|
||||||
|
newButton: ToolbarItem & Identifiable,
|
||||||
|
group: string | number,
|
||||||
|
button: string | number = -1
|
||||||
|
): void {
|
||||||
|
this.updateButtonGroup((component) => {
|
||||||
|
component.buttons = add(
|
||||||
|
component.buttons as (ToolbarItem & Identifiable)[],
|
||||||
|
newButton,
|
||||||
|
button
|
||||||
|
);
|
||||||
|
}, group);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("anki-editor-toolbar", EditorToolbar);
|
||||||
|
|
||||||
|
/* Exports for editor
|
||||||
|
* @ts-expect-error */
|
||||||
|
export { updateActiveButtons, clearActiveButtons } from "./CommandIconButton.svelte";
|
||||||
|
export { enableButtons, disableButtons } from "./EditorToolbar.svelte";
|
||||||
|
|
||||||
|
/* Exports for add-ons */
|
||||||
|
export { default as RawButton } from "./RawButton.svelte";
|
||||||
|
export { default as LabelButton } from "./LabelButton.svelte";
|
||||||
|
export { default as IconButton } from "./IconButton.svelte";
|
||||||
|
export { default as CommandIconButton } from "./CommandIconButton.svelte";
|
||||||
|
export { default as SelectButton } from "./SelectButton.svelte";
|
||||||
|
|
||||||
|
export { default as DropdownMenu } from "./DropdownMenu.svelte";
|
||||||
|
export { default as DropdownItem } from "./DropdownItem.svelte";
|
||||||
|
export { default as ButtonDropdown } from "./DropdownMenu.svelte";
|
||||||
|
export { default as WithDropdownMenu } from "./WithDropdownMenu.svelte";
|
10
ts/editor-toolbar/legacy.scss
Normal file
10
ts/editor-toolbar/legacy.scss
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
.linkb {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topbut {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
width: calc(var(--toolbar-size) - 12px);
|
||||||
|
height: calc(var(--toolbar-size) - 12px);
|
||||||
|
}
|
35
ts/editor-toolbar/notetype.ts
Normal file
35
ts/editor-toolbar/notetype.ts
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
import LabelButton from "./LabelButton.svelte";
|
||||||
|
import type { LabelButtonProps } from "./LabelButton";
|
||||||
|
import ButtonGroup from "./ButtonGroup.svelte";
|
||||||
|
import type { ButtonGroupProps } from "./ButtonGroup";
|
||||||
|
|
||||||
|
import { DynamicSvelteComponent, dynamicComponent } from "sveltelib/dynamicComponent";
|
||||||
|
import { bridgeCommand } from "anki/bridgecommand";
|
||||||
|
import * as tr from "anki/i18n";
|
||||||
|
|
||||||
|
const labelButton = dynamicComponent<typeof LabelButton, LabelButtonProps>(LabelButton);
|
||||||
|
const buttonGroup = dynamicComponent<typeof ButtonGroup, ButtonGroupProps>(ButtonGroup);
|
||||||
|
|
||||||
|
export function getNotetypeGroup(): DynamicSvelteComponent<typeof ButtonGroup> &
|
||||||
|
ButtonGroupProps {
|
||||||
|
const fieldsButton = labelButton({
|
||||||
|
onClick: () => bridgeCommand("fields"),
|
||||||
|
disables: false,
|
||||||
|
label: `${tr.editingFields()}...`,
|
||||||
|
tooltip: tr.editingCustomizeFields(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const cardsButton = labelButton({
|
||||||
|
onClick: () => bridgeCommand("cards"),
|
||||||
|
disables: false,
|
||||||
|
label: `${tr.editingCards()}...`,
|
||||||
|
tooltip: tr.editingCustomizeCardTemplatesCtrlandl(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return buttonGroup({
|
||||||
|
id: "notetype",
|
||||||
|
buttons: [fieldsButton, cardsButton],
|
||||||
|
});
|
||||||
|
}
|
144
ts/editor-toolbar/template.ts
Normal file
144
ts/editor-toolbar/template.ts
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
import IconButton from "./IconButton.svelte";
|
||||||
|
import type { IconButtonProps } from "./IconButton";
|
||||||
|
import DropdownMenu from "./DropdownMenu.svelte";
|
||||||
|
import type { DropdownMenuProps } from "./DropdownMenu";
|
||||||
|
import DropdownItem from "./DropdownItem.svelte";
|
||||||
|
import type { DropdownItemProps } from "./DropdownItem";
|
||||||
|
import WithDropdownMenu from "./WithDropdownMenu.svelte";
|
||||||
|
import type { WithDropdownMenuProps } from "./WithDropdownMenu";
|
||||||
|
import ButtonGroup from "./ButtonGroup.svelte";
|
||||||
|
import type { ButtonGroupProps } from "./ButtonGroup";
|
||||||
|
|
||||||
|
import { bridgeCommand } from "anki/bridgecommand";
|
||||||
|
import { DynamicSvelteComponent, dynamicComponent } from "sveltelib/dynamicComponent";
|
||||||
|
import * as tr from "anki/i18n";
|
||||||
|
|
||||||
|
import paperclipIcon from "./paperclip.svg";
|
||||||
|
import micIcon from "./mic.svg";
|
||||||
|
import functionIcon from "./function-variant.svg";
|
||||||
|
import xmlIcon from "./xml.svg";
|
||||||
|
|
||||||
|
import { getClozeButton } from "./cloze";
|
||||||
|
|
||||||
|
function onAttachment(): void {
|
||||||
|
bridgeCommand("attach");
|
||||||
|
}
|
||||||
|
|
||||||
|
function onRecord(): void {
|
||||||
|
bridgeCommand("record");
|
||||||
|
}
|
||||||
|
|
||||||
|
function onHtmlEdit(): void {
|
||||||
|
bridgeCommand("htmlEdit");
|
||||||
|
}
|
||||||
|
|
||||||
|
const mathjaxMenuId = "mathjaxMenu";
|
||||||
|
|
||||||
|
const iconButton = dynamicComponent<typeof IconButton, IconButtonProps>(IconButton);
|
||||||
|
const withDropdownMenu = dynamicComponent<
|
||||||
|
typeof WithDropdownMenu,
|
||||||
|
WithDropdownMenuProps
|
||||||
|
>(WithDropdownMenu);
|
||||||
|
const dropdownMenu = dynamicComponent<typeof DropdownMenu, DropdownMenuProps>(
|
||||||
|
DropdownMenu
|
||||||
|
);
|
||||||
|
const dropdownItem = dynamicComponent<typeof DropdownItem, DropdownItemProps>(
|
||||||
|
DropdownItem
|
||||||
|
);
|
||||||
|
const buttonGroup = dynamicComponent<typeof ButtonGroup, ButtonGroupProps>(ButtonGroup);
|
||||||
|
|
||||||
|
export function getTemplateGroup(): DynamicSvelteComponent<typeof ButtonGroup> &
|
||||||
|
ButtonGroupProps {
|
||||||
|
const attachmentButton = iconButton({
|
||||||
|
icon: paperclipIcon,
|
||||||
|
onClick: onAttachment,
|
||||||
|
tooltip: tr.editingAttachPicturesaudiovideoF3(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const recordButton = iconButton({
|
||||||
|
icon: micIcon,
|
||||||
|
onClick: onRecord,
|
||||||
|
tooltip: tr.editingRecordAudioF5(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const mathjaxButton = iconButton({
|
||||||
|
icon: functionIcon,
|
||||||
|
foo: 5,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mathjaxButtonWithMenu = withDropdownMenu({
|
||||||
|
button: mathjaxButton,
|
||||||
|
menuId: mathjaxMenuId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const htmlButton = iconButton({
|
||||||
|
icon: xmlIcon,
|
||||||
|
onClick: onHtmlEdit,
|
||||||
|
tooltip: tr.editingHtmlEditor,
|
||||||
|
});
|
||||||
|
|
||||||
|
return buttonGroup({
|
||||||
|
id: "template",
|
||||||
|
buttons: [
|
||||||
|
attachmentButton,
|
||||||
|
recordButton,
|
||||||
|
getClozeButton(),
|
||||||
|
mathjaxButtonWithMenu,
|
||||||
|
htmlButton,
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTemplateMenus(): (DynamicSvelteComponent<typeof DropdownMenu> &
|
||||||
|
DropdownMenuProps)[] {
|
||||||
|
const mathjaxMenuItems = [
|
||||||
|
dropdownItem({
|
||||||
|
// @ts-expect-error
|
||||||
|
onClick: () => wrap("\\(", "\\)"),
|
||||||
|
label: tr.editingMathjaxInline(),
|
||||||
|
endLabel: "Ctrl+M, M",
|
||||||
|
}),
|
||||||
|
dropdownItem({
|
||||||
|
// @ts-expect-error
|
||||||
|
onClick: () => wrap("\\[", "\\]"),
|
||||||
|
label: tr.editingMathjaxBlock(),
|
||||||
|
endLabel: "Ctrl+M, E",
|
||||||
|
}),
|
||||||
|
dropdownItem({
|
||||||
|
// @ts-expect-error
|
||||||
|
onClick: () => wrap("\\(\\ce{", "}\\)"),
|
||||||
|
label: tr.editingMathjaxChemistry(),
|
||||||
|
endLabel: "Ctrl+M, C",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const latexMenuItems = [
|
||||||
|
dropdownItem({
|
||||||
|
// @ts-expect-error
|
||||||
|
onClick: () => wrap("[latex]", "[/latex]"),
|
||||||
|
label: tr.editingLatex(),
|
||||||
|
endLabel: "Ctrl+T, T",
|
||||||
|
}),
|
||||||
|
dropdownItem({
|
||||||
|
// @ts-expect-error
|
||||||
|
onClick: () => wrap("[$]", "[/$]"),
|
||||||
|
label: tr.editingLatexEquation(),
|
||||||
|
endLabel: "Ctrl+T, E",
|
||||||
|
}),
|
||||||
|
dropdownItem({
|
||||||
|
// @ts-expect-error
|
||||||
|
onClick: () => wrap("[$$]", "[/$$]"),
|
||||||
|
label: tr.editingLatexMathEnv(),
|
||||||
|
endLabel: "Ctrl+T, M",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const mathjaxMenu = dropdownMenu({
|
||||||
|
id: mathjaxMenuId,
|
||||||
|
menuItems: [...mathjaxMenuItems, ...latexMenuItems],
|
||||||
|
});
|
||||||
|
|
||||||
|
return [mathjaxMenu];
|
||||||
|
}
|
10
ts/editor-toolbar/types.d.ts
vendored
Normal file
10
ts/editor-toolbar/types.d.ts
vendored
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
import type { DynamicSvelteComponent } from "sveltelib/dynamicComponent";
|
||||||
|
import type { SvelteComponentDev } from "svelte/internal";
|
||||||
|
|
||||||
|
interface ToolbarItem<T extends typeof SvelteComponentDev = typeof SvelteComponentDev>
|
||||||
|
extends DynamicSvelteComponent<T> {
|
||||||
|
id?: string;
|
||||||
|
hidden?: boolean;
|
||||||
|
}
|
@ -6,7 +6,6 @@ import type { Editable } from "./editable";
|
|||||||
import { bridgeCommand } from "./lib";
|
import { bridgeCommand } from "./lib";
|
||||||
import { onInput, onKey, onKeyUp } from "./inputHandlers";
|
import { onInput, onKey, onKeyUp } from "./inputHandlers";
|
||||||
import { onFocus, onBlur } from "./focusHandlers";
|
import { onFocus, onBlur } from "./focusHandlers";
|
||||||
import { updateButtonState } from "./toolbar";
|
|
||||||
|
|
||||||
function onPaste(evt: ClipboardEvent): void {
|
function onPaste(evt: ClipboardEvent): void {
|
||||||
bridgeCommand("paste");
|
bridgeCommand("paste");
|
||||||
@ -60,7 +59,8 @@ export class EditingArea extends HTMLDivElement {
|
|||||||
this.addEventListener("paste", onPaste);
|
this.addEventListener("paste", onPaste);
|
||||||
this.addEventListener("copy", onCutOrCopy);
|
this.addEventListener("copy", onCutOrCopy);
|
||||||
this.addEventListener("oncut", onCutOrCopy);
|
this.addEventListener("oncut", onCutOrCopy);
|
||||||
this.addEventListener("mouseup", updateButtonState);
|
// @ts-expect-error
|
||||||
|
this.addEventListener("mouseup", editorToolbar.updateActiveButtons);
|
||||||
|
|
||||||
const baseStyleSheet = this.baseStyle.sheet as CSSStyleSheet;
|
const baseStyleSheet = this.baseStyle.sheet as CSSStyleSheet;
|
||||||
baseStyleSheet.insertRule("anki-editable {}", 0);
|
baseStyleSheet.insertRule("anki-editable {}", 0);
|
||||||
@ -75,7 +75,8 @@ export class EditingArea extends HTMLDivElement {
|
|||||||
this.removeEventListener("paste", onPaste);
|
this.removeEventListener("paste", onPaste);
|
||||||
this.removeEventListener("copy", onCutOrCopy);
|
this.removeEventListener("copy", onCutOrCopy);
|
||||||
this.removeEventListener("oncut", onCutOrCopy);
|
this.removeEventListener("oncut", onCutOrCopy);
|
||||||
this.removeEventListener("mouseup", updateButtonState);
|
// @ts-expect-error
|
||||||
|
this.removeEventListener("mouseup", editorToolbar.updateActiveButtons);
|
||||||
}
|
}
|
||||||
|
|
||||||
initialize(color: string, content: string): void {
|
initialize(color: string, content: string): void {
|
||||||
|
@ -2,7 +2,6 @@
|
|||||||
* 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 */
|
||||||
|
|
||||||
@use 'ts/sass/base';
|
@use 'ts/sass/base';
|
||||||
@use 'ts/sass/buttons';
|
|
||||||
@use 'ts/sass/scrollbar';
|
@use 'ts/sass/scrollbar';
|
||||||
|
|
||||||
.nightMode {
|
.nightMode {
|
||||||
@ -29,97 +28,6 @@
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#topbutsOuter {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: space-between;
|
|
||||||
|
|
||||||
position: sticky;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 5;
|
|
||||||
padding: 2px;
|
|
||||||
|
|
||||||
background: var(--bg-color);
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.topbuts {
|
|
||||||
margin-bottom: 2px;
|
|
||||||
|
|
||||||
& > * {
|
|
||||||
margin: 0 1px;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.topbut {
|
|
||||||
display: inline-block;
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
margin-top: 4px;
|
|
||||||
vertical-align: -0.125em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rainbow {
|
|
||||||
background-image: -webkit-gradient(
|
|
||||||
linear,
|
|
||||||
left top,
|
|
||||||
left bottom,
|
|
||||||
color-stop(0, #f77),
|
|
||||||
color-stop(50%, #7f7),
|
|
||||||
color-stop(100%, #77f)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
button.linkb {
|
|
||||||
-webkit-appearance: none;
|
|
||||||
margin-bottom: -3px;
|
|
||||||
border: 0;
|
|
||||||
box-shadow: none;
|
|
||||||
padding: 0px 2px;
|
|
||||||
background: transparent;
|
|
||||||
|
|
||||||
&:disabled {
|
|
||||||
opacity: 0.3;
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nightMode & > img {
|
|
||||||
filter: invert(180);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
button:focus {
|
|
||||||
outline: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
button.highlighted {
|
|
||||||
#topbutsleft & {
|
|
||||||
background-color: lightgrey;
|
|
||||||
|
|
||||||
.nightMode & {
|
|
||||||
background: linear-gradient(0deg, #333333 0%, #434343 100%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#topbutsright & {
|
|
||||||
border-bottom: 3px solid black;
|
|
||||||
border-radius: 3px;
|
|
||||||
|
|
||||||
.nightMode & {
|
|
||||||
border-bottom-color: white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#dupes {
|
#dupes {
|
||||||
position: sticky;
|
position: sticky;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
@ -5,13 +5,13 @@ import type { EditingArea } from "./editingArea";
|
|||||||
|
|
||||||
import { saveField } from "./changeTimer";
|
import { saveField } from "./changeTimer";
|
||||||
import { bridgeCommand } from "./lib";
|
import { bridgeCommand } from "./lib";
|
||||||
import { enableButtons, disableButtons } from "./toolbar";
|
|
||||||
|
|
||||||
export function onFocus(evt: FocusEvent): void {
|
export function onFocus(evt: FocusEvent): void {
|
||||||
const currentField = evt.currentTarget as EditingArea;
|
const currentField = evt.currentTarget as EditingArea;
|
||||||
currentField.focusEditable();
|
currentField.focusEditable();
|
||||||
bridgeCommand(`focus:${currentField.ord}`);
|
bridgeCommand(`focus:${currentField.ord}`);
|
||||||
enableButtons();
|
// @ts-expect-error
|
||||||
|
editorToolbar.enableButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function onBlur(evt: FocusEvent): void {
|
export function onBlur(evt: FocusEvent): void {
|
||||||
@ -19,5 +19,6 @@ export function onBlur(evt: FocusEvent): void {
|
|||||||
const currentFieldUnchanged = previousFocus === document.activeElement;
|
const currentFieldUnchanged = previousFocus === document.activeElement;
|
||||||
|
|
||||||
saveField(previousFocus, currentFieldUnchanged ? "key" : "blur");
|
saveField(previousFocus, currentFieldUnchanged ? "key" : "blur");
|
||||||
disableButtons();
|
// @ts-expect-error
|
||||||
|
editorToolbar.disableButtons();
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,6 @@ import { filterHTML } from "html-filter";
|
|||||||
|
|
||||||
import { caretToEnd } from "./helpers";
|
import { caretToEnd } from "./helpers";
|
||||||
import { saveField } from "./changeTimer";
|
import { saveField } from "./changeTimer";
|
||||||
import { updateButtonState, disableButtons } from "./toolbar";
|
|
||||||
|
|
||||||
import { EditorField } from "./editorField";
|
import { EditorField } from "./editorField";
|
||||||
import { LabelContainer } from "./labelContainer";
|
import { LabelContainer } from "./labelContainer";
|
||||||
@ -13,7 +12,6 @@ import { EditingArea } from "./editingArea";
|
|||||||
import { Editable } from "./editable";
|
import { Editable } from "./editable";
|
||||||
|
|
||||||
export { setNoteId, getNoteId } from "./noteId";
|
export { setNoteId, getNoteId } from "./noteId";
|
||||||
export { preventButtonFocus, toggleEditorButton, setFGButton } from "./toolbar";
|
|
||||||
export { saveNow } from "./changeTimer";
|
export { saveNow } from "./changeTimer";
|
||||||
export { wrap, wrapIntoText } from "./wrap";
|
export { wrap, wrapIntoText } from "./wrap";
|
||||||
|
|
||||||
@ -43,7 +41,8 @@ export function focusField(n: number): void {
|
|||||||
if (field) {
|
if (field) {
|
||||||
field.editingArea.focusEditable();
|
field.editingArea.focusEditable();
|
||||||
caretToEnd(field.editingArea);
|
caretToEnd(field.editingArea);
|
||||||
updateButtonState();
|
// @ts-expect-error
|
||||||
|
editorToolbar.updateActiveButtons();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -123,7 +122,8 @@ export function setFields(fields: [string, string][]): void {
|
|||||||
|
|
||||||
if (!getCurrentField()) {
|
if (!getCurrentField()) {
|
||||||
// when initial focus of the window is not on editor (e.g. browser)
|
// when initial focus of the window is not on editor (e.g. browser)
|
||||||
disableButtons();
|
// @ts-expect-error
|
||||||
|
editorToolbar.disableButtons();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,6 +158,7 @@ export function setFormat(cmd: string, arg?: any, nosave: boolean = false): void
|
|||||||
document.execCommand(cmd, false, arg);
|
document.execCommand(cmd, false, arg);
|
||||||
if (!nosave) {
|
if (!nosave) {
|
||||||
saveField(getCurrentField() as EditingArea, "key");
|
saveField(getCurrentField() as EditingArea, "key");
|
||||||
updateButtonState();
|
// @ts-expect-error
|
||||||
|
editorToolbar.updateActiveButtons();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@
|
|||||||
import { EditingArea } from "./editingArea";
|
import { EditingArea } from "./editingArea";
|
||||||
import { caretToEnd, nodeIsElement } from "./helpers";
|
import { caretToEnd, nodeIsElement } from "./helpers";
|
||||||
import { triggerChangeTimer } from "./changeTimer";
|
import { triggerChangeTimer } from "./changeTimer";
|
||||||
import { updateButtonState } from "./toolbar";
|
|
||||||
|
|
||||||
function inListItem(currentField: EditingArea): boolean {
|
function inListItem(currentField: EditingArea): boolean {
|
||||||
const anchor = currentField.getSelection()!.anchorNode!;
|
const anchor = currentField.getSelection()!.anchorNode!;
|
||||||
@ -22,7 +21,8 @@ function inListItem(currentField: EditingArea): boolean {
|
|||||||
export function onInput(event: Event): void {
|
export function onInput(event: Event): void {
|
||||||
// make sure IME changes get saved
|
// make sure IME changes get saved
|
||||||
triggerChangeTimer(event.currentTarget as EditingArea);
|
triggerChangeTimer(event.currentTarget as EditingArea);
|
||||||
updateButtonState();
|
// @ts-ignore
|
||||||
|
editorToolbar.updateActiveButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function onKey(evt: KeyboardEvent): void {
|
export function onKey(evt: KeyboardEvent): void {
|
||||||
@ -69,7 +69,8 @@ globalThis.addEventListener("keydown", (evt: KeyboardEvent) => {
|
|||||||
const newFocusTarget = evt.target;
|
const newFocusTarget = evt.target;
|
||||||
if (newFocusTarget instanceof EditingArea) {
|
if (newFocusTarget instanceof EditingArea) {
|
||||||
caretToEnd(newFocusTarget);
|
caretToEnd(newFocusTarget);
|
||||||
updateButtonState();
|
// @ts-ignore
|
||||||
|
editorToolbar.updateActiveButtons();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{ once: true }
|
{ once: true }
|
||||||
|
@ -1,61 +0,0 @@
|
|||||||
// Copyright: Ankitects Pty Ltd and contributors
|
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
||||||
|
|
||||||
const highlightButtons = ["bold", "italic", "underline", "superscript", "subscript"];
|
|
||||||
|
|
||||||
export function updateButtonState(): void {
|
|
||||||
for (const name of highlightButtons) {
|
|
||||||
const elem = document.querySelector(`#${name}`) as HTMLElement;
|
|
||||||
elem.classList.toggle("highlighted", document.queryCommandState(name));
|
|
||||||
}
|
|
||||||
|
|
||||||
// fixme: forecolor
|
|
||||||
// 'col': document.queryCommandValue("forecolor")
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearButtonHighlight(): void {
|
|
||||||
for (const name of highlightButtons) {
|
|
||||||
const elem = document.querySelector(`#${name}`) as HTMLElement;
|
|
||||||
elem.classList.remove("highlighted");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function preventButtonFocus(): void {
|
|
||||||
for (const element of document.querySelectorAll("button.linkb")) {
|
|
||||||
element.addEventListener("mousedown", (evt: Event) => {
|
|
||||||
evt.preventDefault();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function enableButtons(): void {
|
|
||||||
const buttons = document.querySelectorAll(
|
|
||||||
"button.linkb"
|
|
||||||
) as NodeListOf<HTMLButtonElement>;
|
|
||||||
buttons.forEach((elem: HTMLButtonElement): void => {
|
|
||||||
elem.disabled = false;
|
|
||||||
});
|
|
||||||
updateButtonState();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function disableButtons(): void {
|
|
||||||
const buttons = document.querySelectorAll(
|
|
||||||
"button.linkb:not(.perm)"
|
|
||||||
) as NodeListOf<HTMLButtonElement>;
|
|
||||||
buttons.forEach((elem: HTMLButtonElement): void => {
|
|
||||||
elem.disabled = true;
|
|
||||||
});
|
|
||||||
clearButtonHighlight();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setFGButton(col: string): void {
|
|
||||||
document.getElementById("forecolor")!.style.backgroundColor = col;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toggleEditorButton(buttonOrId: string | HTMLElement): void {
|
|
||||||
const button =
|
|
||||||
typeof buttonOrId === "string"
|
|
||||||
? (document.getElementById(buttonOrId) as HTMLElement)
|
|
||||||
: buttonOrId;
|
|
||||||
button.classList.toggle("highlighted");
|
|
||||||
}
|
|
@ -4,6 +4,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
-->
|
-->
|
||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
import type pb from "anki/backend_proto";
|
import type pb from "anki/backend_proto";
|
||||||
|
import type { PreferenceStore } from "sveltelib/preferences";
|
||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
import Graph from "./Graph.svelte";
|
import Graph from "./Graph.svelte";
|
||||||
@ -17,11 +18,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
import type { HistogramData } from "./histogram-graph";
|
import type { HistogramData } from "./histogram-graph";
|
||||||
import { gatherData, buildHistogram } from "./added";
|
import { gatherData, buildHistogram } from "./added";
|
||||||
import type { GraphData } from "./added";
|
import type { GraphData } from "./added";
|
||||||
import type { PreferenceStore } from "./preferences";
|
|
||||||
|
|
||||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||||
import * as tr from "anki/i18n";
|
import * as tr from "anki/i18n";
|
||||||
export let preferences: PreferenceStore;
|
export let preferences: PreferenceStore<pb.BackendProto.GraphPreferences>;
|
||||||
|
|
||||||
let histogramData = null as HistogramData | null;
|
let histogramData = null as HistogramData | null;
|
||||||
let tableData: TableDatum[] = [];
|
let tableData: TableDatum[] = [];
|
||||||
|
@ -4,6 +4,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
-->
|
-->
|
||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
import type pb from "anki/backend_proto";
|
import type pb from "anki/backend_proto";
|
||||||
|
import type { PreferenceStore } from "sveltelib/preferences";
|
||||||
|
|
||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
@ -15,11 +16,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
import { defaultGraphBounds, RevlogRange } from "./graph-helpers";
|
import { defaultGraphBounds, RevlogRange } from "./graph-helpers";
|
||||||
import type { SearchEventMap } from "./graph-helpers";
|
import type { SearchEventMap } from "./graph-helpers";
|
||||||
import { gatherData, renderCalendar } from "./calendar";
|
import { gatherData, renderCalendar } from "./calendar";
|
||||||
import type { PreferenceStore } from "./preferences";
|
|
||||||
import type { GraphData } from "./calendar";
|
import type { GraphData } from "./calendar";
|
||||||
|
|
||||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
export let sourceData: pb.BackendProto.GraphsOut;
|
||||||
export let preferences: PreferenceStore | null = null;
|
export let preferences: PreferenceStore<pb.BackendProto.GraphPreferences>;
|
||||||
export let revlogRange: RevlogRange;
|
export let revlogRange: RevlogRange;
|
||||||
import * as tr from "anki/i18n";
|
import * as tr from "anki/i18n";
|
||||||
export let nightMode: boolean;
|
export let nightMode: boolean;
|
||||||
|
@ -5,6 +5,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher } from "svelte";
|
||||||
import type pb from "anki/backend_proto";
|
import type pb from "anki/backend_proto";
|
||||||
|
import type { PreferenceStore } from "sveltelib/preferences";
|
||||||
|
|
||||||
import Graph from "./Graph.svelte";
|
import Graph from "./Graph.svelte";
|
||||||
import InputBox from "./InputBox.svelte";
|
import InputBox from "./InputBox.svelte";
|
||||||
@ -13,11 +14,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
import type { SearchEventMap } from "./graph-helpers";
|
import type { SearchEventMap } from "./graph-helpers";
|
||||||
import { gatherData, renderCards } from "./card-counts";
|
import { gatherData, renderCards } from "./card-counts";
|
||||||
import type { GraphData, TableDatum } from "./card-counts";
|
import type { GraphData, TableDatum } from "./card-counts";
|
||||||
import type { PreferenceStore } from "./preferences";
|
|
||||||
|
|
||||||
export let sourceData: pb.BackendProto.GraphsOut;
|
export let sourceData: pb.BackendProto.GraphsOut;
|
||||||
import * as tr2 from "anki/i18n";
|
import * as tr2 from "anki/i18n";
|
||||||
export let preferences: PreferenceStore;
|
export let preferences: PreferenceStore<pb.BackendProto.GraphPreferences>;
|
||||||
|
|
||||||
let { cardCountsSeparateInactive, browserLinksSupported } = preferences;
|
let { cardCountsSeparateInactive, browserLinksSupported } = preferences;
|
||||||
const dispatch = createEventDispatcher<SearchEventMap>();
|
const dispatch = createEventDispatcher<SearchEventMap>();
|
||||||
|
@ -4,6 +4,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
-->
|
-->
|
||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
import type pb from "anki/backend_proto";
|
import type pb from "anki/backend_proto";
|
||||||
|
import * as tr from "anki/i18n";
|
||||||
|
import type { PreferenceStore } from "sveltelib/preferences";
|
||||||
|
|
||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
@ -14,11 +16,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
import type { HistogramData } from "./histogram-graph";
|
import type { HistogramData } from "./histogram-graph";
|
||||||
import { gatherData, prepareData } from "./ease";
|
import { gatherData, prepareData } from "./ease";
|
||||||
import type { TableDatum, SearchEventMap } from "./graph-helpers";
|
import type { TableDatum, SearchEventMap } from "./graph-helpers";
|
||||||
import type { PreferenceStore } from "./preferences";
|
|
||||||
|
|
||||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||||
import * as tr from "anki/i18n";
|
export let preferences: PreferenceStore<pb.BackendProto.GraphPreferences>;
|
||||||
export let preferences: PreferenceStore;
|
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<SearchEventMap>();
|
const dispatch = createEventDispatcher<SearchEventMap>();
|
||||||
|
|
||||||
|
@ -12,17 +12,17 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
import HistogramGraph from "./HistogramGraph.svelte";
|
import HistogramGraph from "./HistogramGraph.svelte";
|
||||||
import GraphRangeRadios from "./GraphRangeRadios.svelte";
|
import GraphRangeRadios from "./GraphRangeRadios.svelte";
|
||||||
import TableData from "./TableData.svelte";
|
import TableData from "./TableData.svelte";
|
||||||
|
import type { PreferenceStore } from "sveltelib/preferences";
|
||||||
|
|
||||||
import type { HistogramData } from "./histogram-graph";
|
import type { HistogramData } from "./histogram-graph";
|
||||||
import { GraphRange, RevlogRange } from "./graph-helpers";
|
import { GraphRange, RevlogRange } from "./graph-helpers";
|
||||||
import type { TableDatum, SearchEventMap } from "./graph-helpers";
|
import type { TableDatum, SearchEventMap } from "./graph-helpers";
|
||||||
import { gatherData, buildHistogram } from "./future-due";
|
import { gatherData, buildHistogram } from "./future-due";
|
||||||
import type { GraphData } from "./future-due";
|
import type { GraphData } from "./future-due";
|
||||||
import type { PreferenceStore } from "./preferences";
|
|
||||||
|
|
||||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||||
import * as tr from "anki/i18n";
|
import * as tr from "anki/i18n";
|
||||||
export let preferences: PreferenceStore;
|
export let preferences: PreferenceStore<pb.BackendProto.GraphPreferences>;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<SearchEventMap>();
|
const dispatch = createEventDispatcher<SearchEventMap>();
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
import { timeSpan, MONTH } from "anki/time";
|
import { timeSpan, MONTH } from "anki/time";
|
||||||
|
|
||||||
import type pb from "anki/backend_proto";
|
import type pb from "anki/backend_proto";
|
||||||
|
import type { PreferenceStore } from "sveltelib/preferences";
|
||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher } from "svelte";
|
||||||
|
|
||||||
import Graph from "./Graph.svelte";
|
import Graph from "./Graph.svelte";
|
||||||
@ -21,11 +22,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
} from "./intervals";
|
} from "./intervals";
|
||||||
import type { IntervalGraphData } from "./intervals";
|
import type { IntervalGraphData } from "./intervals";
|
||||||
import type { TableDatum, SearchEventMap } from "./graph-helpers";
|
import type { TableDatum, SearchEventMap } from "./graph-helpers";
|
||||||
import type { PreferenceStore } from "./preferences";
|
|
||||||
|
|
||||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||||
import * as tr from "anki/i18n";
|
import * as tr from "anki/i18n";
|
||||||
export let preferences: PreferenceStore;
|
export let preferences: PreferenceStore<pb.BackendProto.GraphPreferences>;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<SearchEventMap>();
|
const dispatch = createEventDispatcher<SearchEventMap>();
|
||||||
|
|
||||||
|
@ -4,16 +4,46 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
-->
|
-->
|
||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
import type { Writable } from "svelte/store";
|
import type { Writable } from "svelte/store";
|
||||||
|
import type { PreferencePayload } from "sveltelib/preferences";
|
||||||
|
|
||||||
|
import pb from "anki/backend_proto";
|
||||||
|
import { postRequest } from "anki/postrequest";
|
||||||
|
|
||||||
import useAsync from "sveltelib/async";
|
import useAsync from "sveltelib/async";
|
||||||
import useAsyncReactive from "sveltelib/asyncReactive";
|
import useAsyncReactive from "sveltelib/asyncReactive";
|
||||||
|
import { getPreferences } from "sveltelib/preferences";
|
||||||
|
|
||||||
import { getGraphData, daysToRevlogRange } from "./graph-helpers";
|
import { daysToRevlogRange } from "./graph-helpers";
|
||||||
import { getPreferences } from "./preferences";
|
|
||||||
|
|
||||||
export let search: Writable<string>;
|
export let search: Writable<string>;
|
||||||
export let days: Writable<number>;
|
export let days: Writable<number>;
|
||||||
|
|
||||||
|
async function getGraphData(
|
||||||
|
search: string,
|
||||||
|
days: number
|
||||||
|
): Promise<pb.BackendProto.GraphsOut> {
|
||||||
|
return pb.BackendProto.GraphsOut.decode(
|
||||||
|
await postRequest("/_anki/graphData", JSON.stringify({ search, days }))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getGraphPreferences(): Promise<pb.BackendProto.GraphPreferences> {
|
||||||
|
return pb.BackendProto.GraphPreferences.decode(
|
||||||
|
await postRequest("/_anki/graphPreferences", JSON.stringify({}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setGraphPreferences(
|
||||||
|
prefs: PreferencePayload<pb.BackendProto.GraphPreferences>
|
||||||
|
): Promise<void> {
|
||||||
|
await postRequest(
|
||||||
|
"/_anki/setGraphPreferences",
|
||||||
|
new TextDecoder().decode(
|
||||||
|
pb.BackendProto.GraphPreferences.encode(prefs).finish()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
loading: graphLoading,
|
loading: graphLoading,
|
||||||
error: graphError,
|
error: graphError,
|
||||||
@ -24,7 +54,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
loading: prefsLoading,
|
loading: prefsLoading,
|
||||||
error: prefsError,
|
error: prefsError,
|
||||||
value: prefsValue,
|
value: prefsValue,
|
||||||
} = useAsync(() => getPreferences());
|
} = useAsync(() =>
|
||||||
|
getPreferences(
|
||||||
|
getGraphPreferences,
|
||||||
|
setGraphPreferences,
|
||||||
|
pb.BackendProto.GraphPreferences.toObject.bind(
|
||||||
|
pb.BackendProto.GraphPreferences
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
$: revlogRange = daysToRevlogRange($days);
|
$: revlogRange = daysToRevlogRange($days);
|
||||||
|
|
||||||
|
@ -6,34 +6,8 @@
|
|||||||
@typescript-eslint/no-explicit-any: "off",
|
@typescript-eslint/no-explicit-any: "off",
|
||||||
@typescript-eslint/ban-ts-ignore: "off" */
|
@typescript-eslint/ban-ts-ignore: "off" */
|
||||||
|
|
||||||
import pb from "anki/backend_proto";
|
import type pb from "anki/backend_proto";
|
||||||
import type { Selection } from "d3";
|
import type { Selection } from "d3";
|
||||||
import type { PreferencePayload } from "./preferences";
|
|
||||||
import { postRequest } from "anki/postrequest";
|
|
||||||
|
|
||||||
export async function getGraphData(
|
|
||||||
search: string,
|
|
||||||
days: number
|
|
||||||
): Promise<pb.BackendProto.GraphsOut> {
|
|
||||||
return pb.BackendProto.GraphsOut.decode(
|
|
||||||
await postRequest("/_anki/graphData", JSON.stringify({ search, days }))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getGraphPreferences(): Promise<pb.BackendProto.GraphPreferences> {
|
|
||||||
return pb.BackendProto.GraphPreferences.decode(
|
|
||||||
await postRequest("/_anki/graphPreferences", JSON.stringify({}))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function setGraphPreferences(prefs: PreferencePayload): Promise<void> {
|
|
||||||
return (async (): Promise<void> => {
|
|
||||||
await postRequest(
|
|
||||||
"/_anki/setGraphPreferences",
|
|
||||||
pb.BackendProto.GraphPreferences.encode(prefs).finish()
|
|
||||||
);
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
|
||||||
// amount of data to fetch from backend
|
// amount of data to fetch from backend
|
||||||
export enum RevlogRange {
|
export enum RevlogRange {
|
||||||
|
@ -1,79 +0,0 @@
|
|||||||
// Copyright: Ankitects Pty Ltd and contributors
|
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
||||||
// languageServerHost taken from MIT sources - see below.
|
|
||||||
|
|
||||||
import pb from "anki/backend_proto";
|
|
||||||
import { getGraphPreferences, setGraphPreferences } from "./graph-helpers";
|
|
||||||
import { Writable, writable, get } from "svelte/store";
|
|
||||||
|
|
||||||
export interface CustomStore<T> extends Writable<T> {
|
|
||||||
subscribe: (getter: (value: T) => void) => () => void;
|
|
||||||
set: (value: T) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type PreferenceStore = {
|
|
||||||
[K in keyof Omit<pb.BackendProto.GraphPreferences, "toJSON">]: CustomStore<
|
|
||||||
pb.BackendProto.GraphPreferences[K]
|
|
||||||
>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type PreferencePayload = {
|
|
||||||
[K in keyof Omit<
|
|
||||||
pb.BackendProto.GraphPreferences,
|
|
||||||
"toJSON"
|
|
||||||
>]: pb.BackendProto.GraphPreferences[K];
|
|
||||||
};
|
|
||||||
|
|
||||||
function createPreference<T>(
|
|
||||||
initialValue: T,
|
|
||||||
savePreferences: () => void
|
|
||||||
): CustomStore<T> {
|
|
||||||
const { subscribe, set, update } = writable(initialValue);
|
|
||||||
|
|
||||||
return {
|
|
||||||
subscribe,
|
|
||||||
set: (value: T): void => {
|
|
||||||
set(value);
|
|
||||||
savePreferences();
|
|
||||||
},
|
|
||||||
update: (updater: (value: T) => T): void => {
|
|
||||||
update(updater);
|
|
||||||
savePreferences();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function preparePreferences(
|
|
||||||
GraphPreferences: pb.BackendProto.GraphPreferences
|
|
||||||
): PreferenceStore {
|
|
||||||
const preferences: Partial<PreferenceStore> = {};
|
|
||||||
|
|
||||||
function constructPreferences(): PreferencePayload {
|
|
||||||
const payload: Partial<PreferencePayload> = {};
|
|
||||||
|
|
||||||
for (const key in preferences as PreferenceStore) {
|
|
||||||
payload[key] = get(preferences[key]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return payload as PreferencePayload;
|
|
||||||
}
|
|
||||||
|
|
||||||
function savePreferences(): void {
|
|
||||||
setGraphPreferences(constructPreferences());
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(
|
|
||||||
pb.BackendProto.GraphPreferences.toObject(GraphPreferences, {
|
|
||||||
defaults: true,
|
|
||||||
})
|
|
||||||
)) {
|
|
||||||
preferences[key] = createPreference(value, savePreferences);
|
|
||||||
}
|
|
||||||
|
|
||||||
return preferences as PreferenceStore;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getPreferences(): Promise<PreferenceStore> {
|
|
||||||
const initialPreferences = await getGraphPreferences();
|
|
||||||
return preparePreferences(initialPreferences);
|
|
||||||
}
|
|
@ -7,6 +7,21 @@
|
|||||||
"path": "node_modules/@fluent/bundle",
|
"path": "node_modules/@fluent/bundle",
|
||||||
"licenseFile": "node_modules/@fluent/bundle/README.md"
|
"licenseFile": "node_modules/@fluent/bundle/README.md"
|
||||||
},
|
},
|
||||||
|
"@mdi/svg@5.9.55": {
|
||||||
|
"licenses": "Apache-2.0",
|
||||||
|
"repository": "https://github.com/Templarian/MaterialDesign-SVG",
|
||||||
|
"publisher": "Austin Andrews",
|
||||||
|
"path": "node_modules/@mdi/svg",
|
||||||
|
"licenseFile": "node_modules/@mdi/svg/LICENSE"
|
||||||
|
},
|
||||||
|
"@popperjs/core@2.9.2": {
|
||||||
|
"licenses": "MIT",
|
||||||
|
"repository": "https://github.com/popperjs/popper-core",
|
||||||
|
"publisher": "Federico Zivolo",
|
||||||
|
"email": "federico.zivolo@gmail.com",
|
||||||
|
"path": "node_modules/@popperjs/core",
|
||||||
|
"licenseFile": "node_modules/@popperjs/core/LICENSE.md"
|
||||||
|
},
|
||||||
"@protobufjs/aspromise@1.1.2": {
|
"@protobufjs/aspromise@1.1.2": {
|
||||||
"licenses": "BSD-3-Clause",
|
"licenses": "BSD-3-Clause",
|
||||||
"repository": "https://github.com/dcodeIO/protobuf.js",
|
"repository": "https://github.com/dcodeIO/protobuf.js",
|
||||||
|
@ -11,6 +11,7 @@
|
|||||||
"@pyoner/svelte-types": "^3.4.4-2",
|
"@pyoner/svelte-types": "^3.4.4-2",
|
||||||
"@sqltools/formatter": "^1.2.2",
|
"@sqltools/formatter": "^1.2.2",
|
||||||
"@tsconfig/svelte": "^1.0.10",
|
"@tsconfig/svelte": "^1.0.10",
|
||||||
|
"@types/bootstrap": "^5.0.12",
|
||||||
"@types/d3": "^6.3.0",
|
"@types/d3": "^6.3.0",
|
||||||
"@types/diff": "^5.0.0",
|
"@types/diff": "^5.0.0",
|
||||||
"@types/jest": "^26.0.22",
|
"@types/jest": "^26.0.22",
|
||||||
@ -54,6 +55,8 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fluent/bundle": "^0.15.1",
|
"@fluent/bundle": "^0.15.1",
|
||||||
|
"@mdi/svg": "^5.9.55",
|
||||||
|
"@popperjs/core": "^2.9.2",
|
||||||
"bootstrap": "^5.0.0-beta2",
|
"bootstrap": "^5.0.0-beta2",
|
||||||
"bootstrap-icons": "^1.4.0",
|
"bootstrap-icons": "^1.4.0",
|
||||||
"css-browser-selector": "^0.6.5",
|
"css-browser-selector": "^0.6.5",
|
||||||
|
@ -44,6 +44,14 @@ sass_library(
|
|||||||
visibility = ["//visibility:public"],
|
visibility = ["//visibility:public"],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
sass_library(
|
||||||
|
name = "button_mixins_lib",
|
||||||
|
srcs = [
|
||||||
|
"button_mixins.scss",
|
||||||
|
],
|
||||||
|
visibility = ["//visibility:public"],
|
||||||
|
)
|
||||||
|
|
||||||
# qt package extracts colours from source file
|
# qt package extracts colours from source file
|
||||||
exports_files(
|
exports_files(
|
||||||
["_vars.scss"],
|
["_vars.scss"],
|
||||||
|
82
ts/sass/button_mixins.scss
Normal file
82
ts/sass/button_mixins.scss
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
@mixin btn-day($with-disabled: true) {
|
||||||
|
$base-color: white;
|
||||||
|
|
||||||
|
.btn-day {
|
||||||
|
color: var(--text-fg);
|
||||||
|
background-color: $base-color;
|
||||||
|
border-color: var(--medium-border) !important;
|
||||||
|
|
||||||
|
@content ($base-color);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: darken($base-color, 8%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&.active {
|
||||||
|
@include impressed-shadow(0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active.active {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@if ($with-disabled) {
|
||||||
|
&[disabled] {
|
||||||
|
background-color: $base-color !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin btn-night($with-disabled: true) {
|
||||||
|
$base-color: #666;
|
||||||
|
|
||||||
|
.btn-night {
|
||||||
|
color: var(--text-fg);
|
||||||
|
background-color: $base-color;
|
||||||
|
border-color: $base-color;
|
||||||
|
|
||||||
|
@content ($base-color);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: lighten($base-color, 8%);
|
||||||
|
border-color: lighten($base-color, 8%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active,
|
||||||
|
&.active {
|
||||||
|
@include impressed-shadow(0.35);
|
||||||
|
border-color: darken($base-color, 8%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active.active {
|
||||||
|
box-shadow: none;
|
||||||
|
border-color: $base-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
@if ($with-disabled) {
|
||||||
|
&[disabled] {
|
||||||
|
background-color: $base-color !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
border-color: $base-color !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin impressed-shadow($intensity) {
|
||||||
|
box-shadow: inset 0 calc(var(--toolbar-size) / 15) calc(var(--toolbar-size) / 5)
|
||||||
|
rgba(black, $intensity);
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin rainbow($base) {
|
||||||
|
background: content-box
|
||||||
|
linear-gradient(217deg, rgba(255, 0, 0, 0.8), rgba(255, 0, 0, 0) 70.71%),
|
||||||
|
content-box
|
||||||
|
linear-gradient(127deg, rgba(0, 255, 0, 0.8), rgba(0, 255, 0, 0) 70.71%),
|
||||||
|
content-box
|
||||||
|
linear-gradient(336deg, rgba(0, 0, 255, 0.8), rgba(0, 0, 255, 0) 70.71%),
|
||||||
|
border-box $base;
|
||||||
|
}
|
17
ts/sveltelib/WithBase.svelte
Normal file
17
ts/sveltelib/WithBase.svelte
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="typescript">
|
||||||
|
import useAsync from "sveltelib/async";
|
||||||
|
import { setupI18n } from "anki/i18n";
|
||||||
|
import { checkNightMode } from "anki/nightmode";
|
||||||
|
|
||||||
|
const nightMode = checkNightMode();
|
||||||
|
|
||||||
|
const { loading, value: i18n } = useAsync(() => setupI18n());
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if !$loading}
|
||||||
|
<slot i18n={$i18n} {nightMode} />
|
||||||
|
{/if}
|
19
ts/sveltelib/dynamicComponent.ts
Normal file
19
ts/sveltelib/dynamicComponent.ts
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
import type { SvelteComponentDev } from "svelte/internal";
|
||||||
|
|
||||||
|
export interface DynamicSvelteComponent<
|
||||||
|
T extends typeof SvelteComponentDev = typeof SvelteComponentDev
|
||||||
|
> {
|
||||||
|
component: T;
|
||||||
|
[k: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const dynamicComponent = <
|
||||||
|
Comp extends typeof SvelteComponentDev,
|
||||||
|
DefaultProps = NonNullable<ConstructorParameters<Comp>[0]["props"]>
|
||||||
|
>(
|
||||||
|
component: Comp
|
||||||
|
) => <Props = DefaultProps>(props: Props): DynamicSvelteComponent<Comp> & Props => {
|
||||||
|
return { component, ...props };
|
||||||
|
};
|
85
ts/sveltelib/preferences.ts
Normal file
85
ts/sveltelib/preferences.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
// languageServerHost taken from MIT sources - see below.
|
||||||
|
|
||||||
|
import { Writable, writable, get } from "svelte/store";
|
||||||
|
|
||||||
|
// import pb from "anki/backend_proto";
|
||||||
|
// export async function getGraphPreferences(): Promise<pb.BackendProto.GraphPreferences> {
|
||||||
|
// export async function setGraphPreferences(prefs: PreferencePayload): Promise<void> {
|
||||||
|
// pb.BackendProto.GraphPreferences.toObject(Preferences, {
|
||||||
|
|
||||||
|
export interface CustomStore<T> extends Writable<T> {
|
||||||
|
subscribe: (getter: (value: T) => void) => () => void;
|
||||||
|
set: (value: T) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PreferenceStore<T> = {
|
||||||
|
[K in keyof Omit<T, "toJSON">]: CustomStore<T[K]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PreferencePayload<T> = {
|
||||||
|
[K in keyof Omit<T, "toJSON">]: T[K];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type PreferenceRaw<T> = {
|
||||||
|
[K in keyof T]: T[K];
|
||||||
|
};
|
||||||
|
|
||||||
|
function createPreference<T>(
|
||||||
|
initialValue: T,
|
||||||
|
savePreferences: () => void
|
||||||
|
): CustomStore<T> {
|
||||||
|
const { subscribe, set, update } = writable(initialValue);
|
||||||
|
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
set: (value: T): void => {
|
||||||
|
set(value);
|
||||||
|
savePreferences();
|
||||||
|
},
|
||||||
|
update: (updater: (value: T) => T): void => {
|
||||||
|
update(updater);
|
||||||
|
savePreferences();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function preparePreferences<T>(
|
||||||
|
Preferences: T,
|
||||||
|
setter: (payload: PreferencePayload<T>) => Promise<void>,
|
||||||
|
toObject: (preferences: T, options: { defaults: boolean }) => PreferenceRaw<T>
|
||||||
|
): PreferenceStore<T> {
|
||||||
|
const preferences: Partial<PreferenceStore<T>> = {};
|
||||||
|
|
||||||
|
function constructPreferences(): PreferencePayload<T> {
|
||||||
|
const payload: Partial<PreferencePayload<T>> = {};
|
||||||
|
|
||||||
|
for (const key in preferences as PreferenceStore<T>) {
|
||||||
|
payload[key] = get(preferences[key]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return payload as PreferencePayload<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function savePreferences(): void {
|
||||||
|
setter(constructPreferences());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(
|
||||||
|
toObject(Preferences, { defaults: true })
|
||||||
|
)) {
|
||||||
|
preferences[key] = createPreference(value, savePreferences);
|
||||||
|
}
|
||||||
|
|
||||||
|
return preferences as PreferenceStore<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getPreferences<T>(
|
||||||
|
getter: () => Promise<T>,
|
||||||
|
setter: (payload: PreferencePayload<T>) => Promise<void>,
|
||||||
|
toObject: (preferences: T, options: { defaults: boolean }) => PreferenceRaw<T>
|
||||||
|
): Promise<PreferenceStore<T>> {
|
||||||
|
const initialPreferences = await getter();
|
||||||
|
return preparePreferences(initialPreferences, setter, toObject);
|
||||||
|
}
|
@ -4,8 +4,11 @@ Helpers to copy runtime dependencies from node_modules.
|
|||||||
|
|
||||||
load("//ts:copy.bzl", "copy_select_files")
|
load("//ts:copy.bzl", "copy_select_files")
|
||||||
|
|
||||||
|
def _npm_base_from_name(name):
|
||||||
|
return "external/npm/node_modules/{}/".format(name)
|
||||||
|
|
||||||
def _vendor_js_lib_impl(ctx):
|
def _vendor_js_lib_impl(ctx):
|
||||||
base = ctx.attr.base or "external/npm/node_modules/{}/".format(ctx.attr.name)
|
base = ctx.attr.base or _npm_base_from_name(ctx.attr.name)
|
||||||
return copy_select_files(
|
return copy_select_files(
|
||||||
ctx = ctx,
|
ctx = ctx,
|
||||||
files = ctx.attr.pkg.files,
|
files = ctx.attr.pkg.files,
|
||||||
@ -27,7 +30,8 @@ vendor_js_lib = rule(
|
|||||||
)
|
)
|
||||||
|
|
||||||
def pkg_from_name(name):
|
def pkg_from_name(name):
|
||||||
return "@npm//{0}:{0}__files".format(name)
|
tail = name.split("/")[-1]
|
||||||
|
return "@npm//{0}:{1}__files".format(name, tail)
|
||||||
|
|
||||||
#
|
#
|
||||||
# These could be defined directly in BUILD files, but defining them as
|
# These could be defined directly in BUILD files, but defining them as
|
||||||
@ -126,3 +130,14 @@ def copy_bootstrap_icons(name = "bootstrap-icons", icons = [], visibility = ["//
|
|||||||
strip_prefix = "icons/",
|
strip_prefix = "icons/",
|
||||||
visibility = visibility,
|
visibility = visibility,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def copy_mdi_icons(name = "mdi-icons", icons = [], visibility = ["//visibility:public"]):
|
||||||
|
vendor_js_lib(
|
||||||
|
name = name,
|
||||||
|
pkg = pkg_from_name("@mdi/svg"),
|
||||||
|
base = _npm_base_from_name("@mdi/svg"),
|
||||||
|
include = ["svg/{}".format(icon) for icon in icons],
|
||||||
|
strip_prefix = "svg/",
|
||||||
|
visibility = visibility,
|
||||||
|
)
|
||||||
|
|
||||||
|
28
ts/yarn.lock
28
ts/yarn.lock
@ -503,6 +503,11 @@
|
|||||||
"@types/yargs" "^15.0.0"
|
"@types/yargs" "^15.0.0"
|
||||||
chalk "^4.0.0"
|
chalk "^4.0.0"
|
||||||
|
|
||||||
|
"@mdi/svg@^5.9.55":
|
||||||
|
version "5.9.55"
|
||||||
|
resolved "https://registry.yarnpkg.com/@mdi/svg/-/svg-5.9.55.tgz#7cba058135afd5d8a3da977f51b71ffc6a3a3699"
|
||||||
|
integrity sha512-gO0ZpKIeCn9vFg46QduK9MM+n1fuCNwSdcdlBTtbafnnuvwLveK2uj+byhdLtg/8VJGXDhp+DJ35QUMbeWeULA==
|
||||||
|
|
||||||
"@jest/types@^27.0.0-next.8":
|
"@jest/types@^27.0.0-next.8":
|
||||||
version "27.0.0-next.8"
|
version "27.0.0-next.8"
|
||||||
resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.0.0-next.8.tgz#bbc9f2acad3fea3e71444bfe06af522044a38951"
|
resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.0.0-next.8.tgz#bbc9f2acad3fea3e71444bfe06af522044a38951"
|
||||||
@ -514,6 +519,21 @@
|
|||||||
"@types/yargs" "^16.0.0"
|
"@types/yargs" "^16.0.0"
|
||||||
chalk "^4.0.0"
|
chalk "^4.0.0"
|
||||||
|
|
||||||
|
"@mdi/svg@^5.9.55":
|
||||||
|
version "5.9.55"
|
||||||
|
resolved "https://registry.yarnpkg.com/@mdi/svg/-/svg-5.9.55.tgz#7cba058135afd5d8a3da977f51b71ffc6a3a3699"
|
||||||
|
integrity sha512-gO0ZpKIeCn9vFg46QduK9MM+n1fuCNwSdcdlBTtbafnnuvwLveK2uj+byhdLtg/8VJGXDhp+DJ35QUMbeWeULA==
|
||||||
|
|
||||||
|
"@popperjs/core@2.6.0":
|
||||||
|
version "2.6.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.6.0.tgz#f022195afdfc942e088ee2101285a1d31c7d727f"
|
||||||
|
integrity sha512-cPqjjzuFWNK3BSKLm0abspP0sp/IGOli4p5I5fKFAzdS8fvjdOwDCfZqAaIiXd9lPkOWi3SUUfZof3hEb7J/uw==
|
||||||
|
|
||||||
|
"@popperjs/core@^2.9.2":
|
||||||
|
version "2.9.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.9.2.tgz#adea7b6953cbb34651766b0548468e743c6a2353"
|
||||||
|
integrity sha512-VZMYa7+fXHdwIq1TDhSXoVmSPEGM/aa+6Aiq3nVVJ9bXr24zScr+NlKFKC3iPljA7ho/GAZr+d2jOf5GIRC30Q==
|
||||||
|
|
||||||
"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
|
"@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2":
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf"
|
resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf"
|
||||||
@ -629,6 +649,14 @@
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@babel/types" "^7.3.0"
|
"@babel/types" "^7.3.0"
|
||||||
|
|
||||||
|
"@types/bootstrap@^5.0.12":
|
||||||
|
version "5.0.12"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/bootstrap/-/bootstrap-5.0.12.tgz#d044b6404bf3c89fc90df2822a86dfcd349db522"
|
||||||
|
integrity sha512-iowwPfp9Au6aoxS2hOgeRjXE25xdfLrTpmxzQSUs21z5qY3UZpmjSIWF4h8jPYPEXgZioIKLB2OSU8oWzzJAcQ==
|
||||||
|
dependencies:
|
||||||
|
"@popperjs/core" "2.6.0"
|
||||||
|
"@types/jquery" "*"
|
||||||
|
|
||||||
"@types/d3-array@*":
|
"@types/d3-array@*":
|
||||||
version "2.9.0"
|
version "2.9.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-2.9.0.tgz#fb6c3d7d7640259e68771cd90cc5db5ac1a1a012"
|
resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-2.9.0.tgz#fb6c3d7d7640259e68771cd90cc5db5ac1a1a012"
|
||||||
|
Loading…
Reference in New Issue
Block a user