Stability improvements and cleanup

This commit is contained in:
Thomas Lovén 2021-03-28 09:52:37 +00:00
parent 9dead0c87d
commit 24954fddd2
3 changed files with 176 additions and 171 deletions

View File

@ -1,60 +1,59 @@
import { css, LitElement, property, svg } from "lit-element"; import { css, LitElement, property, svg } from "lit-element";
const NODE_SIZE = 24; const NODE_SIZE = 24;
const VERTICAL_SPACING = 10;
export class HatGraphNode extends LitElement { export class HatGraphNode extends LitElement {
@property() iconPath?; @property() iconPath?;
config = undefined; @property({ reflect: true }) marked?;
@property({ reflect: true }) selected?;
@property({ reflect: true }) disabled?;
connectedCallback() { connectedCallback() {
super.connectedCallback(); super.connectedCallback();
if (!this.hasAttribute("tabindex")) this.setAttribute("tabindex", "0"); if (!this.hasAttribute("tabindex")) this.setAttribute("tabindex", "0");
this.addEventListener("focusin", () => this.setAttribute("selected", ""));
this.addEventListener("focusout", () => this.removeAttribute("selected"));
} }
updateNode(config) { updated() {
this.dispatchEvent( const svg = this.shadowRoot.querySelector("svg");
new CustomEvent("update-node", { const bbox = svg.getBBox();
detail: { config }, svg.setAttribute("width", `${bbox.width + 2}px`);
bubbles: true, svg.setAttribute("height", `${bbox.height + 1}px`);
composed: true, svg.setAttribute(
}) "viewBox",
`${bbox.x - 1} ${bbox.y} ${bbox.width + 2} ${bbox.height + 1}`
); );
} }
deleteNode() {
this.dispatchEvent(new CustomEvent("delete-node", { bubbles: true }));
}
placeNode(config) {
this.dispatchEvent(
new CustomEvent("place-node", { detail: { config }, bubbles: true })
);
}
get width() {
return NODE_SIZE + 5;
}
get height() {
return NODE_SIZE + 5;
}
render() { render() {
return svg` return svg`
<svg <svg
width="${this.width}"
height="${this.height}"
viewBox="${-this.width / 2} 0 ${this.width} ${this.height}"
> >
<path
class="connector"
d="
M 0 ${-VERTICAL_SPACING}
L 0 0
"
line-caps="round"
/>
<g class="node"
transform="translate(0 ${VERTICAL_SPACING})"
>
<circle <circle
cx="0" cx="0"
cy="${this.width / 2}" cy="0"
r="${NODE_SIZE / 2}" r="${NODE_SIZE / 2}"
/> />
<g <g
style="pointer-events: none" style="pointer-events: none"
transform="translate(${-12} ${this.width / 2 - 12})" transform="translate(${-12} ${-12})"
> >
${this.iconPath ? svg`<path d="${this.iconPath}"/>` : ""} ${this.iconPath ? svg`<path d="${this.iconPath}"/>` : ""}
</g> </g>
</g>
</svg> </svg>
`; `;
} }
@ -63,28 +62,40 @@ export class HatGraphNode extends LitElement {
return css` return css`
:host { :host {
display: flex; display: flex;
flex-direction: column;
--stroke-clr: var(--stroke-color, rgb(3, 169, 244)); --stroke-clr: var(--stroke-color, rgb(3, 169, 244));
--hover-clr: var(--hover-color, rgb(255, 152, 0)); --selected-clr: var(--selected-color, rgb(255, 152, 0));
} --marked-clr: var(--marked-color, green);
:host(:hover) { --hover-clr: var(--hover-color, red);
--stroke-clr: var(--hover-clr); --disabled-clr: var(--disabled-color, gray);
} }
:host([selected]),
:host(:focus) { :host(:focus) {
--stroke-clr: green; --circle-clr: var(--selected-clr);
outline: none; outline: none;
} }
:host(.dragging) { :host([marked]) {
--stroke-clr: gray; --stroke-clr: var(--marked-clr);
color: gray;
} }
:host(.dragging) path { :host(:hover) circle {
stroke: gray; --stroke-clr: var(--hover-clr);
fill: gray;
} }
circle { :host([disabled]) circle {
stroke: var(--disabled-clr);
}
:host-context([disabled]) {
--stroke-clr: var(--disabled-clr);
}
circle,
path.connector {
stroke: var(--stroke-clr); stroke: var(--stroke-clr);
stroke-width: 2; stroke-width: 2;
fill: none;
}
circle {
fill: white; fill: white;
stroke: var(--circle-clr, var(--stroke-clr));
} }
`; `;
} }

View File

@ -1,131 +1,131 @@
import { css, html, LitElement, property, svg } from "lit-element"; import { css, html, LitElement, property, svg } from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import "./hat-graph-node";
const BRANCH_HEIGHT = 30; const BRANCH_HEIGHT = 30;
const BRANCH_CURVATURE = 25; const BRANCH_CURVATURE = 25;
const VERTICAL_SPACING = 10;
const NODE_SIZE = 24;
export class HatGraph extends LitElement { export class HatGraph extends LitElement {
@property() _num_items = 0; @property() _num_items = 0;
@property({ reflect: true }) branching?; @property({ reflect: true }) branching?;
config?; @property({ reflect: true }) mark_start?;
@property({ reflect: true }) mark_end?;
updateNode(config) { @property({ reflect: true }) disabled?;
this.dispatchEvent(
new CustomEvent("update-node", {
detail: { config },
bubbles: true,
composed: true,
})
);
}
deleteNode() {
this.dispatchEvent(new CustomEvent("delete-node", { bubbles: true }));
}
placeNode(config) {
this.dispatchEvent(
new CustomEvent("place-node", { detail: { config }, bubbles: true })
);
}
get width() {
let w = 0;
if (this.branching !== undefined) {
for (const c of this.children) {
if (c.slot === "head") continue;
w += (c as any).width;
}
} else {
for (const c of this.children) {
w = Math.max(w, (c as any).width);
}
}
return w;
}
get height() {
let h = 0;
if (this.branching !== undefined) {
for (const c of this.children) {
if (c.slot === "head") continue;
h = Math.max(h, (c as any).height);
}
h += 2 * BRANCH_HEIGHT;
} else {
for (const c of this.children) {
h += (c as any).height + VERTICAL_SPACING;
}
h;
}
return h;
}
async updateChildren() { async updateChildren() {
this._num_items = this.children.length; this._num_items = this.children.length;
} }
render() { render() {
let branch_x = []; let branches = [];
let total = 0; let total_width = 0;
let max_height = 0;
let min_height = Number.POSITIVE_INFINITY;
if (this.branching !== undefined) { if (this.branching !== undefined) {
for (const c of Array.from(this.children)) { for (const c of Array.from(this.children)) {
if (c.slot === "head") continue; if (c.slot === "head") continue;
const rect = c.getBoundingClientRect(); const rect = c.getBoundingClientRect();
branch_x.push(rect.width / 2 + total); branches.push({
total += rect.width; x: rect.width / 2 + total_width,
height: rect.height,
start: c.getAttribute("graph-start") != null,
end: c.getAttribute("graph-end") != null,
});
total_width += rect.width;
max_height = Math.max(max_height, rect.height);
min_height = Math.min(min_height, rect.height);
} }
} }
const line_end = this.height - BRANCH_HEIGHT;
return html` return html`
<svg width="${this.width}" height="${this.height}"> <slot name="head" @slotchange=${this.updateChildren}> </slot>
<rect ${this.branching !== undefined
x="0" ? svg`
y="0" <svg
width="${this.width}" id="top"
height="${this.height}" width="${total_width}"
fill="white" height="${BRANCH_HEIGHT}"
/> >
${branches.map((branch, i) => {
if (branch.start) return "";
return svg`
<path
class="${classMap({
line: true,
marked: this.mark_start === i,
})}"
id="${this.mark_start === i ? "mark-start" : ""}"
index=${i}
d="
M ${total_width / 2} 0
C ${total_width / 2} ${BRANCH_CURVATURE}
${branch.x} ${BRANCH_HEIGHT - BRANCH_CURVATURE}
${branch.x} ${BRANCH_HEIGHT}
"/>
`;
})}
<use xlink:href="#mark-start" />
</svg>
`
: ""}
<div id="branches">
${this.branching !== undefined ${this.branching !== undefined
? branch_x.map((x) => { ? svg`
return svg` <svg
<path id="lines"
class="line" width="${total_width}"
d=" height="${max_height}"
M ${this.width / 2} 0 >
C ${this.width / 2} ${BRANCH_CURVATURE} ${branches.map((branch, i) => {
${x} ${BRANCH_HEIGHT - BRANCH_CURVATURE} if (branch.end) return "";
${x} ${BRANCH_HEIGHT} return svg`
L ${x} ${line_end} <path
C ${x} ${line_end + BRANCH_CURVATURE} class="${classMap({
${this.width / 2} ${this.height - BRANCH_CURVATURE} line: true,
${this.width / 2} ${this.height} marked: this.mark_end === i,
" })}"
/> index=${i}
`; d="
}) M ${branch.x} ${branch.height}
: svg` l 0 ${max_height - branch.height}
<path "/>
class="line" `;
d=" })}
M ${this.width / 2} 0 </svg>
L ${this.width / 2} ${this.height} `
" : ""}
/>
`}
</svg>
<slot name="head"> </slot>
<div
id="branches"
class="${this.querySelector(":scope > [slot='head']") ? "" : "no-head"}"
>
<slot @slotchange=${this.updateChildren}></slot> <slot @slotchange=${this.updateChildren}></slot>
</div> </div>
${this.branching !== undefined
? svg`
<svg
id="bottom"
width="${total_width}"
height="${BRANCH_HEIGHT}"
>
${branches.map((branch, i) => {
if (branch.end) return "";
return svg`
<path
class="${classMap({
line: true,
marked: this.mark_end === i,
})}"
id="${this.mark_end === i ? "mark-end" : ""}"
index=${i}
d="
M ${branch.x} 0
C ${branch.x} ${BRANCH_CURVATURE}
${total_width / 2} ${BRANCH_HEIGHT - BRANCH_CURVATURE}
${total_width / 2} ${BRANCH_HEIGHT}
"/>
`;
})}
<use xlink:href="#mark-end" />
</svg>
`
: ""}
`; `;
} }
@ -134,47 +134,38 @@ export class HatGraph extends LitElement {
:host { :host {
position: relative; position: relative;
display: flex; display: flex;
flex-direction: column;
align-items: center;
--stroke-clr: var(--stroke-color, rgb(3, 169, 244)); --stroke-clr: var(--stroke-color, rgb(3, 169, 244));
--hover-clr: var(--hover-color, rgb(255, 152, 0)); --marked-clr: var(--marked-color, green);
--disabled-clr: var(--disabled-color, gray);
} }
#branches { #branches {
position: absolute; position: relative;
left: 0;
display: flex; display: flex;
flex-direction: row; flex-direction: column;
align-items: center; align-items: center;
} }
:host([branching]) #branches { :host([branching]) #branches {
top: ${BRANCH_HEIGHT}px;
flex-direction: row; flex-direction: row;
align-items: start; align-items: start;
} }
:host(:not([branching])) #branches {
top: ${VERTICAL_SPACING + NODE_SIZE}px; /* SHould be something else*/ #lines {
flex-direction: column; position: absolute;
} z-index: -1;
:host(:not([branching])) #branches.no-head {
top: 0;
} }
path.line { path.line {
stroke: var(--stroke-clr); stroke: var(--stroke-clr);
stroke-width: 2; stroke-width: 2;
fill: none; fill: none;
} }
:host(:not([branching])) ::slotted(*) { path.line.marked {
margin-bottom: 10px; stroke: var(--marked-clr);
} }
::slotted(:last-child) { :host([disabled]) path.line {
margin-bottom: 0; stroke: var(--disabled-clr);
}
::slotted([slot="head"]) {
position: absolute;
top: ${BRANCH_HEIGHT / 2}px;
left: 50%;
transform: translate(-50%, -50%);
}
:host(:focus-within) ::slotted([slot="head"]) {
--stroke-color: green;
} }
`; `;
} }

