kopia lustrzana https://github.com/c9/core
autosave on blur
rodzic
882e4b7d16
commit
7c23e8e7ac
|
@ -630,7 +630,7 @@ define(function(require, module, exports) {
|
||||||
if (!loaded || tab.document.meta.preview)
|
if (!loaded || tab.document.meta.preview)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var lastTab = focussedTab;
|
var lastTab = e.lastTab || focussedTab;
|
||||||
|
|
||||||
if (!focussedTab || focussedTab.pane == tab.pane && focussedTab != tab)
|
if (!focussedTab || focussedTab.pane == tab.pane && focussedTab != tab)
|
||||||
focusTab(tab, true, true);
|
focusTab(tab, true, true);
|
||||||
|
|
|
@ -1,122 +1,117 @@
|
||||||
define(function(require, exports, module) {
|
define(function(require, exports, module) {
|
||||||
main.consumes = [
|
main.consumes = [
|
||||||
"Plugin", "c9", "ui", "layout", "tooltip",
|
"Plugin", "c9", "settings", "tabManager", "preferences", "save", "apf"
|
||||||
"anims", "menus", "tabManager", "save",
|
|
||||||
"preferences.experimental"
|
|
||||||
];
|
];
|
||||||
main.provides = ["autosave"];
|
main.provides = ["autosave"];
|
||||||
return main;
|
return main;
|
||||||
|
|
||||||
function main(options, imports, register) {
|
function main(options, imports, register) {
|
||||||
var c9 = imports.c9;
|
var c9 = imports.c9;
|
||||||
var Plugin = imports.Plugin;
|
var apf = imports.apf;
|
||||||
var save = imports.save;
|
var save = imports.save;
|
||||||
var tooltip = imports.tooltip;
|
|
||||||
var tabs = imports.tabManager;
|
var tabs = imports.tabManager;
|
||||||
var experimental = imports["preferences.experimental"];
|
var prefs = imports.preferences;
|
||||||
|
var Plugin = imports.Plugin;
|
||||||
|
var settings = imports.settings;
|
||||||
|
|
||||||
/***** Initialization *****/
|
/***** Initialization *****/
|
||||||
|
|
||||||
var plugin = new Plugin("Ajax.org", main.consumes);
|
var plugin = new Plugin("Ajax.org", main.consumes);
|
||||||
|
|
||||||
var INTERVAL = 60000;
|
|
||||||
var CHANGE_TIMEOUT = 500;
|
var CHANGE_TIMEOUT = 500;
|
||||||
var SLOW_CHANGE_TIMEOUT = options.slowChangeTimeout || 30000;
|
var SLOW_CHANGE_TIMEOUT = options.slowChangeTimeout || 30000;
|
||||||
var SLOW_SAVE_THRESHOLD = 100 * 1024; // 100KB
|
var SLOW_SAVE_THRESHOLD = 100 * 1024; // 100KB
|
||||||
|
|
||||||
var docChangeTimeout = null;
|
var docChangeTimeout;
|
||||||
var btnSave, autosave = true, saveInterval;
|
var autosave;
|
||||||
var enabled = options.testing
|
|
||||||
|| experimental.addExperiment("autosave", false, "Files/Auto-Save");
|
|
||||||
|
|
||||||
var loaded = false;
|
|
||||||
function load() {
|
function load() {
|
||||||
if (loaded || !enabled) return false;
|
prefs.add({
|
||||||
loaded = true;
|
"File": {
|
||||||
|
position: 150,
|
||||||
// when we're back online we'll trigger an autosave if enabled
|
"Save": {
|
||||||
c9.on("stateChange", function(e) {
|
position: 100,
|
||||||
if (e.state & c9.STORAGE && !(e.last & c9.STORAGE))
|
"Enable Auto-Save On Blur": {
|
||||||
check();
|
type: "checkbox",
|
||||||
}, plugin);
|
position: 100,
|
||||||
|
path: "user/general/@autosave"
|
||||||
save.getElement("btnSave", function(btn) {
|
}
|
||||||
btnSave = btn;
|
}
|
||||||
transformButton();
|
}
|
||||||
});
|
|
||||||
|
|
||||||
tabs.on("tabCreate", function(e) {
|
|
||||||
var tab = e.tab;
|
|
||||||
tab.document.undoManager.on("change", function(e) {
|
|
||||||
if (!autosave || !tab.path)
|
|
||||||
return;
|
|
||||||
|
|
||||||
clearTimeout(docChangeTimeout);
|
|
||||||
docChangeTimeout = setTimeout(function() {
|
|
||||||
saveTab(tab);
|
|
||||||
}, tab.document.meta.$slowSave
|
|
||||||
? SLOW_CHANGE_TIMEOUT
|
|
||||||
: CHANGE_TIMEOUT);
|
|
||||||
}, plugin);
|
|
||||||
}, plugin);
|
|
||||||
|
|
||||||
tabs.on("tabDestroy", function(e) {
|
|
||||||
if (!e.tab.path)
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (tabs.getTabs().length == 1)
|
|
||||||
btnSave.hide();
|
|
||||||
|
|
||||||
saveTab(e.tab);
|
|
||||||
}, plugin);
|
}, plugin);
|
||||||
|
|
||||||
|
settings.setDefaults("user/general", [["autosave", false]]);
|
||||||
|
settings.on("read", onSettingChange, plugin);
|
||||||
|
settings.on("user/general", onSettingChange, plugin);
|
||||||
save.on("beforeWarn", function(e) {
|
save.on("beforeWarn", function(e) {
|
||||||
if (autosave && !e.tab.document.meta.newfile) {
|
if (autosave && saveTab(e.tab))
|
||||||
saveTab(e.tab);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
}, plugin);
|
}, plugin);
|
||||||
}
|
}
|
||||||
|
|
||||||
function transformButton() {
|
|
||||||
if (!btnSave) return;
|
|
||||||
if (btnSave.autosave === autosave) return;
|
|
||||||
|
|
||||||
if (autosave) {
|
|
||||||
// Transform btnSave
|
|
||||||
btnSave.setAttribute("caption", "");
|
|
||||||
btnSave.setAttribute("margin", "0 20");
|
|
||||||
btnSave.removeAttribute("tooltip");
|
|
||||||
btnSave.removeAttribute("command");
|
|
||||||
apf.setStyleClass(btnSave.$ext, "btnSave");
|
|
||||||
|
|
||||||
tooltip.add(btnSave, {
|
|
||||||
message: "Changes to your file are automatically saved.<br />\
|
|
||||||
View all your changes through <a href='javascript:void(0)' \
|
|
||||||
onclick='require(\"ext/revisions/revisions\").toggle();' \
|
|
||||||
class='revisionsInfoLink'>the Revision History pane</a>. \
|
|
||||||
Rollback to a previous state, or make comparisons.",
|
|
||||||
width: "250px",
|
|
||||||
hideonclick: true
|
|
||||||
}, plugin);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
btnSave.autosave = autosave;
|
|
||||||
}
|
|
||||||
|
|
||||||
/***** Helpers *****/
|
/***** Helpers *****/
|
||||||
|
|
||||||
function check() {
|
function onSettingChange() {
|
||||||
if (!autosave) return;
|
autosave = settings.getBool("user/general/@autosave");
|
||||||
|
if (autosave)
|
||||||
var pages = tabs.getTabs();
|
enable();
|
||||||
for (var tab, i = 0, l = pages.length; i < l; i++) {
|
else
|
||||||
if ((tab = pages[i]).document.changed && tab.path)
|
disable();
|
||||||
saveTab(tab);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function enable() {
|
||||||
|
apf.on("movefocus", scheduleCheck);
|
||||||
|
tabs.on("tabAfterActivate", scheduleCheck, plugin);
|
||||||
|
window.addEventListener("blur", scheduleCheck);
|
||||||
|
}
|
||||||
|
|
||||||
|
function disable() {
|
||||||
|
apf.off("movefocus", scheduleCheck);
|
||||||
|
tabs.off("tabAfterActivate", scheduleCheck);
|
||||||
|
window.removeEventListener("blur", scheduleCheck);
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleCheck(e) {
|
||||||
|
if (docChangeTimeout)
|
||||||
|
return;
|
||||||
|
var tab;
|
||||||
|
var fromElement = e.fromElement;
|
||||||
|
var toElement = e.toElement;
|
||||||
|
if (e.type == "blur") {
|
||||||
|
tab = tabs.focussedTab;
|
||||||
|
}
|
||||||
|
else if (fromElement) {
|
||||||
|
var fakePage = fromElement.$fake;
|
||||||
|
if (toElement && (toElement == fakePage || fromElement == toElement.$fake)) {
|
||||||
|
fakePage = fromElement.$prevFake || toElement.$prevFake;
|
||||||
|
if (fakePage)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tab = fromElement.cloud9tab || fakePage && fakePage.cloud9tab;
|
||||||
|
if (!tab || !tab.path)
|
||||||
|
return;
|
||||||
|
while (toElement) {
|
||||||
|
if (/window|menu|item/.test(toElement.localName))
|
||||||
|
return;
|
||||||
|
toElement = toElement.parentNode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (e.lastTab) {
|
||||||
|
tab = e.lastTab;
|
||||||
|
}
|
||||||
|
if (!tab || !tab.path)
|
||||||
|
return;
|
||||||
|
|
||||||
|
docChangeTimeout = setTimeout(function() {
|
||||||
|
docChangeTimeout = null;
|
||||||
|
var activeElement = apf.document.activeElement;
|
||||||
|
var nodeName = activeElement && activeElement.localName;
|
||||||
|
// do nothing if the tab is still focused, or is a clone of the focussed tab
|
||||||
|
if (nodeName === "page" && tabs.focussedTab && tabs.focussedTab.path === tab.path)
|
||||||
|
return;
|
||||||
|
saveTab(tab);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveTab(tab, force) {
|
function saveTab(tab, force) {
|
||||||
|
@ -133,22 +128,16 @@ define(function(require, exports, module) {
|
||||||
|| doc.meta.newfile
|
|| doc.meta.newfile
|
||||||
|| doc.meta.nofs
|
|| doc.meta.nofs
|
||||||
|| doc.meta.error
|
|| doc.meta.error
|
||||||
|| doc.meta.$saving))
|
|| doc.meta.$saving
|
||||||
|
|| doc.meta.preview
|
||||||
|
|| !doc.hasValue()))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
var value = doc.value;
|
|
||||||
var slow = value.length > SLOW_SAVE_THRESHOLD;
|
|
||||||
if (slow && !doc.meta.$slowSave) {
|
|
||||||
doc.meta.$slowSave = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
doc.meta.$slowSave = slow;
|
|
||||||
|
|
||||||
save.save(tab, {
|
save.save(tab, {
|
||||||
silentsave: true,
|
silentsave: true,
|
||||||
timeout: 1,
|
|
||||||
value: value
|
|
||||||
}, function() {});
|
}, function() {});
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/***** Lifecycle *****/
|
/***** Lifecycle *****/
|
||||||
|
@ -156,27 +145,20 @@ define(function(require, exports, module) {
|
||||||
plugin.on("load", function() {
|
plugin.on("load", function() {
|
||||||
load();
|
load();
|
||||||
});
|
});
|
||||||
plugin.on("enable", function() {
|
|
||||||
autosave = true;
|
|
||||||
transformButton();
|
|
||||||
});
|
|
||||||
plugin.on("disable", function() {
|
|
||||||
autosave = false;
|
|
||||||
transformButton();
|
|
||||||
});
|
|
||||||
plugin.on("unload", function() {
|
plugin.on("unload", function() {
|
||||||
if (saveInterval)
|
window.removeEventListener("blur", scheduleCheck);
|
||||||
clearInterval(saveInterval);
|
autosave = false;
|
||||||
|
if (docChangeTimeout) {
|
||||||
loaded = false;
|
clearTimeout(docChangeTimeout);
|
||||||
|
docChangeTimeout = null;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/***** Register and define API *****/
|
/***** Register and define API *****/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implements auto save for Cloud9. When the user enables autosave
|
* Implements auto save for Cloud9. When the user enables autosave
|
||||||
* the contents of files are automatically saved about 500ms after the
|
* the contents of files are automatically saved when the editor is blurred
|
||||||
* change is made.
|
|
||||||
* @singleton
|
* @singleton
|
||||||
**/
|
**/
|
||||||
plugin.freezePublicAPI({ });
|
plugin.freezePublicAPI({ });
|
||||||
|
|
|
@ -2,8 +2,7 @@
|
||||||
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
require(["lib/architect/architect", "lib/chai/chai", "/vfs-root"],
|
require(["lib/chai/chai"], function (chai) {
|
||||||
function (architect, chai, baseProc) {
|
|
||||||
var expect = chai.expect;
|
var expect = chai.expect;
|
||||||
|
|
||||||
document.body.appendChild(document.createElement("div"))
|
document.body.appendChild(document.createElement("div"))
|
||||||
|
@ -16,7 +15,6 @@ require(["lib/architect/architect", "lib/chai/chai", "/vfs-root"],
|
||||||
debug: true,
|
debug: true,
|
||||||
hosted: true,
|
hosted: true,
|
||||||
local: false,
|
local: false,
|
||||||
davPrefix: "/"
|
|
||||||
},
|
},
|
||||||
|
|
||||||
"plugins/c9.core/ext",
|
"plugins/c9.core/ext",
|
||||||
|
@ -37,7 +35,7 @@ require(["lib/architect/architect", "lib/chai/chai", "/vfs-root"],
|
||||||
"plugins/c9.ide.editors/undomanager",
|
"plugins/c9.ide.editors/undomanager",
|
||||||
{
|
{
|
||||||
packagePath: "plugins/c9.ide.editors/editors",
|
packagePath: "plugins/c9.ide.editors/editors",
|
||||||
defaultEditor: "texteditor"
|
defaultEditor: "ace"
|
||||||
},
|
},
|
||||||
"plugins/c9.ide.editors/editor",
|
"plugins/c9.ide.editors/editor",
|
||||||
"plugins/c9.ide.editors/tabmanager",
|
"plugins/c9.ide.editors/tabmanager",
|
||||||
|
@ -48,17 +46,15 @@ require(["lib/architect/architect", "lib/chai/chai", "/vfs-root"],
|
||||||
"plugins/c9.ide.save/save",
|
"plugins/c9.ide.save/save",
|
||||||
{
|
{
|
||||||
packagePath: "plugins/c9.ide.save/autosave",
|
packagePath: "plugins/c9.ide.save/autosave",
|
||||||
testing: true
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
packagePath: "plugins/c9.vfs.client/vfs_client"
|
packagePath: "plugins/c9.vfs.client/vfs_client_mock",
|
||||||
|
storage: false
|
||||||
},
|
},
|
||||||
"plugins/c9.vfs.client/endpoint",
|
|
||||||
"plugins/c9.ide.auth/auth",
|
|
||||||
"plugins/c9.core/api",
|
"plugins/c9.core/api",
|
||||||
{
|
{
|
||||||
packagePath: "plugins/c9.fs/fs",
|
packagePath: "plugins/c9.fs/fs",
|
||||||
baseProc: baseProc
|
cli: true
|
||||||
},
|
},
|
||||||
"plugins/c9.fs/fs.cache.xml",
|
"plugins/c9.fs/fs.cache.xml",
|
||||||
|
|
||||||
|
@ -67,27 +63,15 @@ require(["lib/architect/architect", "lib/chai/chai", "/vfs-root"],
|
||||||
provides: [],
|
provides: [],
|
||||||
setup: main
|
setup: main
|
||||||
}
|
}
|
||||||
], architect);
|
]);
|
||||||
|
|
||||||
function main(options, imports, register) {
|
function main(options, imports, register) {
|
||||||
|
var settings = imports.settings
|
||||||
var tabs = imports.tabManager;
|
var tabs = imports.tabManager;
|
||||||
var fs = imports.fs;
|
var fs = imports.fs;
|
||||||
var save = imports.save;
|
var save = imports.save;
|
||||||
var autosave = imports.autosave;
|
var autosave = imports.autosave;
|
||||||
|
|
||||||
function countEvents(count, expected, done) {
|
|
||||||
if (count == expected)
|
|
||||||
done();
|
|
||||||
else
|
|
||||||
throw new Error("Wrong Event Count: "
|
|
||||||
+ count + " of " + expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
expect.html.setConstructor(function(tab) {
|
|
||||||
if (typeof tab == "object")
|
|
||||||
return tab.pane.aml.getPage("editor::" + tab.editorType).$ext;
|
|
||||||
});
|
|
||||||
|
|
||||||
function changeTab(path, done) {
|
function changeTab(path, done) {
|
||||||
var tab = tabs.findTab(path);
|
var tab = tabs.findTab(path);
|
||||||
tabs.focusTab(tab);
|
tabs.focusTab(tab);
|
||||||
|
@ -98,41 +82,90 @@ require(["lib/architect/architect", "lib/chai/chai", "/vfs-root"],
|
||||||
return tab;
|
return tab;
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('autosave', function() {
|
function createAndChangeTab(path, options, done) {
|
||||||
this.timeout(5000);
|
if (!done) {
|
||||||
|
done = options;
|
||||||
before(function(done) {
|
options = {};
|
||||||
tabs.once("ready", function() {
|
}
|
||||||
tabs.getPanes()[0].focus();
|
|
||||||
var path = "/autosave1.txt";
|
|
||||||
fs.writeFile(path, path, function(err) {
|
fs.writeFile(path, path, function(err) {
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
|
|
||||||
tabs.openFile(path, function() {
|
options.path = path;
|
||||||
setTimeout(done, 50);
|
tabs.open(options, function() {
|
||||||
|
changeTab(path, done);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('autosave', function() {
|
||||||
|
this.timeout(5000);
|
||||||
|
|
||||||
|
beforeEach(function(done) {
|
||||||
|
tabs.once("ready", function() {
|
||||||
|
tabs.setState(null, function() {
|
||||||
|
tabs.getPanes()[0].focus();
|
||||||
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
bar.$ext.style.background = "rgba(220, 220, 220, 0.93)";
|
it("should not autosave when restoring state", function(done) {
|
||||||
bar.$ext.style.position = "fixed";
|
settings.set("user/general/@autosave", false);
|
||||||
bar.$ext.style.left = "20px";
|
var pane = tabs.getPanes()[0].hsplit(true);
|
||||||
bar.$ext.style.right = "20px";
|
|
||||||
bar.$ext.style.bottom = "20px";
|
|
||||||
bar.$ext.style.height = "150px";
|
|
||||||
|
|
||||||
document.body.style.marginBottom = "180px";
|
prepareTabs(testRestoreState);
|
||||||
});
|
|
||||||
|
|
||||||
it('should automatically save a tab that is changed', function(done) {
|
function prepareTabs(callback) {
|
||||||
var path = "/autosave1.txt";
|
createAndChangeTab("/autosave1.txt", function(tab) {
|
||||||
changeTab(path, function(tab) {
|
|
||||||
expect(tab.document.changed).to.ok;
|
expect(tab.document.changed).to.ok;
|
||||||
|
createAndChangeTab("/__proto__", function() {
|
||||||
|
createAndChangeTab("/<h1>", function() {
|
||||||
|
createAndChangeTab("/__lookupSetter__", { pane: pane }, function() {
|
||||||
|
callback();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
function testRestoreState() {
|
||||||
|
settings.set("user/general/@autosave", true);
|
||||||
|
expect(tabs.getTabs().length).to.equal(4);
|
||||||
|
var state = tabs.getState(null, true);
|
||||||
|
tabs.setState(null, function() {
|
||||||
|
expect(tabs.getTabs().length).to.equal(0);
|
||||||
|
setTimeout(function() {
|
||||||
|
tabs.setState(state, function() {
|
||||||
|
expect(tabs.getTabs().length).to.equal(4);
|
||||||
|
expect(tabs.getTabs()[0].document.changed).to.ok;
|
||||||
|
setTimeout(function() {
|
||||||
|
tabs.setState(null, function() {
|
||||||
|
save.off("afterSave", preventSave);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function preventSave() {
|
||||||
|
done(new Error("Save is called"));
|
||||||
|
}
|
||||||
|
save.once("afterSave", preventSave);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should automatically save a tab that is changed when editor is blurred", function(done) {
|
||||||
|
settings.set("user/general/@autosave", true);
|
||||||
|
var path = "/autosave2.txt";
|
||||||
|
createAndChangeTab(path, function(tab) {
|
||||||
|
expect(tab.document.changed).to.ok;
|
||||||
|
createAndChangeTab("/__proto__", function() {
|
||||||
|
});
|
||||||
save.once("afterSave", function() {
|
save.once("afterSave", function() {
|
||||||
fs.readFile(path, function(err, data) {
|
fs.readFile(path, function(err, data) {
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
expect(data).to.equal("test" + path);
|
expect(data).to.equal("test" + path);
|
||||||
|
expect(tab.document.changed).to.not.ok;
|
||||||
|
|
||||||
fs.unlink(path, function() {
|
fs.unlink(path, function() {
|
||||||
done();
|
done();
|
||||||
|
|
|
@ -393,6 +393,7 @@ apf.page = function(struct, tagName) {
|
||||||
if (this.relPage) {
|
if (this.relPage) {
|
||||||
this.relPage.$ext.style.display = "";
|
this.relPage.$ext.style.display = "";
|
||||||
this.parentNode.$setStyleClass(this.relPage.$ext, "curpage");
|
this.parentNode.$setStyleClass(this.relPage.$ext, "curpage");
|
||||||
|
this.relPage.$prevFake = this.relPage.$fake;
|
||||||
this.relPage.$fake = this;
|
this.relPage.$fake = this;
|
||||||
|
|
||||||
|
|
||||||
|
@ -604,6 +605,7 @@ apf.page = function(struct, tagName) {
|
||||||
if (page && page.type == _self.id) {
|
if (page && page.type == _self.id) {
|
||||||
page.relPage = _self;
|
page.relPage = _self;
|
||||||
if (page.$active) {
|
if (page.$active) {
|
||||||
|
_self.$prevFake = _self.$fake;
|
||||||
_self.$fake = page;
|
_self.$fake = page;
|
||||||
page.$activate();
|
page.$activate();
|
||||||
}
|
}
|
||||||
|
|
|
@ -10967,15 +10967,8 @@ apf.window = function(){
|
||||||
|
|
||||||
this.$settingFocus = null;
|
this.$settingFocus = null;
|
||||||
|
|
||||||
apf.dispatchEvent("movefocus", {
|
|
||||||
toElement: amlNode
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
apf.dispatchEvent("movefocus", e);
|
||||||
};
|
};
|
||||||
|
|
||||||
this.$blur = function(amlNode) {
|
this.$blur = function(amlNode) {
|
||||||
|
|
Ładowanie…
Reference in New Issue