mirror of
https://github.com/pulsar-edit/pulsar.git
synced 2024-09-21 07:58:04 +03:00
Merge pull request #15958 from atom/decaf-view-registry
☠☕ Decaffeinate `ViewRegistry`
This commit is contained in:
commit
622589f4f7
@ -1,163 +0,0 @@
|
||||
ViewRegistry = require '../src/view-registry'
|
||||
|
||||
describe "ViewRegistry", ->
|
||||
registry = null
|
||||
|
||||
beforeEach ->
|
||||
registry = new ViewRegistry
|
||||
|
||||
afterEach ->
|
||||
registry.clearDocumentRequests()
|
||||
|
||||
describe "::getView(object)", ->
|
||||
describe "when passed a DOM node", ->
|
||||
it "returns the given DOM node", ->
|
||||
node = document.createElement('div')
|
||||
expect(registry.getView(node)).toBe node
|
||||
|
||||
describe "when passed an object with an element property", ->
|
||||
it "returns the element property if it's an instance of HTMLElement", ->
|
||||
class TestComponent
|
||||
constructor: -> @element = document.createElement('div')
|
||||
|
||||
component = new TestComponent
|
||||
expect(registry.getView(component)).toBe component.element
|
||||
|
||||
describe "when passed an object with a getElement function", ->
|
||||
it "returns the return value of getElement if it's an instance of HTMLElement", ->
|
||||
class TestComponent
|
||||
getElement: ->
|
||||
@myElement ?= document.createElement('div')
|
||||
|
||||
component = new TestComponent
|
||||
expect(registry.getView(component)).toBe component.myElement
|
||||
|
||||
describe "when passed a model object", ->
|
||||
describe "when a view provider is registered matching the object's constructor", ->
|
||||
it "constructs a view element and assigns the model on it", ->
|
||||
class TestModel
|
||||
|
||||
class TestModelSubclass extends TestModel
|
||||
|
||||
class TestView
|
||||
initialize: (@model) -> this
|
||||
|
||||
model = new TestModel
|
||||
|
||||
registry.addViewProvider TestModel, (model) ->
|
||||
new TestView().initialize(model)
|
||||
|
||||
view = registry.getView(model)
|
||||
expect(view instanceof TestView).toBe true
|
||||
expect(view.model).toBe model
|
||||
|
||||
subclassModel = new TestModelSubclass
|
||||
view2 = registry.getView(subclassModel)
|
||||
expect(view2 instanceof TestView).toBe true
|
||||
expect(view2.model).toBe subclassModel
|
||||
|
||||
describe "when a view provider is registered generically, and works with the object", ->
|
||||
it "constructs a view element and assigns the model on it", ->
|
||||
model = {a: 'b'}
|
||||
|
||||
registry.addViewProvider (model) ->
|
||||
if model.a is 'b'
|
||||
element = document.createElement('div')
|
||||
element.className = 'test-element'
|
||||
element
|
||||
|
||||
view = registry.getView({a: 'b'})
|
||||
expect(view.className).toBe 'test-element'
|
||||
|
||||
expect(-> registry.getView({a: 'c'})).toThrow()
|
||||
|
||||
describe "when no view provider is registered for the object's constructor", ->
|
||||
it "throws an exception", ->
|
||||
expect(-> registry.getView(new Object)).toThrow()
|
||||
|
||||
describe "::addViewProvider(providerSpec)", ->
|
||||
it "returns a disposable that can be used to remove the provider", ->
|
||||
class TestModel
|
||||
class TestView
|
||||
initialize: (@model) -> this
|
||||
|
||||
disposable = registry.addViewProvider TestModel, (model) ->
|
||||
new TestView().initialize(model)
|
||||
|
||||
expect(registry.getView(new TestModel) instanceof TestView).toBe true
|
||||
disposable.dispose()
|
||||
expect(-> registry.getView(new TestModel)).toThrow()
|
||||
|
||||
describe "::updateDocument(fn) and ::readDocument(fn)", ->
|
||||
frameRequests = null
|
||||
|
||||
beforeEach ->
|
||||
frameRequests = []
|
||||
spyOn(window, 'requestAnimationFrame').andCallFake (fn) -> frameRequests.push(fn)
|
||||
|
||||
it "performs all pending writes before all pending reads on the next animation frame", ->
|
||||
events = []
|
||||
|
||||
registry.updateDocument -> events.push('write 1')
|
||||
registry.readDocument -> events.push('read 1')
|
||||
registry.readDocument -> events.push('read 2')
|
||||
registry.updateDocument -> events.push('write 2')
|
||||
|
||||
expect(events).toEqual []
|
||||
|
||||
expect(frameRequests.length).toBe 1
|
||||
frameRequests[0]()
|
||||
expect(events).toEqual ['write 1', 'write 2', 'read 1', 'read 2']
|
||||
|
||||
frameRequests = []
|
||||
events = []
|
||||
disposable = registry.updateDocument -> events.push('write 3')
|
||||
registry.updateDocument -> events.push('write 4')
|
||||
registry.readDocument -> events.push('read 3')
|
||||
|
||||
disposable.dispose()
|
||||
|
||||
expect(frameRequests.length).toBe 1
|
||||
frameRequests[0]()
|
||||
expect(events).toEqual ['write 4', 'read 3']
|
||||
|
||||
it "performs writes requested from read callbacks in the same animation frame", ->
|
||||
spyOn(window, 'setInterval').andCallFake(fakeSetInterval)
|
||||
spyOn(window, 'clearInterval').andCallFake(fakeClearInterval)
|
||||
events = []
|
||||
|
||||
registry.updateDocument -> events.push('write 1')
|
||||
registry.readDocument ->
|
||||
registry.updateDocument -> events.push('write from read 1')
|
||||
events.push('read 1')
|
||||
registry.readDocument ->
|
||||
registry.updateDocument -> events.push('write from read 2')
|
||||
events.push('read 2')
|
||||
registry.updateDocument -> events.push('write 2')
|
||||
|
||||
expect(frameRequests.length).toBe 1
|
||||
frameRequests[0]()
|
||||
expect(frameRequests.length).toBe 1
|
||||
|
||||
expect(events).toEqual [
|
||||
'write 1'
|
||||
'write 2'
|
||||
'read 1'
|
||||
'read 2'
|
||||
'write from read 1'
|
||||
'write from read 2'
|
||||
]
|
||||
|
||||
describe "::getNextUpdatePromise()", ->
|
||||
it "returns a promise that resolves at the end of the next update cycle", ->
|
||||
updateCalled = false
|
||||
readCalled = false
|
||||
|
||||
waitsFor 'getNextUpdatePromise to resolve', (done) ->
|
||||
registry.getNextUpdatePromise().then ->
|
||||
expect(updateCalled).toBe true
|
||||
expect(readCalled).toBe true
|
||||
done()
|
||||
|
||||
registry.updateDocument -> updateCalled = true
|
||||
registry.readDocument -> readCalled = true
|
216
spec/view-registry-spec.js
Normal file
216
spec/view-registry-spec.js
Normal file
@ -0,0 +1,216 @@
|
||||
/*
|
||||
* decaffeinate suggestions:
|
||||
* DS102: Remove unnecessary code created because of implicit returns
|
||||
* DS207: Consider shorter variations of null checks
|
||||
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
||||
*/
|
||||
const ViewRegistry = require('../src/view-registry')
|
||||
|
||||
describe('ViewRegistry', () => {
|
||||
let registry = null
|
||||
|
||||
beforeEach(() => {
|
||||
registry = new ViewRegistry()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
registry.clearDocumentRequests()
|
||||
})
|
||||
|
||||
describe('::getView(object)', () => {
|
||||
describe('when passed a DOM node', () =>
|
||||
it('returns the given DOM node', () => {
|
||||
const node = document.createElement('div')
|
||||
expect(registry.getView(node)).toBe(node)
|
||||
})
|
||||
)
|
||||
|
||||
describe('when passed an object with an element property', () =>
|
||||
it("returns the element property if it's an instance of HTMLElement", () => {
|
||||
class TestComponent {
|
||||
constructor () {
|
||||
this.element = document.createElement('div')
|
||||
}
|
||||
}
|
||||
|
||||
const component = new TestComponent()
|
||||
expect(registry.getView(component)).toBe(component.element)
|
||||
})
|
||||
)
|
||||
|
||||
describe('when passed an object with a getElement function', () =>
|
||||
it("returns the return value of getElement if it's an instance of HTMLElement", () => {
|
||||
class TestComponent {
|
||||
getElement () {
|
||||
if (this.myElement == null) {
|
||||
this.myElement = document.createElement('div')
|
||||
}
|
||||
return this.myElement
|
||||
}
|
||||
}
|
||||
|
||||
const component = new TestComponent()
|
||||
expect(registry.getView(component)).toBe(component.myElement)
|
||||
})
|
||||
)
|
||||
|
||||
describe('when passed a model object', () => {
|
||||
describe("when a view provider is registered matching the object's constructor", () =>
|
||||
it('constructs a view element and assigns the model on it', () => {
|
||||
class TestModel {}
|
||||
|
||||
class TestModelSubclass extends TestModel {}
|
||||
|
||||
class TestView {
|
||||
initialize (model) {
|
||||
this.model = model
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
const model = new TestModel()
|
||||
|
||||
registry.addViewProvider(TestModel, (model) =>
|
||||
new TestView().initialize(model)
|
||||
)
|
||||
|
||||
const view = registry.getView(model)
|
||||
expect(view instanceof TestView).toBe(true)
|
||||
expect(view.model).toBe(model)
|
||||
|
||||
const subclassModel = new TestModelSubclass()
|
||||
const view2 = registry.getView(subclassModel)
|
||||
expect(view2 instanceof TestView).toBe(true)
|
||||
expect(view2.model).toBe(subclassModel)
|
||||
})
|
||||
)
|
||||
|
||||
describe('when a view provider is registered generically, and works with the object', () =>
|
||||
it('constructs a view element and assigns the model on it', () => {
|
||||
registry.addViewProvider((model) => {
|
||||
if (model.a === 'b') {
|
||||
const element = document.createElement('div')
|
||||
element.className = 'test-element'
|
||||
return element
|
||||
}
|
||||
})
|
||||
|
||||
const view = registry.getView({a: 'b'})
|
||||
expect(view.className).toBe('test-element')
|
||||
|
||||
expect(() => registry.getView({a: 'c'})).toThrow()
|
||||
})
|
||||
)
|
||||
|
||||
describe("when no view provider is registered for the object's constructor", () =>
|
||||
it('throws an exception', () => {
|
||||
expect(() => registry.getView({})).toThrow()
|
||||
})
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('::addViewProvider(providerSpec)', () =>
|
||||
it('returns a disposable that can be used to remove the provider', () => {
|
||||
class TestModel {}
|
||||
class TestView {
|
||||
initialize (model) {
|
||||
this.model = model
|
||||
return this
|
||||
}
|
||||
}
|
||||
|
||||
const disposable = registry.addViewProvider(TestModel, (model) =>
|
||||
new TestView().initialize(model)
|
||||
)
|
||||
|
||||
expect(registry.getView(new TestModel()) instanceof TestView).toBe(true)
|
||||
disposable.dispose()
|
||||
expect(() => registry.getView(new TestModel())).toThrow()
|
||||
})
|
||||
)
|
||||
|
||||
describe('::updateDocument(fn) and ::readDocument(fn)', () => {
|
||||
let frameRequests = null
|
||||
|
||||
beforeEach(() => {
|
||||
frameRequests = []
|
||||
spyOn(window, 'requestAnimationFrame').andCallFake(fn => frameRequests.push(fn))
|
||||
})
|
||||
|
||||
it('performs all pending writes before all pending reads on the next animation frame', () => {
|
||||
let events = []
|
||||
|
||||
registry.updateDocument(() => events.push('write 1'))
|
||||
registry.readDocument(() => events.push('read 1'))
|
||||
registry.readDocument(() => events.push('read 2'))
|
||||
registry.updateDocument(() => events.push('write 2'))
|
||||
|
||||
expect(events).toEqual([])
|
||||
|
||||
expect(frameRequests.length).toBe(1)
|
||||
frameRequests[0]()
|
||||
expect(events).toEqual(['write 1', 'write 2', 'read 1', 'read 2'])
|
||||
|
||||
frameRequests = []
|
||||
events = []
|
||||
const disposable = registry.updateDocument(() => events.push('write 3'))
|
||||
registry.updateDocument(() => events.push('write 4'))
|
||||
registry.readDocument(() => events.push('read 3'))
|
||||
|
||||
disposable.dispose()
|
||||
|
||||
expect(frameRequests.length).toBe(1)
|
||||
frameRequests[0]()
|
||||
expect(events).toEqual(['write 4', 'read 3'])
|
||||
})
|
||||
|
||||
it('performs writes requested from read callbacks in the same animation frame', () => {
|
||||
spyOn(window, 'setInterval').andCallFake(fakeSetInterval)
|
||||
spyOn(window, 'clearInterval').andCallFake(fakeClearInterval)
|
||||
const events = []
|
||||
|
||||
registry.updateDocument(() => events.push('write 1'))
|
||||
registry.readDocument(() => {
|
||||
registry.updateDocument(() => events.push('write from read 1'))
|
||||
events.push('read 1')
|
||||
})
|
||||
registry.readDocument(() => {
|
||||
registry.updateDocument(() => events.push('write from read 2'))
|
||||
events.push('read 2')
|
||||
})
|
||||
registry.updateDocument(() => events.push('write 2'))
|
||||
|
||||
expect(frameRequests.length).toBe(1)
|
||||
frameRequests[0]()
|
||||
expect(frameRequests.length).toBe(1)
|
||||
|
||||
expect(events).toEqual([
|
||||
'write 1',
|
||||
'write 2',
|
||||
'read 1',
|
||||
'read 2',
|
||||
'write from read 1',
|
||||
'write from read 2'
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
describe('::getNextUpdatePromise()', () =>
|
||||
it('returns a promise that resolves at the end of the next update cycle', () => {
|
||||
let updateCalled = false
|
||||
let readCalled = false
|
||||
|
||||
waitsFor('getNextUpdatePromise to resolve', (done) => {
|
||||
registry.getNextUpdatePromise().then(() => {
|
||||
expect(updateCalled).toBe(true)
|
||||
expect(readCalled).toBe(true)
|
||||
done()
|
||||
})
|
||||
|
||||
registry.updateDocument(() => { updateCalled = true })
|
||||
registry.readDocument(() => { readCalled = true })
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
@ -1,201 +0,0 @@
|
||||
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.
|
||||
#
|
||||
# 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
|
||||
animationFrameRequest: null
|
||||
documentReadInProgress: false
|
||||
|
||||
constructor: (@atomEnvironment) ->
|
||||
@clear()
|
||||
|
||||
clear: ->
|
||||
@views = new WeakMap
|
||||
@providers = []
|
||||
@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.
|
||||
#
|
||||
# ## 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) ->
|
||||
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 typeof object?.getElement is 'function'
|
||||
element = object.getElement()
|
||||
if element instanceof HTMLElement
|
||||
return element
|
||||
|
||||
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
|
||||
|
||||
getNextUpdatePromise: ->
|
||||
@nextUpdatePromise ?= new Promise (resolve) =>
|
||||
@resolveNextUpdatePromise = resolve
|
||||
|
||||
clearDocumentRequests: ->
|
||||
@documentReaders = []
|
||||
@documentWriters = []
|
||||
@nextUpdatePromise = null
|
||||
@resolveNextUpdatePromise = null
|
||||
if @animationFrameRequest?
|
||||
cancelAnimationFrame(@animationFrameRequest)
|
||||
@animationFrameRequest = null
|
||||
|
||||
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()
|
||||
@documentReadInProgress = false
|
||||
|
||||
# process updates requested as a result of reads
|
||||
writer() while writer = @documentWriters.shift()
|
||||
|
||||
resolveNextUpdatePromise?()
|
249
src/view-registry.js
Normal file
249
src/view-registry.js
Normal file
@ -0,0 +1,249 @@
|
||||
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
|
||||
|
||||
let writer
|
||||
while ((writer = this.documentWriters.shift())) { writer() }
|
||||
|
||||
let reader
|
||||
this.documentReadInProgress = true
|
||||
while ((reader = this.documentReaders.shift())) { reader() }
|
||||
this.documentReadInProgress = false
|
||||
|
||||
// process updates requested as a result of reads
|
||||
while ((writer = this.documentWriters.shift())) { writer() }
|
||||
|
||||
if (resolveNextUpdatePromise) { resolveNextUpdatePromise() }
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user