npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@servicetitan/dte-pdf-editor

v2.3.0

Published

A React component library for creating interactive PDF editors with drag-and-drop field placement, data model integration, e-signature support, fillable form fields, **job form (submission) fields**, and calculated fields with formulas.

Readme

@servicetitan/dte-pdf-editor

A React component library for creating interactive PDF editors with drag-and-drop field placement, data model integration, e-signature support, fillable form fields, job form (submission) fields, and calculated fields with formulas.

Features

  • 📄 PDF Rendering: Display and interact with PDF documents using react-pdf and pdfjs-dist
  • 🎯 Drag & Drop Field Placement: Intuitively place fields on PDF pages by dragging from a sidebar
  • 📊 Data Model Integration: Connect fields to structured data models using JSON Schema
  • ✍️ E-Signature Support: Add signature, initials, date signed, and full name fields
  • 📝 Fillable Fields: Support for text, number, date, checkbox, and radio button inputs; text and number fields show a live character counter and only accept as much as fits the field
  • 🧮 Calculated Fields: Formula-based fields with validation, autosuggest, and advanced formatting (number, currency, percent, date, rounding, separators)
  • 📅 Business-Day Schedules: Per-field toggle to exclude weekends and holidays from a date calculated field's arithmetic, with a picker for the tenant's schedules and a drawer to create one (weekend days, catalog holidays, recurring custom dates)
  • 🗂️ Forms Fields: Bind PDF placements to job form submission values (__submission_fields.* paths), with async field definitions from the host and the same paths usable inside calculated-field formulas
  • 📋 Generic Fields: Custom table and text blocks with configurable rows/cells and inline editing in view mode
  • 👥 Multi-Recipient Support: Assign fields to different recipients with color-coded visualization
  • ⚙️ Field Configuration: Configure field properties including position, size, label, recipient assignment, radio group name (shared across options in the same group), formula/format for calculated fields, and table structure (header/body rows and cells) for generic table fields
  • 🔀 Display Conditions: Show/hide rules with merge tags, fillable fields, and form submission fields; radio groups appear as a single data point with equal/not-equal operators and a value picker for group options; form dropdown, radio and checkbox fields are listed too, with a picker for the form's options
  • 👁️ View Mode: Display PDFs with filled data in a read-only or interactive view mode (including generic fields via PdfViewGeneric)

Recent changes

  • DTE-4820 (form choice fields in display conditions) — dropdown, radio and checkbox job form fields can be used in display conditions, with a picker for the form's options (FormFieldInfo.options). They are not offered in the Forms group of the Merge Tags tab. See RELEASE_NOTES.md and Job forms.
  • DTE-4842-fix (conditions) — switching forms in a condition's Select Form keeps the Form source and its picker; Clear Rules always leaves Save disabled. See RELEASE_NOTES.md.
  • DTE-4849 (Date Signed Offset Rules) — Save Changes stays disabled after Clear Rules when the field has no saved rules. See RELEASE_NOTES.md.
  • DTE-4581 (schedule UI shared with Unlayer) — deleting a schedule moves every field bound to it to the default schedule; "Calculated Signing Date" label typo fixed. See RELEASE_NOTES.md.
  • fix/improve-custom-date-drower-ui (e-sign schedule form) — for an e-sign dateSignedOffset field, "Create New Schedule" / "Edit Schedule" open inside the Field Configuration panel instead of a drawer; calculated fields keep the drawer. See RELEASE_NOTES.md and Business-day calendars.
  • fix/hide-sceduler (schedule UI) — the schedule switch, picker and the e-sign schedule are shown only when onScheduleBindingsChange is passed. See RELEASE_NOTES.md and Business-day calendars.
  • fix/shared-incloud-on-BYOD-build (installable package) — the package no longer depends on the unpublished @servicetitan/dte-shared; its code is bundled in, so npm install works. See RELEASE_NOTES.md.
  • feat/DTE-4695/custom-weekend-holiday-exclusion (business-day calendars) — a per-field "Exclude weekends & holidays" toggle, schedule picker and "Create New Schedule" drawer in the Formula Builder (scheduleBindings, holidayCatalog, schedules, onScheduleBindingsChange, onCreateSchedule); date calculated fields take a per-field business-day calendar (fieldCalendars) only while their toggle is on, match the backend's results and sign. See RELEASE_NOTES.md and Business-day calendars.
  • feature/DTE-4671 (fillable text & number character limit) — text, multiline and number fields count characters and accept only what fits the rendered field; text controls now use the field's font family and weight. See RELEASE_NOTES.md and Text and number field limits.
  • fix/inconsistncy — PDF overlay layout, typography, and text clipping aligned with the backend PDF renderer; searchable lookup-backed display-condition values. See RELEASE_NOTES.md.
  • fix/DTE-4360 (radio groups) — see RELEASE_NOTES.md.

