First step for realtime in onlyoffice apps
This commit is contained in:
parent
41a1148c17
commit
efdecd4059
@ -623,6 +623,13 @@ define([
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Onlyoffice
|
||||||
|
var onlyoffice = common.onlyoffice = {};
|
||||||
|
onlyoffice.execCommand = function (data, cb) {
|
||||||
|
postMessage("OO_COMMAND", data, cb);
|
||||||
|
};
|
||||||
|
onlyoffice.onEvent = Util.mkEvent();
|
||||||
|
|
||||||
// Messenger
|
// Messenger
|
||||||
var messenger = common.messenger = {};
|
var messenger = common.messenger = {};
|
||||||
messenger.execCommand = function (data, cb) {
|
messenger.execCommand = function (data, cb) {
|
||||||
@ -1061,6 +1068,8 @@ define([
|
|||||||
common.onNetworkReconnect.fire(data);
|
common.onNetworkReconnect.fire(data);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
// OnlyOffice
|
||||||
|
OO_EVENT: common.onlyoffice.onEvent.fire,
|
||||||
// Chat
|
// Chat
|
||||||
CHAT_EVENT: common.messenger.onEvent.fire,
|
CHAT_EVENT: common.messenger.onEvent.fire,
|
||||||
// Cursor
|
// Cursor
|
||||||
|
|||||||
@ -15,6 +15,7 @@ define([
|
|||||||
'/common/onlyoffice/oocell_base.js',
|
'/common/onlyoffice/oocell_base.js',
|
||||||
'/common/onlyoffice/oodoc_base.js',
|
'/common/onlyoffice/oodoc_base.js',
|
||||||
'/common/onlyoffice/ooslide_base.js',
|
'/common/onlyoffice/ooslide_base.js',
|
||||||
|
'/common/outer/worker-channel.js',
|
||||||
|
|
||||||
'/bower_components/tweetnacl/nacl-fast.min.js',
|
'/bower_components/tweetnacl/nacl-fast.min.js',
|
||||||
'/bower_components/file-saver/FileSaver.min.js',
|
'/bower_components/file-saver/FileSaver.min.js',
|
||||||
@ -38,7 +39,8 @@ define([
|
|||||||
FileCrypto,
|
FileCrypto,
|
||||||
EmptyCell,
|
EmptyCell,
|
||||||
EmptyDoc,
|
EmptyDoc,
|
||||||
EmptySlide)
|
EmptySlide,
|
||||||
|
Channel)
|
||||||
{
|
{
|
||||||
var saveAs = window.saveAs;
|
var saveAs = window.saveAs;
|
||||||
var Nacl = window.nacl;
|
var Nacl = window.nacl;
|
||||||
@ -83,6 +85,35 @@ define([
|
|||||||
return file;
|
return file;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var openRtChannel = function (data) {
|
||||||
|
// XXX
|
||||||
|
var channel = Hash.createChannelId();
|
||||||
|
ctx.sframeChan.query('Q_OO_OPENCHANNEL', channel, function (err, obj) {
|
||||||
|
if (err || (obj && obj.error)) { console.error(err || (obj && obj.error)); }
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var mkChannel = function () {
|
||||||
|
var msgEv = Util.mkEvent();
|
||||||
|
var iframe = $('#cp-app-oo-container > iframe')[0].contentWindow;
|
||||||
|
window.addEventListener('message', function (msg) {
|
||||||
|
if (msg.source !== iframe) { return; }
|
||||||
|
msgEv.fire(msg);
|
||||||
|
});
|
||||||
|
var postMsg = function (data) {
|
||||||
|
iframe.postMessage(data, '*');
|
||||||
|
};
|
||||||
|
Channel.create(msgEv, postMsg, function (chan) {
|
||||||
|
APP.chan = chan;
|
||||||
|
chan.on('CMDFROMOO', function (data) {
|
||||||
|
console.log('command from oo', data);
|
||||||
|
setTimeout(function () {
|
||||||
|
chan.event('RTMSG', 'Pewpewpew');
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
var startOO = function (blob, file) {
|
var startOO = function (blob, file) {
|
||||||
if (APP.ooconfig) { return void console.error('already started'); }
|
if (APP.ooconfig) { return void console.error('already started'); }
|
||||||
var url = URL.createObjectURL(blob);
|
var url = URL.createObjectURL(blob);
|
||||||
@ -144,6 +175,7 @@ define([
|
|||||||
if (ifr) { ifr.remove(); }
|
if (ifr) { ifr.remove(); }
|
||||||
};
|
};
|
||||||
APP.docEditor = new DocsAPI.DocEditor("cp-app-oo-placeholder", APP.ooconfig);
|
APP.docEditor = new DocsAPI.DocEditor("cp-app-oo-placeholder", APP.ooconfig);
|
||||||
|
mkChannel();
|
||||||
};
|
};
|
||||||
|
|
||||||
var getContent = APP.getContent = function () {
|
var getContent = APP.getContent = function () {
|
||||||
|
|||||||
@ -53,6 +53,21 @@ define([
|
|||||||
Cryptpad.setPadAttribute('lastVersion', data.url, cb);
|
Cryptpad.setPadAttribute('lastVersion', data.url, cb);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
sframeChan.on('Q_OO_OPENCHANNEL', function (data, cb) {
|
||||||
|
Cryptpad.onlyoffice.execCommand({
|
||||||
|
cmd: 'OPEN_CHANNEL',
|
||||||
|
data: {
|
||||||
|
channel: data,
|
||||||
|
secret: secret
|
||||||
|
}
|
||||||
|
}, cb);
|
||||||
|
});
|
||||||
|
sframeChan.on('Q_OO_COMMAND', function (data, cb) {
|
||||||
|
Cryptpad.onlyoffice.execCommand(data, cb);
|
||||||
|
});
|
||||||
|
Cryptpad.onlyoffice.onEvent.reg(function (data) {
|
||||||
|
sframeChan.event('EV_OO_EVENT', data);
|
||||||
|
});
|
||||||
};
|
};
|
||||||
SFCommonO.start({
|
SFCommonO.start({
|
||||||
type: 'oo',
|
type: 'oo',
|
||||||
|
|||||||
66
www/common/onlyoffice/sdkjs/cell/sdk-all-min.js
vendored
66
www/common/onlyoffice/sdkjs/cell/sdk-all-min.js
vendored
@ -3770,6 +3770,50 @@ AscBrowser.convertToRetinaValue = function(value, isScale)
|
|||||||
sockjs = this.sockjs = {};
|
sockjs = this.sockjs = {};
|
||||||
//t._state = true;
|
//t._state = true;
|
||||||
|
|
||||||
|
var send = function (data) {
|
||||||
|
setTimeout(function () {
|
||||||
|
console.log(data);
|
||||||
|
sockjs.onmessage({
|
||||||
|
data: JSON.stringify(data)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
var license = {
|
||||||
|
type: 'license',
|
||||||
|
license: {
|
||||||
|
type: 3,
|
||||||
|
light: false,
|
||||||
|
trial: false,
|
||||||
|
rights: 1,
|
||||||
|
buildVersion: "4.3.3",
|
||||||
|
buildNumber: 4,
|
||||||
|
branding: false
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var channel;
|
||||||
|
|
||||||
|
require([
|
||||||
|
'/common/outer/worker-channel.js',
|
||||||
|
'/common/common-util.js'
|
||||||
|
], function (Channel, Util) {
|
||||||
|
var msgEv = Util.mkEvent();
|
||||||
|
var p = window.parent;
|
||||||
|
window.addEventListener('message', function (msg) {
|
||||||
|
if (msg.source !== p) { return; }
|
||||||
|
msgEv.fire(msg);
|
||||||
|
});
|
||||||
|
var postMsg = function (data) {
|
||||||
|
p.postMessage(data, '*');
|
||||||
|
};
|
||||||
|
Channel.create(msgEv, postMsg, function (chan) {
|
||||||
|
channel = chan;
|
||||||
|
send(license);
|
||||||
|
chan.on('RTMSG', function (data) {
|
||||||
|
console.log('receiving RTMSG', data);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
sockjs.onopen = function() {
|
sockjs.onopen = function() {
|
||||||
/*
|
/*
|
||||||
if (t.reconnectTimeout) {
|
if (t.reconnectTimeout) {
|
||||||
@ -3861,14 +3905,6 @@ AscBrowser.convertToRetinaValue = function(value, isScale)
|
|||||||
console.error('Close realtime');
|
console.error('Close realtime');
|
||||||
};
|
};
|
||||||
|
|
||||||
var send = function (data) {
|
|
||||||
setTimeout(function () {
|
|
||||||
console.log(data);
|
|
||||||
sockjs.onmessage({
|
|
||||||
data: JSON.stringify(data)
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
sockjs.send = function (data) {
|
sockjs.send = function (data) {
|
||||||
console.log(data);
|
console.log(data);
|
||||||
try {
|
try {
|
||||||
@ -3893,6 +3929,7 @@ AscBrowser.convertToRetinaValue = function(value, isScale)
|
|||||||
};
|
};
|
||||||
send(msg);
|
send(msg);
|
||||||
send(msg2);
|
send(msg2);
|
||||||
|
channel.event('CMDFROMOO', 'Hey');
|
||||||
break;
|
break;
|
||||||
case 'getMessages':
|
case 'getMessages':
|
||||||
msg = {};
|
msg = {};
|
||||||
@ -3900,19 +3937,6 @@ AscBrowser.convertToRetinaValue = function(value, isScale)
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
var license = {
|
|
||||||
type: 'license',
|
|
||||||
license: {
|
|
||||||
type: 3,
|
|
||||||
light: false,
|
|
||||||
trial: false,
|
|
||||||
rights: 1,
|
|
||||||
buildVersion: "4.3.3",
|
|
||||||
buildNumber: 4,
|
|
||||||
branding: false
|
|
||||||
}
|
|
||||||
};
|
|
||||||
send(license);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,7 @@ define([
|
|||||||
'/common/common-messaging.js',
|
'/common/common-messaging.js',
|
||||||
'/common/common-messenger.js',
|
'/common/common-messenger.js',
|
||||||
'/common/outer/cursor.js',
|
'/common/outer/cursor.js',
|
||||||
|
'/common/outer/onlyoffice.js',
|
||||||
'/common/outer/chainpad-netflux-worker.js',
|
'/common/outer/chainpad-netflux-worker.js',
|
||||||
'/common/outer/network-config.js',
|
'/common/outer/network-config.js',
|
||||||
'/customize/application_config.js',
|
'/customize/application_config.js',
|
||||||
@ -21,7 +22,7 @@ define([
|
|||||||
'/bower_components/nthen/index.js',
|
'/bower_components/nthen/index.js',
|
||||||
'/bower_components/saferphore/index.js',
|
'/bower_components/saferphore/index.js',
|
||||||
], function (Sortify, UserObject, ProxyManager, Migrate, Hash, Util, Constants, Feedback, Realtime, Messaging, Messenger,
|
], function (Sortify, UserObject, ProxyManager, Migrate, Hash, Util, Constants, Feedback, Realtime, Messaging, Messenger,
|
||||||
Cursor, CpNfWorker, NetConfig, AppConfig,
|
Cursor, OnlyOffice, CpNfWorker, NetConfig, AppConfig,
|
||||||
Crypto, ChainPad, Listmap, nThen, Saferphore) {
|
Crypto, ChainPad, Listmap, nThen, Saferphore) {
|
||||||
var Store = {};
|
var Store = {};
|
||||||
|
|
||||||
@ -927,6 +928,14 @@ define([
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// OnlyOffice
|
||||||
|
Store.onlyoffice = {
|
||||||
|
execCommand: function (clientId, data, cb) {
|
||||||
|
if (!store.onlyoffice) { return void cb({error: 'OnlyOffice is disabled'}); }
|
||||||
|
store.onlyoffice.execCommand(data, cb);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Cursor
|
// Cursor
|
||||||
|
|
||||||
Store.cursor = {
|
Store.cursor = {
|
||||||
@ -1237,6 +1246,7 @@ define([
|
|||||||
var dropChannel = function (chanId) {
|
var dropChannel = function (chanId) {
|
||||||
store.messenger.leavePad(chanId);
|
store.messenger.leavePad(chanId);
|
||||||
store.cursor.leavePad(chanId);
|
store.cursor.leavePad(chanId);
|
||||||
|
store.onlyoffice.leavePad(chanId);
|
||||||
|
|
||||||
if (!Store.channels[chanId]) { return; }
|
if (!Store.channels[chanId]) { return; }
|
||||||
|
|
||||||
@ -1256,6 +1266,7 @@ define([
|
|||||||
messengerEventClients.splice(messengerIdx, 1);
|
messengerEventClients.splice(messengerIdx, 1);
|
||||||
}
|
}
|
||||||
store.cursor.removeClient(clientId);
|
store.cursor.removeClient(clientId);
|
||||||
|
store.onlyoffice.removeClient(clientId);
|
||||||
Object.keys(Store.channels).forEach(function (chanId) {
|
Object.keys(Store.channels).forEach(function (chanId) {
|
||||||
var chanIdx = Store.channels[chanId].clients.indexOf(clientId);
|
var chanIdx = Store.channels[chanId].clients.indexOf(clientId);
|
||||||
if (chanIdx !== -1) {
|
if (chanIdx !== -1) {
|
||||||
@ -1339,6 +1350,17 @@ define([
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var loadOnlyOffice = function () {
|
||||||
|
store.onlyoffice = OnlyOffice.init(store, function (ev, data, clients) {
|
||||||
|
clients.forEach(function (cId) {
|
||||||
|
postMessage(cId, 'OO_EVENT', {
|
||||||
|
ev: ev,
|
||||||
|
data: data
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////
|
||||||
/////////////////////// Init /////////////////////////////////////
|
/////////////////////// Init /////////////////////////////////////
|
||||||
//////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////
|
||||||
@ -1426,6 +1448,7 @@ define([
|
|||||||
loadSharedFolders(waitFor);
|
loadSharedFolders(waitFor);
|
||||||
loadMessenger();
|
loadMessenger();
|
||||||
loadCursor();
|
loadCursor();
|
||||||
|
loadOnlyOffice();
|
||||||
}).nThen(function () {
|
}).nThen(function () {
|
||||||
var requestLogin = function () {
|
var requestLogin = function () {
|
||||||
broadcast([], "REQUEST_LOGIN");
|
broadcast([], "REQUEST_LOGIN");
|
||||||
|
|||||||
191
www/common/outer/onlyoffice.js
Normal file
191
www/common/outer/onlyoffice.js
Normal file
@ -0,0 +1,191 @@
|
|||||||
|
define([
|
||||||
|
'/common/common-util.js',
|
||||||
|
'/common/common-constants.js',
|
||||||
|
'/customize/messages.js',
|
||||||
|
'/bower_components/chainpad-crypto/crypto.js',
|
||||||
|
], function (Util, Constants, Messages, Crypto) {
|
||||||
|
var OO = {};
|
||||||
|
|
||||||
|
var convertToUint8 = function (obj) {
|
||||||
|
var l = Object.keys(obj).length;
|
||||||
|
var u = new Uint8Array(l);
|
||||||
|
for (var i = 0; i<l; i++) {
|
||||||
|
u[i] = obj[i];
|
||||||
|
}
|
||||||
|
return u;
|
||||||
|
};
|
||||||
|
|
||||||
|
var openChannel = function (ctx, obj, client, cb) {
|
||||||
|
var channel = obj.channel;
|
||||||
|
var secret = obj.secret;
|
||||||
|
if (secret.keys.cryptKey) {
|
||||||
|
secret.keys.cryptKey = convertToUint8(secret.keys.cryptKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
var padChan = secret.channel;
|
||||||
|
var network = ctx.store.network;
|
||||||
|
var first = true;
|
||||||
|
|
||||||
|
var c = ctx.clients[client];
|
||||||
|
if (!c) {
|
||||||
|
c = ctx.clients[client] = {
|
||||||
|
channel: channel,
|
||||||
|
padChan: padChan,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return void cb();
|
||||||
|
}
|
||||||
|
|
||||||
|
var chan = ctx.channels[channel];
|
||||||
|
if (chan) {
|
||||||
|
// This channel is already open in another tab
|
||||||
|
|
||||||
|
// ==> Set the ID to our client object
|
||||||
|
if (!c.id) { c.id = chan.wc.myID + '-' + client; }
|
||||||
|
|
||||||
|
// ==> Send the join message to the other members of the channel
|
||||||
|
// XXX bcast a "join" message to the channel?
|
||||||
|
|
||||||
|
// ==> And push the new tab to the list
|
||||||
|
chan.clients.push(client);
|
||||||
|
return void cb();
|
||||||
|
}
|
||||||
|
|
||||||
|
var onOpen = function (wc) {
|
||||||
|
|
||||||
|
ctx.channels[channel] = ctx.channels[channel] || {};
|
||||||
|
|
||||||
|
var chan = ctx.channels[channel];
|
||||||
|
if (!c.id) { c.id = wc.myID + '-' + client; }
|
||||||
|
if (chan.clients) {
|
||||||
|
// If 2 tabs from the same worker have been opened at the same time,
|
||||||
|
// we have to fix both of them
|
||||||
|
chan.clients.forEach(function (cl) {
|
||||||
|
if (ctx.clients[cl] && !ctx.clients[cl].id) {
|
||||||
|
ctx.clients[cl].id = wc.myID + '-' + cl;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (!chan.encryptor) { chan.encryptor = Crypto.createEncryptor(secret.keys); }
|
||||||
|
|
||||||
|
wc.on('join', function () {
|
||||||
|
// XXX
|
||||||
|
});
|
||||||
|
wc.on('leave', function (peer) {
|
||||||
|
// XXX
|
||||||
|
});
|
||||||
|
wc.on('message', function (cryptMsg) {
|
||||||
|
var msg = chan.encryptor.decrypt(cryptMsg, secret.keys && secret.keys.validateKey);
|
||||||
|
var parsed;
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(msg);
|
||||||
|
// XXX
|
||||||
|
} catch (e) { console.error(e); }
|
||||||
|
});
|
||||||
|
|
||||||
|
chan.wc = wc;
|
||||||
|
chan.sendMsg = function (msg, cb) {
|
||||||
|
cb = cb || function () {};
|
||||||
|
var cmsg = chan.encryptor.encrypt(msg);
|
||||||
|
wc.bcast(cmsg).then(function () {
|
||||||
|
cb();
|
||||||
|
}, function (err) {
|
||||||
|
cb({error: err});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!first) { return; }
|
||||||
|
chan.clients = [client];
|
||||||
|
first = false;
|
||||||
|
cb();
|
||||||
|
};
|
||||||
|
|
||||||
|
network.join(channel).then(onOpen, function (err) {
|
||||||
|
return void cb({error: err});
|
||||||
|
});
|
||||||
|
|
||||||
|
network.on('reconnect', function () {
|
||||||
|
if (!ctx.channels[channel]) { console.log("cant reconnect", channel); return; }
|
||||||
|
network.join(channel).then(onOpen, function (err) {
|
||||||
|
console.error(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
var leaveChannel = function (ctx, padChan) {
|
||||||
|
// Leave channel and prevent reconnect when we leave a pad
|
||||||
|
Object.keys(ctx.channels).some(function (ooChan) {
|
||||||
|
var channel = ctx.channels[ooChan];
|
||||||
|
if (channel.padChan !== padChan) { return; }
|
||||||
|
if (channel.wc) { channel.wc.leave(); }
|
||||||
|
delete ctx.channels[ooChan];
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove the client from all its channels when a tab is closed
|
||||||
|
var removeClient = function (ctx, clientId) {
|
||||||
|
var filter = function (c) {
|
||||||
|
return c !== clientId;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remove the client from our channels
|
||||||
|
var chan;
|
||||||
|
for (var k in ctx.channels) {
|
||||||
|
chan = ctx.channels[k];
|
||||||
|
chan.clients = chan.clients.filter(filter);
|
||||||
|
if (chan.clients.length === 0) {
|
||||||
|
if (chan.wc) { chan.wc.leave(); }
|
||||||
|
delete ctx.channels[k];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the leave message to the channel we were in
|
||||||
|
if (ctx.clients[clientId]) {
|
||||||
|
var leaveMsg = {
|
||||||
|
leave: true,
|
||||||
|
id: ctx.clients[clientId].id
|
||||||
|
};
|
||||||
|
chan = ctx.channels[ctx.clients[clientId].channel];
|
||||||
|
if (chan) {
|
||||||
|
chan.sendMsg(JSON.stringify(leaveMsg));
|
||||||
|
ctx.emit('MESSAGE', leaveMsg, chan.clients);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
delete ctx.clients[clientId];
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
OO.init = function (store, emit) {
|
||||||
|
var oo = {};
|
||||||
|
var ctx = {
|
||||||
|
store: store,
|
||||||
|
emit: emit,
|
||||||
|
channels: {},
|
||||||
|
clients: {}
|
||||||
|
};
|
||||||
|
|
||||||
|
oo.removeClient = function (clientId) {
|
||||||
|
removeClient(ctx, clientId);
|
||||||
|
};
|
||||||
|
oo.leavePad = function (padChan) {
|
||||||
|
leaveChannel(ctx, padChan);
|
||||||
|
};
|
||||||
|
oo.execCommand = function (clientId, obj, cb) {
|
||||||
|
var cmd = obj.cmd;
|
||||||
|
var data = obj.data;
|
||||||
|
if (cmd === 'OPEN_CHANNEL') {
|
||||||
|
return void openChannel(ctx, data, clientId, cb);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return cursor;
|
||||||
|
};
|
||||||
|
|
||||||
|
return OO;
|
||||||
|
});
|
||||||
@ -62,6 +62,8 @@ define([
|
|||||||
ADD_DIRECT_MESSAGE_HANDLERS: Store.addDirectMessageHandlers,
|
ADD_DIRECT_MESSAGE_HANDLERS: Store.addDirectMessageHandlers,
|
||||||
// Chat
|
// Chat
|
||||||
CHAT_COMMAND: Store.messenger.execCommand,
|
CHAT_COMMAND: Store.messenger.execCommand,
|
||||||
|
// OnlyOffice
|
||||||
|
OO_COMMAND: Store.onlyoffice.execCommand,
|
||||||
// Cursor
|
// Cursor
|
||||||
CURSOR_COMMAND: Store.cursor.execCommand,
|
CURSOR_COMMAND: Store.cursor.execCommand,
|
||||||
// Pad
|
// Pad
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user