Wordpress Gutenberg Anchor Support for Dynamic Block - wordpress

I want to have anchor support for my dynamic wordpress block. I did
//in registerBlockType
supports: {
anchor: true,
},
This adds the HTML Anchor control under the sidebar panel.
My block is a dynamic block that has
save: ( props ) => {
return <InnerBlocks.Content />;
}
I tried everything to get the anchor attribute to to frontend. According to this github issue I should add
anchor: {
type: 'string',
source: 'attribute',
attribute: 'id',
selector: '*',
},
to the blocks attributes. This will make the anchor available in the save function via props.anchor, however it never appears in my render_callback $attributes.
This is basically a port of the github issue to SO. Hope anyone can help here.

if anyone is still interested this worked for me:
so this is my custom block registering, this statement will enable standard wordpress HTML anchor field (with valuable validation for spaces etc.) under Advanced tab of selected gutenberg block:
supports: {
anchor: true
}
then in the same place we define:
attributes: {
anchor: {
type: 'string'
}
}
then in save function (I have it exactly for the same purpose of InnerBlocks):
save: function(props) {
const { anchor } = props.attributes;
return (
el( anchor, {}),
el( InnerBlocks.Content, {})
);
}
if you are using jsx, the save function could look like this:
save: function(props) {
const { anchor } = props.attributes;
return (
<div id={anchor}>
<InnerBlocks.Content />
</div>
);
}
then in your render callback function (in php) it's going to be available via first arg's (which is array) element
function your_callback( $block, $content ) {
// display your anchor value
echo $block['anchor'];
}

You could use this filter (targeting whatever blocks you want)
const withAnchor = props => {
if (props.attributes) { // Some blocks don't have attributes
props.attributes = {
...props.attributes,
anchor: {
type: 'string'
}
}
}
return props
}
wp.hooks.addFilter(
'blocks.registerBlockType',
'namespace/with-anchor',
withAnchor
)
And then you can access the 'anchor' attribute in the render callback
'render_callback' => function($attributes) {
echo $attributes['anchor'];
}

Have you tried manually adding a field that will take care of the ID attribute?
Something like this:
<InspectorControls>
<PanelBody title={ __( 'Element Settings' ) }>
<TextControl
label={ __( 'Element ID', 'fleximpleblocks' ) }
value={ elementID}
placeholder={ __( 'Type in the element ID…' ) }
onChange={ ( value ) => setAttributes( { elementID: value } ) }
/>
</PanelBody>
</InspectorControls>
And then:
save: ( props ) => {
return <InnerBlocks.Content id={ props.attributes.elementID } />;
}
I'm not sure if it'll work, I'm just taking a wild guess here. Let me know how it goes :)

Based on answers above.
You just need create an attribute which collects all other attributes/variables/whatever into string.
Step 1
Create an attribute with string type(in block.json)
"phpRender": {
"type": "string"
}
Step 2
In the "edit" function of the block, create a function to save whatever you need to the attribute above. Put this function in "useEffect" hook.
const saveAllToString = () => {
const blockProps = {
id: attributes.anchor,
}
setAttributes({phpRender: JSON.stringify(blockProps)});
}
useEffect(() => {
saveAllToString();
});
Step 3
Now you can decode this string and use variables easily.
$blockProps = !empty($attributes['phpRender']) ? json_decode($attributes['phpRender']) : false;
echo $blockProps->id;

Related

How to change the Gutenberg Group Block's "Inner blocks use content width" Toggle to Off/False as Default

