Last active
July 15, 2017 17:32
-
-
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.
This file contains 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
(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