UNPKG

vue3-signature-pad

Version:
382 lines (328 loc) 10.3 kB
import { defineComponent, reactive, ref, computed, watch, onMounted, onBeforeUnmount, toRefs, h } from 'vue'; import OriginalSignaturePad from 'signature_pad'; import mergeImages from 'merge-images'; let ImageTypesEnum; (function (ImageTypesEnum) { ImageTypesEnum["PNG"] = "image/png"; ImageTypesEnum["JPEG"] = "image/jpeg"; ImageTypesEnum["SVG"] = "image/svg+xml"; })(ImageTypesEnum || (ImageTypesEnum = {})); let SaveOutputsEnum; (function (SaveOutputsEnum) { SaveOutputsEnum["FILE"] = "file"; SaveOutputsEnum["DATA_URL"] = "data_url"; })(SaveOutputsEnum || (SaveOutputsEnum = {})); function binaryStringToFile(bstr, filename, mimeType, lastModified = Date.now()) { let n = bstr.length; let u8arr = new Uint8Array(n); while (n--) { u8arr[n] = bstr.charCodeAt(n); } return new File([u8arr], filename, { type: mimeType, lastModified: lastModified }); } function dataURLtoFile(dataUrl, filename, lastModified = Date.now()) { const arr = dataUrl.split(','); const leftSideArr = arr[0]; const parts = leftSideArr.match(/:(.*?);/); if (!parts) { throw new Error('Invalid data url'); } const mimeType = parts[1]; const bstr = atob(arr[1]); return binaryStringToFile(bstr, filename, mimeType, lastModified); } // eslint-disable-next-line const convert2NonReactive = observerValue => JSON.parse(JSON.stringify(observerValue)); const TRANSPARENT_PNG = { x: 0, y: 0, src: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=" }; /* eslint-disable no-unused-vars */ const DEFAULT_OPTIONS = { dotSize: (0.5 + 2.5) / 2, minWidth: 0.5, maxWidth: 2.5, throttle: 16, minDistance: 5, backgroundColor: "rgba(0,0,0,0)", penColor: "black", velocityFilterWeight: 0.7 // onBegin: (event) => {}, // onEnd: (event) => {}, }; var script = /*#__PURE__*/defineComponent({ name: "VueSignaturePad", props: { modelValue: { type: [String, File], required: false }, width: { type: Number, default: 250, validator: function (value) { return 0 <= value && value <= 99999; } }, height: { type: Number, default: 150, validator: function (value) { return 0 <= value && value <= 99999; } }, saveType: { type: String, default: ImageTypesEnum.PNG, validator: function (value) { const allowedValues = Object.values(ImageTypesEnum); if (!allowedValues.includes(value)) { console.warn(`The Image type is incorrect! Supported ones are ${allowedValues.join(", ")}.`); return false; } return true; } }, saveOutput: { type: String, default: SaveOutputsEnum.DATA_URL, validator: function (value) { const allowedValues = Object.values(SaveOutputsEnum); if (!allowedValues.includes(value)) { console.warn(`The save output is incorrect! Supported ones are ${allowedValues.join(", ")}.`); return false; } return true; } }, customStyle: { type: Object, default: () => { return {}; } }, options: { type: Object, default: () => { return DEFAULT_OPTIONS; } }, images: { type: Array, default: () => { return []; } } }, setup(props, context) { // state const state = reactive({ cacheImages: [], onResizeHandler: null, signaturePad: {}, signatureData: TRANSPARENT_PNG }); // ref: signaturePadCanvas const signaturePadCanvas = ref(null); // computed properties const signaturePadCanvasElement = computed(() => { if (!signaturePadCanvas.value) { throw new Error('No canvas could be found with this "ref" in the template'); } return signaturePadCanvas.value; }); const propsImagesAndCustomImages = computed(() => { const nonReactiveProrpImages = Array.from(convert2NonReactive(props.images)); const nonReactiveCachImages = Array.from(convert2NonReactive(state.cacheImages)); return [...nonReactiveProrpImages, ...nonReactiveCachImages]; }); // watch watch(() => props.options, nextOptions => { Object.keys(nextOptions).forEach(option => { if (Object.prototype.hasOwnProperty.call(state.signaturePad, option)) { state.signaturePad[option] = nextOptions[option]; } }); }); // methods function resizeCanvas() { const canvas = signaturePadCanvasElement.value; const data = state.signaturePad.toData(); const ratio = Math.max(window.devicePixelRatio || 1, 1); canvas.width = canvas.offsetWidth * ratio; canvas.height = canvas.offsetHeight * ratio; const context = canvas.getContext("2d"); if (context) { context.scale(ratio, ratio); } state.signaturePad.clear(); state.signatureData = TRANSPARENT_PNG; state.signaturePad.fromData(data); } function saveSignature(type = props.saveType, encoderOptions) { if (state.signaturePad.isEmpty()) { if (props.saveOutput === SaveOutputsEnum.FILE) { return { isEmpty: true, file: null, output: SaveOutputsEnum.FILE }; } else if (props.saveOutput === SaveOutputsEnum.DATA_URL) { return { isEmpty: true, data: null, output: SaveOutputsEnum.DATA_URL }; } else { throw new Error(`This saveOutput ${props.saveOutput} is not supported`); } } const dataURL = state.signaturePad.toDataURL(type, encoderOptions); state.signatureData.src = dataURL; if (props.saveOutput === SaveOutputsEnum.FILE) { return { isEmpty: false, file: dataURLtoFile(dataURL, 'signature'), output: SaveOutputsEnum.FILE }; } else if (props.saveOutput === SaveOutputsEnum.DATA_URL) { return { isEmpty: false, data: dataURL, output: SaveOutputsEnum.DATA_URL }; } else { throw new Error(`This saveOutput ${props.saveOutput} is not supported`); } } function undoSignature() { const record = state.signaturePad.toData(); if (record) { state.signaturePad.fromData(record.slice(0, -1)); } } function completedSignature() { const savedSignature = saveSignature(); let inputData = null; if (savedSignature.output === SaveOutputsEnum.FILE) { inputData = savedSignature.file; } else if (savedSignature.output === SaveOutputsEnum.DATA_URL) { inputData = savedSignature.data; } context.emit("input", inputData); } function mergeImageAndSignature(customSignature) { state.signatureData = customSignature; return mergeImages([...props.images, ...state.cacheImages, state.signatureData]); } function addImages(images = []) { state.cacheImages = [...state.cacheImages, ...images]; return mergeImages([...props.images, ...state.cacheImages, state.signatureData]); } function fromDataURL(dataURL, options = {}, callback) { state.signaturePad.fromDataURL(dataURL, options, callback); } function fromData(data) { state.signaturePad.fromData(data); } function toDataURL(type = ImageTypesEnum.PNG, encoderOptions) { return state.signaturePad.toDataURL(type, encoderOptions); } function toData() { return state.signaturePad.toData(); } function lockSignaturePad() { state.signaturePad.off(); } function openSignaturePad() { state.signaturePad.on(); } function isEmpty() { return state.signaturePad.isEmpty(); } function getPropImagesAndCacheImages() { return propsImagesAndCustomImages.value; } function clearCacheImages() { state.cacheImages = []; return state.cacheImages; } function clearSignature() { state.signaturePad.clear(); context.emit("input", null); } // hooks onMounted(() => { const canvas = signaturePadCanvasElement.value; const signaturePad = new OriginalSignaturePad(canvas, { onEnd: completedSignature, ...props.options }); state.signaturePad = signaturePad; state.onResizeHandler = resizeCanvas; window.addEventListener("resize", state.onResizeHandler, false); resizeCanvas(); }); onBeforeUnmount(() => { if (state.onResizeHandler) { window.removeEventListener("resize", state.onResizeHandler, false); } }); return { // data ...toRefs(state), signaturePadCanvas, // computed properties propsImagesAndCustomImages, // methods resizeCanvas, saveSignature, undoSignature, mergeImageAndSignature, addImages, fromDataURL, toDataURL, fromData, toData, lockSignaturePad, openSignaturePad, isEmpty, getPropImagesAndCacheImages, clearCacheImages, clearSignature }; }, render() { const { width, height, customStyle } = this; const baseStyle = { width: `${width}px`, height: `${height}px` }; return h("div", { style: { ...baseStyle, ...customStyle } }, [h("canvas", { style: { width: "100%", height: "100%" }, ref: "signaturePadCanvas" })]); } }); // Import vue component // Default export is installable instance of component. // IIFE injects install function into component, allowing component // to be registered via Vue.use() as well as Vue.component(), var entry_esm = /*#__PURE__*/(() => { // Assign InstallableComponent type const installable = script; // Attach install function executed by Vue.use() installable.install = app => { app.component('SignaturePad', installable); }; return installable; })(); // It's possible to expose named exports when writing components that can // also be used as directives, etc. - eg. import { RollupDemoDirective } from 'rollup-demo'; // export const RollupDemoDirective = directive; export default entry_esm;