Fork service2.js
Due to intensive development, service.js grew complex and convoluted. With lessons learned we shall prepare a cleaner and leaner version of the code.
This commit is contained in:
699
service2.js
Normal file
699
service2.js
Normal file
@@ -0,0 +1,699 @@
|
||||
// TODO: Port popup dialog from service.js
|
||||
// TODO: Test with <div contenteditable="plaintext-only">
|
||||
// TODO: Implement <textarea> class
|
||||
// TODO: Port CKEditor class from service.js
|
||||
|
||||
/**
|
||||
* Collection of all grammar checking services in the document
|
||||
*
|
||||
* We dispatch all window messages to all services registered here.
|
||||
*/
|
||||
let besServices = []
|
||||
|
||||
window.addEventListener('resize', () =>
|
||||
besServices.forEach(service => service.onResize())
|
||||
)
|
||||
|
||||
window.addEventListener('scroll', () =>
|
||||
besServices.forEach(service => service.onScroll())
|
||||
)
|
||||
|
||||
/*************************************************************************
|
||||
*
|
||||
* Base class for all grammar-checking services
|
||||
*
|
||||
*************************************************************************/
|
||||
class BesService {
|
||||
constructor(hostElement) {
|
||||
this.hostElement = hostElement
|
||||
this.abortController = new AbortController()
|
||||
this.createCorrectionPanel()
|
||||
|
||||
// Disable browser built-in spell-checker to prevent collision with our grammar markup.
|
||||
this.originalSpellcheck = this.hostElement.spellcheck
|
||||
this.hostElement.spellcheck = false
|
||||
|
||||
this.handleScroll = () => this.onScroll()
|
||||
this.hostElement.addEventListener('scroll', this.handleScroll)
|
||||
|
||||
besServices.push(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters grammar checking service.
|
||||
*/
|
||||
unregister() {
|
||||
besServices = besServices.filter(item => item !== this)
|
||||
this.hostElement.removeEventListener('scroll', this.handleScroll)
|
||||
this.hostElement.spellcheck = this.originalSpellcheck
|
||||
this.clearCorrectionPanel()
|
||||
this.abortController.abort()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called initially when grammar-checking run is started
|
||||
*/
|
||||
onStartProofing() {
|
||||
this.proofingCount = 0 // Ref-count how many grammar-checking blocks of text are active
|
||||
this.proofingMatches = 0 // Number of grammar mistakes detected in entire grammar-checking run
|
||||
this.updateStatusIcon('bes-status-loading', 'Besana preverja pravopis.')
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when grammar-checking starts proofing each block of text (typically paragraph)
|
||||
*/
|
||||
onProofing() {
|
||||
this.proofingCount++
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when grammar-checking of a block of text failed (as 500 Internal server error, timeout, etc.)
|
||||
*
|
||||
* @param {Response} response HTTP response
|
||||
*/
|
||||
onFailedProofing(response) {
|
||||
this.updateStatusIcon(
|
||||
'bes-status-error',
|
||||
`Pri preverjanju pravopisa je prišlo do napake ${response.status} ${response.statusText}.`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when failed to parse result of a grammar-checking of a block of text
|
||||
*
|
||||
* @param {Error} error Error
|
||||
*/
|
||||
onFailedProofingResult(error) {
|
||||
this.proofingCount--
|
||||
this.updateStatusIcon(
|
||||
'bes-status-error',
|
||||
`Pri obdelavi odgovora pravopisnega strežnika je prišlo do napake: ${error}`
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when one block of text finished grammar-checking
|
||||
*
|
||||
* @param {Number} numberOfMatches Number of grammar mistakes discovered
|
||||
*/
|
||||
onProofingProgress(numberOfMatches) {
|
||||
this.proofingMatches += numberOfMatches
|
||||
if (--this.proofingCount <= 0) {
|
||||
// This was the last block of text in the run we were waiting for.
|
||||
// TODO: If onFailedProofingResult was called on a non-last block of text, the below will override the status icon error state.
|
||||
if (this.proofingMatches > 0)
|
||||
this.updateStatusIcon(
|
||||
'bes-status-mistakes',
|
||||
'Število napak: ' + this.proofingMatches
|
||||
)
|
||||
else this.updateStatusIcon('bes-status-success', 'V besedilu ni napak.')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to report scrolling
|
||||
*/
|
||||
onScroll() {
|
||||
// Scroll panel is "position: absolute", we need to keep it aligned with the host element.
|
||||
this.scrollPanel.style.top = `${-this.hostElement.scrollTop}px`
|
||||
this.scrollPanel.style.left = `${-this.hostElement.scrollLeft}px`
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to report resizing
|
||||
*/
|
||||
onResize() {
|
||||
this.setCorrectionPanelSize()
|
||||
this.setStatusDivPosition()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates auxiliary DOM elements for text adornments.
|
||||
*/
|
||||
createCorrectionPanel() {
|
||||
const panelParent = document.createElement('div')
|
||||
panelParent.classList.add('bes-correction-panel-parent')
|
||||
this.correctionPanel = document.createElement('div')
|
||||
this.scrollPanel = document.createElement('div')
|
||||
this.setCorrectionPanelSize()
|
||||
this.correctionPanel.classList.add('bes-correction-panel')
|
||||
this.scrollPanel.classList.add('bes-correction-panel-scroll')
|
||||
|
||||
this.correctionPanel.appendChild(this.scrollPanel)
|
||||
panelParent.appendChild(this.correctionPanel)
|
||||
this.hostElement.parentElement.insertBefore(panelParent, this.hostElement)
|
||||
|
||||
this.statusDiv = document.createElement('div')
|
||||
this.statusDiv.classList.add('bes-status-div')
|
||||
this.statusIcon = document.createElement('div')
|
||||
this.statusIcon.classList.add('bes-status-icon')
|
||||
this.statusDiv.appendChild(this.statusIcon)
|
||||
this.setStatusDivPosition()
|
||||
this.hostElement.parentNode.insertBefore(
|
||||
this.statusDiv,
|
||||
this.hostElement.nextSibling
|
||||
)
|
||||
const statusPopup = document.createElement('bes-popup-status-el')
|
||||
document.body.appendChild(statusPopup)
|
||||
this.statusDiv.addEventListener('click', e =>
|
||||
this.handleStatusClick(e, statusPopup)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears auxiliary DOM elements for text adornments.
|
||||
*/
|
||||
clearCorrectionPanel() {
|
||||
this.correctionPanel.remove()
|
||||
this.scrollPanel.remove()
|
||||
this.statusDiv.remove()
|
||||
this.statusIcon.remove()
|
||||
}
|
||||
|
||||
/**
|
||||
* Resizes correction and scroll panels to match host element size.
|
||||
*/
|
||||
setCorrectionPanelSize() {
|
||||
const styles = window.getComputedStyle(this.hostElement)
|
||||
const totalWidth = parseFloat(styles.width)
|
||||
const totalHeight =
|
||||
parseFloat(styles.height) +
|
||||
parseFloat(styles.marginTop) +
|
||||
parseFloat(styles.marginBottom) +
|
||||
parseFloat(styles.paddingTop) +
|
||||
parseFloat(styles.paddingBottom)
|
||||
this.correctionPanel.style.width = `${totalWidth}px`
|
||||
this.correctionPanel.style.height = `${totalHeight}px`
|
||||
this.correctionPanel.style.marginLeft = styles.marginLeft
|
||||
this.correctionPanel.style.marginRight = styles.marginRight
|
||||
this.correctionPanel.style.paddingLeft = styles.paddingLeft
|
||||
this.correctionPanel.style.paddingRight = styles.paddingRight
|
||||
this.scrollPanel.style.height = `${this.hostElement.scrollHeight}px`
|
||||
}
|
||||
|
||||
/**
|
||||
* Repositions status DIV element.
|
||||
*/
|
||||
setStatusDivPosition() {
|
||||
const rect = this.hostElement.getBoundingClientRect()
|
||||
const scrollTop = window.scrollY || document.documentElement.scrollTop
|
||||
this.statusDiv.style.left = `${rect.right - 40}px`
|
||||
this.statusDiv.style.top = `${rect.top + rect.height - 30 + scrollTop}px`
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets status icon style and title.
|
||||
*
|
||||
* @param {String} status CSS class name to set status icon to
|
||||
* @param {String} title Title of the status icon
|
||||
*/
|
||||
updateStatusIcon(status, title) {
|
||||
const statuses = [
|
||||
'bes-status-loading',
|
||||
'bes-status-success',
|
||||
'bes-status-mistakes',
|
||||
'bes-status-error'
|
||||
]
|
||||
statuses.forEach(statusClass => {
|
||||
this.statusIcon.classList.remove(statusClass)
|
||||
})
|
||||
this.statusIcon.classList.add(status)
|
||||
this.statusDiv.title = title
|
||||
}
|
||||
}
|
||||
|
||||
/*************************************************************************
|
||||
*
|
||||
* DOM grammar-checking service
|
||||
*
|
||||
*************************************************************************/
|
||||
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)
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers grammar checking service.
|
||||
*
|
||||
* @param {Element} hostElement DOM element to register grammar checking service for
|
||||
* @returns {BesService} Grammar checking service instance
|
||||
*/
|
||||
static register(hostElement) {
|
||||
let service = new BesDOMService(hostElement)
|
||||
service.proofAll()
|
||||
return service
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters grammar checking service.
|
||||
*/
|
||||
unregister() {
|
||||
this.hostElement.removeEventListener('input', this.handleInput)
|
||||
this.hostElement.removeEventListener('beforeinput', this.handleBeforeInput)
|
||||
super.unregister()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to report scrolling
|
||||
*/
|
||||
onScroll() {
|
||||
super.onScroll()
|
||||
|
||||
// Markup is in a "position:absolute" <div> element requiring repositioning when scrolling host element or window.
|
||||
// It is defered to reduce stress in a flood of scroll events.
|
||||
// TODO: We could technically just update scrollTop and scrollLeft of all markup rects for even better performance?
|
||||
if (this.scrollTimeout) clearTimeout(this.scrollTimeout)
|
||||
this.scrollTimeout = setTimeout(() => {
|
||||
this.repositionAllMarkup()
|
||||
delete this.scrollTimeout
|
||||
}, 500)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to report resizing
|
||||
*/
|
||||
onResize() {
|
||||
super.onResize()
|
||||
|
||||
// When window is resized, host element might resize too.
|
||||
// This may cause text to re-wrap requiring markup re
|
||||
this.repositionAllMarkup()
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to report the text is about to change
|
||||
*
|
||||
* Marks section of the text that is about to change as not-yet-grammar-checked.
|
||||
*
|
||||
* @param {InputEvent} event The event notifying the user of editable content changes
|
||||
*/
|
||||
onBeforeInput(event) {
|
||||
// Abort running grammar checking ASAP.
|
||||
if (this.timer) clearTimeout(this.timer)
|
||||
this.abortController.abort()
|
||||
|
||||
// Remove markup of all blocks of text that are about to change.
|
||||
let blockElements = new Set()
|
||||
event.getTargetRanges().forEach(range => {
|
||||
BesDOMService.getNodesInRange(range).forEach(el => {
|
||||
if (
|
||||
el === this.hostElement ||
|
||||
Array.from(this.hostElement.childNodes).includes(el)
|
||||
)
|
||||
blockElements.add(this.getBlockParent(el))
|
||||
})
|
||||
})
|
||||
blockElements.forEach(block => this.clearProofing(block))
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to report the text has changed
|
||||
*/
|
||||
onInput() {
|
||||
// Now that the text is done changing, we can correctly calculate markup position.
|
||||
this.repositionAllMarkup()
|
||||
|
||||
// Defer grammar-checking to reduce stress on grammar-checking server.
|
||||
this.timer = setTimeout(() => {
|
||||
this.abortController = new AbortController()
|
||||
this.proofAll()
|
||||
delete this.timer
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively grammar-(re)checks our host DOM tree.
|
||||
*/
|
||||
proofAll() {
|
||||
this.onStartProofing()
|
||||
this.proofNode(this.hostElement)
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively grammar-checks a DOM node.
|
||||
*
|
||||
* @param {Node} node DOM root node to check
|
||||
* @returns {Array} Markup of text to check using BesStr
|
||||
*/
|
||||
proofNode(node) {
|
||||
switch (node.nodeType) {
|
||||
case Node.TEXT_NODE:
|
||||
return [{ text: node.textContent, node: node, markup: false }]
|
||||
|
||||
case Node.ELEMENT_NODE:
|
||||
if (this.isBlockElement(node)) {
|
||||
// Block elements are grammar-checked independently.
|
||||
if (this.isProofed(node))
|
||||
return [{ text: `<${node.tagName}/>`, node: node, markup: true }]
|
||||
|
||||
let data = []
|
||||
for (const el2 of node.childNodes)
|
||||
data = data.concat(this.proofNode(el2))
|
||||
if (data.some(x => !x.markup && !/^\s*$/.test(x.text))) {
|
||||
// Block element contains some text.
|
||||
this.onProofing()
|
||||
// Save the abort signal reference. It will change on grammar-check re-start.
|
||||
const signal = this.abortController.signal
|
||||
fetch(
|
||||
new Request(besUrl + '/check', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
format: 'plain',
|
||||
data: JSON.stringify({
|
||||
annotation: data.map(x =>
|
||||
x.markup ? { markup: x.text } : { text: x.text }
|
||||
)
|
||||
}),
|
||||
language: node.lang ? node.lang : 'sl',
|
||||
level: 'picky'
|
||||
})
|
||||
}),
|
||||
{ signal }
|
||||
)
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
this.onFailedProofing(response)
|
||||
throw new Error('Unexpected BesStr server response')
|
||||
}
|
||||
return response.json()
|
||||
})
|
||||
.then(responseData => {
|
||||
let matches = []
|
||||
responseData.matches.forEach(match => {
|
||||
let range = document.createRange()
|
||||
|
||||
// Locate start of the grammar mistake.
|
||||
for (
|
||||
let idx = 0, startingOffset = 0;
|
||||
;
|
||||
startingOffset += data[idx++].text.length
|
||||
) {
|
||||
if (
|
||||
!data[idx].markup &&
|
||||
/*startingOffset <= match.offset &&*/ match.offset <
|
||||
startingOffset + data[idx].text.length
|
||||
) {
|
||||
range.setStart(
|
||||
data[idx].node,
|
||||
match.offset - startingOffset
|
||||
)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Locate end of the grammar mistake.
|
||||
let endOffset = match.offset + match.length
|
||||
for (
|
||||
let idx = 0, startingOffset = 0;
|
||||
;
|
||||
startingOffset += data[idx++].text.length
|
||||
) {
|
||||
if (
|
||||
!data[idx].markup &&
|
||||
/*startingOffset <= endOffset ← not needed, kept for reference &&*/ endOffset <=
|
||||
startingOffset + data[idx].text.length
|
||||
) {
|
||||
range.setEnd(data[idx].node, endOffset - startingOffset)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
const { clientRects, highlights } =
|
||||
this.addMistakeMarkup(range)
|
||||
matches.push({
|
||||
rects: clientRects,
|
||||
highlights: highlights,
|
||||
range: range,
|
||||
match: match
|
||||
})
|
||||
})
|
||||
this.markProofed(node, matches)
|
||||
this.onProofingProgress(matches.length)
|
||||
})
|
||||
.catch(error => {
|
||||
if (error.name === 'AbortError') return
|
||||
this.onFailedProofingResult(error)
|
||||
})
|
||||
}
|
||||
return [{ text: `<${node.tagName}/>`, node: node, markup: true }]
|
||||
} else {
|
||||
// Inline elements require no markup. Keep plain text only.
|
||||
let data = []
|
||||
for (const el2 of node.childNodes)
|
||||
data = data.concat(this.proofNode(el2))
|
||||
return data
|
||||
}
|
||||
|
||||
default:
|
||||
return [{ text: `<?${node.nodeType}>`, node: node, markup: true }]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if given block element has already been grammar-checked.
|
||||
*
|
||||
* @param {Element} el DOM element to check
|
||||
* @returns {Boolean} true if the element has already been grammar-checked; false otherwise.
|
||||
*/
|
||||
isProofed(el) {
|
||||
return this.results?.find(result => result.element === el) != null
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks given block element as grammar-checked.
|
||||
*
|
||||
* @param {Element} el DOM element that was checked
|
||||
* @param {Array} matches Grammar mistakes
|
||||
*/
|
||||
markProofed(el, matches) {
|
||||
this.clearProofing(el)
|
||||
this.results.push({
|
||||
element: el,
|
||||
matches: matches
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes given block element from this.results array and clearing its markup.
|
||||
*
|
||||
* @param {Element} el DOM element for removal
|
||||
*/
|
||||
clearProofing(el) {
|
||||
this.clearMarkup(el)
|
||||
this.results = this.results?.filter(result => result.element !== el)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates grammar mistake markup in DOM.
|
||||
*
|
||||
* @param {Range} range Grammar mistake range
|
||||
* @returns {Object} Client rectangles and grammar mistake highlight elements
|
||||
*/
|
||||
addMistakeMarkup(range) {
|
||||
const clientRects = range.getClientRects()
|
||||
const scrollPanelRect = this.scrollPanel.getBoundingClientRect()
|
||||
let highlights = []
|
||||
for (let i = 0, n = clientRects.length; i < n; ++i) {
|
||||
const rect = clientRects[i]
|
||||
const highlight = document.createElement('div')
|
||||
highlight.classList.add('bes-typo-mistake')
|
||||
const topPosition = rect.top - scrollPanelRect.top
|
||||
const leftPosition = rect.left - scrollPanelRect.left
|
||||
highlight.style.left = `${leftPosition}px`
|
||||
highlight.style.top = `${topPosition}px`
|
||||
highlight.style.width = `${rect.width}px`
|
||||
highlight.style.height = `${rect.height}px`
|
||||
this.scrollPanel.appendChild(highlight)
|
||||
highlights.push(highlight)
|
||||
}
|
||||
return { clientRects, highlights }
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates grammar mistake markup positions.
|
||||
*
|
||||
* @param {Element} el DOM element we want to update markup for
|
||||
*
|
||||
* TODO: Unused
|
||||
*/
|
||||
repositionMarkup(el) {
|
||||
let result = this.results?.find(result => result.element === el)
|
||||
if (!result) return
|
||||
result.matches.forEach(match => {
|
||||
const { clientRects, highlights } = this.addMistakeMarkup(match.range)
|
||||
match.rects = clientRects
|
||||
if (match.highlights) match.highlights.forEach(h => h.remove())
|
||||
match.highlights = highlights
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates all grammar mistake markup positions.
|
||||
*/
|
||||
repositionAllMarkup() {
|
||||
this.results.forEach(result => {
|
||||
result.matches.forEach(match => {
|
||||
const { clientRects, highlights } = this.addMistakeMarkup(match.range)
|
||||
match.rects = clientRects
|
||||
if (match.highlights) match.highlights.forEach(h => h.remove())
|
||||
match.highlights = highlights
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears given block element grammar mistake markup.
|
||||
*
|
||||
* @param {Element} el DOM element we want to clean markup for
|
||||
*/
|
||||
clearMarkup(el) {
|
||||
let result = this.results?.find(result => result.element === el)
|
||||
if (!result) return
|
||||
result.matches.forEach(match => {
|
||||
if (match.highlights) {
|
||||
match.highlights.forEach(h => h.remove())
|
||||
delete match.highlights
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all grammar mistake markup.
|
||||
*
|
||||
* TODO: Unused
|
||||
*/
|
||||
clearAllMarkup() {
|
||||
this.results.forEach(result => {
|
||||
result.matches.forEach(match => {
|
||||
if (match.highlights) {
|
||||
match.highlights.forEach(h => h.remove())
|
||||
delete match.highlights
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if given element is block element.
|
||||
*
|
||||
* @param {Element} el DOM element
|
||||
* @returns false if CSS display property is inline; true otherwise.
|
||||
*/
|
||||
isBlockElement(el) {
|
||||
// Always treat our host element as block.
|
||||
// Otherwise, should one make it inline, proofing would not start on it misbelieving it's a part of a bigger block of text.
|
||||
if (el === this.hostElement) return true
|
||||
switch (
|
||||
document.defaultView
|
||||
.getComputedStyle(el, null)
|
||||
.getPropertyValue('display')
|
||||
.toLowerCase()
|
||||
) {
|
||||
case 'inline':
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns first block parent element of a node.
|
||||
*
|
||||
* @param {Node} node DOM node
|
||||
* @returns {Element} Innermost block element containing given node
|
||||
*/
|
||||
getBlockParent(node) {
|
||||
for (; node; node = node.parentNode) {
|
||||
if (node.nodeType === Node.ELEMENT_NODE && this.isBlockElement(node))
|
||||
return node
|
||||
}
|
||||
return node
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns next node in the DOM text flow.
|
||||
*
|
||||
* @param {Node} node DOM node
|
||||
* @returns {Node} Next node
|
||||
*/
|
||||
static getNextNode(node) {
|
||||
if (node.firstChild) return node.firstChild
|
||||
while (node) {
|
||||
if (node.nextSibling) return node.nextSibling
|
||||
node = node.parentNode
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all ancestors of a node.
|
||||
*
|
||||
* @param {Node} node DOM node
|
||||
* @returns {Array} Array of all ancestors (document...node) describing DOM path
|
||||
*/
|
||||
static getParents(node) {
|
||||
let parents = []
|
||||
do {
|
||||
parents.push(node)
|
||||
node = node.parentNode
|
||||
} while (node)
|
||||
return parents.reverse()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all nodes marked by a range.
|
||||
*
|
||||
* @param {Range} range DOM range
|
||||
* @returns {Array} Array of nodes
|
||||
*/
|
||||
static getNodesInRange(range) {
|
||||
let start = range.startContainer
|
||||
let end = range.endContainer
|
||||
|
||||
// Common ancestor is the last element common to both elements' DOM path.
|
||||
let startAncestors = BesDOMService.getParents(start)
|
||||
let endAncestors = BesDOMService.getParents(end)
|
||||
let commonAncestor = null
|
||||
for (
|
||||
let i = 0;
|
||||
i < startAncestors.length &&
|
||||
i < endAncestors.length &&
|
||||
startAncestors[i] === endAncestors[i];
|
||||
++i
|
||||
)
|
||||
commonAncestor = startAncestors[i]
|
||||
|
||||
let nodes = []
|
||||
let node
|
||||
|
||||
// Walk parent nodes from start to common ancestor.
|
||||
for (node = start.parentNode; node; node = node.parentNode) {
|
||||
nodes.push(node)
|
||||
if (node == commonAncestor) break
|
||||
}
|
||||
nodes.reverse()
|
||||
|
||||
// Walk children and siblings from start until end node is found.
|
||||
for (node = start; node; node = BesDOMService.getNextNode(node)) {
|
||||
nodes.push(node)
|
||||
if (node == end) break
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-register all elements with bes-service class.
|
||||
window.addEventListener('load', () => {
|
||||
document.querySelectorAll('.bes-service').forEach(hostElement => {
|
||||
if (hostElement.tagName === 'TEXTAREA') BesTAService.register(hostElement)
|
||||
else BesDOMService.register(hostElement)
|
||||
})
|
||||
})
|
Reference in New Issue
Block a user