diff --git a/package.json b/package.json
index 03c7b6f6..aead5391 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "impro",
- "version": "0.14.90",
+ "version": "0.14.91",
"type": "module",
"scripts": {
"start": "rm -rf build && NODE_ENV=development eleventy --serve",
diff --git a/src/css/style.css b/src/css/style.css
index 8c05baa9..ec227cdf 100644
--- a/src/css/style.css
+++ b/src/css/style.css
@@ -7528,11 +7528,6 @@ image-cropper {
height: 24px;
}
-/* .plugin-settings-tab {
- padding-top: 16px;
- padding-bottom: 16px;
-} */
-
.plugin-settings-tab h1,
.plugin-settings-tab h2,
.plugin-settings-tab h3,
@@ -7565,10 +7560,6 @@ image-cropper {
margin: 8px 0;
}
-/* .plugin-settings-tab a {
- color: var(--text-link-color);
-} */
-
.plugin-setting-item {
display: flex;
flex-wrap: wrap;
@@ -7579,10 +7570,6 @@ image-cropper {
border-bottom: var(--hair) solid var(--generic-border-color);
}
-/* .plugin-setting-item:first-child {
- padding-top: 0;
-} */
-
.plugin-setting-item:last-child {
border-bottom: none;
}
diff --git a/src/js/components/plugin-profiles-list.js b/src/js/components/plugin-profiles-list.js
index 6f0573cb..66b7dfbb 100644
--- a/src/js/components/plugin-profiles-list.js
+++ b/src/js/components/plugin-profiles-list.js
@@ -4,7 +4,7 @@ import { profileFeedTemplate } from "/js/templates/profileFeed.template.js";
class PluginProfilesList extends Component {
static get observedAttributes() {
- return ["dids"];
+ return ["dids", "empty-message"];
}
connectedCallback() {
@@ -57,12 +57,14 @@ class PluginProfilesList extends Component {
render(html`
${this.error}
`, this);
return;
}
+ const emptyMessage = this.getAttribute("empty-message");
render(
profileFeedTemplate({
profiles: this.profiles,
hasMore: false,
showEndMessage: false,
skeletonCount: this.parseDids().length,
+ ...(emptyMessage ? { emptyMessage } : {}),
}),
this,
);
diff --git a/src/js/plugins/pluginRendering.js b/src/js/plugins/pluginRendering.js
index fd3e637b..4fd36d98 100644
--- a/src/js/plugins/pluginRendering.js
+++ b/src/js/plugins/pluginRendering.js
@@ -63,8 +63,6 @@ const ALLOWED_ATTRS = [
"for",
"id",
"href",
- "dids",
- "icon",
];
function isSafeHref(value) {
@@ -77,12 +75,15 @@ function isSafeHref(value) {
}
}
-function isAllowedAttr(name) {
- return (
- ALLOWED_ATTRS.includes(name) ||
- name.startsWith("data-") ||
- name.startsWith("aria-")
- );
+function isAllowedAttr(name, tag) {
+ if (ALLOWED_ATTRS.includes(name)) return true;
+ if (name.startsWith("data-") || name.startsWith("aria-")) return true;
+ if (tag && tag.includes("-")) {
+ // Allow custom elements observed attributes
+ const ctor = customElements.get(tag);
+ if (ctor?.observedAttributes?.includes(name)) return true;
+ }
+ return false;
}
function createVirtualEvent(e) {
@@ -175,7 +176,7 @@ export class PluginRenderer {
}
if (node.attrs) {
for (const [name, value] of Object.entries(node.attrs)) {
- if (!isAllowedAttr(name)) {
+ if (!isAllowedAttr(name, tag)) {
console.warn(
`[plugins] "${pluginId}" tried to set disallowed attribute "${name}" on <${tag}>`,
);
@@ -210,14 +211,15 @@ export class PluginRenderer {
const oldAttrs = oldNode.attrs ?? {};
const newAttrs = newNode.attrs ?? {};
const isFocused = document.activeElement === element;
+ const tag = element.localName;
for (const name of Object.keys(oldAttrs)) {
- if (!(name in newAttrs) && isAllowedAttr(name)) {
+ if (!(name in newAttrs) && isAllowedAttr(name, tag)) {
element.removeAttribute(name);
}
}
for (const [name, value] of Object.entries(newAttrs)) {
- if (!isAllowedAttr(name)) {
+ if (!isAllowedAttr(name, tag)) {
console.warn(
`[plugins] "${pluginId}" tried to set disallowed attribute "${name}"`,
);
diff --git a/tests/unit/specs/plugins/pluginRendering.test.js b/tests/unit/specs/plugins/pluginRendering.test.js
index 250db640..94dbfeb2 100644
--- a/tests/unit/specs/plugins/pluginRendering.test.js
+++ b/tests/unit/specs/plugins/pluginRendering.test.js
@@ -341,6 +341,56 @@ t.describe("PluginRenderer:plugin-icon", (it) => {
});
});
+t.describe("PluginRenderer:custom element observedAttributes", (it) => {
+ it("passes through attrs declared in a custom element's observedAttributes", () => {
+ // plugin-icon declares observedAttributes = ["icon"] — verifies the
+ // observedAttributes lookup is what allows `icon` through now that it
+ // has been removed from the global ALLOWED_ATTRS list.
+ const { bridge } = makeBridge();
+ const renderer = new PluginRenderer(bridge, "demo");
+ const element = renderer.createRoot().render({
+ tag: "plugin-icon",
+ attrs: { icon: "bell" },
+ });
+ assertEquals(element.getAttribute("icon"), "bell");
+ });
+
+ it("drops custom attrs that aren't in observedAttributes", () => {
+ const { bridge } = makeBridge();
+ const renderer = new PluginRenderer(bridge, "demo");
+ const element = renderer.createRoot().render({
+ tag: "plugin-icon",
+ attrs: { icon: "bell", "secret-mode": "on" },
+ });
+ assertEquals(element.getAttribute("icon"), "bell");
+ assert(!element.hasAttribute("secret-mode"));
+ });
+
+ it("does not scope a custom attr from one component onto another", () => {
+ // `dids` is observed by plugin-profiles-list but not by plugin-icon —
+ // it should not leak across tags.
+ const { bridge } = makeBridge();
+ const renderer = new PluginRenderer(bridge, "demo");
+ const element = renderer.createRoot().render({
+ tag: "plugin-icon",
+ attrs: { icon: "bell", dids: "did:test:a" },
+ });
+ assert(!element.hasAttribute("dids"));
+ });
+
+ it("removes a custom attr on patch when it's dropped from the new tree", () => {
+ const { bridge } = makeBridge();
+ const renderer = new PluginRenderer(bridge, "demo");
+ const root = renderer.createRoot();
+ const element = root.render({
+ tag: "plugin-icon",
+ attrs: { icon: "bell" },
+ });
+ root.render({ tag: "plugin-icon", attrs: {} });
+ assert(!element.hasAttribute("icon"));
+ });
+});
+
t.describe("PluginRenderer:anchor tags", (it) => {
it("renders with safe https href and forces target/rel", () => {
const { bridge } = makeBridge();