When you wrap a new set of blocks in the core/group block the "Inner blocks use content width" toggle switch defaults to true. There is an object attribute showing called layout for that block. I'm assuming that I can update the settings on the layout attribute like I can with the align attribute.
Here is how I'm updating the align attribute:
const { addFilter } = wp.hooks;
const { assign, merge } = lodash;
function filterCoverBlockAlignments(settings, name) {
if (name === 'core/group') {
return assign({}, settings, {
attributes: assign( {}, settings.attributes, { align: {
type: 'string', default: 'wide'
} } ),
});
// console.log({ settings, name });
}
return settings;
}
addFilter(
'blocks.registerBlockType',
'intro-to-filters/cover-block/alignment-settings',
filterCoverBlockAlignments,
);
The above works so I assume updating the layout's default would be similar, but either I don't have the syntax for an object type correct, or possibly you can't update the layout object like you can update the align string. This is what I tried for the function:
function filterCoverBlockAlignments(settings, name) {
if (name === 'core/group') {
return assign({}, settings, {
attributes: assign( {}, settings.attributes, { layout: {
type: 'object', [{
type: 'default'
}]
} } ),
});
// console.log({ settings, name });
}
return settings;
}
In short I'm trying to get the blocks layer attribute (which is an object and not a string) have it's attribute of type to default to "default" instead of "constrain".
I mean yes, you could solve it by filtering. What I would suggest however, is making a block-variation. Then setting that block-variation as the default. That block variation then can have any of your settings you like to set. Quite simple in theory.
By default, all variations will show up in the Inserter in addition to the regular block type item. However, setting the isDefault flag for any of the variations listed will override the regular block type in the Inserter.
source: https://developer.wordpress.org/block-editor/reference-guides/block-api/block-variations/
wp.blocks.registerBlockVariation( 'core/group', {
name: 'custom-group',
// this one is important, so you don't end up with an extra block
isDefault: true,
// and here you add all your attributes
attributes: { providerNameSlug: 'custom' },
} );
Here is a slightly cleaner variant (using destructuring), if you have more code:
// extract the function from wp.blocks into a variable
const { registerBlockVariation } = wp.blocks;
// register like above
registerBlockVariation();

Filter Apollo Client cache results, tried readQuery but returns null

