mirror of
https://github.com/zadam/trilium.git
synced 2025-11-02 11:26:15 +01:00
Port small widgets to React (#6830)
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import froca from "../services/froca.js";
|
import froca from "../services/froca.js";
|
||||||
import RootCommandExecutor from "./root_command_executor.js";
|
import RootCommandExecutor from "./root_command_executor.js";
|
||||||
import Entrypoints, { type SqlExecuteResults } from "./entrypoints.js";
|
import Entrypoints from "./entrypoints.js";
|
||||||
import options from "../services/options.js";
|
import options from "../services/options.js";
|
||||||
import utils, { hasTouchBar } from "../services/utils.js";
|
import utils, { hasTouchBar } from "../services/utils.js";
|
||||||
import zoomComponent from "./zoom.js";
|
import zoomComponent from "./zoom.js";
|
||||||
@@ -32,6 +32,7 @@ import type { CreateNoteOpts } from "../services/note_create.js";
|
|||||||
import { ColumnComponent } from "tabulator-tables";
|
import { ColumnComponent } from "tabulator-tables";
|
||||||
import { ChooseNoteTypeCallback } from "../widgets/dialogs/note_type_chooser.jsx";
|
import { ChooseNoteTypeCallback } from "../widgets/dialogs/note_type_chooser.jsx";
|
||||||
import type RootContainer from "../widgets/containers/root_container.js";
|
import type RootContainer from "../widgets/containers/root_container.js";
|
||||||
|
import { SqlExecuteResults } from "@triliumnext/commons";
|
||||||
|
|
||||||
interface Layout {
|
interface Layout {
|
||||||
getRootWidget: (appContext: AppContext) => RootContainer;
|
getRootWidget: (appContext: AppContext) => RootContainer;
|
||||||
@@ -89,6 +90,11 @@ export type CommandMappings = {
|
|||||||
closeTocCommand: CommandData;
|
closeTocCommand: CommandData;
|
||||||
closeHlt: CommandData;
|
closeHlt: CommandData;
|
||||||
showLaunchBarSubtree: CommandData;
|
showLaunchBarSubtree: CommandData;
|
||||||
|
showHiddenSubtree: CommandData;
|
||||||
|
showSQLConsoleHistory: CommandData;
|
||||||
|
logout: CommandData;
|
||||||
|
switchToMobileVersion: CommandData;
|
||||||
|
switchToDesktopVersion: CommandData;
|
||||||
showRevisions: CommandData & {
|
showRevisions: CommandData & {
|
||||||
noteId?: string | null;
|
noteId?: string | null;
|
||||||
};
|
};
|
||||||
@@ -134,6 +140,7 @@ export type CommandMappings = {
|
|||||||
showLeftPane: CommandData;
|
showLeftPane: CommandData;
|
||||||
showAttachments: CommandData;
|
showAttachments: CommandData;
|
||||||
showSearchHistory: CommandData;
|
showSearchHistory: CommandData;
|
||||||
|
showShareSubtree: CommandData;
|
||||||
hoistNote: CommandData & { noteId: string };
|
hoistNote: CommandData & { noteId: string };
|
||||||
leaveProtectedSession: CommandData;
|
leaveProtectedSession: CommandData;
|
||||||
enterProtectedSession: CommandData;
|
enterProtectedSession: CommandData;
|
||||||
|
|||||||
@@ -10,22 +10,7 @@ import bundleService from "../services/bundle.js";
|
|||||||
import froca from "../services/froca.js";
|
import froca from "../services/froca.js";
|
||||||
import linkService from "../services/link.js";
|
import linkService from "../services/link.js";
|
||||||
import { t } from "../services/i18n.js";
|
import { t } from "../services/i18n.js";
|
||||||
import type FNote from "../entities/fnote.js";
|
import { CreateChildrenResponse, SqlExecuteResponse } from "@triliumnext/commons";
|
||||||
|
|
||||||
// TODO: Move somewhere else nicer.
|
|
||||||
export type SqlExecuteResults = string[][][];
|
|
||||||
|
|
||||||
// TODO: Deduplicate with server.
|
|
||||||
interface SqlExecuteResponse {
|
|
||||||
success: boolean;
|
|
||||||
error?: string;
|
|
||||||
results: SqlExecuteResults;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: Deduplicate with server.
|
|
||||||
interface CreateChildrenResponse {
|
|
||||||
note: FNote;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class Entrypoints extends Component {
|
export default class Entrypoints extends Component {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -34,7 +19,7 @@ export default class Entrypoints extends Component {
|
|||||||
|
|
||||||
openDevToolsCommand() {
|
openDevToolsCommand() {
|
||||||
if (utils.isElectron()) {
|
if (utils.isElectron()) {
|
||||||
utils.dynamicRequire("@electron/remote").getCurrentWindow().toggleDevTools();
|
utils.dynamicRequire("@electron/remote").getCurrentWindow().webContents.toggleDevTools();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +109,7 @@ export default class Entrypoints extends Component {
|
|||||||
if (utils.isElectron()) {
|
if (utils.isElectron()) {
|
||||||
// standard JS version does not work completely correctly in electron
|
// standard JS version does not work completely correctly in electron
|
||||||
const webContents = utils.dynamicRequire("@electron/remote").getCurrentWebContents();
|
const webContents = utils.dynamicRequire("@electron/remote").getCurrentWebContents();
|
||||||
const activeIndex = parseInt(webContents.navigationHistory.getActiveIndex());
|
const activeIndex = webContents.navigationHistory.getActiveIndex();
|
||||||
|
|
||||||
webContents.goToIndex(activeIndex - 1);
|
webContents.goToIndex(activeIndex - 1);
|
||||||
} else {
|
} else {
|
||||||
@@ -136,7 +121,7 @@ export default class Entrypoints extends Component {
|
|||||||
if (utils.isElectron()) {
|
if (utils.isElectron()) {
|
||||||
// standard JS version does not work completely correctly in electron
|
// standard JS version does not work completely correctly in electron
|
||||||
const webContents = utils.dynamicRequire("@electron/remote").getCurrentWebContents();
|
const webContents = utils.dynamicRequire("@electron/remote").getCurrentWebContents();
|
||||||
const activeIndex = parseInt(webContents.navigationHistory.getActiveIndex());
|
const activeIndex = webContents.navigationHistory.getActiveIndex();
|
||||||
|
|
||||||
webContents.goToIndex(activeIndex + 1);
|
webContents.goToIndex(activeIndex + 1);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,24 +1,18 @@
|
|||||||
import FlexContainer from "../widgets/containers/flex_container.js";
|
import FlexContainer from "../widgets/containers/flex_container.js";
|
||||||
import GlobalMenuWidget from "../widgets/buttons/global_menu.js";
|
|
||||||
import TabRowWidget from "../widgets/tab_row.js";
|
import TabRowWidget from "../widgets/tab_row.js";
|
||||||
import TitleBarButtonsWidget from "../widgets/title_bar_buttons.js";
|
|
||||||
import LeftPaneContainer from "../widgets/containers/left_pane_container.js";
|
import LeftPaneContainer from "../widgets/containers/left_pane_container.js";
|
||||||
import NoteTreeWidget from "../widgets/note_tree.js";
|
import NoteTreeWidget from "../widgets/note_tree.js";
|
||||||
import NoteTitleWidget from "../widgets/note_title.jsx";
|
import NoteTitleWidget from "../widgets/note_title.jsx";
|
||||||
import NoteDetailWidget from "../widgets/note_detail.js";
|
import NoteDetailWidget from "../widgets/note_detail.js";
|
||||||
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
|
import PromotedAttributesWidget from "../widgets/promoted_attributes.js";
|
||||||
import NoteListWidget from "../widgets/note_list.js";
|
import NoteListWidget from "../widgets/note_list.js";
|
||||||
import SqlResultWidget from "../widgets/sql_result.js";
|
|
||||||
import SqlTableSchemasWidget from "../widgets/sql_table_schemas.js";
|
|
||||||
import NoteIconWidget from "../widgets/note_icon.jsx";
|
import NoteIconWidget from "../widgets/note_icon.jsx";
|
||||||
import SearchResultWidget from "../widgets/search_result.js";
|
|
||||||
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
||||||
import RootContainer from "../widgets/containers/root_container.js";
|
import RootContainer from "../widgets/containers/root_container.js";
|
||||||
import WatchedFileUpdateStatusWidget from "../widgets/watched_file_update_status.js";
|
import WatchedFileUpdateStatusWidget from "../widgets/watched_file_update_status.js";
|
||||||
import SpacerWidget from "../widgets/spacer.js";
|
import SpacerWidget from "../widgets/spacer.js";
|
||||||
import QuickSearchWidget from "../widgets/quick_search.js";
|
import QuickSearchWidget from "../widgets/quick_search.js";
|
||||||
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
|
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
|
||||||
import LeftPaneToggleWidget from "../widgets/buttons/left_pane_toggle.js";
|
|
||||||
import CreatePaneButton from "../widgets/buttons/create_pane_button.js";
|
import CreatePaneButton from "../widgets/buttons/create_pane_button.js";
|
||||||
import ClosePaneButton from "../widgets/buttons/close_pane_button.js";
|
import ClosePaneButton from "../widgets/buttons/close_pane_button.js";
|
||||||
import RightPaneContainer from "../widgets/containers/right_pane_container.js";
|
import RightPaneContainer from "../widgets/containers/right_pane_container.js";
|
||||||
@@ -29,19 +23,25 @@ import TocWidget from "../widgets/toc.js";
|
|||||||
import HighlightsListWidget from "../widgets/highlights_list.js";
|
import HighlightsListWidget from "../widgets/highlights_list.js";
|
||||||
import PasswordNoteSetDialog from "../widgets/dialogs/password_not_set.js";
|
import PasswordNoteSetDialog from "../widgets/dialogs/password_not_set.js";
|
||||||
import LauncherContainer from "../widgets/containers/launcher_container.js";
|
import LauncherContainer from "../widgets/containers/launcher_container.js";
|
||||||
import ApiLogWidget from "../widgets/api_log.js";
|
|
||||||
import MovePaneButton from "../widgets/buttons/move_pane_button.js";
|
import MovePaneButton from "../widgets/buttons/move_pane_button.js";
|
||||||
import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
|
import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
|
||||||
import ScrollPaddingWidget from "../widgets/scroll_padding.js";
|
import ScrollPadding from "../widgets/scroll_padding.js";
|
||||||
import options from "../services/options.js";
|
import options from "../services/options.js";
|
||||||
import utils from "../services/utils.js";
|
import utils from "../services/utils.js";
|
||||||
import CloseZenButton from "../widgets/close_zen_button.js";
|
|
||||||
import type { AppContext } from "../components/app_context.js";
|
import type { AppContext } from "../components/app_context.js";
|
||||||
import type { WidgetsByParent } from "../services/bundle.js";
|
import type { WidgetsByParent } from "../services/bundle.js";
|
||||||
import { applyModals } from "./layout_commons.js";
|
import { applyModals } from "./layout_commons.js";
|
||||||
import Ribbon from "../widgets/ribbon/Ribbon.jsx";
|
import Ribbon from "../widgets/ribbon/Ribbon.jsx";
|
||||||
import FloatingButtons from "../widgets/FloatingButtons.jsx";
|
import FloatingButtons from "../widgets/FloatingButtons.jsx";
|
||||||
import { DESKTOP_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
|
import { DESKTOP_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
|
||||||
|
import SearchResult from "../widgets/search_result.jsx";
|
||||||
|
import GlobalMenu from "../widgets/buttons/global_menu.jsx";
|
||||||
|
import SqlResults from "../widgets/sql_result.js";
|
||||||
|
import SqlTableSchemas from "../widgets/sql_table_schemas.js";
|
||||||
|
import TitleBarButtons from "../widgets/title_bar_buttons.jsx";
|
||||||
|
import LeftPaneToggle from "../widgets/buttons/left_pane_toggle.js";
|
||||||
|
import ApiLog from "../widgets/api_log.jsx";
|
||||||
|
import CloseZenModeButton from "../widgets/close_zen_button.jsx";
|
||||||
|
|
||||||
export default class DesktopLayout {
|
export default class DesktopLayout {
|
||||||
|
|
||||||
@@ -76,9 +76,9 @@ export default class DesktopLayout {
|
|||||||
new FlexContainer("row")
|
new FlexContainer("row")
|
||||||
.class("tab-row-container")
|
.class("tab-row-container")
|
||||||
.child(new FlexContainer("row").id("tab-row-left-spacer"))
|
.child(new FlexContainer("row").id("tab-row-left-spacer"))
|
||||||
.optChild(launcherPaneIsHorizontal, new LeftPaneToggleWidget(true))
|
.optChild(launcherPaneIsHorizontal, <LeftPaneToggle isHorizontalLayout={true} />)
|
||||||
.child(new TabRowWidget().class("full-width"))
|
.child(new TabRowWidget().class("full-width"))
|
||||||
.optChild(customTitleBarButtons, new TitleBarButtonsWidget())
|
.optChild(customTitleBarButtons, <TitleBarButtons />)
|
||||||
.css("height", "40px")
|
.css("height", "40px")
|
||||||
.css("background-color", "var(--launcher-pane-background-color)")
|
.css("background-color", "var(--launcher-pane-background-color)")
|
||||||
.setParent(appContext)
|
.setParent(appContext)
|
||||||
@@ -99,7 +99,7 @@ export default class DesktopLayout {
|
|||||||
new FlexContainer("column")
|
new FlexContainer("column")
|
||||||
.id("rest-pane")
|
.id("rest-pane")
|
||||||
.css("flex-grow", "1")
|
.css("flex-grow", "1")
|
||||||
.optChild(!fullWidthTabBar, new FlexContainer("row").child(new TabRowWidget()).optChild(customTitleBarButtons, new TitleBarButtonsWidget()).css("height", "40px"))
|
.optChild(!fullWidthTabBar, new FlexContainer("row").child(new TabRowWidget()).optChild(customTitleBarButtons, <TitleBarButtons />).css("height", "40px"))
|
||||||
.child(
|
.child(
|
||||||
new FlexContainer("row")
|
new FlexContainer("row")
|
||||||
.filling()
|
.filling()
|
||||||
@@ -136,14 +136,14 @@ export default class DesktopLayout {
|
|||||||
new ScrollingContainer()
|
new ScrollingContainer()
|
||||||
.filling()
|
.filling()
|
||||||
.child(new PromotedAttributesWidget())
|
.child(new PromotedAttributesWidget())
|
||||||
.child(new SqlTableSchemasWidget())
|
.child(<SqlTableSchemas />)
|
||||||
.child(new NoteDetailWidget())
|
.child(new NoteDetailWidget())
|
||||||
.child(new NoteListWidget(false))
|
.child(new NoteListWidget(false))
|
||||||
.child(new SearchResultWidget())
|
.child(<SearchResult />)
|
||||||
.child(new SqlResultWidget())
|
.child(<SqlResults />)
|
||||||
.child(new ScrollPaddingWidget())
|
.child(<ScrollPadding />)
|
||||||
)
|
)
|
||||||
.child(new ApiLogWidget())
|
.child(<ApiLog />)
|
||||||
.child(new FindWidget())
|
.child(new FindWidget())
|
||||||
.child(
|
.child(
|
||||||
...this.customWidgets.get("node-detail-pane"), // typo, let's keep it for a while as BC
|
...this.customWidgets.get("node-detail-pane"), // typo, let's keep it for a while as BC
|
||||||
@@ -162,7 +162,7 @@ export default class DesktopLayout {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.child(new CloseZenButton())
|
.child(<CloseZenModeButton />)
|
||||||
|
|
||||||
// Desktop-specific dialogs.
|
// Desktop-specific dialogs.
|
||||||
.child(<PasswordNoteSetDialog />)
|
.child(<PasswordNoteSetDialog />)
|
||||||
@@ -176,14 +176,18 @@ export default class DesktopLayout {
|
|||||||
let launcherPane;
|
let launcherPane;
|
||||||
|
|
||||||
if (isHorizontal) {
|
if (isHorizontal) {
|
||||||
launcherPane = new FlexContainer("row").css("height", "53px").class("horizontal").child(new LauncherContainer(true)).child(new GlobalMenuWidget(true));
|
launcherPane = new FlexContainer("row")
|
||||||
|
.css("height", "53px")
|
||||||
|
.class("horizontal")
|
||||||
|
.child(new LauncherContainer(true))
|
||||||
|
.child(<GlobalMenu isHorizontalLayout={true} />);
|
||||||
} else {
|
} else {
|
||||||
launcherPane = new FlexContainer("column")
|
launcherPane = new FlexContainer("column")
|
||||||
.css("width", "53px")
|
.css("width", "53px")
|
||||||
.class("vertical")
|
.class("vertical")
|
||||||
.child(new GlobalMenuWidget(false))
|
.child(<GlobalMenu isHorizontalLayout={false} />)
|
||||||
.child(new LauncherContainer(false))
|
.child(new LauncherContainer(false))
|
||||||
.child(new LeftPaneToggleWidget(false));
|
.child(<LeftPaneToggle isHorizontalLayout={false} />);
|
||||||
}
|
}
|
||||||
|
|
||||||
launcherPane.id("launcher-pane");
|
launcherPane.id("launcher-pane");
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import NoteTitleWidget from "../widgets/note_title.js";
|
|||||||
import NoteDetailWidget from "../widgets/note_detail.js";
|
import NoteDetailWidget from "../widgets/note_detail.js";
|
||||||
import QuickSearchWidget from "../widgets/quick_search.js";
|
import QuickSearchWidget from "../widgets/quick_search.js";
|
||||||
import NoteTreeWidget from "../widgets/note_tree.js";
|
import NoteTreeWidget from "../widgets/note_tree.js";
|
||||||
import ToggleSidebarButtonWidget from "../widgets/mobile_widgets/toggle_sidebar_button.js";
|
|
||||||
import MobileDetailMenuWidget from "../widgets/mobile_widgets/mobile_detail_menu.js";
|
|
||||||
import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
|
import ScreenContainer from "../widgets/mobile_widgets/screen_container.js";
|
||||||
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
||||||
import NoteListWidget from "../widgets/note_list.js";
|
import NoteListWidget from "../widgets/note_list.js";
|
||||||
@@ -18,11 +16,13 @@ import type AppContext from "../components/app_context.js";
|
|||||||
import TabRowWidget from "../widgets/tab_row.js";
|
import TabRowWidget from "../widgets/tab_row.js";
|
||||||
import MobileEditorToolbar from "../widgets/type_widgets/ckeditor/mobile_editor_toolbar.js";
|
import MobileEditorToolbar from "../widgets/type_widgets/ckeditor/mobile_editor_toolbar.js";
|
||||||
import { applyModals } from "./layout_commons.js";
|
import { applyModals } from "./layout_commons.js";
|
||||||
import CloseZenButton from "../widgets/close_zen_button.js";
|
|
||||||
import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx";
|
import FilePropertiesTab from "../widgets/ribbon/FilePropertiesTab.jsx";
|
||||||
import { useNoteContext } from "../widgets/react/hooks.jsx";
|
import { useNoteContext } from "../widgets/react/hooks.jsx";
|
||||||
import FloatingButtons from "../widgets/FloatingButtons.jsx";
|
import FloatingButtons from "../widgets/FloatingButtons.jsx";
|
||||||
import { MOBILE_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
|
import { MOBILE_FLOATING_BUTTONS } from "../widgets/FloatingButtonsDefinitions.jsx";
|
||||||
|
import ToggleSidebarButton from "../widgets/mobile_widgets/toggle_sidebar_button.jsx";
|
||||||
|
import CloseZenModeButton from "../widgets/close_zen_button.js";
|
||||||
|
import MobileDetailMenu from "../widgets/mobile_widgets/mobile_detail_menu.js";
|
||||||
|
|
||||||
const MOBILE_CSS = `
|
const MOBILE_CSS = `
|
||||||
<style>
|
<style>
|
||||||
@@ -139,9 +139,9 @@ export default class MobileLayout {
|
|||||||
.contentSized()
|
.contentSized()
|
||||||
.css("font-size", "larger")
|
.css("font-size", "larger")
|
||||||
.css("align-items", "center")
|
.css("align-items", "center")
|
||||||
.child(new ToggleSidebarButtonWidget().contentSized())
|
.child(<ToggleSidebarButton />)
|
||||||
.child(<NoteTitleWidget />)
|
.child(<NoteTitleWidget />)
|
||||||
.child(new MobileDetailMenuWidget(true).contentSized())
|
.child(<MobileDetailMenu />)
|
||||||
)
|
)
|
||||||
.child(new SharedInfoWidget())
|
.child(new SharedInfoWidget())
|
||||||
.child(<FloatingButtons items={MOBILE_FLOATING_BUTTONS} />)
|
.child(<FloatingButtons items={MOBILE_FLOATING_BUTTONS} />)
|
||||||
@@ -154,7 +154,7 @@ export default class MobileLayout {
|
|||||||
.child(new NoteListWidget(false))
|
.child(new NoteListWidget(false))
|
||||||
.child(<FilePropertiesWrapper />)
|
.child(<FilePropertiesWrapper />)
|
||||||
)
|
)
|
||||||
.child(new MobileEditorToolbar())
|
.child(<MobileEditorToolbar />)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
@@ -162,9 +162,14 @@ export default class MobileLayout {
|
|||||||
.contentSized()
|
.contentSized()
|
||||||
.id("mobile-bottom-bar")
|
.id("mobile-bottom-bar")
|
||||||
.child(new TabRowWidget().css("height", "40px"))
|
.child(new TabRowWidget().css("height", "40px"))
|
||||||
.child(new FlexContainer("row").class("horizontal").css("height", "53px").child(new LauncherContainer(true)).child(new GlobalMenuWidget(true)).id("launcher-pane"))
|
.child(new FlexContainer("row")
|
||||||
|
.class("horizontal")
|
||||||
|
.css("height", "53px")
|
||||||
|
.child(new LauncherContainer(true))
|
||||||
|
.child(<GlobalMenuWidget isHorizontalLayout />)
|
||||||
|
.id("launcher-pane"))
|
||||||
)
|
)
|
||||||
.child(new CloseZenButton());
|
.child(<CloseZenModeButton />);
|
||||||
applyModals(rootContainer);
|
applyModals(rootContainer);
|
||||||
return rootContainer;
|
return rootContainer;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,6 +62,10 @@ async function getAction(actionName: string, silent = false) {
|
|||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getActionSync(actionName: string) {
|
||||||
|
return keyboardActionRepo[actionName];
|
||||||
|
}
|
||||||
|
|
||||||
function updateDisplayedShortcuts($container: JQuery<HTMLElement>) {
|
function updateDisplayedShortcuts($container: JQuery<HTMLElement>) {
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
//TODO: each() does not support async callbacks.
|
//TODO: each() does not support async callbacks.
|
||||||
|
|||||||
@@ -218,7 +218,7 @@ function ajax(url: string, method: string, data: unknown, headers: Headers, sile
|
|||||||
if (utils.isElectron()) {
|
if (utils.isElectron()) {
|
||||||
const ipc = utils.dynamicRequire("electron").ipcRenderer;
|
const ipc = utils.dynamicRequire("electron").ipcRenderer;
|
||||||
|
|
||||||
ipc.on("server-response", async (event: string, arg: Arg) => {
|
ipc.on("server-response", async (_, arg: Arg) => {
|
||||||
if (arg.statusCode >= 200 && arg.statusCode < 300) {
|
if (arg.statusCode >= 200 && arg.statusCode < 300) {
|
||||||
handleSuccessfulResponse(arg);
|
handleSuccessfulResponse(arg);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -311,7 +311,13 @@ function copySelectionToClipboard() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function dynamicRequire(moduleName: string) {
|
type dynamicRequireMappings = {
|
||||||
|
"@electron/remote": typeof import("@electron/remote"),
|
||||||
|
"electron": typeof import("electron"),
|
||||||
|
"child_process": typeof import("child_process")
|
||||||
|
};
|
||||||
|
|
||||||
|
export function dynamicRequire<T extends keyof dynamicRequireMappings>(moduleName: T): Awaited<dynamicRequireMappings[T]>{
|
||||||
if (typeof __non_webpack_require__ !== "undefined") {
|
if (typeof __non_webpack_require__ !== "undefined") {
|
||||||
return __non_webpack_require__(moduleName);
|
return __non_webpack_require__(moduleName);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -442,14 +442,20 @@ body #context-menu-container .dropdown-item > span {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-menu kbd {
|
.dropdown-item span.keyboard-shortcut {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-menu kbd {
|
||||||
color: var(--muted-text-color);
|
color: var(--muted-text-color);
|
||||||
border: none;
|
border: none;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
|
padding: 0;
|
||||||
|
flex-grow: 1;
|
||||||
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropdown-item,
|
.dropdown-item,
|
||||||
|
|||||||
@@ -197,13 +197,17 @@ html body .dropdown-item[disabled] {
|
|||||||
|
|
||||||
/* Menu item keyboard shortcut */
|
/* Menu item keyboard shortcut */
|
||||||
.dropdown-item kbd {
|
.dropdown-item kbd {
|
||||||
margin-left: 16px;
|
|
||||||
font-family: unset !important;
|
font-family: unset !important;
|
||||||
font-size: unset !important;
|
font-size: unset !important;
|
||||||
color: var(--menu-item-keyboard-shortcut-color) !important;
|
color: var(--menu-item-keyboard-shortcut-color) !important;
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dropdown-item span.keyboard-shortcut {
|
||||||
|
color: var(--menu-item-keyboard-shortcut-color) !important;
|
||||||
|
margin-left: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.dropdown-divider {
|
.dropdown-divider {
|
||||||
position: relative;
|
position: relative;
|
||||||
border-color: transparent !important;
|
border-color: transparent !important;
|
||||||
|
|||||||
@@ -96,7 +96,6 @@
|
|||||||
background: var(--background) !important;
|
background: var(--background) !important;
|
||||||
color: var(--color) !important;
|
color: var(--color) !important;
|
||||||
line-height: unset;
|
line-height: unset;
|
||||||
cursor: help;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sql-table-schemas-widget .sql-table-schemas button:hover,
|
.sql-table-schemas-widget .sql-table-schemas button:hover,
|
||||||
@@ -106,18 +105,6 @@
|
|||||||
--color: var(--main-text-color);
|
--color: var(--main-text-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Tooltip */
|
|
||||||
|
|
||||||
.tooltip .table-schema {
|
|
||||||
font-family: var(--monospace-font-family);
|
|
||||||
font-size: .85em;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Data type */
|
|
||||||
.tooltip .table-schema td:nth-child(2) {
|
|
||||||
color: var(--muted-text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* NOTE MAP
|
* NOTE MAP
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1682,7 +1682,8 @@
|
|||||||
"hoist-this-note-workspace": "Hoist this note (workspace)",
|
"hoist-this-note-workspace": "Hoist this note (workspace)",
|
||||||
"refresh-saved-search-results": "Refresh saved search results",
|
"refresh-saved-search-results": "Refresh saved search results",
|
||||||
"create-child-note": "Create child note",
|
"create-child-note": "Create child note",
|
||||||
"unhoist": "Unhoist"
|
"unhoist": "Unhoist",
|
||||||
|
"toggle-sidebar": "Toggle sidebar"
|
||||||
},
|
},
|
||||||
"title_bar_buttons": {
|
"title_bar_buttons": {
|
||||||
"window-on-top": "Keep Window on Top"
|
"window-on-top": "Keep Window on Top"
|
||||||
|
|||||||
28
apps/client/src/widgets/api_log.css
Normal file
28
apps/client/src/widgets/api_log.css
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
.api-log-widget {
|
||||||
|
flex-grow: 1;
|
||||||
|
max-height: 40%;
|
||||||
|
position: relative;
|
||||||
|
border-top: 1px solid var(--main-border-color);
|
||||||
|
background-color: var(--accented-background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-log-container {
|
||||||
|
overflow: auto;
|
||||||
|
height: 100%;
|
||||||
|
font-family: var(--monospace-font-family);
|
||||||
|
font-size: 0.8em;
|
||||||
|
white-space: pre;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-api-log-button {
|
||||||
|
padding: 5px;
|
||||||
|
border: 1px solid var(--button-border-color);
|
||||||
|
background-color: var(--button-background-color);
|
||||||
|
border-radius: var(--button-border-radius);
|
||||||
|
color: var(--button-text-color);
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
import type { EventData } from "../components/app_context.js";
|
|
||||||
import type FNote from "../entities/fnote.js";
|
|
||||||
import { t } from "../services/i18n.js";
|
|
||||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
|
||||||
|
|
||||||
const TPL = /*html*/`
|
|
||||||
<div class="api-log-widget">
|
|
||||||
<style>
|
|
||||||
.api-log-widget {
|
|
||||||
padding: 15px;
|
|
||||||
flex-grow: 1;
|
|
||||||
max-height: 40%;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hidden-api-log {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.api-log-container {
|
|
||||||
overflow: auto;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-api-log-button {
|
|
||||||
padding: 5px;
|
|
||||||
border: 1px solid var(--button-border-color);
|
|
||||||
background-color: var(--button-background-color);
|
|
||||||
border-radius: var(--button-border-radius);
|
|
||||||
color: var(--button-text-color);
|
|
||||||
position: absolute;
|
|
||||||
top: 10px;
|
|
||||||
right: 40px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div class="bx bx-x close-api-log-button" title="${t("api_log.close")}"></div>
|
|
||||||
|
|
||||||
<div class="api-log-container"></div>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
export default class ApiLogWidget extends NoteContextAwareWidget {
|
|
||||||
|
|
||||||
private $logContainer!: JQuery<HTMLElement>;
|
|
||||||
private $closeButton!: JQuery<HTMLElement>;
|
|
||||||
|
|
||||||
isEnabled() {
|
|
||||||
return !!this.note && this.note.mime.startsWith("application/javascript;env=") && super.isEnabled();
|
|
||||||
}
|
|
||||||
|
|
||||||
doRender() {
|
|
||||||
this.$widget = $(TPL);
|
|
||||||
this.toggle(false);
|
|
||||||
|
|
||||||
this.$logContainer = this.$widget.find(".api-log-container");
|
|
||||||
this.$closeButton = this.$widget.find(".close-api-log-button");
|
|
||||||
this.$closeButton.on("click", () => this.toggle(false));
|
|
||||||
}
|
|
||||||
|
|
||||||
async refreshWithNote(note: FNote) {
|
|
||||||
this.$logContainer.empty();
|
|
||||||
}
|
|
||||||
|
|
||||||
apiLogMessagesEvent({ messages, noteId }: EventData<"apiLogMessages">) {
|
|
||||||
if (!this.isNote(noteId)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.toggle(true);
|
|
||||||
|
|
||||||
for (const message of messages) {
|
|
||||||
this.$logContainer.append(message).append($("<br>"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
toggle(show: boolean) {
|
|
||||||
this.$widget.toggleClass("hidden-api-log", !show);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
41
apps/client/src/widgets/api_log.tsx
Normal file
41
apps/client/src/widgets/api_log.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
import "./api_log.css";
|
||||||
|
import { useNoteContext, useTriliumEvent } from "./react/hooks";
|
||||||
|
import ActionButton from "./react/ActionButton";
|
||||||
|
import { t } from "../services/i18n";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays the messages that are logged by the current note via `api.log`, for frontend and backend scripts.
|
||||||
|
*/
|
||||||
|
export default function ApiLog() {
|
||||||
|
const { note, noteId } = useNoteContext();
|
||||||
|
const [ messages, setMessages ] = useState<string[]>();
|
||||||
|
|
||||||
|
useTriliumEvent("apiLogMessages", ({ messages, noteId: eventNoteId }) => {
|
||||||
|
if (eventNoteId !== noteId) return;
|
||||||
|
setMessages(messages);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Clear when navigating away.
|
||||||
|
useEffect(() => setMessages(undefined), [ note ]);
|
||||||
|
|
||||||
|
const isEnabled = note?.mime.startsWith("application/javascript;env=") && messages?.length;
|
||||||
|
return (
|
||||||
|
<div className={`api-log-widget ${!isEnabled ? "hidden-ext" : ""}`}>
|
||||||
|
{isEnabled && (
|
||||||
|
<>
|
||||||
|
<ActionButton
|
||||||
|
icon="bx bx-x"
|
||||||
|
className="close-api-log-button"
|
||||||
|
text={t("api_log.close")}
|
||||||
|
onClick={() => setMessages(undefined)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="api-log-container">
|
||||||
|
{messages.join("\n")}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -55,7 +55,7 @@ export class TypedBasicWidget<T extends TypedComponent<any>> extends TypedCompon
|
|||||||
* @param components the components to be added as children to this component provided the condition is truthy.
|
* @param components the components to be added as children to this component provided the condition is truthy.
|
||||||
* @returns self for chaining.
|
* @returns self for chaining.
|
||||||
*/
|
*/
|
||||||
optChild(condition: boolean, ...components: T[]) {
|
optChild(condition: boolean, ...components: (T | VNode)[]) {
|
||||||
if (condition) {
|
if (condition) {
|
||||||
return this.child(...components);
|
return this.child(...components);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
102
apps/client/src/widgets/buttons/global_menu.css
Normal file
102
apps/client/src/widgets/buttons/global_menu.css
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
.global-menu {
|
||||||
|
width: 53px;
|
||||||
|
height: 53px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-menu .dropdown-menu {
|
||||||
|
min-width: 20em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-menu-button {
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
position: relative;
|
||||||
|
padding: 6px;
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-menu-button svg path {
|
||||||
|
fill: var(--launcher-pane-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-menu-button:hover { border: 0; }
|
||||||
|
.global-menu-button:hover svg path {
|
||||||
|
transition: 200ms ease-in-out fill;
|
||||||
|
}
|
||||||
|
.global-menu-button:hover svg path.st0 { fill:#95C980; }
|
||||||
|
.global-menu-button:hover svg path.st1 { fill:#72B755; }
|
||||||
|
.global-menu-button:hover svg path.st2 { fill:#4FA52B; }
|
||||||
|
.global-menu-button:hover svg path.st3 { fill:#EE8C89; }
|
||||||
|
.global-menu-button:hover svg path.st4 { fill:#E96562; }
|
||||||
|
.global-menu-button:hover svg path.st5 { fill:#E33F3B; }
|
||||||
|
.global-menu-button:hover svg path.st6 { fill:#EFB075; }
|
||||||
|
.global-menu-button:hover svg path.st7 { fill:#E99547; }
|
||||||
|
.global-menu-button:hover svg path.st8 { fill:#E47B19; }
|
||||||
|
|
||||||
|
.global-menu-button-update-available {
|
||||||
|
position: absolute;
|
||||||
|
right: -30px;
|
||||||
|
bottom: -30px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-menu .zoom-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-menu .zoom-buttons {
|
||||||
|
margin-left: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-menu .zoom-buttons a {
|
||||||
|
display: inline-block;
|
||||||
|
border: 1px solid var(--button-border-color);
|
||||||
|
border-radius: var(--button-border-radius);
|
||||||
|
color: var(--button-text-color);
|
||||||
|
background-color: var(--button-background-color);
|
||||||
|
padding: 3px;
|
||||||
|
margin-left: 3px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-menu .zoom-buttons a:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-menu .zoom-state {
|
||||||
|
margin-left: 5px;
|
||||||
|
margin-right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-menu .dropdown-item .bx {
|
||||||
|
position: relative;
|
||||||
|
top: 3px;
|
||||||
|
font-size: 120%;
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* #region Update available */
|
||||||
|
.global-menu-button-update-available-button {
|
||||||
|
width: 21px !important;
|
||||||
|
height: 21px !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
|
||||||
|
border-radius: var(--button-border-radius);
|
||||||
|
transform: scale(0.9);
|
||||||
|
border: none;
|
||||||
|
opacity: 0.8;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.global-menu-button-wrapper:hover .global-menu-button-update-available-button {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
/* #endregion */
|
||||||
@@ -1,436 +0,0 @@
|
|||||||
import { t } from "../../services/i18n.js";
|
|
||||||
import BasicWidget from "../basic_widget.js";
|
|
||||||
import utils from "../../services/utils.js";
|
|
||||||
import UpdateAvailableWidget from "./update_available.js";
|
|
||||||
import options from "../../services/options.js";
|
|
||||||
import { Tooltip, Dropdown } from "bootstrap";
|
|
||||||
|
|
||||||
const TPL = /*html*/`
|
|
||||||
<div class="dropdown global-menu">
|
|
||||||
<style>
|
|
||||||
.global-menu {
|
|
||||||
width: 53px;
|
|
||||||
height: 53px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.global-menu .dropdown-menu {
|
|
||||||
min-width: 20em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.global-menu-button {
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
position: relative;
|
|
||||||
padding: 6px;
|
|
||||||
border: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.global-menu-button > svg path {
|
|
||||||
fill: var(--launcher-pane-text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.global-menu-button:hover { border: 0; }
|
|
||||||
.global-menu-button:hover > svg path {
|
|
||||||
transition: 200ms ease-in-out fill;
|
|
||||||
}
|
|
||||||
.global-menu-button:hover > svg path.st0 { fill:#95C980; }
|
|
||||||
.global-menu-button:hover > svg path.st1 { fill:#72B755; }
|
|
||||||
.global-menu-button:hover > svg path.st2 { fill:#4FA52B; }
|
|
||||||
.global-menu-button:hover > svg path.st3 { fill:#EE8C89; }
|
|
||||||
.global-menu-button:hover > svg path.st4 { fill:#E96562; }
|
|
||||||
.global-menu-button:hover > svg path.st5 { fill:#E33F3B; }
|
|
||||||
.global-menu-button:hover > svg path.st6 { fill:#EFB075; }
|
|
||||||
.global-menu-button:hover > svg path.st7 { fill:#E99547; }
|
|
||||||
.global-menu-button:hover > svg path.st8 { fill:#E47B19; }
|
|
||||||
|
|
||||||
.global-menu-button-update-available {
|
|
||||||
position: absolute;
|
|
||||||
right: -30px;
|
|
||||||
bottom: -30px;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.global-menu .zoom-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: baseline;
|
|
||||||
}
|
|
||||||
|
|
||||||
.global-menu .zoom-buttons a {
|
|
||||||
display: inline-block;
|
|
||||||
border: 1px solid var(--button-border-color);
|
|
||||||
border-radius: var(--button-border-radius);
|
|
||||||
color: var(--button-text-color);
|
|
||||||
background-color: var(--button-background-color);
|
|
||||||
padding: 3px;
|
|
||||||
margin-left: 3px;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.global-menu .zoom-buttons a:hover {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.global-menu .zoom-state {
|
|
||||||
margin-left: 5px;
|
|
||||||
margin-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.global-menu .dropdown-item .bx {
|
|
||||||
position: relative;
|
|
||||||
top: 3px;
|
|
||||||
font-size: 120%;
|
|
||||||
margin-right: 6px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<button type="button" data-bs-toggle="dropdown" aria-haspopup="true"
|
|
||||||
aria-expanded="false" class="icon-action global-menu-button">
|
|
||||||
<div class="global-menu-button-update-available"></div>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<ul class="dropdown-menu dropdown-menu-right">
|
|
||||||
<li class="dropdown-item" data-trigger-command="openNewWindow">
|
|
||||||
<span class="bx bx-window-open"></span>
|
|
||||||
${t("global_menu.open_new_window")}
|
|
||||||
<kbd data-command="openNewWindow"></kbd>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dropdown-item" data-trigger-command="showShareSubtree">
|
|
||||||
<span class="bx bx-share-alt"></span>
|
|
||||||
${t("global_menu.show_shared_notes_subtree")}
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<div class="dropdown-divider"></div>
|
|
||||||
|
|
||||||
<span class="zoom-container dropdown-item dropdown-item-container">
|
|
||||||
<div>
|
|
||||||
<span class="bx bx-empty"></span>
|
|
||||||
${t("global_menu.zoom")}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="zoom-buttons">
|
|
||||||
<a data-trigger-command="toggleFullscreen" title="${t("global_menu.toggle_fullscreen")}" class="bx bx-expand-alt"></a>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<a data-trigger-command="zoomOut" title="${t("global_menu.zoom_out")}" class="bx bx-minus"></a>
|
|
||||||
|
|
||||||
<span data-trigger-command="zoomReset" title="${t("global_menu.reset_zoom_level")}" class="zoom-state"></span>
|
|
||||||
|
|
||||||
<a data-trigger-command="zoomIn" title="${t("global_menu.zoom_in")}" class="bx bx-plus"></a>
|
|
||||||
</div>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<li class="dropdown-item toggle-pin">
|
|
||||||
<span class="bx bx-pin"></span>
|
|
||||||
${t("title_bar_buttons.window-on-top")}
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dropdown-item" data-trigger-command="toggleZenMode">
|
|
||||||
<span class="bx bxs-yin-yang"></span>
|
|
||||||
${t("global_menu.toggle-zen-mode")}
|
|
||||||
<kbd data-command="toggleZenMode"></kbd>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<div class="dropdown-divider desktop-only"></div>
|
|
||||||
|
|
||||||
<li class="dropdown-item switch-to-mobile-version-button" data-trigger-command="switchToMobileVersion">
|
|
||||||
<span class="bx bx-mobile"></span>
|
|
||||||
${t("global_menu.switch_to_mobile_version")}
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dropdown-item switch-to-desktop-version-button" data-trigger-command="switchToDesktopVersion">
|
|
||||||
<span class="bx bx-desktop"></span>
|
|
||||||
${t("global_menu.switch_to_desktop_version")}
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dropdown-item" data-trigger-command="showLaunchBarSubtree">
|
|
||||||
<span class="bx ${utils.isMobile() ? "bx-mobile" : "bx-sidebar"}"></span>
|
|
||||||
${t("global_menu.configure_launchbar")}
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dropdown-item dropdown-submenu">
|
|
||||||
<span class="dropdown-toggle">
|
|
||||||
<span class="bx bx-chip"></span>${t("global_menu.advanced")}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<ul class="dropdown-menu">
|
|
||||||
<li class="dropdown-item" data-trigger-command="showHiddenSubtree">
|
|
||||||
<span class="bx bx-hide"></span>
|
|
||||||
${t("global_menu.show_hidden_subtree")}
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dropdown-item" data-trigger-command="showSearchHistory">
|
|
||||||
<span class="bx bx-search-alt"></span>
|
|
||||||
${t("global_menu.open_search_history")}
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<div class="dropdown-divider"></div>
|
|
||||||
|
|
||||||
<li class="dropdown-item" data-trigger-command="showBackendLog">
|
|
||||||
<span class="bx bx-detail"></span>
|
|
||||||
${t("global_menu.show_backend_log")}
|
|
||||||
<kbd data-command="showBackendLog"></kbd>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dropdown-item" data-trigger-command="showSQLConsole">
|
|
||||||
<span class="bx bx-data"></span>
|
|
||||||
${t("global_menu.open_sql_console")}
|
|
||||||
<kbd data-command="showSQLConsole"></kbd>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dropdown-item" data-trigger-command="showSQLConsoleHistory">
|
|
||||||
<span class="bx bx-data"></span>
|
|
||||||
${t("global_menu.open_sql_console_history")}
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<div class="dropdown-divider"></div>
|
|
||||||
|
|
||||||
<li class="dropdown-item open-dev-tools-button" data-trigger-command="openDevTools">
|
|
||||||
<span class="bx bx-bug-alt"></span>
|
|
||||||
${t("global_menu.open_dev_tools")}
|
|
||||||
<kbd data-command="openDevTools"></kbd>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dropdown-item" data-trigger-command="reloadFrontendApp"
|
|
||||||
title="${t("global_menu.reload_hint")}">
|
|
||||||
<span class="bx bx-refresh"></span>
|
|
||||||
${t("global_menu.reload_frontend")}
|
|
||||||
<kbd data-command="reloadFrontendApp"></kbd>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dropdown-item" data-trigger-command="showOptions">
|
|
||||||
<span class="bx bx-cog"></span>
|
|
||||||
${t("global_menu.options")}
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<div class="dropdown-divider desktop-only"></div>
|
|
||||||
|
|
||||||
<li class="dropdown-item show-help-button" data-trigger-command="showHelp">
|
|
||||||
<span class="bx bx-help-circle"></span>
|
|
||||||
${t("global_menu.show_help")}
|
|
||||||
<kbd data-command="showHelp"></kbd>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dropdown-item show-help-button" data-trigger-command="showCheatsheet">
|
|
||||||
<span class="bx bxs-keyboard"></span>
|
|
||||||
${t("global_menu.show-cheatsheet")}
|
|
||||||
<kbd data-command="showCheatsheet"></kbd>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dropdown-item show-about-dialog-button">
|
|
||||||
<span class="bx bx-info-circle"></span>
|
|
||||||
${t("global_menu.about")}
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li class="dropdown-item update-to-latest-version-button" style="display: none;" data-trigger-command="downloadLatestVersion">
|
|
||||||
<span class="bx bx-sync"></span>
|
|
||||||
|
|
||||||
<span class="version-text"></span>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<div class="dropdown-divider logout-button-separator"></div>
|
|
||||||
|
|
||||||
<li class="dropdown-item logout-button" data-trigger-command="logout">
|
|
||||||
<span class="bx bx-log-out"></span>
|
|
||||||
${t("global_menu.logout")}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default class GlobalMenuWidget extends BasicWidget {
|
|
||||||
private updateAvailableWidget: UpdateAvailableWidget;
|
|
||||||
private isHorizontalLayout: boolean;
|
|
||||||
private tooltip!: Tooltip;
|
|
||||||
private dropdown!: Dropdown;
|
|
||||||
|
|
||||||
private $updateToLatestVersionButton!: JQuery<HTMLElement>;
|
|
||||||
private $zoomState!: JQuery<HTMLElement>;
|
|
||||||
private $toggleZenMode!: JQuery<HTMLElement>;
|
|
||||||
|
|
||||||
constructor(isHorizontalLayout: boolean) {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.updateAvailableWidget = new UpdateAvailableWidget();
|
|
||||||
this.isHorizontalLayout = isHorizontalLayout;
|
|
||||||
}
|
|
||||||
|
|
||||||
doRender() {
|
|
||||||
this.$widget = $(TPL);
|
|
||||||
|
|
||||||
if (!this.isHorizontalLayout) {
|
|
||||||
this.$widget.addClass("dropend");
|
|
||||||
}
|
|
||||||
|
|
||||||
const $globalMenuButton = this.$widget.find(".global-menu-button");
|
|
||||||
if (!this.isHorizontalLayout) {
|
|
||||||
$globalMenuButton.prepend(
|
|
||||||
$(`\
|
|
||||||
<svg viewBox="0 0 256 256" data-bs-toggle="tooltip" title="${t("global_menu.menu")}">
|
|
||||||
<g>
|
|
||||||
<path class="st0" d="m202.9 112.7c-22.5 16.1-54.5 12.8-74.9 6.3l14.8-11.8 14.1-11.3 49.1-39.3-51.2 35.9-14.3 10-14.9 10.5c0.7-21.2 7-49.9 28.6-65.4 1.8-1.3 3.9-2.6 6.1-3.8 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.4 2.8-4.9 5.4-7.4 7.8-3.4 3.5-6.8 6.4-10.1 8.8z"/>
|
|
||||||
<path class="st1" d="m213.1 104c-22.2 12.6-51.4 9.3-70.3 3.2l14.1-11.3 49.1-39.3-51.2 35.9-14.3 10c0.5-18.1 4.9-42.1 19.7-58.6 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.3 2.8-4.8 5.4-7.2 7.8z"/>
|
|
||||||
<path class="st2" d="m220.5 96.2c-21.1 8.6-46.6 5.3-63.7-0.2l49.2-39.4-51.2 35.9c0.3-15.8 3.5-36.6 14.3-52.8 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.8 66z"/>
|
|
||||||
|
|
||||||
<path class="st3" d="m106.7 179c-5.8-21 5.2-43.8 15.5-57.2l4.8 14.2 4.5 13.4 15.9 47-12.8-47.6-3.6-13.2-3.7-13.9c15.5 6.2 35.1 18.6 40.7 38.8 0.5 1.7 0.9 3.6 1.2 5.5 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.1-3.8-7.6-1.6-3.5-2.9-6.8-3.8-10z"/>
|
|
||||||
<path class="st4" d="m110.4 188.9c-3.4-19.8 6.9-40.5 16.6-52.9l4.5 13.4 15.9 47-12.8-47.6-3.6-13.2c13.3 5.2 29.9 15 38.1 30.4 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.2-3.8-7.7z"/>
|
|
||||||
<path class="st5" d="m114.2 196.5c-0.7-18 8.6-35.9 17.3-47.1l15.9 47-12.8-47.6c11.6 4.4 26.1 12.4 35.2 24.8 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8z"/>
|
|
||||||
|
|
||||||
<path class="st6" d="m86.3 59.1c21.7 10.9 32.4 36.6 35.8 54.9l-15.2-6.6-14.5-6.3-50.6-22 48.8 24.9 13.6 6.9 14.3 7.3c-16.6 7.9-41.3 14.5-62.1 4.1-1.8-0.9-3.6-1.9-5.4-3.2-2.3-1.5-4.5-3.2-6.8-5.1-19.9-16.4-40.3-46.4-42.7-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.2 0.8 6.2 1.6 9.1 2.5 4 1.3 7.6 2.8 10.9 4.4z"/>
|
|
||||||
<path class="st7" d="m75.4 54.8c18.9 12 28.4 35.6 31.6 52.6l-14.5-6.3-50.6-22 48.7 24.9 13.6 6.9c-14.1 6.8-34.5 13-53.3 8.2-2.3-1.5-4.5-3.2-6.8-5.1-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.1 0.8 6.2 1.6 9.1 2.6z"/>
|
|
||||||
<path class="st8" d="m66.3 52.2c15.3 12.8 23.3 33.6 26.1 48.9l-50.6-22 48.8 24.9c-12.2 6-29.6 11.8-46.5 10-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3z"/>
|
|
||||||
</g>
|
|
||||||
</svg>`)
|
|
||||||
);
|
|
||||||
|
|
||||||
this.tooltip = new Tooltip(this.$widget.find("[data-bs-toggle='tooltip']")[0], { trigger: "hover" });
|
|
||||||
} else {
|
|
||||||
$globalMenuButton.toggleClass("bx bx-menu");
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dropdown = Dropdown.getOrCreateInstance(this.$widget.find("[data-bs-toggle='dropdown']")[0], {
|
|
||||||
popperConfig: {
|
|
||||||
placement: "bottom"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.$widget.find(".show-about-dialog-button").on("click", () => this.triggerCommand("openAboutDialog"));
|
|
||||||
|
|
||||||
const isElectron = utils.isElectron();
|
|
||||||
|
|
||||||
this.$widget.find(".toggle-pin").toggle(isElectron);
|
|
||||||
if (isElectron) {
|
|
||||||
this.$widget.on("click", ".toggle-pin", (e) => {
|
|
||||||
const $el = $(e.target);
|
|
||||||
const remote = utils.dynamicRequire("@electron/remote");
|
|
||||||
const focusedWindow = remote.BrowserWindow.getFocusedWindow();
|
|
||||||
const isAlwaysOnTop = focusedWindow.isAlwaysOnTop();
|
|
||||||
if (isAlwaysOnTop) {
|
|
||||||
focusedWindow.setAlwaysOnTop(false);
|
|
||||||
$el.removeClass("active");
|
|
||||||
} else {
|
|
||||||
focusedWindow.setAlwaysOnTop(true);
|
|
||||||
$el.addClass("active");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$widget.find(".logout-button").toggle(!isElectron);
|
|
||||||
this.$widget.find(".logout-button-separator").toggle(!isElectron);
|
|
||||||
|
|
||||||
this.$widget.find(".open-dev-tools-button").toggle(isElectron);
|
|
||||||
this.$widget.find(".switch-to-mobile-version-button").toggle(!isElectron && utils.isDesktop());
|
|
||||||
this.$widget.find(".switch-to-desktop-version-button").toggle(!isElectron && utils.isMobile());
|
|
||||||
|
|
||||||
this.$widget.on("click", ".dropdown-item", (e) => {
|
|
||||||
if ($(e.target).parent(".zoom-buttons")) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dropdown.toggle();
|
|
||||||
});
|
|
||||||
if (utils.isMobile()) {
|
|
||||||
this.$widget.on("click", ".dropdown-submenu .dropdown-toggle", (e) => {
|
|
||||||
const $submenu = $(e.target).closest(".dropdown-item");
|
|
||||||
$submenu.toggleClass("submenu-open");
|
|
||||||
$submenu.find("ul.dropdown-menu").toggleClass("show");
|
|
||||||
e.stopPropagation();
|
|
||||||
return;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this.$widget.on("click", ".dropdown-submenu", (e) => {
|
|
||||||
if ($(e.target).children(".dropdown-menu").length === 1 || $(e.target).hasClass("dropdown-toggle")) {
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.$widget.find(".global-menu-button-update-available").append(this.updateAvailableWidget.render());
|
|
||||||
|
|
||||||
this.$updateToLatestVersionButton = this.$widget.find(".update-to-latest-version-button");
|
|
||||||
|
|
||||||
if (!utils.isElectron()) {
|
|
||||||
this.$widget.find(".zoom-container").hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$zoomState = this.$widget.find(".zoom-state");
|
|
||||||
this.$toggleZenMode = this.$widget.find('[data-trigger-command="toggleZenMode"');
|
|
||||||
this.$widget.on("show.bs.dropdown", () => this.#onShown());
|
|
||||||
if (this.tooltip) {
|
|
||||||
this.$widget.on("hide.bs.dropdown", () => this.tooltip.enable());
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$widget.find(".zoom-buttons").on(
|
|
||||||
"click",
|
|
||||||
// delay to wait for the actual zoom change
|
|
||||||
() => setTimeout(() => this.updateZoomState(), 300)
|
|
||||||
);
|
|
||||||
|
|
||||||
this.updateVersionStatus();
|
|
||||||
|
|
||||||
setInterval(() => this.updateVersionStatus(), 8 * 60 * 60 * 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
#onShown() {
|
|
||||||
this.$toggleZenMode.toggleClass("active", $("body").hasClass("zen"));
|
|
||||||
this.updateZoomState();
|
|
||||||
if (this.tooltip) {
|
|
||||||
this.tooltip.hide();
|
|
||||||
this.tooltip.disable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateZoomState() {
|
|
||||||
if (!utils.isElectron()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const zoomFactor = utils.dynamicRequire("electron").webFrame.getZoomFactor();
|
|
||||||
const zoomPercent = Math.round(zoomFactor * 100);
|
|
||||||
|
|
||||||
this.$zoomState.text(`${zoomPercent}%`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateVersionStatus() {
|
|
||||||
await options.initializedPromise;
|
|
||||||
|
|
||||||
if (options.get("checkForUpdates") !== "true") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const latestVersion = await this.fetchLatestVersion();
|
|
||||||
this.updateAvailableWidget.updateVersionStatus(latestVersion);
|
|
||||||
// Show "click to download" button in options menu if there's a new version available
|
|
||||||
this.$updateToLatestVersionButton.toggle(utils.isUpdateAvailable(latestVersion, glob.triliumVersion));
|
|
||||||
this.$updateToLatestVersionButton.find(".version-text").text(`Version ${latestVersion} is available, click to download.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
async fetchLatestVersion() {
|
|
||||||
const RELEASES_API_URL = "https://api.github.com/repos/TriliumNext/Trilium/releases/latest";
|
|
||||||
|
|
||||||
const resp = await fetch(RELEASES_API_URL);
|
|
||||||
const data = await resp.json();
|
|
||||||
|
|
||||||
return data?.tag_name?.substring(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
downloadLatestVersionCommand() {
|
|
||||||
window.open("https://github.com/TriliumNext/Trilium/releases/latest");
|
|
||||||
}
|
|
||||||
|
|
||||||
activeContextChangedEvent() {
|
|
||||||
this.dropdown.hide();
|
|
||||||
}
|
|
||||||
|
|
||||||
noteSwitchedEvent() {
|
|
||||||
this.dropdown.hide();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
239
apps/client/src/widgets/buttons/global_menu.tsx
Normal file
239
apps/client/src/widgets/buttons/global_menu.tsx
Normal file
@@ -0,0 +1,239 @@
|
|||||||
|
import Dropdown from "../react/Dropdown";
|
||||||
|
import "./global_menu.css";
|
||||||
|
import { useStaticTooltip, useStaticTooltipWithKeyboardShortcut, useTriliumOption, useTriliumOptionBool } from "../react/hooks";
|
||||||
|
import { useContext, useEffect, useRef, useState } from "preact/hooks";
|
||||||
|
import { t } from "../../services/i18n";
|
||||||
|
import { FormDropdownDivider, FormDropdownSubmenu, FormListItem } from "../react/FormList";
|
||||||
|
import { CommandNames } from "../../components/app_context";
|
||||||
|
import KeyboardShortcut from "../react/KeyboardShortcut";
|
||||||
|
import { KeyboardActionNames } from "@triliumnext/commons";
|
||||||
|
import { ComponentChildren } from "preact";
|
||||||
|
import Component from "../../components/component";
|
||||||
|
import { ParentComponent } from "../react/react_utils";
|
||||||
|
import utils, { dynamicRequire, isElectron, isMobile } from "../../services/utils";
|
||||||
|
|
||||||
|
interface MenuItemProps<T> {
|
||||||
|
icon: string,
|
||||||
|
text: ComponentChildren,
|
||||||
|
title?: string,
|
||||||
|
command: T,
|
||||||
|
disabled?: boolean
|
||||||
|
active?: boolean;
|
||||||
|
outsideChildren?: ComponentChildren;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function GlobalMenu({ isHorizontalLayout }: { isHorizontalLayout: boolean }) {
|
||||||
|
const isVerticalLayout = !isHorizontalLayout;
|
||||||
|
const parentComponent = useContext(ParentComponent);
|
||||||
|
const { isUpdateAvailable, latestVersion } = useTriliumUpdateStatus();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
className="global-menu"
|
||||||
|
buttonClassName={`global-menu-button ${isHorizontalLayout ? "bx bx-menu" : ""}`} noSelectButtonStyle iconAction hideToggleArrow
|
||||||
|
text={<>
|
||||||
|
{isVerticalLayout && <VerticalLayoutIcon />}
|
||||||
|
{isUpdateAvailable && <div class="global-menu-button-update-available">
|
||||||
|
<span className="bx bx-sync global-menu-button-update-available-button" title={t("update_available.update_available")}></span>
|
||||||
|
</div>}
|
||||||
|
</>}
|
||||||
|
>
|
||||||
|
|
||||||
|
<MenuItem command="openNewWindow" icon="bx bx-window-open" text={t("global_menu.open_new_window")} />
|
||||||
|
<MenuItem command="showShareSubtree" icon="bx bx-share-alt" text={t("global_menu.show_shared_notes_subtree")} />
|
||||||
|
<FormDropdownDivider />
|
||||||
|
|
||||||
|
<ZoomControls parentComponent={parentComponent} />
|
||||||
|
<ToggleWindowOnTop />
|
||||||
|
<KeyboardActionMenuItem command="toggleZenMode" icon="bx bxs-yin-yang" text={t("global_menu.toggle-zen-mode")} />
|
||||||
|
<FormDropdownDivider />
|
||||||
|
|
||||||
|
<SwitchToOptions />
|
||||||
|
<MenuItem command="showLaunchBarSubtree" icon={`bx ${isMobile() ? "bx-mobile" : "bx-sidebar"}`} text={t("global_menu.configure_launchbar")} />
|
||||||
|
<AdvancedMenu />
|
||||||
|
<MenuItem command="showOptions" icon="bx bx-cog" text={t("global_menu.options")} />
|
||||||
|
<FormDropdownDivider />
|
||||||
|
|
||||||
|
<KeyboardActionMenuItem command="showHelp" icon="bx bx-help-circle" text={t("global_menu.show_help")} />
|
||||||
|
<KeyboardActionMenuItem command="showCheatsheet" icon="bx bxs-keyboard" text={t("global_menu.show-cheatsheet")} />
|
||||||
|
<MenuItem command="openAboutDialog" icon="bx bx-info-circle" text={t("global_menu.about")} />
|
||||||
|
{isUpdateAvailable && <MenuItem command={() => window.open("https://github.com/TriliumNext/Trilium/releases/latest")} icon="bx bx-sync" text={`Version ${latestVersion} is available, click to download.`} /> }
|
||||||
|
{!isElectron() && <BrowserOnlyOptions />}
|
||||||
|
</Dropdown>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function AdvancedMenu() {
|
||||||
|
return (
|
||||||
|
<FormDropdownSubmenu icon="bx bx-chip" title={t("global_menu.advanced")}>
|
||||||
|
<MenuItem command="showHiddenSubtree" icon="bx bx-hide" text={t("global_menu.show_hidden_subtree")} />
|
||||||
|
<MenuItem command="showSearchHistory" icon="bx bx-search-alt" text={t("global_menu.open_search_history")} />
|
||||||
|
<FormDropdownDivider />
|
||||||
|
|
||||||
|
<KeyboardActionMenuItem command="showBackendLog" icon="bx bx-detail" text={t("global_menu.show_backend_log")} />
|
||||||
|
<KeyboardActionMenuItem command="showSQLConsole" icon="bx bx-data" text={t("global_menu.open_sql_console")} />
|
||||||
|
<MenuItem command="showSQLConsoleHistory" icon="bx bx-data" text={t("global_menu.open_sql_console_history")} />
|
||||||
|
<FormDropdownDivider />
|
||||||
|
|
||||||
|
{isElectron() && <MenuItem command="openDevTools" icon="bx bx-bug-alt" text={t("global_menu.open_dev_tools")} />}
|
||||||
|
<KeyboardActionMenuItem command="reloadFrontendApp" icon="bx bx-refresh" text={t("global_menu.reload_frontend")} title={t("global_menu.reload_hint")} />
|
||||||
|
</FormDropdownSubmenu>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function BrowserOnlyOptions() {
|
||||||
|
return <>
|
||||||
|
<FormDropdownDivider />
|
||||||
|
<MenuItem command="logout" icon="bx bx-log-out" text={t("global_menu.logout")} />
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SwitchToOptions() {
|
||||||
|
if (isElectron()) {
|
||||||
|
return;
|
||||||
|
} else if (!isMobile()) {
|
||||||
|
return <MenuItem command="switchToMobileVersion" icon="bx bx-mobile" text={t("global_menu.switch_to_mobile_version")} />
|
||||||
|
} else {
|
||||||
|
return <MenuItem command="switchToDesktopVersion" icon="bx bx-desktop" text={t("global_menu.switch_to_desktop_version")} />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function MenuItem({ icon, text, title, command, disabled, active }: MenuItemProps<KeyboardActionNames | CommandNames | (() => void)>) {
|
||||||
|
return <FormListItem
|
||||||
|
icon={icon}
|
||||||
|
title={title}
|
||||||
|
triggerCommand={typeof command === "string" ? command : undefined}
|
||||||
|
onClick={typeof command === "function" ? command : undefined}
|
||||||
|
disabled={disabled}
|
||||||
|
active={active}
|
||||||
|
>{text}</FormListItem>
|
||||||
|
}
|
||||||
|
|
||||||
|
function KeyboardActionMenuItem({ text, command, ...props }: MenuItemProps<KeyboardActionNames>) {
|
||||||
|
return <MenuItem
|
||||||
|
{...props}
|
||||||
|
command={command}
|
||||||
|
text={<>{text} <KeyboardShortcut actionName={command as KeyboardActionNames} /></>}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
|
||||||
|
function VerticalLayoutIcon() {
|
||||||
|
const logoRef = useRef<SVGSVGElement>(null);
|
||||||
|
useStaticTooltip(logoRef);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg ref={logoRef} viewBox="0 0 256 256" title={t("global_menu.menu")}>
|
||||||
|
<g>
|
||||||
|
<path className="st0" d="m202.9 112.7c-22.5 16.1-54.5 12.8-74.9 6.3l14.8-11.8 14.1-11.3 49.1-39.3-51.2 35.9-14.3 10-14.9 10.5c0.7-21.2 7-49.9 28.6-65.4 1.8-1.3 3.9-2.6 6.1-3.8 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.4 2.8-4.9 5.4-7.4 7.8-3.4 3.5-6.8 6.4-10.1 8.8z"/>
|
||||||
|
<path className="st1" d="m213.1 104c-22.2 12.6-51.4 9.3-70.3 3.2l14.1-11.3 49.1-39.3-51.2 35.9-14.3 10c0.5-18.1 4.9-42.1 19.7-58.6 2.7-1.5 5.7-2.9 8.8-4.1 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.9 65.9-2.3 2.8-4.8 5.4-7.2 7.8z"/>
|
||||||
|
<path className="st2" d="m220.5 96.2c-21.1 8.6-46.6 5.3-63.7-0.2l49.2-39.4-51.2 35.9c0.3-15.8 3.5-36.6 14.3-52.8 27.1-11.1 68.5-15.3 85.2-9.5 0.1 16.2-15.9 45.4-33.8 66z"/>
|
||||||
|
|
||||||
|
<path className="st3" d="m106.7 179c-5.8-21 5.2-43.8 15.5-57.2l4.8 14.2 4.5 13.4 15.9 47-12.8-47.6-3.6-13.2-3.7-13.9c15.5 6.2 35.1 18.6 40.7 38.8 0.5 1.7 0.9 3.6 1.2 5.5 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.1-3.8-7.6-1.6-3.5-2.9-6.8-3.8-10z"/>
|
||||||
|
<path className="st4" d="m110.4 188.9c-3.4-19.8 6.9-40.5 16.6-52.9l4.5 13.4 15.9 47-12.8-47.6-3.6-13.2c13.3 5.2 29.9 15 38.1 30.4 0.4 2.4 0.6 5 0.7 7.7 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8-1.4-2.6-2.7-5.2-3.8-7.7z"/>
|
||||||
|
<path className="st5" d="m114.2 196.5c-0.7-18 8.6-35.9 17.3-47.1l15.9 47-12.8-47.6c11.6 4.4 26.1 12.4 35.2 24.8 0.9 23.1-7.1 54.9-15.9 65.7-12-4.3-29.3-24-39.7-42.8z"/>
|
||||||
|
|
||||||
|
<path className="st6" d="m86.3 59.1c21.7 10.9 32.4 36.6 35.8 54.9l-15.2-6.6-14.5-6.3-50.6-22 48.8 24.9 13.6 6.9 14.3 7.3c-16.6 7.9-41.3 14.5-62.1 4.1-1.8-0.9-3.6-1.9-5.4-3.2-2.3-1.5-4.5-3.2-6.8-5.1-19.9-16.4-40.3-46.4-42.7-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.2 0.8 6.2 1.6 9.1 2.5 4 1.3 7.6 2.8 10.9 4.4z"/>
|
||||||
|
<path className="st7" d="m75.4 54.8c18.9 12 28.4 35.6 31.6 52.6l-14.5-6.3-50.6-22 48.7 24.9 13.6 6.9c-14.1 6.8-34.5 13-53.3 8.2-2.3-1.5-4.5-3.2-6.8-5.1-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3 3.1 0.8 6.2 1.6 9.1 2.6z"/>
|
||||||
|
<path className="st8" d="m66.3 52.2c15.3 12.8 23.3 33.6 26.1 48.9l-50.6-22 48.8 24.9c-12.2 6-29.6 11.8-46.5 10-19.8-16.4-40.2-46.4-42.6-61.5 12.4-6.5 41.5-5.8 64.8-0.3z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function ZoomControls({ parentComponent }: { parentComponent?: Component | null }) {
|
||||||
|
const [ zoomLevel, setZoomLevel ] = useState(100);
|
||||||
|
|
||||||
|
function updateZoomState() {
|
||||||
|
if (!isElectron()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const zoomFactor = dynamicRequire("electron").webFrame.getZoomFactor();
|
||||||
|
setZoomLevel(Math.round(zoomFactor * 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(updateZoomState, []);
|
||||||
|
|
||||||
|
function ZoomControlButton({ command, title, icon, children }: { command: KeyboardActionNames, title: string, icon?: string, children?: ComponentChildren }) {
|
||||||
|
const linkRef = useRef<HTMLAnchorElement>(null);
|
||||||
|
useStaticTooltipWithKeyboardShortcut(linkRef, title, command);
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
ref={linkRef}
|
||||||
|
onClick={(e) => {
|
||||||
|
parentComponent?.triggerCommand(command);
|
||||||
|
setTimeout(() => updateZoomState(), 300)
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
|
className={icon}
|
||||||
|
>{children}</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return isElectron() ? (
|
||||||
|
<FormListItem
|
||||||
|
icon="bx bx-empty"
|
||||||
|
className="zoom-container"
|
||||||
|
>
|
||||||
|
{t("global_menu.zoom")}
|
||||||
|
<>
|
||||||
|
<div className="zoom-buttons">
|
||||||
|
<ZoomControlButton command="toggleFullscreen" title={t("global_menu.toggle_fullscreen")} icon="bx bx-expand-alt" />
|
||||||
|
|
||||||
|
<ZoomControlButton command="zoomOut" title={t("global_menu.zoom_out")} icon="bx bx-minus" />
|
||||||
|
<ZoomControlButton command="zoomReset" title={t("global_menu.reset_zoom_level")}>{zoomLevel}{t("units.percentage")}</ZoomControlButton>
|
||||||
|
<ZoomControlButton command="zoomIn" title={t("global_menu.zoom_in")} icon="bx bx-plus" />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
</FormListItem>
|
||||||
|
) : (
|
||||||
|
<MenuItem icon="bx bx-expand-alt" command="toggleFullscreen" text={t("global_menu.toggle_fullscreen")} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ToggleWindowOnTop() {
|
||||||
|
const focusedWindow = isElectron() ? dynamicRequire("@electron/remote").BrowserWindow.getFocusedWindow() : null;
|
||||||
|
const [ isAlwaysOnTop, setIsAlwaysOnTop ] = useState(focusedWindow?.isAlwaysOnTop());
|
||||||
|
|
||||||
|
return (isElectron() &&
|
||||||
|
<MenuItem
|
||||||
|
icon="bx bx-pin"
|
||||||
|
text={t("title_bar_buttons.window-on-top")}
|
||||||
|
active={isAlwaysOnTop}
|
||||||
|
command={() => {
|
||||||
|
const newState = !isAlwaysOnTop;
|
||||||
|
focusedWindow?.setAlwaysOnTop(newState);
|
||||||
|
setIsAlwaysOnTop(newState);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function useTriliumUpdateStatus() {
|
||||||
|
const [ latestVersion, setLatestVersion ] = useState<string>();
|
||||||
|
const [ checkForUpdates ] = useTriliumOptionBool("checkForUpdates");
|
||||||
|
const isUpdateAvailable = utils.isUpdateAvailable(latestVersion, glob.triliumVersion);
|
||||||
|
|
||||||
|
async function updateVersionStatus() {
|
||||||
|
const RELEASES_API_URL = "https://api.github.com/repos/TriliumNext/Trilium/releases/latest";
|
||||||
|
|
||||||
|
const resp = await fetch(RELEASES_API_URL);
|
||||||
|
const data = await resp.json();
|
||||||
|
const latestVersion = data?.tag_name?.substring(1);
|
||||||
|
setLatestVersion(latestVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!checkForUpdates) {
|
||||||
|
setLatestVersion(undefined);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateVersionStatus();
|
||||||
|
|
||||||
|
const interval = setInterval(() => updateVersionStatus(), 8 * 60 * 60 * 1000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [ checkForUpdates ]);
|
||||||
|
|
||||||
|
return { isUpdateAvailable, latestVersion };
|
||||||
|
}
|
||||||
@@ -4,15 +4,7 @@ import treeService from "../../services/tree.js";
|
|||||||
import ButtonFromNoteWidget from "./button_from_note.js";
|
import ButtonFromNoteWidget from "./button_from_note.js";
|
||||||
import type FNote from "../../entities/fnote.js";
|
import type FNote from "../../entities/fnote.js";
|
||||||
import type { CommandNames } from "../../components/app_context.js";
|
import type { CommandNames } from "../../components/app_context.js";
|
||||||
|
import type { WebContents } from "electron";
|
||||||
interface WebContents {
|
|
||||||
history: string[];
|
|
||||||
getActiveIndex(): number;
|
|
||||||
clearHistory(): void;
|
|
||||||
canGoBack(): boolean;
|
|
||||||
canGoForward(): boolean;
|
|
||||||
goToIndex(index: string): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ContextMenuItem {
|
interface ContextMenuItem {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -51,14 +43,14 @@ export default class HistoryNavigationButton extends ButtonFromNoteWidget {
|
|||||||
async showContextMenu(e: JQuery.ContextMenuEvent) {
|
async showContextMenu(e: JQuery.ContextMenuEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (!this.webContents || this.webContents.history.length < 2) {
|
if (!this.webContents || this.webContents.navigationHistory.length() < 2) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let items: ContextMenuItem[] = [];
|
let items: ContextMenuItem[] = [];
|
||||||
|
|
||||||
const activeIndex = this.webContents.getActiveIndex();
|
const history = this.webContents.navigationHistory;
|
||||||
const history = this.webContents.history;
|
const activeIndex = history.getActiveIndex();
|
||||||
|
|
||||||
for (const idx in history) {
|
for (const idx in history) {
|
||||||
const url = history[idx];
|
const url = history[idx];
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
import options from "../../services/options.js";
|
|
||||||
import splitService from "../../services/resizer.js";
|
|
||||||
import CommandButtonWidget from "./command_button.js";
|
|
||||||
import { t } from "../../services/i18n.js";
|
|
||||||
import type { EventData } from "../../components/app_context.js";
|
|
||||||
|
|
||||||
export default class LeftPaneToggleWidget extends CommandButtonWidget {
|
|
||||||
private currentLeftPaneVisible: boolean;
|
|
||||||
|
|
||||||
constructor(isHorizontalLayout: boolean) {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.currentLeftPaneVisible = options.is("leftPaneVisible");
|
|
||||||
|
|
||||||
this.class(isHorizontalLayout ? "toggle-button" : "launcher-button");
|
|
||||||
|
|
||||||
this.settings.icon = () => {
|
|
||||||
if (options.get("layoutOrientation") === "horizontal") {
|
|
||||||
return "bx-sidebar";
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.currentLeftPaneVisible ? "bx-chevrons-left" : "bx-chevrons-right";
|
|
||||||
};
|
|
||||||
|
|
||||||
this.settings.title = () => (this.currentLeftPaneVisible ? t("left_pane_toggle.hide_panel") : t("left_pane_toggle.show_panel"));
|
|
||||||
|
|
||||||
this.settings.command = () => (this.currentLeftPaneVisible ? "hideLeftPane" : "showLeftPane");
|
|
||||||
|
|
||||||
if (isHorizontalLayout) {
|
|
||||||
this.settings.titlePlacement = "bottom";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshIcon() {
|
|
||||||
super.refreshIcon();
|
|
||||||
splitService.setupLeftPaneResizer(this.currentLeftPaneVisible);
|
|
||||||
}
|
|
||||||
|
|
||||||
setLeftPaneVisibilityEvent({ leftPaneVisible }: EventData<"setLeftPaneVisibility">) {
|
|
||||||
this.currentLeftPaneVisible = leftPaneVisible ?? !this.currentLeftPaneVisible;
|
|
||||||
this.refreshIcon();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
29
apps/client/src/widgets/buttons/left_pane_toggle.tsx
Normal file
29
apps/client/src/widgets/buttons/left_pane_toggle.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
import ActionButton from "../react/ActionButton";
|
||||||
|
import options from "../../services/options";
|
||||||
|
import { t } from "../../services/i18n";
|
||||||
|
import { useTriliumEvent } from "../react/hooks";
|
||||||
|
import resizer from "../../services/resizer";
|
||||||
|
|
||||||
|
export default function LeftPaneToggle({ isHorizontalLayout }: { isHorizontalLayout: boolean }) {
|
||||||
|
const [ currentLeftPaneVisible, setCurrentLeftPaneVisible ] = useState(options.is("leftPaneVisible"));
|
||||||
|
|
||||||
|
useTriliumEvent("setLeftPaneVisibility", ({ leftPaneVisible }) => {
|
||||||
|
setCurrentLeftPaneVisible(leftPaneVisible ?? !currentLeftPaneVisible);
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
resizer.setupLeftPaneResizer(currentLeftPaneVisible);
|
||||||
|
}, [ currentLeftPaneVisible ]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionButton
|
||||||
|
className={`${isHorizontalLayout ? "toggle-button" : "launcher-button"}`}
|
||||||
|
text={currentLeftPaneVisible ? t("left_pane_toggle.hide_panel") : t("left_pane_toggle.show_panel")}
|
||||||
|
triggerCommand={currentLeftPaneVisible ? "hideLeftPane" : "showLeftPane"}
|
||||||
|
icon={isHorizontalLayout
|
||||||
|
? "bx bx-sidebar"
|
||||||
|
: (currentLeftPaneVisible ? "bx bx-chevrons-left" : "bx bx-chevrons-right" )}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import { t } from "../../services/i18n.js";
|
|
||||||
import BasicWidget from "../basic_widget.js";
|
|
||||||
import utils from "../../services/utils.js";
|
|
||||||
|
|
||||||
const TPL = /*html*/`
|
|
||||||
<div style="display: none;">
|
|
||||||
<style>
|
|
||||||
.global-menu-button-update-available-button {
|
|
||||||
width: 21px !important;
|
|
||||||
height: 21px !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
|
|
||||||
border-radius: var(--button-border-radius);
|
|
||||||
transform: scale(0.9);
|
|
||||||
border: none;
|
|
||||||
opacity: 0.8;
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.global-menu-button-wrapper:hover .global-menu-button-update-available-button {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<span class="bx bx-sync global-menu-button-update-available-button" title="${t("update_available.update_available")}"></span>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default class UpdateAvailableWidget extends BasicWidget {
|
|
||||||
doRender() {
|
|
||||||
this.$widget = $(TPL);
|
|
||||||
}
|
|
||||||
|
|
||||||
updateVersionStatus(latestVersion: string) {
|
|
||||||
this.$widget.toggle(utils.isUpdateAvailable(latestVersion, glob.triliumVersion));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
26
apps/client/src/widgets/close_zen_button.css
Normal file
26
apps/client/src/widgets/close_zen_button.css
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
:root {
|
||||||
|
--zen-button-size: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-zen-container {
|
||||||
|
width: var(--zen-button-size);
|
||||||
|
height: var(--zen-button-size);
|
||||||
|
}
|
||||||
|
|
||||||
|
body.zen .close-zen-container {
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
top: 2px;
|
||||||
|
right: 2px;
|
||||||
|
z-index: 9999;
|
||||||
|
-webkit-app-region: no-drag;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.zen.mobile .close-zen-container {
|
||||||
|
top: -2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.zen.electron:not(.platform-darwin):not(.native-titlebar) .close-zen-container {
|
||||||
|
left: calc(env(titlebar-area-width) - var(--zen-button-size) - 2px);
|
||||||
|
right: unset;
|
||||||
|
}
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
import BasicWidget from "./basic_widget.js";
|
|
||||||
import { t } from "../services/i18n.js";
|
|
||||||
import utils from "../services/utils.js";
|
|
||||||
|
|
||||||
const TPL = /*html*/`\
|
|
||||||
<div class="close-zen-container">
|
|
||||||
<button class="button-widget bx icon-action bxs-yin-yang"
|
|
||||||
data-trigger-command="toggleZenMode"
|
|
||||||
title="${t("zen_mode.button_exit")}"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
--zen-button-size: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.close-zen-container {
|
|
||||||
display: none;
|
|
||||||
width: var(--zen-button-size);
|
|
||||||
height: var(--zen-button-size);
|
|
||||||
}
|
|
||||||
|
|
||||||
body.zen .close-zen-container {
|
|
||||||
display: block;
|
|
||||||
position: fixed;
|
|
||||||
top: 2px;
|
|
||||||
right: 2px;
|
|
||||||
z-index: 9999;
|
|
||||||
-webkit-app-region: no-drag;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.zen.mobile .close-zen-container {
|
|
||||||
top: -2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.zen.electron:not(.platform-darwin):not(.native-titlebar) .close-zen-container {
|
|
||||||
left: calc(env(titlebar-area-width) - var(--zen-button-size) - 2px);
|
|
||||||
right: unset;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default class CloseZenButton extends BasicWidget {
|
|
||||||
|
|
||||||
doRender(): void {
|
|
||||||
this.$widget = $(TPL);
|
|
||||||
}
|
|
||||||
|
|
||||||
zenChangedEvent() {
|
|
||||||
this.toggleInt(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
25
apps/client/src/widgets/close_zen_button.tsx
Normal file
25
apps/client/src/widgets/close_zen_button.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { useState } from "preact/hooks";
|
||||||
|
import { t } from "../services/i18n";
|
||||||
|
import ActionButton from "./react/ActionButton";
|
||||||
|
import { useTriliumEvent } from "./react/hooks";
|
||||||
|
import "./close_zen_button.css";
|
||||||
|
|
||||||
|
export default function CloseZenModeButton() {
|
||||||
|
const [ zenModeEnabled, setZenModeEnabled ] = useState(false);
|
||||||
|
|
||||||
|
useTriliumEvent("zenModeChanged", ({ isEnabled }) => {
|
||||||
|
setZenModeEnabled(isEnabled);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div class={`close-zen-container ${!zenModeEnabled ? "hidden-ext" : ""}`}>
|
||||||
|
{zenModeEnabled && (
|
||||||
|
<ActionButton
|
||||||
|
icon="bx bxs-yin-yang"
|
||||||
|
triggerCommand="toggleZenMode"
|
||||||
|
text={t("zen_mode.button_exit")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
import BasicWidget from "../basic_widget.js";
|
|
||||||
import appContext from "../../components/app_context.js";
|
|
||||||
import contextMenu from "../../menus/context_menu.js";
|
|
||||||
import noteCreateService from "../../services/note_create.js";
|
|
||||||
import branchService from "../../services/branches.js";
|
|
||||||
import treeService from "../../services/tree.js";
|
|
||||||
import { t } from "../../services/i18n.js";
|
|
||||||
|
|
||||||
const TPL = /*html*/`<button type="button" class="action-button bx"></button>`;
|
|
||||||
|
|
||||||
class MobileDetailMenuWidget extends BasicWidget {
|
|
||||||
private isHorizontalLayout: boolean;
|
|
||||||
|
|
||||||
constructor(isHorizontalLayout: boolean) {
|
|
||||||
super();
|
|
||||||
this.isHorizontalLayout = isHorizontalLayout;
|
|
||||||
}
|
|
||||||
|
|
||||||
doRender() {
|
|
||||||
this.$widget = $(TPL);
|
|
||||||
|
|
||||||
this.$widget.addClass(this.isHorizontalLayout ? "bx-dots-vertical-rounded" : "bx-menu");
|
|
||||||
|
|
||||||
this.$widget.on("click", async (e) => {
|
|
||||||
const note = appContext.tabManager.getActiveContextNote();
|
|
||||||
|
|
||||||
contextMenu.show<"insertChildNote" | "delete" | "showRevisions">({
|
|
||||||
x: e.pageX,
|
|
||||||
y: e.pageY,
|
|
||||||
items: [
|
|
||||||
{ title: t("mobile_detail_menu.insert_child_note"), command: "insertChildNote", uiIcon: "bx bx-plus", enabled: note?.type !== "search" },
|
|
||||||
{ title: t("mobile_detail_menu.delete_this_note"), command: "delete", uiIcon: "bx bx-trash", enabled: note?.noteId !== "root" },
|
|
||||||
{ title: "----" },
|
|
||||||
{ title: "Note revisions", command: "showRevisions", uiIcon: "bx bx-history" }
|
|
||||||
],
|
|
||||||
selectMenuItemHandler: async ({ command }) => {
|
|
||||||
if (command === "insertChildNote") {
|
|
||||||
noteCreateService.createNote(appContext.tabManager.getActiveContextNotePath() ?? undefined);
|
|
||||||
} else if (command === "delete") {
|
|
||||||
const notePath = appContext.tabManager.getActiveContextNotePath();
|
|
||||||
if (!notePath) {
|
|
||||||
throw new Error("Cannot get note path to delete.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const branchId = await treeService.getBranchIdFromUrl(notePath);
|
|
||||||
|
|
||||||
if (!branchId) {
|
|
||||||
throw new Error(t("mobile_detail_menu.error_cannot_get_branch_id", { notePath }));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await branchService.deleteNotes([branchId])) {
|
|
||||||
this.triggerCommand("setActiveScreen", { screen: "tree" });
|
|
||||||
}
|
|
||||||
} else if (command) {
|
|
||||||
this.triggerCommand(command);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
forcePositionOnMobile: true
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default MobileDetailMenuWidget;
|
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { useContext } from "preact/hooks";
|
||||||
|
import appContext from "../../components/app_context";
|
||||||
|
import contextMenu from "../../menus/context_menu";
|
||||||
|
import branches from "../../services/branches";
|
||||||
|
import { t } from "../../services/i18n";
|
||||||
|
import note_create from "../../services/note_create";
|
||||||
|
import tree from "../../services/tree";
|
||||||
|
import ActionButton from "../react/ActionButton";
|
||||||
|
import { ParentComponent } from "../react/react_utils";
|
||||||
|
|
||||||
|
export default function MobileDetailMenu() {
|
||||||
|
const parentComponent = useContext(ParentComponent);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionButton
|
||||||
|
icon="bx bx-dots-vertical-rounded"
|
||||||
|
text=""
|
||||||
|
onClick={(e) => {
|
||||||
|
const note = appContext.tabManager.getActiveContextNote();
|
||||||
|
|
||||||
|
contextMenu.show<"insertChildNote" | "delete" | "showRevisions">({
|
||||||
|
x: e.pageX,
|
||||||
|
y: e.pageY,
|
||||||
|
items: [
|
||||||
|
{ title: t("mobile_detail_menu.insert_child_note"), command: "insertChildNote", uiIcon: "bx bx-plus", enabled: note?.type !== "search" },
|
||||||
|
{ title: t("mobile_detail_menu.delete_this_note"), command: "delete", uiIcon: "bx bx-trash", enabled: note?.noteId !== "root" },
|
||||||
|
{ title: "----" },
|
||||||
|
{ title: "Note revisions", command: "showRevisions", uiIcon: "bx bx-history" }
|
||||||
|
],
|
||||||
|
selectMenuItemHandler: async ({ command }) => {
|
||||||
|
if (command === "insertChildNote") {
|
||||||
|
note_create.createNote(appContext.tabManager.getActiveContextNotePath() ?? undefined);
|
||||||
|
} else if (command === "delete") {
|
||||||
|
const notePath = appContext.tabManager.getActiveContextNotePath();
|
||||||
|
if (!notePath) {
|
||||||
|
throw new Error("Cannot get note path to delete.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const branchId = await tree.getBranchIdFromUrl(notePath);
|
||||||
|
|
||||||
|
if (!branchId) {
|
||||||
|
throw new Error(t("mobile_detail_menu.error_cannot_get_branch_id", { notePath }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (await branches.deleteNotes([branchId]) && parentComponent) {
|
||||||
|
parentComponent.triggerCommand("setActiveScreen", { screen: "tree" });
|
||||||
|
}
|
||||||
|
} else if (command && parentComponent) {
|
||||||
|
parentComponent.triggerCommand(command);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
forcePositionOnMobile: true
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import BasicWidget from "../basic_widget.js";
|
|
||||||
|
|
||||||
const TPL = /*html*/`
|
|
||||||
<button type="button" class="action-button bx bx-sidebar"></button>`;
|
|
||||||
|
|
||||||
class ToggleSidebarButtonWidget extends BasicWidget {
|
|
||||||
doRender() {
|
|
||||||
this.$widget = $(TPL);
|
|
||||||
|
|
||||||
this.$widget.on("click", () =>
|
|
||||||
this.triggerCommand("setActiveScreen", {
|
|
||||||
screen: "tree"
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ToggleSidebarButtonWidget;
|
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { useContext } from "preact/hooks";
|
||||||
|
import ActionButton from "../react/ActionButton";
|
||||||
|
import { ParentComponent } from "../react/react_utils";
|
||||||
|
import { t } from "../../services/i18n";
|
||||||
|
|
||||||
|
export default function ToggleSidebarButton() {
|
||||||
|
const parentComponent = useContext(ParentComponent);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ActionButton
|
||||||
|
icon="bx bx-sidebar"
|
||||||
|
text={t("note_tree.toggle-sidebar")}
|
||||||
|
onClick={() => parentComponent?.triggerCommand("setActiveScreen", {
|
||||||
|
screen: "tree"
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import keyboard_actions from "../../services/keyboard_actions";
|
|||||||
|
|
||||||
export interface ActionButtonProps {
|
export interface ActionButtonProps {
|
||||||
text: string;
|
text: string;
|
||||||
titlePosition?: "bottom" | "left"; // TODO: Use it
|
titlePosition?: "bottom" | "left";
|
||||||
icon: string;
|
icon: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
onClick?: (e: MouseEvent) => void;
|
onClick?: (e: MouseEvent) => void;
|
||||||
@@ -25,7 +25,7 @@ export default function ActionButton({ text, icon, className, onClick, triggerCo
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (triggerCommand) {
|
if (triggerCommand) {
|
||||||
keyboard_actions.getAction(triggerCommand).then(action => setKeyboardShortcut(action?.effectiveShortcuts));
|
keyboard_actions.getAction(triggerCommand, true).then(action => setKeyboardShortcut(action?.effectiveShortcuts));
|
||||||
}
|
}
|
||||||
}, [triggerCommand]);
|
}, [triggerCommand]);
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ interface AlertProps {
|
|||||||
type: "info" | "danger" | "warning";
|
type: "info" | "danger" | "warning";
|
||||||
title?: string;
|
title?: string;
|
||||||
children: ComponentChildren;
|
children: ComponentChildren;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Alert({ title, type, children }: AlertProps) {
|
export default function Alert({ title, type, children, className }: AlertProps) {
|
||||||
return (
|
return (
|
||||||
<div className={`alert alert-${type}`}>
|
<div className={`alert alert-${type} ${className ?? ""}`}>
|
||||||
{title && <h4>{title}</h4>}
|
{title && <h4>{title}</h4>}
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -18,9 +18,10 @@ export interface DropdownProps {
|
|||||||
noSelectButtonStyle?: boolean;
|
noSelectButtonStyle?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
text?: ComponentChildren;
|
text?: ComponentChildren;
|
||||||
|
forceShown?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Dropdown({ className, buttonClassName, isStatic, children, title, text, dropdownContainerStyle, dropdownContainerClassName, hideToggleArrow, iconAction, disabled, noSelectButtonStyle }: DropdownProps) {
|
export default function Dropdown({ className, buttonClassName, isStatic, children, title, text, dropdownContainerStyle, dropdownContainerClassName, hideToggleArrow, iconAction, disabled, noSelectButtonStyle, forceShown }: DropdownProps) {
|
||||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||||
const triggerRef = useRef<HTMLButtonElement | null>(null);
|
const triggerRef = useRef<HTMLButtonElement | null>(null);
|
||||||
|
|
||||||
@@ -30,8 +31,12 @@ export default function Dropdown({ className, buttonClassName, isStatic, childre
|
|||||||
if (!triggerRef.current) return;
|
if (!triggerRef.current) return;
|
||||||
|
|
||||||
const dropdown = BootstrapDropdown.getOrCreateInstance(triggerRef.current);
|
const dropdown = BootstrapDropdown.getOrCreateInstance(triggerRef.current);
|
||||||
|
if (forceShown) {
|
||||||
|
dropdown.show();
|
||||||
|
setShown(true);
|
||||||
|
}
|
||||||
return () => dropdown.dispose();
|
return () => dropdown.dispose();
|
||||||
}, []); // Add dependency array
|
}, []);
|
||||||
|
|
||||||
const onShown = useCallback(() => {
|
const onShown = useCallback(() => {
|
||||||
setShown(true);
|
setShown(true);
|
||||||
@@ -75,13 +80,13 @@ export default function Dropdown({ className, buttonClassName, isStatic, childre
|
|||||||
<span className="caret"></span>
|
<span className="caret"></span>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<div
|
<ul
|
||||||
class={`dropdown-menu ${isStatic ? "static" : ""} ${dropdownContainerClassName ?? ""} tn-dropdown-list`}
|
class={`dropdown-menu ${isStatic ? "static" : ""} ${dropdownContainerClassName ?? ""} tn-dropdown-list`}
|
||||||
style={dropdownContainerStyle}
|
style={dropdownContainerStyle}
|
||||||
aria-labelledby={ariaId}
|
aria-labelledby={ariaId}
|
||||||
>
|
>
|
||||||
{shown && children}
|
{shown && children}
|
||||||
</div>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import { Dropdown as BootstrapDropdown } from "bootstrap";
|
import { Dropdown as BootstrapDropdown, Tooltip } from "bootstrap";
|
||||||
import { ComponentChildren } from "preact";
|
import { ComponentChildren } from "preact";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
import { useEffect, useMemo, useRef, type CSSProperties } from "preact/compat";
|
import { useEffect, useMemo, useRef, useState, type CSSProperties } from "preact/compat";
|
||||||
import "./FormList.css";
|
import "./FormList.css";
|
||||||
import { CommandNames } from "../../components/app_context";
|
import { CommandNames } from "../../components/app_context";
|
||||||
|
import { useStaticTooltip } from "./hooks";
|
||||||
|
import { isMobile } from "../../services/utils";
|
||||||
|
|
||||||
interface FormListOpts {
|
interface FormListOpts {
|
||||||
children: ComponentChildren;
|
children: ComponentChildren;
|
||||||
@@ -89,14 +91,24 @@ interface FormListItemOpts {
|
|||||||
rtl?: boolean;
|
rtl?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function FormListItem({ children, icon, value, title, active, badges, disabled, checked, onClick, description, selected, rtl, triggerCommand }: FormListItemOpts) {
|
const TOOLTIP_CONFIG: Partial<Tooltip.Options> = {
|
||||||
|
placement: "right",
|
||||||
|
fallbackPlacements: [ "right" ]
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FormListItem({ className, icon, value, title, active, disabled, checked, onClick, selected, rtl, triggerCommand, description, ...contentProps }: FormListItemOpts) {
|
||||||
|
const itemRef = useRef<HTMLLIElement>(null);
|
||||||
|
|
||||||
if (checked) {
|
if (checked) {
|
||||||
icon = "bx bx-check";
|
icon = "bx bx-check";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useStaticTooltip(itemRef, TOOLTIP_CONFIG);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a
|
<li
|
||||||
class={`dropdown-item ${active ? "active" : ""} ${disabled ? "disabled" : ""} ${selected ? "selected" : ""}`}
|
ref={itemRef}
|
||||||
|
class={`dropdown-item ${active ? "active" : ""} ${disabled ? "disabled" : ""} ${selected ? "selected" : ""} ${className ?? ""}`}
|
||||||
data-value={value} title={title}
|
data-value={value} title={title}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
@@ -104,17 +116,27 @@ export function FormListItem({ children, icon, value, title, active, badges, dis
|
|||||||
dir={rtl ? "rtl" : undefined}
|
dir={rtl ? "rtl" : undefined}
|
||||||
>
|
>
|
||||||
<Icon icon={icon} />
|
<Icon icon={icon} />
|
||||||
<div>
|
{description ? (
|
||||||
{children}
|
<div>
|
||||||
{badges && badges.map(({ className, text }) => (
|
<FormListContent description={description} {...contentProps} />
|
||||||
<span className={`badge ${className ?? ""}`}>{text}</span>
|
</div>
|
||||||
))}
|
) : (
|
||||||
{description && <div className="description">{description}</div>}
|
<FormListContent description={description} {...contentProps} />
|
||||||
</div>
|
)}
|
||||||
</a>
|
</li>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function FormListContent({ children, badges, description }: Pick<FormListItemOpts, "children" | "badges" | "description">) {
|
||||||
|
return <>
|
||||||
|
{children}
|
||||||
|
{badges && badges.map(({ className, text }) => (
|
||||||
|
<span className={`badge ${className ?? ""}`}>{text}</span>
|
||||||
|
))}
|
||||||
|
{description && <div className="description">{description}</div>}
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
|
||||||
interface FormListHeaderOpts {
|
interface FormListHeaderOpts {
|
||||||
text: string;
|
text: string;
|
||||||
}
|
}
|
||||||
@@ -129,4 +151,30 @@ export function FormListHeader({ text }: FormListHeaderOpts) {
|
|||||||
|
|
||||||
export function FormDropdownDivider() {
|
export function FormDropdownDivider() {
|
||||||
return <div className="dropdown-divider" />;
|
return <div className="dropdown-divider" />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function FormDropdownSubmenu({ icon, title, children }: { icon: string, title: ComponentChildren, children: ComponentChildren }) {
|
||||||
|
const [ openOnMobile, setOpenOnMobile ] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li className={`dropdown-item dropdown-submenu ${openOnMobile ? "submenu-open" : ""}`}>
|
||||||
|
<span
|
||||||
|
className="dropdown-toggle"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (isMobile()) {
|
||||||
|
setOpenOnMobile(!openOnMobile);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon icon={icon} />{" "}
|
||||||
|
{title}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<ul className={`dropdown-menu ${openOnMobile ? "show" : ""}`}>
|
||||||
|
{children}
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -2,11 +2,14 @@ import { ActionKeyboardShortcut, KeyboardActionNames } from "@triliumnext/common
|
|||||||
import { useEffect, useState } from "preact/hooks";
|
import { useEffect, useState } from "preact/hooks";
|
||||||
import keyboard_actions from "../../services/keyboard_actions";
|
import keyboard_actions from "../../services/keyboard_actions";
|
||||||
import { joinElements } from "./react_utils";
|
import { joinElements } from "./react_utils";
|
||||||
|
import utils from "../../services/utils";
|
||||||
|
|
||||||
interface KeyboardShortcutProps {
|
interface KeyboardShortcutProps {
|
||||||
actionName: KeyboardActionNames;
|
actionName: KeyboardActionNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isMobile = utils.isMobile();
|
||||||
|
|
||||||
export default function KeyboardShortcut({ actionName }: KeyboardShortcutProps) {
|
export default function KeyboardShortcut({ actionName }: KeyboardShortcutProps) {
|
||||||
|
|
||||||
const [ action, setAction ] = useState<ActionKeyboardShortcut>();
|
const [ action, setAction ] = useState<ActionKeyboardShortcut>();
|
||||||
@@ -18,17 +21,14 @@ export default function KeyboardShortcut({ actionName }: KeyboardShortcutProps)
|
|||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (!isMobile &&
|
||||||
<>
|
<span className="keyboard-shortcut">
|
||||||
{action.effectiveShortcuts?.map((shortcut) => {
|
{joinElements(action.effectiveShortcuts?.map((shortcut) => {
|
||||||
const keys = shortcut.split("+");
|
const keys = shortcut.split("+");
|
||||||
return joinElements(keys
|
return joinElements(
|
||||||
.map((key, i) => (
|
keys.map((key, i) => <kbd>{key}</kbd>)
|
||||||
<>
|
, "+");
|
||||||
<kbd>{key}</kbd> {i + 1 < keys.length && "+ "}
|
}))}
|
||||||
</>
|
</span>
|
||||||
)))
|
|
||||||
})}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@ import { useCallback, useContext, useDebugValue, useEffect, useLayoutEffect, use
|
|||||||
import { EventData, EventNames } from "../../components/app_context";
|
import { EventData, EventNames } from "../../components/app_context";
|
||||||
import { ParentComponent } from "./react_utils";
|
import { ParentComponent } from "./react_utils";
|
||||||
import SpacedUpdate from "../../services/spaced_update";
|
import SpacedUpdate from "../../services/spaced_update";
|
||||||
import { OptionNames } from "@triliumnext/commons";
|
import { KeyboardActionNames, OptionNames } from "@triliumnext/commons";
|
||||||
import options, { type OptionValue } from "../../services/options";
|
import options, { type OptionValue } from "../../services/options";
|
||||||
import utils, { reloadFrontendApp } from "../../services/utils";
|
import utils, { reloadFrontendApp } from "../../services/utils";
|
||||||
import NoteContext from "../../components/note_context";
|
import NoteContext from "../../components/note_context";
|
||||||
@@ -14,6 +14,7 @@ import NoteContextAwareWidget from "../note_context_aware_widget";
|
|||||||
import { RefObject, VNode } from "preact";
|
import { RefObject, VNode } from "preact";
|
||||||
import { Tooltip } from "bootstrap";
|
import { Tooltip } from "bootstrap";
|
||||||
import { CSSProperties } from "preact/compat";
|
import { CSSProperties } from "preact/compat";
|
||||||
|
import keyboard_actions from "../../services/keyboard_actions";
|
||||||
|
|
||||||
export function useTriliumEvent<T extends EventNames>(eventName: T, handler: (data: EventData<T>) => void) {
|
export function useTriliumEvent<T extends EventNames>(eventName: T, handler: (data: EventData<T>) => void) {
|
||||||
const parentComponent = useContext(ParentComponent);
|
const parentComponent = useContext(ParentComponent);
|
||||||
@@ -502,9 +503,10 @@ export function useTooltip(elRef: RefObject<HTMLElement>, config: Partial<Toolti
|
|||||||
* @param elRef the element to bind the tooltip to.
|
* @param elRef the element to bind the tooltip to.
|
||||||
* @param config optionally, the tooltip configuration.
|
* @param config optionally, the tooltip configuration.
|
||||||
*/
|
*/
|
||||||
export function useStaticTooltip(elRef: RefObject<HTMLElement>, config?: Partial<Tooltip.Options>) {
|
export function useStaticTooltip(elRef: RefObject<Element>, config?: Partial<Tooltip.Options>) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!elRef?.current) return;
|
const hasTooltip = config?.title || elRef.current?.getAttribute("title");
|
||||||
|
if (!elRef?.current || !hasTooltip) return;
|
||||||
|
|
||||||
const $el = $(elRef.current);
|
const $el = $(elRef.current);
|
||||||
$el.tooltip(config);
|
$el.tooltip(config);
|
||||||
@@ -514,6 +516,19 @@ export function useStaticTooltip(elRef: RefObject<HTMLElement>, config?: Partial
|
|||||||
}, [ elRef, config ]);
|
}, [ elRef, config ]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function useStaticTooltipWithKeyboardShortcut(elRef: RefObject<Element>, title: string, actionName: KeyboardActionNames | undefined) {
|
||||||
|
const [ keyboardShortcut, setKeyboardShortcut ] = useState<string[]>();
|
||||||
|
useStaticTooltip(elRef, {
|
||||||
|
title: keyboardShortcut?.length ? `${title} (${keyboardShortcut?.join(",")})` : title
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (actionName) {
|
||||||
|
keyboard_actions.getAction(actionName).then(action => setKeyboardShortcut(action?.effectiveShortcuts));
|
||||||
|
}
|
||||||
|
}, [actionName]);
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
||||||
export function useLegacyImperativeHandlers(handlers: Record<string, Function>) {
|
export function useLegacyImperativeHandlers(handlers: Record<string, Function>) {
|
||||||
const parentComponent = useContext(ParentComponent);
|
const parentComponent = useContext(ParentComponent);
|
||||||
|
|||||||
@@ -41,7 +41,9 @@ export function disposeReactWidget(container: Element) {
|
|||||||
render(null, container);
|
render(null, container);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function joinElements(components: ComponentChild[], separator = ", ") {
|
export function joinElements(components: ComponentChild[] | undefined, separator = ", ") {
|
||||||
|
if (!components) return <></>;
|
||||||
|
|
||||||
const joinedComponents: ComponentChild[] = [];
|
const joinedComponents: ComponentChild[] = [];
|
||||||
for (let i=0; i<components.length; i++) {
|
for (let i=0; i<components.length; i++) {
|
||||||
joinedComponents.push(components[i]);
|
joinedComponents.push(components[i]);
|
||||||
@@ -50,5 +52,5 @@ export function joinElements(components: ComponentChild[], separator = ", ") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return joinedComponents;
|
return <>{joinedComponents}</>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||||
import { t } from "../../services/i18n";
|
import { t } from "../../services/i18n";
|
||||||
import { useNoteContext, useNoteProperty, useStaticTooltip, useTooltip, useTriliumEvent, useTriliumEvents } from "../react/hooks";
|
import { useNoteContext, useNoteProperty, useStaticTooltip, useStaticTooltipWithKeyboardShortcut, useTooltip, useTriliumEvent, useTriliumEvents } from "../react/hooks";
|
||||||
import "./style.css";
|
import "./style.css";
|
||||||
import { VNode } from "preact";
|
import { VNode } from "preact";
|
||||||
import BasicPropertiesTab from "./BasicPropertiesTab";
|
import BasicPropertiesTab from "./BasicPropertiesTab";
|
||||||
@@ -252,16 +252,7 @@ export default function Ribbon() {
|
|||||||
|
|
||||||
function RibbonTab({ icon, title, active, onClick, toggleCommand }: { icon: string; title: string; active: boolean, onClick: () => void, toggleCommand?: KeyboardActionNames }) {
|
function RibbonTab({ icon, title, active, onClick, toggleCommand }: { icon: string; title: string; active: boolean, onClick: () => void, toggleCommand?: KeyboardActionNames }) {
|
||||||
const iconRef = useRef<HTMLDivElement>(null);
|
const iconRef = useRef<HTMLDivElement>(null);
|
||||||
const [ keyboardShortcut, setKeyboardShortcut ] = useState<string[]>();
|
useStaticTooltipWithKeyboardShortcut(iconRef, title, toggleCommand);
|
||||||
useStaticTooltip(iconRef, {
|
|
||||||
title: keyboardShortcut?.length ? `${title} (${keyboardShortcut?.join(",")})` : title
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (toggleCommand) {
|
|
||||||
keyboard_actions.getAction(toggleCommand).then(action => setKeyboardShortcut(action?.effectiveShortcuts));
|
|
||||||
}
|
|
||||||
}, [toggleCommand]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
|
||||||
|
|
||||||
const TPL = /*html*/`<div class="scroll-padding-widget"></div>`;
|
|
||||||
|
|
||||||
export default class ScrollPaddingWidget extends NoteContextAwareWidget {
|
|
||||||
|
|
||||||
private $scrollingContainer!: JQuery<HTMLElement>;
|
|
||||||
|
|
||||||
isEnabled() {
|
|
||||||
return super.isEnabled() && ["text", "code"].includes(this.note?.type ?? "");
|
|
||||||
}
|
|
||||||
|
|
||||||
doRender() {
|
|
||||||
this.$widget = $(TPL);
|
|
||||||
this.contentSized();
|
|
||||||
|
|
||||||
this.$widget.on("click", () => this.triggerCommand("scrollToEnd", { ntxId: this.ntxId }));
|
|
||||||
}
|
|
||||||
|
|
||||||
initialRenderCompleteEvent() {
|
|
||||||
this.$scrollingContainer = this.$widget.closest(".scrolling-container");
|
|
||||||
|
|
||||||
new ResizeObserver(() => this.refreshHeight()).observe(this.$scrollingContainer[0]);
|
|
||||||
|
|
||||||
this.refreshHeight();
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshHeight() {
|
|
||||||
const containerHeight = this.$scrollingContainer.height();
|
|
||||||
|
|
||||||
this.$widget.css("height", Math.round((containerHeight ?? 0) / 2));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
42
apps/client/src/widgets/scroll_padding.tsx
Normal file
42
apps/client/src/widgets/scroll_padding.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { useEffect, useRef, useState } from "preact/hooks";
|
||||||
|
import { useNoteContext } from "./react/hooks";
|
||||||
|
|
||||||
|
export default function ScrollPadding() {
|
||||||
|
const { note, parentComponent, ntxId } = useNoteContext();
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const [height, setHeight] = useState<number>(10);
|
||||||
|
const isEnabled = ["text", "code"].includes(note?.type ?? "");
|
||||||
|
|
||||||
|
const refreshHeight = () => {
|
||||||
|
if (!ref.current) return;
|
||||||
|
const container = ref.current.closest(".scrolling-container") as HTMLElement | null;
|
||||||
|
if (!container) return;
|
||||||
|
setHeight(Math.round(container.offsetHeight / 2));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isEnabled) return;
|
||||||
|
|
||||||
|
const container = ref.current?.closest(".scrolling-container") as HTMLElement | null;
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
// Observe container resize
|
||||||
|
const observer = new ResizeObserver(() => refreshHeight());
|
||||||
|
observer.observe(container);
|
||||||
|
|
||||||
|
// Initial resize
|
||||||
|
refreshHeight();
|
||||||
|
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, [note]); // re-run when note changes
|
||||||
|
|
||||||
|
return (isEnabled ?
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className="scroll-padding-widget"
|
||||||
|
style={{ height }}
|
||||||
|
onClick={() => parentComponent.triggerCommand("scrollToEnd", { ntxId })}
|
||||||
|
/>
|
||||||
|
: <div></div>
|
||||||
|
)
|
||||||
|
}
|
||||||
16
apps/client/src/widgets/search_result.css
Normal file
16
apps/client/src/widgets/search_result.css
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
.search-result-widget {
|
||||||
|
flex-grow: 100000;
|
||||||
|
flex-shrink: 100000;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
contain: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-result-widget .note-list {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-no-results, .search-not-executed-yet {
|
||||||
|
margin: 20px;
|
||||||
|
padding: 20px !important;
|
||||||
|
}
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
import { t } from "../services/i18n.js";
|
|
||||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
|
||||||
import NoteListRenderer from "../services/note_list_renderer.js";
|
|
||||||
import type FNote from "../entities/fnote.js";
|
|
||||||
import type { EventData } from "../components/app_context.js";
|
|
||||||
|
|
||||||
const TPL = /*html*/`
|
|
||||||
<div class="search-result-widget">
|
|
||||||
<style>
|
|
||||||
.search-result-widget {
|
|
||||||
flex-grow: 100000;
|
|
||||||
flex-shrink: 100000;
|
|
||||||
min-height: 0;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-result-widget .note-list {
|
|
||||||
padding: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-no-results, .search-not-executed-yet {
|
|
||||||
margin: 20px;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div class="search-no-results alert alert-info">
|
|
||||||
${t("search_result.no_notes_found")}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="search-not-executed-yet alert alert-info">
|
|
||||||
${t("search_result.search_not_executed")}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="search-result-widget-content">
|
|
||||||
</div>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
export default class SearchResultWidget extends NoteContextAwareWidget {
|
|
||||||
|
|
||||||
private $content!: JQuery<HTMLElement>;
|
|
||||||
private $noResults!: JQuery<HTMLElement>;
|
|
||||||
private $notExecutedYet!: JQuery<HTMLElement>;
|
|
||||||
|
|
||||||
isEnabled() {
|
|
||||||
return super.isEnabled() && this.note?.type === "search";
|
|
||||||
}
|
|
||||||
|
|
||||||
doRender() {
|
|
||||||
this.$widget = $(TPL);
|
|
||||||
this.contentSized();
|
|
||||||
this.$content = this.$widget.find(".search-result-widget-content");
|
|
||||||
this.$noResults = this.$widget.find(".search-no-results");
|
|
||||||
this.$notExecutedYet = this.$widget.find(".search-not-executed-yet");
|
|
||||||
}
|
|
||||||
|
|
||||||
async refreshWithNote(note: FNote) {
|
|
||||||
const noResults = note.getChildNoteIds().length === 0 && !!note.searchResultsLoaded;
|
|
||||||
|
|
||||||
this.$content.empty();
|
|
||||||
this.$noResults.toggle(noResults);
|
|
||||||
this.$notExecutedYet.toggle(!note.searchResultsLoaded);
|
|
||||||
|
|
||||||
if (noResults || !note.searchResultsLoaded) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const noteListRenderer = new NoteListRenderer({
|
|
||||||
$parent: this.$content,
|
|
||||||
parentNote: note,
|
|
||||||
showNotePath: true
|
|
||||||
});
|
|
||||||
await noteListRenderer.renderList();
|
|
||||||
}
|
|
||||||
|
|
||||||
searchRefreshedEvent({ ntxId }: EventData<"searchRefreshed">) {
|
|
||||||
if (!this.isNoteContext(ntxId)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
notesReloadedEvent({ noteIds }: EventData<"notesReloaded">) {
|
|
||||||
if (this.noteId && noteIds.includes(this.noteId)) {
|
|
||||||
this.refresh();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
65
apps/client/src/widgets/search_result.tsx
Normal file
65
apps/client/src/widgets/search_result.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { useEffect, useRef, useState } from "preact/hooks";
|
||||||
|
import { t } from "../services/i18n";
|
||||||
|
import Alert from "./react/Alert";
|
||||||
|
import { useNoteContext, useNoteProperty, useTriliumEvent } from "./react/hooks";
|
||||||
|
import "./search_result.css";
|
||||||
|
import NoteListRenderer from "../services/note_list_renderer";
|
||||||
|
|
||||||
|
enum SearchResultState {
|
||||||
|
NO_RESULTS,
|
||||||
|
NOT_EXECUTED,
|
||||||
|
GOT_RESULTS
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function SearchResult() {
|
||||||
|
const { note, ntxId } = useNoteContext();
|
||||||
|
const [ state, setState ] = useState<SearchResultState>();
|
||||||
|
const searchContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
function refresh() {
|
||||||
|
searchContainerRef.current?.replaceChildren();
|
||||||
|
|
||||||
|
if (note?.type !== "search") {
|
||||||
|
setState(undefined);
|
||||||
|
} else if (!note?.searchResultsLoaded) {
|
||||||
|
setState(SearchResultState.NOT_EXECUTED);
|
||||||
|
} else if (note.getChildNoteIds().length === 0) {
|
||||||
|
setState(SearchResultState.NO_RESULTS);
|
||||||
|
} else if (searchContainerRef.current) {
|
||||||
|
setState(SearchResultState.GOT_RESULTS);
|
||||||
|
|
||||||
|
const noteListRenderer = new NoteListRenderer({
|
||||||
|
$parent: $(searchContainerRef.current),
|
||||||
|
parentNote: note,
|
||||||
|
showNotePath: true
|
||||||
|
});
|
||||||
|
noteListRenderer.renderList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => refresh(), [ note ]);
|
||||||
|
useTriliumEvent("searchRefreshed", ({ ntxId: eventNtxId }) => {
|
||||||
|
if (eventNtxId === ntxId) {
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
useTriliumEvent("notesReloaded", ({ noteIds }) => {
|
||||||
|
if (note?.noteId && noteIds.includes(note.noteId)) {
|
||||||
|
refresh();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="search-result-widget">
|
||||||
|
{state === SearchResultState.NOT_EXECUTED && (
|
||||||
|
<Alert type="info" className="search-not-executed-yet">{t("search_result.search_not_executed")}</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{state === SearchResultState.NO_RESULTS && (
|
||||||
|
<Alert type="info" className="search-no-results">{t("search_result.no_notes_found")}</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div ref={searchContainerRef} className="search-result-widget-content" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
7
apps/client/src/widgets/sql_result.css
Normal file
7
apps/client/src/widgets/sql_result.css
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
.sql-result-widget {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sql-console-result-container td {
|
||||||
|
white-space: preserve;
|
||||||
|
}
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
import type { EventData } from "../components/app_context.js";
|
|
||||||
import { t } from "../services/i18n.js";
|
|
||||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
|
||||||
|
|
||||||
const TPL = /*html*/`
|
|
||||||
<div class="sql-result-widget">
|
|
||||||
<style>
|
|
||||||
.sql-result-widget {
|
|
||||||
padding: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sql-console-result-container td {
|
|
||||||
white-space: preserve;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<div class="sql-query-no-rows alert alert-info" style="display: none;">
|
|
||||||
${t("sql_result.no_rows")}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="sql-console-result-container"></div>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
export default class SqlResultWidget extends NoteContextAwareWidget {
|
|
||||||
|
|
||||||
private $resultContainer!: JQuery<HTMLElement>;
|
|
||||||
private $noRowsAlert!: JQuery<HTMLElement>;
|
|
||||||
|
|
||||||
isEnabled() {
|
|
||||||
return this.note && this.note.mime === "text/x-sqlite;schema=trilium" && super.isEnabled();
|
|
||||||
}
|
|
||||||
|
|
||||||
doRender() {
|
|
||||||
this.$widget = $(TPL);
|
|
||||||
|
|
||||||
this.$resultContainer = this.$widget.find(".sql-console-result-container");
|
|
||||||
this.$noRowsAlert = this.$widget.find(".sql-query-no-rows");
|
|
||||||
}
|
|
||||||
|
|
||||||
async sqlQueryResultsEvent({ ntxId, results }: EventData<"sqlQueryResults">) {
|
|
||||||
if (!this.isNoteContext(ntxId)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$noRowsAlert.toggle(results.length === 1 && results[0].length === 0);
|
|
||||||
this.$resultContainer.toggle(results.length > 1 || results[0].length > 0);
|
|
||||||
|
|
||||||
this.$resultContainer.empty();
|
|
||||||
|
|
||||||
for (const rows of results) {
|
|
||||||
if (typeof rows === "object" && !Array.isArray(rows)) {
|
|
||||||
// inserts, updates
|
|
||||||
this.$resultContainer
|
|
||||||
.empty()
|
|
||||||
.show()
|
|
||||||
.append($("<pre>").text(JSON.stringify(rows, null, "\t")));
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!rows.length) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const $table = $('<table class="table table-striped">');
|
|
||||||
this.$resultContainer.append($table);
|
|
||||||
|
|
||||||
const result = rows[0];
|
|
||||||
const $row = $("<tr>");
|
|
||||||
|
|
||||||
for (const key in result) {
|
|
||||||
$row.append($("<th>").text(key));
|
|
||||||
}
|
|
||||||
|
|
||||||
$table.append($row);
|
|
||||||
|
|
||||||
for (const result of rows) {
|
|
||||||
const $row = $("<tr>");
|
|
||||||
|
|
||||||
for (const key in result) {
|
|
||||||
$row.append($("<td>").text(result[key]));
|
|
||||||
}
|
|
||||||
|
|
||||||
$table.append($row);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
62
apps/client/src/widgets/sql_result.tsx
Normal file
62
apps/client/src/widgets/sql_result.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { SqlExecuteResults } from "@triliumnext/commons";
|
||||||
|
import { useNoteContext, useTriliumEvent } from "./react/hooks";
|
||||||
|
import "./sql_result.css";
|
||||||
|
import { useState } from "preact/hooks";
|
||||||
|
import Alert from "./react/Alert";
|
||||||
|
import { t } from "../services/i18n";
|
||||||
|
|
||||||
|
export default function SqlResults() {
|
||||||
|
const { note, ntxId } = useNoteContext();
|
||||||
|
const [ results, setResults ] = useState<SqlExecuteResults>();
|
||||||
|
|
||||||
|
useTriliumEvent("sqlQueryResults", ({ ntxId: eventNtxId, results }) => {
|
||||||
|
if (eventNtxId !== ntxId) return;
|
||||||
|
setResults(results);
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="sql-result-widget">
|
||||||
|
{note?.mime === "text/x-sqlite;schema=trilium" && (
|
||||||
|
results?.length === 1 && Array.isArray(results[0]) && results[0].length === 0 ? (
|
||||||
|
<Alert type="info">
|
||||||
|
{t("sql_result.no_rows")}
|
||||||
|
</Alert>
|
||||||
|
) : (
|
||||||
|
<div class="sql-console-result-container">
|
||||||
|
{results?.map(rows => {
|
||||||
|
// inserts, updates
|
||||||
|
if (typeof rows === "object" && !Array.isArray(rows)) {
|
||||||
|
return <pre>{JSON.stringify(rows, null, "\t")}</pre>
|
||||||
|
}
|
||||||
|
|
||||||
|
// selects
|
||||||
|
return <SqlResultTable rows={rows} />
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function SqlResultTable({ rows }: { rows: object[] }) {
|
||||||
|
if (!rows.length) return;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<table className="table table-striped">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{Object.keys(rows[0]).map(key => <th>{key}</th>)}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{rows.map(row => (
|
||||||
|
<tr>
|
||||||
|
{Object.values(row).map(cell => <td>{cell}</td>)}
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
)
|
||||||
|
}
|
||||||
43
apps/client/src/widgets/sql_table_schemas.css
Normal file
43
apps/client/src/widgets/sql_table_schemas.css
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
.sql-table-schemas-widget {
|
||||||
|
padding: 12px;
|
||||||
|
padding-right: 10%;
|
||||||
|
contain: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sql-table-schemas > .dropdown {
|
||||||
|
display: inline-block !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sql-table-schemas button.btn {
|
||||||
|
padding: 0.25rem 0.4rem;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 0.5;
|
||||||
|
border: 1px solid var(--button-border-color);
|
||||||
|
border-radius: var(--button-border-radius);
|
||||||
|
background: var(--button-background-color);
|
||||||
|
color: var(--button-text-color);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sql-console-result-container {
|
||||||
|
width: 100%;
|
||||||
|
font-size: smaller;
|
||||||
|
margin-top: 10px;
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow: auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-schema td {
|
||||||
|
padding: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown .table-schema {
|
||||||
|
font-family: var(--monospace-font-family);
|
||||||
|
font-size: .85em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Data type */
|
||||||
|
.dropdown .table-schema td:nth-child(2) {
|
||||||
|
color: var(--muted-text-color);
|
||||||
|
}
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import { t } from "../services/i18n.js";
|
|
||||||
import NoteContextAwareWidget from "./note_context_aware_widget.js";
|
|
||||||
import server from "../services/server.js";
|
|
||||||
import type FNote from "../entities/fnote.js";
|
|
||||||
|
|
||||||
const TPL = /*html*/`
|
|
||||||
<div class="sql-table-schemas-widget">
|
|
||||||
<style>
|
|
||||||
.sql-table-schemas-widget {
|
|
||||||
padding: 12px;
|
|
||||||
padding-right: 10%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sql-table-schemas button {
|
|
||||||
padding: 0.25rem 0.4rem;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
line-height: 0.5;
|
|
||||||
border: 1px solid var(--button-border-color);
|
|
||||||
border-radius: var(--button-border-radius);
|
|
||||||
background: var(--button-background-color);
|
|
||||||
color: var(--button-text-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.sql-console-result-container {
|
|
||||||
width: 100%;
|
|
||||||
font-size: smaller;
|
|
||||||
margin-top: 10px;
|
|
||||||
flex-grow: 1;
|
|
||||||
overflow: auto;
|
|
||||||
min-height: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.table-schema td {
|
|
||||||
padding: 5px;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
${t("sql_table_schemas.tables")}:
|
|
||||||
<span class="sql-table-schemas"></span>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
interface SchemaResponse {
|
|
||||||
name: string;
|
|
||||||
columns: {
|
|
||||||
name: string;
|
|
||||||
type: string;
|
|
||||||
}[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class SqlTableSchemasWidget extends NoteContextAwareWidget {
|
|
||||||
|
|
||||||
private tableSchemasShown?: boolean;
|
|
||||||
private $sqlConsoleTableSchemas!: JQuery<HTMLElement>;
|
|
||||||
|
|
||||||
isEnabled() {
|
|
||||||
return this.note && this.note.mime === "text/x-sqlite;schema=trilium" && super.isEnabled();
|
|
||||||
}
|
|
||||||
|
|
||||||
doRender() {
|
|
||||||
this.$widget = $(TPL);
|
|
||||||
this.contentSized();
|
|
||||||
|
|
||||||
this.$sqlConsoleTableSchemas = this.$widget.find(".sql-table-schemas");
|
|
||||||
}
|
|
||||||
|
|
||||||
async refreshWithNote(note: FNote) {
|
|
||||||
if (this.tableSchemasShown) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.tableSchemasShown = true;
|
|
||||||
|
|
||||||
const tableSchema = await server.get<SchemaResponse[]>("sql/schema");
|
|
||||||
|
|
||||||
for (const table of tableSchema) {
|
|
||||||
const $tableLink = $('<button class="btn">').text(table.name);
|
|
||||||
|
|
||||||
const $table = $('<table class="table-schema">');
|
|
||||||
|
|
||||||
for (const column of table.columns) {
|
|
||||||
$table.append($("<tr>").append($("<td>").text(column.name)).append($("<td>").text(column.type)));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$sqlConsoleTableSchemas.append($tableLink).append(" ");
|
|
||||||
|
|
||||||
$tableLink.tooltip({
|
|
||||||
html: true,
|
|
||||||
placement: "bottom",
|
|
||||||
title: $table[0].outerHTML,
|
|
||||||
sanitize: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
46
apps/client/src/widgets/sql_table_schemas.tsx
Normal file
46
apps/client/src/widgets/sql_table_schemas.tsx
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { useEffect, useState } from "preact/hooks";
|
||||||
|
import { t } from "../services/i18n";
|
||||||
|
import { useNoteContext } from "./react/hooks";
|
||||||
|
import "./sql_table_schemas.css";
|
||||||
|
import { SchemaResponse } from "@triliumnext/commons";
|
||||||
|
import server from "../services/server";
|
||||||
|
import Dropdown from "./react/Dropdown";
|
||||||
|
|
||||||
|
export default function SqlTableSchemas() {
|
||||||
|
const { note } = useNoteContext();
|
||||||
|
const [ schemas, setSchemas ] = useState<SchemaResponse[]>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
server.get<SchemaResponse[]>("sql/schema").then(setSchemas);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const isEnabled = note?.mime === "text/x-sqlite;schema=trilium" && schemas;
|
||||||
|
return (
|
||||||
|
<div className={`sql-table-schemas-widget ${!isEnabled ? "hidden-ext" : ""}`}>
|
||||||
|
{isEnabled && (
|
||||||
|
<>
|
||||||
|
{t("sql_table_schemas.tables")}{": "}
|
||||||
|
|
||||||
|
<span class="sql-table-schemas">
|
||||||
|
{schemas.map(({ name, columns }) => (
|
||||||
|
<>
|
||||||
|
<Dropdown text={name} noSelectButtonStyle hideToggleArrow
|
||||||
|
>
|
||||||
|
<table className="table-schema">
|
||||||
|
{columns.map(column => (
|
||||||
|
<tr>
|
||||||
|
<td>{column.name}</td>
|
||||||
|
<td>{column.type}</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</table>
|
||||||
|
</Dropdown>
|
||||||
|
{" "}
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
30
apps/client/src/widgets/title_bar_buttons.css
Normal file
30
apps/client/src/widgets/title_bar_buttons.css
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
.component.title-bar-buttons {
|
||||||
|
flex-shrink: 0;
|
||||||
|
contain: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-bar-buttons div button {
|
||||||
|
border: none !important;
|
||||||
|
border-radius: 0;
|
||||||
|
background: none !important;
|
||||||
|
font-size: 150%;
|
||||||
|
height: 40px;
|
||||||
|
width: 40px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-bar-buttons div:hover button {
|
||||||
|
background-color: var(--accented-background-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-bar-buttons div {
|
||||||
|
display: inline-block;
|
||||||
|
height: 40px;
|
||||||
|
width: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-bar-buttons .btn.focus, .title-bar-buttons .btn:focus {
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
import BasicWidget from "./basic_widget.js";
|
|
||||||
import options from "../services/options.js";
|
|
||||||
import utils from "../services/utils.js";
|
|
||||||
|
|
||||||
const TPL = /*html*/`
|
|
||||||
<div class="title-bar-buttons">
|
|
||||||
<style>
|
|
||||||
.title-bar-buttons {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-bar-buttons div button {
|
|
||||||
border: none !important;
|
|
||||||
border-radius: 0;
|
|
||||||
background: none !important;
|
|
||||||
font-size: 150%;
|
|
||||||
height: 40px;
|
|
||||||
width: 40px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-bar-buttons div:hover button {
|
|
||||||
background-color: var(--accented-background-color) !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-bar-buttons div {
|
|
||||||
display: inline-block;
|
|
||||||
height: 40px;
|
|
||||||
width: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.title-bar-buttons .btn.focus, .title-bar-buttons .btn:focus {
|
|
||||||
box-shadow: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<!-- divs act as a hitbox for the buttons, making them clickable on corners -->
|
|
||||||
<div class="minimize-btn"><button class="btn bx bx-minus"></button></div>
|
|
||||||
<div class="maximize-btn"><button class="btn bx bx-checkbox"></button></div>
|
|
||||||
<div class="close-btn"><button class="btn bx bx-x"></button></div>
|
|
||||||
</div>`;
|
|
||||||
|
|
||||||
export default class TitleBarButtonsWidget extends BasicWidget {
|
|
||||||
doRender() {
|
|
||||||
if (!utils.isElectron() || options.is("nativeTitleBarVisible")) {
|
|
||||||
return (this.$widget = $("<div>"));
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$widget = $(TPL);
|
|
||||||
this.contentSized();
|
|
||||||
|
|
||||||
const $minimizeBtn = this.$widget.find(".minimize-btn");
|
|
||||||
const $maximizeBtn = this.$widget.find(".maximize-btn");
|
|
||||||
const $closeBtn = this.$widget.find(".close-btn");
|
|
||||||
|
|
||||||
$minimizeBtn.on("click", () => {
|
|
||||||
$minimizeBtn.trigger("blur");
|
|
||||||
const remote = utils.dynamicRequire("@electron/remote");
|
|
||||||
remote.BrowserWindow.getFocusedWindow().minimize();
|
|
||||||
});
|
|
||||||
|
|
||||||
$maximizeBtn.on("click", () => {
|
|
||||||
$maximizeBtn.trigger("blur");
|
|
||||||
const remote = utils.dynamicRequire("@electron/remote");
|
|
||||||
const focusedWindow = remote.BrowserWindow.getFocusedWindow();
|
|
||||||
|
|
||||||
if (focusedWindow.isMaximized()) {
|
|
||||||
focusedWindow.unmaximize();
|
|
||||||
} else {
|
|
||||||
focusedWindow.maximize();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$closeBtn.on("click", () => {
|
|
||||||
$closeBtn.trigger("blur");
|
|
||||||
const remote = utils.dynamicRequire("@electron/remote");
|
|
||||||
remote.BrowserWindow.getFocusedWindow().close();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
63
apps/client/src/widgets/title_bar_buttons.tsx
Normal file
63
apps/client/src/widgets/title_bar_buttons.tsx
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { dynamicRequire, isElectron } from "../services/utils";
|
||||||
|
import { useTriliumOption } from "./react/hooks";
|
||||||
|
import "./title_bar_buttons.css";
|
||||||
|
import type { BrowserWindow } from "electron";
|
||||||
|
|
||||||
|
interface TitleBarButtonProps {
|
||||||
|
className: string;
|
||||||
|
icon: string;
|
||||||
|
onClick: (context: {
|
||||||
|
window: BrowserWindow
|
||||||
|
}) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function TitleBarButtons() {
|
||||||
|
const [ nativeTitleBarVisible ] = useTriliumOption("nativeTitleBarVisible");
|
||||||
|
const isEnabled = (isElectron() && nativeTitleBarVisible);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="title-bar-buttons">
|
||||||
|
{isEnabled && (
|
||||||
|
<>
|
||||||
|
<TitleBarButton
|
||||||
|
className="minimize-btn"
|
||||||
|
icon="bx bx-minus"
|
||||||
|
onClick={({ window }) => window.minimize()}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TitleBarButton
|
||||||
|
className="maximize-btn"
|
||||||
|
icon="bx bx-checkbox"
|
||||||
|
onClick={({ window }) => {
|
||||||
|
if (window.isMaximized()) {
|
||||||
|
window.unmaximize();
|
||||||
|
} else {
|
||||||
|
window.maximize();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TitleBarButton
|
||||||
|
className="close-btn"
|
||||||
|
icon="bx bx-x"
|
||||||
|
onClick={({ window }) => window.close()}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TitleBarButton({ className, icon, onClick }: TitleBarButtonProps) {
|
||||||
|
// divs act as a hitbox for the buttons, making them clickable on corners
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<button className={`btn ${icon}`} onClick={() => {
|
||||||
|
const remote = dynamicRequire("@electron/remote");
|
||||||
|
const focusedWindow = remote.BrowserWindow.getFocusedWindow();
|
||||||
|
if (!focusedWindow) return;
|
||||||
|
onClick({ window: focusedWindow });
|
||||||
|
}} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
.classic-toolbar-outer-container {
|
||||||
|
contain: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.classic-toolbar-outer-container.visible {
|
||||||
|
height: 38px;
|
||||||
|
background-color: var(--main-background-color);
|
||||||
|
position: relative;
|
||||||
|
overflow: visible;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#root-widget.virtual-keyboard-opened .classic-toolbar-outer-container.ios {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.classic-toolbar-widget {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 38px;
|
||||||
|
overflow: scroll;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.classic-toolbar-widget::-webkit-scrollbar {
|
||||||
|
height: 0 !important;
|
||||||
|
width: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.classic-toolbar-widget.dropdown-active {
|
||||||
|
height: 50vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.classic-toolbar-widget .ck.ck-toolbar {
|
||||||
|
--ck-color-toolbar-background: transparent;
|
||||||
|
--ck-color-button-default-background: transparent;
|
||||||
|
--ck-color-button-default-disabled-background: transparent;
|
||||||
|
position: absolute;
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.classic-toolbar-widget .ck.ck-button.ck-disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
@@ -1,130 +0,0 @@
|
|||||||
import { isIOS } from "../../../services/utils.js";
|
|
||||||
import NoteContextAwareWidget from "../../note_context_aware_widget.js";
|
|
||||||
|
|
||||||
const TPL = /*html*/`\
|
|
||||||
<div class="classic-toolbar-outer-container">
|
|
||||||
<div class="classic-toolbar-widget"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
.classic-toolbar-outer-container.visible {
|
|
||||||
height: 38px;
|
|
||||||
background-color: var(--main-background-color);
|
|
||||||
position: relative;
|
|
||||||
overflow: visible;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#root-widget.virtual-keyboard-opened .classic-toolbar-outer-container.ios {
|
|
||||||
position: absolute;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.classic-toolbar-widget {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 38px;
|
|
||||||
overflow: scroll;
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-end;
|
|
||||||
user-select: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.classic-toolbar-widget::-webkit-scrollbar {
|
|
||||||
height: 0 !important;
|
|
||||||
width: 0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.classic-toolbar-widget.dropdown-active {
|
|
||||||
height: 50vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.classic-toolbar-widget .ck.ck-toolbar {
|
|
||||||
--ck-color-toolbar-background: transparent;
|
|
||||||
--ck-color-button-default-background: transparent;
|
|
||||||
--ck-color-button-default-disabled-background: transparent;
|
|
||||||
position: absolute;
|
|
||||||
background-color: transparent;
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.classic-toolbar-widget .ck.ck-button.ck-disabled {
|
|
||||||
opacity: 0.3;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
`;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles the editing toolbar for CKEditor in mobile mode. The toolbar acts as a floating bar, with two different mechanism:
|
|
||||||
*
|
|
||||||
* - On iOS, because it does not respect the viewport meta value `interactive-widget=resizes-content`, we need to listen to window resizes and scroll and reposition the toolbar using absolute positioning.
|
|
||||||
* - On Android, the viewport change makes the keyboard resize the content area, all we have to do is to hide the tab bar and global menu (handled in the global style).
|
|
||||||
*/
|
|
||||||
export default class MobileEditorToolbar extends NoteContextAwareWidget {
|
|
||||||
|
|
||||||
private observer: MutationObserver;
|
|
||||||
private $innerWrapper!: JQuery<HTMLElement>;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super();
|
|
||||||
this.observer = new MutationObserver((e) => this.#onDropdownStateChanged(e));
|
|
||||||
}
|
|
||||||
|
|
||||||
get name() {
|
|
||||||
return "classicEditor";
|
|
||||||
}
|
|
||||||
|
|
||||||
doRender() {
|
|
||||||
this.$widget = $(TPL);
|
|
||||||
this.$innerWrapper = this.$widget.find(".classic-toolbar-widget");
|
|
||||||
this.contentSized();
|
|
||||||
|
|
||||||
// Observe when a dropdown is expanded to apply a style that allows the dropdown to be visible, since we can't have the element both visible and the toolbar scrollable.
|
|
||||||
this.observer.disconnect();
|
|
||||||
this.observer.observe(this.$widget[0], {
|
|
||||||
attributeFilter: ["aria-expanded"],
|
|
||||||
subtree: true
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isIOS()) {
|
|
||||||
this.#handlePositioningOniOS();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#handlePositioningOniOS() {
|
|
||||||
const adjustPosition = () => {
|
|
||||||
let bottom = window.innerHeight - (window.visualViewport?.height || 0);
|
|
||||||
this.$widget.css("bottom", `${bottom}px`);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.$widget.addClass("ios");
|
|
||||||
window.visualViewport?.addEventListener("resize", adjustPosition);
|
|
||||||
window.addEventListener("scroll", adjustPosition);
|
|
||||||
}
|
|
||||||
|
|
||||||
#onDropdownStateChanged(e: MutationRecord[]) {
|
|
||||||
const dropdownActive = e.map((e) => (e.target as any).ariaExpanded === "true").reduce((acc, e) => acc && e);
|
|
||||||
this.$innerWrapper.toggleClass("dropdown-active", dropdownActive);
|
|
||||||
}
|
|
||||||
|
|
||||||
async #shouldDisplay() {
|
|
||||||
if (!this.note || this.note.type !== "text") {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await this.noteContext?.isReadOnly()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
async refreshWithNote() {
|
|
||||||
this.toggleExt(await this.#shouldDisplay());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { MutableRef, useCallback, useEffect, useRef, useState } from "preact/hooks";
|
||||||
|
import { useNoteContext } from "../../react/hooks";
|
||||||
|
import "./mobile_editor_toolbar.css";
|
||||||
|
import { isIOS } from "../../../services/utils";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the editing toolbar for CKEditor in mobile mode. The toolbar acts as a floating bar, with two different mechanism:
|
||||||
|
*
|
||||||
|
* - On iOS, because it does not respect the viewport meta value `interactive-widget=resizes-content`, we need to listen to window resizes and scroll and reposition the toolbar using absolute positioning.
|
||||||
|
* - On Android, the viewport change makes the keyboard resize the content area, all we have to do is to hide the tab bar and global menu (handled in the global style).
|
||||||
|
*/
|
||||||
|
export default function MobileEditorToolbar() {
|
||||||
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||||
|
const { note, noteContext } = useNoteContext();
|
||||||
|
const [ shouldDisplay, setShouldDisplay ] = useState(false);
|
||||||
|
const [ dropdownActive, setDropdownActive ] = useState(false);
|
||||||
|
|
||||||
|
usePositioningOniOS(wrapperRef);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
noteContext?.isReadOnly().then(isReadOnly => {
|
||||||
|
setShouldDisplay(note?.type === "text" && !isReadOnly);
|
||||||
|
});
|
||||||
|
}, [ note ]);
|
||||||
|
|
||||||
|
// Observe when a dropdown is expanded to apply a style that allows the dropdown to be visible, since we can't have the element both visible and the toolbar scrollable.
|
||||||
|
useEffect(() => {
|
||||||
|
if (!wrapperRef.current) return;
|
||||||
|
|
||||||
|
const observer = new MutationObserver(e => {
|
||||||
|
setDropdownActive(e.map((e) => (e.target as any).ariaExpanded === "true").reduce((acc, e) => acc && e));
|
||||||
|
});
|
||||||
|
|
||||||
|
observer.observe(wrapperRef.current, {
|
||||||
|
attributeFilter: ["aria-expanded"],
|
||||||
|
subtree: true
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`classic-toolbar-outer-container ${!shouldDisplay ? "hidden-ext" : "visible"} ${isIOS() ? "ios" : ""}`}>
|
||||||
|
<div ref={wrapperRef} className={`classic-toolbar-widget ${dropdownActive ? "dropdown-active" : ""}`}></div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function usePositioningOniOS(wrapperRef: MutableRef<HTMLDivElement | null>) {
|
||||||
|
const adjustPosition = useCallback(() => {
|
||||||
|
if (!wrapperRef.current) return;
|
||||||
|
let bottom = window.innerHeight - (window.visualViewport?.height || 0);
|
||||||
|
wrapperRef.current.style.bottom = `${bottom}px`;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isIOS()) return;
|
||||||
|
|
||||||
|
window.visualViewport?.addEventListener("resize", adjustPosition);
|
||||||
|
window.addEventListener("scroll", adjustPosition);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.visualViewport?.removeEventListener("resize", adjustPosition);
|
||||||
|
window.removeEventListener("scroll", adjustPosition);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ import ValidationError from "../../errors/validation_error.js";
|
|||||||
import blobService from "../../services/blob.js";
|
import blobService from "../../services/blob.js";
|
||||||
import type { Request } from "express";
|
import type { Request } from "express";
|
||||||
import type BBranch from "../../becca/entities/bbranch.js";
|
import type BBranch from "../../becca/entities/bbranch.js";
|
||||||
import type { AttributeRow, DeleteNotesPreview, MetadataResponse } from "@triliumnext/commons";
|
import type { AttributeRow, CreateChildrenResponse, DeleteNotesPreview, MetadataResponse } from "@triliumnext/commons";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
@@ -123,7 +123,7 @@ function createNote(req: Request) {
|
|||||||
return {
|
return {
|
||||||
note,
|
note,
|
||||||
branch
|
branch
|
||||||
};
|
} satisfies CreateChildrenResponse;
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateNoteData(req: Request) {
|
function updateNoteData(req: Request) {
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ function getSchema() {
|
|||||||
for (const tableName of tableNames) {
|
for (const tableName of tableNames) {
|
||||||
tables.push({
|
tables.push({
|
||||||
name: tableName,
|
name: tableName,
|
||||||
columns: sql.getRows(`PRAGMA table_info(${tableName})`)
|
columns: sql.getRows<{ name: string; type: string; }>(`PRAGMA table_info(${tableName})`)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AttachmentRow, AttributeRow, NoteType } from "./rows.js";
|
import { AttachmentRow, AttributeRow, BranchRow, NoteRow, NoteType } from "./rows.js";
|
||||||
|
|
||||||
type Response = {
|
type Response = {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -220,3 +220,25 @@ export type BacklinksResponse = ({
|
|||||||
noteId: string;
|
noteId: string;
|
||||||
excerpts: string[]
|
excerpts: string[]
|
||||||
})[];
|
})[];
|
||||||
|
|
||||||
|
|
||||||
|
export type SqlExecuteResults = (object[] | object)[];
|
||||||
|
|
||||||
|
export interface SqlExecuteResponse {
|
||||||
|
success: boolean;
|
||||||
|
error?: string;
|
||||||
|
results: SqlExecuteResults;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateChildrenResponse {
|
||||||
|
note: NoteRow;
|
||||||
|
branch: BranchRow;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SchemaResponse {
|
||||||
|
name: string;
|
||||||
|
columns: {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user