mirror of
https://github.com/zadam/trilium.git
synced 2025-10-19 22:58:52 +02:00
Feature/presentation_poc (#7374)
This commit is contained in:
commit
2f49d315c1
@ -58,6 +58,7 @@
|
||||
"panzoom": "9.4.3",
|
||||
"preact": "10.27.2",
|
||||
"react-i18next": "16.0.1",
|
||||
"reveal.js": "5.2.1",
|
||||
"split.js": "1.6.5",
|
||||
"svg-pan-zoom": "3.6.2",
|
||||
"tabulator-tables": "6.3.1",
|
||||
@ -71,6 +72,7 @@
|
||||
"@types/leaflet": "1.9.21",
|
||||
"@types/leaflet-gpx": "1.3.8",
|
||||
"@types/mark.js": "8.11.12",
|
||||
"@types/reveal.js": "5.2.1",
|
||||
"@types/tabulator-tables": "6.2.11",
|
||||
"copy-webpack-plugin": "13.0.1",
|
||||
"happy-dom": "20.0.2",
|
||||
|
@ -438,4 +438,22 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
|
||||
}
|
||||
}
|
||||
|
||||
export function openInCurrentNoteContext(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent | React.PointerEvent<HTMLCanvasElement> | null, notePath: string, viewScope?: ViewScope) {
|
||||
const ntxId = $(evt?.target as Element)
|
||||
.closest("[data-ntx-id]")
|
||||
.attr("data-ntx-id");
|
||||
|
||||
const noteContext = ntxId ? appContext.tabManager.getNoteContextById(ntxId) : appContext.tabManager.getActiveContext();
|
||||
|
||||
if (noteContext) {
|
||||
noteContext.setNote(notePath, { viewScope }).then(() => {
|
||||
if (noteContext !== appContext.tabManager.getActiveContext()) {
|
||||
appContext.tabManager.activateNoteContext(noteContext.ntxId);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
appContext.tabManager.openContextWithNote(notePath, { viewScope, activate: true });
|
||||
}
|
||||
}
|
||||
|
||||
export default NoteContext;
|
||||
|
@ -29,7 +29,7 @@ interface Options {
|
||||
|
||||
const CODE_MIME_TYPES = new Set(["application/json"]);
|
||||
|
||||
async function getRenderedContent(this: {} | { ctx: string }, entity: FNote | FAttachment, options: Options = {}) {
|
||||
export async function getRenderedContent(this: {} | { ctx: string }, entity: FNote | FAttachment, options: Options = {}) {
|
||||
|
||||
options = Object.assign(
|
||||
{
|
||||
|
@ -27,7 +27,8 @@ export const byBookType: Record<ViewTypeOptions, string | null> = {
|
||||
calendar: "xWbu3jpNWapp",
|
||||
table: "2FvYrpmOXm29",
|
||||
geoMap: "81SGnPGMk7Xc",
|
||||
board: "CtBQqbwXDx1w"
|
||||
board: "CtBQqbwXDx1w",
|
||||
presentation: null
|
||||
};
|
||||
|
||||
export function getHelpUrlForNote(note: FNote | null | undefined) {
|
||||
|
@ -4,6 +4,7 @@ import appContext, { type NoteCommandData } from "../components/app_context.js";
|
||||
import froca from "./froca.js";
|
||||
import utils from "./utils.js";
|
||||
import { ALLOWED_PROTOCOLS } from "@triliumnext/commons";
|
||||
import { openInCurrentNoteContext } from "../components/note_context.js";
|
||||
|
||||
function getNotePathFromUrl(url: string) {
|
||||
const notePathMatch = /#(root[A-Za-z0-9_/]*)$/.exec(url);
|
||||
@ -316,21 +317,7 @@ function goToLinkExt(evt: MouseEvent | JQuery.ClickEvent | JQuery.MouseDownEvent
|
||||
viewScope
|
||||
});
|
||||
} else if (isLeftClick) {
|
||||
const ntxId = $(evt?.target as any)
|
||||
.closest("[data-ntx-id]")
|
||||
.attr("data-ntx-id");
|
||||
|
||||
const noteContext = ntxId ? appContext.tabManager.getNoteContextById(ntxId) : appContext.tabManager.getActiveContext();
|
||||
|
||||
if (noteContext) {
|
||||
noteContext.setNote(notePath, { viewScope }).then(() => {
|
||||
if (noteContext !== appContext.tabManager.getActiveContext()) {
|
||||
appContext.tabManager.activateNoteContext(noteContext.ntxId);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
appContext.tabManager.openContextWithNote(notePath, { viewScope, activate: true });
|
||||
}
|
||||
openInCurrentNoteContext(evt, notePath, viewScope);
|
||||
}
|
||||
} else if (hrefLink) {
|
||||
const withinEditLink = $link?.hasClass("ck-link-actions__preview");
|
||||
|
@ -168,7 +168,8 @@ async function getBuiltInTemplates(title: string | null, command: TreeCommandNam
|
||||
}
|
||||
|
||||
for (const templateNote of childNotes) {
|
||||
if (templateNote.hasLabel("collection") !== filterCollections) {
|
||||
if (templateNote.hasLabel("collection") !== filterCollections ||
|
||||
!templateNote.hasLabel("template")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -768,6 +768,7 @@
|
||||
"table": "Table",
|
||||
"geo-map": "Geo Map",
|
||||
"board": "Board",
|
||||
"presentation": "Presentation",
|
||||
"include_archived_notes": "Show archived notes"
|
||||
},
|
||||
"edited_notes": {
|
||||
@ -2029,6 +2030,11 @@
|
||||
"edit-note-title": "Click to edit note title",
|
||||
"edit-column-title": "Click to edit column title"
|
||||
},
|
||||
"presentation_view": {
|
||||
"edit-slide": "Edit this slide",
|
||||
"start-presentation": "Start presentation",
|
||||
"slide-overview": "Toggle an overview of the slides"
|
||||
},
|
||||
"command_palette": {
|
||||
"tree-action-name": "Tree: {{name}}",
|
||||
"export_note_title": "Export Note",
|
||||
|
@ -12,8 +12,9 @@ import BoardView from "./board";
|
||||
import { subscribeToMessages, unsubscribeToMessage as unsubscribeFromMessage } from "../../services/ws";
|
||||
import { WebSocketMessage } from "@triliumnext/commons";
|
||||
import froca from "../../services/froca";
|
||||
import PresentationView from "./presentation";
|
||||
|
||||
interface NoteListProps<T extends object> {
|
||||
interface NoteListProps {
|
||||
note: FNote | null | undefined;
|
||||
notePath: string | null | undefined;
|
||||
highlightedTokens?: string[] | null;
|
||||
@ -23,17 +24,17 @@ interface NoteListProps<T extends object> {
|
||||
ntxId: string | null | undefined;
|
||||
}
|
||||
|
||||
export default function NoteList<T extends object>(props: Pick<NoteListProps<T>, "displayOnlyCollections">) {
|
||||
export default function NoteList<T extends object>(props: Pick<NoteListProps, "displayOnlyCollections">) {
|
||||
const { note, noteContext, notePath, ntxId } = useNoteContext();
|
||||
const isEnabled = noteContext?.hasNoteList();
|
||||
return <CustomNoteList note={note} isEnabled={!!isEnabled} notePath={notePath} ntxId={ntxId} {...props} />
|
||||
}
|
||||
|
||||
export function SearchNoteList<T extends object>(props: Omit<NoteListProps<T>, "isEnabled">) {
|
||||
export function SearchNoteList<T extends object>(props: Omit<NoteListProps, "isEnabled">) {
|
||||
return <CustomNoteList {...props} isEnabled={true} />
|
||||
}
|
||||
|
||||
function CustomNoteList<T extends object>({ note, isEnabled: shouldEnable, notePath, highlightedTokens, displayOnlyCollections, ntxId }: NoteListProps<T>) {
|
||||
function CustomNoteList<T extends object>({ note, isEnabled: shouldEnable, notePath, highlightedTokens, displayOnlyCollections, ntxId }: NoteListProps) {
|
||||
const widgetRef = useRef<HTMLDivElement>(null);
|
||||
const viewType = useNoteViewType(note);
|
||||
const noteIds = useNoteIds(note, viewType, ntxId);
|
||||
@ -104,6 +105,8 @@ function getComponentByViewType(viewType: ViewTypeOptions, props: ViewModeProps<
|
||||
return <TableView {...props} />
|
||||
case "board":
|
||||
return <BoardView {...props} />
|
||||
case "presentation":
|
||||
return <PresentationView {...props} />
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import FNote from "../../entities/fnote";
|
||||
|
||||
export const allViewTypes = ["list", "grid", "calendar", "table", "geoMap", "board"] as const;
|
||||
export const allViewTypes = ["list", "grid", "calendar", "table", "geoMap", "board", "presentation"] as const;
|
||||
export type ViewTypeOptions = typeof allViewTypes[number];
|
||||
|
||||
export interface ViewModeProps<T extends object> {
|
||||
|
10
apps/client/src/widgets/collections/presentation/index.css
Normal file
10
apps/client/src/widgets/collections/presentation/index.css
Normal file
@ -0,0 +1,10 @@
|
||||
.presentation-button-bar {
|
||||
position: absolute;
|
||||
top: 1em;
|
||||
right: 1em;
|
||||
}
|
||||
|
||||
.presentation-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
183
apps/client/src/widgets/collections/presentation/index.tsx
Normal file
183
apps/client/src/widgets/collections/presentation/index.tsx
Normal file
@ -0,0 +1,183 @@
|
||||
import { ViewModeProps } from "../interface";
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from "preact/hooks";
|
||||
import Reveal from "reveal.js";
|
||||
import slideBaseStylesheet from "reveal.js/dist/reveal.css?raw";
|
||||
import slideCustomStylesheet from "./slidejs.css?raw";
|
||||
import { buildPresentationModel, PresentationModel, PresentationSlideBaseModel } from "./model";
|
||||
import ShadowDom from "../../react/ShadowDom";
|
||||
import ActionButton from "../../react/ActionButton";
|
||||
import "./index.css";
|
||||
import { RefObject } from "preact";
|
||||
import { openInCurrentNoteContext } from "../../../components/note_context";
|
||||
import { useNoteLabelWithDefault, useTriliumEvent } from "../../react/hooks";
|
||||
import { t } from "../../../services/i18n";
|
||||
import { DEFAULT_THEME, loadPresentationTheme } from "./themes";
|
||||
import FNote from "../../../entities/fnote";
|
||||
|
||||
export default function PresentationView({ note, noteIds }: ViewModeProps<{}>) {
|
||||
const [ presentation, setPresentation ] = useState<PresentationModel>();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [ api, setApi ] = useState<Reveal.Api>();
|
||||
const stylesheets = usePresentationStylesheets(note);
|
||||
|
||||
function refresh() {
|
||||
buildPresentationModel(note).then(setPresentation);
|
||||
}
|
||||
|
||||
useTriliumEvent("entitiesReloaded", ({ loadResults }) => {
|
||||
if (loadResults.getNoteIds().find(noteId => noteIds.includes(noteId))) {
|
||||
refresh();
|
||||
}
|
||||
});
|
||||
|
||||
useLayoutEffect(refresh, [ note, noteIds ]);
|
||||
|
||||
return presentation && stylesheets && (
|
||||
<>
|
||||
<ShadowDom
|
||||
className="presentation-container"
|
||||
containerRef={containerRef}
|
||||
>
|
||||
{stylesheets.map(stylesheet => <style>{stylesheet}</style>)}
|
||||
<Presentation presentation={presentation} setApi={setApi} />
|
||||
</ShadowDom>
|
||||
<ButtonOverlay containerRef={containerRef} api={api} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function usePresentationStylesheets(note: FNote) {
|
||||
const [ themeName ] = useNoteLabelWithDefault(note, "presentation:theme", DEFAULT_THEME);
|
||||
const [ stylesheets, setStylesheets ] = useState<string[]>();
|
||||
|
||||
useLayoutEffect(() => {
|
||||
loadPresentationTheme(themeName).then((themeStylesheet) => {
|
||||
setStylesheets([
|
||||
slideBaseStylesheet,
|
||||
themeStylesheet,
|
||||
slideCustomStylesheet
|
||||
].map(stylesheet => stylesheet.replace(/:root/g, ":host")));
|
||||
});
|
||||
}, [ themeName ]);
|
||||
|
||||
return stylesheets;
|
||||
}
|
||||
|
||||
function ButtonOverlay({ containerRef, api }: { containerRef: RefObject<HTMLDivElement>, api: Reveal.Api | undefined }) {
|
||||
const [ isOverviewActive, setIsOverviewActive ] = useState(false);
|
||||
useEffect(() => {
|
||||
if (!api) return;
|
||||
setIsOverviewActive(api.isOverview());
|
||||
const onEnabled = () => setIsOverviewActive(true);
|
||||
const onDisabled = () => setIsOverviewActive(false);
|
||||
api.on("overviewshown", onEnabled);
|
||||
api.on("overviewhidden", onDisabled);
|
||||
return () => {
|
||||
api.off("overviewshown", onEnabled);
|
||||
api.off("overviewhidden", onDisabled);
|
||||
};
|
||||
}, [ api ]);
|
||||
|
||||
return (
|
||||
<div className="presentation-button-bar">
|
||||
<div className="floating-buttons-children">
|
||||
<ActionButton
|
||||
className="floating-button"
|
||||
icon="bx bx-edit"
|
||||
text={t("presentation_view.edit-slide")}
|
||||
noIconActionClass
|
||||
onClick={e => {
|
||||
const currentSlide = api?.getCurrentSlide();
|
||||
const noteId = getNoteIdFromSlide(currentSlide);
|
||||
|
||||
if (noteId) {
|
||||
openInCurrentNoteContext(e, noteId);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
<ActionButton
|
||||
className="floating-button"
|
||||
icon="bx bx-grid-horizontal"
|
||||
text={t("presentation_view.slide-overview")}
|
||||
active={isOverviewActive}
|
||||
noIconActionClass
|
||||
onClick={() => api?.toggleOverview()}
|
||||
/>
|
||||
|
||||
<ActionButton
|
||||
className="floating-button"
|
||||
icon="bx bx-fullscreen"
|
||||
text={t("presentation_view.start-presentation")}
|
||||
noIconActionClass
|
||||
onClick={() => containerRef.current?.requestFullscreen()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Presentation({ presentation, setApi } : { presentation: PresentationModel, setApi: (api: Reveal.Api | undefined) => void }) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [revealApi, setRevealApi] = useState<Reveal.Api>();
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
const api = new Reveal(containerRef.current, {
|
||||
transition: "slide",
|
||||
embedded: true,
|
||||
keyboardCondition(event) {
|
||||
// Full-screen requests sometimes fail, we rely on the UI button instead.
|
||||
if (event.key === "f") {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
});
|
||||
api.initialize().then(() => {
|
||||
setRevealApi(revealApi);
|
||||
setApi(api);
|
||||
});
|
||||
|
||||
return () => {
|
||||
api.destroy();
|
||||
setRevealApi(undefined);
|
||||
setApi(undefined);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
revealApi?.sync();
|
||||
}, [ presentation, revealApi ]);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="reveal">
|
||||
<div className="slides">
|
||||
{presentation.slides?.map(slide => {
|
||||
if (!slide.verticalSlides) {
|
||||
return <Slide key={slide.noteId} slide={slide} />
|
||||
} else {
|
||||
return (
|
||||
<section>
|
||||
<Slide key={slide.noteId} slide={slide} />
|
||||
{slide.verticalSlides.map(slide => <Slide key={slide.noteId} slide={slide} /> )}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
function Slide({ slide }: { slide: PresentationSlideBaseModel }) {
|
||||
return <section data-note-id={slide.noteId} dangerouslySetInnerHTML={slide.content} />;
|
||||
}
|
||||
|
||||
function getNoteIdFromSlide(slide: HTMLElement | undefined) {
|
||||
if (!slide) return;
|
||||
return slide.dataset.noteId;
|
||||
}
|
49
apps/client/src/widgets/collections/presentation/model.ts
Normal file
49
apps/client/src/widgets/collections/presentation/model.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import FNote from "../../../entities/fnote";
|
||||
import contentRenderer from "../../../services/content_renderer";
|
||||
|
||||
type DangerouslySetInnerHTML = { __html: string; };
|
||||
|
||||
/** A top-level slide with optional vertical slides. */
|
||||
interface PresentationSlideModel extends PresentationSlideBaseModel {
|
||||
verticalSlides: PresentationSlideBaseModel[] | undefined;
|
||||
}
|
||||
|
||||
/** Either a top-level slide or a vertical slide. */
|
||||
export interface PresentationSlideBaseModel {
|
||||
noteId: string;
|
||||
content: DangerouslySetInnerHTML;
|
||||
}
|
||||
|
||||
export interface PresentationModel {
|
||||
slides: PresentationSlideModel[];
|
||||
}
|
||||
|
||||
export async function buildPresentationModel(note: FNote): Promise<PresentationModel> {
|
||||
const slideNotes = await note.getChildNotes();
|
||||
const slides: PresentationSlideModel[] = await Promise.all(slideNotes.map(async slideNote => ({
|
||||
noteId: slideNote.noteId,
|
||||
content: await processContent(slideNote),
|
||||
verticalSlides: await buildVerticalSlides(slideNote)
|
||||
})))
|
||||
|
||||
return { slides };
|
||||
}
|
||||
|
||||
async function buildVerticalSlides(parentSlideNote: FNote): Promise<undefined | PresentationSlideBaseModel[]> {
|
||||
const children = await parentSlideNote.getChildNotes();
|
||||
if (!children.length) return;
|
||||
|
||||
const slides: PresentationSlideBaseModel[] = await Promise.all(children.map(async childNote => ({
|
||||
noteId: childNote.noteId,
|
||||
content: await processContent(childNote)
|
||||
})));
|
||||
|
||||
return slides;
|
||||
}
|
||||
|
||||
async function processContent(note: FNote): Promise<DangerouslySetInnerHTML> {
|
||||
const { $renderedContent } = await contentRenderer.getRenderedContent(note, {
|
||||
|
||||
});
|
||||
return { __html: $renderedContent.html() };
|
||||
}
|
20
apps/client/src/widgets/collections/presentation/slidejs.css
Normal file
20
apps/client/src/widgets/collections/presentation/slidejs.css
Normal file
@ -0,0 +1,20 @@
|
||||
figure img {
|
||||
aspect-ratio: unset !important;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
span.katex-html {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
p:has(span.text-tiny),
|
||||
p:has(span.text-small),
|
||||
p:has(span.text-big),
|
||||
p:has(span.text-huge) {
|
||||
line-height: unset !important;
|
||||
}
|
||||
|
||||
span.text-tiny { font-size: 0.5em; }
|
||||
span.text-small { font-size: 0.75em; }
|
||||
span.text-big { font-size: 1.5em; }
|
||||
span.text-huge { font-size: 2em; }
|
@ -0,0 +1,10 @@
|
||||
import { it, describe } from "vitest";
|
||||
import { getPresentationThemes, loadPresentationTheme } from "./themes";
|
||||
|
||||
describe("Presentation themes", () => {
|
||||
it("can load all themes", async () => {
|
||||
const themes = getPresentationThemes();
|
||||
|
||||
await Promise.all(themes.map(theme => loadPresentationTheme(theme.id)));
|
||||
});
|
||||
});
|
58
apps/client/src/widgets/collections/presentation/themes.ts
Normal file
58
apps/client/src/widgets/collections/presentation/themes.ts
Normal file
@ -0,0 +1,58 @@
|
||||
export const DEFAULT_THEME = "white";
|
||||
|
||||
const themes = {
|
||||
black: {
|
||||
name: "Black",
|
||||
loadTheme: () => import("reveal.js/dist/theme/black.css?raw")
|
||||
},
|
||||
white: {
|
||||
name: "White",
|
||||
loadTheme: () => import("reveal.js/dist/theme/white.css?raw")
|
||||
},
|
||||
beige: {
|
||||
name: "Beige",
|
||||
loadTheme: () => import("reveal.js/dist/theme/beige.css?raw")
|
||||
},
|
||||
serif: {
|
||||
name: "Serif",
|
||||
loadTheme: () => import("reveal.js/dist/theme/serif.css?raw")
|
||||
},
|
||||
simple: {
|
||||
name: "Simple",
|
||||
loadTheme: () => import("reveal.js/dist/theme/simple.css?raw")
|
||||
},
|
||||
solarized: {
|
||||
name: "Solarized",
|
||||
loadTheme: () => import("reveal.js/dist/theme/solarized.css?raw")
|
||||
},
|
||||
moon: {
|
||||
name: "Moon",
|
||||
loadTheme: () => import("reveal.js/dist/theme/moon.css?raw")
|
||||
},
|
||||
dracula: {
|
||||
name: "Dracula",
|
||||
loadTheme: () => import("reveal.js/dist/theme/dracula.css?raw")
|
||||
},
|
||||
sky: {
|
||||
name: "Sky",
|
||||
loadTheme: () => import("reveal.js/dist/theme/sky.css?raw")
|
||||
},
|
||||
blood: {
|
||||
name: "Blood",
|
||||
loadTheme: () => import("reveal.js/dist/theme/blood.css?raw")
|
||||
}
|
||||
} as const;
|
||||
|
||||
export function getPresentationThemes() {
|
||||
return Object.entries(themes).map(([ id, theme ]) => ({
|
||||
id: id,
|
||||
name: theme.name
|
||||
}));
|
||||
}
|
||||
|
||||
export async function loadPresentationTheme(name: keyof typeof themes | string) {
|
||||
let theme = themes[name];
|
||||
if (!theme) theme = themes[DEFAULT_THEME];
|
||||
|
||||
return (await theme.loadTheme()).default;
|
||||
}
|
@ -5,16 +5,18 @@ import keyboard_actions from "../../services/keyboard_actions";
|
||||
|
||||
export interface ActionButtonProps {
|
||||
text: string;
|
||||
titlePosition?: "bottom" | "left";
|
||||
titlePosition?: "top" | "right" | "bottom" | "left";
|
||||
icon: string;
|
||||
className?: string;
|
||||
onClick?: (e: MouseEvent) => void;
|
||||
triggerCommand?: CommandNames;
|
||||
noIconActionClass?: boolean;
|
||||
frame?: boolean;
|
||||
active?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export default function ActionButton({ text, icon, className, onClick, triggerCommand, titlePosition, noIconActionClass, frame }: ActionButtonProps) {
|
||||
export default function ActionButton({ text, icon, className, onClick, triggerCommand, titlePosition, noIconActionClass, frame, active, disabled }: ActionButtonProps) {
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const [ keyboardShortcut, setKeyboardShortcut ] = useState<string[]>();
|
||||
|
||||
@ -32,8 +34,9 @@ export default function ActionButton({ text, icon, className, onClick, triggerCo
|
||||
|
||||
return <button
|
||||
ref={buttonRef}
|
||||
class={`${className ?? ""} ${!noIconActionClass ? "icon-action" : "btn"} ${icon} ${frame ? "btn btn-primary" : ""}`}
|
||||
class={`${className ?? ""} ${!noIconActionClass ? "icon-action" : "btn"} ${icon} ${frame ? "btn btn-primary" : ""} ${disabled ? "disabled" : ""} ${active ? "active" : ""}`}
|
||||
onClick={onClick}
|
||||
data-trigger-command={triggerCommand}
|
||||
disabled={disabled}
|
||||
/>;
|
||||
}
|
||||
|
28
apps/client/src/widgets/react/ShadowDom.tsx
Normal file
28
apps/client/src/widgets/react/ShadowDom.tsx
Normal file
@ -0,0 +1,28 @@
|
||||
import { ComponentChildren, HTMLAttributes, JSX, RefObject, render } from "preact";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { useSyncedRef } from "./hooks";
|
||||
|
||||
interface ShadowDomProps extends Omit<HTMLAttributes<HTMLDivElement>, "ref"> {
|
||||
children: ComponentChildren;
|
||||
containerRef?: RefObject<HTMLDivElement>;
|
||||
}
|
||||
|
||||
export default function ShadowDom({ children, containerRef: externalContainerRef, ...containerProps }: ShadowDomProps) {
|
||||
const containerRef = useSyncedRef<HTMLDivElement>(externalContainerRef, null);
|
||||
const [ shadowRoot, setShadowRoot ] = useState<ShadowRoot | null>(null);
|
||||
|
||||
// Create the shadow root.
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
const shadow = containerRef.current.attachShadow({ mode: "open" });
|
||||
setShadowRoot(shadow);
|
||||
}, []);
|
||||
|
||||
// Render the child elements.
|
||||
useEffect(() => {
|
||||
if (!shadowRoot) return;
|
||||
render(<>{children}</>, shadowRoot);
|
||||
}, [ shadowRoot, children ]);
|
||||
|
||||
return <div ref={containerRef} {...containerProps} />
|
||||
}
|
@ -19,7 +19,8 @@ const VIEW_TYPE_MAPPINGS: Record<ViewTypeOptions, string> = {
|
||||
calendar: t("book_properties.calendar"),
|
||||
table: t("book_properties.table"),
|
||||
geoMap: t("book_properties.geo-map"),
|
||||
board: t("book_properties.board")
|
||||
board: t("book_properties.board"),
|
||||
presentation: t("book_properties.presentation")
|
||||
};
|
||||
|
||||
export default function CollectionPropertiesTab({ note }: TabContext) {
|
||||
|
@ -5,6 +5,7 @@ import NoteContextAwareWidget from "../note_context_aware_widget";
|
||||
import { DEFAULT_MAP_LAYER_NAME, MAP_LAYERS, type MapLayer } from "../collections/geomap/map_layer";
|
||||
import { ViewTypeOptions } from "../collections/interface";
|
||||
import { FilterLabelsByType } from "@triliumnext/commons";
|
||||
import { DEFAULT_THEME, getPresentationThemes } from "../collections/presentation/themes";
|
||||
|
||||
interface BookConfig {
|
||||
properties: BookProperty[];
|
||||
@ -159,6 +160,20 @@ export const bookPropertiesConfig: Record<ViewTypeOptions, BookConfig> = {
|
||||
},
|
||||
board: {
|
||||
properties: []
|
||||
},
|
||||
presentation: {
|
||||
properties: [
|
||||
{
|
||||
label: "Theme",
|
||||
type: "combobox",
|
||||
bindToLabel: "presentation:theme",
|
||||
defaultValue: DEFAULT_THEME,
|
||||
options: getPresentationThemes().map(theme => ({
|
||||
value: theme.id,
|
||||
label: theme.name
|
||||
}))
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -423,7 +423,11 @@
|
||||
"board_note_third": "Third note",
|
||||
"board_status_todo": "To Do",
|
||||
"board_status_progress": "In Progress",
|
||||
"board_status_done": "Done"
|
||||
"board_status_done": "Done",
|
||||
"presentation": "Presentation",
|
||||
"presentation_slide": "Presentation slide",
|
||||
"presentation_slide_first": "First slide",
|
||||
"presentation_slide_second": "Second slide"
|
||||
},
|
||||
"sql_init": {
|
||||
"db_not_initialized_desktop": "DB not initialized, please follow on-screen instructions.",
|
||||
|
@ -345,24 +345,31 @@ function checkHiddenSubtreeRecursively(parentNoteId: string, item: HiddenSubtree
|
||||
let branch;
|
||||
|
||||
if (!note) {
|
||||
// Missing item, add it.
|
||||
({ note, branch } = noteService.createNewNote({
|
||||
noteId: item.id,
|
||||
title: item.title,
|
||||
type: item.type,
|
||||
parentNoteId: parentNoteId,
|
||||
content: "",
|
||||
content: item.content ?? "",
|
||||
ignoreForbiddenParents: true
|
||||
}));
|
||||
} else {
|
||||
// Existing item, check if it's in the right state.
|
||||
branch = note.getParentBranches().find((branch) => branch.parentNoteId === parentNoteId);
|
||||
|
||||
if (item.content && note.getContent() !== item.content) {
|
||||
log.info(`Updating content of ${item.id}.`);
|
||||
note.setContent(item.content);
|
||||
}
|
||||
|
||||
// Clean up any branches that shouldn't exist according to the meta definition
|
||||
// For hidden subtree notes, we want to ensure they only exist in their designated locations
|
||||
if (item.enforceBranches || item.id.startsWith("_help")) {
|
||||
// If the note exists but doesn't have a branch in the expected parent,
|
||||
// create the missing branch to ensure it's in the correct location
|
||||
if (!branch) {
|
||||
console.log("Creating missing branch for note", item.id, "under parent", parentNoteId);
|
||||
log.info(`Creating missing branch for note ${item.id} under parent ${parentNoteId}.`);
|
||||
branch = new BBranch({
|
||||
noteId: item.id,
|
||||
parentNoteId: parentNoteId,
|
||||
@ -466,7 +473,7 @@ function checkHiddenSubtreeRecursively(parentNoteId: string, item: HiddenSubtree
|
||||
}).save();
|
||||
} else if (attr.name === "docName" || (existingAttribute.noteId.startsWith("_help") && attr.name === "iconClass")) {
|
||||
if (existingAttribute.value !== attr.value) {
|
||||
console.log(`Updating attribute ${attrId} from "${existingAttribute.value}" to "${attr.value}"`);
|
||||
log.info(`Updating attribute ${attrId} from "${existingAttribute.value}" to "${attr.value}"`);
|
||||
existingAttribute.value = attr.value ?? "";
|
||||
existingAttribute.save();
|
||||
}
|
||||
|
@ -234,6 +234,72 @@ export default function buildHiddenSubtreeTemplates() {
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "_template_presentation_slide",
|
||||
type: "text",
|
||||
title: t("hidden_subtree_templates.presentation_slide"),
|
||||
icon: "bx bx-rectangle",
|
||||
attributes: [
|
||||
{
|
||||
name: "slide",
|
||||
type: "label"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "_template_presentation",
|
||||
type: "book",
|
||||
title: t("hidden_subtree_templates.presentation"),
|
||||
icon: "bx bx-slideshow",
|
||||
attributes: [
|
||||
{
|
||||
name: "template",
|
||||
type: "label"
|
||||
},
|
||||
{
|
||||
name: "viewType",
|
||||
type: "label",
|
||||
value: "presentation"
|
||||
},
|
||||
{
|
||||
name: "collection",
|
||||
type: "label"
|
||||
},
|
||||
{
|
||||
name: "child:template",
|
||||
type: "relation",
|
||||
value: "_template_presentation_slide"
|
||||
}
|
||||
],
|
||||
children: [
|
||||
{
|
||||
id: "_template_presentation_first",
|
||||
type: "text",
|
||||
title: t("hidden_subtree_templates.presentation_slide_first"),
|
||||
content: t("hidden_subtree_templates.presentation_slide_first"),
|
||||
attributes: [
|
||||
{
|
||||
name: "template",
|
||||
type: "relation",
|
||||
value: "_template_presentation_slide"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: "_template_presentation_second",
|
||||
type: "text",
|
||||
title: t("hidden_subtree_templates.presentation_slide_second"),
|
||||
content: t("hidden_subtree_templates.presentation_slide_second"),
|
||||
attributes: [
|
||||
{
|
||||
name: "template",
|
||||
type: "relation",
|
||||
value: "_template_presentation_slide"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
|
@ -12,7 +12,7 @@ describe("Migration", () => {
|
||||
|
||||
const migration = (await import("./migration.js")).default;
|
||||
await migration.migrateIfNecessary();
|
||||
expect(sql.getValue("SELECT count(*) FROM blobs")).toBe(116);
|
||||
expect(sql.getValue("SELECT count(*) FROM blobs")).toBe(118);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
@ -39,6 +39,7 @@ type Labels = {
|
||||
"board:groupBy": string;
|
||||
maxNestingDepth: number;
|
||||
includeArchived: boolean;
|
||||
"presentation:theme": string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -54,4 +54,10 @@ export interface HiddenSubtreeItem {
|
||||
* definitions will be removed.
|
||||
*/
|
||||
enforceAttributes?: boolean;
|
||||
/**
|
||||
* Optionally, a content to be set in the hidden note. If undefined, an empty string will be set instead.
|
||||
*
|
||||
* The value is also checked at every startup to ensure that it's kept up to date according to the definition.
|
||||
*/
|
||||
content?: string;
|
||||
}
|
||||
|
17
pnpm-lock.yaml
generated
17
pnpm-lock.yaml
generated
@ -253,6 +253,9 @@ importers:
|
||||
react-i18next:
|
||||
specifier: 16.0.1
|
||||
version: 16.0.1(i18next@25.6.0(typescript@5.9.3))(react-dom@19.1.0(react@16.14.0))(react@16.14.0)(typescript@5.9.3)
|
||||
reveal.js:
|
||||
specifier: 5.2.1
|
||||
version: 5.2.1
|
||||
split.js:
|
||||
specifier: 1.6.5
|
||||
version: 1.6.5
|
||||
@ -287,6 +290,9 @@ importers:
|
||||
'@types/mark.js':
|
||||
specifier: 8.11.12
|
||||
version: 8.11.12
|
||||
'@types/reveal.js':
|
||||
specifier: 5.2.1
|
||||
version: 5.2.1
|
||||
'@types/tabulator-tables':
|
||||
specifier: 6.2.11
|
||||
version: 6.2.11
|
||||
@ -5162,6 +5168,9 @@ packages:
|
||||
'@types/retry@0.12.2':
|
||||
resolution: {integrity: sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==}
|
||||
|
||||
'@types/reveal.js@5.2.1':
|
||||
resolution: {integrity: sha512-egr+amW5iilXo94kEGyJv24bJozsu/XAOHnhMHLnaJkHVxoui2gsWqzByaltA5zfXDTH2F4WyWnAkhHRcpytIQ==}
|
||||
|
||||
'@types/safe-compare@1.1.2':
|
||||
resolution: {integrity: sha512-kK/IM1+pvwCMom+Kezt/UlP8LMEwm8rP6UgGbRc6zUnhU/csoBQ5rWgmD2CJuHxiMiX+H1VqPGpo0kDluJGXYA==}
|
||||
|
||||
@ -11964,6 +11973,10 @@ packages:
|
||||
resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
|
||||
engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
|
||||
|
||||
reveal.js@5.2.1:
|
||||
resolution: {integrity: sha512-r7//6mIM5p34hFiDMvYfXgyjXqGRta+/psd9YtytsgRlrpRzFv4RbH76TXd2qD+7ZPZEbpBDhdRhJaFgfQ7zNQ==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
rfdc@1.4.1:
|
||||
resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==}
|
||||
|
||||
@ -19715,6 +19728,8 @@ snapshots:
|
||||
|
||||
'@types/retry@0.12.2': {}
|
||||
|
||||
'@types/reveal.js@5.2.1': {}
|
||||
|
||||
'@types/safe-compare@1.1.2': {}
|
||||
|
||||
'@types/sanitize-html@2.16.0':
|
||||
@ -28091,6 +28106,8 @@ snapshots:
|
||||
|
||||
reusify@1.1.0: {}
|
||||
|
||||
reveal.js@5.2.1: {}
|
||||
|
||||
rfdc@1.4.1: {}
|
||||
|
||||
rgb2hex@0.2.5: {}
|
||||
|
Loading…
x
Reference in New Issue
Block a user