Is there a way I can wrap an external JS script embed with lazy-load behavior to only execute when the embed is in the viewport?
Context: I have an external javascript embed that when run, generates an iframe with a scheduling widget. Works pretty well, except that when the script executes, it steals focus and scrolls you down to the widget when it’s done executing. The vendor has been looking at a fix for a couple weeks, but it’s messing up my pages. I otherwise like the vendor.
Javascript embed call:
<a href=https://10to8.com/book/zgdmlguizqqyrsxvzo/ id="TTE-871dab0c-4011-4293-bee3-7aabab857cfd" target="_blank">See
Online Booking Page</a>
<script src=https://d3saea0ftg7bjt.cloudfront.net/embed/js/embed.min.js> </script> <script>
window.TTE.init({
targetDivId: "TTE-871dab0c-4011-4293-bee3-7aabab857cfd",
uuid: "871dab0c-4011-4293-bee3-7aabab857cfd",
service: 1158717
});
</script>
While I'm waiting for the vendor to fix their js, I wondered if lazy-loading the JS embed may practically eliminate the poor user experience. Warning: I'm a JS/webdev noob, so probably can't do anything complicated. A timer-based workaround is not ideal because users may still be looking at other parts of the page when the timer runs out. Here are the things I’ve tried and what happens:
I tried:
What happened:
Add async to one or both of the script declarations above
Either only shows the link or keeps stealing focus.
Adding type=”module” to one or both script declarations above
Only rendered the link.
Wrapping the above code in an iframe with the appropriate lazy-loading tags
When I tried, it rendered a blank space.
Also, I realize it's basically the same question as this, but it didn't get any workable answers.
I actually also speak french but I'll reply in english for everybody.
Your question was quite interesting because I also wanted to try out some lazy loading so I had a play on Codepen with your example (using your booking id).
I used the appear.js library because I didn't really want to spend time trying some other APIs (perhaps lighter so to take in consideration).
The main JS part I wrote is like this:
// The code to init the appear.js lib and add our logic for the booking links.
(function(){
// Perhaps these constants could be put in the generated HTML. I don't really know
// where they come from but they seem to be related to an account.
const VENDOR_LIB_SRC = "https://d3saea0ftg7bjt.cloudfront.net/embed/js/embed.min.js";
const UUID = "871dab0c-4011-4293-bee3-7aabab857cfd";
const SERVICE = 1158717;
let vendorLibLoaded = false; // Just to avoid loading several times the vendor's lib.
appear({
elements: function() {
return document.querySelectorAll('a.booking-link');
},
appear: function(bookingLink) {
console.log('booking link is visible', bookingLink);
/**
* A function which we'll be able to execute once the vendor's
* script has been loaded or later when we see other booking links
* in the page.
*/
function initBookingLink(bookingLink) {
window.TTE.init({
targetDivId: bookingLink.getAttribute('id'),
uuid: UUID,
service: SERVICE
});
}
if (!vendorLibLoaded) {
// Load the vendor's JS and once it's loaded then init the link.
let script = document.createElement('script');
script.onload = function() {
vendorLibLoaded = true;
initBookingLink(bookingLink);
};
script.src = VENDOR_LIB_SRC;
document.head.appendChild(script);
} else {
initBookingLink(bookingLink);
}
},
reappear: false
});
})();
I let you try my codepen here: https://codepen.io/patacra/pen/gOmaKev?editors=1111
Tell me when to delete it if it contains sensitive data!
Kind regards,
Patrick
This method will Lazy Load HTML Elements only when it is visible to User, If the Element is not scrolled into viewport it will not be loaded, it works like Lazy Loading an Image.
Add LazyHTML script to Head.
<script async src="https://cdn.jsdelivr.net/npm/lazyhtml#1.0.0/dist/lazyhtml.min.js" crossorigin="anonymous" debug></script>
Wrap Element in LazyHTML Wrapper.
<div class="lazyhtml" data-lazyhtml onvisible>
<script type="text/lazyhtml">
<!--
<a href=https://10to8.com/book/zgdmlguizqqyrsxvzo/ id="TTE-871dab0c-4011-4293-bee3-7aabab857cfd" target="_blank">See
Online Booking Page</a>
<script src=https://d3saea0ftg7bjt.cloudfront.net/embed/js/embed.min.js>
</script>
<script>
window.TTE.init({
targetDivId: "TTE-871dab0c-4011-4293-bee3-7aabab857cfd",
uuid: "871dab0c-4011-4293-bee3-7aabab857cfd",
service: 1158717
});
</script>
-->
</script>
</div>
Related
I am working on a web widget that can be embedded on 3rd party websites.
Since a lot of content management systems do not allow users to post/execute scripts, I want my widget to show an image instead of JS-generated content if such situation occurs.
<script type="text/javascript">
(function(){var s = document.createElement('script');s.src = '//example.com/file.js';s.async = "async";document.body.appendChild(s);}());
</script>
<img src="//example.com/image.svg?param1=value1" src="" id="my_fallback">
For now I am using the code above. Is there any way to show the image only if the script did not load? The goal is to reduce transfer usage and provide better user experience.
The first line of my widget script is removing #my_fallback, but it is not fast enough - sometimes I can see the image for a second before the actual widget content replaces it.
The only thing I came up with is to delay creation of the image by including something like sleep() in the beginning of my image generator.
EDIT
No, <noscript> won't work here. I do not want to fallback if user has disabled javascript. I want to fallback when a script has not loaded - for any reason, especially if some security mechanism cut off the <script> section.
Use html tag Noscript
<noscript>Your browser does not support JavaScript! or a image here</noscript>
Remember
In HTML 4.01, the tag can only be used inside the element.
In HTML5, the tag can be used both inside and .
Edit : -
add one html tag
<span class="noscript">script is loading.....or put image</span>
inside your script tag
now in your scripts which has to be load add one code like
add this line at the end
$('.noscript').hide();
This is the other way which you can handle the same!
One quick fix is to create a global variable from that script, visible to the window object.Also the image must be hidden. Then, on a main.js script check for that variable. If it exists then run your widget code from there. If it doesnt exist then fadeIn the fallback image.
Heres a demo
The default img is an image 272x178 size and the widget image is an image 300x400 size.
To simulate the action when the script is unavailable, just name the variable myWidgetIsEnabled with a different name so the condition fails.
Here is some code:
// Code goes here
var widget = (function(){
window.myWidgetIsEnabled = true;
return {
init: function(){
var s = document.createElement('script');s.src = 'file.js';s.async = "async";
document.body.appendChild(s);}
}
}());
$(document).ready(function(){
if(window.myWidgetIsEnabled){
widget.init();
}else{
console.log('not enabled, the default behavior');
$('.fallback').fadeIn();
}
})
I apologize for the possibly naive nature of this question but I am not a web developer by day.
Is it possible to write a script such that, for any arbitrary web page, a function that I have written will be called if a URL is moused over? I was initially thinking that I could use document.links to assemble an array of all of the hrefs in a document and add an onmouseover event attribute to each of them but, unless I'm mistaken, that would overwrite any existing onmouseover attributes already present in the page. Not ideal.
I'm not sure if by arbitrary web page you mean any pages on any domains or any pages of your own domain, but for the latter you could put something like the following in your pages:
$(function () {
$(document).on('mouseenter', 'a', function () {
console.log(this, 'hovered');
});
});
If you mean any page your browse to on the net, then you will have to write a browser extension for the browser your are using. For Chrome have a look at this.
You could try getting everything with the a tag and inject an onmouseover.
window.onload = function(){
for(m=0;m<document.getElementsByTagName('a');m++){
if(document.getElementsByTagName('a')[m].className == 'someclass'){
document.getElementsByTagName('a')[m].onmouseover = function(){
Your Code
}
}
}
}
I'm looking for best practices for using javascript/jQuery snippets in an asp.net project. I know that it is best to put all the scripts in a separate file rather than inline. That's good. It is easy to move these script functions to a common file (may be a couple of different ones to even out the performance of loading a single large file for small functions).
But there is some jQuery stuff that needs to happen on document.Ready on each page. How will I move this to a common .js file? I would like to avoid one script per page as it would be just too many.
For example, say Page1 has a need to manipulate a few radio buttons on load and has the following script inline. (just for illustration)
<script>
$(document).ready(function() {
//check checkboxes
if(true)
call function1();
});
</script>
Same with Page2 but for some other condition, calling different function function2.
I can move the function1 and function2 to a common .js file but how about the document ready sections. Should that stay inline? I assume so because otherwise I'm not sure how the common.js will differentiate between document.ready for different pages.
Then does it defeat the purpose of not having inline javascript? If anyone can throw some light into this, it is much appreciated.
I did some research, but probably due to incorrect keywords, so far I haven't been able to find any good information along the same lines. Unobtrusive JavaScript seems promising in the comments below.
You should specify what behaviors should exist within the HTML using data-* attributes.
You can then use a single universal piece of Javascript code to read these attributes and apply behaviors.
For example:
<div data-fancy-trick="trick-3">...</div>
In the JS file, you can write something like
$('[data-fancy-trick]'.each(function() {
var trickName = $(this).data('fancy-trick');
switch (trickName) {
...
}
});
For real-life examples of this technique, look at Bootstrap's Javascript components.
You can simply have separate js files per page and include them in relevant pages. For shared script code, have a common js file. Following your example:
common.js
var myCommonVar = {};
function myCommonFunction(...){
...
}
page1.js
$(document).ready(function() {
...
function1();
...
});
page2.js
$(document).ready(function() {
...
function2();
...
});
page1.html
...
<script src='/js/common/js'></script>
<script src='/js/page1.js'></script>
...
page2.html
...
<script src='/js/common/js'></script>
<script src='/js/page2.js'></script>
...
Consider the usage of AMD (Asynchronous Module Definiton) design pattern. Put your JavaScript code into modules and on each page use just those you really need to. For example requirejs does a great job and I've been using it with success. If you have a bigger project you can split your modules into namespaces. This approach will keep excellent code maintainability and it's reliable. You simply put the "starter" javascript file on each page and load only those required modules you need to work with per each page.
There are many ways to deal with this problem, either using a JavaScript Framework that is aiming to treat your website as a 'Webapp' (Angular and Ember among the popular), or using your own custom script that will do just that - invoking the appropriate JavaScript per loaded page.
Basically, a custom script that will be able to handle it, will have to make use of (pseudo) 'Namespaces' to separate modules/pages code sections.
Assuming you have 2 hypothetical pages, Home and Browse, Simplified code sample may look like this:
HTML:
<body data-page="Home">
Global.js:
var MyApp = {}; // global namespace
$(document).ready(function()
{
var pageName = $('body').data('page');
if (pageName && MyApp[pageName] && MyApp[pageName].Ready)
MyApp[pageName].Ready();
});
Home.js:
MyApp.Home = MyApp.Home || {}; // 'Home' namespace
MyApp.Home.Ready = function()
{
// here comes your 'Home' document.ready()
};
Browse.js:
MyApp.Browse = MyApp.Browse || {}; // 'Browse' namespace
MyApp.Browse.Ready = function()
{
// here comes your 'Browse' document.ready()
};
MyApp.Browse.AnotherUtilFunc = function()
{
// you could have the rest of your page-specific functions as well
}
Also, since you're using ASP.NET MVC, sometimes your Controller name may fit as the qualified page name, you can set it automatically in your Layout.cshtml (if you have one):
<html>
<head>
</head>
<body data-page="#ViewContext.RouteData.Values["Controller"].ToString()">
#RenderBody()
</body>
</html>
I think its not worth stuffing up everything in a single file and separating them with conditional statements, just to avoid adding a reference on the respective file.
If you have code that can be called on 2,3 or more pages, then we can opt for having them in a common file. But if its going to be called on a single page then we must write code on the respective page only. This will also increase the overhead of declaring the functions that are not going to be called on the current page
And when you are using the common js file, then you don't need to worry about the $(document).ready(); event, you can use a single ready event in the common file and separate the code by using conditional statements.
The new versions of the script manager will combine everything into one blob of a script. In theory it makes fewer round trips and things run faster. In practice you could end up with several large scripts that are nearly identical and each page needs its own blob of a script. If your making one of those never change the url website pages then this is the way to go.
I came up with these best practices when I was working with jquery on ASP.Net
Load Jquery in your master page above the first script manager. Jquery is now available on every page. The browser will only get it once and cache it.
If bandwidth is an issue use a jquery loader like googleload or MS content delivery network
Document.load is always at the bottom of the page to guarantee that everything needed is already loaded.
From my blog that I haven't updated in years...Google Load with ASP.Net
One common way to address this problem would be to have your common script include followed by a per-page script element:
<!-- In 'shoppingcart.html' -->
<script src="main.js"></script>
<script>
// Let there be a onDomReady JS object inside main.js
// that defines the document.ready logic on a per-page basis
$(document).ready(onDomReady.shoppingCart);
</script>
Great question, I have been dealing with the same thing.
Here is what I have been doing:
Have your $(document).ready() call different init functions (if they exist), where each .js file has its own init which adds event listeners and loads functions, messes with css, etc.. Each .js file is separated out into different pieces of functionality.
This way you have one document ready that calls all of your initializers. So each page would include the .js functionality it needs. This way you can separate out what is different.
ex:
ready.js:
$(document).ready(function(){
if (typeof menuNavInit == 'function'){
menuNavInit();
}
if (typeof menuNavDifferentInit == 'function'){
menuNavDifferentInit();
}
//other .js functionality
});
menuNav.js
function menuNavInit(){
$("#menu").on('click', menuNavClick)
}
function menuNavClick(){
//do something
}
menuNavDifferent.js
function menuNavDifferentInit(){
$("#menu").on('click', menuNavDifferentClick)
}
function menuNavDifferentClick(){
//do something else
}
page1.html
...
<script src='scripts/ready.js'></script>
<script src='scripts/menuNav.js'></script>
...
page2.html
...
<script src='scripts/ready.js'></script>
<script src='scripts/menuNavDifferent.js'></script>
...
I am trying to load Skyscanner API dynamically but it doesn't seem to work. I tried every possible way I could think of and all it happens the content disappears.
I tried console.log which gives no results; I tried elements from chrome's developers tools and while all the content's css remains the same, still the content disappears (I thought it could be adding display:none on the html/body sort of). I tried all Google's asynch tricks, yet again blank page. I tried all js plugins for async loading with still the same results.
Skyscanner's API documentation is poor and while they offer a callback it doesn't work the way google's API's callback do.
Example: http://jsfiddle.net/7TWYC/
Example with loading API in head section: http://jsfiddle.net/s2HkR/
So how can I load the api on button click or async? Without the file being in the HEAD section. If there is a way to prevent the document.write to make the page blank or any other way. I wouldn't mind using plain js, jQuery or PHP.
EDIT:
I've set a bounty to 250 ontop of the 50 I had previously.
Orlando Leite answered a really close idea on how to make this asynch api load although some features doesn't work such as selecting dates and I am not able to set styling.
I am looking for an answer of which I will be able to use all the features so that it works as it would work if it was loading on load.
Here is the updated fiddle by Orlando: http://jsfiddle.net/cxysA/12/
-
EDIT 2 ON Gijs ANSWER:
Gijs mentioned two links onto overwriting document.write. That sounds an awesome idea but I think it is not possible to accomplish what I am trying.
I used John's Resig way to prevent document.write of which can be found here: http://ejohn.org/blog/xhtml-documentwrite-and-adsense/
When I used this method, I load the API successfuly but the snippets.js file is not loading at all.
Fiddle: http://jsfiddle.net/9HX7N/
I belive what you want is it:
function loadSkyscanner()
{
function loaded()
{
t.skyscanner.load('snippets', '1', {'nocss' : true});
var snippet = new t.skyscanner.snippets.SearchPanelControl();
snippet.setCurrency('GBP');
snippet.setDeparture('uk');
snippet.draw(document.getElementById('snippet_searchpanel'));
}
var t = document.getElementById('sky_loader').contentWindow;
var head = t.document.getElementsByTagName('head')[0];
var script = document.createElement('script');
script.type = 'text/javascript';
script.onreadystatechange= function() {
if(this.readyState == 'complete') loaded();
}
script.onload= loaded;
script.src= 'http://api.skyscanner.net/api.ashx?key=PUT_HERE_YOUR_SKYSCANNER_API_KEY';
head.appendChild(script);
}
$("button").click(function(e)
{
loadSkyscanner();
});
It's load skyscanner in iframe#sky_loader, after call loaded function to create the SearchPanelControl. But in the end, snippet draws in the main document. It's really a bizarre workaround, but it works.
The only restriction is, you need a iframe. But you can hide it using display:none.
A working example
EDIT
Sorry guy, I didn't see it. Now we can see how awful is skyscanner API. It puts two divs to make the autocomplete, but not relative to the element you call to draw, but the document.
When a script is loaded in a iframe, document is the iframe document.
There is a solution, but I don't recommend, is really a workaround:
function loadSkyscanner()
{
var t;
this.skyscanner;
var iframe = $("<iframe id=\"sky_loader\" src=\"http://fiddle.jshell.net/orlleite/2TqDu/6/show/\"></iframe>");
function realWorkaround()
{
var tbody = t.document.getElementsByTagName("body")[0];
var body = document.getElementsByTagName("body")[0];
while( tbody.children.length != 0 )
{
var temp = tbody.children[0];
tbody.removeChild( temp );
body.appendChild( temp );
}
}
function snippetLoaded()
{
skyscanner = t.skyscanner;
var snippet = new skyscanner.snippets.SearchPanelControl();
snippet.setCurrency('GBP');
snippet.setDeparture('uk');
snippet.draw(document.getElementById('snippet_searchpanel'));
setTimeout( realWorkaround, 2000 );
}
var loaded = function()
{
console.log( "loaded" );
t = document.getElementById('sky_loader').contentWindow;
t.onLoadSnippets( snippetLoaded );
}
$("body").append(iframe);
iframe.load(loaded);
}
$("button").click(function(e)
{
loadSkyscanner();
});
Load a iframe with another html who loads and callback when the snippet is loaded. After loaded create the snippet where you want and after set a timeout because we can't know when the SearchPanelControl is loaded. This realWorkaround move the autocomplete divs to the main document.
You can see a work example here
The iframe loaded is this
EDIT
Fixed the bug you found and updated the link.
the for loop has gone and added a while, works better now.
while( tbody.children.length != 0 )
{
var temp = tbody.children[0];
tbody.removeChild( temp );
body.appendChild( temp );
}
For problematic cases like this, you can just overwrite document.write. Hacky as hell, but it works and you get to decide where all the content goes. See eg. this blogpost by John Resig. This ignores IE, but with a bit of work the trick works in IE as well, see eg. this blogpost.
So, I'd suggest overwriting document.write with your own function, batch up the output where necessary, and put it where you like (eg. in a div at the bottom of your <body>'). That should prevent the script from nuking your page's content.
Edit: OK, so I had/took some time to look into this script. For future reference, use something like http://jsbeautifier.org/ to investigate third-party scripts. Much easier to read that way. Fortunately, there is barely any obfuscation/minification at all, and so you have a supplement for their API documentation (which I was unable to find, by the way -- I only found 'code wizards', which I had no interest in).
Here's an almost-working example: http://jsfiddle.net/a8q2s/1/
Here's the steps I took:
override document.write. This needs to happen before you load the initial script. Your replacement function should append their string of code into the DOM. Don't call the old document.write, that'll just get you errors and won't do what you want anyway. In this case you're lucky because all the content is in a single document.write call (check the source of the initial script). If this weren't the case, you'd have to batch everything up until the HTML they'd given you was valid and/or you were sure there was nothing else coming.
load the initial script on the button click with jQuery's $.getScript or equivalent. Pass a callback function (I used a named function reference for clarity, but you can inline it if you prefer).
Tell Skyscanner to load the module.
Edit #2: Hah, they have an API (skyscanner.loadAndWait) for getting a callback once their script has loaded. Using that works:
http://jsfiddle.net/a8q2s/3/
(note: this still seems to use a timeout loop internally)
In the skyrunner.js file they are using document.write to make the page blank on load call back... So here are some consequences in your scenario..
This is making page blank when you click on button.
So, it removes everything from page even 'jQuery.js' that is why call back is not working.. i.e main function is cannot be invoked as this is written using jQuery.
And you have missed a target 'div' tag with id = map(according to the code). Actually this is the target where map loads.
Another thing i have observed is maps is not actually a div in current context, that is maps api to load.
Here you must go with the Old school approach, That is.. You should include your skyrunner.js file at the top of the head content.
So try downloading that file and include in head tag.
Thanks
I am coding a big website but I have cut down my problem into the following tiny html file:
http://dl.dropbox.com/u/3224566/test.html
The problem is that if I (re)load with JQuery a content that features a facebook code, the latter won't appear, even if I reload the script (leading to a duplication of that all.js script, which is another issue).
How can I fix this?
Regards,
Quentin
Use the FB.XFBML.parse() docs after you load the new content
function loadPage() {
$('#test').load('test.html #test', function() {
FB.XFBML.parse( );
}).fadeOut('slow').fadeIn('slow');
}
Note, that loading a fragment with id test in a div with id test will create multiple (two) elements with the same id (nested in each other) in the page, which should never happen as it is invalid.
To avoid this use the more verbose $.get method
$.get('test.html',
function(data) {
var temp = $('<div>').html(data).find('#test');
$('#test').html(temp.html());
}
);