Merge branch 'new-vc-line-element' of https://brutus.artica.lan:8081/artica/pandorafms into new-vc-line-element

This commit is contained in:
Alejandro Gallardo Escobar 2019-08-12 08:23:47 +02:00
commit 996e7df1db
9 changed files with 569 additions and 24 deletions

View File

@ -110,10 +110,13 @@ if ($getVisualConsole === true) {
return; return;
} else if ($updateVisualConsoleItem === true) { } else if ($updateVisualConsoleItem === true) {
$data = get_parameter('data'); $data = get_parameter('data');
if ($data) {
$data['id'] = $itemId; $data['id'] = $itemId;
$result = $item->save($data); $result = $item->save($data);
echo $item; echo $item;
}
return; return;
} }
} else if ($removeVisualConsoleItem === true) { } else if ($removeVisualConsoleItem === true) {

View File

@ -1816,7 +1816,7 @@ class Item extends CachedModel
// Invalidate the item's cache. // Invalidate the item's cache.
if ($result !== false && $result > 0) { if ($result !== false && $result > 0) {
// TODO: Invalidate the cache with the function clearCachedData. // TODO: Invalidate the cache with the function clearCachedData.
db_process_sql_delete( \db_process_sql_delete(
'tvisual_console_elements_cache', 'tvisual_console_elements_cache',
[ [
'vc_item_id' => (int) $save['id'], 'vc_item_id' => (int) $save['id'],

View File

@ -28,6 +28,50 @@ final class ColorCloud extends Item
protected static $useLinkedModule = true; protected static $useLinkedModule = true;
/**
* Return a valid representation of a record in database.
*
* @param array $data Input data.
*
* @return array Data structure representing a record in database.
*
* @overrides Item->encode.
*/
protected function encode(array $data): array
{
$return = parent::encode($data);
$defaultColor = null;
if (isset($data['defaultColor']) === true) {
$defaultColor = static::extractDefaultColor($data);
}
$colorRanges = null;
if (isset($data['colorRanges']) === true) {
$colorRanges = static::extractColorRanges($data);
}
if (empty($data['id']) === true) {
$return['label'] = json_encode(
[
'default_color' => $defaultColor,
'color_ranges' => $colorRanges,
]
);
} else {
$prevData = $this->toArray();
$return['label'] = json_encode(
[
'default_color' => ($defaultColor !== null) ? $defaultColor : $prevData['defaultColor'],
'color_ranges' => ($colorRanges !== null) ? $colorRanges : $prevData['colorRanges'],
]
);
}
return $return;
}
/** /**
* Returns a valid representation of the model. * Returns a valid representation of the model.
* *

View File

@ -1197,7 +1197,9 @@ abstract class VisualConsoleItem<Props extends ItemProps> {
} }
box.className = "visual-console-item"; box.className = "visual-console-item";
box.style.zIndex = this.props.isOnTop ? "2" : "1"; if (this.props.isOnTop) {
box.classList.add("is-on-top");
}
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`;
@ -1470,6 +1472,14 @@ abstract class VisualConsoleItem<Props extends ItemProps> {
if (!prevProps || prevProps.labelPosition !== this.props.labelPosition) { if (!prevProps || prevProps.labelPosition !== this.props.labelPosition) {
this.changeLabelPosition(this.props.labelPosition); this.changeLabelPosition(this.props.labelPosition);
} }
//Change z-index class is-on-top
if (!prevProps || prevProps.isOnTop !== this.props.isOnTop) {
if (this.props.isOnTop) {
this.elementRef.classList.add("is-on-top");
} else {
this.elementRef.classList.remove("is-on-top");
}
}
// Change link. // Change link.
if ( if (
prevProps && prevProps &&

View File

@ -318,19 +318,9 @@ export default class VisualConsole {
// Force the first render. // Force the first render.
this.render(); this.render();
// Sort by isOnTop, id ASC // Sort by id ASC
items = items.sort(function(a, b) { items = items.sort(function(a, b) {
if ( if (a.id == null || b.id == null) return 0;
a.isOnTop == null ||
b.isOnTop == null ||
a.id == null ||
b.id == null
) {
return 0;
}
if (a.isOnTop && !b.isOnTop) return 1;
else if (!a.isOnTop && b.isOnTop) return -1;
else if (a.id > b.id) return 1; else if (a.id > b.id) return 1;
else return -1; else return -1;
}); });

View File

@ -1,6 +1,7 @@
import { AnyObject } from "../lib/types"; import { AnyObject } from "../lib/types";
import { parseIntOr, notEmptyStringOr } from "../lib"; import { parseIntOr, notEmptyStringOr, t } from "../lib";
import Item, { ItemType, ItemProps, itemBasePropsDecoder } from "../Item"; import Item, { ItemType, ItemProps, itemBasePropsDecoder } from "../Item";
import { InputGroup, FormContainer } from "../Form";
interface BoxProps extends ItemProps { interface BoxProps extends ItemProps {
// Overrided properties. // Overrided properties.
@ -39,6 +40,95 @@ export function boxPropsDecoder(data: AnyObject): BoxProps | never {
}; };
} }
/**
* Class to add item to the Box item form
* This item consists of a label and a color type input color.
* Element border color is stored in the borderColor property
*/
class BorderColorInputGroup extends InputGroup<Partial<BoxProps>> {
protected createContent(): HTMLElement | HTMLElement[] {
const borderColorLabel = document.createElement("label");
borderColorLabel.textContent = t("Border color");
const borderColorInput = document.createElement("input");
borderColorInput.type = "color";
borderColorInput.required = true;
borderColorInput.value = `${this.currentData.borderColor ||
this.initialData.borderColor ||
"#000000"}`;
borderColorInput.addEventListener("change", e => {
this.updateData({
borderColor: (e.target as HTMLInputElement).value
});
});
borderColorLabel.appendChild(borderColorInput);
return borderColorLabel;
}
}
/**
* Class to add item to the Box item form
* This item consists of a label and a color type input number.
* Element border width is stored in the borderWidth property
*/
class BorderWidthInputGroup extends InputGroup<Partial<BoxProps>> {
protected createContent(): HTMLElement | HTMLElement[] {
const borderWidthLabel = document.createElement("label");
borderWidthLabel.textContent = t("Border Width");
const borderWidthInput = document.createElement("input");
borderWidthInput.type = "number";
borderWidthInput.min = "0";
borderWidthInput.required = true;
borderWidthInput.value = `${this.currentData.borderWidth ||
this.initialData.borderWidth ||
0}`;
borderWidthInput.addEventListener("change", e =>
this.updateData({
borderWidth: parseIntOr((e.target as HTMLInputElement).value, 0)
})
);
borderWidthLabel.appendChild(borderWidthInput);
return borderWidthLabel;
}
}
/**
* Class to add item to the Box item form
* This item consists of a label and a color type input color.
* Element fill color is stored in the fillcolor property
*/
class FillColorInputGroup extends InputGroup<Partial<BoxProps>> {
protected createContent(): HTMLElement | HTMLElement[] {
const fillColorLabel = document.createElement("label");
fillColorLabel.textContent = t("Fill color");
const fillColorInput = document.createElement("input");
fillColorInput.type = "color";
fillColorInput.required = true;
fillColorInput.value = `${this.currentData.fillColor ||
this.initialData.fillColor ||
"#000000"}`;
fillColorInput.addEventListener("change", e => {
this.updateData({
fillColor: (e.target as HTMLInputElement).value
});
});
fillColorLabel.appendChild(fillColorInput);
return fillColorLabel;
}
}
export default class Box extends Item<BoxProps> { export default class Box extends Item<BoxProps> {
protected createDomElement(): HTMLElement { protected createDomElement(): HTMLElement {
const box: HTMLDivElement = document.createElement("div"); const box: HTMLDivElement = document.createElement("div");
@ -65,4 +155,46 @@ export default class Box extends Item<BoxProps> {
return box; return box;
} }
/**
* To update the content element.
* @override Item.updateDomElement
*/
protected updateDomElement(element: HTMLElement): void {
if (this.props.fillColor) {
element.style.backgroundColor = this.props.fillColor;
}
// Border.
if (this.props.borderWidth > 0) {
element.style.borderStyle = "solid";
// Control the max width to prevent this item to expand beyond its parent.
const maxBorderWidth = Math.min(this.props.width, this.props.height) / 2;
const borderWidth = Math.min(this.props.borderWidth, maxBorderWidth);
element.style.borderWidth = `${borderWidth}px`;
if (this.props.borderColor) {
element.style.borderColor = this.props.borderColor;
}
}
}
/**
* @override function to add or remove inputsGroups those that are not necessary.
* Add to:
* LinkConsoleInputGroup
*/
public getFormContainer(): FormContainer {
const formContainer = super.getFormContainer();
formContainer.addInputGroup(
new BorderColorInputGroup("border-color", this.props)
);
formContainer.addInputGroup(
new BorderWidthInputGroup("border-width", this.props)
);
formContainer.addInputGroup(
new FillColorInputGroup("fill-width", this.props)
);
return formContainer;
}
} }

View File

@ -3,18 +3,26 @@ import {
LinkedVisualConsoleProps, LinkedVisualConsoleProps,
AnyObject AnyObject
} from "../lib/types"; } from "../lib/types";
import { modulePropsDecoder, linkedVCPropsDecoder } from "../lib"; import { modulePropsDecoder, linkedVCPropsDecoder, t } from "../lib";
import Item, { import Item, {
itemBasePropsDecoder, itemBasePropsDecoder,
ItemType, ItemType,
ItemProps, ItemProps,
LinkConsoleInputGroup LinkConsoleInputGroup
} from "../Item"; } from "../Item";
import { FormContainer } from "../Form"; import { FormContainer, InputGroup } from "../Form";
import fontAwesomeIcon from "../lib/FontAwesomeIcon";
import { faTrashAlt, faPlusCircle } from "@fortawesome/free-solid-svg-icons";
export type ColorCloudProps = { export type ColorCloudProps = {
type: ItemType.COLOR_CLOUD; type: ItemType.COLOR_CLOUD;
color: string; color: string;
defaultColor: string;
colorRanges: {
color: string;
fromValue: number;
toValue: number;
}[];
// TODO: Add the rest of the color cloud values? // TODO: Add the rest of the color cloud values?
} & ItemProps & } & ItemProps &
WithModuleProps & WithModuleProps &
@ -41,11 +49,292 @@ export function colorCloudPropsDecoder(
...itemBasePropsDecoder(data), // Object spread. It will merge the properties of the two objects. ...itemBasePropsDecoder(data), // Object spread. It will merge the properties of the two objects.
type: ItemType.COLOR_CLOUD, type: ItemType.COLOR_CLOUD,
color: data.color, color: data.color,
defaultColor: data.defaultColor,
colorRanges: data.colorRanges,
...modulePropsDecoder(data), // Object spread. It will merge the properties of the two objects. ...modulePropsDecoder(data), // Object spread. It will merge the properties of the two objects.
...linkedVCPropsDecoder(data) // Object spread. It will merge the properties of the two objects. ...linkedVCPropsDecoder(data) // Object spread. It will merge the properties of the two objects.
}; };
} }
/**
* Class to add item to the Color cloud item form
* This item consists of a label and a color type input color.
* Element default color is stored in the color property
*/
class ColorInputGroup extends InputGroup<Partial<ColorCloudProps>> {
protected createContent(): HTMLElement | HTMLElement[] {
const ColorLabel = document.createElement("label");
ColorLabel.textContent = t("Default color");
const ColorInput = document.createElement("input");
ColorInput.type = "color";
ColorInput.required = true;
ColorInput.value = `${this.currentData.defaultColor ||
this.initialData.defaultColor ||
"#000000"}`;
ColorInput.addEventListener("change", e => {
this.updateData({
defaultColor: (e.target as HTMLInputElement).value
});
});
ColorLabel.appendChild(ColorInput);
return ColorLabel;
}
}
type ColorRanges = ColorCloudProps["colorRanges"];
type ColorRange = ColorRanges[0];
class RangesInputGroup extends InputGroup<Partial<ColorCloudProps>> {
protected createContent(): HTMLElement | HTMLElement[] {
const rangesLabel = this.createLabel("Ranges");
const rangesControlsContainer = document.createElement("div");
const createdRangesContainer = document.createElement("div");
rangesControlsContainer.appendChild(createdRangesContainer);
rangesLabel.appendChild(rangesControlsContainer);
const colorRanges =
this.currentData.colorRanges || this.initialData.colorRanges || [];
let buildRanges: (ranges: ColorRanges) => void;
const handleRangeUpdatePartial = (index: number) => (
range: ColorRange
): void => {
const colorRanges =
this.currentData.colorRanges || this.initialData.colorRanges || [];
this.updateData({
colorRanges: [
...colorRanges.slice(0, index),
range,
...colorRanges.slice(index)
]
});
};
const handleDelete = (index: number) => () => {
const colorRanges =
this.currentData.colorRanges || this.initialData.colorRanges || [];
const newRanges = [
...colorRanges.slice(0, index),
...colorRanges.slice(index + 1)
];
this.updateData({
colorRanges: newRanges
});
buildRanges(newRanges);
};
const handleCreate = (range: ColorRange): void => {
const colorRanges =
this.currentData.colorRanges || this.initialData.colorRanges || [];
const newRanges = [...colorRanges, range];
this.updateData({
colorRanges: newRanges
});
buildRanges(newRanges);
};
buildRanges = ranges => {
createdRangesContainer.innerHTML = "";
console.log(ranges);
ranges.forEach((colorRange, index) =>
createdRangesContainer.appendChild(
this.rangeContainer(
colorRange,
handleRangeUpdatePartial(index),
handleDelete(index)
)
)
);
};
buildRanges(colorRanges);
rangesControlsContainer.appendChild(
this.initialRangeContainer(handleCreate)
);
return rangesLabel;
}
private initialRangeContainer(onCreate: (range: ColorRange) => void) {
// TODO: Document
const initialState = { color: "#fff" };
const state: Partial<ColorRange> = initialState;
const handleFromValue = (value: ColorRange["fromValue"]): void => {
state.fromValue = value;
};
const handleToValue = (value: ColorRange["toValue"]): void => {
state.toValue = value;
};
const handleColor = (value: ColorRange["color"]): void => {
state.color = value;
};
// User defined type guard.
// Docs: https://www.typescriptlang.org/docs/handbook/advanced-types.html#user-defined-type-guards
const isValid = (range: Partial<ColorRange>): range is ColorRange =>
typeof range.color !== "undefined" &&
typeof range.toValue !== "undefined" &&
typeof range.fromValue !== "undefined";
const handleCreate = () => {
if (isValid(state)) onCreate(state);
};
const rangesContainer = document.createElement("div");
// Div From value.
const rangesContainerFromValue = document.createElement("div");
const rangesLabelFromValue = this.createLabel("From Value");
const rangesInputFromValue = this.createInputNumber(null, handleFromValue);
rangesContainerFromValue.appendChild(rangesLabelFromValue);
rangesContainerFromValue.appendChild(rangesInputFromValue);
rangesContainer.appendChild(rangesContainerFromValue);
// Div To Value.
const rangesDivContainerToValue = document.createElement("div");
const rangesLabelToValue = this.createLabel("To Value");
const rangesInputToValue = this.createInputNumber(null, handleToValue);
rangesContainerFromValue.appendChild(rangesLabelToValue);
rangesContainerFromValue.appendChild(rangesInputToValue);
rangesContainer.appendChild(rangesDivContainerToValue);
// Div Color.
const rangesDivContainerColor = document.createElement("div");
const rangesLabelColor = this.createLabel("Color");
const rangesInputColor = this.createInputColor(
initialState.color,
handleColor
);
rangesContainerFromValue.appendChild(rangesLabelColor);
rangesContainerFromValue.appendChild(rangesInputColor);
rangesContainer.appendChild(rangesDivContainerColor);
// Button delete.
const createBtn = document.createElement("a");
createBtn.appendChild(
fontAwesomeIcon(faPlusCircle, t("Create color range"))
);
createBtn.addEventListener("click", handleCreate);
rangesContainer.appendChild(createBtn);
return rangesContainer;
}
private rangeContainer(
colorRange: ColorRange,
onUpdate: (range: ColorRange) => void,
onDelete: () => void
): HTMLDivElement {
// TODO: Document
const state = { ...colorRange };
const handleFromValue = (value: ColorRange["fromValue"]): void => {
state.fromValue = value;
onUpdate({ ...state });
};
const handleToValue = (value: ColorRange["toValue"]): void => {
state.toValue = value;
onUpdate({ ...state });
};
const handleColor = (value: ColorRange["color"]): void => {
state.color = value;
onUpdate({ ...state });
};
const rangesContainer = document.createElement("div");
// Div From value.
const rangesContainerFromValue = document.createElement("div");
const rangesLabelFromValue = this.createLabel("From Value");
const rangesInputFromValue = this.createInputNumber(
colorRange.fromValue,
handleFromValue
);
rangesContainerFromValue.appendChild(rangesLabelFromValue);
rangesContainerFromValue.appendChild(rangesInputFromValue);
rangesContainer.appendChild(rangesContainerFromValue);
// Div To Value.
const rangesDivContainerToValue = document.createElement("div");
const rangesLabelToValue = this.createLabel("To Value");
const rangesInputToValue = this.createInputNumber(
colorRange.toValue,
handleToValue
);
rangesContainerFromValue.appendChild(rangesLabelToValue);
rangesContainerFromValue.appendChild(rangesInputToValue);
rangesContainer.appendChild(rangesDivContainerToValue);
// Div Color.
const rangesDivContainerColor = document.createElement("div");
const rangesLabelColor = this.createLabel("Color");
const rangesInputColor = this.createInputColor(
colorRange.color,
handleColor
);
rangesContainerFromValue.appendChild(rangesLabelColor);
rangesContainerFromValue.appendChild(rangesInputColor);
rangesContainer.appendChild(rangesDivContainerColor);
// Button delete.
const deleteBtn = document.createElement("a");
deleteBtn.appendChild(fontAwesomeIcon(faTrashAlt, t("Delete color range")));
deleteBtn.addEventListener("click", onDelete);
rangesContainer.appendChild(deleteBtn);
return rangesContainer;
}
private createLabel(text: string): HTMLLabelElement {
const label = document.createElement("label");
label.textContent = t(text);
return label;
}
private createInputNumber(
value: number | null,
onUpdate: (value: number) => void
): HTMLInputElement {
const input = document.createElement("input");
input.type = "number";
input.required = true;
if (value !== null) input.value = `${value}`;
input.addEventListener("change", e => {
const value = parseInt((e.target as HTMLInputElement).value);
if (!isNaN(value)) onUpdate(value);
});
return input;
}
private createInputColor(
value: string | null,
onUpdate: (value: string) => void
): HTMLInputElement {
const input = document.createElement("input");
input.type = "color";
input.required = true;
if (value !== null) input.value = value;
input.addEventListener("change", e =>
onUpdate((e.target as HTMLInputElement).value)
);
return input;
}
}
const svgNS = "http://www.w3.org/2000/svg"; const svgNS = "http://www.w3.org/2000/svg";
export default class ColorCloud extends Item<ColorCloudProps> { export default class ColorCloud extends Item<ColorCloudProps> {
@ -112,12 +401,18 @@ export default class ColorCloud extends Item<ColorCloudProps> {
* @override function to add or remove inputsGroups those that are not necessary. * @override function to add or remove inputsGroups those that are not necessary.
* Add to: * Add to:
* LinkConsoleInputGroup * LinkConsoleInputGroup
* ColorInputGroup
* RangesInputGroup
*/ */
public getFormContainer(): FormContainer { public getFormContainer(): FormContainer {
const formContainer = super.getFormContainer(); const formContainer = super.getFormContainer();
formContainer.addInputGroup( formContainer.addInputGroup(
new LinkConsoleInputGroup("link-console", this.props) new LinkConsoleInputGroup("link-console", this.props)
); );
formContainer.addInputGroup(new ColorInputGroup("color-cloud", this.props));
formContainer.addInputGroup(
new RangesInputGroup("ranges-cloud", this.props)
);
return formContainer; return formContainer;
} }
} }

View File

@ -5,15 +5,17 @@ import {
notEmptyStringOr, notEmptyStringOr,
stringIsEmpty, stringIsEmpty,
decodeBase64, decodeBase64,
parseBoolean parseBoolean,
t
} from "../lib"; } from "../lib";
import Item, { import Item, {
ItemProps, ItemProps,
itemBasePropsDecoder, itemBasePropsDecoder,
ItemType, ItemType,
LinkConsoleInputGroup LinkConsoleInputGroup,
ImageInputGroup
} from "../Item"; } from "../Item";
import { FormContainer } from "../Form"; import { FormContainer, InputGroup } from "../Form";
export type GroupProps = { export type GroupProps = {
type: ItemType.GROUP_ITEM; type: ItemType.GROUP_ITEM;
@ -66,6 +68,39 @@ export function groupPropsDecoder(data: AnyObject): GroupProps | never {
}; };
} }
// TODO: Document
class ShowStatisticsInputGroup extends InputGroup<Partial<GroupProps>> {
protected createContent(): HTMLElement | HTMLElement[] {
const showStatisticsLabel = document.createElement("label");
showStatisticsLabel.textContent = t("Show statistics");
const showStatisticsInputChkbx = document.createElement("input");
showStatisticsInputChkbx.id = "checkbox-switch";
showStatisticsInputChkbx.className = "checkbox-switch";
showStatisticsInputChkbx.type = "checkbox";
showStatisticsInputChkbx.name = "checkbox-enable-link";
showStatisticsInputChkbx.value = "1";
showStatisticsInputChkbx.checked =
this.currentData.showStatistics ||
this.initialData.showStatistics ||
false;
showStatisticsInputChkbx.addEventListener("change", e =>
this.updateData({
showStatistics: (e.target as HTMLInputElement).checked
})
);
const linkInputLabel = document.createElement("label");
linkInputLabel.className = "label-switch";
linkInputLabel.htmlFor = "checkbox-switch";
showStatisticsLabel.appendChild(showStatisticsInputChkbx);
showStatisticsLabel.appendChild(linkInputLabel);
return showStatisticsLabel;
}
}
export default class Group extends Item<GroupProps> { export default class Group extends Item<GroupProps> {
protected createDomElement(): HTMLElement { protected createDomElement(): HTMLElement {
const element = document.createElement("div"); const element = document.createElement("div");
@ -73,7 +108,8 @@ export default class Group extends Item<GroupProps> {
if (!this.props.showStatistics && this.props.statusImageSrc !== null) { if (!this.props.showStatistics && this.props.statusImageSrc !== null) {
// Icon with status. // Icon with status.
element.style.background = `url(${this.props.statusImageSrc}) no-repeat`; element.style.backgroundImage = `url(${this.props.statusImageSrc})`;
element.style.backgroundRepeat = "no-repeat";
element.style.backgroundSize = "contain"; element.style.backgroundSize = "contain";
element.style.backgroundPosition = "center"; element.style.backgroundPosition = "center";
} else if (this.props.showStatistics && this.props.html != null) { } else if (this.props.showStatistics && this.props.html != null) {
@ -84,16 +120,46 @@ export default class Group extends Item<GroupProps> {
return element; return element;
} }
/**
* To update the content element.
* @override Item.updateDomElement
*/
protected updateDomElement(element: HTMLElement): void {
if (!this.props.showStatistics && this.props.statusImageSrc !== null) {
// Icon with status.
element.style.backgroundImage = `url(${this.props.statusImageSrc})`;
element.style.backgroundRepeat = "no-repeat";
element.style.backgroundSize = "contain";
element.style.backgroundPosition = "center";
element.innerHTML = "";
} else if (this.props.showStatistics && this.props.html != null) {
// Stats table.
element.innerHTML = this.props.html;
}
}
/** /**
* @override function to add or remove inputsGroups those that are not necessary. * @override function to add or remove inputsGroups those that are not necessary.
* Add to: * Add to:
* LinkConsoleInputGroup * LinkConsoleInputGroup
* ImageInputGroup
* ShowStatisticsInputGroup
*/ */
public getFormContainer(): FormContainer { public getFormContainer(): FormContainer {
const formContainer = super.getFormContainer(); const formContainer = super.getFormContainer();
formContainer.addInputGroup( formContainer.addInputGroup(
new LinkConsoleInputGroup("link-console", this.props) new LinkConsoleInputGroup("link-console", this.props)
); );
formContainer.addInputGroup(
new ImageInputGroup("image-console", {
...this.props,
imageKey: "imageSrc",
showStatusImg: true
})
);
formContainer.addInputGroup(
new ShowStatisticsInputGroup("show-statistic", this.props)
);
return formContainer; return formContainer;
} }
} }

View File

@ -13,6 +13,11 @@
justify-items: center; justify-items: center;
align-items: center; align-items: center;
user-select: text; user-select: text;
z-index: 1;
}
.visual-console-item.is-on-top {
z-index: 2;
} }
.visual-console-item.is-editing { .visual-console-item.is-editing {