UNPKG

@contentstack/live-preview-utils

Version:

Contentstack provides the Live Preview SDK to establish a communication channel between the various Contentstack SDKs and your website, transmitting live changes to the preview pane.

1 lines 14.6 kB
{"version":3,"sources":["../../../../src/visualBuilder/utils/collabUtils.ts"],"sourcesContent":["import {\n ICommentState,\n IMentionedList,\n IMentionItem,\n IMessageDTO,\n IUserDTO,\n IUserState,\n} from \"../types/collab.types\";\nimport { maxMessageLength, mentionLimit } from \"./constants\";\nimport { uniqBy } from \"lodash-es\";\nimport DOMPurify from \"dompurify\";\nimport dayjs from \"dayjs\";\n\nconst escapeRegExp = (string: string): string => {\n return string.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n};\n\n/**\n * Generates the title for the thread based on the number of comments.\n * @param {number} commentCount - The number of comments.\n * @returns {string} The title for the thread.\n */\nexport const getThreadTitle = (commentCount: number): string => {\n if (commentCount === 0) return \"Add New Comment\";\n return commentCount === 1 ? \"1 Comment\" : `${commentCount} Comments`;\n};\n\n/**\n * returns the available email.\n * @param {IUserDTO} user - The user object.\n * @returns {string} The user's email.\n */\nexport const getUserName = (user: IUserDTO): string => {\n return user.firstName && user.lastName\n ? `${user.firstName} ${user.lastName}`\n : user.firstName || user.lastName || user.email;\n};\n\n/**\n * Validates the comment length and the number of mentions.\n * @param {string} comment - The comment message.\n * @param {IMentionedList} toUsers - The list of mentioned users.\n * @returns {string} The error message if validation fails, otherwise an empty string.\n */\nexport const validateCommentAndMentions = (\n comment: string,\n toUsers: IMentionedList\n): string => {\n if (comment.length > maxMessageLength) {\n return `Limit exceeded. You can have a maximum length of ${maxMessageLength} characters.`;\n }\n if (toUsers.length > mentionLimit) {\n return `Limit exceeded. You can tag a maximum of ${mentionLimit} users.`;\n }\n return \"\";\n};\n\n/**\n * Removes mentions that no longer exist in the message.\n * @param {string} message - The comment message.\n * @param {IMentionedList} toUsers - The list of mentioned users.\n * @returns {Object} The updated lists of mentioned users.\n */\nexport const filterOutInvalidMentions = (\n message: string,\n toUsers: IMentionedList\n) => {\n const to_users_temp = toUsers.filter((user) =>\n message.includes(user.display)\n );\n\n return {\n toUsers: uniqBy(to_users_temp, \"id\"),\n };\n};\n\n/**\n * Replaces mention placeholders with display names in the comment message.\n * @param {IMessageDTO | undefined} comment - The comment object.\n * @param {IUserState} userState - The user state containing user and role maps.\n * @param {\"text\" | \"html\"} profile - The format for the output message, either plain text or HTML.\n * @returns {string | undefined} The formatted message or undefined if the comment is not provided.\n */\nexport const getMessageWithDisplayName = (\n comment: IMessageDTO | undefined | null,\n userState: IUserState,\n profile: \"text\" | \"html\"\n): string | undefined => {\n if (!comment) return undefined;\n\n let tempText = sanitizeData(comment.message).replace(/<[^>]*>/g, \"\");\n\n comment.toUsers?.forEach((user) => {\n const userPattern = new RegExp(`{{${user}}}`, \"g\");\n const userData = userState.userMap[user];\n const displayName = userData\n ? userData.display || getUserName(userData)\n : `unknown user`;\n\n const replacement =\n profile === \"html\"\n ? `<b class=\"collab-thread-comment--message\">@${displayName}</b>`\n : `@${displayName}`;\n tempText = tempText.replace(userPattern, replacement);\n });\n\n return tempText;\n};\n\n/**\n * Sanitizes HTML content to prevent XSS attacks.\n * @param {any} dirty - The unsanitized HTML content.\n * @returns {string} The sanitized HTML content.\n */\nexport const sanitizeData = (dirty: any): string => {\n return DOMPurify.sanitize(dirty, { USE_PROFILES: { html: true } });\n};\n\n/**\n * Constructs the comment body with mentions replaced by their unique identifiers.\n * @param {ICommentState} state - The state containing the comment and mentions.\n * @returns {Object} The comment body containing the sanitized message and mentioned users.\n */\nexport const getCommentBody = (state: ICommentState): ICommentState => {\n let finalMessage = sanitizeData(state.message)\n .replace(/[^\\S\\r\\n]+/g, \" \")\n .replace(/ *\\n */g, \"\\n\")\n .replace(/<[^>]*>/g, \"\")\n .trim();\n\n const comment = {\n message: finalMessage,\n toUsers: [],\n images: [],\n createdBy: state.createdBy,\n author: state.author,\n };\n\n const updateMentionToUID = (\n entity: IMentionItem,\n result: Array<string>\n ) => {\n const displayName = entity.display;\n\n const escapedDisplayName = escapeRegExp(`@${displayName}`);\n const regexUser = new RegExp(escapedDisplayName, \"g\");\n finalMessage = finalMessage.replace(regexUser, `{{${entity.id}}}`);\n result.push(entity.id);\n };\n\n state.toUsers?.forEach((user) => updateMentionToUID(user, comment.toUsers));\n\n comment.message = finalMessage;\n return comment;\n};\n\nexport function normalizePath(path: string): string {\n if (path === \"/\") return path;\n return path.endsWith(\"/\") ? path.slice(0, -1) : path;\n}\n\nexport function fixSvgXPath(xpath: string | null): string {\n if (!xpath) return \"\";\n return xpath.replace(/\\/svg/g, \"/*[name()='svg']\");\n}\n\n/**\n * populate the position of the thread based on edges of the screen.\n * @param position\n * @param options\n * @returns\n */\nexport function adjustPositionToViewport(\n position: { top: number; left: number },\n options: {\n threadWidth?: number;\n safeMargin?: number;\n topSafeMargin?: number;\n } = {}\n): { top: number; left: number } {\n const { top, left } = position;\n const viewportWidth = window.innerWidth;\n const safeMargin = options.safeMargin ?? 16;\n const topSafeMargin = options.topSafeMargin ?? 42;\n const threadWidth = options.threadWidth ?? 16;\n\n let adjustedLeft = left;\n let adjustedTop = top;\n\n // Adjust position if too close to right edge\n if (adjustedLeft + threadWidth > viewportWidth - safeMargin) {\n adjustedLeft = viewportWidth - safeMargin - threadWidth;\n }\n\n // Adjust position if too close to top edge\n if (adjustedTop - window.scrollY < topSafeMargin) {\n adjustedTop = window.scrollY + topSafeMargin;\n }\n\n return { top: adjustedTop, left: adjustedLeft };\n}\n\nexport function formatDate(dateString: string): string {\n if (!dateString) return \"\";\n return dayjs(dateString).format(\"MMM DD, YYYY, hh:mm A\");\n}\n\ninterface PositionCoords {\n top: number;\n left: number;\n}\n\ninterface Positions {\n bottom: PositionCoords;\n top: PositionCoords;\n left: PositionCoords;\n right: PositionCoords;\n}\n\n/**\n * Calculates and updates tooltip position based on available viewport space.\n */\nexport const positionTooltip = (\n tooltipRef: React.RefObject<HTMLDivElement>,\n targetRef: React.RefObject<HTMLDivElement>,\n position: \"top\" | \"bottom\" | \"left\" | \"right\",\n setActualPosition: (position: \"top\" | \"bottom\" | \"left\" | \"right\") => void\n) => {\n if (!tooltipRef.current || !targetRef.current) return;\n\n const targetRect = targetRef.current.getBoundingClientRect();\n const tooltipRect = tooltipRef.current.getBoundingClientRect();\n const margin = 8;\n\n const positions: Positions = {\n bottom: {\n top: targetRect.bottom + margin,\n left: targetRect.left + (targetRect.width - tooltipRect.width) / 2,\n },\n top: {\n top: targetRect.top - tooltipRect.height - margin,\n left: targetRect.left + (targetRect.width - tooltipRect.width) / 2,\n },\n left: {\n top: targetRect.top + (targetRect.height - tooltipRect.height) / 2,\n left: targetRect.left - tooltipRect.width - margin,\n },\n right: {\n top: targetRect.top + (targetRect.height - tooltipRect.height) / 2,\n left: targetRect.right + margin,\n },\n };\n\n let bestPosition = position;\n let coords = positions[position];\n\n const viewportWidth = window.innerWidth;\n const viewportHeight = window.innerHeight;\n\n const wouldBeOutsideViewport = {\n bottom: coords.top + tooltipRect.height > viewportHeight,\n top: coords.top < 0,\n left: coords.left < 0,\n right: coords.left + tooltipRect.width > viewportWidth,\n };\n\n const horizontalOutOfBounds =\n coords.left < 0 || coords.left + tooltipRect.width > viewportWidth;\n\n if (wouldBeOutsideViewport[position] || horizontalOutOfBounds) {\n const positionPriority = [\"bottom\", \"top\", \"right\", \"left\"];\n\n positionPriority.splice(positionPriority.indexOf(position), 1);\n positionPriority.push(position);\n\n for (const pos of positionPriority) {\n const testCoords = positions[pos as keyof Positions];\n\n const isVisible =\n testCoords.top >= 0 &&\n testCoords.top + tooltipRect.height <= viewportHeight &&\n testCoords.left >= 0 &&\n testCoords.left + tooltipRect.width <= viewportWidth;\n\n if (isVisible) {\n bestPosition = pos as \"top\" | \"bottom\" | \"left\" | \"right\";\n coords = testCoords;\n break;\n }\n }\n }\n\n if (coords.left < 0) {\n coords.left = margin;\n } else if (coords.left + tooltipRect.width > viewportWidth) {\n coords.left = viewportWidth - tooltipRect.width - margin;\n }\n\n if (coords.top < 0) {\n coords.top = margin;\n } else if (coords.top + tooltipRect.height > viewportHeight) {\n coords.top = viewportHeight - tooltipRect.height - margin;\n }\n\n setActualPosition(bestPosition);\n\n Object.assign(tooltipRef.current.style, {\n top: `${coords.top}px`,\n left: `${coords.left}px`,\n });\n};\n"],"mappings":"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","names":["DOMPurify","dayjs"]}