1
0
mirror of https://github.com/invoiceninja/invoiceninja.git synced 2024-11-08 20:22:42 +01:00

New payment flow

This commit is contained in:
David Bomba 2024-06-24 14:38:53 +10:00
parent 5db3eb3d53
commit 3e760e6cc6
25 changed files with 1754 additions and 5 deletions

View File

@ -62,6 +62,7 @@ class InvoiceController extends Controller
$invitation = $invoice->invitations()->where('client_contact_id', auth()->guard('contact')->user()->id)->first();
// @phpstan-ignore-next-line
if ($invitation && auth()->guard('contact') && ! session()->get('is_silent') && ! $invitation->viewed_date) {
$invitation->markViewed();
@ -83,7 +84,7 @@ class InvoiceController extends Controller
return render('invoices.show-fullscreen', $data);
}
return $this->render('invoices.show', $data);
return $this->render('invoices.show_smooth', $data);
}
public function showBlob($hash)

104
app/Livewire/InvoicePay.php Normal file
View File

@ -0,0 +1,104 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Livewire;
use App\Livewire\Terms;
use Livewire\Component;
use App\Utils\HtmlEngine;
use App\Libraries\MultiDB;
use App\Livewire\Signature;
use Livewire\Attributes\On;
use Livewire\Attributes\Computed;
use Livewire\Attributes\Reactive;
class InvoicePay extends Component
{
public $invitation_id;
public $db;
public $settings;
private $invite;
private $variables;
public $terms_accepted = false;
public $signature_accepted = false;
#[On('signature-captured')]
public function signatureCaptured($base64)
{
nlog("signature captured");
$this->signature_accepted = true;
$this->invite = \App\Models\InvoiceInvitation::withTrashed()->find($this->invitation_id)->withoutRelations();
$this->invite->signature_base64 = $base64;
$this->invite->signature_date = now()->addSeconds($this->invite->contact->client->timezone_offset());
$this->invite->save();
}
#[On('terms-accepted')]
public function termsAccepted()
{
nlog("Terms accepted");
$this->invite = \App\Models\InvoiceInvitation::withTrashed()->find($this->invitation_id)->withoutRelations();
$this->terms_accepted =true;
}
#[Computed()]
public function component(): string
{
if(!$this->terms_accepted)
return Terms::class;
if(!$this->signature_accepted)
return Signature::class;
}
#[Computed()]
public function componentUniqueId(): string
{
return "purchase-".md5(time());
}
public function mount()
{
MultiDB::setDb($this->db);
// @phpstan-ignore-next-line
$this->invite = \App\Models\InvoiceInvitation::with('invoice','contact.client','company')->withTrashed()->find($this->invitation_id);
$invoice = $this->invite->invoice;
$company = $this->invite->company;
$contact = $this->invite->contact;
$client = $this->invite->contact->client;
$this->variables = ($this->invite && auth()->guard('contact')->user()->client->getSetting('show_accept_invoice_terms')) ? (new HtmlEngine($this->invite))->generateLabelsAndValues() : false;
$this->settings = $client->getMergedSettings();
}
public function render()
{
return render('components.livewire.invoice-pay', [
'context' => [
'settings' => $this->settings,
'invoice' => $this->invite->invoice,
'variables' => $this->variables,
],
]);
}
}

View File

@ -0,0 +1,25 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Livewire;
use Livewire\Component;
class Signature extends Component
{
public function render()
{
return render('components.livewire.signature');
}
}

35
app/Livewire/Terms.php Normal file
View File

@ -0,0 +1,35 @@
<?php
/**
* Invoice Ninja (https://invoiceninja.com).
*
* @link https://github.com/invoiceninja/invoiceninja source repository
*
* @copyright Copyright (c) 2024. Invoice Ninja LLC (https://invoiceninja.com)
*
* @license https://www.elastic.co/licensing/elastic-license
*/
namespace App\Livewire;
use Livewire\Component;
class Terms extends Component
{
public $invoice;
public $context;
public $variables;
public function mount()
{
$this->invoice = $this->context['invoice'];
$this->variables = $this->context['variables'];
}
public function render()
{
return render('components.livewire.terms');
}
}

13
package-lock.json generated
View File

@ -17,7 +17,8 @@
"lodash": "^4.17.21",
"resolve-url-loader": "^4.0.0",
"sass": "^1.43.4",
"sass-loader": "^12.3.0"
"sass-loader": "^12.3.0",
"signature_pad": "^5.0.2"
},
"devDependencies": {
"@babel/compat-data": "7.15.0",
@ -9599,6 +9600,11 @@
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
},
"node_modules/signature_pad": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/signature_pad/-/signature_pad-5.0.2.tgz",
"integrity": "sha512-FSseAwRWznAQg90CnrTbC570u1QYi8gijZiyboc18SK2IUx7sYVZhNPLnJRCnwhpyOpgdqXf91XAHL4Yg41yCg=="
},
"node_modules/slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
@ -18394,6 +18400,11 @@
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="
},
"signature_pad": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/signature_pad/-/signature_pad-5.0.2.tgz",
"integrity": "sha512-FSseAwRWznAQg90CnrTbC570u1QYi8gijZiyboc18SK2IUx7sYVZhNPLnJRCnwhpyOpgdqXf91XAHL4Yg41yCg=="
},
"slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",

View File

@ -35,7 +35,8 @@
"lodash": "^4.17.21",
"resolve-url-loader": "^4.0.0",
"sass": "^1.43.4",
"sass-loader": "^12.3.0"
"sass-loader": "^12.3.0",
"signature_pad": "^5.0.2"
},
"type": "module"
}

File diff suppressed because one or more lines are too long

View File

@ -240,7 +240,7 @@
"src": "resources/js/setup/setup.js"
},
"resources/sass/app.scss": {
"file": "assets/app-f3b33400.css",
"file": "assets/app-8e387ada.css",
"isEntry": true,
"src": "resources/sass/app.scss"
}

View File

