pptx-angular-viewer 1.1.43 → 1.1.44

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.
@@ -16519,6 +16519,60 @@ class EditorHistory {
16519
16519
  }
16520
16520
  }
16521
16521
 
16522
+ /**
16523
+ * ole-actions.ts - framework-agnostic helpers for the OLE download/open UI.
16524
+ *
16525
+ * Shared by the binding OLE renderers to format the recovered embedded payload
16526
+ * size and decide whether its MIME type can be opened directly in a browser tab
16527
+ * (vs download-only). Pure functions, no framework or DOM dependencies.
16528
+ *
16529
+ * @module shared/render/ole-actions
16530
+ */
16531
+ const BYTE_UNITS = ['bytes', 'KB', 'MB', 'GB', 'TB'];
16532
+ /**
16533
+ * Human-readable binary file size (1024-based), e.g. `1 byte`, `0 bytes`,
16534
+ * `12 MB`. Returns `undefined` for missing, negative, or non-finite input so
16535
+ * callers can simply omit the size when it is unknown.
16536
+ */
16537
+ function formatBytes$1(bytes) {
16538
+ if (bytes === undefined || !Number.isFinite(bytes) || bytes < 0) {
16539
+ return undefined;
16540
+ }
16541
+ if (bytes === 1) {
16542
+ return '1 byte';
16543
+ }
16544
+ if (bytes < 1024) {
16545
+ return `${bytes} bytes`;
16546
+ }
16547
+ let value = bytes;
16548
+ let unit = 0;
16549
+ while (value >= 1024 && unit < BYTE_UNITS.length - 1) {
16550
+ value /= 1024;
16551
+ unit++;
16552
+ }
16553
+ // One decimal place, trimming a trailing `.0`.
16554
+ const rounded = Math.round(value * 10) / 10;
16555
+ const text = Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
16556
+ return `${text} ${BYTE_UNITS[unit]}`;
16557
+ }
16558
+ /**
16559
+ * Whether a payload of the given MIME type can be opened directly in a browser
16560
+ * tab (PDF, images, plain text, JSON, XML). Everything else is download-only.
16561
+ */
16562
+ function isBrowserOpenableMime$1(mime) {
16563
+ if (!mime) {
16564
+ return false;
16565
+ }
16566
+ const value = mime.trim().toLowerCase();
16567
+ if (value === 'application/pdf') {
16568
+ return true;
16569
+ }
16570
+ if (value.startsWith('image/') || value.startsWith('text/')) {
16571
+ return true;
16572
+ }
16573
+ return value === 'application/json' || value === 'application/xml';
16574
+ }
16575
+
16522
16576
  /**
16523
16577
  * Pure snap-and-alignment-guide geometry shared by the React, Vue, and Angular
16524
16578
  * editor bindings. No framework imports, no DOM, no side effects — only data
@@ -45194,6 +45248,112 @@ function getPlaceholderStyle(type) {
45194
45248
  'background-color': `${color}0d`,
45195
45249
  };
45196
45250
  }
45251
+ // ==========================================================================
45252
+ // Embedded-payload size + MIME helpers
45253
+ // ==========================================================================
45254
+ /**
45255
+ * Format a byte count as a short human-readable string (e.g. `"1.5 KB"`,
45256
+ * `"2.3 MB"`) using binary (1024) units. Returns `undefined` for missing,
45257
+ * non-finite, or negative input so callers can omit the size row entirely.
45258
+ *
45259
+ * Kept local to the Angular binding for now: the equivalent shared helper is
45260
+ * being churned by a parallel session, so this avoids a flaky cross-package
45261
+ * dependency. Extract to `pptx-viewer-shared` once that settles.
45262
+ */
45263
+ function formatBytes(bytes) {
45264
+ if (bytes === undefined || !Number.isFinite(bytes) || bytes < 0) {
45265
+ return undefined;
45266
+ }
45267
+ if (bytes < 1024) {
45268
+ return bytes === 1 ? '1 byte' : `${Math.round(bytes)} bytes`;
45269
+ }
45270
+ const units = ['KB', 'MB', 'GB', 'TB'];
45271
+ let value = bytes / 1024;
45272
+ let unitIndex = 0;
45273
+ while (value >= 1024 && unitIndex < units.length - 1) {
45274
+ value /= 1024;
45275
+ unitIndex += 1;
45276
+ }
45277
+ const rounded = Math.round(value * 10) / 10;
45278
+ const text = Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
45279
+ return `${text} ${units[unitIndex]}`;
45280
+ }
45281
+ /**
45282
+ * Whether a payload of the given MIME type can be opened (previewed) inline in
45283
+ * a new browser tab rather than only downloaded. PDFs, plain text / HTML / CSV /
45284
+ * JSON, and any `image/*` or `text/*` type qualify; binary office formats do
45285
+ * not. Matching is case-insensitive and ignores any `; charset=...` parameter.
45286
+ */
45287
+ function isBrowserOpenableMime(mimeType) {
45288
+ if (!mimeType) {
45289
+ return false;
45290
+ }
45291
+ const normalized = mimeType.split(';', 1)[0]?.trim().toLowerCase() ?? '';
45292
+ if (normalized.length === 0) {
45293
+ return false;
45294
+ }
45295
+ if (normalized.startsWith('image/') || normalized.startsWith('text/')) {
45296
+ return true;
45297
+ }
45298
+ return (normalized === 'application/pdf' ||
45299
+ normalized === 'application/json' ||
45300
+ normalized === 'application/xml' ||
45301
+ normalized === 'application/xhtml+xml');
45302
+ }
45303
+ // ==========================================================================
45304
+ // Embedded-payload actions (Download / Open) + richer info
45305
+ // ==========================================================================
45306
+ /**
45307
+ * Resolve the file name to use for the download anchor's `download` attribute:
45308
+ * the recovered embedded name, then the authored file name, then a generic
45309
+ * fallback so the saved file is never nameless.
45310
+ */
45311
+ function getOleDownloadFileName(el) {
45312
+ return el.oleEmbeddedFileName ?? el.fileName ?? 'embedded-object';
45313
+ }
45314
+ /**
45315
+ * Build the descriptive info rows shown in the OLE caption.
45316
+ *
45317
+ * Rows: object type label, original file name (`oleEmbeddedFileName ?? fileName`),
45318
+ * human-readable size (`oleEmbeddedByteSize`), and application (`oleProgId`).
45319
+ * A row is omitted when its value is unknown, so the result may contain only
45320
+ * the always-present type row.
45321
+ */
45322
+ function buildOleInfoRows(el) {
45323
+ const rows = [
45324
+ { key: 'type', label: 'Type', value: getOleTypeLabel(resolveOleType(el)) },
45325
+ ];
45326
+ const fileName = el.oleEmbeddedFileName ?? el.fileName;
45327
+ if (fileName) {
45328
+ rows.push({ key: 'file', label: 'File', value: fileName });
45329
+ }
45330
+ const sizeLabel = formatBytes(el.oleEmbeddedByteSize);
45331
+ if (sizeLabel) {
45332
+ rows.push({ key: 'size', label: 'Size', value: sizeLabel });
45333
+ }
45334
+ if (el.oleProgId) {
45335
+ rows.push({ key: 'application', label: 'Application', value: el.oleProgId });
45336
+ }
45337
+ return rows;
45338
+ }
45339
+ /**
45340
+ * Build the full action model for an OLE element. Download is offered whenever a
45341
+ * non-empty embedded data-URL exists; Open is additionally offered when the
45342
+ * payload's MIME type is one a browser can render inline (PDF / image / text).
45343
+ */
45344
+ function buildOleActionModel(el) {
45345
+ const downloadHref = el.oleEmbeddedData;
45346
+ const canDownload = typeof downloadHref === 'string' && downloadHref.length > 0;
45347
+ const canOpen = canDownload && isBrowserOpenableMime(el.oleEmbeddedMimeType);
45348
+ return {
45349
+ canDownload,
45350
+ canOpen,
45351
+ downloadHref,
45352
+ downloadFileName: getOleDownloadFileName(el),
45353
+ sizeLabel: formatBytes(el.oleEmbeddedByteSize),
45354
+ info: buildOleInfoRows(el),
45355
+ };
45356
+ }
45197
45357
 
