Compare commits

...

30 Commits

Author SHA1 Message Date
d3deb4cb11 Improve popup positioning based on rect's postion and not on click's X,Y coordiantes.
Closes #3
2025-06-11 10:01:40 +02:00
3cd86bf4c3 Fix scrollIntoView behavior and improve popup positioning #4 2025-06-09 11:48:15 +02:00
a04ffb3e70 Improve navigation and fix focus-visible colors in the popup #4 2025-06-05 09:44:52 +02:00
e4ba4dd3f1 Refactor findNextMistake method to be static and its calls #4 2025-06-05 08:54:56 +02:00
b99d233abc Prevent ctrl + enter shortuct when no grammar highlights are present #4 2025-06-05 08:17:19 +02:00
04cd5f2e7d Use scrollintoView after using shortcut navigation for finding a next/previous mistake #4 2025-06-05 08:11:42 +02:00
1163b3c47e Enhance shortcut navigation and add replacement acceptance logic for mistakes #4 2025-06-04 15:03:10 +02:00
e903917179 Add navigation buttons in popup #4 2025-06-04 13:18:30 +02:00
0f2fa218f3 Update popup positioning after using keyboard shortcut #4 2025-06-04 11:50:41 +02:00
99db143007 Add keyboard shortcut navigation for grammar mistakes
- Implements Ctrl+Š/Đ keyboard shortcuts to navigate between grammar mistakes/highlights. #4
2025-06-04 10:26:56 +02:00
c7c90101a2 Implement tab navigation for grammar mistakes
- The feature is still not fully finished yet.
- Navigation does not work in Quill, textarea and static-content examples.
- But it works well in CKEditor and contenteditable examples.

#4
2025-06-03 14:59:25 +02:00
Aljaž Grilc
b9ab9b6a64 Add double underline drawing for specific grammar rules 2025-05-28 21:01:43 +02:00
Aljaž Grilc
c67adcdc99 Adjust highlight color as requested by the company 2025-05-28 19:46:49 +02:00
be08136a31 Improve color of the popup close button #6 2025-05-21 09:14:06 +02:00
9bc8dfbdfc Display AI suggestions using different color 2025-04-24 14:46:23 +02:00
5cbac62de3 Fix BesQuillService when pasting a simple block of text 2025-04-16 09:27:20 +02:00
c27f9628f4 Fine-tune markup drawing
This adjusts markup color slightly for the dark display, makes it fully
opaque, alternates the squiggly line between grammar and spelling
mistakes to visualize one-word-two-mistake-types, paints most important
mistakes last/on top etc.

All to make the markup as visible as possible.
2025-03-04 11:09:57 +01:00
ef0d35ccee Cleanup 2025-03-04 10:49:12 +01:00
2b54735175 Reformat code 2025-03-04 09:21:51 +01:00
9c2151f182 Add clickable tolerance around grammar mistakes
Users are complaining it is hard to make a click on a mistake when
mistake is covering relative small portion of the text.
2025-03-04 09:21:35 +01:00
a507f24326 Relax line height in the samples 2025-03-03 14:28:39 +01:00
20713b8b5d Add dark color-scheme for samples 2025-03-03 14:28:39 +01:00
72b6fb2d91 Enlarge markup font
Due to popular demand. 😉
2025-03-03 14:21:56 +01:00
9815ddfed0 Get markup text from <canvas> element
This allows setting markup text font separately from the host element.
E.g.: A different font is desired for the markup other than font used in
the edit box.
2025-03-03 14:21:16 +01:00
24216a4dff Merge remote-tracking branch 'remotes/origin/revise-canvas' 2025-03-03 13:10:51 +01:00
61401cb3c0 Fix <textarea> overlay 2025-03-03 13:06:02 +01:00
2dd06fcef4 Fix redraw after zoom change
Resize first, then repaint. The Range.getClientRects() is returning
funny coordinates before resize.
2025-02-28 15:05:09 +01:00
32690de8a7 samples: Fix CKEditor markup style switching 2025-02-28 14:56:12 +01:00
0ff4e96c0a Fix offset compensation
See-also: 51099347f2
2025-02-28 14:52:41 +01:00
5e52b71242 Fix <br> and false missing space grammar mistake report
Fixes: #1
2025-02-24 10:39:32 +01:00
14 changed files with 559 additions and 146 deletions

View File

@@ -91,8 +91,8 @@ Kategorije pravopisnih pravil so:
Privzeto servis uporablja podčrtovanje pravopisnih napak (nastavitev `'underline'`). Videz lahko spreminjamo. Privzeto servis uporablja podčrtovanje pravopisnih napak (nastavitev `'underline'`). Videz lahko spreminjamo.
<img src="samples/markup_underline.png" alt="underline" width="448"/> <img src="samples/images/markup_underline.png" alt="underline" width="448"/>
<img src="samples/markup_lector.png" alt="lector" width="448"/> <img src="samples/images/markup_lector.png" alt="lector" width="448"/>
Levo `'underline'`, desno `'lector'`. Levo `'underline'`, desno `'lector'`.

View File

@@ -15,8 +15,8 @@
<p class="my-block">This is an example of a CKEditor edit control. Edit the text, resize the control or browser window, scroll around, click...</p> <p class="my-block">This is an example of a CKEditor edit control. Edit the text, resize the control or browser window, scroll around, click...</p>
<div id="editor"> <div id="editor">
<p>Tukaj vpišite besedilo ki ga želite popraviti.</p> <p>Tukaj vpišite besedilo ki ga želite popraviti.</p>
<p>Prišla je njena lepa hčera. Smatram da tega nebi bilo potrebno storiti. Predavanje je trajalo dve ure. S njim grem v Kamnik. Janez jutri nebo prišel. Prišel je z 100 idejami.</p> <p>Prišla je njena lepa hčera. Smatram da tega nebi bilo potrebno <i>storiti</i>. Predavanje je trajalo dve ure. S njim grem v Kamnik.<br>Janez jutri nebo prišel. Prišel je z 100 idejami.</p>
<p>To velja tudi v Bledu. To se je zgodilo na velikemu vrtu. Prišel je na Kamnik. On je včeraj prišel z svojo torbo. Dve žemlje prosim. Pogosto brskam po temu forumu. Prišel je včeraj in sicer s otroci. To ne vem. Pogleda vse kar daš v odložišče. Nisem jo videl. Ona izgleda dobro. Pri zanikanju ne smete uporabljati tožilnik. Vlak gre v Ljubljano čez Zidani Most. Skočil je čez okno. Slovenija meji na avstrijo. Jaz pišem v Slovenščini vsak Torek. Novica, da je skupina 25 planincev hodila pod vodstvom gorskega vodnika je napačna in zavajujoča. Želim da poješ kosmizailo. Jaz pogosto brskam po temu forumu. Med tem ko je iskal ključe, so se odprla vrata. V takoimenovanem skladišču je bilo veliko ljudi. V sobi sta dve mize. Stekel je h mami. Videl sem Jurčič Micko. To je bil njegov življenski cilj. Po vrsti popravite vse kar želite. Preden zaspiva mi prebere pravljico. Prišel je s stricom. Oni zadanejo tarčo. Mi gremo teči po polju. Mi gremo peči kruh. Usedel se je k miza. Postreži kosilo! Skul je veslanje z dvemi vesli.</p> <p>To velja tudi v Bledu. To se je zgodilo na velikemu vrtu. Prišel je na Kamnik.<br/>On je včeraj prišel z svojo torbo. Dve žemlje prosim. Pogosto brskam po temu forumu. Prišel je včeraj in sicer s otroci. To ne vem. Pogleda vse kar daš v odložišče. Nisem jo videl. Ona izgleda dobro. Pri zanikanju ne smete uporabljati tožilnik. Vlak gre v Ljubljano čez Zidani Most. Skočil je čez okno. Slovenija meji na avstrijo. Jaz pišem v Slovenščini vsak Torek. Novica, da je skupina 25 planincev hodila pod vodstvom gorskega vodnika je napačna in zavajujoča. Želim da poješ kosmizailo. Jaz pogosto brskam po temu forumu. Med tem ko je iskal ključe, so se odprla vrata. V takoimenovanem skladišču je bilo veliko ljudi. V sobi sta dve mize. Stekel je h mami. Videl sem Jurčič Micko. To je bil njegov življenski cilj. Po vrsti popravite vse kar želite. Preden zaspiva mi prebere pravljico. Prišel je s stricom. Oni zadanejo tarčo. Mi gremo teči po polju. Mi gremo peči kruh. Usedel se je k miza. Postreži kosilo! Skul je veslanje z dvemi vesli.</p>
<p>Na mizo nisem položil knjigo.</p> <p>Na mizo nisem položil knjigo.</p>
<p>Kvazimodo ji je ponavadi prinesel hrano in pijačo, medtem ko je spala, da ne bi videla njegov iznakažen in grd obraz. Poleg tega ji je pustil tudi piščalko, da bi ga lahko priklicala, če bi bilo to potrebno. Kvazimodo se je odločil, da razveseli Esmeraldo in ji obljubi, da ji bo pripeljal Febusa. Toda Febus ni želel priti. Kvazimodo ji je raje lagal, da ni mogel najti Febusa, kot da Esmeraldi pove resnico, ker bi ona trpela.</p> <p>Kvazimodo ji je ponavadi prinesel hrano in pijačo, medtem ko je spala, da ne bi videla njegov iznakažen in grd obraz. Poleg tega ji je pustil tudi piščalko, da bi ga lahko priklicala, če bi bilo to potrebno. Kvazimodo se je odločil, da razveseli Esmeraldo in ji obljubi, da ji bo pripeljal Febusa. Toda Febus ni želel priti. Kvazimodo ji je raje lagal, da ni mogel najti Febusa, kot da Esmeraldi pove resnico, ker bi ona trpela.</p>
</div> </div>

