vue3-free-layout
Version:
[vue3+ts]拖动布局,自由拖动,标尺,放大缩小,控件之间的标尺
14,915 lines • 485 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('vue')) :
typeof define === 'function' && define.amd ? define(['exports', 'vue'], factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global["well-bricks"] = {}, global.Vue));
})(this, (function (exports, vue) { 'use strict';
/**
* Make a map and return a function for checking if a key
* is in that map.
* IMPORTANT: all calls of this function must be prefixed with
* \/\*#\_\_PURE\_\_\*\/
* So that rollup can tree-shake them if necessary.
*/
(process.env.NODE_ENV !== 'production')
? Object.freeze({})
: {};
(process.env.NODE_ENV !== 'production') ? Object.freeze([]) : [];
const NOOP = () => { };
const extend = Object.assign;
const isArray$1 = Array.isArray;
const isFunction = (val) => typeof val === 'function';
let activeEffectScope;
function recordEffectScope(effect, scope = activeEffectScope) {
if (scope && scope.active) {
scope.effects.push(effect);
}
}
const createDep = (effects) => {
const dep = new Set(effects);
dep.w = 0;
dep.n = 0;
return dep;
};
const wasTracked = (dep) => (dep.w & trackOpBit) > 0;
const newTracked = (dep) => (dep.n & trackOpBit) > 0;
const initDepMarkers = ({ deps }) => {
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].w |= trackOpBit; // set was tracked
}
}
};
const finalizeDepMarkers = (effect) => {
const { deps } = effect;
if (deps.length) {
let ptr = 0;
for (let i = 0; i < deps.length; i++) {
const dep = deps[i];
if (wasTracked(dep) && !newTracked(dep)) {
dep.delete(effect);
}
else {
deps[ptr++] = dep;
}
// clear bits
dep.w &= ~trackOpBit;
dep.n &= ~trackOpBit;
}
deps.length = ptr;
}
};
// The number of effects currently being tracked recursively.
let effectTrackDepth = 0;
let trackOpBit = 1;
/**
* The bitwise track markers support at most 30 levels of recursion.
* This value is chosen to enable modern JS engines to use a SMI on all platforms.
* When recursion depth is greater, fall back to using a full cleanup.
*/
const maxMarkerBits = 30;
let activeEffect;
Symbol((process.env.NODE_ENV !== 'production') ? 'iterate' : '');
Symbol((process.env.NODE_ENV !== 'production') ? 'Map key iterate' : '');
class ReactiveEffect {
constructor(fn, scheduler = null, scope) {
this.fn = fn;
this.scheduler = scheduler;
this.active = true;
this.deps = [];
this.parent = undefined;
recordEffectScope(this, scope);
}
run() {
if (!this.active) {
return this.fn();
}
let parent = activeEffect;
let lastShouldTrack = shouldTrack;
while (parent) {
if (parent === this) {
return;
}
parent = parent.parent;
}
try {
this.parent = activeEffect;
activeEffect = this;
shouldTrack = true;
trackOpBit = 1 << ++effectTrackDepth;
if (effectTrackDepth <= maxMarkerBits) {
initDepMarkers(this);
}
else {
cleanupEffect(this);
}
return this.fn();
}
finally {
if (effectTrackDepth <= maxMarkerBits) {
finalizeDepMarkers(this);
}
trackOpBit = 1 << --effectTrackDepth;
activeEffect = this.parent;
shouldTrack = lastShouldTrack;
this.parent = undefined;
if (this.deferStop) {
this.stop();
}
}
}
stop() {
// stopped while running itself - defer the cleanup
if (activeEffect === this) {
this.deferStop = true;
}
else if (this.active) {
cleanupEffect(this);
if (this.onStop) {
this.onStop();
}
this.active = false;
}
}
}
function cleanupEffect(effect) {
const { deps } = effect;
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect);
}
deps.length = 0;
}
}
let shouldTrack = true;
function trackEffects(dep, debuggerEventExtraInfo) {
let shouldTrack = false;
if (effectTrackDepth <= maxMarkerBits) {
if (!newTracked(dep)) {
dep.n |= trackOpBit; // set newly tracked
shouldTrack = !wasTracked(dep);
}
}
else {
// Full cleanup mode.
shouldTrack = !dep.has(activeEffect);
}
if (shouldTrack) {
dep.add(activeEffect);
activeEffect.deps.push(dep);
if ((process.env.NODE_ENV !== 'production') && activeEffect.onTrack) {
activeEffect.onTrack(Object.assign({ effect: activeEffect }, debuggerEventExtraInfo));
}
}
}
function triggerEffects(dep, debuggerEventExtraInfo) {
// spread into array for stabilization
const effects = isArray$1(dep) ? dep : [...dep];
for (const effect of effects) {
if (effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo);
}
}
for (const effect of effects) {
if (!effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo);
}
}
}
function triggerEffect(effect, debuggerEventExtraInfo) {
if (effect !== activeEffect || effect.allowRecurse) {
if ((process.env.NODE_ENV !== 'production') && effect.onTrigger) {
effect.onTrigger(extend({ effect }, debuggerEventExtraInfo));
}
if (effect.scheduler) {
effect.scheduler();
}
else {
effect.run();
}
}
}
function toRaw(observed) {
const raw = observed && observed["__v_raw" /* ReactiveFlags.RAW */];
return raw ? toRaw(raw) : observed;
}
function trackRefValue(ref) {
if (shouldTrack && activeEffect) {
ref = toRaw(ref);
if ((process.env.NODE_ENV !== 'production')) {
trackEffects(ref.dep || (ref.dep = createDep()), {
target: ref,
type: "get" /* TrackOpTypes.GET */,
key: 'value'
});
}
else {
trackEffects(ref.dep || (ref.dep = createDep()));
}
}
}
function triggerRefValue(ref, newVal) {
ref = toRaw(ref);
const dep = ref.dep;
if (dep) {
if ((process.env.NODE_ENV !== 'production')) {
triggerEffects(dep, {
target: ref,
type: "set" /* TriggerOpTypes.SET */,
key: 'value',
newValue: newVal
});
}
else {
triggerEffects(dep);
}
}
}
var _a$1;
class ComputedRefImpl {
constructor(getter, _setter, isReadonly, isSSR) {
this._setter = _setter;
this.dep = undefined;
this.__v_isRef = true;
this[_a$1] = false;
this._dirty = true;
this.effect = new ReactiveEffect(getter, () => {
if (!this._dirty) {
this._dirty = true;
triggerRefValue(this);
}
});
this.effect.computed = this;
this.effect.active = this._cacheable = !isSSR;
this["__v_isReadonly" /* ReactiveFlags.IS_READONLY */] = isReadonly;
}
get value() {
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
const self = toRaw(this);
trackRefValue(self);
if (self._dirty || !self._cacheable) {
self._dirty = false;
self._value = self.effect.run();
}
return self._value;
}
set value(newValue) {
this._setter(newValue);
}
}
_a$1 = "__v_isReadonly" /* ReactiveFlags.IS_READONLY */;
function computed(getterOrOptions, debugOptions, isSSR = false) {
let getter;
let setter;
const onlyGetter = isFunction(getterOrOptions);
if (onlyGetter) {
getter = getterOrOptions;
setter = (process.env.NODE_ENV !== 'production')
? () => {
console.warn('Write operation failed: computed value is readonly');
}
: NOOP;
}
else {
getter = getterOrOptions.get;
setter = getterOrOptions.set;
}
const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR);
if ((process.env.NODE_ENV !== 'production') && debugOptions && !isSSR) {
cRef.effect.onTrack = debugOptions.onTrack;
cRef.effect.onTrigger = debugOptions.onTrigger;
}
return cRef;
}
class CheckType {
static getType(v) {
return Object.prototype.toString.call(v);
}
static isNumber(v) {
return CheckType.getType(v) === "[object Number]";
}
static isString(v) {
return CheckType.getType(v) === "[object String]";
}
static isBoolean(v) {
return CheckType.getType(v) === "[object Boolean]";
}
static isObject(v) {
return CheckType.getType(v) === "[object Object]";
}
static isArray(v) {
return CheckType.getType(v) === "[object Array]";
}
static isEmptyObj(v) {
return CheckType.isObject(v) && !Object.keys(v).length;
}
static isEmptyArr(v) {
return CheckType.isArray(v) && !v.length;
}
static isFunction(v) {
return CheckType.getType(v) === "[object Function]";
}
// 不为 undefined && null
static isExist(v) {
return v !== void 0 && v !== null;
}
static isDate(v) {
return CheckType.getType(v) === "[object Date]";
}
}
/**
* Utility module to work with key-value stores.
*
* @module map
*/
/**
* Creates a new Map instance.
*
* @function
* @return {Map<any, any>}
*
* @function
*/
const create$5 = () => new Map();
/**
* Copy a Map object into a fresh Map object.
*
* @function
* @template X,Y
* @param {Map<X,Y>} m
* @return {Map<X,Y>}
*/
const copy = m => {
const r = create$5();
m.forEach((v, k) => { r.set(k, v); });
return r
};
/**
* Get map property. Create T if property is undefined and set T on map.
*
* ```js
* const listeners = map.setIfUndefined(events, 'eventName', set.create)
* listeners.add(listener)
* ```
*
* @function
* @template V,K
* @template {Map<K,V>} MAP
* @param {MAP} map
* @param {K} key
* @param {function():V} createT
* @return {V}
*/
const setIfUndefined = (map, key, createT) => {
let set = map.get(key);
if (set === undefined) {
map.set(key, set = createT());
}
return set
};
/**
* Creates an Array and populates it with the content of all key-value pairs using the `f(value, key)` function.
*
* @function
* @template K
* @template V
* @template R
* @param {Map<K,V>} m
* @param {function(V,K):R} f
* @return {Array<R>}
*/
const map$1 = (m, f) => {
const res = [];
for (const [key, value] of m) {
res.push(f(value, key));
}
return res
};
/**
* Tests whether any key-value pairs pass the test implemented by `f(value, key)`.
*
* @todo should rename to some - similarly to Array.some
*
* @function
* @template K
* @template V
* @param {Map<K,V>} m
* @param {function(V,K):boolean} f
* @return {boolean}
*/
const any = (m, f) => {
for (const [key, value] of m) {
if (f(value, key)) {
return true
}
}
return false
};
/**
* Utility module to work with sets.
*
* @module set
*/
const create$4 = () => new Set();
/**
* Utility module to work with Arrays.
*
* @module array
*/
/**
* Return the last element of an array. The element must exist
*
* @template L
* @param {ArrayLike<L>} arr
* @return {L}
*/
const last = arr => arr[arr.length - 1];
/**
* Append elements from src to dest
*
* @template M
* @param {Array<M>} dest
* @param {Array<M>} src
*/
const appendTo = (dest, src) => {
for (let i = 0; i < src.length; i++) {
dest.push(src[i]);
}
};
/**
* Transforms something array-like to an actual Array.
*
* @function
* @template T
* @param {ArrayLike<T>|Iterable<T>} arraylike
* @return {T}
*/
const from = Array.from;
const isArray = Array.isArray;
/**
* Observable class prototype.
*
* @module observable
*/
/**
* Handles named events.
*
* @template N
*/
class Observable {
constructor () {
/**
* Some desc.
* @type {Map<N, any>}
*/
this._observers = create$5();
}
/**
* @param {N} name
* @param {function} f
*/
on (name, f) {
setIfUndefined(this._observers, name, create$4).add(f);
}
/**
* @param {N} name
* @param {function} f
*/
once (name, f) {
/**
* @param {...any} args
*/
const _f = (...args) => {
this.off(name, _f);
f(...args);
};
this.on(name, _f);
}
/**
* @param {N} name
* @param {function} f
*/
off (name, f) {
const observers = this._observers.get(name);
if (observers !== undefined) {
observers.delete(f);
if (observers.size === 0) {
this._observers.delete(name);
}
}
}
/**
* Emit a named event. All registered event listeners that listen to the
* specified name will receive the event.
*
* @todo This should catch exceptions
*
* @param {N} name The event name.
* @param {Array<any>} args The arguments that are applied to the event listener.
*/
emit (name, args) {
// copy all listeners to an array first to make sure that no event is emitted to listeners that are subscribed while the event handler is called.
return from((this._observers.get(name) || create$5()).values()).forEach(f => f(...args))
}
destroy () {
this._observers = create$5();
}
}
/**
* Common Math expressions.
*
* @module math
*/
const floor = Math.floor;
const abs = Math.abs;
/**
* @function
* @param {number} a
* @param {number} b
* @return {number} The smaller element of a and b
*/
const min = (a, b) => a < b ? a : b;
/**
* @function
* @param {number} a
* @param {number} b
* @return {number} The bigger element of a and b
*/
const max = (a, b) => a > b ? a : b;
const pow = Math.pow;
/**
* @param {number} n
* @return {boolean} Wether n is negative. This function also differentiates between -0 and +0
*/
const isNegativeZero = n => n !== 0 ? n < 0 : 1 / n < 0;
/**
* Utility module to work with strings.
*
* @module string
*/
const fromCharCode = String.fromCharCode;
/**
* @param {string} s
* @return {string}
*/
const toLowerCase = s => s.toLowerCase();
const trimLeftRegex = /^\s*/g;
/**
* @param {string} s
* @return {string}
*/
const trimLeft = s => s.replace(trimLeftRegex, '');
const fromCamelCaseRegex = /([A-Z])/g;
/**
* @param {string} s
* @param {string} separator
* @return {string}
*/
const fromCamelCase = (s, separator) => trimLeft(s.replace(fromCamelCaseRegex, match => `${separator}${toLowerCase(match)}`));
/**
* @param {string} str
* @return {Uint8Array}
*/
const _encodeUtf8Polyfill = str => {
const encodedString = unescape(encodeURIComponent(str));
const len = encodedString.length;
const buf = new Uint8Array(len);
for (let i = 0; i < len; i++) {
buf[i] = /** @type {number} */ (encodedString.codePointAt(i));
}
return buf
};
/* c8 ignore next */
const utf8TextEncoder = /** @type {TextEncoder} */ (typeof TextEncoder !== 'undefined' ? new TextEncoder() : null);
/**
* @param {string} str
* @return {Uint8Array}
*/
const _encodeUtf8Native = str => utf8TextEncoder.encode(str);
/**
* @param {string} str
* @return {Uint8Array}
*/
/* c8 ignore next */
const encodeUtf8 = utf8TextEncoder ? _encodeUtf8Native : _encodeUtf8Polyfill;
/* c8 ignore next */
let utf8TextDecoder = typeof TextDecoder === 'undefined' ? null : new TextDecoder('utf-8', { fatal: true, ignoreBOM: true });
/* c8 ignore start */
if (utf8TextDecoder && utf8TextDecoder.decode(new Uint8Array()).length === 1) {
// Safari doesn't handle BOM correctly.
// This fixes a bug in Safari 13.0.5 where it produces a BOM the first time it is called.
// utf8TextDecoder.decode(new Uint8Array()).length === 1 on the first call and
// utf8TextDecoder.decode(new Uint8Array()).length === 1 on the second call
// Another issue is that from then on no BOM chars are recognized anymore
/* c8 ignore next */
utf8TextDecoder = null;
}
/**
* Often used conditions.
*
* @module conditions
*/
/**
* @template T
* @param {T|null|undefined} v
* @return {T|null}
*/
/* c8 ignore next */
const undefinedToNull = v => v === undefined ? null : v;
/* eslint-env browser */
/**
* Isomorphic variable storage.
*
* Uses LocalStorage in the browser and falls back to in-memory storage.
*
* @module storage
*/
/* c8 ignore start */
class VarStoragePolyfill {
constructor () {
this.map = new Map();
}
/**
* @param {string} key
* @param {any} newValue
*/
setItem (key, newValue) {
this.map.set(key, newValue);
}
/**
* @param {string} key
*/
getItem (key) {
return this.map.get(key)
}
}
/* c8 ignore stop */
/**
* @type {any}
*/
let _localStorage = new VarStoragePolyfill();
let usePolyfill = true;
/* c8 ignore start */
try {
// if the same-origin rule is violated, accessing localStorage might thrown an error
if (typeof localStorage !== 'undefined') {
_localStorage = localStorage;
usePolyfill = false;
}
} catch (e) { }
/* c8 ignore stop */
/**
* This is basically localStorage in browser, or a polyfill in nodejs
*/
/* c8 ignore next */
const varStorage = _localStorage;
/**
* A polyfill for `addEventListener('storage', event => {..})` that does nothing if the polyfill is being used.
*
* @param {function({ key: string, newValue: string, oldValue: string }): void} eventHandler
* @function
*/
/* c8 ignore next */
const onChange = eventHandler => usePolyfill || addEventListener('storage', /** @type {any} */ (eventHandler));
/**
* A polyfill for `removeEventListener('storage', event => {..})` that does nothing if the polyfill is being used.
*
* @param {function({ key: string, newValue: string, oldValue: string }): void} eventHandler
* @function
*/
/* c8 ignore next */
const offChange = eventHandler => usePolyfill || removeEventListener('storage', /** @type {any} */ (eventHandler));
/**
* Utility functions for working with EcmaScript objects.
*
* @module object
*/
/**
* Object.assign
*/
const assign = Object.assign;
/**
* @param {Object<string,any>} obj
*/
const keys = Object.keys;
/**
* @todo implement mapToArray & map
*
* @template R
* @param {Object<string,any>} obj
* @param {function(any,string):R} f
* @return {Array<R>}
*/
const map = (obj, f) => {
const results = [];
for (const key in obj) {
results.push(f(obj[key], key));
}
return results
};
/**
* @param {Object<string,any>} obj
* @return {number}
*/
const length$1 = obj => keys(obj).length;
/**
* @param {Object|undefined} obj
*/
const isEmpty = obj => {
for (const _k in obj) {
return false
}
return true
};
/**
* @param {Object<string,any>} obj
* @param {function(any,string):boolean} f
* @return {boolean}
*/
const every = (obj, f) => {
for (const key in obj) {
if (!f(obj[key], key)) {
return false
}
}
return true
};
/**
* Calls `Object.prototype.hasOwnProperty`.
*
* @param {any} obj
* @param {string|symbol} key
* @return {boolean}
*/
const hasProperty = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key);
/**
* @param {Object<string,any>} a
* @param {Object<string,any>} b
* @return {boolean}
*/
const equalFlat = (a, b) => a === b || (length$1(a) === length$1(b) && every(a, (val, key) => (val !== undefined || hasProperty(b, key)) && b[key] === val));
/**
* Common functions and function call helpers.
*
* @module function
*/
/**
* Calls all functions in `fs` with args. Only throws after all functions were called.
*
* @param {Array<function>} fs
* @param {Array<any>} args
*/
const callAll = (fs, args, i = 0) => {
try {
for (; i < fs.length; i++) {
fs[i](...args);
}
} finally {
if (i < fs.length) {
callAll(fs, args, i + 1);
}
}
};
/**
* @template A
*
* @param {A} a
* @return {A}
*/
const id = a => a;
/**
* @template T
*
* @param {T} a
* @param {T} b
* @return {boolean}
*/
const equalityStrict = (a, b) => a === b;
/* c8 ignore start */
/**
* @param {any} a
* @param {any} b
* @return {boolean}
*/
const equalityDeep = (a, b) => {
if (a == null || b == null) {
return equalityStrict(a, b)
}
if (a.constructor !== b.constructor) {
return false
}
if (a === b) {
return true
}
switch (a.constructor) {
case ArrayBuffer:
a = new Uint8Array(a);
b = new Uint8Array(b);
// eslint-disable-next-line no-fallthrough
case Uint8Array: {
if (a.byteLength !== b.byteLength) {
return false
}
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) {
return false
}
}
break
}
case Set: {
if (a.size !== b.size) {
return false
}
for (const value of a) {
if (!b.has(value)) {
return false
}
}
break
}
case Map: {
if (a.size !== b.size) {
return false
}
for (const key of a.keys()) {
if (!b.has(key) || !equalityDeep(a.get(key), b.get(key))) {
return false
}
}
break
}
case Object:
if (length$1(a) !== length$1(b)) {
return false
}
for (const key in a) {
if (!hasProperty(a, key) || !equalityDeep(a[key], b[key])) {
return false
}
}
break
case Array:
if (a.length !== b.length) {
return false
}
for (let i = 0; i < a.length; i++) {
if (!equalityDeep(a[i], b[i])) {
return false
}
}
break
default:
return false
}
return true
};
/**
* @template V
* @template {V} OPTS
*
* @param {V} value
* @param {Array<OPTS>} options
*/
// @ts-ignore
const isOneOf = (value, options) => options.includes(value);
/* c8 ignore stop */
/**
* Isomorphic module to work access the environment (query params, env variables).
*
* @module map
*/
/* c8 ignore next */
// @ts-ignore
const isNode = typeof process !== 'undefined' && process.release &&
/node|io\.js/.test(process.release.name);
/* c8 ignore next */
const isBrowser = typeof window !== 'undefined' && typeof document !== 'undefined' && !isNode;
/* c8 ignore next 3 */
typeof navigator !== 'undefined'
? /Mac/.test(navigator.platform)
: false;
/**
* @type {Map<string,string>}
*/
let params;
/* c8 ignore start */
const computeParams = () => {
if (params === undefined) {
if (isNode) {
params = create$5();
const pargs = process.argv;
let currParamName = null;
for (let i = 0; i < pargs.length; i++) {
const parg = pargs[i];
if (parg[0] === '-') {
if (currParamName !== null) {
params.set(currParamName, '');
}
currParamName = parg;
} else {
if (currParamName !== null) {
params.set(currParamName, parg);
currParamName = null;
}
}
}
if (currParamName !== null) {
params.set(currParamName, '');
}
// in ReactNative for example this would not be true (unless connected to the Remote Debugger)
} else if (typeof location === 'object') {
params = create$5(); // eslint-disable-next-line no-undef
(location.search || '?').slice(1).split('&').forEach((kv) => {
if (kv.length !== 0) {
const [key, value] = kv.split('=');
params.set(`--${fromCamelCase(key, '-')}`, value);
params.set(`-${fromCamelCase(key, '-')}`, value);
}
});
} else {
params = create$5();
}
}
return params
};
/* c8 ignore stop */
/**
* @param {string} name
* @return {boolean}
*/
/* c8 ignore next */
const hasParam = (name) => computeParams().has(name);
/**
* @param {string} name
* @return {string|null}
*/
/* c8 ignore next 4 */
const getVariable = (name) =>
isNode
? undefinedToNull(process.env[name.toUpperCase()])
: undefinedToNull(varStorage.getItem(name));
/**
* @param {string} name
* @return {boolean}
*/
/* c8 ignore next 2 */
const hasConf = (name) =>
hasParam('--' + name) || getVariable(name) !== null;
/* c8 ignore next */
hasConf('production');
/* c8 ignore next 2 */
const forceColor = isNode &&
isOneOf(process.env.FORCE_COLOR, ['true', '1', '2']);
/* c8 ignore start */
const supportsColor = !hasParam('no-colors') &&
(!isNode || process.stdout.isTTY || forceColor) && (
!isNode || hasParam('color') || forceColor ||
getVariable('COLORTERM') !== null ||
(getVariable('TERM') || '').includes('color')
);
/* c8 ignore stop */
/* eslint-env browser */
/**
* Binary data constants.
*
* @module binary
*/
/**
* n-th bit activated.
*
* @type {number}
*/
const BIT1 = 1;
const BIT2 = 2;
const BIT3 = 4;
const BIT4 = 8;
const BIT6 = 32;
const BIT7 = 64;
const BIT8 = 128;
const BITS5 = 31;
const BITS6 = 63;
const BITS7 = 127;
/**
* @type {number}
*/
const BITS31 = 0x7FFFFFFF;
/**
* Utility helpers for working with numbers.
*
* @module number
*/
const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER;
/**
* @module number
*/
/* c8 ignore next */
const isInteger = Number.isInteger || (num => typeof num === 'number' && isFinite(num) && floor(num) === num);
/**
* Error helpers.
*
* @module error
*/
/**
* @param {string} s
* @return {Error}
*/
/* c8 ignore next */
const create$3 = s => new Error(s);
/**
* @throws {Error}
* @return {never}
*/
/* c8 ignore next 3 */
const methodUnimplemented = () => {
throw create$3('Method unimplemented')
};
/**
* @throws {Error}
* @return {never}
*/
/* c8 ignore next 3 */
const unexpectedCase = () => {
throw create$3('Unexpected case')
};
/**
* Efficient schema-less binary decoding with support for variable length encoding.
*
* Use [lib0/decoding] with [lib0/encoding]. Every encoding function has a corresponding decoding function.
*
* Encodes numbers in little-endian order (least to most significant byte order)
* and is compatible with Golang's binary encoding (https://golang.org/pkg/encoding/binary/)
* which is also used in Protocol Buffers.
*
* ```js
* // encoding step
* const encoder = encoding.createEncoder()
* encoding.writeVarUint(encoder, 256)
* encoding.writeVarString(encoder, 'Hello world!')
* const buf = encoding.toUint8Array(encoder)
* ```
*
* ```js
* // decoding step
* const decoder = decoding.createDecoder(buf)
* decoding.readVarUint(decoder) // => 256
* decoding.readVarString(decoder) // => 'Hello world!'
* decoding.hasContent(decoder) // => false - all data is read
* ```
*
* @module decoding
*/
const errorUnexpectedEndOfArray = create$3('Unexpected end of array');
const errorIntegerOutOfRange = create$3('Integer out of Range');
/**
* A Decoder handles the decoding of an Uint8Array.
*/
class Decoder {
/**
* @param {Uint8Array} uint8Array Binary data to decode
*/
constructor (uint8Array) {
/**
* Decoding target.
*
* @type {Uint8Array}
*/
this.arr = uint8Array;
/**
* Current decoding position.
*
* @type {number}
*/
this.pos = 0;
}
}
/**
* @function
* @param {Uint8Array} uint8Array
* @return {Decoder}
*/
const createDecoder = uint8Array => new Decoder(uint8Array);
/**
* @function
* @param {Decoder} decoder
* @return {boolean}
*/
const hasContent = decoder => decoder.pos !== decoder.arr.length;
/**
* Create an Uint8Array view of the next `len` bytes and advance the position by `len`.
*
* Important: The Uint8Array still points to the underlying ArrayBuffer. Make sure to discard the result as soon as possible to prevent any memory leaks.
* Use `buffer.copyUint8Array` to copy the result into a new Uint8Array.
*
* @function
* @param {Decoder} decoder The decoder instance
* @param {number} len The length of bytes to read
* @return {Uint8Array}
*/
const readUint8Array = (decoder, len) => {
const view = createUint8ArrayViewFromArrayBuffer(decoder.arr.buffer, decoder.pos + decoder.arr.byteOffset, len);
decoder.pos += len;
return view
};
/**
* Read variable length Uint8Array.
*
* Important: The Uint8Array still points to the underlying ArrayBuffer. Make sure to discard the result as soon as possible to prevent any memory leaks.
* Use `buffer.copyUint8Array` to copy the result into a new Uint8Array.
*
* @function
* @param {Decoder} decoder
* @return {Uint8Array}
*/
const readVarUint8Array = decoder => readUint8Array(decoder, readVarUint(decoder));
/**
* Read one byte as unsigned integer.
* @function
* @param {Decoder} decoder The decoder instance
* @return {number} Unsigned 8-bit integer
*/
const readUint8 = decoder => decoder.arr[decoder.pos++];
/**
* Read unsigned integer (32bit) with variable length.
* 1/8th of the storage is used as encoding overhead.
* * numbers < 2^7 is stored in one bytlength
* * numbers < 2^14 is stored in two bylength
*
* @function
* @param {Decoder} decoder
* @return {number} An unsigned integer.length
*/
const readVarUint = decoder => {
let num = 0;
let mult = 1;
const len = decoder.arr.length;
while (decoder.pos < len) {
const r = decoder.arr[decoder.pos++];
// num = num | ((r & binary.BITS7) << len)
num = num + (r & BITS7) * mult; // shift $r << (7*#iterations) and add it to num
mult *= 128; // next iteration, shift 7 "more" to the left
if (r < BIT8) {
return num
}
/* c8 ignore start */
if (num > MAX_SAFE_INTEGER) {
throw errorIntegerOutOfRange
}
/* c8 ignore stop */
}
throw errorUnexpectedEndOfArray
};
/**
* Read signed integer (32bit) with variable length.
* 1/8th of the storage is used as encoding overhead.
* * numbers < 2^7 is stored in one bytlength
* * numbers < 2^14 is stored in two bylength
* @todo This should probably create the inverse ~num if number is negative - but this would be a breaking change.
*
* @function
* @param {Decoder} decoder
* @return {number} An unsigned integer.length
*/
const readVarInt = decoder => {
let r = decoder.arr[decoder.pos++];
let num = r & BITS6;
let mult = 64;
const sign = (r & BIT7) > 0 ? -1 : 1;
if ((r & BIT8) === 0) {
// don't continue reading
return sign * num
}
const len = decoder.arr.length;
while (decoder.pos < len) {
r = decoder.arr[decoder.pos++];
// num = num | ((r & binary.BITS7) << len)
num = num + (r & BITS7) * mult;
mult *= 128;
if (r < BIT8) {
return sign * num
}
/* c8 ignore start */
if (num > MAX_SAFE_INTEGER) {
throw errorIntegerOutOfRange
}
/* c8 ignore stop */
}
throw errorUnexpectedEndOfArray
};
/**
* We don't test this function anymore as we use native decoding/encoding by default now.
* Better not modify this anymore..
*
* Transforming utf8 to a string is pretty expensive. The code performs 10x better
* when String.fromCodePoint is fed with all characters as arguments.
* But most environments have a maximum number of arguments per functions.
* For effiency reasons we apply a maximum of 10000 characters at once.
*
* @function
* @param {Decoder} decoder
* @return {String} The read String.
*/
/* c8 ignore start */
const _readVarStringPolyfill = decoder => {
let remainingLen = readVarUint(decoder);
if (remainingLen === 0) {
return ''
} else {
let encodedString = String.fromCodePoint(readUint8(decoder)); // remember to decrease remainingLen
if (--remainingLen < 100) { // do not create a Uint8Array for small strings
while (remainingLen--) {
encodedString += String.fromCodePoint(readUint8(decoder));
}
} else {
while (remainingLen > 0) {
const nextLen = remainingLen < 10000 ? remainingLen : 10000;
// this is dangerous, we create a fresh array view from the existing buffer
const bytes = decoder.arr.subarray(decoder.pos, decoder.pos + nextLen);
decoder.pos += nextLen;
// Starting with ES5.1 we can supply a generic array-like object as arguments
encodedString += String.fromCodePoint.apply(null, /** @type {any} */ (bytes));
remainingLen -= nextLen;
}
}
return decodeURIComponent(escape(encodedString))
}
};
/* c8 ignore stop */
/**
* @function
* @param {Decoder} decoder
* @return {String} The read String
*/
const _readVarStringNative = decoder =>
/** @type any */ (utf8TextDecoder).decode(readVarUint8Array(decoder));
/**
* Read string of variable length
* * varUint is used to store the length of the string
*
* @function
* @param {Decoder} decoder
* @return {String} The read String
*
*/
/* c8 ignore next */
const readVarString = utf8TextDecoder ? _readVarStringNative : _readVarStringPolyfill;
/**
* @param {Decoder} decoder
* @param {number} len
* @return {DataView}
*/
const readFromDataView = (decoder, len) => {
const dv = new DataView(decoder.arr.buffer, decoder.arr.byteOffset + decoder.pos, len);
decoder.pos += len;
return dv
};
/**
* @param {Decoder} decoder
*/
const readFloat32 = decoder => readFromDataView(decoder, 4).getFloat32(0, false);
/**
* @param {Decoder} decoder
*/
const readFloat64 = decoder => readFromDataView(decoder, 8).getFloat64(0, false);
/**
* @param {Decoder} decoder
*/
const readBigInt64 = decoder => /** @type {any} */ (readFromDataView(decoder, 8)).getBigInt64(0, false);
/**
* @type {Array<function(Decoder):any>}
*/
const readAnyLookupTable = [
decoder => undefined, // CASE 127: undefined
decoder => null, // CASE 126: null
readVarInt, // CASE 125: integer
readFloat32, // CASE 124: float32
readFloat64, // CASE 123: float64
readBigInt64, // CASE 122: bigint
decoder => false, // CASE 121: boolean (false)
decoder => true, // CASE 120: boolean (true)
readVarString, // CASE 119: string
decoder => { // CASE 118: object<string,any>
const len = readVarUint(decoder);
/**
* @type {Object<string,any>}
*/
const obj = {};
for (let i = 0; i < len; i++) {
const key = readVarString(decoder);
obj[key] = readAny(decoder);
}
return obj
},
decoder => { // CASE 117: array<any>
const len = readVarUint(decoder);
const arr = [];
for (let i = 0; i < len; i++) {
arr.push(readAny(decoder));
}
return arr
},
readVarUint8Array // CASE 116: Uint8Array
];
/**
* @param {Decoder} decoder
*/
const readAny = decoder => readAnyLookupTable[127 - readUint8(decoder)](decoder);
/**
* T must not be null.
*
* @template T
*/
class RleDecoder extends Decoder {
/**
* @param {Uint8Array} uint8Array
* @param {function(Decoder):T} reader
*/
constructor (uint8Array, reader) {
super(uint8Array);
/**
* The reader
*/
this.reader = reader;
/**
* Current state
* @type {T|null}
*/
this.s = null;
this.count = 0;
}
read () {
if (this.count === 0) {
this.s = this.reader(this);
if (hasContent(this)) {
this.count = readVarUint(this) + 1; // see encoder implementation for the reason why this is incremented
} else {
this.count = -1; // read the current value forever
}
}
this.count--;
return /** @type {T} */ (this.s)
}
}
class UintOptRleDecoder extends Decoder {
/**
* @param {Uint8Array} uint8Array
*/
constructor (uint8Array) {
super(uint8Array);
/**
* @type {number}
*/
this.s = 0;
this.count = 0;
}
read () {
if (this.count === 0) {
this.s = readVarInt(this);
// if the sign is negative, we read the count too, otherwise count is 1
const isNegative = isNegativeZero(this.s);
this.count = 1;
if (isNegative) {
this.s = -this.s;
this.count = readVarUint(this) + 2;
}
}
this.count--;
return /** @type {number} */ (this.s)
}
}
class IntDiffOptRleDecoder extends Decoder {
/**
* @param {Uint8Array} uint8Array
*/
constructor (uint8Array) {
super(uint8Array);
/**
* @type {number}
*/
this.s = 0;
this.count = 0;
this.diff = 0;
}
/**
* @return {number}
*/
read () {
if (this.count === 0) {
const diff = readVarInt(this);
// if the first bit is set, we read more data
const hasCount = diff & 1;
this.diff = floor(diff / 2); // shift >> 1
this.count = 1;
if (hasCount) {
this.count = readVarUint(this) + 2;
}
}
this.s += this.diff;
this.count--;
return this.s
}
}
class StringDecoder {
/**
* @param {Uint8Array} uint8Array
*/
constructor (uint8Array) {
this.decoder = new UintOptRleDecoder(uint8Array);
this.str = readVarString(this.decoder);
/**
* @type {number}
*/
this.spos = 0;
}
/**
* @return {string}
*/
read () {
const end = this.spos + this.decoder.read();
const res = this.str.slice(this.spos, end);
this.spos = end;
return res
}
}
/**
* Utility functions to work with buffers (Uint8Array).
*
* @module buffer
*/
/**
* @param {number} len
*/
const createUint8ArrayFromLen = len => new Uint8Array(len);
/**
* Create Uint8Array with initial content from buffer
*
* @param {ArrayBuffer} buffer
* @param {number} byteOffset
* @param {number} length
*/
const createUint8ArrayViewFromArrayBuffer = (buffer, byteOffset, length) => new Uint8Array(buffer, byteOffset, length);
/**
* Create Uint8Array with initial content from buffer
*
* @param {ArrayBuffer} buffer
*/
const createUint8ArrayFromArrayBuffer = buffer => new Uint8Array(buffer);
/* c8 ignore start */
/**
* @param {Uint8Array} bytes
* @return {string}
*/
const toBase64Browser = bytes => {
let s = '';
for (let i = 0; i < bytes.byteLength; i++) {
s += fromCharCode(bytes[i]);
}
// eslint-disable-next-line no-undef
return btoa(s)
};
/* c8 ignore stop */
/**
* @param {Uint8Array} bytes
* @return {string}
*/
const toBase64Node = bytes => Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString('base64');
/* c8 ignore start */
/**
* @param {string} s
* @return {Uint8Array}
*/
const fromBase64Browser = s => {
// eslint-disable-next-line no-undef
const a = atob(s);
const bytes = createUint8ArrayFromLen(a.length);
for (let i = 0; i < a.length; i++) {
bytes[i] = a.charCodeAt(i);
}
return bytes
};
/* c8 ignore stop */
/**
* @param {string} s
*/
const fromBase64Node = s => {
const buf = Buffer.from(s, 'base64');
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength)
};
/* c8 ignore next */
const toBase64 = isBrowser ? toBase64Browser : toBase64Node;
/* c8 ignore next */
const fromBase64 = isBrowser ? fromBase64Browser : fromBase64Node;
/**
* Copy the content of an Uint8Array view to a new ArrayBuffer.
*
* @param {Uint8Array} uint8Array
* @return {Uint8Array}
*/
const copyUint8Array = uint8Array => {
const newBuf = createUint8ArrayFromLen(uint8Array.byteLength);
newBuf.set(uint8Array);
return newBuf
};
/**
* Efficient schema-less binary encoding with support for variable length encoding.
*
* Use [lib0/encoding] with [lib0/decoding]. Every encoding function has a corresponding decoding function.
*
* Encodes numbers in little-endian order (least to most significant byte order)
* and is compatible with Golang's binary encoding (https://golang.org/pkg/encoding/binary/)
* which is also used in Protocol Buffers.
*
* ```js
* // encoding step
* const encoder = encoding.createEncoder()
* encoding.writeVarUint(encoder, 256)
* encoding.writeVarString(encoder, 'Hello world!')
* const buf = encoding.toUint8Array(encoder)
* ```
*
* ```js
* // decoding step
* const decoder = decoding.createDecoder(buf)
* decoding.readVarUint(decoder) // => 256
* decoding.readVarString(decoder) // => 'Hello world!'
* decoding.hasContent(decoder) // => false - all data is read
* ```
*
* @module encoding
*/
/**
* A BinaryEncoder handles the encoding to an Uint8Array.
*/
class Encoder {
constructor () {
this.cpos = 0;
this.cbuf = new Uint8Array(100);
/**
* @type {Array<Uint8Array>}
*/
this.bufs = [];
}
}
/**
* @function
* @return {Encoder}
*/
const createEncoder = () => new Encoder();
/**
* The current length of the encoded data.
*
* @function
* @param {Encoder} encoder
* @return {number}
*/
const length = encoder => {
let len = encoder.cpos;
for (let i = 0; i < encoder.bufs.length; i++) {
len += encoder.bufs[i].length;
}
return len
};
/**
* Transform to Uint8Array.
*
* @function
* @param {Encoder} encoder
* @return {Uint8Array} The created ArrayBuffer.
*/
const toUint8Array = encoder => {
const uint8arr = new Uint8Array(length(encoder));
let curPos = 0;
for (let i = 0; i < encoder.bufs.length; i++) {
const d = encoder.bufs[i];
uint8arr.set(d, curPos);
curPos += d.length;
}
uint8arr.set(createUint8ArrayViewFromArrayBuffer(encoder.cbuf.buffer, 0, encoder.cpos), curPos);
return uint8arr
};
/**
* Verify that it is possible to write `len` bytes wtihout checking. If
* necessary, a new Buffer with the required length is attached.
*
* @param {Encoder} encoder
* @param {number} len
*/
const verifyLen = (encoder, len) => {
const bufferLen = encoder.cbuf.length;
if (bufferLen - encoder.cpos < len) {
encoder.bufs.push(createUint8ArrayViewFromArrayBuffer(encoder.cbuf.buffer, 0, encoder.cpos));
encoder.cbuf = new Uint8Array(max(bufferLen, len) * 2);
encoder.cpos = 0;
}
};
/**
* Write one byte to the encoder.
*
* @function
* @param {Encoder} encoder
* @param {number} num The byte that is to be encoded.
*/
const write = (encoder, num) => {
const bufferLen = encoder.cbuf.length;
if (encoder.cpos === bufferLen) {
encoder.bufs.push(encoder.cbuf);
encoder.cbuf = new Uint8Array(bufferLen * 2);
encoder.cpos = 0;
}
encoder.cbuf[encoder.cpos++] = num;
};
/**
* Write one byte as an unsigned integer.
*
* @function
* @param {Encoder} encoder
* @param {number} num The number that is to be encoded.
*/
const writeUint8 = write;
/**
* Write a variable length unsigned integer. Max encodable integer is 2^53.
*
* @function
* @param {Encoder} encoder
* @param {number} num The number that is to be encoded.
*/
const writeVarUint = (encoder, num) => {
while (num > BITS7) {
write(encoder, BIT8 | (BITS7 & num));
num = floor(num / 128); // shift >>> 7
}
write(encoder, BITS7 & num);
};
/**
* Write a variable length integer.
*
* We use the 7th bit instead for signaling that this is a negative number.
*
* @function
* @param {Encoder} encoder
* @param {number} num The number that is to be encoded.
*/
const writeVarInt = (encoder, num) => {
const isNegative = isNegativeZero(num);
if (isNegative) {
num = -num;
}
// |- whether to continue reading |- whether is negative |- number
write(encoder, (num > BITS6 ? BIT8 : 0) | (isNegative ? BIT7 : 0) | (BITS6 & num));
num = floor(num / 64); // shift >>> 6
// We don't need to consider the case of num === 0 so we can use a different
// pattern here than above.
while (num > 0) {
write(encoder, (num > BITS7 ? BIT8 : 0) | (BITS7 & num));
num = floor(num / 128); // shift >>> 7
}
};
/**
* A cache to store strings temporarily
*/
const _strBuffer = new Uint8Array(30000);
const _maxStrBSize = _strBuffer.length / 3;
/**
* Write a variable length string.
*
* @function
* @param {Encoder} encoder
* @param {String} str The string that is to be encoded.
*/
const _writeVarStringNative = (encoder, str) => {
if (str.length < _maxStrBSize) {
// We can encode the string into the existing buffer
/* c8 ignore next */
const written = utf8TextEncoder.encodeInto(str, _strBuffer).written || 0;
writeVarUint(encoder, written);
for (let i = 0; i < written; i++) {
write(encoder, _strBuffer[i]);
}
} else {
writeVarUint8Array(encoder, encodeUtf8(str));
}
};
/**
* Write a variable length string.
*
* @function
* @param {Encoder} encoder
* @param {String} str The string that is to be encoded.
*/
const _writeVarStringPolyfill = (encoder, str) => {
const encodedString = unescape(encodeURIComponent(str));
const len = encodedString.length;
writeVarUint(encoder, len);
for (let i = 0; i < len; i++) {
write(encoder, /** @type {number} */ (encodedString.codePointAt(i)));
}
};
/**
* Write a variable length string.
*
* @function
* @param {Encoder} encoder
* @param {String} str The string that is to be encoded.
*/
/* c8 ignore next */
const writeVarString = (utf8TextEncoder && /** @type {any} */ (utf8TextEncoder).encodeInto) ? _writeVarStringNative : _writeVarStringPolyfill;
/**
* Append fixed-length Uint8Array to the encoder.
*
* @function
* @param {Encoder} encoder
* @param {Uint8Array} uint8Array
*/
const writeUint8Array = (encoder, uint8Array) => {
const bufferLen = encoder.cbuf.length;
const cpos = encoder.cpos;
const leftCopyLen = min(bufferLen - cpos, uint8Array.length);
const rightCopyLen = uint8Array.length - leftCopyLen;
encoder.cbuf.set(uint8Array.subarray(0, leftCopyLen), cpos);
encoder.cpos += leftCopyLen;
if (rightCopyLen > 0) {
// Still something to write, write right half..
// Append new buffer
encoder.bufs.push(encoder.cbuf);
// must have at least size of remaining buffer
encoder.cbuf = new Uint8Array(max(bufferLen * 2, rightCopyLen));
// copy array
encoder.cbuf.set(uint8Array.subarray(leftCopyLen));
encoder.cpos = rightCopyLen;
}
};
/**
* Append an Uint8Array to Encoder.
*
* @function
* @param {Encoder} encoder
* @param {Uint8Array} uint8Array
*/
const writeVarUint8Array = (encoder, uint8Array) => {
writeVarUint(encoder, uint8Array.byteLength);
writeUint8Array(encoder, uint8Array);
};
/**
* Create an DataView of the next `len` bytes. Use it to write data after
* calling this function.
*
* ```js
* // write float32 using DataView
* const dv = writeOnDataView(encoder, 4)
* dv.setFloat32(0, 1.1)
* // read float32 using DataView
* const dv = readFromDataView(encoder, 4)
* dv.getFloat32(0) // => 1.100000023841858 (leaving it to the reader to find out why this is the correct result)
* ```
*
* @param {Encoder} encoder
* @param {number} len
* @return {DataView}
*/
const writeOnDataView = (encoder, len) => {
verifyLen(encoder, len);
const dview = new DataView(encoder.cbuf.buffer, encoder.cpos, len);
encoder.cpos += len;
return dview
};
/**
* @param {Encoder} encoder
* @param {number} num
*/
const writeFloat32 = (encoder, num) => writeOnDataView(encoder, 4).setFloat32(0, num, false);
/**
* @param {Encoder} encoder
* @param {number} num
*/
const writeFloat64 = (encoder, num) => writeOnDataView(encoder, 8).setFloat64(0, num, false);
/**
* @param {Encoder} encoder
* @param {bigint} num
*/
const writeBigInt64 = (encoder, num) => /** @type {any} */ (writeOnDataView(encoder, 8)).setBigInt64(0, num, false);
const floatTestBed = new DataView(new ArrayBuffer(4));
/**
* Check if a number can be encoded as a 32 bit float.
*
* @param {number} num
* @return {boolean}
*/
const isFloat32 = num => {
floatTestBed.setFloat32(0, num);
return floatTestBed.getFloat32(0) === num
};
/**
* Encode data with efficient binary format.
*
* Differences to JSON:
* • Transforms data to a binary format (not to a string)
* • Encodes undefined, NaN, and ArrayBuffer (these can't be represented in JSON)
* • Numbers are efficiently encoded either as a variable length integer, as a
* 32 bit float, as a 64 bit float, or as a 64 bit bigint.
*
* Encoding table:
*
* | Data Type | Prefix | Encoding Method | Comment |
* | ------------------- | -------- | ------------------ | ------- |
* | undefined | 127 | | Functions, symbol, and everything that cannot be identified is encoded as undefined |
* | null | 126 | | |
* | integer | 125 | writeVarInt | Only encodes 32 bit signed integers |
* | float32 | 124 | writeFloat32 | |
* | float64 | 123 | writeFloat64 | |
* | bigint | 122 | writeBigInt64 | |
* | boolean (false) | 121 | | True and false are different data types so we save the following byte |
* | boolean (true) | 120 | | - 0b01111000 so the last bit determines whether true or false |
* | string | 119 | writeVarString | |
* | object<string,any> | 118 | custom | Writes {length} then {length} key-value pairs |
* | array<any> | 117 | custom | Writes {length} then {length} json values |
* | Uint8Array | 116 | writeVarUint8Array | We use Uint8Array for any kind of binary data |
*
* Reasons for the decreasing prefix:
* We need the first bit for extendability (later we may want to encode the
* prefix with writeVarUint). The remaining 7 bits are divided as follows:
* [0-30] the beginning of the data range is used for custom purposes
* (defined by the function that uses this library)
* [31-127] the end of the data range is used for data encoding by
* lib0/encoding.js
*
* @param {Encoder} encoder
* @param {undefined|null|number|bigint|boolean|string|Object<string,any>|Array<any>|Uint8Array} data
*/
const writeAny = (encoder, data) => {
switch (typeof data) {
case 'string':
// TYPE 119: STRING
write(encoder, 119);
writeVarString(encoder, data);
break
case 'number':
if (isInteger(data) && abs(data) <= BITS31) {
// TYPE 125: INTEGER
write(encoder, 125);
writeVarInt(encoder, data);
} else if (isFloat32(data)) {
// TYPE 124: FLOAT32
write(encoder, 124);
writeFloat32(encoder, data);
} else {
// TYPE 123: FLOAT64
write(encoder, 123);
writeFloat64(encoder, data);
}
break
case 'bigint':
// TYPE 122: BigInt
write(encoder, 122);
writeBigInt64(encoder, data);
break
case 'object':
if (data === null) {
// TYPE 126: null
write(encoder, 126);
} else if (isArray(data)) {
// TYPE 117: Array
write(encoder, 117);
writeVarUint(encoder, data.length);
for (let i = 0; i < data.length; i++) {
writeAny(encoder, data[i]);
}
} else if (data instanceof Uint8Array) {
// TYPE 116: ArrayBuffer
write(encoder, 116);
writeVarUint8Array(encoder, data);
} else {
// TYPE 118: Object
write(encoder, 118);
const keys = Object.keys(data);
writeVarUint(encoder, keys.length);
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
writeVarString(encoder, key);
writeAny(encoder, data[key]);
}
}
break
case 'boolean':
// TYPE 120/121: boolean (true/false)
write(encoder, data ? 120 : 121);
break
default:
// TYPE 127: undefined
write(encoder, 127);
}
};
/**
* Now come a few stateful encoder that have their own classes.
*/
/**
* Basic Run Length Encoder - a basic compression implementation.
*
* Encodes [1,1,1,7] to [1,3,7,1] (3 times 1, 1 time 7). This encoder might do more harm than good if there are a lot of values that are not repeated.
*
* It was originally used for image compression. Cool .. article http://csbruce.com/cbm/transactor/pdfs/trans_v7_i06.pdf
*
* @note T must not be null!
*
* @template T
*/
class RleEncoder extends Encoder {
/**
* @param {function(Encoder, T):void} writer
*/
constructor (writer) {
super();
/**
* The writer
*/
this.w = writer;
/**
* Current state
* @type {T|null}
*/
this.s = null;
this.count = 0;
}
/**
* @param {T} v
*/
write (v) {
if (this.s === v) {
this.count++;
} else {
if (this.count > 0) {
// flush counter, unless this is the first value (count = 0)
writeVarUint(this, this.count - 1); // since count is always > 0, we can decrement by one. non-standard encoding ftw
}
this.count = 1;
// write first value
this.w(this, v);
this.s = v;
}
}
}
/**
* @param {UintOptRleEncoder} encoder
*/
const flushUintOptRleEncoder = encoder => {
if (encoder.count > 0) {
// flush counter, unless this is the first value (count = 0)
// case 1: just a single value. set sign to positive
// case 2: write several values. set sign to negative to indicate that there is a length coming
writeVarInt(encoder.encoder, encoder.count === 1 ? encoder.s : -encoder.s);
if (encoder.count > 1) {
writeVarUint(encoder.encoder, encoder.count - 2); // since count is always > 1, we can decrement by one. non-standard encoding ftw
}
}
};
/**
* Optimized Rle encoder that does not suffer from the mentioned problem of the basic Rle encoder.
*
* Internally uses VarInt encoder to write unsigned integers. If the input occurs multiple times, we write
* write it as a negative number. The UintOptRleDecoder then understands that it needs to read a count.
*
* Encodes [1,2,3,3,3] as [1,2,-3,3] (once 1, once 2, three times 3)
*/
class UintOptRleEncoder {
constructor () {
this.encoder = new Encoder();
/**
* @type {number}
*/
this.s = 0;
this.count = 0;
}
/**
* @param {number} v
*/
write (v) {
if (this.s === v) {
this.count++;
} else {
flushUintOptRleEncoder(this);
this.count = 1;
this.s = v;
}
}
toUint8Array () {
flushUintOptRleEncoder(this);
return toUint8Array(this.encoder)
}
}
/**
* @param {IntDiffOptRleEncoder} encoder
*/
const flushIntDiffOptRleEncoder = encoder => {
if (encoder.count > 0) {
// 31 bit making up the diff | wether to write the counter
// const encodedDiff = encoder.diff << 1 | (encoder.count === 1 ? 0 : 1)
const encodedDiff = encoder.diff * 2 + (encoder.count === 1 ? 0 : 1);
// flush counter, unless this is the first value (count = 0)
// case 1: just a single value. set first bit to positive
// case 2: write several values. set first bit to negative to indicate that there is a length coming
writeVarInt(encoder.encoder, encodedDiff);
if (encoder.count > 1) {
writeVarUint(encoder.encoder, encoder.count - 2); // since count is always > 1, we can decrement by one. non-standard encoding ftw
}
}
};
/**
* A combination of the IntDiffEncoder and the UintOptRleEncoder.
*
* The count approach is similar to the UintDiffOptRleEncoder, but instead of using the negative bitflag, it encodes
* in the LSB whether a count is to be read. Therefore this Encoder only supports 31 bit integers!
*
* Encodes [1, 2, 3, 2] as [3, 1, 6, -1] (more specifically [(1 << 1) | 1, (3 << 0) | 0, -1])
*
* Internally uses variable length encoding. Contrary to normal UintVar encoding, the first byte contains:
* * 1 bit that denotes whether the next value is a count (LSB)
* * 1 bit that denotes whether this value is negative (MSB - 1)
* * 1 bit that denotes whether to continue reading the variable length integer (MSB)
*
* Therefore, only five bits remain to encode diff ranges.
*
* Use this Encoder only when appropriate. In most cases, this is probably a bad idea.
*/
class IntDiffOptRleEncoder {
constructor () {
this.encoder = new Encoder();
/**
* @type {number}
*/
this.s = 0;
this.count = 0;
this.diff = 0;
}
/**
* @param {number} v
*/
write (v) {
if (this.diff === v - this.s) {
this.s = v;
this.count++;
} else {
flushIntDiffOptRleEncoder(this);
this.count = 1;
this.diff = v - this.s;
this.s = v;
}
}
toUint8Array () {
flushIntDiffOptRleEncoder(this);
return toUint8Array(this.encoder)
}
}
/**
* Optimized String Encoder.
*
* Encoding many small strings in a simple Encoder is not very efficient. The function call to decode a string takes some time and creates references that must be eventually deleted.
* In practice, when decoding several million small strings, the GC will kick in more and more often to collect orphaned string objects (or maybe there is another reason?).
*
* This string encoder solves the above problem. All strings are concatenated and written as a single string using a single encoding call.
*
* The lengths are encoded using a UintOptRleEncoder.
*/
class StringEncoder {
constructor () {
/**
* @type {Array<string>}
*/
this.sarr = [];
this.s = '';
this.lensE = new UintOptRleEncoder();
}
/**
* @param {string} string
*/
write (string) {
this.s += string;
if (this.s.length > 19) {
this.sarr.push(this.s);
this.s = '';
}
this.lensE.write(string.length);
}
toUint8Array () {
const encoder = new Encoder();
this.sarr.push(this.s);
this.s = '';
writeVarString(encoder, this.sarr.join(''));
writeUint8Array(encoder, this.lensE.toUint8Array());
return toUint8Array(encoder)
}
}
/* eslint-env browser */
const getRandomValues = crypto.getRandomValues.bind(crypto);
const uint32 = () => getRandomValues(new Uint32Array(1))[0];
// @ts-ignore
const uuidv4Template = [1e7] + -1e3 + -4e3 + -8e3 + -1e11;
const uuidv4 = () => uuidv4Template.replace(/[018]/g, /** @param {number} c */ c =>
(c ^ uint32() & 15 >> c / 4).toString(16)
);
/**
* Utility module to work with time.
*
* @module time
*/
/**
* Return current unix time.
*
* @return {number}
*/
const getUnixTime = Date.now;
/**
* Utility helpers to work with promises.
*
* @module promise
*/
/**
* @template T
* @callback PromiseResolve
* @param {T|PromiseLike<T>} [result]
*/
/**
* @template T
* @param {function(PromiseResolve<T>,function(Error):void):any} f
* @return {Promise<T>}
*/
const create$2 = f => /** @type {Promise<T>} */ (new Promise(f));
/**
* Working with value pairs.
*
* @module pair
*/
/**
* @template L,R
*/
class Pair {
/**
* @param {L} left
* @param {R} right
*/
constructor (left, right) {
this.left = left;
this.right = right;
}
}
/**
* @template L,R
* @param {L} left
* @param {R} right
* @return {Pair<L,R>}
*/
const create$1 = (left, right) => new Pair(left, right);
/* eslint-env browser */
/* c8 ignore start */
/**
* @type {Document}
*/
const doc = /** @type {Document} */ (typeof document !== 'undefined' ? document : {});
/** @type {DOMParser} */ (typeof DOMParser !== 'undefined' ? new DOMParser() : null);
/**
* @param {Map<string,string>} m
* @return {string}
*/
const mapToStyleString = m => map$1(m, (value, key) => `${key}:${value};`).join('');
doc.ELEMENT_NODE;
doc.TEXT_NODE;
doc.CDATA_SECTION_NODE;
doc.COMMENT_NODE;
doc.DOCUMENT_NODE;
doc.DOCUMENT_TYPE_NODE;
doc.DOCUMENT_FRAGMENT_NODE;
/* c8 ignore stop */
/**
* Utility module to work with EcmaScript Symbols.
*
* @module symbol
*/
/**
* Return fresh symbol.
*
* @return {Symbol}
*/
const create = Symbol;
const BOLD = create();
const UNBOLD = create();
const BLUE = create();
const GREY = create();
const GREEN = create();
const RED = create();
const PURPLE = create();
const ORANGE = create();
const UNCOLOR = create();
/* c8 ignore start */
/**
* @param {Array<string|Symbol|Object|number>} args
* @return {Array<string|object|number>}
*/
const computeNoColorLoggingArgs = args => {
const logArgs = [];
// try with formatting until we find something unsupported
let i = 0;
for (; i < args.length; i++) {
const arg = args[i];
if (arg.constructor === String || arg.constructor === Number) ; else if (arg.constructor === Object) {
logArgs.push(JSON.stringify(arg));
}
}
return logArgs
};
/* c8 ignore stop */
/**
* Isomorphic logging module with support for colors!
*
* @module logging
*/
/**
* @type {Object<Symbol,pair.Pair<string,string>>}
*/
const _browserStyleMap = {
[BOLD]: create$1('font-weight', 'bold'),
[UNBOLD]: create$1('font-weight', 'normal'),
[BLUE]: create$1('color', 'blue'),
[GREEN]: create$1('color', 'green'),
[GREY]: create$1('color', 'grey'),
[RED]: create$1('color', 'red'),
[PURPLE]: create$1('color', 'purple'),
[ORANGE]: create$1('color', 'orange'), // not well supported in chrome when debugging node with inspector - TODO: deprecate
[UNCOLOR]: create$1('color', 'black')
};
/**
* @param {Array<string|Symbol|Object|number>} args
* @return {Array<string|object|number>}
*/
/* c8 ignore start */
const computeBrowserLoggingArgs = (args) => {
const strBuilder = [];
const styles = [];
const currentStyle = create$5();
/**
* @type {Array<string|Object|number>}
*/
let logArgs = [];
// try with formatting until we find something unsupported
let i = 0;
for (; i < args.length; i++) {
const arg = args[i];
// @ts-ignore
const style = _browserStyleMap[arg];
if (style !== undefined) {
currentStyle.set(style.left, style.right);
} else {
if (arg.constructor === String || arg.constructor === Number) {
const style = mapToStyleString(currentStyle);
if (i > 0 || style.length > 0) {
strBuilder.push('%c' + arg);
styles.push(style);
} else {
strBuilder.push(arg);
}
} else {
break
}
}
}
if (i > 0) {
// create logArgs with what we have so far
logArgs = styles;
logArgs.unshift(strBuilder.join(''));
}
// append the rest
for (; i < args.length; i++) {
const arg = args[i];
if (!(arg instanceof Symbol)) {
logArgs.push(arg);
}
}
return logArgs
};
/* c8 ignore stop */
/* c8 ignore start */
const computeLoggingArgs = supportsColor
? computeBrowserLoggingArgs
: computeNoColorLoggingArgs;
/* c8 ignore stop */
/**
* @param {Array<string|Symbol|Object|number>} args
*/
const print = (...args) => {
console.log(...computeLoggingArgs(args));
/* c8 ignore next */
vconsoles.forEach((vc) => vc.print(args));
};
const vconsoles = create$4();
/**
* Utility module to create and manipulate Iterators.
*
* @module iterator
*/
/**
* @template T
* @param {function():IteratorResult<T>} next
* @return {IterableIterator<T>}
*/
const createIterator = next => ({
/**
* @return {IterableIterator<T>}
*/
[Symbol.iterator] () {
return this
},
// @ts-ignore
next
});
/**
* @template T
* @param {Iterator<T>} iterator
* @param {function(T):boolean} filter
*/
const iteratorFilter = (iterator, filter) => createIterator(() => {
let res;
do {
res = iterator.next();
} while (!res.done && !filter(res.value))
return res
});
/**
* @template T,M
* @param {Iterator<T>} iterator
* @param {function(T):M} fmap
*/
const iteratorMap = (iterator, fmap) => createIterator(() => {
const { done, value } = iterator.next();
return { done, value: done ? undefined : fmap(value) }
});
class DeleteItem {
/**
* @param {number} clock
* @param {number} len
*/
constructor (clock, len) {
/**
* @type {number}
*/
this.clock = clock;
/**
* @type {number}
*/
this.len = len;
}
}
/**
* We no longer maintain a DeleteStore. DeleteSet is a temporary object that is created when needed.
* - When created in a transaction, it must only be accessed after sorting, and merging
* - This DeleteSet is send to other clients
* - We do not create a DeleteSet when we send a sync message. The DeleteSet message is created directly from StructStore
* - We read a DeleteSet as part of a sync/update message. In this case the DeleteSet is already sorted and merged.
*/
class DeleteSet {
constructor () {
/**
* @type {Map<number,Array<DeleteItem>>}
*/
this.clients = new Map();
}
}
/**
* Iterate over all structs that the DeleteSet gc's.
*
* @param {Transaction} transaction
* @param {DeleteSet} ds
* @param {function(GC|Item):void} f
*
* @function
*/
const iterateDeletedStructs = (transaction, ds, f) =>
ds.clients.forEach((deletes, clientid) => {
const structs = /** @type {Array<GC|Item>} */ (transaction.doc.store.clients.get(clientid));
for (let i = 0; i < deletes.length; i++) {
const del = deletes[i];
iterateStructs(transaction, structs, del.clock, del.len, f);
}
});
/**
* @param {Array<DeleteItem>} dis
* @param {number} clock
* @return {number|null}
*
* @private
* @function
*/
const findIndexDS = (dis, clock) => {
let left = 0;
let right = dis.length - 1;
while (left <= right) {
const midindex = floor((left + right) / 2);
const mid = dis[midindex];
const midclock = mid.clock;
if (midclock <= clock) {
if (clock < midclock + mid.len) {
return midindex
}
left = midindex + 1;
} else {
right = midindex - 1;
}
}
return null
};
/**
* @param {DeleteSet} ds
* @param {ID} id
* @return {boolean}
*
* @private
* @function
*/
const isDeleted = (ds, id) => {
const dis = ds.clients.get(id.client);
return dis !== undefined && findIndexDS(dis, id.clock) !== null
};
/**
* @param {DeleteSet} ds
*
* @private
* @function
*/
const sortAndMergeDeleteSet = ds => {
ds.clients.forEach(dels => {
dels.sort((a, b) => a.clock - b.clock);
// merge items without filtering or splicing the array
// i is the current pointer
// j refers to the current insert position for the pointed item
// try to merge dels[i] into dels[j-1] or set dels[j]=dels[i]
let i, j;
for (i = 1, j = 1; i < dels.length; i++) {
const left = dels[j - 1];
const right = dels[i];
if (left.clock + left.len >= right.clock) {
left.len = max(left.len, right.clock + right.len - left.clock);
} else {
if (j < i) {
dels[j] = right;
}
j++;
}
}
dels.length = j;
});
};
/**
* @param {Array<DeleteSet>} dss
* @return {DeleteSet} A fresh DeleteSet
*/
const mergeDeleteSets = dss => {
const merged = new DeleteSet();
for (let dssI = 0; dssI < dss.length; dssI++) {
dss[dssI].clients.forEach((delsLeft, client) => {
if (!merged.clients.has(client)) {
// Write all missing keys from current ds and all following.
// If merged already contains `client` current ds has already been added.
/**
* @type {Array<DeleteItem>}
*/
const dels = delsLeft.slice();
for (let i = dssI + 1; i < dss.length; i++) {
appendTo(dels, dss[i].clients.get(client) || []);
}
merged.clients.set(client, dels);
}
});
}
sortAndMergeDeleteSet(merged);
return merged
};
/**
* @param {DeleteSet} ds
* @param {number} client
* @param {number} clock
* @param {number} length
*
* @private
* @function
*/
const addToDeleteSet = (ds, client, clock, length) => {
setIfUndefined(ds.clients, client, () => /** @type {Array<DeleteItem>} */ ([])).push(new DeleteItem(clock, length));
};
const createDeleteSet = () => new DeleteSet();
/**
* @param {StructStore} ss
* @return {DeleteSet} Merged and sorted DeleteSet
*
* @private
* @function
*/
const createDeleteSetFromStructStore = ss => {
const ds = createDeleteSet();
ss.clients.forEach((structs, client) => {
/**
* @type {Array<DeleteItem>}
*/
const dsitems = [];
for (let i = 0; i < structs.length; i++) {
const struct = structs[i];
if (struct.deleted) {
const clock = struct.id.clock;
let len = struct.length;
if (i + 1 < structs.length) {
for (let next = structs[i + 1]; i + 1 < structs.length && next.deleted; next = structs[++i + 1]) {
len += next.length;
}
}
dsitems.push(new DeleteItem(clock, len));
}
}
if (dsitems.length > 0) {
ds.clients.set(client, dsitems);
}
});
return ds
};
/**
* @param {DSEncoderV1 | DSEncoderV2} encoder
* @param {DeleteSet} ds
*
* @private
* @function
*/
const writeDeleteSet = (encoder, ds) => {
writeVarUint(encoder.restEncoder, ds.clients.size);
// Ensure that the delete set is written in a deterministic order
from(ds.clients.entries())
.sort((a, b) => b[0] - a[0])
.forEach(([client, dsitems]) => {
encoder.resetDsCurVal();
writeVarUint(encoder.restEncoder, client);
const len = dsitems.length;
writeVarUint(encoder.restEncoder, len);
for (let i = 0; i < len; i++) {
const item = dsitems[i];
encoder.writeDsClock(item.clock);
encoder.writeDsLen(item.len);
}
});
};
/**
* @param {DSDecoderV1 | DSDecoderV2} decoder
* @return {DeleteSet}
*
* @private
* @function
*/
const readDeleteSet = decoder => {
const ds = new DeleteSet();
const numClients = readVarUint(decoder.restDecoder);
for (let i = 0; i < numClients; i++) {
decoder.resetDsCurVal();
const client = readVarUint(decoder.restDecoder);
const numberOfDeletes = readVarUint(decoder.restDecoder);
if (numberOfDeletes > 0) {
const dsField = setIfUndefined(ds.clients, client, () => /** @type {Array<DeleteItem>} */ ([]));
for (let i = 0; i < numberOfDeletes; i++) {
dsField.push(new DeleteItem(decoder.readDsClock(), decoder.readDsLen()));
}
}
}
return ds
};
/**
* @todo YDecoder also contains references to String and other Decoders. Would make sense to exchange YDecoder.toUint8Array for YDecoder.DsToUint8Array()..
*/
/**
* @param {DSDecoderV1 | DSDecoderV2} decoder
* @param {Transaction} transaction
* @param {StructStore} store
* @return {Uint8Array|null} Returns a v2 update containing all deletes that couldn't be applied yet; or null if all deletes were applied successfully.
*
* @private
* @function
*/
const readAndApplyDeleteSet = (decoder, transaction, store) => {
const unappliedDS = new DeleteSet();
const numClients = readVarUint(decoder.restDecoder);
for (let i = 0; i < numClients; i++) {
decoder.resetDsCurVal();
const client = readVarUint(decoder.restDecoder);
const numberOfDeletes = readVarUint(decoder.restDecoder);
const structs = store.clients.get(client) || [];
const state = getState(store, client);
for (let i = 0; i < numberOfDeletes; i++) {
const clock = decoder.readDsClock();
const clockEnd = clock + decoder.readDsLen();
if (clock < state) {
if (state < clockEnd) {
addToDeleteSet(unappliedDS, client, state, clockEnd - state);
}
let index = findIndexSS(structs, clock);
/**
* We can ignore the case of GC and Delete structs, because we are going to skip them
* @type {Item}
*/
// @ts-ignore
let struct = structs[index];
// split the first item if necessary
if (!struct.deleted && struct.id.clock < clock) {
structs.splice(index + 1, 0, splitItem(transaction, struct, clock - struct.id.clock));
index++; // increase we now want to use the next struct
}
while (index < structs.length) {
// @ts-ignore
struct = structs[index++];
if (struct.id.clock < clockEnd) {
if (!struct.deleted) {
if (clockEnd < struct.id.clock + struct.length) {
structs.splice(index, 0, splitItem(transaction, struct, clockEnd - struct.id.clock));
}
struct.delete(transaction);
}
} else {
break
}
}
} else {
addToDeleteSet(unappliedDS, client, clock, clockEnd - clock);
}
}
}
if (unappliedDS.clients.size > 0) {
const ds = new UpdateEncoderV2();
writeVarUint(ds.restEncoder, 0); // encode 0 structs
writeDeleteSet(ds, unappliedDS);
return ds.toUint8Array()
}
return null
};
/**
* @module Y
*/
const generateNewClientId = uint32;
/**
* @typedef {Object} DocOpts
* @property {boolean} [DocOpts.gc=true] Disable garbage collection (default: gc=true)
* @property {function(Item):boolean} [DocOpts.gcFilter] Will be called before an Item is garbage collected. Return false to keep the Item.
* @property {string} [DocOpts.guid] Define a globally unique identifier for this document
* @property {string | null} [DocOpts.collectionid] Associate this document with a collection. This only plays a role if your provider has a concept of collection.
* @property {any} [DocOpts.meta] Any kind of meta information you want to associate with this document. If this is a subdocument, remote peers will store the meta information as well.
* @property {boolean} [DocOpts.autoLoad] If a subdocument, automatically load document. If this is a subdocument, remote peers will load the document as well automatically.
* @property {boolean} [DocOpts.shouldLoad] Whether the document should be synced by the provider now. This is toggled to true when you call ydoc.load()
*/
/**
* A Yjs instance handles the state of shared data.
* @extends Observable<string>
*/
class Doc extends Observable {
/**
* @param {DocOpts} opts configuration
*/
constructor ({ guid = uuidv4(), collectionid = null, gc = true, gcFilter = () => true, meta = null, autoLoad = false, shouldLoad = true } = {}) {
super();
this.gc = gc;
this.gcFilter = gcFilter;
this.clientID = generateNewClientId();
this.guid = guid;
this.collectionid = collectionid;
/**
* @type {Map<string, AbstractType<YEvent<any>>>}
*/
this.share = new Map();
this.store = new StructStore();
/**
* @type {Transaction | null}
*/
this._transaction = null;
/**
* @type {Array<Transaction>}
*/
this._transactionCleanups = [];
/**
* @type {Set<Doc>}
*/
this.subdocs = new Set();
/**
* If this document is a subdocument - a document integrated into another document - then _item is defined.
* @type {Item?}
*/
this._item = null;
this.shouldLoad = shouldLoad;
this.autoLoad = autoLoad;
this.meta = meta;
/**
* This is set to true when the persistence provider loaded the document from the database or when the `sync` event fires.
* Note that not all providers implement this feature. Provider authors are encouraged to fire the `load` event when the doc content is loaded from the database.
*
* @type {boolean}
*/
this.isLoaded = false;
/**
* This is set to true when the connection provider has successfully synced with a backend.
* Note that when using peer-to-peer providers this event may not provide very useful.
* Also note that not all providers implement this feature. Provider authors are encouraged to fire
* the `sync` event when the doc has been synced (with `true` as a parameter) or if connection is
* lost (with false as a parameter).
*/
this.isSynced = false;
/**
* Promise that resolves once the document has been loaded from a presistence provider.
*/
this.whenLoaded = create$2(resolve => {
this.on('load', () => {
this.isLoaded = true;
resolve(this);
});
});
const provideSyncedPromise = () => create$2(resolve => {
/**
* @param {boolean} isSynced
*/
const eventHandler = (isSynced) => {
if (isSynced === undefined || isSynced === true) {
this.off('sync', eventHandler);
resolve();
}
};
this.on('sync', eventHandler);
});
this.on('sync', isSynced => {
if (isSynced === false && this.isSynced) {
this.whenSynced = provideSyncedPromise();
}
this.isSynced = isSynced === undefined || isSynced === true;
if (!this.isLoaded) {
this.emit('load', []);
}
});
/**
* Promise that resolves once the document has been synced with a backend.
* This promise is recreated when the connection is lost.
* Note the documentation about the `isSynced` property.
*/
this.whenSynced = provideSyncedPromise();
}
/**
* Notify the parent document that you request to load data into this subdocument (if it is a subdocument).
*
* `load()` might be used in the future to request any provider to load the most current data.
*
* It is safe to call `load()` multiple times.
*/
load () {
const item = this._item;
if (item !== null && !this.shouldLoad) {
transact(/** @type {any} */ (item.parent).doc, transaction => {
transaction.subdocsLoaded.add(this);
}, null, true);
}
this.shouldLoad = true;
}
getSubdocs () {
return this.subdocs
}
getSubdocGuids () {
return new Set(from(this.subdocs).map(doc => doc.guid))
}
/**
* Changes that happen inside of a transaction are bundled. This means that
* the observer fires _after_ the transaction is finished and that all changes
* that happened inside of the transaction are sent as one message to the
* other peers.
*
* @template T
* @param {function(Transaction):T} f The function that should be executed as a transaction
* @param {any} [origin] Origin of who started the transaction. Will be stored on transaction.origin
* @return T
*
* @public
*/
transact (f, origin = null) {
return transact(this, f, origin)
}
/**
* Define a shared data type.
*
* Multiple calls of `y.get(name, TypeConstructor)` yield the same result
* and do not overwrite each other. I.e.
* `y.define(name, Y.Array) === y.define(name, Y.Array)`
*
* After this method is called, the type is also available on `y.share.get(name)`.
*
* *Best Practices:*
* Define all types right after the Yjs instance is created and store them in a separate object.
* Also use the typed methods `getText(name)`, `getArray(name)`, ..
*
* @example
* const y = new Y(..)
* const appState = {
* document: y.getText('document')
* comments: y.getArray('comments')
* }
*
* @param {string} name
* @param {Function} TypeConstructor The constructor of the type definition. E.g. Y.Text, Y.Array, Y.Map, ...
* @return {AbstractType<any>} The created type. Constructed with TypeConstructor
*
* @public
*/
get (name, TypeConstructor = AbstractType) {
const type = setIfUndefined(this.share, name, () => {
// @ts-ignore
const t = new TypeConstructor();
t._integrate(this, null);
return t
});
const Constr = type.constructor;
if (TypeConstructor !== AbstractType && Constr !== TypeConstructor) {
if (Constr === AbstractType) {
// @ts-ignore
const t = new TypeConstructor();
t._map = type._map;
type._map.forEach(/** @param {Item?} n */ n => {
for (; n !== null; n = n.left) {
// @ts-ignore
n.parent = t;
}
});
t._start = type._start;
for (let n = t._start; n !== null; n = n.right) {
n.parent = t;
}
t._length = type._length;
this.share.set(name, t);
t._integrate(this, null);
return t
} else {
throw new Error(`Type with the name ${name} has already been defined with a different constructor`)
}
}
return type
}
/**
* @template T
* @param {string} [name]
* @return {YArray<T>}
*
* @public
*/
getArray (name = '') {
// @ts-ignore
return this.get(name, YArray)
}
/**
* @param {string} [name]
* @return {YText}
*
* @public
*/
getText (name = '') {
// @ts-ignore
return this.get(name, YText)
}
/**
* @template T
* @param {string} [name]
* @return {YMap<T>}
*
* @public
*/
getMap (name = '') {
// @ts-ignore
return this.get(name, YMap)
}
/**
* @param {string} [name]
* @return {YXmlFragment}
*
* @public
*/
getXmlFragment (name = '') {
// @ts-ignore
return this.get(name, YXmlFragment)
}
/**
* Converts the entire document into a js object, recursively traversing each yjs type
* Doesn't log types that have not been defined (using ydoc.getType(..)).
*
* @deprecated Do not use this method and rather call toJSON directly on the shared types.
*
* @return {Object<string, any>}
*/
toJSON () {
/**
* @type {Object<string, any>}
*/
const doc = {};
this.share.forEach((value, key) => {
doc[key] = value.toJSON();
});
return doc
}
/**
* Emit `destroy` event and unregister all event handlers.
*/
destroy () {
from(this.subdocs).forEach(subdoc => subdoc.destroy());
const item = this._item;
if (item !== null) {
this._item = null;
const content = /** @type {ContentDoc} */ (item.content);
content.doc = new Doc({ guid: this.guid, ...content.opts, shouldLoad: false });
content.doc._item = item;
transact(/** @type {any} */ (item).parent.doc, transaction => {
const doc = content.doc;
if (!item.deleted) {
transaction.subdocsAdded.add(doc);
}
transaction.subdocsRemoved.add(this);
}, null, true);
}
this.emit('destroyed', [true]);
this.emit('destroy', [this]);
super.destroy();
}
/**
* @param {string} eventName
* @param {function(...any):any} f
*/
on (eventName, f) {
super.on(eventName, f);
}
/**
* @param {string} eventName
* @param {function} f
*/
off (eventName, f) {
super.off(eventName, f);
}
}
class DSDecoderV1 {
/**
* @param {decoding.Decoder} decoder
*/
constructor (decoder) {
this.restDecoder = decoder;
}
resetDsCurVal () {
// nop
}
/**
* @return {number}
*/
readDsClock () {
return readVarUint(this.restDecoder)
}
/**
* @return {number}
*/
readDsLen () {
return readVarUint(this.restDecoder)
}
}
class UpdateDecoderV1 extends DSDecoderV1 {
/**
* @return {ID}
*/
readLeftID () {
return createID(readVarUint(this.restDecoder), readVarUint(this.restDecoder))
}
/**
* @return {ID}
*/
readRightID () {
return createID(readVarUint(this.restDecoder), readVarUint(this.restDecoder))
}
/**
* Read the next client id.
* Use this in favor of readID whenever possible to reduce the number of objects created.
*/
readClient () {
return readVarUint(this.restDecoder)
}
/**
* @return {number} info An unsigned 8-bit integer
*/
readInfo () {
return readUint8(this.restDecoder)
}
/**
* @return {string}
*/
readString () {
return readVarString(this.restDecoder)
}
/**
* @return {boolean} isKey
*/
readParentInfo () {
return readVarUint(this.restDecoder) === 1
}
/**
* @return {number} info An unsigned 8-bit integer
*/
readTypeRef () {
return readVarUint(this.restDecoder)
}
/**
* Write len of a struct - well suited for Opt RLE encoder.
*
* @return {number} len
*/
readLen () {
return readVarUint(this.restDecoder)
}
/**
* @return {any}
*/
readAny () {
return readAny(this.restDecoder)
}
/**
* @return {Uint8Array}
*/
readBuf () {
return copyUint8Array(readVarUint8Array(this.restDecoder))
}
/**
* Legacy implementation uses JSON parse. We use any-decoding in v2.
*
* @return {any}
*/
readJSON () {
return JSON.parse(readVarString(this.restDecoder))
}
/**
* @return {string}
*/
readKey () {
return readVarString(this.restDecoder)
}
}
class DSDecoderV2 {
/**
* @param {decoding.Decoder} decoder
*/
constructor (decoder) {
/**
* @private
*/
this.dsCurrVal = 0;
this.restDecoder = decoder;
}
resetDsCurVal () {
this.dsCurrVal = 0;
}
/**
* @return {number}
*/
readDsClock () {
this.dsCurrVal += readVarUint(this.restDecoder);
return this.dsCurrVal
}
/**
* @return {number}
*/
readDsLen () {
const diff = readVarUint(this.restDecoder) + 1;
this.dsCurrVal += diff;
return diff
}
}
class UpdateDecoderV2 extends DSDecoderV2 {
/**
* @param {decoding.Decoder} decoder
*/
constructor (decoder) {
super(decoder);
/**
* List of cached keys. If the keys[id] does not exist, we read a new key
* from stringEncoder and push it to keys.
*
* @type {Array<string>}
*/
this.keys = [];
readVarUint(decoder); // read feature flag - currently unused
this.keyClockDecoder = new IntDiffOptRleDecoder(readVarUint8Array(decoder));
this.clientDecoder = new UintOptRleDecoder(readVarUint8Array(decoder));
this.leftClockDecoder = new IntDiffOptRleDecoder(readVarUint8Array(decoder));
this.rightClockDecoder = new IntDiffOptRleDecoder(readVarUint8Array(decoder));
this.infoDecoder = new RleDecoder(readVarUint8Array(decoder), readUint8);
this.stringDecoder = new StringDecoder(readVarUint8Array(decoder));
this.parentInfoDecoder = new RleDecoder(readVarUint8Array(decoder), readUint8);
this.typeRefDecoder = new UintOptRleDecoder(readVarUint8Array(decoder));
this.lenDecoder = new UintOptRleDecoder(readVarUint8Array(decoder));
}
/**
* @return {ID}
*/
readLeftID () {
return new ID(this.clientDecoder.read(), this.leftClockDecoder.read())
}
/**
* @return {ID}
*/
readRightID () {
return new ID(this.clientDecoder.read(), this.rightClockDecoder.read())
}
/**
* Read the next client id.
* Use this in favor of readID whenever possible to reduce the number of objects created.
*/
readClient () {
return this.clientDecoder.read()
}
/**
* @return {number} info An unsigned 8-bit integer
*/
readInfo () {
return /** @type {number} */ (this.infoDecoder.read())
}
/**
* @return {string}
*/
readString () {
return this.stringDecoder.read()
}
/**
* @return {boolean}
*/
readParentInfo () {
return this.parentInfoDecoder.read() === 1
}
/**
* @return {number} An unsigned 8-bit integer
*/
readTypeRef () {
return this.typeRefDecoder.read()
}
/**
* Write len of a struct - well suited for Opt RLE encoder.
*
* @return {number}
*/
readLen () {
return this.lenDecoder.read()
}
/**
* @return {any}
*/
readAny () {
return readAny(this.restDecoder)
}
/**
* @return {Uint8Array}
*/
readBuf () {
return readVarUint8Array(this.restDecoder)
}
/**
* This is mainly here for legacy purposes.
*
* Initial we incoded objects using JSON. Now we use the much faster lib0/any-encoder. This method mainly exists for legacy purposes for the v1 encoder.
*
* @return {any}
*/
readJSON () {
return readAny(this.restDecoder)
}
/**
* @return {string}
*/
readKey () {
const keyClock = this.keyClockDecoder.read();
if (keyClock < this.keys.length) {
return this.keys[keyClock]
} else {
const key = this.stringDecoder.read();
this.keys.push(key);
return key
}
}
}
class DSEncoderV1 {
constructor () {
this.restEncoder = createEncoder();
}
toUint8Array () {
return toUint8Array(this.restEncoder)
}
resetDsCurVal () {
// nop
}
/**
* @param {number} clock
*/
writeDsClock (clock) {
writeVarUint(this.restEncoder, clock);
}
/**
* @param {number} len
*/
writeDsLen (len) {
writeVarUint(this.restEncoder, len);
}
}
class UpdateEncoderV1 extends DSEncoderV1 {
/**
* @param {ID} id
*/
writeLeftID (id) {
writeVarUint(this.restEncoder, id.client);
writeVarUint(this.restEncoder, id.clock);
}
/**
* @param {ID} id
*/
writeRightID (id) {
writeVarUint(this.restEncoder, id.client);
writeVarUint(this.restEncoder, id.clock);
}
/**
* Use writeClient and writeClock instead of writeID if possible.
* @param {number} client
*/
writeClient (client) {
writeVarUint(this.restEncoder, client);
}
/**
* @param {number} info An unsigned 8-bit integer
*/
writeInfo (info) {
writeUint8(this.restEncoder, info);
}
/**
* @param {string} s
*/
writeString (s) {
writeVarString(this.restEncoder, s);
}
/**
* @param {boolean} isYKey
*/
writeParentInfo (isYKey) {
writeVarUint(this.restEncoder, isYKey ? 1 : 0);
}
/**
* @param {number} info An unsigned 8-bit integer
*/
writeTypeRef (info) {
writeVarUint(this.restEncoder, info);
}
/**
* Write len of a struct - well suited for Opt RLE encoder.
*
* @param {number} len
*/
writeLen (len) {
writeVarUint(this.restEncoder, len);
}
/**
* @param {any} any
*/
writeAny (any) {
writeAny(this.restEncoder, any);
}
/**
* @param {Uint8Array} buf
*/
writeBuf (buf) {
writeVarUint8Array(this.restEncoder, buf);
}
/**
* @param {any} embed
*/
writeJSON (embed) {
writeVarString(this.restEncoder, JSON.stringify(embed));
}
/**
* @param {string} key
*/
writeKey (key) {
writeVarString(this.restEncoder, key);
}
}
class DSEncoderV2 {
constructor () {
this.restEncoder = createEncoder(); // encodes all the rest / non-optimized
this.dsCurrVal = 0;
}
toUint8Array () {
return toUint8Array(this.restEncoder)
}
resetDsCurVal () {
this.dsCurrVal = 0;
}
/**
* @param {number} clock
*/
writeDsClock (clock) {
const diff = clock - this.dsCurrVal;
this.dsCurrVal = clock;
writeVarUint(this.restEncoder, diff);
}
/**
* @param {number} len
*/
writeDsLen (len) {
if (len === 0) {
unexpectedCase();
}
writeVarUint(this.restEncoder, len - 1);
this.dsCurrVal += len;
}
}
class UpdateEncoderV2 extends DSEncoderV2 {
constructor () {
super();
/**
* @type {Map<string,number>}
*/
this.keyMap = new Map();
/**
* Refers to the next uniqe key-identifier to me used.
* See writeKey method for more information.
*
* @type {number}
*/
this.keyClock = 0;
this.keyClockEncoder = new IntDiffOptRleEncoder();
this.clientEncoder = new UintOptRleEncoder();
this.leftClockEncoder = new IntDiffOptRleEncoder();
this.rightClockEncoder = new IntDiffOptRleEncoder();
this.infoEncoder = new RleEncoder(writeUint8);
this.stringEncoder = new StringEncoder();
this.parentInfoEncoder = new RleEncoder(writeUint8);
this.typeRefEncoder = new UintOptRleEncoder();
this.lenEncoder = new UintOptRleEncoder();
}
toUint8Array () {
const encoder = createEncoder();
writeVarUint(encoder, 0); // this is a feature flag that we might use in the future
writeVarUint8Array(encoder, this.keyClockEncoder.toUint8Array());
writeVarUint8Array(encoder, this.clientEncoder.toUint8Array());
writeVarUint8Array(encoder, this.leftClockEncoder.toUint8Array());
writeVarUint8Array(encoder, this.rightClockEncoder.toUint8Array());
writeVarUint8Array(encoder, toUint8Array(this.infoEncoder));
writeVarUint8Array(encoder, this.stringEncoder.toUint8Array());
writeVarUint8Array(encoder, toUint8Array(this.parentInfoEncoder));
writeVarUint8Array(encoder, this.typeRefEncoder.toUint8Array());
writeVarUint8Array(encoder, this.lenEncoder.toUint8Array());
// @note The rest encoder is appended! (note the missing var)
writeUint8Array(encoder, toUint8Array(this.restEncoder));
return toUint8Array(encoder)
}
/**
* @param {ID} id
*/
writeLeftID (id) {
this.clientEncoder.write(id.client);
this.leftClockEncoder.write(id.clock);
}
/**
* @param {ID} id
*/
writeRightID (id) {
this.clientEncoder.write(id.client);
this.rightClockEncoder.write(id.clock);
}
/**
* @param {number} client
*/
writeClient (client) {
this.clientEncoder.write(client);
}
/**
* @param {number} info An unsigned 8-bit integer
*/
writeInfo (info) {
this.infoEncoder.write(info);
}
/**
* @param {string} s
*/
writeString (s) {
this.stringEncoder.write(s);
}
/**
* @param {boolean} isYKey
*/
writeParentInfo (isYKey) {
this.parentInfoEncoder.write(isYKey ? 1 : 0);
}
/**
* @param {number} info An unsigned 8-bit integer
*/
writeTypeRef (info) {
this.typeRefEncoder.write(info);
}
/**
* Write len of a struct - well suited for Opt RLE encoder.
*
* @param {number} len
*/
writeLen (len) {
this.lenEncoder.write(len);
}
/**
* @param {any} any
*/
writeAny (any) {
writeAny(this.restEncoder, any);
}
/**
* @param {Uint8Array} buf
*/
writeBuf (buf) {
writeVarUint8Array(this.restEncoder, buf);
}
/**
* This is mainly here for legacy purposes.
*
* Initial we incoded objects using JSON. Now we use the much faster lib0/any-encoder. This method mainly exists for legacy purposes for the v1 encoder.
*
* @param {any} embed
*/
writeJSON (embed) {
writeAny(this.restEncoder, embed);
}
/**
* Property keys are often reused. For example, in y-prosemirror the key `bold` might
* occur very often. For a 3d application, the key `position` might occur very often.
*
* We cache these keys in a Map and refer to them via a unique number.
*
* @param {string} key
*/
writeKey (key) {
const clock = this.keyMap.get(key);
if (clock === undefined) {
/**
* @todo uncomment to introduce this feature finally
*
* Background. The ContentFormat object was always encoded using writeKey, but the decoder used to use readString.
* Furthermore, I forgot to set the keyclock. So everything was working fine.
*
* However, this feature here is basically useless as it is not being used (it actually only consumes extra memory).
*
* I don't know yet how to reintroduce this feature..
*
* Older clients won't be able to read updates when we reintroduce this feature. So this should probably be done using a flag.
*
*/
// this.keyMap.set(key, this.keyClock)
this.keyClockEncoder.write(this.keyClock++);
this.stringEncoder.write(key);
} else {
this.keyClockEncoder.write(clock);
}
}
}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
* @param {Array<GC|Item>} structs All structs by `client`
* @param {number} client
* @param {number} clock write structs starting with `ID(client,clock)`
*
* @function
*/
const writeStructs = (encoder, structs, client, clock) => {
// write first id
clock = max(clock, structs[0].id.clock); // make sure the first id exists
const startNewStructs = findIndexSS(structs, clock);
// write # encoded structs
writeVarUint(encoder.restEncoder, structs.length - startNewStructs);
encoder.writeClient(client);
writeVarUint(encoder.restEncoder, clock);
const firstStruct = structs[startNewStructs];
// write first struct with an offset
firstStruct.write(encoder, clock - firstStruct.id.clock);
for (let i = startNewStructs + 1; i < structs.length; i++) {
structs[i].write(encoder, 0);
}
};
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
* @param {StructStore} store
* @param {Map<number,number>} _sm
*
* @private
* @function
*/
const writeClientsStructs = (encoder, store, _sm) => {
// we filter all valid _sm entries into sm
const sm = new Map();
_sm.forEach((clock, client) => {
// only write if new structs are available
if (getState(store, client) > clock) {
sm.set(client, clock);
}
});
getStateVector(store).forEach((clock, client) => {
if (!_sm.has(client)) {
sm.set(client, 0);
}
});
// write # states that were updated
writeVarUint(encoder.restEncoder, sm.size);
// Write items with higher client ids first
// This heavily improves the conflict algorithm.
from(sm.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, clock]) => {
// @ts-ignore
writeStructs(encoder, store.clients.get(client), client, clock);
});
};
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder The decoder object to read data from.
* @param {Doc} doc
* @return {Map<number, { i: number, refs: Array<Item | GC> }>}
*
* @private
* @function
*/
const readClientsStructRefs = (decoder, doc) => {
/**
* @type {Map<number, { i: number, refs: Array<Item | GC> }>}
*/
const clientRefs = create$5();
const numOfStateUpdates = readVarUint(decoder.restDecoder);
for (let i = 0; i < numOfStateUpdates; i++) {
const numberOfStructs = readVarUint(decoder.restDecoder);
/**
* @type {Array<GC|Item>}
*/
const refs = new Array(numberOfStructs);
const client = decoder.readClient();
let clock = readVarUint(decoder.restDecoder);
// const start = performance.now()
clientRefs.set(client, { i: 0, refs });
for (let i = 0; i < numberOfStructs; i++) {
const info = decoder.readInfo();
switch (BITS5 & info) {
case 0: { // GC
const len = decoder.readLen();
refs[i] = new GC(createID(client, clock), len);
clock += len;
break
}
case 10: { // Skip Struct (nothing to apply)
// @todo we could reduce the amount of checks by adding Skip struct to clientRefs so we know that something is missing.
const len = readVarUint(decoder.restDecoder);
refs[i] = new Skip(createID(client, clock), len);
clock += len;
break
}
default: { // Item with content
/**
* The optimized implementation doesn't use any variables because inlining variables is faster.
* Below a non-optimized version is shown that implements the basic algorithm with
* a few comments
*/
const cantCopyParentInfo = (info & (BIT7 | BIT8)) === 0;
// If parent = null and neither left nor right are defined, then we know that `parent` is child of `y`
// and we read the next string as parentYKey.
// It indicates how we store/retrieve parent from `y.share`
// @type {string|null}
const struct = new Item(
createID(client, clock),
null, // leftd
(info & BIT8) === BIT8 ? decoder.readLeftID() : null, // origin
null, // right
(info & BIT7) === BIT7 ? decoder.readRightID() : null, // right origin
cantCopyParentInfo ? (decoder.readParentInfo() ? doc.get(decoder.readString()) : decoder.readLeftID()) : null, // parent
cantCopyParentInfo && (info & BIT6) === BIT6 ? decoder.readString() : null, // parentSub
readItemContent(decoder, info) // item content
);
/* A non-optimized implementation of the above algorithm:
// The item that was originally to the left of this item.
const origin = (info & binary.BIT8) === binary.BIT8 ? decoder.readLeftID() : null
// The item that was originally to the right of this item.
const rightOrigin = (info & binary.BIT7) === binary.BIT7 ? decoder.readRightID() : null
const cantCopyParentInfo = (info & (binary.BIT7 | binary.BIT8)) === 0
const hasParentYKey = cantCopyParentInfo ? decoder.readParentInfo() : false
// If parent = null and neither left nor right are defined, then we know that `parent` is child of `y`
// and we read the next string as parentYKey.
// It indicates how we store/retrieve parent from `y.share`
// @type {string|null}
const parentYKey = cantCopyParentInfo && hasParentYKey ? decoder.readString() : null
const struct = new Item(
createID(client, clock),
null, // leftd
origin, // origin
null, // right
rightOrigin, // right origin
cantCopyParentInfo && !hasParentYKey ? decoder.readLeftID() : (parentYKey !== null ? doc.get(parentYKey) : null), // parent
cantCopyParentInfo && (info & binary.BIT6) === binary.BIT6 ? decoder.readString() : null, // parentSub
readItemContent(decoder, info) // item content
)
*/
refs[i] = struct;
clock += struct.length;
}
}
}
// console.log('time to read: ', performance.now() - start) // @todo remove
}
return clientRefs
};
/**
* Resume computing structs generated by struct readers.
*
* While there is something to do, we integrate structs in this order
* 1. top element on stack, if stack is not empty
* 2. next element from current struct reader (if empty, use next struct reader)
*
* If struct causally depends on another struct (ref.missing), we put next reader of
* `ref.id.client` on top of stack.
*
* At some point we find a struct that has no causal dependencies,
* then we start emptying the stack.
*
* It is not possible to have circles: i.e. struct1 (from client1) depends on struct2 (from client2)
* depends on struct3 (from client1). Therefore the max stack size is eqaul to `structReaders.length`.
*
* This method is implemented in a way so that we can resume computation if this update
* causally depends on another update.
*
* @param {Transaction} transaction
* @param {StructStore} store
* @param {Map<number, { i: number, refs: (GC | Item)[] }>} clientsStructRefs
* @return { null | { update: Uint8Array, missing: Map<number,number> } }
*
* @private
* @function
*/
const integrateStructs = (transaction, store, clientsStructRefs) => {
/**
* @type {Array<Item | GC>}
*/
const stack = [];
// sort them so that we take the higher id first, in case of conflicts the lower id will probably not conflict with the id from the higher user.
let clientsStructRefsIds = from(clientsStructRefs.keys()).sort((a, b) => a - b);
if (clientsStructRefsIds.length === 0) {
return null
}
const getNextStructTarget = () => {
if (clientsStructRefsIds.length === 0) {
return null
}
let nextStructsTarget = /** @type {{i:number,refs:Array<GC|Item>}} */ (clientsStructRefs.get(clientsStructRefsIds[clientsStructRefsIds.length - 1]));
while (nextStructsTarget.refs.length === nextStructsTarget.i) {
clientsStructRefsIds.pop();
if (clientsStructRefsIds.length > 0) {
nextStructsTarget = /** @type {{i:number,refs:Array<GC|Item>}} */ (clientsStructRefs.get(clientsStructRefsIds[clientsStructRefsIds.length - 1]));
} else {
return null
}
}
return nextStructsTarget
};
let curStructsTarget = getNextStructTarget();
if (curStructsTarget === null && stack.length === 0) {
return null
}
/**
* @type {StructStore}
*/
const restStructs = new StructStore();
const missingSV = new Map();
/**
* @param {number} client
* @param {number} clock
*/
const updateMissingSv = (client, clock) => {
const mclock = missingSV.get(client);
if (mclock == null || mclock > clock) {
missingSV.set(client, clock);
}
};
/**
* @type {GC|Item}
*/
let stackHead = /** @type {any} */ (curStructsTarget).refs[/** @type {any} */ (curStructsTarget).i++];
// caching the state because it is used very often
const state = new Map();
const addStackToRestSS = () => {
for (const item of stack) {
const client = item.id.client;
const unapplicableItems = clientsStructRefs.get(client);
if (unapplicableItems) {
// decrement because we weren't able to apply previous operation
unapplicableItems.i--;
restStructs.clients.set(client, unapplicableItems.refs.slice(unapplicableItems.i));
clientsStructRefs.delete(client);
unapplicableItems.i = 0;
unapplicableItems.refs = [];
} else {
// item was the last item on clientsStructRefs and the field was already cleared. Add item to restStructs and continue
restStructs.clients.set(client, [item]);
}
// remove client from clientsStructRefsIds to prevent users from applying the same update again
clientsStructRefsIds = clientsStructRefsIds.filter(c => c !== client);
}
stack.length = 0;
};
// iterate over all struct readers until we are done
while (true) {
if (stackHead.constructor !== Skip) {
const localClock = setIfUndefined(state, stackHead.id.client, () => getState(store, stackHead.id.client));
const offset = localClock - stackHead.id.clock;
if (offset < 0) {
// update from the same client is missing
stack.push(stackHead);
updateMissingSv(stackHead.id.client, stackHead.id.clock - 1);
// hid a dead wall, add all items from stack to restSS
addStackToRestSS();
} else {
const missing = stackHead.getMissing(transaction, store);
if (missing !== null) {
stack.push(stackHead);
// get the struct reader that has the missing struct
/**
* @type {{ refs: Array<GC|Item>, i: number }}
*/
const structRefs = clientsStructRefs.get(/** @type {number} */ (missing)) || { refs: [], i: 0 };
if (structRefs.refs.length === structRefs.i) {
// This update message causally depends on another update message that doesn't exist yet
updateMissingSv(/** @type {number} */ (missing), getState(store, missing));
addStackToRestSS();
} else {
stackHead = structRefs.refs[structRefs.i++];
continue
}
} else if (offset === 0 || offset < stackHead.length) {
// all fine, apply the stackhead
stackHead.integrate(transaction, offset);
state.set(stackHead.id.client, stackHead.id.clock + stackHead.length);
}
}
}
// iterate to next stackHead
if (stack.length > 0) {
stackHead = /** @type {GC|Item} */ (stack.pop());
} else if (curStructsTarget !== null && curStructsTarget.i < curStructsTarget.refs.length) {
stackHead = /** @type {GC|Item} */ (curStructsTarget.refs[curStructsTarget.i++]);
} else {
curStructsTarget = getNextStructTarget();
if (curStructsTarget === null) {
// we are done!
break
} else {
stackHead = /** @type {GC|Item} */ (curStructsTarget.refs[curStructsTarget.i++]);
}
}
}
if (restStructs.clients.size > 0) {
const encoder = new UpdateEncoderV2();
writeClientsStructs(encoder, restStructs, new Map());
// write empty deleteset
// writeDeleteSet(encoder, new DeleteSet())
writeVarUint(encoder.restEncoder, 0); // => no need for an extra function call, just write 0 deletes
return { missing: missingSV, update: encoder.toUint8Array() }
}
return null
};
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
* @param {Transaction} transaction
*
* @private
* @function
*/
const writeStructsFromTransaction = (encoder, transaction) => writeClientsStructs(encoder, transaction.doc.store, transaction.beforeState);
/**
* Read and apply a document update.
*
* This function has the same effect as `applyUpdate` but accepts an decoder.
*
* @param {decoding.Decoder} decoder
* @param {Doc} ydoc
* @param {any} [transactionOrigin] This will be stored on `transaction.origin` and `.on('update', (update, origin))`
* @param {UpdateDecoderV1 | UpdateDecoderV2} [structDecoder]
*
* @function
*/
const readUpdateV2 = (decoder, ydoc, transactionOrigin, structDecoder = new UpdateDecoderV2(decoder)) =>
transact(ydoc, transaction => {
// force that transaction.local is set to non-local
transaction.local = false;
let retry = false;
const doc = transaction.doc;
const store = doc.store;
// let start = performance.now()
const ss = readClientsStructRefs(structDecoder, doc);
// console.log('time to read structs: ', performance.now() - start) // @todo remove
// start = performance.now()
// console.log('time to merge: ', performance.now() - start) // @todo remove
// start = performance.now()
const restStructs = integrateStructs(transaction, store, ss);
const pending = store.pendingStructs;
if (pending) {
// check if we can apply something
for (const [client, clock] of pending.missing) {
if (clock < getState(store, client)) {
retry = true;
break
}
}
if (restStructs) {
// merge restStructs into store.pending
for (const [client, clock] of restStructs.missing) {
const mclock = pending.missing.get(client);
if (mclock == null || mclock > clock) {
pending.missing.set(client, clock);
}
}
pending.update = mergeUpdatesV2([pending.update, restStructs.update]);
}
} else {
store.pendingStructs = restStructs;
}
// console.log('time to integrate: ', performance.now() - start) // @todo remove
// start = performance.now()
const dsRest = readAndApplyDeleteSet(structDecoder, transaction, store);
if (store.pendingDs) {
// @todo we could make a lower-bound state-vector check as we do above
const pendingDSUpdate = new UpdateDecoderV2(createDecoder(store.pendingDs));
readVarUint(pendingDSUpdate.restDecoder); // read 0 structs, because we only encode deletes in pendingdsupdate
const dsRest2 = readAndApplyDeleteSet(pendingDSUpdate, transaction, store);
if (dsRest && dsRest2) {
// case 1: ds1 != null && ds2 != null
store.pendingDs = mergeUpdatesV2([dsRest, dsRest2]);
} else {
// case 2: ds1 != null
// case 3: ds2 != null
// case 4: ds1 == null && ds2 == null
store.pendingDs = dsRest || dsRest2;
}
} else {
// Either dsRest == null && pendingDs == null OR dsRest != null
store.pendingDs = dsRest;
}
// console.log('time to cleanup: ', performance.now() - start) // @todo remove
// start = performance.now()
// console.log('time to resume delete readers: ', performance.now() - start) // @todo remove
// start = performance.now()
if (retry) {
const update = /** @type {{update: Uint8Array}} */ (store.pendingStructs).update;
store.pendingStructs = null;
applyUpdateV2(transaction.doc, update);
}
}, transactionOrigin, false);
/**
* Apply a document update created by, for example, `y.on('update', update => ..)` or `update = encodeStateAsUpdate()`.
*
* This function has the same effect as `readUpdate` but accepts an Uint8Array instead of a Decoder.
*
* @param {Doc} ydoc
* @param {Uint8Array} update
* @param {any} [transactionOrigin] This will be stored on `transaction.origin` and `.on('update', (update, origin))`
* @param {typeof UpdateDecoderV1 | typeof UpdateDecoderV2} [YDecoder]
*
* @function
*/
const applyUpdateV2 = (ydoc, update, transactionOrigin, YDecoder = UpdateDecoderV2) => {
const decoder = createDecoder(update);
readUpdateV2(decoder, ydoc, transactionOrigin, new YDecoder(decoder));
};
/**
* Apply a document update created by, for example, `y.on('update', update => ..)` or `update = encodeStateAsUpdate()`.
*
* This function has the same effect as `readUpdate` but accepts an Uint8Array instead of a Decoder.
*
* @param {Doc} ydoc
* @param {Uint8Array} update
* @param {any} [transactionOrigin] This will be stored on `transaction.origin` and `.on('update', (update, origin))`
*
* @function
*/
const applyUpdate = (ydoc, update, transactionOrigin) => applyUpdateV2(ydoc, update, transactionOrigin, UpdateDecoderV1);
/**
* Write all the document as a single update message. If you specify the state of the remote client (`targetStateVector`) it will
* only write the operations that are missing.
*
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
* @param {Doc} doc
* @param {Map<number,number>} [targetStateVector] The state of the target that receives the update. Leave empty to write all known structs
*
* @function
*/
const writeStateAsUpdate = (encoder, doc, targetStateVector = new Map()) => {
writeClientsStructs(encoder, doc.store, targetStateVector);
writeDeleteSet(encoder, createDeleteSetFromStructStore(doc.store));
};
/**
* Write all the document as a single update message that can be applied on the remote document. If you specify the state of the remote client (`targetState`) it will
* only write the operations that are missing.
*
* Use `writeStateAsUpdate` instead if you are working with lib0/encoding.js#Encoder
*
* @param {Doc} doc
* @param {Uint8Array} [encodedTargetStateVector] The state of the target that receives the update. Leave empty to write all known structs
* @param {UpdateEncoderV1 | UpdateEncoderV2} [encoder]
* @return {Uint8Array}
*
* @function
*/
const encodeStateAsUpdateV2 = (doc, encodedTargetStateVector = new Uint8Array([0]), encoder = new UpdateEncoderV2()) => {
const targetStateVector = decodeStateVector(encodedTargetStateVector);
writeStateAsUpdate(encoder, doc, targetStateVector);
const updates = [encoder.toUint8Array()];
// also add the pending updates (if there are any)
if (doc.store.pendingDs) {
updates.push(doc.store.pendingDs);
}
if (doc.store.pendingStructs) {
updates.push(diffUpdateV2(doc.store.pendingStructs.update, encodedTargetStateVector));
}
if (updates.length > 1) {
if (encoder.constructor === UpdateEncoderV1) {
return mergeUpdates(updates.map((update, i) => i === 0 ? update : convertUpdateFormatV2ToV1(update)))
} else if (encoder.constructor === UpdateEncoderV2) {
return mergeUpdatesV2(updates)
}
}
return updates[0]
};
/**
* Write all the document as a single update message that can be applied on the remote document. If you specify the state of the remote client (`targetState`) it will
* only write the operations that are missing.
*
* Use `writeStateAsUpdate` instead if you are working with lib0/encoding.js#Encoder
*
* @param {Doc} doc
* @param {Uint8Array} [encodedTargetStateVector] The state of the target that receives the update. Leave empty to write all known structs
* @return {Uint8Array}
*
* @function
*/
const encodeStateAsUpdate = (doc, encodedTargetStateVector) => encodeStateAsUpdateV2(doc, encodedTargetStateVector, new UpdateEncoderV1());
/**
* Read state vector from Decoder and return as Map
*
* @param {DSDecoderV1 | DSDecoderV2} decoder
* @return {Map<number,number>} Maps `client` to the number next expected `clock` from that client.
*
* @function
*/
const readStateVector = decoder => {
const ss = new Map();
const ssLength = readVarUint(decoder.restDecoder);
for (let i = 0; i < ssLength; i++) {
const client = readVarUint(decoder.restDecoder);
const clock = readVarUint(decoder.restDecoder);
ss.set(client, clock);
}
return ss
};
/**
* Read decodedState and return State as Map.
*
* @param {Uint8Array} decodedState
* @return {Map<number,number>} Maps `client` to the number next expected `clock` from that client.
*
* @function
*/
// export const decodeStateVectorV2 = decodedState => readStateVector(new DSDecoderV2(decoding.createDecoder(decodedState)))
/**
* Read decodedState and return State as Map.
*
* @param {Uint8Array} decodedState
* @return {Map<number,number>} Maps `client` to the number next expected `clock` from that client.
*
* @function
*/
const decodeStateVector = decodedState => readStateVector(new DSDecoderV1(createDecoder(decodedState)));
/**
* @param {DSEncoderV1 | DSEncoderV2} encoder
* @param {Map<number,number>} sv
* @function
*/
const writeStateVector = (encoder, sv) => {
writeVarUint(encoder.restEncoder, sv.size);
from(sv.entries()).sort((a, b) => b[0] - a[0]).forEach(([client, clock]) => {
writeVarUint(encoder.restEncoder, client); // @todo use a special client decoder that is based on mapping
writeVarUint(encoder.restEncoder, clock);
});
return encoder
};
/**
* @param {DSEncoderV1 | DSEncoderV2} encoder
* @param {Doc} doc
*
* @function
*/
const writeDocumentStateVector = (encoder, doc) => writeStateVector(encoder, getStateVector(doc.store));
/**
* Encode State as Uint8Array.
*
* @param {Doc|Map<number,number>} doc
* @param {DSEncoderV1 | DSEncoderV2} [encoder]
* @return {Uint8Array}
*
* @function
*/
const encodeStateVectorV2 = (doc, encoder = new DSEncoderV2()) => {
if (doc instanceof Map) {
writeStateVector(encoder, doc);
} else {
writeDocumentStateVector(encoder, doc);
}
return encoder.toUint8Array()
};
/**
* Encode State as Uint8Array.
*
* @param {Doc|Map<number,number>} doc
* @return {Uint8Array}
*
* @function
*/
const encodeStateVector = doc => encodeStateVectorV2(doc, new DSEncoderV1());
/**
* General event handler implementation.
*
* @template ARG0, ARG1
*
* @private
*/
class EventHandler {
constructor () {
/**
* @type {Array<function(ARG0, ARG1):void>}
*/
this.l = [];
}
}
/**
* @template ARG0,ARG1
* @returns {EventHandler<ARG0,ARG1>}
*
* @private
* @function
*/
const createEventHandler = () => new EventHandler();
/**
* Adds an event listener that is called when
* {@link EventHandler#callEventListeners} is called.
*
* @template ARG0,ARG1
* @param {EventHandler<ARG0,ARG1>} eventHandler
* @param {function(ARG0,ARG1):void} f The event handler.
*
* @private
* @function
*/
const addEventHandlerListener = (eventHandler, f) =>
eventHandler.l.push(f);
/**
* Removes an event listener.
*
* @template ARG0,ARG1
* @param {EventHandler<ARG0,ARG1>} eventHandler
* @param {function(ARG0,ARG1):void} f The event handler that was added with
* {@link EventHandler#addEventListener}
*
* @private
* @function
*/
const removeEventHandlerListener = (eventHandler, f) => {
const l = eventHandler.l;
const len = l.length;
eventHandler.l = l.filter(g => f !== g);
if (len === eventHandler.l.length) {
console.error('[yjs] Tried to remove event handler that doesn\'t exist.');
}
};
/**
* Call all event listeners that were added via
* {@link EventHandler#addEventListener}.
*
* @template ARG0,ARG1
* @param {EventHandler<ARG0,ARG1>} eventHandler
* @param {ARG0} arg0
* @param {ARG1} arg1
*
* @private
* @function
*/
const callEventHandlerListeners = (eventHandler, arg0, arg1) =>
callAll(eventHandler.l, [arg0, arg1]);
class ID {
/**
* @param {number} client client id
* @param {number} clock unique per client id, continuous number
*/
constructor (client, clock) {
/**
* Client id
* @type {number}
*/
this.client = client;
/**
* unique per client id, continuous number
* @type {number}
*/
this.clock = clock;
}
}
/**
* @param {ID | null} a
* @param {ID | null} b
* @return {boolean}
*
* @function
*/
const compareIDs = (a, b) => a === b || (a !== null && b !== null && a.client === b.client && a.clock === b.clock);
/**
* @param {number} client
* @param {number} clock
*
* @private
* @function
*/
const createID = (client, clock) => new ID(client, clock);
/**
* The top types are mapped from y.share.get(keyname) => type.
* `type` does not store any information about the `keyname`.
* This function finds the correct `keyname` for `type` and throws otherwise.
*
* @param {AbstractType<any>} type
* @return {string}
*
* @private
* @function
*/
const findRootTypeKey = type => {
// @ts-ignore _y must be defined, otherwise unexpected case
for (const [key, value] of type.doc.share.entries()) {
if (value === type) {
return key
}
}
throw unexpectedCase()
};
class Snapshot {
/**
* @param {DeleteSet} ds
* @param {Map<number,number>} sv state map
*/
constructor (ds, sv) {
/**
* @type {DeleteSet}
*/
this.ds = ds;
/**
* State Map
* @type {Map<number,number>}
*/
this.sv = sv;
}
}
/**
* @param {DeleteSet} ds
* @param {Map<number,number>} sm
* @return {Snapshot}
*/
const createSnapshot = (ds, sm) => new Snapshot(ds, sm);
createSnapshot(createDeleteSet(), new Map());
/**
* @param {Item} item
* @param {Snapshot|undefined} snapshot
*
* @protected
* @function
*/
const isVisible = (item, snapshot) => snapshot === undefined
? !item.deleted
: snapshot.sv.has(item.id.client) && (snapshot.sv.get(item.id.client) || 0) > item.id.clock && !isDeleted(snapshot.ds, item.id);
/**
* @param {Transaction} transaction
* @param {Snapshot} snapshot
*/
const splitSnapshotAffectedStructs = (transaction, snapshot) => {
const meta = setIfUndefined(transaction.meta, splitSnapshotAffectedStructs, create$4);
const store = transaction.doc.store;
// check if we already split for this snapshot
if (!meta.has(snapshot)) {
snapshot.sv.forEach((clock, client) => {
if (clock < getState(store, client)) {
getItemCleanStart(transaction, createID(client, clock));
}
});
iterateDeletedStructs(transaction, snapshot.ds, item => {});
meta.add(snapshot);
}
};
class StructStore {
constructor () {
/**
* @type {Map<number,Array<GC|Item>>}
*/
this.clients = new Map();
/**
* @type {null | { missing: Map<number, number>, update: Uint8Array }}
*/
this.pendingStructs = null;
/**
* @type {null | Uint8Array}
*/
this.pendingDs = null;
}
}
/**
* Return the states as a Map<client,clock>.
* Note that clock refers to the next expected clock id.
*
* @param {StructStore} store
* @return {Map<number,number>}
*
* @public
* @function
*/
const getStateVector = store => {
const sm = new Map();
store.clients.forEach((structs, client) => {
const struct = structs[structs.length - 1];
sm.set(client, struct.id.clock + struct.length);
});
return sm
};
/**
* @param {StructStore} store
* @param {number} client
* @return {number}
*
* @public
* @function
*/
const getState = (store, client) => {
const structs = store.clients.get(client);
if (structs === undefined) {
return 0
}
const lastStruct = structs[structs.length - 1];
return lastStruct.id.clock + lastStruct.length
};
/**
* @param {StructStore} store
* @param {GC|Item} struct
*
* @private
* @function
*/
const addStruct = (store, struct) => {
let structs = store.clients.get(struct.id.client);
if (structs === undefined) {
structs = [];
store.clients.set(struct.id.client, structs);
} else {
const lastStruct = structs[structs.length - 1];
if (lastStruct.id.clock + lastStruct.length !== struct.id.clock) {
throw unexpectedCase()
}
}
structs.push(struct);
};
/**
* Perform a binary search on a sorted array
* @param {Array<Item|GC>} structs
* @param {number} clock
* @return {number}
*
* @private
* @function
*/
const findIndexSS = (structs, clock) => {
let left = 0;
let right = structs.length - 1;
let mid = structs[right];
let midclock = mid.id.clock;
if (midclock === clock) {
return right
}
// @todo does it even make sense to pivot the search?
// If a good split misses, it might actually increase the time to find the correct item.
// Currently, the only advantage is that search with pivoting might find the item on the first try.
let midindex = floor((clock / (midclock + mid.length - 1)) * right); // pivoting the search
while (left <= right) {
mid = structs[midindex];
midclock = mid.id.clock;
if (midclock <= clock) {
if (clock < midclock + mid.length) {
return midindex
}
left = midindex + 1;
} else {
right = midindex - 1;
}
midindex = floor((left + right) / 2);
}
// Always check state before looking for a struct in StructStore
// Therefore the case of not finding a struct is unexpected
throw unexpectedCase()
};
/**
* Expects that id is actually in store. This function throws or is an infinite loop otherwise.
*
* @param {StructStore} store
* @param {ID} id
* @return {GC|Item}
*
* @private
* @function
*/
const find = (store, id) => {
/**
* @type {Array<GC|Item>}
*/
// @ts-ignore
const structs = store.clients.get(id.client);
return structs[findIndexSS(structs, id.clock)]
};
/**
* Expects that id is actually in store. This function throws or is an infinite loop otherwise.
* @private
* @function
*/
const getItem = /** @type {function(StructStore,ID):Item} */ (find);
/**
* @param {Transaction} transaction
* @param {Array<Item|GC>} structs
* @param {number} clock
*/
const findIndexCleanStart = (transaction, structs, clock) => {
const index = findIndexSS(structs, clock);
const struct = structs[index];
if (struct.id.clock < clock && struct instanceof Item) {
structs.splice(index + 1, 0, splitItem(transaction, struct, clock - struct.id.clock));
return index + 1
}
return index
};
/**
* Expects that id is actually in store. This function throws or is an infinite loop otherwise.
*
* @param {Transaction} transaction
* @param {ID} id
* @return {Item}
*
* @private
* @function
*/
const getItemCleanStart = (transaction, id) => {
const structs = /** @type {Array<Item>} */ (transaction.doc.store.clients.get(id.client));
return structs[findIndexCleanStart(transaction, structs, id.clock)]
};
/**
* Expects that id is actually in store. This function throws or is an infinite loop otherwise.
*
* @param {Transaction} transaction
* @param {StructStore} store
* @param {ID} id
* @return {Item}
*
* @private
* @function
*/
const getItemCleanEnd = (transaction, store, id) => {
/**
* @type {Array<Item>}
*/
// @ts-ignore
const structs = store.clients.get(id.client);
const index = findIndexSS(structs, id.clock);
const struct = structs[index];
if (id.clock !== struct.id.clock + struct.length - 1 && struct.constructor !== GC) {
structs.splice(index + 1, 0, splitItem(transaction, struct, id.clock - struct.id.clock + 1));
}
return struct
};
/**
* Replace `item` with `newitem` in store
* @param {StructStore} store
* @param {GC|Item} struct
* @param {GC|Item} newStruct
*
* @private
* @function
*/
const replaceStruct = (store, struct, newStruct) => {
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(struct.id.client));
structs[findIndexSS(structs, struct.id.clock)] = newStruct;
};
/**
* Iterate over a range of structs
*
* @param {Transaction} transaction
* @param {Array<Item|GC>} structs
* @param {number} clockStart Inclusive start
* @param {number} len
* @param {function(GC|Item):void} f
*
* @function
*/
const iterateStructs = (transaction, structs, clockStart, len, f) => {
if (len === 0) {
return
}
const clockEnd = clockStart + len;
let index = findIndexCleanStart(transaction, structs, clockStart);
let struct;
do {
struct = structs[index++];
if (clockEnd < struct.id.clock + struct.length) {
findIndexCleanStart(transaction, structs, clockEnd);
}
f(struct);
} while (index < structs.length && structs[index].id.clock < clockEnd)
};
/**
* 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(() => {
* 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(() => {
* map.set('a', 1)
* map.set('b', 1)
* }) // => "change triggered"
*
* @public
*/
class Transaction {
/**
* @param {Doc} doc
* @param {any} origin
* @param {boolean} local
*/
constructor (doc, origin, local) {
/**
* The Yjs instance.
* @type {Doc}
*/
this.doc = doc;
/**
* Describes the set of deleted items by ids
* @type {DeleteSet}
*/
this.deleteSet = new DeleteSet();
/**
* Holds the state before the transaction started.
* @type {Map<Number,Number>}
*/
this.beforeState = getStateVector(doc.store);
/**
* Holds the state after the transaction.
* @type {Map<Number,Number>}
*/
this.afterState = new Map();
/**
* 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 {Map<AbstractType<YEvent<any>>,Set<String|null>>}
*/
this.changed = new Map();
/**
* Stores the events for the types that observe also child elements.
* It is mainly used by `observeDeep`.
* @type {Map<AbstractType<YEvent<any>>,Array<YEvent<any>>>}
*/
this.changedParentTypes = new Map();
/**
* @type {Array<AbstractStruct>}
*/
this._mergeStructs = [];
/**
* @type {any}
*/
this.origin = origin;
/**
* Stores meta information on the transaction
* @type {Map<any,any>}
*/
this.meta = new Map();
/**
* Whether this change originates from this doc.
* @type {boolean}
*/
this.local = local;
/**
* @type {Set<Doc>}
*/
this.subdocsAdded = new Set();
/**
* @type {Set<Doc>}
*/
this.subdocsRemoved = new Set();
/**
* @type {Set<Doc>}
*/
this.subdocsLoaded = new Set();
}
}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
* @param {Transaction} transaction
* @return {boolean} Whether data was written.
*/
const writeUpdateMessageFromTransaction = (encoder, transaction) => {
if (transaction.deleteSet.clients.size === 0 && !any(transaction.afterState, (clock, client) => transaction.beforeState.get(client) !== clock)) {
return false
}
sortAndMergeDeleteSet(transaction.deleteSet);
writeStructsFromTransaction(encoder, transaction);
writeDeleteSet(encoder, transaction.deleteSet);
return true
};
/**
* If `type.parent` was added in current transaction, `type` technically
* did not change, it was just added and we should not fire events for `type`.
*
* @param {Transaction} transaction
* @param {AbstractType<YEvent<any>>} type
* @param {string|null} parentSub
*/
const addChangedTypeToTransaction = (transaction, type, parentSub) => {
const item = type._item;
if (item === null || (item.id.clock < (transaction.beforeState.get(item.id.client) || 0) && !item.deleted)) {
setIfUndefined(transaction.changed, type, create$4).add(parentSub);
}
};
/**
* @param {Array<AbstractStruct>} structs
* @param {number} pos
*/
const tryToMergeWithLeft = (structs, pos) => {
const left = structs[pos - 1];
const right = structs[pos];
if (left.deleted === right.deleted && left.constructor === right.constructor) {
if (left.mergeWith(right)) {
structs.splice(pos, 1);
if (right instanceof Item && right.parentSub !== null && /** @type {AbstractType<any>} */ (right.parent)._map.get(right.parentSub) === right) {
/** @type {AbstractType<any>} */ (right.parent)._map.set(right.parentSub, /** @type {Item} */ (left));
}
}
}
};
/**
* @param {DeleteSet} ds
* @param {StructStore} store
* @param {function(Item):boolean} gcFilter
*/
const tryGcDeleteSet = (ds, store, gcFilter) => {
for (const [client, deleteItems] of ds.clients.entries()) {
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client));
for (let di = deleteItems.length - 1; di >= 0; di--) {
const deleteItem = deleteItems[di];
const endDeleteItemClock = deleteItem.clock + deleteItem.len;
for (
let si = findIndexSS(structs, deleteItem.clock), struct = structs[si];
si < structs.length && struct.id.clock < endDeleteItemClock;
struct = structs[++si]
) {
const struct = structs[si];
if (deleteItem.clock + deleteItem.len <= struct.id.clock) {
break
}
if (struct instanceof Item && struct.deleted && !struct.keep && gcFilter(struct)) {
struct.gc(store, false);
}
}
}
}
};
/**
* @param {DeleteSet} ds
* @param {StructStore} store
*/
const tryMergeDeleteSet = (ds, store) => {
// try to merge deleted / gc'd items
// merge from right to left for better efficiecy and so we don't miss any merge targets
ds.clients.forEach((deleteItems, client) => {
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client));
for (let di = deleteItems.length - 1; di >= 0; di--) {
const deleteItem = deleteItems[di];
// start with merging the item next to the last deleted item
const mostRightIndexToCheck = min(structs.length - 1, 1 + findIndexSS(structs, deleteItem.clock + deleteItem.len - 1));
for (
let si = mostRightIndexToCheck, struct = structs[si];
si > 0 && struct.id.clock >= deleteItem.clock;
struct = structs[--si]
) {
tryToMergeWithLeft(structs, si);
}
}
});
};
/**
* @param {Array<Transaction>} transactionCleanups
* @param {number} i
*/
const cleanupTransactions = (transactionCleanups, i) => {
if (i < transactionCleanups.length) {
const transaction = transactionCleanups[i];
const doc = transaction.doc;
const store = doc.store;
const ds = transaction.deleteSet;
const mergeStructs = transaction._mergeStructs;
try {
sortAndMergeDeleteSet(ds);
transaction.afterState = getStateVector(transaction.doc.store);
doc.emit('beforeObserverCalls', [transaction, doc]);
/**
* An array of event callbacks.
*
* Each callback is called even if the other ones throw errors.
*
* @type {Array<function():void>}
*/
const fs = [];
// observe events on changed types
transaction.changed.forEach((subs, itemtype) =>
fs.push(() => {
if (itemtype._item === null || !itemtype._item.deleted) {
itemtype._callObserver(transaction, subs);
}
})
);
fs.push(() => {
// deep observe events
transaction.changedParentTypes.forEach((events, type) =>
fs.push(() => {
// We need to think about the possibility that the user transforms the
// Y.Doc in the event.
if (type._item === null || !type._item.deleted) {
events = events
.filter(event =>
event.target._item === null || !event.target._item.deleted
);
events
.forEach(event => {
event.currentTarget = type;
});
// sort events by path length so that top-level events are fired first.
events
.sort((event1, event2) => event1.path.length - event2.path.length);
// We don't need to check for events.length
// because we know it has at least one element
callEventHandlerListeners(type._dEH, events, transaction);
}
})
);
fs.push(() => doc.emit('afterTransaction', [transaction, doc]));
});
callAll(fs, []);
} finally {
// Replace deleted items with ItemDeleted / GC.
// This is where content is actually remove from the Yjs Doc.
if (doc.gc) {
tryGcDeleteSet(ds, store, doc.gcFilter);
}
tryMergeDeleteSet(ds, store);
// on all affected store.clients props, try to merge
transaction.afterState.forEach((clock, client) => {
const beforeClock = transaction.beforeState.get(client) || 0;
if (beforeClock !== clock) {
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client));
// we iterate from right to left so we can safely remove entries
const firstChangePos = max(findIndexSS(structs, beforeClock), 1);
for (let i = structs.length - 1; i >= firstChangePos; i--) {
tryToMergeWithLeft(structs, i);
}
}
});
// try to merge mergeStructs
// @todo: it makes more sense to transform mergeStructs to a DS, sort it, and merge from right to left
// but at the moment DS does not handle duplicates
for (let i = 0; i < mergeStructs.length; i++) {
const { client, clock } = mergeStructs[i].id;
const structs = /** @type {Array<GC|Item>} */ (store.clients.get(client));
const replacedStructPos = findIndexSS(structs, clock);
if (replacedStructPos + 1 < structs.length) {
tryToMergeWithLeft(structs, replacedStructPos + 1);
}
if (replacedStructPos > 0) {
tryToMergeWithLeft(structs, replacedStructPos);
}
}
if (!transaction.local && transaction.afterState.get(doc.clientID) !== transaction.beforeState.get(doc.clientID)) {
print(ORANGE, BOLD, '[yjs] ', UNBOLD, RED, 'Changed the client-id because another client seems to be using it.');
doc.clientID = generateNewClientId();
}
// @todo Merge all the transactions into one and provide send the data as a single update message
doc.emit('afterTransactionCleanup', [transaction, doc]);
if (doc._observers.has('update')) {
const encoder = new UpdateEncoderV1();
const hasContent = writeUpdateMessageFromTransaction(encoder, transaction);
if (hasContent) {
doc.emit('update', [encoder.toUint8Array(), transaction.origin, doc, transaction]);
}
}
if (doc._observers.has('updateV2')) {
const encoder = new UpdateEncoderV2();
const hasContent = writeUpdateMessageFromTransaction(encoder, transaction);
if (hasContent) {
doc.emit('updateV2', [encoder.toUint8Array(), transaction.origin, doc, transaction]);
}
}
const { subdocsAdded, subdocsLoaded, subdocsRemoved } = transaction;
if (subdocsAdded.size > 0 || subdocsRemoved.size > 0 || subdocsLoaded.size > 0) {
subdocsAdded.forEach(subdoc => {
subdoc.clientID = doc.clientID;
if (subdoc.collectionid == null) {
subdoc.collectionid = doc.collectionid;
}
doc.subdocs.add(subdoc);
});
subdocsRemoved.forEach(subdoc => doc.subdocs.delete(subdoc));
doc.emit('subdocs', [{ loaded: subdocsLoaded, added: subdocsAdded, removed: subdocsRemoved }, doc, transaction]);
subdocsRemoved.forEach(subdoc => subdoc.destroy());
}
if (transactionCleanups.length <= i + 1) {
doc._transactionCleanups = [];
doc.emit('afterAllTransactions', [doc, transactionCleanups]);
} else {
cleanupTransactions(transactionCleanups, i + 1);
}
}
}
};
/**
* Implements the functionality of `y.transact(()=>{..})`
*
* @template T
* @param {Doc} doc
* @param {function(Transaction):T} f
* @param {any} [origin=true]
* @return {T}
*
* @function
*/
const transact = (doc, f, origin = null, local = true) => {
const transactionCleanups = doc._transactionCleanups;
let initialCall = false;
/**
* @type {any}
*/
let result = null;
if (doc._transaction === null) {
initialCall = true;
doc._transaction = new Transaction(doc, origin, local);
transactionCleanups.push(doc._transaction);
if (transactionCleanups.length === 1) {
doc.emit('beforeAllTransactions', [doc]);
}
doc.emit('beforeTransaction', [doc._transaction, doc]);
}
try {
result = f(doc._transaction);
} finally {
if (initialCall) {
const finishCleanup = doc._transaction === transactionCleanups[0];
doc._transaction = null;
if (finishCleanup) {
// The first transaction ended, now process observer calls.
// Observer call may create new transactions for which we need to call the observers and do cleanup.
// We don't want to nest these calls, so we execute these calls one after
// another.
// Also we need to ensure that all cleanups are called, even if the
// observes throw errors.
// This file is full of hacky try {} finally {} blocks to ensure that an
// event can throw errors and also that the cleanup is called.
cleanupTransactions(transactionCleanups, 0);
}
}
}
return result
};
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
*/
function * lazyStructReaderGenerator (decoder) {
const numOfStateUpdates = readVarUint(decoder.restDecoder);
for (let i = 0; i < numOfStateUpdates; i++) {
const numberOfStructs = readVarUint(decoder.restDecoder);
const client = decoder.readClient();
let clock = readVarUint(decoder.restDecoder);
for (let i = 0; i < numberOfStructs; i++) {
const info = decoder.readInfo();
// @todo use switch instead of ifs
if (info === 10) {
const len = readVarUint(decoder.restDecoder);
yield new Skip(createID(client, clock), len);
clock += len;
} else if ((BITS5 & info) !== 0) {
const cantCopyParentInfo = (info & (BIT7 | BIT8)) === 0;
// If parent = null and neither left nor right are defined, then we know that `parent` is child of `y`
// and we read the next string as parentYKey.
// It indicates how we store/retrieve parent from `y.share`
// @type {string|null}
const struct = new Item(
createID(client, clock),
null, // left
(info & BIT8) === BIT8 ? decoder.readLeftID() : null, // origin
null, // right
(info & BIT7) === BIT7 ? decoder.readRightID() : null, // right origin
// @ts-ignore Force writing a string here.
cantCopyParentInfo ? (decoder.readParentInfo() ? decoder.readString() : decoder.readLeftID()) : null, // parent
cantCopyParentInfo && (info & BIT6) === BIT6 ? decoder.readString() : null, // parentSub
readItemContent(decoder, info) // item content
);
yield struct;
clock += struct.length;
} else {
const len = decoder.readLen();
yield new GC(createID(client, clock), len);
clock += len;
}
}
}
}
class LazyStructReader {
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
* @param {boolean} filterSkips
*/
constructor (decoder, filterSkips) {
this.gen = lazyStructReaderGenerator(decoder);
/**
* @type {null | Item | Skip | GC}
*/
this.curr = null;
this.done = false;
this.filterSkips = filterSkips;
this.next();
}
/**
* @return {Item | GC | Skip |null}
*/
next () {
// ignore "Skip" structs
do {
this.curr = this.gen.next().value || null;
} while (this.filterSkips && this.curr !== null && this.curr.constructor === Skip)
return this.curr
}
}
class LazyStructWriter {
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
*/
constructor (encoder) {
this.currClient = 0;
this.startClock = 0;
this.written = 0;
this.encoder = encoder;
/**
* We want to write operations lazily, but also we need to know beforehand how many operations we want to write for each client.
*
* This kind of meta-information (#clients, #structs-per-client-written) is written to the restEncoder.
*
* We fragment the restEncoder and store a slice of it per-client until we know how many clients there are.
* When we flush (toUint8Array) we write the restEncoder using the fragments and the meta-information.
*
* @type {Array<{ written: number, restEncoder: Uint8Array }>}
*/
this.clientStructs = [];
}
}
/**
* @param {Array<Uint8Array>} updates
* @return {Uint8Array}
*/
const mergeUpdates = updates => mergeUpdatesV2(updates, UpdateDecoderV1, UpdateEncoderV1);
/**
* This method is intended to slice any kind of struct and retrieve the right part.
* It does not handle side-effects, so it should only be used by the lazy-encoder.
*
* @param {Item | GC | Skip} left
* @param {number} diff
* @return {Item | GC}
*/
const sliceStruct = (left, diff) => {
if (left.constructor === GC) {
const { client, clock } = left.id;
return new GC(createID(client, clock + diff), left.length - diff)
} else if (left.constructor === Skip) {
const { client, clock } = left.id;
return new Skip(createID(client, clock + diff), left.length - diff)
} else {
const leftItem = /** @type {Item} */ (left);
const { client, clock } = leftItem.id;
return new Item(
createID(client, clock + diff),
null,
createID(client, clock + diff - 1),
null,
leftItem.rightOrigin,
leftItem.parent,
leftItem.parentSub,
leftItem.content.splice(diff)
)
}
};
/**
*
* This function works similarly to `readUpdateV2`.
*
* @param {Array<Uint8Array>} updates
* @param {typeof UpdateDecoderV1 | typeof UpdateDecoderV2} [YDecoder]
* @param {typeof UpdateEncoderV1 | typeof UpdateEncoderV2} [YEncoder]
* @return {Uint8Array}
*/
const mergeUpdatesV2 = (updates, YDecoder = UpdateDecoderV2, YEncoder = UpdateEncoderV2) => {
if (updates.length === 1) {
return updates[0]
}
const updateDecoders = updates.map(update => new YDecoder(createDecoder(update)));
let lazyStructDecoders = updateDecoders.map(decoder => new LazyStructReader(decoder, true));
/**
* @todo we don't need offset because we always slice before
* @type {null | { struct: Item | GC | Skip, offset: number }}
*/
let currWrite = null;
const updateEncoder = new YEncoder();
// write structs lazily
const lazyStructEncoder = new LazyStructWriter(updateEncoder);
// Note: We need to ensure that all lazyStructDecoders are fully consumed
// Note: Should merge document updates whenever possible - even from different updates
// Note: Should handle that some operations cannot be applied yet ()
while (true) {
// Write higher clients first ⇒ sort by clientID & clock and remove decoders without content
lazyStructDecoders = lazyStructDecoders.filter(dec => dec.curr !== null);
lazyStructDecoders.sort(
/** @type {function(any,any):number} */ (dec1, dec2) => {
if (dec1.curr.id.client === dec2.curr.id.client) {
const clockDiff = dec1.curr.id.clock - dec2.curr.id.clock;
if (clockDiff === 0) {
// @todo remove references to skip since the structDecoders must filter Skips.
return dec1.curr.constructor === dec2.curr.constructor
? 0
: dec1.curr.constructor === Skip ? 1 : -1 // we are filtering skips anyway.
} else {
return clockDiff
}
} else {
return dec2.curr.id.client - dec1.curr.id.client
}
}
);
if (lazyStructDecoders.length === 0) {
break
}
const currDecoder = lazyStructDecoders[0];
// write from currDecoder until the next operation is from another client or if filler-struct
// then we need to reorder the decoders and find the next operation to write
const firstClient = /** @type {Item | GC} */ (currDecoder.curr).id.client;
if (currWrite !== null) {
let curr = /** @type {Item | GC | null} */ (currDecoder.curr);
let iterated = false;
// iterate until we find something that we haven't written already
// remember: first the high client-ids are written
while (curr !== null && curr.id.clock + curr.length <= currWrite.struct.id.clock + currWrite.struct.length && curr.id.client >= currWrite.struct.id.client) {
curr = currDecoder.next();
iterated = true;
}
if (
curr === null || // current decoder is empty
curr.id.client !== firstClient || // check whether there is another decoder that has has updates from `firstClient`
(iterated && curr.id.clock > currWrite.struct.id.clock + currWrite.struct.length) // the above while loop was used and we are potentially missing updates
) {
continue
}
if (firstClient !== currWrite.struct.id.client) {
writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset);
currWrite = { struct: curr, offset: 0 };
currDecoder.next();
} else {
if (currWrite.struct.id.clock + currWrite.struct.length < curr.id.clock) {
// @todo write currStruct & set currStruct = Skip(clock = currStruct.id.clock + currStruct.length, length = curr.id.clock - self.clock)
if (currWrite.struct.constructor === Skip) {
// extend existing skip
currWrite.struct.length = curr.id.clock + curr.length - currWrite.struct.id.clock;
} else {
writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset);
const diff = curr.id.clock - currWrite.struct.id.clock - currWrite.struct.length;
/**
* @type {Skip}
*/
const struct = new Skip(createID(firstClient, currWrite.struct.id.clock + currWrite.struct.length), diff);
currWrite = { struct, offset: 0 };
}
} else { // if (currWrite.struct.id.clock + currWrite.struct.length >= curr.id.clock) {
const diff = currWrite.struct.id.clock + currWrite.struct.length - curr.id.clock;
if (diff > 0) {
if (currWrite.struct.constructor === Skip) {
// prefer to slice Skip because the other struct might contain more information
currWrite.struct.length -= diff;
} else {
curr = sliceStruct(curr, diff);
}
}
if (!currWrite.struct.mergeWith(/** @type {any} */ (curr))) {
writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset);
currWrite = { struct: curr, offset: 0 };
currDecoder.next();
}
}
}
} else {
currWrite = { struct: /** @type {Item | GC} */ (currDecoder.curr), offset: 0 };
currDecoder.next();
}
for (
let next = currDecoder.curr;
next !== null && next.id.client === firstClient && next.id.clock === currWrite.struct.id.clock + currWrite.struct.length && next.constructor !== Skip;
next = currDecoder.next()
) {
writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset);
currWrite = { struct: next, offset: 0 };
}
}
if (currWrite !== null) {
writeStructToLazyStructWriter(lazyStructEncoder, currWrite.struct, currWrite.offset);
currWrite = null;
}
finishLazyStructWriting(lazyStructEncoder);
const dss = updateDecoders.map(decoder => readDeleteSet(decoder));
const ds = mergeDeleteSets(dss);
writeDeleteSet(updateEncoder, ds);
return updateEncoder.toUint8Array()
};
/**
* @param {Uint8Array} update
* @param {Uint8Array} sv
* @param {typeof UpdateDecoderV1 | typeof UpdateDecoderV2} [YDecoder]
* @param {typeof UpdateEncoderV1 | typeof UpdateEncoderV2} [YEncoder]
*/
const diffUpdateV2 = (update, sv, YDecoder = UpdateDecoderV2, YEncoder = UpdateEncoderV2) => {
const state = decodeStateVector(sv);
const encoder = new YEncoder();
const lazyStructWriter = new LazyStructWriter(encoder);
const decoder = new YDecoder(createDecoder(update));
const reader = new LazyStructReader(decoder, false);
while (reader.curr) {
const curr = reader.curr;
const currClient = curr.id.client;
const svClock = state.get(currClient) || 0;
if (reader.curr.constructor === Skip) {
// the first written struct shouldn't be a skip
reader.next();
continue
}
if (curr.id.clock + curr.length > svClock) {
writeStructToLazyStructWriter(lazyStructWriter, curr, max(svClock - curr.id.clock, 0));
reader.next();
while (reader.curr && reader.curr.id.client === currClient) {
writeStructToLazyStructWriter(lazyStructWriter, reader.curr, 0);
reader.next();
}
} else {
// read until something new comes up
while (reader.curr && reader.curr.id.client === currClient && reader.curr.id.clock + reader.curr.length <= svClock) {
reader.next();
}
}
}
finishLazyStructWriting(lazyStructWriter);
// write ds
const ds = readDeleteSet(decoder);
writeDeleteSet(encoder, ds);
return encoder.toUint8Array()
};
/**
* @param {LazyStructWriter} lazyWriter
*/
const flushLazyStructWriter = lazyWriter => {
if (lazyWriter.written > 0) {
lazyWriter.clientStructs.push({ written: lazyWriter.written, restEncoder: toUint8Array(lazyWriter.encoder.restEncoder) });
lazyWriter.encoder.restEncoder = createEncoder();
lazyWriter.written = 0;
}
};
/**
* @param {LazyStructWriter} lazyWriter
* @param {Item | GC} struct
* @param {number} offset
*/
const writeStructToLazyStructWriter = (lazyWriter, struct, offset) => {
// flush curr if we start another client
if (lazyWriter.written > 0 && lazyWriter.currClient !== struct.id.client) {
flushLazyStructWriter(lazyWriter);
}
if (lazyWriter.written === 0) {
lazyWriter.currClient = struct.id.client;
// write next client
lazyWriter.encoder.writeClient(struct.id.client);
// write startClock
writeVarUint(lazyWriter.encoder.restEncoder, struct.id.clock + offset);
}
struct.write(lazyWriter.encoder, offset);
lazyWriter.written++;
};
/**
* Call this function when we collected all parts and want to
* put all the parts together. After calling this method,
* you can continue using the UpdateEncoder.
*
* @param {LazyStructWriter} lazyWriter
*/
const finishLazyStructWriting = (lazyWriter) => {
flushLazyStructWriter(lazyWriter);
// this is a fresh encoder because we called flushCurr
const restEncoder = lazyWriter.encoder.restEncoder;
/**
* Now we put all the fragments together.
* This works similarly to `writeClientsStructs`
*/
// write # states that were updated - i.e. the clients
writeVarUint(restEncoder, lazyWriter.clientStructs.length);
for (let i = 0; i < lazyWriter.clientStructs.length; i++) {
const partStructs = lazyWriter.clientStructs[i];
/**
* Works similarly to `writeStructs`
*/
// write # encoded structs
writeVarUint(restEncoder, partStructs.written);
// write the rest of the fragment
writeUint8Array(restEncoder, partStructs.restEncoder);
}
};
/**
* @param {Uint8Array} update
* @param {function(Item|GC|Skip):Item|GC|Skip} blockTransformer
* @param {typeof UpdateDecoderV2 | typeof UpdateDecoderV1} YDecoder
* @param {typeof UpdateEncoderV2 | typeof UpdateEncoderV1 } YEncoder
*/
const convertUpdateFormat = (update, blockTransformer, YDecoder, YEncoder) => {
const updateDecoder = new YDecoder(createDecoder(update));
const lazyDecoder = new LazyStructReader(updateDecoder, false);
const updateEncoder = new YEncoder();
const lazyWriter = new LazyStructWriter(updateEncoder);
for (let curr = lazyDecoder.curr; curr !== null; curr = lazyDecoder.next()) {
writeStructToLazyStructWriter(lazyWriter, blockTransformer(curr), 0);
}
finishLazyStructWriting(lazyWriter);
const ds = readDeleteSet(updateDecoder);
writeDeleteSet(updateEncoder, ds);
return updateEncoder.toUint8Array()
};
/**
* @param {Uint8Array} update
*/
const convertUpdateFormatV2ToV1 = update => convertUpdateFormat(update, id, UpdateDecoderV2, UpdateEncoderV1);
/**
* @template {AbstractType<any>} T
* YEvent describes the changes on a YType.
*/
class YEvent {
/**
* @param {T} target The changed type.
* @param {Transaction} transaction
*/
constructor (target, transaction) {
/**
* The type on which this event was created on.
* @type {T}
*/
this.target = target;
/**
* The current target on which the observe callback is called.
* @type {AbstractType<any>}
*/
this.currentTarget = target;
/**
* The transaction that triggered this event.
* @type {Transaction}
*/
this.transaction = transaction;
/**
* @type {Object|null}
*/
this._changes = null;
/**
* @type {null | Map<string, { action: 'add' | 'update' | 'delete', oldValue: any, newValue: any }>}
*/
this._keys = null;
/**
* @type {null | Array<{ insert?: string | Array<any> | object | AbstractType<any>, retain?: number, delete?: number, attributes?: Object<string, any> }>}
*/
this._delta = null;
}
/**
* Computes the path from `y` to the changed type.
*
* @todo v14 should standardize on path: Array<{parent, index}> because that is easier to work with.
*
* The following property holds:
* @example
* let type = y
* event.path.forEach(dir => {
* type = type.get(dir)
* })
* type === event.target // => true
*/
get path () {
// @ts-ignore _item is defined because target is integrated
return getPathTo(this.currentTarget, this.target)
}
/**
* Check if a struct is deleted by this event.
*
* In contrast to change.deleted, this method also returns true if the struct was added and then deleted.
*
* @param {AbstractStruct} struct
* @return {boolean}
*/
deletes (struct) {
return isDeleted(this.transaction.deleteSet, struct.id)
}
/**
* @type {Map<string, { action: 'add' | 'update' | 'delete', oldValue: any, newValue: any }>}
*/
get keys () {
if (this._keys === null) {
const keys = new Map();
const target = this.target;
const changed = /** @type Set<string|null> */ (this.transaction.changed.get(target));
changed.forEach(key => {
if (key !== null) {
const item = /** @type {Item} */ (target._map.get(key));
/**
* @type {'delete' | 'add' | 'update'}
*/
let action;
let oldValue;
if (this.adds(item)) {
let prev = item.left;
while (prev !== null && this.adds(prev)) {
prev = prev.left;
}
if (this.deletes(item)) {
if (prev !== null && this.deletes(prev)) {
action = 'delete';
oldValue = last(prev.content.getContent());
} else {
return
}
} else {
if (prev !== null && this.deletes(prev)) {
action = 'update';
oldValue = last(prev.content.getContent());
} else {
action = 'add';
oldValue = undefined;
}
}
} else {
if (this.deletes(item)) {
action = 'delete';
oldValue = last(/** @type {Item} */ item.content.getContent());
} else {
return // nop
}
}
keys.set(key, { action, oldValue });
}
});
this._keys = keys;
}
return this._keys
}
/**
* This is a computed property. Note that this can only be safely computed during the
* event call. Computing this property after other changes happened might result in
* unexpected behavior (incorrect computation of deltas). A safe way to collect changes
* is to store the `changes` or the `delta` object. Avoid storing the `transaction` object.
*
* @type {Array<{insert?: string | Array<any> | object | AbstractType<any>, retain?: number, delete?: number, attributes?: Object<string, any>}>}
*/
get delta () {
return this.changes.delta
}
/**
* Check if a struct is added by this event.
*
* In contrast to change.deleted, this method also returns true if the struct was added and then deleted.
*
* @param {AbstractStruct} struct
* @return {boolean}
*/
adds (struct) {
return struct.id.clock >= (this.transaction.beforeState.get(struct.id.client) || 0)
}
/**
* This is a computed property. Note that this can only be safely computed during the
* event call. Computing this property after other changes happened might result in
* unexpected behavior (incorrect computation of deltas). A safe way to collect changes
* is to store the `changes` or the `delta` object. Avoid storing the `transaction` object.
*
* @type {{added:Set<Item>,deleted:Set<Item>,keys:Map<string,{action:'add'|'update'|'delete',oldValue:any}>,delta:Array<{insert?:Array<any>|string, delete?:number, retain?:number}>}}
*/
get changes () {
let changes = this._changes;
if (changes === null) {
const target = this.target;
const added = create$4();
const deleted = create$4();
/**
* @type {Array<{insert:Array<any>}|{delete:number}|{retain:number}>}
*/
const delta = [];
changes = {
added,
deleted,
delta,
keys: this.keys
};
const changed = /** @type Set<string|null> */ (this.transaction.changed.get(target));
if (changed.has(null)) {
/**
* @type {any}
*/
let lastOp = null;
const packOp = () => {
if (lastOp) {
delta.push(lastOp);
}
};
for (let item = target._start; item !== null; item = item.right) {
if (item.deleted) {
if (this.deletes(item) && !this.adds(item)) {
if (lastOp === null || lastOp.delete === undefined) {
packOp();
lastOp = { delete: 0 };
}
lastOp.delete += item.length;
deleted.add(item);
} // else nop
} else {
if (this.adds(item)) {
if (lastOp === null || lastOp.insert === undefined) {
packOp();
lastOp = { insert: [] };
}
lastOp.insert = lastOp.insert.concat(item.content.getContent());
added.add(item);
} else {
if (lastOp === null || lastOp.retain === undefined) {
packOp();
lastOp = { retain: 0 };
}
lastOp.retain += item.length;
}
}
}
if (lastOp !== null && lastOp.retain === undefined) {
packOp();
}
}
this._changes = changes;
}
return /** @type {any} */ (changes)
}
}
/**
* Compute the path from this type to the specified target.
*
* @example
* // `child` should be accessible via `type.get(path[0]).get(path[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 {AbstractType<any>} parent
* @param {AbstractType<any>} child target
* @return {Array<string|number>} Path to the target
*
* @private
* @function
*/
const getPathTo = (parent, child) => {
const path = [];
while (child._item !== null && child !== parent) {
if (child._item.parentSub !== null) {
// parent is map-ish
path.unshift(child._item.parentSub);
} else {
// parent is array-ish
let i = 0;
let c = /** @type {AbstractType<any>} */ (child._item.parent)._start;
while (c !== child._item && c !== null) {
if (!c.deleted) {
i++;
}
c = c.right;
}
path.unshift(i);
}
child = /** @type {AbstractType<any>} */ (child._item.parent);
}
return path
};
const maxSearchMarker = 80;
/**
* A unique timestamp that identifies each marker.
*
* Time is relative,.. this is more like an ever-increasing clock.
*
* @type {number}
*/
let globalSearchMarkerTimestamp = 0;
class ArraySearchMarker {
/**
* @param {Item} p
* @param {number} index
*/
constructor (p, index) {
p.marker = true;
this.p = p;
this.index = index;
this.timestamp = globalSearchMarkerTimestamp++;
}
}
/**
* @param {ArraySearchMarker} marker
*/
const refreshMarkerTimestamp = marker => { marker.timestamp = globalSearchMarkerTimestamp++; };
/**
* This is rather complex so this function is the only thing that should overwrite a marker
*
* @param {ArraySearchMarker} marker
* @param {Item} p
* @param {number} index
*/
const overwriteMarker = (marker, p, index) => {
marker.p.marker = false;
marker.p = p;
p.marker = true;
marker.index = index;
marker.timestamp = globalSearchMarkerTimestamp++;
};
/**
* @param {Array<ArraySearchMarker>} searchMarker
* @param {Item} p
* @param {number} index
*/
const markPosition = (searchMarker, p, index) => {
if (searchMarker.length >= maxSearchMarker) {
// override oldest marker (we don't want to create more objects)
const marker = searchMarker.reduce((a, b) => a.timestamp < b.timestamp ? a : b);
overwriteMarker(marker, p, index);
return marker
} else {
// create new marker
const pm = new ArraySearchMarker(p, index);
searchMarker.push(pm);
return pm
}
};
/**
* Search marker help us to find positions in the associative array faster.
*
* They speed up the process of finding a position without much bookkeeping.
*
* A maximum of `maxSearchMarker` objects are created.
*
* This function always returns a refreshed marker (updated timestamp)
*
* @param {AbstractType<any>} yarray
* @param {number} index
*/
const findMarker = (yarray, index) => {
if (yarray._start === null || index === 0 || yarray._searchMarker === null) {
return null
}
const marker = yarray._searchMarker.length === 0 ? null : yarray._searchMarker.reduce((a, b) => abs(index - a.index) < abs(index - b.index) ? a : b);
let p = yarray._start;
let pindex = 0;
if (marker !== null) {
p = marker.p;
pindex = marker.index;
refreshMarkerTimestamp(marker); // we used it, we might need to use it again
}
// iterate to right if possible
while (p.right !== null && pindex < index) {
if (!p.deleted && p.countable) {
if (index < pindex + p.length) {
break
}
pindex += p.length;
}
p = p.right;
}
// iterate to left if necessary (might be that pindex > index)
while (p.left !== null && pindex > index) {
p = p.left;
if (!p.deleted && p.countable) {
pindex -= p.length;
}
}
// we want to make sure that p can't be merged with left, because that would screw up everything
// in that cas just return what we have (it is most likely the best marker anyway)
// iterate to left until p can't be merged with left
while (p.left !== null && p.left.id.client === p.id.client && p.left.id.clock + p.left.length === p.id.clock) {
p = p.left;
if (!p.deleted && p.countable) {
pindex -= p.length;
}
}
// @todo remove!
// assure position
// {
// let start = yarray._start
// let pos = 0
// while (start !== p) {
// if (!start.deleted && start.countable) {
// pos += start.length
// }
// start = /** @type {Item} */ (start.right)
// }
// if (pos !== pindex) {
// debugger
// throw new Error('Gotcha position fail!')
// }
// }
// if (marker) {
// if (window.lengthes == null) {
// window.lengthes = []
// window.getLengthes = () => window.lengthes.sort((a, b) => a - b)
// }
// window.lengthes.push(marker.index - pindex)
// console.log('distance', marker.index - pindex, 'len', p && p.parent.length)
// }
if (marker !== null && abs(marker.index - pindex) < /** @type {YText|YArray<any>} */ (p.parent).length / maxSearchMarker) {
// adjust existing marker
overwriteMarker(marker, p, pindex);
return marker
} else {
// create new marker
return markPosition(yarray._searchMarker, p, pindex)
}
};
/**
* Update markers when a change happened.
*
* This should be called before doing a deletion!
*
* @param {Array<ArraySearchMarker>} searchMarker
* @param {number} index
* @param {number} len If insertion, len is positive. If deletion, len is negative.
*/
const updateMarkerChanges = (searchMarker, index, len) => {
for (let i = searchMarker.length - 1; i >= 0; i--) {
const m = searchMarker[i];
if (len > 0) {
/**
* @type {Item|null}
*/
let p = m.p;
p.marker = false;
// Ideally we just want to do a simple position comparison, but this will only work if
// search markers don't point to deleted items for formats.
// Iterate marker to prev undeleted countable position so we know what to do when updating a position
while (p && (p.deleted || !p.countable)) {
p = p.left;
if (p && !p.deleted && p.countable) {
// adjust position. the loop should break now
m.index -= p.length;
}
}
if (p === null || p.marker === true) {
// remove search marker if updated position is null or if position is already marked
searchMarker.splice(i, 1);
continue
}
m.p = p;
p.marker = true;
}
if (index < m.index || (len > 0 && index === m.index)) { // a simple index <= m.index check would actually suffice
m.index = max(index, m.index + len);
}
}
};
/**
* Call event listeners with an event. This will also add an event to all
* parents (for `.observeDeep` handlers).
*
* @template EventType
* @param {AbstractType<EventType>} type
* @param {Transaction} transaction
* @param {EventType} event
*/
const callTypeObservers = (type, transaction, event) => {
const changedType = type;
const changedParentTypes = transaction.changedParentTypes;
while (true) {
// @ts-ignore
setIfUndefined(changedParentTypes, type, () => []).push(event);
if (type._item === null) {
break
}
type = /** @type {AbstractType<any>} */ (type._item.parent);
}
callEventHandlerListeners(changedType._eH, event, transaction);
};
/**
* @template EventType
* Abstract Yjs Type class
*/
class AbstractType {
constructor () {
/**
* @type {Item|null}
*/
this._item = null;
/**
* @type {Map<string,Item>}
*/
this._map = new Map();
/**
* @type {Item|null}
*/
this._start = null;
/**
* @type {Doc|null}
*/
this.doc = null;
this._length = 0;
/**
* Event handlers
* @type {EventHandler<EventType,Transaction>}
*/
this._eH = createEventHandler();
/**
* Deep event handlers
* @type {EventHandler<Array<YEvent<any>>,Transaction>}
*/
this._dEH = createEventHandler();
/**
* @type {null | Array<ArraySearchMarker>}
*/
this._searchMarker = null;
}
/**
* @return {AbstractType<any>|null}
*/
get parent () {
return this._item ? /** @type {AbstractType<any>} */ (this._item.parent) : null
}
/**
* 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 {Doc} y The Yjs instance
* @param {Item|null} item
*/
_integrate (y, item) {
this.doc = y;
this._item = item;
}
/**
* @return {AbstractType<EventType>}
*/
_copy () {
throw methodUnimplemented()
}
/**
* @return {AbstractType<EventType>}
*/
clone () {
throw methodUnimplemented()
}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} _encoder
*/
_write (_encoder) { }
/**
* The first non-deleted item
*/
get _first () {
let n = this._start;
while (n !== null && n.deleted) {
n = n.right;
}
return n
}
/**
* Creates YEvent and calls all type observers.
* Must be implemented by each type.
*
* @param {Transaction} transaction
* @param {Set<null|string>} _parentSubs Keys changed on this type. `null` if list was modified.
*/
_callObserver (transaction, _parentSubs) {
if (!transaction.local && this._searchMarker) {
this._searchMarker.length = 0;
}
}
/**
* Observe all events that are created on this type.
*
* @param {function(EventType, Transaction):void} f Observer function
*/
observe (f) {
addEventHandlerListener(this._eH, f);
}
/**
* Observe all events that are created by this type and its children.
*
* @param {function(Array<YEvent<any>>,Transaction):void} f Observer function
*/
observeDeep (f) {
addEventHandlerListener(this._dEH, f);
}
/**
* Unregister an observer function.
*
* @param {function(EventType,Transaction):void} f Observer function
*/
unobserve (f) {
removeEventHandlerListener(this._eH, f);
}
/**
* Unregister an observer function.
*
* @param {function(Array<YEvent<any>>,Transaction):void} f Observer function
*/
unobserveDeep (f) {
removeEventHandlerListener(this._dEH, f);
}
/**
* @abstract
* @return {any}
*/
toJSON () {}
}
/**
* @param {AbstractType<any>} type
* @param {number} start
* @param {number} end
* @return {Array<any>}
*
* @private
* @function
*/
const typeListSlice = (type, start, end) => {
if (start < 0) {
start = type._length + start;
}
if (end < 0) {
end = type._length + end;
}
let len = end - start;
const cs = [];
let n = type._start;
while (n !== null && len > 0) {
if (n.countable && !n.deleted) {
const c = n.content.getContent();
if (c.length <= start) {
start -= c.length;
} else {
for (let i = start; i < c.length && len > 0; i++) {
cs.push(c[i]);
len--;
}
start = 0;
}
}
n = n.right;
}
return cs
};
/**
* @param {AbstractType<any>} type
* @return {Array<any>}
*
* @private
* @function
*/
const typeListToArray = type => {
const cs = [];
let n = type._start;
while (n !== null) {
if (n.countable && !n.deleted) {
const c = n.content.getContent();
for (let i = 0; i < c.length; i++) {
cs.push(c[i]);
}
}
n = n.right;
}
return cs
};
/**
* Executes a provided function on once on overy element of this YArray.
*
* @param {AbstractType<any>} type
* @param {function(any,number,any):void} f A function to execute on every element of this YArray.
*
* @private
* @function
*/
const typeListForEach = (type, f) => {
let index = 0;
let n = type._start;
while (n !== null) {
if (n.countable && !n.deleted) {
const c = n.content.getContent();
for (let i = 0; i < c.length; i++) {
f(c[i], index++, type);
}
}
n = n.right;
}
};
/**
* @template C,R
* @param {AbstractType<any>} type
* @param {function(C,number,AbstractType<any>):R} f
* @return {Array<R>}
*
* @private
* @function
*/
const typeListMap = (type, f) => {
/**
* @type {Array<any>}
*/
const result = [];
typeListForEach(type, (c, i) => {
result.push(f(c, i, type));
});
return result
};
/**
* @param {AbstractType<any>} type
* @return {IterableIterator<any>}
*
* @private
* @function
*/
const typeListCreateIterator = type => {
let n = type._start;
/**
* @type {Array<any>|null}
*/
let currentContent = null;
let currentContentIndex = 0;
return {
[Symbol.iterator] () {
return this
},
next: () => {
// find some content
if (currentContent === null) {
while (n !== null && n.deleted) {
n = n.right;
}
// check if we reached the end, no need to check currentContent, because it does not exist
if (n === null) {
return {
done: true,
value: undefined
}
}
// we found n, so we can set currentContent
currentContent = n.content.getContent();
currentContentIndex = 0;
n = n.right; // we used the content of n, now iterate to next
}
const value = currentContent[currentContentIndex++];
// check if we need to empty currentContent
if (currentContent.length <= currentContentIndex) {
currentContent = null;
}
return {
done: false,
value
}
}
}
};
/**
* @param {AbstractType<any>} type
* @param {number} index
* @return {any}
*
* @private
* @function
*/
const typeListGet = (type, index) => {
const marker = findMarker(type, index);
let n = type._start;
if (marker !== null) {
n = marker.p;
index -= marker.index;
}
for (; n !== null; n = n.right) {
if (!n.deleted && n.countable) {
if (index < n.length) {
return n.content.getContent()[index]
}
index -= n.length;
}
}
};
/**
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
* @param {Item?} referenceItem
* @param {Array<Object<string,any>|Array<any>|boolean|number|null|string|Uint8Array>} content
*
* @private
* @function
*/
const typeListInsertGenericsAfter = (transaction, parent, referenceItem, content) => {
let left = referenceItem;
const doc = transaction.doc;
const ownClientId = doc.clientID;
const store = doc.store;
const right = referenceItem === null ? parent._start : referenceItem.right;
/**
* @type {Array<Object|Array<any>|number|null>}
*/
let jsonContent = [];
const packJsonContent = () => {
if (jsonContent.length > 0) {
left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentAny(jsonContent));
left.integrate(transaction, 0);
jsonContent = [];
}
};
content.forEach(c => {
if (c === null) {
jsonContent.push(c);
} else {
switch (c.constructor) {
case Number:
case Object:
case Boolean:
case Array:
case String:
jsonContent.push(c);
break
default:
packJsonContent();
switch (c.constructor) {
case Uint8Array:
case ArrayBuffer:
left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentBinary(new Uint8Array(/** @type {Uint8Array} */ (c))));
left.integrate(transaction, 0);
break
case Doc:
left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentDoc(/** @type {Doc} */ (c)));
left.integrate(transaction, 0);
break
default:
if (c instanceof AbstractType) {
left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentType(c));
left.integrate(transaction, 0);
} else {
throw new Error('Unexpected content type in insert operation')
}
}
}
}
});
packJsonContent();
};
const lengthExceeded = create$3('Length exceeded!');
/**
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
* @param {number} index
* @param {Array<Object<string,any>|Array<any>|number|null|string|Uint8Array>} content
*
* @private
* @function
*/
const typeListInsertGenerics = (transaction, parent, index, content) => {
if (index > parent._length) {
throw lengthExceeded
}
if (index === 0) {
if (parent._searchMarker) {
updateMarkerChanges(parent._searchMarker, index, content.length);
}
return typeListInsertGenericsAfter(transaction, parent, null, content)
}
const startIndex = index;
const marker = findMarker(parent, index);
let n = parent._start;
if (marker !== null) {
n = marker.p;
index -= marker.index;
// we need to iterate one to the left so that the algorithm works
if (index === 0) {
// @todo refactor this as it actually doesn't consider formats
n = n.prev; // important! get the left undeleted item so that we can actually decrease index
index += (n && n.countable && !n.deleted) ? n.length : 0;
}
}
for (; n !== null; n = n.right) {
if (!n.deleted && n.countable) {
if (index <= n.length) {
if (index < n.length) {
// insert in-between
getItemCleanStart(transaction, createID(n.id.client, n.id.clock + index));
}
break
}
index -= n.length;
}
}
if (parent._searchMarker) {
updateMarkerChanges(parent._searchMarker, startIndex, content.length);
}
return typeListInsertGenericsAfter(transaction, parent, n, content)
};
/**
* Pushing content is special as we generally want to push after the last item. So we don't have to update
* the serach marker.
*
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
* @param {Array<Object<string,any>|Array<any>|number|null|string|Uint8Array>} content
*
* @private
* @function
*/
const typeListPushGenerics = (transaction, parent, content) => {
// Use the marker with the highest index and iterate to the right.
const marker = (parent._searchMarker || []).reduce((maxMarker, currMarker) => currMarker.index > maxMarker.index ? currMarker : maxMarker, { index: 0, p: parent._start });
let n = marker.p;
if (n) {
while (n.right) {
n = n.right;
}
}
return typeListInsertGenericsAfter(transaction, parent, n, content)
};
/**
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
* @param {number} index
* @param {number} length
*
* @private
* @function
*/
const typeListDelete = (transaction, parent, index, length) => {
if (length === 0) { return }
const startIndex = index;
const startLength = length;
const marker = findMarker(parent, index);
let n = parent._start;
if (marker !== null) {
n = marker.p;
index -= marker.index;
}
// compute the first item to be deleted
for (; n !== null && index > 0; n = n.right) {
if (!n.deleted && n.countable) {
if (index < n.length) {
getItemCleanStart(transaction, createID(n.id.client, n.id.clock + index));
}
index -= n.length;
}
}
// delete all items until done
while (length > 0 && n !== null) {
if (!n.deleted) {
if (length < n.length) {
getItemCleanStart(transaction, createID(n.id.client, n.id.clock + length));
}
n.delete(transaction);
length -= n.length;
}
n = n.right;
}
if (length > 0) {
throw lengthExceeded
}
if (parent._searchMarker) {
updateMarkerChanges(parent._searchMarker, startIndex, -startLength + length /* in case we remove the above exception */);
}
};
/**
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
* @param {string} key
*
* @private
* @function
*/
const typeMapDelete = (transaction, parent, key) => {
const c = parent._map.get(key);
if (c !== undefined) {
c.delete(transaction);
}
};
/**
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
* @param {string} key
* @param {Object|number|null|Array<any>|string|Uint8Array|AbstractType<any>} value
*
* @private
* @function
*/
const typeMapSet = (transaction, parent, key, value) => {
const left = parent._map.get(key) || null;
const doc = transaction.doc;
const ownClientId = doc.clientID;
let content;
if (value == null) {
content = new ContentAny([value]);
} else {
switch (value.constructor) {
case Number:
case Object:
case Boolean:
case Array:
case String:
content = new ContentAny([value]);
break
case Uint8Array:
content = new ContentBinary(/** @type {Uint8Array} */ (value));
break
case Doc:
content = new ContentDoc(/** @type {Doc} */ (value));
break
default:
if (value instanceof AbstractType) {
content = new ContentType(value);
} else {
throw new Error('Unexpected content type')
}
}
}
new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, null, null, parent, key, content).integrate(transaction, 0);
};
/**
* @param {AbstractType<any>} parent
* @param {string} key
* @return {Object<string,any>|number|null|Array<any>|string|Uint8Array|AbstractType<any>|undefined}
*
* @private
* @function
*/
const typeMapGet = (parent, key) => {
const val = parent._map.get(key);
return val !== undefined && !val.deleted ? val.content.getContent()[val.length - 1] : undefined
};
/**
* @param {AbstractType<any>} parent
* @return {Object<string,Object<string,any>|number|null|Array<any>|string|Uint8Array|AbstractType<any>|undefined>}
*
* @private
* @function
*/
const typeMapGetAll = (parent) => {
/**
* @type {Object<string,any>}
*/
const res = {};
parent._map.forEach((value, key) => {
if (!value.deleted) {
res[key] = value.content.getContent()[value.length - 1];
}
});
return res
};
/**
* @param {AbstractType<any>} parent
* @param {string} key
* @return {boolean}
*
* @private
* @function
*/
const typeMapHas = (parent, key) => {
const val = parent._map.get(key);
return val !== undefined && !val.deleted
};
/**
* @param {Map<string,Item>} map
* @return {IterableIterator<Array<any>>}
*
* @private
* @function
*/
const createMapIterator = map => iteratorFilter(map.entries(), /** @param {any} entry */ entry => !entry[1].deleted);
/**
* @module YArray
*/
/**
* Event that describes the changes on a YArray
* @template T
* @extends YEvent<YArray<T>>
*/
class YArrayEvent extends YEvent {
/**
* @param {YArray<T>} yarray The changed type
* @param {Transaction} transaction The transaction object
*/
constructor (yarray, transaction) {
super(yarray, transaction);
this._transaction = transaction;
}
}
/**
* A shared Array implementation.
* @template T
* @extends AbstractType<YArrayEvent<T>>
* @implements {Iterable<T>}
*/
class YArray extends AbstractType {
constructor () {
super();
/**
* @type {Array<any>?}
* @private
*/
this._prelimContent = [];
/**
* @type {Array<ArraySearchMarker>}
*/
this._searchMarker = [];
}
/**
* Construct a new YArray containing the specified items.
* @template {Object<string,any>|Array<any>|number|null|string|Uint8Array} T
* @param {Array<T>} items
* @return {YArray<T>}
*/
static from (items) {
/**
* @type {YArray<T>}
*/
const a = new YArray();
a.push(items);
return a
}
/**
* 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 {Doc} y The Yjs instance
* @param {Item} item
*/
_integrate (y, item) {
super._integrate(y, item);
this.insert(0, /** @type {Array<any>} */ (this._prelimContent));
this._prelimContent = null;
}
/**
* @return {YArray<T>}
*/
_copy () {
return new YArray()
}
/**
* @return {YArray<T>}
*/
clone () {
/**
* @type {YArray<T>}
*/
const arr = new YArray();
arr.insert(0, this.toArray().map(el =>
el instanceof AbstractType ? /** @type {typeof el} */ (el.clone()) : el
));
return arr
}
get length () {
return this._prelimContent === null ? this._length : this._prelimContent.length
}
/**
* Creates YArrayEvent and calls observers.
*
* @param {Transaction} transaction
* @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
*/
_callObserver (transaction, parentSubs) {
super._callObserver(transaction, parentSubs);
callTypeObservers(this, transaction, new YArrayEvent(this, transaction));
}
/**
* 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(1, [1, 2])
*
* @param {number} index The index to insert content at.
* @param {Array<T>} content The array of content
*/
insert (index, content) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeListInsertGenerics(transaction, this, index, /** @type {any} */ (content));
});
} else {
/** @type {Array<any>} */ (this._prelimContent).splice(index, 0, ...content);
}
}
/**
* Appends content to this YArray.
*
* @param {Array<T>} content Array of content to append.
*
* @todo Use the following implementation in all types.
*/
push (content) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeListPushGenerics(transaction, this, /** @type {any} */ (content));
});
} else {
/** @type {Array<any>} */ (this._prelimContent).push(...content);
}
}
/**
* Preppends content to this YArray.
*
* @param {Array<T>} content Array of content to preppend.
*/
unshift (content) {
this.insert(0, content);
}
/**
* Deletes elements starting from an index.
*
* @param {number} index Index at which to start deleting elements
* @param {number} length The number of elements to remove. Defaults to 1.
*/
delete (index, length = 1) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeListDelete(transaction, this, index, length);
});
} else {
/** @type {Array<any>} */ (this._prelimContent).splice(index, length);
}
}
/**
* Returns the i-th element from a YArray.
*
* @param {number} index The index of the element to return from the YArray
* @return {T}
*/
get (index) {
return typeListGet(this, index)
}
/**
* Transforms this YArray to a JavaScript Array.
*
* @return {Array<T>}
*/
toArray () {
return typeListToArray(this)
}
/**
* Transforms this YArray to a JavaScript Array.
*
* @param {number} [start]
* @param {number} [end]
* @return {Array<T>}
*/
slice (start = 0, end = this.length) {
return typeListSlice(this, start, end)
}
/**
* Transforms this Shared Type to a JSON object.
*
* @return {Array<any>}
*/
toJSON () {
return this.map(c => c instanceof AbstractType ? c.toJSON() : c)
}
/**
* Returns an Array with the result of calling a provided function on every
* element of this YArray.
*
* @template M
* @param {function(T,number,YArray<T>):M} f Function that produces an element of the new Array
* @return {Array<M>} A new array with each element being the result of the
* callback function
*/
map (f) {
return typeListMap(this, /** @type {any} */ (f))
}
/**
* Executes a provided function once on overy element of this YArray.
*
* @param {function(T,number,YArray<T>):void} f A function to execute on every element of this YArray.
*/
forEach (f) {
typeListForEach(this, f);
}
/**
* @return {IterableIterator<T>}
*/
[Symbol.iterator] () {
return typeListCreateIterator(this)
}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
*/
_write (encoder) {
encoder.writeTypeRef(YArrayRefID);
}
}
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder
*
* @private
* @function
*/
const readYArray = _decoder => new YArray();
/**
* @template T
* @extends YEvent<YMap<T>>
* Event that describes the changes on a YMap.
*/
class YMapEvent extends YEvent {
/**
* @param {YMap<T>} ymap The YArray that changed.
* @param {Transaction} transaction
* @param {Set<any>} subs The keys that changed.
*/
constructor (ymap, transaction, subs) {
super(ymap, transaction);
this.keysChanged = subs;
}
}
/**
* @template MapType
* A shared Map implementation.
*
* @extends AbstractType<YMapEvent<MapType>>
* @implements {Iterable<MapType>}
*/
class YMap extends AbstractType {
/**
*
* @param {Iterable<readonly [string, any]>=} entries - an optional iterable to initialize the YMap
*/
constructor (entries) {
super();
/**
* @type {Map<string,any>?}
* @private
*/
this._prelimContent = null;
if (entries === undefined) {
this._prelimContent = new Map();
} else {
this._prelimContent = new Map(entries);
}
}
/**
* 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 {Doc} y The Yjs instance
* @param {Item} item
*/
_integrate (y, item) {
super._integrate(y, item)
;/** @type {Map<string, any>} */ (this._prelimContent).forEach((value, key) => {
this.set(key, value);
});
this._prelimContent = null;
}
/**
* @return {YMap<MapType>}
*/
_copy () {
return new YMap()
}
/**
* @return {YMap<MapType>}
*/
clone () {
/**
* @type {YMap<MapType>}
*/
const map = new YMap();
this.forEach((value, key) => {
map.set(key, value instanceof AbstractType ? /** @type {typeof value} */ (value.clone()) : value);
});
return map
}
/**
* Creates YMapEvent and calls observers.
*
* @param {Transaction} transaction
* @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
*/
_callObserver (transaction, parentSubs) {
callTypeObservers(this, transaction, new YMapEvent(this, transaction, parentSubs));
}
/**
* Transforms this Shared Type to a JSON object.
*
* @return {Object<string,any>}
*/
toJSON () {
/**
* @type {Object<string,MapType>}
*/
const map = {};
this._map.forEach((item, key) => {
if (!item.deleted) {
const v = item.content.getContent()[item.length - 1];
map[key] = v instanceof AbstractType ? v.toJSON() : v;
}
});
return map
}
/**
* Returns the size of the YMap (count of key/value pairs)
*
* @return {number}
*/
get size () {
return [...createMapIterator(this._map)].length
}
/**
* Returns the keys for each element in the YMap Type.
*
* @return {IterableIterator<string>}
*/
keys () {
return iteratorMap(createMapIterator(this._map), /** @param {any} v */ v => v[0])
}
/**
* Returns the values for each element in the YMap Type.
*
* @return {IterableIterator<any>}
*/
values () {
return iteratorMap(createMapIterator(this._map), /** @param {any} v */ v => v[1].content.getContent()[v[1].length - 1])
}
/**
* Returns an Iterator of [key, value] pairs
*
* @return {IterableIterator<any>}
*/
entries () {
return iteratorMap(createMapIterator(this._map), /** @param {any} v */ v => [v[0], v[1].content.getContent()[v[1].length - 1]])
}
/**
* Executes a provided function on once on every key-value pair.
*
* @param {function(MapType,string,YMap<MapType>):void} f A function to execute on every element of this YArray.
*/
forEach (f) {
this._map.forEach((item, key) => {
if (!item.deleted) {
f(item.content.getContent()[item.length - 1], key, this);
}
});
}
/**
* Returns an Iterator of [key, value] pairs
*
* @return {IterableIterator<any>}
*/
[Symbol.iterator] () {
return this.entries()
}
/**
* Remove a specified element from this YMap.
*
* @param {string} key The key of the element to remove.
*/
delete (key) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeMapDelete(transaction, this, key);
});
} else {
/** @type {Map<string, any>} */ (this._prelimContent).delete(key);
}
}
/**
* Adds or updates an element with a specified key and value.
*
* @param {string} key The key of the element to add to this YMap
* @param {MapType} value The value of the element to add
*/
set (key, value) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeMapSet(transaction, this, key, /** @type {any} */ (value));
});
} else {
/** @type {Map<string, any>} */ (this._prelimContent).set(key, value);
}
return value
}
/**
* Returns a specified element from this YMap.
*
* @param {string} key
* @return {MapType|undefined}
*/
get (key) {
return /** @type {any} */ (typeMapGet(this, key))
}
/**
* Returns a boolean indicating whether the specified key exists or not.
*
* @param {string} key The key to test.
* @return {boolean}
*/
has (key) {
return typeMapHas(this, key)
}
/**
* Removes all elements from this YMap.
*/
clear () {
if (this.doc !== null) {
transact(this.doc, transaction => {
this.forEach(function (_value, key, map) {
typeMapDelete(transaction, map, key);
});
});
} else {
/** @type {Map<string, any>} */ (this._prelimContent).clear();
}
}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
*/
_write (encoder) {
encoder.writeTypeRef(YMapRefID);
}
}
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder
*
* @private
* @function
*/
const readYMap = _decoder => new YMap();
/**
* @param {any} a
* @param {any} b
* @return {boolean}
*/
const equalAttrs = (a, b) => a === b || (typeof a === 'object' && typeof b === 'object' && a && b && equalFlat(a, b));
class ItemTextListPosition {
/**
* @param {Item|null} left
* @param {Item|null} right
* @param {number} index
* @param {Map<string,any>} currentAttributes
*/
constructor (left, right, index, currentAttributes) {
this.left = left;
this.right = right;
this.index = index;
this.currentAttributes = currentAttributes;
}
/**
* Only call this if you know that this.right is defined
*/
forward () {
if (this.right === null) {
unexpectedCase();
}
switch (this.right.content.constructor) {
case ContentFormat:
if (!this.right.deleted) {
updateCurrentAttributes(this.currentAttributes, /** @type {ContentFormat} */ (this.right.content));
}
break
default:
if (!this.right.deleted) {
this.index += this.right.length;
}
break
}
this.left = this.right;
this.right = this.right.right;
}
}
/**
* @param {Transaction} transaction
* @param {ItemTextListPosition} pos
* @param {number} count steps to move forward
* @return {ItemTextListPosition}
*
* @private
* @function
*/
const findNextPosition = (transaction, pos, count) => {
while (pos.right !== null && count > 0) {
switch (pos.right.content.constructor) {
case ContentFormat:
if (!pos.right.deleted) {
updateCurrentAttributes(pos.currentAttributes, /** @type {ContentFormat} */ (pos.right.content));
}
break
default:
if (!pos.right.deleted) {
if (count < pos.right.length) {
// split right
getItemCleanStart(transaction, createID(pos.right.id.client, pos.right.id.clock + count));
}
pos.index += pos.right.length;
count -= pos.right.length;
}
break
}
pos.left = pos.right;
pos.right = pos.right.right;
// pos.forward() - we don't forward because that would halve the performance because we already do the checks above
}
return pos
};
/**
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
* @param {number} index
* @return {ItemTextListPosition}
*
* @private
* @function
*/
const findPosition = (transaction, parent, index) => {
const currentAttributes = new Map();
const marker = findMarker(parent, index);
if (marker) {
const pos = new ItemTextListPosition(marker.p.left, marker.p, marker.index, currentAttributes);
return findNextPosition(transaction, pos, index - marker.index)
} else {
const pos = new ItemTextListPosition(null, parent._start, 0, currentAttributes);
return findNextPosition(transaction, pos, index)
}
};
/**
* Negate applied formats
*
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
* @param {ItemTextListPosition} currPos
* @param {Map<string,any>} negatedAttributes
*
* @private
* @function
*/
const insertNegatedAttributes = (transaction, parent, currPos, negatedAttributes) => {
// check if we really need to remove attributes
while (
currPos.right !== null && (
currPos.right.deleted === true || (
currPos.right.content.constructor === ContentFormat &&
equalAttrs(negatedAttributes.get(/** @type {ContentFormat} */ (currPos.right.content).key), /** @type {ContentFormat} */ (currPos.right.content).value)
)
)
) {
if (!currPos.right.deleted) {
negatedAttributes.delete(/** @type {ContentFormat} */ (currPos.right.content).key);
}
currPos.forward();
}
const doc = transaction.doc;
const ownClientId = doc.clientID;
negatedAttributes.forEach((val, key) => {
const left = currPos.left;
const right = currPos.right;
const nextFormat = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val));
nextFormat.integrate(transaction, 0);
currPos.right = nextFormat;
currPos.forward();
});
};
/**
* @param {Map<string,any>} currentAttributes
* @param {ContentFormat} format
*
* @private
* @function
*/
const updateCurrentAttributes = (currentAttributes, format) => {
const { key, value } = format;
if (value === null) {
currentAttributes.delete(key);
} else {
currentAttributes.set(key, value);
}
};
/**
* @param {ItemTextListPosition} currPos
* @param {Object<string,any>} attributes
*
* @private
* @function
*/
const minimizeAttributeChanges = (currPos, attributes) => {
// go right while attributes[right.key] === right.value (or right is deleted)
while (true) {
if (currPos.right === null) {
break
} else if (currPos.right.deleted || (currPos.right.content.constructor === ContentFormat && equalAttrs(attributes[(/** @type {ContentFormat} */ (currPos.right.content)).key] || null, /** @type {ContentFormat} */ (currPos.right.content).value))) ; else {
break
}
currPos.forward();
}
};
/**
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
* @param {ItemTextListPosition} currPos
* @param {Object<string,any>} attributes
* @return {Map<string,any>}
*
* @private
* @function
**/
const insertAttributes = (transaction, parent, currPos, attributes) => {
const doc = transaction.doc;
const ownClientId = doc.clientID;
const negatedAttributes = new Map();
// insert format-start items
for (const key in attributes) {
const val = attributes[key];
const currentVal = currPos.currentAttributes.get(key) || null;
if (!equalAttrs(currentVal, val)) {
// save negated attribute (set null if currentVal undefined)
negatedAttributes.set(key, currentVal);
const { left, right } = currPos;
currPos.right = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val));
currPos.right.integrate(transaction, 0);
currPos.forward();
}
}
return negatedAttributes
};
/**
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
* @param {ItemTextListPosition} currPos
* @param {string|object|AbstractType<any>} text
* @param {Object<string,any>} attributes
*
* @private
* @function
**/
const insertText = (transaction, parent, currPos, text, attributes) => {
currPos.currentAttributes.forEach((_val, key) => {
if (attributes[key] === undefined) {
attributes[key] = null;
}
});
const doc = transaction.doc;
const ownClientId = doc.clientID;
minimizeAttributeChanges(currPos, attributes);
const negatedAttributes = insertAttributes(transaction, parent, currPos, attributes);
// insert content
const content = text.constructor === String ? new ContentString(/** @type {string} */ (text)) : (text instanceof AbstractType ? new ContentType(text) : new ContentEmbed(text));
let { left, right, index } = currPos;
if (parent._searchMarker) {
updateMarkerChanges(parent._searchMarker, currPos.index, content.getLength());
}
right = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, content);
right.integrate(transaction, 0);
currPos.right = right;
currPos.index = index;
currPos.forward();
insertNegatedAttributes(transaction, parent, currPos, negatedAttributes);
};
/**
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
* @param {ItemTextListPosition} currPos
* @param {number} length
* @param {Object<string,any>} attributes
*
* @private
* @function
*/
const formatText = (transaction, parent, currPos, length, attributes) => {
const doc = transaction.doc;
const ownClientId = doc.clientID;
minimizeAttributeChanges(currPos, attributes);
const negatedAttributes = insertAttributes(transaction, parent, currPos, attributes);
// iterate until first non-format or null is found
// delete all formats with attributes[format.key] != null
// also check the attributes after the first non-format as we do not want to insert redundant negated attributes there
// eslint-disable-next-line no-labels
iterationLoop: while (
currPos.right !== null &&
(length > 0 ||
(
negatedAttributes.size > 0 &&
(currPos.right.deleted || currPos.right.content.constructor === ContentFormat)
)
)
) {
if (!currPos.right.deleted) {
switch (currPos.right.content.constructor) {
case ContentFormat: {
const { key, value } = /** @type {ContentFormat} */ (currPos.right.content);
const attr = attributes[key];
if (attr !== undefined) {
if (equalAttrs(attr, value)) {
negatedAttributes.delete(key);
} else {
if (length === 0) {
// no need to further extend negatedAttributes
// eslint-disable-next-line no-labels
break iterationLoop
}
negatedAttributes.set(key, value);
}
currPos.right.delete(transaction);
} else {
currPos.currentAttributes.set(key, value);
}
break
}
default:
if (length < currPos.right.length) {
getItemCleanStart(transaction, createID(currPos.right.id.client, currPos.right.id.clock + length));
}
length -= currPos.right.length;
break
}
}
currPos.forward();
}
// Quill just assumes that the editor starts with a newline and that it always
// ends with a newline. We only insert that newline when a new newline is
// inserted - i.e when length is bigger than type.length
if (length > 0) {
let newlines = '';
for (; length > 0; length--) {
newlines += '\n';
}
currPos.right = new Item(createID(ownClientId, getState(doc.store, ownClientId)), currPos.left, currPos.left && currPos.left.lastId, currPos.right, currPos.right && currPos.right.id, parent, null, new ContentString(newlines));
currPos.right.integrate(transaction, 0);
currPos.forward();
}
insertNegatedAttributes(transaction, parent, currPos, negatedAttributes);
};
/**
* Call this function after string content has been deleted in order to
* clean up formatting Items.
*
* @param {Transaction} transaction
* @param {Item} start
* @param {Item|null} curr exclusive end, automatically iterates to the next Content Item
* @param {Map<string,any>} startAttributes
* @param {Map<string,any>} currAttributes
* @return {number} The amount of formatting Items deleted.
*
* @function
*/
const cleanupFormattingGap = (transaction, start, curr, startAttributes, currAttributes) => {
/**
* @type {Item|null}
*/
let end = start;
/**
* @type {Map<string,ContentFormat>}
*/
const endFormats = create$5();
while (end && (!end.countable || end.deleted)) {
if (!end.deleted && end.content.constructor === ContentFormat) {
const cf = /** @type {ContentFormat} */ (end.content);
endFormats.set(cf.key, cf);
}
end = end.right;
}
let cleanups = 0;
let reachedCurr = false;
while (start !== end) {
if (curr === start) {
reachedCurr = true;
}
if (!start.deleted) {
const content = start.content;
switch (content.constructor) {
case ContentFormat: {
const { key, value } = /** @type {ContentFormat} */ (content);
const startAttrValue = startAttributes.get(key) || null;
if (endFormats.get(key) !== content || startAttrValue === value) {
// Either this format is overwritten or it is not necessary because the attribute already existed.
start.delete(transaction);
cleanups++;
if (!reachedCurr && (currAttributes.get(key) || null) === value && startAttrValue !== value) {
if (startAttrValue === null) {
currAttributes.delete(key);
} else {
currAttributes.set(key, startAttrValue);
}
}
}
if (!reachedCurr && !start.deleted) {
updateCurrentAttributes(currAttributes, /** @type {ContentFormat} */ (content));
}
break
}
}
}
start = /** @type {Item} */ (start.right);
}
return cleanups
};
/**
* @param {Transaction} transaction
* @param {Item | null} item
*/
const cleanupContextlessFormattingGap = (transaction, item) => {
// iterate until item.right is null or content
while (item && item.right && (item.right.deleted || !item.right.countable)) {
item = item.right;
}
const attrs = new Set();
// iterate back until a content item is found
while (item && (item.deleted || !item.countable)) {
if (!item.deleted && item.content.constructor === ContentFormat) {
const key = /** @type {ContentFormat} */ (item.content).key;
if (attrs.has(key)) {
item.delete(transaction);
} else {
attrs.add(key);
}
}
item = item.left;
}
};
/**
* This function is experimental and subject to change / be removed.
*
* Ideally, we don't need this function at all. Formatting attributes should be cleaned up
* automatically after each change. This function iterates twice over the complete YText type
* and removes unnecessary formatting attributes. This is also helpful for testing.
*
* This function won't be exported anymore as soon as there is confidence that the YText type works as intended.
*
* @param {YText} type
* @return {number} How many formatting attributes have been cleaned up.
*/
const cleanupYTextFormatting = type => {
let res = 0;
transact(/** @type {Doc} */ (type.doc), transaction => {
let start = /** @type {Item} */ (type._start);
let end = type._start;
let startAttributes = create$5();
const currentAttributes = copy(startAttributes);
while (end) {
if (end.deleted === false) {
switch (end.content.constructor) {
case ContentFormat:
updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (end.content));
break
default:
res += cleanupFormattingGap(transaction, start, end, startAttributes, currentAttributes);
startAttributes = copy(currentAttributes);
start = end;
break
}
}
end = end.right;
}
});
return res
};
/**
* @param {Transaction} transaction
* @param {ItemTextListPosition} currPos
* @param {number} length
* @return {ItemTextListPosition}
*
* @private
* @function
*/
const deleteText = (transaction, currPos, length) => {
const startLength = length;
const startAttrs = copy(currPos.currentAttributes);
const start = currPos.right;
while (length > 0 && currPos.right !== null) {
if (currPos.right.deleted === false) {
switch (currPos.right.content.constructor) {
case ContentType:
case ContentEmbed:
case ContentString:
if (length < currPos.right.length) {
getItemCleanStart(transaction, createID(currPos.right.id.client, currPos.right.id.clock + length));
}
length -= currPos.right.length;
currPos.right.delete(transaction);
break
}
}
currPos.forward();
}
if (start) {
cleanupFormattingGap(transaction, start, currPos.right, startAttrs, currPos.currentAttributes);
}
const parent = /** @type {AbstractType<any>} */ (/** @type {Item} */ (currPos.left || currPos.right).parent);
if (parent._searchMarker) {
updateMarkerChanges(parent._searchMarker, currPos.index, -startLength + length);
}
return currPos
};
/**
* 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' } }
* ]
* }
*
*/
/**
* Attributes that can be assigned to a selection of text.
*
* @example
* {
* bold: true,
* font-size: '40px'
* }
*
* @typedef {Object} TextAttributes
*/
/**
* @extends YEvent<YText>
* Event that describes the changes on a YText type.
*/
class YTextEvent extends YEvent {
/**
* @param {YText} ytext
* @param {Transaction} transaction
* @param {Set<any>} subs The keys that changed
*/
constructor (ytext, transaction, subs) {
super(ytext, transaction);
/**
* Whether the children changed.
* @type {Boolean}
* @private
*/
this.childListChanged = false;
/**
* Set of all changed attributes.
* @type {Set<string>}
*/
this.keysChanged = new Set();
subs.forEach((sub) => {
if (sub === null) {
this.childListChanged = true;
} else {
this.keysChanged.add(sub);
}
});
}
/**
* @type {{added:Set<Item>,deleted:Set<Item>,keys:Map<string,{action:'add'|'update'|'delete',oldValue:any}>,delta:Array<{insert?:Array<any>|string, delete?:number, retain?:number}>}}
*/
get changes () {
if (this._changes === null) {
/**
* @type {{added:Set<Item>,deleted:Set<Item>,keys:Map<string,{action:'add'|'update'|'delete',oldValue:any}>,delta:Array<{insert?:Array<any>|string|AbstractType<any>|object, delete?:number, retain?:number}>}}
*/
const changes = {
keys: this.keys,
delta: this.delta,
added: new Set(),
deleted: new Set()
};
this._changes = changes;
}
return /** @type {any} */ (this._changes)
}
/**
* Compute the changes in the delta format.
* A {@link https://quilljs.com/docs/delta/|Quill Delta}) that represents the changes on the document.
*
* @type {Array<{insert?:string|object|AbstractType<any>, delete?:number, retain?:number, attributes?: Object<string,any>}>}
*
* @public
*/
get delta () {
if (this._delta === null) {
const y = /** @type {Doc} */ (this.target.doc);
/**
* @type {Array<{insert?:string|object|AbstractType<any>, delete?:number, retain?:number, attributes?: Object<string,any>}>}
*/
const delta = [];
transact(y, transaction => {
const currentAttributes = new Map(); // saves all current attributes for insert
const oldAttributes = new Map();
let item = this.target._start;
/**
* @type {string?}
*/
let action = null;
/**
* @type {Object<string,any>}
*/
const attributes = {}; // counts added or removed new attributes for retain
/**
* @type {string|object}
*/
let insert = '';
let retain = 0;
let deleteLen = 0;
const addOp = () => {
if (action !== null) {
/**
* @type {any}
*/
let op = null;
switch (action) {
case 'delete':
if (deleteLen > 0) {
op = { delete: deleteLen };
}
deleteLen = 0;
break
case 'insert':
if (typeof insert === 'object' || insert.length > 0) {
op = { insert };
if (currentAttributes.size > 0) {
op.attributes = {};
currentAttributes.forEach((value, key) => {
if (value !== null) {
op.attributes[key] = value;
}
});
}
}
insert = '';
break
case 'retain':
if (retain > 0) {
op = { retain };
if (!isEmpty(attributes)) {
op.attributes = assign({}, attributes);
}
}
retain = 0;
break
}
if (op) delta.push(op);
action = null;
}
};
while (item !== null) {
switch (item.content.constructor) {
case ContentType:
case ContentEmbed:
if (this.adds(item)) {
if (!this.deletes(item)) {
addOp();
action = 'insert';
insert = item.content.getContent()[0];
addOp();
}
} else if (this.deletes(item)) {
if (action !== 'delete') {
addOp();
action = 'delete';
}
deleteLen += 1;
} else if (!item.deleted) {
if (action !== 'retain') {
addOp();
action = 'retain';
}
retain += 1;
}
break
case ContentString:
if (this.adds(item)) {
if (!this.deletes(item)) {
if (action !== 'insert') {
addOp();
action = 'insert';
}
insert += /** @type {ContentString} */ (item.content).str;
}
} else if (this.deletes(item)) {
if (action !== 'delete') {
addOp();
action = 'delete';
}
deleteLen += item.length;
} else if (!item.deleted) {
if (action !== 'retain') {
addOp();
action = 'retain';
}
retain += item.length;
}
break
case ContentFormat: {
const { key, value } = /** @type {ContentFormat} */ (item.content);
if (this.adds(item)) {
if (!this.deletes(item)) {
const curVal = currentAttributes.get(key) || null;
if (!equalAttrs(curVal, value)) {
if (action === 'retain') {
addOp();
}
if (equalAttrs(value, (oldAttributes.get(key) || null))) {
delete attributes[key];
} else {
attributes[key] = value;
}
} else if (value !== null) {
item.delete(transaction);
}
}
} else if (this.deletes(item)) {
oldAttributes.set(key, value);
const curVal = currentAttributes.get(key) || null;
if (!equalAttrs(curVal, value)) {
if (action === 'retain') {
addOp();
}
attributes[key] = curVal;
}
} else if (!item.deleted) {
oldAttributes.set(key, value);
const attr = attributes[key];
if (attr !== undefined) {
if (!equalAttrs(attr, value)) {
if (action === 'retain') {
addOp();
}
if (value === null) {
delete attributes[key];
} else {
attributes[key] = value;
}
} else if (attr !== null) { // this will be cleaned up automatically by the contextless cleanup function
item.delete(transaction);
}
}
}
if (!item.deleted) {
if (action === 'insert') {
addOp();
}
updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (item.content));
}
break
}
}
item = item.right;
}
addOp();
while (delta.length > 0) {
const lastOp = delta[delta.length - 1];
if (lastOp.retain !== undefined && lastOp.attributes === undefined) {
// retain delta's if they don't assign attributes
delta.pop();
} else {
break
}
}
});
this._delta = delta;
}
return /** @type {any} */ (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*).
*
* @extends AbstractType<YTextEvent>
*/
class YText extends AbstractType {
/**
* @param {String} [string] The initial value of the YText.
*/
constructor (string) {
super();
/**
* Array of pending operations on this type
* @type {Array<function():void>?}
*/
this._pending = string !== undefined ? [() => this.insert(0, string)] : [];
/**
* @type {Array<ArraySearchMarker>}
*/
this._searchMarker = [];
}
/**
* Number of characters of this text type.
*
* @type {number}
*/
get length () {
return this._length
}
/**
* @param {Doc} y
* @param {Item} item
*/
_integrate (y, item) {
super._integrate(y, item);
try {
/** @type {Array<function>} */ (this._pending).forEach(f => f());
} catch (e) {
console.error(e);
}
this._pending = null;
}
_copy () {
return new YText()
}
/**
* @return {YText}
*/
clone () {
const text = new YText();
text.applyDelta(this.toDelta());
return text
}
/**
* Creates YTextEvent and calls observers.
*
* @param {Transaction} transaction
* @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
*/
_callObserver (transaction, parentSubs) {
super._callObserver(transaction, parentSubs);
const event = new YTextEvent(this, transaction, parentSubs);
const doc = transaction.doc;
callTypeObservers(this, transaction, event);
// If a remote change happened, we try to cleanup potential formatting duplicates.
if (!transaction.local) {
// check if another formatting item was inserted
let foundFormattingItem = false;
for (const [client, afterClock] of transaction.afterState.entries()) {
const clock = transaction.beforeState.get(client) || 0;
if (afterClock === clock) {
continue
}
iterateStructs(transaction, /** @type {Array<Item|GC>} */ (doc.store.clients.get(client)), clock, afterClock, item => {
if (!item.deleted && /** @type {Item} */ (item).content.constructor === ContentFormat) {
foundFormattingItem = true;
}
});
if (foundFormattingItem) {
break
}
}
if (!foundFormattingItem) {
iterateDeletedStructs(transaction, transaction.deleteSet, item => {
if (item instanceof GC || foundFormattingItem) {
return
}
if (item.parent === this && item.content.constructor === ContentFormat) {
foundFormattingItem = true;
}
});
}
transact(doc, (t) => {
if (foundFormattingItem) {
// If a formatting item was inserted, we simply clean the whole type.
// We need to compute currentAttributes for the current position anyway.
cleanupYTextFormatting(this);
} else {
// If no formatting attribute was inserted, we can make due with contextless
// formatting cleanups.
// Contextless: it is not necessary to compute currentAttributes for the affected position.
iterateDeletedStructs(t, t.deleteSet, item => {
if (item instanceof GC) {
return
}
if (item.parent === this) {
cleanupContextlessFormattingGap(t, item);
}
});
}
});
}
}
/**
* Returns the unformatted string representation of this YText type.
*
* @public
*/
toString () {
let str = '';
/**
* @type {Item|null}
*/
let n = this._start;
while (n !== null) {
if (!n.deleted && n.countable && n.content.constructor === ContentString) {
str += /** @type {ContentString} */ (n.content).str;
}
n = n.right;
}
return str
}
/**
* Returns the unformatted string representation of this YText type.
*
* @return {string}
* @public
*/
toJSON () {
return this.toString()
}
/**
* Apply a {@link Delta} on this shared YText type.
*
* @param {any} delta The changes to apply on this element.
* @param {object} opts
* @param {boolean} [opts.sanitize] Sanitize input delta. Removes ending newlines if set to true.
*
*
* @public
*/
applyDelta (delta, { sanitize = true } = {}) {
if (this.doc !== null) {
transact(this.doc, transaction => {
const currPos = new ItemTextListPosition(null, this._start, 0, new Map());
for (let i = 0; i < delta.length; i++) {
const op = delta[i];
if (op.insert !== undefined) {
// Quill assumes that the content starts with an empty paragraph.
// Yjs/Y.Text assumes that it starts empty. We always hide that
// there is a newline at the end of the content.
// If we omit this step, clients will see a different number of
// paragraphs, but nothing bad will happen.
const ins = (!sanitize && typeof op.insert === 'string' && i === delta.length - 1 && currPos.right === null && op.insert.slice(-1) === '\n') ? op.insert.slice(0, -1) : op.insert;
if (typeof ins !== 'string' || ins.length > 0) {
insertText(transaction, this, currPos, ins, op.attributes || {});
}
} else if (op.retain !== undefined) {
formatText(transaction, this, currPos, op.retain, op.attributes || {});
} else if (op.delete !== undefined) {
deleteText(transaction, currPos, op.delete);
}
}
});
} else {
/** @type {Array<function>} */ (this._pending).push(() => this.applyDelta(delta));
}
}
/**
* Returns the Delta representation of this YText type.
*
* @param {Snapshot} [snapshot]
* @param {Snapshot} [prevSnapshot]
* @param {function('removed' | 'added', ID):any} [computeYChange]
* @return {any} The Delta representation of this type.
*
* @public
*/
toDelta (snapshot, prevSnapshot, computeYChange) {
/**
* @type{Array<any>}
*/
const ops = [];
const currentAttributes = new Map();
const doc = /** @type {Doc} */ (this.doc);
let str = '';
let n = this._start;
function packStr () {
if (str.length > 0) {
// pack str with attributes to ops
/**
* @type {Object<string,any>}
*/
const attributes = {};
let addAttributes = false;
currentAttributes.forEach((value, key) => {
addAttributes = true;
attributes[key] = value;
});
/**
* @type {Object<string,any>}
*/
const op = { insert: str };
if (addAttributes) {
op.attributes = attributes;
}
ops.push(op);
str = '';
}
}
const computeDelta = () => {
while (n !== null) {
if (isVisible(n, snapshot) || (prevSnapshot !== undefined && isVisible(n, prevSnapshot))) {
switch (n.content.constructor) {
case ContentString: {
const cur = currentAttributes.get('ychange');
if (snapshot !== undefined && !isVisible(n, snapshot)) {
if (cur === undefined || cur.user !== n.id.client || cur.type !== 'removed') {
packStr();
currentAttributes.set('ychange', computeYChange ? computeYChange('removed', n.id) : { type: 'removed' });
}
} else if (prevSnapshot !== undefined && !isVisible(n, prevSnapshot)) {
if (cur === undefined || cur.user !== n.id.client || cur.type !== 'added') {
packStr();
currentAttributes.set('ychange', computeYChange ? computeYChange('added', n.id) : { type: 'added' });
}
} else if (cur !== undefined) {
packStr();
currentAttributes.delete('ychange');
}
str += /** @type {ContentString} */ (n.content).str;
break
}
case ContentType:
case ContentEmbed: {
packStr();
/**
* @type {Object<string,any>}
*/
const op = {
insert: n.content.getContent()[0]
};
if (currentAttributes.size > 0) {
const attrs = /** @type {Object<string,any>} */ ({});
op.attributes = attrs;
currentAttributes.forEach((value, key) => {
attrs[key] = value;
});
}
ops.push(op);
break
}
case ContentFormat:
if (isVisible(n, snapshot)) {
packStr();
updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (n.content));
}
break
}
}
n = n.right;
}
packStr();
};
if (snapshot || prevSnapshot) {
// snapshots are merged again after the transaction, so we need to keep the
// transaction alive until we are done
transact(doc, transaction => {
if (snapshot) {
splitSnapshotAffectedStructs(transaction, snapshot);
}
if (prevSnapshot) {
splitSnapshotAffectedStructs(transaction, prevSnapshot);
}
computeDelta();
}, 'cleanup');
} else {
computeDelta();
}
return ops
}
/**
* Insert text at a given index.
*
* @param {number} 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
}
const y = this.doc;
if (y !== null) {
transact(y, transaction => {
const pos = findPosition(transaction, this, index);
if (!attributes) {
attributes = {};
// @ts-ignore
pos.currentAttributes.forEach((v, k) => { attributes[k] = v; });
}
insertText(transaction, this, pos, text, attributes);
});
} else {
/** @type {Array<function>} */ (this._pending).push(() => this.insert(index, text, attributes));
}
}
/**
* Inserts an embed at a index.
*
* @param {number} index The index to insert the embed at.
* @param {Object | AbstractType<any>} embed The Object that represents the embed.
* @param {TextAttributes} attributes Attribute information to apply on the
* embed
*
* @public
*/
insertEmbed (index, embed, attributes = {}) {
const y = this.doc;
if (y !== null) {
transact(y, transaction => {
const pos = findPosition(transaction, this, index);
insertText(transaction, this, pos, embed, attributes);
});
} else {
/** @type {Array<function>} */ (this._pending).push(() => this.insertEmbed(index, embed, attributes));
}
}
/**
* Deletes text starting from an index.
*
* @param {number} index Index at which to start deleting.
* @param {number} length The number of characters to remove. Defaults to 1.
*
* @public
*/
delete (index, length) {
if (length === 0) {
return
}
const y = this.doc;
if (y !== null) {
transact(y, transaction => {
deleteText(transaction, findPosition(transaction, this, index), length);
});
} else {
/** @type {Array<function>} */ (this._pending).push(() => this.delete(index, length));
}
}
/**
* Assigns properties to a range of text.
*
* @param {number} index The position where to start formatting.
* @param {number} length The amount of characters to assign properties to.
* @param {TextAttributes} attributes Attribute information to apply on the
* text.
*
* @public
*/
format (index, length, attributes) {
if (length === 0) {
return
}
const y = this.doc;
if (y !== null) {
transact(y, transaction => {
const pos = findPosition(transaction, this, index);
if (pos.right === null) {
return
}
formatText(transaction, this, pos, length, attributes);
});
} else {
/** @type {Array<function>} */ (this._pending).push(() => this.format(index, length, attributes));
}
}
/**
* Removes an attribute.
*
* @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks.
*
* @param {String} attributeName The attribute name that is to be removed.
*
* @public
*/
removeAttribute (attributeName) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeMapDelete(transaction, this, attributeName);
});
} else {
/** @type {Array<function>} */ (this._pending).push(() => this.removeAttribute(attributeName));
}
}
/**
* Sets or updates an attribute.
*
* @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks.
*
* @param {String} attributeName The attribute name that is to be set.
* @param {any} attributeValue The attribute value that is to be set.
*
* @public
*/
setAttribute (attributeName, attributeValue) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeMapSet(transaction, this, attributeName, attributeValue);
});
} else {
/** @type {Array<function>} */ (this._pending).push(() => this.setAttribute(attributeName, attributeValue));
}
}
/**
* Returns an attribute value that belongs to the attribute name.
*
* @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks.
*
* @param {String} attributeName The attribute name that identifies the
* queried value.
* @return {any} The queried attribute value.
*
* @public
*/
getAttribute (attributeName) {
return /** @type {any} */ (typeMapGet(this, attributeName))
}
/**
* Returns all attribute name/value pairs in a JSON Object.
*
* @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks.
*
* @return {Object<string, any>} A JSON Object that describes the attributes.
*
* @public
*/
getAttributes () {
return typeMapGetAll(this)
}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
*/
_write (encoder) {
encoder.writeTypeRef(YTextRefID);
}
}
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder
* @return {YText}
*
* @private
* @function
*/
const readYText = _decoder => new YText();
/**
* @module YXml
*/
/**
* 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
*/
/**
* 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.
*/
/**
* Represents a subset of the nodes of a YXmlElement / YXmlFragment and a
* position within them.
*
* Can be created with {@link YXmlFragment#createTreeWalker}
*
* @public
* @implements {Iterable<YXmlElement|YXmlText|YXmlElement|YXmlHook>}
*/
class YXmlTreeWalker {
/**
* @param {YXmlFragment | YXmlElement} root
* @param {function(AbstractType<any>):boolean} [f]
*/
constructor (root, f = () => true) {
this._filter = f;
this._root = root;
/**
* @type {Item}
*/
this._currentNode = /** @type {Item} */ (root._start);
this._firstCall = true;
}
[Symbol.iterator] () {
return this
}
/**
* Get the next node.
*
* @return {IteratorResult<YXmlElement|YXmlText|YXmlHook>} The next node.
*
* @public
*/
next () {
/**
* @type {Item|null}
*/
let n = this._currentNode;
let type = n && n.content && /** @type {any} */ (n.content).type;
if (n !== null && (!this._firstCall || n.deleted || !this._filter(type))) { // if first call, we check if we can use the first item
do {
type = /** @type {any} */ (n.content).type;
if (!n.deleted && (type.constructor === YXmlElement || type.constructor === YXmlFragment) && type._start !== null) {
// walk down in the tree
n = type._start;
} else {
// walk right or up in the tree
while (n !== null) {
if (n.right !== null) {
n = n.right;
break
} else if (n.parent === this._root) {
n = null;
} else {
n = /** @type {AbstractType<any>} */ (n.parent)._item;
}
}
}
} while (n !== null && (n.deleted || !this._filter(/** @type {ContentType} */ (n.content).type)))
}
this._firstCall = false;
if (n === null) {
// @ts-ignore
return { value: undefined, done: true }
}
this._currentNode = n;
return { value: /** @type {any} */ (n.content).type, done: false }
}
}
/**
* 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
* @extends AbstractType<YXmlEvent>
*/
class YXmlFragment extends AbstractType {
constructor () {
super();
/**
* @type {Array<any>|null}
*/
this._prelimContent = [];
}
/**
* @type {YXmlElement|YXmlText|null}
*/
get firstChild () {
const first = this._first;
return first ? first.content.getContent()[0] : null
}
/**
* 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 {Doc} y The Yjs instance
* @param {Item} item
*/
_integrate (y, item) {
super._integrate(y, item);
this.insert(0, /** @type {Array<any>} */ (this._prelimContent));
this._prelimContent = null;
}
_copy () {
return new YXmlFragment()
}
/**
* @return {YXmlFragment}
*/
clone () {
const el = new YXmlFragment();
// @ts-ignore
el.insert(0, this.toArray().map(item => item instanceof AbstractType ? item.clone() : item));
return el
}
get length () {
return this._prelimContent === null ? this._length : this._prelimContent.length
}
/**
* 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(AbstractType<any>):boolean} 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 {YXmlTreeWalker} 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|YXmlText|YXmlHook|null} The first element that matches the query or null.
*
* @public
*/
querySelector (query) {
query = query.toUpperCase();
// @ts-ignore
const iterator = new YXmlTreeWalker(this, element => element.nodeName && element.nodeName.toUpperCase() === 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|YXmlText|YXmlHook|null>} The elements that match this query.
*
* @public
*/
querySelectorAll (query) {
query = query.toUpperCase();
// @ts-ignore
return from(new YXmlTreeWalker(this, element => element.nodeName && element.nodeName.toUpperCase() === query))
}
/**
* Creates YXmlEvent and calls observers.
*
* @param {Transaction} transaction
* @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
*/
_callObserver (transaction, parentSubs) {
callTypeObservers(this, transaction, new YXmlEvent(this, parentSubs, transaction));
}
/**
* Get the string representation of all the children of this YXmlFragment.
*
* @return {string} The string representation of all children.
*/
toString () {
return typeListMap(this, xml => xml.toString()).join('')
}
/**
* @return {string}
*/
toJSON () {
return this.toString()
}
/**
* 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<string, any>} [hooks={}] Optional property to customize how hooks
* are presented in the DOM
* @param {any} [binding] You should not set this property. This is
* used if DomBinding wants to create a
* association to the created DOM type.
* @return {Node} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
*
* @public
*/
toDOM (_document = document, hooks = {}, binding) {
const fragment = _document.createDocumentFragment();
if (binding !== undefined) {
binding._createAssociation(fragment, this);
}
typeListForEach(this, xmlType => {
fragment.insertBefore(xmlType.toDOM(_document, hooks, binding), null);
});
return fragment
}
/**
* Inserts new content at an index.
*
* @example
* // Insert character 'a' at position 0
* xml.insert(0, [new Y.XmlText('text')])
*
* @param {number} index The index to insert content at
* @param {Array<YXmlElement|YXmlText>} content The array of content
*/
insert (index, content) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeListInsertGenerics(transaction, this, index, content);
});
} else {
// @ts-ignore _prelimContent is defined because this is not yet integrated
this._prelimContent.splice(index, 0, ...content);
}
}
/**
* Inserts new content at an index.
*
* @example
* // Insert character 'a' at position 0
* xml.insert(0, [new Y.XmlText('text')])
*
* @param {null|Item|YXmlElement|YXmlText} ref The index to insert content at
* @param {Array<YXmlElement|YXmlText>} content The array of content
*/
insertAfter (ref, content) {
if (this.doc !== null) {
transact(this.doc, transaction => {
const refItem = (ref && ref instanceof AbstractType) ? ref._item : ref;
typeListInsertGenericsAfter(transaction, this, refItem, content);
});
} else {
const pc = /** @type {Array<any>} */ (this._prelimContent);
const index = ref === null ? 0 : pc.findIndex(el => el === ref) + 1;
if (index === 0 && ref !== null) {
throw create$3('Reference item not found')
}
pc.splice(index, 0, ...content);
}
}
/**
* Deletes elements starting from an index.
*
* @param {number} index Index at which to start deleting elements
* @param {number} [length=1] The number of elements to remove. Defaults to 1.
*/
delete (index, length = 1) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeListDelete(transaction, this, index, length);
});
} else {
// @ts-ignore _prelimContent is defined because this is not yet integrated
this._prelimContent.splice(index, length);
}
}
/**
* Transforms this YArray to a JavaScript Array.
*
* @return {Array<YXmlElement|YXmlText|YXmlHook>}
*/
toArray () {
return typeListToArray(this)
}
/**
* Appends content to this YArray.
*
* @param {Array<YXmlElement|YXmlText>} content Array of content to append.
*/
push (content) {
this.insert(this.length, content);
}
/**
* Preppends content to this YArray.
*
* @param {Array<YXmlElement|YXmlText>} content Array of content to preppend.
*/
unshift (content) {
this.insert(0, content);
}
/**
* Returns the i-th element from a YArray.
*
* @param {number} index The index of the element to return from the YArray
* @return {YXmlElement|YXmlText}
*/
get (index) {
return typeListGet(this, index)
}
/**
* Transforms this YArray to a JavaScript Array.
*
* @param {number} [start]
* @param {number} [end]
* @return {Array<YXmlElement|YXmlText>}
*/
slice (start = 0, end = this.length) {
return typeListSlice(this, start, end)
}
/**
* Executes a provided function on once on overy child element.
*
* @param {function(YXmlElement|YXmlText,number, typeof self):void} f A function to execute on every element of this YArray.
*/
forEach (f) {
typeListForEach(this, f);
}
/**
* 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 {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to.
*/
_write (encoder) {
encoder.writeTypeRef(YXmlFragmentRefID);
}
}
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder
* @return {YXmlFragment}
*
* @private
* @function
*/
const readYXmlFragment = _decoder => new YXmlFragment();
/**
* 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
*/
class YXmlElement extends YXmlFragment {
constructor (nodeName = 'UNDEFINED') {
super();
this.nodeName = nodeName;
/**
* @type {Map<string, any>|null}
*/
this._prelimAttrs = new Map();
}
/**
* @type {YXmlElement|YXmlText|null}
*/
get nextSibling () {
const n = this._item ? this._item.next : null;
return n ? /** @type {YXmlElement|YXmlText} */ (/** @type {ContentType} */ (n.content).type) : null
}
/**
* @type {YXmlElement|YXmlText|null}
*/
get prevSibling () {
const n = this._item ? this._item.prev : null;
return n ? /** @type {YXmlElement|YXmlText} */ (/** @type {ContentType} */ (n.content).type) : null
}
/**
* 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 {Doc} y The Yjs instance
* @param {Item} item
*/
_integrate (y, item) {
super._integrate(y, item)
;(/** @type {Map<string, any>} */ (this._prelimAttrs)).forEach((value, key) => {
this.setAttribute(key, value);
});
this._prelimAttrs = null;
}
/**
* Creates an Item with the same effect as this Item (without position effect)
*
* @return {YXmlElement}
*/
_copy () {
return new YXmlElement(this.nodeName)
}
/**
* @return {YXmlElement}
*/
clone () {
const el = new YXmlElement(this.nodeName);
const attrs = this.getAttributes();
for (const key in attrs) {
el.setAttribute(key, attrs[key]);
}
// @ts-ignore
el.insert(0, this.toArray().map(item => item instanceof AbstractType ? item.clone() : item));
return el
}
/**
* Returns the XML serialization 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 (const 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) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeMapDelete(transaction, this, attributeName);
});
} else {
/** @type {Map<string,any>} */ (this._prelimAttrs).delete(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) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeMapSet(transaction, this, attributeName, attributeValue);
});
} else {
/** @type {Map<string, any>} */ (this._prelimAttrs).set(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 /** @type {any} */ (typeMapGet(this, attributeName))
}
/**
* Returns whether an attribute exists
*
* @param {String} attributeName The attribute name to check for existence.
* @return {boolean} whether the attribute exists.
*
* @public
*/
hasAttribute (attributeName) {
return /** @type {any} */ (typeMapHas(this, attributeName))
}
/**
* Returns all attribute name/value pairs in a JSON Object.
*
* @return {Object<string, any>} A JSON Object that describes the attributes.
*
* @public
*/
getAttributes () {
return typeMapGetAll(this)
}
/**
* 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<string, any>} [hooks={}] Optional property to customize how hooks
* are presented in the DOM
* @param {any} [binding] You should not set this property. This is
* used if DomBinding wants to create a
* association to the created DOM type.
* @return {Node} 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);
const attrs = this.getAttributes();
for (const key in attrs) {
dom.setAttribute(key, attrs[key]);
}
typeListForEach(this, yxml => {
dom.appendChild(yxml.toDOM(_document, hooks, binding));
});
if (binding !== undefined) {
binding._createAssociation(dom, this);
}
return dom
}
/**
* 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 {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to.
*/
_write (encoder) {
encoder.writeTypeRef(YXmlElementRefID);
encoder.writeKey(this.nodeName);
}
}
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
* @return {YXmlElement}
*
* @function
*/
const readYXmlElement = decoder => new YXmlElement(decoder.readKey());
/**
* @extends YEvent<YXmlElement|YXmlText|YXmlFragment>
* An Event that describes changes on a YXml Element or Yxml Fragment
*/
class YXmlEvent extends YEvent {
/**
* @param {YXmlElement|YXmlText|YXmlFragment} target The target on which the event is created.
* @param {Set<string|null>} subs The set of changed attributes. `null` is included if the
* child list changed.
* @param {Transaction} transaction The transaction instance with wich the
* change was created.
*/
constructor (target, subs, transaction) {
super(target, transaction);
/**
* Whether the children changed.
* @type {Boolean}
* @private
*/
this.childListChanged = false;
/**
* Set of all changed attributes.
* @type {Set<string>}
*/
this.attributesChanged = new Set();
subs.forEach((sub) => {
if (sub === null) {
this.childListChanged = true;
} else {
this.attributesChanged.add(sub);
}
});
}
}
/**
* You can manage binding to a custom type with YXmlHook.
*
* @extends {YMap<any>}
*/
class YXmlHook extends YMap {
/**
* @param {string} hookName nodeName of the Dom Node.
*/
constructor (hookName) {
super();
/**
* @type {string}
*/
this.hookName = hookName;
}
/**
* Creates an Item with the same effect as this Item (without position effect)
*/
_copy () {
return new YXmlHook(this.hookName)
}
/**
* @return {YXmlHook}
*/
clone () {
const el = new YXmlHook(this.hookName);
this.forEach((value, key) => {
el.set(key, value);
});
return el
}
/**
* 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.<string, any>} [hooks] Optional property to customize how hooks
* are presented in the DOM
* @param {any} [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);
if (binding !== undefined) {
binding._createAssociation(dom, this);
}
return dom
}
/**
* 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 {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to.
*/
_write (encoder) {
encoder.writeTypeRef(YXmlHookRefID);
encoder.writeKey(this.hookName);
}
}
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
* @return {YXmlHook}
*
* @private
* @function
*/
const readYXmlHook = decoder =>
new YXmlHook(decoder.readKey());
/**
* Represents text in a Dom Element. In the future this type will also handle
* simple formatting information like bold and italic.
*/
class YXmlText extends YText {
/**
* @type {YXmlElement|YXmlText|null}
*/
get nextSibling () {
const n = this._item ? this._item.next : null;
return n ? /** @type {YXmlElement|YXmlText} */ (/** @type {ContentType} */ (n.content).type) : null
}
/**
* @type {YXmlElement|YXmlText|null}
*/
get prevSibling () {
const n = this._item ? this._item.prev : null;
return n ? /** @type {YXmlElement|YXmlText} */ (/** @type {ContentType} */ (n.content).type) : null
}
_copy () {
return new YXmlText()
}
/**
* @return {YXmlText}
*/
clone () {
const text = new YXmlText();
text.applyDelta(this.toDelta());
return text
}
/**
* 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<string, any>} [hooks] Optional property to customize how hooks
* are presented in the DOM
* @param {any} [binding] You should not set this property. This is
* used if DomBinding wants to create a
* association to the created DOM type.
* @return {Text} 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());
if (binding !== undefined) {
binding._createAssociation(dom, this);
}
return dom
}
toString () {
// @ts-ignore
return this.toDelta().map(delta => {
const nestedNodes = [];
for (const nodeName in delta.attributes) {
const attrs = [];
for (const key in delta.attributes[nodeName]) {
attrs.push({ key, value: delta.attributes[nodeName][key] });
}
// sort attributes to get a unique order
attrs.sort((a, b) => a.key < b.key ? -1 : 1);
nestedNodes.push({ nodeName, attrs });
}
// sort node order to get a unique order
nestedNodes.sort((a, b) => a.nodeName < b.nodeName ? -1 : 1);
// now convert to dom string
let str = '';
for (let i = 0; i < nestedNodes.length; i++) {
const node = nestedNodes[i];
str += `<${node.nodeName}`;
for (let j = 0; j < node.attrs.length; j++) {
const attr = node.attrs[j];
str += ` ${attr.key}="${attr.value}"`;
}
str += '>';
}
str += delta.insert;
for (let i = nestedNodes.length - 1; i >= 0; i--) {
str += `</${nestedNodes[i].nodeName}>`;
}
return str
}).join('')
}
/**
* @return {string}
*/
toJSON () {
return this.toString()
}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
*/
_write (encoder) {
encoder.writeTypeRef(YXmlTextRefID);
}
}
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
* @return {YXmlText}
*
* @private
* @function
*/
const readYXmlText = decoder => new YXmlText();
class AbstractStruct {
/**
* @param {ID} id
* @param {number} length
*/
constructor (id, length) {
this.id = id;
this.length = length;
}
/**
* @type {boolean}
*/
get deleted () {
throw methodUnimplemented()
}
/**
* Merge this struct with the item to the right.
* This method is already assuming that `this.id.clock + this.length === this.id.clock`.
* Also this method does *not* remove right from StructStore!
* @param {AbstractStruct} right
* @return {boolean} wether this merged with right
*/
mergeWith (right) {
return false
}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to.
* @param {number} offset
* @param {number} encodingRef
*/
write (encoder, offset, encodingRef) {
throw methodUnimplemented()
}
/**
* @param {Transaction} transaction
* @param {number} offset
*/
integrate (transaction, offset) {
throw methodUnimplemented()
}
}
const structGCRefNumber = 0;
/**
* @private
*/
class GC extends AbstractStruct {
get deleted () {
return true
}
delete () {}
/**
* @param {GC} right
* @return {boolean}
*/
mergeWith (right) {
if (this.constructor !== right.constructor) {
return false
}
this.length += right.length;
return true
}
/**
* @param {Transaction} transaction
* @param {number} offset
*/
integrate (transaction, offset) {
if (offset > 0) {
this.id.clock += offset;
this.length -= offset;
}
addStruct(transaction.doc.store, this);
}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
* @param {number} offset
*/
write (encoder, offset) {
encoder.writeInfo(structGCRefNumber);
encoder.writeLen(this.length - offset);
}
/**
* @param {Transaction} transaction
* @param {StructStore} store
* @return {null | number}
*/
getMissing (transaction, store) {
return null
}
}
class ContentBinary {
/**
* @param {Uint8Array} content
*/
constructor (content) {
this.content = content;
}
/**
* @return {number}
*/
getLength () {
return 1
}
/**
* @return {Array<any>}
*/
getContent () {
return [this.content]
}
/**
* @return {boolean}
*/
isCountable () {
return true
}
/**
* @return {ContentBinary}
*/
copy () {
return new ContentBinary(this.content)
}
/**
* @param {number} offset
* @return {ContentBinary}
*/
splice (offset) {
throw methodUnimplemented()
}
/**
* @param {ContentBinary} right
* @return {boolean}
*/
mergeWith (right) {
return false
}
/**
* @param {Transaction} transaction
* @param {Item} item
*/
integrate (transaction, item) {}
/**
* @param {Transaction} transaction
*/
delete (transaction) {}
/**
* @param {StructStore} store
*/
gc (store) {}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
* @param {number} offset
*/
write (encoder, offset) {
encoder.writeBuf(this.content);
}
/**
* @return {number}
*/
getRef () {
return 3
}
}
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2 } decoder
* @return {ContentBinary}
*/
const readContentBinary = decoder => new ContentBinary(decoder.readBuf());
class ContentDeleted {
/**
* @param {number} len
*/
constructor (len) {
this.len = len;
}
/**
* @return {number}
*/
getLength () {
return this.len
}
/**
* @return {Array<any>}
*/
getContent () {
return []
}
/**
* @return {boolean}
*/
isCountable () {
return false
}
/**
* @return {ContentDeleted}
*/
copy () {
return new ContentDeleted(this.len)
}
/**
* @param {number} offset
* @return {ContentDeleted}
*/
splice (offset) {
const right = new ContentDeleted(this.len - offset);
this.len = offset;
return right
}
/**
* @param {ContentDeleted} right
* @return {boolean}
*/
mergeWith (right) {
this.len += right.len;
return true
}
/**
* @param {Transaction} transaction
* @param {Item} item
*/
integrate (transaction, item) {
addToDeleteSet(transaction.deleteSet, item.id.client, item.id.clock, this.len);
item.markDeleted();
}
/**
* @param {Transaction} transaction
*/
delete (transaction) {}
/**
* @param {StructStore} store
*/
gc (store) {}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
* @param {number} offset
*/
write (encoder, offset) {
encoder.writeLen(this.len - offset);
}
/**
* @return {number}
*/
getRef () {
return 1
}
}
/**
* @private
*
* @param {UpdateDecoderV1 | UpdateDecoderV2 } decoder
* @return {ContentDeleted}
*/
const readContentDeleted = decoder => new ContentDeleted(decoder.readLen());
/**
* @param {string} guid
* @param {Object<string, any>} opts
*/
const createDocFromOpts = (guid, opts) => new Doc({ guid, ...opts, shouldLoad: opts.shouldLoad || opts.autoLoad || false });
/**
* @private
*/
class ContentDoc {
/**
* @param {Doc} doc
*/
constructor (doc) {
if (doc._item) {
console.error('This document was already integrated as a sub-document. You should create a second instance instead with the same guid.');
}
/**
* @type {Doc}
*/
this.doc = doc;
/**
* @type {any}
*/
const opts = {};
this.opts = opts;
if (!doc.gc) {
opts.gc = false;
}
if (doc.autoLoad) {
opts.autoLoad = true;
}
if (doc.meta !== null) {
opts.meta = doc.meta;
}
}
/**
* @return {number}
*/
getLength () {
return 1
}
/**
* @return {Array<any>}
*/
getContent () {
return [this.doc]
}
/**
* @return {boolean}
*/
isCountable () {
return true
}
/**
* @return {ContentDoc}
*/
copy () {
return new ContentDoc(createDocFromOpts(this.doc.guid, this.opts))
}
/**
* @param {number} offset
* @return {ContentDoc}
*/
splice (offset) {
throw methodUnimplemented()
}
/**
* @param {ContentDoc} right
* @return {boolean}
*/
mergeWith (right) {
return false
}
/**
* @param {Transaction} transaction
* @param {Item} item
*/
integrate (transaction, item) {
// this needs to be reflected in doc.destroy as well
this.doc._item = item;
transaction.subdocsAdded.add(this.doc);
if (this.doc.shouldLoad) {
transaction.subdocsLoaded.add(this.doc);
}
}
/**
* @param {Transaction} transaction
*/
delete (transaction) {
if (transaction.subdocsAdded.has(this.doc)) {
transaction.subdocsAdded.delete(this.doc);
} else {
transaction.subdocsRemoved.add(this.doc);
}
}
/**
* @param {StructStore} store
*/
gc (store) { }
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
* @param {number} offset
*/
write (encoder, offset) {
encoder.writeString(this.doc.guid);
encoder.writeAny(this.opts);
}
/**
* @return {number}
*/
getRef () {
return 9
}
}
/**
* @private
*
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
* @return {ContentDoc}
*/
const readContentDoc = decoder => new ContentDoc(createDocFromOpts(decoder.readString(), decoder.readAny()));
/**
* @private
*/
class ContentEmbed {
/**
* @param {Object} embed
*/
constructor (embed) {
this.embed = embed;
}
/**
* @return {number}
*/
getLength () {
return 1
}
/**
* @return {Array<any>}
*/
getContent () {
return [this.embed]
}
/**
* @return {boolean}
*/
isCountable () {
return true
}
/**
* @return {ContentEmbed}
*/
copy () {
return new ContentEmbed(this.embed)
}
/**
* @param {number} offset
* @return {ContentEmbed}
*/
splice (offset) {
throw methodUnimplemented()
}
/**
* @param {ContentEmbed} right
* @return {boolean}
*/
mergeWith (right) {
return false
}
/**
* @param {Transaction} transaction
* @param {Item} item
*/
integrate (transaction, item) {}
/**
* @param {Transaction} transaction
*/
delete (transaction) {}
/**
* @param {StructStore} store
*/
gc (store) {}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
* @param {number} offset
*/
write (encoder, offset) {
encoder.writeJSON(this.embed);
}
/**
* @return {number}
*/
getRef () {
return 5
}
}
/**
* @private
*
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
* @return {ContentEmbed}
*/
const readContentEmbed = decoder => new ContentEmbed(decoder.readJSON());
/**
* @private
*/
class ContentFormat {
/**
* @param {string} key
* @param {Object} value
*/
constructor (key, value) {
this.key = key;
this.value = value;
}
/**
* @return {number}
*/
getLength () {
return 1
}
/**
* @return {Array<any>}
*/
getContent () {
return []
}
/**
* @return {boolean}
*/
isCountable () {
return false
}
/**
* @return {ContentFormat}
*/
copy () {
return new ContentFormat(this.key, this.value)
}
/**
* @param {number} offset
* @return {ContentFormat}
*/
splice (offset) {
throw methodUnimplemented()
}
/**
* @param {ContentFormat} right
* @return {boolean}
*/
mergeWith (right) {
return false
}
/**
* @param {Transaction} transaction
* @param {Item} item
*/
integrate (transaction, item) {
// @todo searchmarker are currently unsupported for rich text documents
/** @type {AbstractType<any>} */ (item.parent)._searchMarker = null;
}
/**
* @param {Transaction} transaction
*/
delete (transaction) {}
/**
* @param {StructStore} store
*/
gc (store) {}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
* @param {number} offset
*/
write (encoder, offset) {
encoder.writeKey(this.key);
encoder.writeJSON(this.value);
}
/**
* @return {number}
*/
getRef () {
return 6
}
}
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
* @return {ContentFormat}
*/
const readContentFormat = decoder => new ContentFormat(decoder.readKey(), decoder.readJSON());
/**
* @private
*/
class ContentJSON {
/**
* @param {Array<any>} arr
*/
constructor (arr) {
/**
* @type {Array<any>}
*/
this.arr = arr;
}
/**
* @return {number}
*/
getLength () {
return this.arr.length
}
/**
* @return {Array<any>}
*/
getContent () {
return this.arr
}
/**
* @return {boolean}
*/
isCountable () {
return true
}
/**
* @return {ContentJSON}
*/
copy () {
return new ContentJSON(this.arr)
}
/**
* @param {number} offset
* @return {ContentJSON}
*/
splice (offset) {
const right = new ContentJSON(this.arr.slice(offset));
this.arr = this.arr.slice(0, offset);
return right
}
/**
* @param {ContentJSON} right
* @return {boolean}
*/
mergeWith (right) {
this.arr = this.arr.concat(right.arr);
return true
}
/**
* @param {Transaction} transaction
* @param {Item} item
*/
integrate (transaction, item) {}
/**
* @param {Transaction} transaction
*/
delete (transaction) {}
/**
* @param {StructStore} store
*/
gc (store) {}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
* @param {number} offset
*/
write (encoder, offset) {
const len = this.arr.length;
encoder.writeLen(len - offset);
for (let i = offset; i < len; i++) {
const c = this.arr[i];
encoder.writeString(c === undefined ? 'undefined' : JSON.stringify(c));
}
}
/**
* @return {number}
*/
getRef () {
return 2
}
}
/**
* @private
*
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
* @return {ContentJSON}
*/
const readContentJSON = decoder => {
const len = decoder.readLen();
const cs = [];
for (let i = 0; i < len; i++) {
const c = decoder.readString();
if (c === 'undefined') {
cs.push(undefined);
} else {
cs.push(JSON.parse(c));
}
}
return new ContentJSON(cs)
};
class ContentAny {
/**
* @param {Array<any>} arr
*/
constructor (arr) {
/**
* @type {Array<any>}
*/
this.arr = arr;
}
/**
* @return {number}
*/
getLength () {
return this.arr.length
}
/**
* @return {Array<any>}
*/
getContent () {
return this.arr
}
/**
* @return {boolean}
*/
isCountable () {
return true
}
/**
* @return {ContentAny}
*/
copy () {
return new ContentAny(this.arr)
}
/**
* @param {number} offset
* @return {ContentAny}
*/
splice (offset) {
const right = new ContentAny(this.arr.slice(offset));
this.arr = this.arr.slice(0, offset);
return right
}
/**
* @param {ContentAny} right
* @return {boolean}
*/
mergeWith (right) {
this.arr = this.arr.concat(right.arr);
return true
}
/**
* @param {Transaction} transaction
* @param {Item} item
*/
integrate (transaction, item) {}
/**
* @param {Transaction} transaction
*/
delete (transaction) {}
/**
* @param {StructStore} store
*/
gc (store) {}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
* @param {number} offset
*/
write (encoder, offset) {
const len = this.arr.length;
encoder.writeLen(len - offset);
for (let i = offset; i < len; i++) {
const c = this.arr[i];
encoder.writeAny(c);
}
}
/**
* @return {number}
*/
getRef () {
return 8
}
}
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
* @return {ContentAny}
*/
const readContentAny = decoder => {
const len = decoder.readLen();
const cs = [];
for (let i = 0; i < len; i++) {
cs.push(decoder.readAny());
}
return new ContentAny(cs)
};
/**
* @private
*/
class ContentString {
/**
* @param {string} str
*/
constructor (str) {
/**
* @type {string}
*/
this.str = str;
}
/**
* @return {number}
*/
getLength () {
return this.str.length
}
/**
* @return {Array<any>}
*/
getContent () {
return this.str.split('')
}
/**
* @return {boolean}
*/
isCountable () {
return true
}
/**
* @return {ContentString}
*/
copy () {
return new ContentString(this.str)
}
/**
* @param {number} offset
* @return {ContentString}
*/
splice (offset) {
const right = new ContentString(this.str.slice(offset));
this.str = this.str.slice(0, offset);
// Prevent encoding invalid documents because of splitting of surrogate pairs: https://github.com/yjs/yjs/issues/248
const firstCharCode = this.str.charCodeAt(offset - 1);
if (firstCharCode >= 0xD800 && firstCharCode <= 0xDBFF) {
// Last character of the left split is the start of a surrogate utf16/ucs2 pair.
// We don't support splitting of surrogate pairs because this may lead to invalid documents.
// Replace the invalid character with a unicode replacement character (� / U+FFFD)
this.str = this.str.slice(0, offset - 1) + '�';
// replace right as well
right.str = '�' + right.str.slice(1);
}
return right
}
/**
* @param {ContentString} right
* @return {boolean}
*/
mergeWith (right) {
this.str += right.str;
return true
}
/**
* @param {Transaction} transaction
* @param {Item} item
*/
integrate (transaction, item) {}
/**
* @param {Transaction} transaction
*/
delete (transaction) {}
/**
* @param {StructStore} store
*/
gc (store) {}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
* @param {number} offset
*/
write (encoder, offset) {
encoder.writeString(offset === 0 ? this.str : this.str.slice(offset));
}
/**
* @return {number}
*/
getRef () {
return 4
}
}
/**
* @private
*
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
* @return {ContentString}
*/
const readContentString = decoder => new ContentString(decoder.readString());
/**
* @type {Array<function(UpdateDecoderV1 | UpdateDecoderV2):AbstractType<any>>}
* @private
*/
const typeRefs = [
readYArray,
readYMap,
readYText,
readYXmlElement,
readYXmlFragment,
readYXmlHook,
readYXmlText
];
const YArrayRefID = 0;
const YMapRefID = 1;
const YTextRefID = 2;
const YXmlElementRefID = 3;
const YXmlFragmentRefID = 4;
const YXmlHookRefID = 5;
const YXmlTextRefID = 6;
/**
* @private
*/
class ContentType {
/**
* @param {AbstractType<any>} type
*/
constructor (type) {
/**
* @type {AbstractType<any>}
*/
this.type = type;
}
/**
* @return {number}
*/
getLength () {
return 1
}
/**
* @return {Array<any>}
*/
getContent () {
return [this.type]
}
/**
* @return {boolean}
*/
isCountable () {
return true
}
/**
* @return {ContentType}
*/
copy () {
return new ContentType(this.type._copy())
}
/**
* @param {number} offset
* @return {ContentType}
*/
splice (offset) {
throw methodUnimplemented()
}
/**
* @param {ContentType} right
* @return {boolean}
*/
mergeWith (right) {
return false
}
/**
* @param {Transaction} transaction
* @param {Item} item
*/
integrate (transaction, item) {
this.type._integrate(transaction.doc, item);
}
/**
* @param {Transaction} transaction
*/
delete (transaction) {
let item = this.type._start;
while (item !== null) {
if (!item.deleted) {
item.delete(transaction);
} else {
// This will be gc'd later and we want to merge it if possible
// We try to merge all deleted items after each transaction,
// but we have no knowledge about that this needs to be merged
// since it is not in transaction.ds. Hence we add it to transaction._mergeStructs
transaction._mergeStructs.push(item);
}
item = item.right;
}
this.type._map.forEach(item => {
if (!item.deleted) {
item.delete(transaction);
} else {
// same as above
transaction._mergeStructs.push(item);
}
});
transaction.changed.delete(this.type);
}
/**
* @param {StructStore} store
*/
gc (store) {
let item = this.type._start;
while (item !== null) {
item.gc(store, true);
item = item.right;
}
this.type._start = null;
this.type._map.forEach(/** @param {Item | null} item */ (item) => {
while (item !== null) {
item.gc(store, true);
item = item.left;
}
});
this.type._map = new Map();
}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
* @param {number} offset
*/
write (encoder, offset) {
this.type._write(encoder);
}
/**
* @return {number}
*/
getRef () {
return 7
}
}
/**
* @private
*
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
* @return {ContentType}
*/
const readContentType = decoder => new ContentType(typeRefs[decoder.readTypeRef()](decoder));
/**
* Split leftItem into two items
* @param {Transaction} transaction
* @param {Item} leftItem
* @param {number} diff
* @return {Item}
*
* @function
* @private
*/
const splitItem = (transaction, leftItem, diff) => {
// create rightItem
const { client, clock } = leftItem.id;
const rightItem = new Item(
createID(client, clock + diff),
leftItem,
createID(client, clock + diff - 1),
leftItem.right,
leftItem.rightOrigin,
leftItem.parent,
leftItem.parentSub,
leftItem.content.splice(diff)
);
if (leftItem.deleted) {
rightItem.markDeleted();
}
if (leftItem.keep) {
rightItem.keep = true;
}
if (leftItem.redone !== null) {
rightItem.redone = createID(leftItem.redone.client, leftItem.redone.clock + diff);
}
// update left (do not set leftItem.rightOrigin as it will lead to problems when syncing)
leftItem.right = rightItem;
// update right
if (rightItem.right !== null) {
rightItem.right.left = rightItem;
}
// right is more specific.
transaction._mergeStructs.push(rightItem);
// update parent._map
if (rightItem.parentSub !== null && rightItem.right === null) {
/** @type {AbstractType<any>} */ (rightItem.parent)._map.set(rightItem.parentSub, rightItem);
}
leftItem.length = diff;
return rightItem
};
/**
* Abstract class that represents any content.
*/
class Item extends AbstractStruct {
/**
* @param {ID} id
* @param {Item | null} left
* @param {ID | null} origin
* @param {Item | null} right
* @param {ID | null} rightOrigin
* @param {AbstractType<any>|ID|null} parent Is a type if integrated, is null if it is possible to copy parent from left or right, is ID before integration to search for it.
* @param {string | null} parentSub
* @param {AbstractContent} content
*/
constructor (id, left, origin, right, rightOrigin, parent, parentSub, content) {
super(id, content.getLength());
/**
* The item that was originally to the left of this item.
* @type {ID | null}
*/
this.origin = origin;
/**
* The item that is currently to the left of this item.
* @type {Item | null}
*/
this.left = left;
/**
* The item that is currently to the right of this item.
* @type {Item | null}
*/
this.right = right;
/**
* The item that was originally to the right of this item.
* @type {ID | null}
*/
this.rightOrigin = rightOrigin;
/**
* @type {AbstractType<any>|ID|null}
*/
this.parent = parent;
/**
* 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._map`.
* @type {String | null}
*/
this.parentSub = parentSub;
/**
* If this type's effect is redone this type refers to the type that undid
* this operation.
* @type {ID | null}
*/
this.redone = null;
/**
* @type {AbstractContent}
*/
this.content = content;
/**
* bit1: keep
* bit2: countable
* bit3: deleted
* bit4: mark - mark node as fast-search-marker
* @type {number} byte
*/
this.info = this.content.isCountable() ? BIT2 : 0;
}
/**
* This is used to mark the item as an indexed fast-search marker
*
* @type {boolean}
*/
set marker (isMarked) {
if (((this.info & BIT4) > 0) !== isMarked) {
this.info ^= BIT4;
}
}
get marker () {
return (this.info & BIT4) > 0
}
/**
* If true, do not garbage collect this Item.
*/
get keep () {
return (this.info & BIT1) > 0
}
set keep (doKeep) {
if (this.keep !== doKeep) {
this.info ^= BIT1;
}
}
get countable () {
return (this.info & BIT2) > 0
}
/**
* Whether this item was deleted or not.
* @type {Boolean}
*/
get deleted () {
return (this.info & BIT3) > 0
}
set deleted (doDelete) {
if (this.deleted !== doDelete) {
this.info ^= BIT3;
}
}
markDeleted () {
this.info |= BIT3;
}
/**
* Return the creator clientID of the missing op or define missing items and return null.
*
* @param {Transaction} transaction
* @param {StructStore} store
* @return {null | number}
*/
getMissing (transaction, store) {
if (this.origin && this.origin.client !== this.id.client && this.origin.clock >= getState(store, this.origin.client)) {
return this.origin.client
}
if (this.rightOrigin && this.rightOrigin.client !== this.id.client && this.rightOrigin.clock >= getState(store, this.rightOrigin.client)) {
return this.rightOrigin.client
}
if (this.parent && this.parent.constructor === ID && this.id.client !== this.parent.client && this.parent.clock >= getState(store, this.parent.client)) {
return this.parent.client
}
// We have all missing ids, now find the items
if (this.origin) {
this.left = getItemCleanEnd(transaction, store, this.origin);
this.origin = this.left.lastId;
}
if (this.rightOrigin) {
this.right = getItemCleanStart(transaction, this.rightOrigin);
this.rightOrigin = this.right.id;
}
if ((this.left && this.left.constructor === GC) || (this.right && this.right.constructor === GC)) {
this.parent = null;
}
// only set parent if this shouldn't be garbage collected
if (!this.parent) {
if (this.left && this.left.constructor === Item) {
this.parent = this.left.parent;
this.parentSub = this.left.parentSub;
}
if (this.right && this.right.constructor === Item) {
this.parent = this.right.parent;
this.parentSub = this.right.parentSub;
}
} else if (this.parent.constructor === ID) {
const parentItem = getItem(store, this.parent);
if (parentItem.constructor === GC) {
this.parent = null;
} else {
this.parent = /** @type {ContentType} */ (parentItem.content).type;
}
}
return null
}
/**
* @param {Transaction} transaction
* @param {number} offset
*/
integrate (transaction, offset) {
if (offset > 0) {
this.id.clock += offset;
this.left = getItemCleanEnd(transaction, transaction.doc.store, createID(this.id.client, this.id.clock - 1));
this.origin = this.left.lastId;
this.content = this.content.splice(offset);
this.length -= offset;
}
if (this.parent) {
if ((!this.left && (!this.right || this.right.left !== null)) || (this.left && this.left.right !== this.right)) {
/**
* @type {Item|null}
*/
let left = this.left;
/**
* @type {Item|null}
*/
let o;
// set o to the first conflicting item
if (left !== null) {
o = left.right;
} else if (this.parentSub !== null) {
o = /** @type {AbstractType<any>} */ (this.parent)._map.get(this.parentSub) || null;
while (o !== null && o.left !== null) {
o = o.left;
}
} else {
o = /** @type {AbstractType<any>} */ (this.parent)._start;
}
// TODO: use something like DeleteSet here (a tree implementation would be best)
// @todo use global set definitions
/**
* @type {Set<Item>}
*/
const conflictingItems = new Set();
/**
* @type {Set<Item>}
*/
const 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 (compareIDs(this.origin, o.origin)) {
// case 1
if (o.id.client < this.id.client) {
left = o;
conflictingItems.clear();
} else if (compareIDs(this.rightOrigin, o.rightOrigin)) {
// this and o are conflicting and point to the same integration points. The id decides which item comes first.
// Since this is to the left of o, we can break here
break
} // else, o might be integrated before an item that this conflicts with. If so, we will find it in the next iterations
} else if (o.origin !== null && itemsBeforeOrigin.has(getItem(transaction.doc.store, o.origin))) { // use getItem instead of getItemCleanEnd because we don't want / need to split items.
// case 2
if (!conflictingItems.has(getItem(transaction.doc.store, o.origin))) {
left = o;
conflictingItems.clear();
}
} else {
break
}
o = o.right;
}
this.left = left;
}
// reconnect left/right + update parent map/start if necessary
if (this.left !== null) {
const right = this.left.right;
this.right = right;
this.left.right = this;
} else {
let r;
if (this.parentSub !== null) {
r = /** @type {AbstractType<any>} */ (this.parent)._map.get(this.parentSub) || null;
while (r !== null && r.left !== null) {
r = r.left;
}
} else {
r = /** @type {AbstractType<any>} */ (this.parent)._start
;/** @type {AbstractType<any>} */ (this.parent)._start = this;
}
this.right = r;
}
if (this.right !== null) {
this.right.left = this;
} else if (this.parentSub !== null) {
// set as current parent value if right === null and this is parentSub
/** @type {AbstractType<any>} */ (this.parent)._map.set(this.parentSub, this);
if (this.left !== null) {
// this is the current attribute value of parent. delete right
this.left.delete(transaction);
}
}
// adjust length of parent
if (this.parentSub === null && this.countable && !this.deleted) {
/** @type {AbstractType<any>} */ (this.parent)._length += this.length;
}
addStruct(transaction.doc.store, this);
this.content.integrate(transaction, this);
// add parent to transaction.changed
addChangedTypeToTransaction(transaction, /** @type {AbstractType<any>} */ (this.parent), this.parentSub);
if ((/** @type {AbstractType<any>} */ (this.parent)._item !== null && /** @type {AbstractType<any>} */ (this.parent)._item.deleted) || (this.parentSub !== null && this.right !== null)) {
// delete if parent is deleted or if this is not the current attribute value of parent
this.delete(transaction);
}
} else {
// parent is not defined. Integrate GC struct instead
new GC(this.id, this.length).integrate(transaction, 0);
}
}
/**
* Returns the next non-deleted item
*/
get next () {
let n = this.right;
while (n !== null && n.deleted) {
n = n.right;
}
return n
}
/**
* Returns the previous non-deleted item
*/
get prev () {
let n = this.left;
while (n !== null && n.deleted) {
n = n.left;
}
return n
}
/**
* Computes the last content address of this Item.
*/
get lastId () {
// allocating ids is pretty costly because of the amount of ids created, so we try to reuse whenever possible
return this.length === 1 ? this.id : createID(this.id.client, this.id.clock + this.length - 1)
}
/**
* Try to merge two items
*
* @param {Item} right
* @return {boolean}
*/
mergeWith (right) {
if (
this.constructor === right.constructor &&
compareIDs(right.origin, this.lastId) &&
this.right === right &&
compareIDs(this.rightOrigin, right.rightOrigin) &&
this.id.client === right.id.client &&
this.id.clock + this.length === right.id.clock &&
this.deleted === right.deleted &&
this.redone === null &&
right.redone === null &&
this.content.constructor === right.content.constructor &&
this.content.mergeWith(right.content)
) {
const searchMarker = /** @type {AbstractType<any>} */ (this.parent)._searchMarker;
if (searchMarker) {
searchMarker.forEach(marker => {
if (marker.p === right) {
// right is going to be "forgotten" so we need to update the marker
marker.p = this;
// adjust marker index
if (!this.deleted && this.countable) {
marker.index -= this.length;
}
}
});
}
if (right.keep) {
this.keep = true;
}
this.right = right.right;
if (this.right !== null) {
this.right.left = this;
}
this.length += right.length;
return true
}
return false
}
/**
* Mark this Item as deleted.
*
* @param {Transaction} transaction
*/
delete (transaction) {
if (!this.deleted) {
const parent = /** @type {AbstractType<any>} */ (this.parent);
// adjust the length of parent
if (this.countable && this.parentSub === null) {
parent._length -= this.length;
}
this.markDeleted();
addToDeleteSet(transaction.deleteSet, this.id.client, this.id.clock, this.length);
addChangedTypeToTransaction(transaction, parent, this.parentSub);
this.content.delete(transaction);
}
}
/**
* @param {StructStore} store
* @param {boolean} parentGCd
*/
gc (store, parentGCd) {
if (!this.deleted) {
throw unexpectedCase()
}
this.content.gc(store);
if (parentGCd) {
replaceStruct(store, this, new GC(this.id, this.length));
} else {
this.content = new ContentDeleted(this.length);
}
}
/**
* 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 {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to.
* @param {number} offset
*/
write (encoder, offset) {
const origin = offset > 0 ? createID(this.id.client, this.id.clock + offset - 1) : this.origin;
const rightOrigin = this.rightOrigin;
const parentSub = this.parentSub;
const info = (this.content.getRef() & BITS5) |
(origin === null ? 0 : BIT8) | // origin is defined
(rightOrigin === null ? 0 : BIT7) | // right origin is defined
(parentSub === null ? 0 : BIT6); // parentSub is non-null
encoder.writeInfo(info);
if (origin !== null) {
encoder.writeLeftID(origin);
}
if (rightOrigin !== null) {
encoder.writeRightID(rightOrigin);
}
if (origin === null && rightOrigin === null) {
const parent = /** @type {AbstractType<any>} */ (this.parent);
if (parent._item !== undefined) {
const parentItem = parent._item;
if (parentItem === null) {
// parent type on y._map
// find the correct key
const ykey = findRootTypeKey(parent);
encoder.writeParentInfo(true); // write parentYKey
encoder.writeString(ykey);
} else {
encoder.writeParentInfo(false); // write parent id
encoder.writeLeftID(parentItem.id);
}
} else if (parent.constructor === String) { // this edge case was added by differential updates
encoder.writeParentInfo(true); // write parentYKey
encoder.writeString(parent);
} else if (parent.constructor === ID) {
encoder.writeParentInfo(false); // write parent id
encoder.writeLeftID(parent);
} else {
unexpectedCase();
}
if (parentSub !== null) {
encoder.writeString(parentSub);
}
}
this.content.write(encoder, offset);
}
}
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
* @param {number} info
*/
const readItemContent = (decoder, info) => contentRefs[info & BITS5](decoder);
/**
* A lookup map for reading Item content.
*
* @type {Array<function(UpdateDecoderV1 | UpdateDecoderV2):AbstractContent>}
*/
const contentRefs = [
() => { unexpectedCase(); }, // GC is not ItemContent
readContentDeleted, // 1
readContentJSON, // 2
readContentBinary, // 3
readContentString, // 4
readContentEmbed, // 5
readContentFormat, // 6
readContentType, // 7
readContentAny, // 8
readContentDoc, // 9
() => { unexpectedCase(); } // 10 - Skip is not ItemContent
];
const structSkipRefNumber = 10;
/**
* @private
*/
class Skip extends AbstractStruct {
get deleted () {
return true
}
delete () {}
/**
* @param {Skip} right
* @return {boolean}
*/
mergeWith (right) {
if (this.constructor !== right.constructor) {
return false
}
this.length += right.length;
return true
}
/**
* @param {Transaction} transaction
* @param {number} offset
*/
integrate (transaction, offset) {
// skip structs cannot be integrated
unexpectedCase();
}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
* @param {number} offset
*/
write (encoder, offset) {
encoder.writeInfo(structSkipRefNumber);
// write as VarUint because Skips can't make use of predictable length-encoding
writeVarUint(encoder.restEncoder, this.length - offset);
}
/**
* @param {Transaction} transaction
* @param {StructStore} store
* @return {null | number}
*/
getMissing (transaction, store) {
return null
}
}
/** eslint-env browser */
const glo = /** @type {any} */ (typeof globalThis !== 'undefined'
? globalThis
: typeof window !== 'undefined'
? window
// @ts-ignore
: typeof global !== 'undefined' ? global : {});
const importIdentifier = '__ $YJS$ __';
if (glo[importIdentifier] === true) {
/**
* Dear reader of this message. Please take this seriously.
*
* If you see this message, make sure that you only import one version of Yjs. In many cases,
* your package manager installs two versions of Yjs that are used by different packages within your project.
* Another reason for this message is that some parts of your project use the commonjs version of Yjs
* and others use the EcmaScript version of Yjs.
*
* This often leads to issues that are hard to debug. We often need to perform constructor checks,
* e.g. `struct instanceof GC`. If you imported different versions of Yjs, it is impossible for us to
* do the constructor checks anymore - which might break the CRDT algorithm.
*
* https://github.com/yjs/yjs/issues/438
*/
console.error('Yjs was already imported. This breaks constructor checks and will lead to issues! - https://github.com/yjs/yjs/issues/438');
}
glo[importIdentifier] = true;
var LineDirection;
(function(LineDirection2) {
LineDirection2[LineDirection2["ROW"] = 0] = "ROW";
LineDirection2[LineDirection2["COLUMN"] = 1] = "COLUMN";
})(LineDirection || (LineDirection = {}));
var DistanceEnum;
(function(DistanceEnum2) {
DistanceEnum2[DistanceEnum2["ADSORB"] = 0] = "ADSORB";
DistanceEnum2[DistanceEnum2["SHOW_LINE"] = 1] = "SHOW_LINE";
DistanceEnum2[DistanceEnum2["REMOTE"] = 2] = "REMOTE";
})(DistanceEnum || (DistanceEnum = {}));
class AlignmentLineService {
constructor(option, designService) {
this.option = option;
this.designService = designService;
this.boundaryLine = [];
this.boundaryLine = vue.reactive([]);
designService.emitter.on("onLayout", this.setPaddingLine.bind(this));
}
setPaddingLine() {
this.designService.emitter.off("onLayout", this.setPaddingLine);
this.delBoundaryLine("padding");
const padding = this.designService.modal.pageRect.padding;
this.boundaryLine.push(...[
{
id: "padding-top",
direction: LineDirection.ROW,
show: false,
x: 0,
y: padding[0],
width: "100%",
height: "0"
},
{
id: "padding-bottom",
direction: LineDirection.ROW,
show: false,
x: 0,
y: this.designService.modal.pageRect.height - padding[2],
width: "100%",
height: "0"
},
{
id: "padding-left",
direction: LineDirection.COLUMN,
show: false,
x: padding[3],
y: 0,
width: "0",
height: "100%"
},
{
id: "padding-right",
direction: LineDirection.COLUMN,
show: false,
x: this.designService.modal.pageRect.width - padding[1],
y: 0,
width: "0",
height: "100%"
}
]);
}
// 监听newWidget移动的
onNewWidgetMove(widgets, designService) {
if (!widgets.length)
return;
const boundary = this.designService.utils.getBoundaryByWidget(widgets);
this.handlerWidgetMove(boundary, this.adsorbNewWidge.bind(this, designService));
}
// 监听一组Widget移动
onWidgetGroupMove(yWidgets) {
if (!yWidgets.length)
return;
const widgets = yWidgets.map((item) => item.toJSON());
const boundary = this.designService.utils.getBoundaryByWidget(widgets);
this.handlerWidgetMove(boundary, this.adsorbWidgets.bind(this, yWidgets));
}
// 处理widget移动
handlerWidgetMove(boundary, adsorbHandler) {
let minRow = null;
let minCol = null;
this.boundaryLine.forEach((line) => {
if (line.direction === LineDirection.ROW) {
const topDistance = this.getDistance(boundary.top, line.y);
const bottomDistance = this.getDistance(boundary.bottom, line.y);
if (topDistance.tag <= DistanceEnum.SHOW_LINE) {
line.show = true;
if (topDistance.tag <= DistanceEnum.ADSORB && this.option.enableAdsorb) {
if (minRow === null || topDistance.distance < minRow.distance) {
minRow = {
place: "top",
line,
boundary,
distance: topDistance.distance
};
}
}
} else if (bottomDistance.tag <= DistanceEnum.SHOW_LINE) {
line.show = true;
if (bottomDistance.tag <= DistanceEnum.ADSORB && this.option.enableAdsorb) {
if (minRow === null || bottomDistance.distance < minRow.distance) {
minRow = {
place: "bottom",
line,
boundary,
distance: bottomDistance.distance
};
}
}
} else {
line.show = false;
}
}
if (line.direction === LineDirection.COLUMN) {
const leftDistance = this.getDistance(boundary.left, line.x);
const rightDistance = this.getDistance(boundary.right, line.x);
if (leftDistance.tag <= DistanceEnum.SHOW_LINE) {
line.show = true;
if (leftDistance.tag <= DistanceEnum.ADSORB && this.option.enableAdsorb) {
if (minCol === null || leftDistance.distance < minCol.distance) {
minCol = {
place: "left",
line,
boundary,
distance: leftDistance.distance
};
}
}
} else if (rightDistance.tag <= DistanceEnum.SHOW_LINE) {
line.show = true;
if (rightDistance.tag <= DistanceEnum.ADSORB && this.option.enableAdsorb) {
if (minCol === null || rightDistance.distance < minCol.distance) {
minCol = {
place: "right",
line,
boundary,
distance: rightDistance.distance
};
}
}
} else {
line.show = false;
}
}
});
minRow && adsorbHandler(minRow.place, minRow.line, minRow.boundary);
minCol && adsorbHandler(minCol.place, minCol.line, minCol.boundary);
}
// 吸附新创建的widget
adsorbNewWidge(designService, place, line) {
if (!designService.modal.newWidget)
return;
const { x, y } = designService.utils.cavnaseP2NewWidgetP(line);
switch (place) {
case "top":
designService.modal.newWidget.y = y;
break;
case "bottom":
designService.modal.newWidget.y = y - designService.modal.newWidget.height;
break;
case "left":
designService.modal.newWidget.x = x;
break;
case "right":
designService.modal.newWidget.x = x - designService.modal.newWidget.width;
break;
}
}
// 吸附移动的一组widget
adsorbWidgets(yWidgets, place, line, boundary) {
switch (place) {
case "top":
yWidgets.forEach((yWidget) => {
const offset = boundary.top - line.y;
yWidget.set("y", yWidget.get("baseY") - offset);
});
break;
case "bottom":
yWidgets.forEach((yWidget) => {
const offset = boundary.bottom - line.y;
yWidget.set("y", yWidget.get("baseY") - offset);
});
break;
case "left":
yWidgets.forEach((yWidget) => {
const offset = boundary.left - line.x;
yWidget.set("x", yWidget.get("baseX") - offset);
});
break;
case "right":
yWidgets.forEach((yWidget) => {
const offset = boundary.right - line.x;
yWidget.set("x", yWidget.get("baseX") - offset);
});
break;
}
}
// 监听选中区域边界的移动
onBoundaryMove(selecteds, direct) {
const widgets = selecteds.map((item) => item.toJSON());
const boundary = this.designService.utils.getBoundaryByWidget(widgets);
let minTop = null;
let minBottom = null;
let minLeft = null;
let minRight = null;
this.boundaryLine.forEach((line) => {
if (direct[1] === "t") {
const topDistance = this.getDistance(boundary.top, line.y);
if (topDistance.tag <= DistanceEnum.SHOW_LINE) {
line.show = true;
if (topDistance.tag <= DistanceEnum.ADSORB && this.option.enableAdsorb) {
if (minTop === null || topDistance.distance < minTop.distance) {
minTop = {
line,
distance: topDistance.distance
};
}
}
}
}
if (direct[1] === "b") {
const bottomDistance = this.getDistance(boundary.bottom, line.y);
if (bottomDistance.tag <= DistanceEnum.SHOW_LINE) {
line.show = true;
if (bottomDistance.tag <= DistanceEnum.ADSORB && this.option.enableAdsorb) {
if (minBottom === null || bottomDistance.distance < minBottom.distance) {
minBottom = {
line,
distance: bottomDistance.distance
};
}
}
}
}
if (direct[0] === "l") {
const leftDistance = this.getDistance(boundary.left, line.x);
if (leftDistance.tag <= DistanceEnum.SHOW_LINE) {
line.show = true;
if (leftDistance.tag <= DistanceEnum.ADSORB && this.option.enableAdsorb) {
if (minLeft === null || leftDistance.distance < minLeft.distance) {
minLeft = {
line,
distance: leftDistance.distance
};
}
}
}
}
if (direct[0] === "r") {
const rightDistance = this.getDistance(boundary.right, line.x);
if (rightDistance.tag <= DistanceEnum.SHOW_LINE) {
line.show = true;
if (rightDistance.tag <= DistanceEnum.ADSORB && this.option.enableAdsorb) {
if (minRight === null || rightDistance.distance < minRight.distance) {
minRight = {
line,
boundary,
distance: rightDistance.distance
};
}
}
}
}
});
if (minTop !== null) {
selecteds.forEach((yWidget) => {
const offset = boundary.top - minTop.line.y;
yWidget.set("y", yWidget.get("baseY") - offset);
yWidget.set("height", yWidget.get("height") + offset);
});
}
if (minBottom !== null) {
selecteds.forEach((yWidget) => {
const offset = boundary.bottom - minBottom.line.y;
yWidget.set("y", yWidget.get("baseY"));
yWidget.set("height", yWidget.get("height") - offset);
});
}
if (minLeft !== null) {
selecteds.forEach((yWidget) => {
const offset = boundary.left - minLeft.line.x;
yWidget.set("x", yWidget.get("baseX") - offset);
yWidget.set("width", yWidget.get("width") + offset);
});
}
if (minRight !== null) {
selecteds.forEach((yWidget) => {
const offset = boundary.right - minRight.line.x;
yWidget.set("x", yWidget.get("baseX"));
yWidget.set("width", yWidget.get("width") - offset);
});
}
}
// 获取Widget离某条线的距离,1:达到吸附距离,2:达到显示距离,3:距离较远
getDistance(span, line) {
const distance = Math.abs(span - line);
let tag = DistanceEnum.REMOTE;
if (distance < this.option.showAlignSpan) {
tag = DistanceEnum.SHOW_LINE;
}
if (distance < this.option.adsorbSpan) {
tag = DistanceEnum.ADSORB;
}
return {
distance,
tag
};
}
// 新增线
addBoundaryLine(widget) {
const lineList = [
{
id: widget.id + "-top",
direction: LineDirection.ROW,
show: false,
x: 0,
y: widget.y,
width: "100%",
height: "0"
},
{
id: widget.id + "-bottom",
direction: LineDirection.ROW,
show: false,
x: 0,
y: widget.y + widget.height,
width: "100%",
height: "0"
},
{
id: widget.id + "-left",
direction: LineDirection.COLUMN,
show: false,
x: widget.x,
y: 0,
width: "0",
height: "100%"
},
{
id: widget.id + "-right",
direction: LineDirection.COLUMN,
show: false,
x: widget.x + widget.width,
y: 0,
width: "0",
height: "100%"
}
];
lineList.forEach((line) => {
const idx = this.boundaryLine.findIndex((item) => item.id === line.id);
if (idx !== -1) {
this.boundaryLine[idx] = Object.assign(this.boundaryLine[idx], line);
} else {
this.boundaryLine.push(line);
}
});
}
// 删除线
delBoundaryLine(id) {
let idx = this.boundaryLine.findIndex((item) => item.id === id + "-top");
idx !== -1 && this.boundaryLine.splice(idx, 1);
idx = this.boundaryLine.findIndex((item) => item.id === id + "-bottom");
idx !== -1 && this.boundaryLine.splice(idx, 1);
idx = this.boundaryLine.findIndex((item) => item.id === id + "-left");
idx !== -1 && this.boundaryLine.splice(idx, 1);
idx = this.boundaryLine.findIndex((item) => item.id === id + "-right");
idx !== -1 && this.boundaryLine.splice(idx, 1);
}
// 删除所有线
clearAllLine() {
this.boundaryLine.length = 0;
}
// 隐藏所有线
hideAllLine() {
this.boundaryLine.forEach((line) => {
line.show = false;
});
}
// 根据yjs更新的数据处理对齐线
handlerAlignmentLine(updateData) {
var _a, _b;
if (updateData.type === "array") {
if (CheckType.isArray((_a = updateData.deltaData) === null || _a === void 0 ? void 0 : _a.insert)) {
(_b = updateData.deltaData) === null || _b === void 0 ? void 0 : _b.insert.forEach((item) => {
if (item instanceof YMap) {
this.addBoundaryLine(item.toJSON());
}
});
}
} else if (updateData.type === "map") {
if (updateData.target.get("moveing") === true || updateData.target.get("resizing") === true) {
this.delBoundaryLine(updateData.target.get("id"));
} else if (updateData.target.get("moveing") === false || updateData.target.get("resizing") === false) {
this.addBoundaryLine(updateData.target.toJSON());
this.hideAllLine();
}
}
}
// 左对齐
leftJustify() {
const widgets = this.designService.modal.selecteds.map((item) => item.toJSON());
const { left } = this.designService.utils.getBoundaryByWidget(widgets);
this.designService.modal.selecteds.forEach((yWidgets) => yWidgets.set("x", left));
}
// 右对齐
rightJustify() {
const widgets = this.designService.modal.selecteds.map((item) => item.toJSON());
const { right } = this.designService.utils.getBoundaryByWidget(widgets);
this.designService.modal.selecteds.forEach((yWidgets) => {
const width = yWidgets.get("width");
yWidgets.set("x", right - width);
});
}
// 上对齐
topJustify() {
const widgets = this.designService.modal.selecteds.map((item) => item.toJSON());
const { top } = this.designService.utils.getBoundaryByWidget(widgets);
this.designService.modal.selecteds.forEach((yWidgets) => yWidgets.set("y", top));
}
// 下对齐
bottomJustify() {
const widgets = this.designService.modal.selecteds.map((item) => item.toJSON());
const { bottom } = this.designService.utils.getBoundaryByWidget(widgets);
this.designService.modal.selecteds.forEach((yWidgets) => {
const height = yWidgets.get("height");
yWidgets.set("y", bottom - height);
});
}
// 横向两端对齐
rowBetween() {
const widgets = this.designService.modal.selecteds.map((item) => item.toJSON());
const { left, right } = this.designService.utils.getBoundaryByWidget(widgets);
const widgetsWidth = widgets.reduce((total, widget) => total + widget.width, 0);
const boundaryWidth = right - left;
const interval = (boundaryWidth - widgetsWidth) / (widgets.length - 1);
let currentLeft = left;
this.designService.modal.selecteds.sort((a, b) => a.get("x") - b.get("x")).forEach((yWidget, index) => {
if (index === 0) {
currentLeft += yWidget.get("width");
} else {
currentLeft += interval;
yWidget.set("x", currentLeft);
currentLeft += yWidget.get("width");
}
});
}
// 纵向两端对齐
columnBetween() {
const widgets = this.designService.modal.selecteds.map((item) => item.toJSON());
const { top, bottom } = this.designService.utils.getBoundaryByWidget(widgets);
const widgetsHeight = widgets.reduce((total, widget) => total + widget.height, 0);
const boundaryHeight = bottom - top;
const interval = (boundaryHeight - widgetsHeight) / (widgets.length - 1);
let currentTop = top;
this.designService.modal.selecteds.sort((a, b) => a.get("y") - b.get("y")).forEach((yWidget, index) => {
if (index === 0) {
currentTop += yWidget.get("height");
} else {
currentTop += interval;
yWidget.set("y", currentTop);
currentTop += yWidget.get("height");
}
});
}
}
/* eslint-env browser */
/**
* @typedef {Object} Channel
* @property {Set<function(any, any):any>} Channel.subs
* @property {any} Channel.bc
*/
/**
* @type {Map<string, Channel>}
*/
const channels = new Map();
/* c8 ignore start */
class LocalStoragePolyfill {
/**
* @param {string} room
*/
constructor (room) {
this.room = room;
/**
* @type {null|function({data:ArrayBuffer}):void}
*/
this.onmessage = null;
/**
* @param {any} e
*/
this._onChange = e => e.key === room && this.onmessage !== null && this.onmessage({ data: fromBase64(e.newValue || '') });
onChange(this._onChange);
}
/**
* @param {ArrayBuffer} buf
*/
postMessage (buf) {
varStorage.setItem(this.room, toBase64(createUint8ArrayFromArrayBuffer(buf)));
}
close () {
offChange(this._onChange);
}
}
/* c8 ignore stop */
// Use BroadcastChannel or Polyfill
/* c8 ignore next */
const BC = typeof BroadcastChannel === 'undefined' ? LocalStoragePolyfill : BroadcastChannel;
/**
* @param {string} room
* @return {Channel}
*/
const getChannel = room =>
setIfUndefined(channels, room, () => {
const subs = create$4();
const bc = new BC(room);
/**
* @param {{data:ArrayBuffer}} e
*/
/* c8 ignore next */
bc.onmessage = e => subs.forEach(sub => sub(e.data, 'broadcastchannel'));
return {
bc, subs
}
});
/**
* Subscribe to global `publish` events.
*
* @function
* @param {string} room
* @param {function(any, any):any} f
*/
const subscribe = (room, f) => {
getChannel(room).subs.add(f);
return f
};
/**
* Unsubscribe from `publish` global events.
*
* @function
* @param {string} room
* @param {function(any, any):any} f
*/
const unsubscribe = (room, f) => {
const channel = getChannel(room);
const unsubscribed = channel.subs.delete(f);
if (unsubscribed && channel.subs.size === 0) {
channel.bc.close();
channels.delete(room);
}
return unsubscribed
};
/**
* Publish data to all subscribers (including subscribers on this tab)
*
* @function
* @param {string} room
* @param {any} data
* @param {any} [origin]
*/
const publish = (room, data, origin = null) => {
const c = getChannel(room);
c.bc.postMessage(data);
c.subs.forEach(sub => sub(data, origin));
};
/**
* @module sync-protocol
*/
/**
* @typedef {Map<number, number>} StateMap
*/
/**
* Core Yjs defines two message types:
* • YjsSyncStep1: Includes the State Set of the sending client. When received, the client should reply with YjsSyncStep2.
* • YjsSyncStep2: Includes all missing structs and the complete delete set. When received, the client is assured that it
* received all information from the remote client.
*
* In a peer-to-peer network, you may want to introduce a SyncDone message type. Both parties should initiate the connection
* with SyncStep1. When a client received SyncStep2, it should reply with SyncDone. When the local client received both
* SyncStep2 and SyncDone, it is assured that it is synced to the remote client.
*
* In a client-server model, you want to handle this differently: The client should initiate the connection with SyncStep1.
* When the server receives SyncStep1, it should reply with SyncStep2 immediately followed by SyncStep1. The client replies
* with SyncStep2 when it receives SyncStep1. Optionally the server may send a SyncDone after it received SyncStep2, so the
* client knows that the sync is finished. There are two reasons for this more elaborated sync model: 1. This protocol can
* easily be implemented on top of http and websockets. 2. The server shoul only reply to requests, and not initiate them.
* Therefore it is necesarry that the client initiates the sync.
*
* Construction of a message:
* [messageType : varUint, message definition..]
*
* Note: A message does not include information about the room name. This must to be handled by the upper layer protocol!
*
* stringify[messageType] stringifies a message definition (messageType is already read from the bufffer)
*/
const messageYjsSyncStep1 = 0;
const messageYjsSyncStep2 = 1;
const messageYjsUpdate = 2;
/**
* Create a sync step 1 message based on the state of the current shared document.
*
* @param {encoding.Encoder} encoder
* @param {Y.Doc} doc
*/
const writeSyncStep1 = (encoder, doc) => {
writeVarUint(encoder, messageYjsSyncStep1);
const sv = encodeStateVector(doc);
writeVarUint8Array(encoder, sv);
};
/**
* @param {encoding.Encoder} encoder
* @param {Y.Doc} doc
* @param {Uint8Array} [encodedStateVector]
*/
const writeSyncStep2 = (encoder, doc, encodedStateVector) => {
writeVarUint(encoder, messageYjsSyncStep2);
writeVarUint8Array(encoder, encodeStateAsUpdate(doc, encodedStateVector));
};
/**
* Read SyncStep1 message and reply with SyncStep2.
*
* @param {decoding.Decoder} decoder The reply to the received message
* @param {encoding.Encoder} encoder The received message
* @param {Y.Doc} doc
*/
const readSyncStep1 = (decoder, encoder, doc) =>
writeSyncStep2(encoder, doc, readVarUint8Array(decoder));
/**
* Read and apply Structs and then DeleteStore to a y instance.
*
* @param {decoding.Decoder} decoder
* @param {Y.Doc} doc
* @param {any} transactionOrigin
*/
const readSyncStep2 = (decoder, doc, transactionOrigin) => {
try {
applyUpdate(doc, readVarUint8Array(decoder), transactionOrigin);
} catch (error) {
// This catches errors that are thrown by event handlers
console.error('Caught error while handling a Yjs update', error);
}
};
/**
* @param {encoding.Encoder} encoder
* @param {Uint8Array} update
*/
const writeUpdate = (encoder, update) => {
writeVarUint(encoder, messageYjsUpdate);
writeVarUint8Array(encoder, update);
};
/**
* Read and apply Structs and then DeleteStore to a y instance.
*
* @param {decoding.Decoder} decoder
* @param {Y.Doc} doc
* @param {any} transactionOrigin
*/
const readUpdate = readSyncStep2;
/**
* @param {decoding.Decoder} decoder A message received from another client
* @param {encoding.Encoder} encoder The reply message. Will not be sent if empty.
* @param {Y.Doc} doc
* @param {any} transactionOrigin
*/
const readSyncMessage = (decoder, encoder, doc, transactionOrigin) => {
const messageType = readVarUint(decoder);
switch (messageType) {
case messageYjsSyncStep1:
readSyncStep1(decoder, encoder, doc);
break
case messageYjsSyncStep2:
readSyncStep2(decoder, doc, transactionOrigin);
break
case messageYjsUpdate:
readUpdate(decoder, doc, transactionOrigin);
break
default:
throw new Error('Unknown message type')
}
return messageType
};
const messagePermissionDenied = 0;
/**
* @callback PermissionDeniedHandler
* @param {any} y
* @param {string} reason
*/
/**
*
* @param {decoding.Decoder} decoder
* @param {Y.Doc} y
* @param {PermissionDeniedHandler} permissionDeniedHandler
*/
const readAuthMessage = (decoder, y, permissionDeniedHandler) => {
switch (readVarUint(decoder)) {
case messagePermissionDenied: permissionDeniedHandler(y, readVarString(decoder));
}
};
/**
* @module awareness-protocol
*/
const outdatedTimeout = 30000;
/**
* @typedef {Object} MetaClientState
* @property {number} MetaClientState.clock
* @property {number} MetaClientState.lastUpdated unix timestamp
*/
/**
* The Awareness class implements a simple shared state protocol that can be used for non-persistent data like awareness information
* (cursor, username, status, ..). Each client can update its own local state and listen to state changes of
* remote clients. Every client may set a state of a remote peer to `null` to mark the client as offline.
*
* Each client is identified by a unique client id (something we borrow from `doc.clientID`). A client can override
* its own state by propagating a message with an increasing timestamp (`clock`). If such a message is received, it is
* applied if the known state of that client is older than the new state (`clock < newClock`). If a client thinks that
* a remote client is offline, it may propagate a message with
* `{ clock: currentClientClock, state: null, client: remoteClient }`. If such a
* message is received, and the known clock of that client equals the received clock, it will override the state with `null`.
*
* Before a client disconnects, it should propagate a `null` state with an updated clock.
*
* Awareness states must be updated every 30 seconds. Otherwise the Awareness instance will delete the client state.
*
* @extends {Observable<string>}
*/
class Awareness extends Observable {
/**
* @param {Y.Doc} doc
*/
constructor (doc) {
super();
this.doc = doc;
/**
* @type {number}
*/
this.clientID = doc.clientID;
/**
* Maps from client id to client state
* @type {Map<number, Object<string, any>>}
*/
this.states = new Map();
/**
* @type {Map<number, MetaClientState>}
*/
this.meta = new Map();
this._checkInterval = /** @type {any} */ (setInterval(() => {
const now = getUnixTime();
if (this.getLocalState() !== null && (outdatedTimeout / 2 <= now - /** @type {{lastUpdated:number}} */ (this.meta.get(this.clientID)).lastUpdated)) {
// renew local clock
this.setLocalState(this.getLocalState());
}
/**
* @type {Array<number>}
*/
const remove = [];
this.meta.forEach((meta, clientid) => {
if (clientid !== this.clientID && outdatedTimeout <= now - meta.lastUpdated && this.states.has(clientid)) {
remove.push(clientid);
}
});
if (remove.length > 0) {
removeAwarenessStates(this, remove, 'timeout');
}
}, floor(outdatedTimeout / 10)));
doc.on('destroy', () => {
this.destroy();
});
this.setLocalState({});
}
destroy () {
this.emit('destroy', [this]);
this.setLocalState(null);
super.destroy();
clearInterval(this._checkInterval);
}
/**
* @return {Object<string,any>|null}
*/
getLocalState () {
return this.states.get(this.clientID) || null
}
/**
* @param {Object<string,any>|null} state
*/
setLocalState (state) {
const clientID = this.clientID;
const currLocalMeta = this.meta.get(clientID);
const clock = currLocalMeta === undefined ? 0 : currLocalMeta.clock + 1;
const prevState = this.states.get(clientID);
if (state === null) {
this.states.delete(clientID);
} else {
this.states.set(clientID, state);
}
this.meta.set(clientID, {
clock,
lastUpdated: getUnixTime()
});
const added = [];
const updated = [];
const filteredUpdated = [];
const removed = [];
if (state === null) {
removed.push(clientID);
} else if (prevState == null) {
if (state != null) {
added.push(clientID);
}
} else {
updated.push(clientID);
if (!equalityDeep(prevState, state)) {
filteredUpdated.push(clientID);
}
}
if (added.length > 0 || filteredUpdated.length > 0 || removed.length > 0) {
this.emit('change', [{ added, updated: filteredUpdated, removed }, 'local']);
}
this.emit('update', [{ added, updated, removed }, 'local']);
}
/**
* @param {string} field
* @param {any} value
*/
setLocalStateField (field, value) {
const state = this.getLocalState();
if (state !== null) {
this.setLocalState({
...state,
[field]: value
});
}
}
/**
* @return {Map<number,Object<string,any>>}
*/
getStates () {
return this.states
}
}
/**
* Mark (remote) clients as inactive and remove them from the list of active peers.
* This change will be propagated to remote clients.
*
* @param {Awareness} awareness
* @param {Array<number>} clients
* @param {any} origin
*/
const removeAwarenessStates = (awareness, clients, origin) => {
const removed = [];
for (let i = 0; i < clients.length; i++) {
const clientID = clients[i];
if (awareness.states.has(clientID)) {
awareness.states.delete(clientID);
if (clientID === awareness.clientID) {
const curMeta = /** @type {MetaClientState} */ (awareness.meta.get(clientID));
awareness.meta.set(clientID, {
clock: curMeta.clock + 1,
lastUpdated: getUnixTime()
});
}
removed.push(clientID);
}
}
if (removed.length > 0) {
awareness.emit('change', [{ added: [], updated: [], removed }, origin]);
awareness.emit('update', [{ added: [], updated: [], removed }, origin]);
}
};
/**
* @param {Awareness} awareness
* @param {Array<number>} clients
* @return {Uint8Array}
*/
const encodeAwarenessUpdate = (awareness, clients, states = awareness.states) => {
const len = clients.length;
const encoder = createEncoder();
writeVarUint(encoder, len);
for (let i = 0; i < len; i++) {
const clientID = clients[i];
const state = states.get(clientID) || null;
const clock = /** @type {MetaClientState} */ (awareness.meta.get(clientID)).clock;
writeVarUint(encoder, clientID);
writeVarUint(encoder, clock);
writeVarString(encoder, JSON.stringify(state));
}
return toUint8Array(encoder)
};
/**
* @param {Awareness} awareness
* @param {Uint8Array} update
* @param {any} origin This will be added to the emitted change event
*/
const applyAwarenessUpdate = (awareness, update, origin) => {
const decoder = createDecoder(update);
const timestamp = getUnixTime();
const added = [];
const updated = [];
const filteredUpdated = [];
const removed = [];
const len = readVarUint(decoder);
for (let i = 0; i < len; i++) {
const clientID = readVarUint(decoder);
let clock = readVarUint(decoder);
const state = JSON.parse(readVarString(decoder));
const clientMeta = awareness.meta.get(clientID);
const prevState = awareness.states.get(clientID);
const currClock = clientMeta === undefined ? 0 : clientMeta.clock;
if (currClock < clock || (currClock === clock && state === null && awareness.states.has(clientID))) {
if (state === null) {
// never let a remote client remove this local state
if (clientID === awareness.clientID && awareness.getLocalState() != null) {
// remote client removed the local state. Do not remote state. Broadcast a message indicating
// that this client still exists by increasing the clock
clock++;
} else {
awareness.states.delete(clientID);
}
} else {
awareness.states.set(clientID, state);
}
awareness.meta.set(clientID, {
clock,
lastUpdated: timestamp
});
if (clientMeta === undefined && state !== null) {
added.push(clientID);
} else if (clientMeta !== undefined && state === null) {
removed.push(clientID);
} else if (state !== null) {
if (!equalityDeep(state, prevState)) {
filteredUpdated.push(clientID);
}
updated.push(clientID);
}
}
}
if (added.length > 0 || filteredUpdated.length > 0 || removed.length > 0) {
awareness.emit('change', [{
added, updated: filteredUpdated, removed
}, origin]);
}
if (added.length > 0 || updated.length > 0 || removed.length > 0) {
awareness.emit('update', [{
added, updated, removed
}, origin]);
}
};
/**
* Utility module to work with urls.
*
* @module url
*/
/**
* @param {Object<string,string>} params
* @return {string}
*/
const encodeQueryParams = params =>
map(params, (val, key) => `${encodeURIComponent(key)}=${encodeURIComponent(val)}`).join('&');
/**
* @module provider/websocket
*/
const messageSync = 0;
const messageQueryAwareness = 3;
const messageAwareness = 1;
const messageAuth = 2;
/**
* encoder, decoder, provider, emitSynced, messageType
* @type {Array<function(encoding.Encoder, decoding.Decoder, WebsocketProvider, boolean, number):void>}
*/
const messageHandlers = [];
messageHandlers[messageSync] = (
encoder,
decoder,
provider,
emitSynced,
_messageType
) => {
writeVarUint(encoder, messageSync);
const syncMessageType = readSyncMessage(
decoder,
encoder,
provider.doc,
provider
);
if (
emitSynced && syncMessageType === messageYjsSyncStep2 &&
!provider.synced
) {
provider.synced = true;
}
};
messageHandlers[messageQueryAwareness] = (
encoder,
_decoder,
provider,
_emitSynced,
_messageType
) => {
writeVarUint(encoder, messageAwareness);
writeVarUint8Array(
encoder,
encodeAwarenessUpdate(
provider.awareness,
Array.from(provider.awareness.getStates().keys())
)
);
};
messageHandlers[messageAwareness] = (
_encoder,
decoder,
provider,
_emitSynced,
_messageType
) => {
applyAwarenessUpdate(
provider.awareness,
readVarUint8Array(decoder),
provider
);
};
messageHandlers[messageAuth] = (
_encoder,
decoder,
provider,
_emitSynced,
_messageType
) => {
readAuthMessage(
decoder,
provider.doc,
(_ydoc, reason) => permissionDeniedHandler(provider, reason)
);
};
// @todo - this should depend on awareness.outdatedTime
const messageReconnectTimeout = 30000;
/**
* @param {WebsocketProvider} provider
* @param {string} reason
*/
const permissionDeniedHandler = (provider, reason) =>
console.warn(`Permission denied to access ${provider.url}.\n${reason}`);
/**
* @param {WebsocketProvider} provider
* @param {Uint8Array} buf
* @param {boolean} emitSynced
* @return {encoding.Encoder}
*/
const readMessage = (provider, buf, emitSynced) => {
const decoder = createDecoder(buf);
const encoder = createEncoder();
const messageType = readVarUint(decoder);
const messageHandler = provider.messageHandlers[messageType];
if (/** @type {any} */ (messageHandler)) {
messageHandler(encoder, decoder, provider, emitSynced, messageType);
} else {
console.error('Unable to compute message');
}
return encoder
};
/**
* @param {WebsocketProvider} provider
*/
const setupWS = (provider) => {
if (provider.shouldConnect && provider.ws === null) {
const websocket = new provider._WS(provider.url);
websocket.binaryType = 'arraybuffer';
provider.ws = websocket;
provider.wsconnecting = true;
provider.wsconnected = false;
provider.synced = false;
websocket.onmessage = (event) => {
provider.wsLastMessageReceived = getUnixTime();
const encoder = readMessage(provider, new Uint8Array(event.data), true);
if (length(encoder) > 1) {
websocket.send(toUint8Array(encoder));
}
};
websocket.onerror = (event) => {
provider.emit('connection-error', [event, provider]);
};
websocket.onclose = (event) => {
provider.emit('connection-close', [event, provider]);
provider.ws = null;
provider.wsconnecting = false;
if (provider.wsconnected) {
provider.wsconnected = false;
provider.synced = false;
// update awareness (all users except local left)
removeAwarenessStates(
provider.awareness,
Array.from(provider.awareness.getStates().keys()).filter((client) =>
client !== provider.doc.clientID
),
provider
);
provider.emit('status', [{
status: 'disconnected'
}]);
} else {
provider.wsUnsuccessfulReconnects++;
}
// Start with no reconnect timeout and increase timeout by
// using exponential backoff starting with 100ms
setTimeout(
setupWS,
min(
pow(2, provider.wsUnsuccessfulReconnects) * 100,
provider.maxBackoffTime
),
provider
);
};
websocket.onopen = () => {
provider.wsLastMessageReceived = getUnixTime();
provider.wsconnecting = false;
provider.wsconnected = true;
provider.wsUnsuccessfulReconnects = 0;
provider.emit('status', [{
status: 'connected'
}]);
// always send sync step 1 when connected
const encoder = createEncoder();
writeVarUint(encoder, messageSync);
writeSyncStep1(encoder, provider.doc);
websocket.send(toUint8Array(encoder));
// broadcast local awareness state
if (provider.awareness.getLocalState() !== null) {
const encoderAwarenessState = createEncoder();
writeVarUint(encoderAwarenessState, messageAwareness);
writeVarUint8Array(
encoderAwarenessState,
encodeAwarenessUpdate(provider.awareness, [
provider.doc.clientID
])
);
websocket.send(toUint8Array(encoderAwarenessState));
}
};
provider.emit('status', [{
status: 'connecting'
}]);
}
};
/**
* @param {WebsocketProvider} provider
* @param {ArrayBuffer} buf
*/
const broadcastMessage = (provider, buf) => {
const ws = provider.ws;
if (provider.wsconnected && ws && ws.readyState === ws.OPEN) {
ws.send(buf);
}
if (provider.bcconnected) {
publish(provider.bcChannel, buf, provider);
}
};
/**
* Websocket Provider for Yjs. Creates a websocket connection to sync the shared document.
* The document name is attached to the provided url. I.e. the following example
* creates a websocket connection to http://localhost:1234/my-document-name
*
* @example
* import * as Y from 'yjs'
* import { WebsocketProvider } from 'y-websocket'
* const doc = new Y.Doc()
* const provider = new WebsocketProvider('http://localhost:1234', 'my-document-name', doc)
*
* @extends {Observable<string>}
*/
class WebsocketProvider extends Observable {
/**
* @param {string} serverUrl
* @param {string} roomname
* @param {Y.Doc} doc
* @param {object} opts
* @param {boolean} [opts.connect]
* @param {awarenessProtocol.Awareness} [opts.awareness]
* @param {Object<string,string>} [opts.params]
* @param {typeof WebSocket} [opts.WebSocketPolyfill] Optionall provide a WebSocket polyfill
* @param {number} [opts.resyncInterval] Request server state every `resyncInterval` milliseconds
* @param {number} [opts.maxBackoffTime] Maximum amount of time to wait before trying to reconnect (we try to reconnect using exponential backoff)
* @param {boolean} [opts.disableBc] Disable cross-tab BroadcastChannel communication
*/
constructor (serverUrl, roomname, doc, {
connect = true,
awareness = new Awareness(doc),
params = {},
WebSocketPolyfill = WebSocket,
resyncInterval = -1,
maxBackoffTime = 2500,
disableBc = false
} = {}) {
super();
// ensure that url is always ends with /
while (serverUrl[serverUrl.length - 1] === '/') {
serverUrl = serverUrl.slice(0, serverUrl.length - 1);
}
const encodedParams = encodeQueryParams(params);
this.maxBackoffTime = maxBackoffTime;
this.bcChannel = serverUrl + '/' + roomname;
this.url = serverUrl + '/' + roomname +
(encodedParams.length === 0 ? '' : '?' + encodedParams);
this.roomname = roomname;
this.doc = doc;
this._WS = WebSocketPolyfill;
this.awareness = awareness;
this.wsconnected = false;
this.wsconnecting = false;
this.bcconnected = false;
this.disableBc = disableBc;
this.wsUnsuccessfulReconnects = 0;
this.messageHandlers = messageHandlers.slice();
/**
* @type {boolean}
*/
this._synced = false;
/**
* @type {WebSocket?}
*/
this.ws = null;
this.wsLastMessageReceived = 0;
/**
* Whether to connect to other peers or not
* @type {boolean}
*/
this.shouldConnect = connect;
/**
* @type {number}
*/
this._resyncInterval = 0;
if (resyncInterval > 0) {
this._resyncInterval = /** @type {any} */ (setInterval(() => {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
// resend sync step 1
const encoder = createEncoder();
writeVarUint(encoder, messageSync);
writeSyncStep1(encoder, doc);
this.ws.send(toUint8Array(encoder));
}
}, resyncInterval));
}
/**
* @param {ArrayBuffer} data
* @param {any} origin
*/
this._bcSubscriber = (data, origin) => {
if (origin !== this) {
const encoder = readMessage(this, new Uint8Array(data), false);
if (length(encoder) > 1) {
publish(this.bcChannel, toUint8Array(encoder), this);
}
}
};
/**
* Listens to Yjs updates and sends them to remote peers (ws and broadcastchannel)
* @param {Uint8Array} update
* @param {any} origin
*/
this._updateHandler = (update, origin) => {
if (origin !== this) {
const encoder = createEncoder();
writeVarUint(encoder, messageSync);
writeUpdate(encoder, update);
broadcastMessage(this, toUint8Array(encoder));
}
};
this.doc.on('update', this._updateHandler);
/**
* @param {any} changed
* @param {any} _origin
*/
this._awarenessUpdateHandler = ({ added, updated, removed }, _origin) => {
const changedClients = added.concat(updated).concat(removed);
const encoder = createEncoder();
writeVarUint(encoder, messageAwareness);
writeVarUint8Array(
encoder,
encodeAwarenessUpdate(awareness, changedClients)
);
broadcastMessage(this, toUint8Array(encoder));
};
this._unloadHandler = () => {
removeAwarenessStates(
this.awareness,
[doc.clientID],
'window unload'
);
};
if (typeof window !== 'undefined') {
window.addEventListener('unload', this._unloadHandler);
} else if (typeof process !== 'undefined') {
process.on('exit', this._unloadHandler);
}
awareness.on('update', this._awarenessUpdateHandler);
this._checkInterval = /** @type {any} */ (setInterval(() => {
if (
this.wsconnected &&
messageReconnectTimeout <
getUnixTime() - this.wsLastMessageReceived
) {
// no message received in a long time - not even your own awareness
// updates (which are updated every 15 seconds)
/** @type {WebSocket} */ (this.ws).close();
}
}, messageReconnectTimeout / 10));
if (connect) {
this.connect();
}
}
/**
* @type {boolean}
*/
get synced () {
return this._synced
}
set synced (state) {
if (this._synced !== state) {
this._synced = state;
this.emit('synced', [state]);
this.emit('sync', [state]);
}
}
destroy () {
if (this._resyncInterval !== 0) {
clearInterval(this._resyncInterval);
}
clearInterval(this._checkInterval);
this.disconnect();
if (typeof window !== 'undefined') {
window.removeEventListener('unload', this._unloadHandler);
} else if (typeof process !== 'undefined') {
process.off('exit', this._unloadHandler);
}
this.awareness.off('update', this._awarenessUpdateHandler);
this.doc.off('update', this._updateHandler);
super.destroy();
}
connectBc () {
if (this.disableBc) {
return
}
if (!this.bcconnected) {
subscribe(this.bcChannel, this._bcSubscriber);
this.bcconnected = true;
}
// send sync step1 to bc
// write sync step 1
const encoderSync = createEncoder();
writeVarUint(encoderSync, messageSync);
writeSyncStep1(encoderSync, this.doc);
publish(this.bcChannel, toUint8Array(encoderSync), this);
// broadcast local state
const encoderState = createEncoder();
writeVarUint(encoderState, messageSync);
writeSyncStep2(encoderState, this.doc);
publish(this.bcChannel, toUint8Array(encoderState), this);
// write queryAwareness
const encoderAwarenessQuery = createEncoder();
writeVarUint(encoderAwarenessQuery, messageQueryAwareness);
publish(
this.bcChannel,
toUint8Array(encoderAwarenessQuery),
this
);
// broadcast local awareness state
const encoderAwarenessState = createEncoder();
writeVarUint(encoderAwarenessState, messageAwareness);
writeVarUint8Array(
encoderAwarenessState,
encodeAwarenessUpdate(this.awareness, [
this.doc.clientID
])
);
publish(
this.bcChannel,
toUint8Array(encoderAwarenessState),
this
);
}
disconnectBc () {
// broadcast message with local awareness state set to null (indicating disconnect)
const encoder = createEncoder();
writeVarUint(encoder, messageAwareness);
writeVarUint8Array(
encoder,
encodeAwarenessUpdate(this.awareness, [
this.doc.clientID
], new Map())
);
broadcastMessage(this, toUint8Array(encoder));
if (this.bcconnected) {
unsubscribe(this.bcChannel, this._bcSubscriber);
this.bcconnected = false;
}
}
disconnect () {
this.shouldConnect = false;
this.disconnectBc();
if (this.ws !== null) {
this.ws.close();
}
}
connect () {
this.shouldConnect = true;
if (!this.wsconnected && this.ws === null) {
setupWS(this);
this.connectBc();
}
}
}
class SynchronizeService {
constructor(serverUrl = `ws://localhost:1234`, roomName = "root2") {
this.onDataUpdate = () => {
};
this.ydoc = new Doc();
this.wsProvider = new WebsocketProvider(serverUrl, roomName, this.ydoc, { connect: false });
this.yWidget = this.ydoc.getArray("arr");
this.yWidget.observeDeep(this._onDataUpdate.bind(this));
vue.onBeforeUnmount(() => this.destroy());
}
createWidget(widget) {
const map = new YMap();
for (let key in widget) {
map.set(key, widget[key]);
}
return map;
}
// 监听Widget数组的更新
_onDataUpdate(YArrayEvent, Transaction) {
const data = this.yWidget.toJSON();
let updateData = [];
YArrayEvent.forEach((event) => {
const eventData = {
added: event.changes.added,
deleted: event.changes.deleted,
delta: event.changes.delta,
keys: event.changes.keys,
path: event.path,
yWidget: event.currentTarget
};
if (eventData.delta.length !== 0) {
let deltaData = {
retain: 0
};
eventData.delta.forEach((item) => {
item.retain && (deltaData.retain = item.retain);
item.insert && (deltaData.insert = item.insert);
item.delete && (deltaData.delete = item.delete);
});
updateData.push({
type: "array",
path: eventData.path,
target: event.target,
deltaData,
keysData: void 0,
handler: this.handlerArray.bind(this, eventData.path, deltaData)
});
}
if (eventData.keys.size !== 0) {
eventData.keys.forEach((value, key) => {
const keysData = {
key,
action: value.action,
value: event.target.get(key)
};
updateData.push({
type: "map",
path: eventData.path,
target: event.target,
deltaData: void 0,
keysData,
handler: this.handlerMap.bind(this, eventData.path, keysData)
});
});
}
});
this.onDataUpdate(data, updateData);
}
// 根据path获取当前被修改的对象
getCurrentByPath(data, path) {
let current = data;
while (path.length > 0) {
const key = path.shift();
if (!CheckType.isExist(key))
return current;
current = current[key];
}
return current;
}
// 处理数组
handlerArray(path, deltaData, currentData) {
const current = this.getCurrentByPath(currentData, path);
if (!CheckType.isArray(current))
throw "handlerArray current not array";
if (deltaData.insert && CheckType.isArray(deltaData.insert)) {
const insertData = deltaData.insert.map((item) => item instanceof YMap || item instanceof YArray ? item.toJSON() : item);
current.splice(deltaData.retain || 0, 0, ...insertData);
} else if (deltaData.delete && CheckType.isNumber(deltaData.delete)) {
current.splice(deltaData.retain || 0, deltaData.delete);
}
}
// 处理map
handlerMap(path, keysData, currentData) {
const current = this.getCurrentByPath(currentData, path);
if (!CheckType.isObject(current))
throw "handlerMap current not object";
if (keysData.action === "delete") {
Reflect.deleteProperty(current, keysData.key);
} else {
if (CheckType.isArray(keysData.value)) {
const insertData = keysData.value.map((item) => item instanceof YMap || item instanceof YArray ? item.toJSON() : item);
Reflect.set(current, keysData.key, insertData);
} else {
Reflect.set(current, keysData.key, keysData.value);
}
}
}
destroy() {
this.yWidget.unobserveDeep(this._onDataUpdate);
this.wsProvider.destroy();
}
}
function mitt(n){return {all:n=n||new Map,on:function(t,e){var i=n.get(t);i?i.push(e):n.set(t,[e]);},off:function(t,e){var i=n.get(t);i&&(e?i.splice(i.indexOf(e)>>>0,1):n.set(t,[]));},emit:function(t,e){var i=n.get(t);i&&i.slice().map(function(n){n(e);}),(i=n.get("*"))&&i.slice().map(function(n){n(t,e);});}}}
class UtilsService {
constructor(service) {
this.service = service;
this.newWidgetP2CavnaseP = (point) => {
return {
x: (point.x - this.service.canvase2PanelRect.value.x + this.service.modal.scrollLeft) / this.service.modal.scale - this.service.modal.pageRect.x,
y: (point.y - this.service.canvase2PanelRect.value.y + this.service.modal.scrollTop) / this.service.modal.scale - this.service.modal.pageRect.y
};
};
this.cavnaseP2NewWidgetP = (point) => {
return {
x: (point.x + this.service.modal.pageRect.x) * this.service.modal.scale - this.service.modal.scrollLeft + this.service.canvase2PanelRect.value.x,
y: (point.y + this.service.modal.pageRect.y) * this.service.modal.scale - this.service.modal.scrollTop + this.service.canvase2PanelRect.value.y
};
};
}
/**
* 重新计算页面布局
*/
recountPage() {
const modal = this.service.modal;
const viewWidth = modal.pageRect.width * modal.scale + DesignService.SPAN * 2;
if (viewWidth < modal.canvaseRect.width) {
modal.pageRect.cwidth = modal.canvaseRect.width / modal.scale;
} else {
modal.pageRect.cwidth = viewWidth / modal.scale;
}
const viewHeight = (modal.pageRect.height + DesignService.SPAN * 2) * modal.scale;
if (viewHeight < modal.canvaseRect.height) {
modal.pageRect.cheight = modal.canvaseRect.height / modal.scale;
} else {
modal.pageRect.cheight = viewHeight / modal.scale;
}
modal.pageRect.x = (modal.pageRect.cwidth - modal.pageRect.width) / 2;
modal.pageRect.y = DesignService.SPAN / modal.scale;
this.service.emit("page-resized", modal.pageRect);
}
// 拖动widget时自动拓展高度
autoHeight(bottom) {
var _a;
if (!this.service.props.autoHeight)
return;
let span = bottom - this.service.modal.pageRect.height + this.service.modal.pageRect.padding[2];
if (span >= 0) {
span = span > 10 ? span : 10;
this.service.modal.pageRect.height += span;
this.service.modal.pageRect.cheight += span;
(_a = this.service.alignLineService) === null || _a === void 0 ? void 0 : _a.setPaddingLine();
this.service.emit("addHeight", this.service.modal.pageRect.height, span);
}
}
paddingFormat(padding) {
let p = [0, 0, 0, 0];
if (padding.length === 1) {
p = [padding[0], padding[0], padding[0], padding[0]];
} else if (padding.length === 2) {
p = [padding[0], padding[1], padding[0], padding[1]];
} else if (padding.length === 3) {
p = [padding[0], padding[1], padding[1], padding[2]];
} else if (padding.length === 4) {
p = padding;
}
return p;
}
// 拖动的widgets是否不覆盖页面上的widgets
isNotOverlap(widgets) {
const { top, bottom, left, right } = this.getBoundaryByWidget(widgets);
const pageRect = this.service.modal.pageRect;
if (left + (right - left) / 2 < 0 || top + (bottom - top) / 2 < 0 || right - (right - left) / 2 > pageRect.width) {
return false;
}
if (widgets.every((item) => item.enableOverlap))
return true;
return this.service.modal.widgets.every((widget) => {
if (widget.enableOverlap)
return true;
if (widgets.some((item) => item.id === widget.id))
return true;
let span = this.service.props.adsorbSpan - 1;
span = span < 0 ? 0 : span;
return !(right > widget.x + span && widget.x + widget.width > left + span && bottom > widget.y + span && widget.y + widget.height > top + span);
});
}
// 计算多个widget构成的边界
getBoundaryByWidget(widgets) {
const tList = [];
const bList = [];
const lList = [];
const rList = [];
widgets.forEach((widget) => {
const { top: top2, bottom: bottom2, left: left2, right: right2 } = this.widget2Boundary(widget);
tList.push(top2);
bList.push(bottom2);
lList.push(left2);
rList.push(right2);
});
const top = Math.min.apply(null, tList);
const bottom = Math.max.apply(null, bList);
const left = Math.min.apply(null, lList);
const right = Math.max.apply(null, rList);
return {
top,
bottom,
left,
right
};
}
// 是否在页面内
inCanvase(event) {
const modal = this.service.modal;
if (modal.newWidget) {
return event.clientX - modal.canvaseRect.x - modal.pageRect.x > 0 && event.clientY - modal.canvaseRect.y - modal.pageRect.y > 0 && event.clientX < modal.canvaseRect.x + modal.pageRect.x + modal.pageRect.width && event.clientY < modal.canvaseRect.y + modal.pageRect.y + modal.pageRect.height;
} else {
return false;
}
}
// 获取widget的边界
widget2Boundary(widget) {
return {
left: widget.x,
right: widget.x + widget.width,
top: widget.y,
bottom: widget.y + widget.height
};
}
getYWidgetById(id) {
let yWidgets = this.service.syncService.yWidget.toArray();
const idx = yWidgets.findIndex((item) => item.get("id") === id);
return {
yWidget: this.service.syncService.yWidget.get(idx),
index: idx
};
}
}
class Publisher {
constructor() {
this.subscriberList = [];
}
// 发布
emit(...args) {
return this.subscriberList.map((item) => {
if (CheckType.isFunction(item)) {
return item.apply(this, args);
} else {
console.error("Publisher emit is not Function");
return item;
}
});
}
// 订阅
on(callback) {
this.subscriberList.push(callback);
}
// 取消订阅
off(callback) {
this.subscriberList = this.subscriberList.filter((fun) => fun !== callback);
}
// 清空订阅者
clear() {
this.subscriberList.length = 0;
}
}
function debounce(fn, delay) {
let timer = null;
return function() {
const self = this;
const args = arguments;
timer && clearTimeout(timer);
timer = setTimeout(function() {
fn.apply(self, args);
}, delay);
};
}
class DesignService {
constructor(props, emit, slots) {
this.props = props;
this.emit = emit;
this.drawerRef = vue.ref(null);
this.boundaryLine = computed(() => {
var _a;
return ((_a = this.alignLineService) === null || _a === void 0 ? void 0 : _a.boundaryLine) || [];
});
this.isNotOverlapInGridPublisher = new Publisher();
this.canvase2PanelRect = computed(() => {
return {
x: this.modal.canvaseRect.x - this.modal.rect.x,
y: this.modal.canvaseRect.y - this.modal.rect.y,
ex: this.modal.canvaseRect.x - this.modal.rect.x + this.modal.canvaseRect.width,
ey: this.modal.canvaseRect.y - this.modal.rect.y + this.modal.canvaseRect.height
};
});
this.emitter = mitt();
this.newWidgetDragingRegistHandler = (event) => {
var _a;
if (!this.modal.newWidget) {
if (this.selectedNewWidget) {
this.selectedMousePoint = { x: event.clientX, y: event.clientY };
let x = event.clientX - this.selectedNewWidget.width / 2 - this.modal.rect.x;
let y = event.clientY - this.selectedNewWidget.height / 2 - this.modal.rect.y;
this.modal.newWidget = Object.assign(Object.assign({}, this.selectedNewWidget), { x, y, state: -1, moveing: false, resizing: false, baseX: x, baseY: y, isOverlapping: false });
this.selectedNewOrgState = Object.assign({}, this.modal.newWidget);
this.emit("addNewWidget", this.modal.newWidget);
}
} else if (this.selectedMousePoint && this.selectedNewOrgState) {
const hspan = event.clientX - this.selectedMousePoint.x;
const vspan = event.y - this.selectedMousePoint.y;
this.modal.newWidget.x = this.selectedNewOrgState.x + hspan;
this.modal.newWidget.y = this.selectedNewOrgState.y + vspan;
this.modal.newWidget.baseX = this.selectedNewOrgState.x + hspan;
this.modal.newWidget.baseY = this.selectedNewOrgState.y + vspan;
const { x, y } = this.utils.newWidgetP2CavnaseP(this.modal.newWidget);
const widget = Object.assign(Object.assign({}, this.modal.newWidget), { x, y });
(_a = this.alignLineService) === null || _a === void 0 ? void 0 : _a.onNewWidgetMove([widget], this);
const gridNotOverlapping = this.isNotOverlapInGridPublisher.emit([widget]).every((item) => item);
this.modal.newWidget.isOverlapping = !(this.utils.isNotOverlap([widget]) && gridNotOverlapping);
this.utils.autoHeight(widget.y + widget.height);
this.emit("drag-moving", this.modal.newWidget);
}
};
this.newWidgetDropRegistHandler = (event) => {
window.removeEventListener("mousemove", this.newWidgetDragingRegistHandler, true);
window.removeEventListener("mouseup", this.newWidgetDropRegistHandler, true);
if (this.modal.newWidget && !this.modal.newWidget.isOverlapping) {
const widget = Object.assign({}, this.modal.newWidget);
widget.state = 0;
const { x, y } = this.utils.newWidgetP2CavnaseP(widget);
widget.x = x;
widget.y = y;
widget.baseX = x;
widget.baseY = y;
const yWidget = this.syncService.createWidget(widget);
this.syncService.yWidget.push([yWidget]);
this.setSelected([yWidget]);
this.emitter.emit("onAddWidget", yWidget);
this.emit("drag-end", widget);
} else {
this.emit("delNewWidget", this.modal.newWidget);
}
this.modal.newWidget = void 0;
this.selectedNewWidget = void 0;
this.selectedMousePoint = void 0;
this.selectedNewOrgState = void 0;
};
vue.provide(DesignService.token, this);
vue.provide(DesignService.itemSlot, slots.item);
this.syncService = new SynchronizeService();
this.alignLineService = new AlignmentLineService(props, this);
this.utils = new UtilsService(this);
let { value: widgets, width, height } = props;
this.modal = vue.reactive({
scale: 1,
rect: { x: 0, y: 0, width: 0, height: 0 },
canvaseRect: { x: 0, y: 0, width: 0, height: 0 },
pageRect: {
x: 0,
y: 0,
width,
height,
cwidth: 0,
cheight: 0,
padding: this.utils.paddingFormat(props.pagePadding)
},
scrollLeft: 0,
scrollTop: 0,
moveing: false,
selecteds: [],
widgets: []
});
this.syncService.onDataUpdate = (data, updateData) => {
updateData.forEach((update) => {
var _a;
update.handler(this.modal.widgets);
(_a = this.alignLineService) === null || _a === void 0 ? void 0 : _a.handlerAlignmentLine(update);
});
};
this.initWidgets(widgets);
this.modal.canvaseRect.height = height + DesignService.SPAN * 2 / this.modal.scale;
vue.watch(() => props.value, () => this.initWidgets(props.value));
this.handlerSelectedChange = this.handlerSelecteds();
}
initWidgets(widgets) {
if (widgets) {
this.syncService.yWidget.delete(0, this.syncService.yWidget.length);
this.modal.selecteds = [];
this.alignLineService.clearAllLine();
this.alignLineService.setPaddingLine();
let w = widgets.map((item) => this.syncService.createWidget(Object.assign(Object.assign({}, item), { state: 0, moveing: false, resizing: false, isOverlapping: false, baseX: item.x, baseY: item.y })));
this.syncService.yWidget.push(w);
}
}
handlerSelecteds() {
let selectedIds = [];
const handler = () => {
selectedIds = [];
return this.modal.selecteds.map((item) => {
selectedIds.push(item.get("id"));
return item.toJSON();
});
};
return debounce(() => {
if (this.modal.selecteds.length !== selectedIds.length) {
const selecteds = handler();
this.emit("selected-change", selecteds);
return;
}
for (const item of this.modal.selecteds) {
const id = item.get("id");
if (!selectedIds.includes(id)) {
const selecteds = handler();
this.emit("selected-change", selecteds);
return;
}
}
}, 100);
}
createWidgetHandler(widget) {
this.selectedNewWidget = widget;
window.addEventListener("mousemove", this.newWidgetDragingRegistHandler, true);
window.addEventListener("mouseup", this.newWidgetDropRegistHandler, true);
}
deleteWidget(id) {
var _a;
const widgetIdx = this.syncService.yWidget.toArray().findIndex((item) => item.get("id") === id);
if (widgetIdx === -1)
return;
this.syncService.yWidget.delete(widgetIdx, 1);
(_a = this.alignLineService) === null || _a === void 0 ? void 0 : _a.delBoundaryLine(id);
}
addScale(value) {
this.setScale(this.modal.scale + value);
}
// 设置缩放
setScale(newscale) {
if (newscale < 0.2) {
newscale = 0.2;
}
if (newscale > 10) {
newscale = 10;
}
this.modal.scale = newscale;
this.utils.recountPage();
}
// 设置选中区域
setSelectedArea(begin, end) {
const pb = {
x: Math.min(begin.x, end.x),
y: Math.min(begin.y, end.y)
};
const pe = {
x: Math.max(begin.x, end.x),
y: Math.max(begin.y, end.y)
};
const selecteds = [];
this.syncService.yWidget.toJSON().forEach((item, index) => {
const itemEnd = { x: item.x + item.width, y: item.y + item.height };
if (Math.max(pb.x, item.x) < Math.min(pe.x, itemEnd.x) && Math.max(pb.y, item.y) < Math.min(pe.y, itemEnd.y)) {
selecteds.push(this.syncService.yWidget.get(index));
}
});
if (selecteds && selecteds.length > 0) {
this.setSelected(selecteds);
}
}
// 设置选中的widget
addSelected(widget) {
if (widget) {
widget.set("state", 1);
this.modal.selecteds.push(widget);
this.handlerSelectedChange();
}
}
// 设置选中的widgets
setSelected(widgets) {
this.clearnSelected();
if (widgets) {
for (let i = 0; i < widgets.length; i++) {
widgets[i].set("state", 1);
}
this.modal.selecteds.push(...widgets);
this.handlerSelectedChange();
}
}
clearnSelected() {
if (this.modal.selecteds && this.modal.selecteds.length > 0) {
for (let i = 0; i < this.modal.selecteds.length; i++) {
this.modal.selecteds[i].set("state", 0);
}
this.modal.selecteds.splice(0, this.modal.selecteds.length);
this.handlerSelectedChange();
}
}
// 删除选中的widget
deleteSelected(id) {
const idx = this.modal.selecteds.findIndex((widget) => widget.get("id") === id);
if (idx === -1)
return;
this.modal.selecteds[idx].set("state", 0);
this.modal.selecteds.splice(idx, 1);
}
// 修改页面尺寸内边距
changePageSize(width, height, padding) {
this.clearnSelected();
const pageRect = this.modal.pageRect;
if (pageRect.width !== width || pageRect.padding[1] !== padding[1] || pageRect.padding[3] !== padding[3]) {
const [oTop, oRight, oBottom, oLeft] = pageRect.padding;
const [nTop, nRight, nBottom, nLeft] = padding;
const newWidth = width - nLeft - nRight;
const oldWidth = pageRect.width - oLeft - oRight;
const widthRatio = (newWidth - oldWidth) / oldWidth;
this.syncService.yWidget.forEach((yWidget) => {
const yWidth = yWidget.get("width");
const x = yWidget.get("x");
yWidget.set("width", yWidth + yWidth * widthRatio);
yWidget.set("x", x + (x + nLeft - oLeft - nLeft) * widthRatio + nLeft - oLeft);
});
}
if (pageRect.height !== height) ;
if (pageRect.padding[0] !== padding[0]) {
const oTop = pageRect.padding[0];
const nTop = padding[0];
this.syncService.yWidget.forEach((yWidget) => {
const y = yWidget.get("y");
yWidget.set("y", y + (nTop - oTop));
});
}
this.modal.pageRect.width = width;
this.modal.pageRect.height = height;
this.modal.pageRect.padding = padding;
this.alignLineService.setPaddingLine();
this.utils.recountPage();
}
getPageWidgets() {
return this.modal.widgets;
}
}
DesignService.token = Symbol("DesignService");
DesignService.itemSlot = Symbol();
DesignService.SPAN = 40;
class DraggingService {
constructor(service, emit) {
this.service = service;
this.emit = emit;
this.orgPosition = /* @__PURE__ */ new Map();
this.copyWidgets = [];
this.dragHandler = (event) => {
var _a;
event.preventDefault();
event.stopPropagation();
if (!this.modal.beginDragging) {
this.modal.beginDragging = true;
for (let i = 0; i < this.service.modal.selecteds.length; i++) {
this.service.modal.selecteds[i].set("state", 3);
}
}
const hspan = (event.clientX - this.dragStartPosition.x) / this.service.modal.scale;
const vspan = (event.clientY - this.dragStartPosition.y) / this.service.modal.scale;
for (let i = 0; i < this.service.modal.selecteds.length; i++) {
const yWidget = this.service.modal.selecteds[i];
const orgPoint = this.orgPosition.get(yWidget.get("id"));
if (orgPoint && yWidget) {
yWidget.set("x", orgPoint.x + hspan);
yWidget.set("y", orgPoint.y + vspan);
yWidget.set("baseX", orgPoint.x + hspan);
yWidget.set("baseY", orgPoint.y + vspan);
this.emit("drag-moving", yWidget.toJSON());
}
}
const widgets = this.service.modal.selecteds.map((item) => item.toJSON());
const gridNotOverlapping = this.service.isNotOverlapInGridPublisher.emit(widgets).every((item) => item);
if (!(this.service.utils.isNotOverlap(widgets) && gridNotOverlapping)) {
this.service.modal.selecteds.forEach((yWidget) => {
yWidget.set("isOverlapping", true);
});
this.modal.isOverlapping = true;
} else {
this.service.modal.selecteds.forEach((yWidget) => {
yWidget.set("isOverlapping", false);
});
this.modal.isOverlapping = false;
}
(_a = this.service.alignLineService) === null || _a === void 0 ? void 0 : _a.onWidgetGroupMove(this.service.modal.selecteds);
const { bottom } = this.service.utils.getBoundaryByWidget(this.service.modal.selecteds.map((yWidget) => yWidget.toJSON()));
this.service.utils.autoHeight(bottom);
};
this.dragEndHandler = (event) => {
event.preventDefault();
event.stopPropagation();
window.removeEventListener("mousemove", this.dragHandler, true);
window.removeEventListener("mouseup", this.dragEndHandler, true);
this.modal.beginDragging = false;
for (let i = 0; i < this.service.modal.selecteds.length; i++) {
const yWidget = this.service.modal.selecteds[i];
yWidget.set("moveing", false);
yWidget.set("state", 1);
if (this.modal.isOverlapping) {
const copyWidget = this.copyWidgets[i];
yWidget.set("x", copyWidget.x);
yWidget.set("y", copyWidget.y);
yWidget.set("baseX", copyWidget.baseX);
yWidget.set("baseY", copyWidget.baseY);
yWidget.set("isOverlapping", false);
}
this.emit("drag-end", yWidget.toJSON());
}
if (this.service.modal.selecteds.length === 1) {
this.service.emitter.emit("onAddWidget", this.service.modal.selecteds[0]);
}
this.modal.isOverlapping = false;
this.orgPosition.clear();
};
this.modal = vue.reactive({
beginDragging: false,
isOverlapping: false
});
this.dragStartPosition = { x: -1, y: -1 };
}
// 拖动单个组件时、拖动网格子组件时yWidget存在,拖动多个组件yWidget不存在
mousedownHandler(event, yWidget) {
event.preventDefault();
event.stopPropagation();
window.addEventListener("mousemove", this.dragHandler, true);
window.addEventListener("mouseup", this.dragEndHandler, true);
this.dragStartPosition.x = event.clientX;
this.dragStartPosition.y = event.clientY;
if (yWidget) {
if (event.altKey || event.metaKey) {
this.service.addSelected(yWidget);
} else {
this.service.setSelected([yWidget]);
}
}
this.modal.beginDragging = false;
this.orgPosition.clear();
for (let i = 0; i < this.service.modal.selecteds.length; i++) {
const yWidget2 = this.service.modal.selecteds[i];
yWidget2.set("moveing", true);
this.orgPosition.set(yWidget2.get("id"), { x: yWidget2.get("x"), y: yWidget2.get("y") });
}
this.copyWidgets = this.service.modal.selecteds.map((item) => item.toJSON());
}
}
const MIN_SPAN = 8;
var SizeBox = vue.defineComponent({
emits: ["drag-start", "drag-moving", "drag-end", "reszie-start", "resize-end", "resizeing"],
setup(props, { emit }) {
let orgMousePosition = { x: 0, y: 0 };
let direct = [];
const designService = vue.inject(DesignService.token);
const service = new DraggingService(designService, emit);
const position = vue.computed(() => {
const bxarray = designService.modal.selecteds.map((item) => item.get("x"));
const byarray = designService.modal.selecteds.map((item) => item.get("y"));
const begin = {
x: Math.min(...bxarray),
y: Math.min(...byarray)
};
const exarray = designService.modal.selecteds.map((item) => item.get("x") + item.get("width"));
const eyarray = designService.modal.selecteds.map((item) => item.get("y") + item.get("height"));
const end = {
x: Math.max(...exarray),
y: Math.max(...eyarray)
};
return {
x: begin.x,
y: begin.y,
width: end.x - begin.x,
height: end.y - begin.y
};
});
let orgPosition = { x: 0, y: 0, width: 0, height: 0 };
const oldWidgetPosition = /* @__PURE__ */ new Map();
const onMouseDown = (directparam, event) => {
event.preventDefault();
event.stopPropagation();
window.addEventListener("mousemove", onMouseMoveHandler, true);
window.addEventListener("mouseup", onMouseUp, true);
direct = directparam;
orgMousePosition = { x: event.clientX, y: event.clientY };
orgPosition = Object.assign({}, position.value);
oldWidgetPosition.clear();
for (let i = 0; i < designService.modal.selecteds.length; i++) {
const yWidget = designService.modal.selecteds[i];
oldWidgetPosition.set(yWidget.get("id"), {
x: yWidget.get("x"),
y: yWidget.get("y"),
width: yWidget.get("width"),
height: yWidget.get("height")
});
emit("reszie-start", yWidget.toJSON());
}
};
const onMouseMoveHandler = (event) => {
var _a;
let changed = false;
const newposition = { x: 0, y: 0, width: 0, height: 0 };
if (direct[0] !== "n") {
let widthDirect = 1;
const hspan = (event.clientX - orgMousePosition.x) / designService.modal.scale;
if (direct[0] === "l") {
widthDirect = -1;
newposition.x = hspan;
}
let widthSpan = hspan * widthDirect;
if (orgPosition.width + widthSpan < MIN_SPAN) {
widthSpan = MIN_SPAN - orgPosition.width;
if (direct[0] === "l") {
newposition.x = orgPosition.width - MIN_SPAN;
}
}
newposition.width = widthSpan;
changed = true;
}
if (direct[1] !== "n") {
let heightDirect = 1;
const vspan = event.clientY - orgMousePosition.y;
if (direct[1] === "t") {
heightDirect = -1;
newposition.y = vspan;
}
let heightSpan = vspan * heightDirect;
if (orgPosition.height + heightSpan < MIN_SPAN) {
heightSpan = MIN_SPAN - orgPosition.width;
if (direct[1] === "t") {
newposition.y = orgPosition.height - MIN_SPAN;
}
}
newposition.height = heightSpan;
changed = true;
}
if (changed) {
for (let i = 0; i < designService.modal.selecteds.length; i++) {
const widget = designService.modal.selecteds[i];
const old = oldWidgetPosition.get(widget.get("id"));
if (widget && old) {
widget.set("x", old.x + newposition.x);
widget.set("y", old.y + newposition.y);
widget.set("width", old.width + newposition.width);
widget.set("height", old.height + newposition.height);
widget.set("baseX", old.x + newposition.x);
widget.set("baseY", old.y + newposition.y);
widget.set("resizing", true);
emit("resizeing", widget.toJSON());
}
}
(_a = designService.alignLineService) === null || _a === void 0 ? void 0 : _a.onBoundaryMove(designService.modal.selecteds, direct);
}
};
const onMouseUp = (event) => {
window.removeEventListener("mousemove", onMouseMoveHandler, true);
window.removeEventListener("mouseup", onMouseUp, true);
for (let i = 0; i < designService.modal.selecteds.length; i++) {
const widget = designService.modal.selecteds[i];
widget.set("resizing", false);
emit("reszie-start", widget.toJSON());
}
oldWidgetPosition.clear();
};
const hx = vue.computed(() => position.value.x + 4);
const htop = vue.computed(() => position.value.y - 4);
const hbottom = vue.computed(() => position.value.y + position.value.height - 4);
const hwidth = vue.computed(() => position.value.width > 8 ? position.value.width - 8 : 0);
const vtop = vue.computed(() => position.value.y + 4);
const vleft = vue.computed(() => position.value.x - 4);
const vright = vue.computed(() => position.value.x + position.value.width - 4);
const vheight = vue.computed(() => position.value.height > 8 ? position.value.height - 8 : 0);
const renderBorders = () => {
if (designService.modal.selecteds.find((widget) => widget.get("moveing")))
return;
return [
// top
vue.h("div", {
class: "size-border-container size-border-h",
style: {
left: hx.value + "px",
top: htop.value + "px",
width: hwidth.value + "px"
},
onmousedown: (event) => onMouseDown(["n", "t"], event)
}, [
vue.h("div", {
class: "size-border-h border"
})
// h('sssxxx',{},[JSON.stringify(position.value)])
]),
// right
vue.h("div", {
class: "size-border-container size-border-v",
style: {
left: vright.value + "px",
top: vtop.value + "px",
height: vheight.value + "px"
},
onmousedown: (event) => onMouseDown(["r", "n"], event)
}, [
vue.h("div", {
class: "size-border-v border"
})
]),
// bottom
vue.h("div", {
class: "size-border-container size-border-h",
style: {
left: hx.value + "px",
top: hbottom.value + "px",
width: hwidth.value + "px"
},
onmousedown: (event) => onMouseDown(["n", "b"], event)
}, [
vue.h("div", {
class: "size-border-h border"
})
]),
// left
vue.h("div", {
class: "size-border-container size-border-v",
style: {
left: vleft.value + "px",
top: vtop.value + "px",
height: vheight.value + "px"
},
onmousedown: (event) => onMouseDown(["l", "n"], event)
}, [
[
vue.h("div", {
class: "size-border-v border"
})
]
])
];
};
const conLTX = vue.computed(() => position.value.x - 6);
const conLTY = vue.computed(() => position.value.y - 6);
const conRTX = vue.computed(() => conLTX.value + position.value.width);
const conRBX = vue.computed(() => conLTY.value + position.value.height);
const drawConerBox = () => {
if (designService.modal.selecteds.find((widget) => widget.get("moveing")))
return;
return [
// left top
vue.h("div", {
class: "size-border-corn nwse-resize",
style: {
left: conLTX.value + "px",
top: conLTY.value + "px"
},
onmousedown: (event) => onMouseDown(["l", "t"], event)
}, [
vue.h("div", {
class: "quart"
})
]),
// right top
vue.h("div", {
class: "size-border-corn nesw-resize",
style: {
left: conRTX.value + "px",
top: conLTY.value + "px"
},
onmousedown: (event) => onMouseDown(["r", "t"], event)
}, [
vue.h("div", {
class: "quart"
})
]),
// right bottom
vue.h("div", {
class: "size-border-corn nwse-resize",
style: {
left: conRTX.value + "px",
top: conRBX.value + "px"
},
onmousedown: (event) => onMouseDown(["r", "b"], event)
}, [
vue.h("div", {
class: "quart"
})
]),
// left bottom
vue.h("div", {
class: "size-border-corn nesw-resize",
style: {
left: conLTX.value + "px",
top: conRBX.value + "px"
},
onmousedown: (event) => onMouseDown(["l", "b"], event)
}, [
vue.h("div", {
class: "quart"
})
])
];
};
const renderCover = () => {
if (designService.modal.selecteds.length > 1) {
return vue.h("div", {
class: "size-cover",
style: {
left: position.value.x + "px",
top: position.value.y + "px",
width: position.value.width + "px",
height: position.value.height + "px",
background: service.modal.isOverlapping ? "rgba(255, 100, 100, 0.05)" : "",
zIndex: 1500
},
onmousedown: (event) => service.mousedownHandler(event, void 0)
});
}
};
return { renderBorders, drawConerBox, renderCover };
},
render() {
return vue.h("div", {}, [
...this.renderBorders() || "",
...this.drawConerBox() || "",
this.renderCover()
]);
}
});
const HEIGHR = 28;
const WIDTH = 35;
var OperationBar = vue.defineComponent({
setup() {
const designService = vue.inject(DesignService.token);
const position = vue.computed(() => {
const bxarray = designService.modal.selecteds.map((item) => item.get("x"));
const byarray = designService.modal.selecteds.map((item) => item.get("y"));
const begin = {
x: Math.min(...bxarray),
y: Math.min(...byarray)
};
const exarray = designService.modal.selecteds.map((item) => item.get("x") + item.get("width"));
const eyarray = designService.modal.selecteds.map((item) => item.get("y") + item.get("height"));
const end = {
x: Math.max(...exarray),
y: Math.max(...eyarray)
};
return {
x: begin.x,
y: begin.y,
width: end.x - begin.x,
height: end.y - begin.y
};
});
function deleteWidget() {
const delWidgets = [];
designService.modal.selecteds.forEach((yWidget) => {
delWidgets.push(yWidget.toJSON());
designService.deleteWidget(yWidget.get("id"));
});
designService.emit("deleted", delWidgets);
designService.modal.selecteds = [];
}
return {
position,
deleteWidget,
designService
};
},
render() {
return vue.h("div", {
class: "operation-bar",
style: {
top: this.position.y + 2 + "px",
left: this.position.x + this.position.width - WIDTH - 2 + "px",
width: WIDTH + "px",
height: HEIGHR + "px"
},
onmousedown: (event) => {
event.preventDefault();
event.stopPropagation();
}
}, [
vue.h("div", {
class: "del-icon",
innerHTML: `<svg class="icon" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor" aria-hidden="true" focusable="false"><path d="M695.96 953.65h-367.3c-76.07 0-137.74-61.67-137.74-137.74V293.63H833.7v522.28c0 76.07-61.67 137.74-137.74 137.74zM626.6 186.44H398.02c0-63.12 51.17-114.29 114.29-114.29S626.6 123.32 626.6 186.44z" data-spm-anchor-id="a313x.7781069.0.i29"></path><path d="M833.7 243.09H188.97c-19.02 0-34.43-15.42-34.43-34.43s15.41-34.43 34.43-34.43H833.7c19.02 0 34.43 15.42 34.43 34.43s-15.41 34.43-34.43 34.43z" data-spm-anchor-id="a313x.7781069.0.i30"></path><path d="M511.33 813.52c-19.02 0-34.43-15.42-34.43-34.43V466.61c0-19.02 15.42-34.43 34.43-34.43s34.43 15.42 34.43 34.43v312.48c.01 19.01-15.41 34.43-34.43 34.43zm-157.27 1.59c-19.02 0-34.43-15.42-34.43-34.43V586.03c0-19.02 15.42-34.43 34.43-34.43s34.43 15.42 34.43 34.43v194.64c.01 19.02-15.41 34.44-34.43 34.44zm314.54 0c-19.02 0-34.43-15.42-34.43-34.43V586.03c0-19.02 15.42-34.43 34.43-34.43s34.43 15.42 34.43 34.43v194.64c.01 19.02-15.41 34.44-34.43 34.44z" data-spm-anchor-id="a313x.7781069.0.i28"></path></svg>`,
onClick: () => this.deleteWidget()
})
]);
}
});
var DragContainer = vue.defineComponent({
name: "DragContainer",
props: {
value: {
type: Object,
required: true
},
widgetIdx: {
type: Number || void 0,
default: void 0
},
radius: {
type: Number,
default: 0
},
rotate: {
type: Number,
default: 0
}
},
emits: ["drag-start", "drag-moving", "drag-end", "state-changed"],
setup(props, { emit }) {
const itemSlot = vue.inject(DesignService.itemSlot);
const container = vue.ref();
vue.onMounted(() => {
if (container.value) {
container.value.addEventListener("transitionend", () => {
setTimeout(() => {
emit("state-changed", { id: props.value.id, state: 0 });
}, 200);
}, true);
}
});
const service = vue.inject(DesignService.token);
const draggingService = new DraggingService(service, emit);
const containerClass = vue.computed(() => {
if (props.value.state === 1) {
return "drag_container selected";
} else if (props.value.state === 3 || props.value.state === -1) {
return "drag_container dragging";
} else if (props.value.state === 4) {
return "drag_container moving";
} else {
return "drag_container";
}
});
const renderCover = () => {
if (props.value.state !== 2) {
return vue.h("div", {
id: props.value.id + "_cover",
class: {
cover: true,
moving: props.value.state === 3,
hover: props.value.enableDragable && props.value.enableResize
},
style: {
borderRadius: props.radius + "px",
background: (service.modal.selecteds.length === 1 || props.value.state === -1) && props.value.isOverlapping ? "rgba(255, 100, 100, 0.05)" : ""
},
onmousedown: (event) => {
event.preventDefault();
event.stopPropagation();
if (props.value.enableDragable) {
if (props.widgetIdx !== void 0) {
const yWidget = service.syncService.yWidget.get(props.widgetIdx);
yWidget && draggingService.mousedownHandler(event, yWidget);
}
emit("drag-start", event, props.value);
}
}
});
}
};
const cssTransform = () => {
let panelPoint = { x: props.value.x, y: props.value.y };
let transform = "translate(" + panelPoint.x + "px," + panelPoint.y + "px)";
if (props.value.state === -1) {
transform += ` scale(${service.modal.scale})`;
}
if (props.value.state === 3 || props.value.state === 4) {
transform += " scale(1)";
}
if (props.rotate !== 0) {
transform += " rotate(" + props.rotate + "deg)";
}
return transform;
};
return {
container,
containerClass,
renderCover,
cssTransform,
service,
itemSlot,
state: props.value.state,
tag: props.value.tag,
levels: props.value.levels
};
},
render() {
return vue.h(
// @ts-ignore
"div",
{
ref: "container",
class: this.containerClass,
style: {
transform: this.cssTransform(),
width: this.$props.value.width + "px",
height: this.$props.value.height + "px",
borderRadius: this.$props.radius + "px",
// 转成string,防止 levels为0 时 结果为false
zIndex: this.state === -1 ? 2e3 : String(this.levels) || 100
}
},
[
this.itemSlot && this.itemSlot(this.$props.value),
// this.$slots.default && this.$slots.default("hahah"),
this.renderCover()
// this.levels
// this.$props.value.id, h('div'),
// this.$props.value.x, '-', this.$props.value.y, h('div'),
// 'state ', this.$props.value.state, h('div'),
// 'index ', this.$props.widgetIdx
// this.$props.value.moveing+'', h('div'),
// this.$props.value.resizing+'', h('div'),
// this.$props.value.baseX, '-', this.$props.value.baseY, h('div'),
// this.$props.value.width, '-', this.$props.value.height
]
);
}
});
var DesignCanvase = vue.defineComponent({
name: "DesignCanvase",
props: {
square: {
type: Number,
default: -1
},
scale: {
type: Number,
default: 1
},
width: {
type: Number,
required: true
},
height: {
type: Number,
required: true
},
padding: {
type: Array,
default: () => [0]
},
drawerBackgroud: {
type: String,
default: "#ffffff"
},
backgroundColor: {
type: String,
default: "#eeeff0"
},
children: {
type: Array,
default: () => []
},
// 是否显示删除按钮
showDelBut: {
type: Boolean,
default: false
}
},
emits: ["drag-start", "drag-moving", "drag-end", "resize-start", "resizeing", "resize-end"],
setup(props, { emit, slots }) {
const drawer = vue.ref(null);
const designService = vue.inject(DesignService.token);
designService.drawerRef = drawer;
const containerWidth = computed(() => designService.modal.pageRect.cwidth * designService.modal.scale);
const containerHeight = computed(() => designService.modal.pageRect.cheight * designService.modal.scale);
vue.onMounted(() => {
window.addEventListener("resize", windowsResizeHandler, true);
windowsResizeHandler();
});
const windowsResizeHandler = () => {
relayoutChildren();
};
const relayoutChildren = () => {
};
const converClientP2PanelP = (event) => {
return {
x: (event.clientX - designService.modal.canvaseRect.x - designService.modal.pageRect.x + designService.modal.scrollLeft) / designService.modal.scale,
y: (event.clientY - designService.modal.canvaseRect.y - designService.modal.pageRect.y + designService.modal.scrollTop) / designService.modal.scale
};
};
const SELECTED_SPAN = 10;
const selectedArea = vue.ref({ begin: void 0, end: void 0 });
const bgmousedownHandler = (event) => {
if (event.button !== 0)
return;
designService.clearnSelected();
selectedArea.value.begin = converClientP2PanelP(event);
window.addEventListener("mousemove", bgmousemoveHandler, true);
window.addEventListener("mouseup", bgmouseupHandler, true);
};
const bgmousemoveHandler = (event) => {
const cpoint = converClientP2PanelP(event);
if (!selectedArea.value.begin) {
selectedArea.value.begin = cpoint;
} else {
const xspan = Math.abs(cpoint.x - selectedArea.value.begin.x);
const yspan = Math.abs(cpoint.y - selectedArea.value.begin.y);
if (xspan > SELECTED_SPAN && yspan > SELECTED_SPAN) {
selectedArea.value.end = cpoint;
designService.setSelectedArea(selectedArea.value.begin, selectedArea.value.end);
} else {
selectedArea.value.end = void 0;
designService.clearnSelected();
}
}
};
const bgmouseupHandler = (event) => {
window.removeEventListener("mousemove", bgmouseupHandler, true);
window.removeEventListener("mousemove", bgmousemoveHandler, true);
selectedArea.value = { begin: void 0, end: void 0 };
};
const renderSelectedArea = () => {
if (selectedArea.value.begin && selectedArea.value.end) {
const left = Math.min(selectedArea.value.begin.x, selectedArea.value.end.x);
const top = Math.min(selectedArea.value.begin.y, selectedArea.value.end.y);
const width = Math.abs(selectedArea.value.begin.x - selectedArea.value.end.x);
const height = Math.abs(selectedArea.value.begin.y - selectedArea.value.end.y);
return vue.h("div", {
class: "selected_area",
style: {
left: left + "px",
top: top + "px",
width: width + "px",
height: height + "px"
}
});
}
};
const renderDrawer = () => {
return vue.h("div", {
ref: "drawer",
id: "drawer",
style: {
background: props.drawerBackgroud,
left: designService.modal.pageRect.x + "px",
top: designService.modal.pageRect.y + "px",
width: designService.modal.pageRect.width + "px",
height: designService.modal.pageRect.height + "px"
}
}, [
renderWidgets(),
renderSizeBorders(),
renderSelectedArea(),
renderAlignmentLine(),
renderOperationBar()
]);
};
const renderWidgets = () => {
if (designService.modal.widgets) {
return designService.modal.widgets.map((widget, idx) => vue.h(DragContainer, {
key: widget.id,
value: widget,
widgetIdx: idx,
onDragMoving: (widget2) => emit("drag-moving", widget2),
onDragStart: (widget2) => emit("drag-start", widget2),
onDragEnd: (widget2) => emit("drag-end", widget2)
}));
}
};
const renderSizeBorders = () => {
if (!designService.modal.selecteds || designService.modal.selecteds.length === 0) {
return;
}
if (designService.modal.selecteds.find((widget) => !widget.get("enableResize"))) {
return;
}
return vue.h(SizeBox, {
onDragMoving: (widget) => emit("drag-moving", widget),
onDragStart: (widget) => emit("drag-start", widget),
onDragEnd: (widget) => emit("drag-end", widget),
onResizeStart: (widget) => emit("resize-start", widget),
onResizeing: (widget) => emit("resizeing", widget),
onResizeEnd: (widget) => emit("resize-end", widget)
});
};
const renderAlignmentLine = () => {
if (designService.boundaryLine.value) {
return designService.boundaryLine.value.map((line) => {
var _a;
const option = (_a = designService.alignLineService) === null || _a === void 0 ? void 0 : _a.option;
const border = `${option === null || option === void 0 ? void 0 : option.alignWeight}px ${option === null || option === void 0 ? void 0 : option.alignColor} dashed`;
return vue.h("div", {
id: line.id,
class: "alignment-line",
style: {
display: line.show && (option === null || option === void 0 ? void 0 : option.showAlign) ? "" : "none",
transform: `translate(${line.x}px, ${line.y}px)`,
borderLeft: line.direction === LineDirection.COLUMN ? border : "",
borderTop: line.direction === LineDirection.ROW ? border : "",
width: line.width,
height: line.height
}
}, [
// line.x, '-', line.y, '-', line.show
]);
});
}
return vue.h("div");
};
const renderOperationBar = () => {
if (!props.showDelBut)
return;
if (!designService.modal.selecteds || designService.modal.selecteds.length === 0)
return;
return vue.h(OperationBar);
};
return { drawer, designService, containerWidth, containerHeight, renderDrawer, bgmousedownHandler };
},
render() {
return vue.h("div", {
style: {
width: this.designService.modal.pageRect.cwidth + "px",
height: this.designService.modal.pageRect.cheight + "px",
transform: "scale(" + this.designService.modal.scale + ")"
},
onmousedown: (event) => this.bgmousedownHandler(event)
}, [
this.renderDrawer()
]);
}
});
function getClientRect(element) {
return new Promise((resolve, reject) => {
if (element) {
let number = 0;
let value = element.getBoundingClientRect();
const interval = setInterval(() => {
const cvalue = element.getBoundingClientRect();
if (cvalue.x === value.x && cvalue.y === value.y && cvalue.width === value.width && cvalue.height === value.height) {
number += 1;
if (number > 8) {
clearInterval(interval);
resolve(cvalue);
}
} else {
number = 0;
}
value = cvalue;
}, 40);
} else {
reject(new Error("element must not be null"));
}
});
}
var CRuler = vue.defineComponent({
name: "CRuler",
props: {
width: {
type: Number,
required: true
},
height: {
type: Number,
required: true
},
start: {
type: Number,
required: true
},
offset: {
type: Number,
required: true
},
scale: {
type: Number,
required: true
},
unit: {
type: String,
required: true
},
backgroundColor: {
type: String,
required: true
},
rulerColorLight: {
type: String,
default: "#848484"
},
rulerColorDark: {
type: String,
default: "#646464"
},
h: {
type: Boolean,
defaut: true
}
},
setup(props) {
const state = vue.reactive({
canvasContext: null
});
const canvas = vue.ref(null);
vue.onMounted(() => {
state.canvasContext = canvas.value && canvas.value.getContext("2d");
updateCanvasContext();
drawCavaseRuler();
});
vue.watch([() => props.width, () => props.height, () => props.start, () => props.scale, () => props.offset], () => {
updateCanvasContext();
drawCavaseRuler();
});
const updateCanvasContext = () => {
if (canvas.value) {
canvas.value.width = props.width;
canvas.value.height = props.height;
const ctx = state.canvasContext;
if (ctx) {
ctx.font = `${12}px -apple-system, "Helvetica Neue", ".SFNSText-Regular", "SF UI Text", Arial, "PingFang SC", "Hiragino Sans GB",
"Microsoft YaHei", "WenQuanYi Zen Hei", sans-serif`;
ctx.lineWidth = 1;
ctx.textBaseline = "middle";
}
}
};
const getGridSize = (scale) => {
if (scale <= 0.25)
return 40;
if (scale <= 0.5)
return 20;
if (scale <= 1)
return 10;
if (scale <= 2)
return 5;
if (scale <= 4)
return 2;
return 1;
};
const drawCavaseRuler = () => {
const ctx = state.canvasContext;
if (ctx && canvas.value) {
const { scale, h: h2, backgroundColor, rulerColorLight, rulerColorDark } = props;
ctx.clearRect(0, 0, canvas.value.width, canvas.value.height);
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, canvas.value.width, canvas.value.height);
const gridSize = getGridSize(scale);
const gridSize10 = gridSize * 10;
const gridPixel = gridSize * scale;
const gridPixel10 = gridSize10 * scale;
let position = props.start - props.offset;
const minPos = h2 ? props.height : props.width;
ctx.beginPath();
ctx.strokeStyle = rulerColorLight;
position = props.start - gridPixel - props.offset;
while (position > minPos) {
drawLine(ctx, h2, position, h2 ? props.height : props.width, false);
position -= gridPixel;
}
position = props.start + gridPixel - props.offset;
while (position < minPos) {
position += gridPixel;
}
while (h2 ? position < canvas.value.width : position < canvas.value.height) {
drawLine(ctx, h2, position, h2 ? canvas.value.height : canvas.value.width, false);
position += gridPixel;
}
ctx.stroke();
ctx.closePath();
ctx.beginPath();
ctx.fillStyle = rulerColorDark;
ctx.strokeStyle = rulerColorDark;
let value = 0;
position = props.start - props.offset;
while (position > minPos) {
position -= gridPixel10;
value -= gridSize10;
drawLine(ctx, h2, position, h2 ? props.height : props.width);
drawText(ctx, h2, position, h2 ? props.height : props.width, value);
}
position = props.start - props.offset;
value = 0;
while (position < minPos) {
position += gridPixel10;
value += gridSize10;
}
while (h2 ? position < canvas.value.width : position < canvas.value.height) {
drawLine(ctx, h2, position, h2 ? props.height : props.width);
drawText(ctx, h2, position, h2 ? props.height : props.width, value);
position += gridPixel10;
value += gridSize10;
}
ctx.stroke();
ctx.closePath();
ctx.setTransform(1, 0, 0, 1, 0, 0);
}
};
const drawLine = (ctx, h2, position, length, max = true) => {
const lineEnd = max ? length * 9 / 16 : length / 3;
if (h2) {
ctx.moveTo(position, 0);
ctx.lineTo(position, lineEnd);
} else {
ctx.moveTo(0, position);
ctx.lineTo(lineEnd, position);
}
};
const drawText = (ctx, h2, position, length, value) => {
const end = length * 0.4;
ctx.save();
h2 ? ctx.translate(position, end) : ctx.translate(end, position);
if (!h2) {
ctx.rotate(-Math.PI / 2);
}
ctx.fillText(value.toString(), 4, 7);
ctx.restore();
};
return { canvas };
},
render() {
return vue.h("canvas", {
ref: "canvas",
class: "ruler_container",
style: {
width: this.$props.width + "px",
height: this.$props.height + "px"
}
});
}
});
const RULER_WIDTH = 24;
var DesignPanel = vue.defineComponent({
name: "DesignPanel",
props: {
value: {
type: Array,
default: () => new Array()
},
width: {
type: Number,
required: true
},
height: {
type: Number,
required: true,
default: 500
},
unit: {
type: String,
default: "px"
},
drawerBackgroud: {
type: String,
default: "#ffffff"
},
backgroundColor: {
type: String,
default: "#eeeff0"
},
showRuler: {
type: Boolean,
default: false
},
rulerColorLight: {
type: String,
default: "#9a9a9a"
},
rulerColorDark: {
type: String,
default: "#646464"
},
// 是否显示对齐线
showAlign: {
type: Boolean,
default: true
},
// 是否吸附
enableAdsorb: {
type: Boolean,
default: true
},
// 对齐线宽度
alignWeight: {
type: Number,
default: 1
},
// 对齐线颜色
alignColor: {
type: String,
default: "#3ca4fc"
},
// 显示对齐线的距离
showAlignSpan: {
type: Number,
default: 20
},
// 吸附距离
adsorbSpan: {
type: Number,
default: 10
},
// 页面内边距
pagePadding: {
type: Array,
default: [10]
},
// 是否显示删除按钮
showDelBut: {
type: Boolean,
default: false
},
// 组件拖动到底部时是否拓展高度
autoHeight: {
type: Boolean,
default: false
}
},
emits: [
"update:value",
"addHeight",
"page-resized",
"added",
"deleted",
"drag-start",
"drag-moving",
"drag-end",
"resize-start",
"resizeing",
"resize-end",
"selected-change",
"addNewWidget",
"delNewWidget"
],
setup(props, { emit, slots }) {
const designContainer = vue.ref();
const designBody = vue.ref();
const service = new DesignService(props, emit, slots);
const createWidget = (widget) => {
service.createWidgetHandler(widget);
emit("added", widget);
emit("update:value", service.modal.widgets);
};
const getPageWidgets = () => {
return service.getPageWidgets();
};
vue.onMounted(() => {
window.addEventListener("resize", resizeHandler, true);
resizeHandler();
});
const resizeHandler = () => {
if (designContainer.value) {
getClientRect(designContainer.value).then((value) => {
service.modal.rect = value;
});
}
if (designBody.value) {
getClientRect(designBody.value).then((value) => {
service.modal.canvaseRect = value;
service.utils.recountPage();
vue.nextTick().then(() => {
service.emitter.emit("onLayout");
});
});
}
};
const renderAddWdiget = () => {
if (service.modal.newWidget) {
return vue.h(DragContainer, {
value: service.modal.newWidget
});
}
};
const renderHRulter = () => {
if (props.showRuler) {
return vue.h(CRuler, {
width: service.modal.canvaseRect.width,
height: RULER_WIDTH,
start: service.modal.pageRect.x * service.modal.scale,
offset: service.modal.scrollLeft,
scale: service.modal.scale,
unit: props.unit,
h: true,
backgroundColor: props.backgroundColor,
rulerColorLight: props.rulerColorLight,
rulerColorDark: props.rulerColorDark,
style: {
left: service.modal.canvaseRect.x - service.modal.rect.x + "px",
top: service.modal.canvaseRect.y - service.modal.rect.y + "px",
zIndex: 2e3
}
});
}
};
const renderVRulter = () => {
if (props.showRuler) {
return vue.h(CRuler, {
width: RULER_WIDTH,
height: service.modal.canvaseRect.height,
start: service.modal.pageRect.y * service.modal.scale,
offset: service.modal.scrollTop,
scale: service.modal.scale,
unit: props.unit,
backgroundColor: props.backgroundColor,
rulerColorLight: props.rulerColorLight,
rulerColorDark: props.rulerColorDark,
h: false,
style: {
left: service.modal.canvaseRect.x - service.modal.rect.x + "px",
top: service.modal.canvaseRect.y - service.modal.rect.y + "px",
zIndex: 2e3
}
});
}
};
const renderBody = () => {
return vue.h(DesignCanvase, Object.assign(Object.assign({ id: "free-layout-canvas", scale: service.modal.scale }, props), { onDragMoving: (widget) => {
emit("drag-moving", widget);
emit("update:value", service.modal.widgets);
}, onDragStart: (widget) => {
emit("drag-start", widget);
emit("update:value", service.modal.widgets);
}, onDragEnd: (widget) => {
emit("drag-end", widget);
emit("update:value", service.modal.widgets);
}, onResizeStart: (widget) => {
emit("resize-start", widget);
emit("update:value", service.modal.widgets);
}, onResizeing: (widget) => {
emit("resizeing", widget);
emit("update:value", service.modal.widgets);
}, onResizeEnd: (widget) => {
emit("resize-end", widget);
emit("update:value", service.modal.widgets);
} }));
};
const mouseWheelHandler = (event) => {
if (event.ctrlKey || event.metaKey) {
event.preventDefault();
event.stopPropagation();
service.addScale(event.deltaY / 100);
}
};
return { designContainer, designBody, service, createWidget, getPageWidgets, renderHRulter, renderVRulter, renderBody, renderAddWdiget, mouseWheelHandler };
},
render() {
return vue.h("div", {
ref: "designContainer",
class: "design"
}, [
this.$slots.header && this.$slots.header(),
vue.h("div", {
class: "content"
}, [
this.$slots.left && this.$slots.left(),
vue.h("div", {
ref: "designBody",
class: "body",
onscroll: () => {
if (this.designBody) {
this.service.modal.scrollLeft = this.designBody.scrollLeft;
this.service.modal.scrollTop = this.designBody.scrollTop;
}
},
onmousewheel: (event) => {
this.mouseWheelHandler(event);
}
}, [
this.renderHRulter(),
this.renderVRulter(),
this.renderBody()
]),
this.$slots.right && this.$slots.right()
]),
this.renderAddWdiget()
]);
}
});
DesignPanel.install = (app) => {
app.component(DesignPanel.name, DesignPanel);
return app;
};
class GridService {
constructor(service, props, emit) {
this.service = service;
this.props = props;
this.emit = emit;
this.containerRef = null;
this.gridChildWidgets = vue.computed(() => {
const widgets = [];
this.model.gridItems.forEach((item) => {
item.widget && widgets.push(item.widget);
});
return widgets;
});
this.gridWidget = this.getGridWidget();
this.model = vue.reactive({
gridRowGap: [],
gridColGap: [],
gridItems: [],
height: this.gridWidget.height
});
}
getGridWidget() {
let gridWidget = this.service.modal.widgets.find((item) => item.id === this.props.id);
if (!gridWidget) {
gridWidget = this.service.modal.newWidget;
}
return gridWidget;
}
initGridGap(rowSpan, colSpan, width, height) {
rowSpan = Number(rowSpan);
colSpan = Number(colSpan);
const gridWidgets = this.model.gridItems.map((item) => ({ widget: item.widget, row: item.row, col: item.col }));
this.model.gridRowGap = [];
this.model.gridColGap = [];
this.model.gridItems = [];
this.model.height = height;
for (let ri = 0; ri < rowSpan - 1; ri++) {
this.model.gridRowGap.push({
y: height / rowSpan * (ri + 1) - 1,
width
});
}
for (let ci = 0; ci < colSpan - 1; ci++) {
this.model.gridColGap.push({
x: width / colSpan * (ci + 1) - 1,
height
});
}
for (let ri = 0; ri < rowSpan; ri++) {
for (let ci = 0; ci < colSpan; ci++) {
const gridWidget = gridWidgets.find((gridWidget2) => gridWidget2.row === ri && gridWidget2.col === ci);
this.model.gridItems.push({
x: width / colSpan * ci,
y: height / rowSpan * ri,
width: width / colSpan,
height: height / rowSpan,
row: ri,
col: ci,
active: false,
widget: (gridWidget === null || gridWidget === void 0 ? void 0 : gridWidget.widget) || null
});
}
}
}
resetGridWidgets() {
this.model.gridItems.forEach(({ widget, x, y, width }) => {
if (!widget)
return;
widget.x = x;
widget.y = y;
widget.width = width;
});
}
initGridWidgets() {
if (!this.props.components.length)
return;
for (const item of this.props.components) {
const cWidget = Object.assign(Object.assign({}, item), { state: 0, moveing: false, resizing: false, isOverlapping: false, baseX: item.x, baseY: item.y });
const center = this.getCenterPoint(cWidget);
for (const gridItem of this.model.gridItems) {
if (this.pointInArea(center, gridItem)) {
gridItem.widget = cWidget;
break;
}
}
}
}
setGridRowGap(row, height) {
let addHeight = 0;
const items = this.model.gridItems.filter((item) => item.row === row);
items.forEach((item) => {
const span = height - item.height;
if (span > addHeight) {
addHeight = span;
}
item.height = height;
});
for (let i = row + 1; i <= this.model.gridRowGap.length; i++) {
const items2 = this.model.gridItems.filter((item) => item.row === i);
items2.forEach((item) => {
if (item.widget) {
item.widget.y += addHeight;
}
item.y += addHeight;
});
}
this.model.gridRowGap.forEach((item, index) => {
if (index >= row) {
item.y += addHeight;
}
});
this.model.gridColGap.forEach((item) => {
item.height += addHeight;
});
this.model.height += addHeight;
}
// 监听组件放下(已排除重叠)
onAddWidget(yWidget) {
const widget = yWidget.toJSON();
if (widget.id === this.props.id)
return;
const exclude = this.props.excludeComponents.includes(widget.tag);
if (exclude && widget.enableOverlap)
return;
const centerPoint = this.getCenterPoint(widget);
if (this.pointInArea(centerPoint, this.gridWidget)) {
this.model.gridItems.forEach((gridItem) => {
var _a;
const point = this.canvasP2GridP(centerPoint);
gridItem.active = false;
if (this.pointInArea(point, gridItem)) {
this.setWidgetPointToGrid(yWidget, gridItem);
} else {
if (((_a = gridItem.widget) === null || _a === void 0 ? void 0 : _a.id) === yWidget.get("id")) {
gridItem.widget = null;
}
}
});
} else {
this.model.gridItems.forEach((gridItem) => {
var _a;
if (((_a = gridItem.widget) === null || _a === void 0 ? void 0 : _a.id) === yWidget.get("id")) {
gridItem.widget = null;
}
});
}
}
// 监听子组件被拖拽,将网格子组件设置为全局并选中
onGridChildDragStart(event, widget) {
var _a;
const { x, y } = this.gridP2CanvasP(widget);
const yWidget = this.service.syncService.createWidget(Object.assign(Object.assign({}, widget), { state: 3, x, y }));
this.service.syncService.yWidget.push([yWidget]);
for (const gridItem of this.model.gridItems) {
if (((_a = gridItem.widget) === null || _a === void 0 ? void 0 : _a.id) === widget.id) {
gridItem.widget = null;
break;
}
}
const draggingService = new DraggingService(this.service, () => {
});
draggingService.mousedownHandler(event, yWidget);
this.emit("update:active", null);
}
// 将拖入网格的组件设置到对应网格位置
setWidgetPointToGrid(yWidget, gridItem) {
const widget = Object.assign(Object.assign({}, yWidget.toJSON()), { x: gridItem.x, y: gridItem.y, width: gridItem.width, height: gridItem.height, state: 0 });
this.service.deleteWidget(yWidget.get("id"));
this.service.modal.selecteds = [];
gridItem.widget = widget;
this.emit("update:active", widget);
const { yWidget: gridYWidget } = this.service.utils.getYWidgetById(this.props.id);
this.service.setSelected([gridYWidget]);
}
// 设置拖动的组件是否在网格中覆盖了其他组件 覆盖false 不覆盖true
setNotOverlapInGrid(widgets) {
if (widgets.length !== 1) {
return !!widgets.find((w) => w.id === this.props.id);
}
const widget = widgets[0];
if (widget.id === this.props.id)
return true;
const exclude = this.props.excludeComponents.includes(widget.tag);
if (exclude && widget.enableOverlap)
return true;
const centerPoint = this.getCenterPoint(widget);
if (this.pointInArea(centerPoint, this.gridWidget)) {
if (exclude)
return false;
return this.model.gridItems.every((gridItems, index) => {
const point = this.canvasP2GridP(centerPoint);
if (this.pointInArea(point, gridItems)) {
if (gridItems.widget && gridItems.widget.id === widget.id) {
gridItems.active = false;
return true;
} else {
gridItems.active = !gridItems.widget;
return !gridItems.widget;
}
} else {
gridItems.active = false;
return true;
}
});
} else {
this.model.gridItems.forEach((item) => {
item.active = false;
});
return !(widget.x + widget.width > this.gridWidget.x && this.gridWidget.x + this.gridWidget.width > widget.x && widget.y + widget.height > this.gridWidget.y && this.gridWidget.y + this.gridWidget.height > widget.y);
}
}
deleteWidget(id) {
for (const item of this.model.gridItems) {
if (item.widget && item.widget.id === id) {
item.widget = null;
break;
}
}
if (this.props.active && this.props.active.id === id) {
this.emit("update:active", null);
}
}
pointInArea(point, area) {
return point.x > area.x && point.x < area.x + area.width && point.y > area.y && point.y < area.y + area.height;
}
canvasP2GridP(point) {
return {
x: point.x - this.gridWidget.x,
y: point.y - this.gridWidget.y
};
}
gridP2CanvasP(point) {
return {
x: point.x + this.gridWidget.x,
y: point.y + this.gridWidget.y
};
}
getCenterPoint(widget) {
return {
x: widget.x + widget.width / 2,
y: widget.y + widget.height / 2
};
}
}
var grid_widget = vue.defineComponent({
name: "GridWidget",
components: { DragContainer },
props: {
id: {
type: String,
required: true
},
tag: {
type: String,
required: true
},
active: {
type: Object || null
},
rowSpan: {
type: String,
default: "5"
},
colSpan: {
type: String,
default: "3"
},
// 不允许拖入的组件
excludeComponents: {
type: Array,
default: () => []
},
gridBorder: {
type: Array,
default: () => [
"INNER_HORIZONTAL",
"INNER_VERTICAL",
"UP",
"DOWN",
"LEFT",
"RIGHT"
]
},
components: {
type: Array,
default: () => []
}
},
emits: ["update:active"],
setup(props, { emit }) {
const service = vue.inject(DesignService.token);
const gridService = new GridService(service, props, emit);
gridService.initGridGap(Number(props.rowSpan), Number(props.colSpan), gridService.gridWidget.width, gridService.gridWidget.height);
gridService.initGridWidgets();
vue.watch([
() => props.rowSpan,
() => props.colSpan
], ([rowSpan, colSpan]) => {
gridService.initGridGap(rowSpan, colSpan, gridService.gridWidget.width, gridService.gridWidget.height);
gridService.resetGridWidgets();
});
vue.watch([
() => gridService.gridWidget.width,
() => gridService.gridWidget.height
], ([width, height]) => {
gridService.initGridGap(props.rowSpan, props.colSpan, width, height);
gridService.resetGridWidgets();
});
const setNotOverlapInGrid = gridService.setNotOverlapInGrid.bind(gridService);
const onAddWidget = gridService.onAddWidget.bind(gridService);
service.emitter.on("onAddWidget", onAddWidget);
service.isNotOverlapInGridPublisher.on(setNotOverlapInGrid);
vue.onBeforeUnmount(() => {
service.emitter.off("onAddWidget", onAddWidget);
service.isNotOverlapInGridPublisher.off(setNotOverlapInGrid);
});
function renderGridRowGap() {
return gridService.model.gridRowGap.map((item) => {
var _a;
return vue.h("div", {
style: {
position: "absolute",
top: item.y + "px",
left: 0,
height: 0,
width: item.width - 1 + "px",
borderBottom: ((_a = props.gridBorder) === null || _a === void 0 ? void 0 : _a.includes("INNER_HORIZONTAL")) ? "1px dashed rgb(201, 201, 201)" : ""
}
});
});
}
function renderGridColGap() {
return gridService.model.gridColGap.map((item) => {
var _a;
return vue.h("div", {
style: {
position: "absolute",
left: item.x + "px",
top: 0,
height: item.height + "px",
width: 0,
borderLeft: ((_a = props.gridBorder) === null || _a === void 0 ? void 0 : _a.includes("INNER_VERTICAL")) ? "1px dashed rgb(201, 201, 201)" : ""
}
});
});
}
function renderGridItems() {
return gridService.model.gridItems.map((item, index) => {
var _a, _b;
return vue.h("div", {
style: {
position: "absolute",
left: item.x + "px",
top: item.y + "px",
height: item.height + "px",
width: item.width + "px",
// zIndex: 50,
background: props.active && ((_a = props.active) === null || _a === void 0 ? void 0 : _a.id) === ((_b = item.widget) === null || _b === void 0 ? void 0 : _b.id) || item.active ? "#ecf4fc" : ""
}
}, []);
});
}
function renderGridWidgets() {
return gridService.gridChildWidgets.value.map((widget) => (
// @ts-ignore
vue.h(DragContainer, {
key: widget.id,
value: widget,
style: {
zIndex: 1600
},
onDragStart: gridService.onGridChildDragStart.bind(gridService)
})
));
}
return {
gridService,
renderGridRowGap,
renderGridColGap,
renderGridItems,
renderGridWidgets,
deleteWidget: gridService.deleteWidget.bind(gridService),
getComponents: () => gridService.gridChildWidgets.value,
getGridItems: () => gridService.model.gridItems,
setGridRowGap: gridService.setGridRowGap.bind(gridService),
getBoundingClientRect: () => {
var _a;
return (_a = gridService.containerRef) === null || _a === void 0 ? void 0 : _a.getBoundingClientRect();
}
};
},
render() {
var _a, _b, _c, _d;
return vue.h("div", {
ref: (el) => this.gridService.containerRef = el,
style: {
position: "relative",
width: "100%",
height: this.gridService.model.height + "px",
borderLeft: ((_a = this.$props.gridBorder) === null || _a === void 0 ? void 0 : _a.includes("LEFT")) ? "1px solid #9a9a9a" : "",
borderRight: ((_b = this.$props.gridBorder) === null || _b === void 0 ? void 0 : _b.includes("RIGHT")) ? "1px solid #9a9a9a" : "",
borderTop: ((_c = this.$props.gridBorder) === null || _c === void 0 ? void 0 : _c.includes("UP")) ? "1px solid #9a9a9a" : "",
borderBottom: ((_d = this.$props.gridBorder) === null || _d === void 0 ? void 0 : _d.includes("DOWN")) ? "1px solid #9a9a9a" : ""
}
}, [
this.renderGridItems(),
this.renderGridRowGap(),
this.renderGridColGap(),
this.renderGridWidgets()
// h('div',{},[JSON.stringify(this.gridWidget.components)])
]);
}
});
const Browser = {
// Firefox 1.0+
isFirefox: () => {
return typeof InstallTrigger !== "undefined";
},
// Internet Explorer 6-11
isIE: () => {
return navigator.userAgent.indexOf("MSIE") !== -1 || !!document.documentMode;
},
// Edge 20+
isEdge: () => {
return !Browser.isIE() && !!window.StyleMedia;
},
// Chrome 1+
isChrome: (context = window) => {
return !!context.chrome;
},
// At least Safari 3+: "[object HTMLElementConstructor]"
isSafari: () => {
return Object.prototype.toString.call(window.HTMLElement).indexOf("Constructor") > 0 || navigator.userAgent.toLowerCase().indexOf("safari") !== -1;
},
// IOS Chrome
isIOSChrome: () => {
return navigator.userAgent.toLowerCase().indexOf("crios") !== -1;
}
};
function printHTML({ domId, width, height, documentTitle, styleStr, cssLinks }) {
let page = document.getElementById(domId);
if (!page) {
console.error("canvas is " + page);
return;
}
page = cloneElement(page);
page.style.left = "0";
page.style.top = "0";
page.style.overflow = "hidden";
const iframe = document.createElement("iframe");
iframe.setAttribute("id", "free-layout-iframe");
iframe.style.width = width + "px";
iframe.style.height = height + "px";
iframe.style.border = "none";
iframe.srcdoc = "<html><head><title>" + (documentTitle || "") + "</title>";
if (cssLinks) {
cssLinks.forEach((file) => {
iframe.srcdoc += '<link rel="stylesheet" href="' + file + '">';
});
}
iframe.srcdoc += "</head><body></body></html>";
document.body.appendChild(iframe);
iframe.onload = () => {
iframe.contentWindow.document.body.appendChild(page);
const style = document.createElement("style");
style.innerText = `
html,body {
padding: 0;
margin:0;
}
#drawer{
position: relative;
overflow: hidden;
}
.drag_container{
position: absolute;
}
` + (styleStr || "");
iframe.contentWindow.document.body.appendChild(style);
performPrint(iframe);
};
}
function performPrint(iframeElement) {
try {
iframeElement.focus();
if (Browser.isEdge() || Browser.isIE()) {
try {
iframeElement.contentWindow.document.execCommand("print", false, null);
} catch (e) {
iframeElement.contentWindow.print();
}
} else {
iframeElement.contentWindow.print();
}
} catch (error) {
console.error(error);
} finally {
if (Browser.isFirefox()) {
iframeElement.style.visibility = "hidden";
iframeElement.style.left = "-1px";
}
cleanUp();
}
}
function cleanUp() {
let event = "mouseover";
if (Browser.isChrome() || Browser.isFirefox()) {
event = "focus";
}
const handler = () => {
window.removeEventListener(event, handler);
const iframe = document.getElementById("free-layout-iframe");
if (iframe) {
iframe.remove();
}
};
window.addEventListener(event, handler);
}
function cloneElement(element) {
const clone = element.cloneNode();
const childNodesArray = Array.prototype.slice.call(element.childNodes);
for (let i = 0; i < childNodesArray.length; i++) {
const clonedChild = cloneElement(childNodesArray[i]);
clone.appendChild(clonedChild);
}
switch (element.tagName) {
case "SELECT":
clone.value = element.value;
break;
case "CANVAS":
clone.getContext("2d").drawImage(element, 0, 0);
break;
}
return clone;
}
function useDesignPanel(designPanel) {
const designService = designPanel ? void 0 : vue.inject(DesignService.token);
if (!designPanel && !designService)
console.error("designPanel \u4E0D\u5B58\u5728");
const service = vue.computed(() => {
var _a;
if (designPanel === null || designPanel === void 0 ? void 0 : designPanel.value) {
return (_a = designPanel === null || designPanel === void 0 ? void 0 : designPanel.value) === null || _a === void 0 ? void 0 : _a.service;
} else {
return designService;
}
});
const designModal = vue.computed(() => {
var _a;
return (_a = service.value) === null || _a === void 0 ? void 0 : _a.modal;
});
function createWidget(widget) {
var _a;
(_a = service.value) === null || _a === void 0 ? void 0 : _a.createWidgetHandler(widget);
}
function getPageWidgets() {
var _a;
return (_a = service.value) === null || _a === void 0 ? void 0 : _a.getPageWidgets();
}
function getYWidgets() {
var _a;
return (_a = service.value) === null || _a === void 0 ? void 0 : _a.syncService.yWidget;
}
function updateWidget(widget) {
if (!service.value)
return;
const { yWidget } = service.value.utils.getYWidgetById(widget.id);
if (yWidget) {
for (const key in widget) {
yWidget.set(key, widget[key]);
}
} else {
console.error("\u8BBE\u7F6E\u7684widget\u4E0D\u5B58\u5728" + widget.id);
}
}
function delWidget(id) {
if (!service.value)
return;
service.value.deleteSelected(id);
service.value.deleteWidget(id);
}
function getPageRect() {
var _a;
return (_a = designModal.value) === null || _a === void 0 ? void 0 : _a.pageRect;
}
function leftJustify() {
var _a;
(_a = service.value) === null || _a === void 0 ? void 0 : _a.alignLineService.leftJustify();
}
function rightJustify() {
var _a;
(_a = service.value) === null || _a === void 0 ? void 0 : _a.alignLineService.rightJustify();
}
function topJustify() {
var _a;
(_a = service.value) === null || _a === void 0 ? void 0 : _a.alignLineService.topJustify();
}
function bottomJustify() {
var _a;
(_a = service.value) === null || _a === void 0 ? void 0 : _a.alignLineService.bottomJustify();
}
function rowBetween() {
var _a;
(_a = service.value) === null || _a === void 0 ? void 0 : _a.alignLineService.rowBetween();
}
function columnBetween() {
var _a;
(_a = service.value) === null || _a === void 0 ? void 0 : _a.alignLineService.columnBetween();
}
function changePageSize(width, height, padding) {
var _a;
const pageRect = designModal.value.pageRect;
(_a = service.value) === null || _a === void 0 ? void 0 : _a.changePageSize(width || pageRect.width, height || pageRect.height, padding || pageRect.padding);
}
function print(option) {
var _a;
(_a = service.value) === null || _a === void 0 ? void 0 : _a.setSelected([]);
vue.nextTick().then(() => {
printHTML(Object.assign({ domId: "drawer", width: designModal.value.pageRect.width, height: designModal.value.pageRect.height }, option));
});
}
function onLayout(callback) {
var _a;
(_a = service.value) === null || _a === void 0 ? void 0 : _a.emitter.on("onLayout", callback);
}
vue.onBeforeUnmount(() => {
var _a;
(_a = service.value) === null || _a === void 0 ? void 0 : _a.emitter.off("onLayout");
});
return {
designModal,
getPageRect,
onLayout,
createWidget,
getPageWidgets,
getYWidgets,
updateWidget,
delWidget,
leftJustify,
rightJustify,
topJustify,
bottomJustify,
rowBetween,
columnBetween,
print,
changePageSize
};
}
exports.DesignPanel = DesignPanel;
exports.GridWidget = grid_widget;
exports.useDesignPanel = useDesignPanel;
Object.defineProperty(exports, '__esModule', { value: true });
}));