import { LitElement, html, css } from 'lit' import { unsafeSVG } from 'lit/directives/unsafe-svg.js' import { renderSVG } from 'uqr' import { DarkMode } from '../shared/theme.js' /** * Block QR Code */ export class BlockQrCodeElement extends LitElement { /** * Metadata for the admin area and the editor's block picker. Collected at build time into * `compiled/blocks.manifest.json`, which the server reads to register the block. Values must be * plain literals. See `props` in `block-index` for what the picker does with that list. */ static definition = { block: 'qr-code', name: 'QR Code', description: 'Shows a QR code for a link or a piece of text.', icon: 'qr', props: [ { name: 'value', type: 'string', label: 'Content', hint: 'Text or URL to encode. The address of this page when left empty.' }, { name: 'size', type: 'number', label: 'Size', hint: 'Width of the code in pixels.', default: 180 }, { name: 'caption', type: 'string', label: 'Caption', hint: 'Shown under the code.' } ] } static get styles() { return css` :host { display: block; } /* -> The gap below the block. On this element rather than :host: see block-index. */ .qr { margin-bottom: 16px; display: inline-flex; flex-direction: column; align-items: center; gap: 8px; padding: 12px; border: 1px solid rgba(0, 0, 0, 0.1); border-radius: 5px; /* White in both themes, and padded: a code is read by a camera looking for dark squares on a light field, so inverting it for dark mode would make it harder to scan, not easier. */ background-color: #fff; } :host([dark]) .qr { border-color: rgba(255, 255, 255, 0.15); } /* -> The drawing is sized here, so the box grows by its own padding rather than eating into it */ .qr svg { display: block; width: var(--qr-size); height: auto; } .caption { max-width: var(--qr-size); color: #424242; font-size: 0.8em; text-align: center; overflow-wrap: anywhere; } .error { color: var(--q-negative, #c10015); border: 1px dashed color-mix(in srgb, currentColor 50%, transparent); border-radius: 5px; padding: 1rem; margin-bottom: 16px; } ` } static get properties() { return { /** * Text or URL to encode * @type {string} */ value: { type: String }, /** * Width of the code in pixels * @type {number} */ size: { type: Number }, /** * Text shown under the code * @type {string} */ caption: { type: String }, // Internal Properties _svg: { state: true }, _error: { state: true } } } constructor() { super() this.value = '' this.size = 180 this.caption = '' this._svg = '' this._error = '' // -> Puts `dark` on this element for the styles above to key off this._darkMode = new DarkMode(this) } /** * What the code stands for. * * An empty `value` means this page, which is the common case — a printed page, or a screen someone * wants to carry on their phone. Taken from the address bar rather than built from the site config, * so it is the URL the reader is actually looking at, and without the fragment, which points at a * place on the page rather than at the page. */ _encoded() { return this.value?.trim() || `${window.location.origin}${window.location.pathname}` } connectedCallback() { super.connectedCallback() try { // -> Drawn at a fixed scale and sized by CSS, so the same markup is crisp at any width this._svg = renderSVG(this._encoded(), { border: 1, pixelSize: 8 }) } catch { // -> Every symbol size has a ceiling, and a long enough string clears the largest of them this._error = 'This is too long to fit in a QR code.' } } render() { if (this._error) { return html`