up
This commit is contained in:
307
src/main.ts
Normal file
307
src/main.ts
Normal file
@@ -0,0 +1,307 @@
|
||||
let lastCreatedNodeId: number = 1;
|
||||
let lastLargeNodeId: number | null = null;
|
||||
|
||||
// Define node content mappings
|
||||
const nodeContents: { [key: string]: string } = {
|
||||
'sendEmailNode': `
|
||||
<label for="email-subject">Subject:</label>
|
||||
<input type="text" id="email-subject" name="email-subject" placeholder="Enter subject">
|
||||
<label for="email-body">Body:</label>
|
||||
<textarea id="email-body" name="email-body" rows="4" placeholder="Enter email body"></textarea>
|
||||
`,
|
||||
'customerSignsUpNode': `
|
||||
<label for="customer-name">Customer Name:</label>
|
||||
<input type="text" id="customer-name" name="customer-name" placeholder="Enter customer name">
|
||||
<label for="customer-email">Customer Email:</label>
|
||||
<input type="email" id="customer-email" name="customer-email" placeholder="Enter customer email">
|
||||
`,
|
||||
'repliedToConversationNode': `
|
||||
<label for="reply-status">Reply Status:</label>
|
||||
<select id="reply-status" name="reply-status">
|
||||
<option value="replied">Replied</option>
|
||||
<option value="not-replied">Not Replied</option>
|
||||
</select>
|
||||
`,
|
||||
'contactExitsNode': `
|
||||
<label for="contact-id">Contact ID:</label>
|
||||
<input type="text" id="contact-id" name="contact-id" placeholder="Enter contact ID">
|
||||
`,
|
||||
'sendSurveyNode': `
|
||||
<label for="survey-question">Survey Question:</label>
|
||||
<input type="text" id="survey-question" name="survey-question" placeholder="Enter survey question">
|
||||
<label for="survey-options">Options (comma-separated):</label>
|
||||
<input type="text" id="survey-options" name="survey-options" placeholder="Option 1, Option 2, ...">
|
||||
`
|
||||
};
|
||||
|
||||
// Function to show the popup with node information
|
||||
function showPopup(nodeId: string): void {
|
||||
const popup = document.getElementById('popup-form') as HTMLElement;
|
||||
const nodeInfoElement = document.getElementById('popup-form-fields') as HTMLElement;
|
||||
const closeBtn = document.querySelector('.popup-close') as HTMLElement;
|
||||
|
||||
if (nodeContents[nodeId]) {
|
||||
nodeInfoElement.innerHTML = nodeContents[nodeId];
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
popup.classList.remove('hidden');
|
||||
|
||||
closeBtn.addEventListener('click', () => {
|
||||
popup.classList.add('hidden');
|
||||
});
|
||||
|
||||
window.addEventListener('click', (event) => {
|
||||
if (event.target === popup) {
|
||||
popup.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Function to add click event listeners to nodes
|
||||
function addClickEventsToParentNodes(): void {
|
||||
document.querySelectorAll('.parent-node').forEach(parentNode => {
|
||||
parentNode.addEventListener('dblclick', (event) => {
|
||||
const drawflowNode = parentNode.querySelector('.node') as HTMLElement;
|
||||
const nodeId = drawflowNode.getAttribute('data-node') as string;
|
||||
console.log(parentNode);
|
||||
if (nodeId) {
|
||||
showPopup(nodeId);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize Drawflow editor
|
||||
var editor = new Drawflow(document.getElementById('drawflow') as HTMLElement);
|
||||
editor.reroute = true;
|
||||
editor.line_path = 0.5;
|
||||
editor.start();
|
||||
|
||||
editor.createCurvature = function (start_x, start_y, end_x, end_y, curvature_value, type) {
|
||||
var center_y = ((end_y - start_y) / 2) + start_y;
|
||||
var default_round_radius = 10;
|
||||
var round_radius = Math.min(default_round_radius, Math.abs(start_x - end_x), Math.abs(start_y - end_y));
|
||||
|
||||
var isRight = end_x > start_x;
|
||||
var isDown = end_y > start_y;
|
||||
return `M ${start_x} ${start_y}
|
||||
L ${start_x} ${isDown ? center_y - round_radius : center_y + round_radius}
|
||||
A ${round_radius} ${round_radius} 0 0 ${isRight ^ !isDown ? 0 : 1} ${isRight ? start_x + round_radius : start_x - round_radius} ${center_y}
|
||||
L ${isRight ? end_x - round_radius : end_x + round_radius} ${center_y}
|
||||
A ${round_radius} ${round_radius} 0 0 ${isRight ^ !isDown ? 1 : 0} ${end_x} ${isDown ? center_y + round_radius : center_y - round_radius}
|
||||
L ${end_x} ${end_y}`;
|
||||
}
|
||||
|
||||
// Define nodes with HTML content
|
||||
const customerSignUp: string =
|
||||
`<div class="customer-sign-up node" data-node="customerSignsUpNode"><p><i class="fa-regular fa-circle-play"></i> Customer signs up for product updates</p></div>`;
|
||||
const sendEmail: string =
|
||||
`<div class="send-email node" data-node="sendEmailNode"><p><i class="fa-regular fa-envelope"></i> Send email</p></div>`;
|
||||
const replyCheck: string =
|
||||
`<div class="reply-check node" data-node="repliedToConversationNode"><p><i class="fa-solid fa-shuffle"></i> Replied to conversation?</p></div>`
|
||||
;
|
||||
const contactExits: string =
|
||||
`<div class="contact-exits node" data-node="contactExitsNode"><i class="fa-regular fa-circle-check"></i><p>Contact Exits</p></div>`
|
||||
;
|
||||
const sendSurvey: string =
|
||||
`<div class="send-survey node" data-node="sendSurveyNode"><p><i class="fa-solid fa-list-check"></i> Send survey</p></div>`
|
||||
;
|
||||
const btnAddNote: string = `<a href="javascript:void(0)" onclick="ClickAddNote()" class="add-note">Thêm <i class="fa-solid fa-plus"></i></a>`;
|
||||
|
||||
// Add nodes to the editor
|
||||
const customerSignsUpNode = editor.addNode('customerSignUp', 0, 1, 200, 50, 'customer-sign-up', {}, customerSignUp);
|
||||
const btn_add_note = editor.addNode('add-note', 1, 1, 200, 175, 'add-note', {}, btnAddNote);
|
||||
// const sendEmailNode = editor.addNode('sendEmail', 1, 1, 400, 175, 'send-email', {}, sendEmail.html);
|
||||
// const repliedToConversationNode = editor.addNode('replyCheck', 1, 2, 400, 300, 'reply-check', {}, replyCheck.html);
|
||||
// const contactExitsNode = editor.addNode('contactExits', 1, 0, 200, 450, 'contact-exits', {}, contactExits.html);
|
||||
// const sendSurveyNode = editor.addNode('sendSurvey', 1, 0, 600, 450, 'send-survey', {}, sendSurvey.html);
|
||||
|
||||
editor.addConnection(customerSignsUpNode, btn_add_note, 'output_1', 'input_1');
|
||||
// editor.addConnection(sendEmailNode, repliedToConversationNode, 'output_1', 'input_1');
|
||||
// editor.addConnection(repliedToConversationNode, contactExitsNode, 'output_1', 'input_1'); // Đường "Yes"
|
||||
// editor.addConnection(repliedToConversationNode, sendSurveyNode, 'output_2', 'input_1'); // Đường "No"
|
||||
|
||||
// Events!
|
||||
|
||||
/* DRAG EVENT */
|
||||
|
||||
// Drag and drop functionality
|
||||
document.getElementById('drawflow')?.addEventListener('drop', drop);
|
||||
document.getElementById('drawflow')?.addEventListener('dragover', allowDrop);
|
||||
|
||||
function allowDrop(ev: DragEvent): void {
|
||||
ev.preventDefault();
|
||||
}
|
||||
|
||||
function drag(ev: DragEvent): void {
|
||||
ev.dataTransfer?.setData("node", (ev.target as HTMLElement).getAttribute('data-node') as string);
|
||||
}
|
||||
|
||||
function drop(ev: DragEvent): void {
|
||||
ev.preventDefault();
|
||||
const nodeName = ev.dataTransfer?.getData("node") as string;
|
||||
const x = ev.clientX - (ev.target as HTMLElement).getBoundingClientRect().left;
|
||||
const y = ev.clientY - (ev.target as HTMLElement).getBoundingClientRect().top;
|
||||
addNodeToDrawFlow(nodeName, x, y);
|
||||
}
|
||||
|
||||
function addNodeToDrawFlow(element: HTMLElement): void {
|
||||
// Ẩn popup và xóa node hiện tại
|
||||
const popup = document.getElementById('popup-drawflow') as HTMLElement;
|
||||
popup.classList.add('hidden');
|
||||
const id_popup = popup.getAttribute('data-id') as string;
|
||||
editor.removeNodeId(id_popup);
|
||||
editor.updateConnectionNodes(id_popup);
|
||||
|
||||
const pos_x = parseInt(element.getAttribute('data-pos_x') as string);
|
||||
const pos_y = parseInt(element.getAttribute('data-pos_y') as string);
|
||||
const nodeName = element.getAttribute('data-name') as string;
|
||||
const output = parseInt(element.getAttribute('data-out') as string);
|
||||
const input = parseInt(element.getAttribute('data-input') as string);
|
||||
const className = element.getAttribute('data-class') as string;
|
||||
const parent = element.getAttribute('data-parent') as string;
|
||||
|
||||
const nodeHtml = document.querySelector(`.drag-drawflow[data-name="${nodeName}"]`) as HTMLElement;
|
||||
const contenthtml = nodeHtml.innerHTML;
|
||||
|
||||
const newNodeId = editor.addNode(nodeName, input, output, pos_x, pos_y, className, {}, contenthtml);
|
||||
|
||||
// Kết nối node mới với node cuối cùng nếu tồn tại
|
||||
if (lastLargeNodeId) {
|
||||
if (nodeName === 'sendSurvey') {
|
||||
editor.addConnection(lastLargeNodeId, newNodeId, 'output_2', 'input_1'); // Kết nối với output_2 của sendSurvey
|
||||
addLabelToConnection(lastLargeNodeId, newNodeId, 'No', 2);
|
||||
} else {
|
||||
editor.addConnection(lastLargeNodeId, newNodeId, 'output_1', 'input_1'); // Kết nối với output_1 của node trước đó
|
||||
addLabelToConnection(lastLargeNodeId, newNodeId, 'Yes', 1);
|
||||
}
|
||||
} else if (lastCreatedNodeId) {
|
||||
editor.addConnection(lastCreatedNodeId, newNodeId, 'output_1', 'input_1'); // Kết nối với output_1 của node trước đó
|
||||
}
|
||||
|
||||
// Cập nhật node cuối cùng được tạo
|
||||
if (output === 2) {
|
||||
// Lưu node lớn (có Yes/No)
|
||||
lastLargeNodeId = newNodeId;
|
||||
} else {
|
||||
// Xóa lưu trữ khi không phải node lớn
|
||||
lastCreatedNodeId = newNodeId;
|
||||
}
|
||||
|
||||
// Loại bỏ nhãn dán cũ nếu có
|
||||
if (nodeName === 'sendSurvey' || nodeName === 'contactExits') {
|
||||
const boxLabel = document.querySelector(`.connection-label`);
|
||||
if (boxLabel) {
|
||||
boxLabel.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Thêm nút "Add Note" nếu cần
|
||||
const new_pos_y = pos_y + 130;
|
||||
if (output === 2) {
|
||||
const btn_add_yes = editor.addNode('add-note', 1, 0, 25, 450, 'add-note', {}, btnAddNote);
|
||||
const btn_add_no = editor.addNode('add-note', 1, 0, 400, 450, 'add-note', {}, btnAddNote);
|
||||
|
||||
editor.addConnection(newNodeId, btn_add_yes, 'output_1', 'input_1');
|
||||
editor.addConnection(newNodeId, btn_add_no, 'output_2', 'input_1');
|
||||
|
||||
addLabelToConnection(newNodeId, btn_add_yes, 'Yes', 1);
|
||||
addLabelToConnection(newNodeId, btn_add_no, 'No', 2);
|
||||
|
||||
} else if (output === 0 && parent === 'replyCheck') {
|
||||
// Do nothing in this case
|
||||
} else {
|
||||
const btn_add_note = editor.addNode('add-note', input, output, pos_x, new_pos_y, 'add-note', {}, btnAddNote);
|
||||
editor.addConnection(newNodeId, btn_add_note, 'output_1', 'input_1');
|
||||
}
|
||||
}
|
||||
|
||||
function ClickAddNote(): void {
|
||||
const btn = document.querySelector('.drawflow-node.add-note') as HTMLElement;
|
||||
|
||||
btn.addEventListener('dblclick', (event) => {
|
||||
const popup = document.getElementById('popup-drawflow') as HTMLElement;
|
||||
popup.classList.remove('hidden');
|
||||
|
||||
// Lấy thuộc tính CSS của phần tử
|
||||
const computedStyle = window.getComputedStyle(btn);
|
||||
const top = computedStyle.getPropertyValue('top');
|
||||
const left = computedStyle.getPropertyValue('left');
|
||||
const new_left = parseFloat(left) + 690;
|
||||
|
||||
popup.style.top = top;
|
||||
popup.style.left = `${new_left}px`;
|
||||
|
||||
const id = btn.getAttribute('id') as string;
|
||||
popup.setAttribute('data-id', id);
|
||||
});
|
||||
}
|
||||
|
||||
// Function to add labels to connections
|
||||
function addLabelToConnection(nodeFrom: number, nodeTo: number, labelText: string, outputIndex: number): void {
|
||||
// Lấy phần tử SVG và đường kẻ kết nối
|
||||
const connectionElement = document.querySelector(`.connection.node_in_node-${nodeTo}.node_out_node-${nodeFrom}.output_${outputIndex}`) as HTMLElement;
|
||||
|
||||
const svg = connectionElement.closest('svg') as SVGElement;
|
||||
const path = svg?.querySelector('path') as SVGPathElement;
|
||||
if (!path) return console.error('Path element not found in SVG');
|
||||
|
||||
// Tạo và thêm nhãn
|
||||
const label = document.createElement('div');
|
||||
label.className = `connection-label`;
|
||||
label.id = `label-${nodeTo}`;
|
||||
label.textContent = labelText;
|
||||
document.body.appendChild(label);
|
||||
|
||||
if (labelText === 'No') {
|
||||
label.classList.add('no');
|
||||
}
|
||||
|
||||
// Cập nhật vị trí nhãn
|
||||
const updateLabelPosition = () => {
|
||||
const connectionBox = connectionElement.getBoundingClientRect();
|
||||
const length = path.getTotalLength();
|
||||
const { x, y } = path.getPointAtLength(length / 2);
|
||||
|
||||
const svgPoint = svg.createSVGPoint();
|
||||
svgPoint.x = x;
|
||||
svgPoint.y = y;
|
||||
const screenPoint = svgPoint.matrixTransform(svg.getScreenCTM() as DOMMatrix);
|
||||
|
||||
label.style.position = 'absolute';
|
||||
label.style.left = `${screenPoint.x}px`;
|
||||
label.style.top = `${screenPoint.y - 12}px`;
|
||||
};
|
||||
|
||||
updateLabelPosition();
|
||||
|
||||
editor.on('zoom', updateLabelPosition);
|
||||
window.addEventListener('resize', updateLabelPosition);
|
||||
window.addEventListener('mouseup', updateLabelPosition);
|
||||
window.addEventListener('mousemove', updateLabelPosition);
|
||||
}
|
||||
|
||||
// Drawflow Events
|
||||
editor.on('connectionCreated', function (connection) {
|
||||
console.log('Connection created', connection);
|
||||
});
|
||||
|
||||
editor.on('connectionRemoved', function (connection) {
|
||||
console.log('Connection removed', connection);
|
||||
});
|
||||
|
||||
editor.on('nodeSelected', function (connection) {
|
||||
// Add click events to nodes
|
||||
addClickEventsToParentNodes();
|
||||
});
|
||||
|
||||
const btn_clear = document.getElementById('btn-clear') as HTMLElement;
|
||||
|
||||
btn_clear.addEventListener('click', () => {
|
||||
const connectionLabels = document.querySelectorAll('.connection-label');
|
||||
connectionLabels.forEach(label => {
|
||||
label.classList.add('hide');
|
||||
});
|
||||
});
|
||||
241
src/style.css
Normal file
241
src/style.css
Normal file
@@ -0,0 +1,241 @@
|
||||
:root {
|
||||
--border-color: #cacaca;
|
||||
--background-color: #ffffff;
|
||||
--background-box-title: #f7f7f7;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0px;
|
||||
padding: 0px;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
font-family: "Roboto", sans-serif;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
margin-top: 50px;
|
||||
width: 100%;
|
||||
height: calc(100vh - 67px);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.col {
|
||||
overflow: auto;
|
||||
width: 350px;
|
||||
height: 100%;
|
||||
border-right: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
#drawflow {
|
||||
position: relative;
|
||||
width: calc(100vw - 301px);
|
||||
height: calc(100% - 50px);
|
||||
top: 0;
|
||||
background: var(--background-color);
|
||||
background-size: 25px 25px;
|
||||
background-image: linear-gradient(to right, #f1f1f1 1px, transparent 1px),
|
||||
linear-gradient(to bottom, #f1f1f1 1px, transparent 1px);
|
||||
}
|
||||
|
||||
.parent-drawflow {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
touch-action: none;
|
||||
outline: 0;
|
||||
}
|
||||
|
||||
.drawflow-node {
|
||||
padding: 10px;
|
||||
border: 1px solid #c3c3c3;
|
||||
background: #fff !important;
|
||||
text-align: center;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.drawflow-node p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.drawflow-node .title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.drag-drawflow {
|
||||
line-height: 50px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding-left: 20px;
|
||||
cursor: move;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.drawflow .drawflow-connection {
|
||||
stroke: #000;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.drawflow .drawflow-connection .main-path {
|
||||
fill: none;
|
||||
stroke: #000;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.drawflow .drawflow-connection .main-path.replied-yes {
|
||||
stroke: #4caf50;
|
||||
}
|
||||
|
||||
.drawflow .drawflow-connection .main-path.replied-no {
|
||||
stroke: #f44336;
|
||||
}
|
||||
|
||||
.drawflow .drawflow-node:hover {
|
||||
box-shadow: 0 0 11px 0px #393939b3;
|
||||
}
|
||||
|
||||
.drawflow .drawflow-node .inputs,
|
||||
.drawflow .drawflow-node .outputs {
|
||||
display: flex;
|
||||
width: auto;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.drawflow .drawflow-node .output {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: rgb(255, 255, 255);
|
||||
border-radius: 50%;
|
||||
color: #8e8e91;
|
||||
border: 2px solid #8e8e91;
|
||||
cursor: crosshair;
|
||||
z-index: 1;
|
||||
top: 24px;
|
||||
right: 0px;
|
||||
}
|
||||
|
||||
.drawflow .drawflow-node .input {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
background: #8e8e91;
|
||||
border-radius: 50%;
|
||||
color: #8e8e91;
|
||||
border: 2px solid #8e8e91;
|
||||
cursor: crosshair;
|
||||
z-index: 1;
|
||||
top: -24px;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.drawflow .drawflow-node {
|
||||
background-color: rgb(255, 255, 255);
|
||||
border-color: #8e8e91;
|
||||
border-radius: 10px;
|
||||
border-style: solid;
|
||||
border-width: 2px;
|
||||
height: 54px;
|
||||
width: 280px;
|
||||
display: block;
|
||||
}
|
||||
.connection-label {
|
||||
padding: 5px;
|
||||
border-radius: 5px;
|
||||
background: #dceccf;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.connection-label.no {
|
||||
background: #f8d0b5;
|
||||
}
|
||||
|
||||
/* Popup Styling */
|
||||
.popup {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.popup.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.popup-content {
|
||||
background: #fff;
|
||||
padding: 20px;
|
||||
border-radius: 5px;
|
||||
width: 100%;
|
||||
max-width: 600px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.popup-close {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
form label {
|
||||
font-weight: bold;
|
||||
margin-bottom: 5px;
|
||||
color: #333;
|
||||
display: block;
|
||||
}
|
||||
|
||||
form input,
|
||||
form textarea,
|
||||
form select {
|
||||
padding: 10px;
|
||||
margin-bottom: 15px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
width: 97%;
|
||||
}
|
||||
|
||||
form textarea {
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
form button {
|
||||
padding: 10px 15px;
|
||||
background-color: #007bff;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
form button:hover {
|
||||
background-color: #0056b3;
|
||||
}
|
||||
|
||||
form .form-group {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
form .form-group:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
#popup-form-fields {
|
||||
width: 99%;
|
||||
padding: 10px 0;
|
||||
}
|
||||
1
src/vite-env.d.ts
vendored
Normal file
1
src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
Reference in New Issue
Block a user