WYSIWYG Düzenleyicisine Yorumlama Sistemi Ekleme
Yayınlanan: 2022-03-10Son yıllarda, İşbirliğinin birçok meslekte çok sayıda dijital iş akışına ve kullanım örneklerine nüfuz ettiğini gördük. Sadece Tasarım ve Yazılım Mühendisliği topluluğu içinde, tasarımcıların Figma gibi araçları kullanarak tasarım eserleri üzerinde işbirliği yaptığını, Sprint ve Proje Planlaması yapan ekiplerin Mural gibi araçları ve CoderPad kullanılarak yapılan röportajları görüyoruz. Tüm bu araçlar, sürekli olarak bu iş akışlarını yürütmek için çevrimiçi ve fiziksel dünya deneyimi arasındaki boşluğu kapatmayı ve işbirliği deneyimini olabildiğince zengin ve kusursuz hale getirmeyi hedefliyor.
Bunun gibi İşbirliği Araçlarının çoğu için, birbirleriyle fikir paylaşma ve aynı içerik hakkında tartışmalar yapma yeteneği bir zorunluluktur. Ortak çalışanların bir belgenin bölümlerine açıklama eklemesini ve bunlar hakkında konuşmalar yapmasını sağlayan bir Yorumlama Sistemi bu konseptin merkezinde yer alır. Makale, WYSIWYG Düzenleyicide metin için bir tane oluşturmanın yanı sıra, WYSIWYG Düzenleyicileri veya Genel olarak Kelime İşlemciler.
Yorumların Belge Yapısında Temsil Edilmesi
Bir zengin metin belgesinin veri yapısındaki yorumları temsil etmenin bir yolunu bulmak için, bir düzenleyici içinde yorumların oluşturulabileceği birkaç senaryoya bakalım.
- Üzerinde stil olmayan metinler üzerinde oluşturulan yorumlar (temel senaryo);
- Kalın/italik/altı çizili olabilecek metin üzerinde oluşturulan yorumlar vb;
- Bir şekilde birbiriyle örtüşen yorumlar (iki yorumun yalnızca birkaç kelimeyi paylaştığı durumlarda kısmi örtüşme veya bir yorumun metninin başka bir yorumun metni içinde tamamen yer aldığı durumlarda tam kapsamlı);
- Bir bağlantının içindeki metin üzerinde oluşturulan yorumlar (bağlar, belge yapımızdaki düğümlerin kendileri olduğu için özeldir);
- Birden çok paragrafa yayılan yorumlar (paragraflar belge yapımızdaki düğümler olduğundan ve yorumlar paragrafın alt öğeleri olan metin düğümlerine uygulandığından özeldir).
Yukarıdaki kullanım örneklerine bakıldığında, zengin bir metin belgesinde bulunabilecekleri şekilde yorumların karakter stillerine (kalın, italik vb.) çok benzediği görülüyor. Birbirleriyle örtüşebilir, bağlantılar gibi diğer düğüm türlerinde metnin üzerinden geçebilir ve hatta paragraflar gibi birden çok ana düğüme yayılabilirler.
Bu nedenle, yorumları temsil etmek için karakter stilleri için kullandığımız yöntemin aynısını kullanıyoruz, yani “İşaretler” (SlateJS terminolojisinde bu şekilde adlandırıldığı gibi). İşaretler yalnızca düğümlerdeki normal özelliklerdir - Slate'in işaretler etrafındaki API'sinin ( Editor.addMark
ve Editor.removeMark
), aynı metin aralığına birden çok işaret uygulandığında düğüm hiyerarşisinin değiştirilmesini yönetmesidir. Çakışan yorumların birçok farklı kombinasyonuyla uğraştığımız için bu bizim için son derece yararlıdır.
İşaret Olarak Yorum Konuları
Bir kullanıcı bir metin aralığı seçip teknik olarak bir yorum eklemeye çalıştığında, o metin aralığı için yeni bir yorum dizisi başlatır. Yorum eklemelerine ve daha sonra bu yoruma yanıt vermelerine izin verdiğimiz için, bu olayı belgede yeni bir yorum dizisi eklemesi olarak değerlendiririz.
Yorum dizilerini işaretler olarak gösterme şeklimiz, her yorum commentThread_threadID
olarak adlandırılan bir işaretle temsil edilmesidir; burada threadID
, her yorum dizisine atadığımız benzersiz bir kimliktir. Bu nedenle, aynı metin aralığının üzerinde iki yorum dizisi varsa, true
olarak ayarlanmış iki özelliğe sahip olur - commentThread_thread1
ve commentThread_thread2
. Yorum dizilerinin karakter stillerine çok benzediği yer burasıdır, çünkü aynı metin kalın ve italik olsaydı, özelliklerin ikisi de true
- bold
ve italic
olarak ayarlanmış olurdu.
Bu yapıyı gerçekten kurmaya başlamadan önce, yorum dizileri uygulandıkça metin düğümlerinin nasıl değiştiğine bakmakta fayda var. Bunun çalışma şekli (herhangi bir işarette olduğu gibi), seçili metin üzerinde bir işaret özelliği ayarlandığında, Slate'in Editor.addMark API'si gerekirse metin düğümlerini böler, böylece ortaya çıkan yapıda metin düğümleri her metin düğümü, işaretin tam olarak aynı değerine sahip olacak şekilde kurulur.
Bunu daha iyi anlamak için, seçilen metne bir yorum dizisi eklendikten sonra metin düğümlerinin önceki ve sonraki durumunu gösteren aşağıdaki üç örneğe bakın:
Yorumlanan Metni Vurgulama
Artık belge yapısında yorumları nasıl temsil edeceğimizi bildiğimize göre, devam edelim ve ilk makaledeki örnek belgeye birkaç tane ekleyelim ve düzenleyiciyi bunları vurgulanmış olarak gösterecek şekilde yapılandıralım. Bu makaledeki yorumlarla ilgilenecek çok sayıda yardımcı fonksiyona sahip olacağımız için, tüm bu araçları barındıracak bir EditorCommentUtils
modülü oluşturuyoruz. Başlangıç olarak, belirli bir yorum dizisi kimliği için bir işaret oluşturan bir işlev oluşturuyoruz. Daha sonra bunu ExampleDocument
birkaç yorum dizisi eklemek için kullanırız.
# src/utils/EditorCommentUtils.js const COMMENT_THREAD_PREFIX = "commentThread_"; export function getMarkForCommentThreadID(threadID) { return `${COMMENT_THREAD_PREFIX}${threadID}`; }
Aşağıdaki resim, bir sonraki kod parçacığına eklenen örnek yorum dizileri olarak sahip olduğumuz metin aralıklarının altını kırmızıyla çiziyor. 'Richard McClintock' metninin birbiriyle örtüşen iki yorum dizisine sahip olduğunu unutmayın. Spesifik olarak, bu, bir yorum dizisinin tamamen diğerinin içinde yer alması durumudur.
# 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, }, ... ];
Bu makalede bir Yorumlama Sisteminin UI tarafına odaklanıyoruz, bu nedenle onlara örnek belgede doğrudan npm paketi uuid'i kullanarak kimlikler atadık. Bir düzenleyicinin üretim sürümünde bu kimliklerin bir arka uç hizmeti tarafından oluşturulma olasılığı çok yüksektir.
Şimdi, bu metin düğümlerini vurgulanmış olarak göstermek için düzenleyiciyi değiştirmeye odaklanıyoruz. Bunu yapmak için, metin düğümlerini işlerken, üzerinde yorum dizileri olup olmadığını anlamanın bir yoluna ihtiyacımız var. Bunun için bir getCommentThreadsOnTextNode
util ekleriz. Yorumlar içeren bir metin düğümü oluşturmaya çalıştığı durumu ele almak için ilk makalede oluşturduğumuz StyledText
bileşenini geliştirdik. Daha sonra yorumlanmış metin düğümlerine eklenecek daha fazla işlevselliğe sahip olduğumuz için, yorumlanmış metni işleyen bir CommentedText
bileşeni oluşturuyoruz. StyledText
, oluşturmaya çalıştığı metin düğümünün üzerinde herhangi bir yorum olup olmadığını kontrol eder. Varsa, CommentedText
işler. Bunu anlamak için getCommentThreadsOnTextNode
util kullanır.
# 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; }
İlk makale, metin düğümlerini (karakter stillerini işleme vb.) işleyen bir StyledText
bileşeni oluşturdu. Bu bileşeni, yukarıdaki kullanımı kullanacak şekilde genişletiriz ve düğümün üzerinde yorumlar varsa, bir CommentedText
bileşeni oluştururuz.
# 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>; }
Aşağıda, metin düğümünü oluşturan ve onu vurgulanmış olarak gösteren CSS'yi ekleyen CommentedText
uygulaması yer almaktadır.
# 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; }
Yukarıdaki kodların tümü bir araya geldiğinde, şimdi düzenleyicide vurgulanmış yorum dizilerine sahip metin düğümleri görüyoruz.
Not : Kullanıcılar şu anda belirli bir metnin üzerinde çakışan yorumlar olup olmadığını anlayamaz. Vurgulanan metin aralığının tamamı tek bir yorum dizisi gibi görünür. Bunu, kullanıcıların belirli bir yorum dizisini seçmesine ve düzenleyicide aralığını görebilmesine olanak tanıyan etkin yorum dizisi kavramını tanıttığımız makalenin ilerleyen bölümlerinde ele alacağız.
Yorumlar için UI Depolaması
Bir kullanıcının yeni yorumlar eklemesini sağlayan işlevselliği eklemeden önce, yorum dizilerimizi tutmak için bir UI durumu kurarız. Bu yazıda, yorum dizilerini, dizilerin içerdiği yorumları ve oluşturma zamanı, durumu, yorum yazarı vb. diğer meta verileri depolamak için durum yönetimi kitaplığımız olarak RecoilJS kullanıyoruz. Uygulamamıza Recoil'i ekleyelim:
> yarn add recoil
Bu iki veri yapısını depolamak için Geri Tepme atomlarını kullanıyoruz. Geri Tepmeye aşina değilseniz, uygulama durumunu tutan atomlardır. Farklı uygulama durumları için, genellikle farklı atomlar kurmak istersiniz. Atom Ailesi bir atom topluluğudur - atomu tanımlayan benzersiz bir anahtardan atomların kendilerine kadar uzanan bir Map
olduğu düşünülebilir. Bu noktada Geri Tepme'nin temel kavramlarını gözden geçirmeye ve kendimizi onlara alıştırmaya değer.
Kullanım durumumuz için, yorum dizilerini bir Atom ailesi olarak saklıyoruz ve ardından uygulamamızı bir RecoilRoot
bileşenine sarıyoruz. Atom değerlerinin kullanılacağı bağlamı sağlamak için RecoilRoot
uygulanır. Makalenin ilerleyen kısımlarında daha fazla atom tanımı ekledikçe, Geri Tepme atom tanımlarımızı tutan ayrı bir CommentState
modülü oluşturuyoruz.
# 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([]), });
Bu atom tanımları hakkında birkaç şey söylemeye değer:
- Her atom/atom ailesi bir
key
benzersiz bir şekilde tanımlanır ve varsayılan bir değerle ayarlanabilir. - Bu makaleyi daha da geliştirdikçe, tüm yorum dizileri üzerinde yineleme yapmak için bir yola ihtiyacımız olacak, bu da temelde
commentThreadsState
atom ailesi üzerinde yineleme yapmak için bir yola ihtiyaç duymak anlamına gelecek. Bu makaleyi yazarken, bunu Recoil ile yapmanın yolu, atom ailesinin tüm kimliklerini tutan başka bir atom kurmaktır. Bunu yukarıdakicommentThreadIDsState
ile yapıyoruz. Yorum dizilerini eklediğimizde/sildiğimizde bu atomların her ikisinin de senkronize tutulması gerekir.
Bu atomları daha sonra kullanabilmemiz için kök App
bileşenimize bir RecoilRoot
sarmalayıcı ekliyoruz. Recoil'in belgeleri aynı zamanda olduğu gibi alıp düzenleyicimize bıraktığımız yardımcı bir Hata Ayıklayıcı bileşeni de sağlar. Geri Tepme atomları gerçek zamanlı olarak güncellendiğinden, bu bileşen console.debug
günlüklerini Dev konsolumuza bırakacaktır.
# 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. }
Ayrıca belgede zaten var olan yorum dizileri ile atomlarımızı başlatan bir kod eklememiz gerekiyor (örneğin, önceki bölümde örnek belgemize eklediklerimiz). Bunu daha sonra, bir belgedeki tüm yorum dizilerini okuması gereken Yorumlar Kenar Çubuğunu oluşturduğumuzda yaparız.
Bu noktada uygulamamızı yüklüyoruz, Geri Tepme kurulumumuza işaret eden herhangi bir hata olmadığından emin oluyoruz ve ilerliyoruz.
Yeni Yorumlar Ekleme
Bu bölümde, seçilen metin aralığı için kullanıcının yorum eklemesine (yani yeni bir yorum dizisi oluşturmasına) izin veren araç çubuğuna bir düğme ekliyoruz. Kullanıcı bir metin aralığı seçip bu butona tıkladığında aşağıdakileri yapmamız gerekiyor:
- Eklenmekte olan yeni yorum dizisine benzersiz bir kimlik atayın.
- Kullanıcının vurgulanan metni görmesi için Slate belge yapısına kimlikle yeni bir işaret ekleyin.
- Bir önceki bölümde oluşturduğumuz Recoil atomlarına yeni yorum dizisini ekleyin.
EditorCommentUtils
#1 ve #2 yapan bir util işlevi ekleyelim.
# 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; }
Her yorum dizisini kendi işareti olarak saklamak için işaret kavramını kullanarak, seçilen metin aralığına yeni bir yorum dizisi eklemek için Editor.addMark
API'sini kullanabiliriz. Tek başına bu çağrı, kısmen örtüşen yorumlar, içte/çakışan bağlantılar, kalın/italik metin üzerindeki yorumlar, paragrafları kapsayan yorumlar vb. - bazılarını önceki bölümde tanımladığımız tüm farklı yorum ekleme durumlarını ele alır. Bu API çağrısı, bu durumları ele almak için gerektiği kadar yeni metin düğümü oluşturmak için düğüm hiyerarşisini ayarlar.
addCommentThreadToState
, yeni yorum dizisini Recoil atomuna eklemek olan 3. adımı işleyen bir geri çağırma işlevidir. Bunu, yeniden kullanılabilir olması için özel bir geri arama kancası olarak uygularız. Bu geri aramanın yeni yorum dizisini her iki atoma da eklemesi gerekir - commentThreadsState
ve commentThreadIDsState
. Bunu yapabilmek için useRecoilCallback
kancasını kullanıyoruz. Bu kanca, atom verilerini okumak/ayarlamak için kullanılabilecek birkaç şeyi alan bir geri arama oluşturmak için kullanılabilir. Şu anda ilgilendiğimiz, bir atom değerini set(atom, newValueOrUpdaterFunction)
olarak güncellemek için kullanılabilecek set
işlevidir.
# 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); }, [] ); }
set için ilk çağrı, yeni kimliği mevcut yorum dizisi kimlikleri set
ekler ve yeni Set
(atomun yeni değeri olur) döndürür.
İkinci çağrıda, atom ailesinden ID için atomu alıyoruz - commentThreadsState
olarak commentThreadsState(id)
ve ardından threadData
değerini onun değeri olarak ayarladık. atomFamilyName(atomID)
, Recoil'in benzersiz anahtarı kullanarak atom ailesinden bir atoma erişmemize nasıl izin verdiğidir. Açıkça söylemek gerekirse, commentThreadsState
bir javascript Haritası olsaydı, bu çağrının temelde — commentThreadsState.set(id, threadData)
.
Artık belgeye yeni bir yorum dizisinin eklenmesini ve Geri Tepme atomlarını işlemek için tüm bu kod kurulumuna sahip olduğumuza göre, araç çubuğumuza bir düğme ekleyelim ve bu işlevlere yapılan çağrıyla onu bağlayalım.
# 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> ); }
Not : Düzenleyicinin odağını kaybetmesine ve seçimin null
olmasına neden olacak onClick
yerine onMouseDown
kullanıyoruz . Bunu ilk makalenin bağlantı ekleme bölümünde biraz daha ayrıntılı olarak tartıştık.
Aşağıdaki örnekte, basit bir yorum dizisi ve bağlantılarla örtüşen bir yorum dizisi için eklemenin eylem halinde olduğunu görüyoruz. Durumumuzun doğru bir şekilde güncellendiğini onaylayan Geri Tepme Hata Ayıklayıcısından güncellemeleri nasıl aldığımıza dikkat edin. Belgeye iş parçacıkları eklenirken yeni metin düğümlerinin oluşturulduğunu da doğrularız.
Çakışan Yorumlar
Yorumlama sistemimize daha fazla özellik eklemeye devam etmeden önce, çakışan yorumlarla ve bunların editördeki farklı kombinasyonlarıyla nasıl başa çıkacağımız konusunda bazı kararlar almamız gerekiyor. Buna neden ihtiyacımız olduğunu anlamak için, Yorum Popover'ın nasıl çalıştığına bir göz atalım - bu, makalenin ilerleyen bölümlerinde oluşturacağımız bir işlevsellik. Bir kullanıcı, üzerinde yorum dizileri olan belirli bir metne tıkladığında, bir yorum dizisini 'seçiyoruz' ve kullanıcının bu konuya yorum ekleyebileceği bir açılır pencere gösteriyoruz.
Yukarıdaki videodan da anlayabileceğiniz gibi, 'tasarımcılar' kelimesi artık üç yorum dizisinin bir parçası. Yani bir kelime üzerinde birbiriyle örtüşen iki yorum dizimiz var. Ve bu yorum dizilerinin her ikisi de (#1 ve #2), tamamen daha uzun bir yorum dizisi metin aralığının (#3) içinde bulunur. Bu birkaç soruyu gündeme getiriyor:
- Kullanıcı 'tasarımcılar' kelimesini tıkladığında hangi yorum dizisini seçip göstermeliyiz?
- Yukarıdaki soruyu nasıl ele almaya karar verdiğimize bağlı olarak, herhangi bir kelimeye tıklamanın belirli bir yorum dizisini asla etkinleştirmeyeceği ve diziye hiç erişilemeyeceği bir örtüşme durumumuz olur mu?
Bu, çakışan yorumlar olması durumunda, dikkate alınması gereken en önemli şeyin şudur: kullanıcı bir yorum dizisi ekledikten sonra, gelecekte içindeki bir metne tıklayarak o yorum dizisini seçebilmelerinin bir yolu var mı? o? Değilse, muhtemelen ilk etapta yerleştirmelerine izin vermek istemeyiz. Editörümüzde çoğu zaman bu ilkeye saygı gösterilmesini sağlamak için örtüşen yorumlarla ilgili iki kural getiriyor ve bunları editörümüzde uyguluyoruz.
Bu kuralları tanımlamadan önce, konu örtüşen yorumlar olduğunda farklı editörlerin ve kelime işlemcilerin farklı yaklaşımları olduğunu belirtmekte fayda var. İşleri basit tutmak için, bazı editörler örtüşen yorumlara hiçbir şekilde izin vermez. Bizim durumumuzda, çok karmaşık çakışma durumlarına izin vermeyerek ancak yine de kullanıcıların daha zengin bir İşbirliği ve İnceleme deneyimi yaşayabilmeleri için çakışan yorumlara izin vererek bir orta yol bulmaya çalışıyoruz.
En Kısa Yorum Aralığı Kuralı
Bu kural, bir kullanıcı üzerinde birden fazla yorum dizisi olan bir metin düğümünü tıkladığında hangi yorum dizisinin seçileceğine ilişkin yukarıdaki 1. soruyu yanıtlamamıza yardımcı olur. Kural:
"Kullanıcı, üzerinde birden fazla yorum dizisi olan bir metne tıklarsa, en kısa metin aralığının yorum dizisini bulur ve onu seçeriz."
Sezgisel olarak, bunu yapmak mantıklıdır, böylece kullanıcı her zaman tamamen başka bir yorum dizisi içinde bulunan en içteki yorum dizisine ulaşmak için bir yola sahip olur. Diğer koşullar için (kısmi örtüşme veya örtüşmeme), üzerinde yalnızca bir yorum dizisi olan bazı metinler olmalıdır, bu nedenle o yorum dizisini seçmek için bu metni kullanmak kolay olmalıdır. Bu, iş parçacıklarının tam (veya yoğun ) örtüşmesi ve neden bu kurala ihtiyacımız olduğudur.
Bu kuralı kullanmamıza ve yorum dizisini seçerken 'doğru olanı yapmamıza' izin veren oldukça karmaşık bir örtüşme durumuna bakalım.
Yukarıdaki örnekte, kullanıcı bu sırayla aşağıdaki yorum dizilerini ekler:
- 1. Konuyu 'B' karakterinin üzerine yorumlayın (uzunluk = 1).
- 2. Konuyu 'AB' üzerinden yorumlayın (uzunluk = 2).
- 3. Konuyu 'BC' üzerinden yorumlayın (uzunluk = 2).
Bu eklemelerin sonunda, Slate'in metin düğümlerini işaretlerle ayırma yöntemi nedeniyle, her karakter için bir tane olmak üzere üç metin düğümümüz olacak. Şimdi, kullanıcı 'B'yi tıklarsa, en kısa uzunluk kuralına göre, üç uzunluktan en kısası olduğu için iplik #1'i seçeriz. Bunu yapmazsak, yalnızca bir karakter uzunluğunda ve aynı zamanda diğer iki ileti dizisinin bir parçası olduğu için Yorum Konusu #1'i seçme şansımız olmazdı.
Bu kural, daha kısa yorum dizilerini ortaya çıkarmayı kolaylaştırsa da, içerdikleri tüm karakterler başka bir kısa yorum dizisinin parçası olduğundan, daha uzun yorum dizilerinin erişilemez hale geldiği durumlarla karşılaşabiliriz. Bunun için bir örneğe bakalım.
100 karakterimiz olduğunu varsayalım (diyelim ki 'A' karakteri 100 defa yazıldı) ve kullanıcı yorum dizilerini aşağıdaki sırayla ekliyor:
- Yorum Konu # 1 aralığı 20,80
- Yorum Konu # 2 aralığı 0,50
- Yorum Konusu # 3, aralık 51,100
Yukarıdaki örnekte görebileceğiniz gibi, burada az önce tanımladığımız kuralı uygularsak, #20 ile #80 arasındaki herhangi bir karaktere tıklamak, #1'den daha kısa ve dolayısıyla #1'den daha kısa oldukları için her zaman #2 veya #3 dizilerini seçer. seçilemezdi. Bu kuralın hangi yorum dizisini seçeceğimiz konusunda bizi kararsız bırakabileceği başka bir senaryo, bir metin düğümünde aynı en kısa uzunlukta birden fazla yorum dizisinin olması durumudur.
Örtüşen yorumların bu tür bir kombinasyonu ve bu kuralın takip edilmesinin metne tıklayarak belirli bir yorum dizisini erişilemez hale getirdiği düşünülebilecek diğer birçok kombinasyon için, bu makalenin ilerleyen kısımlarında kullanıcıya tüm yorum dizilerinin bir görünümünü veren bir Yorumlar Kenar Çubuğu oluşturuyoruz. kenar çubuğundaki bu ileti dizilerini tıklatabilmeleri ve yorum aralığını görmek için düzenleyicide etkinleştirebilmeleri için belgede bulunur. Yine de, yukarıda bahsettiğimiz daha az olası örnekler dışında, birçok örtüşme senaryosunu kapsaması gerektiği için bu kurala sahip olmak ve uygulamak isteriz. Tüm bu çabayı bu kural etrafında harcadık çünkü editörde vurgulanmış metni görmek ve yorum yapmak için üzerine tıklamak, metin üzerinde bir yoruma erişmenin yalnızca kenar çubuğundaki bir yorum listesini kullanmaktan daha sezgisel bir yoludur.
Ekleme Kuralı
Kural:
"Kullanıcının seçtiği ve yorum yapmaya çalıştığı metin, yorum dizileri tarafından zaten tamamen kapsanmışsa, bu eklemeye izin vermeyin."
Bunun nedeni, bu eklemeye izin verirsek, o aralıktaki her karakterin en az iki yorum dizisine sahip olması (biri mevcut, diğeri az önce izin verdiğimiz yeni), hangisini seçeceğimizi belirlememizi zorlaştırır. kullanıcı daha sonra bu karaktere tıklar.
Bu kurala bakıldığında, en küçük metin aralığını seçmemize izin veren En Kısa Yorum Aralığı Kuralına zaten sahipsek, neden ilk etapta buna ihtiyacımız olduğunu merak edebiliriz. Gösterilecek doğru yorum dizisini belirlemek için ilk kuralı kullanabilirsek neden tüm örtüşme kombinasyonlarına izin vermiyoruz? Daha önce tartıştığımız bazı örneklerde olduğu gibi, ilk kural birçok senaryoda işe yarar ancak hepsi için geçerli değildir. Ekleme Kuralı ile, ilk kuralın bize yardımcı olamayacağı senaryoların sayısını en aza indirmeye çalışıyoruz ve kullanıcının bu yorum dizisine erişmesinin tek yolu olarak Kenar Çubuğuna geri dönmemiz gerekiyor. Ekleme Kuralı, yorum dizilerinin tam örtüşmesini de önler. Bu kural genellikle birçok popüler editör tarafından uygulanmaktadır.
Aşağıda, bu kural olmasaydı, Yorum Konusu #3'e izin vereceğimiz ve ardından ilk kuralın bir sonucu olarak, en uzun uzunluk olacağından #3'e erişilemeyeceği bir örnek verilmiştir.
Not : Bu kurala sahip olmak, örtüşen yorumları hiçbir zaman tam olarak içeremeyeceğimiz anlamına gelmez. Çakışan yorumlarla ilgili zor olan şey, kurallara rağmen, yorumların eklendiği sıranın bizi yine de çakışmanın olmasını istemediğimiz bir durumda bırakabilmesidir. 'tasarımcılar' kelimesiyle ilgili yorum örneğimize geri dönersek ' daha önce, oraya eklenen en uzun yorum dizisi, Ekleme Kuralının buna izin vermesi için eklenecek olan sonuncusuydu ve sonunda tamamen kapsanan bir durumla sonuçlandık - #1 ve #2, #3'ün içinde yer alır. Sorun değil çünkü En Kısa Yorum Aralığı Kuralı bize orada yardımcı olacaktır.
Yorum dizilerinin seçimini uyguladığımız bir sonraki bölümde En Kısa Yorum Aralığı Kuralını uygulayacağız. Artık yorum eklemek için bir araç çubuğu düğmemiz olduğundan, kullanıcı bir metin seçtiğinde kuralı kontrol ederek Ekleme Kuralını hemen uygulayabiliriz. Kural yerine getirilmezse, kullanıcıların seçilen metne yeni bir yorum dizisi ekleyememesi için Yorum düğmesini devre dışı bırakırız. Başlayalım!
# src/utils/EditorCommentUtils.js export function shouldAllowNewCommentThreadAtSelection(editor, selection) { if (selection == null || Range.isCollapsed(selection)) { return false; } const textNodeIterator = Editor.nodes(editor, { at: selection, mode: "lowest", }); let nextTextNodeEntry = textNodeIterator.next().value; const textNodeEntriesInSelection = []; while (nextTextNodeEntry != null) { textNodeEntriesInSelection.push(nextTextNodeEntry); nextTextNodeEntry = textNodeIterator.next().value; } if (textNodeEntriesInSelection.length === 0) { return false; } return textNodeEntriesInSelection.some( ([textNode]) => getCommentThreadsOnTextNode(textNode).size === 0 ); }
Bu fonksiyondaki mantık nispeten basittir.
- Kullanıcının seçimi yanıp sönen bir şapka ise, hiçbir metin seçilmediği için buraya yorum eklenmesine izin vermiyoruz.
- Kullanıcının seçimi daraltılmış değilse, seçimdeki tüm metin düğümlerini buluruz. Metin düğümleri gerçekten belge ağacının yaprakları olduğundan, tüm metin düğümlerini seçmemize yardımcı olan
Editor.nodes
(SlateJS'in bir yardımcı işlevi) çağrısındamode: lowest
kullanımına dikkat edin. - Üzerinde yorum dizisi olmayan en az bir metin düğümü varsa, eklemeye izin verebiliriz. Burada daha önce yazdığımız
getCommentThreadsOnTextNode
kullanıyoruz.
Şimdi bu util işlevini, düğmenin devre dışı durumunu kontrol etmek için araç çubuğunun içinde kullanıyoruz.
# src/components/Toolbar.js export default function Toolbar({ selection, previousSelection }) { const editor = useEditor(); .... return ( <div className="toolbar"> .... <ToolBarButton isActive={false} disabled={!shouldAllowNewCommentThreadAtSelection( editor, selection )} label={<i className={`bi ${getIconForButton("comment")}`} />} onMouseDown={onInsertComment} /> </div> );
Yukarıdaki örneğimizi yeniden oluşturarak kuralın uygulanmasını test edelim.
Burada dikkat edilmesi gereken iyi bir kullanıcı deneyimi detayı, kullanıcı burada metnin tamamını seçtiyse araç çubuğu düğmesini devre dışı bırakırken, kullanıcı için deneyimi tamamlamamasıdır. Kullanıcı, düğmenin neden devre dışı bırakıldığını tam olarak anlamayabilir ve muhtemelen buraya bir yorum dizisi ekleme niyetlerine yanıt vermediğimiz için kafası karışabilir. Bunu daha sonra, Yorum Popover'ları, araç çubuğu düğmesi devre dışı bırakılsa bile, yorum dizilerinden birinin açılır penceresi görünecek ve kullanıcı yine de yorum bırakabilecek şekilde oluşturulduğu için ele alacağız.
Ayrıca yorumlanmamış bir metin düğümünün olduğu ve kuralın yeni bir yorum dizisi eklemeye izin verdiği bir durumu da test edelim.
Yorum Konularını Seçme
Bu bölümde, kullanıcının yorum yapılan bir metin düğümüne tıkladığı özelliği etkinleştiriyoruz ve hangi yorum dizisinin seçilmesi gerektiğini belirlemek için En Kısa Yorum Aralığı Kuralını kullanıyoruz. Süreçteki adımlar şunlardır:
- Kullanıcının tıkladığı yorum yapılan metin düğümündeki en kısa yorum dizisini bulun.
- Bu yorum dizisini aktif yorum dizisi olarak ayarlayın. (Bunun için gerçeğin kaynağı olacak yeni bir Recoil atomu yaratıyoruz.)
- Yorum yapılan metin düğümleri, Geri Tepme durumunu dinler ve aktif yorum dizisinin bir parçasıysa, kendilerini farklı şekilde vurgularlar. Bu şekilde, kullanıcı yorum dizisine tıkladığında, tüm metin düğümleri vurgu renklerini güncelleyeceğinden tüm metin aralığı göze çarpar.
Adım 1: En Kısa Yorum Aralığı Kuralını Uygulama
Temelde En Kısa Yorum Aralığı Kuralını uygulayan Adım #1 ile başlayalım. Buradaki amaç, kullanıcının tıkladığı metin düğümündeki en kısa aralığın yorum dizisini bulmaktır. En kısa uzunluktaki diziyi bulmak için, o metin düğümündeki tüm yorum dizilerinin uzunluğunu hesaplamamız gerekir. Bunu yapmak için adımlar şunlardır:
- Söz konusu metin düğümündeki tüm yorum dizilerini alın.
- Bu metin düğümünden herhangi bir yönde hareket edin ve izlenen iplik uzunluklarını güncellemeye devam edin.
- Aşağıdaki kenarlardan birine ulaştığımızda bir yönde geçişi durdurun:
- Yorumsuz bir metin düğümü (izlediğimiz tüm yorum dizilerinin en başlangıç/bitiş sınırına ulaştığımızı ima eder).
- İzlediğimiz tüm yorum dizilerinin bir kenara (başlangıç/bitiş) ulaştığı bir metin düğümü.
- Bu yönde geçilecek başka metin düğümü yok (belgenin başına veya sonuna ya da metin olmayan bir düğüme ulaştığımız anlamına gelir).
İleri ve geri yöndeki geçişler işlevsel olarak aynı olduğundan, temelde bir metin düğümü yineleyicisi alan updateCommentThreadLengthMap
yardımcı işlevi yazacağız. Yineleyiciyi aramaya devam edecek ve izleme iş parçacığı uzunluklarını güncellemeye devam edecektir. Bu işlevi iki kez çağıracağız - bir kez ileri ve bir kez geri yön için. Let's write our main utility function that will use this helper function.
# src/utils/EditorCommentUtils.js export function getSmallestCommentThreadAtTextNode(editor, textNode) { const commentThreads = getCommentThreadsOnTextNode(textNode); const commentThreadsAsArray = [...commentThreads]; let shortestCommentThreadID = commentThreadsAsArray[0]; const reverseTextNodeIterator = (slateEditor, nodePath) => Editor.previous(slateEditor, { at: nodePath, mode: "lowest", match: Text.isText, }); const forwardTextNodeIterator = (slateEditor, nodePath) => Editor.next(slateEditor, { at: nodePath, mode: "lowest", match: Text.isText, }); if (commentThreads.size > 1) { // The map here tracks the lengths of the comment threads. // We initialize the lengths with length of current text node // since all the comment threads span over the current text node // at the least. const commentThreadsLengthByID = new Map( commentThreadsAsArray.map((id) => [id, textNode.text.length]) ); // traverse in the reverse direction and update the map updateCommentThreadLengthMap( editor, commentThreads, reverseTextNodeIterator, commentThreadsLengthByID ); // traverse in the forward direction and update the map updateCommentThreadLengthMap( editor, commentThreads, forwardTextNodeIterator, commentThreadsLengthByID ); let minLength = Number.POSITIVE_INFINITY; // Find the thread with the shortest length. for (let [threadID, length] of commentThreadsLengthByID) { if (length < minLength) { shortestCommentThreadID = threadID; minLength = length; } } } return shortestCommentThreadID; }
The steps we listed out are all covered in the above code. The comments should help follow how the logic flows there.
One thing worth calling out is how we created the traversal functions. We want to give a traversal function to updateCommentThreadLengthMap
such that it can call it while it is iterating text node's path and easily get the previous/next text node. To do that, Slate's traversal utilities Editor.previous
and Editor.next
(defined in the Editor interface) are very helpful. Our iterators reverseTextNodeIterator
and forwardTextNodeIterator
call these helpers with two options mode: lowest
and the match function Text.isText
so we know we're getting a text node from the traversal, if there is one.
Now we implement updateCommentThreadLengthMap
which traverses using these iterators and updates the lengths we're tracking.
# src/utils/EditorCommentUtils.js function updateCommentThreadLengthMap( editor, commentThreads, nodeIterator, map ) { let nextNodeEntry = nodeIterator(editor); while (nextNodeEntry != null) { const nextNode = nextNodeEntry[0]; const commentThreadsOnNextNode = getCommentThreadsOnTextNode(nextNode); const intersection = [...commentThreadsOnNextNode].filter((x) => commentThreads.has(x) ); // All comment threads we're looking for have already ended meaning // reached an uncommented text node OR a commented text node which // has none of the comment threads we care about. if (intersection.length === 0) { break; } // update thread lengths for comment threads we did find on this // text node. for (let i = 0; i < intersection.length; i++) { map.set(intersection[i], map.get(intersection[i]) + nextNode.text.length); } // call the iterator to get the next text node to consider nextNodeEntry = nodeIterator(editor, nextNodeEntry[1]); } return map; }
One might wonder why do we wait until the intersection
becomes 0
to stop iterating in a certain direction. Why can't we just stop if we're reached the edge of at least one comment thread — that would imply we've reached the shortest length in that direction, right? The reason we can't do that is that we know that a comment thread can span over multiple text nodes and we wouldn't know which of those text nodes did the user click on and we started our traversal from. We wouldn't know the range of all comment threads in question without fully traversing to the farthest edges of the union of the text ranges of the comment threads in both the directions.
Check out the below example where we have two comment threads 'A' and 'B' overlapping each other in some way resulting into three text nodes 1,2 and 3 — #2 being the text node with the overlap.
In this example, let's assume we don't wait for intersection to become 0 and just stop when we reach the edge of a comment thread. Now, if the user clicked on #2 and we start traversal in reverse direction, we'd stop at the start of text node #2 itself since that's the start of the comment thread A. As a result, we might not compute the comment thread lengths correctly for A & B. With the implementation above traversing the farthest edges (text nodes 1,2, and 3), we should get B as the shortest comment thread as expected.
To see the implementation visually, below is a walkthrough with a slideshow of the iterations. We have two comment threads A and B that overlap each other over text node #3 and the user clicks on the overlapping text node #3.
Steps 2 & 3: Maintaining State Of The Selected Comment Thread And Highlighting It
Now that we have the logic for the rule fully implemented, let's update the editor code to use it. For that, we first create a Recoil atom that'll store the active comment thread ID for us. We then update the CommentedText
component to use our rule's implementation.
# src/utils/CommentState.js import { atom } from "recoil"; export const activeCommentThreadIDAtom = atom({ key: "activeCommentThreadID", default: null, }); # src/components/CommentedText.js import { activeCommentThreadIDAtom } from "../utils/CommentState"; import classNames from "classnames"; import { getSmallestCommentThreadAtTextNode } from "../utils/EditorCommentUtils"; import { useRecoilState } from "recoil"; export default function CommentedText(props) { .... const { commentThreads, textNode, ...otherProps } = props; const [activeCommentThreadID, setActiveCommentThreadID] = useRecoilState( activeCommentThreadIDAtom ); const onClick = () => { setActiveCommentThreadID( getSmallestCommentThreadAtTextNode(editor, textNode) ); }; return ( <span {...otherProps} className={classNames({ comment: true, // a different background color treatment if this text node's // comment threads do contain the comment thread active on the // document right now. "is-active": commentThreads.has(activeCommentThreadID), })} onClick={onClick} > {props.children} ≷/span> ); }
Bu bileşen, bir bileşenin abone olmasına ve aynı zamanda Geri Tepme atomunun değerini ayarlamasına izin veren useRecoilState
kullanır. Abonenin bu metin düğümünün aktif yorum dizisinin bir parçası olup olmadığını bilmesine ihtiyacımız var, böylece kendisini farklı şekilde şekillendirebilir. Ortadaki yorum dizisinin aktif olduğu ve aralığını net bir şekilde görebildiğimiz aşağıdaki ekran görüntüsüne bakın.
Artık yorum dizilerinin seçimini yapmak için tüm koda sahip olduğumuza göre, eylemde görelim. Geçiş kodumuzu iyi test etmek için, bazı basit örtüşme durumlarını ve aşağıdakiler gibi bazı uç durumları test ediyoruz:
- Editörün başında/sonunda yorumlanmış bir metin düğümüne tıklamak.
- Birden çok paragrafa yayılan yorum dizileri ile yorumlanmış bir metin düğümüne tıklamak.
- Bir görüntü düğümünden hemen önce yorumlanmış bir metin düğümüne tıklamak.
- Bağlantıları örtüşen yorumlanmış bir metin düğümüne tıklamak.
Artık aktif yorum dizisi kimliğini izlemek için bir Geri Tepme atomumuz olduğundan, dikkat edilmesi gereken küçük bir ayrıntı, kullanıcı yeni bir yorum dizisi eklemek için araç çubuğu düğmesini kullandığında yeni oluşturulan yorum dizisini aktif olacak şekilde ayarlamaktır. Bu, bir sonraki bölümde, kullanıcının hemen yorum eklemeye başlayabilmesi için, yorum dizisi açılır penceresini ekleme anında göstermemizi sağlar.
# src/components/Toolbar.js import useAddCommentThreadToState from "../hooks/useAddCommentThreadToState"; import { useSetRecoilState } from "recoil"; export default function Toolbar({ selection, previousSelection }) { ... const setActiveCommentThreadID = useSetRecoilState(activeCommentThreadIDAtom); ..... const onInsertComment = useCallback(() => { const newCommentThreadID = insertCommentThread(editor, addCommentThread); setActiveCommentThreadID(newCommentThreadID); }, [editor, addCommentThread, setActiveCommentThreadID]); return <div className='toolbar'> .... </div>; };
Not: Burada useSetRecoilState
kullanımı (atom için bir ayarlayıcıyı ortaya çıkaran ancak bileşenin değerine abone olmayan bir Geri Tepme kancası), bu durumda araç çubuğu için ihtiyacımız olan şeydir.
Yorum Konusu Popover'ları Ekleme
Bu bölümde, seçili/etkin yorum dizisi kavramını kullanan ve kullanıcının bu yorum dizisine yorum eklemesine izin veren bir açılır pencere gösteren bir Yorum Açılır Penceresi oluşturuyoruz. İnşa etmeden önce, nasıl çalıştığına hızlıca bir göz atalım.
Etkin olan yorum dizisine yakın bir Yorum Popover oluşturmaya çalışırken, Bağlantı Düzenleyici Menüsü ile ilk makalede yaptığımız bazı sorunlarla karşılaşıyoruz. Bu noktada, ilk makaledeki Bağlantı Düzenleyicisi oluşturan bölümü ve bununla karşılaştığımız seçim sorunlarını okumanız önerilir.
İlk önce, aktif yorum dizisinin ne olduğuna bağlı olarak doğru yerde boş bir popover bileşeni oluşturmaya çalışalım. Popover'ın çalışma şekli şudur:
- Yorum Konusu Popover , yalnızca aktif bir yorum dizisi kimliği olduğunda işlenir. Bu bilgiyi almak için bir önceki bölümde oluşturduğumuz Recoil atomunu dinliyoruz.
- Oluşturulduğunda, editörün seçimindeki metin düğümünü buluyoruz ve popover'ı ona yakın hale getiriyoruz.
- Kullanıcı açılır pencerenin dışında herhangi bir yere tıkladığında, etkin yorum dizisini
null
olarak ayarlıyoruz, böylece yorum dizisini devre dışı bırakıyoruz ve ayrıca açılır pencereyi ortadan kaldırıyoruz.
# src/components/CommentThreadPopover.js import NodePopover from "./NodePopover"; import { getFirstTextNodeAtSelection } from "../utils/EditorUtils"; import { useEditor } from "slate-react"; import { useSetRecoilState} from "recoil"; import {activeCommentThreadIDAtom} from "../utils/CommentState"; export default function CommentThreadPopover({ editorOffsets, selection, threadID }) { const editor = useEditor(); const textNode = getFirstTextNodeAtSelection(editor, selection); const setActiveCommentThreadID = useSetRecoilState( activeCommentThreadIDAtom ); const onClickOutside = useCallback( () => {}, [] ); return ( <NodePopover editorOffsets={editorOffsets} isBodyFullWidth={true} node={textNode} className={"comment-thread-popover"} onClickOutside={onClickOutside} > {`Comment Thread Popover for threadID:${threadID}`} </NodePopover> ); }
Popover bileşeninin bu uygulaması için çağrılması gereken birkaç şey:
-
editorOffsets
ve oluşturulacağıEditor
bileşenindekiselection
alır.editorOffsets
Editor bileşeninin sınırlarıdır, bu nedenle popover'ın konumunu hesaplayabiliriz ve kullanıcınınselection
null
olmasına neden olan bir araç çubuğu düğmesi kullanması durumunda seçim geçerli veya öncekiselection
olabilir. Yukarıda bağlantısı verilen ilk makaledeki Bağlantı Düzenleyicisi ile ilgili bölüm, bunları ayrıntılı olarak inceler. - İlk makaledeki
LinkEditor
ve buradakiCommentThreadPopover
, her ikisi de bir metin düğümünün etrafında bir açılır pencere oluşturduğundan, bu ortak mantığı, söz konusu metin düğümüne hizalanmış bileşenin oluşturulmasını yöneten bir bileşenNodePopover
. Uygulama ayrıntıları, ilk makaledeLinkEditor
bileşeninin sahip olduğu şeydir. -
NodePopover
, kullanıcı açılır pencerenin dışında bir yere tıkladığında çağrılan bir destek olarakonClickOutside
yöntemini alır. Bunu, bu fikirle ilgili bu Smashing makalesinde ayrıntılı olarak açıklandığı gibi,document
mousedown
olay dinleyicisi ekleyerek uygularız. -
getFirstTextNodeAtSelection
, açılır pencereyi oluşturmak için kullandığımız kullanıcının seçimi içindeki ilk metin düğümünü alır. Bu işlevin uygulanması, metin düğümünü bulmak için Slate'in yardımcılarını kullanır.
# src/utils/EditorUtils.js export function getFirstTextNodeAtSelection(editor, selection) { const selectionForNode = selection ?? editor.selection; if (selectionForNode == null) { return null; } const textNodeEntry = Editor.nodes(editor, { at: selectionForNode, mode: "lowest", match: Text.isText, }).next().value; return textNodeEntry != null ? textNodeEntry[0] : null; }
Etkin yorum dizisini temizlemesi gereken onClickOutside
geri çağrısını uygulayalım. Ancak, yorum dizisi açılır penceresi açıkken ve belirli bir konu aktifken ve kullanıcı başka bir yorum dizisine tıkladığında senaryoyu hesaba katmalıyız. Bu durumda, diğer CommentedText
bileşenindeki click olayının diğer yorum dizisini aktif hale getirmesi gerektiğinden, onClickOutside
aktif yorum dizisini sıfırlamasını istemiyoruz. Popover'da buna müdahale etmek istemiyoruz.
Bunu yapmanın yolu, tıklama olayının gerçekleştiği DOM düğümüne en yakın Slate Düğümünü bulmaktır. Bu Slate düğümü bir metin düğümüyse ve üzerinde yorumlar varsa, aktif yorum dizisi Recoil atomunu sıfırlamayı atlarız. Hadi uygulayalım!
# src/components/CommentThreadPopover.js const setActiveCommentThreadID = useSetRecoilState(activeCommentThreadIDAtom); const onClickOutside = useCallback( (event) => { const slateDOMNode = event.target.hasAttribute("data-slate-node") ? event.target : event.target.closest('[data-slate-node]'); // The click event was somewhere outside the Slate hierarchy. if (slateDOMNode == null) { setActiveCommentThreadID(null); return; } const slateNode = ReactEditor.toSlateNode(editor, slateDOMNode); // Click is on another commented text node => do nothing. if ( Text.isText(slateNode) && getCommentThreadsOnTextNode(slateNode).size > 0 ) { return; } setActiveCommentThreadID(null); }, [editor, setActiveCommentThreadID] );
Slate, bir DOM düğümüyle veya kendisi bir Slate Düğümü değilse onun en yakın atasıyla eşleşen Slate düğümünü döndüren bir yardımcı toSlateNode
yöntemine sahiptir. Bu yardımcının mevcut uygulaması, null
döndürmek yerine bir Slate düğümü bulamazsa bir hata verir. Bunu, kullanıcı Slate düğümlerinin bulunmadığı düzenleyicinin dışında bir yere tıkladığında çok olası bir senaryo olan null
durumu kendimiz kontrol ederek yukarıda ele alıyoruz.
Artık, activeCommentThreadIDAtom
dinlemek için Editor
bileşenini güncelleyebilir ve açılır pencereyi yalnızca bir yorum dizisi etkin olduğunda oluşturabiliriz.
# src/components/Editor.js import { useRecoilValue } from "recoil"; import { activeCommentThreadIDAtom } from "../utils/CommentState"; export default function Editor({ document, onChange }): JSX.Element { const activeCommentThreadID = useRecoilValue(activeCommentThreadIDAtom); // This hook is described in detail in the first article const [previousSelection, selection, setSelection] = useSelection(editor); return ( <> ... <div className="editor" ref={editorRef}> ... {activeCommentThreadID != null ? ( <CommentThreadPopover editorOffsets={editorOffsets} selection={selection ?? previousSelection} threadID={activeCommentThreadID} /> ) : null} </div> ... </> ); }
Popover'ın doğru yorum dizisi için doğru yere yüklendiğini ve dışarıyı tıkladığımızda aktif yorum dizisini temizlediğini doğrulayalım.
Şimdi, kullanıcıların bir yorum dizisine yorum eklemesine ve bu ileti dizisinin tüm yorumlarını açılır pencerede görmelerine izin vermeye geçiyoruz. Bunun için daha önce makalemizde oluşturduğumuz Recoil atom ailesini — commentThreadsState
.
Bir yorum dizisindeki yorumlar, comments
dizisinde saklanır. Yeni bir yorum eklemeyi etkinleştirmek için, kullanıcının yeni bir yorum girmesine izin veren bir Form girişi oluşturuyoruz. Kullanıcı yorumu yazarken, bunu yerel bir durum değişkeni commentText
. Düğmeye tıklandığında, yorum metnini comments
dizisine yeni yorum olarak ekliyoruz.
# src/components/CommentThreadPopover.js import { commentThreadsState } from "../utils/CommentState"; import { useRecoilState } from "recoil"; import Button from "react-bootstrap/Button"; import Form from "react-bootstrap/Form"; export default function CommentThreadPopover({ editorOffsets, selection, threadID, }) { const [threadData, setCommentThreadData] = useRecoilState( commentThreadsState(threadID) ); const [commentText, setCommentText] = useState(""); const onClick = useCallback(() => { setCommentThreadData((threadData) => ({ ...threadData, comments: [ ...threadData.comments, // append comment to the comments on the thread. { text: commentText, author: "Jane Doe", creationTime: new Date() }, ], })); // clear the input setCommentText(""); }, [commentText, setCommentThreadData]); const onCommentTextChange = useCallback( (event) => setCommentText(event.target.value), [setCommentText] ); return ( <NodePopover ... > <div className={"comment-input-wrapper"}> <Form.Control bsPrefix={"comment-input form-control"} placeholder={"Type a comment"} type="text" value={commentText} onChange={onCommentTextChange} /> <Button size="sm" variant="primary" disabled={commentText.length === 0} onClick={onClick} > Comment </Button> </div> </NodePopover> ); }
Not : Kullanıcının yorum yazması için bir girdi oluşturmamıza rağmen, açılır pencere takıldığında mutlaka odaklanmasına izin vermeyiz. Bu, bir düzenleyiciden diğerine değişebilen bir Kullanıcı Deneyimi kararıdır. Bazı düzenleyiciler, yorum dizisi açılır penceresi açıkken kullanıcıların metni düzenlemesine izin vermez. Bizim durumumuzda, kullanıcının üzerine tıkladığında yorum yapılan metni düzenlemesine izin vermek istiyoruz.
Recoil atom ailesinden belirli yorum dizisinin verilerine nasıl eriştiğimizi çağırmaya değer - atomu şu şekilde çağırarak - commentThreadsState(threadID)
. Bu bize atomun değerini ve ailedeki sadece o atomu güncellemek için bir ayarlayıcı verir. Yorumlar sunucudan tembelce yükleniyorsa, Recoil ayrıca bize atom verilerinin yükleme durumu hakkında bilgi veren bir Loadable nesnesi döndüren bir useRecoilStateLoadable
kancası sağlar. Hala yükleniyorsa, açılır pencerede bir yükleme durumu göstermeyi seçebiliriz.
Şimdi threadData
ve yorumların listesini oluşturuyoruz. Her yorum, CommentRow
bileşeni tarafından işlenir.
# src/components/CommentThreadPopover.js return ( <NodePopover ... > <div className={"comment-list"}> {threadData.comments.map((comment, index) => ( <CommentRow key={`comment_${index}`} comment={comment} /> ))} </div> ... </NodePopover> );
Aşağıda, yorum metnini ve yazar adı ve oluşturma zamanı gibi diğer meta verileri CommentRow
uygulaması yer almaktadır. Biçimlendirilmiş bir oluşturma zamanını göstermek için date-fns
modülünü kullanıyoruz.
# src/components/CommentRow.js import { format } from "date-fns"; export default function CommentRow({ comment: { author, text, creationTime }, }) { return ( <div className={"comment-row"}> <div className="comment-author-photo"> <i className="bi bi-person-circle comment-author-photo"></i> </div> <div> <span className="comment-author-name">{author}</span> <span className="comment-creation-time"> {format(creationTime, "eee MM/dd H:mm")} </span> <div className="comment-text">{text}</div> </div> </div> ); }
Bunu daha sonra Yorum Kenar Çubuğunu uyguladığımızda yeniden kullandığımız için kendi bileşeni olarak çıkardık.
Bu noktada, Yorum Popover'ımız, yeni yorumlar eklemeye ve bunun için Geri Tepme durumunu güncellemeye izin vermek için ihtiyaç duyduğu tüm koda sahiptir. Bunu doğrulayalım. Tarayıcı konsolunda, daha önce eklediğimiz Recoil Debug Observer'ı kullanarak, yorum dizisi için Recoil atomunun, konuya yeni yorumlar ekledikçe doğru şekilde güncellendiğini doğrulayabiliyoruz.
Yorum Kenar Çubuğu Ekleme
Makalede daha önce, neden ara sıra, uyguladığımız kuralların, örtüşmenin kombinasyonuna bağlı olarak, belirli bir yorum dizisine yalnızca metin düğümlerine tıklayarak erişilememesini engelleyebileceğini belirtmiştik. Bu gibi durumlarda, kullanıcının belgedeki tüm yorum dizilerine ulaşmasını sağlayan bir Yorumlar Kenar Çubuğuna ihtiyacımız var.
Yorum Kenar Çubuğu, bir gözden geçirenin tüm yorum dizilerinde birbiri ardına gezinebileceği ve ihtiyaç duyduğu her yerde yorum/yanıt bırakabileceği bir Öneri ve İnceleme iş akışına giren iyi bir eklentidir. Kenar çubuğunu uygulamaya başlamadan önce, aşağıda halletmemiz gereken bitmemiş bir görev var.
Yorum Dizilerinin Geri Tepme Durumunun Başlatılması
Belge düzenleyiciye yüklendiğinde, tüm yorum dizilerini bulmak için belgeyi taramamız ve başlatma işleminin bir parçası olarak yukarıda oluşturduğumuz Recoil atomlarına eklememiz gerekiyor. EditorCommentUtils
metin düğümlerini tarayan, tüm yorum dizilerini bulan ve bunları Recoil atomuna ekleyen bir yardımcı fonksiyon yazalım.
# src/utils/EditorCommentUtils.js export async function initializeStateWithAllCommentThreads( editor, addCommentThread ) { const textNodesWithComments = Editor.nodes(editor, { at: [], mode: "lowest", match: (n) => Text.isText(n) && getCommentThreadsOnTextNode(n).size > 0, }); const commentThreads = new Set(); let textNodeEntry = textNodesWithComments.next().value; while (textNodeEntry != null) { [...getCommentThreadsOnTextNode(textNodeEntry[0])].forEach((threadID) => { commentThreads.add(threadID); }); textNodeEntry = textNodesWithComments.next().value; } Array.from(commentThreads).forEach((id) => addCommentThread(id, { comments: [ { author: "Jane Doe", text: "Comment Thread Loaded from Server", creationTime: new Date(), }, ], status: "open", }) ); }
Arka Uç Depolama ve Performans Değerlendirmesi ile Eşitleme
Makalenin bağlamı için, yalnızca UI uygulamasına odaklandığımız için, başlatma kodunun çalıştığını doğrulamamıza izin veren bazı verilerle bunları başlatıyoruz.
Yorumlama Sisteminin gerçek dünya kullanımında, yorum dizilerinin belge içeriklerinden ayrı olarak saklanması muhtemeldir. Böyle bir durumda, commentThreads
içindeki tüm yorum dizisi kimliklerindeki tüm meta verileri ve yorumları getiren bir API çağrısı yapmak için yukarıdaki kodun güncellenmesi gerekir. Yorum dizileri yüklendikten sonra, birden fazla kullanıcı gerçek zamanlı olarak onlara daha fazla yorum ekledikçe, durumlarını değiştirdikçe vb. güncellenirler. Yorumlama Sisteminin üretim sürümünün, Geri Tepme deposunu sunucuyla senkronize etmeye devam edebileceğimiz şekilde yapılandırması gerekir. Durum yönetimi için Geri Tepmeyi kullanmayı seçerseniz, Atom Etkileri API'sinde (bu makaleyi yazarken deneyseldir) benzer bir şey yapan bazı örnekler vardır.
Bir belge gerçekten uzunsa ve üzerinde birçok yorum dizisinde işbirliği yapan çok sayıda kullanıcı varsa, başlatma kodunu yalnızca belgenin ilk birkaç sayfası için yorum dizilerini yüklemek üzere optimize etmemiz gerekebilir. Alternatif olarak, muhtemelen yükün daha ağır kısmı olan tüm yorum listesi yerine tüm yorum dizilerinin yalnızca hafif meta verilerini yüklemeyi seçebiliriz.
Şimdi, Editor
bileşeni belgeyle bağlandığında, Geri Tepme durumunun doğru şekilde başlatılması için bu işlevi çağırmaya geçelim.
# src/components/Editor.js import { initializeStateWithAllCommentThreads } from "../utils/EditorCommentUtils"; import useAddCommentThreadToState from "../hooks/useAddCommentThreadToState"; export default function Editor({ document, onChange }): JSX.Element { ... const addCommentThread = useAddCommentThreadToState(); useEffect(() => { initializeStateWithAllCommentThreads(editor, addCommentThread); }, [editor, addCommentThread]); return ( <> ... </> ); }
Yeni yorum dizileri eklemek için Araç Çubuğu Yorum Düğmesi uygulamasıyla kullandığımız aynı özel kancayı kullanıyoruz - useAddCommentThreadToState
. Açılır pencere çalıştığı için, belgede önceden var olan yorum dizilerinden birine tıklayabilir ve yukarıdaki ileti dizisini başlatmak için kullandığımız verileri gösterdiğini doğrulayabiliriz.
Artık durumumuz doğru bir şekilde başlatıldığına göre, kenar çubuğunu uygulamaya başlayabiliriz. Kullanıcı arayüzündeki tüm yorum dizilerimiz Recoil atom ailesinde saklanır - commentThreadsState
. Daha önce vurgulandığı gibi, bir Geri Tepme atom ailesindeki tüm öğeler arasında yineleme yöntemimiz, başka bir atomdaki atom anahtarlarını/kimliklerini izlemektir. Bunu commentThreadIDsState
ile yapıyoruz. Bu atomdaki kimlikler kümesini yineleyen ve her biri için bir CommentThread
bileşeni oluşturan CommentSidebar
bileşenini ekleyelim.
# 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> ); }
Şimdi, oluşturduğu yorum dizisine karşılık gelen ailedeki Recoil atomunu dinleyen CommentThread
bileşenini uyguluyoruz. Bu şekilde, kullanıcı düzenleyicide konuya daha fazla yorum ekledikçe veya diğer meta verileri değiştirdikçe, kenar çubuğunu bunu yansıtacak şekilde güncelleyebiliriz.
Kenar çubuğu çok fazla yorum içeren bir belge için gerçekten büyük olabileceğinden, kenar çubuğunu oluşturduğumuzda ilki dışındaki tüm yorumları gizleriz. Kullanıcı, tüm yorum dizisini göstermek/gizlemek için 'Yanıtları Göster/Gizle' düğmesini kullanabilir.
# 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> ); }
CommentRow
bileşenini yeniden kullandık, ancak temelde tüm yorumların kenar çubuğundaki bir diziyle bağlantılı görünmesini sağlayan showConnector
prop kullanarak bir tasarım işlemi ekledik.
Şimdi, Editor
CommentSidebar
oluşturuyoruz ve belgede sahip olduğumuz tüm konuları gösterdiğini ve mevcut konulara yeni konular veya yeni yorumlar ekledikçe doğru şekilde güncellendiğini doğrulıyoruz.
# src/components/Editor.js return ( <> <Slate ... > ..... <div className={"sidebar-wrapper"}> <CommentsSidebar /> </div> </Slate> </> );
Şimdi, editörlerde bulunan popüler bir Yorumlar Kenar Çubuğu etkileşimini uygulamaya geçiyoruz:
Kenar çubuğunda bir yorum dizisine tıklamak, o yorum dizisini seçmeli/etkinleştirmelidir. Ayrıca, düzenleyicide etkinse, kenar çubuğunda bir yorum dizisini vurgulamak için farklı bir tasarım işlemi ekleriz. Bunu yapabilmek için Recoil atom - activeCommentThreadIDAtom
kullanıyoruz. Bunu desteklemek için CommentThread
bileşenini güncelleyelim.
# 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> );
Yakından bakarsak, etkin yorum dizisini kenar çubuğuyla senkronize etme uygulamamızda bir hatamız var. Kenar çubuğunda farklı yorum dizilerine tıkladığımızda, düzenleyicide doğru yorum dizisi gerçekten vurgulanır. Ancak, Yorum Popover'ı aslında değiştirilen etkin yorum dizisine taşınmaz. İlk oluşturulduğu yerde kalır. Comment Popover'ın uygulamasına bakarsak, kendisini editörün seçimindeki ilk metin düğümüne karşı işler. Uygulamanın bu noktasında, bir yorum dizisi seçmenin tek yolu, bir metin düğümüne tıklamaktı, böylece, tıklama olayının bir sonucu olarak Slate tarafından güncellendiğinden, editörün seçimine rahatlıkla güvenebiliriz. Yukarıdaki onClick
olayında, seçimi güncellemeyiz, sadece Slate'in seçiminin değişmeden kalmasına neden olan Geri Tepme atom değerini güncelleriz ve dolayısıyla Yorum Popover'ı hareket etmez.
Bu sorunun bir çözümü, kullanıcı kenar çubuğundaki yorum dizisine tıkladığında Recoil atomunu güncellemekle birlikte editörün seçimini güncellemektir. Bunu yapan adımlar şunlardır:
- Üzerinde bu yorum dizisine sahip olan ve yeni aktif dizi olarak ayarlayacağımız tüm metin düğümlerini bulun.
- Bu metin düğümlerini belgede göründükleri sıraya göre sıralayın (Bunun için
Path.compare
API'sini kullanıyoruz). - İlk metin düğümünün başlangıcından son metin düğümünün sonuna kadar uzanan bir seçim aralığı hesaplayın.
- Seçim aralığını düzenleyicinin yeni seçimi olacak şekilde ayarlayın (Slate'in
Transforms.select
API'sini kullanarak).
Sadece hatayı düzeltmek isteseydik, Adım #1'de yorum dizisine sahip ilk metin düğümünü bulabilir ve bunu editörün seçimi olarak ayarlayabilirdik. Ancak, yorum dizisini gerçekten seçtiğimiz için tüm yorum aralığını seçmek daha temiz bir yaklaşım gibi görünüyor.
Yukarıdaki adımları dahil etmek için onClick
geri arama uygulamasını güncelleyelim.
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]);
Not : allTextNodePaths
, tüm metin düğümlerinin yolunu içerir. Bu yoldaki başlangıç ve bitiş noktalarını almak için Editor.point
API'sini kullanıyoruz. İlk makale Slate'in Konum kavramlarından geçiyor. Ayrıca Slate'in belgelerinde iyi belgelenmiştir.
Bu uygulamanın hatayı düzelttiğini ve Comment Popover'ın etkin yorum dizisine doğru bir şekilde taşındığını doğrulayalım. Bu sefer, orada kırılmadığından emin olmak için üst üste binen bir iş parçacığı durumuyla da test ediyoruz.
Hata düzeltmesiyle, henüz bahsetmediğimiz başka bir kenar çubuğu etkileşimini etkinleştirdik. Gerçekten uzun bir belgemiz varsa ve kullanıcı kenar çubuğunda görünümün dışındaki bir yorum dizisini tıklarsa, kullanıcının düzenleyicideki yorum dizisine odaklanabilmesi için belgenin o kısmına kaydırmak isteriz. Slate'in API'sini kullanarak yukarıdaki seçimi ayarlayarak, bunu ücretsiz olarak elde ederiz. Aşağıda eylemde görelim.
Bununla, kenar çubuğu uygulamamızı tamamlıyoruz. Makalenin sonuna doğru, editörde Yorum Yapma ve İnceleme deneyimini yükseltmeye yardımcı olan Yorumlar Kenar Çubuğuna yapabileceğimiz bazı güzel özellik eklemelerini ve geliştirmelerini listeliyoruz.
Yorumları Çözme ve Yeniden Açma
Bu bölümde, kullanıcıların yorum dizilerini 'Çözüldü' olarak işaretlemesine veya gerekirse bunları tartışma için yeniden açabilmesine odaklanıyoruz. Uygulama ayrıntısı açısından, bu, kullanıcı bu eylemi gerçekleştirirken değiştirdiğimiz bir yorum dizisindeki status
meta verileridir. Bir kullanıcının bakış açısından, bu çok yararlı bir özelliktir, çünkü onlara belgedeki bir şey hakkındaki tartışmanın sona erdiğini veya bazı güncellemeler/yeni bakış açıları vb. nedeniyle yeniden açılması gerektiğini doğrulamanın bir yolunu sunar.
Durumu değiştirmeyi etkinleştirmek için, CommentPopover
, kullanıcının iki durum arasında geçiş yapmasına izin veren bir düğme ekledik: open
ve 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> ); }
Bunu test etmeden önce, kullanıcının hangi yorum dizilerinin çözülmemiş veya açık olduğunu kolayca tespit edebilmesi ve isterlerse bunlara odaklanabilmesi için Yorumlar Kenar Çubuğuna çözümlenen yorumlar için farklı bir tasarım işlemi uygulayalım.
# 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> ); }
Çözüm
Bu yazıda, Zengin Metin Düzenleyicisi üzerinde Yorumlama Sistemi için çekirdek UI altyapısını oluşturduk. Buraya eklediğimiz işlevler grubu, ortak çalışanların belgenin bölümlerine açıklama ekleyebilecekleri ve bunlar hakkında sohbet edebilecekleri bir düzenleyicide daha zengin bir İşbirliği Deneyimi oluşturmak için bir temel görevi görür. Yorum Kenar Çubuğu eklemek, üründe etkinleştirilecek daha fazla sohbete dayalı veya incelemeye dayalı işlevlere sahip olmamız için bize bir alan sağlar.
Bu satırlar boyunca, bir Zengin Metin Düzenleyicisi'nin bu makalede oluşturduklarımıza ek olarak eklemeyi düşünebileceği bazı özellikler şunlardır:
- Ortak çalışanların yorumlarda birbirlerini etiketleyebilmeleri için
@
bahsetme desteği; - Yorum dizilerine eklenecek resim ve video gibi medya türleri için destek;
- Gözden geçirenlerin, değişiklik önerileri olarak görünen belgede düzenlemeler yapmasına olanak tanıyan belge düzeyinde Öneri Modu. Örnek olarak Google Dokümanlar'daki bu özelliğe veya Microsoft Word'deki Değişiklik İzlemeye atıfta bulunulabilir;
- Konuşmaları anahtar kelimeye göre aramak, ileti dizilerini duruma veya yorum yazar(lar)ına göre filtrelemek için kenar çubuğunda iyileştirmeler.