Last active
February 4, 2016 11:50
-
-
Save Lexty/4aba5332dbcdfe36b8c5 to your computer and use it in GitHub Desktop.
JavaScript class for manage parameters in hash part of URL
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @param {{}} [options] | |
* @constructor | |
*/ | |
var Hash = function (options) { | |
options || (options = {}); | |
var hash = options.hasOwnProperty('hash') ? options.hash : window.location.hash; | |
this.options = _.defaults(options, { | |
delimiter: '', | |
state : {}, | |
defaults : {}, | |
arguments: [], | |
parsed : {}, | |
hash : undefined | |
}); | |
if (0 === this.options.arguments.length) { | |
this.options.arguments = Object.keys(this.options.defaults); | |
} | |
this.hash = hash; | |
_.defaults(this.options.state, this.options.defaults); | |
this.events = { | |
'hashChange' : 'hash.change', | |
'paramChange' : 'pararefgsegwerfm.change.', | |
'stateChange' : 'state.change', | |
'defaultChange': 'default.change' | |
}; | |
}; | |
Hash.prototype = { | |
/** | |
* Возвращает или сохраняет значения состяния. | |
* * Вызов без аргументов - возвращает всю коллекцию значений состояния; | |
* * Вызов с одним аргументом-строкой - возвращает значение по ключу переданного аргумента; | |
* * Вызов с одним аргументом-объектом - сливает переданный объект с коллекцией состояний; | |
* * Вызов с двумя аргументами - сохраняет значение второго аргумента под ключом первого. | |
* @param {String|{}} [key] | |
* @param {String|Number} [value] | |
* @param {Boolean} [silence] Если `true`, то при изменении не будет генерироваться событие `state.change`. | |
* @returns {*} | |
* @public | |
*/ | |
state: function(key, value, silence) { | |
if (typeof(value) === 'undefined' && !_.isObject(key)) { | |
return this.getState(key); | |
} else { | |
return this.setState(key, value, silence); | |
} | |
}, | |
/** | |
* Возвращает всю коллекцию состояний (если вызвать без аргумента) или одно значение состояния (если передать ключ значения). | |
* @param {String} [key] | |
* @returns {*} | |
*/ | |
getState: function(key) { | |
if (!key) { | |
return this.options.state; | |
} else { | |
return this.options.state[key]; | |
} | |
}, | |
/** | |
* Сохраняет переданное значение по ключу или сливает весь объект с текущим состоянием. | |
* @param {String|{}} key Ключ значения или объект. | |
* @param {String|Boolean} [value] Значение. | |
* @param {Boolean} [silence] Если `true`, то при изменении не будет генерироваться событие `state.change`. | |
* @returns {Window.app.Hash} | |
* @public | |
*/ | |
setState: function(key, value, silence) { | |
if (_.isObject(key) && typeof silence === 'undefined' && typeof value === 'boolean') { | |
silence = value; | |
value = null; | |
} | |
if (_.isObject(key) && !value) { | |
this.options.state = $.extend({}, this.options.state, key); | |
} else if (!_.isObject(key) && typeof value !== 'undefined') { | |
this.options.state[key] = value; | |
} else { | |
throw Error(); | |
} | |
silence || (this.trigger(this.events.stateChange, [this])); | |
return this; | |
}, | |
/** | |
* Проверяет наличие ключа у коллекции состояния. | |
* @param {String} key | |
* @returns {boolean} | |
*/ | |
hasState: function(key) { | |
return this.options.state.hasOwnProperty(key); | |
}, | |
/** | |
* Сравнивает передаваемое значение со значением параметра. | |
* @param {string} key Имя параметра. | |
* @param {*} value Значение с которым идет сравнение. | |
* @param {boolean} [strict] Строгое или не строгое сравнение (по умолчанию строгое). | |
* @returns {boolean} | |
*/ | |
eq: function(key, value, strict) { | |
strict || (strict = true); | |
return (strict && this.getState(key) === value) || (!strict && this.getState(key) == value) | |
}, | |
/** | |
* Меняет значение параметра на следующее в массиве/объекте или на первое, если последнее было изначально. | |
* @param {string} key Имя параметра. | |
* @param {Array|Object} values Массив или объект значений. | |
* @param {boolean} [silence] Генерировать событие смены состояния или нет. | |
* @returns {Window.app.Hash} | |
*/ | |
toggle: function(key, values, silence) { | |
var value; | |
if (_.isObject(values)) { | |
var keys = Object.keys(values); | |
var len = keys.length; | |
for (var i = 0; i < len; i++) { | |
if (this.getState(key) === values[keys[i]]) { | |
if (i < len - 1) { | |
value = values[keys[i + 1]]; | |
} else { | |
value = values[keys[0]]; | |
} | |
} | |
} | |
} else if (_.isArray(values)) { | |
var i = values.indexOf(this.getState(key)); | |
if (-1 !== i && i === values.length - 1) { | |
value = values[0]; | |
} else if (-1 !== i) { | |
value = values[i + 1]; | |
} | |
} else { | |
throw Error('Hash: Argument "values" must be key-value Object or Array. "{0}" given.'.format(typeof values)); | |
} | |
if (!value) { | |
throw Error('Hash: Parameter "{0}" does not exists.'.format(key)); | |
} | |
this.setState(key, value, silence); | |
return this; | |
}, | |
/** | |
* Если не передавать аргумент, то проверит, есть ли разница между состоянием объекта и хешем адресной строки браузера. | |
* Если передать имя параметра - проверит только этот параметр. | |
* @param {string} [key] Имя параметра. | |
* @returns {boolean} | |
*/ | |
isChanged: function(key) { | |
if (typeof key === 'undefined') { | |
return _.isEmpty(this.options.parsed); | |
} else { | |
return this.options.state[key] !== this.options.parsed[key]; | |
} | |
}, | |
/** | |
* Сбрасывает одно или несколько значений состояния (если был передан ключ или массив ключей) | |
* или все значения состояния (если ничего не передано). | |
* @param {String|Array|Boolean} [key] | |
* @param {Boolean} [silence] | |
* @returns {Window.app.Hash} | |
*/ | |
reset: function(key, silence) { | |
if (typeof silence === 'undefined' && typeof key === 'boolean') { | |
silence = key; | |
key = ''; | |
} else if (typeof key === 'undefined') { | |
silence = false; | |
} | |
if (_.isArray(key)) { | |
var len = key.length; | |
for (var i = 0; i < len; i++) { | |
this.reset(key[i], silence); | |
} | |
} else if (key) { | |
this.setState(key, this.getDefault(key), silence); | |
} else { | |
this.setState(this.getDefault(), silence); | |
} | |
return this; | |
}, | |
/** | |
* Проверяет, является ли значение состояния значением по умолчанию. | |
* @param {String} key | |
* @returns {boolean} | |
*/ | |
isDefault: function(key) { | |
return this.hasState(key) && this.getState(key) === this.getDefault(key); | |
}, | |
/** | |
* Возвращает или сохраняет значения состяния по умолчанию. | |
* * Вызов без аргументов - возвращает всю коллекцию значений состояния по умолчанию; | |
* * Вызов с одним аргументом-строкой - возвращает значение по ключу переданного аргумента; | |
* * Вызов с одним аргументом-объектом - сливает переданный объект с коллекцией состояний по умолчанию; | |
* * Вызов с двумя аргументами - сохраняет значение второго аргумента под ключом первого. | |
* @param {String|{}} [key] | |
* @param {String} [value] | |
* @param {Boolean} [silence] Если `true`, то при изменении не будет генерироваться событие `default.change`. | |
* @returns {*} | |
* @public | |
*/ | |
default: function(key, value, silence) { | |
if (typeof(value) === 'undefined' && !_.isObject(key)) { | |
return this.getDefault(key); | |
} else { | |
return this.setDefault(key, value, silence); | |
} | |
}, | |
/** | |
* Возвращает всю коллекцию состояний по умолчанию (если вызвать без аргумента) или одно значение состояния по умолчанию | |
* (если передать ключ значения). | |
* @param {String} [key] | |
* @returns {*} | |
* @public | |
*/ | |
getDefault: function(key) { | |
if (!key) { | |
return this.options.defaults; | |
} else { | |
return this.options.defaults[key]; | |
} | |
}, | |
/** | |
* Сохраняет переданное значение по ключу или сливает весь объект с текущим состоянием по умолчанию. | |
* @param {String|{}} key Ключ значения или объект. | |
* @param {String|Boolean} [value] Значение. | |
* @param {Boolean} [silence] Если `true`, то при изменении не будет генерироваться событие `default.change`. | |
* @returns {Window.app.Hash} | |
* @public | |
*/ | |
setDefault: function(key, value, silence) { | |
if (_.isObject(key) && typeof silence === 'undefined' && typeof value === 'boolean') { | |
silence = value; | |
value = null; | |
} | |
if (_.isObject(key) && !value) { | |
$.extend(this.options.defaults, options); | |
} else if (!_.isObject(key) && typeof value !== 'undefined') { | |
this.options.defaults[key] = value; | |
} else { | |
throw Error(); | |
} | |
silence || (this.trigger(this.events.defaultChange, [this])); | |
return this; | |
}, | |
/** | |
* Проверяет наличие ключа у коллекции состояния по умолчанию. | |
* @param {String} key | |
* @returns {boolean} | |
*/ | |
hasDefault: function(key) { | |
return this.options.default.hasOwnProperty(key); | |
}, | |
/** | |
* Возвращает или сохраняет разделитель между ключом и значением в адресной строке. | |
* @param {String} [delimiter] | |
* @returns {String|Window.app.Hash} | |
* @public | |
*/ | |
delimiter: function(delimiter) { | |
if (typeof(delimiter) === 'undefined') { | |
return this.getDelimiter(); | |
} else { | |
return this.setDelimiter(delimiter); | |
} | |
}, | |
/** | |
* Возвращает разделитель между ключом и значением в адресной строке. | |
* @returns {String} | |
* @public | |
*/ | |
getDelimiter: function() { | |
return this.options.delimiter | |
}, | |
/** | |
* Сохраняет разделитель между ключом и значением в адресной строке. | |
* @param {String} delimiter | |
* @returns {Window.app.Hash} | |
* @public | |
*/ | |
setDelimiter: function(delimiter) { | |
this.options.delimiter = delimiter; | |
return this; | |
}, | |
/** | |
* Парсит строку хеша. | |
* @param {String} [hash] | |
* @param {Boolean} [silence] Если `true`, то при изменении не будет генерироваться событие `state.change`. | |
* @returns {Window.app.Hash} | |
* @see window.app.Hash.parse parse | |
* @public | |
*/ | |
parseHash: function(hash, silence) { | |
hash || (hash = window.location.hash); | |
this.options.state = _.defaults(this._parse(this.options.arguments, hash, this.options.delimiter), this.options.defaults); | |
this.options.parsed = $.extend({}, this.options.state); | |
silence || (this.trigger(this.events.stateChange, [this])); | |
return this; | |
}, | |
/** | |
* Алиас метода `parse`. | |
* @param {String} [hash] | |
* @param {Boolean} [silence] Если `true`, то при изменении не будет генерироваться событие `state.change`. | |
* @returns {Window.app.Hash} | |
* @see window.app.Hash.parseHash parseHash | |
* @public | |
*/ | |
parse: function(hash, silence) { | |
return this.parseHash(hash, silence); | |
}, | |
/** | |
* Изменяет хеш в адресной строке в соостветствии с состоянием объекта. | |
* Генерирует событие `hash.change`, если не передан второй аргумент со значением `true`. | |
* @param {{}|Boolean} [params] Параметры, для изменеия состояния, или `true`/`false` аргумент `silence`. | |
* @param {Boolean} [silence] Если true, то при изменении не будет генерироваться событие `hash.change`. | |
* @returns {Window.app.Hash} | |
* @see window.app.Hash.change change | |
* @public | |
*/ | |
make: function(params, silence) { | |
var self = this; | |
if (typeof silence === 'undefined' && typeof params === 'boolean') { | |
silence = params; | |
params = {}; | |
} else { | |
params || (params = {}); | |
} | |
var fragments = []; | |
this.setState(params); | |
_.defaults(this.options.state, this.options.defaults); | |
if (!silence) { | |
_.each(this.options.state, function(value, name) { | |
if(value !== self.options.parsed[name]) { | |
self.trigger(self.events.paramChange + name, [self, value]); | |
self.options.parsed[name] = value; | |
} | |
}); | |
} | |
params = this._removeDefaults(this.getState()); | |
_.each(params, function(value, name) { | |
fragments.push(name + self.options.delimiter + self._escape(value)); | |
}); | |
this._setHash(fragments.join('/'), silence); | |
return this; | |
}, | |
/** | |
* Алиас метода `make`. | |
* @param {{}} [params] Параметры, для изменеия состояния. | |
* @param {Boolean} [silence] Если true, то при изменении не будет генерироваться событие `hash.change`. | |
* @returns {Window.app.Hash} | |
* @see window.app.Hash.make make | |
* @public | |
*/ | |
change: function(params, silence) { | |
return this.make(params, silence); | |
}, | |
/** | |
* Генерирует событие у данного объекта. | |
* @param {String} event Событие. | |
* @param {Array|{}} [args] Дополнительные параметры, которые будут отправлены с событием. | |
* @returns {Window.app.Hash} | |
* @public | |
*/ | |
trigger: function(event, args) { | |
$(this).trigger(event, args); | |
return this; | |
}, | |
/** | |
* Подписывается на событие данного объекта. | |
* @param {String} event Событие. | |
* @param {Function} handler Функция, которая будет выполнена, когда произойдет событие. | |
* @returns {Window.app.Hash} | |
* @public | |
*/ | |
on: function(event, handler) { | |
$(this).bind(event, handler); | |
return this; | |
}, | |
/** | |
* Отменяет подписку на событие данного объекта. | |
* @param {String} event Событие. | |
* @returns {Window.app.Hash} | |
* @public | |
*/ | |
off: function(event) { | |
$(this).unbind(event); | |
return this; | |
}, | |
/** | |
* Подписывается на событие изменения в хеше определенного значения. | |
* @param {String} name Имя параметра. | |
* @param {String} [listener] Идентификатор слушателя. | |
* @param {Function} handler Функция, которая будет выполнена, когда произойдет событие. | |
* @returns {Window.app.Hash} | |
* @public | |
*/ | |
onParamChange: function(name, listener, handler) { | |
if (typeof handler === 'undefined') { | |
handler = listener; | |
listener = undefined; | |
} | |
var event = this.events.paramChange + name; | |
if (listener) event += '.' + listener; | |
return this.on(event, handler); | |
}, | |
/** | |
* Отменяет подписку на событие изменения в хеше определенного значения. | |
* @param {String} name Имя параметра. | |
* @param {String} [listener] Идентификатор слушателя. | |
* @returns {Window.app.Hash} | |
* @public | |
*/ | |
offParamChange: function(name, listener) { | |
var event = this.events.paramChange + name; | |
if (listener) event += '.' + listener; | |
return this.off(event); | |
}, | |
/** | |
* @param {{}} [params] | |
* @returns {{}} | |
* @private | |
*/ | |
_removeDefaults: function(params) { | |
var purified = $.extend({}, params); | |
for (var name in purified) { | |
if (purified.hasOwnProperty(name) && this.options.defaults.hasOwnProperty(name) && this.options.defaults[name] == purified[name]) { | |
delete purified[name]; | |
} | |
} | |
return purified; | |
}, | |
/** | |
* @param {String} hash | |
* @param {Boolean} [silence] | |
* @private | |
*/ | |
_setHash: function(hash, silence) { | |
if (hash == '') { | |
// TODO Firefox рандомно ругается на history.pushState() (SecurityError) | |
//history.pushState('', document.title, window.location.pathname); | |
window.location.hash = ''; | |
} else { | |
window.location.hash = hash; | |
} | |
(silence) || this.trigger(this.events.hashChange, [this]); | |
}, | |
/** | |
* @param {String|Number} value | |
* @returns {String} | |
* @private | |
*/ | |
_escape: function(value) { | |
if (!value) return ''; | |
if (typeof value === 'number') return value.toString(); | |
//var escapeChars = ['%', '/', ':']; | |
return value.replace(/%|\/|:/g, encodeURIComponent); | |
}, | |
/** | |
* @param {String} value | |
* @returns {String} | |
* @private | |
*/ | |
_unescape : function(value) { | |
return value.replace(/%25|%2F|%3A/g, decodeURIComponent); | |
}, | |
/** | |
* @param {Array} names | |
* @param {String} hashString | |
* @param {String} delimiter | |
* @returns {{}} | |
* @private | |
*/ | |
_parse : function(names, hashString, delimiter) { | |
var result = {}; | |
var fragments = hashString.substring(1).replace(/^[/]+|[/]+$/g, '').split('/'); | |
var self = this; | |
names.some(function(name, nameIndex) { | |
var search = name + delimiter; | |
fragments.some(function(fragment, fragmentIndex) { | |
if (fragment.indexOf(search) >= 0) { | |
result[name] = self._unescape(fragment.substring(search.length)); | |
return true; | |
} | |
}); | |
}); | |
return result; | |
} | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment