diff --git a/.babelrc b/.babelrc
new file mode 100644
index 00000000..3a05da56
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1,10 @@
+{
+ "presets": [
+ [
+ "@babel/preset-react",
+ {
+ "throwIfNamespace": false
+ }
+ ]
+ ]
+}
diff --git a/package.json b/package.json
index d0c60e63..dff3a556 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,6 @@
"version": "0.1.0",
"author": "The WordPress Contributors",
"license": "GPL-2.0-or-later",
- "main": "build/index.js",
"scripts": {
"build": "webpack --mode=production",
"start": "webpack --mode=development --watch",
@@ -11,7 +10,8 @@
"test": "jest",
"test:watch": "jest --watch",
"plugin-zip": "wp-scripts plugin-zip",
- "wp-env": "wp-env"
+ "wp-env": "wp-env",
+ "ssr": "node ssr.mjs"
},
"prettier": {
"useTabs": true,
@@ -39,6 +39,7 @@
"dependencies": {
"@preact/signals": "^1.1.2",
"hpq": "^1.3.0",
- "preact": "^10.10.6"
+ "preact": "^10.10.6",
+ "ultrahtml": "^0.4.0"
}
}
diff --git a/src/blocks/favorites-number/block.json b/src/blocks/favorites-number/block.json
new file mode 100644
index 00000000..2b7216a6
--- /dev/null
+++ b/src/blocks/favorites-number/block.json
@@ -0,0 +1,15 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 2,
+ "name": "bhe/favorites-number",
+ "version": "0.1.0",
+ "title": "BHE - Favorites Number",
+ "category": "text",
+ "icon": "heart",
+ "description": "",
+ "textdomain": "bhe",
+ "editorScript": "file:./index.js",
+ "editorStyle": "file:./style.css",
+ "style": "file:./style-index.css",
+ "render": "file:./render.php"
+}
diff --git a/src/blocks/favorites-number/edit.js b/src/blocks/favorites-number/edit.js
new file mode 100644
index 00000000..0ed27f42
--- /dev/null
+++ b/src/blocks/favorites-number/edit.js
@@ -0,0 +1,19 @@
+import '@wordpress/block-editor';
+import { useBlockProps } from '@wordpress/block-editor';
+
+const Edit = () => {
+ return (
+
+

+
5
+
+ );
+};
+
+export default Edit;
diff --git a/src/blocks/favorites-number/index.js b/src/blocks/favorites-number/index.js
new file mode 100644
index 00000000..468e578a
--- /dev/null
+++ b/src/blocks/favorites-number/index.js
@@ -0,0 +1,8 @@
+import { registerBlockType } from '@wordpress/blocks';
+import Edit from './edit';
+import './style.scss';
+
+registerBlockType('bhe/favorites-number', {
+ edit: Edit,
+ save: () => null,
+});
diff --git a/src/blocks/favorites-number/render.php b/src/blocks/favorites-number/render.php
new file mode 100644
index 00000000..05743133
--- /dev/null
+++ b/src/blocks/favorites-number/render.php
@@ -0,0 +1,13 @@
+
+