Installation

npm install @servicetitan/dte-pdf-editor

Peer Dependencies

This package requires the following peer dependencies:

npm install @servicetitan/anvil2@^3.7.1 react@~18.3.1 react-dom@~18.3.1

Quick Start

import { PdfEditor, PdfField, PdfView, SchemaObject } from '@servicetitan/dte-pdf-editor';
import { useState } from 'react';

const pdfUrl = 'https://example.com/document.pdf';

function App() {
    const [fields, setFields] = useState<PdfField[]>([]);
    const [data, setData] = useState<Record<string, any>>({});

    const forms = [
        { id: 1, name: 'Job checklist' },
    ];

    const recipients = [
        { id: 1, name: 'technician', displayName: 'Technician' },
        { id: 2, name: 'customer', displayName: 'Customer' },
    ];

    const dataModel: SchemaObject = {
        type: 'object',
        properties: {
            CustomerName: {
                type: 'string',
                title: 'Customer Name',
                options: { placeholder: 'Enter customer name' },
            },
        },
    };

    return (
        <PdfEditor
            pdfUrl={pdfUrl}
            fields={fields}
            onFieldsChange={setFields}
            dataModel={dataModel}
            recipients={recipients}
            forms={forms}
            onFormSelect={(formIds, sendFormFields) => {
                // Load FormFieldInfo[] for each formId, then:
                sendFormFields({
                    /* [formId]: [{ id, header, itemType: 'number' | 'text' | 'date' }, ...] */
                });
            }}
        />
    );
}

Components

PdfEditor

The main component for editing PDF documents and placing fields.

Props

