Skip to content

Commit e39076b

Browse files
committed
Add support for JavaScript in Modal
1 parent dc89bc1 commit e39076b

File tree

8 files changed

+232
-61
lines changed

8 files changed

+232
-61
lines changed

assets/css/dataview.css

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

assets/js/dataview.js

+125-34
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

frontend/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"@types/react-dom": "^18.3.0",
1818
"@vitejs/plugin-react": "^4.3.0",
1919
"@wordpress/data": "^10.2.0",
20-
"@wordpress/dataviews": "^4.3.0",
20+
"@wordpress/dataviews": "^4.6.0",
2121
"eslint": "^8.57.0",
2222
"eslint-plugin-react": "^7.34.2",
2323
"eslint-plugin-react-hooks": "^4.6.2",

frontend/src/Components/Modal.jsx

+23-5
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@
66
*
77
* @since $ver$
88
*/
9-
import { get, replace_tags, datakit_fetch } from '@src/helpers';
10-
import { useState } from 'react';
9+
import { get, replace_tags, datakit_fetch, extract_javascript_fn, strip_javascript } from '@src/helpers';
10+
import { useEffect, useState } from 'react';
1111

1212
export default function Modal( { items, closeModal, context } ) {
1313
const [ body, setBody ] = useState( null );
1414
const [ busy, setBusy ] = useState( false );
15-
15+
const [ script_fn, setScriptFn ] = useState( null );
1616
// Close modal on any element that has `data-close-modal` as a data-attribute.
1717
const handleClick = ( e ) => e.target.matches( '[data-close-modal]' ) && closeModal();
1818

@@ -22,6 +22,7 @@ export default function Modal( { items, closeModal, context } ) {
2222
}
2323

2424
const data = items[ 0 ];
25+
const is_scripts_allowed = get( context, 'is_scripts_allowed', false );
2526
let url = get( context, 'url', null );
2627

2728
if ( url === null ) {
@@ -41,11 +42,28 @@ export default function Modal( { items, closeModal, context } ) {
4142

4243
return response.json();
4344
} )
44-
.then( ( { html } ) => setBody( html ) )
45-
.catch( e => console.error( e ) )
45+
.then( ( { html } ) => {
46+
if ( is_scripts_allowed ) {
47+
setScriptFn( () => extract_javascript_fn( html ) );
48+
} else {
49+
html = strip_javascript( html );
50+
}
51+
setBody( html );
52+
} )
53+
.catch( e => {
54+
console.error( e );
55+
setBody( 'Something went wrong.' );
56+
} )
4657
.finally( () => setBusy( false ) )
4758
}
4859

60+
// Execute JavaScript.
61+
useEffect( () => {
62+
if ( 'function' === typeof script_fn && is_scripts_allowed ) {
63+
script_fn();
64+
}
65+
}, [ body ] );
66+
4967
if ( busy ) {
5068
return <div className='loading'>Loading...</div>;
5169
}

frontend/src/Fields/Html.jsx

+5-17
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,24 @@
11
import { useEffect } from 'react';
2-
import { get } from '@src/helpers.js';
2+
import { get, extract_javascript_fn, strip_javascript } from '@src/helpers.js';
33

