pulsar/src/gutter-component.coffee

189 lines
7.0 KiB
CoffeeScript
Raw Normal View History

_ = require 'underscore-plus'
React = require 'react-atom-fork'
{div} = require 'reactionary-atom-fork'
2014-05-14 18:47:15 +04:00
{isEqual, isEqualForProperties, multiplyString, toArray} = require 'underscore-plus'
SubscriberMixin = require './subscriber-mixin'
2014-04-14 20:11:11 +04:00
2014-05-14 18:47:15 +04:00
WrapperDiv = document.createElement('div')
2014-04-14 20:11:11 +04:00
module.exports =
GutterComponent = React.createClass
2014-04-15 20:39:47 +04:00
displayName: 'GutterComponent'
mixins: [SubscriberMixin]
2014-04-15 20:39:47 +04:00
dummyLineNumberNode: null
2014-04-14 20:11:11 +04:00
render: ->
2014-06-06 23:23:12 +04:00
{scrollHeight, scrollTop, onClick} = @props
2014-06-06 23:23:12 +04:00
div className: 'gutter', onClick: onClick,
div className: 'line-numbers', ref: 'lineNumbers', style:
height: scrollHeight
WebkitTransform: "translate3d(0px, #{-scrollTop}px, 0px)"
2014-05-14 18:47:15 +04:00
componentWillMount: ->
@lineNumberNodesById = {}
@lineNumberIdsByScreenRow = {}
@screenRowsByLineNumberId = {}
@previousDecorations = {}
2014-04-14 20:11:11 +04:00
componentDidMount: ->
@appendDummyLineNumber()
# Only update the gutter if the visible row range has changed or if a
# non-zero-delta change to the screen lines has occurred within the current
# visible row range.
shouldComponentUpdate: (newProps) ->
return true unless isEqualForProperties(newProps, @props,
'renderedRowRange', 'scrollTop', 'lineHeightInPixels', 'mouseWheelScreenRow'
)
{renderedRowRange, pendingChanges, decorations} = newProps
2014-05-14 18:47:15 +04:00
for change in pendingChanges when Math.abs(change.screenDelta) > 0 or Math.abs(change.bufferDelta) > 0
return true unless change.end <= renderedRowRange.start or renderedRowRange.end <= change.start
return true unless _.isEqual(@previousDecorations, decorations)
false
componentDidUpdate: (oldProps) ->
unless oldProps.maxLineNumberDigits is @props.maxLineNumberDigits
@updateDummyLineNumber()
@removeLineNumberNodes()
@clearScreenRowCaches() unless oldProps.lineHeightInPixels is @props.lineHeightInPixels
2014-05-14 18:47:15 +04:00
@updateLineNumbers()
clearScreenRowCaches: ->
@lineNumberIdsByScreenRow = {}
@screenRowsByLineNumberId = {}
# This dummy line number element holds the gutter to the appropriate width,
# since the real line numbers are absolutely positioned for performance reasons.
appendDummyLineNumber: ->
{maxLineNumberDigits} = @props
WrapperDiv.innerHTML = @buildLineNumberHTML(0, false, maxLineNumberDigits)
@dummyLineNumberNode = WrapperDiv.children[0]
@refs.lineNumbers.getDOMNode().appendChild(@dummyLineNumberNode)
updateDummyLineNumber: ->
@dummyLineNumberNode.innerHTML = @buildLineNumberInnerHTML(0, false, @props.maxLineNumberDigits)
2014-05-14 18:47:15 +04:00
updateLineNumbers: ->
lineNumberIdsToPreserve = @appendOrUpdateVisibleLineNumberNodes()
@removeLineNumberNodes(lineNumberIdsToPreserve)
2014-04-16 03:17:30 +04:00
2014-05-14 18:47:15 +04:00
appendOrUpdateVisibleLineNumberNodes: ->
{editor, renderedRowRange, scrollTop, maxLineNumberDigits, decorations} = @props
[startRow, endRow] = renderedRowRange
2014-05-14 18:47:15 +04:00
newLineNumberIds = null
newLineNumbersHTML = null
visibleLineNumberIds = new Set
wrapCount = 0
for bufferRow, index in editor.bufferRowsForScreenRows(startRow, endRow - 1)
screenRow = startRow + index
2014-05-14 18:47:15 +04:00
if bufferRow is lastBufferRow
id = "#{bufferRow}-#{wrapCount++}"
else
id = bufferRow.toString()
lastBufferRow = bufferRow
wrapCount = 0
visibleLineNumberIds.add(id)
if @hasLineNumberNode(id)
@updateLineNumberNode(id, bufferRow, screenRow, wrapCount > 0, decorations[bufferRow])
2014-05-14 18:47:15 +04:00
else
newLineNumberIds ?= []
newLineNumbersHTML ?= ""
newLineNumberIds.push(id)
newLineNumbersHTML += @buildLineNumberHTML(bufferRow, wrapCount > 0, maxLineNumberDigits, screenRow, decorations[bufferRow])
@screenRowsByLineNumberId[id] = screenRow
@lineNumberIdsByScreenRow[screenRow] = id
2014-05-14 18:47:15 +04:00
if newLineNumberIds?
WrapperDiv.innerHTML = newLineNumbersHTML
newLineNumberNodes = toArray(WrapperDiv.children)
node = @refs.lineNumbers.getDOMNode()
for lineNumberId, i in newLineNumberIds
lineNumberNode = newLineNumberNodes[i]
@lineNumberNodesById[lineNumberId] = lineNumberNode
node.appendChild(lineNumberNode)
@previousDecorations = decorations
2014-05-14 18:47:15 +04:00
visibleLineNumberIds
removeLineNumberNodes: (lineNumberIdsToPreserve) ->
{mouseWheelScreenRow} = @props
2014-05-14 18:47:15 +04:00
node = @refs.lineNumbers.getDOMNode()
for lineNumberId, lineNumberNode of @lineNumberNodesById when not lineNumberIdsToPreserve?.has(lineNumberId)
screenRow = @screenRowsByLineNumberId[lineNumberId]
if not screenRow? or screenRow isnt mouseWheelScreenRow
delete @lineNumberNodesById[lineNumberId]
delete @lineNumberIdsByScreenRow[screenRow] if @lineNumberIdsByScreenRow[screenRow] is lineNumberId
delete @screenRowsByLineNumberId[lineNumberId]
node.removeChild(lineNumberNode)
2014-05-14 18:47:15 +04:00
buildLineNumberHTML: (bufferRow, softWrapped, maxLineNumberDigits, screenRow, decorations) ->
if screenRow?
{lineHeightInPixels} = @props
style = "position: absolute; top: #{screenRow * lineHeightInPixels}px;"
else
style = "visibility: hidden;"
innerHTML = @buildLineNumberInnerHTML(bufferRow, softWrapped, maxLineNumberDigits)
classes = ''
if decorations?
for decoration in decorations
classes += decoration.class + ' ' if not softWrapped or softWrapped and decoration.softWrap
classes += 'line-number'
"<div class=\"#{classes}\" style=\"#{style}\" data-buffer-row=\"#{bufferRow}\" data-screen-row=\"#{screenRow}\">#{innerHTML}</div>"
buildLineNumberInnerHTML: (bufferRow, softWrapped, maxLineNumberDigits) ->
2014-05-14 18:47:15 +04:00
if softWrapped
lineNumber = ""
2014-04-14 20:11:11 +04:00
else
2014-05-14 18:47:15 +04:00
lineNumber = (bufferRow + 1).toString()
2014-04-14 20:11:11 +04:00
2014-05-14 18:47:15 +04:00
padding = multiplyString('&nbsp;', maxLineNumberDigits - lineNumber.length)
iconHTML = '<div class="icon-right"></div>'
padding + lineNumber + iconHTML
2014-05-14 18:47:15 +04:00
updateLineNumberNode: (lineNumberId, bufferRow, screenRow, softWrapped, decorations) ->
2014-06-04 04:26:57 +04:00
node = @lineNumberNodesById[lineNumberId]
previousDecorations = @previousDecorations[bufferRow]
2014-06-04 04:26:57 +04:00
if previousDecorations?
for decoration in previousDecorations
node.classList.remove(decoration.class) if not contains(decorations, decoration)
2014-06-04 04:26:57 +04:00
2014-06-06 05:12:36 +04:00
if decorations?
for decoration in decorations
if not contains(previousDecorations, decoration) and (not softWrapped or softWrapped and decoration.softWrap)
node.classList.add(decoration.class)
2014-06-04 05:44:35 +04:00
unless @screenRowsByLineNumberId[lineNumberId] is screenRow
{lineHeightInPixels} = @props
2014-06-04 04:26:57 +04:00
node.style.top = screenRow * lineHeightInPixels + 'px'
node.dataset.screenRow = screenRow
@screenRowsByLineNumberId[lineNumberId] = screenRow
@lineNumberIdsByScreenRow[screenRow] = lineNumberId
2014-05-14 18:47:15 +04:00
hasLineNumberNode: (lineNumberId) ->
@lineNumberNodesById.hasOwnProperty(lineNumberId)
lineNumberNodeForScreenRow: (screenRow) ->
@lineNumberNodesById[@lineNumberIdsByScreenRow[screenRow]]
2014-06-04 04:26:57 +04:00
# Created because underscore uses === not _.isEqual, which we need
contains = (array, target) ->
return false unless array?
for object in array
return true if _.isEqual(object, target)
false