Unable to update Materialize.css tooltips using jQuery after click event - javascript

I am using Materialize.css on my portfolio site, and am using the npm package clipboard.js. I am using this within a floating action button, and the copy to clipboard functionality is working as intended (when user clicks the button, it copies my email to their clipboard like it should).
However, I want the tooltip to update from ""Click to copy my email to your clipboard!" to a success message like "Copied to your clipboard ✅". I have tried the code below and it won't actually update the page, though I can sometimes see the new message (it's just very inconsistent, which I don't want).
This is my html element:
<li>
<a id="email" data-clipboard-text="example#gmail.com" class="btn-floating red waves-effect waves-light tooltipped" data-position="left" data-tooltip="Click to copy my email to your clipboard!"><i class="material-icons">mail</i></a>
</li>
and here's my javascript:
var clipboard = new ClipboardJS('#email');
clipboard.on('success', function(e) {
var anchorElement = $('#email');
anchorElement.attr('data-tooltip', 'Copied to your clipboard ✅');
anchorElement.addClass('success');
anchorElement.tooltip();
// Reset after a timeout
anchorElement.mouseleave(function() {
setTimeout( function(){
anchorElement.attr('data-tooltip', 'Click to copy my email address to your clipboard!');
anchorElement.removeClass('success');
anchorElement.tooltip();
}, 300);
});
e.clearSelection();
});
I would like for the tooltip to show the updated value consistently, but I haven't been able to figure out what's wrong with the code I have. I can tell that it does update the html element, as I can sometimes see the updated text, but it's very inconsistent and for this feature to be worth using at all I need it to be very consistent.
Any help would be greatly appreciated!

The problem you're running into is that materialize.css adds in other DOM elements that eventually appear on screen as the tooltips themselves - elements that you aren't targeting right now - elements that you didn't write in your HTML. You're targeting the original data-attributes that materialize.css uses to create those other elements. Updating those data-attributes after render is too little too late... by then, the materialize.css library has already looked at those attributes and gotten what it needs.
If you look at the official docs page on tooltips, we can investigate a little on that page. Open your dev console and scroll towards the bottom you'll see four DOM elements with the class material-tooltip - these were added by the library, and these are the DOM elements that actually get shown on screen.
Open those divs up and watch what happens to them when you mouse over the Bottom, Top, Left, Right buttons on the screen. As each tooltip appears, you should notice that the message-to-be-displayed gets injected as text into that element, and it animates some CSS properties.
If you want to change the text being displayed, you can probably skip editing the data-attributes (though if the library occasionally refreshes the content, changing the attributes might still be a good idea)... instead, we need to edit the text being shown in ^^THESE^^ elements.
If you only have one tooltip being displayed on that page, it should be as simple as something like this:
$('.material-tooltip').innerText = 'Copied to your clipboard ✅'
or if you have multiple on that page, you can attempt to identify which div is for which tooltip, but I suspect that could end up being unreliable. It would probably be safer to update ALL of the divs... the library will overwrite the content anyways next time it renders... which again, it gets from your data-attributes. Updating all of them would look something like:
$('.material-tooltip').each( eachDiv => {
eachDiv.innerText = 'Copied to your clipboard ✅'
})

here's only solution I found that works for me .. disclaimer - it's a bit hacky, basically it destroys tooltip on click and rebuilds it with new attr, then after a timeOut resets it back.
$('#shareLink').on('click', function (e) {
$('#shareLink').tooltip('dispose').removeClass('material-tooltip').attr('title', 'Link copied!');
$('#shareLink').addClass('material-tooltip').tooltip('show');
setTimeout( function () {
$('#shareLink').tooltip('dispose').removeClass('material-tooltip').attr('title', 'Copy link to Clipboard');
$('#shareLink').addClass('material-tooltip').tooltip('enable');
}, 2000);
});

Related

using tooltipster to display tool tips as part of innerHTML for a div

