@dereekb/dbx-web 13.33.0 → 13.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,8 +1,8 @@
1
1
  import * as i0 from '@angular/core';
2
2
  import { Injectable, input, inject, effect, Directive, NgModule, ChangeDetectionStrategy, Component, output, computed, HostListener, forwardRef, model, InjectionToken, viewChild, ElementRef, makeEnvironmentProviders, untracked, ApplicationRef, Injector, EnvironmentInjector, createComponent, DestroyRef, signal, TemplateRef, SecurityContext, ViewContainerRef, booleanAttribute, Renderer2 } from '@angular/core';
3
- import { asPromise, DASH_CHARACTER_PREFIX_INSTANCE, hashStringToNumber, cssTokenVar, useIterableOrValue, isDefinedAndNotFalse, spaceSeparatedCssClasses, cssClassesSet, objectHasNoKeys, getValueFromGetter, firstValue, filterUndefinedValues, separateValues, splitFront, asDecisionFunction, SLASH_PATH_FILE_TYPE_SEPARATOR, filterMaybeArrayValues, mapIterable, toReadableError, isDefaultReadableError, MS_IN_SECOND, mergeObjects, build, ServerErrorResponse, UnauthorizedServerErrorResponse, makeTimer, MS_IN_MINUTE, toggleTimerRunning, unixDateTimeSecondsNumberForNow, ModelRelationUtility, encodeModelKeyTypePair, safeCompareEquality, addMilliseconds, isPast, asArray, slashPathDetails, mimeTypeForFileExtension, slashPathDirectoryTree, nameToInitials, isMaybeNot, isNotFalse, modifier, combineMaps, addModifiers, removeModifiers, applyBestFit, findNext, maybeModifierMapToFunction, makeValuesGroupMap, compareWithMappedValuesFunction, invertMaybeBoolean, splitCommaSeparatedStringToSet, ZIP_FILE_MIME_TYPE, cachedGetter, sortByNumberFunction, JPEG_MIME_TYPES, JPEG_MIME_TYPE, PNG_MIME_TYPE, PDF_MIME_TYPE, sequentialIncrementingNumberStringModelIdFactory, PDF_HEADER, PDF_EOF_MARKER, PDF_ENCRYPT_MARKER } from '@dereekb/util';
3
+ import { asPromise, DASH_CHARACTER_PREFIX_INSTANCE, hashStringToNumber, cssTokenVar, useIterableOrValue, isDefinedAndNotFalse, spaceSeparatedCssClasses, cssClassesSet, objectHasNoKeys, getValueFromGetter, firstValue, filterUndefinedValues, separateValues, splitFront, asDecisionFunction, SLASH_PATH_FILE_TYPE_SEPARATOR, filterMaybeArrayValues, mapIterable, toReadableError, isDefaultReadableError, MS_IN_SECOND, mergeObjects, build, ServerErrorResponse, UnauthorizedServerErrorResponse, makeTimer, MS_IN_MINUTE, toggleTimerRunning, unixDateTimeSecondsNumberForNow, ModelRelationUtility, encodeModelKeyTypePair, safeCompareEquality, addMilliseconds, isPast, asArray, slashPathDetails, mimeTypeForFileExtension, slashPathDirectoryTree, nameToInitials, isMaybeNot, isNotFalse, modifier, combineMaps, addModifiers, removeModifiers, applyBestFit, findNext, maybeModifierMapToFunction, makeValuesGroupMap, compareWithMappedValuesFunction, invertMaybeBoolean, splitCommaSeparatedStringToSet, ZIP_FILE_MIME_TYPE, cachedGetter, sortByNumberFunction, JPEG_MIME_TYPES, JPEG_MIME_TYPE, PNG_MIME_TYPE, PDF_MIME_TYPE, JSON_MIME_TYPE, sequentialIncrementingNumberStringModelIdFactory, PDF_HEADER, PDF_EOF_MARKER, PDF_ENCRYPT_MARKER } from '@dereekb/util';
4
4
  import * as i1$2 from '@dereekb/dbx-core';
5
- import { completeOnDestroy, clean, AbstractTransitionWatcherDirective, DbxInjectionComponent, dbxActionWorkProgress, AbstractDbxButtonDirective, hasNonTrivialChildNodes, provideDbxButton, DbxCoreButtonModule, createInjectorForInjectionComponentConfig, initInjectionComponent, AbstractDbxActionValueGetterDirective, cleanSubscription, AbstractDbxActionHandlerDirective, FilterSourceDirective, provideActionStoreSource, isClickableFilterPreset, AbstractDbxAnchorDirective, expandClickableAnchorLinkTrees, DbxCoreFilterModule, DbxButton, DbxActionWorkable, provideDbxActionWorkable, DbxActionContextStoreSourceInstance, cleanSubscriptionWithLockSet, DBX_INJECTION_COMPONENT_DATA, checkNgContentWrapperHasContent, cleanLoadingContext, DbxActionSourceDirective, DbxActionSuccessHandlerDirective, DbxActionDirective, transformEmptyStringInputToUndefined, isIdleActionState, canTriggerAction, DbxCoreActionModule, DbxActionButtonDirective, onDbxAppAuth, SimpleStorageAccessorFactory, mergeStaticProviders, asSegueRef, AbstractTransitionDirective, DbxRouterService, AbstractIfDirective, isSegueRefActive, anchorTypeForAnchor } from '@dereekb/dbx-core';
5
+ import { completeOnDestroy, clean, AbstractTransitionWatcherDirective, DbxInjectionComponent, dbxActionWorkProgress, AbstractDbxButtonDirective, hasNonTrivialChildNodes, provideDbxButton, DbxCoreButtonModule, createInjectorForInjectionComponentConfig, initInjectionComponent, AbstractDbxActionValueGetterDirective, cleanSubscription, AbstractDbxActionHandlerDirective, FilterSourceDirective, provideActionStoreSource, isClickableFilterPreset, AbstractDbxAnchorDirective, expandClickableAnchorLinkTrees, DbxCoreFilterModule, DbxButton, DbxActionWorkable, provideDbxActionWorkable, DbxActionContextStoreSourceInstance, cleanSubscriptionWithLockSet, DBX_INJECTION_COMPONENT_DATA, checkNgContentWrapperHasContent, cleanLoadingContext, DbxActionSourceDirective, DbxActionSuccessHandlerDirective, DbxActionDirective, transformEmptyStringInputToUndefined, isIdleActionState, canTriggerAction, DbxCoreActionModule, DbxActionButtonDirective, onDbxAppAuth, SimpleStorageAccessorFactory, mergeStaticProviders, asSegueRef, AbstractTransitionDirective, DbxRouterService, AbstractIfDirective, isSegueRefActive, anchorTypeForAnchor, DbxActionDisabledDirective, DbxActionHandlerDirective } from '@dereekb/dbx-core';
6
6
  import { NgPopoverRef, NgOverlayContainerService } from 'ng-overlay-container';
7
7
  import { of, map, BehaviorSubject, distinctUntilChanged, shareReplay, delay, Subject, first, startWith, filter, throttleTime, skip, defaultIfEmpty, combineLatest, switchMap, delayWhen, timer, asyncScheduler, from, firstValueFrom, mergeMap, race, tap, catchError, distinct, merge, exhaustMap, NEVER } from 'rxjs';
8
8
  import { LockSet, distinctUntilItemsHaveDifferentValues, filterMaybe, skipAllInitialMaybe, maybeValueFromObservableOrValue, switchMapMaybeLoadingContextStream, LoadingStateType, loadingStateType, successResult, valueFromFinishedLoadingState, loadingStateContext, initialize, asObservable, isNot, mapForEach, switchMapMaybe, listLoadingStateContext, isLoadingStateFinishedLoading, startWithBeginLoading, loadingStateFromObs, errorResult, beginLoading, distinctUntilHasDifferentValues } from '@dereekb/rxjs';
@@ -52,7 +52,7 @@ import * as i1$8 from '@angular/material/chips';
52
52
  import { MatChipsModule } from '@angular/material/chips';
53
53
  import * as i2$1 from '@angular/material/tooltip';
54
54
  import { MatTooltipModule } from '@angular/material/tooltip';
55
- import { PDFDocument } from '@cantoo/pdf-lib';
55
+ import { PDFName, PDFString, PDFHexString, PDFDocument, degrees } from '@cantoo/pdf-lib';
56
56
  import { moveItemInArray, CdkDrag, CdkDragHandle, CdkDragPlaceholder, CdkDropList } from '@angular/cdk/drag-drop';
57
57
 
58
58
  /**
@@ -18151,6 +18151,22 @@ const DEFAULT_DBX_PDF_MERGE_ENCRYPTED_HANDLING = 'focus';
18151
18151
  * Error message used when an encrypted entry is projected to the `error` status under {@link DbxPdfMergeEncryptedHandling} `'error'` mode.
18152
18152
  */
18153
18153
  const DBX_PDF_MERGE_ENCRYPTED_ERROR_MESSAGE = 'Password-protected PDFs cannot be merged.';
18154
+ /**
18155
+ * Default value for {@link DbxPdfMergeEditorConfig.pageEditing}. Off, so the editor's default behavior is unchanged.
18156
+ */
18157
+ const DEFAULT_DBX_PDF_MERGE_PAGE_EDITING = false;
18158
+ /**
18159
+ * Default value for {@link DbxPdfMergeEditorConfig.sidecar}. Off, so merged output bytes are unchanged.
18160
+ */
18161
+ const DEFAULT_DBX_PDF_MERGE_SIDECAR = false;
18162
+ /**
18163
+ * Default value for {@link DbxPdfMergeEditorConfig.restoreImportOnClear}. On, so an editor the app populated programmatically is reset to that document by Clear rather than left empty.
18164
+ */
18165
+ const DEFAULT_DBX_PDF_MERGE_RESTORE_IMPORT_ON_CLEAR = true;
18166
+ /**
18167
+ * Message shown on an encrypted entry's row while page editing is enabled. Encrypted documents cannot be opened by `pdf-lib`, so their pages cannot be listed or edited — the entry stays a single opaque row.
18168
+ */
18169
+ const DBX_PDF_MERGE_ENCRYPTED_NOT_EDITABLE_MESSAGE = 'Encrypted — pages cannot be edited.';
18154
18170
  /**
18155
18171
  * MIME types accepted by the PDF merge editor by default: PDF documents and PNG/JPEG images.
18156
18172
  */
@@ -18159,6 +18175,45 @@ const DEFAULT_PDF_MERGE_ACCEPT = [PDF_MIME_TYPE, PNG_MIME_TYPE, JPEG_MIME_TYPE];
18159
18175
  * MIME type emitted by the merged result blob.
18160
18176
  */
18161
18177
  const PDF_MERGE_RESULT_MIME_TYPE = PDF_MIME_TYPE;
18178
+ /**
18179
+ * Group key used for pages whose source entry has no slot id.
18180
+ */
18181
+ const DEFAULT_PDF_MERGE_PAGE_GROUP_KEY = '_';
18182
+ /**
18183
+ * Returns the group key a page reorders within — its entry's slot id, or {@link DEFAULT_PDF_MERGE_PAGE_GROUP_KEY} when the entry is unslotted.
18184
+ *
18185
+ * @param slotId - Slot id of the owning entry, if any.
18186
+ * @returns The group key.
18187
+ * @__NO_SIDE_EFFECTS__
18188
+ */
18189
+ function pdfMergePageGroupKeyForSlotId(slotId) {
18190
+ return slotId ?? DEFAULT_PDF_MERGE_PAGE_GROUP_KEY;
18191
+ }
18192
+ /**
18193
+ * Separator between the entry id and the page index inside a page id.
18194
+ */
18195
+ const PDF_MERGE_PAGE_ID_SEPARATOR = ':';
18196
+ /**
18197
+ * Builds the stable identifier for a page within the editor.
18198
+ *
18199
+ * @param entryId - Id of the owning {@link PdfMergeEntry}.
18200
+ * @param sourceIndex - Zero-based page index within that entry's source document.
18201
+ * @returns The page id.
18202
+ * @__NO_SIDE_EFFECTS__
18203
+ */
18204
+ function makePdfMergePageId(entryId, sourceIndex) {
18205
+ return `${entryId}${PDF_MERGE_PAGE_ID_SEPARATOR}${sourceIndex}`;
18206
+ }
18207
+ /**
18208
+ * Returns the id of the entry a page id belongs to. Used to prune page state when an entry leaves the list.
18209
+ *
18210
+ * @param pageId - Page id produced by {@link makePdfMergePageId}.
18211
+ * @returns The owning entry's id.
18212
+ * @__NO_SIDE_EFFECTS__
18213
+ */
18214
+ function entryIdForPdfMergePageId(pageId) {
18215
+ return pageId.slice(0, pageId.indexOf(PDF_MERGE_PAGE_ID_SEPARATOR));
18216
+ }
18162
18217
  /**
18163
18218
  * Injection token for a workspace-wide default {@link DbxPdfMergeEditorConfig}. Use {@link provideDbxPdfMergeEditorConfig} to register a value.
18164
18219
  */
@@ -18186,6 +18241,233 @@ function provideDbxPdfMergeEditorPreserveEntriesOnSlotDestroy(preserve = true) {
18186
18241
  return { provide: DBX_PDF_MERGE_EDITOR_PRESERVE_ENTRIES_ON_SLOT_DESTROY, useValue: preserve };
18187
18242
  }
18188
18243
 
18244
+ /**
18245
+ * Name of the JSON manifest attached to a merged PDF that records which pages came from which slot.
18246
+ */
18247
+ const PDF_MERGE_SIDECAR_FILE_NAME = 'dbx-pdf-merge.json';
18248
+ /**
18249
+ * Key set on each output page's dictionary holding that page's {@link PdfMergeSidecarPage.tag}.
18250
+ *
18251
+ * Custom page-dictionary keys travel with the page: they survive `copyPages` (including subsetting and reordering), a save/load round trip, and further edits. That is what makes tags — rather than the manifest's recorded indices — the authoritative record of which document a page belongs to.
18252
+ */
18253
+ const PDF_MERGE_PAGE_TAG_KEY = 'DbxPageTag';
18254
+ /**
18255
+ * Schema version of the emitted {@link PdfMergeSidecar}.
18256
+ */
18257
+ const PDF_MERGE_SIDECAR_VERSION = 1;
18258
+ /**
18259
+ * Description recorded on the attached manifest.
18260
+ */
18261
+ const PDF_MERGE_SIDECAR_DESCRIPTION = 'dbx-pdf-merge page manifest';
18262
+ /**
18263
+ * Builds the tag written onto an output page.
18264
+ *
18265
+ * @param slotId - Slot the page's source entry belongs to, or `null` when unslotted.
18266
+ * @param ordinal - Zero-based position of the page within its own slot's pages.
18267
+ * @returns A tag unique within the output document.
18268
+ * @__NO_SIDE_EFFECTS__
18269
+ */
18270
+ function makePdfMergeSidecarPageTag(slotId, ordinal) {
18271
+ return `${slotId ?? '_'}-${ordinal}`;
18272
+ }
18273
+ /**
18274
+ * Writes a page's tag onto its page dictionary so the association survives later reordering.
18275
+ *
18276
+ * @param page - Page in the output document.
18277
+ * @param tag - Tag to record.
18278
+ */
18279
+ function writePdfMergePageTag(page, tag) {
18280
+ page.node.set(PDFName.of(PDF_MERGE_PAGE_TAG_KEY), PDFString.of(tag));
18281
+ }
18282
+ /**
18283
+ * Reads the tag previously written onto a page, if any.
18284
+ *
18285
+ * Uses `get` plus an instance check rather than a typed `lookup`, because `PDFDict.lookup(key, PDFString)` throws when the key is absent — which is the normal case for any PDF that did not come out of this editor.
18286
+ *
18287
+ * @param page - Page to inspect.
18288
+ * @returns The tag, or `null` when the page carries none.
18289
+ */
18290
+ function readPdfMergePageTag(page) {
18291
+ const value = page.node.get(PDFName.of(PDF_MERGE_PAGE_TAG_KEY));
18292
+ let tag;
18293
+ if (value instanceof PDFString || value instanceof PDFHexString) {
18294
+ tag = value.decodeText();
18295
+ }
18296
+ else {
18297
+ tag = null;
18298
+ }
18299
+ return tag;
18300
+ }
18301
+ /**
18302
+ * Attaches a manifest to the output document as an embedded JSON file.
18303
+ *
18304
+ * @param document - Output document being saved.
18305
+ * @param sidecar - Manifest to embed.
18306
+ */
18307
+ async function attachPdfMergeSidecar(document, sidecar) {
18308
+ // Re-wrap with this module's Uint8Array: pdf-lib validates its input with `instanceof`, and a
18309
+ // TextEncoder supplied by another realm (Node's, under jsdom) produces an array that fails that
18310
+ // check even though it is a genuine Uint8Array.
18311
+ const bytes = new Uint8Array(new TextEncoder().encode(JSON.stringify(sidecar)));
18312
+ await document.attach(bytes, PDF_MERGE_SIDECAR_FILE_NAME, {
18313
+ mimeType: JSON_MIME_TYPE,
18314
+ description: PDF_MERGE_SIDECAR_DESCRIPTION
18315
+ });
18316
+ }
18317
+ /**
18318
+ * Builds a {@link PdfMergeSidecar} from the pages written to an output document.
18319
+ *
18320
+ * @param pages - Page records in output order.
18321
+ * @returns The manifest, with one document per distinct slot in first-appearance order.
18322
+ * @__NO_SIDE_EFFECTS__
18323
+ */
18324
+ function makePdfMergeSidecar(pages) {
18325
+ const tagsBySlot = new Map();
18326
+ pages.forEach((page) => {
18327
+ const existing = tagsBySlot.get(page.slotId);
18328
+ if (existing == null) {
18329
+ tagsBySlot.set(page.slotId, [page.tag]);
18330
+ }
18331
+ else {
18332
+ existing.push(page.tag);
18333
+ }
18334
+ });
18335
+ const documents = Array.from(tagsBySlot.entries()).map(([slotId, pageTags]) => ({ slotId, pageTags }));
18336
+ return {
18337
+ version: PDF_MERGE_SIDECAR_VERSION,
18338
+ createdAt: new Date().toISOString(),
18339
+ documents,
18340
+ pages
18341
+ };
18342
+ }
18343
+ async function bytesForSidecarReadInput(input) {
18344
+ return input instanceof Blob ? input.arrayBuffer() : input;
18345
+ }
18346
+ function parseSidecarBytes(data) {
18347
+ let sidecar;
18348
+ try {
18349
+ const parsed = JSON.parse(new TextDecoder().decode(data));
18350
+ sidecar = Array.isArray(parsed?.documents) && Array.isArray(parsed?.pages) ? parsed : null;
18351
+ }
18352
+ catch {
18353
+ sidecar = null;
18354
+ }
18355
+ return sidecar;
18356
+ }
18357
+ /**
18358
+ * Reads the manifest embedded in a merged PDF and resolves each recorded page to its current position in that file.
18359
+ *
18360
+ * Resolution prefers each page's own tag, which follows the page through reordering. The manifest's {@link PdfMergeSidecarPage.outputIndex} is used only as a fallback for pages whose tag has been stripped, and any index that no longer exists is reported through {@link PdfMergeSidecarReadResult.missingTags} instead.
18361
+ *
18362
+ * @param input - The merged PDF's bytes.
18363
+ * @returns The manifest and its resolved documents, or `null` when the file carries no manifest, is unreadable, or is encrypted.
18364
+ */
18365
+ async function readPdfMergeSidecar(input) {
18366
+ let result;
18367
+ try {
18368
+ const bytes = await bytesForSidecarReadInput(input);
18369
+ const document = await PDFDocument.load(bytes);
18370
+ result = resolvePdfMergeSidecar(document);
18371
+ }
18372
+ catch {
18373
+ result = null;
18374
+ }
18375
+ return result;
18376
+ }
18377
+ /**
18378
+ * Resolves the manifest of an already-loaded document. Split out from {@link readPdfMergeSidecar} so {@link splitPdfMergeSidecarDocuments} can reuse the resolution without parsing the same bytes twice.
18379
+ *
18380
+ * @param document - Loaded merged document.
18381
+ * @returns The resolved manifest, or `null` when the document carries none.
18382
+ */
18383
+ function resolvePdfMergeSidecar(document) {
18384
+ let result;
18385
+ {
18386
+ const attachment = document.getAttachments().find((x) => x.name === PDF_MERGE_SIDECAR_FILE_NAME);
18387
+ const sidecar = attachment == null ? null : parseSidecarBytes(attachment.data);
18388
+ if (sidecar == null) {
18389
+ result = null;
18390
+ }
18391
+ else {
18392
+ const pageCount = document.getPageCount();
18393
+ const indexByTag = new Map();
18394
+ let untaggedPageCount = 0;
18395
+ document.getPages().forEach((page, index) => {
18396
+ const tag = readPdfMergePageTag(page);
18397
+ if (tag == null) {
18398
+ untaggedPageCount += 1;
18399
+ }
18400
+ else {
18401
+ indexByTag.set(tag, index);
18402
+ }
18403
+ });
18404
+ const recordByTag = new Map(sidecar.pages.map((page) => [page.tag, page]));
18405
+ const missingTags = [];
18406
+ const documents = sidecar.documents.map((sidecarDocument) => {
18407
+ const pages = [];
18408
+ sidecarDocument.pageTags.forEach((tag) => {
18409
+ const record = recordByTag.get(tag);
18410
+ const taggedIndex = indexByTag.get(tag);
18411
+ if (record == null) {
18412
+ missingTags.push(tag);
18413
+ }
18414
+ else if (taggedIndex != null) {
18415
+ pages.push({ ...record, pageIndex: taggedIndex, resolvedByTag: true });
18416
+ }
18417
+ else if (record.outputIndex >= 0 && record.outputIndex < pageCount) {
18418
+ pages.push({ ...record, pageIndex: record.outputIndex, resolvedByTag: false });
18419
+ }
18420
+ else {
18421
+ missingTags.push(tag);
18422
+ }
18423
+ });
18424
+ return { slotId: sidecarDocument.slotId, pages };
18425
+ });
18426
+ result = { sidecar, documents, missingTags, untaggedPageCount };
18427
+ }
18428
+ }
18429
+ return result;
18430
+ }
18431
+ /**
18432
+ * Splits a previously-merged PDF back into one file per document, using the embedded manifest to decide which pages belong to which slot.
18433
+ *
18434
+ * This is the inverse of a merge with `sidecar: true`, and is what lets a completed document be re-imported into the editor with its slots repopulated instead of arriving as one opaque file. Page order and any rotation baked in at merge time are preserved, and each document's pages are resolved by tag so a document that was reordered after export still splits correctly.
18435
+ *
18436
+ * @param input - Bytes of a merged PDF.
18437
+ * @returns One file per document plus the manifest, or `null` when the input carries no manifest, is unreadable, or is encrypted.
18438
+ */
18439
+ async function splitPdfMergeSidecarDocuments(input) {
18440
+ let result;
18441
+ try {
18442
+ const bytes = await bytesForSidecarReadInput(input);
18443
+ const source = await PDFDocument.load(bytes);
18444
+ const read = resolvePdfMergeSidecar(source);
18445
+ if (read == null) {
18446
+ result = null;
18447
+ }
18448
+ else {
18449
+ const documents = [];
18450
+ for (const document of read.documents) {
18451
+ if (document.pages.length > 0) {
18452
+ const target = await PDFDocument.create();
18453
+ const copied = await target.copyPages(source, document.pages.map((page) => page.pageIndex));
18454
+ copied.forEach((page) => target.addPage(page));
18455
+ const outputBytes = await target.save();
18456
+ const name = document.pages[0].sourceName || `${document.slotId ?? 'document'}.pdf`;
18457
+ const file = new File([outputBytes], name, { type: PDF_MIME_TYPE });
18458
+ documents.push({ slotId: document.slotId, file, pageCount: document.pages.length });
18459
+ }
18460
+ }
18461
+ result = { sidecar: read.sidecar, documents, missingTags: read.missingTags, untaggedPageCount: read.untaggedPageCount };
18462
+ }
18463
+ }
18464
+ catch {
18465
+ result = null;
18466
+ }
18467
+ return result;
18468
+ }
18469
+
18470
+ const FULL_ROTATION_DEGREES = 360;
18189
18471
  const TEXT_DECODER = new TextDecoder('latin1');
18190
18472
  const FORMAT_KILOBYTE = 1024;
18191
18473
  const FORMAT_MEGABYTE = FORMAT_KILOBYTE * 1024;
@@ -18209,6 +18491,33 @@ function formatPdfMergeEntrySize(size) {
18209
18491
  }
18210
18492
  return result;
18211
18493
  }
