--- /dev/null
+// Copyright 2012 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+goog.require('axs.utils');
+
+goog.provide('axs.properties');
+
+/**
+ * @const
+ * @type {string}
+ */
+axs.properties.TEXT_CONTENT_XPATH = './/text()[normalize-space(.)!=""]/parent::*[name()!="script"]';
+
+/**
+ * @param {Element} element
+ * @return {Object.<string, Object>}
+ */
+axs.properties.getFocusProperties = function(element) {
+ var focusProperties = {};
+ var tabindex = element.getAttribute('tabindex');
+ if (tabindex != undefined) {
+ focusProperties['tabindex'] = { value: tabindex, valid: true };
+ } else {
+ if (axs.utils.isElementImplicitlyFocusable(element))
+ focusProperties['implicitlyFocusable'] = { value: true, valid: true };
+ }
+ if (Object.keys(focusProperties).length == 0)
+ return null;
+ var transparent = axs.utils.elementIsTransparent(element);
+ var zeroArea = axs.utils.elementHasZeroArea(element);
+ var outsideScrollArea = axs.utils.elementIsOutsideScrollArea(element);
+ var overlappingElements = axs.utils.overlappingElements(element);
+ if (transparent || zeroArea || outsideScrollArea || overlappingElements.length > 0) {
+ var hidden = axs.utils.isElementOrAncestorHidden(element);
+ var visibleProperties = { value: false,
+ valid: hidden };
+ if (transparent)
+ visibleProperties['transparent'] = true;
+ if (zeroArea)
+ visibleProperties['zeroArea'] = true;
+ if (outsideScrollArea)
+ visibleProperties['outsideScrollArea'] = true;
+ if (overlappingElements && overlappingElements.length > 0)
+ visibleProperties['overlappingElements'] = overlappingElements;
+ var hiddenProperties = { value: hidden, valid: hidden };
+ if (hidden)
+ hiddenProperties['reason'] = axs.properties.getHiddenReason(element);
+ visibleProperties['hidden'] = hiddenProperties;
+ focusProperties['visible'] = visibleProperties;
+ } else {
+ focusProperties['visible'] = { value: true, valid: true };
+ }
+
+ return focusProperties;
+};
+
+axs.properties.getHiddenReason = function(element) {
+ if (!element || !(element instanceof element.ownerDocument.defaultView.HTMLElement))
+ return null;
+
+ if (element.hasAttribute('chromevoxignoreariahidden'))
+ var chromevoxignoreariahidden = true;
+
+ var style = window.getComputedStyle(element, null);
+ if (style.display == 'none')
+ return { 'property': 'display: none',
+ 'on': element };
+
+ if (style.visibility == 'hidden')
+ return { 'property': 'visibility: hidden',
+ 'on': element };
+
+ if (element.hasAttribute('aria-hidden') &&
+ element.getAttribute('aria-hidden').toLowerCase() == 'true') {
+ if (!chromevoxignoreariahidden)
+ return { 'property': 'aria-hidden',
+ 'on': element };
+ }
+
+ return axs.properties.getHiddenReason(axs.utils.parentElement(element));
+};
+
+
+/**
+ * @param {Element} element
+ * @return {Object.<string, Object>}
+ */
+axs.properties.getColorProperties = function(element) {
+ var colorProperties = {};
+ var contrastRatioProperties =
+ axs.properties.getContrastRatioProperties(element);
+ if (contrastRatioProperties)
+ colorProperties['contrastRatio'] = contrastRatioProperties;
+ if (Object.keys(colorProperties).length == 0)
+ return null;
+ return colorProperties;
+};
+
+/**
+ * Determines whether the given element has a text node as a direct descendant.
+ * @param {Element} element
+ * @return {boolean}
+ */
+axs.properties.hasDirectTextDescendant = function(element) {
+ var ownerDocument;
+ if (element.nodeType == Node.DOCUMENT_NODE)
+ ownerDocument = element;
+ else
+ ownerDocument = element.ownerDocument;
+ if (ownerDocument.evaluate) {
+ return hasDirectTextDescendantXpath();
+ }
+ else { // IE
+ return hasDirectTextDescendantTreeWalker();
+ }
+
+ /**
+ * Determines whether element has a text node as a direct descendant.
+ * This method uses XPath on HTML DOM which is not universally supported.
+ */
+ function hasDirectTextDescendantXpath() {
+ var selectorResults = ownerDocument.evaluate(axs.properties.TEXT_CONTENT_XPATH,
+ element,
+ null,
+ XPathResult.ANY_TYPE,
+ null);
+ for (var resultElement = selectorResults.iterateNext();
+ resultElement != null;
+ resultElement = selectorResults.iterateNext()) {
+ if (resultElement !== element)
+ continue;
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Determines whether element has a text node as a direct descendant.
+ * This method uses TreeWalker as a fallback (at time of writing no version
+ * of IE (including IE11) supports XPath in the HTML DOM).
+ */
+ function hasDirectTextDescendantTreeWalker() {
+ var treeWalker = ownerDocument.createTreeWalker(element,
+ NodeFilter.SHOW_TEXT,
+ null,
+ false);
+ while (treeWalker.nextNode()) {
+ var resultElement = treeWalker.currentNode;
+ var parent = resultElement.parentNode;
+ var tagName = parent.tagName.toLowerCase();
+ var value = resultElement.nodeValue.trim();
+ if (value && tagName !== 'script' && element !== resultElement)
+ return true;
+ }
+ return false;
+ }
+};
+
+/**
+ * @param {Element} element
+ * @return {Object.<string, Object>}
+ */
+axs.properties.getContrastRatioProperties = function(element) {
+ if (!axs.properties.hasDirectTextDescendant(element))
+ return null;
+
+ var contrastRatioProperties = {};
+ var style = window.getComputedStyle(element, null);
+ var bgColor = axs.utils.getBgColor(style, element);
+ if (!bgColor)
+ return null;
+
+ contrastRatioProperties['backgroundColor'] = axs.utils.colorToString(bgColor);
+ var fgColor = axs.utils.getFgColor(style, element, bgColor);
+ contrastRatioProperties['foregroundColor'] = axs.utils.colorToString(fgColor);
+ var value = axs.utils.getContrastRatioForElementWithComputedStyle(style, element);
+ if (!value)
+ return null;
+ contrastRatioProperties['value'] = value.toFixed(2);
+ if (axs.utils.isLowContrast(value, style))
+ contrastRatioProperties['alert'] = true;
+ var suggestedColors = axs.utils.suggestColors(bgColor, fgColor, value, style);
+ if (suggestedColors && Object.keys(suggestedColors).length)
+ contrastRatioProperties['suggestedColors'] = suggestedColors;
+ return contrastRatioProperties;
+};
+
+/**
+ * @param {Node} node
+ * @param {!Object} textAlternatives The properties object to fill in
+ * @param {boolean=} opt_recursive Whether this is a recursive call or not
+ * @param {boolean=} opt_force Whether to return text alternatives for this
+ * element regardless of its hidden state.
+ * @return {?string} The calculated text alternative for the given element
+ */
+axs.properties.findTextAlternatives = function(node, textAlternatives, opt_recursive, opt_force) {
+ var recursive = opt_recursive || false;
+
+ /** @type {Element} */ var element = axs.utils.asElement(node);
+ if (!element)
+ return null;
+
+ // 1. Skip hidden elements unless the author specifies to use them via an aria-labelledby or
+ // aria-describedby being used in the current computation.
+ if (!recursive && !opt_force && axs.utils.isElementOrAncestorHidden(element))
+ return null;
+
+ // if this is a text node, just return text content.
+ if (node.nodeType == Node.TEXT_NODE) {
+ var textContentValue = {};
+ textContentValue.type = 'text';
+ textContentValue.text = node.textContent;
+ textContentValue.lastWord = axs.properties.getLastWord(textContentValue.text);
+ textAlternatives['content'] = textContentValue;
+
+ return node.textContent;
+ }
+
+ var computedName = null;
+
+ if (!recursive) {
+ // 2A. The aria-labelledby attribute takes precedence as the element's text alternative
+ // unless this computation is already occurring as the result of a recursive aria-labelledby
+ // declaration.
+ computedName = axs.properties.getTextFromAriaLabelledby(element, textAlternatives);
+ }
+
+ // 2A. If aria-labelledby is empty or undefined, the aria-label attribute, which defines an
+ // explicit text string, is used.
+ if (element.hasAttribute('aria-label')) {
+ var ariaLabelValue = {};
+ ariaLabelValue.type = 'text';
+ ariaLabelValue.text = element.getAttribute('aria-label');
+ ariaLabelValue.lastWord = axs.properties.getLastWord(ariaLabelValue.text);
+ if (computedName)
+ ariaLabelValue.unused = true;
+ else if (!(recursive && axs.utils.elementIsHtmlControl(element)))
+ computedName = ariaLabelValue.text;
+ textAlternatives['ariaLabel'] = ariaLabelValue;
+ }
+
+ // 2A. If aria-labelledby and aria-label are both empty or undefined, and if the element is not
+ // marked as presentational (role="presentation", check for the presence of an equivalent host
+ // language attribute or element for associating a label, and use those mechanisms to determine
+ // a text alternative.
+ if (!element.hasAttribute('role') || element.getAttribute('role') != 'presentation') {
+ computedName = axs.properties.getTextFromHostLanguageAttributes(element,
+ textAlternatives,
+ computedName,
+ recursive);
+ }
+
+ // 2B (HTML version).
+ if (recursive && axs.utils.elementIsHtmlControl(element)) {
+ var defaultView = element.ownerDocument.defaultView;
+
+ // include the value of the embedded control as part of the text alternative in the
+ // following manner:
+ if (element instanceof defaultView.HTMLInputElement) {
+ // If the embedded control is a text field, use its value.
+ var inputElement = /** @type {HTMLInputElement} */ (element);
+ if (inputElement.type == 'text') {
+ if (inputElement.value && inputElement.value.length > 0)
+ textAlternatives['controlValue'] = { 'text': inputElement.value };
+ }
+ // If the embedded control is a range (e.g. a spinbutton or slider), use the value of the
+ // aria-valuetext attribute if available, or otherwise the value of the aria-valuenow
+ // attribute.
+ if (inputElement.type == 'range')
+ textAlternatives['controlValue'] = { 'text': inputElement.value };
+ }
+ // If the embedded control is a menu, use the text alternative of the chosen menu item.
+ // If the embedded control is a select or combobox, use the chosen option.
+ if (element instanceof defaultView.HTMLSelectElement) {
+ var inputElement = /** @type {HTMLSelectElement} */ (element);
+ textAlternatives['controlValue'] = { 'text': inputElement.value };
+ }
+
+ if (textAlternatives['controlValue']) {
+ var controlValue = textAlternatives['controlValue'];
+ if (computedName)
+ controlValue.unused = true;
+ else
+ computedName = controlValue.text;
+ }
+ }
+
+ // 2B (ARIA version).
+ if (recursive && axs.utils.elementIsAriaWidget(element)) {
+ var role = element.getAttribute('role');
+ // If the embedded control is a text field, use its value.
+ if (role == 'textbox') {
+ if (element.textContent && element.textContent.length > 0)
+ textAlternatives['controlValue'] = { 'text': element.textContent };
+ }
+ // If the embedded control is a range (e.g. a spinbutton or slider), use the value of the
+ // aria-valuetext attribute if available, or otherwise the value of the aria-valuenow
+ // attribute.
+ if (role == 'slider' || role == 'spinbutton') {
+ if (element.hasAttribute('aria-valuetext'))
+ textAlternatives['controlValue'] = { 'text': element.getAttribute('aria-valuetext') };
+ else if (element.hasAttribute('aria-valuenow'))
+ textAlternatives['controlValue'] = { 'value': element.getAttribute('aria-valuenow'),
+ 'text': '' + element.getAttribute('aria-valuenow')};
+ }
+ // If the embedded control is a menu, use the text alternative of the chosen menu item.
+ if (role == 'menu') {
+ var menuitems = element.querySelectorAll('[role=menuitemcheckbox], [role=menuitemradio]');
+ var selectedMenuitems = [];
+ for (var i = 0; i < menuitems.length; i++) {
+ if (menuitems[i].getAttribute('aria-checked') == 'true')
+ selectedMenuitems.push(menuitems[i]);
+ }
+ if (selectedMenuitems.length > 0) {
+ var selectedMenuText = '';
+ for (var i = 0; i < selectedMenuitems.length; i++) {
+ selectedMenuText += axs.properties.findTextAlternatives(selectedMenuitems[i], {}, true);
+ if (i < selectedMenuitems.length - 1)
+ selectedMenuText += ', ';
+ }
+ textAlternatives['controlValue'] = { 'text': selectedMenuText };
+ }
+ }
+ // If the embedded control is a select or combobox, use the chosen option.
+ if (role == 'combobox' || role == 'select') {
+ // TODO
+ textAlternatives['controlValue'] = { 'text': 'TODO' };
+ }
+
+ if (textAlternatives['controlValue']) {
+ var controlValue = textAlternatives['controlValue'];
+ if (computedName)
+ controlValue.unused = true;
+ else
+ computedName = controlValue.text;
+ }
+ }
+
+ // 2C. Otherwise, if the attributes checked in rules A and B didn't provide results, text is
+ // collected from descendant content if the current element's role allows "Name From: contents."
+ var hasRole = element.hasAttribute('role');
+ var canGetNameFromContents = true;
+ if (hasRole) {
+ var roleName = element.getAttribute('role');
+ // if element has a role, check that it allows "Name From: contents"
+ var role = axs.constants.ARIA_ROLES[roleName];
+ if (role && (!role.namefrom || role.namefrom.indexOf('contents') < 0))
+ canGetNameFromContents = false;
+ }
+ var textFromContent = axs.properties.getTextFromDescendantContent(element);
+ if (textFromContent && canGetNameFromContents) {
+ var textFromContentValue = {};
+ textFromContentValue.type = 'text';
+ textFromContentValue.text = textFromContent;
+ textFromContentValue.lastWord = axs.properties.getLastWord(textFromContentValue.text);
+ if (computedName)
+ textFromContentValue.unused = true;
+ else
+ computedName = textFromContent;
+ textAlternatives['content'] = textFromContentValue;
+ }
+
+ // 2D. The last resort is to use text from a tooltip attribute (such as the title attribute in
+ // HTML). This is used only if nothing else, including subtree content, has provided results.
+ if (element.hasAttribute('title')) {
+ var titleValue = {};
+ titleValue.type = 'string';
+ titleValue.valid = true;
+ titleValue.text = element.getAttribute('title');
+ titleValue.lastWord = axs.properties.getLastWord(titleValue.lastWord);
+ if (computedName)
+ titleValue.unused = true;
+ else
+ computedName = titleValue.text;
+ textAlternatives['title'] = titleValue;
+ }
+
+ if (Object.keys(textAlternatives).length == 0 && computedName == null)
+ return null;
+
+ return computedName;
+};
+
+/**
+ * @param {Element} element
+ * @return {?string}
+ */
+axs.properties.getTextFromDescendantContent = function(element) {
+ var children = element.childNodes;
+ var childrenTextContent = [];
+ for (var i = 0; i < children.length; i++) {
+ var childTextContent = axs.properties.findTextAlternatives(children[i], {}, true);
+ if (childTextContent)
+ childrenTextContent.push(childTextContent.trim());
+ }
+ if (childrenTextContent.length) {
+ var result = '';
+ // Empty children are allowed, but collapse all of them
+ for (var i = 0; i < childrenTextContent.length; i++)
+ result = [result, childrenTextContent[i]].join(' ').trim();
+ return result;
+ }
+ return null;
+};
+
+/**
+ * @param {Element} element
+ * @param {Object} textAlternatives
+ * @return {?string}
+ */
+axs.properties.getTextFromAriaLabelledby = function(element, textAlternatives) {
+ var computedName = null;
+ if (!element.hasAttribute('aria-labelledby'))
+ return computedName;
+
+ var labelledbyAttr = element.getAttribute('aria-labelledby');
+ var labelledbyIds = labelledbyAttr.split(/\s+/);
+ var labelledbyValue = {};
+ labelledbyValue.valid = true;
+ var labelledbyText = [];
+ var labelledbyValues = [];
+ for (var i = 0; i < labelledbyIds.length; i++) {
+ var labelledby = {};
+ labelledby.type = 'element';
+ var labelledbyId = labelledbyIds[i];
+ labelledby.value = labelledbyId;
+ var labelledbyElement = document.getElementById(labelledbyId);
+ if (!labelledbyElement) {
+ labelledby.valid = false;
+ labelledbyValue.valid = false;
+ labelledby.errorMessage = { 'messageKey': 'noElementWithId', 'args': [labelledbyId] };
+ } else {
+ labelledby.valid = true;
+ labelledby.text = axs.properties.findTextAlternatives(labelledbyElement, {}, true);
+ labelledby.lastWord = axs.properties.getLastWord(labelledby.text);
+ labelledbyText.push(labelledbyElement.textContent.trim());
+ labelledby.element = labelledbyElement;
+ }
+ labelledbyValues.push(labelledby);
+ }
+ if (labelledbyValues.length > 0) {
+ labelledbyValues[labelledbyValues.length - 1].last = true;
+ labelledbyValue.values = labelledbyValues;
+ labelledbyValue.text = labelledbyText.join(' ');
+ labelledbyValue.lastWord = axs.properties.getLastWord(labelledbyValue.text);
+ computedName = labelledbyValue.text;
+ textAlternatives['ariaLabelledby'] = labelledbyValue;
+ }
+
+ return computedName;
+};
+
+axs.properties.getTextFromHostLanguageAttributes = function(element,
+ textAlternatives,
+ existingComputedname,
+ recursive) {
+ var computedName = existingComputedname;
+ if (axs.browserUtils.matchSelector(element, 'img')) {
+ if (element.hasAttribute('alt')) {
+ var altValue = {};
+ altValue.type = 'string';
+ altValue.valid = true;
+ altValue.text = element.getAttribute('alt');
+ if (computedName)
+ altValue.unused = true;
+ else
+ computedName = altValue.text;
+ textAlternatives['alt'] = altValue;
+ } else {
+ var altValue = {};
+ altValue.valid = false;
+ altValue.errorMessage = 'No alt value provided';
+ textAlternatives['alt'] = altValue;
+ var src = element.src;
+ if (typeof(src) == 'string') {
+ var parts = src.split('/');
+ var filename = parts.pop();
+ var filenameValue = { text: filename }
+ textAlternatives['filename'] = filenameValue;
+ computedName = filename;
+ }
+ }
+ }
+
+ var controlsSelector = ['input:not([type="hidden"]):not([disabled])',
+ 'select:not([disabled])',
+ 'textarea:not([disabled])',
+ 'button:not([disabled])',
+ 'video:not([disabled])'].join(', ');
+ if (axs.browserUtils.matchSelector(element, controlsSelector) && !recursive) {
+ if (element.hasAttribute('id')) {
+ var labelForQuerySelector = 'label[for="' + element.id + '"]';
+ var labelsFor = document.querySelectorAll(labelForQuerySelector);
+ var labelForValue = {};
+ var labelForValues = [];
+ var labelForText = [];
+ for (var i = 0; i < labelsFor.length; i++) {
+ var labelFor = {};
+ labelFor.type = 'element';
+ var label = labelsFor[i];
+ var labelText = axs.properties.findTextAlternatives(label, {}, true);
+ if (labelText && labelText.trim().length > 0) {
+ labelFor.text = labelText.trim();
+ labelForText.push(labelText.trim());
+ }
+ labelFor.element = label;
+ labelForValues.push(labelFor);
+ }
+ if (labelForValues.length > 0) {
+ labelForValues[labelForValues.length - 1].last = true;
+ labelForValue.values = labelForValues;
+ labelForValue.text = labelForText.join(' ');
+ labelForValue.lastWord = axs.properties.getLastWord(labelForValue.text);
+ if (computedName)
+ labelForValue.unused = true;
+ else
+ computedName = labelForValue.text;
+ textAlternatives['labelFor'] = labelForValue;
+ }
+ }
+
+ var parent = axs.utils.parentElement(element);
+ var labelWrappedValue = {};
+ while (parent) {
+ if (parent.tagName.toLowerCase() == 'label') {
+ var parentLabel = /** @type {HTMLLabelElement} */ (parent);
+ if (parentLabel.control == element) {
+ labelWrappedValue.type = 'element';
+ labelWrappedValue.text = axs.properties.findTextAlternatives(parentLabel, {}, true);
+ labelWrappedValue.lastWord = axs.properties.getLastWord(labelWrappedValue.text);
+ labelWrappedValue.element = parentLabel;
+ break;
+ }
+ }
+ parent = axs.utils.parentElement(parent);
+ }
+ if (labelWrappedValue.text) {
+ if (computedName)
+ labelWrappedValue.unused = true;
+ else
+ computedName = labelWrappedValue.text;
+ textAlternatives['labelWrapped'] = labelWrappedValue;
+ }
+ if (!Object.keys(textAlternatives).length)
+ textAlternatives['noLabel'] = true;
+ }
+ return computedName;
+};
+
+/**
+ * @param {?string} text
+ * @return {?string}
+ */
+axs.properties.getLastWord = function(text) {
+ if (!text)
+ return null;
+
+ // TODO: this makes a lot of assumptions.
+ var lastSpace = text.lastIndexOf(' ') + 1;
+ var MAXLENGTH = 10;
+ var cutoff = text.length - MAXLENGTH;
+ var wordStart = lastSpace > cutoff ? lastSpace : cutoff;
+ return text.substring(wordStart);
+};
+
+/**
+ * @param {Node} node
+ * @return {Object}
+ */
+axs.properties.getTextProperties = function(node) {
+ var textProperties = {};
+ var computedName = axs.properties.findTextAlternatives(node, textProperties, false, true);
+
+ if (Object.keys(textProperties).length == 0) {
+ if (!computedName)
+ return null;
+ textProperties.hasProperties = false;
+ } else {
+ textProperties.hasProperties = true;
+ }
+
+ textProperties.computedText = computedName;
+ textProperties.lastWord = axs.properties.getLastWord(computedName);
+ return textProperties;
+};
+
+/**
+ * @param {Element} element
+ * @return {Object}
+ */
+axs.properties.getAriaProperties = function(element) {
+ var ariaProperties = {};
+ var statesAndProperties = axs.properties.getGlobalAriaProperties(element);
+
+ for (var property in axs.constants.ARIA_PROPERTIES) {
+ var attributeName = 'aria-' + property;
+ if (element.hasAttribute(attributeName)) {
+ var propertyValue = element.getAttribute(attributeName);
+ statesAndProperties[attributeName] =
+ axs.utils.getAriaPropertyValue(attributeName, propertyValue, element);
+ }
+ }
+ if (Object.keys(statesAndProperties).length > 0)
+ ariaProperties['properties'] = axs.utils.values(statesAndProperties);
+
+ var roles = axs.utils.getRoles(element);
+ if (!roles) {
+ if (Object.keys(ariaProperties).length)
+ return ariaProperties;
+ else
+ return null;
+ }
+ ariaProperties['roles'] = roles;
+ if (!roles.valid || !roles['roles'])
+ return ariaProperties;
+
+ var roleDetails = roles['roles'];
+ for (var i = 0; i < roleDetails.length; i++) {
+ var role = roleDetails[i];
+ if (!role.details || !role.details.propertiesSet)
+ continue;
+ for (var property in role.details.propertiesSet) {
+ if (property in statesAndProperties)
+ continue;
+ if (element.hasAttribute(property)) {
+ var propertyValue = element.getAttribute(property);
+ statesAndProperties[property] =
+ axs.utils.getAriaPropertyValue(property, propertyValue, element);
+ if ('values' in statesAndProperties[property]) {
+ var values = statesAndProperties[property].values;
+ values[values.length - 1].isLast = true;
+ }
+ } else if (role.details.requiredPropertiesSet[property]) {
+ statesAndProperties[property] =
+ { 'name': property, 'valid': false, 'reason': 'Required property not set' };
+ }
+ }
+ }
+ if (Object.keys(statesAndProperties).length > 0)
+ ariaProperties['properties'] = axs.utils.values(statesAndProperties);
+ if (Object.keys(ariaProperties).length > 0)
+ return ariaProperties;
+ return null;
+};
+
+/**
+ * Gets the ARIA properties which apply to all elements, not just elements with ARIA roles.
+ * @param {Element} element
+ * @return {!Object}
+ */
+axs.properties.getGlobalAriaProperties = function(element) {
+ var globalProperties = {};
+ for (var i = 0; i < axs.constants.GLOBAL_PROPERTIES.length; i++) {
+ var property = axs.constants.GLOBAL_PROPERTIES[i];
+ if (element.hasAttribute(property)) {
+ var propertyValue = element.getAttribute(property);
+ globalProperties[property] =
+ axs.utils.getAriaPropertyValue(property, propertyValue, element);
+ }
+ }
+ return globalProperties;
+};
+
+/**
+ * @param {Element} element
+ * @return {Object.<string, Object>}
+ */
+axs.properties.getVideoProperties = function(element) {
+ var videoSelector = 'video';
+ if (!axs.browserUtils.matchSelector(element, videoSelector))
+ return null;
+ var videoProperties = {};
+ videoProperties['captionTracks'] = axs.properties.getTrackElements(element, 'captions');
+ videoProperties['descriptionTracks'] = axs.properties.getTrackElements(element, 'descriptions');
+ videoProperties['chapterTracks'] = axs.properties.getTrackElements(element, 'chapters');
+ // error if no text alternatives?
+ return videoProperties;
+};
+
+/**
+ * @param {Element} element
+ * @param {string} kind
+ * @return {Object}
+ */
+axs.properties.getTrackElements = function(element, kind) {
+ // error if resource is not available
+ var trackElements = element.querySelectorAll('track[kind=' + kind + ']');
+ var result = {};
+ if (!trackElements.length) {
+ result.valid = false;
+ result.reason = { 'messageKey': 'noTracksProvided', 'args': [[kind]] };
+ return result;
+ }
+ result.valid = true;
+ var values = [];
+ for (var i = 0; i < trackElements.length; i++) {
+ var trackElement = {};
+ var src = trackElements[i].getAttribute('src');
+ var srcLang = trackElements[i].getAttribute('srcLang');
+ var label = trackElements[i].getAttribute('label');
+ if (!src) {
+ trackElement.valid = false;
+ trackElement.reason = { 'messageKey': 'noSrcProvided' };
+ } else {
+ trackElement.valid = true;
+ trackElement.src = src;
+ }
+ var name = '';
+ if (label) {
+ name += label;
+ if (srcLang)
+ name += ' ';
+ }
+ if (srcLang)
+ name += '(' + srcLang + ')';
+ if (name == '')
+ name = '[' + { 'messageKey': 'unnamed' } + ']';
+ trackElement.name = name;
+ values.push(trackElement);
+ }
+ result.values = values;
+ return result;
+};
+
+/**
+ * @param {Node} node
+ * @return {Object.<string, Object>}
+ */
+axs.properties.getAllProperties = function(node) {
+ /** @type {Element} */ var element = axs.utils.asElement(node);
+ if (!element)
+ return {};
+
+ var allProperties = {};
+ allProperties['ariaProperties'] = axs.properties.getAriaProperties(element);
+ allProperties['colorProperties'] = axs.properties.getColorProperties(element);
+ allProperties['focusProperties'] = axs.properties.getFocusProperties(element);
+ allProperties['textProperties'] = axs.properties.getTextProperties(node);
+ allProperties['videoProperties'] = axs.properties.getVideoProperties(element);
+ return allProperties;
+};