I have been trying to work with the Apollo Client cache. So, I don't have to make another call to the server. And to help with paging. The stack I am using is Next.js, Apollo Client on the front-end, Keystone.js on the backend.
I am building an e-commerce site. Right now the user can view products by categories. In each category listing, products can be filtered by different attributes. A simple example would be to filter by color and size. Previously I was storing fetched products in state. And I had filtering working pretty well. The main issue I had was paging. When products are filtered on one page, the other pages are not affected. I read up on reading/writing to the cache and thought that would fix the paging issues. But I can't get it to work. Specifically readQuery.
So this is what I have tried and honestly I have not found a good example on using readQuery. It wasn't until I found this question here and read the first answer that I realized you have to use the exact same query that first fetched the results. Or do I?
Here is the parent component and it's first query to fetch products.
\\ Products.jsx
function ProductCategory({ page, category, productType }) {
const [filteredData, setFilteredData] = useState();
const { data, error, loading } = useQuery(ALL_PRODUCTS_FILTERED_QUERY, {
variables: {
skipPage: page * perPage - perPage,
first: perPage,
category,
productType: capitalize(productType),
potency: '0',
},
fetchPolicy: 'cache-first',
});
useEffect(() => {
if (!loading) {
setFilteredData(data?.products);
}
}, [loading]);
if (loading)
return (
<Processing loading={loading.toString()}>
<LoadingIcon tw="animate-spin" />
Loading
</Processing>
);
if (error) return <DisplayError error={error} />;
return (
<>
<Filters
loading={loading}
products={data.products}
setFilteredData={setFilteredData}
productType={productType}
category={category}
page={page}
/>
<ContainerStyles hasBgPrimaryLight20>
<ProductGridStyles>
{filteredData &&
filteredData?.map((product) => (
<Product key={product.id} product={product} />
))}
</ProductGridStyles>
</ContainerStyles>
</>
);
}
ProductCategory.propTypes = {
page: PropTypes.number,
category: PropTypes.string,
productType: PropTypes.string,
};
export default ProductCategory;
My ALL_PRODUCTS_FILTERED_QUERY query:
export const ALL_PRODUCTS_FILTERED_QUERY = gql`
query ALL_PRODUCTS_FILTERED_QUERY(
$skipPage: Int = 0
$first: Int
$category: String
$productType: String
$potency: String
) {
products(
take: $first
skip: $skipPage
orderBy: [{ name: asc }]
where: {
productType: { every: { name: { equals: $productType } } }
category: { slug: { equals: $category } }
flower: { potency: { gte: $potency } }
}
) {
id
name
slug
inventory
price
priceThreshold {
name
price
amount
}
hotDeal
topPick
category {
slug
name
}
photos {
id
image {
publicUrl
}
altText
}
description
status
vendor {
id
name
vendor_ID
}
flower {
label
weight
potency
strain
trimMethod
environment
}
oil {
label
weight
potency
cbd
oilType
solventUsed
}
concentrate {
label
weight
potency
strain
type
}
preRoll {
label
size
potency
strain
type
tube
}
machine {
label
model
modelYear
condition
}
}
}
`;
My Filters.jsx component is what's using the readQuery method to read from the cache and filter results. Or so I hoped. You'll see I am passing the setFilteredData hook from Products.jsx so once products are returned from the cache I am updating the state. Right now I am getting null.
For simplicity I have removed all filters except potency and imports.
\\ Filters.jsx
function Filters({ category, setFilteredData, page, productType }) {
const [potencies, setPotencies] = useState([]);
const [potency, setPotency] = useState();
const { checkboxfilters, setCheckboxFilters } = useFilters([
...strainList,
...environmentList,
...potencyList,
...oilTypeList,
...solventList,
...trimList,
...concentrateTypeList,
...prerollTypeList,
...tubeList,
...priceList,
]);
const client = useApolloClient();
async function fetchProducts(flowerPotency) {
console.log(
page * perPage - perPage,
category,
flowerPotency,
capitalize(productType)
);
try {
const data = await client.readQuery({
query: ALL_PRODUCTS_FILTERED_QUERY,
variables: {
skipPage: page * perPage - perPage,
first: perPage,
category,
productType: capitalize(productType),
potency: flowerPotency,
},
});
setFilteredData(data.products);
} catch (error) {
console.error('Error: ', error);
}
}
const updateCheckboxFilters = (index) => {
setCheckboxFilters(
checkboxfilters.map((filter, currentIndex) =>
currentIndex === index
? {
...filter,
checked: !filter.checked,
}
: filter
)
);
};
const handlePotencyCheck = (e, index) => {
if (e.target.checked) {
setPotency(e.target.value);
fetchProducts(e.target.value);
} else {
setPotency();
}
updateCheckboxFilters(index);
};
return (
<FilterStyles>
<FiltersContainer>
<Popover tw="relative">
<Popover.Button tw="text-sm flex">
Sort{' '}
<ChevronDownIcon
tw="ml-2 h-4 w-4 text-accent"
aria-hidden="true"
/>
</Popover.Button>
<Popover.Panel/>
</Popover>
<div tw="flex space-x-4">
{category === 'flower' ||
category === 'oil' ||
category === 'concentrate' ? (
<Popover tw="relative">
<Popover.Button tw="text-sm flex">
Potency{' '}
<ChevronDownIcon
tw="ml-2 h-4 w-4 text-accent"
aria-hidden="true"
/>
</Popover.Button>
<FilterPopOverPanelStyles>
{potencyList.map((filter) => {
const checkedIndex = checkboxfilters.findIndex(
(check) => check.name === filter.name
);
return (
<Checkbox
key={`potency-${checkedIndex}`}
isChecked={checkboxfilters[checkedIndex].checked}
checkHandler={(e) => handlePotencyCheck(e, checkedIndex)}
label={filter.name.slice(2)}
value={filter.value.slice(2)}
index={checkedIndex}
/>
);
})}
</FilterPopOverPanelStyles>
</Popover>
) : null}
</div>
</FiltersContainer>
<ActiveFilters>
<ActiveFiltersContainer>
<ActiveFiltersHeader>Applied Filters:</ActiveFiltersHeader>
<div tw="flex">
{potencies.map((potency, index) => (
<button
key={index}
type="button"
onClick={() => handleRemoveFilter(potency)}
>
{potency}% <XIcon tw="w-4 h-4 ml-2 text-accent" />
<span tw="sr-only">Click to remove</span>
</button>
))}
</div>
</ActiveFiltersContainer>
</ActiveFilters>
</FilterStyles>
);
}
Filters.propTypes = {
loading: PropTypes.any,
products: PropTypes.any,
setFilteredData: PropTypes.func,
};
export default Filters;
I expected it to return products from the cache based on the potency passed to the query. Instead, I get null. I thought using the exact same query and variables would do the trick. What am I doing wrong? Am I using readQuery correctly? I did try readFragment and got that to successfully work, but it only returns one product. So I know reading from the cache is working.
You're making your life unnecessarily complicated. If you're looking for data that's been previously returned by useQuery or client.readQuery you can query it again with {fetchPolicy: "cache-only"}
Initial query:
const data = await client.readQuery({
query: ALL_PRODUCTS_FILTERED_QUERY,
variables: {
skipPage: page * perPage - perPage,
first: perPage,
category,
productType: capitalize(productType),
potency: flowerPotency,
},
});
Query from cache:
const data = await client.readQuery({
query: ALL_PRODUCTS_FILTERED_QUERY,
variables: {
skipPage: page * perPage - perPage,
first: perPage,
category,
productType: capitalize(productType),
potency: flowerPotency,
},
fetchPolicy: "cache-only", // <-- fetchPolicy!
});
Also you don't need either useState or useEffect to copy the data coming back from your query - it's not adding any value.
After digging back through my code, I realized I had pagination setup incorrectly as it was initially setup for a single product category. Now that I have multiple categories my code needs refactored to take this into account. So conditions were never being met and always fetching from the network. Plus I need to really sit down and truly understand how Apollo client works.
The first issue was as simple as arguments named incorrectly. Initially I had args being passed into my read() function named skip,first. Doing an update last year for Keystone those got renamed to skip,take.
Second issue was my PAGINATION_QUERY was getting the count for all products. But now since they are split into categories, I just need the product count for the current category.
After these two tweaks I can now use readQuery to query the cache.

