Menambahkan Sistem Berkomentar Ke Editor WYSIWYG

Dalam beberapa tahun terakhir, kami telah melihat Kolaborasi menembus banyak alur kerja digital dan kasus penggunaan di banyak profesi. Tepat di dalam komunitas Desain dan Rekayasa Perangkat Lunak, kami melihat desainer berkolaborasi pada artefak desain menggunakan alat seperti Figma , tim melakukan Sprint dan Perencanaan Proyek menggunakan alat seperti Mural dan wawancara dilakukan menggunakan CoderPad . Semua alat ini terus-menerus bertujuan untuk menjembatani kesenjangan antara pengalaman dunia online dan fisik dalam menjalankan alur kerja ini dan menjadikan pengalaman kolaborasi sekaya dan semulus mungkin.

Untuk sebagian besar Alat Kolaborasi seperti ini, kemampuan untuk berbagi pendapat satu sama lain dan berdiskusi tentang konten yang sama adalah hal yang harus dimiliki. Sistem Komentar yang memungkinkan kolaborator membuat anotasi bagian dokumen dan melakukan percakapan tentangnya adalah inti dari konsep ini. Bersamaan dengan membangun satu untuk teks di Editor WYSIWYG, artikel ini mencoba untuk melibatkan pembaca tentang bagaimana kami mencoba untuk mempertimbangkan pro dan kontra dan mencoba untuk menemukan keseimbangan antara kompleksitas aplikasi dan pengalaman pengguna dalam hal membangun fitur untuk Editor WYSIWYG atau Pengolah Kata secara umum.

Mewakili Komentar Dalam Struktur Dokumen

Untuk menemukan cara untuk merepresentasikan komentar dalam struktur data dokumen teks kaya, mari kita lihat beberapa skenario di mana komentar dapat dibuat di dalam editor.

  • Komentar dibuat di atas teks yang tidak memiliki gaya di atasnya (skenario dasar);
  • Komentar dibuat di atas teks yang dapat dicetak tebal / miring / bergaris bawah, dan sebagainya;
  • Komentar yang tumpang tindih dalam beberapa cara (tumpang tindih sebagian di mana dua komentar hanya berbagi beberapa kata atau sepenuhnya ada di mana teks satu komentar sepenuhnya terkandung dalam teks komentar lain);
  • Komentar yang dibuat di atas teks di dalam tautan (khusus karena tautan adalah simpul itu sendiri dalam struktur dokumen kami);
  • Komentar yang menjangkau banyak paragraf (khusus karena paragraf adalah node dalam struktur dokumen kami dan komentar diterapkan ke node teks yang merupakan turunan paragraf).

Melihat kasus penggunaan di atas, sepertinya komentar yang muncul di dokumen teks kaya sangat mirip dengan gaya karakter (tebal, miring, dll.). Mereka dapat tumpang tindih satu sama lain, membahas teks dalam jenis node lain seperti tautan dan bahkan menjangkau beberapa node induk seperti paragraf.

Untuk alasan ini, kami menggunakan metode yang sama untuk merepresentasikan komentar seperti yang kami lakukan untuk gaya karakter, yaitu "Marks" (seperti yang disebut dalam terminologi SlateJS). Tanda hanyalah properti biasa pada node – khususnya API Slate di sekitar tanda ( Editor.addMark dan Editor.removeMark ) menangani perubahan hierarki node karena beberapa tanda diterapkan ke rentang teks yang sama. Ini sangat berguna bagi kami karena kami menangani banyak kombinasi berbeda dari komentar yang tumpang tindih.

Utas Komentar Sebagai Tanda

Setiap kali pengguna memilih serangkaian teks dan mencoba memasukkan komentar, secara teknis, mereka memulai utas komentar baru untuk rentang teks itu. Karena kami mengizinkan mereka untuk menyisipkan komentar dan kemudian membalas komentar tersebut, kami memperlakukan acara ini sebagai penyisipan utas komentar baru dalam dokumen.

Cara kami merepresentasikan utas komentar sebagai tanda adalah setiap utas komentar diwakili oleh tanda bernama commentThread_threadID mana threadID adalah ID unik yang kami tetapkan ke setiap utas komentar. Jadi, jika rentang teks yang sama memiliki dua utas komentar di atasnya, itu akan memiliki dua properti yang disetel ke truecommentThread_thread1 dan commentThread_thread2 . Di sinilah utas komentar sangat mirip dengan gaya karakter karena jika teks yang sama dicetak tebal dan miring, properti tersebut akan disetel ke truebold dan italic .

Sebelum kita benar-benar mendalami pengaturan struktur ini, ada baiknya melihat bagaimana node teks berubah saat utas komentar diterapkan padanya. Cara kerjanya (seperti halnya dengan tanda apa pun) adalah ketika properti mark sedang disetel pada teks yang dipilih, API Editor.addMark Slate akan membagi simpul teks jika diperlukan sehingga dalam struktur yang dihasilkan, simpul teks diatur sedemikian rupa sehingga setiap node teks memiliki nilai tanda yang sama persis.