18494
+ const DEFAULT_BLOB_FILE_NAMES = {
18495
+ [PDF_MIME_TYPE]: 'document.pdf',
18496
+ [PNG_MIME_TYPE]: 'image.png',
18497
+ [JPEG_MIME_TYPE]: 'image.jpg'
18498
+ };
18499
+ const DEFAULT_BLOB_FILE_NAME = DEFAULT_BLOB_FILE_NAMES[PDF_MIME_TYPE];
18500
+ /**
18501
+ * Normalizes a {@link Blob} into a {@link File} so it can become a {@link PdfMergeEntry}, which needs a name to display.
18502
+ *
18503
+ * A {@link File} passes through untouched unless an explicit `fileName` overrides it. A bare {@link Blob} is named from `fileName`, else from its MIME type (`document.pdf` / `image.png` / `image.jpg`), else `document.pdf` — the name only has to be plausible and correctly suffixed, since {@link classifyPdfMergeFile} falls back to the extension when a blob carries no type.
18504
+ *
18505
+ * @param input - Blob or File to normalize.
18506
+ * @param fileName - Optional name, overriding a File's own name and naming a bare Blob.
18507
+ * @returns A File carrying the resolved name.
18508
+ */
18509
+ function asPdfMergeFile(input, fileName) {
18510
+ const mimeType = (input.type ?? '').toLowerCase();
18511
+ let file;
18512
+ if (input instanceof File && fileName == null) {
18513
+ file = input;
18514
+ }
18515
+ else {
18516
+ const name = fileName ?? DEFAULT_BLOB_FILE_NAMES[mimeType] ?? DEFAULT_BLOB_FILE_NAME;
18517
+ file = new File([input], name, { type: input.type || mimeTypeForFileExtension(slashPathDetails(name.toLowerCase()).typedFileExtension) || PDF_MIME_TYPE });
18518
+ }
18519
+ return file;
18520
+ }
18212
18521
  /**
18213
18522
  * Returns the {@link PdfMergeEntryKind} for a file based on its MIME type, with a small fallback to file-extension matching when the browser provided no MIME type.
18214
18523
  *
@@ -18363,30 +18672,305 @@ async function validatePdfMergeEntry(entry) {
18363
18672
  }
18364
18673
  return result;
18365
18674
  }
18366
- async function appendPdfPages(target, entry) {
18367
- const bytes = await entry.file.arrayBuffer();
18368
- const source = await PDFDocument.load(bytes);
18369
- const pages = await target.copyPages(source, source.getPageIndices());
18370
- pages.forEach((page) => target.addPage(page));
18675
+ /**
18676
+ * Normalizes an arbitrary degree value into the `[0, 360)` range.
18677
+ *
18678
+ * @param angle - Angle in degrees, possibly negative or over a full turn.
18679
+ * @returns The equivalent angle in `[0, 360)`.
18680
+ * @__NO_SIDE_EFFECTS__
18681
+ */
18682
+ function normalizeRotationDegrees(angle) {
18683
+ return ((angle % FULL_ROTATION_DEGREES) + FULL_ROTATION_DEGREES) % FULL_ROTATION_DEGREES;
18684
+ }
18685
+ /**
18686
+ * Reads per-page metadata from an entry's source document so the editor can list its pages individually.
18687
+ *
18688
+ * Only called while page editing is enabled — the normal upload path never parses the document, it only scans for the PDF header and markers (see {@link validatePdfMergeEntry}).
18689
+ *
18690
+ * @param entry - Entry to inspect.
18691
+ * @returns One {@link PdfMergePageMeta} per source page, or `null` when the entry is encrypted or cannot be parsed. Image entries report a single synthetic page.
18692
+ */
18693
+ async function readPdfMergeEntryPageMetas(entry) {
18694
+ let metas;
18695
+ if (entry.encrypted) {
18696
+ // pdf-lib cannot open an encrypted document at all, so its pages can never be listed or edited.
18697
+ metas = null;
18698
+ }
18699
+ else if (entry.kind === 'image') {
18700
+ const dimensions = entry.original.dimensions;
18701
+ metas = [{ sourceIndex: 0, width: dimensions?.width ?? 0, height: dimensions?.height ?? 0, sourceRotation: 0 }];
18702
+ }
18703
+ else {
18704
+ try {
18705
+ const bytes = await entry.file.arrayBuffer();
18706
+ const source = await PDFDocument.load(bytes);
18707
+ metas = source.getPages().map((page, sourceIndex) => {
18708
+ const { width, height } = page.getSize();
18709
+ return { sourceIndex, width, height, sourceRotation: normalizeRotationDegrees(page.getRotation().angle) };
18710
+ });
18711
+ }
18712
+ catch {
18713
+ metas = null;
18714
+ }
18715
+ }
18716
+ return metas;
18717
+ }
18718
+ /**
18719
+ * Reconstructs editor entries from a PDF previously exported with an embedded manifest.
18720
+ *
18721
+ * Each document recorded in the manifest becomes one entry carrying its original slot id, so dropping the result into the store repopulates the same slots the file was built from. This is the re-import half of the sidecar: without it a completed document can only come back as a single opaque file.
18722
+ *
18723
+ * @param input - Bytes of a merged PDF, typically the file the user just chose.
18724
+ * @param config - Optional entry-building config (id factory override, and the sidecar-less fallback).
18725
+ * @returns The reconstructed entries and manifest, or a reason when the file cannot be imported.
18726
+ */
18727
+ async function buildPdfMergeEntriesFromSidecar(input, config) {
18728
+ const split = await splitPdfMergeSidecarDocuments(input);
18729
+ let result;
18730
+ if (split == null) {
18731
+ // A readable PDF without a manifest and an unreadable one are distinguished so the UI can
18732
+ // explain the difference — "not exported from here" reads very differently from "corrupt".
18733
+ const readable = await isReadablePdf(input);
18734
+ if (!readable) {
18735
+ result = { error: 'unreadable' };
18736
+ }
18737
+ else if (config?.allowWithoutSidecar) {
18738
+ // No manifest to attribute pages with, so the whole file becomes one unslotted entry. An
18739
+ // `unreadable` file never reaches here — the fallback widens which readable PDFs are
18740
+ // accepted, it does not accept broken ones.
18741
+ const entry = buildPdfMergeEntrySync(asPdfMergeFile(input, config?.fileName), { ...config, slotId: null });
18742
+ result = entry == null ? { error: 'unreadable' } : { entries: [entry], sidecar: null, fromSidecar: false, slotIds: [null], missingTags: [], untaggedPageCount: 0 };
18743
+ }
18744
+ else {
18745
+ result = { error: 'no_sidecar' };
18746
+ }
18747
+ }
18748
+ else if (split.documents.length === 0) {
18749
+ result = { error: 'no_documents' };
18750
+ }
18751
+ else {
18752
+ const entries = split.documents.map((document) => buildPdfMergeEntrySync(document.file, { ...config, slotId: document.slotId })).filter((entry) => entry != null);
18753
+ result = {
18754
+ entries,
18755
+ sidecar: split.sidecar,
18756
+ fromSidecar: true,
18757
+ slotIds: split.documents.map((document) => document.slotId),
18758
+ missingTags: split.missingTags,
18759
+ untaggedPageCount: split.untaggedPageCount
18760
+ };
18761
+ }
18762
+ return result;
18763
+ }
18764
+ async function isReadablePdf(input) {
18765
+ let readable;
18766
+ try {
18767
+ await PDFDocument.load(await input.arrayBuffer());
18768
+ readable = true;
18769
+ }
18770
+ catch {
18771
+ readable = false;
18772
+ }
18773
+ return readable;
18774
+ }
18775
+ function applyPdfMergePageGroupOrder(natural, orderedIds) {
18776
+ let ordered;
18777
+ if (orderedIds == null || orderedIds.length === 0) {
18778
+ ordered = [...natural];
18779
+ }
18780
+ else {
18781
+ const byId = new Map(natural.map((page) => [page.id, page]));
18782
+ const taken = new Set();
18783
+ const result = [];
18784
+ // Stored ids that still resolve keep their stored order; anything else is ignored.
18785
+ orderedIds.forEach((id) => {
18786
+ const page = byId.get(id);
18787
+ if (page != null && !taken.has(id)) {
18788
+ result.push(page);
18789
+ taken.add(id);
18790
+ }
18791
+ });
18792
+ // Pages the stored order does not mention are new since it was written — append them in natural order.
18793
+ natural.forEach((page) => {
18794
+ if (!taken.has(page.id)) {
18795
+ result.push(page);
18796
+ }
18797
+ });
18798
+ ordered = result;
18799
+ }
18800
+ return ordered;
18801
+ }
18802
+ /**
18803
+ * Builds the editor's page plan from the current entries plus the user's sparse edits.
18804
+ *
18805
+ * The result is self-healing: stored ordering ids that no longer resolve are dropped, pages the stored ordering does not mention are appended in natural order, and overrides for departed pages are ignored. That means entries can be added and removed without any page-level bookkeeping.
18806
+ *
18807
+ * Entries whose metadata is `null` contribute no pages — they are encrypted (which `pdf-lib` cannot open) or unparseable. Callers should surface those separately rather than letting them disappear silently.
18808
+ *
18809
+ * @param input - Entries, hydrated metadata, and the stored edits.
18810
+ * @returns The ordered page plan across every group.
18811
+ * @__NO_SIDE_EFFECTS__
18812
+ */
18813
+ function buildPdfMergePagePlan(input) {
18814
+ const { entries, pageMetas, pageOverrides, pageOrder } = input;
18815
+ const eligible = entries.filter((entry) => entry.status === 'ready' && !entry.ignored && pageMetas[entry.id] != null);
18816
+ const groups = makeValuesGroupMap(eligible, (entry) => pdfMergePageGroupKeyForSlotId(entry.slotId));
18817
+ const plan = [];
18818
+ groups.forEach((groupEntries, groupKey) => {
18819
+ const natural = [];
18820
+ groupEntries.forEach((entry) => {
18821
+ const metas = pageMetas[entry.id];
18822
+ metas.forEach((meta) => {
18823
+ const id = makePdfMergePageId(entry.id, meta.sourceIndex);
18824
+ const override = pageOverrides[id];
18825
+ natural.push({
18826
+ id,
18827
+ entryId: entry.id,
18828
+ slotId: entry.slotId ?? null,
18829
+ groupKey: groupKey,
18830
+ sourceName: entry.name,
18831
+ kind: entry.kind,
18832
+ sourceIndex: meta.sourceIndex,
18833
+ sourcePageCount: metas.length,
18834
+ meta,
18835
+ rotation: override?.rotation ?? 0,
18836
+ removed: override?.removed ?? false
18837
+ });
18838
+ });
18839
+ });
18840
+ applyPdfMergePageGroupOrder(natural, pageOrder[groupKey]).forEach((page) => plan.push(page));
18841
+ });
18842
+ return plan;
18843
+ }
18844
+ /**
18845
+ * Returns a loader that parses each entry's source document at most once per merge. A page plan routinely pulls several non-adjacent pages from the same file, so without this the same bytes would be parsed repeatedly.
18846
+ *
18847
+ * @returns Loader that resolves an entry to its parsed source document.
18848
+ */
18849
+ function makePdfSourceDocumentLoader() {
18850
+ const cache = new Map();
18851
+ return (entry) => {
18852
+ let loaded = cache.get(entry.id);
18853
+ if (loaded == null) {
18854
+ loaded = entry.file.arrayBuffer().then((bytes) => PDFDocument.load(bytes));
18855
+ cache.set(entry.id, loaded);
18856
+ }
18857
+ return loaded;
18858
+ };
18371
18859
  }
18372
- async function appendImagePage(target, entry) {
18860
+ async function appendImagePage(target, entry, rotation) {
18373
18861
  const bytes = await entry.file.arrayBuffer();
18374
18862
  const isPng = entry.mimeType === PNG_MIME_TYPE || entry.name.toLowerCase().endsWith('.png');
18375
18863
  const image = isPng ? await target.embedPng(bytes) : await target.embedJpg(bytes);
18376
18864
  const page = target.addPage([image.width, image.height]);
18377
18865
  page.drawImage(image, { x: 0, y: 0, width: image.width, height: image.height });
18866
+ if (rotation !== 0) {
18867
+ page.setRotation(degrees(rotation));
18868
+ }
18869
+ return { entry, sourceIndex: 0, rotation };
18870
+ }
18871
+ /**
18872
+ * Appends every page of every entry, in entry order — the behavior used when no page plan is supplied.
18873
+ *
18874
+ * @param input - Target document, entries, and source loader.
18875
+ * @returns One record per appended page, aligned with the target's pages.
18876
+ */
18877
+ async function appendAllEntryPages(input) {
18878
+ const { target, entries, loadSource } = input;
18879
+ const records = [];
18880
+ for (const entry of entries) {
18881
+ if (entry.kind === 'pdf') {
18882
+ const source = await loadSource(entry);
18883
+ const indices = source.getPageIndices();
18884
+ const copied = await target.copyPages(source, indices);
18885
+ copied.forEach((page, index) => {
18886
+ target.addPage(page);
18887
+ records.push({ entry, sourceIndex: indices[index], rotation: normalizeRotationDegrees(page.getRotation().angle) });
18888
+ });
18889
+ }
18890
+ else {
18891
+ records.push(await appendImagePage(target, entry, 0));
18892
+ }
18893
+ }
18894
+ return records;
18895
+ }
18896
+ /**
18897
+ * Appends pages according to an explicit plan — honoring subsetting, reordering, and per-page rotation.
18898
+ *
18899
+ * Consecutive pages from the same entry are batched into a single `copyPages` call. `copyPages` returns pages in the order of the indices it is given, which is what lets subsetting and reordering happen in one operation.
18900
+ *
18901
+ * @param input - Target document, entries, page plan, and source loader.
18902
+ * @returns One record per appended page, aligned with the target's pages.
18903
+ */
18904
+ async function appendPlannedPages(input) {
18905
+ const { target, entries, plan, loadSource } = input;
18906
+ const entriesById = new Map(entries.map((entry) => [entry.id, entry]));
18907
+ const visible = plan.filter((page) => !page.removed && entriesById.has(page.entryId));
18908
+ const records = [];
18909
+ let index = 0;
18910
+ while (index < visible.length) {
18911
+ const first = visible[index];
18912
+ const entry = entriesById.get(first.entryId);
18913
+ let end = index + 1;
18914
+ while (end < visible.length && visible[end].entryId === first.entryId) {
18915
+ end += 1;
18916
+ }
18917
+ const run = visible.slice(index, end);
18918
+ if (entry.kind === 'image') {
18919
+ for (const planned of run) {
18920
+ records.push(await appendImagePage(target, entry, normalizeRotationDegrees(planned.rotation)));
18921
+ }
18922
+ }
18923
+ else {
18924
+ const source = await loadSource(entry);
18925
+ const copied = await target.copyPages(source, run.map((planned) => planned.sourceIndex));
18926
+ copied.forEach((page, runIndex) => {
18927
+ const planned = run[runIndex];
18928
+ // Compose rather than replace: a scan already stored at 90° that the user rotates by 90° must land at 180°.
18929
+ const rotation = normalizeRotationDegrees(planned.meta.sourceRotation + planned.rotation);
18930
+ target.addPage(page);
18931
+ page.setRotation(degrees(rotation));
18932
+ records.push({ entry, sourceIndex: planned.sourceIndex, rotation });
18933
+ });
18934
+ }
18935
+ index = end;
18936
+ }
18937
+ return records;
18938
+ }
18939
+ /**
18940
+ * Tags each output page and attaches the manifest describing which pages belong to which slot.
18941
+ *
18942
+ * @param target - Document being assembled, whose pages align with `records`.
18943
+ * @param records - One record per appended page, in output order.
18944
+ */
18945
+ async function writePdfMergeSidecarForRecords(target, records) {
18946
+ const ordinalBySlotKey = new Map();
18947
+ const outputPages = target.getPages();
18948
+ const sidecarPages = records.map((record, outputIndex) => {
18949
+ // Normalize to null so the value survives JSON.stringify, which drops undefined.
18950
+ const slotId = record.entry.slotId ?? null;
18951
+ const slotKey = slotId ?? '';
18952
+ const ordinal = ordinalBySlotKey.get(slotKey) ?? 0;
18953
+ const tag = makePdfMergeSidecarPageTag(slotId, ordinal);
18954
+ ordinalBySlotKey.set(slotKey, ordinal + 1);
18955
+ writePdfMergePageTag(outputPages[outputIndex], tag);
18956
+ return { tag, slotId, sourceName: record.entry.name, sourceIndex: record.sourceIndex, rotation: record.rotation, outputIndex };
18957
+ });
18958
+ await attachPdfMergeSidecar(target, makePdfMergeSidecar(sidecarPages));
18378
18959
  }
18379
18960
  /**
18380
18961
  * Merges every `ready` entry in the provided array order into a single PDF and returns it as a `Blob`. PDF entries contribute their full set of pages in order; image entries contribute one page sized to the image.
18381
18962
  *
18382
- * Special case: when the only `ready` entry is a single encrypted PDF, returns a passthrough `Blob` of its original bytes — `pdf-lib` cannot read encrypted PDFs even with `ignoreEncryption: true`, and downstream upload flows still need a usable blob.
18963
+ * Supplying {@link MergePdfMergeEntriesOptions.pages} replaces that default with an explicit page plan, so pages can be subsetted, reordered, and rotated. Supplying {@link MergePdfMergeEntriesOptions.sidecar} additionally embeds a manifest of which pages came from which slot.
18383
18964
  *
18384
- * Throws if no `ready` entries are provided, or if multiple encrypted entries are passed in (the editor only routes here under focus mode where it has narrowed to one).
18965
+ * Special case: when the only `ready` entry is a single encrypted PDF, returns a passthrough `Blob` of its original bytes — `pdf-lib` cannot read encrypted PDFs even with `ignoreEncryption: true`, and downstream upload flows still need a usable blob. Neither the page plan nor the sidecar applies on that path, since the document cannot be opened.
18966
+ *
18967
+ * Throws if no `ready` entries are provided, if multiple encrypted entries are passed in (the editor only routes here under focus mode where it has narrowed to one), or if the page plan leaves no pages to emit.
18385
18968
  *
18386
18969
  * @param entries - Ordered entries to merge.
18970
+ * @param options - Optional page plan and sidecar switch.
18387
18971
  * @returns A Blob with `application/pdf` MIME type.
18388
18972
  */
18389
- async function mergePdfMergeEntries(entries) {
18973
+ async function mergePdfMergeEntries(entries, options) {
18390
18974
  const ready = entries.filter((entry) => entry.status === 'ready');
18391
18975
  let result;
18392
18976
  if (ready.length === 0) {
@@ -18401,13 +18985,14 @@ async function mergePdfMergeEntries(entries) {
18401
18985
  }
18402
18986
  else {
18403
18987
  const target = await PDFDocument.create();
18404
- for (const entry of ready) {
18405
- if (entry.kind === 'pdf') {
18406
- await appendPdfPages(target, entry);
18407
- }
18408
- else {
18409
- await appendImagePage(target, entry);
18410
- }
18988
+ const loadSource = makePdfSourceDocumentLoader();
18989
+ const plan = options?.pages;
18990
+ const records = plan == null ? await appendAllEntryPages({ target, entries: ready, loadSource }) : await appendPlannedPages({ target, entries: ready, plan, loadSource });
18991
+ if (records.length === 0) {
18992
+ throw new Error('No pages to merge.');
18993
+ }
18994
+ if (options?.sidecar) {
18995
+ await writePdfMergeSidecarForRecords(target, records);
18411
18996
  }
18412
18997
  const bytes = await target.save();
18413
18998
  result = new Blob([bytes], { type: PDF_MERGE_RESULT_MIME_TYPE });
@@ -18416,11 +19001,39 @@ async function mergePdfMergeEntries(entries) {
18416
19001
  }
18417
19002
 
18418
19003
  /**
18419
- * Initial state used by {@link DbxPdfMergeEditorStore} — no entries.
19004
+ * Initial state used by {@link DbxPdfMergeEditorStore} — no entries and no page edits.
18420
19005
  */
18421
19006
  const DBX_PDF_MERGE_EDITOR_INITIAL_STATE = {
18422
- rawEntries: []
19007
+ rawEntries: [],
19008
+ pageOverrides: {},
19009
+ pageOrder: {}
18423
19010
  };
19011
+ /**
19012
+ * Drops page overrides and stored orderings that belong to entries no longer in the list.
19013
+ *
19014
+ * The derived plan already ignores stale ids, so this is hygiene rather than correctness — it keeps the two records from growing without bound as the user adds and removes files.
19015
+ *
19016
+ * @param state - Current state.
19017
+ * @param liveEntryIds - Ids of the entries that remain.
19018
+ * @returns The pruned page-state slice.
19019
+ * @__NO_SIDE_EFFECTS__
19020
+ */
19021
+ function prunePdfMergePageState(state, liveEntryIds) {
19022
+ const pageOverrides = {};
19023
+ const pageOrder = {};
19024
+ Object.entries(state.pageOverrides).forEach(([pageId, override]) => {
19025
+ if (liveEntryIds.has(entryIdForPdfMergePageId(pageId))) {
19026
+ pageOverrides[pageId] = override;
19027
+ }
19028
+ });
19029
+ Object.entries(state.pageOrder).forEach(([groupKey, pageIds]) => {
19030
+ const kept = pageIds.filter((pageId) => liveEntryIds.has(entryIdForPdfMergePageId(pageId)));
19031
+ if (kept.length > 0) {
19032
+ pageOrder[groupKey] = kept;
19033
+ }
19034
+ });
19035
+ return { pageOverrides, pageOrder };
19036
+ }
18424
19037
  /**
18425
19038
  * Component-scoped {@link ComponentStore} that owns the list of files staged for merging in the PDF merge editor. Each {@link PdfMergeEntry} carries its own validation promise from the moment it is built; {@link entries$} composes those promises into a live stream — emitting the entry first in `validating` state and then again as each promise resolves to `ready` or `error`. {@link mergeOutput$} emits the merged PDF blob once every entry has finished validating, at least one is `ready`, and the registered validator delegate (if any) reports `true`.
18426
19039
  */
@@ -18429,6 +19042,22 @@ class DbxPdfMergeEditorStore extends ComponentStore {
18429
19042
  _outputSizeLimit$ = new BehaviorSubject(undefined);
18430
19043
  _imageCompression$ = new BehaviorSubject(undefined);
18431
19044
  _encryptedHandling$ = new BehaviorSubject(undefined);
19045
+ _pageEditing$ = new BehaviorSubject(undefined);
19046
+ _sidecar$ = new BehaviorSubject(undefined);
19047
+ /**
19048
+ * Per-entry page metadata, parsed at most once per entry. Keyed by entry id; a resolved `null` means the entry could not be expanded. Only populated while page editing is enabled.
19049
+ */
19050
+ _pageMetaCache = new Map();
19051
+ /**
19052
+ * Mount count per registered slot id. See {@link registerSlotId} for why this is a count rather than a set.
19053
+ */
19054
+ _slotIdCounts = new Map();
19055
+ _registeredSlotIds$ = new BehaviorSubject([]);
19056
+ _importState$ = new BehaviorSubject(undefined);
19057
+ /**
19058
+ * Input of the most recent successful `programmatic` import, replayed by {@link clearEntries} to reset the editor to the document the app supplied. `null` until one succeeds; a user's pick never overwrites it.
19059
+ */
19060
+ _restorableImport;
18432
19061
  constructor() {
18433
19062
  super(DBX_PDF_MERGE_EDITOR_INITIAL_STATE);
18434
19063
  }
@@ -18496,28 +19125,123 @@ class DbxPdfMergeEditorStore extends ComponentStore {
18496
19125
  */
18497
19126
  encryptedEntries$ = this.entries$.pipe(map((entries) => entries.filter((entry) => entry.encrypted && entry.status === 'ready')), shareReplay(1));
18498
19127
  hasReadyEntries$ = this.displayEntries$.pipe(map((entries) => entries.some((entry) => entry.status === 'ready' && !entry.ignored)), distinctUntilChanged(), shareReplay(1));
19128
+ // MARK: Page editing
18499
19129
  /**
18500
- * Emits `true` while any entry's validation promise has not yet resolved (i.e. one or more entries are still in `validating` status). Reads from {@link entries$} (not {@link displayEntries$}) so validation gating ignores the `ignored`/`error` projection done by encryption handling.
19130
+ * Raw {@link DbxPdfMergeEditorConfig.pageEditing} value pushed onto the store, before defaulting. Consumed by the editor as the middle tier of its resolution chain (own `[config]` input → store → {@link DBX_PDF_MERGE_EDITOR_CONFIG} token), so a store-level default does not shadow the token.
18501
19131
  */
18502
- isValidating$ = this.entries$.pipe(map((entries) => entries.some((entry) => entry.status === 'validating')), distinctUntilChanged(), shareReplay(1));
19132
+ pageEditingSetting$ = this._pageEditing$.asObservable();
18503
19133
  /**
18504
- * Emits the boolean output of the registered {@link DbxPdfMergeEditorValidator} delegate, or a constant `true` when no delegate is registered. Composed with {@link sizeLimitValid$} into {@link isValid$} to gate {@link currentMergeOutput$}.
19134
+ * Whether page editing is active, defaulted to {@link DEFAULT_DBX_PDF_MERGE_PAGE_EDITING}. Every page-related stream is inert while this is `false`.
18505
19135
  */
18506
- validatorValid$ = this._validator$.pipe(switchMap((validator) => (validator ? validator(this.entries$) : of(true))), distinctUntilChanged(), shareReplay(1));
19136
+ pageEditing$ = this.pageEditingSetting$.pipe(map((pageEditing) => pageEditing ?? DEFAULT_DBX_PDF_MERGE_PAGE_EDITING), distinctUntilChanged(), shareReplay(1));
18507
19137
  /**
18508
- * Internal pre-validity merge stream produced without consulting {@link isValid$}. Drives both {@link outputSize$} and the eventual {@link currentMergeOutput$} so size-based gating can observe the would-be blob without creating a cycle. Consumes {@link displayEntries$} so the merge respects the active {@link DbxPdfMergeEncryptedHandling} (encrypted-focused entries pass through, ignored entries are dropped, `error` mode demotions are honored).
19138
+ * Raw {@link DbxPdfMergeEditorConfig.sidecar} value pushed onto the store, before defaulting.
18509
19139
  */
18510
- _candidateMergeOutput$ = combineLatest([this.displayEntries$, this.isValidating$, this.validatorValid$]).pipe(switchMap(([entries, isValidating, validatorValid]) => {
18511
- const mergeable = entries.filter((entry) => !entry.ignored);
18512
- const hasReady = mergeable.some((entry) => entry.status === 'ready');
19140
+ sidecarSetting$ = this._sidecar$.asObservable();
19141
+ /**
19142
+ * Whether the merged output should carry an embedded manifest, defaulted to {@link DEFAULT_DBX_PDF_MERGE_SIDECAR}. Independent of {@link pageEditing$}.
19143
+ */
19144
+ sidecar$ = this.sidecarSetting$.pipe(map((sidecar) => sidecar ?? DEFAULT_DBX_PDF_MERGE_SIDECAR), distinctUntilChanged(), shareReplay(1));
19145
+ /**
19146
+ * Hydrated page metadata keyed by entry id.
19147
+ *
19148
+ * Short-circuits to an empty record while page editing is off, so the default configuration never parses a source document — the upload path's cheap header scan stays the only inspection performed. Each entry is parsed at most once thanks to {@link _pageMetaCache}, and metadata for departed entries is pruned on each pass.
19149
+ */
19150
+ pageMetas$ = combineLatest([this.entries$, this.pageEditing$]).pipe(switchMap(([entries, pageEditing]) => {
19151
+ const ready = pageEditing ? entries.filter((entry) => entry.status === 'ready') : [];
18513
19152
  let next$;
18514
- if (isValidating || !hasReady || !validatorValid) {
18515
- next$ = of(undefined);
19153
+ if (ready.length === 0) {
19154
+ next$ = of({});
18516
19155
  }
18517
19156
  else {
18518
- next$ = from(mergePdfMergeEntries(mergeable)).pipe(catchError(() => of(undefined)));
18519
- }
18520
- return next$;
19157
+ const readyIds = new Set(ready.map((entry) => entry.id));
19158
+ Array.from(this._pageMetaCache.keys()).forEach((entryId) => {
19159
+ if (!readyIds.has(entryId)) {
19160
+ this._pageMetaCache.delete(entryId);
19161
+ }
19162
+ });
19163
+ const loaded = ready.map(async (entry) => {
19164
+ let metas = this._pageMetaCache.get(entry.id);
19165
+ if (metas == null) {
19166
+ metas = readPdfMergeEntryPageMetas(entry);
19167
+ this._pageMetaCache.set(entry.id, metas);
19168
+ }
19169
+ return [entry.id, await metas];
19170
+ });
19171
+ next$ = from(Promise.all(loaded)).pipe(map((pairs) => Object.fromEntries(pairs)));
19172
+ }
19173
+ return next$;
19174
+ }), shareReplay(1));
19175
+ /**
19176
+ * The editor's ordered page plan, or `null` while page editing is disabled. Passing `null` through to the merge is what makes the default path emit every page of every entry exactly as it did before page editing existed.
19177
+ */
19178
+ pages$ = combineLatest([this.displayEntries$, this.pageEditing$, this.pageMetas$, this.select((state) => state.pageOverrides), this.select((state) => state.pageOrder)]).pipe(map(([entries, pageEditing, pageMetas, pageOverrides, pageOrder]) => {
19179
+ let pages;
19180
+ if (pageEditing) {
19181
+ pages = buildPdfMergePagePlan({ entries, pageMetas, pageOverrides, pageOrder });
19182
+ }
19183
+ else {
19184
+ pages = null;
19185
+ }
19186
+ return pages;
19187
+ }), shareReplay(1));
19188
+ /**
19189
+ * The page plan split into the groups the UI renders, one per slot plus one for unslotted entries. Empty while page editing is disabled.
19190
+ */
19191
+ pageGroups$ = this.pages$.pipe(map((pages) => {
19192
+ const grouped = makeValuesGroupMap(pages ?? [], (page) => page.groupKey);
19193
+ const groups = [];
19194
+ grouped.forEach((groupPages, groupKey) => {
19195
+ groups.push({ groupKey: groupKey, slotId: groupPages[0]?.slotId ?? null, pages: groupPages });
19196
+ });
19197
+ return groups;
19198
+ }), shareReplay(1));
19199
+ /**
19200
+ * Entries that are ready to merge but whose pages could not be listed — encrypted documents (which `pdf-lib` cannot open) and anything unparseable. Surfaced so the UI can explain why those rows are not expandable instead of letting them silently vanish from the page list. Always empty while page editing is disabled.
19201
+ */
19202
+ unexpandableEntries$ = combineLatest([this.displayEntries$, this.pageEditing$, this.pageMetas$]).pipe(map(([entries, pageEditing, pageMetas]) => (pageEditing ? entries.filter((entry) => entry.status === 'ready' && !entry.ignored && pageMetas[entry.id] === null) : [])), shareReplay(1));
19203
+ /**
19204
+ * Number of pages that will reach the merged output — the plan minus anything marked for removal. Zero while page editing is disabled, where the count is not meaningful.
19205
+ */
19206
+ mergeablePageCount$ = this.pages$.pipe(map((pages) => (pages ?? []).filter((page) => !page.removed).length), distinctUntilChanged(), shareReplay(1));
19207
+ /**
19208
+ * Whether at least one page survives the user's edits. Emits `true` while page editing is disabled so it never gates the default path.
19209
+ */
19210
+ hasMergeablePages$ = this.pages$.pipe(map((pages) => pages == null || pages.some((page) => !page.removed)), distinctUntilChanged(), shareReplay(1));
19211
+ /**
19212
+ * Returns the pages belonging to one group, for a slot rendering its own pages inline.
19213
+ *
19214
+ * @param slotId - Slot identifier, or `null` for the unslotted group.
19215
+ * @returns Observable of that group's ordered pages.
19216
+ */
19217
+ pagesForSlotId$(slotId) {
19218
+ const groupKey = pdfMergePageGroupKeyForSlotId(slotId);
19219
+ return this.pages$.pipe(map((pages) => (pages ?? []).filter((page) => page.groupKey === groupKey)), shareReplay(1));
19220
+ }
19221
+ /**
19222
+ * Emits `true` while any entry's validation promise has not yet resolved (i.e. one or more entries are still in `validating` status). Reads from {@link entries$} (not {@link displayEntries$}) so validation gating ignores the `ignored`/`error` projection done by encryption handling.
19223
+ */
19224
+ isValidating$ = this.entries$.pipe(map((entries) => entries.some((entry) => entry.status === 'validating')), distinctUntilChanged(), shareReplay(1));
19225
+ /**
19226
+ * Emits the boolean output of the registered {@link DbxPdfMergeEditorValidator} delegate, or a constant `true` when no delegate is registered. Composed with {@link sizeLimitValid$} into {@link isValid$} to gate {@link currentMergeOutput$}.
19227
+ */
19228
+ validatorValid$ = this._validator$.pipe(switchMap((validator) => (validator ? validator(this.entries$) : of(true))), distinctUntilChanged(), shareReplay(1));
19229
+ /**
19230
+ * Internal pre-validity merge stream produced without consulting {@link isValid$}. Drives both {@link outputSize$} and the eventual {@link currentMergeOutput$} so size-based gating can observe the would-be blob without creating a cycle. Consumes {@link displayEntries$} so the merge respects the active {@link DbxPdfMergeEncryptedHandling} (encrypted-focused entries pass through, ignored entries are dropped, `error` mode demotions are honored).
19231
+ */
19232
+ _candidateMergeOutput$ = combineLatest([this.displayEntries$, this.isValidating$, this.validatorValid$, this.pages$, this.sidecar$]).pipe(switchMap(([entries, isValidating, validatorValid, pages, sidecar]) => {
19233
+ const mergeable = entries.filter((entry) => !entry.ignored);
19234
+ const hasReady = mergeable.some((entry) => entry.status === 'ready');
19235
+ // A `null` plan means page editing is off, in which case the merge takes its original every-page path.
19236
+ const hasPages = pages == null || pages.some((page) => !page.removed);
19237
+ let next$;
19238
+ if (isValidating || !hasReady || !validatorValid || !hasPages) {
19239
+ next$ = of(undefined);
19240
+ }
19241
+ else {
19242
+ next$ = from(mergePdfMergeEntries(mergeable, { pages, sidecar })).pipe(catchError(() => of(undefined)));
19243
+ }
19244
+ return next$;
18521
19245
  }), shareReplay(1));
18522
19246
  /**
18523
19247
  * Emits the byte size of the most recent candidate merge output, or `undefined` while there is none.
@@ -18537,9 +19261,11 @@ class DbxPdfMergeEditorStore extends ComponentStore {
18537
19261
  return valid;
18538
19262
  }), distinctUntilChanged(), shareReplay(1));
18539
19263
  /**
18540
- * Emits `true` while both the registered {@link DbxPdfMergeEditorValidator} delegate (if any) and the optional output-size limit are satisfied. {@link currentMergeOutput$} gates merge emissions on this stream.
19264
+ * Emits `true` while the registered {@link DbxPdfMergeEditorValidator} delegate (if any), the optional output-size limit, and the page plan are all satisfied. {@link currentMergeOutput$} gates merge emissions on this stream.
19265
+ *
19266
+ * The page-plan term is `true` whenever page editing is disabled, so this stream is unchanged on the default path. With page editing on it blocks the merge once the user has marked every page for deletion.
18541
19267
  */
18542
- isValid$ = combineLatest([this.validatorValid$, this.sizeLimitValid$]).pipe(map(([validatorValid, sizeLimitValid]) => validatorValid && sizeLimitValid), distinctUntilChanged(), shareReplay(1));
19268
+ isValid$ = combineLatest([this.validatorValid$, this.sizeLimitValid$, this.hasMergeablePages$]).pipe(map(([validatorValid, sizeLimitValid, hasMergeablePages]) => validatorValid && sizeLimitValid && hasMergeablePages), distinctUntilChanged(), shareReplay(1));
18543
19269
  /**
18544
19270
  * Emits the merged PDF blob whenever every entry has finished validating (see {@link isValidating$}), at least one is `ready`, and {@link isValid$} reports `true`. Emits `undefined` while validation is in flight, when the list is empty, when the delegate or size limit reports invalid, or when the most recent merge failed.
18545
19271
  */
@@ -18558,6 +19284,176 @@ class DbxPdfMergeEditorStore extends ComponentStore {
18558
19284
  entriesForSlotId$(slotId) {
18559
19285
  return this.displayEntries$.pipe(map((entries) => entries.filter((entry) => entry.slotId === slotId)), shareReplay(1));
18560
19286
  }
19287
+ // MARK: Slot registry
19288
+ /**
19289
+ * Slot ids currently mounted against this store, i.e. the sections this editor declares.
19290
+ *
19291
+ * This is what makes `<dbx-pdf-merge-import>` a zero-configuration drop-in: it derives the set of sections a re-imported file is allowed to name (and whether it renders at all) from the slots actually on screen, rather than from a hand-maintained list duplicating them.
19292
+ *
19293
+ * Emitted **sorted ascending**, which is deterministic but is NOT declaration order. Sorting is what lets the guard below collapse slot churn: a set cycling through `@if` (`['a','b']` → `['b']` → `['b','a']`) ends up equal to the array it started with, where registration order would emit a spuriously different array for an identical set.
19294
+ *
19295
+ * Registered by {@link DbxPdfMergeEditorFileUploadComponent} from its own lifecycle. Kept here rather than on the optional {@link DbxPdfMergeEditorFileUploadValidatorDirective} because it must be available whether or not a validator is present — and because a separately-provided registry could be provided at a different level than the store, letting slots register with one instance while a reader sees another.
19296
+ */
19297
+ registeredSlotIds$ = this._registeredSlotIds$.pipe(distinctUntilChanged((a, b) => a.length === b.length && a.every((slotId, i) => slotId === b[i])), shareReplay(1));
19298
+ /**
19299
+ * Registers a slot id as mounted, adding it to {@link registeredSlotIds$}.
19300
+ *
19301
+ * Reference-counted rather than a set: the same `slotId` can legitimately be mounted twice at once — the PDF merge upload dialog hosts its slots against an ancestor store while inline slots may still exist under {@link DBX_PDF_MERGE_EDITOR_PRESERVE_ENTRIES_ON_SLOT_DESTROY} — and one unmount must not deregister a still-mounted id.
19302
+ *
19303
+ * @param slotId - Slot identifier being mounted.
19304
+ */
19305
+ registerSlotId(slotId) {
19306
+ this._slotIdCounts.set(slotId, (this._slotIdCounts.get(slotId) ?? 0) + 1);
19307
+ this._emitRegisteredSlotIds();
19308
+ }
19309
+ /**
19310
+ * Deregisters one mount of a slot id, removing it from {@link registeredSlotIds$} once the last mount is gone. Unknown ids are ignored.
19311
+ *
19312
+ * Callers must pair this with their own {@link registerSlotId} exactly once. The store frequently outlives its slots (a dialog-hosted slot is destroyed on every close while the page-level store lives on), so a missed call leaves the editor permanently claiming a section that is not on screen.
19313
+ *
19314
+ * @param slotId - Slot identifier being unmounted.
19315
+ */
19316
+ unregisterSlotId(slotId) {
19317
+ const count = this._slotIdCounts.get(slotId);
19318
+ if (count != null) {
19319
+ if (count > 1) {
19320
+ this._slotIdCounts.set(slotId, count - 1);
19321
+ }
19322
+ else {
19323
+ this._slotIdCounts.delete(slotId);
19324
+ }
19325
+ this._emitRegisteredSlotIds();
19326
+ }
19327
+ }
19328
+ _emitRegisteredSlotIds() {
19329
+ const next = Array.from(this._slotIdCounts.keys()).sort();
19330
+ const current = this._registeredSlotIds$.value;
19331
+ if (next.length !== current.length || next.some((slotId, i) => slotId !== current[i])) {
19332
+ this._registeredSlotIds$.next(next);
19333
+ }
19334
+ }
19335
+ // MARK: Import
19336
+ /**
19337
+ * Lifecycle of the most recent {@link importMergedPdf} call, or `undefined` before the first. Lets a consumer that triggered a programmatic import observe its outcome — the picker component is not the only entry point.
19338
+ */
19339
+ importState$ = this._importState$.asObservable();
19340
+ /**
19341
+ * The most recent successful import, or `null` while none has succeeded.
19342
+ */
19343
+ importResult$ = this.importState$.pipe(map((state) => (state?.status === 'imported' ? state.result : null)), distinctUntilChanged(), shareReplay(1));
19344
+ /**
19345
+ * Why the most recent import failed, or `null` when it did not.
19346
+ */
19347
+ importError$ = this.importState$.pipe(map((state) => (state?.status === 'failed' ? state.error : null)), distinctUntilChanged(), shareReplay(1));
19348
+ /**
19349
+ * Emits `true` while an import is in flight.
19350
+ */
19351
+ isImporting$ = this.importState$.pipe(map((state) => state?.status === 'importing'), distinctUntilChanged(), shareReplay(1));
19352
+ /**
19353
+ * Whether a programmatic baseline exists for {@link clearEntries} to restore. Lets a consumer word its own confirmation honestly — "reset to the imported document" rather than "remove everything".
19354
+ *
19355
+ * @returns `true` once a programmatic import has succeeded.
19356
+ */
19357
+ hasRestorableImport() {
19358
+ return this._restorableImport != null;
19359
+ }
19360
+ /**
19361
+ * Imports a previously-exported merged PDF, replacing every entry currently in the store with the per-slot documents recorded in its manifest.
19362
+ *
19363
+ * This is the programmatic equivalent of what {@link DbxPdfMergeImportComponent} does with a picked file, and that component delegates here — so a stored document loaded by an app and a file chosen by a user go through one implementation with one set of error semantics. Progress and outcome are published on {@link importState$} as well as returned.
19364
+ *
19365
+ * @param input - The source blob plus the optional slot check and sidecar-less fallback.
19366
+ * @returns The terminal state of this import.
19367
+ */
19368
+ async importMergedPdf(input) {
19369
+ const { source, expectedSlotIds, allowWithoutSidecar, fileName } = input;
19370
+ const origin = input.origin ?? 'programmatic';
19371
+ const config = { allowWithoutSidecar, fileName };
19372
+ this._importState$.next({ status: 'importing', origin });
19373
+ const outcome = await buildPdfMergeEntriesFromSidecar(source, config);
19374
+ let state;
19375
+ if ('error' in outcome) {
19376
+ state = { status: 'failed', origin, error: outcome.error };
19377
+ }
19378
+ else {
19379
+ // An unsectioned document counts as unexpected too: in a slots-only editor it would render
19380
+ // nowhere, so rejecting it beats importing pages the user can never see again.
19381
+ const unexpected = expectedSlotIds == null ? [] : outcome.slotIds.filter((slotId) => slotId == null || !expectedSlotIds.includes(slotId));
19382
+ if (unexpected.length > 0) {
19383
+ state = { status: 'failed', origin, error: 'unexpected_slots', unexpectedSlotIds: unexpected };
19384
+ }
19385
+ else {
19386
+ const missingSlotIds = expectedSlotIds == null ? [] : expectedSlotIds.filter((slotId) => !outcome.slotIds.includes(slotId));
19387
+ this.replaceEntries(outcome.entries);
19388
+ state = { status: 'imported', origin, result: { ...outcome, missingSlotIds } };
19389
+ // A programmatic import is the app telling the editor what it should be showing, so it
19390
+ // becomes the restore point. A user's pick deliberately does NOT overwrite it: the person
19391
+ // replaced what is on screen, not what the app configured.
19392
+ if (origin === 'programmatic') {
19393
+ this._restorableImport = input;
19394
+ }
19395
+ }
19396
+ }
19397
+ this._importState$.next(state);
19398
+ return state;
19399
+ }
19400
+ /**
19401
+ * Discards the current import lifecycle, so consumers rendering its notices (the picker's "Imported N section(s)" success line, its missing-section warning, its error) fall back to showing nothing.
19402
+ *
19403
+ * Separate from the entry list — this clears the *record* of an import, not its contents.
19404
+ */
19405
+ clearImportState() {
19406
+ this._importState$.next(undefined);
19407
+ }
19408
+ /**
19409
+ * Empties the editor, or resets it to its programmatic baseline when one exists.
19410
+ *
19411
+ * This is the "Clear" affordance's operation, and it deliberately is not a plain {@link clearAll}. When the surrounding app supplied the document (a programmatic {@link importMergedPdf}, or `[source]` on {@link DbxPdfMergeEditorStoreDirective}), emptying the editor would strand it in a state the app never asked for and has no obvious way to notice. Instead the original import is re-run, landing the view exactly where it was when the document first arrived — page edits, section removals, and any user-picked file on top of it all discarded.
19412
+ *
19413
+ * With no baseline — the ordinary case, including an editor whose only import came from {@link DbxPdfMergeImportComponent} — the entries and the import state both go, so no stale success/warning notice outlives the content it described.
19414
+ *
19415
+ * @param input - Optional override for the restore behavior.
19416
+ * @returns Whether the baseline was restored, plus the re-import's terminal state when one ran.
19417
+ */
19418
+ async clearEntries(input) {
19419
+ const restorable = this._restorableImport;
19420
+ const shouldRestore = (input?.restoreImport ?? true) && restorable != null;
19421
+ let result;
19422
+ if (shouldRestore) {
19423
+ const importState = await this.importMergedPdf(restorable);
19424
+ // A baseline that no longer imports (unreadable retained blob) must not leave the old
19425
+ // entries sitting there — the user asked for a clear and got neither.
19426
+ if (importState.status === 'imported') {
19427
+ result = { restored: true, importState };
19428
+ }
19429
+ else {
19430
+ this.clearAll();
19431
+ result = { restored: false, importState };
19432
+ }
19433
+ }
19434
+ else {
19435
+ this.clearAll();
19436
+ this.clearImportState();
19437
+ result = { restored: false };
19438
+ }
19439
+ return result;
19440
+ }
19441
+ /**
19442
+ * Adds a single document to a slot, without any sidecar involvement — the programmatic equivalent of a user dropping one file onto a slot uploader. Existing entries are left alone.
19443
+ *
19444
+ * Unlike {@link importMergedPdf} this neither reads nor requires a manifest; it is the plain "put this file in that slot" primitive, for e.g. an admin appending one document to an existing set. The entry can be added before its slot mounts — {@link entriesForSlotId$} picks it up when the slot renders.
19445
+ *
19446
+ * @param input - The blob or file, its target slot, and an optional name for a bare blob.
19447
+ * @returns The entry that was added, or `null` when the file is not a supported PDF/PNG/JPEG (in which case nothing was added).
19448
+ */
19449
+ async addFileToSlot(input) {
19450
+ const { file, slotId, fileName, imageCompression } = input;
19451
+ const entry = await buildPdfMergeEntry(asPdfMergeFile(file, fileName), { slotId, imageCompression: imageCompression ?? this._imageCompression$.value });
19452
+ if (entry != null) {
19453
+ this.addFiles({ entries: [entry] });
19454
+ }
19455
+ return entry;
19456
+ }
18561
19457
  // MARK: Validator
18562
19458
  /**
18563
19459
  * Registers a {@link DbxPdfMergeEditorValidator} delegate that gates merge emissions. Only one delegate is active at a time — calling this replaces any previously registered delegate.
@@ -18597,6 +19493,22 @@ class DbxPdfMergeEditorStore extends ComponentStore {
18597
19493
  setEncryptedHandling(handling) {
18598
19494
  this._encryptedHandling$.next(handling ?? undefined);
18599
19495
  }
19496
+ /**
19497
+ * Enables or disables page editing, exposed via {@link pageEditing$}. Turning it on expands each ready entry into its individual pages; turning it off returns the editor to its file-granular behavior while preserving any page edits in state, so toggling back on restores them.
19498
+ *
19499
+ * @param pageEditing - Whether page editing is active, or a falsy value to clear and fall back to {@link DEFAULT_DBX_PDF_MERGE_PAGE_EDITING}.
19500
+ */
19501
+ setPageEditing(pageEditing) {
19502
+ this._pageEditing$.next(pageEditing ?? undefined);
19503
+ }
19504
+ /**
19505
+ * Enables or disables the embedded manifest, exposed via {@link sidecar$}.
19506
+ *
19507
+ * @param sidecar - Whether to embed the manifest, or a falsy value to clear and fall back to {@link DEFAULT_DBX_PDF_MERGE_SIDECAR}.
19508
+ */
19509
+ setSidecar(sidecar) {
19510
+ this._sidecar$.next(sidecar ?? undefined);
19511
+ }
18600
19512
  // MARK: Updaters
18601
19513
  /**
18602
19514
  * Appends entries (already constructed) or builds them from raw files and appends them to state. Each entry's validation promise starts when the entry is built; {@link entries$} reflects each result as it resolves. When `input` is an object with `files` and `slotId`, the resulting entries are tagged with that slot id. When `input` is `{ entries }`, the entries are appended as-is — use this shape for entries that went through async client-side compression upstream.
@@ -18619,11 +19531,17 @@ class DbxPdfMergeEditorStore extends ComponentStore {
18619
19531
  }
18620
19532
  return newEntries.length > 0 ? { ...state, rawEntries: [...state.rawEntries, ...newEntries] } : state;
18621
19533
  });
18622
- removeEntry = this.updater((state, id) => ({ ...state, rawEntries: state.rawEntries.filter((entry) => entry.id !== id) }));
19534
+ removeEntry = this.updater((state, id) => {
19535
+ const rawEntries = state.rawEntries.filter((entry) => entry.id !== id);
19536
+ return { ...state, rawEntries, ...prunePdfMergePageState(state, new Set(rawEntries.map((entry) => entry.id))) };
19537
+ });
18623
19538
  /**
18624
19539
  * Removes every entry whose `slotId` matches the given id. Used by {@link DbxPdfMergeEditorFileUploadComponent} on destroy so a slot's entries leave with it.
18625
19540
  */
18626
- removeEntriesBySlotId = this.updater((state, slotId) => ({ ...state, rawEntries: state.rawEntries.filter((entry) => entry.slotId !== slotId) }));
19541
+ removeEntriesBySlotId = this.updater((state, slotId) => {
19542
+ const rawEntries = state.rawEntries.filter((entry) => entry.slotId !== slotId);
19543
+ return { ...state, rawEntries, ...prunePdfMergePageState(state, new Set(rawEntries.map((entry) => entry.id))) };
19544
+ });
18627
19545
  moveEntry = this.updater((state, move) => {
18628
19546
  let nextState;
18629
19547
  if (move.previousIndex === move.currentIndex) {
@@ -18663,7 +19581,49 @@ class DbxPdfMergeEditorStore extends ComponentStore {
18663
19581
  }
18664
19582
  return nextState;
18665
19583
  });
18666
- clearAll = this.updater((state) => ({ ...state, rawEntries: [] }));
19584
+ clearAll = this.updater((state) => ({ ...state, rawEntries: [], pageOverrides: {}, pageOrder: {} }));
19585
+ /**
19586
+ * Replaces the entire entry list, discarding any page edits.
19587
+ *
19588
+ * Used by the sidecar import path, which reconstructs a fresh set of slot-tagged entries from a previously-exported file. Done as a single state change so subscribers never observe an intermediate empty list.
19589
+ */
19590
+ replaceEntries = this.updater((state, entries) => ({ ...state, rawEntries: [...entries], pageOverrides: {}, pageOrder: {} }));
19591
+ // MARK: Page updaters
19592
+ /**
19593
+ * Reorders a page inside its own group. Groups never exchange pages — the page list renders one CDK drop list per group, so a cross-group drag is impossible to express.
19594
+ *
19595
+ * The move rewrites the id array the caller rendered rather than recomputing it from state, keeping the stored order aligned with what the user saw.
19596
+ */
19597
+ movePageWithinGroup = this.updater((state, move) => {
19598
+ let nextState;
19599
+ if (move.previousIndex === move.currentIndex || move.previousIndex < 0 || move.currentIndex < 0 || move.previousIndex >= move.pageIds.length || move.currentIndex >= move.pageIds.length) {
19600
+ nextState = state;
19601
+ }
19602
+ else {
19603
+ const pageIds = [...move.pageIds];
19604
+ moveItemInArray(pageIds, move.previousIndex, move.currentIndex);
19605
+ nextState = { ...state, pageOrder: { ...state.pageOrder, [move.groupKey]: pageIds } };
19606
+ }
19607
+ return nextState;
19608
+ });
19609
+ /**
19610
+ * Sets a page's rotation. The value is composed onto the page's own source rotation at merge time, so this is the rotation the user applied rather than the page's absolute orientation.
19611
+ */
19612
+ setPageRotation = this.updater((state, change) => {
19613
+ const current = state.pageOverrides[change.pageId];
19614
+ return { ...state, pageOverrides: { ...state.pageOverrides, [change.pageId]: { rotation: change.rotation, removed: current?.removed ?? false } } };
19615
+ });
19616
+ /**
19617
+ * Marks a page for deletion or restores it. Marked pages stay in the list so the choice can be undone, but are excluded from the merged output.
19618
+ */
19619
+ setPageRemoved = this.updater((state, change) => {
19620
+ const current = state.pageOverrides[change.pageId];
19621
+ return { ...state, pageOverrides: { ...state.pageOverrides, [change.pageId]: { rotation: current?.rotation ?? 0, removed: change.removed } } };
19622
+ });
19623
+ /**
19624
+ * Discards every page edit, returning the plan to natural order with no rotations or deletions. Leaves the entries themselves untouched.
19625
+ */
19626
+ clearPageEdits = this.updater((state) => ({ ...state, pageOverrides: {}, pageOrder: {} }));
18667
19627
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: DbxPdfMergeEditorStore, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
18668
19628
  static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: DbxPdfMergeEditorStore });
18669
19629
  }
@@ -18688,6 +19648,19 @@ class DbxPdfMergeEditorStoreDirective {
18688
19648
  _injectedConfig = inject(DBX_PDF_MERGE_EDITOR_CONFIG, { optional: true });
18689
19649
  config = input(...(ngDevMode ? [undefined, { debugName: "config" }] : /* istanbul ignore next */ []));
18690
19650
  outputSizeLimit = input(...(ngDevMode ? [undefined, { debugName: "outputSizeLimit" }] : /* istanbul ignore next */ []));
19651
+ /**
19652
+ * A previously-exported merged PDF to load into the store, for an app that fetches a stored document itself and wants the editor to open already populated — no picker involved.
19653
+ *
19654
+ * Imported once per distinct blob instance: re-emitting the same object does nothing, so a template re-render or an unrelated input change cannot re-import. Emitting a *different* blob imports again and replaces the entries, even if the bytes are identical.
19655
+ *
19656
+ * Watch {@link DbxPdfMergeEditorStore.importState$} for the outcome — a failure surfaces there rather than throwing.
19657
+ */
19658
+ source = input(...(ngDevMode ? [undefined, { debugName: "source" }] : /* istanbul ignore next */ []));
19659
+ sourceConfig = input(...(ngDevMode ? [undefined, { debugName: "sourceConfig" }] : /* istanbul ignore next */ []));
19660
+ /**
19661
+ * The blob instance most recently handed to the store, so the same one is never imported twice.
19662
+ */
19663
+ _importedSource;
18691
19664
  effectiveErrorBytesSignal = computed(() => {
18692
19665
  const direct = this.outputSizeLimit();
18693
19666
  if (direct != null) {
@@ -18719,9 +19692,30 @@ class DbxPdfMergeEditorStoreDirective {
18719
19692
  effect(() => {
18720
19693
  this.store.setEncryptedHandling(this.config()?.encryptedHandling);
18721
19694
  });
19695
+ // Same channel for pageEditing, so a dialog-hosted editor opens straight into page
19696
+ // editing mode when the host directive asked for it.
19697
+ effect(() => {
19698
+ this.store.setPageEditing(this.config()?.pageEditing);
19699
+ });
19700
+ // Same channel for the embedded manifest. Gated separately from pageEditing — a plain
19701
+ // slot merge benefits from recording which pages came from which slot too.
19702
+ effect(() => {
19703
+ this.store.setSidecar(this.config()?.sidecar);
19704
+ });
19705
+ // Loading a source is not idempotent the way the setters above are — it REPLACES the store's
19706
+ // entries — so this tracks only `source` and guards on instance identity. `sourceConfig` is
19707
+ // read untracked so changing it never re-imports; it is read at the moment a new blob arrives.
19708
+ effect(() => {
19709
+ const source = this.source();
19710
+ if (source != null && source !== this._importedSource) {
19711
+ this._importedSource = source;
19712
+ const sourceConfig = untracked(() => this.sourceConfig());
19713
+ void this.store.importMergedPdf({ source, expectedSlotIds: sourceConfig?.expectedSlotIds, allowWithoutSidecar: sourceConfig?.allowWithoutSidecar, fileName: sourceConfig?.fileName });
19714
+ }
19715
+ });
18722
19716
  }
18723
19717
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: DbxPdfMergeEditorStoreDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
18724
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.11", type: DbxPdfMergeEditorStoreDirective, isStandalone: true, selector: "[dbxPdfMergeEditorStore]", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null }, outputSizeLimit: { classPropertyName: "outputSizeLimit", publicName: "outputSizeLimit", isSignal: true, isRequired: false, transformFunction: null } }, providers: [DbxPdfMergeEditorStore], exportAs: ["dbxPdfMergeEditorStore"], ngImport: i0 });
19718
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.2.11", type: DbxPdfMergeEditorStoreDirective, isStandalone: true, selector: "[dbxPdfMergeEditorStore]", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null }, outputSizeLimit: { classPropertyName: "outputSizeLimit", publicName: "outputSizeLimit", isSignal: true, isRequired: false, transformFunction: null }, source: { classPropertyName: "source", publicName: "source", isSignal: true, isRequired: false, transformFunction: null }, sourceConfig: { classPropertyName: "sourceConfig", publicName: "sourceConfig", isSignal: true, isRequired: false, transformFunction: null } }, providers: [DbxPdfMergeEditorStore], exportAs: ["dbxPdfMergeEditorStore"], ngImport: i0 });
18725
19719
  }
18726
19720
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: DbxPdfMergeEditorStoreDirective, decorators: [{
18727
19721
  type: Directive,
@@ -18731,10 +19725,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
18731
19725
  providers: [DbxPdfMergeEditorStore],
18732
19726
  exportAs: 'dbxPdfMergeEditorStore'
18733
19727
  }]
18734
- }], ctorParameters: () => [], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: false }] }], outputSizeLimit: [{ type: i0.Input, args: [{ isSignal: true, alias: "outputSizeLimit", required: false }] }] } });
19728
+ }], ctorParameters: () => [], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: false }] }], outputSizeLimit: [{ type: i0.Input, args: [{ isSignal: true, alias: "outputSizeLimit", required: false }] }], source: [{ type: i0.Input, args: [{ isSignal: true, alias: "source", required: false }] }], sourceConfig: [{ type: i0.Input, args: [{ isSignal: true, alias: "sourceConfig", required: false }] }] } });
18735
19729
 
18736
- const PDF_ICON = 'picture_as_pdf';
18737
- const IMAGE_ICON = 'image';
19730
+ const PDF_ICON$1 = 'picture_as_pdf';
19731
+ const IMAGE_ICON$1 = 'image';
18738
19732
  const ERROR_ICON = 'error';
18739
19733
  /**
18740
19734
  * Single row inside the {@link DbxPdfMergeListComponent}: shows the file's icon, name, formatted size, status, drag handle, and a remove button. The component's template is itself a `cdkDrag` element so each row can be reordered inside the parent's `cdkDropList`.
@@ -18751,10 +19745,10 @@ class DbxPdfMergeEntryComponent {
18751
19745
  icon = ERROR_ICON;
18752
19746
  }
18753
19747
  else if (entry.kind === 'pdf') {
18754
- icon = PDF_ICON;
19748
+ icon = PDF_ICON$1;
18755
19749
  }
18756
19750
  else {
18757
- icon = IMAGE_ICON;
19751
+ icon = IMAGE_ICON$1;
18758
19752
  }
18759
19753
  return icon;
18760
19754
  }, ...(ngDevMode ? [{ debugName: "iconSignal" }] : /* istanbul ignore next */ []));
@@ -18829,116 +19823,385 @@ class DbxPdfMergeEntryComponent {
18829
19823
  }
18830
19824
  </div>
18831
19825
  </div>
18832
- @if (isValidatingSignal()) {
18833
- <mat-progress-spinner mode="indeterminate" diameter="20"></mat-progress-spinner>
18834
- }
18835
- <button mat-icon-button type="button" class="dbx-pdf-merge-entry-remove" (click)="onRemove()" aria-label="Remove file">
18836
- <mat-icon>close</mat-icon>
19826
+ @if (isValidatingSignal()) {
19827
+ <mat-progress-spinner mode="indeterminate" diameter="20"></mat-progress-spinner>
19828
+ }
19829
+ <button mat-icon-button type="button" class="dbx-pdf-merge-entry-remove" (click)="onRemove()" aria-label="Remove file">
19830
+ <mat-icon>close</mat-icon>
19831
+ </button>
19832
+ </div>
19833
+ `, isInline: true, dependencies: [{ kind: "directive", type: CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }, { kind: "directive", type: CdkDragPlaceholder, selector: "ng-template[cdkDragPlaceholder]", inputs: ["data"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i1$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatProgressSpinnerModule }, { kind: "component", type: i3.MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }, { kind: "directive", type: DbxChipDirective, selector: "dbx-chip", inputs: ["small", "block", "color", "display", "tone"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
19834
+ }
19835
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: DbxPdfMergeEntryComponent, decorators: [{
19836
+ type: Component,
19837
+ args: [{
19838
+ selector: 'dbx-pdf-merge-entry',
19839
+ template: `
19840
+ <div class="dbx-pdf-merge-entry-row" cdkDrag cdkDragLockAxis="y">
19841
+ <ng-template cdkDragPlaceholder>
19842
+ <div class="dbx-pdf-merge-entry-placeholder"></div>
19843
+ </ng-template>
19844
+ <button mat-icon-button type="button" cdkDragHandle class="dbx-pdf-merge-entry-handle" aria-label="Drag to reorder">
19845
+ <mat-icon>drag_indicator</mat-icon>
19846
+ </button>
19847
+ <mat-icon class="dbx-pdf-merge-entry-icon" [class.dbx-warn]="isErrorSignal()">{{ iconSignal() }}</mat-icon>
19848
+ <div class="dbx-pdf-merge-entry-info dbx-flex-fill-0">
19849
+ <div class="dbx-pdf-merge-entry-name dbx-text-truncate" [title]="entry().name">{{ entry().name }}</div>
19850
+ <div class="dbx-pdf-merge-entry-meta dbx-hint dbx-small">
19851
+ <span>{{ sizeSignal() }}</span>
19852
+ @if (isEncryptedSignal()) {
19853
+ <dbx-chip class="dbx-pdf-merge-entry-encrypted-chip" color="warn" [small]="true">
19854
+ <mat-icon class="dbx-pdf-merge-entry-encrypted-icon">lock</mat-icon>
19855
+ <span>Encrypted</span>
19856
+ </dbx-chip>
19857
+ }
19858
+ @if (compressionLabelSignal(); as compressionLabel) {
19859
+ <span class="dbx-pdf-merge-entry-compression">{{ compressionLabel }}</span>
19860
+ }
19861
+ @if (statusLabelSignal(); as label) {
19862
+ <span class="dbx-pdf-merge-entry-status" [class.dbx-warn]="isErrorSignal()">{{ label }}</span>
19863
+ }
19864
+ </div>
19865
+ </div>
19866
+ @if (isValidatingSignal()) {
19867
+ <mat-progress-spinner mode="indeterminate" diameter="20"></mat-progress-spinner>
19868
+ }
19869
+ <button mat-icon-button type="button" class="dbx-pdf-merge-entry-remove" (click)="onRemove()" aria-label="Remove file">
19870
+ <mat-icon>close</mat-icon>
19871
+ </button>
19872
+ </div>
19873
+ `,
19874
+ host: {
19875
+ class: 'dbx-pdf-merge-entry d-block',
19876
+ '[class.dbx-pdf-merge-entry--error]': 'isErrorSignal()',
19877
+ '[class.dbx-pdf-merge-entry--validating]': 'isValidatingSignal()',
19878
+ '[class.dbx-pdf-merge-entry--ignored]': 'isIgnoredSignal()',
19879
+ '[class.dbx-pdf-merge-entry--encrypted]': 'isEncryptedSignal()'
19880
+ },
19881
+ imports: [CdkDrag, CdkDragHandle, CdkDragPlaceholder, MatIconModule, MatButtonModule, MatProgressSpinnerModule, DbxChipDirective],
19882
+ changeDetection: ChangeDetectionStrategy.OnPush,
19883
+ standalone: true
19884
+ }]
19885
+ }], propDecorators: { entry: [{ type: i0.Input, args: [{ isSignal: true, alias: "entry", required: true }] }] } });
19886
+
19887
+ /**
19888
+ * Renders the staged entries inside a CDK drop list. Each row is a {@link DbxPdfMergeEntryComponent} that hosts its own `cdkDrag`, so reordering events bubble up here as `cdkDropListDropped` and forward to the store's `moveEntry` updater.
19889
+ */
19890
+ class DbxPdfMergeListComponent {
19891
+ store = inject(DbxPdfMergeEditorStore);
19892
+ entries$ = this.store.displayEntries$;
19893
+ onDrop(event) {
19894
+ this.store.moveEntry({ previousIndex: event.previousIndex, currentIndex: event.currentIndex });
19895
+ }
19896
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: DbxPdfMergeListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
19897
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: DbxPdfMergeListComponent, isStandalone: true, selector: "dbx-pdf-merge-list", host: { classAttribute: "dbx-pdf-merge-list" }, ngImport: i0, template: `
19898
+ @if (entries$ | async; as entries) {
19899
+ @if (entries.length === 0) {
19900
+ <div class="dbx-list-empty-content dbx-hint dbx-p4">No files added yet.</div>
19901
+ } @else {
19902
+ <div class="dbx-pdf-merge-list-items" cdkDropList (cdkDropListDropped)="onDrop($event)">
19903
+ @for (entry of entries; track entry.id) {
19904
+ <dbx-pdf-merge-entry [entry]="entry"></dbx-pdf-merge-entry>
19905
+ }
19906
+ </div>
19907
+ }
19908
+ }
19909
+ `, isInline: true, dependencies: [{ kind: "directive", type: CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "component", type: DbxPdfMergeEntryComponent, selector: "dbx-pdf-merge-entry", inputs: ["entry"] }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
19910
+ }
19911
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: DbxPdfMergeListComponent, decorators: [{
19912
+ type: Component,
19913
+ args: [{
19914
+ selector: 'dbx-pdf-merge-list',
19915
+ template: `
19916
+ @if (entries$ | async; as entries) {
19917
+ @if (entries.length === 0) {
19918
+ <div class="dbx-list-empty-content dbx-hint dbx-p4">No files added yet.</div>
19919
+ } @else {
19920
+ <div class="dbx-pdf-merge-list-items" cdkDropList (cdkDropListDropped)="onDrop($event)">
19921
+ @for (entry of entries; track entry.id) {
19922
+ <dbx-pdf-merge-entry [entry]="entry"></dbx-pdf-merge-entry>
19923
+ }
19924
+ </div>
19925
+ }
19926
+ }
19927
+ `,
19928
+ host: {
19929
+ class: 'dbx-pdf-merge-list'
19930
+ },
19931
+ imports: [AsyncPipe, CdkDropList, DbxPdfMergeEntryComponent],
19932
+ changeDetection: ChangeDetectionStrategy.OnPush,
19933
+ standalone: true
19934
+ }]
19935
+ }] });
19936
+
19937
+ const PDF_ICON = 'picture_as_pdf';
19938
+ const IMAGE_ICON = 'image';
19939
+ /**
19940
+ * Degrees added by one press of the rotate control.
19941
+ */
19942
+ const ROTATION_STEP_DEGREES = 90;
19943
+ /**
19944
+ * PDF points per inch. PDF page dimensions are expressed in points; inches read better on a scanned document.
19945
+ */
19946
+ const POINTS_PER_INCH = 72;
19947
+ const ROTATION_VALUES = [0, 90, 180, 270];
19948
+ /**
19949
+ * Single page row inside a {@link DbxPdfMergePageListComponent} group: shows which source file and page it came from, its dimensions and rotation, and controls to rotate it, mark it for removal, and drag it to a new position.
19950
+ *
19951
+ * The template's root element is itself a `cdkDrag`, matching {@link DbxPdfMergeEntryComponent}, so rows reorder inside their group's drop list.
19952
+ */
19953
+ class DbxPdfMergePageComponent {
19954
+ store = inject(DbxPdfMergeEditorStore);
19955
+ page = input.required(...(ngDevMode ? [{ debugName: "page" }] : /* istanbul ignore next */ []));
19956
+ removedSignal = computed(() => this.page().removed, ...(ngDevMode ? [{ debugName: "removedSignal" }] : /* istanbul ignore next */ []));
19957
+ iconSignal = computed(() => (this.page().kind === 'pdf' ? PDF_ICON : IMAGE_ICON), ...(ngDevMode ? [{ debugName: "iconSignal" }] : /* istanbul ignore next */ []));
19958
+ removeLabelSignal = computed(() => (this.removedSignal() ? 'Restore page' : 'Mark page for removal'), ...(ngDevMode ? [{ debugName: "removeLabelSignal" }] : /* istanbul ignore next */ []));
19959
+ positionLabelSignal = computed(() => {
19960
+ const page = this.page();
19961
+ let label;
19962
+ if (page.kind === 'image') {
19963
+ label = 'Image';
19964
+ }
19965
+ else {
19966
+ label = `Page ${page.sourceIndex + 1} of ${page.sourcePageCount}`;
19967
+ }
19968
+ return label;
19969
+ }, ...(ngDevMode ? [{ debugName: "positionLabelSignal" }] : /* istanbul ignore next */ []));
19970
+ dimensionsLabelSignal = computed(() => {
19971
+ const { kind, meta } = this.page();
19972
+ let label;
19973
+ if (meta.width <= 0 || meta.height <= 0) {
19974
+ label = null;
19975
+ }
19976
+ else if (kind === 'image') {
19977
+ label = `${Math.round(meta.width)} × ${Math.round(meta.height)} px`;
19978
+ }
19979
+ else {
19980
+ label = `${(meta.width / POINTS_PER_INCH).toFixed(1)} × ${(meta.height / POINTS_PER_INCH).toFixed(1)} in`;
19981
+ }
19982
+ return label;
19983
+ }, ...(ngDevMode ? [{ debugName: "dimensionsLabelSignal" }] : /* istanbul ignore next */ []));
19984
+ rotationLabelSignal = computed(() => {
19985
+ const rotation = this.page().rotation;
19986
+ return rotation === 0 ? null : `${rotation}°`;
19987
+ }, ...(ngDevMode ? [{ debugName: "rotationLabelSignal" }] : /* istanbul ignore next */ []));
19988
+ onRotate() {
19989
+ const page = this.page();
19990
+ const next = ROTATION_VALUES[((page.rotation + ROTATION_STEP_DEGREES) / ROTATION_STEP_DEGREES) % ROTATION_VALUES.length];
19991
+ this.store.setPageRotation({ pageId: page.id, rotation: next });
19992
+ }
19993
+ onToggleRemoved() {
19994
+ const page = this.page();
19995
+ this.store.setPageRemoved({ pageId: page.id, removed: !page.removed });
19996
+ }
19997
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: DbxPdfMergePageComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
19998
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: DbxPdfMergePageComponent, isStandalone: true, selector: "dbx-pdf-merge-page", inputs: { page: { classPropertyName: "page", publicName: "page", isSignal: true, isRequired: true, transformFunction: null } }, host: { properties: { "class.dbx-pdf-merge-page--removed": "removedSignal()" }, classAttribute: "dbx-pdf-merge-page d-block" }, ngImport: i0, template: `
19999
+ <div class="dbx-pdf-merge-page-row" cdkDrag cdkDragLockAxis="y">
20000
+ <ng-template cdkDragPlaceholder>
20001
+ <div class="dbx-pdf-merge-page-placeholder"></div>
20002
+ </ng-template>
20003
+ <button mat-icon-button type="button" cdkDragHandle class="dbx-pdf-merge-page-handle" aria-label="Drag to reorder page">
20004
+ <mat-icon>drag_indicator</mat-icon>
20005
+ </button>
20006
+ <mat-icon class="dbx-pdf-merge-page-icon">{{ iconSignal() }}</mat-icon>
20007
+ <div class="dbx-pdf-merge-page-info dbx-flex-fill-0">
20008
+ <div class="dbx-pdf-merge-page-name dbx-text-truncate" [title]="page().sourceName">{{ page().sourceName }}</div>
20009
+ <div class="dbx-pdf-merge-page-meta dbx-hint dbx-small">
20010
+ <span class="dbx-pdf-merge-page-position">{{ positionLabelSignal() }}</span>
20011
+ @if (dimensionsLabelSignal(); as dimensions) {
20012
+ <span class="dbx-pdf-merge-page-dimensions">{{ dimensions }}</span>
20013
+ }
20014
+ @if (rotationLabelSignal(); as rotation) {
20015
+ <dbx-chip class="dbx-pdf-merge-page-rotation-chip" [small]="true">
20016
+ <mat-icon class="dbx-pdf-merge-page-rotation-icon">rotate_right</mat-icon>
20017
+ <span>{{ rotation }}</span>
20018
+ </dbx-chip>
20019
+ }
20020
+ @if (removedSignal()) {
20021
+ <span class="dbx-pdf-merge-page-removed-label dbx-warn">Will be removed</span>
20022
+ }
20023
+ </div>
20024
+ </div>
20025
+ <button mat-icon-button type="button" class="dbx-pdf-merge-page-rotate" [disabled]="removedSignal()" (click)="onRotate()" aria-label="Rotate page 90 degrees">
20026
+ <mat-icon>rotate_right</mat-icon>
20027
+ </button>
20028
+ <button mat-icon-button type="button" class="dbx-pdf-merge-page-remove" (click)="onToggleRemoved()" [attr.aria-label]="removeLabelSignal()">
20029
+ <mat-icon>{{ removedSignal() ? 'undo' : 'close' }}</mat-icon>
18837
20030
  </button>
18838
20031
  </div>
18839
- `, isInline: true, dependencies: [{ kind: "directive", type: CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }, { kind: "directive", type: CdkDragPlaceholder, selector: "ng-template[cdkDragPlaceholder]", inputs: ["data"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i1$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "ngmodule", type: MatProgressSpinnerModule }, { kind: "component", type: i3.MatProgressSpinner, selector: "mat-progress-spinner, mat-spinner", inputs: ["color", "mode", "value", "diameter", "strokeWidth"], exportAs: ["matProgressSpinner"] }, { kind: "directive", type: DbxChipDirective, selector: "dbx-chip", inputs: ["small", "block", "color", "display", "tone"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
20032
+ `, isInline: true, dependencies: [{ kind: "directive", type: CdkDrag, selector: "[cdkDrag]", inputs: ["cdkDragData", "cdkDragLockAxis", "cdkDragRootElement", "cdkDragBoundary", "cdkDragStartDelay", "cdkDragFreeDragPosition", "cdkDragDisabled", "cdkDragConstrainPosition", "cdkDragPreviewClass", "cdkDragPreviewContainer", "cdkDragScale"], outputs: ["cdkDragStarted", "cdkDragReleased", "cdkDragEnded", "cdkDragEntered", "cdkDragExited", "cdkDragDropped", "cdkDragMoved"], exportAs: ["cdkDrag"] }, { kind: "directive", type: CdkDragHandle, selector: "[cdkDragHandle]", inputs: ["cdkDragHandleDisabled"] }, { kind: "directive", type: CdkDragPlaceholder, selector: "ng-template[cdkDragPlaceholder]", inputs: ["data"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i1$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatButtonModule }, { kind: "component", type: i1.MatIconButton, selector: "button[mat-icon-button], a[mat-icon-button], button[matIconButton], a[matIconButton]", exportAs: ["matButton", "matAnchor"] }, { kind: "directive", type: DbxChipDirective, selector: "dbx-chip", inputs: ["small", "block", "color", "display", "tone"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
18840
20033
  }
18841
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: DbxPdfMergeEntryComponent, decorators: [{
20034
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: DbxPdfMergePageComponent, decorators: [{
18842
20035
  type: Component,
18843
20036
  args: [{
18844
- selector: 'dbx-pdf-merge-entry',
20037
+ selector: 'dbx-pdf-merge-page',
18845
20038
  template: `
18846
- <div class="dbx-pdf-merge-entry-row" cdkDrag cdkDragLockAxis="y">
20039
+ <div class="dbx-pdf-merge-page-row" cdkDrag cdkDragLockAxis="y">
18847
20040
  <ng-template cdkDragPlaceholder>
18848
- <div class="dbx-pdf-merge-entry-placeholder"></div>
20041
+ <div class="dbx-pdf-merge-page-placeholder"></div>
18849
20042
  </ng-template>
18850
- <button mat-icon-button type="button" cdkDragHandle class="dbx-pdf-merge-entry-handle" aria-label="Drag to reorder">
20043
+ <button mat-icon-button type="button" cdkDragHandle class="dbx-pdf-merge-page-handle" aria-label="Drag to reorder page">
18851
20044
  <mat-icon>drag_indicator</mat-icon>
18852
20045
  </button>
18853
- <mat-icon class="dbx-pdf-merge-entry-icon" [class.dbx-warn]="isErrorSignal()">{{ iconSignal() }}</mat-icon>
18854
- <div class="dbx-pdf-merge-entry-info dbx-flex-fill-0">
18855
- <div class="dbx-pdf-merge-entry-name dbx-text-truncate" [title]="entry().name">{{ entry().name }}</div>
18856
- <div class="dbx-pdf-merge-entry-meta dbx-hint dbx-small">
18857
- <span>{{ sizeSignal() }}</span>
18858
- @if (isEncryptedSignal()) {
18859
- <dbx-chip class="dbx-pdf-merge-entry-encrypted-chip" color="warn" [small]="true">
18860
- <mat-icon class="dbx-pdf-merge-entry-encrypted-icon">lock</mat-icon>
18861
- <span>Encrypted</span>
18862
- </dbx-chip>
20046
+ <mat-icon class="dbx-pdf-merge-page-icon">{{ iconSignal() }}</mat-icon>
20047
+ <div class="dbx-pdf-merge-page-info dbx-flex-fill-0">
20048
+ <div class="dbx-pdf-merge-page-name dbx-text-truncate" [title]="page().sourceName">{{ page().sourceName }}</div>
20049
+ <div class="dbx-pdf-merge-page-meta dbx-hint dbx-small">
20050
+ <span class="dbx-pdf-merge-page-position">{{ positionLabelSignal() }}</span>
20051
+ @if (dimensionsLabelSignal(); as dimensions) {
20052
+ <span class="dbx-pdf-merge-page-dimensions">{{ dimensions }}</span>
18863
20053
  }
18864
- @if (compressionLabelSignal(); as compressionLabel) {
18865
- <span class="dbx-pdf-merge-entry-compression">{{ compressionLabel }}</span>
20054
+ @if (rotationLabelSignal(); as rotation) {
20055
+ <dbx-chip class="dbx-pdf-merge-page-rotation-chip" [small]="true">
20056
+ <mat-icon class="dbx-pdf-merge-page-rotation-icon">rotate_right</mat-icon>
20057
+ <span>{{ rotation }}</span>
20058
+ </dbx-chip>
18866
20059
  }
18867
- @if (statusLabelSignal(); as label) {
18868
- <span class="dbx-pdf-merge-entry-status" [class.dbx-warn]="isErrorSignal()">{{ label }}</span>
20060
+ @if (removedSignal()) {
20061
+ <span class="dbx-pdf-merge-page-removed-label dbx-warn">Will be removed</span>
18869
20062
  }
18870
20063
  </div>
18871
20064
  </div>
18872
- @if (isValidatingSignal()) {
18873
- <mat-progress-spinner mode="indeterminate" diameter="20"></mat-progress-spinner>
18874
- }
18875
- <button mat-icon-button type="button" class="dbx-pdf-merge-entry-remove" (click)="onRemove()" aria-label="Remove file">
18876
- <mat-icon>close</mat-icon>
20065
+ <button mat-icon-button type="button" class="dbx-pdf-merge-page-rotate" [disabled]="removedSignal()" (click)="onRotate()" aria-label="Rotate page 90 degrees">
20066
+ <mat-icon>rotate_right</mat-icon>
20067
+ </button>
20068
+ <button mat-icon-button type="button" class="dbx-pdf-merge-page-remove" (click)="onToggleRemoved()" [attr.aria-label]="removeLabelSignal()">
20069
+ <mat-icon>{{ removedSignal() ? 'undo' : 'close' }}</mat-icon>
18877
20070
  </button>
18878
20071
  </div>
18879
20072
  `,
18880
20073
  host: {
18881
- class: 'dbx-pdf-merge-entry d-block',
18882
- '[class.dbx-pdf-merge-entry--error]': 'isErrorSignal()',
18883
- '[class.dbx-pdf-merge-entry--validating]': 'isValidatingSignal()',
18884
- '[class.dbx-pdf-merge-entry--ignored]': 'isIgnoredSignal()',
18885
- '[class.dbx-pdf-merge-entry--encrypted]': 'isEncryptedSignal()'
20074
+ class: 'dbx-pdf-merge-page d-block',
20075
+ '[class.dbx-pdf-merge-page--removed]': 'removedSignal()'
18886
20076
  },
18887
- imports: [CdkDrag, CdkDragHandle, CdkDragPlaceholder, MatIconModule, MatButtonModule, MatProgressSpinnerModule, DbxChipDirective],
20077
+ imports: [CdkDrag, CdkDragHandle, CdkDragPlaceholder, MatIconModule, MatButtonModule, DbxChipDirective],
18888
20078
  changeDetection: ChangeDetectionStrategy.OnPush,
18889
20079
  standalone: true
18890
20080
  }]
18891
- }], propDecorators: { entry: [{ type: i0.Input, args: [{ isSignal: true, alias: "entry", required: true }] }] } });
20081
+ }], propDecorators: { page: [{ type: i0.Input, args: [{ isSignal: true, alias: "page", required: true }] }] } });
18892
20082
 
18893
20083
  /**
18894
- * Renders the staged entries inside a CDK drop list. Each row is a {@link DbxPdfMergeEntryComponent} that hosts its own `cdkDrag`, so reordering events bubble up here as `cdkDropListDropped` and forward to the store's `moveEntry` updater.
20084
+ * Message shown for an entry that is not encrypted but still could not be parsed into pages.
18895
20085
  */
18896
- class DbxPdfMergeListComponent {
20086
+ const UNREADABLE_PAGES_MESSAGE = 'Pages could not be read.';
20087
+ /**
20088
+ * Renders the editor's page plan while page editing is enabled, one CDK drop list per group.
20089
+ *
20090
+ * Separate, unconnected drop lists are what confine reordering to a single slot: CDK will not let a drag cross into a list it is not connected to, so the rule needs no guard code. Bind {@link slotId} to render only that slot's group — used by {@link DbxPdfMergeEditorFileUploadComponent} to show its own pages inline. Leave it unbound to render every group.
20091
+ *
20092
+ * Entries that could not be expanded into pages (encrypted, or unparseable) are listed separately below the groups rather than silently omitted.
20093
+ */
20094
+ class DbxPdfMergePageListComponent {
18897
20095
  store = inject(DbxPdfMergeEditorStore);
18898
- entries$ = this.store.displayEntries$;
18899
- onDrop(event) {
18900
- this.store.moveEntry({ previousIndex: event.previousIndex, currentIndex: event.currentIndex });
20096
+ /**
20097
+ * Renders only this slot's group when bound. Leave unbound to render every group.
20098
+ */
20099
+ slotId = input(...(ngDevMode ? [undefined, { debugName: "slotId" }] : /* istanbul ignore next */ []));
20100
+ /**
20101
+ * Whether to show each group's slot name above its pages. Defaults to `true` when rendering every group and `false` when scoped to one slot, where the slot already has its own heading.
20102
+ */
20103
+ showGroupLabels = input(...(ngDevMode ? [undefined, { debugName: "showGroupLabels" }] : /* istanbul ignore next */ []));
20104
+ _slotId$ = toObservable(this.slotId);
20105
+ groups$ = this._slotId$.pipe(switchMap((slotId) => {
20106
+ let groups$;
20107
+ if (slotId == null) {
20108
+ groups$ = this.store.pageGroups$;
20109
+ }
20110
+ else {
20111
+ const groupKey = pdfMergePageGroupKeyForSlotId(slotId);
20112
+ groups$ = this.store.pagesForSlotId$(slotId).pipe(map((pages) => (pages.length === 0 ? [] : [{ groupKey, slotId, pages }])));
20113
+ }
20114
+ return groups$;
20115
+ }), shareReplay(1));
20116
+ unexpandable$ = combineLatest([this.store.unexpandableEntries$, this._slotId$]).pipe(map(([entries, slotId]) => (slotId == null ? entries : entries.filter((entry) => entry.slotId === slotId))), shareReplay(1));
20117
+ groupsSignal = toSignal(this.groups$, { initialValue: [] });
20118
+ unexpandableSignal = toSignal(this.unexpandable$, { initialValue: [] });
20119
+ showGroupLabelsSignal = computed(() => {
20120
+ const slotId = this.slotId();
20121
+ return this.showGroupLabels() ?? slotId == null;
20122
+ }, ...(ngDevMode ? [{ debugName: "showGroupLabelsSignal" }] : /* istanbul ignore next */ []));
20123
+ labelForGroup(group) {
20124
+ return this.showGroupLabelsSignal() ? group.slotId : null;
20125
+ }
20126
+ messageForUnexpandable(entry) {
20127
+ return entry.encrypted ? DBX_PDF_MERGE_ENCRYPTED_NOT_EDITABLE_MESSAGE : UNREADABLE_PAGES_MESSAGE;
20128
+ }
20129
+ onDrop(group, event) {
20130
+ this.store.movePageWithinGroup({
20131
+ groupKey: group.groupKey,
20132
+ pageIds: group.pages.map((page) => page.id),
20133
+ previousIndex: event.previousIndex,
20134
+ currentIndex: event.currentIndex
20135
+ });
18901
20136
  }
18902
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: DbxPdfMergeListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
18903
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: DbxPdfMergeListComponent, isStandalone: true, selector: "dbx-pdf-merge-list", host: { classAttribute: "dbx-pdf-merge-list" }, ngImport: i0, template: `
18904
- @if (entries$ | async; as entries) {
18905
- @if (entries.length === 0) {
18906
- <div class="dbx-list-empty-content dbx-hint dbx-p4">No files added yet.</div>
20137
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: DbxPdfMergePageListComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
20138
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: DbxPdfMergePageListComponent, isStandalone: true, selector: "dbx-pdf-merge-page-list", inputs: { slotId: { classPropertyName: "slotId", publicName: "slotId", isSignal: true, isRequired: false, transformFunction: null }, showGroupLabels: { classPropertyName: "showGroupLabels", publicName: "showGroupLabels", isSignal: true, isRequired: false, transformFunction: null } }, host: { classAttribute: "dbx-pdf-merge-page-list d-block" }, ngImport: i0, template: `
20139
+ @if (groupsSignal(); as groups) {
20140
+ @if (groups.length === 0 && unexpandableSignal().length === 0) {
20141
+ <div class="dbx-list-empty-content dbx-hint dbx-p4">No pages yet.</div>
18907
20142
  } @else {
18908
- <div class="dbx-pdf-merge-list-items" cdkDropList (cdkDropListDropped)="onDrop($event)">
18909
- @for (entry of entries; track entry.id) {
18910
- <dbx-pdf-merge-entry [entry]="entry"></dbx-pdf-merge-entry>
18911
- }
18912
- </div>
20143
+ @for (group of groups; track group.groupKey) {
20144
+ <div class="dbx-pdf-merge-page-group">
20145
+ @if (labelForGroup(group); as label) {
20146
+ <div class="dbx-pdf-merge-page-group-label dbx-hint dbx-small">{{ label }}</div>
20147
+ }
20148
+ <div class="dbx-pdf-merge-page-group-items" cdkDropList (cdkDropListDropped)="onDrop(group, $event)">
20149
+ @for (page of group.pages; track page.id) {
20150
+ <dbx-pdf-merge-page [page]="page"></dbx-pdf-merge-page>
20151
+ }
20152
+ </div>
20153
+ </div>
20154
+ }
18913
20155
  }
18914
20156
  }
18915
- `, isInline: true, dependencies: [{ kind: "directive", type: CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "component", type: DbxPdfMergeEntryComponent, selector: "dbx-pdf-merge-entry", inputs: ["entry"] }, { kind: "pipe", type: AsyncPipe, name: "async" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
20157
+ @for (entry of unexpandableSignal(); track entry.id) {
20158
+ <div class="dbx-pdf-merge-page-unexpandable">
20159
+ <mat-icon class="dbx-pdf-merge-page-unexpandable-icon">lock</mat-icon>
20160
+ <span class="dbx-pdf-merge-page-unexpandable-name dbx-text-truncate" [title]="entry.name">{{ entry.name }}</span>
20161
+ <span class="dbx-hint dbx-small">{{ messageForUnexpandable(entry) }}</span>
20162
+ </div>
20163
+ }
20164
+ `, isInline: true, dependencies: [{ kind: "directive", type: CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i1$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: DbxPdfMergePageComponent, selector: "dbx-pdf-merge-page", inputs: ["page"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
18916
20165
  }
18917
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: DbxPdfMergeListComponent, decorators: [{
20166
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: DbxPdfMergePageListComponent, decorators: [{
18918
20167
  type: Component,
18919
20168
  args: [{
18920
- selector: 'dbx-pdf-merge-list',
20169
+ selector: 'dbx-pdf-merge-page-list',
18921
20170
  template: `
18922
- @if (entries$ | async; as entries) {
18923
- @if (entries.length === 0) {
18924
- <div class="dbx-list-empty-content dbx-hint dbx-p4">No files added yet.</div>
20171
+ @if (groupsSignal(); as groups) {
20172
+ @if (groups.length === 0 && unexpandableSignal().length === 0) {
20173
+ <div class="dbx-list-empty-content dbx-hint dbx-p4">No pages yet.</div>
18925
20174
  } @else {
18926
- <div class="dbx-pdf-merge-list-items" cdkDropList (cdkDropListDropped)="onDrop($event)">
18927
- @for (entry of entries; track entry.id) {
18928
- <dbx-pdf-merge-entry [entry]="entry"></dbx-pdf-merge-entry>
18929
- }
18930
- </div>
20175
+ @for (group of groups; track group.groupKey) {
20176
+ <div class="dbx-pdf-merge-page-group">
20177
+ @if (labelForGroup(group); as label) {
20178
+ <div class="dbx-pdf-merge-page-group-label dbx-hint dbx-small">{{ label }}</div>
20179
+ }
20180
+ <div class="dbx-pdf-merge-page-group-items" cdkDropList (cdkDropListDropped)="onDrop(group, $event)">
20181
+ @for (page of group.pages; track page.id) {
20182
+ <dbx-pdf-merge-page [page]="page"></dbx-pdf-merge-page>
20183
+ }
20184
+ </div>
20185
+ </div>
20186
+ }
18931
20187
  }
18932
20188
  }
20189
+ @for (entry of unexpandableSignal(); track entry.id) {
20190
+ <div class="dbx-pdf-merge-page-unexpandable">
20191
+ <mat-icon class="dbx-pdf-merge-page-unexpandable-icon">lock</mat-icon>
20192
+ <span class="dbx-pdf-merge-page-unexpandable-name dbx-text-truncate" [title]="entry.name">{{ entry.name }}</span>
20193
+ <span class="dbx-hint dbx-small">{{ messageForUnexpandable(entry) }}</span>
20194
+ </div>
20195
+ }
18933
20196
  `,
18934
20197
  host: {
18935
- class: 'dbx-pdf-merge-list'
20198
+ class: 'dbx-pdf-merge-page-list d-block'
18936
20199
  },
18937
- imports: [AsyncPipe, CdkDropList, DbxPdfMergeEntryComponent],
20200
+ imports: [CdkDropList, MatIconModule, DbxPdfMergePageComponent],
18938
20201
  changeDetection: ChangeDetectionStrategy.OnPush,
18939
20202
  standalone: true
18940
20203
  }]
18941
- }] });
20204
+ }], propDecorators: { slotId: [{ type: i0.Input, args: [{ isSignal: true, alias: "slotId", required: false }] }], showGroupLabels: [{ type: i0.Input, args: [{ isSignal: true, alias: "showGroupLabels", required: false }] }] } });
18942
20205
 
18943
20206
  /**
18944
20207
  * Renders a PDF in a fixed-height embed container. Accepts either a URL or a `Blob` source.
@@ -19007,6 +20270,21 @@ const DEFAULT_DOWNLOAD_BUTTON = {
19007
20270
  display: { icon: 'download', text: 'Download' },
19008
20271
  style: { type: 'stroked' }
19009
20272
  };
20273
+ const DEFAULT_CLEAR_CONFIRM = {
20274
+ title: 'Clear all files?',
20275
+ prompt: 'Every file added to this editor will be removed, along with any page edits made to them.',
20276
+ confirmText: 'Clear',
20277
+ cancelText: 'Cancel'
20278
+ };
20279
+ /**
20280
+ * Shown in place of {@link DEFAULT_CLEAR_CONFIRM} when the editor has a programmatic baseline, since Clear resets to that document rather than emptying.
20281
+ */
20282
+ const DEFAULT_RESTORE_CONFIRM = {
20283
+ title: 'Reset to the imported document?',
20284
+ prompt: 'Every change made here — added files, page edits, removed sections — will be discarded and the imported document reloaded as it first arrived.',
20285
+ confirmText: 'Reset',
20286
+ cancelText: 'Cancel'
20287
+ };
19010
20288
  class DbxPdfMergeEditorComponent {
19011
20289
  store = inject(DbxPdfMergeEditorStore);
19012
20290
  _matDialog = inject(MatDialog);
@@ -19026,6 +20304,8 @@ class DbxPdfMergeEditorComponent {
19026
20304
  downloadButton = input(...(ngDevMode ? [undefined, { debugName: "downloadButton" }] : /* istanbul ignore next */ []));
19027
20305
  showAddFiles = input(...(ngDevMode ? [undefined, { debugName: "showAddFiles" }] : /* istanbul ignore next */ []));
19028
20306
  showFileList = input(...(ngDevMode ? [undefined, { debugName: "showFileList" }] : /* istanbul ignore next */ []));
20307
+ pageEditing = input(...(ngDevMode ? [undefined, { debugName: "pageEditing" }] : /* istanbul ignore next */ []));
20308
+ sidecar = input(...(ngDevMode ? [undefined, { debugName: "sidecar" }] : /* istanbul ignore next */ []));
19029
20309
  /**
19030
20310
  * Bundles every editor option into one object (see {@link DbxPdfMergeEditorConfig}). Individual inputs override the matching field here, which in turn overrides the workspace-wide {@link DBX_PDF_MERGE_EDITOR_CONFIG} token.
19031
20311
  */
@@ -19039,6 +20319,11 @@ class DbxPdfMergeEditorComponent {
19039
20319
  * Store-level encryption handling default pushed by {@link DbxPdfMergeEditorStoreDirective}. Resolved between the editor's own `config` input and the workspace-wide token in {@link effectiveConfigSignal}.
19040
20320
  */
19041
20321
  storeEncryptedHandlingSignal = toSignal(this.store.encryptedHandling$, { initialValue: undefined });
20322
+ /**
20323
+ * Store-level page-editing and sidecar defaults pushed by {@link DbxPdfMergeEditorStoreDirective}. These read the store's raw (undefaulted) settings so an unset store value still falls through to the workspace-wide token in {@link effectiveConfigSignal}.
20324
+ */
20325
+ storePageEditingSignal = toSignal(this.store.pageEditingSetting$, { initialValue: undefined });
20326
+ storeSidecarSignal = toSignal(this.store.sidecarSetting$, { initialValue: undefined });
19042
20327
  /**
19043
20328
  * Merged config — the editor's own `config` input wins over the store-level default (for `imageCompression`), which in turn wins over the workspace-wide token. The individual inputs are resolved on top of this object in the per-field `*Signal` computeds below.
19044
20329
  */
@@ -19047,6 +20332,8 @@ class DbxPdfMergeEditorComponent {
19047
20332
  const fromToken = this._injectedConfig;
19048
20333
  const storeImageCompression = this.storeImageCompressionSignal();
19049
20334
  const storeEncryptedHandling = this.storeEncryptedHandlingSignal();
20335
+ const storePageEditing = this.storePageEditingSignal();
20336
+ const storeSidecar = this.storeSidecarSignal();
19050
20337
  return {
19051
20338
  imageCompression: fromInput?.imageCompression ?? storeImageCompression ?? fromToken?.imageCompression ?? null,
19052
20339
  outputSizeLimits: fromInput?.outputSizeLimits ?? fromToken?.outputSizeLimits ?? null,
@@ -19058,10 +20345,44 @@ class DbxPdfMergeEditorComponent {
19058
20345
  downloadButton: fromInput?.downloadButton ?? fromToken?.downloadButton,
19059
20346
  showAddFiles: fromInput?.showAddFiles ?? fromToken?.showAddFiles,
19060
20347
  showFileList: fromInput?.showFileList ?? fromToken?.showFileList,
19061
- encryptedHandling: fromInput?.encryptedHandling ?? storeEncryptedHandling ?? fromToken?.encryptedHandling ?? DEFAULT_DBX_PDF_MERGE_ENCRYPTED_HANDLING
20348
+ encryptedHandling: fromInput?.encryptedHandling ?? storeEncryptedHandling ?? fromToken?.encryptedHandling ?? DEFAULT_DBX_PDF_MERGE_ENCRYPTED_HANDLING,
20349
+ pageEditing: fromInput?.pageEditing ?? storePageEditing ?? fromToken?.pageEditing ?? DEFAULT_DBX_PDF_MERGE_PAGE_EDITING,
20350
+ sidecar: fromInput?.sidecar ?? storeSidecar ?? fromToken?.sidecar ?? DEFAULT_DBX_PDF_MERGE_SIDECAR,
20351
+ clearConfirm: fromInput?.clearConfirm ?? fromToken?.clearConfirm,
20352
+ restoreImportOnClear: fromInput?.restoreImportOnClear ?? fromToken?.restoreImportOnClear ?? DEFAULT_DBX_PDF_MERGE_RESTORE_IMPORT_ON_CLEAR
19062
20353
  };
19063
20354
  }, ...(ngDevMode ? [{ debugName: "effectiveConfigSignal" }] : /* istanbul ignore next */ []));
20355
+ restoreImportOnClearSignal = computed(() => this.effectiveConfigSignal().restoreImportOnClear ?? DEFAULT_DBX_PDF_MERGE_RESTORE_IMPORT_ON_CLEAR, ...(ngDevMode ? [{ debugName: "restoreImportOnClearSignal" }] : /* istanbul ignore next */ []));
20356
+ /**
20357
+ * Whether Clear will reset to a programmatic baseline instead of emptying. Only affects the confirmation's wording — {@link DbxPdfMergeEditorStore.clearEntries} makes the actual decision, and re-reads the store at the moment it runs.
20358
+ */
20359
+ clearRestoresImportSignal = computed(() => {
20360
+ // Tracks the entry count so the wording is re-evaluated as the editor's contents change; the
20361
+ // store's restore point is a plain field rather than a stream (nothing else needs to observe it).
20362
+ this.entryCountSignal();
20363
+ return this.restoreImportOnClearSignal() && this.store.hasRestorableImport();
20364
+ }, ...(ngDevMode ? [{ debugName: "clearRestoresImportSignal" }] : /* istanbul ignore next */ []));
20365
+ /**
20366
+ * Config for the confirmation shown before the footer Clear button empties the editor. `clearConfirm` overrides any field of the default; `autoConfirm: true` restores the pre-confirmation one-click behavior.
20367
+ *
20368
+ * The default copy tells the truth about which of the two things Clear is about to do — reset to the app-supplied document, or remove everything.
20369
+ */
20370
+ clearConfirmSignal = computed(() => {
20371
+ const base = this.clearRestoresImportSignal() ? DEFAULT_RESTORE_CONFIRM : DEFAULT_CLEAR_CONFIRM;
20372
+ return { ...base, ...this.effectiveConfigSignal().clearConfirm };
20373
+ }, ...(ngDevMode ? [{ debugName: "clearConfirmSignal" }] : /* istanbul ignore next */ []));
19064
20374
  encryptedHandlingSignal = computed(() => this.effectiveConfigSignal().encryptedHandling ?? DEFAULT_DBX_PDF_MERGE_ENCRYPTED_HANDLING, ...(ngDevMode ? [{ debugName: "encryptedHandlingSignal" }] : /* istanbul ignore next */ []));
20375
+ /**
20376
+ * Whether page editing is active. When `false` the editor renders its original file list and no page machinery is instantiated at all.
20377
+ */
20378
+ pageEditingSignal = computed(() => {
20379
+ const config = this.effectiveConfigSignal();
20380
+ return this.pageEditing() ?? config.pageEditing ?? DEFAULT_DBX_PDF_MERGE_PAGE_EDITING;
20381
+ }, ...(ngDevMode ? [{ debugName: "pageEditingSignal" }] : /* istanbul ignore next */ []));
20382
+ sidecarSignal = computed(() => {
20383
+ const config = this.effectiveConfigSignal();
20384
+ return this.sidecar() ?? config.sidecar ?? DEFAULT_DBX_PDF_MERGE_SIDECAR;
20385
+ }, ...(ngDevMode ? [{ debugName: "sidecarSignal" }] : /* istanbul ignore next */ []));
19065
20386
  imageCompressionConfigSignal = computed(() => this.effectiveConfigSignal().imageCompression, ...(ngDevMode ? [{ debugName: "imageCompressionConfigSignal" }] : /* istanbul ignore next */ []));
19066
20387
  outputSizeLimitsSignal = computed(() => this.effectiveConfigSignal().outputSizeLimits, ...(ngDevMode ? [{ debugName: "outputSizeLimitsSignal" }] : /* istanbul ignore next */ []));
19067
20388
  // Per-field resolution: dedicated input → config (input/token) → default. The config signal is
@@ -19102,6 +20423,10 @@ class DbxPdfMergeEditorComponent {
19102
20423
  errorBytesSignal = computed(() => this.outputSizeLimitsSignal()?.errorBytes, ...(ngDevMode ? [{ debugName: "errorBytesSignal" }] : /* istanbul ignore next */ []));
19103
20424
  hasReadyEntriesSignal = toSignal(this.store.hasReadyEntries$, { initialValue: false });
19104
20425
  entryCountSignal = toSignal(this.store.entryCount$, { initialValue: 0 });
20426
+ /**
20427
+ * Number of pages that will actually reach the output — the page plan minus anything marked for removal. Only surfaced while page editing is enabled.
20428
+ */
20429
+ mergeablePageCountSignal = toSignal(this.store.mergeablePageCount$, { initialValue: 0 });
19105
20430
  /**
19106
20431
  * Mirrors {@link DbxPdfMergeEditorStore.focusActive$} — `true` while `encryptedHandling === 'focus'` and at least one ready encrypted entry exists. Drives the encrypted-PDF focus banner.
19107
20432
  */
@@ -19171,6 +20496,14 @@ class DbxPdfMergeEditorComponent {
19171
20496
  const handling = this.encryptedHandlingSignal();
19172
20497
  this.store.setEncryptedHandling(handling);
19173
20498
  });
20499
+ effect(() => {
20500
+ const pageEditing = this.pageEditingSignal();
20501
+ this.store.setPageEditing(pageEditing);
20502
+ });
20503
+ effect(() => {
20504
+ const sidecar = this.sidecarSignal();
20505
+ this.store.setSidecar(sidecar);
20506
+ });
19174
20507
  }
19175
20508
  async onFiles(event) {
19176
20509
  const accepted = event.matchResult.accepted;
@@ -19184,9 +20517,20 @@ class DbxPdfMergeEditorComponent {
19184
20517
  this.store.addFiles({ entries: filtered });
19185
20518
  }
19186
20519
  }
20520
+ /**
20521
+ * Clears the editor, resetting to a programmatic baseline when there is one. See {@link DbxPdfMergeEditorStore.clearEntries}.
20522
+ *
20523
+ * @returns The clear outcome, so a programmatic caller can tell a reset from an empty.
20524
+ */
19187
20525
  onClear() {
19188
- this.store.clearAll();
20526
+ return this.store.clearEntries({ restoreImport: this.restoreImportOnClearSignal() });
19189
20527
  }
20528
+ /**
20529
+ * Runs as the handler of the footer Clear button's own `dbxAction`, so the `dbxActionConfirm` on that action is what stands between a misclick and the whole entry list. Delegates to {@link onClear} so a programmatic clear and the button share one path.
20530
+ *
20531
+ * @returns An observable the action completes on — a restore re-parses the document, so this is not instantaneous.
20532
+ */
20533
+ handleClear = () => from(this.onClear()).pipe(map(() => undefined));
19190
20534
  onPreview() {
19191
20535
  if (!this.canMergeSignal()) {
19192
20536
  return;
@@ -19213,18 +20557,20 @@ class DbxPdfMergeEditorComponent {
19213
20557
  });
19214
20558
  }
19215
20559
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: DbxPdfMergeEditorComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
19216
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: DbxPdfMergeEditorComponent, isStandalone: true, selector: "dbx-pdf-merge-editor", inputs: { accept: { classPropertyName: "accept", publicName: "accept", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, fileName: { classPropertyName: "fileName", publicName: "fileName", isSignal: true, isRequired: false, transformFunction: null }, showDownloadButton: { classPropertyName: "showDownloadButton", publicName: "showDownloadButton", isSignal: true, isRequired: false, transformFunction: null }, showPreviewButton: { classPropertyName: "showPreviewButton", publicName: "showPreviewButton", isSignal: true, isRequired: false, transformFunction: null }, downloadButton: { classPropertyName: "downloadButton", publicName: "downloadButton", isSignal: true, isRequired: false, transformFunction: null }, showAddFiles: { classPropertyName: "showAddFiles", publicName: "showAddFiles", isSignal: true, isRequired: false, transformFunction: null }, showFileList: { classPropertyName: "showFileList", publicName: "showFileList", isSignal: true, isRequired: false, transformFunction: null }, config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { entriesChanged: "entriesChanged" }, host: { classAttribute: "dbx-pdf-merge-editor d-block" }, ngImport: i0, template: "@if (showAddFilesSignal()) {\n <div class=\"dbx-pdf-merge-editor-upload d-block dbx-mb3\">\n <dbx-file-upload [accept]=\"acceptSignal()\" [multiple]=\"multipleSignal()\" (filesChanged)=\"onFiles($event)\" [hint]=\"'Drop PDFs or images here, or click to browse'\" [text]=\"'Add files'\" icon=\"upload_file\"></dbx-file-upload>\n </div>\n}\n<ng-content></ng-content>\n@if (showFileListSignal()) {\n <dbx-pdf-merge-list></dbx-pdf-merge-list>\n}\n@if (focusActiveSignal()) {\n <div class=\"dbx-pdf-merge-editor-encrypted-banner\">\n <mat-icon>lock</mat-icon>\n <span>Encrypted PDF detected \u2014 only the encrypted file will be used; other files are ignored.</span>\n </div>\n}\n@switch (outputSizeStateSignal()) {\n @case ('warn') {\n <div class=\"dbx-pdf-merge-editor-size-warning\">\n <mat-icon>warning</mat-icon>\n <span>Merged file is {{ formattedOutputSizeSignal() }} \u2014 above the recommended {{ formattedWarnLimitSignal() }} limit.</span>\n </div>\n }\n @case ('error') {\n <div class=\"dbx-pdf-merge-editor-size-error\">\n <mat-icon>error</mat-icon>\n <span>Merged file is {{ formattedOutputSizeSignal() }}, exceeds the {{ formattedErrorLimitSignal() }} limit. Remove or compress files to continue.</span>\n </div>\n }\n}\n<div class=\"dbx-pdf-merge-editor-actions\">\n <span class=\"dbx-hint dbx-small\">{{ entryCountSignal() }} file(s)</span>\n <span class=\"dbx-spacer\"></span>\n <dbx-button text=\"Clear\" icon=\"delete\" [disabled]=\"entryCountSignal() === 0\" (buttonClick)=\"onClear()\"></dbx-button>\n @if (showPreviewButtonSignal()) {\n <dbx-button text=\"Preview\" icon=\"picture_as_pdf\" [disabled]=\"!canMergeSignal()\" (buttonClick)=\"onPreview()\"></dbx-button>\n }\n @if (showDownloadButtonSignal() && canMergeSignal()) {\n <dbx-download-blob-button [config]=\"downloadConfigSignal()\"></dbx-download-blob-button>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i1$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: DbxButtonComponent, selector: "dbx-button", inputs: ["bar", "type", "buttonStyle", "color", "spinnerColor", "customButtonColor", "customTextColor", "customSpinnerColor", "basic", "tonal", "raised", "stroked", "flat", "iconOnly", "fab", "customContent", "allowClickPropagation", "mode"] }, { kind: "component", type: DbxFileUploadComponent, selector: "dbx-file-upload", inputs: ["config", "buttonStyle", "buttonDisplay", "mode", "text", "icon", "hint", "clickAreaToUpload"], outputs: ["filesChanged"] }, { kind: "component", type: DbxDownloadBlobButtonComponent, selector: "dbx-download-blob-button", inputs: ["config"] }, { kind: "component", type: DbxPdfMergeListComponent, selector: "dbx-pdf-merge-list" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
20560
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: DbxPdfMergeEditorComponent, isStandalone: true, selector: "dbx-pdf-merge-editor", inputs: { accept: { classPropertyName: "accept", publicName: "accept", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null }, fileName: { classPropertyName: "fileName", publicName: "fileName", isSignal: true, isRequired: false, transformFunction: null }, showDownloadButton: { classPropertyName: "showDownloadButton", publicName: "showDownloadButton", isSignal: true, isRequired: false, transformFunction: null }, showPreviewButton: { classPropertyName: "showPreviewButton", publicName: "showPreviewButton", isSignal: true, isRequired: false, transformFunction: null }, downloadButton: { classPropertyName: "downloadButton", publicName: "downloadButton", isSignal: true, isRequired: false, transformFunction: null }, showAddFiles: { classPropertyName: "showAddFiles", publicName: "showAddFiles", isSignal: true, isRequired: false, transformFunction: null }, showFileList: { classPropertyName: "showFileList", publicName: "showFileList", isSignal: true, isRequired: false, transformFunction: null }, pageEditing: { classPropertyName: "pageEditing", publicName: "pageEditing", isSignal: true, isRequired: false, transformFunction: null }, sidecar: { classPropertyName: "sidecar", publicName: "sidecar", isSignal: true, isRequired: false, transformFunction: null }, config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { entriesChanged: "entriesChanged" }, host: { classAttribute: "dbx-pdf-merge-editor d-block" }, ngImport: i0, template: "@if (showAddFilesSignal()) {\n <div class=\"dbx-pdf-merge-editor-upload d-block dbx-mb3\">\n <dbx-file-upload [accept]=\"acceptSignal()\" [multiple]=\"multipleSignal()\" (filesChanged)=\"onFiles($event)\" [hint]=\"'Drop PDFs or images here, or click to browse'\" [text]=\"'Add files'\" icon=\"upload_file\"></dbx-file-upload>\n </div>\n}\n<ng-content></ng-content>\n@if (showFileListSignal()) {\n @if (pageEditingSignal()) {\n <dbx-pdf-merge-page-list></dbx-pdf-merge-page-list>\n } @else {\n <dbx-pdf-merge-list></dbx-pdf-merge-list>\n }\n}\n@if (focusActiveSignal()) {\n <div class=\"dbx-pdf-merge-editor-encrypted-banner\">\n <mat-icon>lock</mat-icon>\n <span>Encrypted PDF detected \u2014 only the encrypted file will be used; other files are ignored.</span>\n </div>\n}\n@switch (outputSizeStateSignal()) {\n @case ('warn') {\n <div class=\"dbx-pdf-merge-editor-size-warning\">\n <mat-icon>warning</mat-icon>\n <span>Merged file is {{ formattedOutputSizeSignal() }} \u2014 above the recommended {{ formattedWarnLimitSignal() }} limit.</span>\n </div>\n }\n @case ('error') {\n <div class=\"dbx-pdf-merge-editor-size-error\">\n <mat-icon>error</mat-icon>\n <span>Merged file is {{ formattedOutputSizeSignal() }}, exceeds the {{ formattedErrorLimitSignal() }} limit. Remove or compress files to continue.</span>\n </div>\n }\n}\n<div class=\"dbx-pdf-merge-editor-actions\">\n <span class=\"dbx-hint dbx-small\">{{ entryCountSignal() }} file(s)</span>\n @if (pageEditingSignal()) {\n <span class=\"dbx-hint dbx-small\">{{ mergeablePageCountSignal() }} page(s)</span>\n }\n <span class=\"dbx-spacer\"></span>\n <!-- Emptiness gates the button through the action's own disabled channel: dbxActionButton pushes\n `isDisabled$` into the button, which would otherwise win over a plain [disabled] input. -->\n <div dbxAction dbxActionSnackbarError [dbxActionDisabled]=\"entryCountSignal() === 0\" [dbxActionHandler]=\"handleClear\" [dbxActionConfirm]=\"clearConfirmSignal()\">\n <dbx-button dbxActionButton text=\"Clear\" icon=\"delete\"></dbx-button>\n </div>\n @if (showPreviewButtonSignal()) {\n <dbx-button text=\"Preview\" icon=\"picture_as_pdf\" [disabled]=\"!canMergeSignal()\" (buttonClick)=\"onPreview()\"></dbx-button>\n }\n @if (showDownloadButtonSignal() && canMergeSignal()) {\n <dbx-download-blob-button [config]=\"downloadConfigSignal()\"></dbx-download-blob-button>\n }\n</div>\n", dependencies: [{ kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i1$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: DbxButtonComponent, selector: "dbx-button", inputs: ["bar", "type", "buttonStyle", "color", "spinnerColor", "customButtonColor", "customTextColor", "customSpinnerColor", "basic", "tonal", "raised", "stroked", "flat", "iconOnly", "fab", "customContent", "allowClickPropagation", "mode"] }, { kind: "component", type: DbxFileUploadComponent, selector: "dbx-file-upload", inputs: ["config", "buttonStyle", "buttonDisplay", "mode", "text", "icon", "hint", "clickAreaToUpload"], outputs: ["filesChanged"] }, { kind: "component", type: DbxDownloadBlobButtonComponent, selector: "dbx-download-blob-button", inputs: ["config"] }, { kind: "directive", type: DbxActionDirective, selector: "dbx-action,[dbxAction]", exportAs: ["action", "dbxAction"] }, { kind: "directive", type: DbxActionButtonDirective, selector: "[dbxActionButton]", inputs: ["dbxActionButtonEcho"] }, { kind: "directive", type: DbxActionDisabledDirective, selector: "[dbxActionDisabled]", inputs: ["dbxActionDisabled"] }, { kind: "directive", type: DbxActionHandlerDirective, selector: "[dbxActionHandler]", inputs: ["dbxActionHandler"] }, { kind: "directive", type: DbxActionConfirmDirective, selector: "[dbxActionConfirm]", inputs: ["dbxActionConfirm", "dbxActionConfirmSkip"] }, { kind: "directive", type: DbxActionSnackbarErrorDirective, selector: "[dbxActionSnackbarError]", inputs: ["dbxActionSnackbarError"] }, { kind: "component", type: DbxPdfMergeListComponent, selector: "dbx-pdf-merge-list" }, { kind: "component", type: DbxPdfMergePageListComponent, selector: "dbx-pdf-merge-page-list", inputs: ["slotId", "showGroupLabels"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
19217
20561
  }
19218
20562
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: DbxPdfMergeEditorComponent, decorators: [{
19219
20563
  type: Component,
19220
20564
  args: [{ selector: 'dbx-pdf-merge-editor', host: {
19221
20565
  class: 'dbx-pdf-merge-editor d-block'
19222
- }, imports: [MatIconModule, DbxButtonComponent, DbxFileUploadComponent, DbxDownloadBlobButtonComponent, DbxPdfMergeListComponent], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, template: "@if (showAddFilesSignal()) {\n <div class=\"dbx-pdf-merge-editor-upload d-block dbx-mb3\">\n <dbx-file-upload [accept]=\"acceptSignal()\" [multiple]=\"multipleSignal()\" (filesChanged)=\"onFiles($event)\" [hint]=\"'Drop PDFs or images here, or click to browse'\" [text]=\"'Add files'\" icon=\"upload_file\"></dbx-file-upload>\n </div>\n}\n<ng-content></ng-content>\n@if (showFileListSignal()) {\n <dbx-pdf-merge-list></dbx-pdf-merge-list>\n}\n@if (focusActiveSignal()) {\n <div class=\"dbx-pdf-merge-editor-encrypted-banner\">\n <mat-icon>lock</mat-icon>\n <span>Encrypted PDF detected \u2014 only the encrypted file will be used; other files are ignored.</span>\n </div>\n}\n@switch (outputSizeStateSignal()) {\n @case ('warn') {\n <div class=\"dbx-pdf-merge-editor-size-warning\">\n <mat-icon>warning</mat-icon>\n <span>Merged file is {{ formattedOutputSizeSignal() }} \u2014 above the recommended {{ formattedWarnLimitSignal() }} limit.</span>\n </div>\n }\n @case ('error') {\n <div class=\"dbx-pdf-merge-editor-size-error\">\n <mat-icon>error</mat-icon>\n <span>Merged file is {{ formattedOutputSizeSignal() }}, exceeds the {{ formattedErrorLimitSignal() }} limit. Remove or compress files to continue.</span>\n </div>\n }\n}\n<div class=\"dbx-pdf-merge-editor-actions\">\n <span class=\"dbx-hint dbx-small\">{{ entryCountSignal() }} file(s)</span>\n <span class=\"dbx-spacer\"></span>\n <dbx-button text=\"Clear\" icon=\"delete\" [disabled]=\"entryCountSignal() === 0\" (buttonClick)=\"onClear()\"></dbx-button>\n @if (showPreviewButtonSignal()) {\n <dbx-button text=\"Preview\" icon=\"picture_as_pdf\" [disabled]=\"!canMergeSignal()\" (buttonClick)=\"onPreview()\"></dbx-button>\n }\n @if (showDownloadButtonSignal() && canMergeSignal()) {\n <dbx-download-blob-button [config]=\"downloadConfigSignal()\"></dbx-download-blob-button>\n }\n</div>\n" }]
19223
- }], ctorParameters: () => [], propDecorators: { accept: [{ type: i0.Input, args: [{ isSignal: true, alias: "accept", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], fileName: [{ type: i0.Input, args: [{ isSignal: true, alias: "fileName", required: false }] }], showDownloadButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "showDownloadButton", required: false }] }], showPreviewButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "showPreviewButton", required: false }] }], downloadButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "downloadButton", required: false }] }], showAddFiles: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAddFiles", required: false }] }], showFileList: [{ type: i0.Input, args: [{ isSignal: true, alias: "showFileList", required: false }] }], config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: false }] }], entriesChanged: [{ type: i0.Output, args: ["entriesChanged"] }] } });
20566
+ }, imports: [MatIconModule, DbxButtonComponent, DbxFileUploadComponent, DbxDownloadBlobButtonComponent, DbxActionDirective, DbxActionButtonDirective, DbxActionDisabledDirective, DbxActionHandlerDirective, DbxActionConfirmDirective, DbxActionSnackbarErrorDirective, DbxPdfMergeListComponent, DbxPdfMergePageListComponent], changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, template: "@if (showAddFilesSignal()) {\n <div class=\"dbx-pdf-merge-editor-upload d-block dbx-mb3\">\n <dbx-file-upload [accept]=\"acceptSignal()\" [multiple]=\"multipleSignal()\" (filesChanged)=\"onFiles($event)\" [hint]=\"'Drop PDFs or images here, or click to browse'\" [text]=\"'Add files'\" icon=\"upload_file\"></dbx-file-upload>\n </div>\n}\n<ng-content></ng-content>\n@if (showFileListSignal()) {\n @if (pageEditingSignal()) {\n <dbx-pdf-merge-page-list></dbx-pdf-merge-page-list>\n } @else {\n <dbx-pdf-merge-list></dbx-pdf-merge-list>\n }\n}\n@if (focusActiveSignal()) {\n <div class=\"dbx-pdf-merge-editor-encrypted-banner\">\n <mat-icon>lock</mat-icon>\n <span>Encrypted PDF detected \u2014 only the encrypted file will be used; other files are ignored.</span>\n </div>\n}\n@switch (outputSizeStateSignal()) {\n @case ('warn') {\n <div class=\"dbx-pdf-merge-editor-size-warning\">\n <mat-icon>warning</mat-icon>\n <span>Merged file is {{ formattedOutputSizeSignal() }} \u2014 above the recommended {{ formattedWarnLimitSignal() }} limit.</span>\n </div>\n }\n @case ('error') {\n <div class=\"dbx-pdf-merge-editor-size-error\">\n <mat-icon>error</mat-icon>\n <span>Merged file is {{ formattedOutputSizeSignal() }}, exceeds the {{ formattedErrorLimitSignal() }} limit. Remove or compress files to continue.</span>\n </div>\n }\n}\n<div class=\"dbx-pdf-merge-editor-actions\">\n <span class=\"dbx-hint dbx-small\">{{ entryCountSignal() }} file(s)</span>\n @if (pageEditingSignal()) {\n <span class=\"dbx-hint dbx-small\">{{ mergeablePageCountSignal() }} page(s)</span>\n }\n <span class=\"dbx-spacer\"></span>\n <!-- Emptiness gates the button through the action's own disabled channel: dbxActionButton pushes\n `isDisabled$` into the button, which would otherwise win over a plain [disabled] input. -->\n <div dbxAction dbxActionSnackbarError [dbxActionDisabled]=\"entryCountSignal() === 0\" [dbxActionHandler]=\"handleClear\" [dbxActionConfirm]=\"clearConfirmSignal()\">\n <dbx-button dbxActionButton text=\"Clear\" icon=\"delete\"></dbx-button>\n </div>\n @if (showPreviewButtonSignal()) {\n <dbx-button text=\"Preview\" icon=\"picture_as_pdf\" [disabled]=\"!canMergeSignal()\" (buttonClick)=\"onPreview()\"></dbx-button>\n }\n @if (showDownloadButtonSignal() && canMergeSignal()) {\n <dbx-download-blob-button [config]=\"downloadConfigSignal()\"></dbx-download-blob-button>\n }\n</div>\n" }]
20567
+ }], ctorParameters: () => [], propDecorators: { accept: [{ type: i0.Input, args: [{ isSignal: true, alias: "accept", required: false }] }], multiple: [{ type: i0.Input, args: [{ isSignal: true, alias: "multiple", required: false }] }], fileName: [{ type: i0.Input, args: [{ isSignal: true, alias: "fileName", required: false }] }], showDownloadButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "showDownloadButton", required: false }] }], showPreviewButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "showPreviewButton", required: false }] }], downloadButton: [{ type: i0.Input, args: [{ isSignal: true, alias: "downloadButton", required: false }] }], showAddFiles: [{ type: i0.Input, args: [{ isSignal: true, alias: "showAddFiles", required: false }] }], showFileList: [{ type: i0.Input, args: [{ isSignal: true, alias: "showFileList", required: false }] }], pageEditing: [{ type: i0.Input, args: [{ isSignal: true, alias: "pageEditing", required: false }] }], sidecar: [{ type: i0.Input, args: [{ isSignal: true, alias: "sidecar", required: false }] }], config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: false }] }], entriesChanged: [{ type: i0.Output, args: ["entriesChanged"] }] } });
19224
20568
 
