UNPKG

tandem-front-end

Version:

Visual editor for web components

1,857 lines (1,719 loc) 93.7 kB
// behold the ~~blob~~ import { Action } from "redux"; import * as path from "path"; import { CanvasToolArtboardTitleClicked, NEW_FILE_ADDED, CANVAS_TOOL_ARTBOARD_TITLE_CLICKED, CanvasToolOverlayMouseMoved, PROJECT_DIRECTORY_LOADED, ProjectDirectoryLoaded, FILE_NAVIGATOR_ITEM_CLICKED, FileNavigatorItemClicked, CANVAS_WHEEL, CANVAS_MOUSE_MOVED, CANVAS_MOUNTED, CANVAS_MOUSE_CLICKED, CanvasToolOverlayClicked, VariantClicked, ResizerMoved, RESIZER_MOVED, RESIZER_PATH_MOUSE_STOPPED_MOVING, RESIZER_STOPPED_MOVING, ResizerPathStoppedMoving, RESIZER_PATH_MOUSE_MOVED, ResizerPathMoved, SHORTCUT_ESCAPE_KEY_DOWN, INSERT_TOOL_FINISHED, InsertToolFinished, SHORTCUT_DELETE_KEY_DOWN, CANVAS_TOOL_WINDOW_BACKGROUND_CLICKED, SYNTHETIC_NODES_PASTED, SyntheticVisibleNodesPasted, FILE_NAVIGATOR_ITEM_DOUBLE_CLICKED, OPEN_FILE_ITEM_CLICKED, OPEN_FILE_ITEM_CLOSE_CLICKED, OpenFilesItemClick, SAVED_FILE, SavedFile, SAVED_ALL_FILES, RAW_CSS_TEXT_CHANGED, RawCSSTextChanged, FILE_NAVIGATOR_TOGGLE_DIRECTORY_CLICKED, NewFileAdded, FILE_NAVIGATOR_DROPPED_ITEM, FileNavigatorDroppedItem, SHORTCUT_UNDO_KEY_DOWN, SHORTCUT_REDO_KEY_DOWN, TEXT_VALUE_CHANGED, TextValueChanged, ElementTypeChanged, SHORTCUT_QUICK_SEARCH_KEY_DOWN, QUICK_SEARCH_ITEM_CLICKED, QuickSearchItemClicked, QUICK_SEARCH_BACKGROUND_CLICK, COMPONENT_VARIANT_NAME_CLICKED, NEW_STYLE_VARIANT_CONFIRMED, ComponentVariantNameClicked, EDITOR_TAB_CLICKED, EditorTabClicked, CanvasWheel, SHORTCUT_ZOOM_IN_KEY_DOWN, SHORTCUT_ZOOM_OUT_KEY_DOWN, CanvasMounted, CANVAS_DROPPED_ITEM, CanvasDroppedItem, CANVAS_DRAGGED_OVER, SHORTCUT_CONVERT_TO_COMPONENT_KEY_DOWN, SHORTCUT_T_KEY_DOWN, SHORTCUT_R_KEY_DOWN, CanvasDraggingOver, ELEMENT_TYPE_CHANGED, CSS_PROPERTY_CHANGED, CSS_PROPERTY_CHANGE_COMPLETED, ATTRIBUTE_CHANGED, CSSPropertyChanged, FRAME_MODE_CHANGE_COMPLETE, FrameModeChangeComplete, TOOLBAR_TOOL_CLICKED, ToolbarToolClicked, EDITOR_TAB_CLOSE_BUTTON_CLICKED, STYLE_VARIANT_DROPDOWN_CHANGED, NEW_STYLE_VARIANT_BUTTON_CLICKED, StyleVariantDropdownChanged, REMOVE_STYLE_BUTTON_CLICKED, SHORTCUT_SELECT_NEXT_TAB, SHORTCUT_SELECT_PREVIOUS_TAB, SHORTCUT_CLOSE_CURRENT_TAB, COMPONENT_PICKER_BACKGROUND_CLICK, ComponentPickerItemClick, COMPONENT_PICKER_ITEM_CLICK, SHORTCUT_C_KEY_DOWN, ADD_VARIANT_BUTTON_CLICKED, VARIANT_DEFAULT_SWITCH_CLICKED, VariantDefaultSwitchClicked, VariantLabelChanged, VARIANT_LABEL_CHANGED, REMOVE_VARIANT_BUTTON_CLICKED, RemoveVariantTriggerClicked, COMPONENT_INSTANCE_VARIANT_TOGGLED, INSTANCE_VARIANT_RESET_CLICKED, SHORTCUT_TOGGLE_SIDEBAR, INHERIT_PANE_ADD_BUTTON_CLICK, INHERIT_PANE_REMOVE_BUTTON_CLICK, INHERIT_ITEM_COMPONENT_TYPE_CHANGE_COMPLETE, InheritItemComponentTypeChangeComplete, CANVAS_MOUSE_DOUBLE_CLICKED, CanvasMouseMoved, ComponentControllerItemClicked, COMPONENT_CONTROLLER_PICKED, ComponentControllerPicked, SHORTCUT_WRAP_IN_SLOT_KEY_DOWN, REMOVE_COMPONENT_CONTROLLER_BUTTON_CLICKED, SOURCE_INSPECTOR_LAYER_CLICKED, InspectorLayerEvent, SOURCE_INSPECTOR_LAYER_ARROW_CLICKED, SOURCE_INSPECTOR_LAYER_LABEL_CHANGED, InspectorLayerLabelChanged, SOURCE_INSPECTOR_LAYER_DROPPED, SourceInspectorLayerDropped, RemoveComponentControllerButtonClicked, InheritPaneRemoveButtonClick, PromptConfirmed, PROMPT_CANCEL_BUTTON_CLICKED, EDIT_VARIANT_NAME_CONFIRMED, EDIT_VARIANT_NAME_BUTTON_CLICKED, ADD_VARIABLE_BUTTON_CLICKED, VARIABLE_LABEL_CHANGE_COMPLETED, VARIABLE_VALUE_CHANGED, VariablePropertyChanged, AddVariableButtonClicked, VARIABLE_VALUE_CHANGE_COMPLETED, PROJECT_INFO_LOADED, ProjectInfoLoaded, PROJECT_DIRECTORY_DIR_LOADED, ProjectDirectoryDirLoaded, FILE_NAVIGATOR_BASENAME_CHANGED, FileNavigatorBasenameChanged, QUICK_SEARCH_RESULT_LOADED, QuickSearchFilterChanged, QUICK_SEARCH_FILTER_CHANGED, QuickSearchResultLoaded, FRAME_BOUNDS_CHANGE_COMPLETED, FRAME_BOUNDS_CHANGED, FrameBoundsChanged, ACTIVE_EDITOR_URI_DIRS_LOADED, FILE_ITEM_CONTEXT_MENU_RENAME_CLICKED, FileItemContextMenuAction, FILE_NAVIGATOR_ITEM_BLURRED, SYNTHETIC_NODE_CONTEXT_MENU_CONVERT_TO_COMPONENT_CLICKED, SYNTHETIC_NODE_CONTEXT_MENU_WRAP_IN_ELEMENT_CLICKED, SyntheticNodeContextMenuAction, SYNTHETIC_NODE_CONTEXT_MENU_WRAP_IN_SLOT_CLICKED, SYNTHETIC_NODE_CONTEXT_MENU_SELECT_PARENT_CLICKED, SYNTHETIC_NODE_CONTEXT_MENU_SELECT_SOURCE_NODE_CLICKED, SYNTHETIC_NODE_CONTEXT_MENU_REMOVE_CLICKED, CSS_RESET_PROPERTY_OPTION_CLICKED, ResetPropertyOptionClicked, EXPORT_NAME_CHANGED, SYNTHETIC_NODE_CONTEXT_MENU_CONVERT_TO_STYLE_MIXIN_CLICKED, QUICK_SEARCH_RESULT_ITEM_SPLIT_BUTTON_CLICKED, QuickSearchResultItemSplitButtonClicked, ModuleContextMenuOptionClicked, EDITOR_TAB_CONTEXT_MENU_OPEN_IN_BOTTOM_OPTION_CLICKED, OPEN_CONTROLLER_BUTTON_CLICKED, IMAGE_SOURCE_INPUT_CHANGED, ImageSourceInputChanged, IMAGE_PATH_PICKED, DIRECTORY_PATH_PICKED, DirectoryPathPicked, ImagePathPicked, CSS_INHERITED_FROM_LABEL_CLICKED, CSSInheritedFromLabelClicked, SYNTHETIC_NODE_CONTEXT_MENU_CONVERT_TEXT_STYLES_TO_MIXIN_CLICKED, SYNTHETIC_NODE_CONTEXT_MENU_RENAME_CLICKED, PC_LAYER_DOUBLE_CLICKED, PCLayerRightClicked, SYNTHETIC_NODE_CONTEXT_MENU_SHOW_IN_CANVAS_CLICKED, CANVAS_TEXT_EDIT_CHANGE_COMPLETE, CanvasTextEditChangeComplete, ADD_VARIANT_TRIGGER_CLICKED, VARIANT_TRIGGER_SOURCE_CHANGED, VARIANT_TRIGGER_TARGET_CHANGED, VariantTriggerSourceChanged, VariantTriggerTargetChanged, REMOVE_VARIANT_TRIGGER_CLICKED, ADD_QUERY_BUTTON_CLICKED, AddQueryButtonClicked, QUERY_LABEL_CHANGED, QUERY_CONDITION_CHANGED, QueryConditionChanged, QueryLabelChanged, QUERY_TYPE_CHANGED, BreadCrumbClicked, BREAD_CRUMB_CLICKED, VARIABLE_QUERY_SOURCE_VARIABLE_CHANGE, VariableQuerySourceVariableChange, QueryTypeChanged, CSS_PROPERTIES_CHANGE_COMPLETED, CSSPropertiesChanged, CSS_PROPERTIES_CHANGED, BUILD_BUTTON_CONFIGURE_CLICKED, CONFIGURE_BUILD_MODAL_BACKGROUND_CLICKED, CONFIGURE_BUILD_MODAL_X_CLICKED, SCRIPT_PROCESS_STARTED, SCRIPT_PROCESS_LOGGED, SCRIPT_PROCESS_CLOSED, ScriptProcessStarted, ScriptProcessLogged, BUILD_SCRIPT_STARTED, BuildScriptStarted, SHORTCUT_TOGGLE_PANEL, CLOSE_BOTTOM_GUTTER_BUTTON_CLICKED, BUILD_SCRIPT_CONFIG_CHANGED, OPEN_APP_SCRIPT_CONFIG_CHANGED, ScriptConfigChanged, BUILD_BUTTON_STOP_CLICKED, UNLOADING, UNLOADER_CREATED, UNLOADER_COMPLETED, UnloaderAction, QUICK_SEARCH_INPUT_ENTERED, MODULE_CONTEXT_MENU_CLOSE_OPTION_CLICKED } from "../actions"; import { queueOpenFile, fsSandboxReducer, isImageUri, FS_SANDBOX_ITEM_LOADED, FSSandboxItemLoaded, isSvgUri, FILE_CHANGED, FileChanged, FileChangedEventType } from "fsbox"; import { RootState, updateRootState, updateOpenFileCanvas, getCanvasMouseTargetNodeId, setSelectedInspectorNodes, getSelectionBounds, getBoundedSelection, ToolType, setTool, persistRootState, getOpenFile, openFile, removeTemporaryOpenFiles, setNextOpenFile, updateOpenFile, deselectRootProjectFiles, setHoveringSyntheticVisibleNodeIds, setSelectedFileNodeIds, undo, redo, getEditorWithActiveFileUri, getActiveEditorWindow, getEditorWindowWithFileUri, updateEditorWindow, centerEditorCanvas, getCanvasMouseTargetNodeIdFromPoint, isSelectionMovable, selectInsertedSyntheticVisibleNodes, RegisteredComponent, closeFile, shiftActiveEditorTab, confirm, ConfirmType, openSyntheticVisibleNodeOriginFile, updateSourceInspectorNode, getInsertableSourceNodeScope, getInsertableSourceNodeFromSyntheticNode, getCanvasMouseTargetInspectorNode, setHoveringInspectorNodes, refreshModuleInspectorNodes, teeHistory, pruneOpenFiles, QuickSearchResultType, getGlobalFileUri, setRootStateFileNodeExpanded, centerEditorCanvasOrLater, EditMode, updateProjectScripts, removeBuildScriptProcess, getBuildScriptProcess, RootReadyType, IS_WINDOWS } from "../state"; import { PCSourceTagNames, PCVisibleNode, paperclipReducer, SyntheticElement, createPCElement, createPCTextNode, getSyntheticVisibleNodeRelativeBounds, getSyntheticVisibleNodeDocument, getSyntheticSourceNode, getSyntheticNodeById, SyntheticVisibleNode, getPCNodeDependency, updateSyntheticVisibleNodePosition, updateSyntheticVisibleNodeBounds, persistInsertNode, persistChangeLabel, persistSyntheticVisibleNodeBounds, persistRemoveInspectorNode, getSyntheticNodeSourceDependency, persistConvertNodeToComponent, persistMoveSyntheticVisibleNode, persistAppendPCClips, persistChangeSyntheticTextNodeValue, persistRawCSSText, updatePCNodeMetadata, PCVisibleNodeMetadataKey, getSyntheticDocumentByDependencyUri, getFrameSyntheticNode, SyntheticDocument, PC_DEPENDENCY_GRAPH_LOADED, SYNTHETIC_DOCUMENT_NODE_NAME, DEFAULT_FRAME_BOUNDS, persistChangeElementType, persistAddComponentController, persistRemoveComponentController, PC_RUNTIME_EVALUATED, persistCSSProperty, persistCSSProperties, persistAttribute, getPCNode, persistSyntheticNodeMetadata, createPCComponentInstance, getSyntheticVisibleNodeFrame, persistAddVariant, persistUpdateVariable, persistUpdateVariant, persistRemoveVariant, SyntheticInstanceElement, persistToggleInstanceVariant, persistRemoveVariantOverride, getPCVariants, persistStyleMixin, persistStyleMixinComponentId, isPaperclipUri, syntheticNodeIsInShadow, getSyntheticInstancePath, PCComponentInstanceElement, persistWrapInSlot, getPCNodeModule, getPCNodeContentNode, PCNode, isPCPlug, createPCPlug, persistRemovePCNode, getInstanceSlotContent, SyntheticNode, PCSlot, DependencyGraph, canRemovePCNode, isVisibleNode, persistInspectorNodeStyle, isSyntheticContentNode, getSyntheticDocumentDependencyUri, PCComponent, persistAddVariable, PCVariableType, createRootInspectorNode, xmlToPCNode, PCElement, getInspectorInstanceShadowContentNode, getInspectorInstanceShadow, persistAddQuery, isTextLikePCNode, PCQueryType, persistReplacePCNode, getDerrivedPCLabel, persistConvertInspectorNodeStyleToMixin, getSyntheticSourceUri, persistUpdateVariantTrigger, inspectorNodeInInstanceOfComponent, getInspectorNodeBySourceNodeId, persistAddVariantTrigger, PCVariableQuery } from "paperclip"; import { roundBounds, scaleInnerBounds, moveBounds, keepBoundsAspectRatio, keepBoundsCenter, Bounds, shiftBounds, isDirectory, updateNestedNode, Directory, isFile, getParentTreeNode, appendChildNode, removeNestedTreeNode, boundsFromRect, centerTransformZoom, Translate, zoomBounds, TreeMoveOffset, shiftPoint, Point, zoomPoint, cloneTreeNode, FSItemTagNames, FSItem, getFileFromUri, createFile, createDirectory, sortFSItems, EMPTY_OBJECT, getNestedTreeNodeById, EMPTY_ARRAY, mergeFSItems, stripProtocol, addProtocol, arraySplice, FILE_PROTOCOL, updateFSItemAlts } from "tandem-common"; import { clamp, last } from "lodash"; import { expandInspectorNode, collapseInspectorNode, getInspectorSyntheticNode, isInspectorNode, getInspectorSourceNode, InspectorTreeNodeName, InspectorNode, inspectorNodeInShadow, getSyntheticInspectorNode } from "paperclip"; const ZOOM_SENSITIVITY = IS_WINDOWS ? 2500 : 250; const PAN_X_SENSITIVITY = IS_WINDOWS ? 0.05 : 1; const PAN_Y_SENSITIVITY = IS_WINDOWS ? 0.05 : 1; const MIN_ZOOM = 0.02; const MAX_ZOOM = 6400 / 100; const MAX_LOGS = 100; export const rootReducer = (state: RootState, action: Action): RootState => { state = fsSandboxReducer(state, action); state = paperclipReducer(state, action); state = canvasReducer(state, action); state = shortcutReducer(state, action); state = clipboardReducer(state, action); switch (action.type) { case PROJECT_DIRECTORY_LOADED: { const { directory } = action as ProjectDirectoryLoaded; return updateRootState( { projectDirectory: directory, readyType: RootReadyType.LOADED }, state ); } case PROJECT_INFO_LOADED: { const { info: projectInfo } = action as ProjectInfoLoaded; // check if there's just a simple config change. If so, then just change config info if ( state.projectInfo && state.projectInfo.path === projectInfo.path && state.projectInfo.config.globalFilePath === projectInfo.config.globalFilePath && state.projectInfo.config.mainFilePath === projectInfo.config.mainFilePath && state.projectInfo.config.rootDir === state.projectInfo.config.rootDir ) { state = updateRootState( { projectInfo }, state ); } else { state = updateRootState( { projectInfo, readyType: RootReadyType.LOADED, openFiles: [], fileCache: {}, openedMain: false, sourceNodeInspector: createRootInspectorNode(), projectDirectory: null, graph: {}, documents: [], frames: [], editorWindows: [] }, state ); } const buildProcess = getBuildScriptProcess(state); if (buildProcess) { if ( buildProcess.script !== (state.projectInfo.config.scripts && state.projectInfo.config.scripts.build) ) { state = removeBuildScriptProcess(state); } } return state; } case FILE_NAVIGATOR_ITEM_CLICKED: { const { node } = action as FileNavigatorItemClicked; const uri = node.uri; state = setSelectedFileNodeIds(state, node.id); state = setFileExpanded(node, true, state); if (!isDirectory(node)) { state = openFile(uri, true, false, state); return state; } return state; } case QUICK_SEARCH_INPUT_ENTERED: case QUICK_SEARCH_ITEM_CLICKED: { const { item } = action as QuickSearchItemClicked; if (item.type === QuickSearchResultType.URI) { const uri = item.uri; state = openFile(uri, false, false, state); state = updateRootState({ showQuickSearch: false }, state); } else { } return state; } case QUICK_SEARCH_BACKGROUND_CLICK: { return (state = updateRootState({ showQuickSearch: false }, state)); } case FILE_NAVIGATOR_TOGGLE_DIRECTORY_CLICKED: { const { node } = action as FileNavigatorItemClicked; state = setFileExpanded(node, !node.expanded, state); return state; } case PROJECT_DIRECTORY_DIR_LOADED: { const { items } = action as ProjectDirectoryDirLoaded; const { projectDirectory } = state; state = updateRootState( { projectDirectory: projectDirectory ? mergeFSItems(projectDirectory, ...items) : mergeFSItems(...items) }, state ); return state; } case ACTIVE_EDITOR_URI_DIRS_LOADED: { state = setRootStateFileNodeExpanded( getFileFromUri(state.activeEditorFilePath, state.projectDirectory).id, true, state ); return state; } case FILE_CHANGED: { const { eventType, uri }: FileChanged = action as FileChanged; if ( eventType === FileChangedEventType.ADD || eventType === FileChangedEventType.ADD_DIR ) { const existing = getFileFromUri(uri, state.projectDirectory); if (existing) { return state; } if (eventType === FileChangedEventType.ADD_DIR) { state = updateRootState( { projectDirectory: mergeFSItems( createDirectory(uri), state.projectDirectory ) }, state ); } else if (eventType === FileChangedEventType.ADD) { const file = createFile(uri); const projectDirectory = mergeFSItems(file, state.projectDirectory); state = updateRootState( { projectDirectory }, state ); // refresh in case of PC file opened state = refreshModuleInspectorNodes(state); } } else if ( eventType === FileChangedEventType.UNLINK || eventType === FileChangedEventType.UNLINK_DIR ) { const fsItem = getFileFromUri(uri, state.projectDirectory); let fileCache = state.fileCache; let graph = state.graph; // ick -- these files shouldn't be if (fileCache[uri]) { fileCache = { ...fileCache }; delete fileCache[uri]; } if (graph[uri]) { graph = { ...graph }; delete graph[uri]; } // TODO - check for renamed file state = updateRootState( { selectedFileNodeIds: [], fileCache, graph, projectDirectory: fsItem ? updateFSItemAlts( removeNestedTreeNode(fsItem, state.projectDirectory) ) : state.projectDirectory }, state ); state = pruneOpenFiles(state); } return state; } case FILE_NAVIGATOR_BASENAME_CHANGED: { const { item, basename }: FileNavigatorBasenameChanged = action as FileNavigatorBasenameChanged; const updatedItem = { ...item, uri: addProtocol( FILE_PROTOCOL, path.join(path.dirname(stripProtocol(item.uri)), basename) ) }; state = { ...state, editingBasenameUri: null }; const existingItem = getFileFromUri( updatedItem.uri, state.projectDirectory ); // directory expanded so we can safely dispatch alert here if (existingItem) { return confirm( `The name "${basename}" is already taken. Please choose a different name.`, ConfirmType.ERROR, state ); } let projectDirectory = removeNestedTreeNode(item, state.projectDirectory); projectDirectory = mergeFSItems(updatedItem, projectDirectory); state = updateRootState({ projectDirectory }, state); // TODO - this also needs to work with directories const editorWindow = getEditorWindowWithFileUri(item.uri, state); if (editorWindow) { let graph = { ...state.graph }; let fileCache = { ...state.fileCache }; graph[updatedItem.uri] = graph[item.uri]; fileCache[updatedItem.uri] = fileCache[item.uri]; delete graph[item.uri]; delete fileCache[item.uri]; state = updateEditorWindow( { tabUris: editorWindow.tabUris.map(uri => { return item.uri === uri ? updatedItem.uri : uri; }), activeFilePath: editorWindow.activeFilePath === item.uri ? updatedItem.uri : editorWindow.activeFilePath }, item.uri, state ); state = updateRootState( { graph, fileCache, activeEditorFilePath: state.activeEditorFilePath === item.uri ? updatedItem.uri : state.activeEditorFilePath, openFiles: state.openFiles.map(openFile => ({ ...openFile, uri: openFile.uri === item.uri ? updatedItem.uri : openFile.uri })) }, state ); } return state; } case QUICK_SEARCH_RESULT_LOADED: { const { matches } = action as QuickSearchResultLoaded; state = updateRootState( { quickSearch: { ...state.quickSearch, matches: [...state.quickSearch.matches, ...matches].sort((a, b) => { return a.label < b.label ? -1 : 1; }) } }, state ); return state; } case QUICK_SEARCH_FILTER_CHANGED: { const { value } = action as QuickSearchFilterChanged; state = updateRootState( { quickSearch: { filter: value, matches: [] } }, state ); return state; } case FILE_ITEM_CONTEXT_MENU_RENAME_CLICKED: { const { item: { uri } } = action as FileItemContextMenuAction; state = { ...state, editingBasenameUri: uri }; return state; } case FILE_NAVIGATOR_ITEM_DOUBLE_CLICKED: { const { node: { uri } } = action as FileNavigatorItemClicked; state = { ...state, editingBasenameUri: uri }; return state; } case FILE_NAVIGATOR_ITEM_BLURRED: { state = { ...state, editingBasenameUri: null }; return state; } case CANVAS_MOUNTED: { const { fileUri, element } = action as CanvasMounted; if (!element) { return state; } state = updateEditorWindow( { container: element }, fileUri, state ); const selectedNode = state.selectedInspectorNodes[0]; if (selectedNode) { const document = getSyntheticDocumentByDependencyUri( fileUri, state.documents, state.graph ); if (getNestedTreeNodeById(selectedNode.id, document)) { return centerEditorCanvas( state, fileUri, getSelectionBounds( state.selectedInspectorNodes, state.documents, state.frames, state.graph ) ); } } else { return centerEditorCanvasOrLater(state, fileUri); } } case BUILD_BUTTON_CONFIGURE_CLICKED: { state = { ...state, showConfigureBuildModal: true }; return state; } case CONFIGURE_BUILD_MODAL_X_CLICKED: case CONFIGURE_BUILD_MODAL_BACKGROUND_CLICKED: { state = { ...state, showConfigureBuildModal: false }; return state; } case FILE_NAVIGATOR_DROPPED_ITEM: { const { node, targetNode } = action as FileNavigatorDroppedItem; const parent: Directory = getParentTreeNode( node.id, state.projectDirectory ); const parentUri = parent.uri; const nodeUri = node.uri; state = updateRootState( { projectDirectory: updateNestedNode( parent, state.projectDirectory, parent => removeNestedTreeNode(node, parent) ) }, state ); const targetDir: Directory = targetNode.name !== FSItemTagNames.FILE ? targetNode : getParentTreeNode(targetNode.id, state.projectDirectory); const targetUri = targetDir.uri; state = updateRootState( { projectDirectory: updateNestedNode( targetDir, state.projectDirectory, targetNode => { targetNode = appendChildNode( { ...node, uri: nodeUri.replace(parentUri, targetUri) } as FSItem, targetNode ); targetNode = { ...targetNode, children: sortFSItems(targetNode.children as FSItem[]) }; return targetNode; } ) }, state ); return state; } case NEW_FILE_ADDED: { const { uri, fileType } = action as NewFileAdded; const directory = getFileFromUri( path.dirname(uri), state.projectDirectory ); state = updateRootState( { projectDirectory: updateFSItemAlts( updateNestedNode(directory, state.projectDirectory, dir => { return { ...dir, children: sortFSItems([ ...dir.children, fileType === FSItemTagNames.FILE ? createFile(uri) : createDirectory(uri) ]) }; }) ) }, state ); if (fileType === FSItemTagNames.FILE && isPaperclipUri(uri)) { state = openFile(uri, false, false, state); state = { ...state, recenterUriAfterEvaluation: uri }; } return state; } case FS_SANDBOX_ITEM_LOADED: { const { content, uri } = action as FSSandboxItemLoaded; if (state.queuedDndInfo) { const { item, point, editorUri } = state.queuedDndInfo; return handleLoadedDroppedItem( item, point, editorUri, { ...state, queuedDndInfo: null }, content ); } state = refreshModuleInspectorNodes(state); return state; } case OPEN_FILE_ITEM_CLICKED: { const { uri, sourceEvent } = action as OpenFilesItemClick; if (getEditorWithActiveFileUri(uri, state)) { return state; } state = setNextOpenFile( removeTemporaryOpenFiles(openFile(uri, false, false, state)) ); return state; } case SAVED_FILE: { const { uri } = action as SavedFile; return updateOpenFile({ newContent: null }, uri, state); } case SAVED_ALL_FILES: { return updateRootState( { openFiles: state.openFiles.map(openFile => ({ ...openFile, newContent: null })) }, state ); } case COMPONENT_VARIANT_NAME_CLICKED: { const { name } = action as ComponentVariantNameClicked; state = updateRootState({ selectedComponentVariantName: name }, state); return state; } case SOURCE_INSPECTOR_LAYER_DROPPED: { const { source, target, offset } = action as SourceInspectorLayerDropped; if (!target || source.id === target.id) { return state; } const sourceNode = getInspectorSourceNode( source, state.sourceNodeInspector, state.graph ); let targetNode: PCNode; let targetInspectorNode: InspectorNode; if (target.name === InspectorTreeNodeName.CONTENT) { const parent = (targetInspectorNode = getParentTreeNode( target.id, state.sourceNodeInspector )); let parentSourceNode = getInspectorSourceNode( parent, state.sourceNodeInspector, state.graph ) as PCComponentInstanceElement; let contentNode = getInstanceSlotContent( target.sourceSlotNodeId, parentSourceNode ); if (!contentNode) { state = persistInsertNode( createPCPlug(target.sourceSlotNodeId), parentSourceNode, TreeMoveOffset.APPEND, state ); parentSourceNode = getInspectorSourceNode( parent, state.sourceNodeInspector, state.graph ) as PCComponentInstanceElement; contentNode = getInstanceSlotContent( target.sourceSlotNodeId, parentSourceNode ); } targetNode = contentNode; } else { targetInspectorNode = target; targetNode = getInspectorSourceNode( target, state.sourceNodeInspector, state.graph ); } const oldState = state; state = persistRootState( state => persistMoveSyntheticVisibleNode( sourceNode, targetNode, offset, state ), state ); // this does _NOT_ work for inspector nodes like plugs & slots that // have no assoc synthetic node. // const targetSyntheticNode = getInspectorSyntheticNode( // targetInspectorNode, // state.documents, // state.graph // ); // const document = getSyntheticVisibleNodeDocument( // targetSyntheticNode.id, // state.documents // ); // const mutatedTarget = // offset === TreeMoveOffset.APPEND || offset === TreeMoveOffset.PREPEND // ? targetSyntheticNode // : getParentTreeNode(targetSyntheticNode.id, document); // state = queueSelectInsertedSyntheticVisibleNodes( // oldState, // state, // mutatedTarget // ); return state; } case SOURCE_INSPECTOR_LAYER_CLICKED: { const { node } = action as InspectorLayerEvent; state = selectInspectorNode(node, state); return state; } case SOURCE_INSPECTOR_LAYER_ARROW_CLICKED: { const { node } = action as InspectorLayerEvent; state = updateSourceInspectorNode(state, sourceNodeInspector => { return node.expanded ? collapseInspectorNode(node, sourceNodeInspector) : expandInspectorNode(node, sourceNodeInspector); }); return state; } case SOURCE_INSPECTOR_LAYER_LABEL_CHANGED: { const { node, label } = action as InspectorLayerLabelChanged; state = { ...state, renameInspectorNodeId: null }; state = persistRootState( browser => persistChangeLabel( label, getInspectorSourceNode( node, state.sourceNodeInspector, browser.graph ), browser ), state ); return state; } case OPEN_FILE_ITEM_CLOSE_CLICKED: { // TODO - flag confirm remove state const { uri } = action as OpenFilesItemClick; return closeFile(uri, state); } case EDITOR_TAB_CLICKED: { const { uri } = action as EditorTabClicked; return openFile(uri, false, false, state); } case EDITOR_TAB_CLOSE_BUTTON_CLICKED: { const { uri } = action as EditorTabClicked; return closeFile(uri, state); } case MODULE_CONTEXT_MENU_CLOSE_OPTION_CLICKED: { const { uri } = action as ModuleContextMenuOptionClicked; return closeFile(uri, state); } case EDITOR_TAB_CONTEXT_MENU_OPEN_IN_BOTTOM_OPTION_CLICKED: { const { uri } = action as ModuleContextMenuOptionClicked; state = openFile(uri, false, true, state); return state; } case PC_DEPENDENCY_GRAPH_LOADED: { state = centerEditorCanvasOrLater(state, state.activeEditorFilePath); return state; } } return state; }; export const canvasReducer = (state: RootState, action: Action) => { switch (action.type) { case RESIZER_MOVED: { const { point: newPoint } = action as ResizerMoved; state = updateEditorWindow( { movingOrResizing: true }, state.activeEditorFilePath, state ); if ( isSelectionMovable( state.selectedInspectorNodes, state.sourceNodeInspector, state.graph ) ) { const selectionBounds = getSelectionBounds( state.selectedInspectorNodes, state.documents, state.frames, state.graph ); let movedBounds = moveBounds(selectionBounds, newPoint); for (const node of state.selectedInspectorNodes) { const syntheticNode = getInspectorSyntheticNode( node, state.documents ); const itemBounds = getSyntheticVisibleNodeRelativeBounds( syntheticNode, state.frames, state.graph ); const newBounds = roundBounds( scaleInnerBounds(itemBounds, selectionBounds, movedBounds) ); state = updateSyntheticVisibleNodePosition( newBounds, syntheticNode, state ); } } return state; } case FRAME_BOUNDS_CHANGED: { const { newBounds } = action as FrameBoundsChanged; state = persistSyntheticNodeMetadata( { [PCVisibleNodeMetadataKey.BOUNDS]: newBounds }, getInspectorSyntheticNode( state.selectedInspectorNodes[0], state.documents ), state ); return state; } case FRAME_BOUNDS_CHANGE_COMPLETED: { const { newBounds } = action as FrameBoundsChanged; state = persistRootState(state => { state = persistSyntheticNodeMetadata( { [PCVisibleNodeMetadataKey.BOUNDS]: newBounds }, getInspectorSyntheticNode( state.selectedInspectorNodes[0], state.documents ), state ); return state; }, state); return state; } case COMPONENT_PICKER_BACKGROUND_CLICK: { return setTool(null, state); } case COMPONENT_PICKER_ITEM_CLICK: { const { component } = action as ComponentPickerItemClick; return { ...state, selectedComponentId: component.id }; } case TOOLBAR_TOOL_CLICKED: { const { toolType } = action as ToolbarToolClicked; if (toolType === ToolType.POINTER) { state = setTool(null, state); } else { state = setTool(toolType, state); } return state; } case ADD_VARIANT_TRIGGER_CLICKED: { state = persistRootState(state => { state = persistAddVariantTrigger( state.selectedInspectorNodes[0], state ); return state; }, state); return state; } case QUERY_CONDITION_CHANGED: { const { target, condition } = action as QueryConditionChanged; state = persistRootState(state => { state = persistReplacePCNode( { ...target, condition: condition && { ...(target.condition || EMPTY_OBJECT), ...condition } }, target, state ); return state; }, state); return state; } case QUERY_LABEL_CHANGED: { const { target, label } = action as QueryLabelChanged; state = persistRootState(state => { if (!label) { state = persistRemovePCNode(target, state); } else { state = persistReplacePCNode( { ...target, label }, target, state ); } return state; }, state); return state; } case QUERY_TYPE_CHANGED: { const { target, newType } = action as QueryTypeChanged; state = persistRootState(state => { state = persistReplacePCNode( { ...target, type: newType, condition: null } as any, target, state ); return state; }, state); return state; } case VARIABLE_QUERY_SOURCE_VARIABLE_CHANGE: { const { variable, query } = action as VariableQuerySourceVariableChange; state = persistRootState(state => { state = persistReplacePCNode( { ...(query as any), sourceVariableId: variable.id } as PCVariableQuery, query, state ); return state; }, state); return state; } case ADD_QUERY_BUTTON_CLICKED: { const { queryType } = action as AddQueryButtonClicked; const globalFileUri = getGlobalFileUri(state.projectInfo); const globalDependency = state.graph[globalFileUri]; state = persistRootState(state => { state = persistAddQuery( queryType, EMPTY_OBJECT, null, globalDependency.content, state ); return state; }, state); return state; } case REMOVE_VARIANT_TRIGGER_CLICKED: { const { trigger } = action as RemoveVariantTriggerClicked; state = persistRootState(state => { state = persistRemovePCNode(trigger, state); return state; }, state); return state; } case REMOVE_VARIANT_BUTTON_CLICKED: { const variant = state.selectedVariant; state = persistRootState( state => persistRemoveVariant(variant, state), state ); state = updateRootState({ selectedVariant: null }, state); return state; } case VARIANT_TRIGGER_SOURCE_CHANGED: { const { trigger, value } = action as VariantTriggerSourceChanged; state = persistRootState(state => { state = persistUpdateVariantTrigger( { source: value }, trigger, state ); return state; }, state); return state; } case VARIANT_TRIGGER_TARGET_CHANGED: { const { trigger, value } = action as VariantTriggerTargetChanged; state = persistRootState(state => { state = persistUpdateVariantTrigger( { targetVariantId: value.id }, trigger, state ); return state; }, state); return state; } case VARIANT_DEFAULT_SWITCH_CLICKED: { const { variant } = action as VariantDefaultSwitchClicked; state = persistRootState( state => persistUpdateVariant( { isDefault: !variant.isDefault }, variant, state ), state ); return state; } case VARIANT_LABEL_CHANGED: { const { variant, newLabel } = action as VariantLabelChanged; state = persistRootState( state => persistUpdateVariant({ label: newLabel }, variant, state), state ); return state; } case COMPONENT_INSTANCE_VARIANT_TOGGLED: { const { variant } = action as VariantClicked; const inspectorNode = state.selectedInspectorNodes[0]; state = persistRootState( state => persistToggleInstanceVariant( inspectorNode, variant.id, state.selectedVariant, state ), state ); return state; } case INSTANCE_VARIANT_RESET_CLICKED: { const { variant } = action as VariantClicked; const element = getInspectorSyntheticNode( state.selectedInspectorNodes[0], state.documents ) as SyntheticInstanceElement; state = persistRootState( state => persistRemoveVariantOverride( element, variant.id, state.selectedVariant, state ), state ); return state; } case RESIZER_STOPPED_MOVING: { if ( isSelectionMovable( state.selectedInspectorNodes, state.sourceNodeInspector, state.graph ) ) { state = persistRootState(state => { return state.selectedInspectorNodes.reduce((state, node) => { return persistSyntheticVisibleNodeBounds( getInspectorSyntheticNode(node, state.documents), state ); }, state); }, state); } state = updateEditorWindow( { movingOrResizing: false }, state.activeEditorFilePath, state ); return state; } case CANVAS_WHEEL: { const { metaKey, ctrlKey, deltaX, deltaY, canvasHeight, canvasWidth } = action as CanvasWheel; let delta2X = deltaX * PAN_X_SENSITIVITY; let delta2Y = deltaY * PAN_Y_SENSITIVITY; const editorWindow = getActiveEditorWindow(state); const openFile = getOpenFile( editorWindow.activeFilePath, state.openFiles ); let translate = openFile.canvas.translate; if (metaKey || ctrlKey) { translate = centerTransformZoom( translate, boundsFromRect({ width: canvasWidth, height: canvasHeight }), clamp( translate.zoom + (translate.zoom * deltaY) / ZOOM_SENSITIVITY, MIN_ZOOM, MAX_ZOOM ), editorWindow.mousePosition ); } else { translate = { ...translate, left: translate.left - delta2X, top: translate.top - delta2Y }; } state = updateEditorWindow( { smooth: false }, editorWindow.activeFilePath, state ); state = updateOpenFileCanvas( { translate }, editorWindow.activeFilePath, state ); return state; } case CANVAS_DROPPED_ITEM: { let { item, point, editorUri } = action as CanvasDroppedItem; if (isFile(item)) { return queueOpenFile(item.uri, { ...(state as any), queuedDndInfo: action }); } else { return handleLoadedDroppedItem(item, point, editorUri, state); } } case SHORTCUT_ZOOM_IN_KEY_DOWN: { const editor = getActiveEditorWindow(state); const openFile = getOpenFile(editor.activeFilePath, state.openFiles); state = setCanvasZoom( normalizeZoom(openFile.canvas.translate.zoom) * 2, false, editor.activeFilePath, state ); return state; } case SHORTCUT_TOGGLE_SIDEBAR: { state = { ...state, showSidebar: state.showSidebar !== false ? false : true }; return state; } case BUILD_BUTTON_STOP_CLICKED: { state = removeBuildScriptProcess(state); return state; } case BUILD_SCRIPT_CONFIG_CHANGED: { const { script } = action as ScriptConfigChanged; state = updateProjectScripts( { build: script }, state ); state = removeBuildScriptProcess(state); return state; } case OPEN_APP_SCRIPT_CONFIG_CHANGED: { const { script } = action as ScriptConfigChanged; state = updateProjectScripts( { openApp: script }, state ); return state; } case CLOSE_BOTTOM_GUTTER_BUTTON_CLICKED: case SHORTCUT_TOGGLE_PANEL: { state = { ...state, showBottomGutter: state.showBottomGutter ? false : true }; return state; } case SHORTCUT_ZOOM_OUT_KEY_DOWN: { const editor = getActiveEditorWindow(state); const openFile = getOpenFile(editor.activeFilePath, state.openFiles); state = setCanvasZoom( normalizeZoom(openFile.canvas.translate.zoom) / 2, false, editor.activeFilePath, state ); return state; } case SHORTCUT_SELECT_NEXT_TAB: { return shiftActiveEditorTab(1, state); } case SHORTCUT_SELECT_PREVIOUS_TAB: { return shiftActiveEditorTab(-1, state); } case SHORTCUT_CLOSE_CURRENT_TAB: { return closeFile(state.activeEditorFilePath, state); } case CANVAS_MOUSE_MOVED: { const { editorWindow: { activeFilePath }, sourceEvent: { pageX, pageY } } = action as CanvasMouseMoved; state = updateEditorWindow( { mousePosition: { left: pageX, top: pageY } }, state.activeEditorFilePath, state ); let targetNodeId: string; let targetInspectorNode: InspectorNode; state = updateRootState({ activeEditorFilePath: activeFilePath }, state); const editorWindow = getEditorWindowWithFileUri(activeFilePath, state); if (!editorWindow.movingOrResizing) { if (state.toolType != null) { targetInspectorNode = getCanvasMouseTargetInspectorNode( state, action as CanvasToolOverlayMouseMoved, getInsertFilter(state) ); state = setHoveringInspectorNodes( state, targetInspectorNode ? [targetInspectorNode] : EMPTY_ARRAY ); } else { targetNodeId = getCanvasMouseTargetNodeId( state, (action as CanvasToolOverlayMouseMoved).sourceEvent ); state = setHoveringSyntheticVisibleNodeIds( state, targetNodeId ? [targetNodeId] : EMPTY_ARRAY ); } } return state; } case CANVAS_DRAGGED_OVER: { const { item, offset } = action as CanvasDraggingOver; state = updateEditorWindow( { mousePosition: offset }, state.activeEditorFilePath, state ); // remove selection so that hovering state is visible state = setSelectedInspectorNodes(state); // TODO - in the future, we'll probably want to be able to highlight hovered nodes as the user is moving an element around to indicate where // they can drop the element. let targetNodeId: string; targetNodeId = getCanvasMouseTargetNodeIdFromPoint( state, offset, getDragFilter(item, state) ); state = setHoveringSyntheticVisibleNodeIds( state, targetNodeId ? [targetNodeId] : EMPTY_ARRAY ); return state; } case CANVAS_MOUSE_CLICKED: { return handleCanvasMouseClicked( state, action as CanvasToolOverlayClicked ); } case CANVAS_MOUSE_DOUBLE_CLICKED: { // looped in since there may be nested shadows const targetNodeId = getCanvasMouseTargetNodeId( state, (action as CanvasToolOverlayMouseMoved).sourceEvent ); if (!targetNodeId) { return state; } const syntheticNode = getSyntheticNodeById(targetNodeId, state.documents); const sourceNode = getSyntheticSourceNode(syntheticNode, state.graph); if ( sourceNode.name === PCSourceTagNames.COMPONENT_INSTANCE || sourceNode.name === PCSourceTagNames.COMPONENT ) { const document = getSyntheticVisibleNodeDocument( syntheticNode.id, state.documents ); const inspectorNode = getSyntheticInspectorNode( syntheticNode, document, state.sourceNodeInspector, state.graph ); let currentInstance = inspectorNode; // break past nested shadows while (currentInstance) { const inspectorShadowNode = getInspectorInstanceShadow( currentInstance ); if (!inspectorShadowNode) { break; } state = { ...state, selectedInspectorNodes: [inspectorShadowNode] }; state = handleCanvasMouseClicked( state, action as CanvasToolOverlayClicked ); if ( state.selectedInspectorNodes.length && state.selectedInspectorNodes[0].id !== inspectorNode.id ) { break; } currentInstance = inspectorShadowNode; } } else if (isTextLikePCNode(sourceNode)) { state = { ...state, editMode: EditMode.SECONDARY }; } return state; } case ADD_VARIABLE_BUTTON_CLICKED: { const { variableType: type } = action as AddVariableButtonClicked; const globalFileUri = getGlobalFileUri(state.projectInfo); const globalDependency = state.graph[globalFileUri]; state = persistRootState(state => { state = persistAddVariable( null, type, "", globalDependency.content, state ); return state; }, state); return state; } case VARIABLE_LABEL_CHANGE_COMPLETED: { const { variable, value: label } = action as VariablePropertyChanged; state = persistRootState(state => { if (!label) { return persistRemovePCNode(variable, state); } state = persistUpdateVariable({ label }, variable, state); return state; }, state); return state; } case VARIABLE_VALUE_CHANGED: { const { variable, value } = action as VariablePropertyChanged; state = teeHistory(state); state = persistUpdateVariable({ value }, variable, state); return state; } case VARIABLE_VALUE_CHANGE_COMPLETED: { const { variable, value } = action as VariablePropertyChanged; state = persistRootState(state => { state = persistUpdateVariable({ value }, variable, state); return state; }, state); return state; } case RESIZER_PATH_MOUSE_MOVED: { state = updateEditorWindow( { movingOrResizing: true }, state.activeEditorFilePath, state ); // TODO - possibly use BoundsStruct instead of Bounds since there are cases where bounds prop doesn't exist const newBounds = getResizeActionBounds(action as ResizerPathMoved); for (const node of getBoundedSelection( state.selectedInspectorNodes, state.documents, state.frames, state.graph )) { state = updateSyntheticVisibleNodeBounds( getNewSyntheticVisibleNodeBounds( newBounds, getInspectorSyntheticNode(node, state.documents), state ), getInspectorSyntheticNode(node, state.documents), state ); } return state; } case RESIZER_PATH_MOUSE_STOPPED_MOVING: { state = updateEditorWindow( { movingOrResizing: false }, state.activeEditorFilePath, state ); // TODO - possibly use BoundsStruct instead of Bounds since there are cases where bounds prop doesn't exist const newBounds = getResizeActionBounds( action as ResizerPathStoppedMoving ); state = persistRootSta