base-js-sw 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,125 @@
1
+ <h1>Base JS Components for Gutenberg Blocks</h1>
2
+
3
+ <p>This package contains two components that can be used for WordPress Gutenberg block development:</p>
4
+ <ul>
5
+ <li><strong>Link Picker</strong></li>
6
+ <li><strong>Image Picker</strong></li>
7
+ </ul>
8
+
9
+ <h2>Installation</h2>
10
+ <p>To install this package, use:</p>
11
+ <pre><code>npm install base-js</code></pre>
12
+
13
+ <h2>Usage</h2>
14
+
15
+ <h3>1. <code>LinkPicker</code> Component</h3>
16
+ <p>The <code>LinkPicker</code> component allows you to select a URL, internal post, or anchor link within your block editor. It stores the selected link as a link object and supports setting a post ID or external URL.</p>
17
+
18
+ <h4>Usage in a Gutenberg Block:</h4>
19
+ <pre><code>import { LinkPicker } from 'base-js';
20
+
21
+ registerBlockType('sw/link-block', {
22
+ edit: ({ attributes, setAttributes }) => {return ..
23
+ ```<LinkPicker
24
+ setAttributes={setAttributes}
25
+ at={attributes}
26
+ linkRef="link"
27
+ />```},
28
+ save: () => null, // handled by render.php
29
+ });</code></pre>
30
+
31
+ <p><strong>Attributes:</strong></p>
32
+ <ul>
33
+ <li><strong>setAttributes:</strong> Function to set the block's attributes</li>
34
+ <li><strong>at:</strong> The current block attributes object</li>
35
+ <li><strong>linkRef:</strong> (Optional) The reference name for the link object (default is <code>'link'</code>)</li>
36
+ </ul>
37
+
38
+ <h3>2. <code>ImagePicker</code> Component</h3>
39
+ <p>The <code>ImagePicker</code> component allows you to select and manage images in your Gutenberg blocks, with options to crop, replace, and remove images.</p>
40
+
41
+ <h4>Usage in a Gutenberg Block:</h4>
42
+ <pre><code>import { ImagePicker } from 'base-js';
43
+
44
+ registerBlockType('sw/image-block', {
45
+ edit: ({ attributes, setAttributes }) => {
46
+ return ...
47
+ ```
48
+ <ImagePicker
49
+ setAttributes={setAttributes}
50
+ imageObject={attributes.imageObject}
51
+ imageRef="imageObject"
52
+ buttonText="Select an image"
53
+ />
54
+ ```
55
+ },
56
+ save: () => null, // handled by render.php
57
+ });</code></pre>
58
+
59
+ <p><strong>Attributes:</strong></p>
60
+ <ul>
61
+ <li><strong>setAttributes:</strong> Function to set the block's attributes</li>
62
+ <li><strong>imageObject:</strong> The current image object with properties like <code>url</code>, <code>alt</code>, etc.</li>
63
+ <li><strong>imageRef:</strong> (Optional) The reference name for the image object (default is <code>'imageObject'</code>)</li>
64
+ <li><strong>buttonText:</strong> (Optional) The button label text (default is <code>'Select an image'</code>)</li>
65
+ </ul>
66
+
67
+ <h4>Block requirements</h4>
68
+ <pre><code>
69
+ {
70
+ "$schema": "https://schemas.wp.org/trunk/block.json",
71
+ "apiVersion": 2,
72
+ "name": "theme/block",
73
+ "title": "Block"
74
+ "attributes": {
75
+ "text": {
76
+ "type": "string",
77
+ "default": ""
78
+ },
79
+ "link": {
80
+ "type": "object",
81
+ "properties": {
82
+ "url": {
83
+ "type": "string",
84
+ "default": ""
85
+ },
86
+ "linkTarget": {
87
+ "type": "string",
88
+ "default": ""
89
+ },
90
+ "id": {
91
+ "type": "integer",
92
+ "default": false
93
+ }
94
+ }
95
+ },
96
+ "imageObject":{
97
+ "type": "object",
98
+ "id": {
99
+ "type": "integer",
100
+ "default": ""
101
+ },
102
+ "url": {
103
+ "type": "string",
104
+ "default": ""
105
+ },
106
+ "alt": {
107
+ "type": "string",
108
+ "default": ""
109
+ }
110
+ }
111
+ },
112
+ "editorScript": "theme-block-js",
113
+ "style": "theme-editor-styles",
114
+ "render": "file:./render.php",
115
+ "postTypes": ["all"]
116
+ }
117
+ </code></pre>
118
+
119
+ <h2>Contributing</h2>
120
+ <p>Contributions are welcome. Please submit issues or pull requests to help improve the package.</p>
121
+
122
+ <h2>License</h2>
123
+ <p>This project is licensed under the MIT License.</p>
124
+
125
+
@@ -0,0 +1,270 @@
1
+
2
+ /*
3
+ * Image Picker Component -- See params
4
+ * - Usage 1: <ImagePicker />
5
+ * - Usage 2: <ImagePickerPanelBody />
6
+ * - Usage 3: <ImagePickerPreview />
7
+ */
8
+ import { __ } from '@wordpress/i18n';
9
+ import { MediaUpload, MediaUploadCheck } from '@wordpress/blockEditor';
10
+ import { PanelBody, Button } from '@wordpress/components';
11
+ import { __experimentalAlignmentMatrixControl as AlignmentMatrixControl } from '@wordpress/components';
12
+ const { siteDomain } = themeVars; // vars passed from enqueue_backend.php
13
+
14
+ export const ImagePicker = ({ setAttributes, imageObject = {}, extraClass, imageRef = 'imageObject', buttonText = 'Select image', showCrop = true }) => {
15
+ let componentClass = 'component-image-picker';
16
+
17
+ let imageOptionsString = 'images/width=250,height=150,crop=0';
18
+ let resizedImageUrl = '';
19
+
20
+ if(imageObject.hasOwnProperty('url') && imageObject.url !== ''){
21
+ if(imageObject.url.endsWith('.svg')) {
22
+ resizedImageUrl = imageObject.url;
23
+ } else {
24
+ let imageUrlMinusBase = imageObject.url.replace( siteDomain+'/app/uploads','' ); // remove base and directory
25
+ resizedImageUrl = imageOptionsString + imageUrlMinusBase;
26
+ resizedImageUrl = siteDomain + '/' + resizedImageUrl;
27
+ }
28
+ }
29
+
30
+ return (
31
+ <div className={extraClass ? componentClass + ' ' + extraClass : componentClass}>
32
+ {imageObject.hasOwnProperty('url') && imageObject.url !== '' ? //Has image
33
+ <MediaUploadCheck>
34
+ <div className={componentClass + "__media-wrapper"}>
35
+ <img
36
+ className={componentClass + "__media"}
37
+ src={resizedImageUrl}
38
+ alt={imageObject.alt}
39
+ />
40
+ {showCrop && imageObject && typeof imageObject.crop !== 'undefined' &&
41
+ <>
42
+ <AlignmentMatrixControl
43
+ value={ imageObject.crop }
44
+ onChange={(value) => updateImageCrop(setAttributes, value, imageObject, imageRef)}
45
+ />
46
+ </>
47
+ }
48
+ </div>
49
+ <EditImageButtons
50
+ componentClass={componentClass}
51
+ setAttributes={setAttributes}
52
+ imageRef={imageRef}
53
+ replaceText='Replace'
54
+ removeText='Remove'
55
+ imageId={imageObject.id}
56
+ />
57
+ </MediaUploadCheck>
58
+ : //No image
59
+ <UploadImageButton
60
+ componentClass={componentClass}
61
+ setAttributes={setAttributes}
62
+ imageRef={imageRef}
63
+ buttonText={buttonText}
64
+ />
65
+ }
66
+
67
+
68
+
69
+ </div>
70
+ )
71
+ };
72
+
73
+ /*
74
+ * Image Picker Panel Body
75
+ * Used for Inspector Controls
76
+ */
77
+ export const ImagePickerPanelBody = ({ setAttributes, imageObject, imageRef = null, title = 'Image', showCrop = true }) => {
78
+ return(
79
+ <PanelBody title={title} initialOpen={true}>
80
+ <ImagePicker
81
+ setAttributes={setAttributes}
82
+ imageRef={imageRef}
83
+ imageObject={imageObject}
84
+ showCrop={showCrop}
85
+ />
86
+ </PanelBody>
87
+ )
88
+ }
89
+
90
+ /*
91
+ * Image Picker Preview
92
+ * Used for image inside block in the edit function
93
+ */
94
+
95
+ export const ImagePickerPreview = ({
96
+ setAttributes,
97
+ imageObject = {},
98
+ blockClass,
99
+ imageClass,
100
+ width = 500,
101
+ height = 500,
102
+ crop = true,
103
+ imageRef = 'imageObject',
104
+ buttonText = 'Select image'
105
+ }) => {
106
+
107
+ let componentClass = 'component-image-picker-preview';
108
+ let imageElementClass = '';
109
+ let imageElementWrapperClass = ''
110
+
111
+
112
+ if( imageClass ){//Image class
113
+ imageElementClass = blockClass + imageClass;
114
+ imageElementWrapperClass = blockClass + imageClass + '-wrapper';
115
+
116
+ } else if( blockClass ){//Block class
117
+ imageElementClass = blockClass + '__image';
118
+ imageElementWrapperClass = blockClass + '__image-wrapper';
119
+ }
120
+
121
+
122
+ let imagecrop = crop ? '1':'0';
123
+
124
+ if(imageObject.crop){
125
+ imagecrop = imageObject.crop.replace(/\s+/g, '-');
126
+ }
127
+
128
+ let imageOptionsString = 'images/width='+width+',height='+height+',crop='+imagecrop;
129
+ let imageOptionsStringDouble = 'images/width='+width*2+',height='+height*2+',crop='+imagecrop;
130
+ let resizedImageUrl = '';
131
+ let resizedImageUrlDouble = '';
132
+
133
+ if(imageObject.hasOwnProperty('url') && imageObject.url !== ''){
134
+
135
+ let imageUrlMinusBase = imageObject.url.replace( siteDomain+'/app/uploads','' ); // remove base and directory
136
+
137
+ if(imageObject.url.endsWith('.svg')) {
138
+ resizedImageUrl = imageObject.url;
139
+ resizedImageUrlDouble = imageObject.url;
140
+ } else {
141
+ resizedImageUrl = imageOptionsString + imageUrlMinusBase;
142
+ resizedImageUrl = siteDomain + '/' + resizedImageUrl;
143
+ resizedImageUrlDouble = imageOptionsStringDouble + imageUrlMinusBase
144
+ resizedImageUrlDouble = siteDomain + '/' + resizedImageUrlDouble;
145
+ }
146
+ } else {
147
+ componentClass += ' no-image';
148
+ }
149
+
150
+ return (
151
+ <div className={imageElementWrapperClass ? componentClass + ' ' + imageElementWrapperClass : componentClass}>
152
+ {imageObject.hasOwnProperty('url') && imageObject.url !== '' ? //Has image
153
+ <MediaUploadCheck>
154
+ <img
155
+ className={imageElementClass ? imageElementClass : ''}
156
+ src={resizedImageUrl}
157
+ srcSet={resizedImageUrl+' 1x, '+resizedImageUrlDouble+' 2x'}
158
+ alt={imageObject.alt}
159
+ />
160
+ <EditImageButtons
161
+ componentClass={componentClass}
162
+ setAttributes={setAttributes}
163
+ imageRef={imageRef}
164
+ replaceText=''
165
+ removeText=''
166
+ imageId={imageObject.id}
167
+ />
168
+ </MediaUploadCheck>
169
+ : //No image
170
+ <UploadImageButton
171
+ componentClass={componentClass}
172
+ setAttributes={setAttributes}
173
+ imageRef={imageRef}
174
+ buttonText={buttonText}
175
+ />
176
+ }
177
+ </div>
178
+ )
179
+ };
180
+
181
+ // ============= Image Controls (appear over image within editor)
182
+ export const EditImageButtons = ({ componentClass, setAttributes, replaceText, removeText, imageRef, imageId }) => {
183
+ return (
184
+ <MediaUpload
185
+ title={'Icon'}
186
+ onSelect={(media) => updateImageAttr(setAttributes, media, imageRef)}
187
+ allowedTypes={['image']}
188
+ value={imageId}
189
+ render={({ open }) => (
190
+ <div className={componentClass + "__edit-buttons"}>
191
+
192
+ <Button
193
+ className={componentClass + "__edit-button"}
194
+ isSecondary
195
+ onClick={open}
196
+ icon="images-alt2"
197
+ >
198
+ {replaceText}
199
+ </Button>
200
+
201
+ <Button
202
+ className={componentClass + "__edit-button"}
203
+ isDestructive
204
+ onClick={ (media) => {
205
+ updateImageAttr(setAttributes, false, imageRef)
206
+ }}
207
+ icon="remove"
208
+ >
209
+ {removeText}
210
+ </Button>
211
+ </div>
212
+ )}
213
+ />
214
+ )
215
+ }
216
+
217
+ export const UploadImageButton = ({ componentClass, setAttributes, imageRef, buttonText }) => {
218
+
219
+
220
+
221
+ return (
222
+ <MediaUploadCheck>
223
+ <MediaUpload
224
+ onSelect={(media) => {
225
+ updateImageAttr(setAttributes, media, imageRef)
226
+ }}
227
+ render={({ open }) => {
228
+ return (
229
+ <Button onClick={open} className={componentClass + "__upload-button"} isSecondary icon="images-alt2">
230
+ {buttonText}
231
+ </Button>
232
+ );
233
+ }}
234
+ />
235
+ </MediaUploadCheck>
236
+ )
237
+ }
238
+
239
+ const updateImageCrop = (setAttributes, crop, imageObject, imageRef) => {
240
+ imageObject.crop = crop;
241
+
242
+ setAttributes({ [imageRef]: {
243
+ id: imageObject.id,
244
+ url: imageObject.url,
245
+ alt: imageObject.alt,
246
+ crop: crop
247
+ } }); //used to update imagePickerPreview
248
+ }
249
+
250
+ const updateImageAttr = (
251
+ setAttributes,
252
+ media = false,
253
+ imageRef,
254
+ ) => {
255
+
256
+ imageRef ??= 'imageObject';
257
+
258
+ let newImageUrl = media ? media.sizes.full.url : ''
259
+
260
+
261
+ setAttributes({
262
+ [imageRef]: {
263
+ id: media.id,
264
+ url: newImageUrl,
265
+ alt: media.alt,
266
+ crop: 'center center'
267
+ },
268
+ });
269
+ };
270
+
@@ -0,0 +1,64 @@
1
+ import { __experimentalLinkControl as LinkControl } from '@wordpress/blockEditor';
2
+ import { safeDecodeURI } from '@wordpress/url';
3
+ import { useState, useEffect } from 'react';
4
+
5
+ export const LinkPicker = ({ setAttributes, at, linkRef = 'link' }) => {
6
+ // Get the current link object based on linkRef or fallback to an empty object
7
+ const linkObj = at[linkRef] || {};
8
+ const { url, linkTarget, id } = linkObj;
9
+ const opensInNewTab = linkTarget === '_blank';
10
+
11
+ // Local state to track temporary URL, ID, and link target
12
+ const [localUrl, setLocalUrl] = useState(url || '');
13
+ const [localId, setLocalId] = useState(id || '');
14
+ const [localLinkTarget, setLocalLinkTarget] = useState(opensInNewTab);
15
+
16
+ // Effect to update attributes when the local state changes
17
+ useEffect(() => {
18
+ setAttributes({
19
+ [linkRef]: {
20
+ url: localUrl ? encodeURI(safeDecodeURI(localUrl)) : '', // Ensure proper encoding for the URL if no ID
21
+ linkTarget: localLinkTarget ? '_blank' : undefined,
22
+ id: localId ? localId : undefined, // Set ID if available, else undefined
23
+ },
24
+ });
25
+ }, [localUrl, localId, localLinkTarget]); // Trigger whenever the URL, ID, or target changes
26
+
27
+ const handleLinkChange = (updatedValue) => {
28
+ console.log(updatedValue)
29
+ const { url, opensInNewTab, id, kind } = updatedValue;
30
+
31
+ // Update ID or URL depending on whether it's an internal post or an external link/anchor
32
+ if (kind === 'post-type' && id) {
33
+ // Internal post, use ID
34
+ setLocalId(id);
35
+ setLocalUrl(url);
36
+ } else {
37
+ // External or anchor link, use URL
38
+ setLocalUrl(url);
39
+ setLocalId(''); // Clear the ID since we are using the URL
40
+ }
41
+
42
+ // Update the new tab option
43
+ setLocalLinkTarget(opensInNewTab);
44
+ };
45
+
46
+ return (
47
+ <LinkControl
48
+ value={{ url: localUrl, opensInNewTab: localLinkTarget, id: localId }}
49
+ onChange={(updatedValue) => {
50
+ handleLinkChange(updatedValue);
51
+ }}
52
+ onBlur={() => {
53
+ // Save on blur to ensure the value is saved when clicking away
54
+ setAttributes({
55
+ [linkRef]: {
56
+ url: localId ? '' : localUrl ? encodeURI(safeDecodeURI(localUrl)) : '',
57
+ linkTarget: localLinkTarget ? '_blank' : undefined,
58
+ id: localId ? localId : undefined,
59
+ },
60
+ });
61
+ }}
62
+ />
63
+ );
64
+ };
package/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { LinkPicker } from './components/LinkPicker';
2
+ export { ImagePicker, ImagePickerPanelBody, ImagePickerPreview } from './components/ImagePicker';
package/package.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "base-js-sw",
3
+ "version": "1.0.0",
4
+ "description": "Reusable Gutenberg block components for WordPress projects",
5
+ "main": "index.js",
6
+ "author": "Shape Works",
7
+ "license": "MIT",
8
+ "peerDependencies": {
9
+ "@wordpress/blockEditor": "^12.0.0",
10
+ "@wordpress/i18n": "^3.0.0",
11
+ "@wordpress/url": "^4.0.0",
12
+ "@wordpress/components": "^14.0.0"
13
+ }
14
+ }
15
+