This commit is contained in:
2024-08-01 14:24:58 +07:00
commit 43809e42a9
11 changed files with 1356 additions and 0 deletions

93
dist/index.html vendored Normal file
View File

@@ -0,0 +1,93 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Customer sign email</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/jerosoler/Drawflow/dist/drawflow.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" />
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="wrapper">
<div class="col">
<div class="drag-drawflow" draggable="true" ondragstart="drag(event)" data-node="customerSignUp">
<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>
</div>
<div class="drag-drawflow" draggable="true" ondragstart="drag(event)" data-node="sendEmail">
<i class="fa-regular fa-envelope"></i> Send email
</div>
<div class="drag-drawflow" draggable="true" ondragstart="drag(event)" data-node="replyCheck">
<i class="fa-solid fa-shuffle"></i> Replied to conversation?
</div>
<div class="drag-drawflow" draggable="true" ondragstart="drag(event)" data-node="contactExits">
<i class="fa-regular fa-circle-check"></i> Contact Exits
</div>
<div class="drag-drawflow" draggable="true" ondragstart="drag(event)" data-node="sendSurvey">
<i class="fa-solid fa-list-check"></i> Send survey
</div>
</div>
<div id="drawflow"></div>
<div class="btn-clear" id="btn-clear" onclick="editor.clearModuleSelected()">Clear</div>
</div>
<div id="popup-drawflow" class="hidden">
<div class="popup-content">
<span class="popup-close">&times;</span>
<div id="popup-drawflow-content">
<!-- Form fields will be dynamically added here -->
<div class="drag-drawflow" data-pos_x="200" data-pos_y="175" data-out="1" data-input="1" data-name="sendEmail"
data-class="send-email" data-html="sendEmail" data-parent="customerSignsUpNode"
onclick="addNodeToDrawFlow(this)">
<div class="send-email node" data-node="sendEmailNode">
<p><i class="fa-regular fa-envelope"></i> Send email</p>
</div>
</div>
<div class="drag-drawflow" data-pos_x="200" data-pos_y="300" data-out="2" data-input="1" data-name="replyCheck"
data-class="reply-check" data-html="replyCheck" data-parent="sendEmail" onclick="addNodeToDrawFlow(this)">
<div class="reply-check node" data-node="repliedToConversationNode">
<p><i class="fa-solid fa-shuffle"></i> Replied to conversation?</p>
</div>
</div>
<div class="drag-drawflow" data-pos_x="25" data-pos_y="450" data-out="0" data-input="1" data-name="contactExits"
data-class="contact-exits" data-html="contactExits" data-parent="replyCheck"
onclick="addNodeToDrawFlow(this)">
<div class="contact-exits node" data-node="contactExitsNode">
<p><i class="fa-regular fa-circle-check"></i>
Contact Exits</p>
</div>
</div>
<div class="drag-drawflow" data-pos_x="400" data-pos_y="450" data-out="0" data-input="1" data-name="sendSurvey"
data-class="send-survey" data-html="sendSurvey" data-parent="replyCheck" onclick="addNodeToDrawFlow(this)">
<div class="send-survey node" data-node="sendSurveyNode">
<p><i class="fa-solid fa-list-check"></i> Send survey</p>
</div>
</div>
</div>
</div>
</div>
<!-- Popup Form HTML -->
<div id="popup-form" class="popup global-popup hidden">
<div class="popup-content">
<span class="popup-close">&times;</span>
<div id="popup-form-content" class="conent-form">
<!-- Form fields will be dynamically added here -->
<div id="popup-form-fields"></div>
<button type="submit">Submit</button>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/gh/jerosoler/Drawflow/dist/drawflow.min.js"></script>
<script src="script.js"></script>
</body>
</html>

292
dist/main.css vendored Normal file
View File

@@ -0,0 +1,292 @@
: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;
}
.conent-form {
display: flex;
flex-direction: column;
}
.conent-form label {
font-weight: bold;
margin-bottom: 5px;
color: #333;
display: block;
}
.conent-form input,
.conent-form textarea,
.conent-form select {
padding: 10px;
margin-bottom: 15px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
width: 97%;
}
.conent-form textarea {
resize: vertical;
}
.conent-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;
}
.conent-form button:hover {
background-color: #0056b3;
}
.conent-form .form-group {
margin-bottom: 15px;
}
.conent-form .form-group:last-child {
margin-bottom: 0;
}
#popup-form-fields {
width: 99%;
padding: 10px 0;
}
.btn-clear {
float: right;
position: absolute;
top: 10px;
right: 85px;
color: white;
font-weight: bold;
border: 1px solid #96015b;
background: #e3195a;
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
z-index: 5;
}
.connection-label.hide {
display: none;
}
.drawflow-node .add-note {
color: #000;
text-decoration: none;
}
#popup-drawflow {
position: absolute;
display: block;
z-index: 9999;
}
#popup-drawflow .popup-content {
box-shadow: 0 0 7px 0px #000;
}
#popup-drawflow.hidden {
display: none;
}
#popup-drawflow .drag-drawflow {
padding-left: 0;
}
#popup-drawflow .drag-drawflow p {
display: flex;
align-items: center;
margin: 0;
}
#popup-drawflow .drag-drawflow i {
margin-right: 10px;
}

