This commit is contained in:
2025-08-19 15:05:55 +07:00
parent 70ae66ee4b
commit f78dc34aee
161 changed files with 13703 additions and 0 deletions
+242
View File
@@ -0,0 +1,242 @@
import { Component, useRef, onMounted } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { standardActionServiceProps } from "@web/webclient/actions/action_service";
class CanvasBasics extends Component {
static template = "canvas-basics";
static components = { };
static props = {
...standardActionServiceProps
};
setup() {
this.canvasRef = useRef("interactive-canvas");
this.state = {
startX: 100,
startY: 50,
endX: 1550,
endY: 150,
controlX: 700,
controlY: 300,
};
onMounted(() => {
this._drawShapes();
this.canvasRef.el.width = this.canvasRef.el.parentElement.offsetWidth;
this.canvasRef.el.height = this.canvasRef.el.parentElement.offsetHeight;
this._drawInteractiveCanvas();
})
this.dragging = undefined;
}
onMouseDown(event){
event.preventDefault();
event.stopPropagation();
const cRect = this.canvasRef.el.getBoundingClientRect();
const ctx = this.canvasRef.el.getContext("2d");
const x = event.clientX - cRect.left;
const y = event.clientY - cRect.top;
if (ctx.isPointInPath(this.startPoint, x, y)){
this.dragging = this.startPoint;
} else if (ctx.isPointInPath(this.endPoint, x, y)){
this.dragging = this.endPoint;
} else if (ctx.isPointInPath(this.controlPoint, x, y)){
this.dragging = this.controlPoint;
} else {
this.dragging = undefined;
}
}
onMouseUp(event){
this.dragging = undefined;
}
onMouseMove(event){
if (!this.dragging){
const cRect = this.canvasRef.el.getBoundingClientRect();
const ctx = this.canvasRef.el.getContext("2d");
const x = event.clientX - cRect.left;
const y = event.clientY - cRect.top;
if (ctx.isPointInPath(this.startPoint, x, y)){
document.body.style.cursor = "pointer";
} else if (ctx.isPointInPath(this.endPoint, x, y)){
document.body.style.cursor = "pointer";
} else if (ctx.isPointInPath(this.controlPoint, x, y)){
document.body.style.cursor = "pointer";
} else {
document.body.style.cursor = "auto";
}
}else if (this.dragging){
const cRect = this.canvasRef.el.getBoundingClientRect();
const el = this.dragging;
if (event.x > cRect.left + 5 && event.y > cRect.top + 5
&& event.x < cRect.right - 20 && event.y < cRect.bottom - 20){
let dragging;
if(el == this.startPoint){
this.state.startX += event.movementX;
this.state.startY += event.movementY;
dragging = "s";
} else if (el == this.endPoint){
this.state.endX += event.movementX;
this.state.endY += event.movementY;
dragging = "e";
} else if (el == this.controlPoint){
this.state.controlX += event.movementX;
this.state.controlY += event.movementY;
dragging = "c"
} else {
return;
}
this._drawInteractiveCanvas();
if (dragging === "s") {
this.dragging = this.startPoint;
} else if (dragging === "e"){
this.dragging = this.endPoint;
} else if (dragging === "c"){
this.dragging = this.controlPoint;
}
}
}
}
_drawInteractiveCanvas(){
let ctx = this.canvasRef.el.getContext("2d");
this.startPoint = new Path2D();
this.endPoint = new Path2D();
this.controlPoint = new Path2D();
this.mainPath = new Path2D();
this.controlPath = new Path2D();
ctx.clearRect(0, 0, this.canvasRef.el.width, this.canvasRef.el.height);
ctx.beginPath();
ctx.lineWidth = 3;
ctx.fillStyle = "#b58900";
let path = this.startPoint;
path.arc(this.state.startX, this.state.startY, 6, 0, 2 * Math.PI);
ctx.strokeStyle = "grey";
ctx.fill(path);
ctx.stroke(path);
path = this.mainPath;
path.moveTo(this.state.startX, this.state.startY);
path.quadraticCurveTo(this.state.controlX, this.state.controlY, this.state.endX, this.state.endY);
ctx.strokeStyle = "#b58900";
ctx.stroke(path);
path = this.endPoint;
path.arc(this.state.endX, this.state.endY, 6, 0, 2 * Math.PI);
ctx.strokeStyle = "grey";
ctx.fill(path);
ctx.stroke(path);
path = this.controlPoint;
path.arc(this.state.controlX, this.state.controlY, 6, 0, 2 * Math.PI);
ctx.fill(path);
ctx.stroke(path);
path = this.controlPath;
path.moveTo(this.state.startX, this.state.startY);
path.lineTo(this.state.controlX, this.state.controlY);
path.moveTo(this.state.controlX, this.state.controlY);
path.lineTo(this.state.endX, this.state.endY);
ctx.lineWidth = 1;
ctx.stroke(path);
}
_drawShapes(){
let cvs = document.getElementById("cvsRectangle");
let ctx = cvs.getContext("2d");
ctx.fillStyle = "#b58900";
ctx.fillRect(20, 20, 260, 100);
cvs = document.getElementById("cvsCircle");
ctx = cvs.getContext("2d");
ctx.beginPath();
ctx.arc(150, 72.5, 50, 0, 2 * Math.PI);
ctx.fillStyle = "#b58900";
ctx.fill();
ctx.strokeStyle = "grey";
ctx.stroke();
cvs = document.getElementById("cvsEllipse");
ctx = cvs.getContext("2d");
ctx.beginPath();
ctx.ellipse(150, 72.5, 100, 50, 0, 0, 2 * Math.PI)
ctx.fillStyle = "#b58900";
ctx.fill();
ctx.strokeStyle = "grey";
ctx.stroke();
cvs = document.getElementById("cvsLine");
ctx = cvs.getContext("2d");
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(30, 62.5);
ctx.lineTo(270, 62.5);
ctx.moveTo(30, 72.5);
ctx.lineTo(270, 72.5);
ctx.moveTo(30, 82.5);
ctx.lineTo(270, 82.5);
ctx.strokeStyle = "#b58900";
ctx.stroke();
cvs = document.getElementById("cvsRoundRect");
ctx = cvs.getContext("2d");
ctx.beginPath();
ctx.roundRect(20, 20, 260, 100, [10]);
ctx.fillStyle = "#b58900";
ctx.fill();
ctx.strokeStyle = "grey";
ctx.stroke();
cvs = document.getElementById("cvsCubicCurve");
ctx = cvs.getContext("2d");
ctx.lineWidth = 4;
let start = { x: 20, y: 20 };
let cp1 = { x: 200, y: 50 };
let cp2 = { x: 50, y: 80 };
let end = { x: 270, y: 120 };
ctx.beginPath();
ctx.moveTo(start.x, start.y);
ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, end.x, end.y);
ctx.strokeStyle = "#b58900";
ctx.stroke();
cvs = document.getElementById("cvsQuadraticCurve");
ctx = cvs.getContext("2d");
ctx.lineWidth = 4;
ctx.beginPath();
ctx.moveTo(20, 20);
ctx.quadraticCurveTo(20, 120, 270, 120);
ctx.strokeStyle = "#b58900";
ctx.stroke();
cvs = document.getElementById("cvsMiscShape");
ctx = cvs.getContext("2d");
ctx.lineWidth = 4;
ctx.beginPath();
ctx.moveTo(20, 20);
ctx.lineTo(270, 20);
ctx.lineTo(135, 120);
ctx.closePath();
ctx.strokeStyle = "#b58900";
ctx.stroke();
}
}
registry.category("actions").add("canvas_basics", CanvasBasics);
+117
View File
@@ -0,0 +1,117 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="canvas-basics">
<div class="d-flex flex-column h-100 w-100 p-1">
<div class="d-flex">
<div class="m-0 p-1 col-3">
<div class="card text-center">
<div class="card-header">
Rectangle
</div>
<div class="card-body p-1 overflow-hidden">
<canvas id="cvsRectangle">
</canvas>
</div>
</div>
</div>
<div class="m-0 p-1 col-3">
<div class="card text-center">
<div class="card-header">
Circle
</div>
<div class="card-body p-1 overflow-hidden">
<canvas id="cvsCircle">
</canvas>
</div>
</div>
</div>
<div class="m-0 p-1 col-3">
<div class="card text-center">
<div class="card-header">
Ellipse
</div>
<div class="card-body p-1 overflow-hidden">
<canvas id="cvsEllipse">
</canvas>
</div>
</div>
</div>
<div class="m-0 p-1 col-3">
<div class="card text-center">
<div class="card-header">
Line
</div>
<div class="card-body p-1 overflow-hidden">
<canvas id="cvsLine">
</canvas>
</div>
</div>
</div>
</div>
<div class="d-flex">
<div class="m-0 p-1 col-3">
<div class="card text-center">
<div class="card-header">
Round Rect
</div>
<div class="card-body p-1 overflow-hidden">
<canvas id="cvsRoundRect">
</canvas>
</div>
</div>
</div>
<div class="m-0 p-1 col-3">
<div class="card text-center">
<div class="card-header">
Cubic Bézier Curve
</div>
<div class="card-body p-1 overflow-hidden">
<canvas id="cvsCubicCurve">
</canvas>
</div>
</div>
</div>
<div class="m-0 p-1 col-3">
<div class="card text-center">
<div class="card-header">
Quadratic Bézier Curved
</div>
<div class="card-body p-1 overflow-hidden">
<canvas id="cvsQuadraticCurve">
</canvas>
</div>
</div>
</div>
<div class="m-0 p-1 col-3">
<div class="card text-center">
<div class="card-header">
Misc. Shape
</div>
<div class="card-body p-1 overflow-hidden">
<canvas id="cvsMiscShape">
</canvas>
</div>
</div>
</div>
</div>
<div class="d-flex flex-fill">
<div class="m-0 p-1 col-12 h-100">
<div class="card text-center h-100">
<div class="card-header">
Interactive Curve
</div>
<div class="card-body p-1 overflow-hidden">
<canvas id="cvsInteractivePath"
t-ref="interactive-canvas"
t-on-mousedown.stop.prevent="onMouseDown"
t-on-mouseup.stop.prevent="onMouseUp"
t-on-mousemove.stop.prevent="onMouseMove"
>
</canvas>
</div>
</div>
</div>
</div>
</div>
</t>
</templates>
@@ -0,0 +1,134 @@
import { Component, useRef, useState, onWillStart, onMounted } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { standardActionServiceProps } from "@web/webclient/actions/action_service";
class CanvasConnection extends Component {
static template = "canvas-connection";
static components = {};
static props = {
...standardActionServiceProps,
};
setup() {
this.canvasRef = useRef("canvas");
const startX = 600;
const startY = 400;
const midX = 800;
const midY = 100;
const endX = 1000;
const endY = 400;
const rad = 25;
this.state = useState({
orientation: "auto",
startNode: {
cx: startX,
cy: startY,
r: rad,
},
midNode: {
cx: midX,
cy: midY,
r: rad,
},
endNode: {
cx: endX,
cy: endY,
r: rad,
},
});
onMounted(() => {
this.canvasRef.el.width = this.canvasRef.el.parentElement.offsetWidth;
this.canvasRef.el.height = this.canvasRef.el.parentElement.offsetHeight;
this._drawCanvas();
});
}
onMouseDown(event) {
if (event.target.id === "startNode" || event.target.id === "midNode" || event.target.id === "endNode") {
this.dragging = event.target.id;
} else {
this.dragging = undefined;
}
}
onMouseUp(event) {
this.dragging = undefined;
}
onMouseMove(event) {
if (this.dragging) {
const cRect = this.canvasRef.el.getBoundingClientRect();
if (event.x > cRect.left + 5 && event.y > cRect.top + 5 && event.x < cRect.right - 20 && event.y < cRect.bottom - 20) {
if (this.dragging === "startNode") {
this.state.startNode.cx += event.movementX;
this.state.startNode.cy += event.movementY;
} else if (this.dragging === "midNode") {
this.state.midNode.cx += event.movementX;
this.state.midNode.cy += event.movementY;
} else if (this.dragging === "endNode") {
this.state.endNode.cx += event.movementX;
this.state.endNode.cy += event.movementY;
}
}
this._drawCanvas();
}
}
_drawCanvas() {
const ctx = this.canvasRef.el.getContext("2d");
ctx.clearRect(0, 0, this.canvasRef.el.width, this.canvasRef.el.height);
ctx.beginPath();
ctx.lineWidth = 3;
ctx.strokeStyle = "#b58900";
ctx.fillStyle = "#b58900";
let path1 = new Path2D();
let path2 = new Path2D();
this._drawPath(path1, this.state.startNode, this.state.midNode, this.state.orientation);
this._drawPath(path2, this.state.midNode, this.state.endNode, this.state.orientation);
ctx.stroke(path1);
ctx.stroke(path2);
}
_drawPath(path, firstNode, secondNode, orient = "auto") {
let hDistance = Math.abs(firstNode.cx - secondNode.cx);
let vDistance = Math.abs(firstNode.cy - secondNode.cy);
let orientation = "v";
if (orient == "auto"){
orientation = hDistance > vDistance ? "v" : "h";
} else {
orientation = orient === "vertical" ? "v" : "h"
}
let firstX = firstNode.cx;
let firstY = firstNode.cy;
let secondX = secondNode.cx;
let secondY = secondNode.cy;
let midX = (secondX + firstX) / 2;
let midY = (secondY + firstY) / 2;
if (orientation === "v") {
path.moveTo(firstX, firstY);
path.lineTo(firstX, midY);
path.lineTo(secondX, midY);
path.lineTo(secondX, secondY);
} else {
path.moveTo(firstX, firstY);
path.lineTo(midX, firstY);
path.lineTo(midX, secondY);
path.lineTo(secondX, secondY);
}
}
}
registry.category("actions").add("canvas_connection", CanvasConnection);
@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="canvas-connection">
<div class="d-flex flex-column h-100 w-100 p-1">
<div class="d-flex flex-fill">
<div class="m-0 p-1 col-12 h-100">
<div class="card text-center h-100">
<div class="card-header">
Canvas Connection
</div>
<div id="card-body" class="card-body p-1 overflow-hidden position-relative"
t-on-mousedown.stop.prevent="onMouseDown"
t-on-mouseup.stop.prevent="onMouseUp"
t-on-mousemove.stop.prevent="onMouseMove">
<canvas id="canvas"
t-ref="canvas"
style="left: 0px; top: 0px; position: absolute; z-index: -1000;"/>
<div id="container" class="h-100 w-100"
style="left: 0px; top: 0px; position: absolute;">
<div id="startNode" class="circle"
t-attf-style="left:{{state.startNode.cx - state.startNode.r}}px;
top:{{state.startNode.cy - state.startNode.r}}px;">
</div>
<div id="midNode" class="circle"
t-attf-style="left:{{state.midNode.cx - state.midNode.r}}px;
top:{{state.midNode.cy - state.midNode.r}}px;">
</div>
<div id="endNode" class="circle"
t-attf-style="left:{{state.endNode.cx - state.endNode.r}}px;
top:{{state.endNode.cy - state.endNode.r}}px;">
</div>
</div>
<div class="btn-group" role="group" style="top:10px; right:10px; position: absolute;">
<input id="optOrientationVertical"
autocomplete="off"
name="optOrientation"
class="btn-check"
type="radio"
value="vertical"
t-model="state.orientation"
t-on-change="_drawCanvas">
</input>
<label class="btn btn-primary" for="optOrientationVertical">Vertical</label>
<input id="optOrientationHorizontal"
autocomplete="off"
name="optOrientation"
class="btn-check"
type="radio"
value="horizontal"
t-model="state.orientation"
t-on-change="_drawCanvas">
</input>
<label class="btn btn-primary" for="optOrientationHorizontal">Horizontal</label>
<input id="optOrientationAuto"
autocomplete="off"
name="optOrientation"
class="btn-check"
type="radio"
value="auto"
t-model="state.orientation"
t-on-change="_drawCanvas">
</input>
<label class="btn btn-primary" for="optOrientationAuto">Auto</label>
</div>
</div>
</div>
</div>
</div>
</div>
</t>
</templates>
+171
View File
@@ -0,0 +1,171 @@
import { Component, useRef, useState, onWillStart, onMounted } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { standardActionServiceProps } from "@web/webclient/actions/action_service";
class CanvasKonva extends Component {
static template = "canvas-konva";
static components = {};
static props = {
...standardActionServiceProps,
};
setup() {
this.konvaRef = useRef("konva");
onWillStart(async () => {
await import("/node_ui_basics/static/lib/konva.js");
});
onMounted(() => {
var width = window.innerWidth;
var height = window.innerHeight;
// function to build anchor point
function buildAnchor(x, y) {
var anchor = new Konva.Circle({
x: x,
y: y,
radius: 6,
stroke: "grey",
fill: "#b58900",
strokeWidth: 2,
draggable: true,
});
layer.add(anchor);
// add hover styling
anchor.on("mouseover", function () {
document.body.style.cursor = "pointer";
this.strokeWidth(4);
});
anchor.on("mouseout", function () {
document.body.style.cursor = "default";
this.strokeWidth(2);
});
anchor.on("dragmove", function () {
updateDottedLines();
});
return anchor;
}
var stage = new Konva.Stage({
container: "konva-container",
width: width,
height: height,
});
var layer = new Konva.Layer();
stage.add(layer);
// function to update line points from anchors
function updateDottedLines() {
var q = quad;
var b = bezier;
var quadLinePath = layer.findOne("#quadLinePath");
var bezierLinePath = layer.findOne("#bezierLinePath");
quadLinePath.points([
q.start.x(),
q.start.y(),
q.control.x(),
q.control.y(),
q.end.x(),
q.end.y(),
]);
bezierLinePath.points([
b.start.x(),
b.start.y(),
b.control1.x(),
b.control1.y(),
b.control2.x(),
b.control2.y(),
b.end.x(),
b.end.y(),
]);
}
// we will use custom shape for curve
var quadraticLine = new Konva.Shape({
stroke: "pink",
strokeWidth: 4,
sceneFunc: (ctx, shape) => {
ctx.beginPath();
ctx.moveTo(quad.start.x(), quad.start.y());
ctx.quadraticCurveTo(
quad.control.x(),
quad.control.y(),
quad.end.x(),
quad.end.y()
);
ctx.fillStrokeShape(shape);
},
});
layer.add(quadraticLine);
// we will use custom shape for curve
var bezierLine = new Konva.Shape({
stroke: "#b58900",
strokeWidth: 5,
sceneFunc: (ctx, shape) => {
ctx.beginPath();
ctx.moveTo(bezier.start.x(), bezier.start.y());
ctx.bezierCurveTo(
bezier.control1.x(),
bezier.control1.y(),
bezier.control2.x(),
bezier.control2.y(),
bezier.end.x(),
bezier.end.y()
);
ctx.fillStrokeShape(shape);
},
});
layer.add(bezierLine);
var quadLinePath = new Konva.Line({
dash: [10, 10, 0, 10],
strokeWidth: 3,
stroke: "grey",
lineCap: "round",
id: "quadLinePath",
opacity: 0.3,
points: [0, 0],
});
layer.add(quadLinePath);
var bezierLinePath = new Konva.Line({
dash: [10, 10, 0, 10],
strokeWidth: 3,
stroke: "grey",
lineCap: "round",
id: "bezierLinePath",
opacity: 0.3,
points: [0, 0],
});
layer.add(bezierLinePath);
// special objects to save references to anchors
var quad = {
start: buildAnchor(60, 30),
control: buildAnchor(240, 500),
end: buildAnchor(100, 600),
};
var bezier = {
start: buildAnchor(280, 20),
control1: buildAnchor(530, 500),
control2: buildAnchor(1250, 150),
end: buildAnchor(1580, 600),
};
updateDottedLines();
});
}
}
registry.category("actions").add("canvas_konva", CanvasKonva);
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="canvas-konva">
<div class="d-flex flex-column h-100 w-100 p-1">
<div class="d-flex flex-fill">
<div class="m-0 p-1 col-12 h-100">
<div class="card text-center h-100">
<div class="card-header">
Konva
</div>
<div id="konva-container" class="card-body p-1 overflow-hidden" t-ref="konva">
</div>
</div>
</div>
</div>
</div>
</t>
</templates>
+148
View File
@@ -0,0 +1,148 @@
import { Component, useRef, useState, onMounted } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { standardActionServiceProps } from "@web/webclient/actions/action_service";
import { uuidv4 } from "@node_ui_basics/utils/utils";
class CanvasNodes extends Component {
static template = "canvas-nodes";
static components = {};
static props = {
...standardActionServiceProps,
};
setup() {
this.canvasRef = useRef("canvas");
this.state = useState({
nodes: [],
selected: undefined,
});
onMounted(() => {
this.canvasRef.el.width = this.canvasRef.el.parentElement.offsetWidth;
this.canvasRef.el.height = this.canvasRef.el.parentElement.offsetHeight;
this._drawCanvas();
});
}
onAddButtonClick() {
this.state.nodes.push({
id: uuidv4(),
cx: 50,
cy: 50,
r: 25,
});
this._drawCanvas();
}
onAdd7NodesButtonClick() {
this.state.nodes.push({
id: uuidv4(),
cx: 1000,
cy: 200,
r: 25,
},{
id: uuidv4(),
cx: 750,
cy: 100,
r: 25,
},{
id: uuidv4(),
cx: 500,
cy: 200,
r: 25,
},{
id: uuidv4(),
cx: 500,
cy: 500,
r: 25,
},{
id: uuidv4(),
cx: 750,
cy: 600,
r: 25,
},{
id: uuidv4(),
cx: 1000,
cy: 500,
r: 25,
},{
id: uuidv4(),
cx: 750,
cy: 325,
r: 25,
}
);
this._drawCanvas();
}
onRemoveButtonClick(event) {
if (this.state.selected){
let nodeIdx = this.state.nodes.findIndex(o => o.id === this.state.selected);
if (nodeIdx > -1){
this.state.nodes.splice(nodeIdx, 1);
this._drawCanvas();
}
this.state.selected = undefined;
}
}
onNodeSelected(event){
this.state.selected = event.target.id;
}
onMouseDown(event) {
if (event.target.classList.contains("node")) {
this.dragging = event.target.id;
this.selected = event.target.id;
} else {
this.dragging = undefined;
}
}
onMouseUp(event) {
this.dragging = undefined;
}
onMouseMove(event) {
if (this.dragging) {
const cRect = this.canvasRef.el.getBoundingClientRect();
if (event.x > cRect.left + 5 && event.y > cRect.top + 5 && event.x < cRect.right - 20 && event.y < cRect.bottom - 20) {
const node = this.state.nodes.find((o) => o.id == this.dragging);
if (node){
node.cx += event.movementX;
node.cy += event.movementY;
}
}
this._drawCanvas();
}
}
_drawCanvas() {
if (this.state.nodes.length > 0) {
const nodes = this.state.nodes;
const ctx = this.canvasRef.el.getContext("2d");
ctx.clearRect(0, 0, this.canvasRef.el.width, this.canvasRef.el.height);
ctx.beginPath();
ctx.lineWidth = 3;
ctx.strokeStyle = "#b58900";
ctx.fillStyle = "#b58900";
let path = new Path2D();
path.moveTo(nodes[0].cx, nodes[0].cy);
for (let i = 1; i < this.state.nodes.length; i++) {
path.lineTo(nodes[i].cx, nodes[i].cy);
}
ctx.stroke(path);
}
}
}
registry.category("actions").add("canvas_nodes", CanvasNodes);
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="canvas-nodes">
<div class="d-flex flex-column h-100 w-100 p-1">
<div class="d-flex flex-fill">
<div class="m-0 p-1 col-12 h-100">
<div class="card text-center h-100">
<div class="card-header">
Canvas With DIV Nodes
</div>
<div id="card-body" class="card-body p-1 overflow-hidden position-relative"
t-on-mousedown.stop.prevent="onMouseDown"
t-on-mouseup.stop.prevent="onMouseUp"
t-on-mousemove.stop.prevent="onMouseMove">
<canvas id="canvas" t-ref="canvas"
style="left: 0px; top: 0px; position: absolute; z-index: -1000;"/>
<div id="container" class="h-100 w-100"
style="left: 0px; top: 0px; position: absolute;">
<t t-foreach="state.nodes" t-as="node" t-key="node.id">
<div class="node circle"
t-att-id="node.id"
t-attf-style="left:{{node.cx - node.r}}px;
top:{{node.cy - node.r}}px;
border-color: {{ node.id === state.selected ? '#5f5' : '#999'}}
"
t-on-mousedown.prevent="onNodeSelected">
</div>
</t>
</div>
<div class="btn-group" style="top:10px; right:10px; position: absolute;">
<button class="btn btn-primary my-0" t-on-click="onAddButtonClick">
Add
</button>
<button class="btn btn-primary my-0" t-on-click="onAdd7NodesButtonClick">
Add Seven Nodes
</button>
<button class="btn btn-primary my-0" t-on-click="onRemoveButtonClick">
Remove
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</t>
</templates>
+49
View File
@@ -0,0 +1,49 @@
import { Component, useRef, useState } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { standardActionServiceProps } from "@web/webclient/actions/action_service";
class MovableDiv extends Component {
static template = "movable-div";
static components = {};
static props = {
...standardActionServiceProps,
};
setup() {
this.containerRef = useRef("container");
this.state = useState({
cdLeft: 800,
cdTop: 100,
cLeft: 700,
cTop: 100
});
this.dragging = undefined;
}
onMouseDown(event){
if (event.target.classList.contains("diamond")){
this.dragging = "cd";
} else if (event.target.classList.contains("circle")){
this.dragging = "c";
}
}
onMouseUp(event){
this.dragging = undefined;
}
onMouseMove(event){
if (this.dragging === "cd"){
this.state.cdLeft += event.movementX;
this.state.cdTop += event.movementY;
} else if (this.dragging === "c"){
this.state.cLeft += event.movementX;
this.state.cTop += event.movementY;
}
}
}
registry.category("actions").add("movable_div", MovableDiv);
+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="movable-div">
<div class="d-flex flex-column h-100 w-100 p-1">
<div class="d-flex flex-fill">
<div class="m-0 p-1 col-12 h-100">
<div class="card text-center h-100">
<div class="card-header">
Movable DIV
</div>
<div class="card-body p-1 overflow-hidden" t-ref="container"
t-on-mousedown="onMouseDown" t-on-mousemove="onMouseMove" t-on-mouseup="onMouseUp">
<div class="diamond" t-attf-style="left:{{state.cdLeft}}px; top:{{state.cdTop}}px;">
</div>
<div class="circle" t-attf-style="left:{{state.cLeft}}px; top:{{state.cTop}}px;">
</div>
</div>
</div>
</div>
</div>
</div>
</t>
</templates>
@@ -0,0 +1,68 @@
.diamond {
position: relative;
height: 100px;
width: 100px;
line-height: 200px;
text-align: center;
margin: 10px 40px;
cursor: pointer;
}
.diamond:after {
position: absolute;
top: 10px;
left: 10px;
content: '';
height: calc(100% - 22px);
width: calc(100% - 22px);
background: #b58900;
border: 1px solid #b58900;
transform: rotateX(45deg) rotateZ(45deg);
}
.circle {
position: absolute;
width: 80px;
height: 80px;
background: #b58900;
border-radius: 50%;
cursor: pointer;
border-style: solid;
border-width: 4px;
border-color: #999;
}
.small-circle {
position: absolute;
width: 30px;
height: 30px;
background: #b58900;
border-radius: 50%;
cursor: pointer;
border-style: solid;
border-width: 4px;
border-color: #999;
}
.square {
position: absolute;
width: 80px;
height: 80px;
background: #b58900;
border-radius: 5px;
cursor: pointer;
border-style: solid;
border-width: 4px;
border-color: #999;
}
.node .icon {
color: #f6f6f6;
}
.path {
cursor: pointer;
}
+348
View File
@@ -0,0 +1,348 @@
import { Component, useRef, useState } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { standardActionServiceProps } from "@web/webclient/actions/action_service";
import { uuidv4, createDiv } from "@node_ui_basics/utils/utils";
const icons = [
"fa-envelope-open",
"fa-clone",
"fa-cog",
"fa-database",
"fa-folder-o",
"fa-link",
"fa-lock",
"fa-pencil",
"fa-plus",
"fa-square-o",
]
function getRandomIcon() {
return icons[Math.floor(Math.random() * 10)];
}
const SQUARE_HALF = 40;
const CIRCLE_RAD = 40
class NodeUiSvg extends Component {
static template = "node-ui-svg";
static components = {};
static props = {
...standardActionServiceProps,
};
setup() {
this.containerRef = useRef("container");
this.dragging = undefined;
this.state = useState({
nodes: [],
connections: [],
selected: undefined,
connecting: undefined
});
}
onAddCircleButtonClick() {
this.state.nodes.push({
id: uuidv4(),
cX: 50,
cY: 50,
r: CIRCLE_RAD,
icon: getRandomIcon(),
type: "circle"
});
}
onAddSquareButtonClick() {
this.state.nodes.push({
id: uuidv4(),
cX: 50,
cY: 50,
width: SQUARE_HALF * 2,
height: SQUARE_HALF * 2,
icon: getRandomIcon(),
type: "square"
});
}
onAdd7NodesButtonClick() {
this.state.nodes.push({
id: uuidv4(),
cX: 1000,
cY: 200,
r: CIRCLE_RAD,
icon: getRandomIcon(),
type: "circle"
},{
id: uuidv4(),
cX: 750,
cY: 100,
width: SQUARE_HALF * 2,
height: SQUARE_HALF * 2,
icon: getRandomIcon(),
type: "square"
},{
id: uuidv4(),
cX: 500,
cY: 200,
r: CIRCLE_RAD,
icon: getRandomIcon(),
type: "circle"
},{
id: uuidv4(),
cX: 500,
cY: 500,
width: SQUARE_HALF * 2,
height: SQUARE_HALF * 2,
icon: getRandomIcon(),
type: "square"
},{
id: uuidv4(),
cX: 750,
cY: 600,
r: CIRCLE_RAD,
icon: getRandomIcon(),
type: "circle"
},{
id: uuidv4(),
cX: 1000,
cY: 500,
width: SQUARE_HALF * 2,
height: SQUARE_HALF * 2,
icon: getRandomIcon(),
type: "square"
},{
id: uuidv4(),
cX: 750,
cY: 325,
r: CIRCLE_RAD,
icon: getRandomIcon(),
type: "circle"
}
);
}
onRemoveButtonClick(event) {
if (this.state.selected){
const nodeIdx = this.state.nodes.findIndex(o => o.id === this.state.selected);
if (nodeIdx > -1){
const node = this.state.nodes.at(nodeIdx);
const cnns = this.state.connections.filter(c => c.sourceId === node.id || c.targetId === node.id)
for (const cnn of cnns) {
const cnnIdx = this.state.connections.findIndex(c => c.id === cnn.id);
this.state.connections.splice(cnnIdx, 1);
}
this.state.nodes.splice(nodeIdx, 1);
} else {
const cnnIdx = this.state.connections.findIndex(c => c.id === this.state.selected);
if (cnnIdx > -1) {
this.state.connections.splice(cnnIdx, 1);
}
}
this.state.selected = undefined;
}
}
calcIntersectionPosForCircle(x1, y1, x2, y2, r){
const xDist = x2 - x1;
const yDist = y2 - y1;
const diagDist = Math.sqrt(Math.pow(xDist, 2) + Math.pow(yDist, 2));
const ratio = r/diagDist
const mposX = ratio * xDist;
const mposY = ratio * yDist;
return { x: x1 + mposX, y: y1 + mposY }
}
calcIntersectionPosForSquare(x1, y1, w, h, x2, y2){
const xDist = Math.abs(x2 - x1);
const yDist = Math.abs(y2 - y1);
const signY = Math.sign(y2 - y1)
const signX = Math.sign(x2 - x1)
let res;
if (yDist <= xDist) {
const ratio = Math.abs((w/2)/xDist);
const mposX = (w/2) * signX;
const mposY = ratio * yDist * signY;
res = { x: x1 + mposX, y: y1 + mposY }
} else {
const ratio = Math.abs((h/2)/yDist);
const mposX = ratio * xDist * signX;
const mposY = (h/2) * signY;
res = { x: x1 + mposX, y: y1 + mposY }
}
return res;
}
calcMarkerPosByTargetPos(sourceEl, x, y) {
const cRect = this.containerRef.el.getBoundingClientRect();
const sourceRect = sourceEl.getBoundingClientRect();
const funcForCircle = this.calcIntersectionPosForCircle;
const funcForSquare = this.calcIntersectionPosForSquare;
const pos = sourceEl.classList.contains("circle") ? funcForCircle(
(sourceRect.left + sourceRect.width / 2) - cRect.left,
(sourceRect.top + sourceRect.height / 2) - cRect.top,
x,
y,
CIRCLE_RAD
) : funcForSquare(
(sourceRect.left + sourceRect.width / 2) - cRect.left,
(sourceRect.top + sourceRect.height / 2) - cRect.top,
sourceRect.width,
sourceRect.height,
x,
y
);
return pos;
}
calcMarkerPosByTargetEl(sourceEl, targetEl){
const cRect = this.containerRef.el.getBoundingClientRect();
const sourceRect = sourceEl.getBoundingClientRect();
const targetRect = targetEl.getBoundingClientRect();
const funcForCircle = this.calcIntersectionPosForCircle;
const funcForSquare = this.calcIntersectionPosForSquare;
const pos = sourceEl.classList.contains("circle") ? funcForCircle(
(sourceRect.left + sourceRect.width / 2) - cRect.left,
(sourceRect.top + sourceRect.height / 2) - cRect.top,
(targetRect.left + targetRect.width / 2) - cRect.left,
(targetRect.top + targetRect.height / 2) - cRect.top,
CIRCLE_RAD
) : funcForSquare(
(sourceRect.left + sourceRect.width / 2) - cRect.left,
(sourceRect.top + sourceRect.height / 2) - cRect.top,
sourceRect.width,
sourceRect.height,
(targetRect.left + targetRect.width / 2) - cRect.left,
(targetRect.top + targetRect.height / 2) - cRect.top,
);
return pos;
}
onNodeMouseDown(event){
if (event.ctrlKey) {
event.stopPropagation();
event.preventDefault();
const el = event.target;
const cRect = this.containerRef.el.getBoundingClientRect();
const pos = this.calcMarkerPosByTargetPos(el, event.x - cRect.left, event.y - cRect.top);
this.state.connecting = {
id: uuidv4(),
startX: pos.x,
startY: pos.y,
endX: event.x - cRect.left,
endY: event.y - cRect.top,
sourceId: el.id,
};
} else {
this.state.selected = event.target.id;
}
}
onNodeMouseUp(event){
if (this.state.connecting !== undefined){
event.stopPropagation();
if (event.target.classList.contains("node") && event.target.id !== this.state.connecting.sourceId) {
const targetEl = event.target;
const sourceEl = document.getElementById(this.state.connecting.sourceId);
const targetPos = this.calcMarkerPosByTargetEl(targetEl, sourceEl);
this.state.connecting.endX = targetPos.x;
this.state.connecting.endY = targetPos.y;
this.state.connecting.targetId = targetEl.id;
const cnns = this.state.connections.filter(c =>
c.sourceId === this.state.connecting.id || c.targetId === this.state.connecting.id)
if (cnns.length == 0){
this.state.connections.push(this.state.connecting);
}
}
}
this.state.connecting = undefined;
}
onMouseMove(event) {
if (this.dragging) {
const cRect = this.containerRef.el.getBoundingClientRect();
if (event.x > cRect.left + 5 && event.y > cRect.top + 5
&& event.x < cRect.right - 20 && event.y < cRect.bottom - 20) {
const node = this.state.nodes.find((o) => o.id == this.dragging);
if (node){
node.cX += event.movementX;
node.cY += event.movementY;
const cnns = this.state.connections.filter(c => c.sourceId === node.id || c.targetId === node.id)
for (const cnn of cnns) {
this.updateConnectionPos(cnn);
}
}
}
} else if (this.state.connecting){
const cRect = this.containerRef.el.getBoundingClientRect();
const sourceEl = document.getElementById(this.state.connecting.sourceId);
const pos = this.calcMarkerPosByTargetPos(sourceEl, event.x - cRect.left, event.y - cRect.top);
const cnn = this.state.connecting;
cnn.startX = pos.x;
cnn.startY = pos.y;
cnn.endX = event.x - cRect.left;
cnn.endY = event.y - cRect.top;
}
}
onMouseDown(event){
if (!event.ctrlKey) {
if (event.target.classList.contains("node")) {
this.dragging = event.target.id;
this.selected = event.target.id;
} else {
this.dragging = undefined;
}
}
}
onMouseUp(event){
this.dragging = undefined;
this.state.connecting = undefined;
}
updateConnectionPos(cnn){
const startNode = this.state.nodes.find(n => n.id === cnn.sourceId);
const endNode = this.state.nodes.find(n => n.id === cnn.targetId);
if (startNode !== undefined && endNode !== undefined) {
const startEl = document.getElementById(startNode.id);
const endEl = document.getElementById(endNode.id);
let pos = this.calcMarkerPosByTargetEl(startEl, endEl);
cnn.startX = pos.x;
cnn.startY = pos.y;
pos = this.calcMarkerPosByTargetEl(endEl, startEl);
cnn.endX = pos.x;
cnn.endY = pos.y;
}
}
onLineSelected(event){
this.state.selected = event.target.id;
}
onDeselectButtonClick(event) {
this.state.selected = undefined;
}
}
registry.category("actions").add("node_ui_svg", NodeUiSvg);
+120
View File
@@ -0,0 +1,120 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="node-ui-svg">
<div class="d-flex flex-column h-100 w-100 p-1">
<div class="d-flex flex-fill">
<div class="m-0 p-1 col-12 h-100">
<div class="card text-center h-100">
<div class="card-header">
Node UI Basic with SVG
</div>
<div id="card-body"
class="card-body p-1 overflow-hidden position-relative"
t-on-mousedown.stop.prevent="onMouseDown"
t-on-mouseup.stop.prevent="onMouseUp"
t-on-mousemove.stop.prevent="onMouseMove">
<div t-ref="container"
id="container"
class="h-100 w-100"
style="left: 0px; top: 0px; position: absolute;">
<svg xmlns="http://www.w3.org/2000/svg"
style="width:100%; height:100%">
<defs>
<marker id="arrow"
viewBox="0 0 15 15"
refX="5"
refY="5"
markerWidth="6"
markerHeight="6"
orient="auto-start-reverse">
<path d="M 0 0 L 5 5 L 0 10 z"
stroke="context-stroke"
fill="context-stroke"/>
</marker>
<marker id="dot"
viewBox="0 0 30 30"
refX="5"
refY="5"
markerWidth="15"
markerHeight="10"
orient="auto-start-reverse">
<circle cx="0"
cy="5"
r="5"
fill="context-stroke"/>
</marker>
</defs>
<t t-foreach="state.connections"
t-as="cnn"
t-key="cnn.id">
<line class="path" t-attf-style="stroke-width:3;
stroke:{{ cnn.id === state.selected ? '#5f5' : '#b58900'}};"
marker-end="url(#arrow)"
marker-start="url(#dot)"
t-att-id="cnn.id"
t-att-x1="cnn.startX"
t-att-y1="cnn.startY"
t-att-x2="cnn.endX"
t-att-y2="cnn.endY"
t-on-mousedown.prevent="onLineSelected"/>
</t>
<line t-if="state.connecting !== undefined"
style="stroke-width:3;
stroke:#b58900;"
t-att-id="state.connecting.id"
t-att-x1="state.connecting.startX"
t-att-y1="state.connecting.startY"
t-att-x2="state.connecting.endX"
t-att-y2="state.connecting.endY"/>
</svg>
<t t-foreach="state.nodes"
t-as="node"
t-key="node.id">
<div t-attf-class="node {{ node.type }}"
t-att-id="node.id"
t-attf-style="
left:{{node.type === 'circle' ? node.cX - node.r : node.cX - node.width / 2}}px;
top:{{node.type === 'circle' ? node.cY - node.r : node.cY - node.height / 2}}px;
border-color: {{ node.id === state.selected ? '#5f5' : '#999'}}
"
t-on-mousedown="onNodeMouseDown"
t-on-mouseup.prevent="onNodeMouseUp">
<div class="pe-none h-100 w-100 d-flex justify-content-center align-items-center">
<i t-attf-class="icon fa {{node.icon}} fa-3x align-self-center"></i>
</div>
</div>
</t>
</div>
<div class="btn-group"
style="top:10px; right:10px; position: absolute;">
<button class="btn btn-primary my-0"
t-on-click="onAddCircleButtonClick">
<i t-attf-class="icon fa fa-plus fa-lg align-self-center"></i>
<i t-attf-class="icon fa fa-circle fa-lg align-self-center"></i>
</button>
<button class="btn btn-primary my-0"
t-on-click="onAddSquareButtonClick">
<i t-attf-class="icon fa fa-plus fa-lg align-self-center"></i>
<i t-attf-class="icon fa fa-square fa-lg align-self-center"></i>
</button>
<button class="btn btn-primary my-0"
t-on-click="onAdd7NodesButtonClick">
<i t-attf-class="icon fa fa-plus fa-lg align-self-center"></i>
<i t-attf-class="icon fa fa-stop-circle-o fa-lg align-self-center"></i>
</button>
<button class="btn btn-primary my-0"
t-on-click="onDeselectButtonClick">
<i t-attf-class="icon fa fa-times fa-lg align-self-center"></i>
</button>
<button class="btn btn-primary my-0"
t-on-click="onRemoveButtonClick">
<i t-attf-class="icon fa fa-minus fa-lg align-self-center"></i>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</t>
</templates>
+55
View File
@@ -0,0 +1,55 @@
import { Component, useRef, useState } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { standardActionServiceProps } from "@web/webclient/actions/action_service";
class SvgBasics extends Component {
static template = "svg-basics";
static components = { };
static props = {
...standardActionServiceProps
};
setup() {
this.svgRef = useRef("svg");
this.state = useState({
startX: 100,
startY: 50,
endX: 1550,
endY: 300,
controlX: 200,
controlY: 200,
})
this.dragging = undefined;
}
onMouseDown(event){
this.dragging = event.target;
}
onMouseUp(event){
this.dragging = undefined;
}
onMouseMove(event){
if (this.dragging){
const svgRect = this.svgRef.el.getBoundingClientRect();
const el = this.dragging;
if (event.x > svgRect.left + 5 && event.y > svgRect.top + 5
&& event.x < svgRect.right - 5 && event.y < svgRect.bottom - 5){
if(el.id === "startPoint"){
this.state.startX += event.movementX;
this.state.startY += event.movementY;
} else if (el.id === "endPoint"){
this.state.endX += event.movementX;
this.state.endY += event.movementY;
} else if (el.id === "controlPoint"){
this.state.controlX += event.movementX;
this.state.controlY += event.movementY;
}
}
}
}
}
registry.category("actions").add("svg_basics", SvgBasics);
+178
View File
@@ -0,0 +1,178 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="svg-basics">
<div class="d-flex flex-column h-100 w-100 p-1"
t-on-mousedown.stop.prevent="onMouseDown"
t-on-mouseup.stop.prevent="onMouseUp"
t-on-mousemove.stop.prevent="onMouseMove"
>
<div class="d-flex">
<div class="m-0 p-1 col-3">
<div class="card text-center">
<div class="card-header">
Rectangle
</div>
<div class="card-body p-1 overflow-hidden">
<svg xmlns="http://www.w3.org/2000/svg" style="width: 340px; height: 125px">
<rect width="100" height="100" x="10" y="10" rx="10" ry="10" fill="grey"/>
<rect width="100" height="100" x="120" y="10"
style="fill:#b58900;stroke-width:3;stroke:grey"/>
<rect width="100" height="100" x="230" y="10"
style="fill:grey;stroke:#b58900;stroke-width:3;fill-opacity:0.5;stroke-opacity:0.5"/>
</svg>
</div>
</div>
</div>
<div class="m-0 p-1 col-3">
<div class="card text-center">
<div class="card-header">
Circle
</div>
<div class="card-body p-1 overflow-hidden">
<svg xmlns="http://www.w3.org/2000/svg" style="width: 340px; height: 125px">
<circle r="47.5" cx="55" cy="60" fill="#b58900"/>
<circle r="47.5" cx="167.5" cy="60" fill="#123456" stroke="grey" stroke-width="3"/>
<circle r="47.5" cx="280" cy="60" fill="#b58900" stroke="grey" stroke-width="3" opacity="0.5"/>
</svg>
</div>
</div>
</div>
<div class="m-0 p-1 col-3">
<div class="card text-center">
<div class="card-header">
Ellipse
</div>
<div class="card-body p-1 overflow-hidden">
<svg xmlns="http://www.w3.org/2000/svg" style="width: 340px; height: 125px">
<ellipse rx="50" ry="20" cx="55" cy="60" style="fill:#b58900;stroke:grey;stroke-width:3"/>
<ellipse rx="20" ry="50" cx="167.5" cy="60" style="fill:#b58900;stroke:grey;stroke-width:3"/>
<ellipse rx="50" ry="20" cx="280" cy="60" style="fill:#b58900;stroke:grey;stroke-width:3"/>
</svg>
</div>
</div>
</div>
<div class="m-0 p-1 col-3">
<div class="card text-center">
<div class="card-header">
Line
</div>
<div class="card-body p-1 overflow-hidden">
<svg xmlns="http://www.w3.org/2000/svg" style="width: 340px; height: 125px">
<line x1="55" y1="10" x2="55" y2="115" style="stroke:#b58900;stroke-width:2"/>
<line x1="125" y1="10" x2="225" y2="115" style="stroke:#b58900;stroke-width:2"/>
<line x1="280" y1="55" x2="330" y2="55" style="stroke:#b58900;stroke-width:2"/>
<line x1="280" y1="60" x2="330" y2="60" style="stroke:#b58900;stroke-width:2"/>
<line x1="280" y1="65" x2="330" y2="65" style="stroke:#b58900;stroke-width:2"/>
</svg>
</div>
</div>
</div>
</div>
<div class="d-flex">
<div class="m-0 p-1 col-3">
<div class="card text-center">
<div class="card-header">
Polygon
</div>
<div class="card-body p-1 overflow-hidden">
<svg xmlns="http://www.w3.org/2000/svg" style="width: 340px; height: 125px">
<polygon points="225,10 320,120 10,120" style="fill:#b58900;stroke:grey;stroke-width:3"/>
</svg>
</div>
</div>
</div>
<div class="m-0 p-1 col-3">
<div class="card text-center">
<div class="card-header">
Polyline
</div>
<div class="card-body p-1 overflow-hidden">
<svg xmlns="http://www.w3.org/2000/svg" style="width: 340px; height: 125px">
<polyline points="10,10 50,50 55,55 100,60, 200,100 250,80 320,100"
style="fill:none;stroke:#b58900;stroke-width:3"/>
</svg>
</div>
</div>
</div>
<div class="m-0 p-1 col-3">
<div class="card text-center">
<div class="card-header">
Simple Path
</div>
<div class="card-body p-1 overflow-hidden">
<svg xmlns="http://www.w3.org/2000/svg" style="width: 340px; height: 125px">
<path d="M 10 10 l 330 110" stroke="#b58900" stroke-width="3"/>
</svg>
</div>
</div>
</div>
<div class="m-0 p-1 col-3">
<div class="card text-center">
<div class="card-header">
Curved Path
</div>
<div class="card-body p-1 overflow-hidden">
<svg xmlns="http://www.w3.org/2000/svg" style="width: 340px; height: 125px">
<path d="M 10 100 Q 0 0 330 110" stroke="#b58900" stroke-width="3" fill="none"/>
</svg>
</div>
</div>
</div>
</div>
<div class="d-flex flex-fill">
<div class="m-0 p-1 col-12 h-100">
<div class="card text-center h-100">
<div class="card-header">
Interactive Path
</div>
<div class="card-body p-1 overflow-hidden" t-ref="svg">
<svg xmlns="http://www.w3.org/2000/svg" style="width:100%; height:100%">
<style>
circle { cursor: pointer; }
</style>
<t t-set="startX" t-value="state.startX"/>
<t t-set="startY" t-value="state.startY"/>
<t t-set="endX" t-value="state.endX"/>
<t t-set="endY" t-value="state.endY"/>
<t t-set="controlX" t-value="state.controlX"/>
<t t-set="controlY" t-value="state.controlY"/>
<t t-set="midX" t-value="(startX + endX)/2"/>
<t t-set="midY" t-value="(startY + endY)/2"/>
<t t-call="qbc"/>
</svg>
</div>
</div>
</div>
</div>
</div>
</t>
<t t-name="qbc">
<g xmlns="http://www.w3.org/2000/svg" >
<path id="qBC" t-attf-d="
M {{startX}},{{startY}}
Q {{controlX}},{{controlY}} {{midX}},{{midY}}
T {{endX}},{{endY}}"
stroke="#b58900" stroke-width="3" fill="none"/>
<circle id="startPoint" class="point" style="fill:grey"
t-att-cx="startX"
t-att-cy="startY"
r="6"/>
<circle id="endPoint" class="point" style="fill:grey"
t-att-cx="endX"
t-att-cy="endY"
r="6"/>
<circle id="controlPoint" class="point" style="fill:grey"
t-att-cx="controlX"
t-att-cy="controlY"
r="6"/>
<line t-att-x1="startX" t-att-y1="startY"
t-att-x2="controlX" t-att-y2="controlY"
style="stroke:#b58900;stroke-width:2"
stroke-dasharray="10,10"/>
<line t-att-x1="controlX" t-att-y1="controlY"
t-att-x2="midX" t-att-y2="midY"
style="stroke:#b58900;stroke-width:2"
stroke-dasharray="10,10"/>
</g>
</t>
</templates>
+125
View File
@@ -0,0 +1,125 @@
import { Component, useRef, useState, onWillStart, onMounted } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { standardActionServiceProps } from "@web/webclient/actions/action_service";
class SvgBezier extends Component {
static template = "svg-bezier";
static components = {};
static props = {
...standardActionServiceProps,
};
setup() {
this.containerRef = useRef("container");
const startX = 600;
const startY = 400;
const midX = 800;
const midY = 100;
const endX = 1000;
const endY = 400;
const rad = 15;
this.state = useState({
orientation: "auto",
startNode: {
cx: startX,
cy: startY,
r: rad,
},
midNode: {
cx: midX,
cy: midY,
r: rad,
},
endNode: {
cx: endX,
cy: endY,
r: rad,
},
path: "",
});
onMounted(() => {
this.updatePath();
});
}
onMouseDown(event) {
if (event.target.id === "startNode" || event.target.id === "midNode" || event.target.id === "endNode") {
this.dragging = event.target.id;
} else {
this.dragging = undefined;
}
}
onMouseUp(event) {
this.dragging = undefined;
}
onMouseMove(event) {
if (this.dragging) {
const cRect = this.containerRef.el.getBoundingClientRect();
if (event.x > cRect.left + 5 && event.y > cRect.top + 5 && event.x < cRect.right - 20 && event.y < cRect.bottom - 20) {
if (this.dragging === "startNode") {
this.state.startNode.cx += event.movementX;
this.state.startNode.cy += event.movementY;
} else if (this.dragging === "midNode") {
this.state.midNode.cx += event.movementX;
this.state.midNode.cy += event.movementY;
} else if (this.dragging === "endNode") {
this.state.endNode.cx += event.movementX;
this.state.endNode.cy += event.movementY;
}
this.updatePath();
}
}
}
updatePath(){
this.state.path = this._createPaths();
}
_createPaths(){
let res = "";
const startNode = this.state.startNode;
const midNode = this.state.midNode;
const endNode = this.state.endNode;
res = res + this._createPath(startNode.cx, startNode.cy, midNode.cx, midNode.cy);
res = res + " " + this._createPath(midNode.cx, midNode.cy, endNode.cx, endNode.cy);
return res;
}
// https://stackoverflow.com/a/45245042
_createPath(startX, startY, endX, endY) {
// L
let BX = Math.abs(endX - startX) * 0.05 + startX;
let BY = startY;
// C
let CX = startX + Math.abs(endX - startX) * 0.33;
let CY = startY;
let DX = endX - Math.abs(endX - startX) * 0.33;
let DY = endY;
let EX = -Math.abs(endX - startX) * 0.05 + endX;
let EY = endY;
const svgPath = []
svgPath.push("M", startX, startY);
svgPath.push("L", BX, ",", BY);
svgPath.push("C", CX, ",", CY);
svgPath.push(DX, ",", DY);
svgPath.push(EX, ",", EY);
svgPath.push("L", endX, ",", endY);
const res = svgPath.join(" ");
return res;
}
}
registry.category("actions").add("svg_bezier", SvgBezier);
+41
View File
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="svg-bezier">
<div class="d-flex flex-column h-100 w-100 p-1">
<div class="d-flex flex-fill">
<div class="m-0 p-1 col-12 h-100">
<div class="card text-center h-100">
<div class="card-header">
SVG Connection
</div>
<div id="card-body" class="card-body p-1 overflow-hidden position-relative"
t-on-mousedown.stop.prevent="onMouseDown"
t-on-mouseup.stop.prevent="onMouseUp"
t-on-mousemove.stop.prevent="onMouseMove">
<div t-ref="container" id="container" class="h-100 w-100"
style="left: 0px; top: 0px; position: absolute;">
<svg xmlns="http://www.w3.org/2000/svg" style="width:100%; height:100%">
<path stroke="#b58900" stroke-width="3" fill="none"
t-att-d="state.path"
/>
</svg>
<div id="startNode" class="small-circle"
t-attf-style="left:{{state.startNode.cx - state.startNode.r}}px;
top:{{state.startNode.cy - state.startNode.r}}px;">
</div>
<div id="midNode" class="small-circle"
t-attf-style="left:{{state.midNode.cx - state.midNode.r}}px;
top:{{state.midNode.cy - state.midNode.r}}px;">
</div>
<div id="endNode" class="small-circle"
t-attf-style="left:{{state.endNode.cx - state.endNode.r}}px;
top:{{state.endNode.cy - state.endNode.r}}px;">
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</t>
</templates>
+179
View File
@@ -0,0 +1,179 @@
import { Component, useRef, useState, onWillStart, onMounted } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { standardActionServiceProps } from "@web/webclient/actions/action_service";
class SvgConnection extends Component {
static template = "svg-connection";
static components = {};
static props = {
...standardActionServiceProps,
};
setup() {
this.containerRef = useRef("container");
const startX = 600;
const startY = 400;
const midX = 800;
const midY = 100;
const endX = 1000;
const endY = 400;
const rad = 25;
this.state = useState({
orientation: "auto",
startNode: {
cx: startX,
cy: startY,
r: rad,
},
midNode: {
cx: midX,
cy: midY,
r: rad,
},
endNode: {
cx: endX,
cy: endY,
r: rad,
},
path: "",
});
onMounted(() => {
this.updatePath();
});
}
onMouseDown(event) {
if (event.target.id === "startNode" || event.target.id === "midNode" || event.target.id === "endNode") {
this.dragging = event.target.id;
} else {
this.dragging = undefined;
}
}
onMouseUp(event) {
this.dragging = undefined;
}
onMouseMove(event) {
if (this.dragging) {
const cRect = this.containerRef.el.getBoundingClientRect();
if (event.x > cRect.left + 5 && event.y > cRect.top + 5 && event.x < cRect.right - 20 && event.y < cRect.bottom - 20) {
if (this.dragging === "startNode") {
this.state.startNode.cx += event.movementX;
this.state.startNode.cy += event.movementY;
} else if (this.dragging === "midNode") {
this.state.midNode.cx += event.movementX;
this.state.midNode.cy += event.movementY;
} else if (this.dragging === "endNode") {
this.state.endNode.cx += event.movementX;
this.state.endNode.cy += event.movementY;
}
this.updatePath();
}
}
}
updatePath(){
this.state.path = this._createPaths(this.state.orientation);
}
_createPaths(orient = "auto"){
let res = "";
res = res + this._createPath(this.state.startNode, this.state.midNode, orient);
res = res + " " + this._createPath(this.state.midNode, this.state.endNode, orient);
return res;
}
_createPath(firstNode, secondNode, orient) {
let res = "";
let hDistance = Math.abs(firstNode.cx - secondNode.cx);
let vDistance = Math.abs(firstNode.cy - secondNode.cy);
let orientation = "v";
if (orient == "auto"){
orientation = hDistance > vDistance ? "v" : "h";
} else {
orientation = orient === "vertical" ? "v" : "h"
}
let firstX = firstNode.cx;
let firstY = firstNode.cy;
let secondX = secondNode.cx;
let secondY = secondNode.cy;
let midX = (secondX + firstX) / 2;
let midY = (secondY + firstY) / 2;
const dirX = Math.sign(secondX - firstX);
const dirY = Math.sign(secondY - firstY);
let dirA = dirX > 0 ? 0 : 1;
let dirAFlip = dirA == 0 ? 1 : 0;
if (dirY < 0){
const temp = dirA;
dirA = dirAFlip;
dirAFlip = temp;
}
const minDistance = 5;
const baseMargin = 10;
let margin = baseMargin;
if (hDistance <= margin * 2 || vDistance <= margin * 2){
margin = hDistance > vDistance ? vDistance : hDistance;
}
const marginX = margin * dirX;
const marginY = margin * dirY;
if (orientation === "v") {
res += "M" + (firstX) + "," + (firstY);
res += " L" + (firstX) + "," + (midY - marginY);
if (hDistance > baseMargin * 2){
res += " A" + (margin) + " " + (margin) + " 90 0 " + " "
+ (dirA) + " " + (firstX + marginX) + "," + (midY);
res += " L" + (secondX - marginX) + "," + (midY);
res += " A" + (margin) + " " + (margin) + " 90 0 " + " "
+ (dirAFlip) + " " + (secondX) + "," + (midY + marginY);
} else if (hDistance > minDistance){
res += " A" + (margin) + " " + (margin) + " 90 0 " + " "
+ (dirA) + " " + (firstX + marginX / 2) + "," + (midY);
res += " A" + (margin) + " " + (margin) + " 90 0 " + " "
+ (dirAFlip) + " " + (secondX) + "," + (midY + marginY);
} else {
res += " L" + (secondX) + "," + (midY + marginY);
}
res += " L" + (secondX) + "," + (secondY);
} else {
res += "M" + (firstX) + "," + (firstY);
res += " L" + (midX - marginX) + "," + (firstY);
if (vDistance > baseMargin * 2){
res += " A" + (margin) + " " + (margin) + " 90 0 " + " "
+ (dirAFlip) + " " + (midX) + "," + (firstY + marginY);
res += " L" + (midX) + "," + (secondY - marginY);
res += " A" + (margin) + " " + (margin) + " 90 0 " + " "
+ (dirA) + " " + (midX + marginX) + "," + (secondY);
} else if (vDistance > minDistance){
res += " A" + (margin) + " " + (margin) + " 90 0 " + " "
+ (dirAFlip) + " " + (midX) + "," + (firstY + marginY / 2);
res += " A" + (margin) + " " + (margin) + " 90 0 " + " "
+ (dirA) + " " + (midX + marginX) + "," + (secondY);
} else {
res += " L" + (midX + marginX) + "," + (secondY);
}
res += " L" + (secondX) + "," + (secondY);
}
return res;
}
}
registry.category("actions").add("svg_connection", SvgConnection);
@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="svg-connection">
<div class="d-flex flex-column h-100 w-100 p-1">
<div class="d-flex flex-fill">
<div class="m-0 p-1 col-12 h-100">
<div class="card text-center h-100">
<div class="card-header">
SVG Connection
</div>
<div id="card-body" class="card-body p-1 overflow-hidden position-relative"
t-on-mousedown.stop.prevent="onMouseDown"
t-on-mouseup.stop.prevent="onMouseUp"
t-on-mousemove.stop.prevent="onMouseMove">
<div t-ref="container" id="container" class="h-100 w-100"
style="left: 0px; top: 0px; position: absolute;">
<svg xmlns="http://www.w3.org/2000/svg" style="width:100%; height:100%">
<path stroke="#b58900" stroke-width="3" fill="none"
t-att-d="state.path"
/>
</svg>
<div id="startNode" class="circle"
t-attf-style="left:{{state.startNode.cx - state.startNode.r}}px;
top:{{state.startNode.cy - state.startNode.r}}px;">
</div>
<div id="midNode" class="circle"
t-attf-style="left:{{state.midNode.cx - state.midNode.r}}px;
top:{{state.midNode.cy - state.midNode.r}}px;">
</div>
<div id="endNode" class="circle"
t-attf-style="left:{{state.endNode.cx - state.endNode.r}}px;
top:{{state.endNode.cy - state.endNode.r}}px;">
</div>
</div>
<div class="btn-group" role="group" style="top:10px; right:10px; position: absolute;">
<input id="optOrientationVertical"
autocomplete="off"
name="optOrientation"
class="btn-check"
type="radio"
value="vertical"
t-model="state.orientation"
t-on-change="updatePath">
</input>
<label class="btn btn-primary" for="optOrientationVertical">Vertical</label>
<input id="optOrientationHorizontal"
autocomplete="off"
name="optOrientation"
class="btn-check"
type="radio"
value="horizontal"
t-model="state.orientation"
t-on-change="updatePath">
</input>
<label class="btn btn-primary" for="optOrientationHorizontal">Horizontal</label>
<input id="optOrientationAuto"
autocomplete="off"
name="optOrientation"
class="btn-check"
type="radio"
value="auto"
t-model="state.orientation"
t-on-change="updatePath">
</input>
<label class="btn btn-primary" for="optOrientationAuto">Auto</label>
</div>
</div>
</div>
</div>
</div>
</div>
</t>
</templates>
+113
View File
@@ -0,0 +1,113 @@
import { Component, useRef, useState } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { standardActionServiceProps } from "@web/webclient/actions/action_service";
import { uuidv4 } from "@node_ui_basics/utils/utils";
class SvgNodes extends Component {
static template = "svg-nodes";
static components = {};
static props = {
...standardActionServiceProps,
};
setup() {
this.containerRef = useRef("container");
this.state = useState({
nodes: [],
selected: undefined,
});
}
onAddButtonClick() {
this.state.nodes.push({
id: uuidv4(),
cx: 50,
cy: 50,
r: 25,
});
}
onAdd7NodesButtonClick() {
this.state.nodes.push({
id: uuidv4(),
cx: 1000,
cy: 200,
r: 25,
},{
id: uuidv4(),
cx: 750,
cy: 100,
r: 25,
},{
id: uuidv4(),
cx: 500,
cy: 200,
r: 25,
},{
id: uuidv4(),
cx: 500,
cy: 500,
r: 25,
},{
id: uuidv4(),
cx: 750,
cy: 600,
r: 25,
},{
id: uuidv4(),
cx: 1000,
cy: 500,
r: 25,
},{
id: uuidv4(),
cx: 750,
cy: 325,
r: 25,
}
);
}
onRemoveButtonClick(event) {
if (this.state.selected){
let nodeIdx = this.state.nodes.findIndex(o => o.id === this.state.selected);
if (nodeIdx > -1){
this.state.nodes.splice(nodeIdx, 1);
}
this.state.selected = undefined;
}
}
onNodeSelected(event){
this.state.selected = event.target.id;
}
onMouseDown(event){
if (event.target.classList.contains("node")) {
this.dragging = event.target.id;
this.selected = event.target.id;
} else {
this.dragging = undefined;
}
}
onMouseUp(event){
this.dragging = undefined;
}
onMouseMove(event) {
if (this.dragging) {
const cRect = this.containerRef.el.getBoundingClientRect();
if (event.x > cRect.left + 5 && event.y > cRect.top + 5 && event.x < cRect.right - 20 && event.y < cRect.bottom - 20) {
const node = this.state.nodes.find((o) => o.id == this.dragging);
if (node){
node.cx += event.movementX;
node.cy += event.movementY;
}
}
}
}
}
registry.category("actions").add("svg_nodes", SvgNodes);
+56
View File
@@ -0,0 +1,56 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="svg-nodes">
<div class="d-flex flex-column h-100 w-100 p-1">
<div class="d-flex flex-fill">
<div class="m-0 p-1 col-12 h-100">
<div class="card text-center h-100">
<div class="card-header">
SVG With DIV Nodes
</div>
<div id="card-body" class="card-body p-1 overflow-hidden position-relative"
t-on-mousedown.stop.prevent="onMouseDown"
t-on-mouseup.stop.prevent="onMouseUp"
t-on-mousemove.stop.prevent="onMouseMove">
<div t-ref="container" id="container" class="h-100 w-100"
style="left: 0px; top: 0px; position: absolute;">
<svg xmlns="http://www.w3.org/2000/svg" style="width:100%; height:100%">
<t t-foreach="state.nodes" t-as="node" t-key="node.id">
<t t-if="(state.nodes.length gt 1) and (node_index + 1 lt state.nodes.length)">
<line style="stroke:#b58900;stroke-width:3"
t-att-x1="node.cx"
t-att-y1="node.cy"
t-att-x2="state.nodes[node_index+1].cx"
t-att-y2="state.nodes[node_index+1].cy"/>
</t>
</t>
</svg>
<t t-foreach="state.nodes" t-as="node" t-key="node.id">
<div class="node circle"
t-att-id="node.id"
t-attf-style="left:{{node.cx - node.r}}px;
top:{{node.cy - node.r}}px;
border-color: {{ node.id === state.selected ? '#5f5' : '#999'}}
"
t-on-mousedown.prevent="onNodeSelected">
</div>
</t>
</div>
<div class="btn-group" style="top:10px; right:10px; position: absolute;">
<button class="btn btn-primary my-0" t-on-click="onAddButtonClick">
Add
</button>
<button class="btn btn-primary my-0" t-on-click="onAdd7NodesButtonClick">
Add Seven Nodes
</button>
<button class="btn btn-primary my-0" t-on-click="onRemoveButtonClick">
Remove
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</t>
</templates>
+30
View File
@@ -0,0 +1,30 @@
/*
* comes from o_spreadsheet.js
* https://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
* */
export function uuidv4() {
// mainly for jest and other browsers that do not have the crypto functionality
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(
/[xy]/g,
function (c) {
const r = (Math.random() * 16) | 0,
v = c == "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
}
);
}
export function createDiv(l, t, w, h, c) {
const el = document.createElement("div");
el.className = "debug-div";
el.style.position = "fixed";
el.style.pointerEvents = "none";
el.style.left = `${l}px`;
el.style.top = `${t}px`;
el.style.width = `${w}px`;
el.style.height = `${h}px`;
el.style.background = c;
return document.body.appendChild(el);
}