PluginProbe ʕ •ᴥ•ʔ
GenerateBlocks / 2.1.0
GenerateBlocks v2.1.0
2.4.1 2.4.0 trunk 1.0 1.0.1 1.0.2 1.1.0 1.1.1 1.1.2 1.2.0 1.3.0 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.4.0 1.4.1 1.4.2 1.4.3 1.4.4 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.6.0 1.7.0 1.7.1 1.7.2 1.7.3 1.8.0 1.8.1 1.8.2 1.8.3 1.9.0 1.9.1 2.0.0 2.0.1 2.0.2 2.1.0 2.1.1 2.1.2 2.2.0 2.2.1 2.3.0
generateblocks / src / hoc / withHtmlAttributes.js
generateblocks / src / hoc Last commit date
migrations 3 years ago index.js 1 year ago withButtonContainerLegacyMigration.js 3 years ago withButtonLegacyMigration.js 3 years ago withContainerLegacyMigration.js 3 years ago withDeviceType.js 3 years ago withDynamicTag.js 1 year ago withGridLegacyMigration.js 3 years ago withHeadlineLegacyMigration.js 3 years ago withHtmlAttributes.js 1 year ago withImageLegacyMigration.js 3 years ago withSetAttributes.js 3 years ago withStyles.js 1 year ago withUniqueId.js 2 years ago
withHtmlAttributes.js
231 lines
1 import { useEffect, useMemo, useState } from '@wordpress/element';
2 import { InspectorAdvancedControls } from '@wordpress/block-editor';
3 import { TextControl } from '@wordpress/components';
4 import { useSelect } from '@wordpress/data';
5 import { applyFilters } from '@wordpress/hooks';
6
7 import { useUpdateEffect } from 'react-use';
8
9 import { convertInlineStyleStringToObject } from '@utils/convertInlineStyleStringToObject';
10 import { sanitizeHtmlAttribute } from '@utils/sanitizeHtmlAttribute';
11
12 export const booleanAttributes = [
13 'allowfullscreen',
14 'async',
15 'autofocus',
16 'autoplay',
17 'checked',
18 'controls',
19 'default',
20 'defer',
21 'disabled',
22 'download',
23 'formnovalidate',
24 'hidden',
25 'ismap',
26 'itemscope',
27 'loop',
28 'multiple',
29 'muted',
30 'nomodule',
31 'novalidate',
32 'open',
33 'readonly',
34 'required',
35 'reversed',
36 'selected',
37 ];
38
39 function shallowEqual( obj1, obj2 ) {
40 if ( obj1 === obj2 ) {
41 return true;
42 }
43
44 if ( ! obj1 || ! obj2 ) {
45 return false;
46 }
47
48 const keys1 = Object.keys( obj1 );
49 const keys2 = Object.keys( obj2 );
50
51 if ( keys1.length !== keys2.length ) {
52 return false;
53 }
54
55 for ( const key of keys1 ) {
56 if ( obj1[ key ] !== obj2[ key ] ) {
57 return false;
58 }
59 }
60
61 return true;
62 }
63
64 function sanitizeId( input ) {
65 const cleaned = input.trim()
66 .replace( /[^A-Za-z0-9-_:.]+/g, '-' )
67 .replace( /-+/g, '-' )
68 .replace( /^-|-$/g, '' );
69
70 if ( ! cleaned ) {
71 return '';
72 }
73
74 if ( /^[A-Za-z]/.test( cleaned ) ) {
75 return cleaned;
76 }
77
78 return `id-${ cleaned }`;
79 }
80
81 export function withHtmlAttributes( WrappedComponent ) {
82 return ( ( props ) => {
83 const {
84 attributes,
85 setAttributes,
86 context,
87 } = props;
88
89 const {
90 htmlAttributes = {},
91 uniqueId,
92 className,
93 align,
94 } = attributes;
95
96 const isSavingPost = useSelect( ( select ) => select( 'core/editor' ).isSavingPost() );
97 const { style = '', href, ...otherAttributes } = htmlAttributes;
98 const escapedAttributes = Object.keys( otherAttributes ).reduce( ( acc, key ) => {
99 acc[ key ] = sanitizeHtmlAttribute( otherAttributes[ key ] );
100 return acc;
101 }, {} );
102 const [ processedStyle, setProcessedStyle ] = useState( style );
103
104 useEffect( () => {
105 async function fetchProcessedStyle() {
106 const styleValue = await applyFilters(
107 'generateblocks.editor.htmlAttributes.style',
108 style,
109 { ...props }
110 );
111
112 setProcessedStyle( styleValue );
113 }
114
115 fetchProcessedStyle();
116 }, [ style, context, isSavingPost ] );
117
118 useUpdateEffect( () => {
119 const layoutClasses = [ 'alignwide', 'alignfull' ];
120 const existingClasses = className?.split( ' ' ) || [];
121 const newClasses = existingClasses.filter(
122 ( existingClass ) => ! layoutClasses.includes( existingClass )
123 );
124
125 if ( align ) {
126 newClasses.push( 'align' + align );
127 }
128
129 setAttributes( { className: newClasses.join( ' ' ) } );
130 }, [ align ] );
131
132 const inlineStyleObject = typeof processedStyle === 'string'
133 ? convertInlineStyleStringToObject( processedStyle )
134 : '';
135 const combinedAttributes = {
136 ...escapedAttributes,
137 style: inlineStyleObject,
138 'data-gb-id': uniqueId,
139 'data-context-post-id': context?.postId ?? context?.[ 'generateblocks/loopIndex' ] ?? 0,
140 'data-align': align ? align : undefined,
141 };
142
143 const frontendHtmlAttributes = useMemo( () => {
144 if ( Array.isArray( htmlAttributes ) ) {
145 return {};
146 }
147
148 return htmlAttributes;
149 }, [ JSON.stringify( htmlAttributes ) ] );
150
151 useEffect( () => {
152 // Create a shallow copy of the htmlAttributes object.
153 const updatedHtmlAttributes = { ...htmlAttributes };
154
155 // Loop through the htmlAttributes object and delete those with invalid values.
156 Object.keys( updatedHtmlAttributes ).forEach( ( key ) => {
157 const isDataAttribute = key.startsWith( 'data-' );
158 const value = updatedHtmlAttributes[ key ];
159
160 // Remove non-boolean attributes if they have empty values.
161 if ( ! booleanAttributes.includes( key ) && '' === value && ! isDataAttribute && 'alt' !== key ) {
162 delete updatedHtmlAttributes[ key ];
163 }
164
165 // Remove any values that are not a simple string.
166 if ( 'string' !== typeof value && 'boolean' !== typeof value ) {
167 delete updatedHtmlAttributes[ key ];
168 }
169
170 // We add the `class` attribute elsewhere.
171 if ( 'class' === key ) {
172 delete updatedHtmlAttributes[ key ];
173 }
174 } );
175
176 // Update the block's htmlAttributes if there are changes
177 if ( ! shallowEqual( updatedHtmlAttributes, htmlAttributes ) ) {
178 setAttributes( { htmlAttributes: updatedHtmlAttributes } );
179 }
180 }, [ JSON.stringify( htmlAttributes ) ] );
181
182 return (
183 <>
184 <WrappedComponent
185 { ...props }
186 editorHtmlAttributes={ combinedAttributes }
187 htmlAttributes={ frontendHtmlAttributes }
188 />
189
190 <InspectorAdvancedControls>
191 <TextControl
192 label="HTML ID"
193 value={ htmlAttributes.id ?? '' }
194 onChange={ ( value ) => {
195 setAttributes( {
196 htmlAttributes: {
197 ...htmlAttributes,
198 id: value,
199 },
200 } );
201 } }
202 onBlur={ () => {
203 if ( htmlAttributes.id ) {
204 setAttributes( {
205 htmlAttributes: {
206 ...htmlAttributes,
207 id: sanitizeId( htmlAttributes.id ),
208 },
209 } );
210 }
211 } }
212 />
213
214 <TextControl
215 label="ARIA Label"
216 value={ htmlAttributes[ 'aria-label' ] ?? '' }
217 onChange={ ( value ) => {
218 setAttributes( {
219 htmlAttributes: {
220 ...htmlAttributes,
221 'aria-label': value,
222 },
223 } );
224 } }
225 />
226 </InspectorAdvancedControls>
227 </>
228 );
229 } );
230 }
231