Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 23 additions & 23 deletions assets/build/example.min.js

Large diffs are not rendered by default.

46 changes: 23 additions & 23 deletions assets/build/index.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion assets/src/components/dynamic/base-wrapper/BaseWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ const BaseWrapper = props => {
*/
const buttonType = props.buttonType ?? 'outside'
const hasInsert = !( props.readOnly || props.inputMasking ) && (buttonType === 'outside' || (! props.remove || props.remove.isDisabled))
const hasClear = !( props.readOnly || props.inputMasking ) && (buttonType === 'outside' || (props.remove && props.remove.isDisabled === false))
const hasClear = !( props.readOnly || props.inputMasking ) && props.remove !== undefined && (buttonType === 'outside' || props.remove.isDisabled === false)

const classes = `tf-dynamic-wrapper tf-dynamic-wrapper-buttons-${buttonType} ${props.className ?? ''}`

Expand Down
1 change: 1 addition & 0 deletions assets/src/components/field/combo-box/async.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ const getAsyncProps = props => {
? mapResults(results, props.mapResults)
: results

debounced.current.status = false
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
debounced.current.status = false

Unfortunately I don't think we can add this line here, it prevent the debounced delay to be applied. Did you encounter some issues with it while testing?

If for a reason or another you have a specific case and want the result request to be sent right away you can pass a debounceTime prop and set it to 0

return {
items: getOptions(
(formatedResults ?? []).reduce((items, item) => ({ ...items, [item.id]: item.title }), {})
Expand Down
18 changes: 17 additions & 1 deletion assets/src/components/field/editor/TinyMce.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import {
Description
} from '../../base'

import { BaseWrapper } from '../../dynamic/'

const TinyMce = props => {

const ref = useRef()
Expand Down Expand Up @@ -48,13 +50,27 @@ const TinyMce = props => {

useEffect(() => props.onChange && props.onChange(value), [value])

const insertDynamicValue = token => {
if( tinyMCE.activeEditor ) {
tinyMCE.activeEditor.insertContent(token)
} else {
setValue(prev => (prev ?? '') + token)
}
}

return (
<div className="tf-editor">
{props.label &&
<Label labelProps={ labelProps } parent={ props }>
{props.label}
</Label>}
<textarea ref={ref} {...inputProps}>{value}</textarea>
<BaseWrapper
config={ props.dynamic ?? false }
onValueSelection={ insertDynamicValue }
buttonType="outside"
>
<textarea ref={ref} {...inputProps}>{value}</textarea>
</BaseWrapper>
{ props.description &&
<Description descriptionProps={ descriptionProps } parent={ props }>
{ props.description }
Expand Down
24 changes: 17 additions & 7 deletions assets/src/components/field/editor/prosemirror/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import {
Description
} from '../../../base'

import { BaseWrapper } from '../../../dynamic/'

const Editor = props => {

const [value, setValue] = useState(props.value)
Expand All @@ -27,19 +29,27 @@ const Editor = props => {
props.onChange && props.onChange(value)
}, [value])

const insertDynamicValue = token => setValue(prev => (prev ?? '') + token)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it looks like there is a small issue currently

If I insert a dynamic value, the state will update but it won't show up in the ProseMirror editor

It might be easier to fix if we move the wrapper inside the child component (as suggested in my other comment)


return (
<div className="tf-editor">
{ props.label &&
<Label labelProps={ labelProps } parent={ props }>
{ props.label }
</Label> }
<input { ...inputProps } type="hidden" name={ props.name } value={ value } />
<ProseMirror
ref={ editorRef }
value={ value }
onChange={ setValue }
rawView={ props.rawView ?? true }
/>
<BaseWrapper
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want to move the BaseWrapper here into the child component (ProseMirror.tsx)

Currently the layout would look like this:

Image

By moving it into ProseMirror.tsx, it could look like this instead, which will keep all our action button in the same place:

Image

<BaseWrapper /> will still work if we use it without any children, so we can do something like this:

+import { BaseWrapper } from '../../../dynamic/'
 
 // ...
 
 const ProseMirror = forwardRef(({
   rawView = true,
+  dynamic = false,
   ...props
 }, ref) => {
 
   // ...
 
   return <div className="tf-editor-content">
-    { rawView && <div className="tf-editor-view-toggle">
-      <ButtonGroup
-        label={ 'Switch view' }
-        labelVisuallyHidden={ true }
-        value={ view }
-        onChange={ view => {
-          setView(view)
-          if ( view === 'raw' ) ref.current = null
-        }}
-        choices={{
-          visual : 'Visual',
-          raw    : 'Raw'
-        }}
-      />
+    { ( rawView || dynamic ) && 
+      <div className="tf-editor-view-toggle">
+        { dynamic &&
+          <BaseWrapper
+            config={ dynamic ?? false }
+            onValueSelection={ dynamic => {
+              setValue( value + dynamic )
+              setMountKey(k => k + 1)
+            } }
+            buttonType="outside"
+          /> }
+        { rawView &&
+          <ButtonGroup
+            label={ 'Switch view' }
+            labelVisuallyHidden={ true }
+            value={ view }
+            onChange={ view => {
+              setView(view)
+              if ( view === 'raw' ) ref.current = null
+            }}
+            choices={{
+              visual : 'Visual',
+              raw    : 'Raw'
+            }}
+          /> }

   // ...
})

export default ProseMirror

Which will also require to pass a dynamic props on <ProseMirror />:

 <ProseMirror
     ref={ editorRef }
     value={ value }
     onChange={ setValue }
     rawView={ props.rawView ?? true }
+    dynamic={ props.dynamic ?? false }
   />

config={ props.dynamic ?? false }
onValueSelection={ insertDynamicValue }
buttonType="outside"
>
<input { ...inputProps } type="hidden" name={ props.name } value={ value } />
<ProseMirror
ref={ editorRef }
value={ value }
onChange={ setValue }
rawView={ props.rawView ?? true }
/>
</BaseWrapper>
{ props.description &&
<Description descriptionProps={ descriptionProps } parent={ props }>
{ props.description }
Expand Down
12 changes: 5 additions & 7 deletions assets/src/components/field/list/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,11 @@ const List = props => {
}

const updateItem = (i, name, value) => {
setTimeout(() => {
setItems([
...items.slice(0, i),
{ ...items[i], [name]: value },
...items.slice(i + 1)
])
})
setItems([
...items.slice(0, i),
{ ...items[i], [name]: value },
...items.slice(i + 1)
])
}

const getItemText = item => (
Expand Down
28 changes: 27 additions & 1 deletion assets/src/dynamic-values/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ const allowedTypes = [
'color-picker',
'conditional-panel',
'date-picker',
'editor', // alias of wysiwyg
'number',
'text'
'text',
'wysiwyg'
]

/**
Expand Down Expand Up @@ -81,6 +83,30 @@ const defaultConfig = {
'color',
'number'
]
},
'editor': {
mode : {
default : 'insert',
supported : [ 'insert' ]
},
types : [
'text',
'date',
'color',
'number'
]
},
'wysiwyg': {
mode : {
default : 'insert',
supported : [ 'insert' ]
},
types : [
'text',
'date',
'color',
'number'
]
}
}

Expand Down
12 changes: 7 additions & 5 deletions example/register/sections.php
Original file line number Diff line number Diff line change
Expand Up @@ -483,11 +483,13 @@
'title' => 'Examples',
'path' => 'dynamic-values/examples',
'fields' => [
'dynamic-text' => [ 'json' => true ],
'dynamic-text-replace' => [ 'json' => true ],
'dynamic-color' => [],
'dynamic-date' => [],
'dynamic-number' => [],
'dynamic-text' => [ 'json' => true ],
'dynamic-text-replace' => [ 'json' => true ],
'dynamic-color' => [],
'dynamic-date' => [],
'dynamic-number' => [],
'dynamic-editor' => [ 'json' => true ],
'dynamic-editor-tinymce' => [ 'json' => true ],
]
]
]
Expand Down
47 changes: 47 additions & 0 deletions example/templates/dynamic-values/examples.php
Original file line number Diff line number Diff line change
Expand Up @@ -117,3 +117,50 @@
<div class="tangible-settings-row">
<?php submit_button() ?>
</div>

<h4>Editor (ProseMirror)</h4>

<div class="tangible-settings-row">
<?= $fields->render_field('dynamic-editor', [
'label' => 'Editor field',
'type' => 'wysiwyg',
'value' => $fields->fetch_value('dynamic-editor'),
'description' => 'Example description',
'dynamic' => true
]) ?>
</div>

<?php tangible\see(
'Raw value: ' . $fields->fetch_value('dynamic-editor'),
'Parsed value: ' . $fields->render_value(
$fields->fetch_value('dynamic-editor')
),
); ?>

<div class="tangible-settings-row">
<?php submit_button() ?>
</div>

<h4>Editor (TinyMCE)</h4>

<div class="tangible-settings-row">
<?= $fields->render_field('dynamic-editor-tinymce', [
'label' => 'Editor field (TinyMCE)',
'type' => 'wysiwyg',
'editor' => 'tinymce',
'value' => $fields->fetch_value('dynamic-editor-tinymce'),
'description' => 'Example description',
'dynamic' => true
]) ?>
</div>

<?php tangible\see(
'Raw value: ' . $fields->fetch_value('dynamic-editor-tinymce'),
'Parsed value: ' . $fields->render_value(
$fields->fetch_value('dynamic-editor-tinymce')
),
); ?>

<div class="tangible-settings-row">
<?php submit_button() ?>
</div>
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"composer": "FOLDER=`basename $(realpath $PWD)`; wp-env run cli --env-cwd=wp-content/plugins/$FOLDER composer",
"composer:install": "wp-env run cli sudo apk add git && npm run composer install",
"composer:update": "npm run composer update",
"test": "FOLDER=`basename $(realpath $PWD)`; wp-env run tests-wordpress /var/www/html/wp-content/plugins/$FOLDER/vendor/bin/phpunit --testdox -c /var/www/html/wp-content/plugins/$FOLDER/phpunit.xml --verbose",
"test": "FOLDER=`basename \"$(realpath \"$PWD\")\"`; wp-env run tests-wordpress /var/www/html/wp-content/plugins/$FOLDER/vendor/bin/phpunit --testdox -c /var/www/html/wp-content/plugins/$FOLDER/phpunit.xml --verbose",
"test:7.4": "WP_ENV_PHP_VERSION=7.4 wp-env start && npm run test",
"test:8.2": "WP_ENV_PHP_VERSION=8.2 wp-env start && npm run test",
"test:all": "npm run test:7.4 && npm run test:8.2 && npm run e2e",
Expand Down
27 changes: 16 additions & 11 deletions tests/jest/cases/dynamic/render.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ describe('dynamic values feature - render', () => {
*/
const testTypes = allowedTypes.filter(type => type !== 'conditional-panel')

/**
* Types that only support insert mode (no replace/clear)
*/
const insertOnlyTypes = [ 'text', 'editor', 'wysiwyg' ]

test.each(testTypes)('%p type do not render dynamic values UI if not specified', type => {

const { container } = render(
Expand All @@ -31,8 +36,8 @@ describe('dynamic values feature - render', () => {
})
)

expect(within(container).queryByText('Insert')).toBeFalsy()
expect(within(container).queryByText('Clear')).toBeFalsy()
expect(within(container).queryByRole('button', { name: 'Insert' })).toBeFalsy()
expect(within(container).queryByRole('button', { name: 'Clear' })).toBeFalsy()
})

test.each(testTypes)('%p type do not render dynamic values UI if dynamic is false', type => {
Expand All @@ -46,8 +51,8 @@ describe('dynamic values feature - render', () => {
})
)

expect(within(container).queryByText('Insert')).toBeFalsy()
expect(within(container).queryByText('Clear')).toBeFalsy()
expect(within(container).queryByRole('button', { name: 'Insert' })).toBeFalsy()
expect(within(container).queryByRole('button', { name: 'Clear' })).toBeFalsy()
})

test.each(testTypes)('%p type does render dynamic values UI if dynamic is true', type => {
Expand All @@ -61,13 +66,13 @@ describe('dynamic values feature - render', () => {
})
)

expect(within(container).getByText('Insert')).toBeTruthy()
expect(within(container).getByRole('button', { name: 'Insert' })).toBeTruthy()

// Special case for text, as it uses insert mode by default instead if replace like other types
if( type === 'text' ) {
expect(within(container).queryByText('Clear')).toBeFalsy()
// Insert-only types (text, editor, wysiwyg) do not show a Clear button
if( insertOnlyTypes.includes(type) ) {
expect(within(container).queryByRole('button', { name: 'Clear' })).toBeFalsy()
}
else expect(within(container).getByText('Clear')).toBeTruthy()
else expect(within(container).getByRole('button', { name: 'Clear' })).toBeTruthy()
})

test.each(testTypes)('%p type open dynamic value combobox when clicking on insert button', async type => {
Expand All @@ -87,15 +92,15 @@ describe('dynamic values feature - render', () => {

expect(document.querySelector('.tf-dynamic-wrapper-popover')).toBeFalsy()

await user.click(within(container).getByText('Insert'))
await user.click(within(container).getByRole('button', { name: 'Insert' }))

expect(document.querySelector('.tf-dynamic-wrapper-popover')).toBeTruthy()

await user.click(within(container).getByText('Test click outside'))

expect(document.querySelector('.tf-dynamic-wrapper-popover')).toBeFalsy()

await user.click(within(container).getByText('Insert'))
await user.click(within(container).getByRole('button', { name: 'Insert' }))

expect(document.querySelector('.tf-dynamic-wrapper-popover')).toBeTruthy()
})
Expand Down
61 changes: 61 additions & 0 deletions tests/phpunit/cases/dynamics.php
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,67 @@ function test_dynamic_value_render_with_context() {
$this->assertEquals($parsed_value, $expected_value, 'parsed was not equal to config');
}

/**
* @depends test_dynamic_value_category_registration
*/
function test_dynamic_value_render_in_html_content() {

$fields = tangible_fields();
$fields->register_dynamic_value([
'name' => 'test-value-html',
'category' => 'test-category',
'callback' => function() {
return 'World';
},
'permission_callback_store' => '__return_true',
'permission_callback_parse' => '__return_true'
]);

// Simulate a wysiwyg field value: token inside HTML
$parsed = $fields->render_value('<p>Hello [[test-value-html]]</p>');
$this->assertEquals('<p>Hello World</p>', $parsed, 'dynamic value was not replaced inside HTML content');

// Multiple tokens in HTML
$parsed = $fields->render_value('<p>[[test-value-html]] [[test-value-html]]</p>');
$this->assertEquals('<p>World World</p>', $parsed, 'multiple dynamic values were not replaced inside HTML content');

// Token inside an attribute value should still be replaced
$parsed = $fields->render_value('<a title="[[test-value-html]]">link</a>');
$this->assertEquals('<a title="World">link</a>', $parsed, 'dynamic value was not replaced inside an HTML attribute');
}

/**
* @depends test_dynamic_value_render_in_html_content
*/
function test_dynamic_value_store_strips_tokens_for_wysiwyg() {

$fields = tangible_fields();

$fields->register_field('wysiwyg-dv-test',
[ 'permission_callback' => '__return_true' ]
+ $fields->_store_callbacks['memory']()
);

// Unauthorized token should be stripped on store
$fields->store_value('wysiwyg-dv-test', '<p>Hello [[unauthorized-dynamic-value]]</p>');
$this->assertEquals(
'<p>Hello </p>',
$fields->fetch_value('wysiwyg-dv-test'),
'unauthorized dynamic value was not stripped from wysiwyg content on store'
);

// Authorized token should be preserved on store
$fields->store_value('wysiwyg-dv-test', '<p>Hello [[test-value-html]]</p>');
$this->assertEquals(
'<p>Hello [[test-value-html]]</p>',
$fields->fetch_value('wysiwyg-dv-test'),
'authorized dynamic value token was incorrectly stripped from wysiwyg content on store'
);

// Cleanup
$fields->store_value('wysiwyg-dv-test', null);
}

/**
* @depends test_dynamic_value_category_registration
*/
Expand Down
Loading
Loading