| Prop | Type | Required | Description | |------------------------|--------------------------------------------------------------------------------------------------|----------|-------------------------------------------------------------------------------------------------------------------------| | pdfUrl | string | Yes | URL or path to the PDF file | | fields | PdfField[] | No | Array of fields placed on the PDF | | onFieldsChange | (fields: PdfField[]) => void | Yes | Callback when fields are added, modified, or deleted | | onFormSelect | (formIds: number[], sendFormFields: (formFieldsByFormId: FormFieldsByFormIdI) => void) => void | No | With forms, host supplies FormFieldInfo[] per form id for sidebar, formulas, and preload; omit when not using forms | | dataModel | SchemaObject | No | JSON Schema object defining available data model fields | | recipients | RecipientInfo[] | No | Array of recipients that can be assigned to fields | | forms | FormInfo[] | No | Job forms shown in the sidebar and formula builder; omit to hide the Forms field group | | hideFields | FieldTypeEnum[] | No | Field menu groups to hide (e.g. omit forms when not supported) | | hideConditionalLogic | boolean | No | When true, hides display-condition UI in the field config panel | | loading | boolean | No | Whether the PDF is currently loading | | loadingPlaceholder | ReactNode | No | Custom component to display while PDF is loading | | errorPlaceholder | ReactNode | No | Custom component to display if PDF fails to load | | onLoadSuccess | (numPages: number) => void | No | Called when the PDF document loads successfully | | errors | Record<string, string> | No | Map of field id → validation message rendered on the canvas | | holidays | string[] | No | Holidays of the default business-day calendar, for the formula builder preview; as on PdfView | | fieldCalendars | Record<string, BusinessCalendar> | No | Calendars of date calculated fields bound to a schedule, by field id; as on PdfView | | scheduleBindings | FieldScheduleBinding[] | No | Date fields whose exclusion toggle is on, and the schedule each uses; as on PdfView — see Business-day calendars | | onScheduleBindingsChange | (bindings: FieldScheduleBinding[]) => void | No | Called with the complete desired scheduleBindings when the toggle or picker changes the selected field's binding. Passing it turns the schedule UI on; without it the switch, picker and e-sign schedule are not shown. It is not called for the e-sign schedule, which is stored in the field's data | | holidayCatalog | HolidayCatalog | No | The tenant's known holidays, for the schedule editor's checklist (the schedule UI needs onScheduleBindingsChange) | | holidayCatalogLoading| boolean | No | Whether holidayCatalog is still loading | | holidayCatalogError | string | No | Error message shown in the schedule editor if the catalog failed to load | | schedules | BusinessDaySchedule[] | No | The schedules the tenant can pick from the Formula Builder's schedule picker, default schedule first (the schedule UI needs onScheduleBindingsChange) | | schedulesLoading | boolean | No | Whether schedules is still loading | | schedulesError | string | No | Error message shown in the picker if the schedule list failed to load | | onCreateSchedule | (request: SaveScheduleRequest) => Promise<BusinessDaySchedule> | No | Creates a tenant schedule from the "Create New Schedule" form (a drawer; in the Field Configuration panel for an e-sign date offset); a rejected promise's message is shown as its error (the schedule UI needs onScheduleBindingsChange) | | onUpdateSchedule | (id: number, request: SaveScheduleRequest) => Promise<BusinessDaySchedule> | No | Replaces a tenant schedule in place from the picker's "Edit" action; same error handling as onCreateSchedule (the schedule UI needs onScheduleBindingsChange) | | onDeleteSchedule | (id: number) => Promise<void> | No | Permanently deletes a tenant schedule from the picker's delete action (the schedule UI needs onScheduleBindingsChange) |

Example

<PdfEditor
    pdfUrl="https://example.com/document.pdf"
    fields={fields}
    onFieldsChange={setFields}
    dataModel={dataModel}
    recipients={recipients}
    loading={!pdfUrl}
    loadingPlaceholder={<Spinner />}
    errorPlaceholder={<Alert status="danger" title="Failed to load PDF" />}
/>

PdfView

Component for displaying PDFs with filled data in view mode.

Props

| Prop | Type | Required | Description | |------------------------------|----------------------------------------------------------------|----------|-----------------------------------------------------------------------------------------------------------------------------------------| | pdfUrl | string | Yes | URL or path to the PDF file | | fields | PdfField[] | Yes | Array of fields to display on the PDF | | data | DataModelValues | No | Data object containing values for data model fields | | recipients | RecipientInfo[] | No | Array of recipients for field assignment | | fillingBy | string[] | No | Array of recipient names that are allowed to fill fields | | viewMode | PdfViewMode ('preview' \| 'fill') | No | Controls how the PDF is rendered — see View modes below. Defaults to the recipient-fill experience when omitted. | | makeFillableFieldsReadonly | boolean | No | When true, all fillable fields are non-editable (default false). See Read-only fillable fields below. | | onDataChange | (changedData: { [path: string]: string \| boolean }) => void | No | Callback when fillable field data changes | | loading | boolean | No | Whether the PDF is currently loading | | loadingPlaceholder | ReactNode | No | Custom component to display while PDF is loading | | errorPlaceholder | ReactNode | No | Custom component to display if PDF fails to load | | holidays | string[] | No | Holidays of the default business-day calendar — see Business-day calendars | | fieldCalendars | Record<string, BusinessCalendar> | No | Calendars of date calculated fields bound to a schedule, by field id — see Business-day calendars | | scheduleBindings | FieldScheduleBinding[] | No | Date fields whose exclusion toggle is on, and the schedule each uses — see Business-day calendars |

