diff --git a/docs/reference-guides/core-blocks.md b/docs/reference-guides/core-blocks.md
index 6c6879d36107ab..bd04a70ae871fd 100644
--- a/docs/reference-guides/core-blocks.md
+++ b/docs/reference-guides/core-blocks.md
@@ -339,7 +339,7 @@ Insert an image to make a visual statement. ([Source](https://github.com/WordPre
- **Name:** core/image
- **Category:** media
- **Supports:** anchor, color (~~background~~, ~~text~~), filter (duotone)
-- **Attributes:** align, alt, caption, height, href, id, linkClass, linkDestination, linkTarget, rel, sizeSlug, title, url, width
+- **Attributes:** align, alt, caption, enableLightbox, height, href, id, linkClass, linkDestination, linkTarget, rel, sizeSlug, title, url, width
## Latest Comments
diff --git a/lib/experimental/interactivity-api/script-loader.php b/lib/experimental/interactivity-api/script-loader.php
new file mode 100644
index 00000000000000..f03debc4c7fa18
--- /dev/null
+++ b/lib/experimental/interactivity-api/script-loader.php
@@ -0,0 +1,50 @@
+next_tag( array( 'tag' => 'script' ) );
+ $p->set_attribute( 'defer', true );
+ return $p->get_updated_html();
+ }
+ return $tag;
+}
+add_filter( 'script_loader_tag', 'gutenberg_interactivity_scripts_add_defer_attribute', 10, 2 );
diff --git a/lib/load.php b/lib/load.php
index 8cceb21906293b..785ac2fb1cbf57 100644
--- a/lib/load.php
+++ b/lib/load.php
@@ -97,6 +97,7 @@ function gutenberg_is_experiment_enabled( $name ) {
require __DIR__ . '/experimental/block-editor-settings-mobile.php';
require __DIR__ . '/experimental/block-editor-settings.php';
require __DIR__ . '/experimental/blocks.php';
+require __DIR__ . '/experimental/interactivity-api/script-loader.php';
require __DIR__ . '/experimental/navigation-theme-opt-in.php';
require __DIR__ . '/experimental/kses.php';
require __DIR__ . '/experimental/l10n.php';
diff --git a/package-lock.json b/package-lock.json
index 5154297bb17116..21251971fe606b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -7095,6 +7095,28 @@
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.7.tgz",
"integrity": "sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw=="
},
+ "@preact/signals": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@preact/signals/-/signals-1.1.3.tgz",
+ "integrity": "sha512-N09DuAVvc90bBZVRwD+aFhtGyHAmJLhS3IFoawO/bYJRcil4k83nBOchpCEoS0s5+BXBpahgp0Mjf+IOqP57Og==",
+ "requires": {
+ "@preact/signals-core": "^1.2.3"
+ }
+ },
+ "@preact/signals-core": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.3.0.tgz",
+ "integrity": "sha512-M+M3ZOtd1dtV/uasyk4SZu1vbfEJ4NeENv0F7F12nijZYedB5wSgbtZcuACyssnTznhF4ctUyrR0dZHuHfyWKA=="
+ },
+ "@preact/signals-react": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/@preact/signals-react/-/signals-react-1.3.1.tgz",
+ "integrity": "sha512-YHWoGAT2Mmv2OGlGx7CCCbaLjAH/InV9ytGAR+esX8Y0HJmMAw51QlqGYOD5GPA5LwimV7Ht1x7KEIegDZIoxg==",
+ "requires": {
+ "@preact/signals-core": "^1.3.0",
+ "use-sync-external-store": "^1.2.0"
+ }
+ },
"@radix-ui/primitive": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.0.tgz",
@@ -7253,6 +7275,28 @@
"@babel/runtime": "^7.13.10"
}
},
+ "@preact/signals": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@preact/signals/-/signals-1.1.3.tgz",
+ "integrity": "sha512-N09DuAVvc90bBZVRwD+aFhtGyHAmJLhS3IFoawO/bYJRcil4k83nBOchpCEoS0s5+BXBpahgp0Mjf+IOqP57Og==",
+ "requires": {
+ "@preact/signals-core": "^1.2.3"
+ }
+ },
+ "@preact/signals-core": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@preact/signals-core/-/signals-core-1.2.3.tgz",
+ "integrity": "sha512-Kui4p7PMcEQevBgsTO0JBo3gyQ88Q3qzEvsVCuSp11t0JcN4DmGCTJcGRVSCq7Bn7lGxJBO+57jNSzDoDJ+QmA=="
+ },
+ "@preact/signals-react": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/@preact/signals-react/-/signals-react-1.2.2.tgz",
+ "integrity": "sha512-GoESQ9n1bns2FD+8yqH7lBvQMavboKLCNEW+s0hs3Wcp5B1VHvVxwJo6aFs6rpxoh1/q8Tvwbi4vIeehBD2mzA==",
+ "requires": {
+ "@preact/signals-core": "^1.2.3",
+ "use-sync-external-store": "^1.2.0"
+ }
+ },
"@react-native-clipboard/clipboard": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@react-native-clipboard/clipboard/-/clipboard-1.9.0.tgz",
@@ -16862,6 +16906,7 @@
"version": "file:packages/block-library",
"requires": {
"@babel/runtime": "^7.16.0",
+ "@preact/signals": "^1.1.3",
"@wordpress/a11y": "file:packages/a11y",
"@wordpress/api-fetch": "file:packages/api-fetch",
"@wordpress/autop": "file:packages/autop",
@@ -16894,12 +16939,14 @@
"change-case": "^4.1.2",
"classnames": "^2.3.1",
"colord": "^2.7.0",
+ "deepsignal": "^1.3.0",
"escape-html": "^1.0.3",
"fast-average-color": "^9.1.1",
"fast-deep-equal": "^3.1.3",
"lodash": "^4.17.21",
"memize": "^1.1.0",
"micromodal": "^0.4.10",
+ "preact": "^10.13.2",
"remove-accents": "^0.4.2"
}
},
@@ -25510,7 +25557,7 @@
"array-ify": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz",
- "integrity": "sha1-nlKHYrSpBmrRY6aWKjZEGOlibs4=",
+ "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==",
"dev": true
},
"array-includes": {
@@ -30679,7 +30726,7 @@
"debuglog": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz",
- "integrity": "sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI=",
+ "integrity": "sha512-syBZ+rnAK3EgMsH2aYEOLUW7mZSY9Gb+0wUMCFsZvcmiz+HigA0LOcq/HoQqVuGG+EKykunc7QG2bzrponfaSw==",
"dev": true
},
"decache": {
@@ -30794,6 +30841,16 @@
"integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==",
"dev": true
},
+ "deepsignal": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/deepsignal/-/deepsignal-1.3.0.tgz",
+ "integrity": "sha512-tMh3g3F7Ka6gKALqu7uOzi/3Xm00mGWgWR3hp1GUzGGnTz2J926ime5aOe1haz233v4encyjTkZESr5R6hr8oQ==",
+ "requires": {
+ "@preact/signals": "^1.0.0",
+ "@preact/signals-core": "^1.0.0",
+ "@preact/signals-react": "^1.0.0"
+ }
+ },
"default-browser-id": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-1.0.4.tgz",
@@ -35511,7 +35568,7 @@
"git-remote-origin-url": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/git-remote-origin-url/-/git-remote-origin-url-2.0.0.tgz",
- "integrity": "sha1-UoJlna4hBxRaERJhEq0yFuxfpl8=",
+ "integrity": "sha512-eU+GGrZgccNJcsDH5LkXR3PB9M958hxc7sbA8DFJjrv9j4L2P/eZfKhM+QD6wyzpiv+b1BpK0XrYCxkovtjSLw==",
"dev": true,
"requires": {
"gitconfiglocal": "^1.0.0",
@@ -35558,7 +35615,7 @@
"gitconfiglocal": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/gitconfiglocal/-/gitconfiglocal-1.0.0.tgz",
- "integrity": "sha1-QdBF84UaXqiPA/JMocYXgRRGS5s=",
+ "integrity": "sha512-spLUXeTAVHxDtKsJc8FkFVgFtMdEN9qPGpL23VfSHx4fP4+Ds097IXLvymbnDH8FnmxX5Nr9bPw3A+AQ6mWEaQ==",
"dev": true,
"requires": {
"ini": "^1.3.2"
@@ -36832,7 +36889,7 @@
"humanize-ms": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
- "integrity": "sha1-xG4xWaKT9riW2ikxbYtv6Lt5u+0=",
+ "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
"dev": true,
"requires": {
"ms": "^2.0.0"
@@ -37848,7 +37905,7 @@
"is-text-path": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-1.0.1.tgz",
- "integrity": "sha1-Thqg+1G/vLPpJogAE5cgLBd1tm4=",
+ "integrity": "sha512-xFuJpne9oFz5qDaodwmmG08e3CawH/2ZV8Qqza1Ko7Sk8POWbkRdwIoAWVhqvq0XeUzANEhKo2n0IXUGBm7A/w==",
"dev": true,
"requires": {
"text-extensions": "^1.0.0"
@@ -39621,7 +39678,7 @@
"jsonparse": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz",
- "integrity": "sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA=",
+ "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==",
"dev": true
},
"jsprim": {
@@ -40721,7 +40778,7 @@
"lodash.ismatch": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.ismatch/-/lodash.ismatch-4.4.0.tgz",
- "integrity": "sha1-dWy1FQyjum8RCFp4hJZF8Yj4Xzc=",
+ "integrity": "sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==",
"dev": true
},
"lodash.isplainobject": {
@@ -47601,6 +47658,11 @@
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz",
"integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ=="
},
+ "preact": {
+ "version": "10.13.2",
+ "resolved": "https://registry.npmjs.org/preact/-/preact-10.13.2.tgz",
+ "integrity": "sha512-q44QFLhOhty2Bd0Y46fnYW0gD/cbVM9dUVtNTDKPcdXSMA7jfY+Jpd6rk3GB0lcQss0z5s/6CmVP0Z/hV+g6pw=="
+ },
"prelude-ls": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz",
@@ -48219,7 +48281,7 @@
"promzard": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/promzard/-/promzard-0.3.0.tgz",
- "integrity": "sha1-JqXW7ox97kyxIggwWs+5O6OCqe4=",
+ "integrity": "sha512-JZeYqd7UAcHCwI+sTOeUDYkvEU+1bQ7iE0UT1MgB/tERkAPkesW46MrpIySzODi+owTjZtiF8Ay5j9m60KmMBw==",
"dev": true,
"requires": {
"read": "1"
@@ -48253,7 +48315,7 @@
"proto-list": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz",
- "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=",
+ "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==",
"dev": true
},
"protocols": {
@@ -49810,7 +49872,7 @@
"read": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/read/-/read-1.0.7.tgz",
- "integrity": "sha1-s9oZvQUkMal2cdRKQmNK33ELQMQ=",
+ "integrity": "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ==",
"dev": true,
"requires": {
"mute-stream": "~0.0.4"
@@ -55167,7 +55229,7 @@
"temp-dir": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz",
- "integrity": "sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0=",
+ "integrity": "sha512-xZFXEGbG7SNC3itwBzI3RYjq/cEhBkx2hJuKGIUOcEULmkQExXiHat2z/qkISYsuR+IKumhEfKKbV5qXmhICFQ==",
"dev": true
},
"terminal-link": {
diff --git a/package.json b/package.json
index 45b1e5ef319d25..79f77b87c07484 100644
--- a/package.json
+++ b/package.json
@@ -87,6 +87,8 @@
"@wordpress/warning": "file:packages/warning",
"@wordpress/widgets": "file:packages/widgets",
"@wordpress/wordcount": "file:packages/wordcount",
+ "deepsignal": "1.3.0",
+ "preact": "10.13.2",
"wicg-inert": "3.1.2"
},
"devDependencies": {
diff --git a/packages/block-library/package.json b/packages/block-library/package.json
index 9a34e8ae084fa7..3b6ab1f0140804 100644
--- a/packages/block-library/package.json
+++ b/packages/block-library/package.json
@@ -27,10 +27,12 @@
"sideEffects": [
"build-style/**",
"src/**/*.scss",
- "{src,build,build-module}/*/init.js"
+ "{src,build,build-module}/*/init.js",
+ "{src,build,build-module}/utils/interactivity/index.js"
],
"dependencies": {
"@babel/runtime": "^7.16.0",
+ "@preact/signals": "^1.1.3",
"@wordpress/a11y": "file:../a11y",
"@wordpress/api-fetch": "file:../api-fetch",
"@wordpress/autop": "file:../autop",
@@ -63,12 +65,14 @@
"change-case": "^4.1.2",
"classnames": "^2.3.1",
"colord": "^2.7.0",
+ "deepsignal": "^1.3.0",
"escape-html": "^1.0.3",
"fast-average-color": "^9.1.1",
"fast-deep-equal": "^3.1.3",
"lodash": "^4.17.21",
"memize": "^1.1.0",
"micromodal": "^0.4.10",
+ "preact": "^10.13.2",
"remove-accents": "^0.4.2"
},
"peerDependencies": {
diff --git a/packages/block-library/src/image/block.json b/packages/block-library/src/image/block.json
index 92931455c1144c..93b433fc1e05f6 100644
--- a/packages/block-library/src/image/block.json
+++ b/packages/block-library/src/image/block.json
@@ -80,6 +80,9 @@
"source": "attribute",
"selector": "figure > a",
"attribute": "target"
+ },
+ "enableLightbox": {
+ "type": "boolean"
}
},
"supports": {
diff --git a/packages/block-library/src/image/edit.js b/packages/block-library/src/image/edit.js
index 19e8196dfc7a51..2946d057be1c39 100644
--- a/packages/block-library/src/image/edit.js
+++ b/packages/block-library/src/image/edit.js
@@ -111,6 +111,7 @@ export function ImageEdit( {
width,
height,
sizeSlug,
+ enableLightbox = true,
} = attributes;
const [ temporaryURL, setTemporaryURL ] = useState();
@@ -242,6 +243,7 @@ export function ImageEdit( {
...mediaAttributes,
...additionalAttributes,
linkDestination,
+ enableLightbox,
} );
}
diff --git a/packages/block-library/src/image/image.js b/packages/block-library/src/image/image.js
index c513ede8b9fe29..a1d981da93bbfa 100644
--- a/packages/block-library/src/image/image.js
+++ b/packages/block-library/src/image/image.js
@@ -15,6 +15,8 @@ import {
TextareaControl,
TextControl,
ToolbarButton,
+ ToggleControl,
+ __experimentalHeading as Heading,
} from '@wordpress/components';
import { useViewportMatch, usePrevious } from '@wordpress/compose';
import { useSelect, useDispatch } from '@wordpress/data';
@@ -96,6 +98,7 @@ export default function Image( {
height,
linkTarget,
sizeSlug,
+ enableLightbox,
} = attributes;
const imageRef = useRef();
const prevCaption = usePrevious( caption );
@@ -458,6 +461,24 @@ export default function Image( {
>
}
/>
+
+ Behaviors
+
+ {
+ setAttributes( {
+ enableLightbox: ! enableLightbox,
+ } );
+ } }
+ />
>
);
diff --git a/packages/block-library/src/image/index.php b/packages/block-library/src/image/index.php
index e05939a4d0feac..271e4d284ac420 100644
--- a/packages/block-library/src/image/index.php
+++ b/packages/block-library/src/image/index.php
@@ -14,6 +14,7 @@
* @return string Returns the block content with the data-id attribute added.
*/
function render_block_core_image( $attributes, $content ) {
+
$processor = new WP_HTML_Tag_Processor( $content );
$processor->next_tag( 'img' );
@@ -27,9 +28,52 @@ function render_block_core_image( $attributes, $content ) {
// which now wraps Image Blocks within innerBlocks.
// The data-id attribute is added in a core/gallery `render_block_data` hook.
$processor->set_attribute( 'data-id', $attributes['data-id'] );
+ }
+
+ $link_destination = isset( $attributes['linkDestination'] ) ? $attributes['linkDestination'] : 'none';
+ $enable_lightbox = isset( $attributes['enableLightbox'] ) ? $attributes['enableLightbox'] : false;
+
+ if ( 'none' === $link_destination && $enable_lightbox ) {
+
+ $aria_label = 'Open image lightbox';
+ if ( $processor->get_attribute( 'alt' ) ) {
+ $aria_label .= ' - ' . $processor->get_attribute( 'alt' );
+ }
+
+ $background_color = wp_get_global_styles( array( 'color', 'background' ) );
+ $close_button_icon = '';
+
$content = $processor->get_updated_html();
+
+ return
+<<
+
+
+ $content
+
+
+
+
+HTML;
}
- return $content;
+
+ return $processor->get_updated_html();
}
@@ -37,6 +81,13 @@ function render_block_core_image( $attributes, $content ) {
* Registers the `core/image` block on server.
*/
function register_block_core_image() {
+
+ wp_enqueue_script(
+ 'wp-interactivity-image',
+ plugins_url( '../interactive-blocks/image.min.js', __FILE__ ),
+ array( 'wp-interactivity-runtime' )
+ );
+
register_block_type_from_metadata(
__DIR__ . '/image',
array(
diff --git a/packages/block-library/src/image/interactivity.js b/packages/block-library/src/image/interactivity.js
new file mode 100644
index 00000000000000..1faad492bd6154
--- /dev/null
+++ b/packages/block-library/src/image/interactivity.js
@@ -0,0 +1,81 @@
+/**
+ * Internal dependencies
+ */
+import { store } from '../utils/interactivity';
+
+const raf = window.requestAnimationFrame;
+// Until useSignalEffects is fixed: https://github.com/preactjs/signals/issues/228
+const tick = () => new Promise( ( r ) => raf( () => raf( r ) ) );
+
+store( {
+ actions: {
+ core: {
+ image: {
+ showLightbox: ( { context } ) => {
+ context.core.image.initialized = true;
+ context.core.image.lightboxEnabled = true;
+ context.core.image.lastFocusedElement =
+ window.document.activeElement;
+ context.core.image.scrollPosition = window.scrollY;
+ },
+ hideLightbox: ( { context, event } ) => {
+ if ( context.core.image.lightboxEnabled ) {
+ // If scrolling, wait a moment before closing the lightbox.
+ if (
+ event.type === 'mousewheel' &&
+ Math.abs(
+ window.scrollY -
+ context.core.image.scrollPosition
+ ) < 5
+ ) {
+ return;
+ }
+ context.core.image.lightboxEnabled = false;
+
+ // We only want to focus the last focused element
+ // if the lightbox was closed by the keyboard.
+ // Note: Pressing Enter on a button will trigger
+ // a click event with a blank pointerType.
+ if (
+ ( event.key && event.type === 'keydown' ) ||
+ ( event.type === 'click' &&
+ event.pointerType === '' )
+ ) {
+ context.core.image.lastFocusedElement.focus();
+ }
+ }
+ },
+ handleKeydown: ( { context, actions, event } ) => {
+ if ( context.core.image.lightboxEnabled ) {
+ const isTabKeyPressed =
+ event.key === 'Tab' || event.keyCode === 9;
+ const escapeKeyPressed =
+ event.key === 'Escape' || event.keyCode === 27;
+
+ if ( isTabKeyPressed ) {
+ event.preventDefault();
+ }
+
+ if ( escapeKeyPressed || isTabKeyPressed ) {
+ actions.core.hideLightbox( { context, event } );
+ }
+ }
+ },
+ },
+ },
+ },
+ effects: {
+ core: {
+ image: {
+ initLightbox: async ( { context, ref } ) => {
+ if ( context.core.image.lightboxEnabled ) {
+ // We need to wait until the DOM is able
+ // to receive focus updates for accessibility
+ await tick();
+ ref.querySelector( '.close-button' ).focus();
+ }
+ },
+ },
+ },
+ },
+} );
diff --git a/packages/block-library/src/image/style.scss b/packages/block-library/src/image/style.scss
index 2dfb86228567a2..ddb420de63ea39 100644
--- a/packages/block-library/src/image/style.scss
+++ b/packages/block-library/src/image/style.scss
@@ -151,3 +151,101 @@
.wp-block-image figure {
margin: 0;
}
+
+.wp-lightbox-container {
+ button {
+ border: none;
+ background: none;
+ cursor: zoom-in;
+ }
+}
+
+.wp-lightbox-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ z-index: 100000;
+ overflow: hidden;
+ width: 100vw;
+ height: 100vh;
+ visibility: hidden;
+
+ .close-button {
+ font-size: 40px;
+ position: absolute;
+ top: 20px;
+ right: 20px;
+ cursor: pointer;
+ z-index: 5000000;
+ }
+
+ .wp-block-image {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: 100%;
+ z-index: 3000000;
+ position: absolute;
+ }
+
+ button {
+ border: none;
+ background: none;
+ }
+
+ .scrim {
+ width: 100%;
+ height: 100%;
+ position: absolute;
+ z-index: 2000000;
+ background-color: rgb(255, 255, 255);
+ opacity: 0.9;
+ }
+
+ &.initialized {
+ animation: both turn-off-visibility 300ms;
+
+ img {
+ animation: both turn-off-visibility 250ms;
+ }
+
+ &.active {
+ animation: both turn-on-visibility 250ms;
+
+ img {
+ animation: both turn-on-visibility 300ms;
+ }
+ }
+ }
+}
+
+@keyframes turn-on-visibility {
+ 0% {
+ opacity: 0;
+ visibility: hidden;
+ }
+ 1% {
+ opacity: 0;
+ visibility: visible;
+ }
+ 100% {
+ opacity: 1;
+ visibility: visible;
+ }
+}
+
+@keyframes turn-off-visibility {
+ 0% {
+ opacity: 1;
+ visibility: visible;
+ }
+ 99% {
+ opacity: 0;
+ visibility: visible;
+ }
+ 100% {
+ opacity: 0;
+ visibility: hidden;
+ }
+}
diff --git a/packages/block-library/src/utils/interactivity/constants.js b/packages/block-library/src/utils/interactivity/constants.js
new file mode 100644
index 00000000000000..f462753c9f8179
--- /dev/null
+++ b/packages/block-library/src/utils/interactivity/constants.js
@@ -0,0 +1 @@
+export const directivePrefix = 'data-wp-';
diff --git a/packages/block-library/src/utils/interactivity/directives.js b/packages/block-library/src/utils/interactivity/directives.js
new file mode 100644
index 00000000000000..1e3381a5c0d12e
--- /dev/null
+++ b/packages/block-library/src/utils/interactivity/directives.js
@@ -0,0 +1,193 @@
+/**
+ * External dependencies
+ */
+import { useContext, useMemo, useEffect } from 'preact/hooks';
+import { deepSignal, peek } from 'deepsignal';
+/**
+ * Internal dependencies
+ */
+import { createPortal } from './portals.js';
+
+/**
+ * Internal dependencies
+ */
+import { useSignalEffect } from './utils';
+import { directive } from './hooks';
+
+const isObject = ( item ) =>
+ item && typeof item === 'object' && ! Array.isArray( item );
+
+const mergeDeepSignals = ( target, source ) => {
+ for ( const k in source ) {
+ if ( typeof peek( target, k ) === 'undefined' ) {
+ target[ `$${ k }` ] = source[ `$${ k }` ];
+ } else if (
+ isObject( peek( target, k ) ) &&
+ isObject( peek( source, k ) )
+ ) {
+ mergeDeepSignals(
+ target[ `$${ k }` ].peek(),
+ source[ `$${ k }` ].peek()
+ );
+ }
+ }
+};
+
+export default () => {
+ // data-wp-context
+ directive(
+ 'context',
+ ( {
+ directives: {
+ context: { default: context },
+ },
+ props: { children },
+ context: inherited,
+ } ) => {
+ const { Provider } = inherited;
+ const inheritedValue = useContext( inherited );
+ const value = useMemo( () => {
+ const localValue = deepSignal( context );
+ mergeDeepSignals( localValue, inheritedValue );
+ return localValue;
+ }, [ context, inheritedValue ] );
+
+ return { children };
+ }
+ );
+
+ // data-wp-body
+ directive( 'body', ( { props: { children }, context: inherited } ) => {
+ const { Provider } = inherited;
+ const inheritedValue = useContext( inherited );
+ return createPortal(
+ { children },
+ document.body
+ );
+ } );
+
+ // data-wp-effect.[name]
+ directive( 'effect', ( { directives: { effect }, context, evaluate } ) => {
+ const contextValue = useContext( context );
+ Object.values( effect ).forEach( ( path ) => {
+ useSignalEffect( () => {
+ return evaluate( path, { context: contextValue } );
+ } );
+ } );
+ } );
+
+ // data-wp-init.[name]
+ directive( 'init', ( { directives: { init }, context, evaluate } ) => {
+ const contextValue = useContext( context );
+ Object.values( init ).forEach( ( path ) => {
+ useEffect( () => {
+ return evaluate( path, { context: contextValue } );
+ }, [] );
+ } );
+ } );
+
+ // data-wp-on.[event]
+ directive( 'on', ( { directives: { on }, element, evaluate, context } ) => {
+ const contextValue = useContext( context );
+ Object.entries( on ).forEach( ( [ name, path ] ) => {
+ element.props[ `on${ name }` ] = ( event ) => {
+ evaluate( path, { event, context: contextValue } );
+ };
+ } );
+ } );
+
+ // data-wp-class.[classname]
+ directive(
+ 'class',
+ ( {
+ directives: { class: className },
+ element,
+ evaluate,
+ context,
+ } ) => {
+ const contextValue = useContext( context );
+ Object.keys( className )
+ .filter( ( n ) => n !== 'default' )
+ .forEach( ( name ) => {
+ const result = evaluate( className[ name ], {
+ className: name,
+ context: contextValue,
+ } );
+ const currentClass = element.props.class || '';
+ const classFinder = new RegExp(
+ `(^|\\s)${ name }(\\s|$)`,
+ 'g'
+ );
+ if ( ! result )
+ element.props.class = currentClass
+ .replace( classFinder, ' ' )
+ .trim();
+ else if ( ! classFinder.test( currentClass ) )
+ element.props.class = currentClass
+ ? `${ currentClass } ${ name }`
+ : name;
+
+ useEffect( () => {
+ // This seems necessary because Preact doesn't change the class
+ // names on the hydration, so we have to do it manually. It doesn't
+ // need deps because it only needs to do it the first time.
+ if ( ! result ) {
+ element.ref.current.classList.remove( name );
+ } else {
+ element.ref.current.classList.add( name );
+ }
+ }, [] );
+ } );
+ }
+ );
+
+ // data-wp-bind.[attribute]
+ directive(
+ 'bind',
+ ( { directives: { bind }, element, context, evaluate } ) => {
+ const contextValue = useContext( context );
+ Object.entries( bind )
+ .filter( ( n ) => n !== 'default' )
+ .forEach( ( [ attribute, path ] ) => {
+ const result = evaluate( path, {
+ context: contextValue,
+ } );
+ element.props[ attribute ] = result;
+
+ useEffect( () => {
+ // This seems necessary because Preact doesn't change the attributes
+ // on the hydration, so we have to do it manually. It doesn't need
+ // deps because it only needs to do it the first time.
+ if ( result === false ) {
+ element.ref.current.removeAttribute( attribute );
+ } else {
+ element.ref.current.setAttribute(
+ attribute,
+ result === true ? '' : result
+ );
+ }
+ }, [] );
+ } );
+ }
+ );
+
+ // data-wp-ignore
+ directive(
+ 'ignore',
+ ( {
+ element: {
+ type: Type,
+ props: { innerHTML, ...rest },
+ },
+ } ) => {
+ // Preserve the initial inner HTML.
+ const cached = useMemo( () => innerHTML, [] );
+ return (
+
+ );
+ }
+ );
+};
diff --git a/packages/block-library/src/utils/interactivity/hooks.js b/packages/block-library/src/utils/interactivity/hooks.js
new file mode 100644
index 00000000000000..ca3bd20964d511
--- /dev/null
+++ b/packages/block-library/src/utils/interactivity/hooks.js
@@ -0,0 +1,76 @@
+/**
+ * External dependencies
+ */
+import { h, options, createContext } from 'preact';
+import { useRef, useMemo } from 'preact/hooks';
+/**
+ * Internal dependencies
+ */
+import { rawStore as store } from './store';
+
+// Main context.
+const context = createContext( {} );
+
+// WordPress Directives.
+const directiveMap = {};
+export const directive = ( name, cb ) => {
+ directiveMap[ name ] = cb;
+};
+
+// Resolve the path to some property of the store object.
+const resolve = ( path, ctx ) => {
+ // If path starts with !, remove it and save a flag.
+ const hasNegationOperator =
+ path[ 0 ] === '!' && !! ( path = path.slice( 1 ) );
+ let current = { ...store, context: ctx };
+ path.split( '.' ).forEach( ( p ) => ( current = current[ p ] ) );
+ return hasNegationOperator ? ! current : current;
+};
+
+// Generate the evaluate function.
+const getEvaluate =
+ ( { ref } = {} ) =>
+ ( path, extraArgs = {} ) => {
+ const value = resolve( path, extraArgs.context );
+ return typeof value === 'function'
+ ? value( {
+ ref: ref.current,
+ ...store,
+ ...extraArgs,
+ } )
+ : value;
+ };
+
+// Directive wrapper.
+const Directive = ( { type, directives, props: originalProps } ) => {
+ const ref = useRef( null );
+ const element = h( type, { ...originalProps, ref } );
+ const props = { ...originalProps, children: element };
+ const evaluate = useMemo( () => getEvaluate( { ref } ), [] );
+ const directiveArgs = { directives, props, element, context, evaluate };
+
+ for ( const d in directives ) {
+ const wrapper = directiveMap[ d ]?.( directiveArgs );
+ if ( wrapper !== undefined ) props.children = wrapper;
+ }
+
+ return props.children;
+};
+
+// Preact Options Hook called each time a vnode is created.
+const old = options.vnode;
+options.vnode = ( vnode ) => {
+ if ( vnode.props.__directives ) {
+ const props = vnode.props;
+ const directives = props.__directives;
+ delete props.__directives;
+ vnode.props = {
+ type: vnode.type,
+ directives,
+ props,
+ };
+ vnode.type = Directive;
+ }
+
+ if ( old ) old( vnode );
+};
diff --git a/packages/block-library/src/utils/interactivity/hydration.js b/packages/block-library/src/utils/interactivity/hydration.js
new file mode 100644
index 00000000000000..2fc34eeb64b9b5
--- /dev/null
+++ b/packages/block-library/src/utils/interactivity/hydration.js
@@ -0,0 +1,22 @@
+/**
+ * External dependencies
+ */
+import { hydrate } from 'preact';
+/**
+ * Internal dependencies
+ */
+import { toVdom, hydratedIslands } from './vdom';
+import { createRootFragment } from './utils';
+import { directivePrefix } from './constants';
+
+export const init = async () => {
+ document
+ .querySelectorAll( `[${ directivePrefix }island]` )
+ .forEach( ( node ) => {
+ if ( ! hydratedIslands.has( node ) ) {
+ const fragment = createRootFragment( node.parentNode, node );
+ const vdom = toVdom( node );
+ hydrate( vdom, fragment );
+ }
+ } );
+};
diff --git a/packages/block-library/src/utils/interactivity/index.js b/packages/block-library/src/utils/interactivity/index.js
new file mode 100644
index 00000000000000..6dbac1a45e88ca
--- /dev/null
+++ b/packages/block-library/src/utils/interactivity/index.js
@@ -0,0 +1,17 @@
+/**
+ * Internal dependencies
+ */
+import registerDirectives from './directives';
+import { init } from './hydration';
+export { store } from './store';
+
+/**
+ * Initialize the Interactivity API.
+ */
+registerDirectives();
+
+document.addEventListener( 'DOMContentLoaded', async () => {
+ await init();
+ // eslint-disable-next-line no-console
+ console.log( 'Interactivity API started' );
+} );
diff --git a/packages/block-library/src/utils/interactivity/portals.js b/packages/block-library/src/utils/interactivity/portals.js
new file mode 100644
index 00000000000000..80d70e76e9249b
--- /dev/null
+++ b/packages/block-library/src/utils/interactivity/portals.js
@@ -0,0 +1,82 @@
+import { createElement, render } from 'preact';
+
+/**
+ * @param {import('../../src/index').RenderableProps<{ context: any }>} props
+ */
+function ContextProvider(props) {
+ this.getChildContext = () => props.context;
+ return props.children;
+}
+
+/**
+ * Portal component
+ * @this {import('./internal').Component}
+ * @param {object | null | undefined} props
+ *
+ * TODO: use createRoot() instead of fake root
+ */
+function Portal(props) {
+ const _this = this;
+ let container = props._container;
+
+ _this.componentWillUnmount = function () {
+ render(null, _this._temp);
+ _this._temp = null;
+ _this._container = null;
+ };
+
+ // When we change container we should clear our old container and
+ // indicate a new mount.
+ if (_this._container && _this._container !== container) {
+ _this.componentWillUnmount();
+ }
+
+ // When props.vnode is undefined/false/null we are dealing with some kind of
+ // conditional vnode. This should not trigger a render.
+ if (props._vnode) {
+ if (!_this._temp) {
+ _this._container = container;
+
+ // Create a fake DOM parent node that manages a subset of `container`'s children:
+ _this._temp = {
+ nodeType: 1,
+ parentNode: container,
+ childNodes: [],
+ appendChild(child) {
+ this.childNodes.push(child);
+ _this._container.appendChild(child);
+ },
+ insertBefore(child, before) {
+ this.childNodes.push(child);
+ _this._container.appendChild(child);
+ },
+ removeChild(child) {
+ this.childNodes.splice(this.childNodes.indexOf(child) >>> 1, 1);
+ _this._container.removeChild(child);
+ }
+ };
+ }
+
+ // Render our wrapping element into temp.
+ render(
+ createElement(ContextProvider, { context: _this.context }, props._vnode),
+ _this._temp
+ );
+ }
+ // When we come from a conditional render, on a mounted
+ // portal we should clear the DOM.
+ else if (_this._temp) {
+ _this.componentWillUnmount();
+ }
+}
+
+/**
+ * Create a `Portal` to continue rendering the vnode tree at a different DOM node
+ * @param {import('./internal').VNode} vnode The vnode to render
+ * @param {import('./internal').PreactElement} container The DOM node to continue rendering in to.
+ */
+export function createPortal(vnode, container) {
+ const el = createElement(Portal, { _vnode: vnode, _container: container });
+ el.containerInfo = container;
+ return el;
+}
diff --git a/packages/block-library/src/utils/interactivity/store.js b/packages/block-library/src/utils/interactivity/store.js
new file mode 100644
index 00000000000000..d11af901352017
--- /dev/null
+++ b/packages/block-library/src/utils/interactivity/store.js
@@ -0,0 +1,45 @@
+/**
+ * External dependencies
+ */
+import { deepSignal } from 'deepsignal';
+
+const isObject = ( item ) =>
+ item && typeof item === 'object' && ! Array.isArray( item );
+
+const deepMerge = ( target, source ) => {
+ if ( isObject( target ) && isObject( source ) ) {
+ for ( const key in source ) {
+ if ( isObject( source[ key ] ) ) {
+ if ( ! target[ key ] ) Object.assign( target, { [ key ]: {} } );
+ deepMerge( target[ key ], source[ key ] );
+ } else {
+ Object.assign( target, { [ key ]: source[ key ] } );
+ }
+ }
+ }
+};
+
+const getSerializedState = () => {
+ // TODO: change the store tag ID for a better one.
+ const storeTag = document.querySelector(
+ `script[type="application/json"]#store`
+ );
+ if ( ! storeTag ) return {};
+ try {
+ const { state } = JSON.parse( storeTag.textContent );
+ if ( isObject( state ) ) return state;
+ throw Error( 'Parsed state is not an object' );
+ } catch ( e ) {
+ // eslint-disable-next-line no-console
+ console.log( e );
+ }
+ return {};
+};
+
+const rawState = getSerializedState();
+export const rawStore = { state: deepSignal( rawState ) };
+
+export const store = ( { state, ...block } ) => {
+ deepMerge( rawStore, block );
+ deepMerge( rawState, state );
+};
diff --git a/packages/block-library/src/utils/interactivity/utils.js b/packages/block-library/src/utils/interactivity/utils.js
new file mode 100644
index 00000000000000..21d15da2f94ff9
--- /dev/null
+++ b/packages/block-library/src/utils/interactivity/utils.js
@@ -0,0 +1,66 @@
+/**
+ * External dependencies
+ */
+import { useRef, useEffect } from 'preact/hooks';
+import { effect } from '@preact/signals';
+
+function afterNextFrame( callback ) {
+ const done = () => {
+ window.cancelAnimationFrame( raf );
+ setTimeout( callback );
+ };
+ const raf = window.requestAnimationFrame( done );
+}
+
+// Using the mangled properties:
+// this.c: this._callback
+// this.x: this._compute
+// https://github.com/preactjs/signals/blob/main/mangle.json
+function createFlusher( compute, notify ) {
+ let flush;
+ const dispose = effect( function () {
+ flush = this.c.bind( this );
+ this.x = compute;
+ this.c = notify;
+ return compute();
+ } );
+ return { flush, dispose };
+}
+
+// Version of `useSignalEffect` with a `useEffect`-like execution. This hook
+// implementation comes from this PR:
+// https://github.com/preactjs/signals/pull/290.
+//
+// We need to include it here in this repo until the mentioned PR is merged.
+export function useSignalEffect( cb ) {
+ const callback = useRef( cb );
+ callback.current = cb;
+
+ useEffect( () => {
+ const execute = () => callback.current();
+ const notify = () => afterNextFrame( eff.flush );
+ const eff = createFlusher( execute, notify );
+ return eff.dispose;
+ }, [] );
+}
+
+// For wrapperless hydration.
+// See https://gist.github.com/developit/f4c67a2ede71dc2fab7f357f39cff28c
+export const createRootFragment = ( parent, replaceNode ) => {
+ replaceNode = [].concat( replaceNode );
+ const s = replaceNode[ replaceNode.length - 1 ].nextSibling;
+ function insert( c, r ) {
+ parent.insertBefore( c, r || s );
+ }
+ return ( parent.__k = {
+ nodeType: 1,
+ parentNode: parent,
+ firstChild: replaceNode[ 0 ],
+ childNodes: replaceNode,
+ insertBefore: insert,
+ appendChild: insert,
+ removeChild( c ) {
+ parent.removeChild( c );
+ },
+ } );
+};
diff --git a/packages/block-library/src/utils/interactivity/vdom.js b/packages/block-library/src/utils/interactivity/vdom.js
new file mode 100644
index 00000000000000..07640319b88a8a
--- /dev/null
+++ b/packages/block-library/src/utils/interactivity/vdom.js
@@ -0,0 +1,94 @@
+/**
+ * External dependencies
+ */
+import { h } from 'preact';
+/**
+ * Internal dependencies
+ */
+import { directivePrefix as p } from './constants';
+
+const ignoreAttr = `${ p }ignore`;
+const islandAttr = `${ p }island`;
+const directiveParser = new RegExp( `${ p }([^.]+)\.?(.*)$` );
+
+export const hydratedIslands = new WeakSet();
+
+// Recursive function that transforms a DOM tree into vDOM.
+export function toVdom( root ) {
+ const treeWalker = document.createTreeWalker(
+ root,
+ 205 // ELEMENT + TEXT + COMMENT + CDATA_SECTION + PROCESSING_INSTRUCTION
+ );
+
+ function walk( node ) {
+ const { attributes, nodeType } = node;
+
+ if ( nodeType === 3 ) return [ node.data ];
+ if ( nodeType === 4 ) {
+ const next = treeWalker.nextSibling();
+ node.replaceWith( new window.Text( node.nodeValue ) );
+ return [ node.nodeValue, next ];
+ }
+ if ( nodeType === 8 || nodeType === 7 ) {
+ const next = treeWalker.nextSibling();
+ node.remove();
+ return [ null, next ];
+ }
+
+ const props = {};
+ const children = [];
+ const directives = {};
+ let hasDirectives = false;
+ let ignore = false;
+ let island = false;
+
+ for ( let i = 0; i < attributes.length; i++ ) {
+ const n = attributes[ i ].name;
+ if ( n[ p.length ] && n.slice( 0, p.length ) === p ) {
+ if ( n === ignoreAttr ) {
+ ignore = true;
+ } else if ( n === islandAttr ) {
+ island = true;
+ } else {
+ hasDirectives = true;
+ let val = attributes[ i ].value;
+ try {
+ val = JSON.parse( val );
+ } catch ( e ) {}
+ const [ , prefix, suffix ] = directiveParser.exec( n );
+ directives[ prefix ] = directives[ prefix ] || {};
+ directives[ prefix ][ suffix || 'default' ] = val;
+ }
+ } else if ( n === 'ref' ) {
+ continue;
+ }
+ props[ n ] = attributes[ i ].value;
+ }
+
+ if ( ignore && ! island )
+ return [
+ h( node.localName, {
+ ...props,
+ innerHTML: node.innerHTML,
+ __directives: { ignore: true },
+ } ),
+ ];
+ if ( island ) hydratedIslands.add( node );
+
+ if ( hasDirectives ) props.__directives = directives;
+
+ let child = treeWalker.firstChild();
+ if ( child ) {
+ while ( child ) {
+ const [ vnode, nextChild ] = walk( child );
+ if ( vnode ) children.push( vnode );
+ child = nextChild || treeWalker.nextSibling();
+ }
+ treeWalker.parentNode();
+ }
+
+ return [ h( node.localName, props, children ) ];
+ }
+
+ return walk( treeWalker.currentNode );
+}
diff --git a/tools/webpack/blocks.js b/tools/webpack/blocks.js
index f2c30984141fb2..928f5049031e9e 100644
--- a/tools/webpack/blocks.js
+++ b/tools/webpack/blocks.js
@@ -74,134 +74,209 @@ const createEntrypoints = () => {
}, {} );
};
-module.exports = {
- ...baseConfig,
- name: 'blocks',
- entry: createEntrypoints(),
- output: {
- devtoolNamespace: 'wp',
- filename: './build/block-library/blocks/[name].min.js',
- path: join( __dirname, '..', '..' ),
- },
- plugins: [
- ...plugins,
- new DependencyExtractionWebpackPlugin( { injectPolyfill: false } ),
- new CopyWebpackPlugin( {
- patterns: [].concat(
- [
- 'style',
- 'style-rtl',
- 'editor',
- 'editor-rtl',
- 'theme',
- 'theme-rtl',
- ].map( ( filename ) => ( {
- from: `./packages/block-library/build-style/*/${ filename }.css`,
- to( { absoluteFilename } ) {
- const [ , dirname ] = absoluteFilename.match(
- new RegExp(
- `([\\w-]+)${ escapeRegExp(
- sep
- ) }${ filename }\\.css$`
- )
- );
-
- return join(
- 'build/block-library/blocks',
- dirname,
- filename + '.css'
- );
- },
- transform: stylesTransform,
- } ) ),
- Object.entries( {
- './packages/block-library/src/':
- 'build/block-library/blocks/',
- './packages/edit-widgets/src/blocks/':
- 'build/edit-widgets/blocks/',
- './packages/widgets/src/blocks/': 'build/widgets/blocks/',
- } ).flatMap( ( [ from, to ] ) => [
- {
- from: `${ from }/**/index.php`,
+module.exports = [
+ {
+ ...baseConfig,
+ name: 'blocks',
+ entry: createEntrypoints(),
+ output: {
+ devtoolNamespace: 'wp',
+ filename: './build/block-library/blocks/[name].min.js',
+ path: join( __dirname, '..', '..' ),
+ },
+ plugins: [
+ ...plugins,
+ new DependencyExtractionWebpackPlugin( { injectPolyfill: false } ),
+ new CopyWebpackPlugin( {
+ patterns: [].concat(
+ [
+ 'style',
+ 'style-rtl',
+ 'editor',
+ 'editor-rtl',
+ 'theme',
+ 'theme-rtl',
+ ].map( ( filename ) => ( {
+ from: `./packages/block-library/build-style/*/${ filename }.css`,
to( { absoluteFilename } ) {
const [ , dirname ] = absoluteFilename.match(
new RegExp(
`([\\w-]+)${ escapeRegExp(
sep
- ) }index\\.php$`
+ ) }${ filename }\\.css$`
)
);
- return join( to, `${ dirname }.php` );
+ return join(
+ 'build/block-library/blocks',
+ dirname,
+ filename + '.css'
+ );
},
- transform: ( content ) => {
- const prefix = 'gutenberg_';
- content = content.toString();
+ transform: stylesTransform,
+ } ) ),
+ Object.entries( {
+ './packages/block-library/src/':
+ 'build/block-library/blocks/',
+ './packages/edit-widgets/src/blocks/':
+ 'build/edit-widgets/blocks/',
+ './packages/widgets/src/blocks/':
+ 'build/widgets/blocks/',
+ } ).flatMap( ( [ from, to ] ) => [
+ {
+ from: `${ from }/**/index.php`,
+ to( { absoluteFilename } ) {
+ const [ , dirname ] = absoluteFilename.match(
+ new RegExp(
+ `([\\w-]+)${ escapeRegExp(
+ sep
+ ) }index\\.php$`
+ )
+ );
- // Within content, search and prefix any function calls from
- // `prefixFunctions` list. This is needed because some functions
- // are called inside block files, but have been declared elsewhere.
- // So with the rename we can call Gutenberg override functions, but the
- // block will still call the core function when updates are back ported.
- content = content.replace(
- new RegExp( prefixFunctions.join( '|' ), 'g' ),
- ( match ) =>
- `${ prefix }${ match.replace(
- /^wp_/,
- ''
- ) }`
- );
+ return join( to, `${ dirname }.php` );
+ },
+ transform: ( content ) => {
+ const prefix = 'gutenberg_';
+ content = content.toString();
- // Within content, search for any function definitions. For
- // each, replace every other reference to it in the file.
- return (
- Array.from(
- content.matchAll(
- /^\s*function ([^\(]+)/gm
- )
- )
- .reduce( ( result, [ , functionName ] ) => {
- // Prepend the Gutenberg prefix, substituting any
- // other core prefix (e.g. "wp_").
- return result.replace(
- new RegExp( functionName, 'g' ),
- ( match ) =>
- prefix +
- match.replace( /^wp_/, '' )
- );
- }, content )
- // The core blocks override procedure takes place in
- // the init action default priority to ensure that core
- // blocks would have been registered already. Since the
- // blocks implementations occur at the default priority
- // and due to WordPress hooks behavior not considering
- // mutations to the same priority during another's
- // callback, the Gutenberg build blocks are modified
- // to occur at a later priority.
- .replace(
- /(add_action\(\s*'init',\s*'gutenberg_register_block_[^']+'(?!,))/,
- '$1, 20'
+ // Within content, search and prefix any function calls from
+ // `prefixFunctions` list. This is needed because some functions
+ // are called inside block files, but have been declared elsewhere.
+ // So with the rename we can call Gutenberg override functions, but the
+ // block will still call the core function when updates are back ported.
+ content = content.replace(
+ new RegExp(
+ prefixFunctions.join( '|' ),
+ 'g'
+ ),
+ ( match ) =>
+ `${ prefix }${ match.replace(
+ /^wp_/,
+ ''
+ ) }`
+ );
+
+ // Within content, search for any function definitions. For
+ // each, replace every other reference to it in the file.
+ return (
+ Array.from(
+ content.matchAll(
+ /^\s*function ([^\(]+)/gm
+ )
)
- );
+ .reduce(
+ ( result, [ , functionName ] ) => {
+ // Prepend the Gutenberg prefix, substituting any
+ // other core prefix (e.g. "wp_").
+ return result.replace(
+ new RegExp(
+ functionName,
+ 'g'
+ ),
+ ( match ) =>
+ prefix +
+ match.replace(
+ /^wp_/,
+ ''
+ )
+ );
+ },
+ content
+ )
+ // The core blocks override procedure takes place in
+ // the init action default priority to ensure that core
+ // blocks would have been registered already. Since the
+ // blocks implementations occur at the default priority
+ // and due to WordPress hooks behavior not considering
+ // mutations to the same priority during another's
+ // callback, the Gutenberg build blocks are modified
+ // to occur at a later priority.
+ .replace(
+ /(add_action\(\s*'init',\s*'gutenberg_register_block_[^']+'(?!,))/,
+ '$1, 20'
+ )
+ );
+ },
+ noErrorOnMissing: true,
},
- noErrorOnMissing: true,
- },
- {
- from: `${ from }/*/block.json`,
- to( { absoluteFilename } ) {
- const [ , dirname ] = absoluteFilename.match(
- new RegExp(
- `([\\w-]+)${ escapeRegExp(
- sep
- ) }block\\.json$`
- )
- );
+ {
+ from: `${ from }/*/block.json`,
+ to( { absoluteFilename } ) {
+ const [ , dirname ] = absoluteFilename.match(
+ new RegExp(
+ `([\\w-]+)${ escapeRegExp(
+ sep
+ ) }block\\.json$`
+ )
+ );
- return join( to, dirname, 'block.json' );
+ return join( to, dirname, 'block.json' );
+ },
},
+ ] )
+ ),
+ } ),
+ ].filter( Boolean ),
+ },
+ {
+ entry: {
+ image: './packages/block-library/src/image/interactivity.js',
+ },
+ output: {
+ devtoolNamespace: 'wp',
+ filename: './build/block-library/interactive-blocks/[name].min.js',
+ path: join( __dirname, '..', '..' ),
+ },
+ optimization: {
+ runtimeChunk: {
+ name: 'vendors',
+ },
+ splitChunks: {
+ cacheGroups: {
+ vendors: {
+ name: 'vendors',
+ test: /[\\/]node_modules[\\/]/,
+ minSize: 0,
+ chunks: 'all',
},
- ] )
- ),
- } ),
- ].filter( Boolean ),
-};
+ interactivity: {
+ name: 'interactivity',
+ test: /[\\/]utils\/interactivity[\\/]/,
+ chunks: 'all',
+ minSize: 0,
+ priority: -10,
+ },
+ },
+ },
+ },
+ module: {
+ rules: [
+ {
+ test: /\.(j|t)sx?$/,
+ exclude: /node_modules/,
+ use: [
+ {
+ loader: require.resolve( 'babel-loader' ),
+ options: {
+ cacheDirectory:
+ process.env.BABEL_CACHE_DIRECTORY || true,
+ babelrc: false,
+ configFile: false,
+ presets: [
+ [
+ '@babel/preset-react',
+ {
+ runtime: 'automatic',
+ importSource: 'preact',
+ },
+ ],
+ ],
+ },
+ },
+ ],
+ },
+ ],
+ },
+ },
+];
diff --git a/webpack.config.js b/webpack.config.js
index 9a29ed7782268d..f1c5ce803adc1b 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -5,4 +5,4 @@ const blocksConfig = require( './tools/webpack/blocks' );
const developmentConfigs = require( './tools/webpack/development' );
const packagesConfig = require( './tools/webpack/packages' );
-module.exports = [ blocksConfig, packagesConfig, ...developmentConfigs ];
+module.exports = [ ...blocksConfig, packagesConfig, ...developmentConfigs ];