From cdf3be846be712d79ad901de5a531cc79e8a0bcc Mon Sep 17 00:00:00 2001 From: Jason Rudolph Date: Fri, 20 Oct 2017 20:35:40 -0400 Subject: [PATCH] =?UTF-8?q?=E2=98=A0=E2=98=95=20Decaffeinate=20src/view-re?= =?UTF-8?q?gistry.coffee?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/view-registry.coffee | 201 ------------------------------- src/view-registry.js | 253 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 253 insertions(+), 201 deletions(-) delete mode 100644 src/view-registry.coffee create mode 100644 src/view-registry.js diff --git a/src/view-registry.coffee b/src/view-registry.coffee deleted file mode 100644 index f300cc031..000000000 --- a/src/view-registry.coffee +++ /dev/null @@ -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?() diff --git a/src/view-registry.js b/src/view-registry.js new file mode 100644 index 000000000..d3167cdc1 --- /dev/null +++ b/src/view-registry.js @@ -0,0 +1,253 @@ +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 + if (view = this.views.get(object)) { + return view + } else { + 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] + } + + let viewConstructor + for (let provider of this.providers) { + if (provider.modelConstructor === AnyConstructor) { + if (element = provider.createView(object, this.atomEnvironment)) { + return element + } + continue + } + + if (object instanceof provider.modelConstructor) { + if (element = provider.createView && provider.createView(object, this.atomEnvironment)) { + return element + } + + if (viewConstructor = provider.viewConstructor) { + element = new viewConstructor() + if (element.initialize) { + element.initialize(object) + } else if (element.setModel) { + element.setModel(object) + } + return element + } + } + } + + if (object && object.getViewClass) { + 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() } + } +}