Merge pull request #1109 from hgiesel/toolbar

Svelte Editor Toolbar
This commit is contained in:
Damien Elmes 2021-04-16 10:22:41 +10:00 committed by GitHub
commit db716b92f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
64 changed files with 2093 additions and 409 deletions

View File

@ -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(

View File

@ -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__"],
)

View File

@ -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",
],

View File

@ -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)

View File

@ -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",

View 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",
],
)

View 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
View 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[];
}

View 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
View 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;
}

View 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>

View 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;
}

View 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
View 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;
}

View 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
View 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[];
}

View 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>

View 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
View 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;
}

View 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
View 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;
}

View 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>

View 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}

View 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>

View 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>

View 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>

View 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;
}

View 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
View 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";

View 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(),
});
}

View File

@ -0,0 +1,7 @@
:root {
--foreground-color: black;
}
.forecolor {
color: var(--foreground-color) !important;
}

View 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],
});
}

View 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");

View 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,
],
});
}

View 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
View 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";

View 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);
}

View 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],
});
}

View 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
View 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;
}

View File

@ -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 {

View File

@ -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;

View File

@ -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();
}

View File

@ -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();
}
}

View File

@ -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 }

View File

@ -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");
}

View File

@ -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[] = [];

View File

@ -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;

View File

@ -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>();

View File

@ -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>();

View File

@ -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>();

View File

@ -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>();

View File

@ -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);

View File

@ -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 {

View File

@ -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);
}

View File

@ -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",

View File

@ -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",

View File

@ -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"],

View 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;
}

View 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}

View 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 };
};

View 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);
}

View File

@ -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,
)

View File

@ -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"