Including A Commenting System To A WYSIWYG Editor

No Comments

In recent times, we’ve seen Collaboration penetrate plenty of digital workflows and use-cases throughout many professions. Simply throughout the Design and Software program Engineering group, we see designers collaborate on design artifacts utilizing instruments like Figma, groups doing Dash and Mission Planning utilizing instruments like Mural and interviews being performed utilizing CoderPad. All these instruments are continuously aiming to bridge the hole between an internet and a bodily world expertise of executing these workflows and making the collaboration expertise as wealthy and seamless as potential.

For almost all of the Collaboration Instruments like these, the flexibility to share opinions with each other and have discussions about the identical content material is a must have. A Commenting System that permits collaborators to annotate elements of a doc and have conversations about them is on the coronary heart of this idea. Together with constructing one for textual content in a WYSIWYG Editor, the article tries to interact the readers into how we attempt to weigh the professionals and cons and try and discover a steadiness between utility complexity and person expertise in terms of constructing options for WYSIWYG Editors or Phrase Processors basically.

Representing Feedback In Doc Construction

With a purpose to discover a technique to signify feedback in a wealthy textual content doc’s information construction, let’s have a look at a number of situations below which feedback may very well be created inside an editor.

Feedback created over textual content that has no types on it (primary state of affairs);
Feedback created over textual content that could be daring/italic/underlined, and so forth;
Feedback that overlap one another ultimately (partial overlap the place two feedback share just a few phrases or fully-contained the place one remark’s textual content is absolutely contained inside textual content of one other remark);
Feedback created over textual content inside a hyperlink (particular as a result of hyperlinks are nodes themselves in our doc construction);
Feedback that span a number of paragraphs (particular as a result of paragraphs are nodes in our doc construction and feedback are utilized to textual content nodes that are paragraph’s kids).

Wanting on the above use-cases, it looks as if feedback in the best way they will come up in a wealthy textual content doc are similar to character types (daring, italics and so forth). They will overlap with one another, go over textual content in different kinds of nodes like hyperlinks and even span a number of dad or mum nodes like paragraphs.

Because of this, we use the identical technique to signify feedback as we do for character types, i.e. “Marks” (as they’re so known as in SlateJS terminology). Marks are simply common properties on nodes — speciality being that Slate’s API round marks (Editor.addMark and Editor.removeMark) handles altering of the node hierarchy as a number of marks get utilized to the identical vary of textual content. That is extraordinarily helpful to us as we cope with plenty of completely different combos of overlapping feedback.

Remark Threads As Marks

Every time a person selects a variety of textual content and tries to insert a remark, technically, they’re beginning a brand new remark thread for that textual content vary. As a result of we might enable them to insert a remark and later replies to that remark, we deal with this occasion as a brand new remark thread insertion within the doc.

The best way we signify remark threads as marks is that every remark thread is represented by a mark named as commentThread_threadID the place threadID is a singular ID we assign to every remark thread. So, if the identical vary of textual content has two remark threads over it, it could have two properties set to the true — commentThread_thread1 and commentThread_thread2. That is the place remark threads are similar to character types since if the identical textual content was daring and italic, it could have each the properties set to true — daring and italic.

Earlier than we dive into truly setting this construction up, it’s price how the textual content nodes change as remark threads get utilized to them. The best way this works (because it does with any mark) is that when a mark property is being set on the chosen textual content, Slate’s Editor.addMark API would break up the textual content node(s) if wanted such that within the ensuing construction, textual content nodes are arrange in a method that every textual content node has the very same worth of the mark.

To know this higher, check out the next three examples that present the before-and-after state of the textual content nodes as soon as a remark thread is inserted on the chosen textual content:

Highlighting Commented Textual content

Now that we all know how we’re going to signify feedback within the doc construction, let’s go forward and add a number of to the instance doc from the first article and configure the editor to truly present them as highlighted. Since we may have plenty of utility capabilities to cope with feedback on this article, we create a EditorCommentUtils module that can home all these utils. To begin with, we create a operate that creates a mark for a given remark thread ID. We then use that to insert a number of remark threads in our ExampleDocument.