I am using tooltipster to generate tool tips. All works fine except in the situation where I need to set the contents of a div based on user input using a simple JavaScript function. The div contents consists of images, when hovering over each image, a tool tip should display. However, the tip displays as the default browser behaves for displaying title= with an image. The JavaScript I use is simple:
function setAwards() {
var awardsdiv=document.getElementById("awards"); awardsdiv.innerHTML="";
if (document.setvalues.superstar.checked == true) awardsdiv.innerHTML=awardsdiv.innerHTML + "<img class=\"tooltip\" title=\"Description of award\" width=\"16\" src=\"/pix/superstar.png\" alt=\"[ Super Star ]\" />";
[... stuff removed ...]
}
Is there a way to make this work? Tool tips do display elsewhere on this web page, so the resources needed appear to be set up correctly.
Thank you!
You must initialize the tooltip ($(...).tooltipster({...})) after you have inserted your new HTML content or use delegation.
For "automatic" initialization, you might want to use delegated event listeners for that, see https://github.com/iamceege/tooltipster/issues/499

jQuery UI Dialog: Dialog Element Disappearing from the DOM

I'm working on a project and I am attempting to create a modal dialog "pop-up" to capture data in a form. I haven't worked with jQuery UI's Dialog widget previously, but I've worked with others and it seemed straight forward.
I created the following very simple code snippet to test as I went along:
<div class="app-email">
<div>
<a href="#"
class="app-email-opener">
Click to add or edit your e-mail settings.
</a>
</div>
<div class="app-email-modal">
Oh, Hai.
</div>
</div>
$('.content').on({
click: function () {
console.log('I was totes clicked.');
var parent = $(this).parents('.app-email');
console.log(parent);
var target = parent.find('.app-email-modal');
console.log(target);
$(target).dialog('open');
}
}, '.app-email-opener');
$('.app-email-modal').dialog({
autoOpen: false,
modal: true,
show: false
});
For reference: the class 'content' is a higher level block to catch delegated events without having to go all the way up the DOM.
The issue I'm running into is that the div with class="app-email-modal" seems to flash onto the page and then disappear from the DOM completely. jQuery, therefore, isn't able to find it and do anything because at that point it simply doesn't exist.
The overall project is in ASP.NET MVC 4, using Visual Studio 2013.
Any information would be greatly appreciated.
So, finally discovered what's happening via this previously answered question:
Jquery Dialog - div disappears after initialization
//EDIT
For any possible future usefuless -
What was happening was that jQuery UI will move any DOM elements specified as Dialogs to the bottom of the page, rather than keep them in the location specified in the HTML markup. So, in my case, I was looking for things by class, but only within the scope of the app-email-openers parent app-email div.
To remedy this, I used templating (in my case, Razor) to add unique ids to each app-email-modal div, and added a data- attribute to associate the link with the specific unique id. This way they jQuery UI can move the elements as it sees fit, but there still easily accessible.
//END EDIT
I feel like that functionality should be better spelled out in the documentation. Even their own example doesn't operate like this.
Corollary: I attempted to use the appendTo option to have the DOM elements not be shifted to the bottom of the page, but they're still moved to the bottom. So, there's that.

Expand/Collapse Text