View modes

The viewMode prop controls who is interacting with the PDF and how:

  • 'preview' — Author/admin preview. All fields are visible and interactive so the admin can try out the form and see its behavior. Data changes update an internal preview buffer and are not persisted. Recipient scoping (fillingBy) is ignored — every field is fillable. Calculated fields with no resolvable value fall back to the field label (or 'Calculated Field') so the field is still visible.
  • 'fill' — Read-only display. No fields are editable regardless of fillingBy. Already-filled values are displayed; empty fields render empty.
  • Omitted (undefined) — Recipient fill experience. Only fields whose recipient matches one of the names in fillingBy are editable for the current user; other recipients' fields are read-only. onDataChange is invoked for every edit so the caller can persist the value.

Read-only fillable fields

Set makeFillableFieldsReadonly={true} on PdfView to block edits on every fillable field while still showing current values — useful for a non-interactive preview of a partially or fully filled form.

  • Text, number, date, textarea: uses the native readOnly attribute (value visible, not editable).
  • Checkbox and radio: use disabled instead — HTML does not support readOnly on these input types. Checked state is still shown; controls cannot be toggled and onDataChange is not called.

This prop applies on top of viewMode / fillingBy scoping (e.g. in 'preview' mode, fillable fields are normally interactive unless makeFillableFieldsReadonly is set).

Text and number field limits

Single-line text, multiline text and number fillable fields limit what can be entered to what fits the field, so the filled value never runs past the box the backend PDF renderer clips it to (it hard-clips, with no ellipsis). A typed/capacity counter is shown under each field and turns red when no more text fits.

  • Single-line text and number — the text's width must fit the field width.
  • Multiline text (PdfField.multiline) — the text, word-wrapped at the field width, must fit the lines that fit the field height (floor(height / line height), with a line height of 1× the font size). Explicit newlines, blank lines, long unbroken words and non-Latin text count.
  • Measured, not counted — width is measured at the field's own styles.fontFamily, fontWeight and fontSize, so narrow text (iiii) fills a field further than wide text (WWWW), and bold or a larger font fits less. The counter's capacity is the characters typed plus how many more characters of typical text still fit, so it moves as the user types.
  • Pasting and editing mid-text — text that does not fully fit is cut back to the part that does; text that was already there is never dropped, and an edit that would only add overflow is refused. Deleting is always allowed and a single character always fits.
  • Numbers reserve the browser's spin-button gutter (15px), and are a guaranteed fit: the capacity is bounded by the widest of the digits, . and -.

Notes for hosts:

  • Nothing about the limit is stored or sent — it is purely an editing constraint. Values longer than a field can show (for example set through data) are displayed unchanged and can only be shortened.
  • The limit assumes the fillable controls keep the library's box model: no padding, no border, line-height: 1 (.dte-pdf-field-fillable). Override those in host CSS and capacities will be off.
  • Text, number and textarea controls inherit font-family and font-weight from the field, so a field's styles.fontFamily / fontWeight are applied in PdfView as well as measured.
  • Measurement uses canvas measureText; without canvas (SSR, jsdom) a ratio-based estimate is used.

Example

<PdfView
    pdfUrl="https://example.com/document.pdf"
    fields={fields}
    data={data}
    recipients={recipients}
    fillingBy={['technician']}
    makeFillableFieldsReadonly={false}
    onDataChange={(changedData) => {
        setData({ ...data, ...changedData });
    }}
/>

Types

PdfField

Represents a field placed on a PDF document.

interface PdfField {
    id: string;
    type: FieldTypeEnum;
    subType?: PdfFieldSubType;
    x: number;
    y: number;
    page: number;
    label: string;
    groupName?: string; // fillable radio — shared name for all options with the same path
    width: number;
    height: number;
    required?: boolean;
    path?: string;
    formSnapshot?: FormulaFieldFormSnapshot; // for `type: forms` — mirrors bound submission path metadata
    recipient?: string;
    description?: string;
    data?: FieldDataType; // for generic fields (table/text)
    formula?: StructuredFormula; // for calculated fields
    formulaFormat?: CalculatedFieldFormat; // for calculated fields (includes dateFormat)
    displayCondition?: DisplayConditionState | null;
    styles?: PdfFieldStyles;
}

