Upload file

Hubungkan pemilihan file, drag and drop, dan operasi paste yang hanya berisi file ke alur upload. Demo di halaman ini tidak mengirim file ke server; pada layanan nyata, Anda harus menghubungkan uploader yang menerima file dan mengembalikan URL.

Untuk memasukkan hasil upload sebagai blok gambar, Anda memerlukan wing gambar. Untuk memasukkan file lain sebagai tautan lampiran, Anda memerlukan wing tautan. Jika layanan Anda menerima kedua format tersebut, pilih kedua wing secara eksplisit. Saat upload berlangsung, editor dikunci dan file yang berhasil disisipkan bersama sebagai satu langkah undo.

Sayap
Jenis huruf bawaanJarak
HTMLMemuat editor…
JSONnabi-treeMemuat editor…
Pasang
npm install nabi-note
Kode
import {
  createNabiWith, mountSurface, mountToolbar, mountContextToolbar,
  mountHints, watchSettle, boldWing, italicWing, underlineWing,
  strikeWing, linkWing, imageWing, uploadWing, clearFormatWing,
  mountUpload, mountUploadView, mountViewTools,
} from 'nabi-note'

const selected = [
  boldWing,
  italicWing,
  underlineWing,
  strikeWing,
  linkWing,
  imageWing,
  uploadWing,
  clearFormatWing,
]
const { nabi, registry } = createNabiWith(selected)

const root = document.querySelector('.nabi')!
const content = document.querySelector('.nabi-content')!

const view = mountUploadView({ nabi, surface: content })
const upload = mountUpload({
  nabi, root: content,
  // your upload goes here — report progress with task.onProgress(0–100)
  uploader: async (task) => ({ uri: 'https://cdn.example/uploaded' }),
  extensions: ['png', 'jpg', 'pdf'], maxFileSize: 10 * 1024 * 1024,
  onStart: (tasks) => view.start(tasks),
  onProgress: (id, percent) => view.progress(id, percent),
  onSettle: () => view.settle(),
  onDone: () => view.done(),
})

// The locale also sets the direction — Arabic and Urdu run right to left
mountSurface({ nabi, registry, root: content, locale: 'id', fileSink: upload.take })

const settle = watchSettle(document, { surface: content })
const shared = { nabi, registry, surface: content, settle, locale: 'id' }
const toolbar = mountToolbar({ ...shared, root: document.querySelector('#toolbar')!, onFiles: upload.take })
const context = mountContextToolbar({ ...shared, root: document.querySelector('#context')! })
mountHints({ toolbar, context, root, surface: content })
// The preview and fullscreen buttons — they stand their own box at the end of the row
mountViewTools({ ...shared, root, container: document.querySelector('#toolbar')! })

// on every change — hook up your own code here
// nabi.onChange(() => user_callback(nabi.getHtml()))
ts
const selected = wings()
  .use('img')
  .use('a')
  .use('upload', { allowLocalUrls: false })
  .build()

Jika Anda hanya memilih upload, paket ini secara otomatis melengkapi salah satu dependensi gambar atau tautan yang belum ada. Transfer dihubungkan dengan mountUpload(), dan UI progres pada layar pengeditan biasanya dihubungkan dengan mountUploadView(). Jika server mengembalikan URL HTTPS, opsi URL lokal tidak diperlukan.

Kontrak API server

NABI NOTE tidak mengirim file ke server Anda dengan sendirinya. Fungsi uploader mengirim satu file ke server, lalu jika berhasil hanya mengembalikan URL https: publik atau yang diautentikasi. Kontrak API paling sederhana tampak seperti ini.

text
POST /api/uploads
Content-Type: multipart/form-data
Field name: file

Success: { "url": "https://cdn.example.com/uploads/8f2c.webp" }
Failure: 4xx or 5xx response

Server tidak boleh hanya mempercayai nama file asli, ekstensi, atau nilai MIME yang dikirim browser. Periksa autentikasi dan izin terlebih dahulu, batasi ukuran file selama streaming, lalu periksa jenis file sebenarnya. Buat nama penyimpanan di server. Untuk gambar, lakukan re-encode atau buat thumbnail bila diperlukan. Jika file yang diupload tidak boleh dapat diunduh oleh semua orang, kembalikan jalur unduhan yang memerlukan autentikasi, bukan URL publik.

Yang diperiksa di serverAlasan
Pengguna yang masuk dan izin uploadMencegah penulisan ke penyimpanan pengguna lain
Ukuran per file dan ukuran total permintaanMencegah kehabisan memori dan penyimpanan
Jenis MIME dan ekstensi asli yang diizinkanMemblokir file eksekusi dengan ekstensi yang diganti
Nama penyimpanan acak dan penyimpanan terisolasiMencegah manipulasi path dan penimpaan file yang ada
Akses URL respons dan kebijakan kedaluwarsaMencegah file privat terekspos hanya lewat URL