The code below works fine with ONE Reveal/Hide Text process
<div class="reveal">Click Here to READ MORE...</div>
<div style="display:none;">
<div class="collapse" style="display:none;">Collapse Text</div>
However if this code is duplicated multiple times, the Collapse Text shows up and doesn't disappear and in fact conflicts with the Expand to reveal even more text instead of collapsing as it should.
In this http://jsfiddle.net/syEM3/4/ click on any of the Click Here to READ MORE...
Notice how the Collapse Text shows up at the bottom of the paragraphs and doesn't disappear. Click on the Collapse and it reveal more text.
How do I prevent this and getting to work as it should?
The two slideDown function calls are not specific to the .reveal and/or .collapse that you are currently doing. i.e.
$(".collapse").slideDown(100);
will find all the elements with the class .collapse on the page, and slide them down. irrespective of what element you just clicked.
I would change the slideDown call to be relavant to the element you just clicked i.e. something like this
$('.reveal').click(function() {
$(this).slideUp(100);
$(this).next().slideToggle();
$(this).next().next(".collapse").slideToggle(100);
});
in your code
$('.reveal').click(function() {
$(this).slideUp(100);
$(this).next().slideToggle();
$(".collapse").slideDown(100);
});
$('.collapse').click(function() {
$(this).slideUp(100);
$(this).prev().slideToggle();
$(".reveal").slideDown(100);
});
this two rows doesn’t do what you want as they act on all elements of the specified class
$(".reveal").slideDown(100);
$(".collapse").slideDown(100);
When you do $(".collapse").slideDown(100);, jQuery runs slideDown on everything with the .collapse class, not just the one that's related to your current this. To fix this, refer to the collapse based on its location to $(this).
Do do this, use something like $(this).siblings(".collapse").slideDown(100);
Note that this particular selector will only work if you enclose each text block in its own div. With each text element in its own div, like you have it now, .siblings(".collapse"), which selects all the siblings of $(this) with the collapse class, will still select both of the collapse elements.
Okay, I think you should take a different approach to your problem.
See, jQuery basically has two purposes:
Selecting one or more DOM elements from your HTML page
manipulate the selected elements in some way
This can be repeated multiple times, since jQuery functions are chainable (this means you can call function after function after function...).
If I understood your problem correctly, you are trying to build a list of blog posts and only display teasers of them.
After the user clicks the "read more" button, the complete article gets expanded.
Keep in mind: jQuery selects your elements very much like CSS would do. This makes it extremely easy to
come up with a query for certain elements, but you need to structure your HTML in a good way, like
you would do for formatting reasons.
So I suggest you should use this basic markup for each of your articles (heads up, HTML5 at work!):
<article class="article">
<section class="teaser">
Hey, I am a incredible teaser text! I just introduce you to the article.
</section>
<section class="full">
I am the articles body text. You should not see me initially.
</section>
</article>
You can replace the article and section elements with div elements if you like to.
And here is the CSS for this markup:
/* In case you want to display multiple articles underneath, separate them a bit */
.article{
margin-bottom: 50px;
}
/* we want the teaser to stand out a bit, so we format it bold */
.teaser{
font-weight: bold;
}
/* The article body should be a bit separated from the teaser */
.full{
padding-top: 10px;
}
/* This class is used to hide elements */
.hidden{
display: none;
}
The way we created the markup and CSS allows us to put multiple articles underneath.
Okay, you may have noticed: I completely omitted any "read more" or "collapse" buttons. This is done by intention.
If somebody visits the blog site with javascript disabled (maybe a search engine, or a old mobile which doesn't support JS or whatever),
the logic would be broken. Also, many text-snippets like "read more" and "collapse" are not relevant if they don't actually do anything and are not part of the article.
Initially, no article body is hidden, since we didn't apply the hidden css class anywhere. If we would
have embedded it in the HTML and someone really has no JavaScript, he would be unable to read anything.
Adding some jQuery magic
At the bottom of the page, we are embedding the jQuery library from the google CDN.
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
This is a best practice and will normally speed up your page loading time. Since MANY websites are embedding
jQuery through this URL, chances are high that its already in the visitors browser cache and doesn't have
to be downloaded another time.
Notice that the http: at the beginning of the URL is omitted. This causes browsers to use the pages current protocol,
may it be http or https. If you would try and embed the jQuery lib via http protocol on a https website, some browsers will refuse to download the file from a unsecure connection.
After you included jQuery into the page, we are going to add our logic into a script tag. Normally we would
save the logic into a separate file (again caching and what not all), but this time a script block will do fine.
Finally some JavaScript
At first, we want to hide all elements with the css-class full, since only teasers should remain displayed. This is very easy with jQuery:
$('.full').hide();
The beginning of the script $('.full') tells jQuery: I need all elements with the CSS-class full. Then we call a function on that result, namingly hide() which purpose should be clear.
Okay, in the next step, we want to add some "read more" buttons, next to every teaser. Thats an easy task, too:
$('.teaser').after('<button class="more">Read more</button>');
We now select every element with the css-class teaser and append some HTML code after() each element - a button with the css-class more.
In the next step, we tell jQuery to observe clicks on every one of this freshly created buttons. When a user has clicked, we want to expand the next element with the css-class full after the clicked button.
$('.more').on('click', function(){
//"this" is a reference to the button element!
$(this).slideUp().next('.full').slideDown();
});
Phew, what did we do here?
First, we told jQuery that we wanted to manipulate this, which is a reference to the clicked button. Then we told
jQuery to hide that button (since its not needed anymore) slowly with slideUp().
We immediately continued telling jQuery what to do: Now take the next() element (with the css-class full) and make it visible by sliding it down with slideDown().
Thats the power of jQuerys chaining!
Hiding again
But wait, you wanted to be able to collapse the articles again! So we need a "collapse" button, too and
some more JavaScript:
$('.full').append('<button class="collapse">Collapse text</button>');
Note: we didn't use the after() function to add this button, but the append() function to place the button
INSIDE every element with the css-class full, rather than next to it. This is because we want the
collapse buttons to be hidden with the full texts, too.
Now we need to have some action when the user clicks one of those buttons, too:
$('.collapse').on('click', function(){
$(this).parent().slideUp().prev('.more').slideDown();
});
Now, this was easy: We start with the button element, move the focus to its parent() (which is the element that contains the full text) and tell jQuery to hide that element by sliding it up with slideUp().
Then we move the focus from the full-text container to its previous element with the css-class more, which is its expanding button that has been hidden when expanding the text. We slowly show that button again by calling slideDown().
Thats it :)
I've uploaded my example on jsBin.