PdfFieldStyles (optional on PdfField.styles):

  • fontSize — points; converted to editor pixels to match /conversions/templatetopdf
  • fontFamily — defaults to Arial/Helvetica (PdfSharp default)
  • fontWeight — 'normal' | 'bold'; defaults to 'normal'

DisplayConditionState and related types model optional show/hide rule groups for a field (exported from this package).

DisplayConditionSingle.valueLabel is optional and display-only: when a condition Value comes from a searchable lookup dropdown, persist the selected option’s label so the condition editor can show it on reopen. Condition evaluation still uses value only.

For radio groups, getDocumentFieldsDataPointOptions exposes one entry per shared path (isRadioGroup: true, radioOptions for the value picker). Condition values use the chosen radio’s id.

For checkbox fillable fields, getDocumentFieldsDataPointOptions sets isCheckbox: true. Display conditions show only Selected (is_equal_to) and Unselected (is_not_equal_to) — no value input. Both compare against the literal "true" (fill/preview stores booleans that stringify to "true" / "false").

FieldTypeEnum

Enumeration of field types:

  • dataModel: Field connected to a data model schema
  • forms: Bound to a job form submission field (path under __submission_fields.*)
  • eSign: E-signature field
  • fillable: User-fillable form field
  • calculated: Formula-based calculated field
  • generic: Custom table or text block (configurable rows/cells, inline editing)

ESignFieldType

Types of e-signature fields:

  • signature: Signature field
  • initials: Initials field
  • dateSigned: Date signed field
  • dateSignedOffset: Calculated date a fixed number of days after signing (see ESignDateSignedOffsetSettings below)
  • fullName: Full name field

ESignDateSignedOffsetSettings (PdfField.data for a dateSignedOffset field)

  • rules: an ordered if/else-if/…/else chain — ESignDateSignedOffsetSettingsRule[] (each { days, condition: DisplayConditionState }) followed by exactly one trailing ESignDateSignedOffsetSettingsFinalRule ({ days }, no condition, always matches). The first rule whose condition matches applies its days; this package only authors the rule chain; evaluating it against submission data into an actual date happens on the backend, at signing time.
  • businessCalendarEnabled / scheduleId: the same business-day exclusion toggle and schedule choice as a calculated field's (see Business-day calendars below), stored here instead of in scheduleBindings because, like rules, it applies to every dateSignedOffset field in the document at once, not one field at a time. The e-sign schedule is shown only when onScheduleBindingsChange is passed to PdfEditor; the callback is not called for it.

All dateSignedOffset fields in a document share one data object — editing any one of them (days, rules, or the business-calendar toggle) updates every sibling via onMatchingFieldsConfigChange, and a newly dropped dateSignedOffset field seeds its data from an existing one if the document already has one.

PdfFieldSubType

Union of fillable, e-sign, generic, and form field sub-types: FillableFieldType | ESignFieldType | GenericFieldType | FormFieldType. Used for PdfField.subType and FieldTypeOption.subType (form fields use FormFieldType: number | text | date).

FillableFieldType

Types of fillable fields:

  • text: Text input (single-line, or a textarea when PdfField.multiline is set)
  • number: Number input
  • date: Date picker
  • checkbox: Checkbox
  • radio: Radio button (use + on the canvas to add options; set Radio Group Name and per-option Label in the config panel)

Radio groups store the selected option’s id at the shared path in fill/preview data. Use groupName to identify the group in the UI and in display-condition data points.

GenericFieldType

Types of generic fields:

  • table: Configurable table with header and body rows and editable cells
  • text: Text block with inline editing

FieldDataType

For generic fields, PdfField.data can be:

  • GenericFieldTableDataType: showHeader, header (height, cells), body (array of rows with height and cells)
  • GenericFieldTextDataType: value (string)

