PluginProbe
PatternsWP – Gutenberg Block Patterns & Page Templates Library / 1.1.0
PatternsWP – Gutenberg Block Patterns & Page Templates Library v1.1.0
1.1.0 trunk 1.0.0 1.0.1 1.0.10 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.0.9
patternswp / assets / js / patternswp-editor.js

patternswp-editor.js in PatternsWP – Gutenberg Block Patterns & Page Templates Library 1.1.0, at assets/js/patternswp-editor.js

3,521 lines 97.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * PatternsWP — Block Editor Patterns Library
3 *
4 * Ready-to-use script using WordPress / Gutenberg packages (no build step).
5 * Text domain: patternswp
6 */
7 ( function ( wp, lodash ) {
8 'use strict';
9
10 if ( ! wp || ! wp.element || ! wp.components ) {
11 return;
12 }
13
14 var el = wp.element.createElement;
15 var Fragment = wp.element.Fragment;
16 var useState = wp.element.useState;
17 var useEffect = wp.element.useEffect;
18 var useRef = wp.element.useRef;
19 var useCallback = wp.element.useCallback;
20 var useMemo = wp.element.useMemo;
21 var createRoot = wp.element.createRoot;
22 var render = wp.element.render;
23
24 var __ = wp.i18n.__;
25 var sprintf = wp.i18n.sprintf;
26 var _n = wp.i18n._n;
27
28 var useDispatch = wp.data.useDispatch;
29 var useSelect = wp.data.useSelect;
30 var subscribe = wp.data.subscribe;
31
32 var useDebounce = wp.compose.useDebounce;
33
34 var parse = wp.blocks.parse;
35 var cloneBlock = wp.blocks.cloneBlock;
36 var isUnmodifiedDefaultBlock = wp.blocks.isUnmodifiedDefaultBlock;
37 var registerBlockType = wp.blocks.registerBlockType;
38
39 var BlockPreview = wp.blockEditor.BlockPreview;
40 var storeBlockEditor = wp.blockEditor.store;
41
42 var speak = wp.a11y.speak;
43
44 var castArray = lodash.castArray;
45 var isEmpty = lodash.isEmpty;
46
47 var registerPlugin = wp.plugins && wp.plugins.registerPlugin;
48 var PluginMoreMenuItem =
49 ( wp.editor && wp.editor.PluginMoreMenuItem ) ||
50 ( wp.editPost && wp.editPost.PluginMoreMenuItem );
51
52 var c = wp.components;
53 var Modal = c.Modal;
54 var Button = c.Button;
55 var SearchControl = c.SearchControl;
56 var Spinner = c.Spinner;
57 var DropdownMenu = c.DropdownMenu;
58 var MenuGroup = c.MenuGroup;
59 var MenuItem = c.MenuItem;
60 var Tooltip = c.Tooltip;
61 var Notice = c.Notice;
62 var Icon = c.Icon;
63 var CheckboxControl = c.CheckboxControl;
64 var Dropdown = c.Dropdown;
65 var SelectControl = c.SelectControl;
66 var ButtonGroup = c.ButtonGroup;
67 var Fill = c.Fill;
68
69 var SVG = wp.primitives && wp.primitives.SVG;
70 var Path = wp.primitives && wp.primitives.Path;
71 var Circle = wp.primitives && wp.primitives.Circle;
72
73 /* IconScout Unicons (thinline + line) — https://iconscout.com/unicons */
74 var icons = ( function () {
75 function svgIcon( children ) {
76 if ( ! SVG ) {
77 return null;
78 }
79 return el(
80 SVG,
81 {
82 xmlns: 'http://www.w3.org/2000/svg',
83 viewBox: '0 0 24 24',
84 width: 18,
85 height: 18,
86 fill: 'currentColor',
87 'aria-hidden': true,
88 focusable: false,
89 },
90 children
91 );
92 }
93
94 function uicon( pathD ) {
95 return svgIcon(
96 el( Path, {
97 d: pathD,
98 fill: 'currentColor',
99 } )
100 );
101 }
102
103 function uiconPaths( paths ) {
104 return svgIcon(
105 el(
106 Fragment,
107 null,
108 paths.map( function ( pathD, index ) {
109 return el( Path, {
110 key: index,
111 d: pathD,
112 fill: 'currentColor',
113 } );
114 } )
115 )
116 );
117 }
118
119 return {
120 heart: uicon(
121 'M17.5.917a6.4,6.4,0,0,0-5.5,3.3A6.4,6.4,0,0,0,6.5.917,6.8,6.8,0,0,0,0,7.967c0,6.775,10.956,14.6,11.422,14.932l.578.409.578-.409C13.044,22.569,24,14.742,24,7.967A6.8,6.8,0,0,0,17.5.917ZM12,20.846c-3.253-2.43-10-8.4-10-12.879a4.8,4.8,0,0,1,4.5-5.05A4.8,4.8,0,0,1,11,7.967h2a4.8,4.8,0,0,1,4.5-5.05A4.8,4.8,0,0,1,22,7.967C22,12.448,15.253,18.416,12,20.846Z'
122 ),
123 heartFilled: uicon(
124 'M17.5.917a6.4,6.4,0,0,0-5.5,3.3A6.4,6.4,0,0,0,6.5.917,6.8,6.8,0,0,0,0,7.967c0,6.775,10.956,14.6,11.422,14.932l.578.409.578-.409C13.044,22.569,24,14.742,24,7.967A6.8,6.8,0,0,0,17.5.917Z'
125 ),
126 moreVertical: svgIcon(
127 el(
128 Fragment,
129 null,
130 Circle
131 ? el( Circle, { cx: 12, cy: 2, r: 2, fill: 'currentColor' } )
132 : el( 'circle', { cx: 12, cy: 2, r: 2, fill: 'currentColor' } ),
133 Circle
134 ? el( Circle, { cx: 12, cy: 12, r: 2, fill: 'currentColor' } )
135 : el( 'circle', { cx: 12, cy: 12, r: 2, fill: 'currentColor' } ),
136 Circle
137 ? el( Circle, { cx: 12, cy: 22, r: 2, fill: 'currentColor' } )
138 : el( 'circle', { cx: 12, cy: 22, r: 2, fill: 'currentColor' } )
139 )
140 ),
141 edit: uiconPaths( [
142 'M5,19H9.414L23.057,5.357a3.125,3.125,0,0,0,0-4.414,3.194,3.194,0,0,0-4.414,0L5,14.586Zm2-3.586L20.057,2.357a1.148,1.148,0,0,1,1.586,0,1.123,1.123,0,0,1,0,1.586L8.586,17H7Z',
143 'M23.621,7.622,22,9.243V16H16v6H2V3A1,1,0,0,1,3,2H14.758L16.379.379A5.013,5.013,0,0,1,16.84,0H3A3,3,0,0,0,0,3V24H18.414L24,18.414V7.161A5.15,5.15,0,0,1,23.621,7.622ZM18,21.586V18h3.586Z',
144 ] ),
145 sort: uicon(
146 'M16.29,14.29,12,18.59l-4.29-4.3a1,1,0,0,0-1.42,1.42l5,5a1,1,0,0,0,1.42,0l5-5a1,1,0,0,0-1.42-1.42ZM7.71,9.71,12,5.41l4.29,4.3a1,1,0,0,0,1.42,0,1,1,0,0,0,0-1.42l-5-5a1,1,0,0,0-1.42,0l-5,5A1,1,0,0,0,7.71,9.71Z'
147 ),
148 filter: uicon(
149 'm15 24-6-4.5v-5.12l-8-9v-2.38a3 3 0 0 1 3-3h16a3 3 0 0 1 3 3v2.38l-8 9zm-4-5.5 2 1.5v-6.38l8-9v-1.62a1 1 0 0 0 -1-1h-16a1 1 0 0 0 -1 1v1.62l8 9z'
150 ),
151 grid: uiconPaths( [
152 'M11,11H0V3A3,3,0,0,1,3,0h8ZM2,9H9V2H3A1,1,0,0,0,2,3Z',
153 'M24,11H13V0h8a3,3,0,0,1,3,3ZM15,9h7V3a1,1,0,0,0-1-1H15Z',
154 'M11,24H3a3,3,0,0,1-3-3V13H11ZM2,15v6a1,1,0,0,0,1,1H9V15Z',
155 'M21,24H13V13H24v8A3,3,0,0,1,21,24Zm-6-2h6a1,1,0,0,0,1-1V15H15Z',
156 ] ),
157 list: svgIcon(
158 el(
159 Fragment,
160 null,
161 el( 'rect', { x: 7, y: 4, width: 17, height: 2, fill: 'currentColor' } ),
162 el( 'rect', { x: 7, y: 11, width: 17, height: 2, fill: 'currentColor' } ),
163 el( 'rect', { x: 7, y: 18, width: 17, height: 2, fill: 'currentColor' } ),
164 Circle
165 ? el( Circle, { cx: 2, cy: 5, r: 2, fill: 'currentColor' } )
166 : el( 'circle', { cx: 2, cy: 5, r: 2, fill: 'currentColor' } ),
167 Circle
168 ? el( Circle, { cx: 2, cy: 12, r: 2, fill: 'currentColor' } )
169 : el( 'circle', { cx: 2, cy: 12, r: 2, fill: 'currentColor' } ),
170 Circle
171 ? el( Circle, { cx: 2, cy: 19, r: 2, fill: 'currentColor' } )
172 : el( 'circle', { cx: 2, cy: 19, r: 2, fill: 'currentColor' } )
173 )
174 ),
175 plus: svgIcon(
176 el( 'polygon', {
177 points:
178 '24 11 13 11 13 0 11 0 11 11 0 11 0 13 11 13 11 24 13 24 13 13 24 13 24 11',
179 fill: 'currentColor',
180 } )
181 ),
182 lock: svgIcon(
183 el(
184 Fragment,
185 null,
186 el( Path, {
187 d: 'M19,8V7A7,7,0,0,0,5,7V8H2V21a3,3,0,0,0,3,3H19a3,3,0,0,0,3-3V8ZM7,7A5,5,0,0,1,17,7V8H7ZM20,21a1,1,0,0,1-1,1H5a1,1,0,0,1-1-1V10H20Z',
188 fill: 'currentColor',
189 } ),
190 el( 'rect', {
191 x: 11,
192 y: 14,
193 width: 2,
194 height: 4,
195 fill: 'currentColor',
196 } )
197 )
198 ),
199 check: uicon(
200 'M18.3534546,7.5735474c-0.1932983-0.1972046-0.5098267-0.2003784-0.7070312-0.0070801l-7.8066406,7.8066406l-3.4863281-3.4863281c-0.194397-0.1905518-0.5054321-0.1905518-0.6998291,0c-0.1972046,0.1932373-0.2003784,0.5097656-0.0071411,0.7069702l3.8398438,3.8398438c0.0936279,0.0939331,0.2208862,0.1466675,0.3535156,0.1464844c0.1326294,0.0001221,0.2598267-0.0526123,0.3534546-0.1464844l8.1601562-8.1601562C18.5440063,8.0790405,18.5440063,7.7679443,18.3534546,7.5735474z'
201 ),
202 refresh: uicon(
203 'M12,2A10,10,0,0,0,4.93,4.93,1,1,0,0,0,6.34,6.34,8,8,0,1,1,4,12H7L3,8,0,12H2A10,10,0,1,0,12,2Z'
204 ),
205 plusBox: uicon(
206 'M9,13h2v2a1,1,0,0,0,2,0V13h2a1,1,0,0,0,0-2H13V9a1,1,0,0,0-2,0v2H9a1,1,0,0,0,0,2ZM21,2H3A1,1,0,0,0,2,3V21a1,1,0,0,0,1,1H21a1,1,0,0,0,1-1V3A1,1,0,0,0,21,2ZM20,20H4V4H20Z'
207 ),
208 close: uicon(
209 'M19.8534546,19.1465454L12.7069092,12l7.1465454-7.1465454c0.1871948-0.1937256,0.1871948-0.5009155,0-0.6947021c-0.1918335-0.1986084-0.5083618-0.2041016-0.7069702-0.0122681l-7.1465454,7.1465454L4.8534546,4.1465454c-0.1937256-0.1871338-0.5009155-0.1871338-0.6947021,0C3.960144,4.3383789,3.9546509,4.6549072,4.1464844,4.8535156L11.2929688,12l-7.1464844,7.1464844c-0.09375,0.09375-0.1464233,0.2208862-0.1464233,0.3534546C4,19.776062,4.223877,19.999939,4.5,20c0.1326294,0.0001221,0.2598267-0.0526123,0.3534546-0.1465454l7.1464844-7.1464844l7.1465454,7.1465454C19.2401123,19.9474487,19.3673706,20.0001831,19.5,20c0.1325073-0.000061,0.2595825-0.0526733,0.3533325-0.1463623C20.048645,19.6583862,20.0487061,19.3417969,19.8534546,19.1465454z'
210 ),
211 brand: svgIcon(
212 el(
213 Fragment,
214 null,
215 el( Path, { d: 'M9.34 24L4.45 21.16L4.03 20.92V15.3L9.34 12V24Z', fill: 'currentColor' } ),
216 el( Path, {
217 fillRule: 'evenodd',
218 clipRule: 'evenodd',
219 d: 'M14.66 9.2L20.97 6L14.66 2.8V9.2Z',
220 fill: 'currentColor',
221 } ),
222 el( Path, { d: 'M14.66 2.8L9.34 6L4.03 9.2V2.8L9.34 0L14.66 2.8Z', fill: 'currentColor' } ),
223 el( Path, {
224 fillRule: 'evenodd',
225 clipRule: 'evenodd',
226 d: 'M10.79 11.44V17.84L14.66 15.3L20.97 12V6L14.66 9.2L10.79 11.44Z',
227 fill: 'currentColor',
228 } )
229 )
230 ),
231 settings: uicon(
232 'M15,24H9V20.487a9,9,0,0,1-2.849-1.646L3.107,20.6l-3-5.2L3.15,13.645a9.1,9.1,0,0,1,0-3.29L.107,8.6l3-5.2L6.151,5.159A9,9,0,0,1,9,3.513V0h6V3.513a9,9,0,0,1,2.849,1.646L20.893,3.4l3,5.2L20.85,10.355a9.1,9.1,0,0,1,0,3.29L23.893,15.4l-3,5.2-3.044-1.758A9,9,0,0,1,15,20.487Zm-4-2h2V18.973l.751-.194A6.984,6.984,0,0,0,16.994,16.9l.543-.553,2.623,1.515,1-1.732-2.62-1.513.206-.746a7.048,7.048,0,0,0,0-3.75l-.206-.746,2.62-1.513-1-1.732L17.537,7.649,16.994,7.1a6.984,6.984,0,0,0-3.243-1.875L13,5.027V2H11V5.027l-.751.194A6.984,6.984,0,0,0,7.006,7.1l-.543.553L3.84,6.134l-1,1.732L5.46,9.379l-.206.746a7.048,7.048,0,0,0,0,3.75l.206.746L2.84,16.134l1,1.732,2.623-1.515.543.553a6.984,6.984,0,0,0,3.243,1.875l.751.194Zm1-6a4,4,0,1,1,4-4A4,4,0,0,1,12,16Zm0-6a2,2,0,1,0,2,2A2,2,0,0,0,12,10Z'
233 ),
234 arrowUp: uicon(
235 'M17.71,11.29l-5-5a1,1,0,0,0-.33-.21,1,1,0,0,0-.76,0,1,1,0,0,0-.33.21l-5,5a1,1,0,0,0,1.42,1.42L11,9.41V17a1,1,0,0,0,2,0V9.41l3.29,3.3a1,1,0,0,0,1.42,0A1,1,0,0,0,17.71,11.29Z'
236 ),
237 arrowDown: uicon(
238 'M17.71,11.29a1,1,0,0,0-1.42,0L13,14.59V7a1,1,0,0,0-2,0v7.59l-3.29-3.3a1,1,0,0,0-1.42,1.42l5,5a1,1,0,0,0,.33.21.94.94,0,0,0,.76,0,1,1,0,0,0,.33-.21l5-5A1,1,0,0,0,17.71,11.29Z'
239 ),
240 chevronDown: uicon(
241 'M17,9.17a1,1,0,0,0-1.41,0L12,12.71,8.46,9.17a1,1,0,0,0-1.41,0,1,1,0,0,0,0,1.42l4.24,4.24a1,1,0,0,0,1.42,0L17,10.59A1,1,0,0,0,17,9.17Z'
242 ),
243 chevronUp: uicon(
244 'M16.8535156,13.6465454l-4.5-4.5c-0.000061-0.000061-0.000061-0.0001221-0.0001221-0.0001831c-0.1952515-0.1951294-0.5117188-0.1950684-0.7068481,0.0001831l-4.5,4.5c-0.1871948,0.1937256-0.1871948,0.5009155,0,0.6947021c0.1918335,0.1986084,0.5083618,0.2041016,0.7069702,0.0122681L12,10.2069702l4.1464844,4.1465454C16.2401123,14.4474487,16.3673706,14.5001831,16.5,14.5c0.1325073,0,0.2596436-0.0526733,0.3533936-0.1463623C17.048645,14.1583862,17.0487671,13.8417969,16.8535156,13.6465454z'
245 ),
246 view: uiconPaths( [
247 'M23.821,11.181v0C22.943,9.261,19.5,3,12,3S1.057,9.261.179,11.181a1.969,1.969,0,0,0,0,1.64C1.057,14.739,4.5,21,12,21s10.943-6.261,11.821-8.181A1.968,1.968,0,0,0,23.821,11.181ZM12,19c-6.307,0-9.25-5.366-10-6.989C2.75,10.366,5.693,5,12,5c6.292,0,9.236,5.343,10,7C21.236,13.657,18.292,19,12,19Z',
248 'M12,7a5,5,0,1,0,5,5A5.006,5.006,0,0,0,12,7Zm0,8a3,3,0,1,1,3-3A3,3,0,0,1,12,15Z',
249 ] ),
250 arrowLeft: uicon(
251 'M24,13l0-2-21.445.031L6.877,6.707,5.463,5.293.877,9.879a3,3,0,0,0,0,4.242l4.586,4.586,1.414-1.414L2.615,13.031Z'
252 ),
253 };
254 } )();
255
256 function iconOrDash( modernIcon, dashicon ) {
257 return modernIcon || dashicon;
258 }
259
260 var FAVORITES_KEY = 'patternswp_favorites_v1';
261 var RECENT_KEY = 'patternswp_recent_v1';
262 var PREFS_KEY = 'patternswp_prefs_v2';
263 var MAX_FAVORITES = 40;
264 var MAX_RECENT = 24;
265
266 var PAGE_SIZE = 20;
267
268 var DEFAULT_PREFS = {
269 layout: 'grid',
270 viewport: 'desktop',
271 sortField: 'default',
272 sortOrder: 'asc',
273 typeFilter: 'all',
274 previewSize: 2,
275 showTitle: true,
276 showPreview: true,
277 };
278
279 var PREVIEW_SIZE_HEIGHTS = {
280 1: 160,
281 2: 200,
282 3: 240,
283 4: 300,
284 };
285
286 var SPECIAL = {
287 ALL: '',
288 FAVORITES: '__favorites__',
289 RECENT: '__recent__',
290 };
291
292 var VIEWPORTS = {
293 desktop: 1300,
294 tablet: 778,
295 mobile: 358,
296 };
297
298 var pageCache = new Map();
299 var openModalExternal = null;
300 var insertAnchorClientId = null;
301
302 function setInsertAnchor( clientId ) {
303 insertAnchorClientId = clientId || null;
304 }
305
306 function openPatternsLibrary( options ) {
307 options = options || {};
308 setInsertAnchor( options.anchorClientId || null );
309 if ( openModalExternal ) {
310 openModalExternal();
311 return true;
312 }
313 return false;
314 }
315
316 window.patternswpOpenLibrary = openPatternsLibrary;
317
318 /* ------------------------------------------------------------------ */
319 /* Helpers */
320 /* ------------------------------------------------------------------ */
321
322 function getBoot() {
323 return window.patternsWpData || {
324 isLicenseActive: false,
325 patternCategories: [],
326 patternsNonce: '',
327 libraryComplete: false,
328 ajaxUrl: window.ajaxurl || '',
329 };
330 }
331
332 function ajaxUrl() {
333 var boot = getBoot();
334 return boot.ajaxUrl || window.ajaxurl || '';
335 }
336
337 function isLicenseUnlocked() {
338 return !! getBoot().isLicenseActive;
339 }
340
341 function isPatternLocked( pattern ) {
342 if ( ! pattern || isLicenseUnlocked() ) {
343 return false;
344 }
345 return pattern.locked === true || pattern.type === 'pro';
346 }
347
348 function parseAjaxJson( response ) {
349 return response.text().then( function ( text ) {
350 var json = null;
351 try {
352 json = text ? JSON.parse( text ) : null;
353 } catch ( e ) {
354 json = null;
355 }
356 if ( ! json ) {
357 throw new Error( 'invalid' );
358 }
359 return json;
360 } );
361 }
362
363 function normalizeLibraryPayload( json ) {
364 if ( ! json || ! json.success ) {
365 throw new Error( 'invalid' );
366 }
367 var data = json.data;
368 if ( Array.isArray( data ) ) {
369 return {
370 patterns: data,
371 total: data.length,
372 complete: true,
373 categories: null,
374 };
375 }
376 if ( data && Array.isArray( data.patterns ) ) {
377 if ( typeof data.licenseActive === 'boolean' ) {
378 window.patternsWpData = window.patternsWpData || {};
379 window.patternsWpData.isLicenseActive = data.licenseActive;
380 }
381 return {
382 patterns: data.patterns,
383 total: typeof data.total === 'number' ? data.total : data.patterns.length,
384 complete: !! data.complete,
385 categories: Array.isArray( data.categories ) ? data.categories : null,
386 licenseActive: data.licenseActive === true,
387 };
388 }
389 throw new Error( 'invalid' );
390 }
391
392 function patternKey( pattern ) {
393 return ( pattern.title || '' ) + '::' + ( ( pattern.categories || [] ).join( ',' ) );
394 }
395
396 function formatCategoryLabel( slug ) {
397 if ( ! slug ) {
398 return __( 'All patterns', 'patternswp' );
399 }
400 var map = {
401 cta: 'CTA',
402 faq: 'FAQ',
403 };
404 return String( slug )
405 .replace( /^patternswp-/, '' )
406 .split( '-' )
407 .map( function ( part ) {
408 if ( map[ part ] ) {
409 return map[ part ];
410 }
411 return part.charAt( 0 ).toUpperCase() + part.slice( 1 );
412 } )
413 .join( ' ' );
414 }
415
416 function readStore( key, fallback ) {
417 try {
418 var raw = window.localStorage.getItem( key );
419 if ( ! raw ) {
420 return fallback;
421 }
422 var parsed = JSON.parse( raw );
423 return parsed != null ? parsed : fallback;
424 } catch ( e ) {
425 return fallback;
426 }
427 }
428
429 function writeStore( key, value ) {
430 try {
431 window.localStorage.setItem( key, JSON.stringify( value ) );
432 } catch ( e ) {
433 /* quota / private mode */
434 }
435 }
436
437 function decodePatternContent( content ) {
438 if ( ! content ) {
439 return '';
440 }
441 try {
442 /* API returns base64-encoded block markup */
443 return window.atob( content );
444 } catch ( e ) {
445 return content;
446 }
447 }
448
449 function cacheKey( category, search, page, perPage ) {
450 return category + '|' + search + '|' + page + '|' + perPage;
451 }
452
453 function sortPatterns( list, sortField, sortOrder ) {
454 var items = list.slice();
455 var dir = sortOrder === 'desc' ? -1 : 1;
456
457 if ( sortField === 'title' ) {
458 items.sort( function ( a, b ) {
459 return dir * ( a.title || '' ).localeCompare( b.title || '' );
460 } );
461 return items;
462 }
463
464 if ( sortField === 'type' ) {
465 items.sort( function ( a, b ) {
466 var av = a.type === 'pro' ? 1 : 0;
467 var bv = b.type === 'pro' ? 1 : 0;
468 return dir * ( av - bv ) || ( a.title || '' ).localeCompare( b.title || '' );
469 } );
470 return items;
471 }
472
473 /* default: keep API order; desc reverses loaded list */
474 if ( sortOrder === 'desc' ) {
475 items.reverse();
476 }
477 return items;
478 }
479
480 function filterByType( list, typeFilter ) {
481 if ( typeFilter === 'free' ) {
482 return list.filter( function ( p ) {
483 return p.type !== 'pro';
484 } );
485 }
486 if ( typeFilter === 'pro' ) {
487 return list.filter( function ( p ) {
488 return p.type === 'pro';
489 } );
490 }
491 return list;
492 }
493
494 function upsertByKey( list, items ) {
495 var seen = new Set( list.map( patternKey ) );
496 var next = list.slice();
497 items.forEach( function ( item ) {
498 var key = patternKey( item );
499 if ( ! seen.has( key ) ) {
500 seen.add( key );
501 next.push( item );
502 }
503 } );
504 return next;
505 }
506
507 /* ------------------------------------------------------------------ */
508 /* Data fetching */
509 /* ------------------------------------------------------------------ */
510
511 function fetchPatternsPage( page, search, category, perPage, signal, retries ) {
512 var pageSize = perPage || PAGE_SIZE;
513 var key = cacheKey( category, search, page, pageSize );
514 if ( pageCache.has( key ) ) {
515 return Promise.resolve( pageCache.get( key ) );
516 }
517 if ( typeof retries !== 'number' ) {
518 retries = 1;
519 }
520
521 var boot = getBoot();
522 var body = new window.URLSearchParams( {
523 action: 'fetch_patterns',
524 page: String( page ),
525 patternsPerPage: String( pageSize ),
526 search: search || '',
527 category: category || '',
528 nonce: boot.patternsNonce || '',
529 } );
530
531 return window
532 .fetch( ajaxUrl(), {
533 method: 'POST',
534 credentials: 'same-origin',
535 headers: {
536 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
537 },
538 body: body.toString(),
539 signal: signal,
540 } )
541 .then( parseAjaxJson )
542 .then( function ( json ) {
543 var payload = normalizeLibraryPayload( json );
544 if ( payload.patterns.length || payload.complete ) {
545 pageCache.set( key, payload );
546 }
547 return payload;
548 } )
549 .catch( function ( err ) {
550 if ( err && err.name === 'AbortError' ) {
551 throw err;
552 }
553 if ( retries > 0 ) {
554 return new Promise( function ( resolve ) {
555 window.setTimeout( resolve, 1200 );
556 } ).then( function () {
557 return fetchPatternsPage( page, search, category, perPage, signal, retries - 1 );
558 } );
559 }
560 throw err;
561 } );
562 }
563
564 function warmLibrary( signal, mode, force ) {
565 var boot = getBoot();
566 var body = new window.URLSearchParams( {
567 action: 'patternswp_warm_library',
568 nonce: boot.patternsNonce || '',
569 mode: mode || 'sync',
570 force: force ? '1' : '0',
571 } );
572
573 return window
574 .fetch( ajaxUrl(), {
575 method: 'POST',
576 credentials: 'same-origin',
577 headers: {
578 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
579 },
580 body: body.toString(),
581 signal: signal,
582 } )
583 .then( parseAjaxJson )
584 .then( function ( json ) {
585 if ( ! json || ! json.success || ! json.data || typeof json.data !== 'object' ) {
586 throw new Error( 'invalid' );
587 }
588 return {
589 total: typeof json.data.total === 'number' ? json.data.total : 0,
590 complete: !! json.data.complete,
591 categories: Array.isArray( json.data.categories ) ? json.data.categories : null,
592 hasUpdates: !! json.data.has_updates,
593 };
594 } );
595 }
596
597 /* ------------------------------------------------------------------ */
598 /* Insert hook (mirrors core inserter destination logic) */
599 /* ------------------------------------------------------------------ */
600
601 function useInsertPattern( onInserted ) {
602 var getSelectedBlock = useSelect( function ( select ) {
603 return select( storeBlockEditor ).getSelectedBlock;
604 }, [] );
605
606 var destination = useSelect(
607 function ( select ) {
608 var s = select( storeBlockEditor );
609 var selectedId = s.getSelectedBlockClientId();
610 var rootClientId = '';
611 var index = s.getBlockOrder( rootClientId ).length;
612
613 if ( selectedId ) {
614 rootClientId = s.getBlockRootClientId( selectedId );
615 index = s.getBlockIndex( selectedId ) + 1;
616 }
617
618 return {
619 rootClientId: rootClientId,
620 index: index,
621 };
622 },
623 []
624 );
625
626 var dispatch = useDispatch( storeBlockEditor );
627 var notices = useDispatch( 'core/notices' );
628
629 return useCallback(
630 function ( patternOrList, options ) {
631 options = options || {};
632 var list = Array.isArray( patternOrList ) ? patternOrList : [ patternOrList ];
633 var blocks = [];
634 list.forEach( function ( pattern ) {
635 if ( isPatternLocked( pattern ) ) {
636 return;
637 }
638 try {
639 var markup = decodePatternContent( pattern.content );
640 if ( ! markup ) {
641 return;
642 }
643 parse( markup ).forEach( function ( block ) {
644 blocks.push( cloneBlock( block ) );
645 } );
646 } catch ( e ) {
647 /* skip broken pattern */
648 }
649 } );
650
651 if ( ! blocks.length ) {
652 var blocked = list.some( isPatternLocked );
653 notices.createErrorNotice(
654 blocked
655 ? __( 'Activate your PatternsWP license to insert Pro patterns.', 'patternswp' )
656 : __( 'Could not insert this pattern.', 'patternswp' ),
657 { type: 'snackbar' }
658 );
659 if ( blocked ) {
660 window.open( 'https://thepatternswp.com/pricing/', '_blank', 'noopener,noreferrer' );
661 }
662 return;
663 }
664
665 var anchorId = options.anchorClientId || insertAnchorClientId;
666 var selected = getSelectedBlock();
667 var editorSelect = wp.data.select( storeBlockEditor );
668
669 if ( anchorId && editorSelect.getBlock( anchorId ) ) {
670 var rootClientId = editorSelect.getBlockRootClientId( anchorId );
671 var index = editorSelect.getBlockIndex( anchorId ) + 1;
672 dispatch.insertBlocks( blocks, index, rootClientId, true );
673 } else if ( selected && isUnmodifiedDefaultBlock( selected ) ) {
674 dispatch.replaceBlocks( selected.clientId, blocks );
675 } else {
676 dispatch.insertBlocks(
677 blocks,
678 destination.index,
679 destination.rootClientId,
680 true
681 );
682 }
683
684 var count = castArray( blocks ).length;
685 speak(
686 sprintf(
687 _n( '%d block added.', '%d blocks added.', count, 'patternswp' ),
688 count
689 )
690 );
691
692 if ( list.length === 1 ) {
693 notices.createSuccessNotice(
694 sprintf(
695 /* translators: %s: pattern title */
696 __( '“%s” inserted.', 'patternswp' ),
697 list[ 0 ].title || __( 'Pattern', 'patternswp' )
698 ),
699 { type: 'snackbar' }
700 );
701 } else {
702 notices.createSuccessNotice(
703 sprintf(
704 /* translators: %d: number of patterns */
705 _n( '%d pattern inserted.', '%d patterns inserted.', list.length, 'patternswp' ),
706 list.length
707 ),
708 { type: 'snackbar' }
709 );
710 }
711
712 if ( onInserted ) {
713 onInserted( list );
714 }
715 },
716 [ destination.index, destination.rootClientId, dispatch, getSelectedBlock, notices, onInserted ]
717 );
718 }
719
720 /* ------------------------------------------------------------------ */
721 /* Lazy BlockPreview — mount only while in view */
722 /* ------------------------------------------------------------------ */
723
724 function LazyPreviewInner( props ) {
725 var ref = useRef( null );
726 var visibleState = useState( false );
727 var visible = visibleState[ 0 ];
728 var setVisible = visibleState[ 1 ];
729
730 useEffect(
731 function () {
732 var node = ref.current;
733 if ( ! node || ! window.IntersectionObserver ) {
734 setVisible( true );
735 return;
736 }
737 var observer = new window.IntersectionObserver(
738 function ( entries ) {
739 entries.forEach( function ( entry ) {
740 setVisible( entry.isIntersecting );
741 } );
742 },
743 { root: props.root || null, rootMargin: '300px 0px', threshold: 0.01 }
744 );
745 observer.observe( node );
746 return function () {
747 observer.disconnect();
748 };
749 },
750 [ props.root ]
751 );
752
753 var minHeight = props.minHeight || 200;
754
755 return el(
756 'div',
757 { ref: ref, style: { width: '100%', height: '100%', minHeight: minHeight } },
758 visible
759 ? el( BlockPreview, {
760 blocks: props.blocks,
761 viewportWidth: props.viewportWidth,
762 minHeight: minHeight,
763 } )
764 : el( 'div', { style: { display: 'flex', justifyContent: 'center', alignItems: 'center', height: '100%' } }, el( Spinner ) )
765 );
766 }
767
768 /**
769 * Detail preview that shows the pattern at its real height.
770 * Gutenberg's ScaledBlockPreview hard-caps iframe/content at 2000px, which
771 * clips page templates. CSS removes the cap; this re-applies it after React
772 * writes inline max-height, and sizes the wrapper so the modal can scroll.
773 */
774 function FullPatternPreview( props ) {
775 var viewportWidth = props.viewportWidth || VIEWPORTS.desktop;
776 var wrapRef = useRef( null );
777 var heightState = useState( 0 );
778 var height = heightState[ 0 ];
779 var setHeight = heightState[ 1 ];
780
781 useEffect(
782 function () {
783 var wrap = wrapRef.current;
784 if ( ! wrap ) {
785 return;
786 }
787
788 var timer = 0;
789 var tries = 0;
790 var boundIframe = null;
791
792 function unlockEl( el, propsMap ) {
793 if ( ! el ) {
794 return;
795 }
796 Object.keys( propsMap ).forEach( function ( key ) {
797 el.style.setProperty( key, propsMap[ key ], 'important' );
798 } );
799 }
800
801 function contentHeight( iframe ) {
802 var doc = iframe.contentDocument;
803 if ( ! doc ) {
804 return 0;
805 }
806 var html = doc.documentElement;
807 var body = doc.body;
808 if ( html ) {
809 html.style.setProperty( 'max-height', 'none', 'important' );
810 html.style.setProperty( 'overflow', 'visible', 'important' );
811 }
812 if ( body ) {
813 body.style.setProperty( 'max-height', 'none', 'important' );
814 body.style.setProperty( 'overflow', 'visible', 'important' );
815 }
816 return Math.max(
817 body ? body.scrollHeight : 0,
818 html ? html.scrollHeight : 0,
819 body ? body.offsetHeight : 0,
820 html ? html.offsetHeight : 0
821 );
822 }
823
824 function measure() {
825 var iframe = wrap.querySelector( 'iframe' );
826 var containerEl = wrap.querySelector( '.block-editor-block-preview__container' );
827 var contentEl = wrap.querySelector( '.block-editor-block-preview__content' );
828
829 unlockEl( containerEl, {
830 overflow: 'visible',
831 'max-height': 'none',
832 } );
833 unlockEl( contentEl, {
834 overflow: 'visible',
835 'max-height': 'none',
836 } );
837 unlockEl( wrap.querySelector( '.block-editor-iframe__container' ), {
838 overflow: 'visible',
839 'max-height': 'none',
840 } );
841 unlockEl( wrap.querySelector( '.block-editor-iframe__scale-container' ), {
842 overflow: 'visible',
843 'max-height': 'none',
844 } );
845
846 if ( ! iframe ) {
847 return;
848 }
849
850 if ( boundIframe !== iframe ) {
851 boundIframe = iframe;
852 iframe.addEventListener( 'load', measure );
853 }
854
855 var full = contentHeight( iframe );
856 unlockEl( iframe, {
857 'max-height': 'none',
858 overflow: 'visible',
859 } );
860 if ( full > 40 ) {
861 unlockEl( iframe, { height: full + 'px' } );
862 }
863
864 var next = 0;
865 if ( contentEl ) {
866 next = Math.ceil( contentEl.getBoundingClientRect().height );
867 }
868 if ( ( ! next || next < 40 ) && full > 40 ) {
869 var containerWidth = wrap.getBoundingClientRect().width || viewportWidth;
870 var scale = viewportWidth ? containerWidth / viewportWidth : 1;
871 next = Math.ceil( full * scale );
872 }
873 if ( next > 40 ) {
874 unlockEl( wrap, { 'min-height': next + 'px' } );
875 setHeight( function ( prev ) {
876 return Math.abs( prev - next ) > 8 ? next : prev;
877 } );
878 }
879 }
880
881 function tick() {
882 measure();
883 tries += 1;
884 if ( tries < 40 ) {
885 timer = window.setTimeout( tick, tries < 12 ? 100 : 300 );
886 }
887 }
888
889 var observer = window.MutationObserver
890 ? new window.MutationObserver( function () {
891 measure();
892 } )
893 : null;
894 if ( observer ) {
895 observer.observe( wrap, { childList: true, subtree: true } );
896 }
897 tick();
898
899 return function () {
900 if ( observer ) {
901 observer.disconnect();
902 }
903 window.clearTimeout( timer );
904 if ( boundIframe ) {
905 boundIframe.removeEventListener( 'load', measure );
906 }
907 };
908 },
909 [ props.blocks, viewportWidth ]
910 );
911
912 return el(
913 'div',
914 {
915 ref: wrapRef,
916 className: 'patternswp-detail__preview',
917 style: height ? { minHeight: height } : undefined,
918 },
919 el( BlockPreview, {
920 blocks: props.blocks,
921 viewportWidth: viewportWidth,
922 minHeight: 120,
923 } )
924 );
925 }
926
927 /* ------------------------------------------------------------------ */
928 /* Sidebar nav item */
929 /* ------------------------------------------------------------------ */
930
931 function SidebarNavItem( props ) {
932 return el(
933 'li',
934 null,
935 el(
936 'button',
937 {
938 type: 'button',
939 className: 'patternswp-sidebar__nav-item' + ( props.isActive ? ' is-active' : '' ),
940 onClick: props.onClick,
941 'aria-current': props.isActive ? 'page' : undefined,
942 },
943 props.label
944 )
945 );
946 }
947
948 function SidebarCollapsibleSection( props ) {
949 var isExpanded = !! props.isExpanded;
950 var panelId = props.panelId;
951
952 return el(
953 'div',
954 {
955 className:
956 'patternswp-sidebar__section' +
957 ( isExpanded ? ' is-expanded' : ' is-collapsed' ),
958 },
959 el(
960 'button',
961 {
962 type: 'button',
963 className: 'patternswp-sidebar__section-toggle',
964 onClick: props.onToggle,
965 'aria-expanded': isExpanded,
966 'aria-controls': panelId,
967 'aria-label': isExpanded
968 ? sprintf(
969 /* translators: %s: sidebar section title */
970 __( 'Minimize %s', 'patternswp' ),
971 props.title
972 )
973 : sprintf(
974 /* translators: %s: sidebar section title */
975 __( 'Maximize %s', 'patternswp' ),
976 props.title
977 ),
978 },
979 el( 'span', { className: 'patternswp-sidebar__section-title' }, props.title ),
980 el( Icon, {
981 className: 'patternswp-sidebar__section-toggle-icon',
982 icon: iconOrDash(
983 isExpanded ? icons.chevronUp : icons.chevronDown,
984 isExpanded ? 'arrow-up-alt2' : 'arrow-down-alt2'
985 ),
986 size: 16,
987 } )
988 ),
989 isExpanded
990 ? el(
991 'div',
992 { id: panelId, className: 'patternswp-sidebar__section-panel' },
993 props.children
994 )
995 : null
996 );
997 }
998
999 /* ------------------------------------------------------------------ */
1000 /* Appearance settings panel */
1001 /* ------------------------------------------------------------------ */
1002
1003 function PreviewSizeControl( props ) {
1004 var value = props.value || 2;
1005 var onChange = props.onChange;
1006 var steps = [ 1, 2, 3, 4 ];
1007 /* Fill covers completed segments (value 1 = 25%, … value 4 = 100%). */
1008 var fillPercent = ( value / 4 ) * 100;
1009
1010 function setSize( next, event ) {
1011 if ( event ) {
1012 event.preventDefault();
1013 event.stopPropagation();
1014 }
1015 if ( next >= 1 && next <= 4 && next !== value ) {
1016 onChange( next );
1017 }
1018 }
1019
1020 return el(
1021 'div',
1022 {
1023 className: 'patternswp-preview-size',
1024 role: 'slider',
1025 'aria-label': __( 'Preview size', 'patternswp' ),
1026 'aria-valuemin': 1,
1027 'aria-valuemax': 4,
1028 'aria-valuenow': value,
1029 tabIndex: 0,
1030 onKeyDown: function ( event ) {
1031 if ( event.key === 'ArrowRight' || event.key === 'ArrowUp' ) {
1032 setSize( Math.min( 4, value + 1 ), event );
1033 }
1034 if ( event.key === 'ArrowLeft' || event.key === 'ArrowDown' ) {
1035 setSize( Math.max( 1, value - 1 ), event );
1036 }
1037 },
1038 onMouseDown: function ( event ) {
1039 /* Keep Appearance dropdown open while interacting. */
1040 event.stopPropagation();
1041 },
1042 },
1043 el(
1044 'div',
1045 { className: 'patternswp-preview-size__track' },
1046 el( 'div', {
1047 className: 'patternswp-preview-size__fill',
1048 style: { width: fillPercent + '%' },
1049 } ),
1050 el(
1051 'div',
1052 { className: 'patternswp-preview-size__breaks', 'aria-hidden': true },
1053 el( 'span' ),
1054 el( 'span' ),
1055 el( 'span' )
1056 ),
1057 el( 'span', {
1058 className: 'patternswp-preview-size__thumb',
1059 style: { left: fillPercent + '%' },
1060 } ),
1061 el(
1062 'div',
1063 { className: 'patternswp-preview-size__hits' },
1064 steps.map( function ( step ) {
1065 return el( 'button', {
1066 key: step,
1067 type: 'button',
1068 className:
1069 'patternswp-preview-size__hit' +
1070 ( step === value ? ' is-active' : '' ),
1071 'aria-label': sprintf(
1072 /* translators: %d: preview size level */
1073 __( 'Size %d', 'patternswp' ),
1074 step
1075 ),
1076 onMouseDown: function ( event ) {
1077 setSize( step, event );
1078 },
1079 onClick: function ( event ) {
1080 setSize( step, event );
1081 },
1082 } );
1083 } )
1084 )
1085 )
1086 );
1087 }
1088
1089 /* ------------------------------------------------------------------ */
1090 /* Appearance settings panel */
1091 /* ------------------------------------------------------------------ */
1092
1093 function AppearancePanel( props ) {
1094 var sortField = props.sortField;
1095 var sortOrder = props.sortOrder;
1096 var previewSize = props.previewSize;
1097 var showTitle = props.showTitle;
1098 var showPreview = props.showPreview;
1099
1100 function propertyRow( key, label, checked, onToggle ) {
1101 return el(
1102 'button',
1103 {
1104 type: 'button',
1105 key: key,
1106 className: 'patternswp-appearance__property' + ( checked ? ' is-active' : '' ),
1107 onClick: onToggle,
1108 },
1109 el( 'span', { className: 'patternswp-appearance__property-check' }, checked ? el( Icon, { icon: iconOrDash( icons.check, 'yes' ) } ) : null ),
1110 el( 'span', null, label )
1111 );
1112 }
1113
1114 return el(
1115 'div',
1116 { className: 'patternswp-appearance' },
1117 el(
1118 'div',
1119 { className: 'patternswp-appearance__header' },
1120 el( 'h2', { className: 'patternswp-appearance__title' }, __( 'Appearance', 'patternswp' ) ),
1121 el(
1122 Button,
1123 {
1124 variant: 'tertiary',
1125 className: 'patternswp-appearance__reset',
1126 onClick: props.onReset,
1127 },
1128 __( 'Reset view', 'patternswp' )
1129 )
1130 ),
1131 el(
1132 'div',
1133 { className: 'patternswp-appearance__row patternswp-appearance__row--sort' },
1134 el(
1135 'div',
1136 { className: 'patternswp-appearance__field' },
1137 el( 'span', { className: 'patternswp-appearance__label' }, __( 'Sort by', 'patternswp' ) ),
1138 SelectControl
1139 ? el( SelectControl, {
1140 hideLabelFromVision: true,
1141 label: __( 'Sort by', 'patternswp' ),
1142 value: sortField,
1143 options: [
1144 { label: __( 'Default', 'patternswp' ), value: 'default' },
1145 { label: __( 'Title', 'patternswp' ), value: 'title' },
1146 { label: __( 'Type', 'patternswp' ), value: 'type' },
1147 ],
1148 onChange: props.onSortFieldChange,
1149 __nextHasNoMarginBottom: true,
1150 __next40pxDefaultSize: true,
1151 } )
1152 : null
1153 ),
1154 el(
1155 'div',
1156 { className: 'patternswp-appearance__field patternswp-appearance__field--order' },
1157 el( 'span', { className: 'patternswp-appearance__label' }, __( 'Order', 'patternswp' ) ),
1158 el(
1159 ButtonGroup,
1160 { className: 'patternswp-appearance__order' },
1161 el( Button, {
1162 icon: iconOrDash( icons.arrowUp, 'arrow-up-alt' ),
1163 label: __( 'Ascending', 'patternswp' ),
1164 isPressed: sortOrder === 'asc',
1165 onClick: function () {
1166 props.onSortOrderChange( 'asc' );
1167 },
1168 } ),
1169 el( Button, {
1170 icon: iconOrDash( icons.arrowDown, 'arrow-down-alt' ),
1171 label: __( 'Descending', 'patternswp' ),
1172 isPressed: sortOrder === 'desc',
1173 onClick: function () {
1174 props.onSortOrderChange( 'desc' );
1175 },
1176 } )
1177 )
1178 )
1179 ),
1180 el(
1181 'div',
1182 { className: 'patternswp-appearance__section' },
1183 el( 'span', { className: 'patternswp-appearance__label' }, __( 'Preview size', 'patternswp' ) ),
1184 el( PreviewSizeControl, {
1185 value: previewSize,
1186 onChange: props.onPreviewSizeChange,
1187 } )
1188 ),
1189 el(
1190 'div',
1191 { className: 'patternswp-appearance__section' },
1192 el( 'span', { className: 'patternswp-appearance__label' }, __( 'Properties', 'patternswp' ) ),
1193 el(
1194 'div',
1195 { className: 'patternswp-appearance__properties' },
1196 propertyRow( 'title', __( 'Title', 'patternswp' ), showTitle, function () {
1197 props.onShowTitleChange( ! showTitle );
1198 } ),
1199 propertyRow( 'preview', __( 'Preview', 'patternswp' ), showPreview, function () {
1200 props.onShowPreviewChange( ! showPreview );
1201 } )
1202 )
1203 )
1204 );
1205 }
1206
1207 /* ------------------------------------------------------------------ */
1208 /* Pattern card */
1209 /* ------------------------------------------------------------------ */
1210
1211 function PatternCard( props ) {
1212 var pattern = props.pattern;
1213 var isLicenseActive = props.isLicenseActive;
1214 var isPro = pattern.type === 'pro' || pattern.locked === true;
1215 var locked = isPatternLocked( pattern );
1216 var isFavorite = props.isFavorite;
1217 var isSelected = props.isSelected;
1218 var bulkMode = props.bulkMode;
1219 var viewportWidth = props.viewportWidth;
1220 var scrollRoot = props.scrollRoot;
1221 var showTitle = props.showTitle !== false;
1222 var showPreview = props.showPreview !== false;
1223 var previewHeight = props.previewHeight || 200;
1224
1225 var blocks = useMemo(
1226 function () {
1227 try {
1228 return parse( decodePatternContent( pattern.content ) );
1229 } catch ( e ) {
1230 return [];
1231 }
1232 },
1233 [ pattern.content ]
1234 );
1235
1236 function handlePreviewClick() {
1237 if ( bulkMode ) {
1238 props.onToggleSelect( pattern );
1239 return;
1240 }
1241 if ( locked ) {
1242 props.onView( pattern );
1243 return;
1244 }
1245 props.onInsert( pattern );
1246 }
1247
1248 return el(
1249 'article',
1250 {
1251 className:
1252 'patternswp-card' +
1253 ( locked ? ' is-locked' : '' ) +
1254 ( isSelected ? ' is-selected' : '' ) +
1255 ( ! showPreview ? ' no-preview' : '' ),
1256 },
1257 showPreview
1258 ? el(
1259 'div',
1260 {
1261 className: 'patternswp-card__preview',
1262 style: { height: previewHeight, minHeight: previewHeight, maxHeight: previewHeight },
1263 onClick: handlePreviewClick,
1264 onKeyDown: function ( event ) {
1265 if ( event.key === 'Enter' || event.key === ' ' ) {
1266 event.preventDefault();
1267 handlePreviewClick();
1268 }
1269 },
1270 role: 'button',
1271 tabIndex: 0,
1272 'aria-label': sprintf(
1273 /* translators: %s: pattern title */
1274 __( 'Insert pattern: %s', 'patternswp' ),
1275 pattern.title || ''
1276 ),
1277 },
1278 isPro
1279 ? el( 'span', { className: 'patternswp-card__badge' }, __( 'Pro', 'patternswp' ) )
1280 : null,
1281 el( LazyPreviewInner, {
1282 blocks: blocks,
1283 viewportWidth: viewportWidth,
1284 root: scrollRoot,
1285 minHeight: previewHeight,
1286 } )
1287 )
1288 : null,
1289 el(
1290 'div',
1291 { className: 'patternswp-card__footer' },
1292 el(
1293 'div',
1294 { className: 'patternswp-card__select' },
1295 CheckboxControl
1296 ? el( CheckboxControl, {
1297 checked: !! isSelected,
1298 onChange: function () {
1299 props.onToggleSelect( pattern );
1300 },
1301 __nextHasNoMarginBottom: true,
1302 } )
1303 : el( 'input', {
1304 type: 'checkbox',
1305 checked: !! isSelected,
1306 onChange: function () {
1307 props.onToggleSelect( pattern );
1308 },
1309 'aria-label': __( 'Select pattern', 'patternswp' ),
1310 } )
1311 ),
1312 showTitle
1313 ? el( 'div', { className: 'patternswp-card__title', title: pattern.title }, pattern.title )
1314 : el( 'div', { className: 'patternswp-card__title is-empty' } ),
1315 el(
1316 'div',
1317 { className: 'patternswp-card__icons' },
1318 el(
1319 Tooltip,
1320 { text: __( 'View pattern', 'patternswp' ) },
1321 el( Button, {
1322 icon: iconOrDash( icons.view, 'visibility' ),
1323 label: __( 'View', 'patternswp' ),
1324 size: 'compact',
1325 onClick: function ( event ) {
1326 event.stopPropagation();
1327 props.onView( pattern );
1328 },
1329 } )
1330 ),
1331 el(
1332 Tooltip,
1333 {
1334 text: isFavorite
1335 ? __( 'Remove from favorites', 'patternswp' )
1336 : __( 'Add to favorites', 'patternswp' ),
1337 },
1338 el( Button, {
1339 icon: iconOrDash(
1340 isFavorite ? icons.heartFilled : icons.heart,
1341 'heart'
1342 ),
1343 label: __( 'Favorite', 'patternswp' ),
1344 size: 'compact',
1345 isPressed: !! isFavorite,
1346 onClick: function ( event ) {
1347 event.stopPropagation();
1348 props.onToggleFavorite( pattern );
1349 },
1350 } )
1351 ),
1352 el(
1353 DropdownMenu,
1354 {
1355 icon: iconOrDash( icons.moreVertical, 'ellipsis' ),
1356 label: __( 'Pattern actions', 'patternswp' ),
1357 toggleProps: { size: 'compact' },
1358 },
1359 function ( { onClose } ) {
1360 return el(
1361 MenuGroup,
1362 null,
1363 locked
1364 ? el( MenuItem, {
1365 icon: iconOrDash( icons.lock, 'lock' ),
1366 onClick: function () {
1367 window.open(
1368 'https://thepatternswp.com/pricing/',
1369 '_blank',
1370 'noopener,noreferrer'
1371 );
1372 onClose();
1373 },
1374 }, __( 'Upgrade to insert', 'patternswp' ) )
1375 : el( MenuItem, {
1376 icon: iconOrDash( icons.plus, 'plus' ),
1377 onClick: function () {
1378 props.onInsert( pattern );
1379 onClose();
1380 },
1381 }, __( 'Add pattern', 'patternswp' ) ),
1382 el( MenuItem, {
1383 icon: iconOrDash(
1384 isFavorite ? icons.heartFilled : icons.heart,
1385 'heart'
1386 ),
1387 onClick: function () {
1388 props.onToggleFavorite( pattern );
1389 onClose();
1390 },
1391 }, isFavorite
1392 ? __( 'Remove favorite', 'patternswp' )
1393 : __( 'Add to favorites', 'patternswp' ) )
1394 );
1395 }
1396 )
1397 )
1398 )
1399 );
1400 }
1401
1402 function PatternDetailView( props ) {
1403 var pattern = props.pattern;
1404 var isLicenseActive = props.isLicenseActive;
1405 var isPro = pattern.type === 'pro' || pattern.locked === true;
1406 var locked = isPatternLocked( pattern );
1407 var isFavorite = props.isFavorite;
1408 var viewportWidth = props.viewportWidth;
1409
1410 var blocks = useMemo(
1411 function () {
1412 try {
1413 return parse( decodePatternContent( pattern.content ) );
1414 } catch ( e ) {
1415 return [];
1416 }
1417 },
1418 [ pattern.content ]
1419 );
1420
1421 return el(
1422 'div',
1423 { className: 'patternswp-detail' },
1424 el(
1425 'div',
1426 { className: 'patternswp-detail__header' },
1427 el( Button, {
1428 className: 'patternswp-detail__back',
1429 icon: iconOrDash( icons.arrowLeft, 'arrow-left-alt2' ),
1430 label: __( 'Back to patterns', 'patternswp' ),
1431 showTooltip: true,
1432 onClick: props.onClose,
1433 } ),
1434 el(
1435 'h2',
1436 { className: 'patternswp-detail__title' },
1437 pattern.title || __( 'Untitled pattern', 'patternswp' )
1438 ),
1439 isPro
1440 ? el( 'span', { className: 'patternswp-detail__badge' }, __( 'Pro', 'patternswp' ) )
1441 : null,
1442 el(
1443 'div',
1444 { className: 'patternswp-detail__actions' },
1445 el( Button, {
1446 icon: iconOrDash(
1447 isFavorite ? icons.heartFilled : icons.heart,
1448 'heart'
1449 ),
1450 label: isFavorite
1451 ? __( 'Remove from favorites', 'patternswp' )
1452 : __( 'Add to favorites', 'patternswp' ),
1453 isPressed: !! isFavorite,
1454 onClick: function () {
1455 props.onToggleFavorite( pattern );
1456 },
1457 } ),
1458 locked
1459 ? el( Button, {
1460 variant: 'primary',
1461 icon: iconOrDash( icons.lock, 'lock' ),
1462 onClick: function () {
1463 window.open(
1464 'https://thepatternswp.com/pricing/',
1465 '_blank',
1466 'noopener,noreferrer'
1467 );
1468 },
1469 }, __( 'Upgrade to insert', 'patternswp' ) )
1470 : el( Button, {
1471 className: 'patternswp-detail__insert',
1472 variant: 'primary',
1473 icon: iconOrDash( icons.plus, 'plus' ),
1474 onClick: function () {
1475 props.onInsert( pattern );
1476 },
1477 }, __( 'Add pattern', 'patternswp' ) )
1478 )
1479 ),
1480 el(
1481 'div',
1482 { className: 'patternswp-detail__body' },
1483 el(
1484 'div',
1485 {
1486 className: 'patternswp-detail__canvas',
1487 style: { maxWidth: viewportWidth + 'px' },
1488 },
1489 el( FullPatternPreview, {
1490 blocks: blocks,
1491 viewportWidth: viewportWidth,
1492 } )
1493 )
1494 )
1495 );
1496 }
1497
1498 /* ------------------------------------------------------------------ */
1499 /* Library modal */
1500 /* ------------------------------------------------------------------ */
1501
1502 function PatternsLibrary( props ) {
1503 var onRequestClose = props.onRequestClose;
1504 var boot = getBoot();
1505 var isLicenseActive = !! boot.isLicenseActive;
1506
1507 var remoteCategoriesState = useState( function () {
1508 var list = Array.isArray( boot.patternCategories ) ? boot.patternCategories : [];
1509 return list
1510 .filter( function ( cat ) {
1511 return cat && cat.name;
1512 } )
1513 .slice()
1514 .sort( function ( a, b ) {
1515 return formatCategoryLabel( a.name ).localeCompare( formatCategoryLabel( b.name ) );
1516 } );
1517 } );
1518 var categories = remoteCategoriesState[ 0 ];
1519 var setRemoteCategories = remoteCategoriesState[ 1 ];
1520
1521 var libraryCompleteState = useState( !! boot.libraryComplete );
1522 var libraryComplete = libraryCompleteState[ 0 ];
1523 var setLibraryComplete = libraryCompleteState[ 1 ];
1524
1525 var libraryTotalState = useState( 0 );
1526 var libraryTotal = libraryTotalState[ 0 ];
1527 var setLibraryTotal = libraryTotalState[ 1 ];
1528
1529 var syncingState = useState( false );
1530 var syncing = syncingState[ 0 ];
1531 var setSyncing = syncingState[ 1 ];
1532
1533 var gridReadyState = useState( false );
1534 var gridReady = gridReadyState[ 0 ];
1535 var setGridReady = gridReadyState[ 1 ];
1536
1537 var syncRequestRef = useRef( 0 );
1538
1539 var prefs = Object.assign( {}, DEFAULT_PREFS, readStore( PREFS_KEY, {} ) );
1540
1541 var categoryState = useState( SPECIAL.ALL );
1542 var category = categoryState[ 0 ];
1543 var setCategory = categoryState[ 1 ];
1544
1545 var searchState = useState( '' );
1546 var search = searchState[ 0 ];
1547 var setSearch = searchState[ 1 ];
1548
1549 var debouncedSearchState = useState( '' );
1550 var debouncedSearch = debouncedSearchState[ 0 ];
1551 var setDebouncedSearch = debouncedSearchState[ 1 ];
1552 var debounceSearch = useDebounce( setDebouncedSearch, 250 );
1553
1554 var sortFieldState = useState( prefs.sortField || 'default' );
1555 var sortField = sortFieldState[ 0 ];
1556 var setSortField = sortFieldState[ 1 ];
1557
1558 var sortOrderState = useState( prefs.sortOrder || 'asc' );
1559 var sortOrder = sortOrderState[ 0 ];
1560 var setSortOrder = sortOrderState[ 1 ];
1561
1562 var typeState = useState( prefs.typeFilter || 'all' );
1563 var typeFilter = typeState[ 0 ];
1564 var setTypeFilter = typeState[ 1 ];
1565
1566 var layoutState = useState( prefs.layout || 'grid' );
1567 var layout = layoutState[ 0 ];
1568 var setLayout = layoutState[ 1 ];
1569
1570 var viewportState = useState( prefs.viewport || 'desktop' );
1571 var viewport = viewportState[ 0 ];
1572 var setViewport = viewportState[ 1 ];
1573
1574 var previewSizeState = useState( prefs.previewSize || 2 );
1575 var previewSize = previewSizeState[ 0 ];
1576 var setPreviewSize = previewSizeState[ 1 ];
1577
1578 var showTitleState = useState( prefs.showTitle !== false );
1579 var showTitle = showTitleState[ 0 ];
1580 var setShowTitle = showTitleState[ 1 ];
1581
1582 var showPreviewState = useState( prefs.showPreview !== false );
1583 var showPreview = showPreviewState[ 0 ];
1584 var setShowPreview = showPreviewState[ 1 ];
1585
1586 var patternsState = useState( [] );
1587 var patterns = patternsState[ 0 ];
1588 var setPatterns = patternsState[ 1 ];
1589
1590 var pageState = useState( 1 );
1591 var page = pageState[ 0 ];
1592 var setPage = pageState[ 1 ];
1593
1594 var hasMoreState = useState( true );
1595 var hasMore = hasMoreState[ 0 ];
1596 var setHasMore = hasMoreState[ 1 ];
1597
1598 var loadingState = useState( true );
1599 var loading = loadingState[ 0 ];
1600 var setLoading = loadingState[ 1 ];
1601
1602 var loadingMoreState = useState( false );
1603 var loadingMore = loadingMoreState[ 0 ];
1604 var setLoadingMore = loadingMoreState[ 1 ];
1605
1606 var errorState = useState( '' );
1607 var error = errorState[ 0 ];
1608 var setError = errorState[ 1 ];
1609
1610 var favoritesState = useState( function () {
1611 return readStore( FAVORITES_KEY, [] );
1612 } );
1613 var favorites = favoritesState[ 0 ];
1614 var setFavorites = favoritesState[ 1 ];
1615
1616 var recentState = useState( function () {
1617 return readStore( RECENT_KEY, [] );
1618 } );
1619 var recent = recentState[ 0 ];
1620 var setRecent = recentState[ 1 ];
1621
1622 var bulkModeState = useState( false );
1623 var bulkMode = bulkModeState[ 0 ];
1624 var setBulkMode = bulkModeState[ 1 ];
1625
1626 var collectionsExpandedState = useState( false );
1627 var collectionsExpanded = collectionsExpandedState[ 0 ];
1628 var setCollectionsExpanded = collectionsExpandedState[ 1 ];
1629
1630 var previewPatternState = useState( null );
1631 var previewPattern = previewPatternState[ 0 ];
1632 var setPreviewPattern = previewPatternState[ 1 ];
1633
1634 var selectedState = useState( {} );
1635 var selectedMap = selectedState[ 0 ];
1636 var setSelectedMap = selectedState[ 1 ];
1637
1638 var scrollRef = useRef( null );
1639 var sentinelRef = useRef( null );
1640 var abortRef = useRef( null );
1641 var requestIdRef = useRef( 0 );
1642 var firstPageReadyRef = useRef( false );
1643
1644 useEffect(
1645 function () {
1646 debounceSearch( search );
1647 },
1648 [ search, debounceSearch ]
1649 );
1650
1651 useEffect(
1652 function () {
1653 writeStore( PREFS_KEY, {
1654 layout: layout,
1655 viewport: viewport,
1656 sortField: sortField,
1657 sortOrder: sortOrder,
1658 typeFilter: typeFilter,
1659 previewSize: previewSize,
1660 showTitle: showTitle,
1661 showPreview: showPreview,
1662 } );
1663 },
1664 [
1665 layout,
1666 viewport,
1667 sortField,
1668 sortOrder,
1669 typeFilter,
1670 previewSize,
1671 showTitle,
1672 showPreview,
1673 ]
1674 );
1675
1676 var isLocalCollection =
1677 category === SPECIAL.FAVORITES || category === SPECIAL.RECENT;
1678
1679 useEffect(
1680 function () {
1681 setPreviewPattern( null );
1682 },
1683 [ category, debouncedSearch ]
1684 );
1685
1686 useEffect(
1687 function () {
1688 if ( ! previewPattern ) {
1689 return;
1690 }
1691 function onKeyDown( event ) {
1692 if ( event.key === 'Escape' ) {
1693 event.preventDefault();
1694 event.stopPropagation();
1695 setPreviewPattern( null );
1696 }
1697 }
1698 document.addEventListener( 'keydown', onKeyDown, true );
1699 return function () {
1700 document.removeEventListener( 'keydown', onKeyDown, true );
1701 };
1702 },
1703 [ previewPattern ]
1704 );
1705
1706 var loadPage = useCallback(
1707 function ( nextPage, replace ) {
1708 if ( isLocalCollection ) {
1709 return;
1710 }
1711
1712 if ( abortRef.current ) {
1713 abortRef.current.abort();
1714 }
1715 var controller = window.AbortController ? new window.AbortController() : null;
1716 abortRef.current = controller;
1717 var requestId = ++requestIdRef.current;
1718
1719 if ( replace ) {
1720 setLoading( true );
1721 setError( '' );
1722 } else {
1723 setLoadingMore( true );
1724 }
1725
1726 fetchPatternsPage(
1727 nextPage,
1728 debouncedSearch,
1729 category === SPECIAL.ALL ? '' : category,
1730 PAGE_SIZE,
1731 controller && controller.signal
1732 )
1733 .then( function ( payload ) {
1734 if ( requestId !== requestIdRef.current ) {
1735 return;
1736 }
1737 var data = payload && Array.isArray( payload.patterns ) ? payload.patterns : [];
1738 var complete = !!( payload && payload.complete );
1739 var total = payload && typeof payload.total === 'number' ? payload.total : 0;
1740 if ( payload && payload.categories && payload.categories.length ) {
1741 setRemoteCategories(
1742 payload.categories
1743 .filter( function ( cat ) {
1744 return cat && cat.name;
1745 } )
1746 .slice()
1747 .sort( function ( a, b ) {
1748 return formatCategoryLabel( a.name ).localeCompare(
1749 formatCategoryLabel( b.name )
1750 );
1751 } )
1752 );
1753 }
1754 setLibraryComplete( complete );
1755 if ( total ) {
1756 setLibraryTotal( total );
1757 }
1758 setPage( nextPage );
1759 setPatterns( function ( prev ) {
1760 var next = replace ? data : upsertByKey( prev, data );
1761 setHasMore( next.length < total || ( ! complete && data.length >= PAGE_SIZE ) );
1762 return next;
1763 } );
1764 } )
1765 .catch( function ( err ) {
1766 if ( err && err.name === 'AbortError' ) {
1767 return;
1768 }
1769 if ( requestId !== requestIdRef.current ) {
1770 return;
1771 }
1772 setError(
1773 __( 'Failed to load patterns. Please try again.', 'patternswp' )
1774 );
1775 if ( replace ) {
1776 setPatterns( [] );
1777 setHasMore( false );
1778 }
1779 } )
1780 .finally( function () {
1781 if ( requestId !== requestIdRef.current ) {
1782 return;
1783 }
1784 setLoading( false );
1785 setLoadingMore( false );
1786 if ( ! firstPageReadyRef.current ) {
1787 firstPageReadyRef.current = true;
1788 setGridReady( true );
1789 }
1790 } );
1791 },
1792 [ category, debouncedSearch, isLocalCollection ]
1793 );
1794
1795 /* Local collections (favorites / recent) — no AJAX */
1796 useEffect(
1797 function () {
1798 if ( ! isLocalCollection ) {
1799 return;
1800 }
1801 var source = category === SPECIAL.FAVORITES ? favorites : recent;
1802 var filtered = source;
1803 if ( debouncedSearch ) {
1804 var q = debouncedSearch.toLowerCase();
1805 filtered = source.filter( function ( p ) {
1806 return ( p.title || '' ).toLowerCase().indexOf( q ) !== -1;
1807 } );
1808 }
1809 setPatterns( filtered );
1810 setHasMore( false );
1811 setLoading( false );
1812 setError( '' );
1813 setPage( 1 );
1814 },
1815 [ category, debouncedSearch, isLocalCollection, favorites, recent ]
1816 );
1817
1818 /* Remote patterns — reset + fetch when category / search changes */
1819 useEffect(
1820 function () {
1821 if ( isLocalCollection ) {
1822 return;
1823 }
1824
1825 setPatterns( [] );
1826 setPage( 1 );
1827 setHasMore( true );
1828 loadPage( 1, true );
1829
1830 return function () {
1831 if ( abortRef.current ) {
1832 abortRef.current.abort();
1833 }
1834 };
1835 },
1836 [ category, debouncedSearch, isLocalCollection, loadPage ]
1837 );
1838
1839 /* Fill the rest of the catalog after the first page is on screen. */
1840 useEffect(
1841 function () {
1842 if ( ! gridReady ) {
1843 return;
1844 }
1845
1846 var stopped = false;
1847 var timer = 0;
1848 var tries = 0;
1849 var requestId = ++syncRequestRef.current;
1850
1851 function applyStatus( status ) {
1852 if ( status.categories && status.categories.length ) {
1853 setRemoteCategories(
1854 status.categories
1855 .filter( function ( cat ) {
1856 return cat && cat.name;
1857 } )
1858 .slice()
1859 .sort( function ( a, b ) {
1860 return formatCategoryLabel( a.name ).localeCompare(
1861 formatCategoryLabel( b.name )
1862 );
1863 } )
1864 );
1865 }
1866 if ( typeof status.total === 'number' ) {
1867 setLibraryTotal( status.total );
1868 }
1869 setLibraryComplete( !! status.complete );
1870 if ( ! status.complete ) {
1871 setHasMore( true );
1872 }
1873 }
1874
1875 function tick( mode ) {
1876 if ( stopped || requestId !== syncRequestRef.current || tries > 40 ) {
1877 setSyncing( false );
1878 return;
1879 }
1880 tries += 1;
1881 setSyncing( true );
1882 warmLibrary( null, mode )
1883 .then( function ( status ) {
1884 if ( stopped || requestId !== syncRequestRef.current ) {
1885 return;
1886 }
1887 applyStatus( status );
1888 if ( status.complete && ( mode === 'check' ? ! status.hasUpdates : true ) ) {
1889 setSyncing( false );
1890 return;
1891 }
1892 timer = window.setTimeout( function () {
1893 tick( 'sync' );
1894 }, 600 );
1895 } )
1896 .catch( function ( err ) {
1897 if ( err && err.name === 'AbortError' ) {
1898 return;
1899 }
1900 if ( ! stopped && requestId === syncRequestRef.current ) {
1901 timer = window.setTimeout( function () {
1902 tick( 'sync' );
1903 }, 2000 );
1904 }
1905 } );
1906 }
1907
1908 tick( boot.libraryComplete ? 'check' : 'sync' );
1909
1910 return function () {
1911 stopped = true;
1912 window.clearTimeout( timer );
1913 };
1914 },
1915 [ gridReady ]
1916 );
1917
1918 function onSyncClick() {
1919 syncRequestRef.current += 1;
1920 var requestId = syncRequestRef.current;
1921 setSyncing( true );
1922
1923 function applyStatus( status ) {
1924 if ( status.categories && status.categories.length ) {
1925 setRemoteCategories(
1926 status.categories
1927 .filter( function ( cat ) {
1928 return cat && cat.name;
1929 } )
1930 .slice()
1931 .sort( function ( a, b ) {
1932 return formatCategoryLabel( a.name ).localeCompare(
1933 formatCategoryLabel( b.name )
1934 );
1935 } )
1936 );
1937 }
1938 if ( typeof status.total === 'number' ) {
1939 setLibraryTotal( status.total );
1940 }
1941 setLibraryComplete( !! status.complete );
1942 if ( ! status.complete || status.hasUpdates ) {
1943 setHasMore( true );
1944 }
1945 }
1946
1947 function tick( mode ) {
1948 if ( requestId !== syncRequestRef.current ) {
1949 return;
1950 }
1951 warmLibrary( null, mode, true )
1952 .then( function ( status ) {
1953 if ( requestId !== syncRequestRef.current ) {
1954 return;
1955 }
1956 applyStatus( status );
1957 if ( status.complete && ! status.hasUpdates ) {
1958 setSyncing( false );
1959 return;
1960 }
1961 window.setTimeout( function () {
1962 tick( 'sync' );
1963 }, 500 );
1964 } )
1965 .catch( function () {
1966 if ( requestId === syncRequestRef.current ) {
1967 setSyncing( false );
1968 }
1969 } );
1970 }
1971
1972 tick( 'check' );
1973 }
1974 useEffect(
1975 function () {
1976 var sentinel = sentinelRef.current;
1977 var root = scrollRef.current;
1978 if ( ! sentinel || ! root || ! window.IntersectionObserver ) {
1979 return;
1980 }
1981 if ( isLocalCollection || ! hasMore || loading || loadingMore ) {
1982 return;
1983 }
1984
1985 var observer = new window.IntersectionObserver(
1986 function ( entries ) {
1987 if ( entries[ 0 ] && entries[ 0 ].isIntersecting ) {
1988 loadPage( page + 1, false );
1989 }
1990 },
1991 { root: root, rootMargin: '600px 0px', threshold: 0 }
1992 );
1993 observer.observe( sentinel );
1994 return function () {
1995 observer.disconnect();
1996 };
1997 },
1998 [ hasMore, loading, loadingMore, page, loadPage, isLocalCollection, patterns.length ]
1999 );
2000
2001 var favoriteKeys = useMemo(
2002 function () {
2003 return new Set( favorites.map( patternKey ) );
2004 },
2005 [ favorites ]
2006 );
2007
2008 var visiblePatterns = useMemo(
2009 function () {
2010 return sortPatterns( filterByType( patterns, typeFilter ), sortField, sortOrder );
2011 },
2012 [ patterns, typeFilter, sortField, sortOrder ]
2013 );
2014
2015 var previewHeight = PREVIEW_SIZE_HEIGHTS[ previewSize ] || PREVIEW_SIZE_HEIGHTS[ 2 ];
2016 if ( layout === 'list' ) {
2017 previewHeight = Math.max( previewHeight, 300 );
2018 }
2019
2020 function resetAppearance() {
2021 setSortField( DEFAULT_PREFS.sortField );
2022 setSortOrder( DEFAULT_PREFS.sortOrder );
2023 setPreviewSize( DEFAULT_PREFS.previewSize );
2024 setShowTitle( DEFAULT_PREFS.showTitle );
2025 setShowPreview( DEFAULT_PREFS.showPreview );
2026 setLayout( DEFAULT_PREFS.layout );
2027 setViewport( DEFAULT_PREFS.viewport );
2028 }
2029
2030 var insertPattern = useInsertPattern( function ( patterns ) {
2031 var list = Array.isArray( patterns ) ? patterns : [ patterns ];
2032 setRecent( function ( prev ) {
2033 var next = prev.slice();
2034 list.forEach( function ( pattern ) {
2035 var key = patternKey( pattern );
2036 next = [ pattern ].concat(
2037 next.filter( function ( p ) {
2038 return patternKey( p ) !== key;
2039 } )
2040 );
2041 } );
2042 next = next.slice( 0, MAX_RECENT );
2043 writeStore( RECENT_KEY, next );
2044 return next;
2045 } );
2046 onRequestClose();
2047 } );
2048
2049 var toggleFavorite = useCallback( function ( pattern ) {
2050 setFavorites( function ( prev ) {
2051 var key = patternKey( pattern );
2052 var exists = prev.some( function ( p ) {
2053 return patternKey( p ) === key;
2054 } );
2055 var next;
2056 if ( exists ) {
2057 next = prev.filter( function ( p ) {
2058 return patternKey( p ) !== key;
2059 } );
2060 } else {
2061 next = [ pattern ].concat( prev ).slice( 0, MAX_FAVORITES );
2062 }
2063 writeStore( FAVORITES_KEY, next );
2064 return next;
2065 } );
2066 }, [] );
2067
2068 var toggleSelect = useCallback( function ( pattern ) {
2069 var key = patternKey( pattern );
2070 setSelectedMap( function ( prev ) {
2071 var next = Object.assign( {}, prev );
2072 if ( next[ key ] ) {
2073 delete next[ key ];
2074 } else {
2075 next[ key ] = pattern;
2076 }
2077 return next;
2078 } );
2079 }, [] );
2080
2081 var selectedList = useMemo(
2082 function () {
2083 return Object.keys( selectedMap ).map( function ( key ) {
2084 return selectedMap[ key ];
2085 } );
2086 },
2087 [ selectedMap ]
2088 );
2089
2090 var insertSelected = useCallback(
2091 function () {
2092 var allowed = selectedList.filter( function ( pattern ) {
2093 return ! isPatternLocked( pattern );
2094 } );
2095 if ( ! allowed.length ) {
2096 return;
2097 }
2098 insertPattern( allowed );
2099 },
2100 [ selectedList, insertPattern, isLicenseActive ]
2101 );
2102
2103 var collectionItems = useMemo(
2104 function () {
2105 return [
2106 { value: SPECIAL.ALL, label: __( 'All patterns', 'patternswp' ) },
2107 {
2108 value: SPECIAL.FAVORITES,
2109 label: sprintf(
2110 /* translators: %d: favorites count */
2111 __( 'Favorites (%d)', 'patternswp' ),
2112 favorites.length
2113 ),
2114 },
2115 {
2116 value: SPECIAL.RECENT,
2117 label: sprintf(
2118 /* translators: %d: recent count */
2119 __( 'Recently used (%d)', 'patternswp' ),
2120 recent.length
2121 ),
2122 },
2123 ];
2124 },
2125 [ favorites.length, recent.length ]
2126 );
2127
2128 var categoryItems = useMemo(
2129 function () {
2130 return categories.map( function ( cat ) {
2131 return {
2132 value: cat.name,
2133 label: formatCategoryLabel( cat.name ),
2134 };
2135 } );
2136 },
2137 [ categories ]
2138 );
2139
2140 var viewportWidth = VIEWPORTS[ viewport ] || VIEWPORTS.desktop;
2141
2142 var gridClass =
2143 'patternswp-grid' +
2144 ( layout === 'list' ? ' is-list' : '' ) +
2145 ( viewport === 'tablet' ? ' is-tablet' : '' ) +
2146 ( viewport === 'mobile' ? ' is-mobile' : '' ) +
2147 ' preview-size-' +
2148 previewSize;
2149
2150 return el(
2151 Modal,
2152 {
2153 title: __( 'PatternsWP Patterns', 'patternswp' ),
2154 className: 'patternswp-modal',
2155 onRequestClose: onRequestClose,
2156 isFullScreen: true,
2157 shouldCloseOnClickOutside: false,
2158 headerActions: el(
2159 Button,
2160 {
2161 className: 'patternswp-library-sync' + ( syncing ? ' is-busy' : '' ),
2162 icon: syncing
2163 ? el( Spinner )
2164 : iconOrDash( icons.refresh, 'update' ),
2165 label: syncing
2166 ? __( 'Updating the pattern library', 'patternswp' )
2167 : libraryComplete
2168 ? __( 'Check for new patterns', 'patternswp' )
2169 : __( 'Load remaining patterns', 'patternswp' ),
2170 showTooltip: true,
2171 onClick: onSyncClick,
2172 disabled: syncing,
2173 },
2174 syncing
2175 ? ( libraryTotal
2176 ? sprintf(
2177 /* translators: %d: number of cached patterns */
2178 __( '%d', 'patternswp' ),
2179 libraryTotal
2180 )
2181 : __( 'Syncing', 'patternswp' ) )
2182 : __( 'Sync', 'patternswp' )
2183 ),
2184 },
2185 el(
2186 'div',
2187 { className: 'patternswp-layout' },
2188 el(
2189 'aside',
2190 { className: 'patternswp-sidebar', 'aria-label': __( 'Pattern categories', 'patternswp' ) },
2191 el( SidebarCollapsibleSection, {
2192 title: __( 'Collections', 'patternswp' ),
2193 panelId: 'patternswp-collections-panel',
2194 isExpanded: collectionsExpanded,
2195 onToggle: function () {
2196 setCollectionsExpanded( ! collectionsExpanded );
2197 },
2198 },
2199 el(
2200 'ul',
2201 { className: 'patternswp-sidebar__nav' },
2202 collectionItems.map( function ( item ) {
2203 return el( SidebarNavItem, {
2204 key: item.value || 'all',
2205 label: item.label,
2206 isActive: category === item.value,
2207 onClick: function () {
2208 setCategory( item.value );
2209 },
2210 } );
2211 } )
2212 )
2213 ),
2214 el(
2215 'div',
2216 { className: 'patternswp-sidebar__section' },
2217 el( 'h2', { className: 'patternswp-sidebar__section-title' }, __( 'Categories', 'patternswp' ) ),
2218 el(
2219 'ul',
2220 { className: 'patternswp-sidebar__nav' },
2221 categoryItems.map( function ( item ) {
2222 return el( SidebarNavItem, {
2223 key: item.value,
2224 label: item.label,
2225 isActive: category === item.value,
2226 onClick: function () {
2227 setCategory( item.value );
2228 },
2229 } );
2230 } )
2231 )
2232 ),
2233 el(
2234 'div',
2235 { className: 'patternswp-sidebar__section patternswp-sidebar__footer' },
2236 el( 'h2', { className: 'patternswp-sidebar__section-title' }, __( 'Resources', 'patternswp' ) ),
2237 el(
2238 'a',
2239 {
2240 className: 'patternswp-sidebar__link',
2241 href: 'https://thepatternswp.com/docs/',
2242 target: '_blank',
2243 rel: 'noopener noreferrer',
2244 },
2245 __( 'Pattern guide', 'patternswp' )
2246 ),
2247 el(
2248 'a',
2249 {
2250 className: 'patternswp-sidebar__link',
2251 href: isLicenseActive
2252 ? 'https://thepatternswp.com/account/'
2253 : 'https://thepatternswp.com/pricing/',
2254 target: '_blank',
2255 rel: 'noopener noreferrer',
2256 },
2257 isLicenseActive
2258 ? __( 'PatternsWP account', 'patternswp' )
2259 : __( 'Upgrade to Pro', 'patternswp' )
2260 ),
2261 el(
2262 'a',
2263 {
2264 className: 'patternswp-sidebar__link',
2265 href: 'https://thepatternswp.com/contact/',
2266 target: '_blank',
2267 rel: 'noopener noreferrer',
2268 },
2269 __( 'Support', 'patternswp' )
2270 )
2271 )
2272 ),
2273 el(
2274 'section',
2275 {
2276 className:
2277 'patternswp-main' + ( previewPattern ? ' patternswp-main--detail' : '' ),
2278 },
2279 previewPattern
2280 ? el( PatternDetailView, {
2281 pattern: previewPattern,
2282 isLicenseActive: isLicenseActive,
2283 isFavorite: favoriteKeys.has( patternKey( previewPattern ) ),
2284 viewportWidth: viewportWidth,
2285 onClose: function () {
2286 setPreviewPattern( null );
2287 },
2288 onInsert: insertPattern,
2289 onToggleFavorite: toggleFavorite,
2290 } )
2291 : el(
2292 Fragment,
2293 null,
2294 el(
2295 'div',
2296 { className: 'patternswp-toolbar' },
2297 el(
2298 'div',
2299 { className: 'patternswp-toolbar__search' },
2300 el( SearchControl, {
2301 label: __( 'Search patterns', 'patternswp' ),
2302 placeholder: __( 'Search', 'patternswp' ),
2303 value: search,
2304 onChange: setSearch,
2305 __nextHasNoMarginBottom: true,
2306 } )
2307 ),
2308 el(
2309 'div',
2310 { className: 'patternswp-toolbar__actions' },
2311 el(
2312 'span',
2313 { className: 'patternswp-toolbar__meta' },
2314 loading
2315 ? el( Spinner )
2316 : sprintf(
2317 /* translators: %d: number of patterns */
2318 _n( '%d pattern', '%d patterns', visiblePatterns.length, 'patternswp' ),
2319 visiblePatterns.length
2320 ) + ( hasMore && ! isLocalCollection ? '+' : '' )
2321 ),
2322 el( Button, {
2323 className: 'patternswp-toolbar__bulk-edit',
2324 icon: iconOrDash( icons.edit, 'edit' ),
2325 onClick: function () {
2326 setBulkMode( ! bulkMode );
2327 if ( bulkMode ) {
2328 setSelectedMap( {} );
2329 }
2330 },
2331 isPressed: bulkMode,
2332 }, __( 'Bulk edit', 'patternswp' ) ),
2333 el(
2334 DropdownMenu,
2335 {
2336 icon: iconOrDash( icons.filter, 'filter' ),
2337 label: __( 'Filter', 'patternswp' ),
2338 toggleProps: {
2339 children: __( 'Filter', 'patternswp' ),
2340 },
2341 },
2342 function ( { onClose } ) {
2343 return el(
2344 Fragment,
2345 null,
2346 el(
2347 MenuGroup,
2348 { label: __( 'Type', 'patternswp' ) },
2349 [
2350 { value: 'all', label: __( 'All', 'patternswp' ) },
2351 { value: 'free', label: __( 'Free', 'patternswp' ) },
2352 { value: 'pro', label: __( 'Pro', 'patternswp' ) },
2353 ].map( function ( opt ) {
2354 return el( MenuItem, {
2355 key: opt.value,
2356 icon: typeFilter === opt.value ? iconOrDash( icons.check, 'yes' ) : null,
2357 onClick: function () {
2358 setTypeFilter( opt.value );
2359 onClose();
2360 },
2361 }, opt.label );
2362 } )
2363 ),
2364 el(
2365 MenuGroup,
2366 { label: __( 'Preview width', 'patternswp' ) },
2367 [
2368 { value: 'desktop', label: __( 'Desktop', 'patternswp' ) },
2369 { value: 'tablet', label: __( 'Tablet', 'patternswp' ) },
2370 { value: 'mobile', label: __( 'Mobile', 'patternswp' ) },
2371 ].map( function ( opt ) {
2372 return el( MenuItem, {
2373 key: opt.value,
2374 icon: viewport === opt.value ? iconOrDash( icons.check, 'yes' ) : null,
2375 onClick: function () {
2376 setViewport( opt.value );
2377 onClose();
2378 },
2379 }, opt.label );
2380 } )
2381 )
2382 );
2383 }
2384 ),
2385 el( Button, {
2386 icon: iconOrDash(
2387 layout === 'grid' ? icons.grid : icons.list,
2388 layout === 'grid' ? 'grid-view' : 'list-view'
2389 ),
2390 label:
2391 layout === 'grid'
2392 ? __( 'Grid view', 'patternswp' )
2393 : __( 'List view', 'patternswp' ),
2394 onClick: function () {
2395 setLayout( layout === 'grid' ? 'list' : 'grid' );
2396 },
2397 } ),
2398 Dropdown
2399 ? el( Dropdown, {
2400 className: 'patternswp-appearance-dropdown',
2401 popoverProps: {
2402 placement: 'bottom-end',
2403 offset: 8,
2404 focusOnMount: false,
2405 },
2406 renderToggle: function ( toggleProps ) {
2407 return el( Button, {
2408 className: 'patternswp-appearance-toggle',
2409 icon: iconOrDash( icons.settings, 'admin-generic' ),
2410 label: __( 'Appearance', 'patternswp' ),
2411 onClick: function ( event ) {
2412 event.preventDefault();
2413 toggleProps.onToggle();
2414 },
2415 'aria-expanded': toggleProps.isOpen,
2416 isPressed: !! toggleProps.isOpen,
2417 } );
2418 },
2419 renderContent: function () {
2420 return el( AppearancePanel, {
2421 sortField: sortField,
2422 sortOrder: sortOrder,
2423 previewSize: previewSize,
2424 showTitle: showTitle,
2425 showPreview: showPreview,
2426 onSortFieldChange: setSortField,
2427 onSortOrderChange: setSortOrder,
2428 onPreviewSizeChange: setPreviewSize,
2429 onShowTitleChange: setShowTitle,
2430 onShowPreviewChange: setShowPreview,
2431 onReset: resetAppearance,
2432 } );
2433 },
2434 } )
2435 : null
2436 )
2437 ),
2438 bulkMode
2439 ? el(
2440 'div',
2441 { className: 'patternswp-bulk-bar' },
2442 el(
2443 'span',
2444 null,
2445 sprintf(
2446 /* translators: %d: selected count */
2447 _n( '%d selected', '%d selected', selectedList.length, 'patternswp' ),
2448 selectedList.length
2449 )
2450 ),
2451 el(
2452 'div',
2453 { className: 'patternswp-bulk-bar__actions' },
2454 el( Button, {
2455 variant: 'tertiary',
2456 onClick: function () {
2457 setSelectedMap( {} );
2458 },
2459 disabled: ! selectedList.length,
2460 }, __( 'Clear', 'patternswp' ) ),
2461 el( Button, {
2462 variant: 'primary',
2463 onClick: insertSelected,
2464 disabled: ! selectedList.length,
2465 }, __( 'Insert selected', 'patternswp' ) )
2466 )
2467 )
2468 : null,
2469 error
2470 ? el(
2471 Notice,
2472 {
2473 status: 'error',
2474 isDismissible: true,
2475 onRemove: function () {
2476 setError( '' );
2477 },
2478 },
2479 error
2480 )
2481 : null,
2482 el(
2483 'div',
2484 {
2485 className: 'patternswp-grid-scroll',
2486 ref: scrollRef,
2487 },
2488 loading && isEmpty( visiblePatterns )
2489 ? el( 'div', { className: 'patternswp-loading' }, el( Spinner ), __( 'Loading patterns…', 'patternswp' ) )
2490 : null,
2491 ! loading && isEmpty( visiblePatterns )
2492 ? el(
2493 'div',
2494 { className: 'patternswp-empty' },
2495 el( Icon, { icon: iconOrDash( icons.search, 'search' ), size: 36 } ),
2496 el(
2497 'p',
2498 null,
2499 debouncedSearch
2500 ? __( 'No search results found.', 'patternswp' )
2501 : libraryComplete
2502 ? __( 'No patterns were found.', 'patternswp' )
2503 : __( 'Loading the pattern library…', 'patternswp' )
2504 )
2505 )
2506 : null,
2507 ! isEmpty( visiblePatterns )
2508 ? el(
2509 'div',
2510 { className: gridClass, role: 'list' },
2511 visiblePatterns.map( function ( pattern ) {
2512 var key = patternKey( pattern );
2513 return el( PatternCard, {
2514 key: key,
2515 pattern: pattern,
2516 isLicenseActive: isLicenseActive,
2517 isFavorite: favoriteKeys.has( key ),
2518 isSelected: !! selectedMap[ key ],
2519 bulkMode: bulkMode,
2520 viewportWidth: viewportWidth,
2521 scrollRoot: scrollRef.current,
2522 showTitle: showTitle,
2523 showPreview: showPreview,
2524 previewHeight: previewHeight,
2525 onInsert: insertPattern,
2526 onToggleFavorite: toggleFavorite,
2527 onToggleSelect: toggleSelect,
2528 onView: setPreviewPattern,
2529 } );
2530 } ),
2531 hasMore && ! isLocalCollection
2532 ? el(
2533 'div',
2534 { className: 'patternswp-sentinel', ref: sentinelRef },
2535 loadingMore ? el( Spinner ) : null
2536 )
2537 : null
2538 )
2539 : null
2540 )
2541 )
2542 )
2543 )
2544 );
2545 }
2546
2547 function PatternsModal( props ) {
2548 if ( ! props.isOpen ) {
2549 return null;
2550 }
2551 return el( PatternsLibrary, { onRequestClose: props.onRequestClose } );
2552 }
2553
2554 /* ------------------------------------------------------------------ */
2555 /* Pattern Library block (inserter / /patternswp) */
2556 /* ------------------------------------------------------------------ */
2557
2558 function InstantSearch( props ) {
2559 var clientId = props.clientId;
2560 var isLicenseActive = !! getBoot().isLicenseActive;
2561
2562 var queryState = useState( '' );
2563 var query = queryState[ 0 ];
2564 var setQuery = queryState[ 1 ];
2565
2566 var resultsState = useState( [] );
2567 var results = resultsState[ 0 ];
2568 var setResults = resultsState[ 1 ];
2569
2570 var loadingState = useState( false );
2571 var loading = loadingState[ 0 ];
2572 var setLoading = loadingState[ 1 ];
2573
2574 var openState = useState( false );
2575 var isOpen = openState[ 0 ];
2576 var setOpen = openState[ 1 ];
2577
2578 var wrapRef = useRef( null );
2579 var abortRef = useRef( null );
2580
2581 var insertPattern = useInsertPattern( function ( patterns ) {
2582 var list = Array.isArray( patterns ) ? patterns : [ patterns ];
2583 var prev = readStore( RECENT_KEY, [] );
2584 var next = prev.slice();
2585 list.forEach( function ( pattern ) {
2586 var key = patternKey( pattern );
2587 next = [ pattern ].concat(
2588 next.filter( function ( p ) {
2589 return patternKey( p ) !== key;
2590 } )
2591 );
2592 } );
2593 next = next.slice( 0, MAX_RECENT );
2594 writeStore( RECENT_KEY, next );
2595 setQuery( '' );
2596 setResults( [] );
2597 setOpen( false );
2598 } );
2599
2600 var runSearch = useCallback(
2601 function ( value ) {
2602 if ( abortRef.current ) {
2603 abortRef.current.abort();
2604 }
2605 var trimmed = ( value || '' ).trim();
2606 if ( ! trimmed ) {
2607 setResults( [] );
2608 setLoading( false );
2609 setOpen( false );
2610 return;
2611 }
2612
2613 var controller =
2614 typeof window.AbortController !== 'undefined'
2615 ? new window.AbortController()
2616 : null;
2617 abortRef.current = controller;
2618 setLoading( true );
2619 setOpen( true );
2620
2621 fetchPatternsPage(
2622 1,
2623 trimmed,
2624 '',
2625 undefined,
2626 controller ? controller.signal : undefined
2627 )
2628 .then( function ( payload ) {
2629 var data = payload && Array.isArray( payload.patterns ) ? payload.patterns : [];
2630 setResults( data.slice( 0, 8 ) );
2631 setLoading( false );
2632 } )
2633 .catch( function ( err ) {
2634 if ( err && err.name === 'AbortError' ) {
2635 return;
2636 }
2637 setResults( [] );
2638 setLoading( false );
2639 } );
2640 },
2641 []
2642 );
2643
2644 var debouncedSearch = useDebounce( runSearch, 300 );
2645
2646 useEffect(
2647 function () {
2648 return function () {
2649 if ( abortRef.current ) {
2650 abortRef.current.abort();
2651 }
2652 };
2653 },
2654 []
2655 );
2656
2657 useEffect(
2658 function () {
2659 function onDocClick( event ) {
2660 if ( wrapRef.current && ! wrapRef.current.contains( event.target ) ) {
2661 setOpen( false );
2662 }
2663 }
2664 document.addEventListener( 'mousedown', onDocClick );
2665 return function () {
2666 document.removeEventListener( 'mousedown', onDocClick );
2667 };
2668 },
2669 []
2670 );
2671
2672 function onChange( event ) {
2673 var value = event.target.value;
2674 setQuery( value );
2675 debouncedSearch( value );
2676 }
2677
2678 function onPick( pattern ) {
2679 if ( isPatternLocked( pattern ) ) {
2680 window.open( 'https://thepatternswp.com/pricing/', '_blank', 'noopener,noreferrer' );
2681 return;
2682 }
2683 insertPattern( pattern, { anchorClientId: clientId } );
2684 }
2685
2686 return el(
2687 'div',
2688 { className: 'patternswp-library-block__instant', ref: wrapRef },
2689 el(
2690 'div',
2691 { className: 'patternswp-library-block__instant-head' },
2692 el(
2693 'label',
2694 {
2695 className: 'patternswp-library-block__instant-label',
2696 htmlFor: 'patternswp-instant-search-' + clientId,
2697 },
2698 __( 'Instant Search', 'patternswp' )
2699 ),
2700 el(
2701 'p',
2702 { className: 'patternswp-library-block__instant-help' },
2703 __( 'Type to find a pattern and insert it here.', 'patternswp' )
2704 )
2705 ),
2706 el(
2707 'div',
2708 { className: 'patternswp-library-block__search-wrap' },
2709 el( 'input', {
2710 id: 'patternswp-instant-search-' + clientId,
2711 type: 'search',
2712 className: 'patternswp-library-block__search-input',
2713 placeholder: __( 'Search patterns…', 'patternswp' ),
2714 value: query,
2715 onChange: onChange,
2716 onFocus: function () {
2717 if ( results.length || loading ) {
2718 setOpen( true );
2719 }
2720 },
2721 autoComplete: 'off',
2722 } ),
2723 el(
2724 'span',
2725 { className: 'patternswp-library-block__search-icon', 'aria-hidden': true },
2726 loading
2727 ? el( Spinner )
2728 : el( Icon, { icon: iconOrDash( icons.search, 'search' ) } )
2729 ),
2730 isOpen && ( loading || results.length || query.trim() )
2731 ? el(
2732 'div',
2733 {
2734 className: 'patternswp-library-block__results',
2735 role: 'listbox',
2736 'aria-label': __( 'Pattern search results', 'patternswp' ),
2737 },
2738 ! loading && ! results.length
2739 ? el(
2740 'div',
2741 { className: 'patternswp-library-block__results-empty' },
2742 __( 'No patterns found.', 'patternswp' )
2743 )
2744 : null,
2745 results.map( function ( pattern ) {
2746 var locked = isPatternLocked( pattern );
2747 return el(
2748 'button',
2749 {
2750 type: 'button',
2751 key: patternKey( pattern ),
2752 role: 'option',
2753 className:
2754 'patternswp-library-block__result' +
2755 ( locked ? ' is-locked' : '' ),
2756 onClick: function () {
2757 onPick( pattern );
2758 },
2759 },
2760 el(
2761 'span',
2762 { className: 'patternswp-library-block__result-title' },
2763 pattern.title || __( 'Untitled pattern', 'patternswp' )
2764 ),
2765 pattern.type === 'pro' || pattern.locked
2766 ? el(
2767 'span',
2768 { className: 'patternswp-library-block__result-badge' },
2769 __( 'Pro', 'patternswp' )
2770 )
2771 : null
2772 );
2773 } )
2774 )
2775 : null
2776 )
2777 );
2778 }
2779
2780 function PatternsLibraryBlockEdit( props ) {
2781 var clientId = props.clientId;
2782 var localOpenState = useState( false );
2783 var localOpen = localOpenState[ 0 ];
2784 var setLocalOpen = localOpenState[ 1 ];
2785 var removeBlock = useDispatch( storeBlockEditor ).removeBlock;
2786
2787 function openLibrary() {
2788 var opened = openPatternsLibrary( { anchorClientId: clientId } );
2789 if ( ! opened ) {
2790 setInsertAnchor( clientId );
2791 setLocalOpen( true );
2792 }
2793 }
2794
2795 function closeLocal() {
2796 setLocalOpen( false );
2797 setInsertAnchor( null );
2798 }
2799
2800 function removeLibraryBlock() {
2801 setInsertAnchor( null );
2802 if ( removeBlock ) {
2803 removeBlock( clientId );
2804 }
2805 }
2806
2807 return el(
2808 'div',
2809 { className: 'patternswp-library-block' },
2810 el(
2811 Button,
2812 {
2813 className: 'patternswp-library-block__close',
2814 icon: iconOrDash( icons.close, 'no-alt' ),
2815 label: __( 'Remove PatternsWP Pattern Library', 'patternswp' ),
2816 showTooltip: true,
2817 onClick: removeLibraryBlock,
2818 }
2819 ),
2820 el(
2821 'div',
2822 { className: 'patternswp-library-block__inner' },
2823 el(
2824 'div',
2825 { className: 'patternswp-library-block__header' },
2826 el(
2827 'span',
2828 { className: 'patternswp-library-block__logo', 'aria-hidden': true },
2829 el( Icon, { icon: iconOrDash( icons.brand, 'layout' ) } )
2830 ),
2831 el(
2832 'h2',
2833 { className: 'patternswp-library-block__title' },
2834 __( 'PatternsWP Pattern Library', 'patternswp' )
2835 ),
2836 el(
2837 'p',
2838 { className: 'patternswp-library-block__desc' },
2839 __( 'Search and insert patterns into this page.', 'patternswp' )
2840 )
2841 ),
2842 el( InstantSearch, { clientId: clientId } ),
2843 el(
2844 'div',
2845 { className: 'patternswp-library-block__browse' },
2846 el(
2847 'button',
2848 {
2849 type: 'button',
2850 className: 'patternswp-library-block__browse-link',
2851 onClick: openLibrary,
2852 },
2853 __( 'Or browse the full library', 'patternswp' )
2854 )
2855 )
2856 ),
2857 el( PatternsModal, {
2858 isOpen: localOpen,
2859 onRequestClose: closeLocal,
2860 } )
2861 );
2862 }
2863
2864 if ( registerBlockType ) {
2865 registerBlockType( 'patternswp/library', {
2866 apiVersion: 3,
2867 title: __( 'PatternsWP Pattern Library', 'patternswp' ),
2868 description: __(
2869 'Browse the PatternsWP pattern library and insert patterns into your page.',
2870 'patternswp'
2871 ),
2872 category: 'widgets',
2873 icon: iconOrDash( icons.brand, 'layout' ),
2874 keywords: [ 'patternswp', 'patterns', 'library', 'templates', 'blocks' ],
2875 supports: {
2876 html: false,
2877 multiple: true,
2878 reusable: false,
2879 },
2880 edit: PatternsLibraryBlockEdit,
2881 save: function () {
2882 return null;
2883 },
2884 } );
2885 }
2886
2887 /* ------------------------------------------------------------------ */
2888 /* Header toolbar + canvas block inserter (+) icon */
2889 /* ------------------------------------------------------------------ */
2890
2891 var HEADER_SLOT_CLASS = 'patternswp-header-toolbar-slot';
2892 var HEADER_BUTTON_CLASS = 'patternswp-toolbar-button';
2893 var CANVAS_BUTTON_CLASS = 'patternswp-appender-button';
2894 var CANVAS_INSERTER_SELECTOR =
2895 '.components-dropdown.block-editor-inserter, .block-editor-inserter, .block-editor-inserter__toggle, .block-editor-button-block-appender, .block-list-appender__toggle, button[aria-label="Add block"]';
2896 var toolbarOpenRef = { current: null };
2897
2898 var CANVAS_BRAND_SVG =
2899 '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 25 24" width="24" height="24" fill="currentColor" aria-hidden="true" focusable="false">' +
2900 '<path d="M9.34 24L4.45 21.16L4.03 20.92V15.3L9.34 12V24Z"></path>' +
2901 '<path fill-rule="evenodd" clip-rule="evenodd" d="M14.66 9.2L20.97 6L14.66 2.8V9.2Z"></path>' +
2902 '<path d="M14.66 2.8L9.34 6L4.03 9.2V2.8L9.34 0L14.66 2.8Z"></path>' +
2903 '<path fill-rule="evenodd" clip-rule="evenodd" d="M10.79 11.44V17.84L14.66 15.3L20.97 12V6L14.66 9.2L10.79 11.44Z"></path>' +
2904 '</svg>';
2905
2906 function openPatternsLibraryFromChrome() {
2907 if ( toolbarOpenRef.current ) {
2908 toolbarOpenRef.current();
2909 } else if ( openModalExternal ) {
2910 openModalExternal();
2911 }
2912 }
2913
2914 function getHeaderToolbarLeft() {
2915 return (
2916 document.querySelector( '.editor-document-tools__left' ) ||
2917 document.querySelector( '.edit-post-header-toolbar__left' )
2918 );
2919 }
2920
2921 var editorChrome = ( function () {
2922 var started = false;
2923 var syncTimer = null;
2924 var storeUnsub = null;
2925 var iframeWatch = null;
2926 var lastSelection = null;
2927 var headerLeftObservers = new WeakMap();
2928 var canvasInjectTimer = null;
2929 var canvasPollTimer = null;
2930
2931 function getCanvasDocuments() {
2932 var docs = [];
2933
2934 document
2935 .querySelectorAll( 'iframe' )
2936 .forEach( function ( iframe ) {
2937 try {
2938 if ( iframe.contentDocument && iframe.contentDocument.body ) {
2939 var doc = iframe.contentDocument;
2940 if (
2941 doc.querySelector( '.block-editor-block-list__layout' ) ||
2942 doc.querySelector( '.editor-styles-wrapper' ) ||
2943 doc.body.classList.contains( 'block-editor-page' ) ||
2944 iframe.name === 'editor-canvas'
2945 ) {
2946 docs.push( doc );
2947 }
2948 }
2949 } catch ( e ) {}
2950 } );
2951
2952 // Always include the main document, because Gutenberg renders side inserters
2953 // in the main document DOM even if the block list is inside an iframe.
2954 if ( docs.indexOf( document ) === -1 ) {
2955 docs.push( document );
2956 }
2957
2958 return docs;
2959 }
2960
2961 function canvasNeedsInjection() {
2962 var docs = getCanvasDocuments();
2963 for ( var d = 0; d < docs.length; d++ ) {
2964 var inserters = docs[ d ].querySelectorAll(
2965 CANVAS_INSERTER_SELECTOR
2966 );
2967 for ( var i = 0; i < inserters.length; i++ ) {
2968 var target = inserters[ i ];
2969 var parent = ( target.classList.contains('block-editor-inserter') || target.classList.contains('components-dropdown') ) ? target : target.parentNode;
2970 if (
2971 parent &&
2972 ! parent.querySelector( '.' + CANVAS_BUTTON_CLASS )
2973 ) {
2974 return true;
2975 }
2976 }
2977 }
2978 return false;
2979 }
2980
2981 function injectCanvasIntoDropdown( target ) {
2982 if ( ! target || ! target.isConnected || target.classList.contains(CANVAS_BUTTON_CLASS) ) {
2983 return;
2984 }
2985
2986 var parent;
2987 var toggleBtn;
2988
2989 if ( target.classList.contains('block-editor-inserter') || target.classList.contains('components-dropdown') ) {
2990 parent = target;
2991 toggleBtn = target.querySelector('.block-editor-inserter__toggle') || target.querySelector('button');
2992 } else {
2993 parent = target.parentNode;
2994 toggleBtn = target;
2995 }
2996
2997 // Do not inject if it's the top header toolbar (handled by syncHeaderToolbar)
2998 if ( parent && (parent.closest('.edit-post-header-toolbar') || parent.closest('.editor-document-tools')) ) {
2999 return;
3000 }
3001
3002 // Do not inject next to the inline default block appender (e.g. empty paragraph block)
3003 if ( parent && parent.closest('.block-editor-default-block-appender') ) {
3004 return;
3005 }
3006
3007 if ( ! parent || parent.querySelector( '.' + CANVAS_BUTTON_CLASS ) ) {
3008 return;
3009 }
3010
3011 /* Copy the computed style from the + inserter toggle so we match it */
3012 var bgColor = '#1e1e1e';
3013 var btnSize = '24px';
3014 var borderRadius = '2px';
3015
3016 try {
3017 if ( toggleBtn ) {
3018 var cs = toggleBtn.ownerDocument.defaultView.getComputedStyle( toggleBtn );
3019 if ( cs.backgroundColor && cs.backgroundColor !== 'rgba(0, 0, 0, 0)' && cs.backgroundColor !== 'transparent' ) {
3020 bgColor = cs.backgroundColor;
3021 }
3022 if ( cs.height && cs.height !== 'auto' && cs.height !== '0px' ) {
3023 btnSize = cs.height;
3024 }
3025 if ( cs.borderRadius ) {
3026 borderRadius = cs.borderRadius;
3027 }
3028 }
3029 } catch ( e ) {}
3030
3031 var btn = parent.ownerDocument.createElement( 'button' );
3032 btn.type = 'button';
3033 btn.setAttribute( 'aria-haspopup', 'true' );
3034 btn.className =
3035 'components-button block-editor-inserter__toggle has-icon ' +
3036 CANVAS_BUTTON_CLASS;
3037 btn.setAttribute(
3038 'aria-label',
3039 __( 'PatternsWP Library', 'patternswp' )
3040 );
3041 btn.innerHTML = CANVAS_BRAND_SVG;
3042 btn.style.cssText =
3043 'display:inline-flex;align-items:center;justify-content:center;' +
3044 'padding:0;width:' + btnSize + ';height:' + btnSize + ';border:none;' +
3045 'background:' + bgColor + ';color:#fff;cursor:pointer;' +
3046 'flex-shrink:0;border-radius:' + borderRadius + ';';
3047 btn.addEventListener( 'click', function ( event ) {
3048 event.preventDefault();
3049 event.stopPropagation();
3050 openPatternsLibraryFromChrome();
3051 } );
3052
3053 // Do not break Gutenberg layout by overriding parent display.
3054 // Just ensure our button is inline and has a margin to separate it.
3055 parent.style.display = 'flex';
3056 parent.style.gap = '5px';
3057
3058 // Insert after the toggle button if it exists
3059 if ( toggleBtn && toggleBtn.nextSibling ) {
3060 parent.insertBefore( btn, toggleBtn.nextSibling );
3061 } else {
3062 parent.appendChild( btn );
3063 }
3064 }
3065
3066 /**
3067 * Inject a minimal inline <style> into an iframe/document so the
3068 * appender button is visible even if the main CSS fails to load.
3069 * Styled to match the + inserter: white icon on dark background.
3070 */
3071 var styledDocs = new WeakSet();
3072 function ensureCanvasStyles( doc ) {
3073 if ( ! doc || ! doc.head || styledDocs.has( doc ) ) {
3074 return;
3075 }
3076 styledDocs.add( doc );
3077 var style = doc.createElement( 'style' );
3078 style.textContent =
3079 '.' + CANVAS_BUTTON_CLASS + '{' +
3080 'display:inline-flex!important;align-items:center;justify-content:center;' +
3081 'padding:0;min-width:24px;border:none;' +
3082 'background:#1e1e1e;color:#fff;cursor:pointer;' +
3083 'flex-shrink:0;border-radius:2px;}' +
3084 '.' + CANVAS_BUTTON_CLASS + ' svg{width:20px!important;height:20px!important;fill:#fff!important;}' +
3085 '.' + CANVAS_BUTTON_CLASS + ':hover{background:#000;color:#fff;}' +
3086 '.' + CANVAS_BUTTON_CLASS + ':focus{box-shadow:inset 0 0 0 var(--wp-admin-border-width-focus,1.5px) var(--wp-admin-theme-color,#3858e9);outline:1px solid transparent;}';
3087 doc.head.appendChild( style );
3088 }
3089
3090 function injectAllCanvasButtons() {
3091 var docs = getCanvasDocuments();
3092 for ( var d = 0; d < docs.length; d++ ) {
3093 ensureCanvasStyles( docs[ d ] );
3094 var inserters = docs[ d ].querySelectorAll(
3095 CANVAS_INSERTER_SELECTOR
3096 );
3097 for ( var i = 0; i < inserters.length; i++ ) {
3098 injectCanvasIntoDropdown( inserters[ i ] );
3099 }
3100 }
3101 }
3102
3103 function scheduleCanvasInject() {
3104 if ( canvasInjectTimer ) {
3105 window.clearTimeout( canvasInjectTimer );
3106 }
3107 canvasInjectTimer = window.setTimeout( function () {
3108 canvasInjectTimer = null;
3109 injectAllCanvasButtons();
3110 }, 120 );
3111 }
3112
3113 function observeHeaderToolbarLeft( leftGroup ) {
3114 if ( ! leftGroup || headerLeftObservers.has( leftGroup ) ) {
3115 return;
3116 }
3117
3118 var observer = new MutationObserver( function () {
3119 if (
3120 leftGroup.isConnected &&
3121 ! leftGroup.querySelector( '.' + HEADER_BUTTON_CLASS )
3122 ) {
3123 syncHeaderToolbar();
3124 }
3125 } );
3126 observer.observe( leftGroup, { childList: true } );
3127 headerLeftObservers.set( leftGroup, observer );
3128 }
3129
3130 function isHeaderToolbarMounted() {
3131 var leftGroup = getHeaderToolbarLeft();
3132 return !!(
3133 leftGroup &&
3134 leftGroup.querySelector( '.' + HEADER_BUTTON_CLASS )
3135 );
3136 }
3137
3138 function syncHeaderToolbar() {
3139 var leftGroup = getHeaderToolbarLeft();
3140 if ( ! leftGroup || leftGroup.querySelector( '.' + HEADER_BUTTON_CLASS ) ) {
3141 return;
3142 }
3143
3144 var btn = leftGroup.ownerDocument.createElement( 'button' );
3145 btn.type = 'button';
3146 btn.className =
3147 'components-button is-tertiary has-icon ' + HEADER_BUTTON_CLASS;
3148 btn.setAttribute(
3149 'aria-label',
3150 __( 'PatternsWP Library', 'patternswp' )
3151 );
3152 btn.innerHTML =
3153 CANVAS_BRAND_SVG +
3154 '<span class="patternswp-toolbar-button__label">' +
3155 __( 'PatternsWP Library', 'patternswp' ) +
3156 '</span>';
3157 btn.addEventListener( 'click', function ( event ) {
3158 event.preventDefault();
3159 openPatternsLibraryFromChrome();
3160 } );
3161
3162 leftGroup.appendChild( btn );
3163 observeHeaderToolbarLeft( leftGroup );
3164 }
3165
3166 function syncAll() {
3167 syncHeaderToolbar();
3168 getCanvasDocuments().forEach( observeCanvasBlockList );
3169 injectAllCanvasButtons();
3170 }
3171
3172 function scheduleSync( followUpMs ) {
3173 if ( syncTimer ) {
3174 window.clearTimeout( syncTimer );
3175 }
3176 syncTimer = window.setTimeout( function () {
3177 syncTimer = null;
3178 syncAll();
3179 }, 100 );
3180
3181 if ( followUpMs ) {
3182 window.setTimeout( syncAll, followUpMs );
3183 }
3184 }
3185
3186 var canvasObservers = new WeakMap();
3187
3188 function observeCanvasBlockList( doc ) {
3189 if ( ! doc || ! doc.body ) {
3190 return;
3191 }
3192
3193 var existing = canvasObservers.get( doc );
3194 if (
3195 existing &&
3196 existing._pwpRoot &&
3197 existing._pwpRoot.isConnected
3198 ) {
3199 return;
3200 }
3201
3202 if ( existing ) {
3203 existing.disconnect();
3204 }
3205
3206 var root =
3207 doc.querySelector( '.block-editor-block-list__layout' ) ||
3208 doc.querySelector( '.editor-styles-wrapper' ) ||
3209 doc.body;
3210
3211 if ( ! root ) {
3212 return;
3213 }
3214
3215 var observer = new MutationObserver( function () {
3216 scheduleCanvasInject();
3217 } );
3218 observer._pwpRoot = root;
3219
3220 observer.observe( root, {
3221 childList: true,
3222 subtree: true,
3223 } );
3224
3225 canvasObservers.set( doc, observer );
3226 scheduleCanvasInject();
3227 }
3228
3229 function bindCanvasInteractionSync( doc ) {
3230 if ( ! doc || ! doc.body ) {
3231 return;
3232 }
3233 observeCanvasBlockList( doc );
3234 if ( doc.body.getAttribute( 'data-patternswp-click-bound' ) === 'true' ) {
3235 return;
3236 }
3237 doc.body.setAttribute( 'data-patternswp-click-bound', 'true' );
3238 doc.body.addEventListener(
3239 'click',
3240 function () {
3241 scheduleSync( 280 );
3242 },
3243 true
3244 );
3245 /* Re-inject on mouseover so buttons survive React re-renders */
3246 doc.body.addEventListener(
3247 'mouseover',
3248 function () {
3249 if ( canvasNeedsInjection() ) {
3250 scheduleCanvasInject();
3251 }
3252 },
3253 true
3254 );
3255 }
3256
3257 function bindEditorIframe( iframe ) {
3258 if ( ! iframe || iframe.getAttribute( 'data-patternswp-bound' ) === 'true' ) {
3259 return;
3260 }
3261 iframe.setAttribute( 'data-patternswp-bound', 'true' );
3262
3263 function onIframeReady() {
3264 try {
3265 if ( iframe.contentDocument ) {
3266 bindCanvasInteractionSync( iframe.contentDocument );
3267 }
3268 } catch ( e ) {}
3269 scheduleSync( 320 );
3270 }
3271
3272 iframe.addEventListener( 'load', onIframeReady );
3273 onIframeReady();
3274 }
3275
3276 function watchForEditorIframe() {
3277 document
3278 .querySelectorAll( 'iframe' )
3279 .forEach( bindEditorIframe );
3280
3281 if ( iframeWatch ) {
3282 return;
3283 }
3284
3285 iframeWatch = new MutationObserver( function ( mutations ) {
3286 for ( var i = 0; i < mutations.length; i++ ) {
3287 var mutation = mutations[ i ];
3288 for ( var j = 0; j < mutation.addedNodes.length; j++ ) {
3289 var node = mutation.addedNodes[ j ];
3290 if ( node.nodeType !== 1 ) {
3291 continue;
3292 }
3293 if (
3294 node.tagName && node.tagName.toLowerCase() === 'iframe'
3295 ) {
3296 bindEditorIframe( node );
3297 } else if (
3298 node.querySelectorAll
3299 ) {
3300 node
3301 .querySelectorAll( 'iframe' )
3302 .forEach( bindEditorIframe );
3303 }
3304 }
3305 }
3306 } );
3307 iframeWatch.observe( document.body, { childList: true, subtree: true } );
3308 }
3309
3310 var headerBootObserver = null;
3311 var headerBootTimeoutId = null;
3312
3313 function watchHeaderUntilMounted() {
3314 if ( isHeaderToolbarMounted() ) {
3315 return;
3316 }
3317
3318 syncHeaderToolbar();
3319 if ( isHeaderToolbarMounted() ) {
3320 return;
3321 }
3322
3323 if ( headerBootObserver ) {
3324 return;
3325 }
3326
3327 var retryTimer = null;
3328 function retryHeaderMount() {
3329 if ( retryTimer ) {
3330 window.clearTimeout( retryTimer );
3331 }
3332 retryTimer = window.setTimeout( function () {
3333 retryTimer = null;
3334 syncHeaderToolbar();
3335 if ( isHeaderToolbarMounted() && headerBootObserver ) {
3336 headerBootObserver.disconnect();
3337 headerBootObserver = null;
3338 }
3339 }, 80 );
3340 }
3341
3342 headerBootObserver = new MutationObserver( retryHeaderMount );
3343 headerBootObserver.observe( document.body, {
3344 childList: true,
3345 subtree: true,
3346 } );
3347
3348 headerBootTimeoutId = window.setTimeout( function () {
3349 if ( headerBootObserver ) {
3350 headerBootObserver.disconnect();
3351 headerBootObserver = null;
3352 }
3353 }, 20000 );
3354 }
3355
3356 function start() {
3357 scheduleSync( 350 );
3358
3359 if ( started ) {
3360 watchHeaderUntilMounted();
3361 return;
3362 }
3363 started = true;
3364
3365 bindCanvasInteractionSync( document );
3366 watchForEditorIframe();
3367 watchHeaderUntilMounted();
3368
3369 [ 0, 200, 600, 1500, 3000, 5000 ].forEach( function ( delay ) {
3370 window.setTimeout( function () {
3371 syncAll();
3372 watchHeaderUntilMounted();
3373 }, delay );
3374 } );
3375
3376 function pollCanvasButtons( attemptsLeft ) {
3377 injectAllCanvasButtons();
3378 if ( canvasNeedsInjection() && attemptsLeft > 0 ) {
3379 canvasPollTimer = window.setTimeout( function () {
3380 pollCanvasButtons( attemptsLeft - 1 );
3381 }, 200 );
3382 }
3383 }
3384 pollCanvasButtons( 30 );
3385
3386 /* Persistent heartbeat: re-check every 800ms so the P button
3387 * always comes back if React removes it during re-renders. */
3388 var heartbeatId = window.setInterval( function () {
3389 if ( canvasNeedsInjection() || ! isHeaderToolbarMounted() ) {
3390 syncAll();
3391 }
3392 }, 800 );
3393
3394 if ( subscribe && storeBlockEditor ) {
3395 var lastHeaderCheck = 0;
3396 var lastBlockCount = null;
3397 storeUnsub = subscribe( function () {
3398 var select = wp.data.select( storeBlockEditor );
3399 if ( ! select || ! select.getBlockSelectionStart ) {
3400 return;
3401 }
3402 var selection = select.getBlockSelectionStart();
3403 var selectionChanged = selection !== lastSelection;
3404 var blockCount =
3405 select.getBlockCount && select.getBlockCount();
3406 var blockCountChanged = blockCount !== lastBlockCount;
3407 var canvasNeedsSync = canvasNeedsInjection();
3408 var headerMissing = ! isHeaderToolbarMounted();
3409 var now = Date.now();
3410
3411 if (
3412 ! selectionChanged &&
3413 ! blockCountChanged &&
3414 ! canvasNeedsSync &&
3415 ! ( headerMissing && now - lastHeaderCheck > 1500 )
3416 ) {
3417 return;
3418 }
3419
3420 if ( selectionChanged ) {
3421 lastSelection = selection;
3422 }
3423 if ( blockCountChanged ) {
3424 lastBlockCount = blockCount;
3425 }
3426 if ( headerMissing ) {
3427 lastHeaderCheck = now;
3428 watchHeaderUntilMounted();
3429 }
3430 scheduleSync( 320 );
3431 } );
3432 }
3433 }
3434
3435 return {
3436 start: start,
3437 scheduleSync: scheduleSync,
3438 };
3439 } )();
3440
3441 if ( document.readyState === 'loading' ) {
3442 document.addEventListener( 'DOMContentLoaded', function () {
3443 editorChrome.start();
3444 } );
3445 } else {
3446 editorChrome.start();
3447 }
3448
3449 function HeaderToolbarBridge() {
3450 useEffect(
3451 function () {
3452 editorChrome.start();
3453 editorChrome.scheduleSync( 400 );
3454 return function () {};
3455 },
3456 []
3457 );
3458 return null;
3459 }
3460
3461 function PatternsWPModalHost() {
3462 var openState = useState( false );
3463 var isOpen = openState[ 0 ];
3464 var setOpen = openState[ 1 ];
3465
3466 useEffect(
3467 function () {
3468 function openModalHandler() {
3469 setOpen( true );
3470 }
3471 openModalExternal = openModalHandler;
3472 toolbarOpenRef.current = openModalHandler;
3473 editorChrome.scheduleSync();
3474 return function () {
3475 openModalExternal = null;
3476 toolbarOpenRef.current = null;
3477 };
3478 },
3479 []
3480 );
3481
3482 function openModal() {
3483 setOpen( true );
3484 }
3485
3486 function closeModal() {
3487 setOpen( false );
3488 setInsertAnchor( null );
3489 }
3490
3491 var brandIcon = iconOrDash( icons.brand, 'layout' );
3492
3493 return el(
3494 Fragment,
3495 null,
3496 el( HeaderToolbarBridge, null ),
3497 PluginMoreMenuItem
3498 ? el(
3499 PluginMoreMenuItem,
3500 {
3501 icon: brandIcon,
3502 onClick: openModal,
3503 },
3504 __( 'PatternsWP Library', 'patternswp' )
3505 )
3506 : null,
3507 el( PatternsModal, {
3508 isOpen: isOpen,
3509 onRequestClose: closeModal,
3510 } )
3511 );
3512 }
3513
3514 if ( registerPlugin ) {
3515 registerPlugin( 'patternswp-library', {
3516 render: PatternsWPModalHost,
3517 icon: iconOrDash( icons.brand, 'layout' ),
3518 } );
3519 }
3520 } )( window.wp, window.lodash || {} );
3521