feat(collections/calendar): use own UI for header

This commit is contained in:
Elian Doran 2025-09-06 12:26:42 +03:00
parent 49c80f0e0b
commit afc17f41f6
No known key found for this signature in database
6 changed files with 116 additions and 20 deletions

View File

@ -293,6 +293,11 @@ button.close:hover {
pointer-events: none; pointer-events: none;
} }
.icon-action.btn {
padding: 0 8px;
min-width: unset !important;
}
.ui-widget-content a:not(.ui-tabs-anchor) { .ui-widget-content a:not(.ui-tabs-anchor) {
color: #337ab7 !important; color: #337ab7 !important;
} }

View File

@ -587,7 +587,17 @@
"september": "September", "september": "September",
"october": "October", "october": "October",
"november": "November", "november": "November",
"december": "December" "december": "December",
"week": "Week",
"week_previous": "Previous week",
"week_next": "Next week",
"month": "Month",
"month_previous": "Previous month",
"month_next": "Next month",
"year": "Year",
"year_previous": "Previous year",
"year_next": "Next year",
"list": "List"
}, },
"close_pane_button": { "close_pane_button": {
"close_this_pane": "Close this pane" "close_this_pane": "Close this pane"

View File

@ -60,3 +60,20 @@ body.desktop:not(.zen) .calendar-container .fc-toolbar.fc-header-toolbar {
opacity: 0.85; opacity: 0.85;
overflow: hidden; overflow: hidden;
} }
/* #region Header */
.calendar-header {
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 10px;
}
.calendar-header .btn {
min-width: unset !important;
}
.calendar-header > .title {
flex-grow: 1;
}
/* #endregion */

View File

@ -16,18 +16,50 @@ import date_notes from "../../../services/date_notes";
import appContext from "../../../components/app_context"; import appContext from "../../../components/app_context";
import { DateClickArg } from "@fullcalendar/interaction"; import { DateClickArg } from "@fullcalendar/interaction";
import FNote from "../../../entities/fnote"; import FNote from "../../../entities/fnote";
import Button, { ButtonGroup } from "../../react/Button";
import ActionButton from "../../react/ActionButton";
import { RefObject } from "preact";
interface CalendarViewData { interface CalendarViewData {
} }
interface CalendarViewData {
type: string;
name: string;
previousText: string;
nextText: string;
}
const CALENDAR_VIEWS = [ const CALENDAR_VIEWS = [
"timeGridWeek", {
"dayGridMonth", type: "timeGridWeek",
"multiMonthYear", name: t("calendar.week"),
"listMonth" previousText: t("calendar.week_previous"),
nextText: t("calendar.week_next")
},
{
type: "dayGridMonth",
name: t("calendar.month"),
previousText: t("calendar.month_previous"),
nextText: t("calendar.month_next")
},
{
type: "multiMonthYear",
name: t("calendar.year"),
previousText: t("calendar.year_previous"),
nextText: t("calendar.year_next")
},
{
type: "listMonth",
name: t("calendar.list"),
previousText: t("calendar.month_previous"),
nextText: t("calendar.month_next")
}
] ]
const SUPPORTED_CALENDAR_VIEW_TYPE = CALENDAR_VIEWS.map(v => v.type);
// Here we hard-code the imports in order to ensure that they are embedded by webpack without having to load all the languages. // Here we hard-code the imports in order to ensure that they are embedded by webpack without having to load all the languages.
export const LOCALE_MAPPINGS: Record<LOCALE_IDS, (() => Promise<{ default: LocaleInput }>) | null> = { export const LOCALE_MAPPINGS: Record<LOCALE_IDS, (() => Promise<{ default: LocaleInput }>) | null> = {
de: () => import("@fullcalendar/core/locales/de"), de: () => import("@fullcalendar/core/locales/de"),
@ -83,20 +115,18 @@ export default function CalendarView({ note, noteIds }: ViewModeProps<CalendarVi
return (plugins && return (plugins &&
<div className="calendar-view" ref={containerRef}> <div className="calendar-view" ref={containerRef}>
<CalendarHeader calendarRef={calendarRef} />
<Calendar <Calendar
events={eventBuilder} events={eventBuilder}
calendarRef={calendarRef} calendarRef={calendarRef}
plugins={plugins} plugins={plugins}
tabIndex={100} tabIndex={100}
initialView={initialView.current && CALENDAR_VIEWS.includes(initialView.current) ? initialView.current : "dayGridMonth"} initialView={initialView.current && SUPPORTED_CALENDAR_VIEW_TYPE.includes(initialView.current) ? initialView.current : "dayGridMonth"}
headerToolbar={{ headerToolbar={false}
start: "title",
end: `${CALENDAR_VIEWS.join(",")} today prev,next`
}}
firstDay={firstDayOfWeek ?? 0} firstDay={firstDayOfWeek ?? 0}
weekends={!hideWeekends} weekends={!hideWeekends}
weekNumbers={weekNumbers} weekNumbers={weekNumbers}
height="100%" height="90%"
nowIndicator nowIndicator
handleWindowResize={false} handleWindowResize={false}
locale={locale} locale={locale}
@ -113,6 +143,31 @@ export default function CalendarView({ note, noteIds }: ViewModeProps<CalendarVi
); );
} }
function CalendarHeader({ calendarRef }: { calendarRef: RefObject<FullCalendar> }) {
const currentViewType = calendarRef.current?.view?.type;
const currentViewData = CALENDAR_VIEWS.find(v => calendarRef.current && v.type === currentViewType);
return (
<div className="calendar-header">
<span className="title">{calendarRef.current?.view.title}</span>
<ButtonGroup>
{CALENDAR_VIEWS.map(viewData => (
<Button
text={viewData.name.toLocaleLowerCase()}
className={currentViewType === viewData.type ? "active" : ""}
onClick={() => calendarRef.current?.changeView(viewData.type)}
/>
))}
</ButtonGroup>
<Button text="today" onClick={() => calendarRef.current?.today()} />
<ButtonGroup>
<ActionButton icon="bx bx-chevron-left" text={currentViewData?.previousText ?? ""} frame onClick={() => calendarRef.current?.prev()} />
<ActionButton icon="bx bx-chevron-right" text={currentViewData?.nextText ?? ""} frame onClick={() => calendarRef.current?.next()} />
</ButtonGroup>
</div>
)
}
function usePlugins(isEditable: boolean, isCalendarRoot: boolean) { function usePlugins(isEditable: boolean, isCalendarRoot: boolean) {
const [ plugins, setPlugins ] = useState<PluginDef[]>(); const [ plugins, setPlugins ] = useState<PluginDef[]>();

View File

@ -11,9 +11,10 @@ export interface ActionButtonProps {
onClick?: (e: MouseEvent) => void; onClick?: (e: MouseEvent) => void;
triggerCommand?: CommandNames; triggerCommand?: CommandNames;
noIconActionClass?: boolean; noIconActionClass?: boolean;
frame?: boolean;
} }
export default function ActionButton({ text, icon, className, onClick, triggerCommand, titlePosition, noIconActionClass }: ActionButtonProps) { export default function ActionButton({ text, icon, className, onClick, triggerCommand, titlePosition, noIconActionClass, frame }: ActionButtonProps) {
const buttonRef = useRef<HTMLButtonElement>(null); const buttonRef = useRef<HTMLButtonElement>(null);
const [ keyboardShortcut, setKeyboardShortcut ] = useState<string[]>(); const [ keyboardShortcut, setKeyboardShortcut ] = useState<string[]>();
@ -31,7 +32,7 @@ export default function ActionButton({ text, icon, className, onClick, triggerCo
return <button return <button
ref={buttonRef} ref={buttonRef}
class={`${className ?? ""} ${!noIconActionClass ? "icon-action" : "btn"} ${icon}`} class={`${className ?? ""} ${!noIconActionClass ? "icon-action" : "btn"} ${icon} ${frame ? "btn btn-primary" : ""}`}
onClick={onClick} onClick={onClick}
data-trigger-command={triggerCommand} data-trigger-command={triggerCommand}
/>; />;

View File

@ -1,4 +1,4 @@
import type { RefObject } from "preact"; import type { ComponentChildren, RefObject } from "preact";
import type { CSSProperties } from "preact/compat"; import type { CSSProperties } from "preact/compat";
import { useMemo } from "preact/hooks"; import { useMemo } from "preact/hooks";
import { memo } from "preact/compat"; import { memo } from "preact/compat";
@ -72,4 +72,12 @@ const Button = memo(({ name, buttonRef, className, text, onClick, keyboardShortc
); );
}); });
export function ButtonGroup({ children }: { children: ComponentChildren }) {
return (
<div className="btn-group" role="group">
{children}
</div>
)
}
export default Button; export default Button;