Calculated Fields (formula & format)

Calculated fields use a structured formula and optional display format:

  • StructuredFormula: Token list (numbers, operators + - * /, parentheses, field references) used for validation and safe editing.
  • CalculatedFieldFormat: Controls result display: resultType (number | currency | percent | date), rounding mode, decimal places, thousands/decimal separators, prefix/postfix text, dateFormat (custom pattern using YYYY, MM, DD, etc.).
  • FormulaToken (field): Field tokens carry fieldType: 'date' | 'number' (schema / fillable / form operands) for date-aware evaluation. Optional formSnapshot is filled when the token path is a submission field (__submission_fields.*) and parseExpression is given ParseExpressionFormContext (forms + formFieldsByFormId).

Date fields in formulas use business-day arithmetic: date + number adds business days, date - date yields a business-day count (D1 - D2 counts from D2 to D1, positive when D1 is later). Only +, -, and parentheses are allowed when date fields are present. Each ± N step is applied in turn, left to right, as the backend does.

Business-day calendars

Business-day skipping is opt-in per field, via the Formula Builder's "Exclude weekends & holidays from the calculation" toggle (Advanced Settings, date result type). A field with its toggle off uses plain calendar-day arithmetic — no weekends or holidays are skipped. An e-sign dateSignedOffset field gets the same toggle and picker shown inline in its own config panel (see ESignDateSignedOffsetSettings above), reusing the same tenant schedules (holidayCatalog, schedules, onCreateSchedule/onUpdateSchedule/onDeleteSchedule) but storing the choice on the field's data instead of in scheduleBindings.

  • Showing and hiding the UI: the switch, the picker and the e-sign schedule are offered only when onScheduleBindingsChange is passed. A host that cannot save bindings (for example one without a tenant, or whose schedules failed to load) leaves it out, and the editor then shows none of them. The formula preview still follows scheduleBindings, fieldCalendars and holidays.
  • scheduleBindings (FieldScheduleBinding[], { fieldKey, scheduleId? }, fieldKey = PdfField.id) is the source of truth for which fields have the toggle on, and which schedule each uses (scheduleId undefined = the default schedule). A field with no entry here is unaffected by holidays / fieldCalendars.
  • For an enabled field, the days skipped come from a BusinessCalendar — { weekendDays, holidays, firstYear, lastYear }, the shape DTE's POST api/holidays/calendar returns (weekend days 0 = Sunday, 6 = Saturday; holidays as YYYY-MM-DD, already moved to their observed days).
    • A field listed in fieldCalendars (by PdfField.id) uses only its calendar.
    • An enabled field with no fieldCalendars entry uses the default calendar: Saturday, Sunday and holidays, for the current year ± 1.
  • As in the backend, the result is blank for a fractional offset, a start or result outside the calendar's years, a difference spanning more than 365 days, or a missing or invalid date.

Picking, creating, editing and deleting a schedule (PdfEditor only): with the toggle on, a "Select Schedule or create new" picker lists the default schedule and the tenant's own (schedules, default first, as DTE's GET api/holidays/schedules returns them), with a collapsible preview of the selected schedule's excluded weekends, holidays and custom dates. For a selected tenant schedule (not the read-only default), the preview also shows Edit and delete actions. Choosing "create new" or Edit opens a drawer — "Create New Schedule" or "Edit Schedule" — to name a schedule and set its weekend days, holidays (from holidayCatalog, as DTE's GET api/holidays/catalog returns them) and recurring MM-DD custom dates (02-29 included); creating always calls onCreateSchedule (a new schedule, starting from the default's rules), while editing always calls onUpdateSchedule (replacing the selected schedule's rules in place, starting from its own rules) — either way the result is auto-selected. The delete action calls onDeleteSchedule, after which every field bound to the deleted schedule (the current one included) falls back to the default schedule, toggle still on. For an e-sign dateSignedOffset field the same form is shown inside the Field Configuration panel, in place of the panel's settings, instead of in a drawer; closing the form (X) or saving shows the settings again. Toggling, picking, creating, editing and deleting all call onScheduleBindingsChange with the complete desired scheduleBindings as needed; this package does not persist anything itself beyond that callback and the schedule CRUD callbacks.