@ -0,0 +1,633 @@
/*!
* Signature Pad v5.0.2 | https://github.com/szimek/signature_pad
* (c) 2024 Szymon Nowak | Released under the MIT license
*/
class Point {
constructor(x, y, pressure, time) {
if (isNaN(x) || isNaN(y)) {
throw new Error(`Point is invalid: (${x}, ${y})`);
}
this.x = +x;
this.y = +y;
this.pressure = pressure || 0;
this.time = time || Date.now();
}
distanceTo(start) {
return Math.sqrt(Math.pow(this.x - start.x, 2) + Math.pow(this.y - start.y, 2));
}
equals(other) {
return (this.x === other.x &&
this.y === other.y &&
this.pressure === other.pressure &&
this.time === other.time);
}
velocityFrom(start) {
return this.time !== start.time
? this.distanceTo(start) / (this.time - start.time)
: 0;
}
}
class Bezier {
static fromPoints(points, widths) {
const c2 = this.calculateControlPoints(points[0], points[1], points[2]).c2;
const c3 = this.calculateControlPoints(points[1], points[2], points[3]).c1;
return new Bezier(points[1], c2, c3, points[2], widths.start, widths.end);
}
static calculateControlPoints(s1, s2, s3) {
const dx1 = s1.x - s2.x;
const dy1 = s1.y - s2.y;
const dx2 = s2.x - s3.x;
const dy2 = s2.y - s3.y;
const m1 = { x: (s1.x + s2.x) / 2.0, y: (s1.y + s2.y) / 2.0 };
const m2 = { x: (s2.x + s3.x) / 2.0, y: (s2.y + s3.y) / 2.0 };
const l1 = Math.sqrt(dx1 * dx1 + dy1 * dy1);
const l2 = Math.sqrt(dx2 * dx2 + dy2 * dy2);
const dxm = m1.x - m2.x;
const dym = m1.y - m2.y;
const k = l2 / (l1 + l2);
const cm = { x: m2.x + dxm * k, y: m2.y + dym * k };
const tx = s2.x - cm.x;
const ty = s2.y - cm.y;
return {
c1: new Point(m1.x + tx, m1.y + ty),
c2: new Point(m2.x + tx, m2.y + ty),
};
}
constructor(startPoint, control2, control1, endPoint, startWidth, endWidth) {
this.startPoint = startPoint;
this.control2 = control2;
this.control1 = control1;
this.endPoint = endPoint;
this.startWidth = startWidth;
this.endWidth = endWidth;
}
length() {
const steps = 10;
let length = 0;
let px;
let py;
for (let i = 0; i <= steps; i += 1) {
const t = i / steps;
const cx = this.point(t, this.startPoint.x, this.control1.x, this.control2.x, this.endPoint.x);
const cy = this.point(t, this.startPoint.y, this.control1.y, this.control2.y, this.endPoint.y);
if (i > 0) {
const xdiff = cx - px;
const ydiff = cy - py;
length += Math.sqrt(xdiff * xdiff + ydiff * ydiff);
}
px = cx;
py = cy;
}
return length;
}
point(t, start, c1, c2, end) {
return (start * (1.0 - t) * (1.0 - t) * (1.0 - t))
+ (3.0 * c1 * (1.0 - t) * (1.0 - t) * t)
+ (3.0 * c2 * (1.0 - t) * t * t)
+ (end * t * t * t);
}
}
class SignatureEventTarget {
constructor() {
try {
this._et = new EventTarget();
}
catch (error) {
this._et = document;
}
}
addEventListener(type, listener, options) {
this._et.addEventListener(type, listener, options);
}
dispatchEvent(event) {
return this._et.dispatchEvent(event);
}
removeEventListener(type, callback, options) {
this._et.removeEventListener(type, callback, options);
}
}
function throttle(fn, wait = 250) {
let previous = 0;
let timeout = null;
let result;
let storedContext;
let storedArgs;
const later = () => {
previous = Date.now();
timeout = null;
result = fn.apply(storedContext, storedArgs);
if (!timeout) {
storedContext = null;
storedArgs = [];
}
};
return function wrapper(...args) {
const now = Date.now();
const remaining = wait - (now - previous);
storedContext = this;
storedArgs = args;
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = fn.apply(storedContext, storedArgs);
if (!timeout) {
storedContext = null;
storedArgs = [];
}
}
else if (!timeout) {
timeout = window.setTimeout(later, remaining);
}
return result;
};
}
class SignaturePad extends SignatureEventTarget {
constructor(canvas, options = {}) {
var _a, _b, _c;
super();
this.canvas = canvas;
this._drawingStroke = false;
this._isEmpty = true;
this._lastPoints = [];
this._data = [];
this._lastVelocity = 0;
this._lastWidth = 0;
this._handleMouseDown = (event) => {
if (!this._isLeftButtonPressed(event, true) || this._drawingStroke) {
return;
}
this._strokeBegin(this._pointerEventToSignatureEvent(event));
};
this._handleMouseMove = (event) => {
if (!this._isLeftButtonPressed(event, true) || !this._drawingStroke) {
this._strokeEnd(this._pointerEventToSignatureEvent(event), false);
return;
}
this._strokeMoveUpdate(this._pointerEventToSignatureEvent(event));
};
this._handleMouseUp = (event) => {
if (this._isLeftButtonPressed(event)) {
return;
}
this._strokeEnd(this._pointerEventToSignatureEvent(event));
};
this._handleTouchStart = (event) => {
if (event.targetTouches.length !== 1 || this._drawingStroke) {
return;
}
if (event.cancelable) {
event.preventDefault();
}
this._strokeBegin(this._touchEventToSignatureEvent(event));
};
this._handleTouchMove = (event) => {
if (event.targetTouches.length !== 1) {
return;
}
if (event.cancelable) {
event.preventDefault();
}
if (!this._drawingStroke) {
this._strokeEnd(this._touchEventToSignatureEvent(event), false);
return;
}
this._strokeMoveUpdate(this._touchEventToSignatureEvent(event));
};
this._handleTouchEnd = (event) => {
if (event.targetTouches.length !== 0) {
return;
}
if (event.cancelable) {
event.preventDefault();
}
this.canvas.removeEventListener('touchmove', this._handleTouchMove);
this._strokeEnd(this._touchEventToSignatureEvent(event));
};
this._handlePointerDown = (event) => {
if (!this._isLeftButtonPressed(event) || this._drawingStroke) {
return;
}
event.preventDefault();
this._strokeBegin(this._pointerEventToSignatureEvent(event));
};
this._handlePointerMove = (event) => {
if (!this._isLeftButtonPressed(event, true) || !this._drawingStroke) {
this._strokeEnd(this._pointerEventToSignatureEvent(event), false);
return;
}
event.preventDefault();
this._strokeMoveUpdate(this._pointerEventToSignatureEvent(event));
};
this._handlePointerUp = (event) => {
if (this._isLeftButtonPressed(event)) {
return;
}
event.preventDefault();
this._strokeEnd(this._pointerEventToSignatureEvent(event));
};
this.velocityFilterWeight = options.velocityFilterWeight || 0.7;
this.minWidth = options.minWidth || 0.5;
this.maxWidth = options.maxWidth || 2.5;
this.throttle = (_a = options.throttle) !== null && _a !== void 0 ? _a : 16;
this.minDistance = (_b = options.minDistance) !== null && _b !== void 0 ? _b : 5;
this.dotSize = options.dotSize || 0;
this.penColor = options.penColor || 'black';
this.backgroundColor = options.backgroundColor || 'rgba(0,0,0,0)';
this.compositeOperation = options.compositeOperation || 'source-over';
this.canvasContextOptions = (_c = options.canvasContextOptions) !== null && _c !== void 0 ? _c : {};
this._strokeMoveUpdate = this.throttle
? throttle(SignaturePad.prototype._strokeUpdate, this.throttle)
: SignaturePad.prototype._strokeUpdate;
this._ctx = canvas.getContext('2d', this.canvasContextOptions);
this.clear();
this.on();
}
clear() {
const { _ctx: ctx, canvas } = this;
ctx.fillStyle = this.backgroundColor;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillRect(0, 0, canvas.width, canvas.height);
this._data = [];
this._reset(this._getPointGroupOptions());
this._isEmpty = true;
}
fromDataURL(dataUrl, options = {}) {
return new Promise((resolve, reject) => {
const image = new Image();
const ratio = options.ratio || window.devicePixelRatio || 1;
const width = options.width || this.canvas.width / ratio;
const height = options.height || this.canvas.height / ratio;
const xOffset = options.xOffset || 0;
const yOffset = options.yOffset || 0;
this._reset(this._getPointGroupOptions());
image.onload = () => {
this._ctx.drawImage(image, xOffset, yOffset, width, height);
resolve();
};
image.onerror = (error) => {
reject(error);
};
image.crossOrigin = 'anonymous';
image.src = dataUrl;
this._isEmpty = false;
});
}
toDataURL(type = 'image/png', encoderOptions) {
switch (type) {
case 'image/svg+xml':
if (typeof encoderOptions !== 'object') {
encoderOptions = undefined;
}
return `data:image/svg+xml;base64,${btoa(this.toSVG(encoderOptions))}`;
default:
if (typeof encoderOptions !== 'number') {
encoderOptions = undefined;
}
return this.canvas.toDataURL(type, encoderOptions);
}
}
on() {
this.canvas.style.touchAction = 'none';
this.canvas.style.msTouchAction = 'none';
this.canvas.style.userSelect = 'none';
const isIOS = /Macintosh/.test(navigator.userAgent) && 'ontouchstart' in document;
if (window.PointerEvent && !isIOS) {
this._handlePointerEvents();
}
else {
this._handleMouseEvents();
if ('ontouchstart' in window) {
this._handleTouchEvents();
}
}
}
off() {
this.canvas.style.touchAction = 'auto';
this.canvas.style.msTouchAction = 'auto';
this.canvas.style.userSelect = 'auto';
this.canvas.removeEventListener('pointerdown', this._handlePointerDown);
this.canvas.removeEventListener('mousedown', this._handleMouseDown);
this.canvas.removeEventListener('touchstart', this._handleTouchStart);
this._removeMoveUpEventListeners();
}
_getListenerFunctions() {
var _a;
const canvasWindow = window.document === this.canvas.ownerDocument
? window
: (_a = this.canvas.ownerDocument.defaultView) !== null && _a !== void 0 ? _a : this.canvas.ownerDocument;
return {
addEventListener: canvasWindow.addEventListener.bind(canvasWindow),
removeEventListener: canvasWindow.removeEventListener.bind(canvasWindow),
};
}
_removeMoveUpEventListeners() {
const { removeEventListener } = this._getListenerFunctions();
removeEventListener('pointermove', this._handlePointerMove);
removeEventListener('pointerup', this._handlePointerUp);
removeEventListener('mousemove', this._handleMouseMove);
removeEventListener('mouseup', this._handleMouseUp);
removeEventListener('touchmove', this._handleTouchMove);
removeEventListener('touchend', this._handleTouchEnd);
}
isEmpty() {
return this._isEmpty;
}
fromData(pointGroups, { clear = true } = {}) {
if (clear) {
this.clear();
}
this._fromData(pointGroups, this._drawCurve.bind(this), this._drawDot.bind(this));
this._data = this._data.concat(pointGroups);
}
toData() {
return this._data;
}
_isLeftButtonPressed(event, only) {
if (only) {
return event.buttons === 1;
}
return (event.buttons & 1) === 1;
}
_pointerEventToSignatureEvent(event) {
return {
event: event,
type: event.type,
x: event.clientX,
y: event.clientY,
pressure: 'pressure' in event ? event.pressure : 0,
};
}
_touchEventToSignatureEvent(event) {
const touch = event.changedTouches[0];
return {
event: event,
type: event.type,
x: touch.clientX,
y: touch.clientY,
pressure: touch.force,
};
}
_getPointGroupOptions(group) {
return {
penColor: group && 'penColor' in group ? group.penColor : this.penColor,
dotSize: group && 'dotSize' in group ? group.dotSize : this.dotSize,
minWidth: group && 'minWidth' in group ? group.minWidth : this.minWidth,
maxWidth: group && 'maxWidth' in group ? group.maxWidth : this.maxWidth,
velocityFilterWeight: group && 'velocityFilterWeight' in group
? group.velocityFilterWeight
: this.velocityFilterWeight,
compositeOperation: group && 'compositeOperation' in group
? group.compositeOperation
: this.compositeOperation,
};
}
_strokeBegin(event) {
const cancelled = !this.dispatchEvent(new CustomEvent('beginStroke', { detail: event, cancelable: true }));
if (cancelled) {
return;
}
const { addEventListener } = this._getListenerFunctions();
switch (event.event.type) {
case 'mousedown':
addEventListener('mousemove', this._handleMouseMove);
addEventListener('mouseup', this._handleMouseUp);
break;
case 'touchstart':
addEventListener('touchmove', this._handleTouchMove);
addEventListener('touchend', this._handleTouchEnd);
break;
case 'pointerdown':
addEventListener('pointermove', this._handlePointerMove);
addEventListener('pointerup', this._handlePointerUp);
break;
}
this._drawingStroke = true;
const pointGroupOptions = this._getPointGroupOptions();
const newPointGroup = Object.assign(Object.assign({}, pointGroupOptions), { points: [] });
this._data.push(newPointGroup);
this._reset(pointGroupOptions);
this._strokeUpdate(event);
}
_strokeUpdate(event) {
if (!this._drawingStroke) {
return;
}
if (this._data.length === 0) {
this._strokeBegin(event);
return;
}
this.dispatchEvent(new CustomEvent('beforeUpdateStroke', { detail: event }));
const point = this._createPoint(event.x, event.y, event.pressure);
const lastPointGroup = this._data[this._data.length - 1];
const lastPoints = lastPointGroup.points;
const lastPoint = lastPoints.length > 0 && lastPoints[lastPoints.length - 1];
const isLastPointTooClose = lastPoint
? point.distanceTo(lastPoint) <= this.minDistance
: false;
const pointGroupOptions = this._getPointGroupOptions(lastPointGroup);
if (!lastPoint || !(lastPoint && isLastPointTooClose)) {
const curve = this._addPoint(point, pointGroupOptions);
if (!lastPoint) {
this._drawDot(point, pointGroupOptions);
}
else if (curve) {
this._drawCurve(curve, pointGroupOptions);
}
lastPoints.push({
time: point.time,
x: point.x,
y: point.y,
pressure: point.pressure,
});
}
this.dispatchEvent(new CustomEvent('afterUpdateStroke', { detail: event }));
}
_strokeEnd(event, shouldUpdate = true) {
this._removeMoveUpEventListeners();
if (!this._drawingStroke) {
return;
}
if (shouldUpdate) {
this._strokeUpdate(event);
}
this._drawingStroke = false;
this.dispatchEvent(new CustomEvent('endStroke', { detail: event }));
}
_handlePointerEvents() {
this._drawingStroke = false;
this.canvas.addEventListener('pointerdown', this._handlePointerDown);
}
_handleMouseEvents() {
this._drawingStroke = false;
this.canvas.addEventListener('mousedown', this._handleMouseDown);
}
_handleTouchEvents() {
this.canvas.addEventListener('touchstart', this._handleTouchStart);
}
_reset(options) {
this._lastPoints = [];
this._lastVelocity = 0;
this._lastWidth = (options.minWidth + options.maxWidth) / 2;
this._ctx.fillStyle = options.penColor;
this._ctx.globalCompositeOperation = options.compositeOperation;
}
_createPoint(x, y, pressure) {
const rect = this.canvas.getBoundingClientRect();
return new Point(x - rect.left, y - rect.top, pressure, new Date().getTime());
}
_addPoint(point, options) {
const { _lastPoints } = this;
_lastPoints.push(point);
if (_lastPoints.length > 2) {
if (_lastPoints.length === 3) {
_lastPoints.unshift(_lastPoints[0]);
}
const widths = this._calculateCurveWidths(_lastPoints[1], _lastPoints[2], options);
const curve = Bezier.fromPoints(_lastPoints, widths);
_lastPoints.shift();
return curve;
}
return null;
}
_calculateCurveWidths(startPoint, endPoint, options) {
const velocity = options.velocityFilterWeight * endPoint.velocityFrom(startPoint) +
(1 - options.velocityFilterWeight) * this._lastVelocity;
const newWidth = this._strokeWidth(velocity, options);
const widths = {
end: newWidth,
start: this._lastWidth,
};
this._lastVelocity = velocity;
this._lastWidth = newWidth;
return widths;
}
_strokeWidth(velocity, options) {
return Math.max(options.maxWidth / (velocity + 1), options.minWidth);
}
_drawCurveSegment(x, y, width) {
const ctx = this._ctx;
ctx.moveTo(x, y);
ctx.arc(x, y, width, 0, 2 * Math.PI, false);
this._isEmpty = false;
}
_drawCurve(curve, options) {
const ctx = this._ctx;
const widthDelta = curve.endWidth - curve.startWidth;
const drawSteps = Math.ceil(curve.length()) * 2;
ctx.beginPath();
ctx.fillStyle = options.penColor;
for (let i = 0; i < drawSteps; i += 1) {
const t = i / drawSteps;
const tt = t * t;
const ttt = tt * t;
const u = 1 - t;
const uu = u * u;
const uuu = uu * u;
let x = uuu * curve.startPoint.x;
x += 3 * uu * t * curve.control1.x;
x += 3 * u * tt * curve.control2.x;
x += ttt * curve.endPoint.x;
let y = uuu * curve.startPoint.y;
y += 3 * uu * t * curve.control1.y;
y += 3 * u * tt * curve.control2.y;
y += ttt * curve.endPoint.y;
const width = Math.min(curve.startWidth + ttt * widthDelta, options.maxWidth);
this._drawCurveSegment(x, y, width);
}
ctx.closePath();
ctx.fill();
}
_drawDot(point, options) {
const ctx = this._ctx;
const width = options.dotSize > 0
? options.dotSize
: (options.minWidth + options.maxWidth) / 2;
ctx.beginPath();
this._drawCurveSegment(point.x, point.y, width);
ctx.closePath();
ctx.fillStyle = options.penColor;
ctx.fill();
}
_fromData(pointGroups, drawCurve, drawDot) {
for (const group of pointGroups) {
const { points } = group;
const pointGroupOptions = this._getPointGroupOptions(group);
if (points.length > 1) {
for (let j = 0; j < points.length; j += 1) {
const basicPoint = points[j];
const point = new Point(basicPoint.x, basicPoint.y, basicPoint.pressure, basicPoint.time);
if (j === 0) {
this._reset(pointGroupOptions);
}
const curve = this._addPoint(point, pointGroupOptions);
if (curve) {
drawCurve(curve, pointGroupOptions);
}
}
}
else {
this._reset(pointGroupOptions);
drawDot(points[0], pointGroupOptions);
}
}
}
toSVG({ includeBackgroundColor = false } = {}) {
const pointGroups = this._data;
const ratio = Math.max(window.devicePixelRatio || 1, 1);
const minX = 0;
const minY = 0;
const maxX = this.canvas.width / ratio;
const maxY = this.canvas.height / ratio;
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
svg.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
svg.setAttribute('viewBox', `${minX} ${minY} ${maxX} ${maxY}`);
svg.setAttribute('width', maxX.toString());
svg.setAttribute('height', maxY.toString());
if (includeBackgroundColor && this.backgroundColor) {
const rect = document.createElement('rect');
rect.setAttribute('width', '100%');
rect.setAttribute('height', '100%');
rect.setAttribute('fill', this.backgroundColor);
svg.appendChild(rect);
}
this._fromData(pointGroups, (curve, { penColor }) => {
const path = document.createElement('path');
if (!isNaN(curve.control1.x) &&
!isNaN(curve.control1.y) &&
!isNaN(curve.control2.x) &&
!isNaN(curve.control2.y)) {
const attr = `M ${curve.startPoint.x.toFixed(3)},${curve.startPoint.y.toFixed(3)} ` +
`C ${curve.control1.x.toFixed(3)},${curve.control1.y.toFixed(3)} ` +
`${curve.control2.x.toFixed(3)},${curve.control2.y.toFixed(3)} ` +
`${curve.endPoint.x.toFixed(3)},${curve.endPoint.y.toFixed(3)}`;
path.setAttribute('d', attr);
path.setAttribute('stroke-width', (curve.endWidth * 2.25).toFixed(3));
path.setAttribute('stroke', penColor);
path.setAttribute('fill', 'none');
path.setAttribute('stroke-linecap', 'round');
svg.appendChild(path);
}
}, (point, { penColor, dotSize, minWidth, maxWidth }) => {
const circle = document.createElement('circle');
const size = dotSize > 0 ? dotSize : (minWidth + maxWidth) / 2;
circle.setAttribute('r', size.toString());
circle.setAttribute('cx', point.x.toString());
circle.setAttribute('cy', point.y.toString());
circle.setAttribute('fill', penColor);
svg.appendChild(circle);
});
return svg.outerHTML;
}
}
export { SignaturePad as default };
//# sourceMappingURL=signature_pad.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,641 @@
/*!
* Signature Pad v5.0.2 | https://github.com/szimek/signature_pad
* (c) 2024 Szymon Nowak | Released under the MIT license
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.SignaturePad = factory());
})(this, (function () { 'use strict';
class Point {
constructor(x, y, pressure, time) {
if (isNaN(x) || isNaN(y)) {
throw new Error(`Point is invalid: (${x}, ${y})`);
}
this.x = +x;
this.y = +y;
this.pressure = pressure || 0;
this.time = time || Date.now();
}
distanceTo(start) {
return Math.sqrt(Math.pow(this.x - start.x, 2) + Math.pow(this.y - start.y, 2));
}
equals(other) {
return (this.x === other.x &&
this.y === other.y &&
this.pressure === other.pressure &&
this.time === other.time);
}
velocityFrom(start) {
return this.time !== start.time
? this.distanceTo(start) / (this.time - start.time)
: 0;
}
}
class Bezier {
static fromPoints(points, widths) {
const c2 = this.calculateControlPoints(points[0], points[1], points[2]).c2;
const c3 = this.calculateControlPoints(points[1], points[2], points[3]).c1;
return new Bezier(points[1], c2, c3, points[2], widths.start, widths.end);
}
static calculateControlPoints(s1, s2, s3) {
const dx1 = s1.x - s2.x;
const dy1 = s1.y - s2.y;
const dx2 = s2.x - s3.x;
const dy2 = s2.y - s3.y;
const m1 = { x: (s1.x + s2.x) / 2.0, y: (s1.y + s2.y) / 2.0 };
const m2 = { x: (s2.x + s3.x) / 2.0, y: (s2.y + s3.y) / 2.0 };
const l1 = Math.sqrt(dx1 * dx1 + dy1 * dy1);
const l2 = Math.sqrt(dx2 * dx2 + dy2 * dy2);
const dxm = m1.x - m2.x;
const dym = m1.y - m2.y;
const k = l2 / (l1 + l2);
const cm = { x: m2.x + dxm * k, y: m2.y + dym * k };
const tx = s2.x - cm.x;
const ty = s2.y - cm.y;
return {
c1: new Point(m1.x + tx, m1.y + ty),
c2: new Point(m2.x + tx, m2.y + ty),
};
}
constructor(startPoint, control2, control1, endPoint, startWidth, endWidth) {
this.startPoint = startPoint;
this.control2 = control2;
this.control1 = control1;
this.endPoint = endPoint;
this.startWidth = startWidth;
this.endWidth = endWidth;
}
length() {
const steps = 10;
let length = 0;
let px;
let py;
for (let i = 0; i <= steps; i += 1) {
const t = i / steps;
const cx = this.point(t, this.startPoint.x, this.control1.x, this.control2.x, this.endPoint.x);
const cy = this.point(t, this.startPoint.y, this.control1.y, this.control2.y, this.endPoint.y);
if (i > 0) {
const xdiff = cx - px;
const ydiff = cy - py;
length += Math.sqrt(xdiff * xdiff + ydiff * ydiff);
}
px = cx;
py = cy;
}
return length;
}
point(t, start, c1, c2, end) {
return (start * (1.0 - t) * (1.0 - t) * (1.0 - t))
+ (3.0 * c1 * (1.0 - t) * (1.0 - t) * t)
+ (3.0 * c2 * (1.0 - t) * t * t)
+ (end * t * t * t);
}
}
class SignatureEventTarget {
constructor() {
try {
this._et = new EventTarget();
}
catch (error) {
this._et = document;
}
}
addEventListener(type, listener, options) {
this._et.addEventListener(type, listener, options);
}
dispatchEvent(event) {
return this._et.dispatchEvent(event);
}
removeEventListener(type, callback, options) {
this._et.removeEventListener(type, callback, options);
}
}
function throttle(fn, wait = 250) {
let previous = 0;
let timeout = null;
let result;
let storedContext;
let storedArgs;
const later = () => {
previous = Date.now();
timeout = null;
result = fn.apply(storedContext, storedArgs);
if (!timeout) {
storedContext = null;
storedArgs = [];
}
};
return function wrapper(...args) {
const now = Date.now();
const remaining = wait - (now - previous);
storedContext = this;
storedArgs = args;
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = fn.apply(storedContext, storedArgs);
if (!timeout) {
storedContext = null;
storedArgs = [];
}
}
else if (!timeout) {
timeout = window.setTimeout(later, remaining);
}
return result;
};
}
class SignaturePad extends SignatureEventTarget {
constructor(canvas, options = {}) {
var _a, _b, _c;
super();
this.canvas = canvas;
this._drawingStroke = false;
this._isEmpty = true;
this._lastPoints = [];
this._data = [];
this._lastVelocity = 0;
this._lastWidth = 0;
this._handleMouseDown = (event) => {
if (!this._isLeftButtonPressed(event, true) || this._drawingStroke) {
return;
}
this._strokeBegin(this._pointerEventToSignatureEvent(event));
};
this._handleMouseMove = (event) => {
if (!this._isLeftButtonPressed(event, true) || !this._drawingStroke) {
this._strokeEnd(this._pointerEventToSignatureEvent(event), false);
return;
}
this._strokeMoveUpdate(this._pointerEventToSignatureEvent(event));
};
this._handleMouseUp = (event) => {
if (this._isLeftButtonPressed(event)) {
return;
}
this._strokeEnd(this._pointerEventToSignatureEvent(event));
};
this._handleTouchStart = (event) => {
if (event.targetTouches.length !== 1 || this._drawingStroke) {
return;
}
if (event.cancelable) {
event.preventDefault();
}
this._strokeBegin(this._touchEventToSignatureEvent(event));
};
this._handleTouchMove = (event) => {
if (event.targetTouches.length !== 1) {
return;
}
if (event.cancelable) {
event.preventDefault();
}
if (!this._drawingStroke) {
this._strokeEnd(this._touchEventToSignatureEvent(event), false);
return;
}
this._strokeMoveUpdate(this._touchEventToSignatureEvent(event));
};
this._handleTouchEnd = (event) => {
if (event.targetTouches.length !== 0) {
return;
}
if (event.cancelable) {
event.preventDefault();
}
this.canvas.removeEventListener('touchmove', this._handleTouchMove);
this._strokeEnd(this._touchEventToSignatureEvent(event));
};
this._handlePointerDown = (event) => {
if (!this._isLeftButtonPressed(event) || this._drawingStroke) {
return;
}
event.preventDefault();
this._strokeBegin(this._pointerEventToSignatureEvent(event));
};
this._handlePointerMove = (event) => {
if (!this._isLeftButtonPressed(event, true) || !this._drawingStroke) {
this._strokeEnd(this._pointerEventToSignatureEvent(event), false);
return;
}
event.preventDefault();
this._strokeMoveUpdate(this._pointerEventToSignatureEvent(event));
};
this._handlePointerUp = (event) => {
if (this._isLeftButtonPressed(event)) {
return;
}
event.preventDefault();
this._strokeEnd(this._pointerEventToSignatureEvent(event));
};
this.velocityFilterWeight = options.velocityFilterWeight || 0.7;
this.minWidth = options.minWidth || 0.5;
this.maxWidth = options.maxWidth || 2.5;
this.throttle = (_a = options.throttle) !== null && _a !== void 0 ? _a : 16;
this.minDistance = (_b = options.minDistance) !== null && _b !== void 0 ? _b : 5;
this.dotSize = options.dotSize || 0;
this.penColor = options.penColor || 'black';
this.backgroundColor = options.backgroundColor || 'rgba(0,0,0,0)';
this.compositeOperation = options.compositeOperation || 'source-over';
this.canvasContextOptions = (_c = options.canvasContextOptions) !== null && _c !== void 0 ? _c : {};
this._strokeMoveUpdate = this.throttle
? throttle(SignaturePad.prototype._strokeUpdate, this.throttle)
: SignaturePad.prototype._strokeUpdate;
this._ctx = canvas.getContext('2d', this.canvasContextOptions);
this.clear();
this.on();
}
clear() {
const { _ctx: ctx, canvas } = this;
ctx.fillStyle = this.backgroundColor;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillRect(0, 0, canvas.width, canvas.height);
this._data = [];
this._reset(this._getPointGroupOptions());
this._isEmpty = true;
}
fromDataURL(dataUrl, options = {}) {
return new Promise((resolve, reject) => {
const image = new Image();
const ratio = options.ratio || window.devicePixelRatio || 1;
const width = options.width || this.canvas.width / ratio;
const height = options.height || this.canvas.height / ratio;
const xOffset = options.xOffset || 0;
const yOffset = options.yOffset || 0;
this._reset(this._getPointGroupOptions());
image.onload = () => {
this._ctx.drawImage(image, xOffset, yOffset, width, height);
resolve();
};
image.onerror = (error) => {
reject(error);
};
image.crossOrigin = 'anonymous';
image.src = dataUrl;
this._isEmpty = false;
});
}
toDataURL(type = 'image/png', encoderOptions) {
switch (type) {
case 'image/svg+xml':
if (typeof encoderOptions !== 'object') {
encoderOptions = undefined;
}
return `data:image/svg+xml;base64,${btoa(this.toSVG(encoderOptions))}`;
default:
if (typeof encoderOptions !== 'number') {
encoderOptions = undefined;
}
return this.canvas.toDataURL(type, encoderOptions);
}
}
on() {
this.canvas.style.touchAction = 'none';
this.canvas.style.msTouchAction = 'none';
this.canvas.style.userSelect = 'none';
const isIOS = /Macintosh/.test(navigator.userAgent) && 'ontouchstart' in document;
if (window.PointerEvent && !isIOS) {
this._handlePointerEvents();
}
else {
this._handleMouseEvents();
if ('ontouchstart' in window) {
this._handleTouchEvents();
}
}
}
off() {
this.canvas.style.touchAction = 'auto';
this.canvas.style.msTouchAction = 'auto';
this.canvas.style.userSelect = 'auto';
this.canvas.removeEventListener('pointerdown', this._handlePointerDown);
this.canvas.removeEventListener('mousedown', this._handleMouseDown);
this.canvas.removeEventListener('touchstart', this._handleTouchStart);
this._removeMoveUpEventListeners();
}
_getListenerFunctions() {
var _a;
const canvasWindow = window.document === this.canvas.ownerDocument
? window
: (_a = this.canvas.ownerDocument.defaultView) !== null && _a !== void 0 ? _a : this.canvas.ownerDocument;
return {
addEventListener: canvasWindow.addEventListener.bind(canvasWindow),
removeEventListener: canvasWindow.removeEventListener.bind(canvasWindow),
};
}
_removeMoveUpEventListeners() {
const { removeEventListener } = this._getListenerFunctions();
removeEventListener('pointermove', this._handlePointerMove);
removeEventListener('pointerup', this._handlePointerUp);
removeEventListener('mousemove', this._handleMouseMove);
removeEventListener('mouseup', this._handleMouseUp);
removeEventListener('touchmove', this._handleTouchMove);
removeEventListener('touchend', this._handleTouchEnd);
}
isEmpty() {
return this._isEmpty;
}
fromData(pointGroups, { clear = true } = {}) {
if (clear) {
this.clear();
}
this._fromData(pointGroups, this._drawCurve.bind(this), this._drawDot.bind(this));
this._data = this._data.concat(pointGroups);
}
toData() {
return this._data;
}
_isLeftButtonPressed(event, only) {
if (only) {
return event.buttons === 1;
}
return (event.buttons & 1) === 1;
}
_pointerEventToSignatureEvent(event) {
return {
event: event,
type: event.type,
x: event.clientX,
y: event.clientY,
pressure: 'pressure' in event ? event.pressure : 0,
};
}
_touchEventToSignatureEvent(event) {
const touch = event.changedTouches[0];
return {
event: event,
type: event.type,
x: touch.clientX,
y: touch.clientY,
pressure: touch.force,
};
}
_getPointGroupOptions(group) {
return {
penColor: group && 'penColor' in group ? group.penColor : this.penColor,
dotSize: group && 'dotSize' in group ? group.dotSize : this.dotSize,
minWidth: group && 'minWidth' in group ? group.minWidth : this.minWidth,
maxWidth: group && 'maxWidth' in group ? group.maxWidth : this.maxWidth,
velocityFilterWeight: group && 'velocityFilterWeight' in group
? group.velocityFilterWeight
: this.velocityFilterWeight,
compositeOperation: group && 'compositeOperation' in group
? group.compositeOperation
: this.compositeOperation,
};
}
_strokeBegin(event) {
const cancelled = !this.dispatchEvent(new CustomEvent('beginStroke', { detail: event, cancelable: true }));
if (cancelled) {
return;
}
const { addEventListener } = this._getListenerFunctions();
switch (event.event.type) {
case 'mousedown':
addEventListener('mousemove', this._handleMouseMove);
addEventListener('mouseup', this._handleMouseUp);
break;
case 'touchstart':
addEventListener('touchmove', this._handleTouchMove);
addEventListener('touchend', this._handleTouchEnd);
break;
case 'pointerdown':
addEventListener('pointermove', this._handlePointerMove);
addEventListener('pointerup', this._handlePointerUp);
break;
}
this._drawingStroke = true;
const pointGroupOptions = this._getPointGroupOptions();
const newPointGroup = Object.assign(Object.assign({}, pointGroupOptions), { points: [] });
this._data.push(newPointGroup);
this._reset(pointGroupOptions);
this._strokeUpdate(event);
}
_strokeUpdate(event) {
if (!this._drawingStroke) {
return;
}
if (this._data.length === 0) {
this._strokeBegin(event);
return;
}
this.dispatchEvent(new CustomEvent('beforeUpdateStroke', { detail: event }));
const point = this._createPoint(event.x, event.y, event.pressure);
const lastPointGroup = this._data[this._data.length - 1];
const lastPoints = lastPointGroup.points;
const lastPoint = lastPoints.length > 0 && lastPoints[lastPoints.length - 1];
const isLastPointTooClose = lastPoint
? point.distanceTo(lastPoint) <= this.minDistance
: false;
const pointGroupOptions = this._getPointGroupOptions(lastPointGroup);
if (!lastPoint || !(lastPoint && isLastPointTooClose)) {
const curve = this._addPoint(point, pointGroupOptions);
if (!lastPoint) {
this._drawDot(point, pointGroupOptions);
}
else if (curve) {
this._drawCurve(curve, pointGroupOptions);
}
lastPoints.push({
time: point.time,
x: point.x,
y: point.y,
pressure: point.pressure,
});
}
this.dispatchEvent(new CustomEvent('afterUpdateStroke', { detail: event }));
}
_strokeEnd(event, shouldUpdate = true) {
this._removeMoveUpEventListeners();
if (!this._drawingStroke) {
return;
}
if (shouldUpdate) {
this._strokeUpdate(event);
}
this._drawingStroke = false;
this.dispatchEvent(new CustomEvent('endStroke', { detail: event }));
}
_handlePointerEvents() {
this._drawingStroke = false;
this.canvas.addEventListener('pointerdown', this._handlePointerDown);
}
_handleMouseEvents() {
this._drawingStroke = false;
this.canvas.addEventListener('mousedown', this._handleMouseDown);
}
_handleTouchEvents() {
this.canvas.addEventListener('touchstart', this._handleTouchStart);
}
_reset(options) {
this._lastPoints = [];
this._lastVelocity = 0;
this._lastWidth = (options.minWidth + options.maxWidth) / 2;
this._ctx.fillStyle = options.penColor;
this._ctx.globalCompositeOperation = options.compositeOperation;
}
_createPoint(x, y, pressure) {
const rect = this.canvas.getBoundingClientRect();
return new Point(x - rect.left, y - rect.top, pressure, new Date().getTime());
}
_addPoint(point, options) {
const { _lastPoints } = this;
_lastPoints.push(point);
if (_lastPoints.length > 2) {
if (_lastPoints.length === 3) {
_lastPoints.unshift(_lastPoints[0]);
}
const widths = this._calculateCurveWidths(_lastPoints[1], _lastPoints[2], options);
const curve = Bezier.fromPoints(_lastPoints, widths);
_lastPoints.shift();
return curve;
}
return null;
}
_calculateCurveWidths(startPoint, endPoint, options) {
const velocity = options.velocityFilterWeight * endPoint.velocityFrom(startPoint) +
(1 - options.velocityFilterWeight) * this._lastVelocity;
const newWidth = this._strokeWidth(velocity, options);
const widths = {
end: newWidth,
start: this._lastWidth,
};
this._lastVelocity = velocity;
this._lastWidth = newWidth;
return widths;
}
_strokeWidth(velocity, options) {
return Math.max(options.maxWidth / (velocity + 1), options.minWidth);
}
_drawCurveSegment(x, y, width) {
const ctx = this._ctx;
ctx.moveTo(x, y);
ctx.arc(x, y, width, 0, 2 * Math.PI, false);
this._isEmpty = false;
}
_drawCurve(curve, options) {
const ctx = this._ctx;
const widthDelta = curve.endWidth - curve.startWidth;
const drawSteps = Math.ceil(curve.length()) * 2;
ctx.beginPath();
ctx.fillStyle = options.penColor;
for (let i = 0; i < drawSteps; i += 1) {
const t = i / drawSteps;
const tt = t * t;
const ttt = tt * t;
const u = 1 - t;
const uu = u * u;
const uuu = uu * u;
let x = uuu * curve.startPoint.x;
x += 3 * uu * t * curve.control1.x;
x += 3 * u * tt * curve.control2.x;
x += ttt * curve.endPoint.x;
let y = uuu * curve.startPoint.y;
y += 3 * uu * t * curve.control1.y;
y += 3 * u * tt * curve.control2.y;
y += ttt * curve.endPoint.y;
const width = Math.min(curve.startWidth + ttt * widthDelta, options.maxWidth);
this._drawCurveSegment(x, y, width);
}
ctx.closePath();
ctx.fill();
}
_drawDot(point, options) {
const ctx = this._ctx;
const width = options.dotSize > 0
? options.dotSize
: (options.minWidth + options.maxWidth) / 2;
ctx.beginPath();
this._drawCurveSegment(point.x, point.y, width);
ctx.closePath();
ctx.fillStyle = options.penColor;
ctx.fill();
}
_fromData(pointGroups, drawCurve, drawDot) {
for (const group of pointGroups) {
const { points } = group;
const pointGroupOptions = this._getPointGroupOptions(group);
if (points.length > 1) {
for (let j = 0; j < points.length; j += 1) {
const basicPoint = points[j];
const point = new Point(basicPoint.x, basicPoint.y, basicPoint.pressure, basicPoint.time);
if (j === 0) {
this._reset(pointGroupOptions);
}
const curve = this._addPoint(point, pointGroupOptions);
if (curve) {
drawCurve(curve, pointGroupOptions);
}
}
}
else {
this._reset(pointGroupOptions);
drawDot(points[0], pointGroupOptions);
}
}
}
toSVG({ includeBackgroundColor = false } = {}) {
const pointGroups = this._data;
const ratio = Math.max(window.devicePixelRatio || 1, 1);
const minX = 0;
const minY = 0;
const maxX = this.canvas.width / ratio;
const maxY = this.canvas.height / ratio;
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
svg.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
svg.setAttribute('viewBox', `${minX} ${minY} ${maxX} ${maxY}`);
svg.setAttribute('width', maxX.toString());
svg.setAttribute('height', maxY.toString());
if (includeBackgroundColor && this.backgroundColor) {
const rect = document.createElement('rect');
rect.setAttribute('width', '100%');
rect.setAttribute('height', '100%');
rect.setAttribute('fill', this.backgroundColor);
svg.appendChild(rect);
}
this._fromData(pointGroups, (curve, { penColor }) => {
const path = document.createElement('path');
if (!isNaN(curve.control1.x) &&
!isNaN(curve.control1.y) &&
!isNaN(curve.control2.x) &&
!isNaN(curve.control2.y)) {
const attr = `M ${curve.startPoint.x.toFixed(3)},${curve.startPoint.y.toFixed(3)} ` +
`C ${curve.control1.x.toFixed(3)},${curve.control1.y.toFixed(3)} ` +
`${curve.control2.x.toFixed(3)},${curve.control2.y.toFixed(3)} ` +
`${curve.endPoint.x.toFixed(3)},${curve.endPoint.y.toFixed(3)}`;
path.setAttribute('d', attr);
path.setAttribute('stroke-width', (curve.endWidth * 2.25).toFixed(3));
path.setAttribute('stroke', penColor);
path.setAttribute('fill', 'none');
path.setAttribute('stroke-linecap', 'round');
svg.appendChild(path);
}
}, (point, { penColor, dotSize, minWidth, maxWidth }) => {
const circle = document.createElement('circle');
const size = dotSize > 0 ? dotSize : (minWidth + maxWidth) / 2;
circle.setAttribute('r', size.toString());
circle.setAttribute('cx', point.x.toString());
circle.setAttribute('cy', point.y.toString());
circle.setAttribute('fill', penColor);
svg.appendChild(circle);
});
return svg.outerHTML;
}
}
return SignaturePad;
}));
//# sourceMappingURL=signature_pad.umd.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,17 @@
import { BasicPoint, Point } from './point';
export declare class Bezier {
startPoint: Point;
control2: BasicPoint;
control1: BasicPoint;
endPoint: Point;
startWidth: number;
endWidth: number;
static fromPoints(points: Point[], widths: {
start: number;
end: number;
}): Bezier;
private static calculateControlPoints;
constructor(startPoint: Point, control2: BasicPoint, control1: BasicPoint, endPoint: Point, startWidth: number, endWidth: number);
length(): number;
private point;
}

View File

@ -0,0 +1,16 @@
export interface BasicPoint {
x: number;
y: number;
pressure: number;
time: number;
}
export declare class Point implements BasicPoint {
x: number;
y: number;
pressure: number;
time: number;
constructor(x: number, y: number, pressure?: number, time?: number);
distanceTo(start: BasicPoint): number;
equals(other: BasicPoint): boolean;
velocityFrom(start: BasicPoint): number;
}

View File

@ -0,0 +1,7 @@
export declare class SignatureEventTarget {
private _et;
constructor();
addEventListener(type: string, listener: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions): void;
dispatchEvent(event: Event): boolean;
removeEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: boolean | EventListenerOptions): void;
}

View File

@ -0,0 +1,115 @@
/**
* The main idea and some parts of the code (e.g. drawing variable width Bézier curve) are taken from:
* http://corner.squareup.com/2012/07/smoother-signatures.html
*
* Implementation of interpolation using cubic Bézier curves is taken from:
* https://web.archive.org/web/20160323213433/http://www.benknowscode.com/2012/09/path-interpolation-using-cubic-bezier_9742.html
*
* Algorithm for approximated length of a Bézier curve is taken from:
* http://www.lemoda.net/maths/bezier-length/index.html
*/
import { BasicPoint } from './point';
import { SignatureEventTarget } from './signature_event_target';
export interface SignatureEvent {
event: MouseEvent | TouchEvent | PointerEvent;
type: string;
x: number;
y: number;
pressure: number;
}
export interface FromDataOptions {
clear?: boolean;
}
export interface ToSVGOptions {
includeBackgroundColor?: boolean;
}
export interface PointGroupOptions {
dotSize: number;
minWidth: number;
maxWidth: number;
penColor: string;
velocityFilterWeight: number;
/**
* This is the globalCompositeOperation for the line.
* *default: 'source-over'*
* @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation
*/
compositeOperation: GlobalCompositeOperation;
}
export interface Options extends Partial<PointGroupOptions> {
minDistance?: number;
backgroundColor?: string;
throttle?: number;
canvasContextOptions?: CanvasRenderingContext2DSettings;
}
export interface PointGroup extends PointGroupOptions {
points: BasicPoint[];
}
export default class SignaturePad extends SignatureEventTarget {
private canvas;
dotSize: number;
minWidth: number;
maxWidth: number;
penColor: string;
minDistance: number;
velocityFilterWeight: number;
compositeOperation: GlobalCompositeOperation;
backgroundColor: string;
throttle: number;
canvasContextOptions: CanvasRenderingContext2DSettings;
private _ctx;
private _drawingStroke;
private _isEmpty;
private _lastPoints;
private _data;
private _lastVelocity;
private _lastWidth;
private _strokeMoveUpdate;
constructor(canvas: HTMLCanvasElement, options?: Options);
clear(): void;
fromDataURL(dataUrl: string, options?: {
ratio?: number;
width?: number;
height?: number;
xOffset?: number;
yOffset?: number;
}): Promise<void>;
toDataURL(type: 'image/svg+xml', encoderOptions?: ToSVGOptions): string;
toDataURL(type?: string, encoderOptions?: number): string;
on(): void;
off(): void;
private _getListenerFunctions;
private _removeMoveUpEventListeners;
isEmpty(): boolean;
fromData(pointGroups: PointGroup[], { clear }?: FromDataOptions): void;
toData(): PointGroup[];
_isLeftButtonPressed(event: MouseEvent, only?: boolean): boolean;
private _pointerEventToSignatureEvent;
private _touchEventToSignatureEvent;
private _handleMouseDown;
private _handleMouseMove;
private _handleMouseUp;
private _handleTouchStart;
private _handleTouchMove;
private _handleTouchEnd;
private _handlePointerDown;
private _handlePointerMove;
private _handlePointerUp;
private _getPointGroupOptions;
private _strokeBegin;
private _strokeUpdate;
private _strokeEnd;
private _handlePointerEvents;
private _handleMouseEvents;
private _handleTouchEvents;
private _reset;
private _createPoint;
private _addPoint;
private _calculateCurveWidths;
private _strokeWidth;
private _drawCurveSegment;
private _drawCurve;
private _drawDot;
private _fromData;
toSVG({ includeBackgroundColor }?: ToSVGOptions): string;
}

