Apply a 'table'-class to a WooCommerce table after AJAX-call - javascript

WooCommerce-tables comes with classes like these, out of the box: shop_table shop_table_responsive cart woocommerce-cart-form__contents. So no table-class, which means no nifty Bootstrap-tables.
Huh!
And since overriding the WooCommerce-templates should only be done when absolutely necessary, then let's solve it with JavaScript!
My entire site it encapsulated by a Vue-div, like so:
<div id="app">
...
<table class="shop_table shop_table_responsive cart woocommerce-cart-form__contents">
...
...
</table>
...
</div>
So initially I wrote this code, to add the table-class to all tables:
let tableSelectors = [
'.some-class table',
'.woocommerce-product-attributes',
'.woocommerce-cart-form > table'
];
for( let t = 0; t < tableSelectors.length; t++ ){
let tables = document.querySelectorAll( tableSelectors[t] );
if( tables ){
for( let i = 0; i < tables.length; i++ ){
tables[i].classList.add( 'table' );
}
}
}
... Putting that in the mounted(){ ... }-section.
That worked! So far so good.
But WooCommerce is using jQuery quite a lot. And on the cart page, if I change the quantity (and press 'Update'), then the table-contents are updated using AJAX. If you're curious how it works, then you can check it out here.
And when that runs, I assume that WooCommerce grabs the initial cart-template and reloads that whole table; without the newly added table-class. Bah humbug!
So how can I solve this?
I can override the WooCommerce ./cart/cart.php-template and add the
class to the template. Seems like quite the overkill for adding a class.
I can scan the DOM for tables every second (or so) and apply the table class, if it's not there. Not cool... Regardless if it's done using jQuery or Vue.
Since the whole table is being replaced in the DOM, then it doesn't work to monitor the current table (using watch(){...} in Vue) and apply the class if it changes, - since it never changes (it's replaced).
I'm unable to find a Hook that I can use.
I also tried using ajaxComplete, but I can see in the network-tab that the XHR-request is firing, but this code here is never doing anything (in the console):
jQuery( document ).ajaxComplete(function( event, xhr, settings ) {
console.log( 'Test' );
});
Any other suggestions?

You could use the Mutation Observer API to listen for changes to a wrapper element's contents and re-apply the table classes.
This example is lifted nearly verbatim from the sample code on MDN. Clicking the button replaces the contents of the div, which you can see from the console output fires the observer callback.
// Select the node that will be observed for mutations
const targetNode = document.getElementById('some-id');
// Options for the observer (which mutations to observe)
const config = {
childList: true,
subtree: true
};
// Callback function to execute when mutations are observed
const callback = function(mutationsList, observer) {
for (let mutation of mutationsList) {
if (mutation.type === 'childList') {
console.log('A child node has been added or removed.');
}
}
};
// Create an observer instance linked to the callback function
const observer = new MutationObserver(callback);
// Start observing the target node for configured mutations
observer.observe(targetNode, config);
function doUpdate() {
targetNode.innerText = Math.random();
}
document.querySelector('button').addEventListener('click', doUpdate);
<div id="some-id">(container)</div>
<button>change</button>

The ajaxComplete() function of jQuery can do what you expected,
I don't know why it's not working for you.
I just open the link of cart page you gave above and paste add the below code in the developer console
and it works as expected after each update of the cart, the "table" class successfully appended to the table as per given selectors.
jQuery( document ).ajaxComplete(function( event, xhr, settings ) {
jQuery('.some-class table, .woocommerce-product-attributes, .woocommerce-cart-form > table').addClass("table");
});
It looks like you have not added the code in the proper place. Since ajaxComplete() function is dependent on jQuery you need to execute the above code after jQuery has been loaded successfully. To do that you can use wp_add_inline_script() function with wp_script_is()
Add the following code in your function.php file, It will add the below script to page after jQuery finish loading.
function my_custom_script() {
if ( ! wp_script_is( 'jquery', 'done' ) ) {
wp_enqueue_script( 'jquery' );
}
wp_add_inline_script( 'jquery-migrate', 'jQuery( document ).ajaxComplete(function( event, xhr, settings ) {
jQuery(".some-class table, .woocommerce-product-attributes, .woocommerce-cart-form > table").addClass("table");
});' );
}
add_action( 'wp_enqueue_scripts', 'my_custom_script');

You can try to change the jquery ajax function
(function ($) {
var _oldAjax = $.ajax;
$.ajax = function (options) {
return _oldAjax(options).done(function (data) {
$('.some-class table, .woocommerce-product-attributes, .woocommerce-cart-form > table').addClass("table");
if (typeof (options.done) === "function")
options.done();
});
};
})(jQuery);
above code must be added before any ajax that is called

i've tested it on your site using the javascript console and actually
jQuery( document ).ajaxComplete(function( event, xhr, settings ) {
console.log( 'Test' );
});
fire pretty well.
maybe you are adding that code on wrong place.. or maybe you have "Group Similar" flagged on javascript console settings and you didn't notice the log
so you could just put your code together like this:
jQuery( document ).ajaxComplete(function( event, xhr, settings ) {
try{
let tableSelectors = [
'.some-class table',
'.woocommerce-product-attributes',
'.woocommerce-cart-form > table'
];
for( let t = 0; t < tableSelectors.length; t++ ){
let tables = document.querySelectorAll( tableSelectors[t] );
if( tables ){
for( let i = 0; i < tables.length; i++ ){
tables[i].classList.add( 'table' );
}
}
}
console.dir("ok");
}catch(ex){console.dir(ex);}
});
or use a jquery like solution like this:
jQuery( document ).ajaxComplete(function( event, xhr, settings ) {
jQuery('.some-class table, .woocommerce-product-attributes, .woocommerce-cart-form > table').addClass("table");
});
i've tested both of them directly on your site and both solutions works pretty well
and of course if you want make it works even at start the same script must be applied to jQuery( document ).ready() too:
jQuery( document ).ready(function() {
jQuery('.some-class table, .woocommerce-product-attributes, .woocommerce-cart-form > table').addClass("table");
});

Related

How to wrap a container div around a Ckeditor table onOk

When a user enters a table on the Ckeditor, I want to wrap a div around it with a class but I can't find a way to get this table HTML element. What is the best way to go about it?
I've tried creating a plugin to extend the table dialog onOk function (see code). This gives me all the properties from the table dialog but I don't want to have to create the whole table element again with all the properties as I don't want to re-write the existing table plugin.
I just need to get the code this plugin adds and wrap it in a div.
I thought about doing it in my projects javascript, when page loads, get all tables and wrap it in a div. However, this doesn't seem like the best way to do it at all. I thought there must be a way via ckeditor?
CKEDITOR.plugins.add( 'responsivetables', {
// The plugin initialization logic
init: function(editor) {
vsAddResponsiveTables(editor);
}
});
function vsAddResponsiveTables(editor){
CKEDITOR.on( 'dialogDefinition', function( ev ) {
var dialogName = ev.data.name;
var dialogDefinition = ev.data.definition;
if ( dialogName == 'table') {
addTableHandler(dialogDefinition, editor);
}
});
}
function addTableHandler(dialogDefinition, editor){
dialogDefinition.onOk = function (a) {
// get table element and wrap in div?
}
}
I found the answer so for anyone else that needs it, this is what I did:
I used the insertElement event instead of when dialog was closed, only doing what I need if its a table that's being added.
// Register the plugin within the editor.
CKEDITOR.plugins.add( 'responsivetables', {
// The plugin initialization logic goes inside this method.
init: function(editor) {
vsAddResponsiveTables(editor);
}
});
function vsAddResponsiveTables(editor){
// React to the insertElement event.
editor.on('insertElement', function(event) {
if (event.data.getName() != 'table') {
return;
}
// Create a new div element to use as a wrapper.
var div = new CKEDITOR.dom.element('div').addClass('table-scroll');
// Append the original element to the new wrapper.
event.data.appendTo(div);
// Replace the original element with the wrapper.
event.data = div;
}, null, null, 1);
}
To the previous answer by 'gemmalouise' need to add one more line of code
CKEDITOR.editorConfig = function( config ) {
config.extraPlugins = 'responsivetables';
}
Otherwise it will not work (I cannot indicate this in the comment, because lack of 50 reputation).
And more compact code of this fuctional:
CKEDITOR.plugins.add('responsivetables', {
init: function (editor) {
editor.on('insertElement', function (event) {
if (event.data.getName() === 'table') {
var div = new CKEDITOR.dom.element('div').addClass('table-scroll'); // Create a new div element to use as a wrapper.
div.append(event.data); // Append the original element to the new wrapper.
event.data = div; // Replace the original element with the wrapper.
}
}, null, null, 1);
}
});

