Add highlight decoration flashing

This commit is contained in:
Nathan Sobo 2017-03-17 16:38:59 -06:00 committed by Antonio Scandurra
parent 4e00139965
commit 6023159819
3 changed files with 113 additions and 9 deletions

View File

@ -714,6 +714,76 @@ describe('TextEditorComponent', () => {
expect(Math.round(region3Rect.right)).toBe(clientLeftForCharacter(component, 5, 4))
}
})
it('can flash highlight decorations', async () => {
const {component, element, editor} = buildComponent({rowsPerTile: 3, height: 200})
const marker = editor.markScreenRange([[2, 4], [3, 4]])
const decoration = editor.decorateMarker(marker, {type: 'highlight', class: 'a'})
decoration.flash('b', 10)
// Flash on initial appearence of highlight
await component.getNextUpdatePromise()
const highlights = element.querySelectorAll('.highlight.a')
expect(highlights.length).toBe(2) // split across 2 tiles
expect(highlights[0].classList.contains('b')).toBe(true)
expect(highlights[1].classList.contains('b')).toBe(true)
await conditionPromise(() =>
!highlights[0].classList.contains('b') &&
!highlights[1].classList.contains('b')
)
// Don't flash on next update if another flash wasn't requested
component.refs.scroller.scrollTop = 100
await component.getNextUpdatePromise()
expect(highlights[0].classList.contains('b')).toBe(false)
expect(highlights[1].classList.contains('b')).toBe(false)
// Flash existing highlight
decoration.flash('c', 100)
await component.getNextUpdatePromise()
expect(highlights[0].classList.contains('c')).toBe(true)
expect(highlights[1].classList.contains('c')).toBe(true)
// Add second flash class
decoration.flash('d', 100)
await component.getNextUpdatePromise()
expect(highlights[0].classList.contains('c')).toBe(true)
expect(highlights[1].classList.contains('c')).toBe(true)
expect(highlights[0].classList.contains('d')).toBe(true)
expect(highlights[1].classList.contains('d')).toBe(true)
await conditionPromise(() =>
!highlights[0].classList.contains('c') &&
!highlights[1].classList.contains('c') &&
!highlights[0].classList.contains('d') &&
!highlights[1].classList.contains('d')
)
// Flashing the same class again before the first flash completes
// removes the flash class and adds it back on the next frame to ensure
// CSS transitions apply to the second flash.
decoration.flash('e', 100)
await component.getNextUpdatePromise()
expect(highlights[0].classList.contains('e')).toBe(true)
expect(highlights[1].classList.contains('e')).toBe(true)
decoration.flash('e', 100)
await component.getNextUpdatePromise()
expect(highlights[0].classList.contains('e')).toBe(false)
expect(highlights[1].classList.contains('e')).toBe(false)
await conditionPromise(() =>
highlights[0].classList.contains('e') &&
highlights[1].classList.contains('e')
)
await conditionPromise(() =>
!highlights[0].classList.contains('e') &&
!highlights[1].classList.contains('e')
)
})
})
describe('mouse input', () => {

View File

@ -171,8 +171,7 @@ class Decoration
true
flash: (klass, duration=500) ->
@properties.flashCount ?= 0
@properties.flashCount++
@properties.flashRequested = true
@properties.flashClass = klass
@properties.flashDuration = duration
@decorationManager.emitDidUpdateDecorations()

View File

@ -575,6 +575,10 @@ class TextEditorComponent {
addHighlightDecorationToMeasure(decoration, screenRange) {
screenRange = constrainRangeToRows(screenRange, this.getRenderedStartRow(), this.getRenderedEndRow())
if (screenRange.isEmpty()) return
const {class: className, flashRequested, flashClass, flashDuration} = decoration
decoration.flashRequested = false
let tileStartRow = this.tileStartRowForRow(screenRange.start.row)
const rowsPerTile = this.getRowsPerTile()
@ -587,7 +591,11 @@ class TextEditorComponent {
tileHighlights = []
this.decorationsToMeasure.highlights.set(tileStartRow, tileHighlights)
}
tileHighlights.push({decoration, screenRange: screenRangeInTile})
tileHighlights.push({
screenRange: screenRangeInTile,
className, flashRequested, flashClass, flashDuration
})
this.requestHorizontalMeasurement(screenRangeInTile.start.row, screenRangeInTile.start.column)
this.requestHorizontalMeasurement(screenRangeInTile.end.row, screenRangeInTile.end.column)
@ -1780,6 +1788,7 @@ class LinesTileComponent {
highlightDecorations[i]
)
children[i] = $(HighlightComponent, highlightProps)
highlightDecorations[i].flashRequested = false
}
}
@ -1846,7 +1855,8 @@ class LinesTileComponent {
for (let i = 0, length = oldProps.highlightDecorations.length; i < length; i++) {
const oldHighlight = oldProps.highlightDecorations[i]
const newHighlight = newProps.highlightDecorations[i]
if (oldHighlight.decoration.class !== newHighlight.decoration.class) return true
if (oldHighlight.className !== newHighlight.className) return true
if (newHighlight.flashRequested) return true
if (oldHighlight.startPixelLeft !== newHighlight.startPixelLeft) return true
if (oldHighlight.endPixelLeft !== newHighlight.endPixelLeft) return true
if (!oldHighlight.screenRange.isEqual(newHighlight.screenRange)) return true
@ -1935,17 +1945,43 @@ class HighlightComponent {
constructor (props) {
this.props = props
etch.initialize(this)
if (this.props.flashRequested) this.performFlash()
}
update (props) {
this.props = props
update (newProps) {
this.props = newProps
etch.updateSync(this)
if (newProps.flashRequested) this.performFlash()
}
performFlash () {
const {flashClass, flashDuration} = this.props
const addAndRemoveFlashClass = () => {
this.element.classList.add(flashClass)
if (!this.timeoutsByClassName) {
this.timeoutsByClassName = new Map()
} else if (this.timeoutsByClassName.has(flashClass)) {
window.clearTimeout(this.timeoutsByClassName.get(flashClass))
}
this.timeoutsByClassName.set(flashClass, window.setTimeout(() => {
this.element.classList.remove(flashClass)
}, flashDuration))
}
if (this.element.classList.contains(flashClass)) {
this.element.classList.remove(flashClass)
window.requestAnimationFrame(addAndRemoveFlashClass)
} else {
addAndRemoveFlashClass()
}
}
render () {
let {startPixelTop, endPixelTop} = this.props
const {
decoration, screenRange, parentTileTop, lineHeight,
className, screenRange, parentTileTop, lineHeight,
startPixelLeft, endPixelLeft,
} = this.props
startPixelTop -= parentTileTop
@ -2007,8 +2043,7 @@ class HighlightComponent {
}
}
const className = 'highlight ' + decoration.class
return $.div({className}, children)
return $.div({className: 'highlight ' + className}, children)
}
}