package.dist.index.mjs Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of signature-pad Show documentation
Show all versions of signature-pad Show documentation
Core logic for the signature-pad widget implemented as a state machine
The newest version!
import { createAnatomy } from '@zag-js/anatomy';
import { isLeftClick, isModifierKey, getRelativePoint, trackPointerMove } from '@zag-js/dom-event';
import { createScope, query, getDataUrl, dataAttr, getEventTarget } from '@zag-js/dom-query';
import { createMachine } from '@zag-js/core';
import { createSplitProps, compact } from '@zag-js/utils';
import getStroke from 'perfect-freehand';
import { createProps } from '@zag-js/types';
// src/signature-pad.anatomy.ts
var anatomy = createAnatomy("signature-pad").parts(
"root",
"control",
"segment",
"segmentPath",
"guide",
"clearTrigger",
"label"
);
var parts = anatomy.build();
var dom = createScope({
getRootId: (ctx) => ctx.ids?.root ?? `signature-${ctx.id}`,
getControlId: (ctx) => ctx.ids?.control ?? `signature-control-${ctx.id}`,
getLabelId: (ctx) => ctx.ids?.label ?? `signature-label-${ctx.id}`,
getHiddenInputId: (ctx) => ctx.ids?.hiddenInput ?? `signature-input-${ctx.id}`,
getControlEl: (ctx) => dom.getById(ctx, dom.getControlId(ctx)),
getSegmentEl: (ctx) => query(dom.getControlEl(ctx), "[data-part=segment]"),
getHiddenInputEl: (ctx) => dom.getById(ctx, dom.getHiddenInputId(ctx)),
getDataUrl: (ctx, options) => {
if (ctx.isEmpty) return Promise.resolve("");
return getDataUrl(dom.getSegmentEl(ctx), options);
}
});
// src/signature-pad.connect.ts
var defaultTranslations = {
control: "signature pad",
clearTrigger: "clear signature"
};
function connect(state, send, normalize) {
const drawing = state.matches("drawing");
const empty = state.context.isEmpty;
const interactive = state.context.isInteractive;
const disabled = !!state.context.disabled;
const translations = state.context.translations || defaultTranslations;
return {
empty,
drawing,
currentPath: state.context.currentPath,
paths: state.context.paths,
clear() {
send({ type: "CLEAR" });
},
getDataUrl(type, quality) {
return dom.getDataUrl(state.context, { type, quality });
},
getLabelProps() {
return normalize.label({
...parts.label.attrs,
id: dom.getLabelId(state.context),
"data-disabled": dataAttr(disabled),
htmlFor: dom.getHiddenInputId(state.context),
onClick(event) {
if (!interactive) return;
if (event.defaultPrevented) return;
const controlEl = dom.getControlEl(state.context);
controlEl?.focus({ preventScroll: true });
}
});
},
getRootProps() {
return normalize.element({
...parts.root.attrs,
"data-disabled": dataAttr(disabled),
id: dom.getRootId(state.context)
});
},
getControlProps() {
return normalize.element({
...parts.control.attrs,
tabIndex: disabled ? void 0 : 0,
id: dom.getControlId(state.context),
role: "application",
"aria-roledescription": "signature pad",
"aria-label": translations.control,
"aria-disabled": disabled,
"data-disabled": dataAttr(disabled),
onPointerDown(event) {
if (!isLeftClick(event)) return;
if (isModifierKey(event)) return;
if (!interactive) return;
const target = getEventTarget(event);
if (target?.closest("[data-part=clear-trigger]")) return;
event.currentTarget.setPointerCapture(event.pointerId);
const point = { x: event.clientX, y: event.clientY };
const { offset } = getRelativePoint(point, dom.getControlEl(state.context));
send({ type: "POINTER_DOWN", point: offset, pressure: event.pressure });
},
onPointerUp(event) {
if (!interactive) return;
if (event.currentTarget.hasPointerCapture(event.pointerId)) {
event.currentTarget.releasePointerCapture(event.pointerId);
}
},
style: {
position: "relative",
touchAction: "none",
userSelect: "none",
WebkitUserSelect: "none"
}
});
},
getSegmentProps() {
return normalize.svg({
...parts.segment.attrs,
style: {
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
pointerEvents: "none",
fill: state.context.drawing.fill
}
});
},
getSegmentPathProps(props2) {
return normalize.path({
...parts.segmentPath.attrs,
d: props2.path
});
},
getGuideProps() {
return normalize.element({
...parts.guide.attrs,
"data-disabled": dataAttr(disabled)
});
},
getClearTriggerProps() {
return normalize.button({
...parts.clearTrigger.attrs,
type: "button",
"aria-label": translations.clearTrigger,
hidden: !state.context.paths.length || drawing,
disabled,
onClick() {
send({ type: "CLEAR" });
}
});
},
getHiddenInputProps(props2) {
return normalize.input({
id: dom.getHiddenInputId(state.context),
type: "text",
hidden: true,
disabled,
required: state.context.required,
readOnly: state.context.readOnly,
name: state.context.name,
value: props2.value
});
}
};
}
// src/get-svg-path.ts
var average = (a, b) => (a + b) / 2;
function getSvgPathFromStroke(points, closed = true) {
const len = points.length;
if (len < 4) {
return "";
}
let a = points[0];
let b = points[1];
const c = points[2];
let result = `M${a[0].toFixed(2)},${a[1].toFixed(2)} Q${b[0].toFixed(2)},${b[1].toFixed(2)} ${average(b[0], c[0]).toFixed(2)},${average(
b[1],
c[1]
).toFixed(2)} T`;
for (let i = 2, max = len - 1; i < max; i++) {
a = points[i];
b = points[i + 1];
result += `${average(a[0], b[0]).toFixed(2)},${average(a[1], b[1]).toFixed(2)} `;
}
if (closed) {
result += "Z";
}
return result;
}
// src/signature-pad.machine.ts
function machine(userContext) {
const ctx = compact(userContext);
return createMachine(
{
id: "signature-pad",
initial: "idle",
context: {
readOnly: false,
disabled: false,
...ctx,
paths: [],
currentPoints: [],
currentPath: null,
drawing: {
size: 2,
simulatePressure: false,
thinning: 0.7,
smoothing: 0.4,
streamline: 0.6,
...ctx.drawing
}
},
computed: {
isInteractive: (ctx2) => !(ctx2.disabled || ctx2.readOnly),
isEmpty: (ctx2) => ctx2.paths.length === 0
},
on: {
CLEAR: {
actions: ["clearPoints", "invokeOnDrawEnd", "focusCanvasEl"]
}
},
states: {
idle: {
on: {
POINTER_DOWN: {
target: "drawing",
actions: ["addPoint"]
}
}
},
drawing: {
activities: ["trackPointerMove"],
on: {
POINTER_MOVE: {
actions: ["addPoint", "invokeOnDraw"]
},
POINTER_UP: {
target: "idle",
actions: ["endStroke", "invokeOnDrawEnd"]
}
}
}
}
},
{
activities: {
trackPointerMove(ctx2, _evt, { send }) {
const doc = dom.getDoc(ctx2);
return trackPointerMove(doc, {
onPointerMove({ event, point }) {
const { offset } = getRelativePoint(point, dom.getControlEl(ctx2));
send({ type: "POINTER_MOVE", point: offset, pressure: event.pressure });
},
onPointerUp() {
send({ type: "POINTER_UP" });
}
});
}
},
actions: {
addPoint(ctx2, evt) {
ctx2.currentPoints.push(evt.point);
const stroke = getStroke(ctx2.currentPoints, ctx2.drawing);
ctx2.currentPath = getSvgPathFromStroke(stroke);
},
endStroke(ctx2) {
ctx2.paths.push(ctx2.currentPath);
ctx2.currentPoints = [];
ctx2.currentPath = null;
},
clearPoints(ctx2) {
ctx2.currentPoints = [];
ctx2.paths = [];
},
focusCanvasEl(ctx2) {
queueMicrotask(() => {
dom.getControlEl(ctx2)?.focus({ preventScroll: true });
});
},
invokeOnDraw(ctx2) {
ctx2.onDraw?.({
paths: [...ctx2.paths, ctx2.currentPath]
});
},
invokeOnDrawEnd(ctx2) {
ctx2.onDrawEnd?.({
paths: [...ctx2.paths],
getDataUrl(type, quality = 0.92) {
return dom.getDataUrl(ctx2, { type, quality });
}
});
}
}
}
);
}
var props = createProps()([
"dir",
"disabled",
"drawing",
"getRootNode",
"id",
"ids",
"name",
"onDraw",
"onDrawEnd",
"readOnly",
"required",
"translations"
]);
var splitProps = createSplitProps(props);
export { anatomy, connect, machine, props, splitProps };
© 2015 - 2025 Weber Informatics LLC | Privacy Policy