How to determine if a HashChangeEvent was triggered by an user action in the page (click on a link, button, or something like), or manually input in the address bar?
I think your best bet is to create some kind of wrapper that allows you to change the window.location.hash attribute, and call it from any elements that you want to be tracked. I have created an example on Plunker to demonstrate my idea of how that would be done. The example is pretty rudimentary, however. I think it conveys the point of what you're trying to do though.
I'll also put the example here, but it's not functional obviously. Let me know if you think this is going to cover your use case. I can tweak it if needed.
(function($) {
'use strict';
//Create a route function on the jQuery object
//that we can use anywhere.
/**
* Wraps the setter for window.location.hash
* and tracks who called it.
*
* #param {Element} who The element that called route.
* #param {string} hash The hash to set.
*/
$.route = function(who, hash) {
$.route.who = who;
window.location.hash = hash;
};
})(jQuery);
The code below shows how you can use the $.route function to track which element changed the window.location.hash value.
(function($) {
'use strict';
window.addEventListener('hashchange', function() {
if ($.route.who) {
//We know this was one of our tracked elements.
alert(`Tag Name "${$.route.who.prop('tagName')}" triggered hash change.`);
} else {
//We'll assume that this wasn't being tracked, and therefore
//is most likely to be caused by user input.
alert('We could not track what triggered "hashchange".');
}
//Don't forget to reset this so that if an untracked input triggered this
//it doesn't appear that it was tracked.
$.route.who = null;
});
$(function() {
$('.hash-change').click(function(e) {
//Do some internal routing action that can track the caller.
var $this = $(this);
//Get the hash to set to the URL.
var href = $this.attr('href') || $this.data('href');
//Use our internal routing action to track the caller.
$.route($this, href);
//Prevent the default action of those hyperlinks by returning false.
return false;
});
});
})(jQuery);
The HTML that I used in my example looks like this. Each one has the class hash-change. Elements that natively support href use it, others that don't use the dataset for the href.
<a class="hash-change" href="home">Home</a>
<a class="hash-change" href="about">About</a>
<input type="button" class="hash-change" data-href="contact" value="Contact" />
Those elements will be tracked and will use the $.route function to change the hash value. Anything that doesn't use the $.route function will not set the $.route.who value, and therefore, you can assume it was changed from some source, such as the user manually changing the hash.
Ultimately, whatever solution you implement will have to be a wrapper around existing functionality. Take a look at the Plunker example for a live demo.
Related
I am trying to call JavaScript function when # is present in URL. I know normal behavior is to navigate / scroll to the specific tag. But could not find how to invoke a JavaScript function.
The below example is close but not solving my problem.
What is the meaning of # in URL and how can I use that?
You might be able to leverage the hashchange event to trigger the function, assuming you don't just want to keep polling the location to see if it changes.
DOCS: https://developer.mozilla.org/en-US/docs/Web/API/Window/hashchange_event
This code snippet will add the listener to the current page, then manipulate the hash and fire the function, displaying the new hash value. You could call any function here.
window.addEventListener('hashchange', function() {
alert(location.hash);
});
window.location += "#test";
var hash = window.location.hash;
if(hash){
// do something
}
<script>
if (window.location.href.includes('#')) {
// run your code here
}
</script>
use a location.hash function will solve your problem
var hash = window.location.hash.replace(/#/g, '');
if(hash){
// found a hash
console.log("heyy I found a hash")'
}
else{
// did not find a hash
console.log("Uh oh")
/*
try using :
window.location = window.location + '#' + "some random variable"
to create a new URL and every time the page loads find the hash and
display the wanted data.
*/
}
PS: this only works if your URL is like example.com/#xyz
then it will give you xyz as a console output. This may sound
vague but if you do this you may get a Idea
I am using Sammy.js for my single page app. I want to create functionality similar to SO (the one when you type your question and try to leave the page and it is asking you if you are sure).
If it would not be a single page app, I would just do something like:
$(window).bind('beforeunload', function(){
return 'Are you sure you want to leave?';
});
The problem is that in single page app user do not actually leave the page, but rather changing his document.location.hash (he can leave the page by closing it). Is there a way to make something similar for a SPA, preferably with sammy.js?
We had a similar problem to solve in our Single Page Webapp at my work. We had some pages that could be dirty and, if they were, we wanted to prevent navigation away from that page until a user verifies it's okay to do so. Since we wanted to prevent navigation, we couldn't listen for the onhashchange event, which is fired after the hash is changed, not before. Therefore, we decided to override the default LocationProxy to include logic that allowed us to optionally prevent the navigation before the location was changed.
With that in mind, here is the proxy that we used:
PreventableLocationProxy = (function () {
function PreventableLocationProxy(delegateProxy, navigationValidators) {
/// <summary>This is an implementation of a Sammy Location Proxy that allows cancelling of setting a location based on the validators passed in.</summary>
/// <param name="delegateProxy" type="Sammy.DefaultLocationProxy">The Location Proxy which we will delegate all method calls to.</param>
/// <param name="navigationValidators" type="Function" parameterArray="true" mayBeNull="true">One or more validator functions that will be called whenever someone tries to change the location.</param>
this.delegateProxy = delegateProxy;
this.navigationValidators = Array.prototype.slice.call(arguments, 1);
}
PreventableLocationProxy.prototype.bind = function () {
this.delegateProxy.bind();
};
PreventableLocationProxy.prototype.unbind = function () {
this.delegateProxy.unbind();
};
PreventableLocationProxy.prototype.getLocation = function () {
return this.delegateProxy.getLocation();
};
PreventableLocationProxy.prototype.setLocation = function (new_location) {
var doNavigation = true;
_.each(this.navigationValidators, function (navValidator) {
if (_.isFunction(navValidator)) {
// I don't just want to plug the result of the validator in, it could be anything!
var okayToNavigate = navValidator(new_location);
// A validator explicitly returning false should cancel the setLocation call. All other values will
// allow navigation still.
if (okayToNavigate === false) {
doNavigation = false;
}
}
});
if (doNavigation) {
return this.delegateProxy.setLocation(new_location);
}
};
return PreventableLocationProxy;
}());
This code is pretty simple in and of itself, it is a javascript object that takes a delegate proxy, as well as one or more validator functions. If any of those validators explicitly return false, then the navigation is prevented and the location won't change. Otherwise, the navigation is allowed. In order to make this work, we had to override our anchor tags' default onclick handling to route it through Sammy.Application.setLocation. Once done, though, this cleanly allowed our application to handle the dirty page logic.
For good measure, here is our dirty page validator:
function preventNavigationIfDirty(new_location) {
/// <summary>This is an implementation of a Sammy Location Proxy that allows cancelling of setting a location based on the validators passed in.</summary>
/// <param name="new_location" type="String">The location that will be navigated to.</param>
var currentPageModels = [];
var dirtyPageModels = [];
//-----
// Get the IDs of the current virtual page(s), if any exist.
currentPageModels = _.keys(our.namespace.currentPageModels);
// Iterate through all models on the current page, looking for any that are dirty and haven't had their changes abored.
_.forEach(currentPageModels, function (currentPage) {
if (currentPage.isDirty() && currentPage.cancelled === false) {
dirtyPageModels.push(currentPage);
}
});
// I only want to show a confirmation dialog if we actually have dirty pages that haven't been cancelled.
if (dirtyPageModels.length > 0) {
// Show a dialog with the buttons okay and cancel, and listen for the okay button's onclick event.
our.namespace.confirmDirtyNavigation(true, function () {
// If the user has said they want to navigate away, then mark all dirty pages with the cancelled
// property then do the navigating again. No pages will then prevent the navigation, unlike this
// first run.
_.each(dirtyPageModels, function (dirtyPage) {
dirtyPage.cancelled = true;
});
our.namespace.sammy.setLocation(new_location);
});
// Returns false in order to explicitly cancel the navigation. We don't need to return anything in any
// other case.
return false;
}
}
Remember, this solution won't work if the user explicitly changes the location, but that wasn't a use case that we wanted to support. Hopefully this gets you closer to a solution of your own.
I am using a omniture jasavscript for Site Catalyst.
In which, I am populating the required variables onclick of a link.
But the problem is I get a multiple (2) tracking on a single click, which is not the ideal behaviour. in these 2 tracking, The FIrst one I get is the old one and right after that I get the second latest tracking.
It seems like it is using the cache memory.
UPDATE
I tried reinitializing the object by using var s = {}; before and after the use of s.tl('this','e','',null);
But it didn't worked
Could someone suggest how it can be rectified.
Without seeing any code I can only speculate, but my guess is the additional hit is from SiteCatalyst's auto-link tracking - either an exit link because the target URL is not listed in linkInternalFilters, or a download link because the target URL ends with something listed in linkDownloadFileTypes.
I suspect, given the 'e' argument of your s.tl() example, that the link is an exit link. So on that note.. perhaps the solution here is to piggyback off the auto-exit-link tracking, instead of making your own s.tl() call. Adobe has a plugin called exitLinkHandler that will let you trigger additional variables whenever the auto-exit-link tracking occurs.
Here is the plugin:
/*
* Plugin: exitLinkHandler 0.5 - identify and report exit links
*/
s.exitLinkHandler=new Function("p",""
+"var s=this,h=s.p_gh(),n='linkInternalFilters',i,t;if(!h||(s.linkTyp"
+"e&&(h||s.linkName)))return '';i=h.indexOf('?');t=s[n];s[n]=p?p:t;h="
+"s.linkLeaveQueryString||i<0?h:h.substring(0,i);if(s.lt(h)=='e')s.li"
+"nkType='e';else h='';s[n]=t;return h;");
Within your s_doPlugins function, add the following:
s.url = s.exitLinkHandler();
if (s.url) {
// pop your variables here. Don't forget to pop `linkTrackVars` and `linkTrackEvents`, same as you would have done before
}
Now, this will make your additional variables pop on any exit link triggered. If you want it to only trigger on certain URL matches, or only on a specific match, you can do this several ways, depending on your needs:
If you only need to do a general substring match, you can pass some
or all of the target URL as the first argument for
s.exitLinkHandler() and it will match the passed argument against
the target URL.
If this isn't good enough, within the if(s.url) condition, you can
perform your own matching (e.g. regex matching) against the target
URL using s.url.
If you need to target by some DOM attribute of the link, within the
condition, s.eo is an object reference to the link that was
clicked, so you can write your own conditions around that.
Option 1
Omniture does not track links with # as exit links so you can do something like:
Search
<script>
(function (){
'use strict';
var links = document.querySelectorAll('.prepended-with-hash-for-tracking');
var track = function(e) {
e.preventDefault();
var link = e.currentTarget;
var url = link.href;
var trackingMessage = link.getAttribute('data-track-msg');
// Remove the hash.
if (url[0] === '#') {
url = url.substr(1);
}
// Track in omniture.
var s = s_gi('InsertYourRSID');
s.tl(link, 'o', trackingMessage, null, function(){
window.location.href = url;
});
};
for (var i = 0, len = links.length; i < len; i++) {
links[i].addEventListener('click', track, false);
}
})();
</script>
Option 2
Another work-a-round is to set s.linkLeaveQueryString = true; and then append the url with a query parameter containing your domain name which matches a string in s.linkInternalFilters. e.g. Share
Option 3
Disable omniture's default external link tracking by setting s.trackExternalLinks=false; and then you can handle all external links with an event handler that calls s.tl() with JavaScript similar to option 1.
I would recommend option 3.
My Code is as below.
$(document).ready(function($)
{
var form = $("#video_detail_form");
var name = $("#videoTitle");
var nameInfo = $("#valid_videoTitle");
function validateName(){
//if it's NOT valid
var titleValue=$.trim(name.val());
if(titleValue.length == 0){
nameInfo.text("Please Enter Title");
return false;
}
//if it's valid
else{
nameInfo.text("");
return true;
}
}
name.blur(validateName);
name.keyup(validateName);
name.change(validateName);
$('#editVideoCancel').click(function(){
cancelVideoDetailAjaxCall( '/video/cancelVideoDetail', callBackCancelVideoDetail);
});
});
My cancelVideoDetailAjaxCall function changes text of the videoTitle input box. But my this code is not capturing that event by name.change.
If I change manually then it captures it. So when dynamically my callback function is changing the text then change event is not capturing it.
How should I capture that change?
You can actually extend your value change catching to all changes coming from some script using the jQuery val method, by setting a custom setter in jQuery.valHooks.
Imagine you change the input type to myCustomType, then you will implement jQuery.valHooks.myCustomType.set method which will be called each time val is used to set the input value, and you will include your specific call here. But I insist : it is not a best practice.
You will surely find explicit code on the web for that hooks.
As comments have mentioned, if you programmatically change the value via jQuery you must also trigger that change programmatically, if you want anything subscribed to it to register that change.
You can always make up your own events and trigger them accordingly if you don't want to "interfere" with other things already subscribed to regular events:
$el.on('mycustomevent', function() { ... })
$el.trigger('mycustomevent');
You can even subscribe with the same callback for the 'regular event' and your 'custom event':
$el.on('change', myChangeCallback);
...
$el.on('mycustomevent', myChangeCallback);
If you don't want to keep typing $el.val('...').trigger('mycustomevent') repeatedly, then declare a helper function that does that for you:
// helper function for changing the value
function changeInput(newVal) {
if(!this.$target) this.$target = $('#text'); // stash the target for reuse
this.$target
// programmatically change the value; does not fire 'change' event
.val(newVal)
// now trigger your custom action that behaves
.trigger('customaction'); // add extra parameters, etc
}
Full example: https://jsfiddle.net/drzaus/ds6g745s/8/
As the question states, what I'm attempting to do is have a function that is called when a DOM element is removed from the DOM, much like a destructor.
I looked into unload, but from my understanding that's only called when the browser navigates away from the page.
Thanks in advance!
It is possible to use the special events to build removal tracking. I've created an example that allows to bind to an removed event. Note that this approach only works when the removal is initiated by jQuery (calling $(…).remove()) for example. For a more general solution use DOMNodeRemoved but that wouldn't work in IE.
To enable tracking for an element call $(…).trackRemoval() and the element will fire a removed event when you remove it or one of its parents.
// Create a scope so that our variables are not global
(function(){
/**
* True while unbinding removal tracking
*/
var isUntracking = false;
/**
* A reference that is only known here that nobody else can play with our special event.
*/
var dummy = function(){};
/**
* Special event to track removals. This could have any name but is invoked during jQuery's cleanup on removal to detach event handlers.
*/
jQuery.event.special.elementRemoved = {
remove: function(o){
if(o.handler===dummy && !isUntracking){
$(this).trigger('removed');
}
}
};
/**
* Starts removal tracking on an element
*/
jQuery.fn.trackRemoval = function(){
this.bind('elementRemoved', dummy);
};
/**
* Stops removal tracking on an element
*/
jQuery.fn.untrackRemoval = function(){
isUntracking = true;
this.unbind('elementRemoved', dummy);
isUntracking = false;
};
})();
The jsFiddle contains sample code for usage.
I don't know if it's that what you're looking for. Not really pretty code, just to show what I would like to use:
// Just for this script's sake, you'll want to do it differently
var body = $("body");
var element = $('#loadingBlock');
// Bind the "destructor" firing event
body.bind("elementDeleted", function (element) {
// your "destructor" code
});
// Trigger the event, delete element, can be placed in a function, overloaded, etc.
body.trigger("elementDeleted", element);
element.remove();
There are of course solutions based on watching the DOM directly but the problem is the browser compatibility. You should probably check out mutation events.
Try this:
(function($) {
var _remove = $.fn.remove;
$.fn.remove = function() {
this.trigger('remove'); // notify removal
return _remove.apply(this, arguments); // call original
};
})(jQuery);
usage:
$(element).bind('remove', callback);
...
// some time later
$(element).remove();
See a working example at http://jsfiddle.net/alnitak/25Pnn/
You could always Overload the Remove function (all javascript objects can be dynamically changes in runtime) and replace it with your own function that triggers an event you can use to react to remove.