commit
db716b92f9
@ -380,29 +380,30 @@ class Browser(QMainWindow):
|
||||
self.form.gridLayout.addWidget(switch, 0, 0)
|
||||
|
||||
def setupEditor(self) -> None:
|
||||
def add_preview_button(leftbuttons: List[str], editor: Editor) -> None:
|
||||
preview_shortcut = "Ctrl+Shift+P"
|
||||
leftbuttons.insert(
|
||||
0,
|
||||
editor.addButton(
|
||||
None,
|
||||
"preview",
|
||||
lambda _editor: self.onTogglePreview(),
|
||||
tr.browsing_preview_selected_card(
|
||||
val=shortcut(preview_shortcut),
|
||||
),
|
||||
tr.actions_preview(),
|
||||
id="previewButton",
|
||||
keys=preview_shortcut,
|
||||
disables=False,
|
||||
rightside=False,
|
||||
toggleable=True,
|
||||
),
|
||||
def add_preview_button(editor: Editor) -> None:
|
||||
preview_shortcut = "Ctrl+Shift+P" # TODO
|
||||
|
||||
editor._links["preview"] = lambda _editor: self.onTogglePreview()
|
||||
editor.web.eval(
|
||||
f"""
|
||||
$editorToolbar.addButton({{
|
||||
component: editorToolbar.LabelButton,
|
||||
label: `{tr.actions_preview()}`,
|
||||
tooltip: `{tr.browsing_preview_selected_card(val=shortcut(preview_shortcut))}`,
|
||||
onClick: () => bridgeCommand("preview"),
|
||||
disables: false,
|
||||
}}, "notetype");
|
||||
"""
|
||||
)
|
||||
|
||||
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)
|
||||
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
|
||||
def onRowChanged(
|
||||
|
@ -26,11 +26,20 @@ copy_files_into_group(
|
||||
package = "//ts/editor",
|
||||
)
|
||||
|
||||
copy_files_into_group(
|
||||
name = "editor-toolbar",
|
||||
srcs = [
|
||||
"editor-toolbar.css",
|
||||
],
|
||||
package = "//ts/editor-toolbar",
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "css",
|
||||
srcs = [
|
||||
"css_local",
|
||||
"editor",
|
||||
"editor-toolbar",
|
||||
],
|
||||
visibility = ["//qt:__subpackages__"],
|
||||
)
|
||||
|
@ -37,11 +37,20 @@ copy_files_into_group(
|
||||
package = "//ts/editor",
|
||||
)
|
||||
|
||||
copy_files_into_group(
|
||||
name = "editor-toolbar",
|
||||
srcs = [
|
||||
"editor-toolbar.js",
|
||||
],
|
||||
package = "//ts/editor-toolbar",
|
||||
)
|
||||
|
||||
filegroup(
|
||||
name = "js",
|
||||
srcs = [
|
||||
"aqt_es5",
|
||||
"editor",
|
||||
"editor-toolbar",
|
||||
"mathjax.js",
|
||||
"//qt/aqt/data/web/js/vendor",
|
||||
],
|
||||
|
161
qt/aqt/editor.py
161
qt/aqt/editor.py
@ -82,9 +82,7 @@ _html = """
|
||||
}
|
||||
</style>
|
||||
<div>
|
||||
<div id="topbutsOuter">
|
||||
%s
|
||||
</div>
|
||||
<anki-editor-toolbar id="editorToolbar"></anki-editor-toolbar>
|
||||
<div id="fields">
|
||||
</div>
|
||||
<div id="dupes" class="is-inactive">
|
||||
@ -137,111 +135,62 @@ class Editor:
|
||||
self.web.set_bridge_command(self.onBridgeCmd, self)
|
||||
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
|
||||
# then load page
|
||||
self.web.stdHtml(
|
||||
_html % (bgcol, topbuts, tr.editing_show_duplicates()),
|
||||
_html % (bgcol, tr.editing_show_duplicates()),
|
||||
css=[
|
||||
"css/editor.css",
|
||||
"css/editor-toolbar.css",
|
||||
],
|
||||
js=[
|
||||
"js/vendor/jquery.min.js",
|
||||
"js/vendor/protobuf.min.js",
|
||||
"js/editor.js",
|
||||
"js/editor-toolbar.js",
|
||||
],
|
||||
context=self,
|
||||
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
|
||||
######################################################################
|
||||
@ -353,6 +302,7 @@ class Editor:
|
||||
type="button"
|
||||
title="{tip}"
|
||||
onclick="pycmd('{cmd}');{togglesc}return false;"
|
||||
onmousedown="window.event.preventDefault();"
|
||||
>
|
||||
{imgelm}
|
||||
{labelelm}
|
||||
@ -801,7 +751,8 @@ class Editor:
|
||||
self._wrapWithColour(self.fcolour)
|
||||
|
||||
def _updateForegroundButton(self) -> None:
|
||||
self.web.eval(f"setFGButton('{self.fcolour}')")
|
||||
# self.web.eval(f"setFGButton('{self.fcolour}')")
|
||||
pass
|
||||
|
||||
def onColourChanged(self) -> None:
|
||||
self._updateForegroundButton()
|
||||
@ -1112,6 +1063,10 @@ class Editor:
|
||||
dupes=showDupes,
|
||||
paste=onPaste,
|
||||
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(remove_null_bytes)
|
||||
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")
|
||||
|
||||
compile_sass(
|
||||
srcs = ["deckconfig-base.scss"],
|
||||
group = "base_css",
|
||||
srcs = ["deckconfig-base.scss"],
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//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 { onInput, onKey, onKeyUp } from "./inputHandlers";
|
||||
import { onFocus, onBlur } from "./focusHandlers";
|
||||
import { updateButtonState } from "./toolbar";
|
||||
|
||||
function onPaste(evt: ClipboardEvent): void {
|
||||
bridgeCommand("paste");
|
||||
@ -60,7 +59,8 @@ export class EditingArea extends HTMLDivElement {
|
||||
this.addEventListener("paste", onPaste);
|
||||
this.addEventListener("copy", onCutOrCopy);
|
||||
this.addEventListener("oncut", onCutOrCopy);
|
||||
this.addEventListener("mouseup", updateButtonState);
|
||||
// @ts-expect-error
|
||||
this.addEventListener("mouseup", editorToolbar.updateActiveButtons);
|
||||
|
||||
const baseStyleSheet = this.baseStyle.sheet as CSSStyleSheet;
|
||||
baseStyleSheet.insertRule("anki-editable {}", 0);
|
||||
@ -75,7 +75,8 @@ export class EditingArea extends HTMLDivElement {
|
||||
this.removeEventListener("paste", onPaste);
|
||||
this.removeEventListener("copy", onCutOrCopy);
|
||||
this.removeEventListener("oncut", onCutOrCopy);
|
||||
this.removeEventListener("mouseup", updateButtonState);
|
||||
// @ts-expect-error
|
||||
this.removeEventListener("mouseup", editorToolbar.updateActiveButtons);
|
||||
}
|
||||
|
||||
initialize(color: string, content: string): void {
|
||||
|
@ -2,7 +2,6 @@
|
||||
* License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */
|
||||
|
||||
@use 'ts/sass/base';
|
||||
@use 'ts/sass/buttons';
|
||||
@use 'ts/sass/scrollbar';
|
||||
|
||||
.nightMode {
|
||||
@ -29,97 +28,6 @@
|
||||
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 {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
|
@ -5,13 +5,13 @@ import type { EditingArea } from "./editingArea";
|
||||
|
||||
import { saveField } from "./changeTimer";
|
||||
import { bridgeCommand } from "./lib";
|
||||
import { enableButtons, disableButtons } from "./toolbar";
|
||||
|
||||
export function onFocus(evt: FocusEvent): void {
|
||||
const currentField = evt.currentTarget as EditingArea;
|
||||
currentField.focusEditable();
|
||||
bridgeCommand(`focus:${currentField.ord}`);
|
||||
enableButtons();
|
||||
// @ts-expect-error
|
||||
editorToolbar.enableButtons();
|
||||
}
|
||||
|
||||
export function onBlur(evt: FocusEvent): void {
|
||||
@ -19,5 +19,6 @@ export function onBlur(evt: FocusEvent): void {
|
||||
const currentFieldUnchanged = previousFocus === document.activeElement;
|
||||
|
||||
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 { saveField } from "./changeTimer";
|
||||
import { updateButtonState, disableButtons } from "./toolbar";
|
||||
|
||||
import { EditorField } from "./editorField";
|
||||
import { LabelContainer } from "./labelContainer";
|
||||
@ -13,7 +12,6 @@ import { EditingArea } from "./editingArea";
|
||||
import { Editable } from "./editable";
|
||||
|
||||
export { setNoteId, getNoteId } from "./noteId";
|
||||
export { preventButtonFocus, toggleEditorButton, setFGButton } from "./toolbar";
|
||||
export { saveNow } from "./changeTimer";
|
||||
export { wrap, wrapIntoText } from "./wrap";
|
||||
|
||||
@ -43,7 +41,8 @@ export function focusField(n: number): void {
|
||||
if (field) {
|
||||
field.editingArea.focusEditable();
|
||||
caretToEnd(field.editingArea);
|
||||
updateButtonState();
|
||||
// @ts-expect-error
|
||||
editorToolbar.updateActiveButtons();
|
||||
}
|
||||
}
|
||||
|
||||
@ -123,7 +122,8 @@ export function setFields(fields: [string, string][]): void {
|
||||
|
||||
if (!getCurrentField()) {
|
||||
// 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);
|
||||
if (!nosave) {
|
||||
saveField(getCurrentField() as EditingArea, "key");
|
||||
updateButtonState();
|
||||
// @ts-expect-error
|
||||
editorToolbar.updateActiveButtons();
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,6 @@
|
||||
import { EditingArea } from "./editingArea";
|
||||
import { caretToEnd, nodeIsElement } from "./helpers";
|
||||
import { triggerChangeTimer } from "./changeTimer";
|
||||
import { updateButtonState } from "./toolbar";
|
||||
|
||||
function inListItem(currentField: EditingArea): boolean {
|
||||
const anchor = currentField.getSelection()!.anchorNode!;
|
||||
@ -22,7 +21,8 @@ function inListItem(currentField: EditingArea): boolean {
|
||||
export function onInput(event: Event): void {
|
||||
// make sure IME changes get saved
|
||||
triggerChangeTimer(event.currentTarget as EditingArea);
|
||||
updateButtonState();
|
||||
// @ts-ignore
|
||||
editorToolbar.updateActiveButtons();
|
||||
}
|
||||
|
||||
export function onKey(evt: KeyboardEvent): void {
|
||||
@ -69,7 +69,8 @@ globalThis.addEventListener("keydown", (evt: KeyboardEvent) => {
|
||||
const newFocusTarget = evt.target;
|
||||
if (newFocusTarget instanceof EditingArea) {
|
||||
caretToEnd(newFocusTarget);
|
||||
updateButtonState();
|
||||
// @ts-ignore
|
||||
editorToolbar.updateActiveButtons();
|
||||
}
|
||||
},
|
||||
{ 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">
|
||||
import type pb from "anki/backend_proto";
|
||||
import type { PreferenceStore } from "sveltelib/preferences";
|
||||
import { createEventDispatcher } from "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 { gatherData, buildHistogram } from "./added";
|
||||
import type { GraphData } from "./added";
|
||||
import type { PreferenceStore } from "./preferences";
|
||||
|
||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||
import * as tr from "anki/i18n";
|
||||
export let preferences: PreferenceStore;
|
||||
export let preferences: PreferenceStore<pb.BackendProto.GraphPreferences>;
|
||||
|
||||
let histogramData = null as HistogramData | null;
|
||||
let tableData: TableDatum[] = [];
|
||||
|
@ -4,6 +4,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
import type pb from "anki/backend_proto";
|
||||
import type { PreferenceStore } from "sveltelib/preferences";
|
||||
|
||||
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 type { SearchEventMap } from "./graph-helpers";
|
||||
import { gatherData, renderCalendar } from "./calendar";
|
||||
import type { PreferenceStore } from "./preferences";
|
||||
import type { GraphData } from "./calendar";
|
||||
|
||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||
export let preferences: PreferenceStore | null = null;
|
||||
export let sourceData: pb.BackendProto.GraphsOut;
|
||||
export let preferences: PreferenceStore<pb.BackendProto.GraphPreferences>;
|
||||
export let revlogRange: RevlogRange;
|
||||
import * as tr from "anki/i18n";
|
||||
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">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import type pb from "anki/backend_proto";
|
||||
import type { PreferenceStore } from "sveltelib/preferences";
|
||||
|
||||
import Graph from "./Graph.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 { gatherData, renderCards } from "./card-counts";
|
||||
import type { GraphData, TableDatum } from "./card-counts";
|
||||
import type { PreferenceStore } from "./preferences";
|
||||
|
||||
export let sourceData: pb.BackendProto.GraphsOut;
|
||||
import * as tr2 from "anki/i18n";
|
||||
export let preferences: PreferenceStore;
|
||||
export let preferences: PreferenceStore<pb.BackendProto.GraphPreferences>;
|
||||
|
||||
let { cardCountsSeparateInactive, browserLinksSupported } = preferences;
|
||||
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">
|
||||
import type pb from "anki/backend_proto";
|
||||
import * as tr from "anki/i18n";
|
||||
import type { PreferenceStore } from "sveltelib/preferences";
|
||||
|
||||
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 { gatherData, prepareData } from "./ease";
|
||||
import type { TableDatum, SearchEventMap } from "./graph-helpers";
|
||||
import type { PreferenceStore } from "./preferences";
|
||||
|
||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||
import * as tr from "anki/i18n";
|
||||
export let preferences: PreferenceStore;
|
||||
export let preferences: PreferenceStore<pb.BackendProto.GraphPreferences>;
|
||||
|
||||
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 GraphRangeRadios from "./GraphRangeRadios.svelte";
|
||||
import TableData from "./TableData.svelte";
|
||||
import type { PreferenceStore } from "sveltelib/preferences";
|
||||
|
||||
import type { HistogramData } from "./histogram-graph";
|
||||
import { GraphRange, RevlogRange } from "./graph-helpers";
|
||||
import type { TableDatum, SearchEventMap } from "./graph-helpers";
|
||||
import { gatherData, buildHistogram } from "./future-due";
|
||||
import type { GraphData } from "./future-due";
|
||||
import type { PreferenceStore } from "./preferences";
|
||||
|
||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||
import * as tr from "anki/i18n";
|
||||
export let preferences: PreferenceStore;
|
||||
export let preferences: PreferenceStore<pb.BackendProto.GraphPreferences>;
|
||||
|
||||
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 type pb from "anki/backend_proto";
|
||||
import type { PreferenceStore } from "sveltelib/preferences";
|
||||
import { createEventDispatcher } from "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";
|
||||
import type { IntervalGraphData } from "./intervals";
|
||||
import type { TableDatum, SearchEventMap } from "./graph-helpers";
|
||||
import type { PreferenceStore } from "./preferences";
|
||||
|
||||
export let sourceData: pb.BackendProto.GraphsOut | null = null;
|
||||
import * as tr from "anki/i18n";
|
||||
export let preferences: PreferenceStore;
|
||||
export let preferences: PreferenceStore<pb.BackendProto.GraphPreferences>;
|
||||
|
||||
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">
|
||||
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 useAsyncReactive from "sveltelib/asyncReactive";
|
||||
import { getPreferences } from "sveltelib/preferences";
|
||||
|
||||
import { getGraphData, daysToRevlogRange } from "./graph-helpers";
|
||||
import { getPreferences } from "./preferences";
|
||||
import { daysToRevlogRange } from "./graph-helpers";
|
||||
|
||||
export let search: Writable<string>;
|
||||
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 {
|
||||
loading: graphLoading,
|
||||
error: graphError,
|
||||
@ -24,7 +54,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
loading: prefsLoading,
|
||||
error: prefsError,
|
||||
value: prefsValue,
|
||||
} = useAsync(() => getPreferences());
|
||||
} = useAsync(() =>
|
||||
getPreferences(
|
||||
getGraphPreferences,
|
||||
setGraphPreferences,
|
||||
pb.BackendProto.GraphPreferences.toObject.bind(
|
||||
pb.BackendProto.GraphPreferences
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
$: revlogRange = daysToRevlogRange($days);
|
||||
|
||||
|
@ -6,34 +6,8 @@
|
||||
@typescript-eslint/no-explicit-any: "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 { 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
|
||||
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",
|
||||
"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": {
|
||||
"licenses": "BSD-3-Clause",
|
||||
"repository": "https://github.com/dcodeIO/protobuf.js",
|
||||
|
@ -11,6 +11,7 @@
|
||||
"@pyoner/svelte-types": "^3.4.4-2",
|
||||
"@sqltools/formatter": "^1.2.2",
|
||||
"@tsconfig/svelte": "^1.0.10",
|
||||
"@types/bootstrap": "^5.0.12",
|
||||
"@types/d3": "^6.3.0",
|
||||
"@types/diff": "^5.0.0",
|
||||
"@types/jest": "^26.0.22",
|
||||
@ -54,6 +55,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@fluent/bundle": "^0.15.1",
|
||||
"@mdi/svg": "^5.9.55",
|
||||
"@popperjs/core": "^2.9.2",
|
||||
"bootstrap": "^5.0.0-beta2",
|
||||
"bootstrap-icons": "^1.4.0",
|
||||
"css-browser-selector": "^0.6.5",
|
||||
|
@ -44,6 +44,14 @@ sass_library(
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
sass_library(
|
||||
name = "button_mixins_lib",
|
||||
srcs = [
|
||||
"button_mixins.scss",
|
||||
],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
# qt package extracts colours from source file
|
||||
exports_files(
|
||||
["_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")
|
||||
|
||||
def _npm_base_from_name(name):
|
||||
return "external/npm/node_modules/{}/".format(name)
|
||||
|
||||
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(
|
||||
ctx = ctx,
|
||||
files = ctx.attr.pkg.files,
|
||||
@ -27,7 +30,8 @@ vendor_js_lib = rule(
|
||||
)
|
||||
|
||||
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
|
||||
@ -126,3 +130,14 @@ def copy_bootstrap_icons(name = "bootstrap-icons", icons = [], visibility = ["//
|
||||
strip_prefix = "icons/",
|
||||
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"
|
||||
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":
|
||||
version "27.0.0-next.8"
|
||||
resolved "https://registry.yarnpkg.com/@jest/types/-/types-27.0.0-next.8.tgz#bbc9f2acad3fea3e71444bfe06af522044a38951"
|
||||
@ -514,6 +519,21 @@
|
||||
"@types/yargs" "^16.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":
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz#9b8b0cc663d669a7d8f6f5d0893a14d348f30fbf"
|
||||
@ -629,6 +649,14 @@
|
||||
dependencies:
|
||||
"@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@*":
|
||||
version "2.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-array/-/d3-array-2.9.0.tgz#fb6c3d7d7640259e68771cd90cc5db5ac1a1a012"
|
||||
|
Loading…
Reference in New Issue
Block a user