# Panduan Penggunaan — Universal Document Viewer (Vue 2 + Vue 3)

> **Plug & play**, JavaScript biasa, tanpa TypeScript, tanpa `.vue` Single File Component, dan **tanpa instalasi npm / proses build** untuk memasang komponen pada aplikasi yang sudah berjalan.
>
> Satu paket berisi dua versi. **Pilih salah satu** sesuai versi Vue di aplikasi Anda.

## Daftar isi

1. [Isi package dan pemilihan versi](#1-isi-package-dan-pemilihan-versi)
2. [Instalasi Vue 2](#2-instalasi-vue-2)
3. [Instalasi Vue 3](#3-instalasi-vue-3)
4. [Contoh pemakaian berdasarkan kebutuhan](#4-contoh-pemakaian-berdasarkan-kebutuhan)
5. [Format file dan metode preview](#5-format-file-dan-metode-preview)
6. [Seluruh props](#6-seluruh-props)
7. [Event dan method](#7-event-dan-method)
8. [CDN dan penggunaan di intranet](#8-cdn-dan-penggunaan-di-intranet)
9. [Keamanan dokumen dan PDF locked](#9-keamanan-dokumen-dan-pdf-locked)
10. [Troubleshooting](#10-troubleshooting)
11. [Menjalankan demo dan pengujian](#11-menjalankan-demo-dan-pengujian)

---

## 1. Isi package dan pemilihan versi

```text
dual-vue-document-viewer/
├── PANDUAN_PENGGUNAAN.md          <-- Panduan ini
├── README.md                      <-- Ringkasan teknis
├── dist/
│   ├── document-viewer.vue2.js    <-- Pilih untuk Vue 2
│   ├── document-viewer.vue3.js    <-- Pilih untuk Vue 3
│   └── document-viewer.css        <-- Wajib untuk kedua versi
├── examples/
│   ├── contoh-docx-kop-surat.docx <-- Sampel letterhead (logo JPEG di header)
│   ├── contoh-docx-lokal.docx     <-- Sampel Word sederhana header/footer
│   ├── vue2.html                  <-- Demo lengkap Vue 2
│   ├── vue2-demo.css              <-- Style demo Vue 2
│   ├── DEMO_VUE2.md               <-- Panduan demo Vue 2
│   ├── contoh-pdf.pdf             <-- PDF contoh 2 halaman
│   ├── contoh-excel.xlsx          <-- XLSX contoh 2 sheet
│   ├── contoh-gambar.png          <-- PNG contoh
│   ├── contoh-data.csv/.json      <-- CSV/JSON contoh
│   ├── vue3.html                  <-- Contoh interaktif Vue 3
│   └── demo.css
├── src/
│   └── viewer.common.js           <-- Source bersama
├── tools/
│   └── build.py                   <-- Opsional, build ulang JS
├── tests/
│   └── smoke.test.cjs             <-- Tes dasar
└── index.html                     <-- Halaman pemilihan demo
```

| Aplikasi | JS yang dipakai | Cara registrasi |
|---|---|---|
| **Vue 2.x** | `dist/document-viewer.vue2.js` | **Otomatis**, tanpa `components: {}` |
| **Vue 3.x** | `dist/document-viewer.vue3.js` | Sekali `app.use(window.DocumentViewer)` |

**Jangan include kedua file JS Vue 2 dan Vue 3 pada satu halaman.** CSS-nya sama.

> Penting: pakai Vue versi **full build** (dengan template compiler). Untuk Vue 2: `vue.min.js` / `vue.js`; Vue 3: `vue.global.prod.js` / `vue.global.js`. Build runtime-only tidak dapat mengompilasi template string komponen ini.

### 1.1 Pilih viewer Office: `viewer-default` + radio header (v1.3.12)

**`viewer-default`** memilih engine awal untuk dokumen Office: `local` (default), `microsoft`, atau `google`. Di toolbar viewer ada radio **Microsoft / GDocs / Local** — ganti langsung, **tanpa fallback otomatis**. Microsoft/GDocs hanya aktif bila `src` adalah URL HTTPS publik.

| viewer-default | Hasil (DOCX/XLSX, URL publik) |
|---|---|
| `local` (default) | Renderer lokal (`docx-preview` / SheetJS) |
| `microsoft` | Embed Microsoft Office Online |
| `google` | Embed Google Docs Viewer |

Bila Microsoft/GDocs dipilih tetapi `src` bukan HTTPS publik, viewer otomatis memakai **Local** dan mengirim `@provider-status` `skipped` (`source-not-public-https`). Ganti engine kapan saja lewat radio di header.

```html
<!-- Berlaku sama di Vue 2 dan Vue 3 -->
<document-viewer
  :src="fileUrl"
  filename="surat.docx"
  viewer-default="local"
  :docx-fit-width="true"
  :docx-options="{ breakPages: true, renderHeaders: true, renderFooters: true }"
  :allow-download="false"
  height="720px"
></document-viewer>
```

DOCX lokal dirender memakai `docx-preview` dengan **host style terpisah dan nama class khusus per komponen**, untuk mengurangi konflik style ketika beberapa dokumen terbuka sekaligus. Pengaturan bawaan mempertahankan ukuran/margin kertas, header/footer, footnote/endnote, tabel, gambar, dan font sebisa mungkin. Viewer menyediakan **zoom**, tombol **Pas lebar**, indikator proses render, serta navigasi antarbagian halaman yang ditemukan pada HTML hasil render. Opsi lanjut dapat dikirim melalui `docx-options`, misalnya `renderHeaders`, `renderFooters`, `breakPages`, atau `ignoreLastRenderedPageBreak`.

#### Akurasi kop surat / logo di header-footer (v1.3.4+)

Banyak template surat memakai gambar floating di header (`wp:anchor` + `wrapNone`). Library `docx-preview` menaruh gambar itu di host HTML berukuran **0×0**, sehingga CSS `max-width:100%` (dari aplikasi host) bisa membuat lebar gambar menjadi 0 — logo seolah hilang.

Viewer memperbaiki ini otomatis setelah render:

1. Menonaktifkan `max-width` pada gambar Word di dalam lembar.
2. Mengisi ulang ukuran host 0×0 dari ukuran gambar yang di-author.
3. Menggeser logo yang keluar lembar (`nudgeInsidePage`) agar tidak menutupi shadow halaman.
4. Menjaga clip lembar (`overflow:hidden`) supaya tinta header tetap di dalam kartu halaman.

Uji cepat: buka demo Vue 2 → upload/URL **Kop surat** (`examples/contoh-docx-kop-surat.docx`) dengan `viewer-default="local"`.

**Batasan DOCX lokal:** Konversi DOCX menjadi HTML tidak selalu mampu meniru pagination Microsoft Word, terutama untuk layout kompleks dan section tanpa page break eksplisit. Jumlah halaman yang ditampilkan merupakan bagian halaman terdeteksi, bukan jaminan jumlah halaman Word. Format metafile (**EMF/WMF**) tidak dapat ditampilkan browser tanpa konversi. Jika layout harus identik Word, konversikan ke PDF di server lalu buka dengan PDF.js.

**Penting:** `viewer-default="local"` menonaktifkan embed Microsoft/Google. Library `docx-preview`/JSZip masih diunduh dari CDN saat pertama dipakai, kecuali dikonfigurasi dari server lokal menggunakan `DocumentViewer.configure(...)`.

## 2. Instalasi Vue 2

### 2.1 Salin file

Misal aplikasi PHP/Vue 2 memakai direktori publik `/assets/plugins/document-viewer/`. Salin ketiga file dari `dist/` ke direktori tersebut, tetapi cukup gunakan **JS Vue 2** dan **CSS** di halaman.

### 2.2 Include CSS dan JS

Jika Vue 2 **sudah dimuat** oleh layout aplikasi, jangan muat Vue 2 lagi. Urutannya:

```html
<link rel="stylesheet" href="/assets/plugins/document-viewer/document-viewer.css">

<!-- Hanya jika halaman belum memuat Vue 2 -->
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.min.js"></script>

<script src="/assets/plugins/document-viewer/document-viewer.vue2.js"></script>
```

### 2.3 Panggil dalam template Vue 2

```html
<div id="app">
  <document-viewer
    :src="fileUrl"
    filename="surat-keluar.pdf"
    :locked="true"
    :allow-download="false"
    height="700px"
  ></document-viewer>
</div>

<script>
new Vue({
  el: '#app',
  data: function () {
    return { fileUrl: '/api/documents/123/preview' };
  }
});
</script>
```

**Tidak perlu** menuliskan `Vue.component()` atau menambahkan `components: {}` karena file Vue 2 otomatis mendaftarkan nama `<document-viewer>` secara global.

Bila ingin registrasi lokal pada instance tertentu saja, tersedia `window.DocumentViewer`, misalnya:

```javascript
new Vue({
  el: '#app',
  components: { DocumentViewer: window.DocumentViewer },
  data: function () { return { fileUrl: '/files/dokumen.pdf' }; }
});
```

> Dalam Vue 2 file JS ini **tetap melakukan registrasi global** saat di-include; contoh `components` di atas hanya menunjukkan cara memakai referensi komponennya.

## 3. Instalasi Vue 3

### 3.1 Include CSS dan JS

```html
<link rel="stylesheet" href="/assets/plugins/document-viewer/document-viewer.css">

<!-- Hanya jika halaman belum memuat Vue 3 -->
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.prod.js"></script>

<script src="/assets/plugins/document-viewer/document-viewer.vue3.js"></script>
```

### 3.2 Registrasikan satu kali pada aplikasi

```html
<div id="app">
  <document-viewer
    :src="fileUrl"
    filename="laporan.xlsx"
    :prefer-microsoft="true"
    height="700px"
  ></document-viewer>
</div>

<script>
const app = Vue.createApp({
  data() {
    return { fileUrl: 'https://files.example.com/laporan.xlsx' };
  }
});

app.use(window.DocumentViewer);
app.mount('#app');
</script>
```

Alternatif registrasi: `app.component('document-viewer', window.DocumentViewer)`.

## 4. Contoh pemakaian berdasarkan kebutuhan

Contoh di bawah merupakan isi **di dalam template aplikasi Vue**. Contoh berlaku untuk Vue 2 dan Vue 3 kecuali dijelaskan lain.

### A. PDF hanya preview (tanpa tombol download)

```html
<document-viewer
  src="/files/kontrak.pdf"
  filename="kontrak.pdf"
  :locked="true"
  :allow-download="false"
  watermark="INTERNAL"
  height="75vh"
></document-viewer>
```

`locked=true` menghilangkan aksi download dari toolbar dan menonaktifkan menu klik-kanan **di dalam area PDF viewer**. Parameter ini tidak bisa mengamankan file dari DevTools, screenshot, atau pengambilan URL asli.

### B. DOCX: Microsoft → Google Docs → lokal

```html
<document-viewer
  src="https://files.example.com/dokumen.docx"
  filename="dokumen.docx"
  :prefer-microsoft="true"
  :prefer-google="true"
  :office-timeout="12000"
  height="720px"
></document-viewer>
```

Urutan fallback default: **Microsoft Office → Google Docs Viewer → lokal**. Kedua preview online mengharuskan URL **HTTPS direct-file yang dapat diakses dari internet oleh penyedia**. URL privat/terautentikasi gunakan `:prefer-microsoft="false" :prefer-google="false"` untuk menjamin tidak mengirim data ke penyedia eksternal.

Jika Microsoft mengalami timeout **sebelum iframe mengirim `load`**, memicu error iframe, atau shell sudah `load` tetapi tetap kosong selama `officeTimeout` (status **Dokumen belum terverifikasi**), komponen pindah ke Google lalu ke `docx-preview` lokal. Status belum terverifikasi **bukan** sukses preview, dan event `@loaded` **tidak dipanggil**. Anda juga bisa menekan **Coba Google Docs** / **Preview lokal** lebih awal.

### C. Excel dengan fallback lokal dan batas tampilan

```html
<document-viewer
  src="/api/report/excel/2026"
  filename="rekap.xlsx"
  :prefer-microsoft="false"
  :prefer-google="false"
  :max-rows="500"
  :max-columns="40"
  height="700px"
></document-viewer>
```

Excel lokal menampilkan **tab sheet**, nilai sel terformat, dan sejumlah baris/kolom pertama. Bukan editor spreadsheet; rumus, bagan, pivot, dan tampilan cetak Office tidak selalu sama persis dengan Excel.

### D. File dari input upload, tanpa mengirim ke server terlebih dulu

```html
<div id="app">
  <input type="file" @change="pilihFile">
  <document-viewer :src="file" height="600px"></document-viewer>
</div>
```

Untuk **Vue 2**, tambahkan ke object Vue yang sudah ada:

```javascript
data: function () {
  return { file: null };
},
methods: {
  pilihFile: function (e) {
    this.file = e.target.files[0] || null;
  }
}
```

Untuk **Vue 3 Options API**, data/method sama konsepnya (gunakan `data() { return { file: null }; }`). Input menghasilkan object `File` sehingga tidak membutuhkan fetch URL atau upload backend untuk preview.

### E. URL API dengan Authorization / sesi login

```html
<document-viewer
  :src="previewUrl"
  filename="surat-rahasia.pdf"
  :request-headers="apiHeaders"
  request-credentials="include"
  :locked="true"
  :allow-download="false"
  height="720px"
></document-viewer>
```

Contoh isi data pada aplikasi:

```javascript
// Disimpan di data/computed Vue, bukan sebagai atribut hard-coded di HTML.
previewUrl: '/api/arsip/123/preview',
apiHeaders: { Authorization: 'Bearer ' + accessToken }
```

- Jika menggunakan cookie sesi pada domain sama, default `request-credentials="same-origin"` biasanya cukup.
- Jika beda origin dengan cookie, `include` memerlukan konfigurasi CORS dan cookie yang sesuai pada server.
- Jika header Authorization diisi atau `request-credentials="include"`, komponen memilih renderer lokal untuk file Office yang mendukung fallback. Microsoft dan Google Docs Viewer **tidak** bisa diberi header Authorization internal aplikasi.
- Untuk URL HTTPS yang memerlukan *cookie login* meski tanpa header Authorization, gunakan `:prefer-microsoft="false" :prefer-google="false"`. URL privat tidak boleh dikirim ke provider online.
- Pastikan endpoint backend memvalidasi **hak akses terhadap dokumen**, bukan hanya apakah pengguna sudah login.

### F. Komponen di dalam modal Bootstrap / dialog

```html
<div class="modal-body">
  <document-viewer
    v-if="showPreview"
    :src="selectedFileUrl"
    :filename="selectedFileName"
    height="65vh"
    width="100%"
  ></document-viewer>
</div>
```

Set `showPreview=true` saat modal dibuka dan `false` saat ditutup apabila ingin melepaskan object URL dan memori viewer. Hindari membuat banyak viewer berat sekaligus dalam tabel.

### G. Berpindah file tanpa membuat ulang komponen

```html
<document-viewer :src="fileAktif.url" :filename="fileAktif.nama"></document-viewer>
```

Komponen akan membuka ulang preview saat `src` atau `filename` berubah. **Untuk URL API yang tidak memiliki ekstensi**, beri `filename` dengan ekstensi sebenarnya (misalnya `surat.pdf` atau `laporan.xlsx`).

## 5. Format file dan metode preview

| Format | Renderer utama | Fallback / keterangan |
|---|---|---|
| `.pdf` | PDF.js (canvas) | Tidak memakai toolbar PDF bawaan browser |
| `.jpg`, `.jpeg`, `.png`, `.gif`, `.webp`, `.bmp`, `.avif`, `.svg` | Browser image | Zoom |
| `.docx` | Microsoft → Google Docs → `docx-preview` lokal | Online hanya jika URL HTTPS direct-file yang dapat dijangkau penyedia |
| `.xlsx`, `.xls`, `.xlsm` | Microsoft → Google Docs → SheetJS lokal | Sheet dan batas baris/kolom pada fallback lokal |
| `.doc`, `.ppt`, `.pptx` | Microsoft → Google Docs | **Belum ada fallback lokal**; sediakan konversi di backend |
| `.csv`, `.ods` | SheetJS lokal | Navigasi sheet bila relevan |
| `.txt`, `.log`, `.json`, `.xml`, `.md`, `.markdown`, `.yml`, `.yaml`, `.sql`, `.js`, `.css` | Plain text | JSON dipretty-print jika valid; Markdown belum dirender sebagai HTML |
| `.mp3`, `.wav`, `.ogg`, `.m4a`, `.flac`, `.aac` | Audio browser | Bergantung dukungan codec |
| `.mp4`, `.webm`, `.ogv`, `.mov` | Video browser | Bergantung dukungan codec |

### Tahapan fallback online untuk file Office

```text
DOCX / XLS / XLSX / XLSM / DOC / PPT / PPTX
                   │
           URL direct HTTPS publik?
                   │
              Ya   ▼
       1. MICROSOFT OFFICE
                   │ error / timeout iframe
                   ▼
       2. GOOGLE DOCS VIEWER
                   │ error / timeout iframe
                   ▼
       3. VIEWER LOKAL
          DOCX → docx-preview
          Excel → SheetJS
          DOC/PPT/PPTX → belum didukung secara lokal
```

- **Default:** `prefer-microsoft=true`, `prefer-google=true`, `office-timeout=12000` milidetik untuk **masing-masing** penyedia online.
- **Sebelum iframe termuat:** tampil status `loading`; jika timeout atau ada event iframe `error`, status menjadi `failed` dan otomatis beralih Microsoft → Google → lokal.
- **Sesudah `iframe.onload`:** status `unverified`, bukan `loaded`. Timer loading dihentikan, supaya preview yang sebenarnya sudah terlihat **tidak otomatis hilang** setelah 12 detik. Halaman error/blank di dalam iframe tidak dapat dideteksi dengan andal.
- **Saat tombol fallback diklik:** status `skipped` untuk provider lama; provider baru mulai dari `loading`.
- **Tidak dapat dijangkau penyedia:** `File`, `Blob`, localhost, URL intranet, atau permintaan dengan `request-headers`/`request-credentials="include"` langsung menggunakan renderer lokal. URL publik HTTPS tetap perlu diuji dari sisi layanan eksternal.
- **Manual:** saat Microsoft aktif tersedia tombol **Coba Google Docs** dan **Preview lokal** (bila didukung). Saat Google aktif tersedia tombol **Preview lokal**. Fitur ini berguna ketika iframe sudah `load` tetapi hanya menampilkan error/blank.
- **Batas teknis:** karena *cross-origin iframe*, kita **tidak dapat memverifikasi isi dokumen Microsoft/Google dari JS**. Event `load` bukan bukti preview berhasil; komponen **tidak lagi mengirim `@loaded` untuk iframe**. Auto-fallback hanya dapat mendeteksi sebagian error iframe atau timeout *sebelum* `load`. Jangan menjadikan `status=unverified` sebagai konfirmasi tampilan berhasil.
- **Race condition:** event `load/error` dari iframe lama tidak bisa mengubah provider baru. Setiap percobaan menggunakan `attemptId` yang berbeda.
- **Kerahasiaan:** untuk mematikan dua provider sekaligus gunakan `:online="false"`; ini mengungguli kedua preferensi.
- **Kerahasiaan:** `src` dan isi dokumen dari URL publik dapat diambil layanan Microsoft dan Google. Untuk arsip rahasia gunakan `:prefer-microsoft="false" :prefer-google="false"`.

Contoh **Google dahulu, lalu lokal** (lewati Microsoft):

```html
<document-viewer
  src="https://files.example.com/rekap.xlsx"
  filename="rekap.xlsx"
  :prefer-microsoft="false"
  :prefer-google="true"
></document-viewer>
```

Contoh **langsung lokal**:

```html
<document-viewer
  :src="urlDokumenPrivat"
  filename="laporan.docx"
  :prefer-microsoft="false"
  :prefer-google="false"
></document-viewer>
```

## 6. Seluruh props

| Prop di HTML | Tipe | Default | Fungsi |
|---|---|---|---|
| `src` | `String`, `File`, `Blob` | `null` | URL atau file yang dipreview |
| `filename` | `String` | `''` | Nama file, penting untuk URL API tanpa ekstensi |
| `mime-type` | `String` | `''` | Bantuan deteksi format, mis. `application/pdf` |
| `width` | `String` | `100%` | Lebar viewer |
| `height` | `String` | `650px` | Tinggi viewer |
| `locked` | `Boolean` | `false` | Mematikan download via UI dan menu klik-kanan pada PDF |
| `allow-download` | `Boolean` | `true` | Tampilkan tombol download jika tidak locked |
| `viewer-default` | `String` | `local` | Engine Office awal: `local` \| `microsoft` \| `google` (radio di header; tanpa fallback otomatis) |
| `docx-fit-width` | `Boolean` | `true` | Otomatis menyesuaikan lebar halaman DOCX pada viewer; tersedia tombol Pas lebar |
| `docx-options` | `Object` | `{}` | Opsi bawaan `docx-preview` (breakPages, renderHeaders, renderFooters, dll.) |
| `max-rows` | `Number` | `250` | Maksimal baris Excel lokal |
| `max-columns` | `Number` | `30` | Maksimal kolom Excel lokal |
| `request-headers` | `Object` | `{}` | Headers saat fetch URL untuk render lokal |
| `request-credentials` | `String` | `same-origin` | `same-origin`, `include`, atau `omit` |
| `watermark` | `String` | `''` | Teks overlay di preview |
| `show-toolbar` | `Boolean` | `true` | Tampilkan/sembunyikan toolbar |

**Catatan binding:** nilai Boolean, Number, atau Object dinamis gunakan tanda `:` (`:locked="true"`, `:max-rows="500"`, `:request-headers="headers"`). Tanpa `:`, HTML mengirim string.

## 7. Event dan method

### Event

| Event | Payload | Kapan terjadi |
|---|---|---|
| `@loaded` | `{ engine, filename }` | **Hanya** renderer lokal selesai memproses; **bukan** untuk iframe Microsoft/Google |
| `@viewer-change` | `{ viewer, filename }` | User mengganti radio Microsoft / GDocs / Local |
| `@update:viewerDefault` | `String` | Sinkron nilai `viewer-default` (Vue 2 `.sync` / Vue 3 `v-model:viewer-default`) |
| `@provider-status` | `{ engine, status, reason, verified, filename, attemptId }` | Status lifecycle online: `loading`, `unverified`, `failed`, atau `skipped`; `verified` selalu `false` |
| `@provider-change` | `{ engine, filename }` | Saat perpindahan provider (`microsoft`, `google`, `local`) |
| `@error` | String | Preview mengalami kegagalan |
| `@download` | `{ filename }` | Tombol download internal berhasil dijalankan |

```html
<document-viewer
  ref="viewer"
  :src="url"
  @loaded="onPreviewLoaded"
  @provider-status="onProviderStatus"
  @provider-change="onProviderChange"
  @error="onPreviewError"
></document-viewer>
```

```javascript
methods: {
  onPreviewLoaded: function (info) {
    // Hanya untuk renderer lokal, bukan untuk Microsoft/Google.
    console.log('Lokal siap:', info.engine, info.filename);
  },
  onProviderStatus: function (info) {
    // loading: menghubungkan iframe
    // unverified: iframe termuat, isi dokumen TIDAK terverifikasi
    // failed: iframe error / timeout; fallback otomatis berjalan
    // skipped: pengguna memilih fallback manual
    console.log('Provider:', info.engine, info.status, info.reason);
    if (info.status === 'unverified') {
      // Jangan anggap preview berhasil di database hanya karena iframe onload.
    }
  },
  onProviderChange: function (info) {
    console.log('Berpindah ke:', info.engine);
  },
  onPreviewError: function (pesan) {
    console.error('Gagal preview:', pesan);
  },
  bukaUlang: function () {
    this.$refs.viewer.open();
  }
}
```


### Panduan memastikan preview tidak salah dianggap sukses (v1.2)

| Kejadian | Event | Makna | Perpindahan |
|---|---|---|---|
| Iframe Microsoft mulai dibuka | `provider-status: loading` | Menunggu iframe | Belum |
| `iframe.onload` Microsoft | `provider-status: unverified` | **Halaman iframe termuat; dokumen belum bisa diverifikasi** | Tidak otomatis |
| Tidak ada `load` hingga `office-timeout` | `provider-status: failed`, `reason: iframe-timeout` | Timeout memuat iframe | Otomatis ke Google |
| `iframe.onerror` (jika browser mengirim) | `provider-status: failed`, `reason: iframe-error` | Error iframe | Otomatis ke berikutnya |
| Pengguna klik Coba Google Docs | `provider-status: skipped`, `reason: manual-next` | Memilih provider selanjutnya | Manual |
| Render lokal selesai | `loaded` | Renderer lokal sudah menyelesaikan proses | — |

**Mengapa tidak fallback otomatis saat iframe `onload` tetapi error internal?** Karena browser melarang aplikasi membaca isi iframe provider lintas domain. Mengatur timeout render setelah `onload` akan memindahkan dokumen yang sebenarnya berhasil dilihat. Jika benar-benar perlu verifikasi deterministik, gunakan layanan konversi backend (misalnya DOCX/XLSX ke PDF) dan periksa sukses/gagal hasil konversinya. Pemeriksaan HTTP `200` pada URL file oleh backend pun **tidak membuktikan** dokumen berhasil dirender oleh Microsoft/Google.

### Method melalui `$refs.viewer`

| Method | Fungsi |
|---|---|
| `open()` | Memuat ulang file |
| `tryNextPreview()` | Lanjut Microsoft → Google → lokal sesuai urutan yang diaktifkan |
| `useGoogle()` | Pilih Google Docs Viewer (jika URL/konfigurasi memenuhi syarat) |
| `useLocal()` | Pindah ke viewer lokal (DOCX/Excel yang didukung) |
| `goPage(delta)` | Navigasi halaman PDF; misalnya `goPage(1)` |
| `changeZoom(delta)` | Ubah zoom PDF/gambar; misalnya `changeZoom(0.2)` |
| `toggleFullscreen()` | Masuk/keluar fullscreen |
| `download()` | Unduh file jika `allow-download=true` dan `locked=false` |

Gunakan ref setelah komponen terpasang/mounted dan belum dihancurkan.

## 8. CDN dan penggunaan di intranet

Dependensi format tertentu dimuat **lazy-load**: PDF.js untuk PDF, JSZip + docx-preview untuk DOCX, serta SheetJS untuk Excel. Paket inti berjalan tanpa npm tetapi **browser tetap membutuhkan koneksi ke URL dependensi** pada saat preview, kecuali library di-host sendiri.

Agar bekerja pada jaringan intranet/air-gapped, host masing-masing library di server dan konfigurasi **sebelum viewer dimount**:

```html
<script src="/assets/plugins/document-viewer/document-viewer.vue2.js"></script>
<script>
window.DocumentViewer.configure({
  pdf: '/assets/libs/pdfjs/pdf.min.js',
  pdfWorker: '/assets/libs/pdfjs/pdf.worker.min.js',
  jszip: '/assets/libs/jszip/jszip.min.js',
  docx: '/assets/libs/docx-preview/docx-preview.min.js',
  xlsx: '/assets/libs/xlsx/xlsx.full.min.js'
});
</script>
```

Gunakan file `.vue3.js` sebagai gantinya untuk Vue 3. **Jangan lupa** PDF.js worker dan library yang dipasang harus kompatibel dengan versi di JS viewer. Dependensi bukan dibundel dalam ZIP ini. Verifikasi lisensi dependensi sebelum mendistribusikan dalam produk komersial.

Jika aplikasi menggunakan Content Security Policy (CSP), beri izin script dan worker untuk alamat yang dipakai; bila preview Microsoft/Google diaktifkan, izin `frame-src` untuk **kedua** `https://view.officeapps.live.com` dan `https://docs.google.com` juga diperlukan.

## 9. Keamanan dokumen dan PDF locked

Komponen menyediakan **pengaturan antarmuka**, bukan sistem DRM atau proteksi anti-salin absolut.

- `:locked="true"` atau `:allow-download="false"` menghapus tombol download di komponen; `locked=true` menahan menu klik-kanan pada preview PDF.
- Pengguna dengan akses browser tetap berpotensi mengambil data file dari Network/DevTools, cache, atau screenshot. Jangan menyebutnya anti-download 100%.
- Dokumen rahasia sebaiknya melalui endpoint server yang memeriksa sesi, role, kepemilikan dokumen, dan izin preview pada **setiap request**.
- Gunakan HTTPS, URL berumur pendek jika diperlukan, audit log, watermark identitas pengguna bila relevan, dan kebijakan caching server.
- **Jangan** memasukkan file privat ke layanan Microsoft maupun Google; gunakan **keduanya** `:prefer-microsoft="false" :prefer-google="false"` untuk preview lokal.
- File yang masuk ke layanan Microsoft/Google harus bisa diambil oleh server mereka; URL signed/presigned ikut dikirim ke penyedia. Evaluasi kebijakan klasifikasi dan privasi data organisasi sebelum mengaktifkannya.

## 10. Troubleshooting

| Gejala | Penyebab kemungkinan | Solusi |
|---|---|---|
| Komponen tidak muncul | Vue belum dimuat / salah JS Vue | Muat Vue dulu lalu `document-viewer.vue2.js` atau `.vue3.js` sesuai versi |
| Vue 3 menampilkan unknown component | Belum registrasi | Panggil `app.use(window.DocumentViewer)` sebelum `app.mount(...)` |
| Template tidak bekerja | Vue build runtime-only | Gunakan full build berisi template compiler |
| Muncul "Format file tidak didukung" | URL API tanpa ekstensi atau ekstensi salah | Isi prop `filename="dokumen.pdf"` / `filename="laporan.xlsx"` |
| HTTP 401/403 | Endpoint membutuhkan autentikasi | Periksa token, cookies, permission pengguna, dan endpoint |
| CORS error | API/file lintas origin | Konfigurasi `Access-Control-Allow-Origin` di host file, atau di demo pakai `/api/proxy` (`npm start`) untuk preview lokal |
| Choose File tidak ke Microsoft | `File`/`Blob` bukan URL publik | Di demo: **Upload** (`/api/upload` → `/uploads/`) lalu pilih Microsoft di header; di app host: unggah ke storage lalu set `src` ke URL HTTPS |
| Folder `/uploads/` penuh / uji ulang | File sementara menumpuk / kuota | Di demo: **Kosongkan folder uploads** (`POST /api/flush-uploads`) |
| Upload / flush 403 di host publik | Same-origin / upload dimatikan | Buka dari origin yang sama; cek `DEMO_UPLOAD_ENABLED` |
| PDF.js / DOCX / XLSX library gagal dimuat | CDN diblok atau offline | Host library secara lokal, lalu `DocumentViewer.configure(...)` |
| Microsoft blank / error | File tidak dapat dijangkau Microsoft atau iframe menampilkan error internal | Klik **Coba Google Docs**, kemudian **Preview lokal** bila perlu |
| Google Docs blank / "No preview available" | URL tidak bisa diambil Google, format/ukuran tidak cocok, atau error internal | Klik **Preview lokal**, atau langsung paksa lokal memakai kedua preferensi `false` |
| Google Docs tidak ditampilkan | URL berupa File/Blob, localhost/intranet, atau memiliki header autentikasi | Gunakan lokal atau sediakan direct URL yang benar-benar publik (untuk nonrahasia) |
| Excel hanya sebagian baris/kolom | Batas performa | Naikkan `:max-rows` / `:max-columns` seperlunya |
| Logo/gambar di header DOCX tidak muncul | Gambar floating (`wrapNone`) + CSS `max-width:100%` host | Pastikan memakai `dist` ≥ v1.3.4; hard-refresh demo; uji sampel **Kop surat** |
| Kop surat menutupi shadow lembar | Offset negatif keluar dari page card | Pastikan `dist` ≥ v1.3.5 (`overflow` lembar + nudge logo) |
| Centang Online di demo tidak mengubah preview | Sumber lokal/upload/File bukan HTTPS publik | Normal; lihat status `skipped` / peringatan demo, atau pakai **Coba URL Office publik** |
| Demo masih perilaku lama setelah update | Browser cache JS/CSS `dist` | Hard refresh; demo Vue 2 memakai `?v=` pada aset `dist` |
| `.doc` / `.pptx` privat tidak bisa dipreview | Tidak ada fallback lokal untuk format ini | Konversi di backend ke PDF, DOCX, atau format yang didukung |
| PDF masih bisa disimpan melalui DevTools | Pembatasan teknis browser | Terapkan otorisasi backend, URL terbatas, watermark, audit akses |

## 11. Menjalankan demo dan pengujian

**Demo Vue 2 tersedia lengkap di [`examples/vue2.html`](examples/vue2.html).** Panduan khususnya: [`examples/DEMO_VUE2.md`](examples/DEMO_VUE2.md). Paket sudah menyertakan **kop surat (logo header)**, DOCX sederhana, PDF 2 halaman, XLSX 2 sheet, PNG, CSV, dan JSON.

**Demo publik:** [https://file-preview.inovasiapp.id/](https://file-preview.inovasiapp.id/) · [Vue 2](https://file-preview.inovasiapp.id/examples/vue2.html) · [Vue 3](https://file-preview.inovasiapp.id/examples/vue3.html)

```bash
# Buka terminal pada root package (Node.js ≥18)
npm start
# PORT=8080 npm start
```

Buka **http://localhost:8080/examples/vue2.html** untuk Vue 2, atau **http://localhost:8080/examples/vue3.html** untuk Vue 3. Server Node menyajikan static + API demo (`/api/proxy`, `/api/upload`, `/api/flush-uploads`). Upload dibatasi (same-origin, rate-limit, 5MB, TTL ~2 jam). Demo memakai **upload file** / URL, radio `viewer-default` di header, tombol **Coba URL Office publik**, PDF locked, download, fit DOCX, watermark, log event, dan salin kode. File `examples/contoh-*` tetap tersedia untuk di-upload manual saat uji.

**Gunakan HTTP**, bukan `file://`, karena file contoh dibaca melalui `fetch()`; `file://` dapat diblokir browser. Setelah memperbarui paket, **hard-refresh** agar `dist/document-viewer.vue2.js` / `.css` terbaru terpakai (demo memakai cache-bust `?v=`). Sampel di `localhost` langsung dirender **lokal**: untuk menguji provider Microsoft/Google diperlukan URL file **HTTPS publik**.

```bash
# Opsional: jalankan smoke test pada komponen Vue 2 dan Vue 3
node --test tests/smoke.test.cjs
```

Smoke test dan validasi aset tidak menggantikan uji visual di browser menggunakan CDN/vendor sungguhan. Jika `iframe` Microsoft/Google termuat tetapi berisi pesan error, statusnya tetap **belum terverifikasi** dan pengguna dapat memilih fallback manual.
