PageSourceSearch

https://wai-aria-practices.netlify.app/aria-practices/examples/landmarks/js/visua11y.js

js wai-aria-practices.netlify.app collected 2026-10-03 11:36:03 UTC 47,784 bytes, 1,699 lines download raw bytes

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.