Untuk memahami ini lebih baik, lihat tiga contoh berikut yang menunjukkan status sebelum dan sesudah dari node teks setelah utas komentar disisipkan pada teks yang dipilih:

Menyoroti Teks Berkomentar

Sekarang kita tahu bagaimana kita akan merepresentasikan komentar dalam struktur dokumen, mari kita lanjutkan dan tambahkan beberapa ke dokumen contoh dari artikel pertama dan konfigurasikan editor untuk benar-benar menampilkannya sebagai yang disorot. Karena kita akan memiliki banyak fungsi utilitas untuk menangani komentar di artikel ini, kita membuat EditorCommentUtils yang akan menampung semua utilitas ini. Untuk memulainya, kami membuat fungsi yang membuat tanda untuk ID utas komentar tertentu. Kami kemudian menggunakannya untuk memasukkan beberapa utas komentar di ExampleDocument kami.

 # src/utils/EditorCommentUtils.js const COMMENT_THREAD_PREFIX = "commentThread_"; export function getMarkForCommentThreadID(threadID) { return `${COMMENT_THREAD_PREFIX}${threadID}`; }

Gambar di bawah ini menggarisbawahi dengan warna merah rentang teks yang kita miliki sebagai contoh utas komentar yang ditambahkan di potongan kode berikutnya. Perhatikan bahwa teks 'Richard McClintock' memiliki dua utas komentar yang saling tumpang tindih. Secara khusus, ini adalah kasus satu utas komentar sepenuhnya terkandung di dalam utas lain.

 # src/utils/ExampleDocument.js import { getMarkForCommentThreadID } from "../utils/EditorCommentUtils"; import { v4 as uuid } from "uuid"; const exampleOverlappingCommentThreadID = uuid(); const ExampleDocument = [ ... { text: "Lorem ipsum", [getMarkForCommentThreadID(uuid())]: true, }, ... { text: "Richard McClintock", // note the two comment threads here. [getMarkForCommentThreadID(uuid())]: true, [getMarkForCommentThreadID(exampleOverlappingCommentThreadID)]: true, }, { text: ", a Latin scholar", [getMarkForCommentThreadID(exampleOverlappingCommentThreadID)]: true, }, ... ];

Kami fokus pada sisi UI dari hal-hal Sistem Komentar di artikel ini jadi kami menetapkan ID mereka dalam dokumen contoh secara langsung menggunakan paket npm uuid . Sangat mungkin bahwa dalam versi produksi editor, ID ini dibuat oleh layanan backend.

Kami sekarang fokus pada mengutak-atik editor untuk menampilkan node teks ini sebagai yang disorot. Untuk melakukan itu, saat merender node teks, kita memerlukan cara untuk mengetahui apakah ada untaian komentar di dalamnya. Kami menambahkan util getCommentThreadsOnTextNode untuk itu. Kami membangun StyledText yang kami buat di artikel pertama untuk menangani kasus di mana ia mungkin mencoba membuat simpul teks dengan komentar. Karena kami memiliki beberapa fungsi lagi yang akan ditambahkan ke node teks komentar nanti, kami membuat komponen CommentedText yang menampilkan teks komentar. StyledText akan memeriksa apakah node teks yang coba dirender memiliki komentar di dalamnya. Jika ya, itu membuat CommentedText . Ini menggunakan util getCommentThreadsOnTextNode untuk menyimpulkannya.

 # src/utils/EditorCommentUtils.js export function getCommentThreadsOnTextNode(textNode) { return new Set( // Because marks are just properties on nodes, // we can simply use Object.keys() here. Object.keys(textNode) .filter(isCommentThreadIDMark) .map(getCommentThreadIDFromMark) ); } export function getCommentThreadIDFromMark(mark) { if (!isCommentThreadIDMark(mark)) { throw new Error("Expected mark to be of a comment thread"); } return mark.replace(COMMENT_THREAD_PREFIX, ""); } function isCommentThreadIDMark(mayBeCommentThread) { return mayBeCommentThread.indexOf(COMMENT_THREAD_PREFIX) === 0; }

Artikel pertama membuat komponen StyledText yang merender node teks (menangani gaya karakter dan seterusnya). Kami memperluas komponen itu untuk menggunakan util di atas dan merender CommentedText jika node memiliki komentar di atasnya.

 # src/components/StyledText.js import { getCommentThreadsOnTextNode } from "../utils/EditorCommentUtils"; export default function StyledText({ attributes, children, leaf }) { ... const commentThreads = getCommentThreadsOnTextNode(leaf); if (commentThreads.size > 0) { return ( <CommentedText {...attributes} // We use commentThreads and textNode props later in the article. commentThreads={commentThreads} textNode={leaf} > {children} </CommentedText> ); } return <span {...attributes}>{children}</span>; }

