added textbind example, improved & fixed syncing, RBTree handles ids correctly now, webrtc connector is quite reliable now
This commit is contained in:
parent
f9f8228db6
commit
f78dc52d7b
@ -34,6 +34,8 @@
|
|||||||
"createUsers": true,
|
"createUsers": true,
|
||||||
"getRandomNumber": true,
|
"getRandomNumber": true,
|
||||||
"applyRandomTransactions": true,
|
"applyRandomTransactions": true,
|
||||||
"CustomType": true
|
"CustomType": true,
|
||||||
|
"window": true,
|
||||||
|
"document": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
20
Examples/TextBind/index.html
Normal file
20
Examples/TextBind/index.html
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset=utf-8 />
|
||||||
|
<title>Y Example</title>
|
||||||
|
<script src="../../node_modules/simplewebrtc/simplewebrtc.bundle.js"></script>
|
||||||
|
<script src="../../y.js"></script>
|
||||||
|
<script src="./index.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1 contentEditable> yjs Tutorial</h1>
|
||||||
|
<p> Collaborative Json editing with <a href="https://github.com/rwth-acis/yjs/">yjs</a>
|
||||||
|
and XMPP Connector. </p>
|
||||||
|
|
||||||
|
<textarea style="width:80%;" rows=40 id="textfield"></textarea>
|
||||||
|
|
||||||
|
<p> <a href="https://github.com/y-js/yjs/">yjs</a> is a Framework for Real-Time collaboration on arbitrary data types.
|
||||||
|
</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
27
Examples/TextBind/index.js
Normal file
27
Examples/TextBind/index.js
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
|
||||||
|
Y({
|
||||||
|
db: {
|
||||||
|
name: "Memory"
|
||||||
|
},
|
||||||
|
connector: {
|
||||||
|
name: "WebRTC",
|
||||||
|
room: "mineeeeeee",
|
||||||
|
debug: true
|
||||||
|
}
|
||||||
|
}).then(function(yconfig){
|
||||||
|
window.y = yconfig.root;
|
||||||
|
window.yconfig = yconfig;
|
||||||
|
var textarea = document.getElementById("textfield");
|
||||||
|
yconfig.root.observe(function(events){
|
||||||
|
for (var e in events) {
|
||||||
|
var event = events[e];
|
||||||
|
if (event.name === "text" && (event.type === "add" || event.type === "update")) {
|
||||||
|
event.object.get(event.name).then(function(text){ //eslint-disable-line
|
||||||
|
text.bind(textarea);
|
||||||
|
window.ytext = text;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
yconfig.root.set("text", Y.TextBind);
|
||||||
|
});
|
@ -70,7 +70,8 @@ var options = minimist(process.argv.slice(2), {
|
|||||||
});
|
});
|
||||||
|
|
||||||
var files = {
|
var files = {
|
||||||
y: polyfills.concat(["src/y.js", "src/Connector.js", "src/OperationStore.js", "src/Struct.js", "src/**/*.js", "!src/**/*.spec.js"]),
|
y: polyfills.concat(["src/y.js", "src/Connector.js", "src/OperationStore.js", "src/Struct.js", "src/Utils.js",
|
||||||
|
"src/OperationStores/RedBlackTree.js", "src/**/*.js", "!src/**/*.spec.js"]),
|
||||||
lint: ["src/**/*.js", "gulpfile.js"],
|
lint: ["src/**/*.js", "gulpfile.js"],
|
||||||
test: polyfills.concat([options.testfiles]),
|
test: polyfills.concat([options.testfiles]),
|
||||||
build_test: ["build_test/y.js"]
|
build_test: ["build_test/y.js"]
|
||||||
|
@ -25,6 +25,7 @@ class AbstractConnector { //eslint-disable-line no-unused-vars
|
|||||||
this.syncingClients = [];
|
this.syncingClients = [];
|
||||||
this.forwardToSyncingClients = (opts.forwardToSyncingClients === false) ? false : true;
|
this.forwardToSyncingClients = (opts.forwardToSyncingClients === false) ? false : true;
|
||||||
this.debug = opts.debug ? true : false;
|
this.debug = opts.debug ? true : false;
|
||||||
|
this.broadcastedHB = false;
|
||||||
}
|
}
|
||||||
setUserId (userId) {
|
setUserId (userId) {
|
||||||
this.userId = userId;
|
this.userId = userId;
|
||||||
@ -117,7 +118,8 @@ class AbstractConnector { //eslint-disable-line no-unused-vars
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.debug) {
|
if (this.debug) {
|
||||||
console.log(`${sender} -> ${this.userId}: ${JSON.stringify(m)}`); //eslint-disable-line
|
console.log(`${sender} -> me: ${m.type}`);//eslint-disable-line
|
||||||
|
console.dir(m); //eslint-disable-line
|
||||||
}
|
}
|
||||||
if (m.type === "sync step 1") {
|
if (m.type === "sync step 1") {
|
||||||
// TODO: make transaction, stream the ops
|
// TODO: make transaction, stream the ops
|
||||||
@ -148,6 +150,7 @@ class AbstractConnector { //eslint-disable-line no-unused-vars
|
|||||||
this.y.db.requestTransaction(function*(){
|
this.y.db.requestTransaction(function*(){
|
||||||
var ops = yield* this.getOperations(m.stateVector);
|
var ops = yield* this.getOperations(m.stateVector);
|
||||||
if (ops.length > 0) {
|
if (ops.length > 0) {
|
||||||
|
conn.broadcastedHB = true;
|
||||||
conn.broadcast({
|
conn.broadcast({
|
||||||
type: "update",
|
type: "update",
|
||||||
ops: ops
|
ops: ops
|
||||||
|
@ -12,12 +12,12 @@ class WebRTC extends AbstractConnector {
|
|||||||
|
|
||||||
var room = options.room;
|
var room = options.room;
|
||||||
|
|
||||||
// connect per default to our server
|
var webrtcOptions = {
|
||||||
if(options.url == null){
|
url: options.url || "https://yatta.ninja:8888",
|
||||||
options.url = "https://yatta.ninja:8888";
|
room: options.room
|
||||||
}
|
};
|
||||||
|
|
||||||
var swr = new SimpleWebRTC(options); //eslint-disable-line no-undef
|
var swr = new SimpleWebRTC(webrtcOptions); //eslint-disable-line no-undef
|
||||||
this.swr = swr;
|
this.swr = swr;
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
|
@ -21,32 +21,19 @@ class AbstractTransaction { //eslint-disable-line no-unused-vars
|
|||||||
this.store.initializedTypes[sid] = t;
|
this.store.initializedTypes[sid] = t;
|
||||||
return t;
|
return t;
|
||||||
}
|
}
|
||||||
// returns false if operation is not expected.
|
|
||||||
*addOperation (op) {
|
|
||||||
var state = yield* this.getState(op.id[0]);
|
|
||||||
if (op.id[1] === state.clock){
|
|
||||||
state.clock++;
|
|
||||||
yield* this.setState(state);
|
|
||||||
this.os.add(op);
|
|
||||||
yield* this.store.operationAdded(this, op);
|
|
||||||
return true;
|
|
||||||
} else if (op.id[1] < state.clock) {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
throw new Error("Operations must arrive in order!");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*applyCreatedOperations (ops) {
|
*applyCreatedOperations (ops) {
|
||||||
var send = [];
|
var send = [];
|
||||||
for (var i = 0; i < ops.length; i++) {
|
for (var i = 0; i < ops.length; i++) {
|
||||||
var op = ops[i];
|
var op = ops[i];
|
||||||
yield* Struct[op.struct].execute.call(this, op);
|
yield* this.store.tryExecute.call(this, op);
|
||||||
send.push(copyObject(Struct[op.struct].encode(op)));
|
send.push(copyObject(Struct[op.struct].encode(op)));
|
||||||
}
|
}
|
||||||
this.store.y.connector.broadcast({
|
if (this.store.y.connector.broadcastedHB){
|
||||||
type: "update",
|
this.store.y.connector.broadcast({
|
||||||
ops: send
|
type: "update",
|
||||||
});
|
ops: send
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,10 +66,23 @@ class AbstractOperationStore { //eslint-disable-line no-unused-vars
|
|||||||
// TODO: Use ES7 Weak Maps. This way types that are no longer user,
|
// TODO: Use ES7 Weak Maps. This way types that are no longer user,
|
||||||
// wont be kept in memory.
|
// wont be kept in memory.
|
||||||
this.initializedTypes = {};
|
this.initializedTypes = {};
|
||||||
|
this.whenUserIdSetListener = null;
|
||||||
|
this.waitingOperations = new RBTree();
|
||||||
}
|
}
|
||||||
setUserId (userId) {
|
setUserId (userId) {
|
||||||
this.userId = userId;
|
this.userId = userId;
|
||||||
this.opClock = 0;
|
this.opClock = 0;
|
||||||
|
if (this.whenUserIdSetListener != null) {
|
||||||
|
this.whenUserIdSetListener();
|
||||||
|
this.whenUserIdSetListener = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
whenUserIdSet (f) {
|
||||||
|
if (this.userId != null) {
|
||||||
|
f();
|
||||||
|
} else {
|
||||||
|
this.whenUserIdSetListener = f;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
getNextOpId () {
|
getNextOpId () {
|
||||||
if (this.userId == null) {
|
if (this.userId == null) {
|
||||||
@ -140,7 +140,7 @@ class AbstractOperationStore { //eslint-disable-line no-unused-vars
|
|||||||
|
|
||||||
for (let key in exeNow) {
|
for (let key in exeNow) {
|
||||||
let o = exeNow[key].op;
|
let o = exeNow[key].op;
|
||||||
yield* Struct[o.struct].execute.call(this, o);
|
yield* store.tryExecute.call(this, o);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (var sid in ls){
|
for (var sid in ls){
|
||||||
@ -153,13 +153,37 @@ class AbstractOperationStore { //eslint-disable-line no-unused-vars
|
|||||||
let listener = l[key];
|
let listener = l[key];
|
||||||
let o = listener.op;
|
let o = listener.op;
|
||||||
if (--listener.missing === 0){
|
if (--listener.missing === 0){
|
||||||
yield* Struct[o.struct].execute.call(this, o);
|
yield* store.tryExecute.call(this, o);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
*tryExecute (op) {
|
||||||
|
if (op.struct === "Delete") {
|
||||||
|
yield* Struct.Delete.execute.call(this, op);
|
||||||
|
} else {
|
||||||
|
while (op != null) {
|
||||||
|
var state = yield* this.getState(op.id[0]);
|
||||||
|
if (op.id[1] === state.clock){
|
||||||
|
state.clock++;
|
||||||
|
yield* this.setState.call(this, state);
|
||||||
|
yield* Struct[op.struct].execute.call(this, op);
|
||||||
|
yield* this.addOperation(op);
|
||||||
|
yield* this.store.operationAdded(this, op);
|
||||||
|
// find next operation to execute
|
||||||
|
op = this.store.waitingOperations.find([op.id[0], state.clock]);
|
||||||
|
} else {
|
||||||
|
if (op.id[1] > state.clock) {
|
||||||
|
// has to be executed at some point later
|
||||||
|
this.store.waitingOperations.add(op);
|
||||||
|
}
|
||||||
|
op = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// called by a transaction when an operation is added
|
// called by a transaction when an operation is added
|
||||||
*operationAdded (transaction, op) {
|
*operationAdded (transaction, op) {
|
||||||
var sid = JSON.stringify(op.id);
|
var sid = JSON.stringify(op.id);
|
||||||
|
@ -4,7 +4,10 @@
|
|||||||
if(typeof window !== "undefined"){
|
if(typeof window !== "undefined"){
|
||||||
jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;
|
jasmine.DEFAULT_TIMEOUT_INTERVAL = 1000;
|
||||||
describe("IndexedDB", function() {
|
describe("IndexedDB", function() {
|
||||||
var ob = new Y.IndexedDB(null, {namespace: "Test"});
|
var ob;
|
||||||
|
beforeAll(function(){
|
||||||
|
ob = new Y.IndexedDB(null, {namespace: "Test"});
|
||||||
|
});
|
||||||
|
|
||||||
it("can add and get operation", function(done) {
|
it("can add and get operation", function(done) {
|
||||||
ob.requestTransaction(function*(){
|
ob.requestTransaction(function*(){
|
||||||
|
@ -33,6 +33,9 @@ Y.Memory = (function(){ //eslint-disable-line no-unused-vars
|
|||||||
n.val = op;
|
n.val = op;
|
||||||
return op;
|
return op;
|
||||||
}
|
}
|
||||||
|
*addOperation (op) {
|
||||||
|
this.os.add(op);
|
||||||
|
}
|
||||||
*getOperation (id) {
|
*getOperation (id) {
|
||||||
if (id == null) {
|
if (id == null) {
|
||||||
throw new Error("You must define id!");
|
throw new Error("You must define id!");
|
||||||
@ -97,21 +100,18 @@ Y.Memory = (function(){ //eslint-disable-line no-unused-vars
|
|||||||
}
|
}
|
||||||
*makeOperationReady (ss, op) {
|
*makeOperationReady (ss, op) {
|
||||||
// instead of ss, you could use currSS (a ss that increments when you add an operation)
|
// instead of ss, you could use currSS (a ss that increments when you add an operation)
|
||||||
if (op.right == null) {
|
|
||||||
return op;
|
|
||||||
}
|
|
||||||
var clock;
|
var clock;
|
||||||
var o = op;
|
var o = op;
|
||||||
while (o.right != null){
|
while (o.right != null){
|
||||||
// while unknown, go to the right
|
// while unknown, go to the right
|
||||||
o = yield* this.getOperation(o.right);
|
clock = ss[o.right[0]];
|
||||||
clock = ss[o.id[0]];
|
if (clock != null && o.right[1] < clock) {
|
||||||
if (clock != null && o.id[1] < clock) {
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
o = yield* this.getOperation(o.right);
|
||||||
}
|
}
|
||||||
op = copyObject(op);
|
op = copyObject(op);
|
||||||
op.right = (o == null) ? null : o.id;
|
op.right = o.right;
|
||||||
return op;
|
return op;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,8 @@
|
|||||||
|
|
||||||
|
function smaller (a, b) {
|
||||||
|
return a[0] < b[0] || (a[0] === b[0] && a[1] < b[1]);
|
||||||
|
}
|
||||||
|
|
||||||
class N {
|
class N {
|
||||||
// A created node is always red!
|
// A created node is always red!
|
||||||
constructor (val) {
|
constructor (val) {
|
||||||
@ -106,6 +110,7 @@ class N {
|
|||||||
class RBTree { //eslint-disable-line no-unused-vars
|
class RBTree { //eslint-disable-line no-unused-vars
|
||||||
constructor () {
|
constructor () {
|
||||||
this.root = null;
|
this.root = null;
|
||||||
|
this.length = 0;
|
||||||
}
|
}
|
||||||
findNodeWithLowerBound (from) {
|
findNodeWithLowerBound (from) {
|
||||||
var o = this.root;
|
var o = this.root;
|
||||||
@ -113,11 +118,11 @@ class RBTree { //eslint-disable-line no-unused-vars
|
|||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
while (true) {
|
while (true) {
|
||||||
if ((from === null || from < o.val.id) && o.left !== null) {
|
if ((from === null || smaller(from, o.val.id)) && o.left !== null) {
|
||||||
// o is included in the bound
|
// o is included in the bound
|
||||||
// try to find an element that is closer to the bound
|
// try to find an element that is closer to the bound
|
||||||
o = o.left;
|
o = o.left;
|
||||||
} else if (o.val.id < from) {
|
} else if (smaller(o.val.id, from)) {
|
||||||
// o is not within the bound, maybe one of the right elements is..
|
// o is not within the bound, maybe one of the right elements is..
|
||||||
if (o.right !== null) {
|
if (o.right !== null) {
|
||||||
o = o.right;
|
o = o.right;
|
||||||
@ -134,7 +139,7 @@ class RBTree { //eslint-disable-line no-unused-vars
|
|||||||
}
|
}
|
||||||
iterate (from, to, f) {
|
iterate (from, to, f) {
|
||||||
var o = this.findNodeWithLowerBound(from);
|
var o = this.findNodeWithLowerBound(from);
|
||||||
while ( o !== null && (to === null || o.val.id <= to) ) {
|
while ( o !== null && (to === null || smaller(o.val.id, to) || compareIds(o.val.id, to)) ) {
|
||||||
f(o.val);
|
f(o.val);
|
||||||
o = o.next();
|
o = o.next();
|
||||||
}
|
}
|
||||||
@ -152,9 +157,9 @@ class RBTree { //eslint-disable-line no-unused-vars
|
|||||||
if (o === null) {
|
if (o === null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (id < o.val.id) {
|
if (smaller(id, o.val.id)) {
|
||||||
o = o.left;
|
o = o.left;
|
||||||
} else if (o.val.id < id) {
|
} else if (smaller(o.val.id, id)) {
|
||||||
o = o.right;
|
o = o.right;
|
||||||
} else {
|
} else {
|
||||||
return o;
|
return o;
|
||||||
@ -164,6 +169,10 @@ class RBTree { //eslint-disable-line no-unused-vars
|
|||||||
}
|
}
|
||||||
delete (id) {
|
delete (id) {
|
||||||
var d = this.findNode(id);
|
var d = this.findNode(id);
|
||||||
|
if (d == null) {
|
||||||
|
throw new Error("Element does not exist!");
|
||||||
|
}
|
||||||
|
this.length--;
|
||||||
if (d.left !== null && d.right !== null) {
|
if (d.left !== null && d.right !== null) {
|
||||||
// switch d with the greates element in the left subtree.
|
// switch d with the greates element in the left subtree.
|
||||||
// o should have at most one child.
|
// o should have at most one child.
|
||||||
@ -302,14 +311,14 @@ class RBTree { //eslint-disable-line no-unused-vars
|
|||||||
if (this.root !== null) {
|
if (this.root !== null) {
|
||||||
var p = this.root; // p abbrev. parent
|
var p = this.root; // p abbrev. parent
|
||||||
while (true) {
|
while (true) {
|
||||||
if (node.val.id < p.val.id) {
|
if (smaller(node.val.id, p.val.id)) {
|
||||||
if (p.left === null) {
|
if (p.left === null) {
|
||||||
p.left = node;
|
p.left = node;
|
||||||
break;
|
break;
|
||||||
} else {
|
} else {
|
||||||
p = p.left;
|
p = p.left;
|
||||||
}
|
}
|
||||||
} else if (p.val.id < node.val.id) {
|
} else if (smaller(p.val.id, node.val.id)) {
|
||||||
if (p.right === null) {
|
if (p.right === null) {
|
||||||
p.right = node;
|
p.right = node;
|
||||||
break;
|
break;
|
||||||
@ -324,6 +333,7 @@ class RBTree { //eslint-disable-line no-unused-vars
|
|||||||
} else {
|
} else {
|
||||||
this.root = node;
|
this.root = node;
|
||||||
}
|
}
|
||||||
|
this.length++;
|
||||||
this.root.blacken();
|
this.root.blacken();
|
||||||
}
|
}
|
||||||
_fixInsert (n) {
|
_fixInsert (n) {
|
||||||
|
@ -184,22 +184,14 @@ var Struct = {
|
|||||||
var right = null;
|
var right = null;
|
||||||
parent = parent || (yield* this.getOperation(op.parent));
|
parent = parent || (yield* this.getOperation(op.parent));
|
||||||
|
|
||||||
// NOTE: You you have to call addOperation before you set any other operation!
|
|
||||||
|
|
||||||
// reconnect left and set right of op
|
// reconnect left and set right of op
|
||||||
if (op.left != null) {
|
if (op.left != null) {
|
||||||
left = yield* this.getOperation(op.left);
|
left = yield* this.getOperation(op.left);
|
||||||
op.right = left.right;
|
op.right = left.right;
|
||||||
if ((yield* this.addOperation(op)) === false) { // add here
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
left.right = op.id;
|
left.right = op.id;
|
||||||
yield* this.setOperation(left);
|
yield* this.setOperation(left);
|
||||||
} else {
|
} else {
|
||||||
op.right = op.parentSub ? (parent.map[op.parentSub] || null) : parent.start;
|
op.right = op.parentSub ? (parent.map[op.parentSub] || null) : parent.start;
|
||||||
if ((yield* this.addOperation(op)) === false) { // or here
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// reconnect right
|
// reconnect right
|
||||||
if (op.right != null) {
|
if (op.right != null) {
|
||||||
@ -260,9 +252,6 @@ var Struct = {
|
|||||||
execute: function* (op) {
|
execute: function* (op) {
|
||||||
op.start = null;
|
op.start = null;
|
||||||
op.end = null;
|
op.end = null;
|
||||||
if ((yield* this.addOperation(op)) === false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
ref: function* (op : Op, pos : number) : Insert {
|
ref: function* (op : Op, pos : number) : Insert {
|
||||||
if (op.start == null) {
|
if (op.start == null) {
|
||||||
@ -324,10 +313,7 @@ var Struct = {
|
|||||||
*/
|
*/
|
||||||
return [];
|
return [];
|
||||||
},
|
},
|
||||||
execute: function* (op) {
|
execute: function* () {
|
||||||
if ((yield* this.addOperation(op)) === false) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
get: function* (op, name) {
|
get: function* (op, name) {
|
||||||
var oid = op.map[name];
|
var oid = op.map[name];
|
||||||
@ -347,3 +333,4 @@ var Struct = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
Y.Struct = Struct;
|
||||||
|
@ -149,6 +149,51 @@
|
|||||||
observe (f) {
|
observe (f) {
|
||||||
this.eventHandler.addUserEventListener(f);
|
this.eventHandler.addUserEventListener(f);
|
||||||
}
|
}
|
||||||
|
unobserve (f) {
|
||||||
|
this.eventHandler.removeUserEventListener(f);
|
||||||
|
}
|
||||||
|
observePath (path, f) {
|
||||||
|
var self = this;
|
||||||
|
if (path.length === 0) {
|
||||||
|
this.observe(f);
|
||||||
|
return Promise.resolve(function(){
|
||||||
|
self.unobserve(f);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
var deleteChildObservers;
|
||||||
|
var resetObserverPath = function(){
|
||||||
|
var promise = self.get(path[0]);
|
||||||
|
if (!promise instanceof Promise) {
|
||||||
|
// its either not defined or a premitive value
|
||||||
|
promise = self.set(path[0], Y.Map);
|
||||||
|
}
|
||||||
|
return promise.then(function(map){
|
||||||
|
return map.observePath(path.slice(1), f);
|
||||||
|
}).then(function(_deleteChildObservers){
|
||||||
|
deleteChildObservers = _deleteChildObservers;
|
||||||
|
return Promise.resolve();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
var observer = function(events){
|
||||||
|
for (var e in events) {
|
||||||
|
var event = events[e];
|
||||||
|
if (event.name === path[0]) {
|
||||||
|
deleteChildObservers();
|
||||||
|
if (event.type === "add" || event.type === "update") {
|
||||||
|
resetObserverPath();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
self.observe(observer);
|
||||||
|
return resetObserverPath().then(
|
||||||
|
Promise.resolve(function(){
|
||||||
|
deleteChildObservers();
|
||||||
|
self.unobserve(observer);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
*_changed (transaction, op) {
|
*_changed (transaction, op) {
|
||||||
if (op.struct === "Delete") {
|
if (op.struct === "Delete") {
|
||||||
op.key = (yield* transaction.getOperation(op.target)).parentSub;
|
op.key = (yield* transaction.getOperation(op.target)).parentSub;
|
||||||
|
@ -0,0 +1,287 @@
|
|||||||
|
|
||||||
|
(function(){
|
||||||
|
class YTextBind extends Y.Array.class {
|
||||||
|
constructor (os, _model, idArray, valArray) {
|
||||||
|
super(os, _model, idArray, valArray);
|
||||||
|
this.textfields = [];
|
||||||
|
}
|
||||||
|
toString () {
|
||||||
|
return this.valArray.join("");
|
||||||
|
}
|
||||||
|
insert (pos, content) {
|
||||||
|
super(pos, content.split(""));
|
||||||
|
}
|
||||||
|
bind (textfield, domRoot) {
|
||||||
|
domRoot = domRoot || window; //eslint-disable-line
|
||||||
|
if (domRoot.getSelection == null) {
|
||||||
|
domRoot = window;//eslint-disable-line
|
||||||
|
}
|
||||||
|
|
||||||
|
// don't duplicate!
|
||||||
|
for (var t in this.textfields) {
|
||||||
|
if (this.textfields[t] === textfield) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var creatorToken = false;
|
||||||
|
|
||||||
|
var word = this;
|
||||||
|
textfield.value = this.toString();
|
||||||
|
this.textfields.push(textfield);
|
||||||
|
var createRange, writeRange, writeContent;
|
||||||
|
if(textfield.selectionStart != null && textfield.setSelectionRange != null) {
|
||||||
|
createRange = function (fix) {
|
||||||
|
var left = textfield.selectionStart;
|
||||||
|
var right = textfield.selectionEnd;
|
||||||
|
if (fix != null) {
|
||||||
|
left = fix(left);
|
||||||
|
right = fix(right);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
left: left,
|
||||||
|
right: right
|
||||||
|
};
|
||||||
|
};
|
||||||
|
writeRange = function (range) {
|
||||||
|
writeContent(word.toString());
|
||||||
|
textfield.setSelectionRange(range.left, range.right);
|
||||||
|
};
|
||||||
|
writeContent = function (content){
|
||||||
|
textfield.value = content;
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
createRange = function (fix) {
|
||||||
|
var range = {};
|
||||||
|
var s = domRoot.getSelection();
|
||||||
|
var clength = textfield.textContent.length;
|
||||||
|
range.left = Math.min(s.anchorOffset, clength);
|
||||||
|
range.right = Math.min(s.focusOffset, clength);
|
||||||
|
if(fix != null){
|
||||||
|
range.left = fix(range.left);
|
||||||
|
range.right = fix(range.right);
|
||||||
|
}
|
||||||
|
var editedElement = s.focusNode;
|
||||||
|
if(editedElement === textfield || editedElement === textfield.childNodes[0]){
|
||||||
|
range.isReal = true;
|
||||||
|
} else {
|
||||||
|
range.isReal = false;
|
||||||
|
}
|
||||||
|
return range;
|
||||||
|
};
|
||||||
|
|
||||||
|
writeRange = function (range) {
|
||||||
|
writeContent(word.val());
|
||||||
|
var textnode = textfield.childNodes[0];
|
||||||
|
if(range.isReal && textnode != null) {
|
||||||
|
if(range.left < 0){
|
||||||
|
range.left = 0;
|
||||||
|
}
|
||||||
|
range.right = Math.max(range.left, range.right);
|
||||||
|
if (range.right > textnode.length) {
|
||||||
|
range.right = textnode.length;
|
||||||
|
}
|
||||||
|
range.left = Math.min(range.left, range.right);
|
||||||
|
var r = document.createRange(); //eslint-disable-line
|
||||||
|
r.setStart(textnode, range.left);
|
||||||
|
r.setEnd(textnode, range.right);
|
||||||
|
var s = window.getSelection(); //eslint-disable-line
|
||||||
|
s.removeAllRanges();
|
||||||
|
s.addRange(r);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
writeContent = function (content) {
|
||||||
|
var contentArray = content.replace(new RegExp("\n", 'g')," ").split(" ");//eslint-disable-line
|
||||||
|
textfield.innerText = "";
|
||||||
|
for(var i in contentArray){
|
||||||
|
var c = contentArray[i];
|
||||||
|
textfield.innerText += c;
|
||||||
|
if(i !== contentArray.length - 1){
|
||||||
|
textfield.innerHTML += " ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
writeContent(this.toString());
|
||||||
|
|
||||||
|
this.observe(function (events) {
|
||||||
|
for(var e in events) {
|
||||||
|
var event = events[e];
|
||||||
|
if (!creatorToken) {
|
||||||
|
var oPos, fix;
|
||||||
|
if( event.type === "insert") {
|
||||||
|
oPos = event.index;
|
||||||
|
fix = function (cursor) {//eslint-disable-line
|
||||||
|
if (cursor <= oPos) {
|
||||||
|
return cursor;
|
||||||
|
} else {
|
||||||
|
cursor += 1;
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
var r = createRange(fix);
|
||||||
|
writeRange(r);
|
||||||
|
} else if (event.type === "delete") {
|
||||||
|
oPos = event.index;
|
||||||
|
fix = function (cursor){//eslint-disable-line
|
||||||
|
if (cursor < oPos) {
|
||||||
|
return cursor;
|
||||||
|
} else {
|
||||||
|
cursor -= 1;
|
||||||
|
return cursor;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
r = createRange(fix);
|
||||||
|
writeRange(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// consume all text-insert changes.
|
||||||
|
textfield.onkeypress = function (event) {
|
||||||
|
if (word.is_deleted) {
|
||||||
|
// if word is deleted, do not do anything ever again
|
||||||
|
textfield.onkeypress = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
creatorToken = true;
|
||||||
|
var char;
|
||||||
|
if (event.keyCode === 13) {
|
||||||
|
char = "\n";
|
||||||
|
} else if (event.key != null) {
|
||||||
|
if (event.charCode === 32) {
|
||||||
|
char = " ";
|
||||||
|
} else {
|
||||||
|
char = event.key;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
char = window.String.fromCharCode(event.keyCode); //eslint-disable-line
|
||||||
|
}
|
||||||
|
if (char.length > 1) {
|
||||||
|
return true;
|
||||||
|
} else if (char.length > 0) {
|
||||||
|
var r = createRange();
|
||||||
|
var pos = Math.min(r.left, r.right);
|
||||||
|
var diff = Math.abs(r.right - r.left);
|
||||||
|
word.delete(pos, diff);
|
||||||
|
word.insert(pos, char);
|
||||||
|
r.left = pos + char.length;
|
||||||
|
r.right = r.left;
|
||||||
|
writeRange(r);
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
creatorToken = false;
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
textfield.onpaste = function (event) {
|
||||||
|
if (word.is_deleted) {
|
||||||
|
// if word is deleted, do not do anything ever again
|
||||||
|
textfield.onpaste = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
};
|
||||||
|
textfield.oncut = function (event) {
|
||||||
|
if (word.is_deleted) {
|
||||||
|
// if word is deleted, do not do anything ever again
|
||||||
|
textfield.oncut = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
};
|
||||||
|
//
|
||||||
|
// consume deletes. Note that
|
||||||
|
// chrome: won't consume deletions on keypress event.
|
||||||
|
// keyCode is deprecated. BUT: I don't see another way.
|
||||||
|
// since event.key is not implemented in the current version of chrome.
|
||||||
|
// Every browser supports keyCode. Let's stick with it for now..
|
||||||
|
//
|
||||||
|
textfield.onkeydown = function (event) {
|
||||||
|
creatorToken = true;
|
||||||
|
if (word.is_deleted) {
|
||||||
|
// if word is deleted, do not do anything ever again
|
||||||
|
textfield.onkeydown = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
var r = createRange();
|
||||||
|
var pos = Math.min(r.left, r.right, word.toString().length);
|
||||||
|
var diff = Math.abs(r.left - r.right);
|
||||||
|
if (event.keyCode != null && event.keyCode === 8) { // Backspace
|
||||||
|
if (diff > 0) {
|
||||||
|
word.delete(pos, diff);
|
||||||
|
r.left = pos;
|
||||||
|
r.right = pos;
|
||||||
|
writeRange(r);
|
||||||
|
} else {
|
||||||
|
if (event.ctrlKey != null && event.ctrlKey) {
|
||||||
|
var val = word.toString();
|
||||||
|
var newPos = pos;
|
||||||
|
var delLength = 0;
|
||||||
|
if (pos > 0) {
|
||||||
|
newPos--;
|
||||||
|
delLength++;
|
||||||
|
}
|
||||||
|
while (newPos > 0 && val[newPos] !== " " && val[newPos] !== "\n") {
|
||||||
|
newPos--;
|
||||||
|
delLength++;
|
||||||
|
}
|
||||||
|
word.delete(newPos, pos - newPos);
|
||||||
|
r.left = newPos;
|
||||||
|
r.right = newPos;
|
||||||
|
writeRange(r);
|
||||||
|
} else {
|
||||||
|
if (pos > 0) {
|
||||||
|
word.delete(pos - 1, 1);
|
||||||
|
r.left = pos - 1;
|
||||||
|
r.right = pos - 1;
|
||||||
|
writeRange(r);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
creatorToken = false;
|
||||||
|
return false;
|
||||||
|
} else if (event.keyCode != null && event.keyCode === 46) { // Delete
|
||||||
|
if (diff > 0) {
|
||||||
|
word.delete(pos, diff);
|
||||||
|
r.left = pos;
|
||||||
|
r.right = pos;
|
||||||
|
writeRange(r);
|
||||||
|
} else {
|
||||||
|
word.delete(pos, 1);
|
||||||
|
r.left = pos;
|
||||||
|
r.right = pos;
|
||||||
|
writeRange(r);
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
creatorToken = false;
|
||||||
|
return false;
|
||||||
|
} else {
|
||||||
|
creatorToken = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Y.TextBind = new CustomType({
|
||||||
|
class: YTextBind,
|
||||||
|
createType: function* YTextBindCreator () {
|
||||||
|
var model = {
|
||||||
|
start: null,
|
||||||
|
end: null,
|
||||||
|
struct: "List",
|
||||||
|
type: "TextBind",
|
||||||
|
id: this.store.getNextOpId()
|
||||||
|
};
|
||||||
|
yield* this.applyCreatedOperations([model]);
|
||||||
|
return yield* this.createType(model);
|
||||||
|
},
|
||||||
|
initType: function* YTextBindInitializer(os, model){
|
||||||
|
var valArray = [];
|
||||||
|
var idArray = yield* Y.Struct.List.map.call(this, model, function(c){
|
||||||
|
valArray.push(c.content);
|
||||||
|
return JSON.stringify(c.id);
|
||||||
|
});
|
||||||
|
return new YTextBind(os, model.id, idArray, valArray);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})();
|
10
src/y.js
10
src/y.js
@ -2,8 +2,10 @@
|
|||||||
|
|
||||||
function Y (opts) {
|
function Y (opts) {
|
||||||
var def = Promise.defer();
|
var def = Promise.defer();
|
||||||
new YConfig(opts, function(config){ //eslint-disable-line
|
new YConfig(opts, function(yconfig){ //eslint-disable-line
|
||||||
def.resolve(config);
|
yconfig.db.whenUserIdSet(function(){
|
||||||
|
def.resolve(yconfig);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
return def.promise;
|
return def.promise;
|
||||||
}
|
}
|
||||||
@ -37,7 +39,3 @@ class YConfig { //eslint-disable-line no-unused-vars
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Y.AbstractTransaction = AbstractTransaction;
|
|
||||||
Y.AbstractOperationStore = AbstractOperationStore;
|
|
||||||
Y.Struct = Struct;
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user