Dosya yükleme
Dosya seçimini, sürükle-bırakı ve yalnızca dosya içeren yapıştırma işlemlerini bir yükleme akışına bağlayın. Bu sayfadaki demo dosyaları sunucuya göndermez; gerçek bir hizmette, dosyayı alıp URL döndüren bir yükleyici bağlamanız gerekir.
Yükleme sonuçlarını görsel blokları olarak eklemek için görsel wing'ine, diğer dosyaları ek bağlantıları olarak eklemek için bağlantı wing'ine ihtiyacınız vardır. Hizmetiniz her iki biçimi de kabul ediyorsa iki wing'i de açıkça seçin. Yükleme sürerken düzenleyici kilitlenir ve başarılı dosyalar tek bir geri alma adımı olarak birlikte eklenir.
npm install nabi-noteimport {
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('tr')
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')const selected = wings()
.use('img')
.use('a')
.use('upload', { allowLocalUrls: false })
.build()Yalnızca upload seçerseniz, görsel veya bağlantı bağımlılıklarından eksik olanı otomatik olarak ekler. Aktarım mountUpload() ile, düzenleme ekranındaki ilerleme arayüzü ise genellikle mountUploadView() ile bağlanır. Sunucu HTTPS URL'leri döndürüyorsa yerel URL seçeneğine gerek yoktur.
Yüklemelerin ekleneceği konum
Dosya seçicinin açıldığı veya yüklemenin başlatıldığı konum hatırlanır. Yalnızca imleç taşınırsa ilk konuma eklenir. Yükleme bir tablo hücresinde başladıysa ilerleme göstergesi ve tamamlanan içerik o hücrede kalır. Yüzen alanda başlayan yüklemelerin sonuçları da aynı alana eklenir. Birden çok hücrenin sınırlarını aşan seçime yükleme eklenemez.
Dosya seçimi veya asenkron işlem sırasında belgeyi düzenlemek ya da değiştirmek eklemeyi iptal eder ve bildirim gösterir. Geri alma aynı içeriği getirse bile eski konuma ekleme yapılmaz. Tamamlanmayı yalnızca aktarım başarısına göre değerlendirmeyin: onDone tarafından alınan committed ve cancelled değerlerini kontrol edin. committed, belgeye gerçekten eklenen dosya sayısıdır.
Sunucu API sözleşmesi
NABI NOTE dosyaları kendiliğinden sunucunuza göndermez. uploader işlevi tek bir dosyayı sunucuya yollar ve başarılı olduğunda yalnızca herkese açık veya kimlik doğrulamalı bir https: URL'si döndürür. En basit API sözleşmesi şöyledir.
POST /api/uploads
Content-Type: multipart/form-data
Alan adı: file
Başarı: { "url": "https://cdn.example.com/uploads/8f2c.webp" }
Başarısızlık: 4xx veya 5xx yanıtıSunucu yalnızca özgün dosya adına, uzantıya veya tarayıcının gönderdiği MIME değerine güvenmemelidir. Önce kimlik doğrulama ve izinleri denetleyin, akış sırasında dosya boyutunu sınırlayın ve gerçek dosya türünü inceleyin. Saklanan adı sunucuda oluşturun. Görseller için gerektiğinde yeniden kodlama yapın veya küçük resimler oluşturun. Yüklenen dosyalar herkes tarafından indirilememeliyse herkese açık URL yerine kimlik doğrulaması gerektiren bir indirme yolu döndürün.
| Sunucuda denetlenecekler | Neden |
|---|---|
| Oturum açmış kullanıcı ve yükleme izni | Başka kullanıcının depolama alanına yazılmasını önler |
| Dosya başına boyut ve toplam istek boyutu | Bellek ve depolama alanının tükenmesini önler |
| İzin verilen gerçek MIME türü ve uzantı | Uzantısı değiştirilmiş çalıştırılabilir dosyaları engeller |
| Rastgele saklama adı ve yalıtılmış depolama | Yol manipülasyonunu ve mevcut dosyaların üzerine yazılmasını önler |
| Yanıt URL'sinin erişim ve son kullanma ilkesi | Özel dosyaların yalnızca URL ile açığa çıkmasını önler |
İstemci tarafındaki extensions ve maxFileSize, kullanıcıya hızlı geri bildirim vermenin yalnızca ilk adımıdır. Aynı sınırları sunucuya da koyun.
Tarayıcıda yükleyiciyi bağlama
Aşağıdaki örnek, NABI NOTE'un beklediği gerçek bağlantıdır. Tarayıcının standart fetch() işlevi yükleme ilerlemesi sağlamadığından XMLHttpRequest kullanır. Sunucu yanıtından yalnızca url değerini döndürün; görseller görsel bloklarına, diğer dosyalar ek bağlantılarına dönüşür.
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: 'tr' },
)
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('Yükleme isteği başarısız oldu.')))
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: 'tr',
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: 'tr' })
const surface = mountSurface({
nabi,
registry,
root: content,
fileSink: upload.take,
locale: 'tr',
})
const toolbar = mountToolbar({
nabi,
registry,
root: document.querySelector<HTMLElement>('#toolbar')!,
surface: content,
onFiles: upload.take,
locale: 'tr',
})Sürükle-bırak ve yalnızca dosya yapıştırma işlemlerinin yüklemeye girmesi için fileSink: upload.take bağlantısını kurun. Dosya seçme düğmesini araç çubuğunun onFiles: upload.take seçeneğiyle bağlayın. Yükleme sırasında düzenleyici kilitlenir ve başarılı dosyalar her toplu iş için tek geri alma adımıyla eklenir. upload.cancel() veya uploadView içindeki iptal düğmesi devam eden istekleri AbortSignal aracılığıyla durdurur.
Başarısızlık ve temizleme
Sunucu hata yanıtı verirse veya uploader null döndürürse o dosya belgeye eklenmez. Aynı toplu işteki diğer dosyalar işlenmeye devam eder. Toplam boyut sınırı aşılırsa toplu işin tamamı başlamaz. Ekranı kapatırken, oluşturma sırasının tersine göre kaldırın.
function dispose() {
toolbar.unmount()
surface.unmount()
uploadView.unmount()
upload.unmount()
}Yalnızca geliştirme sırasında anlık önizleme için blob: URL'leri kullanabilirsiniz. Bu durumda düzenleyici kurulumunda, görsel wing'inde ve yükleme wing'inde allowLocalUrls: true seçeneğini açın. Gerçek sunucu yüklemeleri HTTPS URL'leri döndürüyorsa bu seçeneği etkinleştirmemek daha güvenlidir.
CSS stilleri
Tamamlanan normal dosyalar, bağlantı wing'i aracılığıyla a[data-nabi-file] olarak gösterilir. Yayımlanan görünümde yalnızca ek görünümünü değiştirmek istiyorsanız bu seçiciyi kullanın.
.article-body a[data-nabi-file] {
padding: .3em .6em;
border: 1px solid var(--nabi-line);
border-radius: 8px;
background: var(--nabi-soft);
}Görsel yükleme sonuçları görsel wing'inin CSS'ini izler. Yükleme ilerlemesi yalnızca düzenleme görünümünde gösterilir; bu nedenle yayımlanan görünüm CSS'inin bir ilerleme durumu oluşturmasına gerek yoktur.