View File

@@ -14,8 +14,8 @@
<p class="my-block">This is an example of a simple <code>&lt;div contenteditable="true"&gt;</code> edit control. Edit the text, resize the control or browser window, scroll around, click...</p> <p class="my-block">This is an example of a simple <code>&lt;div contenteditable="true"&gt;</code> edit control. Edit the text, resize the control or browser window, scroll around, click...</p>
<div id="my-editor" class="my-block my-control" contenteditable="true"> <div id="my-editor" class="my-block my-control" contenteditable="true">
<p>Tukaj vpišite besedilo ki ga želite popraviti.</p> <p>Tukaj vpišite besedilo ki ga želite popraviti.</p>
<p>Prišla je njena lepa hčera. Smatram da tega nebi bilo potrebno storiti. Predavanje je trajalo dve ure. S njim grem v Kamnik. Janez jutri nebo prišel. Prišel je z 100 idejami.</p> <p>Prišla je njena lepa hčera. Smatram da tega nebi bilo potrebno <i>storiti</i>. Predavanje je trajalo dve ure. S njim grem v Kamnik.<br>Janez jutri nebo prišel. Prišel je z 100 idejami.</p>
<p>To velja tudi v Bledu. To se je zgodilo na velikemu vrtu. Prišel je na Kamnik. On je včeraj prišel z svojo torbo. Dve žemlje prosim. Pogosto brskam po temu forumu. Prišel je včeraj in sicer s otroci. To ne vem. Pogleda vse kar daš v odložišče. Nisem jo videl. Ona izgleda dobro. Pri zanikanju ne smete uporabljati tožilnik. Vlak gre v Ljubljano čez Zidani Most. Skočil je čez okno. Slovenija meji na avstrijo. Jaz pišem v Slovenščini vsak Torek. Novica, da je skupina 25 planincev hodila pod vodstvom gorskega vodnika je napačna in zavajujoča. Želim da poješ kosmizailo. Jaz pogosto brskam po temu forumu. Med tem ko je iskal ključe, so se odprla vrata. V takoimenovanem skladišču je bilo veliko ljudi. V sobi sta dve mize. Stekel je h mami. Videl sem Jurčič Micko. To je bil njegov življenski cilj. Po vrsti popravite vse kar želite. Preden zaspiva mi prebere pravljico. Prišel je s stricom. Oni zadanejo tarčo. Mi gremo teči po polju. Mi gremo peči kruh. Usedel se je k miza. Postreži kosilo! Skul je veslanje z dvemi vesli.</p> <p>To velja tudi v Bledu. To se je zgodilo na velikemu vrtu. Prišel je na Kamnik.<br/>On je včeraj prišel z svojo torbo. Dve žemlje prosim. Pogosto brskam po temu forumu. Prišel je včeraj in sicer s otroci. To ne vem. Pogleda vse kar daš v odložišče. Nisem jo videl. Ona izgleda dobro. Pri zanikanju ne smete uporabljati tožilnik. Vlak gre v Ljubljano čez Zidani Most. Skočil je čez okno. Slovenija meji na avstrijo. Jaz pišem v Slovenščini vsak Torek. Novica, da je skupina 25 planincev hodila pod vodstvom gorskega vodnika je napačna in zavajujoča. Želim da poješ kosmizailo. Jaz pogosto brskam po temu forumu. Med tem ko je iskal ključe, so se odprla vrata. V takoimenovanem skladišču je bilo veliko ljudi. V sobi sta dve mize. Stekel je h mami. Videl sem Jurčič Micko. To je bil njegov življenski cilj. Po vrsti popravite vse kar želite. Preden zaspiva mi prebere pravljico. Prišel je s stricom. Oni zadanejo tarčo. Mi gremo teči po polju. Mi gremo peči kruh. Usedel se je k miza. Postreži kosilo! Skul je veslanje z dvemi vesli.</p>
<p>Na mizo nisem položil knjigo.</p> <p>Na mizo nisem položil knjigo.</p>
<p>Kvazimodo ji je ponavadi prinesel hrano in pijačo, medtem ko je spala, da ne bi videla njegov iznakažen in grd obraz. Poleg tega ji je pustil tudi piščalko, da bi ga lahko priklicala, če bi bilo to potrebno. Kvazimodo se je odločil, da razveseli Esmeraldo in ji obljubi, da ji bo pripeljal Febusa. Toda Febus ni želel priti. Kvazimodo ji je raje lagal, da ni mogel najti Febusa, kot da Esmeraldi pove resnico, ker bi ona trpela.</p> <p>Kvazimodo ji je ponavadi prinesel hrano in pijačo, medtem ko je spala, da ne bi videla njegov iznakažen in grd obraz. Poleg tega ji je pustil tudi piščalko, da bi ga lahko priklicala, če bi bilo to potrebno. Kvazimodo se je odločil, da razveseli Esmeraldo in ji obljubi, da ji bo pripeljal Febusa. Toda Febus ni želel priti. Kvazimodo ji je raje lagal, da ni mogel najti Febusa, kot da Esmeraldi pove resnico, ker bi ona trpela.</p>
</div> </div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

View File

@@ -75,8 +75,8 @@ Kvazimodo ji je ponavadi prinesel hrano in pijačo, medtem ko je spala, da ne bi
let my_ckeditor = null let my_ckeditor = null
ClassicEditor.create(document.querySelector('#ckeditor-control')) ClassicEditor.create(document.querySelector('#ckeditor-control'))
.then(newEditor => { .then(newEditor => {
my_ckeditor = newEditor my_ckeditor = newEditor.ui.view.editable.element
BesCKService.register(newEditor.ui.view.editable.element, newEditor, new BesCKStatusIconEventSink()) BesCKService.register(my_ckeditor, newEditor, new BesCKStatusIconEventSink())
}) })
.catch(error => console.error(error)) .catch(error => console.error(error))
</script> </script>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

View File

@@ -19,23 +19,10 @@
scroll around, click...</p> scroll around, click...</p>
<div id="editor"> <div id="editor">
<p>Tukaj vpišite besedilo ki ga želite popraviti.</p> <p>Tukaj vpišite besedilo ki ga želite popraviti.</p>
<p>Prišla je njena lepa hčera. Smatram da tega nebi bilo potrebno storiti. Predavanje je trajalo dve ure. S njim <p>Prišla je njena lepa hčera. Smatram da tega nebi bilo potrebno <i>storiti</i>. Predavanje je trajalo dve ure. S njim grem v Kamnik.<br>Janez jutri nebo prišel. Prišel je z 100 idejami.</p>
grem v Kamnik. Janez jutri nebo prišel. Prišel je z 100 idejami.</p> <p>To velja tudi v Bledu. To se je zgodilo na velikemu vrtu. Prišel je na Kamnik.<br/>On je včeraj prišel z svojo torbo. Dve žemlje prosim. Pogosto brskam po temu forumu. Prišel je včeraj in sicer s otroci. To ne vem. Pogleda vse kar daš v odložišče. Nisem jo videl. Ona izgleda dobro. Pri zanikanju ne smete uporabljati tožilnik. Vlak gre v Ljubljano čez Zidani Most. Skočil je čez okno. Slovenija meji na avstrijo. Jaz pišem v Slovenščini vsak Torek. Novica, da je skupina 25 planincev hodila pod vodstvom gorskega vodnika je napačna in zavajujoča. Želim da poješ kosmizailo. Jaz pogosto brskam po temu forumu. Med tem ko je iskal ključe, so se odprla vrata. V takoimenovanem skladišču je bilo veliko ljudi. V sobi sta dve mize. Stekel je h mami. Videl sem Jurčič Micko. To je bil njegov življenski cilj. Po vrsti popravite vse kar želite. Preden zaspiva mi prebere pravljico. Prišel je s stricom. Oni zadanejo tarčo. Mi gremo teči po polju. Mi gremo peči kruh. Usedel se je k miza. Postreži kosilo! Skul je veslanje z dvemi vesli.</p>
<p>To velja tudi v Bledu. To se je zgodilo na velikemu vrtu. Prišel je na Kamnik. On je včeraj prišel z svojo torbo.
Dve žemlje prosim. Pogosto brskam po temu forumu. Prišel je včeraj in sicer s otroci. To ne vem. Pogleda vse kar
daš v odložišče. Nisem jo videl. Ona izgleda dobro. Pri zanikanju ne smete uporabljati tožilnik. Vlak gre v
Ljubljano čez Zidani Most. Skočil je čez okno. Slovenija meji na avstrijo. Jaz pišem v Slovenščini vsak Torek.
Novica, da je skupina 25 planincev hodila pod vodstvom gorskega vodnika je napačna in zavajujoča. Želim da poješ
kosmizailo. Jaz pogosto brskam po temu forumu. Med tem ko je iskal ključe, so se odprla vrata. V takoimenovanem
skladišču je bilo veliko ljudi. V sobi sta dve mize. Stekel je h mami. Videl sem Jurčič Micko. To je bil njegov
življenski cilj. Po vrsti popravite vse kar želite. Preden zaspiva mi prebere pravljico. Prišel je s stricom. Oni
zadanejo tarčo. Mi gremo teči po polju. Mi gremo peči kruh. Usedel se je k miza. Postreži kosilo! Skul je veslanje
z dvemi vesli.</p>
<p>Na mizo nisem položil knjigo.</p> <p>Na mizo nisem položil knjigo.</p>
<p>Kvazimodo ji je ponavadi prinesel hrano in pijačo, medtem ko je spala, da ne bi videla njegov iznakažen in grd <p>Kvazimodo ji je ponavadi prinesel hrano in pijačo, medtem ko je spala, da ne bi videla njegov iznakažen in grd obraz. Poleg tega ji je pustil tudi piščalko, da bi ga lahko priklicala, če bi bilo to potrebno. Kvazimodo se je odločil, da razveseli Esmeraldo in ji obljubi, da ji bo pripeljal Febusa. Toda Febus ni želel priti. Kvazimodo ji je raje lagal, da ni mogel najti Febusa, kot da Esmeraldi pove resnico, ker bi ona trpela.</p>
obraz. Poleg tega ji je pustil tudi piščalko, da bi ga lahko priklicala, če bi bilo to potrebno. Kvazimodo se je
odločil, da razveseli Esmeraldo in ji obljubi, da ji bo pripeljal Febusa. Toda Febus ni želel priti. Kvazimodo ji
je raje lagal, da ni mogel najti Febusa, kot da Esmeraldi pove resnico, ker bi ona trpela.</p>
</div> </div>
<bes-popup-el /> <bes-popup-el />
<script> <script>

