PluginProbe
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages / 3.4.3
Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages v3.4.3
3.4.3 3.4.2 3.4.1 3.4.0 3.3.9 3.3.8 3.3.7 3.3.6 3.3.5 3.3.4 3.3.3 3.3.2 3.3.1 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.2.6 2.2.7 2.2.8 2.2.9 2.3.0 2.3.1 All 196 releases
convertkit / resources / backend / js / gutenberg-block-formatters.js

gutenberg-block-formatters.js in Kit (formerly ConvertKit) – Email Newsletter, Email Marketing, Membership, Subscribers and Landing Pages 3.4.3, at resources/backend/js/gutenberg-block-formatters.js

327 lines 8.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Registers formatters in the Block Toolbar in the Gutenberg editor.
3 *
4 * @since 2.2.0
5 *
6 * @author ConvertKit
7 */
8
9 /**
10 * @typedef {import('@wordpress/element').WPElement} WPElement
11 */
12
13 // Register Gutenberg Block Toolbar formatters if the Gutenberg Editor is loaded on screen.
14 // This prevents JS errors if this script is accidentally enqueued on a non-
15 // Gutenberg editor screen, or the Classic Editor Plugin is active.
16 if (convertKitGutenbergEnabled()) {
17 // Register each ConvertKit formatter in Gutenberg.
18 for (const formatter in convertkit_block_formatters) {
19 convertKitGutenbergRegisterBlockFormatter(
20 convertkit_block_formatters[formatter]
21 );
22 }
23 }
24
25 /**
26 * Registers the given formatter in Gutenberg.
27 *
28 * @since 2.2.0
29 *
30 * @param {Object} formatter Block formatter.
31 */
32 function convertKitGutenbergRegisterBlockFormatter(formatter) {
33 (function (editor, richText, element, components) {
34 // Define the Gutenberg/React components to use.
35 const { Fragment, useState, createElement } = element;
36 const {
37 registerFormatType,
38 toggleFormat,
39 applyFormat,
40 useAnchorRef,
41 useAnchor,
42 } = richText;
43 const { RichTextToolbarButton } = editor;
44 const { Popover, SelectControl } = components;
45
46 /**
47 * Returns the icon to display in the block toolbar for this formatter, depending
48 * on the supplied formatter's configuration.
49 *
50 * @since 2.2.0
51 *
52 * @return {WPElement|string} Either a WordPress element (RawHTML) or a dashicon string.
53 */
54 const getIcon = function () {
55 // Return a fallback default icon if none is specified for this block formatter.
56 if (typeof formatter.gutenberg_icon === 'undefined') {
57 return 'dashicons-tablet';
58 }
59
60 // Return HTML element if the icon is an SVG string.
61 if (formatter.gutenberg_icon.search('svg') >= 0) {
62 return element.RawHTML({
63 children: formatter.gutenberg_icon,
64 });
65 }
66
67 // Just return the string, as it's a dashicon CSS class.
68 return formatter.gutenberg_icon;
69 };
70
71 /**
72 * Returns an object of this formatter's attributes if this formatter
73 * has been used on the selected text.
74 *
75 * Returns blank attributes if this formatter has not been used on the
76 * selected text.
77 *
78 * @since 2.2.0
79 *
80 * @param {Object} activeFormats All active formatters applied to the selected text.
81 * @return {Object} Attributes.
82 */
83 const getAttributes = function (activeFormats) {
84 // Define the attribute object.
85 const attributes = {};
86 for (const attribute in formatter.attributes) {
87 attributes[attribute] = '';
88 }
89
90 // Return if no active formats have been applied to the selected text.
91 if (typeof activeFormats === 'undefined') {
92 return attributes;
93 }
94
95 // Return if this formatter has not been used on the selected text.
96 const formats = activeFormats.filter(
97 (format) => 'convertkit/' + formatter.name === format.type
98 );
99 if (formats.length === 0) {
100 return attributes;
101 }
102
103 // This formatter has been applied to the selected text.
104 // Build the object of attributes and return.
105 for (const attribute in formatter.attributes) {
106 if (typeof formats[0].unregisteredAttributes !== 'undefined') {
107 attributes[attribute] =
108 formats[0].unregisteredAttributes[attribute];
109 } else if (typeof formats[0].attributes !== 'undefined') {
110 attributes[attribute] = formats[0].attributes[attribute];
111 }
112 }
113
114 return attributes;
115 };
116
117 /**
118 * Updates the block formatter's attributes when a field in the
119 * formatter's popover modal has its value changed.
120 *
121 * @since 2.2.0
122 *
123 * @param {Object} props Block formatter properties.
124 * @param {Object} field Field definition.
125 * @param {string} newValue New value.
126 */
127 const setAttributes = function (props, field, newValue) {
128 // Define properties and functions to use.
129 const { onChange, value } = props;
130
131 // If no value exists, remove the formatter.
132 if (newValue === '') {
133 return onChange(
134 toggleFormat(value, {
135 type: 'convertkit/' + formatter.name,
136 })
137 );
138 }
139
140 // Build object of new attributes.
141 const attributes = {};
142 for (const attribute in formatter.attributes) {
143 // If 'None' selected, blank the attribute's value.
144 if (newValue === '') {
145 attributes[attribute] = '';
146 } else {
147 attributes[attribute] = field.data[newValue][attribute];
148 }
149 }
150
151 // Apply formatter with new attributes.
152 return onChange(
153 applyFormat(value, {
154 type: 'convertkit/' + formatter.name,
155 attributes,
156 })
157 );
158 };
159
160 /**
161 * Return an array of field elements to display in the popover modal when
162 * this formatter is active.
163 *
164 * @since 2.2.0
165 *
166 * @param {Object} props Block formatter properties.
167 * @param {Function} setShowPopover Function to toggle showing/hiding the popover.
168 * @param {Object} attributes Field attributes.
169 * @return {Array} Field elements
170 */
171 const getFields = function (props, setShowPopover, attributes) {
172 // Define array of field elements.
173 const elements = [];
174
175 // Return if no fields exist.
176 if (formatter.fields.length === 0) {
177 return elements;
178 }
179
180 // Iterate through the formatter's fields, adding a field element for each.
181 for (const fieldName in formatter.fields) {
182 const field = formatter.fields[fieldName];
183
184 // Build options for <select> input.
185 const fieldOptions = [
186 {
187 label: '(None)',
188 value: '',
189 },
190 ];
191 for (const fieldValue in field.values) {
192 fieldOptions.push({
193 label: field.values[fieldValue],
194 value: fieldValue,
195 });
196 }
197
198 // Sort field's options alphabetically by label.
199 fieldOptions.sort(function (x, y) {
200 const a = x.label.toUpperCase(),
201 b = y.label.toUpperCase();
202 return a.localeCompare(b);
203 });
204
205 // Add field to array.
206 elements.push(
207 createElement(SelectControl, {
208 key: 'convertkit_' + formatter.name + '_' + fieldName,
209 id: 'convertkit-' + formatter.name + '-' + fieldName,
210 label: field.label,
211 value: attributes[fieldName],
212 help: field.description,
213 options: fieldOptions,
214 onChange(newValue) {
215 // Hide popover.
216 setShowPopover(false);
217
218 // Update and apply attributes to the selected text.
219 setAttributes(props, field, newValue);
220 },
221 })
222 );
223 }
224
225 // Return elements.
226 return elements;
227 };
228
229 /**
230 * Display modal when the formatter's button is clicked, and save
231 * changes that are made.
232 *
233 * @since 2.2.0
234 *
235 * @param {Object} props Block formatter properties.
236 * @return {Object} Block formatter button and modal elements
237 */
238 const EditFormatType = function (props) {
239 // Get props.
240 const { contentRef, isActive, value } = props;
241 const { activeFormats } = value;
242 let anchorRef;
243
244 // Get anchor reference to the text.
245 if (typeof useAnchor === 'undefined') {
246 // Use WordPress 6.0 and lower useAnchorRef(), as useAnchor() isn't available.
247 anchorRef = useAnchorRef({ ref: contentRef, value }); // eslint-disable-line react-hooks/rules-of-hooks
248 } else {
249 // Use WordPress 6.1+ useAnchor(), as useAnchorRef() is deprecated in 6.2+.
250 // eslint-disable-next-line react-hooks/rules-of-hooks
251 anchorRef = useAnchor({
252 editableContentElement: contentRef.current,
253 value,
254 });
255 }
256
257 // State to show popover.
258 const [showPopover, setShowPopover] = useState(false);
259
260 // Get attributes.
261 const attributes = getAttributes(activeFormats);
262
263 // Define fields to display in the popover modal.
264 const popoverModalElements = getFields(
265 props,
266 setShowPopover,
267 attributes
268 );
269
270 // Return block toolbar button and its modal.
271 return createElement(
272 Fragment,
273 {
274 key:
275 'convertkit_' +
276 formatter.name +
277 '_rich_text_toolbar_fragment',
278 },
279 // Register the button in the rich text toolbar.
280 createElement(RichTextToolbarButton, {
281 key:
282 'convertkit_' +
283 formatter.name +
284 '_rich_text_toolbar_button',
285 icon: getIcon(formatter),
286 title: formatter.title,
287 isActive,
288 onClick() {
289 setShowPopover(true);
290 },
291 }),
292 // Popover which displays fields when the button is active.
293 showPopover &&
294 createElement(
295 Popover,
296 {
297 key: 'convertkit_' + formatter.name + '_popover',
298 className: 'convertkit-popover',
299 anchor: anchorRef,
300 onClose() {
301 setShowPopover(false);
302 },
303 },
304 popoverModalElements
305 )
306 );
307 };
308
309 // Register Format Type.
310 registerFormatType('convertkit/' + formatter.name, {
311 title: formatter.title,
312
313 // The tagName and className combination allow Gutenberg to uniquely identify
314 // whether this formatter has been used on the selected text.
315 tagName: formatter.tag,
316 className: 'convertkit-' + formatter.name,
317 attributes: formatter.attributes,
318 edit: EditFormatType,
319 });
320 })(
321 window.wp.blockEditor,
322 window.wp.richText,
323 window.wp.element,
324 window.wp.components
325 );
326 }
327