Compare commits

..

22 Commits

Author SHA1 Message Date
Kevin Jahns
00135dcfab v13.0.0-62 -- distribution files 2018-06-13 00:08:16 +02:00
Kevin Jahns
5cf6f45f19 13.0.0-62 2018-06-13 00:08:01 +02:00
Kevin Jahns
967903673b fixed undo/redo issues and implemented ability to manually flush the UndoManager 2018-06-13 00:06:38 +02:00
Kevin Jahns
db5312443e 13.0.0-61 2018-05-18 02:02:44 +02:00
Kevin Jahns
dbda07424b fix DomBinding destroy 2018-05-18 02:01:53 +02:00
Kevin Jahns
684d38d6c8 make compatible with webpack and less sophisticated module bundlers 2018-05-13 13:07:24 +02:00
Kevin Jahns
44fa064eb2 Merge branch 'master' of github.com:y-js/yjs 2018-05-12 16:43:28 +02:00
Kevin Jahns
9b6fffd880 Add rollup dependency - closes #110 2018-05-12 16:43:05 +02:00
Kevin Jahns
e9993b2643 Merge pull request #111 from larskarbo/patch-1
(DOCS) Remove y-array duplicates
2018-05-12 16:41:14 +02:00
Kevin Jahns
762e9e8a3a do not log _start in logItemHelper. Fixes #114 2018-05-12 15:42:31 +02:00
Kevin Jahns
6ddeb788c7 Merge branch 'master' of github.com:y-js/yjs 2018-05-09 16:28:07 +02:00
Kevin Jahns
b9245f323c prefer !== undefined check instead of hasOwnProperty 2018-05-09 16:27:55 +02:00
Kevin Jahns
c0e630b635 prefer parentElement instead of parentNode 2018-05-09 14:42:24 +02:00
Kevin Jahns
e56457a0ef 13.0.0-60 2018-05-08 13:46:27 +02:00
Kevin Jahns
ca13849828 fix domBinding infinite loop 2018-05-08 13:45:51 +02:00
Kevin Jahns
92c2fbd6d3 remove debug dependency 2018-05-07 11:52:37 +02:00
Kevin Jahns
65b8921f05 13.0.0-59 2018-05-07 11:45:39 +02:00
Kevin Jahns
1ace7f4b73 fix parentNode binding in case parent doesn't fire MO 2018-05-07 11:44:22 +02:00
Kevin Jahns
6336064516 13.0.0-58 2018-05-05 15:33:53 +02:00
Kevin Jahns
49d2e42b41 fix missing dom-binding - still investigating 2018-05-05 15:33:16 +02:00
Kevin Jahns
c098e8e745 implement scroll-fixer 2018-05-05 14:47:59 +02:00
Lars Karbo
ed2273e2ed Remove y-array duplicates 2018-04-24 12:59:51 -07:00
28 changed files with 1035 additions and 34030 deletions

View File

@@ -70,7 +70,6 @@ missing modules.
<script src="https://cdn.jsdelivr.net/npm/y-array@10/dist/y-array.js"></script>
<script src="https://cdn.jsdelivr.net/npm/y-websockets-client@8/dist/y-websockets-client.js"></script>
<script src="https://cdn.jsdelivr.net/npm/y-memory@8/dist/y-memory.js"></script>
<script src="https://cdn.jsdelivr.net/npm/y-array@10/dist/y-array.js"></script>
<script src="https://cdn.jsdelivr.net/npm/y-map@10/dist/y-map.js"></script>
<script src="https://cdn.jsdelivr.net/npm/y-text@9/dist/y-text.js"></script>
// ..
@@ -89,7 +88,6 @@ var Y = require('yjs')
require('y-array')(Y) // add the y-array type to Yjs
require('y-websockets-client')(Y)
require('y-memory')(Y)
require('y-array')(Y)
require('y-map')(Y)
require('y-text')(Y)
// ..
@@ -102,7 +100,6 @@ import Y from 'yjs'
import yArray from 'y-array'
import yWebsocketsClient from 'y-webrtc'
import yMemory from 'y-memory'
import yArray from 'y-array'
import yMap from 'y-map'
import yText from 'y-text'
// ..

