From a952b4eb84d599cd878b0d70329bcfda3f3df3b8 Mon Sep 17 00:00:00 2001 From: Siddharth Thevaril Date: Thu, 19 Oct 2023 16:54:06 +0530 Subject: [PATCH 1/5] take alternative approach --- src/index.js | 72 +++++++++++++++++++++------------------------------- 1 file changed, 29 insertions(+), 43 deletions(-) diff --git a/src/index.js b/src/index.js index 551ec8d..139fb79 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,5 @@ -import { registerFormatType, toggleFormat, insert } from '@wordpress/rich-text'; -import { Fragment } from '@wordpress/element'; +import { registerFormatType, create, insert } from '@wordpress/rich-text'; +import { Fragment, useState, useRef } from '@wordpress/element'; import { BlockControls, RichTextShortcut } from '@wordpress/block-editor'; import { Popover, ToolbarButton, ToolbarGroup } from '@wordpress/components'; import { applyFilters } from '@wordpress/hooks'; @@ -21,7 +21,6 @@ const InsertSpecialCharactersOptions = { const { name, title, character } = InsertSpecialCharactersOptions; const type = `special-characters/${ name }`; -let anchorRange; let anchorRect; /** @@ -37,29 +36,34 @@ registerFormatType( type, { * The `edit` function is called when the Character Map is selected. * * @param {Object} props Props object. - * @param {boolean} props.isActive State of popover. * @param {boolean} props.value State of popover. * @param {Function} props.onChange Event handler to detect range selection. - * @param {HTMLElement} props.contentRef The editable element. */ - edit( { isActive, value, onChange, contentRef } ) { - const onToggle = () => { - const selection = contentRef.current.ownerDocument.getSelection(); + edit( { value, onChange } ) { + const [ isPopoverActive, setIsPopoverActive ] = useState( false ); + const popoverRef = useRef( null ); + const { start, end } = value; - anchorRange = - selection.rangeCount > 0 ? selection.getRangeAt( 0 ) : null; + function insertCharacter( character ) { + const richTextCharacter = create( { + text: character, + } ); - // Pin the Popover to the caret position. - const boundingClientRect = anchorRange - ? anchorRange.getBoundingClientRect() - : null; + richTextCharacter.formats = [ value.formats.at( start ) ]; + + const modified = insert( + value, + richTextCharacter, + start, + end + ); + + onChange( modified ); + } - anchorRect = anchorRange ? () => boundingClientRect : null; - onChange( toggleFormat( value, { type } ) ); - }; const characters = applyFilters( `${ name }-characters`, Chars ); // Display the character map when it is active. - const specialCharsPopover = isActive && ( + const specialCharsPopover = isPopoverActive && ( { - onChange( toggleFormat( value, { type } ) ); - } } + ref={ popoverRef } > { - const newValue = { - ...value, - // grab the format at the start position, - // if it is undefined then use an empty array. - formats: value.formats.at( value.start ) - ? [ value.formats.at( value.start ) ] - : [], - text: char.char, - }; - - onChange( - insert( - value, - newValue, - newValue.start, - newValue.end - ) - ); + ( obj ) => { + insertCharacter( obj.char ); + setIsPopoverActive( false ); } } categoryNames={ { @@ -142,9 +128,9 @@ registerFormatType( type, { setIsPopoverActive( ! isPopoverActive ) } shortcut={ displayShortcut.primary( character ) } /> @@ -153,7 +139,7 @@ registerFormatType( type, { setIsPopoverActive( ! isPopoverActive ) } /> { specialCharsPopover } From 252522a58615ee4d8240cb2ffcb5cb9d7d1a305c Mon Sep 17 00:00:00 2001 From: Siddharth Thevaril Date: Fri, 20 Oct 2023 13:16:42 +0530 Subject: [PATCH 2/5] add faux cursor --- src/index.js | 74 ++++++++++++++++++++++++++++-- src/insert-special-characters.scss | 19 ++++++++ 2 files changed, 89 insertions(+), 4 deletions(-) diff --git a/src/index.js b/src/index.js index 139fb79..97f1122 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,5 @@ import { registerFormatType, create, insert } from '@wordpress/rich-text'; -import { Fragment, useState, useRef } from '@wordpress/element'; +import { Fragment, useState, useRef, useEffect, useMemo } from '@wordpress/element'; import { BlockControls, RichTextShortcut } from '@wordpress/block-editor'; import { Popover, ToolbarButton, ToolbarGroup } from '@wordpress/components'; import { applyFilters } from '@wordpress/hooks'; @@ -21,7 +21,6 @@ const InsertSpecialCharactersOptions = { const { name, title, character } = InsertSpecialCharactersOptions; const type = `special-characters/${ name }`; -let anchorRect; /** * Register the "Format Type" to create the character inserter. @@ -38,8 +37,9 @@ registerFormatType( type, { * @param {Object} props Props object. * @param {boolean} props.value State of popover. * @param {Function} props.onChange Event handler to detect range selection. + * @param {HTMLElement} props.contentRef The editable element. */ - edit( { value, onChange } ) { + edit( { value, onChange, contentRef } ) { const [ isPopoverActive, setIsPopoverActive ] = useState( false ); const popoverRef = useRef( null ); const { start, end } = value; @@ -61,6 +61,72 @@ registerFormatType( type, { onChange( modified ); } + /** + * Find the text node and its offset within the provided element based on an index. + * + * @param {Node} node The root node to search for the index. + * @param {number} index The index within the text content. + * @returns {Array|null} An array containing the text node and its offset, or null if not found. + */ + function findTextNodeAtIndex( node, index ) { + let currentOffset = 0; + + /** + * Recursively traverse DOM to find the text node and offset. + * + * @param {Node} node The current node. + * @returns {Array|null} Array containing the text node and its offset, or null if not found. + */ + function traverseDOM( node ) { + if ( node.nodeType === Node.TEXT_NODE ) { + const textLength = node.textContent.length; + + if ( currentOffset + textLength >= index ) { + return [ node, index - currentOffset ]; + } + + currentOffset += textLength; + } else { + for ( const childNode of node.childNodes ) { + const result = traverseDOM( childNode ); + + if ( result ) { + return result; + } + } + } + } + + return traverseDOM( node ); + } + + const memoizedPopoverRef = useMemo( () => popoverRef, [] ); + + useEffect( () => { + const fauxCursor = document.createElement( 'span' ); + + if ( contentRef.current && memoizedPopoverRef.current && isPopoverActive ) { + fauxCursor.className = 'insert-special-character__faux-caret'; + + const range = document.createRange(); + const [ textNode, offsetWithinText ] = findTextNodeAtIndex( contentRef.current, start ); + + if ( textNode ) { + range.setStart( textNode, offsetWithinText ); + range.collapse( true ); + range.insertNode( fauxCursor ); + range.setStartAfter( fauxCursor ); + range.collapse( true ); + } + } + + return () => { + if ( fauxCursor ) { + fauxCursor.remove(); + } + }; + }, [ isPopoverActive, memoizedPopoverRef ] ); + const characters = applyFilters( `${ name }-characters`, Chars ); // Display the character map when it is active. const specialCharsPopover = isPopoverActive && ( @@ -69,7 +135,7 @@ registerFormatType( type, { placement="bottom-start" focusOnMount="firstElement" key="charmap-popover" - getAnchorRect={ anchorRect } + anchor={ contentRef.current } expandOnMobile={ true } headerTitle={ __( 'Insert Special Character', diff --git a/src/insert-special-characters.scss b/src/insert-special-characters.scss index e28b820..d0ab0bc 100644 --- a/src/insert-special-characters.scss +++ b/src/insert-special-characters.scss @@ -1,5 +1,24 @@ $grid-border-color: #c8c8c8; +.insert-special-character__faux-caret { + border-left: 1px solid #000; + animation: 1s caret-blink step-end infinite; + margin-left: -1px; +} + +.insert-special-character__faux-selection { + background-color: rgba(0, 0, 0, 0.2); +} + +@keyframes caret-blink { + from, to { + border-color: black; + } + 50% { + border-color: transparent; + } +} + .character-map-popover { .components-popover__content { border: 1px solid #1e1e1e From 08e96f01e95b5908c0733b5bc31d90f7867a7d48 Mon Sep 17 00:00:00 2001 From: Siddharth Thevaril Date: Mon, 23 Oct 2023 00:55:36 +0530 Subject: [PATCH 3/5] fix eslint issues --- .eslintrc.js | 1 + src/index.js | 56 +++++++++++++++++++++++++++++++--------------------- 2 files changed, 34 insertions(+), 23 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 37a38ad..0c8fcba 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -18,5 +18,6 @@ module.exports = { "tenupIscAdminVars": "readonly", "localStorage": "readonly", "jQuery": "readonly", + "Node": "readonly", } }; diff --git a/src/index.js b/src/index.js index 97f1122..4dcd7a4 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,11 @@ import { registerFormatType, create, insert } from '@wordpress/rich-text'; -import { Fragment, useState, useRef, useEffect, useMemo } from '@wordpress/element'; +import { + Fragment, + useState, + useRef, + useEffect, + useMemo, +} from '@wordpress/element'; import { BlockControls, RichTextShortcut } from '@wordpress/block-editor'; import { Popover, ToolbarButton, ToolbarGroup } from '@wordpress/components'; import { applyFilters } from '@wordpress/hooks'; @@ -39,24 +45,19 @@ registerFormatType( type, { * @param {Function} props.onChange Event handler to detect range selection. * @param {HTMLElement} props.contentRef The editable element. */ - edit( { value, onChange, contentRef } ) { + edit: function Edit( { value, onChange, contentRef } ) { const [ isPopoverActive, setIsPopoverActive ] = useState( false ); const popoverRef = useRef( null ); const { start, end } = value; - function insertCharacter( character ) { + function insertCharacter( char ) { const richTextCharacter = create( { - text: character, + text: char, } ); richTextCharacter.formats = [ value.formats.at( start ) ]; - const modified = insert( - value, - richTextCharacter, - start, - end - ); + const modified = insert( value, richTextCharacter, start, end ); onChange( modified ); } @@ -64,9 +65,9 @@ registerFormatType( type, { /** * Find the text node and its offset within the provided element based on an index. * - * @param {Node} node The root node to search for the index. + * @param {Node} node The root node to search for the index. * @param {number} index The index within the text content. - * @returns {Array|null} An array containing the text node and its offset, or null if not found. + * @return {Array|null} An array containing the text node and its offset, or null if not found. */ function findTextNodeAtIndex( node, index ) { let currentOffset = 0; @@ -74,20 +75,20 @@ registerFormatType( type, { /** * Recursively traverse DOM to find the text node and offset. * - * @param {Node} node The current node. - * @returns {Array|null} Array containing the text node and its offset, or null if not found. + * @param {Node} __node The current node. + * @return {Array|null} Array containing the text node and its offset, or null if not found. */ - function traverseDOM( node ) { - if ( node.nodeType === Node.TEXT_NODE ) { - const textLength = node.textContent.length; + function traverseDOM( __node ) { + if ( __node.nodeType === Node.TEXT_NODE ) { + const textLength = __node.textContent.length; if ( currentOffset + textLength >= index ) { - return [ node, index - currentOffset ]; + return [ __node, index - currentOffset ]; } currentOffset += textLength; } else { - for ( const childNode of node.childNodes ) { + for ( const childNode of __node.childNodes ) { const result = traverseDOM( childNode ); if ( result ) { @@ -105,12 +106,19 @@ registerFormatType( type, { useEffect( () => { const fauxCursor = document.createElement( 'span' ); - if ( contentRef.current && memoizedPopoverRef.current && isPopoverActive ) { + if ( + contentRef.current && + memoizedPopoverRef.current && + isPopoverActive + ) { fauxCursor.className = 'insert-special-character__faux-caret'; const range = document.createRange(); - const [ textNode, offsetWithinText ] = findTextNodeAtIndex( contentRef.current, start ); - + const [ textNode, offsetWithinText ] = findTextNodeAtIndex( + contentRef.current, + start + ); + if ( textNode ) { range.setStart( textNode, offsetWithinText ); range.collapse( true ); @@ -196,7 +204,9 @@ registerFormatType( type, { icon="editor-customchar" isPressed={ isPopoverActive } label={ title } - onClick={ () => setIsPopoverActive( ! isPopoverActive ) } + onClick={ () => + setIsPopoverActive( ! isPopoverActive ) + } shortcut={ displayShortcut.primary( character ) } /> From 2ca25b991a36772fa2edc968898cb9b1b0e05c76 Mon Sep 17 00:00:00 2001 From: Siddharth Thevaril Date: Tue, 31 Oct 2023 21:10:08 +0530 Subject: [PATCH 4/5] fix block break --- src/index.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/index.js b/src/index.js index 4dcd7a4..64be309 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,4 @@ -import { registerFormatType, create, insert } from '@wordpress/rich-text'; +import { registerFormatType, create, insert, applyFormat, getActiveFormats } from '@wordpress/rich-text'; import { Fragment, useState, @@ -51,11 +51,13 @@ registerFormatType( type, { const { start, end } = value; function insertCharacter( char ) { - const richTextCharacter = create( { + let richTextCharacter = create( { text: char, } ); - richTextCharacter.formats = [ value.formats.at( start ) ]; + for ( const format of getActiveFormats( value ) ) { + richTextCharacter = applyFormat( richTextCharacter, format, 0, 1 ); + } const modified = insert( value, richTextCharacter, start, end ); From 3dbb95243b238418717dc160c753d4327baa382c Mon Sep 17 00:00:00 2001 From: Siddharth Thevaril Date: Tue, 31 Oct 2023 22:36:11 +0530 Subject: [PATCH 5/5] fix eslint --- src/index.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/index.js b/src/index.js index 64be309..550f85f 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,10 @@ -import { registerFormatType, create, insert, applyFormat, getActiveFormats } from '@wordpress/rich-text'; +import { + registerFormatType, + create, + insert, + applyFormat, + getActiveFormats, +} from '@wordpress/rich-text'; import { Fragment, useState, @@ -56,7 +62,12 @@ registerFormatType( type, { } ); for ( const format of getActiveFormats( value ) ) { - richTextCharacter = applyFormat( richTextCharacter, format, 0, 1 ); + richTextCharacter = applyFormat( + richTextCharacter, + format, + 0, + 1 + ); } const modified = insert( value, richTextCharacter, start, end );