@prairielearn/browser-utils 1.0.2 → 1.1.0
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/.turbo/turbo-build.log +5 -5
- package/CHANGELOG.md +14 -0
- package/README.md +33 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +41 -2
- package/dist/index.mjs +39 -1
- package/dist/template-from-attributes.d.ts +18 -0
- package/package.json +4 -5
- package/src/index.ts +1 -0
- package/src/template-from-attributes.ts +62 -0
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
[34mCLI[39m Building entry: src/index.ts
|
|
2
2
|
[34mCLI[39m Using tsconfig: tsconfig.json
|
|
3
|
-
[34mCLI[39m tsup
|
|
3
|
+
[34mCLI[39m tsup v7.1.0
|
|
4
4
|
[34mCLI[39m Target: es2021
|
|
5
5
|
[34mCJS[39m Build start
|
|
6
6
|
[34mESM[39m Build start
|
|
7
|
-
[32mCJS[39m [1mdist/index.js [22m[
|
|
8
|
-
[32mCJS[39m ⚡️ Build success in
|
|
9
|
-
[32mESM[39m [1mdist/index.mjs [22m[
|
|
10
|
-
[32mESM[39m ⚡️ Build success in
|
|
7
|
+
[32mCJS[39m [1mdist/index.js [22m[32m4.22 KB[39m
|
|
8
|
+
[32mCJS[39m ⚡️ Build success in 125ms
|
|
9
|
+
[32mESM[39m [1mdist/index.mjs [22m[32m2.95 KB[39m
|
|
10
|
+
[32mESM[39m ⚡️ Build success in 125ms
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# @prairielearn/browser-utils
|
|
2
2
|
|
|
3
|
+
## 1.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 0548e8401: Add `templateFromAttributes` function
|
|
8
|
+
|
|
9
|
+
## 1.0.3
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 2b003b4d9: Upgrade all dependencies
|
|
14
|
+
- Updated dependencies [2b003b4d9]
|
|
15
|
+
- @prairielearn/html@3.0.3
|
|
16
|
+
|
|
3
17
|
## 1.0.2
|
|
4
18
|
|
|
5
19
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ const elements = parseHTML(
|
|
|
30
30
|
html`
|
|
31
31
|
<div>Hello, world</div>
|
|
32
32
|
<div>Goodbye, world</div>
|
|
33
|
-
|
|
33
|
+
`,
|
|
34
34
|
);
|
|
35
35
|
const div = parseHTMLElement(document, html`<div>Hello, world</div>`);
|
|
36
36
|
```
|
|
@@ -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
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
|
+
"version": "1.1.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"repository": {
|
|
@@ -21,13 +21,12 @@
|
|
|
21
21
|
"dev": "tsup --watch src/index.ts"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@prairielearn/html": "^3.0.
|
|
24
|
+
"@prairielearn/html": "^3.0.3",
|
|
25
25
|
"js-base64": "^3.7.5"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@prairielearn/tsconfig": "^0.0.0",
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"typescript": "^5.1.3"
|
|
29
|
+
"tsup": "^7.1.0",
|
|
30
|
+
"typescript": "^5.1.6"
|
|
32
31
|
}
|
|
33
32
|
}
|
package/src/index.ts
CHANGED
|
@@ -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
|
+
}
|