PluginProbe ʕ •ᴥ•ʔ
GenerateBlocks / 2.0.0
GenerateBlocks v2.0.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 2 years ago index.js 1 year ago withButtonContainerLegacyMigration.js 2 years ago withButtonLegacyMigration.js 2 years ago withContainerLegacyMigration.js 2 years ago withDeviceType.js 3 years ago withDynamicTag.js 1 year ago withGridLegacyMigration.js 2 years ago withHeadlineLegacyMigration.js 2 years ago withHtmlAttributes.js 1 year ago withImageLegacyMigration.js 2 years ago withSetAttributes.js 2 years ago withStyles.js 1 year ago withUniqueId.js 2 years ago
withHtmlAttributes.js
191 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 export function withHtmlAttributes( WrappedComponent ) {
65 return ( ( props ) => {
66 const {
67 attributes,
68 setAttributes,
69 context,
70 } = props;
71
72 const {
73 htmlAttributes = {},
74 uniqueId,
75 className,
76 align,
77 } = attributes;
78
79 const isSavingPost = useSelect( ( select ) => select( 'core/editor' ).isSavingPost() );
80 const { style = '', href, ...otherAttributes } = htmlAttributes;
81 const escapedAttributes = Object.keys( otherAttributes ).reduce( ( acc, key ) => {
82 acc[ key ] = sanitizeHtmlAttribute( otherAttributes[ key ] );
83 return acc;
84 }, {} );
85 const [ processedStyle, setProcessedStyle ] = useState( style );
86
87 useEffect( () => {
88 async function fetchProcessedStyle() {
89 const styleValue = await applyFilters(
90 'generateblocks.editor.htmlAttributes.style',
91 style,
92 { ...props }
93 );
94
95 setProcessedStyle( styleValue );
96 }
97
98 fetchProcessedStyle();
99 }, [ style, context, isSavingPost ] );
100
101 useUpdateEffect( () => {
102 const layoutClasses = [ 'alignwide', 'alignfull' ];
103 const existingClasses = className?.split( ' ' ) || [];
104 const newClasses = existingClasses.filter(
105 ( existingClass ) => ! layoutClasses.includes( existingClass )
106 );
107
108 if ( align ) {
109 newClasses.push( 'align' + align );
110 }
111
112 setAttributes( { className: newClasses.join( ' ' ) } );
113 }, [ align ] );
114
115 const inlineStyleObject = typeof processedStyle === 'string'
116 ? convertInlineStyleStringToObject( processedStyle )
117 : '';
118 const combinedAttributes = {
119 ...escapedAttributes,
120 style: inlineStyleObject,
121 'data-gb-id': uniqueId,
122 'data-context-post-id': context?.postId ?? context?.[ 'generateblocks/loopIndex' ] ?? 0,
123 'data-align': align ? align : undefined,
124 };
125
126 const frontendHtmlAttributes = useMemo( () => {
127 if ( Array.isArray( htmlAttributes ) ) {
128 return {};
129 }
130
131 return htmlAttributes;
132 }, [ JSON.stringify( htmlAttributes ) ] );
133
134 useEffect( () => {
135 // Create a shallow copy of the htmlAttributes object.
136 const updatedHtmlAttributes = { ...htmlAttributes };
137
138 // Loop through the htmlAttributes object and delete those with invalid values.
139 Object.keys( updatedHtmlAttributes ).forEach( ( key ) => {
140 const isDataAttribute = key.startsWith( 'data-' );
141 const value = updatedHtmlAttributes[ key ];
142
143 // Remove non-boolean attributes if they have empty values.
144 if ( ! booleanAttributes.includes( key ) && '' === value && ! isDataAttribute ) {
145 delete updatedHtmlAttributes[ key ];
146 }
147
148 // Remove any values that are not a simple string.
149 if ( 'string' !== typeof value && 'boolean' !== typeof value ) {
150 delete updatedHtmlAttributes[ key ];
151 }
152
153 // We add the `class` attribute elsewhere.
154 if ( 'class' === key ) {
155 delete updatedHtmlAttributes[ key ];
156 }
157 } );
158
159 // Update the block's htmlAttributes if there are changes
160 if ( ! shallowEqual( updatedHtmlAttributes, htmlAttributes ) ) {
161 setAttributes( { htmlAttributes: updatedHtmlAttributes } );
162 }
163 }, [ JSON.stringify( htmlAttributes ) ] );
164
165 return (
166 <>
167 <WrappedComponent
168 { ...props }
169 editorHtmlAttributes={ combinedAttributes }
170 htmlAttributes={ frontendHtmlAttributes }
171 />
172
173 <InspectorAdvancedControls>
174 <TextControl
175 label="HTML ID"
176 value={ htmlAttributes.id }
177 onChange={ ( value ) => {
178 setAttributes( {
179 htmlAttributes: {
180 ...htmlAttributes,
181 id: value,
182 },
183 } );
184 } }
185 />
186 </InspectorAdvancedControls>
187 </>
188 );
189 } );
190 }
191