Added item selection and a WIP version of the edition form

This commit is contained in:
Alejandro Gallardo Escobar 2019-07-12 14:00:24 +02:00
parent 8047bf2c87
commit 3120058c36
8 changed files with 487 additions and 29 deletions
pandora_console/include/javascript
visual_console_client/src

View File

@ -124,19 +124,57 @@ function createVisualConsole(
visualConsole = new VisualConsole(container, props, items); visualConsole = new VisualConsole(container, props, items);
// VC Item clicked. // VC Item clicked.
visualConsole.onItemClick(function(e) { visualConsole.onItemClick(function(e) {
// Override the link to another VC if it isn't on remote console. var data = e.item.props || {};
if ( var meta = e.item.meta || {};
e.data &&
e.data.linkedLayoutId != null && if (meta.editMode) {
e.data.linkedLayoutId > 0 && // Item selection.
e.data.link != null && if (meta.isSelected) {
e.data.link.length > 0 && visualConsole.unselectItem(data.id);
(e.data.linkedLayoutAgentId == null || e.data.linkedLayoutAgentId === 0) } else {
// Unselect the rest of the elements if the
visualConsole.selectItem(data.id, !e.nativeEvent.metaKey);
}
} else if (
!meta.editMode &&
data.linkedLayoutId != null &&
data.linkedLayoutId > 0 &&
data.link != null &&
data.link.length > 0 &&
(data.linkedLayoutAgentId == null || data.linkedLayoutAgentId === 0)
) { ) {
// Override the link to another VC if it isn't on remote console.
// Stop the current link behavior. // Stop the current link behavior.
e.nativeEvent.preventDefault(); e.nativeEvent.preventDefault();
// Fetch and update the old VC with the new. // Fetch and update the old VC with the new.
updateVisualConsole(e.data.linkedLayoutId, updateInterval); updateVisualConsole(data.linkedLayoutId, updateInterval);
}
});
// VC Item double clicked.
visualConsole.onItemDblClick(function(e) {
e.nativeEvent.preventDefault();
e.nativeEvent.stopPropagation();
var item = e.item || {};
var props = item.props || {};
var meta = item.meta || {};
if (meta.editMode) {
// Item selection.
visualConsole.selectItem(props.id, true);
var formContainer = item.getFormContainer();
var formElement = formContainer.getFormElement();
formContainer.onSubmit(function(e) {
// TODO: Send the update.
console.log("Form submit", e.data);
$(formElement).dialog("close");
});
$(formElement).dialog({
title: formContainer.title
});
// TODO: Add submit and reset button.
} }
}); });
// VC Item moved. // VC Item moved.

View File

@ -0,0 +1,205 @@
import TypedEvent, { Listener, Disposable } from "./lib/TypedEvent";
import { AnyObject } from "./lib/types";
// TODO: Document
export abstract class InputGroup<Data extends {} = {}> {
private _name: string = "";
private _element?: HTMLElement;
public initialData: Data;
protected currentData: Partial<Data> = {};
public constructor(name: string, initialData: Data) {
this.name = name;
this.initialData = initialData;
}
public set name(name: string) {
if (name.length === 0) throw new RangeError("empty name");
this._name = name;
}
public get name(): string {
return this._name;
}
public get data(): Partial<Data> {
return { ...this.currentData };
}
public get element(): HTMLElement {
if (this._element == null) {
const element = document.createElement("div");
element.className = `input-group input-group-${this.name}`;
const content = this.createContent();
if (content instanceof Array) {
content.forEach(element.appendChild);
} else {
element.appendChild(content);
}
this._element = element;
}
return this._element;
}
public reset(): void {
this.currentData = {};
}
protected updateData(data: Partial<Data>): void {
this.currentData = {
...this.currentData,
...data
};
// TODO: Update item.
}
protected abstract createContent(): HTMLElement | HTMLElement[];
// public abstract get isValid(): boolean;
}
export interface SubmitFormEvent {
nativeEvent: Event;
data: AnyObject;
}
// TODO: Document
export class FormContainer {
public readonly title: string;
private inputGroupsByName: { [name: string]: InputGroup } = {};
private enabledInputGroupNames: string[] = [];
// Event manager for submit events.
private readonly submitEventManager = new TypedEvent<SubmitFormEvent>();
public constructor(
title: string,
inputGroups: InputGroup[] = [],
enabledInputGroups: string[] = []
) {
this.title = title;
if (inputGroups.length > 0) {
this.inputGroupsByName = inputGroups.reduce((prevVal, inputGroup) => {
prevVal[inputGroup.name] = inputGroup;
return prevVal;
}, this.inputGroupsByName);
}
if (enabledInputGroups.length > 0) {
this.enabledInputGroupNames = [
...this.enabledInputGroupNames,
...enabledInputGroups.filter(
name => this.inputGroupsByName[name] != null
)
];
}
}
public getInputGroup(inputGroupName: string): InputGroup | null {
return this.inputGroupsByName[inputGroupName] || null;
}
public addInputGroup(
inputGroup: InputGroup,
index: number | null = null
): FormContainer {
this.inputGroupsByName[inputGroup.name] = inputGroup;
// Remove the current stored name if exist.
this.enabledInputGroupNames = this.enabledInputGroupNames.filter(
name => name === inputGroup.name
);
if (index !== null) {
if (index <= 0) {
this.enabledInputGroupNames = [
inputGroup.name,
...this.enabledInputGroupNames
];
} else if (index >= this.enabledInputGroupNames.length) {
this.enabledInputGroupNames = [
...this.enabledInputGroupNames,
inputGroup.name
];
} else {
this.enabledInputGroupNames = [
// part of the array before the specified index
...this.enabledInputGroupNames.slice(0, index),
// inserted item
inputGroup.name,
// part of the array after the specified index
...this.enabledInputGroupNames.slice(index)
];
}
} else {
this.enabledInputGroupNames = [
...this.enabledInputGroupNames,
inputGroup.name
];
}
return this;
}
public removeInputGroup(inputGroupName: string): FormContainer {
// delete this.inputGroupsByName[inputGroupName];
// Remove the current stored name.
this.enabledInputGroupNames = this.enabledInputGroupNames.filter(
name => name === inputGroupName
);
return this;
}
public getFormElement(): HTMLFormElement {
const form = document.createElement("form");
form.addEventListener("submit", e => {
e.preventDefault();
this.submitEventManager.emit({
nativeEvent: e,
data: this.enabledInputGroupNames.reduce((data, name) => {
if (this.inputGroupsByName[name]) {
data = {
...data,
...this.inputGroupsByName[name].data
};
}
return data;
}, {})
});
});
this.enabledInputGroupNames.forEach(name => {
if (this.inputGroupsByName[name]) {
form.appendChild(this.inputGroupsByName[name].element);
}
});
return form;
}
public reset(): void {
this.enabledInputGroupNames.forEach(name => {
if (this.inputGroupsByName[name]) {
this.inputGroupsByName[name].reset();
}
});
}
// public get isValid(): boolean {
// for (let i = 0; i < this.enabledInputGroupNames.length; i++) {
// const inputGroup = this.inputGroupsByName[this.enabledInputGroupNames[i]];
// if (inputGroup && !inputGroup.isValid) return false;
// }
// return true;
// }
public onSubmit(listener: Listener<SubmitFormEvent>): Disposable {
return this.submitEventManager.on(listener);
}
}

View File

@ -16,9 +16,11 @@ import {
humanTime, humanTime,
addMovementListener, addMovementListener,
debounce, debounce,
addResizementListener addResizementListener,
t
} from "./lib"; } from "./lib";
import TypedEvent, { Listener, Disposable } from "./lib/TypedEvent"; import TypedEvent, { Listener, Disposable } from "./lib/TypedEvent";
import { FormContainer, InputGroup } from "./Form";
// Enum: https://www.typescriptlang.org/docs/handbook/enums.html. // Enum: https://www.typescriptlang.org/docs/handbook/enums.html.
export const enum ItemType { export const enum ItemType {
@ -58,10 +60,8 @@ export interface ItemProps extends Position, Size {
aclGroupId: number | null; aclGroupId: number | null;
} }
// FIXME: Fix type compatibility. export interface ItemClickEvent {
export interface ItemClickEvent<Props extends ItemProps> { item: VisualConsoleItem<ItemProps>;
// data: Props;
data: AnyObject;
nativeEvent: Event; nativeEvent: Event;
} }
@ -83,6 +83,41 @@ export interface ItemResizedEvent {
newSize: Size; newSize: Size;
} }
// TODO: Document
class PositionInputGroup extends InputGroup<ItemProps> {
protected createContent(): HTMLElement | HTMLElement[] {
const positionLabel = document.createElement("label");
positionLabel.textContent = t("Position");
const positionInputX = document.createElement("input");
positionInputX.type = "number";
positionInputX.min = "0";
positionInputX.required = true;
positionInputX.value = `${this.currentData.x || this.initialData.x || 0}`;
positionInputX.addEventListener("change", e =>
this.updateData({
x: parseIntOr((e.target as HTMLInputElement).value, 0)
})
);
const positionInputY = document.createElement("input");
positionInputY.type = "number";
positionInputY.min = "0";
positionInputY.required = true;
positionInputY.value = `${this.currentData.y || this.initialData.y || 0}`;
positionInputY.addEventListener("change", e =>
this.updateData({
y: parseIntOr((e.target as HTMLInputElement).value, 0)
})
);
positionLabel.appendChild(positionInputX);
positionLabel.appendChild(positionInputY);
return positionLabel;
}
}
/** /**
* Extract a valid enum value from a raw label positi9on value. * Extract a valid enum value from a raw label positi9on value.
* @param labelPosition Raw value. * @param labelPosition Raw value.
@ -147,7 +182,9 @@ abstract class VisualConsoleItem<Props extends ItemProps> {
// Reference to the DOM element which will contain the view of the item which extends this class. // Reference to the DOM element which will contain the view of the item which extends this class.
protected childElementRef: HTMLElement = document.createElement("div"); protected childElementRef: HTMLElement = document.createElement("div");
// Event manager for click events. // Event manager for click events.
private readonly clickEventManager = new TypedEvent<ItemClickEvent<Props>>(); private readonly clickEventManager = new TypedEvent<ItemClickEvent>();
// Event manager for double click events.
private readonly dblClickEventManager = new TypedEvent<ItemClickEvent>();
// Event manager for moved events. // Event manager for moved events.
private readonly movedEventManager = new TypedEvent<ItemMovedEvent>(); private readonly movedEventManager = new TypedEvent<ItemMovedEvent>();
// Event manager for resized events. // Event manager for resized events.
@ -164,6 +201,10 @@ abstract class VisualConsoleItem<Props extends ItemProps> {
private debouncedMovementSave = debounce( private debouncedMovementSave = debounce(
500, // ms. 500, // ms.
(x: Position["x"], y: Position["y"]) => { (x: Position["x"], y: Position["y"]) => {
// Update the metadata information.
// Don't use the .meta property cause we don't need DOM updates.
this._metadata.isBeingMoved = false;
const prevPosition = { const prevPosition = {
x: this.props.x, x: this.props.x,
y: this.props.y y: this.props.y
@ -197,6 +238,9 @@ abstract class VisualConsoleItem<Props extends ItemProps> {
this.removeMovement = addMovementListener( this.removeMovement = addMovementListener(
element, element,
(x: Position["x"], y: Position["y"]) => { (x: Position["x"], y: Position["y"]) => {
// Update the metadata information.
// Don't use the .meta property cause we don't need DOM updates.
this._metadata.isBeingMoved = true;
// Move the DOM element. // Move the DOM element.
this.moveElement(x, y); this.moveElement(x, y);
// Run the save function. // Run the save function.
@ -219,6 +263,10 @@ abstract class VisualConsoleItem<Props extends ItemProps> {
private debouncedResizementSave = debounce( private debouncedResizementSave = debounce(
500, // ms. 500, // ms.
(width: Size["width"], height: Size["height"]) => { (width: Size["width"], height: Size["height"]) => {
// Update the metadata information.
// Don't use the .meta property cause we don't need DOM updates.
this._metadata.isBeingResized = false;
const prevSize = { const prevSize = {
width: this.props.width, width: this.props.width,
height: this.props.height height: this.props.height
@ -252,6 +300,10 @@ abstract class VisualConsoleItem<Props extends ItemProps> {
this.removeResizement = addResizementListener( this.removeResizement = addResizementListener(
element, element,
(width: Size["width"], height: Size["height"]) => { (width: Size["width"], height: Size["height"]) => {
// Update the metadata information.
// Don't use the .meta property cause we don't need DOM updates.
this._metadata.isBeingResized = true;
// The label it's outside the item's size, so we need // The label it's outside the item's size, so we need
// to get rid of its size to get the real size of the // to get rid of its size to get the real size of the
// item's content. // item's content.
@ -353,13 +405,27 @@ abstract class VisualConsoleItem<Props extends ItemProps> {
box.style.zIndex = this.props.isOnTop ? "2" : "1"; box.style.zIndex = this.props.isOnTop ? "2" : "1";
box.style.left = `${this.props.x}px`; box.style.left = `${this.props.x}px`;
box.style.top = `${this.props.y}px`; box.style.top = `${this.props.y}px`;
// Init the click listener.
// Init the click listeners.
box.addEventListener("dblclick", e => {
if (!this.meta.isBeingMoved && !this.meta.isBeingResized) {
this.dblClickEventManager.emit({
item: this,
nativeEvent: e
});
}
});
box.addEventListener("click", e => { box.addEventListener("click", e => {
if (this.meta.editMode) { if (this.meta.editMode) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
} else { }
this.clickEventManager.emit({ data: this.props, nativeEvent: e });
if (!this.meta.isBeingMoved && !this.meta.isBeingResized) {
this.clickEventManager.emit({
item: this,
nativeEvent: e
});
} }
}); });
@ -377,6 +443,9 @@ abstract class VisualConsoleItem<Props extends ItemProps> {
if (this.meta.isUpdating) { if (this.meta.isUpdating) {
box.classList.add("is-updating"); box.classList.add("is-updating");
} }
if (this.meta.isSelected) {
box.classList.add("is-selected");
}
return box; return box;
} }
@ -643,6 +712,13 @@ abstract class VisualConsoleItem<Props extends ItemProps> {
this.elementRef.classList.remove("is-updating"); this.elementRef.classList.remove("is-updating");
} }
} }
if (!prevMeta || prevMeta.isSelected !== this.meta.isSelected) {
if (this.meta.isSelected) {
this.elementRef.classList.add("is-selected");
} else {
this.elementRef.classList.remove("is-selected");
}
}
} }
/** /**
@ -801,11 +877,20 @@ abstract class VisualConsoleItem<Props extends ItemProps> {
}; };
} }
// TODO: Document
public getFormContainer(): FormContainer {
return new FormContainer(
t("Item"),
[new PositionInputGroup("position", this.props)],
["position"]
);
}
/** /**
* To add an event handler to the click of the linked visual console elements. * To add an event handler to the click of the linked visual console elements.
* @param listener Function which is going to be executed when a linked console is clicked. * @param listener Function which is going to be executed when a linked console is clicked.
*/ */
public onClick(listener: Listener<ItemClickEvent<Props>>): Disposable { public onClick(listener: Listener<ItemClickEvent>): Disposable {
/* /*
* The '.on' function returns a function which will clean the event * The '.on' function returns a function which will clean the event
* listener when executed. We store all the 'dispose' functions to * listener when executed. We store all the 'dispose' functions to
@ -817,6 +902,22 @@ abstract class VisualConsoleItem<Props extends ItemProps> {
return disposable; return disposable;
} }
/**
* To add an event handler to the double click of the linked visual console elements.
* @param listener Function which is going to be executed when a linked console is double clicked.
*/
public onDblClick(listener: Listener<ItemClickEvent>): 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.dblClickEventManager.on(listener);
this.disposables.push(disposable);
return disposable;
}
/** /**
* To add an event handler to the movement of visual console elements. * 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. * @param listener Function which is going to be executed when a linked console is moved.

View File

@ -203,9 +203,9 @@ export default class VisualConsole {
[key: string]: Line; [key: string]: Line;
} = {}; } = {};
// Event manager for click events. // Event manager for click events.
private readonly clickEventManager = new TypedEvent< private readonly clickEventManager = new TypedEvent<ItemClickEvent>();
ItemClickEvent<ItemProps> // Event manager for double click events.
>(); private readonly dblClickEventManager = new TypedEvent<ItemClickEvent>();
// Event manager for move events. // Event manager for move events.
private readonly movedEventManager = new TypedEvent<ItemMovedEvent>(); private readonly movedEventManager = new TypedEvent<ItemMovedEvent>();
// Event manager for resize events. // Event manager for resize events.
@ -217,11 +217,20 @@ export default class VisualConsole {
* React to a click on an element. * React to a click on an element.
* @param e Event object. * @param e Event object.
*/ */
private handleElementClick: (e: ItemClickEvent<ItemProps>) => void = e => { private handleElementClick: (e: ItemClickEvent) => void = e => {
this.clickEventManager.emit(e); this.clickEventManager.emit(e);
// console.log(`Clicked element #${e.data.id}`, e); // console.log(`Clicked element #${e.data.id}`, e);
}; };
/**
* React to a double click on an element.
* @param e Event object.
*/
private handleElementDblClick: (e: ItemClickEvent) => void = e => {
this.dblClickEventManager.emit(e);
// console.log(`Double clicked element #${e.data.id}`, e);
};
/** /**
* React to a movement on an element. * React to a movement on an element.
* @param e Event object. * @param e Event object.
@ -308,6 +317,7 @@ export default class VisualConsole {
this.elementIds.push(itemInstance.props.id); this.elementIds.push(itemInstance.props.id);
// Item event handlers. // Item event handlers.
itemInstance.onClick(this.handleElementClick); itemInstance.onClick(this.handleElementClick);
itemInstance.onDblClick(this.handleElementDblClick);
itemInstance.onMoved(this.handleElementMovement); itemInstance.onMoved(this.handleElementMovement);
itemInstance.onResized(this.handleElementResizement); itemInstance.onResized(this.handleElementResizement);
itemInstance.onRemove(this.handleElementRemove); itemInstance.onRemove(this.handleElementRemove);
@ -685,9 +695,7 @@ export default class VisualConsole {
* Add an event handler to the click of the linked visual console elements. * Add an event handler to the click of the linked visual console elements.
* @param listener Function which is going to be executed when a linked console is clicked. * @param listener Function which is going to be executed when a linked console is clicked.
*/ */
public onItemClick( public onItemClick(listener: Listener<ItemClickEvent>): Disposable {
listener: Listener<ItemClickEvent<ItemProps>>
): Disposable {
/* /*
* The '.on' function returns a function which will clean the event * The '.on' function returns a function which will clean the event
* listener when executed. We store all the 'dispose' functions to * listener when executed. We store all the 'dispose' functions to
@ -699,6 +707,22 @@ export default class VisualConsole {
return disposable; return disposable;
} }
/**
* Add an event handler to the double click of the linked visual console elements.
* @param listener Function which is going to be executed when a linked console is double clicked.
*/
public onItemDblClick(listener: Listener<ItemClickEvent>): 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.dblClickEventManager.on(listener);
this.disposables.push(disposable);
return disposable;
}
/** /**
* Add an event handler to the movement of the visual console elements. * Add an event handler to the movement of the visual console elements.
* @param listener Function which is going to be executed when a linked console is moved. * @param listener Function which is going to be executed when a linked console is moved.
@ -750,4 +774,69 @@ export default class VisualConsole {
}); });
this.containerRef.classList.remove("is-editing"); this.containerRef.classList.remove("is-editing");
} }
/**
* Select an item.
* @param itemId Item Id.
* @param unique To remove the selection of other items or not.
*/
public selectItem(itemId: number, unique: boolean = false): void {
if (unique) {
this.elementIds.forEach(currentItemId => {
const meta = this.elementsById[currentItemId].meta;
if (currentItemId !== itemId && meta.isSelected) {
this.elementsById[currentItemId].meta = {
...meta,
isSelected: false
};
} else if (currentItemId === itemId && !meta.isSelected) {
this.elementsById[currentItemId].meta = {
...meta,
isSelected: true
};
}
});
} else if (this.elementsById[itemId]) {
this.elementsById[itemId].meta = {
...this.elementsById[itemId].meta,
isSelected: true
};
}
}
/**
* Unselect an item.
* @param itemId Item Id.
*/
public unselectItem(itemId: number): void {
if (this.elementsById[itemId]) {
const meta = this.elementsById[itemId].meta;
if (meta.isSelected) {
this.elementsById[itemId].meta = {
...meta,
isSelected: false
};
}
}
}
/**
* Unselect all items.
*/
public unselectItems(): void {
this.elementIds.forEach(itemId => {
if (this.elementsById[itemId]) {
const meta = this.elementsById[itemId].meta;
if (meta.isSelected) {
this.elementsById[itemId].meta = {
...meta,
isSelected: false
};
}
}
});
}
} }

