Membangun Editor Teks Kaya (WYSIWYG) Dari Awal

Dalam beberapa tahun terakhir, bidang Pembuatan dan Representasi Konten pada platform Digital telah mengalami gangguan besar-besaran. Kesuksesan produk yang tersebar luas seperti Quip, Google Docs, dan Dropbox Paper telah menunjukkan bagaimana perusahaan berlomba untuk membangun pengalaman terbaik bagi pembuat konten di domain perusahaan dan mencoba menemukan cara inovatif untuk memecahkan cetakan tradisional tentang bagaimana konten dibagikan dan dikonsumsi. Memanfaatkan jangkauan besar-besaran platform media sosial, ada gelombang baru pembuat konten independen yang menggunakan platform seperti Medium untuk membuat konten dan membagikannya dengan audiens mereka.

Karena begitu banyak orang dari berbagai profesi dan latar belakang mencoba membuat konten pada produk ini, penting bahwa produk ini memberikan pengalaman pembuatan konten yang baik dan lancar serta memiliki tim perancang dan insinyur yang mengembangkan beberapa tingkat keahlian domain dari waktu ke waktu di ruang ini. . Dengan artikel ini, kami mencoba untuk tidak hanya meletakkan dasar dalam membangun editor, tetapi juga memberi pembaca sekilas gambaran tentang bagaimana sedikit nugget fungsi ketika disatukan dapat menciptakan pengalaman pengguna yang luar biasa bagi pembuat konten.

Memahami Struktur Dokumen

Sebelum kita mendalami pembuatan editor, mari kita lihat bagaimana sebuah dokumen disusun untuk Rich Text Editor dan apa saja jenis struktur data yang terlibat.

Node Dokumen

Node dokumen digunakan untuk mewakili isi dokumen. Jenis umum node yang dapat berisi dokumen teks kaya adalah paragraf, tajuk, gambar, video, blok kode, dan kutipan-tarik. Beberapa di antaranya mungkin berisi node lain sebagai anak di dalamnya (misalnya node Paragraph berisi node teks di dalamnya). Node juga menyimpan properti apa pun yang spesifik untuk objek yang mereka wakili yang diperlukan untuk merender node tersebut di dalam editor. (mis. Node gambar berisi src gambar, blok kode mungkin berisi language dan sebagainya).

Sebagian besar ada dua jenis node yang mewakili bagaimana mereka harus dirender –

  • Block Nodes (analog dengan konsep HTML dari elemen level Block) yang masing-masing dirender pada baris baru dan menempati lebar yang tersedia. Node blok dapat berisi node blok lain atau node sebaris di dalamnya. Pengamatan di sini adalah bahwa node tingkat atas dari suatu dokumen akan selalu menjadi node blok.
  • Node Inline (analog dengan konsep HTML dari elemen Inline) yang mulai merender pada baris yang sama seperti node sebelumnya. Ada beberapa perbedaan dalam bagaimana elemen sebaris direpresentasikan dalam pustaka pengeditan yang berbeda. SlateJS memungkinkan elemen inline menjadi node itu sendiri. DraftJS, pustaka Pengeditan Teks Kaya populer lainnya, memungkinkan Anda menggunakan konsep Entitas untuk merender elemen sebaris. Link dan Gambar Inline adalah contoh node Inline.
  • Void Nodes – SlateJS juga mengizinkan kategori node ketiga ini yang akan kita gunakan nanti di artikel ini untuk merender media.

Jika Anda ingin mempelajari lebih lanjut tentang kategori ini, dokumentasi SlateJS di Nodes adalah tempat yang baik untuk memulai.

Atribut

Mirip dengan konsep atribut HTML, atribut dalam Rich Text Document digunakan untuk merepresentasikan properti non-konten dari node atau turunannya. Misalnya, node teks dapat memiliki atribut gaya karakter yang memberi tahu kita apakah teks tersebut dicetak tebal / miring / bergaris bawah dan seterusnya. Meskipun artikel ini merepresentasikan tajuk sebagai node itu sendiri, cara lain untuk merepresentasikannya adalah bahwa node memiliki gaya paragraph (paragraf & h1-h6 ) sebagai atributnya.

Gambar di bawah ini memberikan contoh bagaimana struktur dokumen (dalam JSON) dideskripsikan pada tingkat yang lebih terperinci menggunakan node dan atribut yang menyoroti beberapa elemen dalam struktur di sebelah kiri.

