service2.js: Port popup from service.js

This commit is contained in:
Simon Rozman 2024-05-13 13:03:58 +02:00
parent c54366e95f
commit ff54607e7e
3 changed files with 320 additions and 10 deletions

View File

@ -17,5 +17,6 @@
<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. 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>
</div> </div>
<bes-popup-el/>
</body> </body>
</html> </html>

View File

@ -17,5 +17,6 @@
<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. 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>
</div> </div>
<bes-popup-el/>
</body> </body>
</html> </html>

View File

@ -1,4 +1,3 @@
// TODO: Port popup dialog from service.js
// TODO: Test with contenteditable="plaintext-only" // TODO: Test with contenteditable="plaintext-only"
// TODO: Implement <textarea> class // TODO: Implement <textarea> class
// TODO: Port CKEditor class from service.js // TODO: Port CKEditor class from service.js
@ -32,8 +31,8 @@ class BesService {
this.originalSpellcheck = this.hostElement.spellcheck this.originalSpellcheck = this.hostElement.spellcheck
this.hostElement.spellcheck = false this.hostElement.spellcheck = false
this.handleScroll = () => this.onScroll() this.onScroll = this.onScroll.bind(this)
this.hostElement.addEventListener('scroll', this.handleScroll) this.hostElement.addEventListener('scroll', this.onScroll)
besServices.push(this) besServices.push(this)
} }
@ -43,7 +42,7 @@ class BesService {
*/ */
unregister() { unregister() {
besServices = besServices.filter(item => item !== this) besServices = besServices.filter(item => item !== this)
this.hostElement.removeEventListener('scroll', this.handleScroll) this.hostElement.removeEventListener('scroll', this.onScroll)
this.hostElement.spellcheck = this.originalSpellcheck this.hostElement.spellcheck = this.originalSpellcheck
this.clearCorrectionPanel() this.clearCorrectionPanel()
} }
@ -237,10 +236,12 @@ class BesDOMService extends BesService {
constructor(hostElement) { constructor(hostElement) {
super(hostElement) super(hostElement)
this.results = [] // Results of grammar-checking, one per each block of text this.results = [] // Results of grammar-checking, one per each block of text
this.handleBeforeInput = event => this.onBeforeInput(event) this.onBeforeInput = this.onBeforeInput.bind(this)
this.hostElement.addEventListener('beforeinput', this.handleBeforeInput) this.hostElement.addEventListener('beforeinput', this.onBeforeInput)
this.handleInput = () => this.onInput() this.onInput = this.onInput.bind(this)
this.hostElement.addEventListener('input', this.handleInput) this.hostElement.addEventListener('input', this.onInput)
this.onClick = this.onClick.bind(this)
this.hostElement.addEventListener('click', this.onClick)
} }
/** /**
@ -259,8 +260,9 @@ class BesDOMService extends BesService {
* Unregisters grammar checking service. * Unregisters grammar checking service.
*/ */
unregister() { unregister() {
this.hostElement.removeEventListener('input', this.handleInput) this.hostElement.removeEventListener('click', this.onClick)
this.hostElement.removeEventListener('beforeinput', this.handleBeforeInput) this.hostElement.removeEventListener('input', this.onInput)
this.hostElement.removeEventListener('beforeinput', this.onBeforeInput)
if (this.timer) clearTimeout(this.timer) if (this.timer) clearTimeout(this.timer)
if (this.abortController) this.abortController.abort() if (this.abortController) this.abortController.abort()
super.unregister() super.unregister()
@ -727,7 +729,313 @@ class BesDOMService extends BesService {
return nodes return nodes
} }
/**
* Called to report mouse click
*
* Displays or hides grammar mistake popup.
*
* @param {PointerEvent} event The event produced by a pointer such as the geometry of the contact point, the device type that generated the event, the amount of pressure that was applied on the contact surface, etc.
*/
onClick(event) {
const source = event?.detail !== 1 ? event?.detail : event
const target = this.getBlockParent(source.targetElement || source.target)
if (!target) return
const matches = this.results?.find(
child => child.element === target
)?.matches
if (matches) {
const popup = document.querySelector('bes-popup-el')
for (let m of matches) {
if (m.rects) {
for (let r of m.rects) {
if (
BesDOMService.isPointInRect(source.clientX, source.clientY, r)
) {
popup.changeMessage(m.match.message)
popup.appendReplacements(
target,
m,
this,
this.hostElement.contentEditable !== 'false'
)
popup.show(source.clientX, source.clientY)
return
} }
}
}
}
}
BesPopupEl.hide()
}
/**
* Tests if given coordinate is inside of a rectangle.
*
* @param {Number} x X coordinate
* @param {Number} y Y coordinate
* @param {DOMRect} rect Rectangle
* @returns
*/
static isPointInRect(x, y, rect) {
return rect.left <= x && x < rect.right && rect.top <= y && y < rect.bottom
}
/**
* Replaces grammar checking match with a suggestion provided by grammar checking service.
*
* @param {Element} el Block element containing grammar checking rule match
* @param {*} match Grammar checking rule match
* @param {String} replacement Text to replace grammar checking match with
*/
replaceText(el, match, replacement) {
if (this.timer) clearTimeout(this.timer)
if (this.abortController) this.abortController.abort()
this.clearProofing(el)
match.range.deleteContents()
match.range.insertNode(document.createTextNode(replacement))
this.onStartProofing()
this.proofNode(el, this.abortController)
if (this.proofingCount == 0) {
// No text blocks were discovered for proofing. onProofingProgress() will not be called
// and we need to notify manually.
this.onEndProofing()
}
}
}
/*************************************************************************
*
* Grammar mistake popup dialog
*
*/
class BesPopupEl extends HTMLElement {
constructor() {
super()
this.attachShadow({ mode: 'open' })
}
render() {
this.shadowRoot.innerHTML = `
<style>
:host {
display: none;
}
:host(.show){
z-index: 10;
display: block;
}
.popup-text {
max-width: 160px;
color: black;
text-align: center;
padding: 8px 0;
z-index: 1;
}
.bes-popup-container {
visibility: hidden;
min-width: 200px;
max-width: 300px;
background-color: rgb(241, 243, 249);
box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
border-radius: 5px;
padding: 8px;
z-index: 1;
}
.bes-toolbar {
display: flex;
justify-content: end;
padding: 5px 2px;
}
.bes-toolbar button {
margin-right: 2px;
}
.bes-popup-title {
flex-grow: 1;
cursor: grab;
}
.bes-text-div{
background-color: white;
padding: 10px;
border-radius: 5px;
}
.bes-replacement-btn{
margin: 4px 1px;
padding: 4px;
border: none;
border-radius: 5px;
background-color: #239aff;
color: white;
cursor: pointer;
}
.bes-replacement-btn:hover{
background-color: #1976f0;
}
:host(.show) .bes-popup-container {
visibility: visible;
animation: fadeIn 1s;
}
@keyframes fadeIn {
from {opacity: 0;}
to {opacity:1 ;}
}
</style>
<div class="bes-popup-container">
<div class="bes-toolbar">
<div class="bes-popup-title">Besana</div>
<button class="bes-close-btn" onclick="BesPopupEl.hide()">X</button>
</div>
<div class="bes-text-div">
<span class="popup-text">
</span>
<div class="bes-replacement-div">
</div>
</div>
</div>
`
}
/**
* Shows popup window.
*
* @param {*} x X location hint
* @param {*} y Y location hint
*/
show(x, y) {
this.style.position = 'fixed'
// Element needs some initial placement for the browser to provide this.offsetWidth and this.offsetHeight measurements.
// The fade-in effect on the popup window should prevent flicker.
this.style.left = `0px`
this.style.top = `0px`
this.classList.add('show')
if (x + this.offsetWidth <= window.innerWidth) {
this.style.left = `${x}px`
} else if (this.offsetWidth <= x) {
this.style.left = `${x - this.offsetWidth}px`
} else {
this.style.left = `${x - this.offsetWidth / 2}px`
}
if (y + 20 + this.offsetHeight <= window.innerHeight) {
this.style.top = `${y + 20}px`
} else if (this.offsetHeight <= y) {
this.style.top = `${y - this.offsetHeight}px`
} else {
this.style.top = `${y - this.offsetHeight / 2}px`
}
}
/**
* Clears all grammar mistake suggestions.
*/
clearReplacements() {
Array.from(
this.shadowRoot.querySelector('.bes-replacement-div')?.children
).forEach(replacement => replacement.remove())
}
/**
* Adds a grammar mistake suggestion.
*
* @param {Element} el Block element containing the grammar mistake
* @param {*} match Grammar checking rule match
* @param {BesService} service Grammar checking service
* @param {Boolean} allowReplacements Host element is mutable and grammar mistake may be replaced by suggestion
*/
appendReplacements(el, match, service, allowReplacements) {
const replacementDiv = this.shadowRoot.querySelector('.bes-replacement-div')
match.match.replacements.forEach(replacement => {
const replacementBtn = document.createElement('button')
replacementBtn.classList.add('bes-replacement-btn')
replacementBtn.textContent = replacement.value
replacementBtn.addEventListener('click', () => {
if (allowReplacements) {
service.replaceText(el, match, replacement.value)
BesPopupEl.hide()
}
})
replacementDiv.appendChild(replacementBtn)
})
}
/**
* Sets grammar mistake description
*
* @param {String} text
*/
changeMessage(text) {
this.clearReplacements()
this.shadowRoot.querySelector('.popup-text').textContent = text
}
/**
* Handles the mousedown event.
*
* @param {MouseEvent} e Event
*/
onMouseDown(e) {
e.preventDefault()
this.initialMouseX = e.clientX
this.initialMouseY = e.clientY
this.handleMouseMove = this.onMouseMove.bind(this)
document.addEventListener('mousemove', this.handleMouseMove)
this.handleMouseUp = this.onMouseUp.bind(this)
document.addEventListener('mouseup', this.handleMouseUp)
}
/**
* Handles the mousemove event.
*
* @param {MouseEvent} e Event
*/
onMouseMove(e) {
e.preventDefault()
let diffX = this.initialMouseX - e.clientX
this.initialMouseX = e.clientX
let left = this.offsetLeft - diffX
left = Math.max(0, Math.min(left, window.innerWidth - this.offsetWidth))
this.style.left = `${left}px`
let diffY = this.initialMouseY - e.clientY
this.initialMouseY = e.clientY
let top = this.offsetTop - diffY
top = Math.max(0, Math.min(top, window.innerHeight - this.offsetHeight))
this.style.top = `${top}px`
}
/**
* Handles the mouseup event.
*
* @param {MouseEvent} e Event
*/
onMouseUp(e) {
document.removeEventListener('mouseup', this.handleMouseUp)
document.removeEventListener('mousemove', this.handleMouseMove)
}
/**
* Called each time the element is added to the document
*/
connectedCallback() {
this.render()
this.addEventListener('mousedown', this.onMouseDown)
}
/**
* Dismisses all the popups.
*/
static hide() {
document
.querySelectorAll('bes-popup-el')
.forEach(popup => popup.classList.remove('show'))
}
}
customElements.define('bes-popup-el', BesPopupEl)
// Auto-register all elements with bes-service class. // Auto-register all elements with bes-service class.
window.addEventListener('load', () => { window.addEventListener('load', () => {