View File

@@ -14,8 +14,8 @@
<p class="my-block">This is an example of a simple <code>&lt;div contenteditable="true"&gt;</code> edit control with style grammar rules disabled. Compare the proofing results with <a href="div-contenteditable.html">the default example</a>.</p> <p class="my-block">This is an example of a simple <code>&lt;div contenteditable="true"&gt;</code> edit control with style grammar rules disabled. Compare the proofing results with <a href="div-contenteditable.html">the default example</a>.</p>
<div id="my-editor" class="my-block my-control" contenteditable="true"> <div id="my-editor" class="my-block my-control" contenteditable="true">
<p>Tukaj vpišite besedilo ki ga želite popraviti.</p> <p>Tukaj vpišite besedilo ki ga želite popraviti.</p>
<p>Prišla je njena lepa hčera. Smatram da tega nebi bilo potrebno storiti. Predavanje je trajalo dve ure. S njim grem v Kamnik. Janez jutri nebo prišel. Prišel je z 100 idejami.</p> <p>Prišla je njena lepa hčera. Smatram da tega nebi bilo potrebno <i>storiti</i>. Predavanje je trajalo dve ure. S njim grem v Kamnik.<br>Janez jutri nebo prišel. Prišel je z 100 idejami.</p>
<p>To velja tudi v Bledu. To se je zgodilo na velikemu vrtu. Prišel je na Kamnik. On je včeraj prišel z svojo torbo. Dve žemlje prosim. Pogosto brskam po temu forumu. Prišel je včeraj in sicer s otroci. To ne vem. Pogleda vse kar daš v odložišče. Nisem jo videl. Ona izgleda dobro. Pri zanikanju ne smete uporabljati tožilnik. Vlak gre v Ljubljano čez Zidani Most. Skočil je čez okno. Slovenija meji na avstrijo. Jaz pišem v Slovenščini vsak Torek. Novica, da je skupina 25 planincev hodila pod vodstvom gorskega vodnika je napačna in zavajujoča. Želim da poješ kosmizailo. Jaz pogosto brskam po temu forumu. Med tem ko je iskal ključe, so se odprla vrata. V takoimenovanem skladišču je bilo veliko ljudi. V sobi sta dve mize. Stekel je h mami. Videl sem Jurčič Micko. To je bil njegov življenski cilj. Po vrsti popravite vse kar želite. Preden zaspiva mi prebere pravljico. Prišel je s stricom. Oni zadanejo tarčo. Mi gremo teči po polju. Mi gremo peči kruh. Usedel se je k miza. Postreži kosilo! Skul je veslanje z dvemi vesli.</p> <p>To velja tudi v Bledu. To se je zgodilo na velikemu vrtu. Prišel je na Kamnik.<br/>On je včeraj prišel z svojo torbo. Dve žemlje prosim. Pogosto brskam po temu forumu. Prišel je včeraj in sicer s otroci. To ne vem. Pogleda vse kar daš v odložišče. Nisem jo videl. Ona izgleda dobro. Pri zanikanju ne smete uporabljati tožilnik. Vlak gre v Ljubljano čez Zidani Most. Skočil je čez okno. Slovenija meji na avstrijo. Jaz pišem v Slovenščini vsak Torek. Novica, da je skupina 25 planincev hodila pod vodstvom gorskega vodnika je napačna in zavajujoča. Želim da poješ kosmizailo. Jaz pogosto brskam po temu forumu. Med tem ko je iskal ključe, so se odprla vrata. V takoimenovanem skladišču je bilo veliko ljudi. V sobi sta dve mize. Stekel je h mami. Videl sem Jurčič Micko. To je bil njegov življenski cilj. Po vrsti popravite vse kar želite. Preden zaspiva mi prebere pravljico. Prišel je s stricom. Oni zadanejo tarčo. Mi gremo teči po polju. Mi gremo peči kruh. Usedel se je k miza. Postreži kosilo! Skul je veslanje z dvemi vesli.</p>
<p>Na mizo nisem položil knjigo.</p> <p>Na mizo nisem položil knjigo.</p>
<p>Kvazimodo ji je ponavadi prinesel hrano in pijačo, medtem ko je spala, da ne bi videla njegov iznakažen in grd obraz. Poleg tega ji je pustil tudi piščalko, da bi ga lahko priklicala, če bi bilo to potrebno. Kvazimodo se je odločil, da razveseli Esmeraldo in ji obljubi, da ji bo pripeljal Febusa. Toda Febus ni želel priti. Kvazimodo ji je raje lagal, da ni mogel najti Febusa, kot da Esmeraldi pove resnico, ker bi ona trpela.</p> <p>Kvazimodo ji je ponavadi prinesel hrano in pijačo, medtem ko je spala, da ne bi videla njegov iznakažen in grd obraz. Poleg tega ji je pustil tudi piščalko, da bi ga lahko priklicala, če bi bilo to potrebno. Kvazimodo se je odločil, da razveseli Esmeraldo in ji obljubi, da ji bo pripeljal Febusa. Toda Febus ni želel priti. Kvazimodo ji je raje lagal, da ni mogel najti Febusa, kot da Esmeraldi pove resnico, ker bi ona trpela.</p>
</div> </div>

View File

