Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[REF] drag&drop: generic draggable list component #5238

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 12 additions & 48 deletions src/components/bottom_bar/bottom_bar.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { Component, onWillUpdateProps, useRef, useState } from "@odoo/owl";
import { BACKGROUND_GRAY_COLOR, HEADER_WIDTH } from "../../constants";
import { deepEquals } from "../../helpers";
import { MenuItemRegistry } from "../../registries/menu_items_registry";
import { _t } from "../../translation";
import { MenuMouseEvent, Pixel, Rect, SpreadsheetChildEnv, UID } from "../../types";
import { MenuMouseEvent, Pixel, SpreadsheetChildEnv, UID } from "../../types";
import { Ripple } from "../animation/ripple";
import { DragAndDropListItems } from "../drag_and_drop_list/drag_and_drop_list";
import { css } from "../helpers/css";
import { useDragAndDropListItems } from "../helpers/drag_and_drop_hook";
import { Menu, MenuState } from "../menu/menu";
import { BottomBarSheet } from "./bottom_bar_sheet/bottom_bar_sheet";
import { BottomBarStatistic } from "./bottom_bar_statistic/bottom_bar_statistic";
Expand Down Expand Up @@ -90,12 +89,11 @@ export class BottomBar extends Component<Props, SpreadsheetChildEnv> {
static props = {
onClick: Function,
};
static components = { Menu, Ripple, BottomBarSheet, BottomBarStatistic };
static components = { DragAndDropListItems, Menu, Ripple, BottomBarSheet, BottomBarStatistic };

private bottomBarRef = useRef("bottomBar");
private sheetListRef = useRef("sheetList");

private dragAndDrop = useDragAndDropListItems();
private targetScroll: number | undefined = undefined;
private state = useState({
isSheetListScrollableLeft: false,
Expand All @@ -117,10 +115,6 @@ export class BottomBar extends Component<Props, SpreadsheetChildEnv> {
onWillUpdateProps(() => {
this.updateScrollState();
const visibleSheets = this.getVisibleSheets();
// Cancel sheet dragging when there is a change in the sheets
if (!deepEquals(this.sheetList, visibleSheets)) {
this.dragAndDrop.cancel();
}
this.sheetList = visibleSheets;
});
}
Expand Down Expand Up @@ -239,51 +233,21 @@ export class BottomBar extends Component<Props, SpreadsheetChildEnv> {
this.sheetListRef.el.scrollTo({ top: 0, left: scroll, behavior: "smooth" });
}

canStartDrag() {
return !this.env.model.getters.isReadonly();
}

onSheetMouseDown(sheetId: UID, event: MouseEvent) {
if (event.button !== 0 || this.env.model.getters.isReadonly()) return;
this.closeMenu();

const visibleSheets = this.getVisibleSheets();
const sheetRects = this.getSheetItemRects();

const sheets = visibleSheets.map((sheet, index) => ({
id: sheet.id,
size: sheetRects[index].width,
position: sheetRects[index].x,
}));
this.dragAndDrop.start("horizontal", {
draggedItemId: sheetId,
initialMousePosition: event.clientX,
items: sheets,
containerEl: this.sheetListRef.el!,
onDragEnd: (sheetId: UID, finalIndex: number) => this.onDragEnd(sheetId, finalIndex),
});
}

private onDragEnd(sheetId: UID, finalIndex: number) {
const originalIndex = this.getVisibleSheets().findIndex((sheet) => sheet.id === sheetId);
onDragEnd(sheetId: UID, originalIndex: number, finalIndex: number) {
const delta = finalIndex - originalIndex;
if (sheetId && delta !== 0) {
this.env.model.dispatch("MOVE_SHEET", {
sheetId: sheetId,
delta: delta,
});
}
}

getSheetStyle(sheetId: UID): string {
return this.dragAndDrop.itemsStyle[sheetId] || "";
}

private getSheetItemRects(): Rect[] {
return Array.from(this.bottomBarRef.el!.querySelectorAll<HTMLElement>(`.o-sheet`))
.map((sheetEl) => sheetEl.getBoundingClientRect())
.map((rect) => ({
x: rect.x,
width: rect.width - 1, // -1 to compensate negative margin
y: rect.y,
height: rect.height,
}));
this.env.model.dispatch("MOVE_SHEET", {
sheetId: sheetId,
delta,
});
}

get sheetListCurrentScroll() {
Expand Down
27 changes: 18 additions & 9 deletions src/components/bottom_bar/bottom_bar.xml
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,27 @@
t-if="state.isSheetListScrollableLeft"
/>
<div
class="o-sheet-list d-flex w-100 px-1"
class="o-sheet-list w-100 px-1"
t-ref="sheetList"
t-on-wheel="onWheel"
t-on-scroll="onScroll">
<t t-foreach="getVisibleSheets()" t-as="sheet" t-key="sheet.id">
<BottomBarSheet
style="getSheetStyle(sheet.id)"
sheetId="sheet.id"
openContextMenu="(registry, ev) => this.onSheetContextMenu(sheet.id, registry, ev)"
onMouseDown="(ev) => this.onSheetMouseDown(sheet.id, ev)"
/>
</t>
<DragAndDropListItems
direction="'horizontal'"
draggableItemIds="this.getVisibleSheets().map((sheet) => sheet.id)"
containerClass="'d-flex'"
onDragEnd.bind="onDragEnd"
canStartDrag.bind="canStartDrag">
<t t-set-slot="default" t-slot-scope="dragAndDrop">
<t t-foreach="getVisibleSheets()" t-as="sheet" t-key="sheet.id">
<BottomBarSheet
style="dragAndDrop.itemsStyle[sheet.id]"
sheetId="sheet.id"
openContextMenu="(registry, ev) => this.onSheetContextMenu(sheet.id, registry, ev)"
onMouseDown="(ev) => dragAndDrop.start(sheet.id, ev)"
/>
</t>
</t>
</DragAndDropListItems>
</div>
<div
class="o-bottom-bar-fade-out position-absolute h-100 w-100 pe-none"
Expand Down
83 changes: 83 additions & 0 deletions src/components/drag_and_drop_list/drag_and_drop_list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Component, onWillUpdateProps, useRef } from "@odoo/owl";
import { deepEqualsArray } from "../../helpers";
import { useDragAndDropListItems } from "../helpers/drag_and_drop_hook";

interface Props {
draggableItemIds: string[];
onDragEnd: (draggedItemId: string, originalIndex: number, finalIndex: number) => void;
canStartDrag: (event: MouseEvent) => boolean;
direction: "horizontal" | "vertical";
containerClass?: string;
slots?: Record<string, any>;
}

export class DragAndDropListItems extends Component<Props> {
static template = "o-spreadsheet-DragAndDropListItems";
static props = {
draggableItemIds: { type: Array },
onDragEnd: { type: Function },
direction: { type: String, optional: true },
canStartDrag: { type: Function, optional: true },
containerClass: { type: String, optional: true },
slots: { type: Object, optional: true },
};
static defaultProps = {
canStartDrag: () => true,
direction: "vertical",
};
private containerRef = useRef("container-ref");
private dragAndDrop = useDragAndDropListItems();

setup() {
onWillUpdateProps((nextProps: Props) => {
if (!deepEqualsArray(this.props.draggableItemIds, nextProps.draggableItemIds)) {
this.dragAndDrop.cancel();
}
});
}

startDragAndDrop(itemId: string, event: MouseEvent) {
if (event.button !== 0 || !this.props.canStartDrag(event)) {
return;
}
const rects = this.getDimensionElementsRects();
const direction = this.props.direction;
const isVertical = direction === "vertical";
const items = this.props.draggableItemIds.map((itemId, index) => ({
id: itemId,
size: isVertical ? rects[index].height : rects[index].width,
position: isVertical ? rects[index].y : rects[index].x,
}));
this.dragAndDrop.start(direction, {
draggedItemId: itemId,
initialMousePosition: isVertical ? event.clientY : event.clientX,
items,
containerEl: this.containerRef.el!,
onDragEnd: this.onDragEnd.bind(this),
});
}

private onDragEnd(draggedItemId: string, finalIndex: number) {
const originalIndex = this.props.draggableItemIds.findIndex(
(itemId) => itemId === draggedItemId
);
if (originalIndex === finalIndex) {
return;
}
this.props.onDragEnd(draggedItemId, originalIndex, finalIndex);
}

getDimensionElementsRects() {
return Array.from(this.containerRef.el!.children).map((el) => {
const style = getComputedStyle(el)!;
const rect = el.getBoundingClientRect();
return {
x: rect.x,
y: rect.y,
width: rect.width + parseInt(style.marginLeft || "0") + parseInt(style.marginRight || "0"),
height:
rect.height + parseInt(style.marginTop || "0") + parseInt(style.marginBottom || "0"),
};
});
}
}
15 changes: 15 additions & 0 deletions src/components/drag_and_drop_list/drag_and_drop_list.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<templates>
<t t-name="o-spreadsheet-DragAndDropListItems">
<div
class="list-drag-and-drop-container"
t-att-class="props.containerClass"
t-ref="container-ref">
<t
t-slot="default"
itemsStyle="dragAndDrop.itemsStyle"
draggedItemId="dragAndDrop.draggedItemId"
start.bind="startDragAndDrop"
/>
</div>
</t>
</templates>
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { Component, onWillUpdateProps, useRef } from "@odoo/owl";
import { deepEquals } from "../../../../helpers";
import { Component } from "@odoo/owl";
import { ConditionalFormat, SpreadsheetChildEnv, UID } from "../../../../types";
import { getBoundingRectAsPOJO } from "../../../helpers/dom_helpers";
import { useDragAndDropListItems } from "../../../helpers/drag_and_drop_hook";
import { DragAndDropListItems } from "../../../drag_and_drop_list/drag_and_drop_list";
import { ICONS } from "../../../icons/icons";
import { ConditionalFormatPreview } from "../cf_preview/cf_preview";

Expand All @@ -19,53 +17,16 @@ export class ConditionalFormatPreviewList extends Component<Props, SpreadsheetCh
onPreviewClick: Function,
onAddConditionalFormat: Function,
};
static components = { ConditionalFormatPreview };
static components = { ConditionalFormatPreview, DragAndDropListItems };

icons = ICONS;

private dragAndDrop = useDragAndDropListItems();
private cfListRef = useRef("cfList");

setup() {
onWillUpdateProps((nextProps: Props) => {
if (!deepEquals(this.props.conditionalFormats, nextProps.conditionalFormats)) {
this.dragAndDrop.cancel();
}
});
}

getPreviewDivStyle(cf: ConditionalFormat): string {
return this.dragAndDrop.itemsStyle[cf.id] || "";
}

onPreviewMouseDown(cf: ConditionalFormat, event: MouseEvent) {
if (event.button !== 0) return;
const previewRects = Array.from(this.cfListRef.el!.children).map((previewEl) =>
getBoundingRectAsPOJO(previewEl)
);
const items = this.props.conditionalFormats.map((cf, index) => ({
id: cf.id,
size: previewRects[index].height,
position: previewRects[index].y,
}));
this.dragAndDrop.start("vertical", {
draggedItemId: cf.id,
initialMousePosition: event.clientY,
items: items,
containerEl: this.cfListRef.el!,
onDragEnd: (cfId: UID, finalIndex: number) => this.onDragEnd(cfId, finalIndex),
});
}

private onDragEnd(cfId: UID, finalIndex: number) {
const originalIndex = this.props.conditionalFormats.findIndex((sheet) => sheet.id === cfId);
onDragEnd(cfId: UID, originalIndex: number, finalIndex: number) {
const delta = originalIndex - finalIndex;
if (delta !== 0) {
this.env.model.dispatch("CHANGE_CONDITIONAL_FORMAT_PRIORITY", {
cfId,
delta,
sheetId: this.env.model.getters.getActiveSheetId(),
});
}
this.env.model.dispatch("CHANGE_CONDITIONAL_FORMAT_PRIORITY", {
cfId,
delta,
sheetId: this.env.model.getters.getActiveSheetId(),
});
}
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
<templates>
<t t-name="o-spreadsheet-ConditionalFormatPreviewList">
<div class="o-cf-preview-list h-100 overflow-auto" t-ref="cfList">
<t t-foreach="props.conditionalFormats" t-as="cf" t-key="cf.id">
<DragAndDropListItems
containerClass="'o-cf-preview-list h-100 overflow-auto'"
draggableItemIds="props.conditionalFormats.map(cf => cf.id)"
onDragEnd.bind="onDragEnd">
<t t-set-slot="default" t-slot-scope="dragAndDrop">
<t t-foreach="props.conditionalFormats" t-as="cf" t-key="cf.id">
<div
class="o-cf-preview-container d-flex position-relative"
t-att-style="dragAndDrop.itemsStyle[cf.id]">
<ConditionalFormatPreview
conditionalFormat="cf"
class="dragAndDrop.draggedItemId === cf.id ? 'o-cf-dragging' : ''"
onMouseDown="(ev) => dragAndDrop.start(cf.id, ev)"
onPreviewClick="() => props.onPreviewClick(cf)"
/>
</div>
</t>
<div
class="o-cf-preview-container d-flex position-relative"
t-att-style="getPreviewDivStyle(cf)">
<ConditionalFormatPreview
conditionalFormat="cf"
class="dragAndDrop.draggedItemId === cf.id ? 'o-cf-dragging' : ''"
onMouseDown="(ev) => this.onPreviewMouseDown(cf, ev)"
onPreviewClick="() => props.onPreviewClick(cf)"
/>
class="o-button-link p-4 o-cf-add float-end"
t-on-click.prevent.stop="props.onAddConditionalFormat">
+ Add another rule
</div>
</t>
<div
class="o-button-link p-4 o-cf-add float-end"
t-on-click.prevent.stop="props.onAddConditionalFormat">
+ Add another rule
</div>
</div>
</DragAndDropListItems>
</t>
</templates>
Loading