[PE-155] chore: floating toolbar for pages (#6482)

* chore: add floating toolbar to pages

* fix: locked page toolbar
This commit is contained in:
Aaryan Khandelwal 2025-01-28 20:21:09 +05:30 committed by GitHub
parent 421839ec51
commit b698f44500
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 67 additions and 10 deletions

View file

@ -16,6 +16,7 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => {
const { const {
onTransaction, onTransaction,
aiHandler, aiHandler,
bubbleMenuEnabled = true,
containerClassName, containerClassName,
disabledExtensions, disabledExtensions,
displayConfig = DEFAULT_DISPLAY_CONFIG, displayConfig = DEFAULT_DISPLAY_CONFIG,
@ -75,8 +76,9 @@ const CollaborativeDocumentEditor = (props: ICollaborativeDocumentEditor) => {
return ( return (
<PageRenderer <PageRenderer
displayConfig={displayConfig}
aiHandler={aiHandler} aiHandler={aiHandler}
bubbleMenuEnabled={bubbleMenuEnabled}
displayConfig={displayConfig}
editor={editor} editor={editor}
editorContainerClassName={editorContainerClassNames} editorContainerClassName={editorContainerClassNames}
id={id} id={id}

View file

@ -15,12 +15,13 @@ import { Editor, ReactRenderer } from "@tiptap/react";
// components // components
import { EditorContainer, EditorContentWrapper } from "@/components/editors"; import { EditorContainer, EditorContentWrapper } from "@/components/editors";
import { LinkView, LinkViewProps } from "@/components/links"; import { LinkView, LinkViewProps } from "@/components/links";
import { AIFeaturesMenu, BlockMenu } from "@/components/menus"; import { AIFeaturesMenu, BlockMenu, EditorBubbleMenu } from "@/components/menus";
// types // types
import { TAIHandler, TDisplayConfig } from "@/types"; import { TAIHandler, TDisplayConfig } from "@/types";
type IPageRenderer = { type IPageRenderer = {
aiHandler?: TAIHandler; aiHandler?: TAIHandler;
bubbleMenuEnabled: boolean;
displayConfig: TDisplayConfig; displayConfig: TDisplayConfig;
editor: Editor; editor: Editor;
editorContainerClassName: string; editorContainerClassName: string;
@ -29,7 +30,7 @@ type IPageRenderer = {
}; };
export const PageRenderer = (props: IPageRenderer) => { export const PageRenderer = (props: IPageRenderer) => {
const { aiHandler, displayConfig, editor, editorContainerClassName, id, tabIndex } = props; const { aiHandler, bubbleMenuEnabled, displayConfig, editor, editorContainerClassName, id, tabIndex } = props;
// states // states
const [linkViewProps, setLinkViewProps] = useState<LinkViewProps>(); const [linkViewProps, setLinkViewProps] = useState<LinkViewProps>();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
@ -141,6 +142,7 @@ export const PageRenderer = (props: IPageRenderer) => {
<EditorContentWrapper editor={editor} id={id} tabIndex={tabIndex} /> <EditorContentWrapper editor={editor} id={id} tabIndex={tabIndex} />
{editor.isEditable && ( {editor.isEditable && (
<div> <div>
{bubbleMenuEnabled && <EditorBubbleMenu editor={editor} />}
<BlockMenu editor={editor} /> <BlockMenu editor={editor} />
<AIFeaturesMenu menu={aiHandler?.menu} /> <AIFeaturesMenu menu={aiHandler?.menu} />
</div> </div>

View file

@ -69,6 +69,7 @@ const DocumentReadOnlyEditor = (props: IDocumentReadOnlyEditor) => {
return ( return (
<PageRenderer <PageRenderer
bubbleMenuEnabled={false}
displayConfig={displayConfig} displayConfig={displayConfig}
editor={editor} editor={editor}
editorContainerClassName={editorContainerClassName} editorContainerClassName={editorContainerClassName}

View file

@ -138,8 +138,9 @@ export interface IRichTextEditor extends IEditorProps {
export interface ICollaborativeDocumentEditor export interface ICollaborativeDocumentEditor
extends Omit<IEditorProps, "initialValue" | "onChange" | "onEnterKeyPress" | "value"> { extends Omit<IEditorProps, "initialValue" | "onChange" | "onEnterKeyPress" | "value"> {
editable: boolean;
aiHandler?: TAIHandler; aiHandler?: TAIHandler;
bubbleMenuEnabled?: boolean;
editable: boolean;
embedHandler: TEmbedConfig; embedHandler: TEmbedConfig;
handleEditorReady?: (value: boolean) => void; handleEditorReady?: (value: boolean) => void;
id: string; id: string;

View file

@ -36,6 +36,7 @@ import { TPageInstance } from "@/store/pages/base-page";
export type TPageActions = export type TPageActions =
| "full-screen" | "full-screen"
| "sticky-toolbar"
| "copy-markdown" | "copy-markdown"
| "toggle-lock" | "toggle-lock"
| "toggle-access" | "toggle-access"

View file

@ -30,9 +30,9 @@ export const PageOptionsDropdown: React.FC<Props> = observer((props) => {
// router // router
const router = useRouter(); const router = useRouter();
// store values // store values
const { name } = page; const { name, isContentEditable } = page;
// page filters // page filters
const { isFullWidth, handleFullWidth } = usePageFilters(); const { isFullWidth, handleFullWidth, isStickyToolbarEnabled, handleStickyToolbar } = usePageFilters();
// update query params // update query params
const { updateQueryParams } = useQueryParams(); const { updateQueryParams } = useQueryParams();
// menu items list // menu items list
@ -49,6 +49,18 @@ export const PageOptionsDropdown: React.FC<Props> = observer((props) => {
), ),
className: "flex items-center justify-between gap-2", className: "flex items-center justify-between gap-2",
}, },
{
key: "sticky-toolbar",
action: () => handleStickyToolbar(!isStickyToolbarEnabled),
customContent: (
<>
Sticky toolbar
<ToggleSwitch value={isStickyToolbarEnabled} onChange={() => {}} />
</>
),
className: "flex items-center justify-between gap-2",
shouldRender: isContentEditable,
},
{ {
key: "copy-markdown", key: "copy-markdown",
action: () => { action: () => {
@ -86,7 +98,16 @@ export const PageOptionsDropdown: React.FC<Props> = observer((props) => {
shouldRender: true, shouldRender: true,
}, },
], ],
[editorRef, handleFullWidth, isFullWidth, router, updateQueryParams] [
editorRef,
handleFullWidth,
handleStickyToolbar,
isContentEditable,
isFullWidth,
isStickyToolbarEnabled,
router,
updateQueryParams,
]
); );
return ( return (
@ -102,6 +123,7 @@ export const PageOptionsDropdown: React.FC<Props> = observer((props) => {
extraOptions={EXTRA_MENU_OPTIONS} extraOptions={EXTRA_MENU_OPTIONS}
optionsOrder={[ optionsOrder={[
"full-screen", "full-screen",
"sticky-toolbar",
"copy-link", "copy-link",
"make-a-copy", "make-a-copy",
"move", "move",

View file

@ -23,7 +23,7 @@ export const PageEditorHeaderRoot: React.FC<Props> = observer((props) => {
// derived values // derived values
const { isContentEditable } = page; const { isContentEditable } = page;
// page filters // page filters
const { isFullWidth } = usePageFilters(); const { isFullWidth, isStickyToolbarEnabled } = usePageFilters();
// derived values // derived values
const resolvedEditorRef = editorRef.current; const resolvedEditorRef = editorRef.current;
@ -48,7 +48,9 @@ export const PageEditorHeaderRoot: React.FC<Props> = observer((props) => {
/> />
</div> </div>
)} )}
{editorReady && isContentEditable && editorRef.current && <PageToolbar editorRef={editorRef?.current} />} {isStickyToolbarEnabled && editorReady && isContentEditable && editorRef.current && (
<PageToolbar editorRef={editorRef?.current} />
)}
</Header.LeftItem> </Header.LeftItem>
<PageExtraOptions editorRef={resolvedEditorRef} page={page} /> <PageExtraOptions editorRef={resolvedEditorRef} page={page} />
</Header> </Header>

View file

@ -8,12 +8,14 @@ export type TPagesPersonalizationConfig = {
full_width: boolean; full_width: boolean;
font_size: TEditorFontSize; font_size: TEditorFontSize;
font_style: TEditorFontStyle; font_style: TEditorFontStyle;
sticky_toolbar: boolean;
}; };
const DEFAULT_PERSONALIZATION_VALUES: TPagesPersonalizationConfig = { const DEFAULT_PERSONALIZATION_VALUES: TPagesPersonalizationConfig = {
full_width: false, full_width: false,
font_size: "large-font", font_size: "large-font",
font_style: "sans-serif", font_style: "sans-serif",
sticky_toolbar: true,
}; };
export const usePageFilters = () => { export const usePageFilters = () => {
@ -23,7 +25,17 @@ export const usePageFilters = () => {
DEFAULT_PERSONALIZATION_VALUES DEFAULT_PERSONALIZATION_VALUES
); );
// stored values // stored values
const isFullWidth = useMemo(() => !!pagesConfig?.full_width, [pagesConfig?.full_width]); const isFullWidth = useMemo(
() => (pagesConfig?.full_width === undefined ? DEFAULT_PERSONALIZATION_VALUES.full_width : pagesConfig?.full_width),
[pagesConfig?.full_width]
);
const isStickyToolbarEnabled = useMemo(
() =>
pagesConfig?.sticky_toolbar === undefined
? DEFAULT_PERSONALIZATION_VALUES.sticky_toolbar
: pagesConfig?.sticky_toolbar,
[pagesConfig?.sticky_toolbar]
);
const fontSize = useMemo( const fontSize = useMemo(
() => pagesConfig?.font_size ?? DEFAULT_PERSONALIZATION_VALUES.font_size, () => pagesConfig?.font_size ?? DEFAULT_PERSONALIZATION_VALUES.font_size,
[pagesConfig?.font_size] [pagesConfig?.font_size]
@ -78,6 +90,18 @@ export const usePageFilters = () => {
}, },
[handleUpdateConfig] [handleUpdateConfig]
); );
/**
* @description action to update full_width value
* @param {boolean} value
*/
const handleStickyToolbar = useCallback(
(value: boolean) => {
handleUpdateConfig({
sticky_toolbar: value,
});
},
[handleUpdateConfig]
);
return { return {
fontSize, fontSize,
@ -86,5 +110,7 @@ export const usePageFilters = () => {
handleFontStyle, handleFontStyle,
isFullWidth, isFullWidth,
handleFullWidth, handleFullWidth,
isStickyToolbarEnabled,
handleStickyToolbar,
}; };
}; };