tandem-front-end
Version:
Visual editor for web components
478 lines (451 loc) • 14.8 kB
text/typescript
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,
syntheticNodeContextMenuWrapInSlotClicked,
syntheticNodeContextMenuSelectParentClicked,
syntheticNodeContextMenuSelectSourceNodeClicked,
syntheticNodeContextMenuConvertToComponentClicked,
syntheticNodeContextMenuWrapInElementClicked,
syntheticNodeContextMenuConvertToStyleMixinClicked,
syntheticNodeContextMenuRemoveClicked,
EDITOR_TAB_RIGHT_CLICKED,
EditorTabClicked,
syntheticNodeContextMenuConvertTextStylesToMixinClicked,
syntheticNodeContextMenuRenameClicked,
syntheticNodeContextMenuShowInCanvasClicked,
moduleContextMenuCloseOptionClicked,
PC_LAYER_RIGHT_CLICKED,
editorTabContextMenuOpenInBottomTabOptionClicked,
syntheticNodeContextMenuCopyClicked,
syntheticNodeContextMenuPasteClicked
} 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
} from "paperclip";
export type ShortcutSagaOptions = {
openContextMenu: (anchor: Point, options: ContextMenuOption[]) => void;
};
type OpenSyntheticNodeContextMenuOptions = {
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)
}
]
}
]
);
}
);
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();
yield call(
openSyntheticNodeContextMenu,
getSyntheticNodeById(targetNodeId, state.documents),
{
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);
// maybe shadow
if (!node) {
return;
}
yield call(
openSyntheticNodeContextMenu,
node,
{
left: event.pageX,
top: event.pageY
},
state,
{ showRenameLabelOption: true }
);
});
function* openSyntheticNodeContextMenu(
node: SyntheticNode,
point: Point,
state: RootState,
{
showRenameLabelOption
}: OpenSyntheticNodeContextMenuOptions = EMPTY_OBJECT
) {
// TODO - need to have options here for handling document: close, move to bottom tab
if (node.name === SYNTHETIC_DOCUMENT_NODE_NAME) {
console.warn(`Cannot open context menu for documents (yet)`);
return yield call(
handleModuleRightClicked,
point,
getSyntheticDocumentDependencyUri(
node as SyntheticDocument,
state.graph
)
);
return;
}
const syntheticNode = getSyntheticNodeById(node.id, state.documents);
const sourceNode = getSyntheticSourceNode(syntheticNode, state.graph);
const syntheticDocument = getSyntheticVisibleNodeDocument(
syntheticNode.id,
state.documents
);
const inspectorNode = getSyntheticInspectorNode(
syntheticNode,
syntheticDocument,
state.sourceNodeInspector,
state.graph
);
const contentNode = getPCNodeContentNode(
sourceNode.id,
getPCNodeModule(sourceNode.id, state.graph)
);
const inspectorContentNode = getInspectorContentNode(
inspectorNode,
state.sourceNodeInspector
);
yield call(openContextMenu, point, [
syntheticNodeIsInShadow(syntheticNode, syntheticDocument, state.graph)
? {
type: ContextMenuOptionType.ITEM,
label: "Hide",
action: syntheticNodeContextMenuRemoveClicked(syntheticNode)
}
: {
type: ContextMenuOptionType.GROUP,
options: [
showRenameLabelOption
? {
type: ContextMenuOptionType.ITEM,
label: "Rename",
action: syntheticNodeContextMenuRenameClicked(
syntheticNode
)
}
: null,
{
type: ContextMenuOptionType.ITEM,
label: "Remove",
action: syntheticNodeContextMenuRemoveClicked(syntheticNode)
},
{
type: ContextMenuOptionType.ITEM,
label: "Copy",
action: syntheticNodeContextMenuCopyClicked(syntheticNode)
},
{
type: ContextMenuOptionType.ITEM,
label: "Paste",
action: syntheticNodeContextMenuPasteClicked(syntheticNode)
},
sourceNode.name !== PCSourceTagNames.COMPONENT &&
!inspectorNodeInShadow(inspectorNode, state.sourceNodeInspector)
? {
type: ContextMenuOptionType.ITEM,
label: "Convert to Component",
action: syntheticNodeContextMenuConvertToComponentClicked(
syntheticNode
)
}
: null,
sourceNode.name !== PCSourceTagNames.COMPONENT &&
!inspectorNodeInShadow(inspectorNode, state.sourceNodeInspector)
? {
type: ContextMenuOptionType.ITEM,
label: "Wrap in Element",
action: syntheticNodeContextMenuWrapInElementClicked(
syntheticNode
)
}
: null,
contentNode.name === PCSourceTagNames.COMPONENT &&
contentNode.id !== sourceNode.id &&
!inspectorNodeInShadow(inspectorNode, state.sourceNodeInspector)
? {
type: ContextMenuOptionType.ITEM,
label: "Wrap in Slot",
action: syntheticNodeContextMenuWrapInSlotClicked(
syntheticNode
)
}
: 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: syntheticNodeContextMenuConvertToStyleMixinClicked(
syntheticNode
)
}
: null,
(sourceNode.name === PCSourceTagNames.COMPONENT ||
sourceNode.name === PCSourceTagNames.COMPONENT_INSTANCE ||
sourceNode.name === PCSourceTagNames.ELEMENT ||
sourceNode.name === PCSourceTagNames.TEXT) &&
hasTextStyles(
inspectorNode,
state.sourceNodeInspector,
state.selectedVariant,
state.graph
)
? {
type: ContextMenuOptionType.ITEM,
label: "Move Text Styles to Mixin",
action: syntheticNodeContextMenuConvertTextStylesToMixinClicked(
syntheticNode
)
}
: null
].filter(Boolean) as ContextMenuItem[]
},
{
type: ContextMenuOptionType.GROUP,
options: [
contentNode.id !== sourceNode.id
? {
type: ContextMenuOptionType.ITEM,
label: "Select Parent",
action: syntheticNodeContextMenuSelectParentClicked(
syntheticNode
)
}
: null,
inspectorNodeInShadow(inspectorNode, inspectorContentNode) ||
extendsComponent(sourceNode)
? {
type: ContextMenuOptionType.ITEM,
label: "Select Source Layer",
action: syntheticNodeContextMenuSelectSourceNodeClicked(
syntheticNode
)
}
: null,
{
type: ContextMenuOptionType.ITEM,
label: "Center in Canvas",
action: syntheticNodeContextMenuShowInCanvasClicked(syntheticNode)
}
].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;
});
}
};