45198
45358
  /**
45199
45359
  * OleRendererComponent: Angular port of the React `renderOleElement`
@@ -45261,6 +45421,39 @@ class OleRendererComponent {
45261
45421
  /** Border + background style for the placeholder box. */
45262
45422
  placeholderStyle = computed(() => getPlaceholderStyle(this.oleType()), /* @ts-ignore */
45263
45423
  ...(ngDevMode ? [{ debugName: "placeholderStyle" }] : /* istanbul ignore next */ []));
45424
+ /**
45425
+ * Download / Open action model derived from the recovered embedded payload.
45426
+ * When the input is not an OLE element, every action is disabled.
45427
+ */
45428
+ actions = computed(() => {
45429
+ const el = this.ole();
45430
+ if (!el) {
45431
+ return {
45432
+ canDownload: false,
45433
+ canOpen: false,
45434
+ downloadHref: undefined,
45435
+ downloadFileName: 'embedded-object',
45436
+ sizeLabel: undefined,
45437
+ info: [],
45438
+ };
45439
+ }
45440
+ return buildOleActionModel(el);
45441
+ }, /* @ts-ignore */
45442
+ ...(ngDevMode ? [{ debugName: "actions" }] : /* istanbul ignore next */ []));
45443
+ /**
45444
+ * Descriptive tooltip for the wrapper: the info rows joined as
45445
+ * "Label: value" pairs (e.g. "Type: Excel Spreadsheet, File: budget.xlsx,
45446
+ * Size: 2.3 KB, Application: Excel.Sheet.12"). Falls back to the aria label
45447
+ * when no rows are available.
45448
+ */
45449
+ infoTitle = computed(() => {
45450
+ const rows = this.actions().info;
45451
+ if (rows.length === 0) {
45452
+ return this.ariaLabel();
45453
+ }
45454
+ return rows.map((row) => `${row.label}: ${row.value}`).join(', ');
45455
+ }, /* @ts-ignore */
45456
+ ...(ngDevMode ? [{ debugName: "infoTitle" }] : /* istanbul ignore next */ []));
45264
45457
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: OleRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
45265
45458
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.2", type: OleRendererComponent, isStandalone: true, selector: "pptx-ole-renderer", inputs: { element: { classPropertyName: "element", publicName: "element", isSignal: true, isRequired: true, transformFunction: null }, zIndex: { classPropertyName: "zIndex", publicName: "zIndex", isSignal: true, isRequired: false, transformFunction: null } }, ngImport: i0, template: `
45266
45459
  <div
