How do I remove a history entry in GreaseMonkey? [duplicate] - greasemonkey

Using the HTML5 window.history API, I can control the navigation pretty well on my web app.
The app currently has two states: selectDate (1) and enterDetails (2).
When the app loads, I replaceState and set a popState listener:
history.replaceState({stage:"selectDate",...},...);
window.onpopstate = function(event) {
that.toStage(event.state.stage);
};
When a date is selected and the app moves to stage 2 I push state 2 onto the stack:
history.pushState({stage:"enterDetails",...},...);
This state is replaced anytime details change so they are saved in the history.
There are three ways to leave stage 2:
save (AJAX submit)
cancel
back button
The back button is handled by the popstate listener. The cancel button pushes stage 1 so that the user can go back to the details they were entering the back button. These both work well.
The save button should revert back to stage 1 and not allow the user to navigate back to the details page (since they already submitted). Basical, y it should make the history stack be length = 1.
But there doesn't seem to be a history.delete(), or history.merge(). The best I can do is a history.replaceState(stage1) which leaves the history stack as: ["selectDate","selectDate"].
How do I get rid of one layer?
Edit:
Thought of something else, but it doesn't work either.
history.back(); //moves history to the correct position
location.href = "#foo"; // successfully removes ability to go 'forward',
// but also adds another layer to the history stack
This leaves the history stack as ["selectDate","selectDate#foo"].
So, as an alternative, is there a way to remove the 'forward' history without pushing a new state?

