117 lines
4.0 KiB
JavaScript
117 lines
4.0 KiB
JavaScript
/**
|
|
* A link that opens the generator with somebody's work already in it.
|
|
*
|
|
* WHY THE FRAGMENT
|
|
*
|
|
* Everything rides after the `#`. A fragment is never transmitted: it does not
|
|
* reach Flask, it is not in the IIS log, and the proxy between the two never
|
|
* sees it. A query string would put the encoded content in all three, and the
|
|
* whole point of this feature is to send somebody a code they can print - which
|
|
* means the content is often an asset number, a serial, or a URL somebody would
|
|
* rather not scatter through logs.
|
|
*
|
|
* It is NOT secrecy. The link still sits in a chat message, in browser history
|
|
* and in whatever archives that chat. Anyone holding the link has the content.
|
|
* Secure Send exists for the case where that matters.
|
|
*
|
|
* WHAT TRAVELS
|
|
*
|
|
* What was typed (content, label, copies, the CSV) plus the settings that
|
|
* change how the label looks. Not the picture: a data URL runs to megabytes and
|
|
* would produce a link no chat client will carry.
|
|
*/
|
|
|
|
// Short keys because the whole thing has to survive being pasted into a chat
|
|
// box. `c` is the content, `t` the code type, `p` the label preset.
|
|
const SHORT = {
|
|
c: 'content',
|
|
l: 'label',
|
|
n: 'copies',
|
|
b: 'back',
|
|
v: 'csv',
|
|
s: 'source',
|
|
t: 'codetype',
|
|
e: 'errorcorrection',
|
|
p: 'preset',
|
|
}
|
|
|
|
// Layout values travel under their own names - there are enough of them that
|
|
// abbreviating would cost more in confusion than it saves in characters.
|
|
const PASSTHROUGH = [
|
|
'labelwidth', 'labelheight', 'padding', 'quiet', 'gap', 'codesize',
|
|
'barheight', 'labelfont', 'layout', 'align', 'valign',
|
|
'contentx', 'contenty', 'nudgex', 'nudgey', 'dpi',
|
|
'backenabled', 'backsource', 'backcodetype', 'backtext', 'backlayout',
|
|
'backorder',
|
|
]
|
|
|
|
const LONG = Object.fromEntries(Object.entries(SHORT).map(([k, v]) => [v, k]))
|
|
|
|
/**
|
|
* Build the fragment for a state object. Returns '' when there is nothing to
|
|
* share, so a caller can disable its button rather than emit a bare '#'.
|
|
*/
|
|
export function buildFragment (state, defaults = {}) {
|
|
const params = new URLSearchParams()
|
|
|
|
for (const [key, name] of Object.entries(SHORT)) {
|
|
const value = state[name]
|
|
if (value === undefined || value === null || value === '') continue
|
|
// Copies of 1 is what everybody has; it does not need saying.
|
|
if (name === 'copies' && Number(value) === 1) continue
|
|
params.set(key, String(value))
|
|
}
|
|
|
|
// Only the layout values that DIFFER from this browser's current settings.
|
|
// Sending all twenty would make a link four times longer for no gain, and the
|
|
// recipient's own saved settings are a reasonable starting point.
|
|
for (const name of PASSTHROUGH) {
|
|
const value = state[name]
|
|
if (value === undefined || value === null || value === '') continue
|
|
if (defaults[name] !== undefined && String(defaults[name]) === String(value)) continue
|
|
params.set(name, String(value))
|
|
}
|
|
|
|
const encoded = params.toString()
|
|
return encoded ? '#' + encoded : ''
|
|
}
|
|
|
|
/**
|
|
* Read a fragment back into a partial state object.
|
|
*
|
|
* Unknown keys are ignored rather than refused: a link made by a newer version
|
|
* must still open here, minus whatever this version does not understand.
|
|
*/
|
|
export function readFragment (hash) {
|
|
const raw = String(hash || '').replace(/^#/, '')
|
|
if (!raw) return {}
|
|
|
|
const params = new URLSearchParams(raw)
|
|
const state = {}
|
|
|
|
for (const [key, value] of params.entries()) {
|
|
const name = SHORT[key] || (PASSTHROUGH.includes(key) ? key : null)
|
|
if (!name) continue
|
|
state[name] = value
|
|
}
|
|
return state
|
|
}
|
|
|
|
/**
|
|
* Coerce a fragment value to the type the ref already holds.
|
|
*
|
|
* Everything arrives as a string. Assigning '203' to a numeric input renders,
|
|
* but the moment it is used in arithmetic it concatenates - so a nudge of 0
|
|
* plus '2' becomes '02'. Match the current value's type instead.
|
|
*/
|
|
export function coerce (value, current) {
|
|
if (typeof current === 'number') {
|
|
const number = Number(value)
|
|
return Number.isFinite(number) ? number : current
|
|
}
|
|
if (typeof current === 'boolean') {
|
|
return value === 'true' || value === '1'
|
|
}
|
|
return value
|
|
}
|