Files
shopdb-flask/plugins/tools/frontend/sharelink.js

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
}