Di bawah ini adalah implementasi CommentedText yang merender node teks dan melampirkan CSS yang menunjukkannya sebagai yang disorot.

 # src/components/CommentedText.js import "./CommentedText.css"; import classNames from "classnames"; export default function CommentedText(props) { const { commentThreads, ...otherProps } = props; return ( <span {...otherProps} className={classNames({ comment: true, })} > {props.children} </span> ); } # src/components/CommentedText.css .comment { background-color: #feeab5; }

Dengan semua kode di atas bersatu, sekarang kita melihat node teks dengan utas komentar yang disorot di editor.

Catatan : Pengguna saat ini tidak dapat mengetahui apakah teks tertentu memiliki komentar yang tumpang tindih. Seluruh rentang teks yang disorot tampak seperti utas komentar tunggal. Kami membahasnya nanti di artikel di mana kami memperkenalkan konsep utas komentar aktif yang memungkinkan pengguna memilih utas komentar tertentu dan dapat melihat jangkauannya di editor.

Penyimpanan UI Untuk Komentar

Sebelum kami menambahkan fungsionalitas yang memungkinkan pengguna memasukkan komentar baru, pertama-tama kami menyiapkan status UI untuk menahan utas komentar kami. Dalam artikel ini, kami menggunakan RecoilJS sebagai perpustakaan manajemen negara kami untuk menyimpan utas komentar, komentar yang terkandung di dalam utas dan metadata lain seperti waktu pembuatan, status, penulis komentar, dll. Mari tambahkan Recoil ke aplikasi kami:

 > yarn add recoil

Kami menggunakan atom Recoil untuk menyimpan dua struktur data ini. Jika Anda tidak terbiasa dengan Recoil, atomlah yang memegang status aplikasi. Untuk bagian yang berbeda dari status aplikasi, Anda biasanya ingin menyiapkan atom yang berbeda. Keluarga Atom adalah kumpulan atom – dapat dianggap sebagai Map dari kunci unik yang mengidentifikasi atom ke atom itu sendiri. Ada baiknya mempelajari konsep inti Mundur pada saat ini dan membiasakan diri dengan mereka.

Untuk kasus penggunaan kami, kami menyimpan utas komentar sebagai keluarga Atom dan kemudian membungkus aplikasi kami dalam komponen RecoilRoot RecoilRoot diterapkan untuk memberikan konteks di mana nilai atom akan digunakan. Kami membuat modul CommentState terpisah yang menyimpan definisi atom Recoil kami saat kami menambahkan lebih banyak definisi atom nanti di artikel.

 # src/utils/CommentState.js import { atom, atomFamily } from "recoil"; export const commentThreadsState = atomFamily({ key: "commentThreads", default: [], }); export const commentThreadIDsState = atom({ key: "commentThreadIDs", default: new Set([]), });

Penting untuk menyebutkan beberapa hal tentang definisi atom ini:

  • Setiap keluarga atom / atom secara unik diidentifikasi oleh sebuah key dan dapat diatur dengan nilai default.
  • Saat kita membangun lebih jauh dalam artikel ini, kita akan membutuhkan cara untuk mengulang semua utas komentar yang pada dasarnya berarti membutuhkan cara untuk mengulangi keluarga atom commentThreadsState Pada saat menulis artikel ini, cara melakukannya dengan Recoil adalah dengan memasang atom lain yang menyimpan semua ID dari keluarga atom. Kami melakukannya dengan commentThreadIDsState atas. Kedua atom ini harus tetap sinkron setiap kali kita menambah / menghapus utas komentar.

Kami menambahkan RecoilRoot App root kami sehingga kami dapat menggunakan atom-atom ini nanti. Dokumentasi Recoil juga menyediakan komponen Debugger bermanfaat yang kita ambil apa adanya dan masuk ke editor kita. Komponen ini akan meninggalkan console.debug ke konsol Dev kami karena atom Recoil diperbarui secara real-time.

 # src/components/App.js import { RecoilRoot } from "recoil"; export default function App() { ... return ( <RecoilRoot> > ... <Editor document={document} onChange={updateDocument} /> </RecoilRoot> ); }
 # src/components/Editor.js export default function Editor({ ... }): JSX.Element { ..... return ( <> <Slate> ..... </Slate> <DebugObserver /> </> ); function DebugObserver(): React.Node { // see API link above for implementation. }

Kita juga perlu menambahkan kode yang menginisialisasi atom kita dengan utas komentar yang sudah ada di dokumen (yang kita tambahkan ke dokumen contoh kita di bagian sebelumnya, misalnya). Kami melakukannya di lain waktu ketika kami membangun Bilah Sisi Komentar yang perlu membaca semua utas komentar dalam dokumen.

Pada titik ini, kami memuat aplikasi kami, pastikan tidak ada kesalahan yang mengarah ke pengaturan Recoil kami dan lanjutkan.

Menambahkan Komentar Baru