JQuery/Backbone remove not working in Jasmine

I'm testing a Backbone View in Jasmine. When I call the view's remove method, the element isn't actually removed.
I have this event handler in my view:
onModelChange: function() {
this.$el.html('');
this.render();
}
I have to have it written that way because manually setting the html is the only way to remove it. Calling remove doesn't do anything, and when the view renders itself again it just renders the new content appended to the old content. I even tried calling remove from the developer tools in Chromium but that didn't work either. However, remove does work when I manually test it in the browser, but it doesn't work in Jasmine and it's screwing up my tests.
I think the answer to the problem lies in the jQuery source:
remove: function( selector, keepData /* Internal Use Only */ ) {
var elem,
elems = selector ? jQuery.filter( selector, this ) : this,
i = 0;
for ( ; (elem = elems[i]) != null; i++ ) {
if ( !keepData && elem.nodeType === 1 ) {
jQuery.cleanData( getAll( elem ) );
}
if ( elem.parentNode ) {
if ( keepData && jQuery.contains( elem.ownerDocument, elem ) ) {
setGlobalEval( getAll( elem, "script" ) );
}
elem.parentNode.removeChild( elem ); // right here to be specific
}
}
return this;
},
The remove function is relying on the node's parent to do the removing. I'm guessing that when the tests run in karma, the backbone view's node has no parent. To explore a bit more, I debugged the test. In the console, if I query for a view element's child and I remove it, it works.

Why isn't my data been added to the stash in mojolicious?