View File

@ -49,6 +49,7 @@ const ICONS = {
chooseChoice: mdiCheckBoxOutline, chooseChoice: mdiCheckBoxOutline,
chooseDefault: mdiCheckboxBlankOutline, chooseDefault: mdiCheckboxBlankOutline,
YAML: mdiCodeJson, YAML: mdiCodeJson,
trigger: mdiAsterisk,
}; };
const SPECIAL_NODES = { const SPECIAL_NODES = {
@ -69,8 +70,13 @@ function makeConditionNode(config) {
html` html`
<hat-graph-node slot="head" .iconPath=${mdiAbTesting} @focus=${focused}> <hat-graph-node slot="head" .iconPath=${mdiAbTesting} @focus=${focused}>
</hat-graph-node> </hat-graph-node>
<hat-graph-node .iconPath=${mdiCheck} @focus=${focused}></hat-graph-node> <hat-graph-node .iconPath=${mdiCheck} @focus=${focused}></hat-graph-node>
<hat-graph-node .iconPath=${mdiClose} @focus=${focused}></hat-graph-node> <hat-graph-node
.iconPath=${mdiClose}
@focus=${focused}
graph-end
></hat-graph-node>
`, `,
graph graph
); );
@ -79,7 +85,6 @@ function makeConditionNode(config) {
function makeChooseNode(config) { function makeChooseNode(config) {
const graph = document.createElement("hat-graph") as HatGraph; const graph = document.createElement("hat-graph") as HatGraph;
graph.config = config;
graph.branching = true; graph.branching = true;
const focused = () => const focused = () =>
@ -143,8 +148,6 @@ function makeNode(config) {
node.iconPath = ICONS[type]; node.iconPath = ICONS[type];
node.config = config;
node.addEventListener("focus", (ev) => { node.addEventListener("focus", (ev) => {
node.dispatchEvent( node.dispatchEvent(
new CustomEvent("node-selected", { detail: { config }, bubbles: true }) new CustomEvent("node-selected", { detail: { config }, bubbles: true })