Skip to content

Instantly share code, notes, and snippets.

@slickplaid
Created March 21, 2012 19:18
Show Gist options
  • Save slickplaid/2151586 to your computer and use it in GitHub Desktop.
Save slickplaid/2151586 to your computer and use it in GitHub Desktop.
Focus next tab item on fieldset exit
$(document).on('focus', '.message-form .note textarea, .message-form .subject input', function() {
var textarea = $(this); // or input subject
var tab = $('.content-links').find('.selected');
// make sure none of the inputs are those in the note form
var formElements = $(this).parents('form').find('input');
var inputElement = $(this).parents('form').find('input[type=submit]');
var bindings = $('a, input').not(formElements);
bindings.bind('click.preventNavigation', function(e) {
if(textarea.val() !== '') {
if(e.isDefaultPrevented()) {
// if there's already an event handler on the element that is preventing the default action
noteConfirmation(textarea, tab, bindings, false);
} else {
// if there isn't an event handler, we want to allow them to continue with the action, so we pass it along
e.preventDefault();
noteConfirmation(textarea, tab, bindings, $(this));
}
} else {
// if note is empty, we don't want to do anything so remove any trace this was here
bindings.unbind('click.preventNavigation');
}
});
inputElement.bind('click.cancelNotePrompt', function(e) {
// reset everything so it won't be called again
inputElement.unbind('click.cancelNotePrompt');
bindings.unbind('click.preventNavigation');
});
});
function noteConfirmation(textarea, tab, bindings, clickedElement) {
// unbind the event so it won't register any more clicks until they reblur the textarea again
bindings.unbind('click.preventNavigation');
var html = [
'<fieldset class="inline-form note-confirmation">',
'<legend class="inline-form-head">Message Confirmation</legend>',
'<div class="content-alerts content-errors">',
'<h4 class="title">You have an message that was not saved or sent...</h4>',
'</div>',
'<p class="form-actions">',
'<a class="action note-back" href="#">Please take me back!</a>',
'<a class="cancel-link" href="#">cancel</a>',
'</p>',
'</fieldset>'].join('');
if($('.note-confirmation').length === 0) {
// we want to make sure only one overlay gets created
$('body').append(html);
}
$('.note-confirmation').one('click', 'a', function(e) {
var $this = $(this);
e.preventDefault(); // we'll stop the links in the confirmation box from doing anything normally
// prompt gets removed/hidden regardless of what they select
$('.note-confirmation').hide().remove();
if($this.hasClass('note-back')) { // we still have reference to the removed object
// return them to the previous tab (if applicable) and focus the textarea
tab.find('a').click();
} else {
if(clickedElement) { // return the normal browsing path to whatever they were doing
if(clickedElement.is('a')) {
var url = clickedElement.attr('href');
window.location.assign(url);
} else {
clickedElement.click(); // force the normal action
}
}
}
});
}
$(document).on('keydown.tab', '.field-group input, .field-group select, .field-group textarea, .field-group h4', function(e) {
if(e.which === 9) { // tab key
var $this=$(this);
var $fieldGroup = $this.parents('.field-group');
var focusedElement = $this.get(0);
var id = $fieldGroup.attr('id');
var lastElement = $fieldGroup.find('input, select, textarea, button, h4');
lastElement = lastElement.not(':hidden');
lastElement = lastElement.filter(function() {
return $(this).css('visibility') !== 'hidden';
});
lastElement = lastElement.last().get(0);
if(focusedElement === lastElement) {
var $fieldGroupIndex = $('.field-group-index');
var $tab = $fieldGroupIndex.find('li').find('a[href$=#'+id+']');
var $nextTab = $tab.parent('li').next().find('a');
var lastFGElement = $fieldGroupIndex.find('li').last().find('a').get(0);
var tabElement = $tab.get(0);
if(lastFGElement !== tabElement && $tab.length) {
e.preventDefault();
$nextTab.focus();
}
}
}
});
// As of 2011-11-22, GCF has an outstanding issue where tab index is not maintained properly. This causes the tab focus
// to switch to the first possible tabbable element the first time TAB is pressed, regardless of what other element was
// previously focused. Once the tab key is pressed the first time, tabbing occurs naturally. This behavior resets each
// time a page is navigated to in GCF. (If GCF registry entry "HandleTopLevelRequests" is set to "0", then the behavior
// does not reset on a simple navigation, but does reset with new windows, etc).
// This workaround adds a handler for focusout (which is triggered when the user tabs out of an element) capturing which
// element was last focused. It then checks if the focus is traversing TO the first possible tabbale element (a div with
// ID: ChromeFrameWorkaroundDiv, which is inserted into the DOM programatically) and if so, refocuses the previously
// focused element. It assumes any time the ChromeFrameWorkaroundDiv div is focussed, it was erroneous. It also assumes
// the ChromeFrameWorkaroundDiv div is the first tabbable element in the DOM.
// See http://code.google.com/p/chromium/issues/detail?id=102177 for the current status of the issue as tracked at the
// Chromium project.
// This workaround uses the jQuery framework.
function addChromeFrameFocusWorkaround() {
// Only apply workaround if we are in Google Chrome Frame (window.externalHost is the indicator)
if (navigator.userAgent.indexOf("Chrome") != -1 && window.externalHost) {
$(document).ready(function() {
// Add the empty div to the DOM
$("body").prepend("<div style='width: 0; height: 0;' tabindex='1' id='ChromeFrameWorkaroundDiv'></div>");
// Track the element that is losing focus
var lastFocus = null,
fsElem = null,
index = null,
newFocus = null,
h4Parent = null;
$(document).focusout(function(evt) {
if (evt && evt.target) {
lastFocus = $(evt.target);
// we will get next target rather than move back to the original selection
fsElem = lastFocus.parents('body').find('a, input, textarea, select, h4').not('[tabindex="-1"]');
index = fsElem.index(lastFocus);
if(lastFocus.is('h4')) {
h4Parent = lastFocus.parents('.autocomplete-results');
fsElem = h4Parent.find('h4');
index = fsElem.index(lastFocus);
}
newFocus = fsElem.eq(index+1);
}
});
// When focus is given to the ChromeFrameWorkaroundDiv, give it back to the previous element.
$("#ChromeFrameWorkaroundDiv").focusin(function() {
if(newFocus && index > -1) {
newFocus.focus();
} else {
lastFocus.focus(); // we'll refocus the last focused item in case something goes wrong.
// That way people won't lose their mind having to move their hand to the mouse to refocus and keep moving
}
if(h4Parent) setTimeout(function() {
h4Parent.show();
}, 10); // probably could drop this, but at 10ms, it's almost unnoticable
// basically makes it guaranteed that it'll fire after the hide event is called `ie: nextTick()`
})
});
}
}
addChromeFrameFocusWorkaround();
// define keys
var keymap = {
'copy' : 119, // F8
'edit' : 120, // F9
'add' : 115, // F4
'left' : 37,
'up' : 38,
'right': 39,
'down' : 40
}
// map keys
keymap.allKeys = [];
for(var key in keymap) {
keymap.allKeys.push(keymap[key]);
}
var optNumber = false; // for arrow keys on edit button
// keydown.shortcuts is namespaced so we can disable them if needed by calling:
// $(document).off('keydown.shortcuts');
$(document).on('keydown.shortcuts', function(e) {
// check to see if key used is a shortcut key
if(keymap.allKeys.indexOf(e.which) > -1) {
var action = false;
if(e.which === keymap.copy) {
action = $('.copy-action');
followUrl(action);
handleDropdown(action);
}
if(e.which === keymap.edit) {
action = $('.edit-action');
followUrl(action);
handleDropdown(action);
}
if(e.which === keymap.add) {
action = $('.add-action');
followUrl(action);
handleDropdown(action);
}
}
function followUrl(action) {
// if action has href, follow it
// click() doesn't work on these for whatever reason
var url = action.eq(0).attr('href');
if(url) window.location.assign(url);
}
function handleDropdown(action) {
if(!action.hasClass('action-group'))
action = action.parent('.action-group');
action.toggleClass('active');
if(action.hasClass('active'))
action.find('.action-options').find('.option a').eq(0).focus();
// hide drop down if you click anywhere on the page
$(document).one('click', function() {
action.removeClass('active');
});
// event focus.actionLinks takes over from here
}
});
$(document).on('focus.actionLinks', '.action-options .option a', function() {
var $this = $(this);
var option = $this.parent('.option');
var previous = option.prev().find('a');
var next = option.next().find('a');
arrowKeys();
function arrowKeys() {
$(document).one('keydown.actionLinkKeys', function(e){
if(e.which === keymap.up) {
if(previous.length) {
e.preventDefault();
previous.focus();
} else {
e.preventDefault(); // stop the page from moving down/up
arrowKeys();
}
}
if(e.which === keymap.down) {
if(next.length) {
e.preventDefault();
next.focus();
} else {
e.preventDefault(); // stop the page from moving down/up
arrowKeys();
}
}
if(e.which === keymap.right) {
e.preventDefault();
// $this.click();
}
if(e.which === keymap.left) {
e.preventDefault();
// option.parents('.action-group').removeClass('active');
}
});
}
});
// DAR BE HORRIBLE, NASTY, NEVER DO THIS NORMALLY STUFF IN HERE, YAR!
// This must be placed at the top (or extremely close to the top) of ALL the javascript to keep ajax from continuing through and changing the page
$(document).on('focus', '.message-form .note textarea', function() {
var textarea = $(this);
$(document).one('click', 'a, input', function(e) {
if(textarea.val() !== '' && !$(this).hasClass('save-action')) {
if(!confirm('You have not submitted your note. Would you like to leave this page?')) {
e.preventDefault(); // don't follow the link
e.stopPropagation(); // don't allow other handlers to continue with ajax
}
}
});
});
$(document).on('keypress', '.date-field .control > label > input', function(e) {
if(e.which === 32) { // space key
// date values
var today = new Date();
var month = today.getMonth()+1;
var day = today.getDate();
var year = today.getFullYear();
var parent = $(this).parents('.date-field');
var $month = parent.find('.month input');
var $day = parent.find('.day input');
var $year = parent.find('.year input');
$month.val(month);
$day.val(day);
$year.val(year);
var nextField = parent.next().find('input, select, textarea').eq(0);
while(nextField.length === 0) {
// we'll traverse the dom tree until we come to the next focusable input
parent = parent.parent();
nextField = parent.next().find('input, select, textarea').eq(0);
}
nextField.focus();
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment