Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9d75df1384 | |||
| 11f6e17153 | |||
| 7d7d8760bb | |||
| f628ee1dff | |||
| e3fd8a5421 | |||
| b07351cd33 | |||
| 9c0cd96013 | |||
| c1e73505b2 | |||
| 06d9f4b5aa | |||
| 3bc345dfcb |
@@ -0,0 +1,8 @@
|
||||
/* Display Placeholders on every new line. */
|
||||
.ProseMirror p.is-empty::before {
|
||||
content: attr(data-placeholder);
|
||||
float: left;
|
||||
color: var(--color-placeholder);
|
||||
pointer-events: none;
|
||||
height: 0;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
@import "@plane/propel/styles/fonts";
|
||||
@import url("editor.css");
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
/* Display Placeholders on every new line. */
|
||||
.ProseMirror p.is-empty::before {
|
||||
content: attr(data-placeholder);
|
||||
float: left;
|
||||
color: var(--color-placeholder);
|
||||
pointer-events: none;
|
||||
height: 0;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
@import "@plane/propel/styles/fonts";
|
||||
@import url("editor.css");
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Extensions } from "@tiptap/core";
|
||||
import React from "react";
|
||||
import React, { useMemo } from "react";
|
||||
// plane imports
|
||||
import { cn } from "@plane/utils";
|
||||
// components
|
||||
@@ -20,16 +20,20 @@ const CollaborativeDocumentEditor: React.FC<ICollaborativeDocumentEditorProps> =
|
||||
aiHandler,
|
||||
bubbleMenuEnabled = true,
|
||||
containerClassName,
|
||||
dragDropEnabled = true,
|
||||
extensions: externalExtensions = [],
|
||||
disabledExtensions,
|
||||
displayConfig = DEFAULT_DISPLAY_CONFIG,
|
||||
editable,
|
||||
editorClassName = "",
|
||||
editorProps,
|
||||
embedHandler,
|
||||
fileHandler,
|
||||
flaggedExtensions,
|
||||
forwardedRef,
|
||||
handleEditorReady,
|
||||
id,
|
||||
isMobile = false,
|
||||
mentionHandler,
|
||||
onAssetChange,
|
||||
onChange,
|
||||
@@ -41,21 +45,27 @@ const CollaborativeDocumentEditor: React.FC<ICollaborativeDocumentEditorProps> =
|
||||
user,
|
||||
} = props;
|
||||
|
||||
const extensions: Extensions = [];
|
||||
const extensions: Extensions = useMemo(() => {
|
||||
const allExtensions = externalExtensions;
|
||||
|
||||
if (embedHandler?.issue) {
|
||||
extensions.push(
|
||||
WorkItemEmbedExtension({
|
||||
widgetCallback: embedHandler.issue.widgetCallback,
|
||||
})
|
||||
);
|
||||
}
|
||||
if (embedHandler?.issue) {
|
||||
allExtensions.push(
|
||||
WorkItemEmbedExtension({
|
||||
widgetCallback: embedHandler.issue.widgetCallback,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return allExtensions;
|
||||
}, [externalExtensions, embedHandler.issue]);
|
||||
|
||||
// use document editor
|
||||
const { editor, hasServerConnectionFailed, hasServerSynced } = useCollaborativeEditor({
|
||||
disabledExtensions,
|
||||
dragDropEnabled,
|
||||
editable,
|
||||
editorClassName,
|
||||
editorProps,
|
||||
embedHandler,
|
||||
extensions,
|
||||
fileHandler,
|
||||
@@ -91,6 +101,7 @@ const CollaborativeDocumentEditor: React.FC<ICollaborativeDocumentEditorProps> =
|
||||
editorContainerClassName={cn(editorContainerClassNames, "document-editor")}
|
||||
id={id}
|
||||
isLoading={!hasServerSynced && !hasServerConnectionFailed}
|
||||
isMobile={isMobile}
|
||||
tabIndex={tabIndex}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -15,12 +15,22 @@ type Props = {
|
||||
editorContainerClassName: string;
|
||||
id: string;
|
||||
isLoading?: boolean;
|
||||
isMobile: boolean;
|
||||
tabIndex?: number;
|
||||
};
|
||||
|
||||
export const PageRenderer = (props: Props) => {
|
||||
const { aiHandler, bubbleMenuEnabled, displayConfig, editor, editorContainerClassName, id, isLoading, tabIndex } =
|
||||
props;
|
||||
const {
|
||||
aiHandler,
|
||||
bubbleMenuEnabled,
|
||||
displayConfig,
|
||||
editor,
|
||||
editorContainerClassName,
|
||||
id,
|
||||
isLoading,
|
||||
isMobile,
|
||||
tabIndex,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -36,9 +46,10 @@ export const PageRenderer = (props: Props) => {
|
||||
editor={editor}
|
||||
editorContainerClassName={editorContainerClassName}
|
||||
id={id}
|
||||
isMobile={isMobile}
|
||||
>
|
||||
<EditorContentWrapper editor={editor} id={id} tabIndex={tabIndex} />
|
||||
{editor.isEditable && (
|
||||
{editor.isEditable && !isMobile && (
|
||||
<div>
|
||||
{bubbleMenuEnabled && <EditorBubbleMenu editor={editor} />}
|
||||
<BlockMenu editor={editor} />
|
||||
|
||||
@@ -25,6 +25,7 @@ const DocumentReadOnlyEditor: React.FC<IDocumentReadOnlyEditorProps> = (props) =
|
||||
fileHandler,
|
||||
flaggedExtensions,
|
||||
id,
|
||||
isMobile = false,
|
||||
forwardedRef,
|
||||
handleEditorReady,
|
||||
initialValue,
|
||||
@@ -64,6 +65,7 @@ const DocumentReadOnlyEditor: React.FC<IDocumentReadOnlyEditorProps> = (props) =
|
||||
editor={editor}
|
||||
editorContainerClassName={cn(editorContainerClassName, "document-editor")}
|
||||
id={id}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -16,10 +16,11 @@ interface EditorContainerProps {
|
||||
editor: Editor;
|
||||
editorContainerClassName: string;
|
||||
id: string;
|
||||
isMobile: boolean;
|
||||
}
|
||||
|
||||
export const EditorContainer: FC<EditorContainerProps> = (props) => {
|
||||
const { children, displayConfig, editor, editorContainerClassName, id } = props;
|
||||
const { children, displayConfig, editor, editorContainerClassName, id, isMobile } = props;
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleContainerClick = (event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
@@ -94,7 +95,7 @@ export const EditorContainer: FC<EditorContainerProps> = (props) => {
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
<LinkViewContainer editor={editor} containerRef={containerRef} />
|
||||
{!isMobile && <LinkViewContainer editor={editor} containerRef={containerRef} />}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -12,7 +12,11 @@ export const EditorContentWrapper: FC<EditorContentProps> = (props) => {
|
||||
const { editor, children, id, tabIndex } = props;
|
||||
|
||||
return (
|
||||
<div tabIndex={tabIndex} onFocus={() => editor?.chain().focus(undefined, { scrollIntoView: false }).run()}>
|
||||
<div
|
||||
tabIndex={tabIndex}
|
||||
onFocus={() => editor?.chain().focus(undefined, { scrollIntoView: false }).run()}
|
||||
className="editor-content"
|
||||
>
|
||||
<EditorContent editor={editor} />
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -12,8 +12,6 @@ import { EditorContentWrapper } from "./editor-content";
|
||||
|
||||
type Props = IEditorProps & {
|
||||
children?: (editor: Editor) => React.ReactNode;
|
||||
editable: boolean;
|
||||
extensions: Extensions;
|
||||
};
|
||||
|
||||
export const EditorWrapper: React.FC<Props> = (props) => {
|
||||
@@ -22,11 +20,13 @@ export const EditorWrapper: React.FC<Props> = (props) => {
|
||||
containerClassName,
|
||||
disabledExtensions,
|
||||
displayConfig = DEFAULT_DISPLAY_CONFIG,
|
||||
editable,
|
||||
editable = true,
|
||||
editorClassName = "",
|
||||
editorProps,
|
||||
extensions,
|
||||
id,
|
||||
initialValue,
|
||||
isMobile = false,
|
||||
fileHandler,
|
||||
flaggedExtensions,
|
||||
forwardedRef,
|
||||
@@ -41,9 +41,10 @@ export const EditorWrapper: React.FC<Props> = (props) => {
|
||||
} = props;
|
||||
|
||||
const editor = useEditor({
|
||||
editable,
|
||||
disabledExtensions,
|
||||
editable,
|
||||
editorClassName,
|
||||
editorProps,
|
||||
enableHistory: true,
|
||||
extensions,
|
||||
fileHandler,
|
||||
@@ -75,6 +76,7 @@ export const EditorWrapper: React.FC<Props> = (props) => {
|
||||
editor={editor}
|
||||
editorContainerClassName={editorContainerClassName}
|
||||
id={id}
|
||||
isMobile={isMobile}
|
||||
>
|
||||
{children?.(editor)}
|
||||
<div className="flex flex-col">
|
||||
|
||||
@@ -21,6 +21,7 @@ export const ReadOnlyEditorWrapper = (props: IReadOnlyEditorProps) => {
|
||||
forwardedRef,
|
||||
id,
|
||||
initialValue,
|
||||
isMobile = false,
|
||||
mentionHandler,
|
||||
} = props;
|
||||
|
||||
@@ -47,6 +48,7 @@ export const ReadOnlyEditorWrapper = (props: IReadOnlyEditorProps) => {
|
||||
editor={editor}
|
||||
editorContainerClassName={editorContainerClassName}
|
||||
id={id}
|
||||
isMobile={isMobile}
|
||||
>
|
||||
<div className="flex flex-col">
|
||||
<EditorContentWrapper editor={editor} id={id} />
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
MinusSquare,
|
||||
Palette,
|
||||
AlignCenter,
|
||||
LinkIcon,
|
||||
} from "lucide-react";
|
||||
// constants
|
||||
import { CORE_EXTENSIONS } from "@/constants/extension";
|
||||
@@ -30,6 +31,7 @@ import {
|
||||
insertHorizontalRule,
|
||||
insertImage,
|
||||
insertTableCommand,
|
||||
setLinkEditor,
|
||||
setText,
|
||||
setTextAlign,
|
||||
toggleBackgroundColor,
|
||||
@@ -44,6 +46,7 @@ import {
|
||||
toggleTaskList,
|
||||
toggleTextColor,
|
||||
toggleUnderline,
|
||||
unsetLinkEditor,
|
||||
} from "@/helpers/editor-commands";
|
||||
// types
|
||||
import { TCommandWithProps, TEditorCommands } from "@/types";
|
||||
@@ -189,7 +192,7 @@ export const ImageItem = (editor: Editor): EditorMenuItem<"image"> => ({
|
||||
icon: ImageIcon,
|
||||
});
|
||||
|
||||
export const HorizontalRuleItem = (editor: Editor) =>
|
||||
export const HorizontalRuleItem = (editor: Editor): EditorMenuItem<"divider"> =>
|
||||
({
|
||||
key: "divider",
|
||||
name: "Divider",
|
||||
@@ -198,6 +201,19 @@ export const HorizontalRuleItem = (editor: Editor) =>
|
||||
icon: MinusSquare,
|
||||
}) as const;
|
||||
|
||||
export const LinkItem = (editor: Editor): EditorMenuItem<"link"> =>
|
||||
({
|
||||
key: "link",
|
||||
name: "Link",
|
||||
isActive: () => editor?.isActive("link"),
|
||||
command: (props) => {
|
||||
if (!props) return;
|
||||
if (props.url) setLinkEditor(editor, props.url, props.text);
|
||||
else unsetLinkEditor(editor);
|
||||
},
|
||||
icon: LinkIcon,
|
||||
}) as const;
|
||||
|
||||
export const TextColorItem = (editor: Editor): EditorMenuItem<"text-color"> => ({
|
||||
key: "text-color",
|
||||
name: "Color",
|
||||
@@ -255,6 +271,7 @@ export const getEditorMenuItems = (editor: Editor | null): EditorMenuItem<TEdito
|
||||
ImageItem(editor),
|
||||
HorizontalRuleItem(editor),
|
||||
TextColorItem(editor),
|
||||
LinkItem(editor),
|
||||
BackgroundColorItem(editor),
|
||||
TextAlignItem(editor),
|
||||
];
|
||||
|
||||
@@ -28,6 +28,7 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
|
||||
extension,
|
||||
getPos,
|
||||
imageFromFileSystem,
|
||||
isMobile,
|
||||
node,
|
||||
selected,
|
||||
setEditorContainer,
|
||||
@@ -188,11 +189,13 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
|
||||
const handleImageMouseDown = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
if (isMobile) editor.commands.blur();
|
||||
const pos = getPos();
|
||||
const nodeSelection = NodeSelection.create(editor.state.doc, pos);
|
||||
editor.view.dispatch(editor.state.tr.setSelection(nodeSelection));
|
||||
},
|
||||
[editor, getPos]
|
||||
[editor, getPos, isMobile]
|
||||
);
|
||||
|
||||
// show the image loader if the remote image's src or preview image from filesystem is not set yet (while loading the image post upload) (or)
|
||||
@@ -251,10 +254,14 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
|
||||
if (!imageRef.current) {
|
||||
throw new Error("Image reference not found");
|
||||
}
|
||||
if (!resolvedImageSrc) {
|
||||
throw new Error("No resolved image source available");
|
||||
}
|
||||
imageRef.current.src = resolvedImageSrc;
|
||||
|
||||
// That's a extra step for mobile since they cannot auto-resolve URLs via img tags like web app can
|
||||
// due to limitations with cross-origin authentication cookies
|
||||
const resolvedSrc = await extension?.options?.getImageSource?.(imgNodeSrc);
|
||||
if (!resolvedSrc) {
|
||||
throw new Error("No resolved image source available");
|
||||
}
|
||||
imageRef.current.src = resolvedSrc.toString();
|
||||
} catch {
|
||||
// if the image failed to even restore, then show the error state
|
||||
setFailedToLoadImage(true);
|
||||
@@ -282,6 +289,7 @@ export const CustomImageBlock: React.FC<CustomImageBlockProps> = (props) => {
|
||||
alignment={nodeAlignment ?? "left"}
|
||||
width={size.width}
|
||||
height={size.height}
|
||||
isMobile={isMobile}
|
||||
aspectRatio={size.aspectRatio === null ? 1 : size.aspectRatio}
|
||||
src={resolvedImageSrc}
|
||||
downloadSrc={resolvedDownloadSrc}
|
||||
|
||||
@@ -18,13 +18,14 @@ export type CustomImageNodeViewProps = Omit<NodeViewProps, "extension" | "update
|
||||
};
|
||||
updateAttributes: (attrs: Partial<TCustomImageAttributes>) => void;
|
||||
selected: boolean;
|
||||
isMobile: boolean;
|
||||
};
|
||||
|
||||
export const CustomImageNodeView: React.FC<CustomImageNodeViewProps> = (props) => {
|
||||
const { editor, extension, node } = props;
|
||||
const { src: imgNodeSrc } = node.attrs;
|
||||
|
||||
const [isUploaded, setIsUploaded] = useState(false);
|
||||
const [isUploaded, setIsUploaded] = useState(!!imgNodeSrc);
|
||||
const [resolvedSrc, setResolvedSrc] = useState<string | undefined>(undefined);
|
||||
const [resolvedDownloadSrc, setResolvedDownloadSrc] = useState<string | undefined>(undefined);
|
||||
const [imageFromFileSystem, setImageFromFileSystem] = useState<string | undefined>(undefined);
|
||||
@@ -43,13 +44,13 @@ export const CustomImageNodeView: React.FC<CustomImageNodeViewProps> = (props) =
|
||||
// the image is already uploaded if the image-component node has src attribute
|
||||
// and we need to remove the blob from our file system
|
||||
useEffect(() => {
|
||||
if (resolvedSrc) {
|
||||
if (resolvedSrc || imgNodeSrc) {
|
||||
setIsUploaded(true);
|
||||
setImageFromFileSystem(undefined);
|
||||
} else {
|
||||
setIsUploaded(false);
|
||||
}
|
||||
}, [resolvedSrc]);
|
||||
}, [resolvedSrc, imgNodeSrc]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!imgNodeSrc) {
|
||||
|
||||
@@ -11,10 +11,11 @@ type Props = {
|
||||
activeAlignment: TCustomImageAlignment;
|
||||
handleChange: (alignment: TCustomImageAlignment) => void;
|
||||
toggleToolbarViewStatus: (val: boolean) => void;
|
||||
tooltipDisabled?: boolean;
|
||||
};
|
||||
|
||||
export const ImageAlignmentAction: React.FC<Props> = (props) => {
|
||||
const { activeAlignment, handleChange, toggleToolbarViewStatus } = props;
|
||||
const { activeAlignment, handleChange, toggleToolbarViewStatus, tooltipDisabled = false } = props;
|
||||
// states
|
||||
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
||||
// refs
|
||||
@@ -30,7 +31,7 @@ export const ImageAlignmentAction: React.FC<Props> = (props) => {
|
||||
|
||||
return (
|
||||
<div ref={dropdownRef} className="h-full relative">
|
||||
<Tooltip tooltipContent="Align">
|
||||
<Tooltip disabled={tooltipDisabled} tooltipContent="Align">
|
||||
<button
|
||||
type="button"
|
||||
className="h-full flex items-center gap-1 text-white/60 hover:text-white transition-colors"
|
||||
@@ -43,7 +44,7 @@ export const ImageAlignmentAction: React.FC<Props> = (props) => {
|
||||
{isDropdownOpen && (
|
||||
<div className="absolute top-full left-1/2 -translate-x-1/2 mt-0.5 h-7 bg-black/80 flex items-center gap-2 px-2 rounded">
|
||||
{IMAGE_ALIGNMENT_OPTIONS.map((option) => (
|
||||
<Tooltip key={option.value} tooltipContent={option.label}>
|
||||
<Tooltip disabled={tooltipDisabled} key={option.value} tooltipContent={option.label}>
|
||||
<button
|
||||
type="button"
|
||||
className="flex-shrink-0 h-full grid place-items-center text-white/60 hover:text-white transition-colors"
|
||||
|
||||
@@ -4,13 +4,14 @@ import { Tooltip } from "@plane/ui";
|
||||
|
||||
type Props = {
|
||||
src: string;
|
||||
tooltipDisabled?: boolean;
|
||||
};
|
||||
|
||||
export const ImageDownloadAction: React.FC<Props> = (props) => {
|
||||
const { src } = props;
|
||||
const { src, tooltipDisabled = false } = props;
|
||||
|
||||
return (
|
||||
<Tooltip tooltipContent="Download">
|
||||
<Tooltip disabled={tooltipDisabled} tooltipContent="Download">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => window.open(src, "_blank")}
|
||||
|
||||
+35
-19
@@ -12,6 +12,7 @@ const ZOOM_STEPS = [0.5, 1, 1.5, 2];
|
||||
type Props = {
|
||||
aspectRatio: number;
|
||||
isFullScreenEnabled: boolean;
|
||||
isMobile: boolean;
|
||||
downloadSrc: string;
|
||||
src: string;
|
||||
toggleFullScreenMode: (val: boolean) => void;
|
||||
@@ -19,7 +20,7 @@ type Props = {
|
||||
};
|
||||
|
||||
const ImageFullScreenModalWithoutPortal = (props: Props) => {
|
||||
const { aspectRatio, isFullScreenEnabled, downloadSrc, src, toggleFullScreenMode, width } = props;
|
||||
const { aspectRatio, isFullScreenEnabled, isMobile, downloadSrc, src, toggleFullScreenMode, width } = props;
|
||||
// refs
|
||||
const dragStart = useRef({ x: 0, y: 0 });
|
||||
const dragOffset = useRef({ x: 0, y: 0 });
|
||||
@@ -102,6 +103,9 @@ const ImageFullScreenModalWithoutPortal = (props: Props) => {
|
||||
);
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
if (!imgRef.current) return;
|
||||
|
||||
const imgWidth = imgRef.current.offsetWidth * magnification;
|
||||
@@ -233,7 +237,11 @@ const ImageFullScreenModalWithoutPortal = (props: Props) => {
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleMagnification("decrease")}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleMagnification("decrease");
|
||||
}}
|
||||
className="size-6 grid place-items-center text-white/60 hover:text-white disabled:text-white/30 transition-colors duration-200"
|
||||
disabled={magnification <= MIN_ZOOM}
|
||||
aria-label="Zoom out"
|
||||
@@ -243,7 +251,11 @@ const ImageFullScreenModalWithoutPortal = (props: Props) => {
|
||||
<span className="text-sm w-12 text-center text-white">{Math.round(100 * magnification)}%</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleMagnification("increase")}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleMagnification("increase");
|
||||
}}
|
||||
className="size-6 grid place-items-center text-white/60 hover:text-white disabled:text-white/30 transition-colors duration-200"
|
||||
disabled={magnification >= MAX_ZOOM}
|
||||
aria-label="Zoom in"
|
||||
@@ -251,22 +263,26 @@ const ImageFullScreenModalWithoutPortal = (props: Props) => {
|
||||
<Plus className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => window.open(downloadSrc, "_blank")}
|
||||
className="flex-shrink-0 size-8 grid place-items-center text-white/60 hover:text-white transition-colors duration-200"
|
||||
aria-label="Download image"
|
||||
>
|
||||
<Download className="size-4" />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => window.open(src, "_blank")}
|
||||
className="flex-shrink-0 size-8 grid place-items-center text-white/60 hover:text-white transition-colors duration-200"
|
||||
aria-label="Open image in new tab"
|
||||
>
|
||||
<ExternalLink className="size-4" />
|
||||
</button>
|
||||
{!isMobile && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => window.open(downloadSrc, "_blank")}
|
||||
className="flex-shrink-0 size-8 grid place-items-center text-white/60 hover:text-white transition-colors duration-200"
|
||||
aria-label="Download image"
|
||||
>
|
||||
<Download className="size-4" />
|
||||
</button>
|
||||
)}
|
||||
{!isMobile && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => window.open(src, "_blank")}
|
||||
className="flex-shrink-0 size-8 grid place-items-center text-white/60 hover:text-white transition-colors duration-200"
|
||||
aria-label="Open image in new tab"
|
||||
>
|
||||
<ExternalLink className="size-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+4
-2
@@ -13,11 +13,12 @@ type Props = {
|
||||
width: string;
|
||||
aspectRatio: number;
|
||||
};
|
||||
isMobile: boolean;
|
||||
toggleToolbarViewStatus: (val: boolean) => void;
|
||||
};
|
||||
|
||||
export const ImageFullScreenActionRoot: React.FC<Props> = (props) => {
|
||||
const { image, toggleToolbarViewStatus } = props;
|
||||
const { image, isMobile, toggleToolbarViewStatus } = props;
|
||||
// states
|
||||
const [isFullScreenEnabled, setIsFullScreenEnabled] = useState(false);
|
||||
// derived values
|
||||
@@ -32,12 +33,13 @@ export const ImageFullScreenActionRoot: React.FC<Props> = (props) => {
|
||||
<ImageFullScreenModal
|
||||
aspectRatio={aspectRatio}
|
||||
isFullScreenEnabled={isFullScreenEnabled}
|
||||
isMobile={isMobile}
|
||||
src={src}
|
||||
downloadSrc={downloadSrc}
|
||||
width={width}
|
||||
toggleFullScreenMode={setIsFullScreenEnabled}
|
||||
/>
|
||||
<Tooltip tooltipContent="View in full screen">
|
||||
<Tooltip disabled={isMobile} tooltipContent="View in full screen">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
|
||||
@@ -11,6 +11,7 @@ type Props = {
|
||||
alignment: TCustomImageAlignment;
|
||||
width: string;
|
||||
height: string;
|
||||
isMobile: boolean;
|
||||
aspectRatio: number;
|
||||
src: string;
|
||||
downloadSrc: string;
|
||||
@@ -18,7 +19,7 @@ type Props = {
|
||||
};
|
||||
|
||||
export const ImageToolbarRoot: React.FC<Props> = (props) => {
|
||||
const { alignment, downloadSrc, handleAlignmentChange } = props;
|
||||
const { alignment, downloadSrc, handleAlignmentChange, isMobile } = props;
|
||||
// states
|
||||
const [shouldShowToolbar, setShouldShowToolbar] = useState(false);
|
||||
|
||||
@@ -32,13 +33,14 @@ export const ImageToolbarRoot: React.FC<Props> = (props) => {
|
||||
}
|
||||
)}
|
||||
>
|
||||
<ImageDownloadAction src={downloadSrc} />
|
||||
{!isMobile && <ImageDownloadAction src={downloadSrc} />}
|
||||
<ImageAlignmentAction
|
||||
activeAlignment={alignment}
|
||||
handleChange={handleAlignmentChange}
|
||||
toggleToolbarViewStatus={setShouldShowToolbar}
|
||||
tooltipDisabled={isMobile}
|
||||
/>
|
||||
<ImageFullScreenActionRoot image={props} toggleToolbarViewStatus={setShouldShowToolbar} />
|
||||
<ImageFullScreenActionRoot image={props} isMobile={isMobile} toggleToolbarViewStatus={setShouldShowToolbar} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -16,6 +16,7 @@ import type { CustomImageNodeViewProps } from "./node-view";
|
||||
|
||||
type CustomImageUploaderProps = CustomImageNodeViewProps & {
|
||||
failedToLoadImage: boolean;
|
||||
isMobile: boolean;
|
||||
loadImageFromFileSystem: (file: string) => void;
|
||||
maxFileSize: number;
|
||||
setIsUploaded: (isUploaded: boolean) => void;
|
||||
@@ -27,6 +28,7 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => {
|
||||
extension,
|
||||
failedToLoadImage,
|
||||
getPos,
|
||||
isMobile,
|
||||
loadImageFromFileSystem,
|
||||
maxFileSize,
|
||||
node,
|
||||
@@ -47,9 +49,7 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => {
|
||||
if (!imageEntityId) return;
|
||||
setIsUploaded(true);
|
||||
// Update the node view's src attribute post upload
|
||||
updateAttributes({
|
||||
src: url,
|
||||
});
|
||||
updateAttributes({ src: url });
|
||||
imageComponentImageFileMap?.delete(imageEntityId);
|
||||
|
||||
const pos = getPos();
|
||||
@@ -125,12 +125,12 @@ export const CustomImageUploader = (props: CustomImageUploaderProps) => {
|
||||
uploadFile(meta.file);
|
||||
} else if (meta.event === "insert" && fileInputRef.current && !hasTriggeredFilePickerRef.current) {
|
||||
if (meta.hasOpenedFileInputOnce) return;
|
||||
fileInputRef.current.click();
|
||||
if (!isMobile) fileInputRef.current.click();
|
||||
hasTriggeredFilePickerRef.current = true;
|
||||
imageComponentImageFileMap?.set(imageEntityId ?? "", { ...meta, hasOpenedFileInputOnce: true });
|
||||
}
|
||||
}
|
||||
}, [meta, uploadFile, imageComponentImageFileMap, imageEntityId]);
|
||||
}, [meta, uploadFile, imageComponentImageFileMap, imageEntityId, isMobile]);
|
||||
|
||||
const onFileChange = useCallback(
|
||||
async (e: ChangeEvent<HTMLInputElement>) => {
|
||||
|
||||
+7
-4
@@ -1,4 +1,4 @@
|
||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
||||
import { NodeViewProps, ReactNodeViewRenderer } from "@tiptap/react";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
// constants
|
||||
import { ACCEPTED_IMAGE_MIME_TYPES } from "@/constants/config";
|
||||
@@ -8,17 +8,18 @@ import { insertEmptyParagraphAtNodeBoundaries } from "@/helpers/insert-empty-par
|
||||
// types
|
||||
import type { TFileHandler, TReadOnlyFileHandler } from "@/types";
|
||||
// local imports
|
||||
import { CustomImageNodeView } from "./components/node-view";
|
||||
import { CustomImageNodeView, CustomImageNodeViewProps } from "./components/node-view";
|
||||
import { CustomImageExtensionConfig } from "./extension-config";
|
||||
import { getImageComponentImageFileMap } from "./utils";
|
||||
|
||||
type Props = {
|
||||
fileHandler: TFileHandler | TReadOnlyFileHandler;
|
||||
isEditable: boolean;
|
||||
isMobile: boolean;
|
||||
};
|
||||
|
||||
export const CustomImageExtension = (props: Props) => {
|
||||
const { fileHandler, isEditable } = props;
|
||||
const { fileHandler, isEditable, isMobile } = props;
|
||||
// derived values
|
||||
const { getAssetSrc, getAssetDownloadSrc, restore: restoreImageFn } = fileHandler;
|
||||
|
||||
@@ -116,7 +117,9 @@ export const CustomImageExtension = (props: Props) => {
|
||||
},
|
||||
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(CustomImageNodeView);
|
||||
return ReactNodeViewRenderer((props: CustomImageNodeViewProps) => (
|
||||
<CustomImageNodeView {...props} isMobile={isMobile} />
|
||||
));
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -38,10 +38,16 @@ import { CustomStarterKitExtension } from "./starter-kit";
|
||||
|
||||
type TArguments = Pick<
|
||||
IEditorProps,
|
||||
"disabledExtensions" | "flaggedExtensions" | "fileHandler" | "mentionHandler" | "placeholder" | "tabIndex"
|
||||
| "disabledExtensions"
|
||||
| "editable"
|
||||
| "flaggedExtensions"
|
||||
| "fileHandler"
|
||||
| "isMobile"
|
||||
| "mentionHandler"
|
||||
| "placeholder"
|
||||
| "tabIndex"
|
||||
> & {
|
||||
enableHistory: boolean;
|
||||
editable: boolean;
|
||||
};
|
||||
|
||||
export const CoreEditorExtensions = (args: TArguments): Extensions => {
|
||||
@@ -50,10 +56,11 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
|
||||
enableHistory,
|
||||
fileHandler,
|
||||
flaggedExtensions,
|
||||
isMobile = false,
|
||||
mentionHandler,
|
||||
placeholder,
|
||||
tabIndex,
|
||||
editable,
|
||||
editable = true,
|
||||
} = args;
|
||||
|
||||
const extensions = [
|
||||
@@ -114,10 +121,12 @@ export const CoreEditorExtensions = (args: TArguments): Extensions => {
|
||||
extensions.push(
|
||||
ImageExtension({
|
||||
fileHandler,
|
||||
isMobile,
|
||||
}),
|
||||
CustomImageExtension({
|
||||
fileHandler,
|
||||
isEditable: editable,
|
||||
isMobile,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import { insertEmptyParagraphAtNodeBoundaries } from "@/helpers/insert-empty-par
|
||||
// types
|
||||
import type { TFileHandler, TReadOnlyFileHandler } from "@/types";
|
||||
// local imports
|
||||
import { CustomImageNodeView } from "../custom-image/components/node-view";
|
||||
import { CustomImageNodeView, CustomImageNodeViewProps } from "../custom-image/components/node-view";
|
||||
import { ImageExtensionConfig } from "./extension-config";
|
||||
|
||||
export type ImageExtensionStorage = {
|
||||
@@ -13,10 +13,11 @@ export type ImageExtensionStorage = {
|
||||
|
||||
type Props = {
|
||||
fileHandler: TFileHandler | TReadOnlyFileHandler;
|
||||
isMobile: boolean;
|
||||
};
|
||||
|
||||
export const ImageExtension = (props: Props) => {
|
||||
const { fileHandler } = props;
|
||||
const { fileHandler, isMobile } = props;
|
||||
// derived values
|
||||
const { getAssetSrc } = fileHandler;
|
||||
|
||||
@@ -47,7 +48,9 @@ export const ImageExtension = (props: Props) => {
|
||||
|
||||
// render custom image node
|
||||
addNodeView() {
|
||||
return ReactNodeViewRenderer(CustomImageNodeView);
|
||||
return ReactNodeViewRenderer((props: CustomImageNodeViewProps) => (
|
||||
<CustomImageNodeView {...props} isMobile={isMobile} />
|
||||
));
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
@@ -33,10 +33,13 @@ import { CustomImageExtension } from "./custom-image/extension";
|
||||
import { EmojiExtension } from "./emoji/extension";
|
||||
import { CustomStarterKitExtension } from "./starter-kit";
|
||||
|
||||
type Props = Pick<IReadOnlyEditorProps, "disabledExtensions" | "flaggedExtensions" | "fileHandler" | "mentionHandler">;
|
||||
type Props = Pick<
|
||||
IReadOnlyEditorProps,
|
||||
"disabledExtensions" | "flaggedExtensions" | "fileHandler" | "isMobile" | "mentionHandler"
|
||||
>;
|
||||
|
||||
export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => {
|
||||
const { disabledExtensions, fileHandler, flaggedExtensions, mentionHandler } = props;
|
||||
const { disabledExtensions, fileHandler, flaggedExtensions, isMobile = false, mentionHandler } = props;
|
||||
|
||||
const extensions = [
|
||||
CustomStarterKitExtension({
|
||||
@@ -90,10 +93,12 @@ export const CoreReadOnlyEditorExtensions = (props: Props): Extensions => {
|
||||
extensions.push(
|
||||
ImageExtension({
|
||||
fileHandler,
|
||||
isMobile,
|
||||
}),
|
||||
CustomImageExtension({
|
||||
fileHandler,
|
||||
isEditable: false,
|
||||
isMobile,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -138,7 +138,21 @@ export const unsetLinkEditor = (editor: Editor) => {
|
||||
editor.chain().focus().unsetLink().run();
|
||||
};
|
||||
|
||||
export const setLinkEditor = (editor: Editor, url: string) => {
|
||||
export const setLinkEditor = (editor: Editor, url: string, text?: string) => {
|
||||
const { selection } = editor.state;
|
||||
const previousSelection = { from: selection.from, to: selection.to };
|
||||
if (text) {
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.deleteRange({ from: selection.from, to: selection.to })
|
||||
.insertContentAt(previousSelection.from, text)
|
||||
.run();
|
||||
// Extracting the new selection start point.
|
||||
const previousFrom = previousSelection.from;
|
||||
|
||||
editor.commands.setTextSelection({ from: previousFrom, to: previousFrom + text.length });
|
||||
}
|
||||
editor.chain().focus().setLink({ href: url }).run();
|
||||
};
|
||||
|
||||
|
||||
@@ -17,7 +17,8 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorHookProps) =>
|
||||
onChange,
|
||||
onTransaction,
|
||||
disabledExtensions,
|
||||
editable,
|
||||
dragDropEnabled = true,
|
||||
editable = true,
|
||||
editorClassName = "",
|
||||
editorProps = {},
|
||||
embedHandler,
|
||||
@@ -86,7 +87,7 @@ export const useCollaborativeEditor = (props: TCollaborativeEditorHookProps) =>
|
||||
extensions: [
|
||||
SideMenuExtension({
|
||||
aiEnabled: !disabledExtensions?.includes("ai"),
|
||||
dragDropEnabled: true,
|
||||
dragDropEnabled: dragDropEnabled,
|
||||
}),
|
||||
HeadingListExtension,
|
||||
Collaboration.configure({
|
||||
|
||||
@@ -52,9 +52,7 @@ export const useEditor = (props: TEditorHookProps) => {
|
||||
autofocus,
|
||||
parseOptions: { preserveWhitespace: true },
|
||||
editorProps: {
|
||||
...CoreEditorProps({
|
||||
editorClassName,
|
||||
}),
|
||||
...CoreEditorProps({ editorClassName }),
|
||||
...editorProps,
|
||||
},
|
||||
extensions: [
|
||||
@@ -129,6 +127,39 @@ export const useEditor = (props: TEditorHookProps) => {
|
||||
() => ({
|
||||
...getEditorRefHelpers({ editor, provider }),
|
||||
blur: () => editor?.commands.blur(),
|
||||
createSelectionAtCursorPosition: () => {
|
||||
if (!editor) return;
|
||||
const { empty } = editor.state.selection;
|
||||
|
||||
// if (empty) return null;
|
||||
if (empty) {
|
||||
// Get the text content and position info
|
||||
const { $from } = editor.state.selection;
|
||||
const textContent = $from.parent.textContent;
|
||||
const posInNode = $from.parentOffset;
|
||||
|
||||
// Find word boundaries
|
||||
let start = posInNode;
|
||||
let end = posInNode;
|
||||
|
||||
// Move start position backwards until we hit a word boundary
|
||||
while (start > 0 && /\w/.test(textContent[start - 1])) {
|
||||
start--;
|
||||
}
|
||||
|
||||
// Move end position forwards until we hit a word boundary
|
||||
while (end < textContent.length && /\w/.test(textContent[end])) {
|
||||
end++;
|
||||
}
|
||||
|
||||
// If we found a word, select it using editor commands
|
||||
if (start !== end) {
|
||||
const from = $from.start() + start;
|
||||
const to = $from.start() + end;
|
||||
editor.commands.setTextSelection({ from, to });
|
||||
}
|
||||
}
|
||||
},
|
||||
emitRealTimeUpdate: (message) => provider?.sendStateless(message),
|
||||
executeMenuItemCommand: (props) => {
|
||||
const { itemKey } = props;
|
||||
@@ -143,7 +174,14 @@ export const useEditor = (props: TEditorHookProps) => {
|
||||
console.warn(`No command found for item: ${itemKey}`);
|
||||
}
|
||||
},
|
||||
focus: ({ position = "start", scrollIntoView = false }) => editor?.commands.focus(position, { scrollIntoView }),
|
||||
getCordsFromPos: (pos?: number) => editor?.view.coordsAtPos(pos ?? editor.state.selection.from),
|
||||
getCurrentCursorPosition: () => editor?.state.selection.from,
|
||||
getSelectedNodeAttributes: (attribute) => {
|
||||
if (!editor) return;
|
||||
editor.commands.extendMarkRange("link");
|
||||
return editor.getAttributes(attribute);
|
||||
},
|
||||
getSelectedText: () => {
|
||||
if (!editor) return null;
|
||||
|
||||
@@ -151,6 +189,7 @@ export const useEditor = (props: TEditorHookProps) => {
|
||||
const { from, to, empty } = state.selection;
|
||||
|
||||
if (empty) return null;
|
||||
if (from === to) return null;
|
||||
|
||||
const nodesArray: string[] = [];
|
||||
state.doc.nodesBetween(from, to, (node, _pos, parent) => {
|
||||
@@ -238,7 +277,8 @@ export const useEditor = (props: TEditorHookProps) => {
|
||||
editor?.off("transaction", callback);
|
||||
};
|
||||
},
|
||||
scrollToNodeViaDOMCoordinates(behavior, pos) {
|
||||
redo: () => editor?.commands.redo(),
|
||||
scrollToNodeViaDOMCoordinates({ pos, behavior = "smooth" }) {
|
||||
const resolvedPos = pos ?? editor?.state.selection.from;
|
||||
if (!editor || !resolvedPos) return;
|
||||
scrollToNodeViaDOMCoordinates(editor, resolvedPos, behavior);
|
||||
@@ -270,6 +310,7 @@ export const useEditor = (props: TEditorHookProps) => {
|
||||
if (!document) return;
|
||||
Y.applyUpdate(document, value);
|
||||
},
|
||||
undo: () => editor?.commands.undo(),
|
||||
}),
|
||||
[editor]
|
||||
);
|
||||
|
||||
@@ -24,9 +24,9 @@ export type TFileHandler = TReadOnlyFileHandler & {
|
||||
|
||||
export type TEditorFontStyle = "sans-serif" | "serif" | "monospace";
|
||||
|
||||
export type TEditorFontSize = "small-font" | "large-font";
|
||||
export type TEditorFontSize = "small-font" | "large-font" | "mobile-font";
|
||||
|
||||
export type TEditorLineSpacing = "regular" | "small";
|
||||
export type TEditorLineSpacing = "regular" | "small" | "mobile-regular";
|
||||
|
||||
export type TDisplayConfig = {
|
||||
fontStyle?: TEditorFontStyle;
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { Extensions, JSONContent } from "@tiptap/core";
|
||||
import { Extensions, FocusPosition, JSONContent } from "@tiptap/core";
|
||||
import { MarkType, NodeType } from "@tiptap/pm/model";
|
||||
import { Selection } from "@tiptap/pm/state";
|
||||
// extension types
|
||||
import { EditorProps } from "@tiptap/pm/view";
|
||||
import type { TTextAlign } from "@/extensions";
|
||||
// helpers
|
||||
import type { IMarking } from "@/helpers/scroll-to-node";
|
||||
@@ -42,6 +44,7 @@ export type TEditorCommands =
|
||||
| "table"
|
||||
| "image"
|
||||
| "divider"
|
||||
| "link"
|
||||
| "issue-embed"
|
||||
| "text-color"
|
||||
| "background-color"
|
||||
@@ -66,6 +69,10 @@ export type TCommandExtraProps = {
|
||||
"text-align": {
|
||||
alignment: TTextAlign;
|
||||
};
|
||||
link: {
|
||||
url: string;
|
||||
text?: string;
|
||||
};
|
||||
};
|
||||
|
||||
// Create a utility type that maps a command to its extra props or an empty object if none are defined
|
||||
@@ -100,8 +107,19 @@ export type EditorReadOnlyRefApi = {
|
||||
|
||||
export interface EditorRefApi extends EditorReadOnlyRefApi {
|
||||
blur: () => void;
|
||||
emitRealTimeUpdate: (action: TDocumentEventsServer) => void;
|
||||
createSelectionAtCursorPosition: () => void;
|
||||
focus: ({ position, scrollIntoView }: { position?: FocusPosition; scrollIntoView?: boolean }) => void;
|
||||
getCordsFromPos: (pos?: number) =>
|
||||
| {
|
||||
left: number;
|
||||
right: number;
|
||||
top: number;
|
||||
bottom: number;
|
||||
}
|
||||
| undefined;
|
||||
executeMenuItemCommand: <T extends TEditorCommands>(props: TCommandWithPropsWithItemKey<T>) => void;
|
||||
getSelectedNodeAttributes: (attribute: string | NodeType | MarkType) => Record<string, any> | undefined;
|
||||
emitRealTimeUpdate: (action: TDocumentEventsServer) => void;
|
||||
getCurrentCursorPosition: () => number | undefined;
|
||||
getSelectedText: () => string | null;
|
||||
insertText: (contentHTML: string, insertOnNextLine?: boolean) => void;
|
||||
@@ -111,10 +129,12 @@ export interface EditorRefApi extends EditorReadOnlyRefApi {
|
||||
onDocumentInfoChange: (callback: (documentInfo: TDocumentInfo) => void) => () => void;
|
||||
onHeadingChange: (callback: (headings: IMarking[]) => void) => () => void;
|
||||
onStateChange: (callback: () => void) => () => void;
|
||||
scrollToNodeViaDOMCoordinates: (behavior?: ScrollBehavior, position?: number) => void;
|
||||
redo: () => void;
|
||||
scrollToNodeViaDOMCoordinates: ({ pos, behavior }: { pos?: number; behavior?: ScrollBehavior }) => void;
|
||||
setEditorValueAtCursorPosition: (content: string) => void;
|
||||
setFocusAtPosition: (position: number) => void;
|
||||
setProviderDocument: (value: Uint8Array) => void;
|
||||
undo: () => void;
|
||||
}
|
||||
|
||||
// editor props
|
||||
@@ -124,7 +144,9 @@ export interface IEditorProps {
|
||||
containerClassName?: string;
|
||||
displayConfig?: TDisplayConfig;
|
||||
disabledExtensions: TExtensions[];
|
||||
editable?: boolean;
|
||||
editorClassName?: string;
|
||||
editorProps?: EditorProps;
|
||||
extensions?: Extensions;
|
||||
flaggedExtensions: TExtensions[];
|
||||
fileHandler: TFileHandler;
|
||||
@@ -132,6 +154,8 @@ export interface IEditorProps {
|
||||
handleEditorReady?: (value: boolean) => void;
|
||||
id: string;
|
||||
initialValue: string;
|
||||
isMobile?: boolean;
|
||||
dragDropEnabled?: boolean;
|
||||
mentionHandler: TMentionHandler;
|
||||
onAssetChange?: (assets: TEditorAsset[]) => void;
|
||||
onChange?: (json: object, html: string) => void;
|
||||
@@ -144,15 +168,11 @@ export interface IEditorProps {
|
||||
|
||||
export type ILiteTextEditorProps = IEditorProps;
|
||||
|
||||
export type IRichTextEditorProps = IEditorProps & {
|
||||
dragDropEnabled?: boolean;
|
||||
editable: boolean;
|
||||
};
|
||||
export type IRichTextEditorProps = IEditorProps;
|
||||
|
||||
export interface ICollaborativeDocumentEditorProps
|
||||
extends Omit<IEditorProps, "extensions" | "initialValue" | "onEnterKeyPress" | "value"> {
|
||||
extends Omit<IEditorProps, "initialValue" | "onEnterKeyPress" | "value"> {
|
||||
aiHandler?: TAIHandler;
|
||||
editable: boolean;
|
||||
embedHandler: TEmbedConfig;
|
||||
realtimeConfig: TRealtimeConfig;
|
||||
serverHandler?: TServerHandler;
|
||||
@@ -172,6 +192,7 @@ export interface IReadOnlyEditorProps
|
||||
| "handleEditorReady"
|
||||
| "id"
|
||||
| "initialValue"
|
||||
| "isMobile"
|
||||
> {
|
||||
fileHandler: TReadOnlyFileHandler;
|
||||
forwardedRef?: React.MutableRefObject<EditorReadOnlyRefApi | null>;
|
||||
|
||||
@@ -5,7 +5,13 @@ import type { ICollaborativeDocumentEditorProps, IEditorProps, IReadOnlyEditorPr
|
||||
|
||||
type TCoreHookProps = Pick<
|
||||
IEditorProps,
|
||||
"disabledExtensions" | "editorClassName" | "extensions" | "flaggedExtensions" | "handleEditorReady"
|
||||
| "editable"
|
||||
| "disabledExtensions"
|
||||
| "dragDropEnabled"
|
||||
| "editorClassName"
|
||||
| "extensions"
|
||||
| "flaggedExtensions"
|
||||
| "handleEditorReady"
|
||||
> & {
|
||||
editorProps?: EditorProps;
|
||||
};
|
||||
@@ -25,7 +31,6 @@ export type TEditorHookProps = TCoreHookProps &
|
||||
| "tabIndex"
|
||||
| "value"
|
||||
> & {
|
||||
editable: boolean;
|
||||
enableHistory: boolean;
|
||||
initialValue?: string;
|
||||
provider?: HocuspocusProvider;
|
||||
@@ -34,7 +39,6 @@ export type TEditorHookProps = TCoreHookProps &
|
||||
export type TCollaborativeEditorHookProps = TCoreHookProps &
|
||||
Pick<
|
||||
TEditorHookProps,
|
||||
| "editable"
|
||||
| "fileHandler"
|
||||
| "forwardedRef"
|
||||
| "id"
|
||||
|
||||
@@ -30,15 +30,6 @@
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* Display Placeholders on every new line. */
|
||||
.ProseMirror p.is-empty::before {
|
||||
content: attr(data-placeholder);
|
||||
float: left;
|
||||
color: var(--color-placeholder);
|
||||
pointer-events: none;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.ProseMirror li blockquote {
|
||||
margin-top: 10px;
|
||||
padding-inline-start: 1em;
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
--line-height-regular: 1.5rem;
|
||||
--line-height-code: 1.5rem;
|
||||
--line-height-list: var(--line-height-regular);
|
||||
}
|
||||
}
|
||||
&.small-font {
|
||||
--font-size-h1: 1.4rem;
|
||||
--font-size-h2: 1.2rem;
|
||||
@@ -88,6 +88,27 @@
|
||||
--line-height-code: 1.2rem;
|
||||
--line-height-list: var(--line-height-regular);
|
||||
}
|
||||
&.mobile-font {
|
||||
--font-size-h1: 1.75rem;
|
||||
--font-size-h2: 1.5rem;
|
||||
--font-size-h3: 1.375rem;
|
||||
--font-size-h4: 1.25rem;
|
||||
--font-size-h5: 1.125rem;
|
||||
--font-size-h6: 1rem;
|
||||
--font-size-regular: 0.95rem;
|
||||
--font-size-code: 0.85rem;
|
||||
--font-size-list: var(--font-size-regular);
|
||||
|
||||
--line-height-h1: 2.25rem;
|
||||
--line-height-h2: 2rem;
|
||||
--line-height-h3: 1.75rem;
|
||||
--line-height-h4: 1.5rem;
|
||||
--line-height-h5: 1.5rem;
|
||||
--line-height-h6: 1.5rem;
|
||||
--line-height-regular: 1.5rem;
|
||||
--line-height-code: 1.5rem;
|
||||
--line-height-list: var(--line-height-regular);
|
||||
}
|
||||
/* end font sizes and line heights */
|
||||
|
||||
/* font styles */
|
||||
@@ -146,6 +167,27 @@
|
||||
--divider-padding-top: 0px;
|
||||
--divider-padding-bottom: 4px;
|
||||
}
|
||||
|
||||
&.line-spacing-mobile-regular {
|
||||
--heading-1-padding-top: 16px;
|
||||
--heading-1-padding-bottom: 4px;
|
||||
--heading-2-padding-top: 16px;
|
||||
--heading-2-padding-bottom: 4px;
|
||||
--heading-3-padding-top: 16px;
|
||||
--heading-3-padding-bottom: 4px;
|
||||
--heading-4-padding-top: 16px;
|
||||
--heading-4-padding-bottom: 4px;
|
||||
--heading-5-padding-top: 12px;
|
||||
--heading-5-padding-bottom: 4px;
|
||||
--heading-6-padding-top: 12px;
|
||||
--heading-6-padding-bottom: 4px;
|
||||
--paragraph-padding-top: 2px;
|
||||
--paragraph-padding-bottom: 2px;
|
||||
--paragraph-padding-between: 4px;
|
||||
--list-spacing-y: 0px;
|
||||
--divider-padding-top: 0px;
|
||||
--divider-padding-bottom: 4px;
|
||||
}
|
||||
/* end spacing */
|
||||
}
|
||||
/* end font size and style */
|
||||
|
||||
Reference in New Issue
Block a user