mirror of
https://github.com/zadam/trilium.git
synced 2025-12-04 22:44:25 +01:00
Merge 4571b956830d33b7e0e60e8ac314486dd8c4cc4b into 1195cbd772ffd68f7757855f272a7c5c2c29eb78
This commit is contained in:
commit
1b32a67938
@ -30,7 +30,6 @@ import ScrollingContainer from "../widgets/containers/scrolling_container.js";
|
||||
import ScrollPadding from "../widgets/scroll_padding.js";
|
||||
import SearchResult from "../widgets/search_result.jsx";
|
||||
import SharedInfo from "../widgets/shared_info.jsx";
|
||||
import SpacerWidget from "../widgets/spacer.js";
|
||||
import SplitNoteContainer from "../widgets/containers/split_note_container.js";
|
||||
import SqlResults from "../widgets/sql_result.js";
|
||||
import SqlTableSchemas from "../widgets/sql_table_schemas.js";
|
||||
@ -43,8 +42,8 @@ import UploadAttachmentsDialog from "../widgets/dialogs/upload_attachments.js";
|
||||
import utils from "../services/utils.js";
|
||||
import WatchedFileUpdateStatusWidget from "../widgets/watched_file_update_status.js";
|
||||
import NoteDetail from "../widgets/NoteDetail.jsx";
|
||||
import RightPanelWidget from "../widgets/sidebar/RightPanelWidget.jsx";
|
||||
import PromotedAttributes from "../widgets/PromotedAttributes.jsx";
|
||||
import SpacerWidget from "../widgets/launch_bar/SpacerWidget.jsx";
|
||||
|
||||
export default class DesktopLayout {
|
||||
|
||||
@ -125,7 +124,7 @@ export default class DesktopLayout {
|
||||
.cssBlock(".title-row > * { margin: 5px; }")
|
||||
.child(<NoteIconWidget />)
|
||||
.child(<NoteTitleWidget />)
|
||||
.child(new SpacerWidget(0, 1))
|
||||
.child(<SpacerWidget baseSize={0} growthFactor={1} />)
|
||||
.child(<MovePaneButton direction="left" />)
|
||||
.child(<MovePaneButton direction="right" />)
|
||||
.child(<ClosePaneButton />)
|
||||
|
||||
@ -150,7 +150,7 @@ export function isMac() {
|
||||
|
||||
export const hasTouchBar = (isMac() && isElectron());
|
||||
|
||||
function isCtrlKey(evt: KeyboardEvent | MouseEvent | JQuery.ClickEvent | JQuery.ContextMenuEvent | JQuery.TriggeredEvent | React.PointerEvent<HTMLCanvasElement> | JQueryEventObject) {
|
||||
export function isCtrlKey(evt: KeyboardEvent | MouseEvent | JQuery.ClickEvent | JQuery.ContextMenuEvent | JQuery.TriggeredEvent | React.PointerEvent<HTMLCanvasElement> | JQueryEventObject) {
|
||||
return (!isMac() && evt.ctrlKey) || (isMac() && evt.metaKey);
|
||||
}
|
||||
|
||||
|
||||
@ -212,7 +212,7 @@ body[dir=ltr] #launcher-container {
|
||||
}
|
||||
|
||||
#launcher-pane .launcher-button,
|
||||
#launcher-pane .dropdown {
|
||||
#launcher-pane .right-dropdown-widget {
|
||||
width: calc(var(--launcher-pane-size) - (var(--launcher-pane-button-margin) * 2)) !important;
|
||||
height: calc(var(--launcher-pane-size) - (var(--launcher-pane-button-margin) * 2)) !important;
|
||||
margin: var(--launcher-pane-button-gap) var(--launcher-pane-button-margin);
|
||||
|
||||
@ -1,78 +0,0 @@
|
||||
import FlexContainer from "./containers/flex_container.js";
|
||||
import OpenNoteButtonWidget from "./buttons/open_note_button_widget.js";
|
||||
import BookmarkFolderWidget from "./buttons/bookmark_folder.js";
|
||||
import froca from "../services/froca.js";
|
||||
import utils from "../services/utils.js";
|
||||
import type { EventData } from "../components/app_context.js";
|
||||
import type Component from "../components/component.js";
|
||||
|
||||
interface BookmarkButtonsSettings {
|
||||
titlePlacement?: string;
|
||||
}
|
||||
|
||||
export default class BookmarkButtons extends FlexContainer<Component> {
|
||||
private settings: BookmarkButtonsSettings;
|
||||
private noteIds: string[];
|
||||
|
||||
constructor(isHorizontalLayout: boolean) {
|
||||
super(isHorizontalLayout ? "row" : "column");
|
||||
|
||||
this.contentSized();
|
||||
this.settings = {};
|
||||
this.noteIds = [];
|
||||
}
|
||||
|
||||
async refresh(): Promise<void> {
|
||||
this.$widget.empty();
|
||||
this.children = [];
|
||||
this.noteIds = [];
|
||||
|
||||
const bookmarkParentNote = await froca.getNote("_lbBookmarks");
|
||||
|
||||
if (!bookmarkParentNote) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const note of await bookmarkParentNote.getChildNotes()) {
|
||||
this.noteIds.push(note.noteId);
|
||||
|
||||
let buttonWidget: OpenNoteButtonWidget | BookmarkFolderWidget = note.isLabelTruthy("bookmarkFolder")
|
||||
? new BookmarkFolderWidget(note)
|
||||
: new OpenNoteButtonWidget(note).class("launcher-button");
|
||||
|
||||
if (this.settings.titlePlacement) {
|
||||
if (!("settings" in buttonWidget)) {
|
||||
(buttonWidget as any).settings = {};
|
||||
}
|
||||
|
||||
(buttonWidget as any).settings.titlePlacement = this.settings.titlePlacement;
|
||||
}
|
||||
|
||||
this.child(buttonWidget);
|
||||
|
||||
this.$widget.append(buttonWidget.render());
|
||||
|
||||
buttonWidget.refreshIcon();
|
||||
}
|
||||
|
||||
utils.reloadTray();
|
||||
}
|
||||
|
||||
initialRenderCompleteEvent(): void {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">): void {
|
||||
if (loadResults.getBranchRows().find((branch) => branch.parentNoteId === "_lbBookmarks")) {
|
||||
this.refresh();
|
||||
}
|
||||
|
||||
if (loadResults.getAttributeRows().find((attr) =>
|
||||
attr.type === "label" &&
|
||||
attr.name && ["iconClass", "workspaceIconClass", "bookmarkFolder"].includes(attr.name) &&
|
||||
attr.noteId && this.noteIds.includes(attr.noteId)
|
||||
)) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
import type { EventData } from "../../components/app_context.js";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
import options from "../../services/options.js";
|
||||
import CommandButtonWidget from "./command_button.js";
|
||||
|
||||
export default class AiChatButton extends CommandButtonWidget {
|
||||
|
||||
constructor(note: FNote) {
|
||||
super();
|
||||
|
||||
this.command("createAiChat")
|
||||
.title(() => note.title)
|
||||
.icon(() => note.getIcon())
|
||||
.class("launcher-button");
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
return options.get("aiEnabled") === "true";
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
if (loadResults.isOptionReloaded("aiEnabled")) {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,88 +0,0 @@
|
||||
import RightDropdownButtonWidget from "./right_dropdown_button.js";
|
||||
import linkService from "../../services/link.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
|
||||
const DROPDOWN_TPL = `
|
||||
<div class="bookmark-folder-widget">
|
||||
<style>
|
||||
.bookmark-folder-widget {
|
||||
min-width: 400px;
|
||||
max-height: 500px;
|
||||
padding: 7px 15px 0 15px;
|
||||
font-size: 1.2rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.bookmark-folder-widget ul {
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.bookmark-folder-widget .note-link {
|
||||
display: block;
|
||||
padding: 5px 10px 5px 5px;
|
||||
}
|
||||
|
||||
.bookmark-folder-widget .note-link:hover {
|
||||
background-color: var(--accented-background-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.dropdown-menu .bookmark-folder-widget a:hover {
|
||||
text-decoration: none;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
.bookmark-folder-widget li .note-link {
|
||||
padding-inline-start: 35px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="parent-note"></div>
|
||||
|
||||
<ul class="children-notes"></ul>
|
||||
</div>`;
|
||||
|
||||
interface LinkOptions {
|
||||
showTooltip: boolean;
|
||||
showNoteIcon: boolean;
|
||||
}
|
||||
|
||||
export default class BookmarkFolderWidget extends RightDropdownButtonWidget {
|
||||
private note: FNote;
|
||||
private $parentNote!: JQuery<HTMLElement>;
|
||||
private $childrenNotes!: JQuery<HTMLElement>;
|
||||
declare $dropdownContent: JQuery<HTMLElement>;
|
||||
|
||||
constructor(note: FNote) {
|
||||
super(utils.escapeHtml(note.title), note.getIcon(), DROPDOWN_TPL);
|
||||
|
||||
this.note = note;
|
||||
}
|
||||
|
||||
doRender(): void {
|
||||
super.doRender();
|
||||
|
||||
this.$parentNote = this.$dropdownContent.find(".parent-note");
|
||||
this.$childrenNotes = this.$dropdownContent.find(".children-notes");
|
||||
}
|
||||
|
||||
async dropdownShown(): Promise<void> {
|
||||
this.$parentNote.empty();
|
||||
this.$childrenNotes.empty();
|
||||
|
||||
const linkOptions: LinkOptions = {
|
||||
showTooltip: false,
|
||||
showNoteIcon: true
|
||||
};
|
||||
|
||||
this.$parentNote.append((await linkService.createLink(this.note.noteId, linkOptions)).addClass("note-link"));
|
||||
|
||||
for (const childNote of await this.note.getChildNotes()) {
|
||||
this.$childrenNotes.append($("<li>").append((await linkService.createLink(childNote.noteId, linkOptions)).addClass("note-link")));
|
||||
}
|
||||
}
|
||||
|
||||
refreshIcon(): void {}
|
||||
}
|
||||
@ -1,63 +0,0 @@
|
||||
import froca from "../../services/froca.js";
|
||||
import attributeService from "../../services/attributes.js";
|
||||
import CommandButtonWidget from "./command_button.js";
|
||||
import type { EventData } from "../../components/app_context.js";
|
||||
|
||||
export type ButtonNoteIdProvider = () => string;
|
||||
|
||||
export default class ButtonFromNoteWidget extends CommandButtonWidget {
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.settings.buttonNoteIdProvider = null;
|
||||
}
|
||||
|
||||
buttonNoteIdProvider(provider: ButtonNoteIdProvider) {
|
||||
this.settings.buttonNoteIdProvider = provider;
|
||||
return this;
|
||||
}
|
||||
|
||||
doRender() {
|
||||
super.doRender();
|
||||
|
||||
this.updateIcon();
|
||||
}
|
||||
|
||||
updateIcon() {
|
||||
if (!this.settings.buttonNoteIdProvider) {
|
||||
console.error(`buttonNoteId for '${this.componentId}' is not defined.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const buttonNoteId = this.settings.buttonNoteIdProvider();
|
||||
|
||||
if (!buttonNoteId) {
|
||||
console.error(`buttonNoteId for '${this.componentId}' is not defined.`);
|
||||
return;
|
||||
}
|
||||
|
||||
froca.getNote(buttonNoteId).then((note) => {
|
||||
const icon = note?.getIcon();
|
||||
if (icon) {
|
||||
this.settings.icon = icon;
|
||||
}
|
||||
|
||||
this.refreshIcon();
|
||||
});
|
||||
}
|
||||
|
||||
entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
// TODO: this seems incorrect
|
||||
//@ts-ignore
|
||||
const buttonNote = froca.getNoteFromCache(this.buttonNoteIdProvider());
|
||||
|
||||
if (!buttonNote) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (loadResults.getAttributeRows(this.componentId).find((attr) => attr.type === "label" && attr.name === "iconClass" && attributeService.isAffecting(attr, buttonNote))) {
|
||||
this.updateIcon();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -8,76 +8,8 @@ import options from "../../services/options.js";
|
||||
import { Dropdown } from "bootstrap";
|
||||
import type { EventData } from "../../components/app_context.js";
|
||||
import { dayjs, type Dayjs } from "@triliumnext/commons";
|
||||
import "../../stylesheets/calendar.css";
|
||||
import type { AttributeRow, OptionDefinitions } from "@triliumnext/commons";
|
||||
|
||||
const MONTHS = [
|
||||
t("calendar.january"),
|
||||
t("calendar.february"),
|
||||
t("calendar.march"),
|
||||
t("calendar.april"),
|
||||
t("calendar.may"),
|
||||
t("calendar.june"),
|
||||
t("calendar.july"),
|
||||
t("calendar.august"),
|
||||
t("calendar.september"),
|
||||
t("calendar.october"),
|
||||
t("calendar.november"),
|
||||
t("calendar.december")
|
||||
];
|
||||
|
||||
const DROPDOWN_TPL = `
|
||||
<div class="calendar-dropdown-widget">
|
||||
<style>
|
||||
.calendar-dropdown-widget {
|
||||
width: 400px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="calendar-header">
|
||||
<div class="calendar-month-selector">
|
||||
<button class="calendar-btn tn-tool-button bx bx-chevron-left" data-calendar-toggle="previous"></button>
|
||||
|
||||
<button class="btn dropdown-toggle select-button" type="button"
|
||||
data-bs-toggle="dropdown" data-bs-auto-close="true"
|
||||
aria-expanded="false"
|
||||
data-calendar-input="month"></button>
|
||||
<ul class="dropdown-menu" data-calendar-input="month-list">
|
||||
${Object.entries(MONTHS)
|
||||
.map(([i, month]) => `<li><button class="dropdown-item" data-value=${i}>${month}</button></li>`)
|
||||
.join("")}
|
||||
</ul>
|
||||
|
||||
<button class="calendar-btn tn-tool-button bx bx-chevron-right" data-calendar-toggle="next"></button>
|
||||
</div>
|
||||
|
||||
<div class="calendar-year-selector">
|
||||
<button class="calendar-btn tn-tool-button bx bx-chevron-left" data-calendar-toggle="previousYear"></button>
|
||||
|
||||
<input type="number" min="1900" max="2999" step="1" data-calendar-input="year" />
|
||||
|
||||
<button class="calendar-btn tn-tool-button bx bx-chevron-right" data-calendar-toggle="nextYear"></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="calendar-week"></div>
|
||||
<div class="calendar-body" data-calendar-area="month"></div>
|
||||
</div>`;
|
||||
|
||||
const DAYS_OF_WEEK = [
|
||||
t("calendar.sun"),
|
||||
t("calendar.mon"),
|
||||
t("calendar.tue"),
|
||||
t("calendar.wed"),
|
||||
t("calendar.thu"),
|
||||
t("calendar.fri"),
|
||||
t("calendar.sat")
|
||||
];
|
||||
|
||||
interface DateNotesForMonth {
|
||||
[date: string]: string;
|
||||
}
|
||||
|
||||
interface WeekCalculationOptions {
|
||||
firstWeekType: number;
|
||||
minDaysInFirstWeek: number;
|
||||
@ -110,7 +42,6 @@ export default class CalendarWidget extends RightDropdownButtonWidget {
|
||||
super.doRender();
|
||||
|
||||
this.$month = this.$dropdownContent.find('[data-calendar-area="month"]');
|
||||
this.$weekHeader = this.$dropdownContent.find(".calendar-week");
|
||||
|
||||
this.manageFirstDayOfWeek();
|
||||
this.initWeekCalculation();
|
||||
@ -131,37 +62,6 @@ export default class CalendarWidget extends RightDropdownButtonWidget {
|
||||
}
|
||||
});
|
||||
|
||||
this.$next = this.$dropdownContent.find('[data-calendar-toggle="next"]');
|
||||
this.$next.on("click", () => {
|
||||
this.date = this.date.add(1, 'month');
|
||||
this.createMonth();
|
||||
});
|
||||
this.$previous = this.$dropdownContent.find('[data-calendar-toggle="previous"]');
|
||||
this.$previous.on("click", () => {
|
||||
this.date = this.date.subtract(1, 'month');
|
||||
this.createMonth();
|
||||
});
|
||||
|
||||
// Year navigation
|
||||
this.$yearSelect = this.$dropdownContent.find('[data-calendar-input="year"]');
|
||||
this.$yearSelect.on("input", (e) => {
|
||||
const target = e.target as HTMLInputElement;
|
||||
this.date = this.date.year(parseInt(target.value));
|
||||
this.createMonth();
|
||||
});
|
||||
|
||||
this.$nextYear = this.$dropdownContent.find('[data-calendar-toggle="nextYear"]');
|
||||
this.$nextYear.on("click", () => {
|
||||
this.date = this.date.add(1, 'year');
|
||||
this.createMonth();
|
||||
});
|
||||
|
||||
this.$previousYear = this.$dropdownContent.find('[data-calendar-toggle="previousYear"]');
|
||||
this.$previousYear.on("click", () => {
|
||||
this.date = this.date.subtract(1, 'year');
|
||||
this.createMonth();
|
||||
});
|
||||
|
||||
// Date click
|
||||
this.$dropdownContent.on("click", ".calendar-date", async (ev) => {
|
||||
const date = $(ev.target).closest(".calendar-date").attr("data-calendar-date");
|
||||
@ -226,17 +126,6 @@ export default class CalendarWidget extends RightDropdownButtonWidget {
|
||||
this.weekNoteEnable = noteAttributes.some(a => a.name === 'enableWeekNote');
|
||||
}
|
||||
|
||||
// Store firstDayOfWeek as ISO (1–7)
|
||||
manageFirstDayOfWeek() {
|
||||
const rawFirstDayOfWeek = options.getInt("firstDayOfWeek") || 0;
|
||||
this.firstDayOfWeekISO = rawFirstDayOfWeek === 0 ? 7 : rawFirstDayOfWeek;
|
||||
|
||||
let localeDaysOfWeek = [...DAYS_OF_WEEK];
|
||||
const shifted = localeDaysOfWeek.splice(0, rawFirstDayOfWeek);
|
||||
localeDaysOfWeek = ['', ...localeDaysOfWeek, ...shifted];
|
||||
this.$weekHeader.html(localeDaysOfWeek.map((el) => `<span>${el}</span>`).join(''));
|
||||
}
|
||||
|
||||
initWeekCalculation() {
|
||||
this.weekCalculationOptions = {
|
||||
firstWeekType: options.getInt("firstWeekOfYear") || 0,
|
||||
@ -244,34 +133,13 @@ export default class CalendarWidget extends RightDropdownButtonWidget {
|
||||
};
|
||||
}
|
||||
|
||||
getWeekStartDate(date: Dayjs): Dayjs {
|
||||
const currentISO = date.isoWeekday();
|
||||
const diff = (currentISO - this.firstDayOfWeekISO + 7) % 7;
|
||||
return date.clone().subtract(diff, "day").startOf("day");
|
||||
}
|
||||
|
||||
getWeekNumber(date: Dayjs): number {
|
||||
const weekStart = this.getWeekStartDate(date);
|
||||
return weekStart.isoWeek();
|
||||
}
|
||||
|
||||
async dropdownShown() {
|
||||
await this.getWeekNoteEnable();
|
||||
this.weekNotes = await server.get<string[]>(`attribute-values/weekNote`);
|
||||
this.init(appContext.tabManager.getActiveContextNote()?.getOwnedLabelValue("dateNote") ?? null);
|
||||
this.init( ?? null);
|
||||
}
|
||||
|
||||
init(activeDate: string | null) {
|
||||
this.activeDate = activeDate ? dayjs(`${activeDate}T12:00:00`) : null;
|
||||
this.todaysDate = dayjs();
|
||||
this.date = dayjs(this.activeDate || this.todaysDate).startOf('month');
|
||||
this.createMonth();
|
||||
}
|
||||
|
||||
createDay(dateNotesForMonth: DateNotesForMonth, num: number) {
|
||||
const $newDay = $("<a>")
|
||||
.addClass("calendar-date")
|
||||
.attr("data-calendar-date", this.date.local().format('YYYY-MM-DD'));
|
||||
createDay() {
|
||||
const $date = $("<span>").html(String(num));
|
||||
const dateNoteId = dateNotesForMonth[this.date.local().format('YYYY-MM-DD')];
|
||||
|
||||
@ -296,113 +164,13 @@ export default class CalendarWidget extends RightDropdownButtonWidget {
|
||||
$newWeekNumber.addClass("calendar-date-exists").attr("data-href", `#root/${weekNoteId}`);
|
||||
}
|
||||
} else {
|
||||
$newWeekNumber = $("<span>").addClass("calendar-week-number-disabled");
|
||||
|
||||
}
|
||||
|
||||
$newWeekNumber.addClass("calendar-week-number").attr("data-calendar-week-number", weekNoteId);
|
||||
$newWeekNumber.append($("<span>").html(String(weekNumber)));
|
||||
return $newWeekNumber;
|
||||
}
|
||||
|
||||
// Use isoWeekday() consistently
|
||||
private getPrevMonthDays(firstDayISO: number): { weekNumber: number, dates: Dayjs[] } {
|
||||
const prevMonthLastDay = this.date.subtract(1, 'month').endOf('month');
|
||||
const daysToAdd = (firstDayISO - this.firstDayOfWeekISO + 7) % 7;
|
||||
const dates: Dayjs[] = [];
|
||||
|
||||
const firstDay = this.date.startOf('month');
|
||||
const weekNumber = this.getWeekNumber(firstDay);
|
||||
|
||||
// Get dates from previous month
|
||||
for (let i = daysToAdd - 1; i >= 0; i--) {
|
||||
dates.push(prevMonthLastDay.subtract(i, 'day'));
|
||||
}
|
||||
|
||||
return { weekNumber, dates };
|
||||
}
|
||||
|
||||
private getNextMonthDays(lastDayISO: number): Dayjs[] {
|
||||
const nextMonthFirstDay = this.date.add(1, 'month').startOf('month');
|
||||
const dates: Dayjs[] = [];
|
||||
|
||||
const lastDayOfUserWeek = ((this.firstDayOfWeekISO + 6 - 1) % 7) + 1; // ISO wrap
|
||||
const daysToAdd = (lastDayOfUserWeek - lastDayISO + 7) % 7;
|
||||
|
||||
for (let i = 0; i < daysToAdd; i++) {
|
||||
dates.push(nextMonthFirstDay.add(i, 'day'));
|
||||
}
|
||||
return dates;
|
||||
}
|
||||
|
||||
async createMonth() {
|
||||
const month = this.date.format('YYYY-MM');
|
||||
const dateNotesForMonth: DateNotesForMonth = await server.get(`special-notes/notes-for-month/${month}`);
|
||||
|
||||
this.$month.empty();
|
||||
|
||||
const firstDay = this.date.startOf('month');
|
||||
const firstDayISO = firstDay.isoWeekday();
|
||||
|
||||
// Previous month filler
|
||||
if (firstDayISO !== this.firstDayOfWeekISO) {
|
||||
const { weekNumber, dates } = this.getPrevMonthDays(firstDayISO);
|
||||
const prevMonth = this.date.subtract(1, 'month').format('YYYY-MM');
|
||||
const dateNotesForPrevMonth: DateNotesForMonth = await server.get(`special-notes/notes-for-month/${prevMonth}`);
|
||||
|
||||
const $weekNumber = this.createWeekNumber(weekNumber);
|
||||
this.$month.append($weekNumber);
|
||||
|
||||
dates.forEach(date => {
|
||||
const tempDate = this.date;
|
||||
this.date = date;
|
||||
const $day = this.createDay(dateNotesForPrevMonth, date.date());
|
||||
$day.addClass('calendar-date-prev-month');
|
||||
this.$month.append($day);
|
||||
this.date = tempDate;
|
||||
});
|
||||
}
|
||||
|
||||
const currentMonth = this.date.month();
|
||||
|
||||
// Main month
|
||||
while (this.date.month() === currentMonth) {
|
||||
const weekNumber = this.getWeekNumber(this.date);
|
||||
if (this.date.isoWeekday() === this.firstDayOfWeekISO) {
|
||||
const $weekNumber = this.createWeekNumber(weekNumber);
|
||||
this.$month.append($weekNumber);
|
||||
}
|
||||
|
||||
const $day = this.createDay(dateNotesForMonth, this.date.date());
|
||||
this.$month.append($day);
|
||||
this.date = this.date.add(1, 'day');
|
||||
}
|
||||
// while loop trips over and day is at 30/31, bring it back
|
||||
this.date = this.date.startOf('month').subtract(1, 'month');
|
||||
|
||||
// Add dates from next month
|
||||
const lastDayOfMonth = this.date.endOf('month');
|
||||
const lastDayISO = lastDayOfMonth.isoWeekday();
|
||||
const lastDayOfUserWeek = ((this.firstDayOfWeekISO + 6 - 1) % 7) + 1;
|
||||
|
||||
if (lastDayISO !== lastDayOfUserWeek) {
|
||||
const dates = this.getNextMonthDays(lastDayISO);
|
||||
const nextMonth = this.date.add(1, 'month').format('YYYY-MM');
|
||||
const dateNotesForNextMonth: DateNotesForMonth = await server.get(`special-notes/notes-for-month/${nextMonth}`);
|
||||
|
||||
dates.forEach(date => {
|
||||
const tempDate = this.date;
|
||||
this.date = date;
|
||||
const $day = this.createDay(dateNotesForNextMonth, date.date());
|
||||
$day.addClass('calendar-date-next-month');
|
||||
this.$month.append($day);
|
||||
this.date = tempDate;
|
||||
});
|
||||
}
|
||||
|
||||
this.$monthSelect.text(MONTHS[this.date.month()]);
|
||||
this.$yearSelect.val(this.date.year());
|
||||
}
|
||||
|
||||
async entitiesReloadedEvent({ loadResults }: EventData<"entitiesReloaded">) {
|
||||
const WEEK_OPTIONS: (keyof OptionDefinitions)[] = [
|
||||
"firstDayOfWeek",
|
||||
|
||||
@ -1,73 +0,0 @@
|
||||
import { ActionKeyboardShortcut } from "@triliumnext/commons";
|
||||
import type { CommandNames } from "../../components/app_context.js";
|
||||
import keyboardActionsService from "../../services/keyboard_actions.js";
|
||||
import AbstractButtonWidget, { type AbstractButtonWidgetSettings } from "./abstract_button.js";
|
||||
import type { ButtonNoteIdProvider } from "./button_from_note.js";
|
||||
|
||||
let actions: ActionKeyboardShortcut[];
|
||||
|
||||
keyboardActionsService.getActions().then((as) => (actions = as));
|
||||
|
||||
// TODO: Is this actually used?
|
||||
export type ClickHandler = (widget: CommandButtonWidget, e: JQuery.ClickEvent<any, any, any, any>) => void;
|
||||
type CommandOrCallback = CommandNames | (() => CommandNames);
|
||||
|
||||
interface CommandButtonWidgetSettings extends AbstractButtonWidgetSettings {
|
||||
command?: CommandOrCallback;
|
||||
onClick?: ClickHandler;
|
||||
buttonNoteIdProvider?: ButtonNoteIdProvider | null;
|
||||
}
|
||||
|
||||
export default class CommandButtonWidget extends AbstractButtonWidget<CommandButtonWidgetSettings> {
|
||||
constructor() {
|
||||
super();
|
||||
this.settings = {
|
||||
titlePlacement: "right",
|
||||
title: null,
|
||||
icon: null,
|
||||
onContextMenu: null
|
||||
};
|
||||
}
|
||||
|
||||
doRender() {
|
||||
super.doRender();
|
||||
|
||||
if (this.settings.command) {
|
||||
this.$widget.on("click", () => {
|
||||
this.tooltip.hide();
|
||||
|
||||
if (this._command) {
|
||||
this.triggerCommand(this._command);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.warn(`Button widget '${this.componentId}' has no defined command`, this.settings);
|
||||
}
|
||||
}
|
||||
|
||||
getTitle() {
|
||||
const title = super.getTitle();
|
||||
|
||||
const action = actions.find((act) => act.actionName === this._command);
|
||||
|
||||
if (action?.effectiveShortcuts && action.effectiveShortcuts.length > 0) {
|
||||
return `${title} (${action.effectiveShortcuts.join(", ")})`;
|
||||
} else {
|
||||
return title;
|
||||
}
|
||||
}
|
||||
|
||||
onClick(handler: ClickHandler) {
|
||||
this.settings.onClick = handler;
|
||||
return this;
|
||||
}
|
||||
|
||||
command(command: CommandOrCallback) {
|
||||
this.settings.command = command;
|
||||
return this;
|
||||
}
|
||||
|
||||
get _command() {
|
||||
return typeof this.settings.command === "function" ? this.settings.command() : this.settings.command;
|
||||
}
|
||||
}
|
||||
@ -1,90 +0,0 @@
|
||||
import utils from "../../services/utils.js";
|
||||
import contextMenu, { MenuCommandItem } from "../../menus/context_menu.js";
|
||||
import treeService from "../../services/tree.js";
|
||||
import ButtonFromNoteWidget from "./button_from_note.js";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
import type { CommandNames } from "../../components/app_context.js";
|
||||
import type { WebContents } from "electron";
|
||||
import link from "../../services/link.js";
|
||||
|
||||
export default class HistoryNavigationButton extends ButtonFromNoteWidget {
|
||||
private webContents?: WebContents;
|
||||
|
||||
constructor(launcherNote: FNote, command: string) {
|
||||
super();
|
||||
|
||||
this.title(() => launcherNote.title)
|
||||
.icon(() => launcherNote.getIcon())
|
||||
.command(() => command as CommandNames)
|
||||
.titlePlacement("right")
|
||||
.buttonNoteIdProvider(() => launcherNote.noteId)
|
||||
.onContextMenu((e) => { if (e) this.showContextMenu(e); })
|
||||
.class("launcher-button");
|
||||
}
|
||||
|
||||
doRender() {
|
||||
super.doRender();
|
||||
|
||||
if (utils.isElectron()) {
|
||||
this.webContents = utils.dynamicRequire("@electron/remote").getCurrentWebContents();
|
||||
|
||||
// without this, the history is preserved across frontend reloads
|
||||
this.webContents?.clearHistory();
|
||||
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
|
||||
async showContextMenu(e: JQuery.ContextMenuEvent) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!this.webContents || this.webContents.navigationHistory.length() < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
let items: MenuCommandItem<string>[] = [];
|
||||
|
||||
const history = this.webContents.navigationHistory.getAllEntries();
|
||||
const activeIndex = this.webContents.navigationHistory.getActiveIndex();
|
||||
|
||||
for (const idx in history) {
|
||||
const { notePath } = link.parseNavigationStateFromUrl(history[idx].url);
|
||||
if (!notePath) continue;
|
||||
|
||||
const title = await treeService.getNotePathTitle(notePath);
|
||||
|
||||
items.push({
|
||||
title,
|
||||
command: idx,
|
||||
uiIcon:
|
||||
parseInt(idx) === activeIndex
|
||||
? "bx bx-radio-circle-marked" // compare with type coercion!
|
||||
: parseInt(idx) < activeIndex
|
||||
? "bx bx-left-arrow-alt"
|
||||
: "bx bx-right-arrow-alt"
|
||||
});
|
||||
}
|
||||
|
||||
items.reverse();
|
||||
|
||||
if (items.length > 20) {
|
||||
items = items.slice(0, 50);
|
||||
}
|
||||
|
||||
contextMenu.show({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
items,
|
||||
selectMenuItemHandler: (item: MenuCommandItem<string>) => {
|
||||
if (item && item.command && this.webContents) {
|
||||
const idx = parseInt(item.command, 10);
|
||||
this.webContents.navigationHistory.goToIndex(idx);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
activeNoteChangedEvent() {
|
||||
this.refresh();
|
||||
}
|
||||
}
|
||||
@ -1,99 +0,0 @@
|
||||
import { t } from "../../../services/i18n.js";
|
||||
import AbstractLauncher from "./abstract_launcher.js";
|
||||
import dialogService from "../../../services/dialog.js";
|
||||
import appContext from "../../../components/app_context.js";
|
||||
import utils from "../../../services/utils.js";
|
||||
import linkContextMenuService from "../../../menus/link_context_menu.js";
|
||||
import type FNote from "../../../entities/fnote.js";
|
||||
|
||||
// we're intentionally displaying the launcher title and icon instead of the target,
|
||||
// e.g. you want to make launchers to 2 mermaid diagrams which both have mermaid icon (ok),
|
||||
// but on the launchpad you want them distinguishable.
|
||||
// for titles, the note titles may follow a different scheme than maybe desirable on the launchpad
|
||||
// another reason is the discrepancy between what user sees on the launchpad and in the config (esp. icons).
|
||||
// The only downside is more work in setting up the typical case
|
||||
// where you actually want to have both title and icon in sync, but for those cases there are bookmarks
|
||||
export default class NoteLauncher extends AbstractLauncher {
|
||||
constructor(launcherNote: FNote) {
|
||||
super(launcherNote);
|
||||
|
||||
this.title(() => this.launcherNote.title)
|
||||
.icon(() => this.launcherNote.getIcon())
|
||||
.onClick((widget, evt) => this.launch(evt))
|
||||
.onAuxClick((widget, evt) => this.launch(evt))
|
||||
.onContextMenu(async (evt) => {
|
||||
let targetNoteId = await Promise.resolve(this.getTargetNoteId());
|
||||
|
||||
if (!targetNoteId || !evt) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hoistedNoteId = this.getHoistedNoteId();
|
||||
|
||||
linkContextMenuService.openContextMenu(targetNoteId, evt, {}, hoistedNoteId);
|
||||
});
|
||||
}
|
||||
|
||||
async launch(evt?: JQuery.ClickEvent | JQuery.ContextMenuEvent | JQuery.TriggeredEvent) {
|
||||
// await because subclass overrides can be async
|
||||
const targetNoteId = await this.getTargetNoteId();
|
||||
if (!targetNoteId || evt?.which === 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
const hoistedNoteId = await this.getHoistedNoteId();
|
||||
if (!hoistedNoteId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!evt) {
|
||||
// keyboard shortcut
|
||||
// TODO: Fix once tabManager is ported.
|
||||
//@ts-ignore
|
||||
await appContext.tabManager.openInSameTab(targetNoteId, hoistedNoteId);
|
||||
} else {
|
||||
const ctrlKey = utils.isCtrlKey(evt);
|
||||
const activate = evt.shiftKey ? true : false;
|
||||
|
||||
if ((evt.which === 1 && ctrlKey) || evt.which === 2) {
|
||||
// TODO: Fix once tabManager is ported.
|
||||
//@ts-ignore
|
||||
await appContext.tabManager.openInNewTab(targetNoteId, hoistedNoteId, activate);
|
||||
} else {
|
||||
// TODO: Fix once tabManager is ported.
|
||||
//@ts-ignore
|
||||
await appContext.tabManager.openInSameTab(targetNoteId, hoistedNoteId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getTargetNoteId(): void | string | Promise<string | undefined> {
|
||||
const targetNoteId = this.launcherNote.getRelationValue("target");
|
||||
|
||||
if (!targetNoteId) {
|
||||
dialogService.info(t("note_launcher.this_launcher_doesnt_define_target_note"));
|
||||
return;
|
||||
}
|
||||
|
||||
return targetNoteId;
|
||||
}
|
||||
|
||||
getHoistedNoteId() {
|
||||
return this.launcherNote.getRelationValue("hoistedNote") || appContext.tabManager.getActiveContext()?.hoistedNoteId;
|
||||
}
|
||||
|
||||
getTitle() {
|
||||
const shortcuts = this.launcherNote
|
||||
.getLabels("keyboardShortcut")
|
||||
.map((l) => l.value)
|
||||
.filter((v) => !!v)
|
||||
.join(", ");
|
||||
|
||||
let title = super.getTitle();
|
||||
if (shortcuts) {
|
||||
title += ` (${shortcuts})`;
|
||||
}
|
||||
|
||||
return title;
|
||||
}
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
import type FNote from "../../../entities/fnote.js";
|
||||
import AbstractLauncher from "./abstract_launcher.js";
|
||||
|
||||
export default class ScriptLauncher extends AbstractLauncher {
|
||||
constructor(launcherNote: FNote) {
|
||||
super(launcherNote);
|
||||
|
||||
this.title(() => this.launcherNote.title)
|
||||
.icon(() => this.launcherNote.getIcon())
|
||||
.onClick(() => this.launch());
|
||||
}
|
||||
|
||||
async launch() {
|
||||
if (this.launcherNote.isLabelTruthy("scriptInLauncherContent")) {
|
||||
await this.launcherNote.executeScript();
|
||||
} else {
|
||||
const script = await this.launcherNote.getRelationTarget("script");
|
||||
if (script) {
|
||||
await script.executeScript();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,15 +0,0 @@
|
||||
import NoteLauncher from "./note_launcher.js";
|
||||
import dateNotesService from "../../../services/date_notes.js";
|
||||
import appContext from "../../../components/app_context.js";
|
||||
|
||||
export default class TodayLauncher extends NoteLauncher {
|
||||
async getTargetNoteId() {
|
||||
const todayNote = await dateNotesService.getTodayNote();
|
||||
|
||||
return todayNote?.noteId;
|
||||
}
|
||||
|
||||
getHoistedNoteId() {
|
||||
return appContext.tabManager.getActiveContext()?.hoistedNoteId;
|
||||
}
|
||||
}
|
||||
@ -1,49 +0,0 @@
|
||||
import OnClickButtonWidget from "./onclick_button.js";
|
||||
import linkContextMenuService from "../../menus/link_context_menu.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import appContext from "../../components/app_context.js";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
|
||||
export default class OpenNoteButtonWidget extends OnClickButtonWidget {
|
||||
|
||||
private noteToOpen: FNote;
|
||||
|
||||
constructor(noteToOpen: FNote) {
|
||||
super();
|
||||
|
||||
this.noteToOpen = noteToOpen;
|
||||
|
||||
this.title(() => utils.escapeHtml(this.noteToOpen.title))
|
||||
.icon(() => this.noteToOpen.getIcon())
|
||||
.onClick((widget, evt) => this.launch(evt))
|
||||
.onAuxClick((widget, evt) => this.launch(evt))
|
||||
.onContextMenu((evt) => {
|
||||
if (evt) {
|
||||
linkContextMenuService.openContextMenu(this.noteToOpen.noteId, evt);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async launch(evt: JQuery.ClickEvent | JQuery.TriggeredEvent | JQuery.ContextMenuEvent) {
|
||||
if (evt.which === 3) {
|
||||
return;
|
||||
}
|
||||
const hoistedNoteId = this.getHoistedNoteId();
|
||||
const ctrlKey = utils.isCtrlKey(evt);
|
||||
|
||||
if ((evt.which === 1 && ctrlKey) || evt.which === 2) {
|
||||
const activate = evt.shiftKey ? true : false;
|
||||
await appContext.tabManager.openInNewTab(this.noteToOpen.noteId, hoistedNoteId, activate);
|
||||
} else {
|
||||
await appContext.tabManager.openInSameTab(this.noteToOpen.noteId);
|
||||
}
|
||||
}
|
||||
|
||||
getHoistedNoteId() {
|
||||
return this.noteToOpen.getRelationValue("hoistedNote") || appContext.tabManager.getActiveContext()?.hoistedNoteId;
|
||||
}
|
||||
|
||||
initialRenderCompleteEvent() {
|
||||
// we trigger refresh above
|
||||
}
|
||||
}
|
||||
@ -1,21 +0,0 @@
|
||||
import { t } from "../../services/i18n.js";
|
||||
import protectedSessionHolder from "../../services/protected_session_holder.js";
|
||||
import CommandButtonWidget from "./command_button.js";
|
||||
|
||||
export default class ProtectedSessionStatusWidget extends CommandButtonWidget {
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.class("launcher-button");
|
||||
|
||||
this.settings.icon = () => (protectedSessionHolder.isProtectedSessionAvailable() ? "bx-check-shield" : "bx-shield-quarter");
|
||||
|
||||
this.settings.title = () => (protectedSessionHolder.isProtectedSessionAvailable() ? t("protected_session_status.active") : t("protected_session_status.inactive"));
|
||||
|
||||
this.settings.command = () => (protectedSessionHolder.isProtectedSessionAvailable() ? "leaveProtectedSession" : "enterProtectedSession");
|
||||
}
|
||||
|
||||
protectedSessionStartedEvent() {
|
||||
this.refreshIcon();
|
||||
}
|
||||
}
|
||||
@ -1,133 +0,0 @@
|
||||
import CalendarWidget from "../buttons/calendar.js";
|
||||
import SpacerWidget from "../spacer.js";
|
||||
import BookmarkButtons from "../bookmark_buttons.js";
|
||||
import ProtectedSessionStatusWidget from "../buttons/protected_session_status.js";
|
||||
import SyncStatusWidget from "../sync_status.js";
|
||||
import BasicWidget from "../basic_widget.js";
|
||||
import NoteLauncher from "../buttons/launcher/note_launcher.js";
|
||||
import ScriptLauncher from "../buttons/launcher/script_launcher.js";
|
||||
import CommandButtonWidget from "../buttons/command_button.js";
|
||||
import utils from "../../services/utils.js";
|
||||
import TodayLauncher from "../buttons/launcher/today_launcher.js";
|
||||
import HistoryNavigationButton from "../buttons/history_navigation.js";
|
||||
import QuickSearchLauncherWidget from "../quick_search_launcher.js";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
import type { CommandNames } from "../../components/app_context.js";
|
||||
import AiChatButton from "../buttons/ai_chat_button.js";
|
||||
|
||||
interface InnerWidget extends BasicWidget {
|
||||
settings?: {
|
||||
titlePlacement: "bottom";
|
||||
};
|
||||
}
|
||||
|
||||
export default class LauncherWidget extends BasicWidget {
|
||||
private innerWidget!: InnerWidget;
|
||||
private isHorizontalLayout: boolean;
|
||||
|
||||
constructor(isHorizontalLayout: boolean) {
|
||||
super();
|
||||
|
||||
this.isHorizontalLayout = isHorizontalLayout;
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
return this.innerWidget.isEnabled();
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = this.innerWidget.render();
|
||||
}
|
||||
|
||||
async initLauncher(note: FNote) {
|
||||
if (note.type !== "launcher") {
|
||||
throw new Error(`Note '${note.noteId}' '${note.title}' is not a launcher even though it's in the launcher subtree`);
|
||||
}
|
||||
|
||||
if (!utils.isDesktop() && note.isLabelTruthy("desktopOnly")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const launcherType = note.getLabelValue("launcherType");
|
||||
|
||||
if (glob.TRILIUM_SAFE_MODE && launcherType === "customWidget") {
|
||||
return false;
|
||||
}
|
||||
|
||||
let widget: BasicWidget;
|
||||
if (launcherType === "command") {
|
||||
widget = this.initCommandLauncherWidget(note).class("launcher-button");
|
||||
} else if (launcherType === "note") {
|
||||
widget = new NoteLauncher(note).class("launcher-button");
|
||||
} else if (launcherType === "script") {
|
||||
widget = new ScriptLauncher(note).class("launcher-button");
|
||||
} else if (launcherType === "customWidget") {
|
||||
widget = await this.initCustomWidget(note);
|
||||
} else if (launcherType === "builtinWidget") {
|
||||
widget = this.initBuiltinWidget(note);
|
||||
} else {
|
||||
throw new Error(`Unrecognized launcher type '${launcherType}' for launcher '${note.noteId}' title '${note.title}'`);
|
||||
}
|
||||
|
||||
if (!widget) {
|
||||
throw new Error(`Unknown initialization error for note '${note.noteId}', title '${note.title}'`);
|
||||
}
|
||||
|
||||
this.child(widget);
|
||||
this.innerWidget = widget as InnerWidget;
|
||||
if (this.isHorizontalLayout && this.innerWidget.settings) {
|
||||
this.innerWidget.settings.titlePlacement = "bottom";
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
initCommandLauncherWidget(note: FNote) {
|
||||
return new CommandButtonWidget()
|
||||
.title(() => note.title)
|
||||
.icon(() => note.getIcon())
|
||||
.command(() => note.getLabelValue("command") as CommandNames);
|
||||
}
|
||||
|
||||
async initCustomWidget(note: FNote) {
|
||||
const widget = await note.getRelationTarget("widget");
|
||||
|
||||
if (widget) {
|
||||
return await widget.executeScript();
|
||||
} else {
|
||||
throw new Error(`Custom widget of launcher '${note.noteId}' '${note.title}' is not defined.`);
|
||||
}
|
||||
}
|
||||
|
||||
initBuiltinWidget(note: FNote) {
|
||||
const builtinWidget = note.getLabelValue("builtinWidget");
|
||||
switch (builtinWidget) {
|
||||
case "calendar":
|
||||
return new CalendarWidget(note.title, note.getIcon());
|
||||
case "spacer":
|
||||
// || has to be inside since 0 is a valid value
|
||||
const baseSize = parseInt(note.getLabelValue("baseSize") || "40");
|
||||
const growthFactor = parseInt(note.getLabelValue("growthFactor") || "100");
|
||||
|
||||
return new SpacerWidget(baseSize, growthFactor);
|
||||
case "bookmarks":
|
||||
return new BookmarkButtons(this.isHorizontalLayout);
|
||||
case "protectedSession":
|
||||
return new ProtectedSessionStatusWidget();
|
||||
case "syncStatus":
|
||||
return new SyncStatusWidget();
|
||||
case "backInHistoryButton":
|
||||
return new HistoryNavigationButton(note, "backInNoteHistory");
|
||||
case "forwardInHistoryButton":
|
||||
return new HistoryNavigationButton(note, "forwardInNoteHistory");
|
||||
case "todayInJournal":
|
||||
return new TodayLauncher(note);
|
||||
case "quickSearch":
|
||||
return new QuickSearchLauncherWidget(this.isHorizontalLayout);
|
||||
case "aiChatLauncher":
|
||||
return new AiChatButton(note);
|
||||
default:
|
||||
throw new Error(`Unrecognized builtin widget ${builtinWidget} for launcher ${note.noteId} "${note.title}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
198
apps/client/src/widgets/containers/launcher.tsx
Normal file
198
apps/client/src/widgets/containers/launcher.tsx
Normal file
@ -0,0 +1,198 @@
|
||||
import SyncStatusWidget from "../sync_status.js";
|
||||
import BasicWidget, { wrapReactWidgets } from "../basic_widget.js";
|
||||
import utils, { isMobile } from "../../services/utils.js";
|
||||
import type FNote from "../../entities/fnote.js";
|
||||
import BookmarkButtons from "../launch_bar/BookmarkButtons.jsx";
|
||||
import SpacerWidget from "../launch_bar/SpacerWidget.jsx";
|
||||
import HistoryNavigationButton from "../launch_bar/HistoryNavigation.jsx";
|
||||
import AiChatButton from "../launch_bar/AiChatButton.jsx";
|
||||
import ProtectedSessionStatusWidget from "../launch_bar/ProtectedSessionStatusWidget.jsx";
|
||||
import { VNode } from "preact";
|
||||
import { CommandButton, CustomNoteLauncher, NoteLauncher } from "../launch_bar/GenericButtons.jsx";
|
||||
import date_notes from "../../services/date_notes.js";
|
||||
import { useLegacyWidget, useNoteContext, useNoteRelation, useNoteRelationTarget } from "../react/hooks.jsx";
|
||||
import QuickSearchWidget from "../quick_search.js";
|
||||
import { ParentComponent } from "../react/react_utils.jsx";
|
||||
import { useContext, useEffect, useMemo, useState } from "preact/hooks";
|
||||
import { LaunchBarActionButton, useLauncherIconAndTitle } from "../launch_bar/launch_bar_widgets.jsx";
|
||||
import CalendarWidget from "../launch_bar/CalendarWidget.jsx";
|
||||
|
||||
interface InnerWidget extends BasicWidget {
|
||||
settings?: {
|
||||
titlePlacement: "bottom";
|
||||
};
|
||||
}
|
||||
|
||||
export default class LauncherWidget extends BasicWidget {
|
||||
private innerWidget!: InnerWidget;
|
||||
private isHorizontalLayout: boolean;
|
||||
|
||||
constructor(isHorizontalLayout: boolean) {
|
||||
super();
|
||||
|
||||
this.isHorizontalLayout = isHorizontalLayout;
|
||||
}
|
||||
|
||||
isEnabled() {
|
||||
return this.innerWidget.isEnabled();
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = this.innerWidget.render();
|
||||
}
|
||||
|
||||
async initLauncher(note: FNote) {
|
||||
if (note.type !== "launcher") {
|
||||
throw new Error(`Note '${note.noteId}' '${note.title}' is not a launcher even though it's in the launcher subtree`);
|
||||
}
|
||||
|
||||
if (!utils.isDesktop() && note.isLabelTruthy("desktopOnly")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const launcherType = note.getLabelValue("launcherType");
|
||||
|
||||
if (glob.TRILIUM_SAFE_MODE && launcherType === "customWidget") {
|
||||
return false;
|
||||
}
|
||||
|
||||
let widget: BasicWidget | VNode;
|
||||
if (launcherType === "command") {
|
||||
widget = wrapReactWidgets<BasicWidget>([ <CommandButton launcherNote={note} /> ])[0];
|
||||
} else if (launcherType === "note") {
|
||||
widget = wrapReactWidgets<BasicWidget>([ <NoteLauncher launcherNote={note} /> ])[0];
|
||||
} else if (launcherType === "script") {
|
||||
widget = wrapReactWidgets<BasicWidget>([ <ScriptLauncher launcherNote={note} /> ])[0];
|
||||
} else if (launcherType === "customWidget") {
|
||||
widget = wrapReactWidgets<BasicWidget>([ <CustomWidget launcherNote={note} /> ])[0];
|
||||
} else if (launcherType === "builtinWidget") {
|
||||
widget = wrapReactWidgets<BasicWidget>([ this.initBuiltinWidget(note) ])[0];
|
||||
} else {
|
||||
throw new Error(`Unrecognized launcher type '${launcherType}' for launcher '${note.noteId}' title '${note.title}'`);
|
||||
}
|
||||
|
||||
if (!widget) {
|
||||
throw new Error(`Unknown initialization error for note '${note.noteId}', title '${note.title}'`);
|
||||
}
|
||||
|
||||
this.child(widget);
|
||||
this.innerWidget = widget as InnerWidget;
|
||||
if (this.isHorizontalLayout && this.innerWidget.settings) {
|
||||
this.innerWidget.settings.titlePlacement = "bottom";
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async initCustomWidget(note: FNote) {
|
||||
const widget = await note.getRelationTarget("widget");
|
||||
|
||||
if (widget) {
|
||||
return await widget.executeScript();
|
||||
} else {
|
||||
throw new Error(`Custom widget of launcher '${note.noteId}' '${note.title}' is not defined.`);
|
||||
}
|
||||
}
|
||||
|
||||
initBuiltinWidget(note: FNote) {
|
||||
const builtinWidget = note.getLabelValue("builtinWidget");
|
||||
switch (builtinWidget) {
|
||||
case "calendar":
|
||||
return <CalendarWidget launcherNote={note} />
|
||||
case "spacer":
|
||||
// || has to be inside since 0 is a valid value
|
||||
const baseSize = parseInt(note.getLabelValue("baseSize") || "40");
|
||||
const growthFactor = parseInt(note.getLabelValue("growthFactor") || "100");
|
||||
|
||||
return <SpacerWidget baseSize={baseSize} growthFactor={growthFactor} />;
|
||||
case "bookmarks":
|
||||
return <BookmarkButtons isHorizontalLayout={this.isHorizontalLayout} />
|
||||
case "protectedSession":
|
||||
return <ProtectedSessionStatusWidget />
|
||||
case "syncStatus":
|
||||
return new SyncStatusWidget();
|
||||
case "backInHistoryButton":
|
||||
return <HistoryNavigationButton launcherNote={note} command="backInNoteHistory" />
|
||||
case "forwardInHistoryButton":
|
||||
return <HistoryNavigationButton launcherNote={note} command="forwardInNoteHistory" />
|
||||
case "todayInJournal":
|
||||
return <TodayLauncher launcherNote={note} />
|
||||
case "quickSearch":
|
||||
return <QuickSearchLauncherWidget isHorizontalLayout={this.isHorizontalLayout} />
|
||||
case "aiChatLauncher":
|
||||
return <AiChatButton launcherNote={note} />
|
||||
default:
|
||||
throw new Error(`Unrecognized builtin widget ${builtinWidget} for launcher ${note.noteId} "${note.title}"`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function ScriptLauncher({ launcherNote }: { launcherNote: FNote }) {
|
||||
const { icon, title } = useLauncherIconAndTitle(launcherNote);
|
||||
return (
|
||||
<LaunchBarActionButton
|
||||
icon={icon}
|
||||
text={title}
|
||||
onClick={async () => {
|
||||
if (launcherNote.isLabelTruthy("scriptInLauncherContent")) {
|
||||
await launcherNote.executeScript();
|
||||
} else {
|
||||
const script = await launcherNote.getRelationTarget("script");
|
||||
if (script) {
|
||||
await script.executeScript();
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TodayLauncher({ launcherNote }: { launcherNote: FNote }) {
|
||||
return (
|
||||
<CustomNoteLauncher
|
||||
launcherNote={launcherNote}
|
||||
getTargetNoteId={async () => {
|
||||
const todayNote = await date_notes.getTodayNote();
|
||||
return todayNote?.noteId ?? null;
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function QuickSearchLauncherWidget({ isHorizontalLayout }: { isHorizontalLayout: boolean }) {
|
||||
const widget = useMemo(() => new QuickSearchWidget(), []);
|
||||
const parentComponent = useContext(ParentComponent) as BasicWidget | null;
|
||||
const isEnabled = isHorizontalLayout && !isMobile();
|
||||
parentComponent?.contentSized();
|
||||
|
||||
return (
|
||||
<div>
|
||||
{isEnabled && <LegacyWidgetRenderer widget={widget} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CustomWidget({ launcherNote }: { launcherNote: FNote }) {
|
||||
const [ widgetNote ] = useNoteRelationTarget(launcherNote, "widget");
|
||||
const [ widget, setWidget ] = useState<BasicWidget>();
|
||||
const parentComponent = useContext(ParentComponent) as BasicWidget | null;
|
||||
parentComponent?.contentSized();
|
||||
|
||||
useEffect(() => {
|
||||
widgetNote?.executeScript().then(setWidget);
|
||||
}, [ widgetNote ]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{widget && <LegacyWidgetRenderer widget={widget} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function LegacyWidgetRenderer({ widget }: { widget: BasicWidget }) {
|
||||
const { noteContext } = useNoteContext();
|
||||
const [ widgetEl ] = useLegacyWidget(() => widget, {
|
||||
noteContext
|
||||
});
|
||||
return widgetEl;
|
||||
}
|
||||
16
apps/client/src/widgets/launch_bar/AiChatButton.tsx
Normal file
16
apps/client/src/widgets/launch_bar/AiChatButton.tsx
Normal file
@ -0,0 +1,16 @@
|
||||
import FNote from "../../entities/fnote";
|
||||
import { useTriliumOptionBool } from "../react/hooks";
|
||||
import { LaunchBarActionButton, useLauncherIconAndTitle } from "./launch_bar_widgets";
|
||||
|
||||
export default function AiChatButton({ launcherNote }: { launcherNote: FNote }) {
|
||||
const [ aiEnabled ] = useTriliumOptionBool("aiEnabled");
|
||||
const { icon, title } = useLauncherIconAndTitle(launcherNote);
|
||||
|
||||
return aiEnabled && (
|
||||
<LaunchBarActionButton
|
||||
icon={icon}
|
||||
text={title}
|
||||
triggerCommand="createAiChat"
|
||||
/>
|
||||
)
|
||||
}
|
||||
31
apps/client/src/widgets/launch_bar/BookmarkButtons.css
Normal file
31
apps/client/src/widgets/launch_bar/BookmarkButtons.css
Normal file
@ -0,0 +1,31 @@
|
||||
.bookmark-folder-widget {
|
||||
min-width: 400px;
|
||||
max-height: 500px;
|
||||
padding: 7px 15px 0 15px;
|
||||
font-size: 1.2rem;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.bookmark-folder-widget ul {
|
||||
padding: 0;
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.bookmark-folder-widget .note-link {
|
||||
display: block;
|
||||
padding: 5px 10px 5px 5px;
|
||||
}
|
||||
|
||||
.bookmark-folder-widget .note-link:hover {
|
||||
background-color: var(--accented-background-color);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.dropdown-menu .bookmark-folder-widget a:hover:not(.disabled) {
|
||||
text-decoration: none;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
.bookmark-folder-widget li .note-link {
|
||||
padding-inline-start: 35px;
|
||||
}
|
||||
58
apps/client/src/widgets/launch_bar/BookmarkButtons.tsx
Normal file
58
apps/client/src/widgets/launch_bar/BookmarkButtons.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import { useMemo } from "preact/hooks";
|
||||
import { LaunchBarDropdownButton, useLauncherIconAndTitle, type LaunchBarWidgetProps } from "./launch_bar_widgets";
|
||||
import { CSSProperties } from "preact";
|
||||
import type FNote from "../../entities/fnote";
|
||||
import { useChildNotes, useNoteLabelBoolean } from "../react/hooks";
|
||||
import "./BookmarkButtons.css";
|
||||
import NoteLink from "../react/NoteLink";
|
||||
import { CustomNoteLauncher } from "./GenericButtons";
|
||||
|
||||
const PARENT_NOTE_ID = "_lbBookmarks";
|
||||
|
||||
export default function BookmarkButtons({ isHorizontalLayout }: LaunchBarWidgetProps) {
|
||||
const style = useMemo<CSSProperties>(() => ({
|
||||
display: "flex",
|
||||
flexDirection: isHorizontalLayout ? "row" : "column",
|
||||
contain: "none"
|
||||
}), [ isHorizontalLayout ]);
|
||||
const childNotes = useChildNotes(PARENT_NOTE_ID);
|
||||
|
||||
return (
|
||||
<div style={style}>
|
||||
{childNotes?.map(childNote => <SingleBookmark note={childNote} />)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function SingleBookmark({ note }: { note: FNote }) {
|
||||
const [ bookmarkFolder ] = useNoteLabelBoolean(note, "bookmarkFolder");
|
||||
return bookmarkFolder
|
||||
? <BookmarkFolder note={note} />
|
||||
: <CustomNoteLauncher launcherNote={note} getTargetNoteId={() => note.noteId} />
|
||||
}
|
||||
|
||||
function BookmarkFolder({ note }: { note: FNote }) {
|
||||
const { icon, title } = useLauncherIconAndTitle(note);
|
||||
const childNotes = useChildNotes(note.noteId);
|
||||
|
||||
return (
|
||||
<LaunchBarDropdownButton
|
||||
icon={icon}
|
||||
title={title}
|
||||
>
|
||||
<div className="bookmark-folder-widget">
|
||||
<div className="parent-note">
|
||||
<NoteLink notePath={note.noteId} noPreview showNoteIcon containerClassName="note-link" noTnLink />
|
||||
</div>
|
||||
|
||||
<ul className="children-notes">
|
||||
{childNotes.map(childNote => (
|
||||
<li>
|
||||
<NoteLink notePath={childNote.noteId} noPreview showNoteIcon containerClassName="note-link" noTnLink />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</LaunchBarDropdownButton>
|
||||
)
|
||||
}
|
||||
175
apps/client/src/widgets/launch_bar/Calendar.tsx
Normal file
175
apps/client/src/widgets/launch_bar/Calendar.tsx
Normal file
@ -0,0 +1,175 @@
|
||||
import { useTriliumOptionInt } from "../react/hooks";
|
||||
import clsx from "clsx";
|
||||
import server from "../../services/server";
|
||||
import { VNode } from "preact";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import { Dayjs } from "@triliumnext/commons";
|
||||
import { t } from "../../services/i18n";
|
||||
|
||||
interface DateNotesForMonth {
|
||||
[date: string]: string;
|
||||
}
|
||||
|
||||
const DAYS_OF_WEEK = [
|
||||
t("calendar.sun"),
|
||||
t("calendar.mon"),
|
||||
t("calendar.tue"),
|
||||
t("calendar.wed"),
|
||||
t("calendar.thu"),
|
||||
t("calendar.fri"),
|
||||
t("calendar.sat")
|
||||
];
|
||||
|
||||
interface DateRangeInfo {
|
||||
weekNumbers: number[];
|
||||
dates: Dayjs[];
|
||||
}
|
||||
|
||||
export default function Calendar({ date }: { date: Dayjs }) {
|
||||
const [ rawFirstDayOfWeek ] = useTriliumOptionInt("firstDayOfWeek") ?? 0;
|
||||
const firstDayOfWeekISO = (rawFirstDayOfWeek === 0 ? 7 : rawFirstDayOfWeek);
|
||||
|
||||
const month = date.format('YYYY-MM');
|
||||
const firstDay = date.startOf('month');
|
||||
const firstDayISO = firstDay.isoWeekday();
|
||||
const monthInfo = getMonthInformation(date, firstDayISO, firstDayOfWeekISO);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CalendarWeekHeader rawFirstDayOfWeek={rawFirstDayOfWeek} />
|
||||
<div className="calendar-body" data-calendar-area="month">
|
||||
{firstDayISO !== firstDayOfWeekISO && <PreviousMonthDays date={date} info={monthInfo.prevMonth} />}
|
||||
<CurrentMonthDays date={date} firstDayOfWeekISO={firstDayOfWeekISO} />
|
||||
<NextMonthDays date={date} dates={monthInfo.nextMonth.dates} />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function CalendarWeekHeader({ rawFirstDayOfWeek }: { rawFirstDayOfWeek: number }) {
|
||||
let localeDaysOfWeek = [...DAYS_OF_WEEK];
|
||||
const shifted = localeDaysOfWeek.splice(0, rawFirstDayOfWeek);
|
||||
localeDaysOfWeek = ['', ...localeDaysOfWeek, ...shifted];
|
||||
|
||||
return (
|
||||
<div className="calendar-week">
|
||||
{localeDaysOfWeek.map(dayOfWeek => <span>{dayOfWeek}</span>)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function PreviousMonthDays({ date, info: { dates, weekNumbers } }: { date: Dayjs, info: DateRangeInfo }) {
|
||||
const prevMonth = date.subtract(1, 'month').format('YYYY-MM');
|
||||
const [ dateNotesForPrevMonth, setDateNotesForPrevMonth ] = useState<DateNotesForMonth>();
|
||||
|
||||
useEffect(() => {
|
||||
server.get<DateNotesForMonth>(`special-notes/notes-for-month/${prevMonth}`).then(setDateNotesForPrevMonth);
|
||||
}, [ date ]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<CalendarWeek weekNumber={weekNumbers[0]} />
|
||||
{dates.map(date => <CalendarDay date={date} dateNotesForMonth={dateNotesForPrevMonth} className="calendar-date-prev-month" />)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function CurrentMonthDays({ date, firstDayOfWeekISO }: { date: Dayjs, firstDayOfWeekISO: number }) {
|
||||
let dateCursor = date;
|
||||
const currentMonth = date.month();
|
||||
const items: VNode[] = [];
|
||||
while (dateCursor.month() === currentMonth) {
|
||||
const weekNumber = getWeekNumber(dateCursor, firstDayOfWeekISO);
|
||||
if (dateCursor.isoWeekday() === firstDayOfWeekISO) {
|
||||
items.push(<CalendarWeek weekNumber={weekNumber} />)
|
||||
}
|
||||
|
||||
items.push(<CalendarDay date={dateCursor} dateNotesForMonth={{}} />)
|
||||
dateCursor = dateCursor.add(1, "day");
|
||||
}
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
function NextMonthDays({ date, dates }: { date: Dayjs, dates: Dayjs[] }) {
|
||||
const nextMonth = date.add(1, 'month').format('YYYY-MM');
|
||||
const [ dateNotesForNextMonth, setDateNotesForNextMonth ] = useState<DateNotesForMonth>();
|
||||
|
||||
useEffect(() => {
|
||||
server.get<DateNotesForMonth>(`special-notes/notes-for-month/${nextMonth}`).then(setDateNotesForNextMonth);
|
||||
}, [ date ]);
|
||||
|
||||
return dates.map(date => (
|
||||
<CalendarDay date={date} dateNotesForMonth={dateNotesForNextMonth} className="calendar-date-next-month" />
|
||||
));
|
||||
}
|
||||
|
||||
function CalendarDay({ date, dateNotesForMonth, className }: { date: Dayjs, dateNotesForMonth?: DateNotesForMonth, className?: string }) {
|
||||
return (
|
||||
<a
|
||||
className={clsx("calendar-date", className)}
|
||||
data-calendar-date={date.local().format("YYYY-MM-DD")}
|
||||
>
|
||||
<span>
|
||||
{date.date()}
|
||||
</span>
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
function CalendarWeek({ weekNumber }: { weekNumber: number }) {
|
||||
return (
|
||||
<span className="calendar-week-number calendar-week-number-disabled">{weekNumber}</span>
|
||||
)
|
||||
}
|
||||
|
||||
export function getMonthInformation(date: Dayjs, firstDayISO: number, firstDayOfWeekISO: number) {
|
||||
return {
|
||||
prevMonth: getPrevMonthDays(date, firstDayISO, firstDayOfWeekISO),
|
||||
nextMonth: getNextMonthDays(date, firstDayOfWeekISO)
|
||||
}
|
||||
}
|
||||
|
||||
function getPrevMonthDays(date: Dayjs, firstDayISO: number, firstDayOfWeekISO: number): DateRangeInfo {
|
||||
const prevMonthLastDay = date.subtract(1, 'month').endOf('month');
|
||||
const daysToAdd = (firstDayISO - firstDayOfWeekISO + 7) % 7;
|
||||
const dates: Dayjs[] = [];
|
||||
|
||||
const firstDay = date.startOf('month');
|
||||
const weekNumber = getWeekNumber(firstDay, firstDayOfWeekISO);
|
||||
|
||||
// Get dates from previous month
|
||||
for (let i = daysToAdd - 1; i >= 0; i--) {
|
||||
dates.push(prevMonthLastDay.subtract(i, 'day'));
|
||||
}
|
||||
|
||||
return { weekNumbers: [ weekNumber ], dates };
|
||||
}
|
||||
|
||||
function getNextMonthDays(date: Dayjs, firstDayOfWeekISO: number): DateRangeInfo {
|
||||
const lastDayOfMonth = date.endOf('month');
|
||||
const lastDayISO = lastDayOfMonth.isoWeekday();
|
||||
const lastDayOfUserWeek = ((firstDayOfWeekISO + 6 - 1) % 7) + 1;
|
||||
const nextMonthFirstDay = date.add(1, 'month').startOf('month');
|
||||
const dates: Dayjs[] = [];
|
||||
|
||||
if (lastDayISO !== lastDayOfUserWeek) {
|
||||
const daysToAdd = (lastDayOfUserWeek - lastDayISO + 7) % 7;
|
||||
|
||||
for (let i = 0; i < daysToAdd; i++) {
|
||||
dates.push(nextMonthFirstDay.add(i, 'day'));
|
||||
}
|
||||
}
|
||||
return { weekNumbers: [], dates };
|
||||
}
|
||||
|
||||
export function getWeekNumber(date: Dayjs, firstDayOfWeekISO: number): number {
|
||||
const weekStart = getWeekStartDate(date, firstDayOfWeekISO);
|
||||
return weekStart.isoWeek();
|
||||
}
|
||||
|
||||
function getWeekStartDate(date: Dayjs, firstDayOfWeekISO: number): Dayjs {
|
||||
const currentISO = date.isoWeekday();
|
||||
const diff = (currentISO - firstDayOfWeekISO + 7) % 7;
|
||||
return date.clone().subtract(diff, "day").startOf("day");
|
||||
}
|
||||
@ -175,3 +175,7 @@
|
||||
color: var(--hover-item-text-color);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.calendar-dropdown-widget .form-control {
|
||||
padding: 0;
|
||||
}
|
||||
129
apps/client/src/widgets/launch_bar/CalendarWidget.tsx
Normal file
129
apps/client/src/widgets/launch_bar/CalendarWidget.tsx
Normal file
@ -0,0 +1,129 @@
|
||||
import { Dispatch, StateUpdater, useEffect, useMemo, useState } from "preact/hooks";
|
||||
import FNote from "../../entities/fnote";
|
||||
import { LaunchBarDropdownButton, useLauncherIconAndTitle } from "./launch_bar_widgets";
|
||||
import { Dayjs, dayjs } from "@triliumnext/commons";
|
||||
import appContext from "../../components/app_context";
|
||||
import "./CalendarWidget.css";
|
||||
import Calendar from "./Calendar";
|
||||
import ActionButton from "../react/ActionButton";
|
||||
import { t } from "../../services/i18n";
|
||||
import FormDropdownList from "../react/FormDropdownList";
|
||||
import FormTextBox from "../react/FormTextBox";
|
||||
|
||||
const MONTHS = [
|
||||
t("calendar.january"),
|
||||
t("calendar.february"),
|
||||
t("calendar.march"),
|
||||
t("calendar.april"),
|
||||
t("calendar.may"),
|
||||
t("calendar.june"),
|
||||
t("calendar.july"),
|
||||
t("calendar.august"),
|
||||
t("calendar.september"),
|
||||
t("calendar.october"),
|
||||
t("calendar.november"),
|
||||
t("calendar.december")
|
||||
];
|
||||
|
||||
export default function CalendarWidget({ launcherNote }: { launcherNote: FNote }) {
|
||||
const { title, icon } = useLauncherIconAndTitle(launcherNote);
|
||||
const [ date, setDate ] = useState<Dayjs>();
|
||||
|
||||
return (
|
||||
<LaunchBarDropdownButton
|
||||
icon={icon} title={title}
|
||||
onShown={() => {
|
||||
const dateNote = appContext.tabManager.getActiveContextNote()?.getOwnedLabelValue("dateNote");
|
||||
const activeDate = dateNote ? dayjs(`${dateNote}T12:00:00`) : null;
|
||||
const todaysDate = dayjs();
|
||||
const date = dayjs(activeDate || todaysDate).startOf('month');
|
||||
setDate(date);
|
||||
}}
|
||||
dropdownOptions={{
|
||||
autoClose: "outside"
|
||||
}}
|
||||
>
|
||||
{date && <div className="calendar-dropdown-widget" style={{ width: 400 }}>
|
||||
<CalendarHeader date={date} setDate={setDate} />
|
||||
<Calendar date={date} />
|
||||
</div>}
|
||||
</LaunchBarDropdownButton>
|
||||
)
|
||||
}
|
||||
|
||||
interface CalendarHeaderProps {
|
||||
date: Dayjs;
|
||||
setDate: Dispatch<StateUpdater<Dayjs | undefined>>;
|
||||
}
|
||||
|
||||
function CalendarHeader(props: CalendarHeaderProps) {
|
||||
return (
|
||||
<div className="calendar-header">
|
||||
<CalendarMonthSelector {...props} />
|
||||
<CalendarYearSelector {...props} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CalendarMonthSelector({ date, setDate }: CalendarHeaderProps) {
|
||||
const months = useMemo(() => (
|
||||
Array.from(MONTHS.entries().map(([ index, text ]) => ({
|
||||
index: index.toString(), text
|
||||
})))
|
||||
), []);
|
||||
|
||||
return (
|
||||
<div className="calendar-month-selector">
|
||||
<AdjustDateButton date={date} setDate={setDate} direction="prev" unit="month" />
|
||||
<FormDropdownList
|
||||
values={months} currentValue={date.month().toString()}
|
||||
keyProperty="index" titleProperty="text"
|
||||
onChange={value => {
|
||||
|
||||
}}
|
||||
buttonProps={{ "data-calendar-input": "month" }}
|
||||
/>
|
||||
<AdjustDateButton date={date} setDate={setDate} direction="next" unit="month" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CalendarYearSelector({ date, setDate }: CalendarHeaderProps) {
|
||||
return (
|
||||
<div className="calendar-year-selector">
|
||||
<AdjustDateButton date={date} setDate={setDate} direction="prev" unit="year" />
|
||||
<FormTextBox
|
||||
type="number"
|
||||
min="1900" max="2999" step="1"
|
||||
currentValue={date.year().toString()}
|
||||
onChange={(newValue) => {
|
||||
const year = parseInt(newValue, 10);
|
||||
if (!Number.isNaN(year)) {
|
||||
setDate(date.set("year", year));
|
||||
}
|
||||
}}
|
||||
data-calendar-input="year"
|
||||
/>
|
||||
<AdjustDateButton date={date} setDate={setDate} direction="next" unit="year" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function AdjustDateButton({ date, setDate, unit, direction }: CalendarHeaderProps & {
|
||||
direction: "prev" | "next",
|
||||
unit: "month" | "year"
|
||||
}) {
|
||||
return (
|
||||
<ActionButton
|
||||
icon={direction === "prev" ? "bx bx-chevron-left" : "bx bx-chevron-right" }
|
||||
className="calendar-btn tn-tool-button"
|
||||
noIconActionClass
|
||||
text=""
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
const newDate = direction === "prev" ? date.subtract(1, unit) : date.add(1, unit);
|
||||
setDate(newDate);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
89
apps/client/src/widgets/launch_bar/GenericButtons.tsx
Normal file
89
apps/client/src/widgets/launch_bar/GenericButtons.tsx
Normal file
@ -0,0 +1,89 @@
|
||||
import appContext, { CommandNames } from "../../components/app_context";
|
||||
import FNote from "../../entities/fnote";
|
||||
import link_context_menu from "../../menus/link_context_menu";
|
||||
import { escapeHtml, isCtrlKey } from "../../services/utils";
|
||||
import { useNoteLabel } from "../react/hooks";
|
||||
import { LaunchBarActionButton, useLauncherIconAndTitle } from "./launch_bar_widgets";
|
||||
import dialog from "../../services/dialog";
|
||||
import { t } from "../../services/i18n";
|
||||
|
||||
export function CommandButton({ launcherNote }: { launcherNote: FNote }) {
|
||||
const { icon, title } = useLauncherIconAndTitle(launcherNote);
|
||||
const [ command ] = useNoteLabel(launcherNote, "command");
|
||||
|
||||
return command && (
|
||||
<LaunchBarActionButton
|
||||
icon={icon}
|
||||
text={title}
|
||||
triggerCommand={command as CommandNames}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function CustomNoteLauncher({ launcherNote, getTargetNoteId, getHoistedNoteId }: {
|
||||
launcherNote: FNote,
|
||||
getTargetNoteId: (launcherNote: FNote) => string | null | Promise<string | null>,
|
||||
getHoistedNoteId?: (launcherNote: FNote) => string | null
|
||||
}) {
|
||||
const { icon, title } = useLauncherIconAndTitle(launcherNote);
|
||||
|
||||
async function launch(evt: MouseEvent) {
|
||||
if (evt.which === 3) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetNoteId = await getTargetNoteId(launcherNote);
|
||||
if (!targetNoteId) return;
|
||||
|
||||
const hoistedNoteIdWithDefault = getHoistedNoteId?.(launcherNote) || appContext.tabManager.getActiveContext()?.hoistedNoteId;
|
||||
const ctrlKey = isCtrlKey(evt);
|
||||
|
||||
if ((evt.which === 1 && ctrlKey) || evt.which === 2) {
|
||||
const activate = evt.shiftKey ? true : false;
|
||||
await appContext.tabManager.openInNewTab(targetNoteId, hoistedNoteIdWithDefault, activate);
|
||||
} else {
|
||||
await appContext.tabManager.openInSameTab(targetNoteId);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<LaunchBarActionButton
|
||||
icon={icon}
|
||||
text={escapeHtml(title)}
|
||||
onClick={launch}
|
||||
onAuxClick={launch}
|
||||
onContextMenu={async evt => {
|
||||
evt.preventDefault();
|
||||
const targetNoteId = await getTargetNoteId(launcherNote);
|
||||
if (targetNoteId) {
|
||||
link_context_menu.openContextMenu(targetNoteId, evt);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// we're intentionally displaying the launcher title and icon instead of the target,
|
||||
// e.g. you want to make launchers to 2 mermaid diagrams which both have mermaid icon (ok),
|
||||
// but on the launchpad you want them distinguishable.
|
||||
// for titles, the note titles may follow a different scheme than maybe desirable on the launchpad
|
||||
// another reason is the discrepancy between what user sees on the launchpad and in the config (esp. icons).
|
||||
// The only downside is more work in setting up the typical case
|
||||
// where you actually want to have both title and icon in sync, but for those cases there are bookmarks
|
||||
export function NoteLauncher({ launcherNote, ...restProps }: { launcherNote: FNote, hoistedNoteId?: string }) {
|
||||
return (
|
||||
<CustomNoteLauncher
|
||||
launcherNote={launcherNote}
|
||||
getTargetNoteId={(launcherNote) => {
|
||||
const targetNoteId = launcherNote.getRelationValue("target");
|
||||
if (!targetNoteId) {
|
||||
dialog.info(t("note_launcher.this_launcher_doesnt_define_target_note"));
|
||||
return null;
|
||||
}
|
||||
return targetNoteId;
|
||||
}}
|
||||
getHoistedNoteId={launcherNote => launcherNote.getRelationValue("hoistedNote")}
|
||||
{...restProps}
|
||||
/>
|
||||
);
|
||||
}
|
||||
84
apps/client/src/widgets/launch_bar/HistoryNavigation.tsx
Normal file
84
apps/client/src/widgets/launch_bar/HistoryNavigation.tsx
Normal file
@ -0,0 +1,84 @@
|
||||
import { useEffect, useRef } from "preact/hooks";
|
||||
import FNote from "../../entities/fnote";
|
||||
import { dynamicRequire, isElectron } from "../../services/utils";
|
||||
import { LaunchBarActionButton, useLauncherIconAndTitle } from "./launch_bar_widgets";
|
||||
import type { WebContents } from "electron";
|
||||
import contextMenu, { MenuCommandItem } from "../../menus/context_menu";
|
||||
import tree from "../../services/tree";
|
||||
import link from "../../services/link";
|
||||
|
||||
interface HistoryNavigationProps {
|
||||
launcherNote: FNote;
|
||||
command: "backInNoteHistory" | "forwardInNoteHistory";
|
||||
}
|
||||
|
||||
export default function HistoryNavigationButton({ launcherNote, command }: HistoryNavigationProps) {
|
||||
const { icon, title } = useLauncherIconAndTitle(launcherNote);
|
||||
const webContentsRef = useRef<WebContents>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isElectron()) {
|
||||
const webContents = dynamicRequire("@electron/remote").getCurrentWebContents();
|
||||
// without this, the history is preserved across frontend reloads
|
||||
webContents?.clearHistory();
|
||||
webContentsRef.current = webContents;
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<LaunchBarActionButton
|
||||
icon={icon}
|
||||
text={title}
|
||||
triggerCommand={command}
|
||||
onContextMenu={async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const webContents = webContentsRef.current;
|
||||
if (!webContents || webContents.navigationHistory.length() < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
let items: MenuCommandItem<string>[] = [];
|
||||
|
||||
const history = webContents.navigationHistory.getAllEntries();
|
||||
const activeIndex = webContents.navigationHistory.getActiveIndex();
|
||||
|
||||
for (const idx in history) {
|
||||
const { notePath } = link.parseNavigationStateFromUrl(history[idx].url);
|
||||
if (!notePath) continue;
|
||||
|
||||
const title = await tree.getNotePathTitle(notePath);
|
||||
|
||||
items.push({
|
||||
title,
|
||||
command: idx,
|
||||
uiIcon:
|
||||
parseInt(idx) === activeIndex
|
||||
? "bx bx-radio-circle-marked" // compare with type coercion!
|
||||
: parseInt(idx) < activeIndex
|
||||
? "bx bx-left-arrow-alt"
|
||||
: "bx bx-right-arrow-alt"
|
||||
});
|
||||
}
|
||||
|
||||
items.reverse();
|
||||
|
||||
if (items.length > 20) {
|
||||
items = items.slice(0, 50);
|
||||
}
|
||||
|
||||
contextMenu.show({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
items,
|
||||
selectMenuItemHandler: (item: MenuCommandItem<string>) => {
|
||||
if (item && item.command && webContents) {
|
||||
const idx = parseInt(item.command, 10);
|
||||
webContents.navigationHistory.goToIndex(idx);
|
||||
}
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
import { useState } from "preact/hooks";
|
||||
import protected_session_holder from "../../services/protected_session_holder";
|
||||
import { LaunchBarActionButton } from "./launch_bar_widgets";
|
||||
import { useTriliumEvent } from "../react/hooks";
|
||||
import { t } from "../../services/i18n";
|
||||
|
||||
export default function ProtectedSessionStatusWidget() {
|
||||
const protectedSessionAvailable = useProtectedSessionAvailable();
|
||||
|
||||
return (
|
||||
protectedSessionAvailable ? (
|
||||
<LaunchBarActionButton
|
||||
icon="bx bx-check-shield"
|
||||
text={t("protected_session_status.active")}
|
||||
triggerCommand="leaveProtectedSession"
|
||||
/>
|
||||
) : (
|
||||
<LaunchBarActionButton
|
||||
icon="bx bx-shield-quarter"
|
||||
text={t("protected_session_status.inactive")}
|
||||
triggerCommand="enterProtectedSession"
|
||||
/>
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
function useProtectedSessionAvailable() {
|
||||
const [ protectedSessionAvailable, setProtectedSessionAvailable ] = useState(protected_session_holder.isProtectedSessionAvailable());
|
||||
useTriliumEvent("protectedSessionStarted", () => {
|
||||
setProtectedSessionAvailable(protected_session_holder.isProtectedSessionAvailable());
|
||||
});
|
||||
return protectedSessionAvailable;
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
export default function RightDropdownButton() {
|
||||
return <p>Button goes here.</p>;
|
||||
}
|
||||
35
apps/client/src/widgets/launch_bar/SpacerWidget.tsx
Normal file
35
apps/client/src/widgets/launch_bar/SpacerWidget.tsx
Normal file
@ -0,0 +1,35 @@
|
||||
import appContext, { CommandNames } from "../../components/app_context";
|
||||
import contextMenu from "../../menus/context_menu";
|
||||
import { t } from "../../services/i18n";
|
||||
import { isMobile } from "../../services/utils";
|
||||
|
||||
interface SpacerWidgetProps {
|
||||
baseSize?: number;
|
||||
growthFactor?: number;
|
||||
}
|
||||
|
||||
export default function SpacerWidget({ baseSize, growthFactor }: SpacerWidgetProps) {
|
||||
return (
|
||||
<div
|
||||
className="spacer"
|
||||
style={{
|
||||
flexBasis: baseSize ?? 0,
|
||||
flexGrow: growthFactor ?? 1000,
|
||||
flexShrink: 1000
|
||||
}}
|
||||
onContextMenu={(e) => {
|
||||
e.preventDefault();
|
||||
contextMenu.show<CommandNames>({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
items: [{ title: t("spacer.configure_launchbar"), command: "showLaunchBarSubtree", uiIcon: "bx " + (isMobile() ? "bx-mobile" : "bx-sidebar") }],
|
||||
selectMenuItemHandler: ({ command }) => {
|
||||
if (command) {
|
||||
appContext.triggerCommand(command);
|
||||
}
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
46
apps/client/src/widgets/launch_bar/launch_bar_widgets.tsx
Normal file
46
apps/client/src/widgets/launch_bar/launch_bar_widgets.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import FNote from "../../entities/fnote";
|
||||
import { escapeHtml } from "../../services/utils";
|
||||
import ActionButton, { ActionButtonProps } from "../react/ActionButton";
|
||||
import Dropdown, { DropdownProps } from "../react/Dropdown";
|
||||
import { useNoteLabel, useNoteProperty } from "../react/hooks";
|
||||
import Icon from "../react/Icon";
|
||||
|
||||
export interface LaunchBarWidgetProps {
|
||||
isHorizontalLayout: boolean;
|
||||
}
|
||||
|
||||
export function LaunchBarActionButton(props: Omit<ActionButtonProps, "className" | "noIconActionClass" | "titlePosition">) {
|
||||
return (
|
||||
<ActionButton
|
||||
className="button-widget launcher-button"
|
||||
noIconActionClass
|
||||
titlePosition="right"
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export function LaunchBarDropdownButton({ children, icon, ...props }: Pick<DropdownProps, "title" | "children" | "onShown" | "dropdownOptions"> & { icon: string }) {
|
||||
return (
|
||||
<Dropdown
|
||||
className="right-dropdown-widget"
|
||||
buttonClassName="right-dropdown-button launcher-button"
|
||||
hideToggleArrow
|
||||
text={<Icon icon={icon} />}
|
||||
{...props}
|
||||
>{children}</Dropdown>
|
||||
)
|
||||
}
|
||||
|
||||
export function useLauncherIconAndTitle(note: FNote) {
|
||||
const title = useNoteProperty(note, "title");
|
||||
|
||||
// React to changes.
|
||||
useNoteLabel(note, "iconClass");
|
||||
useNoteLabel(note, "workspaceIconClass");
|
||||
|
||||
return {
|
||||
icon: note.getIcon(),
|
||||
title: escapeHtml(title ?? "")
|
||||
};
|
||||
}
|
||||
@ -2,13 +2,13 @@ import { useEffect, useRef, useState } from "preact/hooks";
|
||||
import { CommandNames } from "../../components/app_context";
|
||||
import { useStaticTooltip } from "./hooks";
|
||||
import keyboard_actions from "../../services/keyboard_actions";
|
||||
import { HTMLAttributes } from "preact";
|
||||
|
||||
export interface ActionButtonProps {
|
||||
export interface ActionButtonProps extends Pick<HTMLAttributes<HTMLButtonElement>, "onClick" | "onAuxClick" | "onContextMenu"> {
|
||||
text: string;
|
||||
titlePosition?: "top" | "right" | "bottom" | "left";
|
||||
icon: string;
|
||||
className?: string;
|
||||
onClick?: (e: MouseEvent) => void;
|
||||
triggerCommand?: CommandNames;
|
||||
noIconActionClass?: boolean;
|
||||
frame?: boolean;
|
||||
@ -16,7 +16,7 @@ export interface ActionButtonProps {
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export default function ActionButton({ text, icon, className, onClick, triggerCommand, titlePosition, noIconActionClass, frame, active, disabled }: ActionButtonProps) {
|
||||
export default function ActionButton({ text, icon, className, triggerCommand, titlePosition, noIconActionClass, frame, active, disabled, ...restProps }: ActionButtonProps) {
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
const [ keyboardShortcut, setKeyboardShortcut ] = useState<string[]>();
|
||||
|
||||
@ -35,8 +35,8 @@ export default function ActionButton({ text, icon, className, onClick, triggerCo
|
||||
return <button
|
||||
ref={buttonRef}
|
||||
class={`${className ?? ""} ${!noIconActionClass ? "icon-action" : "btn"} ${icon} ${frame ? "btn btn-primary" : ""} ${disabled ? "disabled" : ""} ${active ? "active" : ""}`}
|
||||
onClick={onClick}
|
||||
data-trigger-command={triggerCommand}
|
||||
disabled={disabled}
|
||||
{...restProps}
|
||||
/>;
|
||||
}
|
||||
|
||||
@ -1,11 +1,16 @@
|
||||
import { Dropdown as BootstrapDropdown } from "bootstrap";
|
||||
import { ComponentChildren } from "preact";
|
||||
import { ComponentChildren, HTMLAttributes } from "preact";
|
||||
import { CSSProperties, HTMLProps } from "preact/compat";
|
||||
import { useCallback, useEffect, useRef, useState } from "preact/hooks";
|
||||
import { useUniqueName } from "./hooks";
|
||||
|
||||
type DataAttributes = {
|
||||
[key: `data-${string}`]: string | number | boolean | undefined;
|
||||
};
|
||||
|
||||
export interface DropdownProps extends Pick<HTMLProps<HTMLDivElement>, "id" | "className"> {
|
||||
buttonClassName?: string;
|
||||
buttonProps?: Partial<HTMLAttributes<HTMLButtonElement> & DataAttributes>;
|
||||
isStatic?: boolean;
|
||||
children: ComponentChildren;
|
||||
title?: string;
|
||||
@ -21,9 +26,10 @@ export interface DropdownProps extends Pick<HTMLProps<HTMLDivElement>, "id" | "c
|
||||
forceShown?: boolean;
|
||||
onShown?: () => void;
|
||||
onHidden?: () => void;
|
||||
dropdownOptions?: Partial<BootstrapDropdown.Options>;
|
||||
}
|
||||
|
||||
export default function Dropdown({ id, className, buttonClassName, isStatic, children, title, text, dropdownContainerStyle, dropdownContainerClassName, hideToggleArrow, iconAction, disabled, noSelectButtonStyle, noDropdownListStyle, forceShown, onShown: externalOnShown, onHidden: externalOnHidden }: DropdownProps) {
|
||||
export default function Dropdown({ id, className, buttonClassName, isStatic, children, title, text, dropdownContainerStyle, dropdownContainerClassName, hideToggleArrow, iconAction, disabled, noSelectButtonStyle, noDropdownListStyle, forceShown, onShown: externalOnShown, onHidden: externalOnHidden, dropdownOptions, buttonProps }: DropdownProps) {
|
||||
const dropdownRef = useRef<HTMLDivElement | null>(null);
|
||||
const triggerRef = useRef<HTMLButtonElement | null>(null);
|
||||
|
||||
@ -32,7 +38,7 @@ export default function Dropdown({ id, className, buttonClassName, isStatic, chi
|
||||
useEffect(() => {
|
||||
if (!triggerRef.current) return;
|
||||
|
||||
const dropdown = BootstrapDropdown.getOrCreateInstance(triggerRef.current);
|
||||
const dropdown = BootstrapDropdown.getOrCreateInstance(triggerRef.current, dropdownOptions);
|
||||
if (forceShown) {
|
||||
dropdown.show();
|
||||
setShown(true);
|
||||
@ -79,6 +85,7 @@ export default function Dropdown({ id, className, buttonClassName, isStatic, chi
|
||||
title={title}
|
||||
id={id ?? ariaId}
|
||||
disabled={disabled}
|
||||
{...buttonProps}
|
||||
>
|
||||
{text}
|
||||
<span className="caret"></span>
|
||||
|
||||
@ -4,6 +4,7 @@ import { useImperativeSearchHighlighlighting, useTriliumEvent } from "./hooks";
|
||||
|
||||
interface NoteLinkOpts {
|
||||
className?: string;
|
||||
containerClassName?: string;
|
||||
notePath: string | string[];
|
||||
showNotePath?: boolean;
|
||||
showNoteIcon?: boolean;
|
||||
@ -17,7 +18,7 @@ interface NoteLinkOpts {
|
||||
noContextMenu?: boolean;
|
||||
}
|
||||
|
||||
export default function NoteLink({ className, notePath, showNotePath, showNoteIcon, style, noPreview, noTnLink, highlightedTokens, title, viewScope, noContextMenu }: NoteLinkOpts) {
|
||||
export default function NoteLink({ className, containerClassName, notePath, showNotePath, showNoteIcon, style, noPreview, noTnLink, highlightedTokens, title, viewScope, noContextMenu }: NoteLinkOpts) {
|
||||
const stringifiedNotePath = Array.isArray(notePath) ? notePath.join("/") : notePath;
|
||||
const noteId = stringifiedNotePath.split("/").at(-1);
|
||||
const ref = useRef<HTMLSpanElement>(null);
|
||||
@ -71,6 +72,6 @@ export default function NoteLink({ className, notePath, showNotePath, showNoteIc
|
||||
$linkEl?.addClass(className);
|
||||
}
|
||||
|
||||
return <span ref={ref} />
|
||||
return <span className={containerClassName} ref={ref} />
|
||||
|
||||
}
|
||||
|
||||
@ -23,6 +23,7 @@ import toast, { ToastOptions } from "../../services/toast";
|
||||
import utils, { escapeRegExp, reloadFrontendApp } from "../../services/utils";
|
||||
import server from "../../services/server";
|
||||
import { removeIndividualBinding } from "../../services/shortcuts";
|
||||
import froca from "../../services/froca";
|
||||
|
||||
export function useTriliumEvent<T extends EventNames>(eventName: T, handler: (data: EventData<T>) => void) {
|
||||
const parentComponent = useContext(ParentComponent);
|
||||
@ -369,6 +370,16 @@ export function useNoteRelation(note: FNote | undefined | null, relationName: Re
|
||||
] as const;
|
||||
}
|
||||
|
||||
export function useNoteRelationTarget(note: FNote, relationName: RelationNames) {
|
||||
const [ targetNote, setTargetNote ] = useState<FNote | null>();
|
||||
|
||||
useEffect(() => {
|
||||
note.getRelationTarget(relationName).then(setTargetNote);
|
||||
}, [ note ]);
|
||||
|
||||
return [ targetNote ] as const;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows a React component to read or write a note's label while also reacting to changes in value.
|
||||
*
|
||||
@ -836,3 +847,15 @@ async function isNoteReadOnly(note: FNote, noteContext: NoteContext) {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export function useChildNotes(parentNoteId: string) {
|
||||
const [ childNotes, setChildNotes ] = useState<FNote[]>([]);
|
||||
async function refreshChildNotes() {
|
||||
const parentNote = await froca.getNote(parentNoteId);
|
||||
const childNotes = await parentNote?.getChildNotes();
|
||||
setChildNotes(childNotes ?? []);
|
||||
}
|
||||
useEffect(() => { refreshChildNotes() }, [ parentNoteId ]);
|
||||
|
||||
return childNotes;
|
||||
}
|
||||
|
||||
@ -1,43 +0,0 @@
|
||||
import { t } from "../services/i18n.js";
|
||||
import BasicWidget from "./basic_widget.js";
|
||||
import contextMenu from "../menus/context_menu.js";
|
||||
import appContext, { type CommandNames } from "../components/app_context.js";
|
||||
import utils from "../services/utils.js";
|
||||
|
||||
const TPL = /*html*/`<div class="spacer"></div>`;
|
||||
|
||||
export default class SpacerWidget extends BasicWidget {
|
||||
private baseSize: number;
|
||||
private growthFactor: number;
|
||||
|
||||
constructor(baseSize = 0, growthFactor = 1000) {
|
||||
super();
|
||||
|
||||
this.baseSize = baseSize;
|
||||
this.growthFactor = growthFactor;
|
||||
}
|
||||
|
||||
doRender() {
|
||||
this.$widget = $(TPL);
|
||||
this.$widget.css("flex-basis", this.baseSize);
|
||||
this.$widget.css("flex-grow", this.growthFactor);
|
||||
this.$widget.css("flex-shrink", 1000);
|
||||
|
||||
this.$widget.on("contextmenu", (e) => {
|
||||
this.$widget.tooltip("hide");
|
||||
|
||||
contextMenu.show<CommandNames>({
|
||||
x: e.pageX,
|
||||
y: e.pageY,
|
||||
items: [{ title: t("spacer.configure_launchbar"), command: "showLaunchBarSubtree", uiIcon: "bx " + (utils.isMobile() ? "bx-mobile" : "bx-sidebar") }],
|
||||
selectMenuItemHandler: ({ command }) => {
|
||||
if (command) {
|
||||
appContext.triggerCommand(command);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return false; // blocks default browser right click menu
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -24,6 +24,10 @@ type Labels = {
|
||||
orderBy: string;
|
||||
orderDirection: string;
|
||||
|
||||
// Launch bar
|
||||
bookmarkFolder: boolean;
|
||||
command: string;
|
||||
|
||||
// Collection-specific
|
||||
viewType: string;
|
||||
status: string;
|
||||
@ -55,7 +59,11 @@ type Labels = {
|
||||
*/
|
||||
type Relations = [
|
||||
"searchScript",
|
||||
"ancestor"
|
||||
"ancestor",
|
||||
|
||||
// Launcher-specific
|
||||
"target",
|
||||
"widget"
|
||||
];
|
||||
|
||||
export type LabelNames = keyof Labels;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user