4. 使用JS隔离封装signature_pad签名组件 - densen2014/Blazor100 GitHub Wiki
感谢szimek写的棒棒的signature_pad.js项目, 来源: https://github.com/szimek/signature_pad
+signature_pad
|-signature_pad.umd.js
+signature_pad
|-app.js
代码里 wrapperc.invokeMethodAsync("signatureResult", imgBase64) 为签名canvas结果回调到c#
js代码
import '/lib/signature_pad/signature_pad.umd.js';
export function init(wrapperc, element, alertText,) {
//Code modify from https://github.com/szimek/signature_pad
var wrapper = element;//document.getElementById("signature-pad");
var clearButton = wrapper.querySelector("[data-action=clear]");
var changeColorButton = wrapper.querySelector("[data-action=change-color]");
var undoButton = wrapper.querySelector("[data-action=undo]");
var saveBase64Button = wrapper.querySelector("[data-action=save-base64]");
var savePNGButton = wrapper.querySelector("[data-action=save-png]");
var saveJPGButton = wrapper.querySelector("[data-action=save-jpg]");
var saveSVGButton = wrapper.querySelector("[data-action=save-svg]");
var canvas = wrapper.querySelector("canvas");
var signaturePad = new SignaturePad(canvas, {
// It's Necessary to use an opaque color when saving image as JPEG;
// this option can be omitted if only saving as PNG or SVG
backgroundColor: 'rgb(255, 255, 255)'
});
// Adjust canvas coordinate space taking into account pixel ratio,
// to make it look crisp on mobile devices.
// This also causes canvas to be cleared.
function resizeCanvas() {
// When zoomed out to less than 100%, for some very strange reason,
// some browsers report devicePixelRatio as less than 1
// and only part of the canvas is cleared then.
var ratio = Math.max(window.devicePixelRatio || 1, 1);
// This part causes the canvas to be cleared
canvas.width = canvas.offsetWidth * ratio;
canvas.height = canvas.offsetHeight * ratio;
canvas.getContext("2d").scale(ratio, ratio);
// This library does not listen for canvas changes, so after the canvas is automatically
// cleared by the browser, SignaturePad#isEmpty might still return false, even though the
// canvas looks empty, because the internal data of this library wasn't cleared. To make sure
// that the state of this library is consistent with visual state of the canvas, you
// have to clear it manually.
signaturePad.clear();
}
// On mobile devices it might make more sense to listen to orientation change,
// rather than window resize events.
window.onresize = resizeCanvas;
resizeCanvas();
function download(dataURL, filename) {
if (navigator.userAgent.indexOf("Safari") > -1 && navigator.userAgent.indexOf("Chrome") === -1) {
window.open(dataURL);
} else {
var blob = dataURLToBlob(dataURL);
var url = window.URL.createObjectURL(blob);
var a = document.createElement("a");
a.style = "display: none";
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
}
}
// One could simply use Canvas#toBlob method instead, but it's just to show
// that it can be done using result of SignaturePad#toDataURL.
function dataURLToBlob(dataURL) {
// Code taken from https://github.com/ebidel/filer.js
var parts = dataURL.split(';base64,');
var contentType = parts[0].split(":")[1];
var raw = window.atob(parts[1]);
var rawLength = raw.length;
var uInt8Array = new Uint8Array(rawLength);
for (var i = 0; i < rawLength; ++i) {
uInt8Array[i] = raw.charCodeAt(i);
}
return new Blob([uInt8Array], { type: contentType });
}
if (clearButton) clearButton.addEventListener("click", function (event) {
signaturePad.clear();
return wrapperc.invokeMethodAsync("signatureResult", null);
});
if (undoButton) undoButton.addEventListener("click", function (event) {
var data = signaturePad.toData();
if (data) {
data.pop(); // remove the last dot or line
signaturePad.fromData(data);
}
});
if (changeColorButton) changeColorButton.addEventListener("click", function (event) {
var r = Math.round(Math.random() * 255);
var g = Math.round(Math.random() * 255);
var b = Math.round(Math.random() * 255);
var color = "rgb(" + r + "," + g + "," + b + ")";
signaturePad.penColor = color;
});
if (saveBase64Button) saveBase64Button.addEventListener("click", function (event) {
if (signaturePad.isEmpty()) {
alertMessage();
} else {
var imgBase64 = signaturePad.toDataURL("image/jpeg");
//console.log(imgBase64);
return wrapperc.invokeMethodAsync("signatureResult", imgBase64);
}
});
if (savePNGButton) savePNGButton.addEventListener("click", function (event) {
if (signaturePad.isEmpty()) {
alertMessage();
} else {
var dataURL = signaturePad.toDataURL();
download(dataURL, "signature.png");
}
});
if (saveJPGButton) saveJPGButton.addEventListener("click", function (event) {
if (signaturePad.isEmpty()) {
alertMessage();
} else {
var dataURL = signaturePad.toDataURL("image/jpeg");
download(dataURL, "signature.jpg");
}
});
if (saveSVGButton) saveSVGButton.addEventListener("click", function (event) {
if (signaturePad.isEmpty()) {
alertMessage();
} else {
var dataURL = signaturePad.toDataURL('image/svg+xml');
download(dataURL, "signature.svg");
}
});
function alertMessage() {
if (alertText) alert(alertText);
wrapperc.invokeMethodAsync("signatureAlert");
}
}
css代码
*,
*::before,
*::after {
box-sizing: border-box;
}
.signature-pad-body {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: center;
-ms-flex-pack: center;
justify-content: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
height: 400px;
width: 100%;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
margin: 0;
padding: 32px 16px;
font-family: Helvetica, Sans-Serif;
}
.signature-pad {
position: relative;
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-orient: vertical;
-webkit-box-direction: normal;
-ms-flex-direction: column;
flex-direction: column;
font-size: 10px;
width: 100%;
height: 100%;
max-width: 650px;
max-height: 400px;
border: 1px solid #e8e8e8;
background-color: #fff;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.27), 0 0 40px rgba(0, 0, 0, 0.08) inset;
border-radius: 4px;
padding: 16px;
}
.signature-pad::before,
.signature-pad::after {
position: absolute;
z-index: -1;
content: "";
width: 40%;
height: 10px;
bottom: 10px;
background: transparent;
box-shadow: 0 8px 12px rgba(0, 0, 0, 0.4);
}
.signature-pad::before {
left: 20px;
-webkit-transform: skew(-3deg) rotate(-3deg);
transform: skew(-3deg) rotate(-3deg);
}
.signature-pad::after {
right: 20px;
-webkit-transform: skew(3deg) rotate(3deg);
transform: skew(3deg) rotate(3deg);
}
.signature-pad--body {
position: relative;
-webkit-box-flex: 1;
-ms-flex: 1;
flex: 1;
border: 1px solid #f4f4f4;
}
.signature-pad--body
canvas {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
border-radius: 4px;
box-shadow: 0 0 5px rgba(0, 0, 0, 0.02) inset;
}
.signature-pad--footer {
color: #C3C3C3;
text-align: center;
font-size: 1.2em;
margin-top: 8px;
}
.signature-pad--actions {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: justify;
-ms-flex-pack: justify;
justify-content: space-between;
margin-top: 8px;
}
#github img {
border: 0;
}
@media (max-width: 940px) {
#github img {
width: 90px;
height: 90px;
}
}
在 ASP.NET Web Forms 中,可以使用公共属性将参数和数据传递到控件。 这些属性可以使用特性在标记中进行设置,也可以直接在代码中设置。 Razor 组件以类似的方式工作,尽管组件属性还必须使用 [Parameter] 特性进行标记才能被视为组件参数。
以下 Counter 组件定义名为 IncrementAmount 的组件参数,该参数可用于指定每次单击按钮时 Counter 应该递增的数量。
razor
<h1>Counter</h1>
<p>Current count: @currentCount</p>
<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>
@code {
int currentCount = 0;
[Parameter]
public int IncrementAmount { get; set; } = 1;
void IncrementCount()
{
currentCount+=IncrementAmount;
}
}
若要在 Blazor 中指定组件参数,请像在 ASP.NET Web Forms 中一样使用特性:
razor
<Counter IncrementAmount="10" />
定义名为 SaveBase64BtnTitle 的组件参数,该参数可用于设置或者获取 [保存为base64]按钮的文本。
定义名为 OnResult 的组件参数,该参数可用于手写签名结果回调。
/// <summary>
/// 保存为base64按钮文本/Save as Base64 button title
/// </summary>
[Parameter]
public string SaveBase64BtnTitle { get; set; } = "确定";
/// <summary>
/// 手写签名结果回调/SignaturePad result callback method
/// </summary>
[Parameter]
public EventCallback<string> OnResult { get; set; }
仅获取手写签名结果回调
<SignaturePad OnResult="((e) => Result=e)" />
@code{
public string? Result { get; set; }
}
自定义按钮文本
<SignaturePad OnResult="((e) => Result=e)" SaveBase64BtnTitle="完成"/>
<SignaturePad OnResult="((e) => Result=e)" SaveBase64BtnTitle="OK" ClearBtnTitle="Clear"/>
<SignaturePad OnResult="((e) => Result=e)"
SignAboveLabel="Sign above"
UndoBtnTitle="Undo"
SaveBase64BtnTitle="OK"
ChangeColorBtnTitle="Change color"
ClearBtnTitle="Clear" />
@code{
public string? Result { get; set; }
}
自定义按钮css
<SignaturePad OnResult="((e) => Result=e)" BtnCssClass="btn btn-outline-success"/>
@code{
public string? Result { get; set; }
}
razor代码
@implements IAsyncDisposable
@namespace Blazor100.Components
@inject IJSRuntime JS
<div class="signature-pad-body">
<div @ref="SignaturepadElement" class="signature-pad">
<div class="signature-pad--body">
<canvas width="614" style="touch-action: none; user-select: none;" height="242"></canvas>
</div>
<div class="signature-pad--footer">
<div class="description">@SignAboveLabel</div>
<div class="signature-pad--actions">
<div>
<button type="button" class="@BtnCssClass" data-action="clear">@ClearBtnTitle</button>
@if (EnableChangeColorBtn)
{
<button type="button" class="@BtnCssClass" data-action="change-color">@ChangeColorBtnTitle</button>
}
<button type="button" class="@BtnCssClass" data-action="undo">@UndoBtnTitle</button>
</div>
<div>
@if (EnableSaveBase64Btn)
{
<button type="button" class="@BtnCssClass" data-action="save-base64">@SaveBase64BtnTitle</button>
}
@if (EnableSavePNGBtn)
{
<button type="button" class="@BtnCssClass" data-action="save-png">@SavePNGBtnTitle</button>
}
@if (EnableSaveJPGBtn)
{
<button type="button" class="@BtnCssClass" data-action="save-jpg">@SaveJPGBtnTitle</button>
}
@if (EnableSaveSVGBtn)
{
<button type="button" class="@BtnCssClass" data-action="save-svg">@SaveSVGBtnTitle</button>
}
</div>
</div>
</div>
</div>
</div>
@code {
/// <summary>
/// 手写签名结果回调/SignaturePad result callback method
/// </summary>
[Parameter]
public EventCallback<string> OnResult { get; set; }
/// <summary>
/// 手写签名警告信息回调/SignaturePad alert callback method
/// </summary>
[Parameter]
public EventCallback<string> OnAlert { get; set; }
/// <summary>
/// 获得/设置 错误回调方法
/// </summary>
[Parameter]
public Func<string, Task>? OnError { get; set; }
/// <summary>
/// 在框内签名标签文本/Sign above label
/// </summary>
[Parameter]
public string SignAboveLabel { get; set; } = "在框内签名";
/// <summary>
/// 清除按钮文本/Clear button title
/// </summary>
[Parameter]
public string ClearBtnTitle { get; set; } = "清除";
/// <summary>
/// 请先签名提示文本/'Please provide a signature first' alert text
/// </summary>
[Parameter]
public string SignatureAlertText { get; set; } = "请先签名";
/// <summary>
/// 换颜色按钮文本/Change color button title
/// </summary>
[Parameter]
public string ChangeColorBtnTitle { get; set; } = "换颜色";
/// <summary>
/// 撤消按钮文本/Undo button title
/// </summary>
[Parameter]
public string UndoBtnTitle { get; set; } = "撤消";
/// <summary>
/// 保存为base64按钮文本/Save as Base64 button title
/// </summary>
[Parameter]
public string SaveBase64BtnTitle { get; set; } = "确定";
/// <summary>
/// 保存为PNG按钮文本/Save as PNG button title
/// </summary>
[Parameter]
public string SavePNGBtnTitle { get; set; } = "PNG";
/// <summary>
/// 保存为JPG按钮文本/Save as JPG button title
/// </summary>
[Parameter]
public string SaveJPGBtnTitle { get; set; } = "JPG";
/// <summary>
/// 保存为SVG按钮文本/Save as SVG button title
/// </summary>
[Parameter]
public string SaveSVGBtnTitle { get; set; } = "SVG";
/// <summary>
/// 启用换颜色按钮/Enable change color button
/// </summary>
[Parameter]
public bool EnableChangeColorBtn { get; set; } = true;
/// <summary>
/// 启用JS错误弹窗/Enable Alert from JS
/// </summary>
[Parameter]
public bool EnableAlertJS { get; set; } = true;
/// <summary>
/// 启用保存为base64按钮/Enable save as Base64 button
/// </summary>
[Parameter]
public bool EnableSaveBase64Btn { get; set; } = true;
/// <summary>
/// 启用保存为PNG按钮文本/Enable save as PNG button
/// </summary>
[Parameter]
public bool EnableSavePNGBtn { get; set; } = false;
/// <summary>
/// 启用保存为JPG按钮文本/Enable save as JPG button
/// </summary>
[Parameter]
public bool EnableSaveJPGBtn { get; set; } = false;
/// <summary>
/// 启用保存为SVG按钮文本/Enable save as SVG button
/// </summary>
[Parameter]
public bool EnableSaveSVGBtn { get; set; } = false;
/// <summary>
/// 按钮CSS式样/Button css style
/// </summary>
[Parameter]
public string BtnCssClass { get; set; } = "btn btn-light";
private IJSObjectReference? module;
/// <summary>
///
/// </summary>
protected ElementReference SignaturepadElement { get; set; }
// To prevent making JavaScript interop calls during prerendering
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender) return;
try
{
module = await JS.InvokeAsync<IJSObjectReference>("import", "./lib/signature_pad/app.js");
await module.InvokeVoidAsync("init", DotNetObjectReference.Create(this), SignaturepadElement, EnableAlertJS ? SignatureAlertText : null);
}
catch (Exception e)
{
if (OnError != null) await OnError.Invoke(e.Message);
}
}
[JSInvokable("signatureResult")]
public async Task SignatureResult(string val)
{
if (OnResult.HasDelegate) await OnResult.InvokeAsync(val);
}
[JSInvokable("signatureAlert")]
public async Task SignatureAlert()
{
if (OnResult.HasDelegate) await OnAlert.InvokeAsync(SignatureAlertText);
}
async ValueTask IAsyncDisposable.DisposeAsync()
{
if (module is not null)
{
//await module.InvokeVoidAsync("destroy",null);
await module.DisposeAsync();
}
}
}
SignaturePadPage.razor代码
@page "/signaturepad"
<h3>SignaturePad 签名</h3>
<SignaturePad OnResult="((e) => Result=e)" />
@code{
/// <summary>
/// 签名Base64
/// </summary>
public string? Result { get; set; }
}
@using Blazor100.Components
<div class="nav-item px-3">
<NavLink class="nav-link" href="signaturepad">
<span class="oi oi-plus" aria-hidden="true"></span> 手写签名2
</NavLink>
</div>
builder.Services.AddServerSideBlazor(a =>
{
//异步调用JavaScript函数的最大等待时间
a.JSInteropDefaultCallTimeout = TimeSpan.FromMinutes(2);
}).AddHubOptions(o =>
{
//单个传入集线器消息的最大大小。默认 32 KB
o.MaximumReceiveMessageSize = null;
//可为客户端上载流缓冲的最大项数。 如果达到此限制,则会阻止处理调用,直到服务器处理流项。
o.StreamBufferCapacity = 20;
});