# src/utils/EditorCommentUtils.js

const COMMENT_THREAD_PREFIX = “commentThread_”;

export operate getMarkForCommentThreadID(threadID) {
return `${COMMENT_THREAD_PREFIX}${threadID}`;
}

Under picture underlines in pink the ranges of textual content that we now have as instance remark threads added within the subsequent code snippet. Be aware that the textual content ‘Richard McClintock’ has two remark threads that overlap one another. Particularly, this can be a case of 1 remark thread being absolutely contained inside one other.

# src/utils/ExampleDocument.js
import { getMarkForCommentThreadID } from “../utils/EditorCommentUtils”;
import { v4 as uuid } from “uuid”;

const exampleOverlappingCommentThreadID = uuid();

const ExampleDocument = [

{
text: “Lorem ipsum”,
[getMarkForCommentThreadID(uuid())]: true,
},

{
textual content: “Richard McClintock”,
// word the 2 remark threads right here.
[getMarkForCommentThreadID(uuid())]: true,
[getMarkForCommentThreadID(exampleOverlappingCommentThreadID)]: true,
},
{
textual content: “, a Latin scholar”,
[getMarkForCommentThreadID(exampleOverlappingCommentThreadID)]: true,
},

];

We give attention to the UI aspect of issues of a Commenting System on this article so we assign them IDs within the instance doc straight utilizing the npm bundle uuid. Very doubtless that in a manufacturing model of an editor, these IDs are created by a backend service.

We now give attention to tweaking the editor to indicate these textual content nodes as highlighted. With a purpose to try this, when rendering textual content nodes, we’d like a technique to inform if it has remark threads on it. We add a util getCommentThreadsOnTextNode for that. We construct on the StyledText part that we created within the first article to deal with the case the place it might be attempting to render a textual content node with feedback on. Since we now have some extra performance coming that will be added to commented textual content nodes later, we create a part CommentedText that renders the commented textual content. StyledText will examine if the textual content node it’s attempting to render has any feedback on it. If it does, it renders CommentedText. It makes use of a util getCommentThreadsOnTextNode to infer that.

# src/utils/EditorCommentUtils.js

export operate getCommentThreadsOnTextNode(textNode) {
return new Set(
// As a result of marks are simply properties on nodes,
// we will merely use Object.keys() right here.
Object.keys(textNode)
.filter(isCommentThreadIDMark)
.map(getCommentThreadIDFromMark)
);
}

export operate getCommentThreadIDFromMark(mark) {
if (!isCommentThreadIDMark(mark)) {
throw new Error(“Anticipated mark to be of a remark thread”);
}
return mark.exchange(COMMENT_THREAD_PREFIX, “”);
}

operate isCommentThreadIDMark(mayBeCommentThread) {
return mayBeCommentThread.indexOf(COMMENT_THREAD_PREFIX) === 0;
}

The first article constructed a part StyledText that renders textual content nodes (dealing with character types and so forth). We prolong that part to make use of the above util and render a CommentedText part if the node has feedback on it.

# src/parts/StyledText.js

import { getCommentThreadsOnTextNode } from “../utils/EditorCommentUtils”;

export default operate StyledText({ attributes, kids, leaf }) {

const commentThreads = getCommentThreadsOnTextNode(leaf);

if (commentThreads.measurement > 0) {
return (
<CommentedText
{…attributes}
// We use commentThreads and textNode props later within the article.
commentThreads={commentThreads}
textNode={leaf}
>
{kids}
</CommentedText>
);
}

return <span {…attributes}>{kids}</span>;
}

Under is the implementation of CommentedText that renders the textual content node and attaches the CSS that exhibits it as highlighted.

# src/parts/CommentedText.js

import “./CommentedText.css”;

