Browse Source

UI: Improve SVG object edit.

pull/11391/head
Igor Kulikov 2 years ago
parent
commit
cafd260c84
  1. 410
      ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts

410
ui-ngx/src/app/modules/home/components/widget/lib/svg/iot-svg.models.ts

@ -15,7 +15,8 @@
///
import { ValueType } from '@shared/models/constants';
import { Box, Element, Runner, Svg, SVG, Text } from '@svgdotjs/svg.js';
import * as svgjs from '@svgdotjs/svg.js';
import { Box, Element, Rect, Runner, SVG, Svg, Text } from '@svgdotjs/svg.js';
import {
DataToValueType,
GetValueAction,
@ -32,7 +33,7 @@ import {
mergeDeep,
parseFunction
} from '@core/utils';
import { BehaviorSubject, forkJoin, Observable, Observer } from 'rxjs';
import { BehaviorSubject, forkJoin, from, Observable, Observer } from 'rxjs';
import { map, share } from 'rxjs/operators';
import { ValueAction, ValueGetter, ValueSetter } from '@home/components/widget/lib/action/action-widget.models';
import { WidgetContext } from '@home/models/widget-component.models';
@ -41,6 +42,9 @@ import { AttributeScope } from '@shared/models/telemetry/telemetry.models';
import { UtilsService } from '@core/services/utils.service';
import { WidgetAction, WidgetActionType } from '@shared/models/widget.models';
import { ResizeObserver } from '@juggle/resize-observer';
import ITooltipsterInstance = JQueryTooltipster.ITooltipsterInstance;
import ITooltipPosition = JQueryTooltipster.ITooltipPosition;
import ITooltipsterHelper = JQueryTooltipster.ITooltipsterHelper;
export interface IotSvgApi {
formatValue: (value: any, dec?: number, units?: string, showZeroDecimals?: boolean) => string | undefined;
@ -273,10 +277,11 @@ const parseError = (ctx: WidgetContext, err: any): string =>
export class IotSvgEditObject {
private svgShape: Svg;
public svgShape: Svg;
private box: Box;
private shapeResize$: ResizeObserver;
private elements: IotSvgElement[] = [];
private readonly shapeResize$: ResizeObserver;
public scale = 1;
constructor(private rootElement: HTMLElement) {
this.shapeResize$ = new ResizeObserver(() => {
this.resize();
@ -296,6 +301,69 @@ export class IotSvgEditObject {
this.svgShape.size(this.box.width, this.box.height);
this.svgShape.addTo(this.rootElement);
this.resize();
//this.svgShape.cre
this.svgShape.style().rule('.hovered', {filter: 'drop-shadow(0px 0px 1px #FFC107)'});
//this.svgShape.style().rule('.hovered', {filter: 'opacity(50%)'});
this.svgShape.style().rule('.tb-element', {cursor: 'pointer', transition: '0.2s filter ease-in-out'});
(window as any).SVG = svgjs;
forkJoin([
from(import('tooltipster')),
from(import('tooltipster/dist/js/plugins/tooltipster/SVG/tooltipster-SVG.min.js'))
]).subscribe(() => {
this.setupElements();
});
}
private setupElements() {
this.svgShape.children().forEach(child => {
this.addElement(child);
});
const overlappingGroups: IotSvgElement[][] = [];
for (const el of this.elements) {
for (const other of this.elements) {
if (el !== other && el.overlappingCenters(other)) {
let overlappingGroup: IotSvgElement[];
for (const list of overlappingGroups) {
if (list.includes(other) || list.includes(el)) {
overlappingGroup = list;
break;
}
}
if (!overlappingGroup) {
overlappingGroup = [el, other];
overlappingGroups.push(overlappingGroup);
} else {
if (!overlappingGroup.includes(el)) {
overlappingGroup.push(el);
} else if (!overlappingGroup.includes(other)){
overlappingGroup.push(other);
}
}
}
}
}
for (const group of overlappingGroups) {
let offset = - (elementTooltipMinHeight * group.length) / 2 + elementTooltipMinHeight / 2;
for (const element of group) {
element.innerTooltipOffset = offset;
offset += elementTooltipMinHeight;
}
}
for (const el of this.elements) {
el.init();
}
}
private addElement(e: Element) {
if (hasBBox(e)) {
const iotSvgElement = new IotSvgElement(this, e);
this.elements.push(iotSvgElement);
e.children().forEach(child => {
if (!(child.type === 'tspan' && e.type === 'text')) {
this.addElement(child);
}
}, true);
}
}
public destroy() {
@ -308,16 +376,340 @@ export class IotSvgEditObject {
if (this.svgShape) {
const targetWidth = this.rootElement.getBoundingClientRect().width;
const targetHeight = this.rootElement.getBoundingClientRect().height;
let scale: number;
if (targetWidth < targetHeight) {
scale = targetWidth / this.box.width;
this.scale = targetWidth / this.box.width;
} else {
this.scale = targetHeight / this.box.height;
}
this.svgShape.node.style.transform = `scale(${this.scale})`;
}
}
}
const hasBBox = (e: Element): boolean => {
try {
if (e.bbox) {
e.bbox();
return true;
} else {
return false;
}
} catch (_e) {
return false;
}
};
const textTooltip = (el: JQuery<any>, text: string) => {
el.tooltipster({
theme: ['tooltipster-tb'],
trigger: 'hover',
content: text
});
};
const elementTooltipMinHeight = 36 + 8;
const elementTooltipMinWidth = 100;
const groupRectStroke = 10;
class IotSvgElement {
private highlightRect: Rect;
private tooltip: ITooltipsterInstance;
private tag: string;
public innerTooltipOffset = 0;
public readonly box: Box;
private highlighted = false;
constructor(private editObject: IotSvgEditObject,
private element: Element) {
this.tag = element.attr('tb:tag');
this.box = element.rbox(this.editObject.svgShape);
}
public init() {
if (this.isGroup()) {
this.highlightRect =
this.editObject.svgShape
.rect(this.box.width + this.unscaled(groupRectStroke * 4), this.box.height + this.unscaled(groupRectStroke * 4))
.x(this.box.x - this.unscaled(groupRectStroke * 2))
.y(this.box.y - this.unscaled(groupRectStroke * 2))
.attr({fill: 'none', stroke: '#ccc', 'stroke-width': this.unscaled(groupRectStroke), opacity: 0});
this.highlightRect.hide();
} else {
this.element.addClass('tb-element');
}
this.element.on('mouseenter', (event) => {
this.highlight();
});
this.element.on('mouseleave', (event) => {
this.unhighlight();
});
if (this.hasTag()) {
this.createTagTooltip();
} else {
this.createAddTagTooltip();
}
}
public overlappingCenters(otherElement: IotSvgElement): boolean {
if (this.isGroup() || otherElement.isGroup()) {
return false;
}
return Math.abs(this.box.cx - otherElement.box.cx) * this.editObject.scale < elementTooltipMinWidth &&
Math.abs(this.box.cy - otherElement.box.cy) * this.editObject.scale < elementTooltipMinHeight;
}
public highlight() {
if (!this.highlighted) {
this.highlighted = true;
if (this.isGroup()) {
this.highlightRect.width(this.box.width + this.unscaled(groupRectStroke * 4))
.height(this.box.height + this.unscaled(groupRectStroke * 4))
.x(this.box.x - this.unscaled(groupRectStroke * 2))
.y(this.box.y - this.unscaled(groupRectStroke * 2))
.attr({'stroke-width': this.unscaled(groupRectStroke)});
this.highlightRect.show();
this.highlightRect.animate(300).attr({opacity: 1});
} else {
scale = targetHeight / this.box.height;
this.element.addClass('hovered');
}
if (this.hasTag()) {
this.tooltip.reposition();
}
}
}
public unhighlight() {
if (this.highlighted) {
this.highlighted = false;
if (this.isGroup()) {
this.highlightRect.animate(300).attr({opacity: 0}).after(() => {
this.highlightRect.hide();
});
} else {
this.element.removeClass('hovered');
}
this.svgShape.node.style.transform = `scale(${scale})`;
}
}
public clearTag() {
this.tooltip.destroy();
this.tag = null;
this.element.attr('tb:tag', null);
this.createAddTagTooltip();
}
public setTag(tag: string) {
this.tooltip.destroy();
this.tag = tag;
this.element.attr('tb:tag', tag);
this.createTagTooltip();
}
private unscaled(size: number): number {
return size / this.editObject.scale;
}
private createTagTooltip() {
const el = $(this.element.node);
el.tooltipster(
{
arrow: this.isGroup(),
distance: this.isGroup() ? 20 : 6,
theme: ['tooltipster-tb'],
delay: 0,
animationDuration: 0,
interactive: true,
trigger: 'custom',
side: 'top',
trackOrigin: true,
content: '',
functionPosition: (instance, helper, position) =>
this.innerTooltipPosition(instance, helper, position)
}
);
this.tooltip = el.tooltipster('instance');
this.setupTagPanel();
}
private setupTagPanel() {
const tagPanel =
$(`<div style="display: flex; flex-direction: row; align-items: center; gap: 8px;">
<span>${this.element.type}:</span>
<span><b>${this.tag}</b></span>
<span style="cursor: pointer;" class="edit-icon mat-icon tb-mat-18 material-icons">edit</span>
<span style="cursor: pointer;" class="delete-icon mat-icon tb-mat-18 material-icons">delete</span>
</div>`);
tagPanel.on('mouseenter', () => {
this.highlight();
});
tagPanel.on('mouseleave', () => {
this.unhighlight();
});
const updateTagButton = tagPanel.find('.edit-icon');
textTooltip(updateTagButton, 'Update tag');
updateTagButton.on('click', () => {
this.setupEditTagPanel();
});
const deleteButton = tagPanel.find('.delete-icon');
textTooltip(deleteButton, 'Remove tag');
deleteButton.on('click', () => {
this.clearTag();
});
this.tooltip.content(tagPanel);
this.tooltip.open();
}
private setupEditTagPanel() {
const editTagInputPanel =
$(`<div style="display: flex; flex-direction: row; align-items: center; gap: 8px;">
<span>Update tag:</span>
<input class="tag-input"/>
<span style="cursor: pointer;" class="apply-icon mat-icon tb-mat-18 material-icons">done</span>
<span style="cursor: pointer;" class="close-icon mat-icon tb-mat-18 material-icons">close</span>
</div>`);
const tagInput = editTagInputPanel.find('input.tag-input');
const applyTagButton = editTagInputPanel.find('span.apply-icon');
const closeButton = editTagInputPanel.find('span.close-icon');
textTooltip(applyTagButton, 'Apply');
textTooltip(closeButton, 'Cancel');
tagInput.val(this.tag);
let editPanelClosed = false;
tagInput.on('keypress', (event) => {
if (event.which === 13) {
const newTag: string = tagInput.val() as string;
if (newTag) {
editPanelClosed = true;
this.setTag(newTag);
}
}
});
applyTagButton.on('click', () => {
const newTag: string = tagInput.val() as string;
editPanelClosed = true;
if (newTag) {
this.setTag(newTag);
} else {
this.setupTagPanel();
}
});
closeButton.on('click', () => {
editPanelClosed = true;
this.setupTagPanel();
});
tagInput.on('blur', () => {
setTimeout(() => {
if (!editPanelClosed) {
editPanelClosed = true;
this.setupTagPanel();
}
});
});
this.tooltip.content(editTagInputPanel);
tagInput.trigger('focus');
}
private createAddTagTooltip() {
const el = $(this.element.node);
el.tooltipster(
{
arrow: this.isGroup(),
distance: this.isGroup() ? 20 : 6,
theme: ['tooltipster-tb'],
delay: 200,
interactive: true,
trigger: 'hover',
side: 'top',
trackOrigin: true,
content: '',
functionPosition: (instance, helper, position) =>
this.innerTooltipPosition(instance, helper, position)
}
);
this.tooltip = el.tooltipster('instance');
this.setupAddTagPanel();
}
private setupAddTagPanel() {
const addTagPanel =
$(`<div style="display: flex; flex-direction: row; align-items: center; gap: 8px;">
<span>${this.element.type}:</span>
<button class="add-tag-button" style="cursor: pointer;">Add tag</button>
</div>`);
const addTagButton = addTagPanel.find('.add-tag-button');
addTagButton.on('click', () => {
this.setupAddTagInputPanel();
});
this.tooltip.content(addTagPanel);
this.tooltip.off('closing');
}
private setupAddTagInputPanel() {
const addTagInputPanel =
$(`<div style="display: flex; flex-direction: row; align-items: center; gap: 8px;">
<span>Enter tag:</span>
<input class="tag-input"/>
<span style="cursor: pointer;" class="apply-icon mat-icon tb-mat-18 material-icons">done</span>
<span style="cursor: pointer;" class="close-icon mat-icon tb-mat-18 material-icons">close</span>
</div>`);
const tagInput = addTagInputPanel.find('input.tag-input');
const applyTagButton = addTagInputPanel.find('span.apply-icon');
const closeButton = addTagInputPanel.find('span.close-icon');
textTooltip(applyTagButton, 'Apply');
textTooltip(closeButton, 'Cancel');
tagInput.on('keypress', (event) => {
if (event.which === 13) {
const newTag: string = tagInput.val() as string;
if (newTag) {
this.setTag(newTag);
}
}
});
applyTagButton.on('click', () => {
const newTag: string = tagInput.val() as string;
if (newTag) {
this.setTag(newTag);
} else {
this.tooltip.close();
}
});
closeButton.on('click', () => {
this.tooltip.close();
});
this.tooltip.content(addTagInputPanel);
this.tooltip.on('closing', () => {
this.setupAddTagPanel();
});
tagInput.trigger('focus');
}
private innerTooltipPosition(instance: ITooltipsterInstance, helper: ITooltipsterHelper, position: ITooltipPosition): ITooltipPosition {
if (!this.isGroup()) {
const clientRect = helper.origin.getBoundingClientRect();
position.coord.top = clientRect.top + (clientRect.height - position.size.height) / 2
+ this.innerTooltipOffset;
position.coord.left = clientRect.left + (clientRect.width - position.size.width) / 2;
}
return position;
}
private hasTag() {
return !!this.tag;
}
private isGroup() {
return this.element.type === 'g';
}
}
export class IotSvgObject {

Loading…
Cancel
Save