mirror of
https://github.com/zadam/trilium.git
synced 2026-01-07 15:14:24 +01:00
New layout: Right panel (sidebar) (#8095)
This commit is contained in:
commit
78ac59581e
@ -1,40 +1,41 @@
|
||||
import froca from "../services/froca.js";
|
||||
import RootCommandExecutor from "./root_command_executor.js";
|
||||
import Entrypoints from "./entrypoints.js";
|
||||
import options from "../services/options.js";
|
||||
import utils, { hasTouchBar } from "../services/utils.js";
|
||||
import zoomComponent from "./zoom.js";
|
||||
import TabManager from "./tab_manager.js";
|
||||
import Component from "./component.js";
|
||||
import keyboardActionsService from "../services/keyboard_actions.js";
|
||||
import linkService, { type ViewScope } from "../services/link.js";
|
||||
import MobileScreenSwitcherExecutor, { type Screen } from "./mobile_screen_switcher.js";
|
||||
import MainTreeExecutors from "./main_tree_executors.js";
|
||||
import toast from "../services/toast.js";
|
||||
import ShortcutComponent from "./shortcut_component.js";
|
||||
import { t, initLocale } from "../services/i18n.js";
|
||||
import type { ResolveOptions } from "../widgets/dialogs/delete_notes.js";
|
||||
import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
|
||||
import type { ConfirmWithMessageOptions, ConfirmWithTitleOptions } from "../widgets/dialogs/confirm.js";
|
||||
import type LoadResults from "../services/load_results.js";
|
||||
import type { Attribute } from "../services/attribute_parser.js";
|
||||
import type NoteTreeWidget from "../widgets/note_tree.js";
|
||||
import type { default as NoteContext, GetTextEditorCallback } from "./note_context.js";
|
||||
import type { NativeImage, TouchBar } from "electron";
|
||||
import TouchBarComponent from "./touch_bar.js";
|
||||
import type { CKTextEditor } from "@triliumnext/ckeditor5";
|
||||
import type CodeMirror from "@triliumnext/codemirror";
|
||||
import { StartupChecks } from "./startup_checks.js";
|
||||
import type { CreateNoteOpts } from "../services/note_create.js";
|
||||
import { ColumnComponent } from "tabulator-tables";
|
||||
import { ChooseNoteTypeCallback } from "../widgets/dialogs/note_type_chooser.jsx";
|
||||
import type RootContainer from "../widgets/containers/root_container.js";
|
||||
import { SqlExecuteResults } from "@triliumnext/commons";
|
||||
import { AddLinkOpts } from "../widgets/dialogs/add_link.jsx";
|
||||
import { IncludeNoteOpts } from "../widgets/dialogs/include_note.jsx";
|
||||
import type { NativeImage, TouchBar } from "electron";
|
||||
import { ColumnComponent } from "tabulator-tables";
|
||||
|
||||
import type { Attribute } from "../services/attribute_parser.js";
|
||||
import froca from "../services/froca.js";
|
||||
import { initLocale,t } from "../services/i18n.js";
|
||||
import keyboardActionsService from "../services/keyboard_actions.js";
|
||||
import linkService, { type ViewScope } from "../services/link.js";
|
||||
import type LoadResults from "../services/load_results.js";
|
||||
import type { CreateNoteOpts } from "../services/note_create.js";
|
||||
import options from "../services/options.js";
|
||||
import toast from "../services/toast.js";
|
||||
import utils, { hasTouchBar } from "../services/utils.js";
|
||||
import { ReactWrappedWidget } from "../widgets/basic_widget.js";
|
||||
import type { MarkdownImportOpts } from "../widgets/dialogs/markdown_import.jsx";
|
||||
import type RootContainer from "../widgets/containers/root_container.js";
|
||||
import { AddLinkOpts } from "../widgets/dialogs/add_link.jsx";
|
||||
import type { ConfirmWithMessageOptions, ConfirmWithTitleOptions } from "../widgets/dialogs/confirm.js";
|
||||
import type { ResolveOptions } from "../widgets/dialogs/delete_notes.js";
|
||||
import { IncludeNoteOpts } from "../widgets/dialogs/include_note.jsx";
|
||||
import type { InfoProps } from "../widgets/dialogs/info.jsx";
|
||||
import type { MarkdownImportOpts } from "../widgets/dialogs/markdown_import.jsx";
|
||||
import { ChooseNoteTypeCallback } from "../widgets/dialogs/note_type_chooser.jsx";
|
||||
import type { PromptDialogOptions } from "../widgets/dialogs/prompt.js";
|
||||
import type NoteTreeWidget from "../widgets/note_tree.js";
|
||||
import Component from "./component.js";
|
||||
import Entrypoints from "./entrypoints.js";
|
||||
import MainTreeExecutors from "./main_tree_executors.js";
|
||||
import MobileScreenSwitcherExecutor, { type Screen } from "./mobile_screen_switcher.js";
|
||||
import type { default as NoteContext, GetTextEditorCallback } from "./note_context.js";
|
||||
import RootCommandExecutor from "./root_command_executor.js";
|
||||
import ShortcutComponent from "./shortcut_component.js";
|
||||
import { StartupChecks } from "./startup_checks.js";
|
||||
import TabManager from "./tab_manager.js";
|
||||
import TouchBarComponent from "./touch_bar.js";
|
||||
import zoomComponent from "./zoom.js";
|
||||
|
||||
interface Layout {
|
||||
getRootWidget: (appContext: AppContext) => RootContainer;
|
||||
@ -447,6 +448,7 @@ type EventMappings = {
|
||||
};
|
||||
searchRefreshed: { ntxId?: string | null };
|
||||
textEditorRefreshed: { ntxId?: string | null, editor: CKTextEditor };
|
||||
contentElRefreshed: { ntxId?: string | null, contentEl: HTMLElement };
|
||||
hoistedNoteChanged: {
|
||||
noteId: string;
|
||||
ntxId: string | null;
|
||||
@ -695,10 +697,8 @@ $(window).on("beforeunload", () => {
|
||||
console.log(`Component ${component.componentId} is not finished saving its state.`);
|
||||
allSaved = false;
|
||||
}
|
||||
} else {
|
||||
if (!listener()) {
|
||||
allSaved = false;
|
||||
}
|
||||
} else if (!listener()) {
|
||||
allSaved = false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -708,7 +708,7 @@ $(window).on("beforeunload", () => {
|
||||
}
|
||||
});
|
||||
|
||||
$(window).on("hashchange", function () {
|
||||
$(window).on("hashchange", () => {
|
||||
const { notePath, ntxId, viewScope, searchString } = linkService.parseNavigationStateFromUrl(window.location.href);
|
||||
|
||||
if (notePath || ntxId) {
|
||||
|
||||
@ -390,7 +390,7 @@ class NoteContext extends Component implements EventListener<"entitiesReloaded">
|
||||
* If no content could be determined `null` is returned instead.
|
||||
*/
|
||||
async getContentElement() {
|
||||
return this.timeout<JQuery<HTMLElement>>(
|
||||
return this.timeout<JQuery<HTMLElement> | null>(
|
||||
new Promise((resolve) =>
|
||||
appContext.triggerCommand("executeWithContentElement", {
|
||||
resolve,
|
||||
|
||||
@ -9,6 +9,7 @@ import CreatePaneButton from "../widgets/buttons/create_pane_button.js";
|
||||
import GlobalMenu from "../widgets/buttons/global_menu.jsx";
|
||||
import LeftPaneToggle from "../widgets/buttons/left_pane_toggle.js";
|
||||
import MovePaneButton from "../widgets/buttons/move_pane_button.js";
|
||||
import RightPaneToggle from "../widgets/buttons/right_pane_toggle.jsx";
|
||||
import CloseZenModeButton from "../widgets/close_zen_button.jsx";
|
||||
import NoteList from "../widgets/collections/NoteList.jsx";
|
||||
import ContentHeader from "../widgets/containers/content_header.js";
|
||||
@ -44,6 +45,7 @@ import Ribbon from "../widgets/ribbon/Ribbon.jsx";
|
||||
import ScrollPadding from "../widgets/scroll_padding.js";
|
||||
import SearchResult from "../widgets/search_result.jsx";
|
||||
import SharedInfo from "../widgets/shared_info.jsx";
|
||||
import RightPanelContainer from "../widgets/sidebar/RightPanelContainer.jsx";
|
||||
import SqlResults from "../widgets/sql_result.js";
|
||||
import SqlTableSchemas from "../widgets/sql_table_schemas.js";
|
||||
import TabRowWidget from "../widgets/tab_row.js";
|
||||
@ -90,6 +92,7 @@ export default class DesktopLayout {
|
||||
.optChild(launcherPaneIsHorizontal, <LeftPaneToggle isHorizontalLayout={true} />)
|
||||
.child(<TabHistoryNavigationButtons />)
|
||||
.child(new TabRowWidget().class("full-width"))
|
||||
.optChild(launcherPaneIsHorizontal && isNewLayout, <RightPaneToggle />)
|
||||
.optChild(customTitleBarButtons, <TitleBarButtons />)
|
||||
.css("height", "40px")
|
||||
.css("background-color", "var(--launcher-pane-background-color)")
|
||||
@ -113,10 +116,14 @@ export default class DesktopLayout {
|
||||
.css("flex-grow", "1")
|
||||
.optChild(!fullWidthTabBar,
|
||||
new FlexContainer("row")
|
||||
.class("tab-row-container")
|
||||
.child(<TabHistoryNavigationButtons />)
|
||||
.child(new TabRowWidget())
|
||||
.optChild(isNewLayout, <RightPaneToggle />)
|
||||
.optChild(customTitleBarButtons, <TitleBarButtons />)
|
||||
.css("height", "40px"))
|
||||
.css("height", "40px")
|
||||
.css("align-items", "center")
|
||||
)
|
||||
.optChild(isNewLayout, <FixedFormattingToolbar />)
|
||||
.child(
|
||||
new FlexContainer("row")
|
||||
@ -145,7 +152,7 @@ export default class DesktopLayout {
|
||||
.optChild(isNewLayout, <NoteActions />))
|
||||
.optChild(!isNewLayout, <Ribbon />)
|
||||
.child(new WatchedFileUpdateStatusWidget())
|
||||
.child(<FloatingButtons items={DESKTOP_FLOATING_BUTTONS} />)
|
||||
.optChild(!isNewLayout, <FloatingButtons items={DESKTOP_FLOATING_BUTTONS} />)
|
||||
.child(
|
||||
new ScrollingContainer()
|
||||
.filling()
|
||||
@ -165,21 +172,19 @@ export default class DesktopLayout {
|
||||
)
|
||||
.child(<ApiLog />)
|
||||
.child(new FindWidget())
|
||||
.child(
|
||||
...this.customWidgets.get("node-detail-pane"), // typo, let's keep it for a while as BC
|
||||
...this.customWidgets.get("note-detail-pane")
|
||||
)
|
||||
.child(...this.customWidgets.get("note-detail-pane"))
|
||||
)
|
||||
)
|
||||
.child(...this.customWidgets.get("center-pane"))
|
||||
|
||||
)
|
||||
.child(
|
||||
.optChild(!isNewLayout,
|
||||
new RightPaneContainer()
|
||||
.child(new TocWidget())
|
||||
.child(new HighlightsListWidget())
|
||||
.child(...this.customWidgets.get("right-pane"))
|
||||
)
|
||||
.optChild(isNewLayout, <RightPanelContainer customWidgets={this.customWidgets.get("right-pane")} />)
|
||||
)
|
||||
.optChild(!launcherPaneIsHorizontal && isNewLayout, <StatusBar />)
|
||||
)
|
||||
|
||||
@ -1961,7 +1961,7 @@ body.electron.platform-darwin:not(.native-titlebar):not(.full-screen) #tab-row-l
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.tab-row-widget {
|
||||
.tab-row-container {
|
||||
padding-inline-end: calc(100vw - env(titlebar-area-width, 100vw));
|
||||
}
|
||||
|
||||
|
||||
@ -1723,7 +1723,12 @@
|
||||
},
|
||||
"highlights_list_2": {
|
||||
"title": "Highlights List",
|
||||
"options": "Options"
|
||||
"title_with_count_one": "{{count}} highlight",
|
||||
"title_with_count_other": "{{count}} highlights",
|
||||
"options": "Options",
|
||||
"modal_title": "Configure Highlights List",
|
||||
"menu_configure": "Configure highlights list...",
|
||||
"no_highlights": "No highlights found."
|
||||
},
|
||||
"quick-search": {
|
||||
"placeholder": "Quick search",
|
||||
@ -1794,7 +1799,8 @@
|
||||
},
|
||||
"toc": {
|
||||
"table_of_contents": "Table of Contents",
|
||||
"options": "Options"
|
||||
"options": "Options",
|
||||
"no_headings": "No headings."
|
||||
},
|
||||
"watched_file_update_status": {
|
||||
"file_last_modified": "File <code class=\"file-path\"></code> has been last modified on <span class=\"file-last-modified\"></span>.",
|
||||
@ -2196,5 +2202,11 @@
|
||||
"note_paths_other": "{{count}} paths",
|
||||
"note_paths_title": "Note paths",
|
||||
"code_note_switcher": "Change language mode"
|
||||
},
|
||||
"right_pane": {
|
||||
"empty_message": "Nothing to show for this note",
|
||||
"empty_button": "Hide the panel",
|
||||
"toggle": "Toggle right panel",
|
||||
"custom_widget_go_to_source": "Go to source code"
|
||||
}
|
||||
}
|
||||
|
||||
@ -78,10 +78,8 @@ export const POPUP_HIDDEN_FLOATING_BUTTONS: FloatingButtonsList = [
|
||||
ToggleReadOnlyButton
|
||||
];
|
||||
|
||||
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
|
||||
|
||||
function RefreshBackendLogButton({ note, parentComponent, noteContext, isDefaultViewMode }: FloatingButtonContext) {
|
||||
const isEnabled = !isNewLayout && (note.noteId === "_backendLog" || note.type === "render") && isDefaultViewMode;
|
||||
const isEnabled = (note.noteId === "_backendLog" || note.type === "render") && isDefaultViewMode;
|
||||
return isEnabled && <FloatingButton
|
||||
text={t("backend_log.refresh")}
|
||||
icon="bx bx-refresh"
|
||||
@ -90,7 +88,7 @@ function RefreshBackendLogButton({ note, parentComponent, noteContext, isDefault
|
||||
}
|
||||
|
||||
function SwitchSplitOrientationButton({ note, isReadOnly, isDefaultViewMode }: FloatingButtonContext) {
|
||||
const isEnabled = !isNewLayout && note.type === "mermaid" && note.isContentAvailable() && !isReadOnly && isDefaultViewMode;
|
||||
const isEnabled = note.type === "mermaid" && note.isContentAvailable() && !isReadOnly && isDefaultViewMode;
|
||||
const [ splitEditorOrientation, setSplitEditorOrientation ] = useTriliumOption("splitEditorOrientation");
|
||||
const upcomingOrientation = splitEditorOrientation === "horizontal" ? "vertical" : "horizontal";
|
||||
|
||||
@ -103,7 +101,7 @@ function SwitchSplitOrientationButton({ note, isReadOnly, isDefaultViewMode }: F
|
||||
|
||||
function ToggleReadOnlyButton({ note, viewType, isDefaultViewMode }: FloatingButtonContext) {
|
||||
const [ isReadOnly, setReadOnly ] = useNoteLabelBoolean(note, "readOnly");
|
||||
const isEnabled = !isNewLayout && ([ "mermaid", "mindMap", "canvas" ].includes(note.type) || viewType === "geoMap")
|
||||
const isEnabled = ([ "mermaid", "mindMap", "canvas" ].includes(note.type) || viewType === "geoMap")
|
||||
&& note.isContentAvailable() && isDefaultViewMode;
|
||||
|
||||
return isEnabled && <FloatingButton
|
||||
@ -173,7 +171,7 @@ function ShowHighlightsListWidgetButton({ note, noteContext, isDefaultViewMode }
|
||||
}
|
||||
|
||||
function RunActiveNoteButton({ note }: FloatingButtonContext) {
|
||||
const isEnabled = !isNewLayout && (note.mime.startsWith("application/javascript") || note.mime === "text/x-sqlite;schema=trilium");
|
||||
const isEnabled = (note.mime.startsWith("application/javascript") || note.mime === "text/x-sqlite;schema=trilium");
|
||||
return isEnabled && <FloatingButton
|
||||
icon="bx bx-play"
|
||||
text={t("code_buttons.execute_button_title")}
|
||||
@ -182,7 +180,7 @@ function RunActiveNoteButton({ note }: FloatingButtonContext) {
|
||||
}
|
||||
|
||||
function OpenTriliumApiDocsButton({ note }: FloatingButtonContext) {
|
||||
const isEnabled = !isNewLayout && note.mime.startsWith("application/javascript;env=");
|
||||
const isEnabled = note.mime.startsWith("application/javascript;env=");
|
||||
return isEnabled && <FloatingButton
|
||||
icon="bx bx-help-circle"
|
||||
text={t("code_buttons.trilium_api_docs_button_title")}
|
||||
@ -191,7 +189,7 @@ function OpenTriliumApiDocsButton({ note }: FloatingButtonContext) {
|
||||
}
|
||||
|
||||
function SaveToNoteButton({ note }: FloatingButtonContext) {
|
||||
const isEnabled = !isNewLayout && note.mime === "text/x-sqlite;schema=trilium" && note.isHiddenCompletely();
|
||||
const isEnabled = note.mime === "text/x-sqlite;schema=trilium" && note.isHiddenCompletely();
|
||||
return isEnabled && <FloatingButton
|
||||
icon="bx bx-save"
|
||||
text={t("code_buttons.save_to_note_button_title")}
|
||||
@ -213,7 +211,7 @@ export function buildSaveSqlToNoteHandler(note: FNote) {
|
||||
}
|
||||
|
||||
function RelationMapButtons({ note, isDefaultViewMode, triggerEvent }: FloatingButtonContext) {
|
||||
const isEnabled = (!isNewLayout && note.type === "relationMap" && isDefaultViewMode);
|
||||
const isEnabled = (note.type === "relationMap" && isDefaultViewMode);
|
||||
return isEnabled && (
|
||||
<>
|
||||
<FloatingButton
|
||||
@ -246,7 +244,7 @@ function RelationMapButtons({ note, isDefaultViewMode, triggerEvent }: FloatingB
|
||||
}
|
||||
|
||||
function GeoMapButtons({ triggerEvent, viewType, isReadOnly }: FloatingButtonContext) {
|
||||
const isEnabled = !isNewLayout && viewType === "geoMap" && !isReadOnly;
|
||||
const isEnabled = viewType === "geoMap" && !isReadOnly;
|
||||
return isEnabled && (
|
||||
<FloatingButton
|
||||
icon="bx bx-plus-circle"
|
||||
@ -259,8 +257,7 @@ function GeoMapButtons({ triggerEvent, viewType, isReadOnly }: FloatingButtonCon
|
||||
function CopyImageReferenceButton({ note, isDefaultViewMode }: FloatingButtonContext) {
|
||||
const hiddenImageCopyRef = useRef<HTMLDivElement>(null);
|
||||
const isEnabled = (
|
||||
!isNewLayout
|
||||
&& ["mermaid", "canvas", "mindMap", "image"].includes(note?.type ?? "")
|
||||
["mermaid", "canvas", "mindMap", "image"].includes(note?.type ?? "")
|
||||
&& note?.isContentAvailable() && isDefaultViewMode
|
||||
);
|
||||
|
||||
@ -287,7 +284,7 @@ function CopyImageReferenceButton({ note, isDefaultViewMode }: FloatingButtonCon
|
||||
}
|
||||
|
||||
function ExportImageButtons({ note, triggerEvent, isDefaultViewMode }: FloatingButtonContext) {
|
||||
const isEnabled = !isNewLayout && ["mermaid", "mindMap"].includes(note?.type ?? "")
|
||||
const isEnabled = ["mermaid", "mindMap"].includes(note?.type ?? "")
|
||||
&& note?.isContentAvailable() && isDefaultViewMode;
|
||||
return isEnabled && (
|
||||
<>
|
||||
@ -308,7 +305,7 @@ function ExportImageButtons({ note, triggerEvent, isDefaultViewMode }: FloatingB
|
||||
|
||||
function InAppHelpButton({ note }: FloatingButtonContext) {
|
||||
const helpUrl = getHelpUrlForNote(note);
|
||||
const isEnabled = !!helpUrl && !isNewLayout;
|
||||
const isEnabled = !!helpUrl;
|
||||
|
||||
return isEnabled && (
|
||||
<FloatingButton
|
||||
@ -335,7 +332,7 @@ function Backlinks({ note, isDefaultViewMode }: FloatingButtonContext) {
|
||||
}
|
||||
}, [ popupOpen, windowHeight ]);
|
||||
|
||||
const isEnabled = !isNewLayout && isDefaultViewMode && backlinkCount > 0;
|
||||
const isEnabled = isDefaultViewMode && backlinkCount > 0;
|
||||
return (isEnabled &&
|
||||
<div className="backlinks-widget has-overflow">
|
||||
<div
|
||||
|
||||
21
apps/client/src/widgets/buttons/right_pane_toggle.tsx
Normal file
21
apps/client/src/widgets/buttons/right_pane_toggle.tsx
Normal file
@ -0,0 +1,21 @@
|
||||
import clsx from "clsx";
|
||||
|
||||
import { t } from "../../services/i18n";
|
||||
import ActionButton from "../react/ActionButton";
|
||||
import { useTriliumOptionBool } from "../react/hooks";
|
||||
|
||||
export default function RightPaneToggle() {
|
||||
const [ rightPaneVisible, setRightPaneVisible ] = useTriliumOptionBool("rightPaneVisible");
|
||||
|
||||
return (
|
||||
<ActionButton
|
||||
className={clsx(
|
||||
`toggle-button right-pane-toggle-button bx-flip-horizontal`,
|
||||
rightPaneVisible ? "action-collapse" : "action-expand"
|
||||
)}
|
||||
text={t("right_pane.toggle")}
|
||||
icon="bx bx-sidebar"
|
||||
onClick={() => setRightPaneVisible(!rightPaneVisible)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -5,14 +5,14 @@
|
||||
* - For example, if there is a formula in the middle of the highlighted text, the two ends of the formula will be regarded as two entries
|
||||
*/
|
||||
|
||||
import { t } from "../services/i18n.js";
|
||||
import attributeService from "../services/attributes.js";
|
||||
import RightPanelWidget from "./right_panel_widget.js";
|
||||
import options from "../services/options.js";
|
||||
import OnClickButtonWidget from "./buttons/onclick_button.js";
|
||||
import appContext, { type EventData } from "../components/app_context.js";
|
||||
import type FNote from "../entities/fnote.js";
|
||||
import attributeService from "../services/attributes.js";
|
||||
import { t } from "../services/i18n.js";
|
||||
import katex from "../services/math.js";
|
||||
import options from "../services/options.js";
|
||||
import OnClickButtonWidget from "./buttons/onclick_button.js";
|
||||
import RightPanelWidget from "./right_panel_widget.js";
|
||||
|
||||
const TPL = /*html*/`<div class="highlights-list-widget">
|
||||
<style>
|
||||
@ -159,13 +159,13 @@ export default class HighlightsListWidget extends RightPanelWidget {
|
||||
*/
|
||||
async replaceMathTextWithKatax(html: string) {
|
||||
const mathTextRegex = /<span class="math-tex">\\\(([\s\S]*?)\\\)<\/span>/g;
|
||||
var matches = [...html.matchAll(mathTextRegex)];
|
||||
const matches = [...html.matchAll(mathTextRegex)];
|
||||
let modifiedText = html;
|
||||
|
||||
if (matches.length > 0) {
|
||||
// Process all matches asynchronously
|
||||
for (const match of matches) {
|
||||
let latexCode = match[1];
|
||||
const latexCode = match[1];
|
||||
let rendered;
|
||||
|
||||
try {
|
||||
@ -234,7 +234,7 @@ export default class HighlightsListWidget extends RightPanelWidget {
|
||||
}
|
||||
|
||||
findSubStr = findSubStr.substring(1);
|
||||
combinedRegexStr = `(` + combinedRegexStr.substring(1) + `)`;
|
||||
combinedRegexStr = `(${combinedRegexStr.substring(1)})`;
|
||||
const combinedRegex = new RegExp(combinedRegexStr, "gi");
|
||||
const $highlightsList = $("<ol>");
|
||||
let prevEndIndex = -1,
|
||||
@ -302,26 +302,28 @@ export default class HighlightsListWidget extends RightPanelWidget {
|
||||
let targetElement;
|
||||
if (isReadOnly) {
|
||||
const $container = await this.noteContext.getContentElement();
|
||||
targetElement = $container
|
||||
.find(findSubStr)
|
||||
.filter(function () {
|
||||
if (findSubStr.indexOf("color") >= 0 && findSubStr.indexOf("background-color") < 0) {
|
||||
let color = this.style.color;
|
||||
const $el = $(this as HTMLElement);
|
||||
return !($el.prop("tagName") === "SPAN" && color === "");
|
||||
} else {
|
||||
if ($container) {
|
||||
targetElement = $container
|
||||
.find(findSubStr)
|
||||
.filter(function () {
|
||||
if (findSubStr.indexOf("color") >= 0 && findSubStr.indexOf("background-color") < 0) {
|
||||
const color = this.style.color;
|
||||
const $el = $(this as HTMLElement);
|
||||
return !($el.prop("tagName") === "SPAN" && color === "");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
})
|
||||
.filter(function () {
|
||||
const $el = $(this as HTMLElement);
|
||||
return (
|
||||
$el.parent(findSubStr).length === 0 &&
|
||||
$el.parent().parent(findSubStr).length === 0 &&
|
||||
$el.parent().parent().parent(findSubStr).length === 0 &&
|
||||
$el.parent().parent().parent().parent(findSubStr).length === 0
|
||||
);
|
||||
});
|
||||
|
||||
})
|
||||
.filter(function () {
|
||||
const $el = $(this as HTMLElement);
|
||||
return (
|
||||
$el.parent(findSubStr).length === 0 &&
|
||||
$el.parent().parent(findSubStr).length === 0 &&
|
||||
$el.parent().parent().parent(findSubStr).length === 0 &&
|
||||
$el.parent().parent().parent().parent(findSubStr).length === 0
|
||||
);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const textEditor = await this.noteContext.getTextEditor();
|
||||
const el = textEditor?.editing.view.domRoots.values().next().value;
|
||||
@ -333,11 +335,11 @@ export default class HighlightsListWidget extends RightPanelWidget {
|
||||
// the background-color error will be regarded as color, so it needs to be filtered
|
||||
const $el = $(this as HTMLElement);
|
||||
if (findSubStr.indexOf("color") >= 0 && findSubStr.indexOf("background-color") < 0) {
|
||||
let color = this.style.color;
|
||||
const color = this.style.color;
|
||||
return !($el.prop("tagName") === "SPAN" && color === "");
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
|
||||
})
|
||||
.filter(function () {
|
||||
// Need to filter out the child elements of the element that has been found
|
||||
|
||||
@ -1,8 +1,16 @@
|
||||
interface IconProps {
|
||||
import clsx from "clsx";
|
||||
import { HTMLAttributes } from "preact";
|
||||
|
||||
interface IconProps extends Pick<HTMLAttributes<HTMLSpanElement>, "className" | "onClick"> {
|
||||
icon?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function Icon({ icon, className }: IconProps) {
|
||||
return <span class={`${icon ?? "bx bx-empty"} ${className ?? ""}`}></span>
|
||||
}
|
||||
export default function Icon({ icon, className, ...restProps }: IconProps) {
|
||||
return (
|
||||
<span
|
||||
class={clsx(icon ?? "bx bx-empty", className)}
|
||||
{...restProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { CKTextEditor } from "@triliumnext/ckeditor5";
|
||||
import { FilterLabelsByType, KeyboardActionNames, OptionNames, RelationNames } from "@triliumnext/commons";
|
||||
import { Tooltip } from "bootstrap";
|
||||
import Mark from "mark.js";
|
||||
@ -21,7 +22,8 @@ import shortcuts, { Handler, removeIndividualBinding } from "../../services/shor
|
||||
import SpacedUpdate from "../../services/spaced_update";
|
||||
import toast, { ToastOptions } from "../../services/toast";
|
||||
import tree from "../../services/tree";
|
||||
import utils, { escapeRegExp, randomString, reloadFrontendApp } from "../../services/utils";
|
||||
import utils, { escapeRegExp, getErrorMessage, randomString, reloadFrontendApp } from "../../services/utils";
|
||||
import ws from "../../services/ws";
|
||||
import BasicWidget, { ReactWrappedWidget } from "../basic_widget";
|
||||
import NoteContextAwareWidget from "../note_context_aware_widget";
|
||||
import { DragData } from "../note_tree";
|
||||
@ -167,13 +169,20 @@ export function useTriliumOption(name: OptionNames, needsRefresh?: boolean): [st
|
||||
|
||||
const wrappedSetValue = useMemo(() => {
|
||||
return async (newValue: OptionValue) => {
|
||||
await options.save(name, newValue);
|
||||
const originalValue = value;
|
||||
setValue(String(newValue));
|
||||
try {
|
||||
await options.save(name, newValue);
|
||||
} catch (e: unknown) {
|
||||
ws.logError(getErrorMessage(e));
|
||||
setValue(originalValue);
|
||||
}
|
||||
|
||||
if (needsRefresh) {
|
||||
reloadFrontendApp(`option change: ${name}`);
|
||||
}
|
||||
};
|
||||
}, [ name, needsRefresh ]);
|
||||
}, [ name, needsRefresh, value ]);
|
||||
|
||||
useTriliumEvent("entitiesReloaded", useCallback(({ loadResults }) => {
|
||||
if (loadResults.getOptionNames().includes(name)) {
|
||||
@ -625,13 +634,14 @@ export function useLegacyWidget<T extends BasicWidget>(widgetFactory: () => T, {
|
||||
|
||||
const renderedWidget = widget.render();
|
||||
return [ widget, renderedWidget ];
|
||||
}, []);
|
||||
}, [ noteContext, parentComponent, widgetFactory]);
|
||||
|
||||
// Attach the widget to the parent.
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
ref.current.innerHTML = "";
|
||||
renderedWidget.appendTo(ref.current);
|
||||
const parentContainer = ref.current;
|
||||
if (parentContainer) {
|
||||
parentContainer.replaceChildren();
|
||||
renderedWidget.appendTo(parentContainer);
|
||||
}
|
||||
}, [ renderedWidget ]);
|
||||
|
||||
@ -640,7 +650,7 @@ export function useLegacyWidget<T extends BasicWidget>(widgetFactory: () => T, {
|
||||
if (noteContext && widget instanceof NoteContextAwareWidget) {
|
||||
widget.activeContextChangedEvent({ noteContext });
|
||||
}
|
||||
}, [ noteContext ]);
|
||||
}, [ noteContext, widget ]);
|
||||
|
||||
useDebugValue(widget);
|
||||
|
||||
@ -1074,3 +1084,56 @@ export function useNoteColorClass(note: FNote | null | undefined) {
|
||||
}, [ color, note ]);
|
||||
return colorClass;
|
||||
}
|
||||
|
||||
export function useTextEditor(noteContext: NoteContext | null | undefined) {
|
||||
const [ textEditor, setTextEditor ] = useState<CKTextEditor | null>(null);
|
||||
const requestIdRef = useRef(0);
|
||||
|
||||
// React to note context change and initial state.
|
||||
useEffect(() => {
|
||||
if (!noteContext) {
|
||||
setTextEditor(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const requestId = ++requestIdRef.current;
|
||||
noteContext.getTextEditor((textEditor) => {
|
||||
// Prevent stale async.
|
||||
if (requestId !== requestIdRef.current) return;
|
||||
setTextEditor(textEditor);
|
||||
});
|
||||
}, [ noteContext ]);
|
||||
|
||||
// React to editor initializing.
|
||||
useTriliumEvent("textEditorRefreshed", ({ ntxId: eventNtxId, editor }) => {
|
||||
if (eventNtxId !== noteContext?.ntxId) return;
|
||||
setTextEditor(editor);
|
||||
});
|
||||
|
||||
return textEditor;
|
||||
}
|
||||
|
||||
export function useContentElement(noteContext: NoteContext | null | undefined) {
|
||||
const [ contentElement, setContentElement ] = useState<HTMLElement | null>(null);
|
||||
const requestIdRef = useRef(0);
|
||||
const [, forceUpdate] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
const requestId = ++requestIdRef.current;
|
||||
noteContext?.getContentElement().then(contentElement => {
|
||||
// Prevent stale async.
|
||||
if (requestId !== requestIdRef.current) return;
|
||||
setContentElement(contentElement?.[0] ?? null);
|
||||
forceUpdate(v => v + 1);
|
||||
});
|
||||
}, [ noteContext ]);
|
||||
|
||||
// React to content changes initializing.
|
||||
useTriliumEvent("contentElRefreshed", ({ ntxId: eventNtxId, contentEl }) => {
|
||||
if (eventNtxId !== noteContext?.ntxId) return;
|
||||
setContentElement(contentEl);
|
||||
forceUpdate(v => v + 1);
|
||||
});
|
||||
|
||||
return contentElement;
|
||||
}
|
||||
|
||||
283
apps/client/src/widgets/sidebar/HighlightsList.tsx
Normal file
283
apps/client/src/widgets/sidebar/HighlightsList.tsx
Normal file
@ -0,0 +1,283 @@
|
||||
import { CKTextEditor, ModelText } from "@triliumnext/ckeditor5";
|
||||
import { createPortal } from "preact/compat";
|
||||
import { useCallback, useEffect, useState } from "preact/hooks";
|
||||
|
||||
import { t } from "../../services/i18n";
|
||||
import { useActiveNoteContext, useContentElement, useIsNoteReadOnly, useNoteProperty, useTextEditor, useTriliumOptionJson } from "../react/hooks";
|
||||
import Modal from "../react/Modal";
|
||||
import { HighlightsListOptions } from "../type_widgets/options/text_notes";
|
||||
import RightPanelWidget from "./RightPanelWidget";
|
||||
|
||||
interface RawHighlight {
|
||||
id: string;
|
||||
text: string;
|
||||
attrs: {
|
||||
bold: boolean;
|
||||
italic: boolean;
|
||||
underline: boolean;
|
||||
color: string | undefined;
|
||||
background: string | undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export default function HighlightsList() {
|
||||
const { note, noteContext } = useActiveNoteContext();
|
||||
const noteType = useNoteProperty(note, "type");
|
||||
const { isReadOnly } = useIsNoteReadOnly(note, noteContext);
|
||||
|
||||
return (
|
||||
<>
|
||||
{noteType === "text" && isReadOnly && <ReadOnlyTextHighlightsList />}
|
||||
{noteType === "text" && !isReadOnly && <EditableTextHighlightsList />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function HighlightListOptionsModal({ shown, setShown }: { shown: boolean, setShown(value: boolean): void }) {
|
||||
return (
|
||||
<Modal
|
||||
className="highlights-list-options-modal"
|
||||
size="md"
|
||||
title={t("highlights_list_2.modal_title")}
|
||||
show={shown}
|
||||
onHidden={() => setShown(false)}
|
||||
>
|
||||
<HighlightsListOptions />
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function AbstractHighlightsList<T extends RawHighlight>({ highlights, scrollToHighlight }: {
|
||||
highlights: T[],
|
||||
scrollToHighlight(highlight: T): void;
|
||||
}) {
|
||||
const [ highlightsList ] = useTriliumOptionJson<["bold" | "italic" | "underline" | "color" | "bgColor"]>("highlightsList");
|
||||
const highlightsListSet = new Set(highlightsList || []);
|
||||
const filteredHighlights = highlights.filter(highlight => {
|
||||
const { attrs } = highlight;
|
||||
return (
|
||||
(highlightsListSet.has("bold") && attrs.bold) ||
|
||||
(highlightsListSet.has("italic") && attrs.italic) ||
|
||||
(highlightsListSet.has("underline") && attrs.underline) ||
|
||||
(highlightsListSet.has("color") && !!attrs.color) ||
|
||||
(highlightsListSet.has("bgColor") && !!attrs.background)
|
||||
);
|
||||
});
|
||||
|
||||
const [ shown, setShown ] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<RightPanelWidget
|
||||
id="highlights"
|
||||
title={t("highlights_list_2.title_with_count", { count: filteredHighlights.length })}
|
||||
contextMenuItems={[
|
||||
{
|
||||
title: t("highlights_list_2.menu_configure"),
|
||||
uiIcon: "bx bx-cog",
|
||||
handler: () => setShown(true)
|
||||
}
|
||||
]}
|
||||
grow
|
||||
>
|
||||
<span className="highlights-list">
|
||||
{filteredHighlights.length > 0 ? (
|
||||
<ol>
|
||||
{filteredHighlights.map(highlight => (
|
||||
<li
|
||||
key={highlight.id}
|
||||
onClick={() => scrollToHighlight(highlight)}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
fontWeight: highlight.attrs.bold ? "700" : undefined,
|
||||
fontStyle: highlight.attrs.italic ? "italic" : undefined,
|
||||
textDecoration: highlight.attrs.underline ? "underline" : undefined,
|
||||
color: highlight.attrs.color,
|
||||
backgroundColor: highlight.attrs.background
|
||||
}}
|
||||
>{highlight.text}</span>
|
||||
</li>
|
||||
))}
|
||||
</ol>
|
||||
) : (
|
||||
<div className="no-highlights">
|
||||
{t("highlights_list_2.no_highlights")}
|
||||
</div>
|
||||
)}
|
||||
</span>
|
||||
</RightPanelWidget>
|
||||
{createPortal(<HighlightListOptionsModal shown={shown} setShown={setShown} />, document.body)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
//#region Editable text (CKEditor)
|
||||
interface CKHighlight extends RawHighlight {
|
||||
textNode: ModelText;
|
||||
offset: number | null;
|
||||
}
|
||||
|
||||
function EditableTextHighlightsList() {
|
||||
const { note, noteContext } = useActiveNoteContext();
|
||||
const textEditor = useTextEditor(noteContext);
|
||||
const [ highlights, setHighlights ] = useState<CKHighlight[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!textEditor) return;
|
||||
setHighlights(extractHighlightsFromTextEditor(textEditor));
|
||||
|
||||
// React to changes.
|
||||
const changeCallback = () => {
|
||||
const changes = textEditor.model.document.differ.getChanges();
|
||||
const affectsHighlights = changes.some(change => {
|
||||
// Text inserted or removed
|
||||
if (change.type === 'insert' || change.type === 'remove') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Formatting attribute changed
|
||||
if (change.type === 'attribute' &&
|
||||
(
|
||||
change.attributeKey === 'bold' ||
|
||||
change.attributeKey === 'italic' ||
|
||||
change.attributeKey === 'underline' ||
|
||||
change.attributeKey === 'fontColor' ||
|
||||
change.attributeKey === 'fontBackgroundColor'
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
if (affectsHighlights) {
|
||||
setHighlights(extractHighlightsFromTextEditor(textEditor));
|
||||
}
|
||||
};
|
||||
|
||||
textEditor.model.document.on("change:data", changeCallback);
|
||||
return () => textEditor.model.document.off("change:data", changeCallback);
|
||||
}, [ textEditor, note ]);
|
||||
|
||||
const scrollToHeading = useCallback((highlight: CKHighlight) => {
|
||||
if (!textEditor) return;
|
||||
|
||||
const modelPos = textEditor.model.createPositionAt(highlight.textNode, "before");
|
||||
const viewPos = textEditor.editing.mapper.toViewPosition(modelPos);
|
||||
const domConverter = textEditor.editing.view.domConverter;
|
||||
const domPos = domConverter.viewPositionToDom(viewPos);
|
||||
|
||||
if (!domPos) return;
|
||||
if (domPos.parent instanceof HTMLElement) {
|
||||
domPos.parent.scrollIntoView();
|
||||
} else if (domPos.parent instanceof Text) {
|
||||
domPos.parent.parentElement?.scrollIntoView();
|
||||
}
|
||||
|
||||
}, [ textEditor ]);
|
||||
|
||||
return <AbstractHighlightsList
|
||||
highlights={highlights}
|
||||
scrollToHighlight={scrollToHeading}
|
||||
/>;
|
||||
}
|
||||
|
||||
function extractHighlightsFromTextEditor(editor: CKTextEditor) {
|
||||
const result: CKHighlight[] = [];
|
||||
const root = editor.model.document.getRoot();
|
||||
if (!root) return [];
|
||||
|
||||
for (const { item } of editor.model.createRangeIn(root).getWalker({ ignoreElementEnd: true })) {
|
||||
if (!item.is('$textProxy') || !item.data.trim()) continue;
|
||||
|
||||
const attrs: RawHighlight["attrs"] = {
|
||||
bold: item.hasAttribute('bold'),
|
||||
italic: item.hasAttribute('italic'),
|
||||
underline: item.hasAttribute('underline'),
|
||||
color: item.getAttribute('fontColor') as string | undefined,
|
||||
background: item.getAttribute('fontBackgroundColor') as string | undefined
|
||||
};
|
||||
|
||||
if (Object.values(attrs).some(Boolean)) {
|
||||
result.push({
|
||||
id: crypto.randomUUID(),
|
||||
text: item.data,
|
||||
attrs,
|
||||
textNode: item.textNode,
|
||||
offset: item.startOffset
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region Read-only text
|
||||
interface DomHighlight extends RawHighlight {
|
||||
element: HTMLElement;
|
||||
}
|
||||
|
||||
function ReadOnlyTextHighlightsList() {
|
||||
const { noteContext } = useActiveNoteContext();
|
||||
const contentEl = useContentElement(noteContext);
|
||||
const highlights = extractHighlightsFromStaticHtml(contentEl);
|
||||
|
||||
const scrollToHighlight = useCallback((highlight: DomHighlight) => {
|
||||
highlight.element.scrollIntoView();
|
||||
}, []);
|
||||
|
||||
return <AbstractHighlightsList
|
||||
highlights={highlights}
|
||||
scrollToHighlight={scrollToHighlight}
|
||||
/>;
|
||||
}
|
||||
|
||||
function extractHighlightsFromStaticHtml(el: HTMLElement | null) {
|
||||
if (!el) return [];
|
||||
|
||||
const { color: defaultColor, backgroundColor: defaultBackgroundColor } = getComputedStyle(el);
|
||||
|
||||
const walker = document.createTreeWalker(
|
||||
el,
|
||||
NodeFilter.SHOW_TEXT,
|
||||
null
|
||||
);
|
||||
|
||||
const highlights: DomHighlight[] = [];
|
||||
|
||||
let node: Node | null;
|
||||
while ((node = walker.nextNode())) {
|
||||
const el = node.parentElement;
|
||||
if (!el || !node.textContent?.trim()) continue;
|
||||
|
||||
const style = getComputedStyle(el);
|
||||
|
||||
if (
|
||||
el.closest('strong, em, u') ||
|
||||
style.color !== defaultColor ||
|
||||
style.backgroundColor !== defaultBackgroundColor
|
||||
) {
|
||||
const attrs: RawHighlight["attrs"] = {
|
||||
bold: !!el.closest("strong"),
|
||||
italic: !!el.closest("em"),
|
||||
underline: !!el.closest("u"),
|
||||
background: el.style.backgroundColor,
|
||||
color: el.style.color
|
||||
};
|
||||
|
||||
if (Object.values(attrs).some(Boolean)) {
|
||||
highlights.push({
|
||||
id: crypto.randomUUID(),
|
||||
text: node.textContent,
|
||||
element: el,
|
||||
attrs
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return highlights;
|
||||
}
|
||||
//#endregion
|
||||
61
apps/client/src/widgets/sidebar/RightPanelContainer.css
Normal file
61
apps/client/src/widgets/sidebar/RightPanelContainer.css
Normal file
@ -0,0 +1,61 @@
|
||||
body.experimental-feature-new-layout #right-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.card {
|
||||
margin-inline: 0;
|
||||
border-bottom: 1px solid var(--main-border-color);
|
||||
border-radius: 0;
|
||||
|
||||
.card-header {
|
||||
padding: 1px 0 0 0;
|
||||
cursor: pointer;
|
||||
justify-content: flex-start;
|
||||
--icon-button-size: 26px;
|
||||
|
||||
.card-header-title {
|
||||
padding-inline: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.card-header-buttons {
|
||||
transform: none;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
&.collapsed .card-header > .bx {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
}
|
||||
|
||||
.card.grow:not(.collapsed) {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.gutter-vertical + .card .card-header {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.no-items {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
padding: 0.75em;
|
||||
color: var(--muted-text-color);
|
||||
|
||||
.bx {
|
||||
font-size: 3em;
|
||||
}
|
||||
|
||||
button {
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
148
apps/client/src/widgets/sidebar/RightPanelContainer.tsx
Normal file
148
apps/client/src/widgets/sidebar/RightPanelContainer.tsx
Normal file
@ -0,0 +1,148 @@
|
||||
//! This is currently only used for the new layout.
|
||||
import "./RightPanelContainer.css";
|
||||
|
||||
import Split from "@triliumnext/split.js";
|
||||
import { VNode } from "preact";
|
||||
import { useEffect, useRef } from "preact/hooks";
|
||||
|
||||
import appContext from "../../components/app_context";
|
||||
import { t } from "../../services/i18n";
|
||||
import options from "../../services/options";
|
||||
import { DEFAULT_GUTTER_SIZE } from "../../services/resizer";
|
||||
import BasicWidget from "../basic_widget";
|
||||
import Button from "../react/Button";
|
||||
import { useActiveNoteContext, useLegacyWidget, useNoteProperty, useTriliumOptionBool, useTriliumOptionJson } from "../react/hooks";
|
||||
import Icon from "../react/Icon";
|
||||
import LegacyRightPanelWidget from "../right_panel_widget";
|
||||
import HighlightsList from "./HighlightsList";
|
||||
import RightPanelWidget from "./RightPanelWidget";
|
||||
import TableOfContents from "./TableOfContents";
|
||||
|
||||
const MIN_WIDTH_PERCENT = 5;
|
||||
|
||||
interface RightPanelWidgetDefinition {
|
||||
el: VNode;
|
||||
enabled: boolean;
|
||||
position: number;
|
||||
}
|
||||
|
||||
export default function RightPanelContainer({ customWidgets }: { customWidgets: BasicWidget[] }) {
|
||||
const [ rightPaneVisible, setRightPaneVisible ] = useTriliumOptionBool("rightPaneVisible");
|
||||
const items = useItems(rightPaneVisible, customWidgets);
|
||||
useSplit(rightPaneVisible);
|
||||
|
||||
return (
|
||||
<div id="right-pane">
|
||||
{rightPaneVisible && (
|
||||
items.length > 0 ? (
|
||||
items
|
||||
) : (
|
||||
<div className="no-items">
|
||||
<Icon icon="bx bx-sidebar" />
|
||||
{t("right_pane.empty_message")}
|
||||
<Button
|
||||
text={t("right_pane.empty_button")}
|
||||
onClick={() => setRightPaneVisible(!rightPaneVisible)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function useItems(rightPaneVisible: boolean, customWidgets: BasicWidget[]) {
|
||||
const { note } = useActiveNoteContext();
|
||||
const noteType = useNoteProperty(note, "type");
|
||||
const [ highlightsList ] = useTriliumOptionJson<string[]>("highlightsList");
|
||||
|
||||
if (!rightPaneVisible) return [];
|
||||
const definitions: RightPanelWidgetDefinition[] = [
|
||||
{
|
||||
el: <TableOfContents />,
|
||||
enabled: (noteType === "text" || noteType === "doc"),
|
||||
position: 10,
|
||||
},
|
||||
{
|
||||
el: <HighlightsList />,
|
||||
enabled: noteType === "text" && highlightsList.length > 0,
|
||||
position: 20,
|
||||
},
|
||||
...customWidgets.map((w, i) => ({
|
||||
el: <CustomWidget key={w._noteId} originalWidget={w as LegacyRightPanelWidget} />,
|
||||
enabled: true,
|
||||
position: w.position ?? 30 + i * 10
|
||||
}))
|
||||
];
|
||||
|
||||
return definitions
|
||||
.filter(e => e.enabled)
|
||||
.toSorted((a, b) => a.position - b.position)
|
||||
.map(e => e.el);
|
||||
}
|
||||
|
||||
function useSplit(visible: boolean) {
|
||||
// Split between right pane and the content pane.
|
||||
useEffect(() => {
|
||||
if (!visible) return;
|
||||
|
||||
// We are intentionally omitting useTriliumOption to avoid re-render due to size change.
|
||||
const rightPaneWidth = Math.max(MIN_WIDTH_PERCENT, options.getInt("rightPaneWidth") ?? MIN_WIDTH_PERCENT);
|
||||
const splitInstance = Split(["#center-pane", "#right-pane"], {
|
||||
sizes: [100 - rightPaneWidth, rightPaneWidth],
|
||||
gutterSize: DEFAULT_GUTTER_SIZE,
|
||||
minSize: [300, 180],
|
||||
rtl: glob.isRtl,
|
||||
onDragEnd: (sizes) => options.save("rightPaneWidth", Math.round(sizes[1]))
|
||||
});
|
||||
return () => splitInstance.destroy();
|
||||
}, [ visible ]);
|
||||
}
|
||||
|
||||
function CustomWidget({ originalWidget }: { originalWidget: LegacyRightPanelWidget }) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
return (
|
||||
<RightPanelWidget
|
||||
id={originalWidget._noteId}
|
||||
title={originalWidget.widgetTitle}
|
||||
containerRef={containerRef}
|
||||
contextMenuItems={[
|
||||
{
|
||||
title: t("right_pane.custom_widget_go_to_source"),
|
||||
uiIcon: "bx bx-code-curly",
|
||||
handler: () => appContext.tabManager.openInNewTab(originalWidget._noteId, null, true)
|
||||
}
|
||||
]}
|
||||
>
|
||||
<CustomWidgetContent originalWidget={originalWidget} />
|
||||
</RightPanelWidget>
|
||||
);
|
||||
}
|
||||
|
||||
function CustomWidgetContent({ originalWidget }: { originalWidget: LegacyRightPanelWidget }) {
|
||||
const { noteContext } = useActiveNoteContext();
|
||||
const [ el ] = useLegacyWidget(() => {
|
||||
originalWidget.contentSized();
|
||||
|
||||
// Monkey-patch the original widget by replacing the default initialization logic.
|
||||
originalWidget.doRender = function doRender(this: LegacyRightPanelWidget) {
|
||||
this.$widget = $("<div>");
|
||||
this.$body = this.$widget;
|
||||
const renderResult = this.doRenderBody();
|
||||
if (typeof renderResult === "object" && "catch" in renderResult) {
|
||||
this.initialized = renderResult.catch((e) => {
|
||||
this.logRenderingError(e);
|
||||
});
|
||||
} else {
|
||||
this.initialized = Promise.resolve();
|
||||
}
|
||||
};
|
||||
|
||||
return originalWidget;
|
||||
}, {
|
||||
noteContext
|
||||
});
|
||||
|
||||
return el;
|
||||
}
|
||||
@ -1,15 +1,26 @@
|
||||
import { useContext, useRef } from "preact/hooks";
|
||||
import clsx from "clsx";
|
||||
import { ComponentChildren, RefObject } from "preact";
|
||||
import { useContext, useState } from "preact/hooks";
|
||||
|
||||
import contextMenu, { MenuItem } from "../../menus/context_menu";
|
||||
import ActionButton from "../react/ActionButton";
|
||||
import { useSyncedRef, useTriliumOptionJson } from "../react/hooks";
|
||||
import { ParentComponent } from "../react/react_utils";
|
||||
import { ComponentChildren } from "preact";
|
||||
|
||||
interface RightPanelWidgetProps {
|
||||
id: string;
|
||||
title: string;
|
||||
children: ComponentChildren;
|
||||
buttons?: ComponentChildren;
|
||||
containerRef?: RefObject<HTMLDivElement>;
|
||||
contextMenuItems?: MenuItem<unknown>[];
|
||||
grow?: boolean;
|
||||
}
|
||||
|
||||
export default function RightPanelWidget({ title, buttons, children }: RightPanelWidgetProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
export default function RightPanelWidget({ id, title, buttons, children, containerRef: externalContainerRef, contextMenuItems, grow }: RightPanelWidgetProps) {
|
||||
const [ rightPaneCollapsedItems, setRightPaneCollapsedItems ] = useTriliumOptionJson<string[]>("rightPaneCollapsedItems");
|
||||
const [ expanded, setExpanded ] = useState(!rightPaneCollapsedItems.includes(id));
|
||||
const containerRef = useSyncedRef<HTMLDivElement>(externalContainerRef, null);
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
|
||||
if (parentComponent) {
|
||||
@ -17,16 +28,53 @@ export default function RightPanelWidget({ title, buttons, children }: RightPane
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={containerRef} class="card widget" style={{contain: "none"}}>
|
||||
<div class="card-header">
|
||||
<div
|
||||
ref={containerRef}
|
||||
class={clsx("card widget", {
|
||||
collapsed: !expanded,
|
||||
grow
|
||||
})}
|
||||
>
|
||||
<div
|
||||
class="card-header"
|
||||
onClick={() => {
|
||||
const newExpanded = !expanded;
|
||||
setExpanded(newExpanded);
|
||||
const rightPaneCollapsedItemsSet = new Set(rightPaneCollapsedItems);
|
||||
if (newExpanded) {
|
||||
rightPaneCollapsedItemsSet.delete(id);
|
||||
} else {
|
||||
rightPaneCollapsedItemsSet.add(id);
|
||||
}
|
||||
setRightPaneCollapsedItems(Array.from(rightPaneCollapsedItemsSet));
|
||||
}}
|
||||
>
|
||||
<ActionButton icon="bx bx-chevron-down" text="" />
|
||||
<div class="card-header-title">{title}</div>
|
||||
<div class="card-header-buttons">{buttons}</div>
|
||||
<div class="card-header-buttons">
|
||||
{buttons}
|
||||
{contextMenuItems && (
|
||||
<ActionButton
|
||||
icon="bx bx-dots-vertical-rounded"
|
||||
text=""
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
contextMenu.show({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
items: contextMenuItems,
|
||||
selectMenuItemHandler: () => {}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id={parentComponent?.componentId} class="body-wrapper">
|
||||
<div class="card-body">
|
||||
{expanded && <div class="card-body">
|
||||
{children}
|
||||
</div>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
82
apps/client/src/widgets/sidebar/TableOfContents.css
Normal file
82
apps/client/src/widgets/sidebar/TableOfContents.css
Normal file
@ -0,0 +1,82 @@
|
||||
.toc ol {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding-inline-start: 0px;
|
||||
transition: max-height 0.3s ease;
|
||||
}
|
||||
|
||||
.toc li.collapsed + ol {
|
||||
display:none;
|
||||
}
|
||||
|
||||
.toc li + ol:before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
border-inline-start: 1px solid var(--main-border-color);
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.toc li {
|
||||
display: flex;
|
||||
position: relative;
|
||||
list-style: none;
|
||||
align-items: center;
|
||||
padding-inline-start: 7px;
|
||||
cursor: pointer;
|
||||
text-align: justify;
|
||||
word-wrap: break-word;
|
||||
hyphens: auto;
|
||||
}
|
||||
|
||||
.toc > ol {
|
||||
--toc-depth-level: 1;
|
||||
}
|
||||
.toc > ol > ol {
|
||||
--toc-depth-level: 2;
|
||||
}
|
||||
.toc > ol > ol > ol {
|
||||
--toc-depth-level: 3;
|
||||
}
|
||||
.toc > ol > ol > ol > ol {
|
||||
--toc-depth-level: 4;
|
||||
}
|
||||
.toc > ol > ol > ol > ol > ol {
|
||||
--toc-depth-level: 5;
|
||||
}
|
||||
|
||||
.toc > ol ol::before {
|
||||
inset-inline-start: calc((var(--toc-depth-level) - 2) * 20px + 14px);
|
||||
}
|
||||
|
||||
.toc li {
|
||||
padding-inline-start: calc((var(--toc-depth-level) - 1) * 20px + 4px);
|
||||
}
|
||||
|
||||
.toc li .collapse-button {
|
||||
display: flex;
|
||||
position: relative;
|
||||
width: 21px;
|
||||
height: 21px;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.toc li.collapsed .collapse-button {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.toc li .item-content {
|
||||
margin-inline-start: 25px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.toc li .collapse-button + .item-content {
|
||||
margin-inline-start: 4px;
|
||||
}
|
||||
|
||||
.toc li:hover {
|
||||
font-weight: bold;
|
||||
}
|
||||
222
apps/client/src/widgets/sidebar/TableOfContents.tsx
Normal file
222
apps/client/src/widgets/sidebar/TableOfContents.tsx
Normal file
@ -0,0 +1,222 @@
|
||||
import "./TableOfContents.css";
|
||||
|
||||
import { CKTextEditor, ModelElement } from "@triliumnext/ckeditor5";
|
||||
import clsx from "clsx";
|
||||
import { useCallback, useEffect, useState } from "preact/hooks";
|
||||
|
||||
import { t } from "../../services/i18n";
|
||||
import { useActiveNoteContext, useContentElement, useIsNoteReadOnly, useNoteProperty, useTextEditor } from "../react/hooks";
|
||||
import Icon from "../react/Icon";
|
||||
import RightPanelWidget from "./RightPanelWidget";
|
||||
|
||||
//#region Generic impl.
|
||||
interface RawHeading {
|
||||
id: string;
|
||||
level: number;
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface HeadingsWithNesting extends RawHeading {
|
||||
children: HeadingsWithNesting[];
|
||||
}
|
||||
|
||||
export default function TableOfContents() {
|
||||
const { note, noteContext } = useActiveNoteContext();
|
||||
const noteType = useNoteProperty(note, "type");
|
||||
const { isReadOnly } = useIsNoteReadOnly(note, noteContext);
|
||||
|
||||
return (
|
||||
<RightPanelWidget id="toc" title={t("toc.table_of_contents")} grow>
|
||||
{((noteType === "text" && isReadOnly) || (noteType === "doc")) && <ReadOnlyTextTableOfContents />}
|
||||
{noteType === "text" && !isReadOnly && <EditableTextTableOfContents />}
|
||||
</RightPanelWidget>
|
||||
);
|
||||
}
|
||||
|
||||
function AbstractTableOfContents<T extends RawHeading>({ headings, scrollToHeading }: {
|
||||
headings: T[];
|
||||
scrollToHeading(heading: T): void;
|
||||
}) {
|
||||
const nestedHeadings = buildHeadingTree(headings);
|
||||
return (
|
||||
<span className="toc">
|
||||
{nestedHeadings.length > 0 ? (
|
||||
<ol>
|
||||
{nestedHeadings.map(heading => <TableOfContentsHeading key={heading.id} heading={heading} scrollToHeading={scrollToHeading} />)}
|
||||
</ol>
|
||||
) : (
|
||||
<div className="no-headings">{t("toc.no_headings")}</div>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function TableOfContentsHeading({ heading, scrollToHeading }: {
|
||||
heading: HeadingsWithNesting;
|
||||
scrollToHeading(heading: RawHeading): void;
|
||||
}) {
|
||||
const [ collapsed, setCollapsed ] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<li className={clsx(collapsed && "collapsed")}>
|
||||
{heading.children.length > 0 && (
|
||||
<Icon
|
||||
className="collapse-button"
|
||||
icon="bx bx-chevron-down"
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
/>
|
||||
)}
|
||||
<span
|
||||
className="item-content"
|
||||
onClick={() => scrollToHeading(heading)}
|
||||
>{heading.text}</span>
|
||||
</li>
|
||||
{heading.children && (
|
||||
<ol>
|
||||
{heading.children.map(heading => <TableOfContentsHeading key={heading.id} heading={heading} scrollToHeading={scrollToHeading} />)}
|
||||
</ol>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function buildHeadingTree(headings: RawHeading[]): HeadingsWithNesting[] {
|
||||
const root: HeadingsWithNesting = { level: 0, text: "", children: [], id: "_root" };
|
||||
const stack: HeadingsWithNesting[] = [root];
|
||||
|
||||
for (const h of headings) {
|
||||
const node: HeadingsWithNesting = { ...h, children: [] };
|
||||
|
||||
// Pop until we find a parent with lower level
|
||||
while (stack.length > 1 && stack[stack.length - 1].level >= h.level) {
|
||||
stack.pop();
|
||||
}
|
||||
|
||||
// Attach to current parent
|
||||
stack[stack.length - 1].children.push(node);
|
||||
|
||||
// This node becomes the new parent
|
||||
stack.push(node);
|
||||
}
|
||||
|
||||
return root.children;
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region Editable text (CKEditor)
|
||||
const TOC_ID = 'tocId';
|
||||
|
||||
interface CKHeading extends RawHeading {
|
||||
element: ModelElement;
|
||||
}
|
||||
|
||||
function EditableTextTableOfContents() {
|
||||
const { note, noteContext } = useActiveNoteContext();
|
||||
const textEditor = useTextEditor(noteContext);
|
||||
const [ headings, setHeadings ] = useState<CKHeading[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!textEditor) return;
|
||||
const headings = extractTocFromTextEditor(textEditor);
|
||||
setHeadings(headings);
|
||||
|
||||
// React to changes.
|
||||
const changeCallback = () => {
|
||||
const changes = textEditor.model.document.differ.getChanges();
|
||||
|
||||
const affectsHeadings = changes.some( change => {
|
||||
return (
|
||||
change.type === 'insert' || change.type === 'remove' || (change.type === 'attribute' && change.attributeKey === 'headingLevel')
|
||||
);
|
||||
});
|
||||
if (affectsHeadings) {
|
||||
setHeadings(extractTocFromTextEditor(textEditor));
|
||||
}
|
||||
};
|
||||
|
||||
textEditor.model.document.on("change:data", changeCallback);
|
||||
return () => textEditor.model.document.off("change:data", changeCallback);
|
||||
}, [ textEditor, note ]);
|
||||
|
||||
const scrollToHeading = useCallback((heading: CKHeading) => {
|
||||
if (!textEditor) return;
|
||||
|
||||
const viewEl = textEditor.editing.mapper.toViewElement(heading.element);
|
||||
if (!viewEl) return;
|
||||
|
||||
const domEl = textEditor.editing.view.domConverter.mapViewToDom(viewEl);
|
||||
domEl?.scrollIntoView();
|
||||
}, [ textEditor ]);
|
||||
|
||||
return <AbstractTableOfContents
|
||||
headings={headings}
|
||||
scrollToHeading={scrollToHeading}
|
||||
/>;
|
||||
}
|
||||
|
||||
function extractTocFromTextEditor(editor: CKTextEditor) {
|
||||
const headings: CKHeading[] = [];
|
||||
|
||||
const root = editor.model.document.getRoot();
|
||||
if (!root) return [];
|
||||
|
||||
editor.model.change(writer => {
|
||||
for (const { type, item } of editor.model.createRangeIn(root).getWalker()) {
|
||||
if (type !== "elementStart" || !item.is('element') || !item.name.startsWith('heading')) continue;
|
||||
|
||||
const level = Number(item.name.replace( 'heading', '' ));
|
||||
const text = Array.from( item.getChildren() )
|
||||
.map( c => c.is( '$text' ) ? c.data : '' )
|
||||
.join( '' );
|
||||
|
||||
// Assign a unique ID
|
||||
let tocId = item.getAttribute(TOC_ID) as string | undefined;
|
||||
if (!tocId) {
|
||||
tocId = crypto.randomUUID();
|
||||
writer.setAttribute(TOC_ID, tocId, item);
|
||||
}
|
||||
|
||||
headings.push({ level, text, element: item, id: tocId });
|
||||
}
|
||||
});
|
||||
|
||||
return headings;
|
||||
}
|
||||
//#endregion
|
||||
|
||||
//#region Read-only text
|
||||
interface DomHeading extends RawHeading {
|
||||
element: HTMLHeadingElement;
|
||||
}
|
||||
|
||||
function ReadOnlyTextTableOfContents() {
|
||||
const { noteContext } = useActiveNoteContext();
|
||||
const contentEl = useContentElement(noteContext);
|
||||
const headings = extractTocFromStaticHtml(contentEl);
|
||||
|
||||
const scrollToHeading = useCallback((heading: DomHeading) => {
|
||||
heading.element.scrollIntoView();
|
||||
}, []);
|
||||
|
||||
return <AbstractTableOfContents
|
||||
headings={headings}
|
||||
scrollToHeading={scrollToHeading}
|
||||
/>;
|
||||
}
|
||||
|
||||
function extractTocFromStaticHtml(el: HTMLElement | null) {
|
||||
if (!el) return [];
|
||||
|
||||
const headings: DomHeading[] = [];
|
||||
for (const headingEl of el.querySelectorAll<HTMLHeadingElement>("h1,h2,h3,h4,h5,h6")) {
|
||||
headings.push({
|
||||
id: crypto.randomUUID(),
|
||||
level: parseInt(headingEl.tagName.substring(1), 10),
|
||||
text: headingEl.textContent,
|
||||
element: headingEl
|
||||
});
|
||||
}
|
||||
|
||||
return headings;
|
||||
}
|
||||
//#endregion
|
||||
@ -1,10 +1,12 @@
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
import { RawHtmlBlock } from "../react/RawHtml";
|
||||
import renderDoc from "../../services/doc_renderer";
|
||||
import "./Doc.css";
|
||||
import { TypeWidgetProps } from "./type_widget";
|
||||
|
||||
import { useEffect, useRef } from "preact/hooks";
|
||||
|
||||
import appContext from "../../components/app_context";
|
||||
import renderDoc from "../../services/doc_renderer";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import { refToJQuerySelector } from "../react/react_utils";
|
||||
import { TypeWidgetProps } from "./type_widget";
|
||||
|
||||
export default function Doc({ note, viewScope, ntxId }: TypeWidgetProps) {
|
||||
const initialized = useRef<Promise<void> | null>(null);
|
||||
@ -14,9 +16,11 @@ export default function Doc({ note, viewScope, ntxId }: TypeWidgetProps) {
|
||||
if (!note) return;
|
||||
|
||||
initialized.current = renderDoc(note).then($content => {
|
||||
containerRef.current?.replaceChildren(...$content);
|
||||
if (!containerRef.current) return;
|
||||
containerRef.current.replaceChildren(...$content);
|
||||
appContext.triggerEvent("contentElRefreshed", { ntxId, contentEl: containerRef.current });
|
||||
});
|
||||
}, [ note ]);
|
||||
}, [ note, ntxId ]);
|
||||
|
||||
useTriliumEvent("executeWithContentElement", async ({ resolve, ntxId: eventNtxId}) => {
|
||||
if (eventNtxId !== ntxId) return;
|
||||
|
||||
@ -1,24 +1,28 @@
|
||||
import { useEffect, useMemo, useState } from "preact/hooks";
|
||||
import { t } from "../../../services/i18n";
|
||||
import FormCheckbox from "../../react/FormCheckbox";
|
||||
import FormRadioGroup from "../../react/FormRadioGroup";
|
||||
import { useTriliumOption, useTriliumOptionBool, useTriliumOptionJson } from "../../react/hooks";
|
||||
import OptionsSection from "./components/OptionsSection";
|
||||
import { formatDateTime, toggleBodyClass } from "../../../services/utils";
|
||||
import FormGroup from "../../react/FormGroup";
|
||||
import Column from "../../react/Column";
|
||||
import { FormSelectGroup, FormSelectWithGroups } from "../../react/FormSelect";
|
||||
import { Themes } from "@triliumnext/highlightjs";
|
||||
import { ensureMimeTypesForHighlighting, loadHighlightingTheme } from "../../../services/syntax_highlight";
|
||||
import { normalizeMimeTypeForCKEditor, type OptionNames } from "@triliumnext/commons";
|
||||
import { getHtml } from "../../react/RawHtml";
|
||||
import { Themes } from "@triliumnext/highlightjs";
|
||||
import type { CSSProperties } from "preact/compat";
|
||||
import { useEffect, useMemo, useState } from "preact/hooks";
|
||||
import { Trans } from "react-i18next";
|
||||
|
||||
import { isExperimentalFeatureEnabled } from "../../../services/experimental_features";
|
||||
import { t } from "../../../services/i18n";
|
||||
import { ensureMimeTypesForHighlighting, loadHighlightingTheme } from "../../../services/syntax_highlight";
|
||||
import { formatDateTime, toggleBodyClass } from "../../../services/utils";
|
||||
import Column from "../../react/Column";
|
||||
import FormCheckbox from "../../react/FormCheckbox";
|
||||
import FormGroup from "../../react/FormGroup";
|
||||
import FormRadioGroup from "../../react/FormRadioGroup";
|
||||
import { FormSelectGroup, FormSelectWithGroups } from "../../react/FormSelect";
|
||||
import FormText from "../../react/FormText";
|
||||
import FormTextBox, { FormTextBoxWithUnit } from "../../react/FormTextBox";
|
||||
import CheckboxList from "./components/CheckboxList";
|
||||
import { useTriliumOption, useTriliumOptionBool, useTriliumOptionJson } from "../../react/hooks";
|
||||
import KeyboardShortcut from "../../react/KeyboardShortcut";
|
||||
import { Trans } from "react-i18next";
|
||||
import { getHtml } from "../../react/RawHtml";
|
||||
import AutoReadOnlySize from "./components/AutoReadOnlySize";
|
||||
import CheckboxList from "./components/CheckboxList";
|
||||
import OptionsSection from "./components/OptionsSection";
|
||||
|
||||
const isNewLayout = isExperimentalFeatureEnabled("new-layout");
|
||||
|
||||
export default function TextNoteSettings() {
|
||||
return (
|
||||
@ -32,7 +36,7 @@ export default function TextNoteSettings() {
|
||||
<AutoReadOnlySize option="autoReadonlySizeText" label={t("text_auto_read_only_size.label")} />
|
||||
<DateTimeFormatOptions />
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function FormattingToolbar() {
|
||||
@ -65,7 +69,7 @@ function FormattingToolbar() {
|
||||
containerStyle={{ marginInlineStart: "1em" }}
|
||||
/>
|
||||
</OptionsSection>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function EditorFeatures() {
|
||||
@ -119,7 +123,7 @@ function CodeBlockStyle() {
|
||||
|
||||
for (const [ id, theme ] of Object.entries(Themes)) {
|
||||
const data: ThemeData = {
|
||||
val: "default:" + id,
|
||||
val: `default:${ id}`,
|
||||
title: theme.name
|
||||
};
|
||||
|
||||
@ -177,7 +181,7 @@ function CodeBlockStyle() {
|
||||
|
||||
<CodeBlockPreview theme={codeBlockTheme} wordWrap={codeBlockWordWrap} />
|
||||
</OptionsSection>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const SAMPLE_LANGUAGE = normalizeMimeTypeForCKEditor("application/javascript;env=frontend");
|
||||
@ -219,9 +223,9 @@ function CodeBlockPreview({ theme, wordWrap }: { theme: string, wordWrap: boolea
|
||||
const codeStyle = useMemo<CSSProperties>(() => {
|
||||
if (wordWrap) {
|
||||
return { whiteSpace: "pre-wrap" };
|
||||
} else {
|
||||
return { whiteSpace: "pre"};
|
||||
}
|
||||
return { whiteSpace: "pre"};
|
||||
|
||||
}, [ wordWrap ]);
|
||||
|
||||
return (
|
||||
@ -230,7 +234,7 @@ function CodeBlockPreview({ theme, wordWrap }: { theme: string, wordWrap: boolea
|
||||
<code className="code-sample" style={codeStyle} dangerouslySetInnerHTML={getHtml(code)} />
|
||||
</pre>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
interface ThemeData {
|
||||
@ -241,7 +245,7 @@ interface ThemeData {
|
||||
function TableOfContent() {
|
||||
const [ minTocHeadings, setMinTocHeadings ] = useTriliumOption("minTocHeadings");
|
||||
|
||||
return (
|
||||
return (!isNewLayout &&
|
||||
<OptionsSection title={t("table_of_contents.title")}>
|
||||
<FormText>{t("table_of_contents.description")}</FormText>
|
||||
|
||||
@ -257,14 +261,31 @@ function TableOfContent() {
|
||||
<FormText>{t("table_of_contents.disable_info")}</FormText>
|
||||
<FormText>{t("table_of_contents.shortcut_info")}</FormText>
|
||||
</OptionsSection>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function HighlightsList() {
|
||||
return (
|
||||
<OptionsSection title={t("highlights_list.title")}>
|
||||
<HighlightsListOptions />
|
||||
|
||||
{!isNewLayout && (
|
||||
<>
|
||||
<hr />
|
||||
<h5>{t("highlights_list.visibility_title")}</h5>
|
||||
<FormText>{t("highlights_list.visibility_description")}</FormText>
|
||||
<FormText>{t("highlights_list.shortcut_info")}</FormText>
|
||||
</>
|
||||
)}
|
||||
</OptionsSection>
|
||||
);
|
||||
}
|
||||
|
||||
export function HighlightsListOptions() {
|
||||
const [ highlightsList, setHighlightsList ] = useTriliumOptionJson<string[]>("highlightsList");
|
||||
|
||||
return (
|
||||
<OptionsSection title={t("highlights_list.title")}>
|
||||
<>
|
||||
<FormText>{t("highlights_list.description")}</FormText>
|
||||
<CheckboxList
|
||||
values={[
|
||||
@ -277,13 +298,8 @@ function HighlightsList() {
|
||||
keyProperty="val" titleProperty="title"
|
||||
currentValue={highlightsList} onChange={setHighlightsList}
|
||||
/>
|
||||
<hr />
|
||||
|
||||
<h5>{t("highlights_list.visibility_title")}</h5>
|
||||
<FormText>{t("highlights_list.visibility_description")}</FormText>
|
||||
<FormText>{t("highlights_list.shortcut_info")}</FormText>
|
||||
</OptionsSection>
|
||||
)
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function DateTimeFormatOptions() {
|
||||
@ -302,19 +318,19 @@ function DateTimeFormatOptions() {
|
||||
</FormText>
|
||||
|
||||
<div className="row align-items-center">
|
||||
<FormGroup name="custom-date-time-format" className="col-md-6" label={t("custom_date_time_format.format_string")}>
|
||||
<FormTextBox
|
||||
<FormGroup name="custom-date-time-format" className="col-md-6" label={t("custom_date_time_format.format_string")}>
|
||||
<FormTextBox
|
||||
placeholder="YYYY-MM-DD HH:mm"
|
||||
currentValue={customDateTimeFormat || "YYYY-MM-DD HH:mm"} onChange={setCustomDateTimeFormat}
|
||||
/>
|
||||
</FormGroup>
|
||||
/>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup name="formatted-date" className="col-md-6" label={t("custom_date_time_format.formatted_time")}>
|
||||
<div>
|
||||
{formatDateTime(new Date(), customDateTimeFormat)}
|
||||
</div>
|
||||
</FormGroup>
|
||||
<FormGroup name="formatted-date" className="col-md-6" label={t("custom_date_time_format.formatted_time")}>
|
||||
<div>
|
||||
{formatDateTime(new Date(), customDateTimeFormat)}
|
||||
</div>
|
||||
</FormGroup>
|
||||
</div>
|
||||
</OptionsSection>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,23 +1,24 @@
|
||||
import { useEffect, useMemo, useRef } from "preact/hooks";
|
||||
import { TypeWidgetProps } from "../type_widget";
|
||||
import "./ReadOnlyText.css";
|
||||
import { useNoteBlob, useNoteLabel, useTriliumEvent, useTriliumOptionBool } from "../../react/hooks";
|
||||
import { RawHtmlBlock } from "../../react/RawHtml";
|
||||
|
||||
// we load CKEditor also for read only notes because they contain content styles required for correct rendering of even read only notes
|
||||
// we could load just ckeditor-content.css but that causes CSS conflicts when both build CSS and this content CSS is loaded at the same time
|
||||
// (see https://github.com/zadam/trilium/issues/1590 for example of such conflict)
|
||||
import "@triliumnext/ckeditor5";
|
||||
|
||||
import clsx from "clsx";
|
||||
import { useEffect, useMemo, useRef } from "preact/hooks";
|
||||
|
||||
import appContext from "../../../components/app_context";
|
||||
import FNote from "../../../entities/fnote";
|
||||
import { applyInlineMermaid, rewriteMermaidDiagramsInContainer } from "../../../services/content_renderer_text";
|
||||
import { getLocaleById } from "../../../services/i18n";
|
||||
import { loadIncludedNote, refreshIncludedNote, setupImageOpening } from "./utils";
|
||||
import { renderMathInElement } from "../../../services/math";
|
||||
import { formatCodeBlocks } from "../../../services/syntax_highlight";
|
||||
import { useNoteBlob, useNoteLabel, useTriliumEvent, useTriliumOptionBool } from "../../react/hooks";
|
||||
import { RawHtmlBlock } from "../../react/RawHtml";
|
||||
import TouchBar, { TouchBarButton, TouchBarSpacer } from "../../react/TouchBar";
|
||||
import appContext from "../../../components/app_context";
|
||||
import { TypeWidgetProps } from "../type_widget";
|
||||
import { applyReferenceLinks } from "./read_only_helper";
|
||||
import { applyInlineMermaid, rewriteMermaidDiagramsInContainer } from "../../../services/content_renderer_text";
|
||||
import clsx from "clsx";
|
||||
import { loadIncludedNote, refreshIncludedNote, setupImageOpening } from "./utils";
|
||||
|
||||
export default function ReadOnlyText({ note, noteContext, ntxId }: TypeWidgetProps) {
|
||||
const blob = useNoteBlob(note);
|
||||
@ -30,6 +31,8 @@ export default function ReadOnlyText({ note, noteContext, ntxId }: TypeWidgetPro
|
||||
const container = contentRef.current;
|
||||
if (!container) return;
|
||||
|
||||
appContext.triggerEvent("contentElRefreshed", { ntxId, contentEl: container });
|
||||
|
||||
rewriteMermaidDiagramsInContainer(container);
|
||||
applyInlineMermaid(container);
|
||||
applyIncludedNotes(container);
|
||||
@ -74,7 +77,7 @@ export default function ReadOnlyText({ note, noteContext, ntxId }: TypeWidgetPro
|
||||
/>
|
||||
</TouchBar>
|
||||
</>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function useNoteLanguage(note: FNote) {
|
||||
|
||||
@ -51,8 +51,9 @@ const ALLOWED_OPTIONS = new Set<OptionNames>([
|
||||
"imageMaxWidthHeight",
|
||||
"imageJpegQuality",
|
||||
"leftPaneWidth",
|
||||
"rightPaneWidth",
|
||||
"leftPaneVisible",
|
||||
"rightPaneWidth",
|
||||
"rightPaneCollapsedItems",
|
||||
"rightPaneVisible",
|
||||
"nativeTitleBarVisible",
|
||||
"headingStyle",
|
||||
|
||||
@ -105,6 +105,7 @@ const defaultOptions: DefaultOption[] = [
|
||||
{ name: "leftPaneVisible", value: "true", isSynced: false },
|
||||
{ name: "rightPaneWidth", value: "25", isSynced: false },
|
||||
{ name: "rightPaneVisible", value: "true", isSynced: false },
|
||||
{ name: "rightPaneCollapsedItems", value: "[]", isSynced: false },
|
||||
{ name: "nativeTitleBarVisible", value: "false", isSynced: false },
|
||||
{ name: "eraseEntitiesAfterTimeInSeconds", value: "604800", isSynced: true }, // default is 7 days
|
||||
{ name: "eraseEntitiesAfterTimeScale", value: "86400", isSynced: true }, // default 86400 seconds = Day
|
||||
|
||||
@ -6,7 +6,7 @@ import { BalloonEditor, DecoupledEditor, FindAndReplaceEditing, FindCommand } fr
|
||||
import "./translation_overrides.js";
|
||||
export { default as EditorWatchdog } from "./custom_watchdog";
|
||||
export { PREMIUM_PLUGINS } from "./plugins.js";
|
||||
export type { EditorConfig, MentionFeed, MentionFeedObjectItem, ModelNode, ModelPosition, ModelElement, WatchdogConfig, WatchdogState } from "ckeditor5";
|
||||
export type { EditorConfig, MentionFeed, MentionFeedObjectItem, ModelNode, ModelPosition, ModelElement, ModelText, WatchdogConfig, WatchdogState } from "ckeditor5";
|
||||
export type { TemplateDefinition } from "ckeditor5-premium-features";
|
||||
export { default as buildExtraCommands } from "./extra_slash_commands.js";
|
||||
export { default as getCkLocale } from "./i18n.js";
|
||||
|
||||
@ -77,6 +77,7 @@ export interface OptionDefinitions extends KeyboardShortcutsOptions<KeyboardActi
|
||||
imageJpegQuality: number;
|
||||
leftPaneWidth: number;
|
||||
rightPaneWidth: number;
|
||||
rightPaneCollapsedItems: string;
|
||||
eraseEntitiesAfterTimeInSeconds: number;
|
||||
eraseEntitiesAfterTimeScale: number;
|
||||
autoReadonlySizeText: number;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user