Beberapa hal yang perlu diperhatikan di sini dengan strukturnya adalah:

  • Node teks direpresentasikan sebagai {text: 'text content'}
  • Properti dari node disimpan langsung di node (misalnya url untuk link dan caption untuk gambar)
  • Representasi atribut teks khusus SlateJS memecah node teks menjadi node mereka sendiri jika gaya karakter berubah. Oleh karena itu, teks ' Duis aute irure dolor ' adalah simpul teksnya sendiri dengan bold: true di atasnya. Sama halnya dengan teks miring, garis bawah, dan gaya kode dalam dokumen ini.

Lokasi Dan Pilihan

Saat membuat editor teks kaya, penting untuk memiliki pemahaman tentang bagaimana bagian paling terperinci dari dokumen (misalnya karakter) dapat direpresentasikan dengan beberapa jenis koordinat. Ini membantu kita menavigasi struktur dokumen saat runtime untuk memahami di mana kita berada dalam hierarki dokumen. Yang terpenting, objek lokasi memberi kita cara untuk merepresentasikan pilihan pengguna yang cukup ekstensif digunakan untuk menyesuaikan pengalaman pengguna editor secara real time. Kami akan menggunakan pilihan untuk membangun toolbar kami nanti di artikel ini. Contohnya bisa jadi:

  • Apakah kursor pengguna saat ini berada di dalam link, mungkin kita harus menampilkan menu untuk mengedit / menghapus link?
  • Sudahkah pengguna memilih gambar? Mungkin kami memberi mereka menu untuk mengubah ukuran gambar.
  • Jika pengguna memilih teks tertentu dan menekan tombol DELETE, kami menentukan teks apa yang dipilih pengguna dan menghapusnya dari dokumen.

Dokumen SlateJS di Lokasi menjelaskan struktur data ini secara ekstensif tetapi kami membahasnya di sini dengan cepat karena kami menggunakan istilah-istilah ini pada berbagai contoh dalam artikel dan menunjukkan contoh dalam diagram yang mengikuti.

  • Jalan
    Diwakili oleh deretan angka, jalur adalah cara untuk sampai ke simpul dalam dokumen. Misalnya, jalur [2,3] mewakili simpul anak ke-3 dari simpul kedua dalam dokumen.
  • Titik
    Lokasi konten yang lebih terperinci diwakili oleh jalur + offset. Misalnya, titik {path: [2,3], offset: 14} mewakili karakter ke-14 dari simpul anak ke-3 di dalam simpul ke-2 dokumen.
  • Jarak
    Sepasang titik (disebut anchor dan focus ) yang mewakili rentang teks di dalam dokumen. Konsep ini berasal dari API Seleksi Web di mana anchor adalah tempat pemilihan pengguna dimulai dan focus adalah tempat berakhirnya. Rentang / seleksi yang diciutkan menunjukkan di mana jangkar dan titik fokus sama (pikirkan kursor yang berkedip dalam input teks misalnya).

Sebagai contoh, misalkan pilihan pengguna dalam contoh dokumen di atas adalah ipsum :

