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
 | 
					node_modules
 | 
				
			||||||
dist
 | 
					bower_components
 | 
				
			||||||
.vscode
 | 
					 | 
				
			||||||
docs
 | 
					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)
 | 
					The MIT License (MIT)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Copyright (c) 2023
 | 
					Copyright (c) 2014
 | 
				
			||||||
  - Kevin Jahns <kevin.jahns@protonmail.com>.
 | 
					  - Kevin Jahns <kevin.jahns@rwth-aachen.de>.
 | 
				
			||||||
  - Chair of Computer Science 5 (Databases & Information Systems), RWTH Aachen University, Germany
 | 
					  - 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
 | 
					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
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										11017
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										11017
									
								
								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",
 | 
					  "name": "yjs",
 | 
				
			||||||
  "version": "13.6.24",
 | 
					  "version": "13.0.0-60",
 | 
				
			||||||
  "description": "Shared Editing Library",
 | 
					  "description": "A framework for real-time p2p shared editing on any data",
 | 
				
			||||||
  "main": "./dist/yjs.cjs",
 | 
					  "main": "./y.node.js",
 | 
				
			||||||
  "module": "./dist/yjs.mjs",
 | 
					  "browser": "./y.js",
 | 
				
			||||||
  "types": "./dist/src/index.d.ts",
 | 
					  "module": "./src/y.js",
 | 
				
			||||||
  "type": "module",
 | 
					 | 
				
			||||||
  "sideEffects": false,
 | 
					 | 
				
			||||||
  "funding": {
 | 
					 | 
				
			||||||
    "type": "GitHub Sponsors ❤",
 | 
					 | 
				
			||||||
    "url": "https://github.com/sponsors/dmonad"
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  "scripts": {
 | 
					  "scripts": {
 | 
				
			||||||
    "clean": "rm -rf dist docs",
 | 
					    "start": "node --experimental-modules src/Connectors/WebsocketsConnector/server.mjs",
 | 
				
			||||||
    "test": "npm run dist && NODE_ENV=development node ./dist/tests.cjs --repetition-time 50",
 | 
					    "test": "npm run lint",
 | 
				
			||||||
    "test-extensive": "npm run lint && npm run dist && node ./dist/tests.cjs --production --repetition-time 10000",
 | 
					    "debug": "concurrently 'rollup -wc rollup.test.js' 'cutest-serve y.test.js -o'",
 | 
				
			||||||
    "dist": "npm run clean && rollup -c && tsc",
 | 
					    "lint": "standard src/**/*.mjs test/**/*.mjs tests-lib/**/*.mjs",
 | 
				
			||||||
    "watch": "rollup -wc",
 | 
					    "docs": "esdoc",
 | 
				
			||||||
    "lint": "markdownlint README.md && standard && tsc",
 | 
					    "serve-docs": "npm run docs && serve ./docs/",
 | 
				
			||||||
    "docs": "rm -rf docs; jsdoc --configure ./.jsdoc.json --verbose --readme ./README.md --package ./package.json || true",
 | 
					    "dist": "rollup -c rollup.browser.js; rollup -c rollup.node.js",
 | 
				
			||||||
    "serve-docs": "npm run docs && http-server ./docs/",
 | 
					    "watch": "concurrently 'rollup -wc rollup.browser.js' 'rollup -wc rollup.node.js'",
 | 
				
			||||||
    "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",
 | 
					    "postversion": "npm run dist",
 | 
				
			||||||
    "debug": "concurrently 'http-server -o test.html' 'npm run watch'",
 | 
					    "postpublish": "tag-dist-files --overwrite-existing-tag",
 | 
				
			||||||
    "trace-deopt": "clear && rollup -c  && node --trace-deopt dist/test.cjs",
 | 
					    "demos": "concurrently 'node --experimental-modules src/Connectors/WebsocketsConnector/server.mjs' 'http-server'"
 | 
				
			||||||
    "trace-opt": "clear && rollup -c  && node --trace-opt dist/test.cjs"
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "exports": {
 | 
					  "now": {
 | 
				
			||||||
    ".": {
 | 
					    "engines": {
 | 
				
			||||||
      "types": "./dist/src/index.d.ts",
 | 
					      "node": "10.x.x"
 | 
				
			||||||
      "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"
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "files": [
 | 
					  "files": [
 | 
				
			||||||
    "dist/yjs.*",
 | 
					    "y.*",
 | 
				
			||||||
    "dist/src",
 | 
					    "src/*",
 | 
				
			||||||
    "src",
 | 
					    ".esdoc.json",
 | 
				
			||||||
    "tests/testHelper.js",
 | 
					    "docs/*"
 | 
				
			||||||
    "dist/testHelper.mjs",
 | 
					 | 
				
			||||||
    "sponsor-y.js"
 | 
					 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
  "dictionaries": {
 | 
					 | 
				
			||||||
    "test": "tests"
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  "standard": {
 | 
					  "standard": {
 | 
				
			||||||
    "ignore": [
 | 
					    "ignore": [
 | 
				
			||||||
      "/dist",
 | 
					      "/y.js",
 | 
				
			||||||
      "/node_modules",
 | 
					      "/y.js.map"
 | 
				
			||||||
      "/docs"
 | 
					 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "repository": {
 | 
					  "repository": {
 | 
				
			||||||
    "type": "git",
 | 
					    "type": "git",
 | 
				
			||||||
    "url": "https://github.com/yjs/yjs.git"
 | 
					    "url": "https://github.com/y-js/yjs.git"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "keywords": [
 | 
					  "keywords": [
 | 
				
			||||||
    "Yjs",
 | 
					    "Yjs",
 | 
				
			||||||
    "CRDT",
 | 
					    "OT",
 | 
				
			||||||
    "offline",
 | 
					    "Collaboration",
 | 
				
			||||||
    "offline-first",
 | 
					    "Synchronization",
 | 
				
			||||||
    "shared-editing",
 | 
					    "ShareJS",
 | 
				
			||||||
    "concurrency",
 | 
					    "Coweb",
 | 
				
			||||||
    "collaboration"
 | 
					    "Concurrency"
 | 
				
			||||||
  ],
 | 
					  ],
 | 
				
			||||||
  "author": "Kevin Jahns",
 | 
					  "author": "Kevin Jahns",
 | 
				
			||||||
  "email": "kevin.jahns@protonmail.com",
 | 
					  "email": "kevin.jahns@rwth-aachen.de",
 | 
				
			||||||
  "license": "MIT",
 | 
					  "license": "MIT",
 | 
				
			||||||
  "bugs": {
 | 
					  "bugs": {
 | 
				
			||||||
    "url": "https://github.com/yjs/yjs/issues"
 | 
					    "url": "https://github.com/y-js/yjs/issues"
 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  "homepage": "https://docs.yjs.dev",
 | 
					 | 
				
			||||||
  "dependencies": {
 | 
					 | 
				
			||||||
    "lib0": "^0.2.99"
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					  "homepage": "http://y-js.org",
 | 
				
			||||||
  "devDependencies": {
 | 
					  "devDependencies": {
 | 
				
			||||||
    "@rollup/plugin-commonjs": "^24.0.1",
 | 
					    "babel-cli": "^6.24.1",
 | 
				
			||||||
    "@rollup/plugin-node-resolve": "^15.0.1",
 | 
					    "babel-plugin-external-helpers": "^6.22.0",
 | 
				
			||||||
    "@types/node": "^18.15.5",
 | 
					    "babel-plugin-transform-regenerator": "^6.24.1",
 | 
				
			||||||
    "concurrently": "^3.6.1",
 | 
					    "babel-plugin-transform-runtime": "^6.23.0",
 | 
				
			||||||
    "http-server": "^0.12.3",
 | 
					    "babel-preset-latest": "^6.24.1",
 | 
				
			||||||
    "jsdoc": "^3.6.7",
 | 
					    "chance": "^1.0.9",
 | 
				
			||||||
    "markdownlint-cli": "^0.41.0",
 | 
					    "codemirror": "^5.37.0",
 | 
				
			||||||
    "rollup": "^3.20.0",
 | 
					    "concurrently": "^3.4.0",
 | 
				
			||||||
    "standard": "^16.0.4",
 | 
					    "cutest": "^0.1.9",
 | 
				
			||||||
    "tui-jsdoc-template": "^1.2.2",
 | 
					    "esdoc": "^1.0.4",
 | 
				
			||||||
    "typescript": "^4.9.5",
 | 
					    "esdoc-standard-plugin": "^1.0.0",
 | 
				
			||||||
    "y-protocols": "^1.0.5"
 | 
					    "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": {
 | 
					  "dependencies": {
 | 
				
			||||||
    "npm": ">=8.0.0",
 | 
					    "uws": "^10.148.0"
 | 
				
			||||||
    "node": ">=16.0.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