diff --git a/app/javascript/draw.js b/app/javascript/draw.js index a4936e8da0..a33d7ea80a 100644 --- a/app/javascript/draw.js +++ b/app/javascript/draw.js @@ -4,12 +4,7 @@ import { isValidSignatureCanvas } from './submission_form/validate_signature' window.customElements.define('draw-signature', class extends HTMLElement { connectedCallback () { - const scale = 3 - - this.canvas.width = this.canvas.parentNode.clientWidth * scale - this.canvas.height = this.canvas.parentNode.clientHeight * scale - - this.canvas.getContext('2d').scale(scale, scale) + this.scale = 3 this.pad = new SignaturePad(this.canvas) @@ -50,6 +45,104 @@ window.customElements.define('draw-signature', class extends HTMLElement { this.submitButton.disabled = false }) }) + + this.setupCanvasSizing() + } + + disconnectedCallback () { + this.teardownCanvasSizing() + } + + setupCanvasSizing () { + this.resizeCanvas() + + this.onResizeCanvas = () => { + if (this.resizeRaf) { + cancelAnimationFrame(this.resizeRaf) + } + + this.resizeRaf = requestAnimationFrame(() => { + this.resizeRaf = requestAnimationFrame(() => { + this.resizeCanvas() + }) + }) + } + + window.addEventListener('resize', this.onResizeCanvas) + screen?.orientation?.addEventListener('change', this.onResizeCanvas) + + if (typeof ResizeObserver !== 'undefined' && this.canvas?.parentNode) { + this.resizeObserver = new ResizeObserver(this.onResizeCanvas) + this.resizeObserver.observe(this.canvas.parentNode) + } + } + + teardownCanvasSizing () { + if (this.resizeRaf) { + cancelAnimationFrame(this.resizeRaf) + this.resizeRaf = null + } + + if (this.onResizeCanvas) { + window.removeEventListener('resize', this.onResizeCanvas) + screen?.orientation?.removeEventListener('change', this.onResizeCanvas) + } + + this.resizeObserver?.disconnect() + } + + resizeCanvas () { + if (!this.canvas?.parentNode) { + return + } + + const width = this.canvas.parentNode.clientWidth + const height = this.canvas.parentNode.clientHeight + + if (!width || !height) { + return + } + + const nextW = width * this.scale + const nextH = height * this.scale + + if (this.canvas.width === nextW && this.canvas.height === nextH) { + return + } + + const prevCssW = this.canvas.width / this.scale + const prevCssH = this.canvas.height / this.scale + const ratioX = prevCssW > 0 ? width / prevCssW : 1 + const ratioY = prevCssH > 0 ? height / prevCssH : 1 + + let data = [] + + if (this.pad) { + data = this.pad.toData() + + if (data.length && (ratioX !== 1 || ratioY !== 1)) { + data = data.map((group) => ({ + ...group, + points: group.points.map((point) => ({ + ...point, + x: point.x * ratioX, + y: point.y * ratioY + })) + })) + } + } + + this.canvas.width = nextW + this.canvas.height = nextH + this.canvas.getContext('2d').scale(this.scale, this.scale) + + if (this.pad) { + this.pad.clear() + + if (data.length) { + this.pad.fromData(data) + } + } } clearSignaturePad () { diff --git a/app/javascript/elements/signature_form.js b/app/javascript/elements/signature_form.js index 55020081a9..f0efad8564 100644 --- a/app/javascript/elements/signature_form.js +++ b/app/javascript/elements/signature_form.js @@ -5,12 +5,7 @@ export default targetable(class extends HTMLElement { static [target.static] = ['canvas', 'input', 'clear', 'button'] async connectedCallback () { - const scale = 3 - - this.canvas.width = this.canvas.parentNode.clientWidth * scale - this.canvas.height = this.canvas.parentNode.clientHeight * scale - - this.canvas.getContext('2d').scale(scale, scale) + this.scale = 3 const { default: SignaturePad } = await import('signature_pad') @@ -29,6 +24,104 @@ export default targetable(class extends HTMLElement { this.submit() }) + + this.setupCanvasSizing() + } + + disconnectedCallback () { + this.teardownCanvasSizing() + } + + setupCanvasSizing () { + this.resizeCanvas() + + this.onResizeCanvas = () => { + if (this.resizeRaf) { + cancelAnimationFrame(this.resizeRaf) + } + + this.resizeRaf = requestAnimationFrame(() => { + this.resizeRaf = requestAnimationFrame(() => { + this.resizeCanvas() + }) + }) + } + + window.addEventListener('resize', this.onResizeCanvas) + screen?.orientation?.addEventListener('change', this.onResizeCanvas) + + if (typeof ResizeObserver !== 'undefined' && this.canvas?.parentNode) { + this.resizeObserver = new ResizeObserver(this.onResizeCanvas) + this.resizeObserver.observe(this.canvas.parentNode) + } + } + + teardownCanvasSizing () { + if (this.resizeRaf) { + cancelAnimationFrame(this.resizeRaf) + this.resizeRaf = null + } + + if (this.onResizeCanvas) { + window.removeEventListener('resize', this.onResizeCanvas) + screen?.orientation?.removeEventListener('change', this.onResizeCanvas) + } + + this.resizeObserver?.disconnect() + } + + resizeCanvas () { + if (!this.canvas?.parentNode) { + return + } + + const width = this.canvas.parentNode.clientWidth + const height = this.canvas.parentNode.clientHeight + + if (!width || !height) { + return + } + + const nextW = width * this.scale + const nextH = height * this.scale + + if (this.canvas.width === nextW && this.canvas.height === nextH) { + return + } + + const prevCssW = this.canvas.width / this.scale + const prevCssH = this.canvas.height / this.scale + const ratioX = prevCssW > 0 ? width / prevCssW : 1 + const ratioY = prevCssH > 0 ? height / prevCssH : 1 + + let data = [] + + if (this.pad) { + data = this.pad.toData() + + if (data.length && (ratioX !== 1 || ratioY !== 1)) { + data = data.map((group) => ({ + ...group, + points: group.points.map((point) => ({ + ...point, + x: point.x * ratioX, + y: point.y * ratioY + })) + })) + } + } + + this.canvas.width = nextW + this.canvas.height = nextH + this.canvas.getContext('2d').scale(this.scale, this.scale) + + if (this.pad) { + this.pad.clear() + + if (data.length) { + this.pad.fromData(data) + } + } } async submit () { diff --git a/app/javascript/submission_form/initials_step.vue b/app/javascript/submission_form/initials_step.vue index 4bc61cd454..5190329fe7 100644 --- a/app/javascript/submission_form/initials_step.vue +++ b/app/javascript/submission_form/initials_step.vue @@ -218,15 +218,6 @@ export default { } }, async mounted () { - this.$nextTick(() => { - if (this.$refs.canvas) { - this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale - this.$refs.canvas.height = (this.$refs.canvas.parentNode.clientWidth / 4.5) * scale - - this.$refs.canvas.getContext('2d').scale(scale, scale) - } - }) - if (this.$refs.canvas) { this.pad = new SignaturePad(this.$refs.canvas) @@ -240,26 +231,118 @@ export default { this.$emit('start') }) - this.intersectionObserver = new IntersectionObserver((entries, observer) => { - entries.forEach(entry => { - if (entry.isIntersecting) { - this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale - this.$refs.canvas.height = (this.$refs.canvas.parentNode.clientWidth / 4.5) * scale + this.setupCanvasSizing() + } + }, + beforeUnmount () { + this.teardownCanvasSizing() + }, + methods: { + canvasHeightForWidth (width) { + return (width / 4.5) * scale + }, + setupCanvasSizing () { + this.resizeCanvas() + + this.onResizeCanvas = () => { + if (this.resizeRaf) { + cancelAnimationFrame(this.resizeRaf) + } - this.$refs.canvas.getContext('2d').scale(scale, scale) + // Double rAF: orientationchange often fires before layout has settled. + this.resizeRaf = requestAnimationFrame(() => { + this.resizeRaf = requestAnimationFrame(() => { + this.resizeCanvas() + }) + }) + } - this.intersectionObserver?.disconnect() + window.addEventListener('resize', this.onResizeCanvas) + screen?.orientation?.addEventListener('change', this.onResizeCanvas) + + if (typeof ResizeObserver !== 'undefined' && this.$refs.canvas?.parentNode) { + this.resizeObserver = new ResizeObserver(this.onResizeCanvas) + this.resizeObserver.observe(this.$refs.canvas.parentNode) + } + + this.intersectionObserver = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + this.resizeCanvas() } }) }) this.intersectionObserver.observe(this.$refs.canvas) - } - }, - beforeUnmount () { - this.intersectionObserver?.disconnect() - }, - methods: { + }, + teardownCanvasSizing () { + if (this.resizeRaf) { + cancelAnimationFrame(this.resizeRaf) + this.resizeRaf = null + } + + if (this.onResizeCanvas) { + window.removeEventListener('resize', this.onResizeCanvas) + screen?.orientation?.removeEventListener('change', this.onResizeCanvas) + } + + this.resizeObserver?.disconnect() + this.intersectionObserver?.disconnect() + }, + resizeCanvas () { + const canvas = this.$refs.canvas + + if (!canvas?.parentNode) { + return + } + + const width = canvas.parentNode.clientWidth + + if (!width) { + return + } + + const nextW = width * scale + const nextH = this.canvasHeightForWidth(width) + + if (canvas.width === nextW && canvas.height === nextH) { + return + } + + const prevCssW = canvas.width / scale + const ratio = prevCssW > 0 ? width / prevCssW : 1 + + let data = [] + + if (this.pad) { + data = this.pad.toData() + + if (data.length && ratio !== 1) { + data = data.map((group) => ({ + ...group, + points: group.points.map((point) => ({ + ...point, + x: point.x * ratio, + y: point.y * ratio + })) + })) + } + } + + canvas.width = nextW + canvas.height = nextH + canvas.getContext('2d').scale(scale, scale) + + if (this.pad) { + this.pad.clear() + + if (data.length) { + this.pad.fromData(data) + } else if (!this.isDrawInitials && this.$refs.textInput?.value) { + this.updateWrittenInitials({ target: this.$refs.textInput }) + } + } + }, drawOnCanvas: SignatureStep.methods.drawOnCanvas, drawImage (event) { this.remove() diff --git a/app/javascript/submission_form/signature_step.vue b/app/javascript/submission_form/signature_step.vue index d85c8c8594..efbfafe7cb 100644 --- a/app/javascript/submission_form/signature_step.vue +++ b/app/javascript/submission_form/signature_step.vue @@ -428,15 +428,6 @@ export default { } }, async mounted () { - this.$nextTick(() => { - if (this.$refs.canvas) { - this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale - this.$refs.canvas.height = this.$refs.canvas.parentNode.clientWidth * scale / 3 - - this.$refs.canvas.getContext('2d').scale(scale, scale) - } - }) - if (this.$refs.canvas) { this.pad = new SignaturePad(this.$refs.canvas) @@ -450,27 +441,119 @@ export default { this.$emit('start') }) - this.intersectionObserver = new IntersectionObserver((entries, observer) => { - entries.forEach(entry => { - if (entry.isIntersecting) { - this.$refs.canvas.width = this.$refs.canvas.parentNode.clientWidth * scale - this.$refs.canvas.height = this.$refs.canvas.parentNode.clientWidth * scale / 3 + this.setupCanvasSizing() + } + }, + beforeUnmount () { + this.teardownCanvasSizing() + this.stopCheckSignature() + }, + methods: { + canvasHeightForWidth (width) { + return width * scale / 3 + }, + setupCanvasSizing () { + this.resizeCanvas() - this.$refs.canvas.getContext('2d').scale(scale, scale) + this.onResizeCanvas = () => { + if (this.resizeRaf) { + cancelAnimationFrame(this.resizeRaf) + } - this.intersectionObserver?.disconnect() + // Double rAF: orientationchange often fires before layout has settled. + this.resizeRaf = requestAnimationFrame(() => { + this.resizeRaf = requestAnimationFrame(() => { + this.resizeCanvas() + }) + }) + } + + window.addEventListener('resize', this.onResizeCanvas) + screen?.orientation?.addEventListener('change', this.onResizeCanvas) + + if (typeof ResizeObserver !== 'undefined' && this.$refs.canvas?.parentNode) { + this.resizeObserver = new ResizeObserver(this.onResizeCanvas) + this.resizeObserver.observe(this.$refs.canvas.parentNode) + } + + this.intersectionObserver = new IntersectionObserver((entries) => { + entries.forEach(entry => { + if (entry.isIntersecting) { + this.resizeCanvas() } }) }) this.intersectionObserver.observe(this.$refs.canvas) - } - }, - beforeUnmount () { - this.intersectionObserver?.disconnect() - this.stopCheckSignature() - }, - methods: { + }, + teardownCanvasSizing () { + if (this.resizeRaf) { + cancelAnimationFrame(this.resizeRaf) + this.resizeRaf = null + } + + if (this.onResizeCanvas) { + window.removeEventListener('resize', this.onResizeCanvas) + screen?.orientation?.removeEventListener('change', this.onResizeCanvas) + } + + this.resizeObserver?.disconnect() + this.intersectionObserver?.disconnect() + }, + resizeCanvas () { + const canvas = this.$refs.canvas + + if (!canvas?.parentNode) { + return + } + + const width = canvas.parentNode.clientWidth + + if (!width) { + return + } + + const nextW = width * scale + const nextH = this.canvasHeightForWidth(width) + + if (canvas.width === nextW && canvas.height === nextH) { + return + } + + const prevCssW = canvas.width / scale + const ratio = prevCssW > 0 ? width / prevCssW : 1 + + let data = [] + + if (this.pad) { + data = this.pad.toData() + + if (data.length && ratio !== 1) { + data = data.map((group) => ({ + ...group, + points: group.points.map((point) => ({ + ...point, + x: point.x * ratio, + y: point.y * ratio + })) + })) + } + } + + canvas.width = nextW + canvas.height = nextH + canvas.getContext('2d').scale(scale, scale) + + if (this.pad) { + this.pad.clear() + + if (data.length) { + this.pad.fromData(data) + } else if (this.isTextSignature && this.$refs.textInput?.value) { + this.updateWrittenSignature({ target: this.$refs.textInput }) + } + } + }, remove () { this.$emit('update:model-value', '')