Compare commits
	
		
			No commits in common. "main" and "experimental-connectors" have entirely different histories.
		
	
	
		
			main
			...
			experiment
		
	
		
							
								
								
									
										12
									
								
								.babelrc
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								.babelrc
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,12 @@
 | 
			
		||||
{
 | 
			
		||||
  "presets": [
 | 
			
		||||
    ["latest", {
 | 
			
		||||
      "es2015": {
 | 
			
		||||
        "modules": false
 | 
			
		||||
      }
 | 
			
		||||
    }]
 | 
			
		||||
  ],
 | 
			
		||||
  "plugins": [
 | 
			
		||||
    "external-helpers"
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								.esdoc.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								.esdoc.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,10 @@
 | 
			
		||||
{
 | 
			
		||||
  "source": "./src",
 | 
			
		||||
  "destination": "./docs",
 | 
			
		||||
  "plugins": [{
 | 
			
		||||
    "name": "esdoc-standard-plugin",
 | 
			
		||||
    "option": {
 | 
			
		||||
      "accessor": {"access": ["public"], "autoPrivate": true}
 | 
			
		||||
    }
 | 
			
		||||
  }]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										14
									
								
								.flowconfig
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								.flowconfig
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,14 @@
 | 
			
		||||
[ignore]
 | 
			
		||||
.*/node_modules/.*
 | 
			
		||||
.*/dist/.*
 | 
			
		||||
.*/build/.*
 | 
			
		||||
 | 
			
		||||
[include]
 | 
			
		||||
./src/
 | 
			
		||||
./tests-lib/
 | 
			
		||||
./test/
 | 
			
		||||
 | 
			
		||||
[libs]
 | 
			
		||||
./declarations/
 | 
			
		||||
 | 
			
		||||
[options]
 | 
			
		||||
							
								
								
									
										31
									
								
								.github/workflows/node.js.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										31
									
								
								.github/workflows/node.js.yml
									
									
									
									
										vendored
									
									
								
							@ -1,31 +0,0 @@
 | 
			
		||||
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
 | 
			
		||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
 | 
			
		||||
 | 
			
		||||
name: Node.js CI
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  push:
 | 
			
		||||
    branches: [ main ]
 | 
			
		||||
  pull_request:
 | 
			
		||||
    branches: [ main ]
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  build:
 | 
			
		||||
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
 | 
			
		||||
    strategy:
 | 
			
		||||
      matrix:
 | 
			
		||||
        node-version: [16.x, 20.x]
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
    - uses: actions/checkout@v4
 | 
			
		||||
    - name: Use Node.js ${{ matrix.node-version }}
 | 
			
		||||
      uses: actions/setup-node@v3
 | 
			
		||||
      with:
 | 
			
		||||
        node-version: ${{ matrix.node-version }}
 | 
			
		||||
    - run: npm ci
 | 
			
		||||
    - run: npm run lint
 | 
			
		||||
    - run: npm run test
 | 
			
		||||
      env:
 | 
			
		||||
        CI: true
 | 
			
		||||
							
								
								
									
										7
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@ -1,4 +1,7 @@
 | 
			
		||||
node_modules
 | 
			
		||||
dist
 | 
			
		||||
.vscode
 | 
			
		||||
bower_components
 | 
			
		||||
docs
 | 
			
		||||
/y.*
 | 
			
		||||
/examples/yjs-dist.js*
 | 
			
		||||
.vscode
 | 
			
		||||
.yjsPersisted
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										52
									
								
								.jsdoc.json
									
									
									
									
									
								
							
							
						
						
									
										52
									
								
								.jsdoc.json
									
									
									
									
									
								
							@ -1,52 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "sourceType": "module",
 | 
			
		||||
  "tags": {
 | 
			
		||||
    "allowUnknownTags": true,
 | 
			
		||||
    "dictionaries": ["jsdoc"]
 | 
			
		||||
  },
 | 
			
		||||
  "source": {
 | 
			
		||||
    "include": ["./src"],
 | 
			
		||||
    "includePattern": ".js$"
 | 
			
		||||
  },
 | 
			
		||||
  "plugins": [
 | 
			
		||||
    "plugins/markdown"
 | 
			
		||||
  ],
 | 
			
		||||
  "templates": {
 | 
			
		||||
    "referenceTitle": "Yjs",
 | 
			
		||||
    "disableSort": false,
 | 
			
		||||
    "useCollapsibles": true,
 | 
			
		||||
    "collapse": true,
 | 
			
		||||
    "resources": {
 | 
			
		||||
      "yjs.dev": "Website",
 | 
			
		||||
      "docs.yjs.dev": "Docs",
 | 
			
		||||
      "discuss.yjs.dev": "Forum",
 | 
			
		||||
      "https://gitter.im/Yjs/community": "Chat"
 | 
			
		||||
    },
 | 
			
		||||
    "logo": {
 | 
			
		||||
      "url": "https://yjs.dev/images/logo/yjs-512x512.png",
 | 
			
		||||
      "width": "162px",
 | 
			
		||||
      "height": "162px",
 | 
			
		||||
      "link": "/"
 | 
			
		||||
    },
 | 
			
		||||
    "tabNames": {
 | 
			
		||||
      "api": "API",
 | 
			
		||||
      "tutorials": "Examples"
 | 
			
		||||
    },
 | 
			
		||||
    "footerText": "Shared Editing",
 | 
			
		||||
    "css": [
 | 
			
		||||
      "./style.css"
 | 
			
		||||
    ],
 | 
			
		||||
    "default": {
 | 
			
		||||
      "staticFiles": {
 | 
			
		||||
          "include": []
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "opts": {
 | 
			
		||||
    "destination": "./docs/",
 | 
			
		||||
    "encoding": "utf8",
 | 
			
		||||
    "private": false,
 | 
			
		||||
    "recurse": true,
 | 
			
		||||
    "template": "./node_modules/tui-jsdoc-template"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -1,4 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "default": true,
 | 
			
		||||
  "no-inline-html": false
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										179
									
								
								INTERNALS.md
									
									
									
									
									
								
							
							
						
						
									
										179
									
								
								INTERNALS.md
									
									
									
									
									
								
							@ -1,179 +0,0 @@
 | 
			
		||||
# Yjs Internals
 | 
			
		||||
 | 
			
		||||
This document roughly explains how Yjs works internally. There is a complete
 | 
			
		||||
walkthrough of the Yjs codebase available as a recording:
 | 
			
		||||
https://youtu.be/0l5XgnQ6rB4
 | 
			
		||||
 | 
			
		||||
The Yjs CRDT algorithm is described in the [YATA
 | 
			
		||||
paper](https://www.researchgate.net/publication/310212186_Near_Real-Time_Peer-to-Peer_Shared_Editing_on_Extensible_Data_Types)
 | 
			
		||||
from 2016. For an algorithmic view of how it works, the paper is a reasonable
 | 
			
		||||
place to start. There are a handful of small improvements implemented in Yjs
 | 
			
		||||
which aren't described in the paper. The most notable is that items have an
 | 
			
		||||
`originRight` as well as an `origin` property, which improves performance when
 | 
			
		||||
many concurrent inserts happen after the same character.
 | 
			
		||||
 | 
			
		||||
At its heart, Yjs is a list CRDT. Everything is squeezed into a list in order to
 | 
			
		||||
reuse the CRDT resolution algorithm:
 | 
			
		||||
 | 
			
		||||
- Arrays are easy - they're lists of arbitrary items.
 | 
			
		||||
- Text is a list of characters, optionally punctuated by formatting markers and
 | 
			
		||||
  embeds for rich text support. Several characters can be wrapped in a single
 | 
			
		||||
linked list `Item` (this is also known as the compound representation of
 | 
			
		||||
CRDTs). More information about this in [this blog
 | 
			
		||||
article](https://blog.kevinjahns.de/are-crdts-suitable-for-shared-editing/).
 | 
			
		||||
- Maps are lists of entries. The last inserted entry for each key is used, and
 | 
			
		||||
  all other duplicates for each key are flagged as deleted.
 | 
			
		||||
 | 
			
		||||
Each client is assigned a unique *clientID* property on first insert. This is a
 | 
			
		||||
random 53-bit integer (53 bits because that fits in the javascript safe integer
 | 
			
		||||
range \[JavaScript uses IEEE 754 floats\]).
 | 
			
		||||
 | 
			
		||||
## List items
 | 
			
		||||
 | 
			
		||||
Each item in a Yjs list is made up of two objects:
 | 
			
		||||
 | 
			
		||||
- An `Item` (*src/structs/Item.js*). This is used to relate the item to other
 | 
			
		||||
  adjacent items.
 | 
			
		||||
- An object in the `AbstractType` hierarchy (subclasses of
 | 
			
		||||
  *src/types/AbstractType.js* - eg `YText`). This stores the actual content in
 | 
			
		||||
the Yjs document.
 | 
			
		||||
 | 
			
		||||
The item and type object pair have a 1-1 mapping. The item's `content` field
 | 
			
		||||
references the AbstractType object and the AbstractType object's `_item` field
 | 
			
		||||
references the item.
 | 
			
		||||
 | 
			
		||||
Everything inserted in a Yjs document is given a unique ID, formed from a
 | 
			
		||||
*ID(clientID, clock)* pair (also known as a [Lamport
 | 
			
		||||
Timestamp](https://en.wikipedia.org/wiki/Lamport_timestamp)). The clock counts
 | 
			
		||||
up from 0 with the first inserted character or item a client makes. This is
 | 
			
		||||
similar to automerge's operation IDs, but note that the clock is only
 | 
			
		||||
incremented by inserts. Deletes are handled in a very different way (see
 | 
			
		||||
below).
 | 
			
		||||
 | 
			
		||||
If a run of characters is inserted into a document (eg `"abc"`), the clock will
 | 
			
		||||
be incremented for each character (eg 3 times here). But Yjs will only add a
 | 
			
		||||
single `Item` into the list. This has no effect on the core CRDT algorithm, but
 | 
			
		||||
the optimization dramatically decreases the number of javascript objects
 | 
			
		||||
created during normal text editing. This optimization only applies if the
 | 
			
		||||
characters share the same clientID, they're inserted in order, and all
 | 
			
		||||
characters have either been deleted or all characters are not deleted. The item
 | 
			
		||||
will be split if the run is interrupted for any reason (eg a character in the
 | 
			
		||||
middle of the run is deleted).
 | 
			
		||||
 | 
			
		||||
When an item is created, it stores a reference to the IDs of the preceding and
 | 
			
		||||
succeeding item. These are stored in the item's `origin` and `originRight`
 | 
			
		||||
fields, respectively. These are used when peers concurrently insert at the same
 | 
			
		||||
location in a document. Though quite rare in practice, Yjs needs to make sure
 | 
			
		||||
the list items always resolve to the same order on all peers. The actual logic
 | 
			
		||||
is relatively simple - its only a couple dozen lines of code and it lives in
 | 
			
		||||
the `Item#integrate()` method. The YATA paper has much more detail on this
 | 
			
		||||
algorithm.
 | 
			
		||||
 | 
			
		||||
### Item Storage
 | 
			
		||||
 | 
			
		||||
The items themselves are stored in two data structures and a cache:
 | 
			
		||||
 | 
			
		||||
- The items are stored in a tree of doubly-linked lists in *document order*.
 | 
			
		||||
  Each item has `left` and `right` properties linking to its siblings in the
 | 
			
		||||
document. Items also have a `parent` property to reference their parent in the
 | 
			
		||||
document tree (null at the root). (And you can access an item's children, if
 | 
			
		||||
any, through `item.content`).
 | 
			
		||||
- All items are referenced in *insertion order* inside the struct store
 | 
			
		||||
  (*src/utils/StructStore.js*). This references the list of items inserted by
 | 
			
		||||
for each client, in chronological order. This is used to find an item in the
 | 
			
		||||
tree with a given ID (using a binary search). It is also used to efficiently
 | 
			
		||||
gather the operations a peer is missing during sync (more on this below).
 | 
			
		||||
 | 
			
		||||
When a local insert happens, Yjs needs to map the insert position in the
 | 
			
		||||
document (eg position 1000) to an ID. With just the linked list, this would
 | 
			
		||||
require a slow O(n) linear scan of the list. But when editing a document, most
 | 
			
		||||
inserts are either at the same position as the last insert, or nearby. To
 | 
			
		||||
improve performance, Yjs stores a cache of the 80 most recently looked up
 | 
			
		||||
insert positions in the document. This is consulted and updated when a position
 | 
			
		||||
is looked up to improve performance in the average case. The cache is updated
 | 
			
		||||
using a heuristic that is still changing (currently, it is updated when a new
 | 
			
		||||
position significantly diverges from existing markers in the cache). Internally
 | 
			
		||||
this is referred to as the skip list / fast search marker.
 | 
			
		||||
 | 
			
		||||
### Deletions
 | 
			
		||||
 | 
			
		||||
Deletions in Yjs are treated very differently from insertions. Insertions are
 | 
			
		||||
implemented as a sequential operation based CRDT, but deletions are treated as
 | 
			
		||||
a simpler state based CRDT.
 | 
			
		||||
 | 
			
		||||
When an item has been deleted by any peer, at any point in history, it is
 | 
			
		||||
flagged as deleted on the item. (Internally Yjs uses the `info` bitfield.) Yjs
 | 
			
		||||
does not record metadata about a deletion:
 | 
			
		||||
 | 
			
		||||
- No data is kept on *when* an item was deleted, or which user deleted it.
 | 
			
		||||
- The struct store does not contain deletion records
 | 
			
		||||
- The clientID's clock is not incremented
 | 
			
		||||
 | 
			
		||||
If garbage collection is enabled in Yjs, when an object is deleted its content
 | 
			
		||||
is discarded. If a deleted object contains children (eg a field is deleted in
 | 
			
		||||
an object), the content is replaced with a `GC` object (*src/structs/GC.js*).
 | 
			
		||||
This is a very lightweight structure - it only stores the length of the removed
 | 
			
		||||
content.
 | 
			
		||||
 | 
			
		||||
Yjs has some special logic to share which content in a document has been
 | 
			
		||||
deleted:
 | 
			
		||||
 | 
			
		||||
- When a delete happens, as well as marking the item, the deleted IDs are
 | 
			
		||||
  listed locally within the transaction. (See below for more information about
 | 
			
		||||
transactions.) When a transaction has been committed locally, the set of
 | 
			
		||||
deleted items is appended to a transaction's update message.
 | 
			
		||||
- A snapshot (a marked point in time in the Yjs history) is specified using
 | 
			
		||||
  both the set of (clientID, clock) pairs *and* the set of all deleted item
 | 
			
		||||
IDs. The deleted set is O(n), but because deletions usually happen in runs,
 | 
			
		||||
this data set is usually tiny in practice. (The real world editing trace from
 | 
			
		||||
the B4 benchmark document contains 182k inserts and 77k deleted characters. The
 | 
			
		||||
deleted set size in a snapshot is only 4.5Kb).
 | 
			
		||||
 | 
			
		||||
## Transactions
 | 
			
		||||
 | 
			
		||||
All updates in Yjs happen within a *transaction*. (Defined in
 | 
			
		||||
*src/utils/Transaction.js*.)
 | 
			
		||||
 | 
			
		||||
The transaction collects a set of updates to the Yjs document to be applied on
 | 
			
		||||
remote peers atomically. Once a transaction has been committed locally, it
 | 
			
		||||
generates a compressed *update message* which is broadcast to synchronized
 | 
			
		||||
remote peers to notify them of the local change. The update message contains:
 | 
			
		||||
 | 
			
		||||
- The set of newly inserted items
 | 
			
		||||
- The set of items deleted within the transaction.
 | 
			
		||||
 | 
			
		||||
## Network protocol
 | 
			
		||||
 | 
			
		||||
The network protocol is not really a part of Yjs. There are a few relevant
 | 
			
		||||
concepts that can be used to create a custom network protocol:
 | 
			
		||||
 | 
			
		||||
* `update`: The Yjs document can be encoded to an *update* object that can be
 | 
			
		||||
  parsed to reconstruct the document. Also every change on the document fires
 | 
			
		||||
an incremental document update that allows clients to sync with each other.
 | 
			
		||||
The update object is a Uint8Array that efficiently encodes `Item` objects and
 | 
			
		||||
the delete set.
 | 
			
		||||
* `state vector`: A state vector defines the known state of each user (a set of
 | 
			
		||||
  tuples `(client, clock)`). This object is also efficiently encoded as a
 | 
			
		||||
Uint8Array.
 | 
			
		||||
 | 
			
		||||
The client can ask a remote client for missing document updates by sending
 | 
			
		||||
their state vector (often referred to as *sync step 1*). The remote peer can
 | 
			
		||||
compute the missing `Item` objects using the `clocks` of the respective clients
 | 
			
		||||
and compute a minimal update message that reflects all missing updates (sync
 | 
			
		||||
step 2).
 | 
			
		||||
 | 
			
		||||
An implementation of the syncing process is in
 | 
			
		||||
[y-protocols](https://github.com/yjs/y-protocols).
 | 
			
		||||
 | 
			
		||||
## Snapshots
 | 
			
		||||
 | 
			
		||||
A snapshot can be used to restore an old document state. It is a `state vector`
 | 
			
		||||
\+ `delete set`. A client can restore an old document state by iterating through
 | 
			
		||||
the sequence CRDT and ignoring all Items that have an `id.clock >
 | 
			
		||||
stateVector[id.client].clock`. Instead of using `item.deleted` the client will
 | 
			
		||||
use the delete set to find out if an item was deleted or not.
 | 
			
		||||
 | 
			
		||||
It is not recommended to restore an old document state using snapshots,
 | 
			
		||||
although that would certainly be possible. Instead, the old state should be
 | 
			
		||||
computed by iterating through the newest state and using the additional
 | 
			
		||||
information from the state vector.
 | 
			
		||||
							
								
								
									
										4
									
								
								LICENSE
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								LICENSE
									
									
									
									
									
								
							@ -1,7 +1,7 @@
 | 
			
		||||
The MIT License (MIT)
 | 
			
		||||
 | 
			
		||||
Copyright (c) 2023
 | 
			
		||||
  - Kevin Jahns <kevin.jahns@protonmail.com>.
 | 
			
		||||
Copyright (c) 2014
 | 
			
		||||
  - Kevin Jahns <kevin.jahns@rwth-aachen.de>.
 | 
			
		||||
  - Chair of Computer Science 5 (Databases & Information Systems), RWTH Aachen University, Germany
 | 
			
		||||
 | 
			
		||||
Permission is hereby granted, free of charge, to any person obtaining a copy
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										33
									
								
								examples/ace/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								examples/ace/index.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,33 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html>
 | 
			
		||||
  <head>
 | 
			
		||||
    <style type="text/css" media="screen">
 | 
			
		||||
    #aceContainer {
 | 
			
		||||
        position: absolute;
 | 
			
		||||
        top: 0;
 | 
			
		||||
        right: 0;
 | 
			
		||||
        bottom: 0;
 | 
			
		||||
        left: 0;
 | 
			
		||||
    }
 | 
			
		||||
    .inserted {
 | 
			
		||||
      position:absolute;
 | 
			
		||||
      z-index:20;
 | 
			
		||||
      background-color: #FFC107;
 | 
			
		||||
    }
 | 
			
		||||
    .deleted {
 | 
			
		||||
      position:absolute;
 | 
			
		||||
      z-index:20;
 | 
			
		||||
      background-color: #FFC107;
 | 
			
		||||
    }
 | 
			
		||||
    </style>
 | 
			
		||||
  </head>
 | 
			
		||||
  <body>
 | 
			
		||||
 | 
			
		||||
    <div id="aceContainer"></div>
 | 
			
		||||
    <script src="../../y.js"></script>
 | 
			
		||||
    <script src='../../../y-websockets-client/y-websockets-client.js'></script>
 | 
			
		||||
    <script src="../bower_components/ace-builds/src/ace.js"></script>
 | 
			
		||||
 | 
			
		||||
    <script src="./index.js"></script>
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										17
									
								
								examples/ace/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								examples/ace/index.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,17 @@
 | 
			
		||||
/* global Y, ace */
 | 
			
		||||
 | 
			
		||||
let y = new Y('ace-example', {
 | 
			
		||||
  connector: {
 | 
			
		||||
    name: 'websockets-client',
 | 
			
		||||
    url: 'http://127.0.0.1:1234'
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
window.yAce = y
 | 
			
		||||
 | 
			
		||||
// bind the textarea to a shared text element
 | 
			
		||||
var editor = ace.edit('aceContainer')
 | 
			
		||||
editor.setTheme('ace/theme/chrome')
 | 
			
		||||
editor.getSession().setMode('ace/mode/javascript')
 | 
			
		||||
 | 
			
		||||
y.define('ace', Y.Text).bindAce(editor)
 | 
			
		||||
							
								
								
									
										19
									
								
								examples/bower.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								examples/bower.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,19 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "yjs-examples",
 | 
			
		||||
  "version": "0.0.0",
 | 
			
		||||
  "homepage": "y-js.org",
 | 
			
		||||
  "authors": [
 | 
			
		||||
    "Kevin Jahns <kevin.jahns@rwth-aachen.de>"
 | 
			
		||||
  ],
 | 
			
		||||
  "description": "Examples for Yjs",
 | 
			
		||||
  "license": "MIT",
 | 
			
		||||
  "ignore": [],
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "quill": "^1.0.0-rc.2",
 | 
			
		||||
    "ace": "~1.2.3",
 | 
			
		||||
    "ace-builds": "~1.2.3",
 | 
			
		||||
    "jquery": "~2.2.2",
 | 
			
		||||
    "d3": "^3.5.16",
 | 
			
		||||
    "codemirror": "^5.25.0"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										19
									
								
								examples/chat/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								examples/chat/index.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,19 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html>
 | 
			
		||||
  <body>
 | 
			
		||||
    <style>
 | 
			
		||||
      #chat p span {
 | 
			
		||||
        color: blue;
 | 
			
		||||
      }
 | 
			
		||||
    </style>
 | 
			
		||||
    <div id="chat"></div>
 | 
			
		||||
    <form id="chatform">
 | 
			
		||||
      <input name="username" type="text" style="width:15%;">
 | 
			
		||||
      <input name="message" type="text" style="width:60%;">
 | 
			
		||||
      <input type="submit" value="Send">
 | 
			
		||||
    </form>
 | 
			
		||||
    <script src="../../y.js"></script>
 | 
			
		||||
    <script src='../../../y-websockets-client/y-websockets-client.js'></script>
 | 
			
		||||
    <script src="./index.js"></script>
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										65
									
								
								examples/chat/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								examples/chat/index.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,65 @@
 | 
			
		||||
/* global Y */
 | 
			
		||||
 | 
			
		||||
let y = new Y('chat-example', {
 | 
			
		||||
  connector: {
 | 
			
		||||
    name: 'websockets-client',
 | 
			
		||||
    url: 'http://127.0.0.1:1234'
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
window.yChat = y
 | 
			
		||||
 | 
			
		||||
let chatprotocol = y.define('chatprotocol', Y.Array)
 | 
			
		||||
 | 
			
		||||
let chatcontainer = document.querySelector('#chat')
 | 
			
		||||
 | 
			
		||||
// This functions inserts a message at the specified position in the DOM
 | 
			
		||||
function appendMessage (message, position) {
 | 
			
		||||
  var p = document.createElement('p')
 | 
			
		||||
  var uname = document.createElement('span')
 | 
			
		||||
  uname.appendChild(document.createTextNode(message.username + ': '))
 | 
			
		||||
  p.appendChild(uname)
 | 
			
		||||
  p.appendChild(document.createTextNode(message.message))
 | 
			
		||||
  chatcontainer.insertBefore(p, chatcontainer.children[position] || null)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// This function makes sure that only 7 messages exist in the chat history.
 | 
			
		||||
// The rest is deleted
 | 
			
		||||
function cleanupChat () {
 | 
			
		||||
  if (chatprotocol.length > 7) {
 | 
			
		||||
    chatprotocol.delete(0, chatprotocol.length - 7)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
cleanupChat()
 | 
			
		||||
 | 
			
		||||
// Insert the initial content
 | 
			
		||||
chatprotocol.toArray().forEach(appendMessage)
 | 
			
		||||
 | 
			
		||||
// whenever content changes, make sure to reflect the changes in the DOM
 | 
			
		||||
chatprotocol.observe(function (event) {
 | 
			
		||||
  // concurrent insertions may result in a history > 7, so cleanup here
 | 
			
		||||
  cleanupChat()
 | 
			
		||||
  chatcontainer.innerHTML = ''
 | 
			
		||||
  chatprotocol.toArray().forEach(appendMessage)
 | 
			
		||||
})
 | 
			
		||||
document.querySelector('#chatform').onsubmit = function (event) {
 | 
			
		||||
  // the form is submitted
 | 
			
		||||
  var message = {
 | 
			
		||||
    username: this.querySelector('[name=username]').value,
 | 
			
		||||
    message: this.querySelector('[name=message]').value
 | 
			
		||||
  }
 | 
			
		||||
  if (message.username.length > 0 && message.message.length > 0) {
 | 
			
		||||
    if (chatprotocol.length > 6) {
 | 
			
		||||
      // If we are goint to insert the 8th element, make sure to delete first.
 | 
			
		||||
      chatprotocol.delete(0)
 | 
			
		||||
    }
 | 
			
		||||
    // Here we insert a message in the shared chat type.
 | 
			
		||||
    // This will call the observe function (see line 40)
 | 
			
		||||
    // and reflect the change in the DOM
 | 
			
		||||
    chatprotocol.push([message])
 | 
			
		||||
    this.querySelector('[name=message]').value = ''
 | 
			
		||||
  }
 | 
			
		||||
  // Do not send this form!
 | 
			
		||||
  event.preventDefault()
 | 
			
		||||
  return false
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										24
									
								
								examples/codemirror/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								examples/codemirror/index.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,24 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html>
 | 
			
		||||
  <head>
 | 
			
		||||
  </head>
 | 
			
		||||
  <body>
 | 
			
		||||
    <div id="codeMirrorContainer"></div>
 | 
			
		||||
 | 
			
		||||
    <script src="../../y.js"></script>
 | 
			
		||||
    <script src='../../../y-websockets-client/y-websockets-client.js'></script>
 | 
			
		||||
    <script src="../bower_components/codemirror/lib/codemirror.js"></script>
 | 
			
		||||
    <script src="../bower_components/codemirror/mode/javascript/javascript.js"></script>
 | 
			
		||||
    <link rel="stylesheet" href="../bower_components/codemirror/lib/codemirror.css">
 | 
			
		||||
    <style>
 | 
			
		||||
      .CodeMirror {
 | 
			
		||||
        position: fixed;
 | 
			
		||||
        top: 0;
 | 
			
		||||
        left: 0;
 | 
			
		||||
        height: 100%;
 | 
			
		||||
        width: 100%;
 | 
			
		||||
      }
 | 
			
		||||
    </style>
 | 
			
		||||
    <script src="./index.js"></script>
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										16
									
								
								examples/codemirror/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								examples/codemirror/index.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,16 @@
 | 
			
		||||
/* global Y, CodeMirror */
 | 
			
		||||
 | 
			
		||||
let y = new Y('codemirror-example', {
 | 
			
		||||
  connector: {
 | 
			
		||||
    name: 'websockets-client',
 | 
			
		||||
    url: 'http://127.0.0.1:1234'
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
window.yCodeMirror = y
 | 
			
		||||
 | 
			
		||||
var editor = CodeMirror(document.querySelector('#codeMirrorContainer'), {
 | 
			
		||||
  mode: 'javascript',
 | 
			
		||||
  lineNumbers: true
 | 
			
		||||
})
 | 
			
		||||
y.define('codemirror', Y.Text).bindCodeMirror(editor)
 | 
			
		||||
							
								
								
									
										20
									
								
								examples/drawing/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								examples/drawing/index.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,20 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html>
 | 
			
		||||
<body>
 | 
			
		||||
  <style>
 | 
			
		||||
    path {
 | 
			
		||||
      fill: none;
 | 
			
		||||
      stroke: blue;
 | 
			
		||||
      stroke-width: 1px;
 | 
			
		||||
      stroke-linejoin: round;
 | 
			
		||||
      stroke-linecap: round;
 | 
			
		||||
    }
 | 
			
		||||
  </style>
 | 
			
		||||
  <button type="button" id="clearDrawingCanvas">Clear Drawing</button>
 | 
			
		||||
  <svg id="drawingCanvas" viewbox="0 0 100 100" width="100%"></svg>
 | 
			
		||||
  <script src="../../y.js"></script>
 | 
			
		||||
  <script src='../../../y-websockets-client/y-websockets-client.js'></script>
 | 
			
		||||
  <script src="../bower_components/d3/d3.min.js"></script>
 | 
			
		||||
  <script src="./index.js"></script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										74
									
								
								examples/drawing/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								examples/drawing/index.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,74 @@
 | 
			
		||||
/* globals Y, d3 */
 | 
			
		||||
 | 
			
		||||
let y = new Y('drawing-example', {
 | 
			
		||||
  connector: {
 | 
			
		||||
    name: 'websockets-client',
 | 
			
		||||
    url: 'http://127.0.0.1:1234'
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
window.yDrawing = y
 | 
			
		||||
var drawing = y.define('drawing', Y.Array)
 | 
			
		||||
var renderPath = d3.svg.line()
 | 
			
		||||
  .x(function (d) { return d[0] })
 | 
			
		||||
  .y(function (d) { return d[1] })
 | 
			
		||||
  .interpolate('basic')
 | 
			
		||||
 | 
			
		||||
var svg = d3.select('#drawingCanvas')
 | 
			
		||||
  .call(d3.behavior.drag()
 | 
			
		||||
    .on('dragstart', dragstart)
 | 
			
		||||
    .on('drag', drag)
 | 
			
		||||
    .on('dragend', dragend))
 | 
			
		||||
 | 
			
		||||
// create line from a shared array object and update the line when the array changes
 | 
			
		||||
function drawLine (yarray) {
 | 
			
		||||
  var line = svg.append('path').datum(yarray.toArray())
 | 
			
		||||
  line.attr('d', renderPath)
 | 
			
		||||
  yarray.observe(function (event) {
 | 
			
		||||
    line.remove()
 | 
			
		||||
    line = svg.append('path').datum(yarray.toArray())
 | 
			
		||||
    line.attr('d', renderPath)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
// call drawLine every time an array is appended
 | 
			
		||||
drawing.observe(function (event) {
 | 
			
		||||
  event.removedElements.forEach(function () {
 | 
			
		||||
    // if one is deleted, all will be deleted!!
 | 
			
		||||
    svg.selectAll('path').remove()
 | 
			
		||||
  })
 | 
			
		||||
  event.addedElements.forEach(function (path) {
 | 
			
		||||
    drawLine(path)
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
// draw all existing content
 | 
			
		||||
for (var i = 0; i < drawing.length; i++) {
 | 
			
		||||
  drawLine(drawing.get(i))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// clear canvas on request
 | 
			
		||||
document.querySelector('#clearDrawingCanvas').onclick = function () {
 | 
			
		||||
  drawing.delete(0, drawing.length)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var sharedLine = null
 | 
			
		||||
function dragstart () {
 | 
			
		||||
  drawing.insert(drawing.length, [Y.Array])
 | 
			
		||||
  sharedLine = drawing.get(drawing.length - 1)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// After one dragged event is recognized, we ignore them for 33ms.
 | 
			
		||||
var ignoreDrag = null
 | 
			
		||||
function drag () {
 | 
			
		||||
  if (sharedLine != null && ignoreDrag == null) {
 | 
			
		||||
    ignoreDrag = window.setTimeout(function () {
 | 
			
		||||
      ignoreDrag = null
 | 
			
		||||
    }, 10)
 | 
			
		||||
    sharedLine.push([d3.mouse(this)])
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function dragend () {
 | 
			
		||||
  sharedLine = null
 | 
			
		||||
  window.clearTimeout(ignoreDrag)
 | 
			
		||||
  ignoreDrag = null
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										36
									
								
								examples/html-editor-drawing-hook/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								examples/html-editor-drawing-hook/index.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,36 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html>
 | 
			
		||||
</head>
 | 
			
		||||
  <script src="../../y.js"></script>
 | 
			
		||||
  <script src='../../../y-websockets-client/y-websockets-client.js'></script>
 | 
			
		||||
  <script src="../bower_components/d3/d3.min.js"></script>
 | 
			
		||||
  <script src="./index.js"></script>
 | 
			
		||||
  <style>
 | 
			
		||||
    magic-drawing .drawingCanvas path {
 | 
			
		||||
      fill: none;
 | 
			
		||||
      stroke: blue;
 | 
			
		||||
      stroke-width: 2px;
 | 
			
		||||
      stroke-linejoin: round;
 | 
			
		||||
      stroke-linecap: round;
 | 
			
		||||
    }
 | 
			
		||||
    magic-drawing .drawingCanvas {
 | 
			
		||||
      width: 500px;
 | 
			
		||||
      height: 500px;
 | 
			
		||||
      cursor: default;
 | 
			
		||||
      padding:1px;
 | 
			
		||||
      border:1px solid #021a40;
 | 
			
		||||
    }
 | 
			
		||||
    magic-drawing .clearDrawingButton {
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      top: 0;
 | 
			
		||||
      left: 0;
 | 
			
		||||
    }
 | 
			
		||||
    magic-drawing {
 | 
			
		||||
      position: relative;
 | 
			
		||||
      display: block;
 | 
			
		||||
    }
 | 
			
		||||
  </style>
 | 
			
		||||
</head>
 | 
			
		||||
<body contenteditable="true">
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										134
									
								
								examples/html-editor-drawing-hook/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										134
									
								
								examples/html-editor-drawing-hook/index.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,134 @@
 | 
			
		||||
/* global Y, d3 */
 | 
			
		||||
 | 
			
		||||
const hooks = {
 | 
			
		||||
  'magic-drawing': {
 | 
			
		||||
    fillType: function (dom, type) {
 | 
			
		||||
      initDrawingBindings(type, dom)
 | 
			
		||||
    },
 | 
			
		||||
    createDom: function (type) {
 | 
			
		||||
      const dom = document.createElement('magic-drawing')
 | 
			
		||||
      initDrawingBindings(type, dom)
 | 
			
		||||
      return dom
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
window.onload = function () {
 | 
			
		||||
  window.domBinding = new Y.DomBinding(window.yXmlType, document.body, { hooks })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
window.addMagicDrawing = function addMagicDrawing () {
 | 
			
		||||
  let mt = document.createElement('magic-drawing')
 | 
			
		||||
  mt.setAttribute('data-yjs-hook', 'magic-drawing')
 | 
			
		||||
  document.body.append(mt)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var renderPath = d3.svg.line()
 | 
			
		||||
  .x(function (d) { return d[0] })
 | 
			
		||||
  .y(function (d) { return d[1] })
 | 
			
		||||
  .interpolate('basic')
 | 
			
		||||
 | 
			
		||||
function initDrawingBindings (type, dom) {
 | 
			
		||||
  dom.contentEditable = 'false'
 | 
			
		||||
  dom.setAttribute('data-yjs-hook', 'magic-drawing')
 | 
			
		||||
  var drawing = type.get('drawing')
 | 
			
		||||
  if (drawing === undefined) {
 | 
			
		||||
    drawing = type.set('drawing', new Y.Array())
 | 
			
		||||
  }
 | 
			
		||||
  var canvas = dom.querySelector('.drawingCanvas')
 | 
			
		||||
  if (canvas == null) {
 | 
			
		||||
    canvas = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
 | 
			
		||||
    canvas.setAttribute('class', 'drawingCanvas')
 | 
			
		||||
    canvas.setAttribute('viewbox', '0 0 100 100')
 | 
			
		||||
    dom.insertBefore(canvas, null)
 | 
			
		||||
  }
 | 
			
		||||
  var clearDrawingButton = dom.querySelector('.clearDrawingButton')
 | 
			
		||||
  if (clearDrawingButton == null) {
 | 
			
		||||
    clearDrawingButton = document.createElement('button')
 | 
			
		||||
    clearDrawingButton.setAttribute('type', 'button')
 | 
			
		||||
    clearDrawingButton.setAttribute('class', 'clearDrawingButton')
 | 
			
		||||
    clearDrawingButton.innerText = 'Clear Drawing'
 | 
			
		||||
    dom.insertBefore(clearDrawingButton, null)
 | 
			
		||||
  }
 | 
			
		||||
  var svg = d3.select(canvas)
 | 
			
		||||
    .call(d3.behavior.drag()
 | 
			
		||||
      .on('dragstart', dragstart)
 | 
			
		||||
      .on('drag', drag)
 | 
			
		||||
      .on('dragend', dragend))
 | 
			
		||||
  // create line from a shared array object and update the line when the array changes
 | 
			
		||||
  function drawLine (yarray, svg) {
 | 
			
		||||
    var line = svg.append('path').datum(yarray.toArray())
 | 
			
		||||
    line.attr('d', renderPath)
 | 
			
		||||
    yarray.observe(function (event) {
 | 
			
		||||
      line.remove()
 | 
			
		||||
      line = svg.append('path').datum(yarray.toArray())
 | 
			
		||||
      line.attr('d', renderPath)
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
  // call drawLine every time an array is appended
 | 
			
		||||
  drawing.observe(function (event) {
 | 
			
		||||
    event.removedElements.forEach(function () {
 | 
			
		||||
      // if one is deleted, all will be deleted!!
 | 
			
		||||
      svg.selectAll('path').remove()
 | 
			
		||||
    })
 | 
			
		||||
    event.addedElements.forEach(function (path) {
 | 
			
		||||
      drawLine(path, svg)
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
  // draw all existing content
 | 
			
		||||
  for (var i = 0; i < drawing.length; i++) {
 | 
			
		||||
    drawLine(drawing.get(i), svg)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // clear canvas on request
 | 
			
		||||
  clearDrawingButton.onclick = function () {
 | 
			
		||||
    drawing.delete(0, drawing.length)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  var sharedLine = null
 | 
			
		||||
  function dragstart () {
 | 
			
		||||
    drawing.insert(drawing.length, [Y.Array])
 | 
			
		||||
    sharedLine = drawing.get(drawing.length - 1)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // After one dragged event is recognized, we ignore them for 33ms.
 | 
			
		||||
  var ignoreDrag = null
 | 
			
		||||
  function drag () {
 | 
			
		||||
    if (sharedLine != null && ignoreDrag == null) {
 | 
			
		||||
      ignoreDrag = window.setTimeout(function () {
 | 
			
		||||
        ignoreDrag = null
 | 
			
		||||
      }, 10)
 | 
			
		||||
      sharedLine.push([d3.mouse(this)])
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function dragend () {
 | 
			
		||||
    sharedLine = null
 | 
			
		||||
    window.clearTimeout(ignoreDrag)
 | 
			
		||||
    ignoreDrag = null
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let y = new Y('html-editor-drawing-hook-example', {
 | 
			
		||||
  connector: {
 | 
			
		||||
    name: 'websockets-client',
 | 
			
		||||
    url: 'http://127.0.0.1:1234'
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
window.yXml = y
 | 
			
		||||
window.yXmlType = y.define('xml', Y.XmlFragment)
 | 
			
		||||
window.undoManager = new Y.utils.UndoManager(window.yXmlType, {
 | 
			
		||||
  captureTimeout: 500
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
document.onkeydown = function interceptUndoRedo (e) {
 | 
			
		||||
  if (e.keyCode === 90 && e.metaKey) {
 | 
			
		||||
    if (!e.shiftKey) {
 | 
			
		||||
      window.undoManager.undo()
 | 
			
		||||
    } else {
 | 
			
		||||
      window.undoManager.redo()
 | 
			
		||||
    }
 | 
			
		||||
    e.preventDefault()
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										11
									
								
								examples/html-editor/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								examples/html-editor/index.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,11 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html>
 | 
			
		||||
</head>
 | 
			
		||||
  <script src="./index.mjs" type="module"></script>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
    <label for="room">Room: </label>
 | 
			
		||||
    <input type="text" id="room" name="room">
 | 
			
		||||
    <div id="content" contenteditable style="position:absolute;top:35px;left:0;right:0;bottom:0;outline: 0px solid transparent;"></div>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										77
									
								
								examples/html-editor/index.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										77
									
								
								examples/html-editor/index.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,77 @@
 | 
			
		||||
 | 
			
		||||
import YWebsocketsConnector from '../../src/Connectors/WebsocketsConnector/WebsocketsConnector.mjs'
 | 
			
		||||
import Y from '../../src/Y.mjs'
 | 
			
		||||
import DomBinding from '../../src/Bindings/DomBinding/DomBinding.mjs'
 | 
			
		||||
import UndoManager from '../../src/Util/UndoManager.mjs'
 | 
			
		||||
import YXmlFragment from '../../src/Types/YXml/YXmlFragment.mjs'
 | 
			
		||||
import YXmlText from '../../src/Types/YXml/YXmlText.mjs'
 | 
			
		||||
import YXmlElement from '../../src/Types/YXml/YXmlElement.mjs'
 | 
			
		||||
import YIndexdDBPersistence from '../../src/Persistences/IndexedDBPersistence.mjs'
 | 
			
		||||
 | 
			
		||||
const connector = new YWebsocketsConnector()
 | 
			
		||||
const persistence = new YIndexdDBPersistence()
 | 
			
		||||
 | 
			
		||||
const roomInput = document.querySelector('#room')
 | 
			
		||||
 | 
			
		||||
let currentRoomName = null
 | 
			
		||||
let y = null
 | 
			
		||||
let domBinding = null
 | 
			
		||||
 | 
			
		||||
function setRoomName (roomName) {
 | 
			
		||||
  if (currentRoomName !== roomName) {
 | 
			
		||||
    console.log(`change room: "${roomName}"`)
 | 
			
		||||
    roomInput.value = roomName
 | 
			
		||||
    currentRoomName = roomName
 | 
			
		||||
    location.hash = '#' + roomName
 | 
			
		||||
    if (y !== null) {
 | 
			
		||||
      domBinding.destroy()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const room = connector._rooms.get(roomName)
 | 
			
		||||
    if (room !== undefined) {
 | 
			
		||||
      y = room.y
 | 
			
		||||
    } else {
 | 
			
		||||
      y = new Y(roomName, null, null, { gc: true })
 | 
			
		||||
      persistence.connectY(roomName, y).then(() => {
 | 
			
		||||
        // connect after persisted content was applied to y
 | 
			
		||||
        // If we don't wait for persistence, the other peer will send all data, waisting
 | 
			
		||||
        // network bandwidth..
 | 
			
		||||
        connector.connectY(roomName, y)
 | 
			
		||||
      })
 | 
			
		||||
      window.y = y
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    window.y = y
 | 
			
		||||
    window.yXmlType = y.define('xml', YXmlFragment)
 | 
			
		||||
 | 
			
		||||
    domBinding = new DomBinding(window.yXmlType, document.querySelector('#content'), { scrollingElement: document.scrollingElement })
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
window.setRoomName = setRoomName
 | 
			
		||||
 | 
			
		||||
window.createRooms = function (i = 0) {
 | 
			
		||||
  setInterval(function () {
 | 
			
		||||
    setRoomName(i + '')
 | 
			
		||||
    i++
 | 
			
		||||
    const nodes = []
 | 
			
		||||
    for (let j = 0; j < 100; j++) {
 | 
			
		||||
      const node = new YXmlElement('p')
 | 
			
		||||
      node.insert(0, [new YXmlText(`This is the ${i}th paragraph of room ${i}`)])
 | 
			
		||||
      nodes.push(node)
 | 
			
		||||
    }
 | 
			
		||||
    y.share.xml.insert(0, nodes)
 | 
			
		||||
  }, 100)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
connector.syncPersistence(persistence)
 | 
			
		||||
 | 
			
		||||
window.connector = connector
 | 
			
		||||
window.persistence = persistence
 | 
			
		||||
 | 
			
		||||
window.onload = function () {
 | 
			
		||||
  setRoomName((location.hash || '#default').slice(1))
 | 
			
		||||
  roomInput.addEventListener('input', e => {
 | 
			
		||||
    const roomName = e.target.value
 | 
			
		||||
    setRoomName(roomName)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										24
									
								
								examples/indexeddb/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								examples/indexeddb/index.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,24 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html>
 | 
			
		||||
  <head>
 | 
			
		||||
  </head>
 | 
			
		||||
  <body>
 | 
			
		||||
    <div id="codeMirrorContainer"></div>
 | 
			
		||||
    <script src="../bower_components/codemirror/lib/codemirror.js"></script>
 | 
			
		||||
    <script src="../bower_components/codemirror/mode/javascript/javascript.js"></script>
 | 
			
		||||
    <link rel="stylesheet" href="../bower_components/codemirror/lib/codemirror.css">
 | 
			
		||||
    <style>
 | 
			
		||||
      .CodeMirror {
 | 
			
		||||
        position: fixed;
 | 
			
		||||
        top: 0;
 | 
			
		||||
        left: 0;
 | 
			
		||||
        height: 100%;
 | 
			
		||||
        width: 100%;
 | 
			
		||||
      }
 | 
			
		||||
    </style>
 | 
			
		||||
    <script src="../../y.js"></script>
 | 
			
		||||
    <script src='../../../y-websockets-client/y-websockets-client.js'></script>
 | 
			
		||||
    <script src='../../../y-indexeddb/y-indexeddb.js'></script>
 | 
			
		||||
    <script src="./index.js"></script>
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										19
									
								
								examples/indexeddb/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								examples/indexeddb/index.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,19 @@
 | 
			
		||||
/* global Y, CodeMirror */
 | 
			
		||||
 | 
			
		||||
const persistence = new Y.IndexedDB()
 | 
			
		||||
const connector = {
 | 
			
		||||
  connector: {
 | 
			
		||||
    name: 'websockets-client',
 | 
			
		||||
    room: 'codemirror-example'
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const y = new Y('codemirror-example', connector, persistence)
 | 
			
		||||
window.yCodeMirror = y
 | 
			
		||||
 | 
			
		||||
var editor = CodeMirror(document.querySelector('#codeMirrorContainer'), {
 | 
			
		||||
  mode: 'javascript',
 | 
			
		||||
  lineNumbers: true
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
y.define('codemirror', Y.Text).bindCodeMirror(editor)
 | 
			
		||||
							
								
								
									
										55
									
								
								examples/infiniteyjs/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								examples/infiniteyjs/index.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,55 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html>
 | 
			
		||||
<body>
 | 
			
		||||
  <style>
 | 
			
		||||
    .wrapper {
 | 
			
		||||
      display: grid;
 | 
			
		||||
      grid-template-columns: repeat(3, 1fr);
 | 
			
		||||
      grid-gap: 7px;
 | 
			
		||||
    }
 | 
			
		||||
    .one {
 | 
			
		||||
      grid-column: 1 ;
 | 
			
		||||
    }
 | 
			
		||||
    .two {
 | 
			
		||||
      grid-column: 2;
 | 
			
		||||
    }
 | 
			
		||||
    .three {
 | 
			
		||||
      grid-column: 3;
 | 
			
		||||
    }
 | 
			
		||||
    textarea {
 | 
			
		||||
      width: calc(100% - 10px)
 | 
			
		||||
    }
 | 
			
		||||
    .editor-container {
 | 
			
		||||
      background-color: #4caf50;
 | 
			
		||||
      padding: 4px 5px 10px 5px;
 | 
			
		||||
      border-radius: 11px;
 | 
			
		||||
    }
 | 
			
		||||
    .editor-container[disconnected] {
 | 
			
		||||
      background-color: red;
 | 
			
		||||
    }
 | 
			
		||||
    .disconnected-info {
 | 
			
		||||
      display: none;
 | 
			
		||||
    }
 | 
			
		||||
    .editor-container[disconnected] .disconnected-info {
 | 
			
		||||
      display: inline;
 | 
			
		||||
    }
 | 
			
		||||
  </style>
 | 
			
		||||
  <div class="wrapper">
 | 
			
		||||
    <div id="container1" class="one editor-container">
 | 
			
		||||
      <h1>Server 1 <span class="disconnected-info">(disconnected)</span></h1>
 | 
			
		||||
      <textarea id="textarea1" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div id="container2" class="two editor-container">
 | 
			
		||||
      <h1>Server 2 <span class="disconnected-info">(disconnected)</span></h1>
 | 
			
		||||
      <textarea id="textarea2" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div id="container3" class="three editor-container">
 | 
			
		||||
      <h1>Server 3 <span class="disconnected-info">(disconnected)</span></h1>
 | 
			
		||||
      <textarea id="textarea3" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
  <script src="../../y.js"></script>
 | 
			
		||||
  <script src='../../../y-websockets-client/y-websockets-client.js'></script>
 | 
			
		||||
  <script src="./index.js"></script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										38
									
								
								examples/infiniteyjs/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								examples/infiniteyjs/index.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,38 @@
 | 
			
		||||
/* global Y */
 | 
			
		||||
 | 
			
		||||
function bindYjsInstance (y, suffix) {
 | 
			
		||||
  y.define('textarea', Y.Text).bind(document.getElementById('textarea' + suffix))
 | 
			
		||||
  y.connector.socket.on('connection', function () {
 | 
			
		||||
    document.getElementById('container' + suffix).removeAttribute('disconnected')
 | 
			
		||||
  })
 | 
			
		||||
  y.connector.socket.on('disconnect', function () {
 | 
			
		||||
    document.getElementById('container' + suffix).setAttribute('disconnected', true)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let y1 = new Y('infinite-example', {
 | 
			
		||||
  connector: {
 | 
			
		||||
    name: 'websockets-client',
 | 
			
		||||
    url: 'http://127.0.0.1:1234'
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
window.y1 = y1
 | 
			
		||||
bindYjsInstance(y1, '1')
 | 
			
		||||
 | 
			
		||||
let y2 = new Y('infinite-example', {
 | 
			
		||||
  connector: {
 | 
			
		||||
    name: 'websockets-client',
 | 
			
		||||
    url: 'http://127.0.0.1:1234'
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
window.y2 = y2
 | 
			
		||||
bindYjsInstance(y2, '2')
 | 
			
		||||
 | 
			
		||||
let y3 = new Y('infinite-example', {
 | 
			
		||||
  connector: {
 | 
			
		||||
    name: 'websockets-client',
 | 
			
		||||
    url: 'http://127.0.0.1:1234'
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
window.y3 = y3
 | 
			
		||||
bindYjsInstance(y1, '3')
 | 
			
		||||
							
								
								
									
										24
									
								
								examples/jigsaw/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								examples/jigsaw/index.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,24 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html>
 | 
			
		||||
<head>
 | 
			
		||||
  <style type="text/css">
 | 
			
		||||
    .draggable {
 | 
			
		||||
      cursor: move;
 | 
			
		||||
    }
 | 
			
		||||
  </style>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
  <svg id="puzzle-example" width="100%" viewBox="0 0 800 800">
 | 
			
		||||
    <g>
 | 
			
		||||
      <path d="M 311.76636,154.23389 C 312.14136,171.85693 318.14087,184.97998 336.13843,184.23047 C 354.13647,183.48047 351.88647,180.48096 354.88599,178.98096 C 357.8855,177.48096 368.38452,170.35693 380.00806,169.98193 C 424.61841,168.54297 419.78296,223.6001 382.25757,223.6001 C 377.75806,223.6001 363.51001,219.10107 356.38599,211.97656 C 349.26196,204.85254 310.64185,207.10254 314.76636,236.34863 C 316.34888,247.5708 324.08374,267.90723 324.84595,286.23486 C 325.29321,296.99414 323.17603,307.00635 321.58911,315.6377 C 360.11353,305.4585 367.73462,304.30518 404.00513,312.83936 C 410.37915,314.33887 436.62573,310.21436 421.25269,290.3418 C 405.87964,270.46924 406.25464,248.34717 417.12817,240.84814 C 428.00171,233.34912 446.74976,228.84961 457.99829,234.09912 C 469.24683,239.34814 484.61987,255.84619 475.24585,271.59424 C 465.87231,287.34229 452.74878,290.7168 456.49829,303.84033 C 460.2478,316.96387 479.74536,320.33838 500.74292,321.83789 C 509.70142,322.47803 527.97192,323.28467 542.10864,320.12939 C 549.91821,318.38672 556.92212,315.89502 562.46753,313.56396 C 561.40796,277.80664 560.84888,245.71729 560.3606,241.97314 C 558.85278,230.41455 542.49536,217.28564 525.86499,223.2251 C 520.61548,225.1001 519.86548,231.84912 505.24243,232.59912 C 444.92798,235.69238 462.06958,143.26709 525.86499,180.48096 C 539.52759,188.45068 575.19409,190.7583 570.10913,156.85889 C 567.85962,141.86035 553.98608,102.86523 553.98608,102.86523 C 553.98608,102.86523 477.23755,111.82227 451.99878,91.991699 C 441.50024,83.74292 444.87476,69.494629 449.37427,61.245605 C 453.87378,52.996582 465.12231,46.622559 464.74731,36.123779 C 463.02563,-12.086426 392.96704,-10.902832 396.5061,36.873535 C 397.25562,46.997314 406.62964,52.621582 410.75415,60.495605 C 420.00757,78.161377 405.50024,96.073486 384.50757,99.490723 C 377.36206,100.65381 349.17505,102.65332 320.39429,102.23486 C 319.677,102.22461 318.95923,102.21143 318.24194,102.19775 C 315.08423,120.9751 311.55688,144.39697 311.76636,154.23389 z " style="fill:#f2c569;stroke:#000000" id="path2502"/>
 | 
			
		||||
      <path d="M 500.74292,321.83789 C 479.74536,320.33838 460.2478,316.96387 456.49829,303.84033 C 452.74878,290.7168 465.87231,287.34229 475.24585,271.59424 C 484.61987,255.84619 469.24683,239.34814 457.99829,234.09912 C 446.74976,228.84961 428.00171,233.34912 417.12817,240.84814 C 406.25464,248.34717 405.87964,270.46924 421.25269,290.3418 C 436.62573,310.21436 410.37915,314.33887 404.00513,312.83936 C 367.73462,304.30518 360.11353,305.4585 321.58911,315.6377 C 320.56372,321.21484 319.75854,326.2207 320.01538,330.46191 C 320.76538,342.83545 329.3894,385.95508 327.8894,392.7041 C 326.3894,399.45312 313.64136,418.20117 297.89331,407.32715 C 282.14526,396.45361 276.52075,393.4541 265.27222,394.5791 C 254.02368,395.70361 239.77563,402.07812 239.77563,419.32568 C 239.77563,436.57373 250.27417,449.69727 268.64673,447.82227 C 287.36353,445.9126 317.92163,423.11035 325.63989,452.69678 C 330.1394,469.94434 330.51392,487.19238 330.1394,498.44092 C 329.95825,503.87646 326.09985,518.06592 322.16089,531.28125 C 353.2854,532.73682 386.47095,531.26611 394.2561,529.93701 C 430.30933,523.78174 429.31909,496.09766 412.62866,477.44385 C 406.25464,470.31934 401.75513,455.32129 405.87964,444.82275 C 414.07056,423.97314 458.8064,422.17773 473.37134,438.82324 C 483.86987,450.82178 475.99585,477.44385 468.49683,482.69287 C 453.52222,493.17529 457.22485,516.83008 473.37134,528.06201 C 504.79126,549.91943 572.35913,535.56152 572.35913,535.56152 C 572.35913,535.56152 567.85962,498.06592 567.48462,471.81934 C 567.10962,445.57275 589.60669,450.07227 593.3562,450.07227 C 597.10571,450.07227 604.22974,455.32129 609.47925,459.4458 C 614.72876,463.57031 618.85327,469.94434 630.85181,470.69434 C 677.43726,473.60596 674.58813,420.7373 631.97632,413.32666 C 623.35229,411.82666 614.72876,416.32617 603.10522,424.57519 C 591.48169,432.82422 577.23315,425.32519 570.10913,417.45117 C 566.07788,412.99561 563.8479,360.16406 562.46753,313.56396 C 556.92212,315.89502 549.91821,318.38672 542.10864,320.12939 C 527.97192,323.28467 509.70142,322.47803 500.74292,321.83789 z " style="fill:#f3f3d6;stroke:#000000" id="path2504"/>
 | 
			
		||||
      <path d="M 240.52563,141.86035 C 257.60327,159.6499 243.94507,188.68799 214.65356,190.22949 C 185.09448,191.78516 164.66675,157.17822 190.28589,136.61621 C 200.49585,128.42139 198.05786,114.12158 179.78296,106.98975 C 154.4187,97.091553 90.54419,107.73975 90.54419,107.73975 C 90.54419,107.73975 100.88794,135.11328 101.41772,168.48242 C 101.79272,192.104 68.796875,189.47949 63.172607,186.85498 C 57.54834,184.23047 45.924805,173.73145 37.675781,173.73145 C -14.411865,173.73145 -10.013184,245.84375 39.925537,232.22412 C 48.174316,229.97461 56.42334,220.97559 68.796875,222.47559 C 81.17041,223.9751 87.544434,232.59912 87.544434,246.09766 C 87.544434,252.51709 87.0354,281.24268 86.340576,312.87012 C 119.15894,313.67676 160.60962,314.46582 170.03442,313.58887 C 186.15698,312.08936 195.90601,301.59033 188.40698,293.3418 C 180.90796,285.09277 156.16089,256.59619 179.03296,239.34814 C 201.90503,222.10059 235.65112,231.84912 239.77563,247.22217 C 243.90015,262.59521 240.52563,273.46924 234.90112,279.09326 C 229.27661,284.71777 210.52905,298.96582 221.40259,308.71484 C 232.27661,318.46338 263.77222,330.83691 302.39282,320.71338 C 309.58862,318.82715 315.92114,317.13525 321.58911,315.6377 C 323.17603,307.00635 325.29321,296.99414 324.84595,286.23486 C 324.08374,267.90723 316.34888,247.5708 314.76636,236.34863 C 310.64185,207.10254 349.26196,204.85254 356.38599,211.97656 C 363.51001,219.10107 377.75806,223.6001 382.25757,223.6001 C 419.78296,223.6001 424.61841,168.54297 380.00806,169.98193 C 368.38452,170.35693 357.8855,177.48096 354.88599,178.98096 C 351.88647,180.48096 354.13647,183.48047 336.13843,184.23047 C 318.14087,184.97998 312.14136,171.85693 311.76636,154.23389 C 311.55688,144.39697 315.08423,120.9751 318.24194,102.19775 C 290.37524,101.67725 262.46069,98.968262 254.39868,97.991211 C 233.38013,95.443359 217.17456,117.53662 240.52563,141.86035 z " style="fill:#bebcdb;stroke:#000000" id="path2506"/>
 | 
			
		||||
      <path d="M 325.63989,452.69678 C 317.92163,423.11035 287.36353,445.9126 268.64673,447.82227 C 250.27417,449.69727 239.77563,436.57373 239.77563,419.32568 C 239.77563,402.07812 254.02368,395.70361 265.27222,394.5791 C 276.52075,393.4541 282.14526,396.45361 297.89331,407.32715 C 313.64136,418.20117 326.3894,399.45313 327.8894,392.7041 C 329.3894,385.95508 320.76538,342.83545 320.01538,330.46191 C 319.75855,326.2207 320.56372,321.21484 321.58911,315.6377 C 315.92114,317.13525 309.58862,318.82715 302.39282,320.71338 C 263.77222,330.83691 232.27661,318.46338 221.40259,308.71484 C 210.52905,298.96582 229.27661,284.71777 234.90112,279.09326 C 240.52563,273.46924 243.90015,262.59521 239.77563,247.22217 C 235.65112,231.84912 201.90503,222.10059 179.03296,239.34814 C 156.16089,256.59619 180.90796,285.09277 188.40698,293.3418 C 195.90601,301.59033 186.15698,312.08936 170.03442,313.58887 C 160.60962,314.46582 119.15894,313.67676 86.340576,312.87012 C 85.573975,347.74561 84.581299,386.15088 83.794922,402.07812 C 82.295166,432.44922 109.29175,422.32568 115.66577,420.82568 C 122.04028,419.32568 126.16479,409.57715 143.03735,408.45215 C 185.9231,405.59326 186.09985,466.69629 144.16235,467.69482 C 128.41431,468.06982 113.79126,451.19678 108.16675,447.44727 C 102.54272,443.69775 87.919433,442.94775 83.794922,457.9458 C 82.01709,464.41113 78.118652,481.65137 78.098144,496.18994 C 78.071045,515.38037 82.295166,531.81201 82.295166,531.81201 C 82.295166,531.81201 105.54224,526.5625 149.41187,526.5625 C 193.28149,526.5625 199.65552,547.93506 194.78101,558.80859 C 189.90649,569.68213 181.28296,568.93213 179.40796,583.18066 C 172.7063,634.11133 253.34106,631.08203 249.14917,584.68018 C 247.96948,571.62354 237.16528,571.66699 232.27661,557.68359 C 222.17944,528.80273 244.64966,523.56299 257.39819,524.68799 C 263.59351,525.23437 290.95679,529.73389 320.75757,531.21582 C 321.22437,531.23877 321.69312,531.25928 322.16089,531.28125 C 326.09985,518.06592 329.95825,503.87646 330.1394,498.44092 C 330.51392,487.19238 330.1394,469.94434 325.63989,452.69678 z " style="fill:#d3ea9d;stroke:#000000" id="path2508"/>
 | 
			
		||||
		</g>
 | 
			
		||||
  </svg>
 | 
			
		||||
  <script src="../../y.js"></script>
 | 
			
		||||
  <script src='../../../y-websockets-client/y-websockets-client.js'></script>
 | 
			
		||||
  <script src="../bower_components/d3/d3.js"></script>
 | 
			
		||||
  <script src="./index.js"></script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										67
									
								
								examples/jigsaw/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								examples/jigsaw/index.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,67 @@
 | 
			
		||||
/* global Y, d3 */
 | 
			
		||||
 | 
			
		||||
let y = new Y('jigsaw-example', {
 | 
			
		||||
  connector: {
 | 
			
		||||
    name: 'websockets-client',
 | 
			
		||||
    url: 'http://127.0.0.1:1234'
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
let jigsaw = y.define('jigsaw', Y.Map)
 | 
			
		||||
window.yJigsaw = y
 | 
			
		||||
 | 
			
		||||
var origin // mouse start position - translation of piece
 | 
			
		||||
var drag = d3.behavior.drag()
 | 
			
		||||
  .on('dragstart', function (params) {
 | 
			
		||||
    // get the translation of the element
 | 
			
		||||
    var translation = d3
 | 
			
		||||
      .select(this)
 | 
			
		||||
      .attr('transform')
 | 
			
		||||
      .slice(10, -1)
 | 
			
		||||
      .split(',')
 | 
			
		||||
      .map(Number)
 | 
			
		||||
    // mouse coordinates
 | 
			
		||||
    var mouse = d3.mouse(this.parentNode)
 | 
			
		||||
    origin = {
 | 
			
		||||
      x: mouse[0] - translation[0],
 | 
			
		||||
      y: mouse[1] - translation[1]
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
  .on('drag', function () {
 | 
			
		||||
    var mouse = d3.mouse(this.parentNode)
 | 
			
		||||
    var x = mouse[0] - origin.x // =^= mouse - mouse at dragstart + translation at dragstart
 | 
			
		||||
    var y = mouse[1] - origin.y
 | 
			
		||||
    d3.select(this).attr('transform', 'translate(' + x + ',' + y + ')')
 | 
			
		||||
  })
 | 
			
		||||
  .on('dragend', function (piece, i) {
 | 
			
		||||
    // save the current translation of the puzzle piece
 | 
			
		||||
    var mouse = d3.mouse(this.parentNode)
 | 
			
		||||
    var x = mouse[0] - origin.x
 | 
			
		||||
    var y = mouse[1] - origin.y
 | 
			
		||||
    jigsaw.set(piece, {x: x, y: y})
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
var data = ['piece1', 'piece2', 'piece3', 'piece4']
 | 
			
		||||
var pieces = d3.select(document.querySelector('#puzzle-example')).selectAll('path').data(data)
 | 
			
		||||
 | 
			
		||||
pieces
 | 
			
		||||
  .classed('draggable', true)
 | 
			
		||||
  .attr('transform', function (piece) {
 | 
			
		||||
    var translation = piece.get('translation') || {x: 0, y: 0}
 | 
			
		||||
    return 'translate(' + translation.x + ',' + translation.y + ')'
 | 
			
		||||
  }).call(drag)
 | 
			
		||||
 | 
			
		||||
data.forEach(function (piece) {
 | 
			
		||||
  jigsaw.observe(function () {
 | 
			
		||||
    // whenever a property of a piece changes, update the translation of the pieces
 | 
			
		||||
    pieces
 | 
			
		||||
      .transition()
 | 
			
		||||
      .attr('transform', function (piece) {
 | 
			
		||||
        var translation = piece.get(piece)
 | 
			
		||||
        if (translation == null || typeof translation.x !== 'number' || typeof translation.y !== 'number') {
 | 
			
		||||
          translation = { x: 0, y: 0 }
 | 
			
		||||
        }
 | 
			
		||||
        return 'translate(' + translation.x + ',' + translation.y + ')'
 | 
			
		||||
      })
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
							
								
								
									
										21
									
								
								examples/monaco/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								examples/monaco/index.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,21 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html>
 | 
			
		||||
  <head>
 | 
			
		||||
  </head>
 | 
			
		||||
  <body>
 | 
			
		||||
    <div id="monacoContainer"></div>
 | 
			
		||||
    <style>
 | 
			
		||||
      #monacoContainer {
 | 
			
		||||
        position: fixed;
 | 
			
		||||
        top: 0;
 | 
			
		||||
        left: 0;
 | 
			
		||||
        height: 100%;
 | 
			
		||||
        width: 100%;
 | 
			
		||||
      }
 | 
			
		||||
    </style>
 | 
			
		||||
    <script src="../../y.js"></script>
 | 
			
		||||
    <script src='../../../y-websockets-client/y-websockets-client.js'></script>
 | 
			
		||||
    <script src="../node_modules/monaco-editor/min/vs/loader.js"></script>
 | 
			
		||||
    <script src="./index.js"></script>
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										22
									
								
								examples/monaco/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								examples/monaco/index.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
			
		||||
/* global Y, monaco */
 | 
			
		||||
 | 
			
		||||
require.config({ paths: { 'vs': '../node_modules/monaco-editor/min/vs' } })
 | 
			
		||||
 | 
			
		||||
let y = new Y('monaco-example', {
 | 
			
		||||
  connector: {
 | 
			
		||||
    name: 'websockets-client',
 | 
			
		||||
    url: 'http://127.0.0.1:1234'
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
require(['vs/editor/editor.main'], function () {
 | 
			
		||||
  window.yMonaco = y
 | 
			
		||||
 | 
			
		||||
  // Create Monaco editor
 | 
			
		||||
  var editor = monaco.editor.create(document.getElementById('monacoContainer'), {
 | 
			
		||||
    language: 'javascript'
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  // Bind to y.share.monaco
 | 
			
		||||
  y.define('monaco', Y.Text).bindMonaco(editor)
 | 
			
		||||
})
 | 
			
		||||
							
								
								
									
										8
									
								
								examples/notes/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								examples/notes/index.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html>
 | 
			
		||||
</head>
 | 
			
		||||
  <script src="./index.mjs" type="module"></script>
 | 
			
		||||
</head>
 | 
			
		||||
<body contenteditable="true">
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										48
									
								
								examples/notes/index.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								examples/notes/index.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,48 @@
 | 
			
		||||
 | 
			
		||||
import IndexedDBPersistence from '../../src/Persistences/IndexeddbPersistence.mjs'
 | 
			
		||||
import YWebsocketsConnector from '../../src/Connectors/WebsocketsConnector/WebsocketsConnector.mjs'
 | 
			
		||||
import Y from '../../src/Y.mjs'
 | 
			
		||||
import YXmlFragment from '../../src/Types/YXml/YXmlFragment.mjs'
 | 
			
		||||
 | 
			
		||||
const yCollection = new YCollection(new YWebsocketsConnector(), new IndexedDBPersistence())
 | 
			
		||||
 | 
			
		||||
const y = yCollection.getDocument('my-notes')
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
persistence.addConnector(persistence)
 | 
			
		||||
 | 
			
		||||
const y = new Y()
 | 
			
		||||
await persistence.persistY(y)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
connector.connectY('html-editor', y)
 | 
			
		||||
persistence.connectY('html-editor', y)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
window.connector = connector
 | 
			
		||||
 | 
			
		||||
window.onload = function () {
 | 
			
		||||
  window.domBinding = new DomBinding(window.yXmlType, document.body, { scrollingElement: document.scrollingElement })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
window.y = y
 | 
			
		||||
window.yXmlType = y.define('xml', YXmlFragment)
 | 
			
		||||
window.undoManager = new UndoManager(window.yXmlType, {
 | 
			
		||||
  captureTimeout: 500
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
document.onkeydown = function interceptUndoRedo (e) {
 | 
			
		||||
  if (e.keyCode === 90 && (e.metaKey || e.ctrlKey)) {
 | 
			
		||||
    if (!e.shiftKey) {
 | 
			
		||||
      window.undoManager.undo()
 | 
			
		||||
    } else {
 | 
			
		||||
      window.undoManager.redo()
 | 
			
		||||
    }
 | 
			
		||||
    e.preventDefault()
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										23
									
								
								examples/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								examples/package.json
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,23 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "examples",
 | 
			
		||||
  "version": "0.0.0",
 | 
			
		||||
  "description": "",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "dist": "rollup -c",
 | 
			
		||||
    "watch": "rollup -cw"
 | 
			
		||||
  },
 | 
			
		||||
  "author": "Kevin Jahns",
 | 
			
		||||
  "license": "MIT",
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "monaco-editor": "^0.8.3",
 | 
			
		||||
    "rollup": "^0.52.3"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "standard": "^10.0.2"
 | 
			
		||||
  },
 | 
			
		||||
  "standard": {
 | 
			
		||||
    "ignore": [
 | 
			
		||||
      "bower_components"
 | 
			
		||||
    ]
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										21
									
								
								examples/quill-cursors/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								examples/quill-cursors/index.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,21 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html>
 | 
			
		||||
<head>
 | 
			
		||||
  <!-- Main quill library -->
 | 
			
		||||
  <script src="../../node_modules/quill/dist/quill.min.js"></script>
 | 
			
		||||
  <link href="../../node_modules/quill/dist/quill.snow.css" rel="stylesheet">
 | 
			
		||||
  <!-- Quill cursors module -->
 | 
			
		||||
  <script src="../../node_modules/quill-cursors/dist/quill-cursors.min.js"></script>
 | 
			
		||||
  <link href="../../node_modules/quill-cursors/dist/quill-cursors.css" rel="stylesheet">
 | 
			
		||||
  <!-- Yjs Library and connector -->
 | 
			
		||||
  <script src="../../y.js"></script>
 | 
			
		||||
  <script src='../../../y-websockets-client/y-websockets-client.js'></script>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
  <div id="quill-container">
 | 
			
		||||
    <div id="quill">
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
  <script src="./index.js"></script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										78
									
								
								examples/quill-cursors/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								examples/quill-cursors/index.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,78 @@
 | 
			
		||||
/* global Y, Quill, QuillCursors */
 | 
			
		||||
 | 
			
		||||
Quill.register('modules/cursors', QuillCursors)
 | 
			
		||||
 | 
			
		||||
let y = new Y('quill-0', {
 | 
			
		||||
  connector: {
 | 
			
		||||
    name: 'websockets-client',
 | 
			
		||||
    url: 'http://127.0.0.1:1234'
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
let users = y.define('users', Y.Array)
 | 
			
		||||
let myUserInfo = new Y.Map()
 | 
			
		||||
myUserInfo.set('name', 'dada')
 | 
			
		||||
myUserInfo.set('color', 'red')
 | 
			
		||||
users.push([myUserInfo])
 | 
			
		||||
 | 
			
		||||
let quill = new Quill('#quill-container', {
 | 
			
		||||
  modules: {
 | 
			
		||||
    toolbar: [
 | 
			
		||||
      [{ header: [1, 2, false] }],
 | 
			
		||||
      ['bold', 'italic', 'underline'],
 | 
			
		||||
      ['image', 'code-block'],
 | 
			
		||||
      [{ color: [] }, { background: [] }],    // Snow theme fills in values
 | 
			
		||||
      [{ script: 'sub' }, { script: 'super' }],
 | 
			
		||||
      ['link', 'image'],
 | 
			
		||||
      ['link', 'code-block'],
 | 
			
		||||
      [{ list: 'ordered' }, { list: 'bullet' }]
 | 
			
		||||
    ],
 | 
			
		||||
    cursors: {
 | 
			
		||||
      hideDelay: 500
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  placeholder: 'Compose an epic...',
 | 
			
		||||
  theme: 'snow'  // or 'bubble'
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
let cursors = quill.getModule('cursors')
 | 
			
		||||
 | 
			
		||||
function drawCursors () {
 | 
			
		||||
  cursors.clearCursors()
 | 
			
		||||
  users.map((user, userId) => {
 | 
			
		||||
    if (user !== myUserInfo) {
 | 
			
		||||
      let relativeRange = user.get('range')
 | 
			
		||||
      let lastUpdated = new Date(user.get('last updated'))
 | 
			
		||||
      if (lastUpdated != null && new Date() - lastUpdated < 20000 && relativeRange != null) {
 | 
			
		||||
        let start = Y.utils.fromRelativePosition(y, relativeRange.start).offset
 | 
			
		||||
        let end = Y.utils.fromRelativePosition(y, relativeRange.end).offset
 | 
			
		||||
        let range = { index: start, length: end - start }
 | 
			
		||||
        cursors.setCursor(userId + '', range, user.get('name'), user.get('color'))
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
users.observeDeep(drawCursors)
 | 
			
		||||
drawCursors()
 | 
			
		||||
 | 
			
		||||
quill.on('selection-change', function (range) {
 | 
			
		||||
  if (range != null) {
 | 
			
		||||
    myUserInfo.set('range', {
 | 
			
		||||
      start: Y.utils.getRelativePosition(yText, range.index),
 | 
			
		||||
      end: Y.utils.getRelativePosition(yText, range.index + range.length)
 | 
			
		||||
    })
 | 
			
		||||
  } else {
 | 
			
		||||
    myUserInfo.delete('range')
 | 
			
		||||
  }
 | 
			
		||||
  myUserInfo.set('last updated', new Date().toString())
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
let yText = y.define('quill', Y.Text)
 | 
			
		||||
let quillBinding = new Y.QuillBinding(yText, quill)
 | 
			
		||||
 | 
			
		||||
window.quillBinding = quillBinding
 | 
			
		||||
window.yText = yText
 | 
			
		||||
window.y = y
 | 
			
		||||
window.quill = quill
 | 
			
		||||
window.users = users
 | 
			
		||||
window.cursors = cursors
 | 
			
		||||
							
								
								
									
										18
									
								
								examples/quill/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								examples/quill/index.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,18 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html>
 | 
			
		||||
<head>
 | 
			
		||||
  <!-- Main Quill library -->
 | 
			
		||||
  <script src="../../node_modules/quill/dist/quill.min.js"></script>
 | 
			
		||||
  <link href="../../node_modules/quill/dist/quill.snow.css" rel="stylesheet">
 | 
			
		||||
  <!-- Yjs Library and connector -->
 | 
			
		||||
  <script src="../../y.js"></script>
 | 
			
		||||
  <script src='../../../y-websockets-client/y-websockets-client.js'></script>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
  <div id="quill-container">
 | 
			
		||||
    <div id="quill">
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
  <script src="./index.js"></script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										33
									
								
								examples/quill/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								examples/quill/index.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,33 @@
 | 
			
		||||
/* global Y, Quill */
 | 
			
		||||
 | 
			
		||||
let y = new Y('quill-cursors-0', {
 | 
			
		||||
  connector: {
 | 
			
		||||
    name: 'websockets-client',
 | 
			
		||||
    url: 'http://127.0.0.1:1234'
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
let quill = new Quill('#quill-container', {
 | 
			
		||||
  modules: {
 | 
			
		||||
    toolbar: [
 | 
			
		||||
      [{ header: [1, 2, false] }],
 | 
			
		||||
      ['bold', 'italic', 'underline'],
 | 
			
		||||
      ['image', 'code-block'],
 | 
			
		||||
      [{ color: [] }, { background: [] }],    // Snow theme fills in values
 | 
			
		||||
      [{ script: 'sub' }, { script: 'super' }],
 | 
			
		||||
      ['link', 'image'],
 | 
			
		||||
      ['link', 'code-block'],
 | 
			
		||||
      [{ list: 'ordered' }, { list: 'bullet' }]
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  placeholder: 'Compose an epic...',
 | 
			
		||||
  theme: 'snow'  // or 'bubble'
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
let yText = y.define('quill', Y.Text)
 | 
			
		||||
 | 
			
		||||
let quillBinding = new Y.QuillBinding(yText, quill)
 | 
			
		||||
window.quillBinding = quillBinding
 | 
			
		||||
window.yText = yText
 | 
			
		||||
window.y = y
 | 
			
		||||
window.quill = quill
 | 
			
		||||
							
								
								
									
										29
									
								
								examples/rollup.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								examples/rollup.config.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,29 @@
 | 
			
		||||
import nodeResolve from 'rollup-plugin-node-resolve'
 | 
			
		||||
import commonjs from 'rollup-plugin-commonjs'
 | 
			
		||||
 | 
			
		||||
var pkg = require('./package.json')
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  input: 'yjs-dist.mjs',
 | 
			
		||||
  name: 'Y',
 | 
			
		||||
  output: {
 | 
			
		||||
    file: 'yjs-dist.js',
 | 
			
		||||
    format: 'umd'
 | 
			
		||||
  },
 | 
			
		||||
  plugins: [
 | 
			
		||||
    nodeResolve({
 | 
			
		||||
      main: true,
 | 
			
		||||
      module: true,
 | 
			
		||||
      browser: true
 | 
			
		||||
    }),
 | 
			
		||||
    commonjs()
 | 
			
		||||
  ],
 | 
			
		||||
  sourcemap: true,
 | 
			
		||||
  banner: `
 | 
			
		||||
/**
 | 
			
		||||
 * ${pkg.name} - ${pkg.description}
 | 
			
		||||
 * @version v${pkg.version}
 | 
			
		||||
 * @license ${pkg.license}
 | 
			
		||||
 */
 | 
			
		||||
`
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										31
									
								
								examples/serviceworker/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								examples/serviceworker/index.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,31 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html>
 | 
			
		||||
<head>
 | 
			
		||||
  <!-- quill does not include dist files! We are using the hosted version instead -->
 | 
			
		||||
  <!--link rel="stylesheet" href="../bower_components/quill/dist/quill.snow.css" /-->
 | 
			
		||||
  <link href="https://cdn.quilljs.com/1.0.4/quill.snow.css" rel="stylesheet">
 | 
			
		||||
  <link href="//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.css" rel="stylesheet">
 | 
			
		||||
  <link href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/styles/monokai-sublime.min.css" rel="stylesheet">
 | 
			
		||||
  <style>
 | 
			
		||||
    #quill-container {
 | 
			
		||||
      border: 1px solid gray;
 | 
			
		||||
      box-shadow: 0px 0px 10px gray;
 | 
			
		||||
    }
 | 
			
		||||
  </style>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
  <div id="quill-container">
 | 
			
		||||
    <div id="quill">
 | 
			
		||||
    </div>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <script src="//cdnjs.cloudflare.com/ajax/libs/KaTeX/0.5.1/katex.min.js" type="text/javascript"></script>
 | 
			
		||||
  <script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/9.2.0/highlight.min.js" type="text/javascript"></script>
 | 
			
		||||
  <script src="https://cdn.quilljs.com/1.0.4/quill.js"></script>
 | 
			
		||||
  <!-- quill does not include dist files! We are using the hosted version instead (see above)
 | 
			
		||||
  <script src="../bower_components/quill/dist/quill.js"></script>
 | 
			
		||||
  -->
 | 
			
		||||
  <script src="../bower_components/yjs/y.js"></script>
 | 
			
		||||
  <script src="./index.js"></script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										49
									
								
								examples/serviceworker/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								examples/serviceworker/index.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,49 @@
 | 
			
		||||
/* global Y, Quill */
 | 
			
		||||
 | 
			
		||||
// register yjs service worker
 | 
			
		||||
if ('serviceWorker' in navigator) {
 | 
			
		||||
  // Register service worker
 | 
			
		||||
  // it is important to copy yjs-sw-template to the root directory!
 | 
			
		||||
  navigator.serviceWorker.register('./yjs-sw-template.js').then(function (reg) {
 | 
			
		||||
    console.log('Yjs service worker registration succeeded. Scope is ' + reg.scope)
 | 
			
		||||
  }).catch(function (err) {
 | 
			
		||||
    console.error('Yjs service worker registration failed with error ' + err)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// initialize a shared object. This function call returns a promise!
 | 
			
		||||
Y({
 | 
			
		||||
  db: {
 | 
			
		||||
    name: 'memory'
 | 
			
		||||
  },
 | 
			
		||||
  connector: {
 | 
			
		||||
    name: 'serviceworker',
 | 
			
		||||
    room: 'ServiceWorkerExample2'
 | 
			
		||||
  },
 | 
			
		||||
  sourceDir: '/bower_components',
 | 
			
		||||
  share: {
 | 
			
		||||
    richtext: 'Richtext' // y.share.richtext is of type Y.Richtext
 | 
			
		||||
  }
 | 
			
		||||
}).then(function (y) {
 | 
			
		||||
  window.yServiceWorker = y
 | 
			
		||||
 | 
			
		||||
  // create quill element
 | 
			
		||||
  window.quill = new Quill('#quill', {
 | 
			
		||||
    modules: {
 | 
			
		||||
      formula: true,
 | 
			
		||||
      syntax: true,
 | 
			
		||||
      toolbar: [
 | 
			
		||||
        [{ size: ['small', false, 'large', 'huge'] }],
 | 
			
		||||
        ['bold', 'italic', 'underline'],
 | 
			
		||||
        [{ color: [] }, { background: [] }],    // Snow theme fills in values
 | 
			
		||||
        [{ script: 'sub' }, { script: 'super' }],
 | 
			
		||||
        ['link', 'image'],
 | 
			
		||||
        ['link', 'code-block'],
 | 
			
		||||
        [{ list: 'ordered' }]
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    theme: 'snow'
 | 
			
		||||
  })
 | 
			
		||||
  // bind quill to richtext type
 | 
			
		||||
  y.share.richtext.bind(window.quill)
 | 
			
		||||
})
 | 
			
		||||
							
								
								
									
										22
									
								
								examples/serviceworker/yjs-sw-template.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								examples/serviceworker/yjs-sw-template.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
			
		||||
/* eslint-env worker */
 | 
			
		||||
 | 
			
		||||
// copy and modify this file
 | 
			
		||||
 | 
			
		||||
self.DBConfig = {
 | 
			
		||||
  name: 'indexeddb'
 | 
			
		||||
}
 | 
			
		||||
self.ConnectorConfig = {
 | 
			
		||||
  name: 'websockets-client',
 | 
			
		||||
  // url: '..',
 | 
			
		||||
  options: {
 | 
			
		||||
    jsonp: false
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
importScripts(
 | 
			
		||||
  '/bower_components/yjs/y.js',
 | 
			
		||||
  '/bower_components/y-memory/y-memory.js',
 | 
			
		||||
  '/bower_components/y-indexeddb/y-indexeddb.js',
 | 
			
		||||
  '/bower_components/y-websockets-client/y-websockets-client.js',
 | 
			
		||||
  '/bower_components/y-serviceworker/yjs-sw-include.js'
 | 
			
		||||
)
 | 
			
		||||
							
								
								
									
										9
									
								
								examples/textarea/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								examples/textarea/index.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,9 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html>
 | 
			
		||||
<body>
 | 
			
		||||
  <textarea style="width:80%;" rows=40 autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false"></textarea>
 | 
			
		||||
  <script src="../../y.js"></script>
 | 
			
		||||
  <script src='../../../y-websockets-client/y-websockets-client.js'></script>
 | 
			
		||||
  <script src="./index.js"></script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										15
									
								
								examples/textarea/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								examples/textarea/index.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,15 @@
 | 
			
		||||
/* global Y */
 | 
			
		||||
 | 
			
		||||
let y = new Y('textarea-example', {
 | 
			
		||||
  connector: {
 | 
			
		||||
    name: 'websockets-client',
 | 
			
		||||
    url: 'http://127.0.0.1:1234'
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
window.yTextarea = y
 | 
			
		||||
 | 
			
		||||
// bind the textarea to a shared text element
 | 
			
		||||
let type = y.define('textarea', Y.Text)
 | 
			
		||||
let textarea = document.querySelector('textarea')
 | 
			
		||||
window.binding = new Y.TextareaBinding(type, textarea)
 | 
			
		||||
							
								
								
									
										43
									
								
								examples/xml/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								examples/xml/index.html
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,43 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html>
 | 
			
		||||
</head>
 | 
			
		||||
  <!-- jquery is not required for YXml. It is just here for convenience, and to test batch operations. -->
 | 
			
		||||
  <script src="https://code.jquery.com/jquery-3.2.1.min.js"></script>
 | 
			
		||||
  <script src="../../y.js"></script>
 | 
			
		||||
  <script src='../../../y-websockets-client/y-websockets-client.js'></script>
 | 
			
		||||
  <script src="./index.js"></script>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
  <h1> Shared DOM Example </h1>
 | 
			
		||||
  <p> Use native DOM function or jQuery to manipulate the shared DOM (window.sharedDom). </p>
 | 
			
		||||
  <div class="command">
 | 
			
		||||
    <button type="button">Execute</button>
 | 
			
		||||
    <input type="text" value='$(sharedDom).append("<h3>Appended headline</h3>")' size="40"/>
 | 
			
		||||
  </div>
 | 
			
		||||
  <div class="command">
 | 
			
		||||
    <button type="button">Execute</button>
 | 
			
		||||
    <input type="text" value='$(sharedDom).attr("align","right")' size="40"/>
 | 
			
		||||
  </div>
 | 
			
		||||
  <div class="command">
 | 
			
		||||
    <button type="button">Execute</button>
 | 
			
		||||
    <input type="text" value='$(sharedDom).attr("style","color:blue;")' size="40"/>
 | 
			
		||||
  </div>
 | 
			
		||||
 | 
			
		||||
  <script>
 | 
			
		||||
    /* global $ */
 | 
			
		||||
    var commands = document.querySelectorAll('.command')
 | 
			
		||||
    Array.prototype.forEach.call(commands, function (command) {
 | 
			
		||||
      var execute = function () {
 | 
			
		||||
        // eslint-disable-next-line no-eval
 | 
			
		||||
        eval(command.querySelector('input').value)
 | 
			
		||||
      }
 | 
			
		||||
      command.querySelector('button').onclick = execute
 | 
			
		||||
      $(command.querySelector('input')).keyup(function (e) {
 | 
			
		||||
        if (e.keyCode === 13) {
 | 
			
		||||
          execute()
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
  </script>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
							
								
								
									
										13
									
								
								examples/xml/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								examples/xml/index.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,13 @@
 | 
			
		||||
/* global Y */
 | 
			
		||||
 | 
			
		||||
let y = new Y('xml-example', {
 | 
			
		||||
  connector: {
 | 
			
		||||
    name: 'websockets-client',
 | 
			
		||||
    url: 'http://127.0.0.1:1234'
 | 
			
		||||
  }
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
window.yXml = y
 | 
			
		||||
// bind xml type to a dom, and put it in body
 | 
			
		||||
window.sharedDom = y.define('xml', Y.XmlElement).toDom()
 | 
			
		||||
document.body.appendChild(window.sharedDom)
 | 
			
		||||
							
								
								
									
										142
									
								
								funding.json
									
									
									
									
									
								
							
							
						
						
									
										142
									
								
								funding.json
									
									
									
									
									
								
							@ -1,142 +0,0 @@
 | 
			
		||||
{
 | 
			
		||||
  "version": "v1.0.0",
 | 
			
		||||
  "entity": {
 | 
			
		||||
    "type": "group",
 | 
			
		||||
    "role": "steward",
 | 
			
		||||
    "name": "Kevin Jahns",
 | 
			
		||||
    "email": "kevin.jahns@protonmail.com",
 | 
			
		||||
    "phone": "",
 | 
			
		||||
    "description": "OSS Developer",
 | 
			
		||||
    "webpageUrl": {
 | 
			
		||||
      "url": "https://github.com/yjs"
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "projects": [
 | 
			
		||||
    {
 | 
			
		||||
      "guid": "yjs",
 | 
			
		||||
      "name": "Yjs",
 | 
			
		||||
      "description": "A library for building collaborative applications. #p2p #local-first #CRDT Funding this project will also enable me to maintain the other Yjs-related technologies.",
 | 
			
		||||
      "webpageUrl": {
 | 
			
		||||
        "url": "https://github.com/yjs/yjs"
 | 
			
		||||
      },
 | 
			
		||||
      "repositoryUrl": {
 | 
			
		||||
        "url": "https://github.com/yjs/yjs"
 | 
			
		||||
      },
 | 
			
		||||
      "licenses": [
 | 
			
		||||
        "spdx:MIT"
 | 
			
		||||
      ],
 | 
			
		||||
      "tags": [
 | 
			
		||||
        "collaboration",
 | 
			
		||||
        "p2p",
 | 
			
		||||
        "CRDT",
 | 
			
		||||
        "rich-text",
 | 
			
		||||
        "real-time"
 | 
			
		||||
      ]
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      "guid": "Titanic",
 | 
			
		||||
      "name": "Y/Titanic",
 | 
			
		||||
      "description": "A provider for syncing millions of docs efficiently with other peers. This will become the foundation for building real local-first apps with Yjs.",
 | 
			
		||||
      "webpageUrl": {
 | 
			
		||||
        "url": "https://github.com/yjs/titanic",
 | 
			
		||||
        "wellKnown": "https://github.com/yjs/titanic/blob/main/.well-known/funding-manifest-urls"
 | 
			
		||||
      },
 | 
			
		||||
      "repositoryUrl": {
 | 
			
		||||
        "url": "https://github.com/yjs/titanic",
 | 
			
		||||
        "wellKnown": "https://github.com/yjs/titanic/blob/main/.well-known/funding-manifest-urls"
 | 
			
		||||
      },
 | 
			
		||||
      "licenses": [
 | 
			
		||||
        "spdx:MIT"
 | 
			
		||||
      ],
 | 
			
		||||
      "tags": [
 | 
			
		||||
        "privacy",
 | 
			
		||||
        "collaboration",
 | 
			
		||||
        "p2p",
 | 
			
		||||
        "CRDT",
 | 
			
		||||
        "rich-text",
 | 
			
		||||
        "real-time",
 | 
			
		||||
        "web-development"
 | 
			
		||||
      ]
 | 
			
		||||
    }
 | 
			
		||||
  ],
 | 
			
		||||
  "funding": {
 | 
			
		||||
    "channels": [
 | 
			
		||||
      {
 | 
			
		||||
        "guid": "github-sponsors",
 | 
			
		||||
        "type": "payment-provider",
 | 
			
		||||
        "address": "",
 | 
			
		||||
        "description": "For funding of the Yjs project"
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "guid": "y-collective",
 | 
			
		||||
        "type": "payment-provider",
 | 
			
		||||
        "address": "https://opencollective.com/y-collective",
 | 
			
		||||
        "description": "For funding the Y-CRDT - the Rust implementation of Yjs and other listed projects."
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "plans": [
 | 
			
		||||
      {
 | 
			
		||||
        "guid": "supporter",
 | 
			
		||||
        "status": "active",
 | 
			
		||||
        "name": "Supporter",
 | 
			
		||||
        "description": "",
 | 
			
		||||
        "amount": 0,
 | 
			
		||||
        "currency": "USD",
 | 
			
		||||
        "frequency": "monthly",
 | 
			
		||||
        "channels": [
 | 
			
		||||
          "github-sponsors",
 | 
			
		||||
          "y-collective"
 | 
			
		||||
        ]
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "guid": "titanic-funding",
 | 
			
		||||
        "status": "active",
 | 
			
		||||
        "name": "Titanic Funding",
 | 
			
		||||
        "description": "Fund the next generation of local-first providers.",
 | 
			
		||||
        "amount": 30000,
 | 
			
		||||
        "currency": "USD",
 | 
			
		||||
        "frequency": "one-time",
 | 
			
		||||
        "channels": [
 | 
			
		||||
          "github-sponsors"
 | 
			
		||||
        ]
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "guid": "bronze-sponsor",
 | 
			
		||||
        "status": "active",
 | 
			
		||||
        "name": "Bronze Sponsor",
 | 
			
		||||
        "description": "This is the recommended plan for companies that use Yjs.",
 | 
			
		||||
        "amount": 500,
 | 
			
		||||
        "currency": "USD",
 | 
			
		||||
        "frequency": "monthly",
 | 
			
		||||
        "channels": [
 | 
			
		||||
          "github-sponsors"
 | 
			
		||||
        ]
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "guid": "silver-sponsor",
 | 
			
		||||
        "status": "active",
 | 
			
		||||
        "name": "Silver Sponsor",
 | 
			
		||||
        "description": "This is the recommended plan for large/successfull companies that use Yjs.",
 | 
			
		||||
        "amount": 1000,
 | 
			
		||||
        "currency": "USD",
 | 
			
		||||
        "frequency": "monthly",
 | 
			
		||||
        "channels": [
 | 
			
		||||
          "github-sponsors"
 | 
			
		||||
        ]
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        "guid": "gold-sponsor",
 | 
			
		||||
        "status": "active",
 | 
			
		||||
        "name": "Gold Sponsor",
 | 
			
		||||
        "description": "This is the recommended plan for successful companies that build their entire product around Yjs-related technologies.",
 | 
			
		||||
        "amount": 3000,
 | 
			
		||||
        "currency": "USD",
 | 
			
		||||
        "frequency": "monthly",
 | 
			
		||||
        "channels": [
 | 
			
		||||
          "github-sponsors"
 | 
			
		||||
        ]
 | 
			
		||||
      }
 | 
			
		||||
    ],
 | 
			
		||||
    "history": null
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										11193
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										11193
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										136
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										136
									
								
								package.json
									
									
									
									
									
								
							@ -1,99 +1,87 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "yjs",
 | 
			
		||||
  "version": "13.6.24",
 | 
			
		||||
  "description": "Shared Editing Library",
 | 
			
		||||
  "main": "./dist/yjs.cjs",
 | 
			
		||||
  "module": "./dist/yjs.mjs",
 | 
			
		||||
  "types": "./dist/src/index.d.ts",
 | 
			
		||||
  "type": "module",
 | 
			
		||||
  "sideEffects": false,
 | 
			
		||||
  "funding": {
 | 
			
		||||
    "type": "GitHub Sponsors ❤",
 | 
			
		||||
    "url": "https://github.com/sponsors/dmonad"
 | 
			
		||||
  },
 | 
			
		||||
  "version": "13.0.0-60",
 | 
			
		||||
  "description": "A framework for real-time p2p shared editing on any data",
 | 
			
		||||
  "main": "./y.node.js",
 | 
			
		||||
  "browser": "./y.js",
 | 
			
		||||
  "module": "./src/y.js",
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "clean": "rm -rf dist docs",
 | 
			
		||||
    "test": "npm run dist && NODE_ENV=development node ./dist/tests.cjs --repetition-time 50",
 | 
			
		||||
    "test-extensive": "npm run lint && npm run dist && node ./dist/tests.cjs --production --repetition-time 10000",
 | 
			
		||||
    "dist": "npm run clean && rollup -c && tsc",
 | 
			
		||||
    "watch": "rollup -wc",
 | 
			
		||||
    "lint": "markdownlint README.md && standard && tsc",
 | 
			
		||||
    "docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.md --package ./package.json || true",
 | 
			
		||||
    "serve-docs": "npm run docs && http-server ./docs/",
 | 
			
		||||
    "preversion": "npm run lint && PRODUCTION=1 npm run dist && npm run docs && node ./dist/tests.cjs --repetition-time 1000 && test -e dist/src/index.d.ts && test -e dist/yjs.cjs && test -e dist/yjs.cjs",
 | 
			
		||||
    "debug": "concurrently 'http-server -o test.html' 'npm run watch'",
 | 
			
		||||
    "trace-deopt": "clear && rollup -c  && node --trace-deopt dist/test.cjs",
 | 
			
		||||
    "trace-opt": "clear && rollup -c  && node --trace-opt dist/test.cjs"
 | 
			
		||||
    "start": "node --experimental-modules src/Connectors/WebsocketsConnector/server.mjs",
 | 
			
		||||
    "test": "npm run lint",
 | 
			
		||||
    "debug": "concurrently 'rollup -wc rollup.test.js' 'cutest-serve y.test.js -o'",
 | 
			
		||||
    "lint": "standard src/**/*.mjs test/**/*.mjs tests-lib/**/*.mjs",
 | 
			
		||||
    "docs": "esdoc",
 | 
			
		||||
    "serve-docs": "npm run docs && serve ./docs/",
 | 
			
		||||
    "dist": "rollup -c rollup.browser.js; rollup -c rollup.node.js",
 | 
			
		||||
    "watch": "concurrently 'rollup -wc rollup.browser.js' 'rollup -wc rollup.node.js'",
 | 
			
		||||
    "postversion": "npm run dist",
 | 
			
		||||
    "postpublish": "tag-dist-files --overwrite-existing-tag",
 | 
			
		||||
    "demos": "concurrently 'node --experimental-modules src/Connectors/WebsocketsConnector/server.mjs' 'http-server'"
 | 
			
		||||
  },
 | 
			
		||||
  "exports": {
 | 
			
		||||
    ".": {
 | 
			
		||||
      "types": "./dist/src/index.d.ts",
 | 
			
		||||
      "module": "./dist/yjs.mjs",
 | 
			
		||||
      "import": "./dist/yjs.mjs",
 | 
			
		||||
      "require": "./dist/yjs.cjs"
 | 
			
		||||
    },
 | 
			
		||||
    "./src/index.js": "./src/index.js",
 | 
			
		||||
    "./tests/testHelper.js": "./tests/testHelper.js",
 | 
			
		||||
    "./testHelper": "./dist/testHelper.mjs",
 | 
			
		||||
    "./package.json": "./package.json"
 | 
			
		||||
  "now": {
 | 
			
		||||
    "engines": {
 | 
			
		||||
      "node": "10.x.x"
 | 
			
		||||
    }
 | 
			
		||||
  },
 | 
			
		||||
  "files": [
 | 
			
		||||
    "dist/yjs.*",
 | 
			
		||||
    "dist/src",
 | 
			
		||||
    "src",
 | 
			
		||||
    "tests/testHelper.js",
 | 
			
		||||
    "dist/testHelper.mjs",
 | 
			
		||||
    "sponsor-y.js"
 | 
			
		||||
    "y.*",
 | 
			
		||||
    "src/*",
 | 
			
		||||
    ".esdoc.json",
 | 
			
		||||
    "docs/*"
 | 
			
		||||
  ],
 | 
			
		||||
  "dictionaries": {
 | 
			
		||||
    "test": "tests"
 | 
			
		||||
  },
 | 
			
		||||
  "standard": {
 | 
			
		||||
    "ignore": [
 | 
			
		||||
      "/dist",
 | 
			
		||||
      "/node_modules",
 | 
			
		||||
      "/docs"
 | 
			
		||||
      "/y.js",
 | 
			
		||||
      "/y.js.map"
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "repository": {
 | 
			
		||||
    "type": "git",
 | 
			
		||||
    "url": "https://github.com/yjs/yjs.git"
 | 
			
		||||
    "url": "https://github.com/y-js/yjs.git"
 | 
			
		||||
  },
 | 
			
		||||
  "keywords": [
 | 
			
		||||
    "Yjs",
 | 
			
		||||
    "CRDT",
 | 
			
		||||
    "offline",
 | 
			
		||||
    "offline-first",
 | 
			
		||||
    "shared-editing",
 | 
			
		||||
    "concurrency",
 | 
			
		||||
    "collaboration"
 | 
			
		||||
    "OT",
 | 
			
		||||
    "Collaboration",
 | 
			
		||||
    "Synchronization",
 | 
			
		||||
    "ShareJS",
 | 
			
		||||
    "Coweb",
 | 
			
		||||
    "Concurrency"
 | 
			
		||||
  ],
 | 
			
		||||
  "author": "Kevin Jahns",
 | 
			
		||||
  "email": "kevin.jahns@protonmail.com",
 | 
			
		||||
  "email": "kevin.jahns@rwth-aachen.de",
 | 
			
		||||
  "license": "MIT",
 | 
			
		||||
  "bugs": {
 | 
			
		||||
    "url": "https://github.com/yjs/yjs/issues"
 | 
			
		||||
  },
 | 
			
		||||
  "homepage": "https://docs.yjs.dev",
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "lib0": "^0.2.99"
 | 
			
		||||
    "url": "https://github.com/y-js/yjs/issues"
 | 
			
		||||
  },
 | 
			
		||||
  "homepage": "http://y-js.org",
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@rollup/plugin-commonjs": "^24.0.1",
 | 
			
		||||
    "@rollup/plugin-node-resolve": "^15.0.1",
 | 
			
		||||
    "@types/node": "^18.15.5",
 | 
			
		||||
    "concurrently": "^3.6.1",
 | 
			
		||||
    "http-server": "^0.12.3",
 | 
			
		||||
    "jsdoc": "^3.6.7",
 | 
			
		||||
    "markdownlint-cli": "^0.41.0",
 | 
			
		||||
    "rollup": "^3.20.0",
 | 
			
		||||
    "standard": "^16.0.4",
 | 
			
		||||
    "tui-jsdoc-template": "^1.2.2",
 | 
			
		||||
    "typescript": "^4.9.5",
 | 
			
		||||
    "y-protocols": "^1.0.5"
 | 
			
		||||
    "babel-cli": "^6.24.1",
 | 
			
		||||
    "babel-plugin-external-helpers": "^6.22.0",
 | 
			
		||||
    "babel-plugin-transform-regenerator": "^6.24.1",
 | 
			
		||||
    "babel-plugin-transform-runtime": "^6.23.0",
 | 
			
		||||
    "babel-preset-latest": "^6.24.1",
 | 
			
		||||
    "chance": "^1.0.9",
 | 
			
		||||
    "codemirror": "^5.37.0",
 | 
			
		||||
    "concurrently": "^3.4.0",
 | 
			
		||||
    "cutest": "^0.1.9",
 | 
			
		||||
    "esdoc": "^1.0.4",
 | 
			
		||||
    "esdoc-standard-plugin": "^1.0.0",
 | 
			
		||||
    "quill": "^1.3.5",
 | 
			
		||||
    "quill-cursors": "^1.0.2",
 | 
			
		||||
    "rollup": "^0.58.2",
 | 
			
		||||
    "rollup-plugin-babel": "^2.7.1",
 | 
			
		||||
    "rollup-plugin-commonjs": "^8.0.2",
 | 
			
		||||
    "rollup-plugin-inject": "^2.0.0",
 | 
			
		||||
    "rollup-plugin-multi-entry": "^2.0.1",
 | 
			
		||||
    "rollup-plugin-node-resolve": "^3.0.0",
 | 
			
		||||
    "rollup-plugin-uglify": "^1.0.2",
 | 
			
		||||
    "rollup-regenerator-runtime": "^6.23.1",
 | 
			
		||||
    "rollup-watch": "^3.2.2",
 | 
			
		||||
    "standard": "^11.0.1",
 | 
			
		||||
    "tag-dist-files": "^0.1.6"
 | 
			
		||||
  },
 | 
			
		||||
  "engines": {
 | 
			
		||||
    "npm": ">=8.0.0",
 | 
			
		||||
    "node": ">=16.0.0"
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "uws": "^10.148.0"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										46
									
								
								rollup.browser.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								rollup.browser.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,46 @@
 | 
			
		||||
import babel from 'rollup-plugin-babel'
 | 
			
		||||
import uglify from 'rollup-plugin-uglify'
 | 
			
		||||
import nodeResolve from 'rollup-plugin-node-resolve'
 | 
			
		||||
import commonjs from 'rollup-plugin-commonjs'
 | 
			
		||||
var pkg = require('./package.json')
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  input: 'src/Y.dist.mjs',
 | 
			
		||||
  name: 'Y',
 | 
			
		||||
  sourcemap: true,
 | 
			
		||||
  output: {
 | 
			
		||||
    file: 'y.js',
 | 
			
		||||
    format: 'umd'
 | 
			
		||||
  },
 | 
			
		||||
  plugins: [
 | 
			
		||||
    nodeResolve({
 | 
			
		||||
      main: true,
 | 
			
		||||
      module: true,
 | 
			
		||||
      browser: true
 | 
			
		||||
    }),
 | 
			
		||||
    commonjs(),
 | 
			
		||||
    babel(),
 | 
			
		||||
    uglify({
 | 
			
		||||
      mangle: {
 | 
			
		||||
        except: ['YMap', 'Y', 'YArray', 'YText', 'YXmlHook', 'YXmlFragment', 'YXmlElement', 'YXmlEvent', 'YXmlText', 'YEvent', 'YArrayEvent', 'YMapEvent', 'Type', 'Delete', 'ItemJSON', 'ItemString', 'Item']
 | 
			
		||||
      },
 | 
			
		||||
      output: {
 | 
			
		||||
        comments: function (node, comment) {
 | 
			
		||||
          var text = comment.value
 | 
			
		||||
          var type = comment.type
 | 
			
		||||
          if (type === 'comment2') {
 | 
			
		||||
            // multiline comment
 | 
			
		||||
            return /@license/i.test(text)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  ],
 | 
			
		||||
  banner: `
 | 
			
		||||
/**
 | 
			
		||||
 * ${pkg.name} - ${pkg.description}
 | 
			
		||||
 * @version v${pkg.version}
 | 
			
		||||
 * @license ${pkg.license}
 | 
			
		||||
 */
 | 
			
		||||
`
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										106
									
								
								rollup.config.js
									
									
									
									
									
								
							
							
						
						
									
										106
									
								
								rollup.config.js
									
									
									
									
									
								
							@ -1,106 +0,0 @@
 | 
			
		||||
import nodeResolve from '@rollup/plugin-node-resolve'
 | 
			
		||||
import commonjs from '@rollup/plugin-commonjs'
 | 
			
		||||
 | 
			
		||||
const localImports = process.env.LOCALIMPORTS
 | 
			
		||||
 | 
			
		||||
const customModules = new Set([
 | 
			
		||||
  'y-websocket',
 | 
			
		||||
  'y-codemirror',
 | 
			
		||||
  'y-ace',
 | 
			
		||||
  'y-textarea',
 | 
			
		||||
  'y-quill',
 | 
			
		||||
  'y-dom',
 | 
			
		||||
  'y-prosemirror'
 | 
			
		||||
])
 | 
			
		||||
/**
 | 
			
		||||
 * @type {Set<any>}
 | 
			
		||||
 */
 | 
			
		||||
const customLibModules = new Set([
 | 
			
		||||
  'lib0',
 | 
			
		||||
  'y-protocols'
 | 
			
		||||
])
 | 
			
		||||
const debugResolve = {
 | 
			
		||||
  resolveId (importee) {
 | 
			
		||||
    if (importee === 'yjs') {
 | 
			
		||||
      return `${process.cwd()}/src/index.js`
 | 
			
		||||
    }
 | 
			
		||||
    if (localImports) {
 | 
			
		||||
      if (customModules.has(importee.split('/')[0])) {
 | 
			
		||||
        return `${process.cwd()}/../${importee}/src/${importee}.js`
 | 
			
		||||
      }
 | 
			
		||||
      if (customLibModules.has(importee.split('/')[0])) {
 | 
			
		||||
        return `${process.cwd()}/../${importee}`
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return null
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default [{
 | 
			
		||||
  input: './src/index.js',
 | 
			
		||||
  output: {
 | 
			
		||||
    name: 'Y',
 | 
			
		||||
    file: 'dist/yjs.cjs',
 | 
			
		||||
    format: 'cjs',
 | 
			
		||||
    sourcemap: true
 | 
			
		||||
  },
 | 
			
		||||
  external: id => /^lib0\//.test(id)
 | 
			
		||||
}, {
 | 
			
		||||
  input: './src/index.js',
 | 
			
		||||
  output: {
 | 
			
		||||
    name: 'Y',
 | 
			
		||||
    file: 'dist/yjs.mjs',
 | 
			
		||||
    format: 'esm',
 | 
			
		||||
    sourcemap: true
 | 
			
		||||
  },
 | 
			
		||||
  external: id => /^lib0\//.test(id)
 | 
			
		||||
}, {
 | 
			
		||||
  input: './tests/testHelper.js',
 | 
			
		||||
  output: {
 | 
			
		||||
    name: 'Y',
 | 
			
		||||
    file: 'dist/testHelper.mjs',
 | 
			
		||||
    format: 'esm',
 | 
			
		||||
    sourcemap: true
 | 
			
		||||
  },
 | 
			
		||||
  external: id => /^lib0\//.test(id) || id === 'yjs',
 | 
			
		||||
  plugins: [{
 | 
			
		||||
    resolveId (importee) {
 | 
			
		||||
      if (importee === '../src/index.js') {
 | 
			
		||||
        return 'yjs'
 | 
			
		||||
      }
 | 
			
		||||
      return null
 | 
			
		||||
    }
 | 
			
		||||
  }]
 | 
			
		||||
}, {
 | 
			
		||||
  input: './tests/index.js',
 | 
			
		||||
  output: {
 | 
			
		||||
    name: 'test',
 | 
			
		||||
    file: 'dist/tests.js',
 | 
			
		||||
    format: 'iife',
 | 
			
		||||
    sourcemap: true
 | 
			
		||||
  },
 | 
			
		||||
  plugins: [
 | 
			
		||||
    debugResolve,
 | 
			
		||||
    nodeResolve({
 | 
			
		||||
      mainFields: ['browser', 'module', 'main']
 | 
			
		||||
    }),
 | 
			
		||||
    commonjs()
 | 
			
		||||
  ]
 | 
			
		||||
}, {
 | 
			
		||||
  input: './tests/index.js',
 | 
			
		||||
  output: {
 | 
			
		||||
    name: 'test',
 | 
			
		||||
    file: 'dist/tests.cjs',
 | 
			
		||||
    format: 'cjs',
 | 
			
		||||
    sourcemap: true
 | 
			
		||||
  },
 | 
			
		||||
  plugins: [
 | 
			
		||||
    debugResolve,
 | 
			
		||||
    nodeResolve({
 | 
			
		||||
      mainFields: ['node', 'module', 'main'],
 | 
			
		||||
      exportConditions: ['node', 'module', 'import', 'default']
 | 
			
		||||
    }),
 | 
			
		||||
    commonjs()
 | 
			
		||||
  ],
 | 
			
		||||
  external: id => /^lib0\//.test(id)
 | 
			
		||||
}]
 | 
			
		||||
							
								
								
									
										28
									
								
								rollup.node.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								rollup.node.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,28 @@
 | 
			
		||||
import nodeResolve from 'rollup-plugin-node-resolve'
 | 
			
		||||
import commonjs from 'rollup-plugin-commonjs'
 | 
			
		||||
var pkg = require('./package.json')
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  input: 'src/Y.dist.mjs',
 | 
			
		||||
  nameame: 'Y',
 | 
			
		||||
  sourcemap: true,
 | 
			
		||||
  output: {
 | 
			
		||||
    file: 'y.node.js',
 | 
			
		||||
    format: 'cjs'
 | 
			
		||||
  },
 | 
			
		||||
  plugins: [
 | 
			
		||||
    nodeResolve({
 | 
			
		||||
      main: true,
 | 
			
		||||
      module: true,
 | 
			
		||||
      browser: true
 | 
			
		||||
    }),
 | 
			
		||||
    commonjs()
 | 
			
		||||
  ],
 | 
			
		||||
  banner: `
 | 
			
		||||
/**
 | 
			
		||||
 * ${pkg.name} - ${pkg.description}
 | 
			
		||||
 * @version v${pkg.version}
 | 
			
		||||
 * @license ${pkg.license}
 | 
			
		||||
 */
 | 
			
		||||
`
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										22
									
								
								rollup.test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								rollup.test.js
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,22 @@
 | 
			
		||||
import nodeResolve from 'rollup-plugin-node-resolve'
 | 
			
		||||
import commonjs from 'rollup-plugin-commonjs'
 | 
			
		||||
import multiEntry from 'rollup-plugin-multi-entry'
 | 
			
		||||
 | 
			
		||||
export default {
 | 
			
		||||
  input: 'test/index.mjs',
 | 
			
		||||
  name: 'y-tests',
 | 
			
		||||
  sourcemap: true,
 | 
			
		||||
  output: {
 | 
			
		||||
    file: 'y.test.js',
 | 
			
		||||
    format: 'umd'
 | 
			
		||||
  },
 | 
			
		||||
  plugins: [
 | 
			
		||||
    multiEntry(),
 | 
			
		||||
    nodeResolve({
 | 
			
		||||
      main: true,
 | 
			
		||||
      module: true,
 | 
			
		||||
      browser: true
 | 
			
		||||
    }),
 | 
			
		||||
    commonjs()
 | 
			
		||||
  ]
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										47
									
								
								src/Bindings/Binding.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/Bindings/Binding.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,47 @@
 | 
			
		||||
 | 
			
		||||
import { createMutualExclude } from '../Util/mutualExclude.mjs'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Abstract class for bindings.
 | 
			
		||||
 *
 | 
			
		||||
 * A binding handles data binding from a Yjs type to a data object. For example,
 | 
			
		||||
 * you can bind a Quill editor instance to a YText instance with the `QuillBinding` class.
 | 
			
		||||
 *
 | 
			
		||||
 * It is expected that a concrete implementation accepts two parameters
 | 
			
		||||
 * (type and binding target).
 | 
			
		||||
 *
 | 
			
		||||
 * @example
 | 
			
		||||
 *   const quill = new Quill(document.createElement('div'))
 | 
			
		||||
 *   const type = y.define('quill', Y.Text)
 | 
			
		||||
 *   const binding = new Y.QuillBinding(quill, type)
 | 
			
		||||
 *
 | 
			
		||||
 */
 | 
			
		||||
export default class Binding {
 | 
			
		||||
  /**
 | 
			
		||||
   * @param {YType} type Yjs type.
 | 
			
		||||
   * @param {any} target Binding Target.
 | 
			
		||||
   */
 | 
			
		||||
  constructor (type, target) {
 | 
			
		||||
    /**
 | 
			
		||||
     * The Yjs type that is bound to `target`
 | 
			
		||||
     * @type {YType}
 | 
			
		||||
     */
 | 
			
		||||
    this.type = type
 | 
			
		||||
    /**
 | 
			
		||||
     * The target that `type` is bound to.
 | 
			
		||||
     * @type {*}
 | 
			
		||||
     */
 | 
			
		||||
    this.target = target
 | 
			
		||||
    /**
 | 
			
		||||
     * @private
 | 
			
		||||
     */
 | 
			
		||||
    this._mutualExclude = createMutualExclude()
 | 
			
		||||
  }
 | 
			
		||||
  /**
 | 
			
		||||
   * Remove all data observers (both from the type and the target).
 | 
			
		||||
   */
 | 
			
		||||
  destroy () {
 | 
			
		||||
    this.type = null
 | 
			
		||||
    this.target = null
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										56
									
								
								src/Bindings/CodeMirrorBinding/CodeMirrorBinding.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								src/Bindings/CodeMirrorBinding/CodeMirrorBinding.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,56 @@
 | 
			
		||||
 | 
			
		||||
import Binding from '../Binding.mjs'
 | 
			
		||||
import simpleDiff from '../../Util/simpleDiff.mjs'
 | 
			
		||||
import { getRelativePosition, fromRelativePosition } from '../../Util/relativePosition.mjs'
 | 
			
		||||
 | 
			
		||||
function typeObserver () {
 | 
			
		||||
  this._mutualExclude(() => {
 | 
			
		||||
    const textarea = this.target
 | 
			
		||||
    const textType = this.type
 | 
			
		||||
    const relativeStart = getRelativePosition(textType, textarea.selectionStart)
 | 
			
		||||
    const relativeEnd = getRelativePosition(textType, textarea.selectionEnd)
 | 
			
		||||
    textarea.value = textType.toString()
 | 
			
		||||
    const start = fromRelativePosition(textType._y, relativeStart)
 | 
			
		||||
    const end = fromRelativePosition(textType._y, relativeEnd)
 | 
			
		||||
    textarea.setSelectionRange(start, end)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function domObserver () {
 | 
			
		||||
  this._mutualExclude(() => {
 | 
			
		||||
    let diff = simpleDiff(this.type.toString(), this.target.value)
 | 
			
		||||
    this.type.delete(diff.pos, diff.remove)
 | 
			
		||||
    this.type.insert(diff.pos, diff.insert)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A binding that binds a YText to a dom textarea.
 | 
			
		||||
 *
 | 
			
		||||
 * This binding is automatically destroyed when its parent is deleted.
 | 
			
		||||
 *
 | 
			
		||||
 * @example
 | 
			
		||||
 *   const textare = document.createElement('textarea')
 | 
			
		||||
 *   const type = y.define('textarea', Y.Text)
 | 
			
		||||
 *   const binding = new Y.QuillBinding(type, textarea)
 | 
			
		||||
 *
 | 
			
		||||
 */
 | 
			
		||||
export default class TextareaBinding extends Binding {
 | 
			
		||||
  constructor (textType, domTextarea) {
 | 
			
		||||
    // Binding handles textType as this.type and domTextarea as this.target
 | 
			
		||||
    super(textType, domTextarea)
 | 
			
		||||
    // set initial value
 | 
			
		||||
    domTextarea.value = textType.toString()
 | 
			
		||||
    // Observers are handled by this class
 | 
			
		||||
    this._typeObserver = typeObserver.bind(this)
 | 
			
		||||
    this._domObserver = domObserver.bind(this)
 | 
			
		||||
    textType.observe(this._typeObserver)
 | 
			
		||||
    domTextarea.addEventListener('input', this._domObserver)
 | 
			
		||||
  }
 | 
			
		||||
  destroy () {
 | 
			
		||||
    // Remove everything that is handled by this class
 | 
			
		||||
    this.type.unobserve(this._typeObserver)
 | 
			
		||||
    this.target.unobserve(this._domObserver)
 | 
			
		||||
    super.destroy()
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										140
									
								
								src/Bindings/DomBinding/DomBinding.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								src/Bindings/DomBinding/DomBinding.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,140 @@
 | 
			
		||||
/* global MutationObserver */
 | 
			
		||||
 | 
			
		||||
import Binding from '../Binding.mjs'
 | 
			
		||||
import { createAssociation, removeAssociation } from './util.mjs'
 | 
			
		||||
import { beforeTransactionSelectionFixer, afterTransactionSelectionFixer } from './selection.mjs'
 | 
			
		||||
import { defaultFilter, applyFilterOnType } from './filter.mjs'
 | 
			
		||||
import typeObserver from './typeObserver.mjs'
 | 
			
		||||
import domObserver from './domObserver.mjs'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A binding that binds the children of a YXmlFragment to a DOM element.
 | 
			
		||||
 *
 | 
			
		||||
 * This binding is automatically destroyed when its parent is deleted.
 | 
			
		||||
 *
 | 
			
		||||
 * @example
 | 
			
		||||
 * const div = document.createElement('div')
 | 
			
		||||
 * const type = y.define('xml', Y.XmlFragment)
 | 
			
		||||
 * const binding = new Y.QuillBinding(type, div)
 | 
			
		||||
 *
 | 
			
		||||
 */
 | 
			
		||||
export default class DomBinding extends Binding {
 | 
			
		||||
  /**
 | 
			
		||||
   * @param {YXmlFragment} type The bind source. This is the ultimate source of
 | 
			
		||||
   *                            truth.
 | 
			
		||||
   * @param {Element} target The bind target. Mirrors the target.
 | 
			
		||||
   * @param {Object} [opts] Optional configurations
 | 
			
		||||
 | 
			
		||||
   * @param {FilterFunction} [opts.filter=defaultFilter] The filter function to use.
 | 
			
		||||
   */
 | 
			
		||||
  constructor (type, target, opts = {}) {
 | 
			
		||||
    // Binding handles textType as this.type and domTextarea as this.target
 | 
			
		||||
    super(type, target)
 | 
			
		||||
    this.opts = opts
 | 
			
		||||
    opts.document = opts.document || document
 | 
			
		||||
    opts.hooks = opts.hooks || {}
 | 
			
		||||
    this.scrollingElement = opts.scrollingElement || null
 | 
			
		||||
    /**
 | 
			
		||||
     * Maps each DOM element to the type that it is associated with.
 | 
			
		||||
     * @type {Map}
 | 
			
		||||
     */
 | 
			
		||||
    this.domToType = new Map()
 | 
			
		||||
    /**
 | 
			
		||||
     * Maps each YXml type to the DOM element that it is associated with.
 | 
			
		||||
     * @type {Map}
 | 
			
		||||
     */
 | 
			
		||||
    this.typeToDom = new Map()
 | 
			
		||||
    /**
 | 
			
		||||
     * Defines which DOM attributes and elements to filter out.
 | 
			
		||||
     * Also filters remote changes.
 | 
			
		||||
     * @type {FilterFunction}
 | 
			
		||||
     */
 | 
			
		||||
    this.filter = opts.filter || defaultFilter
 | 
			
		||||
    // set initial value
 | 
			
		||||
    target.innerHTML = ''
 | 
			
		||||
    type.forEach(child => {
 | 
			
		||||
      target.insertBefore(child.toDom(opts.document, opts.hooks, this), null)
 | 
			
		||||
    })
 | 
			
		||||
    this._typeObserver = typeObserver.bind(this)
 | 
			
		||||
    this._domObserver = (mutations) => {
 | 
			
		||||
      domObserver.call(this, mutations, opts.document)
 | 
			
		||||
    }
 | 
			
		||||
    type.observeDeep(this._typeObserver)
 | 
			
		||||
    this._mutationObserver = new MutationObserver(this._domObserver)
 | 
			
		||||
    this._mutationObserver.observe(target, {
 | 
			
		||||
      childList: true,
 | 
			
		||||
      attributes: true,
 | 
			
		||||
      characterData: true,
 | 
			
		||||
      subtree: true
 | 
			
		||||
    })
 | 
			
		||||
    const y = type._y
 | 
			
		||||
    // Force flush dom changes before Type changes are applied (they might
 | 
			
		||||
    // modify the dom)
 | 
			
		||||
    this._beforeTransactionHandler = (y, transaction, remote) => {
 | 
			
		||||
      this._domObserver(this._mutationObserver.takeRecords())
 | 
			
		||||
      beforeTransactionSelectionFixer(y, this, transaction, remote)
 | 
			
		||||
    }
 | 
			
		||||
    y.on('beforeTransaction', this._beforeTransactionHandler)
 | 
			
		||||
    this._afterTransactionHandler = (y, transaction, remote) => {
 | 
			
		||||
      afterTransactionSelectionFixer(y, this, transaction, remote)
 | 
			
		||||
      // remove associations
 | 
			
		||||
      // TODO: this could be done more efficiently
 | 
			
		||||
      // e.g. Always delete using the following approach, or removeAssociation
 | 
			
		||||
      // in dom/type-observer..
 | 
			
		||||
      transaction.deletedStructs.forEach(type => {
 | 
			
		||||
        const dom = this.typeToDom.get(type)
 | 
			
		||||
        if (dom !== undefined) {
 | 
			
		||||
          removeAssociation(this, dom, type)
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
    y.on('afterTransaction', this._afterTransactionHandler)
 | 
			
		||||
    // Before calling observers, apply dom filter to all changed and new types.
 | 
			
		||||
    this._beforeObserverCallsHandler = (y, transaction) => {
 | 
			
		||||
      // Apply dom filter to new and changed types
 | 
			
		||||
      transaction.changedTypes.forEach((subs, type) => {
 | 
			
		||||
        // Only check attributes. New types are filtered below.
 | 
			
		||||
        if ((subs.size > 1 || (subs.size === 1 && subs.has(null) === false))) {
 | 
			
		||||
          applyFilterOnType(y, this, type)
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      transaction.newTypes.forEach(type => {
 | 
			
		||||
        applyFilterOnType(y, this, type)
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
    y.on('beforeObserverCalls', this._beforeObserverCallsHandler)
 | 
			
		||||
    createAssociation(this, target, type)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * NOTE: currently does not apply filter to existing elements!
 | 
			
		||||
   * @param {FilterFunction} filter The filter function to use from now on.
 | 
			
		||||
   */
 | 
			
		||||
  setFilter (filter) {
 | 
			
		||||
    this.filter = filter
 | 
			
		||||
    // TODO: apply filter to all elements
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Remove all properties that are handled by this class.
 | 
			
		||||
   */
 | 
			
		||||
  destroy () {
 | 
			
		||||
    this.domToType = null
 | 
			
		||||
    this.typeToDom = null
 | 
			
		||||
    this.type.unobserveDeep(this._typeObserver)
 | 
			
		||||
    this._mutationObserver.disconnect()
 | 
			
		||||
    const y = this.type._y
 | 
			
		||||
    y.off('beforeTransaction', this._beforeTransactionHandler)
 | 
			
		||||
    y.off('beforeObserverCalls', this._beforeObserverCallsHandler)
 | 
			
		||||
    y.off('afterTransaction', this._afterTransactionHandler)
 | 
			
		||||
    super.destroy()
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * A filter defines which elements and attributes to share.
 | 
			
		||||
   * Return null if the node should be filtered. Otherwise return the Map of
 | 
			
		||||
   * accepted attributes.
 | 
			
		||||
   *
 | 
			
		||||
   * @typedef {function(nodeName: String, attrs: Map): Map|null} FilterFunction
 | 
			
		||||
   */
 | 
			
		||||
							
								
								
									
										144
									
								
								src/Bindings/DomBinding/domObserver.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								src/Bindings/DomBinding/domObserver.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,144 @@
 | 
			
		||||
 | 
			
		||||
import YXmlHook from '../../Types/YXml/YXmlHook.mjs'
 | 
			
		||||
import {
 | 
			
		||||
  iterateUntilUndeleted,
 | 
			
		||||
  removeAssociation,
 | 
			
		||||
  insertNodeHelper } from './util.mjs'
 | 
			
		||||
import diff from '../../Util/simpleDiff.mjs'
 | 
			
		||||
import YXmlFragment from '../../Types/YXml/YXmlFragment.mjs'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * 1. Check if any of the nodes was deleted
 | 
			
		||||
 * 2. Iterate over the children.
 | 
			
		||||
 *    2.1 If a node exists that is not yet bound to a type, insert a new node
 | 
			
		||||
 *    2.2 If _contents.length < dom.childNodes.length, fill the
 | 
			
		||||
 *        rest of _content with childNodes
 | 
			
		||||
 *    2.3 If a node was moved, delete it and
 | 
			
		||||
 *       recreate a new yxml element that is bound to that node.
 | 
			
		||||
 *       You can detect that a node was moved because expectedId
 | 
			
		||||
 *       !== actualId in the list
 | 
			
		||||
 * @private
 | 
			
		||||
 */
 | 
			
		||||
function applyChangesFromDom (binding, dom, yxml, _document) {
 | 
			
		||||
  if (yxml == null || yxml === false || yxml.constructor === YXmlHook) {
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
  const y = yxml._y
 | 
			
		||||
  const knownChildren = new Set()
 | 
			
		||||
  for (let i = dom.childNodes.length - 1; i >= 0; i--) {
 | 
			
		||||
    const type = binding.domToType.get(dom.childNodes[i])
 | 
			
		||||
    if (type !== undefined && type !== false) {
 | 
			
		||||
      knownChildren.add(type)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  // 1. Check if any of the nodes was deleted
 | 
			
		||||
  yxml.forEach(function (childType) {
 | 
			
		||||
    if (knownChildren.has(childType) === false) {
 | 
			
		||||
      childType._delete(y)
 | 
			
		||||
      removeAssociation(binding, binding.typeToDom.get(childType), childType)
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
  // 2. iterate
 | 
			
		||||
  const childNodes = dom.childNodes
 | 
			
		||||
  const len = childNodes.length
 | 
			
		||||
  let prevExpectedType = null
 | 
			
		||||
  let expectedType = iterateUntilUndeleted(yxml._start)
 | 
			
		||||
  for (let domCnt = 0; domCnt < len; domCnt++) {
 | 
			
		||||
    const childNode = childNodes[domCnt]
 | 
			
		||||
    const childType = binding.domToType.get(childNode)
 | 
			
		||||
    if (childType !== undefined) {
 | 
			
		||||
      if (childType === false) {
 | 
			
		||||
        // should be ignored or is going to be deleted
 | 
			
		||||
        continue
 | 
			
		||||
      }
 | 
			
		||||
      if (expectedType !== null) {
 | 
			
		||||
        if (expectedType !== childType) {
 | 
			
		||||
          // 2.3 Not expected node
 | 
			
		||||
          if (childType._parent !== yxml) {
 | 
			
		||||
            // child was moved from another parent
 | 
			
		||||
            // childType is going to be deleted by its previous parent
 | 
			
		||||
            removeAssociation(binding, childNode, childType)
 | 
			
		||||
          } else {
 | 
			
		||||
            // child was moved to a different position.
 | 
			
		||||
            removeAssociation(binding, childNode, childType)
 | 
			
		||||
            childType._delete(y)
 | 
			
		||||
          }
 | 
			
		||||
          prevExpectedType = insertNodeHelper(yxml, prevExpectedType, childNode, _document, binding)
 | 
			
		||||
        } else {
 | 
			
		||||
          // Found expected node. Continue.
 | 
			
		||||
          prevExpectedType = expectedType
 | 
			
		||||
          expectedType = iterateUntilUndeleted(expectedType._right)
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        // 2.2 Fill _content with child nodes
 | 
			
		||||
        prevExpectedType = insertNodeHelper(yxml, prevExpectedType, childNode, _document, binding)
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      // 2.1 A new node was found
 | 
			
		||||
      prevExpectedType = insertNodeHelper(yxml, prevExpectedType, childNode, _document, binding)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @private
 | 
			
		||||
 */
 | 
			
		||||
export default function domObserver (mutations, _document) {
 | 
			
		||||
  this._mutualExclude(() => {
 | 
			
		||||
    this.type._y.transact(() => {
 | 
			
		||||
      let diffChildren = new Set()
 | 
			
		||||
      mutations.forEach(mutation => {
 | 
			
		||||
        const dom = mutation.target
 | 
			
		||||
        const yxml = this.domToType.get(dom)
 | 
			
		||||
        if (yxml === undefined) { // In case yxml is undefined, we double check if we forgot to bind the dom
 | 
			
		||||
          let parent = dom
 | 
			
		||||
          let yParent
 | 
			
		||||
          do {
 | 
			
		||||
            parent = parent.parentElement
 | 
			
		||||
            yParent = this.domToType.get(parent)
 | 
			
		||||
          } while (yParent === undefined && parent !== null)
 | 
			
		||||
          if (yParent !== false && yParent !== undefined && yParent.constructor !== YXmlHook) {
 | 
			
		||||
            diffChildren.add(parent)
 | 
			
		||||
          }
 | 
			
		||||
          return
 | 
			
		||||
        } else if (yxml === false || yxml.constructor === YXmlHook) {
 | 
			
		||||
          // dom element is filtered / a dom hook
 | 
			
		||||
          return
 | 
			
		||||
        }
 | 
			
		||||
        switch (mutation.type) {
 | 
			
		||||
          case 'characterData':
 | 
			
		||||
            var change = diff(yxml.toString(), dom.nodeValue)
 | 
			
		||||
            yxml.delete(change.pos, change.remove)
 | 
			
		||||
            yxml.insert(change.pos, change.insert)
 | 
			
		||||
            break
 | 
			
		||||
          case 'attributes':
 | 
			
		||||
            if (yxml.constructor === YXmlFragment) {
 | 
			
		||||
              break
 | 
			
		||||
            }
 | 
			
		||||
            let name = mutation.attributeName
 | 
			
		||||
            let val = dom.getAttribute(name)
 | 
			
		||||
            // check if filter accepts attribute
 | 
			
		||||
            let attributes = new Map()
 | 
			
		||||
            attributes.set(name, val)
 | 
			
		||||
            if (yxml.constructor !== YXmlFragment && this.filter(dom.nodeName, attributes).size > 0) {
 | 
			
		||||
              if (yxml.getAttribute(name) !== val) {
 | 
			
		||||
                if (val == null) {
 | 
			
		||||
                  yxml.removeAttribute(name)
 | 
			
		||||
                } else {
 | 
			
		||||
                  yxml.setAttribute(name, val)
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
            break
 | 
			
		||||
          case 'childList':
 | 
			
		||||
            diffChildren.add(mutation.target)
 | 
			
		||||
            break
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      for (let dom of diffChildren) {
 | 
			
		||||
        const yxml = this.domToType.get(dom)
 | 
			
		||||
        applyChangesFromDom(this, dom, yxml, _document)
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										61
									
								
								src/Bindings/DomBinding/domToType.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								src/Bindings/DomBinding/domToType.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,61 @@
 | 
			
		||||
 | 
			
		||||
import YXmlText from '../../Types/YXml/YXmlText.mjs'
 | 
			
		||||
import YXmlHook from '../../Types/YXml/YXmlHook.mjs'
 | 
			
		||||
import YXmlElement from '../../Types/YXml/YXmlElement.mjs'
 | 
			
		||||
import { createAssociation, domsToTypes } from './util.mjs'
 | 
			
		||||
import { filterDomAttributes, defaultFilter } from './filter.mjs'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Creates a Yjs type (YXml) based on the contents of a DOM Element.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {Element|TextNode} element The DOM Element
 | 
			
		||||
 * @param {?Document} _document Optional. Provide the global document object
 | 
			
		||||
 * @param {Hooks} [hooks = {}] Optional. Set of Yjs Hooks
 | 
			
		||||
 * @param {Filter} [filter=defaultFilter] Optional. Dom element filter
 | 
			
		||||
 * @param {?DomBinding} binding Warning: This property is for internal use only!
 | 
			
		||||
 * @return {YXmlElement | YXmlText}
 | 
			
		||||
 */
 | 
			
		||||
export default function domToType (element, _document = document, hooks = {}, filter = defaultFilter, binding) {
 | 
			
		||||
  let type
 | 
			
		||||
  switch (element.nodeType) {
 | 
			
		||||
    case _document.ELEMENT_NODE:
 | 
			
		||||
      let hookName = null
 | 
			
		||||
      let hook
 | 
			
		||||
      // configure `hookName !== undefined` if element is a hook.
 | 
			
		||||
      if (element.hasAttribute('data-yjs-hook')) {
 | 
			
		||||
        hookName = element.getAttribute('data-yjs-hook')
 | 
			
		||||
        hook = hooks[hookName]
 | 
			
		||||
        if (hook === undefined) {
 | 
			
		||||
          console.error(`Unknown hook "${hookName}". Deleting yjsHook dataset property.`)
 | 
			
		||||
          delete element.removeAttribute('data-yjs-hook')
 | 
			
		||||
          hookName = null
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      if (hookName === null) {
 | 
			
		||||
        // Not a hook
 | 
			
		||||
        const attrs = filterDomAttributes(element, filter)
 | 
			
		||||
        if (attrs === null) {
 | 
			
		||||
          type = false
 | 
			
		||||
        } else {
 | 
			
		||||
          type = new YXmlElement(element.nodeName)
 | 
			
		||||
          attrs.forEach((val, key) => {
 | 
			
		||||
            type.setAttribute(key, val)
 | 
			
		||||
          })
 | 
			
		||||
          type.insert(0, domsToTypes(element.childNodes, document, hooks, filter, binding))
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        // Is a hook
 | 
			
		||||
        type = new YXmlHook(hookName)
 | 
			
		||||
        hook.fillType(element, type)
 | 
			
		||||
      }
 | 
			
		||||
      break
 | 
			
		||||
    case _document.TEXT_NODE:
 | 
			
		||||
      type = new YXmlText()
 | 
			
		||||
      type.insert(0, element.nodeValue)
 | 
			
		||||
      break
 | 
			
		||||
    default:
 | 
			
		||||
      throw new Error('Can\'t transform this node type to a YXml type!')
 | 
			
		||||
  }
 | 
			
		||||
  createAssociation(binding, element, type)
 | 
			
		||||
  return type
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										60
									
								
								src/Bindings/DomBinding/filter.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src/Bindings/DomBinding/filter.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,60 @@
 | 
			
		||||
import isParentOf from '../../Util/isParentOf.mjs'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Default filter method (does nothing).
 | 
			
		||||
 *
 | 
			
		||||
 * @param {String} nodeName The nodeName of the element
 | 
			
		||||
 * @param {Map} attrs Map of key-value pairs that are attributes of the node.
 | 
			
		||||
 * @return {Map | null} The allowed attributes or null, if the element should be
 | 
			
		||||
 *                      filtered.
 | 
			
		||||
 */
 | 
			
		||||
export function defaultFilter (nodeName, attrs) {
 | 
			
		||||
  // TODO: implement basic filter that filters out dangerous properties!
 | 
			
		||||
  return attrs
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 *
 | 
			
		||||
 */
 | 
			
		||||
export function filterDomAttributes (dom, filter) {
 | 
			
		||||
  const attrs = new Map()
 | 
			
		||||
  for (let i = dom.attributes.length - 1; i >= 0; i--) {
 | 
			
		||||
    const attr = dom.attributes[i]
 | 
			
		||||
    attrs.set(attr.name, attr.value)
 | 
			
		||||
  }
 | 
			
		||||
  return filter(dom.nodeName, attrs)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Applies a filter on a type.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {Y} y The Yjs instance.
 | 
			
		||||
 * @param {DomBinding} binding The DOM binding instance that has the dom filter.
 | 
			
		||||
 * @param {YXmlElement | YXmlFragment } type The type to apply the filter to.
 | 
			
		||||
 *
 | 
			
		||||
 * @private
 | 
			
		||||
 */
 | 
			
		||||
export function applyFilterOnType (y, binding, type) {
 | 
			
		||||
  if (isParentOf(binding.type, type)) {
 | 
			
		||||
    const nodeName = type.nodeName
 | 
			
		||||
    let attributes = new Map()
 | 
			
		||||
    if (type.getAttributes !== undefined) {
 | 
			
		||||
      let attrs = type.getAttributes()
 | 
			
		||||
      for (let key in attrs) {
 | 
			
		||||
        attributes.set(key, attrs[key])
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    const filteredAttributes = binding.filter(nodeName, new Map(attributes))
 | 
			
		||||
    if (filteredAttributes === null) {
 | 
			
		||||
      type._delete(y)
 | 
			
		||||
    } else {
 | 
			
		||||
      // iterate original attributes
 | 
			
		||||
      attributes.forEach((value, key) => {
 | 
			
		||||
        // delete all attributes that are not in filteredAttributes
 | 
			
		||||
        if (filteredAttributes.has(key) === false) {
 | 
			
		||||
          type.removeAttribute(key)
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										84
									
								
								src/Bindings/DomBinding/selection.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								src/Bindings/DomBinding/selection.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,84 @@
 | 
			
		||||
/* globals getSelection */
 | 
			
		||||
 | 
			
		||||
import { getRelativePosition, fromRelativePosition } from '../../Util/relativePosition.mjs'
 | 
			
		||||
 | 
			
		||||
let browserSelection = null
 | 
			
		||||
let relativeSelection = null
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @private
 | 
			
		||||
 */
 | 
			
		||||
export let beforeTransactionSelectionFixer
 | 
			
		||||
if (typeof getSelection !== 'undefined') {
 | 
			
		||||
  beforeTransactionSelectionFixer = function _beforeTransactionSelectionFixer (y, domBinding, transaction, remote) {
 | 
			
		||||
    if (!remote) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    relativeSelection = { from: null, to: null, fromY: null, toY: null }
 | 
			
		||||
    browserSelection = getSelection()
 | 
			
		||||
    const anchorNode = browserSelection.anchorNode
 | 
			
		||||
    const anchorNodeType = domBinding.domToType.get(anchorNode)
 | 
			
		||||
    if (anchorNode !== null && anchorNodeType !== undefined) {
 | 
			
		||||
      relativeSelection.from = getRelativePosition(anchorNodeType, browserSelection.anchorOffset)
 | 
			
		||||
      relativeSelection.fromY = anchorNodeType._y
 | 
			
		||||
    }
 | 
			
		||||
    const focusNode = browserSelection.focusNode
 | 
			
		||||
    const focusNodeType = domBinding.domToType.get(focusNode)
 | 
			
		||||
    if (focusNode !== null && focusNodeType !== undefined) {
 | 
			
		||||
      relativeSelection.to = getRelativePosition(focusNodeType, browserSelection.focusOffset)
 | 
			
		||||
      relativeSelection.toY = focusNodeType._y
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
} else {
 | 
			
		||||
  beforeTransactionSelectionFixer = function _fakeBeforeTransactionSelectionFixer () {}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @private
 | 
			
		||||
 */
 | 
			
		||||
export function afterTransactionSelectionFixer (y, domBinding, transaction, remote) {
 | 
			
		||||
  if (relativeSelection === null || !remote) {
 | 
			
		||||
    return
 | 
			
		||||
  }
 | 
			
		||||
  const to = relativeSelection.to
 | 
			
		||||
  const from = relativeSelection.from
 | 
			
		||||
  const fromY = relativeSelection.fromY
 | 
			
		||||
  const toY = relativeSelection.toY
 | 
			
		||||
  let shouldUpdate = false
 | 
			
		||||
  let anchorNode = browserSelection.anchorNode
 | 
			
		||||
  let anchorOffset = browserSelection.anchorOffset
 | 
			
		||||
  let focusNode = browserSelection.focusNode
 | 
			
		||||
  let focusOffset = browserSelection.focusOffset
 | 
			
		||||
  if (from !== null) {
 | 
			
		||||
    let sel = fromRelativePosition(fromY, from)
 | 
			
		||||
    if (sel !== null) {
 | 
			
		||||
      let node = domBinding.typeToDom.get(sel.type)
 | 
			
		||||
      let offset = sel.offset
 | 
			
		||||
      if (node !== anchorNode || offset !== anchorOffset) {
 | 
			
		||||
        anchorNode = node
 | 
			
		||||
        anchorOffset = offset
 | 
			
		||||
        shouldUpdate = true
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (to !== null) {
 | 
			
		||||
    let sel = fromRelativePosition(toY, to)
 | 
			
		||||
    if (sel !== null) {
 | 
			
		||||
      let node = domBinding.typeToDom.get(sel.type)
 | 
			
		||||
      let offset = sel.offset
 | 
			
		||||
      if (node !== focusNode || offset !== focusOffset) {
 | 
			
		||||
        focusNode = node
 | 
			
		||||
        focusOffset = offset
 | 
			
		||||
        shouldUpdate = true
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (shouldUpdate) {
 | 
			
		||||
    browserSelection.setBaseAndExtent(
 | 
			
		||||
      anchorNode,
 | 
			
		||||
      anchorOffset,
 | 
			
		||||
      focusNode,
 | 
			
		||||
      focusOffset
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										99
									
								
								src/Bindings/DomBinding/typeObserver.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								src/Bindings/DomBinding/typeObserver.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,99 @@
 | 
			
		||||
/* global getSelection */
 | 
			
		||||
 | 
			
		||||
import YXmlText from '../../Types/YXml/YXmlText.mjs'
 | 
			
		||||
import YXmlHook from '../../Types/YXml/YXmlHook.mjs'
 | 
			
		||||
import { removeDomChildrenUntilElementFound } from './util.mjs'
 | 
			
		||||
 | 
			
		||||
function findScrollReference (scrollingElement) {
 | 
			
		||||
  if (scrollingElement !== null) {
 | 
			
		||||
    let anchor = getSelection().anchorNode
 | 
			
		||||
    if (anchor == null) {
 | 
			
		||||
      let children = scrollingElement.children // only iterate through non-text nodes
 | 
			
		||||
      for (let i = 0; i < children.length; i++) {
 | 
			
		||||
        const elem = children[i]
 | 
			
		||||
        const rect = elem.getBoundingClientRect()
 | 
			
		||||
        if (rect.top >= 0) {
 | 
			
		||||
          return { elem, top: rect.top }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      if (anchor.nodeType === document.TEXT_NODE) {
 | 
			
		||||
        anchor = anchor.parentElement
 | 
			
		||||
      }
 | 
			
		||||
      const top = anchor.getBoundingClientRect().top
 | 
			
		||||
      return { elem: anchor, top: top }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return null
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function fixScroll (scrollingElement, ref) {
 | 
			
		||||
  if (ref !== null) {
 | 
			
		||||
    const { elem, top } = ref
 | 
			
		||||
    const currentTop = elem.getBoundingClientRect().top
 | 
			
		||||
    const newScroll = scrollingElement.scrollTop + currentTop - top
 | 
			
		||||
    if (newScroll >= 0) {
 | 
			
		||||
      scrollingElement.scrollTop = newScroll
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @private
 | 
			
		||||
 */
 | 
			
		||||
export default function typeObserver (events) {
 | 
			
		||||
  this._mutualExclude(() => {
 | 
			
		||||
    const scrollRef = findScrollReference(this.scrollingElement)
 | 
			
		||||
    events.forEach(event => {
 | 
			
		||||
      const yxml = event.target
 | 
			
		||||
      const dom = this.typeToDom.get(yxml)
 | 
			
		||||
      if (dom !== undefined && dom !== false) {
 | 
			
		||||
        if (yxml.constructor === YXmlText) {
 | 
			
		||||
          dom.nodeValue = yxml.toString()
 | 
			
		||||
        } else if (event.attributesChanged !== undefined) {
 | 
			
		||||
          // update attributes
 | 
			
		||||
          event.attributesChanged.forEach(attributeName => {
 | 
			
		||||
            const value = yxml.getAttribute(attributeName)
 | 
			
		||||
            if (value === undefined) {
 | 
			
		||||
              dom.removeAttribute(attributeName)
 | 
			
		||||
            } else {
 | 
			
		||||
              dom.setAttribute(attributeName, value)
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
          /*
 | 
			
		||||
           * TODO: instead of hard-checking the types, it would be best to
 | 
			
		||||
           *       specify the type's features. E.g.
 | 
			
		||||
           *         - _yxmlHasAttributes
 | 
			
		||||
           *         - _yxmlHasChildren
 | 
			
		||||
           *       Furthermore, the features shouldn't be encoded in the types,
 | 
			
		||||
           *       only in the attributes (above)
 | 
			
		||||
           */
 | 
			
		||||
          if (event.childListChanged && yxml.constructor !== YXmlHook) {
 | 
			
		||||
            let currentChild = dom.firstChild
 | 
			
		||||
            yxml.forEach(childType => {
 | 
			
		||||
              const childNode = this.typeToDom.get(childType)
 | 
			
		||||
              switch (childNode) {
 | 
			
		||||
                case undefined:
 | 
			
		||||
                  // Does not exist. Create it.
 | 
			
		||||
                  const node = childType.toDom(this.opts.document, this.opts.hooks, this)
 | 
			
		||||
                  dom.insertBefore(node, currentChild)
 | 
			
		||||
                  break
 | 
			
		||||
                case false:
 | 
			
		||||
                  // nop
 | 
			
		||||
                  break
 | 
			
		||||
                default:
 | 
			
		||||
                  // Is already attached to the dom.
 | 
			
		||||
                  // Find it and remove all dom nodes in-between.
 | 
			
		||||
                  removeDomChildrenUntilElementFound(dom, currentChild, childNode)
 | 
			
		||||
                  currentChild = childNode.nextSibling
 | 
			
		||||
                  break
 | 
			
		||||
              }
 | 
			
		||||
            })
 | 
			
		||||
            removeDomChildrenUntilElementFound(dom, currentChild, null)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    fixScroll(this.scrollingElement, scrollRef)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										124
									
								
								src/Bindings/DomBinding/util.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								src/Bindings/DomBinding/util.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,124 @@
 | 
			
		||||
 | 
			
		||||
import domToType from './domToType.mjs'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Iterates items until an undeleted item is found.
 | 
			
		||||
 *
 | 
			
		||||
 * @private
 | 
			
		||||
 */
 | 
			
		||||
export function iterateUntilUndeleted (item) {
 | 
			
		||||
  while (item !== null && item._deleted) {
 | 
			
		||||
    item = item._right
 | 
			
		||||
  }
 | 
			
		||||
  return item
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Removes an association (the information that a DOM element belongs to a
 | 
			
		||||
 * type).
 | 
			
		||||
 *
 | 
			
		||||
 * @param {DomBinding} domBinding The binding object
 | 
			
		||||
 * @param {Element} dom The dom that is to be associated with type
 | 
			
		||||
 * @param {YXmlElement|YXmlHook} type The type that is to be associated with dom
 | 
			
		||||
 *
 | 
			
		||||
 */
 | 
			
		||||
export function removeAssociation (domBinding, dom, type) {
 | 
			
		||||
  domBinding.domToType.delete(dom)
 | 
			
		||||
  domBinding.typeToDom.delete(type)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Creates an association (the information that a DOM element belongs to a
 | 
			
		||||
 * type).
 | 
			
		||||
 *
 | 
			
		||||
 * @param {DomBinding} domBinding The binding object
 | 
			
		||||
 * @param {Element} dom The dom that is to be associated with type
 | 
			
		||||
 * @param {YXmlElement|YXmlHook} type The type that is to be associated with dom
 | 
			
		||||
 *
 | 
			
		||||
 */
 | 
			
		||||
export function createAssociation (domBinding, dom, type) {
 | 
			
		||||
  if (domBinding !== undefined) {
 | 
			
		||||
    domBinding.domToType.set(dom, type)
 | 
			
		||||
    domBinding.typeToDom.set(type, dom)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * If oldDom is associated with a type, associate newDom with the type and
 | 
			
		||||
 * forget about oldDom. If oldDom is not associated with any type, nothing happens.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {DomBinding} domBinding The binding object
 | 
			
		||||
 * @param {Element} oldDom The existing dom
 | 
			
		||||
 * @param {Element} newDom The new dom object
 | 
			
		||||
 */
 | 
			
		||||
export function switchAssociation (domBinding, oldDom, newDom) {
 | 
			
		||||
  if (domBinding !== undefined) {
 | 
			
		||||
    const type = domBinding.domToType.get(oldDom)
 | 
			
		||||
    if (type !== undefined) {
 | 
			
		||||
      removeAssociation(domBinding, oldDom, type)
 | 
			
		||||
      createAssociation(domBinding, newDom, type)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Insert Dom Elements after one of the children of this YXmlFragment.
 | 
			
		||||
 * The Dom elements will be bound to a new YXmlElement and inserted at the
 | 
			
		||||
 * specified position.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {YXmlElement} type The type in which to insert DOM elements.
 | 
			
		||||
 * @param {YXmlElement|null} prev The reference node. New YxmlElements are
 | 
			
		||||
 *                           inserted after this node. Set null to insert at
 | 
			
		||||
 *                           the beginning.
 | 
			
		||||
 * @param {Array<Element>} doms The Dom elements to insert.
 | 
			
		||||
 * @param {?Document} _document Optional. Provide the global document object.
 | 
			
		||||
 * @param {DomBinding} binding The dom binding
 | 
			
		||||
 * @return {Array<YXmlElement>} The YxmlElements that are inserted.
 | 
			
		||||
 *
 | 
			
		||||
 * @private
 | 
			
		||||
 */
 | 
			
		||||
export function insertDomElementsAfter (type, prev, doms, _document, binding) {
 | 
			
		||||
  const types = domsToTypes(doms, _document, binding.opts.hooks, binding.filter, binding)
 | 
			
		||||
  return type.insertAfter(prev, types)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function domsToTypes (doms, _document, hooks, filter, binding) {
 | 
			
		||||
  const types = []
 | 
			
		||||
  for (let dom of doms) {
 | 
			
		||||
    const t = domToType(dom, _document, hooks, filter, binding)
 | 
			
		||||
    if (t !== false) {
 | 
			
		||||
      types.push(t)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return types
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @private
 | 
			
		||||
 */
 | 
			
		||||
export function insertNodeHelper (yxml, prevExpectedNode, child, _document, binding) {
 | 
			
		||||
  let insertedNodes = insertDomElementsAfter(yxml, prevExpectedNode, [child], _document, binding)
 | 
			
		||||
  if (insertedNodes.length > 0) {
 | 
			
		||||
    return insertedNodes[0]
 | 
			
		||||
  } else {
 | 
			
		||||
    return prevExpectedNode
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Remove children until `elem` is found.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {Element} parent The parent of `elem` and `currentChild`.
 | 
			
		||||
 * @param {Element} currentChild Start removing elements with `currentChild`. If
 | 
			
		||||
 *                               `currentChild` is `elem` it won't be removed.
 | 
			
		||||
 * @param {Element|null} elem The elemnt to look for.
 | 
			
		||||
 *
 | 
			
		||||
 * @private
 | 
			
		||||
 */
 | 
			
		||||
export function removeDomChildrenUntilElementFound (parent, currentChild, elem) {
 | 
			
		||||
  while (currentChild !== elem) {
 | 
			
		||||
    const del = currentChild
 | 
			
		||||
    currentChild = currentChild.nextSibling
 | 
			
		||||
    parent.removeChild(del)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										53
									
								
								src/Bindings/QuillBinding/QuillBinding.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/Bindings/QuillBinding/QuillBinding.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,53 @@
 | 
			
		||||
import Binding from '../Binding.mjs'
 | 
			
		||||
 | 
			
		||||
function typeObserver (event) {
 | 
			
		||||
  const quill = this.target
 | 
			
		||||
  // Force flush Quill changes.
 | 
			
		||||
  quill.update('yjs')
 | 
			
		||||
  this._mutualExclude(function () {
 | 
			
		||||
    // Apply computed delta.
 | 
			
		||||
    quill.updateContents(event.delta, 'yjs')
 | 
			
		||||
    // Force flush Quill changes. Ignore applied changes.
 | 
			
		||||
    quill.update('yjs')
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function quillObserver (delta) {
 | 
			
		||||
  this._mutualExclude(() => {
 | 
			
		||||
    this.type.applyDelta(delta.ops)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A Binding that binds a YText type to a Quill editor.
 | 
			
		||||
 *
 | 
			
		||||
 * @example
 | 
			
		||||
 * const quill = new Quill(document.createElement('div'))
 | 
			
		||||
 * const type = y.define('quill', Y.Text)
 | 
			
		||||
 * const binding = new Y.QuillBinding(quill, type)
 | 
			
		||||
 * // Now modifications on the DOM will be reflected in the Type, and the other
 | 
			
		||||
 * // way around!
 | 
			
		||||
 */
 | 
			
		||||
export default class QuillBinding extends Binding {
 | 
			
		||||
  /**
 | 
			
		||||
   * @param {YText} textType
 | 
			
		||||
   * @param {Quill} quill
 | 
			
		||||
   */
 | 
			
		||||
  constructor (textType, quill) {
 | 
			
		||||
    // Binding handles textType as this.type and quill as this.target.
 | 
			
		||||
    super(textType, quill)
 | 
			
		||||
    // Set initial value.
 | 
			
		||||
    quill.setContents(textType.toDelta(), 'yjs')
 | 
			
		||||
    // Observers are handled by this class.
 | 
			
		||||
    this._typeObserver = typeObserver.bind(this)
 | 
			
		||||
    this._quillObserver = quillObserver.bind(this)
 | 
			
		||||
    textType.observe(this._typeObserver)
 | 
			
		||||
    quill.on('text-change', this._quillObserver)
 | 
			
		||||
  }
 | 
			
		||||
  destroy () {
 | 
			
		||||
    // Remove everything that is handled by this class.
 | 
			
		||||
    this.type.unobserve(this._typeObserver)
 | 
			
		||||
    this.target.off('text-change', this._quillObserver)
 | 
			
		||||
    super.destroy()
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										56
									
								
								src/Bindings/TextareaBinding/TextareaBinding.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								src/Bindings/TextareaBinding/TextareaBinding.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,56 @@
 | 
			
		||||
 | 
			
		||||
import Binding from '../Binding.mjs'
 | 
			
		||||
import simpleDiff from '../../Util/simpleDiff.mjs'
 | 
			
		||||
import { getRelativePosition, fromRelativePosition } from '../../Util/relativePosition.mjs'
 | 
			
		||||
 | 
			
		||||
function typeObserver () {
 | 
			
		||||
  this._mutualExclude(() => {
 | 
			
		||||
    const textarea = this.target
 | 
			
		||||
    const textType = this.type
 | 
			
		||||
    const relativeStart = getRelativePosition(textType, textarea.selectionStart)
 | 
			
		||||
    const relativeEnd = getRelativePosition(textType, textarea.selectionEnd)
 | 
			
		||||
    textarea.value = textType.toString()
 | 
			
		||||
    const start = fromRelativePosition(textType._y, relativeStart)
 | 
			
		||||
    const end = fromRelativePosition(textType._y, relativeEnd)
 | 
			
		||||
    textarea.setSelectionRange(start, end)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function domObserver () {
 | 
			
		||||
  this._mutualExclude(() => {
 | 
			
		||||
    let diff = simpleDiff(this.type.toString(), this.target.value)
 | 
			
		||||
    this.type.delete(diff.pos, diff.remove)
 | 
			
		||||
    this.type.insert(diff.pos, diff.insert)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A binding that binds a YText to a dom textarea.
 | 
			
		||||
 *
 | 
			
		||||
 * This binding is automatically destroyed when its parent is deleted.
 | 
			
		||||
 *
 | 
			
		||||
 * @example
 | 
			
		||||
 *   const textare = document.createElement('textarea')
 | 
			
		||||
 *   const type = y.define('textarea', Y.Text)
 | 
			
		||||
 *   const binding = new Y.QuillBinding(type, textarea)
 | 
			
		||||
 *
 | 
			
		||||
 */
 | 
			
		||||
export default class TextareaBinding extends Binding {
 | 
			
		||||
  constructor (textType, domTextarea) {
 | 
			
		||||
    // Binding handles textType as this.type and domTextarea as this.target
 | 
			
		||||
    super(textType, domTextarea)
 | 
			
		||||
    // set initial value
 | 
			
		||||
    domTextarea.value = textType.toString()
 | 
			
		||||
    // Observers are handled by this class
 | 
			
		||||
    this._typeObserver = typeObserver.bind(this)
 | 
			
		||||
    this._domObserver = domObserver.bind(this)
 | 
			
		||||
    textType.observe(this._typeObserver)
 | 
			
		||||
    domTextarea.addEventListener('input', this._domObserver)
 | 
			
		||||
  }
 | 
			
		||||
  destroy () {
 | 
			
		||||
    // Remove everything that is handled by this class
 | 
			
		||||
    this.type.unobserve(this._typeObserver)
 | 
			
		||||
    this.target.unobserve(this._domObserver)
 | 
			
		||||
    super.destroy()
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										297
									
								
								src/Connector.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										297
									
								
								src/Connector.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,297 @@
 | 
			
		||||
import BinaryEncoder from './Util/Binary/Encoder.mjs'
 | 
			
		||||
import BinaryDecoder from './Util/Binary/Decoder.mjs'
 | 
			
		||||
 | 
			
		||||
import { sendSyncStep1, readSyncStep1 } from './MessageHandler/syncStep1.mjs'
 | 
			
		||||
import { readSyncStep2 } from './MessageHandler/syncStep2.mjs'
 | 
			
		||||
import { integrateRemoteStructs } from './MessageHandler/integrateRemoteStructs.mjs'
 | 
			
		||||
 | 
			
		||||
import debug from 'debug'
 | 
			
		||||
 | 
			
		||||
// TODO: rename Connector
 | 
			
		||||
 | 
			
		||||
export default class AbstractConnector {
 | 
			
		||||
  constructor (y, opts) {
 | 
			
		||||
    this.y = y
 | 
			
		||||
    this.opts = opts
 | 
			
		||||
    if (opts.role == null || opts.role === 'master') {
 | 
			
		||||
      this.role = 'master'
 | 
			
		||||
    } else if (opts.role === 'slave') {
 | 
			
		||||
      this.role = 'slave'
 | 
			
		||||
    } else {
 | 
			
		||||
      throw new Error("Role must be either 'master' or 'slave'!")
 | 
			
		||||
    }
 | 
			
		||||
    this.log = debug('y:connector')
 | 
			
		||||
    this.logMessage = debug('y:connector-message')
 | 
			
		||||
    this._forwardAppliedStructs = opts.forwardAppliedOperations || false // TODO: rename
 | 
			
		||||
    this.role = opts.role
 | 
			
		||||
    this.connections = new Map()
 | 
			
		||||
    this.isSynced = false
 | 
			
		||||
    this.userEventListeners = []
 | 
			
		||||
    this.whenSyncedListeners = []
 | 
			
		||||
    this.currentSyncTarget = null
 | 
			
		||||
    this.debug = opts.debug === true
 | 
			
		||||
    this.broadcastBuffer = new BinaryEncoder()
 | 
			
		||||
    this.broadcastBufferSize = 0
 | 
			
		||||
    this.protocolVersion = 11
 | 
			
		||||
    this.authInfo = opts.auth || null
 | 
			
		||||
    this.checkAuth = opts.checkAuth || function () { return Promise.resolve('write') } // default is everyone has write access
 | 
			
		||||
    if (opts.maxBufferLength == null) {
 | 
			
		||||
      this.maxBufferLength = -1
 | 
			
		||||
    } else {
 | 
			
		||||
      this.maxBufferLength = opts.maxBufferLength
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  reconnect () {
 | 
			
		||||
    this.log('reconnecting..')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  disconnect () {
 | 
			
		||||
    this.log('discronnecting..')
 | 
			
		||||
    this.connections = new Map()
 | 
			
		||||
    this.isSynced = false
 | 
			
		||||
    this.currentSyncTarget = null
 | 
			
		||||
    this.whenSyncedListeners = []
 | 
			
		||||
    return Promise.resolve()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  onUserEvent (f) {
 | 
			
		||||
    this.userEventListeners.push(f)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  removeUserEventListener (f) {
 | 
			
		||||
    this.userEventListeners = this.userEventListeners.filter(g => f !== g)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  userLeft (user) {
 | 
			
		||||
    if (this.connections.has(user)) {
 | 
			
		||||
      this.log('%s: User left %s', this.y.userID, user)
 | 
			
		||||
      this.connections.delete(user)
 | 
			
		||||
      // check if isSynced event can be sent now
 | 
			
		||||
      this._setSyncedWith(null)
 | 
			
		||||
      for (var f of this.userEventListeners) {
 | 
			
		||||
        f({
 | 
			
		||||
          action: 'userLeft',
 | 
			
		||||
          user: user
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  userJoined (user, role, auth) {
 | 
			
		||||
    if (role == null) {
 | 
			
		||||
      throw new Error('You must specify the role of the joined user!')
 | 
			
		||||
    }
 | 
			
		||||
    if (this.connections.has(user)) {
 | 
			
		||||
      throw new Error('This user already joined!')
 | 
			
		||||
    }
 | 
			
		||||
    this.log('%s: User joined %s', this.y.userID, user)
 | 
			
		||||
    this.connections.set(user, {
 | 
			
		||||
      uid: user,
 | 
			
		||||
      isSynced: false,
 | 
			
		||||
      role: role,
 | 
			
		||||
      processAfterAuth: [],
 | 
			
		||||
      processAfterSync: [],
 | 
			
		||||
      auth: auth || null,
 | 
			
		||||
      receivedSyncStep2: false
 | 
			
		||||
    })
 | 
			
		||||
    let defer = {}
 | 
			
		||||
    defer.promise = new Promise(function (resolve) { defer.resolve = resolve })
 | 
			
		||||
    this.connections.get(user).syncStep2 = defer
 | 
			
		||||
    for (var f of this.userEventListeners) {
 | 
			
		||||
      f({
 | 
			
		||||
        action: 'userJoined',
 | 
			
		||||
        user: user,
 | 
			
		||||
        role: role
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
    this._syncWithUser(user)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Execute a function _when_ we are connected.
 | 
			
		||||
  // If not connected, wait until connected
 | 
			
		||||
  whenSynced (f) {
 | 
			
		||||
    if (this.isSynced) {
 | 
			
		||||
      f()
 | 
			
		||||
    } else {
 | 
			
		||||
      this.whenSyncedListeners.push(f)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _syncWithUser (userID) {
 | 
			
		||||
    if (this.role === 'slave') {
 | 
			
		||||
      return // "The current sync has not finished or this is controlled by a master!"
 | 
			
		||||
    }
 | 
			
		||||
    sendSyncStep1(this, userID)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _fireIsSyncedListeners () {
 | 
			
		||||
    if (!this.isSynced) {
 | 
			
		||||
      this.isSynced = true
 | 
			
		||||
      // It is safer to remove this!
 | 
			
		||||
      // call whensynced listeners
 | 
			
		||||
      for (var f of this.whenSyncedListeners) {
 | 
			
		||||
        f()
 | 
			
		||||
      }
 | 
			
		||||
      this.whenSyncedListeners = []
 | 
			
		||||
      this.y._setContentReady()
 | 
			
		||||
      this.y.emit('synced')
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  send (uid, buffer) {
 | 
			
		||||
    const y = this.y
 | 
			
		||||
    if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
 | 
			
		||||
      throw new Error('Expected Message to be an ArrayBuffer or Uint8Array - don\'t use this method to send custom messages')
 | 
			
		||||
    }
 | 
			
		||||
    this.log('User%s to User%s: Send \'%y\'', y.userID, uid, buffer)
 | 
			
		||||
    this.logMessage('User%s to User%s: Send %Y', y.userID, uid, [y, buffer])
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  broadcast (buffer) {
 | 
			
		||||
    const y = this.y
 | 
			
		||||
    if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
 | 
			
		||||
      throw new Error('Expected Message to be an ArrayBuffer or Uint8Array - don\'t use this method to send custom messages')
 | 
			
		||||
    }
 | 
			
		||||
    this.log('User%s: Broadcast \'%y\'', y.userID, buffer)
 | 
			
		||||
    this.logMessage('User%s: Broadcast: %Y', y.userID, [y, buffer])
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /*
 | 
			
		||||
    Buffer operations, and broadcast them when ready.
 | 
			
		||||
  */
 | 
			
		||||
  broadcastStruct (struct) {
 | 
			
		||||
    const firstContent = this.broadcastBuffer.length === 0
 | 
			
		||||
    if (firstContent) {
 | 
			
		||||
      this.broadcastBuffer.writeVarString(this.y.room)
 | 
			
		||||
      this.broadcastBuffer.writeVarString('update')
 | 
			
		||||
      this.broadcastBufferSize = 0
 | 
			
		||||
      this.broadcastBufferSizePos = this.broadcastBuffer.pos
 | 
			
		||||
      this.broadcastBuffer.writeUint32(0)
 | 
			
		||||
    }
 | 
			
		||||
    this.broadcastBufferSize++
 | 
			
		||||
    struct._toBinary(this.broadcastBuffer)
 | 
			
		||||
    if (this.maxBufferLength > 0 && this.broadcastBuffer.length > this.maxBufferLength) {
 | 
			
		||||
      // it is necessary to send the buffer now
 | 
			
		||||
      // cache the buffer and check if server is responsive
 | 
			
		||||
      const buffer = this.broadcastBuffer
 | 
			
		||||
      buffer.setUint32(this.broadcastBufferSizePos, this.broadcastBufferSize)
 | 
			
		||||
      this.broadcastBuffer = new BinaryEncoder()
 | 
			
		||||
      this.whenRemoteResponsive().then(() => {
 | 
			
		||||
        this.broadcast(buffer.createBuffer())
 | 
			
		||||
      })
 | 
			
		||||
    } else if (firstContent) {
 | 
			
		||||
      // send the buffer when all transactions are finished
 | 
			
		||||
      // (or buffer exceeds maxBufferLength)
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        if (this.broadcastBuffer.length > 0) {
 | 
			
		||||
          const buffer = this.broadcastBuffer
 | 
			
		||||
          buffer.setUint32(this.broadcastBufferSizePos, this.broadcastBufferSize)
 | 
			
		||||
          this.broadcast(buffer.createBuffer())
 | 
			
		||||
          this.broadcastBuffer = new BinaryEncoder()
 | 
			
		||||
        }
 | 
			
		||||
      }, 0)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /*
 | 
			
		||||
   * Somehow check the responsiveness of the remote clients/server
 | 
			
		||||
   * Default behavior:
 | 
			
		||||
   *   Wait 100ms before broadcasting the next batch of operations
 | 
			
		||||
   *
 | 
			
		||||
   * Only used when maxBufferLength is set
 | 
			
		||||
   *
 | 
			
		||||
   */
 | 
			
		||||
  whenRemoteResponsive () {
 | 
			
		||||
    return new Promise(function (resolve) {
 | 
			
		||||
      setTimeout(resolve, 100)
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /*
 | 
			
		||||
    You received a raw message, and you know that it is intended for Yjs. Then call this function.
 | 
			
		||||
  */
 | 
			
		||||
  receiveMessage (sender, buffer, skipAuth) {
 | 
			
		||||
    const y = this.y
 | 
			
		||||
    const userID = y.userID
 | 
			
		||||
    skipAuth = skipAuth || false
 | 
			
		||||
    if (!(buffer instanceof ArrayBuffer || buffer instanceof Uint8Array)) {
 | 
			
		||||
      return Promise.reject(new Error('Expected Message to be an ArrayBuffer or Uint8Array!'))
 | 
			
		||||
    }
 | 
			
		||||
    if (sender === userID) {
 | 
			
		||||
      return Promise.resolve()
 | 
			
		||||
    }
 | 
			
		||||
    let decoder = new BinaryDecoder(buffer)
 | 
			
		||||
    let encoder = new BinaryEncoder()
 | 
			
		||||
    let roomname = decoder.readVarString() // read room name
 | 
			
		||||
    encoder.writeVarString(roomname)
 | 
			
		||||
    let messageType = decoder.readVarString()
 | 
			
		||||
    let senderConn = this.connections.get(sender)
 | 
			
		||||
    this.log('User%s from User%s: Receive \'%s\'', userID, sender, messageType)
 | 
			
		||||
    this.logMessage('User%s from User%s: Receive %Y', userID, sender, [y, buffer])
 | 
			
		||||
    if (senderConn == null && !skipAuth) {
 | 
			
		||||
      throw new Error('Received message from unknown peer!')
 | 
			
		||||
    }
 | 
			
		||||
    if (messageType === 'sync step 1' || messageType === 'sync step 2') {
 | 
			
		||||
      let auth = decoder.readVarUint()
 | 
			
		||||
      if (senderConn.auth == null) {
 | 
			
		||||
        senderConn.processAfterAuth.push([messageType, senderConn, decoder, encoder, sender])
 | 
			
		||||
        // check auth
 | 
			
		||||
        return this.checkAuth(auth, y, sender).then(authPermissions => {
 | 
			
		||||
          if (senderConn.auth == null) {
 | 
			
		||||
            senderConn.auth = authPermissions
 | 
			
		||||
            y.emit('userAuthenticated', {
 | 
			
		||||
              user: senderConn.uid,
 | 
			
		||||
              auth: authPermissions
 | 
			
		||||
            })
 | 
			
		||||
          }
 | 
			
		||||
          let messages = senderConn.processAfterAuth
 | 
			
		||||
          senderConn.processAfterAuth = []
 | 
			
		||||
 | 
			
		||||
          messages.forEach(m =>
 | 
			
		||||
            this.computeMessage(m[0], m[1], m[2], m[3], m[4])
 | 
			
		||||
          )
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if ((skipAuth || senderConn.auth != null) && (messageType !== 'update' || senderConn.isSynced)) {
 | 
			
		||||
      this.computeMessage(messageType, senderConn, decoder, encoder, sender, skipAuth)
 | 
			
		||||
    } else {
 | 
			
		||||
      senderConn.processAfterSync.push([messageType, senderConn, decoder, encoder, sender, false])
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  computeMessage (messageType, senderConn, decoder, encoder, sender, skipAuth) {
 | 
			
		||||
    if (messageType === 'sync step 1' && (senderConn.auth === 'write' || senderConn.auth === 'read')) {
 | 
			
		||||
      // cannot wait for sync step 1 to finish, because we may wait for sync step 2 in sync step 1 (->lock)
 | 
			
		||||
      readSyncStep1(decoder, encoder, this.y, senderConn, sender)
 | 
			
		||||
    } else {
 | 
			
		||||
      const y = this.y
 | 
			
		||||
      y.transact(function () {
 | 
			
		||||
        if (messageType === 'sync step 2' && senderConn.auth === 'write') {
 | 
			
		||||
          readSyncStep2(decoder, encoder, y, senderConn, sender)
 | 
			
		||||
        } else if (messageType === 'update' && (skipAuth || senderConn.auth === 'write')) {
 | 
			
		||||
          integrateRemoteStructs(y, decoder)
 | 
			
		||||
        } else {
 | 
			
		||||
          throw new Error('Unable to receive message')
 | 
			
		||||
        }
 | 
			
		||||
      }, true)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _setSyncedWith (user) {
 | 
			
		||||
    if (user != null) {
 | 
			
		||||
      const userConn = this.connections.get(user)
 | 
			
		||||
      userConn.isSynced = true
 | 
			
		||||
      const messages = userConn.processAfterSync
 | 
			
		||||
      userConn.processAfterSync = []
 | 
			
		||||
      messages.forEach(m => {
 | 
			
		||||
        this.computeMessage(m[0], m[1], m[2], m[3], m[4])
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
    const conns = Array.from(this.connections.values())
 | 
			
		||||
    if (conns.length > 0 && conns.every(u => u.isSynced)) {
 | 
			
		||||
      this._fireIsSyncedListeners()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										140
									
								
								src/Connectors/WebsocketsConnector/WebsocketsConnector.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								src/Connectors/WebsocketsConnector/WebsocketsConnector.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,140 @@
 | 
			
		||||
import BinaryEncoder from '../../Util/Binary/Encoder.mjs'
 | 
			
		||||
/* global WebSocket */
 | 
			
		||||
import NamedEventHandler from '../../Util/NamedEventHandler.mjs'
 | 
			
		||||
import decodeMessage, { messageSS, messageSubscribe, messageStructs } from './decodeMessage.mjs'
 | 
			
		||||
import { createMutualExclude } from '../../Util/mutualExclude.mjs'
 | 
			
		||||
import { messageCheckUpdateCounter } from './decodeMessage.mjs'
 | 
			
		||||
 | 
			
		||||
export const STATE_DISCONNECTED = 0 
 | 
			
		||||
export const STATE_CONNECTED = 1
 | 
			
		||||
 | 
			
		||||
export default class WebsocketsConnector extends NamedEventHandler {
 | 
			
		||||
  constructor (url = 'ws://localhost:1234') {
 | 
			
		||||
    super()
 | 
			
		||||
    this.url = url
 | 
			
		||||
    this._state = STATE_DISCONNECTED
 | 
			
		||||
    this._socket = null
 | 
			
		||||
    this._rooms = new Map()
 | 
			
		||||
    this._connectToServer = true
 | 
			
		||||
    this._reconnectTimeout = 300
 | 
			
		||||
    this._mutualExclude = createMutualExclude()
 | 
			
		||||
    this._persistence = null
 | 
			
		||||
    this.connect()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  getRoom (roomName) {
 | 
			
		||||
    return this._rooms.get(roomName) || { y: null, roomName, localUpdateCounter: 1 }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  syncPersistence (persistence) {
 | 
			
		||||
    this._persistence = persistence
 | 
			
		||||
    if (this._state === STATE_CONNECTED) {
 | 
			
		||||
      persistence.getAllDocuments().then(docs => {
 | 
			
		||||
        const encoder = new BinaryEncoder()
 | 
			
		||||
        docs.forEach(doc => {
 | 
			
		||||
          messageCheckUpdateCounter(doc.roomName, encoder, doc.remoteUpdateCounter)
 | 
			
		||||
        });
 | 
			
		||||
        this.send(encoder)
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  connectY (roomName, y) {
 | 
			
		||||
    let room = this._rooms.get(roomName)
 | 
			
		||||
    if (room !== undefined) {
 | 
			
		||||
      throw new Error('Room is already taken! There can be only one Yjs instance per roomName!')
 | 
			
		||||
    }
 | 
			
		||||
    this._rooms.set(roomName, {
 | 
			
		||||
      roomName,
 | 
			
		||||
      y,
 | 
			
		||||
      localUpdateCounter: 1
 | 
			
		||||
    })
 | 
			
		||||
    y.on('afterTransaction', (y, transaction) => {
 | 
			
		||||
      this._mutualExclude(() => {
 | 
			
		||||
        if (transaction.encodedStructsLen > 0) {
 | 
			
		||||
          const encoder = new BinaryEncoder()
 | 
			
		||||
          const room = this._rooms.get(roomName)
 | 
			
		||||
          messageStructs(roomName, y, encoder, transaction.encodedStructs, ++room.localUpdateCounter)
 | 
			
		||||
          this.send(encoder)
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
    if (this._state === STATE_CONNECTED) {
 | 
			
		||||
      const encoder = new BinaryEncoder()
 | 
			
		||||
      messageSS(roomName, y, encoder)
 | 
			
		||||
      messageSubscribe(roomName, y, encoder)
 | 
			
		||||
      this.send(encoder)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _setState (state) {
 | 
			
		||||
    this._state = state
 | 
			
		||||
    this.emit('stateChanged', {
 | 
			
		||||
      state: this.state
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get state () {
 | 
			
		||||
    return this._state === STATE_DISCONNECTED ? 'disconnected' : 'connected'
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _onOpen () {
 | 
			
		||||
    this._setState(STATE_CONNECTED)
 | 
			
		||||
    if (this._persistence === null) {
 | 
			
		||||
      const encoder = new BinaryEncoder()
 | 
			
		||||
      for (const [roomName, room] of this._rooms) {
 | 
			
		||||
        const y = room.y
 | 
			
		||||
        messageSS(roomName, y, encoder)
 | 
			
		||||
        messageSubscribe(roomName, y, encoder)
 | 
			
		||||
      }
 | 
			
		||||
      this.send(encoder)
 | 
			
		||||
    } else {
 | 
			
		||||
      this.syncPersistence(this._persistence)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  send (encoder) {
 | 
			
		||||
    if (encoder.length > 0 && this._socket.readyState === WebSocket.OPEN) {
 | 
			
		||||
      this._socket.send(encoder.createBuffer())
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _onClose () {
 | 
			
		||||
    this._setState(STATE_DISCONNECTED)
 | 
			
		||||
    this._socket = null
 | 
			
		||||
    if (this._connectToServer) {
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        if (this._connectToServer) {
 | 
			
		||||
          this.connect()
 | 
			
		||||
        }
 | 
			
		||||
      }, this._reconnectTimeout)
 | 
			
		||||
      this.connect()
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _onMessage (message) {
 | 
			
		||||
    if (message.data.byteLength > 0) {
 | 
			
		||||
      const reply = decodeMessage(this, message.data, null, false, this._persistence)
 | 
			
		||||
      this.send(reply)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  disconnect (code = 1000, reason = 'Client manually disconnected') {
 | 
			
		||||
    const socket = this._socket
 | 
			
		||||
    this._connectToServer = false
 | 
			
		||||
    socket.close(code, reason)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  connect () {
 | 
			
		||||
    if (this._socket === null) {
 | 
			
		||||
      const socket = new WebSocket(this.url)
 | 
			
		||||
      socket.binaryType = 'arraybuffer'
 | 
			
		||||
      this._socket = socket
 | 
			
		||||
      this._connectToServer = true
 | 
			
		||||
      // Connection opened
 | 
			
		||||
      socket.addEventListener('open', this._onOpen.bind(this))
 | 
			
		||||
      socket.addEventListener('close', this._onClose.bind(this))
 | 
			
		||||
      socket.addEventListener('message', this._onMessage.bind(this))
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										159
									
								
								src/Connectors/WebsocketsConnector/decodeMessage.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										159
									
								
								src/Connectors/WebsocketsConnector/decodeMessage.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,159 @@
 | 
			
		||||
import BinaryDecoder from '../../Util/Binary/Decoder.mjs'
 | 
			
		||||
import BinaryEncoder from '../../Util/Binary/Encoder.mjs'
 | 
			
		||||
import { readStateSet, writeStateSet } from '../../MessageHandler/stateSet.mjs'
 | 
			
		||||
import { writeStructs } from '../../MessageHandler/syncStep1.mjs'
 | 
			
		||||
import { writeDeleteSet, readDeleteSet } from '../../MessageHandler/deleteSet.mjs'
 | 
			
		||||
import { integrateRemoteStructs } from '../../MessageHandler/integrateRemoteStructs.mjs'
 | 
			
		||||
 | 
			
		||||
const CONTENT_GET_SS = 4
 | 
			
		||||
export function messageGetSS (roomName, y, encoder) {
 | 
			
		||||
  encoder.writeVarString(roomName)
 | 
			
		||||
  encoder.writeVarUint(CONTENT_GET_SS)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const CONTENT_SUBSCRIBE = 3
 | 
			
		||||
export function messageSubscribe (roomName, y, encoder) {
 | 
			
		||||
  encoder.writeVarString(roomName)
 | 
			
		||||
  encoder.writeVarUint(CONTENT_SUBSCRIBE)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const CONTENT_SS = 0
 | 
			
		||||
export function messageSS (roomName, y, encoder) {
 | 
			
		||||
  encoder.writeVarString(roomName)
 | 
			
		||||
  encoder.writeVarUint(CONTENT_SS)
 | 
			
		||||
  writeStateSet(y, encoder)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const CONTENT_STRUCTS_DSS = 2
 | 
			
		||||
export function messageStructsDSS (roomName, y, encoder, ss, updateCounter) {
 | 
			
		||||
  encoder.writeVarString(roomName)
 | 
			
		||||
  encoder.writeVarUint(CONTENT_STRUCTS_DSS)
 | 
			
		||||
  encoder.writeVarUint(updateCounter)
 | 
			
		||||
  const structsDS = new BinaryEncoder()
 | 
			
		||||
  writeStructs(y, structsDS, ss)
 | 
			
		||||
  writeDeleteSet(y, structsDS)
 | 
			
		||||
  encoder.writeVarUint(structsDS.length)
 | 
			
		||||
  encoder.writeBinaryEncoder(structsDS)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const CONTENT_STRUCTS = 5
 | 
			
		||||
export function messageStructs (roomName, y, encoder, structsBinaryEncoder, updateCounter) {
 | 
			
		||||
  encoder.writeVarString(roomName)
 | 
			
		||||
  encoder.writeVarUint(CONTENT_STRUCTS)
 | 
			
		||||
  encoder.writeVarUint(updateCounter)
 | 
			
		||||
  encoder.writeVarUint(structsBinaryEncoder.length)
 | 
			
		||||
  encoder.writeBinaryEncoder(structsBinaryEncoder)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const CONTENT_CHECK_COUNTER = 6
 | 
			
		||||
export function messageCheckUpdateCounter (roomName, encoder, updateCounter = 0) {
 | 
			
		||||
  encoder.writeVarString(roomName)
 | 
			
		||||
  encoder.writeVarUint(CONTENT_CHECK_COUNTER)
 | 
			
		||||
  encoder.writeVarUint(updateCounter)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Decodes a client-message.
 | 
			
		||||
 *
 | 
			
		||||
 * A client-message consists of multiple message-elements that are concatenated without delimiter.
 | 
			
		||||
 * Each has the following structure:
 | 
			
		||||
 * - roomName
 | 
			
		||||
 * - content_type
 | 
			
		||||
 * - content (additional info that is encoded based on the value of content_type)
 | 
			
		||||
 *
 | 
			
		||||
 * The message is encoded until no more message-elements are available.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {*} connector The connector that handles the connections
 | 
			
		||||
 * @param {*} message The binary encoded message
 | 
			
		||||
 * @param {*} ws The connection object
 | 
			
		||||
 */
 | 
			
		||||
export default function decodeMessage (connector, message, ws, isServer = false, persistence) {
 | 
			
		||||
  const decoder = new BinaryDecoder(message)
 | 
			
		||||
  const encoder = new BinaryEncoder()
 | 
			
		||||
  while (decoder.hasContent()) {
 | 
			
		||||
    const roomName = decoder.readVarString()
 | 
			
		||||
    const contentType = decoder.readVarUint()
 | 
			
		||||
    const room = connector.getRoom(roomName)
 | 
			
		||||
    const y = room.y
 | 
			
		||||
    switch (contentType) {
 | 
			
		||||
      case CONTENT_CHECK_COUNTER:
 | 
			
		||||
        const updateCounter = decoder.readVarUint()
 | 
			
		||||
        if (room.localUpdateCounter !== updateCounter) {
 | 
			
		||||
          messageGetSS(roomName, y, encoder)
 | 
			
		||||
        }
 | 
			
		||||
        connector.subscribe(roomName, ws)
 | 
			
		||||
        break
 | 
			
		||||
      case CONTENT_STRUCTS:
 | 
			
		||||
        console.log(`${roomName}: received update`)
 | 
			
		||||
        connector._mutualExclude(() => {
 | 
			
		||||
          const remoteUpdateCounter = decoder.readVarUint()
 | 
			
		||||
          persistence.setRemoteUpdateCounter(roomName, remoteUpdateCounter)
 | 
			
		||||
          const messageLen = decoder.readVarUint()
 | 
			
		||||
          if (y === null) {
 | 
			
		||||
            persistence._persistStructs(roomName, decoder.readArrayBuffer(messageLen))
 | 
			
		||||
          } else {
 | 
			
		||||
            y.transact(() => {
 | 
			
		||||
              integrateRemoteStructs(y, decoder)
 | 
			
		||||
            }, true)
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
        break
 | 
			
		||||
      case CONTENT_GET_SS:
 | 
			
		||||
        if (y !== null) {
 | 
			
		||||
          messageSS(roomName, y, encoder)
 | 
			
		||||
        } else {
 | 
			
		||||
          persistence._createYInstance(roomName).then(y => {
 | 
			
		||||
            const encoder = new BinaryEncoder()
 | 
			
		||||
            messageSS(roomName, y, encoder)
 | 
			
		||||
            connector.send(encoder, ws)
 | 
			
		||||
          })
 | 
			
		||||
        }
 | 
			
		||||
        break
 | 
			
		||||
      case CONTENT_SUBSCRIBE:
 | 
			
		||||
        connector.subscribe(roomName, ws)
 | 
			
		||||
        break
 | 
			
		||||
      case CONTENT_SS:
 | 
			
		||||
        // received state set
 | 
			
		||||
        // reply with missing content
 | 
			
		||||
        const ss = readStateSet(decoder)
 | 
			
		||||
        const sendStructsDSS = () => {
 | 
			
		||||
          if (y !== null) { // TODO: how to sync local content?
 | 
			
		||||
            const encoder = new BinaryEncoder()
 | 
			
		||||
            messageStructsDSS(roomName, y, encoder, ss, room.localUpdateCounter) // room.localUpdateHandler in case it changes
 | 
			
		||||
            if (isServer) {
 | 
			
		||||
              messageSS(roomName, y, encoder)
 | 
			
		||||
            }
 | 
			
		||||
            connector.send(encoder, ws)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        if (room.persistenceLoaded !== undefined) {
 | 
			
		||||
          room.persistenceLoaded.then(sendStructsDSS)
 | 
			
		||||
        } else {
 | 
			
		||||
          sendStructsDSS()
 | 
			
		||||
        }
 | 
			
		||||
        break
 | 
			
		||||
      case CONTENT_STRUCTS_DSS:
 | 
			
		||||
        console.log(`${roomName}: synced`)
 | 
			
		||||
        connector._mutualExclude(() => {
 | 
			
		||||
          const remoteUpdateCounter = decoder.readVarUint()
 | 
			
		||||
          persistence.setRemoteUpdateCounter(roomName, remoteUpdateCounter)
 | 
			
		||||
          const messageLen = decoder.readVarUint()
 | 
			
		||||
          if (y === null) {
 | 
			
		||||
            persistence._persistStructsDS(roomName, decoder.readArrayBuffer(messageLen))
 | 
			
		||||
          } else {
 | 
			
		||||
            y.transact(() => {
 | 
			
		||||
              integrateRemoteStructs(y, decoder)
 | 
			
		||||
              readDeleteSet(y, decoder)
 | 
			
		||||
            }, true)
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
        break
 | 
			
		||||
      default:
 | 
			
		||||
        console.error('Unexpected content type!')
 | 
			
		||||
        if (ws !== null) {
 | 
			
		||||
          ws.close() // TODO: specify reason
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return encoder
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										124
									
								
								src/Connectors/WebsocketsConnector/server.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								src/Connectors/WebsocketsConnector/server.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,124 @@
 | 
			
		||||
import Y from '../../Y.mjs'
 | 
			
		||||
import uws from 'uws'
 | 
			
		||||
import BinaryEncoder from '../../Util/Binary/Encoder.mjs'
 | 
			
		||||
import decodeMessage, { messageStructs } from './decodeMessage.mjs'
 | 
			
		||||
import FilePersistence from '../../Persistences/FilePersistence.mjs'
 | 
			
		||||
 | 
			
		||||
const WebsocketsServer = uws.Server
 | 
			
		||||
const persistence = new FilePersistence('.yjsPersisted')
 | 
			
		||||
/**
 | 
			
		||||
  * Maps from room-name to ..
 | 
			
		||||
  * {
 | 
			
		||||
  *   connections, // Set of ws-clients that listen to the room
 | 
			
		||||
  *   y            // Yjs instance that handles the room
 | 
			
		||||
  * }
 | 
			
		||||
  */
 | 
			
		||||
const rooms = new Map()
 | 
			
		||||
/**
 | 
			
		||||
 * Maps from ws-connection to Set<roomName> - the set of connected roomNames
 | 
			
		||||
 */
 | 
			
		||||
const connections = new Map()
 | 
			
		||||
const port = process.env.PORT || 1234
 | 
			
		||||
const wss = new WebsocketsServer({
 | 
			
		||||
  port,
 | 
			
		||||
  perMessageDeflate: {}
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Set of room names that are scheduled to be sweeped (destroyed because they don't have a connection anymore)
 | 
			
		||||
 */
 | 
			
		||||
const scheduledSweeps = new Set()
 | 
			
		||||
/* TODO: enable sweeping
 | 
			
		||||
setInterval(function sweepRoomes () {
 | 
			
		||||
  scheduledSweeps.forEach(roomName => {
 | 
			
		||||
    const room = rooms.get(roomName)
 | 
			
		||||
    if (room !== undefined) {
 | 
			
		||||
      if (room.connections.size === 0) {
 | 
			
		||||
        persistence.saveState(roomName, room.y).then(() => {
 | 
			
		||||
          if (room.connections.size === 0) {
 | 
			
		||||
            room.y.destroy()
 | 
			
		||||
            rooms.delete(roomName)
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
  scheduledSweeps.clear()
 | 
			
		||||
}, 5000) */ 
 | 
			
		||||
 | 
			
		||||
const wsConnector = {
 | 
			
		||||
  send: (encoder, ws) => {
 | 
			
		||||
    const message = encoder.createBuffer()
 | 
			
		||||
    ws.send(message, null, null, true)
 | 
			
		||||
  },
 | 
			
		||||
  _mutualExclude: f => { f() },
 | 
			
		||||
  subscribe: function subscribe (roomName, ws) {
 | 
			
		||||
    let roomNames = connections.get(ws)
 | 
			
		||||
    if (roomNames === undefined) {
 | 
			
		||||
      roomNames = new Set()
 | 
			
		||||
      connections.set(ws, roomNames)
 | 
			
		||||
    }
 | 
			
		||||
    roomNames.add(roomName)
 | 
			
		||||
    const room = this.getRoom(roomName)
 | 
			
		||||
    room.connections.add(ws)
 | 
			
		||||
  },
 | 
			
		||||
  getRoom: function getRoom (roomName) {
 | 
			
		||||
    let room = rooms.get(roomName)
 | 
			
		||||
    if (room === undefined) {
 | 
			
		||||
      const y = new Y(roomName, null, null, { gc: true })
 | 
			
		||||
      const persistenceLoaded = persistence.readState(roomName, y)
 | 
			
		||||
      room = {
 | 
			
		||||
        name: roomName,
 | 
			
		||||
        connections: new Set(),
 | 
			
		||||
        y,
 | 
			
		||||
        persistenceLoaded,
 | 
			
		||||
        localUpdateCounter: 1
 | 
			
		||||
      }
 | 
			
		||||
      y.on('afterTransaction', (y, transaction) => {
 | 
			
		||||
        if (transaction.encodedStructsLen > 0) {
 | 
			
		||||
          // save to persistence
 | 
			
		||||
          persistence.saveUpdate(roomName, y, transaction.encodedStructs)
 | 
			
		||||
          // forward update to clients
 | 
			
		||||
          persistence._mutex(() => { // do not broadcast if persistence.readState is called
 | 
			
		||||
            const encoder = new BinaryEncoder()
 | 
			
		||||
            messageStructs(roomName, y, encoder, transaction.encodedStructs, ++room.localUpdateCounter)
 | 
			
		||||
            const message = encoder.createBuffer()
 | 
			
		||||
            // when changed, broakcast update to all connections
 | 
			
		||||
            room.connections.forEach(conn => {
 | 
			
		||||
              conn.send(message, null, null, true)
 | 
			
		||||
            })
 | 
			
		||||
          })
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      rooms.set(roomName, room)
 | 
			
		||||
    }
 | 
			
		||||
    return room
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
wss.on('connection', (ws) => {
 | 
			
		||||
  ws.on('message', function onWSMessage (message) {
 | 
			
		||||
    if (message.byteLength > 0) {
 | 
			
		||||
      const reply = decodeMessage(wsConnector, message, ws, true, persistence)
 | 
			
		||||
      if (reply.length > 0) {
 | 
			
		||||
        ws.send(reply.createBuffer(), null, null, true)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
  ws.on('close', function onWSClose () {
 | 
			
		||||
    const roomNames = connections.get(ws)
 | 
			
		||||
    if (roomNames !== undefined) {
 | 
			
		||||
      roomNames.forEach(roomName => {
 | 
			
		||||
        const room = rooms.get(roomName)
 | 
			
		||||
        if (room !== undefined) {
 | 
			
		||||
          const connections = room.connections
 | 
			
		||||
          connections.delete(ws)
 | 
			
		||||
          if (connections.size === 0) {
 | 
			
		||||
            scheduledSweeps.add(roomName)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      connections.delete(ws)
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
})
 | 
			
		||||
							
								
								
									
										32
									
								
								src/MessageHandler/binaryEncode.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								src/MessageHandler/binaryEncode.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,32 @@
 | 
			
		||||
 | 
			
		||||
import { writeStructs } from './syncStep1.mjs'
 | 
			
		||||
import { integrateRemoteStructs } from './integrateRemoteStructs.mjs'
 | 
			
		||||
import { readDeleteSet, writeDeleteSet } from './deleteSet.mjs'
 | 
			
		||||
import BinaryEncoder from '../Util/Binary/Encoder.mjs'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Read the Decoder and fill the Yjs instance with data in the decoder.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {Y} y The Yjs instance
 | 
			
		||||
 * @param {BinaryDecoder} decoder The BinaryDecoder to read from.
 | 
			
		||||
 */
 | 
			
		||||
export function fromBinary (y, decoder) {
 | 
			
		||||
  y.transact(function () {
 | 
			
		||||
    integrateRemoteStructs(y, decoder)
 | 
			
		||||
    readDeleteSet(y, decoder)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Encode the Yjs model to binary format.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {Y} y The Yjs instance
 | 
			
		||||
 * @return {BinaryEncoder} The encoder instance that can be transformed
 | 
			
		||||
 *                         to ArrayBuffer or Buffer.
 | 
			
		||||
 */
 | 
			
		||||
export function toBinary (y) {
 | 
			
		||||
  let encoder = new BinaryEncoder()
 | 
			
		||||
  writeStructs(y, encoder, new Map())
 | 
			
		||||
  writeDeleteSet(y, encoder)
 | 
			
		||||
  return encoder
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										130
									
								
								src/MessageHandler/deleteSet.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								src/MessageHandler/deleteSet.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,130 @@
 | 
			
		||||
import { deleteItemRange } from '../Struct/Delete.mjs'
 | 
			
		||||
import ID from '../Util/ID/ID.mjs'
 | 
			
		||||
 | 
			
		||||
export function stringifyDeleteSet (y, decoder, strBuilder) {
 | 
			
		||||
  let dsLength = decoder.readUint32()
 | 
			
		||||
  for (let i = 0; i < dsLength; i++) {
 | 
			
		||||
    let user = decoder.readVarUint()
 | 
			
		||||
    strBuilder.push(' -' + user + ':')
 | 
			
		||||
    let dvLength = decoder.readVarUint()
 | 
			
		||||
    for (let j = 0; j < dvLength; j++) {
 | 
			
		||||
      let from = decoder.readVarUint()
 | 
			
		||||
      let len = decoder.readVarUint()
 | 
			
		||||
      let gc = decoder.readUint8() === 1
 | 
			
		||||
      strBuilder.push(`clock: ${from}, length: ${len}, gc: ${gc}`)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return strBuilder
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function writeDeleteSet (y, encoder) {
 | 
			
		||||
  let currentUser = null
 | 
			
		||||
  let currentLength
 | 
			
		||||
  let lastLenPos
 | 
			
		||||
 | 
			
		||||
  let numberOfUsers = 0
 | 
			
		||||
  let laterDSLenPus = encoder.pos
 | 
			
		||||
  encoder.writeUint32(0)
 | 
			
		||||
 | 
			
		||||
  y.ds.iterate(null, null, function (n) {
 | 
			
		||||
    var user = n._id.user
 | 
			
		||||
    var clock = n._id.clock
 | 
			
		||||
    var len = n.len
 | 
			
		||||
    var gc = n.gc
 | 
			
		||||
    if (currentUser !== user) {
 | 
			
		||||
      numberOfUsers++
 | 
			
		||||
      // a new user was found
 | 
			
		||||
      if (currentUser !== null) { // happens on first iteration
 | 
			
		||||
        encoder.setUint32(lastLenPos, currentLength)
 | 
			
		||||
      }
 | 
			
		||||
      currentUser = user
 | 
			
		||||
      encoder.writeVarUint(user)
 | 
			
		||||
      // pseudo-fill pos
 | 
			
		||||
      lastLenPos = encoder.pos
 | 
			
		||||
      encoder.writeUint32(0)
 | 
			
		||||
      currentLength = 0
 | 
			
		||||
    }
 | 
			
		||||
    encoder.writeVarUint(clock)
 | 
			
		||||
    encoder.writeVarUint(len)
 | 
			
		||||
    encoder.writeUint8(gc ? 1 : 0)
 | 
			
		||||
    currentLength++
 | 
			
		||||
  })
 | 
			
		||||
  if (currentUser !== null) { // happens on first iteration
 | 
			
		||||
    encoder.setUint32(lastLenPos, currentLength)
 | 
			
		||||
  }
 | 
			
		||||
  encoder.setUint32(laterDSLenPus, numberOfUsers)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function readDeleteSet (y, decoder) {
 | 
			
		||||
  let dsLength = decoder.readUint32()
 | 
			
		||||
  for (let i = 0; i < dsLength; i++) {
 | 
			
		||||
    let user = decoder.readVarUint()
 | 
			
		||||
    let dv = []
 | 
			
		||||
    let dvLength = decoder.readUint32()
 | 
			
		||||
    for (let j = 0; j < dvLength; j++) {
 | 
			
		||||
      let from = decoder.readVarUint()
 | 
			
		||||
      let len = decoder.readVarUint()
 | 
			
		||||
      let gc = decoder.readUint8() === 1
 | 
			
		||||
      dv.push([from, len, gc])
 | 
			
		||||
    }
 | 
			
		||||
    if (dvLength > 0) {
 | 
			
		||||
      let pos = 0
 | 
			
		||||
      let d = dv[pos]
 | 
			
		||||
      let deletions = []
 | 
			
		||||
      y.ds.iterate(new ID(user, 0), new ID(user, Number.MAX_VALUE), function (n) {
 | 
			
		||||
        // cases:
 | 
			
		||||
        // 1. d deletes something to the right of n
 | 
			
		||||
        //  => go to next n (break)
 | 
			
		||||
        // 2. d deletes something to the left of n
 | 
			
		||||
        //  => create deletions
 | 
			
		||||
        //  => reset d accordingly
 | 
			
		||||
        //  *)=> if d doesn't delete anything anymore, go to next d (continue)
 | 
			
		||||
        // 3. not 2) and d deletes something that also n deletes
 | 
			
		||||
        //  => reset d so that it doesn't contain n's deletion
 | 
			
		||||
        //  *)=> if d does not delete anything anymore, go to next d (continue)
 | 
			
		||||
        while (d != null) {
 | 
			
		||||
          var diff = 0 // describe the diff of length in 1) and 2)
 | 
			
		||||
          if (n._id.clock + n.len <= d[0]) {
 | 
			
		||||
            // 1)
 | 
			
		||||
            break
 | 
			
		||||
          } else if (d[0] < n._id.clock) {
 | 
			
		||||
            // 2)
 | 
			
		||||
            // delete maximum the len of d
 | 
			
		||||
            // else delete as much as possible
 | 
			
		||||
            diff = Math.min(n._id.clock - d[0], d[1])
 | 
			
		||||
            // deleteItemRange(y, user, d[0], diff, true)
 | 
			
		||||
            deletions.push([user, d[0], diff])
 | 
			
		||||
          } else {
 | 
			
		||||
            // 3)
 | 
			
		||||
            diff = n._id.clock + n.len - d[0] // never null (see 1)
 | 
			
		||||
            if (d[2] && !n.gc) {
 | 
			
		||||
              // d marks as gc'd but n does not
 | 
			
		||||
              // then delete either way
 | 
			
		||||
              // deleteItemRange(y, user, d[0], Math.min(diff, d[1]), true)
 | 
			
		||||
              deletions.push([user, d[0], Math.min(diff, d[1])])
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          if (d[1] <= diff) {
 | 
			
		||||
            // d doesn't delete anything anymore
 | 
			
		||||
            d = dv[++pos]
 | 
			
		||||
          } else {
 | 
			
		||||
            d[0] = d[0] + diff // reset pos
 | 
			
		||||
            d[1] = d[1] - diff // reset length
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      // TODO: It would be more performant to apply the deletes in the above loop
 | 
			
		||||
      // Adapt the Tree implementation to support delete while iterating
 | 
			
		||||
      for (let i = deletions.length - 1; i >= 0; i--) {
 | 
			
		||||
        const del = deletions[i]
 | 
			
		||||
        deleteItemRange(y, del[0], del[1], del[2], true)
 | 
			
		||||
      }
 | 
			
		||||
      // for the rest.. just apply it
 | 
			
		||||
      for (; pos < dv.length; pos++) {
 | 
			
		||||
        d = dv[pos]
 | 
			
		||||
        deleteItemRange(y, user, d[0], d[1], true)
 | 
			
		||||
        // deletions.push([user, d[0], d[1], d[2]])
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										109
									
								
								src/MessageHandler/integrateRemoteStructs.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								src/MessageHandler/integrateRemoteStructs.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,109 @@
 | 
			
		||||
import { getStruct } from '../Util/structReferences.mjs'
 | 
			
		||||
import BinaryDecoder from '../Util/Binary/Decoder.mjs'
 | 
			
		||||
import { logID } from './messageToString.mjs'
 | 
			
		||||
import GC from '../Struct/GC.mjs'
 | 
			
		||||
 | 
			
		||||
class MissingEntry {
 | 
			
		||||
  constructor (decoder, missing, struct) {
 | 
			
		||||
    this.decoder = decoder
 | 
			
		||||
    this.missing = missing.length
 | 
			
		||||
    this.struct = struct
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @private
 | 
			
		||||
 * Integrate remote struct
 | 
			
		||||
 * When a remote struct is integrated, other structs might be ready to ready to
 | 
			
		||||
 * integrate.
 | 
			
		||||
 */
 | 
			
		||||
function _integrateRemoteStructHelper (y, struct) {
 | 
			
		||||
  const id = struct._id
 | 
			
		||||
  if (id === undefined) {
 | 
			
		||||
    struct._integrate(y)
 | 
			
		||||
  } else {
 | 
			
		||||
    if (y.ss.getState(id.user) > id.clock) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    if (!y.gcEnabled || struct.constructor === GC || (struct._parent.constructor !== GC && struct._parent._deleted === false)) {
 | 
			
		||||
      // Is either a GC or Item with an undeleted parent
 | 
			
		||||
      // save to integrate
 | 
			
		||||
      struct._integrate(y)
 | 
			
		||||
    } else {
 | 
			
		||||
      // Is an Item. parent was deleted.
 | 
			
		||||
      struct._gc(y)
 | 
			
		||||
    }
 | 
			
		||||
    let msu = y._missingStructs.get(id.user)
 | 
			
		||||
    if (msu != null) {
 | 
			
		||||
      let clock = id.clock
 | 
			
		||||
      const finalClock = clock + struct._length
 | 
			
		||||
      for (;clock < finalClock; clock++) {
 | 
			
		||||
        const missingStructs = msu.get(clock)
 | 
			
		||||
        if (missingStructs !== undefined) {
 | 
			
		||||
          missingStructs.forEach(missingDef => {
 | 
			
		||||
            missingDef.missing--
 | 
			
		||||
            if (missingDef.missing === 0) {
 | 
			
		||||
              const decoder = missingDef.decoder
 | 
			
		||||
              let oldPos = decoder.pos
 | 
			
		||||
              let missing = missingDef.struct._fromBinary(y, decoder)
 | 
			
		||||
              decoder.pos = oldPos
 | 
			
		||||
              if (missing.length === 0) {
 | 
			
		||||
                y._readyToIntegrate.push(missingDef.struct)
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          })
 | 
			
		||||
          msu.delete(clock)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function stringifyStructs (y, decoder, strBuilder) {
 | 
			
		||||
  const len = decoder.readUint32()
 | 
			
		||||
  for (let i = 0; i < len; i++) {
 | 
			
		||||
    let reference = decoder.readVarUint()
 | 
			
		||||
    let Constr = getStruct(reference)
 | 
			
		||||
    let struct = new Constr()
 | 
			
		||||
    let missing = struct._fromBinary(y, decoder)
 | 
			
		||||
    let logMessage = '  ' + struct._logString()
 | 
			
		||||
    if (missing.length > 0) {
 | 
			
		||||
      logMessage += ' .. missing: ' + missing.map(logID).join(', ')
 | 
			
		||||
    }
 | 
			
		||||
    strBuilder.push(logMessage)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function integrateRemoteStructs (y, decoder) {
 | 
			
		||||
  const len = decoder.readUint32()
 | 
			
		||||
  for (let i = 0; i < len; i++) {
 | 
			
		||||
    let reference = decoder.readVarUint()
 | 
			
		||||
    let Constr = getStruct(reference)
 | 
			
		||||
    let struct = new Constr()
 | 
			
		||||
    let decoderPos = decoder.pos
 | 
			
		||||
    let missing = struct._fromBinary(y, decoder)
 | 
			
		||||
    if (missing.length === 0) {
 | 
			
		||||
      while (struct != null) {
 | 
			
		||||
        _integrateRemoteStructHelper(y, struct)
 | 
			
		||||
        struct = y._readyToIntegrate.shift()
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      let _decoder = new BinaryDecoder(decoder.uint8arr)
 | 
			
		||||
      _decoder.pos = decoderPos
 | 
			
		||||
      let missingEntry = new MissingEntry(_decoder, missing, struct)
 | 
			
		||||
      let missingStructs = y._missingStructs
 | 
			
		||||
      for (let i = missing.length - 1; i >= 0; i--) {
 | 
			
		||||
        let m = missing[i]
 | 
			
		||||
        if (!missingStructs.has(m.user)) {
 | 
			
		||||
          missingStructs.set(m.user, new Map())
 | 
			
		||||
        }
 | 
			
		||||
        let msu = missingStructs.get(m.user)
 | 
			
		||||
        if (!msu.has(m.clock)) {
 | 
			
		||||
          msu.set(m.clock, [])
 | 
			
		||||
        }
 | 
			
		||||
        let mArray = msu = msu.get(m.clock)
 | 
			
		||||
        mArray.push(missingEntry)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										65
									
								
								src/MessageHandler/messageToString.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								src/MessageHandler/messageToString.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,65 @@
 | 
			
		||||
import BinaryDecoder from '../Util/Binary/Decoder.mjs'
 | 
			
		||||
import { stringifyStructs } from './integrateRemoteStructs.mjs'
 | 
			
		||||
import { stringifySyncStep1 } from './syncStep1.mjs'
 | 
			
		||||
import { stringifySyncStep2 } from './syncStep2.mjs'
 | 
			
		||||
import ID from '../Util/ID/ID.mjs'
 | 
			
		||||
import RootID from '../Util/ID/RootID.mjs'
 | 
			
		||||
import Y from '../Y.mjs'
 | 
			
		||||
 | 
			
		||||
export function messageToString ([y, buffer]) {
 | 
			
		||||
  let decoder = new BinaryDecoder(buffer)
 | 
			
		||||
  decoder.readVarString() // read roomname
 | 
			
		||||
  let type = decoder.readVarString()
 | 
			
		||||
  let strBuilder = []
 | 
			
		||||
  strBuilder.push('\n === ' + type + ' ===')
 | 
			
		||||
  if (type === 'update') {
 | 
			
		||||
    stringifyStructs(y, decoder, strBuilder)
 | 
			
		||||
  } else if (type === 'sync step 1') {
 | 
			
		||||
    stringifySyncStep1(y, decoder, strBuilder)
 | 
			
		||||
  } else if (type === 'sync step 2') {
 | 
			
		||||
    stringifySyncStep2(y, decoder, strBuilder)
 | 
			
		||||
  } else {
 | 
			
		||||
    strBuilder.push('-- Unknown message type - probably an encoding issue!!!')
 | 
			
		||||
  }
 | 
			
		||||
  return strBuilder.join('\n')
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function messageToRoomname (buffer) {
 | 
			
		||||
  let decoder = new BinaryDecoder(buffer)
 | 
			
		||||
  decoder.readVarString() // roomname
 | 
			
		||||
  return decoder.readVarString() // messageType
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function logID (id) {
 | 
			
		||||
  if (id !== null && id._id != null) {
 | 
			
		||||
    id = id._id
 | 
			
		||||
  }
 | 
			
		||||
  if (id === null) {
 | 
			
		||||
    return '()'
 | 
			
		||||
  } else if (id instanceof ID) {
 | 
			
		||||
    return `(${id.user},${id.clock})`
 | 
			
		||||
  } else if (id instanceof RootID) {
 | 
			
		||||
    return `(${id.name},${id.type})`
 | 
			
		||||
  } else if (id.constructor === Y) {
 | 
			
		||||
    return `y`
 | 
			
		||||
  } else {
 | 
			
		||||
    throw new Error('This is not a valid ID!')
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Helper utility to convert an item to a readable format.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {String} name The name of the item class (YText, ItemString, ..).
 | 
			
		||||
 * @param {Item} item The item instance.
 | 
			
		||||
 * @param {String} [append] Additional information to append to the returned
 | 
			
		||||
 *                          string.
 | 
			
		||||
 * @return {String} A readable string that represents the item object.
 | 
			
		||||
 *
 | 
			
		||||
 * @private
 | 
			
		||||
 */
 | 
			
		||||
export function logItemHelper (name, item, append) {
 | 
			
		||||
  const left = item._left !== null ? item._left._lastId : null
 | 
			
		||||
  const origin = item._origin !== null ? item._origin._lastId : null
 | 
			
		||||
  return `${name}(id:${logID(item._id)},left:${logID(left)},origin:${logID(origin)},right:${logID(item._right)},parent:${logID(item._parent)},parentSub:${item._parentSub}${append !== undefined ? ' - ' + append : ''})`
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										23
									
								
								src/MessageHandler/stateSet.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								src/MessageHandler/stateSet.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,23 @@
 | 
			
		||||
 | 
			
		||||
export function readStateSet (decoder) {
 | 
			
		||||
  let ss = new Map()
 | 
			
		||||
  let ssLength = decoder.readUint32()
 | 
			
		||||
  for (let i = 0; i < ssLength; i++) {
 | 
			
		||||
    let user = decoder.readVarUint()
 | 
			
		||||
    let clock = decoder.readVarUint()
 | 
			
		||||
    ss.set(user, clock)
 | 
			
		||||
  }
 | 
			
		||||
  return ss
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function writeStateSet (y, encoder) {
 | 
			
		||||
  let lenPosition = encoder.pos
 | 
			
		||||
  let len = 0
 | 
			
		||||
  encoder.writeUint32(0)
 | 
			
		||||
  for (let [user, clock] of y.ss.state) {
 | 
			
		||||
    encoder.writeVarUint(user)
 | 
			
		||||
    encoder.writeVarUint(clock)
 | 
			
		||||
    len++
 | 
			
		||||
  }
 | 
			
		||||
  encoder.setUint32(lenPosition, len)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										83
									
								
								src/MessageHandler/syncStep1.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								src/MessageHandler/syncStep1.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,83 @@
 | 
			
		||||
import BinaryEncoder from '../Util/Binary/Encoder.mjs'
 | 
			
		||||
import { readStateSet, writeStateSet } from './stateSet.mjs'
 | 
			
		||||
import { writeDeleteSet } from './deleteSet.mjs'
 | 
			
		||||
import ID from '../Util/ID/ID.mjs'
 | 
			
		||||
import { RootFakeUserID } from '../Util/ID/RootID.mjs'
 | 
			
		||||
 | 
			
		||||
export function stringifySyncStep1 (y, decoder, strBuilder) {
 | 
			
		||||
  let auth = decoder.readVarString()
 | 
			
		||||
  let protocolVersion = decoder.readVarUint()
 | 
			
		||||
  strBuilder.push(`  - auth: "${auth}"`)
 | 
			
		||||
  strBuilder.push(`  - protocolVersion: ${protocolVersion}`)
 | 
			
		||||
  // write SS
 | 
			
		||||
  let ssBuilder = []
 | 
			
		||||
  let len = decoder.readUint32()
 | 
			
		||||
  for (let i = 0; i < len; i++) {
 | 
			
		||||
    let user = decoder.readVarUint()
 | 
			
		||||
    let clock = decoder.readVarUint()
 | 
			
		||||
    ssBuilder.push(`(${user}:${clock})`)
 | 
			
		||||
  }
 | 
			
		||||
  strBuilder.push('  == SS: ' + ssBuilder.join(','))
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function sendSyncStep1 (connector, syncUser) {
 | 
			
		||||
  let encoder = new BinaryEncoder()
 | 
			
		||||
  encoder.writeVarString(connector.y.room)
 | 
			
		||||
  encoder.writeVarString('sync step 1')
 | 
			
		||||
  encoder.writeVarString(connector.authInfo || '')
 | 
			
		||||
  encoder.writeVarUint(connector.protocolVersion)
 | 
			
		||||
  writeStateSet(connector.y, encoder)
 | 
			
		||||
  connector.send(syncUser, encoder.createBuffer())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @private
 | 
			
		||||
 * Write all Items that are not not included in ss to
 | 
			
		||||
 * the encoder object.
 | 
			
		||||
 */
 | 
			
		||||
export function writeStructs (y, encoder, ss) {
 | 
			
		||||
  const lenPos = encoder.pos
 | 
			
		||||
  encoder.writeUint32(0)
 | 
			
		||||
  let len = 0
 | 
			
		||||
  for (let user of y.ss.state.keys()) {
 | 
			
		||||
    let clock = ss.get(user) || 0
 | 
			
		||||
    if (user !== RootFakeUserID) {
 | 
			
		||||
      const minBound = new ID(user, clock)
 | 
			
		||||
      const overlappingLeft = y.os.findPrev(minBound)
 | 
			
		||||
      const rightID = overlappingLeft === null ? null : overlappingLeft._id
 | 
			
		||||
      if (rightID !== null && rightID.user === user && rightID.clock + overlappingLeft._length > clock) {
 | 
			
		||||
        const struct = overlappingLeft._clonePartial(clock - rightID.clock)
 | 
			
		||||
        struct._toBinary(encoder)
 | 
			
		||||
        len++
 | 
			
		||||
      }
 | 
			
		||||
      y.os.iterate(minBound, new ID(user, Number.MAX_VALUE), function (struct) {
 | 
			
		||||
        struct._toBinary(encoder)
 | 
			
		||||
        len++
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  encoder.setUint32(lenPos, len)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function readSyncStep1 (decoder, encoder, y, senderConn, sender) {
 | 
			
		||||
  let protocolVersion = decoder.readVarUint()
 | 
			
		||||
  // check protocol version
 | 
			
		||||
  if (protocolVersion !== y.connector.protocolVersion) {
 | 
			
		||||
    console.warn(
 | 
			
		||||
      `You tried to sync with a Yjs instance that has a different protocol version
 | 
			
		||||
      (You: ${protocolVersion}, Client: ${protocolVersion}).
 | 
			
		||||
      `)
 | 
			
		||||
    y.destroy()
 | 
			
		||||
  }
 | 
			
		||||
  // write sync step 2
 | 
			
		||||
  encoder.writeVarString('sync step 2')
 | 
			
		||||
  encoder.writeVarString(y.connector.authInfo || '')
 | 
			
		||||
  const ss = readStateSet(decoder)
 | 
			
		||||
  writeStructs(y, encoder, ss)
 | 
			
		||||
  writeDeleteSet(y, encoder)
 | 
			
		||||
  y.connector.send(senderConn.uid, encoder.createBuffer())
 | 
			
		||||
  senderConn.receivedSyncStep2 = true
 | 
			
		||||
  if (y.connector.role === 'slave') {
 | 
			
		||||
    sendSyncStep1(y.connector, sender)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										28
									
								
								src/MessageHandler/syncStep2.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/MessageHandler/syncStep2.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,28 @@
 | 
			
		||||
import { stringifyStructs, integrateRemoteStructs } from './integrateRemoteStructs.mjs'
 | 
			
		||||
import { readDeleteSet } from './deleteSet.mjs'
 | 
			
		||||
 | 
			
		||||
export function stringifySyncStep2 (y, decoder, strBuilder) {
 | 
			
		||||
  strBuilder.push('     - auth: ' + decoder.readVarString())
 | 
			
		||||
  strBuilder.push('  == OS:')
 | 
			
		||||
  stringifyStructs(y, decoder, strBuilder)
 | 
			
		||||
  // write DS to string
 | 
			
		||||
  strBuilder.push('  == DS:')
 | 
			
		||||
  let len = decoder.readUint32()
 | 
			
		||||
  for (let i = 0; i < len; i++) {
 | 
			
		||||
    let user = decoder.readVarUint()
 | 
			
		||||
    strBuilder.push(`    User: ${user}: `)
 | 
			
		||||
    let len2 = decoder.readUint32()
 | 
			
		||||
    for (let j = 0; j < len2; j++) {
 | 
			
		||||
      let from = decoder.readVarUint()
 | 
			
		||||
      let to = decoder.readVarUint()
 | 
			
		||||
      let gc = decoder.readUint8() === 1
 | 
			
		||||
      strBuilder.push(`[${from}, ${to}, ${gc}]`)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function readSyncStep2 (decoder, encoder, y, senderConn, sender) {
 | 
			
		||||
  integrateRemoteStructs(y, decoder)
 | 
			
		||||
  readDeleteSet(y, decoder)
 | 
			
		||||
  y.connector._setSyncedWith(sender)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										122
									
								
								src/Persistence.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								src/Persistence.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,122 @@
 | 
			
		||||
import BinaryEncoder from './Util/Binary/Encoder.mjs'
 | 
			
		||||
import BinaryDecoder from './Util/Binary/Decoder.mjs'
 | 
			
		||||
import { toBinary, fromBinary } from './MessageHandler/binaryEncode.mjs'
 | 
			
		||||
import { integrateRemoteStructs } from './MessageHandler/integrateRemoteStructs.mjs'
 | 
			
		||||
import { createMutualExclude } from './Util/mutualExclude.mjs'
 | 
			
		||||
 | 
			
		||||
function getFreshCnf () {
 | 
			
		||||
  let buffer = new BinaryEncoder()
 | 
			
		||||
  buffer.writeUint32(0)
 | 
			
		||||
  return {
 | 
			
		||||
    len: 0,
 | 
			
		||||
    buffer
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Abstract persistence class.
 | 
			
		||||
 */
 | 
			
		||||
export default class AbstractPersistence {
 | 
			
		||||
  constructor (opts) {
 | 
			
		||||
    this.opts = opts
 | 
			
		||||
    this.ys = new Map()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _init (y) {
 | 
			
		||||
    let cnf = this.ys.get(y)
 | 
			
		||||
    if (cnf === undefined) {
 | 
			
		||||
      cnf = getFreshCnf()
 | 
			
		||||
      cnf.mutualExclude = createMutualExclude()
 | 
			
		||||
      this.ys.set(y, cnf)
 | 
			
		||||
      return this.init(y).then(() => {
 | 
			
		||||
        y.on('afterTransaction', (y, transaction) => {
 | 
			
		||||
          let cnf = this.ys.get(y)
 | 
			
		||||
          if (cnf.len > 0) {
 | 
			
		||||
            cnf.buffer.setUint32(0, cnf.len)
 | 
			
		||||
            this.saveUpdate(y, cnf.buffer.createBuffer(), transaction)
 | 
			
		||||
            let _cnf = getFreshCnf()
 | 
			
		||||
            for (let key in _cnf) {
 | 
			
		||||
              cnf[key] = _cnf[key]
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
        return this.retrieve(y)
 | 
			
		||||
      }).then(function () {
 | 
			
		||||
        return Promise.resolve(cnf)
 | 
			
		||||
      })
 | 
			
		||||
    } else {
 | 
			
		||||
      return Promise.resolve(cnf)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  deinit (y) {
 | 
			
		||||
    this.ys.delete(y)
 | 
			
		||||
    y.persistence = null
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  destroy () {
 | 
			
		||||
    this.ys = null
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Remove all persisted data that belongs to a room.
 | 
			
		||||
   * Automatically destroys all Yjs all Yjs instances that persist to
 | 
			
		||||
   * the room. If `destroyYjsInstances = false` the persistence functionality
 | 
			
		||||
   * will be removed from the Yjs instances.
 | 
			
		||||
   *
 | 
			
		||||
   * ** Must be overwritten! **
 | 
			
		||||
   */
 | 
			
		||||
  removePersistedData (room, destroyYjsInstances = true) {
 | 
			
		||||
    this.ys.forEach((cnf, y) => {
 | 
			
		||||
      if (y.room === room) {
 | 
			
		||||
        if (destroyYjsInstances) {
 | 
			
		||||
          y.destroy()
 | 
			
		||||
        } else {
 | 
			
		||||
          this.deinit(y)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* overwrite */
 | 
			
		||||
  saveUpdate (buffer) {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Save struct to update buffer.
 | 
			
		||||
   * saveUpdate is called when transaction ends
 | 
			
		||||
   */
 | 
			
		||||
  saveStruct (y, struct) {
 | 
			
		||||
    let cnf = this.ys.get(y)
 | 
			
		||||
    if (cnf !== undefined) {
 | 
			
		||||
      cnf.mutualExclude(function () {
 | 
			
		||||
        struct._toBinary(cnf.buffer)
 | 
			
		||||
        cnf.len++
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* overwrite */
 | 
			
		||||
  retrieve (y, model, updates) {
 | 
			
		||||
    let cnf = this.ys.get(y)
 | 
			
		||||
    if (cnf !== undefined) {
 | 
			
		||||
      cnf.mutualExclude(function () {
 | 
			
		||||
        y.transact(function () {
 | 
			
		||||
          if (model != null) {
 | 
			
		||||
            fromBinary(y, new BinaryDecoder(new Uint8Array(model)))
 | 
			
		||||
          }
 | 
			
		||||
          if (updates != null) {
 | 
			
		||||
            for (let i = 0; i < updates.length; i++) {
 | 
			
		||||
              integrateRemoteStructs(y, new BinaryDecoder(new Uint8Array(updates[i])))
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
        y.emit('persistenceReady')
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /* overwrite */
 | 
			
		||||
  persist (y) {
 | 
			
		||||
    return toBinary(y).createBuffer()
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										2
									
								
								src/Persistences/AbstractPersistence.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/Persistences/AbstractPersistence.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,2 @@
 | 
			
		||||
 | 
			
		||||
export default class AbstractPersistence {}
 | 
			
		||||
							
								
								
									
										72
									
								
								src/Persistences/FilePersistence.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								src/Persistences/FilePersistence.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,72 @@
 | 
			
		||||
import fs from 'fs'
 | 
			
		||||
import path from 'path'
 | 
			
		||||
import BinaryDecoder from '../Util/Binary/Decoder.mjs'
 | 
			
		||||
import BinaryEncoder from '../Util/Binary/Encoder.mjs'
 | 
			
		||||
import { createMutualExclude } from '../Util/mutualExclude.mjs'
 | 
			
		||||
import { encodeUpdate, encodeStructsDS, decodePersisted } from './decodePersisted.mjs'
 | 
			
		||||
 | 
			
		||||
function createFilePath (persistence, roomName) {
 | 
			
		||||
  // TODO: filename checking!
 | 
			
		||||
  return path.join(persistence.dir, roomName)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default class FilePersistence {
 | 
			
		||||
  constructor (dir) {
 | 
			
		||||
    this.dir = dir
 | 
			
		||||
    this._mutex = createMutualExclude()
 | 
			
		||||
  }
 | 
			
		||||
  setRemoteUpdateCounter (roomName, remoteUpdateCounter) {
 | 
			
		||||
    // TODO: implement
 | 
			
		||||
    // nop
 | 
			
		||||
  }
 | 
			
		||||
  saveUpdate (room, y, encodedStructs) {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      this._mutex(() => {
 | 
			
		||||
        const filePath = createFilePath(this, room)
 | 
			
		||||
        const updateMessage = new BinaryEncoder()
 | 
			
		||||
        encodeUpdate(y, encodedStructs, updateMessage)
 | 
			
		||||
        fs.appendFile(filePath, Buffer.from(updateMessage.createBuffer()), (err) => {
 | 
			
		||||
          if (err !== null) {
 | 
			
		||||
            reject(err)
 | 
			
		||||
          } else {
 | 
			
		||||
            resolve()
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
      }, resolve)
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
  saveState (roomName, y) {
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      const encoder = new BinaryEncoder()
 | 
			
		||||
      encodeStructsDS(y, encoder)
 | 
			
		||||
      const filePath = createFilePath(this, roomName)
 | 
			
		||||
      fs.writeFile(filePath, Buffer.from(encoder.createBuffer()), (err) => {
 | 
			
		||||
        if (err !== null) {
 | 
			
		||||
          reject(err)
 | 
			
		||||
        } else {
 | 
			
		||||
          resolve()
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
  readState (roomName, y) {
 | 
			
		||||
    // Check if the file exists in the current directory.
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      const filePath = path.join(this.dir, roomName)
 | 
			
		||||
      fs.readFile(filePath, (err, data) => {
 | 
			
		||||
        if (err !== null) {
 | 
			
		||||
          resolve()
 | 
			
		||||
          // reject(err)
 | 
			
		||||
        } else {
 | 
			
		||||
          this._mutex(() => {
 | 
			
		||||
            console.info(`unpacking data (${data.length})`)
 | 
			
		||||
            console.time('unpacking')
 | 
			
		||||
            decodePersisted(y, new BinaryDecoder(data))
 | 
			
		||||
            console.timeEnd('unpacking')
 | 
			
		||||
          })
 | 
			
		||||
          resolve()
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										283
									
								
								src/Persistences/IndexedDBPersistence.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										283
									
								
								src/Persistences/IndexedDBPersistence.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,283 @@
 | 
			
		||||
/* global indexedDB, location, BroadcastChannel */
 | 
			
		||||
 | 
			
		||||
import Y from '../Y.mjs'
 | 
			
		||||
import { createMutualExclude } from '../Util/mutualExclude.mjs'
 | 
			
		||||
import { decodePersisted, encodeStructsDS, encodeUpdate } from './decodePersisted.mjs'
 | 
			
		||||
import BinaryDecoder from '../Util/Binary/Decoder.mjs'
 | 
			
		||||
import BinaryEncoder from '../Util/Binary/Encoder.mjs'
 | 
			
		||||
import { PERSIST_STRUCTS_DS } from './decodePersisted.mjs';
 | 
			
		||||
import { PERSIST_UPDATE } from './decodePersisted.mjs';
 | 
			
		||||
/*
 | 
			
		||||
 * Request to Promise transformer
 | 
			
		||||
 */
 | 
			
		||||
function rtop (request) {
 | 
			
		||||
  return new Promise(function (resolve, reject) {
 | 
			
		||||
    request.onerror = function (event) {
 | 
			
		||||
      reject(new Error(event.target.error))
 | 
			
		||||
    }
 | 
			
		||||
    request.onblocked = function () {
 | 
			
		||||
      location.reload()
 | 
			
		||||
    }
 | 
			
		||||
    request.onsuccess = function (event) {
 | 
			
		||||
      resolve(event.target.result)
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function openDB (room) {
 | 
			
		||||
  return new Promise(function (resolve, reject) {
 | 
			
		||||
    let request = indexedDB.open(room)
 | 
			
		||||
    request.onupgradeneeded = function (event) {
 | 
			
		||||
      const db = event.target.result
 | 
			
		||||
      if (db.objectStoreNames.contains('updates')) {
 | 
			
		||||
        db.deleteObjectStore('updates')
 | 
			
		||||
      }
 | 
			
		||||
      db.createObjectStore('updates', {autoIncrement: true})
 | 
			
		||||
    }
 | 
			
		||||
    request.onerror = function (event) {
 | 
			
		||||
      reject(new Error(event.target.error))
 | 
			
		||||
    }
 | 
			
		||||
    request.onblocked = function () {
 | 
			
		||||
      location.reload()
 | 
			
		||||
    }
 | 
			
		||||
    request.onsuccess = function (event) {
 | 
			
		||||
      const db = event.target.result
 | 
			
		||||
      db.onversionchange = function () { db.close() }
 | 
			
		||||
      resolve(db)
 | 
			
		||||
    }
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function persist (room) {
 | 
			
		||||
  let t = room.db.transaction(['updates'], 'readwrite')
 | 
			
		||||
  let updatesStore = t.objectStore('updates')
 | 
			
		||||
  return rtop(updatesStore.getAll())
 | 
			
		||||
  .then(updates => {
 | 
			
		||||
    // apply all previous updates before deleting them
 | 
			
		||||
    room.mutex(() => {
 | 
			
		||||
      updates.forEach(update => {
 | 
			
		||||
        decodePersisted(y, new BinaryDecoder(update))
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
    const encoder = new BinaryEncoder()
 | 
			
		||||
    encodeStructsDS(y, encoder)
 | 
			
		||||
    // delete all pending updates
 | 
			
		||||
    rtop(updatesStore.clear()).then(() => {
 | 
			
		||||
      // write current model
 | 
			
		||||
      updatesStore.put(encoder.createBuffer())
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function saveUpdate (room, updateBuffer) {
 | 
			
		||||
  const db = room.db
 | 
			
		||||
  if (db !== null) {
 | 
			
		||||
    const t = db.transaction(['updates'], 'readwrite')
 | 
			
		||||
    const updatesStore = t.objectStore('updates')
 | 
			
		||||
    const updatePut = rtop(updatesStore.put(updateBuffer))
 | 
			
		||||
    rtop(updatesStore.count()).then(cnt => {
 | 
			
		||||
      if (cnt >= PREFERRED_TRIM_SIZE) {
 | 
			
		||||
        persist(room)
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    return updatePut
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function registerRoomInPersistence (documentsDB, roomName) {
 | 
			
		||||
  return documentsDB.then(
 | 
			
		||||
    db => Promise.all([
 | 
			
		||||
      db,
 | 
			
		||||
      rtop(db.transaction(['documents'], 'readonly').objectStore('documents').get(roomName))
 | 
			
		||||
    ])
 | 
			
		||||
  ).then(
 | 
			
		||||
    ([db, doc]) => {
 | 
			
		||||
      if (doc === undefined) {
 | 
			
		||||
        return rtop(db.transaction(['documents'], 'readwrite').objectStore('documents').add({ roomName, serverUpdateCounter: 0 }))
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const PREFERRED_TRIM_SIZE = 400
 | 
			
		||||
 | 
			
		||||
export default class IndexedDBPersistence {
 | 
			
		||||
  constructor () {
 | 
			
		||||
    this._rooms = new Map()
 | 
			
		||||
    this._documentsDB = new Promise(function (resolve, reject) {
 | 
			
		||||
      let request = indexedDB.open('_yjs_documents')
 | 
			
		||||
      request.onupgradeneeded = function (event) {
 | 
			
		||||
        const db = event.target.result
 | 
			
		||||
        if (db.objectStoreNames.contains('documents')) {
 | 
			
		||||
          db.deleteObjectStore('documents')
 | 
			
		||||
        }
 | 
			
		||||
        db.createObjectStore('documents', { keyPath: "roomName" })
 | 
			
		||||
      }
 | 
			
		||||
      request.onerror = function (event) {
 | 
			
		||||
        reject(new Error(event.target.error))
 | 
			
		||||
      }
 | 
			
		||||
      request.onblocked = function () {
 | 
			
		||||
        location.reload()
 | 
			
		||||
      }
 | 
			
		||||
      request.onsuccess = function (event) {
 | 
			
		||||
        const db = event.target.result
 | 
			
		||||
        db.onversionchange = function () { db.close() }
 | 
			
		||||
        resolve(db)
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    addEventListener('unload', () => {
 | 
			
		||||
      // close everything when page unloads
 | 
			
		||||
      this._rooms.forEach(room => {
 | 
			
		||||
        if (room.db !== null) {
 | 
			
		||||
          room.db.close()
 | 
			
		||||
        } else {
 | 
			
		||||
          room.dbPromise.then(db => db.close())
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      this._documentsDB.then(db => db.close())
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
  getAllDocuments () {
 | 
			
		||||
    return this._documentsDB.then(
 | 
			
		||||
      db => rtop(db.transaction(['documents'], 'readonly').objectStore('documents').getAll())
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
  setRemoteUpdateCounter (roomName, remoteUpdateCounter) {
 | 
			
		||||
    this._documentsDB.then(
 | 
			
		||||
      db => rtop(db.transaction(['documents'], 'readwrite').objectStore('documents').put({ roomName, remoteUpdateCounter }))
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _createYInstance (roomName) {
 | 
			
		||||
    const room = this._rooms.get(roomName)
 | 
			
		||||
    if (room !== undefined) {
 | 
			
		||||
      return room.y
 | 
			
		||||
    }
 | 
			
		||||
    const y = new Y()
 | 
			
		||||
    return openDB(roomName).then(
 | 
			
		||||
      db => rtop(db.transaction(['updates'], 'readonly').objectStore('updates').getAll())
 | 
			
		||||
    ).then(
 | 
			
		||||
      updates =>
 | 
			
		||||
        y.transact(() => {
 | 
			
		||||
          updates.forEach(update => {
 | 
			
		||||
            decodePersisted(y, new BinaryDecoder(update))
 | 
			
		||||
          })
 | 
			
		||||
        }, true)
 | 
			
		||||
    ).then(() => Promise.resolve(y))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _persistStructsDS (roomName, structsDS) {
 | 
			
		||||
    const encoder = new BinaryEncoder()
 | 
			
		||||
    encoder.writeVarUint(PERSIST_STRUCTS_DS)
 | 
			
		||||
    encoder.writeArrayBuffer(structsDS)
 | 
			
		||||
    return openDB(roomName).then(db => {
 | 
			
		||||
      const t = db.transaction(['updates'], 'readwrite')
 | 
			
		||||
      const updatesStore = t.objectStore('updates')
 | 
			
		||||
      return rtop(updatesStore.put(encoder.createBuffer()))
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _persistStructs (roomName, structs) {
 | 
			
		||||
    const encoder = new BinaryEncoder()
 | 
			
		||||
    encoder.writeVarUint(PERSIST_UPDATE)
 | 
			
		||||
    encoder.writeArrayBuffer(structs)
 | 
			
		||||
    return openDB(roomName).then(db => {
 | 
			
		||||
      const t = db.transaction(['updates'], 'readwrite')
 | 
			
		||||
      const updatesStore = t.objectStore('updates')
 | 
			
		||||
      return rtop(updatesStore.put(encoder.createBuffer()))
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  connectY (roomName, y) {
 | 
			
		||||
    if (this._rooms.has(roomName)) {
 | 
			
		||||
      throw new Error('A Y instance is already bound to this room!')
 | 
			
		||||
    }
 | 
			
		||||
    let room = {
 | 
			
		||||
      db: null,
 | 
			
		||||
      dbPromise: null,
 | 
			
		||||
      channel: null,
 | 
			
		||||
      mutex: createMutualExclude(),
 | 
			
		||||
      y
 | 
			
		||||
    }
 | 
			
		||||
    if (typeof BroadcastChannel !== 'undefined') {
 | 
			
		||||
      room.channel = new BroadcastChannel('__yjs__' + roomName)
 | 
			
		||||
      room.channel.addEventListener('message', e => {
 | 
			
		||||
        room.mutex(function () {
 | 
			
		||||
          decodePersisted(y, new BinaryDecoder(e.data))
 | 
			
		||||
        })
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
    y.on('destroyed', () => {
 | 
			
		||||
      this.disconnectY(roomName, y)
 | 
			
		||||
    })
 | 
			
		||||
    y.on('afterTransaction', (y, transaction) => {
 | 
			
		||||
      room.mutex(() => {
 | 
			
		||||
        if (transaction.encodedStructsLen > 0) {
 | 
			
		||||
          const encoder = new BinaryEncoder()
 | 
			
		||||
          const update = new BinaryEncoder()
 | 
			
		||||
          encodeUpdate(y, transaction.encodedStructs, update)
 | 
			
		||||
          const updateBuffer = update.createBuffer()
 | 
			
		||||
          if (room.channel !== null) {
 | 
			
		||||
            room.channel.postMessage(updateBuffer)
 | 
			
		||||
          }
 | 
			
		||||
          if (transaction.encodedStructsLen > 0) {
 | 
			
		||||
            if (room.db !== null) {
 | 
			
		||||
              saveUpdate(room, updateBuffer)
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
    // register document in documentsDB
 | 
			
		||||
    this._documentsDB.then(
 | 
			
		||||
      db => 
 | 
			
		||||
        rtop(db.transaction(['documents'], 'readonly').objectStore('documents').get(roomName))
 | 
			
		||||
          .then(
 | 
			
		||||
            doc => doc === undefined && rtop(db.transaction(['documents'], 'readwrite').objectStore('documents').add({ roomName, serverUpdateCounter: -1 }))
 | 
			
		||||
          )
 | 
			
		||||
    )
 | 
			
		||||
    // open room db and read existing data
 | 
			
		||||
    return room.dbPromise = openDB(roomName)
 | 
			
		||||
      .then(db => {
 | 
			
		||||
        room.db = db
 | 
			
		||||
        const t = room.db.transaction(['updates'], 'readwrite')
 | 
			
		||||
        const updatesStore = t.objectStore('updates')
 | 
			
		||||
        // write current state as update
 | 
			
		||||
        const encoder = new BinaryEncoder()
 | 
			
		||||
        encodeStructsDS(y, encoder)
 | 
			
		||||
        return rtop(updatesStore.put(encoder.createBuffer())).then(() => {
 | 
			
		||||
          // read persisted state
 | 
			
		||||
          return rtop(updatesStore.getAll()).then(updates => {
 | 
			
		||||
            room.mutex(() => {
 | 
			
		||||
              y.transact(() => {
 | 
			
		||||
                updates.forEach(update => {
 | 
			
		||||
                  decodePersisted(y, new BinaryDecoder(update))
 | 
			
		||||
                })
 | 
			
		||||
              }, true)
 | 
			
		||||
            })
 | 
			
		||||
          })
 | 
			
		||||
        })
 | 
			
		||||
      })
 | 
			
		||||
  }
 | 
			
		||||
  disconnectY (roomName) {
 | 
			
		||||
    const {
 | 
			
		||||
      db, channel
 | 
			
		||||
    } = this._rooms.get(roomName)
 | 
			
		||||
    db.close()
 | 
			
		||||
    if (channel !== null) {
 | 
			
		||||
      channel.close()
 | 
			
		||||
    }
 | 
			
		||||
    this._rooms.delete(roomName)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Remove all persisted data that belongs to a room.
 | 
			
		||||
   * Automatically destroys all Yjs all Yjs instances that persist to
 | 
			
		||||
   * the room. If `destroyYjsInstances = false` the persistence functionality
 | 
			
		||||
   * will be removed from the Yjs instances.
 | 
			
		||||
   */
 | 
			
		||||
  removePersistedData (roomName, destroyYjsInstances = true) {
 | 
			
		||||
    this.disconnectY(roomName)
 | 
			
		||||
    return rtop(indexedDB.deleteDatabase(roomName))
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										51
									
								
								src/Persistences/decodePersisted.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								src/Persistences/decodePersisted.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,51 @@
 | 
			
		||||
import { integrateRemoteStructs } from '../MessageHandler/integrateRemoteStructs.mjs'
 | 
			
		||||
import { writeStructs } from '../MessageHandler/syncStep1.mjs'
 | 
			
		||||
import { writeDeleteSet, readDeleteSet } from '../MessageHandler/deleteSet.mjs'
 | 
			
		||||
 | 
			
		||||
export const PERSIST_UPDATE = 0
 | 
			
		||||
/**
 | 
			
		||||
 * Write an update to an encoder.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {Yjs} y A Yjs instance
 | 
			
		||||
 * @param {BinaryEncoder} updateEncoder I.e. transaction.encodedStructs
 | 
			
		||||
 */
 | 
			
		||||
export function encodeUpdate (y, updateEncoder, encoder) {
 | 
			
		||||
  encoder.writeVarUint(PERSIST_UPDATE)
 | 
			
		||||
  encoder.writeBinaryEncoder(updateEncoder)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const PERSIST_STRUCTS_DS = 1
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Write the current Yjs data model to an encoder.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {Yjs} y A Yjs instance
 | 
			
		||||
 * @param {BinaryEncoder} encoder An encoder to write to
 | 
			
		||||
 */
 | 
			
		||||
export function encodeStructsDS (y, encoder) {
 | 
			
		||||
  encoder.writeVarUint(PERSIST_STRUCTS_DS)
 | 
			
		||||
  writeStructs(y, encoder, new Map())
 | 
			
		||||
  writeDeleteSet(y, encoder)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Feed the Yjs instance with the persisted state
 | 
			
		||||
 * @param {Yjs} y A Yjs instance.
 | 
			
		||||
 * @param {BinaryDecoder} decoder A Decoder instance that holds the file content.
 | 
			
		||||
 */
 | 
			
		||||
export function decodePersisted (y, decoder) {
 | 
			
		||||
  y.transact(() => {
 | 
			
		||||
    while (decoder.hasContent()) {
 | 
			
		||||
      const contentType = decoder.readVarUint()
 | 
			
		||||
      switch (contentType) {
 | 
			
		||||
        case PERSIST_UPDATE:
 | 
			
		||||
          integrateRemoteStructs(y, decoder)
 | 
			
		||||
          break
 | 
			
		||||
        case PERSIST_STRUCTS_DS:
 | 
			
		||||
          integrateRemoteStructs(y, decoder)
 | 
			
		||||
          readDeleteSet(y, decoder)
 | 
			
		||||
          break
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }, true)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										90
									
								
								src/Store/DeleteStore.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								src/Store/DeleteStore.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,90 @@
 | 
			
		||||
 | 
			
		||||
import Tree from '../Util/Tree.mjs'
 | 
			
		||||
import ID from '../Util/ID/ID.mjs'
 | 
			
		||||
 | 
			
		||||
class DSNode {
 | 
			
		||||
  constructor (id, len, gc) {
 | 
			
		||||
    this._id = id
 | 
			
		||||
    this.len = len
 | 
			
		||||
    this.gc = gc
 | 
			
		||||
  }
 | 
			
		||||
  clone () {
 | 
			
		||||
    return new DSNode(this._id, this.len, this.gc)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export default class DeleteStore extends Tree {
 | 
			
		||||
  logTable () {
 | 
			
		||||
    const deletes = []
 | 
			
		||||
    this.iterate(null, null, function (n) {
 | 
			
		||||
      deletes.push({
 | 
			
		||||
        user: n._id.user,
 | 
			
		||||
        clock: n._id.clock,
 | 
			
		||||
        len: n.len,
 | 
			
		||||
        gc: n.gc
 | 
			
		||||
      })
 | 
			
		||||
    })
 | 
			
		||||
    console.table(deletes)
 | 
			
		||||
  }
 | 
			
		||||
  isDeleted (id) {
 | 
			
		||||
    var n = this.findWithUpperBound(id)
 | 
			
		||||
    return n !== null && n._id.user === id.user && id.clock < n._id.clock + n.len
 | 
			
		||||
  }
 | 
			
		||||
  mark (id, length, gc) {
 | 
			
		||||
    if (length === 0) return
 | 
			
		||||
    // Step 1. Unmark range
 | 
			
		||||
    const leftD = this.findWithUpperBound(new ID(id.user, id.clock - 1))
 | 
			
		||||
    // Resize left DSNode if necessary
 | 
			
		||||
    if (leftD !== null && leftD._id.user === id.user) {
 | 
			
		||||
      if (leftD._id.clock < id.clock && id.clock < leftD._id.clock + leftD.len) {
 | 
			
		||||
        // node is overlapping. need to resize
 | 
			
		||||
        if (id.clock + length < leftD._id.clock + leftD.len) {
 | 
			
		||||
          // overlaps new mark range and some more
 | 
			
		||||
          // create another DSNode to the right of new mark
 | 
			
		||||
          this.put(new DSNode(new ID(id.user, id.clock + length), leftD._id.clock + leftD.len - id.clock - length, leftD.gc))
 | 
			
		||||
        }
 | 
			
		||||
        // resize left DSNode
 | 
			
		||||
        leftD.len = id.clock - leftD._id.clock
 | 
			
		||||
      } // Otherwise there is no overlapping
 | 
			
		||||
    }
 | 
			
		||||
    // Resize right DSNode if necessary
 | 
			
		||||
    const upper = new ID(id.user, id.clock + length - 1)
 | 
			
		||||
    const rightD = this.findWithUpperBound(upper)
 | 
			
		||||
    if (rightD !== null && rightD._id.user === id.user) {
 | 
			
		||||
      if (rightD._id.clock < id.clock + length && id.clock <= rightD._id.clock && id.clock + length < rightD._id.clock + rightD.len) { // we only consider the case where we resize the node
 | 
			
		||||
        const d = id.clock + length - rightD._id.clock
 | 
			
		||||
        rightD._id = new ID(rightD._id.user, rightD._id.clock + d)
 | 
			
		||||
        rightD.len -= d
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    // Now we only have to delete all inner marks
 | 
			
		||||
    const deleteNodeIds = []
 | 
			
		||||
    this.iterate(id, upper, m => {
 | 
			
		||||
      deleteNodeIds.push(m._id)
 | 
			
		||||
    })
 | 
			
		||||
    for (let i = deleteNodeIds.length - 1; i >= 0; i--) {
 | 
			
		||||
      this.delete(deleteNodeIds[i])
 | 
			
		||||
    }
 | 
			
		||||
    let newMark = new DSNode(id, length, gc)
 | 
			
		||||
    // Step 2. Check if we can extend left or right
 | 
			
		||||
    if (leftD !== null && leftD._id.user === id.user && leftD._id.clock + leftD.len === id.clock && leftD.gc === gc) {
 | 
			
		||||
      // We can extend left
 | 
			
		||||
      leftD.len += length
 | 
			
		||||
      newMark = leftD
 | 
			
		||||
    }
 | 
			
		||||
    const rightNext = this.find(new ID(id.user, id.clock + length))
 | 
			
		||||
    if (rightNext !== null && rightNext._id.user === id.user && id.clock + length === rightNext._id.clock && gc === rightNext.gc) {
 | 
			
		||||
      // We can merge newMark and rightNext
 | 
			
		||||
      newMark.len += rightNext.len
 | 
			
		||||
      this.delete(rightNext._id)
 | 
			
		||||
    }
 | 
			
		||||
    if (leftD !== newMark) {
 | 
			
		||||
      // only put if we didn't extend left
 | 
			
		||||
      this.put(newMark)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  // TODO: exchange markDeleted for mark()
 | 
			
		||||
  markDeleted (id, length) {
 | 
			
		||||
    this.mark(id, length, false)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										94
									
								
								src/Store/OperationStore.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								src/Store/OperationStore.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,94 @@
 | 
			
		||||
import Tree from '../Util/Tree.mjs'
 | 
			
		||||
import RootID from '../Util/ID/RootID.mjs'
 | 
			
		||||
import { getStruct } from '../Util/structReferences.mjs'
 | 
			
		||||
import { logID } from '../MessageHandler/messageToString.mjs'
 | 
			
		||||
import GC from '../Struct/GC.mjs'
 | 
			
		||||
 | 
			
		||||
export default class OperationStore extends Tree {
 | 
			
		||||
  constructor (y) {
 | 
			
		||||
    super()
 | 
			
		||||
    this.y = y
 | 
			
		||||
  }
 | 
			
		||||
  logTable () {
 | 
			
		||||
    const items = []
 | 
			
		||||
    this.iterate(null, null, function (item) {
 | 
			
		||||
      if (item.constructor === GC) {
 | 
			
		||||
        items.push({
 | 
			
		||||
          id: logID(item),
 | 
			
		||||
          content: item._length,
 | 
			
		||||
          deleted: 'GC'
 | 
			
		||||
        })
 | 
			
		||||
      } else {
 | 
			
		||||
        items.push({
 | 
			
		||||
          id: logID(item),
 | 
			
		||||
          origin: logID(item._origin === null ? null : item._origin._lastId),
 | 
			
		||||
          left: logID(item._left === null ? null : item._left._lastId),
 | 
			
		||||
          right: logID(item._right),
 | 
			
		||||
          right_origin: logID(item._right_origin),
 | 
			
		||||
          parent: logID(item._parent),
 | 
			
		||||
          parentSub: item._parentSub,
 | 
			
		||||
          deleted: item._deleted,
 | 
			
		||||
          content: JSON.stringify(item._content)
 | 
			
		||||
        })
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    console.table(items)
 | 
			
		||||
  }
 | 
			
		||||
  get (id) {
 | 
			
		||||
    let struct = this.find(id)
 | 
			
		||||
    if (struct === null && id instanceof RootID) {
 | 
			
		||||
      const Constr = getStruct(id.type)
 | 
			
		||||
      const y = this.y
 | 
			
		||||
      struct = new Constr()
 | 
			
		||||
      struct._id = id
 | 
			
		||||
      struct._parent = y
 | 
			
		||||
      y.transact(() => {
 | 
			
		||||
        struct._integrate(y)
 | 
			
		||||
      })
 | 
			
		||||
      this.put(struct)
 | 
			
		||||
    }
 | 
			
		||||
    return struct
 | 
			
		||||
  }
 | 
			
		||||
  // Use getItem for structs with _length > 1
 | 
			
		||||
  getItem (id) {
 | 
			
		||||
    var item = this.findWithUpperBound(id)
 | 
			
		||||
    if (item === null) {
 | 
			
		||||
      return null
 | 
			
		||||
    }
 | 
			
		||||
    const itemID = item._id
 | 
			
		||||
    if (id.user === itemID.user && id.clock < itemID.clock + item._length) {
 | 
			
		||||
      return item
 | 
			
		||||
    } else {
 | 
			
		||||
      return null
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  // Return an insertion such that id is the first element of content
 | 
			
		||||
  // This function manipulates an item, if necessary
 | 
			
		||||
  getItemCleanStart (id) {
 | 
			
		||||
    var ins = this.getItem(id)
 | 
			
		||||
    if (ins === null || ins._length === 1) {
 | 
			
		||||
      return ins
 | 
			
		||||
    }
 | 
			
		||||
    const insID = ins._id
 | 
			
		||||
    if (insID.clock === id.clock) {
 | 
			
		||||
      return ins
 | 
			
		||||
    } else {
 | 
			
		||||
      return ins._splitAt(this.y, id.clock - insID.clock)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  // Return an insertion such that id is the last element of content
 | 
			
		||||
  // This function manipulates an operation, if necessary
 | 
			
		||||
  getItemCleanEnd (id) {
 | 
			
		||||
    var ins = this.getItem(id)
 | 
			
		||||
    if (ins === null || ins._length === 1) {
 | 
			
		||||
      return ins
 | 
			
		||||
    }
 | 
			
		||||
    const insID = ins._id
 | 
			
		||||
    if (insID.clock + ins._length - 1 === id.clock) {
 | 
			
		||||
      return ins
 | 
			
		||||
    } else {
 | 
			
		||||
      ins._splitAt(this.y, id.clock - insID.clock + 1)
 | 
			
		||||
      return ins
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										47
									
								
								src/Store/StateStore.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/Store/StateStore.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,47 @@
 | 
			
		||||
import ID from '../Util/ID/ID.mjs'
 | 
			
		||||
 | 
			
		||||
export default class StateStore {
 | 
			
		||||
  constructor (y) {
 | 
			
		||||
    this.y = y
 | 
			
		||||
    this.state = new Map()
 | 
			
		||||
  }
 | 
			
		||||
  logTable () {
 | 
			
		||||
    const entries = []
 | 
			
		||||
    for (let [user, state] of this.state) {
 | 
			
		||||
      entries.push({
 | 
			
		||||
        user, state
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
    console.table(entries)
 | 
			
		||||
  }
 | 
			
		||||
  getNextID (len) {
 | 
			
		||||
    const user = this.y.userID
 | 
			
		||||
    const state = this.getState(user)
 | 
			
		||||
    this.setState(user, state + len)
 | 
			
		||||
    return new ID(user, state)
 | 
			
		||||
  }
 | 
			
		||||
  updateRemoteState (struct) {
 | 
			
		||||
    let user = struct._id.user
 | 
			
		||||
    let userState = this.state.get(user)
 | 
			
		||||
    while (struct !== null && struct._id.clock === userState) {
 | 
			
		||||
      userState += struct._length
 | 
			
		||||
      struct = this.y.os.get(new ID(user, userState))
 | 
			
		||||
    }
 | 
			
		||||
    this.state.set(user, userState)
 | 
			
		||||
  }
 | 
			
		||||
  getState (user) {
 | 
			
		||||
    let state = this.state.get(user)
 | 
			
		||||
    if (state == null) {
 | 
			
		||||
      return 0
 | 
			
		||||
    }
 | 
			
		||||
    return state
 | 
			
		||||
  }
 | 
			
		||||
  setState (user, state) {
 | 
			
		||||
    // TODO: modify missingi structs here
 | 
			
		||||
    const beforeState = this.y._transaction.beforeState
 | 
			
		||||
    if (!beforeState.has(user)) {
 | 
			
		||||
      beforeState.set(user, this.getState(user))
 | 
			
		||||
    }
 | 
			
		||||
    this.state.set(user, state)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										124
									
								
								src/Struct/Delete.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								src/Struct/Delete.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,124 @@
 | 
			
		||||
import { getStructReference } from '../Util/structReferences.mjs'
 | 
			
		||||
import ID from '../Util/ID/ID.mjs'
 | 
			
		||||
import { logID } from '../MessageHandler/messageToString.mjs'
 | 
			
		||||
import { writeStructToTransaction } from '../Transaction.mjs'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @private
 | 
			
		||||
 * Delete all items in an ID-range
 | 
			
		||||
 * TODO: implement getItemCleanStartNode for better performance (only one lookup)
 | 
			
		||||
 */
 | 
			
		||||
export function deleteItemRange (y, user, clock, range, gcChildren) {
 | 
			
		||||
  const createDelete = y.connector !== null && y.connector._forwardAppliedStructs
 | 
			
		||||
  let item = y.os.getItemCleanStart(new ID(user, clock))
 | 
			
		||||
  if (item !== null) {
 | 
			
		||||
    if (!item._deleted) {
 | 
			
		||||
      item._splitAt(y, range)
 | 
			
		||||
      item._delete(y, createDelete, true)
 | 
			
		||||
    }
 | 
			
		||||
    let itemLen = item._length
 | 
			
		||||
    range -= itemLen
 | 
			
		||||
    clock += itemLen
 | 
			
		||||
    if (range > 0) {
 | 
			
		||||
      let node = y.os.findNode(new ID(user, clock))
 | 
			
		||||
      while (node !== null && node.val !== null && range > 0 && node.val._id.equals(new ID(user, clock))) {
 | 
			
		||||
        const nodeVal = node.val
 | 
			
		||||
        if (!nodeVal._deleted) {
 | 
			
		||||
          nodeVal._splitAt(y, range)
 | 
			
		||||
          nodeVal._delete(y, createDelete, gcChildren)
 | 
			
		||||
        }
 | 
			
		||||
        const nodeLen = nodeVal._length
 | 
			
		||||
        range -= nodeLen
 | 
			
		||||
        clock += nodeLen
 | 
			
		||||
        node = node.next()
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @private
 | 
			
		||||
 * A Delete change is not a real Item, but it provides the same interface as an
 | 
			
		||||
 * Item. The only difference is that it will not be saved in the ItemStore
 | 
			
		||||
 * (OperationStore), but instead it is safed in the DeleteStore.
 | 
			
		||||
 */
 | 
			
		||||
export default class Delete {
 | 
			
		||||
  constructor () {
 | 
			
		||||
    this._target = null
 | 
			
		||||
    this._length = null
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @private
 | 
			
		||||
   * Read the next Item in a Decoder and fill this Item with the read data.
 | 
			
		||||
   *
 | 
			
		||||
   * This is called when data is received from a remote peer.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Y} y The Yjs instance that this Item belongs to.
 | 
			
		||||
   * @param {BinaryDecoder} decoder The decoder object to read data from.
 | 
			
		||||
   */
 | 
			
		||||
  _fromBinary (y, decoder) {
 | 
			
		||||
    // TODO: set target, and add it to missing if not found
 | 
			
		||||
    // There is an edge case in p2p networks!
 | 
			
		||||
    const targetID = decoder.readID()
 | 
			
		||||
    this._targetID = targetID
 | 
			
		||||
    this._length = decoder.readVarUint()
 | 
			
		||||
    if (y.os.getItem(targetID) === null) {
 | 
			
		||||
      return [targetID]
 | 
			
		||||
    } else {
 | 
			
		||||
      return []
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @private
 | 
			
		||||
   * Transform the properties of this type to binary and write it to an
 | 
			
		||||
   * BinaryEncoder.
 | 
			
		||||
   *
 | 
			
		||||
   * This is called when this Item is sent to a remote peer.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {BinaryEncoder} encoder The encoder to write data to.
 | 
			
		||||
   */
 | 
			
		||||
  _toBinary (encoder) {
 | 
			
		||||
    encoder.writeUint8(getStructReference(this.constructor))
 | 
			
		||||
    encoder.writeID(this._targetID)
 | 
			
		||||
    encoder.writeVarUint(this._length)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @private
 | 
			
		||||
   * Integrates this Item into the shared structure.
 | 
			
		||||
   *
 | 
			
		||||
   * This method actually applies the change to the Yjs instance. In the case of
 | 
			
		||||
   * Delete it marks the delete target as deleted.
 | 
			
		||||
   *
 | 
			
		||||
   * * If created remotely (a remote user deleted something),
 | 
			
		||||
   *   this Delete is applied to all structs in id-range.
 | 
			
		||||
   * * If created lokally (e.g. when y-array deletes a range of elements),
 | 
			
		||||
   *   this struct is broadcasted only (it is already executed)
 | 
			
		||||
   */
 | 
			
		||||
  _integrate (y, locallyCreated = false) {
 | 
			
		||||
    if (!locallyCreated) {
 | 
			
		||||
      // from remote
 | 
			
		||||
      const id = this._targetID
 | 
			
		||||
      deleteItemRange(y, id.user, id.clock, this._length, false)
 | 
			
		||||
    } else if (y.connector !== null) {
 | 
			
		||||
      // from local
 | 
			
		||||
      y.connector.broadcastStruct(this)
 | 
			
		||||
    }
 | 
			
		||||
    if (y.persistence !== null) {
 | 
			
		||||
      y.persistence.saveStruct(y, this)
 | 
			
		||||
    }
 | 
			
		||||
    writeStructToTransaction(y._transaction, this)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Transform this YXml Type to a readable format.
 | 
			
		||||
   * Useful for logging as all Items and Delete implement this method.
 | 
			
		||||
   *
 | 
			
		||||
   * @private
 | 
			
		||||
   */
 | 
			
		||||
  _logString () {
 | 
			
		||||
    return `Delete - target: ${logID(this._targetID)}, len: ${this._length}`
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										96
									
								
								src/Struct/GC.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										96
									
								
								src/Struct/GC.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,96 @@
 | 
			
		||||
import { getStructReference } from '../Util/structReferences.mjs'
 | 
			
		||||
import { RootFakeUserID } from '../Util/ID/RootID.mjs'
 | 
			
		||||
import ID from '../Util/ID/ID.mjs'
 | 
			
		||||
import { writeStructToTransaction } from '../Transaction.mjs'
 | 
			
		||||
 | 
			
		||||
// TODO should have the same base class as Item
 | 
			
		||||
export default class GC {
 | 
			
		||||
  constructor () {
 | 
			
		||||
    this._id = null
 | 
			
		||||
    this._length = 0
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get _deleted () {
 | 
			
		||||
    return true
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _integrate (y) {
 | 
			
		||||
    const id = this._id
 | 
			
		||||
    const userState = y.ss.getState(id.user)
 | 
			
		||||
    if (id.clock === userState) {
 | 
			
		||||
      y.ss.setState(id.user, id.clock + this._length)
 | 
			
		||||
    }
 | 
			
		||||
    y.ds.mark(this._id, this._length, true)
 | 
			
		||||
    let n = y.os.put(this)
 | 
			
		||||
    const prev = n.prev().val
 | 
			
		||||
    if (prev !== null && prev.constructor === GC && prev._id.user === n.val._id.user && prev._id.clock + prev._length === n.val._id.clock) {
 | 
			
		||||
      // TODO: do merging for all items!
 | 
			
		||||
      prev._length += n.val._length
 | 
			
		||||
      y.os.delete(n.val._id)
 | 
			
		||||
      n = prev
 | 
			
		||||
    }
 | 
			
		||||
    if (n.val) {
 | 
			
		||||
      n = n.val
 | 
			
		||||
    }
 | 
			
		||||
    const next = y.os.findNext(n._id)
 | 
			
		||||
    if (next !== null && next.constructor === GC && next._id.user === n._id.user && next._id.clock === n._id.clock + n._length) {
 | 
			
		||||
      n._length += next._length
 | 
			
		||||
      y.os.delete(next._id)
 | 
			
		||||
    }
 | 
			
		||||
    if (id.user !== RootFakeUserID) {
 | 
			
		||||
      if (y.connector !== null && (y.connector._forwardAppliedStructs || id.user === y.userID)) {
 | 
			
		||||
        y.connector.broadcastStruct(this)
 | 
			
		||||
      }
 | 
			
		||||
      if (y.persistence !== null) {
 | 
			
		||||
        y.persistence.saveStruct(y, this)
 | 
			
		||||
      }
 | 
			
		||||
      writeStructToTransaction(y._transaction, this)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Transform the properties of this type to binary and write it to an
 | 
			
		||||
   * BinaryEncoder.
 | 
			
		||||
   *
 | 
			
		||||
   * This is called when this Item is sent to a remote peer.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {BinaryEncoder} encoder The encoder to write data to.
 | 
			
		||||
   * @private
 | 
			
		||||
   */
 | 
			
		||||
  _toBinary (encoder) {
 | 
			
		||||
    encoder.writeUint8(getStructReference(this.constructor))
 | 
			
		||||
    encoder.writeID(this._id)
 | 
			
		||||
    encoder.writeVarUint(this._length)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Read the next Item in a Decoder and fill this Item with the read data.
 | 
			
		||||
   *
 | 
			
		||||
   * This is called when data is received from a remote peer.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Y} y The Yjs instance that this Item belongs to.
 | 
			
		||||
   * @param {BinaryDecoder} decoder The decoder object to read data from.
 | 
			
		||||
   * @private
 | 
			
		||||
   */
 | 
			
		||||
  _fromBinary (y, decoder) {
 | 
			
		||||
    const id = decoder.readID()
 | 
			
		||||
    this._id = id
 | 
			
		||||
    this._length = decoder.readVarUint()
 | 
			
		||||
    const missing = []
 | 
			
		||||
    if (y.ss.getState(id.user) < id.clock) {
 | 
			
		||||
      missing.push(new ID(id.user, id.clock - 1))
 | 
			
		||||
    }
 | 
			
		||||
    return missing
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _splitAt () {
 | 
			
		||||
    return this
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _clonePartial (diff) {
 | 
			
		||||
    const gc = new GC()
 | 
			
		||||
    gc._id = new ID(this._id.user, this._id.clock + diff)
 | 
			
		||||
    gc._length = this._length - diff
 | 
			
		||||
    return gc
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										528
									
								
								src/Struct/Item.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										528
									
								
								src/Struct/Item.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,528 @@
 | 
			
		||||
import { getStructReference } from '../Util/structReferences.mjs'
 | 
			
		||||
import ID from '../Util/ID/ID.mjs'
 | 
			
		||||
import { default as RootID, RootFakeUserID } from '../Util/ID/RootID.mjs'
 | 
			
		||||
import Delete from './Delete.mjs'
 | 
			
		||||
import { transactionTypeChanged, writeStructToTransaction } from '../Transaction.mjs'
 | 
			
		||||
import GC from './GC.mjs'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @private
 | 
			
		||||
 * Helper utility to split an Item (see {@link Item#_splitAt})
 | 
			
		||||
 * - copies all properties from a to b
 | 
			
		||||
 * - connects a to b
 | 
			
		||||
 * - assigns the correct _id
 | 
			
		||||
 * - saves b to os
 | 
			
		||||
 */
 | 
			
		||||
export function splitHelper (y, a, b, diff) {
 | 
			
		||||
  const aID = a._id
 | 
			
		||||
  b._id = new ID(aID.user, aID.clock + diff)
 | 
			
		||||
  b._origin = a
 | 
			
		||||
  b._left = a
 | 
			
		||||
  b._right = a._right
 | 
			
		||||
  if (b._right !== null) {
 | 
			
		||||
    b._right._left = b
 | 
			
		||||
  }
 | 
			
		||||
  b._right_origin = a._right_origin
 | 
			
		||||
  // do not set a._right_origin, as this will lead to problems when syncing
 | 
			
		||||
  a._right = b
 | 
			
		||||
  b._parent = a._parent
 | 
			
		||||
  b._parentSub = a._parentSub
 | 
			
		||||
  b._deleted = a._deleted
 | 
			
		||||
  // now search all relevant items to the right and update origin
 | 
			
		||||
  // if origin is not it foundOrigins, we don't have to search any longer
 | 
			
		||||
  let foundOrigins = new Set()
 | 
			
		||||
  foundOrigins.add(a)
 | 
			
		||||
  let o = b._right
 | 
			
		||||
  while (o !== null && foundOrigins.has(o._origin)) {
 | 
			
		||||
    if (o._origin === a) {
 | 
			
		||||
      o._origin = b
 | 
			
		||||
    }
 | 
			
		||||
    foundOrigins.add(o)
 | 
			
		||||
    o = o._right
 | 
			
		||||
  }
 | 
			
		||||
  y.os.put(b)
 | 
			
		||||
  if (y._transaction.newTypes.has(a)) {
 | 
			
		||||
    y._transaction.newTypes.add(b)
 | 
			
		||||
  } else if (y._transaction.deletedStructs.has(a)) {
 | 
			
		||||
    y._transaction.deletedStructs.add(b)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Abstract class that represents any content.
 | 
			
		||||
 */
 | 
			
		||||
export default class Item {
 | 
			
		||||
  constructor () {
 | 
			
		||||
    /**
 | 
			
		||||
     * The uniqe identifier of this type.
 | 
			
		||||
     * @type {ID}
 | 
			
		||||
     */
 | 
			
		||||
    this._id = null
 | 
			
		||||
    /**
 | 
			
		||||
     * The item that was originally to the left of this item.
 | 
			
		||||
     * @type {Item}
 | 
			
		||||
     */
 | 
			
		||||
    this._origin = null
 | 
			
		||||
    /**
 | 
			
		||||
     * The item that is currently to the left of this item.
 | 
			
		||||
     * @type {Item}
 | 
			
		||||
     */
 | 
			
		||||
    this._left = null
 | 
			
		||||
    /**
 | 
			
		||||
     * The item that is currently to the right of this item.
 | 
			
		||||
     * @type {Item}
 | 
			
		||||
     */
 | 
			
		||||
    this._right = null
 | 
			
		||||
    /**
 | 
			
		||||
     * The item that was originally to the right of this item.
 | 
			
		||||
     * @type {Item}
 | 
			
		||||
     */
 | 
			
		||||
    this._right_origin = null
 | 
			
		||||
    /**
 | 
			
		||||
     * The parent type.
 | 
			
		||||
     * @type {Y|YType}
 | 
			
		||||
     */
 | 
			
		||||
    this._parent = null
 | 
			
		||||
    /**
 | 
			
		||||
     * If the parent refers to this item with some kind of key (e.g. YMap, the
 | 
			
		||||
     * key is specified here. The key is then used to refer to the list in which
 | 
			
		||||
     * to insert this item. If `parentSub = null` type._start is the list in
 | 
			
		||||
     * which to insert to. Otherwise it is `parent._start`.
 | 
			
		||||
     * @type {String}
 | 
			
		||||
     */
 | 
			
		||||
    this._parentSub = null
 | 
			
		||||
    /**
 | 
			
		||||
     * Whether this item was deleted or not.
 | 
			
		||||
     * @type {Boolean}
 | 
			
		||||
     */
 | 
			
		||||
    this._deleted = false
 | 
			
		||||
    /**
 | 
			
		||||
     * If this type's effect is reundone this type refers to the type that undid
 | 
			
		||||
     * this operation.
 | 
			
		||||
     * @type {Item}
 | 
			
		||||
     */
 | 
			
		||||
    this._redone = null
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Creates an Item with the same effect as this Item (without position effect)
 | 
			
		||||
   *
 | 
			
		||||
   * @private
 | 
			
		||||
   */
 | 
			
		||||
  _copy () {
 | 
			
		||||
    return new this.constructor()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Redoes the effect of this operation.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Y} y The Yjs instance.
 | 
			
		||||
   *
 | 
			
		||||
   * @private
 | 
			
		||||
   */
 | 
			
		||||
  _redo (y) {
 | 
			
		||||
    if (this._redone !== null) {
 | 
			
		||||
      return this._redone
 | 
			
		||||
    }
 | 
			
		||||
    let struct = this._copy()
 | 
			
		||||
    let left = this._left
 | 
			
		||||
    let right = this
 | 
			
		||||
    let parent = this._parent
 | 
			
		||||
    // make sure that parent is redone
 | 
			
		||||
    if (parent._deleted === true && parent._redone === null) {
 | 
			
		||||
      parent._redo(y)
 | 
			
		||||
    }
 | 
			
		||||
    if (parent._redone !== null) {
 | 
			
		||||
      parent = parent._redone
 | 
			
		||||
      // find next cloned items
 | 
			
		||||
      while (left !== null) {
 | 
			
		||||
        if (left._redone !== null && left._redone._parent === parent) {
 | 
			
		||||
          left = left._redone
 | 
			
		||||
          break
 | 
			
		||||
        }
 | 
			
		||||
        left = left._left
 | 
			
		||||
      }
 | 
			
		||||
      while (right !== null) {
 | 
			
		||||
        if (right._redone !== null && right._redone._parent === parent) {
 | 
			
		||||
          right = right._redone
 | 
			
		||||
        }
 | 
			
		||||
        right = right._right
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    struct._origin = left
 | 
			
		||||
    struct._left = left
 | 
			
		||||
    struct._right = right
 | 
			
		||||
    struct._right_origin = right
 | 
			
		||||
    struct._parent = parent
 | 
			
		||||
    struct._parentSub = this._parentSub
 | 
			
		||||
    struct._integrate(y)
 | 
			
		||||
    this._redone = struct
 | 
			
		||||
    return struct
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Computes the last content address of this Item.
 | 
			
		||||
   *
 | 
			
		||||
   * @private
 | 
			
		||||
   */
 | 
			
		||||
  get _lastId () {
 | 
			
		||||
    return new ID(this._id.user, this._id.clock + this._length - 1)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Computes the length of this Item.
 | 
			
		||||
   *
 | 
			
		||||
   * @private
 | 
			
		||||
   */
 | 
			
		||||
  get _length () {
 | 
			
		||||
    return 1
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Should return false if this Item is some kind of meta information
 | 
			
		||||
   * (e.g. format information).
 | 
			
		||||
   *
 | 
			
		||||
   * * Whether this Item should be addressable via `yarray.get(i)`
 | 
			
		||||
   * * Whether this Item should be counted when computing yarray.length
 | 
			
		||||
   *
 | 
			
		||||
   * @private
 | 
			
		||||
   */
 | 
			
		||||
  get _countable () {
 | 
			
		||||
    return true
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Splits this Item so that another Items can be inserted in-between.
 | 
			
		||||
   * This must be overwritten if _length > 1
 | 
			
		||||
   * Returns right part after split
 | 
			
		||||
   * * diff === 0 => this
 | 
			
		||||
   * * diff === length => this._right
 | 
			
		||||
   * * otherwise => split _content and return right part of split
 | 
			
		||||
   * (see {@link ItemJSON}/{@link ItemString} for implementation)
 | 
			
		||||
   *
 | 
			
		||||
   * @private
 | 
			
		||||
   */
 | 
			
		||||
  _splitAt (y, diff) {
 | 
			
		||||
    if (diff === 0) {
 | 
			
		||||
      return this
 | 
			
		||||
    }
 | 
			
		||||
    return this._right
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Mark this Item as deleted.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Y} y The Yjs instance
 | 
			
		||||
   * @param {boolean} createDelete Whether to propagate a message that this
 | 
			
		||||
   *                               Type was deleted.
 | 
			
		||||
   *
 | 
			
		||||
   * @private
 | 
			
		||||
   */
 | 
			
		||||
  _delete (y, createDelete = true) {
 | 
			
		||||
    if (!this._deleted) {
 | 
			
		||||
      this._deleted = true
 | 
			
		||||
      y.ds.mark(this._id, this._length, false)
 | 
			
		||||
      let del = new Delete()
 | 
			
		||||
      del._targetID = this._id
 | 
			
		||||
      del._length = this._length
 | 
			
		||||
      if (createDelete) {
 | 
			
		||||
        // broadcast and persists Delete
 | 
			
		||||
        del._integrate(y, true)
 | 
			
		||||
      } else if (y.persistence !== null) {
 | 
			
		||||
        // only persist Delete
 | 
			
		||||
        y.persistence.saveStruct(y, del)
 | 
			
		||||
      }
 | 
			
		||||
      transactionTypeChanged(y, this._parent, this._parentSub)
 | 
			
		||||
      y._transaction.deletedStructs.add(this)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _gcChildren (y) {}
 | 
			
		||||
 | 
			
		||||
  _gc (y) {
 | 
			
		||||
    const gc = new GC()
 | 
			
		||||
    gc._id = this._id
 | 
			
		||||
    gc._length = this._length
 | 
			
		||||
    y.os.delete(this._id)
 | 
			
		||||
    gc._integrate(y)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * This is called right before this Item receives any children.
 | 
			
		||||
   * It can be overwritten to apply pending changes before applying remote changes
 | 
			
		||||
   *
 | 
			
		||||
   * @private
 | 
			
		||||
   */
 | 
			
		||||
  _beforeChange () {
 | 
			
		||||
    // nop
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Integrates this Item into the shared structure.
 | 
			
		||||
   *
 | 
			
		||||
   * This method actually applies the change to the Yjs instance. In case of
 | 
			
		||||
   * Item it connects _left and _right to this Item and calls the
 | 
			
		||||
   * {@link Item#beforeChange} method.
 | 
			
		||||
   *
 | 
			
		||||
   * * Integrate the struct so that other types/structs can see it
 | 
			
		||||
   * * Add this struct to y.os
 | 
			
		||||
   * * Check if this is struct deleted
 | 
			
		||||
   *
 | 
			
		||||
   * @private
 | 
			
		||||
   */
 | 
			
		||||
  _integrate (y) {
 | 
			
		||||
    y._transaction.newTypes.add(this)
 | 
			
		||||
    const parent = this._parent
 | 
			
		||||
    const selfID = this._id
 | 
			
		||||
    const user = selfID === null ? y.userID : selfID.user
 | 
			
		||||
    const userState = y.ss.getState(user)
 | 
			
		||||
    if (selfID === null) {
 | 
			
		||||
      this._id = y.ss.getNextID(this._length)
 | 
			
		||||
    } else if (selfID.user === RootFakeUserID) {
 | 
			
		||||
      // nop
 | 
			
		||||
    } else if (selfID.clock < userState) {
 | 
			
		||||
      // already applied..
 | 
			
		||||
      return []
 | 
			
		||||
    } else if (selfID.clock === userState) {
 | 
			
		||||
      y.ss.setState(selfID.user, userState + this._length)
 | 
			
		||||
    } else {
 | 
			
		||||
      // missing content from user
 | 
			
		||||
      throw new Error('Can not apply yet!')
 | 
			
		||||
    }
 | 
			
		||||
    if (!parent._deleted && !y._transaction.changedTypes.has(parent) && !y._transaction.newTypes.has(parent)) {
 | 
			
		||||
      // this is the first time parent is updated
 | 
			
		||||
      // or this types is new
 | 
			
		||||
      this._parent._beforeChange()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /*
 | 
			
		||||
    # $this has to find a unique position between origin and the next known character
 | 
			
		||||
    # case 1: $origin equals $o.origin: the $creator parameter decides if left or right
 | 
			
		||||
    #         let $OL= [o1,o2,o3,o4], whereby $this is to be inserted between o1 and o4
 | 
			
		||||
    #         o2,o3 and o4 origin is 1 (the position of o2)
 | 
			
		||||
    #         there is the case that $this.creator < o2.creator, but o3.creator < $this.creator
 | 
			
		||||
    #         then o2 knows o3. Since on another client $OL could be [o1,o3,o4] the problem is complex
 | 
			
		||||
    #         therefore $this would be always to the right of o3
 | 
			
		||||
    # case 2: $origin < $o.origin
 | 
			
		||||
    #         if current $this insert_position > $o origin: $this ins
 | 
			
		||||
    #         else $insert_position will not change
 | 
			
		||||
    #         (maybe we encounter case 1 later, then this will be to the right of $o)
 | 
			
		||||
    # case 3: $origin > $o.origin
 | 
			
		||||
    #         $this insert_position is to the left of $o (forever!)
 | 
			
		||||
    */
 | 
			
		||||
    // handle conflicts
 | 
			
		||||
    let o
 | 
			
		||||
    // set o to the first conflicting item
 | 
			
		||||
    if (this._left !== null) {
 | 
			
		||||
      o = this._left._right
 | 
			
		||||
    } else if (this._parentSub !== null) {
 | 
			
		||||
      o = this._parent._map.get(this._parentSub) || null
 | 
			
		||||
    } else {
 | 
			
		||||
      o = this._parent._start
 | 
			
		||||
    }
 | 
			
		||||
    let conflictingItems = new Set()
 | 
			
		||||
    let itemsBeforeOrigin = new Set()
 | 
			
		||||
    // Let c in conflictingItems, b in itemsBeforeOrigin
 | 
			
		||||
    // ***{origin}bbbb{this}{c,b}{c,b}{o}***
 | 
			
		||||
    // Note that conflictingItems is a subset of itemsBeforeOrigin
 | 
			
		||||
    while (o !== null && o !== this._right) {
 | 
			
		||||
      itemsBeforeOrigin.add(o)
 | 
			
		||||
      conflictingItems.add(o)
 | 
			
		||||
      if (this._origin === o._origin) {
 | 
			
		||||
        // case 1
 | 
			
		||||
        if (o._id.user < this._id.user) {
 | 
			
		||||
          this._left = o
 | 
			
		||||
          conflictingItems.clear()
 | 
			
		||||
        }
 | 
			
		||||
      } else if (itemsBeforeOrigin.has(o._origin)) {
 | 
			
		||||
        // case 2
 | 
			
		||||
        if (!conflictingItems.has(o._origin)) {
 | 
			
		||||
          this._left = o
 | 
			
		||||
          conflictingItems.clear()
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        break
 | 
			
		||||
      }
 | 
			
		||||
      // TODO: try to use right_origin instead.
 | 
			
		||||
      // Then you could basically omit conflictingItems!
 | 
			
		||||
      // Note: you probably can't use right_origin in every case.. only when setting _left
 | 
			
		||||
      o = o._right
 | 
			
		||||
    }
 | 
			
		||||
    // reconnect left/right + update parent map/start if necessary
 | 
			
		||||
    const parentSub = this._parentSub
 | 
			
		||||
    if (this._left === null) {
 | 
			
		||||
      let right
 | 
			
		||||
      if (parentSub !== null) {
 | 
			
		||||
        const pmap = parent._map
 | 
			
		||||
        right = pmap.get(parentSub) || null
 | 
			
		||||
        pmap.set(parentSub, this)
 | 
			
		||||
      } else {
 | 
			
		||||
        right = parent._start
 | 
			
		||||
        parent._start = this
 | 
			
		||||
      }
 | 
			
		||||
      this._right = right
 | 
			
		||||
      if (right !== null) {
 | 
			
		||||
        right._left = this
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      const left = this._left
 | 
			
		||||
      const right = left._right
 | 
			
		||||
      this._right = right
 | 
			
		||||
      left._right = this
 | 
			
		||||
      if (right !== null) {
 | 
			
		||||
        right._left = this
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (parent._deleted) {
 | 
			
		||||
      this._delete(y, false)
 | 
			
		||||
    }
 | 
			
		||||
    y.os.put(this)
 | 
			
		||||
    transactionTypeChanged(y, parent, parentSub)
 | 
			
		||||
    if (this._id.user !== RootFakeUserID) {
 | 
			
		||||
      if (y.connector !== null && (y.connector._forwardAppliedStructs || this._id.user === y.userID)) {
 | 
			
		||||
        y.connector.broadcastStruct(this)
 | 
			
		||||
      }
 | 
			
		||||
      if (y.persistence !== null) {
 | 
			
		||||
        y.persistence.saveStruct(y, this)
 | 
			
		||||
      }
 | 
			
		||||
      writeStructToTransaction(y._transaction, this)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Transform the properties of this type to binary and write it to an
 | 
			
		||||
   * BinaryEncoder.
 | 
			
		||||
   *
 | 
			
		||||
   * This is called when this Item is sent to a remote peer.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {BinaryEncoder} encoder The encoder to write data to.
 | 
			
		||||
   *
 | 
			
		||||
   * @private
 | 
			
		||||
   */
 | 
			
		||||
  _toBinary (encoder) {
 | 
			
		||||
    encoder.writeUint8(getStructReference(this.constructor))
 | 
			
		||||
    let info = 0
 | 
			
		||||
    if (this._origin !== null) {
 | 
			
		||||
      info += 0b1 // origin is defined
 | 
			
		||||
    }
 | 
			
		||||
    // TODO: remove
 | 
			
		||||
    /* no longer send _left
 | 
			
		||||
    if (this._left !== this._origin) {
 | 
			
		||||
      info += 0b10 // do not copy origin to left
 | 
			
		||||
    }
 | 
			
		||||
    */
 | 
			
		||||
    if (this._right_origin !== null) {
 | 
			
		||||
      info += 0b100
 | 
			
		||||
    }
 | 
			
		||||
    if (this._parentSub !== null) {
 | 
			
		||||
      info += 0b1000
 | 
			
		||||
    }
 | 
			
		||||
    encoder.writeUint8(info)
 | 
			
		||||
    encoder.writeID(this._id)
 | 
			
		||||
    if (info & 0b1) {
 | 
			
		||||
      encoder.writeID(this._origin._lastId)
 | 
			
		||||
    }
 | 
			
		||||
    // TODO: remove
 | 
			
		||||
    /* see above
 | 
			
		||||
    if (info & 0b10) {
 | 
			
		||||
      encoder.writeID(this._left._lastId)
 | 
			
		||||
    }
 | 
			
		||||
    */
 | 
			
		||||
    if (info & 0b100) {
 | 
			
		||||
      encoder.writeID(this._right_origin._id)
 | 
			
		||||
    }
 | 
			
		||||
    if ((info & 0b101) === 0) {
 | 
			
		||||
      // neither origin nor right is defined
 | 
			
		||||
      encoder.writeID(this._parent._id)
 | 
			
		||||
    }
 | 
			
		||||
    if (info & 0b1000) {
 | 
			
		||||
      encoder.writeVarString(JSON.stringify(this._parentSub))
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Read the next Item in a Decoder and fill this Item with the read data.
 | 
			
		||||
   *
 | 
			
		||||
   * This is called when data is received from a remote peer.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Y} y The Yjs instance that this Item belongs to.
 | 
			
		||||
   * @param {BinaryDecoder} decoder The decoder object to read data from.
 | 
			
		||||
   *
 | 
			
		||||
   * @private
 | 
			
		||||
   */
 | 
			
		||||
  _fromBinary (y, decoder) {
 | 
			
		||||
    let missing = []
 | 
			
		||||
    const info = decoder.readUint8()
 | 
			
		||||
    const id = decoder.readID()
 | 
			
		||||
    this._id = id
 | 
			
		||||
    // read origin
 | 
			
		||||
    if (info & 0b1) {
 | 
			
		||||
      // origin != null
 | 
			
		||||
      const originID = decoder.readID()
 | 
			
		||||
      // we have to query for left again because it might have been split/merged..
 | 
			
		||||
      const origin = y.os.getItemCleanEnd(originID)
 | 
			
		||||
      if (origin === null) {
 | 
			
		||||
        missing.push(originID)
 | 
			
		||||
      } else {
 | 
			
		||||
        this._origin = origin
 | 
			
		||||
        this._left = this._origin
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    // read right
 | 
			
		||||
    if (info & 0b100) {
 | 
			
		||||
      // right != null
 | 
			
		||||
      const rightID = decoder.readID()
 | 
			
		||||
      // we have to query for right again because it might have been split/merged..
 | 
			
		||||
      const right = y.os.getItemCleanStart(rightID)
 | 
			
		||||
      if (right === null) {
 | 
			
		||||
        missing.push(rightID)
 | 
			
		||||
      } else {
 | 
			
		||||
        this._right = right
 | 
			
		||||
        this._right_origin = right
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    // read parent
 | 
			
		||||
    if ((info & 0b101) === 0) {
 | 
			
		||||
      // neither origin nor right is defined
 | 
			
		||||
      const parentID = decoder.readID()
 | 
			
		||||
      // parent does not change, so we don't have to search for it again
 | 
			
		||||
      if (this._parent === null) {
 | 
			
		||||
        let parent
 | 
			
		||||
        if (parentID.constructor === RootID) {
 | 
			
		||||
          parent = y.os.get(parentID)
 | 
			
		||||
        } else {
 | 
			
		||||
          parent = y.os.getItem(parentID)
 | 
			
		||||
        }
 | 
			
		||||
        if (parent === null) {
 | 
			
		||||
          missing.push(parentID)
 | 
			
		||||
        } else {
 | 
			
		||||
          this._parent = parent
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    } else if (this._parent === null) {
 | 
			
		||||
      if (this._origin !== null) {
 | 
			
		||||
        if (this._origin.constructor === GC) {
 | 
			
		||||
          // if origin is a gc, set parent also gc'd
 | 
			
		||||
          this._parent = this._origin
 | 
			
		||||
        } else {
 | 
			
		||||
          this._parent = this._origin._parent
 | 
			
		||||
        }
 | 
			
		||||
      } else if (this._right_origin !== null) {
 | 
			
		||||
        // if origin is a gc, set parent also gc'd
 | 
			
		||||
        if (this._right_origin.constructor === GC) {
 | 
			
		||||
          this._parent = this._right_origin
 | 
			
		||||
        } else {
 | 
			
		||||
          this._parent = this._right_origin._parent
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (info & 0b1000) {
 | 
			
		||||
      // TODO: maybe put this in read parent condition (you can also read parentsub from left/right)
 | 
			
		||||
      this._parentSub = JSON.parse(decoder.readVarString())
 | 
			
		||||
    }
 | 
			
		||||
    if (y.ss.getState(id.user) < id.clock) {
 | 
			
		||||
      missing.push(new ID(id.user, id.clock - 1))
 | 
			
		||||
    }
 | 
			
		||||
    return missing
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										35
									
								
								src/Struct/ItemEmbed.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/Struct/ItemEmbed.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,35 @@
 | 
			
		||||
import Item from './Item.mjs'
 | 
			
		||||
import { logItemHelper } from '../MessageHandler/messageToString.mjs'
 | 
			
		||||
 | 
			
		||||
export default class ItemEmbed extends Item {
 | 
			
		||||
  constructor () {
 | 
			
		||||
    super()
 | 
			
		||||
    this.embed = null
 | 
			
		||||
  }
 | 
			
		||||
  _copy (undeleteChildren, copyPosition) {
 | 
			
		||||
    let struct = super._copy(undeleteChildren, copyPosition)
 | 
			
		||||
    struct.embed = this.embed
 | 
			
		||||
    return struct
 | 
			
		||||
  }
 | 
			
		||||
  get _length () {
 | 
			
		||||
    return 1
 | 
			
		||||
  }
 | 
			
		||||
  _fromBinary (y, decoder) {
 | 
			
		||||
    const missing = super._fromBinary(y, decoder)
 | 
			
		||||
    this.embed = JSON.parse(decoder.readVarString())
 | 
			
		||||
    return missing
 | 
			
		||||
  }
 | 
			
		||||
  _toBinary (encoder) {
 | 
			
		||||
    super._toBinary(encoder)
 | 
			
		||||
    encoder.writeVarString(JSON.stringify(this.embed))
 | 
			
		||||
  }
 | 
			
		||||
  /**
 | 
			
		||||
   * Transform this YXml Type to a readable format.
 | 
			
		||||
   * Useful for logging as all Items and Delete implement this method.
 | 
			
		||||
   *
 | 
			
		||||
   * @private
 | 
			
		||||
   */
 | 
			
		||||
  _logString () {
 | 
			
		||||
    return logItemHelper('ItemEmbed', this, `embed:${JSON.stringify(this.embed)}`)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										42
									
								
								src/Struct/ItemFormat.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/Struct/ItemFormat.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,42 @@
 | 
			
		||||
import Item from './Item.mjs'
 | 
			
		||||
import { logItemHelper } from '../MessageHandler/messageToString.mjs'
 | 
			
		||||
 | 
			
		||||
export default class ItemFormat extends Item {
 | 
			
		||||
  constructor () {
 | 
			
		||||
    super()
 | 
			
		||||
    this.key = null
 | 
			
		||||
    this.value = null
 | 
			
		||||
  }
 | 
			
		||||
  _copy (undeleteChildren, copyPosition) {
 | 
			
		||||
    let struct = super._copy(undeleteChildren, copyPosition)
 | 
			
		||||
    struct.key = this.key
 | 
			
		||||
    struct.value = this.value
 | 
			
		||||
    return struct
 | 
			
		||||
  }
 | 
			
		||||
  get _length () {
 | 
			
		||||
    return 1
 | 
			
		||||
  }
 | 
			
		||||
  get _countable () {
 | 
			
		||||
    return false
 | 
			
		||||
  }
 | 
			
		||||
  _fromBinary (y, decoder) {
 | 
			
		||||
    const missing = super._fromBinary(y, decoder)
 | 
			
		||||
    this.key = decoder.readVarString()
 | 
			
		||||
    this.value = JSON.parse(decoder.readVarString())
 | 
			
		||||
    return missing
 | 
			
		||||
  }
 | 
			
		||||
  _toBinary (encoder) {
 | 
			
		||||
    super._toBinary(encoder)
 | 
			
		||||
    encoder.writeVarString(this.key)
 | 
			
		||||
    encoder.writeVarString(JSON.stringify(this.value))
 | 
			
		||||
  }
 | 
			
		||||
  /**
 | 
			
		||||
   * Transform this YXml Type to a readable format.
 | 
			
		||||
   * Useful for logging as all Items and Delete implement this method.
 | 
			
		||||
   *
 | 
			
		||||
   * @private
 | 
			
		||||
   */
 | 
			
		||||
  _logString () {
 | 
			
		||||
    return logItemHelper('ItemFormat', this, `key:${JSON.stringify(this.key)},value:${JSON.stringify(this.value)}`)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										68
									
								
								src/Struct/ItemJSON.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										68
									
								
								src/Struct/ItemJSON.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,68 @@
 | 
			
		||||
import Item, { splitHelper } from './Item.mjs'
 | 
			
		||||
import { logItemHelper } from '../MessageHandler/messageToString.mjs'
 | 
			
		||||
 | 
			
		||||
export default class ItemJSON extends Item {
 | 
			
		||||
  constructor () {
 | 
			
		||||
    super()
 | 
			
		||||
    this._content = null
 | 
			
		||||
  }
 | 
			
		||||
  _copy () {
 | 
			
		||||
    let struct = super._copy()
 | 
			
		||||
    struct._content = this._content
 | 
			
		||||
    return struct
 | 
			
		||||
  }
 | 
			
		||||
  get _length () {
 | 
			
		||||
    return this._content.length
 | 
			
		||||
  }
 | 
			
		||||
  _fromBinary (y, decoder) {
 | 
			
		||||
    let missing = super._fromBinary(y, decoder)
 | 
			
		||||
    let len = decoder.readVarUint()
 | 
			
		||||
    this._content = new Array(len)
 | 
			
		||||
    for (let i = 0; i < len; i++) {
 | 
			
		||||
      const ctnt = decoder.readVarString()
 | 
			
		||||
      let parsed
 | 
			
		||||
      if (ctnt === 'undefined') {
 | 
			
		||||
        parsed = undefined
 | 
			
		||||
      } else {
 | 
			
		||||
        parsed = JSON.parse(ctnt)
 | 
			
		||||
      }
 | 
			
		||||
      this._content[i] = parsed
 | 
			
		||||
    }
 | 
			
		||||
    return missing
 | 
			
		||||
  }
 | 
			
		||||
  _toBinary (encoder) {
 | 
			
		||||
    super._toBinary(encoder)
 | 
			
		||||
    let len = this._content.length
 | 
			
		||||
    encoder.writeVarUint(len)
 | 
			
		||||
    for (let i = 0; i < len; i++) {
 | 
			
		||||
      let encoded
 | 
			
		||||
      let content = this._content[i]
 | 
			
		||||
      if (content === undefined) {
 | 
			
		||||
        encoded = 'undefined'
 | 
			
		||||
      } else {
 | 
			
		||||
        encoded = JSON.stringify(content)
 | 
			
		||||
      }
 | 
			
		||||
      encoder.writeVarString(encoded)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  /**
 | 
			
		||||
   * Transform this YXml Type to a readable format.
 | 
			
		||||
   * Useful for logging as all Items and Delete implement this method.
 | 
			
		||||
   *
 | 
			
		||||
   * @private
 | 
			
		||||
   */
 | 
			
		||||
  _logString () {
 | 
			
		||||
    return logItemHelper('ItemJSON', this, `content:${JSON.stringify(this._content)}`)
 | 
			
		||||
  }
 | 
			
		||||
  _splitAt (y, diff) {
 | 
			
		||||
    if (diff === 0) {
 | 
			
		||||
      return this
 | 
			
		||||
    } else if (diff >= this._length) {
 | 
			
		||||
      return this._right
 | 
			
		||||
    }
 | 
			
		||||
    let item = new ItemJSON()
 | 
			
		||||
    item._content = this._content.splice(diff)
 | 
			
		||||
    splitHelper(y, this, item, diff)
 | 
			
		||||
    return item
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										47
									
								
								src/Struct/ItemString.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/Struct/ItemString.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,47 @@
 | 
			
		||||
import Item, { splitHelper } from './Item.mjs'
 | 
			
		||||
import { logItemHelper } from '../MessageHandler/messageToString.mjs'
 | 
			
		||||
 | 
			
		||||
export default class ItemString extends Item {
 | 
			
		||||
  constructor () {
 | 
			
		||||
    super()
 | 
			
		||||
    this._content = null
 | 
			
		||||
  }
 | 
			
		||||
  _copy () {
 | 
			
		||||
    let struct = super._copy()
 | 
			
		||||
    struct._content = this._content
 | 
			
		||||
    return struct
 | 
			
		||||
  }
 | 
			
		||||
  get _length () {
 | 
			
		||||
    return this._content.length
 | 
			
		||||
  }
 | 
			
		||||
  _fromBinary (y, decoder) {
 | 
			
		||||
    let missing = super._fromBinary(y, decoder)
 | 
			
		||||
    this._content = decoder.readVarString()
 | 
			
		||||
    return missing
 | 
			
		||||
  }
 | 
			
		||||
  _toBinary (encoder) {
 | 
			
		||||
    super._toBinary(encoder)
 | 
			
		||||
    encoder.writeVarString(this._content)
 | 
			
		||||
  }
 | 
			
		||||
  /**
 | 
			
		||||
   * Transform this YXml Type to a readable format.
 | 
			
		||||
   * Useful for logging as all Items and Delete implement this method.
 | 
			
		||||
   *
 | 
			
		||||
   * @private
 | 
			
		||||
   */
 | 
			
		||||
  _logString () {
 | 
			
		||||
    return logItemHelper('ItemString', this, `content:"${this._content}"`)
 | 
			
		||||
  }
 | 
			
		||||
  _splitAt (y, diff) {
 | 
			
		||||
    if (diff === 0) {
 | 
			
		||||
      return this
 | 
			
		||||
    } else if (diff >= this._length) {
 | 
			
		||||
      return this._right
 | 
			
		||||
    }
 | 
			
		||||
    let item = new ItemString()
 | 
			
		||||
    item._content = this._content.slice(diff)
 | 
			
		||||
    this._content = this._content.slice(0, diff)
 | 
			
		||||
    splitHelper(y, this, item, diff)
 | 
			
		||||
    return item
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										243
									
								
								src/Struct/Type.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										243
									
								
								src/Struct/Type.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,243 @@
 | 
			
		||||
import Item from './Item.mjs'
 | 
			
		||||
import EventHandler from '../Util/EventHandler.mjs'
 | 
			
		||||
import ID from '../Util/ID/ID.mjs'
 | 
			
		||||
 | 
			
		||||
// restructure children as if they were inserted one after another
 | 
			
		||||
function integrateChildren (y, start) {
 | 
			
		||||
  let right
 | 
			
		||||
  do {
 | 
			
		||||
    right = start._right
 | 
			
		||||
    start._right = null
 | 
			
		||||
    start._right_origin = null
 | 
			
		||||
    start._origin = start._left
 | 
			
		||||
    start._integrate(y)
 | 
			
		||||
    start = right
 | 
			
		||||
  } while (right !== null)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getListItemIDByPosition (type, i) {
 | 
			
		||||
  let pos = 0
 | 
			
		||||
  let n = type._start
 | 
			
		||||
  while (n !== null) {
 | 
			
		||||
    if (!n._deleted) {
 | 
			
		||||
      if (pos <= i && i < pos + n._length) {
 | 
			
		||||
        const id = n._id
 | 
			
		||||
        return new ID(id.user, id.clock + i - pos)
 | 
			
		||||
      }
 | 
			
		||||
      pos++
 | 
			
		||||
    }
 | 
			
		||||
    n = n._right
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function gcChildren (y, item) {
 | 
			
		||||
  while (item !== null) {
 | 
			
		||||
    item._delete(y, false, true)
 | 
			
		||||
    item._gc(y)
 | 
			
		||||
    item = item._right
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Abstract Yjs Type class
 | 
			
		||||
 */
 | 
			
		||||
export default class Type extends Item {
 | 
			
		||||
  constructor () {
 | 
			
		||||
    super()
 | 
			
		||||
    this._map = new Map()
 | 
			
		||||
    this._start = null
 | 
			
		||||
    this._y = null
 | 
			
		||||
    this._eventHandler = new EventHandler()
 | 
			
		||||
    this._deepEventHandler = new EventHandler()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Compute the path from this type to the specified target.
 | 
			
		||||
   *
 | 
			
		||||
   * @example
 | 
			
		||||
   * It should be accessible via `this.get(result[0]).get(result[1])..`
 | 
			
		||||
   * const path = type.getPathTo(child)
 | 
			
		||||
   * // assuming `type instanceof YArray`
 | 
			
		||||
   * console.log(path) // might look like => [2, 'key1']
 | 
			
		||||
   * child === type.get(path[0]).get(path[1])
 | 
			
		||||
   *
 | 
			
		||||
   * @param {YType} type Type target
 | 
			
		||||
   * @return {Array<string>} Path to the target
 | 
			
		||||
   */
 | 
			
		||||
  getPathTo (type) {
 | 
			
		||||
    if (type === this) {
 | 
			
		||||
      return []
 | 
			
		||||
    }
 | 
			
		||||
    const path = []
 | 
			
		||||
    const y = this._y
 | 
			
		||||
    while (type !== this && type !== y) {
 | 
			
		||||
      let parent = type._parent
 | 
			
		||||
      if (type._parentSub !== null) {
 | 
			
		||||
        path.unshift(type._parentSub)
 | 
			
		||||
      } else {
 | 
			
		||||
        // parent is array-ish
 | 
			
		||||
        for (let [i, child] of parent) {
 | 
			
		||||
          if (child === type) {
 | 
			
		||||
            path.unshift(i)
 | 
			
		||||
            break
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      type = parent
 | 
			
		||||
    }
 | 
			
		||||
    if (type !== this) {
 | 
			
		||||
      throw new Error('The type is not a child of this node')
 | 
			
		||||
    }
 | 
			
		||||
    return path
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @private
 | 
			
		||||
   * Call event listeners with an event. This will also add an event to all
 | 
			
		||||
   * parents (for `.observeDeep` handlers).
 | 
			
		||||
   */
 | 
			
		||||
  _callEventHandler (transaction, event) {
 | 
			
		||||
    const changedParentTypes = transaction.changedParentTypes
 | 
			
		||||
    this._eventHandler.callEventListeners(transaction, event)
 | 
			
		||||
    let type = this
 | 
			
		||||
    while (type !== this._y) {
 | 
			
		||||
      let events = changedParentTypes.get(type)
 | 
			
		||||
      if (events === undefined) {
 | 
			
		||||
        events = []
 | 
			
		||||
        changedParentTypes.set(type, events)
 | 
			
		||||
      }
 | 
			
		||||
      events.push(event)
 | 
			
		||||
      type = type._parent
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @private
 | 
			
		||||
   * Helper method to transact if the y instance is available.
 | 
			
		||||
   *
 | 
			
		||||
   * TODO: Currently event handlers are not thrown when a type is not registered
 | 
			
		||||
   *       with a Yjs instance.
 | 
			
		||||
   */
 | 
			
		||||
  _transact (f) {
 | 
			
		||||
    const y = this._y
 | 
			
		||||
    if (y !== null) {
 | 
			
		||||
      y.transact(f)
 | 
			
		||||
    } else {
 | 
			
		||||
      f(y)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Observe all events that are created on this type.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Function} f Observer function
 | 
			
		||||
   */
 | 
			
		||||
  observe (f) {
 | 
			
		||||
    this._eventHandler.addEventListener(f)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Observe all events that are created by this type and its children.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Function} f Observer function
 | 
			
		||||
   */
 | 
			
		||||
  observeDeep (f) {
 | 
			
		||||
    this._deepEventHandler.addEventListener(f)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Unregister an observer function.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Function} f Observer function
 | 
			
		||||
   */
 | 
			
		||||
  unobserve (f) {
 | 
			
		||||
    this._eventHandler.removeEventListener(f)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Unregister an observer function.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Function} f Observer function
 | 
			
		||||
   */
 | 
			
		||||
  unobserveDeep (f) {
 | 
			
		||||
    this._deepEventHandler.removeEventListener(f)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @private
 | 
			
		||||
   * Integrate this type into the Yjs instance.
 | 
			
		||||
   *
 | 
			
		||||
   * * Save this struct in the os
 | 
			
		||||
   * * This type is sent to other client
 | 
			
		||||
   * * Observer functions are fired
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Y} y The Yjs instance
 | 
			
		||||
   */
 | 
			
		||||
  _integrate (y) {
 | 
			
		||||
    super._integrate(y)
 | 
			
		||||
    this._y = y
 | 
			
		||||
    // when integrating children we must make sure to
 | 
			
		||||
    // integrate start
 | 
			
		||||
    const start = this._start
 | 
			
		||||
    if (start !== null) {
 | 
			
		||||
      this._start = null
 | 
			
		||||
      integrateChildren(y, start)
 | 
			
		||||
    }
 | 
			
		||||
    // integrate map children
 | 
			
		||||
    const map = this._map
 | 
			
		||||
    this._map = new Map()
 | 
			
		||||
    for (let t of map.values()) {
 | 
			
		||||
      // TODO make sure that right elements are deleted!
 | 
			
		||||
      integrateChildren(y, t)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _gcChildren (y) {
 | 
			
		||||
    gcChildren(y, this._start)
 | 
			
		||||
    this._start = null
 | 
			
		||||
    this._map.forEach(item => {
 | 
			
		||||
      gcChildren(y, item)
 | 
			
		||||
    })
 | 
			
		||||
    this._map = new Map()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _gc (y) {
 | 
			
		||||
    this._gcChildren(y)
 | 
			
		||||
    super._gc(y)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @private
 | 
			
		||||
   * Mark this Item as deleted.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Y} y The Yjs instance
 | 
			
		||||
   * @param {boolean} createDelete Whether to propagate a message that this
 | 
			
		||||
   *                               Type was deleted.
 | 
			
		||||
   * @param {boolean} [gcChildren=y._hasUndoManager===false] Whether to garbage
 | 
			
		||||
   *                                         collect the children of this type.
 | 
			
		||||
   */
 | 
			
		||||
  _delete (y, createDelete, gcChildren) {
 | 
			
		||||
    if (gcChildren === undefined || !y.gcEnabled) {
 | 
			
		||||
      gcChildren = y._hasUndoManager === false && y.gcEnabled
 | 
			
		||||
    }
 | 
			
		||||
    super._delete(y, createDelete, gcChildren)
 | 
			
		||||
    y._transaction.changedTypes.delete(this)
 | 
			
		||||
    // delete map types
 | 
			
		||||
    for (let value of this._map.values()) {
 | 
			
		||||
      if (value instanceof Item && !value._deleted) {
 | 
			
		||||
        value._delete(y, false, gcChildren)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    // delete array types
 | 
			
		||||
    let t = this._start
 | 
			
		||||
    while (t !== null) {
 | 
			
		||||
      if (!t._deleted) {
 | 
			
		||||
        t._delete(y, false, gcChildren)
 | 
			
		||||
      }
 | 
			
		||||
      t = t._right
 | 
			
		||||
    }
 | 
			
		||||
    if (gcChildren) {
 | 
			
		||||
      this._gcChildren(y)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										91
									
								
								src/Transaction.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										91
									
								
								src/Transaction.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,91 @@
 | 
			
		||||
import BinaryEncoder from './Util/Binary/Encoder.mjs'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A transaction is created for every change on the Yjs model. It is possible
 | 
			
		||||
 * to bundle changes on the Yjs model in a single transaction to
 | 
			
		||||
 * minimize the number on messages sent and the number of observer calls.
 | 
			
		||||
 * If possible the user of this library should bundle as many changes as
 | 
			
		||||
 * possible. Here is an example to illustrate the advantages of bundling:
 | 
			
		||||
 *
 | 
			
		||||
 * @example
 | 
			
		||||
 * const map = y.define('map', YMap)
 | 
			
		||||
 * // Log content when change is triggered
 | 
			
		||||
 * map.observe(function () {
 | 
			
		||||
 *   console.log('change triggered')
 | 
			
		||||
 * })
 | 
			
		||||
 * // Each change on the map type triggers a log message:
 | 
			
		||||
 * map.set('a', 0) // => "change triggered"
 | 
			
		||||
 * map.set('b', 0) // => "change triggered"
 | 
			
		||||
 * // When put in a transaction, it will trigger the log after the transaction:
 | 
			
		||||
 * y.transact(function () {
 | 
			
		||||
 *   map.set('a', 1)
 | 
			
		||||
 *   map.set('b', 1)
 | 
			
		||||
 * }) // => "change triggered"
 | 
			
		||||
 *
 | 
			
		||||
 */
 | 
			
		||||
export default class Transaction {
 | 
			
		||||
  constructor (y) {
 | 
			
		||||
    /**
 | 
			
		||||
     * @type {Y} The Yjs instance.
 | 
			
		||||
     */
 | 
			
		||||
    this.y = y
 | 
			
		||||
    /**
 | 
			
		||||
     * All new types that are added during a transaction.
 | 
			
		||||
     * @type {Set<Item>}
 | 
			
		||||
     */
 | 
			
		||||
    this.newTypes = new Set()
 | 
			
		||||
    /**
 | 
			
		||||
     * All types that were directly modified (property added or child
 | 
			
		||||
     * inserted/deleted). New types are not included in this Set.
 | 
			
		||||
     * Maps from type to parentSubs (`item._parentSub = null` for YArray)
 | 
			
		||||
     * @type {Set<YType,String>}
 | 
			
		||||
     */
 | 
			
		||||
    this.changedTypes = new Map()
 | 
			
		||||
    // TODO: rename deletedTypes
 | 
			
		||||
    /**
 | 
			
		||||
     * Set of all deleted Types and Structs.
 | 
			
		||||
     * @type {Set<Item>}
 | 
			
		||||
     */
 | 
			
		||||
    this.deletedStructs = new Set()
 | 
			
		||||
    /**
 | 
			
		||||
     * Saves the old state set of the Yjs instance. If a state was modified,
 | 
			
		||||
     * the original value is saved here.
 | 
			
		||||
     * @type {Map<Number,Number>}
 | 
			
		||||
     */
 | 
			
		||||
    this.beforeState = new Map()
 | 
			
		||||
    /**
 | 
			
		||||
     * Stores the events for the types that observe also child elements.
 | 
			
		||||
     * It is mainly used by `observeDeep`.
 | 
			
		||||
     * @type {Map<YType,Array<YEvent>>}
 | 
			
		||||
     */
 | 
			
		||||
    this.changedParentTypes = new Map()
 | 
			
		||||
    this.encodedStructsLen = 0
 | 
			
		||||
    this._encodedStructs = new BinaryEncoder()
 | 
			
		||||
    this._encodedStructs.writeUint32(0)
 | 
			
		||||
  }
 | 
			
		||||
  get encodedStructs () {
 | 
			
		||||
    this._encodedStructs.setUint32(0, this.encodedStructsLen)
 | 
			
		||||
    return this._encodedStructs
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function writeStructToTransaction (transaction, struct) {
 | 
			
		||||
  transaction.encodedStructsLen++
 | 
			
		||||
  struct._toBinary(transaction._encodedStructs)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @private
 | 
			
		||||
 */
 | 
			
		||||
export function transactionTypeChanged (y, type, sub) {
 | 
			
		||||
  if (type !== y && !type._deleted && !y._transaction.newTypes.has(type)) {
 | 
			
		||||
    const changedTypes = y._transaction.changedTypes
 | 
			
		||||
    let subs = changedTypes.get(type)
 | 
			
		||||
    if (subs === undefined) {
 | 
			
		||||
      // create if it doesn't exist yet
 | 
			
		||||
      subs = new Set()
 | 
			
		||||
      changedTypes.set(type, subs)
 | 
			
		||||
    }
 | 
			
		||||
    subs.add(sub)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										378
									
								
								src/Types/YArray/YArray.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										378
									
								
								src/Types/YArray/YArray.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,378 @@
 | 
			
		||||
import Type from '../../Struct/Type.mjs'
 | 
			
		||||
import ItemJSON from '../../Struct/ItemJSON.mjs'
 | 
			
		||||
import ItemString from '../../Struct/ItemString.mjs'
 | 
			
		||||
import { logID, logItemHelper } from '../../MessageHandler/messageToString.mjs'
 | 
			
		||||
import YEvent from '../../Util/YEvent.mjs'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Event that describes the changes on a YArray
 | 
			
		||||
 *
 | 
			
		||||
 * @param {YArray} yarray The changed type
 | 
			
		||||
 * @param {Boolean} remote Whether the changed was caused by a remote peer
 | 
			
		||||
 * @param {Transaction} transaction The transaction object
 | 
			
		||||
 */
 | 
			
		||||
export class YArrayEvent extends YEvent {
 | 
			
		||||
  constructor (yarray, remote, transaction) {
 | 
			
		||||
    super(yarray)
 | 
			
		||||
    this.remote = remote
 | 
			
		||||
    this._transaction = transaction
 | 
			
		||||
    this._addedElements = null
 | 
			
		||||
    this._removedElements = null
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Child elements that were added in this transaction.
 | 
			
		||||
   *
 | 
			
		||||
   * @return {Set}
 | 
			
		||||
   */
 | 
			
		||||
  get addedElements () {
 | 
			
		||||
    if (this._addedElements === null) {
 | 
			
		||||
      const target = this.target
 | 
			
		||||
      const transaction = this._transaction
 | 
			
		||||
      const addedElements = new Set()
 | 
			
		||||
      transaction.newTypes.forEach(function (type) {
 | 
			
		||||
        if (type._parent === target && !transaction.deletedStructs.has(type)) {
 | 
			
		||||
          addedElements.add(type)
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      this._addedElements = addedElements
 | 
			
		||||
    }
 | 
			
		||||
    return this._addedElements
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Child elements that were removed in this transaction.
 | 
			
		||||
   *
 | 
			
		||||
   * @return {Set}
 | 
			
		||||
   */
 | 
			
		||||
  get removedElements () {
 | 
			
		||||
    if (this._removedElements === null) {
 | 
			
		||||
      const target = this.target
 | 
			
		||||
      const transaction = this._transaction
 | 
			
		||||
      const removedElements = new Set()
 | 
			
		||||
      transaction.deletedStructs.forEach(function (struct) {
 | 
			
		||||
        if (struct._parent === target && !transaction.newTypes.has(struct)) {
 | 
			
		||||
          removedElements.add(struct)
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      this._removedElements = removedElements
 | 
			
		||||
    }
 | 
			
		||||
    return this._removedElements
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A shared Array implementation.
 | 
			
		||||
 */
 | 
			
		||||
export default class YArray extends Type {
 | 
			
		||||
  /**
 | 
			
		||||
   * @private
 | 
			
		||||
   * Creates YArray Event and calls observers.
 | 
			
		||||
   */
 | 
			
		||||
  _callObserver (transaction, parentSubs, remote) {
 | 
			
		||||
    this._callEventHandler(transaction, new YArrayEvent(this, remote, transaction))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the i-th element from a YArray.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Integer} index The index of the element to return from the YArray
 | 
			
		||||
   */
 | 
			
		||||
  get (index) {
 | 
			
		||||
    let n = this._start
 | 
			
		||||
    while (n !== null) {
 | 
			
		||||
      if (!n._deleted && n._countable) {
 | 
			
		||||
        if (index < n._length) {
 | 
			
		||||
          if (n.constructor === ItemJSON || n.constructor === ItemString) {
 | 
			
		||||
            return n._content[index]
 | 
			
		||||
          } else {
 | 
			
		||||
            return n
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        index -= n._length
 | 
			
		||||
      }
 | 
			
		||||
      n = n._right
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Transforms this YArray to a JavaScript Array.
 | 
			
		||||
   *
 | 
			
		||||
   * @return {Array}
 | 
			
		||||
   */
 | 
			
		||||
  toArray () {
 | 
			
		||||
    return this.map(c => c)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Transforms this Shared Type to a JSON object.
 | 
			
		||||
   *
 | 
			
		||||
   * @return {Array}
 | 
			
		||||
   */
 | 
			
		||||
  toJSON () {
 | 
			
		||||
    return this.map(c => {
 | 
			
		||||
      if (c instanceof Type) {
 | 
			
		||||
        if (c.toJSON !== null) {
 | 
			
		||||
          return c.toJSON()
 | 
			
		||||
        } else {
 | 
			
		||||
          return c.toString()
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      return c
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns an Array with the result of calling a provided function on every
 | 
			
		||||
   * element of this YArray.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Function} f Function that produces an element of the new Array
 | 
			
		||||
   * @return {Array} A new array with each element being the result of the
 | 
			
		||||
   *                 callback function
 | 
			
		||||
   */
 | 
			
		||||
  map (f) {
 | 
			
		||||
    const res = []
 | 
			
		||||
    this.forEach((c, i) => {
 | 
			
		||||
      res.push(f(c, i, this))
 | 
			
		||||
    })
 | 
			
		||||
    return res
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Executes a provided function on once on overy element of this YArray.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Function} f A function to execute on every element of this YArray.
 | 
			
		||||
   */
 | 
			
		||||
  forEach (f) {
 | 
			
		||||
    let index = 0
 | 
			
		||||
    let n = this._start
 | 
			
		||||
    while (n !== null) {
 | 
			
		||||
      if (!n._deleted && n._countable) {
 | 
			
		||||
        if (n instanceof Type) {
 | 
			
		||||
          f(n, index++, this)
 | 
			
		||||
        } else {
 | 
			
		||||
          const content = n._content
 | 
			
		||||
          const contentLen = content.length
 | 
			
		||||
          for (let i = 0; i < contentLen; i++) {
 | 
			
		||||
            index++
 | 
			
		||||
            f(content[i], index, this)
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      n = n._right
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Computes the length of this YArray.
 | 
			
		||||
   */
 | 
			
		||||
  get length () {
 | 
			
		||||
    let length = 0
 | 
			
		||||
    let n = this._start
 | 
			
		||||
    while (n !== null) {
 | 
			
		||||
      if (!n._deleted && n._countable) {
 | 
			
		||||
        length += n._length
 | 
			
		||||
      }
 | 
			
		||||
      n = n._right
 | 
			
		||||
    }
 | 
			
		||||
    return length
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  [Symbol.iterator] () {
 | 
			
		||||
    return {
 | 
			
		||||
      next: function () {
 | 
			
		||||
        while (this._item !== null && (this._item._deleted || this._item._length <= this._itemElement)) {
 | 
			
		||||
          // item is deleted or itemElement does not exist (is deleted)
 | 
			
		||||
          this._item = this._item._right
 | 
			
		||||
          this._itemElement = 0
 | 
			
		||||
        }
 | 
			
		||||
        if (this._item === null) {
 | 
			
		||||
          return {
 | 
			
		||||
            done: true
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        let content
 | 
			
		||||
        if (this._item instanceof Type) {
 | 
			
		||||
          content = this._item
 | 
			
		||||
        } else {
 | 
			
		||||
          content = this._item._content[this._itemElement++]
 | 
			
		||||
        }
 | 
			
		||||
        return {
 | 
			
		||||
          value: content,
 | 
			
		||||
          done: false
 | 
			
		||||
        }
 | 
			
		||||
      },
 | 
			
		||||
      _item: this._start,
 | 
			
		||||
      _itemElement: 0,
 | 
			
		||||
      _count: 0
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Deletes elements starting from an index.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Integer} index Index at which to start deleting elements
 | 
			
		||||
   * @param {Integer} length The number of elements to remove. Defaults to 1.
 | 
			
		||||
   */
 | 
			
		||||
  delete (index, length = 1) {
 | 
			
		||||
    this._y.transact(() => {
 | 
			
		||||
      let item = this._start
 | 
			
		||||
      let count = 0
 | 
			
		||||
      while (item !== null && length > 0) {
 | 
			
		||||
        if (!item._deleted && item._countable) {
 | 
			
		||||
          if (count <= index && index < count + item._length) {
 | 
			
		||||
            const diffDel = index - count
 | 
			
		||||
            item = item._splitAt(this._y, diffDel)
 | 
			
		||||
            item._splitAt(this._y, length)
 | 
			
		||||
            length -= item._length
 | 
			
		||||
            item._delete(this._y)
 | 
			
		||||
            count += diffDel
 | 
			
		||||
          } else {
 | 
			
		||||
            count += item._length
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        item = item._right
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    if (length > 0) {
 | 
			
		||||
      throw new Error('Delete exceeds the range of the YArray')
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @private
 | 
			
		||||
   * Inserts content after an element container.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Item} left The element container to use as a reference.
 | 
			
		||||
   * @param {Array} content The Array of content to insert (see {@see insert})
 | 
			
		||||
   */
 | 
			
		||||
  insertAfter (left, content) {
 | 
			
		||||
    this._transact(y => {
 | 
			
		||||
      let right
 | 
			
		||||
      if (left === null) {
 | 
			
		||||
        right = this._start
 | 
			
		||||
      } else {
 | 
			
		||||
        right = left._right
 | 
			
		||||
      }
 | 
			
		||||
      let prevJsonIns = null
 | 
			
		||||
      for (let i = 0; i < content.length; i++) {
 | 
			
		||||
        let c = content[i]
 | 
			
		||||
        if (typeof c === 'function') {
 | 
			
		||||
          c = new c() // eslint-disable-line new-cap
 | 
			
		||||
        }
 | 
			
		||||
        if (c instanceof Type) {
 | 
			
		||||
          if (prevJsonIns !== null) {
 | 
			
		||||
            if (y !== null) {
 | 
			
		||||
              prevJsonIns._integrate(y)
 | 
			
		||||
            }
 | 
			
		||||
            left = prevJsonIns
 | 
			
		||||
            prevJsonIns = null
 | 
			
		||||
          }
 | 
			
		||||
          c._origin = left
 | 
			
		||||
          c._left = left
 | 
			
		||||
          c._right = right
 | 
			
		||||
          c._right_origin = right
 | 
			
		||||
          c._parent = this
 | 
			
		||||
          if (y !== null) {
 | 
			
		||||
            c._integrate(y)
 | 
			
		||||
          } else if (left === null) {
 | 
			
		||||
            this._start = c
 | 
			
		||||
          } else {
 | 
			
		||||
            left._right = c
 | 
			
		||||
          }
 | 
			
		||||
          left = c
 | 
			
		||||
        } else {
 | 
			
		||||
          if (prevJsonIns === null) {
 | 
			
		||||
            prevJsonIns = new ItemJSON()
 | 
			
		||||
            prevJsonIns._origin = left
 | 
			
		||||
            prevJsonIns._left = left
 | 
			
		||||
            prevJsonIns._right = right
 | 
			
		||||
            prevJsonIns._right_origin = right
 | 
			
		||||
            prevJsonIns._parent = this
 | 
			
		||||
            prevJsonIns._content = []
 | 
			
		||||
          }
 | 
			
		||||
          prevJsonIns._content.push(c)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      if (prevJsonIns !== null) {
 | 
			
		||||
        if (y !== null) {
 | 
			
		||||
          prevJsonIns._integrate(y)
 | 
			
		||||
        } else if (prevJsonIns._left === null) {
 | 
			
		||||
          this._start = prevJsonIns
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    return content
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Inserts new content at an index.
 | 
			
		||||
   *
 | 
			
		||||
   * Important: This function expects an array of content. Not just a content
 | 
			
		||||
   * object. The reason for this "weirdness" is that inserting several elements
 | 
			
		||||
   * is very efficient when it is done as a single operation.
 | 
			
		||||
   *
 | 
			
		||||
   * @example
 | 
			
		||||
   *  // Insert character 'a' at position 0
 | 
			
		||||
   *  yarray.insert(0, ['a'])
 | 
			
		||||
   *  // Insert numbers 1, 2 at position 1
 | 
			
		||||
   *  yarray.insert(2, [1, 2])
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Integer} index The index to insert content at.
 | 
			
		||||
   * @param {Array} content The array of content
 | 
			
		||||
   */
 | 
			
		||||
  insert (index, content) {
 | 
			
		||||
    this._transact(() => {
 | 
			
		||||
      let left = null
 | 
			
		||||
      let right = this._start
 | 
			
		||||
      let count = 0
 | 
			
		||||
      const y = this._y
 | 
			
		||||
      while (right !== null) {
 | 
			
		||||
        const rightLen = right._deleted ? 0 : (right._length - 1)
 | 
			
		||||
        if (count <= index && index <= count + rightLen) {
 | 
			
		||||
          const splitDiff = index - count
 | 
			
		||||
          right = right._splitAt(y, splitDiff)
 | 
			
		||||
          left = right._left
 | 
			
		||||
          count += splitDiff
 | 
			
		||||
          break
 | 
			
		||||
        }
 | 
			
		||||
        if (!right._deleted) {
 | 
			
		||||
          count += right._length
 | 
			
		||||
        }
 | 
			
		||||
        left = right
 | 
			
		||||
        right = right._right
 | 
			
		||||
      }
 | 
			
		||||
      if (index > count) {
 | 
			
		||||
        throw new Error('Index exceeds array range!')
 | 
			
		||||
      }
 | 
			
		||||
      this.insertAfter(left, content)
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Appends content to this YArray.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Array} content Array of content to append.
 | 
			
		||||
   */
 | 
			
		||||
  push (content) {
 | 
			
		||||
    let n = this._start
 | 
			
		||||
    let lastUndeleted = null
 | 
			
		||||
    while (n !== null) {
 | 
			
		||||
      if (!n._deleted) {
 | 
			
		||||
        lastUndeleted = n
 | 
			
		||||
      }
 | 
			
		||||
      n = n._right
 | 
			
		||||
    }
 | 
			
		||||
    this.insertAfter(lastUndeleted, content)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Transform this YXml Type to a readable format.
 | 
			
		||||
   * Useful for logging as all Items and Delete implement this method.
 | 
			
		||||
   *
 | 
			
		||||
   * @private
 | 
			
		||||
   */
 | 
			
		||||
  _logString () {
 | 
			
		||||
    return logItemHelper('YArray', this, `start:${logID(this._start)}"`)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										175
									
								
								src/Types/YMap/YMap.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										175
									
								
								src/Types/YMap/YMap.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,175 @@
 | 
			
		||||
import Item from '../../Struct/Item.mjs'
 | 
			
		||||
import Type from '../../Struct/Type.mjs'
 | 
			
		||||
import ItemJSON from '../../Struct/ItemJSON.mjs'
 | 
			
		||||
import { logItemHelper } from '../../MessageHandler/messageToString.mjs'
 | 
			
		||||
import YEvent from '../../Util/YEvent.mjs'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Event that describes the changes on a YMap.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {YMap} ymap The YArray that changed.
 | 
			
		||||
 * @param {Set<any>} subs The keys that changed.
 | 
			
		||||
 * @param {boolean} remote Whether the change was created by a remote peer.
 | 
			
		||||
 */
 | 
			
		||||
export class YMapEvent extends YEvent {
 | 
			
		||||
  constructor (ymap, subs, remote) {
 | 
			
		||||
    super(ymap)
 | 
			
		||||
    this.keysChanged = subs
 | 
			
		||||
    this.remote = remote
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A shared Map implementation.
 | 
			
		||||
 */
 | 
			
		||||
export default class YMap extends Type {
 | 
			
		||||
  /**
 | 
			
		||||
   * @private
 | 
			
		||||
   * Creates YMap Event and calls observers.
 | 
			
		||||
   */
 | 
			
		||||
  _callObserver (transaction, parentSubs, remote) {
 | 
			
		||||
    this._callEventHandler(transaction, new YMapEvent(this, parentSubs, remote))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Transforms this Shared Type to a JSON object.
 | 
			
		||||
   *
 | 
			
		||||
   * @return {Object}
 | 
			
		||||
   */
 | 
			
		||||
  toJSON () {
 | 
			
		||||
    const map = {}
 | 
			
		||||
    for (let [key, item] of this._map) {
 | 
			
		||||
      if (!item._deleted) {
 | 
			
		||||
        let res
 | 
			
		||||
        if (item instanceof Type) {
 | 
			
		||||
          if (item.toJSON !== undefined) {
 | 
			
		||||
            res = item.toJSON()
 | 
			
		||||
          } else {
 | 
			
		||||
            res = item.toString()
 | 
			
		||||
          }
 | 
			
		||||
        } else {
 | 
			
		||||
          res = item._content[0]
 | 
			
		||||
        }
 | 
			
		||||
        map[key] = res
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return map
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the keys for each element in the YMap Type.
 | 
			
		||||
   *
 | 
			
		||||
   * @return {Array}
 | 
			
		||||
   */
 | 
			
		||||
  keys () {
 | 
			
		||||
    // TODO: Should return either Iterator or Set!
 | 
			
		||||
    let keys = []
 | 
			
		||||
    for (let [key, value] of this._map) {
 | 
			
		||||
      if (!value._deleted) {
 | 
			
		||||
        keys.push(key)
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return keys
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Remove a specified element from this YMap.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {encodable} key The key of the element to remove.
 | 
			
		||||
   */
 | 
			
		||||
  delete (key) {
 | 
			
		||||
    this._transact((y) => {
 | 
			
		||||
      let c = this._map.get(key)
 | 
			
		||||
      if (y !== null && c !== undefined) {
 | 
			
		||||
        c._delete(y)
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Adds or updates an element with a specified key and value.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {encodable} key The key of the element to add to this YMap.
 | 
			
		||||
   * @param {encodable | YType} value The value of the element to add to this
 | 
			
		||||
   *                                  YMap.
 | 
			
		||||
   */
 | 
			
		||||
  set (key, value) {
 | 
			
		||||
    this._transact(y => {
 | 
			
		||||
      const old = this._map.get(key) || null
 | 
			
		||||
      if (old !== null) {
 | 
			
		||||
        if (
 | 
			
		||||
          old.constructor === ItemJSON &&
 | 
			
		||||
          !old._deleted && old._content[0] === value
 | 
			
		||||
        ) {
 | 
			
		||||
          // Trying to overwrite with same value
 | 
			
		||||
          // break here
 | 
			
		||||
          return value
 | 
			
		||||
        }
 | 
			
		||||
        if (y !== null) {
 | 
			
		||||
          old._delete(y)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      let v
 | 
			
		||||
      if (typeof value === 'function') {
 | 
			
		||||
        v = new value() // eslint-disable-line new-cap
 | 
			
		||||
        value = v
 | 
			
		||||
      } else if (value instanceof Item) {
 | 
			
		||||
        v = value
 | 
			
		||||
      } else {
 | 
			
		||||
        v = new ItemJSON()
 | 
			
		||||
        v._content = [value]
 | 
			
		||||
      }
 | 
			
		||||
      v._right = old
 | 
			
		||||
      v._right_origin = old
 | 
			
		||||
      v._parent = this
 | 
			
		||||
      v._parentSub = key
 | 
			
		||||
      if (y !== null) {
 | 
			
		||||
        v._integrate(y)
 | 
			
		||||
      } else {
 | 
			
		||||
        this._map.set(key, v)
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
    return value
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns a specified element from this YMap.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {encodable} key The key of the element to return.
 | 
			
		||||
   */
 | 
			
		||||
  get (key) {
 | 
			
		||||
    let v = this._map.get(key)
 | 
			
		||||
    if (v === undefined || v._deleted) {
 | 
			
		||||
      return undefined
 | 
			
		||||
    }
 | 
			
		||||
    if (v instanceof Type) {
 | 
			
		||||
      return v
 | 
			
		||||
    } else {
 | 
			
		||||
      return v._content[v._content.length - 1]
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns a boolean indicating whether the specified key exists or not.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {encodable} key The key to test.
 | 
			
		||||
   */
 | 
			
		||||
  has (key) {
 | 
			
		||||
    let v = this._map.get(key)
 | 
			
		||||
    if (v === undefined || v._deleted) {
 | 
			
		||||
      return false
 | 
			
		||||
    } else {
 | 
			
		||||
      return true
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Transform this YXml Type to a readable format.
 | 
			
		||||
   * Useful for logging as all Items and Delete implement this method.
 | 
			
		||||
   *
 | 
			
		||||
   * @private
 | 
			
		||||
   */
 | 
			
		||||
  _logString () {
 | 
			
		||||
    return logItemHelper('YMap', this, `mapSize:${this._map.size}`)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										656
									
								
								src/Types/YText/YText.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										656
									
								
								src/Types/YText/YText.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,656 @@
 | 
			
		||||
import ItemEmbed from '../../Struct/ItemEmbed.mjs'
 | 
			
		||||
import ItemString from '../../Struct/ItemString.mjs'
 | 
			
		||||
import ItemFormat from '../../Struct/ItemFormat.mjs'
 | 
			
		||||
import { logItemHelper } from '../../MessageHandler/messageToString.mjs'
 | 
			
		||||
import { YArrayEvent, default as YArray } from '../YArray/YArray.mjs'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @private
 | 
			
		||||
 */
 | 
			
		||||
function integrateItem (item, parent, y, left, right) {
 | 
			
		||||
  item._origin = left
 | 
			
		||||
  item._left = left
 | 
			
		||||
  item._right = right
 | 
			
		||||
  item._right_origin = right
 | 
			
		||||
  item._parent = parent
 | 
			
		||||
  if (y !== null) {
 | 
			
		||||
    item._integrate(y)
 | 
			
		||||
  } else if (left === null) {
 | 
			
		||||
    parent._start = item
 | 
			
		||||
  } else {
 | 
			
		||||
    left._right = item
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @private
 | 
			
		||||
 */
 | 
			
		||||
function findNextPosition (currentAttributes, parent, left, right, count) {
 | 
			
		||||
  while (right !== null && count > 0) {
 | 
			
		||||
    switch (right.constructor) {
 | 
			
		||||
      case ItemEmbed:
 | 
			
		||||
      case ItemString:
 | 
			
		||||
        const rightLen = right._deleted ? 0 : (right._length - 1)
 | 
			
		||||
        if (count <= rightLen) {
 | 
			
		||||
          right = right._splitAt(parent._y, count)
 | 
			
		||||
          left = right._left
 | 
			
		||||
          return [left, right, currentAttributes]
 | 
			
		||||
        }
 | 
			
		||||
        if (right._deleted === false) {
 | 
			
		||||
          count -= right._length
 | 
			
		||||
        }
 | 
			
		||||
        break
 | 
			
		||||
      case ItemFormat:
 | 
			
		||||
        if (right._deleted === false) {
 | 
			
		||||
          updateCurrentAttributes(currentAttributes, right)
 | 
			
		||||
        }
 | 
			
		||||
        break
 | 
			
		||||
    }
 | 
			
		||||
    left = right
 | 
			
		||||
    right = right._right
 | 
			
		||||
  }
 | 
			
		||||
  return [left, right, currentAttributes]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @private
 | 
			
		||||
 */
 | 
			
		||||
function findPosition (parent, index) {
 | 
			
		||||
  let currentAttributes = new Map()
 | 
			
		||||
  let left = null
 | 
			
		||||
  let right = parent._start
 | 
			
		||||
  return findNextPosition(currentAttributes, parent, left, right, index)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Negate applied formats
 | 
			
		||||
 *
 | 
			
		||||
 * @private
 | 
			
		||||
 */
 | 
			
		||||
function insertNegatedAttributes (y, parent, left, right, negatedAttributes) {
 | 
			
		||||
  // check if we really need to remove attributes
 | 
			
		||||
  while (
 | 
			
		||||
    right !== null && (
 | 
			
		||||
      right._deleted === true || (
 | 
			
		||||
        right.constructor === ItemFormat &&
 | 
			
		||||
        (negatedAttributes.get(right.key) === right.value)
 | 
			
		||||
      )
 | 
			
		||||
    )
 | 
			
		||||
  ) {
 | 
			
		||||
    if (right._deleted === false) {
 | 
			
		||||
      negatedAttributes.delete(right.key)
 | 
			
		||||
    }
 | 
			
		||||
    left = right
 | 
			
		||||
    right = right._right
 | 
			
		||||
  }
 | 
			
		||||
  for (let [key, val] of negatedAttributes) {
 | 
			
		||||
    let format = new ItemFormat()
 | 
			
		||||
    format.key = key
 | 
			
		||||
    format.value = val
 | 
			
		||||
    integrateItem(format, parent, y, left, right)
 | 
			
		||||
    left = format
 | 
			
		||||
  }
 | 
			
		||||
  return [left, right]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @private
 | 
			
		||||
 */
 | 
			
		||||
function updateCurrentAttributes (currentAttributes, item) {
 | 
			
		||||
  const value = item.value
 | 
			
		||||
  const key = item.key
 | 
			
		||||
  if (value === null) {
 | 
			
		||||
    currentAttributes.delete(key)
 | 
			
		||||
  } else {
 | 
			
		||||
    currentAttributes.set(key, value)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @private
 | 
			
		||||
 */
 | 
			
		||||
function minimizeAttributeChanges (left, right, currentAttributes, attributes) {
 | 
			
		||||
  // go right while attributes[right.key] === right.value (or right is deleted)
 | 
			
		||||
  while (true) {
 | 
			
		||||
    if (right === null) {
 | 
			
		||||
      break
 | 
			
		||||
    } else if (right._deleted === true) {
 | 
			
		||||
      // continue
 | 
			
		||||
    } else if (right.constructor === ItemFormat && (attributes[right.key] || null) === right.value) {
 | 
			
		||||
      // found a format, update currentAttributes and continue
 | 
			
		||||
      updateCurrentAttributes(currentAttributes, right)
 | 
			
		||||
    } else {
 | 
			
		||||
      break
 | 
			
		||||
    }
 | 
			
		||||
    left = right
 | 
			
		||||
    right = right._right
 | 
			
		||||
  }
 | 
			
		||||
  return [left, right]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @private
 | 
			
		||||
 */
 | 
			
		||||
function insertAttributes (y, parent, left, right, attributes, currentAttributes) {
 | 
			
		||||
  const negatedAttributes = new Map()
 | 
			
		||||
  // insert format-start items
 | 
			
		||||
  for (let key in attributes) {
 | 
			
		||||
    const val = attributes[key]
 | 
			
		||||
    const currentVal = currentAttributes.get(key)
 | 
			
		||||
    if (currentVal !== val) {
 | 
			
		||||
      // save negated attribute (set null if currentVal undefined)
 | 
			
		||||
      negatedAttributes.set(key, currentVal || null)
 | 
			
		||||
      let format = new ItemFormat()
 | 
			
		||||
      format.key = key
 | 
			
		||||
      format.value = val
 | 
			
		||||
      integrateItem(format, parent, y, left, right)
 | 
			
		||||
      left = format
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return [left, right, negatedAttributes]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @private
 | 
			
		||||
 */
 | 
			
		||||
function insertText (y, text, parent, left, right, currentAttributes, attributes) {
 | 
			
		||||
  for (let [key] of currentAttributes) {
 | 
			
		||||
    if (attributes[key] === undefined) {
 | 
			
		||||
      attributes[key] = null
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  [left, right] = minimizeAttributeChanges(left, right, currentAttributes, attributes)
 | 
			
		||||
  let negatedAttributes
 | 
			
		||||
  [left, right, negatedAttributes] = insertAttributes(y, parent, left, right, attributes, currentAttributes)
 | 
			
		||||
  // insert content
 | 
			
		||||
  let item
 | 
			
		||||
  if (text.constructor === String) {
 | 
			
		||||
    item = new ItemString()
 | 
			
		||||
    item._content = text
 | 
			
		||||
  } else {
 | 
			
		||||
    item = new ItemEmbed()
 | 
			
		||||
    item.embed = text
 | 
			
		||||
  }
 | 
			
		||||
  integrateItem(item, parent, y, left, right)
 | 
			
		||||
  left = item
 | 
			
		||||
  return insertNegatedAttributes(y, parent, left, right, negatedAttributes)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @private
 | 
			
		||||
 */
 | 
			
		||||
function formatText (y, length, parent, left, right, currentAttributes, attributes) {
 | 
			
		||||
  [left, right] = minimizeAttributeChanges(left, right, currentAttributes, attributes)
 | 
			
		||||
  let negatedAttributes
 | 
			
		||||
  [left, right, negatedAttributes] = insertAttributes(y, parent, left, right, attributes, currentAttributes)
 | 
			
		||||
  // iterate until first non-format or null is found
 | 
			
		||||
  // delete all formats with attributes[format.key] != null
 | 
			
		||||
  while (length > 0 && right !== null) {
 | 
			
		||||
    if (right._deleted === false) {
 | 
			
		||||
      switch (right.constructor) {
 | 
			
		||||
        case ItemFormat:
 | 
			
		||||
          const attr = attributes[right.key]
 | 
			
		||||
          if (attr !== undefined) {
 | 
			
		||||
            if (attr === right.value) {
 | 
			
		||||
              negatedAttributes.delete(right.key)
 | 
			
		||||
            } else {
 | 
			
		||||
              negatedAttributes.set(right.key, right.value)
 | 
			
		||||
            }
 | 
			
		||||
            right._delete(y)
 | 
			
		||||
          }
 | 
			
		||||
          updateCurrentAttributes(currentAttributes, right)
 | 
			
		||||
          break
 | 
			
		||||
        case ItemEmbed:
 | 
			
		||||
        case ItemString:
 | 
			
		||||
          right._splitAt(y, length)
 | 
			
		||||
          length -= right._length
 | 
			
		||||
          break
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    left = right
 | 
			
		||||
    right = right._right
 | 
			
		||||
  }
 | 
			
		||||
  return insertNegatedAttributes(y, parent, left, right, negatedAttributes)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * @private
 | 
			
		||||
 */
 | 
			
		||||
function deleteText (y, length, parent, left, right, currentAttributes) {
 | 
			
		||||
  while (length > 0 && right !== null) {
 | 
			
		||||
    if (right._deleted === false) {
 | 
			
		||||
      switch (right.constructor) {
 | 
			
		||||
        case ItemFormat:
 | 
			
		||||
          updateCurrentAttributes(currentAttributes, right)
 | 
			
		||||
          break
 | 
			
		||||
        case ItemEmbed:
 | 
			
		||||
        case ItemString:
 | 
			
		||||
          right._splitAt(y, length)
 | 
			
		||||
          length -= right._length
 | 
			
		||||
          right._delete(y)
 | 
			
		||||
          break
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    left = right
 | 
			
		||||
    right = right._right
 | 
			
		||||
  }
 | 
			
		||||
  return [left, right]
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO: In the quill delta representation we should also use the format {ops:[..]}
 | 
			
		||||
/**
 | 
			
		||||
 * The Quill Delta format represents changes on a text document with
 | 
			
		||||
 * formatting information. For mor information visit {@link https://quilljs.com/docs/delta/|Quill Delta}
 | 
			
		||||
 *
 | 
			
		||||
 * @example
 | 
			
		||||
 *   {
 | 
			
		||||
 *     ops: [
 | 
			
		||||
 *       { insert: 'Gandalf', attributes: { bold: true } },
 | 
			
		||||
 *       { insert: ' the ' },
 | 
			
		||||
 *       { insert: 'Grey', attributes: { color: '#cccccc' } }
 | 
			
		||||
 *     ]
 | 
			
		||||
 *   }
 | 
			
		||||
 *
 | 
			
		||||
 * @typedef {Array<Object>} Delta
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
 /**
 | 
			
		||||
  * Attributes that can be assigned to a selection of text.
 | 
			
		||||
  *
 | 
			
		||||
  * @example
 | 
			
		||||
  *   {
 | 
			
		||||
  *     bold: true,
 | 
			
		||||
  *     font-size: '40px'
 | 
			
		||||
  *   }
 | 
			
		||||
  *
 | 
			
		||||
  * @typedef {Object} TextAttributes
 | 
			
		||||
  */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Event that describes the changes on a YText type.
 | 
			
		||||
 *
 | 
			
		||||
 * @private
 | 
			
		||||
 */
 | 
			
		||||
class YTextEvent extends YArrayEvent {
 | 
			
		||||
  constructor (ytext, remote, transaction) {
 | 
			
		||||
    super(ytext, remote, transaction)
 | 
			
		||||
    this._delta = null
 | 
			
		||||
  }
 | 
			
		||||
  // TODO: Should put this in a separate function. toDelta shouldn't be included
 | 
			
		||||
  //       in every Yjs distribution
 | 
			
		||||
  /**
 | 
			
		||||
   * Compute the changes in the delta format.
 | 
			
		||||
   *
 | 
			
		||||
   * @return {Delta} A {@link https://quilljs.com/docs/delta/|Quill Delta}) that
 | 
			
		||||
   *                 represents the changes on the document.
 | 
			
		||||
   *
 | 
			
		||||
   * @public
 | 
			
		||||
   */
 | 
			
		||||
  get delta () {
 | 
			
		||||
    if (this._delta === null) {
 | 
			
		||||
      const y = this.target._y
 | 
			
		||||
      y.transact(() => {
 | 
			
		||||
        let item = this.target._start
 | 
			
		||||
        const delta = []
 | 
			
		||||
        const added = this.addedElements
 | 
			
		||||
        const removed = this.removedElements
 | 
			
		||||
        this._delta = delta
 | 
			
		||||
        let action = null
 | 
			
		||||
        let attributes = {} // counts added or removed new attributes for retain
 | 
			
		||||
        const currentAttributes = new Map() // saves all current attributes for insert
 | 
			
		||||
        const oldAttributes = new Map()
 | 
			
		||||
        let insert = ''
 | 
			
		||||
        let retain = 0
 | 
			
		||||
        let deleteLen = 0
 | 
			
		||||
        const addOp = function addOp () {
 | 
			
		||||
          if (action !== null) {
 | 
			
		||||
            let op
 | 
			
		||||
            switch (action) {
 | 
			
		||||
              case 'delete':
 | 
			
		||||
                op = { delete: deleteLen }
 | 
			
		||||
                deleteLen = 0
 | 
			
		||||
                break
 | 
			
		||||
              case 'insert':
 | 
			
		||||
                op = { insert }
 | 
			
		||||
                if (currentAttributes.size > 0) {
 | 
			
		||||
                  op.attributes = {}
 | 
			
		||||
                  for (let [key, value] of currentAttributes) {
 | 
			
		||||
                    if (value !== null) {
 | 
			
		||||
                      op.attributes[key] = value
 | 
			
		||||
                    }
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
                insert = ''
 | 
			
		||||
                break
 | 
			
		||||
              case 'retain':
 | 
			
		||||
                op = { retain }
 | 
			
		||||
                if (Object.keys(attributes).length > 0) {
 | 
			
		||||
                  op.attributes = {}
 | 
			
		||||
                  for (let key in attributes) {
 | 
			
		||||
                    op.attributes[key] = attributes[key]
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
                retain = 0
 | 
			
		||||
                break
 | 
			
		||||
            }
 | 
			
		||||
            delta.push(op)
 | 
			
		||||
            action = null
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        while (item !== null) {
 | 
			
		||||
          switch (item.constructor) {
 | 
			
		||||
            case ItemEmbed:
 | 
			
		||||
              if (added.has(item)) {
 | 
			
		||||
                addOp()
 | 
			
		||||
                action = 'insert'
 | 
			
		||||
                insert = item.embed
 | 
			
		||||
                addOp()
 | 
			
		||||
              } else if (removed.has(item)) {
 | 
			
		||||
                if (action !== 'delete') {
 | 
			
		||||
                  addOp()
 | 
			
		||||
                  action = 'delete'
 | 
			
		||||
                }
 | 
			
		||||
                deleteLen += 1
 | 
			
		||||
              } else if (item._deleted === false) {
 | 
			
		||||
                if (action !== 'retain') {
 | 
			
		||||
                  addOp()
 | 
			
		||||
                  action = 'retain'
 | 
			
		||||
                }
 | 
			
		||||
                retain += 1
 | 
			
		||||
              }
 | 
			
		||||
              break
 | 
			
		||||
            case ItemString:
 | 
			
		||||
              if (added.has(item)) {
 | 
			
		||||
                if (action !== 'insert') {
 | 
			
		||||
                  addOp()
 | 
			
		||||
                  action = 'insert'
 | 
			
		||||
                }
 | 
			
		||||
                insert += item._content
 | 
			
		||||
              } else if (removed.has(item)) {
 | 
			
		||||
                if (action !== 'delete') {
 | 
			
		||||
                  addOp()
 | 
			
		||||
                  action = 'delete'
 | 
			
		||||
                }
 | 
			
		||||
                deleteLen += item._length
 | 
			
		||||
              } else if (item._deleted === false) {
 | 
			
		||||
                if (action !== 'retain') {
 | 
			
		||||
                  addOp()
 | 
			
		||||
                  action = 'retain'
 | 
			
		||||
                }
 | 
			
		||||
                retain += item._length
 | 
			
		||||
              }
 | 
			
		||||
              break
 | 
			
		||||
            case ItemFormat:
 | 
			
		||||
              if (added.has(item)) {
 | 
			
		||||
                const curVal = currentAttributes.get(item.key) || null
 | 
			
		||||
                if (curVal !== item.value) {
 | 
			
		||||
                  if (action === 'retain') {
 | 
			
		||||
                    addOp()
 | 
			
		||||
                  }
 | 
			
		||||
                  if (item.value === (oldAttributes.get(item.key) || null)) {
 | 
			
		||||
                    delete attributes[item.key]
 | 
			
		||||
                  } else {
 | 
			
		||||
                    attributes[item.key] = item.value
 | 
			
		||||
                  }
 | 
			
		||||
                } else {
 | 
			
		||||
                  item._delete(y)
 | 
			
		||||
                }
 | 
			
		||||
              } else if (removed.has(item)) {
 | 
			
		||||
                oldAttributes.set(item.key, item.value)
 | 
			
		||||
                const curVal = currentAttributes.get(item.key) || null
 | 
			
		||||
                if (curVal !== item.value) {
 | 
			
		||||
                  if (action === 'retain') {
 | 
			
		||||
                    addOp()
 | 
			
		||||
                  }
 | 
			
		||||
                  attributes[item.key] = curVal
 | 
			
		||||
                }
 | 
			
		||||
              } else if (item._deleted === false) {
 | 
			
		||||
                oldAttributes.set(item.key, item.value)
 | 
			
		||||
                const attr = attributes[item.key]
 | 
			
		||||
                if (attr !== undefined) {
 | 
			
		||||
                  if (attr !== item.value) {
 | 
			
		||||
                    if (action === 'retain') {
 | 
			
		||||
                      addOp()
 | 
			
		||||
                    }
 | 
			
		||||
                    if (item.value === null) {
 | 
			
		||||
                      attributes[item.key] = item.value
 | 
			
		||||
                    } else {
 | 
			
		||||
                      delete attributes[item.key]
 | 
			
		||||
                    }
 | 
			
		||||
                  } else {
 | 
			
		||||
                    item._delete(y)
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
              if (item._deleted === false) {
 | 
			
		||||
                if (action === 'insert') {
 | 
			
		||||
                  addOp()
 | 
			
		||||
                }
 | 
			
		||||
                updateCurrentAttributes(currentAttributes, item)
 | 
			
		||||
              }
 | 
			
		||||
              break
 | 
			
		||||
          }
 | 
			
		||||
          item = item._right
 | 
			
		||||
        }
 | 
			
		||||
        addOp()
 | 
			
		||||
        while (this._delta.length > 0) {
 | 
			
		||||
          let lastOp = this._delta[this._delta.length - 1]
 | 
			
		||||
          if (lastOp.retain !== undefined && lastOp.attributes === undefined) {
 | 
			
		||||
            // retain delta's if they don't assign attributes
 | 
			
		||||
            this._delta.pop()
 | 
			
		||||
          } else {
 | 
			
		||||
            break
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
    return this._delta
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Type that represents text with formatting information.
 | 
			
		||||
 *
 | 
			
		||||
 * This type replaces y-richtext as this implementation is able to handle
 | 
			
		||||
 * block formats (format information on a paragraph), embeds (complex elements
 | 
			
		||||
 * like pictures and videos), and text formats (**bold**, *italic*).
 | 
			
		||||
 *
 | 
			
		||||
 * @param {String} string The initial value of the YText.
 | 
			
		||||
 */
 | 
			
		||||
export default class YText extends YArray {
 | 
			
		||||
  constructor (string) {
 | 
			
		||||
    super()
 | 
			
		||||
    if (typeof string === 'string') {
 | 
			
		||||
      const start = new ItemString()
 | 
			
		||||
      start._parent = this
 | 
			
		||||
      start._content = string
 | 
			
		||||
      this._start = start
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @private
 | 
			
		||||
   * Creates YMap Event and calls observers.
 | 
			
		||||
   */
 | 
			
		||||
  _callObserver (transaction, parentSubs, remote) {
 | 
			
		||||
    this._callEventHandler(transaction, new YTextEvent(this, remote, transaction))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the unformatted string representation of this YText type.
 | 
			
		||||
   *
 | 
			
		||||
   * @public
 | 
			
		||||
   */
 | 
			
		||||
  toString () {
 | 
			
		||||
    let str = ''
 | 
			
		||||
    let n = this._start
 | 
			
		||||
    while (n !== null) {
 | 
			
		||||
      if (!n._deleted && n._countable) {
 | 
			
		||||
        str += n._content
 | 
			
		||||
      }
 | 
			
		||||
      n = n._right
 | 
			
		||||
    }
 | 
			
		||||
    return str
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Apply a {@link Delta} on this shared YText type.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Delta} delta The changes to apply on this element.
 | 
			
		||||
   *
 | 
			
		||||
   * @public
 | 
			
		||||
   */
 | 
			
		||||
  applyDelta (delta) {
 | 
			
		||||
    this._transact(y => {
 | 
			
		||||
      let left = null
 | 
			
		||||
      let right = this._start
 | 
			
		||||
      const currentAttributes = new Map()
 | 
			
		||||
      for (let i = 0; i < delta.length; i++) {
 | 
			
		||||
        let op = delta[i]
 | 
			
		||||
        if (op.insert !== undefined) {
 | 
			
		||||
          ;[left, right] = insertText(y, op.insert, this, left, right, currentAttributes, op.attributes || {})
 | 
			
		||||
        } else if (op.retain !== undefined) {
 | 
			
		||||
          ;[left, right] = formatText(y, op.retain, this, left, right, currentAttributes, op.attributes || {})
 | 
			
		||||
        } else if (op.delete !== undefined) {
 | 
			
		||||
          ;[left, right] = deleteText(y, op.delete, this, left, right, currentAttributes)
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the Delta representation of this YText type.
 | 
			
		||||
   *
 | 
			
		||||
   * @return {Delta} The Delta representation of this type.
 | 
			
		||||
   *
 | 
			
		||||
   * @public
 | 
			
		||||
   */
 | 
			
		||||
  toDelta () {
 | 
			
		||||
    let ops = []
 | 
			
		||||
    let currentAttributes = new Map()
 | 
			
		||||
    let str = ''
 | 
			
		||||
    let n = this._start
 | 
			
		||||
    function packStr () {
 | 
			
		||||
      if (str.length > 0) {
 | 
			
		||||
        // pack str with attributes to ops
 | 
			
		||||
        let attributes = {}
 | 
			
		||||
        let addAttributes = false
 | 
			
		||||
        for (let [key, value] of currentAttributes) {
 | 
			
		||||
          addAttributes = true
 | 
			
		||||
          attributes[key] = value
 | 
			
		||||
        }
 | 
			
		||||
        let op = { insert: str }
 | 
			
		||||
        if (addAttributes) {
 | 
			
		||||
          op.attributes = attributes
 | 
			
		||||
        }
 | 
			
		||||
        ops.push(op)
 | 
			
		||||
        str = ''
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    while (n !== null) {
 | 
			
		||||
      if (!n._deleted) {
 | 
			
		||||
        switch (n.constructor) {
 | 
			
		||||
          case ItemString:
 | 
			
		||||
            str += n._content
 | 
			
		||||
            break
 | 
			
		||||
          case ItemFormat:
 | 
			
		||||
            packStr()
 | 
			
		||||
            updateCurrentAttributes(currentAttributes, n)
 | 
			
		||||
            break
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      n = n._right
 | 
			
		||||
    }
 | 
			
		||||
    packStr()
 | 
			
		||||
    return ops
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Insert text at a given index.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Integer} index The index at which to start inserting.
 | 
			
		||||
   * @param {String} text The text to insert at the specified position.
 | 
			
		||||
   * @param {TextAttributes} attributes Optionally define some formatting
 | 
			
		||||
   *                                    information to apply on the inserted
 | 
			
		||||
   *                                    Text.
 | 
			
		||||
   *
 | 
			
		||||
   * @public
 | 
			
		||||
   */
 | 
			
		||||
  insert (index, text, attributes = {}) {
 | 
			
		||||
    if (text.length <= 0) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    this._transact(y => {
 | 
			
		||||
      let [left, right, currentAttributes] = findPosition(this, index)
 | 
			
		||||
      insertText(y, text, this, left, right, currentAttributes, attributes)
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Inserts an embed at a index.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Integer} index The index to insert the embed at.
 | 
			
		||||
   * @param {Object} embed The Object that represents the embed.
 | 
			
		||||
   * @param {TextAttributes} attributes Attribute information to apply on the
 | 
			
		||||
   *                                    embed
 | 
			
		||||
   *
 | 
			
		||||
   * @public
 | 
			
		||||
   */
 | 
			
		||||
  insertEmbed (index, embed, attributes = {}) {
 | 
			
		||||
    if (embed.constructor !== Object) {
 | 
			
		||||
      throw new Error('Embed must be an Object')
 | 
			
		||||
    }
 | 
			
		||||
    this._transact(y => {
 | 
			
		||||
      let [left, right, currentAttributes] = findPosition(this, index)
 | 
			
		||||
      insertText(y, embed, this, left, right, currentAttributes, attributes)
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Deletes text starting from an index.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Integer} index Index at which to start deleting.
 | 
			
		||||
   * @param {Integer} length The number of characters to remove. Defaults to 1.
 | 
			
		||||
   *
 | 
			
		||||
   * @public
 | 
			
		||||
   */
 | 
			
		||||
  delete (index, length) {
 | 
			
		||||
    if (length === 0) {
 | 
			
		||||
      return
 | 
			
		||||
    }
 | 
			
		||||
    this._transact(y => {
 | 
			
		||||
      let [left, right, currentAttributes] = findPosition(this, index)
 | 
			
		||||
      deleteText(y, length, this, left, right, currentAttributes)
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Assigns properties to a range of text.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Integer} index The position where to start formatting.
 | 
			
		||||
   * @param {Integer} length The amount of characters to assign properties to.
 | 
			
		||||
   * @param {TextAttributes} attributes Attribute information to apply on the
 | 
			
		||||
   *                                    text.
 | 
			
		||||
   *
 | 
			
		||||
   * @public
 | 
			
		||||
   */
 | 
			
		||||
  format (index, length, attributes) {
 | 
			
		||||
    this._transact(y => {
 | 
			
		||||
      let [left, right, currentAttributes] = findPosition(this, index)
 | 
			
		||||
      if (right === null) {
 | 
			
		||||
        return
 | 
			
		||||
      }
 | 
			
		||||
      formatText(y, length, this, left, right, currentAttributes, attributes)
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
  // TODO: De-duplicate code. The following code is in every type.
 | 
			
		||||
  /**
 | 
			
		||||
   * Transform this YText to a readable format.
 | 
			
		||||
   * Useful for logging as all Items implement this method.
 | 
			
		||||
   *
 | 
			
		||||
   * @private
 | 
			
		||||
   */
 | 
			
		||||
  _logString () {
 | 
			
		||||
    return logItemHelper('YText', this)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										190
									
								
								src/Types/YXml/YXmlElement.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										190
									
								
								src/Types/YXml/YXmlElement.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,190 @@
 | 
			
		||||
import YMap from '../YMap/YMap.mjs'
 | 
			
		||||
import YXmlFragment from './YXmlFragment.mjs'
 | 
			
		||||
import { createAssociation } from '../../Bindings/DomBinding/util.mjs'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * An YXmlElement imitates the behavior of a
 | 
			
		||||
 * {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}.
 | 
			
		||||
 *
 | 
			
		||||
 * * An YXmlElement has attributes (key value pairs)
 | 
			
		||||
 * * An YXmlElement has childElements that must inherit from YXmlElement
 | 
			
		||||
 *
 | 
			
		||||
 * @param {String} nodeName Node name
 | 
			
		||||
 */
 | 
			
		||||
export default class YXmlElement extends YXmlFragment {
 | 
			
		||||
  constructor (nodeName = 'UNDEFINED') {
 | 
			
		||||
    super()
 | 
			
		||||
    this.nodeName = nodeName.toUpperCase()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @private
 | 
			
		||||
   * Creates an Item with the same effect as this Item (without position effect)
 | 
			
		||||
   */
 | 
			
		||||
  _copy () {
 | 
			
		||||
    let struct = super._copy()
 | 
			
		||||
    struct.nodeName = this.nodeName
 | 
			
		||||
    return struct
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @private
 | 
			
		||||
   * Read the next Item in a Decoder and fill this Item with the read data.
 | 
			
		||||
   *
 | 
			
		||||
   * This is called when data is received from a remote peer.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Y} y The Yjs instance that this Item belongs to.
 | 
			
		||||
   * @param {BinaryDecoder} decoder The decoder object to read data from.
 | 
			
		||||
   */
 | 
			
		||||
  _fromBinary (y, decoder) {
 | 
			
		||||
    const missing = super._fromBinary(y, decoder)
 | 
			
		||||
    this.nodeName = decoder.readVarString()
 | 
			
		||||
    return missing
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Transform the properties of this type to binary and write it to an
 | 
			
		||||
   * BinaryEncoder.
 | 
			
		||||
   *
 | 
			
		||||
   * This is called when this Item is sent to a remote peer.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {BinaryEncoder} encoder The encoder to write data to.
 | 
			
		||||
   *
 | 
			
		||||
   * @private
 | 
			
		||||
   */
 | 
			
		||||
  _toBinary (encoder) {
 | 
			
		||||
    super._toBinary(encoder)
 | 
			
		||||
    encoder.writeVarString(this.nodeName)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Integrates this Item into the shared structure.
 | 
			
		||||
   *
 | 
			
		||||
   * This method actually applies the change to the Yjs instance. In case of
 | 
			
		||||
   * Item it connects _left and _right to this Item and calls the
 | 
			
		||||
   * {@link Item#beforeChange} method.
 | 
			
		||||
   *
 | 
			
		||||
   * * Checks for nodeName
 | 
			
		||||
   * * Sets domFilter
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Y} y The Yjs instance
 | 
			
		||||
   *
 | 
			
		||||
   * @private
 | 
			
		||||
   */
 | 
			
		||||
  _integrate (y) {
 | 
			
		||||
    if (this.nodeName === null) {
 | 
			
		||||
      throw new Error('nodeName must be defined!')
 | 
			
		||||
    }
 | 
			
		||||
    super._integrate(y)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the string representation of this YXmlElement.
 | 
			
		||||
   * The attributes are ordered by attribute-name, so you can easily use this
 | 
			
		||||
   * method to compare YXmlElements
 | 
			
		||||
   *
 | 
			
		||||
   * @return {String} The string representation of this type.
 | 
			
		||||
   *
 | 
			
		||||
   * @public
 | 
			
		||||
   */
 | 
			
		||||
  toString () {
 | 
			
		||||
    const attrs = this.getAttributes()
 | 
			
		||||
    const stringBuilder = []
 | 
			
		||||
    const keys = []
 | 
			
		||||
    for (let key in attrs) {
 | 
			
		||||
      keys.push(key)
 | 
			
		||||
    }
 | 
			
		||||
    keys.sort()
 | 
			
		||||
    const keysLen = keys.length
 | 
			
		||||
    for (let i = 0; i < keysLen; i++) {
 | 
			
		||||
      const key = keys[i]
 | 
			
		||||
      stringBuilder.push(key + '="' + attrs[key] + '"')
 | 
			
		||||
    }
 | 
			
		||||
    const nodeName = this.nodeName.toLocaleLowerCase()
 | 
			
		||||
    const attrsString = stringBuilder.length > 0 ? ' ' + stringBuilder.join(' ') : ''
 | 
			
		||||
    return `<${nodeName}${attrsString}>${super.toString()}</${nodeName}>`
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Removes an attribute from this YXmlElement.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {String} attributeName The attribute name that is to be removed.
 | 
			
		||||
   *
 | 
			
		||||
   * @public
 | 
			
		||||
   */
 | 
			
		||||
  removeAttribute (attributeName) {
 | 
			
		||||
    return YMap.prototype.delete.call(this, attributeName)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Sets or updates an attribute.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {String} attributeName The attribute name that is to be set.
 | 
			
		||||
   * @param {String} attributeValue The attribute value that is to be set.
 | 
			
		||||
   *
 | 
			
		||||
   * @public
 | 
			
		||||
   */
 | 
			
		||||
  setAttribute (attributeName, attributeValue) {
 | 
			
		||||
    return YMap.prototype.set.call(this, attributeName, attributeValue)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns an attribute value that belongs to the attribute name.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {String} attributeName The attribute name that identifies the
 | 
			
		||||
   *                               queried value.
 | 
			
		||||
   * @return {String} The queried attribute value.
 | 
			
		||||
   *
 | 
			
		||||
   * @public
 | 
			
		||||
   */
 | 
			
		||||
  getAttribute (attributeName) {
 | 
			
		||||
    return YMap.prototype.get.call(this, attributeName)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns all attribute name/value pairs in a JSON Object.
 | 
			
		||||
   *
 | 
			
		||||
   * @return {Object} A JSON Object that describes the attributes.
 | 
			
		||||
   *
 | 
			
		||||
   * @public
 | 
			
		||||
   */
 | 
			
		||||
  getAttributes () {
 | 
			
		||||
    const obj = {}
 | 
			
		||||
    for (let [key, value] of this._map) {
 | 
			
		||||
      if (!value._deleted) {
 | 
			
		||||
        obj[key] = value._content[0]
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return obj
 | 
			
		||||
  }
 | 
			
		||||
  // TODO: outsource the binding property.
 | 
			
		||||
  /**
 | 
			
		||||
   * Creates a Dom Element that mirrors this YXmlElement.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Document} [_document=document] The document object (you must define
 | 
			
		||||
   *                                        this when calling this method in
 | 
			
		||||
   *                                        nodejs)
 | 
			
		||||
   * @param {Object<key:hookDefinition>} [hooks={}] Optional property to customize how hooks
 | 
			
		||||
   *                                             are presented in the DOM
 | 
			
		||||
   * @param {DomBinding} [binding] You should not set this property. This is
 | 
			
		||||
   *                               used if DomBinding wants to create a
 | 
			
		||||
   *                               association to the created DOM type.
 | 
			
		||||
   * @return {Element} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
 | 
			
		||||
   *
 | 
			
		||||
   * @public
 | 
			
		||||
   */
 | 
			
		||||
  toDom (_document = document, hooks = {}, binding) {
 | 
			
		||||
    const dom = _document.createElement(this.nodeName)
 | 
			
		||||
    let attrs = this.getAttributes()
 | 
			
		||||
    for (let key in attrs) {
 | 
			
		||||
      dom.setAttribute(key, attrs[key])
 | 
			
		||||
    }
 | 
			
		||||
    this.forEach(yxml => {
 | 
			
		||||
      dom.appendChild(yxml.toDom(_document, hooks, binding))
 | 
			
		||||
    })
 | 
			
		||||
    createAssociation(binding, dom, this)
 | 
			
		||||
    return dom
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
YXmlFragment._YXmlElement = YXmlElement
 | 
			
		||||
							
								
								
									
										47
									
								
								src/Types/YXml/YXmlEvent.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/Types/YXml/YXmlEvent.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,47 @@
 | 
			
		||||
import YEvent from '../../Util/YEvent.mjs'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * An Event that describes changes on a YXml Element or Yxml Fragment
 | 
			
		||||
 *
 | 
			
		||||
 * @protected
 | 
			
		||||
 */
 | 
			
		||||
export default class YXmlEvent extends YEvent {
 | 
			
		||||
  /**
 | 
			
		||||
   * @param {YType} target The target on which the event is created.
 | 
			
		||||
   * @param {Set} subs The set of changed attributes. `null` is included if the
 | 
			
		||||
   *                   child list changed.
 | 
			
		||||
   * @param {Boolean} remote Whether this change was created by a remote peer.
 | 
			
		||||
   * @param {Transaction} transaction The transaction instance with wich the
 | 
			
		||||
   *                                  change was created.
 | 
			
		||||
   */
 | 
			
		||||
  constructor (target, subs, remote, transaction) {
 | 
			
		||||
    super(target)
 | 
			
		||||
    /**
 | 
			
		||||
     * The transaction instance for the computed change.
 | 
			
		||||
     * @type {Transaction}
 | 
			
		||||
     */
 | 
			
		||||
    this._transaction = transaction
 | 
			
		||||
    /**
 | 
			
		||||
     * Whether the children changed.
 | 
			
		||||
     * @type {Boolean}
 | 
			
		||||
     */
 | 
			
		||||
    this.childListChanged = false
 | 
			
		||||
    /**
 | 
			
		||||
     * Set of all changed attributes.
 | 
			
		||||
     * @type {Set}
 | 
			
		||||
     */
 | 
			
		||||
    this.attributesChanged = new Set()
 | 
			
		||||
    /**
 | 
			
		||||
     * Whether this change was created by a remote peer.
 | 
			
		||||
     * @type {Boolean}
 | 
			
		||||
     */
 | 
			
		||||
    this.remote = remote
 | 
			
		||||
    subs.forEach((sub) => {
 | 
			
		||||
      if (sub === null) {
 | 
			
		||||
        this.childListChanged = true
 | 
			
		||||
      } else {
 | 
			
		||||
        this.attributesChanged.add(sub)
 | 
			
		||||
      }
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										167
									
								
								src/Types/YXml/YXmlFragment.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										167
									
								
								src/Types/YXml/YXmlFragment.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,167 @@
 | 
			
		||||
import { createAssociation } from '../../Bindings/DomBinding/util.mjs'
 | 
			
		||||
import YXmlTreeWalker from './YXmlTreeWalker.mjs'
 | 
			
		||||
 | 
			
		||||
import YArray from '../YArray/YArray.mjs'
 | 
			
		||||
import YXmlEvent from './YXmlEvent.mjs'
 | 
			
		||||
import { logItemHelper } from '../../MessageHandler/messageToString.mjs'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Dom filter function.
 | 
			
		||||
 *
 | 
			
		||||
 * @callback domFilter
 | 
			
		||||
 * @param {string} nodeName The nodeName of the element
 | 
			
		||||
 * @param {Map} attributes The map of attributes.
 | 
			
		||||
 * @return {boolean} Whether to include the Dom node in the YXmlElement.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Define the elements to which a set of CSS queries apply.
 | 
			
		||||
 * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors|CSS_Selectors}
 | 
			
		||||
 *
 | 
			
		||||
 * @example
 | 
			
		||||
 *   query = '.classSelector'
 | 
			
		||||
 *   query = 'nodeSelector'
 | 
			
		||||
 *   query = '#idSelector'
 | 
			
		||||
 *
 | 
			
		||||
 * @typedef {string} CSS_Selector
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Represents a list of {@link YXmlElement}.and {@link YXmlText} types.
 | 
			
		||||
 * A YxmlFragment is similar to a {@link YXmlElement}, but it does not have a
 | 
			
		||||
 * nodeName and it does not have attributes. Though it can be bound to a DOM
 | 
			
		||||
 * element - in this case the attributes and the nodeName are not shared.
 | 
			
		||||
 *
 | 
			
		||||
 * @public
 | 
			
		||||
 */
 | 
			
		||||
export default class YXmlFragment extends YArray {
 | 
			
		||||
  /**
 | 
			
		||||
   * Create a subtree of childNodes.
 | 
			
		||||
   *
 | 
			
		||||
   * @example
 | 
			
		||||
   * const walker = elem.createTreeWalker(dom => dom.nodeName === 'div')
 | 
			
		||||
   * for (let node in walker) {
 | 
			
		||||
   *   // `node` is a div node
 | 
			
		||||
   *   nop(node)
 | 
			
		||||
   * }
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Function} filter Function that is called on each child element and
 | 
			
		||||
   *                          returns a Boolean indicating whether the child
 | 
			
		||||
   *                          is to be included in the subtree.
 | 
			
		||||
   * @return {TreeWalker} A subtree and a position within it.
 | 
			
		||||
   *
 | 
			
		||||
   * @public
 | 
			
		||||
   */
 | 
			
		||||
  createTreeWalker (filter) {
 | 
			
		||||
    return new YXmlTreeWalker(this, filter)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns the first YXmlElement that matches the query.
 | 
			
		||||
   * Similar to DOM's {@link querySelector}.
 | 
			
		||||
   *
 | 
			
		||||
   * Query support:
 | 
			
		||||
   *   - tagname
 | 
			
		||||
   * TODO:
 | 
			
		||||
   *   - id
 | 
			
		||||
   *   - attribute
 | 
			
		||||
   *
 | 
			
		||||
   * @param {CSS_Selector} query The query on the children.
 | 
			
		||||
   * @return {?YXmlElement} The first element that matches the query or null.
 | 
			
		||||
   *
 | 
			
		||||
   * @public
 | 
			
		||||
   */
 | 
			
		||||
  querySelector (query) {
 | 
			
		||||
    query = query.toUpperCase()
 | 
			
		||||
    const iterator = new YXmlTreeWalker(this, element => element.nodeName === query)
 | 
			
		||||
    const next = iterator.next()
 | 
			
		||||
    if (next.done) {
 | 
			
		||||
      return null
 | 
			
		||||
    } else {
 | 
			
		||||
      return next.value
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Returns all YXmlElements that match the query.
 | 
			
		||||
   * Similar to Dom's {@link querySelectorAll}.
 | 
			
		||||
   *
 | 
			
		||||
   * TODO: Does not yet support all queries. Currently only query by tagName.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {CSS_Selector} query The query on the children
 | 
			
		||||
   * @return {Array<YXmlElement>} The elements that match this query.
 | 
			
		||||
   *
 | 
			
		||||
   * @public
 | 
			
		||||
   */
 | 
			
		||||
  querySelectorAll (query) {
 | 
			
		||||
    query = query.toUpperCase()
 | 
			
		||||
    return Array.from(new YXmlTreeWalker(this, element => element.nodeName === query))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Creates YArray Event and calls observers.
 | 
			
		||||
   *
 | 
			
		||||
   * @private
 | 
			
		||||
   */
 | 
			
		||||
  _callObserver (transaction, parentSubs, remote) {
 | 
			
		||||
    this._callEventHandler(transaction, new YXmlEvent(this, parentSubs, remote, transaction))
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Get the string representation of all the children of this YXmlFragment.
 | 
			
		||||
   *
 | 
			
		||||
   * @return {string} The string representation of all children.
 | 
			
		||||
   */
 | 
			
		||||
  toString () {
 | 
			
		||||
    return this.map(xml => xml.toString()).join('')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * @private
 | 
			
		||||
   * Unbind from Dom and mark this Item as deleted.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Y} y The Yjs instance
 | 
			
		||||
   * @param {boolean} createDelete Whether to propagate a message that this
 | 
			
		||||
   *                               Type was deleted.
 | 
			
		||||
   * @param {boolean} [gcChildren=y._hasUndoManager===false] Whether to garbage
 | 
			
		||||
   *                                         collect the children of this type.
 | 
			
		||||
   *
 | 
			
		||||
   * @private
 | 
			
		||||
   */
 | 
			
		||||
  _delete (y, createDelete, gcChildren) {
 | 
			
		||||
    super._delete(y, createDelete, gcChildren)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Creates a Dom Element that mirrors this YXmlElement.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Document} [_document=document] The document object (you must define
 | 
			
		||||
   *                                        this when calling this method in
 | 
			
		||||
   *                                        nodejs)
 | 
			
		||||
   * @param {Object<key:hookDefinition>} [hooks={}] Optional property to customize how hooks
 | 
			
		||||
   *                                             are presented in the DOM
 | 
			
		||||
   * @param {DomBinding} [binding] You should not set this property. This is
 | 
			
		||||
   *                               used if DomBinding wants to create a
 | 
			
		||||
   *                               association to the created DOM type
 | 
			
		||||
   * @return {Element} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
 | 
			
		||||
   *
 | 
			
		||||
   * @public
 | 
			
		||||
   */
 | 
			
		||||
  toDom (_document = document, hooks = {}, binding) {
 | 
			
		||||
    const fragment = _document.createDocumentFragment()
 | 
			
		||||
    createAssociation(binding, fragment, this)
 | 
			
		||||
    this.forEach(xmlType => {
 | 
			
		||||
      fragment.insertBefore(xmlType.toDom(_document, hooks, binding), null)
 | 
			
		||||
    })
 | 
			
		||||
    return fragment
 | 
			
		||||
  }
 | 
			
		||||
  /**
 | 
			
		||||
   * Transform this YXml Type to a readable format.
 | 
			
		||||
   * Useful for logging as all Items and Delete implement this method.
 | 
			
		||||
   *
 | 
			
		||||
   * @private
 | 
			
		||||
   */
 | 
			
		||||
  _logString () {
 | 
			
		||||
    return logItemHelper('YXml', this)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										108
									
								
								src/Types/YXml/YXmlHook.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								src/Types/YXml/YXmlHook.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,108 @@
 | 
			
		||||
import YMap from '../YMap/YMap.mjs'
 | 
			
		||||
import { createAssociation } from '../../Bindings/DomBinding/util.mjs'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * You can manage binding to a custom type with YXmlHook.
 | 
			
		||||
 *
 | 
			
		||||
 * @public
 | 
			
		||||
 */
 | 
			
		||||
export default class YXmlHook extends YMap {
 | 
			
		||||
  /**
 | 
			
		||||
   * @param {String} hookName nodeName of the Dom Node.
 | 
			
		||||
   */
 | 
			
		||||
  constructor (hookName) {
 | 
			
		||||
    super()
 | 
			
		||||
    this.hookName = null
 | 
			
		||||
    if (hookName !== undefined) {
 | 
			
		||||
      this.hookName = hookName
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Creates an Item with the same effect as this Item (without position effect)
 | 
			
		||||
   *
 | 
			
		||||
   * @private
 | 
			
		||||
   */
 | 
			
		||||
  _copy () {
 | 
			
		||||
    const struct = super._copy()
 | 
			
		||||
    struct.hookName = this.hookName
 | 
			
		||||
    return struct
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Creates a Dom Element that mirrors this YXmlElement.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Document} [_document=document] The document object (you must define
 | 
			
		||||
   *                                        this when calling this method in
 | 
			
		||||
   *                                        nodejs)
 | 
			
		||||
   * @param {Object<key:hookDefinition>} [hooks] Optional property to customize how hooks
 | 
			
		||||
   *                                             are presented in the DOM
 | 
			
		||||
   * @param {DomBinding} [binding] You should not set this property. This is
 | 
			
		||||
   *                               used if DomBinding wants to create a
 | 
			
		||||
   *                               association to the created DOM type
 | 
			
		||||
   * @return {Element} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
 | 
			
		||||
   *
 | 
			
		||||
   * @public
 | 
			
		||||
   */
 | 
			
		||||
  toDom (_document = document, hooks = {}, binding) {
 | 
			
		||||
    const hook = hooks[this.hookName]
 | 
			
		||||
    let dom
 | 
			
		||||
    if (hook !== undefined) {
 | 
			
		||||
      dom = hook.createDom(this)
 | 
			
		||||
    } else {
 | 
			
		||||
      dom = document.createElement(this.hookName)
 | 
			
		||||
    }
 | 
			
		||||
    dom.setAttribute('data-yjs-hook', this.hookName)
 | 
			
		||||
    createAssociation(binding, dom, this)
 | 
			
		||||
    return dom
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Read the next Item in a Decoder and fill this Item with the read data.
 | 
			
		||||
   *
 | 
			
		||||
   * This is called when data is received from a remote peer.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Y} y The Yjs instance that this Item belongs to.
 | 
			
		||||
   * @param {BinaryDecoder} decoder The decoder object to read data from.
 | 
			
		||||
   *
 | 
			
		||||
   * @private
 | 
			
		||||
   */
 | 
			
		||||
  _fromBinary (y, decoder) {
 | 
			
		||||
    const missing = super._fromBinary(y, decoder)
 | 
			
		||||
    this.hookName = decoder.readVarString()
 | 
			
		||||
    return missing
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Transform the properties of this type to binary and write it to an
 | 
			
		||||
   * BinaryEncoder.
 | 
			
		||||
   *
 | 
			
		||||
   * This is called when this Item is sent to a remote peer.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {BinaryEncoder} encoder The encoder to write data to.
 | 
			
		||||
   *
 | 
			
		||||
   * @private
 | 
			
		||||
   */
 | 
			
		||||
  _toBinary (encoder) {
 | 
			
		||||
    super._toBinary(encoder)
 | 
			
		||||
    encoder.writeVarString(this.hookName)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Integrate this type into the Yjs instance.
 | 
			
		||||
   *
 | 
			
		||||
   * * Save this struct in the os
 | 
			
		||||
   * * This type is sent to other client
 | 
			
		||||
   * * Observer functions are fired
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Y} y The Yjs instance
 | 
			
		||||
   *
 | 
			
		||||
   * @private
 | 
			
		||||
   */
 | 
			
		||||
  _integrate (y) {
 | 
			
		||||
    if (this.hookName === null) {
 | 
			
		||||
      throw new Error('hookName must be defined!')
 | 
			
		||||
    }
 | 
			
		||||
    super._integrate(y)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										46
									
								
								src/Types/YXml/YXmlText.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								src/Types/YXml/YXmlText.mjs
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,46 @@
 | 
			
		||||
import YText from '../YText/YText.mjs'
 | 
			
		||||
import { createAssociation } from '../../Bindings/DomBinding/util.mjs'
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Represents text in a Dom Element. In the future this type will also handle
 | 
			
		||||
 * simple formatting information like bold and italic.
 | 
			
		||||
 *
 | 
			
		||||
 * @param {String} arg1 Initial value.
 | 
			
		||||
 */
 | 
			
		||||
export default class YXmlText extends YText {
 | 
			
		||||
  /**
 | 
			
		||||
   * Creates a Dom Element that mirrors this YXmlText.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Document} [_document=document] The document object (you must define
 | 
			
		||||
   *                                        this when calling this method in
 | 
			
		||||
   *                                        nodejs)
 | 
			
		||||
   * @param {Object<key:hookDefinition>} [hooks] Optional property to customize how hooks
 | 
			
		||||
   *                                             are presented in the DOM
 | 
			
		||||
   * @param {DomBinding} [binding] You should not set this property. This is
 | 
			
		||||
   *                               used if DomBinding wants to create a
 | 
			
		||||
   *                               association to the created DOM type.
 | 
			
		||||
   * @return {Element} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
 | 
			
		||||
   *
 | 
			
		||||
   * @public
 | 
			
		||||
   */
 | 
			
		||||
  toDom (_document = document, hooks, binding) {
 | 
			
		||||
    const dom = _document.createTextNode(this.toString())
 | 
			
		||||
    createAssociation(binding, dom, this)
 | 
			
		||||
    return dom
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Mark this Item as deleted.
 | 
			
		||||
   *
 | 
			
		||||
   * @param {Y} y The Yjs instance
 | 
			
		||||
   * @param {boolean} createDelete Whether to propagate a message that this
 | 
			
		||||
   *                               Type was deleted.
 | 
			
		||||
   * @param {boolean} [gcChildren=y._hasUndoManager===false] Whether to garbage
 | 
			
		||||
   *                                         collect the children of this type.
 | 
			
		||||
   *
 | 
			
		||||
   * @private
 | 
			
		||||
   */
 | 
			
		||||
  _delete (y, createDelete, gcChildren) {
 | 
			
		||||
    super._delete(y, createDelete, gcChildren)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user