+
+ 0
+
+
\ No newline at end of file
diff --git a/src/blocks/favorites-number/style.scss b/src/blocks/favorites-number/style.scss
new file mode 100644
index 00000000..cadd0f72
--- /dev/null
+++ b/src/blocks/favorites-number/style.scss
@@ -0,0 +1,4 @@
+.wp-block-bhe-favorites-number {
+ border: 1px solid rgb(209, 21, 21);
+ position: relative;
+}
\ No newline at end of file
diff --git a/src/blocks/favorites-number/view.js b/src/blocks/favorites-number/view.js
new file mode 100644
index 00000000..31d4f2d5
--- /dev/null
+++ b/src/blocks/favorites-number/view.js
@@ -0,0 +1,48 @@
+import wpx from '../../runtime/wpx';
+import { deepMerge } from '../../runtime/utils';
+
+wpx({
+ state: {
+ favorites: {
+ posts: [],
+ count: ({ state }) => state.favorites.posts.length,
+ },
+ },
+ selectors: {
+ favorites: {
+ isPostIncluded: ({ state, context: { post } }) =>
+ `https://s.w.org/images/core/emoji/14.0.0/svg/${
+ state.favorites.posts.includes(post.id) ? '2764' : '1f90d'
+ }.svg`,
+ isFavoritePostsEmpty: ({ state }) =>
+ `https://s.w.org/images/core/emoji/14.0.0/svg/${
+ state.favorites.posts.length !== 0 ? '2764' : '1f90d'
+ }.svg`,
+ },
+ },
+ actions: {
+ favorites: {
+ togglePost: ({ state, context }) => {
+ const index = state.favorites.posts.findIndex(
+ (post) => post === context.post.id
+ );
+ if (index === -1) state.favorites.posts.push(context.post.id);
+ else state.favorites.posts.splice(index, 1);
+ },
+ save: ({ state }) => {
+ localStorage.setItem(
+ 'wpmoviesdemo.favorites',
+ JSON.stringify(state.favorites)
+ );
+ },
+ restore: ({ state }) => {
+ deepMerge(
+ state.favorites,
+ JSON.parse(
+ localStorage.getItem('wpmoviesdemo.favorites')
+ ) || {}
+ );
+ },
+ },
+ },
+});
diff --git a/src/blocks/post-favorite/block.json b/src/blocks/post-favorite/block.json
new file mode 100644
index 00000000..e2c77c94
--- /dev/null
+++ b/src/blocks/post-favorite/block.json
@@ -0,0 +1,15 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 2,
+ "name": "bhe/post-favorite",
+ "version": "0.1.0",
+ "title": "BHE - Post Favorite",
+ "category": "text",
+ "icon": "heart",
+ "description": "",
+ "textdomain": "bhe",
+ "editorScript": "file:./index.js",
+ "editorStyle": "file:./style.css",
+ "style": "file:./style-index.css",
+ "render": "file:./render.php"
+}
diff --git a/src/blocks/post-favorite/edit.js b/src/blocks/post-favorite/edit.js
new file mode 100644
index 00000000..d555f2fc
--- /dev/null
+++ b/src/blocks/post-favorite/edit.js
@@ -0,0 +1,18 @@
+import '@wordpress/block-editor';
+import { useBlockProps } from '@wordpress/block-editor';
+
+const Edit = () => {
+ return (
+
+

+
+ );
+};
+
+export default Edit;
diff --git a/src/blocks/post-favorite/index.js b/src/blocks/post-favorite/index.js
new file mode 100644
index 00000000..31831a71
--- /dev/null
+++ b/src/blocks/post-favorite/index.js
@@ -0,0 +1,8 @@
+import { registerBlockType } from '@wordpress/blocks';
+import Edit from './edit';
+import './style.scss';
+
+registerBlockType('bhe/post-favorite', {
+ edit: Edit,
+ save: () => null,
+});
diff --git a/src/blocks/post-favorite/render.php b/src/blocks/post-favorite/render.php
new file mode 100644
index 00000000..97f1e788
--- /dev/null
+++ b/src/blocks/post-favorite/render.php
@@ -0,0 +1,15 @@
+
+
+
+
+ wp-on:click="actions.favorites.togglePost"
+ class="emoji"
+ alt=":heart:"
+ src="https://s.w.org/images/core/emoji/14.0.0/svg/1f90d.svg"
+ wp-bind:src="selectors.favorites.isPostIncluded"
+ />
+
\ No newline at end of file
diff --git a/src/blocks/post-favorite/style.scss b/src/blocks/post-favorite/style.scss
new file mode 100644
index 00000000..7e2c2e16
--- /dev/null
+++ b/src/blocks/post-favorite/style.scss
@@ -0,0 +1,4 @@
+.wp-block-bhe-post-favorite {
+ width: 1rem;
+ cursor: pointer;
+}
\ No newline at end of file
diff --git a/src/blocks/tabs/block.json b/src/blocks/tabs/block.json
new file mode 100644
index 00000000..0b3716d4
--- /dev/null
+++ b/src/blocks/tabs/block.json
@@ -0,0 +1,15 @@
+{
+ "$schema": "https://schemas.wp.org/trunk/block.json",
+ "apiVersion": 2,
+ "name": "bhe/tabs",
+ "version": "0.1.0",
+ "title": "BHE - Tabs",
+ "category": "text",
+ "icon": "tab",
+ "description": "Tabs",
+ "textdomain": "bhe",
+ "editorScript": "file:./index.js",
+ "editorStyle": "file:./style.css",
+ "style": "file:./style-index.css",
+ "render": "file:./render.php"
+}
diff --git a/src/blocks/tabs/edit.js b/src/blocks/tabs/edit.js
new file mode 100644
index 00000000..60a9952f
--- /dev/null
+++ b/src/blocks/tabs/edit.js
@@ -0,0 +1,7 @@
+import { useBlockProps } from '@wordpress/block-editor';
+
+const Edit = () => {
+ return I'm a tab
;
+};
+
+export default Edit;
diff --git a/src/blocks/tabs/index.js b/src/blocks/tabs/index.js
new file mode 100644
index 00000000..2bd23955
--- /dev/null
+++ b/src/blocks/tabs/index.js
@@ -0,0 +1,8 @@
+import { registerBlockType } from '@wordpress/blocks';
+import Edit from './edit';
+import './style.scss';
+
+registerBlockType('bhe/tabs', {
+ edit: Edit,
+ save: () => null,
+});
diff --git a/src/blocks/tabs/render.php b/src/blocks/tabs/render.php
new file mode 100644
index 00000000..28a22996
--- /dev/null
+++ b/src/blocks/tabs/render.php
@@ -0,0 +1,11 @@
+ [
+ 'show' => true,
+ 'dontShow' => false,
+ ],
+]); ?>
+
+The tabs!
+I should be shown
+I should not be shown
\ No newline at end of file
diff --git a/src/blocks/tabs/style.scss b/src/blocks/tabs/style.scss
new file mode 100644
index 00000000..e4736146
--- /dev/null
+++ b/src/blocks/tabs/style.scss
@@ -0,0 +1,2 @@
+.wp-block-bhe-tabs {
+}
diff --git a/src/runtime/components.js b/src/runtime/components.js
index 8edcf5f4..a45512cf 100644
--- a/src/runtime/components.js
+++ b/src/runtime/components.js
@@ -4,8 +4,13 @@ import { component } from './hooks';
export default () => {
const WpContext = ({ children, data, context: { Provider } }) => {
- const signals = useMemo(() => deepSignal(JSON.parse(data)), []);
+ const signals = useMemo(() => deepSignal(JSON.parse(data)), [data]);
return {children};
};
component('wp-context', WpContext);
+
+ const WpShow = ({ children }) => {
+ return children;
+ };
+ component('wp-show', WpShow);
};
diff --git a/src/runtime/deepsignal.js b/src/runtime/deepsignal.js
index 84901785..08742367 100644
--- a/src/runtime/deepsignal.js
+++ b/src/runtime/deepsignal.js
@@ -1,25 +1,27 @@
import { signal } from '@preact/signals';
-import { knownSymbols, shouldWrap } from './utils';
const proxyToSignals = new WeakMap();
const objToProxy = new WeakMap();
-const returnSignal = /^\$/;
export const deepSignal = (obj) => new Proxy(obj, handlers);
+export const options = { returnSignal: /^\$/ };
const handlers = {
get(target, prop, receiver) {
- if (typeof prop === 'symbol' && knownSymbols.has(prop))
- return Reflect.get(target, prop, receiver);
- const shouldReturnSignal = returnSignal.test(prop);
- const key = shouldReturnSignal ? prop.replace(returnSignal, '') : prop;
+ const returnSignal = options.returnSignal.test(prop);
+ const key = returnSignal
+ ? prop.replace(options.returnSignal, '')
+ : prop;
if (!proxyToSignals.has(receiver))
proxyToSignals.set(receiver, new Map());
const signals = proxyToSignals.get(receiver);
if (!signals.has(key)) {
let val = Reflect.get(target, key, receiver);
- if (typeof val === 'object' && val !== null && shouldWrap(val))
- val = new Proxy(val, handlers);
+ if (typeof val === 'object' && val !== null) {
+ if (!objToProxy.has(val))
+ objToProxy.set(val, new Proxy(val, handlers));
+ val = objToProxy.get(val);
+ }
signals.set(key, signal(val));
}
return returnSignal ? signals.get(key) : signals.get(key).value;
@@ -27,7 +29,7 @@ const handlers = {
set(target, prop, val, receiver) {
let internal = val;
- if (typeof val === 'object' && val !== null && shouldWrap(val)) {
+ if (typeof val === 'object' && val !== null) {
if (!objToProxy.has(val))
objToProxy.set(val, new Proxy(val, handlers));
internal = objToProxy.get(val);
diff --git a/src/runtime/directives.js b/src/runtime/directives.js
index 5741046d..808b7aa4 100644
--- a/src/runtime/directives.js
+++ b/src/runtime/directives.js
@@ -21,7 +21,10 @@ export default () => {
props: { children },
context: { Provider },
}) => {
- const signals = useMemo(() => deepSignal(context.default), []);
+ const signals = useMemo(
+ () => deepSignal(context.default),
+ [JSON.stringify(context.default)]
+ );
return {children};
}
);
@@ -34,7 +37,12 @@ export default () => {
Object.values(effect).forEach((callback) => {
useSignalEffect(() => {
const cb = getCallback(callback);
- cb({ context, tick, ref: element.ref.current });
+ cb({
+ context,
+ tick,
+ ref: element.ref.current,
+ state: window.wpx.state,
+ });
});
});
}
@@ -46,7 +54,7 @@ export default () => {
Object.entries(on).forEach(([name, callback]) => {
element.props[`on${name}`] = (event) => {
const cb = getCallback(callback);
- cb({ context, event });
+ cb({ context, event, state: window.wpx.state });
};
});
});
@@ -64,7 +72,7 @@ export default () => {
.filter((n) => n !== 'default')
.forEach((name) => {
const cb = getCallback(className[name]);
- const result = cb({ context });
+ const result = cb({ context, state: window.wpx.state });
if (!result) element.props.class.replace(name, '');
else if (!element.props.class.includes(name))
element.props.class += ` ${name}`;
@@ -81,7 +89,10 @@ export default () => {
.filter((n) => n !== 'default')
.forEach(([attribute, callback]) => {
const cb = getCallback(callback);
- element.props[attribute] = cb({ context });
+ element.props[attribute] = cb({
+ context,
+ state: window.wpx.state,
+ });
});
}
);
@@ -109,18 +120,7 @@ export default () => {
event.preventDefault();
// Fetch the page (or return it from cache).
- await navigate(href);
-
- // Update the scroll, depending on the option. True by default.
- if (link?.scroll === 'smooth') {
- window.scrollTo({
- top: 0,
- left: 0,
- behavior: 'smooth',
- });
- } else if (link?.scroll !== false) {
- window.scrollTo(0, 0);
- }
+ await navigate(href, link?.scroll);
};
}
}
diff --git a/src/runtime/hooks.js b/src/runtime/hooks.js
index 46e4012d..617f6009 100644
--- a/src/runtime/hooks.js
+++ b/src/runtime/hooks.js
@@ -38,8 +38,11 @@ options.vnode = (vnode) => {
const wp = vnode.props.wp;
if (typeof type === 'string' && type.startsWith('wp-')) {
- vnode.type = components[type];
- vnode.props.context = context;
+ vnode.props.children = h(
+ components[type],
+ { ...vnode.props, context },
+ vnode.props.children
+ );
}
if (wp) {
diff --git a/src/runtime/router.js b/src/runtime/router.js
index 94c6f659..81ae1bcc 100644
--- a/src/runtime/router.js
+++ b/src/runtime/router.js
@@ -83,10 +83,10 @@ export const navigate = async (href) => {
// cache.
window.addEventListener('popstate', async () => {
const url = cleanUrl(window.location); // Remove hash.
- const page = pages.has(url) && (await pages.get(url));
- if (page) {
- document.head.replaceChildren(...page.head);
- render(page.body, rootFragment);
+ if (pages.has(url)) {
+ const { body, head } = await pages.get(url);
+ document.head.replaceChildren(...head);
+ render(body, rootFragment);
} else {
window.location.reload();
}
diff --git a/src/runtime/wpx.js b/src/runtime/wpx.js
new file mode 100644
index 00000000..6f7c1b47
--- /dev/null
+++ b/src/runtime/wpx.js
@@ -0,0 +1,10 @@
+import { deepSignal } from './deepsignal';
+import { deepMerge } from './utils';
+
+const rawState = {};
+window.wpx = { state: deepSignal(rawState) };
+
+export default ({ state, ...block }) => {
+ deepMerge(window.wpx, block);
+ deepMerge(rawState, state);
+};
diff --git a/ssr.mjs b/ssr.mjs
new file mode 100644
index 00000000..03c4062d
--- /dev/null
+++ b/ssr.mjs
@@ -0,0 +1,31 @@
+import { transform, html } from 'ultrahtml';
+import { readFile, writeFile } from 'fs/promises';
+
+const file = '/blocks/tabs/render.php';
+
+const propsToArray = (props) => {
+ let result = '[';
+ Object.entries(props).forEach(([key, value]) => {
+ result += `["${key}", "${value}"]`;
+ });
+ result += ']';
+ return result;
+};
+
+const start = async () => {
+ const php = await readFile('./src' + file, {
+ encoding: 'utf8',
+ });
+ const output = await transform(php, {
+ components: {
+ 'wp-show': (props, children) =>
+ html`
+ ${children}
+ `,
+ },
+ });
+ await writeFile('./build' + file, output);
+ console.log('done!');
+};
+
+start();
diff --git a/webpack.config.js b/webpack.config.js
index c2070f52..a86ef4fc 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -1,54 +1,61 @@
+const defaultConfig = require('@wordpress/scripts/config/webpack.config');
const { resolve } = require('path');
-module.exports = {
- entry: {
- runtime: './src/runtime',
- },
- output: {
- filename: '[name].js',
- path: resolve(process.cwd(), 'build'),
- },
- optimization: {
- runtimeChunk: {
- name: 'vendors',
+module.exports = [
+ defaultConfig,
+ {
+ ...defaultConfig,
+ entry: {
+ runtime: './src/runtime',
+ 'blocks/favorites-number/view':
+ './src/blocks/favorites-number/view',
+ },
+ output: {
+ filename: '[name].js',
+ path: resolve(process.cwd(), 'build'),
},
- splitChunks: {
- cacheGroups: {
- vendors: {
- test: /[\\/]node_modules[\\/]/,
- name: 'vendors',
- minSize: 0,
- chunks: 'all',
+ optimization: {
+ runtimeChunk: {
+ name: 'vendors',
+ },
+ splitChunks: {
+ cacheGroups: {
+ vendors: {
+ test: /[\\/]node_modules[\\/]/,
+ name: 'vendors',
+ minSize: 0,
+ chunks: 'all',
+ },
},
},
},
- },
- 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',
- },
+ 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/wp-directives.php b/wp-directives.php
index 68bb435a..60fb3e11 100644
--- a/wp-directives.php
+++ b/wp-directives.php
@@ -75,8 +75,14 @@ function wp_directives_add_wp_link_attribute($block_content)
$link = parse_url($w->get_attribute('href'));
if (!isset($link['host']) || $link['host'] === $site_url['host']) {
$classes = $w->get_attribute('class');
- if (str_contains($classes, 'query-pagination') || str_contains($classes, 'page-numbers')) {
- $w->set_attribute('wp-link', '{ "prefetch": true, "scroll": false }');
+ if (
+ str_contains($classes, 'query-pagination') ||
+ str_contains($classes, 'page-numbers')
+ ) {
+ $w->set_attribute(
+ 'wp-link',
+ '{ "prefetch": true, "scroll": false }'
+ );
} else {
$w->set_attribute('wp-link', '{ "prefetch": true }');
}
@@ -85,8 +91,18 @@ function wp_directives_add_wp_link_attribute($block_content)
return (string) $w;
}
// We go only through the Query Loops and the template parts until we find a better solution.
-add_filter('render_block_core/query', 'wp_directives_add_wp_link_attribute', 10, 1);
-add_filter('render_block_core/template-part', 'wp_directives_add_wp_link_attribute', 10, 1);
+add_filter(
+ 'render_block_core/query',
+ 'wp_directives_add_wp_link_attribute',
+ 10,
+ 1
+);
+add_filter(
+ 'render_block_core/template-part',
+ 'wp_directives_add_wp_link_attribute',
+ 10,
+ 1
+);
function wp_directives_client_site_transitions_meta_tag()
{
@@ -106,3 +122,85 @@ function wp_directives_client_site_transitions_option()
'client_side_transitions',
'wp_directives_client_site_transitions_option'
);
+
+/* Blocks */
+add_action('init', function () {
+ register_block_type(__DIR__ . '/build/blocks/post-favorite');
+ register_block_type(__DIR__ . '/build/blocks/favorites-number');
+ register_block_type(__DIR__ . '/build/blocks/tabs');
+});
+
+add_filter('render_block_bhe/favorites-number', function ($content) {
+ wp_enqueue_script(
+ 'bhe/favorites-number',
+ plugin_dir_url(__FILE__) . 'build/blocks/favorites-number/view.js'
+ );
+ return $content;
+});
+
+/**
+ * SSR in PHP
+ */
+
+// wp-show
+function wpx_show_open_tag($prop_entries)
+{
+ global $wpx;
+ $props = wpx_prop_entries_to_array($prop_entries);
+ $attributes = wpx_prop_entries_to_attributes($prop_entries);
+ $value = wpx_get_state($props['when']);
+ if ($value) {
+ echo "";
+ } else {
+ echo "";
+ }
+}
+
+function wpx_show_close_tag($prop_entries)
+{
+ global $wpx;
+ $props = wpx_prop_entries_to_array($prop_entries);
+ $value = wpx_get_state($props['when']);
+ if ($value) {
+ echo '';
+ } else {
+ echo '';
+ }
+}
+
+// Utils
+$GLOBALS['wpx'] = [];
+function wpx($data)
+{
+ global $wpx;
+ $wpx = array_merge_recursive($wpx, $data);
+}
+
+function wpx_get_state($path)
+{
+ global $wpx;
+ $current = $wpx;
+ $array = explode('.', $path);
+ foreach ($array as $p) {
+ $current = $current[$p];
+ }
+ return $current;
+}
+
+function wpx_prop_entries_to_array($prop_entries)
+{
+ $array = [];
+ foreach ($prop_entries as list($key, $value)) {
+ $array[$key] = $value;
+ }
+ return $array;
+}
+
+function wpx_prop_entries_to_attributes($prop_entries)
+{
+ $attributes = '';
+ foreach ($prop_entries as list($key, $value)) {
+ $attributes .= "$key='$value'";
+ }
+ return $attributes;
+}