diff --git a/package.json b/package.json index b05837050..6eb57d2fe 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/spec/text-editor-element-spec.coffee b/spec/text-editor-element-spec.coffee index 867599c55..a64d1ac20 100644 --- a/spec/text-editor-element-spec.coffee +++ b/spec/text-editor-element-spec.coffee @@ -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 diff --git a/src/text-editor-component.js b/src/text-editor-component.js index 73fec8f7e..b601a4d35 100644 --- a/src/text-editor-component.js +++ b/src/text-editor-component.js @@ -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 diff --git a/src/text-editor-element.js b/src/text-editor-element.js index 2a0f46496..9cf995ee9 100644 --- a/src/text-editor-element.js +++ b/src/text-editor-element.js @@ -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. diff --git a/src/text-editor.coffee b/src/text-editor.coffee index 7196e2118..ac0a05ca5 100644 --- a/src/text-editor.coffee +++ b/src/text-editor.coffee @@ -321,6 +321,7 @@ class TextEditor extends Model @cursorLineDecorations = null else @decorateCursorLine() + @component?.scheduleUpdate() when 'placeholderText' if value isnt @placeholderText