Skip to content

Instantly share code, notes, and snippets.

@nickolasburr
Last active July 15, 2017 17:32
Show Gist options
  • Save nickolasburr/2f203ecf2f1ffc611dedb9fd4f9565e1 to your computer and use it in GitHub Desktop.
Save nickolasburr/2f203ecf2f1ffc611dedb9fd4f9565e1 to your computer and use it in GitHub Desktop.
Use CSS selectors in fragment identifiers to anchor a webpage on an arbitrary element.
(function () {
'use strict';
// queryTarget hash delimiter
var HASH_DELIM = '#::';
/**
* @ref https://gist.github.com/nickolasburr/9ebee93c0ac155cc25d54eaf44952a0e
*/
/**
* Exception handling methods
*/
var Exception = {};
// throw TypeError exception with message
Exception.throwTypeError = function (message) {
throw new TypeError(message);
};
// throw Error exception with message
Exception.throwGenericError = function (message) {
throw new Error(message);
};
/**
* Utility methods
*/
var Utils = Object.create(Exception);
// coerce `value` to boolean
Utils.toBool = function (value) {
return !!(value);
};
// coerce `value` to number
Utils.toNumber = function (value) {
return +(value);
};
// coerce `value` to string
Utils.toString = function (value) {
return ('' + value);
};
// coerce `value` to array
Utils.toArray = function (value, sep) {
sep = sep || '';
if (!this.isScalar(value)) {
this.throwTypeError('`Utils.toArray` -> `value` must be a scalar, this is a[n] ' + this.getType(value));
}
return this.toString(value).split(sep);
};
// get primitive type of `arg`
Utils.getType = function (arg) {
return (typeof arg);
};
/**
* determine if `arg` is null
*
* @notes This performs strict checking against `arg`,
* so even if `arg` is a falsey value (e.g. '', 0, false, undefined),
* it will only return true if `arg` contains the null object
*/
Utils.isNull = function (arg) {
return this.toBool(arg === null);
};
/**
* determine if `arg` is undefined
*
* @notes Like `Utils.isNull`, this performs a strict checking against `arg`,
* so even if `arg` is a falsey value (e.g. '', 0, false, null),
* it will only return true if `arg` is actually undefined
*/
Utils.isUndefined = function (arg) {
return (this.getType(arg) === 'undefined');
};
/**
* determine if `arg` is defined (syntactic sugar for negated `Utils.isUndefined`)
*
* @notes See `Utils.isUndefined` for important notes
*/
Utils.isDefined = function (arg) {
return !this.isUndefined(arg);
};
/**
* determine if `arg` is an empty string
*/
Utils.isEmpty = function (arg) {
return this.toBool(arg === '');
};
// determine if `source` is an instance of `target`
Utils.isInstanceOf = function (source, target) {
return this.toBool(source instanceof target);
};
/**
* determine if `obj` is of type 'object'
*
* @notes this is a **very** loose check on the type 'object', e.g.
* it will return true for an object literal, object instance,
* array literal, array instance, HTMLElement, Node, and so on...
*/
Utils.isObject = function (obj) {
return this.isInstanceOf(obj, Object);
};
/**
* determine if `obj` is an object constructed from the native
* 'Object' prototype and not a different object constructor
*/
Utils.isObjectNative = function (obj) {
return this.toBool(this.isObject(obj) && Object.getPrototypeOf(obj).constructor.name === 'Object');
};
// determine if object is empty (has zero properties)
Utils.isObjectEmpty = function (obj) {
if (!this.isObject(obj)) {
this.throwTypeError('`Utils.isObjectEmpty` -> `obj` must be an object, not ' + this.getType(obj));
}
return !this.toBool(Object.keys(obj).length);
};
// determine if `needle` is in `haystack`
Utils.inArray = function (needle, haystack) {
if (!this.isArray(haystack)) {
this.throwTypeError('`Utils.inArray` -> `haystack` must be an array, not ' + this.getType(haystack));
}
return this.toBool(haystack.indexOf(needle) > -1);
};
// determine if `arr` is an Array
Utils.isArray = function (arr) {
return this.isInstanceOf(arr, Array);
};
// determine if `func` is a Function
Utils.isFunc = function (func) {
return this.toBool(this.getType(func) === 'function' && this.isInstanceOf(func, Function));
};
// determine if `element` is a valid Element object
Utils.isElement = function (element) {
return this.isInstanceOf(element, Element);
};
// determine if `node` is a valid Node object
Utils.isNode = function (node) {
return this.isInstanceOf(node, Node);
};
// determine if `arg` is a scalar type
Utils.isScalar = function (arg) {
var scalars = [
'string', 'number', 'boolean'
];
return this.inArray(this.getType(arg), scalars);
};
// get parent node from Node object
Utils.getParent = function (node) {
if (!this.isNode(node)) {
this.throwTypeError('`Utils.getParent` -> `node` must be a valid Node object!');
}
return node.parentNode;
};
// get keys from object
Utils.getKeys = function (obj) {
if (!this.isObject(obj)) {
this.throwTypeError('`Utils.getKeys` -> `obj` must be an object, not ' + this.getType(obj));
}
return Object.keys(obj);
};
// get values from object
Utils.getValues = function (obj) {
if (!this.isObject(obj)) {
this.throwTypeError('`Utils.getValues` -> `obj` must be an object, not ' + this.getType(obj));
}
return Object.values(obj);
};
// get index of element in array
Utils.getIndexOf = function (needle, haystack) {
// `needle` must be a scalar type in order for us to perform the lookup
if (!this.isScalar(needle)) {
this.throwTypeError('`Utils.getIndexOf` -> `needle` must be a scalar, this is a[n] ' + this.getType(needle));
}
if (!this.isArray(haystack)) {
this.throwTypeError('`Utils.getIndexOf` -> `haystack` must be an array, not ' + this.getType(haystack));
}
return haystack.indexOf(needle);
};
/**
* @description Get last index of `arr`
* @return {number} Last index
*/
Utils.getLastIndex = function (arr) {
if (!this.isArray(arr)) {
this.throwTypeError('`Utils.getLastIndex` -> `arr` must be an array, not ' + this.getType(arr));
}
return this.toNumber(arr.length - 1);
};
/**
* Query methods
*/
var Query = Object.create(Utils);
/**
* `window.onload` event handler
*/
Query.onLoad = function () {
var onHashChange = this.onHashChange.bind(this);
// attach `window.onhashchange` event listener
window.addEventListener('hashchange', onHashChange, false);
this.setViewport();
};
/**
* `window.onhashchange` event handler
*/
Query.onHashChange = function () {
this.setViewport();
};
/**
* Get specified URI hash
*/
Query.getHash = function () {
return window.decodeURIComponent(document.location.hash);
};
/**
* Update viewport
*/
Query.setViewport = function () {
// URI hash (e.g. -> '#::body>div.container')
var hash = this.getHash();
// if there's no hash to check, we needn't continue onward
if (this.isEmpty(hash)) {
return null;
}
// URI hash, split into an array at intersection of `HASH_DELIM`
var components = this.toArray(hash, HASH_DELIM),
lastIndex = this.getLastIndex(components);
// check for queryTarget syntax in hash, return if no match is found
if (!this.toBool(lastIndex)) {
return null;
}
// search document for an element matching the selector
var element = document.querySelector(components[lastIndex]);
// verify `element` is a valid element
if (!this.isNode(element)) {
return null;
}
// y-coordinate offset, measured from top of element to top of document
var distance = (element.getBoundingClientRect().top + window.scrollY);
window.setTimeout(function () {
// scroll to the calculated y-coordinate offset
window.scrollTo(0, distance);
}, 10);
return this;
};
var loadHandler = Query.onLoad.bind(Query);
window.addEventListener('load', loadHandler, false);
}).call(this);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment