Create, update and destroy highlights manually

Etch's reconciliation routine causes elements to be sometimes
re-ordered. In order to move an element, however, Etch needs to first
detach it from the DOM and then re-append it at the right location.

This behavior is unacceptable for highlight decorations because it could
re-start CSS animations on a certain highlight decoration when a
completely different one is added or removed.

Even though we are still interested in restructuring etch's
reconciliation logic to prevent unwanted re-orderings, with this commit
we are switching to a custom routine to create/update/remove highlight
decorations that prevents unnecessary moves and, as a result, fixes the
undesired behavior described above.
This commit is contained in:
Antonio Scandurra
2017-08-10 17:25:50 +02:00
parent e48b980d41
commit 00d27befe8
2 changed files with 82 additions and 27 deletions

View File

@@ -3417,8 +3417,10 @@ class CursorsAndInputComponent {
class LinesTileComponent {
constructor (props) {
this.highlightComponentsByKey = new Map()
this.props = props
etch.initialize(this)
this.updateHighlights()
this.createLines()
this.updateBlockDecorations({}, props)
}
@@ -3432,13 +3434,22 @@ class LinesTileComponent {
this.updateLines(oldProps, newProps)
this.updateBlockDecorations(oldProps, newProps)
}
this.updateHighlights()
}
}
destroy () {
this.highlightComponentsByKey.forEach((highlightComponent) => {
highlightComponent.destroy()
})
this.highlightComponentsByKey.clear()
for (let i = 0; i < this.lineComponents.length; i++) {
this.lineComponents[i].destroy()
}
this.lineComponents.length = 0
return etch.destroy(this)
}
render () {
@@ -3456,34 +3467,12 @@ class LinesTileComponent {
backgroundColor: 'inherit'
}
},
this.renderHighlights()
// Lines and block decorations will be manually inserted here for efficiency
)
}
renderHighlights () {
const {top, lineHeight, highlightDecorations} = this.props
let children = null
if (highlightDecorations) {
const decorationCount = highlightDecorations.length
children = new Array(decorationCount)
for (let i = 0; i < decorationCount; i++) {
const highlightProps = Object.assign(
{parentTileTop: top, lineHeight},
highlightDecorations[i]
)
children[i] = $(HighlightComponent, highlightProps)
highlightDecorations[i].flashRequested = false
}
}
return $.div(
{
$.div({
ref: 'highlights',
className: 'highlights',
style: {contain: 'layout'}
},
children
style: {layout: 'contain'}
})
// Lines and block decorations will be manually inserted here for efficiency
)
}
@@ -3676,6 +3665,40 @@ class LinesTileComponent {
}
}
updateHighlights () {
const {top, lineHeight, highlightDecorations} = this.props
const visibleHighlightDecorations = new Set()
if (highlightDecorations) {
for (let i = 0; i < highlightDecorations.length; i++) {
const highlightDecoration = highlightDecorations[i]
const highlightProps = Object.assign(
{parentTileTop: top, lineHeight},
highlightDecorations[i]
)
let highlightComponent = this.highlightComponentsByKey.get(highlightDecoration.key)
if (highlightComponent) {
highlightComponent.update(highlightProps)
} else {
highlightComponent = new HighlightComponent(highlightProps)
this.refs.highlights.appendChild(highlightComponent.element)
this.highlightComponentsByKey.set(highlightDecoration.key, highlightComponent)
}
highlightDecorations[i].flashRequested = false
visibleHighlightDecorations.add(highlightDecoration.key)
}
}
this.highlightComponentsByKey.forEach((highlightComponent, key) => {
if (!visibleHighlightDecorations.has(key)) {
highlightComponent.destroy()
this.highlightComponentsByKey.delete(key)
}
})
}
shouldUpdate (newProps) {
const oldProps = this.props
if (oldProps.top !== newProps.top) return true
@@ -3876,6 +3899,17 @@ class HighlightComponent {
if (this.props.flashRequested) this.performFlash()
}
destroy () {
if (this.timeoutsByClassName) {
this.timeoutsByClassName.forEach((timeout) => {
window.clearTimeout(timeout)
})
this.timeoutsByClassName.clear()
}
return etch.destroy(this)
}
update (newProps) {
this.props = newProps
etch.updateSync(this)