Get TextEditorElement tests passing

This commit is contained in:
Nathan Sobo
2017-04-15 12:30:33 -06:00
committed by Antonio Scandurra
parent eb33b5c39b
commit c8f2fbb657
5 changed files with 136 additions and 92 deletions

View File

@@ -27,7 +27,7 @@
"color": "^0.7.3",
"dedent": "^0.6.0",
"devtron": "1.3.0",
"etch": "^0.12.0",
"etch": "^0.12.2",
"event-kit": "^2.3.0",
"find-parent-dir": "^0.3.0",
"first-mate": "7.0.4",

View File

@@ -30,11 +30,12 @@ describe "TextEditorElement", ->
expect(element.getModel().getText()).toBe 'testing'
describe "when the model is assigned", ->
it "adds the 'mini' attribute if .isMini() returns true on the model", ->
it "adds the 'mini' attribute if .isMini() returns true on the model", (done) ->
element = new TextEditorElement
model = new TextEditor({mini: true})
element.setModel(model)
expect(element.hasAttribute('mini')).toBe true
element.getModel().update({mini: true})
atom.views.getNextUpdatePromise().then ->
expect(element.hasAttribute('mini')).toBe true
done()
describe "when the editor is attached to the DOM", ->
it "mounts the component and unmounts when removed from the dom", ->
@@ -42,12 +43,12 @@ describe "TextEditorElement", ->
jasmine.attachToDOM(element)
component = element.component
expect(component.mounted).toBe true
expect(component.attached).toBe true
element.remove()
expect(component.mounted).toBe false
expect(component.attached).toBe false
jasmine.attachToDOM(element)
expect(element.component.mounted).toBe true
expect(element.component.attached).toBe true
describe "when the editor is detached from the DOM and then reattached", ->
it "does not render duplicate line numbers", ->
@@ -140,40 +141,6 @@ describe "TextEditorElement", ->
jasmineContent.appendChild(parentElement)
expect(document.activeElement).toBe element.querySelector('input')
describe "when the themes finish loading", ->
[themeReloadCallback, initialThemeLoadComplete, element] = []
beforeEach ->
themeReloadCallback = null
initialThemeLoadComplete = false
spyOn(atom.themes, 'isInitialLoadComplete').andCallFake ->
initialThemeLoadComplete
spyOn(atom.themes, 'onDidChangeActiveThemes').andCallFake (fn) ->
themeReloadCallback = fn
new Disposable
element = new TextEditorElement()
element.style.height = '200px'
element.getModel().update({autoHeight: false})
element.getModel().setText [0..20].join("\n")
it "re-renders the scrollbar", ->
jasmineContent.appendChild(element)
atom.styles.addStyleSheet("""
::-webkit-scrollbar {
width: 8px;
}
""", context: 'atom-text-editor')
initialThemeLoadComplete = true
themeReloadCallback()
verticalScrollbarNode = element.querySelector(".vertical-scrollbar")
scrollbarWidth = verticalScrollbarNode.offsetWidth - verticalScrollbarNode.clientWidth
expect(scrollbarWidth).toEqual(8)
describe "::onDidAttach and ::onDidDetach", ->
it "invokes callbacks when the element is attached and detached", ->
element = new TextEditorElement
@@ -236,19 +203,13 @@ describe "TextEditorElement", ->
jasmine.attachToDOM(element)
expect(element.getMaxScrollTop()).toBe(0)
element.style.height = '100px'
editor.update({autoHeight: false})
element.component.measureDimensions()
expect(element.getMaxScrollTop()).toBe(60)
element.style.height = '120px'
element.component.measureDimensions()
expect(element.getMaxScrollTop()).toBe(40)
element.style.height = '200px'
element.component.measureDimensions()
expect(element.getMaxScrollTop()).toBe(0)
waitsForPromise -> editor.update({autoHeight: false})
runs -> element.style.height = '100px'
waitsFor -> element.getMaxScrollTop() is 60
runs -> element.style.height = '120px'
waitsFor -> element.getMaxScrollTop() is 40
runs -> element.style.height = '200px'
waitsFor -> element.getMaxScrollTop() is 0
describe "on TextEditor::setMini", ->
it "changes the element's 'mini' attribute", ->
@@ -256,9 +217,9 @@ describe "TextEditorElement", ->
jasmine.attachToDOM(element)
expect(element.hasAttribute('mini')).toBe false
element.getModel().setMini(true)
expect(element.hasAttribute('mini')).toBe true
element.getModel().setMini(false)
expect(element.hasAttribute('mini')).toBe false
waitsFor -> element.hasAttribute('mini')
runs -> element.getModel().setMini(false)
waitsFor -> not element.hasAttribute('mini')
describe "events", ->
element = null