Di bagian ini, kami menambahkan tombol ke toolbar yang memungkinkan pengguna menambahkan komentar (yaitu membuat utas komentar baru) untuk rentang teks yang dipilih. Ketika pengguna memilih rentang teks dan mengklik tombol ini, kita perlu melakukan hal di bawah ini:

  1. Tetapkan ID unik ke utas komentar baru yang sedang dimasukkan.
  2. Tambahkan tanda baru ke struktur dokumen Slate dengan ID sehingga pengguna melihat teks itu disorot.
  3. Tambahkan utas komentar baru ke Recoil atom yang kita buat di bagian sebelumnya.

Mari tambahkan fungsi util ke EditorCommentUtils yang melakukan # 1 dan # 2.

 # src/utils/EditorCommentUtils.js import { Editor } from "slate"; import { v4 as uuidv4 } from "uuid"; export function insertCommentThread(editor, addCommentThreadToState) { const threadID = uuidv4(); const newCommentThread = { // comments as added would be appended to the thread here. comments: [], creationTime: new Date(), // Newly created comment threads are OPEN. We deal with statuses // later in the article. status: "open", }; addCommentThreadToState(threadID, newCommentThread); Editor.addMark(editor, getMarkForCommentThreadID(threadID), true); return threadID; }

Dengan menggunakan konsep tanda untuk menyimpan setiap utas komentar sebagai tandanya sendiri, kita cukup menggunakan Editor.addMark untuk menambahkan utas komentar baru pada rentang teks yang dipilih. Panggilan ini sendiri menangani semua kasus berbeda dalam menambahkan komentar – beberapa di antaranya telah kami jelaskan di bagian sebelumnya – komentar yang sebagian tumpang tindih, komentar di dalam / tautan yang tumpang tindih, komentar di atas teks tebal / miring, komentar yang mencakup paragraf, dan sebagainya. Panggilan API ini menyesuaikan hierarki node untuk membuat node teks baru sebanyak yang diperlukan untuk menangani kasus ini.

addCommentThreadToState adalah fungsi panggilan balik yang menangani langkah # 3 – menambahkan utas komentar baru ke atom Recoil. Kami mengimplementasikan itu selanjutnya sebagai hook callback kustom sehingga dapat digunakan kembali. Callback ini perlu menambahkan utas komentar baru ke kedua atom – commentThreadsState dan commentThreadIDsState . Untuk dapat melakukan ini, kami menggunakan hook useRecoilCallback Hook ini dapat digunakan untuk membuat callback yang mendapatkan beberapa hal yang dapat digunakan untuk membaca / mengatur data atom. Yang kami minati sekarang adalah set yang dapat digunakan untuk memperbarui nilai atom sebagai set(atom, newValueOrUpdaterFunction) .

 # src/hooks/useAddCommentThreadToState.js import { commentThreadIDsState, commentThreadsState, } from "../utils/CommentState"; import { useRecoilCallback } from "recoil"; export default function useAddCommentThreadToState() { return useRecoilCallback( ({ set }) => (id, threadData) => { set(commentThreadIDsState, (ids) => new Set([...Array.from(ids), id])); set(commentThreadsState(id), threadData); }, [] ); }

Panggilan pertama untuk set menambahkan ID baru ke kumpulan ID utas komentar yang ada dan mengembalikan Set baru (yang menjadi nilai baru atom).

Dalam panggilan kedua, kita mendapatkan atom untuk ID dari keluarga atom – commentThreadsState sebagai commentThreadsState(id) dan kemudian menyetel threadData menjadi nilainya. atomFamilyName(atomID) adalah cara Recoil memungkinkan kita mengakses atom dari keluarga atomnya menggunakan kunci unik. Secara longgar, kita dapat mengatakan bahwa jika commentThreadsState adalah Peta javascript, panggilan ini pada dasarnya adalah – commentThreadsState.set(id, threadData) .

Sekarang kita memiliki semua pengaturan kode ini untuk menangani penyisipan utas komentar baru ke dokumen dan atom Recoil, mari tambahkan tombol ke toolbar kita dan sambungkan dengan panggilan ke fungsi-fungsi ini.

 # src/components/Toolbar.js import { insertCommentThread } from "../utils/EditorCommentUtils"; import useAddCommentThreadToState from "../hooks/useAddCommentThreadToState"; export default function Toolbar({ selection, previousSelection }) { const editor = useEditor(); ... const addCommentThread = useAddCommentThreadToState(); const onInsertComment = useCallback(() => { const newCommentThreadID = insertCommentThread(editor, addCommentThread); }, [editor, addCommentThread]); return ( <div className="toolbar"> ... <ToolBarButton isActive={false} label={<i className={ bi ${getIconForButton("comment")} } />} onMouseDown={onInsertComment} /> </div> ); }

Catatan : Kami menggunakan onMouseDown dan bukan onClick yang akan membuat editor kehilangan fokus dan pemilihan menjadi null . Kami telah membahasnya dengan sedikit lebih detail di bagian penyisipan tautan di artikel pertama .

