1/* 2 * Copyright 2011-2014 University of Illinois 3 * Authors: Thomas Foltz and Jon Gunderson 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 "use strict"; 19 20var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; }; 21 22/* 23* headings.js: highlight heading elements 24*/ 25 26function initHeadings() { 27 28 addPolyfills(); 29 30 var targetList = [{ selector: "h1", color: "navy", label: "h1" }, 31 { selector: "h2", color: "olive", label: "h2" }, 32 { selector: "h3", color: "purple", label: "h3" }, 33 { selector: "h4", color: "green", label: "h4" }, 34 { selector: "h5", color: "gray", label: "h5" }, 35 { selector: "h6", color: "brown", label: "h6" }]; 36 37 var selectors = targetList.map(function (tgt) { 38 return tgt.selector; 39 }).join(', '); 40 41 function getInfo(element, target) { 42 var info = new InfoObject(element, 'HEADING INFO'); 43 info.addProps('level ' + target.label.substring(1)); 44 return info; 45 } 46 47 var params = { 48 appName: "Headings", 49 cssClass: getCssClass("Headings"), 50 msgText: "No heading elements (" + selectors + ") found.", 51 targetList: targetList, 52 getInfo: getInfo, 53 dndFlag: true 54 }; 55 56 return new Bookmarklet(params); 57} 58 59/* 60* landmarks.js: highlight ARIA landmarks 61*/ 62 63function initLandmarks() { 64 65 addPolyfills(); 66 67 // Filter function called on a list of elements returned by selector 68 // 'footer, [role="contentinfo"]'. It returns true for the following 69 // conditions: (1) element IS NOT a footer element; (2) element IS a 70 // footer element AND IS NOT a descendant of article or section. 71 function isContentinfo(element) { 72 if (element.tagName.toLowerCase() !== 'footer') return true; 73 if (!isDescendantOf(element, ['article', 'section'])) return true; 74 return false; 75 } 76 77 // Filter function called on a list of elements returned by selector 78 // 'header, [role="banner"]'. It returns true for the following 79 // conditions: (1) element IS NOT a header element; (2) element IS a 80 // header element AND IS NOT a descendant of article or section. 81 function isBanner(element) { 82 if (element.tagName.toLowerCase() !== 'header') return true; 83 if (!isDescendantOf(element, ['article', 'section'])) return true; 84 return false; 85 } 86 87 var targetList = [{ selector: 'aside:not([role]), [role~="complementary"], [role~="COMPLEMENTARY"]', color: "maroon", label: "complementary" }, 88 { selector: 'form[aria-labelledby], form[aria-label], form[title], [role~="form"], [role~="form"]', color: "maroon", label: "form" }, 89 { selector: 'footer, [role~="contentinfo"], [role~="CONTENTINFO"]', filter: isContentinfo, color: "olive", label: "contentinfo" }, 90 { selector: '[role~="application"], [role~="APPLICATION"]', color: "black", label: "application" }, 91 { selector: 'nav, [role~="navigation"], [role~="NAVIGATION"]', color: "green", label: "navigation" }, 92 { selector: '[role~="region"][aria-labelledby], [role~="REGION"][aria-labelledby]', color: "teal", label: "region" }, 93 { selector: '[role~="region"][aria-label], [role~="REGION"][aria-label]', color: "teal", label: "region" }, 94 { selector: 'section[aria-labelledby], section[aria-label]', color: "teal", label: "region" }, 95 { selector: 'header, [role~="banner"], [role~="BANNER"]', filter: isBanner, color: "gray", label: "banner" }, 96 { selector: '[role~="search"], [role~="SEARCH"]', color: "purple", label: "search" }, 97 { selector: 'main, [role~="main"], [role~="MAIN"]', color: "navy", label: "main" }]; 98 99 var selectors = targetList.map(function (tgt) { 100 return '<li>' + tgt.selector + '</li>'; 101 }).join(''); 102 103 function getInfo(element, target) { 104 return new InfoObject(element, 'LANDMARK INFO'); 105 } 106 107 var params = { 108 appName: "Landmarks", 109 cssClass: getCssClass("Landmarks"), 110 msgText: "No elements with ARIA Landmark roles found: <ul>" + selectors + "</ul>", 111 targetList: targetList, 112 getInfo: getInfo, 113 dndFlag: true 114 }; 115 116 return new Bookmarklet(params); 117} 118 119/* 120* Bookmarklet.js 121*/ 122 123/* eslint no-console: 0 */ 124function logVersionInfo(appName) { 125 console.log(getTitle() + ' : v' + getVersion() + ' : ' + appName); 126} 127 128function Bookmarklet(params) { 129 var globalName = getGlobalName(params.appName); 130 131 // use singleton pattern 132 if (_typeof(window[globalName]) === 'object') return window[globalName]; 133 134 this.appName = params.appName; 135 this.cssClass = params.cssClass; 136 this.msgText = params.msgText; 137 this.params = params; 138 this.show = false;
139 140 var dialog = new MessageDialog(); 141 window.addEventListener('resize', function () { 142 removeNodes(this.cssClass); 143 dialog.resize(); 144 this.show = false; 145 }); 146 147 window[globalName] = this; 148 logVersionInfo(this.appName); 149} 150 151Bookmarklet.prototype.run = function () { 152 var dialog = new MessageDialog(); 153 154 dialog.hide(); 155 this.show = !this.show; 156 157 if (this.show) { 158 if (addNodes(this.params) === 0) { 159 dialog.show(this.appName, this.msgText); 160 this.show = false; 161 } 162 } else { 163 removeNodes(this.cssClass); 164 } 165 166 return this.show; 167}; 168 169/* 170* InfoObject.js 171*/ 172 173/* 174* nameIncludesDescription: Determine whether accName object's name 175* property includes the accDesc object's name property content. 176*/ 177function nameIncludesDescription(accName, accDesc) { 178 if (accName === null || accDesc === null) return false; 179 180 var name = accName.name, 181 desc = accDesc.name; 182 if (typeof name === 'string' && typeof desc === 'string') { 183 return name.toLowerCase().includes(desc.toLowerCase()); 184 } 185 186 return false; 187} 188 189function InfoObject(element, title) { 190 this.title = title; 191 this.element = getElementInfo(element); 192 this.grpLabels = getGroupingLabels(element); 193 this.accName = getAccessibleName(element); 194 this.accDesc = getAccessibleDesc(element); 195 this.role = getAriaRole(element); 196 197 // Ensure that accessible description is not a duplication 198 // of accessible name content. If it is, nullify the desc. 199 if (nameIncludesDescription(this.accName, this.accDesc)) { 200 this.accDesc = null; 201 } 202} 203 204InfoObject.prototype.addProps = function (val) { 205 this.props = val; 206}; 207 208/* 209* constants.js 210*/ 211 212var CONSTANTS = {}; 213Object.defineProperty(CONSTANTS, 'classPrefix', { value: 'a11yGfdXALm' }); 214Object.defineProperty(CONSTANTS, 'globalPrefix', { value: 'a11y' }); 215Object.defineProperty(CONSTANTS, 'title', { value: 'oaa-tools/bookmarklets' }); 216Object.defineProperty(CONSTANTS, 'version', { value: '0.2.2' }); 217 218function getCssClass(appName) { 219 var prefix = CONSTANTS.classPrefix; 220 221 switch (appName) { 222 case 'Forms': 223 return prefix + '0'; 224 case 'Headings': 225 return prefix + '1'; 226 case 'Images': 227 return prefix + '2'; 228 case 'Landmarks': 229 return prefix + '3'; 230 case 'Lists': 231 return prefix + '4'; 232 case 'Interactive': 233 return prefix + '5'; 234 } 235 236 return 'unrecognizedName'; 237} 238 239function getGlobalName(appName) { 240 return CONSTANTS.globalPrefix + appName; 241} 242 243function getTitle() { 244 return CONSTANTS.title; 245} 246function getVersion() { 247 return CONSTANTS.version; 248} 249 250/* 251* dialog.js: functions for creating, modifying and deleting message dialog 252*/ 253 254/* 255* setBoxGeometry: Set the width and position of message dialog based on 256* the width of the browser window. Called by functions resizeMessage and 257* createMsgOverlay. 258*/ 259function setBoxGeometry(dialog) { 260 var width = window.innerWidth / 3.2; 261 var left = window.innerWidth / 2 - width / 2; 262 var scroll = getScrollOffsets(); 263 264 dialog.style.width = width + "px"; 265 dialog.style.left = scroll.x + left + "px"; 266 dialog.style.top = scroll.y + 30 + "px"; 267} 268 269/* 270* createMsgDialog: Construct and position the message dialog whose 271* purpose is to alert the user when no target elements are found by 272* a bookmarklet. 273*/ 274function createMsgDialog(cssClass, handler) { 275 var dialog = document.createElement("div"); 276 var button = document.createElement("button"); 277 278 dialog.className = cssClass; 279 setBoxGeometry(dialog); 280 281 button.onclick = handler; 282 283 dialog.appendChild(button); 284 document.body.appendChild(dialog); 285 return dialog; 286} 287 288/* 289* deleteMsgDialog: Use reference to delete message dialog. 290*/ 291function deleteMsgDialog(dialog) { 292 if (dialog) document.body.removeChild(dialog); 293} 294 295/* 296* MessageDialog: Wrapper for show, hide and resize methods 297*/ 298function MessageDialog() { 299 this.GLOBAL_NAME = 'a11yMessageDialog'; 300 this.CSS_CLASS = 'oaa-message-dialog'; 301} 302 303/* 304* show: show message dialog 305*/ 306MessageDialog.prototype.show = function (title, message) { 307 var MSG_DIALOG = this.GLOBAL_NAME; 308 var h2, div; 309 310 if (!window[MSG_DIALOG]) window[MSG_DIALOG] = createMsgDialog(this.CSS_CLASS, function () { 311 this.hide(); 312 }); 313 314 h2 = document.createElement("h2"); 315 h2.innerHTML = title; 316 window[MSG_DIALOG].appendChild(h2); 317 318 div = document.createElement("div"); 319 div.innerHTML = message; 320 window[MSG_DIALOG].appendChild(div); 321}; 322 323/* 324* hide: hide message dialog 325*/ 326MessageDialog.prototype.hide = function () { 327 var MSG_DIALOG = this.GLOBAL_NAME; 328 if (window[MSG_DIALOG]) { 329 deleteMsgDialog(window[MSG_DIALOG]); 330 delete window[MSG_DIALOG]; 331 } 332}; 333 334/* 335* resize: resize message dialog 336*/ 337MessageDialog.prototype.resize = function () { 338 var MSG_DIALOG = this.GLOBAL_NAME; 339 if (window[MSG_DIALOG]) setBoxGeometry(window[MSG_DIALOG]); 340}; 341 342/* 343* dom.js: functions and constants for adding and removing DOM overlay elements 344*/ 345 346/* 347* isVisible: Recursively check element properties from getComputedStyle 348* until document element is reached, to determine whether element or any 349* of its ancestors has properties set that affect its visibility. Called 350* by addNodes function. 351*/ 352function isVisible(element) { 353 354 function isVisibleRec(el) {
355 if (el.nodeType === Node.DOCUMENT_NODE) return true; 356 357 var computedStyle = window.getComputedStyle(el, null); 358 var display = computedStyle.getPropertyValue('display'); 359 var visibility = computedStyle.getPropertyValue('visibility'); 360 var hidden = el.getAttribute('hidden'); 361 var ariaHidden = el.getAttribute('aria-hidden'); 362 363 if (display === 'none' || visibility === 'hidden' || hidden !== null || ariaHidden === 'true') { 364 return false; 365 } 366 return isVisibleRec(el.parentNode); 367 } 368 369 return isVisibleRec(element); 370} 371 372/* 373* countChildrenWithTagNames: For the specified DOM element, return the 374* number of its child elements with tagName equal to one of the values 375* in the tagNames array. 376*/ 377function countChildrenWithTagNames(element, tagNames) { 378 var count = 0; 379 380 var child = element.firstElementChild; 381 while (child) { 382 if (tagNames.indexOf(child.tagName) > -1) count += 1; 383 child = child.nextElementSibling; 384 } 385 386 return count; 387} 388 389/* 390* isDescendantOf: Determine whether element is a descendant of any 391* element in the DOM with a tagName in the list of tagNames. 392*/ 393function isDescendantOf(element, tagNames) { 394 if (typeof element.closest === 'function') { 395 return tagNames.some(function (name) { 396 return element.closest(name) !== null; 397 }); 398 } 399 return false; 400} 401 402/* 403* hasParentWithName: Determine whether element has a parent with 404* tagName in the list of tagNames. 405*/ 406function hasParentWithName(element, tagNames) { 407 var parentTagName = element.parentElement.tagName.toLowerCase(); 408 if (parentTagName) { 409 return tagNames.some(function (name) { 410 return parentTagName === name; 411 }); 412 } 413 return false; 414} 415 416/* 417* addNodes: Use targetList to generate nodeList of elements and to 418* each of these, add an overlay with a unique CSS class name. 419* Optionally, if getInfo is specified, add tooltip information; 420* if dndFlag is set, add drag-and-drop functionality. 421*/ 422function addNodes(params) { 423 var targetList = params.targetList, 424 cssClass = params.cssClass, 425 getInfo = params.getInfo, 426 evalInfo = params.evalInfo, 427 dndFlag = params.dndFlag; 428 var counter = 0; 429 430 targetList.forEach(function (target) { 431 // Collect elements based on selector defined for target 432 var elements = document.querySelectorAll(target.selector); 433 434 // Filter elements if target defines a filter function 435 if (typeof target.filter === 'function') elements = Array.prototype.filter.call(elements, target.filter); 436 437 Array.prototype.forEach.call(elements, function (element) { 438 if (isVisible(element)) { 439 var info = getInfo(element, target); 440 if (evalInfo) evalInfo(info, target); 441 var boundingRect = element.getBoundingClientRect(); 442 var overlayNode = createOverlay(target, boundingRect, cssClass); 443 if (dndFlag) addDragAndDrop(overlayNode); 444 var labelNode = overlayNode.firstChild; 445 labelNode.title = formatInfo(info); 446 document.body.appendChild(overlayNode); 447 counter += 1; 448 } 449 }); 450 }); 451 452 return counter; 453} 454 455/* 456* removeNodes: Use the unique CSS class name supplied to addNodes 457* to remove all instances of the overlay nodes. 458*/ 459function removeNodes(cssClass) { 460 var selector = "div." + cssClass; 461 var elements = document.querySelectorAll(selector); 462 Array.prototype.forEach.call(elements, function (element) { 463 document.body.removeChild(element); 464 }); 465} 466 467/* 468* embedded.js 469*/ 470 471// LOW-LEVEL FUNCTIONS 472 473/* 474* getInputValue: Get current value of 'input' or 'textarea' element. 475*/ 476function getInputValue(element) { 477 return normalize(element.value); 478} 479 480/* 481* getRangeValue: Get current value of control with role 'spinbutton' 482* or 'slider' (i.e., subclass of abstract 'range' role). 483*/ 484function getRangeValue(element) { 485 var value; 486 487 value = getAttributeValue(element, 'aria-valuetext'); 488 if (value.length) return value; 489 490 value = getAttributeValue(element, 'aria-valuenow'); 491 if (value.length) return value; 492 493 return getInputValue(element); 494} 495 496// HELPER FUNCTIONS FOR SPECIFIC ROLES 497 498function getTextboxValue(element) { 499 var inputTypes = ['email', 'password', 'search', 'tel', 'text', 'url'], 500 tagName = element.tagName.toLowerCase(), 501 type = element.type; 502 503 if (tagName === 'input' && inputTypes.indexOf(type) !== -1) { 504 return getInputValue(element); 505 } 506 507 if (tagName === 'textarea') { 508 return getInputValue(element); 509 } 510 511 return ''; 512} 513 514function getComboboxValue(element) { 515 var inputTypes = ['email', 'search', 'tel', 'text', 'url'], 516 tagName = element.tagName.toLowerCase(), 517 type = element.type; 518 519 if (tagName === 'input' && inputTypes.indexOf(type) !== -1) { 520 return getInputValue(element); 521 } 522 523 return ''; 524} 525 526function getSliderValue(element) { 527 var tagName = element.tagName.toLowerCase(), 528 type = element.type; 529 530 if (tagName === 'input' && type === 'range') { 531 return getRangeValue(element); 532 } 533 534 return ''; 535} 536 537function getSpinbuttonValue(element) { 538 var tagName = element.tagName.toLowerCase(), 539 type = element.type; 540 541 if (tagName === 'input' && type === 'number') { 542 return getRangeValue(element); 543 } 544 545 return ''; 546} 547 548function getListboxValue(element) { 549 var tagName = element.tagName.toLowerCase(); 550 551 if (tagName === 'select') { 552 var arr = [], 553 selectedOptions = element.selectedOptions; 554 555 for (var i = 0; i < selectedOptions.length; i++) { 556 var option = selectedOptions[i]; 557 var value = normalize(option.value); 558 if (value.length) arr.push(value); 559 } 560 561 if (arr.length) return arr.join(' '); 562 } 563 564 return ''; 565} 566 567/* 568* isEmbeddedControl: Determine whether element has a role that corresponds
569* to an HTML form control that could be embedded within text content. 570*/ 571function isEmbeddedControl(element) { 572 var embeddedControlRoles = ['textbox', 'combobox', 'listbox', 'slider', 'spinbutton']; 573 var role = getAriaRole(element); 574 575 return embeddedControlRoles.indexOf(role) !== -1; 576} 577 578/* 579* getEmbeddedControlValue: Based on the role of element, use native semantics 580* of HTML to get the corresponding text value of the embedded control. 581*/ 582function getEmbeddedControlValue(element) { 583 var role = getAriaRole(element); 584 585 switch (role) { 586 case 'textbox': 587 return getTextboxValue(element); 588 589 case 'combobox': 590 return getComboboxValue(element); 591 592 case 'listbox': 593 return getListboxValue(element); 594 595 case 'slider': 596 return getSliderValue(element); 597 598 case 'spinbutton': 599 return getSpinbuttonValue(element); 600 } 601 602 return ''; 603} 604 605/* 606* getaccname.js 607* 608* Note: Information in this module is based on the following documents: 609* 1. HTML Accessibility API Mappings 1.0 (http://rawgit.com/w3c/aria/master/html-aam/html-aam.html) 610* 2. SVG Accessibility API Mappings (http://rawgit.com/w3c/aria/master/svg-aam/svg-aam.html) 611*/ 612 613/* 614* getFieldsetLegendLabels: Recursively collect legend contents of 615* fieldset ancestors, starting with the closest (innermost). 616* Return collection as a possibly empty array of strings. 617*/ 618function getFieldsetLegendLabels(element) { 619 var arrayOfStrings = []; 620 621 if (typeof element.closest !== 'function') { 622 return arrayOfStrings; 623 } 624 625 function getLabelsRec(elem, arr) { 626 var fieldset = elem.closest('fieldset'); 627 628 if (fieldset) { 629 var legend = fieldset.querySelector('legend'); 630 if (legend) { 631 var text = getElementContents(legend); 632 if (text.length) { 633 arr.push({ name: text, source: 'fieldset/legend' }); 634 } 635 } 636 // process ancestors 637 getLabelsRec(fieldset.parentNode, arr); 638 } 639 } 640 641 getLabelsRec(element, arrayOfStrings); 642 return arrayOfStrings; 643} 644 645/* 646* getGroupingLabels: Return an array of grouping label objects for 647* element, each with two properties: 'name' and 'source'. 648*/ 649function getGroupingLabels(element) { 650 // We currently only handle labelable elements as defined in HTML 5.1 651 if (isLabelableElement(element)) { 652 return getFieldsetLegendLabels(element); 653 } 654 655 return []; 656} 657 658/* 659* nameFromNativeSemantics: Use method appropriate to the native semantics 660* of element to find accessible name. Includes methods for all interactive 661* elements. For non-interactive elements, if the element's ARIA role allows 662* its acc. name to be derived from its text contents, or if recFlag is set, 663* indicating that we are in a recursive aria-labelledby calculation, the 664* nameFromContents method is used. 665*/ 666function nameFromNativeSemantics(element, recFlag) { 667 var tagName = element.tagName.toLowerCase(), 668 ariaRole = getAriaRole(element), 669 accName = null; 670 671 // TODO: Verify that this applies to all elements 672 if (ariaRole && (ariaRole === 'presentation' || ariaRole === 'none')) { 673 return null; 674 } 675 676 switch (tagName) { 677 // FORM ELEMENTS: INPUT 678 case 'input': 679 switch (element.type) { 680 // HIDDEN 681 case 'hidden': 682 if (recFlag) { 683 accName = nameFromLabelElement(element); 684 } 685 break; 686 687 // TEXT FIELDS 688 case 'email': 689 case 'password': 690 case 'search': 691 case 'tel': 692 case 'text': 693 case 'url': 694 accName = nameFromLabelElement(element); 695 if (accName === null) accName = nameFromAttribute(element, 'placeholder'); 696 break; 697 698 // OTHER INPUT TYPES 699 case 'button': 700 accName = nameFromAttribute(element, 'value'); 701 break; 702 703 case 'reset': 704 accName = nameFromAttribute(element, 'value'); 705 if (accName === null) accName = nameFromDefault('Reset'); 706 break; 707 708 case 'submit': 709 accName = nameFromAttribute(element, 'value'); 710 if (accName === null) accName = nameFromDefault('Submit'); 711 break; 712 713 case 'image': 714 accName = nameFromAltAttribute(element); 715 if (accName === null) accName = nameFromAttribute(element, 'value'); 716 break; 717 718 default: 719 accName = nameFromLabelElement(element); 720 break; 721 } 722 break; 723 724 // FORM ELEMENTS: OTHER 725 case 'button': 726 accName = nameFromContents(element); 727 break; 728 729 case 'label': 730 accName = nameFromContents(element); 731 break; 732 733 case 'keygen': 734 case 'meter': 735 case 'output': 736 case 'progress': 737 case 'select': 738 accName = nameFromLabelElement(element); 739 break; 740 741 case 'textarea': 742 accName = nameFromLabelElement(element); 743 if (accName === null) accName = nameFromAttribute(element, 'placeholder'); 744 break; 745 746 // EMBEDDED ELEMENTS 747 case 'audio': 748 // if 'controls' attribute is present
749 accName = { name: 'NOT YET IMPLEMENTED', source: '' }; 750 break; 751 752 case 'embed': 753 accName = { name: 'NOT YET IMPLEMENTED', source: '' }; 754 break; 755 756 case 'iframe': 757 accName = nameFromAttribute(element, 'title'); 758 break; 759 760 case 'img': 761 case 'area': 762 // added 763 accName = nameFromAltAttribute(element); 764 break; 765 766 case 'object': 767 accName = { name: 'NOT YET IMPLEMENTED', source: '' }; 768 break; 769 770 case 'svg': 771 // added 772 accName = nameFromDescendant(element, 'title'); 773 break; 774 775 case 'video': 776 // if 'controls' attribute is present 777 accName = { name: 'NOT YET IMPLEMENTED', source: '' }; 778 break; 779 780 // OTHER ELEMENTS 781 case 'a': 782 accName = nameFromContents(element); 783 break; 784 785 case 'details': 786 accName = nameFromDetailsOrSummary(element); 787 break; 788 789 case 'figure': 790 accName = nameFromDescendant(element, 'figcaption'); 791 break; 792 793 case 'table': 794 accName = nameFromDescendant(element, 'caption'); 795 break; 796 797 // ELEMENTS NOT SPECIFIED ABOVE 798 default: 799 if (nameFromIncludesContents(element) || recFlag) accName = nameFromContents(element); 800 break; 801 } 802 803 // LAST RESORT USE TITLE 804 if (accName === null) accName = nameFromAttribute(element, 'title'); 805 806 return accName; 807} 808 809/* 810* nameFromAttributeIdRefs: Get the value of attrName on element (a space- 811* separated list of IDREFs), visit each referenced element in the order it 812* appears in the list and obtain its accessible name (skipping recursive 813* aria-labelledby or aria-describedby calculations), and return an object 814* with name property set to a string that is a space-separated concatenation 815* of those results if any, otherwise return null. 816*/ 817function nameFromAttributeIdRefs(element, attribute) { 818 var value = getAttributeValue(element, attribute); 819 var idRefs, 820 refElement, 821 accName, 822 arr = []; 823 824 if (value.length) { 825 idRefs = value.split(' '); 826 827 for (var i = 0; i < idRefs.length; i++) { 828 refElement = document.getElementById(idRefs[i]); 829 if (refElement) { 830 accName = getAccessibleName(refElement, true); 831 if (accName && accName.name.length) arr.push(accName.name); 832 } 833 } 834 } 835 836 if (arr.length) return { name: arr.join(' '), source: attribute }; 837 838 return null; 839} 840 841/* 842* getAccessibleName: Use the ARIA Roles Model specification for accessible 843* name calculation based on its precedence order: 844* (1) Use aria-labelledby, unless a traversal is already underway; 845* (2) Use aria-label attribute value; 846* (3) Use whatever method is specified by the native semantics of the 847* element, which includes, as last resort, use of the title attribute. 848*/ 849function getAccessibleName(element, recFlag) { 850 var accName = null; 851 852 if (!recFlag) accName = nameFromAttributeIdRefs(element, 'aria-labelledby'); 853 if (accName === null) accName = nameFromAttribute(element, 'aria-label'); 854 if (accName === null) accName = nameFromNativeSemantics(element, recFlag); 855 856 return accName; 857} 858 859/* 860* getAccessibleDesc: Use the ARIA Roles Model specification for accessible 861* description calculation based on its precedence order: 862* (1) Use aria-describedby, unless a traversal is already underway; 863* (2) As last resort, use the title attribute. 864*/ 865function getAccessibleDesc(element, recFlag) { 866 var accDesc = null; 867 868 if (!recFlag) accDesc = nameFromAttributeIdRefs(element, 'aria-describedby'); 869 if (accDesc === null) accDesc = nameFromAttribute(element, 'title'); 870 871 return accDesc; 872} 873 874/* 875* info.js: Function for displaying information on highlighted elements 876*/ 877 878/* 879* getElementInfo: Extract tagName and other attribute information 880* based on tagName and return as formatted string. 881*/ 882function getElementInfo(element) { 883 var tagName = element.tagName.toLowerCase(), 884 elementInfo = tagName; 885 886 if (tagName === 'input') { 887 var type = element.type; 888 if (type && type.length) elementInfo += ' [type="' + type + '"]'; 889 } 890 891 if (tagName === 'label') { 892 var forVal = getAttributeValue(element, 'for'); 893 if (forVal.length) elementInfo += ' [for="' + forVal + '"]'; 894 } 895 896 if (isLabelableElement(element)) {
897 var id = element.id; 898 if (id && id.length) elementInfo += ' [id="' + id + '"]'; 899 } 900 901 if (element.hasAttribute('role')) { 902 var role = getAttributeValue(element, 'role'); 903 if (role.length) elementInfo += ' [role="' + role + '"]'; 904 } 905 906 return elementInfo; 907} 908 909/* 910* formatInfo: Convert info properties into a string with line breaks. 911*/ 912function formatInfo(info) { 913 var value = ''; 914 var title = info.title, 915 element = info.element, 916 grpLabels = info.grpLabels, 917 accName = info.accName, 918 accDesc = info.accDesc, 919 role = info.role, 920 props = info.props; 921 922 value += '=== ' + title + ' ==='; 923 924 if (element) value += '\nELEMENT: ' + element; 925 926 if (grpLabels && grpLabels.length) { 927 // array starts with innermost label, so process from the end 928 for (var i = grpLabels.length - 1; i >= 0; i--) { 929 value += '\nGRP. LABEL: ' + grpLabels[i].name + '\nFROM: ' + grpLabels[i].source; 930 } 931 } 932 933 if (accName) { 934 value += '\nACC. NAME: ' + accName.name + '\nFROM: ' + accName.source; 935 } 936 937 if (accDesc) { 938 value += '\nACC. DESC: ' + accDesc.name + '\nFROM: ' + accDesc.source; 939 } 940 941 if (role) value += '\nROLE: ' + role; 942 943 if (props) value += '\nPROPERTIES: ' + props; 944 945 return value; 946} 947 948/* 949* namefrom.js 950*/ 951 952// LOW-LEVEL FUNCTIONS 953 954/* 955* normalize: Trim leading and trailing whitespace and condense all 956* internal sequences of whitespace to a single space. Adapted from 957* Mozilla documentation on String.prototype.trim polyfill. Handles 958* BOM and NBSP characters. 959*/ 960function normalize(s) { 961 var rtrim = /^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g; 962 return s.replace(rtrim, '').replace(/\s+/g, ' '); 963} 964 965/* 966* getAttributeValue: Return attribute value if present on element, 967* otherwise return empty string. 968*/ 969function getAttributeValue(element, attribute) { 970 var value = element.getAttribute(attribute); 971 return value === null ? '' : normalize(value); 972} 973 974/* 975* couldHaveAltText: Based on HTML5 specification, determine whether 976* element could have an 'alt' attribute. 977*/ 978function couldHaveAltText(element) { 979 var tagName = element.tagName.toLowerCase(); 980 981 switch (tagName) { 982 case 'img': 983 case 'area': 984 return true; 985 case 'input': 986 return element.type && element.type === 'image'; 987 } 988 989 return false; 990} 991 992/* 993* hasEmptyAltText: Determine whether the alt attribute is present 994* and its value is the empty string. 995*/ 996function hasEmptyAltText(element) { 997 var value = element.getAttribute('alt'); 998 999 // Attribute is present 1000 if (value !== null) return normalize(value).length === 0; 1001 1002 return false; 1003} 1004 1005/* 1006* isLabelableElement: Based on HTML5 specification, determine whether 1007* element can be associated with a label. 1008*/ 1009function isLabelableElement(element) { 1010 var tagName = element.tagName.toLowerCase(), 1011 type = element.type; 1012 1013 switch (tagName) { 1014 case 'input': 1015 return type !== 'hidden'; 1016 case 'button': 1017 case 'keygen': 1018 case 'meter': 1019 case 'output': 1020 case 'progress': 1021 case 'select': 1022 case 'textarea': 1023 return true; 1024 default: 1025 return false; 1026 } 1027} 1028 1029/* 1030* addCssGeneratedContent: Add CSS-generated content for pseudo-elements 1031* :before and :after. According to the CSS spec, test that content value 1032* is other than the default computed value of 'none'. 1033* 1034* Note: Even if an author specifies content: 'none', because browsers add 1035* the double-quote character to the beginning and end of computed string 1036* values, the result cannot and will not be equal to 'none'. 1037*/ 1038function addCssGeneratedContent(element, contents) { 1039 var result = contents, 1040 prefix = getComputedStyle(element, ':before').content, 1041 suffix = getComputedStyle(element, ':after').content; 1042 1043 if (prefix !== 'none') result = prefix + result; 1044 if (suffix !== 'none') result = result + suffix; 1045 1046 return result; 1047} 1048 1049/* 1050* getNodeContents: Recursively process element and text nodes by aggregating 1051* their text values for an ARIA text equivalent calculation. 1052* 1. This includes special handling of elements with 'alt' text and embedded 1053* controls. 1054* 2. The forElem parameter is needed for label processing to avoid inclusion 1055* of an embedded control's value when the label is for the control itself. 1056*/ 1057function getNodeContents(node, forElem) { 1058 var contents = ''; 1059 1060 if (node === forElem) return ''; 1061 1062 switch (node.nodeType) { 1063 case Node.ELEMENT_NODE: 1064 if (couldHaveAltText(node)) { 1065 contents = getAttributeValue(node, 'alt'); 1066 } else if (isEmbeddedControl(node)) { 1067 contents = getEmbeddedControlValue(node); 1068 } else { 1069 if (node.hasChildNodes()) { 1070 var children = node.childNodes, 1071 arr = []; 1072 1073 for (var i = 0; i < children.length; i++) { 1074 var nc = getNodeContents(children[i], forElem); 1075 if (nc.length) arr.push(nc); 1076 } 1077 1078 contents = arr.length ? arr.join(' ') : ''; 1079 } 1080 } 1081 // For all branches of the ELEMENT_NODE case... 1082 contents = addCssGeneratedContent(node, contents); 1083 break; 1084 1085 case Node.TEXT_NODE: 1086 contents = normalize(node.textContent); 1087 } 1088 1089 return contents; 1090} 1091 1092/* 1093* getElementContents: Construct the ARIA text alternative for element by 1094* processing its element and text node descendants and then adding any CSS- 1095* generated content if present. 1096*/ 1097function getElementContents(element, forElement) { 1098 var result = ''; 1099 1100 if (element.hasChildNodes()) { 1101 var children = element.childNodes, 1102 arrayOfStrings = []; 1103 1104 for (var i = 0; i < children.length; i++) { 1105 var contents = getNodeContents(children[i], forElement); 1106 if (contents.length) arrayOfStrings.push(contents); 1107 } 1108 1109 result = arrayOfStrings.length ? arrayOfStrings.join(' ') : ''; 1110 } 1111 1112 return addCssGeneratedContent(element, result); 1113} 1114 1115/* 1116* getContentsOfChildNodes: Using predicate function for filtering element 1117* nodes, collect text content from all child nodes of element. 1118*/ 1119function getContentsOfChildNodes(element, predicate) { 1120 var arr = [], 1121 content; 1122 1123 Array.prototype.forEach.call(element.childNodes, function (node) { 1124 switch (node.nodeType) { 1125 case Node.ELEMENT_NODE: 1126 if (predicate(node)) { 1127 content = getElementContents(node); 1128 if (content.length) arr.push(content); 1129 } 1130 break; 1131 case Node.TEXT_NODE: 1132 content = normalize(node.textContent); 1133 if (content.length) arr.push(content); 1134 break; 1135 } 1136 }); 1137 1138 if (arr.length) return arr.join(' '); 1139 return ''; 1140} 1141 1142// HIGHER-LEVEL FUNCTIONS THAT RETURN AN OBJECT WITH SOURCE PROPERTY 1143 1144/* 1145* nameFromAttribute 1146*/ 1147function nameFromAttribute(element, attribute) { 1148 var name; 1149 1150 name = getAttributeValue(element, attribute); 1151 if (name.length) return { name: name, source: attribute }; 1152 1153 return null; 1154} 1155 1156/* 1157* nameFromAltAttribute 1158*/ 1159function nameFromAltAttribute(element) { 1160 var name = element.getAttribute('alt'); 1161 1162 // Attribute is present 1163 if (name !== null) { 1164 name = normalize(name); 1165 return name.length ? { name: name, source: 'alt' } : { name: '<empty>', source: 'alt' }; 1166 } 1167 1168 // Attribute not present 1169 return null; 1170} 1171 1172/* 1173* nameFromContents 1174*/ 1175function nameFromContents(element) { 1176 var name; 1177 1178 name = getElementContents(element); 1179 if (name.length) return { name: name, source: 'contents' }; 1180 1181 return null; 1182} 1183 1184/* 1185* nameFromDefault 1186*/ 1187function nameFromDefault(name) { 1188 return name.length ? { name: name, source: 'default' } : null; 1189} 1190 1191/*
1192* nameFromDescendant 1193*/ 1194function nameFromDescendant(element, tagName) { 1195 var descendant = element.querySelector(tagName); 1196 if (descendant) { 1197 var name = getElementContents(descendant); 1198 if (name.length) return { name: name, source: tagName + ' element' }; 1199 } 1200 1201 return null; 1202} 1203 1204/* 1205* nameFromLabelElement 1206*/ 1207function nameFromLabelElement(element) { 1208 var name, label; 1209 1210 // label [for=id] 1211 if (element.id) { 1212 label = document.querySelector('[for="' + element.id + '"]'); 1213 if (label) { 1214 name = getElementContents(label, element); 1215 if (name.length) return { name: name, source: 'label reference' }; 1216 } 1217 } 1218 1219 // label encapsulation 1220 if (typeof element.closest === 'function') { 1221 label = element.closest('label'); 1222 if (label) { 1223 name = getElementContents(label, element); 1224 if (name.length) return { name: name, source: 'label encapsulation' }; 1225 } 1226 } 1227 1228 return null; 1229} 1230 1231/* 1232* nameFromDetailsOrSummary: If element is expanded (has open attribute), 1233* return the contents of the summary element followed by the text contents 1234* of element and all of its non-summary child elements. Otherwise, return 1235* only the contents of the first summary element descendant. 1236*/ 1237function nameFromDetailsOrSummary(element) { 1238 var name, summary; 1239 1240 function isExpanded(elem) { 1241 return elem.hasAttribute('open'); 1242 } 1243 1244 // At minimum, always use summary contents 1245 summary = element.querySelector('summary'); 1246 if (summary) name = getElementContents(summary); 1247 1248 // Return either summary + details (non-summary) or summary only 1249 if (isExpanded(element)) { 1250 name += getContentsOfChildNodes(element, function (elem) { 1251 return elem.tagName.toLowerCase() !== 'summary'; 1252 }); 1253 if (name.length) return { name: name, source: 'contents' }; 1254 } else { 1255 if (name.length) return { name: name, source: 'summary element' }; 1256 } 1257 1258 return null; 1259} 1260 1261/* 1262* overlay.js: functions for creating and modifying DOM overlay elements 1263*/ 1264 1265var zIndex = 100000; 1266 1267/* 1268* createOverlay: Create overlay div with size and position based on the 1269* boundingRect properties of its corresponding target element. 1270*/ 1271function createOverlay(tgt, rect, cname) { 1272 var scrollOffsets = getScrollOffsets(); 1273 var MINWIDTH = 68; 1274 var MINHEIGHT = 27; 1275 1276 var node = document.createElement("div"); 1277 node.setAttribute("class", [cname, 'oaa-element-overlay'].join(' ')); 1278 node.startLeft = rect.left + scrollOffsets.x + "px"; 1279 node.startTop = rect.top + scrollOffsets.y + "px"; 1280 1281 node.style.left = node.startLeft; 1282 node.style.top = node.startTop; 1283 node.style.width = Math.max(rect.width, MINWIDTH) + "px"; 1284 node.style.height = Math.max(rect.height, MINHEIGHT) + "px"; 1285 node.style.borderColor = tgt.color; 1286 node.style.zIndex = zIndex; 1287 1288 var label = document.createElement("div"); 1289 label.setAttribute("class", 'oaa-overlay-label'); 1290 label.style.backgroundColor = tgt.color; 1291 label.innerHTML = tgt.label; 1292 1293 node.appendChild(label); 1294 return node; 1295} 1296 1297/* 1298* addDragAndDrop: Add drag-and-drop and reposition functionality to an 1299* overlay div element created by the createOverlay function. 1300*/ 1301function addDragAndDrop(node) { 1302 1303 function hoistZIndex(el) { 1304 var incr = 100; 1305 el.style.zIndex = zIndex += incr; 1306 } 1307 1308 function repositionOverlay(el) { 1309 el.style.left = el.startLeft; 1310 el.style.top = el.startTop; 1311 } 1312 1313 var labelDiv = node.firstChild; 1314 1315 labelDiv.onmousedown = function (event) { 1316 drag(this.parentNode, hoistZIndex, event); 1317 }; 1318 1319 labelDiv.ondblclick = function (event) { 1320 repositionOverlay(this.parentNode); 1321 }; 1322} 1323 1324/* 1325* roles.js 1326* 1327* Note: The information in this module is based on the following documents: 1328* 1. ARIA in HTML (https://specs.webplatform.org/html-aria/webspecs/master/) 1329* 2. WAI-ARIA 1.1 (http://www.w3.org/TR/wai-aria-1.1/) 1330* 3. WAI-ARIA 1.0 (http://www.w3.org/TR/wai-aria/) 1331*/ 1332 1333/* 1334* inListOfOptions: Determine whether element is a child of 1335* 1. a select element 1336* 2. an optgroup element that is a child of a select element 1337* 3. a datalist element 1338*/ 1339function inListOfOptions(element) { 1340 var parent = element.parentElement, 1341 parentName = parent.tagName.toLowerCase(), 1342 parentOfParentName = parent.parentElement.tagName.toLowerCase(); 1343 1344 if (parentName === 'select') return true; 1345 1346 if (parentName === 'optgroup' && parentOfParentName === 'select') return true; 1347 1348 if (parentName === 'datalist') return true; 1349 1350 return false;
1351} 1352 1353/* 1354* validRoles: Reference list of all concrete ARIA roles as specified in 1355* WAI-ARIA 1.1 Working Draft of 14 July 2015 1356*/ 1357var validRoles = [ 1358 1359// LANDMARK 1360'application', 'banner', 'complementary', 'contentinfo', 'form', 'main', 'navigation', 'search', 1361 1362// WIDGET 1363'alert', 'alertdialog', 'button', 'checkbox', 'dialog', 'gridcell', 'link', 'log', 'marquee', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'progressbar', 'radio', 'scrollbar', 'searchbox', // ARIA 1.1 1364'slider', 'spinbutton', 'status', 'switch', // ARIA 1.1 1365'tab', 'tabpanel', 'textbox', 'timer', 'tooltip', 'treeitem', 1366 1367// COMPOSITE WIDGET 1368'combobox', 'grid', 'listbox', 'menu', 'menubar', 'radiogroup', 'tablist', 'tree', 'treegrid', 1369 1370// DOCUMENT STRUCTURE 1371'article', 'cell', // ARIA 1.1 1372'columnheader', 'definition', 'directory', 'document', 'group', 'heading', 'img', 'list', 'listitem', 'math', 'none', // ARIA 1.1 1373'note', 'presentation', 'region', 'row', 'rowgroup', 'rowheader', 'separator', 'table', // ARIA 1.1 1374'text', // ARIA 1.1 1375'toolbar']; 1376 1377/* 1378* getValidRole: Examine each value in space-separated list by attempting 1379* to find its match in the validRoles array. If a match is found, return 1380* it. Otherwise, return null. 1381*/ 1382function getValidRole(spaceSepList) { 1383 var arr = spaceSepList.split(' '); 1384 1385 for (var i = 0; i < arr.length; i++) { 1386 var value = arr[i].toLowerCase(); 1387 var validRole = validRoles.find(function (role) { 1388 return role === value; 1389 }); 1390 if (validRole) return validRole; 1391 } 1392 1393 return null; 1394} 1395 1396/* 1397* getAriaRole: Get the value of the role attribute, if it is present. If 1398* not specified, get the default role of element if it has one. Based on 1399* ARIA in HTML as of 21 October 2015. 1400*/ 1401function getAriaRole(element) { 1402 var tagName = element.tagName.toLowerCase(), 1403 type = element.type; 1404 1405 if (element.hasAttribute('role')) { 1406 return getValidRole(getAttributeValue(element, 'role')); 1407 } 1408 1409 switch (tagName) { 1410 1411 case 'a': 1412 if (element.hasAttribute('href')) return 'link'; 1413 break; 1414 1415 case 'area': 1416 if (element.hasAttribute('href')) return 'link'; 1417 break; 1418 1419 case 'article': 1420 return 'article'; 1421 case 'aside': 1422 return 'complementary'; 1423 case 'body': 1424 return 'document'; 1425 case 'button': 1426 return 'button'; 1427 case 'datalist': 1428 return 'listbox'; 1429 case 'details': 1430 return 'group'; 1431 case 'dialog': 1432 return 'dialog'; 1433 case 'dl': 1434 return 'list'; 1435 case 'fieldset': 1436 return 'group'; 1437 1438 case 'footer': 1439 if (!isDescendantOf(element, ['article', 'section'])) return 'contentinfo'; 1440 break; 1441 1442 case 'form': 1443 return 'form'; 1444 1445 case 'h1': 1446 return 'heading'; 1447 case 'h2': 1448 return 'heading'; 1449 case 'h3': 1450 return 'heading'; 1451 case 'h4': 1452 return 'heading'; 1453 case 'h5': 1454 return 'heading'; 1455 case 'h6': 1456 return 'heading'; 1457 1458 case 'header': 1459 if (!isDescendantOf(element, ['article', 'section'])) return 'banner'; 1460 break; 1461 1462 case 'hr': 1463 return 'separator'; 1464 1465 case 'img': 1466 if (!hasEmptyAltText(element)) return 'img'; 1467 break; 1468 1469 case 'input': 1470 if (type === 'button') return 'button'; 1471 if (type === 'checkbox') return 'checkbox'; 1472 if (type === 'email') return element.hasAttribute('list') ? 'combobox' : 'textbox'; 1473 if (type === 'image') return 'button'; 1474 if (type === 'number') return 'spinbutton'; 1475 if (type === 'password') return 'textbox'; 1476 if (type === 'radio') return 'radio'; 1477 if (type === 'range') return 'slider'; 1478 if (type === 'reset') return 'button'; 1479 if (type === 'search') return element.hasAttribute('list') ? 'combobox' : 'textbox'; 1480 if (type === 'submit') return 'button'; 1481 if (type === 'tel') return element.hasAttribute('list') ? 'combobox' : 'textbox'; 1482 if (type === 'text') return element.hasAttribute('list') ? 'combobox' : 'textbox'; 1483 if (type === 'url') return element.hasAttribute('list') ? 'combobox' : 'textbox'; 1484 break; 1485 1486 case 'li': 1487 if (hasParentWithName(element, ['ol', 'ul'])) return 'listitem'; 1488 break; 1489 1490 case 'link': 1491 if (element.hasAttribute('href')) return 'link'; 1492 break; 1493 1494 case 'main': 1495 return 'main'; 1496 1497 case 'menu': 1498 if (type === 'toolbar') return 'toolbar'; 1499 break; 1500 1501 case 'menuitem': 1502 if (type === 'command') return 'menuitem'; 1503 if (type === 'checkbox') return 'menuitemcheckbox'; 1504 if (type === 'radio') return 'menuitemradio'; 1505 break; 1506 1507 case 'meter': 1508 return 'progressbar'; 1509 case 'nav': 1510 return 'navigation'; 1511 case 'ol': 1512 return 'list'; 1513 1514 case 'option': 1515 if (inListOfOptions(element)) return 'option'; 1516 break; 1517 1518 case 'output': 1519 return 'status'; 1520 case 'progress': 1521 return 'progressbar'; 1522 case 'section': 1523 return 'region'; 1524 case 'select': 1525 return 'listbox'; 1526 case 'summary': 1527 return 'button'; 1528 1529 case 'tbody': 1530 return 'rowgroup'; 1531 case 'tfoot': 1532 return 'rowgroup'; 1533 case 'thead': 1534 return 'rowgroup'; 1535 1536 case 'textarea': 1537 return 'textbox'; 1538
1539 // TODO: th can have role 'columnheader' or 'rowheader' 1540 case 'th': 1541 return 'columnheader'; 1542 1543 case 'ul': 1544 return 'list'; 1545 } 1546 1547 return null; 1548} 1549 1550/* 1551* nameFromIncludesContents: Determine whether the ARIA role of element 1552* specifies that its 'name from' includes 'contents'. 1553*/ 1554function nameFromIncludesContents(element) { 1555 var elementRole = getAriaRole(element); 1556 if (elementRole === null) return false; 1557 1558 var contentsRoles = ['button', 'cell', // ARIA 1.1 1559 'checkbox', 'columnheader', 'directory', 'gridcell', 'heading', 'link', 'listitem', 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'radio', 'row', 'rowgroup', 'rowheader', 'switch', // ARIA 1.1 1560 'tab', 'text', // ARIA 1.1 1561 'tooltip', 'treeitem']; 1562 1563 var contentsRole = contentsRoles.find(function (role) { 1564 return role === elementRole; 1565 }); 1566 return typeof contentsRole !== 'undefined'; 1567} 1568 1569/* 1570* utils.js: utility functions 1571*/ 1572 1573/* 1574* getScrollOffsets: Use x and y scroll offsets to calculate positioning 1575* coordinates that take into account whether the page has been scrolled. 1576* From Mozilla Developer Network: Element.getBoundingClientRect() 1577*/ 1578function getScrollOffsets() { 1579 var t; 1580 1581 var xOffset = typeof window.pageXOffset === "undefined" ? (((t = document.documentElement) || (t = document.body.parentNode)) && typeof t.ScrollLeft === 'number' ? t : document.body).ScrollLeft : window.pageXOffset; 1582 1583 var yOffset = typeof window.pageYOffset === "undefined" ? (((t = document.documentElement) || (t = document.body.parentNode)) && typeof t.ScrollTop === 'number' ? t : document.body).ScrollTop : window.pageYOffset; 1584 1585 return { x: xOffset, y: yOffset }; 1586} 1587 1588/* 1589* drag: Add drag and drop functionality to an element by setting this 1590* as its mousedown handler. Depends upon getScrollOffsets function. 1591* From JavaScript: The Definitive Guide, 6th Edition (slightly modified) 1592*/ 1593function drag(elementToDrag, dragCallback, event) { 1594 var scroll = getScrollOffsets(); 1595 var startX = event.clientX + scroll.x; 1596 var startY = event.clientY + scroll.y; 1597 1598 var origX = elementToDrag.offsetLeft; 1599 var origY = elementToDrag.offsetTop; 1600 1601 var deltaX = startX - origX; 1602 var deltaY = startY - origY; 1603 1604 if (dragCallback) dragCallback(elementToDrag); 1605 1606 if (document.addEventListener) { 1607 document.addEventListener("mousemove", moveHandler, true); 1608 document.addEventListener("mouseup", upHandler, true); 1609 } else if (document.attachEvent) { 1610 elementToDrag.setCapture(); 1611 elementToDrag.attachEvent("onmousemove", moveHandler); 1612 elementToDrag.attachEvent("onmouseup", upHandler); 1613 elementToDrag.attachEvent("onlosecapture", upHandler); 1614 } 1615 1616 if (event.stopPropagation) event.stopPropagation();else event.cancelBubble = true; 1617 1618 if (event.preventDefault) event.preventDefault();else event.returnValue = false; 1619 1620 function moveHandler(e) { 1621 if (!e) e = window.event; 1622 1623 var scroll = getScrollOffsets(); 1624 elementToDrag.style.left = e.clientX + scroll.x - deltaX + "px"; 1625 elementToDrag.style.top = e.clientY + scroll.y - deltaY + "px"; 1626 1627 elementToDrag.style.cursor = "move"; 1628 1629 if (e.stopPropagation) e.stopPropagation();else e.cancelBubble = true; 1630 } 1631 1632 function upHandler(e) { 1633 if (!e) e = window.event; 1634 1635 elementToDrag.style.cursor = "grab"; 1636 elementToDrag.style.cursor = "-moz-grab"; 1637 elementToDrag.style.cursor = "-webkit-grab"; 1638 1639 if (document.removeEventListener) { 1640 document.removeEventListener("mouseup", upHandler, true); 1641 document.removeEventListener("mousemove", moveHandler, true); 1642 } else if (document.detachEvent) { 1643 elementToDrag.detachEvent("onlosecapture", upHandler); 1644 elementToDrag.detachEvent("onmouseup", upHandler); 1645 elementToDrag.detachEvent("onmousemove", moveHandler); 1646 elementToDrag.releaseCapture(); 1647 } 1648 1649 if (e.stopPropagation) e.stopPropagation();else e.cancelBubble = true; 1650 } 1651} 1652 1653/* 1654* addPolyfills: Add polyfill implementations for JavaScript object methods 1655* defined in ES6 and used by bookmarklets: 1656* 1. Array.prototype.find 1657* 2. String.prototype.includes 1658*/ 1659function addPolyfills() { 1660 1661 // From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find 1662 if (!Array.prototype.find) { 1663 Array.prototype.find = function (predicate) { 1664 if (this === null) { 1665 throw new TypeError('Array.prototype.find called on null or undefined'); 1666 } 1667 if (typeof predicate !== 'function') { 1668 throw new TypeError('predicate must be a function'); 1669 } 1670 var list = Object(this); 1671 var length = list.length >>> 0; 1672 var thisArg = arguments[1]; 1673 var value; 1674 1675 for (var i = 0; i < length; i++) { 1676 value = list[i]; 1677 if (predicate.call(thisArg, value, i, list)) { 1678 return value; 1679 } 1680 } 1681 return undefined; 1682 }; 1683 } 1684 1685 // From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes 1686 if (!String.prototype.includes) { 1687 String.prototype.includes = function (search, start) { 1688 if (typeof start !== 'number') { 1689 start = 0; 1690 } 1691 1692 if (start + search.length > this.length) { 1693 return false;
1694 } else { 1695 return this.indexOf(search, start) !== -1; 1696 } 1697 }; 1698 } 1699}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.