|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
|
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 "use strict"; |
|
5 |
|
6 const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components; |
|
7 |
|
8 Cu.import("resource://gre/modules/Services.jsm"); |
|
9 Cu.import("resource://gre/modules/XPCOMUtils.jsm"); |
|
10 Cu.import("resource://gre/modules/Task.jsm"); |
|
11 Cu.import("resource:///modules/devtools/SideMenuWidget.jsm"); |
|
12 Cu.import("resource:///modules/devtools/ViewHelpers.jsm"); |
|
13 |
|
14 const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require; |
|
15 const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise; |
|
16 const EventEmitter = require("devtools/toolkit/event-emitter"); |
|
17 const {Tooltip} = require("devtools/shared/widgets/Tooltip"); |
|
18 const Editor = require("devtools/sourceeditor/editor"); |
|
19 |
|
20 // The panel's window global is an EventEmitter firing the following events: |
|
21 const EVENTS = { |
|
22 // When new programs are received from the server. |
|
23 NEW_PROGRAM: "ShaderEditor:NewProgram", |
|
24 PROGRAMS_ADDED: "ShaderEditor:ProgramsAdded", |
|
25 |
|
26 // When the vertex and fragment sources were shown in the editor. |
|
27 SOURCES_SHOWN: "ShaderEditor:SourcesShown", |
|
28 |
|
29 // When a shader's source was edited and compiled via the editor. |
|
30 SHADER_COMPILED: "ShaderEditor:ShaderCompiled", |
|
31 |
|
32 // When the UI is reset from tab navigation |
|
33 UI_RESET: "ShaderEditor:UIReset", |
|
34 |
|
35 // When the editor's error markers are all removed |
|
36 EDITOR_ERROR_MARKERS_REMOVED: "ShaderEditor:EditorCleaned" |
|
37 }; |
|
38 |
|
39 const STRINGS_URI = "chrome://browser/locale/devtools/shadereditor.properties" |
|
40 const HIGHLIGHT_TINT = [1, 0, 0.25, 1]; // rgba |
|
41 const TYPING_MAX_DELAY = 500; // ms |
|
42 const SHADERS_AUTOGROW_ITEMS = 4; |
|
43 const GUTTER_ERROR_PANEL_OFFSET_X = 7; // px |
|
44 const GUTTER_ERROR_PANEL_DELAY = 100; // ms |
|
45 const DEFAULT_EDITOR_CONFIG = { |
|
46 gutters: ["errors"], |
|
47 lineNumbers: true, |
|
48 showAnnotationRuler: true |
|
49 }; |
|
50 |
|
51 /** |
|
52 * The current target and the WebGL Editor front, set by this tool's host. |
|
53 */ |
|
54 let gToolbox, gTarget, gFront; |
|
55 |
|
56 /** |
|
57 * Initializes the shader editor controller and views. |
|
58 */ |
|
59 function startupShaderEditor() { |
|
60 return promise.all([ |
|
61 EventsHandler.initialize(), |
|
62 ShadersListView.initialize(), |
|
63 ShadersEditorsView.initialize() |
|
64 ]); |
|
65 } |
|
66 |
|
67 /** |
|
68 * Destroys the shader editor controller and views. |
|
69 */ |
|
70 function shutdownShaderEditor() { |
|
71 return promise.all([ |
|
72 EventsHandler.destroy(), |
|
73 ShadersListView.destroy(), |
|
74 ShadersEditorsView.destroy() |
|
75 ]); |
|
76 } |
|
77 |
|
78 /** |
|
79 * Functions handling target-related lifetime events. |
|
80 */ |
|
81 let EventsHandler = { |
|
82 /** |
|
83 * Listen for events emitted by the current tab target. |
|
84 */ |
|
85 initialize: function() { |
|
86 this._onHostChanged = this._onHostChanged.bind(this); |
|
87 this._onTabNavigated = this._onTabNavigated.bind(this); |
|
88 this._onProgramLinked = this._onProgramLinked.bind(this); |
|
89 this._onProgramsAdded = this._onProgramsAdded.bind(this); |
|
90 gToolbox.on("host-changed", this._onHostChanged); |
|
91 gTarget.on("will-navigate", this._onTabNavigated); |
|
92 gTarget.on("navigate", this._onTabNavigated); |
|
93 gFront.on("program-linked", this._onProgramLinked); |
|
94 |
|
95 }, |
|
96 |
|
97 /** |
|
98 * Remove events emitted by the current tab target. |
|
99 */ |
|
100 destroy: function() { |
|
101 gToolbox.off("host-changed", this._onHostChanged); |
|
102 gTarget.off("will-navigate", this._onTabNavigated); |
|
103 gTarget.off("navigate", this._onTabNavigated); |
|
104 gFront.off("program-linked", this._onProgramLinked); |
|
105 }, |
|
106 |
|
107 /** |
|
108 * Handles a host change event on the parent toolbox. |
|
109 */ |
|
110 _onHostChanged: function() { |
|
111 if (gToolbox.hostType == "side") { |
|
112 $("#shaders-pane").removeAttribute("height"); |
|
113 } |
|
114 }, |
|
115 |
|
116 /** |
|
117 * Called for each location change in the debugged tab. |
|
118 */ |
|
119 _onTabNavigated: function(event) { |
|
120 switch (event) { |
|
121 case "will-navigate": { |
|
122 Task.spawn(function() { |
|
123 // Make sure the backend is prepared to handle WebGL contexts. |
|
124 gFront.setup({ reload: false }); |
|
125 |
|
126 // Reset UI. |
|
127 ShadersListView.empty(); |
|
128 $("#reload-notice").hidden = true; |
|
129 $("#waiting-notice").hidden = false; |
|
130 yield ShadersEditorsView.setText({ vs: "", fs: "" }); |
|
131 $("#content").hidden = true; |
|
132 }).then(() => window.emit(EVENTS.UI_RESET)); |
|
133 break; |
|
134 } |
|
135 case "navigate": { |
|
136 // Manually retrieve the list of program actors known to the server, |
|
137 // because the backend won't emit "program-linked" notifications |
|
138 // in the case of a bfcache navigation (since no new programs are |
|
139 // actually linked). |
|
140 gFront.getPrograms().then(this._onProgramsAdded); |
|
141 break; |
|
142 } |
|
143 } |
|
144 }, |
|
145 |
|
146 /** |
|
147 * Called every time a program was linked in the debugged tab. |
|
148 */ |
|
149 _onProgramLinked: function(programActor) { |
|
150 this._addProgram(programActor); |
|
151 window.emit(EVENTS.NEW_PROGRAM); |
|
152 }, |
|
153 |
|
154 /** |
|
155 * Callback for the front's getPrograms() method. |
|
156 */ |
|
157 _onProgramsAdded: function(programActors) { |
|
158 programActors.forEach(this._addProgram); |
|
159 window.emit(EVENTS.PROGRAMS_ADDED); |
|
160 }, |
|
161 |
|
162 /** |
|
163 * Adds a program to the shaders list and unhides any modal notices. |
|
164 */ |
|
165 _addProgram: function(programActor) { |
|
166 $("#waiting-notice").hidden = true; |
|
167 $("#reload-notice").hidden = true; |
|
168 $("#content").hidden = false; |
|
169 ShadersListView.addProgram(programActor); |
|
170 } |
|
171 }; |
|
172 |
|
173 /** |
|
174 * Functions handling the sources UI. |
|
175 */ |
|
176 let ShadersListView = Heritage.extend(WidgetMethods, { |
|
177 /** |
|
178 * Initialization function, called when the tool is started. |
|
179 */ |
|
180 initialize: function() { |
|
181 this.widget = new SideMenuWidget(this._pane = $("#shaders-pane"), { |
|
182 showArrows: true, |
|
183 showItemCheckboxes: true |
|
184 }); |
|
185 |
|
186 this._onProgramSelect = this._onProgramSelect.bind(this); |
|
187 this._onProgramCheck = this._onProgramCheck.bind(this); |
|
188 this._onProgramMouseEnter = this._onProgramMouseEnter.bind(this); |
|
189 this._onProgramMouseLeave = this._onProgramMouseLeave.bind(this); |
|
190 |
|
191 this.widget.addEventListener("select", this._onProgramSelect, false); |
|
192 this.widget.addEventListener("check", this._onProgramCheck, false); |
|
193 this.widget.addEventListener("mouseenter", this._onProgramMouseEnter, true); |
|
194 this.widget.addEventListener("mouseleave", this._onProgramMouseLeave, true); |
|
195 }, |
|
196 |
|
197 /** |
|
198 * Destruction function, called when the tool is closed. |
|
199 */ |
|
200 destroy: function() { |
|
201 this.widget.removeEventListener("select", this._onProgramSelect, false); |
|
202 this.widget.removeEventListener("check", this._onProgramCheck, false); |
|
203 this.widget.removeEventListener("mouseenter", this._onProgramMouseEnter, true); |
|
204 this.widget.removeEventListener("mouseleave", this._onProgramMouseLeave, true); |
|
205 }, |
|
206 |
|
207 /** |
|
208 * Adds a program to this programs container. |
|
209 * |
|
210 * @param object programActor |
|
211 * The program actor coming from the active thread. |
|
212 */ |
|
213 addProgram: function(programActor) { |
|
214 if (this.hasProgram(programActor)) { |
|
215 return; |
|
216 } |
|
217 |
|
218 // Currently, there's no good way of differentiating between programs |
|
219 // in a way that helps humans. It will be a good idea to implement a |
|
220 // standard of allowing debuggees to add some identifiable metadata to their |
|
221 // program sources or instances. |
|
222 let label = L10N.getFormatStr("shadersList.programLabel", this.itemCount); |
|
223 let contents = document.createElement("label"); |
|
224 contents.className = "plain program-item"; |
|
225 contents.setAttribute("value", label); |
|
226 contents.setAttribute("crop", "start"); |
|
227 contents.setAttribute("flex", "1"); |
|
228 |
|
229 // Append a program item to this container. |
|
230 this.push([contents], { |
|
231 index: -1, /* specifies on which position should the item be appended */ |
|
232 attachment: { |
|
233 label: label, |
|
234 programActor: programActor, |
|
235 checkboxState: true, |
|
236 checkboxTooltip: L10N.getStr("shadersList.blackboxLabel") |
|
237 } |
|
238 }); |
|
239 |
|
240 // Make sure there's always a selected item available. |
|
241 if (!this.selectedItem) { |
|
242 this.selectedIndex = 0; |
|
243 } |
|
244 |
|
245 // Prevent this container from growing indefinitely in height when the |
|
246 // toolbox is docked to the side. |
|
247 if (gToolbox.hostType == "side" && this.itemCount == SHADERS_AUTOGROW_ITEMS) { |
|
248 this._pane.setAttribute("height", this._pane.getBoundingClientRect().height); |
|
249 } |
|
250 }, |
|
251 |
|
252 /** |
|
253 * Returns whether a program was already added to this programs container. |
|
254 * |
|
255 * @param object programActor |
|
256 * The program actor coming from the active thread. |
|
257 * @param boolean |
|
258 * True if the program was added, false otherwise. |
|
259 */ |
|
260 hasProgram: function(programActor) { |
|
261 return !!this.attachments.filter(e => e.programActor == programActor).length; |
|
262 }, |
|
263 |
|
264 /** |
|
265 * The select listener for the programs container. |
|
266 */ |
|
267 _onProgramSelect: function({ detail: sourceItem }) { |
|
268 if (!sourceItem) { |
|
269 return; |
|
270 } |
|
271 // The container is not empty and an actual item was selected. |
|
272 let attachment = sourceItem.attachment; |
|
273 |
|
274 function getShaders() { |
|
275 return promise.all([ |
|
276 attachment.vs || (attachment.vs = attachment.programActor.getVertexShader()), |
|
277 attachment.fs || (attachment.fs = attachment.programActor.getFragmentShader()) |
|
278 ]); |
|
279 } |
|
280 function getSources([vertexShaderActor, fragmentShaderActor]) { |
|
281 return promise.all([ |
|
282 vertexShaderActor.getText(), |
|
283 fragmentShaderActor.getText() |
|
284 ]); |
|
285 } |
|
286 function showSources([vertexShaderText, fragmentShaderText]) { |
|
287 return ShadersEditorsView.setText({ |
|
288 vs: vertexShaderText, |
|
289 fs: fragmentShaderText |
|
290 }); |
|
291 } |
|
292 |
|
293 getShaders() |
|
294 .then(getSources) |
|
295 .then(showSources) |
|
296 .then(null, Cu.reportError); |
|
297 }, |
|
298 |
|
299 /** |
|
300 * The check listener for the programs container. |
|
301 */ |
|
302 _onProgramCheck: function({ detail: { checked }, target }) { |
|
303 let sourceItem = this.getItemForElement(target); |
|
304 let attachment = sourceItem.attachment; |
|
305 attachment.isBlackBoxed = !checked; |
|
306 attachment.programActor[checked ? "unblackbox" : "blackbox"](); |
|
307 }, |
|
308 |
|
309 /** |
|
310 * The mouseenter listener for the programs container. |
|
311 */ |
|
312 _onProgramMouseEnter: function(e) { |
|
313 let sourceItem = this.getItemForElement(e.target, { noSiblings: true }); |
|
314 if (sourceItem && !sourceItem.attachment.isBlackBoxed) { |
|
315 sourceItem.attachment.programActor.highlight(HIGHLIGHT_TINT); |
|
316 |
|
317 if (e instanceof Event) { |
|
318 e.preventDefault(); |
|
319 e.stopPropagation(); |
|
320 } |
|
321 } |
|
322 }, |
|
323 |
|
324 /** |
|
325 * The mouseleave listener for the programs container. |
|
326 */ |
|
327 _onProgramMouseLeave: function(e) { |
|
328 let sourceItem = this.getItemForElement(e.target, { noSiblings: true }); |
|
329 if (sourceItem && !sourceItem.attachment.isBlackBoxed) { |
|
330 sourceItem.attachment.programActor.unhighlight(); |
|
331 |
|
332 if (e instanceof Event) { |
|
333 e.preventDefault(); |
|
334 e.stopPropagation(); |
|
335 } |
|
336 } |
|
337 } |
|
338 }); |
|
339 |
|
340 /** |
|
341 * Functions handling the editors displaying the vertex and fragment shaders. |
|
342 */ |
|
343 let ShadersEditorsView = { |
|
344 /** |
|
345 * Initialization function, called when the tool is started. |
|
346 */ |
|
347 initialize: function() { |
|
348 XPCOMUtils.defineLazyGetter(this, "_editorPromises", () => new Map()); |
|
349 this._vsFocused = this._onFocused.bind(this, "vs", "fs"); |
|
350 this._fsFocused = this._onFocused.bind(this, "fs", "vs"); |
|
351 this._vsChanged = this._onChanged.bind(this, "vs"); |
|
352 this._fsChanged = this._onChanged.bind(this, "fs"); |
|
353 }, |
|
354 |
|
355 /** |
|
356 * Destruction function, called when the tool is closed. |
|
357 */ |
|
358 destroy: function() { |
|
359 this._toggleListeners("off"); |
|
360 }, |
|
361 |
|
362 /** |
|
363 * Sets the text displayed in the vertex and fragment shader editors. |
|
364 * |
|
365 * @param object sources |
|
366 * An object containing the following properties |
|
367 * - vs: the vertex shader source code |
|
368 * - fs: the fragment shader source code |
|
369 * @return object |
|
370 * A promise resolving upon completion of text setting. |
|
371 */ |
|
372 setText: function(sources) { |
|
373 let view = this; |
|
374 function setTextAndClearHistory(editor, text) { |
|
375 editor.setText(text); |
|
376 editor.clearHistory(); |
|
377 } |
|
378 |
|
379 return Task.spawn(function() { |
|
380 yield view._toggleListeners("off"); |
|
381 yield promise.all([ |
|
382 view._getEditor("vs").then(e => setTextAndClearHistory(e, sources.vs)), |
|
383 view._getEditor("fs").then(e => setTextAndClearHistory(e, sources.fs)) |
|
384 ]); |
|
385 yield view._toggleListeners("on"); |
|
386 }).then(() => window.emit(EVENTS.SOURCES_SHOWN, sources)); |
|
387 }, |
|
388 |
|
389 /** |
|
390 * Lazily initializes and returns a promise for an Editor instance. |
|
391 * |
|
392 * @param string type |
|
393 * Specifies for which shader type should an editor be retrieved, |
|
394 * either are "vs" for a vertex, or "fs" for a fragment shader. |
|
395 * @return object |
|
396 * Returns a promise that resolves to an editor instance |
|
397 */ |
|
398 _getEditor: function(type) { |
|
399 if ($("#content").hidden) { |
|
400 return promise.reject(new Error("Shader Editor is still waiting for a WebGL context to be created.")); |
|
401 } |
|
402 if (this._editorPromises.has(type)) { |
|
403 return this._editorPromises.get(type); |
|
404 } |
|
405 |
|
406 let deferred = promise.defer(); |
|
407 this._editorPromises.set(type, deferred.promise); |
|
408 |
|
409 // Initialize the source editor and store the newly created instance |
|
410 // in the ether of a resolved promise's value. |
|
411 let parent = $("#" + type +"-editor"); |
|
412 let editor = new Editor(DEFAULT_EDITOR_CONFIG); |
|
413 editor.config.mode = Editor.modes[type]; |
|
414 editor.appendTo(parent).then(() => deferred.resolve(editor)); |
|
415 |
|
416 return deferred.promise; |
|
417 }, |
|
418 |
|
419 /** |
|
420 * Toggles all the event listeners for the editors either on or off. |
|
421 * |
|
422 * @param string flag |
|
423 * Either "on" to enable the event listeners, "off" to disable them. |
|
424 * @return object |
|
425 * A promise resolving upon completion of toggling the listeners. |
|
426 */ |
|
427 _toggleListeners: function(flag) { |
|
428 return promise.all(["vs", "fs"].map(type => { |
|
429 return this._getEditor(type).then(editor => { |
|
430 editor[flag]("focus", this["_" + type + "Focused"]); |
|
431 editor[flag]("change", this["_" + type + "Changed"]); |
|
432 }); |
|
433 })); |
|
434 }, |
|
435 |
|
436 /** |
|
437 * The focus listener for a source editor. |
|
438 * |
|
439 * @param string focused |
|
440 * The corresponding shader type for the focused editor (e.g. "vs"). |
|
441 * @param string focused |
|
442 * The corresponding shader type for the other editor (e.g. "fs"). |
|
443 */ |
|
444 _onFocused: function(focused, unfocused) { |
|
445 $("#" + focused + "-editor-label").setAttribute("selected", ""); |
|
446 $("#" + unfocused + "-editor-label").removeAttribute("selected"); |
|
447 }, |
|
448 |
|
449 /** |
|
450 * The change listener for a source editor. |
|
451 * |
|
452 * @param string type |
|
453 * The corresponding shader type for the focused editor (e.g. "vs"). |
|
454 */ |
|
455 _onChanged: function(type) { |
|
456 setNamedTimeout("gl-typed", TYPING_MAX_DELAY, () => this._doCompile(type)); |
|
457 |
|
458 // Remove all the gutter markers and line classes from the editor. |
|
459 this._cleanEditor(type); |
|
460 }, |
|
461 |
|
462 /** |
|
463 * Recompiles the source code for the shader being edited. |
|
464 * This function is fired at a certain delay after the user stops typing. |
|
465 * |
|
466 * @param string type |
|
467 * The corresponding shader type for the focused editor (e.g. "vs"). |
|
468 */ |
|
469 _doCompile: function(type) { |
|
470 Task.spawn(function() { |
|
471 let editor = yield this._getEditor(type); |
|
472 let shaderActor = yield ShadersListView.selectedAttachment[type]; |
|
473 |
|
474 try { |
|
475 yield shaderActor.compile(editor.getText()); |
|
476 this._onSuccessfulCompilation(); |
|
477 } catch (e) { |
|
478 this._onFailedCompilation(type, editor, e); |
|
479 } |
|
480 }.bind(this)); |
|
481 }, |
|
482 |
|
483 /** |
|
484 * Called uppon a successful shader compilation. |
|
485 */ |
|
486 _onSuccessfulCompilation: function() { |
|
487 // Signal that the shader was compiled successfully. |
|
488 window.emit(EVENTS.SHADER_COMPILED, null); |
|
489 }, |
|
490 |
|
491 /** |
|
492 * Called uppon an unsuccessful shader compilation. |
|
493 */ |
|
494 _onFailedCompilation: function(type, editor, errors) { |
|
495 let lineCount = editor.lineCount(); |
|
496 let currentLine = editor.getCursor().line; |
|
497 let listeners = { mouseenter: this._onMarkerMouseEnter }; |
|
498 |
|
499 function matchLinesAndMessages(string) { |
|
500 return { |
|
501 // First number that is not equal to 0. |
|
502 lineMatch: string.match(/\d{2,}|[1-9]/), |
|
503 // The string after all the numbers, semicolons and spaces. |
|
504 textMatch: string.match(/[^\s\d:][^\r\n|]*/) |
|
505 }; |
|
506 } |
|
507 function discardInvalidMatches(e) { |
|
508 // Discard empty line and text matches. |
|
509 return e.lineMatch && e.textMatch; |
|
510 } |
|
511 function sanitizeValidMatches(e) { |
|
512 return { |
|
513 // Drivers might yield confusing line numbers under some obscure |
|
514 // circumstances. Don't throw the errors away in those cases, |
|
515 // just display them on the currently edited line. |
|
516 line: e.lineMatch[0] > lineCount ? currentLine : e.lineMatch[0] - 1, |
|
517 // Trim whitespace from the beginning and the end of the message, |
|
518 // and replace all other occurences of double spaces to a single space. |
|
519 text: e.textMatch[0].trim().replace(/\s{2,}/g, " ") |
|
520 }; |
|
521 } |
|
522 function sortByLine(first, second) { |
|
523 // Sort all the errors ascending by their corresponding line number. |
|
524 return first.line > second.line ? 1 : -1; |
|
525 } |
|
526 function groupSameLineMessages(accumulator, current) { |
|
527 // Group errors corresponding to the same line number to a single object. |
|
528 let previous = accumulator[accumulator.length - 1]; |
|
529 if (!previous || previous.line != current.line) { |
|
530 return [...accumulator, { |
|
531 line: current.line, |
|
532 messages: [current.text] |
|
533 }]; |
|
534 } else { |
|
535 previous.messages.push(current.text); |
|
536 return accumulator; |
|
537 } |
|
538 } |
|
539 function displayErrors({ line, messages }) { |
|
540 // Add gutter markers and line classes for every error in the source. |
|
541 editor.addMarker(line, "errors", "error"); |
|
542 editor.setMarkerListeners(line, "errors", "error", listeners, messages); |
|
543 editor.addLineClass(line, "error-line"); |
|
544 } |
|
545 |
|
546 (this._errors[type] = errors.link |
|
547 .split("ERROR") |
|
548 .map(matchLinesAndMessages) |
|
549 .filter(discardInvalidMatches) |
|
550 .map(sanitizeValidMatches) |
|
551 .sort(sortByLine) |
|
552 .reduce(groupSameLineMessages, [])) |
|
553 .forEach(displayErrors); |
|
554 |
|
555 // Signal that the shader wasn't compiled successfully. |
|
556 window.emit(EVENTS.SHADER_COMPILED, errors); |
|
557 }, |
|
558 |
|
559 /** |
|
560 * Event listener for the 'mouseenter' event on a marker in the editor gutter. |
|
561 */ |
|
562 _onMarkerMouseEnter: function(line, node, messages) { |
|
563 if (node._markerErrorsTooltip) { |
|
564 return; |
|
565 } |
|
566 |
|
567 let tooltip = node._markerErrorsTooltip = new Tooltip(document); |
|
568 tooltip.defaultOffsetX = GUTTER_ERROR_PANEL_OFFSET_X; |
|
569 tooltip.setTextContent({ messages: messages }); |
|
570 tooltip.startTogglingOnHover(node, () => true, GUTTER_ERROR_PANEL_DELAY); |
|
571 }, |
|
572 |
|
573 /** |
|
574 * Removes all the gutter markers and line classes from the editor. |
|
575 */ |
|
576 _cleanEditor: function(type) { |
|
577 this._getEditor(type).then(editor => { |
|
578 editor.removeAllMarkers("errors"); |
|
579 this._errors[type].forEach(e => editor.removeLineClass(e.line)); |
|
580 this._errors[type].length = 0; |
|
581 window.emit(EVENTS.EDITOR_ERROR_MARKERS_REMOVED); |
|
582 }); |
|
583 }, |
|
584 |
|
585 _errors: { |
|
586 vs: [], |
|
587 fs: [] |
|
588 } |
|
589 }; |
|
590 |
|
591 /** |
|
592 * Localization convenience methods. |
|
593 */ |
|
594 let L10N = new ViewHelpers.L10N(STRINGS_URI); |
|
595 |
|
596 /** |
|
597 * Convenient way of emitting events from the panel window. |
|
598 */ |
|
599 EventEmitter.decorate(this); |
|
600 |
|
601 /** |
|
602 * DOM query helper. |
|
603 */ |
|
604 function $(selector, target = document) target.querySelector(selector); |