Dalam contoh di bawah ini, kita melihat penyisipan beraksi untuk utas komentar sederhana dan utas komentar yang tumpang tindih dengan tautan. Perhatikan bagaimana kami mendapatkan pembaruan dari Recoil Debugger yang mengonfirmasi bahwa status kami diperbarui dengan benar. Kami juga memverifikasi bahwa node teks baru dibuat saat utas ditambahkan ke dokumen.

Dalam contoh di atas, pengguna menyisipkan utas komentar berikut dalam urutan itu:

  1. Urutan Komentar # 1 atas karakter 'B' (panjang = 1).
  2. Thread Komentar # 2 atas 'AB' (panjang = 2).
  3. Thread Komentar # 3 di atas 'BC' (panjang = 2).

Di akhir penyisipan ini, karena cara Slate membagi node teks dengan tanda, kita akan memiliki tiga node teks – satu untuk setiap karakter. Sekarang, jika pengguna mengklik 'B', mengikuti aturan panjang terpendek, kami memilih utas # 1 karena ini adalah yang terpendek dari ketiganya. Jika kami tidak melakukan itu, kami tidak akan memiliki cara untuk memilih Thread Komentar # 1 karena panjangnya hanya satu karakter dan juga merupakan bagian dari dua utas lainnya.

Meskipun aturan ini memudahkan untuk menampilkan utas komentar dengan panjang yang lebih pendek, kami dapat mengalami situasi di mana utas komentar yang lebih panjang menjadi tidak dapat diakses karena semua karakter yang ada di dalamnya adalah bagian dari utas komentar pendek lainnya. Mari kita lihat contoh untuk itu.

Mari kita asumsikan kita memiliki 100 karakter (katakanlah, karakter 'A' diketik 100 kali) dan pengguna memasukkan utas komentar dalam urutan berikut:

  1. Urutan Komentar # 1 dari kisaran 20,80
  2. Thread Komentar # 2 dari kisaran 0,50
  3. Urutan Komentar # 3 dari kisaran 51.100

Seperti yang Anda lihat pada contoh di atas, jika kita mengikuti aturan yang baru saja kita jelaskan di sini, mengklik karakter apa pun antara # 20 dan # 80, akan selalu memilih utas # 2 atau # 3 karena lebih pendek dari # 1 dan karenanya # 1 tidak akan bisa dipilih. Skenario lain di mana aturan ini dapat membuat kita ragu-ragu tentang utas komentar mana yang harus dipilih adalah ketika ada lebih dari satu utas komentar dengan panjang terpendek yang sama pada simpul teks.

Untuk kombinasi komentar yang tumpang tindih dan banyak kombinasi lainnya yang dapat dipikirkan orang di mana mengikuti aturan ini membuat utas komentar tertentu tidak dapat diakses dengan mengklik teks, kami membangun Sidebar Komentar nanti di artikel ini yang memberikan pengguna pandangan dari semua utas komentar hadir di dokumen sehingga mereka dapat mengklik utas tersebut di bilah sisi dan mengaktifkannya di editor untuk melihat rentang komentar. Kami masih ingin memiliki aturan ini dan menerapkannya karena harus mencakup banyak skenario tumpang tindih kecuali untuk contoh yang kemungkinan kecil kami kutip di atas. Kami melakukan semua upaya ini di sekitar aturan ini terutama karena melihat teks yang disorot di editor dan mengkliknya untuk berkomentar adalah cara yang lebih intuitif untuk mengakses komentar pada teks daripada hanya menggunakan daftar komentar di bilah sisi.

Aturan Penyisipan

Aturannya adalah:

"Jika pengguna teks telah memilih dan mencoba mengomentari sudah sepenuhnya tercakup oleh utas komentar, jangan izinkan penyisipan itu."

Ini karena jika kami mengizinkan penyisipan ini, setiap karakter dalam rentang itu akan memiliki setidaknya dua utas komentar (satu yang sudah ada dan yang baru yang baru saja kami izinkan) sehingga sulit bagi kami untuk menentukan mana yang akan dipilih ketika pengguna mengklik karakter itu nanti.

Melihat aturan ini, orang mungkin bertanya-tanya mengapa kita membutuhkannya jika kita sudah memiliki Aturan Rentang Komentar Terpendek yang memungkinkan kita memilih rentang teks terkecil. Mengapa tidak mengizinkan semua kombinasi tumpang tindih jika kita dapat menggunakan aturan pertama untuk menyimpulkan utas komentar yang tepat untuk ditampilkan? Seperti beberapa contoh yang telah kita bahas sebelumnya, aturan pertama berfungsi untuk banyak skenario, tetapi tidak semuanya. Dengan Aturan Penyisipan, kami mencoba meminimalkan jumlah skenario di mana aturan pertama tidak dapat membantu kami dan kami harus mundur di Sidebar sebagai satu-satunya cara bagi pengguna untuk mengakses utas komentar tersebut. Aturan Penyisipan juga mencegah tumpang tindih utas komentar. Aturan ini biasanya diterapkan oleh banyak editor populer.

