WP Block Styles - Trigger JS when selecting a block style - javascript

Is there a 'right way' to run JS in the WP Block editor when a block style is selected?
I've searched through documentation and google but can't see an example of anyone running a JS script on a block style.
Essentially, I want to inject two elements around a core/group block, if it has a particular style.
My code works just fine on the front end, but in the backend, I can only get it working on a page refresh, after the block style is selected. Here is what i've got so far:
Registerd by block style with php:
register_block_style(
'core/group',
array(
'name' => 'shape-pattern-1',
'label' => 'Shape Pattern 1'
)
);
Function I want to run when style is in use
var initialiseShapePatterns = function($block) {
$block.prepend('<span class="shape-before"></span>');
$block.append('<span class="shape-after"></span>');
$(window).on('load scroll', function() {
if( $block.isInViewport() ) {
$block.addClass('animate');
} else {
$block.removeClass('animate');
}
});
}
Calling function on front-end (working)
$(document).ready(function() {
$('.is-style-shape-pattern-1, .is-style-shape-pattern-2, .is-style-shape-pattern-3').each(function() {
initialiseShapePatterns( $(this) );
});
});
Calling function in block editor (only works if style is already selected on page load)
if( wp.domReady ) {
wp.domReady(function() {
$('.is-style-shape-pattern-1, .is-style-shape-pattern-2, .is-style-shape-pattern-3').each(function() {
initialiseShapePatterns( $(this) );
});
});
}
I get that I'm only telling it to run when the dom is loaded, but I can't find anything in the documentation about running code on style select.
Any ideas?

In jquery, you can add event listners, which will trigger an action when a event happens.
The following code snippet will execute when a button with the id 'myButtonId' is clicked.
$( "#myButtonId" ).click(function() {
//myButtonId was clicked.
$('.is-style-shape-pattern-1, .is-style-shape-pattern-2, .is-style-shape-pattern-3').each(function() {
initialiseShapePatterns( $(this) );
});
});
You will need to set the id of the button which you want to trigger this function. You can do this with the html id attribute
<button id='myButtonId'> Button Text </button>
In wordpress you can add custom HTML.You mentioned blocks so i assume you are using the gutenburg block editor. This means that you can find the block 'custom HTML' and then put that html button in the custom code.