I'm having an issue with Mojolicious and the stash and I think I'm probably just not understanding the way it works?
I have a page with 2 combo boxes and when the first entry changes I wish to update the options in the second.
So I add an event handler like below, which then calls my controller sub routine 'devicecommandset' and then puts the result of a DBIx query into an array of hashes which I add to my stash.
I am then just render some benign text. My subroutine gets called and there is the expected contents in '#commandsets'. However I cannot see it in the stash on the browsers console ( I'm running in debug mode ).
Do I need to actually modify the DOM for the stash to be populated? Basically I'm just trying to get data back from my request to fill the combobox options.
In my template
$(document).ready(function() {
$('select:not([name*="command"])').live('change', function (e) {
$.get('devicecommandset', { device: $(this).attr("value") },
function (data) {
alert("Made it this far");
});
});
});
In my Controller
sub devicecommandset {
my $self = shift;
my $device = $self->param('device') || '';
my #commandsets = $self->db->resultset('CommandSet')->search_commandsets_by_devicename($device);
$self->stash(commandsets => \#commandsets );
print Dumper(#commandsets);
$self->render(text => 'success' );
}
You're printing a dumper to the log basically, not the browser. Your stash is not used in the render because you're not referencing it. Use inline render type and the "dumper" helper.
Try:
$self->stash(commandsets => \#commandsets );
$self->render( inline => '<%= dumper $commandsets %>' );

Dynamically adding script to a div does not execute the script

PROBLEM:
Why does this not show the alert? And how can I make it so?
<script>
function onSuccess() {
var response= "<script> alert(1);</\script>";
document.getElementById("xxx").innerHTML = response;
}
</script>
<div id="xxx">existing text</div>
<button id="click" onclick="onSuccess();">click</button>
http://jsfiddle.net/7hWRR/1/
This is just a simplified version of my problem. In our application (in one very old module in particular) we use an ancient home-grown AJAX class which just innerHTMLs all AJAX responses.Traditionally we have only sent back HTML as AJAX response but I would like to execute JS in the success handler.I do not have access to the JS file so cannot modify the way the response is handled. I can only work with the fact that the success handler calls div.innerHTML='<my response>'
So stupid as it may be, I'm hoping for some help using these constraints!
SIMILAR LINKS:
Dynamically adding script element to a div does not execute the script
Dynamically added script will not execute
Caveat: Here I'm assuming the <div> on which the results are inserted is known.
A possible solution is to use a MutationObserver (and the DOMNodeInserted event, to support IE 9 and 10) to watch said <div> for changes on its contents, and execute the code on any inserted <script> tags.
Example built upon your jsFiddle:
watchNodeForScripts(document.getElementById("xxx"));
function watchNodeForScripts(scriptRecipient) {
if ('MutationObserver' in window) {
// Prefer MutationObserver: https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
watchUsingMutationObserver();
} else {
// Fallback to Mutation Events: https://developer.mozilla.org/en-US/docs/Web/Guide/API/DOM/Events/Mutation_events
watchUsingDeprecatedMutationEvents();
}
function watchUsingMutationObserver() {
var observer = new MutationObserver(function (mutations) {
mutations.forEach(function (mutation) {
var i, addedNodes = mutation.addedNodes;
for (i = 0; i < addedNodes.length; i++) {
handleAddedNode(addedNodes[i]);
}
});
});
observer.observe(scriptRecipient, {
childList: true
});
}
function watchUsingDeprecatedMutationEvents() {
scriptRecipient.addEventListener("DOMNodeInserted", function (event) {
handleAddedNode(event.target);
});
}
function handleAddedNode(node) {
// Don't try to execute non-script elements
if (!(node instanceof HTMLScriptElement)) return;
// Don't try to execute linked scripts
if (node.src !== "") return;
// Use 'new Function' instead of eval to avoid
// the creation of a (undesired) closure
fn = new Function(node.textContent);
fn.call(window);
}
}
Updated fiddle: http://jsfiddle.net/7hWRR/13/
Edit: Changed innerText to the more cross-compatible textContent.
Edit2: Don't execute code that isn't inside a <script> element.
Edit3: Don't execute scripts with the src attribute, and add mutation events fallback

Detecting when jasmine tests complete

I am running jasmine tests like this;
jasmine.getEnv().addReporter(new jasmine.TrivialReporter());
jasmine.getEnv().execute();
I would like to detect, using JavaScript, when the tests complete. How can I?
As #Xv. suggests, adding a reporter will work. You can do something as simple as:
jasmine.getEnv().addReporter({
jasmineDone: function () {
// the specs have finished!
}
});
See http://jasmine.github.io/2.2/custom_reporter.html.
Some alternative ways:
A) Use the ConsoleRunner, that accepts an onComplete option. Older versions (1.2rc1) receive the complete callback as a standalone parameter.
Since you also supply the function that writes (options.print) you keep control about having the test reports written to the console.
You can have several reporters active at the same time jasmineEnv.addReporter().
B) Haven't tried, but you could create your own reporter, with empty implementations of every public method but jasmineDone()
C) Check an old post in the Jasmine google group, where the author saves and overrides jasmine.getEnv().currentRunner().finishCallback:
var oldCallback = jasmineEnv.currentRunner().finishCallback;
jasmineEnv.currentRunner().finishCallback = function () {
oldCallback.apply(this, arguments);
$("body").append( "<div id='_test_complete_signal_'></div" );
};
jasmineEnv.execute();
I found two different ways to solve this issue. One is to hack jasmine to throw a custom event when it completes. Because I wanted to screen scrape after the test loaded, I inserted the event trigger into jasmine-html.js at the end of "reportRunnerResults"
$( 'body' ).trigger( "jasmine:complete" );
Then it's a matter of listening for the event:
$( 'body' ).bind("jasmine:complete", function(e) { ... }
In my case, I was running jasmine in an iFrame and wanted to pass the results to a parent window, so I trigger an event in the parent from my first bind:
$(window.parent).find('body').trigger("jasmine:complete");
It is also possible to do this without jquery. My strategy was to poll for text to be added to the "finished-at" span. In this example I poll every .5 seconds for 8 seconds.
var counter = 0;
function checkdone() {
if ( $('#test-frame' ).contents().find('span.finished-at').text().length > 0) {
...
clearInterval(timer);
} else {
counter += 500;
if (counter > 8000) {
...
clearInterval(timer);
}
}
}
var timer = setInterval( "checkdone()", 500 );
I'm running Jasmine 1.3.1 with the HtmlReporter. I ended up hooking in like this:
var orig_done = jasmineEnv.currentRunner_.finishCallback;
jasmineEnv.currentRunner_.finishCallback = function() {
orig_done.call(this);
// custom code here
};

Categories

Resources