From 75228e1a3a2f409a00f7418007ac1eb792514717 Mon Sep 17 00:00:00 2001 From: Lightling Date: Wed, 15 May 2024 01:32:33 -0400 Subject: [PATCH] create a link embed - proof of concept of a separate web components package - link embed has icon/title/subtitle - web components are registered just before app mounts, making them available in the markdown as well as the app --- libs/embeds/package.json | 7 ++ libs/embeds/src/index.ts | 1 + libs/embeds/src/link.ts | 101 +++++++++++++++++++++++ libs/embeds/src/observer.d.ts | 7 ++ libs/types/package.json | 2 +- package-lock.json | 23 ++++-- projects/frontend/package.json | 1 + projects/frontend/src/main.ts | 4 + projects/frontend/src/utilities/fetch.ts | 12 +-- 9 files changed, 141 insertions(+), 17 deletions(-) create mode 100644 libs/embeds/package.json create mode 100644 libs/embeds/src/index.ts create mode 100644 libs/embeds/src/link.ts create mode 100644 libs/embeds/src/observer.d.ts diff --git a/libs/embeds/package.json b/libs/embeds/package.json new file mode 100644 index 0000000..33afe6c --- /dev/null +++ b/libs/embeds/package.json @@ -0,0 +1,7 @@ +{ + "name": "@goldenwere/static-web-templates-embeds", + "private": true, + "version": "0.0.0", + "type": "module", + "main": "src/index.ts" +} diff --git a/libs/embeds/src/index.ts b/libs/embeds/src/index.ts new file mode 100644 index 0000000..6bbafd2 --- /dev/null +++ b/libs/embeds/src/index.ts @@ -0,0 +1 @@ +export * from './link' diff --git a/libs/embeds/src/link.ts b/libs/embeds/src/link.ts new file mode 100644 index 0000000..342731e --- /dev/null +++ b/libs/embeds/src/link.ts @@ -0,0 +1,101 @@ +import type { AttributeObserver } from './observer' + +export type LinkEmbedAttributes = typeof LinkEmbed.observedAttributes[number] + +/** + * A link embed is a link with a title and an optional icon + subtitle + */ +export class LinkEmbed extends HTMLElement { + static observedAttributes = [ + 'icon', + 'title', + 'subtitle', + 'href', + 'target', + ] as const + + _observers: AttributeObserver = {} as any + + constructor() { + super() + + const { ownerDocument } = this + + const _link = ownerDocument.createElement('a') + _link.setAttribute('rel', 'noreferrer noopener') + this._observers.href = { + element: _link, + observer: (oldVal, newVal) => { + const { element } = this._observers.href + element!.setAttribute('href', newVal) + }, + } + this._observers.target = { + element: _link, + observer: (oldVal, newVal) => { + const { element } = this._observers.href + if (!!newVal) { + element!.setAttribute('target', newVal) + } else { + element!.removeAttribute('target') + } + }, + } + this.append(_link) + + const _icon = ownerDocument.createElement('img') + _icon.alt = '' // decorative + this._observers.icon = { + element: _icon, + observer: (oldVal, newVal) => { + const { element } = this._observers.icon + if (!!newVal) { + element!.setAttribute('src', newVal) + } else { + element!.style.display = 'hidden' + } + }, + } + + const _textWrapper = ownerDocument.createElement('div') + _textWrapper.classList.add('text') + _link.append(_textWrapper) + + const _title = ownerDocument.createElement('p') + this._observers.title = { + element: _title, + observer: (oldVal, newVal) => { + const { element } = this._observers.title + element!.innerHTML = newVal + }, + } + + const _subtitle = ownerDocument.createElement('p') + this._observers.subtitle = { + element: _subtitle, + observer: (oldVal, newVal) => { + const { element } = this._observers.subtitle + if (!!newVal) { + element!.innerHTML = newVal + } else { + element!.style.display = 'hidden' + } + }, + } + + _textWrapper.append( + _title, + _subtitle, + ) + } + + attributeChangedCallback(name: LinkEmbedAttributes, oldVal: any, newVal: any) { + this._observers[name].observer(oldVal, newVal) + } +} + +export const registerLinkEmbed = () => { + if (!customElements.get('link-embed')) { + customElements.define('link-embed', LinkEmbed) + } +} diff --git a/libs/embeds/src/observer.d.ts b/libs/embeds/src/observer.d.ts new file mode 100644 index 0000000..29e2871 --- /dev/null +++ b/libs/embeds/src/observer.d.ts @@ -0,0 +1,7 @@ +export type AttributeObserver = Record void +}> diff --git a/libs/types/package.json b/libs/types/package.json index 007d12f..79e13c0 100644 --- a/libs/types/package.json +++ b/libs/types/package.json @@ -1,5 +1,5 @@ { - "name": "types", + "name": "@goldenwere/static-web-templates-types", "private": true, "version": "0.0.0", "type": "module", diff --git a/package-lock.json b/package-lock.json index 79f9848..e5d6f5d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,11 @@ "libs/*" ] }, + "libs/embeds": { + "version": "0.0.0" + }, "libs/types": { + "name": "@goldenwere/static-web-templates-types", "version": "0.0.0" }, "node_modules/@asamuzakjp/dom-selector": { @@ -438,12 +442,8 @@ "node": ">=12" } }, - "node_modules/@goldenwere/static-web-templates-frontend": { - "resolved": "projects/frontend", - "link": true - }, - "node_modules/@goldenwere/static-web-templates-sites": { - "resolved": "projects/sites", + "node_modules/@goldenwere/static-web-templates-embeds": { + "resolved": "libs/embeds", "link": true }, "node_modules/@goldenwere/static-web-templates-types": { @@ -1518,6 +1518,10 @@ "node": ">= 6" } }, + "node_modules/frontend": { + "resolved": "projects/frontend", + "link": true + }, "node_modules/fs-extra": { "version": "11.2.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", @@ -2741,6 +2745,10 @@ "node": ">= 0.4" } }, + "node_modules/sites": { + "resolved": "projects/sites", + "link": true + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -3351,9 +3359,9 @@ } }, "projects/frontend": { - "name": "@goldenwere/static-web-templates-frontend", "version": "0.0.0", "devDependencies": { + "@goldenwere/static-web-templates-embeds": "*", "@goldenwere/static-web-templates-types": "*", "@types/dompurify": "3.0.5", "@types/js-yaml": "4.0.9", @@ -3381,7 +3389,6 @@ } }, "projects/sites": { - "name": "@goldenwere/static-web-templates-sites", "version": "0.0.0", "devDependencies": { "symlink-dir": "6.0.0" diff --git a/projects/frontend/package.json b/projects/frontend/package.json index a260eb9..631e45b 100644 --- a/projects/frontend/package.json +++ b/projects/frontend/package.json @@ -10,6 +10,7 @@ }, "devDependencies": { "@goldenwere/static-web-templates-types": "*", + "@goldenwere/static-web-templates-embeds": "*", "@types/dompurify": "3.0.5", "@types/js-yaml": "4.0.9", "@types/node": "18.16.x", diff --git a/projects/frontend/src/main.ts b/projects/frontend/src/main.ts index 2a63009..43fb38c 100644 --- a/projects/frontend/src/main.ts +++ b/projects/frontend/src/main.ts @@ -4,6 +4,8 @@ import hljs from 'highlight.js' import { marked } from 'marked' import { markedHighlight } from 'marked-highlight' +import { registerLinkEmbed } from '@goldenwere/static-web-templates-embeds' + import main from './main.vue' import './main.sass' @@ -27,6 +29,8 @@ export const createApp = ViteSSG( { routes: createRoutes() }, // function to have custom setups ({ app, router, routes, isClient, initialState }) => { + registerLinkEmbed() + app.use(createPinia()) initializeRouteStore(routes) }, diff --git a/projects/frontend/src/utilities/fetch.ts b/projects/frontend/src/utilities/fetch.ts index c9cb4b5..e82c64f 100644 --- a/projects/frontend/src/utilities/fetch.ts +++ b/projects/frontend/src/utilities/fetch.ts @@ -19,15 +19,11 @@ const domPurifyConfig = { 'src', 'style', 'title', + 'target', - // project-tile - 'info', - 'view-path', - 'thumbnail-background', - 'thumbnail-position', - 'caption', + // embeds 'subtitle', - 'summary', + 'icon', ], ALLOWED_TAGS: [ 'a', @@ -80,7 +76,7 @@ const domPurifyConfig = { 'dl', 'dt', - 'project-tile', + 'link-embed', ], }