Files
atom/src/text-editor-element.coffee
2015-10-09 13:46:32 -07:00

342 lines
11 KiB
CoffeeScript

{Emitter, CompositeDisposable} = require 'event-kit'
Path = require 'path'
{defaults} = require 'underscore-plus'
TextBuffer = require 'text-buffer'
TextEditor = require './text-editor'
TextEditorComponent = require './text-editor-component'
StylesElement = require './styles-element'
ShadowStyleSheet = null
class TextEditorElement extends HTMLElement
model: null
componentDescriptor: null
component: null
attached: false
tileSize: null
focusOnAttach: false
hasTiledRendering: true
logicalDisplayBuffer: true
createdCallback: ->
# Use globals when the following instance variables aren't set.
@config = atom.config
@themes = atom.themes
@workspace = atom.workspace
@assert = atom.assert
@views = atom.views
@styles = atom.styles
@grammars = atom.grammars
@emitter = new Emitter
@subscriptions = new CompositeDisposable
@addEventListener 'focus', @focused.bind(this)
@addEventListener 'blur', @blurred.bind(this)
@classList.add('editor')
@setAttribute('tabindex', -1)
initializeContent: (attributes) ->
if @config.get('editor.useShadowDOM')
@useShadowDOM = true
unless ShadowStyleSheet?
ShadowStyleSheet = document.createElement('style')
ShadowStyleSheet.textContent = @themes.loadLessStylesheet(require.resolve('../static/text-editor-shadow.less'))
@createShadowRoot()
@shadowRoot.appendChild(ShadowStyleSheet.cloneNode(true))
@stylesElement = new StylesElement
@stylesElement.initialize(@styles)
@stylesElement.setAttribute('context', 'atom-text-editor')
@rootElement = document.createElement('div')
@rootElement.classList.add('editor--private')
@shadowRoot.appendChild(@stylesElement)
@shadowRoot.appendChild(@rootElement)
else
@useShadowDOM = false
@classList.add('editor', 'editor-colors')
@stylesElement = document.head.querySelector('atom-styles')
@rootElement = this
attachedCallback: ->
@buildModel() unless @getModel()?
@assert(@model.isAlive(), "Attaching a view for a destroyed editor")
@mountComponent() unless @component?
@listenForComponentEvents()
@component.checkForVisibilityChange()
if this is document.activeElement
@focused()
@emitter.emit("did-attach")
detachedCallback: ->
@unmountComponent()
@subscriptions.dispose()
@subscriptions = new CompositeDisposable
@emitter.emit("did-detach")
listenForComponentEvents: ->
@subscriptions.add @component.onDidChangeScrollTop =>
@emitter.emit("did-change-scroll-top", arguments...)
@subscriptions.add @component.onDidChangeScrollLeft =>
@emitter.emit("did-change-scroll-left", arguments...)
initialize: (model, {@views, @config, @themes, @workspace, @assert, @styles, @grammars}) ->
throw new Error("Must pass a config parameter when initializing TextEditorElements") unless @views?
throw new Error("Must pass a config parameter when initializing TextEditorElements") unless @config?
throw new Error("Must pass a themes parameter when initializing TextEditorElements") unless @themes?
throw new Error("Must pass a workspace parameter when initializing TextEditorElements") unless @workspace?
throw new Error("Must pass a assert parameter when initializing TextEditorElements") unless @assert?
throw new Error("Must pass a styles parameter when initializing TextEditorElements") unless @styles?
throw new Error("Must pass a grammars parameter when initializing TextEditorElements") unless @grammars?
@setModel(model)
this
setModel: (model) ->
throw new Error("Model already assigned on TextEditorElement") if @model?
return if model.isDestroyed()
@model = model
@initializeContent()
@mountComponent()
@addGrammarScopeAttribute()
@addMiniAttribute() if @model.isMini()
@addEncodingAttribute()
@model.onDidChangeGrammar => @addGrammarScopeAttribute()
@model.onDidChangeEncoding => @addEncodingAttribute()
@model.onDidDestroy => @unmountComponent()
@model.onDidChangeMini (mini) => if mini then @addMiniAttribute() else @removeMiniAttribute()
@model
getModel: ->
@model ? @buildModel()
buildModel: ->
@setModel(@workspace.buildTextEditor(
buffer: new TextBuffer(@textContent)
softWrapped: false
tabLength: 2
softTabs: true
mini: @hasAttribute('mini')
lineNumberGutterVisible: not @hasAttribute('gutter-hidden')
placeholderText: @getAttribute('placeholder-text')
))
mountComponent: ->
@component = new TextEditorComponent(
hostElement: this
rootElement: @rootElement
stylesElement: @stylesElement
editor: @model
tileSize: @tileSize
useShadowDOM: @useShadowDOM
views: @views
themes: @themes
config: @config
workspace: @workspace
assert: @assert
grammars: @grammars
)
@rootElement.appendChild(@component.getDomNode())
if @useShadowDOM
@shadowRoot.addEventListener('blur', @shadowRootBlurred.bind(this), true)
else
inputNode = @component.hiddenInputComponent.getDomNode()
inputNode.addEventListener 'focus', @focused.bind(this)
inputNode.addEventListener 'blur', => @dispatchEvent(new FocusEvent('blur', bubbles: false))
unmountComponent: ->
if @component?
@component.destroy()
@component.getDomNode().remove()
@component = null
focused: ->
@component?.focused()
blurred: (event) ->
unless @useShadowDOM
if event.relatedTarget is @component.hiddenInputComponent.getDomNode()
event.stopImmediatePropagation()
return
@component?.blurred()
# Work around what seems to be a bug in Chromium. Focus can be stolen from the
# hidden input when clicking on the gutter and transferred to the
# already-focused host element. The host element never gets a 'focus' event
# however, which leaves us in a limbo state where the text editor element is
# focused but the hidden input isn't focused. This always refocuses the hidden
# input if a blur event occurs in the shadow DOM that is transferring focus
# back to the host element.
shadowRootBlurred: (event) ->
@component.focused() if event.relatedTarget is this
addGrammarScopeAttribute: ->
@dataset.grammar = @model.getGrammar()?.scopeName?.replace(/\./g, ' ')
addMiniAttribute: ->
@setAttributeNode(document.createAttribute("mini"))
removeMiniAttribute: ->
@removeAttribute("mini")
addEncodingAttribute: ->
@dataset.encoding = @model.getEncoding()
hasFocus: ->
this is document.activeElement or @contains(document.activeElement)
setUpdatedSynchronously: (@updatedSynchronously) -> @updatedSynchronously
isUpdatedSynchronously: -> @updatedSynchronously
# Extended: Continuously reflows lines and line numbers. (Has performance overhead)
#
# `continuousReflow` A {Boolean} indicating whether to keep reflowing or not.
setContinuousReflow: (continuousReflow) ->
@component?.setContinuousReflow(continuousReflow)
# Extended: get the width of a character of text displayed in this element.
#
# Returns a {Number} of pixels.
getDefaultCharacterWidth: ->
@getModel().getDefaultCharWidth()
# Extended: Converts a buffer position to a pixel position.
#
# * `bufferPosition` An object that represents a buffer position. It can be either
# an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point}
#
# Returns an {Object} with two values: `top` and `left`, representing the pixel position.
pixelPositionForBufferPosition: (bufferPosition) ->
@component.pixelPositionForBufferPosition(bufferPosition)
# Extended: Converts a screen position to a pixel position.
#
# * `screenPosition` An object that represents a screen position. It can be either
# an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point}
#
# Returns an {Object} with two values: `top` and `left`, representing the pixel positions.
pixelPositionForScreenPosition: (screenPosition) ->
@component.pixelPositionForScreenPosition(screenPosition)
# Extended: Retrieves the number of the row that is visible and currently at the
# top of the editor.
#
# Returns a {Number}.
getFirstVisibleScreenRow: ->
@getVisibleRowRange()[0]
# Extended: Retrieves the number of the row that is visible and currently at the
# bottom of the editor.
#
# Returns a {Number}.
getLastVisibleScreenRow: ->
@getVisibleRowRange()[1]
# Extended: call the given `callback` when the editor is attached to the DOM.
#
# * `callback` {Function}
onDidAttach: (callback) ->
@emitter.on("did-attach", callback)
# Extended: call the given `callback` when the editor is detached from the DOM.
#
# * `callback` {Function}
onDidDetach: (callback) ->
@emitter.on("did-detach", callback)
onDidChangeScrollTop: (callback) ->
@emitter.on("did-change-scroll-top", callback)
onDidChangeScrollLeft: (callback) ->
@emitter.on("did-change-scroll-left", callback)
setScrollLeft: (scrollLeft) ->
@component.setScrollLeft(scrollLeft)
setScrollRight: (scrollRight) ->
@component.setScrollRight(scrollRight)
setScrollTop: (scrollTop) ->
@component.setScrollTop(scrollTop)
setScrollBottom: (scrollBottom) ->
@component.setScrollBottom(scrollBottom)
# Essential: Scrolls the editor to the top
scrollToTop: ->
@setScrollTop(0)
# Essential: Scrolls the editor to the bottom
scrollToBottom: ->
@setScrollBottom(Infinity)
getScrollTop: ->
@component?.getScrollTop() or 0
getScrollLeft: ->
@component?.getScrollLeft() or 0
getScrollRight: ->
@component?.getScrollRight() or 0
getScrollBottom: ->
@component?.getScrollBottom() or 0
getScrollHeight: ->
@component?.getScrollHeight() or 0
getScrollWidth: ->
@component?.getScrollWidth() or 0
getVerticalScrollbarWidth: ->
@component?.getVerticalScrollbarWidth() or 0
getHorizontalScrollbarHeight: ->
@component?.getHorizontalScrollbarHeight() or 0
getVisibleRowRange: ->
@component?.getVisibleRowRange() or [0, 0]
intersectsVisibleRowRange: (startRow, endRow) ->
[visibleStart, visibleEnd] = @getVisibleRowRange()
not (endRow <= visibleStart or visibleEnd <= startRow)
selectionIntersectsVisibleRowRange: (selection) ->
{start, end} = selection.getScreenRange()
@intersectsVisibleRowRange(start.row, end.row + 1)
screenPositionForPixelPosition: (pixelPosition) ->
@component.screenPositionForPixelPosition(pixelPosition)
pixelRectForScreenRange: (screenRange) ->
@component.pixelRectForScreenRange(screenRange)
pixelRangeForScreenRange: (screenRange) ->
@component.pixelRangeForScreenRange(screenRange)
setWidth: (width) ->
@style.width = (@component.getGutterWidth() + width) + "px"
@component.measureDimensions()
getWidth: ->
@offsetWidth - @component.getGutterWidth()
setHeight: (height) ->
@style.height = height + "px"
@component.measureDimensions()
getHeight: ->
@offsetHeight
module.exports = TextEditorElement = document.registerElement 'atom-text-editor', prototype: TextEditorElement.prototype