نحوه ساختن ویرایشگر متن در React با استفاده از SlateJS

ترجمه و تالیف : عرفان حشمتی
تاریخ انتشار : 20 دی 99
خواندن در 6 دقیقه
دسته بندی ها : react

ساخت یک ویرایشگر عالی برای برنامه وب مبتنی بر React به هیچ وجه آسان نیست. اما با SlateJS کارها بسیار راحت‌تر می‌شوند. حتی با کمک Slate، ساخت یک ویرایشگر با امکانات کامل بیش از چیزی است که بتوانیم در یک مقاله کوتاه آن را پوشش دهیم، بنابراین ما یک تصویر کلی به شما می‌دهیم و در مقالات بعدی به جزئیات دقیق وارد می‌شویم.

توجه: این مقاله براساس گفتگوی اخیر در نشست جاوااسکریپت NYC نوشته شده است.

SlateJS

ما در حال ساخت Kitemaker هستیم. یک جایگزین جدید، سریع و کاملا مشارکتی برای پیگیری مسائل مانندJira ، Trello و Clubhouse. همچنین معتقد به کارهای از راه دور و به ویژه کار غیرهمزمان هستیم تا اعضای تیم زمان زیادی را برای انجام کارهای بزرگ و بدون وقفه دریافت کنند. نکته اصلی در حمایت از این نوع کارها داشتن یک ویرایشگر بسیار عالی است تا تیم‌ها از همکاری در زمینه مسائل و اطمینان از همسویی برخوردار شوند و فکر می‌کنیم که بهترین ویرایشگر را در اختیار داریم:

میانبرهای علامت گذاری، بلوک‌های کد با برجسته سازی سینتکس، تصاویر، جاسازی طرح‌ها از Figma، عبارات ریاضی با LaTex، نمودارهای MermaidJS و البته شکلک‌ها و اموجی‌های مختلف. همه این کارها کاملا با Slate انجام شده است.

اما چرا در وهله اول تصمیم گرفتیم با Slate پیش برویم؟ این قطعا تنها فریمورک ویرایشگر موجود نیست. مواردی که ما را به سمت Slate سوق داد، در زیر ذکر شده‌اند:

  • در مورد چگونگی ساختار متن محدودیت ندارد و این انعطاف پذیری لازم را به ما می‌دهد.
  • هیچ نوار ابزار داخلی یا تصاویر دیگری را به شما تحمیل نمی‌کند.
  • با در نظر گرفتن React ساخته شده و تفاوت زیادی را در هنگام ارائه اسناد پیچیده ایجاد می‌کند.
  • پایه و اساس ویرایش مشترک را ایجاد می‌کند، چیزی که ما معتقدیم برای Kitemaker اهمیت دارد.
  • فلسفه پشت آن واقعا دوست داشتنی است.
  • یک جامعه پر رونق دارد، به ویژه در Slack.

Slate چگونه متن نوشته‌ها را نمایش می‌دهد

یکی از بهترین قسمت‌های Slate این است که در مورد چگونگی ساختار اسناد کمتر می‌پردازد. این تنها چند مفهوم دارد:

ویرایشگر - محتوای سطح بالای نوشتار شما

عناصر سطح بلوک – بخش‌های خاصی که اسناد شما را تشکیل می‌دهند مانند پاراگراف‌ها، بلوک‌های کد و لیست‌ها. این مواردی است که می‌توانید در سند خود بین آنها خطوط افقی بکشید.

عناصر درون خطی - عناصر خاصی که متناسب با متن سند شما جریان دارند مانند پیوندها.

گره‌های متنی - این شامل متن واقعی موجود در سند است.

علائم - حاشیه نویسی‌هایی که روی متن قرار می‌گیرند مانند علامت گذاری متن به صورت پررنگ یا مورب.

خوانندگان تیزبین متوجه می‌شوند که این ساختار کاملا شبیه DOM است و متن، بلوک و خطوط داخلی Slate کاملا مشابه همتایان خود در DOM هستند.