View File

@@ -1,7 +1,7 @@
/* global Y */
window.onload = function () {
window.domBinding = new Y.DomBinding(window.yXmlType, document.body)
window.domBinding = new Y.DomBinding(window.yXmlType, document.body, { scrollingElement: document.scrollingElement })
}
let y = new Y('htmleditor', {

View File

@@ -20,7 +20,7 @@ let quill = new Quill('#quill-container', {
[{ header: [1, 2, false] }],
['bold', 'italic', 'underline'],
['image', 'code-block'],
[{ color: [] }, { background: [] }], // Snow theme fills in values
[{ color: [] }, { background: [] }], // Snow theme fills in values
[{ script: 'sub' }, { script: 'super' }],
['link', 'image'],
['link', 'code-block'],
@@ -31,7 +31,7 @@ let quill = new Quill('#quill-container', {
}
},
placeholder: 'Compose an epic...',
theme: 'snow' // or 'bubble'
theme: 'snow' // or 'bubble'
})
let cursors = quill.getModule('cursors')

View File

@@ -13,7 +13,7 @@ let quill = new Quill('#quill-container', {
[{ header: [1, 2, false] }],
['bold', 'italic', 'underline'],
['image', 'code-block'],
[{ color: [] }, { background: [] }], // Snow theme fills in values
[{ color: [] }, { background: [] }], // Snow theme fills in values
[{ script: 'sub' }, { script: 'super' }],
['link', 'image'],
['link', 'code-block'],
@@ -21,7 +21,7 @@ let quill = new Quill('#quill-container', {
]
},
placeholder: 'Compose an epic...',
theme: 'snow' // or 'bubble'
theme: 'snow' // or 'bubble'
})
let yText = y.define('quill', Y.Text)

View File

@@ -35,7 +35,7 @@ Y({
toolbar: [
[{ size: ['small', false, 'large', 'huge'] }],
['bold', 'italic', 'underline'],
[{ color: [] }, { background: [] }], // Snow theme fills in values
[{ color: [] }, { background: [] }], // Snow theme fills in values
[{ script: 'sub' }, { script: 'super' }],
['link', 'image'],
['link', 'code-block'],

28
package-lock.json generated
View File

@@ -1,9 +1,21 @@
{
"name": "yjs",
"version": "13.0.0-57",
"version": "13.0.0-62",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"@types/estree": {
"version": "0.0.38",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.38.tgz",
"integrity": "sha512-F/v7t1LwS4vnXuPooJQGBRKRGIoxWUTmA4VHfqjOccFsNDThD5bfUNpITive6s352O7o384wcpEaDV8rHCehDA==",
"dev": true
},
"@types/node": {
"version": "6.0.110",
"resolved": "https://registry.npmjs.org/@types/node/-/node-6.0.110.tgz",
"integrity": "sha512-LiaH3mF+OAqR+9Wo1OTJDbZDtCewAVjTbMhF1ZgUJ3fc8xqOJq6VqbpBh9dJVCVzByGmYIg2fREbuXNX0TKiJA==",
"dev": true
},
"abab": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz",
@@ -1498,6 +1510,7 @@
"version": "2.6.8",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz",
"integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=",
"dev": true,
"requires": {
"ms": "2.0.0"
}
@@ -4806,7 +4819,8 @@
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
"dev": true
},
"mute-stream": {
"version": "0.0.5",
@@ -5629,6 +5643,16 @@
"glob": "7.1.2"
}
},
"rollup": {
"version": "0.58.2",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-0.58.2.tgz",
"integrity": "sha512-RZVvCWm9BHOYloaE6LLiE/ibpjv1CmI8F8k0B0Cp+q1eezo3cswszJH1DN0djgzSlo0hjuuCmyeI+1XOYLl4wg==",
"dev": true,
"requires": {
"@types/estree": "0.0.38",
"@types/node": "6.0.110"
}
},
"rollup-plugin-babel": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/rollup-plugin-babel/-/rollup-plugin-babel-2.7.1.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "yjs",
"version": "13.0.0-57",
"version": "13.0.0-62",
"description": "A framework for real-time p2p shared editing on any data",
"main": "./y.node.js",
"browser": "./y.js",
@@ -61,6 +61,7 @@
"esdoc-standard-plugin": "^1.0.0",
"quill": "^1.3.5",
"quill-cursors": "^1.0.2",
"rollup": "^0.58.2",
"rollup-plugin-babel": "^2.7.1",
"rollup-plugin-commonjs": "^8.0.2",
"rollup-plugin-inject": "^2.0.0",
@@ -71,8 +72,5 @@
"rollup-watch": "^3.2.2",
"standard": "^10.0.2",
"tag-dist-files": "^0.1.6"
},
"dependencies": {
"debug": "^2.6.8"
}
}

View File

@@ -1,8 +1,9 @@
/* global MutationObserver */
/* global MutationObserver, getSelection */
import { fromRelativePosition } from '../../Util/relativePosition.js'
import Binding from '../Binding.js'
import { createAssociation, removeAssociation } from './util.js'
import { beforeTransactionSelectionFixer, afterTransactionSelectionFixer } from './selection.js'
import { beforeTransactionSelectionFixer, afterTransactionSelectionFixer, getCurrentRelativeSelection } from './selection.js'
import { defaultFilter, applyFilterOnType } from './filter.js'
import typeObserver from './typeObserver.js'
import domObserver from './domObserver.js'
@@ -33,6 +34,7 @@ export default class DomBinding extends Binding {
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}
@@ -66,16 +68,25 @@ export default class DomBinding extends Binding {
characterData: true,
subtree: true
})
this._currentSel = null
document.addEventListener('selectionchange', () => {
this._currentSel = getCurrentRelativeSelection(this)
})
const y = type._y
this.y = 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)
this._mutualExclude(() => {
beforeTransactionSelectionFixer(this, remote)
})
}
y.on('beforeTransaction', this._beforeTransactionHandler)
this._afterTransactionHandler = (y, transaction, remote) => {
afterTransactionSelectionFixer(y, this, transaction, remote)
this._mutualExclude(() => {
afterTransactionSelectionFixer(this, remote)
})
// remove associations
// TODO: this could be done more efficiently
// e.g. Always delete using the following approach, or removeAssociation
@@ -105,17 +116,6 @@ export default class DomBinding extends Binding {
createAssociation(this, target, type)
}
/**
* Enables the smart scrolling functionality for a Dom Binding.
* This is useful when YXml is bound to a shared editor. When activated,
* the viewport will be changed to accommodate remote changes.
*
* @param {Element} scrollElement The node that is
*/
enableSmartScrolling (scrollElement) {
// @TODO: implement smart scrolling
}
/**
* NOTE: currently does not apply filter to existing elements!
* @param {FilterFunction} filter The filter function to use from now on.
@@ -125,27 +125,86 @@ export default class DomBinding extends Binding {
// TODO: apply filter to all elements
}
_getUndoStackInfo () {
return this.getSelection()
}
_restoreUndoStackInfo (info) {
this.restoreSelection(info)
}
getSelection () {
return this._currentSel
}
restoreSelection (selection) {
if (selection !== null) {
const { to, from } = selection
let shouldUpdate = false
/**
* There is little information on the difference between anchor/focus and base/extent.
* MDN doesn't even mention base/extent anymore.. though you still have to call
* setBaseAndExtent to change the selection..
* I can observe that base/extend refer to notes higher up in the xml hierachy.
* Espesially for undo/redo this is preferred. If this becomes a problem in the future,
* we should probably go back to anchor/focus.
*/
const browserSelection = getSelection()
let { baseNode, baseOffset, extentNode, extentOffset } = browserSelection
if (from !== null) {
let sel = fromRelativePosition(this.y, from)
if (sel !== null) {
let node = this.typeToDom.get(sel.type)
let offset = sel.offset
if (node !== baseNode || offset !== baseOffset) {
baseNode = node
baseOffset = offset
shouldUpdate = true
}
}
}
if (to !== null) {
let sel = fromRelativePosition(this.y, to)
if (sel !== null) {
let node = this.typeToDom.get(sel.type)
let offset = sel.offset
if (node !== extentNode || offset !== extentOffset) {
extentNode = node
extentOffset = offset
shouldUpdate = true
}
}
}
if (shouldUpdate) {
browserSelection.setBaseAndExtent(
baseNode,
baseOffset,
extentNode,
extentOffset
)
}
}
}
/**
* Remove all properties that are handled by this class.
*/
destroy () {
this.domToType = null
this.typeToDom = null
this.type.unobserve(this._typeObserver)
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('afterObserverCalls', this._afterObserverCallsHandler)
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
*/
/**
* 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
*/

View File

@@ -60,8 +60,8 @@ function applyChangesFromDom (binding, dom, yxml, _document) {
removeAssociation(binding, childNode, childType)
} else {
// child was moved to a different position.
childType._delete(y)
removeAssociation(binding, childNode, childType)
childType._delete(y)
}
prevExpectedType = insertNodeHelper(yxml, prevExpectedType, childNode, _document, binding)
} else {
@@ -90,8 +90,19 @@ export default function domObserver (mutations, _document) {
mutations.forEach(mutation => {
const dom = mutation.target
const yxml = this.domToType.get(dom)
if (yxml === false || yxml === undefined || yxml.constructor === YXmlHook) {
// dom element is filtered
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) {
@@ -125,9 +136,6 @@ export default function domObserver (mutations, _document) {
}
})
for (let dom of diffChildren) {
if (dom.yOnChildrenChanged !== undefined) {
dom.yOnChildrenChanged()
}
const yxml = this.domToType.get(dom)
applyChangesFromDom(this, dom, yxml, _document)
}

View File

@@ -1,5 +1,7 @@
import { YXmlText, YXmlElement, YXmlHook } from '../../Types/YXml/YXml.js'
import YXmlText from '../../Types/YXml/YXmlText.js'
import YXmlHook from '../../Types/YXml/YXmlHook.js'
import YXmlElement from '../../Types/YXml/YXmlElement.js'
import { createAssociation, domsToTypes } from './util.js'
import { filterDomAttributes, defaultFilter } from './filter.js'

View File

@@ -1,84 +1,35 @@
/* globals getSelection */
import { getRelativePosition, fromRelativePosition } from '../../Util/relativePosition.js'
import { getRelativePosition } from '../../Util/relativePosition.js'
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
function _getCurrentRelativeSelection (domBinding) {
const { baseNode, baseOffset, extentNode, extentOffset } = getSelection()
const baseNodeType = domBinding.domToType.get(baseNode)
const extentNodeType = domBinding.domToType.get(extentNode)
if (baseNodeType !== undefined && extentNodeType !== undefined) {
return {
from: getRelativePosition(baseNodeType, baseOffset),
to: getRelativePosition(extentNodeType, extentOffset)
}
}
} else {
beforeTransactionSelectionFixer = function _fakeBeforeTransactionSelectionFixer () {}
return null
}
export const getCurrentRelativeSelection = typeof getSelection !== 'undefined' ? _getCurrentRelativeSelection : () => null
export function beforeTransactionSelectionFixer (domBinding, remote) {
if (remote) {
relativeSelection = getCurrentRelativeSelection(domBinding)
}
}
/**
* @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
)
export function afterTransactionSelectionFixer (domBinding, remote) {
if (relativeSelection !== null && remote) {
domBinding.restoreSelection(relativeSelection)
}
}

View File

@@ -1,20 +1,55 @@
/* global getSelection */
import YXmlText from '../../Types/YXml/YXmlText.js'
import YXmlHook from '../../Types/YXml/YXmlHook.js'
import { removeDomChildrenUntilElementFound } from './util.js'
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()
// TODO: use hasOwnProperty instead of === undefined check
} else if (event.attributesChanged !== undefined) {
// update attributes
event.attributesChanged.forEach(attributeName => {
@@ -59,5 +94,6 @@ export default function typeObserver (events) {
}
}
})
fixScroll(this.scrollingElement, scrollRef)
})
}