View File

@ -7,12 +7,17 @@
import "./main.css"; // CSS import. import "./main.css"; // CSS import.
import VisualConsole from "./VisualConsole"; import VisualConsole from "./VisualConsole";
import * as Form from "./Form";
import AsyncTaskManager from "./lib/AsyncTaskManager"; import AsyncTaskManager from "./lib/AsyncTaskManager";
// Export the VisualConsole class to the global object. // Export the VisualConsole class to the global object.
// eslint-disable-next-line // eslint-disable-next-line
(window as any).VisualConsole = VisualConsole; (window as any).VisualConsole = VisualConsole;
// Export the VisualConsole's Form classes to the global object.
// eslint-disable-next-line
(window as any).VisualConsole.Form = Form;
// Export the AsyncTaskManager class to the global object. // Export the AsyncTaskManager class to the global object.
// eslint-disable-next-line // eslint-disable-next-line
(window as any).AsyncTaskManager = AsyncTaskManager; (window as any).AsyncTaskManager = AsyncTaskManager;

View File

@ -284,7 +284,10 @@ export function itemMetaDecoder(data: UnknownObject): ItemMeta | never {
editMode: parseBoolean(data.editMode), editMode: parseBoolean(data.editMode),
isFromCache: parseBoolean(data.isFromCache), isFromCache: parseBoolean(data.isFromCache),
isFetching: false, isFetching: false,
isUpdating: false isUpdating: false,
isBeingMoved: false,
isBeingResized: false,
isSelected: false
}; };
} }
@ -428,14 +431,17 @@ function getOffset(el: HTMLElement | null) {
* *
* @param element Element to move. * @param element Element to move.
* @param onMoved Function to execute when the element moves. * @param onMoved Function to execute when the element moves.
* @param altContainer Alternative element to contain the moved element.
* *
* @return A function which will clean the event handlers when executed. * @return A function which will clean the event handlers when executed.
*/ */
export function addMovementListener( export function addMovementListener(
element: HTMLElement, element: HTMLElement,
onMoved: (x: Position["x"], y: Position["y"]) => void onMoved: (x: Position["x"], y: Position["y"]) => void,
altContainer?: HTMLElement
): Function { ): Function {
const container = element.parentElement as HTMLElement; const container = altContainer || (element.parentElement as HTMLElement);
// Store the initial draggable state. // Store the initial draggable state.
const isDraggable = element.draggable; const isDraggable = element.draggable;
// Init the coordinates. // Init the coordinates.
@ -747,3 +753,7 @@ export function addResizementListener(
handleEnd(); handleEnd();
}; };
} }
export function t(text: string): string {
return text;
}

View File

@ -56,5 +56,8 @@ export interface ItemMeta {
isFromCache: boolean; isFromCache: boolean;
isFetching: boolean; isFetching: boolean;
isUpdating: boolean; isUpdating: boolean;
isSelected: boolean;
isBeingMoved: boolean;
isBeingResized: boolean;
editMode: boolean; editMode: boolean;
} }

View File

@ -32,3 +32,10 @@
background: url(./resize-handle.svg); background: url(./resize-handle.svg);
cursor: se-resize; cursor: se-resize;
} }
.visual-console-item.is-editing.is-selected {
border: 2px dashed #2b2b2b;
transform: translateX(-2px) translateY(-2px);
cursor: move;
user-select: none;
}