در اینجا یک تصویر حاوی یک ویرایشگر Slate برای توضیح این مفاهیم شرح داده شده است:

Slate از قالب JSON بسیار ساده برای نمایش اسناد استفاده می‌کند و سند بالا در نمای Slate به این شکل است:

[
  {
    "type": "paragraph",
    "children": [
      {
        "text": "Text with a link "
      },
      {
        "type": "link",
        "url": "https://kitemaker.co",
        "children": [
          {
            "text": "https://kitemaker.co"
          }
        ]
      },
      {
        "text": " here"
      }
    ]
  },
  {
    "type": "paragraph",
    "children": [
      {
        "text": "Text with "
      },
      {
        "text": "bold",
        "bold": true
      },
      {
        "text": " and "
      },
      {
        "text": "italic",
        "italic": true
      },
      {
        "text": " here"
      }
    ]
  }
]

همانطور که قبلا گفتیم Slate واقعا در مورد چگونگی ساختار اسناد اظهارنظر نمیکند. در کد JSON بالا تنها چیزی که Slate به آن توجه دارد این است که آرایه‌ای از عناصر بلوک را با خصوصیت فرزند به دست می‌آورد و اینکه این فرزندان یا سایر عناصر بلوک هستند یا مخلوطی از گره‌های متنی و عناصر درون خطی هستند. Slate به نوع، url یا ویژگی‌های برجسته و همچنین به نحوه ارائه گره‌های مختلف اهمیتی نمی‌دهد. این باعث می‌شود که کار با آن واقعا انعطاف‌پذیر و قدرتمند باشد.

ویرایشگر Hello World

مقدمه چینی کافی است. بیایید چند کد را بررسی کنیم و ببینیم که یک کامپوننت ویرایشگر ساده با استفاده از Slate چگونه به نظر می‌رسد:

function MyEditor() {
  const editor = useMemo(() => withReact(createEditor()), []);
  const [value, setValue] = React.useState<Node[]>([
    {
      children: [{ text: 'Testing' }],
    },
  ]);

  return (
    <Slate editor={editor} value={value} onChange={(v) => setValue(v)}>
      <Editable />
    </Slate>
  );
}

همین کد بالا یک کامپوننت کاملا مفید با Slate است.

  • از ()createEditor برای ایجاد ویرایشگر استفاده می‌کنیم. (فعلا برای ()withReact نگران نباشید - این یک افزونه است که در زیر به آن خواهیم پرداخت)
  • یک آرایه ساده از گره‌ها برای ذخیره سند ایجاد می‌کنیم. (دقیقا همانطور که در بالا دیدید)
  • یک کامپوننت <Slate> می‌سازیم که به عنوان یک ارائه دهنده متن عمل می‌کند. ویرایشگری را که در بالا ایجاد کردیم به تمام اجزای زیر آن ارائه می‌دهیم. این واقعا جالب است زیرا به عنوان مثال می‌توانید در زیر کامپوننت <Slate> نوار ابزار و سایر بخش‌ها را اضافه کنید که می‌تواند متن را گرفته و آن را دستکاری کند. همچنین خصوصیت‌های value و onChange را که شبیه هر ورودی در React است، به هم متصل می‌کنیم.
  • یک کامپوننت <Editable> اضافه می‌کنیم که ویرایشگر واقعی است و کاربر با آن در صفحه تعامل دارد.

چگونه کارها را گسترش دهیم؟

هرچند مثال قبلی پیش پا افتاده بود، اما هنوز فقط یک ویرایشگر ساده داریم که مانند یک <TextArea> کار می‌کند و این خیلی هیجان انگیز نیست.

