tandem-front-end
Version:
Visual editor for web components
492 lines (464 loc) • 15.2 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,
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;
});
}
};