Added a functionality to allows the live vc item resizement

This commit is contained in:
Alejandro Gallardo Escobar 2019-06-17 12:22:55 +02:00
parent ee7fb4db5f
commit dbf15a759f
6 changed files with 312 additions and 5 deletions

View File

@ -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.

View File

@ -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<ItemProps>;
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<Props extends ItemProps> {
private readonly clickEventManager = new TypedEvent<ItemClickEvent<Props>>();
// Event manager for moved events.
private readonly movedEventManager = new TypedEvent<ItemMovedEvent>();
// Event manager for resized events.
private readonly resizedEventManager = new TypedEvent<ItemResizedEvent>();
// Event manager for remove events.
private readonly removeEventManager = new TypedEvent<
ItemRemoveEvent<Props>
@ -163,6 +172,9 @@ abstract class VisualConsoleItem<Props extends ItemProps> {
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<Props extends ItemProps> {
}
}
// 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<Props extends ItemProps> {
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<Props extends ItemProps> {
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<Props extends ItemProps> {
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<ItemResizedEvent>): 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.

View File

@ -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<ItemMovedEvent>();
// Event manager for resize events.
private readonly resizedEventManager = new TypedEvent<ItemResizedEvent>();
// 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<ItemResizedEvent>): 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.
*/

View File

@ -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();
};
}

View File

@ -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;
}

View File

@ -0,0 +1,24 @@
<svg version="1.1" id="Capa_1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" width="15px" height="15px" viewBox="0 0 15 15" enable-background="new 0 0 15 15" xml:space="preserve">
<line fill="none" stroke="#B2B2B2" stroke-width="1.5" stroke-miterlimit="10" x1="0.562" y1="36.317" x2="14.231" y2="22.648"/>
<line fill="none" stroke="#B2B2B2" stroke-width="1.5" stroke-miterlimit="10" x1="4.971" y1="36.595" x2="14.409" y2="27.155"/>
<line fill="none" stroke="#B2B2B2" stroke-width="1.5" stroke-miterlimit="10" x1="10.017" y1="36.433" x2="14.231" y2="32.218"/>
<g id="jGEeKn_1_">
<image overflow="visible" width="46" height="37" id="jGEeKn" xlink:href="data:image/jpeg;base64,/9j/4AAQSkZJRgABAgEASABIAAD/7AARRHVja3kAAQAEAAAAHgAA/+4AIUFkb2JlAGTAAAAAAQMA
EAMCAwYAAAGRAAABswAAAgL/2wCEABALCwsMCxAMDBAXDw0PFxsUEBAUGx8XFxcXFx8eFxoaGhoX
Hh4jJSclIx4vLzMzLy9AQEBAQEBAQEBAQEBAQEABEQ8PERMRFRISFRQRFBEUGhQWFhQaJhoaHBoa
JjAjHh4eHiMwKy4nJycuKzU1MDA1NUBAP0BAQEBAQEBAQEBAQP/CABEIACYALwMBIgACEQEDEQH/
xAB5AAEBAQEBAAAAAAAAAAAAAAAAAQQCBgEBAAAAAAAAAAAAAAAAAAAAABAAAQQDAAAAAAAAAAAA
AAAAAQAxAgMQMBIRAAEBBgUFAQAAAAAAAAAAAAECABEhQWEDECBxkRIwMYEyEwQSAQAAAAAAAAAA
AAAAAAAAADD/2gAMAwEAAhEDEQAAAPfAS5zTZTkGPrULZQAAD//aAAgBAgABBQDT/9oACAEDAAEF
ANP/2gAIAQEAAQUAzJg2SQBC2d0w2bZSvVNc5oMuAuQuAuRp/9oACAECAgY/AB//2gAIAQMCBj8A
H//aAAgBAQEGPwDE6ZXmAYqtwsJBHI91GlMqnvT+e37QL1kS0b7XBwADrVoQCRXGe5ae5ae5afk9
H//Z" transform="matrix(1 0 0 1 -46.875 -9.8906)">
</image>
</g>
<line fill="none" stroke="#B2B2B2" stroke-width="1.5" stroke-miterlimit="10" x1="13.483" y1="0.644" x2="0.828" y2="13.301"/>
<line fill="none" stroke="#B2B2B2" stroke-width="1.5" stroke-miterlimit="10" x1="13.734" y1="5.141" x2="5.325" y2="13.549"/>
<line fill="none" stroke="#B2B2B2" stroke-width="1.5" stroke-miterlimit="10" x1="14.231" y1="9.388" x2="9.806" y2="13.813"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB