-
Notifications
You must be signed in to change notification settings - Fork 22
/
Copy pathLiveEditorLocalDriver.js
326 lines (275 loc) · 10.7 KB
/
LiveEditorLocalDriver.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
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
/*
* Copyright (c) 2013 Adobe Systems Incorporated.
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*/
/*jslint vars: true, plusplus: true, devel: true, nomen: true, regexp: true, indent: 4, maxerr: 50 */
/*global define, $, brackets, window, _reset: false, _reconnect: false, update: false */
define(function (require, exports, module) {
"use strict";
var Inspector = brackets.getModule("LiveDevelopment/Inspector/Inspector"),
_ = brackets.getModule("thirdparty/lodash");
var LiveEditorRemoteDriver = require("text!LiveEditorRemoteDriver.js"),
/** @type {string} namspace in the inspected page where live editor methods live */
_namespace = "window._LD_CSS_EDITOR",
/** @type {Object} snapshot of remote model from live editor in the inspected page (live preivew) */
_model = {},
/** @type {boolean} true if live editor instance was set up */
_hasEditor = false,
/** @type {number} milliseconds interval after which to sync the remote model with the local _model snapshot */
_syncFrequency = 100,
/** @type {Interval} result of setInterval() */
_syncInterval,
/** @type {number} number of attempts to reconnect after an error */
_retryCount = 5,
/** @type {Object} misc storage; used in reconnect scenario */
_cache = {};
/**
* @private
* Evaluate the given expression in the context of the pave in LivePreview.
* Returns a promise.
* Fails the promise if the inspector is not connected.
* Fails the promise if an error was raised in the LivePreview.
*
* @param {!string} expression JavaScript code to be evaluated
* @return {$.Promise}
*/
function _call(expression) {
var deferred = $.Deferred();
if (!expression || typeof expression !== "string") {
throw new TypeError("Invalid input. Expected string JS expression, got: " + expression);
}
if (Inspector.connected() !== true) {
return deferred.reject();
}
Inspector.Runtime.evaluate(expression, function (resp) {
if (!resp || resp.wasThrown) {
console.error(resp.result);
deferred.reject(resp.result);
} else {
deferred.resolve(resp.result);
}
});
return deferred.promise();
}
/**
* Inject remote live editor driver and any specified editor providers.
* The remote live editor driver mirrors most of the local live editor driver API
* to provide an interface to the in-browser live editor.
* @param {Array.<string>=} providers String sources of editors to be available in the browser; optional
*/
function init(providers) {
var scripts = [].concat(LiveEditorRemoteDriver, providers || []);
// cache dependencies for reuse when a re-init is required (ex: after a page refresh)
_cache.dependencies = scripts;
$(exports).triggerHandler("init");
return _call(scripts.join(";"));
}
/**
* Send instructions to remove the live editor from the page in LivePreview.
* @return {$.Promise}
*/
function remove() {
if (_hasEditor === false) {
var deferred = $.Deferred();
return deferred.reject().promise();
}
_cache.model = undefined; // do not move in _reset(), otherwise the _reconnect() scenario misses the cache and fails
_reset();
var expr = _namespace + ".remove()";
return _call(expr);
}
/**
* @private
* Handle the succesful promise of getting the model from the browser.
*
* Dispatches these events:
* update.model -- when the model received differs from the local snapshot
*
* @throws {TypeError} if the promise result is not a string.
* @param {!string} response JSON stringified object with CSS property, value
* @return {$.Promise}
*/
function _whenGetRemoteModel(response) {
if (!response || !response.value || typeof response.value !== "string") {
throw new TypeError("Invalid result from remote driver .getModel(). Expected JSON string, got:" + response.value);
}
var data = JSON.parse(response.value),
deferred = $.Deferred(),
hasChanged = false,
key;
if (!data) {
remove();
return deferred.reject().promise();
}
// sync the local model snapshot with the remote model
_.forEach(data, function (value, key) {
if (!_model[key] || !_.isEqual(_model[key], value)) {
_model[key] = value;
hasChanged = true;
}
});
// notify Brackets so it can update the code editor
if (hasChanged || data.forceUpdate) {
$(exports).triggerHandler("update.model", [_model, data.forceUpdate]);
}
return deferred.promise();
}
/**
* @private
* Handle failed promises for eval() calls to the inspected page.
* Promises fail if the user manually refreshes the page or navigates
* because the injected editor files will be lost.
* If this is the case, attempt to reconnect.
*
* Promises also fail because of errors thrown in the remote page.
* If this is the case, remove the editor.
*/
function _whenRemoteCallFailed() {
// check if the remote editor namespace is still defined on the page
_call(_namespace)
.then(function (response) {
if (response.type === "undefined") {
_reconnect();
} else {
remove();
}
})
.fail(remove);
}
/**
* @private
* Stop polling for the remote model
*/
function _stopSyncLoop() {
window.clearInterval(_syncInterval);
}
/**
* @private
* Reset flags and clear snapshot of remote model
*/
function _reset() {
_stopSyncLoop();
_hasEditor = false;
_model = {};
}
/**
* @private
* Attempt to get the model from the page in LivePreview.
*/
function _onSyncTick() {
var expr = _namespace + ".getModel()";
_call(expr).then(_whenGetRemoteModel).fail(_whenRemoteCallFailed);
}
/**
* @private
* Poll for the remote model
*/
function _startSyncLoop() {
_syncInterval = window.setInterval(_onSyncTick, _syncFrequency);
}
/**
* Send instructions to setup a live editor in the page in LivePreview
* using the selector, css property and css value in the given model.
*
* If an editor for the current model already exists, then update it.
* The model here is an instance of Model, not an object literal, like the local _model.
*
* @param {!Model} model Instance of Model with attributes from code editor
* @return {$.Promise}
*/
function setup(model) {
_cache.model = _cache.model || model;
var attr = {
selector: model.get("selector"),
value: model.get("value"),
property: model.get("property")
};
if (_hasEditor) {
// If we are asked to re-setup the same editor, update the existing one
if (attr.selector === _model.selector && attr.property === _model.property) {
return update(model);
}
}
var expr = _namespace + ".setup(" + JSON.stringify(attr) + ")";
return _call(expr)
.then(_startSyncLoop)
.then(function () { _hasEditor = true; })
.fail(_whenRemoteCallFailed);
}
/**
* Send instructions to update the existing live editor in
* the page in LivePreview with the state of the given model.
*
* The model here is an instance of Model, not an object literal, like _model.
*
* @throws {TypeError} if the input model is falsy.
* @param {!Model} model Instance of Model obj with attributes from code editor.
* @return {$.Promise}
*/
function update(model) {
if (!model) {
throw new TypeError("Invalid update() input. Expected {Model} instance, got: " + model);
}
if (_hasEditor === false) {
return setup(model);
}
_cache.model = model;
var attr = {
selector: model.get("selector"),
value: model.get("value"),
property: model.get("property")
};
// Asking to update a different element / property? Setup a new editor
if (attr.selector !== _model.selector || attr.property !== _model.property) {
return remove().then(function () { return setup(model); });
}
var expr = _namespace + ".update(" + JSON.stringify(attr) + ")";
return _call(expr).fail(_whenRemoteCallFailed);
}
/**
* @private
* When a user refreshes the live preview window, the injected live editor
* and its dependecies get lost.
*
* This method attempts to re-inject them. It tries
* a number of times before giving up.
*
* After a successful reconnect, it sets up the editor in the last cached state.
*
* @return {$.Promise}
*/
function _reconnect() {
var deferred = $.Deferred();
function onPostInit() {
_reset();
setup(_cache.model);
_retryCount = 5;
}
if (_retryCount === 0) {
return deferred.reject();
}
_retryCount--;
return init(_cache.dependencies).then(onPostInit);
}
exports.init = init;
exports.setup = setup;
exports.update = update;
exports.remove = remove;
});