View File

@ -0,0 +1 @@
export declare function throttle(fn: (...args: any[]) => any, wait?: number): (this: any, ...args: any[]) => any;

View File

@ -0,0 +1,3 @@
<div>
@livewire($this->component,['context' => $context], key($this->componentUniqueId()))
</div>

View File

@ -0,0 +1,49 @@
<div style="w-full">
<div class="flex flex-col items-center justify-center min-h-screen bg-gray-100 p-6">
<div class="bg-white p-6 rounded-lg shadow-lg w-full max-w-md">
<h2 class="text-xl text-center mx-auto py-4 px-4">{{ ctrans('texts.sign_here_ux_tip') }}</h2>
<canvas id="signature-pad" class="border border-gray-300 w-full h-64"></canvas>
<div class="flex justify-between px-4 py-4">
<button id="clear-signature" class="px-4 bg-red-500 text-white rounded">{{ ctrans('texts.clear') }}</button>
<button id="save-button" class="button button-primary bg-primary hover:bg-primary-darken inline-flex items-center">{{ ctrans('texts.next') }}</button>
</div>
</div>
</div>
@assets
<script src="{{ asset('vendor/signature_pad@5/signature_pad.umd.min.js') }}"></script>
@endassets
@script
<script>
const canvas = document.getElementById('signature-pad');
const signaturePad = new SignaturePad(canvas);
// Resize canvas to fit the parent container
function resizeCanvas() {
const ratio = Math.max(window.devicePixelRatio || 1, 1);
canvas.width = canvas.offsetWidth * ratio;
canvas.height = canvas.offsetHeight * ratio;
canvas.getContext("2d").scale(ratio, ratio);
}
window.addEventListener('resize', resizeCanvas);
resizeCanvas();
document.getElementById('save-button').addEventListener('click', function() {
if (!signaturePad.isEmpty()) {
$wire.dispatch('signature-captured', {base64: signaturePad.toDataURL()});
} else {
alert('Please provide a signature first.');
}
});
document.getElementById('clear-signature').addEventListener('click', function() {
signaturePad.clear();
});
</script>
@endscript
</div>

