Drag to rearrange layers in the layer panel (#434)

* Add insert line

* Implement dragging

* Improve CSS and variable naming consistency

* Resolved folder crash, added Undo/Redo support

* Removed marker TODO leftover

* JS cleanup

* WIP preserving expanded state (via LayerData) when copy/pasting and moving layers in layer panel

* Finish making folders copy/paste preserving expanded state, but not recursively yet

* Add cut, copy, and paste to the Edit menu (#440)

* Add cut

* Hook up edit dropdown

* Use copy message

Co-authored-by: Keavon Chambers <keavon@keavon.com>
Co-authored-by: otdavies <oliver@psyfer.io>
This commit is contained in:
0HyperCube
2021-12-30 09:18:18 -08:00
committed by Keavon Chambers
co-authored by Keavon Chambers otdavies
parent b87b527566
commit 1a70667f63
9 changed files with 264 additions and 36 deletions
+145 -3
View File
@@ -15,8 +15,8 @@
</PopoverButton>
</LayoutRow>
<LayoutRow :class="'layer-tree scrollable-y'">
<LayoutCol :class="'list'" @click="deselectAllLayers">
<div class="layer-row" v-for="layer in layers" :key="layer.path">
<LayoutCol :class="'list'" ref="layerTreeList" @click="deselectAllLayers" @dragover="updateLine($event)" @dragend="drop()">
<div class="layer-row" v-for="(layer, index) in layers" :key="layer.path">
<div class="layer-visibility">
<IconButton
:action="(e) => (toggleLayerVisibility(layer.path), e.stopPropagation())"
@@ -40,6 +40,9 @@
@click.shift.ctrl.exact.stop="selectLayer(layer, true, true)"
@click.ctrl.exact.stop="selectLayer(layer, true, false)"
@click.exact.stop="selectLayer(layer, false, false)"
:data-index="index"
draggable="true"
@dragstart="dragStart($event, layer)"
>
<div class="layer-thumbnail" v-html="layer.thumbnail"></div>
<div class="layer-type-icon">
@@ -85,7 +88,8 @@
flex: 0 0 auto;
position: relative;
& + .layer-row {
& + .layer-row,
& + .insert-mark + .layer-row {
margin-top: 2px;
}
@@ -190,6 +194,33 @@
z-index: 0;
}
}
.insert-mark {
position: relative;
margin-right: 16px;
height: 0;
z-index: 2;
&::after {
content: "";
position: absolute;
background: var(--color-accent-hover);
width: 100%;
height: 6px;
}
&:not(:first-child, :last-child) {
top: -2px;
}
&:first-child::after {
top: 0;
}
&:last-child::after {
bottom: 0;
}
}
}
}
</style>
@@ -250,6 +281,10 @@ const blendModeEntries: SectionsOfMenuListEntries<BlendMode> = [
],
];
const RANGE_TO_INSERT_WITHIN_BOTTOM_FOLDER_NOT_ROOT = 40;
const LAYER_LEFT_MARGIN_OFFSET = 28;
const LAYER_LEFT_INDENT_OFFSET = 16;
export default defineComponent({
inject: ["editor"],
data() {
@@ -265,6 +300,7 @@ export default defineComponent({
selectionRangeStartLayer: undefined as undefined | LayerPanelEntry,
selectionRangeEndLayer: undefined as undefined | LayerPanelEntry,
opacity: 100,
draggingData: undefined as undefined | { path: BigUint64Array; above: boolean; nearestPath: BigUint64Array; insertLine: HTMLDivElement },
MenuDirection,
SeparatorType,
LayerTypeOptions,
@@ -303,6 +339,112 @@ export default defineComponent({
layer.layer_data.selected = false;
});
},
closest(tree: HTMLElement, clientY: number): [BigUint64Array, boolean, Node] {
const treeChildren = tree.children;
// Closest distance to the middle of the row along the Y axis
let closest = Infinity;
// The nearest row parent (element of the tree)
let nearestElement = tree.lastChild as Node;
// The nearest element in the path to the mouse
let nearestPath = new BigUint64Array();
// Item goes above or below the mouse
let above = false;
Array.from(treeChildren).forEach((treeChild) => {
if (treeChild.childElementCount <= 2) return;
const child = treeChild.children[2] as HTMLElement;
const indexAttribute = child.getAttribute("data-index");
if (!indexAttribute) return;
const layer = this.layers[parseInt(indexAttribute, 10)];
const rect = child.getBoundingClientRect();
const position = rect.top + rect.height / 2;
const distance = position - clientY;
// Inserting above current row
if (distance > 0 && distance < closest) {
closest = distance;
nearestPath = layer.path;
above = true;
if (child.parentNode) {
nearestElement = child.parentNode;
}
}
// Inserting below current row
else if (distance > -closest && distance > -RANGE_TO_INSERT_WITHIN_BOTTOM_FOLDER_NOT_ROOT && distance < 0 && layer.layer_type !== LayerTypeOptions.Folder) {
closest = -distance;
nearestPath = layer.path;
if (child.parentNode && child.parentNode.nextSibling) {
nearestElement = child.parentNode.nextSibling;
}
}
// Inserting with no nesting at the end of the panel
else if (closest === Infinity) {
nearestPath = layer.path.slice(0, 1);
}
});
return [nearestPath, above, nearestElement];
},
async dragStart(event: DragEvent, layer: LayerPanelEntry) {
// Set style of cursor for drag
if (event.dataTransfer) {
event.dataTransfer.dropEffect = "move";
event.dataTransfer.effectAllowed = "move";
}
const tree = (this.$refs.layerTreeList as typeof LayoutCol).$el;
// Create the insert line
const insertLine = document.createElement("div") as HTMLDivElement;
insertLine.classList.add("insert-mark");
tree.appendChild(insertLine);
const [nearestPath, above, nearestElement] = this.closest(tree, event.clientY);
// Set the initial state of the line
if (nearestElement.parentNode) {
insertLine.style.marginLeft = `${LAYER_LEFT_MARGIN_OFFSET + LAYER_LEFT_INDENT_OFFSET * nearestPath.length}px`;
tree.insertBefore(insertLine, nearestElement);
}
this.draggingData = { path: layer.path, above, nearestPath, insertLine };
},
updateLine(event: DragEvent) {
// Stop the drag from being shown as cancelled
event.preventDefault();
const tree = (this.$refs.layerTreeList as typeof LayoutCol).$el as HTMLElement;
const [nearestPath, above, nearestElement] = this.closest(tree, event.clientY);
if (this.draggingData) {
this.draggingData.nearestPath = nearestPath;
this.draggingData.above = above;
if (nearestElement.parentNode) {
this.draggingData.insertLine.style.marginLeft = `${LAYER_LEFT_MARGIN_OFFSET + LAYER_LEFT_INDENT_OFFSET * nearestPath.length}px`;
tree.insertBefore(this.draggingData.insertLine, nearestElement);
}
}
},
removeLine() {
if (this.draggingData) {
this.draggingData.insertLine.remove();
}
},
async drop() {
this.removeLine();
if (this.draggingData) {
this.editor.instance.move_layer_in_tree(this.draggingData.path, this.draggingData.above, this.draggingData.nearestPath);
}
},
setBlendModeForSelectedLayers() {
const selected = this.layers.filter((layer) => layer.layer_data.selected);
@@ -197,6 +197,8 @@ export enum MenuType {
Dialog = "Dialog",
}
const POINTER_STRAY_DISTANCE = 100;
export default defineComponent({
components: {},
props: {
@@ -313,7 +315,6 @@ export default defineComponent({
floatingMenuContent.style.minWidth = minWidth;
},
pointerMoveHandler(e: PointerEvent) {
const POINTER_STRAY_DISTANCE = 100;
const target = e.target as HTMLElement;
const pointerOverFloatingMenuKeepOpen = target && (target.closest("[data-hover-menu-keep-open]") as HTMLElement);
const pointerOverFloatingMenuSpawner = target && (target.closest("[data-hover-menu-spawner]") as HTMLElement);