UNPKG

@plait/mind

Version:

Implementation of the core logic of the mind map plugin.

1,323 lines (1,292 loc) 179 kB
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