Di bawah ini adalah contoh di mana jika aturan ini tidak ada, kami akan mengizinkan Thread Komentar # 3 dan sebagai akibat dari aturan pertama, # 3 tidak akan dapat diakses karena akan menjadi yang terpanjang panjangnya.

Dalam contoh ini, mari kita asumsikan kita tidak menunggu perpotongan menjadi 0 dan berhenti begitu kita mencapai tepi utas komentar. Sekarang, jika pengguna mengklik # 2 dan kami memulai traversal dalam arah sebaliknya, kami akan berhenti di awal node teks # 2 itu sendiri karena itu adalah awal dari utas komentar A. Akibatnya, kami mungkin tidak menghitung komentar panjang utas dengan benar untuk A & B. Dengan implementasi di atas melintasi tepi terjauh (simpul teks 1,2, dan 3), kita harus mendapatkan B sebagai utas komentar terpendek seperti yang diharapkan.

Untuk melihat implementasinya secara visual, di bawah ini adalah panduan dengan slideshow dari iterasi. Kami memiliki dua utas komentar A dan B yang tumpang tindih satu sama lain di atas simpul teks # 3 dan pengguna mengeklik pada simpul teks yang tumpang tindih # 3.

Sekarang kita memiliki semua kode untuk membuat pilihan utas komentar berfungsi, mari kita lihat beraksi. Untuk menguji kode traversal kami dengan baik, kami menguji beberapa kasus langsung tumpang tindih dan beberapa kasus edge seperti:

  • Mengklik node teks yang diberi komentar di awal / akhir editor.
  • Mengklik node teks yang diberi komentar dengan utas komentar yang mencakup beberapa paragraf.
  • Mengklik node teks yang diberi komentar tepat sebelum node gambar.
  • Mengklik link tumpang tindih node teks yang diberi komentar.

Sekarang status kita diinisialisasi dengan benar, kita dapat mulai menerapkan sidebar. Semua utas komentar kami di UI disimpan dalam keluarga atom Recoil – commentThreadsState . Seperti yang disorot sebelumnya, cara kami mengulang semua item dalam keluarga atom Recoil adalah dengan melacak kunci / id atom di atom lain. Kami telah melakukan itu dengan commentThreadIDsState . Mari tambahkan CommentSidebar yang melakukan iterasi melalui kumpulan id di atom ini dan membuat CommentThread untuk masing-masing.

 # src/components/CommentsSidebar.js import "./CommentSidebar.css"; import {commentThreadIDsState,} from "../utils/CommentState"; import { useRecoilValue } from "recoil"; export default function CommentsSidebar(params) { const allCommentThreadIDs = useRecoilValue(commentThreadIDsState); return ( <Card className={"comments-sidebar"}> <Card.Header>Comments</Card.Header> <Card.Body> {Array.from(allCommentThreadIDs).map((id) => ( <Row key={id}> <Col> <CommentThread id={id} /> </Col> </Row> ))} </Card.Body> </Card> ); }

Sekarang, kami menerapkan CommentThread yang mendengarkan atom Recoil dalam keluarga yang sesuai dengan utas komentar yang dirender. Dengan cara ini, saat pengguna menambahkan lebih banyak komentar pada utas di editor atau mengubah metadata lainnya, kami dapat memperbarui bilah sisi untuk mencerminkannya.

Karena sidebar dapat menjadi sangat besar untuk dokumen dengan banyak komentar, kami menyembunyikan semua komentar kecuali yang pertama saat kami merender sidebar. Pengguna dapat menggunakan tombol 'Tampilkan / Sembunyikan Balasan' untuk menampilkan / menyembunyikan seluruh utas komentar.

 # src/components/CommentSidebar.js function CommentThread({ id }) { const { comments } = useRecoilValue(commentThreadsState(id)); const [shouldShowReplies, setShouldShowReplies] = useState(false); const onBtnClick = useCallback(() => { setShouldShowReplies(!shouldShowReplies); }, [shouldShowReplies, setShouldShowReplies]); if (comments.length === 0) { return null; } const [firstComment, ...otherComments] = comments; return ( <Card body={true} className={classNames({ "comment-thread-container": true, })} > <CommentRow comment={firstComment} showConnector={false} /> {shouldShowReplies ? otherComments.map((comment, index) => ( <CommentRow key={ comment-${index} } comment={comment} showConnector={true} /> )) : null} {comments.length > 1 ? ( <Button className={"show-replies-btn"} size="sm" variant="outline-primary" onClick={onBtnClick} > {shouldShowReplies ? "Hide Replies" : "Show Replies"} </Button> ) : null} </Card> ); }

Kami telah menggunakan kembali CommentRow dari popover meskipun kami menambahkan perlakuan desain menggunakan showConnector yang pada dasarnya membuat semua komentar terlihat terhubung dengan utas di sidebar.

