@plait/mind
Version:
Implementation of the core logic of the mind map plugin.
1,323 lines (1,292 loc) • 179 kB
JavaScript
import { DEFAULT_COLOR, DefaultThemeColor, ColorfulThemeColor, SoftThemeColor, RetroThemeColor, DarkThemeColor, StarryThemeColor, rgbaToHEX, PlaitElement, PlaitNode, Path, isNullOrUndefined, PlaitBoard, getSelectedElements, getI18nValue, idCreator, Transforms, clearSelectedElement, addSelectedElement, distanceBetweenPointAndRectangle, RectangleClient, setDragging, depthFirstRecursion, getIsRecursionFunc, drawRoundRectangle, drawLinearPath, drawBezierPath, setStrokeLinecap, createG, createForeignObject, updateForeignObject, getRectangleByElements, toActiveRectangleFromViewBoxRectangle, ACTIVE_STROKE_WIDTH, SELECTION_RECTANGLE_CLASS_NAME, NODE_TO_PARENT, removeSelectedElement, PlaitHistoryBoard, isSelectedElement, createText, isSelectionMoving, isDragging, isMovingElements, NODE_TO_INDEX, PlaitPointerType, isMainPointer, toViewBoxPoint, toHostPoint, getHitElementByPoint, distanceBetweenPointAndPoint, CoreTransforms, toActivePointFromViewBoxPoint, BoardTransforms, throttleRAF, isContextmenu, temporaryDisableSelection, hotkeys, createClipboardContext, WritableClipboardType, Point, ResizeCursorClass, WritableClipboardOperationType, addOrCreateClipboardContext } from '@plait/core';
import { MindLayoutType, AbstractNode, isIndentedLayout, isHorizontalLogicLayout, ConnectingPosition, isHorizontalLayout, getNonAbstractChildren, isStandardLayout, isLeftLayout, isRightLayout, isVerticalLogicLayout, isTopLayout, isBottomLayout, getCorrectStartEnd, getAbstractLayout, GlobalLayout } from '@plait/layouts';
import { StrokeStyle, getFirstTextManage, buildText, getElementSize, DEFAULT_FONT_FAMILY, RESIZE_HANDLE_DIAMETER, getRectangleResizeHandleRefs, addElementOfFocusedImage, ImageGenerator, removeElementOfFocusedImage, getStrokeLineDash, getXDistanceBetweenPoint, moveXOfPoint, moveYOfPoint, Generator, PropertyTransforms, TRANSPARENT, measureElement, isResizing, CommonElementFlavour, WithTextPluginKey, TextManage, isDrawingMode, isDndMode, setCreationMode, BoardCreationMode, isExpandHotkey, isTabHotkey, isEnterHotkey, isVirtualKey, isDelete, isSpaceHotkey, getElementOfFocusedImage, acceptImageTypes, buildImage, withResize, getElementsText } from '@plait/common';
import { DEFAULT_FONT_SIZE, PlaitMarkEditor, MarkTypes, FontSizes } from '@plait/text-plugins';
import { Node as Node$1, Path as Path$1 } from 'slate';
import { pointsOnBezierCurves } from 'points-on-curve';
import { isHotkey } from 'is-hotkey';
const MIND_ELEMENT_TO_NODE = new WeakMap();
const MindNode = {
get(root, path) {
let node = root;
for (let i = 0; i < path.length; i++) {
const p = path[i];
if (!node || !node.children || !node.children[p]) {
throw new Error(`Cannot find a descendant at path [${path}]`);
}
node = node.children[p];
}
return node;
}
};
var LayoutDirection;
(function (LayoutDirection) {
LayoutDirection["top"] = "top";
LayoutDirection["right"] = "right";
LayoutDirection["bottom"] = "bottom";
LayoutDirection["left"] = "left";
})(LayoutDirection || (LayoutDirection = {}));
const LayoutDirectionsMap = {
[MindLayoutType.right]: [LayoutDirection.right],
[MindLayoutType.left]: [LayoutDirection.left],
[MindLayoutType.upward]: [LayoutDirection.top],
[MindLayoutType.downward]: [LayoutDirection.bottom],
[MindLayoutType.rightBottomIndented]: [LayoutDirection.right, LayoutDirection.bottom],
[MindLayoutType.rightTopIndented]: [LayoutDirection.right, LayoutDirection.top],
[MindLayoutType.leftBottomIndented]: [LayoutDirection.left, LayoutDirection.bottom],
[MindLayoutType.leftTopIndented]: [LayoutDirection.left, LayoutDirection.top]
};
var MindPointerType;
(function (MindPointerType) {
MindPointerType["mind"] = "mind";
})(MindPointerType || (MindPointerType = {}));
const DEFAULT_BRANCH_COLORS = [
'#A287E0',
'#6E80DB',
'#6DC4C4',
'#E0B75E',
'#B1C675',
'#77C386',
'#C18976',
'#E48484',
'#E582D4',
'#6AB1E4'
];
const COLORFUL_BRANCH_COLORS = [
'#F94239',
'#FF8612',
'#F3D222',
'#B3D431',
'#00BC7B',
'#06ADBF',
'#476BFF',
'#4E49BE',
'#8957E5',
'#FE5DA1'
];
const SOFT_BRANCH_COLORS = [
'#6D89C1',
'#F2BDC7',
'#B796D9',
'#5BA683',
'#B3D431 ',
'#F2DC6C',
'#F7C98D',
'#60B4D1',
'#838F9E',
'#C1A381'
];
const RETRO_BRANCH_COLORS = [
'#459476',
'#9A894F',
'#D48444',
'#E9C358 ',
'#4B9D9D',
'#C14C41',
'#827086 ',
'#60718D',
'#D38183',
'#9DC19D'
];
const DARK_BRANCH_COLORS = [
'#3DD1AE',
'#F6C659',
'#A9E072',
'#FF877B ',
'#F693E7',
'#5DCFFF',
'#868AF6',
'#4C6DC7',
'#D97C26',
'#268FAC'
];
const STARRY_BRANCH_COLORS = [
'#E46C57',
'#579360',
'#B98339',
'#3A62D1 ',
'#B883B7',
'#42ABE5',
'#2B9D8F',
'#A4705E',
'#265833',
'#787865'
];
const MindDefaultThemeColor = {
...DefaultThemeColor,
branchColors: DEFAULT_BRANCH_COLORS,
rootFill: '#f5f5f5',
rootTextColor: DEFAULT_COLOR
};
const MindColorfulThemeColor = {
...ColorfulThemeColor,
branchColors: COLORFUL_BRANCH_COLORS,
rootFill: DEFAULT_COLOR,
rootTextColor: '#FFFFFF'
};
const MindSoftThemeColor = {
...SoftThemeColor,
branchColors: SOFT_BRANCH_COLORS,
rootFill: '#FFFFFF',
rootTextColor: DEFAULT_COLOR
};
const MindRetroThemeColor = {
...RetroThemeColor,
branchColors: RETRO_BRANCH_COLORS,
rootFill: '#153D5D',
rootTextColor: '#FFFFFF'
};
const MindDarkThemeColor = {
...DarkThemeColor,
branchColors: DARK_BRANCH_COLORS,
rootFill: '#FFFFFF',
rootTextColor: DEFAULT_COLOR
};
const MindStarryThemeColor = {
...StarryThemeColor,
branchColors: STARRY_BRANCH_COLORS,
rootFill: '#FFFFFF',
rootTextColor: DEFAULT_COLOR
};
const MindThemeColors = [
MindDefaultThemeColor,
MindColorfulThemeColor,
MindSoftThemeColor,
MindRetroThemeColor,
MindDarkThemeColor,
MindStarryThemeColor
];
const MindThemeColor = {
isMindThemeColor(value) {
if (value.branchColors && value.rootFill && value.rootTextColor) {
return true;
}
else {
return false;
}
}
};
const WithMindPluginKey = 'plait-mind-plugin-key';
const BASE = 4;
const PRIMARY_COLOR = '#6698FF';
const GRAY_COLOR = '#AAAAAA';
const STROKE_WIDTH = 2;
const RESIZE_HANDLE_BUFFER_DISTANCE = 8;
const NODE_MORE_LINE_DISTANCE = 10;
const NODE_MORE_STROKE_WIDTH = 2;
const NODE_MORE_ICON_DIAMETER = 20;
const NODE_MORE_BRIDGE_DISTANCE = 10;
const NODE_ADD_CIRCLE_COLOR = rgbaToHEX('#000000', 0.2);
const NODE_ADD_HOVER_COLOR = '#6698FF';
const NODE_ADD_INNER_CROSS_COLOR = 'white';
const DEFAULT_MIND_IMAGE_WIDTH = 240;
var MindI18nKey;
(function (MindI18nKey) {
MindI18nKey["mindCentralText"] = "mind-center-text";
MindI18nKey["abstractNodeText"] = "abstract-node-text";
})(MindI18nKey || (MindI18nKey = {}));
function getEmojisWidthHeight(board, element) {
const options = board.getPluginOptions(WithMindPluginKey);
const count = element.data.emojis.length;
const fontSize = getEmojiFontSize(element);
return {
width: fontSize * count + count * 2 * options.emojiPadding + (count - 1) * options.spaceBetweenEmojis,
height: element.height
};
}
function getEmojiFontSize(element) {
if (PlaitMind.isMind(element)) {
return 18 + 2;
}
else {
return 14 + 2;
}
}
const getAvailableProperty = (board, element, propertyKey) => {
return element[propertyKey];
};
const DefaultAbstractNodeStyle = {
branch: { color: GRAY_COLOR, width: 2 },
shape: {
strokeColor: GRAY_COLOR,
strokeWidth: 2
}
};
const DefaultNodeStyle = {
branch: {
width: 2
},
shape: {
rectangleRadius: 4,
strokeWidth: 2,
fill: 'none'
}
};
const separateChildren = (parentElement) => {
const rightNodeCount = parentElement.rightNodeCount;
const children = parentElement.children;
let rightChildren = [], leftChildren = [];
for (let i = 0; i < children.length; i++) {
const child = children[i];
if (AbstractNode.isAbstract(child) && child.end < rightNodeCount) {
rightChildren.push(child);
continue;
}
if (AbstractNode.isAbstract(child) && child.start >= rightNodeCount) {
leftChildren.push(child);
continue;
}
if (i < rightNodeCount) {
rightChildren.push(child);
}
else {
leftChildren.push(child);
}
}
return { leftChildren, rightChildren };
};
const isSetAbstract = (element) => {
return !!getCorrespondingAbstract(element);
};
const canSetAbstract = (element) => {
return !PlaitElement.isRootElement(element) && !AbstractNode.isAbstract(element) && !isSetAbstract(element);
};
const getCorrespondingAbstract = (element) => {
const parent = MindElement.findParent(element);
if (!parent)
return undefined;
const elementIndex = parent.children.indexOf(element);
return parent.children.find(child => {
return AbstractNode.isAbstract(child) && elementIndex >= child.start && elementIndex <= child.end;
});
};
const getBehindAbstracts = (element) => {
const parent = MindElement.findParent(element);
if (!parent)
return [];
const index = parent.children.indexOf(element);
return parent.children.filter(child => AbstractNode.isAbstract(child) && child.start > index);
};
/**
* return corresponding abstract that is not child of elements
*/
const getOverallAbstracts = (board, elements) => {
const overallAbstracts = [];
elements
.filter(value => !AbstractNode.isAbstract(value) && !PlaitMind.isMind(value))
.forEach(value => {
const abstract = getCorrespondingAbstract(value);
if (abstract && elements.indexOf(abstract) === -1 && overallAbstracts.indexOf(abstract) === -1) {
const { start, end } = abstract;
const parent = MindElement.getParent(value);
const isOverall = parent.children.slice(start, end + 1).every(includedElement => elements.indexOf(includedElement) > -1);
if (isOverall) {
overallAbstracts.push(abstract);
}
}
});
return overallAbstracts;
};
/**
* abstract node is valid when elements contains at least one element it is referenced with
*/
const getValidAbstractRefs = (board, elements) => {
const validAbstractRefs = [];
elements
.filter(value => !AbstractNode.isAbstract(value) && !PlaitMind.isMind(value))
.forEach(value => {
const abstract = getCorrespondingAbstract(value);
if (abstract && elements.indexOf(abstract) > 0) {
const index = validAbstractRefs.findIndex(value => value.abstract === abstract);
if (index === -1) {
validAbstractRefs.push({
abstract: abstract,
references: [value]
});
}
else {
validAbstractRefs[index].references.push(value);
}
}
});
return validAbstractRefs;
};
function getRelativeStartEndByAbstractRef(abstractRef, elements) {
const start = elements.indexOf(abstractRef.references[0]);
const end = elements.indexOf(abstractRef.references[abstractRef.references.length - 1]);
return { start, end };
}
const insertElementHandleAbstract = (board, path, step = 1,
// This distinguishes between dragging and adding to the last node summarized in the abstract node
isExtendPreviousNode = true, effectedAbstracts = new Map()) => {
const parent = PlaitNode.parent(board, path);
const hasPreviousNode = path[path.length - 1] !== 0;
let behindAbstracts;
if (!hasPreviousNode) {
behindAbstracts = parent.children.filter(child => AbstractNode.isAbstract(child));
}
else {
const selectedElement = PlaitNode.get(board, Path.previous(path));
behindAbstracts = getBehindAbstracts(selectedElement);
}
if (behindAbstracts.length) {
behindAbstracts.forEach(abstract => {
let newProperties = effectedAbstracts.get(abstract);
if (!newProperties) {
newProperties = { start: 0, end: 0 };
effectedAbstracts.set(abstract, newProperties);
}
newProperties.start = newProperties.start + step;
newProperties.end = newProperties.end + step;
});
}
if (!hasPreviousNode) {
return effectedAbstracts;
}
const selectedElement = PlaitNode.get(board, Path.previous(path));
const correspondingAbstract = getCorrespondingAbstract(selectedElement);
const isDragToLast = !isExtendPreviousNode && correspondingAbstract && correspondingAbstract.end === path[path.length - 1] - 1;
if (correspondingAbstract && !isDragToLast) {
let newProperties = effectedAbstracts.get(correspondingAbstract);
if (!newProperties) {
newProperties = { start: 0, end: 0 };
effectedAbstracts.set(correspondingAbstract, newProperties);
}
newProperties.end = newProperties.end + step;
}
return effectedAbstracts;
};
const deleteElementHandleAbstract = (board, deletableElements, effectedAbstracts = new Map()) => {
deletableElements.forEach(node => {
if (!PlaitMind.isMind(node)) {
const behindAbstracts = getBehindAbstracts(node).filter(abstract => !deletableElements.includes(abstract));
if (behindAbstracts.length) {
behindAbstracts.forEach(abstract => {
let newProperties = effectedAbstracts.get(abstract);
if (!newProperties) {
newProperties = { start: 0, end: 0 };
effectedAbstracts.set(abstract, newProperties);
}
newProperties.start = newProperties.start - 1;
newProperties.end = newProperties.end - 1;
});
}
const correspondingAbstract = getCorrespondingAbstract(node);
if (correspondingAbstract && !deletableElements.includes(correspondingAbstract)) {
let newProperties = effectedAbstracts.get(correspondingAbstract);
if (!newProperties) {
newProperties = { start: 0, end: 0 };
effectedAbstracts.set(correspondingAbstract, newProperties);
}
newProperties.end = newProperties.end - 1;
}
}
});
return effectedAbstracts;
};
const isChildOfAbstract = (board, element) => {
const ancestors = MindElement.getAncestors(board, element);
return !!ancestors.find(value => AbstractNode.isAbstract(value));
};
/**
* Processing of branch color, width, style, etc. of the mind node
*/
const getBranchColorByMindElement = (board, element) => {
if (AbstractNode.isAbstract(element) || isChildOfAbstract(board, element)) {
return getAbstractBranchColor(board, element);
}
const branchColor = getAvailableProperty(board, element, 'branchColor') || getAvailableProperty(board, element, 'strokeColor');
return branchColor || getDefaultBranchColor(board, element);
};
const getBranchShapeByMindElement = (board, element) => {
const branchShape = getAvailableProperty(board, element, 'branchShape');
return branchShape || BranchShape.bight;
};
const getBranchWidthByMindElement = (board, element) => {
const branchWidth = getAvailableProperty(board, element, 'branchWidth') || getAvailableProperty(board, element, 'strokeWidth');
return branchWidth || STROKE_WIDTH;
};
const getAbstractBranchWidth = (board, element) => {
if (!isNullOrUndefined(element.branchWidth)) {
return element.branchWidth;
}
return DefaultAbstractNodeStyle.branch.width;
};
const getAbstractBranchColor = (board, element) => {
if (element.branchColor || element.strokeColor) {
return element.branchColor || element.strokeColor;
}
return DefaultAbstractNodeStyle.branch.color;
};
const getNextBranchColor = (board, root) => {
const index = root.children.length;
return getDefaultBranchColorByIndex(board, index);
};
const getDefaultBranchColor = (board, element) => {
const path = PlaitBoard.findPath(board, element);
return getDefaultBranchColorByIndex(board, path[1]);
};
const getDefaultBranchColorByIndex = (board, index) => {
const themeColor = getMindThemeColor(board);
const length = themeColor.branchColors.length;
const remainder = index % length;
return themeColor.branchColors[remainder];
};
const getMindThemeColor = (board) => {
const themeColors = PlaitBoard.getThemeColors(board);
const themeColor = themeColors.find(val => val.mode === board.theme.themeColorMode);
if (themeColor && MindThemeColor.isMindThemeColor(themeColor)) {
return themeColor;
}
else {
return MindDefaultThemeColor;
}
};
const getStrokeColorByElement = (board, element) => {
if (PlaitMind.isMind(element)) {
const defaultRootStroke = getMindThemeColor(board).rootFill;
return element.strokeColor || defaultRootStroke;
}
if (AbstractNode.isAbstract(element) || isChildOfAbstract(board, element)) {
return element.strokeColor || DefaultAbstractNodeStyle.shape.strokeColor;
}
return getAvailableProperty(board, element, 'strokeColor') || getDefaultBranchColor(board, element);
};
const getStrokeStyleByElement = (board, element) => {
return element.strokeStyle || StrokeStyle.solid;
};
const getStrokeWidthByElement = (board, element) => {
const strokeWidth = element.strokeWidth ||
(AbstractNode.isAbstract(element) ? DefaultAbstractNodeStyle.shape.strokeWidth : DefaultNodeStyle.shape.strokeWidth);
return strokeWidth;
};
const getFillByElement = (board, element) => {
if (element.fill) {
return element.fill;
}
const defaultRootFill = getMindThemeColor(board).rootFill;
return PlaitMind.isMind(element) ? defaultRootFill : DefaultNodeStyle.shape.fill;
};
const getShapeByElement = (board, element) => {
const shape = getAvailableProperty(board, element, 'shape');
return shape || MindElementShape.roundRectangle;
};
function editTopic(element) {
const textManage = getFirstTextManage(element);
textManage?.edit(() => { }, event => {
const keyboardEvent = event;
return keyboardEvent.key === 'Enter' && !keyboardEvent.shiftKey;
});
}
const getSelectedMindElements = (board, elements) => {
const selectedElements = elements?.length ? elements : getSelectedElements(board);
return selectedElements.filter(value => MindElement.isMindElement(board, value));
};
const getBranchDirectionsByLayouts = (branchLayouts) => {
const branchDirections = [];
branchLayouts.forEach(l => {
const directions = LayoutDirectionsMap[l];
directions.forEach(d => {
if (!branchDirections.includes(d) && !branchDirections.includes(getLayoutReverseDirection(d))) {
branchDirections.push(d);
}
});
});
return branchDirections;
};
const isCorrectLayout = (root, layout) => {
const rootLayout = root.layout || getDefaultLayout();
return !getInCorrectLayoutDirection(rootLayout, layout);
};
const isMixedLayout = (parentLayout, layout) => {
return (!isIndentedLayout(parentLayout) && isIndentedLayout(layout)) || (isIndentedLayout(parentLayout) && !isIndentedLayout(layout));
};
const getInCorrectLayoutDirection = (rootLayout, layout) => {
const directions = LayoutDirectionsMap[rootLayout];
const subLayoutDirections = LayoutDirectionsMap[layout];
if (!subLayoutDirections) {
throw new Error(`unexpected layout: ${layout} on correct layout`);
}
return subLayoutDirections.find(d => directions.includes(getLayoutReverseDirection(d)));
};
const correctLayoutByDirection = (layout, direction) => {
const isHorizontal = direction === LayoutDirection.left || direction === LayoutDirection.right ? true : false;
let inverseDirectionLayout = MindLayoutType.standard;
switch (layout) {
case MindLayoutType.left:
inverseDirectionLayout = MindLayoutType.right;
break;
case MindLayoutType.right:
inverseDirectionLayout = MindLayoutType.left;
break;
case MindLayoutType.downward:
inverseDirectionLayout = MindLayoutType.upward;
break;
case MindLayoutType.upward:
inverseDirectionLayout = MindLayoutType.downward;
break;
case MindLayoutType.rightBottomIndented:
inverseDirectionLayout = isHorizontal ? MindLayoutType.leftBottomIndented : MindLayoutType.rightTopIndented;
break;
case MindLayoutType.leftBottomIndented:
inverseDirectionLayout = isHorizontal ? MindLayoutType.rightBottomIndented : MindLayoutType.leftTopIndented;
break;
case MindLayoutType.rightTopIndented:
inverseDirectionLayout = isHorizontal ? MindLayoutType.leftTopIndented : MindLayoutType.rightBottomIndented;
break;
case MindLayoutType.leftTopIndented:
inverseDirectionLayout = isHorizontal ? MindLayoutType.rightTopIndented : MindLayoutType.leftBottomIndented;
break;
}
return inverseDirectionLayout;
};
const getLayoutDirection$1 = (root) => {
const layout = root.layout || getDefaultLayout();
return LayoutDirectionsMap[layout];
};
const getDefaultLayout = () => {
return MindLayoutType.standard;
};
const getAvailableSubLayoutsByLayoutDirections = (directions) => {
const result = [];
const reverseDirections = directions.map(getLayoutReverseDirection);
for (const key in MindLayoutType) {
const layout = MindLayoutType[key];
const layoutDirections = LayoutDirectionsMap[layout];
if (layoutDirections) {
const hasSameDirection = layoutDirections.some(d => directions.includes(d));
const hasReverseDirection = layoutDirections.some(r => reverseDirections.includes(r));
if (hasSameDirection && !hasReverseDirection) {
result.push(layout);
}
}
}
return result;
};
const getLayoutReverseDirection = (layoutDirection) => {
let reverseDirection = LayoutDirection.right;
switch (layoutDirection) {
case LayoutDirection.top:
reverseDirection = LayoutDirection.bottom;
break;
case LayoutDirection.bottom:
reverseDirection = LayoutDirection.top;
break;
case LayoutDirection.right:
reverseDirection = LayoutDirection.left;
break;
case LayoutDirection.left:
reverseDirection = LayoutDirection.right;
break;
}
return reverseDirection;
};
const getRootLayout = (root) => {
return root.layout || getDefaultLayout();
};
const getLayoutOptions = (board) => {
function getMainAxle(element, parent) {
if (PlaitMind.isMind(element)) {
return BASE * 12;
}
if (parent && parent.isRoot()) {
return BASE * 3;
}
return BASE * 3;
}
function getSecondAxle(element, parent) {
if (PlaitMind.isMind(element)) {
return BASE * 12;
}
return BASE * 8.5;
}
return {
getHeight(element) {
return NodeSpace.getNodeHeight(board, element);
},
getWidth(element) {
return NodeSpace.getNodeWidth(board, element);
},
getHorizontalGap(element, parent) {
const _layout = (parent && parent.layout) || getRootLayout(element);
const isHorizontal = isHorizontalLayout(_layout);
if (isIndentedLayout(_layout)) {
return BASE * 6;
}
if (!isHorizontal) {
return getMainAxle(element, parent);
}
else {
return getSecondAxle(element, parent);
}
},
getVerticalGap(element, parent) {
const _layout = (parent && parent.layout) || getRootLayout(element);
if (isIndentedLayout(_layout)) {
return BASE * 3.5;
}
const isHorizontal = isHorizontalLayout(_layout);
if (isHorizontal) {
return getMainAxle(element, parent);
}
else {
return getSecondAxle(element, parent);
}
},
getVerticalConnectingPosition(element, parent) {
if (element.shape === MindElementShape.underline && parent && isHorizontalLogicLayout(parent.layout)) {
return ConnectingPosition.bottom;
}
return undefined;
},
getExtendHeight(node) {
return 0;
},
getIndentedCrossLevelGap() {
return BASE * 1;
}
};
};
const MIND_CENTRAL_TEXT = '中心主题';
const ABSTRACT_NODE_TEXT = '概要';
const getDefaultMindNameText = (board) => {
return getI18nValue(board, MindI18nKey.mindCentralText, MIND_CENTRAL_TEXT);
};
const getAbstractNodeText = (board) => {
return getI18nValue(board, MindI18nKey.abstractNodeText, ABSTRACT_NODE_TEXT);
};
const createEmptyMind = (board, point) => {
const text = getDefaultMindNameText(board);
const element = createMindElement(text, { layout: MindLayoutType.right });
element.type = 'mind';
const width = NodeSpace.getNodeWidth(board, element);
const height = NodeSpace.getNodeHeight(board, element);
element.points = [[point[0] - width / 2, point[1] - height / 2]];
return element;
};
const createMindElement = (text, options) => {
const newElement = {
id: idCreator(),
type: 'mind_child',
data: {
topic: buildText(text)
},
children: []
};
let key;
for (key in options) {
if (!isNullOrUndefined(options[key])) {
newElement[key] = options[key];
}
}
return newElement;
};
const INHERIT_ATTRIBUTE_KEYS = [
'fill',
'strokeColor',
'strokeWidth',
'strokeStyle',
'shape',
'layout',
'branchColor',
'branchWidth',
'branchShape'
];
const TOPIC_FONT_SIZE = 14;
const ROOT_TOPIC_FONT_SIZE = 18;
const TOPIC_DEFAULT_MAX_WORD_COUNT = 34;
const getChildrenCount = (element) => {
const count = element.children.reduce((p, c) => {
return p + getChildrenCount(c);
}, 0);
return count + element.children.length;
};
const isChildElement = (origin, child) => {
let parent = MindElement.findParent(child);
while (parent) {
if (parent === origin) {
return true;
}
parent = MindElement.findParent(parent);
}
return false;
};
const getFirstLevelElement = (elements) => {
let result = [];
elements.forEach((element) => {
const isChild = elements.some((node) => {
return isChildElement(node, element);
});
if (!isChild) {
result.push(element);
}
});
return result;
};
const isChildRight = (parent, child) => {
return parent.x < child.x;
};
const isChildUp = (parent, child) => {
return parent.y > child.y;
};
const copyNewNode = (node) => {
const newNode = { ...node };
newNode.id = idCreator();
newNode.children = [];
for (const childNode of node.children) {
newNode.children.push(copyNewNode(childNode));
}
return newNode;
};
const insertMindElement = (board, inheritNode, path) => {
const newNode = {};
if (!PlaitMind.isMind(inheritNode)) {
INHERIT_ATTRIBUTE_KEYS.forEach((attr) => {
newNode[attr] = inheritNode[attr];
});
delete newNode.layout;
}
const newElement = createMindElement('', newNode);
Transforms.insertNode(board, newElement, path);
clearSelectedElement(board);
addSelectedElement(board, newElement);
setTimeout(() => {
editTopic(newElement);
});
};
const findLastChild = (child) => {
let result = child;
while (result.children.length !== 0) {
result = result.children[result.children.length - 1];
}
return result;
};
const divideElementByParent = (elements) => {
const abstractIncludedGroups = [];
const parentElements = [];
for (let i = 0; i < elements.length; i++) {
const parent = MindElement.getParent(elements[i]);
const parentIndex = parentElements.indexOf(parent);
if (parentIndex === -1) {
parentElements.push(parent);
abstractIncludedGroups.push([elements[i]]);
}
else {
abstractIncludedGroups[parentIndex].push(elements[i]);
}
}
return { parentElements, abstractIncludedGroups };
};
const getDefaultFontSizeForMindElement = (element) => {
if (PlaitMind.isMind(element)) {
return ROOT_TOPIC_FONT_SIZE;
}
if (MindElement.isMindElement(null, element)) {
return TOPIC_FONT_SIZE;
}
throw new Error('can not find default font-size');
};
const ABSTRACT_HANDLE_COLOR = '#6698FF80'; //primary color 50% opacity
const ABSTRACT_INCLUDED_OUTLINE_OFFSET = 3.5;
const ABSTRACT_HANDLE_LENGTH = 10;
const ABSTRACT_HANDLE_MASK_WIDTH = 8;
const NodeDefaultSpace = {
horizontal: {
nodeAndText: BASE * 2.5,
emojiAndText: BASE * 1.5
},
vertical: {
nodeAndText: BASE,
nodeAndImage: BASE,
imageAndText: BASE * 1.5
}
};
const RootDefaultSpace = {
horizontal: {
nodeAndText: BASE * 4,
emojiAndText: BASE * 2
},
vertical: {
nodeAndText: BASE * 2
}
};
const getHorizontalSpaceBetweenNodeAndText = (board, element) => {
const isMind = PlaitMind.isMind(element);
const nodeAndText = isMind ? RootDefaultSpace.horizontal.nodeAndText : NodeDefaultSpace.horizontal.nodeAndText;
const strokeWidth = getStrokeWidthByElement(board, element);
return nodeAndText + strokeWidth;
};
const getVerticalSpaceBetweenNodeAndText = (board, element) => {
const isMind = PlaitMind.isMind(element);
const strokeWidth = getStrokeWidthByElement(board, element);
const nodeAndText = isMind ? RootDefaultSpace.vertical.nodeAndText : NodeDefaultSpace.vertical.nodeAndText;
return nodeAndText + strokeWidth;
};
const getSpaceEmojiAndText = (element) => {
const isMind = PlaitMind.isMind(element);
const emojiAndText = isMind ? RootDefaultSpace.horizontal.emojiAndText : NodeDefaultSpace.horizontal.emojiAndText;
return emojiAndText;
};
const NodeSpace = {
getNodeWidth(board, element) {
const nodeAndText = getHorizontalSpaceBetweenNodeAndText(board, element);
if (MindElement.hasEmojis(element)) {
return (NodeSpace.getEmojiLeftSpace(board, element) +
getEmojisWidthHeight(board, element).width +
getSpaceEmojiAndText(element) +
NodeSpace.getTopicDynamicWidth(board, element) +
nodeAndText);
}
return nodeAndText + NodeSpace.getTopicDynamicWidth(board, element) + nodeAndText;
},
getNodeHeight(board, element) {
const topicSize = getElementSize(board, element.data.topic, { fontSize: DEFAULT_FONT_SIZE, fontFamily: DEFAULT_FONT_FAMILY }, NodeSpace.getTopicMaxDynamicWidth(board, element));
const normalizedSize = normalizeWidthAndHeight(board, element, topicSize.width, topicSize.height);
const nodeAndText = getVerticalSpaceBetweenNodeAndText(board, element);
if (MindElement.hasImage(element)) {
return NodeSpace.getTextTopSpace(board, element) + normalizedSize.height + nodeAndText;
}
return nodeAndText + normalizedSize.height + nodeAndText;
},
getTopicDynamicWidth(board, element) {
const topicSize = getElementSize(board, element.data.topic, { fontSize: getDefaultFontSizeForMindElement(element), fontFamily: DEFAULT_FONT_FAMILY }, NodeSpace.getTopicMaxDynamicWidth(board, element));
const normalizedSize = normalizeWidthAndHeight(board, element, topicSize.width, topicSize.width);
const width = element.manualWidth || normalizedSize.width;
const imageWidth = MindElement.hasImage(element) ? element.data.image?.width : 0;
return Math.max(width, imageWidth);
},
getTopicHeight(board, element) {
const topicSize = getElementSize(board, element.data.topic, { fontSize: DEFAULT_FONT_SIZE, fontFamily: DEFAULT_FONT_FAMILY }, NodeSpace.getTopicMaxDynamicWidth(board, element));
const normalizedSize = normalizeWidthAndHeight(board, element, topicSize.width, topicSize.height);
return normalizedSize.height;
},
getTopicMaxDynamicWidth(board, element) {
const fontSize = getDefaultFontSizeForMindElement(element);
if (element.manualWidth) {
return Math.max(element.manualWidth, MindElement.hasImage(element) ? element.data.image?.width : 0);
}
return Math.max(fontSize * TOPIC_DEFAULT_MAX_WORD_COUNT, MindElement.hasImage(element) ? element.data.image?.width : 0);
},
getNodeResizableMinWidth(board, element) {
const minTopicWidth = NodeSpace.getNodeTopicMinWidth(board, element);
if (MindElement.hasImage(element) && element.data.image.width > minTopicWidth) {
return element.data.image.width;
}
else {
return minTopicWidth;
}
},
getNodeTopicMinWidth(board, element) {
return getFontSizeByMindElement(board, element);
},
getTextLeftSpace(board, element) {
const nodeAndText = getHorizontalSpaceBetweenNodeAndText(board, element);
if (MindElement.hasEmojis(element)) {
return NodeSpace.getEmojiLeftSpace(board, element) + getEmojisWidthHeight(board, element).width + getSpaceEmojiAndText(element);
}
else {
return nodeAndText;
}
},
getTextTopSpace(board, element) {
const nodeAndText = getVerticalSpaceBetweenNodeAndText(board, element);
if (MindElement.hasImage(element)) {
return NodeSpace.getImageTopSpace(board, element) + element.data.image.height + NodeDefaultSpace.vertical.imageAndText;
}
else {
return nodeAndText;
}
},
getImageTopSpace(board, element) {
const strokeWidth = getStrokeWidthByElement(board, element);
return strokeWidth + NodeDefaultSpace.vertical.nodeAndImage;
},
getEmojiLeftSpace(board, element) {
const options = board.getPluginOptions(WithMindPluginKey);
const nodeAndText = getHorizontalSpaceBetweenNodeAndText(board, element);
return nodeAndText - options.emojiPadding;
},
getEmojiTopSpace(board, element) {
const nodeAndText = getVerticalSpaceBetweenNodeAndText(board, element);
return nodeAndText;
}
};
const getFontSizeByMindElement = (board, element) => {
const defaultFontSize = getDefaultFontSizeForMindElement(element);
const marks = PlaitMarkEditor.getMarksByElement(element.data.topic);
const fontSize = marks[MarkTypes.fontSize] || defaultFontSize;
return fontSize;
};
const normalizeWidthAndHeight = (board, element, width, height) => {
const minWidth = NodeSpace.getNodeTopicMinWidth(board, element);
const newWidth = width < minWidth ? minWidth : width;
return { width: newWidth, height };
};
function getRectangleByNode(node) {
const x = node.x + node.hGap;
let y = node.y + node.vGap;
const width = node.width - node.hGap * 2;
const height = node.height - node.vGap * 2;
return {
x,
y,
width,
height
};
}
function getRectangleByElement(board, element) {
const width = NodeSpace.getNodeWidth(board, element);
const height = NodeSpace.getNodeHeight(board, element);
const nodeRectangle = {
x: element.points[0][0],
y: element.points[0][1],
width,
height
};
return nodeRectangle;
}
function isHitMindElement(board, point, element) {
const node = MIND_ELEMENT_TO_NODE.get(element);
if (node && distanceBetweenPointAndRectangle(point[0], point[1], getRectangleByNode(node)) === 0) {
return true;
}
else {
return false;
}
}
function getEmojiRectangle(board, element) {
let { x, y } = getRectangleByNode(MindElement.getNode(element));
x = x + NodeSpace.getEmojiLeftSpace(board, element);
const { width, height } = getEmojisWidthHeight(board, element);
return {
x,
y,
width,
height
};
}
function getEmojiForeignRectangle(board, element) {
let { x, y } = getRectangleByNode(MindElement.getNode(element));
x = x + NodeSpace.getEmojiLeftSpace(board, element);
const { width } = getEmojisWidthHeight(board, element);
return {
x,
y,
width,
height: NodeSpace.getNodeHeight(board, element)
};
}
const isHitEmojis = (board, element, point) => {
return RectangleClient.isHit(RectangleClient.getRectangleByPoints([point, point]), getEmojiRectangle(board, element));
};
function getTopicRectangleByNode(board, node) {
let nodeRectangle = getRectangleByNode(node);
const result = getTopicRectangleByElement(board, nodeRectangle, node.origin);
return result;
}
function getTopicRectangleByElement(board, nodeRectangle, element) {
const x = nodeRectangle.x + NodeSpace.getTextLeftSpace(board, element);
const y = nodeRectangle.y + NodeSpace.getTextTopSpace(board, element);
const width = NodeSpace.getTopicDynamicWidth(board, element);
const height = NodeSpace.getTopicHeight(board, element);
return { height, width, x, y };
}
function getImageForeignRectangle(board, element) {
let { x, y } = getRectangleByNode(MindElement.getNode(element));
const elementWidth = element.manualWidth || element.width;
x =
elementWidth > element.data.image.width
? x + NodeSpace.getTextLeftSpace(board, element) + (elementWidth - element.data.image.width) / 2
: x + NodeSpace.getTextLeftSpace(board, element);
y = NodeSpace.getImageTopSpace(board, element) + y;
const { width, height } = element.data.image;
const rectangle = {
x,
y,
width,
height
};
return rectangle;
}
const isHitImage = (board, element, point) => {
const imageRectangle = getImageForeignRectangle(board, element);
const imageOutlineRectangle = RectangleClient.getOutlineRectangle(imageRectangle, -RESIZE_HANDLE_DIAMETER / 2);
return RectangleClient.isPointInRectangle(imageOutlineRectangle, point);
};
const getHitImageResizeHandleDirection = (board, element, point) => {
const imageRectangle = getImageForeignRectangle(board, element);
const resizeHandleRefs = getRectangleResizeHandleRefs(imageRectangle, RESIZE_HANDLE_DIAMETER);
const result = resizeHandleRefs.find(resizeHandleRef => {
return RectangleClient.isHit(RectangleClient.getRectangleByPoints([point, point]), resizeHandleRef.rectangle);
});
return result;
};
const adjustRootToNode = (board, node) => {
const newNode = { ...node };
delete newNode.rightNodeCount;
newNode.type = 'mind_child';
if (newNode.layout === MindLayoutType.standard) {
delete newNode.layout;
}
return newNode;
};
const adjustAbstractToNode = (node) => {
const newNode = { ...node };
delete newNode.start;
delete newNode.end;
return newNode;
};
const adjustNodeToRoot = (board, node) => {
const newElement = { ...node };
if (!Node$1.string(newElement.data.topic)) {
newElement.data.topic = { children: [{ text: '思维导图' }] };
}
delete newElement?.strokeColor;
delete newElement?.fill;
delete newElement?.shape;
delete newElement?.strokeWidth;
delete newElement?.isCollapsed;
return {
...newElement,
layout: newElement.layout ?? MindLayoutType.right,
type: 'mind'
};
};
const addImageFocus = (board, element) => {
addElementOfFocusedImage(board, element);
const commonElementRef = PlaitElement.getElementRef(element);
const imageGenerator = commonElementRef.getGenerator(ImageGenerator.key);
imageGenerator.setFocus(element, true);
};
const removeImageFocus = (board, element) => {
removeElementOfFocusedImage(board);
const commonElementRef = PlaitElement.getElementRef(element);
const imageGenerator = commonElementRef.getGenerator(ImageGenerator.key);
imageGenerator.setFocus(element, false);
};
/**
* 1. return new node height if height changed
* 2. new height is effected by zoom
*/
const getNewNodeHeight = (board, element, newNodeDynamicWidth) => {
const textManage = getFirstTextManage(element);
const { height } = textManage.getSize(undefined, newNodeDynamicWidth);
if (Math.abs(height - element.height) > 2) {
return height;
}
return undefined;
};
const addActiveOnDragOrigin = (activeElement) => {
PlaitElement.getElementG(activeElement).classList.add('dragging-node');
!activeElement.isCollapsed &&
activeElement.children.forEach(child => {
addActiveOnDragOrigin(child);
});
};
const removeActiveOnDragOrigin = (activeElement) => {
PlaitElement.getElementG(activeElement).classList.remove('dragging-node');
!activeElement.isCollapsed &&
activeElement.children.forEach(child => {
removeActiveOnDragOrigin(child);
});
};
const setMindDragging = (board, state) => {
setDragging(board, state);
if (state) {
PlaitBoard.getBoardContainer(board).classList.add('mind-node-dragging');
}
else {
PlaitBoard.getBoardContainer(board).classList.remove('mind-node-dragging');
}
};
const hasPreviousOrNextOfDropPath = (parent, dropTarget, dropPath) => {
let children = getNonAbstractChildren(parent);
if (PlaitMind.isMind(parent) && isStandardLayout(getRootLayout(parent))) {
const isDropRight = isDropStandardRight(parent, dropTarget);
if (isDropRight) {
children = children.slice(0, parent.rightNodeCount);
}
if (!isDropRight) {
children = children.slice(parent.rightNodeCount, children.length);
dropPath = [...dropPath, dropPath[dropPath.length - 1] - parent.rightNodeCount];
}
}
let hasPreviousNode = dropPath[dropPath.length - 1] !== 0;
let hasNextNode = dropPath[dropPath.length - 1] !== (children?.length || 0);
if (parent.isCollapsed) {
hasNextNode = false;
hasPreviousNode = false;
}
return {
hasPreviousNode,
hasNextNode
};
};
const isDropStandardRight = (parent, dropTarget) => {
const target = dropTarget.target;
return ((PlaitMind.isMind(parent) &&
isStandardLayout(getRootLayout(parent)) &&
parent.children.indexOf(target) !== -1 &&
parent.children.indexOf(target) < parent.rightNodeCount) ||
(PlaitMind.isMind(target) && isStandardLayout(getRootLayout(target)) && dropTarget.detectResult === 'right'));
};
const directionCorrector = (board, node, detectResults) => {
if (!PlaitMind.isMind(node.origin) && !AbstractNode.isAbstract(node.origin)) {
const parentLayout = MindQueries.getCorrectLayoutByElement(board, node?.parent.origin);
if (isStandardLayout(parentLayout)) {
const idx = node.parent.children.findIndex((x) => x === node);
const isLeft = idx >= (node.parent.origin.rightNodeCount || 0);
return getAllowedDirection(detectResults, [isLeft ? 'right' : 'left']);
}
if (isLeftLayout(parentLayout)) {
return getAllowedDirection(detectResults, ['right']);
}
if (isRightLayout(parentLayout)) {
return getAllowedDirection(detectResults, ['left']);
}
if (parentLayout === MindLayoutType.upward) {
return getAllowedDirection(detectResults, ['bottom']);
}
if (parentLayout === MindLayoutType.downward) {
return getAllowedDirection(detectResults, ['top']);
}
}
else {
const layout = MindQueries.getCorrectLayoutByElement(board, node?.origin);
if (isStandardLayout(layout)) {
return getAllowedDirection(detectResults, ['top', 'bottom']);
}
if (layout === MindLayoutType.upward) {
return getAllowedDirection(detectResults, ['left', 'right', 'bottom']);
}
if (layout === MindLayoutType.downward) {
return getAllowedDirection(detectResults, ['left', 'right', 'top']);
}
if (isLeftLayout(layout)) {
return getAllowedDirection(detectResults, ['right', 'top', 'bottom']);
}
if (isRightLayout(layout)) {
return getAllowedDirection(detectResults, ['left', 'top', 'bottom']);
}
}
return null;
};
const getAllowedDirection = (detectResults, illegalDirections) => {
const directions = detectResults;
illegalDirections.forEach((item) => {
const bottomDirectionIndex = directions.findIndex((direction) => direction === item);
if (bottomDirectionIndex !== -1) {
directions.splice(bottomDirectionIndex, 1);
}
});
return directions.length ? directions : null;
};
const detectDropTarget = (board, detectPoint, dropTarget, activeElements) => {
let detectResult = null;
depthFirstRecursion(board, (element) => {
if (!MindElement.isMindElement(board, element) || detectResult) {
return;
}
const node = MindElement.getNode(element);
const directions = directionDetector(node, detectPoint);
if (directions) {
detectResult = directionCorrector(board, node, directions);
}
dropTarget = null;
const isValid = activeElements.every((element) => isValidTarget(element, node.origin));
if (detectResult && isValid) {
dropTarget = { target: node.origin, detectResult: detectResult[0] };
}
}, getIsRecursionFunc(board));
return dropTarget;
};
const directionDetector = (targetNode, centerPoint) => {
const { x, y, width, height } = getRectangleByNode(targetNode);
const yCenter = y + height / 2;
const xCenter = x + width / 2;
const top = targetNode.y;
const bottom = targetNode.y + targetNode.height;
const left = targetNode.x;
const right = targetNode.x + targetNode.width;
const direction = [];
// x-axis
if (centerPoint[1] > y && centerPoint[1] < y + height) {
if (centerPoint[0] > left && centerPoint[0] < xCenter) {
direction.push('left');
}
if (centerPoint[0] > xCenter && centerPoint[0] < right) {
direction.push('right');
}
// Overlapping area, return in both directions
if ((centerPoint[0] > x && centerPoint[0] < xCenter) || (centerPoint[0] > xCenter && centerPoint[0] < x + width)) {
if (centerPoint[1] < yCenter) {
direction.push('top');
}
else {
direction.push('bottom');
}
}
return direction.length ? direction : null;
}
// y-axis
if (centerPoint[0] > x && centerPoint[0] < x + width) {
if (centerPoint[1] > top && centerPoint[1] < yCenter) {
direction.push('top');
}
if (centerPoint[1] > yCenter && centerPoint[1] < bottom) {
direction.push('bottom');
}
if ((centerPoint[1] > y && centerPoint[1] < y + height) || (centerPoint[1] > yCenter && centerPoint[1] < y + height)) {
if (centerPoint[0] < xCenter) {
direction.push('left');
}
else {
direction.push('right');
}
}
return direction.length ? direction : null;
}
return null;
};
const isValidTarget = (origin, target) => {
return origin !== target && !isChildElement(origin, target);
};
const getPathByDropTarget = (board, dropTarget) => {
let targetPath = PlaitBoard.findPath(board, dropTarget?.target);
const layout = PlaitMind.isMind(dropTarget?.target)
? getRootLayout(dropTarget?.target)
: MindQueries.getCorrectLayoutByElement(board, MindElement.getParent(dropTarget?.target));
const children = getNonAbstractChildren(dropTarget.target);
if (isVerticalLogicLayout(layout)) {
if (dropTarget.detectResult === 'top' || dropTarget.detectResult === 'bottom') {
targetPath.push(children.length);
}
if (dropTarget.detectResult === 'right') {
targetPath = Path.next(targetPath);
}
}
if (isHorizontalLogicLayout(layout)) {
if (dropTarget.detectResult === 'right') {
if (PlaitMind.isMind(dropTarget?.target) && isStandardLayout(layout)) {
targetPath.push(dropTarget?.target.rightNodeCount);
}
else {
targetPath.push(children.length);
}
}
if (dropTarget.detectResult === 'left') {
targetPath.push(children.length);
}
if (dropTarget.detectResult === 'bottom') {
targetPath = Path.next(targetPath);
}
}
if (isIndentedLayout(layout)) {
if (isTopLayout(layout) && dropTarget.detectResult === 'top') {
targetPath = Path.next(targetPath);
}
if (isBottomLayout(layout) && dropTarget.detectResult === 'bottom') {
targetPath = Path.next(targetPath);
}
if (isLeftLayout(layout) && dropTarget.detectResult === 'left') {
targetPath.push(children.leng