commit 43809e42a9988ccf81cca8fc18d97f16693e5b93 Author: Tieptk Date: Thu Aug 1 14:24:58 2024 +0700 up diff --git a/dist/index.html b/dist/index.html new file mode 100644 index 0000000..7531953 --- /dev/null +++ b/dist/index.html @@ -0,0 +1,93 @@ + + + + + + + Customer sign email + + + + + + + +
+
+
+ +
+
+ Send email +
+
+ Replied to conversation? +
+
+ Contact Exits +
+
+ Send survey +
+
+
+ +
Clear
+
+ + + + + + + + + + + + \ No newline at end of file diff --git a/dist/main.css b/dist/main.css new file mode 100644 index 0000000..e92c228 --- /dev/null +++ b/dist/main.css @@ -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; +} diff --git a/dist/main.js b/dist/main.js new file mode 100644 index 0000000..b885fbf --- /dev/null +++ b/dist/main.js @@ -0,0 +1,258 @@ +"use strict"; +let lastCreatedNodeId = 1; +let lastLargeNodeId = null; +// Define node content mappings +const nodeContents = { + 'sendEmailNode': ` + + + + + `, + 'customerSignsUpNode': ` + + + + + `, + 'repliedToConversationNode': ` + + + `, + 'contactExitsNode': ` + + + `, + 'sendSurveyNode': ` + + + + + ` +}; +// 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 = `

Customer signs up for product updates

`; +const sendEmail = `

Send email

`; +const replyCheck = `

Replied to conversation?

`; +const contactExits = `

Contact Exits

`; +const sendSurvey = `

Send survey

`; +const btnAddNote = `Thêm `; +// 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'); + }); +}); diff --git a/dist/vite.svg b/dist/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/dist/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..86f2fe1 --- /dev/null +++ b/index.html @@ -0,0 +1,93 @@ + + + + + + + Customer sign email + + + + + + + +
+
+
+ +
+
+ Send email +
+
+ Replied to conversation? +
+
+ Contact Exits +
+
+ Send survey +
+
+
+ +
Clear
+
+ + + + + + + + + + + + \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..ed86a00 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..1d75581 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,307 @@ +let lastCreatedNodeId: number = 1; +let lastLargeNodeId: number | null = null; + +// Define node content mappings +const nodeContents: { [key: string]: string } = { + 'sendEmailNode': ` + + + + + `, + 'customerSignsUpNode': ` + + + + + `, + 'repliedToConversationNode': ` + + + `, + 'contactExitsNode': ` + + + `, + 'sendSurveyNode': ` + + + + + ` +}; + +// 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 = + `

Customer signs up for product updates

`; +const sendEmail: string = + `

Send email

`; +const replyCheck: string = + `

Replied to conversation?

` + ; +const contactExits: string = + `

Contact Exits

` + ; +const sendSurvey: string = + `

Send survey

` + ; +const btnAddNote: string = `Thêm `; + +// 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'); + }); +}); diff --git a/src/style.css b/src/style.css new file mode 100644 index 0000000..7ea975a --- /dev/null +++ b/src/style.css @@ -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; +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..eb7f9d1 --- /dev/null +++ b/tsconfig.json @@ -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 +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..29c19e4 --- /dev/null +++ b/vite.config.ts @@ -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]' + } + } + } +});