258
dist/main.js vendored Normal file
View File

@@ -0,0 +1,258 @@
"use strict";
let lastCreatedNodeId = 1;
let lastLargeNodeId = null;
// Define node content mappings
const nodeContents = {
'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) {
const popup = document.getElementById('popup-form');
const nodeInfoElement = document.getElementById('popup-form-fields');
const closeBtn = document.querySelector('.popup-close');
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() {
document.querySelectorAll('.parent-node').forEach(parentNode => {
parentNode.addEventListener('dblclick', (event) => {
const drawflowNode = parentNode.querySelector('.node');
const nodeId = drawflowNode.getAttribute('data-node');
console.log(parentNode);
if (nodeId) {
showPopup(nodeId);
}
});
});
}
// Initialize Drawflow editor
var editor = new Drawflow(document.getElementById('drawflow'));
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 = `<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 = `<div class="send-email node" data-node="sendEmailNode"><p><i class="fa-regular fa-envelope"></i> Send email</p></div>`;
const replyCheck = `<div class="reply-check node" data-node="repliedToConversationNode"><p><i class="fa-solid fa-shuffle"></i> Replied to conversation?</p></div>`;
const contactExits = `<div class="contact-exits node" data-node="contactExitsNode"><i class="fa-regular fa-circle-check"></i><p>Contact Exits</p></div>`;
const sendSurvey = `<div class="send-survey node" data-node="sendSurveyNode"><p><i class="fa-solid fa-list-check"></i> Send survey</p></div>`;
const btnAddNote = `<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) {
ev.preventDefault();
}
function drag(ev) {
ev.dataTransfer?.setData("node", ev.target.getAttribute('data-node'));
}
function drop(ev) {
ev.preventDefault();
const nodeName = ev.dataTransfer?.getData("node");
const x = ev.clientX - ev.target.getBoundingClientRect().left;
const y = ev.clientY - ev.target.getBoundingClientRect().top;
addNodeToDrawFlow(nodeName, x, y);
}
function addNodeToDrawFlow(element) {
// Ẩn popup và xóa node hiện tại
const popup = document.getElementById('popup-drawflow');
popup.classList.add('hidden');
const id_popup = popup.getAttribute('data-id');
editor.removeNodeId(id_popup);
editor.updateConnectionNodes(id_popup);
const pos_x = parseInt(element.getAttribute('data-pos_x'));
const pos_y = parseInt(element.getAttribute('data-pos_y'));
const nodeName = element.getAttribute('data-name');
const output = parseInt(element.getAttribute('data-out'));
const input = parseInt(element.getAttribute('data-input'));
const className = element.getAttribute('data-class');
const parent = element.getAttribute('data-parent');
const nodeHtml = document.querySelector(`.drag-drawflow[data-name="${nodeName}"]`);
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() {
const btn = document.querySelector('.drawflow-node.add-note');
btn.addEventListener('dblclick', (event) => {
const popup = document.getElementById('popup-drawflow');
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');
popup.setAttribute('data-id', id);
});
}
// Function to add labels to connections
function addLabelToConnection(nodeFrom, nodeTo, labelText, outputIndex) {
// 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}`);
const svg = connectionElement.closest('svg');
const path = svg?.querySelector('path');
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());
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');
btn_clear.addEventListener('click', () => {
const connectionLabels = document.querySelectorAll('.connection-label');
connectionLabels.forEach(label => {
label.classList.add('hide');
});
});

1
dist/vite.svg vendored Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

93
index.html Normal file
View File

@@ -0,0 +1,93 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Customer sign email</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/jerosoler/Drawflow/dist/drawflow.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" />
<link rel="stylesheet" href="./dist/main.css">
</head>
<body>
<div class="wrapper">
<div class="col">
<div class="drag-drawflow" draggable="true" ondragstart="drag(event)" data-node="customerSignUp">
<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>
</div>
<div class="drag-drawflow" draggable="true" ondragstart="drag(event)" data-node="sendEmail">
<i class="fa-regular fa-envelope"></i> Send email
</div>
<div class="drag-drawflow" draggable="true" ondragstart="drag(event)" data-node="replyCheck">
<i class="fa-solid fa-shuffle"></i> Replied to conversation?
</div>
<div class="drag-drawflow" draggable="true" ondragstart="drag(event)" data-node="contactExits">
<i class="fa-regular fa-circle-check"></i> Contact Exits
</div>
<div class="drag-drawflow" draggable="true" ondragstart="drag(event)" data-node="sendSurvey">
<i class="fa-solid fa-list-check"></i> Send survey
</div>
</div>
<div id="drawflow"></div>
<div class="btn-clear" id="btn-clear" onclick="editor.clearModuleSelected()">Clear</div>
</div>
<div id="popup-drawflow" class="hidden">
<div class="popup-content">
<span class="popup-close">&times;</span>
<div id="popup-drawflow-content">
<!-- Form fields will be dynamically added here -->
<div class="drag-drawflow" data-pos_x="200" data-pos_y="175" data-out="1" data-input="1" data-name="sendEmail"
data-class="send-email" data-html="sendEmail" data-parent="customerSignsUpNode"
onclick="addNodeToDrawFlow(this)">
<div class="send-email node" data-node="sendEmailNode">
<p><i class="fa-regular fa-envelope"></i> Send email</p>
</div>
</div>
<div class="drag-drawflow" data-pos_x="200" data-pos_y="300" data-out="2" data-input="1" data-name="replyCheck"
data-class="reply-check" data-html="replyCheck" data-parent="sendEmail" onclick="addNodeToDrawFlow(this)">
<div class="reply-check node" data-node="repliedToConversationNode">
<p><i class="fa-solid fa-shuffle"></i> Replied to conversation?</p>
</div>
</div>
<div class="drag-drawflow" data-pos_x="25" data-pos_y="450" data-out="0" data-input="1" data-name="contactExits"
data-class="contact-exits" data-html="contactExits" data-parent="replyCheck"
onclick="addNodeToDrawFlow(this)">
<div class="contact-exits node" data-node="contactExitsNode">
<p><i class="fa-regular fa-circle-check"></i>
Contact Exits</p>
</div>
</div>
<div class="drag-drawflow" data-pos_x="400" data-pos_y="450" data-out="0" data-input="1" data-name="sendSurvey"
data-class="send-survey" data-html="sendSurvey" data-parent="replyCheck" onclick="addNodeToDrawFlow(this)">
<div class="send-survey node" data-node="sendSurveyNode">
<p><i class="fa-solid fa-list-check"></i> Send survey</p>
</div>
</div>
</div>
</div>
</div>
<!-- Popup Form HTML -->
<div id="popup-form" class="popup global-popup hidden">
<div class="popup-content">
<span class="popup-close">&times;</span>
<div id="popup-form-content" class="conent-form">
<!-- Form fields will be dynamically added here -->
<div id="popup-form-fields"></div>
<button type="submit">Submit</button>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/gh/jerosoler/Drawflow/dist/drawflow.min.js"></script>
<script src="./dist/main.js"></script>
</body>
</html>

32
package.json Normal file
View File

@@ -0,0 +1,32 @@
{
"name": "customer_signs",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"serve": "vite preview",
"lint": "eslint . --ext .ts",
"format": "prettier --write ."
},
"devDependencies": {
"@types/drawflow": "^0.0.12",
"@typescript-eslint/eslint-plugin": "^7.17.0",
"@typescript-eslint/parser": "^7.17.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"prettier": "^3.3.3",
"ts-loader": "^9.5.1",
"typescript": "^5.5.4",
"vite": "^5.3.4",
"vite-tsconfig-paths": "^4.3.2",
"webpack": "^5.93.0",
"webpack-cli": "^5.1.4"
},
"dependencies": {
"@vitejs/plugin-vue": "^5.1.0",
"drawflow": "^0.0.59"
}
}

