Wordpress Gutenberg PluginDocumentSettingPanel not working with controls? - reactjs

I want to add a custom meta field to the gutenberg document panel and used this doc. For the custom meta field I used this tutorial.
The problem I have occurs when trying to put them together.
Here is my code so far:
const { __ } = wp.i18n;
const { registerBlockType } = wp.blocks;
const { InspectorControls } = wp.editor;
const { registerPlugin } = wp.plugins
const { PluginDocumentSettingPanel } = wp.editPost
const { PanelBody, PanelRow, TextControl } = wp.components
const PluginDocumentSettingPanelDemo = () => (
<PluginDocumentSettingPanel
name="custom-panel"
title="Custom Panel"
className="custom-panel"
>
<TextControl
value={wp.data.select('core/editor').getEditedPostAttribute('meta')['_myprefix_text_metafield']}
label={ "Text Meta" }
onChange={(value) => wp.data.dispatch('core/editor').editPost({meta: {_myprefix_text_metafield: value}})}
/>
</PluginDocumentSettingPanel>
)
registerPlugin('plugin-document-setting-panel-demo', {
render: PluginDocumentSettingPanelDemo
})
Edit: Thanks to Ivan I solved this side issue :)
My Sidebar looks okay at first:
But when I try to change the inputs value it isn't updated (but the storage in wp.data is). I can't delete it, too. It stays at it's initial value. When I remove the part where I set the initial value it works like it should be but since I need the initial value this isn't an option for me ;)
Here an example log from the console when I add an "x" to the end of the input (as mentioned above the text in the input itself doesn't change)
Does anyone know how to make the input field working properly?

First of all, make sure you have https://wordpress.org/plugins/gutenberg/ plugin installed, because PluginDocumentSettingPanel is not fully implemented in core WP yet. It should be for 5.3 version, as per these tweets.
Second, you don't need the interval function for the wp.plugins. The reason it is undefined at first is that WordPress doesn't know that you need the wp-plugins loaded first. From the WordPress documentation
If you wanted to use the PlainText component from the editor module, first you would specify wp-editor as a dependency when you enqueue your script
The same applies for all other modules (read scripts, like 'wp-plugins').
You have to add the 'wp-plugins' script as a dependency, when registering your js plugin script:
<?php
/*
Plugin Name: Sidebar plugin
*/
function sidebar_plugin_register() {
wp_register_script(
'plugin-sidebar-js',
plugins_url( 'plugin-sidebar.js', __FILE__ ),
array( 'wp-plugins', 'wp-edit-post', 'wp-element' ) // <== the dependencies array is important!
);
}
add_action( 'init', 'sidebar_plugin_register' );
function sidebar_plugin_script_enqueue() {
wp_enqueue_script( 'plugin-sidebar-js' );
}
add_action( 'enqueue_block_editor_assets', 'sidebar_plugin_script_enqueue' );
The PHP above is taken from the official WP documentation.
I would also suggest reading thoroughly this awesome tutorial from Css Tricks. It goes in depth about setting up an ESNext environment with only the #wordpress/scripts package. It goes over the dependencies, adding meta fields and much more :) I hope this helps!
--------------- Initial answer ends here ---------------
Edit: After testing the code from the author, I found out a couple of issues. First of all, there was a missing closing tag for the TextControl. Second, I added Higher order components from the wp-data package, which I then used to "enhance" the TextControl, so that it doesn't manipulate or read data directly, but abstract that logic into it's higher order components. The code looks like so:
const { __ } = wp.i18n;
const { registerPlugin } = wp.plugins;
const { PluginDocumentSettingPanel } = wp.editPost;
const { TextControl } = wp.components;
const { withSelect, withDispatch, dispatch, select } = wp.data;
// All the necessary code is pulled from the wp global variable,
// so you don't have to install anything
// import { withSelect, withDispatch, dispatch, select } from "#wordpress/data";
// !!! You should install all the packages locally,
// so your editor could access the files so you could
// look up the functions and classes directly.
// It will not add to the final bundle if you
// run it through wp-scripts. If not, you can
// still use the wp global variable, like you have done so far.
let TextController = props => (
<TextControl
value={props.text_metafield}
label={__("Text Meta", "textdomain")}
onChange={(value) => props.onMetaFieldChange(value)}
/>
);
TextController = withSelect(
(select) => {
return {
text_metafield: select('core/editor').getEditedPostAttribute('meta')['_myprefix_text_metafield']
}
}
)(TextController);
TextController = withDispatch(
(dispatch) => {
return {
onMetaFieldChange: (value) => {
dispatch('core/editor').editPost({ meta: { _myprefix_text_metafield: value } })
}
}
}
)(TextController);
const PluginDocumentSettingPanelDemo = () => {
// Check if a value has been set
// This is for editing a post, because you don't want to override it everytime
if (!select('core/editor').getEditedPostAttribute('meta')['_myprefix_text_metafield']) {
// Set initial value
dispatch('core/editor').editPost({ meta: { _myprefix_text_metafield: 'Your custom value' } });
}
return (
<PluginDocumentSettingPanel
name="custom-panel"
title="Custom Panel"
className="custom-panel"
>
<TextController />
</PluginDocumentSettingPanel>
)
};
registerPlugin('plugin-document-setting-panel-demo', {
render: PluginDocumentSettingPanelDemo
})
Since the meta field is registered with an underscore as a first symbol in the name, WordPress will not allow you to save it, because it treats it as a private value, so you need to add extra code, when registering the field:
function myprefix_register_meta()
{
register_post_meta('post', '_myprefix_text_metafield', array(
'show_in_rest' => true,
'type' => 'string',
'single' => true,
'sanitize_callback' => 'sanitize_text_field',
'auth_callback' => function () {
return current_user_can('edit_posts');
}
));
}
add_action('init', 'myprefix_register_meta');
Again, all of this is explained in the Css tricks tutorial.

I had the same problem - values were not being updated in the field and in the database - and, after some research, I have found that the reason for this is that you should add 'custom-fields' to the 'supports' array in your call to register_post_type(), like this:
register_post_type(
'my_post_type_slug',
array(
...
'supports' => array( 'title', 'editor', 'custom-fields' ),
...
)
);
You can test that this works by calling wp.data.select( 'core/editor' ).getCurrentPost().meta in your JavaScript console, when the block editor is loaded. If your post type does not add support for 'custom-fields', this call will return undefined; if it does, it will return an empty array (or rather, an array with the already existing meta from the database). This behavior is mentioned in the Gutenberg docs, in a note on registering your post meta:
To make sure the field has been loaded, query the block editor internal data structures, also known as stores. Open your browser’s console, and execute this piece of code:
wp.data.select( 'core/editor' ).getCurrentPost().meta;
Before adding the register_post_meta function to the plugin, this code returns a void array, because WordPress hasn’t been told to load any meta field yet. After registering the field, the same code will return an object containing the registered meta field you registered.

I worked on a similar implementation recently, and worked through a bunch of examples as well. Between the above-mentioned articles, and this great series by one of the Automattic devs, I got a working version of the above example using the newer useSelect and useDispatch custom hooks. It's really quite similar, but utilizes custom hooks from React 16.8 for a slightly more concise dev experience:
(Also, using #wordpress/scripts, so the imports are from the npm packages instead of the wp object directly, but either would work.)
import { __ } from '#wordpress/i18n';
import { useSelect, useDispatch } from '#wordpress/data';
import { PluginDocumentSettingPanel } from '#wordpress/edit-post';
import { TextControl } from '#wordpress/components';
const TextController = (props) => {
const meta = useSelect(
(select) =>
select('core/editor').getEditedPostAttribute('meta')['_myprefix_text_metafield']
);
const { editPost } = useDispatch('core/editor');
return (
<TextControl
label={__("Text Meta", "textdomain")}
value={meta}
onChange={(value) => editPost({ meta: { _myprefix_text_metafield: value } })}
/>
);
};
const PluginDocumentSettingPanelDemo = () => {
return (
<PluginDocumentSettingPanel
name="custom-panel"
title="Custom Panel"
className="custom-panel"
>
<TextController />
</PluginDocumentSettingPanel>
);
};
export default PluginDocumentSettingPanelDemo;
Hopefully that helps someone else searching.

Related

Triggering a lexical.js mentions menu programatically when clicking on a mention

What I need
Let's start with The mentions plugin taken from the docs.
I would like to enhance if with the following functionality:
Whenever I click on an existing MentionNode, the menu gets rendered (like it does when menuRenderFunction gets called), with the full list of options, regardless of queryString matching
Selecting an option from menu replaces said mention with the newly selected one
Is there a way to implement this while leaving LexicalTypeaheadMenuPlugin in control of the menu?
Thank you for your time 🙏🏻
What I've tried
I figured that maybe I could achieve my desired behaviour simply by returning the right QueryMatch from triggerFn. Something like this:
const x: FC = () => {
const nodeAtSelection = useNodeAtSelection() // Returns LexicalNode at selection
return (
<LexicalTypeaheadMenuPlugin<VariableTypeaheadOption>
triggerFn={(text, editor) => {
if ($isMentionsNode(nodeAtSelection)) {
// No idea how to implement `getQueryMatchForMentionsNode`,
// or whether it's even possible
return getQueryMatchForMentionsNode(nodeAtSelection, text, editor)
}
return checkForVariableBeforeCaret(text, editor)
}}
/>
)
}
I played around with it for about half an hour, unfortunately I couldn't really find any documentation for triggerFn or QueryMatch, and haven't really made any progress just by messing around.
I also thought of a potential solution the I think would work, but feels very hacky and I would prefer not to use it. I'll post it as an answer.
So here is my "dirty" solution that should work, but feels very hacky:
I could basically take the function which I provide to menuRenderFn prop and call it manually.
Let's say I render the plugin like this:
const menuRenderer = (
anchorElementRef,
{ selectedIndex, selectOptionAndCleanUp, setHighlightedIndex }
) => { /* ... */}
return (
<LexicalTypeaheadMenuPlugin menuRenderFn={menuRenderer} /* ... other props */ />
)
I could then create a parallel environment for rendering menuRenderer, something like this:
const useParallelMenu = (
menuRenderer: MenuRenderFn<any>,
allOptions: TypeaheadOption[],
queryString: string
) => {
// I could get anchor element:
// 1. either by using document.querySelector("." + anchorClassName)
// 2. or by extracting it from inside `menuRenderFn`:
// menuRenderFn={(...params) => {
// extractedRef.current = params[0].current;
// return menuRenderer(...params)
// }}
const anchorEl = x
const [selectedIndex, setHighlightedIndex] = useState(0)
const nodeAtSelection = useNodeAtSelection() // Returns LexicalNode at selection
const selectOptionAndCleanUp = (option: TypeaheadOption) => {
// Replace nodeAtSelection with new MentionsNode from `option`
}
return () =>
$isMentionsNode(nodeAtSelection) &&
menuRenderer(
anchorEl,
{
selectedIndex,
setHighlightedIndex,
selectOptionAndCleanUp,
options: allOptions
},
queryString
)
}
On paper, this seems like a viable approach to me... but I would really prefer not to have to do this and instead let LexicalTypeaheadMenuPlugin manage the state of my menu, as it is intended to do.

ReactJs: How to replace html and string template with a component?

I want to manage the content of the page from a content editor where I am getting page content from the API.
Check this screenshot.
I used two different react modules for this react-html-parser and react-string-replace but it is still not working.
Here is my code.
let pageData = '';
pageData = ReactHtmlParser(page.content);
// replacing contact us form with a contact us form component
pageData = reactStringReplace(pageData, '{CONTACT_US_FORM}', (match, i) => (
<ContactUsForm />
));
return <div>{pageData}</div>;
react-html-parser -> It is used to parse HTML tags which are in string format into tree of elements.
react-string-replace -> It is used to replace a string into react a component.
Note: If I use react-html-parser or react-string-replace individually then it works fine but it does not work together.
Any suggestion?
Depends on the expected structure of page.content. If it contains HTML you are right in using react-html-parser, which has a replace option.
import parse from 'html-react-parser';
const macro = '{CONTACT_US_FORM}';
const replaceContactUsForm = (domhandlerNode) => {
if (domhandlerNode.type === 'text' && domhandlerNode.data.includes(macro))
return <ContactUsForm />;
};
// ...
const elements = parse(page.content, { replace: replaceContactUsForm });
return <div>{elements}</div>;
Additionally, If the string {CONTACT_US_FORM} is embedded in text you could use react-string-replace to keep the rest of the text intact:
const replaceContactUsForm = (domhandlerNode) => {
if (domhandlerNode.type === 'text' && domhandlerNode.data.includes(macro))
return <>{reactStringReplace(domhandlerNode.data, macro, () => (<ContactUsForm />))}</>;
};
If page.content does not contain HTML you do not need react-html-parser. But judging from your screenshot some markup is probably contained.

GraphQL Directive requires its own query result as a variable. What is a smarter way to do this?

I've been in a feedback loop of trying to figure this out (new to GraphQL + Apollo). I got my directive "working" (the boolean does change the part of the fragment that is called, as intended). The problem is in order to write the expression that returns my directive variable, I need the resulting query that the directive is being called on! I don't know how to break this contradiction and am throwing the towel in in hopes that someone more experienced can point me in a better direction.
Here's the query and the hook it's being called on:
export const getDatasetPage = gql`
query dataset($datasetId: ID!, $editTrue: Boolean!) {
dataset(id: $datasetId) {
id
created
public
following
starred
...DatasetDraft //editTrue omits a piece of this fragment if user !hasEditPermissions
...DatasetPermissions
...DatasetSnapshots
...DatasetIssues
...DatasetMetadata
...DatasetComments
uploader {
id
name
email
}
analytics {
downloads
views
}
onBrainlife
}
}
${DatasetQueryFragments.DRAFT_FRAGMENT}
${DatasetQueryFragments.PERMISSION_FRAGMENT}
${DatasetQueryFragments.DATASET_SNAPSHOTS}
${DatasetQueryFragments.DATASET_ISSUES}
${DatasetQueryFragments.DATASET_METADATA}
${DATASET_COMMENTS}
`
// IDEAL CONDITION TO CHECK WRITE PERMISSIONS (won't work because dataset is required //prop...which is the result of the query)
// const editTrue = (dataset) => {
// const user = getProfile()
// return (user && user.admin) ||
// hasEditPermissions(dataset.permissions, user && user.sub)
// }
export const DatasetQueryHook = ({ datasetId, editTrue }) => {
const {
data: { dataset },
loading,
error,
} = useQuery(getDatasetPage, {
variables: { datasetId, editTrue },
})
if (loading) {
return <Spinner text="Loading Dataset" active />
} else {
if (error) Sentry.captureException(error)
return (
<ErrorBoundary error={error} subject={'error in dataset page'}>
<DatasetQueryContext.Provider
value={{
datasetId,
}}>
<DatasetPage dataset={dataset} />
</DatasetQueryContext.Provider>
</ErrorBoundary>
)
}
}
const DatasetQuery = ({ match }) => (
<ErrorBoundaryAssertionFailureException subject={'error in dataset query'}>
<DatasetQueryHook datasetId={match.params.datasetId} editTrue={editTrue} />
</ErrorBoundaryAssertionFailureException>
)
...
After reading tons of gql docs, I'm just frustrated that after finally managing to implement a directive on a piece of a fragment, I'm met with the challenge that the query itself produces a crucial piece of data that determines what that directive is.
To add some context, I'm basically trying to omit certain files being queried when a user doesn't have write permissions to them. Currently, they're being queried for all users despite not being displayed and thus causing hefty load times.

how to pass jsx as a string inside the return call

Is it possible to do something like:
const data={
star: "<h1>STAR</h1>",
moon: "<h3>moon</h3>"
}
const App = () => {
return(
<div>{data.start}</div>
);
}
what i get is the actual string of <h1>STAR</h1> not just STAR
I don't think you can. You can return an html string and possibly get it to display, but JSX isn't a string, it gets compiled into javscript code that creates those elements. it works when your app is built, I don't think you can use dynamic strings with it at run-time. You could do something like this:
const getData = (which) => {
if (which === 'star') {
return (<h1>STAR</h1>);
}
if (which === 'moon') {
return (<h3>moon</h3>);
}
return null; // nothing will display
}
const App = () => {
return (
<div>{getData('star')}</div>
);
};
Strings can be converted to JSX with third-party libraries such as h2x or react-render-html. It may be unsafe to do this with user input because of possible vulnerabilities and security problems that may exist libraries that parse DOM.
It's impossible to use components this way because component names aren't associated with functions that implement them during conversion.

How to generate the customized key in slatejs?

I'm trying to make WYSIWYG editor that it is possible to annotate about selected text.
Firstly, I used Draft.js. However, it was not suitable for pointing the annotated text using key because entity key of Draft.js was initiated when selections were duplicated.
So, I found the slatejs while I searched other libraries related this stuff.
The slatejs had 'setKeyGenerator' utils. However, I couldn't find any information about 'setKeyGenerator' of slatejs. This util is just setting function like below.
function setKeyGenerator(func) {
generate = func;
}
And I didn't know how to generate key using this function.
Then, Anyone know how to use this function or have any idea for annotation selected text?
If you're trying to generate a key to reference an element (block) by, here's what you can do:
// A key to reference to block by (you should make it more unique than `Math.random()`)
var uniqueKey = Math.random();
// Insert a block with a unique key
var newState = this.state
.transform()
.insertBlock({
type: 'some-block-type',
data: {
uniqueKey: uniqueKey
},
})
.apply();
// Get the block's unique Slate key (used internally)
var blockKey;
var { document } = self.state;
document.nodes.some(function(node) {
if (node.data.get('uniqueKey') == uniqueKey) {
blockKey = node.key;
}
});
// Update data on the block, using it's key to find it.
newState = newState
.transform()
.setNodeByKey(blockKey, {
data: {
// Define any data parameters you want attached to the block.
someNewKey: 'some new value!'
},
})
.apply();
That will allow you to set a unique key on an insert block, and then get the block's actual SlateJs key and update the block with it.
Slate provides a KeyUtils.setGenerator(myKeygenFunction) to pass our own key generator. This gives us the opportunity to create truly unique keys across Editor instances.
In the parent that imports this component, pass a different idFromParentIteration prop for each instance of PlainText component and you should be good.
Like so:
['first-editor', 'second-editor'].map((name, idx) => <PlainText idFromParentIteration={name + idx} />)
And here's a complete example with a custom key generator.
import React from "react";
import Plain from "slate-plain-serializer";
import { KeyUtils } from 'slate';
import { Editor } from "slate-react";
const initialValue = Plain.deserialize(
"This is editable plain text, just like a <textarea>!"
);
class PlainText extends React.Component {
constructor(props) {
super(props);
let key = 0;
const keygen = () => {
key += 1;
return props.idFromParentIteration + key; // custom keys
};
KeyUtils.setGenerator(keygen);
}
render() {
return (
<Editor
placeholder="Enter some plain text..."
defaultValue={initialValue}
/>
);
}
}
export default PlainText;

Resources