Files
atom/src/view-registry.js
2017-10-26 15:31:43 -06:00

260 lines
8.5 KiB
JavaScript

const Grim = require('grim')
const {Disposable} = require('event-kit')
const 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.
//
// Note: Models can be any object, but must implement a `getTitle()` function
// if they are to be displayed in a {Pane}
//
// 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`.
module.exports =
class ViewRegistry {
constructor (atomEnvironment) {
this.animationFrameRequest = null
this.documentReadInProgress = false
this.performDocumentUpdate = this.performDocumentUpdate.bind(this)
this.atomEnvironment = atomEnvironment
this.clear()
}
clear () {
this.views = new WeakMap()
this.providers = []
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) {
let provider
if (arguments.length === 1) {
switch (typeof modelConstructor) {
case 'function':
provider = {createView: modelConstructor, modelConstructor: AnyConstructor}
break
case 'object':
Grim.deprecate('atom.views.addViewProvider now takes 2 arguments: a model constructor and a createView function. See docs for details.')
provider = modelConstructor
break
default:
throw new TypeError('Arguments to addViewProvider must be functions')
}
} else {
provider = {modelConstructor, createView}
}
this.providers.push(provider)
return new Disposable(() => {
this.providers = this.providers.filter(p => p !== provider)
})
}
getViewProviderCount () {
return this.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.
//
// ## 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 method named `getElement` that returns an
// instance of `HTMLElement`? If true, return that value.
// 3. Does the object have a property named `element` with a value which is
// an instance of `HTMLElement`? If true, return the property value.
// 4. 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]`).
// 5. 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) {
if (object == null) { return }
let view = this.views.get(object)
if (!view) {
view = this.createView(object)
this.views.set(object, view)
}
return view
}
createView (object) {
if (object instanceof HTMLElement) { return object }
let element
if (object && (typeof object.getElement === 'function')) {
element = object.getElement()
if (element instanceof HTMLElement) {
return element
}
}
if (object && object.element instanceof HTMLElement) {
return object.element
}
if (object && object.jquery) {
return object[0]
}
for (let provider of this.providers) {
if (provider.modelConstructor === AnyConstructor) {
element = provider.createView(object, this.atomEnvironment)
if (element) { return element }
continue
}
if (object instanceof provider.modelConstructor) {
element = provider.createView && provider.createView(object, this.atomEnvironment)
if (element) { return element }
let ViewConstructor = provider.viewConstructor
if (ViewConstructor) {
element = new ViewConstructor()
if (element.initialize) {
element.initialize(object)
} else if (element.setModel) {
element.setModel(object)
}
return element
}
}
}
if (object && object.getViewClass) {
let ViewConstructor = object.getViewClass()
if (ViewConstructor) {
const 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) {
this.documentWriters.push(fn)
if (!this.documentReadInProgress) { this.requestDocumentUpdate() }
return new Disposable(() => {
this.documentWriters = this.documentWriters.filter(writer => writer !== fn)
})
}
readDocument (fn) {
this.documentReaders.push(fn)
this.requestDocumentUpdate()
return new Disposable(() => {
this.documentReaders = this.documentReaders.filter(reader => reader !== fn)
})
}
getNextUpdatePromise () {
if (this.nextUpdatePromise == null) {
this.nextUpdatePromise = new Promise(resolve => {
this.resolveNextUpdatePromise = resolve
})
}
return this.nextUpdatePromise
}
clearDocumentRequests () {
this.documentReaders = []
this.documentWriters = []
this.nextUpdatePromise = null
this.resolveNextUpdatePromise = null
if (this.animationFrameRequest != null) {
cancelAnimationFrame(this.animationFrameRequest)
this.animationFrameRequest = null
}
}
requestDocumentUpdate () {
if (this.animationFrameRequest == null) {
this.animationFrameRequest = requestAnimationFrame(this.performDocumentUpdate)
}
}
performDocumentUpdate () {
const { resolveNextUpdatePromise } = this
this.animationFrameRequest = null
this.nextUpdatePromise = null
this.resolveNextUpdatePromise = null
var writer = this.documentWriters.shift()
while (writer) {
writer()
writer = this.documentWriters.shift()
}
var reader = this.documentReaders.shift()
this.documentReadInProgress = true
while (reader) {
reader()
reader = this.documentReaders.shift()
}
this.documentReadInProgress = false
// process updates requested as a result of reads
writer = this.documentWriters.shift()
while (writer) {
writer()
writer = this.documentWriters.shift()
}
if (resolveNextUpdatePromise) { resolveNextUpdatePromise() }
}
}