Completely Reload TinyMCE

I have been writing a CMS for a while now and am currently putting the last few touches on it. one of which includes using ajax to deliver a tinyMCE editor in a lightbox styled window.
when it is loaded, the first time it works perfectly, but when i do it the second time or more, the element names get messed up and it doesn't send data back, or display the current value in the TinyMCE window. When I use Chrome to inspect the element, I can see that the span that contains the previous tinyMCE window is still there.
I use document.body.removeChild to remove the div that is holding it. Does anyone have any ideas?
Addition:
when AJAX gets back from making the request (it has all the html code of what goes in the window), it creates a new div element and uses document.body.appendChild to add the element to the document and puts the html code into the div tag.
Then it travels through the new code and searches for the scripts in that area (of which one is the MCE initiator) and appends them to the head so they are executed.
if the user clicks cancel rather than save, it removes the div tag by using:
document.body.removeChild(document.getElementById("popupbox"));
which works fine,
however when i bring up popup and repopulate as said before, and inspect the elements there, there is still a span there which was created by TinyMCE and the naming has been changed (instead of being identified by 'content', it is now 8 for some reason) and there is no content in the editor region.
I have used:
tinyMCE.execCommand('mceRemoveControl',true,'mce{$Setting['name']}');
tinyMCE.editors[0].destroy();
but neither of them work. They return the tinymce window to a textarea, but the elements are still there.
Removing the editor as you described (using the correct tinymce editor id) before moving or removing the html element holding the tinymce iframe is a good choice. Where do you load your tinymce.js? If you deliver it using ajax i think it might be better to load it on the parent page(!). Some more code would be helpfull.
EDIT: I remember a situation where i had to remove a leftover span. Here is my code for this:
// remove leftover span
$('div .mceEditor').each(function(item){
if (typeof $(this).attr('style') !== "undefined" ){
$(this).removeAttr('style'); // entfernt "style: none";
}
else {
$(this).remove();
}
});

Is it possible to make a change with jQuery and then immediately reverse that change?