خوشبختانه Slate مکانیزمی را برای جالب‌تر کردن این موارد فراهم می‌کند:

  • پلاگین‌ها: پلاگین‌ها به ما امکان می‌دهند تا رفتار اصلی ویرایشگر را نادیده بگیریم. آنها مربوط به رندرینگ نیستند، بلکه فقط نشان می‌دهند در هنگام قرار دادن متن یا تصویر چه چیزی اتفاق می‌افتد.
  • کنترل کننده‌های رویداد: این کنترلرها به ما امکان می‌دهند مواردی مانند فشار دادن کلید را کنترل کنیم. بنابراین می‌توانیم کلیدهای میانبر را اضافه کنیم یا اجازه دهیم با فشردن کلید "tab" لیست بولت ایجاد شود.
  • رندر سفارشی با React : با Slate می‌توان دقیقا نحوه رندر هر گره در سند را با استفاده از React مشخص کرد.

بیایید نگاهی سریع به هر یک از اینها بیندازیم.

پلاگین‌ها

پلاگین‌ها یک مفهوم فریبنده ساده و قدرتمند در Slate هستند. به طور کلی پلاگین‌ها چیزی شبیه به این دارند:

export function withMyPlugin(editor: ReactEditor) {
    const { insertText, insertData, normalizeNode, isVoid, isInline } = editor;
  
    // called whenever text is inserted into the document (e.g. when
    // the user types something)
    editor.insertText = (text) => {
      // do something interesting!
      insertText(text);
    };
  
    // called when the users pastes or drags things into the editor
    editor.insertData = (data) => {
      // do something interesting!
      insertData(data);
    };
  
    // we'll dedicate a whole post to this one, but the gist is that it's used
    // to enforce your own custom schema to the document JSON
    editor.normalizeNode = (entry) => {
      // do something interesting!
      normalizeNode(entry);
    };
  
    // tells slate that certain nodes don't have any text content (they're _void_)
    // super handy for stuff like images and diagrams
    editor.isVoid = (element) => {
      if (element.type === 'image') {
        return true;
      }
      return isVoid(element);
    };
  
    // tells slate that certain nodes are inline and should flow with text, like
    // the link in our example above
    editor.isInline = (element) => {
      if (element.type === 'link') {
        return true;
      }
      return isInline(element);
    };
  
    return editor;
  }

طبق قرارداد نام آنها با with شروع می‌شود. آنها یک ویرایشگر Slate را می‌گیرند، هر تابعی را که برای لغو نیاز دارند دریافت می‌کنند و ویرایشگر اصلاح شده را دوباره برمی‌گردانند. اغلب اوقات آنها در برخی از این توابع موارد کمی را کنترل می‌کنند و برای بقیه به حالت پیش فرض برمی‌گردند.

حدود 80٪ اضافه کردن قابلیت‌ها به ویرایشگر Slate، تطبیق رشته در این توابع پلاگین است و سپس با استفاده از API می‌توانید سند را دستکاری کنید.

بیشتر عملکردهای نشان داده شده در بالا را می‌توانید نادیده بگیرید، اما اینها تا کنون معمول ترینها هستند. همچنین می‌توانید همه اطلاعات مربوط به افزونه‌ها را در مستندات Slate مطالعه کنید.

یکی از قدرتمندترین قسمت‌ پلاگین‌های Slate این است که می‌توانند با هم ترکیب شوند. هر پلاگین می‌تواند به دنبال چیزهایی باشد که برای آن مهم است و موارد دیگر را بدون تغییر منتقل کند. سپس می‌توانید چندین افزونه را با هم ترکیب کنید و حتی یک ویرایشگر قدرتمندتر نیز به دست آورید:

