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

feat: drag-resizing of dashboard items #7791

Merged
merged 12 commits into from
Sep 12, 2024
17 changes: 16 additions & 1 deletion dev/dashboard.html
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@
}

.chart {
height: 300px;
height: 100%;
min-height: 300px;
background: repeating-linear-gradient(45deg, #e0e0e0, #e0e0e0 10px, #f5f5f5 10px, #f5f5f5 20px);
}
</style>
Expand Down Expand Up @@ -102,6 +103,20 @@
console.log('dashboard-item-reorder-end');
console.log('items after reorder', e.target.items);
});

dashboard.addEventListener('dashboard-item-resize-start', (e) => {
console.log('dashboard-item-resize-start', e.detail);
});

dashboard.addEventListener('dashboard-item-drag-resize', (e) => {
console.log('dashboard-item-drag-resize', e.detail);
// e.preventDefault();
});

dashboard.addEventListener('dashboard-item-resize-end', (e) => {
console.log('dashboard-item-resize-end');
console.log('item after resize', e.detail);
});
</script>
</head>

Expand Down
28 changes: 28 additions & 0 deletions packages/dashboard/src/vaadin-dashboard-widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,32 @@ class DashboardWidget extends ControllerMixin(ElementMixin(PolylitMixin(LitEleme

#content {
flex: 1;
min-height: 100px;
}

#resize-handle {
display: var(--_vaadin-dashboard-widget-actions-display, none);
}

#resize-handle::before {
position: absolute;
bottom: 0;
right: 0;
font-size: 30px;
content: '\\2921';
cursor: grab;
line-height: 1;
}

:host::after {
content: '';
z-index: 100;
position: absolute;
inset-inline-start: 0;
top: 0;
width: var(--_vaadin-dashboard-widget-resizer-width, 0);
height: var(--_vaadin-dashboard-widget-resizer-height, 0);
background: rgba(0, 0, 0, 0.1);
}
`,
dashboardWidgetAndSectionStyles,
Expand Down Expand Up @@ -80,6 +106,8 @@ class DashboardWidget extends ControllerMixin(ElementMixin(PolylitMixin(LitEleme
<div id="content">
<slot></slot>
</div>

<div id="resize-handle" class="resize-handle"></div>
`;
}

Expand Down
29 changes: 29 additions & 0 deletions packages/dashboard/src/vaadin-dashboard.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,12 +65,41 @@ export type DashboardItemDragReorderEvent<TItem extends DashboardItem> = CustomE
targetIndex: number;
}>;

/**
* Fired when item resizing starts
*/
export type DashboardItemResizeStartEvent<TItem extends DashboardItem> = CustomEvent<{
item: TItem;
}>;

/**
* Fired when item resizing ends
*/
export type DashboardItemResizeEndEvent<TItem extends DashboardItem> = CustomEvent<{
item: TItem;
}>;

/**
* Fired when an item will be resized by dragging
*/
export type DashboardItemDragResizeEvent<TItem extends DashboardItem> = CustomEvent<{
item: TItem;
colspan: number;
rowspan: number;
}>;

export interface DashboardCustomEventMap<TItem extends DashboardItem> {
'dashboard-item-reorder-start': DashboardItemReorderStartEvent;

'dashboard-item-reorder-end': DashboardItemReorderEndEvent;

'dashboard-item-drag-reorder': DashboardItemDragReorderEvent<TItem>;

'dashboard-item-resize-start': DashboardItemResizeStartEvent<TItem>;

'dashboard-item-resize-end': DashboardItemResizeEndEvent<TItem>;

'dashboard-item-drag-resize': DashboardItemDragResizeEvent<TItem>;
}