Sekarang, kami merender CommentSidebar di Editor dan memverifikasi bahwa itu menunjukkan semua utas yang kami miliki di dokumen dan memperbarui dengan benar saat kami menambahkan utas baru atau komentar baru ke utas yang ada.

 # src/components/Editor.js return ( <> <Slate ... > ..... <div className={"sidebar-wrapper"}> <CommentsSidebar /> </div> </Slate> </> );

Kami sekarang beralih ke penerapan interaksi Bilah Sisi Komentar populer yang ditemukan di editor:

Mengklik utas komentar di bar samping harus memilih / mengaktifkan utas komentar itu. Kami juga menambahkan perlakuan desain diferensial untuk menyorot utas komentar di bilah sisi jika aktif di editor. Untuk dapat melakukannya, kami menggunakan Recoil atom – activeCommentThreadIDAtom . Mari perbarui CommentThread untuk mendukung ini.

 # src/components/CommentsSidebar.js function CommentThread({ id }) { const [activeCommentThreadID, setActiveCommentThreadID] = useRecoilState( activeCommentThreadIDAtom ); const onClick = useCallback(() => {
setActiveCommentThreadID(id); }, [id, setActiveCommentThreadID]); ... return ( <Card body={true} className={classNames({ "comment-thread-container": true, "is-active": activeCommentThreadID === id,
})} onClick={onClick} > .... </Card> );

Jika kita melihat lebih dekat, kita memiliki bug dalam implementasi sinkronisasi utas komentar aktif dengan sidebar. Saat kami mengklik utas komentar yang berbeda di bilah sisi, utas komentar yang benar memang disorot di editor. Namun, Popover Komentar tidak benar-benar pindah ke utas komentar aktif yang diubah. Itu tetap di tempat itu pertama kali diberikan. Jika kita melihat implementasi dari Popover Komentar, itu membuat dirinya sendiri terhadap simpul teks pertama dalam pemilihan editor. Pada saat itu dalam penerapan, satu-satunya cara untuk memilih utas komentar adalah dengan mengeklik simpul teks sehingga kami dapat dengan mudah mengandalkan pilihan editor karena itu diperbarui oleh Slate sebagai hasil dari peristiwa klik. Dalam onClick atas, kami tidak memperbarui pilihan tetapi hanya memperbarui nilai atom Recoil yang menyebabkan pilihan Slate tetap tidak berubah dan karenanya Popover Komentar tidak bergerak.

Solusi untuk masalah ini adalah memperbarui pilihan editor bersama dengan memperbarui atom Recoil ketika pengguna mengklik utas komentar di sidebar. Langkah-langkah melakukan ini adalah:

  1. Temukan semua node teks yang memiliki utas komentar ini yang akan kita atur sebagai utas aktif baru.
  2. Urutkan node teks ini dalam urutan kemunculannya di dokumen (Kami menggunakan Path.compare Slate untuk ini).
  3. Hitung rentang pilihan yang membentang dari awal node teks pertama hingga akhir node teks terakhir.
  4. Setel rentang pilihan menjadi pilihan baru editor (menggunakan Transforms.select Slate).

Jika kita hanya ingin memperbaiki bug, kita bisa menemukan node teks pertama di Langkah # 1 yang memiliki utas komentar dan mengaturnya menjadi pilihan editor. Namun, ini terasa seperti pendekatan yang lebih bersih untuk memilih seluruh rentang komentar karena kami benar-benar memilih utas komentar.

