update
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
# Node UI Basics
|
||||
> [!WARNING]
|
||||
> This module is purely experimental and for educational purpose use only.
|
||||
>
|
||||
> Do not use it in any environment but in an experimental one, definitely not in a production environment.
|
||||
>
|
||||
> I'm not responsible for any damage or harm by the use of anything from this repo.
|
||||
>
|
||||
> Use it at your own risk.
|
||||
|
||||
> [!CAUTION]
|
||||
> Do not use this module unless you have reviewed the source codes thoroughly, understand what it does and in an experimental environment.
|
||||
|
||||
Contains the underlying basic concepts to create Node based UI.
|
||||
|
||||
Please watch these videos for more details:
|
||||
|
||||
[](https://youtu.be/sMDIly3bddo)
|
||||
[](https://youtu.be/iFPyQjJ2Uyw)
|
||||
[](https://youtu.be/knC4BaGbWGo)
|
||||
[](https://youtu.be/v9TKiaKA_SE)
|
||||
@@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
{
|
||||
'name': "Node UI Basics",
|
||||
'summary': """Node UI Basics""",
|
||||
'description': """
|
||||
Tech stacks for developing Node UI
|
||||
""",
|
||||
'author': "Yoni Tjio",
|
||||
'category': 'Productivity',
|
||||
'version': '18.0.1.0.0',
|
||||
'depends': ['web'],
|
||||
'data': [
|
||||
'views/node_ui_basics_views.xml',
|
||||
],
|
||||
'assets': {
|
||||
"web.assets_backend": [
|
||||
"node_ui_basics/static/src/**/*",
|
||||
],
|
||||
},
|
||||
"license":"Other proprietary",
|
||||
"application": True,
|
||||
"installable": True,
|
||||
"auto_install": False
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<odoo>
|
||||
<data>
|
||||
<record model="ir.actions.client" id="node_ui_basics_svg">
|
||||
<field name="name">SVG</field>
|
||||
<field name="tag">svg_basics</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.client" id="node_ui_basics_svg_nodes">
|
||||
<field name="name">SVG with Nodes</field>
|
||||
<field name="tag">svg_nodes</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.client" id="node_ui_basics_svg_connection">
|
||||
<field name="name">SVG Connection</field>
|
||||
<field name="tag">svg_connection</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.client" id="node_ui_basics_svg_bezier">
|
||||
<field name="name">SVG Bezier</field>
|
||||
<field name="tag">svg_bezier</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.client" id="node_ui_basics_node_ui_svg">
|
||||
<field name="name">Node UI with SVG</field>
|
||||
<field name="tag">node_ui_svg</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.client" id="node_ui_basics_canvas">
|
||||
<field name="name">Canvas</field>
|
||||
<field name="tag">canvas_basics</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.client" id="node_ui_basics_canvas_nodes">
|
||||
<field name="name">Canvas with Nodes</field>
|
||||
<field name="tag">canvas_nodes</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.client" id="node_ui_basics_canvas_connection">
|
||||
<field name="name">Canvas Connection</field>
|
||||
<field name="tag">canvas_connection</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.client" id="node_ui_basics_canvas_connection">
|
||||
<field name="name">Canvas Connection</field>
|
||||
<field name="tag">canvas_connection</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.client" id="node_ui_basics_konva">
|
||||
<field name="name">Konva</field>
|
||||
<field name="tag">canvas_konva</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.client" id="node_ui_basics_movable_div">
|
||||
<field name="name">movable DIV</field>
|
||||
<field name="tag">movable_div</field>
|
||||
</record>
|
||||
|
||||
<menuitem name="Node UI Basics" id="node_ui_basics.menu_root" />
|
||||
|
||||
<menuitem name="Canvas" id="node_ui_basics.canvas_menu"
|
||||
parent="node_ui_basics.menu_root" sequence="1"/>
|
||||
|
||||
<menuitem name="Basics" id="node_ui_basics.canvas_basics_menu"
|
||||
parent="node_ui_basics.canvas_menu" action="node_ui_basics_canvas" sequence="1"/>
|
||||
|
||||
<menuitem name="Canvas with Nodes" id="node_ui_basics.canvas_nodes_menu"
|
||||
parent="node_ui_basics.canvas_menu" action="node_ui_basics_canvas_nodes" sequence="2"/>
|
||||
|
||||
<menuitem name="Canvas Connection" id="node_ui_basics.canvas_connection_menu"
|
||||
parent="node_ui_basics.canvas_menu" action="node_ui_basics_canvas_connection" sequence="2"/>
|
||||
|
||||
<menuitem name="Konva" id="node_ui_basics.canvas_konva_menu"
|
||||
parent="node_ui_basics.menu_root" action="node_ui_basics_konva" sequence="2"/>
|
||||
|
||||
<menuitem name="SVG" id="node_ui_basics.svg_menu"
|
||||
parent="node_ui_basics.menu_root" sequence="3"/>
|
||||
|
||||
<menuitem name="Basics" id="node_ui_basics.svg_basics_menu"
|
||||
parent="node_ui_basics.svg_menu" action="node_ui_basics_svg" sequence="1"/>
|
||||
|
||||
<menuitem name="SVG with Nodes" id="node_ui_basics.svg_nodes_menu"
|
||||
parent="node_ui_basics.svg_menu" action="node_ui_basics_svg_nodes" sequence="2"/>
|
||||
|
||||
<menuitem name="SVG Connection" id="node_ui_basics.svg_connection_menu"
|
||||
parent="node_ui_basics.svg_menu" action="node_ui_basics_svg_connection" sequence="2"/>
|
||||
|
||||
<menuitem name="SVG Bezier" id="node_ui_basics.svg_bezier_menu"
|
||||
parent="node_ui_basics.svg_menu" action="node_ui_basics_svg_bezier" sequence="3"/>
|
||||
|
||||
<menuitem name="Node UI with SVG" id="node_ui_basics.node_ui_svg_menu"
|
||||
parent="node_ui_basics.svg_menu" action="node_ui_basics_node_ui_svg" sequence="4"/>
|
||||
|
||||
<menuitem name="DIV" id="node_ui_basics.movable_div_menu"
|
||||
parent="node_ui_basics.menu_root" action="node_ui_basics_movable_div" sequence="4"/>
|
||||
|
||||
</data>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user