export type DashboardEventMap<TItem extends DashboardItem> = DashboardCustomEventMap<TItem> & HTMLElementEventMap;
Expand Down
35 changes: 35 additions & 0 deletions packages/dashboard/src/vaadin-dashboard.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,17 @@ import { css, ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themabl
import { DashboardLayoutMixin } from './vaadin-dashboard-layout-mixin.js';
import { hasWidgetWrappers } from './vaadin-dashboard-styles.js';
import { WidgetReorderController } from './widget-reorder-controller.js';
import { WidgetResizeController } from './widget-resize-controller.js';

/**
* A responsive, grid-based dashboard layout component
*
* @fires {CustomEvent} dashboard-item-drag-reorder - Fired when an items will be reordered by dragging
* @fires {CustomEvent} dashboard-item-reorder-start - Fired when item reordering starts
* @fires {CustomEvent} dashboard-item-reorder-end - Fired when item reordering ends
* @fires {CustomEvent} dashboard-item-drag-resize - Fired when an item will be resized by dragging
* @fires {CustomEvent} dashboard-item-resize-start - Fired when item resizing starts
* @fires {CustomEvent} dashboard-item-resize-end - Fired when item resizing ends
*
* @customElement
* @extends HTMLElement
Expand All @@ -49,6 +53,11 @@ class Dashboard extends ControllerMixin(DashboardLayoutMixin(ElementMixin(Themab
:host([editable]) {
--_vaadin-dashboard-widget-actions-display: block;
}

#grid[resizing] {
-webkit-user-select: none;
user-select: none;
}
`,
hasWidgetWrappers,
];
Expand Down Expand Up @@ -98,12 +107,20 @@ class Dashboard extends ControllerMixin(DashboardLayoutMixin(ElementMixin(Themab
constructor() {
super();
this.__widgetReorderController = new WidgetReorderController(this);
this.__widgetResizeController = new WidgetResizeController(this);
}

/** @protected */
disconnectedCallback() {
super.disconnectedCallback();
this.__widgetResizeController.cleanup();
}

/** @protected */
ready() {
super.ready();
this.addController(this.__widgetReorderController);
this.addController(this.__widgetResizeController);
}

/** @protected */
Expand Down Expand Up @@ -182,6 +199,24 @@ class Dashboard extends ControllerMixin(DashboardLayoutMixin(ElementMixin(Themab
*
* @event dashboard-item-drag-reorder
*/

/**
* Fired when item resizing starts
*
* @event dashboard-item-resize-start
*/

/**
* Fired when item resizing ends
*
* @event dashboard-item-resize-end
*/

/**
* Fired when an item will be resized by dragging
*
* @event dashboard-item-drag-resize
*/
}

defineCustomElement(Dashboard);
Expand Down
111 changes: 58 additions & 53 deletions packages/dashboard/src/widget-reorder-controller.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,67 +24,72 @@ export class WidgetReorderController extends EventTarget {

/** @private */
__dragStart(e) {
if ([...e.composedPath()].some((el) => el.classList && el.classList.contains('drag-handle'))) {
this.__draggedElement = e.target;
this.draggedItem = this.__getElementItem(this.__draggedElement);
const handle = [...e.composedPath()].find((el) => el.classList && el.classList.contains('drag-handle'));
if (!handle) {
return;
}

// Set the drag image to the dragged element
const { left, top } = this.__draggedElement.getBoundingClientRect();
e.dataTransfer.setDragImage(this.__draggedElement, e.clientX - left, e.clientY - top);
// Set the text/plain data to enable dragging on mobile devices
e.dataTransfer.setData('text/plain', 'item');
this.__draggedElement = e.target;
this.draggedItem = this.__getElementItem(this.__draggedElement);

// Observe the removal of the dragged element from the DOM
this.draggedElementRemoveObserver.observe(this.host, { childList: true, subtree: true });
// Set the drag image to the dragged element
const { left, top } = this.__draggedElement.getBoundingClientRect();
e.dataTransfer.setDragImage(this.__draggedElement, e.clientX - left, e.clientY - top);
// Set the text/plain data to enable dragging on mobile devices
e.dataTransfer.setData('text/plain', 'item');

this.host.dispatchEvent(new CustomEvent('dashboard-item-reorder-start'));
// Observe the removal of the dragged element from the DOM
this.draggedElementRemoveObserver.observe(this.host, { childList: true, subtree: true });

requestAnimationFrame(() => {
// Re-render to have the dragged element turn into a placeholder
this.host.items = [...this.host.items];
});
}
this.host.dispatchEvent(new CustomEvent('dashboard-item-reorder-start'));

requestAnimationFrame(() => {
// Re-render to have the dragged element turn into a placeholder
this.host.items = [...this.host.items];
});
}

/** @private */
__dragOver(e) {
if (this.draggedItem) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';

// Get all elements that are candidates for reordering with the dragged element
const dragContextElements = this.__getDragContextElements(this.__draggedElement);
// Find the up-to-date element instance representing the dragged item
const draggedElement = dragContextElements.find((element) => this.__getElementItem(element) === this.draggedItem);
if (!draggedElement) {
return;
}
// Get all elements except the dragged element from the drag context
const otherElements = dragContextElements.filter((element) => element !== draggedElement);
// Find the element closest to the x and y coordinates of the drag event
const closestElement = this.__getClosestElement(otherElements, e.clientX, e.clientY);

// Check if the dragged element is dragged enough over the element closest to the drag event coordinates
if (!this.__reordering && this.__isDraggedOver(draggedElement, closestElement, e.clientX, e.clientY)) {
// Prevent reordering multiple times in quick succession
this.__reordering = true;
setTimeout(() => {
this.__reordering = false;
}, REORDER_EVENT_TIMEOUT);

const targetItem = this.__getElementItem(closestElement);
const targetItems = this.__getItemsArrayOfItem(targetItem);
const targetIndex = targetItems.indexOf(targetItem);

const reorderEvent = new CustomEvent('dashboard-item-drag-reorder', {
detail: { item: this.draggedItem, targetIndex },
cancelable: true,
});

// Dispatch the reorder event and reorder items if the event is not canceled
if (this.host.dispatchEvent(reorderEvent)) {
this.__reorderItems(this.draggedItem, targetIndex);
}
if (!this.draggedItem) {
return;
}

e.preventDefault();
e.dataTransfer.dropEffect = 'move';

// Get all elements that are candidates for reordering with the dragged element
const dragContextElements = this.__getDragContextElements(this.__draggedElement);
// Find the up-to-date element instance representing the dragged item
const draggedElement = dragContextElements.find((element) => this.__getElementItem(element) === this.draggedItem);
if (!draggedElement) {
return;
}
// Get all elements except the dragged element from the drag context
const otherElements = dragContextElements.filter((element) => element !== draggedElement);
// Find the element closest to the x and y coordinates of the drag event
const closestElement = this.__getClosestElement(otherElements, e.clientX, e.clientY);

// Check if the dragged element is dragged enough over the element closest to the drag event coordinates
if (!this.__reordering && this.__isDraggedOver(draggedElement, closestElement, e.clientX, e.clientY)) {
// Prevent reordering multiple times in quick succession
this.__reordering = true;
setTimeout(() => {
this.__reordering = false;
}, REORDER_EVENT_TIMEOUT);

const targetItem = this.__getElementItem(closestElement);
const targetItems = this.__getItemsArrayOfItem(targetItem);
const targetIndex = targetItems.indexOf(targetItem);

const reorderEvent = new CustomEvent('dashboard-item-drag-reorder', {
detail: { item: this.draggedItem, targetIndex },
cancelable: true,
});

// Dispatch the reorder event and reorder items if the event is not canceled
if (this.host.dispatchEvent(reorderEvent)) {
this.__reorderItems(this.draggedItem, targetIndex);
}
}
}
Expand Down
Loading
Loading