View File

@@ -74,6 +74,7 @@ class TextEditorComponent {
this.nextUpdateOnlyBlinksCursors = null
this.horizontalPositionsToMeasure = new Map() // Keys are rows with positions we want to measure, values are arrays of columns to measure
this.horizontalPixelPositionsByScreenLineId = new Map() // Values are maps from column to horiontal pixel positions
this.blockDecorationsToMeasure = new Set()
this.lineNodesByScreenLineId = new Map()
this.textNodesByScreenLineId = new Map()
this.shouldRenderDummyScrollbars = true
@@ -114,27 +115,6 @@ class TextEditorComponent {
this.gutterContainerVnode = null
this.cursorsVnode = null
this.placeholderTextVnode = null
this.blockDecorationMeasurementAreaVnode = $.div({
ref: 'blockDecorationMeasurementArea',
key: 'blockDecorationMeasurementArea',
style: {
contain: 'strict',
position: 'absolute',
visibility: 'hidden'
}
})
this.characterMeasurementLineVnode = $.div(
{
key: 'characterMeasurementLine',
ref: 'characterMeasurementLine',
className: 'line dummy',
style: {position: 'absolute', visibility: 'hidden'}
},
$.span({ref: 'normalWidthCharacterSpan'}, NORMAL_WIDTH_CHARACTER),
$.span({ref: 'doubleWidthCharacterSpan'}, DOUBLE_WIDTH_CHARACTER),
$.span({ref: 'halfWidthCharacterSpan'}, HALF_WIDTH_CHARACTER),
$.span({ref: 'koreanCharacterSpan'}, KOREAN_CHARACTER)
)
this.queryGuttersToRender()
this.queryMaxLineNumberDigits()
@@ -385,11 +365,7 @@ class TextEditorComponent {
attributes,
dataset,
tabIndex: -1,
on: {
focus: this.didFocus,
blur: this.didBlur,
mousewheel: this.didMouseWheel
}
on: {mousewheel: this.didMouseWheel}
},
$.div(
{
@@ -531,14 +507,14 @@ class TextEditorComponent {
children = [
this.renderCursorsAndInput(),
this.renderLineTiles(),
this.blockDecorationMeasurementAreaVnode,
this.characterMeasurementLineVnode,
this.renderBlockDecorationMeasurementArea(),
this.renderCharacterMeasurementLine(),
this.renderPlaceholderText()
]
} else {
children = [
this.blockDecorationMeasurementAreaVnode,
this.characterMeasurementLineVnode
this.renderBlockDecorationMeasurementArea(),
this.renderCharacterMeasurementLine()
]
}
@@ -667,6 +643,33 @@ class TextEditorComponent {
return this.placeholderTextVnode
}
renderCharacterMeasurementLine () {
return $.div(
{
key: 'characterMeasurementLine',
ref: 'characterMeasurementLine',
className: 'line dummy',
style: {position: 'absolute', visibility: 'hidden'}
},
$.span({ref: 'normalWidthCharacterSpan'}, NORMAL_WIDTH_CHARACTER),
$.span({ref: 'doubleWidthCharacterSpan'}, DOUBLE_WIDTH_CHARACTER),
$.span({ref: 'halfWidthCharacterSpan'}, HALF_WIDTH_CHARACTER),
$.span({ref: 'koreanCharacterSpan'}, KOREAN_CHARACTER)
)
}
renderBlockDecorationMeasurementArea () {
return $.div({
ref: 'blockDecorationMeasurementArea',
key: 'blockDecorationMeasurementArea',
style: {
contain: 'strict',
position: 'absolute',
visibility: 'hidden'
}
})
}
renderHiddenInput () {
let top, left
if (this.hiddenInputPosition) {
@@ -1205,6 +1208,8 @@ class TextEditorComponent {
}
}
// Called by TextEditorElement so that focus events can be handled before
// the element is attached to the DOM.
didFocus () {
// This element can be focused from a parent custom element's
// attachedCallback before *its* attachedCallback is fired. This protects
@@ -1243,6 +1248,9 @@ class TextEditorComponent {
}
}
// Called by TextEditorElement so that this function is always the first
// listener to be fired, even if other listeners are bound before creating
// the component.
didBlur (event) {
if (event.relatedTarget === this.refs.hiddenInput) {
event.stopImmediatePropagation()
@@ -2026,7 +2034,6 @@ class TextEditorComponent {
this.disposables.add(model.onDidRemoveGutter(scheduleUpdate))
this.disposables.add(model.selectionsMarkerLayer.onDidUpdate(this.didUpdateSelections.bind(this)))
this.disposables.add(model.onDidRequestAutoscroll(this.didRequestAutoscroll.bind(this)))
this.blockDecorationsToMeasure = new Set()
this.disposables.add(model.observeDecorations((decoration) => {
if (decoration.getProperties().type === 'block') this.observeBlockDecoration(decoration)
}))
@@ -2187,7 +2194,7 @@ class TextEditorComponent {
}
getGutterContainerWidth () {
return this.measurements.gutterContainerWidth
return (this.measurements) ? this.measurements.gutterContainerWidth : 0
}
getLineNumberGutterWidth () {
@@ -2262,6 +2269,7 @@ class TextEditorComponent {
if (scrollTop !== this.scrollTop) {
this.scrollTopPending = true
this.scrollTop = scrollTop
this.element.emitter.emit('did-change-scroll-top', scrollTop)
return true
} else {
return false
@@ -2290,6 +2298,7 @@ class TextEditorComponent {
if (scrollLeft !== this.scrollLeft) {
this.scrollLeftPending = true
this.scrollLeft = scrollLeft
this.element.emitter.emit('did-change-scroll-left', scrollLeft)
return true
} else {
return false

View File

@@ -5,7 +5,6 @@ const dedent = require('dedent')
class TextEditorElement extends HTMLElement {
initialize (component) {
this.component = component
this.emitter = new Emitter()
return this
}
@@ -19,27 +18,85 @@ class TextEditorElement extends HTMLElement {
return this
}
createdCallback () {
this.emitter = new Emitter()
this.initialText = this.textContent
this.tabIndex = -1
this.addEventListener('focus', (event) => this.getComponent().didFocus(event))
this.addEventListener('blur', (event) => this.getComponent().didBlur(event))
}
attachedCallback () {
this.getComponent().didAttach()
this.emitter.emit('did-attach')
this.updateModelFromAttributes()
}
detachedCallback () {
this.emitter.emit('did-detach')
this.getComponent().didDetach()
}
attributeChangedCallback (name, oldValue, newValue) {
if (this.component) {
switch (name) {
case 'mini':
this.getModel().update({mini: newValue != null})
break;
case 'placeholder-text':
this.getModel().update({placeholderText: newValue})
break;
case 'gutter-hidden':
this.getModel().update({isVisible: newValue != null})
break;
}
}
}
getModel () {
return this.getComponent().props.model
}
setModel (model) {
this.getComponent().setModel(model)
this.getComponent().update({model})
this.updateModelFromAttributes()
}
updateModelFromAttributes () {
const props = {
mini: this.hasAttribute('mini'),
placeholderText: this.getAttribute('placeholder-text'),
}
if (this.hasAttribute('gutter-hidden')) props.lineNumberGutterVisible = false
this.getModel().update(props)
if (this.initialText) this.getModel().setText(this.initialText)
}
onDidAttach (callback) {
return this.emitter.on('did-attach', callback)
}
onDidDetach (callback) {
return this.emitter.on('did-detach', callback)
}
setWidth (width) {
this.style.width = this.getComponent().getGutterContainerWidth() + width + 'px'
}
getWidth () {
this.offsetWidth - this.getComponent().getGutterContainerWidth()
}
setHeight (height) {
this.style.height = height + 'px'
}
getHeight () {
return this.offsetHeight
}
onDidChangeScrollLeft (callback) {
return this.emitter.on('did-change-scroll-left', callback)
}
@@ -52,14 +109,26 @@ class TextEditorElement extends HTMLElement {
return this.getComponent().getBaseCharacterWidth()
}
getMaxScrollTop () {
return this.getComponent().getMaxScrollTop()
}
getScrollTop () {
return this.getComponent().getScrollTop()
}
setScrollTop (scrollTop) {
this.getComponent().setScrollTop(scrollTop)
}
getScrollLeft () {
return this.getComponent().getScrollLeft()
}
setScrollLeft (scrollLeft) {
this.getComponent().setScrollLeft(scrollLeft)
}
hasFocus () {
return this.getComponent().focused
}
@@ -81,6 +150,10 @@ class TextEditorElement extends HTMLElement {
return updatedSynchronously
}
isUpdatedSynchronously () {
return this.component ? this.component.updatedSynchronously : this.updatedSynchronously
}
// Experimental: Invalidate the passed block {Decoration}'s dimensions,
// forcing them to be recalculated and the surrounding content to be adjusted
// on the next animation frame.

View File

@@ -321,6 +321,7 @@ class TextEditor extends Model
@cursorLineDecorations = null
else
@decorateCursorLine()
@component?.scheduleUpdate()
when 'placeholderText'
if value isnt @placeholderText