@@ -13,8 +13,8 @@
<p class="my-block">This is an example of grammar-checking static HTML content. The below text contains proofing markup.</p> <p class="my-block">This is an example of grammar-checking static HTML content. The below text contains proofing markup.</p>
<div class="my-block my-control bes-service"> <div class="my-block my-control bes-service">
<p>Tukaj vpišite besedilo ki ga želite popraviti.</p> <p>Tukaj vpišite besedilo ki ga želite popraviti.</p>
<p>Prišla je njena lepa hčera. Smatram da tega nebi bilo potrebno storiti. Predavanje je trajalo dve ure. S njim grem v Kamnik. Janez jutri nebo prišel. Prišel je z 100 idejami.</p> <p>Prišla je njena lepa hčera. Smatram da tega nebi bilo potrebno <i>storiti</i>. Predavanje je trajalo dve ure. S njim grem v Kamnik.<br>Janez jutri nebo prišel. Prišel je z 100 idejami.</p>
<p>To velja tudi v Bledu. To se je zgodilo na velikemu vrtu. Prišel je na Kamnik. On je včeraj prišel z svojo torbo. Dve žemlje prosim. Pogosto brskam po temu forumu. Prišel je včeraj in sicer s otroci. To ne vem. Pogleda vse kar daš v odložišče. Nisem jo videl. Ona izgleda dobro. Pri zanikanju ne smete uporabljati tožilnik. Vlak gre v Ljubljano čez Zidani Most. Skočil je čez okno. Slovenija meji na avstrijo. Jaz pišem v Slovenščini vsak Torek. Novica, da je skupina 25 planincev hodila pod vodstvom gorskega vodnika je napačna in zavajujoča. Želim da poješ kosmizailo. Jaz pogosto brskam po temu forumu. Med tem ko je iskal ključe, so se odprla vrata. V takoimenovanem skladišču je bilo veliko ljudi. V sobi sta dve mize. Stekel je h mami. Videl sem Jurčič Micko. To je bil njegov življenski cilj. Po vrsti popravite vse kar želite. Preden zaspiva mi prebere pravljico. Prišel je s stricom. Oni zadanejo tarčo. Mi gremo teči po polju. Mi gremo peči kruh. Usedel se je k miza. Postreži kosilo! Skul je veslanje z dvemi vesli.</p> <p>To velja tudi v Bledu. To se je zgodilo na velikemu vrtu. Prišel je na Kamnik.<br/>On je včeraj prišel z svojo torbo. Dve žemlje prosim. Pogosto brskam po temu forumu. Prišel je včeraj in sicer s otroci. To ne vem. Pogleda vse kar daš v odložišče. Nisem jo videl. Ona izgleda dobro. Pri zanikanju ne smete uporabljati tožilnik. Vlak gre v Ljubljano čez Zidani Most. Skočil je čez okno. Slovenija meji na avstrijo. Jaz pišem v Slovenščini vsak Torek. Novica, da je skupina 25 planincev hodila pod vodstvom gorskega vodnika je napačna in zavajujoča. Želim da poješ kosmizailo. Jaz pogosto brskam po temu forumu. Med tem ko je iskal ključe, so se odprla vrata. V takoimenovanem skladišču je bilo veliko ljudi. V sobi sta dve mize. Stekel je h mami. Videl sem Jurčič Micko. To je bil njegov življenski cilj. Po vrsti popravite vse kar želite. Preden zaspiva mi prebere pravljico. Prišel je s stricom. Oni zadanejo tarčo. Mi gremo teči po polju. Mi gremo peči kruh. Usedel se je k miza. Postreži kosilo! Skul je veslanje z dvemi vesli.</p>
<p>Na mizo nisem položil knjigo.</p> <p>Na mizo nisem položil knjigo.</p>
<p>Kvazimodo ji je ponavadi prinesel hrano in pijačo, medtem ko je spala, da ne bi videla njegov iznakažen in grd obraz. Poleg tega ji je pustil tudi piščalko, da bi ga lahko priklicala, če bi bilo to potrebno. Kvazimodo se je odločil, da razveseli Esmeraldo in ji obljubi, da ji bo pripeljal Febusa. Toda Febus ni želel priti. Kvazimodo ji je raje lagal, da ni mogel najti Febusa, kot da Esmeraldi pove resnico, ker bi ona trpela.</p> <p>Kvazimodo ji je ponavadi prinesel hrano in pijačo, medtem ko je spala, da ne bi videla njegov iznakažen in grd obraz. Poleg tega ji je pustil tudi piščalko, da bi ga lahko priklicala, če bi bilo to potrebno. Kvazimodo se je odločil, da razveseli Esmeraldo in ji obljubi, da ji bo pripeljal Febusa. Toda Febus ni želel priti. Kvazimodo ji je raje lagal, da ni mogel najti Febusa, kot da Esmeraldi pove resnico, ker bi ona trpela.</p>
</div> </div>

View File

