I've been reading about history.pushState and popstate but for a web-page that's populated with mostly AJAX requests (say a book store that allows users to add items to a cart), is there a function with the history object or something similar, that can rewind these additions?
A simpler representation of this is in the below example. A user clicks on a div and "Shown" appears in the respective div. Below these divs is the text, You have selected [detail]. This changes with respect to the selection and triggering the popState changes the text in the right way.
But the divs still have "Shown". Is there a way to remove these in order as they were added similar to how the line of text changes, or do I have to create a separate function to take care of this - which would be the opposite of the function that added "Shown" to the div?
I've added some function that I've edited in to achieve the above but I just don't like how hacky it is.
<html>
<head>
<link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
<div class='name'></div>
<div class='age'></div>
<div class='sex'></div>
<p>You have selected <span>no color</span></p>
</body>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.3/jquery.min.js">
</script>
<script>
$('div').on('click', function() {
var detail = $(this).attr('class');
showDetails(detail);
doPushState(detail);
});
function showDetails(detail) {
var classDeets = '.'+detail
$(classDeets).text("Showing " + detail);
$('p span').text(detail);
}
function doPushState(detail) {
var state = { selected : detail },
title = "Page title",
path = "/" + detail;
history.pushState(state, title, path);
}
function removeDetails(detail) {
var classDeets = '.'+detail
$(classDeets).text("");
}
function checkState(detail) {
var temp = document.getElementsByClassName(detail)[0].innerHTML;
if(temp.length == 0){
showDetails(detail);
}
else{
removeDetails(detail);
}
}
$(window).on('popstate', function(event) {
var state = event.originalEvent.state;
if (state) {
checkState(state.selected);
}
});
</script>
I'm not sure exactly what your use case is, but I immediately think of ajax loaded report and list pages that I do. I encode the options for the report after a "#". It makes the browser buttons work as intended for updating url based and it makes it linkable to others. You then just need to poll the # encoded data to detect changes and update accordingly. Since you use the # (in page link) it doesn't trigger a page load to the server. You just have to make sure that if you use encoded hash data that you don't use in-page links for page navigation. If this sounds like an option, let me know and I can post some helper code for setting it up.
Without more context, it's tough to rewrite your code. That said, you need to use history.replaceState() instead of history.pushState().
history.replaceState() modifies the state of a history entry. From MDN: "replaceState() is particularly useful when you want to update the state object or URL of the current history entry in response to some user action."
Related
I run a WoW guild forum based on php (phpbb), javascript and html. Ever since long, Wowhead allows links to be posted to their item/spell IDs etc. The basic code to the Wowhead JS and it's variables is:
<script src="//static.wowhead.com/widgets/power.js"></script>
<script>var wowhead_tooltips = { "colorlinks": true, "iconizelinks": true, "renamelinks": true }</script>
There is an extension that puts this code in the footer of every page via a HTML file. Every Wowhead link posted will be converted in a link with a tooltip explaining what it links to. The '"renamelink": true' portion of the wowhead_tooltips variable makes it as such that any link of an item or spell is renamed to the exact name of what it is linked to.
The problem: when I generate custom URLs using a Wowhead link, ie:
Teleport
instead of displaying 'Teleport' with a tooltip of Blink, it will rename the entire URL to Blink with an icon, as described in the wowhead_tooltips variable.
What I want to achieve is:
Any direct URL to Wowhead should be converted into a renamed spell/item.
Any custom URL to Wowhead should be retain it's custom text, but retrieve the tooltip.
This should both be possible on a single page.
The best solution I have come up with is to add an 'if' function to var wowhead_tooltips based on class, then add the class to URLs:
<script>if ($('a').hasClass("wowrename")) { var wowhead_tooltips = { "colorlinks": true, "iconizelinks": true, "renamelinks": false } }</script>
<a class="wowrename" href="http://www.wowhead.com/spell=1953">Teleport</a>
This works, however, the problem with this solution is that once the script recognizes one URL with the class "wowrename" on the page it will stop renaming all links, meaning that custom URLs and direct URLs can't be mixed on a single page.
Any other solution I've tried, using IDs, defining different variables etc either don't work or come up with the same restriction.
Hence the question, is it possible to change Javascript variables (in this case "var wowhead_tooltips { "renamelinks": false}" per element (URL), based on id, class or anything else?
Direct link that gets renamed with tooltip and iccn.
Teleport
Custom link with tooltip and original text.
I've stored the original link text as a data attribute so we can restore it after it's been changed.
<a class="wowrename" href="http://www.wowhead.com/spell=1953" data-value="Teleport">Teleport</a>
Keep checking for when static.wowhead.com/widgets/power.js changes the last link text. Once changed, restore using the data-value value, remove the styling added that creates the icon and stop the timer.
$(function () {
//timmer
checkChanged = setInterval(function () {
// check for when the last link text has changed
var lastItem = $("a.wowrenameoff").last();
if (lastItem.text() !== lastItem.data('value')) {
$("a.wowrenameoff").each(function () {
//change value
$(this).text($(this).data('value'));
//remove icon
$(this).attr('style', '');
//stop timer
clearInterval(checkChanged);
});
}
i++;
}, 100);
});
This does cause the link icon to flicker on then off, but it is repeated after a page refresh.
JSFiddle demo
This is simple solution. It's not the best way.
var wowhead_tooltips = { "colorlinks": true, "iconizelinks": true, "renamelinks": true }
$('a').hover(function() {
if ($(this).hasClass('wowrename') {
wowhead_tooltips.renamelinks = true;
}
else {
wowhead_tooltips.renamelinks = false;
}
});
I don't know how exactly wowhead API works, but if wowhead_tooltips variable is loaded exactly in the moment when the user points the link with the mouse (without any timeout) - this can fail or randomly work/not work.
The reason can be that the javascript don't know which function to execute first.
I hope this will work. If it's not - comment I will think for another way.
You have to loop on all the links, like this:
$("a.wowrename").each(function() {
// some code
});
Good Afternoon everyone.
I'm trying to color the comment item when I go to the page with the comment included through notifications. - Like fb or stack overflow.
I have everything working except the part that I mentioned above.
Notification Events
Template.notification.events({
'click a':function() {
var commentTemplate = this.commentId;
console.log(commentTemplate); //Target commentId returns successfully
//Code here needed
//to color comment when page moves to
//coresponding page.
}
Template.commentList
//HTML
{{#each comments}}
{{> commentItem}}
{{/each}}
//JS
comments: function(){
return Comments.find({postId:this._id});
}
I've also tried grabbing the corresponding commentItem's id through console.log using this._id.
So What I would like to know is,
is there a way to link this.commentId from notifications and access <template name = "commentItem"> with corresponding _id. And then manipulate it's element / dom by using things such as .addClass.
Please nudge me in the right direction!
If I understand you correctly, when you click a link in the notifications template, you want to take the user to the commentList page and then manipulate the CSS of the referred-to comment? There are probably a couple of ways to do this.
The first step is going to be to make sure that you have a way to select that particular DOM element once the page loads. For that, in your commentItem template, you might do something like this:
<template name='commentItem'>
<div class='commentItem' id='comment-{{_id}}'>
....
</div>
</template>
If you're using iron:router, a quick and easy way (but not a particularly robust way) to do this would be to manually redirect to the commentList page, then perform your manipulation as part of the event handler once the page has rendered:
Template.notification.events({
'click a':function(event) {
event.preventDefault(); // don't let this take you anywhere
var commentTemplate = this.commentId;
Router.go($(event.currentTarget).attr('href'));
setTimeout(function() {
$('#comment-' + commentTemplate).addClass('whatever-class-you-want');
}, 500); // need to wait for the page to render first -- this is probably not a robust method
}
});
A more robust option, which has the added benefit of persisting on page refresh, might be to add an optional referrer parameter to the URL (i.e., have the link be something like ..., where [comment _id] is replaced by the _id of the comment in question), then in your commentList template, once your page has rendered, you can see if there's a referrer, and if there is, change the CSS:
Template.commentList.rendered = function() {
var referrer = Router.current().params.query.referrer;
if (referrer) {
$('#comment-' + referrer).addClass('whatever-class-you-want');
}
}
I'm using Polymer for my web-app and I'm currently having some issues with page linking. It's the first site I'm making using all ajax/javascript, thus I haven't used the history functions of javascript a lot yet.
Anyhow, I have a main menu in the left sidebar. When one of those is pressed, it should change the url of the browser and also put it in the history of the browser. To do so I have the following code:
Polymer('my-app', {
mainMenu: function(){
this.$.mainPages.selected = this.$.mainMenu.selected;
console.log("Pusing state " + this.$.mainMenu.selected);
history.pushState(null , "title", this.$.mainMenu.selected);
}
});
Now the problem is that it gets called twice, so if you click once the pushState is called twice. Needless to say this is not good.
I have made a sample code here: click me.
In this sample code you can see after pressing a couple of menu-items, first of all the event gets fired two times. I've also noticed when pressing the back-button, updating the page seems to also refire the pushState.
So in short, my first concern is that the event of pressing an element from the core-menu, the mainMenu function is called twice. Second concern is that I am repushing states when going back, which I presume should be prevented as well.
Updated code:
Polymer('my-app', {
ready: function(){
var link = document.URL.split("/");
this.$.mainMenu.selected = link[link.length-1];
console.log("ready with link: "+link[link.length-1]);
window.onpopstate = function(){
var link = document.URL.split("/");
console.log("Calling onpopstate. New link is: "+link[link.length-1]);
this.$.mainMenu.selected = link[link.length-1];
this.$.mainPages.selected = this.$.mainMenu.selected;
}
},
nav: function(){
this.$.drawerPanel.togglePanel();
},
back: function(){
window.history.back();
},
mainMenu: function(){
console.log("Pusing state " + this.$.mainMenu.selected);
this.$.mainPages.selected = this.$.mainMenu.selected;
history.pushState(null , "title", this.$.mainMenu.selected);
}
}
You are much better off using data-binding. The idea here is to make your element model-driven, an model-view-presenter (MVP) pattern. The model is made up of properties in your element, the view is described by the template, and the presenter is in your script. Data-binding allows us to loosely couple the view from the logic.
Let's decide the current page will be controlled by a property called page.
Then we can set up our UI to be driven by the page property:
<core-menu selected="{{page}}" valueattr="id">
...
</core-menu>
<core-animated-pages class="main" selected="{{page}}" valueattr="id" transitions="slide-from-right">
This is good because we have decoupled the UI from the code. The code never talks directly to the core-animated-pages or the core-menu (notice I removed the ids). We don't listen to any events. This way we can change the UI at will without having to modify the script.
Now, we want the page to be initially selected by the URL, so we'll initialize it that way, or default to 'home'. We also want to synchronize page to back events, so we'll listen to `onpopstate'.
ready: function() {
// scrape the initial page off the window location
this.page = location.hash.slice(1) || 'home';
// listen for 'back' events
addEventListener('popstate', this.popstate.bind(this));
},
popstate: function(event) {
// comes here whether we went 'back' from code or UI
this.poppedPage = this.page = event.state;
}
We keep track of poppedPage so we can differentiate back and forward. We only want to push a new state when we go forward.
Now we need the page to be reflected in the history, so we need to push state when the page changes. As above, the one caveat is that we need to only push the state if we are going forward.
pageChanged: function() {
// if the selected page changes, push a state (unless we are going backward)
if (this.poppedPage !== this.page) {
history.pushState(this.page, "Title", '#' + this.page);
}
}
Here it is all put together:
http://jsbin.com/luwitudu/9/edit
You get two events, one for the deselection of the currently selected item and one for the new selected item. Check the event object that is passed to your callback function:
mainMenu: function(e) {
console.log(e.detail.isSelected);
console.log(e.detail.item);
}
But if you use on-core-activate your callback is only called once when a menu item is tapped by the user (it is not called when you change the menu selection programmatically.) This prevents the unwanted window history push when you change the menu selection in your program.
Then you should setup a window.onpopstate callback in which you select the (previous) menu item. If you call document.URL in you back() function, it will return the current URL, not the new one set by window.history.back(). Instead only pop from the history in this function.
This should work:
<core-menu ... on-core-activate="{{mainMenu}}">
Polymer("my-app", {
ready: function() {
var self = this;
this.$.mainMenu.selected = "home";
history.pushState(null, "Title", this.$.mainMenu.selected);
window.onpopstate = function() {
var link = document.URL.split("/");
self.$.mainMenu.selected = link[link.length-1];
self.$.mainPages.selected = self.$.mainMenu.selected;
};
},
mainMenu: function() {
this.$.mainPages.selected = this.$.mainMenu.selected;
history.pushState(null, "Title", this.$.mainMenu.selected);
},
back: function() {
window.history.back();
}
});
index.php
<html>
<head>
<title>My Title</title>
<script type="text/javascript">
function getLink(data) {
document.getElementById("box").innerHTML="This is "+data;
}
</script>
</head>
<body>
Home<br />
Profile<br />
Message<br />
Setting<br />
<hr />
<div id="box"></div>
</body>
</html>
Output
Home
Profile
Message
Setting
This is Home
As the code says my Div contents updated when i click any of the link but the problem is that when user goes back by clicking Back Button of Browser the content of my Div donot changes.
I want that either user Goes Back, Goes Forward or he directly puts the path in the address bar www.*****/index.php#profile the content of my Div should be change.
Note
I used document.location.hash to get the value of hash like this :
<head>
<script>
var hashValue=document.location.hash;
alert(hashValue);
</script>
</head>
but it works only when user goes back and then refresh the page
Plz help me how can i achieve this :(
You need to use hashchange event:
function hash_changed() {
var data = document.location.hash.substr(1);
var box = document.getElementById("box");
if (data) {
// inner page
box.innerHTML="This is " + data;
}
else {
// homepage
box.innerHTML = "";
}
}
window.onhashchange = function () {
hash_changed();
};
window.onload = function () {
hash_changed();
};
Also when you are using hashchange event, there is
no need to set onclick for your links:
Home
Profile
Message
Setting
When user click on a link, the hash automatically changes (with href attribute of link),
and hashchange event get fired.
Check DEMO here.
First Time
When a user come to your page for the first time with a hash:
http://fiddle.jshell.net/B8C8s/9/show/#message
We must show the wanted page (message here), so we must run hash_changed() function
we declare above, at first time. For this, we must wait for DOM ready or window.onload.
Check the HTML5 history API. It allows you to work with the browser history
HTML5 history api
$(window).on('hashchange', function() {
alert(location.hash);
});
or window.onhashchange event if you don't want to use jQuery
If you're going to be using AJAX, you'll really want to look into using jQuery instead of raw javascript unless your intention is educational. jQuery is just a mainstay of the web now.
If you must use those hashes...
Use jQuery Special Events, and use the hashchange event:
<a href='#home'>Home</a>
Script:
$(window).on('hashchange', function() {
$('#box').html("This is "+event.fragment);
});
However, for your scenario...
You don't need to use those # values at all as you're passing the values in your function arguments anyway according to the code you provided, just do this:
Home<br />
Alternatively (and preferably, as you're using AJAX according to the tags) you can use jQuery and its builtin selector click events which use Event Listeners:
<a href='javascript:void();' class='divLink' id='home'>Home</a><br/>
Script is this easy:
$('.divLink').click(function(){
$('#box').html("This is "+$(this).id());
}
On a page with Ajax event, I want to disable all actions until the Ajax call returns (to prevent issues with double-submit etc.)
I tried this by prepending return false; to the current onclick events when "locking" the page, and removing this later on when "unlocking" the page. However, the actions are not active any more after they are "unlocked" -- you just can't trigger them.
Why is this not working? See example page below. Any other idea to achieve my goal?
Example code:
both the link and the button are showing a JS alert; when pressing lock, then unlock the event handler is the same as it was before, but doesn't work...?!?
The code is meant to work with Trinidad in the end, but should work outside as well.
<html><head><title>Test</title>
<script type="text/javascript">
function lockPage()
{
document.body.style.cursor = 'wait';
lockElements(document.getElementsByTagName("a"));
lockElements(document.getElementsByTagName("input"));
if (typeof TrPage != "undefined")
{
TrPage.getInstance().getRequestQueue().addStateChangeListener(unlockPage);
}
}
function lockElements(el)
{
for (var i=0; i<el.length; i++)
{
el[i].style.cursor = 'wait';
if (el[i].onclick)
{
var newEvent = 'return false;' + el[i].onclick;
alert(el[i].onclick + "\n\nlock -->\n\n" + newEvent);
el[i].onclick = newEvent;
}
}
}
function unlockPage(state)
{
if (typeof TrRequestQueue == "undefined" || state == TrRequestQueue.STATE_READY)
{
//alert("unlocking for state: " + state);
document.body.style.cursor = 'auto';
unlockElements(document.getElementsByTagName("a"));
unlockElements(document.getElementsByTagName("input"));
}
}
function unlockElements(el)
{
for (var i=0; i<el.length; i++)
{
el[i].style.cursor = 'auto';
if (el[i].onclick && el[i].onclick.search(/^return false;/)==0)
{
var newEvent = el[i].onclick.substring(13);
alert(el[i].onclick + "\n\nunlock -->\n\n" + newEvent);
el[i].onclick = newEvent;
}
}
}
</script>
<style type="text/css">
</style>
</head>
<body>
<h1>Page lock/unlock test</h1>
<p>Use these actions to lock or unlock active elements on the page:
lock,
unlock.</p>
<p>And now some elements:</p>
<a onclick="alert('This is the action!');return false;" href="#">link action</a>
<input type="button" value="button action" onclick="alert('This is another action!')"/>
</body>
</html>
Thanks guys for your ideas and answers.
Now I see that I have mixed up Strings and functions, which obviously can't work ;(
I should have made clear that we use some Web FW and tag libraries (Trinidad) which create the event handling (and Ajax) code, hence I can't edit that directly or use synchronous Ajax etc.
Moreover, Ajax is only one scenario where this code should be executed. It's purpose is to prevent the user to double-submit a page/action, which is also relevant for non-Ajax pages where you could kind of doulbe-click on a button. I know that this is not really safe, and it's only meant to be a "convenience" thingy to avoid getting the navigation error page too often (we have server-side protection, of course).
So, will try the div overlay, probably.
Thanks again,
Christoph.
How about setting up a global var
actions_disabled = 0
increment when the AJAX call starts then decrement when it finishes. All your "action" handlers can then start with
if (actions_disabled) return false;
Much simpler than debugging self-modifying code!
Alternatively, to lock your controls you could set:
control.disabled="disabled"
which will have the bonus of greying them out, making it obvious to the user that they can't submit. To unlock, simply set:
control.disabled=""
NEW IDEA BASED ON COMMENTS (can't quote code in comments, it appears ...):
You can always just hang extra attributes off Javascript objects:
To lock, you could:
control.onclick_old = control.onclick
control.onclick = "return false;"
To unlock, you could:
control.onclick = control.onclick_old
I once achieved this goal by creating a DIV that covered the area I wanted disabled, setting its z-index higher than any of the other elements on the page, and then setting its opacity to 0. By default, this DIV was hidden by display: none, so that it wouldn't interfere with anything. However, when I wanted the area disabled, I just set its display to block.
Steve
AJAX. Asynchronous. Just make the HTTP request synchronous. Problem solved.
The problem with your code is a result of not coming to grips with types in javascript.
When you say:
var newEvent = 'return false;' + el[i].onclick
what this does is coerce el[i].onclick (which is a function) to a string, then concatenates it to the string 'return false;'. Then when you reassign it as so:
el[i].onclick = newEvent;
onclick which was previously a function is now a string.
Then you attempt to resurrect your old function from the string by taking a substring:
var newEvent = el[i].onclick.substring(13);
which is fine, except newEvent is still a string! So when you assign it back to onclick again, you are assigning the string representation of the original function, not the function itself.
You could use eval to evaluate the string and return the function, but please don't do that. There are a number of better ways to do this, as has been suggested by other commenters.
I would also question why you wish to use AJAX at all if you don't want to allow asynchronous requests.
Put lockPage() at top of activete() function, and unlockPage() at bottom of deactivate().
activate: function() {
function lockPage()
{
lockElements(document.getElementsByTagName("a"));
lockElements(document.getElementsByTagName("input"));
lockElements(document.getElementsByTagName("button"));
};
function lockElements(el)
{
for (var i=0; i<el.length; i++)
{
el[i].style.pointerEvents="none";
}
};
lockPage();
// ...
},
deactivate: function() {
// ...
function unlockPage() {
unlockElements(document.getElementsByTagName("a"));
unlockElements(document.getElementsByTagName("input"));
unlockElements(document.getElementsByTagName("button"));
};
function unlockElements(el)
{
for (var i=0; i<el.length; i++)
{
el[i].style.pointerEvents="auto";
}
};
unlockPage();
},
Using a div overlay does not prevent a user from tab-ing into your page. Usually that is OK, since most users do not tab through a page anyhow.
If you use any keyboard shortcuts on your page, they will still be available, so separate handling will be needed for those.
Alse, I assume that clicking an element that can have focus (eg. an <a> tag), then pressing enter, would still cause a double submit.