WordPress TinyMCE custom styles with input option

I want to add a custom style to the wordpress tiny mce. There are tons of tutorials for just adding a simple option like "highlight" which will add a span with a "highlight" class. Like: https://torquemag.io/2016/09/add-custom-styles-wordpress-editor-manually-via-plugin/
But what I need is an option to add additional data, like if you add a link. You mark the words, hit the link button, an input for the url shows up.
What I want to achieve? A custom style "abbriation" (https://get.foundation/sites/docs/typography-base.html). The solution I'm thinking of is, the user marks the word, chooses the abbriation style, an input for the descriptions shows up. fin.
Hope you can help me out!
So I have something similar in most of my WordPress projects. I have a TinyMCE toolbar button that has a couple of fields that output a bootstrap button.
What you need to do is create your own TinyMCE "plugin" and to achieve this you need two parts:
A javascript file (your plugin)
A snippet of PHP to load your javascript (plugin) into the TinyMCE editor.
First we create the plugin:
/js/my-tinymce-plugin.js
( function() {
'use strict';
// Register our plugin with a relevant name
tinymce.PluginManager.add( 'my_custom_plugin', function( editor, url ) {
editor.addButton( 'my_custom_button', {
tooltip: 'I am the helper text',
icon: 'code', // #link https://www.tiny.cloud/docs/advanced/editor-icon-identifiers/
onclick: function() {
// Get the current selected tag (if has one)
var selectedNode = editor.selection.getNode();
// If we have a selected node, get the inner content else just get the full selection
var selectedText = selectedNode ? selectedNode.innerHTML : editor.selection.getContent();
// Open a popup
editor.windowManager.open( {
title: 'My popup title',
body: [
// Create a simple text field
{
type: 'textbox',
name: 'field_name_textbox',
label: 'Field label',
value: selectedText || 'I am a default value' // Use the selected value or set a default
},
// Create a select field
{
type: 'listbox',
name: 'field_name_listbox',
label: 'Field list',
value: '',
values: {
'value': 'Option 1',
'value-2': 'Option 2'
}
},
// Create a boolean checkbox
{
type: 'checkbox',
name: 'field_name_checkbox',
label: 'Will you tick me?',
checked: true
}
],
onsubmit: function( e ) {
// Get the value of our text field
var textboxValue = e.data.field_name_textbox;
// Get the value of our select field
var listboxValue = e.data.field_name_listbox;
// Get the value of our checkbox
var checkboxValue = e.data.field_name_checkbox;
// If the user has a tag selected
if ( selectedNode ) {
// Do something with selected node
// For example we can add a class
selectedNode.classList.add( 'im-a-custom-class' );
} else {
// Insert insert content
// For example we will create a span with the text field value
editor.insertContent( '<span>' + ( textboxValue || 'We have no value!' ) + '</span>' );
}
}
} );
}
} );
} );
} )();
Now we add and modify the below snippet to your themes functions.php file.
/functions.php
<?php
add_action( 'admin_head', function() {
global $typenow;
// Check user permissions
if ( !current_user_can( 'edit_posts' ) && !current_user_can( 'edit_pages' ) ) {
return;
}
// Check if WYSIWYG is enabled
if ( user_can_richedit() ) {
// Push my button to the second row of TinyMCE actions
add_filter( 'mce_buttons', function( $buttons ) {
$buttons[] = 'my_custom_button'; // Relates to the value added in the `editor.addButton` function
return $buttons;
} );
// Load our custom js into the TinyMCE iframe
add_filter( 'mce_external_plugins', function( $plugin_array ) {
// Push the path to our custom js to the loaded scripts array
$plugin_array[ 'my_custom_plugin' ] = get_template_directory_uri() . '/js/my-tinymce-plugin.js';
return $plugin_array;
} );
}
} );
Make sure to update the file name and path if you it's different to this example!
WordPress uses TinyMCE 4 and the documentation for this is lacking so finding exactly what you need can be painful.
This is merely a starting point and has not been tested.
Hope this helps!
EDIT
The below code should help you with the insertion of an "abbreviations" tag and title attribute.
( function() {
'use strict';
tinymce.PluginManager.add( 'my_custom_plugin', function( editor, url ) {
editor.addButton( 'my_custom_button', {
tooltip: 'Insert an abbreviation',
icon: 'plus',
onclick: function() {
var selectedNode = editor.selection.getNode();
var selectedText = selectedNode ? selectedNode.innerHTML : editor.selection.getContent();
editor.windowManager.open( {
title: 'Insert an abbreviation',
body: [
{
type: 'textbox',
name: 'abbreviation',
label: 'The abbreviated term',
value: selectedText
},
{
type: 'textbox',
name: 'title',
label: 'The full term',
value: ''
}
],
onsubmit: function( e ) {
var abbreviation = e.data.abbreviation;
var title = e.data.title.replace( '"', '\"' );
if ( selectedNode && selectedNode.tagName === 'ABBR' ) {
selectedNode.innerText = abbreviation;
selectedNode.setAttribute( 'title', title );
} else {
editor.insertContent( '<abbr title="' + title + '">' + abbreviation + '</abbr>' );
}
}
} );
}
} );
} );
} )();

Disable Algolia for WP SearchAsYouType

I have implemented Algolia for Wordpress and am customizing the search template in the "instantsearch.php" file.
Looking at the documentation here https://www.algolia.com/doc/api-reference/widgets/search-box/js/#widget-param-searchasyoutype setting the searchAsYouType parameter to false should prevent Algolia from detecting user input and searching while a user types. However, this parameter is not disabling the search as you type for my site.
Below is the code for my input field widget and other custom components:
/* Instantiate instantsearch.js */
var search = instantsearch({
appId: algolia.application_id,
apiKey: algolia.search_api_key,
indexName: algolia.indices.searchable_posts.name,
urlSync: {
mapping: {'q': 's'},
trackedParameters: ['query']
},
searchParameters: {
facetingAfterDistinct: true,
highlightPreTag: '__ais-highlight__',
highlightPostTag: '__/ais-highlight__'
}
});
/* Search box widget */
search.addWidget(
instantsearch.widgets.searchBox({
container: '#algolia-search-box',
placeholder: 'Search ...',
searchAsYouType: false,
wrapInput: false,
poweredBy: algolia.powered_by_enabled
})
);
/* Hits widget */
search.addWidget(
instantsearch.widgets.hits({
container: '#algolia-hits',
hitsPerPage: 10,
templates: {
empty: 'No results were found for "<strong>{{query}}</strong>".',
item: wp.template('instantsearch-hit')
},
transformData: {
item: function (hit) {
function replace_highlights_recursive (item) {
if( item instanceof Object && item.hasOwnProperty('value')) {
item.value = _.escape(item.value);
item.value = item.value.replace(/__ais-highlight__/g, '<em>').replace(/__\/ais-highlight__/g, '</em>');
} else {
for (var key in item) {
item[key] = replace_highlights_recursive(item[key]);
}
}
return item;
}
hit._highlightResult = replace_highlights_recursive(hit._highlightResult);
hit._snippetResult = replace_highlights_recursive(hit._snippetResult);
console.log(hit);
if ( hit.post_excerpt != '' ){
hit._snippetResult['content']['value'] = hit.post_excerpt;
}
if ( hit.short_title.length > 0 && hit.short_title[0] != '' ){
hit._highlightResult['post_title']['value'] = hit.short_title[0];
}
return hit;
}
}
})
);
/* Pagination widget */
search.addWidget(
instantsearch.widgets.pagination({
container: '#algolia-pagination'
})
);
/* Tags refinement widget */
search.addWidget(
instantsearch.widgets.refinementList({
container: '#facet-tags',
attributeName: 'taxonomies.aar_article_type',
operator: 'and',
limit: 15,
sortBy: ['isRefined:desc', 'count:desc', 'name:asc'],
templates: {
header: '<h2 class="widgettitle">Filter Results</h2>'
}
})
);
/* Start */
search.start();
I have added the parameter to the search box widget, but when i type, the page still auto updates without me having to hit submit. Am I missing a configuration or something?
The correct property for accomplishing this is searchOnEnterKeyPressOnly , not searchAsYouType
The proper documentation for the widget I am using can be found here https://community.algolia.com/instantsearch.js/v2/widgets/searchBox.html#struct-SearchBoxWidgetOptions-searchOnEnterKeyPressOnly

reactjs datagrid use html

Im using the datagrid component here
I would like to use html in one of the fields and show a link or a picture etc.
I tried using the render function for the column as below
var columns = [
{ name = 'field' },
{ name = 'link',render : function(uri) { return 'link'} },
];
however it prints out the html as text
This is because by default React escapes HTML, in order to prevent XSS attacks. You can by pass this, by using the prop dangerouslySetInnerHTML, as described here.
However, as the name suggests, this leads to a vulnerability. I would suggest instead to use Mardown, especially the marked package.
You can write a general component like this one and then use it everywhere.
import React from 'react';
import marked from 'marked';
const Markdown = React.createClass({
render() {
const raw = marked(this.props.text, {sanitize: true});
return <span dangerouslySetInnerHTML={{__html: rawMarkup}} />;
}
});
In your case then
var columns = [
{ name = 'field' },
{ name = 'link', render : function(uri) { return <Markdown text={'[link](' + uri + ')'} />} },
];
First I created a class which will output a link
var Linkify = React.createClass({
render : function(){
return (
<a href={this.props.link}>{this.props.title}</a>
)
},
});
Then used this class in the render function
var columns = [
{ name : 'edit', render : function(id){
var editlink = "http://www.example.com/id="+id;
return ( <Linkify link={editlink} title="edit" />)
}
},
This way any html can be used in the datagrid column by simply using the react component.

Resources