You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
wiki/blocks/block-qr-code/component.js

169 lines
4.5 KiB

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`<div class="error">${this._error}</div>`
}
const size = `${Math.min(Math.max(Number(this.size) || 180, 80), 600)}px`
return html`
<div class="qr" style="--qr-size: ${size}">
${unsafeSVG(this._svg)}
${this.caption ? html`<div class="caption">${this.caption}</div>` : null}
</div>
`
}
}
window.customElements.define('block-qr-code', BlockQrCodeElement)