import classNames from “classnames”;

export default operate CommentedText(props) {
const { commentThreads, …otherProps } = props;
return (
<span
{…otherProps}
className={classNames({
remark: true,
})}
>
{props.kids}
</span>
);
}

# src/parts/CommentedText.css

.remark {
background-color: #feeab5;
}

With all the above code coming collectively, we now see textual content nodes with remark threads highlighted within the editor.

Be aware: The customers presently can’t inform if sure textual content has overlapping feedback on it. The whole highlighted textual content vary appears like a single remark thread. We handle that later within the article the place we introduce the idea of energetic remark thread which lets customers choose a selected remark thread and be capable to see its vary within the editor.

UI Storage For Feedback

Earlier than we add the performance that permits a person to insert new feedback, we first setup a UI state to carry our remark threads. On this article, we use RecoilJS as our state administration library to retailer remark threads, feedback contained contained in the threads and different metadata like creation time, standing, remark creator and so forth. Let’s add Recoil to our utility:

> yarn add recoil

We use Recoil atoms to retailer these two information constructions. If you happen to’re not accustomed to Recoil, atoms are what maintain the appliance state. For various items of utility state, you’d often need to arrange completely different atoms. Atom Household is a set of atoms — it may be considered a Map from a singular key figuring out the atom to the atoms themselves. It’s price going by core ideas of Recoil at this level and familiarizing ourselves with them.

For our use case, we retailer remark threads as an Atom household after which wrap our utility in a RecoilRoot part. RecoilRoot is utilized to supply the context during which the atom values are going for use. We create a separate module CommentState that holds our Recoil atom definitions as we add extra atom definitions later within the article.

# src/utils/CommentState.js

import { atom, atomFamily } from “recoil”;

export const commentThreadsState = atomFamily({
key: “commentThreads”,
default: [],
});

export const commentThreadIDsState = atom({
key: “commentThreadIDs”,
default: new Set([]),
});

Price calling out few issues about these atom definitions:

Every atom/atom household is uniquely recognized by a key and may be arrange with a default worth.
As we construct additional on this article, we’re going to want a technique to iterate over all of the remark threads which might principally imply needing a technique to iterate over commentThreadsState atom household. On the time of writing this text, the best way to do this with Recoil is to arrange one other atom that holds all of the IDs of the atom household. We try this with commentThreadIDsState above. Each these atoms must be stored in sync at any time when we add/delete remark threads.

We add a RecoilRoot wrapper in our root App part so we will use these atoms later. Recoil’s documentation additionally offers a useful Debugger part that we take as it’s and drop into our editor. This part will depart console.debug logs to our Dev console as Recoil atoms are up to date in real-time.

# src/parts/App.js

import { RecoilRoot } from “recoil”;

export default operate App() {

return (
<RecoilRoot>
>

<Editor doc={doc} onChange={updateDocument} />

</RecoilRoot>
);
}

# src/parts/Editor.js

