base-js-sw 1.0.10 → 1.0.12
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/components/LinkPicker.js +382 -62
- package/package.json +1 -1
package/components/LinkPicker.js
CHANGED
|
@@ -1,9 +1,29 @@
|
|
|
1
1
|
import { __experimentalLinkControl as LinkControl } from '@wordpress/blockEditor';
|
|
2
|
-
import { Icon
|
|
2
|
+
import { Icon } from '@wordpress/components';
|
|
3
3
|
import { external, update } from '@wordpress/icons';
|
|
4
4
|
import { safeDecodeURI } from '@wordpress/url';
|
|
5
5
|
import { useState, useEffect } from 'react';
|
|
6
6
|
|
|
7
|
+
const linkedPostStatusCache = {};
|
|
8
|
+
|
|
9
|
+
const STATUSES = [
|
|
10
|
+
'draft',
|
|
11
|
+
'private',
|
|
12
|
+
'trash',
|
|
13
|
+
'pending',
|
|
14
|
+
'future',
|
|
15
|
+
'auto-draft',
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
const STATUS_MESSAGES = {
|
|
19
|
+
draft: 'Post is a draft.',
|
|
20
|
+
private: 'Post is private.',
|
|
21
|
+
trash: 'Post is in the trash.',
|
|
22
|
+
pending: 'Post is pending review.',
|
|
23
|
+
future: 'Post is scheduled.',
|
|
24
|
+
'auto-draft': 'Post is an auto draft.',
|
|
25
|
+
};
|
|
26
|
+
|
|
7
27
|
const linkPickerStyles = `
|
|
8
28
|
.base-js-link-picker {
|
|
9
29
|
width: 100%;
|
|
@@ -34,9 +54,9 @@ const linkPickerStyles = `
|
|
|
34
54
|
gap: 8px;
|
|
35
55
|
margin-top: 12px;
|
|
36
56
|
color: #757575;
|
|
37
|
-
cursor: help;
|
|
38
57
|
font-size: 13px;
|
|
39
58
|
line-height: 1.4;
|
|
59
|
+
width: 100%;
|
|
40
60
|
}
|
|
41
61
|
|
|
42
62
|
.base-js-link-picker__status svg {
|
|
@@ -45,91 +65,391 @@ const linkPickerStyles = `
|
|
|
45
65
|
fill: currentColor;
|
|
46
66
|
stroke: currentColor;
|
|
47
67
|
}
|
|
68
|
+
|
|
69
|
+
.base-js-link-picker__status-detail {
|
|
70
|
+
display: flex;
|
|
71
|
+
align-items: center;
|
|
72
|
+
gap: 8px;
|
|
73
|
+
margin-top: 8px;
|
|
74
|
+
width: 100%;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.base-js-link-picker__status-detail .components-badge {
|
|
78
|
+
max-width: 100%;
|
|
79
|
+
width: 100%;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.base-js-link-picker__status-detail .base-js-link-picker__broken-link {
|
|
83
|
+
max-width: 100%;
|
|
84
|
+
width: 100%;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.base-js-link-picker__broken-link .components-badge {
|
|
88
|
+
max-width: 100%;
|
|
89
|
+
width: 100%;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.base-js-link-picker__status-loading {
|
|
93
|
+
margin-top: 8px;
|
|
94
|
+
font-size: 13px;
|
|
95
|
+
line-height: 1.4;
|
|
96
|
+
color: #757575;
|
|
97
|
+
}
|
|
48
98
|
`;
|
|
49
99
|
|
|
50
|
-
|
|
51
|
-
|
|
100
|
+
const getResultStatus = (result) => {
|
|
101
|
+
return String(
|
|
102
|
+
result?.status ||
|
|
103
|
+
result?.post_status ||
|
|
104
|
+
result?.postStatus ||
|
|
105
|
+
''
|
|
106
|
+
).toLowerCase();
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const getPostId = (value) => {
|
|
110
|
+
if (typeof value === 'number') {
|
|
111
|
+
return Number.isInteger(value) && value > 0 ? value : '';
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (typeof value !== 'string') {
|
|
115
|
+
return '';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const trimmedValue = value.trim();
|
|
119
|
+
|
|
120
|
+
if (!/^\d+$/.test(trimmedValue)) {
|
|
121
|
+
return '';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const numericValue = Number(trimmedValue);
|
|
125
|
+
|
|
126
|
+
return Number.isSafeInteger(numericValue) && numericValue > 0
|
|
127
|
+
? numericValue
|
|
128
|
+
: '';
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const isDirectUrlValue = ({ id, kind, type, url }) => {
|
|
132
|
+
const normalizedKind = String(kind || '').toLowerCase();
|
|
133
|
+
const normalizedType = String(type || '').toLowerCase();
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
normalizedKind === 'url' ||
|
|
137
|
+
normalizedType === 'url' ||
|
|
138
|
+
(typeof id === 'string' && id === url)
|
|
139
|
+
);
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const getLinkedPostStatusMessage = (result) => {
|
|
143
|
+
const status = getResultStatus(result);
|
|
144
|
+
|
|
145
|
+
if (STATUS_MESSAGES[status]) {
|
|
146
|
+
return STATUS_MESSAGES[status];
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (result?.message) {
|
|
150
|
+
return result.message;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return 'Linked post needs review.';
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const BrokenLinkBadge = ({ message }) => {
|
|
157
|
+
return (
|
|
158
|
+
<div className="base-js-link-picker__broken-link">
|
|
159
|
+
<span className="components-badge is-warning has-icon">
|
|
160
|
+
<span className="components-badge__flex-wrapper">
|
|
161
|
+
<svg
|
|
162
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
163
|
+
viewBox="0 0 24 24"
|
|
164
|
+
width="16"
|
|
165
|
+
height="16"
|
|
166
|
+
fill="currentColor"
|
|
167
|
+
className="components-badge__icon"
|
|
168
|
+
aria-hidden="true"
|
|
169
|
+
focusable="false"
|
|
170
|
+
>
|
|
171
|
+
<path
|
|
172
|
+
fillRule="evenodd"
|
|
173
|
+
clipRule="evenodd"
|
|
174
|
+
d="M5.5 12a6.5 6.5 0 1 0 13 0 6.5 6.5 0 0 0-13 0ZM12 4a8 8 0 1 0 0 16 8 8 0 0 0 0-16Zm-.75 12v-1.5h1.5V16h-1.5Zm0-8v5h1.5V8h-1.5Z"
|
|
175
|
+
/>
|
|
176
|
+
</svg>
|
|
177
|
+
|
|
178
|
+
<span className="components-badge__content">
|
|
179
|
+
{message}
|
|
180
|
+
</span>
|
|
181
|
+
</span>
|
|
182
|
+
</span>
|
|
183
|
+
</div>
|
|
184
|
+
);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
export const LinkPicker = ({
|
|
188
|
+
setAttributes,
|
|
189
|
+
at,
|
|
190
|
+
linkRef = 'link'
|
|
191
|
+
}) => {
|
|
192
|
+
|
|
52
193
|
const linkObj = at[linkRef] || {};
|
|
53
|
-
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
194
|
+
|
|
195
|
+
const {
|
|
196
|
+
url,
|
|
197
|
+
linkTarget,
|
|
198
|
+
id
|
|
199
|
+
} = linkObj;
|
|
200
|
+
|
|
201
|
+
const opensInNewTab =
|
|
202
|
+
linkTarget === '_blank';
|
|
203
|
+
|
|
204
|
+
const initialPostId =
|
|
205
|
+
getPostId(id);
|
|
206
|
+
|
|
207
|
+
const [localUrl, setLocalUrl] =
|
|
208
|
+
useState(url || '');
|
|
209
|
+
|
|
210
|
+
const [localId, setLocalId] =
|
|
211
|
+
useState(initialPostId);
|
|
212
|
+
|
|
213
|
+
const [localLinkTarget, setLocalLinkTarget] =
|
|
214
|
+
useState(opensInNewTab);
|
|
215
|
+
|
|
216
|
+
const [linkedPostStatus, setLinkedPostStatus] =
|
|
217
|
+
useState({
|
|
218
|
+
isLoading: false,
|
|
219
|
+
result: null,
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
const postId =
|
|
223
|
+
getPostId(localId);
|
|
224
|
+
|
|
225
|
+
const isInternalPost =
|
|
226
|
+
Boolean(postId);
|
|
227
|
+
|
|
228
|
+
const hasLinkData =
|
|
229
|
+
Boolean(localUrl || postId);
|
|
230
|
+
|
|
231
|
+
const statusLabel =
|
|
232
|
+
isInternalPost
|
|
233
|
+
? 'Dynamic WordPress link'
|
|
234
|
+
: 'External link';
|
|
235
|
+
|
|
236
|
+
const currentStatus =
|
|
237
|
+
linkedPostStatus.result
|
|
238
|
+
? getResultStatus(linkedPostStatus.result)
|
|
239
|
+
: '';
|
|
240
|
+
|
|
241
|
+
const brokenLinkMessage =
|
|
242
|
+
linkedPostStatus.result
|
|
243
|
+
? getLinkedPostStatusMessage(linkedPostStatus.result)
|
|
244
|
+
: '';
|
|
245
|
+
|
|
246
|
+
const shouldShowBrokenLinkStatus =
|
|
247
|
+
isInternalPost
|
|
248
|
+
&& linkedPostStatus.result
|
|
249
|
+
&& STATUSES.includes(currentStatus);
|
|
250
|
+
|
|
68
251
|
useEffect(() => {
|
|
252
|
+
|
|
253
|
+
const numericPostId =
|
|
254
|
+
getPostId(localId);
|
|
255
|
+
|
|
256
|
+
const hasValidPostId =
|
|
257
|
+
Boolean(numericPostId);
|
|
258
|
+
|
|
259
|
+
if (!hasValidPostId) {
|
|
260
|
+
|
|
261
|
+
setLinkedPostStatus({
|
|
262
|
+
isLoading: false,
|
|
263
|
+
result: null,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
return;
|
|
267
|
+
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (linkedPostStatusCache[numericPostId]) {
|
|
271
|
+
|
|
272
|
+
setLinkedPostStatus({
|
|
273
|
+
isLoading: false,
|
|
274
|
+
result: linkedPostStatusCache[numericPostId],
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
return;
|
|
278
|
+
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const apiFetch =
|
|
282
|
+
window.wp?.apiFetch;
|
|
283
|
+
|
|
284
|
+
if (!apiFetch) {
|
|
285
|
+
|
|
286
|
+
setLinkedPostStatus({
|
|
287
|
+
isLoading: false,
|
|
288
|
+
result: {
|
|
289
|
+
status: 'draft',
|
|
290
|
+
message: 'Could not check this post.',
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
return;
|
|
295
|
+
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
let isMounted = true;
|
|
299
|
+
|
|
300
|
+
setLinkedPostStatus({
|
|
301
|
+
isLoading: true,
|
|
302
|
+
result: null,
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
apiFetch({
|
|
306
|
+
path: `/theme/v1/linked-post-validator/post/${numericPostId}`,
|
|
307
|
+
})
|
|
308
|
+
.then((result) => {
|
|
309
|
+
|
|
310
|
+
if (!isMounted) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
linkedPostStatusCache[numericPostId] =
|
|
315
|
+
result;
|
|
316
|
+
|
|
317
|
+
setLinkedPostStatus({
|
|
318
|
+
isLoading: false,
|
|
319
|
+
result,
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
})
|
|
323
|
+
.catch(() => {
|
|
324
|
+
|
|
325
|
+
if (!isMounted) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const errorResult = {
|
|
330
|
+
status: 'draft',
|
|
331
|
+
message: 'Could not check this post.',
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
setLinkedPostStatus({
|
|
335
|
+
isLoading: false,
|
|
336
|
+
result: errorResult,
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
return () => {
|
|
342
|
+
isMounted = false;
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
}, [localId]);
|
|
346
|
+
|
|
347
|
+
const handleLinkChange = (updatedValue = {}) => {
|
|
348
|
+
|
|
349
|
+
const {
|
|
350
|
+
url,
|
|
351
|
+
opensInNewTab,
|
|
352
|
+
id,
|
|
353
|
+
kind,
|
|
354
|
+
type,
|
|
355
|
+
} = updatedValue;
|
|
356
|
+
|
|
357
|
+
const nextUrl =
|
|
358
|
+
url || '';
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* When manually typing,
|
|
362
|
+
* Gutenberg keeps previous selected id.
|
|
363
|
+
*
|
|
364
|
+
* Manual typing has no kind/type.
|
|
365
|
+
*/
|
|
366
|
+
const isManualInput =
|
|
367
|
+
!kind
|
|
368
|
+
&& !type;
|
|
369
|
+
|
|
370
|
+
const nextPostId =
|
|
371
|
+
isManualInput
|
|
372
|
+
? undefined
|
|
373
|
+
: getPostId(id);
|
|
374
|
+
|
|
375
|
+
setLocalUrl(nextUrl);
|
|
376
|
+
|
|
377
|
+
setLocalId(
|
|
378
|
+
nextPostId || ''
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
setLocalLinkTarget(
|
|
382
|
+
Boolean(opensInNewTab)
|
|
383
|
+
);
|
|
384
|
+
|
|
69
385
|
setAttributes({
|
|
70
386
|
[linkRef]: {
|
|
71
|
-
url:
|
|
72
|
-
|
|
73
|
-
|
|
387
|
+
url: nextUrl
|
|
388
|
+
? encodeURI(safeDecodeURI(nextUrl))
|
|
389
|
+
: '',
|
|
390
|
+
linkTarget: opensInNewTab
|
|
391
|
+
? '_blank'
|
|
392
|
+
: undefined,
|
|
393
|
+
id: nextPostId || undefined,
|
|
74
394
|
},
|
|
75
395
|
});
|
|
76
|
-
}, [localUrl, localId, localLinkTarget]); // Trigger whenever the URL, ID, or target changes
|
|
77
|
-
|
|
78
|
-
const handleLinkChange = (updatedValue) => {
|
|
79
|
-
const { url, opensInNewTab, id } = updatedValue;
|
|
80
|
-
const nextUrl = url || '';
|
|
81
|
-
const isPostSelection = Boolean(id);
|
|
82
|
-
|
|
83
|
-
// Update ID or URL depending on whether it's an internal post or an external link/anchor
|
|
84
|
-
if (isPostSelection) {
|
|
85
|
-
// Internal post, use ID
|
|
86
|
-
setLocalId(id);
|
|
87
|
-
setLocalUrl(nextUrl);
|
|
88
|
-
} else {
|
|
89
|
-
// External or anchor link, use URL
|
|
90
|
-
setLocalUrl(nextUrl);
|
|
91
|
-
setLocalId(''); // Clear the ID since we are using the URL
|
|
92
|
-
}
|
|
93
396
|
|
|
94
|
-
// Update the new tab option
|
|
95
|
-
setLocalLinkTarget(Boolean(opensInNewTab));
|
|
96
397
|
};
|
|
97
398
|
|
|
98
399
|
return (
|
|
99
400
|
<div className="base-js-link-picker">
|
|
401
|
+
|
|
100
402
|
<style>{linkPickerStyles}</style>
|
|
403
|
+
|
|
101
404
|
<div className="base-js-link-picker__control">
|
|
405
|
+
|
|
102
406
|
<LinkControl
|
|
103
|
-
value={{
|
|
407
|
+
value={{
|
|
408
|
+
url: localUrl,
|
|
409
|
+
opensInNewTab: localLinkTarget,
|
|
410
|
+
id: postId || undefined,
|
|
411
|
+
}}
|
|
104
412
|
onChange={(updatedValue) => {
|
|
105
413
|
handleLinkChange(updatedValue);
|
|
106
414
|
}}
|
|
107
|
-
onBlur={() => {
|
|
108
|
-
// Save on blur to ensure the value is saved when clicking away
|
|
109
|
-
setAttributes({
|
|
110
|
-
[linkRef]: {
|
|
111
|
-
url: localId ? '' : localUrl ? encodeURI(safeDecodeURI(localUrl)) : '',
|
|
112
|
-
linkTarget: localLinkTarget ? '_blank' : undefined,
|
|
113
|
-
id: localId ? localId : undefined,
|
|
114
|
-
},
|
|
115
|
-
});
|
|
116
|
-
}}
|
|
117
415
|
/>
|
|
416
|
+
|
|
118
417
|
</div>
|
|
418
|
+
|
|
119
419
|
{hasLinkData && (
|
|
120
420
|
<div className="base-js-link-picker__status-wrapper">
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
421
|
+
|
|
422
|
+
<div className="base-js-link-picker__status">
|
|
423
|
+
|
|
424
|
+
<Icon
|
|
425
|
+
icon={isInternalPost ? update : external}
|
|
426
|
+
size={18}
|
|
427
|
+
/>
|
|
428
|
+
|
|
429
|
+
<span>{statusLabel}</span>
|
|
430
|
+
|
|
431
|
+
</div>
|
|
432
|
+
|
|
433
|
+
{isInternalPost && linkedPostStatus.isLoading && (
|
|
434
|
+
<div className="base-js-link-picker__status-loading">
|
|
435
|
+
Checking linked post status…
|
|
129
436
|
</div>
|
|
130
|
-
|
|
437
|
+
)}
|
|
438
|
+
|
|
439
|
+
{shouldShowBrokenLinkStatus && (
|
|
440
|
+
<div className="base-js-link-picker__status-detail">
|
|
441
|
+
|
|
442
|
+
<BrokenLinkBadge
|
|
443
|
+
message={brokenLinkMessage}
|
|
444
|
+
/>
|
|
445
|
+
|
|
446
|
+
</div>
|
|
447
|
+
)}
|
|
448
|
+
|
|
131
449
|
</div>
|
|
132
450
|
)}
|
|
451
|
+
|
|
133
452
|
</div>
|
|
134
453
|
);
|
|
135
|
-
|
|
454
|
+
|
|
455
|
+
};
|