19225
20569
  /**
19226
20570
  * Directive that registers as the active {@link DbxPdfMergeEditorValidator} on a {@link DbxPdfMergeEditorStore} and gates merge emissions on the readiness of every registered slot. {@link DbxPdfMergeEditorFileUploadComponent} instances walk the injector tree, find this directive, and call {@link registerSlot}/{@link unregisterSlot} during their own lifecycle.
19227
20571
  *
20572
+ * This slot set is intentionally separate from {@link DbxPdfMergeEditorStore.registeredSlotIds$}, and the two should not be merged. This one is keyed by object identity because it exists solely to `combineLatest` each slot's `isValid$`; the store's is keyed by slot id, answers "which sections does this editor declare", and must be populated whether or not a validator is present — which is why it cannot live on this optional directive.
20573
+ *
19228
20574
  * @example
19229
20575
  * ```html
19230
20576
  * <dbx-pdf-merge-editor [showAddFiles]="false" [showFileList]="false">
@@ -19293,11 +20639,19 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
19293
20639
 
19294
20640
  const DEFAULT_MIN_FILES = 1;
19295
20641
  const DEFAULT_REQUIRED = true;
20642
+ const DEFAULT_ADD_BUTTON_TEXT = 'Add';
20643
+ const DEFAULT_CLEAR_BUTTON_TEXT = 'Clear';
20644
+ const DEFAULT_CLEAR_CONFIRM_TITLE = 'Clear this section?';
20645
+ const DEFAULT_CLEAR_CONFIRM_PROMPT = 'Every file in this section will be removed, along with any page edits made to them.';
20646
+ const DEFAULT_CLEAR_CONFIRM_CONFIRM_TEXT = 'Clear';
20647
+ const DEFAULT_CLEAR_CONFIRM_CANCEL_TEXT = 'Cancel';
19296
20648
  /**
19297
20649
  * Slot-scoped uploader for use inside a {@link DbxPdfMergeEditorComponent}. Adds files to the shared {@link DbxPdfMergeEditorStore} tagged with this slot's `slotId`, displays the slot's owned entries inline using {@link DbxPdfMergeEntryComponent}, and reports its readiness to the optional ancestor {@link DbxPdfMergeEditorFileUploadValidatorDirective}. On destroy the slot removes its owned entries from the store.
19298
20650
  *
19299
20651
  * Projects an `<ng-content>` slot inside its header element so consumers can render state-aware indicators (e.g. via the `dbxPdfMergeEditorFileUploadHasState` structural directive) alongside the optional `label`.
19300
20652
  *
20653
+ * Once the slot owns entries its header gains an Add button (while the slot is under capacity) and a Clear button, so a section that already has content can still be extended, emptied, or replaced. Clearing runs as its own `dbxAction` behind a `dbxActionConfirm` prompt — a misclick costs a dialog, not the section's files.
20654
+ *
19301
20655
  * @example
19302
20656
  * ```html
19303
20657
  * <dbx-pdf-merge-editor-file-upload slotId="license" [config]="{ label: 'Driver’s License', accept: ['application/pdf'] }">
@@ -19310,6 +20664,10 @@ class DbxPdfMergeEditorFileUploadComponent {
19310
20664
  _validator = inject(DbxPdfMergeEditorFileUploadValidatorDirective, { optional: true });
19311
20665
  _injectedConfig = inject(DBX_PDF_MERGE_EDITOR_CONFIG, { optional: true });
19312
20666
  _preserveEntriesOnDestroy = inject(DBX_PDF_MERGE_EDITOR_PRESERVE_ENTRIES_ON_SLOT_DESTROY, { optional: true }) ?? false;
20667
+ /**
20668
+ * The slot id this component registered with the store, captured at registration time so destroy deregisters the same key. `null` before {@link ngOnInit} and after teardown.
20669
+ */
20670
+ _registeredSlotId = null;
19313
20671
  slotId = input.required(...(ngDevMode ? [{ debugName: "slotId" }] : /* istanbul ignore next */ []));
19314
20672
  config = input(...(ngDevMode ? [undefined, { debugName: "config" }] : /* istanbul ignore next */ []));
19315
20673
  acceptSignal = computed(() => this.config()?.accept ?? DEFAULT_PDF_MERGE_ACCEPT, ...(ngDevMode ? [{ debugName: "acceptSignal" }] : /* istanbul ignore next */ []));
