mirror of
https://github.com/atom/atom.git
synced 2026-01-24 14:28:14 -05:00
277 lines
9.1 KiB
CoffeeScript
277 lines
9.1 KiB
CoffeeScript
{find} = require 'underscore-plus'
|
|
Grim = require 'grim'
|
|
{Disposable} = require 'event-kit'
|
|
_ = require 'underscore-plus'
|
|
|
|
AnyConstructor = Symbol('any-constructor')
|
|
|
|
# Essential: `ViewRegistry` handles the association between model and view
|
|
# types in Atom. We call this association a View Provider. As in, for a given
|
|
# model, this class can provide a view via {::getView}, as long as the
|
|
# model/view association was registered via {::addViewProvider}
|
|
#
|
|
# If you're adding your own kind of pane item, a good strategy for all but the
|
|
# simplest items is to separate the model and the view. The model handles
|
|
# application logic and is the primary point of API interaction. The view
|
|
# just handles presentation.
|
|
#
|
|
# View providers inform the workspace how your model objects should be
|
|
# presented in the DOM. A view provider must always return a DOM node, which
|
|
# makes [HTML 5 custom elements](http://www.html5rocks.com/en/tutorials/webcomponents/customelements/)
|
|
# an ideal tool for implementing views in Atom.
|
|
#
|
|
# You can access the `ViewRegistry` object via `atom.views`.
|
|
#
|
|
# ## Examples
|
|
#
|
|
# ### Getting the workspace element
|
|
#
|
|
# ```coffee
|
|
# workspaceElement = atom.views.getView(atom.workspace)
|
|
# ```
|
|
#
|
|
# ### Getting An Editor Element
|
|
#
|
|
# ```coffee
|
|
# textEditor = atom.workspace.getActiveTextEditor()
|
|
# textEditorElement = atom.views.getView(textEditor)
|
|
# ```
|
|
#
|
|
# ### Getting A Pane Element
|
|
#
|
|
# ```coffee
|
|
# pane = atom.workspace.getActivePane()
|
|
# paneElement = atom.views.getView(pane)
|
|
# ```
|
|
module.exports =
|
|
class ViewRegistry
|
|
animationFrameRequest: null
|
|
documentReadInProgress: false
|
|
performDocumentPollAfterUpdate: false
|
|
debouncedPerformDocumentPoll: null
|
|
minimumPollInterval: 200
|
|
|
|
constructor: (@atomEnvironment) ->
|
|
@observer = new MutationObserver(@requestDocumentPoll)
|
|
@clear()
|
|
|
|
clear: ->
|
|
@views = new WeakMap
|
|
@providers = []
|
|
@debouncedPerformDocumentPoll = _.throttle(@performDocumentPoll, @minimumPollInterval).bind(this)
|
|
@clearDocumentRequests()
|
|
|
|
# Essential: Add a provider that will be used to construct views in the
|
|
# workspace's view layer based on model objects in its model layer.
|
|
#
|
|
# ## Examples
|
|
#
|
|
# Text editors are divided into a model and a view layer, so when you interact
|
|
# with methods like `atom.workspace.getActiveTextEditor()` you're only going
|
|
# to get the model object. We display text editors on screen by teaching the
|
|
# workspace what view constructor it should use to represent them:
|
|
#
|
|
# ```coffee
|
|
# atom.views.addViewProvider TextEditor, (textEditor) ->
|
|
# textEditorElement = new TextEditorElement
|
|
# textEditorElement.initialize(textEditor)
|
|
# textEditorElement
|
|
# ```
|
|
#
|
|
# * `modelConstructor` (optional) Constructor {Function} for your model. If
|
|
# a constructor is given, the `createView` function will only be used
|
|
# for model objects inheriting from that constructor. Otherwise, it will
|
|
# will be called for any object.
|
|
# * `createView` Factory {Function} that is passed an instance of your model
|
|
# and must return a subclass of `HTMLElement` or `undefined`. If it returns
|
|
# `undefined`, then the registry will continue to search for other view
|
|
# providers.
|
|
#
|
|
# Returns a {Disposable} on which `.dispose()` can be called to remove the
|
|
# added provider.
|
|
addViewProvider: (modelConstructor, createView) ->
|
|
if arguments.length is 1
|
|
switch typeof modelConstructor
|
|
when 'function'
|
|
provider = {createView: modelConstructor, modelConstructor: AnyConstructor}
|
|
when 'object'
|
|
Grim.deprecate("atom.views.addViewProvider now takes 2 arguments: a model constructor and a createView function. See docs for details.")
|
|
provider = modelConstructor
|
|
else
|
|
throw new TypeError("Arguments to addViewProvider must be functions")
|
|
else
|
|
provider = {modelConstructor, createView}
|
|
|
|
@providers.push(provider)
|
|
new Disposable =>
|
|
@providers = @providers.filter (p) -> p isnt provider
|
|
|
|
getViewProviderCount: ->
|
|
@providers.length
|
|
|
|
# Essential: Get the view associated with an object in the workspace.
|
|
#
|
|
# If you're just *using* the workspace, you shouldn't need to access the view
|
|
# layer, but view layer access may be necessary if you want to perform DOM
|
|
# manipulation that isn't supported via the model API.
|
|
#
|
|
# ## Examples
|
|
#
|
|
# ### Getting An Editor Element
|
|
#
|
|
# ```coffee
|
|
# textEditor = atom.workspace.getActiveTextEditor()
|
|
# textEditorElement = atom.views.getView(textEditor)
|
|
# ```
|
|
#
|
|
# ### Getting A Pane Element
|
|
#
|
|
# ```coffee
|
|
# pane = atom.workspace.getActivePane()
|
|
# paneElement = atom.views.getView(pane)
|
|
# ```
|
|
#
|
|
# ### Getting The Workspace Element
|
|
#
|
|
# ```coffee
|
|
# workspaceElement = atom.views.getView(atom.workspace)
|
|
# ```
|
|
#
|
|
# * `object` The object for which you want to retrieve a view. This can be a
|
|
# pane item, a pane, or the workspace itself.
|
|
#
|
|
# ## View Resolution Algorithm
|
|
#
|
|
# The view associated with the object is resolved using the following
|
|
# sequence
|
|
#
|
|
# 1. Is the object an instance of `HTMLElement`? If true, return the object.
|
|
# 2. Does the object have a property named `element` with a value which is
|
|
# an instance of `HTMLElement`? If true, return the property value.
|
|
# 3. Is the object a jQuery object, indicated by the presence of a `jquery`
|
|
# property? If true, return the root DOM element (i.e. `object[0]`).
|
|
# 4. Has a view provider been registered for the object? If true, use the
|
|
# provider to create a view associated with the object, and return the
|
|
# view.
|
|
#
|
|
# If no associated view is returned by the sequence an error is thrown.
|
|
#
|
|
# Returns a DOM element.
|
|
getView: (object) ->
|
|
return unless object?
|
|
|
|
if view = @views.get(object)
|
|
view
|
|
else
|
|
view = @createView(object)
|
|
@views.set(object, view)
|
|
view
|
|
|
|
createView: (object) ->
|
|
if object instanceof HTMLElement
|
|
return object
|
|
|
|
if object?.element instanceof HTMLElement
|
|
return object.element
|
|
|
|
if object?.jquery
|
|
return object[0]
|
|
|
|
for provider in @providers
|
|
if provider.modelConstructor is AnyConstructor
|
|
if element = provider.createView(object, @atomEnvironment)
|
|
return element
|
|
continue
|
|
|
|
if object instanceof provider.modelConstructor
|
|
if element = provider.createView?(object, @atomEnvironment)
|
|
return element
|
|
|
|
if viewConstructor = provider.viewConstructor
|
|
element = new viewConstructor
|
|
element.initialize?(object) ? element.setModel?(object)
|
|
return element
|
|
|
|
if viewConstructor = object?.getViewClass?()
|
|
view = new viewConstructor(object)
|
|
return view[0]
|
|
|
|
throw new Error("Can't create a view for #{object.constructor.name} instance. Please register a view provider.")
|
|
|
|
updateDocument: (fn) ->
|
|
@documentWriters.push(fn)
|
|
@requestDocumentUpdate() unless @documentReadInProgress
|
|
new Disposable =>
|
|
@documentWriters = @documentWriters.filter (writer) -> writer isnt fn
|
|
|
|
readDocument: (fn) ->
|
|
@documentReaders.push(fn)
|
|
@requestDocumentUpdate()
|
|
new Disposable =>
|
|
@documentReaders = @documentReaders.filter (reader) -> reader isnt fn
|
|
|
|
pollDocument: (fn) ->
|
|
@startPollingDocument() if @documentPollers.length is 0
|
|
@documentPollers.push(fn)
|
|
new Disposable =>
|
|
@documentPollers = @documentPollers.filter (poller) -> poller isnt fn
|
|
@stopPollingDocument() if @documentPollers.length is 0
|
|
|
|
pollAfterNextUpdate: ->
|
|
@performDocumentPollAfterUpdate = true
|
|
|
|
getNextUpdatePromise: ->
|
|
@nextUpdatePromise ?= new Promise (resolve) =>
|
|
@resolveNextUpdatePromise = resolve
|
|
|
|
clearDocumentRequests: ->
|
|
@documentReaders = []
|
|
@documentWriters = []
|
|
@documentPollers = []
|
|
@nextUpdatePromise = null
|
|
@resolveNextUpdatePromise = null
|
|
if @animationFrameRequest?
|
|
cancelAnimationFrame(@animationFrameRequest)
|
|
@animationFrameRequest = null
|
|
@stopPollingDocument()
|
|
|
|
requestDocumentUpdate: ->
|
|
@animationFrameRequest ?= requestAnimationFrame(@performDocumentUpdate)
|
|
|
|
performDocumentUpdate: =>
|
|
resolveNextUpdatePromise = @resolveNextUpdatePromise
|
|
@animationFrameRequest = null
|
|
@nextUpdatePromise = null
|
|
@resolveNextUpdatePromise = null
|
|
|
|
writer() while writer = @documentWriters.shift()
|
|
|
|
@documentReadInProgress = true
|
|
reader() while reader = @documentReaders.shift()
|
|
@performDocumentPoll() if @performDocumentPollAfterUpdate
|
|
@performDocumentPollAfterUpdate = false
|
|
@documentReadInProgress = false
|
|
|
|
# process updates requested as a result of reads
|
|
writer() while writer = @documentWriters.shift()
|
|
|
|
resolveNextUpdatePromise?()
|
|
|
|
startPollingDocument: ->
|
|
window.addEventListener('resize', @requestDocumentPoll)
|
|
@observer.observe(document, {subtree: true, childList: true, attributes: true})
|
|
|
|
stopPollingDocument: ->
|
|
window.removeEventListener('resize', @requestDocumentPoll)
|
|
@observer.disconnect()
|
|
|
|
requestDocumentPoll: =>
|
|
if @animationFrameRequest?
|
|
@performDocumentPollAfterUpdate = true
|
|
else
|
|
@debouncedPerformDocumentPoll()
|
|
|
|
performDocumentPoll: ->
|
|
poller() for poller in @documentPollers
|
|
return
|