Pilihan pengguna dapat direpresentasikan sebagai:

 { anchor: {path: [2,0], offset: 5}, / 0th text node inside the paragraph node which itself is index 2 in the document / focus: {path: [2,0], offset: 11}, // space + 'ipsum' }`

Menyiapkan Editor

Di bagian ini, kita akan menyiapkan aplikasi dan menggunakan editor teks kaya dasar dengan SlateJS. Aplikasi boilerplate akan berupa create-react-app dengan dependensi SlateJS yang ditambahkan ke dalamnya. Kami sedang membangun UI aplikasi menggunakan komponen dari react-bootstrap . Ayo mulai!

Buat folder bernama wysiwyg-editor dan jalankan perintah di bawah ini dari dalam direktori untuk mengatur aplikasi react. Kami kemudian menjalankan yarn start yang harus memutar server web lokal (port default ke 3000) dan menampilkan layar selamat datang React.

 npx create-react-app . yarn start

Kami kemudian melanjutkan untuk menambahkan dependensi SlateJS ke aplikasi.

 yarn add slate slate-react

slate adalah paket inti SlateJS dan slate-react menyertakan sekumpulan komponen React yang akan kita gunakan untuk membuat editor Slate. SlateJS memaparkan beberapa paket lagi yang diatur oleh fungsionalitas yang dapat dipertimbangkan untuk ditambahkan ke editor mereka.

Pertama-tama kita membuat utils yang menampung modul utilitas apa pun yang kita buat di aplikasi ini. Kita mulai dengan membuat ExampleDocument.js yang mengembalikan struktur dokumen dasar yang berisi paragraf dengan beberapa teks. Modul ini terlihat seperti di bawah ini:

 const ExampleDocument = [ { type: "paragraph", children: [ { text: "Hello World! This is my paragraph inside a sample document." }, ], }, ]; export default ExampleDocument;

Kami sekarang menambahkan folder bernama components yang akan menampung semua komponen React kami dan melakukan hal berikut:

  • Tambahkan komponen React Editor.js pertama kita ke dalamnya. Ini hanya mengembalikan div untuk saat ini.
  • Perbarui App.js untuk menyimpan dokumen dalam keadaannya yang diinisialisasi ke ExampleDocument kami di atas.
  • Render Editor di dalam aplikasi dan teruskan status dokumen dan onChange ke Editor sehingga status dokumen kita diperbarui saat pengguna memperbaruinya.
  • Kami menggunakan komponen Nav dari React bootstrap untuk menambahkan bilah navigasi ke aplikasi juga.

App.js sekarang terlihat seperti di bawah ini:

 import Editor from './components/Editor'; function App() { const [document, updateDocument] = useState(ExampleDocument); return ( <> <Navbar bg="dark" variant="dark"> <Navbar.Brand href="#"> <img alt="" src="/app-icon.png" width="30" height="30" className="d-inline-block align-top" />{" "} WYSIWYG Editor </Navbar.Brand> </Navbar> <div className="App"> <Editor document={document} onChange={updateDocument} /> </div> </> );

Di dalam komponen Editor, kami kemudian membuat instance editor SlateJS dan menahannya di dalam useMemo sehingga objek tidak berubah di antara render ulang.

 // dependencies imported as below. import { withReact } from "slate-react"; import { createEditor } from "slate"; const editor = useMemo(() => withReact(createEditor()), []);

createEditor memberi kami editor SlateJS yang kami gunakan secara ekstensif melalui aplikasi untuk mengakses pilihan, menjalankan transformasi data, dan sebagainya. withReact adalah plugin SlateJS yang menambahkan perilaku React dan DOM ke objek editor. SlateJS Plugins adalah fungsi Javascript yang menerima editor dan melampirkan beberapa konfigurasi padanya. Ini memungkinkan pengembang web untuk menambahkan konfigurasi ke instance editor SlateJS mereka dengan cara yang dapat disusun.

Kita sekarang mengimpor dan merender <Slate /> dan <Editable /> dari SlateJS dengan prop dokumen yang kita dapatkan dari App.js. Slate memperlihatkan sekumpulan konteks React yang kami gunakan untuk mengakses dalam kode aplikasi. Editable adalah komponen yang membuat hierarki dokumen untuk diedit. Secara keseluruhan, Editor.js pada tahap ini terlihat seperti di bawah ini:

 import { Editable, Slate, withReact } from "slate-react"; import { createEditor } from "slate"; import { useMemo } from "react"; export default function Editor({ document, onChange }) { const editor = useMemo(() => withReact(createEditor()), []); return ( <Slate editor={editor} value={document} onChange={onChange}> <Editable /> </Slate> ); }

Pada titik ini, kita perlu menambahkan komponen React dan editor diisi dengan dokumen contoh. Editor kami sekarang harus disiapkan sehingga memungkinkan kami untuk mengetik dan mengubah konten secara real time – seperti pada screencast di bawah ini.

Gaya Karakter

Mirip dengan renderElement , SlateJS memberikan fungsi prop yang disebut renderLeaf yang dapat digunakan untuk menyesuaikan rendering node teks ( Leaf mengacu pada node teks yang merupakan node tingkat daun / terendah dari pohon dokumen). Mengikuti contoh renderElement , kami menulis implementasi untuk renderLeaf .

 export default function useEditorConfig(editor) { return { renderElement, renderLeaf }; } // ... function renderLeaf({ attributes, children, leaf }) { let el = <>{children}</>; if (leaf.bold) { el = <strong>{el}</strong>; } if (leaf.code) { el = <code>{el}</code>; } if (leaf.italic) { el = <em>{el}</em>; } if (leaf.underline) { el = <u>{el}</u>; } return <span {...attributes}>{el}</span>; }

Pengamatan penting dari implementasi di atas adalah memungkinkan kita untuk menghormati semantik HTML untuk gaya karakter. Karena renderLeaf memberi kita akses ke leaf simpul teks itu sendiri, kita dapat menyesuaikan fungsi untuk mengimplementasikan rendering yang lebih disesuaikan. Misalnya, Anda mungkin memiliki cara untuk mengizinkan pengguna memilih highlightColor untuk teks dan memeriksa properti daun di sini untuk melampirkan gaya masing-masing.

Kami sekarang memperbarui komponen Editor untuk menggunakan yang di atas, ExampleDocument untuk memiliki beberapa node teks dalam paragraf dengan kombinasi gaya ini dan memverifikasi bahwa mereka dirender seperti yang diharapkan di Editor dengan tag semantik yang kami gunakan.

 # src/components/Editor.js const { renderElement, renderLeaf } = useEditorConfig(editor); return ( ... <Editable renderElement={renderElement} renderLeaf={renderLeaf} /> );
 # src/utils/ExampleDocument.js { type: "paragraph", children: [ { text: "Hello World! This is my paragraph inside a sample document." }, { text: "Bold text.", bold: true, code: true }, { text: "Italic text.", italic: true }, { text: "Bold and underlined text.", bold: true, underline: true }, { text: "variableFoo", code: true }, ], }, 

Menambahkan Toolbar

Mari kita mulai dengan menambahkan komponen baru Toolbar.js yang kita tambahkan beberapa tombol untuk gaya karakter dan dropdown untuk gaya paragraf dan kita pasang nanti di bagian.

 const PARAGRAPH_STYLES = ["h1", "h2", "h3", "h4", "paragraph", "multiple"]; const CHARACTER_STYLES = ["bold", "italic", "underline", "code"]; export default function Toolbar({ selection, previousSelection }) { return ( <div className="toolbar"> {/* Dropdown for paragraph styles */} <DropdownButton className={"block-style-dropdown"} disabled={false} id="block-style" title={getLabelForBlockStyle("paragraph")} > {PARAGRAPH_STYLES.map((blockType) => ( <Dropdown.Item eventKey={blockType} key={blockType}> {getLabelForBlockStyle(blockType)} </Dropdown.Item> ))} </DropdownButton> {/* Buttons for character styles */} {CHARACTER_STYLES.map((style) => ( <ToolBarButton key={style} icon={<i className={`bi ${getIconForButton(style)}`} />} isActive={false} /> ))} </div> ); } function ToolBarButton(props) { const { icon, isActive, ...otherProps } = props; return ( <Button variant="outline-primary" className="toolbar-btn" active={isActive} {...otherProps} > {icon} </Button> ); }

Kami memisahkan tombol ke ToolbarButton yang merupakan pembungkus di sekitar komponen Tombol React Bootstrap. Kami kemudian merender toolbar di atas komponen Editable inside Editor dan memverifikasi bahwa toolbar muncul di aplikasi.

Berikut adalah tiga fungsi utama yang perlu didukung oleh toolbar:

  1. Saat kursor pengguna berada di tempat tertentu di dokumen dan mereka mengklik salah satu tombol gaya karakter, kita perlu mengganti gaya untuk teks yang mungkin mereka ketik berikutnya.
  2. Saat pengguna memilih serangkaian teks dan mengklik salah satu tombol gaya karakter, kita perlu mengubah gaya untuk bagian tertentu itu.
  3. Saat pengguna memilih serangkaian teks, kami ingin memperbarui dropdown gaya paragraf untuk mencerminkan jenis paragraf pilihan. Jika mereka memilih nilai yang berbeda dari pilihan, kami ingin memperbarui gaya paragraf dari seluruh pilihan menjadi apa yang mereka pilih.

Mari kita lihat bagaimana fungsi ini bekerja pada Editor sebelum kita mulai menerapkannya.

Menambahkan Tombol Tautan ke Toolbar

Mari tambahkan Tombol Tautan ke bilah alat yang memungkinkan pengguna melakukan hal berikut:

  • Memilih beberapa teks dan mengklik tombol akan mengubah teks tersebut menjadi link
  • Memiliki kursor yang berkedip (pilihan diciutkan) dan mengklik tombol akan menyisipkan link baru di sana
  • Jika pilihan pengguna ada di dalam link, mengklik tombol tersebut akan mengalihkan link – artinya mengubah link kembali menjadi teks.

Untuk membangun fungsionalitas ini, kita membutuhkan cara di toolbar untuk mengetahui apakah pilihan pengguna ada di dalam node link. Kami menambahkan util fungsi yang melintasi tingkat di arah ke atas dari pilihan pengguna untuk menemukan node link yang jika ada satu, menggunakan Editor.above fungsi pembantu dari SlateJS.

 # src/utils/EditorUtils.js export function isLinkNodeAtSelection(editor, selection) { if (selection == null) { return false; } return ( Editor.above(editor, { at: selection, match: (n) => n.type === "link", }) != null ); }

Sekarang, mari tambahkan tombol ke toolbar yang dalam status aktif jika pilihan pengguna ada di dalam node link.

 # src/components/Toolbar.js return ( <div className="toolbar"> ... {/* Link Button */} <ToolBarButton isActive={isLinkNodeAtSelection(editor, editor.selection)} label={<i className={`bi ${getIconForButton("link")}`} />} /> </div> ); 

Jika kita harus melakukan ini sendiri, kita harus mencari tahu kisaran pemilihan dan membuat tiga simpul baru (teks, tautan, teks) yang menggantikan simpul teks asli. SlateJS memiliki fungsi pembantu yang disebut Transforms.wrapNodes yang melakukan hal ini – membungkus node di suatu lokasi menjadi node kontainer baru. Kami juga memiliki pembantu yang tersedia untuk kebalikan dari proses ini – Transforms.unwrapNodes yang kami gunakan untuk menghapus tautan dari teks yang dipilih dan menggabungkan teks itu kembali ke simpul teks di sekitarnya. Dengan itu, toggleLinkAtSelection memiliki implementasi di bawah ini untuk menyisipkan tautan baru pada pilihan yang diperluas.

 # src/utils/EditorUtils.js export function toggleLinkAtSelection(editor) { if (!isLinkNodeAtSelection(editor, editor.selection)) { const isSelectionCollapsed = Range.isCollapsed(editor.selection); if (isSelectionCollapsed) { Transforms.insertNodes( editor, { type: "link", url: '#', children: [{ text: 'link' }], }, { at: editor.selection } ); } else { Transforms.wrapNodes( editor, { type: "link", url: '#', children: [{ text: '' }] }, { split: true, at: editor.selection } ); } } else { Transforms.unwrapNodes(editor, { match: (n) => Element.isElement(n) && n.type === "link", }); } }

Jika pilihan diciutkan, kami memasukkan node baru di sana dengan Transform.insertNodes yang memasukkan node di lokasi tertentu dalam dokumen. Kami menghubungkan fungsi ini dengan tombol toolbar dan sekarang seharusnya memiliki cara untuk menambah / menghapus tautan dari dokumen dengan bantuan tombol tautan.

 # src/components/Toolbar.js <ToolBarButton ... isActive={isLinkNodeAtSelection(editor, editor.selection)} onMouseDown={() => toggleLinkAtSelection(editor)} /> 

Jika teks 'ABCDE' adalah simpul teks pertama dari paragraf pertama dalam dokumen, nilai poin kami adalah –

 cursorPoint = { path: [0,0], offset: 5} startPointOfLastCharacter = { path: [0,0], offset: 4}

Jika karakter terakhir adalah spasi, kita tahu di mana itu dimulai – startPointOfLastCharacter. Mari pindah ke langkah-2 di mana kita bergerak mundur karakter demi karakter sampai kita menemukan spasi lain atau awal dari simpul teks itu sendiri.

 ... if (lastCharacter !== " ") { return; } let end = startPointOfLastCharacter; start = Editor.before(editor, end, { unit: "character", }); const startOfTextNode = Editor.point(editor, currentNodePath, { edge: "start", }); while ( Editor.string(editor, Editor.range(editor, start, end)) !== " " && !Point.isBefore(start, startOfTextNode) ) { end = start; start = Editor.before(editor, end, { unit: "character" }); } const lastWordRange = Editor.range(editor, end, startPointOfLastCharacter); const lastWord = Editor.string(editor, lastWordRange);

Berikut adalah diagram yang menunjukkan di mana titik-titik yang berbeda ini menunjuk setelah kita menemukan kata terakhir yang dimasukkan menjadi ABCDE .

Perhatikan bahwa start dan end adalah titik sebelum dan sesudah spasi di sana. Demikian pula, startPointOfLastCharacter dan cursorPoint adalah titik sebelum dan sesudah pengguna spasi baru saja disisipkan. Karenanya [end,startPointOfLastCharacter] memberi kita kata terakhir yang disisipkan.

Kami mencatat nilai lastWord ke konsol dan memverifikasi nilai saat kami mengetik.

Sekarang mari fokus pada pengeditan teks. Cara yang kami inginkan agar ini menjadi pengalaman yang mulus bagi pengguna adalah saat mereka mengeklik teks, kami menampilkan masukan teks tempat mereka dapat mengedit teks tersebut. Jika mereka mengklik di luar input atau menekan tombol RETURN, kami memperlakukannya sebagai konfirmasi untuk menerapkan teks. Kami kemudian memperbarui teks pada node gambar dan mengalihkan teks kembali ke mode baca. Mari kita lihat cara kerjanya sehingga kita memiliki gambaran tentang apa yang sedang kita bangun.

Mari perbarui komponen Image kita agar memiliki status untuk mode baca-edit teks. Kami memperbarui status teks lokal saat pengguna memperbaruinya dan ketika mereka mengklik ( onBlur ) atau menekan RETURN ( onKeyDown ), kami menerapkan teks tersebut ke node dan beralih ke mode baca lagi.

 const Image = ({ attributes, children, element }) => { const [isEditingCaption, setEditingCaption] = useState(false); const [caption, setCaption] = useState(element.caption); ... const applyCaptionChange = useCallback( (captionInput) => { const imageNodeEntry = Editor.above(editor, { match: (n) => n.type === "image", }); if (imageNodeEntry == null) { return; } if (captionInput != null) { setCaption(captionInput); } Transforms.setNodes( editor, { caption: captionInput }, { at: imageNodeEntry[1] } ); }, [editor, setCaption] ); const onCaptionChange = useCallback( (event) => { setCaption(event.target.value); }, [editor.selection, setCaption] ); const onKeyDown = useCallback( (event) => { if (!isHotkey("enter", event)) { return; } applyCaptionChange(event.target.value); setEditingCaption(false); }, [applyCaptionChange, setEditingCaption] ); const onToggleCaptionEditMode = useCallback( (event) => { const wasEditing = isEditingCaption; setEditingCaption(!isEditingCaption); wasEditing && applyCaptionChange(caption); }, [editor.selection, isEditingCaption, applyCaptionChange, caption] ); return ( ... {isEditingCaption ? ( <Form.Control autoFocus={true} className={"image-caption-input"} size="sm" type="text" defaultValue={element.caption} onKeyDown={onKeyDown} onChange={onCaptionChange} onBlur={onToggleCaptionEditMode} /> ) : ( <div className={"image-caption-read-mode"} onClick={onToggleCaptionEditMode} > {caption} </div> )} </div> ...

Dengan itu, fungsi pengeditan teks selesai. Kami sekarang pindah ke menambahkan cara bagi pengguna untuk mengunggah gambar ke editor. Mari tambahkan tombol bilah alat yang memungkinkan pengguna memilih dan mengunggah gambar.

 # src/components/Toolbar.js const onImageSelected = useImageUploadHandler(editor, previousSelection); return ( <div className="toolbar"> .... <ToolBarButton isActive={false} as={"label"} htmlFor="image-upload" label={ <> <i className={`bi ${getIconForButton("image")}`} /> <input type="file" id="image-upload" className="image-upload-input" accept="image/png, image/jpeg" onChange={onImageSelected} /> </> } /> </div>

Saat kami bekerja dengan unggahan gambar, kodenya bisa bertambah sedikit sehingga kami memindahkan penanganan unggahan gambar ke hook useImageUploadHandler yang memberikan callback yang dilampirkan ke elemen input file. Kita akan membahas lama tentang mengapa ia membutuhkan previousSelection negara.

Sebelum kami menerapkan useImageUploadHandler , kami akan menyiapkan server agar dapat mengunggah gambar ke. Kami setup server Express dan menginstal dua paket lainnya – cors dan multer yang menangani file upload untuk kita.

 yarn add express cors multer

Kami kemudian menambahkan src/server.js yang mengonfigurasi server Express dengan cors dan multer dan memperlihatkan titik akhir /upload yang akan kami unggah gambarnya.

 # src/server.js const storage = multer.diskStorage({ destination: function (req, file, cb) { cb(null, "./public/photos/"); }, filename: function (req, file, cb) { cb(null, file.originalname); }, }); var upload = multer({ storage: storage }).single("photo"); app.post("/upload", function (req, res) { upload(req, res, function (err) { if (err instanceof multer.MulterError) { return res.status(500).json(err); } else if (err) { return res.status(500).json(err); } return res.status(200).send(req.file); }); }); app.use(cors()); app.listen(port, () => console.log(`Listening on port ${port}`));

Sekarang setelah kita memiliki pengaturan server, kita dapat fokus menangani pengunggahan gambar. Saat pengguna mengunggah gambar, mungkin perlu beberapa detik sebelum gambar diunggah dan kami memiliki URL untuk itu. Namun, kami melakukan apa untuk memberikan umpan balik langsung kepada pengguna bahwa unggahan gambar sedang berlangsung sehingga mereka tahu bahwa gambar sedang disisipkan di editor. Berikut adalah langkah-langkah yang kami terapkan untuk membuat perilaku ini berfungsi –

  1. Setelah pengguna memilih gambar, kami menyisipkan node gambar pada posisi kursor pengguna dengan flag isUploading disetel di atasnya sehingga kami dapat menunjukkan kepada pengguna status pemuatan.
  2. Kami mengirim permintaan ke server untuk mengunggah gambar.
  3. Setelah permintaan selesai dan kami memiliki URL gambar, kami mengaturnya pada gambar dan menghapus status pemuatan.

Mari kita mulai dengan langkah pertama di mana kita memasukkan node gambar. Sekarang, bagian yang sulit di sini adalah kita mengalami masalah yang sama dengan pemilihan seperti pada tombol tautan di toolbar. Segera setelah pengguna mengklik tombol Gambar di toolbar, editor kehilangan fokus dan pemilihan menjadi null . Jika kami mencoba memasukkan gambar, kami tidak tahu di mana kursor pengguna berada. Melacak previousSelection memberi kita lokasi itu dan kita menggunakannya untuk memasukkan node.

 # src/hooks/useImageUploadHandler.js import { v4 as uuidv4 } from "uuid"; export default function useImageUploadHandler(editor, previousSelection) { return useCallback( (event) => { event.preventDefault(); const files = event.target.files; if (files.length === 0) { return; } const file = files[0]; const fileName = file.name; const formData = new FormData(); formData.append("photo", file); const id = uuidv4(); Transforms.insertNodes( editor, { id, type: "image", caption: fileName, url: null, isUploading: true, children: [{ text: "" }], }, { at: previousSelection, select: true } ); }, [editor, previousSelection] ); }

Saat kami memasukkan node gambar baru, kami juga menetapkannya sebagai id pengenal menggunakan paket uuid. Kita akan membahas dalam implementasi Langkah (3) mengapa kita membutuhkannya. Kami sekarang memperbarui komponen gambar untuk menggunakan isUploading untuk menunjukkan status pemuatan.

 {!element.isUploading && element.url != null ? ( <img src={element.url} alt={caption} className={"image"} /> ) : ( <div className={"image-upload-placeholder"}> <Spinner animation="border" variant="dark" /> </div> )}

Itu menyelesaikan implementasi langkah 1. Mari kita verifikasi bahwa kita dapat memilih gambar untuk diunggah, lihat node gambar dimasukkan dengan indikator pemuatan di mana itu dimasukkan ke dalam dokumen.

Pindah ke Langkah (2), kita akan menggunakan perpustakaan axois untuk mengirim permintaan ke server.

 export default function useImageUploadHandler(editor, previousSelection) { return useCallback((event) => { .... Transforms.insertNodes( … {at: previousSelection, select: true} ); axios .post("/upload", formData, { headers: { "content-type": "multipart/form-data", }, }) .then((response) => { // update the image node. }) .catch((error) => { // Fire another Transform.setNodes to set an upload failed state on the image }); }, [...]); }

Kami memverifikasi bahwa unggahan gambar berfungsi dan gambar tersebut muncul di public/photos aplikasi. Sekarang setelah pengunggahan gambar selesai, kita pindah ke Langkah (3) di mana kita ingin mengatur URL pada gambar dalam resolve() dari janji axios. Kami dapat memperbarui gambar dengan Transforms.setNodes tetapi kami memiliki masalah – kami tidak memiliki jalur ke node gambar yang baru dimasukkan. Mari kita lihat apa pilihan kita untuk mendapatkan gambar itu –

  • Tidak bisakah kita menggunakan editor.selection karena seleksi harus berada pada node gambar yang baru disisipkan? Kami tidak dapat menjamin ini karena saat gambar diunggah, pengguna mungkin telah mengklik di tempat lain dan pilihan mungkin telah berubah.
  • Bagaimana dengan penggunaan previousSelection yang dulu kita gunakan untuk memasukkan node gambar? Untuk alasan yang sama kita tidak dapat menggunakan editor.selection , kita tidak bisa menggunakan previousSelection karena mungkin telah berubah juga.
  • SlateJS memiliki modul Sejarah yang melacak semua perubahan yang terjadi pada dokumen. Kita dapat menggunakan modul ini untuk mencari sejarah dan menemukan simpul gambar yang terakhir disisipkan. Ini juga tidak sepenuhnya dapat diandalkan jika butuh waktu lebih lama untuk mengunggah gambar dan pengguna memasukkan lebih banyak gambar di berbagai bagian dokumen sebelum unggahan pertama selesai.
  • Saat ini, Transform.insertNodes tidak mengembalikan informasi apa pun tentang node yang disisipkan. Jika itu bisa mengembalikan jalur ke simpul yang disisipkan, kita bisa menggunakannya untuk menemukan simpul gambar yang tepat yang harus kita perbarui.

Karena tidak ada pendekatan di atas yang berfungsi, kami menerapkan id ke node gambar yang disisipkan (pada Langkah (1)) dan menggunakan id sama lagi untuk menemukannya ketika unggahan gambar selesai. Dengan itu, kode kita untuk Langkah (3) terlihat seperti di bawah ini –

 axios .post("/upload", formData, { headers: { "content-type": "multipart/form-data", }, }) .then((response) => { const newImageEntry = Editor.nodes(editor, { match: (n) => n.id === id, }); if (newImageEntry == null) { return; } Transforms.setNodes( editor, { isUploading: false, url: `/photos/${fileName}` }, { at: newImageEntry[1] } ); }) .catch((error) => { // Fire another Transform.setNodes to set an upload failure state // on the image. });

Dengan implementasi ketiga langkah selesai, kami siap menguji unggahan gambar secara menyeluruh.

Dengan itu, kami telah menyelesaikan Gambar untuk editor kami. Saat ini, kami menampilkan status pemuatan dengan ukuran yang sama terlepas dari gambarnya. Ini bisa menjadi pengalaman yang tidak menyenangkan bagi pengguna jika status pemuatan diganti dengan gambar yang secara drastis lebih kecil atau lebih besar saat upload selesai. Tindak lanjut yang baik untuk pengalaman pengunggahan adalah mendapatkan dimensi gambar sebelum pengunggahan dan menampilkan placeholder dengan ukuran tersebut sehingga transisi berjalan mulus. Hook yang kami tambahkan di atas dapat diperpanjang untuk mendukung jenis media lain seperti video atau dokumen dan merender jenis node tersebut juga.

Kesimpulan

Dalam artikel ini, kami telah membangun Editor WYSIWYG yang memiliki sekumpulan fungsionalitas dasar dan beberapa pengalaman pengguna mikro seperti deteksi tautan, pengeditan tautan di tempat, dan pengeditan teks gambar yang membantu kami mempelajari SlateJS lebih dalam dan konsep Pengeditan Teks Kaya di umum. Jika masalah ruang seputar Pengeditan Teks Kaya atau Pemrosesan Kata ini menarik minat Anda, beberapa masalah keren yang harus Anda hadapi mungkin:

  • Kolaborasi
  • Pengalaman pengeditan teks yang lebih kaya yang mendukung perataan teks, gambar sebaris, salin-tempel, mengubah font dan warna teks, dll.
  • Mengimpor dari format populer seperti dokumen Word dan penurunan harga.

Jika Anda ingin mempelajari lebih lanjut SlateJS, berikut adalah beberapa tautan yang mungkin bisa membantu.

  • Contoh SlateJS
    Banyak contoh yang melampaui dasar-dasar dan membangun fungsi yang biasanya ditemukan di Editor seperti Penelusuran & Sorotan, Pratinjau Penurunan Harga, dan Sebutan.
  • Dokumen API
    Referensi ke banyak fungsi pembantu yang diekspos oleh SlateJS yang mungkin ingin tetap berguna saat mencoba melakukan kueri / transformasi kompleks pada objek SlateJS.

Terakhir, Slack Channel SlateJS adalah komunitas pengembang web yang sangat aktif membangun aplikasi Pengeditan Teks Kaya menggunakan SlateJS dan tempat yang tepat untuk mempelajari lebih lanjut tentang perpustakaan dan mendapatkan bantuan jika diperlukan.

May 21, 2021

codeorayo

Ampuh! Ini rahasia mengembangkan aplikasi secara instan, tinggal download dan kembangkan. Gabung sekarang juga! Premium Membership [PRIVATE] https://premium.codeorayo.com

Leave a Reply

Your email address will not be published. Required fields are marked *