export default operate Editor({ … }): JSX.Component {
…..

return (
<>
<Slate>
…..
</Slate>
<DebugObserver />
</>
);

operate DebugObserver(): React.Node {
// see API hyperlink above for implementation.
}

We additionally want to wish so as to add code that initializes our atoms with the remark threads that exist already on the doc (those we added to our instance doc within the earlier part, as an example). We try this at a later level once we construct the Feedback Sidebar that should learn all of the remark threads in a doc.

At this level, we load our utility, be sure there are not any errors pointing to our Recoil setup and transfer ahead.

Including New Feedback

On this part, we add a button to the toolbar that lets the person add feedback (viz. create a brand new remark thread) for the chosen textual content vary. When the person selects a textual content vary and clicks on this button, we have to do the beneath:

Assign a singular ID to the brand new remark thread being inserted.
Add a brand new mark to Slate doc construction with the ID so the person sees that textual content highlighted.
Add the brand new remark thread to Recoil atoms we created within the earlier part.

Let’s add a util operate to EditorCommentUtils that does #1 and #2.

# src/utils/EditorCommentUtils.js

import { Editor } from “slate”;
import { v4 as uuidv4 } from “uuid”;

export operate insertCommentThread(editor, addCommentThreadToState) {
const threadID = uuidv4();
const newCommentThread = {
// feedback as added can be appended to the thread right here.
feedback: [],
creationTime: new Date(),
// Newly created remark threads are OPEN. We cope with statuses
// later within the article.
standing: “open”,
};
addCommentThreadToState(threadID, newCommentThread);
Editor.addMark(editor, getMarkForCommentThreadID(threadID), true);
return threadID;
}

By utilizing the idea of marks to retailer every remark thread as its personal mark, we’re in a position to merely use the Editor.addMark API so as to add a brand new remark thread on the textual content vary chosen. This name alone handles all of the completely different instances of including feedback — a few of which we described within the earlier part — partially overlapping feedback, feedback inside/overlapping hyperlinks, feedback over daring/italic textual content, feedback spanning paragraphs and so forth. This API name adjusts the node hierarchy to create as many new textual content nodes as wanted to deal with these instances.

addCommentThreadToState is a callback operate that handles step #3 — including the brand new remark thread to Recoil atom . We implement that subsequent as a customized callback hook in order that it’s re-usable. This callback wants so as to add the brand new remark thread to each the atoms — commentThreadsState and commentThreadIDsState. To have the ability to do that, we use the useRecoilCallback hook. This hook can be utilized to assemble a callback which will get a number of issues that can be utilized to learn/set atom information. The one we’re enthusiastic about proper now’s the set operate which can be utilized to replace an atom worth as set(atom, newValueOrUpdaterFunction).

# src/hooks/useAddCommentThreadToState.js

import {
commentThreadIDsState,
commentThreadsState,
} from “../utils/CommentState”;

import { useRecoilCallback } from “recoil”;

export default operate useAddCommentThreadToState() {
return useRecoilCallback(
({ set }) => (id, threadData) => {
set(commentThreadIDsState, (ids) => new Set([…Array.from(ids), id]));
set(commentThreadsState(id), threadData);
},
[]
);
}

The primary name to set provides the brand new ID to the present set of remark thread IDs and returns the brand new Set(which turns into the brand new worth of the atom).

Within the second name, we get the atom for the ID from the atom household — commentThreadsState as commentThreadsState(id) after which set the threadData to be its worth. atomFamilyName(atomID) is how Recoil lets us entry an atom from its atom household utilizing the distinctive key. Loosely talking, let’s imagine that if commentThreadsState was a javascript Map, this name is principally — commentThreadsState.set(id, threadData).

Now that we now have all this code setup to deal with insertion of a brand new remark thread to the doc and Recoil atoms, lets add a button to our toolbar and wire it up with the decision to those capabilities.

# src/parts/Toolbar.js

import { insertCommentThread } from “../utils/EditorCommentUtils”;
import useAddCommentThreadToState from “../hooks/useAddCommentThreadToState”;

export default operate Toolbar({ choice, previousSelection }) {
const editor = useEditor();

const addCommentThread = useAddCommentThreadToState();

const onInsertComment = useCallback(() => {
const newCommentThreadID = insertCommentThread(editor, addCommentThread);
}, [editor, addCommentThread]);

return (
<div className=”toolbar”>

<ToolBarButton
isActive={false}
label={<i className={bi ${getIconForButton(“remark”)}} />}
onMouseDown={onInsertComment}
/>
</div>
);
}

Be aware: We use onMouseDown and never onClick which might have made the editor lose focus and choice to grow to be null. We’ve mentioned that in slightly extra element within the hyperlink insertion part of the first article.

Within the beneath instance, we see the insertion in motion for a easy remark thread and an overlapping remark thread with hyperlinks. Discover how we get updates from Recoil Debugger confirming our state is getting up to date appropriately. We additionally confirm that new textual content nodes are created as threads are being added to the doc.

Within the above instance, the person inserts the next remark threads in that order:

Remark Thread #1 over character ‘B’ (size = 1).
Remark Thread #2 over ‘AB’ (size = 2).
Remark Thread #3 over ‘BC’ (size = 2).

On the finish of those insertions, due to the best way Slate splits the textual content nodes with marks, we may have three textual content nodes — one for every character. Now, if the person clicks on ‘B’, going by the shortest size rule, we choose thread #1 as it’s the shortest of the three in size. If we don’t try this, we wouldn’t have a technique to choose Remark Thread #1 ever since it’s only one-character in size and likewise part of two different threads.

Though this rule makes it simple to floor shorter-length remark threads, we may run into conditions the place longer remark threads grow to be inaccessible since all of the characters contained in them are a part of another shorter remark thread. Let’s have a look at an instance for that.

Let’s assume we now have 100 characters (say, character ‘A’ typed 100 instances that’s) and the person inserts remark threads within the following order:

Remark Thread # 1 of vary 20,80
Remark Thread # 2 of vary 0,50
Remark Thread # 3 of vary 51,100

As you may see within the above instance, if we comply with the rule we simply described right here, clicking on any character between #20 and #80, would at all times choose threads #2 or #3 since they’re shorter than #1 and therefore #1 wouldn’t be selectable. One other state of affairs the place this rule can depart us undecided as to which remark thread to pick out is when there are multiple remark threads of the identical shortest size on a textual content node.

For such mixture of overlapping feedback and plenty of different such combos that one may consider the place following this rule makes a sure remark thread inaccessible by clicking on textual content, we construct a Feedback Sidebar later on this article which supplies person a view of all of the remark threads current within the doc to allow them to click on on these threads within the sidebar and activate them within the editor to see the vary of the remark. We nonetheless would need to have this rule and implement it because it ought to cowl plenty of overlap situations aside from the less-likely examples we cited above. We put in all this effort round this rule primarily as a result of seeing highlighted textual content within the editor and clicking on it to remark is a extra intuitive method of accessing a touch upon textual content than merely utilizing an inventory of feedback within the sidebar.

Insertion Rule

The rule is:

“If the textual content person has chosen and is attempting to touch upon is already absolutely coated by remark thread(s), don’t enable that insertion.”

That is so as a result of if we did enable this insertion, every character in that vary would find yourself having at the least two remark threads (one present and one other the brand new one we simply allowed) making it tough for us to find out which one to pick out when the person clicks on that character later.

Taking a look at this rule, one may surprise why we’d like it within the first place if we have already got the Shortest Remark Vary Rule that permits us to pick out the smallest textual content vary. Why not enable all combos of overlaps if we will use the primary rule to infer the proper remark thread to indicate? As a number of the examples we’ve mentioned earlier, the primary rule works for lots of situations however not all of them. With the Insertion Rule, we attempt to decrease the variety of situations the place the primary rule can’t assist us and we now have to fallback on the Sidebar as the one method for the person to entry that remark thread. Insertion Rule additionally prevents exact-overlaps of remark threads. This rule is usually applied by plenty of in style editors.

Under is an instance the place if this rule didn’t exist, we might enable the Remark Thread #3 after which on account of the primary rule, #3 wouldn’t be accessible since it could grow to be the longest in size.

On this instance, let’s assume we don’t look forward to intersection to grow to be 0 and simply cease once we attain the sting of a remark thread. Now, if the person clicked on #2 and we begin traversal in reverse course, we’d cease firstly of textual content node #2 itself since that’s the beginning of the remark thread A. Consequently, we would not compute the remark thread lengths appropriately for A & B. With the implementation above traversing the farthest edges (textual content nodes 1,2, and three), we should always get B because the shortest remark thread as anticipated.

To see the implementation visually, beneath is a walkthrough with a slideshow of the iterations. We have now two remark threads A and B that overlap one another over textual content node #3 and the person clicks on the overlapping textual content node #3.

Now that we now have all of the code in to make collection of remark threads work, let’s see it in motion. To check our traversal code effectively, we check some simple instances of overlap and a few edge instances like:

Clicking on a commented textual content node firstly/finish of the editor.
Clicking on a commented textual content node with remark threads spanning a number of paragraphs.
Clicking on a commented textual content node proper earlier than a picture node.
Clicking on a commented textual content node overlapping hyperlinks.

Now that our state is appropriately initialized, we will begin implementing the sidebar. All our remark threads within the UI are saved within the Recoil atom household — commentThreadsState. As highlighted earlier, the best way we iterate by all of the objects in a Recoil atom household is by monitoring the atom keys/ids in one other atom. We’ve been doing that with commentThreadIDsState. Let’s add the CommentSidebar part that iterates by the set of ids on this atom and renders a CommentThread part for every.

# src/parts/CommentsSidebar.js

import “./CommentSidebar.css”;

import {commentThreadIDsState,} from “../utils/CommentState”;
import { useRecoilValue } from “recoil”;

export default operate CommentsSidebar(params) {
const allCommentThreadIDs = useRecoilValue(commentThreadIDsState);

return (
<Card className={“comments-sidebar”}>
<Card.Header>Feedback</Card.Header>
<Card.Physique>
{Array.from(allCommentThreadIDs).map((id) => (
<Row key={id}>
<Col>
<CommentThread id={id} />
</Col>
</Row>
))}
</Card.Physique>
</Card>
);
}

Now, we implement the CommentThread part that listens to the Recoil atom within the household equivalent to the remark thread it’s rendering. This fashion, because the person provides extra feedback on the thread within the editor or modifications some other metadata, we will replace the sidebar to mirror that.

Because the sidebar may develop to be actually huge for a doc with plenty of feedback, we cover all feedback however the first one once we render the sidebar. The person can use the ‘Present/Conceal Replies’ button to indicate/cover your complete thread of feedback.

# src/parts/CommentSidebar.js

operate CommentThread({ id }) {
const { feedback } = useRecoilValue(commentThreadsState(id));

const [shouldShowReplies, setShouldShowReplies] = useState(false);
const onBtnClick = useCallback(() => {
setShouldShowReplies(!shouldShowReplies);
}, [shouldShowReplies, setShouldShowReplies]);

if (feedback.size === 0) {
return null;
}

const [firstComment, …otherComments] = feedback;
return (
<Card
physique={true}
className={classNames({
“comment-thread-container”: true,
})}
>
<CommentRow remark={firstComment} showConnector={false} />
{shouldShowReplies
? otherComments.map((remark, index) => (
<CommentRow key={comment-${index}} remark={remark} showConnector={true} />
))
: null}
{feedback.size > 1 ? (
<Button
className={“show-replies-btn”}
measurement=”sm”
variant=”outline-primary”
onClick={onBtnClick}
>
{shouldShowReplies ? “Conceal Replies” : “Present Replies”}
</Button>
) : null}
</Card>
);
}

We’ve reused the CommentRow part from the popover though we added a design remedy utilizing showConnector prop that principally makes all of the feedback look related with a thread within the sidebar.

Now, we render the CommentSidebar within the Editor and confirm that it exhibits all of the threads we now have within the doc and appropriately updates as we add new threads or new feedback to present threads.

# src/parts/Editor.js

return (
<>
<Slate … >
…..
<div className={“sidebar-wrapper”}>
<CommentsSidebar />
</div>
</Slate>
</>
);

We now transfer on to implementing a well-liked Feedback Sidebar interplay present in editors:

Clicking on a remark thread within the sidebar ought to choose/activate that remark thread. We additionally add a differential design remedy to focus on a remark thread within the sidebar if it’s energetic within the editor. To have the ability to accomplish that, we use the Recoil atom — activeCommentThreadIDAtom. Let’s replace the CommentThread part to help this.

# src/parts/CommentsSidebar.js

operate CommentThread({ id }) {

const [activeCommentThreadID, setActiveCommentThreadID] = useRecoilState(
activeCommentThreadIDAtom
);

const onClick = useCallback(() => {
setActiveCommentThreadID(id);
}, [id, setActiveCommentThreadID]);

return (
<Card
physique={true}
className={classNames({
“comment-thread-container”: true,
“is-active”: activeCommentThreadID === id,
})}
onClick={onClick}
>
….
</Card>
);

If we glance intently, we now have a bug in our implementation of sync-ing the energetic remark thread with the sidebar. As we click on on completely different remark threads within the sidebar, the right remark thread is certainly highlighted within the editor. Nevertheless, the Remark Popover doesn’t truly transfer to the modified energetic remark thread. It stays the place it was first rendered. If we have a look at the implementation of the Remark Popover, it renders itself in opposition to the primary textual content node within the editor’s choice. At that time within the implementation, the one technique to choose a remark thread was to click on on a textual content node so we may conveniently depend on the editor’s choice because it was up to date by Slate on account of the press occasion. Within the above onClick occasion, we don’t replace the choice however merely replace the Recoil atom worth inflicting Slate’s choice to stay unchanged and therefore the Remark Popover doesn’t transfer.

An answer to this drawback is to replace the editor’s choice together with updating the Recoil atom when the person clicks on the remark thread within the sidebar. The steps do that are:

Discover all textual content nodes which have this remark thread on them that we’re going to set as the brand new energetic thread.
Type these textual content nodes within the order during which they seem within the doc (We use Slate’s Path.examine API for this).
Compute a variety vary that spans from the beginning of the primary textual content node to the tip of the final textual content node.
Set the choice vary to be the editor’s new choice (utilizing Slate’s Transforms.choose API).

If we simply wished to repair the bug, we may simply discover the primary textual content node in Step #1 that has the remark thread and set that to be the editor’s choice. Nevertheless, it appears like a cleaner strategy to pick out your complete remark vary as we actually are deciding on the remark thread.

Let’s replace the onClick callback implementation to incorporate the steps above.

const onClick = useCallback(() => {

const textNodesWithThread = Editor.nodes(editor, {
at: [],
mode: “lowest”,
match: (n) => Textual content.isText(n) && getCommentThreadsOnTextNode(n).has(id),
});

let textNodeEntry = textNodesWithThread.subsequent().worth;
const allTextNodePaths = [];

whereas (textNodeEntry != null) {
allTextNodePaths.push(textNodeEntry[1]);
textNodeEntry = textNodesWithThread.subsequent().worth;
}

// kind the textual content nodes
allTextNodePaths.kind((p1, p2) => Path.examine(p1, p2));

// set the choice on the editor
Transforms.choose(editor, {
anchor: Editor.level(editor, allTextNodePaths[0], { edge: “begin” }),
focus: Editor.level(
editor,
allTextNodePaths[allTextNodePaths.length – 1],
{ edge: “finish” }
),
});

// Replace the Recoil atom worth.
setActiveCommentThreadID(id);
}, [editor, id, setActiveCommentThreadID]);

Be aware: allTextNodePaths comprises the trail to all of the textual content nodes. We use the Editor.level API to get the beginning and finish factors at that path. The first article goes by Slate’s Location ideas. They’re additionally well-documented on Slate’s documentation.

Let’s confirm that this implementation does repair the bug and the Remark Popover strikes to the energetic remark thread appropriately. This time, we additionally check with a case of overlapping threads to ensure it doesn’t break there.

With the bug repair, we’ve enabled one other sidebar interplay that we haven’t mentioned but. If we now have a very lengthy doc and the person clicks on a remark thread within the sidebar that’s outdoors the viewport, we’d need to scroll to that a part of the doc so the person can give attention to the remark thread within the editor. By setting the choice above utilizing Slate’s API, we get that without cost. Let’s see it in motion beneath.

With that, we wrap our implementation of the sidebar. In direction of the tip of the article, we record out some good characteristic additions and enhancements we will do to the Feedback Sidebar that assist elevate the Commenting and Overview expertise on the editor.

Resolving And Re-Opening Feedback

On this part, we give attention to enabling customers to mark remark threads as ‘Resolved’ or be capable to re-open them for dialogue if wanted. From an implementation element perspective, that is the standing metadata on a remark thread that we modify because the person performs this motion. From a person’s perspective, this can be a very helpful characteristic because it offers them a technique to affirm that the dialogue about one thing on the doc has concluded or must be re-opened as a result of there are some updates/new views, and so forth.

To allow toggling the standing, we add a button to the CommentPopover that permits the person to toggle between the 2 statuses: open and resolved.

# src/parts/CommentThreadPopover.js

export default operate CommentThreadPopover({
editorOffsets,
choice,
threadID,
}) {

const [threadData, setCommentThreadData] = useRecoilState(
commentThreadsState(threadID)
);

const onToggleStatus = useCallback(() => {
const currentStatus = threadData.standing;
setCommentThreadData((threadData) => ({
…threadData,
standing: currentStatus === “open” ? “resolved” : “open”,
}));
}, [setCommentThreadData, threadData.status]);

return (
<NodePopover

header={
<Header
standing={threadData.standing}
shouldAllowStatusChange={threadData.feedback.size > 0}
onToggleStatus={onToggleStatus}
/>
}
>
<div className={“comment-list”}>

</div>
</NodePopover>
);
}

operate Header({ onToggleStatus, shouldAllowStatusChange, standing }) {
return (
<div className={“comment-thread-popover-header”}>
{shouldAllowStatusChange && standing != null ? (
<Button measurement=”sm” variant=”major” onClick={onToggleStatus}>
{standing === “open” ? “Resolve” : “Re-Open”}
</Button>
) : null}
</div>
);
}

Earlier than we check this, let’s additionally give the Feedback Sidebar a differential design remedy for resolved feedback in order that the person can simply detect which remark threads are un-resolved or open and give attention to these in the event that they need to.

# src/parts/CommentsSidebar.js

operate CommentThread({ id }) {

const { feedback, standing } = useRecoilValue(commentThreadsState(id));


return (
<Card
physique={true}
className={classNames({
“comment-thread-container”: true,
“is-resolved”: standing === “resolved”,
“is-active”: activeCommentThreadID === id,
})}
onClick={onClick}
>

</Card>
);
}

Conclusion

On this article, we constructed the core UI infrastructure for a Commenting System on a Wealthy Textual content Editor. The set of functionalities we add right here act as a basis to construct a richer Collaboration Expertise on an editor the place collaborators may annotate elements of the doc and have conversations about them. Including a Feedback Sidebar offers us an area to have extra conversational or review-based functionalities to be enabled on the product.

Alongside these traces, listed here are a few of options {that a} Wealthy Textual content Editor may contemplate including on high of what we constructed on this article:

Help for @ mentions so collaborators may tag each other in feedback;
Help for media varieties like photographs and movies to be added to remark threads;
Suggestion Mode on the doc stage that permits reviewers to make edits to the doc that seem as options for modifications. One may confer with this characteristic in Google Docs or Change Monitoring in Microsoft Phrase as examples;
Enhancements to the sidebar to look conversations by key phrase, filter threads by standing or remark creator(s), and so forth.

    About Marketing Solution Australia

    We are a digital marketing company with a focus on helping our customers achieve great results across several key areas.

    Request a free quote

    We offer professional SEO services that help websites increase their organic search score drastically in order to compete for the highest rankings even when it comes to highly competitive keywords.

    Subscribe to our newsletter!

    More from our blog

    See all posts

    Leave a Comment