307
src/main.ts Normal file
View 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
View 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
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

19
tsconfig.json Normal file
View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "esnext", // Chỉ định phiên bản ECMAScript mục tiêu
"module": "esnext", // Sử dụng mô-đun ES cho đầu ra
"moduleResolution": "node", // Quyết định cách TypeScript tìm các mô-đun
"strict": true, // Bật tất cả các kiểm tra kiểu nghiêm ngặt
"esModuleInterop": true, // Đảm bảo tương thích với các mô-đun ES6
"skipLibCheck": true, // Bỏ qua kiểm tra loại trong các tệp thư viện
"forceConsistentCasingInFileNames": true, // Buộc sự nhất quán trong việc phân biệt chữ hoa chữ thường trong tên tệp
"baseUrl": ".", // Cấu hình đường dẫn cơ sở để tìm mô-đun
"paths": {
"@/*": ["src/*"] // Định nghĩa các đường dẫn alias
},
"outDir": "./dist", // Thư mục đầu ra cho các tệp JavaScript đã biên dịch
"rootDir": "./src" // Thư mục gốc chứa các tệp nguồn TypeScript
},
"include": ["src/**/*.ts", "src/**/*.tsx"], // Các tệp cần biên dịch
"exclude": ["node_modules", "dist"] // Các thư mục cần loại bỏ khỏi biên dịch
}

19
vite.config.ts Normal file
View File

@@ -0,0 +1,19 @@
import { defineConfig } from 'vite';
import { resolve } from 'path';
export default defineConfig({
build: {
outDir: 'dist',
minify: false,
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html')
},
output: {
entryFileNames: 'assets/[name].js',
chunkFileNames: 'assets/[name].js',
assetFileNames: 'assets/[name].[ext]'
}
}
}
});