Move common functions into utils file
[MAILPOET-6354]
This commit is contained in:
@ -4,6 +4,11 @@ import { BlockControls } from '@wordpress/block-editor';
|
|||||||
import { ToolbarButton, ToolbarGroup } from '@wordpress/components';
|
import { ToolbarButton, ToolbarGroup } from '@wordpress/components';
|
||||||
import { storeName } from '../../store';
|
import { storeName } from '../../store';
|
||||||
import { useSelect, useDispatch } from '@wordpress/data';
|
import { useSelect, useDispatch } from '@wordpress/data';
|
||||||
|
import {
|
||||||
|
createTextToHtmlMap,
|
||||||
|
getCursorPosition,
|
||||||
|
isMatchingComment,
|
||||||
|
} from '../../components/personalization-tags/rich-text-utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disable Rich text formats we currently cannot support
|
* Disable Rich text formats we currently cannot support
|
||||||
@ -21,9 +26,6 @@ function disableCertainRichTextFormats() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
isActive: boolean;
|
|
||||||
value: string;
|
|
||||||
onChange: ( value: string ) => void;
|
|
||||||
contentRef: React.RefObject< HTMLElement >;
|
contentRef: React.RefObject< HTMLElement >;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -50,163 +52,6 @@ function PersonalizationTagsButton( { contentRef }: Props ) {
|
|||||||
return attributes?.content?.originalHTML || attributes?.content || ''; // After first saving the content does not have property originalHTML, so we need to check for content as well
|
return attributes?.content?.originalHTML || attributes?.content || ''; // After first saving the content does not have property originalHTML, so we need to check for content as well
|
||||||
} );
|
} );
|
||||||
|
|
||||||
// Convert `RichText` DOM offset to stored value offset
|
|
||||||
const mapRichTextToValue = ( html ) => {
|
|
||||||
const mapping = []; // Maps HTML indices to stored value indices
|
|
||||||
let htmlIndex = 0;
|
|
||||||
let valueIndex = 0;
|
|
||||||
let isInsideTag = false;
|
|
||||||
|
|
||||||
while ( htmlIndex < html.length ) {
|
|
||||||
const htmlChar = html[ htmlIndex ];
|
|
||||||
if ( htmlChar === '<' ) {
|
|
||||||
isInsideTag = true;
|
|
||||||
}
|
|
||||||
if ( htmlChar === '>' ) {
|
|
||||||
isInsideTag = false;
|
|
||||||
}
|
|
||||||
mapping[ htmlIndex ] = valueIndex;
|
|
||||||
if ( ! isInsideTag ) {
|
|
||||||
valueIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
htmlIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return mapping;
|
|
||||||
};
|
|
||||||
|
|
||||||
const createTextToHtmlMap = ( html ) => {
|
|
||||||
const text = [];
|
|
||||||
const mapping = [];
|
|
||||||
let isInsideComment = false;
|
|
||||||
|
|
||||||
for ( let i = 0; i < html.length; i++ ) {
|
|
||||||
const char = html[ i ];
|
|
||||||
|
|
||||||
// Detect start of an HTML comment
|
|
||||||
if ( ! isInsideComment && html.slice( i, i + 4 ) === '<!--' ) {
|
|
||||||
i += 4; // Adjust loop
|
|
||||||
isInsideComment = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detect end of an HTML comment
|
|
||||||
if ( isInsideComment && html.slice( i, i + 3 ) === '-->' ) {
|
|
||||||
i += 3; // Adjust loop
|
|
||||||
isInsideComment = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
text.push( char );
|
|
||||||
mapping[ text.length - 1 ] = i;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append mapping for positions between adjacent comments
|
|
||||||
if (
|
|
||||||
mapping.length === 0 ||
|
|
||||||
mapping[ mapping.length - 1 ] !== html.length
|
|
||||||
) {
|
|
||||||
mapping[ text.length ] = html.length; // Map end of content
|
|
||||||
}
|
|
||||||
|
|
||||||
return { mapping };
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCursorPosition = ( richTextRef ) => {
|
|
||||||
const selection =
|
|
||||||
richTextRef.current.ownerDocument.defaultView.getSelection();
|
|
||||||
|
|
||||||
if ( ! selection.rangeCount ) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const range = selection.getRangeAt( 0 );
|
|
||||||
const container = range.startContainer;
|
|
||||||
const currentValue = blockContent;
|
|
||||||
|
|
||||||
// Ensure the selection is within the RichText component
|
|
||||||
if ( ! richTextRef.current.contains( container ) ) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let offset = range.startOffset; // Initial offset within the current node
|
|
||||||
let currentNode = container;
|
|
||||||
|
|
||||||
// Traverse the DOM tree to calculate the total offset
|
|
||||||
if ( currentNode !== richTextRef.current ) {
|
|
||||||
while ( currentNode && currentNode !== richTextRef.current ) {
|
|
||||||
while ( currentNode.previousSibling ) {
|
|
||||||
currentNode = currentNode.previousSibling;
|
|
||||||
offset += currentNode.textContent.length;
|
|
||||||
}
|
|
||||||
currentNode = currentNode.parentNode;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Locate the selected content in the HTML
|
|
||||||
const htmlContent = richTextRef.current.innerHTML;
|
|
||||||
const selectedText = range.toString();
|
|
||||||
const startIndex = htmlContent.indexOf( selectedText, offset );
|
|
||||||
const mapping = mapRichTextToValue( htmlContent );
|
|
||||||
|
|
||||||
// Translate `offset` from `RichText` HTML to stored value
|
|
||||||
const translatedOffset = mapping[ startIndex ] || 0;
|
|
||||||
|
|
||||||
// Search for the HTML comment in the stored value
|
|
||||||
const htmlCommentRegex = /<!--\[(.*?)\]-->/g;
|
|
||||||
let match;
|
|
||||||
let commentStart = -1;
|
|
||||||
let commentEnd = -1;
|
|
||||||
|
|
||||||
while (
|
|
||||||
( match = htmlCommentRegex.exec( currentValue ) ) !== null
|
|
||||||
) {
|
|
||||||
const [ fullMatch ] = match;
|
|
||||||
const matchStartIndex = match.index;
|
|
||||||
const matchEndIndex = matchStartIndex + fullMatch.length;
|
|
||||||
|
|
||||||
if (
|
|
||||||
translatedOffset >= matchStartIndex &&
|
|
||||||
translatedOffset <= matchEndIndex
|
|
||||||
) {
|
|
||||||
commentStart = matchStartIndex;
|
|
||||||
commentEnd = matchEndIndex;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If a comment is detected, return its range
|
|
||||||
if ( commentStart !== -1 && commentEnd !== -1 ) {
|
|
||||||
return {
|
|
||||||
start: commentStart,
|
|
||||||
end: commentEnd,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
start: Math.min( offset, currentValue.length ),
|
|
||||||
end: Math.min(
|
|
||||||
offset + range.toString().length,
|
|
||||||
currentValue.length
|
|
||||||
),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const isMatchingComment = ( content, start, end ): boolean => {
|
|
||||||
// Extract the substring
|
|
||||||
const substring = content.slice( start, end );
|
|
||||||
|
|
||||||
// Define the regex for HTML comments
|
|
||||||
const htmlCommentRegex = /^<!--(.*?)-->$/;
|
|
||||||
|
|
||||||
// Test if the substring matches the regex
|
|
||||||
const match = htmlCommentRegex.exec( substring );
|
|
||||||
|
|
||||||
if ( match ) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleInsert = ( tag: string ) => {
|
const handleInsert = ( tag: string ) => {
|
||||||
const selection =
|
const selection =
|
||||||
contentRef.current.ownerDocument.defaultView.getSelection();
|
contentRef.current.ownerDocument.defaultView.getSelection();
|
||||||
@ -219,27 +64,19 @@ function PersonalizationTagsButton( { contentRef }: Props ) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate text-to-HTML mapping
|
|
||||||
const { mapping } = createTextToHtmlMap( blockContent );
|
const { mapping } = createTextToHtmlMap( blockContent );
|
||||||
|
let { start, end } = getCursorPosition( contentRef, blockContent );
|
||||||
|
|
||||||
// Ensure selection range is within bounds
|
|
||||||
const selectionRange = getCursorPosition( contentRef );
|
|
||||||
const start = selectionRange.start;
|
|
||||||
const end = selectionRange.end;
|
|
||||||
|
|
||||||
// Default values for starting and ending indexes.
|
|
||||||
let htmlStart = start;
|
|
||||||
let htmlEnd = end;
|
|
||||||
// If indexes are not matching a comment, update them
|
// If indexes are not matching a comment, update them
|
||||||
if ( ! isMatchingComment( blockContent, htmlStart, htmlEnd ) ) {
|
if ( ! isMatchingComment( blockContent, start, end ) ) {
|
||||||
htmlStart = mapping[ start ] ?? blockContent.length;
|
start = mapping[ start ] ?? blockContent.length;
|
||||||
htmlEnd = mapping[ end ] ?? blockContent.length;
|
end = mapping[ end ] ?? blockContent.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
const updatedContent =
|
const updatedContent =
|
||||||
blockContent.slice( 0, htmlStart ) +
|
blockContent.slice( 0, start ) +
|
||||||
`<!--${ tag }-->` +
|
`<!--${ tag }-->` +
|
||||||
blockContent.slice( htmlEnd );
|
blockContent.slice( end );
|
||||||
|
|
||||||
updateBlockAttributes( selectedBlockId, { content: updatedContent } );
|
updateBlockAttributes( selectedBlockId, { content: updatedContent } );
|
||||||
};
|
};
|
||||||
|
@ -0,0 +1,191 @@
|
|||||||
|
import * as React from '@wordpress/element';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps HTML indices to corresponding stored value indices in RichText content.
|
||||||
|
* This function skips over HTML tags, only mapping visible text content.
|
||||||
|
*
|
||||||
|
* @param {string} html - The HTML string to map. Example: 'Hello <span contenteditable="false" data-rich-text-comment="[user/firstname]"><span>[user/firstname]</span></span>!'
|
||||||
|
* @return {number[]} - A mapping array where each HTML index points to its corresponding stored value index.
|
||||||
|
*/
|
||||||
|
const mapRichTextToValue = ( html: string ) => {
|
||||||
|
const mapping = []; // Maps HTML indices to stored value indices
|
||||||
|
let htmlIndex = 0;
|
||||||
|
let valueIndex = 0;
|
||||||
|
let isInsideTag = false;
|
||||||
|
|
||||||
|
while ( htmlIndex < html.length ) {
|
||||||
|
const htmlChar = html[ htmlIndex ];
|
||||||
|
if ( htmlChar === '<' ) {
|
||||||
|
isInsideTag = true; // Entering an HTML tag
|
||||||
|
}
|
||||||
|
if ( htmlChar === '>' ) {
|
||||||
|
isInsideTag = false; // Exiting an HTML tag
|
||||||
|
}
|
||||||
|
mapping[ htmlIndex ] = valueIndex;
|
||||||
|
if ( ! isInsideTag ) {
|
||||||
|
valueIndex++; // Increment value index only for visible text
|
||||||
|
}
|
||||||
|
|
||||||
|
htmlIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapping;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a mapping between plain text indices and corresponding HTML indices.
|
||||||
|
* This includes handling of HTML comments, ensuring text is mapped correctly.
|
||||||
|
* We need to this step because the text displayed to the user is different from the HTML content.
|
||||||
|
*
|
||||||
|
* @param {string} html - The HTML string to map. Example: 'Hello, <!--[user/firstname]-->!'
|
||||||
|
* @return {{ mapping: number[] }} - An object containing the mapping array.
|
||||||
|
*/
|
||||||
|
const createTextToHtmlMap = ( html: string ) => {
|
||||||
|
const text = [];
|
||||||
|
const mapping = [];
|
||||||
|
let isInsideComment = false;
|
||||||
|
|
||||||
|
for ( let i = 0; i < html.length; i++ ) {
|
||||||
|
const char = html[ i ];
|
||||||
|
|
||||||
|
// Detect start of an HTML comment
|
||||||
|
if ( ! isInsideComment && html.slice( i, i + 4 ) === '<!--' ) {
|
||||||
|
i += 4; // Skip the start of the comment
|
||||||
|
isInsideComment = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect end of an HTML comment
|
||||||
|
if ( isInsideComment && html.slice( i, i + 3 ) === '-->' ) {
|
||||||
|
i += 3; // Skip the end of the comment
|
||||||
|
isInsideComment = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
text.push( char );
|
||||||
|
mapping[ text.length - 1 ] = i; // Map text index to HTML index
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the mapping includes the end of the content
|
||||||
|
if (
|
||||||
|
mapping.length === 0 ||
|
||||||
|
mapping[ mapping.length - 1 ] !== html.length
|
||||||
|
) {
|
||||||
|
mapping[ text.length ] = html.length; // Map end of content
|
||||||
|
}
|
||||||
|
|
||||||
|
return { mapping };
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the cursor position within a RichText component.
|
||||||
|
* Calculates the offset in plain text while accounting for HTML tags and comments.
|
||||||
|
*
|
||||||
|
* @param {React.RefObject<HTMLElement>} richTextRef - Reference to the RichText component.
|
||||||
|
* @param {string} content - The plain text content of the block.
|
||||||
|
* @return {{ start: number, end: number } | null} - The cursor position as start and end offsets.
|
||||||
|
*/
|
||||||
|
const getCursorPosition = (
|
||||||
|
richTextRef: React.RefObject< HTMLElement >,
|
||||||
|
content: string
|
||||||
|
): { start: number; end: number } => {
|
||||||
|
const selection =
|
||||||
|
richTextRef.current.ownerDocument.defaultView.getSelection();
|
||||||
|
|
||||||
|
if ( ! selection.rangeCount ) {
|
||||||
|
return null; // No selection present
|
||||||
|
}
|
||||||
|
|
||||||
|
const range = selection.getRangeAt( 0 );
|
||||||
|
const container = range.startContainer;
|
||||||
|
|
||||||
|
// Ensure the selection is within the RichText component
|
||||||
|
if ( ! richTextRef.current.contains( container ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let offset = range.startOffset; // Initial offset within the current node
|
||||||
|
let currentNode = container;
|
||||||
|
|
||||||
|
// Traverse the DOM tree to calculate the total offset
|
||||||
|
if ( currentNode !== richTextRef.current ) {
|
||||||
|
while ( currentNode && currentNode !== richTextRef.current ) {
|
||||||
|
while ( currentNode.previousSibling ) {
|
||||||
|
currentNode = currentNode.previousSibling;
|
||||||
|
offset += currentNode.textContent.length; // Add text content length of siblings
|
||||||
|
}
|
||||||
|
currentNode = currentNode.parentNode;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Locate the selected content in the HTML
|
||||||
|
const htmlContent = richTextRef.current.innerHTML;
|
||||||
|
const selectedText = range.toString();
|
||||||
|
|
||||||
|
// The selected text is wrapped by span and it is also in the HTML attribute. Adding brackets helps to find the correct index.
|
||||||
|
// After that we need increase the index by 5 to get the correct index. (4 for start of the HTML comment and 1 for the first bracket)
|
||||||
|
const startIndex =
|
||||||
|
htmlContent.indexOf( `>${ selectedText }<`, offset ) + 5;
|
||||||
|
|
||||||
|
// Map the startIndex to the stored value
|
||||||
|
const mapping = mapRichTextToValue( htmlContent );
|
||||||
|
const translatedOffset = mapping[ startIndex ] || 0;
|
||||||
|
|
||||||
|
// Search for HTML comments within the stored value
|
||||||
|
const htmlCommentRegex = /<!--\[(.*?)\]-->/g;
|
||||||
|
let match;
|
||||||
|
let commentStart = -1;
|
||||||
|
let commentEnd = -1;
|
||||||
|
|
||||||
|
while ( ( match = htmlCommentRegex.exec( content ) ) !== null ) {
|
||||||
|
const [ fullMatch ] = match;
|
||||||
|
const matchStartIndex = match.index;
|
||||||
|
const matchEndIndex = matchStartIndex + fullMatch.length;
|
||||||
|
|
||||||
|
if (
|
||||||
|
translatedOffset >= matchStartIndex &&
|
||||||
|
translatedOffset <= matchEndIndex
|
||||||
|
) {
|
||||||
|
commentStart = matchStartIndex;
|
||||||
|
commentEnd = matchEndIndex;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return comment range if found
|
||||||
|
if ( commentStart !== -1 && commentEnd !== -1 ) {
|
||||||
|
return { start: commentStart, end: commentEnd };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default to the current offset if no comment is found
|
||||||
|
return {
|
||||||
|
start: offset,
|
||||||
|
end: offset + range.toString().length,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if a given substring within content matches an HTML comment.
|
||||||
|
*
|
||||||
|
* @param {string} content - The full content string to search.
|
||||||
|
* @param {number} start - The start index of the substring.
|
||||||
|
* @param {number} end - The end index of the substring.
|
||||||
|
* @return {boolean} - True if the substring matches an HTML comment, otherwise false.
|
||||||
|
*/
|
||||||
|
const isMatchingComment = (
|
||||||
|
content: string,
|
||||||
|
start: number,
|
||||||
|
end: number
|
||||||
|
): boolean => {
|
||||||
|
const substring = content.slice( start, end );
|
||||||
|
|
||||||
|
// Regular expression to match HTML comments
|
||||||
|
const htmlCommentRegex = /^<!--([\s\S]*?)-->$/;
|
||||||
|
|
||||||
|
return htmlCommentRegex.test( substring );
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
isMatchingComment,
|
||||||
|
getCursorPosition,
|
||||||
|
createTextToHtmlMap,
|
||||||
|
mapRichTextToValue,
|
||||||
|
};
|
@ -11,6 +11,11 @@ import { createInterpolateElement, useState, useRef } from '@wordpress/element';
|
|||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import { storeName } from '../../store';
|
import { storeName } from '../../store';
|
||||||
import { RichText } from '@wordpress/block-editor';
|
import { RichText } from '@wordpress/block-editor';
|
||||||
|
import {
|
||||||
|
createTextToHtmlMap,
|
||||||
|
getCursorPosition,
|
||||||
|
isMatchingComment,
|
||||||
|
} from '../personalization-tags/rich-text-utils';
|
||||||
|
|
||||||
const previewTextMaxLength = 150;
|
const previewTextMaxLength = 150;
|
||||||
const previewTextRecommendedLength = 80;
|
const previewTextRecommendedLength = 80;
|
||||||
@ -41,166 +46,6 @@ export function DetailsPanel() {
|
|||||||
const subjectRef = useRef( null );
|
const subjectRef = useRef( null );
|
||||||
const preheaderRef = useRef( null );
|
const preheaderRef = useRef( null );
|
||||||
|
|
||||||
// Convert `RichText` DOM offset to stored value offset
|
|
||||||
const mapRichTextToValue = ( html ) => {
|
|
||||||
const mapping = []; // Maps HTML indices to stored value indices
|
|
||||||
let htmlIndex = 0;
|
|
||||||
let valueIndex = 0;
|
|
||||||
let isInsideTag = false;
|
|
||||||
|
|
||||||
while ( htmlIndex < html.length ) {
|
|
||||||
const htmlChar = html[ htmlIndex ];
|
|
||||||
if ( htmlChar === '<' ) {
|
|
||||||
isInsideTag = true;
|
|
||||||
}
|
|
||||||
if ( htmlChar === '>' ) {
|
|
||||||
isInsideTag = false;
|
|
||||||
}
|
|
||||||
mapping[ htmlIndex ] = valueIndex;
|
|
||||||
if ( ! isInsideTag ) {
|
|
||||||
valueIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
htmlIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
return mapping;
|
|
||||||
};
|
|
||||||
|
|
||||||
const createTextToHtmlMap = ( html ) => {
|
|
||||||
const text = [];
|
|
||||||
const mapping = [];
|
|
||||||
let isInsideComment = false;
|
|
||||||
|
|
||||||
for ( let i = 0; i < html.length; i++ ) {
|
|
||||||
const char = html[ i ];
|
|
||||||
|
|
||||||
// Detect start of an HTML comment
|
|
||||||
if ( ! isInsideComment && html.slice( i, i + 4 ) === '<!--' ) {
|
|
||||||
i += 4; // Adjust loop
|
|
||||||
isInsideComment = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detect end of an HTML comment
|
|
||||||
if ( isInsideComment && html.slice( i, i + 3 ) === '-->' ) {
|
|
||||||
i += 3; // Adjust loop
|
|
||||||
isInsideComment = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
text.push( char );
|
|
||||||
mapping[ text.length - 1 ] = i;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append mapping for positions between adjacent comments
|
|
||||||
if (
|
|
||||||
mapping.length === 0 ||
|
|
||||||
mapping[ mapping.length - 1 ] !== html.length
|
|
||||||
) {
|
|
||||||
mapping[ text.length ] = html.length; // Map end of content
|
|
||||||
}
|
|
||||||
|
|
||||||
return { mapping };
|
|
||||||
};
|
|
||||||
|
|
||||||
const getCursorPosition = ( richTextRef ) => {
|
|
||||||
const selection =
|
|
||||||
richTextRef.current.ownerDocument.defaultView.getSelection();
|
|
||||||
|
|
||||||
if ( ! selection.rangeCount ) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const range = selection.getRangeAt( 0 );
|
|
||||||
const container = range.startContainer;
|
|
||||||
const currentValue =
|
|
||||||
activeRichText === 'subject'
|
|
||||||
? mailpoetEmailData?.subject ?? ''
|
|
||||||
: mailpoetEmailData?.preheader ?? '';
|
|
||||||
|
|
||||||
// Ensure the selection is within the RichText component
|
|
||||||
if ( ! richTextRef.current.contains( container ) ) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
let offset = range.startOffset; // Initial offset within the current node
|
|
||||||
let currentNode = container;
|
|
||||||
|
|
||||||
// Traverse the DOM tree to calculate the total offset
|
|
||||||
if ( currentNode !== richTextRef.current ) {
|
|
||||||
while ( currentNode && currentNode !== richTextRef.current ) {
|
|
||||||
while ( currentNode.previousSibling ) {
|
|
||||||
currentNode = currentNode.previousSibling;
|
|
||||||
offset += currentNode.textContent.length;
|
|
||||||
}
|
|
||||||
currentNode = currentNode.parentNode;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Locate the selected content in the HTML
|
|
||||||
const htmlContent = richTextRef.current.innerHTML;
|
|
||||||
const selectedText = range.toString();
|
|
||||||
const startIndex = htmlContent.indexOf( selectedText, offset );
|
|
||||||
const mapping = mapRichTextToValue( htmlContent );
|
|
||||||
|
|
||||||
// Translate `offset` from `RichText` HTML to stored value
|
|
||||||
const translatedOffset = mapping[ startIndex ] || 0;
|
|
||||||
|
|
||||||
// Search for the HTML comment in the stored value
|
|
||||||
const htmlCommentRegex = /<!--\[(.*?)\]-->/g;
|
|
||||||
let match;
|
|
||||||
let commentStart = -1;
|
|
||||||
let commentEnd = -1;
|
|
||||||
|
|
||||||
while (
|
|
||||||
( match = htmlCommentRegex.exec( currentValue ) ) !== null
|
|
||||||
) {
|
|
||||||
const [ fullMatch ] = match;
|
|
||||||
const matchStartIndex = match.index;
|
|
||||||
const matchEndIndex = matchStartIndex + fullMatch.length;
|
|
||||||
|
|
||||||
if (
|
|
||||||
translatedOffset >= matchStartIndex &&
|
|
||||||
translatedOffset <= matchEndIndex
|
|
||||||
) {
|
|
||||||
commentStart = matchStartIndex;
|
|
||||||
commentEnd = matchEndIndex;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// If a comment is detected, return its range
|
|
||||||
if ( commentStart !== -1 && commentEnd !== -1 ) {
|
|
||||||
return {
|
|
||||||
start: commentStart,
|
|
||||||
end: commentEnd,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
start: Math.min( offset, currentValue.length ),
|
|
||||||
end: Math.min(
|
|
||||||
offset + range.toString().length,
|
|
||||||
currentValue.length
|
|
||||||
),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const isMatchingComment = ( value, start, end ): boolean => {
|
|
||||||
// Extract the substring
|
|
||||||
const substring = value.slice( start, end );
|
|
||||||
|
|
||||||
// Define the regex for HTML comments
|
|
||||||
const htmlCommentRegex = /^<!--(.*?)-->$/;
|
|
||||||
|
|
||||||
// Test if the substring matches the regex
|
|
||||||
const match = htmlCommentRegex.exec( substring );
|
|
||||||
|
|
||||||
if ( match ) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleInsertPersonalizationTag = async ( value ) => {
|
const handleInsertPersonalizationTag = async ( value ) => {
|
||||||
if ( ! activeRichText || ! selectionRange ) {
|
if ( ! activeRichText || ! selectionRange ) {
|
||||||
return;
|
return;
|
||||||
@ -221,9 +66,8 @@ export function DetailsPanel() {
|
|||||||
const { mapping } = createTextToHtmlMap( currentValue );
|
const { mapping } = createTextToHtmlMap( currentValue );
|
||||||
|
|
||||||
// Ensure selection range is within bounds
|
// Ensure selection range is within bounds
|
||||||
const maxLength = mapping.length - 1; // Length of plain text
|
const start = selectionRange.start;
|
||||||
const start = Math.min( selectionRange.start, maxLength );
|
const end = selectionRange.end;
|
||||||
const end = Math.min( selectionRange.end, maxLength );
|
|
||||||
|
|
||||||
// Default values for starting and ending indexes.
|
// Default values for starting and ending indexes.
|
||||||
let htmlStart = start;
|
let htmlStart = start;
|
||||||
@ -361,15 +205,30 @@ export function DetailsPanel() {
|
|||||||
) }
|
) }
|
||||||
onFocus={ () => {
|
onFocus={ () => {
|
||||||
setActiveRichText( 'subject' );
|
setActiveRichText( 'subject' );
|
||||||
setSelectionRange( getCursorPosition( subjectRef ) );
|
setSelectionRange(
|
||||||
|
getCursorPosition(
|
||||||
|
subjectRef,
|
||||||
|
mailpoetEmailData?.subject ?? ''
|
||||||
|
)
|
||||||
|
);
|
||||||
} }
|
} }
|
||||||
onKeyUp={ () => {
|
onKeyUp={ () => {
|
||||||
setActiveRichText( 'subject' );
|
setActiveRichText( 'subject' );
|
||||||
setSelectionRange( getCursorPosition( subjectRef ) );
|
setSelectionRange(
|
||||||
|
getCursorPosition(
|
||||||
|
subjectRef,
|
||||||
|
mailpoetEmailData?.subject ?? ''
|
||||||
|
)
|
||||||
|
);
|
||||||
} }
|
} }
|
||||||
onClick={ () => {
|
onClick={ () => {
|
||||||
setActiveRichText( 'subject' );
|
setActiveRichText( 'subject' );
|
||||||
setSelectionRange( getCursorPosition( subjectRef ) );
|
setSelectionRange(
|
||||||
|
getCursorPosition(
|
||||||
|
subjectRef,
|
||||||
|
mailpoetEmailData?.subject ?? ''
|
||||||
|
)
|
||||||
|
);
|
||||||
} }
|
} }
|
||||||
onChange={ ( value ) =>
|
onChange={ ( value ) =>
|
||||||
updateEmailMailPoetProperty( 'subject', value )
|
updateEmailMailPoetProperty( 'subject', value )
|
||||||
@ -394,15 +253,30 @@ export function DetailsPanel() {
|
|||||||
) }
|
) }
|
||||||
onFocus={ () => {
|
onFocus={ () => {
|
||||||
setActiveRichText( 'preheader' );
|
setActiveRichText( 'preheader' );
|
||||||
setSelectionRange( getCursorPosition( preheaderRef ) );
|
setSelectionRange(
|
||||||
|
getCursorPosition(
|
||||||
|
preheaderRef,
|
||||||
|
mailpoetEmailData?.preheader ?? ''
|
||||||
|
)
|
||||||
|
);
|
||||||
} }
|
} }
|
||||||
onKeyUp={ () => {
|
onKeyUp={ () => {
|
||||||
setActiveRichText( 'preheader' );
|
setActiveRichText( 'preheader' );
|
||||||
setSelectionRange( getCursorPosition( preheaderRef ) );
|
setSelectionRange(
|
||||||
|
getCursorPosition(
|
||||||
|
preheaderRef,
|
||||||
|
mailpoetEmailData?.preheader ?? ''
|
||||||
|
)
|
||||||
|
);
|
||||||
} }
|
} }
|
||||||
onClick={ () => {
|
onClick={ () => {
|
||||||
setActiveRichText( 'preheader' );
|
setActiveRichText( 'preheader' );
|
||||||
setSelectionRange( getCursorPosition( preheaderRef ) );
|
setSelectionRange(
|
||||||
|
getCursorPosition(
|
||||||
|
preheaderRef,
|
||||||
|
mailpoetEmailData?.preheader ?? ''
|
||||||
|
)
|
||||||
|
);
|
||||||
} }
|
} }
|
||||||
onChange={ ( value ) =>
|
onChange={ ( value ) =>
|
||||||
updateEmailMailPoetProperty( 'preheader', value )
|
updateEmailMailPoetProperty( 'preheader', value )
|
||||||
|
Reference in New Issue
Block a user