@secretstache/wordpress-gutenberg 0.4.14 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@secretstache/wordpress-gutenberg",
3
- "version": "0.4.14",
3
+ "version": "0.5.1",
4
4
  "description": "",
5
5
  "author": "Secret Stache",
6
6
  "license": "GPL-2.0-or-later",
@@ -27,7 +27,6 @@ const Control = ({ label, max, min, value, onChange, disabled, tooltip, ...other
27
27
  max={max}
28
28
  marks={generateMarks(min, max)}
29
29
  resetFallbackValue={-1}
30
- help="Use -1 for default settings."
31
30
  renderTooltipContent={(value) => {
32
31
  if (value === -1) return 'Default';
33
32
 
@@ -20,6 +20,6 @@ export const useAllowedBlocks = (blockName, excludedBlocks) => {
20
20
  && (!blockHasAncestor || isAncestor);
21
21
  })
22
22
  ?.map((block) => block.name),
23
- [allBlocks, excludedBlocks, blockName],
23
+ [ allBlocks, excludedBlocks, blockName ],
24
24
  );
25
25
  };
@@ -1,7 +1,18 @@
1
+ import { filters } from '@wordpress/hooks';
1
2
  import apiFetch from '@wordpress/api-fetch';
2
3
  import slugify from 'slugify';
3
4
  import classNames from 'classnames';
5
+ import { select, subscribe } from '@wordpress/data';
6
+ import { getBlockType, unregisterBlockType } from '@wordpress/blocks';
4
7
 
8
+ /**
9
+ * Loads select options by fetching posts from WordPress REST API.
10
+ * @async
11
+ * @param {string} inputValue - Search term to filter posts
12
+ * @param {string} postType - WordPress post type to query
13
+ * @param {Function|null} [mapper=null] - Optional function to transform API response items
14
+ * @returns {Promise<Array<{value: number, label: string}>>} Array of select options
15
+ */
5
16
  export const loadSelectOptions = async (inputValue, postType, mapper = null) => {
6
17
  const response = await apiFetch({
7
18
  path: `/wp/v2/${postType}?search=${encodeURIComponent(inputValue)}`,
@@ -23,12 +34,22 @@ export const loadSelectOptions = async (inputValue, postType, mapper = null) =>
23
34
  }
24
35
  };
25
36
 
37
+ /**
38
+ * Converts a string to a URL-friendly slug.
39
+ * @param {string} name - String to convert to slug
40
+ * @returns {string} URL-friendly slug in lowercase with hyphens
41
+ */
26
42
  export const getSlug = (name) => slugify(name, {
27
43
  replacement: '-',
28
44
  lower: true,
29
45
  strict: true,
30
46
  });
31
47
 
48
+ /**
49
+ * Cleans SVG string by removing XML declaration and extra whitespace.
50
+ * @param {string} svgString - Raw SVG string
51
+ * @returns {string} Cleaned SVG string
52
+ */
32
53
  export const cleanSvgString = (svgString) => {
33
54
  if (svgString.startsWith('<?xml')) {
34
55
  const endOfXml = svgString.indexOf('?>');
@@ -43,9 +64,20 @@ export const cleanSvgString = (svgString) => {
43
64
  return svgString;
44
65
  };
45
66
 
46
- const SVG_MIME_TYPE = 'image/svg+xml';
47
-
67
+ /**
68
+ * Fetches and processes image data, handling both SVG and regular images.
69
+ * @async
70
+ * @param {Object} mediaData - WordPress media object
71
+ * @param {string} mediaData.mime - Media MIME type
72
+ * @param {string} mediaData.mime_type - Alternative MIME type property
73
+ * @param {string} mediaData.url - Media URL
74
+ * @param {number} mediaData.width - Image width
75
+ * @param {number} mediaData.height - Image height
76
+ * @returns {Promise<{isSvg: boolean, imageUrl: string|null, svgCode: string|null, width: number, height: number}>}
77
+ */
48
78
  export const getImage = async (mediaData) => {
79
+ const SVG_MIME_TYPE = 'image/svg+xml';
80
+
49
81
  const isSvg = mediaData?.mime === SVG_MIME_TYPE || mediaData?.mime_type === SVG_MIME_TYPE;
50
82
  const imagePayload = {
51
83
  isSvg,
@@ -64,6 +96,11 @@ export const getImage = async (mediaData) => {
64
96
  return imagePayload;
65
97
  };
66
98
 
99
+ /**
100
+ * Formats a phone number string into XXX-XXX-XXXX format.
101
+ * @param {string} phone - Phone number starting with '+1' followed by 10 digits
102
+ * @returns {string} Formatted phone number or empty string if invalid
103
+ */
67
104
  export const getPhoneNumber = (phone) => {
68
105
  if (!phone) return '';
69
106
 
@@ -79,6 +116,16 @@ export const getPhoneNumber = (phone) => {
79
116
  return formatted;
80
117
  };
81
118
 
119
+ /**
120
+ * Generates a formatted address string from location components.
121
+ * @param {Object} location - Location object
122
+ * @param {string} [location.street_number] - Street number
123
+ * @param {string} [location.street_name] - Street name
124
+ * @param {string} [location.city] - City
125
+ * @param {string} [location.state_short] - State abbreviation
126
+ * @param {string} [location.post_code] - Postal code
127
+ * @returns {string} Formatted address with HTML line breaks
128
+ */
82
129
  export const getLocationAddress = (location) => {
83
130
  const {
84
131
  street_number = '',
@@ -110,6 +157,11 @@ export const getLocationAddress = (location) => {
110
157
  return addressParts.join('');
111
158
  };
112
159
 
160
+ /**
161
+ * Decodes HTML entities to their corresponding characters.
162
+ * @param {string} text - Text containing HTML entities
163
+ * @returns {string} Decoded text
164
+ */
113
165
  export const decodeHtmlEntities = (text) => {
114
166
  const tempElement = document.createElement('div');
115
167
  tempElement.innerHTML = text;
@@ -144,3 +196,42 @@ export const getSpacingClasses = (
144
196
  [`${mobilePrefix}pb-${spacing?.mobile?.padding?.bottom}`]: spacing?.mobile?.padding?.bottom !== -1,
145
197
  });
146
198
  };
199
+
200
+ /**
201
+ * Retrieves WordPress filters by namespace.
202
+ * @param {string} namespace - Filter namespace to search for
203
+ * @returns {Array<{filterName: string, namespace: string}>} Array of matching filters
204
+ */
205
+ const getFiltersByNamespace = (namespace) => {
206
+ const list = [];
207
+
208
+ Object.entries(filters).forEach(([filterName, filterData]) => {
209
+ const handlers = filterData.handlers || [];
210
+
211
+ handlers.forEach((handler) => {
212
+ if (handler.namespace.startsWith(namespace)) {
213
+ list.push({ filterName, namespace: handler.namespace });
214
+ }
215
+ });
216
+ });
217
+
218
+ return list;
219
+ };
220
+
221
+ /**
222
+ * Unregisters a block type for a specific post type when editor loads.
223
+ * @param {string} blockName - Name of the block to unregister
224
+ * @param {string} postType - Post type to check against
225
+ */
226
+ const unsetBlockForPostType = (blockName, postType) => {
227
+ const unsubscribe = subscribe(
228
+ () => {
229
+ const currentPostType = select('core/editor').getCurrentPostType();
230
+ if (currentPostType === postType && getBlockType(blockName)) {
231
+ unregisterBlockType(blockName);
232
+ unsubscribe();
233
+ }
234
+ },
235
+ 'core/editor'
236
+ );
237
+ }
@@ -1,5 +1,5 @@
1
1
  export * from './rootBlock/index.js';
2
- export * from './waitForContainer/index.js';
2
+ export * from './rootContainer/index.js';
3
3
 
4
4
  export * from './attributes.js';
5
5
  export * from './constants.js';
@@ -1,25 +1,17 @@
1
1
  import { createBlock } from '@wordpress/blocks';
2
2
  import { dispatch } from '@wordpress/data';
3
3
 
4
- import { waitForContainer } from '../waitForContainer/index.js';
4
+ import { getRootContainer } from '../rootContainer/index.js';
5
5
 
6
- const ROOT_CONTAINER_SELECTOR = '.is-root-container';
7
- const ROOT_BLOCK_APPENDER_SELECTOR = '.is-root-container .root-block-appender';
6
+ const ROOT_BLOCK_APPENDER_SELECTOR = '.root-block-appender';
8
7
 
9
8
  /**
10
9
  * Initializes the custom button for the root appender.
10
+ * @param {Element} rootContainer - The root container of the editor.
11
11
  * @param {string} blockName - The name of the block to be created when the appender is clicked.
12
12
  * @param {string} tooltipText - The tooltip text displayed on the appender.
13
13
  */
14
- const initialize = (blockName, tooltipText) => {
15
- const rootContainer = document.querySelector(ROOT_CONTAINER_SELECTOR);
16
-
17
- if (!rootContainer) {
18
- console.error('Root container not found');
19
-
20
- return;
21
- }
22
-
14
+ const initialize = (rootContainer, blockName, tooltipText) => {
23
15
  const button = document.createElement('button');
24
16
 
25
17
  button.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" aria-hidden="true" focusable="false"><path d="M11 12.5V17.5H12.5V12.5H17.5V11H12.5V6H11V11H6V12.5H11Z"></path></svg>';
@@ -32,6 +24,8 @@ const initialize = (blockName, tooltipText) => {
32
24
  });
33
25
 
34
26
  rootContainer.prepend(button);
27
+
28
+ return !!rootContainer.querySelector(ROOT_BLOCK_APPENDER_SELECTOR);
35
29
  };
36
30
 
37
31
  /**
@@ -42,13 +36,27 @@ const initialize = (blockName, tooltipText) => {
42
36
  * @param {string} [tooltipText='Add Row'] - The tooltip text displayed on the appender.
43
37
  */
44
38
  export const setRootBlockAppender = (blockName, tooltipText = 'Add Row') => {
45
- waitForContainer(() => initialize(blockName, tooltipText), ROOT_CONTAINER_SELECTOR);
39
+ const rootContainer = getRootContainer();
40
+
41
+ if (rootContainer) {
42
+ initialize(rootContainer, blockName, tooltipText);
43
+ } else {
44
+ console.error('Root container is not found.')
45
+ }
46
46
  };
47
47
 
48
48
  export const unsetRootBlockAppender = () => {
49
- const appender = document.querySelector(ROOT_BLOCK_APPENDER_SELECTOR);
50
-
51
- if (appender) {
52
- appender.remove();
49
+ const rootContainer = getRootContainer();
50
+
51
+ if (rootContainer) {
52
+ const appender = rootContainer.querySelector(ROOT_BLOCK_APPENDER_SELECTOR);
53
+
54
+ if (appender) {
55
+ appender.remove();
56
+ } else {
57
+ console.error('Root block appender is not found.');
58
+ }
59
+ } else {
60
+ console.error('Root container is not found.')
53
61
  }
54
- }
62
+ };
@@ -1,4 +1,6 @@
1
- export * from './setRootBlock.js';
2
- export * from './setRootBlockAppender.js';
1
+ export * from './setRootBlockForPostTypes.js'
2
+ export * from './setRootBlockFilter.js'
3
+ export * from './unsetRootBlockFilter.js';
4
+ export * from './rootBlockVisibilityFilter.js';
5
+ export * from './appender.js';
3
6
  export * from './hideRootBlockForInlineInserter.js';
4
- export * from './hideRootBlockForOtherBlocks.js';
@@ -0,0 +1,35 @@
1
+ import { addFilter, removeFilter } from '@wordpress/hooks';
2
+ import { getBlockTypes } from '@wordpress/blocks';
3
+
4
+ export const rootBlockVisibilityFilter = {
5
+ add({ rootBlockName }) {
6
+ addFilter(
7
+ 'blocks.registerBlockType',
8
+ 'ssm/root-block-visibility',
9
+ (blockSettings, blockName) => {
10
+ const isRootBlock = blockName === rootBlockName;
11
+ const hasOwnAllowedBlocks = !!blockSettings?.allowedBlocks;
12
+ const hasParent = !!blockSettings?.parent;
13
+
14
+ if (isRootBlock || hasParent || hasOwnAllowedBlocks) {
15
+ return blockSettings;
16
+ }
17
+
18
+ // get all blockTypes
19
+ blockSettings.allowedBlocks = getBlockTypes()
20
+ ?.filter((allowedBlock) => {
21
+ const isRootBlock = allowedBlock.name === rootBlockName;
22
+ const hasParent = !!allowedBlock?.parent;
23
+
24
+ return !isRootBlock && !hasParent;
25
+ })
26
+ ?.map(allowedBlock => allowedBlock.name);
27
+
28
+ return blockSettings;
29
+ },
30
+ );
31
+ },
32
+ remove() {
33
+ removeFilter('blocks.registerBlockType', 'ssm/root-block-visibility');
34
+ },
35
+ };
@@ -0,0 +1,29 @@
1
+ import { addFilter, removeFilter } from '@wordpress/hooks';
2
+
3
+ /**
4
+ * Adds a filter to set the specified block as the root block by modifying block settings during registration.
5
+ * Blocks other than the root block will have their 'ancestor' property set to the root block name,
6
+ * making them only insertable within the root block.
7
+ */
8
+ export const setRootBlockFilter = {
9
+ add(rootBlockName) {
10
+ addFilter(
11
+ 'blocks.registerBlockType',
12
+ 'ssm/set-root-block',
13
+ (settings, name) => {
14
+ const isRootBlock = name === rootBlockName;
15
+ const isBaseBlock = name === 'core/block';
16
+ const hasAncestor = !!settings?.ancestor;
17
+
18
+ if (!isRootBlock && !isBaseBlock && !hasAncestor) {
19
+ settings.ancestor = [rootBlockName];
20
+ }
21
+
22
+ return settings;
23
+ },
24
+ );
25
+ },
26
+ remove() {
27
+ removeFilter('blocks.registerBlockType', 'ssm/set-root-block');
28
+ },
29
+ };
@@ -0,0 +1,73 @@
1
+ import { dispatch, select, subscribe } from '@wordpress/data';
2
+ import { setRootBlockFilter } from './setRootBlockFilter.js';
3
+ import { unsetRootBlockFilter } from './unsetRootBlockFilter.js';
4
+ import { rootBlockVisibilityFilter } from './rootBlockVisibilityFilter.js';
5
+ import { waitForRootContainer } from '../rootContainer/index.js';
6
+ import { setRootBlockAppender, unsetRootBlockAppender } from './appender.js';
7
+
8
+ /**
9
+ * Configures a root block for specific post types
10
+ *
11
+ * @param {string} rootBlockName - The name of the root block to set.
12
+ * @param {Array<string>} [postTypes=['page', 'post']] - The post types for which the root block should be enabled.
13
+ * @param {Function} [callback] - Optional callback to execute when the root block state changes.
14
+ * @param {Array<Object>} [filters=[rootBlockVisibilityFilter]] - Filters to apply or remove when enabling/disabling the root block.
15
+ * @param {boolean} [initAppender=true] - Whether to initialize the root block appender.
16
+ * @param {string} [appenderTooltipText='Add Row'] - Tooltip text for the root block appender.
17
+ */
18
+ export const setRootBlockForPostTypes = (
19
+ rootBlockName,
20
+ postTypes = ['page', 'post'],
21
+ callback,
22
+ filters = [ rootBlockVisibilityFilter ],
23
+ initAppender = true,
24
+ appenderTooltipText = 'Add Row',
25
+ ) => {
26
+ let isRootBlockEnabled = false;
27
+
28
+ waitForRootContainer().then(() => {
29
+ console.log('Root Container found.');
30
+
31
+ subscribe(() => {
32
+ const currentPostType = select('core/editor').getCurrentPostType();
33
+
34
+ if (postTypes.includes(currentPostType) && !isRootBlockEnabled) {
35
+ isRootBlockEnabled = true;
36
+
37
+ setRootBlockFilter.add(rootBlockName);
38
+ unsetRootBlockFilter.remove();
39
+
40
+ if (filters?.length > 0) {
41
+ filters.forEach((filter) => filter.add({ rootBlockName, isRootBlockEnabled, currentPostType }));
42
+ dispatch('core/blocks').reapplyBlockTypeFilters();
43
+ }
44
+
45
+ if (callback) {
46
+ callback({ isRootBlockEnabled, currentPostType });
47
+ }
48
+
49
+ if (initAppender) {
50
+ setRootBlockAppender(rootBlockName, appenderTooltipText);
51
+ }
52
+ } else if (!postTypes.includes(currentPostType) && isRootBlockEnabled) {
53
+ isRootBlockEnabled = false;
54
+
55
+ setRootBlockFilter.remove()
56
+ unsetRootBlockFilter.add(rootBlockName);
57
+
58
+ if (filters?.length > 0) {
59
+ filters.forEach((filter) => filter.remove({ rootBlockName, isRootBlockEnabled, currentPostType }));
60
+ dispatch('core/blocks').reapplyBlockTypeFilters();
61
+ }
62
+
63
+ if (callback) {
64
+ callback({ isRootBlockEnabled, currentPostType });
65
+ }
66
+
67
+ if (initAppender) {
68
+ unsetRootBlockAppender();
69
+ }
70
+ }
71
+ }, 'core/block-editor');
72
+ })
73
+ };
@@ -0,0 +1,26 @@
1
+ import { addFilter, removeFilter } from '@wordpress/hooks';
2
+
3
+ /**
4
+ * Adds a filter to unset the root block restrictions by removing the 'ancestor' property from block settings
5
+ * if it includes the specified root block name.
6
+ */
7
+ export const unsetRootBlockFilter = {
8
+ add(rootBlockName) {
9
+ addFilter(
10
+ 'blocks.registerBlockType',
11
+ 'ssm/unset-root-block',
12
+ (settings) => {
13
+ const hasRootAncestor = settings.ancestor && settings.ancestor.includes(rootBlockName);
14
+
15
+ if (hasRootAncestor) {
16
+ settings.ancestor = null;
17
+ }
18
+
19
+ return settings;
20
+ },
21
+ );
22
+ },
23
+ remove() {
24
+ removeFilter('blocks.registerBlockType', 'ssm/unset-root-block');
25
+ },
26
+ };
@@ -0,0 +1,72 @@
1
+ ## Overview
2
+
3
+ The `waitForRootContainer` function is a utility that periodically checks for the presence of the Gutenberg editor's root container, identified by the class `.is-root-container`. Once the container is found, it resolves a promise, allowing for additional initialization logic.
4
+
5
+ ## Function Signature
6
+
7
+ ```javascript
8
+ /**
9
+ * Periodically checks for the presence of the Gutenberg editor's root container and resolves when found.
10
+ *
11
+ * @param {number} [maxAttempts=10] - The maximum number of attempts to check for the root container.
12
+ * @param {number} [interval=500] - The interval time (in milliseconds) between attempts.
13
+ * @returns {Promise<Element>} - Resolves with the root container element if found, or rejects if not found after max attempts.
14
+ */
15
+ export const waitForRootContainer = (maxAttempts = 10, interval = 500);
16
+ ```
17
+
18
+ ### Parameters
19
+ - **maxAttempts** (`number`, optional): The maximum number of attempts to check for the root container. Default is `10`.
20
+ - **interval** (`number`, optional): The interval time (in milliseconds) between each attempt. Default is `500`.
21
+
22
+ ### Returns
23
+ - **Promise<Element>**: Resolves with the root container element when found. Rejects with an error if the container is not found after the maximum attempts.
24
+
25
+ ---
26
+
27
+ ## Usage Example
28
+ To use the `waitForRootContainer` function, import it into your script and handle the promise:
29
+
30
+ ```javascript
31
+ import { waitForRootContainer } from '@secretstache/wordpress-gutenberg';
32
+
33
+ waitForRootContainer(10, 500)
34
+ .then((rootContainer) => {
35
+ console.log('Gutenberg root container found:', rootContainer);
36
+ // Your initialization logic here
37
+ })
38
+ .catch((error) => {
39
+ console.error('Failed to find Gutenberg root container:', error);
40
+ });
41
+ ```
42
+
43
+ ### Example Output
44
+ In this example:
45
+ - The function checks for the Gutenberg root container (identified by `.is-root-container`) up to 10 times, waiting 500ms between each check.
46
+ - If the container is found, it resolves with the container element.
47
+ - If the container is not found after 10 attempts, it rejects with an error.
48
+
49
+ ---
50
+
51
+ ## getRootContainer
52
+
53
+ The `getRootContainer` function retrieves the Gutenberg editor's root container element from the DOM.
54
+
55
+ ### Usage Example
56
+
57
+ ```javascript
58
+ import { getRootContainer } from '@secretstache/wordpress-gutenberg';
59
+
60
+ const rootContainer = getRootContainer();
61
+ if (rootContainer) {
62
+ console.log('Gutenberg root container found:', rootContainer);
63
+ } else {
64
+ console.log('Gutenberg root container not found');
65
+ }
66
+ ```
67
+
68
+ ---
69
+
70
+ ### Notes
71
+ - The `getRootContainer` function searches both the main DOM and an iframe (if applicable) for the Gutenberg root container with the class `.is-root-container`.
72
+ - `waitForRootContainer` is built on top of `getRootContainer` and provides retry logic for situations where the root container is not immediately available.
@@ -0,0 +1,42 @@
1
+ const ROOT_CONTAINER_SELECTOR = '.is-root-container';
2
+
3
+ /**
4
+ * Retrieves the Gutenberg editor's root container element from the DOM or an iframe.
5
+ *
6
+ * @returns {Element|null} - Returns the root container element if found, or null if not found.
7
+ */
8
+ export const getRootContainer = () => {
9
+ const rootContainer = document.querySelector(ROOT_CONTAINER_SELECTOR);
10
+
11
+ if (rootContainer) {
12
+ return rootContainer;
13
+ }
14
+
15
+ const iframe = document.querySelector('.block-editor iframe');
16
+ const iframeDocument = iframe?.contentDocument || iframe?.contentWindow?.document;
17
+
18
+ return iframeDocument?.querySelector(ROOT_CONTAINER_SELECTOR) || null;
19
+ };
20
+
21
+ export const waitForRootContainer = (maxAttempts = 10, interval = 500) => {
22
+ return new Promise((resolve, reject) => {
23
+ let attempts = 0;
24
+
25
+ const checkRootContainer = () => {
26
+ const rootContainer = getRootContainer();
27
+
28
+ if (rootContainer) {
29
+ return resolve(rootContainer);
30
+ } else {
31
+ if (attempts <= maxAttempts) {
32
+ attempts++;
33
+ setTimeout(checkRootContainer, interval);
34
+ } else {
35
+ reject(new Error('Root container not found after max attempts.'));
36
+ }
37
+ }
38
+ };
39
+
40
+ checkRootContainer();
41
+ });
42
+ };