Add an even listener to the clicked object when selecting style that does it for you. Something like this (change #styleselectbutton to the correct element).
$( "#styleselectbutton" ).click(function() {
$('.is-style-shape-pattern-1, .is-style-shape-pattern-2, .is-style-shape-pattern-3').each(function() {
initialiseShapePatterns( $(this) );
});
});

You can try like as follows..
function my_plugin_blocks() {
wp_add_inline_script( 'wp-edit-post', "
wp.blocks.registerBlockStyle( 'core/group', {
name: 'shape-pattern-1',
label: 'Shape Pattern 1',
});
jQuery.fn.isInViewport = function() {
var elementTop = jQuery(this).offset().top;
var elementBottom = elementTop + jQuery(this).outerHeight();
var viewportTop = jQuery(window).scrollTop();
var viewportBottom = viewportTop + jQuery(window).height();
return elementBottom > viewportTop && elementTop < viewportBottom;
};
var initialiseShapePatterns = function(\$block) {
if(\$block.find('> span.shape-before').length == 0)
\$block.prepend('<span class=\"shape-before\"></span>');
if(\$block.find('> span.shape-after').length == 0)
\$block.append('<span class=\"shape-after\"></span>');
};
const getBlockList = () => wp.data.select( 'core/block-editor' ).getBlocks();
let blockList = getBlockList();
wp.data.subscribe(() => {
const newBlockList = getBlockList();
const blockListChanged = newBlockList !== blockList;
blockList = newBlockList;
if ( blockListChanged ) {
jQuery('.is-style-shape-pattern-1, .is-style-shape-pattern-2, .is-style-shape-pattern-3').each(function() {
initialiseShapePatterns( jQuery(this) );
});
}
});
function scrollcontent()
{
jQuery('.is-style-shape-pattern-1, .is-style-shape-pattern-2, .is-style-shape-pattern-3').each(function() {
if(jQuery(this).isInViewport()) {
jQuery(this).addClass('animate');
} else {
jQuery(this).removeClass('animate');
}
});
};
setTimeout(function(){
scrollcontent();
jQuery('.edit-post-layout__content').on('load scroll resize', function() { scrollcontent(); });
}, 500); ");
}
add_action( 'enqueue_block_editor_assets', 'my_plugin_blocks' );
Here i hooked script from functions.php and registerBlockStyle using js not php.You can use any other ways.
I guess you need to write animate when content area scrolls not for window. Because content area is fixed in Wordpress block editor.
Here the helpful docs:
Doc1
Doc2

None of the answers were satisfying to me, so here's my solution. I am using a block called section and I want a trigger when the style variant dark is selected. I am using WordPress 5.6.
My first approach:
document.querySelectorAll('.block-editor-block-styles__item').forEach(
(element) => addClickListener(element)
);
// get the title of the block to make sure to only target this block with the style variant 'dark' (because there might be other blocks which also have this variant name.
const blockTitle = document.querySelector('.block-editor-block-card__title');
function addClickListener(styleButton) {
styleButton.addEventListener('click', () => {
if (blockTitle && blockTitle.textContent === 'Section') {
// Get the right styleButton by the label that is assigned to it.
if(styleButton.getAttribute('aria-label') === 'Dark'){
soSomething(); // Execute this function when the style 'Dark' is selected.
}
}
})
}
Now as you can tell, this approach is quite bad. It uses the title of the block that is rendered in some HTML and also the label of the style variant to apply an event listener to. It does work, but chances are the markup (HTML) of the editor will change in the future, breaking this code.
My second (better) approach:
// Checking for changes in the classNames to make changes to the bloc (only when the block is selected).
if (props.isSelected) {
const getCurrentBlock = () => wp.data.select('core/editor').getBlock(props.clientId);
let oldBlock = getCurrentBlock();
wp.data.subscribe(() => {
const newBlock = getCurrentBlock();
// Change this line with your classname and callback function.
whenStyleIsChanged('is-style-dark', () => doSomething() )
function whenStyleIsChanged(style, callback) {
if (newBlock?.attributes?.className?.includes(style) && !oldBlock?.attributes?.className?.includes(style)) {
oldBlock = newBlock; // Reset to prevent never-ending recursion.
callback()
}
}
})
}
Not only is the second approach much cleaner and future-proof, it is also the 'Gutenberg way' (Redux in reality) and I can trigger my function only when my block style is changed, as opposed to whenever a click is registered on the "style button". This is done by comparing the classnames of the block before and after the update, in my case the class is-style-dark. Make sure to change this and the callback function accordingly.

Related

Javascript ignoring if statements

I'm somewhat new to Javascript. I'm trying to make it so that clicking on an image on one page takes you to a new page and shows a specific div on that new page, so I used sessionStorage to remember and booleans to keep track of which image is being clicked. Right now, the code always executes the first if statement, regardless of which image is clicked. This code works fine in normal java so I can't figure out why my if statements are being ignored in javascript. I also tried adding an 'else' at the end, and tried ===. Here's my javscript, and thank you!
sessionStorage.clickedLeft;
sessionStorage.clickedMiddle;
sessionStorage.clickedRight;
function openedProjectFromGallery() {
if(sessionStorage.clickedLeft) {
$(".left-project-pop-up").show();
} else if (sessionStorage.clickedMiddle) {
$(".middle-project-pop-up").show();
} else if (sessionStorage.clickedRight) {
$(".right-project-pop-up").show();
}
sessionStorage.clickedLeft = false;
sessionStorage.clickedMiddle = false;
sessionStorage.clickedRight = false;
}
$("document").ready(function () {
$(".pop-up .x-button").click(function(){
$(".pop-up").hide();
});
$(".project-description .x-button").click(function(){
$(".project-pop-up").hide();
});
$(".left-project-thumb img").on("click", ".left-project-thumb img", function(){
sessionStorage.clickedLeft = true;
sessionStorage.clickedMiddle = false;
sessionStorage.clickedRight = false;
openedProjectFromGallery();
});
$(".profile-left-project img").click(function(){
$(".left-project-pop-up").show(1000);
});
$(".middle-project-thumb img").on("click", ".middle-project-thumb img", (function(){
sessionStorage.clickedMiddle = true;
sessionStorage.clickedLeft = false;
sessionStorage.clickedRight = false;
openedProjectFromGallery();
});
$(".profile-middle-project img").click(function(){
$(".middle-project-pop-up").show(1000);
});
$(".right-project-thumb img").on("click", ".right-project-thumb img", (function(){
sessionStorage.clickedRight = true;
sessionStorage.clickedLeft = false;
sessionStorage.clickedMiddle = false;
openedProjectFromGallery();
});
$(".profile-right-project img").click(function(){
$(".right-project-pop-up").show(1000);
});
});
You are defining function openedProjectFromGallery() with in document.ready . Define it outside document.ready and also give your three booleans some initial value at the top of your code if not initialized with some value or they are empty. I hope this would help.
It is not really answer to your orginal question,as the main issue with your code is, as #njzk2 says, that openProjectFromGallery only being called once, and not on each event, however I wanted to put my two coins on how this code could look like.
This is good example where custom events should be used
$(document).on('showPopup', function( e, popup ) {
$('.'+popup + '-project-pop-up').show()
})
$(document).on('hidePopup', function( e ) {
$('.popup').hide()
})
$('.left-project-thumb img').on('click', function(e) {
$(document).trigger('showPopup', ['left'])
})
$('.right-project-thumb img').on('click', function(e) {
$(document).trigger('showPopup', ['right'])
})
I think you get an idea.
On the other hand, it always nice to use event delegation with a lot of similar events as well as dom data.
<div class='popup' data-popup='left'>
<img />
</div>
$(document).on('click','.popup', function( e ) {
$(document).trigger('showPopup', [$(this).data('popup')])
})
From what I can see openedProjectFromGallery is only getting called on document load.
Add a call to it into each of the event handling functions or use jQuery's delegate function to assign event handling to each image.

Using .style.opacity = using javascript is not working for some reason

I'm new to javascript and jquery and I am trying to make a video's opacity change when I mouseover a li item. I know 'onmouseover' works because I have tested using the same jquery I use to scroll to the top of the page onclick.
The problem seems to be the syntax to check and update the style of the video div is not working. I adapted the code from a lesson on codeacademy and don't see why it work:
window.onload = function () {
// Get the array of the li elements
var vidlink = document.getElementsByClassName('video');
// Get the iframe
var framecss = document.getElementsByClassName('videoplayer1');
// Loop through LI ELEMENTS
for (var i = 0; i < vidlink.length; i++) {
// mouseover function:
vidlink[i].onmouseover = function () {
//this doesn't work:
if (framecss.style.opacity === "0.1") {
framecss.style.opacity = "0.5";
}
};
//onclick function to scroll to the top when clicked:
vidlink[i].onclick = function () {
$("html, body").animate({
scrollTop: 0
}, 600);
};
}
};
Here is a jsfiddle you can see the html and css:
http://jsfiddle.net/m00sh00/CsyJY/11/
It seems like such a simple problem so I'm sorry if I'm missing something obvious.
Any help is much appreciated
Try this:
vidlink[i].onmouseover = function () {
if (framecss[0].style.opacity === "0.1") {
framecss[0].style.opacity = "0.5";
}
};
Or alternatively:
var framecss = document.getElementsByClassName('videoplayer1')[0];
Or, better, give the iframe an id and use document.getElementById().
The getElementsByClassName() function returns a list, not a single element. The list doesn't have a style property. In your case you know the list should have one item in it, which you access via the [0] index.
Or, given that you are using jQuery, you could rewrite it something like this:
$(document).ready(function(){
// Get the iframe
var $framecss = $('.videoplayer1');
$('.video').on({
mouseover: function () {
if ($framecss.css('opacity') < 0.15) {
$framecss.css('opacity', 0.5);
}
},
click: function () {
$("html, body").animate({
scrollTop: 0
}, 600);
}
});
});
Note that I'm testing if the opacity is less than 0.15 because when I tried it out in your fiddle it was returned as 0.10000000149011612 rather than 0.1.
Also, note that the code in your fiddle didn't run, because by default jsfiddle puts your JS in an onload handler (this can be changed from the drop-down on the left) and you then wrapped your code in window.onload = as well. And you hadn't selected jQuery from the other drop-down so .animate() wouldn't work.
Here's an updated fiddle: http://jsfiddle.net/CsyJY/23/

jQuery switching between more than two classes

I've already posted a question about jQuery toggle method here
But the problem is that even with the migrate plugin it does not work.
I want to write a script that will switch between five classes (0 -> 1 -> 2 -> 3 -> 4 -> 5).
Here is the part of the JS code I use:
$('div.priority#priority'+id).on('click', function() {
$(this).removeClass('priority').addClass('priority-low');
});
$('div.priority-low#priority'+id).on('click' ,function() {
$(this).removeClass('priority-low').addClass('priority-medium');
});
$('div.priority-medium#priority'+id).on('click', function() {
$(this).removeClass('priority-medium').addClass('priority-normal');
});
$('div.priority-normal#priority'+id).on('click', function() {
$(this).removeClass('priority-normal').addClass('priority-high');
});
$('div.priority-high'+id).on('click', function() {
$(this).removeClass('priority-high').addClass('priority-emergency');
});
$('div.priority-emergency'+id).on('click', function() {
$(this).removeClass('priority-emergency').addClass('priority-low');
});
This is not the first version of the code - I already tried some other things, like:
$('div.priority#priority'+id).toggle(function() {
$(this).attr('class', 'priority-low');
}, function() {
$(this).attr('class', 'priority-medium');
}, function() {
...)
But this time it only toggles between the first one and the last one elements.
This is where my project is: strasbourgmeetings.org/todo
The thing is that your code will hook your handlers to the elements with those classes when your code runs. The same handlers remain attached when you change the classes on the elements.
You can use a single handler and then check which class the element has when the click occurs:
$('div#priority'+id).on('click', function() {
var $this = $(this);
if ($this.hasClass('priority')) {
$this.removeClass('priority').addClass('priority-low');
}
else if (this.hasClass('priority-low')) {
$this.removeClass('priority-low').addClass('priority-medium');
}
else /* ...and so on... */
});
You can also do it with a map:
var nextPriorities = {
"priority": "priority-low",
"priority-low": "priority-medium",
//...and so on...
"priority-emergency": "priority"
};
$('div#priority'+id).on('click', function() {
var $this = $(this),
match = /\bpriority(?:-\w+)?\b/.exec(this.className),
current = match && match[0],
next = nextPriorities[current];
if (current) {
$this.removeClass(current).addClass(next || 'priority');
}
});
[edit: working demo]
Assuming you have 'priority' as the default class already on the element at the initialization phase, this will cycle through the others:
$('div#priority' + id)
.data('classes.cycle', [
'priority',
'priority-low',
'priority-medium',
'priority-normal',
'priority-high',
'priority-emergency'
])
.data('classes.current', 0)
.on('click', function () {
var $this = $(this),
cycle = $this.data('classes.cycle'),
current = $this.data('classes.current');
$this
.removeClass(cycle[current % cycle.length])
.data('classes.current', ++current)
.addClass(cycle[current % cycle.length]);
});
I have tried myself to do this with the sole help of toggleClass() and didn't succeeded.
Try my method that declares an array with your five classes and toggles dynamically through
them.Do adapt to your own names.
//variable for the classes array
var classes=["one","two","three","four","five"];
//add a counter data to your divs to have a counter for the array
$('div#priority').data("counter",0);
$(document).on('click','div#priority',function(){
var $this=$(this);
//the current counter that is stored
var count=$this.data("counter");
//remove the previous class if is there
if(($this).hasClass(classes[count-1])){
$(this).removeClass(classes[count-1]));
}
//check if we've reached the end of the array so to restart from the first class.
//Note:remove the comment on return statement if you want to see the default class applied.
if(count===classes.length){
$this.data("counter",0);
//return;//with return the next line is out of reach so class[0] wont be added
}
$(this).addClass(classes[count++]);
//udpate the counter data
$this.data("counter",count);
});
//If you use toggleClass() instead of addClass() you will toggle off your other classes.Hope is a good answer.

Best way to silently bind window resize event to jQuery plugin without keeping a reference to the targeted element

I'm looking for best-practice advice.
I'm writing a small jQuery plugin to manage horizontal scroll on elements.
I need all the dom elements targeted by that plugin to update on window resize.
Fact is, my website is a full ajax 'app' so when I remove DOM elements, I need them gone so memory doesn't leak.
But I can't find a way to bind the resize event without keeping a reference to the DOM node.
EDIT :
Actually I need the resize handler to get the plugin-targeted elements at 'call' time, coz I don't want to keep any reference to those elements in memory, because I might call .html('') on a parent of theirs...
I did not paste all my code, just an empty shell. I already have a destroy method that unbinds handlers. But I'm generating, removing and appending html nodes dynamically and I the the elements targeted by the plugin to remove silently.
Kevin B stated I could override jQuery .remove method to deal with the handlers, but would have to load jQuery UI for it to work. I don't want that either..
Here is what I tried (attempts commented):
(function($) {
// SOLUTION 2 (see below too)
// Not good either coz elements are not removed until resize is triggered
/*
var hScrolls = $([]);
$(window).bind('resize.hScroll',function(){
if(!hScrolls.length) return;
hScrolls.each(function(){
if($(this).data('hScroll')) $(this).hScroll('updateDimensions');
else hScrolls = hScrolls.not($(this));
});
});
*/
// END SOLUTION 2
// SOLUTION 3 (not implemented but I think I'm on the right path)
$(window).bind('resize.hScroll',function(){
// need to get hScroll'ed elements via selector...
$('[data-hScroll]').hScroll('updateDimensions');
// I don't know how....
});
// END SOLUTION 3
var methods = {
init : function(options) {
var settings = $.extend( {
defaults: true
}, options);
return this.each(function() {
var $this = $(this),
data = $this.data('hScroll');
if (!data) {
$this.data('hScroll', {
target: $this
});
// SOLUTION 1
// This is not good: it keeps a reference to $this when I remove it...
/*
$(window).bind('resize.hScroll', function(){
$this.hScroll('updateDimensions');
});
*/
// END SOLUTION 1
$this.hScroll('updateDimensions');
// SOLUTION 2 (see above too)
hScrolls = hScrolls.add(this);
}
});
},
updateDimensions: function(){
var hScroll = this.data('hScroll');
// do stuff with hScroll.target
}
}
$.fn.hScroll = function(method) {
if (methods[method]) {
return methods[method].apply(this, Array.prototype.slice.call(arguments, 1));
} else if ( typeof method === 'object' || !method) {
return methods.init.apply(this, arguments);
} else {
$.error('Method ' + method + ' does not exist on jQuery.hScroll');
}
};
})(jQuery);​
Thanks all in advance!
jQuery calls cleanData any time you do something that removes or replaces elements (yes, even if you use parent.html("") ). You can take advantage of that by extending it and having it trigger an event on the target elements.
// This is taken from https://github.com/jquery/jquery-ui/blob/master/ui/jquery.ui.widget.js 10/17/2012
if (!$.widget) { // prevent duplicating if jQuery ui widget is already included
var _cleanData = $.cleanData;
$.cleanData = function( elems ) {
for ( var i = 0, elem; (elem = elems[i]) != null; i++ ) {
try {
$( elem ).triggerHandler( "remove" );
// http://bugs.jquery.com/ticket/8235
} catch( e ) {}
}
_cleanData( elems );
};
}
Now you can bind to the remove event when setting up your plugin and have it run your destroy method.
$(elem).bind("remove",methods.destroy)
You might use a class name and forward the resize event:
$.fn.hScroll = function(method) {
this
.addClass('hScroll')
.data('method', arguments)
};
var methods['alert_text'] = function(config){
alert( config + " " + $(this).text() );
}
$(window).bind('resize.hScroll',function(){
$(".hScroll").each(function(){
var method_config = $(this).data('method');
var method = method_config.shift();
// Forward the resize event with all resize event arguments:
methods[method].apply(this, method_config);
})
})
// Register a resize event for all a.test elements:
$("a.test").hScroll('alert_text', "hey");
// Would alert "hey you" for <a class="test">you</a> on every resize
Update
If you change the dom and want to keep the selector you might try this one:
var elements = [];
$.fn.hScroll = function(method) {
elements.push({'selector' : this.selector, 'arguments' : arguments });
};
var methods['alert_text'] = function(config){
alert( config + " " + $(this).text() );
}
$(window).bind('resize.hScroll',function(){
$.each(elements,function(i, element){
$(element.selector).each(function(){
var method_config = element.arguments;
var method = method_config.shift();
// Forward the resize event with all resize event arguments:
methods[method].apply(this, method_config);
})
})
})
// Register a resize event for all a.test elements:
$("a.test").hScroll('alert_text', "hey");
$(document.body).html("<a class='test'>you</a>");
// Would alert "hey you" for every window resize
You should have the scroll event bound in the extension. Also, you will want to add a "destroy" method to your extension as well. Before you remove the element from the DOM, you will want to call this method. Inside the detroy method is where you will want to unbind the resize event.
One important thing in making this work is that you have a reference to each handler method that is bound to the resize event. Alternatively, you can unbind All resize events upon the removal on an element and then rebind the scroll event to the remaining elements that require it.

using .delegate function in slide show

I have a script that runs a slideshow for my page. I'm trying to use .delegate() to insert a new set of images shown within the slideshow including its thumbnails. I'm using a .load() function to load an external <div> to replace some HTML within the active page. I also have buttons with IDs, (#kick1, #kwick2, etc.) that determine what set of slide show is loaded.
jQuery("#kwick2").click(function () {
jQuery("body").delegate('#slideshow', 'click', function() {
jQuery('#slideshow').load('/design.html #design');
)};
)};
Pretty sure the syntax is all wrong. Can someone help me?
The #slideshow div is something I created to contain some other divs directly
effected by the slideshow script. Within div ID #slideshow are
<div class="main_image">, <div class="desc"> and <div class="image_thumb">.
These are being replaced directly when you click a KWICK button, they are all pretty much self explanatory, image thumb has and unordered list with image links.
You should not re-deligate every time you click the button. You are doing the wrong.
Instead what you should have is something like:
var foobar = (function () {
var func , mod1 , mod2;
mod1 = function () {
/* do something in state 1 */
};
mod2 = function () {
/* do something in state 2 */
};
return {
state: function (e) {
switch (this.id){
case 'kwick1':
func = mod1;
break;
case 'kwick2':
func = mod2;
break;
}
},
callback: function (e) {
func.call();
}
}
})();
jQuery("#kwick1").click( foobar.state ); // and you really should delegate this
jQuery("#kwick2").click( foobar.state );
jQuery("body").delegate('#slideshow','click', foobar.callback);
Or something similar to this ..
And no , i did not test this code. It is written to explain the concept, not to spoon-feed people.
not sure if i have understood the question well, anyway you have to specify what event you are delegating
jQuery("#kwick2").click(function () {
jQuery("body").delegate('#slideshow',"click", function(){
jQuery('#slideshow').load('/design.html #design');
)};
)};
delegate

Categories