forked from atom/atom
-
Notifications
You must be signed in to change notification settings - Fork 31
/
Copy pathview-registry.js
287 lines (259 loc) · 8.83 KB
/
view-registry.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
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 = this.documentWriters.shift();
while (writer) {
writer();
writer = this.documentWriters.shift();
}
let 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();
}
}
};