View File

@ -0,0 +1,40 @@
<div>
<div class="bg-color: white">
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-xl leading-6 font-medium text-gray-900">
{{ ctrans('texts.terms') }}
</h3>
<div class="mt-4 h-64 overflow-y-auto">
<div class="mb-4">
<p class="text-sm leading-6 font-medium text-gray-500">{{ ctrans('texts.invoice') }} {{ $invoice->number }}:</p>
@if($variables && $invoice->terms)
<h5 data-ref="entity-terms">{!! $invoice->parseHtmlVariables('terms', $variables) !!}</h5>
@elseif($invoice->terms)
<h5 data-ref="entity-terms" class="text-sm leading-5 text-gray-900">{!! $invoice->terms !!}</h5>
@else
<i class="text-sm leading-5 text-gray-500">{{ ctrans('texts.not_specified') }}</i>
@endif
</div>
</div>
</div>
</div>
<div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<div class="flex w-full rounded-md shadow-sm sm:ml-3 sm:w-auto" x-data>
<button id="accept-terms-button" class="button button-primary bg-primary hover:bg-primary-darken inline-flex items-center">{{ ctrans('texts.next') }}</button>
</div>
</div>
@script
<script>
document.addEventListener('DOMContentLoaded', function () {
document.getElementById('accept-terms-button').addEventListener('click', function() {
$wire.dispatch('terms-accepted');
});
});
</script>
@endscript
</div>

