UNPKG

tandem-front-end

Version:

Visual editor for web components

492 lines (464 loc) 15.2 kB
import { fork, put, take, call, spawn, takeEvery, select } from "redux-saga/effects"; import { eventChannel } from "redux-saga"; import { mapKeys } from "lodash"; import { FILE_ITEM_RIGHT_CLICKED, FileItemRightClicked, fileItemContextMenuDeleteClicked, fileItemContextMenuCopyPathClicked, fileItemContextMenuOpenClicked, fileItemContextMenuRenameClicked, CANVAS_RIGHT_CLICKED, PCLayerRightClicked, inspectorNodeContextMenuWrapInSlotClicked, inspectorNodeContextMenuSelectParentClicked, inspectorNodeContextMenuSelectSourceNodeClicked, inspectorNodeContextMenuConvertToComponentClicked, inspectorNodeContextMenuWrapInElementClicked, inspectorNodeContextMenuConvertToStyleMixinClicked, inspectorNodeContextMenuRemoveClicked, EDITOR_TAB_RIGHT_CLICKED, EditorTabClicked, inspectorNodeContextMenuConvertTextStylesToMixinClicked, inspectorNodeContextMenuRenameClicked, inspectorNodeContextMenuShowInCanvasClicked, moduleContextMenuCloseOptionClicked, PC_LAYER_RIGHT_CLICKED, editorTabContextMenuOpenInBottomTabOptionClicked, inspectorNodeContextMenuCopyClicked, inspectorNodeContextMenuPasteClicked, fileItemContextMenuCreateDirectoryClicked, fileItemContextMenuCreateBlankFileClicked, fileItemContextMenuCreateComponentFileClicked } from "../actions"; import { ContextMenuItem, ContextMenuOptionType, ContextMenuOption, RootState, getCanvasMouseTargetNodeIdFromPoint, getCanvasMouseTargetNodeId } from "../state"; import { Point, FSItemTagNames, EMPTY_OBJECT } from "tandem-common"; import { getSyntheticNodeById, getSyntheticSourceNode, PCSourceTagNames, getPCNodeContentNode, getSyntheticInspectorNode, getSyntheticVisibleNodeDocument, syntheticNodeIsInShadow, getPCNodeModule, SyntheticNode, getInspectorSyntheticNode, inspectorNodeInShadow, extendsComponent, hasTextStyles, getInspectorContentNode, SYNTHETIC_DOCUMENT_NODE_NAME, getSyntheticDocumentDependencyUri, SyntheticDocument, getInspectorSourceNode, InspectorTreeNodeName, getPCNode, InspectorNode } from "paperclip"; export type ShortcutSagaOptions = { openContextMenu: (anchor: Point, options: ContextMenuOption[]) => void; }; type openInspectorNodeContextMenuOptions = { showRenameLabelOption?: boolean; }; export const createShortcutSaga = ({ openContextMenu }: ShortcutSagaOptions) => { return function*() { yield takeEvery( FILE_ITEM_RIGHT_CLICKED, function* handleFileItemRightClick({ event, item }: FileItemRightClicked) { yield call( openContextMenu, { left: event.pageX, top: event.pageY }, [ { type: ContextMenuOptionType.GROUP, options: [ { type: ContextMenuOptionType.ITEM, label: "Copy Path", action: fileItemContextMenuCopyPathClicked(item) }, { type: ContextMenuOptionType.ITEM, label: item.name === FSItemTagNames.DIRECTORY ? "Open in Finder" : "Open in Text Editor", action: fileItemContextMenuOpenClicked(item) } ] as ContextMenuItem[] }, { type: ContextMenuOptionType.GROUP, options: [ { type: ContextMenuOptionType.ITEM, label: "Rename", action: fileItemContextMenuRenameClicked(item) }, { type: ContextMenuOptionType.ITEM, label: "Delete", action: fileItemContextMenuDeleteClicked(item) } ] }, item.name === FSItemTagNames.DIRECTORY ? { type: ContextMenuOptionType.GROUP, options: [ { type: ContextMenuOptionType.ITEM, label: "Create Directory", action: fileItemContextMenuCreateDirectoryClicked(item) }, { type: ContextMenuOptionType.ITEM, label: "Create Blank File", action: fileItemContextMenuCreateBlankFileClicked(item) }, { type: ContextMenuOptionType.ITEM, label: "Create Component File", action: fileItemContextMenuCreateComponentFileClicked( item ) } ] } : null ].filter(Boolean) as ContextMenuOption[] ); } ); yield takeEvery( EDITOR_TAB_RIGHT_CLICKED, function* handleEditorRightClicked({ event, uri }: EditorTabClicked) { yield call( handleModuleRightClicked, { left: event.pageX, top: event.pageY }, uri ); } ); function* handleModuleRightClicked(point: Point, uri: string) { yield call(openContextMenu, point, [ { type: ContextMenuOptionType.ITEM, label: "Close", action: moduleContextMenuCloseOptionClicked(uri) }, { type: ContextMenuOptionType.ITEM, label: "Open in Bottom Tab", action: editorTabContextMenuOpenInBottomTabOptionClicked(uri) } ]); } yield takeEvery(CANVAS_RIGHT_CLICKED, function* handleFileItemRightClick({ event, item }: FileItemRightClicked) { const state: RootState = yield select(); const targetNodeId = getCanvasMouseTargetNodeId(state, event); if (targetNodeId) { yield call( openCanvasSyntheticNodeContextMenu, targetNodeId, event, state ); } }); function* openCanvasSyntheticNodeContextMenu( targetNodeId: string, event: React.MouseEvent<any>, state: RootState ) { const ownerWindow = (event.nativeEvent.target as HTMLDivElement) .ownerDocument.defaultView; const parent = ownerWindow.top; const ownerIframe = Array.from( parent.document.querySelectorAll("iframe") ).find((iframe: HTMLIFrameElement) => { return iframe.contentDocument === ownerWindow.document; }); const rect = ownerIframe.getBoundingClientRect(); const syntheticNode = getSyntheticNodeById(targetNodeId, state.documents); const document = getSyntheticVisibleNodeDocument( syntheticNode.id, state.documents ); const inspectorNode = getSyntheticInspectorNode( syntheticNode, document, state.sourceNodeInspector, state.graph ); yield call( openInspectorNodeContextMenu, inspectorNode, { left: event.pageX + rect.left, top: event.pageY + rect.top }, state ); } yield takeEvery(PC_LAYER_RIGHT_CLICKED, function* handleFileItemRightClick({ event, item }: PCLayerRightClicked) { // this will happen for if (!item) { console.warn( `ModuleContextMenuOptionClicked dispatched without an inspectorNode` ); return; } const state: RootState = yield select(); // const node = getInspectorSyntheticNode(item, state.documents); // getInspectorSourceNode(item, if (item.name === InspectorTreeNodeName.SHADOW) { return; } yield call( openInspectorNodeContextMenu, item, { left: event.pageX, top: event.pageY }, state, { showRenameLabelOption: true } ); }); function* openInspectorNodeContextMenu( node: InspectorNode, point: Point, state: RootState, { showRenameLabelOption }: openInspectorNodeContextMenuOptions = EMPTY_OBJECT ) { // const syntheticNode = getSyntheticNodeById(node.id, state.documents); const sourceNode = getInspectorSourceNode( node, state.sourceNodeInspector, state.graph ); const inspectorContentNode = getInspectorContentNode( node, state.sourceNodeInspector ); const contentNode = getPCNodeContentNode( sourceNode.id, getPCNodeModule(sourceNode.id, state.graph) ); const inShadow = inspectorNodeInShadow(node, inspectorContentNode); yield call(openContextMenu, point, [ inShadow ? { type: ContextMenuOptionType.ITEM, label: "Hide", action: inspectorNodeContextMenuRemoveClicked(node) } : { type: ContextMenuOptionType.GROUP, options: [ showRenameLabelOption ? { type: ContextMenuOptionType.ITEM, label: "Rename", action: inspectorNodeContextMenuRenameClicked(node) } : null, { type: ContextMenuOptionType.ITEM, label: "Remove", action: inspectorNodeContextMenuRemoveClicked(node) }, sourceNode.name !== PCSourceTagNames.SLOT ? { type: ContextMenuOptionType.ITEM, label: "Copy", action: inspectorNodeContextMenuCopyClicked(node) } : null, { type: ContextMenuOptionType.ITEM, label: "Paste", action: inspectorNodeContextMenuPasteClicked(node) }, sourceNode.name !== PCSourceTagNames.COMPONENT && !inShadow ? { type: ContextMenuOptionType.ITEM, label: "Convert to Component", action: inspectorNodeContextMenuConvertToComponentClicked( node ) } : null, sourceNode.name !== PCSourceTagNames.COMPONENT && !inShadow ? { type: ContextMenuOptionType.ITEM, label: "Wrap in Element", action: inspectorNodeContextMenuWrapInElementClicked(node) } : null, contentNode.name === PCSourceTagNames.COMPONENT && contentNode.id !== sourceNode.id && !inShadow ? { type: ContextMenuOptionType.ITEM, label: "Wrap in Slot", action: inspectorNodeContextMenuWrapInSlotClicked(node) } : null, sourceNode.name === PCSourceTagNames.COMPONENT || sourceNode.name === PCSourceTagNames.COMPONENT_INSTANCE || sourceNode.name === PCSourceTagNames.ELEMENT || sourceNode.name === PCSourceTagNames.TEXT ? { type: ContextMenuOptionType.ITEM, label: "Move All Styles to Mixin", action: inspectorNodeContextMenuConvertToStyleMixinClicked( node ) } : null, (sourceNode.name === PCSourceTagNames.COMPONENT || sourceNode.name === PCSourceTagNames.COMPONENT_INSTANCE || sourceNode.name === PCSourceTagNames.ELEMENT || sourceNode.name === PCSourceTagNames.TEXT) && hasTextStyles( node, state.sourceNodeInspector, state.selectedVariant, state.graph ) ? { type: ContextMenuOptionType.ITEM, label: "Move Text Styles to Mixin", action: inspectorNodeContextMenuConvertTextStylesToMixinClicked( node ) } : null ].filter(Boolean) as ContextMenuItem[] }, { type: ContextMenuOptionType.GROUP, options: [ contentNode.id !== sourceNode.id ? { type: ContextMenuOptionType.ITEM, label: "Select Parent", action: inspectorNodeContextMenuSelectParentClicked(node) } : null, inShadow || extendsComponent(sourceNode) ? { type: ContextMenuOptionType.ITEM, label: "Select Source Layer", action: inspectorNodeContextMenuSelectSourceNodeClicked(node) } : null, { type: ContextMenuOptionType.ITEM, label: "Center in Canvas", action: inspectorNodeContextMenuShowInCanvasClicked(node) } ].filter(Boolean) as ContextMenuItem[] } ].filter(Boolean) as ContextMenuOption[]); } }; }; export function* shortcutSaga() { // yield fork(handleFileItemRightClick); // yield fork(mapHotkeys({ // // artboard // "a": wrapDispatch(SHORTCUT_A_KEY_DOWN), // // rectangle // "r": wrapDispatch(SHORTCUT_R_KEY_DOWN), // // text // "t": wrapDispatch(SHORTCUT_T_KEY_DOWN), // // artboard // "escape": wrapDispatch(SHORTCUT_ESCAPE_KEY_DOWN), // // artboard // "backspace": wrapDispatch(SHORTCUT_DELETE_KEY_DOWN) // })); } const wrapDispatch = (type: string) => function*(sourceEvent) { // yield put(shortcutKeyDown(type)); }; const mapHotkeys = (map: { [identifier: string]: (event: KeyboardEvent) => any; }) => function*() { const ordererdMap = mapKeys(map, (value: any, key: string) => key .split(" ") .sort() .join(" ") ); const keysDown: string[] = []; const chan = yield eventChannel(emit => { document.addEventListener("keydown", (event: KeyboardEvent) => { if (keysDown.indexOf(event.key) === -1) { keysDown.push(event.key); } const handler = ordererdMap[ keysDown .join(" ") .toLocaleLowerCase() .split(" ") .sort() .join(" ") ]; if (handler) { emit(call(handler, event)); } }); document.addEventListener("keyup", (event: KeyboardEvent) => { keysDown.splice(keysDown.indexOf(event.key), 1); }); return () => {}; }); while (1) { const action = yield take(chan); yield spawn(function*() { yield action; }); } };