added some comments, fix test server, better formatting

This commit is contained in:
André Fiedler 2024-12-03 21:32:18 +01:00
parent 0009b4fc55
commit 77ae56924e
15 changed files with 2429 additions and 154 deletions

2
.gitignore vendored
View File

@ -2,3 +2,5 @@
.vscode/
.DS_Store
node_modules/

9
.prettierrc.json Normal file
View File

@ -0,0 +1,9 @@
{
"trailingComma": "none",
"tabWidth": 4,
"semi": false,
"singleQuote": true,
"singleAttributePerLine": false,
"html.format.wrapLineLength": 0,
"printWidth": 140
}

View File

@ -2,6 +2,7 @@ import SVGRenderer from '/src/SVGRenderer.mjs'
import PNGRenderer from '/src/PNGRenderer.mjs'
import PDFRenderer from '/src/PDFRenderer.mjs'
// Get references to the custom elements
const box = document.querySelector('fabaccess-preview-box')
const form = document.querySelector('fabaccess-settings-form')
@ -12,10 +13,12 @@ form.addEventListener('change', (e) => {
box.update()
})
// Add click event listeners to the buttons
document.querySelector('#download-qr-code-svg').addEventListener('click', () => SVGRenderer.downloadSVG(form.machineID, form.size))
document.querySelector('#download-qr-code-png').addEventListener('click', () => PNGRenderer.downloadPNG(form.machineID, form.size))
document.querySelector('#add-qr-code-to-page').addEventListener('click', () => PDFRenderer.addToPDF(form.machineID, form.size))
document.querySelector('#download-pdf').addEventListener('click', () => PDFRenderer.downloadPDF())
// Initialize the preview box
box.setAttribute('value', form.machineID)
box.setAttribute('size', form.size)

1997
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,16 +1,25 @@
{
"name": "fabaccess-sticker-generator",
"version": "1.0.0",
"description": "FabAccess Sticker Generator",
"homepage": "https://fabaccess-sticker-generator.sternenlabor.de/",
"main": "index.mjs",
"scripts": {
"start": "light-server --serve ./"
},
"author": "André Fiedler",
"license": "CC0",
"bugs": {
"url": "https://github.com/Sternenlabor/fabaccess-sticker-generator/issues"
},
"dependencies": {}
"name": "fabaccess-sticker-generator",
"version": "1.0.0",
"description": "FabAccess Sticker Generator",
"homepage": "https://fabaccess-sticker-generator.sternenlabor.de/",
"main": "index.mjs",
"scripts": {
"start": "npm run lite",
"lite": "lite-server"
},
"author": "André Fiedler",
"license": "CC0",
"bugs": {
"url": "https://github.com/Sternenlabor/fabaccess-sticker-generator/issues"
},
"devDependencies": {
"axios": "^1.7.8",
"lite-server": "^2.6.1"
},
"overrides": {
"localtunnel": {
"axios": "1.6.2"
}
}
}

View File

