diff --git a/apps/client/src/layouts/layout_commons.tsx b/apps/client/src/layouts/layout_commons.tsx index 031ef03de..62f810430 100644 --- a/apps/client/src/layouts/layout_commons.tsx +++ b/apps/client/src/layouts/layout_commons.tsx @@ -22,16 +22,8 @@ import RevisionsDialog from "../widgets/dialogs/revisions.js"; import DeleteNotesDialog from "../widgets/dialogs/delete_notes.js"; import InfoDialog from "../widgets/dialogs/info.js"; import IncorrectCpuArchDialog from "../widgets/dialogs/incorrect_cpu_arch.js"; -import PopupEditorDialog from "../widgets/dialogs/popup_editor.js"; -import FlexContainer from "../widgets/containers/flex_container.js"; -import NoteIconWidget from "../widgets/note_icon"; -import PromotedAttributesWidget from "../widgets/promoted_attributes.js"; import CallToActionDialog from "../widgets/dialogs/call_to_action.jsx"; -import NoteTitleWidget from "../widgets/note_title.jsx"; -import FormattingToolbar from "../widgets/ribbon/FormattingToolbar.js"; -import NoteList from "../widgets/collections/NoteList.jsx"; -import NoteDetail from "../widgets/NoteDetail.jsx"; -import StandaloneRibbonAdapter from "../widgets/ribbon/components/StandaloneRibbonAdapter.jsx"; +import PopupEditorDialog from "../widgets/dialogs/PopupEditor.jsx"; export function applyModals(rootContainer: RootContainer) { rootContainer @@ -57,16 +49,6 @@ export function applyModals(rootContainer: RootContainer) { .child() .child() .child() - .child(new PopupEditorDialog() - .child(new FlexContainer("row") - .class("title-row") - .css("align-items", "center") - .cssBlock(".title-row > * { margin: 5px; }") - .child() - .child()) - .child() - .child(new PromotedAttributesWidget()) - .child() - .child()) + .child() .child(); } diff --git a/apps/client/src/stylesheets/style.css b/apps/client/src/stylesheets/style.css index 9ef1c5e2d..36805a3d4 100644 --- a/apps/client/src/stylesheets/style.css +++ b/apps/client/src/stylesheets/style.css @@ -2591,7 +2591,7 @@ iframe.print-iframe { flex-direction: column; } -.scrolling-container > .note-detail.full-height, +.note-detail.full-height, .scrolling-container > .note-list-widget.full-height { position: relative; flex-grow: 1; diff --git a/apps/client/src/widgets/dialogs/PopupEditor.css b/apps/client/src/widgets/dialogs/PopupEditor.css new file mode 100644 index 000000000..325d9ea47 --- /dev/null +++ b/apps/client/src/widgets/dialogs/PopupEditor.css @@ -0,0 +1,64 @@ +/** Reduce the z-index of modals so that ckeditor popups are properly shown on top of it. */ +body.popup-editor-open > .modal-backdrop { z-index: 998; } +body.popup-editor-open .popup-editor-dialog { z-index: 999; } +body.popup-editor-open .ck-clipboard-drop-target-line { z-index: 1000; } + +body.desktop .modal.popup-editor-dialog .modal-dialog { + max-width: 75vw; +} + +.modal.popup-editor-dialog .modal-header .modal-title { + font-size: 1.1em; +} + +.modal.popup-editor-dialog .modal-header .title-row { + flex-grow: 1; + display: flex; + align-items: center; +} + +.modal.popup-editor-dialog .modal-header .title-row > * { + margin: 5px; +} + +.modal.popup-editor-dialog .modal-body { + padding: 0; + height: 75vh; + overflow: auto; + display: flex; + flex-direction: column; +} + +.modal.popup-editor-dialog .note-detail-editable-text { + padding: 0 1em; +} + +.modal.popup-editor-dialog .title-row, +.modal.popup-editor-dialog .modal-title, +.modal.popup-editor-dialog .note-icon-widget { + height: 32px; +} + +.modal.popup-editor-dialog .note-icon-widget { + width: 32px; + margin: 0; + padding: 0; +} + +.modal.popup-editor-dialog .note-icon-widget button.note-icon, +.modal.popup-editor-dialog .note-title-widget input.note-title { + font-size: 1em; +} + +.modal.popup-editor-dialog .classic-toolbar-widget { + position: sticky; + top: 0; + inset-inline-start: 0; + inset-inline-end: 0; + background: var(--modal-background-color); + z-index: 998; +} + +.modal.popup-editor-dialog .note-detail-file { + padding: 0; +} \ No newline at end of file diff --git a/apps/client/src/widgets/dialogs/PopupEditor.tsx b/apps/client/src/widgets/dialogs/PopupEditor.tsx new file mode 100644 index 000000000..c2d98b544 --- /dev/null +++ b/apps/client/src/widgets/dialogs/PopupEditor.tsx @@ -0,0 +1,85 @@ +import { useContext, useEffect, useRef, useState } from "preact/hooks"; +import Modal from "../react/Modal"; +import "./PopupEditor.css"; +import { useNoteContext, useTriliumEvent } from "../react/hooks"; +import NoteTitleWidget from "../note_title"; +import NoteIcon from "../note_icon"; +import NoteContext from "../../components/note_context"; +import { NoteContextContext, ParentComponent } from "../react/react_utils"; +import NoteDetail from "../NoteDetail"; +import { ComponentChildren } from "preact"; +import NoteList from "../collections/NoteList"; +import StandaloneRibbonAdapter from "../ribbon/components/StandaloneRibbonAdapter"; +import FormattingToolbar from "../ribbon/FormattingToolbar"; + +export default function PopupEditor() { + const [ shown, setShown ] = useState(false); + const parentComponent = useContext(ParentComponent); + const [ noteContext, setNoteContext ] = useState(new NoteContext("_popup-editor")); + + useTriliumEvent("openInPopup", async ({ noteIdOrPath }) => { + const noteContext = new NoteContext("_popup-editor"); + await noteContext.setNote(noteIdOrPath, { + viewScope: { + readOnlyTemporarilyDisabled: true + } + }); + + setNoteContext(noteContext); + setShown(true); + }); + + // Add a global class to be able to handle issues with z-index due to rendering in a popup. + useEffect(() => { + document.body.classList.toggle("popup-editor-open", shown); + }, [shown]); + + return ( + + + } + className="popup-editor-dialog" + size="lg" + show={shown} + onShown={() => { + parentComponent?.handleEvent("focusOnDetail", { ntxId: noteContext.ntxId }); + }} + onHidden={() => setShown(false)} + > + + + + + + + ) +} + +export function DialogWrapper({ children }: { children: ComponentChildren }) { + const { note } = useNoteContext(); + const wrapperRef = useRef(null); + const [ hasTint, setHasTint ] = useState(false); + + // Apply the tinted-dialog class only if the custom color CSS class specifies a hue + useEffect(() => { + if (!wrapperRef.current) return; + const customHue = getComputedStyle(wrapperRef.current).getPropertyValue("--custom-color-hue"); + setHasTint(!!customHue); + }, [ note ]); + + return ( +
+ {children} +
+ ) +} + +export function TitleRow() { + return ( +
+ + +
+ ) +} diff --git a/apps/client/src/widgets/dialogs/popup_editor.ts b/apps/client/src/widgets/dialogs/popup_editor.ts deleted file mode 100644 index 80f8f5915..000000000 --- a/apps/client/src/widgets/dialogs/popup_editor.ts +++ /dev/null @@ -1,187 +0,0 @@ -import type { EventNames, EventData } from "../../components/app_context.js"; -import NoteContext from "../../components/note_context.js"; -import { openDialog } from "../../services/dialog.js"; -import BasicWidget, { ReactWrappedWidget } from "../basic_widget.js"; -import Container from "../containers/container.js"; - -const TPL = /*html*/`\ - -`; - -export default class PopupEditorDialog extends Container { - - private noteContext: NoteContext; - private $modalHeader!: JQuery; - private $modalBody!: JQuery; - private $wrapper!: JQuery; - - constructor() { - super(); - this.noteContext = new NoteContext("_popup-editor"); - } - - doRender() { - // This will populate this.$widget with the content of the children. - super.doRender(); - - // Now we wrap it in the modal. - const $newWidget = $(TPL); - this.$modalHeader = $newWidget.find(".modal-title"); - this.$modalBody = $newWidget.find(".modal-body"); - this.$wrapper = $newWidget.find(".quick-edit-dialog-wrapper"); - - const children = this.$widget.children(); - this.$modalHeader.append(children[0]); - this.$modalBody.append(children.slice(1)); - this.$widget = $newWidget; - this.setVisibility(false); - } - - async openInPopupEvent({ noteIdOrPath }: EventData<"openInPopup">) { - const $dialog = await openDialog(this.$widget, false, { - focus: false - }); - - await this.noteContext.setNote(noteIdOrPath, { - viewScope: { - readOnlyTemporarilyDisabled: true - } - }); - - const colorClass = this.noteContext.note?.getColorClass(); - const wrapperElement = this.$wrapper.get(0)!; - - if (colorClass) { - wrapperElement.className = "quick-edit-dialog-wrapper " + colorClass; - } else { - wrapperElement.className = "quick-edit-dialog-wrapper"; - } - - const customHue = getComputedStyle(wrapperElement).getPropertyValue("--custom-color-hue"); - if (customHue) { - /* Apply the tinted-dialog class only if the custom color CSS class specifies a hue */ - wrapperElement.classList.add("tinted-quick-edit-dialog"); - } - - const activeEl = document.activeElement; - if (activeEl && "blur" in activeEl) { - (activeEl as HTMLElement).blur(); - } - - $dialog.on("shown.bs.modal", async () => { - await this.handleEventInChildren("activeContextChanged", { noteContext: this.noteContext }); - this.setVisibility(true); - await this.handleEventInChildren("focusOnDetail", { ntxId: this.noteContext.ntxId }); - }); - $dialog.on("hidden.bs.modal", () => { - const $typeWidgetEl = $dialog.find(".note-detail-printable"); - if ($typeWidgetEl.length) { - const typeWidget = glob.getComponentByEl($typeWidgetEl[0]) as ReactWrappedWidget; - typeWidget.cleanup(); - } - - this.setVisibility(false); - }); - } - - setVisibility(visible: boolean) { - const $bodyItems = this.$modalBody.find("> div"); - if (visible) { - $bodyItems.fadeIn(); - this.$modalHeader.children().show(); - document.body.classList.add("popup-editor-open"); - - } else { - $bodyItems.hide(); - this.$modalHeader.children().hide(); - document.body.classList.remove("popup-editor-open"); - } - } - - handleEventInChildren(name: T, data: EventData): Promise | null { - // Avoid events related to the current tab interfere with our popup. - if (["noteSwitched", "noteSwitchedAndActivated", "exportAsPdf", "printActiveNote"].includes(name)) { - return Promise.resolve(); - } - - // Avoid not showing recent notes when creating a new empty tab. - if ("noteContext" in data && data.noteContext.ntxId !== "_popup-editor") { - return Promise.resolve(); - } - - return super.handleEventInChildren(name, data); - } - -} diff --git a/apps/client/src/widgets/react/hooks.tsx b/apps/client/src/widgets/react/hooks.tsx index 856896f2c..34a7c9ed8 100644 --- a/apps/client/src/widgets/react/hooks.tsx +++ b/apps/client/src/widgets/react/hooks.tsx @@ -2,7 +2,7 @@ import { CSSProperties } from "preact/compat"; import { DragData } from "../note_tree"; import { FilterLabelsByType, KeyboardActionNames, OptionNames, RelationNames } from "@triliumnext/commons"; import { MutableRef, useCallback, useContext, useDebugValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from "preact/hooks"; -import { ParentComponent, refToJQuerySelector } from "./react_utils"; +import { NoteContextContext, ParentComponent, refToJQuerySelector } from "./react_utils"; import { RefObject, VNode } from "preact"; import { Tooltip } from "bootstrap"; import { ViewMode, ViewScope } from "../../services/link"; @@ -257,18 +257,29 @@ export function useUniqueName(prefix?: string) { } export function useNoteContext() { - const [ noteContext, setNoteContext ] = useState(); + const noteContextContext = useContext(NoteContextContext); + const [ noteContext, setNoteContext ] = useState(noteContextContext ?? undefined); const [ notePath, setNotePath ] = useState(); const [ note, setNote ] = useState(); const [ , setViewScope ] = useState(); const [ isReadOnlyTemporarilyDisabled, setIsReadOnlyTemporarilyDisabled ] = useState(noteContext?.viewScope?.isReadOnly); const [ refreshCounter, setRefreshCounter ] = useState(0); + useEffect(() => { + if (!noteContextContext) return; + setNoteContext(noteContextContext); + setNote(noteContextContext.note); + setNotePath(noteContextContext.notePath); + setViewScope(noteContextContext.viewScope); + setIsReadOnlyTemporarilyDisabled(noteContextContext?.viewScope?.readOnlyTemporarilyDisabled); + }, [ noteContextContext ]); + useEffect(() => { setNote(noteContext?.note); }, [ notePath ]); useTriliumEvents([ "setNoteContext", "activeContextChanged", "noteSwitchedAndActivated", "noteSwitched" ], ({ noteContext }) => { + if (noteContextContext) return; setNoteContext(noteContext); setNotePath(noteContext.notePath); setViewScope(noteContext.viewScope); @@ -282,6 +293,7 @@ export function useNoteContext() { } }); useTriliumEvent("readOnlyTemporarilyDisabled", ({ noteContext: eventNoteContext }) => { + if (noteContextContext) return; if (eventNoteContext.ntxId === noteContext?.ntxId) { setIsReadOnlyTemporarilyDisabled(eventNoteContext?.viewScope?.readOnlyTemporarilyDisabled); } diff --git a/apps/client/src/widgets/react/react_utils.tsx b/apps/client/src/widgets/react/react_utils.tsx index d752662f5..468a0e73e 100644 --- a/apps/client/src/widgets/react/react_utils.tsx +++ b/apps/client/src/widgets/react/react_utils.tsx @@ -1,8 +1,11 @@ import { ComponentChild, createContext, render, type JSX, type RefObject } from "preact"; import Component from "../../components/component"; +import NoteContext from "../../components/note_context"; export const ParentComponent = createContext(null); +export const NoteContextContext = createContext(null); + /** * Takes in a React ref and returns a corresponding JQuery selector. * diff --git a/apps/client/src/widgets/type_widgets/helpers/SvgSplitEditor.tsx b/apps/client/src/widgets/type_widgets/helpers/SvgSplitEditor.tsx index 8a9e64a24..7f95bfb1b 100644 --- a/apps/client/src/widgets/type_widgets/helpers/SvgSplitEditor.tsx +++ b/apps/client/src/widgets/type_widgets/helpers/SvgSplitEditor.tsx @@ -163,7 +163,12 @@ function useResizer(containerRef: RefObject, noteId: string, svg pan: zoomInstance.getPan(), zoom: zoomInstance.getZoom() } - zoomInstance.destroy(); + try { + zoomInstance.destroy(); + } catch (e) { + // Sometimes crashes with "Matrix is not invertible" which can cause havoc such as breaking the popup editor from ever showing up again. + console.warn(e); + } }; }, [ svg ]);