From e952ab2e0263c8d3410a805064fc770dfd2012cf Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 14 Apr 2014 13:48:54 -0600 Subject: [PATCH] Extract a LinesComponent --- spec/editor-component-spec.coffee | 24 ++-- src/editor-component.coffee | 13 +- src/editor-scroll-view-component.coffee | 155 +++--------------------- src/lines-component.coffee | 112 +++++++++++++++++ 4 files changed, 142 insertions(+), 162 deletions(-) create mode 100644 src/lines-component.coffee diff --git a/spec/editor-component-spec.coffee b/spec/editor-component-spec.coffee index efbe92a21..ab37e2d7c 100644 --- a/spec/editor-component-spec.coffee +++ b/spec/editor-component-spec.coffee @@ -27,7 +27,9 @@ describe "EditorComponent", -> {component} = wrapperView component.setLineHeight(1.3) component.setFontSize(20) - {lineHeightInPixels, charWidth} = component.measureLineDimensions() + + lineHeightInPixels = editor.getLineHeight() + charWidth = editor.getDefaultCharWidth() node = component.getDOMNode() verticalScrollbarNode = node.querySelector('.vertical-scrollbar') horizontalScrollbarNode = node.querySelector('.horizontal-scrollbar') @@ -35,7 +37,7 @@ describe "EditorComponent", -> describe "line rendering", -> it "renders only the currently-visible lines", -> node.style.height = 4.5 * lineHeightInPixels + 'px' - component.updateAllDimensions() + component.updateModelDimensions() lines = node.querySelectorAll('.line') expect(lines.length).toBe 6 @@ -111,7 +113,7 @@ describe "EditorComponent", -> it "renders the currently-visible line numbers", -> node.style.height = 4.5 * lineHeightInPixels + 'px' - component.updateAllDimensions() + component.updateModelDimensions() lines = node.querySelectorAll('.line-number') expect(lines.length).toBe 6 @@ -136,7 +138,7 @@ describe "EditorComponent", -> editor.setSoftWrap(true) node.style.height = 4.5 * lineHeightInPixels + 'px' node.style.width = 30 * charWidth + 'px' - component.updateAllDimensions() + component.updateModelDimensions() lines = node.querySelectorAll('.line-number') expect(lines.length).toBe 6 @@ -153,7 +155,7 @@ describe "EditorComponent", -> cursor1.setScreenPosition([0, 5]) node.style.height = 4.5 * lineHeightInPixels + 'px' - component.updateAllDimensions() + component.updateModelDimensions() cursorNodes = node.querySelectorAll('.cursor') expect(cursorNodes.length).toBe 1 @@ -248,7 +250,7 @@ describe "EditorComponent", -> inputNode = node.querySelector('.hidden-input') node.style.height = 5 * lineHeightInPixels + 'px' node.style.width = 10 * charWidth + 'px' - component.updateAllDimensions() + component.updateModelDimensions() expect(editor.getCursorScreenPosition()).toEqual [0, 0] editor.setScrollTop(3 * lineHeightInPixels) @@ -332,7 +334,7 @@ describe "EditorComponent", -> it "moves the cursor to the nearest screen position", -> node.style.height = 4.5 * lineHeightInPixels + 'px' node.style.width = 10 * charWidth + 'px' - component.updateAllDimensions() + component.updateModelDimensions() editor.setScrollTop(3.5 * lineHeightInPixels) editor.setScrollLeft(2 * charWidth) @@ -445,7 +447,7 @@ describe "EditorComponent", -> describe "scrolling", -> it "updates the vertical scrollbar when the scrollTop is changed in the model", -> node.style.height = 4.5 * lineHeightInPixels + 'px' - component.updateAllDimensions() + component.updateModelDimensions() expect(verticalScrollbarNode.scrollTop).toBe 0 @@ -454,7 +456,7 @@ describe "EditorComponent", -> it "updates the horizontal scrollbar and scroll view content x transform based on the scrollLeft of the model", -> node.style.width = 30 * charWidth + 'px' - component.updateAllDimensions() + component.updateModelDimensions() scrollViewContentNode = node.querySelector('.scroll-view-content') expect(scrollViewContentNode.style['-webkit-transform']).toBe "translate(0px, 0px)" @@ -466,7 +468,7 @@ describe "EditorComponent", -> it "updates the scrollLeft of the model when the scrollLeft of the horizontal scrollbar changes", -> node.style.width = 30 * charWidth + 'px' - component.updateAllDimensions() + component.updateModelDimensions() expect(editor.getScrollLeft()).toBe 0 horizontalScrollbarNode.scrollLeft = 100 @@ -478,7 +480,7 @@ describe "EditorComponent", -> it "updates the horizontal or vertical scrollbar depending on which delta is greater (x or y)", -> node.style.height = 4.5 * lineHeightInPixels + 'px' node.style.width = 20 * charWidth + 'px' - component.updateAllDimensions() + component.updateModelDimensions() expect(verticalScrollbarNode.scrollTop).toBe 0 expect(horizontalScrollbarNode.scrollLeft).toBe 0 diff --git a/src/editor-component.coffee b/src/editor-component.coffee index 12f1f0f7f..2f4cd8cfb 100644 --- a/src/editor-component.coffee +++ b/src/editor-component.coffee @@ -4,7 +4,6 @@ React = require 'react' GutterComponent = require './gutter-component' EditorScrollViewComponent = require './editor-scroll-view-component' -{DummyLineNode} = EditorScrollViewComponent ScrollbarComponent = require './scrollbar-component' SubscriberMixin = require './subscriber-mixin' @@ -14,8 +13,6 @@ EditorCompont = React.createClass pendingScrollLeft: null selectOnMouseMove: false - statics: {DummyLineNode} - mixins: [SubscriberMixin] render: -> @@ -286,11 +283,5 @@ EditorCompont = React.createClass requestUpdate: -> @forceUpdate() - measureLineDimensions: -> - @refs.scrollView.measureLineDimensions() - - updateAllDimensions: -> - @refs.scrollView.updateAllDimensions() - - updateScrollViewDimensions: -> - @refs.scrollView.updateScrollViewDimensions() + updateModelDimensions: -> + @refs.scrollView.updateModelDimensions() diff --git a/src/editor-scroll-view-component.coffee b/src/editor-scroll-view-component.coffee index 722452b2b..a5a46c17d 100644 --- a/src/editor-scroll-view-component.coffee +++ b/src/editor-scroll-view-component.coffee @@ -2,22 +2,23 @@ React = require 'react' ReactUpdates = require 'react/lib/ReactUpdates' {div, span} = require 'reactionary' {debounce, isEqual, multiplyString, pick} = require 'underscore-plus' -{$$} = require 'space-pen' InputComponent = require './input-component' +LinesComponent = require './lines-component' CursorComponent = require './cursor-component' SelectionComponent = require './selection-component' SubscriberMixin = require './subscriber-mixin' -DummyLineNode = $$(-> @div className: 'line', style: 'position: absolute; visibility: hidden;', => @span 'x')[0] -AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT} - module.exports = EditorScrollViewComponent = React.createClass mixins: [SubscriberMixin] render: -> - {onInputFocused, onInputBlurred} = @props + {editor, fontSize, fontFamily, lineHeight, showIndentGuide} = @props + {visibleRowRange, onInputFocused, onInputBlurred} = @props + contentStyle = + height: editor.getScrollHeight() + WebkitTransform: "translate(#{-editor.getScrollLeft()}px, #{-editor.getScrollTop()}px)" div className: 'scroll-view', ref: 'scrollView', InputComponent @@ -27,18 +28,11 @@ EditorScrollViewComponent = React.createClass onInput: @onInput onFocus: onInputFocused onBlur: onInputBlurred - @renderScrollViewContent() - renderScrollViewContent: -> - {editor} = @props - style = - height: editor.getScrollHeight() - WebkitTransform: "translate(#{-editor.getScrollLeft()}px, #{-editor.getScrollTop()}px)" - - div {className: 'scroll-view-content', style, @onMouseDown}, - @renderCursors() - @renderVisibleLines() - @renderUnderlayer() + div className: 'scroll-view-content', style: contentStyle, onMouseDown: @onMouseDown, + @renderCursors() + LinesComponent({ref: 'lines', editor, fontSize, fontFamily, lineHeight, visibleRowRange, showIndentGuide}) + @renderUnderlayer() renderCursors: -> {editor} = @props @@ -47,21 +41,6 @@ EditorScrollViewComponent = React.createClass for selection in editor.getSelections() when editor.selectionIntersectsVisibleRowRange(selection) CursorComponent(cursor: selection.cursor, blinkOff: blinkCursorsOff) - renderVisibleLines: -> - {editor, visibleRowRange} = @props - {showIndentGuide} = @props - [startRow, endRow] = visibleRowRange - lineHeightInPixels = editor.getLineHeight() - precedingHeight = startRow * lineHeightInPixels - followingHeight = (editor.getScreenLineCount() - endRow) * lineHeightInPixels - - div className: 'lines', ref: 'lines', [ - div className: 'spacer', key: 'top-spacer', style: {height: precedingHeight} - (for tokenizedLine in @props.editor.linesForScreenRows(startRow, endRow - 1) - LineComponent({tokenizedLine, showIndentGuide, key: tokenizedLine.id}))... - div className: 'spacer', key: 'bottom-spacer', style: {height: followingHeight} - ] - renderUnderlayer: -> {editor} = @props @@ -73,25 +52,11 @@ EditorScrollViewComponent = React.createClass blinkCursorsOff: false componentDidMount: -> - @measuredLines = new WeakSet - - @getDOMNode().addEventListener 'overflowchanged', @onOverflowChanged - + @getDOMNode().addEventListener 'overflowchanged', @updateModelDimensions @subscribe @props.editor, 'cursors-moved', @pauseCursorBlinking - - - @updateAllDimensions() + @updateModelDimensions() @startBlinkingCursors() - componentDidUpdate: (prevProps) -> - unless isEqual(pick(prevProps, 'fontSize', 'fontFamily', 'lineHeight'), pick(@props, 'fontSize', 'fontFamily', 'lineHeight')) - @updateLineDimensions() - - unless isEqual(pick(prevProps, 'fontSize', 'fontFamily'), pick(@props, 'fontSize', 'fontFamily')) - @clearScopedCharWidths() - - @measureNewLines() - focus: -> @refs.input.focus() @@ -196,98 +161,8 @@ EditorScrollViewComponent = React.createClass left = clientX - editorClientRect.left + editor.getScrollLeft() {top, left} - onOverflowChanged: -> + updateModelDimensions: -> {editor} = @props - {height, width} = @measureScrollViewDimensions() - editor.setHeight(height) - editor.setWidth(width) - - updateAllDimensions: -> - @updateScrollViewDimensions() - @updateLineDimensions() - - updateScrollViewDimensions: -> - {editor} = @props - {height, width} = @measureScrollViewDimensions() - editor.setHeight(height) - editor.setWidth(width) - - updateLineDimensions: -> - {editor} = @props - {lineHeightInPixels, charWidth} = @measureLineDimensions() - editor.setLineHeight(lineHeightInPixels) - editor.setDefaultCharWidth(charWidth) - - measureScrollViewDimensions: -> node = @getDOMNode() - {height: node.clientHeight, width: node.clientWidth} - - measureLineDimensions: -> - linesNode = @refs.lines.getDOMNode() - linesNode.appendChild(DummyLineNode) - lineHeightInPixels = DummyLineNode.getBoundingClientRect().height - charWidth = DummyLineNode.firstChild.getBoundingClientRect().width - linesNode.removeChild(DummyLineNode) - {lineHeightInPixels, charWidth} - - measureNewLines: -> - [visibleStartRow, visibleEndRow] = @props.visibleRowRange - linesNode = @refs.lines.getDOMNode() - - for tokenizedLine, i in @props.editor.linesForScreenRows(visibleStartRow, visibleEndRow - 1) - unless @measuredLines.has(tokenizedLine) - lineNode = linesNode.children[i + 1] - @measureCharactersInLine(tokenizedLine, lineNode) - - measureCharactersInLine: (tokenizedLine, lineNode) -> - {editor} = @props - iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT, AcceptFilter) - rangeForMeasurement = document.createRange() - - for {value, scopes} in tokenizedLine.tokens - textNode = iterator.nextNode() - charWidths = editor.getScopedCharWidths(scopes) - for char, i in value - unless charWidths[char]? - rangeForMeasurement.setStart(textNode, i) - rangeForMeasurement.setEnd(textNode, i + 1) - charWidth = rangeForMeasurement.getBoundingClientRect().width - editor.setScopedCharWidth(scopes, char, charWidth) - - @measuredLines.add(tokenizedLine) - - clearScopedCharWidths: -> - @measuredLines.clear() - @props.editor.clearScopedCharWidths() - -LineComponent = React.createClass - render: -> - div className: 'line', dangerouslySetInnerHTML: {__html: @buildInnerHTML()} - - buildInnerHTML: -> - if @props.tokenizedLine.text.length is 0 - @buildEmptyLineHTML() - else - @buildScopeTreeHTML(@props.tokenizedLine.getScopeTree()) - - buildEmptyLineHTML: -> - {showIndentGuide, tokenizedLine} = @props - {indentLevel, tabLength} = tokenizedLine - - if showIndentGuide and indentLevel > 0 - indentSpan = "#{multiplyString(' ', tabLength)}" - multiplyString(indentSpan, indentLevel + 1) - else - " " - - buildScopeTreeHTML: (scopeTree) -> - if scopeTree.children? - html = "" - html += @buildScopeTreeHTML(child) for child in scopeTree.children - html += "" - html - else - "#{scopeTree.getValueAsHtml({hasIndentGuide: @props.showIndentGuide})}" - - shouldComponentUpdate: (newProps, newState) -> - newProps.showIndentGuide isnt @props.showIndentGuide + editor.setHeight(node.clientHeight) + editor.setWidth(node.clientWidth) diff --git a/src/lines-component.coffee b/src/lines-component.coffee new file mode 100644 index 000000000..b700e91dd --- /dev/null +++ b/src/lines-component.coffee @@ -0,0 +1,112 @@ +React = require 'react' +{div, span} = require 'reactionary' +{debounce, isEqual, multiplyString, pick} = require 'underscore-plus' +{$$} = require 'space-pen' + +DummyLineNode = $$(-> @div className: 'line', style: 'position: absolute; visibility: hidden;', => @span 'x')[0] +AcceptFilter = {acceptNode: -> NodeFilter.FILTER_ACCEPT} + +module.exports = +LinesComponent = React.createClass + render: -> + {editor, visibleRowRange, showIndentGuide} = @props + [startRow, endRow] = visibleRowRange + lineHeightInPixels = editor.getLineHeight() + precedingHeight = startRow * lineHeightInPixels + followingHeight = (editor.getScreenLineCount() - endRow) * lineHeightInPixels + + div className: 'lines', ref: 'lines', [ + div className: 'spacer', key: 'top-spacer', style: {height: precedingHeight} + (for tokenizedLine in @props.editor.linesForScreenRows(startRow, endRow - 1) + LineComponent({tokenizedLine, showIndentGuide, key: tokenizedLine.id}))... + div className: 'spacer', key: 'bottom-spacer', style: {height: followingHeight} + ] + + componentDidMount: -> + @measuredLines = new WeakSet + @updateModelDimensions() + + componentDidUpdate: (prevProps) -> + @updateModelDimensions() unless @compareProps(prevProps, @props, 'fontSize', 'fontFamily', 'lineHeight') + @clearScopedCharWidths() unless @compareProps(prevProps, @props, 'fontSize', 'fontFamily') + @measureCharactersInNewLines() + + compareProps: (a, b, whiteList...) -> + isEqual(pick(a, whiteList...), pick(b, whiteList...)) + + updateModelDimensions: -> + {editor} = @props + {lineHeightInPixels, charWidth} = @measureLineDimensions() + editor.setLineHeight(lineHeightInPixels) + editor.setDefaultCharWidth(charWidth) + + measureLineDimensions: -> + linesNode = @refs.lines.getDOMNode() + linesNode.appendChild(DummyLineNode) + lineHeightInPixels = DummyLineNode.getBoundingClientRect().height + charWidth = DummyLineNode.firstChild.getBoundingClientRect().width + linesNode.removeChild(DummyLineNode) + {lineHeightInPixels, charWidth} + + measureCharactersInNewLines: -> + [visibleStartRow, visibleEndRow] = @props.visibleRowRange + linesNode = @refs.lines.getDOMNode() + + for tokenizedLine, i in @props.editor.linesForScreenRows(visibleStartRow, visibleEndRow - 1) + unless @measuredLines.has(tokenizedLine) + lineNode = linesNode.children[i + 1] + @measureCharactersInLine(tokenizedLine, lineNode) + + measureCharactersInLine: (tokenizedLine, lineNode) -> + {editor} = @props + iterator = document.createNodeIterator(lineNode, NodeFilter.SHOW_TEXT, AcceptFilter) + rangeForMeasurement = document.createRange() + + for {value, scopes} in tokenizedLine.tokens + textNode = iterator.nextNode() + charWidths = editor.getScopedCharWidths(scopes) + for char, i in value + unless charWidths[char]? + rangeForMeasurement.setStart(textNode, i) + rangeForMeasurement.setEnd(textNode, i + 1) + charWidth = rangeForMeasurement.getBoundingClientRect().width + editor.setScopedCharWidth(scopes, char, charWidth) + + @measuredLines.add(tokenizedLine) + + clearScopedCharWidths: -> + @measuredLines.clear() + @props.editor.clearScopedCharWidths() + + +LineComponent = React.createClass + render: -> + div className: 'line', dangerouslySetInnerHTML: {__html: @buildInnerHTML()} + + buildInnerHTML: -> + if @props.tokenizedLine.text.length is 0 + @buildEmptyLineHTML() + else + @buildScopeTreeHTML(@props.tokenizedLine.getScopeTree()) + + buildEmptyLineHTML: -> + {showIndentGuide, tokenizedLine} = @props + {indentLevel, tabLength} = tokenizedLine + + if showIndentGuide and indentLevel > 0 + indentSpan = "#{multiplyString(' ', tabLength)}" + multiplyString(indentSpan, indentLevel + 1) + else + " " + + buildScopeTreeHTML: (scopeTree) -> + if scopeTree.children? + html = "" + html += @buildScopeTreeHTML(child) for child in scopeTree.children + html += "" + html + else + "#{scopeTree.getValueAsHtml({hasIndentGuide: @props.showIndentGuide})}" + + shouldComponentUpdate: (newProps, newState) -> + newProps.showIndentGuide isnt @props.showIndentGuide