I have a pretty specific scenario where I would like to select all elements with jQuery, make a CSS change, save the elements, then reverse the change I made.
The Goal
I created a jQuery plugin called jQuery.sendFeedback. This plugin allows the user to highlight areas of the screen, as shown in this demo. When they submit their feedback the plugin grabs all the HTML on the page and dumps it into a callback function. Like so:
$('*').each(function ()
{
$(this).width($(this).width());
$(this).height($(this).height());
});
var feedbackInformation = {
subject: $feedbackSubject.val(),
details: $feedbackDetails.val(),
html: '<html>' + $('html').html() + '</html>'
};
if (settings.feedbackSent)
settings.feedbackSent(feedbackInformation);
The callback function accepts this feedback information and makes an AJAX call to store the page HTML on the server (this HTML includes the red box highlights the user drew on the screen). When someone from tech support needs to view the user's "screen shot" they navigate to a page that serves up the stored HTML so the developer can see where the user drew their highlights on the screen.
My original problem was that different screen resolutions made the elements different sizes and the red highlights would highlight the wrong areas as the screen changed. This was fixed pretty easily by selecting all elements on the page and manually setting their height and width to their current height and width when the user takes the snap shot. This makes all the element sizes static, which is perfect.
$('*').each(function ()
{
$(this).width($(this).width());
$(this).height($(this).height());
});
The Problem
The issue with this is that when the plugin is done transmitting this HTML the page currently being viewed now has static heights and widths on every element. This prevents dropdown menus and some other things from operating as they should. I cannot think of an easy way to reverse the change I made to the DOM without refreshing the page (which may very well end up being my only option). I'd prefer not to refresh the page.
Attempted Solution
What I need is a way to manipulate the HTML that I'm sending to the server, but not the DOM. I tried to change the above code to pull out the HTML first, then do the operation on the string containing the HTML (thus not affecting the DOM), but I'm not quite sure what I'm doing here.
var html = '<html>' + $('html').html() + '</html>';
$('*', html).each(function ()
{
$(this).width($(this).width());
$(this).height($(this).height());
});
This did not work. So either I need to be able to manipulate the string of HTML or I need to be able to manipulate the DOM and undo the manipulation afterward. I'm not quite sure what to do here.
Update
I employed the solution that I posted below it is working beautifully now. Now I am wondering if there is a way to statically write all the css for each element to the element, eliminating the need for style sheets to be referenced.
I think you are mostly on the right track by trying to make the modifications to the HTML as a string rather than on the current page for the user.
If you check this post, you might also want to follow the recommendation of creating a temporary <div> on the page, cloning your intended content to the new <div> ensuring it is invisible using "display:none." By also putting a custom Id on the new <div> you can safely apply your static sizing CSS to those elements using more careful selectors. Once you have sent the content to the server, you can blow away the new <div> completely.
Maybe?
After much pain and suffering I figured a crude but effective method for reverting my modifications to the DOM. Though I hadn't gotten around to trying #fdfrye's suggestion of cloning, I will be trying that next to see if there is a mroe elegant solution. In the meantime, here is the new code in case anyone else can benefit from it:
$('*').each(function () {
if ($(this).attr('style'))
$(this).data('oldStyle', $(this).attr('style'));
else
$(this).data('oldStyle', 'none');
$(this).width($(this).width());
$(this).height($(this).height());
});
var html = '<html>' + $('html').html() + '</html>';
$('*').each(function () {
if ($(this).data('oldStyle') != 'none')
$(this).attr('style', $(this).data('oldStyle'));
else
$(this).removeAttr('style');
});
When I'm looping through every element and modifying the css, I log the original value onto the element as data. After I assign the DOM HTML to a variable I then loop through all elements again and restore the style attribute to its original value. If there was no style attribute then I log 'none' to the element data and then remove the style attribute entirely when looping through again.
This is more performance heavy than I wish it was since it loops through all elements twice; it takes a few seconds to finish. Not horrible but it seems like a little much for such a small task. Anyway, it works. I get a string with fixed-sized HTML elements and the DOM goes back to normal as if the plugin never touched it.

Categories