service2.js: Port popup from service.js
This commit is contained in:
328
service2.js
328
service2.js
@@ -1,4 +1,3 @@
|
||||
// TODO: Port popup dialog from service.js
|
||||
// TODO: Test with contenteditable="plaintext-only"
|
||||
// TODO: Implement <textarea> class
|
||||
// TODO: Port CKEditor class from service.js
|
||||
@@ -32,8 +31,8 @@ class BesService {
|
||||
this.originalSpellcheck = this.hostElement.spellcheck
|
||||
this.hostElement.spellcheck = false
|
||||
|
||||
this.handleScroll = () => this.onScroll()
|
||||
this.hostElement.addEventListener('scroll', this.handleScroll)
|
||||
this.onScroll = this.onScroll.bind(this)
|
||||
this.hostElement.addEventListener('scroll', this.onScroll)
|
||||
|
||||
besServices.push(this)
|
||||
}
|
||||
@@ -43,7 +42,7 @@ class BesService {
|
||||
*/
|
||||
unregister() {
|
||||
besServices = besServices.filter(item => item !== this)
|
||||
this.hostElement.removeEventListener('scroll', this.handleScroll)
|
||||
this.hostElement.removeEventListener('scroll', this.onScroll)
|
||||
this.hostElement.spellcheck = this.originalSpellcheck
|
||||
this.clearCorrectionPanel()
|
||||
}
|
||||
@@ -237,10 +236,12 @@ class BesDOMService extends BesService {
|
||||
constructor(hostElement) {
|
||||
super(hostElement)
|
||||
this.results = [] // Results of grammar-checking, one per each block of text
|
||||
this.handleBeforeInput = event => this.onBeforeInput(event)
|
||||
this.hostElement.addEventListener('beforeinput', this.handleBeforeInput)
|
||||
this.handleInput = () => this.onInput()
|
||||
this.hostElement.addEventListener('input', this.handleInput)
|
||||
this.onBeforeInput = this.onBeforeInput.bind(this)
|
||||
this.hostElement.addEventListener('beforeinput', this.onBeforeInput)
|
||||
this.onInput = this.onInput.bind(this)
|
||||
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.
|
||||
*/
|
||||
unregister() {
|
||||
this.hostElement.removeEventListener('input', this.handleInput)
|
||||
this.hostElement.removeEventListener('beforeinput', this.handleBeforeInput)
|
||||
this.hostElement.removeEventListener('click', this.onClick)
|
||||
this.hostElement.removeEventListener('input', this.onInput)
|
||||
this.hostElement.removeEventListener('beforeinput', this.onBeforeInput)
|
||||
if (this.timer) clearTimeout(this.timer)
|
||||
if (this.abortController) this.abortController.abort()
|
||||
super.unregister()
|
||||
@@ -727,8 +729,314 @@ class BesDOMService extends BesService {
|
||||
|
||||
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.
|
||||
window.addEventListener('load', () => {
|
||||
document.querySelectorAll('.bes-service').forEach(hostElement => {
|
||||
|
Reference in New Issue
Block a user