Datei-Upload ​

Verbinden Sie Dateiauswahl, Drag & Drop und Einfügeoperationen, die nur Dateien enthalten, mit einem Upload-Vorgang. Die Demo auf dieser Seite sendet keine Dateien an einen Server; in einem echten Dienst müssen Sie einen Uploader verbinden, der eine Datei empfängt und eine URL zurückgibt.

Um Upload-Ergebnisse als Bildblöcke einzufügen, benötigen Sie die Image-Wing. Um andere Dateien als Anhangslinks einzufügen, benötigen Sie die Link-Wing. Wenn Ihr Dienst beide Formate akzeptiert, wählen Sie beide Wings explizit aus. Während des Uploads ist der Editor gesperrt, und erfolgreiche Dateien werden gemeinsam als ein Undo-Schritt eingefügt.

Flügel
StandardschriftartAbstand
HTMLEditor wird geladen…
JSONnabi-treeEditor wird geladen…
Installation
npm install nabi-note
Code
import {
  createLocale, 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 locale = createLocale('de')
const { nabi, registry } = createNabiWith(selected, { locale })

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

const view = mountUploadView({ nabi, surface: content, locale })
const upload = mountUpload({
  nabi, root: content, locale,
  // 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, fileSink: upload.take })

const settle = watchSettle(document, { surface: content })
const shared = { nabi, registry, surface: content, settle, locale }
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()))

// Call from your language selector. Content and editing history stay intact.
// locale.setLocale('ko')
ts
const selected = wings()
  .use('img')
  .use('a')
  .use('upload', { allowLocalUrls: false })
  .build()

Wenn Sie nur upload auswählen, wird automatisch die jeweils fehlende Abhängigkeit von Image oder Link bereitgestellt. Die Übertragung wird mit mountUpload() verbunden, und die Fortschritts-UI des Bearbeitungsbildschirms wird normalerweise mit mountUploadView() verbunden. Wenn der Server HTTPS-URLs zurückgibt, ist die lokale URL-Option nicht erforderlich.

Einfügeposition für Uploads ​

Die Position, an der die Dateiauswahl geöffnet oder der Upload gestartet wird, bleibt gespeichert. Wird nur der Cursor versetzt, erfolgt das Einfügen weiterhin an der ursprünglichen Position. Beginnt der Upload in einer Tabellenzelle, bleiben Fortschrittsanzeige und fertiger Inhalt in dieser Zelle. Auch Uploads aus einem Float-Bereich werden dort eingefügt. Eine Auswahl über mehrere Zellen hinweg kann keine Uploads aufnehmen.

Wird das Dokument während der Dateiauswahl oder asynchronen Verarbeitung bearbeitet oder ersetzt, wird das Einfügen mit einem Hinweis abgebrochen. Auch wenn Rückgängig denselben Inhalt wiederherstellt, wird nicht an der alten Position eingefügt. Bewerten Sie den Abschluss nicht allein anhand der erfolgreichen Übertragung: Prüfen Sie committed und cancelled, die onDone erhält. committed zählt die tatsächlich ins Dokument eingefügten Dateien.

Server-API-Vertrag ​

NABI NOTE sendet keine Dateien selbst an Ihren Server. Die Funktion uploader sendet eine Datei an den Server und gibt im Erfolgsfall nur eine öffentliche oder authentifizierte https:-URL zurück. Der einfachste API-Vertrag sieht wie folgt aus.

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

Der Server darf dem ursprünglichen Dateinamen, der Erweiterung oder dem MIME-Wert, die vom Browser gesendet werden, nicht blind vertrauen. Prüfen Sie zuerst Authentifizierung und Berechtigung, begrenzen Sie die Dateigröße während des Streamings und untersuchen Sie den tatsächlichen Dateityp. Erzeugen Sie den gespeicherten Namen auf dem Server. Kodieren Sie Bilder bei Bedarf neu oder erstellen Sie Thumbnails. Wenn hochgeladene Dateien nicht für alle herunterladbar sein sollen, geben Sie statt einer öffentlichen URL einen Download-Pfad zurück, der Authentifizierung erfordert.

Prüfung auf dem ServerWarum
Angemeldeter Benutzer und Upload-BerechtigungVerhindert das Schreiben in den Speicher eines anderen Benutzers
Größe pro Datei und Gesamtgröße der AnfrageVerhindert das Erschöpfen von Arbeitsspeicher und Speicherplatz
Erlaubter tatsächlicher MIME-Typ und ErweiterungBlockiert ausführbare Dateien mit umbenannten Erweiterungen
Zufälliger gespeicherter Name und isolierter SpeicherVerhindert Pfadmanipulation und Überschreiben bestehender Dateien
Zugriff auf die Antwort-URL und AblaufrichtlinieVerhindert, dass private Dateien allein durch die URL offengelegt werden

Clientseitige extensions und maxFileSize sind nur der erste Schritt für schnelles Feedback an den Benutzer. Setzen Sie dieselben Grenzen auch auf dem Server.

Uploader im Browser verbinden ​

Das folgende Beispiel ist die echte Verbindung, die NABI NOTE erwartet. Es verwendet XMLHttpRequest, da das Standard-fetch() des Browsers keinen Upload-Fortschritt bereitstellt. Geben Sie nur die url aus der Serverantwort zurück; Bilder werden zu Bildblöcken, andere Dateien werden zu Anhangslinks.

ts
import {
  createNabiWith,
  mountSurface,
  mountToolbar,
  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: 'de' },
)

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: 'de',
  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: 'de' })

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

const toolbar = mountToolbar({
  nabi,
  registry,
  root: document.querySelector<HTMLElement>('#toolbar')!,
  surface: content,
  onFiles: upload.take,
  locale: 'de',
})

Verbinden Sie fileSink: upload.take, damit Drag & Drop und das Einfügen ausschließlich von Dateien den Upload auslösen. Verbinden Sie die Dateiauswahl über onFiles: upload.take an der Werkzeugleiste. Während des Uploads ist der Editor gesperrt; erfolgreiche Dateien werden pro Stapel in einem Rückgängig-Schritt eingefügt. upload.cancel() oder die Abbrechen-Schaltfläche in uploadView beendet laufende Anfragen über AbortSignal.

Fehlerbereinigung ​

Wenn der Server eine Fehlerantwort zurückgibt oder uploader null zurückgibt, wird diese Datei nicht in das Dokument eingefügt. Andere Dateien im selben Batch werden weiterhin verarbeitet. Wenn das Gesamtgrößenlimit überschritten wird, startet der gesamte Batch nicht. Beim Schließen des Bildschirms unmounten Sie in umgekehrter Reihenfolge der Erstellung.

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

Nur während der Entwicklung können Sie blob:-URLs für eine sofortige Vorschau verwenden. Aktivieren Sie in diesem Fall allowLocalUrls: true in der Editor-Zusammenstellung, in der Image-Wing und in der Upload-Wing. Wenn echte Server-Uploads HTTPS-URLs zurückgeben, ist es sicherer, diese Option nicht zu aktivieren.

CSS-Stile ​

Abgeschlossene normale Dateien werden über die Link-Wing als a[data-nabi-file] angezeigt. Verwenden Sie diesen Selektor, wenn Sie nur das Erscheinungsbild des Anhangs in der veröffentlichten Ansicht ändern möchten.

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

Bild-Upload-Ergebnisse folgen dem CSS des Image-Wings. Der Upload-Fortschritt erscheint nur in der Bearbeitungsansicht, sodass die CSS der veröffentlichten Ansicht keinen Fortschrittszustand erstellen muss.