* Comment model * Framework, model, policy, presenter, api endpoint etc * Iteration, first pass of UI * fixes, refactors * Comment commands * comment socket support * typing indicators * comment component, styling * wip * right sidebar resize * fix: CMD+Enter submit * Add usePersistedState fix: Main page scrolling on comment highlight * drafts * Typing indicator * refactor * policies * Click thread to highlight Improve comment timestamps * padding * Comment menu v1 * Change comments to use editor * Basic comment editing * fix: Hide commenting button when disabled at team level * Enable opening sidebar without mark * Move selected comment to location state * Add comment delete confirmation * Add comment count to document meta * fix: Comment sidebar togglable Add copy link to comment * stash * Restore History changes * Refactor right sidebar to allow for comment animation * Update to new router best practices * stash * Various improvements * stash * Handle click outside * Fix incorrect placeholder in input fix: Input box appearing on other sessions erroneously * stash * fix: Don't leave orphaned child comments * styling * stash * Enable comment toggling again * Edit styling, merge conflicts * fix: Cannot navigate from insights to comments * Remove draft comment mark on click outside * Fix: Empty comment sidebar, tsc * Remove public toggle * fix: All comments are recessed fix: Comments should not be printed * fix: Associated mark should be removed on comment delete * Revert unused changes * Empty state, basic RTL support * Create dont toggle comment mark * Make it feel more snappy * Highlight active comment in text * fix animation * RTL support * Add reply CTA * Translations
156 lines
3.8 KiB
TypeScript
156 lines
3.8 KiB
TypeScript
import { Node } from "prosemirror-model";
|
|
import headingToSlug from "../editor/lib/headingToSlug";
|
|
|
|
export type Heading = {
|
|
/* The heading in plain text */
|
|
title: string;
|
|
/* The level of the heading */
|
|
level: number;
|
|
/* The unique id of the heading */
|
|
id: string;
|
|
};
|
|
|
|
export type CommentMark = {
|
|
/* The unique id of the comment */
|
|
id: string;
|
|
/* The id of the user who created the comment */
|
|
userId: string;
|
|
};
|
|
|
|
export type Task = {
|
|
/* The text of the task */
|
|
text: string;
|
|
/* Whether the task is completed or not */
|
|
completed: boolean;
|
|
};
|
|
|
|
export default class ProsemirrorHelper {
|
|
/**
|
|
* Removes any empty paragraphs from the beginning and end of the document.
|
|
*
|
|
* @returns True if the editor is empty
|
|
*/
|
|
static trim(doc: Node) {
|
|
const first = doc.firstChild;
|
|
const last = doc.lastChild;
|
|
const firstIsEmpty =
|
|
first?.type.name === "paragraph" && !first.textContent.trim();
|
|
const lastIsEmpty =
|
|
last?.type.name === "paragraph" && !last.textContent.trim();
|
|
const firstIsLast = first === last;
|
|
|
|
return doc.cut(
|
|
firstIsEmpty ? first.nodeSize : 0,
|
|
lastIsEmpty && !firstIsLast ? doc.nodeSize - last.nodeSize : undefined
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns true if the trimmed content of the passed document is an empty
|
|
* string.
|
|
*
|
|
* @returns True if the editor is empty
|
|
*/
|
|
static isEmpty(doc: Node) {
|
|
return !doc || doc.textContent.trim() === "";
|
|
}
|
|
|
|
/**
|
|
* Iterates through the document to find all of the comments that exist as
|
|
* marks.
|
|
*
|
|
* @param doc Prosemirror document node
|
|
* @returns Array<CommentMark>
|
|
*/
|
|
static getComments(doc: Node): CommentMark[] {
|
|
const comments: CommentMark[] = [];
|
|
|
|
doc.descendants((node) => {
|
|
node.marks.forEach((mark) => {
|
|
if (mark.type.name === "comment") {
|
|
comments.push(mark.attrs as CommentMark);
|
|
}
|
|
});
|
|
|
|
return true;
|
|
});
|
|
|
|
return comments;
|
|
}
|
|
|
|
/**
|
|
* Iterates through the document to find all of the tasks and their completion
|
|
* state.
|
|
*
|
|
* @param doc Prosemirror document node
|
|
* @returns Array<Task>
|
|
*/
|
|
static getTasks(doc: Node): Task[] {
|
|
const tasks: Task[] = [];
|
|
|
|
doc.descendants((node) => {
|
|
if (!node.isBlock) {
|
|
return false;
|
|
}
|
|
|
|
if (node.type.name === "checkbox_list") {
|
|
node.content.forEach((listItem) => {
|
|
let text = "";
|
|
|
|
listItem.forEach((contentNode) => {
|
|
if (contentNode.type.name === "paragraph") {
|
|
text += contentNode.textContent;
|
|
}
|
|
});
|
|
|
|
tasks.push({
|
|
text,
|
|
completed: listItem.attrs.checked,
|
|
});
|
|
});
|
|
}
|
|
|
|
return true;
|
|
});
|
|
|
|
return tasks;
|
|
}
|
|
|
|
/**
|
|
* Iterates through the document to find all of the headings and their level.
|
|
*
|
|
* @param doc Prosemirror document node
|
|
* @returns Array<Heading>
|
|
*/
|
|
static getHeadings(doc: Node) {
|
|
const headings: Heading[] = [];
|
|
const previouslySeen = {};
|
|
|
|
doc.forEach((node) => {
|
|
if (node.type.name === "heading") {
|
|
// calculate the optimal id
|
|
const id = headingToSlug(node);
|
|
let name = id;
|
|
|
|
// check if we've already used it, and if so how many times?
|
|
// Make the new id based on that number ensuring that we have
|
|
// unique ID's even when headings are identical
|
|
if (previouslySeen[id] > 0) {
|
|
name = headingToSlug(node, previouslySeen[id]);
|
|
}
|
|
|
|
// record that we've seen this id for the next loop
|
|
previouslySeen[id] =
|
|
previouslySeen[id] !== undefined ? previouslySeen[id] + 1 : 1;
|
|
|
|
headings.push({
|
|
title: node.textContent,
|
|
level: node.attrs.level,
|
|
id: name,
|
|
});
|
|
}
|
|
});
|
|
return headings;
|
|
}
|
|
}
|