mirror of
https://github.com/atom/atom.git
synced 2026-01-24 14:28:14 -05:00
342 lines
11 KiB
CoffeeScript
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
|