Types: WeekendDay (0 | 6); KnownHoliday ({ code, displayName, rule }); HolidayCatalog ({ jurisdiction, holidays: KnownHoliday[] }); BusinessCalendarRules ({ weekendDays, knownHolidays, customDates }, the unresolved rules of a schedule — knownHolidays are catalog codes, customDates are MM-DD strings); BusinessDaySchedule ({ id?, name, isDefault, calendar: BusinessCalendarRules }); SaveScheduleRequest (BusinessCalendarRules & { name }, the onCreateSchedule argument); FieldScheduleBinding ({ fieldKey, scheduleId? }).

Schema fields can opt in via options.useInCalculatedFields and options.useInConditionals for data model extraction in formula builder and conditionals.

Job forms (FormInfo, FormFieldInfo, submission data)

  • FormInfo: { id: number; name: string } — entries passed as PdfEditor forms.
  • FormFieldInfo: { id: string; header: string; itemType: 'number' | 'text' | 'date' } — returned from the host for each requested form id.
  • FormFieldsByFormIdI: Record<number, FormFieldInfo[]> — argument to sendFormFields in the optional ** onFormSelect** callback.
  • Preview / PdfView data: Provide values under __submission_fields mirroring the path layout, e.g. { __submission_fields: { [formId]: { [normalizedFieldId]: value } } } (field id keys without hyphens match buildFormFieldKey).

SchemaObject

JSON Schema object defining the data model structure. Supports nested objects, arrays, and various string subtypes (string, text, html, image).

interface SchemaObject {
    type: 'object';
    properties: Record<string, SchemaNode>;
    title?: string;
    options?: SchemaFieldBaseOptions;
}

RecipientInfo

Information about a recipient that can be assigned to fields.

interface RecipientInfo {
    id: number;
    name: string;
    displayName: string;
}

Usage Examples

Basic Editor with Data Model

import { PdfEditor, PdfField, SchemaObject } from '@servicetitan/dte-pdf-editor';
import { useState } from 'react';

const dataModel: SchemaObject = {
    type: 'object',
    properties: {
        JobInformation: {
            type: 'object',
            properties: {
                JobNumber: {
                    type: 'string',
                    title: 'Job Number',
                    options: { placeholder: 'Enter job number' },
                },
                JobName: {
                    type: 'string',
                    title: 'Job Name',
                    options: { placeholder: 'Enter job name' },
                },
            },
        },
    },
};

function MyEditor() {
    const [fields, setFields] = useState<PdfField[]>([]);

    return (
        <PdfEditor
            pdfUrl="https://example.com/document.pdf"
            fields={fields}
            onFieldsChange={setFields}
            dataModel={dataModel}
        />
    );
}

Editor with Recipients

const recipients = [
    { id: 1, name: 'technician', displayName: 'Technician' },
    { id: 2, name: 'customer', displayName: 'Customer' },
    { id: 3, name: 'accountant', displayName: 'Accountant' },
];

<PdfEditor
    pdfUrl={pdfUrl}
    fields={fields}
    onFieldsChange={setFields}
    recipients={recipients}
/>

View Mode with Data

const [data, setData] = useState({
    'JobInformation.JobNumber': 'JOB-12345',
    'JobInformation.JobName': 'HVAC Installation',
});

<PdfView
    pdfUrl={pdfUrl}
    fields={fields}
    data={data}
    recipients={recipients}
    fillingBy={['technician']}
    onDataChange={(changedData) => {
        setData({ ...data, ...changedData });
    }}
/>

Toggle Between Edit and View Modes