Mari perbarui onClick untuk menyertakan langkah-langkah di atas.

 const onClick = useCallback(() => { const textNodesWithThread = Editor.nodes(editor, { at: [], mode: "lowest", match: (n) => Text.isText(n) && getCommentThreadsOnTextNode(n).has(id), }); let textNodeEntry = textNodesWithThread.next().value; const allTextNodePaths = []; while (textNodeEntry != null) { allTextNodePaths.push(textNodeEntry[1]); textNodeEntry = textNodesWithThread.next().value; } // sort the text nodes allTextNodePaths.sort((p1, p2) => Path.compare(p1, p2)); // set the selection on the editor Transforms.select(editor, { anchor: Editor.point(editor, allTextNodePaths[0], { edge: "start" }), focus: Editor.point( editor, allTextNodePaths[allTextNodePaths.length - 1], { edge: "end" } ), }); // Update the Recoil atom value. setActiveCommentThreadID(id); }, [editor, id, setActiveCommentThreadID]);

Catatan : allTextNodePaths berisi jalur ke semua node teks. Kami menggunakan Editor.point untuk mendapatkan titik awal dan akhir di jalur itu. Artikel pertama membahas konsep Lokasi Slate. Mereka juga terdokumentasi dengan baik pada Slate dokumentasi .

Mari kita verifikasi bahwa implementasi ini memperbaiki bug dan Popover Komentar berpindah ke utas komentar aktif dengan benar. Kali ini, kami juga menguji kasus utas yang tumpang tindih untuk memastikan tidak rusak di sana.

Dengan perbaikan bug, kami telah mengaktifkan interaksi bilah sisi lain yang belum kami diskusikan. Jika kami memiliki dokumen yang sangat panjang dan pengguna mengklik utas komentar di sidebar yang berada di luar viewport, kami ingin menggulir ke bagian dokumen itu sehingga pengguna dapat fokus pada utas komentar di editor. Dengan mengatur pilihan di atas menggunakan API Slate, kami mendapatkannya secara gratis. Mari kita lihat aksinya di bawah ini.

Dengan itu, kami membungkus implementasi sidebar kami. Menjelang akhir artikel, kami mencantumkan beberapa penambahan dan peningkatan fitur bagus yang dapat kami lakukan ke Bilah Sisi Komentar yang membantu meningkatkan pengalaman Mengomentari dan Meninjau di editor.

Menyelesaikan Dan Membuka Kembali Komentar

Di bagian ini, kami fokus untuk memungkinkan pengguna menandai utas komentar sebagai 'Terselesaikan' atau dapat membukanya kembali untuk diskusi jika diperlukan. Dari perspektif detail implementasi, ini adalah status metadata pada utas komentar yang kami ubah saat pengguna melakukan tindakan ini. Dari sudut pandang pengguna, ini adalah fitur yang sangat berguna karena memberi mereka cara untuk menegaskan bahwa pembahasan tentang sesuatu di dokumen telah selesai atau perlu dibuka kembali karena ada beberapa pembaruan / perspektif baru, dan sebagainya.

Untuk mengaktifkan toggling status, kami menambahkan tombol ke CommentPopover yang memungkinkan pengguna untuk beralih di antara dua status: open dan resolved .

 # src/components/CommentThreadPopover.js export default function CommentThreadPopover({ editorOffsets, selection, threadID, }) { … const [threadData, setCommentThreadData] = useRecoilState( commentThreadsState(threadID) ); ... const onToggleStatus = useCallback(() => { const currentStatus = threadData.status; setCommentThreadData((threadData) => ({ ...threadData, status: currentStatus === "open" ? "resolved" : "open", })); }, [setCommentThreadData, threadData.status]); return ( <NodePopover ... header={ <Header status={threadData.status} shouldAllowStatusChange={threadData.comments.length > 0} onToggleStatus={onToggleStatus} /> } > <div className={"comment-list"}> ... </div> </NodePopover> ); } function Header({ onToggleStatus, shouldAllowStatusChange, status }) { return ( <div className={"comment-thread-popover-header"}> {shouldAllowStatusChange && status != null ? ( <Button size="sm" variant="primary" onClick={onToggleStatus}> {status === "open" ? "Resolve" : "Re-Open"} </Button> ) : null} </div> ); }

Sebelum kita mengujinya, mari kita juga memberi Bilah Sisi Komentar perlakuan desain yang berbeda untuk komentar yang diselesaikan sehingga pengguna dapat dengan mudah mendeteksi utas komentar mana yang tidak terselesaikan atau terbuka dan fokus pada itu jika mereka mau.

 # src/components/CommentsSidebar.js function CommentThread({ id }) { ... const { comments, status } = useRecoilValue(commentThreadsState(id)); ... return ( <Card body={true} className={classNames({ "comment-thread-container": true, "is-resolved": status === "resolved", "is-active": activeCommentThreadID === id, })} onClick={onClick} > ...
</Card> ); }

Kesimpulan

Dalam artikel ini, kami membangun infrastruktur UI inti untuk Sistem Komentar pada Editor Teks Kaya. Kumpulan fungsi yang kami tambahkan di sini bertindak sebagai fondasi untuk membangun Pengalaman Kolaborasi yang lebih kaya pada editor tempat kolaborator dapat membuat anotasi bagian dokumen dan melakukan percakapan tentangnya. Menambahkan Bilah Sisi Komentar memberi kami ruang untuk memiliki lebih banyak fungsi percakapan atau berbasis ulasan untuk diaktifkan pada produk.

Sejalan dengan itu, berikut adalah beberapa fitur yang dapat dipertimbangkan untuk ditambahkan oleh Editor Teks Kaya selain dari apa yang kami buat di artikel ini:

  • Dukungan untuk @ menyebutkan sehingga kolaborator bisa tag satu sama lain dalam komentar;
  • Dukungan untuk jenis media seperti gambar dan video yang akan ditambahkan ke utas komentar;
  • Mode Saran di tingkat dokumen yang memungkinkan peninjau mengedit dokumen yang muncul sebagai saran untuk perubahan. Seseorang dapat merujuk ke fitur ini di Google Docs atau Ubah Pelacakan di Microsoft Word sebagai contoh;
  • Penyempurnaan pada bilah sisi untuk mencari percakapan berdasarkan kata kunci, memfilter utas berdasarkan status atau penulis komentar, dan sebagainya.
May 28, 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 *