@ -1,40 +1,60 @@
// Create a template element to define the structure of the custom checkbox component
const template = document.createElement('template')
template.innerHTML = /* html */ `
<style> @import url("/src/Checkbox.css"); </style>
<label class="checkbox">
<input type="checkbox" id="checkbox" />
<span class="checkmark"></span>
<slot></slot>
</label>`
<style> @import url("/src/Checkbox.css"); </style>
<label class="checkbox">
<input type="checkbox" id="checkbox" />
<span class="checkmark"></span>
<slot></slot>
</label>`
// Define a custom Checkbox class that extends HTMLElement
class Checkbox extends HTMLElement {
#root = null
#root = null // Private property to store the root node
/**
* Creates an instance of Checkbox and initializes the shadow DOM.
*/
constructor() {
super()
super() // Call the parent class constructor
// Attach a shadow DOM tree to this element
this.attachShadow({ mode: 'open' })
// Append the cloned template content to the shadow root
this.shadowRoot.append(template.content.cloneNode(true))
// Get the root node of the shadow DOM
this.#root = this.shadowRoot.getRootNode()
// Get the checkbox input element from the shadow DOM
const checkbox = this.#root.getElementById('checkbox')
// Add an event listener for the 'change' event on the checkbox
checkbox.addEventListener('change', this.#handleChange.bind(this), {
passive: false
})
}
/**
* Handles the 'change' event of the checkbox input.
* @param {Event} e - The event object.
* @private
*/
#handleChange(e) {
const val = e.target.checked
const val = e.target.checked // Get the checked state of the checkbox
// Dispatch a custom 'change' event from the custom element
this.dispatchEvent(
new CustomEvent('change', {
detail: {
checked: val
checked: val // Include the checked state in the event detail
},
composed: true
composed: true // Allow the event to bubble up through the shadow DOM boundary
})
)
}
}
// Define the custom element 'fabaccess-checkbox' associated with the Checkbox class
customElements.define('fabaccess-checkbox', Checkbox)

View File

@ -1,54 +1,70 @@
import Color from '/lib/color.js'
import Color from '/lib/color.js' // Import the Color library
// Thanks to Firefox who's not supporting all color stuff in SVG like Chrome does ... we need to do this...
// Due to Firefox not supporting all color functionalities in SVG like Chrome does, we need to do this workaround
export default class ColorUtils {
/**
* Converts a CSS color function to a hex color code.
* @param {string} cssColorFunction - The CSS color function string to convert.
* @returns {string|null} - The converted hex color code or null if conversion is not possible.
*/
static convertColor(cssColorFunction) {
// Regular expression to match hex color codes
// Regular expression to match hex color codes (e.g., #FFF or #FFFFFF)
const hexColorRegex = /^#(?:[0-9a-fA-F]{3}){1,2}$/
// Check if the input is a valid hex color code
if (hexColorRegex.test(cssColorFunction)) {
return cssColorFunction // Return the hex color code if it matches the pattern
} else {
// Extracting the adjustments and hex value
// Extract the adjustments and hex value from the CSS color function
const result = this.extractAdjustmentsAndHex(cssColorFunction)
if (result) {
// Create a new Color object from the extracted hex value and convert it to OKLCH color space
const oklch = new Color(result.hex).to('oklch')
console.log(oklch.l, oklch.c, oklch.h)
console.log(oklch.l, oklch.c, oklch.h) // Log the original OKLCH values
// Apply the adjustments to the OKLCH components
oklch.l += result.lAdjust
oklch.c += result.cAdjust
oklch.h += result.hAdjust
console.log(oklch.l, oklch.c, oklch.h)
console.log(oklch.l, oklch.c, oklch.h) // Log the adjusted OKLCH values
// Convert the adjusted OKLCH color back to a hex color code in A98 RGB color space
const hex = oklch.to('a98rgb').toString({ format: 'hex' })
console.log(hex)
console.log(hex) // Log the final hex color code
return hex
return hex // Return the final hex color code
} else {
// If no adjustments and hex value could be extracted, log a message
console.log('No values found.', cssColorFunction)
}
}
}
/**
* Extracts adjustments and hex value from a CSS OKLCH color function string.
* @param {string} css - The CSS color function string to extract from.
* @returns {Object|null} - An object containing hex, lAdjust, cAdjust, hAdjust or null if no matches found.
*/
static extractAdjustmentsAndHex(css) {
// Regular expression to match the specific OKLCH color function format
const regex = /oklch\(from (#\w{6}) calc\(l \+ ([\d\.\-]+)\) calc\(c \+ ([\d\.\-]+)\) calc\(h - ([\d\.\-]+)\)\)/
const matches = css.match(regex)
if (matches) {
// Destructure the matches to extract hex value and adjustments
const [_, hex, lAdjust, cAdjust, hAdjust] = matches
return {
hex: hex,
lAdjust: parseFloat(lAdjust),
cAdjust: parseFloat(cAdjust),
hAdjust: parseFloat(hAdjust)
hex: hex, // Original hex color code
lAdjust: parseFloat(lAdjust), // Adjustment for lightness
cAdjust: parseFloat(cAdjust), // Adjustment for chroma
hAdjust: parseFloat(hAdjust) // Adjustment for hue
}
} else {
// Return null or some default values if no matches found
// Return null if no matches found
return null
}
}

View File

@ -1,27 +1,49 @@
// Implementation of a custom EventTarget class to manage event listeners and dispatch events
export default class EventTarget {
#listeners = {}
#listeners = {} // Private property to store event listeners
/**
* Adds an event listener for a specific event type.
* @param {string} event - The event type to listen for.
* @param {Function} callback - The callback function to be called when the event is dispatched.
*/
addEventListener(event, callback) {
// Initialize the listeners array for the event if it doesn't exist
if (!this.#listeners[event]) {
this.#listeners[event] = []
}
// Add the callback to the listeners array for the event
this.#listeners[event].push(callback)
}
/**
* Removes an event listener for a specific event type.
* @param {string} event - The event type for which the listener should be removed.
* @param {Function} callback - The callback function to remove from the listeners.
*/
removeEventListener(event, callback) {
// If there are no listeners for the event, exit the method
if (!this.#listeners[event]) {
return
}
// Find the index of the callback in the listeners array
const callbackIndex = this.#listeners[event].indexOf(callback)
// If the callback exists, remove it from the array
if (callbackIndex > -1) {
this.#listeners[event].splice(callbackIndex, 1)
}
}
/**
* Dispatches an event to all registered listeners of the event's type.
* @param {Object} event - The event object to dispatch. Should have a 'type' property.
*/
dispatchEvent(event) {
// If there are no listeners for the event type, exit the method
if (!this.#listeners[event.type]) {
return
}
// Call each listener's callback function with the event object
this.#listeners[event.type].forEach((callback) => {
callback(event)
})

View File

@ -6,6 +6,7 @@ import { changeDpiBlob } from '/lib/changeDPI/index.js'
import Utils from '/src/Utils.mjs'
import SVGRenderer from '/src/SVGRenderer.mjs'
// Constants for rendering
const DPI = 300.0
const SVG_PIXEL_HEIGHT = 108.0
const SVG_PIXEL_WIDTH = 197.0
@ -16,10 +17,15 @@ const PAGE_MARGIN_Y = 10.0 // mm
const SPACE_BETWEEN_CODE_AND_LABEL = 4.0 // mm
export default class PDFRenderer {
static #qrCodes = []
static #pdfDoc = null
static #listeners = {}
static #qrCodes = [] // Array to store QR code data
static #pdfDoc = null // PDF document instance
static #listeners = {} // Event listeners
/**
* Adds an event listener for a specific event type.
* @param {string} event - The event type to listen for.
* @param {Function} callback - The callback function to be called when the event is dispatched.
*/
static addEventListener(event, callback) {
if (!this.#listeners[event]) {
this.#listeners[event] = []
@ -27,6 +33,11 @@ export default class PDFRenderer {
this.#listeners[event].push(callback)
}
/**
* Removes an event listener for a specific event type.
* @param {string} event - The event type for which the listener should be removed.
* @param {Function} callback - The callback function to remove from the listeners.
*/
static removeEventListener(event, callback) {
if (!this.#listeners[event]) {
return
@ -37,6 +48,10 @@ export default class PDFRenderer {
}
}
/**
* Dispatches an event to all registered listeners of the event's type.
* @param {Object} event - The event object to dispatch. Should have a 'type' property.
*/
static dispatchEvent(event) {
if (!this.#listeners[event.type]) {
return
@ -46,30 +61,39 @@ export default class PDFRenderer {
})
}
/**
* Adds a QR code to the PDF document.
* @param {string} machineID - The machine ID to encode in the QR code.
* @param {number|string} size - The size of the QR code in millimeters.
*/
static async addToPDF(machineID, size) {
const mmHeight = parseFloat(size)
const mmWidth = (mmHeight / SVG_PIXEL_HEIGHT) * SVG_PIXEL_WIDTH
const mmHeight = parseFloat(size) // Convert size to float
const mmWidth = (mmHeight / SVG_PIXEL_HEIGHT) * SVG_PIXEL_WIDTH // Calculate width proportionally
const inches = mmHeight /* mm */ / 25.4 // There are 25.4 millimeters in an inch
const pixelHeight = inches * DPI
const pixelWidth = (pixelHeight / SVG_PIXEL_HEIGHT) * SVG_PIXEL_WIDTH
const pixelHeight = inches * DPI // Calculate pixel height
const pixelWidth = (pixelHeight / SVG_PIXEL_HEIGHT) * SVG_PIXEL_WIDTH // Calculate pixel width
const height = Math.round(pixelHeight)
const width = Math.round(pixelWidth)
const height = Math.round(pixelHeight) // Round to nearest integer
const width = Math.round(pixelWidth) // Round to nearest integer
// Generate SVG code for the QR code
const svgCode = SVGRenderer.getCode(machineID, height, width)
// Create an offscreen canvas to render the SVG
const c = new OffscreenCanvas(width, height)
const ctx = c.getContext('2d')
// Use canvg to render SVG to canvas
const v = await canvg.Canvg.fromString(ctx, svgCode, canvg.presets.offscreen())
v.resize(width, height, 'xMidYMid meet')
v.resize(width, height, 'xMidYMid meet') // Resize the SVG to fit the canvas
await v.render()
await v.render() // Render the SVG onto the canvas
let b = await c.convertToBlob()
b = await changeDpiBlob(b, DPI)
const imgData = URL.createObjectURL(b)
let b = await c.convertToBlob() // Convert canvas to Blob
b = await changeDpiBlob(b, DPI) // Change the DPI of the blob
const imgData = URL.createObjectURL(b) // Create a URL for the blob
// Store QR code data
this.#qrCodes.push({
machineID,
mmHeight,
@ -79,12 +103,17 @@ export default class PDFRenderer {
imgData
})
// Render the PDF with the new QR code
this.renderPDF()
}
/**
* Renders the PDF document with all added QR codes.
*/
static async renderPDF() {
const compress = 'fast'
const compress = 'fast' // Compression option for images
// Create a new jsPDF document
this.#pdfDoc = new jspdf.jsPDF({
orientation: 'portrait',
unit: 'mm',
@ -94,47 +123,55 @@ export default class PDFRenderer {
compressPdf: false
})
this.#pdfDoc.setFontSize(9)
this.#pdfDoc.setTextColor('#3c474d')
this.#pdfDoc.setFontSize(9) // Set font size for labels
this.#pdfDoc.setTextColor('#3c474d') // Set text color
// Get page dimensions
const pageSize = this.#pdfDoc.internal.pageSize
const pageHeight = pageSize.height ? pageSize.height : pageSize.getHeight()
const pageWidth = this.#pdfDoc.internal.pageSize.width || this.#pdfDoc.internal.pageSize.getWidth()
const pageWidth = pageSize.width ? pageSize.width : pageSize.getWidth()
// Sort QR codes by height
this.#qrCodes.sort((a, b) => a.mmHeight - b.mmHeight)
let curX = PAGE_MARGIN_X
let curY = PAGE_MARGIN_Y
let curX = PAGE_MARGIN_X // Current X position
let curY = PAGE_MARGIN_Y // Current Y position
for (let i = 0; i < this.#qrCodes.length; i++) {
const { machineID, imgData, mmWidth, mmHeight } = this.#qrCodes[i]
// Add QR code image to PDF
this.#pdfDoc.addImage(imgData, 'PNG', curX, curY, Math.round(mmWidth), Math.round(mmHeight), undefined, compress)
// Get dimensions of the machine ID text
const txtDim = this.#pdfDoc.getTextDimensions(machineID)
// Add machine ID text below the QR code, centered
this.#pdfDoc.text(machineID, curX + (mmWidth - txtDim.w) / 2, curY + mmHeight + SPACE_BETWEEN_CODE_AND_LABEL)
curX += mmWidth + PADDING_BETWEEN_CODES_X
curX += mmWidth + PADDING_BETWEEN_CODES_X // Update X position
if (i < this.#qrCodes.length - 1) {
// we are not at the end
// We are not at the end
if (curX + PADDING_BETWEEN_CODES_X + this.#qrCodes[i + 1].mmWidth > pageWidth) {
// next code will not fit on the current line
curX = PAGE_MARGIN_X
curY += mmHeight + SPACE_BETWEEN_CODE_AND_LABEL + txtDim.h + PADDING_BETWEEN_CODES_Y
// Next code will not fit on the current line
curX = PAGE_MARGIN_X // Reset X position
curY += mmHeight + SPACE_BETWEEN_CODE_AND_LABEL + txtDim.h + PADDING_BETWEEN_CODES_Y // Move to next line
}
if (curY + PADDING_BETWEEN_CODES_Y + this.#qrCodes[i + 1].mmHeight + SPACE_BETWEEN_CODE_AND_LABEL + txtDim.h > pageHeight) {
this.#pdfDoc.addPage()
curX = PAGE_MARGIN_X
curY = PAGE_MARGIN_Y
// Next code will not fit on the current page
this.#pdfDoc.addPage() // Add a new page
curX = PAGE_MARGIN_X // Reset X position
curY = PAGE_MARGIN_Y // Reset Y position
}
}
}
// Generate blob from PDF and create URL
var blobPDF = new Blob([this.#pdfDoc.output('blob')], { type: 'application/pdf' })
var blobUrl = URL.createObjectURL(blobPDF)
// Dispatch event with the PDF URL
this.dispatchEvent(
new CustomEvent('change', {
detail: {
@ -145,7 +182,10 @@ export default class PDFRenderer {
)
}
/**
* Initiates the download of the generated PDF document.
*/
static downloadPDF() {
this.#pdfDoc?.save(Utils.getFilename('pdf'))
this.#pdfDoc?.save(Utils.getFilename('pdf')) // Save the PDF with a generated filename
}
}

View File

@ -5,98 +5,124 @@ import PDFRenderer from '/src/PDFRenderer.mjs'
pdfjsLib.GlobalWorkerOptions.workerSrc = '/lib/jspdf/build/pdf.worker.mjs'
// Create a template element to define the structure of the PDF viewer component
const template = document.createElement('template')
template.innerHTML = /* html */ `
<style> @import url("/src/PDFViewer.css"); </style>
<div id="pdf-viewer">
<div class="toolbar">
<button id="prev" class="toolbar-button" title="Previous Page">
<span>Previous</span>
</button>
<button id="next" class="toolbar-button" title="Next Page">
<span>Next</span>
</button>
<span id="npages"></span>
<style> @import url("/src/PDFViewer.css"); </style>
<div id="pdf-viewer">
<div class="toolbar">
<button id="prev" class="toolbar-button" title="Previous Page">
<span>Previous</span>
</button>
<button id="next" class="toolbar-button" title="Next Page">
<span>Next</span>
</button>
<span id="npages"></span>
<span id="pdf-title">PDF</span>
<span id="pdf-title">PDF</span>
<!--
<button id="print" class="toolbar-button" title="Print">
<span>Print</span>
</button>
-->
<button id="download" class="toolbar-button" title="Save">
<span>Save</span>
</button>
</div>
<canvas id="cnv"></canvas>
</div>`
<!--
<button id="print" class="toolbar-button" title="Print">
<span>Print</span>
</button>
-->
<button id="download" class="toolbar-button" title="Save">
<span>Save</span>
</button>
</div>
<canvas id="cnv"></canvas>
</div>`
// Define a custom PDFViewer class that extends HTMLElement
class PDFViewer extends HTMLElement {
#root = null
#root = null // Private property to store the root node
/**
* Creates an instance of PDFViewer and initializes the component.
*/
constructor() {
super()
super() // Call the parent class constructor
// Attach a shadow DOM tree to this element
this.attachShadow({ mode: 'open' })
// Append the cloned template content to the shadow root
this.shadowRoot.append(template.content.cloneNode(true))
// Get the root node of the shadow DOM
this.#root = this.shadowRoot.getRootNode()
//this.#root.querySelector('#print').addEventListener('click', () => {})
// Add event listener for the 'download' button to download the PDF
// this.#root.querySelector('#print').addEventListener('click', () => {})
this.#root.querySelector('#download').addEventListener('click', () => PDFRenderer.downloadPDF())
// Listen for 'change' events from PDFRenderer to display the PDF
PDFRenderer.addEventListener('change', (e) => {
this.showPDF(e.detail.pdf)
})
}
/**
* Displays the PDF document in the viewer.
* @param {string} blobUrl - The URL of the PDF blob to display.
*/
async showPDF(blobUrl) {
// Load the PDF document
const loadingTask = pdfjsLib.getDocument(blobUrl)
const canvas = this.#root.querySelector('#cnv')
const ctx = canvas.getContext('2d')
const scale = 1.5
let numPage = 1
const canvas = this.#root.querySelector('#cnv') // Get the canvas element
const ctx = canvas.getContext('2d') // Get the 2D rendering context
const scale = 1.5 // Scale for rendering the PDF pages
let numPage = 1 // Current page number
const doc = await loadingTask.promise
const doc = await loadingTask.promise // Wait for the PDF to be loaded
/**
* Renders a specific page of the PDF document.
* @param {number} numPage - The page number to display.
*/
const showPage = async (numPage) => {
const page = await doc.getPage(numPage)
let viewport = page.getViewport({ scale: scale })
canvas.height = viewport.height
canvas.width = viewport.width
const page = await doc.getPage(numPage) // Get the page
let viewport = page.getViewport({ scale: scale }) // Get the viewport at the desired scale
canvas.height = viewport.height // Set canvas height
canvas.width = viewport.width // Set canvas width
let renderContext = {
canvasContext: ctx,
viewport: viewport
}
page.render(renderContext)
await page.render(renderContext) // Render the page into the canvas
// Update the page number display
this.#root.querySelector('#npages').innerHTML = `Page ${numPage} / ${doc.numPages}`
}
// Show the first page
this.#root.querySelector('#npages').innerHTML = `Page 1 / ${doc.numPages}`
showPage(numPage)
// Function to go to the previous page
const prevPage = () => {
if (numPage === 1) {
return
return // Do nothing if already at the first page
}
numPage--
showPage(numPage)
}
// Function to go to the next page
const nextPage = () => {
if (numPage >= doc.numPages) {
return
return // Do nothing if already at the last page
}
numPage++
showPage(numPage)
}
// Add event listeners for the 'prev' and 'next' buttons
this.#root.querySelector('#prev').addEventListener('click', prevPage)
this.#root.querySelector('#next').addEventListener('click', nextPage)
}
}
// Define the custom element 'fabaccess-pdf-viewer' associated with the PDFViewer class
customElements.define('fabaccess-pdf-viewer', PDFViewer)

