UNPKG

decks

Version:

JavaScript UI library for viewing collections of items.

446 lines (382 loc) 11.5 kB
var _ = require("lodash"); var rect = require("../utils/rect"); var validate = require("../utils/validate"); /** * DOM manipulation helper module to encapsulate browser and DOM API version * differences. * * @module decks/ui/dom */ module.exports = { /** * Wrapper for querySelectorAll * * @param {!string} selector - DOM selector * @param {?HTMLElement} [context=document] - context element for DOM query * @return {NodeList} */ query: function query(selector, context) { validate(selector, "selector", { isString: true }); context = context || document; return context.querySelectorAll(selector); }, /** * Wrapper for querySelector * * @param {!string} selector - DOM selector * @param {?Element} [context=document] - context element for DOM query * @return {undefined} */ querySingle: function querySingle(selector, context) { validate(selector, "selector", { isString: true }); context = context || document; return context.querySelector(selector); }, /** * Creates a DOM element by name (e.g. "div"). * * @param {!String} type - the type of DOM element to create (e.g. "div") * @returns {Element} - the DOM element */ create: function create(type, options) { validate(type, "type", { isString: true }); options = options || {}; var element = document.createElement(type); if (_.has(options, "id")) { element.id = options.id; } if (_.has(options, "className")) { element.className = options.className; } if (_.has(options, "styles")) { this.setStyles(element, options.styles); } if (_.has(options, "attrs")) { this.setAttrs(element, options.attrs); } return element; }, /** * Gets or sets an element's innerHTML. * * @param {!Element} element - the Element for which to get or set HTML. * @param {?String} data - the HTML to set, or if not specified, the method will return the HTML. * @return {String} - the element's innerHTML */ html: function html(element, data) { if (!data) { return element.innerHTML; } if (_.isElement(data)) { element.innerHTML = data.outerHTML; return; } if (_.isString(data)) { element.innerHTML = data; return; } throw new Error("dom.create: cannot set element html"); }, /** * Parses an HTML string, and returns the resulting Element or Elements. * * @param {!string} html - the HTML string * @param {?Object} [options={}] - additional options * @param {?boolean} [options.multiple=false] - whether to return a all top-level sibling elements * @return {Element|NodeList} - the resulting Element or NodeList of elements */ parse: function parse(html, options) { options = options || {}; var element = this.create("dom"); element.innerHTML = html; // Default to returning firstChild, unless options.multiple === true if (options.multiple) { return element.children; } return element.firstChild; }, /** * Gets or sets the textContent/innerText of the Element * * @param element * @param data * @return {undefined} */ text: function text(element, data) { if (!_.isString(data)) { return element.textContent || element.innerText; } if (!_.isUndefined(element.textContent)) { element.textContent = data; } else { element.innerText = data; } }, /** * Empties an element by removing all children * * @param element * @return {undefined} */ empty: function empty(element) { while (element.firstChild) { element.removeChild(element.firstChild); } }, /** * Appends a child element to a parent element * * @param parent * @param child * @return {undefined} */ append: function append(parent, child) { parent.appendChild(child); }, /** * Prepends a child element in a parent element * * @param parent * @param child * @return {undefined} */ prepend: function prepend(parent, child) { parent.insertBefore(child, parent.firstChild); }, /** * Removes a child element from a parent element, or removes the parent element * from its parent if no child specified. * * @param parent * @param child * @return {undefined} */ remove: function remove(parent, child) { if (!child) { return parent.parentNode.removeChild(parent); } else { return parent.removeChild(child); } }, /** * Gets or sets an Element attribute value * * @param element * @param name * @param value * @return {undefined} */ attr: function attr(element, name, value) { if (_.isUndefined(value)) { return this.getAttr(element, name); } this.setAttr(element, name, value); }, /** * Gets an Element's attribute value by name * * @param element * @param name * @return {undefined} */ getAttr: function getAttr(element, name) { return element.getAttribute(name); }, /** * Sets an Element's attribute value by name * * @param element * @param name * @param value * @return {undefined} */ setAttr: function setAttr(element, key, value) { element.setAttribute(key, value); }, setAttrs: function setAttrs(element, attrs) { _.each(attrs, function(value, key) { this.setAttr(element, key, value); }, this); }, /** * Indicates if an Element has the given class * * @param element * @param className * @return {undefined} */ hasClass: function hasClass(element, className) { if (element.classList) { return element.classList.contains(className); } else { return new RegExp('(^| )' + className + '( |$)', 'gi').test(element.className); } }, addClass: function addClass(element, className) { var classNames = _.map(className.split(" "), function(name) { return name.trim(); }); _.each(classNames, function(className) { if (this.hasClass(element, className)) { return; } if (element.classList) { element.classList.add(className); } else { element.className += ' ' + className; } }, this); }, removeClass: function removeClass(element, className) { var classNames = _.map(className.split(" "), function(className) { return className.trim(); }); _.each(classNames, function(className) { if (!this.hasClass(element, className)) { return; } if (element.classList) { element.classList.remove(className); } else { element.className = element.className.replace(new RegExp('(^|\\b)' + className.split(' ').join('|') + '(\\b|$)', 'gi'), ' '); } }, this); }, toggleClass: function toggleClass(element, className) { if (element.classList) { element.classList.toggle(className); } else { var classes = element.className.split(' '); var existingIndex = -1; for (var i = classes.length; i--;) { if (classes[i] === className) { existingIndex = i; } } if (existingIndex >= 0) { classes.splice(existingIndex, 1); } else { classes.push(className); } element.className = classes.join(' '); } }, getStyle: function getStyle(element, name, options) { options = options || {}; var value = element.style[name]; if (options.parseInt) { value = _.parseInt(value); } if (options.parseFloat) { value = parseFloat(value); } return value; }, setStyle: function setStyle(element, name, value) { if (_.isNumber(value)) { var unit = this.autoUnits[name]; if (unit) { value = value + unit; } } element.style[name] = value; }, setStyles: function setStyles(element, styles) { _.each(styles, function(value, key) { this.setStyle(element, key, value); }, this); }, removeStyle: function removeStyle(element, name) { element.style[name] = ""; }, isPositioned: function isPositioned(element) { var position = this.getStyle(element, "position"); return _.contains(["absolute", "relative", "fixed"], position); }, isVisible: function(element) { validate(element, "element", { isElement: true }); return element.style.display !== "none" && element.style.visibility !== "hidden"; }, closest: function closest(element, predicate) { if (element && predicate(element)) { return element; } if (element.parentNode) { return this.closest(element.parentNode, predicate); } return null; }, closestWithClass: function closestWithClass(element, className) { var self = this; return self.closest(element, function(el) { return self.hasClass(el, className); }); }, /** * Get the element whose bounding rect top/left is nearest to the given point in * distance. * * @param {!Object} point - the point to compare all elements to (in top/left or x/y) * @param {!(Element[]|NodeList)} elements - the elements to check * @return {Element} - the element whose top/left is nearest to point */ nearest: function nearest(point, elements, options) { options = options || {}; var minDistance = Infinity; if (options.ignoreInvisibleElements) { elements = _.filter(elements, function(element) { return this.isVisible(element); }, this); } return _.reduce(elements, function(nearestElement, element) { var elementRect = rect.normalize(element); var distance = rect.distance(point, elementRect); if (distance < minDistance) { minDistance = distance; return element; } return nearestElement; }, null); }, /** * Default tolerance value for isOverflowed methods */ defaultOverflowTolerance: 2, /** * Indicates if an element is overflowing it's parent in the horizontal direction. * * @param {!Element} element - element to check * @param {?number} [tolerance=2] - tolerance to add to element.clientWidth * @return {boolean} - whether element is overflowed horizontally */ isOverflowedX: function isOverflowedX(element, tolerance) { tolerance = _.isNumber(tolerance) ? tolerance : this.defaultOverflowTolerance; return element.clientWidth + tolerance < element.scrollWidth; }, /** * Indicates if an element is overflowing it's parent in the vertical direction. * * @param {!Element} element - element to check * @param {?number} [tolerance=2] - tolerance to add to element.clientHeight * @return {boolean} - whether element is overflowed vertically */ isOverflowedY: function isOverflowedY(element, tolerance) { tolerance = _.isNumber(tolerance) ? tolerance : this.defaultOverflowTolerance; return element.clientHeight + tolerance < element.scrollHeight; }, /** * Indicates if an element is overflowing it's parent in any direction. * * @param {!Element} element - element to check * @param {?number} [tolerance=2] - tolerance to add to element.clientHeight and element.clientWidth * @return {boolean} - whether element is overflowed */ isOverflowed: function isOverflowed(element, tolerance) { return this.isOverflowedX(element, tolerance) || this.isOverflowedY(element, tolerance); }, autoUnits: { "top": "px", "bottom": "px", "left": "px", "right": "px", "width": "px", "height": "px" } };