From dbf141d866af5b8644d5a1f19c490421e0cd159e Mon Sep 17 00:00:00 2001 From: Alejandro Gallardo Escobar Date: Wed, 14 Aug 2019 14:24:06 +0200 Subject: [PATCH] Improved the movement of the visual console line element --- .../javascript/pandora_visual_console.js | 44 +++ .../models/VisualConsole/Container.php | 4 +- .../models/VisualConsole/Items/Line.php | 74 ++-- visual_console_client/src/VisualConsole.ts | 46 ++- visual_console_client/src/items/Line.ts | 359 ++++++++++++++---- 5 files changed, 405 insertions(+), 122 deletions(-) diff --git a/pandora_console/include/javascript/pandora_visual_console.js b/pandora_console/include/javascript/pandora_visual_console.js index d49f26df28..75697d1225 100755 --- a/pandora_console/include/javascript/pandora_visual_console.js +++ b/pandora_console/include/javascript/pandora_visual_console.js @@ -469,6 +469,50 @@ function createVisualConsole( }) .init(); }); + // VC Line Item moved. + visualConsole.onLineMoved(function(e) { + var id = e.item.props.id; + var data = { + startX: e.startPosition.x, + startY: e.startPosition.y, + endX: e.endPosition.x, + endY: e.endPosition.y + }; + var taskId = "visual-console-item-update-" + id; + + // Persist the new position. + 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" + ); + + // TODO: Move the element to its initial position. + } + + done(); + } + ); + + return { + cancel: function() { + abortable.abort(); + } + }; + }) + .init(); + }); // VC Item resized. visualConsole.onItemResized(function(e) { diff --git a/pandora_console/include/rest-api/models/VisualConsole/Container.php b/pandora_console/include/rest-api/models/VisualConsole/Container.php index 6ae852d9e8..f45fee7651 100644 --- a/pandora_console/include/rest-api/models/VisualConsole/Container.php +++ b/pandora_console/include/rest-api/models/VisualConsole/Container.php @@ -426,10 +426,10 @@ final class Container extends Model * * @param integer $itemId Identifier of the Item. * - * @return Item Item. + * @return Model Item or Line. * @throws \Exception When the data cannot be retrieved from the DB. */ - public static function getItemFromDB(int $itemId): Item + public static function getItemFromDB(int $itemId): Model { // Default filter. $filter = ['id' => $itemId]; diff --git a/pandora_console/include/rest-api/models/VisualConsole/Items/Line.php b/pandora_console/include/rest-api/models/VisualConsole/Items/Line.php index e7f4e7dc67..0342a43aec 100644 --- a/pandora_console/include/rest-api/models/VisualConsole/Items/Line.php +++ b/pandora_console/include/rest-api/models/VisualConsole/Items/Line.php @@ -218,64 +218,70 @@ final class Line extends Model protected function encode(array $data): array { $result = []; + $result['type'] = LINE_ITEM; $id = static::getId($data); if ($id) { $result['id'] = $id; } - $id_layout = static::getIdLayout($data); - if ($id_layout) { - $result['id_layout'] = $id_layout; + $layoutId = static::getIdLayout($data); + if ($layoutId > 0) { + $result['id_layout'] = $layoutId; } - $pos_x = static::parseIntOr( - static::issetInArray($data, ['x', 'pos_x', 'posX']), + $startX = static::parseIntOr( + static::issetInArray($data, ['pos_x', 'startX']), null ); - if ($pos_x !== null) { - $result['pos_x'] = $pos_x; + if ($startX !== null) { + $result['pos_x'] = $startX; } - $pos_y = static::parseIntOr( - static::issetInArray($data, ['y', 'pos_y', 'posY']), + $startY = static::parseIntOr( + static::issetInArray($data, ['pos_y', 'startY']), null ); - if ($pos_y !== null) { - $result['pos_y'] = $pos_y; + if ($startY !== null) { + $result['pos_y'] = $startY; } - $height = static::getHeight($data); - if ($height !== null) { - $result['height'] = $height; - } - - $width = static::getWidth($data); - if ($width !== null) { - $result['width'] = $width; - } - - $type = static::parseIntOr( - static::issetInArray($data, ['type']), + $endX = static::parseIntOr( + static::issetInArray($data, ['width', 'endX']), null ); - if ($type !== null) { - $result['type'] = $type; + if ($endX !== null) { + $result['width'] = $endX; } - $border_width = static::getBorderWidth($data); - if ($border_width !== null) { - $result['border_width'] = $border_width; + $endY = static::parseIntOr( + static::issetInArray($data, ['height', 'endY']), + null + ); + if ($endY !== null) { + $result['height'] = $endY; } - $border_color = static::extractBorderColor($data); - if ($border_color !== null) { - $result['border_color'] = $border_color; + $borderWidth = static::getBorderWidth($data); + if ($borderWidth !== null) { + $result['border_width'] = $borderWidth; } - $show_on_top = static::issetInArray($data, ['isOnTop', 'show_on_top', 'showOnTop']); - if ($show_on_top !== null) { - $result['show_on_top'] = static::parseBool($show_on_top); + $borderColor = static::extractBorderColor($data); + if ($borderColor !== null) { + $result['border_color'] = $borderColor; + } + + $showOnTop = static::issetInArray( + $data, + [ + 'isOnTop', + 'show_on_top', + 'showOnTop', + ] + ); + if ($showOnTop !== null) { + $result['show_on_top'] = static::parseBool($showOnTop); } return $result; diff --git a/visual_console_client/src/VisualConsole.ts b/visual_console_client/src/VisualConsole.ts index e1e2235565..4adecdd5f5 100644 --- a/visual_console_client/src/VisualConsole.ts +++ b/visual_console_client/src/VisualConsole.ts @@ -23,7 +23,7 @@ import ColorCloud, { colorCloudPropsDecoder } from "./items/ColorCloud"; import Group, { groupPropsDecoder } from "./items/Group"; import Clock, { clockPropsDecoder } from "./items/Clock"; import Box, { boxPropsDecoder } from "./items/Box"; -import Line, { linePropsDecoder } from "./items/Line"; +import Line, { linePropsDecoder, LineMovedEvent } from "./items/Line"; import Label, { labelPropsDecoder } from "./items/Label"; import SimpleValue, { simpleValuePropsDecoder } from "./items/SimpleValue"; import EventsHistory, { @@ -211,6 +211,8 @@ export default class VisualConsole { private readonly dblClickEventManager = new TypedEvent(); // Event manager for move events. private readonly movedEventManager = new TypedEvent(); + // Event manager for line move events. + private readonly lineMovedEventManager = new TypedEvent(); // Event manager for resize events. private readonly resizedEventManager = new TypedEvent(); // Event manager for remove events. @@ -275,6 +277,17 @@ export default class VisualConsole { // console.log(`Movement finished for element #${e.item.props.id}`, e); }; + /** + * React to a line movement. + * @param e Event object. + */ + private handleLineElementMovementFinished: ( + e: LineMovedEvent + ) => void = e => { + this.lineMovedEventManager.emit(e); + // console.log(`Movement finished for element #${e.item.props.id}`, e); + }; + /** * React to a resizement on an element. * @param e Event object. @@ -403,13 +416,20 @@ export default class VisualConsole { // Item event handlers. itemInstance.onClick(context.handleElementClick); itemInstance.onDblClick(context.handleElementDblClick); - itemInstance.onMoved(context.handleElementMovement); - itemInstance.onMovementFinished(context.handleElementMovementFinished); - itemInstance.onResized(context.handleElementResizement); - itemInstance.onResizeFinished(context.handleElementResizementFinished); itemInstance.onRemove(context.handleElementRemove); itemInstance.onSelectionChanged(context.handleElementSelectionChanged); + if (itemInstance instanceof Line) { + itemInstance.onLineMovementFinished( + context.handleLineElementMovementFinished + ); + } else { + itemInstance.onMoved(context.handleElementMovement); + itemInstance.onMovementFinished(context.handleElementMovementFinished); + itemInstance.onResized(context.handleElementResizement); + itemInstance.onResizeFinished(context.handleElementResizementFinished); + } + // Add the item to the DOM. context.containerRef.append(itemInstance.elementRef); return itemInstance; @@ -806,6 +826,22 @@ export default class VisualConsole { return disposable; } + /** + * Add an event handler to the movement of the visual console line elements. + * @param listener Function which is going to be executed when a linked console is moved. + */ + public onLineMoved(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.lineMovedEventManager.on(listener); + this.disposables.push(disposable); + + 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. diff --git a/visual_console_client/src/items/Line.ts b/visual_console_client/src/items/Line.ts index 2861dc5421..68339a4935 100644 --- a/visual_console_client/src/items/Line.ts +++ b/visual_console_client/src/items/Line.ts @@ -6,6 +6,7 @@ import { addMovementListener } from "../lib"; import Item, { ItemType, ItemProps, itemBasePropsDecoder } from "../Item"; +import TypedEvent, { Listener, Disposable } from "../lib/TypedEvent"; interface LineProps extends ItemProps { // Overrided properties. @@ -68,68 +69,143 @@ export function linePropsDecoder(data: AnyObject): LineProps | never { ...props, // Enhance the props extracting the box size and position. // eslint-disable-next-line @typescript-eslint/no-use-before-define - ...Line.extractBoxSizeAndPosition(props) + ...Line.extractBoxSizeAndPosition(props.startPosition, props.endPosition) }; } +const svgNS = "http://www.w3.org/2000/svg"; + +export interface LineMovedEvent { + item: Line; + startPosition: LineProps["startPosition"]; + endPosition: LineProps["endPosition"]; +} + export default class Line extends Item { + private circleRadius = 8; // To control if the line movement is enabled. private moveMode: boolean = false; + // To control if the line is moving. + private isMoving: boolean = false; - // // This function will only run the 2nd arg function after the time - // // of the first arg have passed after its last execution. - // private debouncedMovementSave = debounce( - // 500, // ms. - // (x: Position["x"], y: Position["y"]) => { - // const prevPosition = { - // x: this.props.x, - // y: this.props.y - // }; - // const newPosition = { - // x: x, - // y: y - // }; + // Event manager for moved events. + private readonly lineMovedEventManager = new TypedEvent(); + // List of references to clean the event listeners. + private readonly lineMovedEventDisposables: Disposable[] = []; - // if (!this.positionChanged(prevPosition, newPosition)) return; + // This function will only run the 2nd arg function after the time + // of the first arg have passed after its last execution. + private debouncedStartPositionMovementSave = debounce( + 500, // ms. + (x: Position["x"], y: Position["y"]) => { + this.isMoving = false; + const startPosition = { x, y }; + // Emit the movement event. + this.lineMovedEventManager.emit({ + item: this, + startPosition, + endPosition: this.props.endPosition + }); + } + ); + // This property will store the function + // to clean the movement listener. + private removeStartPositionMovement: Function | null = null; - // // Save the new position to the props. - // this.move(x, y); - // // Emit the movement event. - // this.movedEventManager.emit({ - // item: this, - // prevPosition: prevPosition, - // newPosition: newPosition - // }); - // } - // ); - // // This property will store the function - // // to clean the movement listener. - // private removeMovement: Function | null = null; + /** + * Start the movement funtionality for the start position. + * @param element Element to move inside its container. + */ + private initStartPositionMovementListener( + element: HTMLElement, + container: HTMLElement + ): void { + this.removeStartPositionMovement = addMovementListener( + element, + (x: Position["x"], y: Position["y"]) => { + // Calculate the center of the circle. + x += this.circleRadius; + y += this.circleRadius; - // /** - // * Start the movement funtionality. - // * @param element Element to move inside its container. - // */ - // private initMovementListener(element: HTMLElement): void { - // this.removeMovement = addMovementListener( - // element, - // (x: Position["x"], y: Position["y"]) => { - // // Move the DOM element. - // this.moveElement(x, y); - // // Run the save function. - // this.debouncedMovementSave(x, y); - // } - // ); - // } - // /** - // * Stop the movement fun - // */ - // private stopMovementListener(): void { - // if (this.removeMovement) { - // this.removeMovement(); - // this.removeMovement = null; - // } - // } + const startPosition = { x, y }; + + this.isMoving = true; + this.props = { + ...this.props, + startPosition + }; + + // Run the end function. + this.debouncedStartPositionMovementSave(x, y); + }, + container + ); + } + /** + * Stop the movement fun + */ + private stopStartPositionMovementListener(): void { + if (this.removeStartPositionMovement) { + this.removeStartPositionMovement(); + this.removeStartPositionMovement = null; + } + } + + // This function will only run the 2nd arg function after the time + // of the first arg have passed after its last execution. + private debouncedEndPositionMovementSave = debounce( + 500, // ms. + (x: Position["x"], y: Position["y"]) => { + this.isMoving = false; + const endPosition = { x, y }; + // Emit the movement event. + this.lineMovedEventManager.emit({ + item: this, + endPosition, + startPosition: this.props.startPosition + }); + } + ); + // This property will store the function + // to clean the movement listener. + private removeEndPositionMovement: Function | null = null; + + /** + * End the movement funtionality for the end position. + * @param element Element to move inside its container. + */ + private initEndPositionMovementListener( + element: HTMLElement, + container: HTMLElement + ): void { + this.removeEndPositionMovement = addMovementListener( + element, + (x: Position["x"], y: Position["y"]) => { + // Calculate the center of the circle. + x += this.circleRadius; + y += this.circleRadius; + + this.isMoving = true; + this.props = { + ...this.props, + endPosition: { x, y } + }; + + // Run the end function. + this.debouncedEndPositionMovementSave(x, y); + }, + container + ); + } + /** + * Stop the movement function. + */ + private stopEndPositionMovementListener(): void { + if (this.removeEndPositionMovement) { + this.removeEndPositionMovement(); + this.removeEndPositionMovement = null; + } + } /** * @override @@ -142,7 +218,10 @@ export default class Line extends Item { super( { ...props, - ...Line.extractBoxSizeAndPosition(props) + ...Line.extractBoxSizeAndPosition( + props.startPosition, + props.endPosition + ) }, { ...meta, @@ -164,7 +243,10 @@ export default class Line extends Item { public setProps(newProps: LineProps) { super.setProps({ ...newProps, - ...Line.extractBoxSizeAndPosition(newProps) + ...Line.extractBoxSizeAndPosition( + newProps.startPosition, + newProps.endPosition + ) }); } @@ -202,15 +284,11 @@ export default class Line extends Item { color // Line color } = this.props; - const startIsLeft = startPosition.x - endPosition.x <= 0; - const startIsTop = startPosition.y - endPosition.y <= 0; - const x1 = startPosition.x - x + lineWidth / 2; const y1 = startPosition.y - y + lineWidth / 2; const x2 = endPosition.x - x + lineWidth / 2; const y2 = endPosition.y - y + lineWidth / 2; - const svgNS = "http://www.w3.org/2000/svg"; // SVG container. const svg = document.createElementNS(svgNS, "svg"); // Set SVG size. @@ -227,36 +305,130 @@ export default class Line extends Item { svg.append(line); element.append(svg); + return element; + } + + protected updateDomElement(element: HTMLElement): void { + const { + x, // Box x + y, // Box y + width, // Box width + height, // Box height + lineWidth, // Line thickness + startPosition, // Line start position + endPosition, // Line end position + color // Line color + } = this.props; + + const x1 = startPosition.x - x + lineWidth / 2; + const y1 = startPosition.y - y + lineWidth / 2; + const x2 = endPosition.x - x + lineWidth / 2; + const y2 = endPosition.y - y + lineWidth / 2; + + const svgs = element.getElementsByTagName("svg"); + + if (svgs.length > 0) { + const svg = svgs.item(0); + + if (svg != null) { + // Set SVG size. + svg.setAttribute("width", `${width + lineWidth}`); + svg.setAttribute("height", `${height + lineWidth}`); + + const lines = svg.getElementsByTagNameNS(svgNS, "line"); + + if (lines.length > 0) { + const line = lines.item(0); + + if (line != null) { + line.setAttribute("x1", `${x1}`); + line.setAttribute("y1", `${y1}`); + line.setAttribute("x2", `${x2}`); + line.setAttribute("y2", `${y2}`); + line.setAttribute("stroke", color || "black"); + line.setAttribute("stroke-width", `${lineWidth}`); + } + } + } + } + if (this.moveMode) { - const startCircle = document.createElement("div"); - startCircle.style.width = "16px"; - startCircle.style.height = "16px"; + const startIsLeft = startPosition.x - endPosition.x <= 0; + const startIsTop = startPosition.y - endPosition.y <= 0; + + let startCircle: HTMLElement = document.createElement("div"); + let endCircle: HTMLElement = document.createElement("div"); + + if (this.isMoving) { + const circlesStart = element.getElementsByClassName( + "visual-console-item-line-circle-start" + ); + if (circlesStart.length > 0) { + const circle = circlesStart.item(0) as HTMLElement; + if (circle) startCircle = circle; + } + const circlesEnd = element.getElementsByClassName( + "visual-console-item-line-circle-end" + ); + if (circlesEnd.length > 0) { + const circle = circlesEnd.item(0) as HTMLElement; + if (circle) endCircle = circle; + } + } + + startCircle.classList.add( + "visual-console-item-line-circle", + "visual-console-item-line-circle-start" + ); + startCircle.style.width = `${this.circleRadius * 2}px`; + startCircle.style.height = `${this.circleRadius * 2}px`; startCircle.style.borderRadius = "50%"; startCircle.style.backgroundColor = "white"; startCircle.style.position = "absolute"; startCircle.style.left = startIsLeft - ? "-8px" - : `${width + lineWidth - 8}px`; + ? `-${this.circleRadius}px` + : `${width + lineWidth - this.circleRadius}px`; startCircle.style.top = startIsTop - ? "-8px" - : `${height + lineWidth - 8}px`; + ? `-${this.circleRadius}px` + : `${height + lineWidth - this.circleRadius}px`; - const endCircle = document.createElement("div"); - endCircle.style.width = "16px"; - endCircle.style.height = "16px"; + endCircle.classList.add( + "visual-console-item-line-circle", + "visual-console-item-line-circle-end" + ); + endCircle.style.width = `${this.circleRadius * 2}px`; + endCircle.style.height = `${this.circleRadius * 2}px`; endCircle.style.borderRadius = "50%"; - endCircle.style.backgroundColor = "white"; + endCircle.style.backgroundColor = "black"; endCircle.style.position = "absolute"; endCircle.style.left = startIsLeft ? `${width + lineWidth - 8}px` - : "-8px"; - endCircle.style.top = startIsTop ? `${height + lineWidth - 8}px` : "-8px"; + : `-${this.circleRadius}px`; + endCircle.style.top = startIsTop + ? `${height + lineWidth - this.circleRadius}px` + : `-${this.circleRadius}px`; - element.append(startCircle); - element.append(endCircle); + if (!this.isMoving) { + element.appendChild(startCircle); + element.appendChild(endCircle); + + // Init the movement listeners. + this.initStartPositionMovementListener(startCircle, this.elementRef + .parentElement as HTMLElement); + this.initEndPositionMovementListener(endCircle, this.elementRef + .parentElement as HTMLElement); + } + } else if (!this.isMoving) { + this.stopStartPositionMovementListener(); + // Remove circles. + const circles = element.getElementsByClassName( + "visual-console-item-line-circle" + ); + while (circles.length > 0) { + const circle = circles.item(0); + if (circle) circle.remove(); + } } - - return element; } /** @@ -264,12 +436,15 @@ export default class Line extends Item { * the start and the finish of the line. * @param props Item properties. */ - public static extractBoxSizeAndPosition(props: LineProps): Size & Position { + public static extractBoxSizeAndPosition( + startPosition: Position, + endPosition: Position + ): Size & Position { return { - width: Math.abs(props.startPosition.x - props.endPosition.x), - height: Math.abs(props.startPosition.y - props.endPosition.y), - x: Math.min(props.startPosition.x, props.endPosition.x), - y: Math.min(props.startPosition.y, props.endPosition.y) + width: Math.abs(startPosition.x - endPosition.x), + height: Math.abs(startPosition.y - endPosition.y), + x: Math.min(startPosition.x, endPosition.x), + y: Math.min(startPosition.y, endPosition.y) }; } @@ -278,7 +453,29 @@ export default class Line extends Item { * @override Item.remove */ public remove(): void { - // TODO: clear the item's event listeners. + // Clear the item's event listeners. + this.stopStartPositionMovementListener(); + // Call the parent's .remove() super.remove(); } + + /** + * To add an event handler to the movement of visual console elements. + * @param listener Function which is going to be executed when a linked console is moved. + * + * @override Item.onMoved + */ + public onLineMovementFinished( + 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.lineMovedEventManager.on(listener); + this.lineMovedEventDisposables.push(disposable); + + return disposable; + } }