fryr.js
/*!
* Fryr v1.2.2
* Command location.hash like a cook in the kitchen
* MIT License
*/
(function (window, factory) {
'use strict';
if (typeof define === 'function' && define.amd) {
define([], factory(window));
} else if (typeof exports === 'object') {
module.exports = factory(window);
} else {
window.Fryr = factory(window);
}
})(window, function factory(window) {
'use strict';
/**
* Private variable used to hold the callback function
* Necessary for the destory method when we decouple the event listener
* @type {Function}
*/
var hashCallback;
/**
* Remove all traces of a hash if it's blank
* @private
* @param {String} hash - The existing hash
* @see {@link http://stackoverflow.com/a/5298684}
* @fires history.pushState OR window.onhashchange
*/
function removeHashIfBlank(hash) {
if(hash === '#') {
// Prevent scrolling by storing the page's current scroll offset
var scrollV = document.documentElement.scrollTop;
var scrollH = document.documentElement.scrollLeft;
window.location.hash = '';
// Restore the scroll offset
document.documentElement.scrollTop = scrollV;
document.documentElement.scrollLeft = scrollH;
} else {
// If the hash isn't blank, fire onhashchange
window.location.hash = hash;
}
}
/**
* Delete value based on key
* @private
* @param {String} key - Param to target
* @param {String|Number} value - Value to delete from param
*/
function removeValue(key, value) {
var hash = window.location.hash;
// check for key, anything between key and value, the value itself, and optionally the trailing comma
var regex_for_value = new RegExp('(' + key + '=.*)(' + value + ',?)');
// results come back [<search through value>, <search before value>, <value>]
var regex_match = hash.match(regex_for_value);
// replace <search through value> with everything before <value>
// Key is included in search in case multiple keys have the same value
hash = hash.replace(regex_match[0], regex_match[1]);
// Remove trailing commas, ,&, and =,
hash = hash.replace(/,$|(,(?=&))|((?==),)/, '')
removeHashIfBlank(hash);
}
/**
* Add value based on key
* @private
* @param {String} key - Param to target
* @param {String|Number} value - Value to add to param
* @param {Boolean} should_replace_value - if false, value will be appended to the param
* @fires window.onhashchange
*/
function addValue(key, value, should_replace_value) {
var hash = window.location.hash;
var old_value = param(key);
var new_value;
// If it's blank or is the value of the next param
if(old_value.charAt(0) === '&' || old_value === '') {
old_value = key + '=';
new_value = old_value + value;
} else {
old_value = key + '=' + old_value;
// If the value of the param should be replaced, don't append it to the existing value
new_value = should_replace_value ? (key + '=' + value) : (old_value + ',' + value);
}
hash = hash.replace(old_value, new_value);
window.location.hash = hash;
}
/**
* Remove key from hash. Key's value must be removed prior to executing this function
* @private
* @param {String} key - Key to search and destroy
*/
function removeKey(key) {
var hash = window.location.hash;
var key_search = new RegExp('[?&]' + key + '=', 'g');
hash = hash.replace(key_search, '');
// if initial key removed, replace ampersand with question
hash = hash.replace('#&', '#?');
removeHashIfBlank(hash);
}
/**
* @private
* @see {@link Fryr#param documentation in the public `param` function}
*/
function param(key) {
if(!window.location.hash) {
return '';
}
var hash = window.location.hash;
var search = new RegExp('#.*[?&]' + key + '=([^&]+)(&|$)');
var key_value = hash.match(search);
return (key_value ? key_value[1] : '');
}
/**
* Apply value to variable if it has none
* @private
* @param {*} variable Variable to set default to
* @param {*} value - Default value to attribute to variable
* @return {*} Existing value or passed value argument
*/
function setDefault(variable, value){
return (typeof variable === 'undefined') ? value : variable;
}
/**
* Remove key/value if present in hash; add key/value if not present in hash
* @private
* @param {String} key - Param key to query against
* @param {String|Number} value - Value for param key
* @param {Boolean} key_is_required - If the key is not required, it will be removed from the hash
* @param {Boolean} should_replace_value - If false, value will be appended to the key
* @see {@link Fryr#update}
* @see {@link Fryr#append}
* @fires window.onhashchange
*/
function update(key, value, key_is_required, should_replace_value) {
var hash = window.location.hash;
// Ensure key exists in the hash
if(hash.indexOf(key) !== -1) {
var key_value = param(key);
// If the key is required or value isn't blank and is not in key_value
var value_not_in_key_value = key_value.split(',').indexOf(value) === -1;
if (key_is_required || (value_not_in_key_value && value !== '')) {
// add the value, replacing it if necessary
addValue(key, value, should_replace_value);
} else {
// If the value is blank, remove the original value from the key
if(value === '') {
removeValue(key, key_value);
// Otherwise remove the vanilla value if it's different than the original value
// or value should not be replaced (appended)
} else {
if(key_value !== value || !should_replace_value) {
removeValue(key, value);
}
}
// If key's value is blank, remove it from hash
if(param(key) === '') {
removeKey(key);
}
}
// Add key if it doesn't exist
} else {
if(window.location.hash && window.location.hash !== '#') {
window.location.hash += '&' + key + '=' + value;
} else {
// Use a question mark if first key
window.location.hash = '?' + key + '=' + value;
}
}
}
/**
* Call once to initialize filtering
* @param {Function} hashChangeCallback - Called on every hashchange
* @param {Object} Updated params
* @param {Object} [defaults={}] - Key/value pairs for values that should be added on init. Pass {} to skip defaults
* @param {Boolean} [call_on_init=true] - Execute callback on initialization. Always true if defaults is supplied
* @return {Fryr}
*/
function Fryr(hashChangeCallback, defaults, call_on_init) {
defaults = setDefault(defaults, {});
call_on_init = setDefault(call_on_init, true);
// Set for the privateHashChange
var _this = this;
/**
* Very important object holder
* @type {Object}
*/
this.params = {};
/**
* Callback with new params. Callback defined in initialization
* @private
* @fires hashChangeCallback
*/
function privateHashChange() {
var params = _this.parse();
hashChangeCallback.call(_this, params);
}
hashCallback = privateHashChange;
window.addEventListener('hashchange', hashCallback);
// Apply defaults (if present) to hash, which will file window.onhashchange
if( Object.keys(defaults).length && window.location.hash === '' ) {
this.merge(defaults, true);
// Execute the callback on load
} else if(call_on_init) {
privateHashChange.call(this);
}
return this;
}
Fryr.prototype = {
/**
* Replace key/value if present in hash; add key/value if not present in hash
* @param {String} key - Param key to query against
* @param {String|Number} value - Value for param key
* @param {Boolean} [key_is_required=false] - if the key is not required, it will be removed from the hash
* @see {@link Fryr#append}
*/
update: function(key, value, key_is_required) {
key_is_required = setDefault(key_is_required, false);
update(key, value, key_is_required, true);
},
/**
* Add value to key's value in a comma-delineated list if it's not present in hash
* @param {String} key - Param key to query against
* @param {String|Number} value - Value for param key
* @see {@link Fryr#update}
*/
append: function(key, value) {
update(key, value, false, false);
},
/**
* Evaluate the hash
* @return {Object} Key/value hash of the hash broken down by params
*/
parse: function() {
var hash = window.location.hash;
var params;
// clear zombie keys and values
this.params = {};
if(window.location.hash && /\?/g.test(hash)) {
params = hash.split('?')[1];
params = params.split('&');
// Separate params into key values
for(var i = 0; i < params.length; i++) {
var key_value = params[i].split('=');
var key = key_value[0];
var value = key_value[1];
this.params[key] = value;
}
}
return this.params;
},
/**
* Change a JSON object into a string for the hash
* @param {Object|String} obj - object to convert
* @return {String|Boolean} For use in window.location.hash. Returns false if param is not object or string
*/
convert: function(obj) {
if( obj.constructor === String ) {
obj = JSON.parse(obj);
}
// Escape if we're not dealing with an object
if( obj.constructor !== Object ) {
return false;
}
var keys = Object.keys(obj);
// Set start with a ?
var new_hash = '?';
// Loop through all keys in the obj
for(var i = 0; i < keys.length; i++) {
var key = keys[i];
var value = obj[ key ];
if (value === '') {
continue;
}
if( value.constructor === Array ) {
value = value.join(',');
}
// On next key, if it isn't the first, precede with an ampersand
if(new_hash !== '?') {
new_hash += '&';
}
// Append key/value to new_hash
new_hash += key + '=' + value;
}
if (new_hash === '?') {
new_hash = '';
}
return new_hash;
},
/**
* Wipe out or selectively replace keys in params
* @param {Object|String} obj - Query to replace
* @param {Boolean} [replace_all=false] - Whether or not to blast existing params away or replace only changed keys
* @fires window.onhashchange
* @return {String} The new hash
*/
merge: function(obj, replace_all) {
replace_all = setDefault(replace_all, false);
// If it's a string, convert to an object
if( obj.constructor === String ) {
obj = JSON.parse(obj);
}
// Override or add key values from existing params and put them into the object
if( !replace_all ) {
var new_params = this.parse();
var keys = Object.keys(new_params);
for(var i = 0; i < keys.length; i++) {
var key = keys[i];
var value = new_params[ key ];
if(!obj.hasOwnProperty(key)) {
obj[key] = value;
}
}
}
// Change hash to string; if replace_all is false, original value is used
var new_hash = this.convert(obj);
window.location.hash = '#' + new_hash;
return new_hash;
},
/**
* Retrieve a key's value
* @param {String} key - Param to target
* @example
* window.location.hash = '?color=blue'
* fryr.param('color') // => 'blue'
* @return {String} The value of the key
*/
param: function(key) {
return param(key);
},
/**
* Determine if param is blank or undefined
* @param {String} key - Param to target
* @return {Boolean}
*/
paramPresent: function(key) {
var value = this.params[key];
return (typeof value !== 'undefined' && value !== '');
},
/**
* Destroy current initialization, unbind hashchange listener, and reset the hash to an empty state
* @param {Boolean} [retain_hash=false] - Keep items in hash
*/
destroy: function(retain_hash){
retain_hash = setDefault(retain_hash, false);
window.removeEventListener('hashchange', hashCallback);
if(!retain_hash) {
window.location.hash = '';
}
}
};
return Fryr;
});