tandem-front-end
Version:
Visual editor for web components
1,857 lines (1,719 loc) • 93.7 kB
text/typescript
// 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