Skip to content
Browse files

@misteroneill exposed DOM helpers. closes #2754

  • Loading branch information...
1 parent cd800b0 commit f2fa8f86870ed52ee47a891ced97d4af25bf0814 @misteroneill misteroneill committed with gkatsev
View
1 .jshintrc
@@ -41,6 +41,7 @@
"stop",
"strictEqual",
"test",
+ "throws",
"sinon"
]
}
View
1 CHANGELOG.md
@@ -10,6 +10,7 @@ CHANGELOG
* @paladox updated grunt-cli dependency ([view](https://github.com/videojs/video.js/pull/2555))
* @paladox updated grunt-contrib-jshint ([view](https://github.com/videojs/video.js/pull/2554))
* @siebrand updated dutch translations ([view](https://github.com/videojs/video.js/pull/2556))
+* @misteroneill exposed DOM helpers ([view](https://github.com/videojs/video.js/pull/2754))
--------------------
View
59 src/js/component.js
@@ -831,6 +831,46 @@ class Component {
}
/**
+ * Finds a single DOM element matching `selector` within the component's
+ * `contentEl` or another custom context.
+ *
+ * @method $
+ * @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}
+ */
+ $(selector, context) {
+ return Dom.$(selector, context || this.contentEl());
+ }
+
+ /**
+ * Finds a all DOM elements matching `selector` within the component's
+ * `contentEl` or another custom context.
+ *
+ * @method $$
+ * @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}
+ */
+ $$(selector, context) {
+ return Dom.$$(selector, context || this.contentEl());
+ }
+
+ /**
* Check if a component's element has a CSS class name
*
* @param {String} classToCheck Classname to check
@@ -854,7 +894,7 @@ class Component {
}
/**
- * Remove and return a CSS class name from the component's element
+ * Remove a CSS class name from the component's element
*
* @param {String} classToRemove Classname to remove
* @return {Component}
@@ -866,6 +906,23 @@ class Component {
}
/**
+ * Add or remove a CSS class name from the component's 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.
+ *
+ * @return {Component}
+ * @method toggleClass
+ */
+ toggleClass(classToToggle, predicate) {
+ Dom.toggleElClass(this.el_, classToToggle, predicate);
+ return this;
+ }
+
+ /**
* Show the component element if hidden
*
* @return {Component}
View
4 src/js/player.js
@@ -281,8 +281,8 @@ class Player extends Component {
// of the player in a way that's still overrideable by CSS, just like the
// video element
this.styleEl_ = stylesheet.createStyleElement('vjs-styles-dimensions');
- let defaultsStyleEl = document.querySelector('.vjs-styles-defaults');
- let head = document.querySelector('head');
+ let defaultsStyleEl = Dom.$('.vjs-styles-defaults');
+ let head = Dom.$('head');
head.insertBefore(this.styleEl_, defaultsStyleEl ? defaultsStyleEl.nextSibling : head.firstChild);
// Pass in the width/height/aspectRatio options which will update the style el
View
2 src/js/tech/html5.js
@@ -771,7 +771,7 @@ class Html5 extends Tech {
this.remoteTextTracks().removeTrack_(track);
- tracks = this.el().querySelectorAll('track');
+ tracks = this.$$('track');
i = tracks.length;
while (i--) {
View
2 src/js/tracks/text-track-list-converter.js
@@ -44,6 +44,8 @@ let trackToJson_ = function(track) {
* @function textTracksToJson
*/
let textTracksToJson = function(tech) {
+
+ // Cannot use $$ here because it is not an instance of Tech
let trackEls = tech.el().querySelectorAll('track');
let trackObjs = Array.prototype.map.call(trackEls, (t) => t.track);
View
96 src/js/tracks/text-track-settings.js
@@ -27,33 +27,33 @@ class TextTrackSettings extends Component {
this.options_.persistTextTrackSettings = this.options_.playerOptions.persistTextTrackSettings;
}
- Events.on(this.el().querySelector('.vjs-done-button'), 'click', Fn.bind(this, function() {
+ Events.on(this.$('.vjs-done-button'), 'click', Fn.bind(this, function() {
this.saveSettings();
this.hide();
}));
- Events.on(this.el().querySelector('.vjs-default-button'), 'click', Fn.bind(this, function() {
- this.el().querySelector('.vjs-fg-color > select').selectedIndex = 0;
- this.el().querySelector('.vjs-bg-color > select').selectedIndex = 0;
- this.el().querySelector('.window-color > select').selectedIndex = 0;
- this.el().querySelector('.vjs-text-opacity > select').selectedIndex = 0;
- this.el().querySelector('.vjs-bg-opacity > select').selectedIndex = 0;
- this.el().querySelector('.vjs-window-opacity > select').selectedIndex = 0;
- this.el().querySelector('.vjs-edge-style select').selectedIndex = 0;
- this.el().querySelector('.vjs-font-family select').selectedIndex = 0;
- this.el().querySelector('.vjs-font-percent select').selectedIndex = 2;
+ Events.on(this.$('.vjs-default-button'), 'click', Fn.bind(this, function() {
+ this.$('.vjs-fg-color > select').selectedIndex = 0;
+ this.$('.vjs-bg-color > select').selectedIndex = 0;
+ this.$('.window-color > select').selectedIndex = 0;
+ this.$('.vjs-text-opacity > select').selectedIndex = 0;
+ this.$('.vjs-bg-opacity > select').selectedIndex = 0;
+ this.$('.vjs-window-opacity > select').selectedIndex = 0;
+ this.$('.vjs-edge-style select').selectedIndex = 0;
+ this.$('.vjs-font-family select').selectedIndex = 0;
+ this.$('.vjs-font-percent select').selectedIndex = 2;
this.updateDisplay();
}));
- Events.on(this.el().querySelector('.vjs-fg-color > select'), 'change', Fn.bind(this, this.updateDisplay));
- Events.on(this.el().querySelector('.vjs-bg-color > select'), 'change', Fn.bind(this, this.updateDisplay));
- Events.on(this.el().querySelector('.window-color > select'), 'change', Fn.bind(this, this.updateDisplay));
- Events.on(this.el().querySelector('.vjs-text-opacity > select'), 'change', Fn.bind(this, this.updateDisplay));
- Events.on(this.el().querySelector('.vjs-bg-opacity > select'), 'change', Fn.bind(this, this.updateDisplay));
- Events.on(this.el().querySelector('.vjs-window-opacity > select'), 'change', Fn.bind(this, this.updateDisplay));
- Events.on(this.el().querySelector('.vjs-font-percent select'), 'change', Fn.bind(this, this.updateDisplay));
- Events.on(this.el().querySelector('.vjs-edge-style select'), 'change', Fn.bind(this, this.updateDisplay));
- Events.on(this.el().querySelector('.vjs-font-family select'), 'change', Fn.bind(this, this.updateDisplay));
+ Events.on(this.$('.vjs-fg-color > select'), 'change', Fn.bind(this, this.updateDisplay));
+ Events.on(this.$('.vjs-bg-color > select'), 'change', Fn.bind(this, this.updateDisplay));
+ Events.on(this.$('.window-color > select'), 'change', Fn.bind(this, this.updateDisplay));
+ Events.on(this.$('.vjs-text-opacity > select'), 'change', Fn.bind(this, this.updateDisplay));
+ Events.on(this.$('.vjs-bg-opacity > select'), 'change', Fn.bind(this, this.updateDisplay));
+ Events.on(this.$('.vjs-window-opacity > select'), 'change', Fn.bind(this, this.updateDisplay));
+ Events.on(this.$('.vjs-font-percent select'), 'change', Fn.bind(this, this.updateDisplay));
+ Events.on(this.$('.vjs-edge-style select'), 'change', Fn.bind(this, this.updateDisplay));
+ Events.on(this.$('.vjs-font-family select'), 'change', Fn.bind(this, this.updateDisplay));
if (this.options_.persistTextTrackSettings) {
this.restoreSettings();
@@ -74,7 +74,7 @@ class TextTrackSettings extends Component {
}
/**
- * Get texttrack settings
+ * Get texttrack settings
* Settings are
* .vjs-edge-style
* .vjs-font-family
@@ -83,23 +83,21 @@ class TextTrackSettings extends Component {
* .vjs-bg-color
* .vjs-bg-opacity
* .window-color
- * .vjs-window-opacity
+ * .vjs-window-opacity
*
- * @return {Object}
+ * @return {Object}
* @method getValues
*/
getValues() {
- const el = this.el();
-
- const textEdge = getSelectedOptionValue(el.querySelector('.vjs-edge-style select'));
- const fontFamily = getSelectedOptionValue(el.querySelector('.vjs-font-family select'));
- const fgColor = getSelectedOptionValue(el.querySelector('.vjs-fg-color > select'));
- const textOpacity = getSelectedOptionValue(el.querySelector('.vjs-text-opacity > select'));
- const bgColor = getSelectedOptionValue(el.querySelector('.vjs-bg-color > select'));
- const bgOpacity = getSelectedOptionValue(el.querySelector('.vjs-bg-opacity > select'));
- const windowColor = getSelectedOptionValue(el.querySelector('.window-color > select'));
- const windowOpacity = getSelectedOptionValue(el.querySelector('.vjs-window-opacity > select'));
- const fontPercent = window['parseFloat'](getSelectedOptionValue(el.querySelector('.vjs-font-percent > select')));
+ const textEdge = getSelectedOptionValue(this.$('.vjs-edge-style select'));
+ const fontFamily = getSelectedOptionValue(this.$('.vjs-font-family select'));
+ const fgColor = getSelectedOptionValue(this.$('.vjs-fg-color > select'));
+ const textOpacity = getSelectedOptionValue(this.$('.vjs-text-opacity > select'));
+ const bgColor = getSelectedOptionValue(this.$('.vjs-bg-color > select'));
+ const bgOpacity = getSelectedOptionValue(this.$('.vjs-bg-opacity > select'));
+ const windowColor = getSelectedOptionValue(this.$('.window-color > select'));
+ const windowOpacity = getSelectedOptionValue(this.$('.vjs-window-opacity > select'));
+ const fontPercent = window['parseFloat'](getSelectedOptionValue(this.$('.vjs-font-percent > select')));
let result = {
'backgroundOpacity': bgOpacity,
@@ -121,7 +119,7 @@ class TextTrackSettings extends Component {
}
/**
- * Set texttrack settings
+ * Set texttrack settings
* Settings are
* .vjs-edge-style
* .vjs-font-family
@@ -130,22 +128,20 @@ class TextTrackSettings extends Component {
* .vjs-bg-color
* .vjs-bg-opacity
* .window-color
- * .vjs-window-opacity
+ * .vjs-window-opacity
*
* @param {Object} values Object with texttrack setting values
* @method setValues
*/
setValues(values) {
- const el = this.el();
-
- setSelectedOption(el.querySelector('.vjs-edge-style select'), values.edgeStyle);
- setSelectedOption(el.querySelector('.vjs-font-family select'), values.fontFamily);
- setSelectedOption(el.querySelector('.vjs-fg-color > select'), values.color);
- setSelectedOption(el.querySelector('.vjs-text-opacity > select'), values.textOpacity);
- setSelectedOption(el.querySelector('.vjs-bg-color > select'), values.backgroundColor);
- setSelectedOption(el.querySelector('.vjs-bg-opacity > select'), values.backgroundOpacity);
- setSelectedOption(el.querySelector('.window-color > select'), values.windowColor);
- setSelectedOption(el.querySelector('.vjs-window-opacity > select'), values.windowOpacity);
+ setSelectedOption(this.$('.vjs-edge-style select'), values.edgeStyle);
+ setSelectedOption(this.$('.vjs-font-family select'), values.fontFamily);
+ setSelectedOption(this.$('.vjs-fg-color > select'), values.color);
+ setSelectedOption(this.$('.vjs-text-opacity > select'), values.textOpacity);
+ setSelectedOption(this.$('.vjs-bg-color > select'), values.backgroundColor);
+ setSelectedOption(this.$('.vjs-bg-opacity > select'), values.backgroundOpacity);
+ setSelectedOption(this.$('.window-color > select'), values.windowColor);
+ setSelectedOption(this.$('.vjs-window-opacity > select'), values.windowOpacity);
let fontPercent = values.fontPercent;
@@ -153,11 +149,11 @@ class TextTrackSettings extends Component {
fontPercent = fontPercent.toFixed(2);
}
- setSelectedOption(el.querySelector('.vjs-font-percent > select'), fontPercent);
+ setSelectedOption(this.$('.vjs-font-percent > select'), fontPercent);
}
/**
- * Restore texttrack settings
+ * Restore texttrack settings
*
* @method restoreSettings
*/
@@ -174,7 +170,7 @@ class TextTrackSettings extends Component {
}
/**
- * Save texttrack settings to local storage
+ * Save texttrack settings to local storage
*
* @method saveSettings
*/
@@ -194,7 +190,7 @@ class TextTrackSettings extends Component {
}
/**
- * Update display of texttrack settings
+ * Update display of texttrack settings
*
* @method updateDisplay
*/
View
217 src/js/utils/dom.js
@@ -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');
View
163 src/js/video.js
@@ -99,10 +99,10 @@ var videojs = function(id, options, ready){
};
// Add default styles
-let style = document.querySelector('.vjs-styles-defaults');
+let style = Dom.$('.vjs-styles-defaults');
if (!style) {
style = stylesheet.createStyleElement('vjs-styles-defaults');
- let head = document.querySelector('head');
+ let head = Dom.$('head');
head.insertBefore(style, head.firstChild);
stylesheet.setTextContent(style, `
.video-js {
@@ -545,22 +545,149 @@ videojs.xhr = xhr;
*/
videojs.TextTrack = TextTrack;
-// REMOVING: We probably should add this to the migration plugin
-// // Expose but deprecate the window[componentName] method for accessing components
-// Object.getOwnPropertyNames(Component.components).forEach(function(name){
-// let component = Component.components[name];
-//
-// // A deprecation warning as the constuctor
-// module.exports[name] = function(player, options, ready){
-// log.warn('Using videojs.'+name+' to access the '+name+' component has been deprecated. Please use videojs.getComponent("componentName")');
-//
-// return new Component(player, options, ready);
-// };
-//
-// // Allow the prototype and class methods to be accessible still this way
-// // Though anything that attempts to override class methods will no longer work
-// assign(module.exports[name], component);
-// });
+/**
+ * Determines, via duck typing, whether or not a value is a DOM element.
+ *
+ * @method isEl
+ * @param {Mixed} value
+ * @return {Boolean}
+ */
+videojs.isEl = Dom.isEl;
+
+/**
+ * Determines, via duck typing, whether or not a value is a text node.
+ *
+ * @method isTextNode
+ * @param {Mixed} value
+ * @return {Boolean}
+ */
+videojs.isTextNode = Dom.isTextNode;
+
+/**
+ * Check if an element has a CSS class
+ *
+ * @method hasClass
+ * @param {Element} element Element to check
+ * @param {String} classToCheck Classname to check
+ */
+videojs.hasClass = Dom.hasElClass;
+
+/**
+ * Add a CSS class name to an element
+ *
+ * @method addClass
+ * @param {Element} element Element to add class name to
+ * @param {String} classToAdd Classname to add
+ */
+videojs.addClass = Dom.addElClass;
+
+/**
+ * Remove a CSS class name from an element
+ *
+ * @method removeClass
+ * @param {Element} element Element to remove from class name
+ * @param {String} classToRemove Classname to remove
+ */
+videojs.removeClass = Dom.removeElClass;
+
+/**
+ * Adds or removes a CSS class name on an element depending on an optional
+ * condition or the presence/absence of the class name.
+ *
+ * @method 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.
+ */
+videojs.toggleClass = Dom.toggleElClass;
+
+/**
+ * Apply attributes to an HTML element.
+ *
+ * @method setAttributes
+ * @param {Element} el Target element.
+ * @param {Object=} attributes Element attributes to be applied.
+ */
+videojs.setAttributes = Dom.setElAttributes;
+
+/**
+ * Get an element's attribute values, as defined on the HTML tag
+ * Attributes are not the same as properties. They're defined on the tag
+ * or with setAttribute (which shouldn't be used with HTML)
+ * This will return true or false for boolean attributes.
+ *
+ * @method getAttributes
+ * @param {Element} tag Element from which to get tag attributes
+ * @return {Object}
+ */
+videojs.getAttributes = Dom.getElAttributes;
+
+/**
+ * Empties the contents of an element.
+ *
+ * @method emptyEl
+ * @param {Element} el
+ * @return {Element}
+ */
+videojs.emptyEl = Dom.emptyEl;
+
+/**
+ * Normalizes and appends content to an element.
+ *
+ * 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.
+ *
+ * - 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.
+ *
+ * @method appendContent
+ * @param {Element} el
+ * @param {String|Element|TextNode|Array|Function} content
+ * @return {Element}
+ */
+videojs.appendContent = Dom.appendContent;
+
+/**
+ * Normalizes and inserts content into an element; this is identical to
+ * `appendContent()`, except it empties the element first.
+ *
+ * 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.
+ *
+ * - 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.
+ *
+ * @method insertContent
+ * @param {Element} el
+ * @param {String|Element|TextNode|Array|Function} content
+ * @return {Element}
+ */
+videojs.insertContent = Dom.insertContent;
/*
* Custom Universal Module Definition (UMD)
View
19 test/unit/component.test.js
@@ -471,6 +471,10 @@ test('should add and remove a CSS class', function(){
ok(comp.el().className.indexOf('test-class') !== -1);
comp.removeClass('test-class');
ok(comp.el().className.indexOf('test-class') === -1);
+ comp.toggleClass('test-class');
+ ok(comp.el().className.indexOf('test-class') !== -1);
+ comp.toggleClass('test-class');
+ ok(comp.el().className.indexOf('test-class') === -1);
});
test('should show and hide an element', function(){
@@ -695,3 +699,18 @@ test('should provide interval methods that automatically get cleared on componen
ok(intervalsFired === 5, 'Interval was cleared when component was disposed');
});
+
+test('$ and $$ functions', function() {
+ var comp = new Component(getFakePlayer());
+ var contentEl = document.createElement('div');
+ var children = [
+ document.createElement('div'),
+ document.createElement('div')
+ ];
+
+ comp.contentEl_ = contentEl;
+ children.forEach(child => contentEl.appendChild(child));
+
+ strictEqual(comp.$('div'), children[0], '$ defaults to contentEl as scope');
+ strictEqual(comp.$$('div').length, children.length, '$$ defaults to contentEl as scope');
+});
View
74 test/unit/tracks/text-track-settings.test.js
@@ -35,17 +35,17 @@ test('should update settings', function() {
player.textTrackSettings.setValues(newSettings);
deepEqual(player.textTrackSettings.getValues(), newSettings, 'values are updated');
- equal(player.el().querySelector('.vjs-fg-color > select').selectedIndex, 1, 'fg-color is set to new value');
- equal(player.el().querySelector('.vjs-bg-color > select').selectedIndex, 1, 'bg-color is set to new value');
- equal(player.el().querySelector('.window-color > select').selectedIndex, 1, 'window-color is set to new value');
- equal(player.el().querySelector('.vjs-text-opacity > select').selectedIndex, 1, 'text-opacity is set to new value');
- equal(player.el().querySelector('.vjs-bg-opacity > select').selectedIndex, 1, 'bg-opacity is set to new value');
- equal(player.el().querySelector('.vjs-window-opacity > select').selectedIndex, 1, 'window-opacity is set to new value');
- equal(player.el().querySelector('.vjs-edge-style select').selectedIndex, 1, 'edge-style is set to new value');
- equal(player.el().querySelector('.vjs-font-family select').selectedIndex, 1, 'font-family is set to new value');
- equal(player.el().querySelector('.vjs-font-percent select').selectedIndex, 3, 'font-percent is set to new value');
-
- Events.trigger(player.el().querySelector('.vjs-done-button'), 'click');
+ equal(player.$('.vjs-fg-color > select').selectedIndex, 1, 'fg-color is set to new value');
+ equal(player.$('.vjs-bg-color > select').selectedIndex, 1, 'bg-color is set to new value');
+ equal(player.$('.window-color > select').selectedIndex, 1, 'window-color is set to new value');
+ equal(player.$('.vjs-text-opacity > select').selectedIndex, 1, 'text-opacity is set to new value');
+ equal(player.$('.vjs-bg-opacity > select').selectedIndex, 1, 'bg-opacity is set to new value');
+ equal(player.$('.vjs-window-opacity > select').selectedIndex, 1, 'window-opacity is set to new value');
+ equal(player.$('.vjs-edge-style select').selectedIndex, 1, 'edge-style is set to new value');
+ equal(player.$('.vjs-font-family select').selectedIndex, 1, 'font-family is set to new value');
+ equal(player.$('.vjs-font-percent select').selectedIndex, 3, 'font-percent is set to new value');
+
+ Events.trigger(player.$('.vjs-done-button'), 'click');
deepEqual(safeParseTuple(window.localStorage.getItem('vjs-text-track-settings'))[1], newSettings, 'values are saved');
player.dispose();
@@ -57,32 +57,32 @@ test('should restore default settings', function() {
persistTextTrackSettings: true
});
- player.el().querySelector('.vjs-fg-color > select').selectedIndex = 1;
- player.el().querySelector('.vjs-bg-color > select').selectedIndex = 1;
- player.el().querySelector('.window-color > select').selectedIndex = 1;
- player.el().querySelector('.vjs-text-opacity > select').selectedIndex = 1;
- player.el().querySelector('.vjs-bg-opacity > select').selectedIndex = 1;
- player.el().querySelector('.vjs-window-opacity > select').selectedIndex = 1;
- player.el().querySelector('.vjs-edge-style select').selectedIndex = 1;
- player.el().querySelector('.vjs-font-family select').selectedIndex = 1;
- player.el().querySelector('.vjs-font-percent select').selectedIndex = 3;
+ player.$('.vjs-fg-color > select').selectedIndex = 1;
+ player.$('.vjs-bg-color > select').selectedIndex = 1;
+ player.$('.window-color > select').selectedIndex = 1;
+ player.$('.vjs-text-opacity > select').selectedIndex = 1;
+ player.$('.vjs-bg-opacity > select').selectedIndex = 1;
+ player.$('.vjs-window-opacity > select').selectedIndex = 1;
+ player.$('.vjs-edge-style select').selectedIndex = 1;
+ player.$('.vjs-font-family select').selectedIndex = 1;
+ player.$('.vjs-font-percent select').selectedIndex = 3;
- Events.trigger(player.el().querySelector('.vjs-done-button'), 'click');
- Events.trigger(player.el().querySelector('.vjs-default-button'), 'click');
- Events.trigger(player.el().querySelector('.vjs-done-button'), 'click');
+ Events.trigger(player.$('.vjs-done-button'), 'click');
+ Events.trigger(player.$('.vjs-default-button'), 'click');
+ Events.trigger(player.$('.vjs-done-button'), 'click');
deepEqual(player.textTrackSettings.getValues(), {}, 'values are defaulted');
deepEqual(window.localStorage.getItem('vjs-text-track-settings'), null, 'values are saved');
- equal(player.el().querySelector('.vjs-fg-color > select').selectedIndex, 0, 'fg-color is set to default value');
- equal(player.el().querySelector('.vjs-bg-color > select').selectedIndex, 0, 'bg-color is set to default value');
- equal(player.el().querySelector('.window-color > select').selectedIndex, 0, 'window-color is set to default value');
- equal(player.el().querySelector('.vjs-text-opacity > select').selectedIndex, 0, 'text-opacity is set to default value');
- equal(player.el().querySelector('.vjs-bg-opacity > select').selectedIndex, 0, 'bg-opacity is set to default value');
- equal(player.el().querySelector('.vjs-window-opacity > select').selectedIndex, 0, 'window-opacity is set to default value');
- equal(player.el().querySelector('.vjs-edge-style select').selectedIndex, 0, 'edge-style is set to default value');
- equal(player.el().querySelector('.vjs-font-family select').selectedIndex, 0, 'font-family is set to default value');
- equal(player.el().querySelector('.vjs-font-percent select').selectedIndex, 2, 'font-percent is set to default value');
+ equal(player.$('.vjs-fg-color > select').selectedIndex, 0, 'fg-color is set to default value');
+ equal(player.$('.vjs-bg-color > select').selectedIndex, 0, 'bg-color is set to default value');
+ equal(player.$('.window-color > select').selectedIndex, 0, 'window-color is set to default value');
+ equal(player.$('.vjs-text-opacity > select').selectedIndex, 0, 'text-opacity is set to default value');
+ equal(player.$('.vjs-bg-opacity > select').selectedIndex, 0, 'bg-opacity is set to default value');
+ equal(player.$('.vjs-window-opacity > select').selectedIndex, 0, 'window-opacity is set to default value');
+ equal(player.$('.vjs-edge-style select').selectedIndex, 0, 'edge-style is set to default value');
+ equal(player.$('.vjs-font-family select').selectedIndex, 0, 'font-family is set to default value');
+ equal(player.$('.vjs-font-percent select').selectedIndex, 2, 'font-percent is set to default value');
player.dispose();
});
@@ -91,7 +91,7 @@ test('should open on click', function() {
var player = TestHelpers.makePlayer({
tracks: tracks
});
- Events.trigger(player.el().querySelector('.vjs-texttrack-settings'), 'click');
+ Events.trigger(player.$('.vjs-texttrack-settings'), 'click');
ok(!player.textTrackSettings.hasClass('vjs-hidden'), 'settings open');
player.dispose();
@@ -101,8 +101,8 @@ test('should close on done click', function() {
var player = TestHelpers.makePlayer({
tracks: tracks
});
- Events.trigger(player.el().querySelector('.vjs-texttrack-settings'), 'click');
- Events.trigger(player.el().querySelector('.vjs-done-button'), 'click');
+ Events.trigger(player.$('.vjs-texttrack-settings'), 'click');
+ Events.trigger(player.$('.vjs-done-button'), 'click');
ok(player.textTrackSettings.hasClass('vjs-hidden'), 'settings closed');
player.dispose();
@@ -141,7 +141,7 @@ test('if persist option is set, save settings when "done"', function() {
save++;
};
- Events.trigger(player.el().querySelector('.vjs-done-button'), 'click');
+ Events.trigger(player.$('.vjs-done-button'), 'click');
equal(save, 1, 'save was called');
@@ -171,7 +171,7 @@ test('do not try to restore or save settings if persist option is not set', func
equal(restore, 0, 'restore was not called');
- Events.trigger(player.el().querySelector('.vjs-done-button'), 'click');
+ Events.trigger(player.$('.vjs-done-button'), 'click');
// saveSettings is called but does nothing
equal(save, 1, 'save was not called');
View
194 test/unit/utils/dom.test.js
@@ -62,23 +62,164 @@ test('should get and remove data from an element', function(){
ok(!Dom.hasElData(el), 'cached item emptied');
});
-test('should add and remove a class name on an element', function(){
+test('addElClass()', function(){
var el = document.createElement('div');
+
+ expect(5);
+
Dom.addElClass(el, 'test-class');
- ok(el.className === 'test-class', 'class added');
+ strictEqual(el.className, 'test-class', 'adds a single class');
+
Dom.addElClass(el, 'test-class');
- ok(el.className === 'test-class', 'same class not duplicated');
- Dom.addElClass(el, 'test-class2');
- ok(el.className === 'test-class test-class2', 'added second class');
+ strictEqual(el.className, 'test-class', 'does not duplicate classes');
+
+ throws(function(){
+ Dom.addElClass(el, 'foo foo-bar');
+ }, 'throws when attempting to add a class with whitespace');
+
+ Dom.addElClass(el, 'test2_className');
+ strictEqual(el.className, 'test-class test2_className', 'adds second class');
+
+ Dom.addElClass(el, 'FOO');
+ strictEqual(el.className, 'test-class test2_className FOO', 'adds third class');
+});
+
+test('removeElClass()', function() {
+ var el = document.createElement('div');
+
+ el.className = 'test-class foo foo test2_className FOO bar';
+
+ expect(5);
+
Dom.removeElClass(el, 'test-class');
- ok(el.className === 'test-class2', 'removed first class');
+ strictEqual(el.className, 'foo foo test2_className FOO bar', 'removes one class');
+
+ Dom.removeElClass(el, 'foo');
+ strictEqual(el.className, 'test2_className FOO bar', 'removes all instances of a class');
+
+ throws(function(){
+ Dom.removeElClass(el, 'test2_className bar');
+ }, 'throws when attempting to remove a class with whitespace');
+
+ Dom.removeElClass(el, 'test2_className');
+ strictEqual(el.className, 'FOO bar', 'removes another class');
+
+ Dom.removeElClass(el, 'FOO');
+ strictEqual(el.className, 'bar', 'removes another class');
});
-test('should read class names on an element', function(){
+test('hasElClass()', function(){
var el = document.createElement('div');
- Dom.addElClass(el, 'test-class1');
- ok(Dom.hasElClass(el, 'test-class1') === true, 'class detected');
- ok(Dom.hasElClass(el, 'test-class') === false, 'substring correctly not detected');
+
+ el.className = 'test-class foo foo test2_className FOO bar';
+
+ strictEqual(Dom.hasElClass(el, 'test-class'), true, 'class detected');
+ strictEqual(Dom.hasElClass(el, 'foo'), true, 'class detected');
+ strictEqual(Dom.hasElClass(el, 'test2_className'), true, 'class detected');
+ strictEqual(Dom.hasElClass(el, 'FOO'), true, 'class detected');
+ strictEqual(Dom.hasElClass(el, 'bar'), true, 'class detected');
+ strictEqual(Dom.hasElClass(el, 'test2'), false, 'valid substring - but not a class - correctly not detected');
+ strictEqual(Dom.hasElClass(el, 'className'), false, 'valid substring - but not a class - correctly not detected');
+
+ throws(function(){
+ Dom.hasElClass(el, 'FOO bar');
+ }, 'throws when attempting to detect a class with whitespace');
+});
+
+test('toggleElClass()', function() {
+ let el = Dom.createEl('div', {className: 'foo bar'});
+
+ let predicateToggles = [
+ {
+ toggle: 'foo',
+ predicate: true,
+ className: 'foo bar',
+ message: 'if predicate `true` matches state of the element, do nothing'
+ },
+ {
+ toggle: 'baz',
+ predicate: false,
+ className: 'foo bar',
+ message: 'if predicate `false` matches state of the element, do nothing'
+ },
+ {
+ toggle: 'baz',
+ predicate: true,
+ className: 'foo bar baz',
+ message: 'if predicate `true` differs from state of the element, add the class'
+ },
+ {
+ toggle: 'foo',
+ predicate: false,
+ className: 'bar baz',
+ message: 'if predicate `false` differs from state of the element, remove the class'
+ },
+ {
+ toggle: 'bar',
+ predicate: () => true,
+ className: 'bar baz',
+ message: 'if a predicate function returns `true`, matching the state of the element, do nothing'
+ },
+ {
+ toggle: 'foo',
+ predicate: () => false,
+ className: 'bar baz',
+ message: 'if a predicate function returns `false`, matching the state of the element, do nothing'
+ },
+ {
+ toggle: 'foo',
+ predicate: () => true,
+ className: 'bar baz foo',
+ message: 'if a predicate function returns `true`, differing from state of the element, add the class'
+ },
+ {
+ toggle: 'foo',
+ predicate: () => false,
+ className: 'bar baz',
+ message: 'if a predicate function returns `false`, differing from state of the element, remove the class'
+ },
+ {
+ toggle: 'foo',
+ predicate: Function.prototype,
+ className: 'bar baz foo',
+ message: 'if a predicate function returns `undefined` and the element does not have the class, add the class'
+ },
+ {
+ toggle: 'bar',
+ predicate: Function.prototype,
+ className: 'baz foo',
+ message: 'if a predicate function returns `undefined` and the element has the class, remove the class'
+ },
+ {
+ toggle: 'bar',
+ predicate: () => [],
+ className: 'baz foo bar',
+ message: 'if a predicate function returns a defined non-boolean value and the element does not have the class, add the class'
+ },
+ {
+ toggle: 'baz',
+ predicate: () => 'this is incorrect',
+ className: 'foo bar',
+ message: 'if a predicate function returns a defined non-boolean value and the element has the class, remove the class'
+ },
+ ];
+
+ expect(3 + predicateToggles.length);
+
+ Dom.toggleElClass(el, 'bar');
+ strictEqual(el.className, 'foo', 'toggles a class off, if present');
+
+ Dom.toggleElClass(el, 'bar');
+ strictEqual(el.className, 'foo bar', 'toggles a class on, if absent');
+
+ throws(function(){
+ Dom.toggleElClass(el, 'foo bar');
+ }, 'throws when attempting to toggle a class with whitespace');
+
+ predicateToggles.forEach(x => {
+ Dom.toggleElClass(el, x.toggle, x.predicate);
+ strictEqual(el.className, x.className, x.message);
+ });
});
test('should set element attributes from object', function(){
@@ -293,4 +434,37 @@ test('Dom.appendContent', function(assert) {
assert.strictEqual(el.firstChild.nextSibling, p2, 'the second paragraph was appended');
});
+test('$() and $$()', function() {
+ let fixture = document.getElementById('qunit-fixture');
+ let container = document.createElement('div');
+ let children = [
+ document.createElement('div'),
+ document.createElement('div'),
+ document.createElement('div'),
+ ];
+ children.forEach(child => container.appendChild(child));
+ fixture.appendChild(container);
+
+ let totalDivCount = document.getElementsByTagName('div').length;
+
+ expect(12);
+
+ strictEqual(Dom.$('#qunit-fixture'), fixture, 'can find an element in the document context');
+ strictEqual(Dom.$$('div').length, totalDivCount, 'finds elements in the document context');
+
+ strictEqual(Dom.$('div', container), children[0], 'can find an element in a DOM element context');
+ strictEqual(Dom.$$('div', container).length, children.length, 'finds elements in a DOM element context');
+
+ strictEqual(Dom.$('#qunit-fixture', document.querySelector('unknown')), fixture, 'falls back to document given a bad context element');
+ strictEqual(Dom.$$('div', document.querySelector('unknown')).length, totalDivCount, 'falls back to document given a bad context element');
+
+ strictEqual(Dom.$('#qunit-fixture', 'body'), fixture, 'can find an element in a selector context');
+ strictEqual(Dom.$$('div', '#qunit-fixture').length, 1 + children.length, 'finds elements in a selector context');
+
+ strictEqual(Dom.$('#qunit-fixture', 'unknown'), fixture, 'falls back to document given a bad context selector');
+ strictEqual(Dom.$$('div', 'unknown').length, totalDivCount, 'falls back to document given a bad context selector');
+
+ strictEqual(Dom.$('div', children[0]), null, 'returns null for missing elements');
+ strictEqual(Dom.$$('div', children[0]).length, 0, 'returns 0 for missing elements');
+});
View
27 test/unit/video.test.js
@@ -1,6 +1,7 @@
import videojs from '../../src/js/video.js';
import TestHelpers from './test-helpers.js';
import Player from '../../src/js/player.js';
+import * as Dom from '../../src/js/utils/dom.js';
import log from '../../src/js/utils/log.js';
import document from 'global/document';
@@ -78,3 +79,29 @@ test('should expose options and players properties for backward-compatibility',
ok(typeof videojs.options, 'object', 'options should be an object');
ok(typeof videojs.players, 'object', 'players should be an object');
});
+
+test('should expose DOM functions', function() {
+
+ // Keys are videojs methods, values are Dom methods.
+ let methods = {
+ isEl: 'isEl',
+ isTextNode: 'isTextNode',
+ hasClass: 'hasElClass',
+ addClass: 'addElClass',
+ removeClass: 'removeElClass',
+ toggleClass: 'toggleElClass',
+ setAttributes: 'setElAttributes',
+ getAttributes: 'getElAttributes',
+ emptyEl: 'emptyEl',
+ insertContent: 'insertContent',
+ appendContent: 'appendContent'
+ };
+
+ let keys = Object.keys(methods);
+
+ expect(keys.length);
+ keys.forEach(function(vjsName) {
+ let domName = methods[vjsName];
+ strictEqual(videojs[vjsName], Dom[domName], `videojs.${vjsName} is a reference to Dom.${domName}`);
+ });
+});

0 comments on commit f2fa8f8

Please sign in to comment.
Something went wrong with that request. Please try again.