Skip to content

Commit

Permalink
feat: drag-resizing of dashboard items (#7791)
Browse files Browse the repository at this point in the history
  • Loading branch information
tomivirkki authored Sep 12, 2024
1 parent a51684e commit b64f6b6
Show file tree
Hide file tree
Showing 9 changed files with 941 additions and 59 deletions.
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

0 comments on commit b64f6b6

Please sign in to comment.