@@ -19346,13 +20704,78 @@ class DbxPdfMergeEditorFileUploadComponent {
19346
20704
  ownedEntries$ = toObservable(this.slotId).pipe(switchMap((slotId) => this.store.entriesForSlotId$(slotId)), shareReplay(1));
19347
20705
  ownedEntriesSignal = toSignal(this.ownedEntries$, { initialValue: [] });
19348
20706
  /**
19349
- * Whether the slot still has room for more files. Drives visibility of the upload UI — once the slot is at capacity, the uploader is hidden and the user must remove an existing entry to add another.
20707
+ * Whether the shared store has page editing enabled. Drives whether this slot lists its own pages or its own files.
20708
+ */
20709
+ pageEditingSignal = toSignal(this.store.pageEditing$, { initialValue: false });
20710
+ /**
20711
+ * Pages contributed by this slot's entries. Empty while page editing is disabled.
20712
+ */
20713
+ ownedPages$ = toObservable(this.slotId).pipe(switchMap((slotId) => this.store.pagesForSlotId$(slotId)), shareReplay(1));
20714
+ /**
20715
+ * Whether the slot still has room for more files. Gates every add affordance — once the slot is at capacity neither the drop area nor the header Add button is offered, and the user must clear the slot (or remove an entry) to add another.
19350
20716
  */
19351
20717
  canAddFilesSignal = computed(() => this.ownedEntriesSignal().length < this.capacitySignal(), ...(ngDevMode ? [{ debugName: "canAddFilesSignal" }] : /* istanbul ignore next */ []));
20718
+ showAddButtonConfigSignal = computed(() => this.config()?.showAddButton ?? true, ...(ngDevMode ? [{ debugName: "showAddButtonConfigSignal" }] : /* istanbul ignore next */ []));
20719
+ showClearButtonConfigSignal = computed(() => this.config()?.showClearButton ?? true, ...(ngDevMode ? [{ debugName: "showClearButtonConfigSignal" }] : /* istanbul ignore next */ []));
20720
+ /**
20721
+ * Whether the compact header Add button is rendered. Only meaningful once the slot owns something — while it is empty the drop area is already the add affordance.
20722
+ */
20723
+ showAddButtonSignal = computed(() => {
20724
+ const ownedEntries = this.ownedEntriesSignal();
20725
+ const canAddFiles = this.canAddFilesSignal();
20726
+ return this.showAddButtonConfigSignal() && ownedEntries.length > 0 && canAddFiles;
20727
+ }, ...(ngDevMode ? [{ debugName: "showAddButtonSignal" }] : /* istanbul ignore next */ []));
20728
+ showClearButtonSignal = computed(() => {
20729
+ const ownedEntries = this.ownedEntriesSignal();
20730
+ return this.showClearButtonConfigSignal() && ownedEntries.length > 0;
20731
+ }, ...(ngDevMode ? [{ debugName: "showClearButtonSignal" }] : /* istanbul ignore next */ []));
20732
+ /**
20733
+ * Whether the full drop area is rendered. It is the slot's empty state while the header Add button is enabled; with the button turned off it falls back to staying visible until the slot reaches capacity.
20734
+ */
20735
+ showUploadAreaSignal = computed(() => {
20736
+ const showAddButtonConfig = this.showAddButtonConfigSignal();
20737
+ const canAddFiles = this.canAddFilesSignal();
20738
+ const isEmpty = this.ownedEntriesSignal().length === 0;
20739
+ return canAddFiles && (isEmpty || !showAddButtonConfig);
20740
+ }, ...(ngDevMode ? [{ debugName: "showUploadAreaSignal" }] : /* istanbul ignore next */ []));
20741
+ addButtonTextSignal = computed(() => this.config()?.addButtonText ?? DEFAULT_ADD_BUTTON_TEXT, ...(ngDevMode ? [{ debugName: "addButtonTextSignal" }] : /* istanbul ignore next */ []));
20742
+ clearButtonTextSignal = computed(() => this.config()?.clearButtonText ?? DEFAULT_CLEAR_BUTTON_TEXT, ...(ngDevMode ? [{ debugName: "clearButtonTextSignal" }] : /* istanbul ignore next */ []));
20743
+ addButtonAriaLabelSignal = computed(() => {
20744
+ const label = this.labelSignal();
20745
+ return label ? `Add files to ${label}` : 'Add files to this section';
20746
+ }, ...(ngDevMode ? [{ debugName: "addButtonAriaLabelSignal" }] : /* istanbul ignore next */ []));
20747
+ clearButtonAriaLabelSignal = computed(() => {
20748
+ const label = this.labelSignal();
20749
+ return label ? `Clear ${label}` : 'Clear this section';
20750
+ }, ...(ngDevMode ? [{ debugName: "clearButtonAriaLabelSignal" }] : /* istanbul ignore next */ []));
20751
+ /**
20752
+ * Config for the confirmation shown before the slot is cleared. The slot's `label` is folded into the default title so a page of sections says which one is about to be emptied; `clearConfirm` overrides any field of it.
20753
+ */
20754
+ clearConfirmSignal = computed(() => {
20755
+ const label = this.labelSignal();
20756
+ const overrides = this.config()?.clearConfirm;
20757
+ return {
20758
+ title: label ? `Clear ${label}?` : DEFAULT_CLEAR_CONFIRM_TITLE,
20759
+ prompt: DEFAULT_CLEAR_CONFIRM_PROMPT,
20760
+ confirmText: DEFAULT_CLEAR_CONFIRM_CONFIRM_TEXT,
20761
+ cancelText: DEFAULT_CLEAR_CONFIRM_CANCEL_TEXT,
20762
+ ...overrides
20763
+ };
20764
+ }, ...(ngDevMode ? [{ debugName: "clearConfirmSignal" }] : /* istanbul ignore next */ []));
20765
+ /**
20766
+ * Clears every entry this slot owns. Runs as the handler of the header's own `dbxAction`, so the `dbxActionConfirm` on that action is what stands between a misclick and the section's files.
20767
+ *
20768
+ * @param _value - Unused. The confirmation supplies no value, only the go-ahead.
20769
+ * @param context - Work context completed as soon as the store drops the entries.
20770
+ */
20771
+ handleClear = (_value, context) => {
20772
+ this.store.removeEntriesBySlotId(this.slotId());
20773
+ context.success();
20774
+ };
19352
20775
  /**
19353
20776
  * High-level state of the slot — `no_file` when empty, `valid` when owned entries satisfy the slot's thresholds, `invalid` when owned entries fail or are still being validated.
19354
20777
  */
19355
- state$ = this.ownedEntries$.pipe(map((entries) => {
20778
+ state$ = combineLatest([this.ownedEntries$, this.store.pageEditing$, this.ownedPages$]).pipe(map(([entries, pageEditing, pages]) => {
19356
20779
  let state;
19357
20780
  if (entries.length === 0) {
19358
20781
  state = 'no_file';
@@ -19362,6 +20785,10 @@ class DbxPdfMergeEditorFileUploadComponent {
19362
20785
  const readyCount = entries.filter((entry) => entry.status === 'ready').length;
19363
20786
  const minFiles = this.minFilesSignal();
19364
20787
  const maxFiles = this.maxFilesSignal();
20788
+ // Only meaningful once the slot actually has pages: an empty list means editing is off, the
20789
+ // entry is still hydrating, or it could not be expanded (encrypted) — none of which are the
20790
+ // user deleting their own content.
20791
+ const allPagesRemoved = pageEditing && pages.length > 0 && pages.every((page) => page.removed);
19365
20792
  if (validating) {
19366
20793
  state = 'invalid';
19367
20794
  }
@@ -19371,6 +20798,9 @@ class DbxPdfMergeEditorFileUploadComponent {
19371
20798
  else if (maxFiles != null && readyCount > maxFiles) {
19372
20799
  state = 'invalid';
19373
20800
  }
20801
+ else if (allPagesRemoved) {
20802
+ state = 'invalid';
20803
+ }
19374
20804
  else {
19375
20805
  state = 'valid';
19376
20806
  }
@@ -19397,9 +20827,26 @@ class DbxPdfMergeEditorFileUploadComponent {
19397
20827
  }), distinctUntilChanged(), shareReplay(1));
19398
20828
  isValidSignal = toSignal(this.isValid$, { initialValue: false });
19399
20829
  ngOnInit() {
20830
+ // Two registrations, deliberately: the store learns this slot's ID (so consumers like
20831
+ // <dbx-pdf-merge-import> can enumerate the sections this editor declares), while the optional
20832
+ // validator directive tracks the component itself to aggregate `isValid$`. They are separate
20833
+ // because the store is always present while the validator is not, and because the store's
20834
+ // `setValidator` seam exists precisely so it never has to hold slot component references.
20835
+ this._registeredSlotId = this.slotId();
20836
+ this.store.registerSlotId(this._registeredSlotId);
19400
20837
  this._validator?.registerSlot(this);
19401
20838
  }
19402
20839
  ngOnDestroy() {
20840
+ // Unconditional, and deliberately OUTSIDE the `_preserveEntriesOnDestroy` check below: that
20841
+ // token governs whether the slot's ENTRIES survive, not whether the slot is mounted. The
20842
+ // upload dialog sets it to `true` and is torn down on every close, so gating this on it would
20843
+ // leave the store permanently claiming a section that is no longer on screen. The captured id
20844
+ // (rather than a fresh `slotId()` read) guarantees this decrements the same key `ngOnInit`
20845
+ // incremented, and clearing it keeps a repeat destroy from evicting a still-mounted duplicate.
20846
+ if (this._registeredSlotId != null) {
20847
+ this.store.unregisterSlotId(this._registeredSlotId);
20848
+ this._registeredSlotId = null;
20849
+ }
19403
20850
  this._validator?.unregisterSlot(this);
19404
20851
  // Default behavior: removing a slot from a template (e.g. via `@if`) also drops the slot's
19405
20852
  // entries from the store. Opt out by providing `DBX_PDF_MERGE_EDITOR_PRESERVE_ENTRIES_ON_SLOT_DESTROY`
@@ -19460,20 +20907,36 @@ class DbxPdfMergeEditorFileUploadComponent {
19460
20907
  <span class="dbx-pdf-merge-editor-file-upload-label">{{ label }}</span>
19461
20908
  }
19462
20909
  <ng-content></ng-content>
20910
+ @if (showAddButtonSignal() || showClearButtonSignal()) {
20911
+ <div class="dbx-pdf-merge-editor-file-upload-actions">
20912
+ @if (showAddButtonSignal()) {
20913
+ <dbx-file-upload-button [accept]="acceptSignal()" [multiple]="multipleSignal()" [text]="addButtonTextSignal()" icon="add" [ariaLabel]="addButtonAriaLabelSignal()" (filesChanged)="onFiles($event)"></dbx-file-upload-button>
20914
+ }
20915
+ @if (showClearButtonSignal()) {
20916
+ <div dbxAction dbxActionSnackbarError [dbxActionHandler]="handleClear" [dbxActionConfirm]="clearConfirmSignal()">
20917
+ <dbx-button dbxActionButton [text]="clearButtonTextSignal()" icon="delete" [ariaLabel]="clearButtonAriaLabelSignal()"></dbx-button>
20918
+ </div>
20919
+ }
20920
+ </div>
20921
+ }
19463
20922
  </div>
19464
- @if (canAddFilesSignal()) {
20923
+ @if (showUploadAreaSignal()) {
19465
20924
  <dbx-file-upload [accept]="acceptSignal()" [multiple]="multipleSignal()" [mode]="modeSignal()" [hint]="hintSignal()" [text]="textSignal()" [icon]="iconSignal()" (filesChanged)="onFiles($event)"></dbx-file-upload>
19466
20925
  }
19467
20926
  @if (ownedEntriesSignal(); as owned) {
19468
20927
  @if (owned.length > 0) {
19469
- <div class="dbx-pdf-merge-editor-file-upload-entries" cdkDropList (cdkDropListDropped)="onDrop($event)">
19470
- @for (entry of owned; track entry.id) {
19471
- <dbx-pdf-merge-entry [entry]="entry"></dbx-pdf-merge-entry>
19472
- }
19473
- </div>
20928
+ @if (pageEditingSignal()) {
20929
+ <dbx-pdf-merge-page-list class="dbx-pdf-merge-editor-file-upload-pages" [slotId]="slotId()"></dbx-pdf-merge-page-list>
20930
+ } @else {
20931
+ <div class="dbx-pdf-merge-editor-file-upload-entries" cdkDropList (cdkDropListDropped)="onDrop($event)">
20932
+ @for (entry of owned; track entry.id) {
20933
+ <dbx-pdf-merge-entry [entry]="entry"></dbx-pdf-merge-entry>
20934
+ }
20935
+ </div>
20936
+ }
19474
20937
  }
19475
20938
  }
19476
- `, isInline: true, dependencies: [{ kind: "directive", type: CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "component", type: DbxFileUploadComponent, selector: "dbx-file-upload", inputs: ["config", "buttonStyle", "buttonDisplay", "mode", "text", "icon", "hint", "clickAreaToUpload"], outputs: ["filesChanged"] }, { kind: "component", type: DbxPdfMergeEntryComponent, selector: "dbx-pdf-merge-entry", inputs: ["entry"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
20939
+ `, isInline: true, dependencies: [{ kind: "directive", type: CdkDropList, selector: "[cdkDropList], cdk-drop-list", inputs: ["cdkDropListConnectedTo", "cdkDropListData", "cdkDropListOrientation", "id", "cdkDropListLockAxis", "cdkDropListDisabled", "cdkDropListSortingDisabled", "cdkDropListEnterPredicate", "cdkDropListSortPredicate", "cdkDropListAutoScrollDisabled", "cdkDropListAutoScrollStep", "cdkDropListElementContainer", "cdkDropListHasAnchor"], outputs: ["cdkDropListDropped", "cdkDropListEntered", "cdkDropListExited", "cdkDropListSorted"], exportAs: ["cdkDropList"] }, { kind: "component", type: DbxButtonComponent, selector: "dbx-button", inputs: ["bar", "type", "buttonStyle", "color", "spinnerColor", "customButtonColor", "customTextColor", "customSpinnerColor", "basic", "tonal", "raised", "stroked", "flat", "iconOnly", "fab", "customContent", "allowClickPropagation", "mode"] }, { kind: "component", type: DbxFileUploadComponent, selector: "dbx-file-upload", inputs: ["config", "buttonStyle", "buttonDisplay", "mode", "text", "icon", "hint", "clickAreaToUpload"], outputs: ["filesChanged"] }, { kind: "component", type: DbxFileUploadButtonComponent, selector: "dbx-file-upload-button", inputs: ["text", "icon", "ariaLabel", "buttonStyle"], outputs: ["filesChanged"] }, { kind: "directive", type: DbxActionDirective, selector: "dbx-action,[dbxAction]", exportAs: ["action", "dbxAction"] }, { kind: "directive", type: DbxActionButtonDirective, selector: "[dbxActionButton]", inputs: ["dbxActionButtonEcho"] }, { kind: "directive", type: DbxActionHandlerDirective, selector: "[dbxActionHandler]", inputs: ["dbxActionHandler"] }, { kind: "directive", type: DbxActionConfirmDirective, selector: "[dbxActionConfirm]", inputs: ["dbxActionConfirm", "dbxActionConfirmSkip"] }, { kind: "directive", type: DbxActionSnackbarErrorDirective, selector: "[dbxActionSnackbarError]", inputs: ["dbxActionSnackbarError"] }, { kind: "component", type: DbxPdfMergeEntryComponent, selector: "dbx-pdf-merge-entry", inputs: ["entry"] }, { kind: "component", type: DbxPdfMergePageListComponent, selector: "dbx-pdf-merge-page-list", inputs: ["slotId", "showGroupLabels"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
19477
20940
  }
19478
20941
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: DbxPdfMergeEditorFileUploadComponent, decorators: [{
19479
20942
  type: Component,
@@ -19485,17 +20948,33 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
19485
20948
  <span class="dbx-pdf-merge-editor-file-upload-label">{{ label }}</span>
19486
20949
  }
19487
20950
  <ng-content></ng-content>
20951
+ @if (showAddButtonSignal() || showClearButtonSignal()) {
20952
+ <div class="dbx-pdf-merge-editor-file-upload-actions">
20953
+ @if (showAddButtonSignal()) {
20954
+ <dbx-file-upload-button [accept]="acceptSignal()" [multiple]="multipleSignal()" [text]="addButtonTextSignal()" icon="add" [ariaLabel]="addButtonAriaLabelSignal()" (filesChanged)="onFiles($event)"></dbx-file-upload-button>
20955
+ }
20956
+ @if (showClearButtonSignal()) {
20957
+ <div dbxAction dbxActionSnackbarError [dbxActionHandler]="handleClear" [dbxActionConfirm]="clearConfirmSignal()">
20958
+ <dbx-button dbxActionButton [text]="clearButtonTextSignal()" icon="delete" [ariaLabel]="clearButtonAriaLabelSignal()"></dbx-button>
20959
+ </div>
20960
+ }
20961
+ </div>
20962
+ }
19488
20963
  </div>
19489
- @if (canAddFilesSignal()) {
20964
+ @if (showUploadAreaSignal()) {
19490
20965
  <dbx-file-upload [accept]="acceptSignal()" [multiple]="multipleSignal()" [mode]="modeSignal()" [hint]="hintSignal()" [text]="textSignal()" [icon]="iconSignal()" (filesChanged)="onFiles($event)"></dbx-file-upload>
19491
20966
  }
19492
20967
  @if (ownedEntriesSignal(); as owned) {
19493
20968
  @if (owned.length > 0) {
19494
- <div class="dbx-pdf-merge-editor-file-upload-entries" cdkDropList (cdkDropListDropped)="onDrop($event)">
19495
- @for (entry of owned; track entry.id) {
19496
- <dbx-pdf-merge-entry [entry]="entry"></dbx-pdf-merge-entry>
19497
- }
19498
- </div>
20969
+ @if (pageEditingSignal()) {
20970
+ <dbx-pdf-merge-page-list class="dbx-pdf-merge-editor-file-upload-pages" [slotId]="slotId()"></dbx-pdf-merge-page-list>
20971
+ } @else {
20972
+ <div class="dbx-pdf-merge-editor-file-upload-entries" cdkDropList (cdkDropListDropped)="onDrop($event)">
20973
+ @for (entry of owned; track entry.id) {
20974
+ <dbx-pdf-merge-entry [entry]="entry"></dbx-pdf-merge-entry>
20975
+ }
20976
+ </div>
20977
+ }
19499
20978
  }
19500
20979
  }
19501
20980
  `,
@@ -19505,7 +20984,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
19505
20984
  '[class.dbx-pdf-merge-editor-file-upload--valid]': 'stateSignal() === "valid"',
19506
20985
  '[class.dbx-pdf-merge-editor-file-upload--no-file]': 'stateSignal() === "no_file"'
19507
20986
  },
19508
- imports: [CdkDropList, DbxFileUploadComponent, DbxPdfMergeEntryComponent],
20987
+ imports: [CdkDropList, DbxButtonComponent, DbxFileUploadComponent, DbxFileUploadButtonComponent, DbxActionDirective, DbxActionButtonDirective, DbxActionHandlerDirective, DbxActionConfirmDirective, DbxActionSnackbarErrorDirective, DbxPdfMergeEntryComponent, DbxPdfMergePageListComponent],
19509
20988
  changeDetection: ChangeDetectionStrategy.OnPush,
19510
20989
  standalone: true
19511
20990
  }]
@@ -19544,6 +21023,252 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
19544
21023
  }]
19545
21024
  }], propDecorators: { targetState: [{ type: i0.Input, args: [{ isSignal: true, alias: "dbxPdfMergeEditorFileUploadHasState", required: false }] }] } });
19546
21025
 
21026
+ /**
21027
+ * Messages shown for each reason an import can fail.
21028
+ */
21029
+ const DBX_PDF_MERGE_IMPORT_ERROR_MESSAGES = {
21030
+ unreadable: 'This file could not be read as a PDF. It may be corrupt or password protected.',
21031
+ no_sidecar: 'This PDF has no embedded manifest, so its pages cannot be matched back to sections. Choose a PDF that was exported from this editor.',
21032
+ no_documents: 'This PDF has a manifest, but none of its recorded pages could be found in the file.'
21033
+ };
21034
+ /**
21035
+ * Loads a PDF that was previously exported from the editor with an embedded manifest, and repopulates the editor's slots from it.
21036
+ *
21037
+ * This is the payoff of the sidecar: a completed single-file document goes back to being the set of per-slot documents it was assembled from, so a user can replace or edit one section in place instead of rebuilding the whole file. Every entry currently in the store is replaced.
21038
+ *
21039
+ * **Zero configuration.** The sections this editor accepts are read from the `<dbx-pdf-merge-editor-file-upload>` slots mounted against the shared {@link DbxPdfMergeEditorStore} (see {@link DbxPdfMergeEditorStore.registeredSlotIds$}), so the common case needs no bindings at all. A file naming any other section is rejected rather than partially imported — without that check, entries carrying an unrecognized slot id would import and then render nowhere.
21040
+ *
21041
+ * **Renders nothing when no sections are configured.** This component exists to split a document back into slots, so with no slots mounted (and no explicit {@link expectedSlotIds}) it has nothing to import into and collapses to zero size. Bind {@link enforceExpectedSlots} to `false` for the unusual case of an editor that wants the import affordance without declaring slots.
21042
+ *
21043
+ * @example
21044
+ * ```html
21045
+ * <!-- derives its sections from the slots below it -->
21046
+ * <dbx-pdf-merge-import (imported)="onImported($event)"></dbx-pdf-merge-import>
21047
+ * <dbx-pdf-merge-editor>
21048
+ * <dbx-pdf-merge-editor-file-upload slotId="license"></dbx-pdf-merge-editor-file-upload>
21049
+ * <dbx-pdf-merge-editor-file-upload slotId="cert"></dbx-pdf-merge-editor-file-upload>
21050
+ * </dbx-pdf-merge-editor>
21051
+ * ```
21052
+ */
21053
+ class DbxPdfMergeImportComponent {
21054
+ store = inject(DbxPdfMergeEditorStore);
21055
+ config = input(...(ngDevMode ? [undefined, { debugName: "config" }] : /* istanbul ignore next */ []));
21056
+ /**
21057
+ * Optional override for the slot ids this editor renders. Leave unbound to derive them from the slots mounted against the store — the zero-configuration default. When set, a file whose manifest names any other slot is rejected rather than partially imported.
21058
+ *
21059
+ * An explicitly bound empty array reads as "this editor deliberately has no sections" and hides the component, the same as having no slots mounted.
21060
+ */
21061
+ expectedSlotIds = input(...(ngDevMode ? [undefined, { debugName: "expectedSlotIds" }] : /* istanbul ignore next */ []));
21062
+ /**
21063
+ * Whether the imported file's sections must match this editor's. Defaults to `true`.
21064
+ *
21065
+ * Binding `false` is the single escape hatch for an editor with no slots that still wants the import affordance: it both skips the check and keeps the component rendered.
21066
+ */
21067
+ enforceExpectedSlots = input(true, ...(ngDevMode ? [{ debugName: "enforceExpectedSlots" }] : /* istanbul ignore next */ []));
21068
+ imported = output();
21069
+ importFailed = output();
21070
+ /**
21071
+ * Emits the expected sections the imported file did not fill, when there are any. The import itself still succeeds — a missing section is a partial state the user can fill in.
21072
+ */
21073
+ missingSlots = output();
21074
+ _error = signal(null, ...(ngDevMode ? [{ debugName: "_error" }] : /* istanbul ignore next */ []));
21075
+ _result = signal(null, ...(ngDevMode ? [{ debugName: "_result" }] : /* istanbul ignore next */ []));
21076
+ _missingSlotIds = signal([], ...(ngDevMode ? [{ debugName: "_missingSlotIds" }] : /* istanbul ignore next */ []));
21077
+ errorSignal = this._error.asReadonly();
21078
+ resultSignal = this._result.asReadonly();
21079
+ missingSlotIdsSignal = this._missingSlotIds.asReadonly();
21080
+ _registeredSlotIdsSignal = toSignal(this.store.registeredSlotIds$, { initialValue: [] });
21081
+ /**
21082
+ * The exact import state the notices below currently describe — the one this component's own picked file produced. Identity, not contents: it is the only reliable way to tell "still my file" from "the store moved on".
21083
+ */
21084
+ _displayedState;
21085
+ constructor() {
21086
+ // The notices are set from what `onFiles` gets back rather than from `importState$`, so that
21087
+ // they only ever describe a file picked here. They must not outlive it, though: once the store
21088
+ // is showing anything other than this component's own import — cleared outright, restored to a
21089
+ // programmatic baseline, or replaced by an import from elsewhere — the notice is describing
21090
+ // content the editor no longer holds, and goes.
21091
+ this.store.importState$.pipe(takeUntilDestroyed()).subscribe((state) => {
21092
+ if (state !== this._displayedState) {
21093
+ this.resetDisplay();
21094
+ }
21095
+ });
21096
+ }
21097
+ /**
21098
+ * The sections an imported file is allowed to name, or `null` when no check applies. Resolves the explicit {@link expectedSlotIds} binding over the slots registered with the store.
21099
+ */
21100
+ effectiveExpectedSlotIdsSignal = computed(() => {
21101
+ const registered = this._registeredSlotIdsSignal();
21102
+ const bound = this.expectedSlotIds();
21103
+ let expected;
21104
+ if (!this.enforceExpectedSlots()) {
21105
+ expected = null;
21106
+ }
21107
+ else if (bound == null) {
21108
+ // An empty registry is "nothing declared yet", not "nothing allowed" — the component hides
21109
+ // rather than rejecting, so a null here never becomes a check that fails every file.
21110
+ expected = registered.length > 0 ? registered : null;
21111
+ }
21112
+ else {
21113
+ expected = bound;
21114
+ }
21115
+ return expected;
21116
+ }, ...(ngDevMode ? [{ debugName: "effectiveExpectedSlotIdsSignal" }] : /* istanbul ignore next */ []));
21117
+ /**
21118
+ * Whether this component has anything to import into. `false` collapses it to nothing — see the class docs.
21119
+ */
21120
+ activeSignal = computed(() => {
21121
+ const expected = this.effectiveExpectedSlotIdsSignal();
21122
+ return !this.enforceExpectedSlots() || (expected != null && expected.length > 0);
21123
+ }, ...(ngDevMode ? [{ debugName: "activeSignal" }] : /* istanbul ignore next */ []));
21124
+ acceptSignal = computed(() => this.config()?.accept ?? [PDF_MIME_TYPE], ...(ngDevMode ? [{ debugName: "acceptSignal" }] : /* istanbul ignore next */ []));
21125
+ modeSignal = computed(() => this.config()?.mode ?? 'default', ...(ngDevMode ? [{ debugName: "modeSignal" }] : /* istanbul ignore next */ []));
21126
+ labelSignal = computed(() => this.config()?.label, ...(ngDevMode ? [{ debugName: "labelSignal" }] : /* istanbul ignore next */ []));
21127
+ hintSignal = computed(() => this.config()?.hint ?? 'Choose a PDF previously exported from this editor', ...(ngDevMode ? [{ debugName: "hintSignal" }] : /* istanbul ignore next */ []));
21128
+ textSignal = computed(() => this.config()?.text ?? 'Import merged PDF', ...(ngDevMode ? [{ debugName: "textSignal" }] : /* istanbul ignore next */ []));
21129
+ iconSignal = computed(() => this.config()?.icon ?? 'upload_file', ...(ngDevMode ? [{ debugName: "iconSignal" }] : /* istanbul ignore next */ []));
21130
+ successLabelSignal = computed(() => {
21131
+ const result = this._result();
21132
+ let label;
21133
+ if (result == null) {
21134
+ label = null;
21135
+ }
21136
+ else {
21137
+ const sections = result.slotIds.map((slotId) => slotId ?? 'unsectioned').join(', ');
21138
+ label = `Imported ${result.entries.length} section(s): ${sections}.`;
21139
+ }
21140
+ return label;
21141
+ }, ...(ngDevMode ? [{ debugName: "successLabelSignal" }] : /* istanbul ignore next */ []));
21142
+ /**
21143
+ * Warning shown when the imported file left one or more expected sections empty. The import succeeded — this only explains why a slot is still blank.
21144
+ */
21145
+ missingLabelSignal = computed(() => {
21146
+ const missing = this._missingSlotIds();
21147
+ return missing.length > 0 ? `This PDF did not include section(s): ${missing.join(', ')}.` : null;
21148
+ }, ...(ngDevMode ? [{ debugName: "missingLabelSignal" }] : /* istanbul ignore next */ []));
21149
+ async onFiles(event) {
21150
+ const file = event.matchResult.accepted[0];
21151
+ if (file != null) {
21152
+ this.resetDisplay();
21153
+ // Delegates to the store so the picker and a programmatic import share one implementation
21154
+ // and one set of error semantics. The slot check is opt-in on the store, so the component's
21155
+ // own resolved expectation is what preserves the picker's behavior. The returned state is
21156
+ // used rather than `importState$` so these outputs only ever describe a picked file.
21157
+ // `origin: 'user'` marks this as discardable — it must not become the store's clear-time
21158
+ // restore point, which belongs to whatever document the app itself supplied.
21159
+ const state = await this.store.importMergedPdf({ source: file, expectedSlotIds: this.effectiveExpectedSlotIdsSignal(), origin: 'user' });
21160
+ // Claim this state before rendering it: the subscription above resets on anything it does
21161
+ // not recognise, and the 'importing'/terminal emissions have already gone past by now.
21162
+ this._displayedState = state;
21163
+ if (state.status === 'failed') {
21164
+ this.fail(this.messageForFailure(state));
21165
+ }
21166
+ else if (state.result != null) {
21167
+ const { result } = state;
21168
+ this._result.set(result);
21169
+ this._missingSlotIds.set(result.missingSlotIds);
21170
+ this.imported.emit(result);
21171
+ if (result.missingSlotIds.length > 0) {
21172
+ this.missingSlots.emit(result.missingSlotIds);
21173
+ }
21174
+ }
21175
+ }
21176
+ }
21177
+ messageForFailure(state) {
21178
+ const unexpected = state.unexpectedSlotIds ?? [];
21179
+ let message;
21180
+ if (state.error === 'unexpected_slots') {
21181
+ message = `This PDF contains section(s) this editor does not have: ${unexpected.map((slotId) => slotId ?? 'unsectioned').join(', ')}.`;
21182
+ }
21183
+ else {
21184
+ message = DBX_PDF_MERGE_IMPORT_ERROR_MESSAGES[state.error ?? 'unreadable'];
21185
+ }
21186
+ return message;
21187
+ }
21188
+ fail(message) {
21189
+ this._error.set(message);
21190
+ this.importFailed.emit(message);
21191
+ }
21192
+ /**
21193
+ * Drops every notice this component renders, returning it to its pre-import appearance and releasing its claim on the store's import state.
21194
+ */
21195
+ resetDisplay() {
21196
+ this._displayedState = null;
21197
+ this._error.set(null);
21198
+ this._result.set(null);
21199
+ this._missingSlotIds.set([]);
21200
+ }
21201
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: DbxPdfMergeImportComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
21202
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: DbxPdfMergeImportComponent, isStandalone: true, selector: "dbx-pdf-merge-import", inputs: { config: { classPropertyName: "config", publicName: "config", isSignal: true, isRequired: false, transformFunction: null }, expectedSlotIds: { classPropertyName: "expectedSlotIds", publicName: "expectedSlotIds", isSignal: true, isRequired: false, transformFunction: null }, enforceExpectedSlots: { classPropertyName: "enforceExpectedSlots", publicName: "enforceExpectedSlots", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { imported: "imported", importFailed: "importFailed", missingSlots: "missingSlots" }, host: { properties: { "class.d-block": "activeSignal()", "class.dbx-mb3": "activeSignal()" }, classAttribute: "dbx-pdf-merge-import" }, ngImport: i0, template: `
21203
+ @if (activeSignal()) {
21204
+ @if (labelSignal(); as label) {
21205
+ <div class="dbx-pdf-merge-import-label">{{ label }}</div>
21206
+ }
21207
+ <dbx-file-upload [accept]="acceptSignal()" [multiple]="false" [mode]="modeSignal()" [hint]="hintSignal()" [text]="textSignal()" [icon]="iconSignal()" (filesChanged)="onFiles($event)"></dbx-file-upload>
21208
+ @if (errorSignal(); as error) {
21209
+ <div class="dbx-pdf-merge-import-error">
21210
+ <mat-icon class="dbx-pdf-merge-import-icon">error</mat-icon>
21211
+ <span>{{ error }}</span>
21212
+ </div>
21213
+ }
21214
+ @if (successLabelSignal(); as success) {
21215
+ <div class="dbx-pdf-merge-import-success">
21216
+ <mat-icon class="dbx-pdf-merge-import-icon">check_circle</mat-icon>
21217
+ <span>{{ success }}</span>
21218
+ </div>
21219
+ }
21220
+ @if (missingLabelSignal(); as missing) {
21221
+ <div class="dbx-pdf-merge-import-warning">
21222
+ <mat-icon class="dbx-pdf-merge-import-icon">warning</mat-icon>
21223
+ <span>{{ missing }}</span>
21224
+ </div>
21225
+ }
21226
+ }
21227
+ `, isInline: true, dependencies: [{ kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i1$1.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "component", type: DbxFileUploadComponent, selector: "dbx-file-upload", inputs: ["config", "buttonStyle", "buttonDisplay", "mode", "text", "icon", "hint", "clickAreaToUpload"], outputs: ["filesChanged"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
21228
+ }
21229
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: DbxPdfMergeImportComponent, decorators: [{
21230
+ type: Component,
21231
+ args: [{
21232
+ selector: 'dbx-pdf-merge-import',
21233
+ template: `
21234
+ @if (activeSignal()) {
21235
+ @if (labelSignal(); as label) {
21236
+ <div class="dbx-pdf-merge-import-label">{{ label }}</div>
21237
+ }
21238
+ <dbx-file-upload [accept]="acceptSignal()" [multiple]="false" [mode]="modeSignal()" [hint]="hintSignal()" [text]="textSignal()" [icon]="iconSignal()" (filesChanged)="onFiles($event)"></dbx-file-upload>
21239
+ @if (errorSignal(); as error) {
21240
+ <div class="dbx-pdf-merge-import-error">
21241
+ <mat-icon class="dbx-pdf-merge-import-icon">error</mat-icon>
21242
+ <span>{{ error }}</span>
21243
+ </div>
21244
+ }
21245
+ @if (successLabelSignal(); as success) {
21246
+ <div class="dbx-pdf-merge-import-success">
21247
+ <mat-icon class="dbx-pdf-merge-import-icon">check_circle</mat-icon>
21248
+ <span>{{ success }}</span>
21249
+ </div>
21250
+ }
21251
+ @if (missingLabelSignal(); as missing) {
21252
+ <div class="dbx-pdf-merge-import-warning">
21253
+ <mat-icon class="dbx-pdf-merge-import-icon">warning</mat-icon>
21254
+ <span>{{ missing }}</span>
21255
+ </div>
21256
+ }
21257
+ }
21258
+ `,
21259
+ host: {
21260
+ class: 'dbx-pdf-merge-import',
21261
+ // Collapsed rather than merely emptied while inactive: without `d-block` the host is an empty
21262
+ // inline element of zero size, and dropping the margin keeps it from occupying layout.
21263
+ '[class.d-block]': 'activeSignal()',
21264
+ '[class.dbx-mb3]': 'activeSignal()'
21265
+ },
21266
+ imports: [MatIconModule, DbxFileUploadComponent],
21267
+ changeDetection: ChangeDetectionStrategy.OnPush,
21268
+ standalone: true
21269
+ }]
21270
+ }], ctorParameters: () => [], propDecorators: { config: [{ type: i0.Input, args: [{ isSignal: true, alias: "config", required: false }] }], expectedSlotIds: [{ type: i0.Input, args: [{ isSignal: true, alias: "expectedSlotIds", required: false }] }], enforceExpectedSlots: [{ type: i0.Input, args: [{ isSignal: true, alias: "enforceExpectedSlots", required: false }] }], imported: [{ type: i0.Output, args: ["imported"] }], importFailed: [{ type: i0.Output, args: ["importFailed"] }], missingSlots: [{ type: i0.Output, args: ["missingSlots"] }] } });
21271
+
19547
21272
  const DEFAULT_UPLOAD_FOOTER_ICON = 'cloud_upload';
19548
21273
  const DEFAULT_UPLOAD_FOOTER_TEXT = 'Upload';
19549
21274
  const DEFAULT_UPLOAD_FOOTER_COLOR = 'primary';
@@ -19605,7 +21330,7 @@ class DbxPdfMergeUploadDialogComponent extends AbstractDialogDirective {
19605
21330
  </div>
19606
21331
  }
19607
21332
  </dbx-dialog-content>
19608
- `, isInline: true, dependencies: [{ kind: "directive", type: DbxDialogContentDirective, selector: "dbx-dialog-content,[dbxDialogContent],.dbx-dialog-content", inputs: ["width"] }, { kind: "component", type: DbxDialogContentCloseComponent, selector: "dbx-dialog-content-close", inputs: ["padded"], outputs: ["close"] }, { kind: "component", type: DbxInjectionComponent, selector: "dbx-injection, [dbxInjection], [dbx-injection]", inputs: ["config", "template"] }, { kind: "component", type: DbxPdfMergeEditorComponent, selector: "dbx-pdf-merge-editor", inputs: ["accept", "multiple", "fileName", "showDownloadButton", "showPreviewButton", "downloadButton", "showAddFiles", "showFileList", "config"], outputs: ["entriesChanged"] }, { kind: "component", type: DbxButtonComponent, selector: "dbx-button", inputs: ["bar", "type", "buttonStyle", "color", "spinnerColor", "customButtonColor", "customTextColor", "customSpinnerColor", "basic", "tonal", "raised", "stroked", "flat", "iconOnly", "fab", "customContent", "allowClickPropagation", "mode"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
21333
+ `, isInline: true, dependencies: [{ kind: "directive", type: DbxDialogContentDirective, selector: "dbx-dialog-content,[dbxDialogContent],.dbx-dialog-content", inputs: ["width"] }, { kind: "component", type: DbxDialogContentCloseComponent, selector: "dbx-dialog-content-close", inputs: ["padded"], outputs: ["close"] }, { kind: "component", type: DbxInjectionComponent, selector: "dbx-injection, [dbxInjection], [dbx-injection]", inputs: ["config", "template"] }, { kind: "component", type: DbxPdfMergeEditorComponent, selector: "dbx-pdf-merge-editor", inputs: ["accept", "multiple", "fileName", "showDownloadButton", "showPreviewButton", "downloadButton", "showAddFiles", "showFileList", "pageEditing", "sidecar", "config"], outputs: ["entriesChanged"] }, { kind: "component", type: DbxButtonComponent, selector: "dbx-button", inputs: ["bar", "type", "buttonStyle", "color", "spinnerColor", "customButtonColor", "customTextColor", "customSpinnerColor", "basic", "tonal", "raised", "stroked", "flat", "iconOnly", "fab", "customContent", "allowClickPropagation", "mode"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
19609
21334
  }
19610
21335
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: DbxPdfMergeUploadDialogComponent, decorators: [{
19611
21336
  type: Component,
@@ -19808,5 +21533,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImpo
19808
21533
  * Generated bundle index. Do not edit.
19809
21534
  */
19810
21535
 
19811
- export { APP_POPUP_MINIMIZED_WIDTH, APP_POPUP_NORMAL_HEIGHT, APP_POPUP_NORMAL_WIDTH, AbstractDbxClipboardDirective, AbstractDbxErrorWidgetComponent, AbstractDbxFileUploadComponent, AbstractDbxHelpWidgetDirective, AbstractDbxListAccordionViewDirective, AbstractDbxListGridViewDirective, AbstractDbxListViewDirective, AbstractDbxListWrapperDirective, AbstractDbxPartialPresetFilterMenuDirective, AbstractDbxSegueAnchorDirective, AbstractDbxSelectionListViewDirective, AbstractDbxSelectionListWrapperDirective, AbstractDbxValueListItemModifierDirective, AbstractDbxValueListViewDirective, AbstractDbxValueListViewItemComponent, AbstractDbxWidgetComponent, AbstractDialogDirective, AbstractFilterPopoverButtonDirective, AbstractPopoverDirective, AbstractPopoverRefDirective, AbstractPopoverRefWithEventsDirective, AbstractPopupDirective, AbstractPromptConfirmDirective, CompactContextStore, CompactMode, DBX_ACTION_SNACKBAR_DEFAULTS, DBX_ACTION_SNACKBAR_SERVICE_CONFIG, DBX_AVATAR_CONTEXT_DATA_TOKEN, DBX_COLOR_CUSTOM_BG_CSS_CLASS, DBX_COLOR_CUSTOM_TEXT_CSS_CLASS, DBX_CURATED_COLOR_COUNT, DBX_CURATED_COLOR_TEMPLATES, DBX_CURATED_COLOR_TEMPLATE_KEY_PREFIX, DBX_DARK_STYLE_CLASS_SUFFIX, DBX_HELP_WIDGET_ENTRY_DATA_TOKEN, DBX_LIST_ACCORDION_VIEW_COMPONENT_IMPORTS_AND_EXPORTS, DBX_LIST_GRID_VIEW_COMPONENT_IMPORTS_AND_EXPORTS, DBX_LIST_ITEM_DISABLE_RIPPLE_LIST_ITEM_MODIFIER_KEY, DBX_LIST_ITEM_IS_SELECTED_ITEM_MODIFIER_KEY, DBX_MODEL_VIEW_TRACKER_STORAGE_ACCESSOR_TOKEN, DBX_PDF_MERGE_EDITOR_CONFIG, DBX_PDF_MERGE_EDITOR_INITIAL_STATE, DBX_PDF_MERGE_EDITOR_PRESERVE_ENTRIES_ON_SLOT_DESTROY, DBX_PDF_MERGE_ENCRYPTED_ERROR_MESSAGE, DBX_PROGRESS_BUTTON_GLOBAL_CONFIG, DBX_ROUTER_ANCHOR_COMPONENTS, DBX_ROUTER_VALUE_LIST_ITEM_MODIFIER_KEY, DBX_THEME_COLORS, DBX_THEME_COLORS_EXTRA, DBX_THEME_COLORS_EXTRA_SECONDARY, DBX_THEME_COLORS_MAIN, DBX_VALUE_LIST_VIEW_ITEM, DBX_WEB_FILE_PREVIEW_SERVICE_ENTRIES_TOKEN, DBX_WEB_FILE_PREVIEW_SERVICE_ZIP_COMPONENT_PRESET, DBX_WEB_FILE_PREVIEW_SERVICE_ZIP_PRESET_ENTRY, DBX_WEB_PAGE_TITLE_SERVICE_CONFIG, DEFAULT_DBX_CHIP_TONE, DEFAULT_DBX_DETACH_KEY, DEFAULT_DBX_ERROR_SNACKBAR_CONFIG, DEFAULT_DBX_HELP_VIEW_POPOVER_KEY, DEFAULT_DBX_LINKIFY_STRING_TYPE, DEFAULT_DBX_LIST_ACCORDION_VIEW_COMPONENT_CONFIGURATION_TEMPLATE, DEFAULT_DBX_LIST_GRID_VIEW_COMPONENT_CONFIGURATION_TEMPLATE, DEFAULT_DBX_LIST_ITEM_DISABLE_FUNCTION, DEFAULT_DBX_LIST_ITEM_IS_SELECTED_FUNCTION, DEFAULT_DBX_LIST_SCROLL_DISTANCE, DEFAULT_DBX_LIST_THROTTLE_SCROLL, DEFAULT_DBX_LIST_VIEW_META_ICON, DEFAULT_DBX_PDF_MERGE_ENCRYPTED_HANDLING, DEFAULT_DBX_PROMPT_CONFIRM_DIALOG_CONFIG, DEFAULT_DBX_SELECTION_VALUE_LIST_COMPONENT_CONFIGURATION_TEMPLATE, DEFAULT_DBX_SIDENAV_MENU_ICON, DEFAULT_DBX_STYLE_CONFIG_TOKEN, DEFAULT_DBX_VALUE_LIST_COMPONENT_CONFIGURATION_TEMPLATE, DEFAULT_DBX_WEB_FILE_PREVIEW_SERVICE_DIALOG_WITH_COMPONENT_FUNCTION, DEFAULT_DBX_WEB_FILE_PREVIEW_SERVICE_PREVIEW_COMPONENT_FUNCTION, DEFAULT_ERROR_POPOVER_KEY, DEFAULT_ERROR_WIDGET_CODE, DEFAULT_FILTER_POPOVER_KEY, DEFAULT_IMAGE_BITMAP_TO_BLOB_ENCODER, DEFAULT_IMAGE_JPEG_QUALITY, DEFAULT_LIST_GRID_SIZE_CONFIG, DEFAULT_LIST_WRAPPER_COMPONENT_CONFIGURATION_TEMPLATE, DEFAULT_LOADING_PROGRESS_DIAMETER, DEFAULT_PDF_MERGE_ACCEPT, DEFAULT_SCREEN_MEDIA_SERVICE_CONFIG, DEFAULT_SNACKBAR_DIRECTIVE_DURATION, DEFAULT_TWO_COLUMNS_MIN_RIGHT_WIDTH, DEFAULT_VALUE_LIST_VIEW_CONTENT_COMPONENT_TRACK_BY_FUNCTION, DbxAccordionHeaderHeightDirective, DbxActionConfirmDirective, DbxActionDialogDirective, DbxActionErrorDirective, DbxActionKeyTriggerDirective, DbxActionLoadingContextDirective, DbxActionModule, DbxActionPopoverDirective, DbxActionSnackbarComponent, DbxActionSnackbarDirective, DbxActionSnackbarErrorDirective, DbxActionSnackbarModule, DbxActionSnackbarService, DbxActionTransitionSafetyDirective, DbxActionUIRouterTransitionSafetyDialogComponent, DbxAnchorComponent, DbxAnchorContentComponent, DbxAnchorIconComponent, DbxAnchorListComponent, DbxAngularRouterSegueAnchorComponent, DbxAvatarComponent, DbxAvatarViewComponent, DbxAvatarViewService, DbxAvatarViewServiceConfig, DbxBarDirective, DbxBarHeaderComponent, DbxBarLayoutModule, DbxBasicLoadingComponent, DbxBodyDirective, DbxButtonComponent, DbxButtonModule, DbxButtonSpacerDirective, DbxCardBoxComponent, DbxCardBoxContainerDirective, DbxCardBoxLayoutModule, DbxChipDirective, DbxChipListComponent, DbxClickToCopyTextComponent, DbxClickToCopyTextDirective, DbxColorDirective, DbxColorService, DbxColorServiceConfig, DbxColumnLayoutModule, DbxCompactDirective, DbxCompactLayoutModule, DbxContentBorderDirective, DbxContentBoxDirective, DbxContentContainerDirective, DbxContentDirective, DbxContentElevateDirective, DbxContentLayoutModule, DbxContentPageDirective, DbxContentPitDirective, DbxDetachContentComponent, DbxDetachControlButtonsComponent, DbxDetachController, DbxDetachControlsComponent, DbxDetachInitDirective, DbxDetachInteractionModule, DbxDetachOutletComponent, DbxDetachOverlayComponent, DbxDetachService, DbxDetachWindowState, DbxDetailBlockComponent, DbxDetailBlockHeaderComponent, DbxDialogContentCloseComponent, DbxDialogContentDirective, DbxDialogContentFooterComponent, DbxDialogInteractionModule, DbxDialogModule, DbxDownloadBlobButtonComponent, DbxDownloadTextViewComponent, DbxEmbedComponent, DbxErrorComponent, DbxErrorDefaultErrorWidgetComponent, DbxErrorDetailsComponent, DbxErrorPopoverComponent, DbxErrorSnackbarComponent, DbxErrorSnackbarService, DbxErrorViewComponent, DbxErrorWidgetService, DbxErrorWidgetViewComponent, DbxFileUploadActionCompatable, DbxFileUploadActionSyncDirective, DbxFileUploadAreaComponent, DbxFileUploadButtonComponent, DbxFileUploadComponent, DbxFilterInteractionModule, DbxFilterPopoverButtonComponent, DbxFilterPopoverComponent, DbxFilterWrapperComponent, DbxFlagComponent, DbxFlagLayoutModule, DbxFlagPromptComponent, DbxFlexGroupDirective, DbxFlexLayoutModule, DbxFlexSizeDirective, DbxHelpContextDirective, DbxHelpContextService, DbxHelpViewListComponent, DbxHelpViewListEntryComponent, DbxHelpViewPopoverButtonComponent, DbxHelpViewPopoverComponent, DbxHelpWidgetService, DbxHelpWidgetServiceConfig, DbxIconButtonComponent, DbxIconButtonModule, DbxIconItemComponent, DbxIconSpacerDirective, DbxIconTileComponent, DbxIconTileDirective, DbxIfSidenavDisplayModeDirective, DbxIframeComponent, DbxInjectionDialogComponent, DbxInteractionModule, DbxIntroActionSectionComponent, DbxLabelBlockComponent, DbxLayoutModule, DbxLinkComponent, DbxLinkifyComponent, DbxLinkifyService, DbxLinkifyServiceConfig, DbxListAccordionViewComponentImportsModule, DbxListComponent, DbxListEmptyContentComponent, DbxListGridViewComponentImportsModule, DbxListInternalContentDirective, DbxListItemAnchorModifierDirective, DbxListItemDisableRippleModifierDirective, DbxListItemIsSelectedModifierDirective, DbxListModifierModule, DbxListModule, DbxListTitleGroupDirective, DbxListView, DbxListViewMetaIconComponent, DbxListViewWrapper, DbxListWrapperComponentImportsModule, DbxLoadingComponent, DbxLoadingErrorDirective, DbxLoadingModule, DbxLoadingProgressComponent, DbxModelObjectStateService, actions as DbxModelStateActions, model_actions as DbxModelStateModelActions, DbxModelTrackerService, DbxModelTypesService, DbxModelViewTrackerStorage, DbxNavbarComponent, DbxNumberWithLimitComponent, DbxOneColumnComponent, DbxOneColumnLayoutModule, DbxPagebarComponent, DbxPartialPresetFilterListComponent, DbxPartialPresetFilterMenuComponent, DbxPdfMergeEditorComponent, DbxPdfMergeEditorFileUploadComponent, DbxPdfMergeEditorFileUploadHasStateDirective, DbxPdfMergeEditorFileUploadValidatorDirective, DbxPdfMergeEditorStore, DbxPdfMergeEditorStoreDirective, DbxPdfMergeEntryComponent, DbxPdfMergeListComponent, DbxPdfMergeUploadActionDirective, DbxPdfMergeUploadButtonDirective, DbxPdfMergeUploadDialogComponent, DbxPdfPreviewComponent, DbxPopoverCloseButtonComponent, DbxPopoverComponent, DbxPopoverComponentController, DbxPopoverContentComponent, DbxPopoverController, DbxPopoverControlsDirective, DbxPopoverCoordinatorComponent, DbxPopoverCoordinatorService, DbxPopoverHeaderComponent, DbxPopoverInteractionContentModule, DbxPopoverInteractionModule, DbxPopoverScrollContentDirective, DbxPopoverService, DbxPopupComponent, DbxPopupComponentController, DbxPopupContentComponent, DbxPopupControlButtonsComponent, DbxPopupController, DbxPopupControlsComponent, DbxPopupCoordinatorComponent, DbxPopupCoordinatorService, DbxPopupInteractionModule, DbxPopupService, DbxPopupWindowState, DbxPresetFilterListComponent, DbxPresetFilterMenuComponent, DbxProgressBarButtonComponent, DbxProgressButtonsModule, DbxProgressSpinnerButtonComponent, DbxPromptBoxDirective, DbxPromptComponent, DbxPromptConfirm, DbxPromptConfirmButtonDirective, DbxPromptConfirmComponent, DbxPromptConfirmDialogComponent, DbxPromptConfirmDirective, DbxPromptModule, DbxPromptPageComponent, DbxReadableErrorModule, DbxResizedDirective, DbxRouterAnchorModule, DbxRouterLayoutModule, DbxRouterSidenavModule, DbxRouterWebProviderConfig, DbxScreenMediaService, DbxScreenMediaServiceConfig, DbxSectionComponent, DbxSectionHeaderComponent, DbxSectionLayoutModule, DbxSectionPageComponent, DbxSelectionValueListViewComponent, DbxSelectionValueListViewComponentImportsModule, DbxSelectionValueListViewContentComponent, DbxSetStyleDirective, DbxSidenavButtonComponent, DbxSidenavComponent, DbxSidenavPageComponent, DbxSidenavPagebarComponent, DbxSpacerDirective, DbxStepBlockComponent, DbxStructureDirective, DbxStructureModule, DbxStyleBodyDirective, DbxStyleDirective, DbxStyleLayoutModule, DbxStyleService, DbxSubSectionComponent, DbxTextChipsComponent, DbxTextColorDirective, DbxTextModule, DbxTwoBlockComponent, DbxTwoColumnBackDirective, DbxTwoColumnColumnHeadDirective, DbxTwoColumnComponent, DbxTwoColumnContextDirective, DbxTwoColumnFullLeftDirective, DbxTwoColumnLayoutModule, DbxTwoColumnRightComponent, DbxTwoColumnSrefDirective, DbxTwoColumnSrefShowRightDirective, DbxUIRouterSegueAnchorComponent, DbxUnitedStatesAddressComponent, DbxValueListAccordionViewComponent, DbxValueListAccordionViewContentComponent, DbxValueListAccordionViewContentGroupComponent, DbxValueListGridSizeDirective, DbxValueListGridViewComponent, DbxValueListGridViewContentComponent, DbxValueListGridViewContentGroupComponent, DbxValueListItemModifier, DbxValueListItemModifierDirective, DbxValueListView, DbxValueListViewComponent, DbxValueListViewComponentImportsModule, DbxValueListViewContentComponent, DbxValueListViewContentGroupComponent, DbxValueListViewGroupDelegate, DbxWebFilePreviewComponent, DbxWebFilePreviewService, DbxWebModule, DbxWebPageTitleInfoDirective, DbxWebPageTitleService, DbxWidgetListGridComponent, DbxWidgetListGridViewComponent, DbxWidgetListGridViewItemComponent, DbxWidgetService, DbxWidgetViewComponent, DbxWindowKeyDownListenerDirective, DbxZipBlobPreviewComponent, DbxZipPreviewComponent, PDF_MERGE_RESULT_MIME_TYPE, PopoverPositionStrategy, PopupGlobalPositionStrategy, SCREEN_MEDIA_WIDTH_TYPE_SIZE_MAP, SIDE_NAV_DISPLAY_MODE_ORDER, SideNavDisplayMode, TRACK_BY_MODEL_ID, TRACK_BY_MODEL_KEY, TwoColumnsContextStore, UNKNOWN_ERROR_WIDGET_CODE, addConfigToValueListItems, allDbxModelViewTrackerEventModelKeys, allDbxModelViewTrackerEventSetModelKeys, buildPdfMergeEntry, buildPdfMergeEntrySync, catchErrorServerParams, classifyPdfMergeFile, compactModeFromInput, compareScreenMediaWidthTypes, compressImageFile, convertServerErrorParams, convertToPOJOServerErrorResponse, convertToServerErrorResponse, copyToClipboardFunction, dbxColorBackground, dbxCuratedColorConfigForString, dbxListAccordionViewComponentImportsAndExports, dbxListGridViewComponentImportsAndExports, dbxPresetFilterMenuButtonIconObservable, dbxPresetFilterMenuButtonTextObservable, dbxStyleClassCleanSuffix, dbxThemeColorCssToken, dbxThemeColorCssTokenVar, dbxThemeColorCssVariable, dbxThemeColorCssVariableVar, dbxValueListItemDecisionFunction, dbxValueListItemKeyForItemValue, dbxWebDefaultPageTitleDelegate, dbxZipBlobPreviewEntryTreeFromEntries, defaultDbxModelViewTrackerStorageAccessorFactory, defaultDbxValueListViewGroupDelegate, defaultDbxValueListViewGroupValuesFunction, disableRightClickInCdkBackdrop, fileAcceptFilterTypeStringArray, fileAcceptFunction, fileAcceptString, fileArrayAcceptMatchFunction, flattenAccordionGroups, formatPdfMergeEntrySize, index as fromDbxModel, injectCopyToClipboardFunction, injectCopyToClipboardFunctionWithSnackbarMessage, isDbxColorConfig, listItemModifier, makeDbxActionSnackbarDisplayConfigGeneratorFunction, mapCompactModeObs, mapValuesToValuesListItemConfigObs, mergePdfMergeEntries, index$1 as onDbxModel, openEmbedDialog, openIframeDialog, openPdfPreviewDialog, openZipPreviewDialog, overrideClickElementEffect, provideDbxDetachController, provideDbxFileUploadActionCompatable, provideDbxHelpServices, provideDbxLinkify, provideDbxListView, provideDbxListViewWrapper, provideDbxModelService, provideDbxPdfMergeEditorConfig, provideDbxPdfMergeEditorPreserveEntriesOnSlotDestroy, provideDbxProgressButtonGlobalConfig, provideDbxPromptConfirm, provideDbxRouterWebAngularRouterProviderConfig, provideDbxRouterWebUiRouterProviderConfig, provideDbxScreenMediaService, provideDbxStyleService, provideDbxValueListView, provideDbxValueListViewGroupDelegate, provideDbxValueListViewModifier, provideDbxWebFilePreviewServiceEntries, provideDbxWebPageTitleService, provideTwoColumnsContext, registerHelpContextKeysWithDbxHelpContextService, resizeSignal, resolveSideNavDisplayMode, sanitizeDbxDialogContentConfig, screenMediaWidthTypeIsActive, trackByModelKeyRef, trackByUniqueIdentifier, validatePdfMergeEntry };
21536
+ export { APP_POPUP_MINIMIZED_WIDTH, APP_POPUP_NORMAL_HEIGHT, APP_POPUP_NORMAL_WIDTH, AbstractDbxClipboardDirective, AbstractDbxErrorWidgetComponent, AbstractDbxFileUploadComponent, AbstractDbxHelpWidgetDirective, AbstractDbxListAccordionViewDirective, AbstractDbxListGridViewDirective, AbstractDbxListViewDirective, AbstractDbxListWrapperDirective, AbstractDbxPartialPresetFilterMenuDirective, AbstractDbxSegueAnchorDirective, AbstractDbxSelectionListViewDirective, AbstractDbxSelectionListWrapperDirective, AbstractDbxValueListItemModifierDirective, AbstractDbxValueListViewDirective, AbstractDbxValueListViewItemComponent, AbstractDbxWidgetComponent, AbstractDialogDirective, AbstractFilterPopoverButtonDirective, AbstractPopoverDirective, AbstractPopoverRefDirective, AbstractPopoverRefWithEventsDirective, AbstractPopupDirective, AbstractPromptConfirmDirective, CompactContextStore, CompactMode, DBX_ACTION_SNACKBAR_DEFAULTS, DBX_ACTION_SNACKBAR_SERVICE_CONFIG, DBX_AVATAR_CONTEXT_DATA_TOKEN, DBX_COLOR_CUSTOM_BG_CSS_CLASS, DBX_COLOR_CUSTOM_TEXT_CSS_CLASS, DBX_CURATED_COLOR_COUNT, DBX_CURATED_COLOR_TEMPLATES, DBX_CURATED_COLOR_TEMPLATE_KEY_PREFIX, DBX_DARK_STYLE_CLASS_SUFFIX, DBX_HELP_WIDGET_ENTRY_DATA_TOKEN, DBX_LIST_ACCORDION_VIEW_COMPONENT_IMPORTS_AND_EXPORTS, DBX_LIST_GRID_VIEW_COMPONENT_IMPORTS_AND_EXPORTS, DBX_LIST_ITEM_DISABLE_RIPPLE_LIST_ITEM_MODIFIER_KEY, DBX_LIST_ITEM_IS_SELECTED_ITEM_MODIFIER_KEY, DBX_MODEL_VIEW_TRACKER_STORAGE_ACCESSOR_TOKEN, DBX_PDF_MERGE_EDITOR_CONFIG, DBX_PDF_MERGE_EDITOR_INITIAL_STATE, DBX_PDF_MERGE_EDITOR_PRESERVE_ENTRIES_ON_SLOT_DESTROY, DBX_PDF_MERGE_ENCRYPTED_ERROR_MESSAGE, DBX_PDF_MERGE_ENCRYPTED_NOT_EDITABLE_MESSAGE, DBX_PDF_MERGE_IMPORT_ERROR_MESSAGES, DBX_PROGRESS_BUTTON_GLOBAL_CONFIG, DBX_ROUTER_ANCHOR_COMPONENTS, DBX_ROUTER_VALUE_LIST_ITEM_MODIFIER_KEY, DBX_THEME_COLORS, DBX_THEME_COLORS_EXTRA, DBX_THEME_COLORS_EXTRA_SECONDARY, DBX_THEME_COLORS_MAIN, DBX_VALUE_LIST_VIEW_ITEM, DBX_WEB_FILE_PREVIEW_SERVICE_ENTRIES_TOKEN, DBX_WEB_FILE_PREVIEW_SERVICE_ZIP_COMPONENT_PRESET, DBX_WEB_FILE_PREVIEW_SERVICE_ZIP_PRESET_ENTRY, DBX_WEB_PAGE_TITLE_SERVICE_CONFIG, DEFAULT_DBX_CHIP_TONE, DEFAULT_DBX_DETACH_KEY, DEFAULT_DBX_ERROR_SNACKBAR_CONFIG, DEFAULT_DBX_HELP_VIEW_POPOVER_KEY, DEFAULT_DBX_LINKIFY_STRING_TYPE, DEFAULT_DBX_LIST_ACCORDION_VIEW_COMPONENT_CONFIGURATION_TEMPLATE, DEFAULT_DBX_LIST_GRID_VIEW_COMPONENT_CONFIGURATION_TEMPLATE, DEFAULT_DBX_LIST_ITEM_DISABLE_FUNCTION, DEFAULT_DBX_LIST_ITEM_IS_SELECTED_FUNCTION, DEFAULT_DBX_LIST_SCROLL_DISTANCE, DEFAULT_DBX_LIST_THROTTLE_SCROLL, DEFAULT_DBX_LIST_VIEW_META_ICON, DEFAULT_DBX_PDF_MERGE_ENCRYPTED_HANDLING, DEFAULT_DBX_PDF_MERGE_PAGE_EDITING, DEFAULT_DBX_PDF_MERGE_RESTORE_IMPORT_ON_CLEAR, DEFAULT_DBX_PDF_MERGE_SIDECAR, DEFAULT_DBX_PROMPT_CONFIRM_DIALOG_CONFIG, DEFAULT_DBX_SELECTION_VALUE_LIST_COMPONENT_CONFIGURATION_TEMPLATE, DEFAULT_DBX_SIDENAV_MENU_ICON, DEFAULT_DBX_STYLE_CONFIG_TOKEN, DEFAULT_DBX_VALUE_LIST_COMPONENT_CONFIGURATION_TEMPLATE, DEFAULT_DBX_WEB_FILE_PREVIEW_SERVICE_DIALOG_WITH_COMPONENT_FUNCTION, DEFAULT_DBX_WEB_FILE_PREVIEW_SERVICE_PREVIEW_COMPONENT_FUNCTION, DEFAULT_ERROR_POPOVER_KEY, DEFAULT_ERROR_WIDGET_CODE, DEFAULT_FILTER_POPOVER_KEY, DEFAULT_IMAGE_BITMAP_TO_BLOB_ENCODER, DEFAULT_IMAGE_JPEG_QUALITY, DEFAULT_LIST_GRID_SIZE_CONFIG, DEFAULT_LIST_WRAPPER_COMPONENT_CONFIGURATION_TEMPLATE, DEFAULT_LOADING_PROGRESS_DIAMETER, DEFAULT_PDF_MERGE_ACCEPT, DEFAULT_PDF_MERGE_PAGE_GROUP_KEY, DEFAULT_SCREEN_MEDIA_SERVICE_CONFIG, DEFAULT_SNACKBAR_DIRECTIVE_DURATION, DEFAULT_TWO_COLUMNS_MIN_RIGHT_WIDTH, DEFAULT_VALUE_LIST_VIEW_CONTENT_COMPONENT_TRACK_BY_FUNCTION, DbxAccordionHeaderHeightDirective, DbxActionConfirmDirective, DbxActionDialogDirective, DbxActionErrorDirective, DbxActionKeyTriggerDirective, DbxActionLoadingContextDirective, DbxActionModule, DbxActionPopoverDirective, DbxActionSnackbarComponent, DbxActionSnackbarDirective, DbxActionSnackbarErrorDirective, DbxActionSnackbarModule, DbxActionSnackbarService, DbxActionTransitionSafetyDirective, DbxActionUIRouterTransitionSafetyDialogComponent, DbxAnchorComponent, DbxAnchorContentComponent, DbxAnchorIconComponent, DbxAnchorListComponent, DbxAngularRouterSegueAnchorComponent, DbxAvatarComponent, DbxAvatarViewComponent, DbxAvatarViewService, DbxAvatarViewServiceConfig, DbxBarDirective, DbxBarHeaderComponent, DbxBarLayoutModule, DbxBasicLoadingComponent, DbxBodyDirective, DbxButtonComponent, DbxButtonModule, DbxButtonSpacerDirective, DbxCardBoxComponent, DbxCardBoxContainerDirective, DbxCardBoxLayoutModule, DbxChipDirective, DbxChipListComponent, DbxClickToCopyTextComponent, DbxClickToCopyTextDirective, DbxColorDirective, DbxColorService, DbxColorServiceConfig, DbxColumnLayoutModule, DbxCompactDirective, DbxCompactLayoutModule, DbxContentBorderDirective, DbxContentBoxDirective, DbxContentContainerDirective, DbxContentDirective, DbxContentElevateDirective, DbxContentLayoutModule, DbxContentPageDirective, DbxContentPitDirective, DbxDetachContentComponent, DbxDetachControlButtonsComponent, DbxDetachController, DbxDetachControlsComponent, DbxDetachInitDirective, DbxDetachInteractionModule, DbxDetachOutletComponent, DbxDetachOverlayComponent, DbxDetachService, DbxDetachWindowState, DbxDetailBlockComponent, DbxDetailBlockHeaderComponent, DbxDialogContentCloseComponent, DbxDialogContentDirective, DbxDialogContentFooterComponent, DbxDialogInteractionModule, DbxDialogModule, DbxDownloadBlobButtonComponent, DbxDownloadTextViewComponent, DbxEmbedComponent, DbxErrorComponent, DbxErrorDefaultErrorWidgetComponent, DbxErrorDetailsComponent, DbxErrorPopoverComponent, DbxErrorSnackbarComponent, DbxErrorSnackbarService, DbxErrorViewComponent, DbxErrorWidgetService, DbxErrorWidgetViewComponent, DbxFileUploadActionCompatable, DbxFileUploadActionSyncDirective, DbxFileUploadAreaComponent, DbxFileUploadButtonComponent, DbxFileUploadComponent, DbxFilterInteractionModule, DbxFilterPopoverButtonComponent, DbxFilterPopoverComponent, DbxFilterWrapperComponent, DbxFlagComponent, DbxFlagLayoutModule, DbxFlagPromptComponent, DbxFlexGroupDirective, DbxFlexLayoutModule, DbxFlexSizeDirective, DbxHelpContextDirective, DbxHelpContextService, DbxHelpViewListComponent, DbxHelpViewListEntryComponent, DbxHelpViewPopoverButtonComponent, DbxHelpViewPopoverComponent, DbxHelpWidgetService, DbxHelpWidgetServiceConfig, DbxIconButtonComponent, DbxIconButtonModule, DbxIconItemComponent, DbxIconSpacerDirective, DbxIconTileComponent, DbxIconTileDirective, DbxIfSidenavDisplayModeDirective, DbxIframeComponent, DbxInjectionDialogComponent, DbxInteractionModule, DbxIntroActionSectionComponent, DbxLabelBlockComponent, DbxLayoutModule, DbxLinkComponent, DbxLinkifyComponent, DbxLinkifyService, DbxLinkifyServiceConfig, DbxListAccordionViewComponentImportsModule, DbxListComponent, DbxListEmptyContentComponent, DbxListGridViewComponentImportsModule, DbxListInternalContentDirective, DbxListItemAnchorModifierDirective, DbxListItemDisableRippleModifierDirective, DbxListItemIsSelectedModifierDirective, DbxListModifierModule, DbxListModule, DbxListTitleGroupDirective, DbxListView, DbxListViewMetaIconComponent, DbxListViewWrapper, DbxListWrapperComponentImportsModule, DbxLoadingComponent, DbxLoadingErrorDirective, DbxLoadingModule, DbxLoadingProgressComponent, DbxModelObjectStateService, actions as DbxModelStateActions, model_actions as DbxModelStateModelActions, DbxModelTrackerService, DbxModelTypesService, DbxModelViewTrackerStorage, DbxNavbarComponent, DbxNumberWithLimitComponent, DbxOneColumnComponent, DbxOneColumnLayoutModule, DbxPagebarComponent, DbxPartialPresetFilterListComponent, DbxPartialPresetFilterMenuComponent, DbxPdfMergeEditorComponent, DbxPdfMergeEditorFileUploadComponent, DbxPdfMergeEditorFileUploadHasStateDirective, DbxPdfMergeEditorFileUploadValidatorDirective, DbxPdfMergeEditorStore, DbxPdfMergeEditorStoreDirective, DbxPdfMergeEntryComponent, DbxPdfMergeImportComponent, DbxPdfMergeListComponent, DbxPdfMergePageComponent, DbxPdfMergePageListComponent, DbxPdfMergeUploadActionDirective, DbxPdfMergeUploadButtonDirective, DbxPdfMergeUploadDialogComponent, DbxPdfPreviewComponent, DbxPopoverCloseButtonComponent, DbxPopoverComponent, DbxPopoverComponentController, DbxPopoverContentComponent, DbxPopoverController, DbxPopoverControlsDirective, DbxPopoverCoordinatorComponent, DbxPopoverCoordinatorService, DbxPopoverHeaderComponent, DbxPopoverInteractionContentModule, DbxPopoverInteractionModule, DbxPopoverScrollContentDirective, DbxPopoverService, DbxPopupComponent, DbxPopupComponentController, DbxPopupContentComponent, DbxPopupControlButtonsComponent, DbxPopupController, DbxPopupControlsComponent, DbxPopupCoordinatorComponent, DbxPopupCoordinatorService, DbxPopupInteractionModule, DbxPopupService, DbxPopupWindowState, DbxPresetFilterListComponent, DbxPresetFilterMenuComponent, DbxProgressBarButtonComponent, DbxProgressButtonsModule, DbxProgressSpinnerButtonComponent, DbxPromptBoxDirective, DbxPromptComponent, DbxPromptConfirm, DbxPromptConfirmButtonDirective, DbxPromptConfirmComponent, DbxPromptConfirmDialogComponent, DbxPromptConfirmDirective, DbxPromptModule, DbxPromptPageComponent, DbxReadableErrorModule, DbxResizedDirective, DbxRouterAnchorModule, DbxRouterLayoutModule, DbxRouterSidenavModule, DbxRouterWebProviderConfig, DbxScreenMediaService, DbxScreenMediaServiceConfig, DbxSectionComponent, DbxSectionHeaderComponent, DbxSectionLayoutModule, DbxSectionPageComponent, DbxSelectionValueListViewComponent, DbxSelectionValueListViewComponentImportsModule, DbxSelectionValueListViewContentComponent, DbxSetStyleDirective, DbxSidenavButtonComponent, DbxSidenavComponent, DbxSidenavPageComponent, DbxSidenavPagebarComponent, DbxSpacerDirective, DbxStepBlockComponent, DbxStructureDirective, DbxStructureModule, DbxStyleBodyDirective, DbxStyleDirective, DbxStyleLayoutModule, DbxStyleService, DbxSubSectionComponent, DbxTextChipsComponent, DbxTextColorDirective, DbxTextModule, DbxTwoBlockComponent, DbxTwoColumnBackDirective, DbxTwoColumnColumnHeadDirective, DbxTwoColumnComponent, DbxTwoColumnContextDirective, DbxTwoColumnFullLeftDirective, DbxTwoColumnLayoutModule, DbxTwoColumnRightComponent, DbxTwoColumnSrefDirective, DbxTwoColumnSrefShowRightDirective, DbxUIRouterSegueAnchorComponent, DbxUnitedStatesAddressComponent, DbxValueListAccordionViewComponent, DbxValueListAccordionViewContentComponent, DbxValueListAccordionViewContentGroupComponent, DbxValueListGridSizeDirective, DbxValueListGridViewComponent, DbxValueListGridViewContentComponent, DbxValueListGridViewContentGroupComponent, DbxValueListItemModifier, DbxValueListItemModifierDirective, DbxValueListView, DbxValueListViewComponent, DbxValueListViewComponentImportsModule, DbxValueListViewContentComponent, DbxValueListViewContentGroupComponent, DbxValueListViewGroupDelegate, DbxWebFilePreviewComponent, DbxWebFilePreviewService, DbxWebModule, DbxWebPageTitleInfoDirective, DbxWebPageTitleService, DbxWidgetListGridComponent, DbxWidgetListGridViewComponent, DbxWidgetListGridViewItemComponent, DbxWidgetService, DbxWidgetViewComponent, DbxWindowKeyDownListenerDirective, DbxZipBlobPreviewComponent, DbxZipPreviewComponent, PDF_MERGE_PAGE_ID_SEPARATOR, PDF_MERGE_PAGE_TAG_KEY, PDF_MERGE_RESULT_MIME_TYPE, PDF_MERGE_SIDECAR_DESCRIPTION, PDF_MERGE_SIDECAR_FILE_NAME, PDF_MERGE_SIDECAR_VERSION, PopoverPositionStrategy, PopupGlobalPositionStrategy, SCREEN_MEDIA_WIDTH_TYPE_SIZE_MAP, SIDE_NAV_DISPLAY_MODE_ORDER, SideNavDisplayMode, TRACK_BY_MODEL_ID, TRACK_BY_MODEL_KEY, TwoColumnsContextStore, UNKNOWN_ERROR_WIDGET_CODE, addConfigToValueListItems, allDbxModelViewTrackerEventModelKeys, allDbxModelViewTrackerEventSetModelKeys, asPdfMergeFile, attachPdfMergeSidecar, buildPdfMergeEntriesFromSidecar, buildPdfMergeEntry, buildPdfMergeEntrySync, buildPdfMergePagePlan, catchErrorServerParams, classifyPdfMergeFile, compactModeFromInput, compareScreenMediaWidthTypes, compressImageFile, convertServerErrorParams, convertToPOJOServerErrorResponse, convertToServerErrorResponse, copyToClipboardFunction, dbxColorBackground, dbxCuratedColorConfigForString, dbxListAccordionViewComponentImportsAndExports, dbxListGridViewComponentImportsAndExports, dbxPresetFilterMenuButtonIconObservable, dbxPresetFilterMenuButtonTextObservable, dbxStyleClassCleanSuffix, dbxThemeColorCssToken, dbxThemeColorCssTokenVar, dbxThemeColorCssVariable, dbxThemeColorCssVariableVar, dbxValueListItemDecisionFunction, dbxValueListItemKeyForItemValue, dbxWebDefaultPageTitleDelegate, dbxZipBlobPreviewEntryTreeFromEntries, defaultDbxModelViewTrackerStorageAccessorFactory, defaultDbxValueListViewGroupDelegate, defaultDbxValueListViewGroupValuesFunction, disableRightClickInCdkBackdrop, entryIdForPdfMergePageId, fileAcceptFilterTypeStringArray, fileAcceptFunction, fileAcceptString, fileArrayAcceptMatchFunction, flattenAccordionGroups, formatPdfMergeEntrySize, index as fromDbxModel, injectCopyToClipboardFunction, injectCopyToClipboardFunctionWithSnackbarMessage, isDbxColorConfig, listItemModifier, makeDbxActionSnackbarDisplayConfigGeneratorFunction, makePdfMergePageId, makePdfMergeSidecar, makePdfMergeSidecarPageTag, mapCompactModeObs, mapValuesToValuesListItemConfigObs, mergePdfMergeEntries, index$1 as onDbxModel, openEmbedDialog, openIframeDialog, openPdfPreviewDialog, openZipPreviewDialog, overrideClickElementEffect, pdfMergePageGroupKeyForSlotId, provideDbxDetachController, provideDbxFileUploadActionCompatable, provideDbxHelpServices, provideDbxLinkify, provideDbxListView, provideDbxListViewWrapper, provideDbxModelService, provideDbxPdfMergeEditorConfig, provideDbxPdfMergeEditorPreserveEntriesOnSlotDestroy, provideDbxProgressButtonGlobalConfig, provideDbxPromptConfirm, provideDbxRouterWebAngularRouterProviderConfig, provideDbxRouterWebUiRouterProviderConfig, provideDbxScreenMediaService, provideDbxStyleService, provideDbxValueListView, provideDbxValueListViewGroupDelegate, provideDbxValueListViewModifier, provideDbxWebFilePreviewServiceEntries, provideDbxWebPageTitleService, provideTwoColumnsContext, readPdfMergeEntryPageMetas, readPdfMergePageTag, readPdfMergeSidecar, registerHelpContextKeysWithDbxHelpContextService, resizeSignal, resolveSideNavDisplayMode, sanitizeDbxDialogContentConfig, screenMediaWidthTypeIsActive, splitPdfMergeSidecarDocuments, trackByModelKeyRef, trackByUniqueIdentifier, validatePdfMergeEntry, writePdfMergePageTag };
19812
21537
  //# sourceMappingURL=dereekb-dbx-web.mjs.map