View File

@ -0,0 +1,34 @@
@extends('portal.ninja2020.layout.app')
@section('meta_title', ctrans('texts.view_invoice'))
@push('head')
@endpush
@section('body')
@if($invoice->isPayable() && $client->getSetting('custom_message_unpaid_invoice'))
@component('portal.ninja2020.components.message')
<pre>{{ $client->getSetting('custom_message_unpaid_invoice') }}</pre>
@endcomponent
@elseif($invoice->status_id === 4 && $client->getSetting('custom_message_paid_invoice'))
@component('portal.ninja2020.components.message')
<pre>{{ $client->getSetting('custom_message_paid_invoice') }}</pre>
@endcomponent
@endif
@if($invoice->isPayable())
@livewire('invoice-pay', ['invitation_id' => $invitation->id, 'db' => $invoice->company->db])
@endif
@include('portal.ninja2020.components.entity-documents', ['entity' => $invoice])
@endsection
@section('footer')
<!-- @include('portal.ninja2020.invoices.includes.required-fields') -->
<!-- @include('portal.ninja2020.invoices.includes.signature') -->
<!-- @include('portal.ninja2020.invoices.includes.terms', ['entities' => [$invoice], 'variables' => $variables, 'entity_type' => ctrans('texts.invoice')]) -->
@endsection
@push('head')
@endpush