From dbf15a759f074d230c4531963e4372b440f0e1f8 Mon Sep 17 00:00:00 2001 From: Alejandro Gallardo Escobar Date: Mon, 17 Jun 2019 12:22:55 +0200 Subject: [PATCH] Added a functionality to allows the live vc item resizement --- .../javascript/pandora_visual_console.js | 48 +++++++- visual_console_client/src/Item.ts | 89 +++++++++++++- visual_console_client/src/VisualConsole.ts | 31 ++++- visual_console_client/src/lib/index.ts | 112 ++++++++++++++++++ visual_console_client/src/main.css | 13 +- visual_console_client/src/resize-handle.svg | 24 ++++ 6 files changed, 312 insertions(+), 5 deletions(-) create mode 100644 visual_console_client/src/resize-handle.svg diff --git a/pandora_console/include/javascript/pandora_visual_console.js b/pandora_console/include/javascript/pandora_visual_console.js index ca1fed6e79..13d7f899e4 100755 --- a/pandora_console/include/javascript/pandora_visual_console.js +++ b/pandora_console/include/javascript/pandora_visual_console.js @@ -148,6 +148,7 @@ function createVisualConsole( }; var taskId = "visual-console-item-move-" + id; + // Persist the new position. asyncTaskManager .add(taskId, function(done) { var abortable = updateVisualConsoleItem( @@ -156,8 +157,8 @@ function createVisualConsole( id, data, function(error, data) { - if (!error && !data) return; - if (error) { + // if (!error && !data) return; + if (error || !data) { console.log( "[ERROR]", "[VISUAL-CONSOLE-CLIENT]", @@ -181,6 +182,49 @@ function createVisualConsole( }) .init(); }); + // VC Item resized. + visualConsole.onItemResized(function(e) { + var id = e.item.props.id; + var data = { + width: e.newSize.width, + height: e.newSize.height + }; + var taskId = "visual-console-item-resize-" + id; + + // Persist the new size. + asyncTaskManager + .add(taskId, function(done) { + var abortable = updateVisualConsoleItem( + baseUrl, + visualConsole.props.id, + id, + data, + function(error, data) { + // if (!error && !data) return; + if (error || !data) { + console.log( + "[ERROR]", + "[VISUAL-CONSOLE-CLIENT]", + "[API]", + error ? error.message : "Invalid response" + ); + + // Resize the element to its initial Size. + e.item.resize(e.prevSize.width, e.prevSize.height); + } + + done(); + } + ); + + return { + cancel: function() { + abortable.abort(); + } + }; + }) + .init(); + }); if (updateInterval != null && updateInterval > 0) { // Start an interval to update the Visual Console. diff --git a/visual_console_client/src/Item.ts b/visual_console_client/src/Item.ts index aa60913796..8d8bd08027 100644 --- a/visual_console_client/src/Item.ts +++ b/visual_console_client/src/Item.ts @@ -15,7 +15,8 @@ import { humanDate, humanTime, addMovementListener, - debounce + debounce, + addResizementListener } from "./lib"; import TypedEvent, { Listener, Disposable } from "./lib/TypedEvent"; @@ -76,6 +77,12 @@ export interface ItemMovedEvent { newPosition: Position; } +export interface ItemResizedEvent { + item: VisualConsoleItem; + prevSize: Size; + newSize: Size; +} + /** * Extract a valid enum value from a raw label positi9on value. * @param labelPosition Raw value. @@ -143,6 +150,8 @@ abstract class VisualConsoleItem { private readonly clickEventManager = new TypedEvent>(); // Event manager for moved events. private readonly movedEventManager = new TypedEvent(); + // Event manager for resized events. + private readonly resizedEventManager = new TypedEvent(); // Event manager for remove events. private readonly removeEventManager = new TypedEvent< ItemRemoveEvent @@ -163,6 +172,9 @@ abstract class VisualConsoleItem { x: x, y: y }; + + if (!this.positionChanged(prevPosition, newPosition)) return; + // Save the new position to the props. this.move(x, y); // Emit the movement event. @@ -202,6 +214,61 @@ abstract class VisualConsoleItem { } } + // This function will only run the 2nd arg function after the time + // of the first arg have passed after its last execution. + private debouncedResizementSave = debounce( + 500, // ms. + (width: Size["width"], height: Size["height"]) => { + const prevSize = { + width: this.props.width, + height: this.props.height + }; + const newSize = { + width: width, + height: height + }; + + if (!this.sizeChanged(prevSize, newSize)) return; + + // Save the new position to the props. + this.resize(width, height); + // Emit the resizement event. + this.resizedEventManager.emit({ + item: this, + prevSize: prevSize, + newSize: newSize + }); + } + ); + // This property will store the function + // to clean the resizement listener. + private removeResizement: Function | null = null; + + /** + * Start the resizement funtionality. + * @param element Element to move inside its container. + */ + private initResizementListener(element: HTMLElement): void { + this.removeResizement = addResizementListener( + element, + (width: Size["width"], height: Size["height"]) => { + // Move the DOM element. + this.resizeElement(width, height); + // Run the save function. + this.debouncedResizementSave(width, height); + } + ); + } + /** + * Stop the resizement functionality. + */ + private stopResizementListener(): void { + if (this.removeResizement) { + this.removeResizement(); + this.removeResizement = null; + } + } + /** * To create a new element which will be inside the item box. * @return Item. @@ -269,6 +336,8 @@ abstract class VisualConsoleItem { box.classList.add("is-editing"); // Init the movement listener. this.initMovementListener(box); + // Init the resizement listener. + this.initResizementListener(box); } if (this.meta.isFetching) { box.classList.add("is-fetching"); @@ -503,9 +572,11 @@ abstract class VisualConsoleItem { if (this.meta.editMode) { this.elementRef.classList.add("is-editing"); this.initMovementListener(this.elementRef); + this.initResizementListener(this.elementRef); } else { this.elementRef.classList.remove("is-editing"); this.stopMovementListener(); + this.stopResizementListener(); } } if (!prevMeta || prevMeta.isFetching !== this.meta.isFetching) { @@ -693,6 +764,22 @@ abstract class VisualConsoleItem { return disposable; } + /** + * To add an event handler to the resizement of visual console elements. + * @param listener Function which is going to be executed when a linked console is moved. + */ + public onResized(listener: Listener): Disposable { + /* + * The '.on' function returns a function which will clean the event + * listener when executed. We store all the 'dispose' functions to + * call them when the item should be cleared. + */ + const disposable = this.resizedEventManager.on(listener); + this.disposables.push(disposable); + + return disposable; + } + /** * To add an event handler to the removal of the item. * @param listener Function which is going to be executed when a item is removed. diff --git a/visual_console_client/src/VisualConsole.ts b/visual_console_client/src/VisualConsole.ts index 868342b44b..2dbdcd3a09 100644 --- a/visual_console_client/src/VisualConsole.ts +++ b/visual_console_client/src/VisualConsole.ts @@ -11,7 +11,8 @@ import Item, { ItemProps, ItemClickEvent, ItemRemoveEvent, - ItemMovedEvent + ItemMovedEvent, + ItemResizedEvent } from "./Item"; import StaticGraph, { staticGraphPropsDecoder } from "./items/StaticGraph"; import Icon, { iconPropsDecoder } from "./items/Icon"; @@ -207,6 +208,8 @@ export default class VisualConsole { >(); // Event manager for move events. private readonly movedEventManager = new TypedEvent(); + // Event manager for resize events. + private readonly resizedEventManager = new TypedEvent(); // List of references to clean the event listeners. private readonly disposables: Disposable[] = []; @@ -228,6 +231,15 @@ export default class VisualConsole { // console.log(`Moved element #${e.item.props.id}`, e); }; + /** + * React to a resizement on an element. + * @param e Event object. + */ + private handleElementResizement: (e: ItemResizedEvent) => void = e => { + this.resizedEventManager.emit(e); + // console.log(`Resized element #${e.item.props.id}`, e); + }; + /** * Clear some element references. * @param e Event object. @@ -277,6 +289,7 @@ export default class VisualConsole { // Item event handlers. itemInstance.onClick(this.handleElementClick); itemInstance.onMoved(this.handleElementMovement); + itemInstance.onResized(this.handleElementResizement); itemInstance.onRemove(this.handleElementRemove); // Add the item to the DOM. this.containerRef.append(itemInstance.elementRef); @@ -595,6 +608,22 @@ export default class VisualConsole { return disposable; } + /** + * Add an event handler to the resizement of the visual console elements. + * @param listener Function which is going to be executed when a linked console is moved. + */ + public onItemResized(listener: Listener): Disposable { + /* + * The '.on' function returns a function which will clean the event + * listener when executed. We store all the 'dispose' functions to + * call them when the item should be cleared. + */ + const disposable = this.resizedEventManager.on(listener); + this.disposables.push(disposable); + + return disposable; + } + /** * Enable the edition mode. */ diff --git a/visual_console_client/src/lib/index.ts b/visual_console_client/src/lib/index.ts index 6a7bf89909..eaad5bbab6 100644 --- a/visual_console_client/src/lib/index.ts +++ b/visual_console_client/src/lib/index.ts @@ -558,3 +558,115 @@ export function addMovementListener( handleEnd(); }; } + +/** + * Add the grab & resize functionality to a certain element. + * + * @param element Element to move. + * @param onResized Function to execute when the element is resized. + * + * @return A function which will clean the event handlers when executed. + */ +export function addResizementListener( + element: HTMLElement, + onResized: (x: Position["x"], y: Position["y"]) => void +): Function { + const resizeDraggable = document.createElement("div"); + resizeDraggable.className = "resize-draggable"; + element.appendChild(resizeDraggable); + + // Store the initial draggable state. + const isDraggable = element.draggable; + // Init the coordinates. + let lastWidth: Size["width"] = 0; + let lastHeight: Size["height"] = 0; + let lastMouseX: Position["x"] = 0; + let lastMouseY: Position["y"] = 0; + let mouseElementOffsetX: Position["x"] = 0; + let mouseElementOffsetY: Position["y"] = 0; + + // Will run onResized 32ms after its last execution. + const debouncedResizement = debounce( + 32, + (width: Size["width"], height: Size["height"]) => onResized(width, height) + ); + // Will run onResized one time max every 16ms. + const throttledResizement = throttle( + 16, + (width: Size["width"], height: Size["height"]) => onResized(width, height) + ); + + const handleResize = (e: MouseEvent) => { + // Calculate the new element coordinates. + let width = lastWidth + (e.pageX - lastMouseX); + let height = lastHeight + (e.pageY - lastMouseY); + + // TODO: Document. + + // Minimum value. + if (width <= 0) width = 10; + if (height <= 0) height = 10; + + // Run the movement events. + throttledResizement(width, height); + debouncedResizement(width, height); + + // Store the coordinates of the element. + lastWidth = width; + lastHeight = height; + // Store the last mouse coordinates. + lastMouseX = e.pageX; + lastMouseY = e.pageY; + }; + const handleEnd = () => { + // Reset the positions. + lastWidth = 0; + lastHeight = 0; + lastMouseX = 0; + lastMouseY = 0; + mouseElementOffsetX = 0; + mouseElementOffsetY = 0; + // Remove the move event. + document.removeEventListener("mousemove", handleResize); + // Clean itself. + document.removeEventListener("mouseup", handleEnd); + // Reset the draggable property to its initial state. + element.draggable = isDraggable; + // Reset the body selection property to a default state. + document.body.style.userSelect = "auto"; + }; + const handleStart = (e: MouseEvent) => { + e.stopPropagation(); + + // Disable the drag temporarily. + element.draggable = false; + + // Store the difference between the cursor and + // the initial coordinates of the element. + const { width, height } = element.getBoundingClientRect(); + lastWidth = width; + lastHeight = height; + // Store the mouse position. + lastMouseX = e.pageX; + lastMouseY = e.pageY; + // Store the relative position between the mouse and the element. + mouseElementOffsetX = e.offsetX; + mouseElementOffsetY = e.offsetY; + + // Listen to the mouse movement. + document.addEventListener("mousemove", handleResize); + // Listen to the moment when the mouse click is not pressed anymore. + document.addEventListener("mouseup", handleEnd); + // Limit the mouse selection of the body. + document.body.style.userSelect = "none"; + }; + + // Event to listen the init of the movement. + resizeDraggable.addEventListener("mousedown", handleStart); + + // Returns a function to clean the event listeners. + return () => { + resizeDraggable.remove(); + handleEnd(); + }; +} diff --git a/visual_console_client/src/main.css b/visual_console_client/src/main.css index b737d1768a..99df3bb925 100644 --- a/visual_console_client/src/main.css +++ b/visual_console_client/src/main.css @@ -16,7 +16,18 @@ } .visual-console-item.is-editing { - border: 2px dashed #33ccff; + border: 2px dashed #b2b2b2; transform: translateX(-2px) translateY(-2px); cursor: move; } + +.visual-console-item.is-editing > .resize-draggable { + float: right; + position: absolute; + right: 0; + bottom: 0; + width: 15px; + height: 15px; + background: url(./resize-handle.svg); + cursor: se-resize; +} diff --git a/visual_console_client/src/resize-handle.svg b/visual_console_client/src/resize-handle.svg new file mode 100644 index 0000000000..b851b85377 --- /dev/null +++ b/visual_console_client/src/resize-handle.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + +