رفع الملفات ​

يربط اختيار الملفات والسحب والإفلات واللصق الذي يحتوي ملفات فقط بمسار الرفع. لا يرسل عرض هذه الصفحة الملفات إلى خادم؛ في الخدمة الفعلية يجب ربط uploader يستقبل ملفًا ويعيد URL.

لإدراج النتائج ككتل صور تحتاج image wing، ولإدراج الملفات الأخرى كروابط مرفقات تحتاج link wing. إذا قبلت خدمتك النوعين، فاختر wings كلتيهما صراحة. يُقفل المحرر أثناء الرفع، وتُدرج الملفات الناجحة معًا كخطوة تراجع واحدة.

الأجنحة
نوع الخط الافتراضيالارتفاع
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('ar')
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 وحدها، فهي توفر تلقائيًا اعتماد image أو link الناقص. يُربط النقل بـmountUpload()، وتُربط واجهة تقدم التحرير عادة بـmountUploadView(). إذا أعاد الخادم عناوين HTTPS، فلا حاجة لخيار العناوين المحلية.

موضع إدراج الملفات المرفوعة ​

يُحفظ الموضع الذي فُتح فيه منتقي الملفات أو بدأ فيه الرفع. إذا حُرّك المؤشر فقط، يبقى الإدراج في الموضع الأصلي. إذا بدأ الرفع داخل خلية، يظل مؤشر التقدم والمحتوى المكتمل داخلها. وتُدرج نتائج الرفع الذي بدأ في منطقة عائمة داخل تلك المنطقة أيضًا. لا يمكن إدراج الرفع في تحديد يعبر حدود عدة خلايا.

يؤدي تحرير المستند أو استبداله أثناء اختيار الملفات أو المعالجة غير المتزامنة إلى إلغاء الإدراج وإظهار إشعار. حتى إذا أعاد التراجع المحتوى نفسه، لا يُستأنف الإدراج في الموضع القديم. لا تعتمد على نجاح النقل وحده لتحديد الاكتمال: تحقّق من committed وcancelled اللذين يستقبلهما onDone. يمثّل committed عدد الملفات التي أُدرجت فعليًا في المستند.

عقد API الخادم ​

لا يرسل NABI NOTE الملفات إلى خادمك بنفسه. ترسل دالة uploader ملفًا واحدًا، وتعيد عند النجاح عنوان https: عامًا أو محميًا فقط. أبسط عقد هو:

text
POST /api/uploads
Content-Type: multipart/form-data
اسم الحقل: file

نجاح: { "url": "https://cdn.example.com/uploads/8f2c.webp" }
فشل: استجابة 4xx أو 5xx

يجب ألا يثق الخادم باسم الملف الأصلي أو امتداده أو MIME الذي يرسله المتصفح وحدها. افحص تسجيل الدخول والصلاحية أولًا، وحدد الحجم أثناء القراءة المتدفقة، وافحص النوع الحقيقي. أنشئ اسم التخزين على الخادم. وأعد ترميز الصور أو أنشئ صورًا مصغرة عند الحاجة. إذا لم يكن الملف متاحًا للجميع، فأعد مسار تنزيل يتطلب تسجيل الدخول بدل URL عام.

الفحص على الخادمالسبب
المستخدم المسجل وصلاحية الرفعيمنع الكتابة في مساحة مستخدم آخر
حجم كل ملف والحجم الكلي للطلبيمنع استنزاف الذاكرة والتخزين
MIME الحقيقي والامتداد المسموحانيمنع الملفات التنفيذية ذات الامتداد المغير
اسم تخزين عشوائي ومساحة معزولةيمنع التلاعب بالمسار واستبدال ملف قائم
صلاحية الوصول إلى 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: 'ar' },
)

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

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

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

صِل fileSink: upload.take لتمرير السحب والإفلات واللصق الذي يحتوي ملفات فقط إلى مسار الرفع. صِل زر اختيار الملفات عبر onFiles: upload.take في شريط الأدوات. يُقفل المحرر أثناء الرفع، وتُدرج الملفات الناجحة بخطوة تراجع واحدة لكل دفعة. يلغي upload.cancel() أو زر الإلغاء في uploadView الطلبات الجارية عبر AbortSignal.

الفشل والتنظيف ​

إذا أعاد الخادم خطأ أو أعادت uploader القيمة null، فلا يُدرج ذلك الملف. وتستمر معالجة بقية ملفات الدفعة. وإذا تجاوز الحجم الإجمالي الحد، فلا تبدأ الدفعة كلها. عند إغلاق الشاشة، فك التركيب بعكس ترتيب الإنشاء.

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

في التطوير فقط، يمكن استخدام عناوين blob: للمعاينة الفورية. عندها فعّل allowLocalUrls: true في تجميع المحرر وimage wing وupload wing. إذا كان الرفع الحقيقي يعيد HTTPS، فمن الآمن عدم تمكينه.

أنماط CSS ​

تعرض link wing الملفات العادية المكتملة كـa[data-nabi-file]. استخدم هذا المحدد لتغيير شكل المرفق في صفحة النشر وحدها.

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

تتبع صور الرفع CSS الخاصة بـimage wing. يظهر تقدم الرفع في شاشة التحرير فقط، لذلك لا تحتاج CSS صفحة النشر إلى إنشاء حالة تقدم.