@prairielearn/browser-utils 1.0.3 → 1.1.1

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.
@@ -4,7 +4,7 @@
4
4
  CLI Target: es2021
5
5
  CJS Build start
6
6
  ESM Build start
7
- CJS dist/index.js 2.68 KB
8
- CJS ⚡️ Build success in 139ms
9
- ESM dist/index.mjs 1.47 KB
10
- ESM ⚡️ Build success in 140ms
7
+ CJS dist/index.js 4.22 KB
8
+ CJS ⚡️ Build success in 146ms
9
+ ESM dist/index.mjs 2.95 KB
10
+ ESM ⚡️ Build success in 138ms
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # @prairielearn/browser-utils
2
2
 
3
+ ## 1.1.1
4
+
5
+ ### Patch Changes
6
+
7
+ - Updated dependencies [098f581da]
8
+ - @prairielearn/html@3.0.4
9
+
10
+ ## 1.1.0
11
+
12
+ ### Minor Changes
13
+
14
+ - 0548e8401: Add `templateFromAttributes` function
15
+
3
16
  ## 1.0.3
4
17
 
5
18
  ### Patch Changes
package/README.md CHANGED
@@ -59,6 +59,38 @@ onDocumentReady(() => {
59
59
  });
60
60
  ```
61
61
 
62
+ ### `templateFromAttributes`
63
+
64
+ This function simplifies the common pattern of taking attributes from one HTML element and using them as the content of other HTML elements. This is often done with modals that need to display information about a specific entity.
65
+
66
+ Consider the following simplified markup:
67
+
68
+ ```html
69
+ <button class="js-delete-course" data-course-name="CS 123">Delete course</button>
70
+
71
+ <div class="modal" id="deleteCourseModal">
72
+ <p>Are you sure you want to delete course <strong class="js-course-name"></strong>?</p>
73
+ <button type="button">Cancel</button>
74
+ <button type="button">Delete <span class="js-course-name"></span></button>
75
+ </div>
76
+ ```
77
+
78
+ The following JavaScript will "template" the value from `data-course-name` on the button into the elements with the `.js-course-name` in the modal.
79
+
80
+ ```ts
81
+ import { templateFromAttributes } from '@prairielearn/browser-utils';
82
+
83
+ const modal = document.querySelector('#deleteCourseModal');
84
+ document.querySelectorAll('.js-delete-course').forEach((el) => {
85
+ el.addEventListener('click', (e) => {
86
+ const button = e.target;
87
+ templateFromAttributes(e.currentTarget, modal, {
88
+ 'data-course-name': '.js-course-name',
89
+ });
90
+ });
91
+ });
92
+ ```
93
+
62
94
  ## Development
63
95
 
64
96
  Unlike most other `@prairielearn` packages, this one is built with [`tsup`](https://tsup.egoist.dev/), which is used to generate both CJS and ESM output. The latter is important for ensuring that tree-shaking works correctly when building client bundles, which is important for minimizing bundle sizes.
package/dist/index.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export { onDocumentReady } from './on-document-ready';
2
2
  export { parseHTML, parseHTMLElement } from './parse-html';
3
3
  export { EncodedData, decodeData } from './encode-data';
4
+ export { templateFromAttributes } from './template-from-attributes';
package/dist/index.js CHANGED
@@ -24,7 +24,8 @@ __export(src_exports, {
24
24
  decodeData: () => decodeData,
25
25
  onDocumentReady: () => onDocumentReady,
26
26
  parseHTML: () => parseHTML,
27
- parseHTMLElement: () => parseHTMLElement
27
+ parseHTMLElement: () => parseHTMLElement,
28
+ templateFromAttributes: () => templateFromAttributes
28
29
  });
29
30
  module.exports = __toCommonJS(src_exports);
30
31
 
@@ -73,11 +74,49 @@ function decodeData(elementId) {
73
74
  const data = JSON.parse(jsonData);
74
75
  return data;
75
76
  }
77
+
78
+ // src/template-from-attributes.ts
79
+ function templateFromAttributes(source, target, attributes) {
80
+ Object.entries(attributes).forEach(([sourceAttribute, targetSelector]) => {
81
+ const attributeValue = source.getAttribute(sourceAttribute);
82
+ if (attributeValue == null) {
83
+ console.error(`Attribute "${sourceAttribute}" not found on source element`);
84
+ return;
85
+ }
86
+ const targets = target.querySelectorAll(targetSelector);
87
+ if (targets.length === 0) {
88
+ console.error(`No elements found matching selector "${targetSelector}"`);
89
+ return;
90
+ }
91
+ targets.forEach((targetElement) => {
92
+ if (targetElement instanceof HTMLInputElement) {
93
+ if (targetElement.type === "checkbox") {
94
+ const attributeParsed = JSON.parse(attributeValue);
95
+ targetElement.checked = !!attributeParsed;
96
+ targetElement.dispatchEvent(new Event("change", { bubbles: true }));
97
+ } else {
98
+ targetElement.value = attributeValue;
99
+ }
100
+ } else if (targetElement instanceof HTMLSelectElement) {
101
+ const i = Array.from(targetElement.options).findIndex((o) => o.value === attributeValue);
102
+ if (i >= 0) {
103
+ targetElement.selectedIndex = i;
104
+ targetElement.dispatchEvent(new Event("change", { bubbles: true }));
105
+ } else {
106
+ console.error(`Could not find option with value "${attributeValue}"`);
107
+ }
108
+ } else {
109
+ targetElement.textContent = attributeValue;
110
+ }
111
+ });
112
+ });
113
+ }
76
114
  // Annotate the CommonJS export names for ESM import in node:
77
115
  0 && (module.exports = {
78
116
  EncodedData,
79
117
  decodeData,
80
118
  onDocumentReady,
81
119
  parseHTML,
82
- parseHTMLElement
120
+ parseHTMLElement,
121
+ templateFromAttributes
83
122
  });
package/dist/index.mjs CHANGED
@@ -43,10 +43,48 @@ function decodeData(elementId) {
43
43
  const data = JSON.parse(jsonData);
44
44
  return data;
45
45
  }
46
+
47
+ // src/template-from-attributes.ts
48
+ function templateFromAttributes(source, target, attributes) {
49
+ Object.entries(attributes).forEach(([sourceAttribute, targetSelector]) => {
50
+ const attributeValue = source.getAttribute(sourceAttribute);
51
+ if (attributeValue == null) {
52
+ console.error(`Attribute "${sourceAttribute}" not found on source element`);
53
+ return;
54
+ }
55
+ const targets = target.querySelectorAll(targetSelector);
56
+ if (targets.length === 0) {
57
+ console.error(`No elements found matching selector "${targetSelector}"`);
58
+ return;
59
+ }
60
+ targets.forEach((targetElement) => {
61
+ if (targetElement instanceof HTMLInputElement) {
62
+ if (targetElement.type === "checkbox") {
63
+ const attributeParsed = JSON.parse(attributeValue);
64
+ targetElement.checked = !!attributeParsed;
65
+ targetElement.dispatchEvent(new Event("change", { bubbles: true }));
66
+ } else {
67
+ targetElement.value = attributeValue;
68
+ }
69
+ } else if (targetElement instanceof HTMLSelectElement) {
70
+ const i = Array.from(targetElement.options).findIndex((o) => o.value === attributeValue);
71
+ if (i >= 0) {
72
+ targetElement.selectedIndex = i;
73
+ targetElement.dispatchEvent(new Event("change", { bubbles: true }));
74
+ } else {
75
+ console.error(`Could not find option with value "${attributeValue}"`);
76
+ }
77
+ } else {
78
+ targetElement.textContent = attributeValue;
79
+ }
80
+ });
81
+ });
82
+ }
46
83
  export {
47
84
  EncodedData,
48
85
  decodeData,
49
86
  onDocumentReady,
50
87
  parseHTML,
51
- parseHTMLElement
88
+ parseHTMLElement,
89
+ templateFromAttributes
52
90
  };
@@ -0,0 +1,18 @@
1
+ type AttributeMap = Record<string, string>;
2
+ /**
3
+ * For each key in `attributes`, copies that attribute's value from `source`
4
+ * into all elements within `target` that match the corresponding value in
5
+ * `attributes`.
6
+ *
7
+ * For `<input type="checkbox">` elements it interprets the attribute as JSON
8
+ * and uses the truthiness of it to set `checked`. For other `<input>` elements,
9
+ * it sets the `value` attribute. For all others, it sets the `textContent`
10
+ * attribute.
11
+ *
12
+ * @param source The element to copy attributes from
13
+ * @param target The element to copy attributes into
14
+ * @param attributes A map of attributes to copy from `source` to `target`
15
+ * @param param.debug If true, logs debug information to the console
16
+ */
17
+ export declare function templateFromAttributes(source: HTMLElement, target: HTMLElement, attributes: AttributeMap): void;
18
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prairielearn/browser-utils",
3
- "version": "1.0.3",
3
+ "version": "1.1.1",
4
4
  "main": "dist/index.js",
5
5
  "sideEffects": false,
6
6
  "repository": {
@@ -21,7 +21,7 @@
21
21
  "dev": "tsup --watch src/index.ts"
22
22
  },
23
23
  "dependencies": {
24
- "@prairielearn/html": "^3.0.3",
24
+ "@prairielearn/html": "^3.0.4",
25
25
  "js-base64": "^3.7.5"
26
26
  },
27
27
  "devDependencies": {
package/src/index.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  export { onDocumentReady } from './on-document-ready';
2
2
  export { parseHTML, parseHTMLElement } from './parse-html';
3
3
  export { EncodedData, decodeData } from './encode-data';
4
+ export { templateFromAttributes } from './template-from-attributes';
@@ -0,0 +1,62 @@
1
+ type AttributeMap = Record<string, string>;
2
+
3
+ /**
4
+ * For each key in `attributes`, copies that attribute's value from `source`
5
+ * into all elements within `target` that match the corresponding value in
6
+ * `attributes`.
7
+ *
8
+ * For `<input type="checkbox">` elements it interprets the attribute as JSON
9
+ * and uses the truthiness of it to set `checked`. For other `<input>` elements,
10
+ * it sets the `value` attribute. For all others, it sets the `textContent`
11
+ * attribute.
12
+ *
13
+ * @param source The element to copy attributes from
14
+ * @param target The element to copy attributes into
15
+ * @param attributes A map of attributes to copy from `source` to `target`
16
+ * @param param.debug If true, logs debug information to the console
17
+ */
18
+ export function templateFromAttributes(
19
+ source: HTMLElement,
20
+ target: HTMLElement,
21
+ attributes: AttributeMap,
22
+ ) {
23
+ Object.entries(attributes).forEach(([sourceAttribute, targetSelector]) => {
24
+ const attributeValue = source.getAttribute(sourceAttribute);
25
+ if (attributeValue == null) {
26
+ console.error(`Attribute "${sourceAttribute}" not found on source element`);
27
+ return;
28
+ }
29
+
30
+ const targets = target.querySelectorAll(targetSelector);
31
+ if (targets.length === 0) {
32
+ console.error(`No elements found matching selector "${targetSelector}"`);
33
+ return;
34
+ }
35
+
36
+ targets.forEach((targetElement) => {
37
+ if (targetElement instanceof HTMLInputElement) {
38
+ if (targetElement.type === 'checkbox') {
39
+ const attributeParsed = JSON.parse(attributeValue);
40
+ targetElement.checked = !!attributeParsed;
41
+ // Manually trigger a 'change' event. This does not trigger
42
+ // automatically when we change properties like 'checked'.
43
+ targetElement.dispatchEvent(new Event('change', { bubbles: true }));
44
+ } else {
45
+ targetElement.value = attributeValue;
46
+ }
47
+ } else if (targetElement instanceof HTMLSelectElement) {
48
+ const i = Array.from(targetElement.options).findIndex((o) => o.value === attributeValue);
49
+ if (i >= 0) {
50
+ targetElement.selectedIndex = i;
51
+ // Manually trigger a 'change' event. This does not trigger
52
+ // automatically when we change properties like 'checked'.
53
+ targetElement.dispatchEvent(new Event('change', { bubbles: true }));
54
+ } else {
55
+ console.error(`Could not find option with value "${attributeValue}"`);
56
+ }
57
+ } else {
58
+ targetElement.textContent = attributeValue;
59
+ }
60
+ });
61
+ });
62
+ }
package/tsconfig.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "outDir": "./dist",
5
5
  "rootDir": "./src",
6
6
  // Unlike other PrairieLearn packages, we use another tool to actually
7
- // emit transpiled JavaScript. See note in `README.md`.
8
- "emitDeclarationOnly": true,
9
- },
7
+ // emit transpiled JavaScript. See note in `README.md`.
8
+ "emitDeclarationOnly": true
9
+ }
10
10
  }