@@ -1,3 +1,14 @@
body {
color: #000;
background-color: #eee;
}
input, textarea {
color: #000;
background-color: #f5f5f5;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.my-block { .my-block {
max-width: 500px; max-width: 500px;
margin: 0 auto; margin: 0 auto;
@@ -13,7 +24,6 @@
border-radius: 10px; border-radius: 10px;
background-color: #f5f5f5; background-color: #f5f5f5;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
line-height: 20px;
} }
.ck-editor__editable { .ck-editor__editable {
@@ -52,3 +62,22 @@
.bes-status-icon.bes-status-mistakes { .bes-status-icon.bes-status-mistakes {
background-image: url('images/mistake-svgrepo-com.svg'); background-image: url('images/mistake-svgrepo-com.svg');
} }
@media (prefers-color-scheme: dark) {
body {
color: #eee;
background-color: #444;
font-weight: bolder;
}
input, textarea {
color: #eee;
background-color: #222;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
}
.my-control {
background-color: #222;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
}
}

View File

@@ -1,4 +1,5 @@
// TODO: Research if there is a way to disable languageTool & Grammarly extensions in CKEditor // TODO: Research if there is a way to disable languageTool & Grammarly extensions in CKEditor
// TODO: Add mutation observer should any style of hostElement/textElement change and repaint markup (e.g. notice font-weight difference when toggling light/dark color-scheme)
/** /**
* Collection of all grammar checking services in the document * Collection of all grammar checking services in the document
@@ -59,10 +60,11 @@ class BesService {
this.hostElement.setAttribute('data-gramm', 'false') this.hostElement.setAttribute('data-gramm', 'false')
this.hostElement.setAttribute('data-gramm_editor', 'false') this.hostElement.setAttribute('data-gramm_editor', 'false')
this.hostElement.setAttribute('data-enable-grammarly', 'false') this.hostElement.setAttribute('data-enable-grammarly', 'false')
this.textFont = window.getComputedStyle(this.hostElement).fontFamily
this.onScroll = this.onScroll.bind(this) this.onScroll = this.onScroll.bind(this)
this.hostElement.addEventListener('scroll', this.onScroll) this.hostElement.addEventListener('scroll', this.onScroll)
this.onShortcutNavigation = this.onShortcutNavigation.bind(this)
this.hostElement.addEventListener('keydown', this.onShortcutNavigation)
this.hostBoundingClientRect = this.hostElement.getBoundingClientRect() this.hostBoundingClientRect = this.hostElement.getBoundingClientRect()
this.mutationObserver = new MutationObserver(this.onBodyMutate.bind(this)) this.mutationObserver = new MutationObserver(this.onBodyMutate.bind(this))
@@ -109,6 +111,7 @@ class BesService {
if (this.abortController) this.abortController.abort() if (this.abortController) this.abortController.abort()
besServices = besServices.filter(item => item !== this) besServices = besServices.filter(item => item !== this)
this.mutationObserver.disconnect() this.mutationObserver.disconnect()
this.hostElement.removeEventListener('keydown', this.onShortcutNavigation)
this.hostElement.removeEventListener('scroll', this.onScroll) this.hostElement.removeEventListener('scroll', this.onScroll)
this.hostElement.setAttribute('spellcheck', this.originalSpellcheck) this.hostElement.setAttribute('spellcheck', this.originalSpellcheck)
this.hostElement.setAttribute('data-gramm', this.originalDataGramm) this.hostElement.setAttribute('data-gramm', this.originalDataGramm)
@@ -276,6 +279,9 @@ class BesService {
*/ */
onProofingProgress(numberOfMatches) { onProofingProgress(numberOfMatches) {
this.proofingMatches += numberOfMatches this.proofingMatches += numberOfMatches
// Sorting the array here is preferable to sorting only in onEndProofing.This way it allows users to interact
// with and navigate newly detected mistakes as soon as they appear.
this.sortMatchesArray()
if (this.eventSink && 'proofingProgress' in this.eventSink) if (this.eventSink && 'proofingProgress' in this.eventSink)
this.eventSink.proofingProgress(this) this.eventSink.proofingProgress(this)
if (--this.proofingCount <= 0) this.onEndProofing() if (--this.proofingCount <= 0) this.onEndProofing()
@@ -324,6 +330,46 @@ class BesService {
} }
} }
/**
* Called to report keydown event
*
* @param {Event} e
*/
onShortcutNavigation(e) {
switch (e.code) {
case 'BracketLeft':
if (e.ctrlKey) {
// Handle Ctrl + [ OR Ctrl + Š
e.preventDefault()
e.stopPropagation()
BesService.findNextMistake(this, -1)
}
break
case 'BracketRight':
if (e.ctrlKey) {
// Handle Ctrl + ] OR Ctrl + Đ
e.preventDefault()
e.stopPropagation()
BesService.findNextMistake(this, 1)
}
break
case 'Enter':
// Handle Ctrl + Enter
if (e.ctrlKey) {
if (!this.highlightElements.length) return
e.preventDefault()
e.stopPropagation()
this.acceptReplacement()
}
break
case 'Escape':
this.dismissPopup()
break
default:
break
}
}
/** /**
* Called to report repositioning * Called to report repositioning
*/ */
@@ -362,23 +408,33 @@ class BesService {
* @param {*} match Grammar checking rule match * @param {*} match Grammar checking rule match
*/ */
drawMistakeMarkup(match) { drawMistakeMarkup(match) {
const range = match.range
match.highlights = Array.from(range.getClientRects())
if (match.highlights.length === 0) return
const canvasPanelRect = this.canvasPanel.getBoundingClientRect() const canvasPanelRect = this.canvasPanel.getBoundingClientRect()
for (let rect of match.highlights) { match.highlights = BesService.getClientRects(
rect.x -= canvasPanelRect.x match.range,
rect.y -= canvasPanelRect.y canvasPanelRect.x,
} canvasPanelRect.y
)
if (match.highlights.length === 0) return
const dpr = window.devicePixelRatio const dpr = window.devicePixelRatio
this.ctx.lineWidth = 2 * dpr // Use 2 for clearer visibility this.ctx.lineWidth = 2 * dpr // Use 2 for clearer visibility
let amplitude = 0
const ruleId = match.match.rule.id const ruleId = match.match.rule.id
this.ctx.strokeStyle = ruleId.startsWith('MORFOLOGIK_RULE') if (ruleId.startsWith('MORFOLOGIK_RULE')) {
? 'rgba(0, 123, 255, 0.8)' const styles = window.getComputedStyle(this.highlightSpelling)
: 'rgba(255, 115, 0, 0.8)' this.ctx.strokeStyle = styles.color
this.ctx.fillStyle = ruleId.startsWith('MORFOLOGIK_RULE') this.ctx.fillStyle = styles.color
? 'rgba(0, 123, 255, 0.8)' amplitude = -1
: 'rgba(255, 115, 0, 0.8)' } else if (ruleId === 'BESANA_178' /*PR_VNAP_POPRAVEK_UI*/) {
const styles = window.getComputedStyle(this.highlightAI)
this.ctx.strokeStyle = styles.color
this.ctx.fillStyle = styles.color
amplitude = 1
} else {
const styles = window.getComputedStyle(this.highlightGrammar)
this.ctx.strokeStyle = styles.color
this.ctx.fillStyle = styles.color
amplitude = 1
}
let markerY1, markerY2 let markerY1, markerY2
switch (this.markupStyle) { switch (this.markupStyle) {
case 'lector': case 'lector':
@@ -486,21 +542,29 @@ class BesService {
const scale = (markerY2 - markerY1) / 18 const scale = (markerY2 - markerY1) / 18
if (/^\s+$/.test(toRemove)) { if (/^\s+$/.test(toRemove)) {
const rect = this.makeRange( const rect = BesService.getClientRects(
this.makeRange(
match.data, match.data,
match.match.offset, match.match.offset,
match.match.offset - lengthDiff match.match.offset - lengthDiff
)?.getClientRects()[0] ),
canvasPanelRect.x,
canvasPanelRect.y
)[0]
const x = (rect.left + rect.right) / 2 const x = (rect.left + rect.right) / 2
const y1 = rect.top const y1 = rect.top
const y2 = rect.bottom const y2 = rect.bottom
this.drawWrongSpacing(x, y1, y2, scale) this.drawWrongSpacing(x, y1, y2, scale)
} else { } else {
for (let rect of this.makeRange( for (let rect of BesService.getClientRects(
this.makeRange(
match.data, match.data,
match.match.offset, match.match.offset,
match.match.offset - lengthDiff match.match.offset - lengthDiff
)?.getClientRects()) ),
canvasPanelRect.x,
canvasPanelRect.y
))
this.drawExcessiveText( this.drawExcessiveText(
rect.left, rect.left,
rect.bottom, rect.bottom,
@@ -516,21 +580,29 @@ class BesService {
const scale = (markerY2 - markerY1) / 18 const scale = (markerY2 - markerY1) / 18
if (/^\s+$/.test(toRemove)) { if (/^\s+$/.test(toRemove)) {
const rect = this.makeRange( const rect = BesService.getClientRects(
this.makeRange(
match.data, match.data,
match.match.offset + match.match.length + lengthDiff, match.match.offset + match.match.length + lengthDiff,
match.match.offset + match.match.length match.match.offset + match.match.length
)?.getClientRects()[0] ),
canvasPanelRect.x,
canvasPanelRect.y
)[0]
const x = (rect.left + rect.right) / 2 const x = (rect.left + rect.right) / 2
const y1 = rect.top const y1 = rect.top
const y2 = rect.bottom const y2 = rect.bottom
this.drawWrongSpacing(x, y1, y2, scale) this.drawWrongSpacing(x, y1, y2, scale)
} else { } else {
for (let rect of this.makeRange( for (let rect of BesService.getClientRects(
this.makeRange(
match.data, match.data,
match.match.offset + match.match.length + lengthDiff, match.match.offset + match.match.length + lengthDiff,
match.match.offset + match.match.length match.match.offset + match.match.length
)?.getClientRects()) ),
canvasPanelRect.x,
canvasPanelRect.y
))
this.drawExcessiveText( this.drawExcessiveText(
rect.left, rect.left,
rect.bottom, rect.bottom,
@@ -574,12 +646,14 @@ class BesService {
} }
} else { } else {
// Patch differences. // Patch differences.
const rects = Array.from( const rects = BesService.getClientRects(
this.makeRange( this.makeRange(
match.data, match.data,
match.match.offset + lengthL, match.match.offset + lengthL,
match.match.offset + match.match.length - lengthR match.match.offset + match.match.length - lengthR
)?.getClientRects() ),
canvasPanelRect.x,
canvasPanelRect.y
) )
markerY1 = Math.min(...rects.map(rect => rect.top)) markerY1 = Math.min(...rects.map(rect => rect.top))
markerY2 = Math.max(...rects.map(rect => rect.bottom)) markerY2 = Math.max(...rects.map(rect => rect.bottom))
@@ -664,7 +738,9 @@ class BesService {
const x2 = rect.right const x2 = rect.right
const y = rect.bottom const y = rect.bottom
const scale = (rect.bottom - rect.top) / 18 const scale = (rect.bottom - rect.top) / 18
this.drawAttentionRequired(x1, x2, y, scale) if (ruleId !== 'MORFOLOGIK_RULE') {
this.drawDoubleUnderline(x1, x2, y, scale)
} else this.drawAttentionRequired(x1, x2, y, amplitude, scale)
} }
markerY1 = Math.min(...match.highlights.map(rect => rect.top)) markerY1 = Math.min(...match.highlights.map(rect => rect.top))
@@ -706,7 +782,7 @@ class BesService {
this.ctx.stroke() this.ctx.stroke()
if (comment) { if (comment) {
this.ctx.font = `${12 * scale * dpr}px ${this.textFont}` this.setCtxFont(scale, dpr)
this.ctx.textAlign = 'center' this.ctx.textAlign = 'center'
this.ctx.textBaseline = 'bottom' this.ctx.textBaseline = 'bottom'
this.ctx.fillText('?', (x + 2 * scale) * dpr, (y - 6 * scale) * dpr) this.ctx.fillText('?', (x + 2 * scale) * dpr, (y - 6 * scale) * dpr)
@@ -790,7 +866,7 @@ class BesService {
this.ctx.lineTo(x2 * dpr, (y2 + 6 * scale) * dpr) this.ctx.lineTo(x2 * dpr, (y2 + 6 * scale) * dpr)
this.ctx.stroke() this.ctx.stroke()
this.ctx.font = `${12 * scale * dpr}px ${this.textFont}` this.setCtxFont(scale, dpr)
this.ctx.textAlign = 'left' // Thou we want the text to be centered, we align it manually to prevent it getting off canvas. this.ctx.textAlign = 'left' // Thou we want the text to be centered, we align it manually to prevent it getting off canvas.
this.ctx.textBaseline = 'bottom' this.ctx.textBaseline = 'bottom'
const textMetrics = this.ctx.measureText(text) const textMetrics = this.ctx.measureText(text)
@@ -825,7 +901,7 @@ class BesService {
this.ctx.lineTo(x * dpr, (y2 + 6 * scale) * dpr) this.ctx.lineTo(x * dpr, (y2 + 6 * scale) * dpr)
this.ctx.stroke() this.ctx.stroke()
this.ctx.font = `${12 * scale * dpr}px ${this.textFont}` this.setCtxFont(scale, dpr)
this.ctx.textAlign = 'left' // Thou we want the text to be centered, we align it manually to prevent it getting off canvas. this.ctx.textAlign = 'left' // Thou we want the text to be centered, we align it manually to prevent it getting off canvas.
this.ctx.textBaseline = 'bottom' this.ctx.textBaseline = 'bottom'
const textMetrics = this.ctx.measureText(text) const textMetrics = this.ctx.measureText(text)
@@ -848,21 +924,69 @@ class BesService {
* @param {Number} x1 Sign left [px] * @param {Number} x1 Sign left [px]
* @param {Number} x2 Sign right [px] * @param {Number} x2 Sign right [px]
* @param {Number} y Sign baseline [px] * @param {Number} y Sign baseline [px]
* @param {Number} amplitude Sign amplitude [px]
* @param {Number} scale Sign scale * @param {Number} scale Sign scale
*/ */
drawAttentionRequired(x1, x2, y, scale) { drawAttentionRequired(x1, x2, y, amplitude, scale) {
const dpr = window.devicePixelRatio const dpr = window.devicePixelRatio
this.ctx.beginPath() this.ctx.beginPath()
this.ctx.moveTo(x1 * dpr, (y - scale) * dpr) this.ctx.moveTo(x1 * dpr, (y - amplitude * scale) * dpr)
for (let x = x1; ; ) { for (let x = x1; ; ) {
if (x >= x2) break if (x >= x2) break
this.ctx.lineTo((x += 2 * scale) * dpr, (y + scale) * dpr) this.ctx.lineTo((x += 2 * scale) * dpr, (y + amplitude * scale) * dpr)
if (x >= x2) break if (x >= x2) break
this.ctx.lineTo((x += 2 * scale) * dpr, (y - scale) * dpr) this.ctx.lineTo((x += 2 * scale) * dpr, (y - amplitude * scale) * dpr)
} }
this.ctx.stroke() this.ctx.stroke()
} }
/**
*
* @param {Number} x1 Sign left [px]
* @param {Number} x2 Sign right [px]
* @param {Number} y Sign baseline [px]
* @param {Number} scale Sign scale
*/
drawDoubleUnderline(x1, x2, y, scale) {
const dpr = window.devicePixelRatio
this.ctx.beginPath()
this.ctx.moveTo(x1 * dpr, (y - 2 * scale) * dpr)
this.ctx.lineTo(x2 * dpr, (y - 2 * scale) * dpr)
this.ctx.moveTo(x1 * dpr, (y + 1 * scale) * dpr)
this.ctx.lineTo(x2 * dpr, (y + 1 * scale) * dpr)
this.ctx.stroke()
}
/**
* Sets markup font
*
* @param {Number} scale Sign scale
* @param {Number} dpr Device pixel ratio
*/
setCtxFont(scale, dpr) {
const styles = window.getComputedStyle(this.canvasPanel)
this.ctx.font = `${styles.fontStyle} ${styles.fontWeight} ${
14 * scale * dpr
}px ${styles.fontFamily}`
}
/**
* Calculates rectangles covering a given range and compensates for scroll offset
*
* @param {Range} range Range to get client rectangles for
* @param {Number} offsetX X offset to subtract from coordinates [px]
* @param {Number} offsetY Y offset to subtract from coordinates [px]
* @returns Array of rectangles
*/
static getClientRects(range, offsetX, offsetY) {
const rects = Array.from(range.getClientRects())
for (let rect of rects) {
rect.x -= offsetX
rect.y -= offsetY
}
return rects
}
/** /**
* Calculates common string prefix length * Calculates common string prefix length
* *
@@ -898,10 +1022,21 @@ class BesService {
* @param {Number} x X coordinate * @param {Number} x X coordinate
* @param {Number} y Y coordinate * @param {Number} y Y coordinate
* @param {DOMRect} rect Rectangle * @param {DOMRect} rect Rectangle
* @param {Number} tolerance Extra margin around the rectangle treated as "inside"
* @returns * @returns
*/ */
static isPointInRect(x, y, rect) { static isPointInRect(x, y, rect, tolerance) {
return rect.left <= x && x < rect.right && rect.top <= y && y < rect.bottom return (
rect.left - tolerance <= x &&
x < rect.right + tolerance &&
rect.top - tolerance <= y &&
y < rect.bottom + tolerance
)
}
static arrowBtnNavigation(value, service) {
const direction = value === 'forward' ? 1 : value === 'back' ? -1 : 0
BesService.findNextMistake(service, direction)
} }
/** /**
@@ -919,7 +1054,20 @@ class BesService {
this.ctx = this.canvasPanel.getContext('2d') this.ctx = this.canvasPanel.getContext('2d')
this.ctx.scale(1, 1) this.ctx.scale(1, 1)
this.highlightSpelling = document.createElement('div')
this.highlightSpelling.classList.add('bes-highlight-placeholder')
this.highlightSpelling.classList.add('bes-highlight-spelling')
this.highlightAI = document.createElement('div')
this.highlightAI.classList.add('bes-highlight-placeholder')
this.highlightAI.classList.add('bes-highlight-ai')
this.highlightGrammar = document.createElement('div')
this.highlightGrammar.classList.add('bes-highlight-placeholder')
this.highlightGrammar.classList.add('bes-highlight-grammar')
this.correctionPanel.appendChild(this.scrollPanel) this.correctionPanel.appendChild(this.scrollPanel)
this.correctionPanel.appendChild(this.highlightSpelling)
this.correctionPanel.appendChild(this.highlightAI)
this.correctionPanel.appendChild(this.highlightGrammar)
this.scrollPanel.appendChild(this.canvasPanel) this.scrollPanel.appendChild(this.canvasPanel)
this.textElement.parentElement.insertBefore( this.textElement.parentElement.insertBefore(
this.correctionPanel, this.correctionPanel,
@@ -942,23 +1090,6 @@ class BesService {
this.disableMutationObserver() this.disableMutationObserver()
const styles = window.getComputedStyle(this.hostElement) const styles = window.getComputedStyle(this.hostElement)
this.textFont = styles.fontFamily
// Resize canvas if needed.
this.canvasPanel.style.width = `${this.hostElement.scrollWidth}px`
this.canvasPanel.style.height = `${this.hostElement.scrollHeight}px`
const dpr = window.devicePixelRatio
const canvasPanelRect = this.canvasPanel.getBoundingClientRect()
const newCanvasWidth = Math.round(canvasPanelRect.width * dpr)
const newCanvasHeight = Math.round(canvasPanelRect.height * dpr)
if (
this.canvasPanel.width !== newCanvasWidth ||
this.canvasPanel.height !== newCanvasHeight
) {
this.canvasPanel.width = newCanvasWidth
this.canvasPanel.height = newCanvasHeight
this.redrawAllMistakeMarkup()
}
// Note: Firefox is not happy when syncing all margins at once. // Note: Firefox is not happy when syncing all margins at once.
this.scrollPanel.style.marginLeft = styles.marginLeft this.scrollPanel.style.marginLeft = styles.marginLeft
@@ -982,24 +1113,46 @@ class BesService {
this.scrollPanel.style.height = `${hostRect.height}px` this.scrollPanel.style.height = `${hostRect.height}px`
} }
// Resize canvas if needed.
this.canvasPanel.style.width = `${this.hostElement.scrollWidth}px`
this.canvasPanel.style.height = `${this.hostElement.scrollHeight}px`
const dpr = window.devicePixelRatio
const canvasPanelRect = this.canvasPanel.getBoundingClientRect()
const newCanvasWidth = Math.round(canvasPanelRect.width * dpr)
const newCanvasHeight = Math.round(canvasPanelRect.height * dpr)
if (
this.canvasPanel.width !== newCanvasWidth ||
this.canvasPanel.height !== newCanvasHeight
) {
this.canvasPanel.width = newCanvasWidth
this.canvasPanel.height = newCanvasHeight
this.redrawAllMistakeMarkup()
}
this.enableMutationObserver() this.enableMutationObserver()
} }
/** /**
* Prepares and displays popup. * Prepares and displays popup.
* *
* @param {*} elMatch Array containing block element/paragraph containing grammar checking rule match and a match * @param {*} hits Array containing block element/paragraph containing grammar checking rule match and a match
* @param {PointerEvent} source Click event source * @param {PointerEvent} source Click event source
*/ */
preparePopup(elMatch, source) { preparePopup(hits, source) {
this.dismissPopup() this.dismissPopup()
const popup = document.querySelector('bes-popup-el') const popup = document.querySelector('bes-popup-el')
BesPopup.clearReplacements() BesPopup.clearReplacements()
elMatch.forEach(({ el, match }) => { hits.forEach(({ el, match }) => {
popup.setContent(el, match, this, this.isContentEditable()) popup.setContent(el, match, this, this.isContentEditable())
this.highlightMistake(match) this.highlightMistake(match)
const containerRect = this.hostElement.getBoundingClientRect()
match.highlights.forEach(rect => {
const clientX = rect.x + containerRect.left
const clientY =
rect.y + containerRect.top + rect.height - this.hostElement.scrollTop
popup.show(clientX, clientY, this)
})
}) })
popup.show(source.clientX, source.clientY)
} }
/** /**
@@ -1014,27 +1167,123 @@ class BesService {
el.classList.add('bes-highlight-rect') el.classList.add('bes-highlight-rect')
el.classList.add( el.classList.add(
match.match.rule.id.startsWith('MORFOLOGIK_RULE') match.match.rule.id.startsWith('MORFOLOGIK_RULE')
? 'bes-highlight-spelling-rect' ? 'bes-highlight-spelling'
: 'bes-highlight-grammar-rect' : match.match.rule.id === 'BESANA_178' /*PR_VNAP_POPRAVEK_UI*/
? 'bes-highlight-ai'
: 'bes-highlight-grammar'
) )
el.style.left = `${rect.x + canvasPanelRect.x + window.scrollX}px` el.style.left = `${rect.x + canvasPanelRect.x + window.scrollX}px`
el.style.top = `${rect.y + canvasPanelRect.y + window.scrollY}px` el.style.top = `${rect.y + canvasPanelRect.y + window.scrollY}px`
el.style.width = `${rect.width}px` el.style.width = `${rect.width}px`
el.style.height = `${rect.height}px` el.style.height = `${rect.height}px`
document.body.appendChild(el) document.body.appendChild(el)
this.highlightElements.push(el) const matchSorted =
this.sortedMatches.find(entry => entry.match === match) || null
this.highlightElements.push({ el, matchSorted })
}) })
} }
/**
* This function finds the next/previous mistake.
* @param {Number} direction Navigation direction: 1 for next, -1 for previous
* @returns
*/
static findNextMistake(service, direction = 1) {
if (!service || !service.sortedMatches || !service.sortedMatches.length)
return
const active = service.highlightElements.find(
({ matchSorted }) => matchSorted
)
let current = -1
if (active && active.matchSorted) {
current = service.sortedMatches.findIndex(
entry => entry.match === active.matchSorted.match
)
}
const len = service.sortedMatches.length
const next = (current + direction + len) % len
service.activeMatchIndex = next
const { el, match } = service.sortedMatches[next]
if (el && typeof el.scrollIntoView === 'function') {
el.scrollIntoView({ behavior: 'instant', block: 'center' })
}
// Not the cleanest solution to setTimeout()
setTimeout(() => {
service.dismissPopup()
const popup = document.querySelector('bes-popup-el')
BesPopup.clearReplacements()
popup.setContent(el, match, service, service.isContentEditable())
service.highlightMistake(match)
const containerRect = service.hostElement.getBoundingClientRect()
match.highlights.forEach(rect => {
const clientX = rect.x + containerRect.left
const clientY =
rect.y +
containerRect.top +
rect.height -
service.hostElement.scrollTop
popup.show(clientX, clientY, service)
})
}, 150)
}
/**
* Accepts the replacement for the current grammar mistake.
*/
acceptReplacement() {
const popup = document.querySelector('bes-popup-el')
const replacementDiv = popup.shadowRoot.querySelector(
'.bes-replacement-div'
)
const firstReplacement = replacementDiv?.firstChild
if (replacementDiv.childElementCount === 1) {
// Is this an ugly solution?
firstReplacement.click()
} else if (replacementDiv.childElementCount > 1) {
firstReplacement.focus()
} else BesService.findNextMistake(this, 1)
}
/** /**
* Clears highlight and hides popup * Clears highlight and hides popup
*/ */
dismissPopup() { dismissPopup() {
BesPopup.hide() BesPopup.hide()
this.highlightElements.forEach(el => el.remove()) this.highlightElements.forEach(obj => {
if (obj.el && typeof obj.el.remove === 'function') {
obj.el.remove()
}
})
this.highlightElements = [] this.highlightElements = []
} }
/**
* This function collects all matches from the results array, flattens them into a single array,
* and sorts them in order: first by their Y axis, then by X axis.
*/
sortMatchesArray() {
this.sortedMatches = []
this.results.forEach(element => {
element.matches.forEach(match => {
if (!match.highlights || !match.highlights.length) return
this.sortedMatches.push({
el: element.element,
match,
top: match.highlights[0].top
})
})
})
this.sortedMatches.sort((a, b) => {
if (a.top !== b.top) return a.top - b.top
const aLeft = a.match.highlights[0].left
const bLeft = b.match.highlights[0].left
return aLeft - bLeft
})
}
/** /**
* Checks if host element content is editable. * Checks if host element content is editable.
* *
@@ -1055,7 +1304,9 @@ class BesService {
redrawAllMistakeMarkup() { redrawAllMistakeMarkup() {
this.ctx.clearRect(0, 0, this.canvasPanel.width, this.canvasPanel.height) this.ctx.clearRect(0, 0, this.canvasPanel.width, this.canvasPanel.height)
this.results.forEach(result => { this.results.forEach(result => {
result.matches.forEach(match => this.drawMistakeMarkup(match)) // Most important matches are first, we want to draw them last => iterate in reverse.
for (let i = result.matches.length; i-- > 0; )
this.drawMistakeMarkup(result.matches[i])
}) })
} }
@@ -1211,9 +1462,11 @@ class BesTreeService extends BesService {
), ),
match: match match: match
} }
this.drawMistakeMarkup(m)
matches.push(m) matches.push(m)
}) })
// Most important matches are first, we want to draw them last => iterate in reverse.
for (let i = matches.length; i-- > 0; )
this.drawMistakeMarkup(matches[i])
this.markProofed(node, matches) this.markProofed(node, matches)
this.onProofingProgress(matches.length) this.onProofingProgress(matches.length)
}) })
@@ -1221,8 +1474,22 @@ class BesTreeService extends BesService {
} }
return [{ text: `<${node.tagName}/>`, node: node, markup: true }] return [{ text: `<${node.tagName}/>`, node: node, markup: true }]
} else { } else {
// Inline elements require no markup. Keep plain text only.
let data = [] let data = []
if (this.doesElementAddSpace(node)) {
// Inline element adds some space between text. Convert node to spaces.
const inner = node.innerHTML
const len =
inner.length > 0
? node.outerHTML.indexOf(inner)
: node.outerHTML.length
data = data.concat({
text: ' '.repeat(len),
node: node,
markup: false
})
} else {
// Inline elements require no markup. Keep plain text only.
}
for (const el2 of node.childNodes) for (const el2 of node.childNodes)
data = data.concat(this.proofNode(el2, abortController)) data = data.concat(this.proofNode(el2, abortController))
return data return data
@@ -1334,6 +1601,26 @@ class BesTreeService extends BesService {
} }
} }
/**
* Tests if given element adds space before child/next text
*
* @param {Element} el DOM element
* @returns true if adds space; false otherwise.
*/
doesElementAddSpace(el) {
const prevNode = el.previousSibling
const nextNode = el.firstChild || el.nextSibling
if (!prevNode || !nextNode) return false
const range = document.createRange()
range.setStart(
prevNode,
prevNode.nodeType === Node.TEXT_NODE ? prevNode.length : 0
)
range.setEnd(nextNode, 0)
const bounds = range.getBoundingClientRect()
return bounds.width !== 0
}
/** /**
* Returns first block parent element of a node. * Returns first block parent element of a node.
* *
@@ -1435,19 +1722,19 @@ class BesTreeService extends BesService {
const canvasPanelRect = this.canvasPanel.getBoundingClientRect() const canvasPanelRect = this.canvasPanel.getBoundingClientRect()
let x = source.clientX - canvasPanelRect.x let x = source.clientX - canvasPanelRect.x
let y = source.clientY - canvasPanelRect.y let y = source.clientY - canvasPanelRect.y
const pointsInRect = [] const hits = []
for (let result of this.results) { for (let result of this.results) {
for (let m of result.matches) { for (let m of result.matches) {
for (let rect of m.highlights) { for (let rect of m.highlights) {
if (BesService.isPointInRect(x, y, rect)) { if (BesService.isPointInRect(x, y, rect, 5)) {
pointsInRect.push({ el, match: m }) hits.push({ el, match: m })
break break
} }
} }
} }
} }
this.dismissPopup() this.dismissPopup()
if (pointsInRect.length) this.preparePopup(pointsInRect, source) if (hits.length) this.preparePopup(hits, source)
} }
} }
@@ -1751,6 +2038,7 @@ class BesQuillService extends BesTreeService {
onChangeData(delta) { onChangeData(delta) {
let index = 0 let index = 0
let reproofNeeded = false let reproofNeeded = false
const affectedBlocks = new Set()
delta.ops.forEach(op => { delta.ops.forEach(op => {
if (op.retain) { if (op.retain) {
@@ -1760,11 +2048,12 @@ class BesQuillService extends BesTreeService {
} }
} else if (op.insert) { } else if (op.insert) {
reproofNeeded = true reproofNeeded = true
index += op.insert.length index += typeof op.insert === 'string' ? op.insert.length : 1 // Handle string or embed
} else if (op.delete) { } else if (op.delete) {
reproofNeeded = true reproofNeeded = true
} }
}) })
if (reproofNeeded) { if (reproofNeeded) {
const editorLength = this.quillInstance.getLength() const editorLength = this.quillInstance.getLength()
const clampedIndex = Math.min(index, editorLength - 1) const clampedIndex = Math.min(index, editorLength - 1)
@@ -1773,22 +2062,55 @@ class BesQuillService extends BesTreeService {
if (leaf) { if (leaf) {
let domElement = leaf.domNode let domElement = leaf.domNode
// Traverse up to find the block element
while (domElement && !this.isBlockElement(domElement)) { while (domElement && !this.isBlockElement(domElement)) {
domElement = domElement.parentNode domElement = domElement.parentNode
} }
if (domElement) {
this.clearProofing(domElement)
setTimeout(() => { if (domElement) affectedBlocks.add(domElement)
this.redrawAllMistakeMarkup()
this.scheduleProofing(1000)
}, 0)
}
} else { } else {
console.warn( console.warn(
'Leaf is null. The index might be out of bounds or the editor content is empty.' 'Leaf is null. The index might be out of bounds or the editor content is empty.'
) )
} }
// Handle pasted content spanning multiple blocks
const selection = this.quillInstance.getSelection()
if (selection) {
const [startLeaf] = this.quillInstance.getLeaf(selection.index)
const [endLeaf] = this.quillInstance.getLeaf(
selection.index + selection.length
)
if (startLeaf && endLeaf) {
let startElement = startLeaf.domNode
let endElement = endLeaf.domNode
while (startElement && !this.isBlockElement(startElement)) {
startElement = startElement.parentNode
}
while (endElement && !this.isBlockElement(endElement)) {
endElement = endElement.parentNode
}
if (startElement && endElement) {
let currentElement = startElement
while (currentElement) {
affectedBlocks.add(currentElement)
if (currentElement === endElement) break
currentElement = currentElement.nextElementSibling
}
}
}
}
// Clear proofing for all affected blocks
affectedBlocks.forEach(block => this.clearProofing(block))
// Schedule proofing for all affected blocks
setTimeout(() => {
this.scheduleProofing(1000)
}, 0)
} }
} }
@@ -1966,9 +2288,11 @@ class BesPlainTextService extends BesService {
), ),
match: match match: match
} }
this.drawMistakeMarkup(m)
matches.push(m) matches.push(m)
}) })
// Most important matches are first, we want to draw them last => iterate in reverse.
for (let i = matches.length; i-- > 0; )
this.drawMistakeMarkup(matches[i])
this.markProofed(paragraphRange, matches) this.markProofed(paragraphRange, matches)
this.onProofingProgress(matches.length) this.onProofingProgress(matches.length)
}) })
@@ -2103,19 +2427,19 @@ class BesPlainTextService extends BesService {
const canvasPanelRect = this.canvasPanel.getBoundingClientRect() const canvasPanelRect = this.canvasPanel.getBoundingClientRect()
let x = source.clientX - canvasPanelRect.x let x = source.clientX - canvasPanelRect.x
let y = source.clientY - canvasPanelRect.y let y = source.clientY - canvasPanelRect.y
const pointsInRect = [] const hits = []
for (let result of this.results) { for (let result of this.results) {
for (let m of result.matches) { for (let m of result.matches) {
for (let rect of m.highlights) { for (let rect of m.highlights) {
if (BesService.isPointInRect(x, y, rect)) { if (BesService.isPointInRect(x, y, rect, 5)) {
pointsInRect.push({ el: result.range, match: m }) hits.push({ el: result.range, match: m })
break break
} }
} }
} }
} }
this.dismissPopup() this.dismissPopup()
if (pointsInRect.length) this.preparePopup(pointsInRect, source) if (hits.length) this.preparePopup(hits, source)
} }
/** /**
@@ -2367,21 +2691,34 @@ class BesTAService extends BesPlainTextService {
const scrollLeft = window.scrollX || document.documentElement.scrollLeft const scrollLeft = window.scrollX || document.documentElement.scrollLeft
const styles = window.getComputedStyle(hostElement) const styles = window.getComputedStyle(hostElement)
textElement.style.zIndex = hostElement.style.zIndex - 1 textElement.style.display = styles.display
textElement.style.font = styles.font textElement.style.font = styles.font
textElement.style.lineHeight = styles.lineHeight textElement.style.lineHeight = styles.lineHeight
textElement.style.whiteSpace = styles.whiteSpace textElement.style.whiteSpace = styles.whiteSpace
textElement.style.whiteSpaceCollapse = styles.whiteSpaceCollapse textElement.style.whiteSpaceCollapse = styles.whiteSpaceCollapse
textElement.style.hyphens = styles.hyphens textElement.style.hyphens = styles.hyphens
textElement.style.boxSizing = styles.boxSizing
textElement.style.scrollBehavior = styles.scrollBehavior textElement.style.scrollBehavior = styles.scrollBehavior
textElement.style.overflow = styles.overflow
textElement.style.border = styles.border textElement.style.border = styles.border
textElement.style.borderRadius = styles.borderRadius textElement.style.borderRadius = styles.borderRadius
textElement.style.borderColor = 'transparent'
textElement.style.padding = styles.padding textElement.style.padding = styles.padding
textElement.style.left = `${rect.left + scrollLeft}px` textElement.style.left = `${rect.left + scrollLeft}px`
textElement.style.top = `${rect.top + scrollTop}px` textElement.style.top = `${rect.top + scrollTop}px`
textElement.style.width = styles.width textElement.style.width = `${
textElement.style.height = styles.height rect.width -
parseFloat(styles.borderLeftWidth) -
parseFloat(styles.paddingLeft) -
parseFloat(styles.paddingRight) -
parseFloat(styles.borderRightWidth)
}px`
textElement.style.height = `${
rect.height -
parseFloat(styles.borderTopWidth) -
parseFloat(styles.paddingTop) -
parseFloat(styles.paddingBottom) -
parseFloat(styles.borderBottomWidth)
}px`
} }
/** /**
@@ -2579,7 +2916,7 @@ class BesPopup extends HTMLElement {
padding: 3px 2px; padding: 3px 2px;
} }
.bes-toolbar button { .bes-toolbar button {
margin-right: 2px; margin-right: 0px;
} }
.bes-popup-title { .bes-popup-title {
color: #333; color: #333;
@@ -2604,6 +2941,12 @@ class BesPopup extends HTMLElement {
} }
.bes-replacement-btn:hover{ .bes-replacement-btn:hover{
background-color: #1976f0; background-color: #1976f0;
}
.bes-replacement-btn:focus{
outline: -webkit-focus-ring-color auto 1px;
}
.bes-replacement-btn:focus-visible{
outline: -webkit-focus-ring-color auto 1px;
} }
.bes-replacement-div{ .bes-replacement-div{
margin-top: 4px; margin-top: 4px;
@@ -2619,11 +2962,17 @@ class BesPopup extends HTMLElement {
.bes-close-btn svg { .bes-close-btn svg {
width: 100%; width: 100%;
height: 100%; height: 100%;
fill: #333; }
.bes-mistake-nav{
margin-right: 10px;
}
:host{
--bes-close-icon: #485362;
--hover-bg-clr: #dee3ed;
} }
.bes-close-btn:hover { .bes-close-btn:hover {
background: #dee3ed; background: var(--hover-bg-clr);
border-radius: 8px border-radius: 4px
} }
:host(.show) .bes-popup-container { :host(.show) .bes-popup-container {
visibility: visible; visibility: visible;
@@ -2652,17 +3001,45 @@ class BesPopup extends HTMLElement {
background-color: #111213; background-color: #111213;
border: 1px solid #2e3036; border: 1px solid #2e3036;
} }
:host{
--bes-close-icon: #a4b5c7;
--hover-bg-clr:rgba(189, 189, 189, 0.28);
}
} }
</style> </style>
<div class="bes-popup-container"> <div class="bes-popup-container">
<div class="bes-toolbar"> <div class="bes-toolbar">
<div class="bes-popup-title">Besana</div> <div class="bes-popup-title">Besana</div>
<div class="bes-mistake-nav">
<button class="bes-close-btn" title="Prejšnja napaka">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="var(--bes-close-icon)" d="M11 20V7.825l-5.6 5.6L4 12l8-8l8 8l-1.4 1.425l-5.6-5.6V20z"/></svg>
</button>
<button class="bes-close-btn" title="Naslednja napaka">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><!-- Icon from Material Symbols by Google - https://github.com/google/material-design-icons/blob/master/LICENSE --><path fill="var(--bes-close-icon)" d="M11 4v12.175l-5.6-5.6L4 12l8 8l8-8l-1.4-1.425l-5.6 5.6V4z"/></svg>
</button>
</div>
<button class="bes-close-btn" onclick="BesPopup.dismiss()"> <button class="bes-close-btn" onclick="BesPopup.dismiss()">
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="currentColor" d="M13.46 12L19 17.54V19h-1.46L12 13.46L6.46 19H5v-1.46L10.54 12L5 6.46V5h1.46L12 10.54L17.54 5H19v1.46z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><path fill="var(--bes-close-icon)" d="M13.46 12L19 17.54V19h-1.46L12 13.46L6.46 19H5v-1.46L10.54 12L5 6.46V5h1.46L12 10.54L17.54 5H19v1.46z"/></svg>
</button> </button>
</div> </div>
</div> </div>
` `
const prevBtn = this.shadowRoot.querySelector(
'.bes-mistake-nav .bes-close-btn[title="Prejšnja napaka"]'
)
const nextBtn = this.shadowRoot.querySelector(
'.bes-mistake-nav .bes-close-btn[title="Naslednja napaka"]'
)
prevBtn.addEventListener('click', () => {
if (this.hostElService)
BesService.arrowBtnNavigation('back', this.hostElService)
})
nextBtn.addEventListener('click', () => {
if (this.hostElService)
BesService.arrowBtnNavigation('forward', this.hostElService)
})
this.addEventListener('mousedown', this.onMouseDown) this.addEventListener('mousedown', this.onMouseDown)
} }
@@ -2672,8 +3049,9 @@ class BesPopup extends HTMLElement {
* @param {Number} x X location hint * @param {Number} x X location hint
* @param {Number} y Y location hint * @param {Number} y Y location hint
*/ */
show(x, y) { show(x, y, service) {
this.style.position = 'fixed' this.style.position = 'fixed'
this.hostElService = service
// Element needs some initial placement for the browser to provide this.offsetWidth and this. // Element needs some initial placement for the browser to provide this.offsetWidth and this.
// offsetHeight measurements. // offsetHeight measurements.

View File

@@ -1,25 +1,39 @@
/* TODO: Dark mode theme */
/* Mistake types styles */ /* Mistake types styles */
.bes-spelling-mistake {
border-bottom: 2px solid #ff7300;
position: absolute;
z-index: 3;
cursor: text;
}
.bes-highlight-rect { .bes-highlight-rect {
position: absolute; position: absolute;
opacity: 0.3;
cursor: text; cursor: text;
} }
.bes-highlight-spelling-rect { .bes-highlight-spelling {
background: rgb(0, 123, 255); color: rgb(242, 90, 90);
background: hsla(0, 100%, 67%, 0.18);
} }
.bes-highlight-grammar-rect { .bes-highlight-ai {
background: rgb(255, 115, 0); color: rgb(139, 62, 223);
background: hsla(262, 70%, 56%, 0.18);
}
.bes-highlight-grammar {
color: rgb(60, 120, 220);
background: hsla(220, 80%, 56%, 0.18);
}
@media (prefers-color-scheme: dark) {
.bes-highlight-spelling {
color: rgb(255, 120, 120);
background: hsla(0, 100%, 67%, 0.32);
}
.bes-highlight-ai {
color: rgb(180, 120, 255);
background: hsla(262, 70%, 56%, 0.32);
}
.bes-highlight-grammar {
color: rgb(100, 164, 243);
background: hsla(220, 80%, 56%, 0.32);
}
} }
/* Styles required to ensure full functionality and optimal user experience. */ /* Styles required to ensure full functionality and optimal user experience. */
@@ -50,9 +64,14 @@
.bes-text-panel { .bes-text-panel {
position: absolute; position: absolute;
overflow: hidden;
margin: 0px; margin: 0px;
color: transparent; color: transparent;
border-color: transparent; border-color: transparent;
background: none; background: none;
z-index: -1;
}
.bes-highlight-placeholder {
display: none;
visibility: hidden;
} }