cytoscape-angular
Version:
Angular 12+ components for cytoscape charting.
995 lines (983 loc) • 97.7 kB
JavaScript
import * as i0 from '@angular/core';
import { Component, ViewChild, Input, EventEmitter, Output, NgModule } from '@angular/core';
import * as i2 from '@angular/common';
import { CommonModule } from '@angular/common';
import * as i2$1 from 'primeng/progressspinner';
import { ProgressSpinnerModule } from 'primeng/progressspinner';
import { __classPrivateFieldGet, __classPrivateFieldSet } from 'tslib';
import * as i1$1 from 'primeng/overlaypanel';
import { OverlayPanelModule } from 'primeng/overlaypanel';
import * as i2$3 from 'primeng/api';
import * as i7 from 'primeng/dropdown';
import { DropdownModule } from 'primeng/dropdown';
import * as i1 from '@angular/forms';
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
import * as i8 from 'primeng/button';
import { ButtonModule } from 'primeng/button';
import * as i3 from 'primeng/fieldset';
import { FieldsetModule } from 'primeng/fieldset';
import * as i4 from 'primeng/inputswitch';
import { InputSwitchModule } from 'primeng/inputswitch';
import * as i5 from 'primeng/tooltip';
import { TooltipModule } from 'primeng/tooltip';
import * as i6 from 'primeng/inputtext';
import { InputTextModule } from 'primeng/inputtext';
import * as i2$2 from 'primeng/autocomplete';
import { AutoCompleteModule } from 'primeng/autocomplete';
import { ProgressBarModule } from 'primeng/progressbar';
import { SpinnerModule } from 'primeng/spinner';
const _c0$2 = ["cyGraph"];
function CytoscapeGraphComponent_p_progressSpinner_0_Template(rf, ctx) { if (rf & 1) {
i0.ɵɵelement(0, "p-progressSpinner", 3);
} }
/**
* The API is a little odd to provide flexibility.
* EITHER bind to cyOptions (type CytoscapeOptions), to control the options yourself
* OR this component will build a CytoscapeOptions internally by using all the other inputs.
* If cyOptions is supplied, all other inputs are ignored.
* The cyOptions container (HTML element) is always ignored and set internally.
*/
class CytoscapeGraphComponent {
constructor() {
this.debug = false;
this.showToolbar = true;
this.loading = false;
}
ngOnChanges(changes) {
console.log('cytoscape graph component ngOnChanges. changes:', JSON.stringify(changes));
if (changes["style"]) {
console.log('changes["style"]:', JSON.stringify(changes["style"]));
this.runWhileLoading(this.updateStyles.bind(this));
}
}
centerElements(selector) {
if (!this.cy) {
return;
}
const elems = this.cy.$(selector);
this.cy.center(elems);
}
zoomToElement(selector, level = 3) {
var _a, _b;
let position = (_b = (_a = this.cy) === null || _a === void 0 ? void 0 : _a.$(selector)) === null || _b === void 0 ? void 0 : _b.position();
if (!position) {
console.warn(`Cannot zoom to ${selector}`);
}
this.cy.zoom({
level: level,
position: position
});
}
render() {
this.runWhileLoading(this.rerender.bind(this));
}
runWhileLoading(f) {
this.loading = true;
setTimeout(() => {
f();
setTimeout(() => {
this.loading = false;
}, 30);
}, 0);
}
updateStyles() {
if (this.cy && this.style) {
this.cy.style(this.style);
}
}
rerender() {
//TODO : this takes a heavy-handed approach, refine for performance
if (!this.cyGraph) {
console.warn(`No cyGraph found`);
return;
}
const cyOptions = this.cyOptions || {
// ignored, use nodes and edges
// elements: this.elements,
autolock: this.autolock,
autoungrabify: this.autoungrabify,
autounselectify: this.autounselectify,
boxSelectionEnabled: this.boxSelectionEnabled,
container: this.cyGraph.nativeElement,
desktopTapThreshold: this.desktopTapThreshold,
hideEdgesOnViewport: this.hideEdgesOnViewport,
hideLabelsOnViewport: this.hideLabelsOnViewport,
layout: this.layoutOptions,
maxZoom: this.maxZoom,
minZoom: this.minZoom,
motionBlur: this.motionBlur,
motionBlurOpacity: this.motionBlurOpacity,
pan: this.pan,
panningEnabled: this.panningEnabled,
pixelRatio: this.pixelRatio,
selectionType: this.selectionType,
style: this.style,
styleEnabled: this.styleEnabled,
textureOnViewport: this.textureOnViewport,
touchTapThreshold: this.touchTapThreshold,
userPanningEnabled: this.userPanningEnabled,
userZoomingEnabled: this.userZoomingEnabled,
wheelSensitivity: this.wheelSensitivity,
zoomingEnabled: this.zoomingEnabled,
zoom: this.zoom,
};
// TODO do reset() instead?
this.cy = cytoscape(cyOptions);
this.cy.startBatch();
this.cy.boxSelectionEnabled(this.boxSelectionEnabled);
this.cy.nodes().remove();
this.cy.edges().remove();
if (this.nodes) {
this.cy.add(this.nodes);
}
if (this.edges) {
this.cy.add(this.edges);
}
this.cy.endBatch();
if (this.layoutOptions) {
this.cy.layout(this.layoutOptions).run();
}
}
}
CytoscapeGraphComponent.ɵfac = function CytoscapeGraphComponent_Factory(t) { return new (t || CytoscapeGraphComponent)(); };
CytoscapeGraphComponent.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: CytoscapeGraphComponent, selectors: [["cytoscape-graph"]], viewQuery: function CytoscapeGraphComponent_Query(rf, ctx) { if (rf & 1) {
i0.ɵɵviewQuery(_c0$2, 5);
} if (rf & 2) {
let _t;
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.cyGraph = _t.first);
} }, inputs: { debug: "debug", nodes: "nodes", edges: "edges", autolock: "autolock", autoungrabify: "autoungrabify", autounselectify: "autounselectify", boxSelectionEnabled: "boxSelectionEnabled", desktopTapThreshold: "desktopTapThreshold", hideEdgesOnViewport: "hideEdgesOnViewport", hideLabelsOnViewport: "hideLabelsOnViewport", layoutOptions: "layoutOptions", maxZoom: "maxZoom", minZoom: "minZoom", motionBlur: "motionBlur", motionBlurOpacity: "motionBlurOpacity", pan: "pan", panningEnabled: "panningEnabled", pixelRatio: "pixelRatio", selectionType: "selectionType", style: "style", styleEnabled: "styleEnabled", textureOnViewport: "textureOnViewport", touchTapThreshold: "touchTapThreshold", userPanningEnabled: "userPanningEnabled", userZoomingEnabled: "userZoomingEnabled", wheelSensitivity: "wheelSensitivity", zoom: "zoom", zoomingEnabled: "zoomingEnabled", showToolbar: "showToolbar" }, features: [i0.ɵɵNgOnChangesFeature], decls: 3, vars: 1, consts: [["class", "spinner", "strokeWidth", "4", "fill", "#EEEEEE", "animationDuration", ".5s", 4, "ngIf"], [1, "graphWrapper"], ["cyGraph", ""], ["strokeWidth", "4", "fill", "#EEEEEE", "animationDuration", ".5s", 1, "spinner"]], template: function CytoscapeGraphComponent_Template(rf, ctx) { if (rf & 1) {
i0.ɵɵtemplate(0, CytoscapeGraphComponent_p_progressSpinner_0_Template, 1, 0, "p-progressSpinner", 0);
i0.ɵɵelement(1, "div", 1, 2);
} if (rf & 2) {
i0.ɵɵproperty("ngIf", ctx.loading);
} }, directives: [i2.NgIf, i2$1.ProgressSpinner], styles: [".spinner[_ngcontent-%COMP%] {\n position: absolute;\n left: '350px';\n z-index: 10;\n width: '250px';\n height: '250px';\n }\n @keyframes ui-progress-spinner-color {\n 100%,\n 0% {\n stroke: #d62d20;\n }\n 40% {\n stroke: #0057e7;\n }\n 66% {\n stroke: #008744;\n }\n 80%,\n 90% {\n stroke: #ffa700;\n }\n }\n .graphWrapper[_ngcontent-%COMP%] {\n height: 100%;\n width: 100%;\n }"] });
(function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(CytoscapeGraphComponent, [{
type: Component,
args: [{
selector: 'cytoscape-graph',
template: `
<p-progressSpinner *ngIf="loading" class="spinner" strokeWidth="4" fill="#EEEEEE" animationDuration=".5s"></p-progressSpinner>
<div #cyGraph class="graphWrapper">
</div>
`,
styles: [`
.spinner {
position: absolute;
left: '350px';
z-index: 10;
width: '250px';
height: '250px';
}
@keyframes ui-progress-spinner-color {
100%,
0% {
stroke: #d62d20;
}
40% {
stroke: #0057e7;
}
66% {
stroke: #008744;
}
80%,
90% {
stroke: #ffa700;
}
}
.graphWrapper {
height: 100%;
width: 100%;
}`
]
}]
}], function () { return []; }, { cyGraph: [{
type: ViewChild,
args: ['cyGraph']
}], debug: [{
type: Input
}], nodes: [{
type: Input
}], edges: [{
type: Input
}], autolock: [{
type: Input
}], autoungrabify: [{
type: Input
}], autounselectify: [{
type: Input
}], boxSelectionEnabled: [{
type: Input
}], desktopTapThreshold: [{
type: Input
}], hideEdgesOnViewport: [{
type: Input
}], hideLabelsOnViewport: [{
type: Input
}], layoutOptions: [{
type: Input
}], maxZoom: [{
type: Input
}], minZoom: [{
type: Input
}], motionBlur: [{
type: Input
}], motionBlurOpacity: [{
type: Input
}], pan: [{
type: Input
}], panningEnabled: [{
type: Input
}], pixelRatio: [{
type: Input
}], selectionType: [{
type: Input
}], style: [{
type: Input
}], styleEnabled: [{
type: Input
}], textureOnViewport: [{
type: Input
}], touchTapThreshold: [{
type: Input
}], userPanningEnabled: [{
type: Input
}], userZoomingEnabled: [{
type: Input
}], wheelSensitivity: [{
type: Input
}], zoom: [{
type: Input
}], zoomingEnabled: [{
type: Input
}], showToolbar: [{
type: Input
}] }); })();
class BaseLayoutOptionsImpl {
ready(e) {
// tslint:disable-next-line:no-console
console.debug(`layout ready, cytoscape.LayoutEventObject: ${JSON.stringify(e)}`); // on layoutready
}
stop(e) {
// tslint:disable-next-line:no-console
console.debug(`layout stop, cytoscape.LayoutEventObject: ${JSON.stringify(e)}`); // on layoutstop
}
}
class NullLayoutOptionsImpl extends BaseLayoutOptionsImpl {
constructor() {
super(...arguments);
this.name = 'null';
}
}
class AnimateLayoutOptionsImpl extends BaseLayoutOptionsImpl {
constructor() {
super(...arguments);
// the zoom level to set (prob want fit = false if set)
this.zoom = null;
// the pan level to set (prob want fit = false if set)
this.pan = null;
// whether to transition the node positions
this.animate = false;
// duration of animation in ms if enabled
this.animationDuration = 500;
// easing of animation if enabled
this.animationEasing = undefined;
// a function that determines whether the node should be animated.
// All nodes animated by default on animate enabled. Non-animated nodes are
// positioned immediately when the layout starts
this.animateFilter = (node, i) => true;
}
}
class PresetLayoutOptionsImpl extends AnimateLayoutOptionsImpl {
constructor() {
super(...arguments);
this.name = 'preset';
// transform a given node position. Useful for changing flow direction in discrete layouts
this.transform = (node, position) => position;
}
}
class ShapedLayoutOptionsImpl extends AnimateLayoutOptionsImpl {
constructor() {
super(...arguments);
// whether to fit to viewport
this.fit = true;
// fit padding
this.padding = 30;
// constrain layout bounds
this.boundingBox = null;
// prevents node overlap, may overflow boundingBox if not enough space
this.avoidOverlap = true;
// Excludes the label when calculating node bounding boxes for the layout algorithm
this.nodeDimensionsIncludeLabels = false;
// Applies a multiplicative factor (>0) to expand or compress the overall area that the nodes take up
this.spacingFactor = 1.75;
// a sorting function to order the nodes; e.g. function(a, b){ return a.data('weight') - b.data('weight') }
this.sort = null;
// transform a given node position. Useful for changing flow direction in discrete layouts
this.transform = (node, position) => position;
}
}
class GridLayoutOptionsImpl extends ShapedLayoutOptionsImpl {
constructor() {
super(...arguments);
this.name = 'grid';
// extra spacing around nodes when avoidOverlap: true
this.avoidOverlapPadding = 10;
// uses all available space on false, uses minimal space on true
this.condense = false;
// force num of rows in the grid
this.rows = null;
// force num of columns in the grid
this.cols = null;
// returns { row, col } for element
// (node: NodeSingular) => return { row: number; col: number; }
this.position = null;
}
}
class RandomLayoutOptionsImpl extends AnimateLayoutOptionsImpl {
constructor() {
super(...arguments);
this.name = 'random';
this.fit = true;
this.padding = 20;
// constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
this.boundingBox = null;
// transform a given node position. Useful for changing flow direction in discrete layouts
this.transform = (node, position) => position;
}
}
class CircleLayoutOptionsImpl extends ShapedLayoutOptionsImpl {
constructor() {
super(...arguments);
this.name = 'circle';
this.startAngle = 3 / 2 * Math.PI; // where nodes start in radians
this.sweep = null; // how many radians should be between the first and last node (defaults to full circle)
}
}
// Note: "radius" is not part of concentric, imperfect extension
class ConcentricLayoutOptionsImpl {
constructor() {
this.name = 'concentric';
// where nodes start in radians, e.g. 3 / 2 * Math.PI,
this.startAngle = 3 / 2 * Math.PI;
// height of layout area (overrides container height)
this.height = null;
// width of layout area (overrides container width)
this.width = null;
}
concentric(node) {
return 0;
}
levelWidth(node) {
return 0;
}
}
class BreadthFirstLayoutOptionsImpl extends ShapedLayoutOptionsImpl {
constructor() {
super(...arguments);
this.name = 'breadthfirst';
// whether the tree is directed downwards (or edges can point in any direction if false)
this.directed = false;
// put depths in concentric circles if true, put depths top down if false
this.circle = false;
// whether to shift nodes down their natural BFS depths in order to avoid upwards edges (DAGS only)
this.maximal = false;
this.grid = false; // whether to create an even grid into which the DAG is placed (circle:false only)
}
}
class CoseLayoutOptionsImpl extends ShapedLayoutOptionsImpl {
constructor() {
super(...arguments);
this.name = 'cose';
// Number of iterations between consecutive screen positions update
this.refresh = 20;
// Randomize the initial positions of the nodes (true) or use existing positions (false)
this.randomize = false;
// Extra spacing between components in non-compound graphs
this.componentSpacing = 40;
// Node repulsion (overlapping) multiplier
this.nodeOverlap = 4;
// Nesting factor (multiplier) to compute ideal edge length for nested edges
this.nestingFactor = 1.2;
// Gravity force (constant)
this.gravity = 1;
// Maximum number of iterations to perform
this.numIter = 1000;
// Initial temperature (maximum node displacement)
this.initialTemp = 1000;
// Cooling factor (how the temperature is reduced between consecutive iterations
this.coolingFactor = 0.99;
// Lower temperature threshold (below this point the layout will end)
this.minTemp = 1.0;
// Node repulsion (non overlapping) multiplier
this.nodeRepulsion = (node) => 2048;
// Ideal edge (non nested) length
this.idealEdgeLength = (edge) => 32;
// Divisor to compute edge forces
this.edgeElasticity = (edge) => 32;
}
}
class DagreLayoutOptionsImpl extends ShapedLayoutOptionsImpl {
constructor() {
super();
this.name = 'dagre';
this.nodeSep = null; // the separation between adjacent nodes in the same rank
this.edgeSep = null; // the separation between adjacent edges in the same rank
this.rankSep = null; // the separation between each rank in the layout
// TB for top to bottom flow, 'LR' for left to right
this.rankDir = 'TB';
// Type of algorithm to assign a rank to each node in the input graph.
// Possible values: 'network-simplex', 'tight-tree' or 'longest-path'
this.ranker = null;
// number of ranks to keep between the source and target of the edge
this.minLen = (edge) => 1;
this.edgeWeight = (edge) => 1; // higher weight edges are generally made shorter and straighter than lower weight edges
}
}
class FormInfo {
constructor(title, fieldsets, showSubmitButton = false, submitText = 'Submit', disableSubmitOnFormInvalid = false,
/* if the model has a property that isn't in a fieldset, but it in an fieldset created by the form */
otherFieldsetTitle = null) {
this.title = title;
this.fieldsets = fieldsets;
this.showSubmitButton = showSubmitButton;
this.submitText = submitText;
this.disableSubmitOnFormInvalid = disableSubmitOnFormInvalid;
this.otherFieldsetTitle = otherFieldsetTitle;
}
}
class FieldsetInfo {
constructor(legend, fieldInfos, displayOnlyIfProperties) {
this.legend = legend;
this.fieldInfos = fieldInfos;
this.displayOnlyIfProperties = displayOnlyIfProperties;
}
showFieldsetForModel(model) {
if (!this.displayOnlyIfProperties) {
return true;
}
for (const fieldInfo of this.fieldInfos) {
for (const modelProperty of Object.keys(model)) {
if (fieldInfo.modelProperty === modelProperty) {
return true;
}
}
}
return false;
}
}
class FieldInfo {
constructor(/* label to show the user next to the field, can be a function for i18n/dynamic labels */ label,
/* The form has a model, this is the name of the property on the form's model object that this field */
modelProperty,
/* computed from the model property type by default */
type,
/* The tooltip to display on hover */
tooltip,
/* The list of Angular Form Validators for the control or a function that returns such an array */
validators,
/* disable the field if it's not valid */
disableWhenInvalid = false,
/* If true and model[modelProperty] is undefined, don't create a field.*/
hideWhenNoModelProperty = true,
/* Input only - by default the label is used as a placeholder and floats (how to downcast in a template?) */
placeholder,
/* Input only - same as HTML input (how to downcast in a template?) */
inputType = 'text',
/* Input only - same as HTML input (how to downcast in a template?) */
inputSize = 8,
/* Select only either an array of object or the name of a model property or function that is/returns an array of objects */
options,
/* In an options object, what field to display to the user (or function that returns a string
given the option object and the model) */
optionArrayLabelField,
/* In an options object, what field to return for the value of the option
(or function that returns a string given the option object and the model) */
optionArrayValueField) {
this.label = label;
this.modelProperty = modelProperty;
this.type = type;
this.tooltip = tooltip;
this.validators = validators;
this.disableWhenInvalid = disableWhenInvalid;
this.hideWhenNoModelProperty = hideWhenNoModelProperty;
this.placeholder = placeholder;
this.inputType = inputType;
this.inputSize = inputSize;
this.options = options;
this.optionArrayLabelField = optionArrayLabelField;
this.optionArrayValueField = optionArrayValueField;
this.fieldTypes = {};
}
fieldType(model) {
const cached = this.fieldTypes[this.modelProperty];
if (cached) {
return cached;
}
else {
const fieldValueType = typeof model[this.modelProperty];
const result = this.type ? this.type : (this.options ? 'options' : fieldValueType);
this.fieldTypes[this.modelProperty] = result;
return result;
}
}
setValue(newValue, model, modelChange) {
model[this.modelProperty] = newValue;
modelChange.emit({ property: this.modelProperty, value: newValue });
}
}
function FluidFormComponent_ng_container_1_p_fieldset_1_div_2_ng_container_2_Template(rf, ctx) { if (rf & 1) {
i0.ɵɵelementContainerStart(0);
i0.ɵɵelementStart(1, "span", 10);
i0.ɵɵtext(2);
i0.ɵɵelementEnd();
i0.ɵɵelement(3, "p-inputSwitch", 11);
i0.ɵɵelementContainerEnd();
} if (rf & 2) {
const fieldInfo_r5 = i0.ɵɵnextContext().$implicit;
i0.ɵɵadvance(2);
i0.ɵɵtextInterpolate1(" ", fieldInfo_r5.label, " ");
i0.ɵɵadvance(1);
i0.ɵɵpropertyInterpolate("name", fieldInfo_r5.modelProperty);
i0.ɵɵpropertyInterpolate("pTooltip", fieldInfo_r5.tooltip);
i0.ɵɵpropertyInterpolate("formControlName", fieldInfo_r5.modelProperty);
} }
function FluidFormComponent_ng_container_1_p_fieldset_1_div_2_ng_container_3_Template(rf, ctx) { if (rf & 1) {
i0.ɵɵelementContainerStart(0);
i0.ɵɵelementStart(1, "span", 12);
i0.ɵɵelement(2, "input", 13);
i0.ɵɵelementStart(3, "label", 14);
i0.ɵɵtext(4);
i0.ɵɵelementEnd();
i0.ɵɵelementEnd();
i0.ɵɵelementContainerEnd();
} if (rf & 2) {
const fieldInfo_r5 = i0.ɵɵnextContext().$implicit;
i0.ɵɵadvance(2);
i0.ɵɵpropertyInterpolate("id", fieldInfo_r5.modelProperty);
i0.ɵɵpropertyInterpolate("name", fieldInfo_r5.modelProperty);
i0.ɵɵpropertyInterpolate("formControlName", fieldInfo_r5.modelProperty);
i0.ɵɵproperty("pTooltip", fieldInfo_r5.tooltip)("type", fieldInfo_r5.inputType)("size", fieldInfo_r5.inputSize);
i0.ɵɵadvance(1);
i0.ɵɵpropertyInterpolate("for", fieldInfo_r5.modelProperty);
i0.ɵɵadvance(1);
i0.ɵɵtextInterpolate(fieldInfo_r5.label);
} }
function FluidFormComponent_ng_container_1_p_fieldset_1_div_2_ng_container_4_Template(rf, ctx) { if (rf & 1) {
i0.ɵɵelementContainerStart(0);
i0.ɵɵelementStart(1, "span", 12);
i0.ɵɵelement(2, "p-dropdown", 15);
i0.ɵɵelementStart(3, "label", 14);
i0.ɵɵtext(4);
i0.ɵɵelementEnd();
i0.ɵɵelementEnd();
i0.ɵɵelementContainerEnd();
} if (rf & 2) {
const fieldInfo_r5 = i0.ɵɵnextContext().$implicit;
i0.ɵɵadvance(2);
i0.ɵɵpropertyInterpolate("formControlName", fieldInfo_r5.modelProperty);
i0.ɵɵproperty("name", fieldInfo_r5.modelProperty)("options", fieldInfo_r5.options)("optionLabel", fieldInfo_r5.optionArrayLabelField)("pTooltip", fieldInfo_r5.tooltip);
i0.ɵɵadvance(1);
i0.ɵɵpropertyInterpolate("for", fieldInfo_r5.modelProperty);
i0.ɵɵadvance(1);
i0.ɵɵtextInterpolate(fieldInfo_r5.label);
} }
function FluidFormComponent_ng_container_1_p_fieldset_1_div_2_Template(rf, ctx) { if (rf & 1) {
i0.ɵɵelementStart(0, "div", 7);
i0.ɵɵelementStart(1, "div", 8);
i0.ɵɵtemplate(2, FluidFormComponent_ng_container_1_p_fieldset_1_div_2_ng_container_2_Template, 4, 4, "ng-container", 9);
i0.ɵɵtemplate(3, FluidFormComponent_ng_container_1_p_fieldset_1_div_2_ng_container_3_Template, 5, 8, "ng-container", 9);
i0.ɵɵtemplate(4, FluidFormComponent_ng_container_1_p_fieldset_1_div_2_ng_container_4_Template, 5, 7, "ng-container", 9);
i0.ɵɵelementEnd();
i0.ɵɵelementEnd();
} if (rf & 2) {
const fieldInfo_r5 = ctx.$implicit;
const ctx_r4 = i0.ɵɵnextContext(3);
i0.ɵɵadvance(2);
i0.ɵɵproperty("ngIf", fieldInfo_r5.fieldType(ctx_r4.model) === "boolean");
i0.ɵɵadvance(1);
i0.ɵɵproperty("ngIf", fieldInfo_r5.fieldType(ctx_r4.model) === "string" || fieldInfo_r5.fieldType(ctx_r4.model) === "number");
i0.ɵɵadvance(1);
i0.ɵɵproperty("ngIf", fieldInfo_r5.fieldType(ctx_r4.model) === "options");
} }
function FluidFormComponent_ng_container_1_p_fieldset_1_Template(rf, ctx) { if (rf & 1) {
i0.ɵɵelementStart(0, "p-fieldset", 4);
i0.ɵɵelementStart(1, "div", 5);
i0.ɵɵtemplate(2, FluidFormComponent_ng_container_1_p_fieldset_1_div_2_Template, 5, 3, "div", 6);
i0.ɵɵelementEnd();
i0.ɵɵelementEnd();
} if (rf & 2) {
const fieldSetInfo_r2 = i0.ɵɵnextContext().$implicit;
i0.ɵɵpropertyInterpolate("legend", fieldSetInfo_r2.legend);
i0.ɵɵadvance(2);
i0.ɵɵproperty("ngForOf", fieldSetInfo_r2.fieldInfos);
} }
function FluidFormComponent_ng_container_1_Template(rf, ctx) { if (rf & 1) {
i0.ɵɵelementContainerStart(0);
i0.ɵɵtemplate(1, FluidFormComponent_ng_container_1_p_fieldset_1_Template, 3, 2, "p-fieldset", 3);
i0.ɵɵelementContainerEnd();
} if (rf & 2) {
const fieldSetInfo_r2 = ctx.$implicit;
const ctx_r0 = i0.ɵɵnextContext();
i0.ɵɵadvance(1);
i0.ɵɵproperty("ngIf", fieldSetInfo_r2.showFieldsetForModel(ctx_r0.model));
} }
function FluidFormComponent_button_2_Template(rf, ctx) { if (rf & 1) {
const _r14 = i0.ɵɵgetCurrentView();
i0.ɵɵelementStart(0, "button", 16);
i0.ɵɵlistener("submit", function FluidFormComponent_button_2_Template_button_submit_0_listener() { i0.ɵɵrestoreView(_r14); const ctx_r13 = i0.ɵɵnextContext(); return ctx_r13.onSubmit(); });
i0.ɵɵtext(1);
i0.ɵɵelementEnd();
} if (rf & 2) {
const ctx_r1 = i0.ɵɵnextContext();
i0.ɵɵproperty("disabled", ctx_r1.formInfo.disableSubmitOnFormInvalid && !ctx_r1.formGroup.valid);
i0.ɵɵadvance(1);
i0.ɵɵtextInterpolate(ctx_r1.formInfo.submitText || "Submit");
} }
class FluidFormComponent {
constructor() {
this.modelChange = new EventEmitter();
}
ngOnInit() {
console.debug('FluidFormComponent this.formInfo:', JSON.stringify(this.formInfo));
let controls = {};
this.formInfo.fieldsets.forEach(fieldsetInfo => {
fieldsetInfo.fieldInfos.forEach(fieldInfo => {
let modelValue = this.model[fieldInfo.modelProperty];
// console.log('fieldInfo.modelProperty:', fieldInfo.modelProperty, ', modelValue:', modelValue)
const validators = typeof fieldInfo.validators === 'function' ? fieldInfo.validators() : fieldInfo.validators;
const asyncValidators = typeof fieldInfo.asyncValidators === 'function' ? fieldInfo.asyncValidators() : fieldInfo.asyncValidators;
const { updateOn } = fieldInfo;
let formControl = new FormControl(modelValue, { validators, asyncValidators, updateOn });
formControl.valueChanges.subscribe((change) => {
console.debug('form control change ', JSON.stringify(change), ' for prop ', fieldInfo.modelProperty, ', changing current model value ', this.model[fieldInfo.modelProperty], ' to ', change);
fieldInfo.setValue(change, this.model, this.modelChange);
});
controls[fieldInfo.modelProperty] = formControl;
});
});
this.formGroup = new FormGroup(controls);
}
ngOnChanges(changes) {
var _a;
console.debug('ngOnChanges fluid-form changes:', JSON.stringify(changes));
if (changes['model']) {
const model = changes['model'].currentValue;
for (let key of Object.keys(model)) {
console.debug('ngOnChanges model key copying to form:', key);
const control = (_a = this.formGroup) === null || _a === void 0 ? void 0 : _a.controls[key];
control ? control.setValue(model[key], { emitEvent: false }) : console.warn('no control for model key ', key);
}
}
}
ngAfterViewInit() {
// console.debug("ngAfterViewInit")
}
ngAfterViewChecked() {
// console.debug("ngAfterViewChecked")
}
onSubmit() {
console.log(`Form submitted`);
}
}
FluidFormComponent.ɵfac = function FluidFormComponent_Factory(t) { return new (t || FluidFormComponent)(); };
FluidFormComponent.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: FluidFormComponent, selectors: [["cyng-fluid-form"]], inputs: { model: "model", modelProperty: "modelProperty", formInfo: "formInfo" }, outputs: { modelChange: "modelChange" }, features: [i0.ɵɵNgOnChangesFeature], decls: 3, vars: 4, consts: [[3, "formGroup", "title", "ngSubmit"], [4, "ngFor", "ngForOf"], ["pButton", "", 3, "disabled", "submit", 4, "ngIf"], ["class", "fieldset", 3, "legend", 4, "ngIf"], [1, "fieldset", 3, "legend"], [1, "ui-g", "ui-fluid"], ["class", "ui-g-12 ui-md-4 field", 4, "ngFor", "ngForOf"], [1, "ui-g-12", "ui-md-4", "field"], [1, "ui-inputgroup"], [4, "ngIf"], [1, "ui-chkbox-label"], [3, "name", "pTooltip", "formControlName"], [1, "ui-float-label"], ["pInputText", "", 3, "id", "name", "formControlName", "pTooltip", "type", "size"], [3, "for"], [3, "formControlName", "name", "options", "optionLabel", "pTooltip"], ["pButton", "", 3, "disabled", "submit"]], template: function FluidFormComponent_Template(rf, ctx) { if (rf & 1) {
i0.ɵɵelementStart(0, "form", 0);
i0.ɵɵlistener("ngSubmit", function FluidFormComponent_Template_form_ngSubmit_0_listener() { return ctx.onSubmit(); });
i0.ɵɵtemplate(1, FluidFormComponent_ng_container_1_Template, 2, 1, "ng-container", 1);
i0.ɵɵelementEnd();
i0.ɵɵtemplate(2, FluidFormComponent_button_2_Template, 2, 2, "button", 2);
} if (rf & 2) {
i0.ɵɵproperty("formGroup", ctx.formGroup)("title", ctx.formInfo == null ? null : ctx.formInfo.title);
i0.ɵɵadvance(1);
i0.ɵɵproperty("ngForOf", ctx.formInfo == null ? null : ctx.formInfo.fieldsets);
i0.ɵɵadvance(1);
i0.ɵɵproperty("ngIf", ctx.formInfo.showSubmitButton);
} }, directives: [i1.ɵNgNoValidate, i1.NgControlStatusGroup, i1.FormGroupDirective, i2.NgForOf, i2.NgIf, i3.Fieldset, i4.InputSwitch, i5.Tooltip, i1.NgControlStatus, i1.FormControlName, i1.DefaultValueAccessor, i6.InputText, i7.Dropdown, i8.ButtonDirective], styles: [".ui-chkbox-label[_ngcontent-%COMP%] {\n padding-right: 0.5em;\n }\n\n .ui-dropdown-label[_ngcontent-%COMP%] {\n align-self: center;\n padding-right: 0.5em\n }\n\n .field[_ngcontent-%COMP%]:nth-child(n+4) {\n margin-top: 1em; // otherwise overlap betwen a field and a floating label of the field below it\n }\n\n .fieldset[_ngcontent-%COMP%] {\n }"] });
(function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(FluidFormComponent, [{
type: Component,
args: [{
selector: 'cyng-fluid-form',
template: `
<form [formGroup]="formGroup" [title]="formInfo?.title" (ngSubmit)="onSubmit()">
<ng-container *ngFor="let fieldSetInfo of formInfo?.fieldsets">
<p-fieldset *ngIf="fieldSetInfo.showFieldsetForModel(model)" class="fieldset" legend="{{fieldSetInfo.legend}}">
<div class="ui-g ui-fluid">
<div class="ui-g-12 ui-md-4 field" *ngFor="let fieldInfo of fieldSetInfo.fieldInfos">
<div class="ui-inputgroup">
<ng-container *ngIf="fieldInfo.fieldType(model) === 'boolean'">
<span class="ui-chkbox-label">
{{fieldInfo.label}}
</span>
<p-inputSwitch
name="{{fieldInfo.modelProperty}}"
pTooltip="{{fieldInfo.tooltip}}"
formControlName="{{fieldInfo.modelProperty}}"
>
</p-inputSwitch>
</ng-container>
<ng-container
*ngIf="fieldInfo.fieldType(model) === 'string' || fieldInfo.fieldType(model) === 'number'">
<span class="ui-float-label">
<input pInputText
id="{{fieldInfo.modelProperty}}"
name="{{fieldInfo.modelProperty}}"
formControlName="{{fieldInfo.modelProperty}}"
[pTooltip]="fieldInfo.tooltip"
[type]="fieldInfo.inputType"
[size]="fieldInfo.inputSize"
/>
<label for="{{fieldInfo.modelProperty}}">{{fieldInfo.label}}</label>
</span>
</ng-container>
<ng-container *ngIf="fieldInfo.fieldType(model) === 'options'">
<span class="ui-float-label">
<p-dropdown
formControlName="{{fieldInfo.modelProperty}}"
[name]="fieldInfo.modelProperty"
[options]="fieldInfo.options"
[optionLabel]="fieldInfo.optionArrayLabelField"
[pTooltip]="fieldInfo.tooltip"
></p-dropdown>
<label for="{{fieldInfo.modelProperty}}">{{fieldInfo.label}}</label>
</span>
</ng-container>
</div>
</div>
</div>
</p-fieldset>
</ng-container>
</form>
<button *ngIf="formInfo.showSubmitButton" pButton
[disabled]="formInfo.disableSubmitOnFormInvalid && !formGroup.valid"
(submit)="onSubmit()">{{formInfo.submitText || 'Submit' }}</button>
`,
styles: [`
.ui-chkbox-label {
padding-right: 0.5em;
}
.ui-dropdown-label {
align-self: center;
padding-right: 0.5em
}
.field:nth-child(n+4) {
margin-top: 1em; // otherwise overlap betwen a field and a floating label of the field below it
}
.fieldset {
}
`]
}]
}], function () { return []; }, { model: [{
type: Input
}], modelChange: [{
type: Output
}], modelProperty: [{
type: Input
}], formInfo: [{
type: Input
}] }); })();
const _c0$1 = ["layoutForm"];
class CytoscapeLayoutToolComponent {
constructor() {
this.changed = false;
this.layoutOptionsChange = new EventEmitter();
this.layoutOptionsList = [
new BreadthFirstLayoutOptionsImpl(),
new CoseLayoutOptionsImpl(),
new DagreLayoutOptionsImpl(),
new CircleLayoutOptionsImpl(),
new ConcentricLayoutOptionsImpl(),
new GridLayoutOptionsImpl(),
new PresetLayoutOptionsImpl(),
new RandomLayoutOptionsImpl(),
new NullLayoutOptionsImpl(),
];
}
get layoutOptions() {
return this._layoutOptions;
}
set layoutOptions(value) {
console.log(`set layoutOptions: ${value === null || value === void 0 ? void 0 : value.name}`);
this._layoutOptions = value;
}
ngOnInit() {
this.formInfo = CytoscapeLayoutToolComponent.createLayoutFormInfo();
let layoutOptionsSelect = this.layoutOptionsList[5];
console.log('setting the initial selected layout, default: ', layoutOptionsSelect.name);
if (this.layoutOptions) {
console.log(`setting the initial selected layout based on input/output layout ${JSON.stringify(this.layoutOptions)}`);
this.addOrReplaceInLayoutOptionsList(this.layoutOptions);
}
console.log('Initializing this.selectedLayoutInfo with layoutOptionsSelect ', JSON.stringify(layoutOptionsSelect));
}
ngOnChanges(changes) {
console.log('ngOnChanges layout changes:', JSON.stringify(changes));
if (changes['layoutOptions']) {
}
}
onLayoutModelChange() {
console.log('Layout model change: ', JSON.stringify(this.layoutOptions));
this.changed = true;
}
onFormModelChange() {
console.log('onFormModelChange');
this.changed = true;
}
onApplyLayout() {
this.changed = false;
this.layoutOptionsChange.emit(this.layoutOptions);
}
addOrReplaceInLayoutOptionsList(layoutOptions) {
let matchingOptions = this.layoutOptionsList.find(selectOption => selectOption.name === layoutOptions.name);
if (matchingOptions) {
console.log('got matching layoutOptions: ', JSON.stringify(matchingOptions));
this.layoutOptionsList.splice(this.layoutOptionsList.indexOf(matchingOptions), 1, layoutOptions);
}
else {
console.info(`Did you pass a new kind of layout? The layout name ${name} was not found, adding a new one to the top of the list.`);
this.layoutOptionsList.unshift(layoutOptions);
}
}
static createLayoutFormInfo() {
let fit = new FieldInfo('Fit', 'fit', 'boolean', 'Whether to fit to viewport');
let padding = new FieldInfo('Padding', 'padding', 'number', 'When fit to viewport, padding inside the viewport.');
let fitFieldset = new FieldsetInfo('Fit', [
fit, padding
], ['fit']);
const zoom = new FieldInfo('Zoom', 'zoom', 'number', 'the zoom level to set (likely want fit = false if set)');
const pan = new FieldInfo('Pan', 'pan', 'number', 'the pan level to set (likely want fit = false if set)');
const animate = new FieldInfo('Animate', "animate", 'boolean', "whether to transition the node positions");
const animationDuration = new FieldInfo("Animation Duration", 'animationDuration', 'number', "duration of animation in ms if enabled");
const animationEasing = new FieldInfo("Animation Easing", 'animationEasing', 'number', "easing of animation if enabled");
let animationFieldset = new FieldsetInfo('Animation', [
zoom, pan, animate, animationDuration, animationEasing
], ['animate']);
let avoidOverlap = new FieldInfo('Avoid Overlap', 'avoidOverlap', 'boolean', 'prevents node overlap, may overflow boundingBox if not enough space');
let spacingFactor = new FieldInfo('Spacing Factor', 'spacingFactor', 'number', 'Applies a multiplicative factor (>0) to expand or compress the overall area that the nodes take up');
let nodeDimensionsIncludeLabels = new FieldInfo('Node Dimensions Include Labels', 'nodeDimensionsIncludeLabels', 'boolean', 'Excludes the label when calculating node bounding boxes for the layout algorithm');
let shapedFieldset = new FieldsetInfo('Shaped', [
avoidOverlap, spacingFactor, nodeDimensionsIncludeLabels
], ['avoidOverlap']);
let directed = new FieldInfo('Directed', 'breadthFirst', 'boolean', 'whether the tree is breadthFirst downwards (or edges can point in any direction if false)');
let circle = new FieldInfo('Circle', 'circle', 'boolean', 'put depths in concentric circles if true, put depths top down if false');
let maximalAdjustments = new FieldInfo('Maximal Adjustments', 'maximalAdjustments', 'number', 'how many times to try to position the nodes in a maximal way (i.e. no backtracking)');
let maximal = new FieldInfo('Maximal', 'maximal', 'boolean', 'whether to shift nodes down their natural BFS depths in order to avoid upwards edges (DAGS only)');
let grid = new FieldInfo('Grid', 'grid', 'boolean', 'whether to shift nodes down their natural BFS depths in order to avoid upwards edges (DAGS only)');
let roots = new FieldInfo('Roots', 'roots', 'string', 'the roots of the trees');
let breadthFirstFieldset = new FieldsetInfo('Breadth First', [
directed, circle, maximalAdjustments, maximal, grid, roots
], ['breadthFirst']);
let nodeSep = new FieldInfo('Node Separation', 'nodeSep', 'number', 'the separation between adjacent nodes in the same rank');
let edgeSep = new FieldInfo('Edge Separation', 'edgeSep', 'number', 'the separation between adjacent edges in the same rank');
let rankSep = new FieldInfo('Rank Separation', 'rankSep', 'number', 'the separation between each rank in the layout');
let ranker = new FieldInfo('Ranker', 'ranker', 'options', 'Type of algorithm to assign a rank to each node in the input graph.');
ranker.options = [
{ name: '', label: '' },
{ name: 'network-simplex', label: 'network-simplex' },
{ name: 'tight-tree', label: 'tight-tree' },
{ name: 'longest-path', label: 'longest-path' }
];
let dagreFieldset = new FieldsetInfo('Dagre', [
nodeSep, edgeSep, rankSep, ranker
], ['nodeSep']);
let animationThreshold = new FieldInfo('Animation Threshold', 'animationThreshold', 'number', 'The layout animates only after this many milliseconds when animate is true (prevents flashing on fast runs)');
let refresh = new FieldInfo('Refresh', 'refresh', 'number', 'Number of iterations between consecutive screen positions update');
let randomize = new FieldInfo('Randomize', 'randomize', 'boolean', 'Randomize the initial positions of the nodes (true) or use existing positions (false)');
let componentSpacing = new FieldInfo('Component Spacing', 'componentSpacing', 'number', 'Extra spacing between components in non-compound graphs');
let nodeOverlap = new FieldInfo('Node Overlap', 'nodeOverlap', 'number', 'Node repulsion (overlapping) multiplier');
let nestingFactor = new FieldInfo('Nesting Factor', 'nestingFactor', 'number', 'Nesting factor (multiplier) to compute ideal edge length for nested edges');
let gravity = new FieldInfo('Gravity', 'gravity', 'number', 'Gravity force (constant)');
let numIter = new FieldInfo('Max Iterations', 'numIter', 'number', 'Maximum number of iterations to perform');
let initialTemp = new FieldInfo('Initial Temp', 'initialTemp', 'number', 'Initial temperature (maximum node displacement)');
let coolingFactor = new FieldInfo('Cooling Factor', 'coolingFactor', 'number', 'Cooling factor (how the temperature is reduced between consecutive iterations');
let minTemp = new FieldInfo('Min. Temp', 'minTemp', 'number', 'Lower temperature threshold (below this point the layout will end)');
let coseFieldset = new FieldsetInfo('COSE', [
animationThreshold, refresh, randomize, componentSpacing, nodeOverlap, nestingFactor, gravity, numIter,
initialTemp, coolingFactor, minTemp
], ['coolingFactor']);
let avoidOverlapPadding = new FieldInfo('avoidOverlapPadding', 'avoidOverlapPadding', 'number', 'extra spacing around nodes when avoidOverlap: true');
let condense = new FieldInfo('condense', 'condense', 'boolean', 'uses all available space on false, uses minimal space on true');
let rows = new FieldInfo('Rows', 'rows', 'number', 'force num of rows in the grid');
let cols = new FieldInfo('Columns', 'cols', 'number', 'force num of columns in the grid');
let gridFieldset = new FieldsetInfo('Grid', [
avoidOverlapPadding, condense, rows, cols
], ['cols']);
let radius = new FieldInfo('Radius', 'radius', 'number', 'the radius of the circle');
let startAngle = new FieldInfo('Start Angle', 'startAngle', 'number', 'where nodes start in radians (default:3 / 2 * Math.PI)');
let sweep = new FieldInfo('Sweep', 'sweep', 'number', 'how many radians should be between the first and last node (defaults to full circle)');
let clockwise = new FieldInfo('Clockwise', 'clockwise', 'number', 'whether the layout should go clockwise (true) or counterclockwise/anticlockwise (false)');
let circularFieldSet = new FieldsetInfo('Circular', [
radius, startAngle, sweep, clockwise
], ['clockwise']);
let equidistant = new FieldInfo('Equidistant', 'equidistant', 'boolean', 'whether levels have an equal radial distance betwen them, may cause bounding box overflow');
let minNodeSpacing = new FieldInfo('Min. Node Spacing', 'minNodeSpacing', 'number', 'min spacing between outside of nodes (used for radius adjustment)');
let height = new FieldInfo('Height', 'height', 'number', '');
let width = new FieldInfo('Width', 'width', 'number', '');
let concentricFieldSet = new FieldsetInfo('Concentric', [
equidistant, minNodeSpacing, startAngle, height, width
], ['equidistant']);
//boundingBox: undefined // constrain layout bounds; { x1, y1, x2, y2 } or { x1, y1, w, h }
return new FormInfo('Layout', [
breadthFirstFieldset, coseFieldset, dagreFieldset, gridFieldset, circularFieldSet, concentricFieldSet,
fitFieldset, animationFieldset, shapedFieldset
], false);
}
}
CytoscapeLayoutToolComponent.LAYOUT_FORM_INFO = CytoscapeLayoutToolComponent.createLayoutFormInfo();
CytoscapeLayoutToolComponent.ɵfac = function CytoscapeLayoutToolComponent_Factory(t) { return new (t || CytoscapeLayoutToolComponent)(); };
CytoscapeLayoutToolComponent.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: CytoscapeLayoutToolComponent, selectors: [["cytoscape-layout-tool"]], viewQuery: function CytoscapeLayoutToolComponent_Query(rf, ctx) { if (rf & 1) {
i0.ɵɵviewQuery(_c0$1, 5);
} if (rf & 2) {
let _t;
i0.ɵɵqueryRefresh(_t = i0.ɵɵloadQuery()) && (ctx.layoutForm = _t.first);
} }, inputs: { layoutOptions: "layoutOptions" }, outputs: { layoutOptionsChange: "layoutOptionsChange" }, features: [i0.ɵɵNgOnChangesFeature], decls: 8, vars: 5, consts: [[2, "display", "flex"], [1, "layout-header"], ["name", "selectedLayoutInfo", "optionLabel", "name", 1, "layout-dropdown", 3, "options", "ngModel", "ngModelChange"], ["pButton", "", "label", "Apply", 1, "apply-button", 3, "disabled", "click"], [3, "model", "formInfo", "modelChange"]], template: function CytoscapeLayoutToolComponent_Template(rf, ctx) { if (rf & 1) {
i0.ɵɵelementStart(0, "div");
i0.ɵɵelementStart(1, "div", 0);
i0.ɵɵelementStart(2, "div", 1);
i0.ɵɵtext(3, "Edit Layout");
i0.ɵɵelementEnd();
i0.ɵɵelementEnd();
i0.ɵɵelementStart(4, "p-dropdown", 2);
i0.ɵɵlistener("ngModelChange", function CytoscapeLayoutToolComponent_Template_p_dropdown_ngModelChange_4_listener($event) { return ctx.layoutOptions = $event; })("ngModelChange", function CytoscapeLayoutToolComponent_Template_p_dropdown_ngModelChange_4_listener() { return ctx.onLayoutModelChange(); });
i0.ɵɵtext(5, " >");
i0.ɵɵelementEnd();
i0.ɵɵelementStart(6, "button", 3);
i0.ɵɵlistener("click", function CytoscapeLayoutToolComponent_Template_button_click_6_listener() { return ctx.onApplyLayout(); });
i0.ɵɵelementEnd();
i0.ɵɵelementEnd();
i0.ɵɵelementStart(7, "cyng-fluid-form", 4);
i0.ɵɵlistener("modelChange", function CytoscapeLayoutToolComponent_Template_cyng_fluid_form_modelChange_7_listener() { return ctx.onFormModelChange(); });
i0.ɵɵelementEnd();
} if (rf & 2) {
i0.ɵɵadvance(4);
i0.ɵɵproperty("options", ctx.layoutOptionsList)("ngModel", ctx.layoutOptions);
i0.ɵɵadvance(2);
i0.ɵɵproperty("disabled", !ctx.changed);
i0.ɵɵadvance(1);
i0.ɵɵproperty("model", ctx.layoutOptions)("formInfo", ctx.formInfo);
} }, directives: [i7.Dropdown, i1.NgControlStatus, i1.NgModel, i8.ButtonDirective, FluidFormComponent], styles: ["[_nghost-%COMP%] {\n width: 400px;\n height: 2em;\n }\n\n .layout-header[_ngcontent-%COMP%] {\n width: 100%;\n height: 20px;\n }\n\n .layout-dropdown[_ngcontent-%COMP%] {\n padding-right: 10px;\n }\n\n input[_ngcontent-%COMP%]:disabled {\n background-color: rgba(204, 204, 204, .33);\n }"] });
(function () { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(CytoscapeLayoutToolComponent, [{
type: Component,
args: [{
selector: 'cytoscape-layout-tool',
styles: [
`
:host {
width: 400px;
height: 2em;
}
.layout-header {
width: 100%;
height: 20px;
}
.layout-dropdown {
padding-right: 10px;
}
input:disabled {
background-color: rgba(204, 204, 204, .33);
}
`
],
template: `
<div>
<div style="display: flex;">
<div class="layout-header">Edit Layout</div>