|
@@ -8,6 +8,59 @@ import log from './log.js'; |
|
|
import tsml from 'tsml';
|
|
|
|
|
|
/**
|
|
|
+ * Detect if a value is a string with any non-whitespace characters.
|
|
|
+ *
|
|
|
+ * @param {String} str
|
|
|
+ * @return {Boolean}
|
|
|
+ */
|
|
|
+function isNonBlankString(str) {
|
|
|
+ return typeof str === 'string' && /\S/.test(str);
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Throws an error if the passed string has whitespace. This is used by
|
|
|
+ * class methods to be relatively consistent with the classList API.
|
|
|
+ *
|
|
|
+ * @param {String} str
|
|
|
+ * @return {Boolean}
|
|
|
+ */
|
|
|
+function throwIfWhitespace(str) {
|
|
|
+ if (/\s/.test(str)) {
|
|
|
+ throw new Error('class has illegal whitespace characters');
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Produce a regular expression for matching a class name.
|
|
|
+ *
|
|
|
+ * @param {String} className
|
|
|
+ * @return {RegExp}
|
|
|
+ */
|
|
|
+function classRegExp(className) {
|
|
|
+ return new RegExp('(^|\\s)' + className + '($|\\s)');
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Creates functions to query the DOM using a given method.
|
|
|
+ *
|
|
|
+ * @function createQuerier
|
|
|
+ * @private
|
|
|
+ * @param {String} method
|
|
|
+ * @return {Function}
|
|
|
+ */
|
|
|
+function createQuerier(method) {
|
|
|
+ return function (selector, context) {
|
|
|
+ if (!isNonBlankString(selector)) {
|
|
|
+ return document[method](null);
|
|
|
+ }
|
|
|
+ if (isNonBlankString(context)) {
|
|
|
+ context = document.querySelector(context);
|
|
|
+ }
|
|
|
+ return (isEl(context) ? context : document)[method](selector);
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
* Shorthand for document.getElementById()
|
|
|
* Also allows for CSS (jQuery) ID syntax. But nothing other than IDs.
|
|
|
*
|
|
@@ -181,47 +234,99 @@ export function removeElData(el) { |
|
|
/**
|
|
|
* Check if an element has a CSS class
|
|
|
*
|
|
|
+ * @function hasElClass
|
|
|
* @param {Element} element Element to check
|
|
|
* @param {String} classToCheck Classname to check
|
|
|
- * @function hasElClass
|
|
|
*/
|
|
|
export function hasElClass(element, classToCheck) {
|
|
|
- return ((' ' + element.className + ' ').indexOf(' ' + classToCheck + ' ') !== -1);
|
|
|
+ if (element.classList) {
|
|
|
+ return element.classList.contains(classToCheck);
|
|
|
+ } else {
|
|
|
+ throwIfWhitespace(classToCheck);
|
|
|
+ return classRegExp(classToCheck).test(element.className);
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Add a CSS class name to an element
|
|
|
*
|
|
|
+ * @function addElClass
|
|
|
* @param {Element} element Element to add class name to
|
|
|
* @param {String} classToAdd Classname to add
|
|
|
- * @function addElClass
|
|
|
*/
|
|
|
export function addElClass(element, classToAdd) {
|
|
|
- if (!hasElClass(element, classToAdd)) {
|
|
|
- element.className = element.className === '' ? classToAdd : element.className + ' ' + classToAdd;
|
|
|
+ if (element.classList) {
|
|
|
+ element.classList.add(classToAdd);
|
|
|
+
|
|
|
+ // Don't need to `throwIfWhitespace` here because `hasElClass` will do it
|
|
|
+ // in the case of classList not being supported.
|
|
|
+ } else if (!hasElClass(element, classToAdd)) {
|
|
|
+ element.className = (element.className + ' ' + classToAdd).trim();
|
|
|
}
|
|
|
+
|
|
|
+ return element;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* Remove a CSS class name from an element
|
|
|
*
|
|
|
+ * @function removeElClass
|
|
|
* @param {Element} element Element to remove from class name
|
|
|
* @param {String} classToRemove Classname to remove
|
|
|
- * @function removeElClass
|
|
|
*/
|
|
|
export function removeElClass(element, classToRemove) {
|
|
|
- if (!hasElClass(element, classToRemove)) {return;}
|
|
|
+ if (element.classList) {
|
|
|
+ element.classList.remove(classToRemove);
|
|
|
+ } else {
|
|
|
+ throwIfWhitespace(classToRemove);
|
|
|
+ element.className = element.className.split(/\s+/).filter(function(c) {
|
|
|
+ return c !== classToRemove;
|
|
|
+ }).join(' ');
|
|
|
+ }
|
|
|
|
|
|
- let classNames = element.className.split(' ');
|
|
|
+ return element;
|
|
|
+}
|
|
|
|
|
|
- // no arr.indexOf in ie8, and we don't want to add a big shim
|
|
|
- for (let i = classNames.length - 1; i >= 0; i--) {
|
|
|
- if (classNames[i] === classToRemove) {
|
|
|
- classNames.splice(i,1);
|
|
|
- }
|
|
|
+/**
|
|
|
+ * Adds or removes a CSS class name on an element depending on an optional
|
|
|
+ * condition or the presence/absence of the class name.
|
|
|
+ *
|
|
|
+ * @function toggleElClass
|
|
|
+ * @param {Element} element
|
|
|
+ * @param {String} classToToggle
|
|
|
+ * @param {Boolean|Function} [predicate]
|
|
|
+ * Can be a function that returns a Boolean. If `true`, the class
|
|
|
+ * will be added; if `false`, the class will be removed. If not
|
|
|
+ * given, the class will be added if not present and vice versa.
|
|
|
+ */
|
|
|
+export function toggleElClass(element, classToToggle, predicate) {
|
|
|
+
|
|
|
+ // This CANNOT use `classList` internally because IE does not support the
|
|
|
+ // second parameter to the `classList.toggle()` method! Which is fine because
|
|
|
+ // `classList` will be used by the add/remove functions.
|
|
|
+ let has = hasElClass(element, classToToggle);
|
|
|
+
|
|
|
+ if (typeof predicate === 'function') {
|
|
|
+ predicate = predicate(element, classToToggle);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (typeof predicate !== 'boolean') {
|
|
|
+ predicate = !has;
|
|
|
+ }
|
|
|
+
|
|
|
+ // If the necessary class operation matches the current state of the
|
|
|
+ // element, no action is required.
|
|
|
+ if (predicate === has) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (predicate) {
|
|
|
+ addElClass(element, classToToggle);
|
|
|
+ } else {
|
|
|
+ removeElClass(element, classToToggle);
|
|
|
}
|
|
|
|
|
|
- element.className = classNames.join(' ');
|
|
|
+ return element;
|
|
|
}
|
|
|
|
|
|
/**
|
|
@@ -292,7 +397,7 @@ export function getElAttributes(tag) { |
|
|
* Attempt to block the ability to select text while dragging controls
|
|
|
*
|
|
|
* @return {Boolean}
|
|
|
- * @method blockTextSelection
|
|
|
+ * @function blockTextSelection
|
|
|
*/
|
|
|
export function blockTextSelection() {
|
|
|
document.body.focus();
|
|
@@ -305,7 +410,7 @@ export function blockTextSelection() { |
|
|
* Turn off text selection blocking
|
|
|
*
|
|
|
* @return {Boolean}
|
|
|
- * @method unblockTextSelection
|
|
|
+ * @function unblockTextSelection
|
|
|
*/
|
|
|
export function unblockTextSelection() {
|
|
|
document.onselectstart = function() {
|
|
@@ -318,9 +423,9 @@ export function unblockTextSelection() { |
|
|
* getBoundingClientRect technique from
|
|
|
* John Resig http://ejohn.org/blog/getboundingclientrect-is-awesome/
|
|
|
*
|
|
|
+ * @function findElPosition
|
|
|
* @param {Element} el Element from which to get offset
|
|
|
- * @return {Object=}
|
|
|
- * @method findElPosition
|
|
|
+ * @return {Object}
|
|
|
*/
|
|
|
export function findElPosition(el) {
|
|
|
let box;
|
|
@@ -359,10 +464,10 @@ export function findElPosition(el) { |
|
|
* Returns an object with x and y coordinates.
|
|
|
* The base on the coordinates are the bottom left of the element.
|
|
|
*
|
|
|
+ * @function getPointerPosition
|
|
|
* @param {Element} el Element on which to get the pointer position on
|
|
|
* @param {Event} event Event object
|
|
|
- * @return {Object=} position This object will have x and y coordinates corresponding to the mouse position
|
|
|
- * @metho getPointerPosition
|
|
|
+ * @return {Object} This object will have x and y coordinates corresponding to the mouse position
|
|
|
*/
|
|
|
export function getPointerPosition(el, event) {
|
|
|
let position = {};
|
|
@@ -389,8 +494,9 @@ export function getPointerPosition(el, event) { |
|
|
/**
|
|
|
* Determines, via duck typing, whether or not a value is a DOM element.
|
|
|
*
|
|
|
- * @param {Mixed} value
|
|
|
- * @return {Boolean}
|
|
|
+ * @function isEl
|
|
|
+ * @param {Mixed} value
|
|
|
+ * @return {Boolean}
|
|
|
*/
|
|
|
export function isEl(value) {
|
|
|
return !!value && typeof value === 'object' && value.nodeType === 1;
|
|
@@ -427,18 +533,25 @@ export function emptyEl(el) { |
|
|
* from falling into the trap of simply writing to `innerHTML`, which is
|
|
|
* an XSS concern.
|
|
|
*
|
|
|
- * The content for an element can be passed in multiple types, whose
|
|
|
- * behavior is as follows:
|
|
|
+ * The content for an element can be passed in multiple types and
|
|
|
+ * combinations, whose behavior is as follows:
|
|
|
+ *
|
|
|
+ * - String
|
|
|
+ * Normalized into a text node.
|
|
|
+ *
|
|
|
+ * - Element, TextNode
|
|
|
+ * Passed through.
|
|
|
*
|
|
|
- * - String: Normalized into a text node.
|
|
|
- * - Node: An Element or TextNode is passed through.
|
|
|
- * - Array: A one-dimensional array of strings, nodes, or functions (which
|
|
|
- * return single strings or nodes).
|
|
|
- * - Function: If the sole argument, is expected to produce a string, node,
|
|
|
- * or array.
|
|
|
+ * - Array
|
|
|
+ * A one-dimensional array of strings, elements, nodes, or functions (which
|
|
|
+ * return single strings, elements, or nodes).
|
|
|
+ *
|
|
|
+ * - Function
|
|
|
+ * If the sole argument, is expected to produce a string, element,
|
|
|
+ * node, or array.
|
|
|
*
|
|
|
* @function normalizeContent
|
|
|
- * @param {String|Element|Array|Function} content
|
|
|
+ * @param {String|Element|TextNode|Array|Function} content
|
|
|
* @return {Array}
|
|
|
*/
|
|
|
export function normalizeContent(content) {
|
|
@@ -474,7 +587,8 @@ export function normalizeContent(content) { |
|
|
*
|
|
|
* @function appendContent
|
|
|
* @param {Element} el
|
|
|
- * @param {String|Element|Array|Function} content
|
|
|
+ * @param {String|Element|TextNode|Array|Function} content
|
|
|
+ * See: `normalizeContent`
|
|
|
* @return {Element}
|
|
|
*/
|
|
|
export function appendContent(el, content) {
|
|
@@ -488,9 +602,46 @@ export function appendContent(el, content) { |
|
|
*
|
|
|
* @function insertContent
|
|
|
* @param {Element} el
|
|
|
- * @param {String|Element|Array|Function} content
|
|
|
+ * @param {String|Element|TextNode|Array|Function} content
|
|
|
+ * See: `normalizeContent`
|
|
|
* @return {Element}
|
|
|
*/
|
|
|
export function insertContent(el, content) {
|
|
|
return appendContent(emptyEl(el), content);
|
|
|
}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Finds a single DOM element matching `selector` within the optional
|
|
|
+ * `context` of another DOM element (defaulting to `document`).
|
|
|
+ *
|
|
|
+ * @function $
|
|
|
+ * @param {String} selector
|
|
|
+ * A valid CSS selector, which will be passed to `querySelector`.
|
|
|
+ *
|
|
|
+ * @param {Element|String} [context=document]
|
|
|
+ * A DOM element within which to query. Can also be a selector
|
|
|
+ * string in which case the first matching element will be used
|
|
|
+ * as context. If missing (or no element matches selector), falls
|
|
|
+ * back to `document`.
|
|
|
+ *
|
|
|
+ * @return {Element|null}
|
|
|
+ */
|
|
|
+export const $ = createQuerier('querySelector');
|
|
|
+
|
|
|
+/**
|
|
|
+ * Finds a all DOM elements matching `selector` within the optional
|
|
|
+ * `context` of another DOM element (defaulting to `document`).
|
|
|
+ *
|
|
|
+ * @function $$
|
|
|
+ * @param {String} selector
|
|
|
+ * A valid CSS selector, which will be passed to `querySelectorAll`.
|
|
|
+ *
|
|
|
+ * @param {Element|String} [context=document]
|
|
|
+ * A DOM element within which to query. Can also be a selector
|
|
|
+ * string in which case the first matching element will be used
|
|
|
+ * as context. If missing (or no element matches selector), falls
|
|
|
+ * back to `document`.
|
|
|
+ *
|
|
|
+ * @return {NodeList}
|
|
|
+ */
|
|
|
+export const $$ = createQuerier('querySelectorAll');
|
0 comments on commit
f2fa8f8