mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-04 11:09:01 +08:00
@@ -0,0 +1,66 @@
|
||||
import { generateKeyBetween } from 'fractional-indexing';
|
||||
|
||||
function hasSamePrefix(a: string, b: string) {
|
||||
return a.startsWith(b) || b.startsWith(a);
|
||||
}
|
||||
/**
|
||||
* generate a key between a and b, the result key is always satisfied with a < result < b.
|
||||
* the key always has a random suffix, so there is no need to worry about collision.
|
||||
*
|
||||
* make sure a and b are generated by this function.
|
||||
*
|
||||
* @param customPostfix custom postfix for the key, only letters and numbers are allowed
|
||||
*/
|
||||
export function generateKeyBetweenV2(a: string | null, b: string | null) {
|
||||
const randomSize = 32;
|
||||
function postfix(length: number = randomSize) {
|
||||
const chars =
|
||||
'123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
||||
const values = new Uint8Array(length);
|
||||
crypto.getRandomValues(values);
|
||||
let result = '';
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += chars.charAt(values[i] % chars.length);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
if (a !== null && b !== null && a >= b) {
|
||||
throw new Error('a should be smaller than b');
|
||||
}
|
||||
// get the subkey in full key
|
||||
// e.g.
|
||||
// a0xxxx -> a
|
||||
// a0x0xxxx -> a0x
|
||||
function subkey(key: string | null) {
|
||||
if (key === null) {
|
||||
return null;
|
||||
}
|
||||
if (key.length <= randomSize + 1) {
|
||||
// no subkey
|
||||
return key;
|
||||
}
|
||||
const splitAt = key.substring(0, key.length - randomSize - 1);
|
||||
return splitAt;
|
||||
}
|
||||
const aSubkey = subkey(a);
|
||||
const bSubkey = subkey(b);
|
||||
if (aSubkey === null && bSubkey === null) {
|
||||
// generate a new key
|
||||
return generateKeyBetween(null, null) + '0' + postfix();
|
||||
} else if (aSubkey === null && bSubkey !== null) {
|
||||
// generate a key before b
|
||||
return generateKeyBetween(null, bSubkey) + '0' + postfix();
|
||||
} else if (bSubkey === null && aSubkey !== null) {
|
||||
// generate a key after a
|
||||
return generateKeyBetween(aSubkey, null) + '0' + postfix();
|
||||
} else if (aSubkey !== null && bSubkey !== null) {
|
||||
// generate a key between a and b
|
||||
if (hasSamePrefix(aSubkey, bSubkey) && a !== null && b !== null) {
|
||||
// conflict, if the subkeys are the same, generate a key between fullkeys
|
||||
return generateKeyBetween(a, b) + '0' + postfix();
|
||||
} else {
|
||||
return generateKeyBetween(aSubkey, bSubkey) + '0' + postfix();
|
||||
}
|
||||
}
|
||||
throw new Error('Never reach here');
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import type { Store } from '@blocksuite/store';
|
||||
import { effect } from '@preact/signals-core';
|
||||
|
||||
import { SurfaceBlockModel } from '../gfx/model/surface/surface-model.js';
|
||||
|
||||
export function onSurfaceAdded(
|
||||
doc: Store,
|
||||
callback: (model: SurfaceBlockModel | null) => void
|
||||
) {
|
||||
let found = false;
|
||||
let foundId = '';
|
||||
|
||||
const dispose = effect(() => {
|
||||
// if the surface is already found, no need to search again
|
||||
if (found && doc.getBlock(foundId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const block of Object.values(doc.blocks.value)) {
|
||||
if (block.model instanceof SurfaceBlockModel) {
|
||||
callback(block.model);
|
||||
found = true;
|
||||
foundId = block.id;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
callback(null);
|
||||
});
|
||||
|
||||
return dispose;
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import type { Store } from '@blocksuite/store';
|
||||
|
||||
import type { GfxLocalElementModel } from '../gfx/index.js';
|
||||
import type { Layer } from '../gfx/layer.js';
|
||||
import {
|
||||
type GfxGroupCompatibleInterface,
|
||||
isGfxGroupCompatibleModel,
|
||||
} from '../gfx/model/base.js';
|
||||
import type { GfxBlockElementModel } from '../gfx/model/gfx-block-model.js';
|
||||
import type { GfxModel } from '../gfx/model/model.js';
|
||||
import type { SurfaceBlockModel } from '../gfx/model/surface/surface-model.js';
|
||||
|
||||
export function getLayerEndZIndex(layers: Layer[], layerIndex: number) {
|
||||
const layer = layers[layerIndex];
|
||||
return layer
|
||||
? layer.type === 'block'
|
||||
? layer.zIndex + layer.elements.length - 1
|
||||
: layer.zIndex
|
||||
: 0;
|
||||
}
|
||||
|
||||
export function updateLayersZIndex(layers: Layer[], startIdx: number) {
|
||||
const startLayer = layers[startIdx];
|
||||
let curIndex = startLayer.zIndex;
|
||||
|
||||
for (let i = startIdx; i < layers.length; ++i) {
|
||||
const curLayer = layers[i];
|
||||
|
||||
curLayer.zIndex = curIndex;
|
||||
curIndex += curLayer.type === 'block' ? curLayer.elements.length : 1;
|
||||
}
|
||||
}
|
||||
|
||||
export function getElementIndex(indexable: GfxModel) {
|
||||
const groups = indexable.groups as GfxGroupCompatibleInterface[];
|
||||
|
||||
if (groups.length) {
|
||||
const groupIndexes = groups
|
||||
.map(group => group.index)
|
||||
.reverse()
|
||||
.join('-');
|
||||
|
||||
return `${groupIndexes}-${indexable.index}`;
|
||||
}
|
||||
|
||||
return indexable.index;
|
||||
}
|
||||
|
||||
export function ungroupIndex(index: string) {
|
||||
return index.split('-')[0];
|
||||
}
|
||||
|
||||
export function insertToOrderedArray(array: GfxModel[], element: GfxModel) {
|
||||
let idx = 0;
|
||||
while (
|
||||
idx < array.length &&
|
||||
[SortOrder.BEFORE, SortOrder.SAME].includes(compare(array[idx], element))
|
||||
) {
|
||||
++idx;
|
||||
}
|
||||
|
||||
array.splice(idx, 0, element);
|
||||
}
|
||||
|
||||
export function removeFromOrderedArray(array: GfxModel[], element: GfxModel) {
|
||||
const idx = array.indexOf(element);
|
||||
|
||||
if (idx !== -1) {
|
||||
array.splice(idx, 1);
|
||||
}
|
||||
}
|
||||
|
||||
export enum SortOrder {
|
||||
AFTER = 1,
|
||||
BEFORE = -1,
|
||||
SAME = 0,
|
||||
}
|
||||
|
||||
export function isInRange(edges: [GfxModel, GfxModel], target: GfxModel) {
|
||||
return compare(target, edges[0]) >= 0 && compare(target, edges[1]) < 0;
|
||||
}
|
||||
|
||||
export function renderableInEdgeless(
|
||||
doc: Store,
|
||||
surface: SurfaceBlockModel,
|
||||
block: GfxBlockElementModel
|
||||
) {
|
||||
const parent = doc.getParent(block);
|
||||
|
||||
return parent === doc.root || parent === surface;
|
||||
}
|
||||
|
||||
/**
|
||||
* A comparator function for sorting elements in the surface.
|
||||
* SortOrder.AFTER means a should be rendered after b and so on.
|
||||
* @returns
|
||||
*/
|
||||
export function compare(
|
||||
a: GfxModel | GfxLocalElementModel,
|
||||
b: GfxModel | GfxLocalElementModel
|
||||
) {
|
||||
if (isGfxGroupCompatibleModel(a) && b.groups.includes(a)) {
|
||||
return SortOrder.BEFORE;
|
||||
} else if (isGfxGroupCompatibleModel(b) && a.groups.includes(b)) {
|
||||
return SortOrder.AFTER;
|
||||
} else {
|
||||
const aGroups = a.groups as GfxGroupCompatibleInterface[];
|
||||
const bGroups = b.groups as GfxGroupCompatibleInterface[];
|
||||
|
||||
let i = 1;
|
||||
let aGroup:
|
||||
| GfxModel
|
||||
| GfxGroupCompatibleInterface
|
||||
| GfxLocalElementModel
|
||||
| undefined = aGroups.at(-i);
|
||||
let bGroup:
|
||||
| GfxModel
|
||||
| GfxGroupCompatibleInterface
|
||||
| GfxLocalElementModel
|
||||
| undefined = bGroups.at(-i);
|
||||
|
||||
while (aGroup === bGroup && aGroup) {
|
||||
++i;
|
||||
aGroup = aGroups.at(-i);
|
||||
bGroup = bGroups.at(-i);
|
||||
}
|
||||
|
||||
aGroup = aGroup ?? a;
|
||||
bGroup = bGroup ?? b;
|
||||
|
||||
return aGroup.index === bGroup.index
|
||||
? SortOrder.SAME
|
||||
: aGroup.index < bGroup.index
|
||||
? SortOrder.BEFORE
|
||||
: SortOrder.AFTER;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import type { Store } from '@blocksuite/store';
|
||||
|
||||
import {
|
||||
type GfxCompatibleInterface,
|
||||
type GfxGroupCompatibleInterface,
|
||||
isGfxGroupCompatibleModel,
|
||||
} from '../gfx/model/base.js';
|
||||
import type { GfxGroupModel, GfxModel } from '../gfx/model/model.js';
|
||||
|
||||
/**
|
||||
* Get the top elements from the list of elements, which are in some tree structures.
|
||||
*
|
||||
* For example: a list `[G1, E1, G2, E2, E3, E4, G4, E5, E6]`,
|
||||
* and they are in the elements tree like:
|
||||
* ```
|
||||
* G1 G4 E6
|
||||
* / \ |
|
||||
* E1 G2 E5
|
||||
* / \
|
||||
* E2 G3*
|
||||
* / \
|
||||
* E3 E4
|
||||
* ```
|
||||
* where the star symbol `*` denote it is not in the list.
|
||||
*
|
||||
* The result should be `[G1, G4, E6]`
|
||||
*/
|
||||
export function getTopElements(elements: GfxModel[]): GfxModel[] {
|
||||
const results = new Set(elements);
|
||||
|
||||
elements = [...new Set(elements)];
|
||||
|
||||
elements.forEach(e1 => {
|
||||
elements.forEach(e2 => {
|
||||
if (isGfxGroupCompatibleModel(e1) && e1.hasDescendant(e2)) {
|
||||
results.delete(e2);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
return [...results];
|
||||
}
|
||||
|
||||
function traverse(
|
||||
element: GfxModel,
|
||||
preCallback?: (element: GfxModel) => void | boolean,
|
||||
postCallBack?: (element: GfxModel) => void
|
||||
) {
|
||||
// avoid infinite loop caused by circular reference
|
||||
const visited = new Set<GfxModel>();
|
||||
|
||||
const innerTraverse = (element: GfxModel) => {
|
||||
if (visited.has(element)) return;
|
||||
visited.add(element);
|
||||
|
||||
if (preCallback) {
|
||||
const interrupt = preCallback(element);
|
||||
if (interrupt) return;
|
||||
}
|
||||
|
||||
if (isGfxGroupCompatibleModel(element)) {
|
||||
element.childElements.forEach(child => {
|
||||
innerTraverse(child);
|
||||
});
|
||||
}
|
||||
|
||||
postCallBack && postCallBack(element);
|
||||
};
|
||||
|
||||
innerTraverse(element);
|
||||
}
|
||||
|
||||
export function descendantElementsImpl(
|
||||
container: GfxGroupCompatibleInterface
|
||||
): GfxModel[] {
|
||||
const results: GfxModel[] = [];
|
||||
container.childElements.forEach(child => {
|
||||
traverse(child, element => {
|
||||
results.push(element);
|
||||
});
|
||||
});
|
||||
return results;
|
||||
}
|
||||
|
||||
export function hasDescendantElementImpl(
|
||||
container: GfxGroupCompatibleInterface,
|
||||
element: GfxCompatibleInterface
|
||||
): boolean {
|
||||
let _container = element.group;
|
||||
while (_container) {
|
||||
if (_container === container) return true;
|
||||
_container = _container.group;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* This checker is used to prevent circular reference, when adding a child element to a container.
|
||||
*/
|
||||
export function canSafeAddToContainer(
|
||||
container: GfxGroupModel,
|
||||
element: GfxCompatibleInterface
|
||||
) {
|
||||
if (
|
||||
element === container ||
|
||||
(isGfxGroupCompatibleModel(element) && element.hasDescendant(container))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function isLockedByAncestorImpl(
|
||||
element: GfxCompatibleInterface
|
||||
): boolean {
|
||||
return element.groups.some(isLockedBySelfImpl);
|
||||
}
|
||||
|
||||
export function isLockedBySelfImpl(element: GfxCompatibleInterface): boolean {
|
||||
return element.lockedBySelf ?? false;
|
||||
}
|
||||
|
||||
export function isLockedImpl(element: GfxCompatibleInterface): boolean {
|
||||
return isLockedBySelfImpl(element) || isLockedByAncestorImpl(element);
|
||||
}
|
||||
|
||||
export function lockElementImpl(doc: Store, element: GfxCompatibleInterface) {
|
||||
doc.transact(() => {
|
||||
element.lockedBySelf = true;
|
||||
});
|
||||
}
|
||||
|
||||
export function unlockElementImpl(doc: Store, element: GfxCompatibleInterface) {
|
||||
doc.transact(() => {
|
||||
element.lockedBySelf = false;
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user