@prairielearn/browser-utils 2.6.0 → 2.6.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +15 -0
- package/dist/downloads.d.ts.map +1 -1
- package/dist/downloads.js.map +1 -1
- package/dist/encode-data.d.ts.map +1 -1
- package/dist/encode-data.js.map +1 -1
- package/dist/focus.d.ts.map +1 -1
- package/dist/focus.js +1 -1
- package/dist/focus.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/on-document-ready.d.ts.map +1 -1
- package/dist/on-document-ready.js.map +1 -1
- package/dist/parse-html.d.ts.map +1 -1
- package/dist/parse-html.js.map +1 -1
- package/dist/template-from-attributes.d.ts.map +1 -1
- package/dist/template-from-attributes.js.map +1 -1
- package/package.json +5 -4
- package/src/focus.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# @prairielearn/browser-utils
|
|
2
2
|
|
|
3
|
+
## 2.6.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [3914bb4]
|
|
8
|
+
- @prairielearn/html@5.0.0
|
|
9
|
+
|
|
10
|
+
## 2.6.1
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- 0900843: Switch to the `tsgo` compiler
|
|
15
|
+
- Updated dependencies [0900843]
|
|
16
|
+
- @prairielearn/html@4.0.24
|
|
17
|
+
|
|
3
18
|
## 2.6.0
|
|
4
19
|
|
|
5
20
|
### Minor Changes
|
package/dist/downloads.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"downloads.d.ts","sourceRoot":"","sources":["../src/downloads.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAW1F;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAGhE;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAGzF"}
|
|
1
|
+
{"version":3,"file":"downloads.d.ts","sourceRoot":"","sources":["../src/downloads.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAW1F;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,GAAG,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAGhE;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI,CAGzF","sourcesContent":["import { stringify } from 'csv-stringify/browser/esm/sync';\n\n/**\n * Triggers a browser download of a text-based file.\n *\n * @param content The content of the file.\n * @param filename The desired filename.\n * @param mimeType The MIME type of the file.\n */\nexport function downloadTextFile(content: string, filename: string, mimeType: string): void {\n if (typeof window === 'undefined') return;\n const blob = new Blob([content], { type: mimeType });\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = filename;\n document.body.append(a);\n a.click();\n a.remove();\n URL.revokeObjectURL(url);\n}\n\n/**\n * Triggers a browser download of a JSON file.\n *\n * @param data The data to be included in the JSON file.\n * @param filename The desired filename.\n */\nexport function downloadAsJSON(data: any, filename: string): void {\n const jsonContent = JSON.stringify(data, null, 2);\n downloadTextFile(jsonContent, filename, 'application/json');\n}\n\n/**\n * Triggers a browser download of a CSV file.\n *\n * @param header The header row of the CSV.\n * @param data The data rows of the CSV.\n * @param filename The desired filename.\n */\nexport function downloadAsCSV(header: string[], data: unknown[][], filename: string): void {\n const csvContent = stringify(data, { header: true, columns: header });\n downloadTextFile(csvContent, filename, 'text/csv');\n}\n"]}
|
package/dist/downloads.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"downloads.js","sourceRoot":"","sources":["../src/downloads.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAE3D;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAe,EAAE,QAAgB,EAAE,QAAgB;
|
|
1
|
+
{"version":3,"file":"downloads.js","sourceRoot":"","sources":["../src/downloads.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,gCAAgC,CAAC;AAE3D;;;;;;GAMG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAe,EAAE,QAAgB,EAAE,QAAgB,EAAQ;IAC1F,IAAI,OAAO,MAAM,KAAK,WAAW;QAAE,OAAO;IAC1C,MAAM,IAAI,GAAG,IAAI,IAAI,CAAC,CAAC,OAAO,CAAC,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,CAAC,CAAC;IACrD,MAAM,GAAG,GAAG,GAAG,CAAC,eAAe,CAAC,IAAI,CAAC,CAAC;IACtC,MAAM,CAAC,GAAG,QAAQ,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC;IACtC,CAAC,CAAC,IAAI,GAAG,GAAG,CAAC;IACb,CAAC,CAAC,QAAQ,GAAG,QAAQ,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;IACxB,CAAC,CAAC,KAAK,EAAE,CAAC;IACV,CAAC,CAAC,MAAM,EAAE,CAAC;IACX,GAAG,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC;AAAA,CAC1B;AAED;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAAC,IAAS,EAAE,QAAgB,EAAQ;IAChE,MAAM,WAAW,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;IAClD,gBAAgB,CAAC,WAAW,EAAE,QAAQ,EAAE,kBAAkB,CAAC,CAAC;AAAA,CAC7D;AAED;;;;;;GAMG;AACH,MAAM,UAAU,aAAa,CAAC,MAAgB,EAAE,IAAiB,EAAE,QAAgB,EAAQ;IACzF,MAAM,UAAU,GAAG,SAAS,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;IACtE,gBAAgB,CAAC,UAAU,EAAE,QAAQ,EAAE,UAAU,CAAC,CAAC;AAAA,CACpD","sourcesContent":["import { stringify } from 'csv-stringify/browser/esm/sync';\n\n/**\n * Triggers a browser download of a text-based file.\n *\n * @param content The content of the file.\n * @param filename The desired filename.\n * @param mimeType The MIME type of the file.\n */\nexport function downloadTextFile(content: string, filename: string, mimeType: string): void {\n if (typeof window === 'undefined') return;\n const blob = new Blob([content], { type: mimeType });\n const url = URL.createObjectURL(blob);\n const a = document.createElement('a');\n a.href = url;\n a.download = filename;\n document.body.append(a);\n a.click();\n a.remove();\n URL.revokeObjectURL(url);\n}\n\n/**\n * Triggers a browser download of a JSON file.\n *\n * @param data The data to be included in the JSON file.\n * @param filename The desired filename.\n */\nexport function downloadAsJSON(data: any, filename: string): void {\n const jsonContent = JSON.stringify(data, null, 2);\n downloadTextFile(jsonContent, filename, 'application/json');\n}\n\n/**\n * Triggers a browser download of a CSV file.\n *\n * @param header The header row of the CSV.\n * @param data The data rows of the CSV.\n * @param filename The desired filename.\n */\nexport function downloadAsCSV(header: string[], data: unknown[][], filename: string): void {\n const csvContent = stringify(data, { header: true, columns: header });\n downloadTextFile(csvContent, filename, 'text/csv');\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"encode-data.d.ts","sourceRoot":"","sources":["../src/encode-data.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,cAAc,EAAoB,MAAM,oBAAoB,CAAC;AAE3E;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,SAAS,EAAE,MAAM,GAAG,cAAc,CAKnF;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,CAAC,GAAG,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,CAAC,CAQ5D"}
|
|
1
|
+
{"version":3,"file":"encode-data.d.ts","sourceRoot":"","sources":["../src/encode-data.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,cAAc,EAAoB,MAAM,oBAAoB,CAAC;AAE3E;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,CAAC,EAAE,SAAS,EAAE,MAAM,GAAG,cAAc,CAKnF;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,CAAC,GAAG,OAAO,EAAE,SAAS,EAAE,MAAM,GAAG,CAAC,CAQ5D","sourcesContent":["import { decode, encode } from 'js-base64';\n\nimport { type HtmlSafeString, html, unsafeHtml } from '@prairielearn/html';\n\n/**\n * Use this function as an HTML component encode data that will be passed to the client.\n *\n * @param data The data to encode.\n * @param elementId The element ID to use for the encoded data.\n *\n */\nexport function EncodedData<T = unknown>(data: T, elementId: string): HtmlSafeString {\n const encodedData = unsafeHtml(encode(JSON.stringify(data)));\n return html`<script id=\"${elementId}\" type=\"application/base64\">\n ${encodedData}\n </script>`;\n}\n\n/**\n * Decode data that was passed to the client from in HTML component using EncodeData().\n *\n * @param elementId The element ID that stores the encoded data, from from EncodedData().\n * @returns The decoded data.\n */\nexport function decodeData<T = unknown>(elementId: string): T {\n const base64Data = document.getElementById(elementId)?.textContent;\n if (base64Data == null) {\n throw new Error(`No data found in element with ID \"${elementId}\"`);\n }\n const jsonData = decode(base64Data);\n const data = JSON.parse(jsonData);\n return data;\n}\n"]}
|
package/dist/encode-data.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"encode-data.js","sourceRoot":"","sources":["../src/encode-data.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAE3C,OAAO,EAAuB,IAAI,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAE3E;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAc,IAAO,EAAE,SAAiB;
|
|
1
|
+
{"version":3,"file":"encode-data.js","sourceRoot":"","sources":["../src/encode-data.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,WAAW,CAAC;AAE3C,OAAO,EAAuB,IAAI,EAAE,UAAU,EAAE,MAAM,oBAAoB,CAAC;AAE3E;;;;;;GAMG;AACH,MAAM,UAAU,WAAW,CAAc,IAAO,EAAE,SAAiB,EAAkB;IACnF,MAAM,WAAW,GAAG,UAAU,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC7D,OAAO,IAAI,CAAA,eAAe,SAAS;MAC/B,WAAW;YACL,CAAC;AAAA,CACZ;AAED;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CAAc,SAAiB,EAAK;IAC5D,MAAM,UAAU,GAAG,QAAQ,CAAC,cAAc,CAAC,SAAS,CAAC,EAAE,WAAW,CAAC;IACnE,IAAI,UAAU,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CAAC,qCAAqC,SAAS,GAAG,CAAC,CAAC;IACrE,CAAC;IACD,MAAM,QAAQ,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC;IACpC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;IAClC,OAAO,IAAI,CAAC;AAAA,CACb","sourcesContent":["import { decode, encode } from 'js-base64';\n\nimport { type HtmlSafeString, html, unsafeHtml } from '@prairielearn/html';\n\n/**\n * Use this function as an HTML component encode data that will be passed to the client.\n *\n * @param data The data to encode.\n * @param elementId The element ID to use for the encoded data.\n *\n */\nexport function EncodedData<T = unknown>(data: T, elementId: string): HtmlSafeString {\n const encodedData = unsafeHtml(encode(JSON.stringify(data)));\n return html`<script id=\"${elementId}\" type=\"application/base64\">\n ${encodedData}\n </script>`;\n}\n\n/**\n * Decode data that was passed to the client from in HTML component using EncodeData().\n *\n * @param elementId The element ID that stores the encoded data, from from EncodedData().\n * @returns The decoded data.\n */\nexport function decodeData<T = unknown>(elementId: string): T {\n const base64Data = document.getElementById(elementId)?.textContent;\n if (base64Data == null) {\n throw new Error(`No data found in element with ID \"${elementId}\"`);\n }\n const jsonData = decode(base64Data);\n const data = JSON.parse(jsonData);\n return data;\n}\n"]}
|
package/dist/focus.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"focus.d.ts","sourceRoot":"","sources":["../src/focus.ts"],"names":[],"mappings":"AA+DA,MAAM,WAAW,SAAS;IACxB,UAAU,IAAI,IAAI,CAAC;CACpB;AAED,wBAAgB,SAAS,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,CAsCrD;AAED,wBAAgB,wBAAwB,CAAC,EAAE,EAAE,WAAW,QAsBvD"}
|
|
1
|
+
{"version":3,"file":"focus.d.ts","sourceRoot":"","sources":["../src/focus.ts"],"names":[],"mappings":"AA+DA,MAAM,WAAW,SAAS;IACxB,UAAU,IAAI,IAAI,CAAC;CACpB;AAED,wBAAgB,SAAS,CAAC,OAAO,EAAE,OAAO,GAAG,SAAS,CAsCrD;AAED,wBAAgB,wBAAwB,CAAC,EAAE,EAAE,WAAW,QAsBvD","sourcesContent":["// Borrowed from Bootstrap:\n// https://github.com/twbs/bootstrap/blob/5f75413735d8779aeefe0097af9dc5a416208ae5/js/src/dom/selector-engine.js#L67\nconst FOCUSABLE_SELECTOR = [\n 'a',\n 'button',\n 'input',\n 'textarea',\n 'select',\n 'details',\n '[tabindex]',\n '[contenteditable=\"true\"]',\n]\n .map((selector) => `${selector}:not([tabindex^=\"-\"]):not(.btn-close)`)\n .join(',');\n\nfunction isElement(object: Element) {\n return object?.nodeType !== undefined;\n}\n\nfunction isVisible(element: Element) {\n if (!isElement(element) || element.getClientRects().length === 0) {\n return false;\n }\n\n const elementIsVisible = getComputedStyle(element).getPropertyValue('visibility') === 'visible';\n // Handle `details` element as its content may falsely appear visible when it is closed\n const closedDetails = element.closest('details:not([open])');\n\n if (!closedDetails) {\n return elementIsVisible;\n }\n\n if (closedDetails !== element) {\n const summary = element.closest('summary');\n if (summary && summary.parentNode !== closedDetails) {\n return false;\n }\n\n if (summary === null) {\n return false;\n }\n }\n\n return elementIsVisible;\n}\n\nfunction isDisabled(element: Element) {\n if (!element || element.nodeType !== Node.ELEMENT_NODE) {\n return true;\n }\n\n if ((element as any).disabled !== undefined) {\n return (element as any).disabled;\n }\n\n return element.hasAttribute('disabled') && element.getAttribute('disabled') !== 'false';\n}\n\nfunction focusableChildren(element: Element): HTMLElement[] {\n const focusableChildren = element.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR);\n return Array.from(focusableChildren).filter((child) => !isDisabled(child) && isVisible(child));\n}\n\nexport interface FocusTrap {\n deactivate(): void;\n}\n\nexport function trapFocus(element: Element): FocusTrap {\n // Store the previous active element so we can restore it later.\n const previousActiveElement = document.activeElement ?? document.body;\n\n function keyDown(e: KeyboardEvent) {\n if (e.key !== 'Tab') return;\n\n const focusable = focusableChildren(element);\n const firstFocusable = focusable[0];\n const lastFocusable = focusable[focusable.length - 1];\n\n if (e.shiftKey) {\n // Tabbing backwards\n if (isSameFocusContext(document.activeElement, firstFocusable)) {\n focusElementOrCheckedOption(focusable[focusable.length - 1]);\n e.preventDefault();\n }\n } else {\n // Tabbing forwards\n if (isSameFocusContext(document.activeElement, lastFocusable)) {\n focusElementOrCheckedOption(focusable[0]);\n e.preventDefault();\n }\n }\n }\n\n document.addEventListener('keydown', keyDown);\n\n return {\n deactivate() {\n document.removeEventListener('keydown', keyDown);\n // Restore focus to the previously active element, but only if focus is\n // currently inside the trap container.\n if (element.contains(document.activeElement)) {\n focusElementOrCheckedOption(previousActiveElement as HTMLElement, { preventScroll: true });\n }\n },\n };\n}\n\nexport function focusFirstFocusableChild(el: HTMLElement) {\n // In case the user (or more frequently, Cypress) is too fast and focuses a\n // specific element inside the container before this script runs, don't transfer\n // focus to a different element.\n if (el.contains(document.activeElement)) return;\n\n // Escape hatch: if the first element isn't the one that should be focused,\n // add the `autofocus` attribute to the element that should be.\n const autofocusElement = el.querySelector<HTMLElement>('[autofocus]');\n if (autofocusElement) {\n autofocusElement.focus();\n return;\n }\n\n const focusablePopoverChildren = focusableChildren(el);\n if (focusablePopoverChildren.length > 0) {\n focusElementOrCheckedOption(focusablePopoverChildren[0]);\n return;\n }\n\n // If we still couldn't find a child element, focus the container itself.\n el.focus();\n}\n\n/**\n * Focus on the element, or if it's a radio button, focus on the checked radio button in the same group.\n */\nfunction focusElementOrCheckedOption(element: HTMLElement, focusOptions?: FocusOptions) {\n // If the element receiving focus is a radio button, and there is another\n // radio button in the same group that is currently checked, focus on that one\n // instead.\n // https://www.w3.org/WAI/ARIA/apg/patterns/radio/\n if (element.tagName === 'INPUT') {\n const inputElement = element as HTMLInputElement;\n if (inputElement.type === 'radio' && inputElement.name) {\n const checkedRadio = (inputElement.form ?? document).querySelector<HTMLInputElement>(\n `input[type=\"radio\"][name=\"${CSS.escape(inputElement.name)}\"]:checked`,\n );\n if (checkedRadio) {\n checkedRadio.focus(focusOptions);\n return;\n }\n }\n }\n\n // Otherwise, just focus on the element itself.\n element.focus(focusOptions);\n}\n\n/**\n * Check if two elements are in the same focus context. This is true if they are the\n * same element, or if they are radio buttons in the same group.\n */\nfunction isSameFocusContext(element1: Element | null, element2: Element | null) {\n if (!element1 || !element2) return false;\n if (element1 === element2) return true;\n if (element1.tagName === 'INPUT' && element2.tagName === 'INPUT') {\n const input1 = element1 as HTMLInputElement;\n const input2 = element2 as HTMLInputElement;\n // https://html.spec.whatwg.org/multipage/input.html#radio-button-state-(type=radio)\n if (\n input1.type === 'radio' &&\n input2.type === 'radio' &&\n // radios without a name are not in the same group, even if they share the same form\n input1.name &&\n input1.name === input2.name &&\n // either both have no form, or both have the same form\n input1.form === input2.form\n ) {\n return true;\n }\n }\n return false;\n}\n"]}
|
package/dist/focus.js
CHANGED
|
@@ -20,7 +20,7 @@ function isVisible(element) {
|
|
|
20
20
|
return false;
|
|
21
21
|
}
|
|
22
22
|
const elementIsVisible = getComputedStyle(element).getPropertyValue('visibility') === 'visible';
|
|
23
|
-
// Handle `details` element as its content may
|
|
23
|
+
// Handle `details` element as its content may falsely appear visible when it is closed
|
|
24
24
|
const closedDetails = element.closest('details:not([open])');
|
|
25
25
|
if (!closedDetails) {
|
|
26
26
|
return elementIsVisible;
|
package/dist/focus.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"focus.js","sourceRoot":"","sources":["../src/focus.ts"],"names":[],"mappings":"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","sourcesContent":["// Borrowed from Bootstrap:\n// https://github.com/twbs/bootstrap/blob/5f75413735d8779aeefe0097af9dc5a416208ae5/js/src/dom/selector-engine.js#L67\nconst FOCUSABLE_SELECTOR = [\n 'a',\n 'button',\n 'input',\n 'textarea',\n 'select',\n 'details',\n '[tabindex]',\n '[contenteditable=\"true\"]',\n]\n .map((selector) => `${selector}:not([tabindex^=\"-\"]):not(.btn-close)`)\n .join(',');\n\nfunction isElement(object: Element) {\n return object?.nodeType !== undefined;\n}\n\nfunction isVisible(element: Element) {\n if (!isElement(element) || element.getClientRects().length === 0) {\n return false;\n }\n\n const elementIsVisible = getComputedStyle(element).getPropertyValue('visibility') === 'visible';\n // Handle `details` element as its content may falsly appear visible when it is closed\n const closedDetails = element.closest('details:not([open])');\n\n if (!closedDetails) {\n return elementIsVisible;\n }\n\n if (closedDetails !== element) {\n const summary = element.closest('summary');\n if (summary && summary.parentNode !== closedDetails) {\n return false;\n }\n\n if (summary === null) {\n return false;\n }\n }\n\n return elementIsVisible;\n}\n\nfunction isDisabled(element: Element) {\n if (!element || element.nodeType !== Node.ELEMENT_NODE) {\n return true;\n }\n\n if ((element as any).disabled !== undefined) {\n return (element as any).disabled;\n }\n\n return element.hasAttribute('disabled') && element.getAttribute('disabled') !== 'false';\n}\n\nfunction focusableChildren(element: Element): HTMLElement[] {\n const focusableChildren = element.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR);\n return Array.from(focusableChildren).filter((child) => !isDisabled(child) && isVisible(child));\n}\n\nexport interface FocusTrap {\n deactivate(): void;\n}\n\nexport function trapFocus(element: Element): FocusTrap {\n // Store the previous active element so we can restore it later.\n const previousActiveElement = document.activeElement ?? document.body;\n\n function keyDown(e: KeyboardEvent) {\n if (e.key !== 'Tab') return;\n\n const focusable = focusableChildren(element);\n const firstFocusable = focusable[0];\n const lastFocusable = focusable[focusable.length - 1];\n\n if (e.shiftKey) {\n // Tabbing backwards\n if (isSameFocusContext(document.activeElement, firstFocusable)) {\n focusElementOrCheckedOption(focusable[focusable.length - 1]);\n e.preventDefault();\n }\n } else {\n // Tabbing forwards\n if (isSameFocusContext(document.activeElement, lastFocusable)) {\n focusElementOrCheckedOption(focusable[0]);\n e.preventDefault();\n }\n }\n }\n\n document.addEventListener('keydown', keyDown);\n\n return {\n deactivate() {\n document.removeEventListener('keydown', keyDown);\n // Restore focus to the previously active element, but only if focus is\n // currently inside the trap container.\n if (element.contains(document.activeElement)) {\n focusElementOrCheckedOption(previousActiveElement as HTMLElement, { preventScroll: true });\n }\n },\n };\n}\n\nexport function focusFirstFocusableChild(el: HTMLElement) {\n // In case the user (or more frequently, Cypress) is too fast and focuses a\n // specific element inside the container before this script runs, don't transfer\n // focus to a different element.\n if (el.contains(document.activeElement)) return;\n\n // Escape hatch: if the first element isn't the one that should be focused,\n // add the `autofocus` attribute to the element that should be.\n const autofocusElement = el.querySelector<HTMLElement>('[autofocus]');\n if (autofocusElement) {\n autofocusElement.focus();\n return;\n }\n\n const focusablePopoverChildren = focusableChildren(el);\n if (focusablePopoverChildren.length > 0) {\n focusElementOrCheckedOption(focusablePopoverChildren[0]);\n return;\n }\n\n // If we still couldn't find a child element, focus the container itself.\n el.focus();\n}\n\n/**\n * Focus on the element, or if it's a radio button, focus on the checked radio button in the same group.\n */\nfunction focusElementOrCheckedOption(element: HTMLElement, focusOptions?: FocusOptions) {\n // If the element receiving focus is a radio button, and there is another\n // radio button in the same group that is currently checked, focus on that one\n // instead.\n // https://www.w3.org/WAI/ARIA/apg/patterns/radio/\n if (element.tagName === 'INPUT') {\n const inputElement = element as HTMLInputElement;\n if (inputElement.type === 'radio' && inputElement.name) {\n const checkedRadio = (inputElement.form ?? document).querySelector<HTMLInputElement>(\n `input[type=\"radio\"][name=\"${CSS.escape(inputElement.name)}\"]:checked`,\n );\n if (checkedRadio) {\n checkedRadio.focus(focusOptions);\n return;\n }\n }\n }\n\n // Otherwise, just focus on the element itself.\n element.focus(focusOptions);\n}\n\n/**\n * Check if two elements are in the same focus context. This is true if they are the\n * same element, or if they are radio buttons in the same group.\n */\nfunction isSameFocusContext(element1: Element | null, element2: Element | null) {\n if (!element1 || !element2) return false;\n if (element1 === element2) return true;\n if (element1.tagName === 'INPUT' && element2.tagName === 'INPUT') {\n const input1 = element1 as HTMLInputElement;\n const input2 = element2 as HTMLInputElement;\n // https://html.spec.whatwg.org/multipage/input.html#radio-button-state-(type=radio)\n if (\n input1.type === 'radio' &&\n input2.type === 'radio' &&\n // radios without a name are not in the same group, even if they share the same form\n input1.name &&\n input1.name === input2.name &&\n // either both have no form, or both have the same form\n input1.form === input2.form\n ) {\n return true;\n }\n }\n return false;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"focus.js","sourceRoot":"","sources":["../src/focus.ts"],"names":[],"mappings":"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","sourcesContent":["// Borrowed from Bootstrap:\n// https://github.com/twbs/bootstrap/blob/5f75413735d8779aeefe0097af9dc5a416208ae5/js/src/dom/selector-engine.js#L67\nconst FOCUSABLE_SELECTOR = [\n 'a',\n 'button',\n 'input',\n 'textarea',\n 'select',\n 'details',\n '[tabindex]',\n '[contenteditable=\"true\"]',\n]\n .map((selector) => `${selector}:not([tabindex^=\"-\"]):not(.btn-close)`)\n .join(',');\n\nfunction isElement(object: Element) {\n return object?.nodeType !== undefined;\n}\n\nfunction isVisible(element: Element) {\n if (!isElement(element) || element.getClientRects().length === 0) {\n return false;\n }\n\n const elementIsVisible = getComputedStyle(element).getPropertyValue('visibility') === 'visible';\n // Handle `details` element as its content may falsely appear visible when it is closed\n const closedDetails = element.closest('details:not([open])');\n\n if (!closedDetails) {\n return elementIsVisible;\n }\n\n if (closedDetails !== element) {\n const summary = element.closest('summary');\n if (summary && summary.parentNode !== closedDetails) {\n return false;\n }\n\n if (summary === null) {\n return false;\n }\n }\n\n return elementIsVisible;\n}\n\nfunction isDisabled(element: Element) {\n if (!element || element.nodeType !== Node.ELEMENT_NODE) {\n return true;\n }\n\n if ((element as any).disabled !== undefined) {\n return (element as any).disabled;\n }\n\n return element.hasAttribute('disabled') && element.getAttribute('disabled') !== 'false';\n}\n\nfunction focusableChildren(element: Element): HTMLElement[] {\n const focusableChildren = element.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR);\n return Array.from(focusableChildren).filter((child) => !isDisabled(child) && isVisible(child));\n}\n\nexport interface FocusTrap {\n deactivate(): void;\n}\n\nexport function trapFocus(element: Element): FocusTrap {\n // Store the previous active element so we can restore it later.\n const previousActiveElement = document.activeElement ?? document.body;\n\n function keyDown(e: KeyboardEvent) {\n if (e.key !== 'Tab') return;\n\n const focusable = focusableChildren(element);\n const firstFocusable = focusable[0];\n const lastFocusable = focusable[focusable.length - 1];\n\n if (e.shiftKey) {\n // Tabbing backwards\n if (isSameFocusContext(document.activeElement, firstFocusable)) {\n focusElementOrCheckedOption(focusable[focusable.length - 1]);\n e.preventDefault();\n }\n } else {\n // Tabbing forwards\n if (isSameFocusContext(document.activeElement, lastFocusable)) {\n focusElementOrCheckedOption(focusable[0]);\n e.preventDefault();\n }\n }\n }\n\n document.addEventListener('keydown', keyDown);\n\n return {\n deactivate() {\n document.removeEventListener('keydown', keyDown);\n // Restore focus to the previously active element, but only if focus is\n // currently inside the trap container.\n if (element.contains(document.activeElement)) {\n focusElementOrCheckedOption(previousActiveElement as HTMLElement, { preventScroll: true });\n }\n },\n };\n}\n\nexport function focusFirstFocusableChild(el: HTMLElement) {\n // In case the user (or more frequently, Cypress) is too fast and focuses a\n // specific element inside the container before this script runs, don't transfer\n // focus to a different element.\n if (el.contains(document.activeElement)) return;\n\n // Escape hatch: if the first element isn't the one that should be focused,\n // add the `autofocus` attribute to the element that should be.\n const autofocusElement = el.querySelector<HTMLElement>('[autofocus]');\n if (autofocusElement) {\n autofocusElement.focus();\n return;\n }\n\n const focusablePopoverChildren = focusableChildren(el);\n if (focusablePopoverChildren.length > 0) {\n focusElementOrCheckedOption(focusablePopoverChildren[0]);\n return;\n }\n\n // If we still couldn't find a child element, focus the container itself.\n el.focus();\n}\n\n/**\n * Focus on the element, or if it's a radio button, focus on the checked radio button in the same group.\n */\nfunction focusElementOrCheckedOption(element: HTMLElement, focusOptions?: FocusOptions) {\n // If the element receiving focus is a radio button, and there is another\n // radio button in the same group that is currently checked, focus on that one\n // instead.\n // https://www.w3.org/WAI/ARIA/apg/patterns/radio/\n if (element.tagName === 'INPUT') {\n const inputElement = element as HTMLInputElement;\n if (inputElement.type === 'radio' && inputElement.name) {\n const checkedRadio = (inputElement.form ?? document).querySelector<HTMLInputElement>(\n `input[type=\"radio\"][name=\"${CSS.escape(inputElement.name)}\"]:checked`,\n );\n if (checkedRadio) {\n checkedRadio.focus(focusOptions);\n return;\n }\n }\n }\n\n // Otherwise, just focus on the element itself.\n element.focus(focusOptions);\n}\n\n/**\n * Check if two elements are in the same focus context. This is true if they are the\n * same element, or if they are radio buttons in the same group.\n */\nfunction isSameFocusContext(element1: Element | null, element2: Element | null) {\n if (!element1 || !element2) return false;\n if (element1 === element2) return true;\n if (element1.tagName === 'INPUT' && element2.tagName === 'INPUT') {\n const input1 = element1 as HTMLInputElement;\n const input2 = element2 as HTMLInputElement;\n // https://html.spec.whatwg.org/multipage/input.html#radio-button-state-(type=radio)\n if (\n input1.type === 'radio' &&\n input2.type === 'radio' &&\n // radios without a name are not in the same group, even if they share the same form\n input1.name &&\n input1.name === input2.name &&\n // either both have no form, or both have the same form\n input1.form === input2.form\n ) {\n return true;\n }\n }\n return false;\n}\n"]}
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,EAAE,KAAK,SAAS,EAAE,SAAS,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AACjF,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,SAAS,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAC9D,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AACvE,OAAO,EAAE,KAAK,SAAS,EAAE,SAAS,EAAE,wBAAwB,EAAE,MAAM,YAAY,CAAC;AACjF,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC","sourcesContent":["export { onDocumentReady } from './on-document-ready.js';\nexport { parseHTML, parseHTMLElement } from './parse-html.js';\nexport { EncodedData, decodeData } from './encode-data.js';\nexport { templateFromAttributes } from './template-from-attributes.js';\nexport { type FocusTrap, trapFocus, focusFirstFocusableChild } from './focus.js';\nexport { downloadAsCSV, downloadAsJSON } from './downloads.js';\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"on-document-ready.d.ts","sourceRoot":"","sources":["../src/on-document-ready.ts"],"names":[],"mappings":"AAAA,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI,CAQpD"}
|
|
1
|
+
{"version":3,"file":"on-document-ready.d.ts","sourceRoot":"","sources":["../src/on-document-ready.ts"],"names":[],"mappings":"AAAA,wBAAgB,eAAe,CAAC,EAAE,EAAE,MAAM,IAAI,GAAG,IAAI,CAQpD","sourcesContent":["export function onDocumentReady(fn: () => void): void {\n if (document.readyState === 'interactive' || document.readyState === 'complete') {\n fn();\n } else {\n document.addEventListener('DOMContentLoaded', () => {\n fn();\n });\n }\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"on-document-ready.js","sourceRoot":"","sources":["../src/on-document-ready.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,eAAe,CAAC,EAAc;
|
|
1
|
+
{"version":3,"file":"on-document-ready.js","sourceRoot":"","sources":["../src/on-document-ready.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,eAAe,CAAC,EAAc,EAAQ;IACpD,IAAI,QAAQ,CAAC,UAAU,KAAK,aAAa,IAAI,QAAQ,CAAC,UAAU,KAAK,UAAU,EAAE,CAAC;QAChF,EAAE,EAAE,CAAC;IACP,CAAC;SAAM,CAAC;QACN,QAAQ,CAAC,gBAAgB,CAAC,kBAAkB,EAAE,GAAG,EAAE,CAAC;YAClD,EAAE,EAAE,CAAC;QAAA,CACN,CAAC,CAAC;IACL,CAAC;AAAA,CACF","sourcesContent":["export function onDocumentReady(fn: () => void): void {\n if (document.readyState === 'interactive' || document.readyState === 'complete') {\n fn();\n } else {\n document.addEventListener('DOMContentLoaded', () => {\n fn();\n });\n }\n}\n"]}
|
package/dist/parse-html.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parse-html.d.ts","sourceRoot":"","sources":["../src/parse-html.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzD,wBAAgB,SAAS,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,gBAAgB,CAK7F;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO,EAC1D,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,MAAM,GAAG,cAAc,GAC5B,CAAC,CAMH"}
|
|
1
|
+
{"version":3,"file":"parse-html.d.ts","sourceRoot":"","sources":["../src/parse-html.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AAEzD,wBAAgB,SAAS,CAAC,QAAQ,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,GAAG,cAAc,GAAG,gBAAgB,CAK7F;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,OAAO,GAAG,OAAO,EAC1D,QAAQ,EAAE,QAAQ,EAClB,IAAI,EAAE,MAAM,GAAG,cAAc,GAC5B,CAAC,CAMH","sourcesContent":["import type { HtmlSafeString } from '@prairielearn/html';\n\nexport function parseHTML(document: Document, html: string | HtmlSafeString): DocumentFragment {\n if (typeof html !== 'string') html = html.toString();\n const template = document.createElement('template');\n template.innerHTML = html;\n return document.importNode(template.content, true);\n}\n\n/**\n * Like {@link parseHTML}, but returns an {@link Element} instead of a\n * {@link DocumentFragment}. If the HTML being parsed does not contain\n * exactly one element, an error is thrown.\n */\nexport function parseHTMLElement<T extends Element = Element>(\n document: Document,\n html: string | HtmlSafeString,\n): T {\n const documentFragment = parseHTML(document, html);\n if (documentFragment.childElementCount !== 1) {\n throw new Error('Expected HTML to contain exactly one element');\n }\n return documentFragment.firstElementChild as T;\n}\n"]}
|
package/dist/parse-html.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parse-html.js","sourceRoot":"","sources":["../src/parse-html.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,SAAS,CAAC,QAAkB,EAAE,IAA6B;
|
|
1
|
+
{"version":3,"file":"parse-html.js","sourceRoot":"","sources":["../src/parse-html.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,SAAS,CAAC,QAAkB,EAAE,IAA6B,EAAoB;IAC7F,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,IAAI,GAAG,IAAI,CAAC,QAAQ,EAAE,CAAC;IACrD,MAAM,QAAQ,GAAG,QAAQ,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC;IACpD,QAAQ,CAAC,SAAS,GAAG,IAAI,CAAC;IAC1B,OAAO,QAAQ,CAAC,UAAU,CAAC,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;AAAA,CACpD;AAED;;;;GAIG;AACH,MAAM,UAAU,gBAAgB,CAC9B,QAAkB,EAClB,IAA6B,EAC1B;IACH,MAAM,gBAAgB,GAAG,SAAS,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IACnD,IAAI,gBAAgB,CAAC,iBAAiB,KAAK,CAAC,EAAE,CAAC;QAC7C,MAAM,IAAI,KAAK,CAAC,8CAA8C,CAAC,CAAC;IAClE,CAAC;IACD,OAAO,gBAAgB,CAAC,iBAAsB,CAAC;AAAA,CAChD","sourcesContent":["import type { HtmlSafeString } from '@prairielearn/html';\n\nexport function parseHTML(document: Document, html: string | HtmlSafeString): DocumentFragment {\n if (typeof html !== 'string') html = html.toString();\n const template = document.createElement('template');\n template.innerHTML = html;\n return document.importNode(template.content, true);\n}\n\n/**\n * Like {@link parseHTML}, but returns an {@link Element} instead of a\n * {@link DocumentFragment}. If the HTML being parsed does not contain\n * exactly one element, an error is thrown.\n */\nexport function parseHTMLElement<T extends Element = Element>(\n document: Document,\n html: string | HtmlSafeString,\n): T {\n const documentFragment = parseHTML(document, html);\n if (documentFragment.childElementCount !== 1) {\n throw new Error('Expected HTML to contain exactly one element');\n }\n return documentFragment.firstElementChild as T;\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"template-from-attributes.d.ts","sourceRoot":"","sources":["../src/template-from-attributes.ts"],"names":[],"mappings":"AAAA,KAAK,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAE3C;;;;;;;;;;;;;GAaG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,WAAW,EACnB,MAAM,EAAE,WAAW,EACnB,UAAU,EAAE,YAAY,QAyCzB"}
|
|
1
|
+
{"version":3,"file":"template-from-attributes.d.ts","sourceRoot":"","sources":["../src/template-from-attributes.ts"],"names":[],"mappings":"AAAA,KAAK,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAE3C;;;;;;;;;;;;;GAaG;AACH,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,WAAW,EACnB,MAAM,EAAE,WAAW,EACnB,UAAU,EAAE,YAAY,QAyCzB","sourcesContent":["type AttributeMap = Record<string, string>;\n\n/**\n * For each key in `attributes`, copies that attribute's value from `source`\n * into all elements within `target` that match the corresponding value in\n * `attributes`.\n *\n * For `<input type=\"checkbox\">` elements it interprets the attribute as JSON\n * and uses the truthiness of it to set `checked`. For other `<input>` elements,\n * it sets the `value` attribute. For all others, it sets the `textContent`\n * attribute.\n *\n * @param source The element to copy attributes from\n * @param target The element to copy attributes into\n * @param attributes A map of attributes to copy from `source` to `target`\n */\nexport function templateFromAttributes(\n source: HTMLElement,\n target: HTMLElement,\n attributes: AttributeMap,\n) {\n Object.entries(attributes).forEach(([sourceAttribute, targetSelector]) => {\n const attributeValue = source.getAttribute(sourceAttribute);\n if (attributeValue == null) {\n console.error(`Attribute \"${sourceAttribute}\" not found on source element`);\n return;\n }\n\n const targets = target.querySelectorAll(targetSelector);\n if (targets.length === 0) {\n console.error(`No elements found matching selector \"${targetSelector}\"`);\n return;\n }\n\n targets.forEach((targetElement) => {\n if (targetElement instanceof HTMLInputElement) {\n if (targetElement.type === 'checkbox') {\n const attributeParsed = JSON.parse(attributeValue);\n targetElement.checked = !!attributeParsed;\n // Manually trigger a 'change' event. This does not trigger\n // automatically when we change properties like 'checked'.\n targetElement.dispatchEvent(new Event('change', { bubbles: true }));\n } else {\n targetElement.value = attributeValue;\n }\n } else if (targetElement instanceof HTMLSelectElement) {\n const i = Array.from(targetElement.options).findIndex((o) => o.value === attributeValue);\n if (i !== -1) {\n targetElement.selectedIndex = i;\n // Manually trigger a 'change' event. This does not trigger\n // automatically when we change properties like 'checked'.\n targetElement.dispatchEvent(new Event('change', { bubbles: true }));\n } else {\n console.error(`Could not find option with value \"${attributeValue}\"`);\n }\n } else {\n targetElement.textContent = attributeValue;\n }\n });\n });\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"template-from-attributes.js","sourceRoot":"","sources":["../src/template-from-attributes.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,sBAAsB,CACpC,MAAmB,EACnB,MAAmB,EACnB,UAAwB;
|
|
1
|
+
{"version":3,"file":"template-from-attributes.js","sourceRoot":"","sources":["../src/template-from-attributes.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;GAaG;AACH,MAAM,UAAU,sBAAsB,CACpC,MAAmB,EACnB,MAAmB,EACnB,UAAwB,EACxB;IACA,MAAM,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,eAAe,EAAE,cAAc,CAAC,EAAE,EAAE,CAAC;QACxE,MAAM,cAAc,GAAG,MAAM,CAAC,YAAY,CAAC,eAAe,CAAC,CAAC;QAC5D,IAAI,cAAc,IAAI,IAAI,EAAE,CAAC;YAC3B,OAAO,CAAC,KAAK,CAAC,cAAc,eAAe,+BAA+B,CAAC,CAAC;YAC5E,OAAO;QACT,CAAC;QAED,MAAM,OAAO,GAAG,MAAM,CAAC,gBAAgB,CAAC,cAAc,CAAC,CAAC;QACxD,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACzB,OAAO,CAAC,KAAK,CAAC,wCAAwC,cAAc,GAAG,CAAC,CAAC;YACzE,OAAO;QACT,CAAC;QAED,OAAO,CAAC,OAAO,CAAC,CAAC,aAAa,EAAE,EAAE,CAAC;YACjC,IAAI,aAAa,YAAY,gBAAgB,EAAE,CAAC;gBAC9C,IAAI,aAAa,CAAC,IAAI,KAAK,UAAU,EAAE,CAAC;oBACtC,MAAM,eAAe,GAAG,IAAI,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;oBACnD,aAAa,CAAC,OAAO,GAAG,CAAC,CAAC,eAAe,CAAC;oBAC1C,2DAA2D;oBAC3D,0DAA0D;oBAC1D,aAAa,CAAC,aAAa,CAAC,IAAI,KAAK,CAAC,QAAQ,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;gBACtE,CAAC;qBAAM,CAAC;oBACN,aAAa,CAAC,KAAK,GAAG,cAAc,CAAC;gBACvC,CAAC;YACH,CAAC;iBAAM,IAAI,aAAa,YAAY,iBAAiB,EAAE,CAAC;gBACtD,MAAM,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,cAAc,CAAC,CAAC;gBACzF,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;oBACb,aAAa,CAAC,aAAa,GAAG,CAAC,CAAC;oBAChC,2DAA2D;oBAC3D,0DAA0D;oBAC1D,aAAa,CAAC,aAAa,CAAC,IAAI,KAAK,CAAC,QAAQ,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC;gBACtE,CAAC;qBAAM,CAAC;oBACN,OAAO,CAAC,KAAK,CAAC,qCAAqC,cAAc,GAAG,CAAC,CAAC;gBACxE,CAAC;YACH,CAAC;iBAAM,CAAC;gBACN,aAAa,CAAC,WAAW,GAAG,cAAc,CAAC;YAC7C,CAAC;QAAA,CACF,CAAC,CAAC;IAAA,CACJ,CAAC,CAAC;AAAA,CACJ","sourcesContent":["type AttributeMap = Record<string, string>;\n\n/**\n * For each key in `attributes`, copies that attribute's value from `source`\n * into all elements within `target` that match the corresponding value in\n * `attributes`.\n *\n * For `<input type=\"checkbox\">` elements it interprets the attribute as JSON\n * and uses the truthiness of it to set `checked`. For other `<input>` elements,\n * it sets the `value` attribute. For all others, it sets the `textContent`\n * attribute.\n *\n * @param source The element to copy attributes from\n * @param target The element to copy attributes into\n * @param attributes A map of attributes to copy from `source` to `target`\n */\nexport function templateFromAttributes(\n source: HTMLElement,\n target: HTMLElement,\n attributes: AttributeMap,\n) {\n Object.entries(attributes).forEach(([sourceAttribute, targetSelector]) => {\n const attributeValue = source.getAttribute(sourceAttribute);\n if (attributeValue == null) {\n console.error(`Attribute \"${sourceAttribute}\" not found on source element`);\n return;\n }\n\n const targets = target.querySelectorAll(targetSelector);\n if (targets.length === 0) {\n console.error(`No elements found matching selector \"${targetSelector}\"`);\n return;\n }\n\n targets.forEach((targetElement) => {\n if (targetElement instanceof HTMLInputElement) {\n if (targetElement.type === 'checkbox') {\n const attributeParsed = JSON.parse(attributeValue);\n targetElement.checked = !!attributeParsed;\n // Manually trigger a 'change' event. This does not trigger\n // automatically when we change properties like 'checked'.\n targetElement.dispatchEvent(new Event('change', { bubbles: true }));\n } else {\n targetElement.value = attributeValue;\n }\n } else if (targetElement instanceof HTMLSelectElement) {\n const i = Array.from(targetElement.options).findIndex((o) => o.value === attributeValue);\n if (i !== -1) {\n targetElement.selectedIndex = i;\n // Manually trigger a 'change' event. This does not trigger\n // automatically when we change properties like 'checked'.\n targetElement.dispatchEvent(new Event('change', { bubbles: true }));\n } else {\n console.error(`Could not find option with value \"${attributeValue}\"`);\n }\n } else {\n targetElement.textContent = attributeValue;\n }\n });\n });\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prairielearn/browser-utils",
|
|
3
|
-
"version": "2.6.
|
|
3
|
+
"version": "2.6.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -9,16 +9,17 @@
|
|
|
9
9
|
},
|
|
10
10
|
"main": "dist/index.js",
|
|
11
11
|
"scripts": {
|
|
12
|
-
"build": "
|
|
13
|
-
"dev": "
|
|
12
|
+
"build": "tsgo",
|
|
13
|
+
"dev": "tsgo --watch --preserveWatchOutput"
|
|
14
14
|
},
|
|
15
15
|
"dependencies": {
|
|
16
|
-
"@prairielearn/html": "^
|
|
16
|
+
"@prairielearn/html": "^5.0.0",
|
|
17
17
|
"csv-stringify": "^6.6.0",
|
|
18
18
|
"js-base64": "^3.7.8"
|
|
19
19
|
},
|
|
20
20
|
"devDependencies": {
|
|
21
21
|
"@prairielearn/tsconfig": "^0.0.0",
|
|
22
|
+
"@typescript/native-preview": "^7.0.0-dev.20260106.1",
|
|
22
23
|
"typescript": "^5.9.3"
|
|
23
24
|
},
|
|
24
25
|
"sideEffects": false
|
package/src/focus.ts
CHANGED
|
@@ -23,7 +23,7 @@ function isVisible(element: Element) {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
const elementIsVisible = getComputedStyle(element).getPropertyValue('visibility') === 'visible';
|
|
26
|
-
// Handle `details` element as its content may
|
|
26
|
+
// Handle `details` element as its content may falsely appear visible when it is closed
|
|
27
27
|
const closedDetails = element.closest('details:not([open])');
|
|
28
28
|
|
|
29
29
|
if (!closedDetails) {
|