View File

@@ -61,5 +61,5 @@ export function logID (id) {
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)},start:${logID(item._start)},left:${logID(left)},origin:${logID(origin)},right:${logID(item._right)},parent:${logID(item._parent)},parentSub:${item._parentSub}${append !== undefined ? ' - ' + append : ''})`
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 : ''})`
}

View File

@@ -120,7 +120,7 @@ export default class Item {
*
* @private
*/
_redo (y) {
_redo (y, redoitems) {
if (this._redone !== null) {
return this._redone
}
@@ -130,7 +130,10 @@ export default class Item {
let parent = this._parent
// make sure that parent is redone
if (parent._deleted === true && parent._redone === null) {
parent._redo(y)
// try to undo parent if it will be undone anyway
if (!redoitems.has(parent) || !parent._redo(y, redoitems)) {
return false
}
}
if (parent._redone !== null) {
parent = parent._redone
@@ -157,7 +160,7 @@ export default class Item {
struct._parentSub = this._parentSub
struct._integrate(y)
this._redone = struct
return struct
return true
}
/**

View File

@@ -155,7 +155,7 @@ function insertAttributes (y, parent, left, right, attributes, currentAttributes
*/
function insertText (y, text, parent, left, right, currentAttributes, attributes) {
for (let [key] of currentAttributes) {
if (attributes.hasOwnProperty(key) === false) {
if (attributes[key] === undefined) {
attributes[key] = null
}
}
@@ -189,8 +189,9 @@ function formatText (y, length, parent, left, right, currentAttributes, attribut
if (right._deleted === false) {
switch (right.constructor) {
case ItemFormat:
if (attributes.hasOwnProperty(right.key)) {
if (attributes[right.key] === right.value) {
const attr = attributes[right.key]
if (attr !== undefined) {
if (attr === right.value) {
negatedAttributes.delete(right.key)
} else {
negatedAttributes.set(right.key, right.value)
@@ -253,7 +254,7 @@ function deleteText (y, length, parent, left, right, currentAttributes) {
* @typedef {Array<Object>} Delta
*/
/**
/**
* Attributes that can be assigned to a selection of text.
*
* @example
@@ -405,8 +406,9 @@ class YTextEvent extends YArrayEvent {
}
} else if (item._deleted === false) {
oldAttributes.set(item.key, item.value)
if (attributes.hasOwnProperty(item.key)) {
if (attributes[item.key] !== item.value) {
const attr = attributes[item.key]
if (attr !== undefined) {
if (attr !== item.value) {
if (action === 'retain') {
addOp()
}
@@ -433,7 +435,7 @@ class YTextEvent extends YArrayEvent {
addOp()
while (this._delta.length > 0) {
let lastOp = this._delta[this._delta.length - 1]
if (lastOp.hasOwnProperty('retain') && !lastOp.hasOwnProperty('attributes')) {
if (lastOp.retain !== undefined && lastOp.attributes === undefined) {
// retain delta's if they don't assign attributes
this._delta.pop()
} else {
@@ -505,11 +507,11 @@ export default class YText extends YArray {
const currentAttributes = new Map()
for (let i = 0; i < delta.length; i++) {
let op = delta[i]
if (op.hasOwnProperty('insert')) {
if (op.insert !== undefined) {
;[left, right] = insertText(y, op.insert, this, left, right, currentAttributes, op.attributes || {})
} else if (op.hasOwnProperty('retain')) {
} else if (op.retain !== undefined) {
;[left, right] = formatText(y, op.retain, this, left, right, currentAttributes, op.attributes || {})
} else if (op.hasOwnProperty('delete')) {
} else if (op.delete !== undefined) {
;[left, right] = deleteText(y, op.delete, this, left, right, currentAttributes)
}
}

View File

@@ -1,12 +0,0 @@
import YXmlFragment from './YXmlFragment.js'
import YXmlElement from './YXmlElement.js'
import YXmlHook from './YXmlHook.js'
export { default as YXmlFragment } from './YXmlFragment.js'
export { default as YXmlElement } from './YXmlElement.js'
export { default as YXmlText } from './YXmlText.js'
export { default as YXmlHook } from './YXmlHook.js'
YXmlFragment._YXmlElement = YXmlElement
YXmlFragment._YXmlHook = YXmlHook

View File

@@ -1,5 +1,5 @@
import YMap from '../YMap/YMap.js'
import { YXmlFragment } from './YXml.js'
import YXmlFragment from './YXmlFragment.js'
import { createAssociation } from '../../Bindings/DomBinding/util.js'
/**
@@ -186,3 +186,5 @@ export default class YXmlElement extends YXmlFragment {
return dom
}
}
YXmlFragment._YXmlElement = YXmlElement

View File

@@ -2,7 +2,7 @@ import ID from './ID/ID.js'
import isParentOf from './isParentOf.js'
class ReverseOperation {
constructor (y, transaction) {
constructor (y, transaction, bindingInfos) {
this.created = new Date()
const beforeState = transaction.beforeState
if (beforeState.has(y.userID)) {
@@ -12,15 +12,26 @@ class ReverseOperation {
this.toState = null
this.fromState = null
}
this.deletedStructs = transaction.deletedStructs
this.deletedStructs = new Set()
transaction.deletedStructs.forEach(struct => {
this.deletedStructs.add({
from: struct._id,
len: struct._length
})
})
/**
* Maps from binding to binding information (e.g. cursor information)
*/
this.bindingInfos = bindingInfos
}
}
function applyReverseOperation (y, scope, reverseBuffer) {
let performedUndo = false
let undoOp
y.transact(() => {
while (!performedUndo && reverseBuffer.length > 0) {
let undoOp = reverseBuffer.pop()
undoOp = reverseBuffer.pop()
// make sure that it is possible to iterate {from}-{to}
if (undoOp.fromState !== null) {
y.os.getItemCleanStart(undoOp.fromState)
@@ -35,23 +46,39 @@ function applyReverseOperation (y, scope, reverseBuffer) {
}
})
}
for (let op of undoOp.deletedStructs) {
if (
isParentOf(scope, op) &&
op._parent !== y &&
(
op._id.user !== y.userID ||
undoOp.fromState === null ||
op._id.clock < undoOp.fromState.clock ||
op._id.clock > undoOp.toState.clock
)
) {
performedUndo = true
op._redo(y)
}
const redoitems = new Set()
for (let del of undoOp.deletedStructs) {
const fromState = del.from
const toState = new ID(fromState.user, fromState.clock + del.len - 1)
y.os.getItemCleanStart(fromState)
y.os.getItemCleanEnd(toState)
y.os.iterate(fromState, toState, op => {
if (
isParentOf(scope, op) &&
op._parent !== y &&
(
op._id.user !== y.userID ||
undoOp.fromState === null ||
op._id.clock < undoOp.fromState.clock ||
op._id.clock > undoOp.toState.clock
)
) {
redoitems.add(op)
}
})
}
redoitems.forEach(op => {
const opUndone = op._redo(y, redoitems)
performedUndo = performedUndo || opUndone
})
}
})
if (performedUndo) {
// should be performed after the undo transaction
undoOp.bindingInfos.forEach((info, binding) => {
binding._restoreUndoStackInfo(info)
})
}
return performedUndo
}
@@ -66,6 +93,7 @@ export default class UndoManager {
*/
constructor (scope, options = {}) {
this.options = options
this._bindings = new Set(options.bindings)
options.captureTimeout = options.captureTimeout == null ? 500 : options.captureTimeout
this._undoBuffer = []
this._redoBuffer = []
@@ -76,16 +104,28 @@ export default class UndoManager {
const y = scope._y
this.y = y
y._hasUndoManager = true
let bindingInfos
y.on('beforeTransaction', (y, transaction, remote) => {
if (!remote) {
// Store binding information before transaction is executed
// By restoring the binding information, we can make sure that the state
// before the transaction can be recovered
bindingInfos = new Map()
this._bindings.forEach(binding => {
bindingInfos.set(binding, binding._getUndoStackInfo())
})
}
})
y.on('afterTransaction', (y, transaction, remote) => {
if (!remote && transaction.changedParentTypes.has(scope)) {
let reverseOperation = new ReverseOperation(y, transaction)
let reverseOperation = new ReverseOperation(y, transaction, bindingInfos)
if (!this._undoing) {
let lastUndoOp = this._undoBuffer.length > 0 ? this._undoBuffer[this._undoBuffer.length - 1] : null
if (
this._redoing === false &&
this._lastTransactionWasUndo === false &&
lastUndoOp !== null &&
reverseOperation.created - lastUndoOp.created <= options.captureTimeout
(options.captureTimeout < 0 || reverseOperation.created - lastUndoOp.created <= options.captureTimeout)
) {
lastUndoOp.created = reverseOperation.created
if (reverseOperation.toState !== null) {
@@ -110,6 +150,13 @@ export default class UndoManager {
})
}
/**
* Enforce that the next change is created as a separate item in the undo stack
*/
flushChanges () {
this._lastTransactionWasUndo = true
}
/**
* Undo the last locally created change.
*/

View File

@@ -76,7 +76,10 @@ export function fromRelativePosition (y, rpos) {
} else {
id = new RootID(rpos[3], rpos[4])
}
const type = y.os.get(id)
let type = y.os.get(id)
while (type._redone !== null) {
type = type._redone
}
if (type === null || type.constructor === GC) {
return null
}
@@ -87,12 +90,16 @@ export function fromRelativePosition (y, rpos) {
} else {
let offset = 0
let struct = y.os.findNodeWithUpperBound(new ID(rpos[0], rpos[1])).val
const diff = rpos[1] - struct._id.clock
while (struct._redone !== null) {
struct = struct._redone
}
const parent = struct._parent
if (struct.constructor === GC || parent._deleted) {
return null
}
if (!struct._deleted) {
offset = rpos[1] - struct._id.clock
offset = diff
}
struct = struct._left
while (struct !== null) {

View File

@@ -1,7 +1,10 @@
import YArray from '../Types/YArray/YArray.js'
import YMap from '../Types/YMap/YMap.js'
import YText from '../Types/YText/YText.js'
import { YXmlFragment, YXmlElement, YXmlText, YXmlHook } from '../Types/YXml/YXml.js'
import YXmlText from '../Types/YXml/YXmlText.js'
import YXmlHook from '../Types/YXml/YXmlHook.js'
import YXmlFragment from '../Types/YXml/YXmlFragment.js'
import YXmlElement from '../Types/YXml/YXmlElement.js'
import Delete from '../Struct/Delete.js'
import ItemJSON from '../Struct/ItemJSON.js'

View File

@@ -10,7 +10,10 @@ import Persistence from './Persistence.js'
import YArray from './Types/YArray/YArray.js'
import YMap from './Types/YMap/YMap.js'
import YText from './Types/YText/YText.js'
import { YXmlFragment, YXmlElement, YXmlText, YXmlHook } from './Types/YXml/YXml.js'
import YXmlText from './Types/YXml/YXmlText.js'
import YXmlHook from './Types/YXml/YXmlHook.js'
import YXmlFragment from './Types/YXml/YXmlFragment.js'
import YXmlElement from './Types/YXml/YXmlElement.js'
import BinaryDecoder from './Util/Binary/Decoder.js'
import { getRelativePosition, fromRelativePosition } from './Util/relativePosition.js'
import { registerStruct } from './Util/structReferences.js'

View File

@@ -111,7 +111,7 @@ function compareEvent (t, is, should) {
t.assert(
should[key] === is[key] ||
JSON.stringify(should[key]) === JSON.stringify(is[key])
, 'event works as expected'
, 'event works as expected'
)
}
}

8
y.js

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1334
y.node.js

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

33250
y.test.js

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long