44
/**
55
* JavaScript side of the HTML field.
66
*
77
* @since $ver$
88
*/
9-
10-
const script_tag_regex = /(?:\r\n)*<script[^>]*>(.*?)<\/[\r\n\s]*script>(?:\r\n)*/isg;
11-
const script_inline_regex = /\s(on\w+\s*=\s*".*?"|href\s*=\s*"\s*javascript:.*?")/isg;
12-
139
export default function Html( { name, item, context } ) {
1410
const is_script_allowed = get( context, 'is_scripts_allowed', false );
1511
let content = item[ name ] || '';
16-
let script_body = '';
12+
let script_func = null;
1713

1814
if ( is_script_allowed ) {
19-
// Record all scripts from the tags
20-
const scripts = content.match( script_tag_regex );
21-
if ( scripts ) {
22-
for ( const script of scripts ) {
23-
script_body += script.replace( script_tag_regex, '$1' );
24-
}
25-
}
15+
script_func = extract_javascript_fn( content );
2616
} else {
27-
// Remove script tags from the html.
28-
content = content.replace( script_tag_regex, '' ).replace( script_inline_regex, '' );
17+
content = strip_javascript( content );
2918
}
3019

3120
useEffect( () => {
32-
if ( is_script_allowed && script_body ) {
33-
const script_func = new Function( script_body );
21+
if ( is_script_allowed && typeof script_func === 'function' ) {
3422
script_func();
3523
}
3624
}, [ content ] );

frontend/src/helpers.js

+40
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,43 @@ export function datakit_fetch( url, options ) {
6060

6161
return fetch( url, merged_options );
6262
}
63+
64+
const script_tag_regex = /(?:\r\n)*<script[^>]*>(.*?)<\/[\r\n\s]*script>(?:\r\n)*/isg;
65+
const script_inline_regex = /\s(on\w+\s*=\s*".*?"|href\s*=\s*"\s*javascript:.*?")/isg;
66+
67+
/**
68+
* Returns a custom function containing the javascript from the content.
69+
*
70+
* @since $ver$
71+
*
72+
* @param {String} content The HTML content.
73+
*
74+
* @return {Function|null} The function or null.
75+
*/
76+
export function extract_javascript_fn( content ) {
77+
let script_body = '';
78+
const scripts = content.match( script_tag_regex );
79+
if ( scripts ) {
80+
for ( const script of scripts ) {
81+
script_body += script.replace( script_tag_regex, '$1' );
82+
}
83+
}
84+
85+
if ( !script_body ) {
86+
return null;
87+
}
88+
return new Function( script_body );
89+
}
90+
91+
/**
92+
* Returns the content stripped of JavaScripts.
93+
*
94+
* @since $ver$
95+
*
96+
* @param {String} content The HTML content.
97+
*
98+
* @return {String} The cleaned content.
99+
*/
100+
export function strip_javascript( content ) {
101+
return content.replace( script_tag_regex, '' ).replace( script_inline_regex, '' );
102+
}

src/DataView/Action.php

+26
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,32 @@ public function bulk(): self {
284284
return $action;
285285
}
286286

287+
/**
288+
* Returns an instance that allows scripts to be executed.
289+
*
290+
* @since $ver$
291+
* @return self The action.
292+
*/
293+
public function allow_scripts(): self {
294+
$clone = clone $this;
295+
$clone->context['is_scripts_allowed'] = true;
296+
297+
return $clone;
298+
}
299+
300+
/**
301+
* Returns an instance that removes scripts from the content.
302+
*
303+
* @since $ver$
304+
* @return self The action.
305+
*/
306+
public function deny_scripts(): self {
307+
$clone = clone $this;
308+
$clone->context['is_scripts_allowed'] = false;
309+
310+
return $clone;
311+
}
312+
287313
/**
288314
* Returns an action that can be performed one item at a time.
289315
*

src/DataView/DataView.php

+11-3
Original file line numberDiff line numberDiff line change
@@ -585,12 +585,13 @@ public function to_js( bool $is_pretty = false ): string {
585585
*
586586
* @since $ver$
587587
*
588-
* @param array $fields The fields to show.
589-
* @param string $label The label to call the action.
588+
* @param array $fields The fields to show.
589+
* @param string $label The label to call the action.
590+
* @param callable|null $callback Callback that receives the action as the single argument to perform changes on.
590591
*
591592
* @return self The DataView with the view action.
592593
*/
593-
public function viewable( array $fields, string $label = 'View' ): self {
594+
public function viewable( array $fields, string $label = 'View', ?callable $callback = null ): self {
594595
$this->add_view_fields( ...$fields );
595596

596597
$actions = $this->actions ? iterator_to_array( $this->actions ) : [];
@@ -599,6 +600,13 @@ public function viewable( array $fields, string $label = 'View' ): self {
599600
$view_action = Action::modal( 'view', $label, $view_rest_url, true )
600601
->primary( 'info' );
601602

603+
if ( $callback ) {
604+
$view_action = $callback( $view_action );
605+
if ( ! $view_action instanceof Action ) {
606+
throw new InvalidArgumentException( 'The provided callback should return an Action object.' );
607+
}
608+
}
609+
602610
$actions[] = $view_action;
603611

604612
$this->actions = Actions::of( ...$actions );

0 commit comments

Comments
 (0)