function PdfEditorPage() {
    const [fields, setFields] = useState<PdfField[]>([]);
    const [isView, setIsView] = useState(false);
    const [data, setData] = useState<Record<string, any>>({});

    return (
        <>
            <Button onClick={() => setIsView(!isView)}>
                {isView ? 'Edit' : 'View'}
            </Button>
            {!isView ? (
                <PdfEditor
                    pdfUrl={pdfUrl}
                    fields={fields}
                    onFieldsChange={setFields}
                    dataModel={dataModel}
                    recipients={recipients}
                />
            ) : (
                <PdfView
                    pdfUrl={pdfUrl}
                    fields={fields}
                    data={data}
                    recipients={recipients}
                    fillingBy={['technician']}
                    onDataChange={(changedData) => {
                        setData({ ...data, ...changedData });
                    }}
                />
            )}
        </>
    );
}

Field Operations

Adding Fields

Fields are added by dragging from the sidebar onto the PDF canvas. The sidebar displays:

  • Data Model Fields (Merge Tags): Grouped by schema structure
  • E-Sign Fields: Signature, initials, date signed, full name
  • Fillable Fields: Text, number, date, checkbox, radio
  • Forms (when forms + onFormSelect are provided): Choose a job form, then drag a field onto the canvas; the new PdfField stores path and formSnapshot for the bound submission key
  • Calculated Fields: Formula-based fields (configure formula and format in the config panel)
  • Generic Fields: Text and Table (configure table rows/cells in the config panel; inline editing in view mode)

Configuring Fields

Click on a field to open the configuration panel where you can:

  • Change the label
  • For radio fillable fields: set Radio Group Name (applies to all options with the same path) and Label per option
  • Adjust position and size
  • Assign a recipient
  • Set the required status (fillable fields, except checkbox)
  • Update the data model path
  • For calculated fields: edit formula (with validation and autosuggest from data model, fillable fields, and number/date job form fields via the Forms slide-out), set result type (number/currency/percent/date), rounding, decimals, separators, and date format
  • For forms fields: edit label like other display labels; binding is defined by path / formSnapshot from placement
  • For generic table fields: configure header visibility, header/body row heights, and cell values via Table configs; generic text fields support inline editing in the overlay
  • Display conditions (when not hidden): rules can reference merge tags, fillable fields, and form fields; radio groups appear once by groupName with equal/not-equal operators and a Select value for the selected option’s id

Radio groups (fillable)

  1. Drag Radio onto the PDF.
  2. Open the config panel: set Radio Group Name and the Label for the first option.
  3. Click + on the radio overlay to add more options in the same group (shared path and groupName, unique id and label per option).
  4. In display conditions, pick the group under Fillable field → Data point; choose Is equal to or Is not equal to and select an option in Value.

Moving and Resizing

  • Move: Click and drag a field to reposition it
  • Resize: Use the resize handles on field corners

Deleting Fields

Select a field and use the delete button in the configuration panel, or press the delete key.

Styling

The package includes default styles. To customize, you can override CSS classes:

  • .dte-pdf-editor: Main editor container
  • .dte-pdf-view-container: View mode container
  • .dte-pdf-wrapper: PDF document wrapper
  • .dte-pdf-field-overlay: Field overlay container
  • .dte-pdf-field-text--single-line / --multiline / --wrapped: Text clip modes (hard clip, no ellipsis)
  • .dte-pdf-field-char-count: Character counter under text and number fields (--at-limit when full)
  • .dte-pdf-editor-sidebar-container: Sidebar container

Field overlays use border-box sizing with no padding so width/height match the backend PDF clip rectangle. Editor selection chrome uses outline (not border) so it does not affect layout.

Lookup-backed display condition values

Schema fields tagged with options.fieldLookupSource can drive the Value control in display conditions. The host resolves options via the lookup event bus (host-events.ts):

  • Static source — host returns the full option list; the editor filters locally (SelectFieldSync).
  • Searchable source — host sets searchable: true on the first response; the editor requests options per typed query and can resolve saved keys via keys for label rehydration. Persist valueLabel on DisplayConditionSingle when the user picks a searchable option.

Browser Support

This package requires modern browsers with support for:

  • ES6+
  • Canvas API
  • Drag and Drop API

License

Copyright © ServiceTitan