mirror of
https://github.com/pulsar-edit/pulsar.git
synced 2024-11-10 10:17:11 +03:00
Add highlight decoration flashing
This commit is contained in:
parent
4e00139965
commit
6023159819
@ -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', () => {
|
||||
|
@ -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()
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user