11/**
22 * WordPress dependencies
33 */
4+ import { __ } from '@wordpress/i18n' ;
45import { useState } from '@wordpress/element' ;
5- import { Icon } from '@wordpress/icons' ;
6- import { Popover , MenuItem } from '@wordpress/components' ;
76import {
8- create ,
9- insert ,
10- isCollapsed ,
11- useAnchorRef ,
12- toggleFormat ,
13- slice ,
14- getTextContent ,
7+ reset ,
8+ formatCapitalize ,
9+ formatLowercase ,
10+ formatUppercase ,
11+ } from '@wordpress/icons' ;
12+ import {
13+ Popover ,
14+ __experimentalToggleGroupControl as ToggleGroupControl ,
15+ __experimentalToggleGroupControlOptionIcon as ToggleGroupControlOptionIcon ,
16+ } from '@wordpress/components' ;
17+ import {
18+ useAnchor ,
19+ applyFormat ,
20+ removeFormat ,
1521 registerFormatType ,
1622} from '@wordpress/rich-text' ;
1723import { RichTextToolbarButton } from '@wordpress/block-editor' ;
@@ -20,137 +26,161 @@ import { RichTextToolbarButton } from '@wordpress/block-editor';
2026 * Internal dependencies
2127 */
2228import './editor.scss' ;
29+ import './style.scss' ;
2330
2431const name = 'lubus/change-case' ;
2532const title = 'Change Case' ;
2633
2734/**
28- * Icon
35+ * Main icon for toolbar button
2936 */
30- function formatIcon ( ) {
31- return (
32- < Icon
33- icon = {
34- < svg xmlns = "http://www.w3.org/2000/svg" viewBox = "0 0 24 24" >
35- < defs />
36- < path fill = "none" d = "M0 0h24v24H0V0z" />
37- < path d = "M9 4v3h5v12h3V7h5V4H9zm-6 8h3v7h3v-7h3V9H3v3z" />
38- </ svg >
39- }
40- />
41- ) ;
42- }
37+ const ChangeCaseIcon = ( ) => (
38+ < svg width = "24" height = "24" viewBox = "0 0 24 24" xmlns = "http://www.w3.org/2000/svg" >
39+ < path fill = "none" d = "M0 0h24v24H0V0z" />
40+ < path d = "M9 4v3h5v12h3V7h5V4H9zm-6 8h3v7h3v-7h3V9H3v3z" fill = "currentColor" />
41+ </ svg >
42+ ) ;
43+
44+ /**
45+ * Text transform options matching WordPress core
46+ */
47+ const TEXT_TRANSFORMS = [
48+ {
49+ label : __ ( 'None' ) ,
50+ value : 'none' ,
51+ icon : reset ,
52+ } ,
53+ {
54+ label : __ ( 'Uppercase' ) ,
55+ value : 'uppercase' ,
56+ icon : formatUppercase ,
57+ } ,
58+ {
59+ label : __ ( 'Lowercase' ) ,
60+ value : 'lowercase' ,
61+ icon : formatLowercase ,
62+ } ,
63+ {
64+ label : __ ( 'Capitalize' ) ,
65+ value : 'capitalize' ,
66+ icon : formatCapitalize ,
67+ } ,
68+ ] ;
4369
4470/**
4571 * Format edit
4672 */
4773function EditButton ( props ) {
48- const { value, onChange, onFocus, isActive, contentRef } = props ;
74+ const { value, onChange, onFocus, isActive, contentRef, activeAttributes } = props ;
4975
50- const [ isChangingCase , setIsChangingCase ] = useState ( false ) ;
76+ const [ isPopoverOpen , setIsPopoverOpen ] = useState ( false ) ;
5177
52- const anchorRef = useAnchorRef ( {
53- ref : contentRef ,
54- value,
55- settings : {
56- title,
57- } ,
78+ const anchor = useAnchor ( {
79+ editableContentElement : contentRef . current ,
80+ settings : { isActive } ,
5881 } ) ;
5982
60- function toTitleCase ( str ) {
61- const text = str
62- . match (
63- / [ A - Z ] { 2 , } (? = [ A - Z ] [ a - z ] + [ 0 - 9 ] * | \b ) | [ A - Z ] ? [ a - z ] + [ 0 - 9 ] * | [ A - Z ] | [ 0 - 9 ] + / g
64- )
65- . map ( ( x ) => x . charAt ( 0 ) . toUpperCase ( ) + x . slice ( 1 ) )
66- . join ( ' ' ) ;
67-
68- return text ;
83+ // Get the currently active case type from the class attribute
84+ const activeClass = activeAttributes ?. class || '' ;
85+ let activeCaseType = activeClass . replace ( 'has-' , '' ) . replace ( '-case' , '' ) ;
86+
87+ // Map our class names to WordPress core values
88+ if ( activeCaseType === 'titlecase' ) {
89+ activeCaseType = 'capitalize' ;
90+ }
91+ if ( ! activeCaseType ) {
92+ activeCaseType = 'none' ;
6993 }
7094
71- function onToggle ( type ) {
72- let text = getTextContent ( slice ( value ) ) ;
73-
74- switch ( type ) {
75- case 'upper' :
76- text = text . toUpperCase ( ) ;
77- break ;
78-
79- case 'lower' :
80- text = text . toLowerCase ( ) ;
81- break ;
95+ // Check if selection contains multiple words (for capitalize validation)
96+ const selectedText = value . text . slice ( value . start , value . end ) ;
97+ const hasMultipleWords = selectedText . trim ( ) . split ( / \s + / ) . length > 1 ;
8298
83- case 'title' :
84- text = toTitleCase ( text ) ;
85- break ;
99+ // Filter options based on selection
100+ const availableOptions = TEXT_TRANSFORMS . filter ( ( option ) => {
101+ // Show capitalize only for multi-word selections
102+ if ( option . value === 'capitalize' && ! hasMultipleWords ) {
103+ return false ;
86104 }
105+ return true ;
106+ } ) ;
87107
88- const toInsert = toggleFormat (
89- create ( { text } ) ,
90- {
91- type : name ,
92- } ,
93- 0 ,
94- text . length
95- ) ;
108+ function handleChange ( newValue ) {
109+ // Remove any existing case format first
110+ let newFormattedValue = removeFormat ( value , name ) ;
96111
97- onChange ( insert ( value , toInsert ) ) ;
98- }
112+ // If newValue is not 'none', apply the format
113+ if ( newValue && newValue !== 'none' ) {
114+ // Map WordPress core values to our class names
115+ let caseType = newValue ;
116+ if ( newValue === 'capitalize' ) {
117+ caseType = 'titlecase' ;
118+ }
99119
100- function onClick ( type ) {
101- if ( isCollapsed ( value ) ) {
102- return ;
120+ const className = `has-${ caseType } -case` ;
121+ newFormattedValue = applyFormat ( newFormattedValue , {
122+ type : name ,
123+ attributes : {
124+ class : className ,
125+ } ,
126+ } ) ;
103127 }
104128
105- onToggle ( type ) ;
129+ onChange ( newFormattedValue ) ;
106130 onFocus ( ) ;
107- setIsChangingCase ( false ) ;
108- }
109-
110- function openOptions ( ) {
111- setIsChangingCase ( true ) ;
112- }
113-
114- function closeOptions ( ) {
115- setIsChangingCase ( false ) ;
131+ setIsPopoverOpen ( false ) ;
116132 }
117133
118134 return (
119135 < >
120136 < RichTextToolbarButton
121- icon = { formatIcon }
137+ icon = { ChangeCaseIcon }
122138 title = { title }
123- onClick = { openOptions }
139+ onClick = { ( ) => setIsPopoverOpen ( true ) }
124140 isActive = { isActive }
125141 />
126- { isChangingCase && (
142+ { isPopoverOpen && (
127143 < Popover
128- position = "bottom center"
129- anchorRef = { anchorRef }
130- onClose = { closeOptions }
131- className = "block-editor-format-toolbar__lubus-changecase-popover"
144+ anchor = { anchor }
145+ onClose = { ( ) => setIsPopoverOpen ( false ) }
146+ placement = "bottom"
147+ shift
148+ offset = { 10 }
132149 >
133- < MenuItem onClick = { ( ) => onClick ( 'upper' ) } >
134- UPPER CASE
135- </ MenuItem >
136- < MenuItem onClick = { ( ) => onClick ( 'lower' ) } >
137- lower case
138- </ MenuItem >
139- < MenuItem onClick = { ( ) => onClick ( 'title' ) } >
140- Title Case
141- </ MenuItem >
150+ < div style = { { padding : '8px' } } >
151+ < ToggleGroupControl
152+ __next40pxDefaultSize
153+ __nextHasNoMarginBottom
154+ isBlock
155+ label = { __ ( 'Letter case' ) }
156+ value = { activeCaseType }
157+ onChange = { handleChange }
158+ >
159+ { availableOptions . map ( ( option ) => (
160+ < ToggleGroupControlOptionIcon
161+ key = { option . value }
162+ value = { option . value }
163+ icon = { option . icon }
164+ label = { option . label }
165+ />
166+ ) ) }
167+ </ ToggleGroupControl >
168+ </ div >
142169 </ Popover >
143170 ) }
144171 </ >
145172 ) ;
146173}
147174
148175/**
149- * Register Richtext Color Format.
176+ * Register Change Case Format.
150177 */
151178registerFormatType ( name , {
152179 title,
153- tagName : 'ChangeText ' ,
154- className : null ,
180+ tagName : 'span ' ,
181+ className : 'has-change-case-format' ,
155182 edit : EditButton ,
183+ attributes : {
184+ class : 'class' ,
185+ } ,
156186} ) ;
0 commit comments