@@ -45269,7 +45462,7 @@ class OleRendererComponent {
45269
45462
  [attr.data-element-id]="element().id"
45270
45463
  role="img"
45271
45464
  [attr.aria-label]="ariaLabel()"
45272
- title="Embedded object"
45465
+ [attr.title]="infoTitle()"
45273
45466
  >
45274
45467
  @if (previewSrc()) {
45275
45468
  <!-- Preview image with type-badge overlay -->
@@ -45514,8 +45707,43 @@ class OleRendererComponent {
45514
45707
  }
45515
45708
  </div>
45516
45709
  }
45710
+ @if (actions().canDownload) {
45711
+ <!--
45712
+ Download / Open actions for the recovered embedded payload.
45713
+ Pointer events are isolated so clicking an action never starts a
45714
+ selection/drag of the underlying element; the controls are
45715
+ keyboard-focusable and only paint on hover / focus-within.
45716
+ -->
45717
+ <div
45718
+ class="pptx-ng-ole-actions"
45719
+ (pointerdown)="$event.stopPropagation()"
45720
+ (mousedown)="$event.stopPropagation()"
45721
+ >
45722
+ <a
45723
+ class="pptx-ng-ole-action"
45724
+ [href]="actions().downloadHref"
45725
+ [attr.download]="actions().downloadFileName"
45726
+ [attr.aria-label]="'Download ' + actions().downloadFileName"
45727
+ (click)="$event.stopPropagation()"
45728
+ >
45729
+ Download
45730
+ </a>
45731
+ @if (actions().canOpen) {
45732
+ <a
45733
+ class="pptx-ng-ole-action"
45734
+ [href]="actions().downloadHref"
45735
+ target="_blank"
45736
+ rel="noopener noreferrer"
45737
+ [attr.aria-label]="'Open ' + actions().downloadFileName + ' in a new tab'"
45738
+ (click)="$event.stopPropagation()"
45739
+ >
45740
+ Open
45741
+ </a>
45742
+ }
45743
+ </div>
45744
+ }
45517
45745
  </div>
45518
- `, isInline: true, styles: [".pptx-ng-ole-preview{position:relative;width:100%;height:100%}.pptx-ng-ole-img{width:100%;height:100%;object-fit:contain;pointer-events:none;-webkit-user-select:none;user-select:none;display:block}.pptx-ng-ole-badge{position:absolute;bottom:4px;right:4px;z-index:10}.pptx-ng-ole-placeholder{width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;pointer-events:none;box-sizing:border-box}.pptx-ng-ole-name{margin-top:8px;font-size:12px;font-weight:500;max-width:90%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pptx-ng-ole-sublabel{margin-top:2px;font-size:10px;color:#00000073;max-width:90%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n"], dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
45746
+ `, isInline: true, styles: [".pptx-ng-ole-preview{position:relative;width:100%;height:100%}.pptx-ng-ole-img{width:100%;height:100%;object-fit:contain;pointer-events:none;-webkit-user-select:none;user-select:none;display:block}.pptx-ng-ole-badge{position:absolute;bottom:4px;right:4px;z-index:10}.pptx-ng-ole-placeholder{width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;pointer-events:none;box-sizing:border-box}.pptx-ng-ole-name{margin-top:8px;font-size:12px;font-weight:500;max-width:90%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pptx-ng-ole-sublabel{margin-top:2px;font-size:10px;color:#00000073;max-width:90%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pptx-ng-ole-actions{position:absolute;bottom:4px;left:4px;display:flex;gap:4px;z-index:11;opacity:0;transition:opacity .12s ease-in-out}.pptx-ng-ole:hover .pptx-ng-ole-actions,.pptx-ng-ole-actions:focus-within{opacity:1}.pptx-ng-ole-action{font-size:11px;line-height:1;padding:4px 8px;border-radius:4px;background-color:#000000b8;color:#fff;text-decoration:none;cursor:pointer;white-space:nowrap;pointer-events:auto}.pptx-ng-ole-action:hover{background-color:#000000d9}.pptx-ng-ole-action:focus-visible{outline:2px solid #fff;outline-offset:1px}\n"], dependencies: [{ kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
45519
45747
  }
45520
45748
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImport: i0, type: OleRendererComponent, decorators: [{
45521
45749
  type: Component,
@@ -45526,7 +45754,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
45526
45754
  [attr.data-element-id]="element().id"
45527
45755
  role="img"
45528
45756
  [attr.aria-label]="ariaLabel()"
45529
- title="Embedded object"
45757
+ [attr.title]="infoTitle()"
45530
45758
  >
45531
45759
  @if (previewSrc()) {
45532
45760
  <!-- Preview image with type-badge overlay -->
@@ -45771,8 +45999,43 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.2", ngImpor
45771
45999
  }
45772
46000
  </div>
45773
46001
  }
46002
+ @if (actions().canDownload) {
46003
+ <!--
46004
+ Download / Open actions for the recovered embedded payload.
46005
+ Pointer events are isolated so clicking an action never starts a
46006
+ selection/drag of the underlying element; the controls are
46007
+ keyboard-focusable and only paint on hover / focus-within.
46008
+ -->
46009
+ <div
46010
+ class="pptx-ng-ole-actions"
46011
+ (pointerdown)="$event.stopPropagation()"
46012
+ (mousedown)="$event.stopPropagation()"
46013
+ >
46014
+ <a
46015
+ class="pptx-ng-ole-action"
46016
+ [href]="actions().downloadHref"
46017
+ [attr.download]="actions().downloadFileName"
46018
+ [attr.aria-label]="'Download ' + actions().downloadFileName"
46019
+ (click)="$event.stopPropagation()"
46020
+ >
46021
+ Download
46022
+ </a>
46023
+ @if (actions().canOpen) {
46024
+ <a
46025
+ class="pptx-ng-ole-action"
46026
+ [href]="actions().downloadHref"
46027
+ target="_blank"
46028
+ rel="noopener noreferrer"
46029
+ [attr.aria-label]="'Open ' + actions().downloadFileName + ' in a new tab'"
46030
+ (click)="$event.stopPropagation()"
46031
+ >
46032
+ Open
46033
+ </a>
46034
+ }
46035
+ </div>
46036
+ }
45774
46037
  </div>
45775
- `, styles: [".pptx-ng-ole-preview{position:relative;width:100%;height:100%}.pptx-ng-ole-img{width:100%;height:100%;object-fit:contain;pointer-events:none;-webkit-user-select:none;user-select:none;display:block}.pptx-ng-ole-badge{position:absolute;bottom:4px;right:4px;z-index:10}.pptx-ng-ole-placeholder{width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;pointer-events:none;box-sizing:border-box}.pptx-ng-ole-name{margin-top:8px;font-size:12px;font-weight:500;max-width:90%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pptx-ng-ole-sublabel{margin-top:2px;font-size:10px;color:#00000073;max-width:90%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}\n"] }]
46038
+ `, styles: [".pptx-ng-ole-preview{position:relative;width:100%;height:100%}.pptx-ng-ole-img{width:100%;height:100%;object-fit:contain;pointer-events:none;-webkit-user-select:none;user-select:none;display:block}.pptx-ng-ole-badge{position:absolute;bottom:4px;right:4px;z-index:10}.pptx-ng-ole-placeholder{width:100%;height:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;pointer-events:none;box-sizing:border-box}.pptx-ng-ole-name{margin-top:8px;font-size:12px;font-weight:500;max-width:90%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pptx-ng-ole-sublabel{margin-top:2px;font-size:10px;color:#00000073;max-width:90%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.pptx-ng-ole-actions{position:absolute;bottom:4px;left:4px;display:flex;gap:4px;z-index:11;opacity:0;transition:opacity .12s ease-in-out}.pptx-ng-ole:hover .pptx-ng-ole-actions,.pptx-ng-ole-actions:focus-within{opacity:1}.pptx-ng-ole-action{font-size:11px;line-height:1;padding:4px 8px;border-radius:4px;background-color:#000000b8;color:#fff;text-decoration:none;cursor:pointer;white-space:nowrap;pointer-events:auto}.pptx-ng-ole-action:hover{background-color:#000000d9}.pptx-ng-ole-action:focus-visible{outline:2px solid #fff;outline-offset:1px}\n"] }]
45776
46039
  }], propDecorators: { element: [{ type: i0.Input, args: [{ isSignal: true, alias: "element", required: true }] }], zIndex: [{ type: i0.Input, args: [{ isSignal: true, alias: "zIndex", required: false }] }] } });
45777
46040
 
45778
46041
  /**