بارگذاری فایل ​

انتخاب فایل، drag and drop و عملیات paste که تنها شامل فایل هستند را به جریان بارگذاری متصل کنید. دموی این صفحه فایلی به سرور نمی‌فرستد؛ در سرویس واقعی باید uploaderی متصل کنید که فایل را دریافت و URL برگرداند.

برای واردکردن نتیجه‌های بارگذاری به‌شکل بلوک تصویر، wing تصویر لازم است. برای واردکردن فایل‌های دیگر به‌شکل پیوند پیوست، wing پیوند لازم است. اگر سرویس شما هر دو قالب را می‌پذیرد، هر دو wing را صریحاً انتخاب کنید. هنگام بارگذاری، ویرایشگر قفل است و فایل‌های موفق با هم در یک گام undo وارد می‌شوند.

بال‌ها
قلم پایهفاصله
HTMLویرایشگر در حال بارگیری است…
JSONnabi-treeویرایشگر در حال بارگیری است…
نصب
npm install nabi-note
کد
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('fa')
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()

اگر فقط upload را انتخاب کنید، وابستگی گم‌شدهٔ تصویر یا پیوند را خودکار فراهم می‌کند. انتقال با mountUpload() متصل می‌شود و UI پیشرفت در صفحهٔ ویرایش معمولاً با mountUploadView() متصل می‌شود. اگر سرور URLهای HTTPS برگرداند، گزینهٔ URL محلی لازم نیست.

محل درج بارگذاری‌ها ​

محلی که انتخابگر فایل باز یا بارگذاری شروع شده است به خاطر سپرده می‌شود. اگر فقط مکان‌نما جابه‌جا شود، درج در همان محل اولیه انجام می‌شود. اگر بارگذاری در سلول شروع شود، نشانگر پیشرفت و محتوای نهایی در همان سلول می‌مانند. نتیجهٔ بارگذاری از ناحیهٔ شناور نیز در همان ناحیه درج می‌شود. درج بارگذاری در انتخابی که از مرز چند سلول عبور کند ممکن نیست.

ویرایش یا جایگزینی سند هنگام انتخاب فایل یا پردازش ناهمگام، درج را لغو کرده و پیام نشان می‌دهد. بازگرداندن همان محتوا با واگرد هم درج در محل قبلی را بازنمی‌گرداند. تکمیل را فقط بر اساس موفقیت انتقال نسنجید: committed و cancelled دریافتی در onDone را بررسی کنید. committed تعداد فایل‌هایی است که واقعاً در سند درج شده‌اند.

قرارداد API سرور ​

NABI NOTE به‌خودی‌خود فایل‌ها را به سرور شما نمی‌فرستد. تابع uploader یک فایل را به سرور می‌فرستد و در موفقیت فقط یک URL عمومی یا دارای احراز هویت https: برمی‌گرداند. ساده‌ترین قرارداد API چنین است.

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

سرور نباید تنها به نام اصلی فایل، پسوند یا مقدار MIME فرستاده‌شده از مرورگر اعتماد کند. ابتدا احراز هویت و مجوز را بررسی کنید، هنگام stream اندازهٔ فایل را محدود و نوع واقعی فایل را وارسی کنید. نام ذخیره‌شده را در سرور بسازید. برای تصویرها در صورت نیاز بازرمزگذاری یا thumbnail بسازید. اگر فایل‌های بارگذاری‌شده نباید برای همه قابل‌دریافت باشند، به‌جای URL عمومی مسیر دانلودِ نیازمند احراز هویت برگردانید.

بررسی در سروردلیل
کاربر واردشده و مجوز بارگذاریاز نوشتن در فضای ذخیره‌سازی کاربر دیگر جلوگیری می‌کند
اندازهٔ هر فایل و اندازهٔ کل درخواستاز فرسودگی حافظه و ذخیره‌سازی جلوگیری می‌کند
نوع MIME واقعی و پسوند مجازفایل‌های اجرایی با پسوند تغییرکرده را مسدود می‌کند
نام ذخیره‌شدهٔ تصادفی و ذخیره‌سازی جدااز دست‌کاری مسیر و بازنویسی فایل موجود جلوگیری می‌کند
سیاست دسترسی و انقضای URL پاسخاز آشکارشدن فایل خصوصی صرفاً با URL جلوگیری می‌کند

extensions و maxFileSize سمت کاربر فقط نخستین گام برای بازخورد سریع به کاربرند. همان محدودیت‌ها را در سرور نیز بگذارید.

اتصال uploader در مرورگر ​

نمونهٔ زیر اتصال واقعیِ مورد انتظار NABI NOTE است. از XMLHttpRequest استفاده می‌کند، زیرا fetch() استاندارد مرورگر پیشرفت بارگذاری را نمی‌دهد. از پاسخ سرور فقط url را برگردانید؛ تصویرها به بلوک تصویر و فایل‌های دیگر به پیوند پیوست تبدیل می‌شوند.

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

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

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

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

fileSink: upload.take را وصل کنید تا کشیدن و رها کردن و چسباندنِ فقط فایل وارد جریان بارگذاری شوند. دکمهٔ انتخاب فایل را با onFiles: upload.take نوار ابزار وصل کنید. هنگام بارگذاری ویرایشگر قفل می‌شود و فایل‌های موفق هر دسته در یک مرحلهٔ واگرد درج می‌شوند. upload.cancel() یا دکمهٔ لغو در uploadView درخواست‌های جاری را از طریق AbortSignal متوقف می‌کند.

خطا و پاک‌سازی ​

اگر سرور پاسخ خطا دهد یا uploader مقدار null برگرداند، آن فایل وارد سند نمی‌شود. پردازش فایل‌های دیگر همان batch ادامه می‌یابد. اگر از محدودیت اندازهٔ کل عبور شود، کل batch آغاز نمی‌شود. هنگام بستن صفحه، با ترتیب معکوس ساخت unmount کنید.

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

تنها هنگام توسعه می‌توانید برای پیش‌نمایش فوری از URLهای blob: استفاده کنید. در این صورت allowLocalUrls: true را در پیکربندی ویرایشگر، wing تصویر و wing بارگذاری روشن کنید. اگر بارگذاری واقعی سرور URLهای HTTPS برمی‌گرداند، فعال نکردن این گزینه امن‌تر است.

سبک‌های CSS ​

فایل‌های عادیِ کامل‌شده از راه wing پیوند به‌شکل a[data-nabi-file] نشان داده می‌شوند. وقتی فقط می‌خواهید ظاهر پیوست را در نمای منتشرشده تغییر دهید از این selector استفاده کنید.

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

نتیجه‌های بارگذاری تصویر از CSS wing تصویر پیروی می‌کنند. پیشرفت بارگذاری فقط در نمای ویرایش ظاهر می‌شود، پس CSS نمای منتشرشده لازم نیست وضعیت پیشرفت بسازد.