You may have moved on by now, but... as far as I know there's no way to delete a history entry (or state).
One option I've been looking into is to handle the history yourself in JavaScript and use the window.history object as a carrier of sorts.
Basically, when the page first loads you create your custom history object (we'll go with an array here, but use whatever makes sense for your situation), then do your initial pushState. I would pass your custom history object as the state object, as it may come in handy if you also need to handle users navigating away from your app and coming back later.
var myHistory = [];
function pageLoad() {
window.history.pushState(myHistory, "<name>", "<url>");
//Load page data.
}
Now when you navigate, you add to your own history object (or don't - the history is now in your hands!) and use replaceState to keep the browser out of the loop.
function nav_to_details() {
myHistory.push("page_im_on_now");
window.history.replaceState(myHistory, "<name>", "<url>");
//Load page data.
}
When the user navigates backwards, they'll be hitting your "base" state (your state object will be null) and you can handle the navigation according to your custom history object. Afterward, you do another pushState.
function on_popState() {
// Note that some browsers fire popState on initial load,
// so you should check your state object and handle things accordingly.
// (I did not do that in these examples!)
if (myHistory.length > 0) {
var pg = myHistory.pop();
window.history.pushState(myHistory, "<name>", "<url>");
//Load page data for "pg".
} else {
//No "history" - let them exit or keep them in the app.
}
}
The user will never be able to navigate forward using their browser buttons because they are always on the newest page.
From the browser's perspective, every time they go "back", they've immediately pushed forward again.
From the user's perspective, they're able to navigate backwards through the pages but not forward (basically simulating the smartphone "page stack" model).
From the developer's perspective, you now have a high level of control over how the user navigates through your application, while still allowing them to use the familiar navigation buttons on their browser. You can add/remove items from anywhere in the history chain as you please. If you use objects in your history array, you can track extra information about the pages as well (like field contents and whatnot).
If you need to handle user-initiated navigation (like the user changing the URL in a hash-based navigation scheme), then you might use a slightly different approach like...
var myHistory = [];
function pageLoad() {
// When the user first hits your page...
// Check the state to see what's going on.
if (window.history.state === null) {
// If the state is null, this is a NEW navigation,
// the user has navigated to your page directly (not using back/forward).
// First we establish a "back" page to catch backward navigation.
window.history.replaceState(
{ isBackPage: true },
"<back>",
"<back>"
);
// Then push an "app" page on top of that - this is where the user will sit.
// (As browsers vary, it might be safer to put this in a short setTimeout).
window.history.pushState(
{ isBackPage: false },
"<name>",
"<url>"
);
// We also need to start our history tracking.
myHistory.push("<whatever>");
return;
}
// If the state is NOT null, then the user is returning to our app via history navigation.
// (Load up the page based on the last entry of myHistory here)
if (window.history.state.isBackPage) {
// If the user came into our app via the back page,
// you can either push them forward one more step or just use pushState as above.
window.history.go(1);
// or window.history.pushState({ isBackPage: false }, "<name>", "<url>");
}
setTimeout(function() {
// Add our popstate event listener - doing it here should remove
// the issue of dealing with the browser firing it on initial page load.
window.addEventListener("popstate", on_popstate);
}, 100);
}
function on_popstate(e) {
if (e.state === null) {
// If there's no state at all, then the user must have navigated to a new hash.
// <Look at what they've done, maybe by reading the hash from the URL>
// <Change/load the new page and push it onto the myHistory stack>
// <Alternatively, ignore their navigation attempt by NOT loading anything new or adding to myHistory>
// Undo what they've done (as far as navigation) by kicking them backwards to the "app" page
window.history.go(-1);
// Optionally, you can throw another replaceState in here, e.g. if you want to change the visible URL.
// This would also prevent them from using the "forward" button to return to the new hash.
window.history.replaceState(
{ isBackPage: false },
"<new name>",
"<new url>"
);
} else {
if (e.state.isBackPage) {
// If there is state and it's the 'back' page...
if (myHistory.length > 0) {
// Pull/load the page from our custom history...
var pg = myHistory.pop();
// <load/render/whatever>
// And push them to our "app" page again
window.history.pushState(
{ isBackPage: false },
"<name>",
"<url>"
);
} else {
// No more history - let them exit or keep them in the app.
}
}
// Implied 'else' here - if there is state and it's NOT the 'back' page
// then we can ignore it since we're already on the page we want.
// (This is the case when we push the user back with window.history.go(-1) above)
}
}

There is no way to delete or read the past history.
You could try going around it by emulating history in your own memory and calling history.pushState everytime window popstate event is emitted (which is proposed by the currently accepted Mike's answer), but it has a lot of disadvantages that will result in even worse UX than not supporting the browser history at all in your dynamic web app, because:
popstate event can happen when user goes back ~2-3 states to the past
popstate event can happen when user goes forward
So even if you try going around it by building virtual history, it's very likely that it can also lead into a situation where you have blank history states (to which going back/forward does nothing), or where that going back/forward skips some of your history states totally.

A simple solution:
var ismobilescreen = $(window).width() < 480;
var backhistory_pushed = false;
$('.editbtn').click( function()
{
// push to browser history, so back button will close the editor
// and not navigate away from site
if (ismobilescreen && !backhistory_pushed)
{
window.history.pushState('forward', null, window.location);
backhistory_pushed = true;
}
}
Then:
if (window.history && window.history.pushState)
{
$(window).on('popstate', function()
{
if (ismobilescreen && backhistory_pushed && $('.editor').is(':visible'))
{
// hide editor window (we initiate a click on the cancel button)
$('.editor:visible .cancelbtn').click();
backhistory_pushed = false;
}
});
}
Results in:
User opens editor DIV, the history state is saved.
User hits back button, history state is taken into account.
Users stays on page!
Instead of navigating back, the editor DIV is closed.
One issue: If you use a "Cancel" button on your DIV and this hides the editor, then the user has to click the mobile's back button two times to go back to the previous URL.
To solve this problem you can call window.history.back(); to remove the history entry by yourself which actually deletes the state as requested.
For example:
$('.btn-cancel').click( function()
{
if (ismobilescreen && backhistory_pushed)
{
window.history.back();
}
}
Alternatively you could push a URL into the history that holds an anchor, e.g. #editor and then push to history or not if the anchor exists in the recent URL or not.

Related

How to display standby dialog in XPages view when expanding a section

In my XPages application I am using the xe:dynamicViewPanel control and would like to add a standby/wait dialog/popup when a section is being expanded by the user (click on the expand-icon to open the section).
Sometimes the view index is not up-to-date and opening a category holding a lot of documents will last a while, in the meantime I want to display some "loading dialog" (which I already have, so, no need to explain how to do this).
My problem is, that I can not find any event or entry point where to start from.
Thank you all !
Alex
You can try code from this link:
https://openntf.org/XSnippets.nsf/snippet.xsp?id=standby-dialog-custom-control
If you want to show stanby dialog on the current section, replace the 79 line
var forms=dojo.body()
with some other container. For example, a partial refresh element
var forms = dojo.byId(refreshId)
In this case you need to replace lines 75 and 140 to pass the id parameter
function StandbyDialog_Started(refreshId) {
try{
if(StandbyDialog_Do==true){
if(this.StandbyDialog_Obj==null) {
var forms= (refreshId)?dojo.byId(refreshId):dojo.body();
this.StandbyDialog_Obj = new dojox.widget.Standby({
target: forms,
zIndex: 10000
});
document.body.appendChild(this.StandbyDialog_Obj.domNode);
this.StandbyDialog_Obj.startup();
}
StandbyDialog_StoreField()
setTimeout("if(StandbyDialog_Do==true){StandbyDialog_StoreField()}",50);
setTimeout("if(StandbyDialog_Do==true){this.StandbyDialog_Obj.show()}",200);
}
}catch(e){
console.log("StandbyDialog_Started:"+e.toString())
}
}
and
dojo.subscribe( 'partialrefresh-start', null, function( method, form, refreshId ){
StandbyDialog_Do=true
StandbyDialog_Started(refreshId)
});
I didn't test it, but I hope it can help you to go further.

How to find out if a user is dragging a tab

I am trying to figure out, if a user is dragging a tab, any tab. I don't care which tab it is, I just need to know, if any tab is being dragged.
What is the best way to do this?
Please note: I asked a similar question. However, in that other question I wanted to know, when dragging stopped so I could perform my move operation. The solution given there (retrying until it works) doesn't seem to apply to this new question.
There is no way to tell whether any tab is being "dragged" (=mouse button held down on a tab).
If you want to know that a tab drag has occurred (opposed to "is about to happen"), then you could use the chrome.tabs.onMoved (moved within a tab) and/or chrome.tabs.onAttached / chrome.tabs.onDetached events.
I built a solution based on the fact that Chrome doesn't allow the moving of tabs in the window that contains a tab that is currently being dragged.
In this case, chrome.runtime.lastError.message will be Tabs cannot be edited right now (user may be dragging a tab).
I utilize this by getting the first tab of the focused window and moving it to its index. Because I use its own index, there isn't actually a visual change, when the operation succeeds.
var Chrome = {
isUserDragging: function (callback) {
chrome.windows.getAll({ populate: true }, function (windows) {
var window = windows.filter(function (x) {
return x.type === 'normal' && x.focused && x.tabs
&& x.tabs.length;
})[0];
if (window === undefined)
return;
var tab = window.tabs[0];
chrome.tabs.move(tab.id, { index: tab.index }, function () {
callback(
chrome.runtime.lastError !== undefined &&
chrome.runtime.lastError.message.indexOf('dragging') !== -1);
});
});
}
}
Usage would be:
Chrome.isUserDragging(function(userIsDragging) {
if(userIsDragging)
// do something
else
// do something else
});
Now, based on this, I built a polling mechanism using setTimeout that checks periodically if the user is still dragging and executes an action, when the user stopped dragging.
The full implementation can be seen here and it uses these two helper classes.

Best way to deal with document locking in xPages?

What is the best way to deal with document locking in xPages? Currently we use the standard soft locking and it seems to work fairly well in the Notes client.
In xPages I considered using the "Allow Document Locking" feature but I am worried that people would close the browser without using a close or save button then the lock would never be cleared.
Is there a way to clear the locks when the user has closed his session? I am seeing no such event.
Or is there an easier way to have document locking?
I realize I can clear the locks using an agent but when to run it? I would think sometime a night then I am fairly certain the lock should no longer really be active.
Here is code I'm using:
/* DOCUMENT LOCKING */
/*
use the global object "documentLocking" with:
.lock(doc) -> locks a document
.unlock(doc) -> unlocks a document
.isLocked(doc) -> returns true/false
.lockedBy(doc) -> returns name of lock holder
.lockedDT(doc) -> returns datetime stamp of lock
*/
function ynDocumentLocking() {
/*
a lock is an entry in the application scope
with key = "$ynlock_"+UNID
containing an array with
(0) = username of lock holder
(1) = timestamp of lock
*/
var lockMaxAge = 60 * 120; // in seconds, default 120 min
this.getUNID = function(v) {
if (!v) return null;
if (typeof v == "NotesXspDocument") return v.getDocument().getUniversalID();
if (typeof v == "string") return v;
return v.getUniversalID();
}
/* puts a lock into application scope */
this.lock = function(doc:NotesDocument) {
var a = new Array(1);
a[0] = #UserName();
a[1] = #Now();
applicationScope.put("$ynlock_"+this.getUNID(doc), a);
// print("SET LOCK "+"$ynlock_"+doc.getUniversalID()+" / "+a[0]+" / "+a[1]);
}
/* removes a lock from the application scope */
this.unlock = function(doc:NotesDocument) {
applicationScope.put("$ynlock_"+this.getUNID(doc), null);
//print("REMOVED LOCK for "+"$ynlock_"+doc.getUniversalID());
}
this.isLocked = function(doc:NotesDocument) {
try {
//print("ISLOCKED for "+"$ynlock_"+doc.getUniversalID());
// check how old the lock is
var v = applicationScope.get("$ynlock_"+this.getUNID(doc));
if (!v) {
//print("no lock found -> return false");
return false;
}
// if lock holder is the current user, treat as not locked
if (v[0] == #UserName()) {
//print("lock holder = user -> not locked");
return false;
}
var dLock:NotesDateTime = session.createDateTime(v[1]);
var dNow:NotesDateTime = session.createDateTime(#Now());
// diff is in seconds
//print("time diff="+dNow.timeDifference(dLock)+" dLock="+v[1]+" now="+#Now());
// if diff > x seconds then remove lock, it not locked
if (dNow.timeDifference(dLock) > lockMaxAge) {
// print("LOCK is older than maxAge "+lockMaxAge+" -> returning false");
return false;
}
//print("return true");
return true;
// TODO: check how old the lock is
} catch (e) {
print("ynDocumentLocking.isLocked: "+e);
}
}
this.lockedBy = function(doc:NotesDocument) {
try {
var v = applicationScope.get("$ynlock_"+this.getUNID(doc));
if (!v) return "";
//print("ISLOCKEDBY "+"$ynlock_"+doc.getUniversalID()+" = "+v[0]);
return v[0];
} catch (e) {
print("ynDocumentLocking.isLockedBy: "+e);
}
}
this.lockedDT = function(doc:NotesDocument) {
try {
var v = applicationScope.get("$ynlock_"+this.getUNID(doc));
if (!v) return "";
return v[1];
} catch (e) {
print("ynDocumentLocking.isLockedBy: "+e);
}
}
}
var documentLocking = new ynDocumentLocking();
You could take a page from the way webDAV works. There a servlet manages a "lock-list" of locked documents. The locks automatically expire after 10 minutes. Locks can be renewed or terminated trough calls. So when you edit a document you would request a lock, then kick off a CSJS timer that calls the relocking function every 8 minutes (so you have some margin for error) and the postSave calls the unlock (unless you stay in edit mode).
If a user closes the browser after 10 minutes the document is automatically unlocked. Since you are free how to implement the locking function, you can capture user/location and use that information in the "lock failed" display (you event could push that further and let the original author know about it or do some "retry" option.
It isn't simple to implement, but once implemented simple to use
ApplicationScope may be a good place to capture "locked" documents. After all, for applicationScope to expire, all users' sessions have to have expired, so anyone with the page open will not be able to save anyway.
Maybe capture UNID, user and time when someone edits a doc. Clear the value when the document is saved. Bear in mind that the user might close the browser etc. I've been discussing this approach internally and if we end up building this I would look to add it to OpenNTF. But we're unlikely to get onto it within the next month.
I Prefer to use a solution similar to Mr. Withers' answer. The main issue is how to deal with the unwanted and dreaded back button. It is easy to lock a document when it is opened, but there are many ways to close the XPage, and the user is not limited to just the navigation you provide but also can, as he stated, close the browser completely, use the back button, etc. So, the best way that I can think of is to create a few java objects which we will use in the application and session scopes.
The first step is to create a "LockedDocument" class. As we know, the documents are not serializable and we do not want to save the document itself in this object, we want to save the UNID and the time it was saved. We want to do it this way so that we can manage to clear the object after a given time (like thirty minutes to an hour). This class should also implement the comparable interface in order to sort the collection by this time so that the oldest documents are first and the newest documents are last.
Next we create another class that holds a list or a map with these LockedDocuments. This class must also have a thread (implement Runnable) that will check all documents every five minutes or so, I did not test this yet, but it should work). Any document that was locked thirty to sixty minutes ago (predefined) will be unlocked (deleted from the list). It is important that the list be sorted as described above and that the loop is "broken" when a time less than the locktime is reached in order to prevent unwanted processing.
The next step would be to include the user specific list in the sessionScope. This list is the LockedDocuments that this current user has. It is set when the user changes the document's status to editable, and is checked before the document is set to editable to prevent one document from being opened in multiple tabs by the same user. The lock is once again checked onquerysave(). Once a main page is opened, the lock is automatically released. The onquerysave() must also check to make sure the documents UNID is in the sessionScope list, or if the document is new before allowing a save.
quick recap
Any UNID saved in the applicationScope LockedDocumentList would not be editable by anyone unless it exists in their own sessionScope list.
It is possible to warn a user that their lockedTime is approaching and reset the timer.
The class containing a list with the locked documents must be a singleton
There are probably ways to improve this answer, and I am sure I am missing something. It is just a thought.
There might be a better way to handle this, but it is the best I found.
You can remove the Domino lock in window.onunload event:
window.onunload = function(){
dojo.xhrGet(...
}
No need to reinvent the wheel.

How do I store values for specific tab for Chrome, and Safari extensions?

I am developing an extension for all the browsers. How do I store tab specific values in the session? I solved this problem in Firefox with an NSISessionStore object. In Safari and Google Chrome, I used SessionStorage; this object stores values for a specific tab with a specific domain. I want a solution for how to store values for a specific tab.
If you're asking how to manage data throughout the life of a tab you can simply create an object for the tab when it's created and delete it when it is closed.
// Create data store
var tabDataStore = {};
// Create listeners
chrome.tabs.onCreated.addListener(function (tab) {
tabDataStore['tab_' + tab.id] = {
urls: []
};
});
chrome.tabs.onRemoved.addListener(function (tabId) {
delete tabDataStore['tab_' + tabId];
});
// Save something against that tab's data
function saveUrl(tab) {
tabDataStore['tab_' + tab.id].urls.push(tab.url);
}
// Load something from tab's data
function loadOriginalUrl(tab) {
tabDataStore['tab_' + tab.id].urls[0];
}
However, this is all an assumption and you may want something completely different. Also, it depends when and what exactly you want to store.
Further information on tabs can be found in the official documentation.
If you want to persist anything you can use localStorage.
a simple way to do it, though not ideal as it would look messy would be to store the values in the URL hash
say the URL of the tab was http://whatever.com/
you could store the value in the hash like so
http://whatever.com/#value1=12&value2=10&value3=15212
there will also be a problem if the website uses the hash object for anyhting such as "in page" anchors, or ajaxy type stuff
Safari Answer
In your global page save directly to the tab.. so for instance on message from injected script
// global page
safari.application.addEventListener("message", function(event){
switch(event.name){
case "saveData":
event.target.page.tabData = { data: myData }
break;
case "getData":
event.target.page.dispatchMessage("tabData", myData);
break;
}
}, false);
-
// injected page
// first save data
safari.self.tab.dispatchMessage("saveData", {firstname:"mike", age: 25} );
// setup listner to recevie data
safari.self.addEventListener("message", function(event){
switch(event.name){
case "tabData":
// get data for page
console.debug(event.message);
// { firstname: "mike", age: 25 }
break;
}
}, false);
// send message to trigger response
safari.self.tab.dispatchMessage("getData", {} );

Implementing a blocking modal view/dialog like in Windows Forms - is it possible?

In short:
I want to show a view or action sheet and only continue code execution after the user has dismissed the view / sheet. So: line one shows the view, line two reads some result variable.
In detail why I would need this:
I'm porting a Windows Forms application over to the iPad. The original implementation has a communication class which uses a web service to communicate with the server. It offers a couple of methods to get data. Conveniently it checks prior to each call if the user still has a valid connection or if he has to re-enter his password for security reasons.
If the password is required, the .NET class shows a modal dialog which blocks any further code executio and if the password was entered, retries the last call it has made before showing the dialog.
Now using CocoaTouch I'm facing a problem. I replaced the code that shows the dialog with a UIActionSheet. Works great but code execution continues immediately, whereas in Windows Forms it is blocked (the next line in Windows Forms after showing the dialogs is to read the entered password from the dialog) until the dialog has been closed.
I tried a Thread.Sleep() until the user dismisses the UIActionSheet but the Thread.Sleep() also blocks the main loop and my view won't even be drawn.
The alternative I currently see is to change all methods in the already working class and give them a return value: if password required, handle it, then retry.
But this means that all over my code I will have to add these checks because at any given moment the password might be needed. That's why it is nested in communication class in Windows Forms.
Any other ideas?
René
Yes, it is possible.
To do this, what you can do is to run the mainloop manually. I have not managed to stop the mainloop directly, so I instead run the mainloop for 0.5 seconds and wait until the user responds.
The following function shows how you could implement a modal query with the above approach:
int WaitForClick ()
{
int clicked = -1;
var x = new UIAlertView ("Title", "Message", null, "Cancel", "OK", "Perhaps");
x.Show ();
bool done = false;
x.Clicked += (sender, buttonArgs) => {
Console.WriteLine ("User clicked on {0}", buttonArgs.ButtonIndex);
clicked = buttonArgs.ButtonIndex;
};
while (clicked == -1){
NSRunLoop.Current.RunUntil (NSDate.FromTimeIntervalSinceNow (0.5));
Console.WriteLine ("Waiting for another 0.5 seconds");
}
Console.WriteLine ("The user clicked {0}", clicked);
return clicked;
}
I think this approach using async/await is much better, and doesn't suffer from freezing the app when rotating the device, or when the autoscrolling interferes and leaves you stuck in the RunUntil loop forever without the ability to click a button (at least these problems are easy to reproduce on iOS7).
Modal UIAlertView
Task<int> ShowModalAletViewAsync (string title, string message, params string[] buttons)
{
var alertView = new UIAlertView (title, message, null, null, buttons);
alertView.Show ();
var tsc = new TaskCompletionSource<int> ();
alertView.Clicked += (sender, buttonArgs) => {
Console.WriteLine ("User clicked on {0}", buttonArgs.ButtonIndex);
tsc.TrySetResult(buttonArgs.ButtonIndex);
};
return tsc.Task;
}

Resources