mirror of https://github.com/docusealco/docuseal
* feat: add typed signature with font picker to user profile - Add 'Type' tab alongside 'Draw' and 'Upload' in signature/initials modals - Create typed-signature-form custom element for canvas-based typed signatures - Persist font preference per user via UserConfig (signature_font/initials_font) - Pre-select saved font when reopening the modal - Add i18n keys for 'type' and 'type_signature_here' in all 7 languages * fix: use options_for_select to satisfy BetterHtml validation --------- Co-authored-by: Bob Develop <developbob50@gmail.com>pull/639/head
parent
7def97ef4a
commit
7029590b32
@ -0,0 +1,137 @@
|
|||||||
|
import { target, targetable } from '@github/catalyst/lib/targetable'
|
||||||
|
import { cropCanvasAndExportToPNG } from '../submission_form/crop_canvas'
|
||||||
|
|
||||||
|
const SIGNATURE_FONTS = {
|
||||||
|
'Dancing Script': 'DancingScript-Regular.otf',
|
||||||
|
'Great Vibes': 'GreatVibes-Regular.ttf',
|
||||||
|
Pacifico: 'Pacifico-Regular.ttf',
|
||||||
|
Caveat: 'Caveat-Regular.ttf',
|
||||||
|
'Homemade Apple': 'HomemadeApple-Regular.ttf',
|
||||||
|
'Mrs Saint Delafield': 'MrsSaintDelafield-Regular.ttf',
|
||||||
|
'Shadows Into Light': 'ShadowsIntoLight-Regular.ttf',
|
||||||
|
'Alex Brush': 'AlexBrush-Regular.ttf',
|
||||||
|
Kalam: 'Kalam-Regular.ttf',
|
||||||
|
Sacramento: 'Sacramento-Regular.ttf',
|
||||||
|
'Herr Von Muellerhoff': 'HerrVonMuellerhoff-Regular.ttf'
|
||||||
|
}
|
||||||
|
|
||||||
|
const fontLoadPromises = {}
|
||||||
|
const scale = 3
|
||||||
|
|
||||||
|
export default targetable(class extends HTMLElement {
|
||||||
|
static [target.static] = ['canvas', 'textInput', 'fontSelect', 'input', 'button', 'fontHidden']
|
||||||
|
|
||||||
|
async connectedCallback () {
|
||||||
|
this.setCanvasSize()
|
||||||
|
|
||||||
|
this.resizeObserver = new ResizeObserver(() => {
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
if (!this.canvas) return
|
||||||
|
|
||||||
|
this.setCanvasSize()
|
||||||
|
this.updateCanvas()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
this.resizeObserver.observe(this.canvas.parentNode)
|
||||||
|
|
||||||
|
this.textInput.addEventListener('input', () => this.updateCanvas())
|
||||||
|
|
||||||
|
this.fontSelect.addEventListener('change', async () => {
|
||||||
|
this.fontHidden.value = this.fontSelect.value
|
||||||
|
await this.loadFont(this.fontSelect.value)
|
||||||
|
this.updateCanvas()
|
||||||
|
})
|
||||||
|
|
||||||
|
this.button.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
if (!this.textInput.value.trim()) return
|
||||||
|
|
||||||
|
this.button.disabled = true
|
||||||
|
this.submit()
|
||||||
|
})
|
||||||
|
|
||||||
|
await this.loadFont(this.fontSelect.value)
|
||||||
|
|
||||||
|
if (this.textInput.value) {
|
||||||
|
this.updateCanvas()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnectedCallback () {
|
||||||
|
if (this.resizeObserver) {
|
||||||
|
this.resizeObserver.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setCanvasSize () {
|
||||||
|
const width = this.canvas.parentNode.clientWidth
|
||||||
|
const height = width / 2.5
|
||||||
|
|
||||||
|
if (this.canvas.width !== width * scale || this.canvas.height !== height * scale) {
|
||||||
|
this.canvas.width = width * scale
|
||||||
|
this.canvas.height = height * scale
|
||||||
|
this.canvas.getContext('2d').scale(scale, scale)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadFont (fontName) {
|
||||||
|
const file = SIGNATURE_FONTS[fontName]
|
||||||
|
if (!file) return Promise.resolve()
|
||||||
|
|
||||||
|
if (!fontLoadPromises[fontName]) {
|
||||||
|
const ext = file.endsWith('.otf') ? 'opentype' : 'truetype'
|
||||||
|
const font = new FontFace(fontName, `url(/fonts/${file}) format("${ext}")`)
|
||||||
|
|
||||||
|
fontLoadPromises[fontName] = font.load().then((loadedFont) => {
|
||||||
|
document.fonts.add(loadedFont)
|
||||||
|
}).catch((error) => {
|
||||||
|
console.error('Font loading failed:', error)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return fontLoadPromises[fontName]
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCanvas () {
|
||||||
|
const context = this.canvas.getContext('2d')
|
||||||
|
const text = this.textInput.value
|
||||||
|
const fontFamily = this.fontSelect.value
|
||||||
|
const initialFontSize = 44
|
||||||
|
|
||||||
|
const setFontSize = (size) => {
|
||||||
|
context.font = `italic ${size}px "${fontFamily}"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const maxWidth = this.canvas.width / scale
|
||||||
|
let size = initialFontSize
|
||||||
|
|
||||||
|
setFontSize(size)
|
||||||
|
|
||||||
|
while (context.measureText(text).width > maxWidth && size > 1) {
|
||||||
|
size -= 1
|
||||||
|
setFontSize(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
context.textAlign = 'center'
|
||||||
|
context.clearRect(0, 0, this.canvas.width / scale, this.canvas.height / scale)
|
||||||
|
context.fillText(text, this.canvas.width / 2 / scale, this.canvas.height / 2 / scale + 11)
|
||||||
|
}
|
||||||
|
|
||||||
|
async submit () {
|
||||||
|
const blob = await cropCanvasAndExportToPNG(this.canvas)
|
||||||
|
const file = new File([blob], 'signature.png', { type: 'image/png' })
|
||||||
|
|
||||||
|
const dataTransfer = new DataTransfer()
|
||||||
|
dataTransfer.items.add(file)
|
||||||
|
|
||||||
|
this.input.files = dataTransfer.files
|
||||||
|
|
||||||
|
if (this.input.webkitEntries.length) {
|
||||||
|
this.input.dataset.file = `${dataTransfer.files[0].name}`
|
||||||
|
}
|
||||||
|
|
||||||
|
this.closest('form').requestSubmit()
|
||||||
|
}
|
||||||
|
})
|
||||||
Loading…
Reference in new issue