tandem-front-end
Version:
Visual editor for web components
1,235 lines (1,100 loc) • 28.9 kB
text/typescript
import {
arraySplice,
Directory,
memoize,
EMPTY_ARRAY,
StructReference,
Point,
Translate,
Bounds,
pointIntersectsBounds,
getSmallestBounds,
mergeBounds,
Bounded,
Struct,
getTreeNodeIdMap,
getNestedTreeNodeById,
boundsFromRect,
getFileFromUri,
File,
updateNestedNode,
FileAttributeNames,
isDirectory,
getParentTreeNode,
TreeNode,
getBoundsSize,
centerTransformZoom,
createZeroBounds,
getTreeNodeHeight,
flattenTreeNode,
shiftBounds,
shiftPoint,
flipPoint,
moveBounds,
FSItem,
FSItemNamespaces,
getTreeNodePath,
updateNestedNodeTrail,
getTreeNodeFromPath
} from "tandem-common";
import {
SyntheticVisibleNode,
PCEditorState,
getSyntheticDocumentByDependencyUri,
getSyntheticSourceNode,
getPCNodeDependency,
findRootInstanceOfPCNode,
getSyntheticNodeById,
getSyntheticVisibleNodeDocument,
updateSyntheticVisibleNode,
Frame,
getSyntheticDocumentDependencyUri,
getSyntheticVisibleNodeRelativeBounds,
updateDependencyGraph,
updateSyntheticVisibleNodeMetadata,
isSyntheticVisibleNodeMovable,
isSyntheticVisibleNodeResizable,
updateFrame,
diffSyntheticNode,
SyntheticOperationalTransformType,
SyntheticInsertChildOperationalTransform,
PCSourceTagNames,
patchSyntheticNode,
getSyntheticDocumentById,
SyntheticDocument,
updateSyntheticDocument,
getFrameByContentNodeId,
getFramesByDependencyUri,
isPaperclipUri,
PCVisibleNode
} from "paperclip";
import {
CanvasToolOverlayMouseMoved,
CanvasToolOverlayClicked
} from "../actions";
import { uniq, pull, values, clamp } from "lodash";
import { stat } from "fs";
import {
replaceDependency,
PCDependency,
Dependency,
DependencyGraph,
getModifiedDependencies
} from "paperclip";
import { FSSandboxRootState } from "fsbox";
export enum ToolType {
TEXT,
POINTER,
COMPONENT,
ELEMENT
}
export enum FrameMode {
PREVIEW = "preview",
DESIGN = "design"
}
export const REGISTERED_COMPONENT = "REGISTERED_COMPONENT";
export enum SyntheticVisibleNodeMetadataKeys {
EDITING_LABEL = "editingLabel",
EXPANDED = "expanded"
}
export type RegisteredComponent = {
uri?: string;
tagName: string;
template: TreeNode<any>;
};
export type Canvas = {
backgroundColor: string;
panning?: boolean;
translate: Translate;
};
export enum InsertFileType {
FILE,
DIRECTORY
}
export type InsertFileInfo = {
type: InsertFileType;
directoryId: string;
};
export type DependencyHistory = {
index: number;
snapshots: Dependency<any>[];
};
export type GraphHistory = {
[identifier: string]: DependencyHistory;
};
export type Editor = {
canvas: Canvas;
};
export type EditorWindow = {
tabUris: string[];
activeFilePath?: string;
mousePosition?: Point;
movingOrResizing?: boolean;
smooth?: boolean;
secondarySelection?: boolean;
fullScreen?: boolean;
container?: HTMLElement;
};
export type RootState = {
editorWindows: EditorWindow[];
mount: Element;
openFiles: OpenFile[];
toolType?: ToolType;
activeEditorFilePath?: string;
// TODO - should be actual instances for type safety
hoveringNodeIds: string[];
// TODO - this should be actual node instances
selectedNodeIds: string[];
selectedFileNodeIds: string[];
selectedComponentVariantName?: string;
projectDirectory?: Directory;
insertFileInfo?: InsertFileInfo;
history: GraphHistory;
showQuickSearch?: boolean;
selectedComponentId: string;
} & PCEditorState &
FSSandboxRootState;
// TODO - change this to Editor
export type OpenFile = {
temporary: boolean;
newContent?: Buffer;
uri: string;
canvas: Canvas;
};
export const updateRootState = (
properties: Partial<RootState>,
root: RootState
) => ({
...root,
...properties
});
export const deselectRootProjectFiles = (state: RootState) =>
updateRootState(
{
selectedFileNodeIds: []
},
state
);
export const persistRootState = (
persistPaperclipState: (state: RootState) => RootState,
state: RootState,
newSelectionScope?: SyntheticVisibleNode | SyntheticDocument
) => {
const oldState = state;
const oldGraph = state.graph;
state = keepActiveFileOpen(
updateRootState(persistPaperclipState(state), state)
);
const modifiedDeps = getModifiedDependencies(state.graph, oldGraph);
state = addHistory(state, modifiedDeps.map(dep => oldGraph[dep.uri]));
state = modifiedDeps.reduce(
(state, dep: Dependency<any>) => setOpenFileContent(dep, state),
state
);
return state;
};
const getUpdatedSyntheticVisibleNodes = (
newState: RootState,
oldState: RootState,
scope: SyntheticVisibleNode | SyntheticDocument
) => {
const MAX_DEPTH = 0;
const oldScope = getSyntheticNodeById(scope.id, oldState.documents);
const newScope = getSyntheticNodeById(scope.id, newState.documents);
let newSyntheticVisibleNodes: SyntheticVisibleNode[] = [];
let model = oldScope;
diffSyntheticNode(oldScope, newScope).forEach(ot => {
const target = getTreeNodeFromPath(ot.nodePath, model);
model = patchSyntheticNode([ot], model);
if (ot.nodePath.length > MAX_DEPTH) {
return;
}
// TODO - will need to check if new parent is not in an instance of a component.
// Will also need to consider child overrides though.
if (ot.type === SyntheticOperationalTransformType.INSERT_CHILD) {
newSyntheticVisibleNodes.push(ot.child);
} else if (
ot.type === SyntheticOperationalTransformType.SET_PROPERTY &&
ot.name === "source"
) {
newSyntheticVisibleNodes.push(target);
}
});
return uniq(newSyntheticVisibleNodes);
};
export const selectInsertedSyntheticVisibleNodes = (
oldState: RootState,
newState: RootState,
scope: SyntheticVisibleNode | SyntheticDocument
) => {
return setSelectedSyntheticVisibleNodeIds(
newState,
...getUpdatedSyntheticVisibleNodes(newState, oldState, scope).map(
node => node.id
)
);
};
const setOpenFileContent = (dep: Dependency<any>, state: RootState) =>
updateOpenFile(
{
temporary: false,
newContent: new Buffer(JSON.stringify(dep.content, null, 2), "utf8")
},
dep.uri,
state
);
const addHistory = (root: RootState, modifiedDeps: Dependency<any>[]) => {
return modifiedDeps.reduce((state, dep) => {
const history: DependencyHistory = state.history[dep.uri] || {
index: 0,
snapshots: EMPTY_ARRAY
};
const snapshots = [...history.snapshots.slice(0, history.index), dep];
return updateRootState(
{
history: {
[dep.uri]: {
index: snapshots.length,
snapshots
}
}
},
state
);
}, root);
};
const moveDependencyRecordHistory = (
uri: string,
pos: number,
root: RootState
): RootState => {
const record = root.history[uri];
if (!record) {
return root;
}
const index = Math.max(
0,
Math.min(record.snapshots.length, record.index + pos)
);
// if index exceeds snapshot count, then we're at the end.
const dep = record.snapshots[index] || root.graph[uri];
root = updateRootState(
{
history: {
[uri]: {
...record,
index
}
},
selectedFileNodeIds: [],
selectedNodeIds: [],
hoveringNodeIds: []
},
root
);
root = setOpenFileContent(dep, root);
root = replaceDependency(dep, root);
return root;
};
const DEFAULT_CANVAS: Canvas = {
backgroundColor: "#EEE",
translate: {
left: 0,
top: 0,
zoom: 1
}
};
export const undo = (root: RootState) =>
root.editorWindows.reduce(
(state, editor) =>
moveDependencyRecordHistory(editor.activeFilePath, -1, root),
root
);
export const redo = (root: RootState) =>
root.editorWindows.reduce(
(state, editor) =>
moveDependencyRecordHistory(editor.activeFilePath, 1, root),
root
);
export const getOpenFile = (uri: string, state: RootState) =>
state.openFiles.find(openFile => openFile.uri === uri);
export const getOpenFilesWithContent = (state: RootState) =>
state.openFiles.filter(openFile => openFile.newContent);
export const updateOpenFileContent = (
uri: string,
newContent: Buffer,
state: RootState
) => {
return updateOpenFile(
{
temporary: false,
newContent
},
uri,
state
);
};
export const getActiveEditorWindow = (state: RootState) =>
getEditorWithActiveFileUri(state.activeEditorFilePath, state);
export const updateOpenFile = (
properties: Partial<OpenFile>,
uri: string,
state: RootState
) => {
const file = getOpenFile(uri, state);
if (!file) {
state = addOpenFile(uri, false, state);
return updateOpenFile(properties, uri, state);
}
const index = state.openFiles.indexOf(file);
return updateRootState(
{
openFiles: arraySplice(state.openFiles, index, 1, {
...file,
...properties
})
},
state
);
};
export const upsertOpenFile = (
uri: string,
temporary: boolean,
state: RootState
): RootState => {
const file = getOpenFile(uri, state);
if (file) {
if (file.temporary !== temporary) {
return updateOpenFile({ temporary }, uri, state);
}
return state;
}
return addOpenFile(uri, temporary, state);
};
export const getEditorWindowWithFileUri = (
uri: string,
state: RootState
): EditorWindow => {
return state.editorWindows.find(window => window.tabUris.indexOf(uri) !== -1);
};
export const getEditorWithActiveFileUri = (
uri: string,
state: RootState
): EditorWindow => {
return state.editorWindows.find(editor => editor.activeFilePath === uri);
};
export const openSecondEditor = (uri: string, state: RootState) => {
const editor = getEditorWindowWithFileUri(uri, state);
const i = state.editorWindows.indexOf(editor);
if (i === 1) {
return openEditorFileUri(uri, state);
}
if (i === 0 && editor.tabUris.length === 1) {
return state;
}
const newTabUris = arraySplice(
editor.tabUris,
editor.tabUris.indexOf(uri),
1
);
state = {
...state,
editorWindows: arraySplice(state.editorWindows, i, 1, {
...editor,
tabUris: newTabUris,
activeFilePath:
editor.activeFilePath === uri
? newTabUris[newTabUris.length - 1]
: editor.activeFilePath
})
};
if (state.editorWindows.length === 1) {
state = {
...state,
editorWindows: [
...state.editorWindows,
{ tabUris: [], activeFilePath: null }
]
};
}
const secondEditor = state.editorWindows[1];
return {
...state,
editors: arraySplice(
state.editorWindows,
state.editorWindows.indexOf(secondEditor),
1,
{
...secondEditor,
tabUris: [...secondEditor.tabUris, uri],
activeFilePath: uri
}
)
};
};
export const getSyntheticWindowBounds = memoize(
(uri: string, state: RootState) => {
const frames = getFramesByDependencyUri(
uri,
state.frames,
state.documents,
state.graph
);
if (!window) return createZeroBounds();
return mergeBounds(...(frames || EMPTY_ARRAY).map(frame => frame.bounds));
}
);
export const isImageMimetype = (mimeType: string) => /^image\//.test(mimeType);
export const openEditorFileUri = (uri: string, state: RootState): RootState => {
const editor =
getEditorWindowWithFileUri(uri, state) || state.editorWindows[0];
return {
...state,
hoveringNodeIds: [],
selectedNodeIds: [],
activeEditorFilePath: uri,
editorWindows: editor
? arraySplice(
state.editorWindows,
state.editorWindows.indexOf(editor),
1,
{
...editor,
tabUris:
editor.tabUris.indexOf(uri) === -1
? [...editor.tabUris, uri]
: editor.tabUris,
activeFilePath: uri
}
)
: [
{
tabUris: [uri],
activeFilePath: uri
}
]
};
};
const queuePreview = (uri: string, state: RootState): RootState => {
return state;
};
export const shiftActiveEditorTab = (
delta: number,
state: RootState
): RootState => {
const editor = getActiveEditorWindow(state);
// nothing open
if (!editor) {
return state;
}
const index = editor.tabUris.indexOf(editor.activeFilePath);
let newIndex = index + delta;
if (newIndex < 0) {
newIndex = editor.tabUris.length + delta;
} else if (newIndex >= editor.tabUris.length) {
newIndex = -1 + delta;
}
newIndex = clamp(newIndex, 0, editor.tabUris.length - 1);
return openEditorFileUri(editor.tabUris[newIndex], state);
};
const removeEditorWindow = (
{ activeFilePath }: EditorWindow,
state: RootState
): RootState => {
const editor = getEditorWithActiveFileUri(activeFilePath, state);
return {
...state,
editorWindows: arraySplice(
state.editorWindows,
state.editorWindows.indexOf(editor),
1
)
};
};
export const closeFile = (uri: string, state: RootState): RootState => {
const editorWindow = getEditorWindowWithFileUri(uri, state);
if (editorWindow.tabUris.length === 1) {
state = removeEditorWindow(editorWindow, state);
} else {
state = updateEditorWindow(
{
tabUris: editorWindow.tabUris.filter(furi => furi !== uri)
},
uri,
state
);
}
state = updateRootState(
{
openFiles: state.openFiles.filter(openFile => openFile.uri !== uri)
},
state
);
state = setNextOpenFile(state);
return state;
};
export const setNextOpenFile = (state: RootState): RootState => {
const hasOpenFile = state.openFiles.find(openFile =>
Boolean(getEditorWithActiveFileUri(openFile.uri, state))
);
if (hasOpenFile) {
return state;
}
state = {
...state,
hoveringNodeIds: [],
selectedNodeIds: []
};
if (state.openFiles.length) {
state = openEditorFileUri(state.openFiles[0].uri, state);
}
return state;
};
export const removeTemporaryOpenFiles = (state: RootState) => {
return {
...state,
openFiles: state.openFiles.filter(openFile => !openFile.temporary)
};
};
export const openSyntheticVisibleNodeOriginFile = (
node: SyntheticVisibleNode,
state: RootState
) => {
const sourceNode = getSyntheticSourceNode(
node as SyntheticVisibleNode,
state.graph
) as PCVisibleNode;
const uri = getPCNodeDependency(sourceNode.id, state.graph).uri;
state = openEditorFileUri(uri, state);
const instance = findRootInstanceOfPCNode(sourceNode, state.documents);
state = setActiveFilePath(uri, state);
state = setSelectedSyntheticVisibleNodeIds(state, instance.id);
return state;
};
export const addOpenFile = (
uri: string,
temporary: boolean,
state: RootState
): RootState => {
const file = getOpenFile(uri, state);
if (file) {
return state;
}
state = removeTemporaryOpenFiles(state);
return {
...state,
openFiles: [
...state.openFiles,
{
uri,
temporary,
canvas: DEFAULT_CANVAS
}
]
};
};
// export const getInsertedWindowElementIds = (
// oldWindow: SyntheticWindow,
// targetFrameId: string,
// newBrowser: PCEditorState
// ): string[] => {
// const elementIds = oldWindow.documents
// .filter(document => !targetFrameId || document.id === targetFrameId)
// .reduce((nodeIds, oldFrame) => {
// return [
// ...nodeIds,
// ...getInsertedFrameElementIds(oldFrame, newBrowser)
// ];
// }, []);
// const newWindow = newBrowser.windows.find(
// window => window.location === oldWindow.location
// );
// return [
// ...elementIds,
// ...newWindow.documents
// .filter(document => {
// const isInserted =
// oldWindow.documents.find(oldFrame => {
// return oldFrame.id === document.id;
// }) == null;
// return isInserted;
// })
// .map(document => document.root.id)
// ];
// };
// export const getInsertedFrameElementIds = (
// oldFrame: Frame,
// newBrowser: PCEditorState
// ): string[] => {
// const newFrame = getFrameById(oldFrame.id, newBrowser);
// if (!newFrame) {
// return [];
// }
// const oldIds = Object.keys(oldFrame.nativeNodeMap);
// const newIds = Object.keys(newFrame.nativeNodeMap);
// return pull(newIds, ...oldIds);
// };
export const keepActiveFileOpen = (state: RootState): RootState => {
return {
...state,
openFiles: state.openFiles.map(openFile => ({
...openFile,
temporary: false
}))
};
};
// export const updateRootStateSyntheticWindowFrame = (
// documentId: string,
// properties: Partial<Frame>,
// root: RootState
// ) => {
// const window = getFrameWindow(documentId, root);
// const document = getFrameById(documentId, root);
// return updateRootState(
// {
// browser: updateSyntheticWindow(
// window.location,
// {
// documents: arraySplice(
// window.documents,
// window.documents.indexOf(document),
// 1,
// {
// ...document,
// ...properties
// }
// )
// },
// root
// )
// },
// root
// );
// };
export const setRootStateSyntheticVisibleNodeExpanded = (
nodeId: string,
value: boolean,
state: RootState
) => {
const node = getSyntheticNodeById(nodeId, state.documents);
const document = getSyntheticVisibleNodeDocument(node.id, state.documents);
state = updateSyntheticDocument(
setSyntheticVisibleNodeExpanded(node, value, document),
document,
state
);
return state;
};
const setSyntheticVisibleNodeExpanded = (
node: SyntheticVisibleNode,
value: boolean,
document: SyntheticDocument
): SyntheticVisibleNode => {
const path = getTreeNodePath(node.id, document);
const updater = (node: SyntheticVisibleNode) => {
return {
...node,
metadata: {
...node.metadata,
[SyntheticVisibleNodeMetadataKeys.EXPANDED]: value
}
};
};
return (value
? updateNestedNodeTrail(path, document, updater)
: updateNestedNode(node, document, updater)) as SyntheticVisibleNode;
};
export const setRootStateSyntheticVisibleNodeLabelEditing = (
nodeId: string,
value: boolean,
state: RootState
) => {
const node = getSyntheticNodeById(nodeId, state.documents);
const document = getSyntheticVisibleNodeDocument(node.id, state.documents);
state = updateSyntheticDocument(
updateSyntheticVisibleNodeMetadata(
{
[SyntheticVisibleNodeMetadataKeys.EDITING_LABEL]: value
},
node,
document
),
document,
state
);
return state;
};
export const setRootStateFileNodeExpanded = (
nodeId: string,
value: boolean,
state: RootState
) => {
return updateRootState(
{
projectDirectory: updateNestedNode(
getNestedTreeNodeById(nodeId, state.projectDirectory),
state.projectDirectory,
(child: FSItem) => ({
...child,
expanded: value
})
)
},
state
);
};
export const updateEditorWindow = (
properties: Partial<EditorWindow>,
uri: string,
root: RootState
) => {
const window = getEditorWindowWithFileUri(uri, root);
const i = root.editorWindows.indexOf(window);
return updateRootState(
{
editorWindows: arraySplice(root.editorWindows, i, 1, {
...window,
...properties
})
},
root
);
};
const INITIAL_ZOOM_PADDING = 50;
export const centerEditorCanvas = (
state: RootState,
editorFileUri: string,
innerBounds?: Bounds,
smooth: boolean = false,
zoomOrZoomToFit: boolean | number = true
) => {
if (!innerBounds) {
const frames = getFramesByDependencyUri(
editorFileUri,
state.frames,
state.documents,
state.graph
);
if (!frames.length) {
return state;
}
innerBounds = getSyntheticWindowBounds(editorFileUri, state);
}
// no windows loaded
if (
innerBounds.left +
innerBounds.right +
innerBounds.top +
innerBounds.bottom ===
0
) {
console.warn(` Cannot center when bounds has no size`);
return updateOpenFileCanvas(
{
translate: { left: 0, top: 0, zoom: 1 }
},
editorFileUri,
state
);
}
const editorWindow = getEditorWindowWithFileUri(editorFileUri, state);
const openFile = getOpenFile(editorFileUri, state);
const { container } = editorWindow;
if (!container) {
console.warn("cannot center canvas without a container");
return state;
}
const {
canvas: { translate }
} = openFile;
const { width, height } = container.getBoundingClientRect();
const innerSize = getBoundsSize(innerBounds);
const centered = {
left: -innerBounds.left + width / 2 - innerSize.width / 2,
top: -innerBounds.top + height / 2 - innerSize.height / 2
};
const scale =
typeof zoomOrZoomToFit === "boolean"
? Math.min(
(width - INITIAL_ZOOM_PADDING) / innerSize.width,
(height - INITIAL_ZOOM_PADDING) / innerSize.height
)
: typeof zoomOrZoomToFit === "number"
? zoomOrZoomToFit
: translate.zoom;
state = updateEditorWindow(
{
smooth
},
editorFileUri,
state
);
state = updateOpenFileCanvas(
{
translate: centerTransformZoom(
{
...centered,
zoom: 1
},
{ left: 0, top: 0, right: width, bottom: height },
Math.min(scale, 1)
)
},
editorFileUri,
state
);
return state;
};
export const setActiveFilePath = (
newActiveFilePath: string,
root: RootState
) => {
if (getEditorWithActiveFileUri(newActiveFilePath, root)) {
return root;
}
root = openEditorFileUri(newActiveFilePath, root);
root = addOpenFile(newActiveFilePath, true, root);
root = centerEditorCanvas(root, newActiveFilePath);
return root;
};
export const updateOpenFileCanvas = (
properties: Partial<Canvas>,
uri: string,
root: RootState
) => {
const openFile = getOpenFile(uri, root);
return updateOpenFile(
{
canvas: {
...openFile.canvas,
...properties
}
},
uri,
root
);
};
export const setInsertFile = (type: InsertFileType, state: RootState) => {
const file = getNestedTreeNodeById(
state.selectedFileNodeIds[0] || state.projectDirectory.id,
state.projectDirectory
);
return updateRootState(
{
insertFileInfo: {
type,
directoryId: isDirectory(file)
? file.id
: getParentTreeNode(file.id, state.projectDirectory).id
}
},
state
);
};
export const setTool = (toolType: ToolType, root: RootState) => {
if (!root.editorWindows.length) {
return root;
}
root = { ...root, selectedComponentId: null };
root = updateRootState({ toolType }, root);
root = setSelectedSyntheticVisibleNodeIds(root);
return root;
};
export const getActiveFrames = (root: RootState): Frame[] =>
values(root.frames).filter(frame =>
root.editorWindows.some(
editor =>
editor.activeFilePath ===
getSyntheticDocumentDependencyUri(
getSyntheticVisibleNodeDocument(frame.contentNodeId, root.documents),
root.graph
)
)
);
export const getCanvasTranslate = (canvas: Canvas) => canvas.translate;
export const getScaledMouseCanvasPosition = (
state: RootState,
point: Point
) => {
const canvas = getOpenFile(state.activeEditorFilePath, state).canvas;
const translate = getCanvasTranslate(canvas);
const scaledPageX = (point.left - translate.left) / translate.zoom;
const scaledPageY = (point.top - translate.top) / translate.zoom;
return { left: scaledPageX, top: scaledPageY };
};
export const getCanvasMouseTargetNodeId = (
state: RootState,
event: CanvasToolOverlayMouseMoved | CanvasToolOverlayClicked,
filter?: (node: TreeNode<any>) => boolean
): string => {
return getCanvasMouseTargetNodeIdFromPoint(
state,
{
left: event.sourceEvent.pageX,
top: event.sourceEvent.pageY
},
filter
);
};
export const getCanvasMouseTargetNodeIdFromPoint = (
state: RootState,
point: Point,
filter?: (node: TreeNode<any>) => boolean
): string => {
const editor = getActiveEditorWindow(state);
const canvas = getOpenFile(editor.activeFilePath, state).canvas;
const translate = getCanvasTranslate(canvas);
const toolType = state.toolType;
const scaledMousePos = getScaledMouseCanvasPosition(state, point);
const frame = getFrameFromPoint(scaledMousePos, state);
if (!frame) return null;
const contentNode = getSyntheticNodeById(
frame.contentNodeId,
state.documents
);
const { left: scaledPageX, top: scaledPageY } = scaledMousePos;
const deadZone = getSelectionBounds(state);
const mouseX = scaledPageX - frame.bounds.left;
const mouseY = scaledPageY - frame.bounds.top;
const computedInfo = frame.computed || {};
const intersectingBounds: Bounds[] = [];
const intersectingBoundsMap = new Map<Bounds, string>();
const mouseFramePoint = { left: mouseX, top: mouseY };
for (const $id in computedInfo) {
const { bounds } = computedInfo[$id];
if (
pointIntersectsBounds(mouseFramePoint, bounds) &&
!pointIntersectsBounds(scaledMousePos, deadZone) &&
(toolType == null ||
getNestedTreeNodeById($id, contentNode).name !==
PCSourceTagNames.TEXT) &&
(!filter || filter(getNestedTreeNodeById($id, contentNode)))
) {
intersectingBounds.push(bounds);
intersectingBoundsMap.set(bounds, $id);
}
}
if (!intersectingBounds.length) return null;
const smallestBounds = getSmallestBounds(...intersectingBounds);
return intersectingBoundsMap.get(smallestBounds);
};
export const getCanvasMouseFrame = (
state: RootState,
event: CanvasToolOverlayMouseMoved | CanvasToolOverlayClicked
) => {
return getFrameFromPoint(
getScaledMouseCanvasPosition(state, {
left: event.sourceEvent.pageX,
top: event.sourceEvent.pageY
}),
state
);
};
export const getFrameFromPoint = (point: Point, state: RootState) => {
const activeFrames = getActiveFrames(state);
if (!activeFrames.length) return null;
for (let j = activeFrames.length; j--; ) {
const frame = activeFrames[j];
if (pointIntersectsBounds(point, frame.bounds)) {
return frame;
}
}
};
export const setSelectedSyntheticVisibleNodeIds = (
root: RootState,
...selectionIds: string[]
) => {
const nodeIds = uniq([...selectionIds]).filter(Boolean);
root = nodeIds.reduce(
(state, nodeId) =>
setRootStateSyntheticVisibleNodeExpanded(nodeId, true, root),
root
);
root = updateRootState(
{
selectedNodeIds: nodeIds
},
root
);
return root;
};
export const setSelectedFileNodeIds = (
root: RootState,
...selectionIds: string[]
) => {
const nodeIds = uniq([...selectionIds]);
root = nodeIds.reduce(
(state, nodeId) => setRootStateFileNodeExpanded(nodeId, true, root),
root
);
root = updateRootState(
{
selectedFileNodeIds: nodeIds
},
root
);
return root;
};
export const setHoveringSyntheticVisibleNodeIds = (
root: RootState,
...selectionIds: string[]
) => {
return updateRootState(
{
hoveringNodeIds: uniq([...selectionIds])
},
root
);
};
// export const updateRootSyntheticPosition = (
// position: Point,
// nodeId: string,
// root: RootState
// ) =>
// updateRootState(
// {
// browser: updateSyntheticItemPosition(position, nodeId, root)
// },
// root
// );
// export const updateRootSyntheticBounds = (
// bounds: Bounds,
// nodeId: string,
// root: RootState
// ) =>
// updateRootState(
// {
// browser: updateSyntheticItemBounds(bounds, nodeId, root)
// },
// root
// );
export const getBoundedSelection = memoize((root: RootState): string[] =>
root.selectedNodeIds.filter(nodeId =>
getSyntheticVisibleNodeRelativeBounds(
getSyntheticNodeById(nodeId, root.documents),
root.frames
)
)
);
export const getSelectionBounds = memoize((root: RootState) =>
mergeBounds(
...getBoundedSelection(root).map(nodeId =>
getSyntheticVisibleNodeRelativeBounds(
getSyntheticNodeById(nodeId, root.documents),
root.frames
)
)
)
);
export const isSelectionMovable = memoize((root: RootState) => {
return !root.selectedNodeIds.some(nodeId => {
const node = getSyntheticNodeById(nodeId, root.documents);
return !isSyntheticVisibleNodeMovable(node);
});
});
export const isSelectionResizable = memoize((root: RootState) => {
return !root.selectedNodeIds.some(nodeId => {
const node = getSyntheticNodeById(nodeId, root.documents);
return !isSyntheticVisibleNodeResizable(node);
});
});