extensions dan maxFileSize di sisi klien hanyalah langkah awal untuk memberi umpan balik cepat kepada pengguna. Terapkan batas yang sama di server juga.

Menghubungkan uploader di browser

Contoh di bawah ini adalah integrasi nyata yang diharapkan NABI NOTE. Contoh memakai XMLHttpRequest karena fetch() standar browser tidak menyediakan progres upload. Kembalikan hanya url dari respons server; gambar menjadi blok gambar dan file lainnya menjadi tautan lampiran.

ts
import {
  createNabiWith,
  mountSurface,
  mountUpload,
  mountUploadView,
  wings,
  type UploadTask,
} from 'nabi-note'

const content = document.querySelector<HTMLElement>('#content')!
const { nabi, registry } = createNabiWith(
  wings().use('img').use('a').use('upload').build(),
  { locale: 'en' },
)

function sendUpload(task: UploadTask): Promise<{ uri: string } | null> {
  return new Promise((resolve, reject) => {
    const request = new XMLHttpRequest()
    request.open('POST', '/api/uploads')
    request.responseType = 'json'

    request.upload.addEventListener('progress', (event) => {
      if (event.lengthComputable) task.onProgress((event.loaded / event.total) * 100)
    })

    request.addEventListener('load', () => {
      const url = request.response?.url
      if (request.status >= 200 && request.status < 300 && typeof url === 'string') {
        resolve({ uri: url })
      } else {
        resolve(null)
      }
    })
    request.addEventListener('error', () => reject(new Error('Upload request failed.')))
    task.signal.addEventListener('abort', () => request.abort(), { once: true })

    const body = new FormData()
    body.append('file', task.file as File, task.name)
    request.send(body)
  })
}

let uploadView: ReturnType<typeof mountUploadView>
const upload = mountUpload({
  nabi,
  root: content,
  uploader: sendUpload,
  extensions: ['png', 'jpg', 'jpeg', 'webp', 'pdf'],
  maxFileSize: 10 * 1024 * 1024,
  maxTotalSize: 20 * 1024 * 1024,
  locale: 'en',
  onStart: (tasks) => uploadView.start(tasks),
  onProgress: (id, percent) => uploadView.progress(id, percent),
  onSettle: () => uploadView.settle(),
  onDone: () => uploadView.done(),
})

uploadView = mountUploadView({ nabi, surface: content, upload, locale: 'en' })

const surface = mountSurface({
  nabi,
  registry,
  root: content,
  fileSink: upload.take,
  locale: 'en',
})

Hubungkan fileSink: upload.take agar drag and drop, serta operasi paste yang hanya berisi file, masuk ke alur upload. UI wing upload meneruskan hasil tombol pemilihan file ke upload.take(). Saat upload berlangsung, editor dikunci dan file yang berhasil dari setiap batch disisipkan sebagai satu langkah undo. upload.cancel() atau tombol batal di uploadView membatalkan permintaan yang sedang berjalan melalui AbortSignal.

Kegagalan dan pembersihan

Jika server mengembalikan respons error atau uploader mengembalikan null, file tersebut tidak disisipkan ke dokumen. File lain dalam batch yang sama tetap diproses. Jika batas ukuran total terlampaui, seluruh batch tidak dimulai. Saat menutup layar, unmount dalam urutan kebalikan dari pembuatan.

ts
function dispose() {
  surface.unmount()
  uploadView.unmount()
  upload.unmount()
}

Hanya selama pengembangan, Anda dapat menggunakan URL blob: untuk pratinjau langsung. Dalam hal ini, aktifkan allowLocalUrls: true pada perakitan editor, wing gambar, dan wing upload. Jika upload server sungguhan mengembalikan URL HTTPS, lebih aman untuk tidak mengaktifkan opsi ini.

Gaya CSS

File biasa yang selesai diupload ditampilkan oleh wing tautan sebagai a[data-nabi-file]. Gunakan selector ini jika Anda hanya ingin mengubah tampilan lampiran pada tampilan terbitan.

css
.article-body a[data-nabi-file] {
  padding: .3em .6em;
  border: 1px solid var(--nabi-line);
  border-radius: 8px;
  background: var(--nabi-soft);
}

Hasil upload gambar mengikuti CSS wing gambar. Progres upload hanya muncul di tampilan pengeditan, jadi tampilan terbitan tidak perlu membuat status progres.