@vaadin-component-factory/vcf-network
Version:
Hierarchical network visualizing web component
467 lines (436 loc) • 14.3 kB
JavaScript
/**
* @license
* Copyright (C) 2015 Vaadin Ltd.
* This program is available under Commercial Vaadin Add-On License 3.0 (CVALv3).
* See the file LICENSE.md distributed with this software for more information about licensing.
* See [the website]{@link https://vaadin.com/license/cval-3} for the complete license.
*/
import { html, PolymerElement } from '@polymer/polymer/polymer-element';
import { ThemableMixin } from '@vaadin/vaadin-themable-mixin/vaadin-themable-mixin';
import vis from 'vis-network/dist/vis-network.esm';
/**
* Right side panel of VCF Network.
* Contains buttons for adding nodes and custom component templates.
*/
class VcfNetworkToolPanel extends ThemableMixin(PolymerElement) {
static get template() {
return html`
<style include="lumo-typography">
.panel-container {
box-shadow: inset -1px 0 0 0 var(--lumo-shade-10pct);
display: flex;
flex-direction: column;
overflow: auto;
width: 240px;
flex-shrink: 0;
height: 100%;
transition: width 0.2s;
}
.panel-container.add-node-toggle .section-item.active {
animation: active 5s linear infinite;
}
@keyframes active {
0% {
box-shadow: inset 0px 0px 0px 1px var(--lumo-shade-10pct);
}
50% {
box-shadow: inset 0px 0px 20px 2px var(--lumo-shade-20pct);
}
100% {
box-shadow: inset 0px 0px 0px 1px var(--lumo-shade-10pct);
}
}
:host([hidden]) {
display: none ;
}
/* Section */
.section:not(.collapsed) {
padding-bottom: var(--lumo-space-s);
}
.section:not(:last-child) {
box-shadow: inset 0 -1px 0 0 var(--lumo-shade-10pct);
}
.section.collapsed .section-items {
max-height: 0 ;
}
/* Section header */
.section-header {
align-items: center;
cursor: pointer;
display: flex;
height: var(--lumo-size-l);
padding: 0 var(--lumo-space-m);
}
.section-header h6 {
color: var(--lumo-secondary-text-color);
margin: 0 auto 0 0;
}
.section-header iron-icon {
color: var(--lumo-secondary-text-color);
transition: all 0.2s;
}
.section.collapsed .section-header iron-icon {
transform: rotate(180deg);
}
/* Section items */
.section-items {
overflow: hidden;
transform-origin: top;
transition: all 0.2s;
}
.section-item {
align-items: center;
cursor: pointer;
display: flex;
height: var(--lumo-size-l);
padding-left: var(--lumo-space-m);
transition: all 0.2s;
}
iron-icon.blue,
iron-icon.green,
iron-icon.red {
margin-right: var(--lumo-space-m);
}
.section-item vcf-network-color-option {
margin: auto var(--lumo-space-m) auto 0;
}
.section-item span {
color: var(--lumo-body-text-color);
font-size: var(--lumo-font-size-s);
transition: all 0.2s;
}
.section:first-child .section-item span::first-letter {
text-decoration: underline;
}
.section-item:hover {
background-color: var(--lumo-shade-5pct);
}
.section-item.active {
background-color: var(--lumo-primary-color-10pct);
}
.section-item.active span {
color: var(--lumo-primary-text-color);
font-weight: 500;
}
iron-icon.blue {
color: var(--lumo-primary-color);
}
iron-icon.green {
color: var(--lumo-success-text-color);
}
iron-icon.red {
color: var(--lumo-error-text-color);
}
.section-item {
display: flex;
align-items: center;
}
.section-item div {
flex-grow: 1;
display: flex;
height: 100%;
}
.section-item span {
display: block;
margin: auto 0;
}
.hidden {
display: none;
}
.edit-hidden .edit-template,
.edit-hidden .delete-template,
.edit-hidden #new-template-button {
display: none;
}
.section-grow {
flex-grow: 1;
}
.template-item {
margin: var(--lumo-space-s) var(--lumo-space-m);
}
.section-footer {
text-align: right;
cursor: pointer;
}
.no-templates {
margin: var(--lumo-space-s) var(--lumo-space-m);
color: var(--lumo-disabled-text-color);
}
/** Closed **/
.panel-container.closed {
width: 36px;
}
.closed span,
.closed h6,
.closed #template-panel {
display: none;
}
.closed .section-header {
padding: 0;
justify-content: center;
}
.closed .section-header,
.closed .section-item {
padding: 0;
justify-content: center;
}
.closed iron-icon.blue,
.closed iron-icon.green,
.closed iron-icon.red {
margin-right: 0;
}
.closed .section-footer iron-icon {
transform: rotate(180deg);
}
.closed .section-footer {
text-align: center;
}
</style>
<div id="tool-panel" class="panel-container">
<div class="section">
<div class="section-header">
<h6>Default</h6>
<iron-icon icon="vcf-network:keyboard-arrow-down"></iron-icon>
</div>
<div class="section-items">
<div id="add-node" class="section-item">
<iron-icon icon="vcf-network:format-shapes" class="blue"></iron-icon>
<span>Node</span>
</div>
<div id="add-input-node" class="section-item">
<iron-icon icon="vcf-network:exit-to-app" class="green"></iron-icon>
<span>Input Node</span>
</div>
<div id="add-output-node" class="section-item">
<iron-icon icon="vcf-network:exit-to-app" class="red"></iron-icon>
<span>Output Node</span>
</div>
</div>
</div>
<div id="template-panel" class="section edit-hidden">
<div class="section-header">
<h6>Template</h6>
<iron-icon icon="vcf-network:keyboard-arrow-down"></iron-icon>
</div>
<div class="section-items" id="custom">
<template is="dom-if" if="[[!components.length]]" on-dom-change="_initToolbar">
<div class="no-templates">No templates</div>
</template>
<template is="dom-if" if="[[components.length]]" on-dom-change="_initToolbar">
<template is="dom-repeat" items="[[components]]" on-dom-change="_initToolbar">
<div class="section-item">
<div on-click="_addComponentListener">
<vcf-network-color-option color="[[item.componentColor]]" class="icon"></vcf-network-color-option>
<span>[[item.label]]</span>
</div>
<vaadin-button
class="edit-template"
theme="tertiary small"
title="Edit template"
on-click="_updateTemplateListener"
>
<iron-icon icon="vcf-network:create" slot="prefix"></iron-icon>
</vaadin-button>
<vaadin-button
class="delete-template"
theme="tertiary error small"
title="Delete template"
on-click="_deleteTemplateListener"
>
<iron-icon icon="vcf-network:delete" slot="prefix"></iron-icon>
</vaadin-button>
</div>
</template>
</template>
<div id="new-template-button" class="template-item">
<vaadin-button style="flex-grow:1;" title="Add template" on-click="_addTemplateListener">
<iron-icon icon="vcf-network:add" slot="prefix"></iron-icon>
New template
</vaadin-button>
</div>
</div>
</div>
<div class="section section-grow"></div>
<div class="section-footer">
<iron-icon icon="vcf-network:keyboard-arrow-left"></iron-icon>
</div>
</div>
`;
}
static get is() {
return 'vcf-network-tool-panel';
}
static get properties() {
return {
addNodeToggle: {
type: Boolean,
observer: '_addNodeToggleChanged'
},
components: {
type: Array,
value: []
}
};
}
get _sectionItems() {
return this.shadowRoot.querySelectorAll('.section-item');
}
connectedCallback() {
super.connectedCallback();
this._initToolbar();
this._initEventListeners();
}
clear(exclude = null) {
this._sectionItems.forEach(item => {
if (!exclude || (exclude && exclude !== item)) item.classList.remove('active');
});
}
_initEventListeners() {
/* section-header */
const sectionHeaders = this.shadowRoot.querySelectorAll('.section-header');
sectionHeaders.forEach(header => {
const section = header.parentElement;
header.addEventListener('click', () => {
if (section.classList.contains('collapsed')) {
section.classList.remove('collapsed');
} else {
section.classList.add('collapsed');
}
});
});
const sectionFooters = this.shadowRoot.querySelectorAll('.section-footer');
sectionFooters.forEach(footer => {
const section = footer.parentElement;
footer.addEventListener('click', () => {
if (section.classList.contains('closed')) {
section.classList.remove('closed');
} else {
section.classList.add('closed');
}
this.main._network.redraw();
});
});
/* buttons */
this.$['add-node'].addEventListener('click', () => this._addNode());
this.$['add-input-node'].addEventListener('click', () => this._addNode('input'));
this.$['add-output-node'].addEventListener('click', () => this._addNode('output'));
}
_initToolbar() {
// Set max-height of sections for collapse animation
const sections = this.shadowRoot.querySelectorAll('.section-items');
sections.forEach(section => {
section.style.maxHeight = 'unset';
section.style.maxHeight = `${section.clientHeight}px`;
});
}
_addNode(type = true) {
let button = 'add-node';
if (type === 'input' || type === 'output') {
button = `add-${type}-node`;
this.main._nodeType = type;
} else {
this.main._nodeType = '';
}
this._setMode(this.$[button], 'addingNode');
}
_addComponentListener(event) {
var sectionItem = event.target.matches('.section-item') ? event.target : event.target.parentElement;
sectionItem = sectionItem.matches('.section-item') ? sectionItem : sectionItem.parentElement;
this.main._componentTemplate = event.model.item;
this._setMode(sectionItem, 'addingComponent');
}
_setMode(item, mode) {
this.clear(item);
if (item.classList.contains('active')) {
item.classList.remove('active');
this.main[mode] = false;
} else {
item.classList.add('active');
this.main[mode] = true;
}
}
/**
* call new-template-event
*/
_addTemplateListener() {
const evt = new CustomEvent('vcf-network-new-template', {
cancelable: true
});
const cancelled = !this.main.dispatchEvent(evt);
if (!cancelled) {
// JCG todo default behaviour for the client side
this.confirmAddTemplate({
id: vis.util.randomUUID(),
label: 'new template'
});
}
}
/**
* Refresh the client model
*/
confirmAddTemplate(component) {
this.components.push(component);
this.set('components', this.components.slice());
console.info('template added');
}
/**
* call delete-template-event
*/
_deleteTemplateListener(event) {
const evt = new CustomEvent('vcf-network-delete-template', {
detail: { id: event.model.item.id },
cancelable: true
});
const cancelled = !this.main.dispatchEvent(evt);
if (!cancelled) {
this.confirmDeleteTemplate(event.model.item.id);
}
}
/**
* Refresh the client model
*/
confirmDeleteTemplate(id) {
this.set('components', this.components.filter(template => template.id !== id));
}
/**
* call update-template-event
*/
_updateTemplateListener(event) {
const evt = new CustomEvent('vcf-network-update-template', {
detail: { id: event.model.item.id },
cancelable: true
});
const cancelled = !this.main.dispatchEvent(evt);
if (!cancelled) {
this.confirmUpdateTemplate(event.model.item);
}
}
confirmUpdateTemplate(component) {
const index = this.components.findIndex(item => item.id === component.id);
this.splice('components', index, 1, { ...component });
console.info('template updated component =', component);
}
hideEditTemplateButton() {
this.$['template-panel'].classList.add('edit-hidden');
}
showEditTemplateButton() {
this.$['template-panel'].classList.remove('edit-hidden');
}
hideTemplatePanel() {
this.$['template-panel'].classList.add('hidden');
}
showTemplatePanel() {
this.$['template-panel'].classList.remove('hidden');
}
closePanel() {
this.$['tool-panel'].classList.add('closed');
}
openPanel() {
this.$['tool-panel'].classList.remove('closed');
}
_addNodeToggleChanged(addNodeToggle) {
if (addNodeToggle) this.$['tool-panel'].classList.add('add-node-toggle');
else this.$['tool-panel'].classList.remove('add-node-toggle');
}
}
customElements.define(VcfNetworkToolPanel.is, VcfNetworkToolPanel);