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";
const NODE_SIZE = 24;
const VERTICAL_SPACING = 10;
export class HatGraphNode extends LitElement {
@property() iconPath?;
config = undefined;
@property({ reflect: true }) marked?;
@property({ reflect: true }) selected?;
@property({ reflect: true }) disabled?;
connectedCallback() {
super.connectedCallback();
if (!this.hasAttribute("tabindex")) this.setAttribute("tabindex", "0");
this.addEventListener("focusin", () => this.setAttribute("selected", ""));
this.addEventListener("focusout", () => this.removeAttribute("selected"));
}
updateNode(config) {
this.dispatchEvent(
new CustomEvent("update-node", {
detail: { config },
bubbles: true,
composed: true,
})
updated() {
const svg = this.shadowRoot.querySelector("svg");
const bbox = svg.getBBox();
svg.setAttribute("width", `${bbox.width + 2}px`);
svg.setAttribute("height", `${bbox.height + 1}px`);
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() {
return 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
cx="0"
cy="${this.width / 2}"
cy="0"
r="${NODE_SIZE / 2}"
/>
<g
style="pointer-events: none"
transform="translate(${-12} ${this.width / 2 - 12})"
transform="translate(${-12} ${-12})"
>
${this.iconPath ? svg`<path d="${this.iconPath}"/>` : ""}
</g>
</g>
</svg>
`;
}
@ -63,28 +62,40 @@ export class HatGraphNode extends LitElement {
return css`
:host {
display: flex;
flex-direction: column;
--stroke-clr: var(--stroke-color, rgb(3, 169, 244));
--hover-clr: var(--hover-color, rgb(255, 152, 0));
}
:host(:hover) {
--stroke-clr: var(--hover-clr);
--selected-clr: var(--selected-color, rgb(255, 152, 0));
--marked-clr: var(--marked-color, green);
--hover-clr: var(--hover-color, red);
--disabled-clr: var(--disabled-color, gray);
}
:host([selected]),
:host(:focus) {
--stroke-clr: green;
--circle-clr: var(--selected-clr);
outline: none;
}
:host(.dragging) {
--stroke-clr: gray;
color: gray;
:host([marked]) {
--stroke-clr: var(--marked-clr);
}
:host(.dragging) path {
stroke: gray;
fill: gray;
:host(:hover) circle {
--stroke-clr: var(--hover-clr);
}
circle {
:host([disabled]) circle {
stroke: var(--disabled-clr);
}
:host-context([disabled]) {
--stroke-clr: var(--disabled-clr);
}
circle,
path.connector {
stroke: var(--stroke-clr);
stroke-width: 2;
fill: none;
}
circle {
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 "./hat-graph-node";
import { classMap } from "lit-html/directives/class-map";
const BRANCH_HEIGHT = 30;
const BRANCH_CURVATURE = 25;
const VERTICAL_SPACING = 10;
const NODE_SIZE = 24;
export class HatGraph extends LitElement {
@property() _num_items = 0;
@property({ reflect: true }) branching?;
config?;
updateNode(config) {
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;
}
@property({ reflect: true }) mark_start?;
@property({ reflect: true }) mark_end?;
@property({ reflect: true }) disabled?;
async updateChildren() {
this._num_items = this.children.length;
}
render() {
let branch_x = [];
let total = 0;
let branches = [];
let total_width = 0;
let max_height = 0;
let min_height = Number.POSITIVE_INFINITY;
if (this.branching !== undefined) {
for (const c of Array.from(this.children)) {
if (c.slot === "head") continue;
const rect = c.getBoundingClientRect();
branch_x.push(rect.width / 2 + total);
total += rect.width;
branches.push({
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`
<svg width="${this.width}" height="${this.height}">
<rect
x="0"
y="0"
width="${this.width}"
height="${this.height}"
fill="white"
/>
<slot name="head" @slotchange=${this.updateChildren}> </slot>
${this.branching !== undefined
? branch_x.map((x) => {
? svg`
<svg
id="top"
width="${total_width}"
height="${BRANCH_HEIGHT}"
>
${branches.map((branch, i) => {
if (branch.start) return "";
return svg`
<path
class="line"
class="${classMap({
line: true,
marked: this.mark_start === i,
})}"
id="${this.mark_start === i ? "mark-start" : ""}"
index=${i}
d="
M ${this.width / 2} 0
C ${this.width / 2} ${BRANCH_CURVATURE}
${x} ${BRANCH_HEIGHT - BRANCH_CURVATURE}
${x} ${BRANCH_HEIGHT}
L ${x} ${line_end}
C ${x} ${line_end + BRANCH_CURVATURE}
${this.width / 2} ${this.height - BRANCH_CURVATURE}
${this.width / 2} ${this.height}
"
/>
M ${total_width / 2} 0
C ${total_width / 2} ${BRANCH_CURVATURE}
${branch.x} ${BRANCH_HEIGHT - BRANCH_CURVATURE}
${branch.x} ${BRANCH_HEIGHT}
"/>
`;
})
: svg`
<path
class="line"
d="
M ${this.width / 2} 0
L ${this.width / 2} ${this.height}
"
/>
`}
})}
<use xlink:href="#mark-start" />
</svg>
<slot name="head"> </slot>
<div
id="branches"
class="${this.querySelector(":scope > [slot='head']") ? "" : "no-head"}"
`
: ""}
<div id="branches">
${this.branching !== undefined
? svg`
<svg
id="lines"
width="${total_width}"
height="${max_height}"
>
${branches.map((branch, i) => {
if (branch.end) return "";
return svg`
<path
class="${classMap({
line: true,
marked: this.mark_end === i,
})}"
index=${i}
d="
M ${branch.x} ${branch.height}
l 0 ${max_height - branch.height}
"/>
`;
})}
</svg>
`
: ""}
<slot @slotchange=${this.updateChildren}></slot>
</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 {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
--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 {
position: absolute;
left: 0;
position: relative;
display: flex;
flex-direction: row;
flex-direction: column;
align-items: center;
}
:host([branching]) #branches {
top: ${BRANCH_HEIGHT}px;
flex-direction: row;
align-items: start;
}
:host(:not([branching])) #branches {
top: ${VERTICAL_SPACING + NODE_SIZE}px; /* SHould be something else*/
flex-direction: column;
}
:host(:not([branching])) #branches.no-head {
top: 0;
#lines {
position: absolute;
z-index: -1;
}
path.line {
stroke: var(--stroke-clr);
stroke-width: 2;
fill: none;
}
:host(:not([branching])) ::slotted(*) {
margin-bottom: 10px;
path.line.marked {
stroke: var(--marked-clr);
}
::slotted(:last-child) {
margin-bottom: 0;
}
::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;
:host([disabled]) path.line {
stroke: var(--disabled-clr);
}
`;
}

View File

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