vue3-signature-pad
Version:
A Vue3 Signature Pad
382 lines (328 loc) • 10.3 kB
JavaScript
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;