40 | * @see https://www.php-fig.org/psr/psr-0/
41 | * @see https://www.php-fig.org/psr/psr-4/
42 | */
43 | class ClassLoader
44 | {
45 | private $vendorDir;
46 |
47 | // PSR-4
48 | private $prefixLengthsPsr4 = array();
49 | private $prefixDirsPsr4 = array();
50 | private $fallbackDirsPsr4 = array();
51 |
52 | // PSR-0
53 | private $prefixesPsr0 = array();
54 | private $fallbackDirsPsr0 = array();
55 |
56 | private $useIncludePath = false;
57 | private $classMap = array();
58 | private $classMapAuthoritative = false;
59 | private $missingClasses = array();
60 | private $apcuPrefix;
61 |
62 | private static $registeredLoaders = array();
63 |
64 | public function __construct($vendorDir = null)
65 | {
66 | $this->vendorDir = $vendorDir;
67 | }
68 |
69 | public function getPrefixes()
70 | {
71 | if (!empty($this->prefixesPsr0)) {
72 | return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
73 | }
74 |
75 | return array();
76 | }
77 |
78 | public function getPrefixesPsr4()
79 | {
80 | return $this->prefixDirsPsr4;
81 | }
82 |
83 | public function getFallbackDirs()
84 | {
85 | return $this->fallbackDirsPsr0;
86 | }
87 |
88 | public function getFallbackDirsPsr4()
89 | {
90 | return $this->fallbackDirsPsr4;
91 | }
92 |
93 | public function getClassMap()
94 | {
95 | return $this->classMap;
96 | }
97 |
98 | /**
99 | * @param array $classMap Class to filename map
100 | */
101 | public function addClassMap(array $classMap)
102 | {
103 | if ($this->classMap) {
104 | $this->classMap = array_merge($this->classMap, $classMap);
105 | } else {
106 | $this->classMap = $classMap;
107 | }
108 | }
109 |
110 | /**
111 | * Registers a set of PSR-0 directories for a given prefix, either
112 | * appending or prepending to the ones previously set for this prefix.
113 | *
114 | * @param string $prefix The prefix
115 | * @param array|string $paths The PSR-0 root directories
116 | * @param bool $prepend Whether to prepend the directories
117 | */
118 | public function add($prefix, $paths, $prepend = false)
119 | {
120 | if (!$prefix) {
121 | if ($prepend) {
122 | $this->fallbackDirsPsr0 = array_merge(
123 | (array) $paths,
124 | $this->fallbackDirsPsr0
125 | );
126 | } else {
127 | $this->fallbackDirsPsr0 = array_merge(
128 | $this->fallbackDirsPsr0,
129 | (array) $paths
130 | );
131 | }
132 |
133 | return;
134 | }
135 |
136 | $first = $prefix[0];
137 | if (!isset($this->prefixesPsr0[$first][$prefix])) {
138 | $this->prefixesPsr0[$first][$prefix] = (array) $paths;
139 |
140 | return;
141 | }
142 | if ($prepend) {
143 | $this->prefixesPsr0[$first][$prefix] = array_merge(
144 | (array) $paths,
145 | $this->prefixesPsr0[$first][$prefix]
146 | );
147 | } else {
148 | $this->prefixesPsr0[$first][$prefix] = array_merge(
149 | $this->prefixesPsr0[$first][$prefix],
150 | (array) $paths
151 | );
152 | }
153 | }
154 |
155 | /**
156 | * Registers a set of PSR-4 directories for a given namespace, either
157 | * appending or prepending to the ones previously set for this namespace.
158 | *
159 | * @param string $prefix The prefix/namespace, with trailing '\\'
160 | * @param array|string $paths The PSR-4 base directories
161 | * @param bool $prepend Whether to prepend the directories
162 | *
163 | * @throws \InvalidArgumentException
164 | */
165 | public function addPsr4($prefix, $paths, $prepend = false)
166 | {
167 | if (!$prefix) {
168 | // Register directories for the root namespace.
169 | if ($prepend) {
170 | $this->fallbackDirsPsr4 = array_merge(
171 | (array) $paths,
172 | $this->fallbackDirsPsr4
173 | );
174 | } else {
175 | $this->fallbackDirsPsr4 = array_merge(
176 | $this->fallbackDirsPsr4,
177 | (array) $paths
178 | );
179 | }
180 | } elseif (!isset($this->prefixDirsPsr4[$prefix])) {
181 | // Register directories for a new namespace.
182 | $length = strlen($prefix);
183 | if ('\\' !== $prefix[$length - 1]) {
184 | throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
185 | }
186 | $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
187 | $this->prefixDirsPsr4[$prefix] = (array) $paths;
188 | } elseif ($prepend) {
189 | // Prepend directories for an already registered namespace.
190 | $this->prefixDirsPsr4[$prefix] = array_merge(
191 | (array) $paths,
192 | $this->prefixDirsPsr4[$prefix]
193 | );
194 | } else {
195 | // Append directories for an already registered namespace.
196 | $this->prefixDirsPsr4[$prefix] = array_merge(
197 | $this->prefixDirsPsr4[$prefix],
198 | (array) $paths
199 | );
200 | }
201 | }
202 |
203 | /**
204 | * Registers a set of PSR-0 directories for a given prefix,
205 | * replacing any others previously set for this prefix.
206 | *
207 | * @param string $prefix The prefix
208 | * @param array|string $paths The PSR-0 base directories
209 | */
210 | public function set($prefix, $paths)
211 | {
212 | if (!$prefix) {
213 | $this->fallbackDirsPsr0 = (array) $paths;
214 | } else {
215 | $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
216 | }
217 | }
218 |
219 | /**
220 | * Registers a set of PSR-4 directories for a given namespace,
221 | * replacing any others previously set for this namespace.
222 | *
223 | * @param string $prefix The prefix/namespace, with trailing '\\'
224 | * @param array|string $paths The PSR-4 base directories
225 | *
226 | * @throws \InvalidArgumentException
227 | */
228 | public function setPsr4($prefix, $paths)
229 | {
230 | if (!$prefix) {
231 | $this->fallbackDirsPsr4 = (array) $paths;
232 | } else {
233 | $length = strlen($prefix);
234 | if ('\\' !== $prefix[$length - 1]) {
235 | throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
236 | }
237 | $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
238 | $this->prefixDirsPsr4[$prefix] = (array) $paths;
239 | }
240 | }
241 |
242 | /**
243 | * Turns on searching the include path for class files.
244 | *
245 | * @param bool $useIncludePath
246 | */
247 | public function setUseIncludePath($useIncludePath)
248 | {
249 | $this->useIncludePath = $useIncludePath;
250 | }
251 |
252 | /**
253 | * Can be used to check if the autoloader uses the include path to check
254 | * for classes.
255 | *
256 | * @return bool
257 | */
258 | public function getUseIncludePath()
259 | {
260 | return $this->useIncludePath;
261 | }
262 |
263 | /**
264 | * Turns off searching the prefix and fallback directories for classes
265 | * that have not been registered with the class map.
266 | *
267 | * @param bool $classMapAuthoritative
268 | */
269 | public function setClassMapAuthoritative($classMapAuthoritative)
270 | {
271 | $this->classMapAuthoritative = $classMapAuthoritative;
272 | }
273 |
274 | /**
275 | * Should class lookup fail if not found in the current class map?
276 | *
277 | * @return bool
278 | */
279 | public function isClassMapAuthoritative()
280 | {
281 | return $this->classMapAuthoritative;
282 | }
283 |
284 | /**
285 | * APCu prefix to use to cache found/not-found classes, if the extension is enabled.
286 | *
287 | * @param string|null $apcuPrefix
288 | */
289 | public function setApcuPrefix($apcuPrefix)
290 | {
291 | $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
292 | }
293 |
294 | /**
295 | * The APCu prefix in use, or null if APCu caching is not enabled.
296 | *
297 | * @return string|null
298 | */
299 | public function getApcuPrefix()
300 | {
301 | return $this->apcuPrefix;
302 | }
303 |
304 | /**
305 | * Registers this instance as an autoloader.
306 | *
307 | * @param bool $prepend Whether to prepend the autoloader or not
308 | */
309 | public function register($prepend = false)
310 | {
311 | spl_autoload_register(array($this, 'loadClass'), true, $prepend);
312 |
313 | if (null === $this->vendorDir) {
314 | //no-op
315 | } elseif ($prepend) {
316 | self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
317 | } else {
318 | unset(self::$registeredLoaders[$this->vendorDir]);
319 | self::$registeredLoaders[$this->vendorDir] = $this;
320 | }
321 | }
322 |
323 | /**
324 | * Unregisters this instance as an autoloader.
325 | */
326 | public function unregister()
327 | {
328 | spl_autoload_unregister(array($this, 'loadClass'));
329 |
330 | if (null !== $this->vendorDir) {
331 | unset(self::$registeredLoaders[$this->vendorDir]);
332 | }
333 | }
334 |
335 | /**
336 | * Loads the given class or interface.
337 | *
338 | * @param string $class The name of the class
339 | * @return bool|null True if loaded, null otherwise
340 | */
341 | public function loadClass($class)
342 | {
343 | if ($file = $this->findFile($class)) {
344 | includeFile($file);
345 |
346 | return true;
347 | }
348 | }
349 |
350 | /**
351 | * Finds the path to the file where the class is defined.
352 | *
353 | * @param string $class The name of the class
354 | *
355 | * @return string|false The path if found, false otherwise
356 | */
357 | public function findFile($class)
358 | {
359 | // class map lookup
360 | if (isset($this->classMap[$class])) {
361 | return $this->classMap[$class];
362 | }
363 | if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
364 | return false;
365 | }
366 | if (null !== $this->apcuPrefix) {
367 | $file = apcu_fetch($this->apcuPrefix.$class, $hit);
368 | if ($hit) {
369 | return $file;
370 | }
371 | }
372 |
373 | $file = $this->findFileWithExtension($class, '.php');
374 |
375 | // Search for Hack files if we are running on HHVM
376 | if (false === $file && defined('HHVM_VERSION')) {
377 | $file = $this->findFileWithExtension($class, '.hh');
378 | }
379 |
380 | if (null !== $this->apcuPrefix) {
381 | apcu_add($this->apcuPrefix.$class, $file);
382 | }
383 |
384 | if (false === $file) {
385 | // Remember that this class does not exist.
386 | $this->missingClasses[$class] = true;
387 | }
388 |
389 | return $file;
390 | }
391 |
392 | /**
393 | * Returns the currently registered loaders indexed by their corresponding vendor directories.
394 | *
395 | * @return self[]
396 | */
397 | public static function getRegisteredLoaders()
398 | {
399 | return self::$registeredLoaders;
400 | }
401 |
402 | private function findFileWithExtension($class, $ext)
403 | {
404 | // PSR-4 lookup
405 | $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
406 |
407 | $first = $class[0];
408 | if (isset($this->prefixLengthsPsr4[$first])) {
409 | $subPath = $class;
410 | while (false !== $lastPos = strrpos($subPath, '\\')) {
411 | $subPath = substr($subPath, 0, $lastPos);
412 | $search = $subPath . '\\';
413 | if (isset($this->prefixDirsPsr4[$search])) {
414 | $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
415 | foreach ($this->prefixDirsPsr4[$search] as $dir) {
416 | if (file_exists($file = $dir . $pathEnd)) {
417 | return $file;
418 | }
419 | }
420 | }
421 | }
422 | }
423 |
424 | // PSR-4 fallback dirs
425 | foreach ($this->fallbackDirsPsr4 as $dir) {
426 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
427 | return $file;
428 | }
429 | }
430 |
431 | // PSR-0 lookup
432 | if (false !== $pos = strrpos($class, '\\')) {
433 | // namespaced class name
434 | $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
435 | . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
436 | } else {
437 | // PEAR-like class name
438 | $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
439 | }
440 |
441 | if (isset($this->prefixesPsr0[$first])) {
442 | foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
443 | if (0 === strpos($class, $prefix)) {
444 | foreach ($dirs as $dir) {
445 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
446 | return $file;
447 | }
448 | }
449 | }
450 | }
451 | }
452 |
453 | // PSR-0 fallback dirs
454 | foreach ($this->fallbackDirsPsr0 as $dir) {
455 | if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
456 | return $file;
457 | }
458 | }
459 |
460 | // PSR-0 include paths.
461 | if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
462 | return $file;
463 | }
464 |
465 | return false;
466 | }
467 | }
468 |
469 | /**
470 | * Scope isolated include.
471 | *
472 | * Prevents access to $this/self from included files.
473 | */
474 | function includeFile($file)
475 | {
476 | include $file;
477 | }
478 |
--------------------------------------------------------------------------------
/lib/composer/InstalledVersions.php:
--------------------------------------------------------------------------------
1 |
27 | array (
28 | 'pretty_version' => 'dev-main',
29 | 'version' => 'dev-main',
30 | 'aliases' =>
31 | array (
32 | ),
33 | 'reference' => '98fdd65c94cacda5c9c5206215cac2130140a5db',
34 | 'name' => 'dlxplugins/pattern-wrangler',
35 | ),
36 | 'versions' =>
37 | array (
38 | 'dlxplugins/pattern-wrangler' =>
39 | array (
40 | 'pretty_version' => 'dev-main',
41 | 'version' => 'dev-main',
42 | 'aliases' =>
43 | array (
44 | ),
45 | 'reference' => '98fdd65c94cacda5c9c5206215cac2130140a5db',
46 | ),
47 | ),
48 | );
49 | private static $canGetVendors;
50 | private static $installedByVendor = array();
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | public static function getInstalledPackages()
59 | {
60 | $packages = array();
61 | foreach (self::getInstalled() as $installed) {
62 | $packages[] = array_keys($installed['versions']);
63 | }
64 |
65 |
66 | if (1 === \count($packages)) {
67 | return $packages[0];
68 | }
69 |
70 | return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
71 | }
72 |
73 |
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 | public static function isInstalled($packageName)
82 | {
83 | foreach (self::getInstalled() as $installed) {
84 | if (isset($installed['versions'][$packageName])) {
85 | return true;
86 | }
87 | }
88 |
89 | return false;
90 | }
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 | public static function satisfies(VersionParser $parser, $packageName, $constraint)
106 | {
107 | $constraint = $parser->parseConstraints($constraint);
108 | $provided = $parser->parseConstraints(self::getVersionRanges($packageName));
109 |
110 | return $provided->matches($constraint);
111 | }
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 | public static function getVersionRanges($packageName)
123 | {
124 | foreach (self::getInstalled() as $installed) {
125 | if (!isset($installed['versions'][$packageName])) {
126 | continue;
127 | }
128 |
129 | $ranges = array();
130 | if (isset($installed['versions'][$packageName]['pretty_version'])) {
131 | $ranges[] = $installed['versions'][$packageName]['pretty_version'];
132 | }
133 | if (array_key_exists('aliases', $installed['versions'][$packageName])) {
134 | $ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
135 | }
136 | if (array_key_exists('replaced', $installed['versions'][$packageName])) {
137 | $ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
138 | }
139 | if (array_key_exists('provided', $installed['versions'][$packageName])) {
140 | $ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
141 | }
142 |
143 | return implode(' || ', $ranges);
144 | }
145 |
146 | throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
147 | }
148 |
149 |
150 |
151 |
152 |
153 | public static function getVersion($packageName)
154 | {
155 | foreach (self::getInstalled() as $installed) {
156 | if (!isset($installed['versions'][$packageName])) {
157 | continue;
158 | }
159 |
160 | if (!isset($installed['versions'][$packageName]['version'])) {
161 | return null;
162 | }
163 |
164 | return $installed['versions'][$packageName]['version'];
165 | }
166 |
167 | throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
168 | }
169 |
170 |
171 |
172 |
173 |
174 | public static function getPrettyVersion($packageName)
175 | {
176 | foreach (self::getInstalled() as $installed) {
177 | if (!isset($installed['versions'][$packageName])) {
178 | continue;
179 | }
180 |
181 | if (!isset($installed['versions'][$packageName]['pretty_version'])) {
182 | return null;
183 | }
184 |
185 | return $installed['versions'][$packageName]['pretty_version'];
186 | }
187 |
188 | throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
189 | }
190 |
191 |
192 |
193 |
194 |
195 | public static function getReference($packageName)
196 | {
197 | foreach (self::getInstalled() as $installed) {
198 | if (!isset($installed['versions'][$packageName])) {
199 | continue;
200 | }
201 |
202 | if (!isset($installed['versions'][$packageName]['reference'])) {
203 | return null;
204 | }
205 |
206 | return $installed['versions'][$packageName]['reference'];
207 | }
208 |
209 | throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
210 | }
211 |
212 |
213 |
214 |
215 |
216 | public static function getRootPackage()
217 | {
218 | $installed = self::getInstalled();
219 |
220 | return $installed[0]['root'];
221 | }
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 | public static function getRawData()
230 | {
231 | return self::$installed;
232 | }
233 |
234 |
235 |
236 |
237 |
238 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 |
250 |
251 |
252 | public static function reload($data)
253 | {
254 | self::$installed = $data;
255 | self::$installedByVendor = array();
256 | }
257 |
258 |
259 |
260 |
261 | private static function getInstalled()
262 | {
263 | if (null === self::$canGetVendors) {
264 | self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
265 | }
266 |
267 | $installed = array();
268 |
269 | if (self::$canGetVendors) {
270 |
271 | foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
272 | if (isset(self::$installedByVendor[$vendorDir])) {
273 | $installed[] = self::$installedByVendor[$vendorDir];
274 | } elseif (is_file($vendorDir.'/composer/installed.php')) {
275 | $installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php';
276 | }
277 | }
278 | }
279 |
280 | $installed[] = self::$installed;
281 |
282 | return $installed;
283 | }
284 | }
285 |
--------------------------------------------------------------------------------
/lib/composer/LICENSE:
--------------------------------------------------------------------------------
1 |
2 | Copyright (c) Nils Adermann, Jordi Boggiano
3 |
4 | Permission is hereby granted, free of charge, to any person obtaining a copy
5 | of this software and associated documentation files (the "Software"), to deal
6 | in the Software without restriction, including without limitation the rights
7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 | copies of the Software, and to permit persons to whom the Software is furnished
9 | to do so, subject to the following conditions:
10 |
11 | The above copyright notice and this permission notice shall be included in all
12 | copies or substantial portions of the Software.
13 |
14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20 | THE SOFTWARE.
21 |
22 |
--------------------------------------------------------------------------------
/lib/composer/autoload_classmap.php:
--------------------------------------------------------------------------------
1 | $vendorDir . '/composer/InstalledVersions.php',
10 | );
11 |
--------------------------------------------------------------------------------
/lib/composer/autoload_namespaces.php:
--------------------------------------------------------------------------------
1 | array($baseDir . '/php'),
10 | );
11 |
--------------------------------------------------------------------------------
/lib/composer/autoload_real.php:
--------------------------------------------------------------------------------
1 | = 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());
30 | if ($useStaticLoader) {
31 | require __DIR__ . '/autoload_static.php';
32 |
33 | call_user_func(\Composer\Autoload\ComposerStaticInit27b17fd2c20dabf2ea56e4a83a2355a0::getInitializer($loader));
34 | } else {
35 | $map = require __DIR__ . '/autoload_namespaces.php';
36 | foreach ($map as $namespace => $path) {
37 | $loader->set($namespace, $path);
38 | }
39 |
40 | $map = require __DIR__ . '/autoload_psr4.php';
41 | foreach ($map as $namespace => $path) {
42 | $loader->setPsr4($namespace, $path);
43 | }
44 |
45 | $classMap = require __DIR__ . '/autoload_classmap.php';
46 | if ($classMap) {
47 | $loader->addClassMap($classMap);
48 | }
49 | }
50 |
51 | $loader->register(true);
52 |
53 | return $loader;
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/lib/composer/autoload_static.php:
--------------------------------------------------------------------------------
1 |
11 | array (
12 | 'DLXPlugins\\PatternWrangler\\' => 27,
13 | ),
14 | );
15 |
16 | public static $prefixDirsPsr4 = array (
17 | 'DLXPlugins\\PatternWrangler\\' =>
18 | array (
19 | 0 => __DIR__ . '/../..' . '/php',
20 | ),
21 | );
22 |
23 | public static $classMap = array (
24 | 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
25 | );
26 |
27 | public static function getInitializer(ClassLoader $loader)
28 | {
29 | return \Closure::bind(function () use ($loader) {
30 | $loader->prefixLengthsPsr4 = ComposerStaticInit27b17fd2c20dabf2ea56e4a83a2355a0::$prefixLengthsPsr4;
31 | $loader->prefixDirsPsr4 = ComposerStaticInit27b17fd2c20dabf2ea56e4a83a2355a0::$prefixDirsPsr4;
32 | $loader->classMap = ComposerStaticInit27b17fd2c20dabf2ea56e4a83a2355a0::$classMap;
33 |
34 | }, null, ClassLoader::class);
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/lib/composer/installed.json:
--------------------------------------------------------------------------------
1 | {
2 | "packages": [],
3 | "dev": true,
4 | "dev-package-names": []
5 | }
6 |
--------------------------------------------------------------------------------
/lib/composer/installed.php:
--------------------------------------------------------------------------------
1 |
3 | array (
4 | 'pretty_version' => 'dev-main',
5 | 'version' => 'dev-main',
6 | 'aliases' =>
7 | array (
8 | ),
9 | 'reference' => '98fdd65c94cacda5c9c5206215cac2130140a5db',
10 | 'name' => 'dlxplugins/pattern-wrangler',
11 | ),
12 | 'versions' =>
13 | array (
14 | 'dlxplugins/pattern-wrangler' =>
15 | array (
16 | 'pretty_version' => 'dev-main',
17 | 'version' => 'dev-main',
18 | 'aliases' =>
19 | array (
20 | ),
21 | 'reference' => '98fdd65c94cacda5c9c5206215cac2130140a5db',
22 | ),
23 | ),
24 | );
25 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "pattern-wrangler",
3 | "version": "1.0.0",
4 | "description": "Manage your block patterns.",
5 | "main": "src/index.js",
6 | "scripts": {
7 | "build": "wp-scripts build --env mode=production",
8 | "start": "wp-scripts start --env mode=development"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/dlxplugins/pattern-wrangler.git"
13 | },
14 | "keywords": [
15 | "pattern",
16 | "wrangler"
17 | ],
18 | "author": "Ronald Huereca",
19 | "license": "GPL-3.0-or-later",
20 | "bugs": {
21 | "url": "https://github.com/dlxplugins/pattern-wrangler/issues"
22 | },
23 | "homepage": "https://dlxplugins.com/plugins/pattern-wrangler/",
24 | "devDependencies": {
25 | "@babel/eslint-parser": "^7.18.2",
26 | "@babel/plugin-transform-arrow-functions": "^7.17.12",
27 | "@babel/plugin-transform-class-properties": "^7.23.3",
28 | "@babel/preset-env": "^7.18.2",
29 | "@babel/preset-react": "^7.17.12",
30 | "@types/photoswipe": "^4.1.2",
31 | "@types/react": "^18.0.14",
32 | "@wordpress/commands": "^0.16.0",
33 | "@wordpress/components": "^25.11.0",
34 | "@wordpress/element": "^4.9.0",
35 | "@wordpress/eslint-plugin": "^12.8.0",
36 | "@wordpress/i18n": "^4.11.0",
37 | "@wordpress/icons": "^9.36.0",
38 | "@wordpress/interface": "^8.3.0",
39 | "@wordpress/scripts": "^30.7.0",
40 | "ajv": "^8.17.1",
41 | "axios": "^1.6.1",
42 | "babel-plugin-macros": "^3.1.0",
43 | "classnames": "^2.3.1",
44 | "css-loader": "^6.7.1",
45 | "dompurify": "^2.4.0",
46 | "eslint": "^8.18.0",
47 | "eslint-config-prettier": "^8.3.0",
48 | "eslint-plugin-prettier": "^4.0.0",
49 | "grunt": "^1.6.1",
50 | "grunt-contrib-compress": "^2.0.0",
51 | "lodash.uniqueid": "^4.0.1",
52 | "lucide-react": "^0.292.0",
53 | "mini-css-extract-plugin": "^2.6.1",
54 | "prettier": "^2.7.1",
55 | "prettier-eslint": "^15.0.1",
56 | "process": "^0.11.10",
57 | "prop-types": "^15.8.1",
58 | "qs": "^6.10.5",
59 | "react": "^18.2.0",
60 | "react-dom": "^18.2.0",
61 | "react-hook-form": "^7.48.2",
62 | "resolve-url-loader": "^5.0.0",
63 | "sass": "^1.66.1",
64 | "sass-loader": "^13.0.2",
65 | "scss-tokenizer": "^0.4.3",
66 | "use-async-resource": "^2.2.2",
67 | "util": "^0.12.4",
68 | "webpack": "^5.73.0",
69 | "webpack-cli": "^4.10.0",
70 | "webpack-remove-empty-scripts": "^0.8.1"
71 | },
72 | "dependencies": {
73 | "@fancyapps/ui": "^5.0.36",
74 | "@wordpress/a11y": "^3.11.0",
75 | "grunt-cli": "^1.4.3",
76 | "html-to-react": "^1.4.8"
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/pattern-wrangler.php:
--------------------------------------------------------------------------------
1 | run();
69 |
70 | $patterns = new Patterns();
71 | $patterns->run();
72 |
73 | $drafts = new Drafts();
74 | $drafts->run();
75 |
76 | $preview = new Preview();
77 | $preview->run();
78 |
79 | // Determine if blocks can run or not.
80 | $options = Options::get_options();
81 | $can_disable_blocks = (bool) $options['disablePatternImporterBlock'];
82 | if ( ! $can_disable_blocks ) {
83 | $blocks = new Blocks();
84 | $blocks->run();
85 | }
86 |
87 | /**
88 | * When PatternWrangler can be extended.
89 | *
90 | * Filter when PatternWrangler can be extended.
91 | *
92 | * @since 1.0.0
93 | */
94 | do_action( 'dlxplugins_pw_loaded' );
95 | }
96 |
97 | /**
98 | * Init all the things.
99 | */
100 | public function init() {
101 |
102 | // Nothing here yet.
103 | }
104 | }
105 |
106 | add_action(
107 | 'plugins_loaded',
108 | function () {
109 | $pattern_wrangler = PatternWrangler::get_instance();
110 | $pattern_wrangler->plugins_loaded();
111 | }
112 | );
113 |
--------------------------------------------------------------------------------
/php/Admin.php:
--------------------------------------------------------------------------------
1 | sprintf( '%s', esc_url( Functions::get_settings_url() ), esc_html__( 'Settings', 'pattern-wrangler' ) ),
77 | 'docs' => sprintf( '%s', esc_url( 'https://docs.dlxplugins.com/v/pattern-wrangler' ), esc_html__( 'Docs', 'pattern-wrangler' ) ),
78 | 'site' => sprintf( '%s', esc_url( 'https://dlxplugins.com/plugins/pattern-wrangler/' ), esc_html__( 'Plugin Home', 'pattern-wrangler' ) ),
79 | );
80 | if ( ! is_array( $settings ) ) {
81 | return $setting_links;
82 | } else {
83 | return array_merge( $setting_links, $settings );
84 | }
85 | }
86 |
87 | /**
88 | * Save the options via Ajax.
89 | */
90 | public function ajax_save_options() {
91 | // Get form data.
92 | $form_data = filter_input( INPUT_POST, 'formData', FILTER_SANITIZE_SPECIAL_CHARS, FILTER_REQUIRE_ARRAY );
93 |
94 | $nonce = $form_data['saveNonce'] ?? false;
95 | if ( ! wp_verify_nonce( $nonce, 'dlx-pw-admin-save-options' ) || ! current_user_can( 'manage_options' ) ) {
96 | wp_send_json_error(
97 | array(
98 | 'message' => __( 'Nonce or permission verification failed.', 'pattern-wrangler' ),
99 | 'type' => 'critical',
100 | 'dismissable' => true,
101 | 'title' => __( 'Error', 'pattern-wrangler' ),
102 | )
103 | );
104 | }
105 |
106 | // Get array values.
107 | $form_data = Functions::sanitize_array_recursive( $form_data );
108 |
109 | // Update options.
110 | Options::update_options( $form_data );
111 |
112 | // Send success message.
113 | wp_send_json_success(
114 | array(
115 | 'message' => __( 'Options saved.', 'pattern-wrangler' ),
116 | 'type' => 'success',
117 | 'dismissable' => true,
118 | )
119 | );
120 | }
121 |
122 | /**
123 | * Reset the options.
124 | */
125 | public function ajax_reset_options() {
126 | // Get form data.
127 | $form_data = filter_input( INPUT_POST, 'formData', FILTER_SANITIZE_SPECIAL_CHARS, FILTER_REQUIRE_ARRAY );
128 |
129 | $nonce = $form_data['resetNonce'] ?? false;
130 | if ( ! wp_verify_nonce( $nonce, 'dlx-pw-admin-reset-options' ) || ! current_user_can( 'manage_options' ) ) {
131 | wp_send_json_error(
132 | array(
133 | 'message' => __( 'Nonce or permission verification failed.', 'pattern-wrangler' ),
134 | 'type' => 'error',
135 | 'dismissable' => true,
136 | 'title' => __( 'Error', 'pattern-wrangler' ),
137 | )
138 | );
139 | }
140 |
141 | // Get existing options.
142 | $options = Options::get_options();
143 |
144 | // Get defaults and reset.
145 | $default_options = Options::get_defaults();
146 |
147 | Options::update_options( $default_options );
148 |
149 | // Pull in nonces to default options before returning.
150 | $default_options['saveNonce'] = $options['saveNonce'];
151 | $default_options['resetNonce'] = $options['resetNonce'];
152 |
153 | // Send success message.
154 | wp_send_json_success(
155 | array(
156 | 'message' => __( 'Options reset.', 'pattern-wrangler' ),
157 | 'type' => 'success',
158 | 'dismissable' => true,
159 | 'formData' => $default_options,
160 | )
161 | );
162 | }
163 |
164 | /**
165 | * Retrieve options via Ajax.
166 | */
167 | public function ajax_get_options() {
168 | // Get nonce.
169 | $nonce = sanitize_text_field( filter_input( INPUT_POST, 'nonce', FILTER_SANITIZE_SPECIAL_CHARS ) );
170 |
171 | // Verify nonce.
172 | $nonce_action = 'dlx-pw-admin-get-options';
173 | if ( ! wp_verify_nonce( $nonce, $nonce_action ) || ! current_user_can( 'manage_options' ) ) {
174 | wp_send_json_error(
175 | array(
176 | 'message' => __( 'Nonce or permission verification failed.', 'pattern-wrangler' ),
177 | 'type' => 'error',
178 | 'dismissable' => true,
179 | 'title' => __( 'Error', 'pattern-wrangler' ),
180 | )
181 | );
182 | }
183 | $options = Options::get_options();
184 |
185 | $categories = Functions::get_pattern_categories();
186 | $options['registered'] = $categories['registered'];
187 | $options['categories'] = $categories['categories'];
188 |
189 | wp_send_json_success( $options );
190 | }
191 |
192 | /**
193 | * Add synced/unsynced status to patterns.
194 | *
195 | * @param array $columns Columns.
196 | *
197 | * @return array Updated columns.
198 | */
199 | public function add_pattern_sync_column( $columns ) {
200 | $new_column['pattern_sync'] = __( 'Synced', 'pattern-wrangler' );
201 |
202 | // Add new column before last item of array.
203 | $columns = array_slice( $columns, 0, -1, true ) + $new_column + array_slice( $columns, -1, null, true );
204 | return $columns;
205 | }
206 |
207 | /**
208 | * Output synced vs unsynced for post column.
209 | *
210 | * @param string $column Column name.
211 | * @param int $post_id Post ID.
212 | */
213 | public function output_pattern_sync_column( $column, $post_id ) {
214 | if ( 'pattern_sync' === $column ) {
215 | $synced = get_post_meta( $post_id, 'wp_pattern_sync_status', true );
216 | if ( 'unsynced' === $synced ) {
217 | // Unsynced patterns are explicitly set in post meta, whereas synced are not and assumed synced.
218 | echo ' ' . esc_html__( 'Unsynced Pattern', 'pattern-wrangler' );
219 | } else {
220 | echo ' ' . esc_html__( 'Synced Pattern', 'pattern-wrangler' );
221 | }
222 | }
223 | }
224 |
225 | /**
226 | * Add the admin menu.
227 | */
228 | public function add_admin_menu() {
229 | $options = Options::get_options();
230 | $hide_all_patterns = (bool) $options['hideAllPatterns'] ?? false;
231 | $hide_patterns_menu = (bool) $options['hidePatternsMenu'] ?? false;
232 |
233 | remove_submenu_page( 'themes.php', 'edit.php?post_type=wp_block' ); // Remove from Appearance in WP 6.5.
234 | remove_submenu_page( 'generateblocks', 'edit.php?post_type=wp_block' ); // Remove from GenerateBlocks screen.
235 |
236 | if ( $hide_all_patterns && $hide_patterns_menu ) {
237 | $hook = add_submenu_page(
238 | 'themes.php',
239 | __( 'Patterns', 'pattern-wrangler' ),
240 | __( 'Patterns', 'pattern-wrangler' ),
241 | 'manage_options',
242 | 'pattern-wrangler',
243 | array( $this, 'admin_page' ),
244 | 4
245 | );
246 | add_action( 'admin_print_scripts-' . $hook, array( $this, 'enqueue_scripts' ) );
247 | return;
248 | }
249 | add_menu_page(
250 | __( 'Patterns', 'pattern-wrangler' ),
251 | __( 'Patterns', 'pattern-wrangler' ),
252 | 'manage_options',
253 | 'edit.php?post_type=wp_block',
254 | '',
255 | 'dashicons-layout',
256 | 6
257 | );
258 |
259 | add_submenu_page(
260 | 'edit.php?post_type=wp_block',
261 | __( 'Categories', 'pattern-wrangler' ),
262 | __( 'Categories', 'pattern-wrangler' ),
263 | 'edit_posts',
264 | 'edit-tags.php?taxonomy=wp_pattern_category&post_type=wp_block',
265 | '',
266 | 5
267 | );
268 |
269 | $hook = add_submenu_page(
270 | 'edit.php?post_type=wp_block',
271 | __( 'Settings', 'pattern-wrangler' ),
272 | __( 'Settings', 'pattern-wrangler' ),
273 | 'edit_posts',
274 | 'pattern-wrangler',
275 | array( $this, 'admin_page' ),
276 | 10
277 | );
278 | add_action( 'admin_print_scripts-' . $hook, array( $this, 'enqueue_scripts' ) );
279 | }
280 |
281 | /**
282 | * Set the category submenu as current.
283 | *
284 | * @param WP_Screen $screen The current screen.
285 | */
286 | public function set_category_submenu_current( $screen ) {
287 | if ( ! is_admin() ) {
288 | return;
289 | }
290 | // Check if current page is pattern categories and mark categories as curent if slug matches.
291 | $current_screen = get_current_screen();
292 | if ( 'edit-wp_pattern_category' === $current_screen->id ) {
293 | // Doing JS here because there are no filters for marking submenus as current.
294 | ?>
295 |
309 | wp_create_nonce( 'dlx-pw-admin-get-options' ),
335 | 'saveNonce' => wp_create_nonce( 'dlx-pw-admin-save-options' ),
336 | 'resetNonce' => wp_create_nonce( 'dlx-pw-admin-reset-options' ),
337 | 'previewNonce' => wp_create_nonce( 'dlx-pw-admin-preview' ),
338 | 'ajaxurl' => admin_url( 'admin-ajax.php' ),
339 | )
340 | );
341 | \wp_set_script_translations( 'dlx-pw-admin', 'pattern-wrangler' );
342 | }
343 |
344 | // Enqueue admin styles.
345 | wp_enqueue_style(
346 | 'dlx-pw-admin-css',
347 | Functions::get_plugin_url( 'dist/dlx-pw-admin-css.css' ),
348 | array(),
349 | Functions::get_plugin_version(),
350 | 'all'
351 | );
352 | }
353 |
354 | /**
355 | * Render the admin page.
356 | */
357 | public function admin_page() {
358 | ?>
359 |
360 |
373 |
376 |
377 |
386 |
387 |
388 | post_type ) {
43 | return true;
44 | }
45 | return $use_block_editor;
46 | }
47 |
48 | /**
49 | * Register the rest routes needed.
50 | */
51 | public function init_rest_api() {
52 | register_rest_route(
53 | 'dlxplugins/pattern-wrangler/v1',
54 | '/process_image',
55 | array(
56 | 'methods' => 'POST',
57 | 'callback' => array( $this, 'rest_add_remote_image' ),
58 | 'permission_callback' => array( $this, 'rest_image_sideload_permissions' ),
59 | )
60 | );
61 | }
62 |
63 | /**
64 | * Process a list of images for a pattern.
65 | *
66 | * @param WP_Rest $request REST request.
67 | */
68 | public function rest_add_remote_image( $request ) {
69 | $image_url = filter_var( $request->get_param( 'imgUrl' ), FILTER_VALIDATE_URL );
70 | $image_alt = sanitize_text_field( $request->get_param( 'imgAlt' ) );
71 |
72 | if ( $image_url ) {
73 | // Check file extension.
74 | $extension = pathinfo( $image_url, PATHINFO_EXTENSION );
75 |
76 | // Strip query vars from extension.
77 | $extension = preg_replace( '/\?.*/', '', $extension );
78 |
79 | // Get current domain.
80 | $domain = wp_parse_url( $image_url, PHP_URL_HOST );
81 |
82 | // If we're on same domain, bail successfully.
83 | if ( $domain === $_SERVER['HTTP_HOST'] ) {
84 | \wp_send_json_success(
85 | array(
86 | 'attachmentId' => 0,
87 | 'attachmentUrl' => esc_url( $image_url ),
88 | )
89 | );
90 | }
91 |
92 | if ( ! $extension ) {
93 | \wp_send_json_error(
94 | array(
95 | 'message' => __( 'File extension not found.', 'pattern-wrangler' ),
96 | ),
97 | 400
98 | );
99 | }
100 | $valid_extensions = Functions::get_supported_file_extensions();
101 | if ( ! in_array( $extension, $valid_extensions, true ) ) {
102 | \wp_send_json_error(
103 | array(
104 | 'message' => __( 'Invalid file extension.', 'pattern-wrangler' ),
105 | ),
106 | 400
107 | );
108 | }
109 |
110 | // Save the image to the media library.
111 | if ( ! function_exists( 'media_sideload_image' ) ) {
112 | require_once ABSPATH . 'wp-admin/includes/image.php';
113 | require_once ABSPATH . 'wp-admin/includes/file.php';
114 | require_once ABSPATH . 'wp-admin/includes/media.php';
115 | }
116 | $attachment_id = media_sideload_image( $image_url, 0, '', 'id' );
117 |
118 | // Add order to attachment.
119 | if ( ! is_wp_error( $attachment_id ) ) {
120 |
121 | // Get attachment URL.
122 | $attachment_url_src = wp_get_attachment_image_src( $attachment_id, 'full' );
123 | $attachment_url = $attachment_url_src[0];
124 |
125 | // Update alt attribute.
126 | update_post_meta( $attachment_id, '_wp_attachment_image_alt', $image_alt );
127 |
128 | // Send success.
129 | \wp_send_json_success(
130 | array(
131 | 'attachmentId' => absint( $attachment_id ),
132 | 'attachmentUrl' => esc_url( $attachment_url ),
133 | )
134 | );
135 | } else {
136 | \wp_send_json_error(
137 | array(
138 | 'message' => $attachment_id->get_error_message(),
139 | ),
140 | 400
141 | );
142 | }
143 | }
144 | \wp_send_json_error(
145 | array(
146 | 'message' => __( 'Invalid image URL.', 'pattern-wrangler' ),
147 | ),
148 | 400
149 | );
150 | }
151 |
152 | /**
153 | * Check if user has access to REST API for retrieving and sideloading images.
154 | */
155 | public function rest_image_sideload_permissions() {
156 | return current_user_can( 'publish_posts' );
157 | }
158 |
159 | /**
160 | * Init action callback.
161 | */
162 | public function init() {
163 |
164 | register_block_type(
165 | Functions::get_plugin_dir( 'build/js/blocks/pattern-importer/block.json' ),
166 | array(
167 | 'render_callback' => '__return_empty_string',
168 | )
169 | );
170 |
171 | // Enqueue block assets.
172 | add_action( 'enqueue_block_editor_assets', array( $this, 'register_block_editor_scripts' ) );
173 | }
174 |
175 | /**
176 | * Register the block editor script with localized vars.
177 | */
178 | public function register_block_editor_scripts() {
179 |
180 | $deps = require_once Functions::get_plugin_dir( 'build/index.asset.php' );
181 |
182 | wp_register_script(
183 | 'dlx-pw-pattern-inserter-block',
184 | Functions::get_plugin_url( 'build/index.js' ),
185 | $deps['dependencies'],
186 | $deps['version'],
187 | true
188 | );
189 |
190 | wp_localize_script(
191 | 'dlx-pw-pattern-inserter-block',
192 | 'dlxPWPatternInserter',
193 | array(
194 | 'restUrl' => rest_url( 'dlxplugins/pattern-wrangler/v1' ),
195 | 'restNonce' => wp_create_nonce( 'wp_rest' ),
196 | )
197 | );
198 | }
199 | }
200 |
--------------------------------------------------------------------------------
/php/Drafts.php:
--------------------------------------------------------------------------------
1 | %s
',
55 | esc_html( $notice_message )
56 | );
57 | }
58 |
59 | /**
60 | * Intercept draft/publish actions.
61 | */
62 | public function intercept_draft_publish() {
63 | $action = sanitize_text_field( filter_input( INPUT_GET, 'action', FILTER_SANITIZE_SPECIAL_CHARS ) );
64 | $nonce = sanitize_text_field( filter_input( INPUT_GET, 'nonce', FILTER_SANITIZE_SPECIAL_CHARS ) );
65 | $post_id = absint( filter_input( INPUT_GET, 'post', \FILTER_SANITIZE_NUMBER_INT ) );
66 |
67 | if ( ! $action ) {
68 | return;
69 | }
70 | if ( ! current_user_can( 'edit_posts' ) ) {
71 | return;
72 | }
73 | $notice_action = 'draft_pattern';
74 | switch ( $action ) {
75 | case 'draft_pattern':
76 | if ( ! wp_verify_nonce( $nonce, 'draft-pattern_' . $post_id ) ) {
77 | return;
78 | }
79 | wp_update_post(
80 | array(
81 | 'ID' => $post_id,
82 | 'post_status' => 'draft',
83 | )
84 | );
85 | break;
86 | case 'publish_pattern':
87 | if ( ! wp_verify_nonce( $nonce, 'publish-pattern_' . $post_id ) ) {
88 | return;
89 | }
90 | $notice_action = 'publish_pattern';
91 | wp_update_post(
92 | array(
93 | 'ID' => $post_id,
94 | 'post_status' => 'publish',
95 | )
96 | );
97 | break;
98 | default:
99 | return;
100 | }
101 |
102 | // Build redirect URL.
103 | $redirect_url = add_query_arg(
104 | array(
105 | 'post_type' => 'wp_block',
106 | 'notice_action' => $notice_action,
107 | ),
108 | admin_url( 'edit.php' )
109 | );
110 | wp_safe_redirect( esc_url_raw( $redirect_url ) );
111 | exit;
112 | }
113 |
114 | /**
115 | * Add a draft button to the quick actions for the wp_block post type.
116 | *
117 | * @param array $actions Array of actions.
118 | * @param WP_Post $post Post object.
119 | *
120 | * @return array
121 | */
122 | public function add_draft_button_quick_action( $actions, $post ) {
123 | if ( 'wp_block' !== $post->post_type ) {
124 | return $actions;
125 | }
126 | if ( ! current_user_can( 'edit_posts' ) ) {
127 | return $actions;
128 | }
129 | $draft_disable_url = add_query_arg(
130 | array(
131 | 'action' => 'draft_pattern',
132 | 'nonce' => wp_create_nonce( 'draft-pattern_' . $post->ID ),
133 | 'post' => $post->ID,
134 | ),
135 | admin_url( 'edit.php?post_type=wp_block' )
136 | );
137 | $draft_publish_url = add_query_arg(
138 | array(
139 | 'action' => 'publish_pattern',
140 | 'nonce' => wp_create_nonce( 'publish-pattern_' . $post->ID ),
141 | 'post' => $post->ID,
142 | ),
143 | admin_url( 'edit.php?post_type=wp_block' )
144 | );
145 | if ( 'draft' === $post->post_status ) {
146 | $actions['draft_pattern'] = sprintf(
147 | '%s',
148 | esc_url_raw( $draft_publish_url ),
149 | esc_html__( 'Publish', 'pattern-wrangler' )
150 | );
151 | }
152 | if ( 'publish' === $post->post_status ) {
153 | $actions['preview_pattern'] = sprintf(
154 | '%s',
155 | esc_url_raw( $draft_disable_url ),
156 | esc_html__( 'Switch to Draft', 'pattern-wrangler' )
157 | );
158 | }
159 | return $actions;
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/php/Functions.php:
--------------------------------------------------------------------------------
1 | 'wp_pattern_category',
97 | 'hide_empty' => false,
98 | 'count' => true,
99 | )
100 | );
101 | return $categories;
102 | }
103 |
104 | /**
105 | * Get the pattern categories.
106 | *
107 | * @return array The pattern categories.
108 | */
109 | public static function get_pattern_categories() {
110 | $options = Options::get_options();
111 |
112 | // Get registered block categories.
113 | $pattern_categories = \WP_Block_Pattern_Categories_Registry::get_instance();
114 | $pattern_categories = $pattern_categories->get_all_registered();
115 |
116 | // Get all registered block patterns. We'll use this for a count.
117 | $pattern_registry = \WP_Block_Patterns_Registry::get_instance();
118 | $pattern_registry = $pattern_registry->get_all_registered();
119 |
120 | // Get all pattern categories from the built-in WP taxonomy.
121 | $pattern_categories_taxonomy = self::get_pattern_categories_from_taxonomy();
122 |
123 | // Get saved category data.
124 | $custom_pattern_categories = $options['categories'];
125 |
126 | // Loop through custom categories, and determine if a category is on or off.
127 | $all_categories = array();
128 |
129 | // Exclude these categories as they are deprecated in WordPress core.
130 | $excluded_cats = array(
131 | 'buttons',
132 | 'columns',
133 | 'query',
134 | );
135 |
136 | foreach ( $pattern_categories as $category ) {
137 | /* Excluded Categories */
138 | if ( in_array( $category['name'], $excluded_cats, true ) ) {
139 | continue;
140 | }
141 |
142 | // Loop through custom categories, and determine if a category is on or off.
143 | $category_enabled = isset( $custom_pattern_categories[ $category['name'] ]['enabled'] ) ? (bool) $custom_pattern_categories[ $category['name'] ]['enabled'] : true;
144 | $category_custom = isset( $custom_pattern_categories[ $category['name'] ]['customLabel'] ) ? $custom_pattern_categories[ $category['name'] ]['customLabel'] : $category['label'];
145 | $category_mapped_to = isset( $custom_pattern_categories[ $category['name'] ]['mappedTo'] ) ? $custom_pattern_categories[ $category['name'] ]['mappedTo'] : false;
146 | $all_categories[ $category['name'] ] = array(
147 | 'label' => $category['label'],
148 | 'customLabel' => ! empty( $category_custom ) ? $category_custom : $category['label'],
149 | 'enabled' => $category_enabled,
150 | 'slug' => $category['name'],
151 | 'count' => $category['count'] ?? 0,
152 | 'mappedTo' => $category_mapped_to,
153 | );
154 | }
155 |
156 | // Ensure all categories are unique.
157 | $all_categories = array_unique( $all_categories, SORT_REGULAR );
158 |
159 | // Sort by label.
160 | uasort(
161 | $all_categories,
162 | function ( $a, $b ) {
163 | return strcasecmp( $a['customLabel'], $b['customLabel'] );
164 | }
165 | );
166 |
167 | // Loop through all patterns and increment a count for each category. Since core tax pattern categories have a count for core patterns.
168 | foreach ( $pattern_registry as $pattern ) {
169 | $pattern_categories = $pattern['categories'];
170 | foreach ( $pattern_categories as $category ) {
171 | if ( isset( $all_categories[ $category ] ) ) {
172 | ++$all_categories[ $category ]['count'];
173 | }
174 | }
175 | }
176 |
177 | return array(
178 | 'registered' => $all_categories,
179 | 'categories' => $pattern_categories_taxonomy,
180 | );
181 | }
182 |
183 | /**
184 | * Check if a pattern is synced.
185 | *
186 | * @param int $pattern_id The pattern ID.
187 | *
188 | * @return bool true if synced, false if not.
189 | */
190 | public static function is_pattern_synced( $pattern_id ) {
191 | $synced = get_post_meta( $pattern_id, 'wp_pattern_sync_status', true );
192 | if ( 'unsynced' === $synced ) {
193 | return false;
194 | }
195 | return true;
196 | }
197 |
198 | /**
199 | * Get preview URL for previewing a pattern.
200 | *
201 | * @param int $post_id The post ID.
202 | *
203 | * @return string The preview URL (unescaped).
204 | */
205 | public static function get_pattern_preview_url( $post_id ) {
206 | $preview_url = add_query_arg(
207 | array(
208 | 'dlxpw_preview' => '1',
209 | 'action' => 'preview',
210 | 'pattern' => $post_id,
211 | 'nonce' => wp_create_nonce( 'preview-pattern_' . $post_id ),
212 | ),
213 | home_url()
214 | );
215 | return $preview_url;
216 | }
217 |
218 | /**
219 | * Get the plugin's supported file extensions.
220 | *
221 | * @since 1.0.0
222 | *
223 | * @return array The supported file extensions.
224 | */
225 | public static function get_supported_file_extensions() {
226 | $file_extensions = array(
227 | 'jpeg',
228 | 'jpg',
229 | 'gif',
230 | 'png',
231 | 'webp',
232 | 'avif',
233 | );
234 | /**
235 | * Filter the valid file extensions for the photo block.
236 | *
237 | * @param array $file_extensions The valid mime types.
238 | */
239 | $file_extensions = apply_filters( 'dlxpw_block_file_extensions', $file_extensions );
240 |
241 | return $file_extensions;
242 | }
243 |
244 | /**
245 | * Get the current admin tab.
246 | *
247 | * @return null|string Current admin tab.
248 | */
249 | public static function get_admin_tab() {
250 | $tab = filter_input( INPUT_GET, 'tab', FILTER_SANITIZE_SPECIAL_CHARS );
251 | if ( $tab && is_string( $tab ) ) {
252 | return sanitize_text_field( sanitize_title( $tab ) );
253 | }
254 | return null;
255 | }
256 |
257 | /**
258 | * Return the URL to the admin screen
259 | *
260 | * @param string $tab Tab path to load.
261 | * @param string $sub_tab Subtab path to load.
262 | *
263 | * @return string URL to admin screen. Output is not escaped.
264 | */
265 | public static function get_settings_url( $tab = '', $sub_tab = '' ) {
266 | $options = Options::get_options();
267 | $hide_all_patterns = (bool) $options['hideAllPatterns'] ?? false;
268 | $hide_patterns_menu = (bool) $options['hidePatternsMenu'] ?? false;
269 | $options_url = admin_url( 'edit.php?post_type=wp_block&page=pattern-wrangler' );
270 | if ( $hide_all_patterns && $hide_patterns_menu ) {
271 | $options_url = admin_url( 'themes.php?page=pattern-wrangler' );
272 | }
273 |
274 | if ( ! empty( $tab ) ) {
275 | $options_url = add_query_arg( array( 'tab' => sanitize_title( $tab ) ), $options_url );
276 | if ( ! empty( $sub_tab ) ) {
277 | $options_url = add_query_arg( array( 'subtab' => sanitize_title( $sub_tab ) ), $options_url );
278 | }
279 | }
280 | return $options_url;
281 | }
282 |
283 | /**
284 | * Checks to see if an asset is activated or not.
285 | *
286 | * @since 1.0.0
287 | *
288 | * @param string $path Path to the asset.
289 | * @param string $type Type to check if it is activated or not.
290 | *
291 | * @return bool true if activated, false if not.
292 | */
293 | public static function is_activated( $path, $type = 'plugin' ) {
294 |
295 | // Gets all active plugins on the current site.
296 | $active_plugins = self::is_multisite() ? get_site_option( 'active_sitewide_plugins' ) : get_option( 'active_plugins', array() );
297 | if ( in_array( $path, $active_plugins, true ) ) {
298 | return true;
299 | }
300 | return false;
301 | }
302 |
303 | /**
304 | * Take a _ separated field and convert to camelcase.
305 | *
306 | * @param string $field Field to convert to camelcase.
307 | *
308 | * @return string camelCased field.
309 | */
310 | public static function to_camelcase( string $field ) {
311 | return str_replace( '_', '', lcfirst( ucwords( $field, '_' ) ) );
312 | }
313 |
314 | /**
315 | * Return the plugin slug.
316 | *
317 | * @return string plugin slug.
318 | */
319 | public static function get_plugin_slug() {
320 | return dirname( plugin_basename( DLXPW_PATTERN_WRANGLER_FILE ) );
321 | }
322 |
323 | /**
324 | * Return the basefile for the plugin.
325 | *
326 | * @return string base file for the plugin.
327 | */
328 | public static function get_plugin_file() {
329 | return plugin_basename( DLXPW_PATTERN_WRANGLER_FILE );
330 | }
331 |
332 | /**
333 | * Return the version for the plugin.
334 | *
335 | * @return float version for the plugin.
336 | */
337 | public static function get_plugin_version() {
338 | return DLXPW_PATTERN_WRANGLER_VERSION;
339 | }
340 |
341 | /**
342 | * Returns appropriate html for KSES.
343 | *
344 | * @param bool $svg Whether to add SVG data to KSES.
345 | */
346 | public static function get_kses_allowed_html( $svg = true ) {
347 | $allowed_tags = wp_kses_allowed_html();
348 |
349 | $allowed_tags['nav'] = array(
350 | 'class' => array(),
351 | );
352 | $allowed_tags['a']['class'] = array();
353 | $allowed_tags['input'] = array(
354 | 'type' => array(),
355 | 'name' => array(),
356 | 'value' => array(),
357 | 'class' => array(),
358 | 'readonly' => array(),
359 | );
360 | $allowed_tags['button'] = array(
361 | 'type' => array(),
362 | 'name' => array(),
363 | 'value' => array(),
364 | 'class' => array(),
365 | 'title' => array(),
366 | );
367 | $allowed_tags['div'] = array(
368 | 'class' => array(),
369 | );
370 | $allowed_tags['span'] = array(
371 | 'class' => array(),
372 | );
373 |
374 | if ( ! $svg ) {
375 | return $allowed_tags;
376 | }
377 | $allowed_tags['svg'] = array(
378 | 'xmlns' => array(),
379 | 'fill' => array(),
380 | 'viewbox' => array(),
381 | 'role' => array(),
382 | 'aria-hidden' => array(),
383 | 'focusable' => array(),
384 | 'class' => array(),
385 | );
386 |
387 | $allowed_tags['path'] = array(
388 | 'd' => array(),
389 | 'fill' => array(),
390 | 'opacity' => array(),
391 | );
392 |
393 | $allowed_tags['g'] = array();
394 |
395 | $allowed_tags['use'] = array(
396 | 'xlink:href' => array(),
397 | );
398 |
399 | $allowed_tags['symbol'] = array(
400 | 'aria-hidden' => array(),
401 | 'viewBox' => array(),
402 | 'id' => array(),
403 | 'xmls' => array(),
404 | );
405 |
406 | return $allowed_tags;
407 | }
408 |
409 | /**
410 | * Array data that must be sanitized.
411 | *
412 | * @param array $data Data to be sanitized.
413 | *
414 | * @return array Sanitized data.
415 | */
416 | public static function sanitize_array_recursive( array $data ) {
417 | $sanitized_data = array();
418 | foreach ( $data as $key => $value ) {
419 | if ( '0' === $value ) {
420 | $value = 0;
421 | }
422 | if ( 'true' === $value ) {
423 | $value = true;
424 | } elseif ( 'false' === $value ) {
425 | $value = false;
426 | }
427 | if ( is_array( $value ) ) {
428 | $value = self::sanitize_array_recursive( $value );
429 | $sanitized_data[ $key ] = $value;
430 | continue;
431 | }
432 | if ( is_bool( $value ) ) {
433 | $sanitized_data[ $key ] = (bool) $value;
434 | continue;
435 | }
436 | if ( is_int( $value ) ) {
437 | $sanitized_data[ $key ] = (int) $value;
438 | continue;
439 | }
440 | if ( is_string( $value ) ) {
441 | $sanitized_data[ $key ] = sanitize_text_field( $value );
442 | continue;
443 | }
444 | }
445 | return $sanitized_data;
446 | }
447 |
448 | /**
449 | * Get the plugin directory for a path.
450 | *
451 | * @param string $path The path to the file.
452 | *
453 | * @return string The new path.
454 | */
455 | public static function get_plugin_dir( $path = '' ) {
456 | $dir = rtrim( plugin_dir_path( DLXPW_PATTERN_WRANGLER_FILE ), '/' );
457 | if ( ! empty( $path ) && is_string( $path ) ) {
458 | $dir .= '/' . ltrim( $path, '/' );
459 | }
460 | return $dir;
461 | }
462 |
463 | /**
464 | * Return a plugin URL path.
465 | *
466 | * @param string $path Path to the file.
467 | *
468 | * @return string URL to to the file.
469 | */
470 | public static function get_plugin_url( $path = '' ) {
471 | $dir = rtrim( plugin_dir_url( DLXPW_PATTERN_WRANGLER_FILE ), '/' );
472 | if ( ! empty( $path ) && is_string( $path ) ) {
473 | $dir .= '/' . ltrim( $path, '/' );
474 | }
475 | return $dir;
476 | }
477 |
478 | /**
479 | * Gets the highest priority for a filter.
480 | *
481 | * @param int $subtract The amount to subtract from the high priority.
482 | *
483 | * @return int priority.
484 | */
485 | public static function get_highest_priority( $subtract = 0 ) {
486 | $highest_priority = PHP_INT_MAX;
487 | $subtract = absint( $subtract );
488 | if ( 0 === $subtract ) {
489 | --$highest_priority;
490 | } else {
491 | $highest_priority = absint( $highest_priority - $subtract );
492 | }
493 | return $highest_priority;
494 | }
495 | }
496 |
--------------------------------------------------------------------------------
/php/Options.php:
--------------------------------------------------------------------------------
1 | &$option ) {
46 | switch ( $key ) {
47 | case 'enabled':
48 | $option = filter_var( $options[ $key ], FILTER_VALIDATE_BOOLEAN );
49 | break;
50 | default:
51 | if ( is_array( $option ) ) {
52 | $option = Functions::sanitize_array_recursive( $option );
53 | } else {
54 | $option = sanitize_text_field( $options[ $key ] );
55 | }
56 | break;
57 | }
58 | }
59 | $options = wp_parse_args( $options, $current_options );
60 | if ( Functions::is_multisite() ) {
61 | update_site_option( self::$options_key, $options );
62 | } else {
63 | update_option( self::$options_key, $options );
64 | }
65 | self::$options = $options;
66 | return $options;
67 | }
68 |
69 | /**
70 | * Return a list of options.
71 | *
72 | * @param bool $force Whether to get options from cache or not.
73 | *
74 | * @return array Array of options.
75 | */
76 | public static function get_options( $force = false ) {
77 | if ( is_array( self::$options ) && ! $force ) {
78 | return self::$options;
79 | }
80 | if ( Functions::is_multisite() ) {
81 | $options = get_site_option( self::$options_key, array() );
82 | } else {
83 | $options = get_option( self::$options_key, array() );
84 | }
85 |
86 | $defaults = self::get_defaults();
87 | $options = wp_parse_args( $options, $defaults );
88 | self::$options = $options;
89 | return $options;
90 | }
91 |
92 | /**
93 | * Get defaults for SCE options
94 | *
95 | * @since 1.0.0
96 | * @access public
97 | *
98 | * @return array default options
99 | */
100 | public static function get_defaults() {
101 |
102 | $defaults = array(
103 | 'hideAllPatterns' => false,
104 | 'hidePatternsMenu' => false, /* only if hideAllPatterns is true, place in the Appearance menu */
105 | 'hideCorePatterns' => false,
106 | 'hideRemotePatterns' => false,
107 | 'hideThemePatterns' => false,
108 | 'hidePluginPatterns' => false,
109 | 'hideCoreSyncedPatterns' => false,
110 | 'hideCoreUnsyncedPatterns' => false,
111 | 'disablePatternImporterBlock' => false,
112 | 'categories' => array(),
113 | 'allowFrontendPatternPreview' => true,
114 | 'hideUncategorizedPatterns' => false,
115 | 'showCustomizerUI' => true,
116 | 'showMenusUI' => true,
117 | 'loadCustomizerCSSBlockEditor' => false,
118 | 'loadCustomizerCSSFrontend' => true,
119 | 'makePatternsExportable' => false,
120 | );
121 | return $defaults;
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/php/Preview.php:
--------------------------------------------------------------------------------
1 | post_type ) {
47 | return;
48 | }
49 | $deps = require_once Functions::get_plugin_dir( 'build/dlx-pw-preview.asset.php' );
50 | wp_enqueue_script(
51 | 'dlx-pattern-wrangler-preview',
52 | Functions::get_plugin_url( 'build/dlx-pw-preview.js' ),
53 | $deps['dependencies'],
54 | $deps['version'],
55 | true
56 | );
57 | wp_localize_script(
58 | 'dlx-pattern-wrangler-preview',
59 | 'dlxPatternWranglerPreview',
60 | array(
61 | 'previewUrl' => Functions::get_pattern_preview_url( get_the_ID() ),
62 | )
63 | );
64 | }
65 |
66 | /**
67 | * Override the template for the wp_block post type.
68 | *
69 | * @param string $template Template path.
70 | *
71 | * @return string Updated path.
72 | */
73 | public function maybe_override_template( $template ) {
74 | $preview = get_query_var( 'dlxpw_preview' );
75 | if ( ! $preview ) {
76 | return $template;
77 | }
78 | $template = Functions::get_plugin_dir( 'templates/pattern.php' );
79 | return $template;
80 | }
81 |
82 | /**
83 | * Add preview query var to frontend.
84 | *
85 | * @param array $query_vars Array of query vars.
86 | *
87 | * @return array updated query vars.
88 | */
89 | public function add_preview_query_var( $query_vars ) {
90 | $query_vars[] = 'dlxpw_preview';
91 | return $query_vars;
92 | }
93 |
94 | /**
95 | * Add a preview button to the quick actions for the wp_block post type.
96 | *
97 | * @param array $actions Array of actions.
98 | * @param WP_Post $post Post object.
99 | *
100 | * @return array
101 | */
102 | public function add_preview_button_quick_action( $actions, $post ) {
103 | if ( 'wp_block' === $post->post_type ) {
104 | $actions['preview_pattern'] = sprintf(
105 | '%s',
106 | esc_url_raw( Functions::get_pattern_preview_url( $post->ID ) ),
107 | esc_html__( 'Preview', 'pattern-wrangler' )
108 | );
109 | }
110 | return $actions;
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/phpcs.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | .
16 |
17 | /node_modules/
18 | /vendor/
19 | /lib/
20 | /build/
21 | /dist/
22 | /php/Plugin_Updater.php
23 |
24 |
--------------------------------------------------------------------------------
/readme.txt:
--------------------------------------------------------------------------------
1 | === Pattern Wrangler - Manage WordPress Block Patterns Effortlessly ===
2 | Contributors: ronalfy
3 | Tags: patterns, reusable blocks, block editor, shortcode, block management
4 | Requires at least: 6.5
5 | Tested up to: 6.7
6 | Requires PHP: 7.2
7 | Stable tag: 1.2.0
8 | License: GPLv2 or later
9 | License URI: https://www.gnu.org/licenses/gpl-2.0.html
10 |
11 | Manage your block patterns efficiently with Pattern Wrangler.
12 |
13 | == Description ==
14 |
15 | Pattern Wrangler makes managing WordPress block patterns simple and efficient, with features that cater to both beginners and advanced users. Whether you're organizing patterns for a complex site, a hybrid setup, or just hiding ones you don't need, Pattern Wrangler has you covered.
16 |
17 | Here are the major features:
18 |
19 | * **Hide All Patterns** - Completely hide patterns from the block editor in one click. This also hides the Patterns menu item.
20 | * **Selective Hiding** - Hide core, remote, theme, or plugin patterns while keeping your custom patterns visible. You can also hide synced and unsynced patterns together or separately.
21 | * **Category Management** - Disable, map, and rename registered categories from themes and plugins for better organization. This will help you keep local and registered patterns organized together.
22 | * **Output Patterns Anywhere** - Use a shortcode to display local patterns in page builders, widgets, your theme,or other blocks.
23 | * **Pattern Preview** - Preview a pattern on the frontend with shortcuts in the block editor or from the Patterns post list view.
24 | * **Cross-Site Pattern Copying** - Transfer patterns, including the remote images, between WordPress sites effortlessly. This is useful if you're copying a pattern from one site to another or copying a pattern from a production site to a development site.
25 |
26 | > Pattern Wrangler integrates seamlessly with block-based and classic themes offering a hybrid setup with unmatched flexibility.
27 |
28 | === Quick Links ===
29 |
30 | All Features and Documentation | Sponsor Us | Pattern Wrangler Home
31 |
32 | > Source code is available on GitHub.
33 |
34 | === Requirements and Compatibility ===
35 |
36 | Requires WordPress 6.5 or higher. 6.7 is recommended.
37 |
38 | Fully compatible with most themes, including block themes. Ideal for hybrid setups.
39 |
40 | == Installation ==
41 |
42 | 1. Upload the plugin files to the `/wp-content/plugins/pattern-wrangler` directory, or install the plugin through the WordPress plugins screen directly.
43 | 2. Activate the plugin through the 'Plugins' screen in WordPress.
44 | 3. Use the plugin through the block editor by adding new patterns or importing existing ones.
45 |
46 | == Frequently Asked Questions ==
47 |
48 | = Can I import Patterns from any WordPress site? =
49 |
50 | Yes! If you have the pattern's code, Pattern Wrangler can import it and localize any associated images.
51 |
52 | = Can I use Patterns in page builders like Elementor? =
53 |
54 | Yes! You can use the `[wp_block slug="pattern-slug"]` shortcode to output block patterns anywhere in your theme or other blocks.
55 |
56 | = Does this work with Block Themes? =
57 |
58 | Yes. Although it is designed for hybrid setups, it works with block themes, allowing you to merge theme and plugin-based patterns with your own local patterns stored in your database.
59 |
60 | Pattern Wrangler simply makes visible the default `wp_block` post type and category, which is where local patterns are stored.
61 |
62 | The Patterns view of this plugin uses the classic Patterns screen, with plans to eventually modernize it and put it on par with the Patterns viewer in the Full-Site Editor.
63 |
64 | == Screenshots ==
65 |
66 | 1. An example of an organized Patterns screen.
67 | 2. Enhanced Patterns List View with shortcode and category/sync columns.
68 | 3. Map registered categories to terms, or rename for better organization or translations.
69 | 4. Enable the Customizer UI, and load Additional CSS in the block editor.
70 | 5. Hide all patterns, or hide them from core, remote, themes, or plugins.
71 | 6. Preview a Pattern on the frontend.
72 |
73 | == Changelog ==
74 |
75 | = 1.2.0 =
76 | * Released 2024-12-18
77 | * New Feature: Show or hide all unsynced (non-reusable) patterns.
78 | * New Feature: Show or hide all synced (reusable) patterns.
79 | * New Feature: Disable both unsynced and synced patterns to completely disable all local patterns.
80 | * Bug fix: Preview button in the block editor has been fixed for WP 6.7.
81 | * Note: The next major version of Pattern Wrangler (i.e., 1.3.0) will only be compatible with WP 6.7 or higher. The 1.2.x series will involve minor improvements and bug fixes.
82 |
83 | = 1.1.2 =
84 | * Released 2024-08-16
85 | * Loading script translations is now working.
86 |
87 | = 1.1.1 =
88 | * Released 2024-08-16
89 | * Fixing admin script enqueueing for other language support.
90 |
91 | = 1.1.0 =
92 | * Released 2024-05-22
93 | * Updated Pattern Importer icon.
94 | * Added hooks to load custom headers/footers for the preview.
95 |
96 | = 1.0.10 =
97 | * Released 2024-04-18
98 | * Added miscelleanous option to make Patterns exportable via the WP exporter.
99 | * Fixed categories not showing when resetting options.
100 |
101 | = 1.0.9 =
102 | * Released 2024-04-14
103 | * Removed old dead code.
104 | * Fixing settings and docs links.
105 | * Initial WordPress.org release!
106 |
107 | = 1.0.7 =
108 | * Released 2024-04-12
109 | * Fixing sanitization issues.
110 | * Added Fancybox to Patterns screen. See @fancyapps/ui for more information.
111 | * Fixed issue with mapped patterns would not show up if a category was empty.
112 |
113 | = 1.0.3 =
114 | * Released 2024-04-09
115 | * Refactored categories so only registed categories can be mapped to terms.
116 |
117 | = 1.0.1 =
118 | * Added variable height preview image to Patterns screen.
119 | * Added Pattern Categories to Patterns menu item.
120 | * Removing unneeded code.
121 |
122 | = 1.0.0 =
123 | * Initial release.
124 |
125 | == Upgrade Notice ==
126 |
127 | = 1.2.0 =
128 | New features: Show or hide all synced or unsynced patterns, or disable both to completely disable all local patterns. Bug fix: Preview has been fixed for WP 6.7. Note: To keep up with the pace of WP development, the nex major version (i.e., 1.3.0) will have WP 6.7 and above as a requirement.
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import './js/blocks/pattern-importer/index';
--------------------------------------------------------------------------------
/src/js/blocks/commands/index.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { useCommand } from '@wordpress/commands';
3 | import { registerPlugin } from '@wordpress/plugins';
4 | import { settings, upload } from '@wordpress/icons';
5 | import {
6 | Modal,
7 | SelectControl,
8 | TextControl,
9 | Spinner,
10 | } from '@wordpress/components';
11 | import SendCommand from '../utils/SendCommand';
12 |
13 | const GBCommands = () => {
14 | const [ isModalOpen, setIsModalOpen ] = useState( false );
15 | const [ groupsLoading, setGroupsLoading ] = useState( false );
16 | const [ groups, setGroups ] = useState( [] );
17 |
18 | useCommand( {
19 | name: 'dlx-gb-admin-settings',
20 | label: 'Go to GenerateBlocks Settings',
21 | icon: settings,
22 | callback: () => {
23 | document.location.href = 'admin.php?page=generateblocks-settings';
24 | },
25 | context: 'block-editor',
26 | } );
27 | useCommand( {
28 | name: 'dlx-gb-local-patterns',
29 | label: 'Go to GenerateBlocks Local Patterns',
30 | icon: settings,
31 | callback: () => {
32 | document.location.href = 'edit.php?post_type=gblocks_templates';
33 | },
34 | context: 'block-editor',
35 | } );
36 | useCommand( {
37 | name: 'dlx-gb-global-styles',
38 | label: 'Go to GenerateBlocks Global Styles',
39 | icon: settings,
40 | callback: () => {
41 | document.location.href = 'edit.php?post_type=gblocks_templates';
42 | },
43 | context: 'block-editor',
44 | } );
45 | useCommand( {
46 | name: 'dlx-pattern-wrangler-Settings',
47 | label: 'Go to GenerateBlocks (GB) Hacks Settings',
48 | icon: settings,
49 | callback: () => {
50 | document.location.href = 'admin.php?page=dlx-pattern-wrangler';
51 | },
52 | context: 'block-editor',
53 | } );
54 | // useCommand( {
55 | // name: 'dlx-gb-svg-add-asset-library',
56 | // label: 'Add an SVG to the GenerateBlocks Asset Library',
57 | // icon: upload,
58 | // callback: async() => {
59 | // setIsModalOpen( true );
60 | // setGroupsLoading( true );
61 | // const response = await SendCommand(
62 | // gbHacksPatternInserter.restNonce,
63 | // {},
64 | // gbHacksPatternInserter.restUrl + '/get_asset_icon_groups',
65 | // 'get'
66 | // );
67 | // // Extract out data.
68 | // const { data, success } = response.data;
69 | // if ( success ) {
70 | // setGroups( data.groups );
71 | // }
72 | // setGroupsLoading( false );
73 | // },
74 | // context: 'block-editor',
75 | // } );
76 |
77 | // const getGroups = () => {
78 |
79 | // }
80 | return (
81 | <>
82 | { isModalOpen && (
83 | {
89 | setIsModalOpen( false );
90 | } }
91 | >
92 | { groupsLoading && (
93 | <>
94 |
95 | >
96 | ) }
97 |
98 | ) }
99 | >
100 | );
101 | };
102 |
103 | registerPlugin( 'dlxgb-commands', {
104 | render: GBCommands,
105 | } );
106 |
--------------------------------------------------------------------------------
/src/js/blocks/components/AlertButton/editor.scss:
--------------------------------------------------------------------------------
1 | .alerts-dlx-button-popover-base-control {
2 | padding: 16px;
3 | }
4 | .alertx-dlx-button-link {
5 | max-width: 100%;
6 | border: 1px solid #ddd;
7 | border-radius: 2px;
8 | }
9 |
10 | .alerts-dlx-button-wrapper {
11 | position: relative;
12 | display: inline-flex;
13 | align-items: center;
14 | }
15 | .alerts-dlx-button-wrapper {
16 | .alertx-dlx-button-link-icon {
17 | visibility: hidden;
18 | }
19 |
20 | &:hover .alertx-dlx-button-link-icon, &:focus .alertx-dlx-button-link-icon{
21 | visibility: visible;
22 | }
23 | }
24 | .alerts-dlx-link-toggle {
25 | margin-top: 15px;
26 | }
--------------------------------------------------------------------------------
/src/js/blocks/components/AlertButton/index.js:
--------------------------------------------------------------------------------
1 | import {
2 | ToggleControl,
3 | Button,
4 | Popover,
5 | BaseControl,
6 | SlotFillProvider,
7 | } from '@wordpress/components';
8 | import { URLInput, RichText } from '@wordpress/block-editor';
9 | import { link } from '@wordpress/icons';
10 | import { useState } from '@wordpress/element';
11 | import { __ } from '@wordpress/i18n';
12 | import './editor.scss';
13 |
14 | const AlertButton = ( props ) => {
15 | const [ isPopOverVisible, setIsPopOverVisible ] = useState( false );
16 | const [ isFocusedOutside, setIsFocusedOutside ] = useState( false );
17 |
18 | const { attributes, setAttributes } = props;
19 |
20 | const { buttonText, buttonUrl, buttonTarget, buttonRelNoFollow, buttonRelSponsored } =
21 | attributes;
22 |
23 | const toggleVisible = () => {
24 | setIsPopOverVisible( ( state ) => ! state );
25 | };
26 |
27 | return (
28 | <>
29 |
33 |
49 |
129 | >
130 | );
131 | };
132 |
133 | export default AlertButton;
134 |
--------------------------------------------------------------------------------
/src/js/blocks/components/GBHacksIcon/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | const GBHacksIcon = ( props ) => (
3 |
65 | );
66 | export default GBHacksIcon;
67 |
--------------------------------------------------------------------------------
/src/js/blocks/components/IconPicker/editor.scss:
--------------------------------------------------------------------------------
1 | .alerts-dlx-icon-popover .components-popover__content > div {
2 | min-width: 350px;
3 | }
4 | .alerts-dlx-icon-picker {
5 | padding: 16px;
6 | text-align: center;
7 |
8 | h2 {
9 | margin: 0;
10 | padding: 0;
11 | }
12 | }
13 | .alerts-dlx-icon-list {
14 | display: grid;
15 | grid-template-columns: 1fr 1fr 1fr;
16 | grid-gap: 10px;
17 | justify-content: center;
18 | max-height: 350px;
19 | overflow: auto;
20 | padding-top: 15px;
21 |
22 | li {
23 | text-align: center;
24 | }
25 | svg {
26 | width: 24px;
27 | height: 24px;
28 | }
29 | }
30 | .alerts-dlx-custom-icon-input {
31 | button {
32 | &:first-child {
33 | margin-right: 15px;
34 | }
35 | &:last-child {
36 | margin-left: 15px;
37 | }
38 | }
39 | }
40 | .components-base-control.alerts-dlx-icon-wrapper {
41 | font-size: 1em;
42 | }
43 | .alerts-dlx-icon-preview {
44 | display: flex;
45 | justify-content: center;
46 | font-size: 1em;
47 | svg {
48 | width: 1.2em;
49 | height: 1.2em;
50 | margin-top: 0.175em;
51 | }
52 | }
53 | .alerts-dlx-custom-icon-preview {
54 | svg {
55 | max-width: 48px;
56 | max-height: 48px;
57 | }
58 | }
59 | button.components-button.alerts-dlx-icon-preview-button {
60 | display: flex;
61 | align-items: flex-start;
62 | justify-content: center;
63 | padding: 0;
64 | margin: 0;
65 | font-size: inherit;
66 | line-height: 0;
67 | }
68 |
--------------------------------------------------------------------------------
/src/js/blocks/components/IconPicker/index.js:
--------------------------------------------------------------------------------
1 | import './editor.scss';
2 | import { __ } from '@wordpress/i18n';
3 | import { renderToString, useState } from '@wordpress/element';
4 | import {
5 | BaseControl,
6 | TextControl,
7 | Tooltip,
8 | Button,
9 | Popover,
10 | } from '@wordpress/components';
11 | import sanitizeSVG from '../../utils/sanitize-svg';
12 |
13 | const IconPicker = ( props ) => {
14 | const [ isCustomIcon, setIsCustomIcon ] = useState( false );
15 | const [ selectedIcon, setSelectedIcon ] = useState( props.defaultSvg );
16 | const [ isPopoverVisible, setIsPopOverVisible ] = useState( false );
17 | const [ isFocusedOutside, setIsFocusedOutside ] = useState( false );
18 |
19 | const { defaultSvg, setAttributes, icons } = props;
20 |
21 | /**
22 | * Retrieve popover content for custom icons or regular icons.
23 | *
24 | * @return {string} Popover content.
25 | */
26 | const getPopoverContent = () => {
27 | if ( ! isCustomIcon ) {
28 | return (
29 | <>
30 |
31 | { Object.keys( icons ).map( ( svg, i ) => {
32 | return (
33 | -
34 |
35 |
47 |
48 |
49 | );
50 | } ) }
51 |
52 |
66 | >
67 | );
68 | }
69 | // Return custom icon interface.
70 | return (
71 | <>
72 |
73 |
76 |
77 |
78 | {
82 | setSelectedIcon( value );
83 | } }
84 | />
85 |
96 |
104 |
105 | >
106 | );
107 | };
108 |
109 | const toggleVisible = () => {
110 | setIsPopOverVisible( ( state ) => ! state );
111 | };
112 |
113 | return (
114 | <>
115 |
116 |
117 |
129 |
130 |
131 | { isPopoverVisible && (
132 | {
133 | setIsFocusedOutside( true );
134 | setIsPopOverVisible( false );
135 | } }>
136 |
137 | { __( 'Select an Icon', 'alerts-dlx' ) }
138 | { getPopoverContent() }
139 |
140 |
141 | ) }
142 | >
143 | );
144 | };
145 | export default IconPicker;
146 |
--------------------------------------------------------------------------------
/src/js/blocks/components/icons/AlertsLogo.js:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | const AlertsLogo = ( props ) => (
4 |
38 | );
39 |
40 | export default AlertsLogo;
41 |
--------------------------------------------------------------------------------
/src/js/blocks/components/icons/BootstrapLogo.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-unused-vars
2 | import * as React from 'react';
3 |
4 | const BootstrapLogo = ( props ) => (
5 |
19 |
20 | );
21 |
22 | export default BootstrapLogo;
23 |
--------------------------------------------------------------------------------
/src/js/blocks/components/icons/ChakraUILogo.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-unused-vars
2 | import * as React from 'react';
3 |
4 | const ChakraUILogo = ( props ) => (
5 |
57 | );
58 |
59 | export default ChakraUILogo;
60 |
--------------------------------------------------------------------------------
/src/js/blocks/components/icons/ContainerLogo.js:
--------------------------------------------------------------------------------
1 | const ContainerLogo = () => {
2 | return (
3 |
4 | );
5 | }
6 | export default ContainerLogo;
--------------------------------------------------------------------------------
/src/js/blocks/components/icons/MaterialIconsLogo.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-unused-vars
2 | import * as React from 'react';
3 |
4 | const MaterialIconsLogo = ( props ) => (
5 |
18 | );
19 |
20 | export default MaterialIconsLogo;
21 |
--------------------------------------------------------------------------------
/src/js/blocks/components/icons/ReplaceIcon.js:
--------------------------------------------------------------------------------
1 | const ReplaceIcon = () => {
2 | return (
3 |
15 | );
16 | };
17 | export default ReplaceIcon;
18 |
--------------------------------------------------------------------------------
/src/js/blocks/components/icons/ShoelaceLogo.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-unused-vars
2 | import * as React from 'react';
3 |
4 | const ShoelaceLogo = ( props ) => (
5 |
12 | );
13 |
14 | export default ShoelaceLogo;
15 |
--------------------------------------------------------------------------------
/src/js/blocks/components/unit-picker/editor.scss:
--------------------------------------------------------------------------------
1 | /* Unit Picker Component - Forked from @GenerateBlocks */
2 | .components-has-units-control-header__units {
3 | display: flex;
4 | justify-content: space-between;
5 | margin-bottom: 5px;
6 | align-items: center;
7 | }
8 |
9 | .components-has-control__units {
10 | .components-has-control-buttons__units {
11 | button.components-button {
12 | background: #fff;
13 | box-shadow: none !important;
14 | color: #929da7;
15 | font-size: 10px;
16 | padding: 0 5px;
17 | position: relative;
18 | text-align: center;
19 | text-shadow: none;
20 | border: 0;
21 | border-radius: 0 !important;
22 | line-height: 20px;
23 | padding: 0 5px;
24 | height: auto;
25 |
26 | &.is-primary {
27 | background: #fff !important;
28 | color: #000 !important;
29 | cursor: default;
30 | z-index: 1;
31 | font-weight: bold;
32 | }
33 | }
34 | }
35 | }
--------------------------------------------------------------------------------
/src/js/blocks/components/unit-picker/index.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Unit Picker Component.
3 | * Credit: Forked from @GenerateBlocks
4 | */
5 |
6 | import { __, sprintf, _x } from '@wordpress/i18n';
7 | import './editor.scss';
8 |
9 | import { ButtonGroup, Button, Tooltip } from '@wordpress/components';
10 |
11 | const UnitChooser = ( props ) => {
12 | const { label, value, onClick, units } = props;
13 |
14 | return (
15 |
16 |
{ label }
17 |
18 |
19 |
23 | { units.map( ( unit ) => {
24 | let unitName = unit;
25 |
26 | if ( 'px' === unit ) {
27 | unitName = _x(
28 | 'Pixel',
29 | 'A size unit for CSS markup',
30 | 'quotes-dlx'
31 | );
32 | }
33 |
34 | if ( 'em' === unit ) {
35 | unitName = _x(
36 | 'Em',
37 | 'A size unit for CSS markup',
38 | 'quotes-dlx'
39 | );
40 | }
41 |
42 | if ( '%' === unit ) {
43 | unitName = _x(
44 | 'Percentage',
45 | 'A size unit for CSS markup',
46 | 'quotes-dlx'
47 | );
48 | }
49 |
50 | if ( 'vw' === unit ) {
51 | unitName = _x(
52 | 'View Width',
53 | 'A size unit for CSS markup',
54 | 'quotes-dlx'
55 | );
56 | }
57 |
58 | if ( 'rem' === unit ) {
59 | unitName = _x(
60 | 'Rem',
61 | 'A size unit for CSS markup',
62 | 'quotes-dlx'
63 | );
64 | }
65 |
66 | if ( 'deg' === unit ) {
67 | unitName = _x(
68 | 'Degree',
69 | 'A size unit for CSS markup',
70 | 'quotes-dlx'
71 | );
72 | }
73 |
74 | return (
75 |
83 |
98 |
99 | );
100 | } ) }
101 |
102 |
103 |
104 | );
105 | };
106 |
107 | export default UnitChooser;
108 |
--------------------------------------------------------------------------------
/src/js/blocks/pattern-importer/block.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef */
2 | /* eslint-disable no-unused-vars */
3 | /* eslint-disable camelcase */
4 | /**
5 | * External dependencies
6 | */
7 |
8 | import classnames from 'classnames';
9 | import { useState } from 'react';
10 | import { __ } from '@wordpress/i18n';
11 | import uniqueId from 'lodash.uniqueid';
12 | import {
13 | PanelBody,
14 | PanelRow,
15 | ToggleControl,
16 | TextControl,
17 | Button,
18 | ButtonGroup,
19 | RangeControl,
20 | BaseControl,
21 | TextareaControl,
22 | Card,
23 | CardHeader,
24 | CardFooter,
25 | CardBody,
26 | Spinner,
27 | CheckboxControl,
28 | } from '@wordpress/components';
29 |
30 | import { parse } from '@wordpress/blocks';
31 | import { useDispatch } from '@wordpress/data';
32 |
33 | import {
34 | InspectorControls,
35 | RichText,
36 | useBlockProps,
37 | useInnerBlocksProps,
38 | insertBlocks,
39 | store,
40 | } from '@wordpress/block-editor';
41 |
42 | import { useInstanceId } from '@wordpress/compose';
43 | import SendCommand from '../utils/SendCommand';
44 |
45 | // Image RegEx.
46 | const imageUrlRegex = /(http(?:s?):)([\/|.|@|\w|\s|-])*\.(?:jpg|gif|png|jpeg|webp|avif)/gi;
47 | const uniqueIdRegex = /\"uniqueId\"\:\"([^"]+)\"/gi;
48 |
49 | // Unique ID storing.
50 | const uniqueIds = [];
51 |
52 | // For storing the number of images imported.
53 | let imageCount = 0;
54 |
55 | const escapeRegExp = ( content ) => {
56 | return content.replace( /[.*+\-?^${}()|[\]\\]/g, '\\$&' ); // $& means the whole matched string
57 | };
58 |
59 | const PatternImporter = ( props ) => {
60 | // Shortcuts.
61 | const { attributes, setAttributes, clientId } = props;
62 |
63 | const [ patternText, setPatternText ] = useState( '' );
64 | const [ patternImages, setPatternImages ] = useState( [] );
65 | const [ patternBackgroundImages, setPatternBackgroundImages ] = useState( [] );
66 | const [ importing, setImporting ] = useState( false );
67 | const [ imageProcessingCount, setImageProcessingCount ] = useState( 0 );
68 | const [ doNotImportRemoteImages, setDoNotImportRemoteImages ] = useState( false );
69 |
70 | const { replaceBlock } = useDispatch( store );
71 |
72 | const onPatternSubmit = async() => {
73 | setImporting( true );
74 | const processImage = async( imgUrl, imgAlt ) => {
75 | const response = await SendCommand(
76 | dlxPWPatternInserter.restNonce,
77 | {
78 | imgUrl,
79 | imgAlt,
80 | },
81 | dlxPWPatternInserter.restUrl + '/process_image'
82 | );
83 | return response;
84 | };
85 |
86 | /**
87 | * Import a pattern.
88 | *
89 | * @param {string} pattern The pattern.
90 | */
91 | const importPattern = ( pattern ) => {
92 | pattern = replaceUniqueIds( pattern );
93 |
94 | // Convert pattern to blocks.
95 | try {
96 | const patternBlocks = parse( pattern );
97 |
98 | replaceBlock( clientId, patternBlocks );
99 |
100 | // Insert block in place of this one.
101 | //replaceInnerBlocks( clientId, patternBlocks );
102 | } catch ( error ) {
103 | }
104 | };
105 |
106 | const matches = [ ...patternText.matchAll( imageUrlRegex ) ];
107 | const imagesToProcess = [];
108 | let localPatternText = patternText;
109 |
110 | if ( ! doNotImportRemoteImages ) {
111 | // If there are matches, we need to process them.
112 | if ( matches.length ) {
113 | matches.forEach( ( match ) => {
114 | // Push if not a duplicate.
115 | if ( ! imagesToProcess.includes( match[ 0 ] ) ) {
116 | imagesToProcess.push( match[ 0 ] );
117 | }
118 | } );
119 | setPatternImages( imagesToProcess );
120 | }
121 |
122 | const imagesProcessed = [];
123 | let imagePromises = [];
124 |
125 | // Let's loop through images and process.
126 | if ( imagesToProcess.length ) {
127 | imagePromises = imagesToProcess.map( ( image ) => {
128 | try {
129 | const response = processImage( image, '' );
130 | response.then( ( restResponse ) => {
131 | imagesProcessed.push( image );
132 | const { data, success } = restResponse.data;
133 | if ( success ) {
134 | imageCount++;
135 | setImageProcessingCount( imageCount );
136 |
137 | // Get the image URL and replace in pattern.
138 | const newImageUrl = data.attachmentUrl;
139 |
140 | // Replace old URL with new URL.
141 | localPatternText = localPatternText.replace( image, newImageUrl );
142 | setPatternText( localPatternText );
143 | } else {
144 | // Fail silently.
145 | imageCount++;
146 | setImageProcessingCount( imageCount );
147 | }
148 | } ).catch( ( error ) => {
149 | // Fail silently.
150 | imageCount++;
151 | setImageProcessingCount( imageCount );
152 | } );
153 | return response;
154 | } catch ( error ) {
155 | // Fail silently.
156 | imageCount++;
157 | setImageProcessingCount( imageCount );
158 | }
159 | } );
160 | }
161 |
162 | Promise.all( imagePromises ).then( () => {
163 | importPattern( localPatternText );
164 | } ).catch( ( error ) => {
165 | importPattern( localPatternText );
166 | } );
167 | } else {
168 | importPattern( localPatternText );
169 | }
170 | };
171 |
172 | /**
173 | * Return and generate a new unique ID.
174 | *
175 | * @param {string} blockPatternText The block pattern text.
176 | *
177 | * @return {string} The blockPatternText.
178 | */
179 | const replaceUniqueIds = ( blockPatternText ) => {
180 | const pwUniqueIdMatches = [ ...blockPatternText.matchAll( uniqueIdRegex ) ];
181 |
182 | if ( pwUniqueIdMatches.length ) {
183 | // Loop through matches, generate unique ID, and replace.
184 | pwUniqueIdMatches.forEach( ( match ) => {
185 | const newUniqueId = generateUniqueId();
186 | uniqueIds.push( newUniqueId );
187 | blockPatternText.replace( match[ 1 ], `"uniqueId":"${ newUniqueId }"` );
188 | } );
189 | }
190 | return blockPatternText;
191 | };
192 |
193 | /**
194 | * Return and generate a new unique ID.
195 | *
196 | * @return {string} The uniqueId.
197 | */
198 | const generateUniqueId = () => {
199 | // Get the substr of current client ID for prefix.
200 | const prefix = clientId.substring( 2, 9 ).replace( '-', '' );
201 | const newUniqueId = uniqueId( prefix );
202 |
203 | // Make sure it isn't in the array already. Recursive much?
204 | if ( uniqueIds.includes( newUniqueId ) ) {
205 | return generateUniqueId();
206 | }
207 | return newUniqueId;
208 | };
209 |
210 | const block = (
211 | <>
212 |
213 |
214 | { __( 'Pattern Importer', 'pattern-wrangler' ) }
215 |
216 |
217 | setPatternText( value ) }
222 | disabled={ importing }
223 | />
224 | setDoNotImportRemoteImages( value ) }
228 | disabled={ importing }
229 | />
230 |
231 |
232 |
239 | { importing && (
240 |
241 |
242 | {
243 | `Processing ${ imageCount } of ${ patternImages.length } images.`
244 | }
245 |
246 | ) }
247 |
248 |
249 | >
250 | );
251 |
252 | const blockProps = useBlockProps( { className: 'dlx-pattern-inserter-wrapper' } );
253 |
254 | return (
255 | <>
256 | { block }
257 | >
258 | );
259 | };
260 |
261 | export default PatternImporter;
262 |
--------------------------------------------------------------------------------
/src/js/blocks/pattern-importer/block.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://schemas.wp.org/trunk/block.json",
3 | "title": "Pattern Inserter",
4 | "apiVersion": 3,
5 | "name": "dlxplugins/dlx-pw-pattern-inserter",
6 | "category": "design",
7 | "icon": "",
8 | "description": "Paste in a pattern and it will be inserted for you.",
9 | "keywords": ["remote", "pattern", "inserter"],
10 | "version": "1.0.0",
11 | "textdomain": "dlx-pattern-wrangler",
12 | "attributes": {
13 | "preview": {
14 | "type": "boolean",
15 | "default": false
16 | }
17 | },
18 | "example": {
19 | "attributes": {
20 | "preview": true
21 | }
22 | },
23 | "editorScript": "dlx-pw-pattern-inserter-block"
24 | }
25 |
--------------------------------------------------------------------------------
/src/js/blocks/pattern-importer/index.js:
--------------------------------------------------------------------------------
1 | import { registerBlockType, createBlock } from '@wordpress/blocks';
2 | import Edit from './block';
3 | import metaData from './block.json';
4 |
5 | const PatternIcon = (
6 |
81 | );
82 |
83 | registerBlockType(metaData, {
84 | edit: Edit,
85 | save() {
86 | return null;
87 | },
88 | icon: PatternIcon,
89 | });
90 |
--------------------------------------------------------------------------------
/src/js/blocks/plugins/pattern-preview.js:
--------------------------------------------------------------------------------
1 | import { __ } from '@wordpress/i18n';
2 | import { registerPlugin } from '@wordpress/plugins';
3 | import { useSelect } from '@wordpress/data';
4 |
5 | // Try to get ActionItem, but don't fail if it's not available
6 | let PluginPreviewMenuItem;
7 | try {
8 | const { PluginPreviewMenuItem: ImportedPluginPreviewMenuItem } = require( '@wordpress/editor' );
9 | PluginPreviewMenuItem = ImportedPluginPreviewMenuItem;
10 | } catch ( e ) {
11 | // ActionItem not available
12 | }
13 |
14 | /**
15 | * Render a Preview Button.
16 | *
17 | * @return {Object|null} The rendered component or null if ActionItem not available.
18 | */
19 | const PatternPreviewButton = () => {
20 | // Return early if ActionItem isn't available
21 | if ( ! PluginPreviewMenuItem ) {
22 | return null;
23 | }
24 |
25 | return (
26 | {
30 | window.open( dlxPatternWranglerPreview.previewUrl, '_blank' );
31 | } }
32 | >
33 | { __( 'Preview Pattern', 'pattern-wrangler' ) }
34 |
35 | );
36 | };
37 |
38 | // Only register if ActionItem is available
39 | if ( PluginPreviewMenuItem ) {
40 | registerPlugin( 'dlx-pattern-wrangler-preview-button', {
41 | render: PatternPreviewButton,
42 | } );
43 | }
44 |
45 |
--------------------------------------------------------------------------------
/src/js/blocks/sidebar/index.js:
--------------------------------------------------------------------------------
1 | import { registerPlugin } from '@wordpress/plugins';
2 | import { PluginSidebar } from '@wordpress/edit-post';
3 | import { PanelBody } from '@wordpress/components';
4 | import GBHacksIcon from '../components/GBHacksIcon';
5 | const MySidebar = () => (
6 |
10 |
11 | asdfsadf
12 |
13 |
14 | );
15 |
16 | registerPlugin( 'dlx-gb-sidebar', {
17 | icon: ,
18 | render: MySidebar,
19 | } );
20 |
--------------------------------------------------------------------------------
/src/js/blocks/utils/SendCommand.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef */
2 | /* eslint-disable camelcase */
3 | import axios from 'axios';
4 | import qs from 'qs';
5 |
6 | /**
7 | * Send a REST request via JS.
8 | *
9 | * @param {string} nonce The REST nonce.
10 | * @param {Object} data The REST data to pass.
11 | * @param {string} restEndPoint The REST endpoint to use.
12 | * @param {string} method The REST method to use. Defaults to 'post'.
13 | * @return {Promise} The REST request promise.
14 | */
15 | export default function SendCommand( nonce, data, restEndPoint, method = 'post' ) {
16 | if ( 'undefined' === typeof data ) {
17 | data = {};
18 | }
19 |
20 | const options = {
21 | method,
22 | url: restEndPoint,
23 | params: data,
24 | headers: {
25 | 'X-WP-Nonce': nonce,
26 | },
27 | data,
28 | };
29 |
30 | return axios( options );
31 | }
32 |
--------------------------------------------------------------------------------
/src/js/blocks/utils/sanitize-svg/index.js:
--------------------------------------------------------------------------------
1 | import DOMPurify from 'dompurify';
2 |
3 | export default function sanitizeSVG( svg ) {
4 | return DOMPurify.sanitize( svg, { USE_PROFILES: { svg: true, svgFilters: true } } );
5 | }
6 |
--------------------------------------------------------------------------------
/src/js/fancybox/index.js:
--------------------------------------------------------------------------------
1 | import {Fancybox } from '@fancyapps/ui/dist/fancybox/fancybox.umd.js';
2 | import "@fancyapps/ui/dist/fancybox/fancybox.css";
3 |
4 | document.addEventListener("DOMContentLoaded", function () {
5 | const patternPreviews = document.querySelectorAll( '.admin-fancybox' );
6 | if ( null !== patternPreviews ) {
7 | patternPreviews.forEach( function ( patternPreview ) {
8 | patternPreview.addEventListener( 'click', function ( event ) {
9 | event.preventDefault();
10 | const anchor = event.target.closest( 'a' );
11 | Fancybox.show( [ {
12 | src: anchor.href,
13 | caption: anchor.title,
14 | type: 'image',
15 | zoom: false,
16 | compact: true,
17 | width: '60%',
18 | } ] );
19 | } );
20 | } );
21 | }
22 | } );
--------------------------------------------------------------------------------
/src/js/react/components/Notice/index.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-unused-vars
2 | import React, { useEffect } from 'react';
3 | import PropTypes from 'prop-types'; // ES6
4 | import { speak } from '@wordpress/a11y';
5 | import { __ } from '@wordpress/i18n';
6 | import { Notice as WPNotice } from '@wordpress/components';
7 | import classNames from 'classnames';
8 |
9 | const Notice = ( props ) => {
10 | const { message, status, politeness, icon, className, inline, children, hasToTop = false } = props;
11 |
12 | useEffect( () => {
13 | speak( message, politeness );
14 | }, [ message, status, politeness ] );
15 |
16 | const hasIcon = () => {
17 | return icon !== null;
18 | };
19 | const getIcon = ( Icon ) => {
20 | return ;
21 | };
22 |
23 | const containerClasses = classNames( className, 'dlx-pw-admin__notice', {
24 | 'dlx-pw-admin__notice--has-icon': hasIcon(),
25 | [ `dlx-pw-admin__notice-type--${ status }` ]: true,
26 | [ `dlx-pw-admin__notice-appearance--inline` ]: inline,
27 | [ `dlx-pw-admin__notice-appearance--block` ]: ! inline,
28 | } );
29 |
30 | const actions = [
31 | {
32 | label: __( 'Back to Top', 'wp-dlx-pw-comments' ),
33 | url: '#dlx-pw-admin-header',
34 | variant: 'link',
35 | className: 'dlx-pw-admin__notice-action dlx-pw-admin__notice-action--to-top',
36 | } ];
37 | return (
38 |
39 |
40 | { hasIcon() &&
41 | { getIcon( icon ) }
42 | }
43 | <>{ message } { children } >
44 |
45 |
46 | );
47 | };
48 |
49 | Notice.defaultProps = {
50 | message: '',
51 | status: 'info',
52 | politeness: 'polite',
53 | icon: null,
54 | className: '',
55 | inline: false,
56 | hasToTop: false,
57 | };
58 |
59 | Notice.propTypes = {
60 | message: PropTypes.string.isRequired,
61 | status: PropTypes.oneOf( [ 'info', 'warning', 'success', 'error' ] ),
62 | politeness: PropTypes.oneOf( [ 'assertive', 'polite' ] ),
63 | icon: PropTypes.func,
64 | className: PropTypes.string,
65 | inline: PropTypes.bool,
66 | hasToTop: PropTypes.bool,
67 | };
68 |
69 | export default Notice;
70 |
--------------------------------------------------------------------------------
/src/js/react/components/SaveResetButtons/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Loader2, ClipboardCheck } from 'lucide-react';
3 | import { __ } from '@wordpress/i18n';
4 | import classNames from 'classnames';
5 | import { Button, Snackbar } from '@wordpress/components';
6 | import Notice from '../Notice';
7 | import SendCommand from '../../utils/SendCommand';
8 | import SnackPop from '../SnackPop';
9 |
10 | export function onSave( formData, setError ) {
11 |
12 | }
13 |
14 | export function onReset( { formValues, setError, reset } ) {
15 |
16 | }
17 |
18 | const SaveResetButtons = ( props ) => {
19 | // Gather props.
20 | const {
21 | formValues,
22 | setError,
23 | reset,
24 | errors,
25 | isDirty,
26 | dirtyFields,
27 | trigger,
28 | } = props;
29 |
30 | const [ saving, setSaving ] = useState( false );
31 | const [ resetting, setResetting ] = useState( false );
32 | const [ isSaved, setIsSaved ] = useState( false );
33 | const [ isReset, setIsReset ] = useState( false );
34 | const [ savePromise, setSavePromise ] = useState( null );
35 | const [ resetPromise, setResetPromise ] = useState( null );
36 |
37 | /**
38 | * Save the options by setting promise as state.
39 | */
40 | const saveOptions = async () => {
41 | const saveOptionsPromise = SendCommand( 'dlx_pw_save_options', { formData: formValues } );
42 | setSavePromise( saveOptionsPromise );
43 | setSaving( true );
44 | await saveOptionsPromise;
45 | setSaving( false );
46 | };
47 |
48 | /**
49 | * Reset the options by setting promise as state.
50 | */
51 | const resetOptions = async () => {
52 | const resetOptionsPromise = SendCommand( 'dlx_pw_reset_options', { formData: formValues } );
53 | setResetPromise( resetOptionsPromise );
54 | setResetting( true );
55 | const resetResponse = await resetOptionsPromise;
56 | reset(
57 | resetResponse.data.data.formData,
58 | {
59 | keepErrors: false,
60 | keepDirty: false,
61 | },
62 | );
63 | setResetting( false );
64 | };
65 |
66 | const hasErrors = () => {
67 | return Object.keys( errors ).length > 0;
68 | };
69 |
70 | const getSaveIcon = () => {
71 | if ( saving ) {
72 | return () => ;
73 | }
74 | if ( isSaved ) {
75 | return () => ;
76 | }
77 | return false;
78 | };
79 |
80 | const getSaveText = () => {
81 | if ( saving ) {
82 | return __( 'Saving…', 'pattern-wrangler' );
83 | }
84 | if ( isSaved ) {
85 | return __( 'Saved', 'pattern-wrangler' );
86 | }
87 | return __( 'Save Options', 'pattern-wrangler' );
88 | };
89 |
90 | const getResetText = () => {
91 | if ( resetting ) {
92 | return __( 'Resetting to Defaults…', 'pattern-wrangler' );
93 | }
94 | if ( isReset ) {
95 | return __( 'Options Restored to Defaults', 'pattern-wrangler' );
96 | }
97 | return __( 'Reset to Defaults', 'pattern-wrangler' );
98 | };
99 |
100 | return (
101 | <>
102 |
103 |
146 |
147 |
151 |
155 | { hasErrors() && (
156 |
164 | ) }
165 |
166 | >
167 | );
168 | };
169 | export default SaveResetButtons;
170 |
--------------------------------------------------------------------------------
/src/js/react/components/SnackPop/index.js:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from 'react';
2 | import { Loader2, CheckCircle2, AlertCircle } from 'lucide-react';
3 | import { Snackbar as WPSnackBar, Modal, Button } from '@wordpress/components';
4 | import classnames from 'classnames';
5 | import { __ } from '@wordpress/i18n';
6 | import Notice from '../Notice';
7 |
8 | /**
9 | * SnackPop is a component which handles alerts and notifications for the user.
10 | * It can handle multiple alerts at once, toggles and forms, and will display the notifications in a queue.
11 | *
12 | * @param {Object} props Component props.
13 | *
14 | * @return {Element} JSX markup for the component.
15 | */
16 | const SnackPop = ( props ) => {
17 | const { ajaxOptions, loadingMessage } = props;
18 |
19 | const snackbarDefaults = {
20 | type: 'info',
21 | message: '',
22 | title: '',
23 | isDismissable: false,
24 | isPersistent: false,
25 | isSuccess: false,
26 | loadingMessage,
27 | politeness: 'polite', /* can also be assertive */
28 | };
29 |
30 | const [ notificationOptions, setNotificationOptions ] = useState( snackbarDefaults );
31 | const [ isBusy, setIsBusy ] = useState( false );
32 | const [ isModalVisible, setIsModalVisible ] = useState( false );
33 | const [ isSnackbarVisible, setIsSnackbarVisible ] = useState( false );
34 | const [ snackbarTimeout, setSnackbarTimeout ] = useState( null );
35 |
36 | useEffect( () => {
37 | const getPromise = async () => {
38 | const response = await ajaxOptions;
39 | return response;
40 | };
41 | if ( ajaxOptions instanceof Promise ) {
42 | // Set state to busy.
43 | setNotificationOptions( snackbarDefaults );
44 | setIsSnackbarVisible( true );
45 | setIsBusy( true );
46 |
47 | getPromise().then( ( response ) => {
48 | const { data } = response;
49 | const { success: isSuccess } = data;
50 | const { data: responseData } = data;
51 |
52 | // Get the type of notification. (error, info, success, warning, critical, confirmation).
53 | const type = responseData.type || 'info';
54 |
55 | // Get the message.
56 | const message = responseData.message || '';
57 |
58 | // Get the title.
59 | const title = responseData.title || ''; /* title of snackbar or modal */
60 |
61 | // Get whether the notification is dismissable.
62 | const isDismissable = responseData.dismissable || false; /* whether the snackbar or modal is dismissable */
63 |
64 | // Get whether the notification is persistent.
65 | const isPersistent = responseData.persistent || false; /* whether the snackbar or modal is persistent */
66 |
67 | // Get the politeness based on if successful.
68 | const politeness = isSuccess ? 'polite' : 'assertive';
69 |
70 | // Set state with the notification.
71 | setNotificationOptions( {
72 | type,
73 | message,
74 | title,
75 | isDismissable,
76 | isBusy: false,
77 | isPersistent,
78 | politeness,
79 | } );
80 |
81 | if ( isSuccess ) {
82 | //onSuccess( notificationOptions );
83 | } else {
84 | //onError( notificationOptions );
85 | }
86 | if ( 'critical' === type ) {
87 | setIsSnackbarVisible( false );
88 | setIsModalVisible( true );
89 | } else {
90 | clearTimeout( snackbarTimeout );
91 | setSnackbarTimeout( setTimeout( () => {
92 | setIsSnackbarVisible( false );
93 | setNotificationOptions( snackbarDefaults );
94 | }, 6000 ) );
95 | }
96 | } ).catch( ( error ) => {
97 | // Handle error
98 | setNotificationOptions( {
99 | type: 'critical',
100 | message: error.message,
101 | title: __( 'An Error Has Occurred', 'pattern-wrangler' ),
102 | isDismissable: false,
103 | isBusy: false,
104 | isPersistent: true,
105 | politeness: 'assertive',
106 | } );
107 | //onError( notificationOptions );
108 | } ).then( () => {
109 | // Set state to not busy.
110 | setIsBusy( false );
111 | } );
112 | }
113 | }, [ ajaxOptions ] );
114 |
115 | // Bail if no promise.
116 | if ( null === ajaxOptions ) {
117 | return (
118 | <>>
119 | );
120 | }
121 |
122 | /**
123 | * Gets the icon for the notification.
124 | *
125 | * @return {Element} JSX markup for the icon.
126 | */
127 | const getIcon = () => {
128 | switch ( notificationOptions.type ) {
129 | case 'success':
130 | return ;
131 | case 'error':
132 | case 'critical':
133 | return ;
134 | default:
135 | return ;
136 | }
137 | };
138 |
139 | const getSnackbarActions = () => {
140 | const actions = [];
141 | if ( notificationOptions.type === 'success' ) {
142 | actions.push( {
143 | label: __( 'Back to Top', 'pattern-wrangler' ),
144 | url: '#dlx-pw-admin-header',
145 | variant: 'link',
146 | className: 'dlx-pw-admin__notice-action dlx-pw-admin__notice-action--to-top',
147 | } );
148 | }
149 | return actions;
150 | };
151 |
152 | const getSnackBar = () => {
153 | return (
154 | setIsSnackbarVisible( false ) }
166 | explicitDismiss={ notificationOptions.isDismissable }
167 | >
168 | { isBusy ? loadingMessage : notificationOptions.message }
169 |
170 | );
171 | };
172 |
173 | const getModal = () => {
174 | if ( 'critical' === notificationOptions.type ) {
175 | return (
176 | {
188 | setIsModalVisible( false );
189 | } }
190 | isDismissible={ true }
191 | shouldCloseOnClickOutside={ notificationOptions.isPersistent }
192 | shouldCloseOnEsc={ notificationOptions.isPersistent }
193 | >
194 |
201 |
202 | {
206 | setIsModalVisible( false );
207 | } }
208 | >
209 | { __( 'OK', 'pattern-wrangler' ) }
210 |
211 |
212 |
213 | );
214 | }
215 | };
216 |
217 | return (
218 | <>
219 | { isSnackbarVisible && getSnackBar() } { /* Show snackbar */ }
220 | { isModalVisible && getModal() } { /* Show modal */ }
221 | >
222 | );
223 | };
224 | export default SnackPop;
225 |
--------------------------------------------------------------------------------
/src/js/react/utils/SendCommand.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef */
2 | /* eslint-disable camelcase */
3 | import axios from 'axios';
4 | import qs from 'qs';
5 |
6 | export default function sendCommand( action, data, ajaxUrl = '' ) {
7 | const params = {
8 | action,
9 | };
10 |
11 | const default_data = {
12 | nonce: false,
13 | action,
14 | };
15 |
16 | if ( 'undefined' === typeof data ) {
17 | data = {};
18 | }
19 |
20 | for ( const opt in default_data ) {
21 | if ( ! data.hasOwnProperty( opt ) ) {
22 | data[ opt ] = default_data[ opt ];
23 | }
24 | }
25 |
26 | let sendAjaxUrl = '';
27 |
28 | if ( typeof ajaxurl === 'undefined' ) {
29 | sendAjaxUrl = ajaxUrl;
30 | } else {
31 | sendAjaxUrl = ajaxurl;
32 | }
33 |
34 | const options = {
35 | method: 'post',
36 | url: sendAjaxUrl,
37 | params,
38 | paramsSerializer( jsparams ) {
39 | return qs.stringify( jsparams, { arrayFormat: 'brackets' } );
40 | },
41 | data: qs.stringify( data ),
42 | };
43 |
44 | return axios( options );
45 | }
46 |
--------------------------------------------------------------------------------
/src/js/react/views/license/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { createRoot } from 'react-dom/client';
4 | import License from './license';
5 |
6 | const container = document.getElementById( 'dlx-pw-license' );
7 | const root = createRoot( container );
8 | root.render(
9 |
10 |
11 |
12 | );
13 |
--------------------------------------------------------------------------------
/src/js/react/views/license/license.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-unused-vars
2 | import React, { Suspense, useState } from 'react';
3 | import {
4 | Button,
5 | TextControl,
6 | } from '@wordpress/components';
7 | import { __ } from '@wordpress/i18n';
8 | import { useForm, Controller, useWatch, useFormState } from 'react-hook-form';
9 | import { useAsyncResource } from 'use-async-resource';
10 | import classNames from 'classnames';
11 | import { CheckCircle, Key, Loader2, Eye, EyeOff, AlertCircle } from 'lucide-react';
12 |
13 | // Local imports.
14 | import SendCommand from '../../utils/SendCommand';
15 | import Notice from '../../components/Notice';
16 |
17 | const retrieveOptions = () => {
18 | return SendCommand( 'dlx_pw_license_get_options', {
19 | nonce: dlxPatternWranglerLicense.getNonce,
20 | } );
21 | };
22 |
23 | const License = ( props ) => {
24 | const [ defaults ] = useAsyncResource(
25 | retrieveOptions,
26 | []
27 | );
28 | return (
29 |
32 | { __( 'Loading…', 'pattern-wrangler' ) }
33 | >
34 | }
35 | >
36 |
37 |
38 | );
39 | };
40 |
41 | const Interface = ( props ) => {
42 | const { defaults } = props;
43 | const response = defaults();
44 | const { data } = response.data;
45 |
46 | const [ showSecret, setShowSecret ] = useState( data.licenseValid ? false : true );
47 | const [ licenseData, setLicenseData ] = useState( data.licenseData );
48 | const [ saving, setSaving ] = useState( false );
49 | const [ isSaved, setIsSaved ] = useState( false );
50 | const [ betaSaving, setBetaSaving ] = useState( false );
51 | const [ betaEnabled, setBetaEnabled ] = useState( data.beta );
52 | const [ revokingLicense, setRevokingLicense ] = useState( false );
53 | const [ isRevoked, setIsRevoked ] = useState( false );
54 | const [ validLicense, setValidLicense ] = useState( data.licenseKey );
55 |
56 | const hasErrors = () => {
57 | return Object.keys( errors ).length > 0;
58 | };
59 |
60 | const {
61 | control,
62 | handleSubmit,
63 | getValues,
64 | reset,
65 | setError,
66 | trigger,
67 | setValue,
68 | } = useForm( {
69 | defaultValues: {
70 | licenseKey: data.licenseKey,
71 | priceId: data.priceId,
72 | licenseValid: data.licenseValid,
73 | },
74 | } );
75 |
76 |
77 | const formValues = useWatch( { control } );
78 | const { errors, isDirty, dirtyFields } = useFormState( {
79 | control,
80 | } );
81 |
82 |
83 | const onSubmit = ( formData ) => {
84 | setSaving( true );
85 | SendCommand( 'dlx_pw_save_license', {
86 | nonce: dlxPatternWranglerLicense.saveNonce,
87 | formData,
88 | } )
89 | .then( ( ajaxResponse ) => {
90 | const ajaxData = ajaxResponse.data.data;
91 | const ajaxSuccess = ajaxResponse.data.success;
92 |
93 |
94 | if ( ajaxSuccess ) {
95 | reset( ajaxData, {
96 | keepErrors: false,
97 | keepDirty: false,
98 | } );
99 | setLicenseData( ajaxData.licenseData );
100 | setIsSaved( true );
101 |
102 | // Reset count.
103 | setTimeout( () => {
104 | setIsSaved( false );
105 | }, 3000 );
106 | } else {
107 | // Error stuff.
108 | setError( 'licenseKey', {
109 | type: 'validate',
110 | message: ajaxData.message,
111 | } );
112 | }
113 | } )
114 | .catch( ( ajaxResponse ) => {} )
115 | .then( ( ajaxResponse ) => {
116 | setSaving( false );
117 | } );
118 | };
119 |
120 | const revokeLicense = ( e ) => {
121 | setRevokingLicense( true );
122 | SendCommand( 'dlx_pw_revoke_license', {
123 | nonce: dlxPatternWranglerLicense.revokeNonce,
124 | formData: formValues,
125 | } )
126 | .then( ( ajaxResponse ) => {
127 | const ajaxData = ajaxResponse.data.data;
128 | const ajaxSuccess = ajaxResponse.data.success;
129 | if ( ajaxSuccess ) {
130 | // // Reset count.
131 | setIsRevoked( true );
132 | reset( ajaxData, {
133 | keepErrors: false,
134 | keepDirty: false,
135 | } );
136 | setTimeout( () => {
137 | setIsRevoked( false );
138 | }, 3000 );
139 | } else {
140 | setError( 'licenseKey', {
141 | type: 'validate',
142 | message: __(
143 | 'Revoking the license resulted in an error and could not be deactivated. Please reactivate the license and try again.',
144 | 'pattern-wrangler'
145 | ),
146 | } );
147 | }
148 | } )
149 | .catch( ( ajaxResponse ) => {} )
150 | .then( ( ajaxResponse ) => {
151 | setRevokingLicense( false );
152 | } );
153 | };
154 |
155 | /**
156 | * Retrieve a license type for a user.
157 | *
158 | * @return {string} License type.
159 | */
160 | const getLicenseType = () => {
161 | switch ( getValues( 'priceId' ) ) {
162 | case '1':
163 | return 'Guru';
164 | case '2':
165 | return 'Freelancer';
166 | case '3':
167 | return 'Agency';
168 | case '4':
169 | return 'Unlimited';
170 | default:
171 | return 'Subscriber';
172 | }
173 | };
174 |
175 | /**
176 | * Retrieve a license notice.
177 | *
178 | * @return {React.ReactElement} Notice.
179 | */
180 | const getLicenseNotice = () => {
181 | if ( getValues( 'licenseValid' ) ) {
182 | return (
183 | }
188 | />
189 | );
190 | }
191 | return (
192 | }
200 | />
201 | );
202 | };
203 |
204 | const getSaveButton = () => {
205 | let saveText = __( 'Save License', 'pattern-wrangler' );
206 | let saveTextLoading = __( 'Saving…', 'pattern-wrangler' );
207 |
208 | if ( ! getValues( 'licenseValid' ) ) {
209 | saveText = __( 'Activate License', 'pattern-wrangler' );
210 | saveTextLoading = __( 'Activating…', 'pattern-wrangler' );
211 | }
212 | return (
213 | <>
214 | : false }
224 | iconSize="1x"
225 | iconPosition="right"
226 | disabled={ saving || revokingLicense }
227 | variant="primary"
228 | />
229 | >
230 | );
231 | };
232 |
233 | return (
234 | <>
235 |
236 |
{ __( 'License', 'pattern-wrangler' ) }
237 | {
238 | getLicenseNotice()
239 | }
240 |
241 | { /* eslint-disable-next-line no-unused-vars */ }
242 |
476 | >
477 | );
478 | };
479 |
480 | export default License;
481 |
--------------------------------------------------------------------------------
/src/js/react/views/main/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { createRoot } from 'react-dom/client';
4 | import { Popover, SlotFillProvider } from '@wordpress/components';
5 | import Main from './main';
6 |
7 | const container = document.getElementById( 'dlx-pattern-wrangler' );
8 | const root = createRoot( container );
9 | root.render(
10 |
11 |
12 |
13 |
14 |
15 |
16 | );
17 |
--------------------------------------------------------------------------------
/src/js/utils/index.js:
--------------------------------------------------------------------------------
1 | ( function() {
2 | 'use strict';
3 |
4 | const { __ } = wp.i18n;
5 | const { speak } = wp.a11y;
6 |
7 | /**
8 | * Determine if an element is visible or not.
9 | *
10 | * @param {Element} element The element to check if visible or not.
11 | * @return {boolean} true if visible, false if not.
12 | */
13 | const isVisible = ( element ) => {
14 | const style = window.getComputedStyle( element );
15 | return (
16 | style.display !== 'none' &&
17 | style.visibility !== 'hidden' &&
18 | style.opacity !== '0'
19 | );
20 | };
21 |
22 | // Set up copy event.
23 | const copyShortcodeButtons = document.querySelectorAll( '.dlxpw-copy-shortcode' );
24 |
25 | const clipboardSupported = typeof ClipboardItem !== 'undefined';
26 | if ( clipboardSupported && copyShortcodeButtons ) {
27 | copyShortcodeButtons.forEach( ( button ) => {
28 | button.classList.remove( 'dlx-copy-shortcode-hidden' );
29 |
30 | button.addEventListener( 'click', ( event ) => {
31 | event.preventDefault();
32 |
33 | // Get the value of the previous input element.
34 | const input = button.previousElementSibling;
35 | if ( input && input.tagName.toLowerCase() === 'input' ) {
36 | const shortcodeValue = input.value;
37 |
38 | // Copy the value to the clipboard.
39 | navigator.clipboard.writeText( shortcodeValue ).then( () => {
40 | // Logs success message in console.
41 |
42 | // a11y text here.
43 | speak( __( 'Shortcode copied to clipboard', 'pattern-wrangler' ), 'assertive' );
44 |
45 | const buttonIcon = button.querySelector( 'span' );
46 |
47 | // Replace button content with "Copied" text.
48 | buttonIcon.classList.remove( 'dashicons-clipboard' );
49 | buttonIcon.classList.add( 'dashicons-yes' );
50 |
51 | // Revert back to original state after 3 seconds.
52 | setTimeout( () => {
53 | buttonIcon.classList.remove( 'dashicons-yes' );
54 | buttonIcon.classList.add( 'dashicons-clipboard' );
55 | }, 3000 );
56 | } ).catch( ( error ) => {
57 | // Logs error message in console.
58 | console.error( 'Error copying shortcode to clipboard:', error );
59 | } );
60 | }
61 | } );
62 | } );
63 | }
64 | }() );
65 |
--------------------------------------------------------------------------------
/src/scss/admin-utils.scss:
--------------------------------------------------------------------------------
1 | // Copy to Clipboard
2 | .dlxpw-copy-shortcode {
3 | min-height: 30px;
4 | vertical-align: top;
5 | cursor: pointer;
6 | }
7 | .dlxpw-copy-shortcode-container {
8 | display: grid;
9 | grid-template-columns: 1fr 36px;
10 | column-gap: 0.25rem;
11 |
12 | input {
13 | display: inline-block;
14 | }
15 | }
--------------------------------------------------------------------------------
/src/scss/breakpoints-mixin.scss:
--------------------------------------------------------------------------------
1 | /* Responsive styles - In hindsight, should've used mobile-first */
2 | /* Mixin from: https://css-tricks.com/snippets/sass/mixin-manage-breakpoints/ */
3 | /* It's only used here, so no external file for mixin? */
4 | $breakpoints: (
5 | 'xs': 20em,
6 | 'small': 30em,
7 | 'medium': 48em,
8 | 'large': 64em,
9 | 'xl': 75em,
10 | 'xxl': 81.25em
11 | ) !default;
12 |
13 | @mixin respond-to($breakpoint) {
14 | // If the key exists in the map
15 | @if map-has-key($breakpoints, $breakpoint) {
16 | // Prints a media query based on the value
17 | @media (min-width: map-get($breakpoints, $breakpoint)) {
18 | @content;
19 | }
20 | }
21 |
22 | // If the key doesn't exist in the map
23 | @else {
24 | @warn "Unfortunately, no value could be retrieved from `#{$breakpoint}`. "
25 | + "Available breakpoints are: #{map-keys($breakpoints)}.";
26 | }
27 | }
--------------------------------------------------------------------------------
/src/scss/button-resets.scss:
--------------------------------------------------------------------------------
1 | button {
2 | &.button-reset,
3 | &.button-reset:focus,
4 | &.button-reset:hover {
5 | background: none;
6 | color: inherit;
7 | border: none;
8 | padding: 0;
9 | cursor: pointer;
10 | outline: inherit;
11 | text-transform: unset;
12 | }
13 | &.button-reset.show-cursor {
14 | cursor: pointer;
15 | }
16 | }
17 | .button-reset,
18 | .button-reset:focus,
19 | .button-reset:hover {
20 | button {
21 | background: none;
22 | color: inherit;
23 | border: none;
24 | padding: 0;
25 | cursor: pointer;
26 | outline: inherit;
27 | text-transform: unset;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/scss/common.scss:
--------------------------------------------------------------------------------
1 | @import 'button-resets.scss';
2 | @import 'block-editor.scss';
3 |
4 | $white: #ffffff;
5 |
6 | /* MaterialUI Fonts */
7 | .template-material *,
8 | .template-chakra *,
9 | .template-bootstrap * {
10 | font-family: 'Lato', 'Helvetica', 'Arial', sans-serif;
11 | }
12 |
13 | a.alerts-dlx-button.button-reset,
14 | a.alerts-dlx-button,
15 | button.button-reset,
16 | button.button-reset:hover,
17 | a.alerts-dlx-button,
18 | button.button-reset.components-button,
19 | button.button-reset.components-button:hover {
20 | padding: 10px 30px;
21 | border-radius: 0.3125em;
22 | margin-top: 0.5em;
23 | line-height: 1.2em;
24 | font-size: 1em;
25 | height: auto;
26 | text-decoration: none;
27 | transition: all 0.3s ease-in-out;
28 |
29 | & ~ .button-reset,
30 | & ~ .button-reset:hover {
31 | margin-top: 0.5em;
32 | padding: 0;
33 | transition: all 0.3s ease-in-out;
34 | }
35 | }
36 |
37 | .template-chakra {
38 | a.alerts-dlx-button.button-reset,
39 | a.alerts-dlx-button,
40 | button.button-reset,
41 | button.button-reset:hover,
42 | a.alerts-dlx-button,
43 | button.button-reset.components-button,
44 | button.button-reset.components-button:hover {
45 | border-radius: 0;
46 | }
47 | }
48 |
49 | /* Block margins on front-end and block editor */
50 | .template-material,
51 | .template-chakra,
52 | .template-bootstrap,
53 | .template-shoelace {
54 | margin-top: 1.5rem;
55 | margin-bottom: 1.5rem;
56 | margin-block-start: 1.5rem;
57 | margin-block-end: 1.5rem;
58 |
59 | figcaption h2 {
60 | font-family: 'Lato', 'Helvetica', 'Arial', sans-serif;
61 | font-weight: 900;
62 | }
63 | }
64 |
65 | /* Consolidate variants for ChakraUI options */
66 | .alerts-dlx-chakra-variants,
67 | .alerts-dlx-material-variants,
68 | .alerts-dlx-shoelace-variants,
69 | .alerts-dlx-chakra-mode,
70 | .alerts-dlx-material-mode,
71 | .alerts-dlx-bootstrap-mode {
72 | .components-button-group {
73 | display: grid;
74 | grid-template-columns: 1fr 1fr;
75 | row-gap: 8px;
76 | column-gap: 8px;
77 | justify-content: center;
78 | button {
79 | display: flex;
80 | justify-content: center;
81 | width: 100%;
82 | }
83 | }
84 | }
85 |
86 | figure.alerts-dlx-alert {
87 | text-align: left;
88 | box-sizing: border-box;
89 | }
90 | .alerts-dlx-content a,
91 | .alerts-dlx-content a:hover {
92 | color: inherit;
93 | text-decoration: underline;
94 | }
95 |
--------------------------------------------------------------------------------
/templates/pattern.php:
--------------------------------------------------------------------------------
1 | $pattern_id,
28 | 'post_type' => 'wp_block',
29 | )
30 | );
31 | if ( ! $wp_query->have_posts() ) {
32 | die( 'Pattern not found.' );
33 | } else {
34 | $wp_query->the_post();
35 | }
36 |
37 | /**
38 | * Action to output custom actions.
39 | */
40 | do_action( 'dlxpw_preview_actions' );
41 |
42 | // Get header if theme is not FSE theme.
43 | if ( ! wp_is_block_theme() ) {
44 | $blocks = do_blocks( $wp_query->post->post_content );
45 | $current_post = $wp_query->post;
46 |
47 | /**
48 | * Filter to use default header or not.
49 | *
50 | * @since 1.1.0
51 | */
52 | $use_default_header = apply_filters( 'dlxpw_use_default_header', true );
53 | if ( ! $use_default_header ) {
54 | /**
55 | * Action to output custom header.
56 | */
57 | do_action( 'dlxpw_default_header' );
58 | } else {
59 | get_header();
60 | }
61 | \setup_postdata( $current_post );
62 | the_content();
63 | } else {
64 | ?>
65 |
66 | >
67 |
68 |
69 | post->post_content );
72 | ?>
73 |
74 |
75 | >
76 |
77 |
78 |
81 |
84 |
108 |
111 |
112 |
113 |
114 |
115 | {
7 | return [
8 | {
9 | ...defaultConfig,
10 | module: {
11 | ...defaultConfig.module,
12 | rules: [ ...defaultConfig.module.rules ],
13 | },
14 | mode: env.mode,
15 | devtool: 'production' === env.mode ? false : 'source-map',
16 | entry: {
17 | index: '/src/index.js',
18 | 'dlx-pw-preview': './src/js/blocks/plugins/pattern-preview.js',
19 | 'dlx-pw-fancybox': './src/js/fancybox/index.js',
20 | },
21 | },
22 | {
23 | entry: {
24 | 'dlx-pw-admin': './src/js/react/views/main/index.js',
25 | 'dlx-pw-admin-license': './src/js/react/views/license/index.js',
26 | 'dlx-pw-admin-css': './src/scss/admin.scss',
27 | 'dlx-pw-post-utilities': './src/js/utils/index.js',
28 | 'dlx-pw-admin-utils-css': './src/scss/admin-utils.scss',
29 | },
30 | resolve: {
31 | alias: {
32 | react: path.resolve( 'node_modules/react' ),
33 | },
34 | },
35 | mode: env.mode,
36 | devtool: 'production' === env.mode ? false : 'source-map',
37 | output: {
38 | filename: '[name].js',
39 | sourceMapFilename: '[file].map[query]',
40 | assetModuleFilename: 'fonts/[name][ext]',
41 | clean: true,
42 | },
43 | module: {
44 | rules: [
45 | {
46 | test: /\.(js|jsx)$/,
47 | exclude: /(node_modules|bower_components)/,
48 | loader: 'babel-loader',
49 | options: {
50 | presets: [ '@babel/preset-env', '@babel/preset-react' ],
51 | plugins: [
52 | '@babel/plugin-transform-class-properties',
53 | '@babel/plugin-transform-arrow-functions',
54 | ],
55 | },
56 | },
57 | {
58 | test: /\.scss$/,
59 | exclude: /(node_modules|bower_components)/,
60 | use: [
61 | {
62 | loader: MiniCssExtractPlugin.loader,
63 | },
64 | {
65 | loader: 'css-loader',
66 | options: {
67 | sourceMap: true,
68 | },
69 | },
70 | {
71 | loader: 'resolve-url-loader',
72 | },
73 | {
74 | loader: 'sass-loader',
75 | options: {
76 | sourceMap: true,
77 | },
78 | },
79 | ],
80 | },
81 | {
82 | test: /\.css$/,
83 | use: [
84 | {
85 | loader: MiniCssExtractPlugin.loader,
86 | },
87 | {
88 | loader: 'css-loader',
89 | options: {
90 | sourceMap: true,
91 | },
92 | },
93 | 'sass-loader',
94 | ],
95 | },
96 | {
97 | test: /\.(woff2?|ttf|otf|eot|svg)$/,
98 | include: [ path.resolve( __dirname, 'fonts' ) ],
99 | exclude: /(node_modules|bower_components)/,
100 | type: 'asset/resource',
101 | },
102 | ],
103 | },
104 | plugins: [ new RemoveEmptyScriptsPlugin(), new MiniCssExtractPlugin(), new DependencyExtractionWebpackPlugin() ],
105 | },
106 | ];
107 | };
108 |
--------------------------------------------------------------------------------