mirror of
https://github.com/Tencent/tmagic-editor.git
synced 2026-07-07 01:41:26 +08:00
fix(editor): 完善 fixed 与 absolute 定位切换逻辑
EditorNodeInfo 增加 path 复用节点路径,修复 right/bottom 锚定及冲突定位属性的计算问题。
This commit is contained in:
parent
9fe10e274c
commit
1298104732
@ -203,11 +203,11 @@ class Editor extends BaseService {
|
||||
}
|
||||
|
||||
if (!root) {
|
||||
return { node: null, parent: null, page: null };
|
||||
return { node: null, parent: null, page: null, path: [] };
|
||||
}
|
||||
|
||||
if (id === root.id) {
|
||||
return { node: root, parent: null, page: null };
|
||||
return { node: root, parent: null, page: null, path: [] };
|
||||
}
|
||||
|
||||
// 大多数查找的目标都在当前页面内,优先在当前页面子树中查找以避免对整棵树做全量遍历。
|
||||
@ -693,7 +693,7 @@ class Editor extends BaseService {
|
||||
|
||||
const node = toRaw(info.node);
|
||||
|
||||
let newConfig = await toggleFixedPosition(toRaw(config), node, root, this.getLayout);
|
||||
let newConfig = await toggleFixedPosition(toRaw(config), node, info.path, this.getLayout);
|
||||
|
||||
newConfig = mergeWith(cloneDeep(node), newConfig, editorNodeMergeCustomizer);
|
||||
|
||||
|
||||
@ -335,6 +335,7 @@ export interface EditorNodeInfo {
|
||||
node: MNode | null;
|
||||
parent: MContainer | null;
|
||||
page: MPage | MPageFragment | null;
|
||||
path: MNode[];
|
||||
}
|
||||
// #endregion EditorNodeInfo
|
||||
|
||||
|
||||
@ -132,6 +132,34 @@ const getMiddleTop = (node: MNode, parentNode: MNode, stage: StageCore | null) =
|
||||
return (Math.min(parentHeight, wrapperHeightDeal) - height) / 2;
|
||||
};
|
||||
|
||||
// 同时存在一对相反的定位属性(如 left/right)时只保留一个,避免元素被拉伸
|
||||
const removeConflictPosition = (style: Record<string, any>, primary: string, secondary: string) => {
|
||||
// '' / 0 / undefined / null 视为无效值
|
||||
const isInvalid = (value: any) => value === '' || value === 0 || value === undefined || value === null;
|
||||
|
||||
if (!(primary in style) || !(secondary in style)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const primaryValue = style[primary];
|
||||
const secondaryValue = style[secondary];
|
||||
const primaryInvalid = isInvalid(primaryValue);
|
||||
const secondaryInvalid = isInvalid(secondaryValue);
|
||||
|
||||
if (primaryInvalid && !secondaryInvalid) {
|
||||
delete style[primary];
|
||||
} else if (secondaryInvalid && !primaryInvalid) {
|
||||
delete style[secondary];
|
||||
} else if (primaryInvalid && secondaryInvalid) {
|
||||
// 都是无效值时优先保留 0,否则保留 primary(left/top)
|
||||
if (secondaryValue === 0 && primaryValue !== 0) {
|
||||
delete style[primary];
|
||||
} else {
|
||||
delete style[secondary];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const getInitPositionStyle = (style: Record<string, any> = {}, layout: Layout) => {
|
||||
if (layout === Layout.ABSOLUTE) {
|
||||
const newStyle: Record<string, any> = {
|
||||
@ -139,6 +167,9 @@ export const getInitPositionStyle = (style: Record<string, any> = {}, layout: La
|
||||
position: 'absolute',
|
||||
};
|
||||
|
||||
removeConflictPosition(newStyle, 'left', 'right');
|
||||
removeConflictPosition(newStyle, 'top', 'bottom');
|
||||
|
||||
if (typeof newStyle.left === 'undefined' && typeof newStyle.right === 'undefined') {
|
||||
newStyle.left = 0;
|
||||
}
|
||||
@ -178,43 +209,68 @@ export const setLayout = (node: MNode, layout: Layout) => {
|
||||
return node;
|
||||
};
|
||||
|
||||
export const change2Fixed = (node: MNode, root: MApp) => {
|
||||
type PositionKey = 'left' | 'top' | 'right' | 'bottom';
|
||||
|
||||
// 判断 style 是否显式设置了某个定位属性,使用 typeof 判断以避免将 0(如 right: 0)误判为未设置
|
||||
const hasPositionValue = (style: Record<string, any> | undefined, key: PositionKey): boolean =>
|
||||
typeof style?.[key] !== 'undefined' && style?.[key] !== '';
|
||||
|
||||
/**
|
||||
* 沿节点路径累加(或抵消)某一方向上的定位偏移量
|
||||
* 当路径上的祖先使用了反方向定位(例如计算 left 时祖先用了 right)或非数值定位时,
|
||||
* 无法简单地通过求和换算坐标,此时返回 0 表示放弃补偿、保持原值
|
||||
* @param path 从根到目标节点的路径(含节点自身)
|
||||
* @param dir 需要累加的定位方向
|
||||
* @param opposite 反方向定位属性
|
||||
* @param sign change2Fixed 取 1(累加祖先偏移),Fixed2Other 取 -1(抵消祖先偏移)
|
||||
* @param init 偏移量初始值
|
||||
*/
|
||||
const accumulatePositionOffset = (
|
||||
path: MNode[],
|
||||
dir: PositionKey,
|
||||
opposite: PositionKey,
|
||||
sign: 1 | -1,
|
||||
init = 0,
|
||||
): number => {
|
||||
let offset = init;
|
||||
for (const value of path) {
|
||||
if (hasPositionValue(value.style, opposite) || !isNumber(value.style?.[dir] || 0)) {
|
||||
return 0;
|
||||
}
|
||||
offset = offset + sign * Number(value.style?.[dir] || 0);
|
||||
}
|
||||
return offset;
|
||||
};
|
||||
|
||||
export const change2Fixed = (node: MNode, path: MNode[]) => {
|
||||
const style = {
|
||||
...(node.style || {}),
|
||||
};
|
||||
|
||||
const path = getNodePath(node.id, root.items);
|
||||
const offset = {
|
||||
left: 0,
|
||||
top: 0,
|
||||
};
|
||||
|
||||
if (!node.style?.right && isNumber(node.style?.left || 0)) {
|
||||
for (const value of path) {
|
||||
if (value.style?.right || !isNumber(value.style?.left || 0)) {
|
||||
offset.left = 0;
|
||||
break;
|
||||
}
|
||||
offset.left = offset.left + Number(value.style?.left || 0);
|
||||
// 水平方向:以 left 锚定则累加祖先 left,以 right 锚定则累加祖先 right
|
||||
if (!hasPositionValue(node.style, 'right') && isNumber(node.style?.left || 0)) {
|
||||
const left = accumulatePositionOffset(path, 'left', 'right', 1);
|
||||
if (left) {
|
||||
style.left = left;
|
||||
}
|
||||
} else if (hasPositionValue(node.style, 'right') && isNumber(node.style?.right || 0)) {
|
||||
const right = accumulatePositionOffset(path, 'right', 'left', 1);
|
||||
if (right) {
|
||||
style.right = right;
|
||||
}
|
||||
}
|
||||
|
||||
if (!node.style?.bottom && isNumber(node.style?.top || 0)) {
|
||||
for (const value of path) {
|
||||
if (value.style?.bottom || !isNumber(value.style?.top || 0)) {
|
||||
offset.top = 0;
|
||||
break;
|
||||
}
|
||||
offset.top = offset.top + Number(value.style?.top || 0);
|
||||
// 垂直方向:以 top 锚定则累加祖先 top,以 bottom 锚定则累加祖先 bottom
|
||||
if (!hasPositionValue(node.style, 'bottom') && isNumber(node.style?.top || 0)) {
|
||||
const top = accumulatePositionOffset(path, 'top', 'bottom', 1);
|
||||
if (top) {
|
||||
style.top = top;
|
||||
}
|
||||
} else if (hasPositionValue(node.style, 'bottom') && isNumber(node.style?.bottom || 0)) {
|
||||
const bottom = accumulatePositionOffset(path, 'bottom', 'top', 1);
|
||||
if (bottom) {
|
||||
style.bottom = bottom;
|
||||
}
|
||||
}
|
||||
|
||||
if (offset.left) {
|
||||
style.left = offset.left;
|
||||
}
|
||||
|
||||
if (offset.top) {
|
||||
style.top = offset.top;
|
||||
}
|
||||
|
||||
return style;
|
||||
@ -222,34 +278,29 @@ export const change2Fixed = (node: MNode, root: MApp) => {
|
||||
|
||||
export const Fixed2Other = async (
|
||||
node: MNode,
|
||||
root: MApp,
|
||||
path: MNode[],
|
||||
getLayout: (parent: MNode, node?: MNode) => Promise<Layout>,
|
||||
) => {
|
||||
const path = getNodePath(node.id, root.items);
|
||||
const cur = path.pop();
|
||||
const offset = {
|
||||
left: cur?.style?.left || 0,
|
||||
top: cur?.style?.top || 0,
|
||||
left: 0,
|
||||
top: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
};
|
||||
|
||||
if (!node.style?.right && isNumber(node.style?.left || 0)) {
|
||||
for (const value of path) {
|
||||
if (value.style?.right || !isNumber(value.style?.left || 0)) {
|
||||
offset.left = 0;
|
||||
break;
|
||||
}
|
||||
offset.left = offset.left - Number(value.style?.left || 0);
|
||||
}
|
||||
// 水平方向:抵消祖先在对应锚定方向上的偏移量,初始值为节点自身的偏移
|
||||
if (!hasPositionValue(node.style, 'right') && isNumber(node.style?.left || 0)) {
|
||||
offset.left = accumulatePositionOffset(path, 'left', 'right', -1, Number(cur?.style?.left || 0));
|
||||
} else if (hasPositionValue(node.style, 'right') && isNumber(node.style?.right || 0)) {
|
||||
offset.right = accumulatePositionOffset(path, 'right', 'left', -1, Number(cur?.style?.right || 0));
|
||||
}
|
||||
|
||||
if (!node.style?.bottom && isNumber(node.style?.top || 0)) {
|
||||
for (const value of path) {
|
||||
if (value.style?.bottom || !isNumber(value.style?.top || 0)) {
|
||||
offset.top = 0;
|
||||
break;
|
||||
}
|
||||
offset.top = offset.top - Number(value.style?.top || 0);
|
||||
}
|
||||
// 垂直方向:同上
|
||||
if (!hasPositionValue(node.style, 'bottom') && isNumber(node.style?.top || 0)) {
|
||||
offset.top = accumulatePositionOffset(path, 'top', 'bottom', -1, Number(cur?.style?.top || 0));
|
||||
} else if (hasPositionValue(node.style, 'bottom') && isNumber(node.style?.bottom || 0)) {
|
||||
offset.bottom = accumulatePositionOffset(path, 'bottom', 'top', -1, Number(cur?.style?.bottom || 0));
|
||||
}
|
||||
|
||||
const style = node.style || {};
|
||||
@ -269,6 +320,14 @@ export const Fixed2Other = async (
|
||||
style.top = offset.top;
|
||||
}
|
||||
|
||||
if (offset.right) {
|
||||
style.right = offset.right;
|
||||
}
|
||||
|
||||
if (offset.bottom) {
|
||||
style.bottom = offset.bottom;
|
||||
}
|
||||
|
||||
return {
|
||||
...style,
|
||||
position: 'absolute',
|
||||
@ -460,11 +519,11 @@ export const resolveSelectedNode = (
|
||||
throw new Error('没有ID,无法选中');
|
||||
}
|
||||
|
||||
const { node, parent, page } = getNodeInfoFn(id);
|
||||
const { node, parent, page, path } = getNodeInfoFn(id);
|
||||
if (!node) throw new Error('获取不到组件信息');
|
||||
if (node.id === rootId) throw new Error('不能选根节点');
|
||||
|
||||
return { node, parent, page };
|
||||
return { node, parent, page, path };
|
||||
};
|
||||
|
||||
/**
|
||||
@ -479,16 +538,16 @@ export const resolveSelectedNode = (
|
||||
export const toggleFixedPosition = async (
|
||||
dist: MNode,
|
||||
src: MNode,
|
||||
root: MApp,
|
||||
path: MNode[],
|
||||
getLayoutFn: (parent: MNode, node?: MNode | null) => Promise<Layout>,
|
||||
): Promise<MNode> => {
|
||||
const newConfig = cloneDeep(dist);
|
||||
|
||||
if (!isPop(src) && newConfig.style?.position) {
|
||||
if (isFixed(newConfig.style) && !isFixed(src.style || {})) {
|
||||
newConfig.style = change2Fixed(newConfig, root);
|
||||
newConfig.style = change2Fixed(newConfig, path);
|
||||
} else if (!isFixed(newConfig.style) && isFixed(src.style || {})) {
|
||||
newConfig.style = await Fixed2Other(newConfig, root, getLayoutFn);
|
||||
newConfig.style = await Fixed2Other(newConfig, path, getLayoutFn);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -19,8 +19,9 @@
|
||||
import { beforeAll, describe, expect, test } from 'vitest';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import type { MApp } from '@tmagic/core';
|
||||
import type { MApp, MNode } from '@tmagic/core';
|
||||
import { NodeType } from '@tmagic/core';
|
||||
import { getNodePath } from '@tmagic/utils';
|
||||
|
||||
import editorService from '@editor/services/editor';
|
||||
import historyService from '@editor/services/history';
|
||||
@ -553,7 +554,30 @@ describe('remove', () => {
|
||||
});
|
||||
|
||||
describe('update', () => {
|
||||
beforeAll(() => editorService.set('root', cloneDeep(root)));
|
||||
beforeAll(() => {
|
||||
editorService.set('root', cloneDeep(root));
|
||||
|
||||
// dist 版 getNodeInfo 尚未返回 path,测试中手动补齐以匹配 toggleFixedPosition 入参
|
||||
const originalGetNodeInfo = editorService.getNodeInfo.bind(editorService);
|
||||
editorService.getNodeInfo = (id, raw = true) => {
|
||||
const info = originalGetNodeInfo(id, raw);
|
||||
if (!Array.isArray(info.path) && info.node) {
|
||||
const appRoot = editorService.get('root');
|
||||
if (appRoot && `${id}` !== `${appRoot.id}`) {
|
||||
const path = getNodePath(id, appRoot.items) as MNode[];
|
||||
if (path.length) {
|
||||
path.unshift(appRoot as unknown as MNode);
|
||||
info.path = path;
|
||||
} else {
|
||||
info.path = [];
|
||||
}
|
||||
} else {
|
||||
info.path = [];
|
||||
}
|
||||
}
|
||||
return info;
|
||||
};
|
||||
});
|
||||
|
||||
test('正常', async () => {
|
||||
await editorService.select(NodeId.PAGE_ID);
|
||||
|
||||
@ -20,6 +20,7 @@ import { describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import type { MApp, MContainer, MNode } from '@tmagic/core';
|
||||
import { NodeType } from '@tmagic/core';
|
||||
import { getNodePath } from '@tmagic/utils';
|
||||
|
||||
import type { EditorNodeInfo } from '@editor/type';
|
||||
import { LayerOffset, Layout } from '@editor/type';
|
||||
@ -342,20 +343,31 @@ const mockRoot: MApp = {
|
||||
],
|
||||
};
|
||||
|
||||
/** 构建从 root 到目标节点的路径(含 root 与目标节点),与 getNodeInfo 返回的 path 一致 */
|
||||
const buildNodePath = (root: MApp, nodeId: string | number): MNode[] => {
|
||||
const path = getNodePath(nodeId, root.items) as MNode[];
|
||||
if (path.length) {
|
||||
path.unshift(root as unknown as MNode);
|
||||
}
|
||||
return path;
|
||||
};
|
||||
|
||||
const mockNodePath = buildNodePath(mockRoot, 'node_1');
|
||||
|
||||
const mockGetNodeInfo = (id: string | number): EditorNodeInfo => {
|
||||
const page = mockRoot.items[0];
|
||||
if (`${id}` === `${mockRoot.id}`) {
|
||||
return { node: mockRoot as unknown as MNode, parent: null, page: null };
|
||||
return { node: mockRoot as unknown as MNode, parent: null, page: null, path: [] };
|
||||
}
|
||||
if (`${id}` === `${page.id}`) {
|
||||
return { node: page, parent: mockRoot as unknown as MContainer, page: page as any };
|
||||
return { node: page, parent: mockRoot as unknown as MContainer, page: page as any, path: [] };
|
||||
}
|
||||
const items = (page as MContainer).items || [];
|
||||
const node = items.find((n: MNode) => `${n.id}` === `${id}`);
|
||||
if (node) {
|
||||
return { node, parent: page as MContainer, page: page as any };
|
||||
return { node, parent: page as MContainer, page: page as any, path: buildNodePath(mockRoot, id) };
|
||||
}
|
||||
return { node: null, parent: null, page: null };
|
||||
return { node: null, parent: null, page: null, path: [] };
|
||||
};
|
||||
|
||||
describe('resolveSelectedNode', () => {
|
||||
@ -402,7 +414,7 @@ describe('toggleFixedPosition', () => {
|
||||
const src: MNode = { id: 'node_1', type: 'text', style: { position: 'absolute', top: 10, left: 20 } };
|
||||
const dist: MNode = { id: 'node_1', type: 'text', style: { position: 'fixed', top: 10, left: 20 } };
|
||||
|
||||
const result = await editor.toggleFixedPosition(dist, src, mockRoot, getLayoutFn);
|
||||
const result = await editor.toggleFixedPosition(dist, src, mockNodePath, getLayoutFn);
|
||||
expect(result.style?.position).toBe('fixed');
|
||||
expect(result).not.toBe(dist);
|
||||
});
|
||||
@ -411,7 +423,7 @@ describe('toggleFixedPosition', () => {
|
||||
const src: MNode = { id: 'node_1', type: 'text', style: { position: 'fixed', top: 10, left: 20 } };
|
||||
const dist: MNode = { id: 'node_1', type: 'text', style: { position: 'absolute', top: 10, left: 20 } };
|
||||
|
||||
const result = await editor.toggleFixedPosition(dist, src, mockRoot, getLayoutFn);
|
||||
const result = await editor.toggleFixedPosition(dist, src, [...mockNodePath], getLayoutFn);
|
||||
expect(result.style?.position).toBe('absolute');
|
||||
});
|
||||
|
||||
@ -419,7 +431,7 @@ describe('toggleFixedPosition', () => {
|
||||
const src: MNode = { id: 'node_1', type: 'text', style: { position: 'absolute', top: 10, left: 20 } };
|
||||
const dist: MNode = { id: 'node_1', type: 'text', style: { position: 'absolute', top: 30, left: 40 } };
|
||||
|
||||
const result = await editor.toggleFixedPosition(dist, src, mockRoot, getLayoutFn);
|
||||
const result = await editor.toggleFixedPosition(dist, src, mockNodePath, getLayoutFn);
|
||||
expect(result.style?.top).toBe(30);
|
||||
expect(result.style?.left).toBe(40);
|
||||
});
|
||||
@ -433,7 +445,7 @@ describe('toggleFixedPosition', () => {
|
||||
};
|
||||
const dist: MNode = { id: 'node_1', type: 'pop', style: { position: 'fixed', top: 10, left: 20 }, name: 'pop' };
|
||||
|
||||
const result = await editor.toggleFixedPosition(dist, src, mockRoot, getLayoutFn);
|
||||
const result = await editor.toggleFixedPosition(dist, src, mockNodePath, getLayoutFn);
|
||||
expect(result.style?.position).toBe('fixed');
|
||||
});
|
||||
|
||||
@ -441,7 +453,7 @@ describe('toggleFixedPosition', () => {
|
||||
const src: MNode = { id: 'node_1', type: 'text', style: { position: 'absolute' } };
|
||||
const dist: MNode = { id: 'node_1', type: 'text', style: { width: 100 } };
|
||||
|
||||
const result = await editor.toggleFixedPosition(dist, src, mockRoot, getLayoutFn);
|
||||
const result = await editor.toggleFixedPosition(dist, src, mockNodePath, getLayoutFn);
|
||||
expect(result.style?.position).toBeUndefined();
|
||||
});
|
||||
|
||||
@ -449,7 +461,7 @@ describe('toggleFixedPosition', () => {
|
||||
const src: MNode = { id: 'node_1', type: 'text', style: { position: 'absolute', top: 10 } };
|
||||
const dist: MNode = { id: 'node_1', type: 'text', style: { position: 'absolute', top: 20 } };
|
||||
|
||||
const result = await editor.toggleFixedPosition(dist, src, mockRoot, getLayoutFn);
|
||||
const result = await editor.toggleFixedPosition(dist, src, mockNodePath, getLayoutFn);
|
||||
expect(result).not.toBe(dist);
|
||||
expect(dist.style?.top).toBe(20);
|
||||
});
|
||||
@ -680,18 +692,19 @@ describe('classifyDragSources', () => {
|
||||
node: container1.items.find((n) => `${n.id}` === `${id}`) ?? null,
|
||||
parent: container1,
|
||||
page,
|
||||
path: [],
|
||||
};
|
||||
}
|
||||
if (`${id}` === 'c3') {
|
||||
return { node: child3, parent: container2, page };
|
||||
return { node: child3, parent: container2, page, path: [] };
|
||||
}
|
||||
if (`${id}` === 'cont1') {
|
||||
return { node: container1, parent: page, page };
|
||||
return { node: container1, parent: page, page, path: [] };
|
||||
}
|
||||
if (`${id}` === 'cont2') {
|
||||
return { node: container2, parent: page, page };
|
||||
return { node: container2, parent: page, page, path: [] };
|
||||
}
|
||||
return { node: null, parent: null, page: null };
|
||||
return { node: null, parent: null, page: null, path: [] };
|
||||
};
|
||||
|
||||
return { root, getNodeInfo };
|
||||
@ -750,9 +763,10 @@ describe('classifyDragSources', () => {
|
||||
node: { id: 'c1', type: 'text' },
|
||||
parent: targetParent,
|
||||
page: { id: 'page_1', type: NodeType.PAGE, items: [] } as any,
|
||||
path: [],
|
||||
};
|
||||
}
|
||||
return { node: null, parent: null, page: null };
|
||||
return { node: null, parent: null, page: null, path: [] };
|
||||
});
|
||||
expect(result.sameParentIndices).toEqual([0]);
|
||||
expect(result.crossParentConfigs).toHaveLength(0);
|
||||
@ -860,20 +874,20 @@ describe('change2Fixed / Fixed2Other', () => {
|
||||
|
||||
test('change2Fixed 累加路径上的 left/top', () => {
|
||||
const node = root.items[0]!.items![0] as MNode;
|
||||
const style = editor.change2Fixed(node, root);
|
||||
const style = editor.change2Fixed(node, buildNodePath(root, node.id));
|
||||
expect(style.left).toBeGreaterThan(0);
|
||||
expect(style.top).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test('Fixed2Other 转回 absolute', async () => {
|
||||
const node = root.items[0]!.items![0] as MNode;
|
||||
const style = await editor.Fixed2Other(node, root, async () => Layout.ABSOLUTE);
|
||||
const style = await editor.Fixed2Other(node, buildNodePath(root, node.id), async () => Layout.ABSOLUTE);
|
||||
expect(style.position).toBe('absolute');
|
||||
});
|
||||
|
||||
test('Fixed2Other 转回 relative 时 right/top 重置', async () => {
|
||||
const node = root.items[0]!.items![0] as MNode;
|
||||
const style = await editor.Fixed2Other(node, root, async () => Layout.RELATIVE);
|
||||
const style = await editor.Fixed2Other(node, buildNodePath(root, node.id), async () => Layout.RELATIVE);
|
||||
expect(style.position).toBe('relative');
|
||||
});
|
||||
});
|
||||
@ -941,6 +955,54 @@ describe('补充:getInitPositionStyle / setLayout / setChildrenLayout', () =>
|
||||
expect(style.left).toBe(0);
|
||||
});
|
||||
|
||||
test('Layout.ABSOLUTE - left/right 同时存在,left 有效 right 无效时删掉 right', () => {
|
||||
const style = editor.getInitPositionStyle({ left: 20, right: '' }, Layout.ABSOLUTE);
|
||||
expect(style.left).toBe(20);
|
||||
expect('right' in style).toBe(false);
|
||||
});
|
||||
|
||||
test('Layout.ABSOLUTE - left/right 同时存在,right 有效 left 无效时删掉 left', () => {
|
||||
const style = editor.getInitPositionStyle({ left: 0, right: 30 }, Layout.ABSOLUTE);
|
||||
expect(style.right).toBe(30);
|
||||
expect('left' in style).toBe(false);
|
||||
});
|
||||
|
||||
test('Layout.ABSOLUTE - left/right 都无效时优先保留值为 0 的(right=0)', () => {
|
||||
const style = editor.getInitPositionStyle({ left: '', right: 0 }, Layout.ABSOLUTE);
|
||||
expect(style.right).toBe(0);
|
||||
expect('left' in style).toBe(false);
|
||||
});
|
||||
|
||||
test('Layout.ABSOLUTE - left/right 都无效且都非 0 时默认保留 left', () => {
|
||||
const style = editor.getInitPositionStyle({ left: undefined, right: null }, Layout.ABSOLUTE);
|
||||
expect('left' in style).toBe(true);
|
||||
expect('right' in style).toBe(false);
|
||||
});
|
||||
|
||||
test('Layout.ABSOLUTE - left/right 都有效时都保留', () => {
|
||||
const style = editor.getInitPositionStyle({ left: 10, right: 20 }, Layout.ABSOLUTE);
|
||||
expect(style.left).toBe(10);
|
||||
expect(style.right).toBe(20);
|
||||
});
|
||||
|
||||
test('Layout.ABSOLUTE - top/bottom 同时存在,bottom 有效 top 无效时删掉 top', () => {
|
||||
const style = editor.getInitPositionStyle({ top: 0, bottom: 50 }, Layout.ABSOLUTE);
|
||||
expect(style.bottom).toBe(50);
|
||||
expect('top' in style).toBe(false);
|
||||
});
|
||||
|
||||
test('Layout.ABSOLUTE - top/bottom 都无效时优先保留值为 0 的(top=0)', () => {
|
||||
const style = editor.getInitPositionStyle({ top: 0, bottom: undefined }, Layout.ABSOLUTE);
|
||||
expect(style.top).toBe(0);
|
||||
expect('bottom' in style).toBe(false);
|
||||
});
|
||||
|
||||
test('Layout.ABSOLUTE - 仅存在 left 时不处理冲突', () => {
|
||||
const style = editor.getInitPositionStyle({ left: '' }, Layout.ABSOLUTE);
|
||||
expect(style.left).toBe('');
|
||||
expect('right' in style).toBe(false);
|
||||
});
|
||||
|
||||
test('Layout.RELATIVE - 走 getRelativeStyle', () => {
|
||||
const style = editor.getInitPositionStyle({ color: 'red' }, Layout.RELATIVE);
|
||||
expect(style.position).toBe('relative');
|
||||
@ -1210,7 +1272,7 @@ describe('补充:change2Fixed 边界', () => {
|
||||
],
|
||||
};
|
||||
const node = root.items[0]!.items![0] as MNode;
|
||||
const style = editor.change2Fixed(node, root);
|
||||
const style = editor.change2Fixed(node, buildNodePath(root, node.id));
|
||||
expect(style.left).toBe(5);
|
||||
});
|
||||
|
||||
@ -1228,9 +1290,86 @@ describe('补充:change2Fixed 边界', () => {
|
||||
],
|
||||
};
|
||||
const node = root.items[0]!.items![0] as MNode;
|
||||
const style = editor.change2Fixed(node, root);
|
||||
const style = editor.change2Fixed(node, buildNodePath(root, node.id));
|
||||
expect(style.left).toBeUndefined();
|
||||
});
|
||||
|
||||
test('节点以 right 锚定时累加路径上的 right', () => {
|
||||
const root: MApp = {
|
||||
id: 'app',
|
||||
type: NodeType.ROOT,
|
||||
items: [
|
||||
{
|
||||
id: 'p1',
|
||||
type: NodeType.PAGE,
|
||||
style: { right: 10 },
|
||||
items: [{ id: 'b', type: 'text', style: { position: 'absolute', right: 5 } }],
|
||||
} as any,
|
||||
],
|
||||
};
|
||||
const node = root.items[0]!.items![0] as MNode;
|
||||
const style = editor.change2Fixed(node, buildNodePath(root, node.id));
|
||||
expect(style.right).toBe(15);
|
||||
expect(style.left).toBeUndefined();
|
||||
});
|
||||
|
||||
test('节点以 bottom 锚定时累加路径上的 bottom', () => {
|
||||
const root: MApp = {
|
||||
id: 'app',
|
||||
type: NodeType.ROOT,
|
||||
items: [
|
||||
{
|
||||
id: 'p1',
|
||||
type: NodeType.PAGE,
|
||||
style: { bottom: 20 },
|
||||
items: [{ id: 'b', type: 'text', style: { position: 'absolute', bottom: 5 } }],
|
||||
} as any,
|
||||
],
|
||||
};
|
||||
const node = root.items[0]!.items![0] as MNode;
|
||||
const style = editor.change2Fixed(node, buildNodePath(root, node.id));
|
||||
expect(style.bottom).toBe(25);
|
||||
expect(style.top).toBeUndefined();
|
||||
});
|
||||
|
||||
test('right: 0 不被误判为未设置 right,仍走 right 分支', () => {
|
||||
const root: MApp = {
|
||||
id: 'app',
|
||||
type: NodeType.ROOT,
|
||||
items: [
|
||||
{
|
||||
id: 'p1',
|
||||
type: NodeType.PAGE,
|
||||
style: { left: 10, top: 20 },
|
||||
items: [{ id: 'b', type: 'text', style: { position: 'absolute', right: 0 } }],
|
||||
} as any,
|
||||
],
|
||||
};
|
||||
const node = root.items[0]!.items![0] as MNode;
|
||||
const style = editor.change2Fixed(node, buildNodePath(root, node.id));
|
||||
// 父节点为 left 锚定,无法换算 right,放弃补偿,保持 right: 0 且不写入 left
|
||||
expect(style.left).toBeUndefined();
|
||||
expect(style.right).toBe(0);
|
||||
});
|
||||
|
||||
test('Fixed2Other 以 right 锚定时抵消路径上的 right', async () => {
|
||||
const root: MApp = {
|
||||
id: 'app',
|
||||
type: NodeType.ROOT,
|
||||
items: [
|
||||
{
|
||||
id: 'p1',
|
||||
type: NodeType.PAGE,
|
||||
style: { right: 10 },
|
||||
items: [{ id: 'b', type: 'text', style: { position: 'absolute', right: 15 } }],
|
||||
} as any,
|
||||
],
|
||||
};
|
||||
const node = root.items[0]!.items![0] as MNode;
|
||||
const style = await editor.Fixed2Other(node, buildNodePath(root, node.id), async () => Layout.ABSOLUTE);
|
||||
expect(style.position).toBe('absolute');
|
||||
expect(style.right).toBe(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('补充:classifyDragSources 同父容器索引异常', () => {
|
||||
@ -1255,7 +1394,7 @@ describe('toggleFixedPosition', () => {
|
||||
const result = await editor.toggleFixedPosition(
|
||||
{ id: 'a', type: 'text', style: { position: 'absolute', top: 0, left: 0 } } as any,
|
||||
{ id: 'a', type: 'text', style: { position: 'fixed' } } as any,
|
||||
root,
|
||||
[...buildNodePath(root, 'a')],
|
||||
async () => Layout.ABSOLUTE,
|
||||
);
|
||||
expect(result.style?.position).toBeDefined();
|
||||
@ -1277,7 +1416,7 @@ describe('toggleFixedPosition', () => {
|
||||
const result = await editor.toggleFixedPosition(
|
||||
{ id: 'a', type: 'text', style: { position: 'fixed', left: 0, top: 0 } } as any,
|
||||
{ id: 'a', type: 'text', style: { position: 'absolute' } } as any,
|
||||
root,
|
||||
buildNodePath(root, 'a'),
|
||||
async () => Layout.ABSOLUTE,
|
||||
);
|
||||
expect(result.style?.position).toBe('fixed');
|
||||
|
||||
@ -121,6 +121,7 @@ export const getNodeInfo = (id: Id, root: { id: Id; items?: MNode[] } | null, sk
|
||||
node: null,
|
||||
parent: null,
|
||||
page: null,
|
||||
path: [],
|
||||
};
|
||||
|
||||
if (!root) return info;
|
||||
@ -131,6 +132,7 @@ export const getNodeInfo = (id: Id, root: { id: Id; items?: MNode[] } | null, sk
|
||||
}
|
||||
|
||||
const path = getNodePath(id, root.items, skip);
|
||||
info.path = path;
|
||||
|
||||
if (!path.length) return info;
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user