View File

@ -11,32 +11,43 @@ const SVG_PIXEL_HEIGHT = 108.0
const SVG_PIXEL_WIDTH = 197.0
export default class PNGRenderer {
/**
* Downloads a PNG image of a QR code for the given machine ID and size.
* @param {string} machineID - The machine ID to encode in the QR code.
* @param {number|string} height - The height of the image in millimeters.
* @param {number|null} [width=null] - The width of the image in millimeters (optional).
*/
static async downloadPNG(machineID, height, width = null) {
const n = Utils.getFilename('png')
// Convert height from millimeters to inches
const inches = parseFloat(height) /* mm */ / 25.4 // There are 25.4 millimeters in an inch
const pixelHeight = inches * DPI
const pixelWidth = (pixelHeight / SVG_PIXEL_HEIGHT) * SVG_PIXEL_WIDTH
const pixelHeight = inches * DPI // Calculate pixel height based on DPI
const pixelWidth = (pixelHeight / SVG_PIXEL_HEIGHT) * SVG_PIXEL_WIDTH // Calculate pixel width proportionally
const heightPx = Math.round(pixelHeight)
const widthPx = Math.round(pixelWidth)
const heightPx = Math.round(pixelHeight) // Round pixel height to nearest integer
const widthPx = Math.round(pixelWidth) // Round pixel width to nearest integer
// Generate SVG code for the QR code
const svgCode = SVGRenderer.getCode(machineID, heightPx, widthPx)
// Create an offscreen canvas to render the SVG
const c = new OffscreenCanvas(widthPx, heightPx)
const ctx = c.getContext('2d')
// Use canvg to render SVG to canvas
const v = await canvg.Canvg.fromString(ctx, svgCode, canvg.presets.offscreen())
v.resize(widthPx, heightPx, 'xMidYMid meet')
v.resize(widthPx, heightPx, 'xMidYMid meet') // Resize the SVG to fit the canvas
await v.render()
await v.render() // Render the SVG onto the canvas
let b = await c.convertToBlob()
let b = await c.convertToBlob() // Convert canvas to Blob
b = await changeDpiBlob(b, DPI)
b = await changeDpiBlob(b, DPI) // Change the DPI of the blob
// Handle file download for different browsers
if (window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveOrOpenBlob(b, n)
window.navigator.msSaveOrOpenBlob(b, n) // For IE and Edge
} else {
const a = document.createElement('a')
const u = URL.createObjectURL(b)

View File

@ -1,65 +1,100 @@
import SVGRenderer from '/src/SVGRenderer.mjs'
// Create a template element to define the structure of the preview box component
const template = document.createElement('template')
template.innerHTML = /* html */ `
<style> @import url("/src/PreviewBox.css"); </style>
<div id="box"></div>`
<style> @import url("/src/PreviewBox.css"); </style>
<div id="box"></div>`
// Define a custom PreviewBox class that extends HTMLElement
class PreviewBox extends HTMLElement {
value = ''
size = 0
value = '' // The value to be encoded in the QR code
size = 0 // The size of the QR code
#root = null
#box = null
#animationId = 0
#root = null // Private property to store the root node
#box = null // Private property to store the box element
#animationId = 0 // Private property for requestAnimationFrame
/**
* Creates an instance of PreviewBox and initializes the component.
*/
constructor() {
super()
super() // Call the parent class constructor
// Attach a shadow DOM tree to this element
this.attachShadow({ mode: 'open' })
// Append the cloned template content to the shadow root
this.shadowRoot.append(template.content.cloneNode(true))
// Get the root node of the shadow DOM
this.#root = this.shadowRoot.getRootNode()
// Get the box element from the shadow DOM
this.#box = this.#root.getElementById('box')
// Initialize value and size from attributes
this.value = this.getAttribute('value') || ''
this.size = parseFloat(this.getAttribute('size'))
}
/**
* Observed attributes for the custom element.
* @returns {Array<string>} - The list of attributes to observe.
*/
static get observedAttributes() {
return ['value', 'size']
}
/**
* Called when one of the observed attributes changes.
* @param {string} name - The name of the attribute that changed.
* @param {string} oldValue - The old value of the attribute.
* @param {string} newValue - The new value of the attribute.
*/
attributeChangedCallback(name, oldValue, newValue) {
if (newValue != oldValue) {
switch (name) {
case 'value':
this[name] = newValue
this[name] = newValue // Update the value property
break
case 'size':
this[name] = parseFloat(newValue)
this[name] = parseFloat(newValue) // Update the size property
break
}
this.#render()
this.#render() // Re-render the QR code
}
}
/**
* Public method to update the QR code rendering.
*/
update() {
this.#render()
}
/**
* Clears the content of the box element.
* @private
*/
#clear() {
while (this.#box.childNodes[0]) {
this.#box.removeChild(this.#box.childNodes[0])
while (this.#box.firstChild) {
this.#box.removeChild(this.#box.firstChild)
}
}
/**
* Renders the QR code inside the box element.
* @private
*/
#render() {
// Cancel any pending animation frame
window.cancelAnimationFrame(this.#animationId)
// Schedule the rendering in the next animation frame
this.#animationId = window.requestAnimationFrame(() => {
this.#clear()
this.#clear() // Clear existing content
const time = new Date()
// Generate the QR code SVG and insert it into the box
this.#box.innerHTML = SVGRenderer.getCode(this.value, `${this.size}mm`)
console.log('QRCode generation time: ' + (new Date() - time) + ' ms')
@ -67,4 +102,5 @@ class PreviewBox extends HTMLElement {
}
}
// Define the custom element 'fabaccess-preview-box' associated with the PreviewBox class
customElements.define('fabaccess-preview-box', PreviewBox)

View File

@ -7,6 +7,13 @@ const SVG_PIXEL_WIDTH = 197.0
export default class SVGRenderer {
static optimizeForPrint = false
/**
* Generates the SVG code for a QR code with the given machine ID and dimensions.
* @param {string} machineID - The machine ID to encode in the QR code.
* @param {number|string} height - The height of the SVG in pixels or millimeters.
* @param {number|null} [width=null] - The width of the SVG in pixels or millimeters (optional).
* @returns {string} - The generated SVG code.
*/
static getCode(machineID, height, width = null) {
const data = {
msg: `urn:fabaccess:resource:${machineID}`,
@ -27,6 +34,7 @@ export default class SVGRenderer {
wh += `height="${height}" `
}
// Generate the SVG code with placeholders for colors
let svgCode = `
<svg version="1.1" viewBox="0 0 ${SVG_PIXEL_WIDTH} ${SVG_PIXEL_HEIGHT}" ${wh} xmlns="http://www.w3.org/2000/svg" shape-rendering="crispEdges">
<path d="m7.35 0c-4.06 0-7.33 3.27-7.33 7.33v23h-0.0197v70.7c0 4.06 3.27 7.33 7.33 7.33h182c4.06 0 7.33-3.27 7.33-7.33v-14.2h0.0197v-79.4c0-4.06-3.27-7.33-7.33-7.33z"
@ -55,15 +63,23 @@ export default class SVGRenderer {
return svgCode
}
/**
* Downloads the generated SVG code as an SVG file.
* @param {string} machineID - The machine ID to encode in the QR code.
* @param {number|string} height - The height of the SVG in pixels or millimeters.
* @param {number|null} [width=null] - The width of the SVG in pixels or millimeters (optional).
*/
static async downloadSVG(machineID, height, width = null) {
const svgCode = this.getCode(machineID, height, width)
const n = Utils.getFilename('svg')
// Create a new Blob object with the SVG code
const b = new Blob([svgCode], {
type: 'image/svg+xml'
})
// Handle file download
if (window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveOrOpenBlob(b, n)
} else {

View File

@ -1,51 +1,66 @@
import SVGRenderer from '/src/SVGRenderer.mjs'
import {} from '/src/Checkbox.mjs'
// Create a template element to define the structure of the settings form component
const template = document.createElement('template')
template.innerHTML = /* html */ `
<style> @import url("/src/SettingsForm.css"); </style>
<style> @import url("/src/SettingsForm.css"); </style>
<div class="form-group group-1">
<label for="machineID">Machine ID:</label>
<input type="text" name="machineID" id="machineID" placeholder="Enter the machine ID" />
</div>
<div class="form-group group-1">
<label for="machineID">Machine ID:</label>
<input type="text" name="machineID" id="machineID" placeholder="Enter the machine ID" />
</div>
<div class="form-group group-2">
<label for="size">QR Code Height:</label>
<div class="select-wrapper">
<select name="size" id="size">
<option value="15">15 mm</option>
<option value="25" selected>25 mm</option>
<option value="40">40 mm</option>
<option value="80">80 mm</option>
</select>
</div>
<div class="form-group group-2">
<label for="size">QR Code Height:</label>
<div class="select-wrapper">
<select name="size" id="size">
<option value="15">15 mm</option>
<option value="25" selected>25 mm</option>
<option value="40">40 mm</option>
<option value="80">80 mm</option>
</select>
</div>
</div>
<fabaccess-checkbox id="optimize-for-laser-printer" />Optimize Colors for Laser printing</fabaccess-checkbox>`
`
// Define a custom SettingsForm class that extends HTMLElement
class SettingsForm extends HTMLElement {
machineID = ''
size = 25
optimizeForPrint = false
machineID = '' // The machine ID entered by the user
size = 25 // The selected size for the QR code
optimizeForPrint = false // Whether to optimize colors for laser printing
#root = null
#root = null // Private property to store the root node
/**
* Creates an instance of SettingsForm and initializes the component.
*/
constructor() {
super()
super() // Call the parent class constructor
// Attach a shadow DOM tree to this element
this.attachShadow({ mode: 'open' })
// Append the cloned template content to the shadow root
this.shadowRoot.append(template.content.cloneNode(true))
// Get the root node of the shadow DOM
this.#root = this.shadowRoot.getRootNode()
// Initialize properties from attributes
this.machineID = this.getAttribute('machineid') || ''
this.size = parseFloat(this.getAttribute('size') || 25)
this.optimizeForPrint = this.getAttribute('optimizeforprint') == 'true' || this.getAttribute('optimizeforprint') == '1'
// Get form elements from the shadow DOM
const machineIdInput = this.#root.getElementById('machineID')
const optimizeForPrint = this.#root.getElementById('optimize-for-laser-printer')
const size = this.#root.getElementById('size')
// Add event listener for input changes
machineIdInput.addEventListener('keyup', this.#handleInputChange.bind(this), {
passive: false
})
@ -58,13 +73,24 @@ class SettingsForm extends HTMLElement {
passive: false
})
// Set focus to the machine ID input after the element is rendered
setTimeout(() => machineIdInput.focus(), 0)
}
/**
* Observed attributes for the custom element.
* @returns {Array<string>} - The list of attributes to observe.
*/
static get observedAttributes() {
return ['value', 'size']
}
/**
* Called when one of the observed attributes changes.
* @param {string} name - The name of the attribute that changed.
* @param {string} oldValue - The old value of the attribute.
* @param {string} newValue - The new value of the attribute.
*/
attributeChangedCallback(name, oldValue, newValue) {
if (newValue != oldValue) {
switch (name) {
@ -78,11 +104,17 @@ class SettingsForm extends HTMLElement {
}
}
/**
* Handles input changes in the machine ID input field.
* @param {Event} e - The event object.
* @private
*/
#handleInputChange(e) {
e.stopPropagation()
const val = e.target.value
this.machineID = val
// Dispatch a custom 'change' event with the updated values
this.dispatchEvent(
new CustomEvent('change', {
detail: {
@ -95,11 +127,17 @@ class SettingsForm extends HTMLElement {
)
}
/**
* Handles changes in the size select dropdown.
* @param {Event} e - The event object.
* @private
*/
#handleSelectChange(e) {
e.stopPropagation()
const val = e.target.value
this.size = parseInt(val, 10)
// Dispatch a custom 'change' event with the updated values
this.dispatchEvent(
new CustomEvent('change', {
detail: {
@ -112,11 +150,17 @@ class SettingsForm extends HTMLElement {
)
}
/**
* Handles changes in the optimize for print checkbox.
* @param {Event} e - The event object.
* @private
*/
#handleCheckboxChange(e) {
e.stopPropagation()
const val = e.detail.checked
this.optimizeForPrint = val
// Dispatch a custom 'change' event with the updated values
this.dispatchEvent(
new CustomEvent('change', {
detail: {
@ -130,4 +174,5 @@ class SettingsForm extends HTMLElement {
}
}
// Define the custom element 'fabaccess-settings-form' associated with the SettingsForm class
customElements.define('fabaccess-settings-form', SettingsForm)

View File

@ -1,32 +1,55 @@
import ColorUtils from '/src/ColorUtils.mjs'
import ColorUtils from '/src/ColorUtils.mjs' // Import the ColorUtils module
// Define the Utils class providing utility functions
export default class Utils {
/**
* Replaces all occurrences of specified substrings in a string with their corresponding replacements.
* @param {string} d - The original string to perform replacements on.
* @param {Object} r - An object where keys are substrings to replace, and values are their replacements.
* @returns {string} - The modified string with replacements made.
*/
static replace(d, r) {
// Iterate over each key in the replacements object
for (const k of Object.keys(r)) {
// Replace all occurrences of the key with its corresponding value
d = d.replaceAll(k, r[k])
}
return d
return d // Return the modified string
}
/**
* Generates a filename based on the current date and time and the specified file type.
* @param {string} type - The file extension or type (e.g., 'svg', 'png', 'pdf').
* @returns {string} - The generated filename.
*/
static getFilename(type) {
// Get the current date and time in ISO format and remove special characters
const dateTime = this.replace(new Date().toISOString().slice(0, 19), {
':': '',
'-': '',
T: '-'
})
// Determine the filename format based on the file type
switch (type) {
case 'svg':
case 'png':
return `fabaccess-qrcode-${dateTime}.${type}`
return `fabaccess-qrcode-${dateTime}.${type}` // For SVG and PNG files
case 'pdf':
return `fabaccess-qrcodes-${dateTime}.pdf`
return `fabaccess-qrcodes-${dateTime}.pdf` // For PDF files
}
// Default filename format if type doesn't match known cases
return `fabaccess-${dateTime}.${type}`
}
/**
* Retrieves the computed CSS value of a given property.
* @param {string} prop - The CSS property name.
* @returns {string} - The computed CSS value.
*/
static getCssValue(prop) {
// Get the computed style of the root document element for the given property
const cssColorFunction = getComputedStyle(document.documentElement).getPropertyValue(prop)
return cssColorFunction
//return ColorUtils.convertColor(cssColorFunction) //fuuuu Firefox
return cssColorFunction // Return the computed CSS value
//return ColorUtils.convertColor(cssColorFunction) // fuuuu Firefox
}
}