Skip to content

Instantly share code, notes, and snippets.

@Lexty
Last active February 4, 2016 11:50
Show Gist options
  • Save Lexty/4aba5332dbcdfe36b8c5 to your computer and use it in GitHub Desktop.
Save Lexty/4aba5332dbcdfe36b8c5 to your computer and use it in GitHub Desktop.
JavaScript class for manage parameters in hash part of URL
/**
* @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