2015-02-10 19:41:24 +00:00
|
|
|
define(function(require, module, exports) {
|
|
|
|
main.consumes = ["Plugin", "ui", "clipboard"];
|
|
|
|
main.provides = ["Editor"];
|
|
|
|
return main;
|
|
|
|
|
|
|
|
function main(options, imports, register) {
|
|
|
|
var Plugin = imports.Plugin;
|
|
|
|
var clipboard = imports.clipboard;
|
|
|
|
var ui = imports.ui;
|
|
|
|
|
|
|
|
function Editor(developer, deps, extensions) {
|
|
|
|
// Editor extends ext.Plugin
|
|
|
|
var plugin = new Plugin(developer, deps);
|
|
|
|
var emit = plugin.getEmitter();
|
|
|
|
emit.setMaxListeners(1000);
|
|
|
|
|
|
|
|
var amlTab, type, pane, activeDocument, focussed;
|
|
|
|
var meta = {};
|
|
|
|
|
|
|
|
/***** Methods *****/
|
|
|
|
|
|
|
|
function unloadDocument(doc, options) {
|
|
|
|
if (!options) options = {};
|
|
|
|
options.doc = doc;
|
|
|
|
emit("documentUnload", options);
|
|
|
|
doc.getSession().unload();
|
|
|
|
}
|
|
|
|
|
|
|
|
function loadDocument(doc) {
|
|
|
|
// Old Editor
|
|
|
|
var lastEditor = doc.editor;
|
|
|
|
if (lastEditor && lastEditor != plugin) {
|
|
|
|
lastEditor.unloadDocument(doc, {
|
|
|
|
toEditor: plugin,
|
|
|
|
fromEditor: lastEditor
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
activeDocument = doc;
|
|
|
|
|
|
|
|
// Initialize the Document in the Editor
|
|
|
|
// When the Document unloads the editor should clear all it's state
|
|
|
|
// When the Editor unloads it should clear all it's state
|
|
|
|
if (lastEditor != plugin) {
|
|
|
|
var editor = plugin;
|
|
|
|
doc.editor = plugin;
|
|
|
|
|
|
|
|
var session = doc.getSession();
|
|
|
|
var state = (doc.lastState || false)[editor.type]; // Retrieve last state
|
|
|
|
bufferEvent.call(plugin, "documentLoad", {
|
|
|
|
doc: doc,
|
|
|
|
state: state || {}
|
|
|
|
});
|
|
|
|
|
|
|
|
// Check if session was unloaded from the previous editor
|
|
|
|
if (!session.loaded)
|
|
|
|
session.load();
|
|
|
|
|
|
|
|
if (!doc.meta.$unloadEditor) {
|
|
|
|
doc.meta.$unloadEditor = true;
|
|
|
|
doc.on("unload", function(){
|
|
|
|
doc.editor.unloadDocument(doc);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
// editor.on("unload", function(){
|
|
|
|
// editor.unloadDocument(doc);
|
|
|
|
// }, session);
|
|
|
|
|
|
|
|
if (state)
|
|
|
|
plugin.setState(doc, state);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set Document Active in the Editor
|
|
|
|
bufferEvent.call(plugin, "documentActivate", { doc : doc });
|
|
|
|
}
|
|
|
|
|
|
|
|
function bufferEvent(name, event) {
|
|
|
|
var _self = plugin;
|
|
|
|
|
|
|
|
emit(name, event);
|
|
|
|
|
|
|
|
// Add new listeners
|
|
|
|
function listenForEvent(curName, listener) {
|
|
|
|
if (curName == name)
|
|
|
|
listener(event);
|
|
|
|
}
|
|
|
|
plugin.on("newListener", listenForEvent);
|
|
|
|
plugin.on("documentUnload", function callee(e) {
|
|
|
|
if (e.doc == event.doc) {
|
|
|
|
_self.off("newListener", listenForEvent);
|
|
|
|
_self.off("documentUnload", callee);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function getState(doc, filter) {
|
|
|
|
var state = {};
|
|
|
|
|
|
|
|
emit("getState", {
|
|
|
|
doc: doc,
|
|
|
|
state: state,
|
|
|
|
filter: filter || false
|
|
|
|
});
|
|
|
|
|
|
|
|
return state;
|
|
|
|
}
|
|
|
|
|
|
|
|
function setState(doc, state) {
|
|
|
|
emit("setState", {
|
|
|
|
doc: doc,
|
|
|
|
state: state || {}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function clear(){
|
|
|
|
emit("clear");
|
|
|
|
}
|
|
|
|
|
|
|
|
// Clipboard support
|
|
|
|
function isClipboardAvailable(e) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
function cut(e) {
|
|
|
|
emit("cut", e || { clipboardData: clipboard.clipboardData });
|
|
|
|
}
|
|
|
|
|
|
|
|
function copy(e) {
|
|
|
|
emit("copy", e || { clipboardData: clipboard.clipboardData });
|
|
|
|
}
|
|
|
|
|
|
|
|
function paste(e) {
|
|
|
|
emit("paste", e || { clipboardData: clipboard.clipboardData });
|
|
|
|
}
|
|
|
|
|
|
|
|
function focus(regain, lost) {
|
|
|
|
if (!lost && amlTab)
|
|
|
|
amlTab.focus(); //@todo this might break selenium editor
|
|
|
|
|
|
|
|
emit("focus", {
|
|
|
|
regain: regain || false,
|
|
|
|
lost: lost || false
|
|
|
|
});
|
|
|
|
|
|
|
|
focussed = lost ? 0 : true;
|
|
|
|
}
|
|
|
|
|
|
|
|
function blur(){
|
|
|
|
emit("blur");
|
|
|
|
focussed = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
function resize(e) {
|
|
|
|
if (pane && pane.visible)
|
|
|
|
emit("resize", e || {});
|
|
|
|
}
|
|
|
|
|
|
|
|
function isValid(doc, info) {
|
|
|
|
return emit("validate", {document: doc, info: info});
|
|
|
|
}
|
|
|
|
|
|
|
|
function attachTo(tb) {
|
|
|
|
if (amlTab && !amlTab.$amlDestroyed)
|
|
|
|
throw new Error("Editors should only be attached once");
|
|
|
|
|
|
|
|
//Create Tab Element
|
|
|
|
plugin.addElement(
|
|
|
|
amlTab = new ui.page({
|
|
|
|
id: "editor::" + plugin.type,
|
|
|
|
mimeTypes: extensions,
|
|
|
|
visible: false,
|
|
|
|
realtime: false,
|
|
|
|
focussable: true,
|
|
|
|
$focussable: true
|
|
|
|
})
|
|
|
|
);
|
|
|
|
|
|
|
|
// Remember which pane we belong too
|
|
|
|
pane = tb;
|
|
|
|
|
|
|
|
// Set the reference to us on the tab element
|
|
|
|
amlTab.editor = this;
|
|
|
|
|
|
|
|
// Insert our tab into the pane element
|
|
|
|
pane.aml.insertBefore(amlTab, pane.aml.getPage(0));
|
|
|
|
|
|
|
|
// Emit draw event
|
|
|
|
var event = { tab: amlTab, htmlNode: amlTab.$int };
|
|
|
|
emit.sticky("draw", event);
|
|
|
|
|
|
|
|
var editor = this;
|
|
|
|
pane.on("unload", function(){
|
|
|
|
editor.unload();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
function drawOn(htmlNode, pg) {
|
|
|
|
amlTab = pg;
|
|
|
|
|
|
|
|
// Emit draw event
|
|
|
|
var event = { htmlNode: htmlNode, tab: amlTab };
|
|
|
|
emit.sticky("draw", event);
|
|
|
|
}
|
|
|
|
|
|
|
|
/***** Register and define API *****/
|
|
|
|
|
2015-02-21 23:09:43 +00:00
|
|
|
// This is a base class
|
|
|
|
plugin.baseclass();
|
2015-02-10 19:41:24 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Editor base class for Cloud9 Editors. Each file that is opened
|
|
|
|
* in Cloud9 has an editor to display it's content. An editor can register
|
|
|
|
* itself for a set of file extensions. There are also editors that
|
|
|
|
* don't display files but have a different purpose. For instance
|
|
|
|
* the {@link terminal.Terminal Terminal} and
|
|
|
|
* {@link preferences.Preferences Preferences} panel.
|
|
|
|
*
|
|
|
|
* The editor relates to other objects as such:
|
|
|
|
*
|
|
|
|
* * {@link Pane} - Represent a single pane, housing multiple tabs
|
|
|
|
* * {@link Tab} - A single tab (button) in a pane
|
|
|
|
* * **Editor - The editor responsible for displaying the file in the tab**
|
|
|
|
* * {@link Document} - The representation of a file in the tab
|
|
|
|
* * {@link Session} - The session information of the editor
|
|
|
|
* * {@link UndoManager} - The object that manages the undo stack for this document
|
|
|
|
*
|
|
|
|
* Panes can live in certain areas of Cloud9. By default these areas are:
|
|
|
|
*
|
|
|
|
* * {@link panes} - The main area where editor panes are displayed
|
|
|
|
* * {@link console} - The console in the bottom of the screen
|
|
|
|
*
|
|
|
|
* Tabs are managed by the {@link tabManager}. The default way to
|
|
|
|
* open a new file in an editor uses the tabManager:
|
|
|
|
*
|
|
|
|
* tabManager.openFile("/file.js", true, function(err, tab) {
|
|
|
|
* var editor = tab.editor;
|
|
|
|
* editor.focus();
|
|
|
|
* });
|
|
|
|
*
|
|
|
|
* The event flow of an editor plugin is as follows:
|
|
|
|
*
|
|
|
|
* * {@link #event-documentLoad} - *A document is loaded in the editor*
|
|
|
|
* * {@link #event-documentActivate} - *A becomes active in the editor*
|
|
|
|
* * {@link #event-documentUnload} - *The document is unloaded in the editor*
|
|
|
|
*
|
|
|
|
* This is in addition to the event flow of the {@link Plugin} base class.
|
|
|
|
*
|
|
|
|
* #### User Actions:
|
|
|
|
*
|
|
|
|
* * {@link #event-draw} - *The editor is drawn*
|
|
|
|
* * {@link #event-focus} - *The editor receives focus*
|
|
|
|
* * {@link #event-blur} - *The editor lost focus*
|
|
|
|
* * {@link #event-clear} - *The editor's contents is cleared*
|
|
|
|
* * {@link #event-cut} - *The editor's selection is copied and deleted*
|
|
|
|
* * {@link #event-copy} - *The editor's selection is copied*
|
|
|
|
* * {@link #event-paste} - *The editor's selection is deleted and new content is pasted in*
|
|
|
|
* * {@link #event-resize} - *The editor is resized*
|
|
|
|
*
|
|
|
|
* Implementing your own editor takes a new Editor() object rather
|
|
|
|
* than a new Plugin() object. See the {@link texteditor.TextEditor TextEditor} for a
|
|
|
|
* full implementation of an editor. Here's a short example:
|
|
|
|
*
|
|
|
|
* function TextEditor(){
|
|
|
|
* var plugin = new Editor("(Company) Name", main.consumes, ["txt"]);
|
|
|
|
* var emit = plugin.getEmitter();
|
|
|
|
*
|
|
|
|
* plugin.freezePublicAPI({
|
|
|
|
* example: function(){
|
|
|
|
*
|
|
|
|
* }
|
|
|
|
* });
|
|
|
|
*
|
|
|
|
* plugin.load(null, "texteditor");
|
|
|
|
*
|
|
|
|
* return plugin;
|
|
|
|
* }
|
|
|
|
*
|
|
|
|
* @class Editor
|
|
|
|
* @extends Plugin
|
|
|
|
*/
|
|
|
|
/**
|
|
|
|
* @constructor
|
|
|
|
* Creates a new Editor instance.
|
|
|
|
* @param {String} developer The name of the developer of the plugin
|
|
|
|
* @param {String[]} deps A list of dependencies for this
|
|
|
|
* plugin. In most cases it's a reference to main.consumes.
|
|
|
|
* @param {String[]} extensions A list of file extension that this
|
|
|
|
* editor can handle.
|
|
|
|
*/
|
|
|
|
plugin.freezePublicAPI({
|
|
|
|
/**
|
|
|
|
* @ignore
|
|
|
|
*/
|
|
|
|
get aml(){ return amlTab; },
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The pane that this editor is a part of. Each editor is
|
|
|
|
* initialized in the context of a single pane and the editor
|
|
|
|
* is destroyed when the pane is destroyed.
|
|
|
|
* @property {Pane} pane
|
|
|
|
* @readonly
|
|
|
|
*/
|
|
|
|
get pane(){ return pane; },
|
|
|
|
|
|
|
|
/**
|
|
|
|
* The document that is displayed in this editor.
|
|
|
|
* @property {Document} activeDocument
|
|
|
|
* @readonly
|
|
|
|
*/
|
|
|
|
get activeDocument(){ return activeDocument; },
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @property {String} type the unique identifier for this editor
|
|
|
|
* @readonly
|
|
|
|
*/
|
|
|
|
get type(){ return type; },
|
|
|
|
set type(val) {
|
|
|
|
if (!type)
|
|
|
|
type = val;
|
|
|
|
else
|
|
|
|
throw new Error("Plugin Type Exception");
|
|
|
|
},
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @property {Object} meta
|
|
|
|
*/
|
|
|
|
get meta(){ return meta; },
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @property {String[]} fileExtensions Array of file extensions supported by this editor
|
|
|
|
* @readonly
|
|
|
|
*/
|
|
|
|
get fileExtensions(){ return extensions; },
|
|
|
|
|
|
|
|
_events: [
|
|
|
|
/**
|
|
|
|
* Fires when a document is loaded into the editor.
|
|
|
|
* This event is also fired when this document is attached to another
|
|
|
|
* instance of the same editor (in a split view situation). Often you
|
|
|
|
* want to keep the session information partially in tact when this
|
|
|
|
* happens.
|
|
|
|
* @event documentLoad
|
|
|
|
* @param {Object} e
|
|
|
|
* @param {Document} e.doc the document that is loaded into the editor
|
|
|
|
* @param {Object} e.state state that was saved in the document
|
|
|
|
*/
|
|
|
|
"documentLoad",
|
|
|
|
/**
|
|
|
|
* Fires when a document becomes the active document of an editor
|
|
|
|
* This event is called every time a tab becomes the active tab of
|
|
|
|
* a pane. Use it to show / hide whatever is necessary.
|
|
|
|
*
|
|
|
|
* @event documentActivate
|
|
|
|
* @param {Object} e
|
|
|
|
* @param {Document} e.doc the document that is activate
|
|
|
|
*/
|
|
|
|
"documentActivate",
|
|
|
|
/**
|
|
|
|
* Fires when a document is unloaded from the editor.
|
|
|
|
* This event is also fired when this document is attached to another
|
|
|
|
* instance of the same editor (in a split view situation).
|
|
|
|
* @event documentUnload
|
|
|
|
* @param {Object} e
|
|
|
|
* @param {Document} e.doc the document that was loaded into the editor
|
|
|
|
*/
|
|
|
|
"documentUnload",
|
|
|
|
/**
|
|
|
|
* Fires when the state of the editor is retrieved
|
|
|
|
* @event getState
|
|
|
|
* @param {Object} e
|
|
|
|
* @param {Document} e.doc the document for which the state is retrieved
|
|
|
|
* @param {Object} e.state the state to add values to {See Editor#getState}
|
|
|
|
*/
|
|
|
|
"getState",
|
|
|
|
/**
|
|
|
|
* Fires when the state of the editor is set
|
|
|
|
* @event setState
|
|
|
|
* @param {Object} e
|
|
|
|
* @param {Document} e.doc the document for which the state is set
|
|
|
|
* @param {Object} e.state the state that is being set
|
|
|
|
*/
|
|
|
|
"setState",
|
|
|
|
/**
|
|
|
|
* Fires when the editor is cleared
|
|
|
|
* @event clear
|
|
|
|
*/
|
|
|
|
"clear",
|
|
|
|
/**
|
|
|
|
* Fires when the editor gets the focus. See also
|
|
|
|
* {@link tabManager#focusTab}, {@link tabManager#focussedTab}
|
|
|
|
* @event focus
|
|
|
|
* @param {Object} e
|
|
|
|
* @param {Boolean} e.regain whether the focus is regained.
|
|
|
|
* This means that the editor had lost the focus
|
|
|
|
* previously (the focus event with e.lost set to true
|
|
|
|
* was called.) and now the focus has been given back to
|
|
|
|
* the tabs.
|
|
|
|
* @param {Boolean} e.lost whether the focus is lost,
|
|
|
|
* while the editor remains the focussed editor. This
|
|
|
|
* happens when an element outside of the editors
|
|
|
|
* (for instance the tree or a menu) gets the focus.
|
|
|
|
*/
|
|
|
|
"focus",
|
|
|
|
/**
|
|
|
|
* Fires when the editor looses focus.
|
|
|
|
* @event blur
|
|
|
|
*/
|
|
|
|
"blur",
|
|
|
|
/**
|
|
|
|
* Fires when the cut command is dispatched to this editor,
|
|
|
|
* either using a keybinding or programmatically.
|
|
|
|
*
|
|
|
|
* plugin.on("cut", function(e) {
|
|
|
|
* var data = this.yankDataFromSomewhere();
|
|
|
|
* e.clipboardData.setData("text/plain", data);
|
|
|
|
* });
|
|
|
|
*
|
|
|
|
* @event cut
|
|
|
|
* @param {Object} e
|
|
|
|
* @param {clipboard.ClipboardData} e.clipboardData the api to interact with the clipboard
|
|
|
|
* @param {Boolean} e.native whether this is a native clipboard event
|
|
|
|
*/
|
|
|
|
"cut",
|
|
|
|
/**
|
|
|
|
* Fires when the copy command is dispatched to this editor,
|
|
|
|
* either using a keybinding or programmatically.
|
|
|
|
*
|
|
|
|
* plugin.on("cut", function(e) {
|
|
|
|
* var data = this.getDataFromSomewhere();
|
|
|
|
* e.clipboardData.setData("text/plain", data);
|
|
|
|
* });
|
|
|
|
*
|
|
|
|
* @event copy
|
|
|
|
* @param {Object} e
|
|
|
|
* @param {clipboard.ClipboardData} e.clipboardData the api to interact with the clipboard
|
|
|
|
* @param {Boolean} e.native whether this is a native clipboard event
|
|
|
|
*/
|
|
|
|
"copy",
|
|
|
|
/**
|
|
|
|
* Fires when the copy command is dispatched to this editor,
|
|
|
|
* either using a keybinding or programmatically.
|
|
|
|
*
|
|
|
|
* plugin.on("paste", function(e) {
|
|
|
|
* e.clipboardData.getData("text/plain", function(err, data) {
|
|
|
|
* // Process the data here
|
|
|
|
* });
|
|
|
|
* });
|
|
|
|
*
|
|
|
|
* @event paste
|
|
|
|
* @param {Object} e
|
|
|
|
* @param {clipboard.ClipboardData} e.clipboardData the api to interact with the clipboard
|
|
|
|
* @param {Boolean} e.native whether this is a native clipboard event
|
|
|
|
*/
|
|
|
|
"paste",
|
|
|
|
/**
|
|
|
|
* Fires when the editor resizes
|
|
|
|
* @event resize
|
|
|
|
* @param {Event} e the DOMEvent related to the resize event.
|
|
|
|
*/
|
|
|
|
"resize",
|
|
|
|
/**
|
|
|
|
* Fires when the editor is requested to draw itself. Cloud9
|
|
|
|
* is optimized by lazy loading everyhing. This means that
|
|
|
|
* the editor won't be loaded until absolutely necessary.
|
|
|
|
* This is usually the case when the tab is active for the
|
|
|
|
* first time.
|
|
|
|
* @event draw
|
|
|
|
* @param {Object} e
|
|
|
|
* @param {HTMLElement} e.htmlNode
|
|
|
|
* @param {Tab} e.tab
|
|
|
|
*/
|
|
|
|
"draw",
|
|
|
|
/**
|
|
|
|
* Fires when a new document is going to be loaded in this
|
|
|
|
* editor. The purpose of this event is to allow a check
|
|
|
|
* prior to switching from one editor to the next for the
|
|
|
|
* same document.
|
|
|
|
* @event validate
|
|
|
|
* @param {Object} e
|
|
|
|
* @param {Document} e.document the document that will be loaded
|
|
|
|
* @param {Object} e.info extra information about the document
|
|
|
|
*/
|
|
|
|
"validate",
|
|
|
|
],
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Unloads the document from this editor.
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
unloadDocument: unloadDocument,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Loads the document in this editor to be displayed.
|
|
|
|
* @param {Document} doc the document to display
|
|
|
|
*/
|
|
|
|
loadDocument: loadDocument,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Retrieves the state of a document in relation to this editor
|
|
|
|
* @param {Document} doc the document for which to return the state
|
|
|
|
* @return {Object}
|
|
|
|
*/
|
|
|
|
getState: getState,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets the state of this editor (related to a document)
|
|
|
|
* @param {Document} doc the document for which to set the state
|
|
|
|
* @param {Object} state the state of the document for this editor
|
|
|
|
*/
|
|
|
|
setState: setState,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Clears the value of the document in the editor
|
|
|
|
*/
|
|
|
|
clear: clear,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks if clipboard action is available
|
|
|
|
* @param {Object} clipboard action type
|
|
|
|
*/
|
|
|
|
isClipboardAvailable: isClipboardAvailable,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Cuts the current selection from the editor into the clipboard
|
|
|
|
*/
|
|
|
|
cut: cut,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Copies the current selection from the editor into the clipboard
|
|
|
|
*/
|
|
|
|
copy: copy,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Pastes the current clipboard buffer into this editor
|
|
|
|
*/
|
|
|
|
paste: paste,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets the focus to this editor
|
|
|
|
*/
|
|
|
|
focus: focus,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Removes the focus from this editor
|
|
|
|
*/
|
|
|
|
blur: blur,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Resize the editor to fit it's container
|
|
|
|
*/
|
|
|
|
resize: resize,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Checks whether the passed document would be valid in the context
|
|
|
|
* of this editor.
|
|
|
|
* @param {Document} doc the document to check
|
|
|
|
* @param {Object} info object to put values on to display in a return alert (title, head, message)
|
|
|
|
*/
|
|
|
|
isValid: isValid,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Attaches editor to pane element. This can be done only once
|
|
|
|
* @param pane {AmlNode.Tab} the pane element to add this editor to
|
|
|
|
*/
|
|
|
|
attachTo: attachTo,
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
drawOn: drawOn
|
|
|
|
});
|
|
|
|
|
|
|
|
return plugin;
|
|
|
|
}
|
|
|
|
|
|
|
|
/***** Register and define API *****/
|
|
|
|
|
|
|
|
register(null, {
|
|
|
|
Editor: Editor
|
|
|
|
})
|
|
|
|
}
|
|
|
|
});
|