fix: prevent asset conflicts between React and Grid.js versions

Add coexistence checks to all enqueue methods to prevent loading
both React and Grid.js assets simultaneously.

Changes:
- ReactAdmin.php: Only enqueue React assets when ?react=1
- Init.php: Skip Grid.js when React active on admin pages
- Form.php, Coupon.php, Access.php: Restore classic assets when ?react=0
- Customer.php, Product.php, License.php: Add coexistence checks

Now the toggle between Classic and React versions works correctly.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
dwindown
2026-04-18 17:02:14 +07:00
parent bd9cdac02e
commit e8fbfb14c1
74973 changed files with 6658406 additions and 71 deletions

View File

@@ -0,0 +1,240 @@
import { createElement } from "react";
/**
* WordPress dependencies
*/
import { renderToString, useRef, useState, useEffect } from '@wordpress/element';
import { useFocusableIframe, useMergeRefs } from '@wordpress/compose';
/**
* Internal dependencies
*/
const observeAndResizeJS = function () {
const {
MutationObserver
} = window;
if (!MutationObserver || !document.body || !window.parent) {
return;
}
function sendResize() {
const clientBoundingRect = document.body.getBoundingClientRect();
window.parent.postMessage({
action: 'resize',
width: clientBoundingRect.width,
height: clientBoundingRect.height
}, '*');
}
const observer = new MutationObserver(sendResize);
observer.observe(document.body, {
attributes: true,
attributeOldValue: false,
characterData: true,
characterDataOldValue: false,
childList: true,
subtree: true
});
window.addEventListener('load', sendResize, true);
// Hack: Remove viewport unit styles, as these are relative
// the iframe root and interfere with our mechanism for
// determining the unconstrained page bounds.
function removeViewportStyles(ruleOrNode) {
if (ruleOrNode.style) {
['width', 'height', 'minHeight', 'maxHeight'].forEach(function (style) {
if (/^\\d+(vw|vh|svw|lvw|dvw|svh|lvh|dvh|vi|svi|lvi|dvi|vb|svb|lvb|dvb|vmin|svmin|lvmin|dvmin|vmax|svmax|lvmax|dvmax)$/.test(ruleOrNode.style[style])) {
ruleOrNode.style[style] = '';
}
});
}
}
Array.prototype.forEach.call(document.querySelectorAll('[style]'), removeViewportStyles);
Array.prototype.forEach.call(document.styleSheets, function (stylesheet) {
Array.prototype.forEach.call(stylesheet.cssRules || stylesheet.rules, removeViewportStyles);
});
document.body.style.position = 'absolute';
document.body.style.width = '100%';
document.body.setAttribute('data-resizable-iframe-connected', '');
sendResize();
// Resize events can change the width of elements with 100% width, but we don't
// get an DOM mutations for that, so do the resize when the window is resized, too.
window.addEventListener('resize', sendResize, true);
};
// TODO: These styles shouldn't be coupled with WordPress.
const style = `
body {
margin: 0;
}
html,
body,
body > div {
width: 100%;
}
html.wp-has-aspect-ratio,
body.wp-has-aspect-ratio,
body.wp-has-aspect-ratio > div,
body.wp-has-aspect-ratio > div iframe {
width: 100%;
height: 100%;
overflow: hidden; /* If it has an aspect ratio, it shouldn't scroll. */
}
body > div > * {
margin-top: 0 !important; /* Has to have !important to override inline styles. */
margin-bottom: 0 !important;
}
`;
/**
* This component provides an isolated environment for arbitrary HTML via iframes.
*
* ```jsx
* import { SandBox } from '@wordpress/components';
*
* const MySandBox = () => (
* <SandBox html="<p>Content</p>" title="SandBox" type="embed" />
* );
* ```
*/
function SandBox({
html = '',
title = '',
type,
styles = [],
scripts = [],
onFocus,
tabIndex
}) {
const ref = useRef();
const [width, setWidth] = useState(0);
const [height, setHeight] = useState(0);
function isFrameAccessible() {
try {
return !!ref.current?.contentDocument?.body;
} catch (e) {
return false;
}
}
function trySandBox(forceRerender = false) {
if (!isFrameAccessible()) {
return;
}
const {
contentDocument,
ownerDocument
} = ref.current;
if (!forceRerender && null !== contentDocument?.body.getAttribute('data-resizable-iframe-connected')) {
return;
}
// Put the html snippet into a html document, and then write it to the iframe's document
// we can use this in the future to inject custom styles or scripts.
// Scripts go into the body rather than the head, to support embedded content such as Instagram
// that expect the scripts to be part of the body.
const htmlDoc = createElement("html", {
lang: ownerDocument.documentElement.lang,
className: type
}, createElement("head", null, createElement("title", null, title), createElement("style", {
dangerouslySetInnerHTML: {
__html: style
}
}), styles.map((rules, i) => createElement("style", {
key: i,
dangerouslySetInnerHTML: {
__html: rules
}
}))), createElement("body", {
"data-resizable-iframe-connected": "data-resizable-iframe-connected",
className: type
}, createElement("div", {
dangerouslySetInnerHTML: {
__html: html
}
}), createElement("script", {
type: "text/javascript",
dangerouslySetInnerHTML: {
__html: `(${observeAndResizeJS.toString()})();`
}
}), scripts.map(src => createElement("script", {
key: src,
src: src
}))));
// Writing the document like this makes it act in the same way as if it was
// loaded over the network, so DOM creation and mutation, script execution, etc.
// all work as expected.
contentDocument.open();
contentDocument.write('<!DOCTYPE html>' + renderToString(htmlDoc));
contentDocument.close();
}
useEffect(() => {
trySandBox();
function tryNoForceSandBox() {
trySandBox(false);
}
function checkMessageForResize(event) {
const iframe = ref.current;
// Verify that the mounted element is the source of the message.
if (!iframe || iframe.contentWindow !== event.source) {
return;
}
// Attempt to parse the message data as JSON if passed as string.
let data = event.data || {};
if ('string' === typeof data) {
try {
data = JSON.parse(data);
} catch (e) {}
}
// Update the state only if the message is formatted as we expect,
// i.e. as an object with a 'resize' action.
if ('resize' !== data.action) {
return;
}
setWidth(data.width);
setHeight(data.height);
}
const iframe = ref.current;
const defaultView = iframe?.ownerDocument?.defaultView;
// This used to be registered using <iframe onLoad={} />, but it made the iframe blank
// after reordering the containing block. See these two issues for more details:
// https://github.com/WordPress/gutenberg/issues/6146
// https://github.com/facebook/react/issues/18752
iframe?.addEventListener('load', tryNoForceSandBox, false);
defaultView?.addEventListener('message', checkMessageForResize);
return () => {
iframe?.removeEventListener('load', tryNoForceSandBox, false);
defaultView?.removeEventListener('message', checkMessageForResize);
};
// Ignore reason: passing `exhaustive-deps` will likely involve a more detailed refactor.
// See https://github.com/WordPress/gutenberg/pull/44378
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
trySandBox();
// Ignore reason: passing `exhaustive-deps` will likely involve a more detailed refactor.
// See https://github.com/WordPress/gutenberg/pull/44378
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [title, styles, scripts]);
useEffect(() => {
trySandBox(true);
// Ignore reason: passing `exhaustive-deps` will likely involve a more detailed refactor.
// See https://github.com/WordPress/gutenberg/pull/44378
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [html, type]);
return createElement("iframe", {
ref: useMergeRefs([ref, useFocusableIframe()]),
title: title,
tabIndex: tabIndex,
className: "components-sandbox",
sandbox: "allow-scripts allow-same-origin allow-presentation",
onFocus: onFocus,
width: Math.ceil(width),
height: Math.ceil(height)
});
}
export default SandBox;
//# sourceMappingURL=index.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,344 @@
import { createElement } from "react";
/**
* External dependencies
*/
import { Dimensions, StyleSheet } from 'react-native';
import { WebView } from 'react-native-webview';
/**
* WordPress dependencies
*/
import { Platform, renderToString, memo, useRef, useState, useEffect, forwardRef, useCallback } from '@wordpress/element';
import { usePreferredColorScheme } from '@wordpress/compose';
/**
* Internal dependencies
*/
import sandboxStyles from './style.scss';
const observeAndResizeJS = `
(function() {
const { MutationObserver } = window;
if ( ! MutationObserver || ! document.body || ! window.parent ) {
return;
}
function sendResize() {
const clientBoundingRect = document.body.getBoundingClientRect();
// The function postMessage is exposed by the react-native-webview library
// to communicate between React Native and the WebView, in this case,
// we use it for notifying resize changes.
window.ReactNativeWebView.postMessage(
JSON.stringify( {
action: 'resize',
width: clientBoundingRect.width,
height: clientBoundingRect.height,
} )
);
}
const observer = new MutationObserver( sendResize );
observer.observe( document.body, {
attributes: true,
attributeOldValue: false,
characterData: true,
characterDataOldValue: false,
childList: true,
subtree: true,
} );
window.addEventListener( 'load', sendResize, true );
// Hack: Remove viewport unit styles, as these are relative
// the iframe root and interfere with our mechanism for
// determining the unconstrained page bounds.
function removeViewportStyles( ruleOrNode ) {
if ( ruleOrNode.style ) {
[ 'width', 'height', 'minHeight', 'maxHeight' ].forEach( function (
style
) {
if (
/^\\d+(vw|vh|svw|lvw|dvw|svh|lvh|dvh|vi|svi|lvi|dvi|vb|svb|lvb|dvb|vmin|svmin|lvmin|dvmin|vmax|svmax|lvmax|dvmax)$/.test( ruleOrNode.style[ style ] )
) {
ruleOrNode.style[ style ] = '';
}
} );
}
}
Array.prototype.forEach.call(
document.querySelectorAll( '[style]' ),
removeViewportStyles
);
Array.prototype.forEach.call(
document.styleSheets,
function ( stylesheet ) {
Array.prototype.forEach.call(
stylesheet.cssRules || stylesheet.rules,
removeViewportStyles
);
}
);
document.body.style.position = 'absolute';
document.body.style.width = '100%';
document.body.setAttribute( 'data-resizable-iframe-connected', '' );
sendResize();
// Resize events can change the width of elements with 100% width, but we don't
// get an DOM mutations for that, so do the resize when the window is resized, too.
window.addEventListener( 'resize', sendResize, true );
window.addEventListener( 'orientationchange', sendResize, true );
})();
`;
const style = `
body {
margin: 0;
}
html,
body,
body > div,
body > div iframe {
width: 100%;
}
body > div > * {
margin-top: 0 !important; /* Has to have !important to override inline styles. */
margin-bottom: 0 !important;
}
.wp-block-embed__wrapper {
position: relative;
}
body.wp-has-aspect-ratio > div iframe {
height: 100%;
overflow: hidden; /* If it has an aspect ratio, it shouldn't scroll. */
}
/**
* Add responsiveness to embeds with aspect ratios.
*
* These styles have been copied from the web version (https://github.com/WordPress/gutenberg/blob/7901895ca20cf61e402925e31571d659dab64721/packages/block-library/src/embed/style.scss#L42-L89) and
* adapted for the native version.
*/
.wp-has-aspect-ratio.wp-block-embed__wrapper::before {
content: "";
display: block;
padding-top: 50%; // Default to 2:1 aspect ratio.
}
.wp-has-aspect-ratio iframe {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
height: 100%;
width: 100%;
}
.wp-embed-aspect-21-9.wp-block-embed__wrapper::before {
padding-top: 42.85%; // 9 / 21 * 100
}
.wp-embed-aspect-18-9.wp-block-embed__wrapper::before {
padding-top: 50%; // 9 / 18 * 100
}
.wp-embed-aspect-16-9.wp-block-embed__wrapper::before {
padding-top: 56.25%; // 9 / 16 * 100
}
.wp-embed-aspect-4-3.wp-block-embed__wrapper::before {
padding-top: 75%; // 3 / 4 * 100
}
.wp-embed-aspect-1-1.wp-block-embed__wrapper::before {
padding-top: 100%; // 1 / 1 * 100
}
.wp-embed-aspect-9-16.wp-block-embed__wrapper::before {
padding-top: 177.77%; // 16 / 9 * 100
}
.wp-embed-aspect-1-2.wp-block-embed__wrapper::before {
padding-top: 200%; // 2 / 1 * 100
}
`;
const EMPTY_ARRAY = [];
const Sandbox = forwardRef(function Sandbox({
containerStyle,
customJS,
html = '',
lang = 'en',
providerUrl = '',
scripts = EMPTY_ARRAY,
styles = EMPTY_ARRAY,
title = '',
type,
url,
onWindowEvents = {},
viewportProps = '',
onLoadEnd = () => {},
testID
}, ref) {
const colorScheme = usePreferredColorScheme();
const [height, setHeight] = useState(0);
const [contentHtml, setContentHtml] = useState(getHtmlDoc());
const windowSize = Dimensions.get('window');
const [isLandscape, setIsLandscape] = useState(windowSize.width >= windowSize.height);
const wasLandscape = useRef(isLandscape);
// On Android, we need to recreate the WebView on any of the following actions, otherwise it disappears:
// - Device rotation
// - Light/dark mode changes
// For this purpose, the key prop used in the WebView will be updated with the value of the actions.
const key = Platform.select({
android: `${url}-${isLandscape ? 'landscape' : 'portrait'}-${colorScheme}`,
ios: url
});
function getHtmlDoc() {
// Put the html snippet into a html document, and update the state to refresh the WebView,
// we can use this in the future to inject custom styles or scripts.
// Scripts go into the body rather than the head, to support embedded content such as Instagram
// that expect the scripts to be part of the body.
// Avoid comma issues with props.viewportProps.
const addViewportProps = viewportProps.trim().replace(/(^[^,])/, ', $1');
const htmlDoc = createElement("html", {
lang: lang
}, createElement("head", null, createElement("title", null, title), createElement("meta", {
name: "viewport",
content: `width=device-width, initial-scale=1${addViewportProps}`
}), createElement("style", {
dangerouslySetInnerHTML: {
__html: style
}
}), styles.map((rules, i) => createElement("style", {
key: i,
dangerouslySetInnerHTML: {
__html: rules
}
}))), createElement("body", {
"data-resizable-iframe-connected": "data-resizable-iframe-connected",
className: type
}, createElement("div", {
dangerouslySetInnerHTML: {
__html: html
}
}), scripts.map(src => createElement("script", {
key: src,
src: src
}))));
return '<!DOCTYPE html>' + renderToString(htmlDoc);
}
const getInjectedJavaScript = useCallback(() => {
// Allow parent to override the resize observers with prop.customJS (legacy support)
let injectedJS = customJS || observeAndResizeJS;
// Add any event listeners that were passed in.
Object.keys(onWindowEvents).forEach(eventType => {
injectedJS += `
window.addEventListener( '${eventType}', function( event ) {
window.ReactNativeWebView.postMessage( JSON.stringify( { type: '${eventType}', ...event.data } ) );
});`;
});
return injectedJS;
}, [customJS, onWindowEvents]);
function updateContentHtml(forceRerender = false) {
const newContentHtml = getHtmlDoc();
if (forceRerender && contentHtml === newContentHtml) {
// The re-render is forced by updating the state with empty HTML,
// waiting for the JS code to be executed with "setImmediate" and then
// setting the content HTML again.
setContentHtml('');
setImmediate(() => setContentHtml(newContentHtml));
} else {
setContentHtml(newContentHtml);
}
}
function getSizeStyle() {
const contentHeight = Math.ceil(height);
return contentHeight ? {
height: contentHeight
} : {
aspectRatio: 1
};
}
function onChangeDimensions(dimensions) {
setIsLandscape(dimensions.window.width >= dimensions.window.height);
}
const onMessage = useCallback(message => {
let data = message?.nativeEvent?.data;
try {
data = JSON.parse(data);
} catch (e) {
return;
}
// check for resize event
if ('resize' === data?.action) {
setHeight(data.height);
}
// Forward the event to parent event listeners
Object.keys(onWindowEvents).forEach(eventType => {
if (data?.type === eventType) {
try {
onWindowEvents[eventType](data);
} catch (e) {
// eslint-disable-next-line no-console
console.warn(`Error handling event ${eventType}`, e);
}
}
});
}, [onWindowEvents]);
useEffect(() => {
const dimensionsChangeSubscription = Dimensions.addEventListener('change', onChangeDimensions);
return () => {
dimensionsChangeSubscription.remove();
};
}, []);
useEffect(() => {
updateContentHtml();
// Disable reason: deferring this refactor to the native team.
// see https://github.com/WordPress/gutenberg/pull/41166
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [html, title, type, styles, scripts]);
useEffect(() => {
// When device orientation changes we have to recalculate the size,
// for this purpose we reset the current size value.
if (wasLandscape.current !== isLandscape) {
setHeight(0);
}
wasLandscape.current = isLandscape;
}, [isLandscape]);
return createElement(WebView, {
containerStyle: [sandboxStyles['sandbox-webview__container'], containerStyle],
injectedJavaScript: getInjectedJavaScript(),
key: key,
ref: ref,
source: {
baseUrl: providerUrl,
html: contentHtml
}
// Wildcard value is required for static HTML
// Reference: https://github.com/react-native-webview/react-native-webview/blob/master/docs/Reference.md#source
,
originWhitelist: ['*'],
style: [sandboxStyles['sandbox-webview__content'], getSizeStyle(), Platform.isAndroid && workaroundStyles.webView],
onMessage: onMessage,
scrollEnabled: false,
setBuiltInZoomControls: false,
showsHorizontalScrollIndicator: false,
showsVerticalScrollIndicator: false,
mediaPlaybackRequiresUserAction: false,
onLoadEnd: onLoadEnd,
testID: testID
});
});
const workaroundStyles = StyleSheet.create({
webView: {
/**
* The slight opacity below is a workaround for an Android crash caused from combining Android
* 12's new scroll overflow behavior and webviews.
* https://github.com/react-native-webview/react-native-webview/issues/1915#issuecomment-808869253
*/
opacity: 0.99
}
});
export default memo(Sandbox);
//# sourceMappingURL=index.native.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,2 @@
export {};
//# sourceMappingURL=types.js.map

View File

@@ -0,0 +1 @@
{"version":3,"names":[],"sources":["@wordpress/components/src/sandbox/types.ts"],"sourcesContent":["export type SandBoxProps = {\n\t/**\n\t * The HTML to render in the body of the iframe document.\n\t *\n\t * @default ''\n\t */\n\thtml?: string;\n\t/**\n\t * The `<title>` of the iframe document.\n\t *\n\t * @default ''\n\t */\n\ttitle?: string;\n\t/**\n\t * The CSS class name to apply to the `<html>` and `<body>` elements of the iframe.\n\t */\n\ttype?: string;\n\t/**\n\t * An array of CSS strings to inject into the `<head>` of the iframe document.\n\t *\n\t * @default []\n\t */\n\tstyles?: string[];\n\t/**\n\t * An array of script URLs to inject as `<script>` tags into the bottom of the `<body>` of the iframe document.\n\t *\n\t * @default []\n\t */\n\tscripts?: string[];\n\t/**\n\t * The `onFocus` callback for the iframe.\n\t */\n\tonFocus?: React.DOMAttributes< HTMLIFrameElement >[ 'onFocus' ];\n\t/**\n\t * The `tabindex` the iframe should receive.\n\t *\n\t * @default 0\n\t */\n\ttabIndex?: HTMLElement[ 'tabIndex' ];\n};\n"],"mappings":""}