function MyEditor() {
  const editor = useMemo(() => withReact(withDragAndDrop(withMarkdownShortcuts(withEmojis(withReact(createEditor())))), []);
  ...
}

کنترل کننده‌های رویداد

مانند بسیاری از کامپوننت‌های ورودی React ، کامپوننت <Editable> نیز دارای تعدادی رویداد است که می‌توانید به آنها گوش دهید. ما نمی‌توانیم همه آنها را در اینجا مرور کنیم، اما یکی را که بیشتر اوقات استفاده می‌کنیم ذکر خواهیم کرد:

()onKeyDown

با مدیریت این رویداد می‌توانیم همه کارهای قدرتمند را در ویرایشگر خود انجام دهیم، مانند اضافه کردن کلیدهای میانبر برای مثال:

<Editable
  onKeyDown={(e) => {
    // let's make the current text bold if the user holds command and hits "b"
    if (e.metaKey && e.key === 'b') {
      e.preventDefault();
      Editor.addMark(editor, 'bold', true);
    }
  }}
  ...
/>

ما از رویدادهای مهم در همه جای Kitemaker استفاده می‌کنیم:

  • لیست کردن فهرست‌ها، هنگام فشار دادن کلید tab
  • خروج از لیست‌ها و بلوک‌های کد (در بعضی موارد بازگشت به فضای خالی)، هنگام فشار دادن کلید Enter
  • تقسیم بلوک‌ها (به عنوان مثال سرفصل ها)، هنگام فشار دادن کلید Enter در وسط یک بلوک

رندر سفارشی با React

Slate در مورد چگونگی نمایش بلوک‌ها و خطوط داخلی روی صفحه نظری ندارد. به طور پیش فرض فقط تمام بلوک‌ها را به عناصر ساده <div> و تمام خطوط را به عناصر ساده <span> سوق می‌دهد، اما این کار بسیار کسل کننده است.

برای نادیده گرفتن رفتار پیش فرض Slate، تمام کاری که ما باید انجام دهیم این است که یک تابع را به ویژگی renderElement کامپوننت <Editable> منتقل کنیم:

<Editable
  renderElement={({ element, attributes, children }) => {
    switch (element.type) {
      case 'code':
        return <pre {...attributes}>{children}</pre>;
      case 'link':
        return <a href={element.url} {...attributes}>{children}</a>;
      default:
        return <div {...attributes}>{children}</div>;
    }
  }}
/>

تمام آنچه که این کد انجام می‌دهد این است که به دنبال ویژگی type در یک گره و انتخاب مسیر رندرینگ متفاوت بر اساس آن است. به یاد داشته باشید همانطور که قبلا گفتیم، Slate به این خواص اهمیت نمی‌دهد. بنابراین قرارداد استفاده از type برای نشان دادن نوع گره است، اما هیچ چیزی شما را مجبور به انجام این کار نمی‌کند. همچنین می‌توانید انواع خصوصیات دیگری را که به رندرینگ کمک می‌کند، به اجزای خود اضافه کنید (مانند ویژگی url که در پیوندهای بالا مشاهده کردیم).

مواردی که از renderElement برگردانده شده‌اند فقط باید کامپوننت‌های React باشند. شکل ظاهری آنها و پیچیدگی آنها کاملا به خود شما بستگی دارد. در اینجا ما در حال برگرداندن یک عنصر <pre> ساده برای نشان دادن یک بلوک کد هستیم، اما هیچ چیز مانع بازگشت یک کامپوننت <Code> نمی‌شود که از برجسته سازی سینتکسی پشتیبانی می‌کند (مانند کاری که در Kitemaker انجام می‌دهیم).

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

این مقدمه‌ای فوق‌العاده سریع برای رندرینگ سفارشی بود، بنابراین اگر هنوز آن را کاملا درک نکرده‌اید، نگران نباشید.

چه مشکلاتی در Slate وجود دارد؟

تا اینجا برخی از اصول Slate را دیدید، بنابراین آماده شروع هستید. ما فکر کردیم که کمی در مورد برخی از مشکلات و معایب کار با Slate به شما توضیح دهیم تا به مشکل نخورید:

  • کپی و چسباندن: کپی و چسباندن در وب یک نوع خرابکاری است. ما یک مقاله کامل را به نحوه مدیریت این موضوع اختصاص خواهیم داد. به علاوه برای تست "منطق چسباندن" یک سند تا حدودی پیچیده در مجموعه ویرایشگران وب محبوب مانندGoogle Docs ،Notion ، Dropbox Paper و Quip ساخته‌ایم.
  • تاریخچه: به طور پیش فرض Slate از undo / redo پشتیبانی نمیکند. با این وجود افزونه‌ای به نام ()useHistory ارائه شده که این قابلیت را فراهم می‌کند. با این حال کاربران متوجه شده‌اند که این تجربه کاربری مورد نظرشان را فراهم نمی‌کند، بنابراین مجبور شده‌اند خودشان آن را گسترش دهند.
  • منوهای شناور: مواردی که به صورت پاپ آپ ظاهر می‌شوند، باید به درستی موقعیت یابی شوند (مانند آنچه در ویرایشگر Kitemaker در بالا هنگام تایپ کردن "/" برای وارد کردن بلوک یا "@" برای ذکر یک کاربر تایپ می‌شود) و این می‌تواند مشکل ساز باشد.
  • مدیریت کلیدها: Kitemaker محصولی با تعداد زیادی کلید میانبر برای هر کاری است (ما می‌خواهیم کاربران بتوانند بدون برداشتن دست از روی کلیدها از آنها استفاده کنند) اما گاهی اوقات با چالش هایی در مورد کنترل کلید هنگام کار با Slate مواجه می‌شویم.
  • API کاملا گسترده: توابع بسیار زیادی برای دستکاری نوشته وجود دارد (افزودن گره‌ها، از بین بردن گره‌ها، تقسیم گره‌ها، پیچیدن گره‌ها، افزودن متن، حذف کلمات و مواردی از این دست) و همیشه کاملا مشخص نیست که در چه شرایطی باید از API استفاده کنید.
  • متدهای ورودی مانند نوار لمسی Macbook Pro: کاربران از MBP و همچنین مواردی مانند ورودی‌های قلم نوری برای نوشتن حروف ژاپنی شکایت کرده‌اند. برای رفع برخی از این رفتارهای عجیب و غریب، یک تیم پشتیبانی وجود دارد که امیدواریم به زودی رفع شوند.

چند اخطار مختصر و مفید

هرچند که ما تاکنون از Slate بسیار راضی بوده‌ایم، اما چند هشدار وجود دارد که هر تیمی برای ایجاد ویرایشگر خود باید از آنها آگاه باشد:

  • مانند هر پروژه متن‌باز دیگری، جای پیشرفت در Slate وجود دارد. در حالی که سال گذشته بازنویسی عظیمی صورت گرفت و اوضاع به سرعت تغییر کرد. اکنون پیشرفت به میزان قابل توجهی کند شده است. بسیاری از مسائل به صورت حل نشده وجود دارد. امیدواریم که بتوانیم سهم خود را برای بهبود این پیشرفت انجام دهیم، اما جامعه می‌تواند از حمایت بیشتری بهره ببرد.
  • پس از بازنویسی سال گذشته، مستندات کاملا به استاندارد قبل از بازنویسی برنگشتند. مقدار قابل توجهی از مستندات اصلی وجود دارد که جزئیات مورد نیاز توسعه دهندگانی را که تازه شروع به کار کردند، ندارد. برای بهبود این موضوع تغییراتی ارائه داده‌ایم، اما تلاش و تمهید بیشتری لازم است.
  • Slate در اندروید به درستی پشتیبانی نمی‌شود. خوشبختانه یک پروژه Kickstarter برای رفع این مشکل تأمین اعتبار شد که خبر خوشحال کننده‌ای است.

سخن پایانی

امیدواریم که این مقاله به عنوان یک مقدمه خوب و عالی در مورد Slate برای شما مفید واقع شود و برخی از اطلاعات مورد نیاز در مورد کار با Slate یا عدم استفاده از آن را به شما ارائه دهد.

منبع

گردآوری و تالیف عرفان حشمتی
آفلاین
user-avatar

مهندس معماری سیستم های کامپیوتری، برنامه نویس و طراح وب سایت، علاقه مند به دنیای آی تی و تکنولوژی.

دیدگاه‌ها و پرسش‌ها

برای ارسال نظر لازم است ابتدا وارد سایت شوید
در حال دریافت نظرات از سرور، لطفا منتظر بمانید