[WEB-460] refactor: editors, chore: pages list improvement (#4090)

* fix: stroing the transactions in page

* fix: page details changes

* chore: page response change

* chore: removed duplicated endpoints

* chore: optimised the urls

* chore: removed archived and favorite pages

* chore: revamping pages store and components

* mentions loading state part done

* fixed mentions not showing in modals

* removed comments and cleaned up types

* removed unused types

* reset: head

* chore: pages store and component updates

* style: pages list item UI

* fix: improved colors and drag handle width

* fix: slash commands are no more shown in the code blocks

* fix: cleanup/hide drag handles post drop

* fix: hide/cleanup drag handles post drag start

* fix: aligning the drag handles better with the node post css changes of the length

* fix: juggling back and forth of drag handles in ordered and unordered lists

* chore: fix imports, ts errors and other things

* fix: clearing nodes to default node i.e paragraph before converting it to other types of nodes

For more reference on what this does, please refer https://tiptap.dev/docs/editor/api/commands/clear-nodes

* chore: clearNodes after delete in case of selections being present

* fix: hiding link selector in the bubble menu if inline code block is selected

* chore: filtering, ordering and searching implemented

* chore: updated pages store and updated UI

* chore: new core editor just for document editor created

* chore: removed setIsSubmitting prop in doc editor

* fix: fixed submitting state for image uploads

* refactor: setShouldShowAlert removed

* refactor: rerenderOnPropsChange prop removed

* chore: type inference magic in ref to expose an api for controlling editor menu items from outside

* fix: naming imports

* chore: change names of the exposed functions and removing old types

* refactor: remove debouncedUpdatesEnabled prop;

* refactor: editor heading markings now parsed using html

* chore: removed unrelated components from the document editor

* refactor: page details granular components

* fix: remove onActionCompleteHandler

* refactor: removed rerenderOnProps change prop

* feat: added getMarkDown function

* chore: update dropdown option actions

* fix: sidebar markings update logic

* chore: add image and to-do list actions to the toolbar

* fix: handling refs and populating them via callbacks

* feat: scroll to node api exposed

* cleaning up editor refs when the editor is destroyed

* feat: scrolling added to read only instance of the editor

* fix: markings logic

* fix: build errors with types

* fix: build erros

* fix: subscribing to transactions of editor via ref

* chore: remove debug statements

* fix: type errors

* fix: temporary different slash commands for document editor

* chore: inline code extension style

* chore: remove border from readOnly editor

* fix: editor bottom padding

* chore: pages improvements

* chore: handle Enter key on the page title

* feat: added loading indicator logic in mentions

* fix: mentions and slash commands now work well with multiple editors in one place

* refactor: page store structure, filtering logic

* feat: added better seperation in inline code blocks

* feat: list autojoining added

* fix: pages folder structure

* fix: image refocus from external parts

* working lists somewhat

* chore: implement page reactions

* fix: build errors

* fix: build errors

* fixed drag handles stuff

* task list item fixed

* working

* fix: working on multiple nested lists

* chore: remove debug statements

* fix: Tab key on first list item handled to not go out of editor focus

* feat: threshold auto scroll support added and multi nested list selection fixed

* fix: caret color bug with improved inline code blocks

* fix: node range error when bulk deleting with list

* fix: removed slash commands from working in code blocks

* chore: update typography margins

* chore: new field added in page model

* fix: better type inference in slash commands

* chore: code block UI

* feat: image insertion at correct position using ref added

* feat: added improved mentions support for space

* fix: type errors in mentions for comments in web app

* sync: core with document-core

* fix: build errors

* fix: fallback for appendTo not being able to find active container instantly

* fix: page store

* fix: page description

* fix: css quality issues

* chore: code cleanup

* chore: removed placeholder text in codeblocks

* chore: archived pages response change

* chore: archived pages response change

* fix: initial pages list fetch

* fix: pages list filters and ordering

* chore: add access change option in the quick actions dropdown

* fix: inline code block caret fixed

* regression: removing extra text

* chore: caret color removed

* feat: copy code button added in code blocks

* fix: initial load of page details

* fix: initial load of page details

* fix: image resizing weird behavior on click/expanding it too much fixed now

* chore: copy page response

* fix: todo list spacing

* chore: description html in the copy page

* chore: handle latest description on refetch

* fix: saner scroll behaviours

* fix: block menu positioning

* fix: updated empty string description

* feat: tab change sync support added

* fix: infinite rerendering with markings

* fix: block menu finally

* fix: intial load on reload bug fixed

* fix: nested lists alignment

* fix: editor padding

* fix: first level list items copyable

* chore: list spacing

* fix: title change

* fix: pages list block items interaction

* fix: saving chip position

* fix: delete action from block menu to focus properly

* fix: margin-bottom as 0 to avoid weird spacing when a paragraph node follows a list node

* style: table, chore: lite text editor toolbar

* fix: page description tab sync

* fix: lists spacing and alignment

* refactor: document editor props

* feat: rich text editor wrapper created and migrated core

* feat: created wrapper around lite text editor and merged core

* chore: add lite text editor toolbar

* fix: build errors

* fix: type errors and addead live updation of toolbar

* chore: pages migration

* fix: inbox issue

* refactor: remove redundant package

* refactor: unused files

* fix: add dompurify to space app

* fix: inline code margin

* fix: editor className props

* fix: build errors

* fix: traversing up the tree before assuming the parent is not a list item

* fix: drag handle positions for list items fixed

* fix: removed focus at end logic after deleting block

* fix: image wrapper overflow scroll fix with block menu's position

* fix: selection and deletion logic for nested lists fixed!!

* fix: hiding the block menu while scrolling in the document/app

* fix: merge conflicts resolved from develop

* fix: inbox issue description

* chore: move page title to the web app

* fix: handling edge cases for table selection

* chore: lint issues

* refactor: list item functions moved to same file

* refactor: use mention hook

* fix: added try catch blocks for mention suggestions

* chore: remove unused code

* fix: remove console logs

* fix: remove console logs

---------

Co-authored-by: NarayanBavisetti <narayan3119@gmail.com>
Co-authored-by: sriram veeraghanta <veeraghanta.sriram@gmail.com>
Co-authored-by: gurusainath <gurusainath007@gmail.com>
Co-authored-by: Palanikannan1437 <73993394+Palanikannan1437@users.noreply.github.com>
This commit is contained in:
Aaryan Khandelwal
2024-04-11 21:28:59 +05:30
committed by GitHub
parent 8b6035d315
commit 3e2355e223
248 changed files with 7602 additions and 5619 deletions

View File

@@ -29,6 +29,7 @@
},
"dependencies": {
"@plane/editor-core": "*",
"@plane/ui": "*",
"@tiptap/core": "^2.1.13",
"@tiptap/pm": "^2.1.13",
"@tiptap/react": "^2.1.13",

View File

@@ -1,9 +1,18 @@
import { Extension } from "@tiptap/core";
import { PluginKey, NodeSelection, Plugin } from "@tiptap/pm/state";
// @ts-ignore
import { NodeSelection, Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
import { Fragment, Slice, Node } from "@tiptap/pm/model";
// @ts-expect-error __serializeForClipboard's is not exported
import { __serializeForClipboard, EditorView } from "@tiptap/pm/view";
import React from "react";
export interface DragHandleOptions {
dragHandleWidth: number;
setHideDragHandle?: (hideDragHandlerFromDragDrop: () => void) => void;
scrollThreshold: {
up: number;
down: number;
};
}
function createDragHandleElement(): HTMLElement {
const dragHandleElement = document.createElement("div");
@@ -29,13 +38,8 @@ function createDragHandleElement(): HTMLElement {
return dragHandleElement;
}
export interface DragHandleOptions {
dragHandleWidth: number;
setHideDragHandle?: (hideDragHandlerFromDragDrop: () => void) => void;
}
function absoluteRect(node: Element) {
const data = node?.getBoundingClientRect();
const data = node.getBoundingClientRect();
return {
top: data.top,
@@ -57,55 +61,77 @@ function nodeDOMAtCoords(coords: { x: number; y: number }) {
"pre",
"blockquote",
"h1, h2, h3",
".table-wrapper",
"[data-type=horizontalRule]",
".tableWrapper",
].join(", ")
)
);
}
function nodePosAtDOM(node: Element, view: EditorView) {
const boundingRect = node?.getBoundingClientRect();
if (node.nodeName === "IMG") {
return view.posAtCoords({
left: boundingRect.left + 1,
top: boundingRect.top + 1,
})?.pos;
}
if (node.nodeName === "PRE") {
return (
view.posAtCoords({
left: boundingRect.left + 1,
top: boundingRect.top + 1,
})?.pos! - 1
);
}
function nodePosAtDOM(node: Element, view: EditorView, options: DragHandleOptions) {
const boundingRect = node.getBoundingClientRect();
return view.posAtCoords({
left: boundingRect.left + 1,
left: boundingRect.left + 50 + options.dragHandleWidth,
top: boundingRect.top + 1,
})?.inside;
}
function calcNodePos(pos: number, view: EditorView) {
const $pos = view.state.doc.resolve(pos);
if ($pos.depth > 1) return $pos.before($pos.depth);
return pos;
}
function DragHandle(options: DragHandleOptions) {
let listType = "";
function handleDragStart(event: DragEvent, view: EditorView) {
view.focus();
if (!event.dataTransfer) return;
const node = nodeDOMAtCoords({
x: event.clientX + options.dragHandleWidth + 50,
x: event.clientX + 50 + options.dragHandleWidth,
y: event.clientY,
});
if (!(node instanceof Element)) return;
const nodePos = nodePosAtDOM(node, view);
if (nodePos === null || nodePos === undefined || nodePos < 0) return;
let draggedNodePos = nodePosAtDOM(node, view, options);
if (draggedNodePos == null || draggedNodePos < 0) return;
draggedNodePos = calcNodePos(draggedNodePos, view);
view.dispatch(view.state.tr.setSelection(NodeSelection.create(view.state.doc, nodePos)));
const { from, to } = view.state.selection;
const diff = from - to;
const fromSelectionPos = calcNodePos(from, view);
let differentNodeSelected = false;
const nodePos = view.state.doc.resolve(fromSelectionPos);
// Check if nodePos points to the top level node
if (nodePos.node().type.name === "doc") differentNodeSelected = true;
else {
const nodeSelection = NodeSelection.create(view.state.doc, nodePos.before());
// Check if the node where the drag event started is part of the current selection
differentNodeSelected = !(
draggedNodePos + 1 >= nodeSelection.$from.pos && draggedNodePos <= nodeSelection.$to.pos
);
}
if (!differentNodeSelected && diff !== 0 && !(view.state.selection instanceof NodeSelection)) {
const endSelection = NodeSelection.create(view.state.doc, to - 1);
const multiNodeSelection = TextSelection.create(view.state.doc, draggedNodePos, endSelection.$to.pos);
view.dispatch(view.state.tr.setSelection(multiNodeSelection));
} else {
const nodeSelection = NodeSelection.create(view.state.doc, draggedNodePos);
view.dispatch(view.state.tr.setSelection(nodeSelection));
}
// If the selected node is a list item, we need to save the type of the wrapping list e.g. OL or UL
if (view.state.selection instanceof NodeSelection && view.state.selection.node.type.name === "listItem") {
listType = node.parentElement!.tagName;
}
const slice = view.state.selection.content();
const { dom, text } = __serializeForClipboard(view, slice);
@@ -123,8 +149,6 @@ function DragHandle(options: DragHandleOptions) {
function handleClick(event: MouseEvent, view: EditorView) {
view.focus();
view.dom.classList.remove("dragging");
const node = nodeDOMAtCoords({
x: event.clientX + 50 + options.dragHandleWidth,
y: event.clientY,
@@ -132,11 +156,18 @@ function DragHandle(options: DragHandleOptions) {
if (!(node instanceof Element)) return;
const nodePos = nodePosAtDOM(node, view);
let nodePos = nodePosAtDOM(node, view, options);
if (nodePos === null || nodePos === undefined || nodePos < 0) return;
if (nodePos === null || nodePos === undefined) return;
view.dispatch(view.state.tr.setSelection(NodeSelection.create(view.state.doc, nodePos)));
// Adjust the nodePos to point to the start of the node, ensuring NodeSelection can be applied
nodePos = calcNodePos(nodePos, view);
// Use NodeSelection to select the node at the calculated position
const nodeSelection = NodeSelection.create(view.state.doc, nodePos);
// Dispatch the transaction to update the selection
view.dispatch(view.state.tr.setSelection(nodeSelection));
}
let dragHandleElement: HTMLElement | null = null;
@@ -166,11 +197,15 @@ function DragHandle(options: DragHandleOptions) {
handleClick(e, view);
});
dragHandleElement.addEventListener("dragstart", (e) => {
handleDragStart(e, view);
});
dragHandleElement.addEventListener("click", (e) => {
handleClick(e, view);
dragHandleElement.addEventListener("drag", (e) => {
hideDragHandle();
const a = document.querySelector(".frame-renderer");
if (!a) return;
if (e.clientY < options.scrollThreshold.up) {
a.scrollBy({ top: -70, behavior: "smooth" });
} else if (window.innerHeight - e.clientY < options.scrollThreshold.down) {
a.scrollBy({ top: 70, behavior: "smooth" });
}
});
hideDragHandle();
@@ -192,11 +227,11 @@ function DragHandle(options: DragHandleOptions) {
}
const node = nodeDOMAtCoords({
x: event.clientX + options.dragHandleWidth,
x: event.clientX + 50 + options.dragHandleWidth,
y: event.clientY,
});
if (!(node instanceof Element)) {
if (!(node instanceof Element) || node.matches("ul, ol")) {
hideDragHandle();
return;
}
@@ -207,32 +242,74 @@ function DragHandle(options: DragHandleOptions) {
const rect = absoluteRect(node);
rect.top += (lineHeight - 24) / 2;
rect.top += (lineHeight - 20) / 2;
rect.top += paddingTop;
// Li markers
if (node.matches("ul:not([data-type=taskList]) li, ol li")) {
rect.left -= options.dragHandleWidth;
rect.top += 4;
rect.left -= 18;
}
rect.width = options.dragHandleWidth;
if (!dragHandleElement) return;
dragHandleElement.style.left = `${rect.left - rect.width}px`;
dragHandleElement.style.top = `${rect.top + 3}px`;
dragHandleElement.style.top = `${rect.top}px`;
showDragHandle();
},
keydown: () => {
hideDragHandle();
},
wheel: () => {
mousewheel: () => {
hideDragHandle();
},
// dragging className is used for CSS
dragstart: (view) => {
dragenter: (view) => {
view.dom.classList.add("dragging");
hideDragHandle();
},
drop: (view) => {
drop: (view, event) => {
view.dom.classList.remove("dragging");
hideDragHandle();
let droppedNode: Node | null = null;
const dropPos = view.posAtCoords({
left: event.clientX,
top: event.clientY,
});
if (!dropPos) return;
if (view.state.selection instanceof NodeSelection) {
droppedNode = view.state.selection.node;
}
if (!droppedNode) return;
const resolvedPos = view.state.doc.resolve(dropPos.pos);
let isDroppedInsideList = false;
// Traverse up the document tree to find if we're inside a list item
for (let i = resolvedPos.depth; i > 0; i--) {
if (resolvedPos.node(i).type.name === "listItem") {
isDroppedInsideList = true;
break;
}
}
// If the selected node is a list item and is not dropped inside a list, we need to wrap it inside <ol> tag otherwise ol list items will be transformed into ul list item when dropped
if (
view.state.selection instanceof NodeSelection &&
view.state.selection.node.type.name === "listItem" &&
!isDroppedInsideList &&
listType == "OL"
) {
const text = droppedNode.textContent;
if (!text) return;
const paragraph = view.state.schema.nodes.paragraph?.createAndFill({}, view.state.schema.text(text));
const listItem = view.state.schema.nodes.listItem?.createAndFill({}, paragraph);
const newList = view.state.schema.nodes.orderedList?.createAndFill(null, listItem);
const slice = new Slice(Fragment.from(newList), 0, 0);
view.dragging = { slice, move: event.ctrlKey };
}
},
dragend: (view) => {
view.dom.classList.remove("dragging");
@@ -250,6 +327,7 @@ export const DragAndDrop = (setHideDragHandle?: (hideDragHandlerFromDragDrop: ()
return [
DragHandle({
dragHandleWidth: 24,
scrollThreshold: { up: 300, down: 100 },
setHideDragHandle,
}),
];

View File

@@ -0,0 +1,2 @@
export * from "./drag-drop";
export * from "./slash-commands";

View File

@@ -54,7 +54,20 @@ const Command = Extension.create<SlashCommandOptions>({
props.command({ editor, range });
},
allow({ editor }: { editor: Editor }) {
return !editor.isActive("table");
const { selection } = editor.state;
const parentNode = selection.$from.node(selection.$from.depth);
const blockType = parentNode.type.name;
if (blockType === "codeBlock") {
return false;
}
if (editor.isActive("table")) {
return false;
}
return true;
},
allowSpaces: true,
},
@@ -71,11 +84,7 @@ const Command = Extension.create<SlashCommandOptions>({
});
const getSuggestionItems =
(
uploadFile: UploadImage,
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void,
additionalOptions?: Array<ISlashCommandItem>
) =>
(uploadFile: UploadImage, additionalOptions?: Array<ISlashCommandItem>) =>
({ query }: { query: string }) => {
let slashCommands: ISlashCommandItem[] = [
{
@@ -186,7 +195,7 @@ const getSuggestionItems =
searchTerms: ["img", "photo", "picture", "media"],
icon: <ImageIcon className="h-3.5 w-3.5" />,
command: ({ editor, range }: CommandProps) => {
insertImageCommand(editor, uploadFile, setIsSubmitting, range);
insertImageCommand(editor, uploadFile, null, range);
},
},
{
@@ -300,9 +309,9 @@ const CommandList = ({ items, command }: { items: CommandItemProps[]; command: a
<button
key={item.key}
className={cn(
`flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-sm text-custom-text-100 hover:bg-custom-primary-100/5`,
`flex w-full items-center gap-2 rounded-md px-2.5 py-1.5 text-sm text-custom-text-100 hover:bg-custom-background-80`,
{
"bg-custom-primary-100/5": index === selectedIndex,
"bg-custom-background-80": index === selectedIndex,
}
)}
onClick={() => selectItem(index)}
@@ -315,19 +324,21 @@ const CommandList = ({ items, command }: { items: CommandItemProps[]; command: a
) : null;
};
const renderItems = () => {
let component: ReactRenderer | null = null;
let popup: any | null = null;
interface CommandListInstance {
onKeyDown: (props: { event: KeyboardEvent }) => boolean;
}
const renderItems = () => {
let component: ReactRenderer<CommandListInstance, typeof CommandList> | null = null;
let popup: any | null = null;
return {
onStart: (props: { editor: Editor; clientRect?: (() => DOMRect | null) | null }) => {
component = new ReactRenderer(CommandList, {
props,
// @ts-ignore
editor: props.editor,
});
// @ts-ignore
// @ts-expect-error Tippy overloads are messed up
popup = tippy("body", {
getReferenceClientRect: props.clientRect,
appendTo: () => document.querySelector(".active-editor") ?? document.querySelector("#editor-container"),
@@ -353,8 +364,10 @@ const renderItems = () => {
return true;
}
// @ts-ignore
return component?.ref?.onKeyDown(props);
if (component?.ref?.onKeyDown(props)) {
return true;
}
return false;
},
onExit: () => {
popup?.[0].destroy();
@@ -363,14 +376,10 @@ const renderItems = () => {
};
};
export const SlashCommand = (
uploadFile: UploadImage,
setIsSubmitting?: (isSubmitting: "submitting" | "submitted" | "saved") => void,
additionalOptions?: Array<ISlashCommandItem>
) =>
export const SlashCommand = (uploadFile: UploadImage, additionalOptions?: Array<ISlashCommandItem>) =>
Command.configure({
suggestion: {
items: getSuggestionItems(uploadFile, setIsSubmitting, additionalOptions),
items: getSuggestionItems(uploadFile, additionalOptions),
render: renderItems,
},
});

View File

@@ -1,4 +1,3 @@
import "src/styles/drag-drop.css";
export { SlashCommand } from "src/extensions/slash-commands";
export { DragAndDrop } from "src/extensions/drag-drop";
export { DragAndDrop, SlashCommand } from "src/extensions";

View File

@@ -1,25 +1,30 @@
/* drag handle */
.drag-handle {
position: fixed;
opacity: 1;
transition: opacity ease-in 0.2s;
height: 18px;
height: 20px;
width: 15px;
display: grid;
place-items: center;
z-index: 10;
z-index: 5;
cursor: grab;
border-radius: 2px;
background-color: rgb(var(--color-background-90));
}
.drag-handle:hover {
background-color: rgb(var(--color-background-80));
transition: background-color 0.2s;
}
.drag-handle.hidden {
opacity: 0;
pointer-events: none;
&:hover {
background-color: rgba(var(--color-background-80));
}
&:active {
background-color: rgba(var(--color-background-80));
cursor: grabbing;
}
&.hidden {
opacity: 0;
pointer-events: none;
}
}
@media screen and (max-width: 600px) {
@@ -32,7 +37,6 @@
.drag-handle-container {
height: 15px;
width: 15px;
cursor: grab;
display: grid;
place-items: center;
}
@@ -46,8 +50,46 @@
}
.drag-handle-dot {
height: 2.75px;
width: 3px;
background-color: rgba(var(--color-text-200));
height: 2.5px;
width: 2.5px;
background-color: rgba(var(--color-text-300));
border-radius: 50%;
}
/* end drag handle */
.ProseMirror:not(.dragging) .ProseMirror-selectednode {
position: relative;
cursor: grab;
outline: none !important;
box-shadow: none;
}
.ProseMirror:not(.dragging) .ProseMirror-selectednode::after {
content: "";
position: absolute;
top: 0;
left: -5px;
height: 100%;
width: 100%;
background-color: rgba(var(--color-primary-100), 0.2);
border-radius: 4px;
}
.ProseMirror img {
transition: filter 0.1s ease-in-out;
cursor: pointer;
&:hover {
filter: brightness(90%);
}
&.ProseMirror-selectednode {
filter: brightness(90%);
}
}
:not(.dragging) .ProseMirror-selectednode.table-wrapper {
padding: 4px 2px;
background-color: rgba(var(--color-primary-300), 0.1) !important;
box-shadow: rgba(var(--color-primary-100)) 0px 0px 0px 2px inset !important;
}