Reduce duplication across modal components

This commit is contained in:
Abdo 2025-08-09 05:27:51 +03:00
parent 93f38924a6
commit 3f6d904236
5 changed files with 249 additions and 337 deletions

View file

@ -6,16 +6,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import * as tr from "@generated/ftl";
import { renderMarkdown } from "@tslib/helpers";
import Carousel from "bootstrap/js/dist/carousel";
import Modal from "bootstrap/js/dist/modal";
import { createEventDispatcher, getContext, onDestroy, onMount } from "svelte";
import { createEventDispatcher, onMount } from "svelte";
import Modal from "./Modal.svelte";
import { infoCircle } from "$lib/components/icons";
import { registerModalClosingHandler } from "$lib/sveltelib/modal-closing";
import { pageTheme } from "$lib/sveltelib/theme";
import Badge from "./Badge.svelte";
import Col from "./Col.svelte";
import { modalsKey } from "./context-keys";
import HelpSection from "./HelpSection.svelte";
import Icon from "./Icon.svelte";
import Row from "./Row.svelte";
@ -27,50 +25,20 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export let helpSections: HelpItem[];
export let fsrs = false;
export const modalKey: string = Math.random().toString(36).substring(2);
const modals = getContext<Map<string, Modal>>(modalsKey);
let modal: Modal;
let carousel: Carousel;
let modalRef: HTMLDivElement;
let modal: Modal;
let carouselRef: HTMLDivElement;
function onOkClicked(): void {
modal.hide();
}
const dispatch = createEventDispatcher();
const { set: setModalOpen, remove: removeModalClosingHandler } =
registerModalClosingHandler(onOkClicked);
function onShown() {
setModalOpen(true);
}
function onHidden() {
setModalOpen(false);
}
onMount(() => {
modalRef.addEventListener("shown.bs.modal", onShown);
modalRef.addEventListener("hidden.bs.modal", onHidden);
modal = new Modal(modalRef, { keyboard: false });
carousel = new Carousel(carouselRef, { interval: false, ride: false });
/* Bootstrap's Carousel.Event interface doesn't seem to work as a type here */
carouselRef.addEventListener("slide.bs.carousel", (e: any) => {
activeIndex = e.to;
});
dispatch("mount", { modal: modal, carousel: carousel });
modals.set(modalKey, modal);
});
onDestroy(() => {
removeModalClosingHandler();
modalRef.removeEventListener("shown.bs.modal", onShown);
modalRef.removeEventListener("hidden.bs.modal", onHidden);
});
let activeIndex = startIndex;
@ -80,119 +48,98 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<Icon icon={infoCircle} />
</Badge>
<div
bind:this={modalRef}
class="modal fade"
tabindex="-1"
aria-labelledby="modalLabel"
aria-hidden="true"
>
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<div style="display: flex;">
<h1 class="modal-title" id="modalLabel">
{title}
</h1>
<button
type="button"
class="btn-close"
class:invert={$pageTheme.isDark}
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
{#if url}
<div class="chapter-redirect">
{@html renderMarkdown(
tr.helpForMoreInfo({
link: `<a href="${url}" title="${tr.helpOpenManualChapter(
{
name: title,
},
)}">${title}</a>`,
}),
)}
</div>
{/if}
<Modal bind:this={modal} dialogClass="modal-lg">
<div slot="header" class="modal-header">
<div style="display: flex;">
<h1 class="modal-title" id="modalLabel">
{title}
</h1>
<button
type="button"
class="btn-close"
class:invert={$pageTheme.isDark}
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
{#if url}
<div class="chapter-redirect">
{@html renderMarkdown(
tr.helpForMoreInfo({
link: `<a href="${url}" title="${tr.helpOpenManualChapter({
name: title,
})}">${title}</a>`,
}),
)}
</div>
<div class="modal-body">
<Row --cols={4}>
<Col --col-size={1}>
<nav>
<div id="nav">
<ul>
{#each helpSections as item, i}
<li>
<button
on:click={() => {
activeIndex = i;
carousel.to(activeIndex);
}}
class:active={i == activeIndex}
class:d-none={fsrs
? item.sched ===
HelpItemScheduler.SM2
: item.sched ==
HelpItemScheduler.FSRS}
>
{item.title}
</button>
</li>
{/each}
</ul>
</div>
</nav>
</Col>
<Col --col-size={3}>
<div
id="helpSectionIndicators"
class="carousel slide"
bind:this={carouselRef}
>
<div class="carousel-inner">
{#each helpSections as item, i}
<div
class="carousel-item"
class:active={i == startIndex}
{/if}
</div>
<div slot="body" class="modal-body">
<Row --cols={4}>
<Col --col-size={1}>
<nav>
<div id="nav">
<ul>
{#each helpSections as item, i}
<li>
<button
on:click={() => {
activeIndex = i;
carousel.to(activeIndex);
}}
class:active={i == activeIndex}
class:d-none={fsrs
? item.sched === HelpItemScheduler.SM2
: item.sched == HelpItemScheduler.FSRS}
>
<HelpSection {item} />
</div>
{/each}
{item.title}
</button>
</li>
{/each}
</ul>
</div>
</nav>
</Col>
<Col --col-size={3}>
<div
id="helpSectionIndicators"
class="carousel slide"
bind:this={carouselRef}
>
<div class="carousel-inner">
{#each helpSections as item, i}
<div
class="carousel-item"
class:active={i == startIndex}
class:d-none={fsrs
? item.sched === HelpItemScheduler.SM2
: item.sched == HelpItemScheduler.FSRS}
>
<HelpSection {item} />
</div>
</div>
</Col>
</Row>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" on:click={onOkClicked}>
{tr.helpOk()}
</button>
</div>
</div>
{/each}
</div>
</div>
</Col>
</Row>
</div>
</div>
<div slot="footer" class="modal-footer">
<button type="button" class="btn btn-primary" on:click={modal.onOkClicked}>
{tr.helpOk()}
</button>
</div>
</Modal>
<style lang="scss">
#nav {
margin-bottom: 1.5rem;
}
.modal {
z-index: 1066;
background-color: rgba($color: black, $alpha: 0.5);
}
.modal-title {
margin-inline-end: 0.75rem;
}
.modal-content {
background-color: var(--canvas);
color: var(--fg);
:global(.modal-content) {
border-radius: var(--border-radius-medium, 10px);
}

View file

@ -0,0 +1,98 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import Modal from "bootstrap/js/dist/modal";
import { getContext, onDestroy, onMount } from "svelte";
import { registerModalClosingHandler } from "$lib/sveltelib/modal-closing";
import { modalsKey } from "./context-keys";
export let modalKey: string = Math.random().toString(36).substring(2);
export const dialogClass: string = "";
export const onOkClicked: () => void = () => {};
export const onCancelClicked: () => void = () => {};
export const onShown: () => void = () => {};
export const onHidden: () => void = () => {};
const modals = getContext<Map<string, Modal>>(modalsKey);
let modal: Modal;
let modalRef: HTMLDivElement;
function onOkClicked_(): void {
modal.hide();
onOkClicked();
}
function onCancelClicked_(): void {
modal.hide();
onCancelClicked();
}
export function show(): void {
modal.show();
}
export function hide(): void {
modal.hide();
}
export { onOkClicked_ as acceptHandler, onCancelClicked_ as cancelHandler };
const { set: setModalOpen, remove: removeModalClosingHandler } =
registerModalClosingHandler(onOkClicked_);
function onShown_() {
setModalOpen(true);
onShown();
}
function onHidden_() {
setModalOpen(false);
onHidden();
}
onMount(() => {
modalRef.addEventListener("shown.bs.modal", onShown_);
modalRef.addEventListener("hidden.bs.modal", onHidden_);
modal = new Modal(modalRef, { keyboard: false });
modals.set(modalKey, modal);
});
onDestroy(() => {
removeModalClosingHandler();
modalRef.removeEventListener("shown.bs.modal", onShown_);
modalRef.removeEventListener("hidden.bs.modal", onHidden_);
});
</script>
<div
bind:this={modalRef}
class="modal fade"
tabindex="-1"
aria-labelledby="modalLabel"
aria-hidden="true"
>
<div class="modal-dialog {dialogClass}">
<div class="modal-content">
<slot name="header" />
<slot name="body" />
<slot name="footer" />
</div>
</div>
</div>
<style lang="scss">
.modal {
z-index: 1066;
background-color: rgba($color: black, $alpha: 0.5);
}
.modal-content {
background-color: var(--canvas);
color: var(--fg);
}
</style>

View file

@ -3,11 +3,7 @@ Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import Modal from "bootstrap/js/dist/modal";
import { getContext, onDestroy, onMount } from "svelte";
import { modalsKey } from "$lib/components/context-keys";
import { registerModalClosingHandler } from "$lib/sveltelib/modal-closing";
import Modal from "$lib/components/Modal.svelte";
import { pageTheme } from "$lib/sveltelib/theme";
export let title: string;
@ -16,117 +12,63 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export let onOk: (text: string) => void;
$: value = initialValue;
export const modalKey: string = Math.random().toString(36).substring(2);
const modals = getContext<Map<string, Modal>>(modalsKey);
let modalRef: HTMLDivElement;
let modal: Modal;
let inputRef: HTMLInputElement;
let modal: Modal;
export let modalKey: string;
function onOkClicked(): void {
onOk(inputRef.value);
modal.hide();
value = initialValue;
}
function onCancelClicked(): void {
modal.hide();
value = initialValue;
}
function onShown(): void {
inputRef.focus();
setModalOpen(true);
}
function onHidden() {
setModalOpen(false);
}
const { set: setModalOpen, remove: removeModalClosingHandler } =
registerModalClosingHandler(onCancelClicked);
onMount(() => {
modalRef.addEventListener("shown.bs.modal", onShown);
modalRef.addEventListener("hidden.bs.modal", onHidden);
modal = new Modal(modalRef, { keyboard: false });
modals.set(modalKey, modal);
});
onDestroy(() => {
removeModalClosingHandler();
modalRef.removeEventListener("shown.bs.modal", onShown);
modalRef.removeEventListener("hidden.bs.modal", onHidden);
});
</script>
<div
bind:this={modalRef}
class="modal fade"
tabindex="-1"
aria-labelledby="modalLabel"
aria-hidden="true"
>
<div class="modal-dialog">
<div class="modal-content" class:default-colors={$pageTheme.isDark}>
<div class="modal-header">
<h5 class="modal-title" id="modalLabel">{title}</h5>
<button
type="button"
class="btn-close"
class:invert={$pageTheme.isDark}
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div class="modal-body">
<form on:submit|preventDefault={onOkClicked}>
<div class="mb-3">
<label for="prompt-input" class="col-form-label">
{prompt}:
</label>
<input
id="prompt-input"
bind:this={inputRef}
type="text"
class:nightMode={$pageTheme.isDark}
class="form-control"
bind:value
/>
</div>
</form>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-secondary"
on:click={onCancelClicked}
>
Cancel
</button>
<button type="button" class="btn btn-primary" on:click={onOkClicked}>
OK
</button>
</div>
</div>
<Modal bind:this={modal} bind:modalKey {onOkClicked} {onShown} {onCancelClicked}>
<div slot="header" class="modal-header">
<h5 class="modal-title" id="modalLabel">{title}</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
</div>
<div slot="body" class="modal-body">
<form on:submit|preventDefault={modal.acceptHandler}>
<div class="mb-3">
<label for="prompt-input" class="col-form-label">
{prompt}:
</label>
<input
id="prompt-input"
bind:this={inputRef}
type="text"
class:nightMode={$pageTheme.isDark}
class="form-control"
bind:value
/>
</div>
</form>
</div>
<div slot="footer" class="modal-footer">
<button type="button" class="btn btn-secondary" on:click={modal.cancelHandler}>
Cancel
</button>
<button type="button" class="btn btn-primary" on:click={onOkClicked}>OK</button>
</div>
</Modal>
<style lang="scss">
@use "$lib/sass/night-mode" as nightmode;
@use "../../lib/sass/night-mode" as nightmode;
.nightMode {
@include nightmode.input;
}
.default-colors {
background-color: var(--canvas);
color: var(--fg);
}
.invert {
filter: invert(1) grayscale(100%) brightness(200%);
}
</style>

View file

@ -3,35 +3,14 @@ Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="ts">
import Modal from "bootstrap/js/dist/modal";
import { getContext, onDestroy, onMount } from "svelte";
import * as tr from "@generated/ftl";
import { modalsKey } from "$lib/components/context-keys";
import { registerModalClosingHandler } from "$lib/sveltelib/modal-closing";
import Modal from "$lib/components/Modal.svelte";
import { pageTheme } from "$lib/sveltelib/theme";
import type { HistoryEntry } from "./types";
import { searchInBrowser } from "@generated/backend";
export const modalKey: string = Math.random().toString(36).substring(2);
export let history: HistoryEntry[] = [];
const modals = getContext<Map<string, Modal>>(modalsKey);
let modalRef: HTMLDivElement;
let modal: Modal;
function onCancelClicked(): void {
modal.hide();
}
function onShown(): void {
setModalOpen(true);
}
function onHidden() {
setModalOpen(false);
}
export let modal: Modal;
function onEntryClick(entry: HistoryEntry): void {
searchInBrowser({
@ -42,91 +21,41 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
});
modal.hide();
}
const { set: setModalOpen, remove: removeModalClosingHandler } =
registerModalClosingHandler(onCancelClicked);
onMount(() => {
modalRef.addEventListener("shown.bs.modal", onShown);
modalRef.addEventListener("hidden.bs.modal", onHidden);
modal = new Modal(modalRef, { keyboard: false });
modals.set(modalKey, modal);
});
onDestroy(() => {
removeModalClosingHandler();
modalRef.removeEventListener("shown.bs.modal", onShown);
modalRef.removeEventListener("hidden.bs.modal", onHidden);
});
</script>
<div
bind:this={modalRef}
class="modal fade"
class:nightMode={$pageTheme.isDark}
tabindex="-1"
aria-labelledby="modalLabel"
aria-hidden="true"
>
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="modalLabel">{tr.addingHistory()}</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
<div class="modal-body">
<ul class="history-list">
{#each history as entry}
<li>
<button
type="button"
class="history-entry"
on:click={() => onEntryClick(entry)}
>
{entry.text}
</button>
</li>
{/each}
</ul>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-secondary"
on:click={onCancelClicked}
>
Cancel
</button>
</div>
</div>
<Modal bind:this={modal}>
<div slot="header" class="modal-header">
<h5 class="modal-title" id="modalLabel">{tr.addingHistory()}</h5>
<button
type="button"
class="btn-close"
data-bs-dismiss="modal"
aria-label="Close"
></button>
</div>
</div>
<div slot="body" class="modal-body" class:nightMode={$pageTheme.isDark}>
<ul class="history-list">
{#each history as entry}
<li>
<button
type="button"
class="history-entry"
on:click={() => onEntryClick(entry)}
>
{entry.text}
</button>
</li>
{/each}
</ul>
</div>
<div slot="footer" class="modal-footer">
<button type="button" class="btn btn-secondary" on:click={modal.cancelHandler}>
Cancel
</button>
</div>
</Modal>
<style lang="scss">
.modal {
--link-color: #007bff;
--canvas-elevated-hover: rgba(0, 0, 0, 0.05);
}
.nightMode.modal {
--link-color: #4dabf7;
--canvas-elevated-hover: rgba(255, 255, 255, 0.1): ;
}
.nightMode .modal-content {
background-color: var(--canvas);
color: var(--fg);
}
.nightMode .btn-close {
filter: invert(1) grayscale(100%) brightness(200%);
}
.history-list {
list-style: none;
padding: 0;

View file

@ -15,9 +15,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import LabelName from "./LabelName.svelte";
import { EditorState, type EditorMode } from "./types";
import { ContextMenu, Item } from "$lib/context-menu";
import type Modal from "bootstrap/js/dist/modal";
import { getContext } from "svelte";
import { modalsKey } from "$lib/components/context-keys";
export interface NoteEditorAPI {
fields: EditorFieldAPI[];
@ -451,9 +448,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
await addCurrentNote(1n);
}
const modals = getContext<Map<string, Modal>>(modalsKey);
let modalKey: string;
let historyModal: Modal;
let history: HistoryEntry[] = [];
export async function addNoteToHistory(note: Note) {
@ -476,7 +471,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
}
export function onHistory() {
modals.get(modalKey)!.show();
historyModal.show();
}
export function saveOnPageHide() {
@ -724,6 +719,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import HistoryModal from "./HistoryModal.svelte";
import { HelpPageLinkRequest_HelpPage } from "@generated/anki/links_pb";
import { MessageBoxType } from "@generated/anki/frontend_pb";
import type Modal from "$lib/components/Modal.svelte";
$: isIOImageLoaded = false;
$: ioImageLoadedStore.set(isIOImageLoaded);
@ -1389,7 +1385,7 @@ components and functionality for general note editing.
{#if !isLegacy}
<ActionButtons {mode} {onClose} {onAdd} {onHistory} {history} />
<HistoryModal bind:modalKey {history} />
<HistoryModal bind:modal={historyModal} {history} />
{/if}
{#if !isLegacy}