dbdoc_engine 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/README.md +331 -0
- data/Rakefile +8 -0
- data/app/assets/builds/dbdoc_engine/application.css +5 -0
- data/app/assets/images/dbdoc_engine/arrowdown.svg +3 -0
- data/app/assets/images/dbdoc_engine/arrowhorizontal.svg +3 -0
- data/app/assets/images/dbdoc_engine/arrowleft.svg +3 -0
- data/app/assets/images/dbdoc_engine/changelog.svg +3 -0
- data/app/assets/images/dbdoc_engine/column_stats_dbdocs.svg +23 -0
- data/app/assets/images/dbdoc_engine/diagram.svg +3 -0
- data/app/assets/images/dbdoc_engine/double_arrow.svg +4 -0
- data/app/assets/images/dbdoc_engine/group_bu.svg +3 -0
- data/app/assets/images/dbdoc_engine/japan_circle.png +0 -0
- data/app/assets/images/dbdoc_engine/log_in_image.png +0 -0
- data/app/assets/images/dbdoc_engine/logo.svg +12 -0
- data/app/assets/images/dbdoc_engine/orange_changelog.svg +3 -0
- data/app/assets/images/dbdoc_engine/orange_fields.svg +23 -0
- data/app/assets/images/dbdoc_engine/orange_logo.svg +12 -0
- data/app/assets/images/dbdoc_engine/orange_table.svg +21 -0
- data/app/assets/images/dbdoc_engine/orange_updates.svg +43 -0
- data/app/assets/images/dbdoc_engine/orange_wiki.svg +3 -0
- data/app/assets/images/dbdoc_engine/search.svg +3 -0
- data/app/assets/images/dbdoc_engine/setting.svg +3 -0
- data/app/assets/images/dbdoc_engine/table_dbdocs.svg +21 -0
- data/app/assets/images/dbdoc_engine/uk_circle_transparent.png +0 -0
- data/app/assets/images/dbdoc_engine/update_stats_dbdocs.svg +43 -0
- data/app/assets/images/dbdoc_engine/wiki.svg +3 -0
- data/app/assets/stylesheets/dbdoc_engine/admin.css +176 -0
- data/app/assets/stylesheets/dbdoc_engine/admin_header.css +179 -0
- data/app/assets/stylesheets/dbdoc_engine/application.scss +1 -0
- data/app/assets/stylesheets/dbdoc_engine/changelog.css +173 -0
- data/app/assets/stylesheets/dbdoc_engine/dashboard.css +513 -0
- data/app/assets/stylesheets/dbdoc_engine/dbdoc_application.css +117 -0
- data/app/assets/stylesheets/dbdoc_engine/ecommerce.css +253 -0
- data/app/assets/stylesheets/dbdoc_engine/group_details.css +178 -0
- data/app/assets/stylesheets/dbdoc_engine/header.css +212 -0
- data/app/assets/stylesheets/dbdoc_engine/loading_spinner.css +127 -0
- data/app/assets/stylesheets/dbdoc_engine/login.css +213 -0
- data/app/assets/stylesheets/dbdoc_engine/schema_diagram.css +149 -0
- data/app/assets/stylesheets/dbdoc_engine/sidebar.css +296 -0
- data/app/assets/stylesheets/dbdoc_engine/table_details.css +417 -0
- data/app/controllers/dbdoc_engine/admin/base_controller.rb +23 -0
- data/app/controllers/dbdoc_engine/admin/dashboard_controller.rb +16 -0
- data/app/controllers/dbdoc_engine/admin/data_transfer_controller.rb +63 -0
- data/app/controllers/dbdoc_engine/admin/db_design_dynamic_tables_controller.rb +198 -0
- data/app/controllers/dbdoc_engine/admin/db_design_table_groups_controller.rb +107 -0
- data/app/controllers/dbdoc_engine/application_controller.rb +65 -0
- data/app/controllers/dbdoc_engine/concerns/internationalization.rb +57 -0
- data/app/controllers/dbdoc_engine/db_doc_sessions_controller.rb +33 -0
- data/app/controllers/dbdoc_engine/home_controller.rb +79 -0
- data/app/controllers/dbdoc_engine/schema_diagram_controller.rb +293 -0
- data/app/helper/dbdoc_engine/application_helper.rb +35 -0
- data/app/helpers/dbdoc_engine/application_helper.rb +4 -0
- data/app/helpers/dbdoc_engine/changelogs_helper.rb +27 -0
- data/app/helpers/dbdoc_engine/column_helper.rb +30 -0
- data/app/helpers/dbdoc_engine/db_design_dynamic_tables_helper.rb +15 -0
- data/app/helpers/dbdoc_engine/home_helper.rb +75 -0
- data/app/javascript/dbdoc_engine/application.js +12 -0
- data/app/javascript/dbdoc_engine/controllers/application.js +29 -0
- data/app/javascript/dbdoc_engine/controllers/auto_submit_controller.js +17 -0
- data/app/javascript/dbdoc_engine/controllers/chart_controller.js +58 -0
- data/app/javascript/dbdoc_engine/controllers/column-type_controller.js +149 -0
- data/app/javascript/dbdoc_engine/controllers/column_controller.js +362 -0
- data/app/javascript/dbdoc_engine/controllers/column_search_controller.js +42 -0
- data/app/javascript/dbdoc_engine/controllers/dbdoc_accordion_controller.js +42 -0
- data/app/javascript/dbdoc_engine/controllers/ecommerce_controller.js +73 -0
- data/app/javascript/dbdoc_engine/controllers/group_details_controller.js +88 -0
- data/app/javascript/dbdoc_engine/controllers/import_export_controller.js +200 -0
- data/app/javascript/dbdoc_engine/controllers/index.js +9 -0
- data/app/javascript/dbdoc_engine/controllers/language_controller.js +100 -0
- data/app/javascript/dbdoc_engine/controllers/loading_spinner_controller.js +48 -0
- data/app/javascript/dbdoc_engine/controllers/login_controller.js +75 -0
- data/app/javascript/dbdoc_engine/controllers/notification_controller.js +15 -0
- data/app/javascript/dbdoc_engine/controllers/schema_diagram_controller.js +1129 -0
- data/app/javascript/dbdoc_engine/controllers/select2_controller.js +67 -0
- data/app/javascript/dbdoc_engine/controllers/sidebar_controller.js +943 -0
- data/app/javascript/dbdoc_engine/controllers/table_details_controller.js +245 -0
- data/app/javascript/dbdoc_engine/controllers/table_group_validation_controller.js +148 -0
- data/app/javascript/dbdoc_engine/controllers/table_validation_controller.js +423 -0
- data/app/jobs/dbdoc_engine/application_job.rb +4 -0
- data/app/mailers/dbdoc_engine/application_mailer.rb +6 -0
- data/app/models/dbdoc_engine/application_record.rb +6 -0
- data/app/models/dbdoc_engine/concerns/soft_deletable.rb +30 -0
- data/app/models/dbdoc_engine/db_design_changelog.rb +44 -0
- data/app/models/dbdoc_engine/db_design_dynamic_column.rb +211 -0
- data/app/models/dbdoc_engine/db_design_dynamic_table.rb +124 -0
- data/app/models/dbdoc_engine/db_design_table_group.rb +88 -0
- data/app/models/dbdoc_engine/user.rb +21 -0
- data/app/queries/dbdoc_engine/admin_dashboard_queries.rb +71 -0
- data/app/queries/dbdoc_engine/db_design_changelog_queries.rb +68 -0
- data/app/queries/dbdoc_engine/db_design_dynamic_column_queries.rb +37 -0
- data/app/queries/dbdoc_engine/db_design_dynamic_table_commands.rb +106 -0
- data/app/queries/dbdoc_engine/db_design_dynamic_table_queries.rb +194 -0
- data/app/queries/dbdoc_engine/db_design_table_group_queries.rb +154 -0
- data/app/services/dbdoc_engine/db_design_dynamic_table_export_service.rb +38 -0
- data/app/services/dbdoc_engine/db_design_dynamic_table_handler_service.rb +49 -0
- data/app/services/dbdoc_engine/db_design_dynamic_tables_service.rb +21 -0
- data/app/services/dbdoc_engine/error_handler_service.rb +43 -0
- data/app/services/dbdoc_engine/schema_rb_import_service.rb +194 -0
- data/app/services/dbdoc_engine/schema_rb_parser_service.rb +339 -0
- data/app/services/dbdoc_engine/table_filter_service.rb +35 -0
- data/app/services/dbdoc_engine/table_groups_service.rb +199 -0
- data/app/services/dbdoc_engine/table_management_service.rb +192 -0
- data/app/views/dbdoc_engine/admin/dashboard/_action_badge.html.erb +11 -0
- data/app/views/dbdoc_engine/admin/dashboard/_changelog_rows.html.erb +22 -0
- data/app/views/dbdoc_engine/admin/dashboard/_changelog_table_headers.html.erb +8 -0
- data/app/views/dbdoc_engine/admin/dashboard/_filter_fields.html.erb +43 -0
- data/app/views/dbdoc_engine/admin/dashboard/index.html.erb +159 -0
- data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/_column_fields.html.erb +225 -0
- data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/_deleted_table_index.html.erb +110 -0
- data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/_foreign_key_fields.html.erb +51 -0
- data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/_form.html.erb +75 -0
- data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/_recent_activity.html.erb +39 -0
- data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/_table_columns.html.erb +127 -0
- data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/_table_index.html.erb +109 -0
- data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/_table_information.html.erb +99 -0
- data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/deleted_tables.html.erb +95 -0
- data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/edit.html.erb +23 -0
- data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/export_all_to_excel.xlsx.axlsx +240 -0
- data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/export_to_excel.xlsx.axlsx +135 -0
- data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/index.html.erb +109 -0
- data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/new.html.erb +25 -0
- data/app/views/dbdoc_engine/admin/db_design_dynamic_tables/show_table_info.html.erb +125 -0
- data/app/views/dbdoc_engine/admin/db_design_table_groups/_deleted_table_groups_list.html.erb +75 -0
- data/app/views/dbdoc_engine/admin/db_design_table_groups/_form.html.erb +88 -0
- data/app/views/dbdoc_engine/admin/db_design_table_groups/_table_groups_list.html.erb +82 -0
- data/app/views/dbdoc_engine/admin/db_design_table_groups/deleted_groups.html.erb +60 -0
- data/app/views/dbdoc_engine/admin/db_design_table_groups/edit.html.erb +25 -0
- data/app/views/dbdoc_engine/admin/db_design_table_groups/index.html.erb +85 -0
- data/app/views/dbdoc_engine/admin/db_design_table_groups/new.html.erb +26 -0
- data/app/views/dbdoc_engine/db_doc_sessions/new.html.erb +59 -0
- data/app/views/dbdoc_engine/home/changelog_details.html.erb +80 -0
- data/app/views/dbdoc_engine/home/changelogs.html.erb +20 -0
- data/app/views/dbdoc_engine/home/group_details.html.erb +94 -0
- data/app/views/dbdoc_engine/home/index.html.erb +11 -0
- data/app/views/dbdoc_engine/home/partials/_action_badge.html.erb +11 -0
- data/app/views/dbdoc_engine/home/partials/_breadcrumb_navigation.html.erb +30 -0
- data/app/views/dbdoc_engine/home/partials/_changelog_rows.html.erb +35 -0
- data/app/views/dbdoc_engine/home/partials/_changelog_table_headers.html.erb +16 -0
- data/app/views/dbdoc_engine/home/partials/_column_headers.html.erb +23 -0
- data/app/views/dbdoc_engine/home/partials/_column_row.html.erb +157 -0
- data/app/views/dbdoc_engine/home/partials/_filter_form.html.erb +47 -0
- data/app/views/dbdoc_engine/home/partials/_group_section.html.erb +84 -0
- data/app/views/dbdoc_engine/home/partials/_pagination.html.erb +5 -0
- data/app/views/dbdoc_engine/home/partials/_stats_container.html.erb +46 -0
- data/app/views/dbdoc_engine/home/partials/_table_groups.html.erb +7 -0
- data/app/views/dbdoc_engine/home/partials/_table_information_section.html.erb +50 -0
- data/app/views/dbdoc_engine/home/partials/_table_section.html.erb +48 -0
- data/app/views/dbdoc_engine/home/table_details.html.erb +9 -0
- data/app/views/dbdoc_engine/schema_diagram/index.html.erb +102 -0
- data/app/views/dbdoc_engine/shared/_admin_header.html.erb +78 -0
- data/app/views/dbdoc_engine/shared/_header.html.erb +94 -0
- data/app/views/dbdoc_engine/shared/_js_translations.html.erb +3 -0
- data/app/views/dbdoc_engine/shared/_language_button.html.erb +14 -0
- data/app/views/dbdoc_engine/shared/_sidebar.html.erb +128 -0
- data/app/views/kaminari/dbdoc_engine/_first_page.html.erb +3 -0
- data/app/views/kaminari/dbdoc_engine/_gap.html.erb +3 -0
- data/app/views/kaminari/dbdoc_engine/_last_page.html.erb +3 -0
- data/app/views/kaminari/dbdoc_engine/_next_page.html.erb +3 -0
- data/app/views/kaminari/dbdoc_engine/_page.html.erb +9 -0
- data/app/views/kaminari/dbdoc_engine/_paginator.html.erb +17 -0
- data/app/views/kaminari/dbdoc_engine/_prev_page.html.erb +3 -0
- data/app/views/layouts/dbdoc_engine/application.html.erb +107 -0
- data/app/views/layouts/dbdoc_engine/header.html.erb +108 -0
- data/config/importmap.rb +11 -0
- data/config/locales/en.yml +307 -0
- data/config/locales/ja.yml +306 -0
- data/config/routes.rb +73 -0
- data/db/migrate/rails7/20250227060610_create_db_design_table_groups.rb +15 -0
- data/db/migrate/rails7/20250227094626_create_db_design_dynamic_tables.rb +19 -0
- data/db/migrate/rails7/20250228022732_create_db_design_dynamic_columns.rb +34 -0
- data/db/migrate/rails7/20250401051453_create_db_design_changelogs.rb +26 -0
- data/db/migrate/rails7/20250411040822_create_users.rb +14 -0
- data/db/migrate/rails7/20250421080851_add_missing_indexes_to_dbdoc_tables.rb +23 -0
- data/db/migrate/rails8/20250227060610_create_db_design_table_groups.rb +15 -0
- data/db/migrate/rails8/20250227094626_create_db_design_dynamic_tables.rb +19 -0
- data/db/migrate/rails8/20250228022732_create_db_design_dynamic_columns.rb +34 -0
- data/db/migrate/rails8/20250401051453_create_db_design_changelogs.rb +26 -0
- data/db/migrate/rails8/20250411040822_create_users.rb +14 -0
- data/db/migrate/rails8/20250421080851_add_missing_indexes_to_dbdoc_tables.rb +23 -0
- data/db/seeds.rb +28 -0
- data/lib/dbdoc_engine/engine.rb +57 -0
- data/lib/dbdoc_engine/version.rb +3 -0
- data/lib/dbdoc_engine.rb +9 -0
- data/lib/generators/dbdoc_engine/install/install_generator.rb +245 -0
- data/lib/generators/dbdoc_engine/uninstall/uninstall_generator.rb +196 -0
- data/lib/tasks/dbdoc_engine_tasks.rake +44 -0
- data/public/dbdoc_engine_assets/images/camel_chess_head.png +0 -0
- data/public/dbdoc_engine_assets/images/dblogo.svg +4 -0
- data/public/dbdoc_engine_assets/images/japan_circle.png +0 -0
- data/public/dbdoc_engine_assets/images/king_chess_head.png +0 -0
- data/public/dbdoc_engine_assets/images/login-bg.svg +44 -0
- data/public/dbdoc_engine_assets/images/logo.png +0 -0
- data/public/dbdoc_engine_assets/images/logo.svg +12 -0
- data/public/dbdoc_engine_assets/images/queen_chess_head.png +0 -0
- data/public/dbdoc_engine_assets/images/soldier_chess_headd.png +0 -0
- data/public/dbdoc_engine_assets/images/uk_circle_transparent.png +0 -0
- metadata +415 -0
|
@@ -0,0 +1,1129 @@
|
|
|
1
|
+
// app/javascript/controllers/schema_diagram_controller.js
|
|
2
|
+
import { Controller } from "@hotwired/stimulus"
|
|
3
|
+
|
|
4
|
+
export default class extends Controller {
|
|
5
|
+
static targets = [
|
|
6
|
+
"diagram", "groupSelect", "zoomIn", "zoomOut", "resetView"
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
connect() {
|
|
10
|
+
this.zoom = 1
|
|
11
|
+
this.pan = { x: 0, y: 0 }
|
|
12
|
+
this.isDragging = false
|
|
13
|
+
this.lastMouse = { x: 0, y: 0 }
|
|
14
|
+
this.columnData = {}
|
|
15
|
+
this.relationshipData = {}
|
|
16
|
+
this.groupColors = {}
|
|
17
|
+
this.originalViewBox = null
|
|
18
|
+
|
|
19
|
+
this.renderDiagram()
|
|
20
|
+
|
|
21
|
+
// Event listeners for controls
|
|
22
|
+
if (this.hasGroupSelectTarget) {
|
|
23
|
+
this.groupSelectTarget.addEventListener("change", () => this.renderDiagram())
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (this.hasZoomInTarget) {
|
|
27
|
+
this.zoomInTarget.addEventListener("click", (e) => {
|
|
28
|
+
e.preventDefault()
|
|
29
|
+
this.zoomDiagram(0.2)
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (this.hasZoomOutTarget) {
|
|
34
|
+
this.zoomOutTarget.addEventListener("click", (e) => {
|
|
35
|
+
e.preventDefault()
|
|
36
|
+
this.zoomDiagram(-0.2)
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (this.hasResetViewTarget) {
|
|
41
|
+
this.resetViewTarget.addEventListener("click", (e) => {
|
|
42
|
+
e.preventDefault()
|
|
43
|
+
this.resetView()
|
|
44
|
+
})
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Pan events
|
|
48
|
+
this.diagramTarget.addEventListener("mousedown", this.startPan.bind(this))
|
|
49
|
+
document.addEventListener("mousemove", this.handlePan.bind(this))
|
|
50
|
+
document.addEventListener("mouseup", this.endPan.bind(this))
|
|
51
|
+
this.diagramTarget.addEventListener("contextmenu", (e) => {
|
|
52
|
+
if (this.isDragging) e.preventDefault()
|
|
53
|
+
})
|
|
54
|
+
this.diagramTarget.style.cursor = 'grab'
|
|
55
|
+
|
|
56
|
+
this.createTooltip()
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
createTooltip() {
|
|
60
|
+
this.tooltip = document.createElement('div')
|
|
61
|
+
this.tooltip.id = 'column-tooltip'
|
|
62
|
+
this.tooltip.className = 'position-absolute p-2'
|
|
63
|
+
this.tooltip.style.cssText = `
|
|
64
|
+
display: none;
|
|
65
|
+
z-index: 1000;
|
|
66
|
+
max-width: 300px;
|
|
67
|
+
font-size: 12px;
|
|
68
|
+
line-height: 1.4;
|
|
69
|
+
pointer-events: none;
|
|
70
|
+
background: rgba(0, 0, 0, 0.9);
|
|
71
|
+
color: white;
|
|
72
|
+
border-radius: 4px;
|
|
73
|
+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
74
|
+
`
|
|
75
|
+
document.body.appendChild(this.tooltip)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async renderDiagram() {
|
|
79
|
+
const groupId = this.hasGroupSelectTarget ? this.groupSelectTarget.value : "all"
|
|
80
|
+
this.diagramTarget.innerHTML = '<div class="d-flex justify-content-center align-items-center h-100"><span class="text-muted fst-italic">Generating diagram...</span></div>'
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
const url = '/dbdoc/schema_diagram/data?group_id=' + groupId
|
|
84
|
+
const response = await fetch(url, { headers: { 'Accept': 'application/json' } })
|
|
85
|
+
if (!response.ok) throw new Error(`HTTP error! Status: ${response.status}`)
|
|
86
|
+
const data = await response.json()
|
|
87
|
+
this.diagramTarget.innerHTML = data.diagram_svg
|
|
88
|
+
this.initializeTableHoverListeners()
|
|
89
|
+
this.columnData = data.column_data
|
|
90
|
+
this.relationshipData = data.relationship_data || {}
|
|
91
|
+
this.groupColors = data.group_colors || {}
|
|
92
|
+
|
|
93
|
+
// Wait a bit for SVG to render, then initialize listeners
|
|
94
|
+
setTimeout(() => {
|
|
95
|
+
this.addTooltipListeners()
|
|
96
|
+
this.initializeEdgeListeners()
|
|
97
|
+
this.addHighlightStyles()
|
|
98
|
+
}, 100)
|
|
99
|
+
} catch (error) {
|
|
100
|
+
this.diagramTarget.innerHTML = `<div class="alert alert-danger">Error loading diagram: ${error.message}</div>`
|
|
101
|
+
console.error(error)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
addHighlightStyles() {
|
|
106
|
+
// Add CSS styles for highlighting if not already present
|
|
107
|
+
if (!document.getElementById('diagram-highlight-styles')) {
|
|
108
|
+
const style = document.createElement('style')
|
|
109
|
+
style.id = 'diagram-highlight-styles'
|
|
110
|
+
style.textContent = `
|
|
111
|
+
.edge-highlighted path {
|
|
112
|
+
stroke-dasharray: 8, 4;
|
|
113
|
+
animation: dash-flow 2s linear infinite;
|
|
114
|
+
filter: drop-shadow(0 0 6px rgba(52, 152, 219, 0.8));
|
|
115
|
+
stroke-width: 3px !important;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
@keyframes dash-flow {
|
|
119
|
+
0% { stroke-dashoffset: 0; }
|
|
120
|
+
100% { stroke-dashoffset: 24; }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* Keep other styles from second version */
|
|
124
|
+
.edge-highlighted polygon {
|
|
125
|
+
fill: var(--highlight-color) !important;
|
|
126
|
+
stroke: var(--highlight-color) !important;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* Simple column highlighting - no shadows or complex effects */
|
|
130
|
+
.column-highlighted {
|
|
131
|
+
fill: var(--highlight-color-bg) !important;
|
|
132
|
+
stroke: var(--highlight-color) !important;
|
|
133
|
+
stroke-width: 2px !important;
|
|
134
|
+
transition: all 0.2s ease !important;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/* Simple text highlighting - no shadows or pulsing */
|
|
138
|
+
.column-text-highlighted {
|
|
139
|
+
fill: var(--highlight-color) !important;
|
|
140
|
+
font-weight: bold !important;
|
|
141
|
+
transition: all 0.2s ease !important;
|
|
142
|
+
}
|
|
143
|
+
`
|
|
144
|
+
document.head.appendChild(style)
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
initializeEdgeListeners() {
|
|
149
|
+
const svg = this.diagramTarget.querySelector('svg')
|
|
150
|
+
if (!svg) return
|
|
151
|
+
|
|
152
|
+
// Clear any existing listeners to prevent duplicates
|
|
153
|
+
this.clearExistingEdgeListeners(svg)
|
|
154
|
+
|
|
155
|
+
// Find and attach edge listeners with improved precision
|
|
156
|
+
this.findAndAttachEdgeListeners(svg)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
clearExistingEdgeListeners(svg) {
|
|
160
|
+
// Remove existing edge event listeners
|
|
161
|
+
const existingEdges = svg.querySelectorAll('[data-edge-id]')
|
|
162
|
+
existingEdges.forEach(edge => {
|
|
163
|
+
edge.removeAttribute('data-edge-id')
|
|
164
|
+
edge.style.cursor = ''
|
|
165
|
+
// Clone and replace to remove all event listeners
|
|
166
|
+
const newEdge = edge.cloneNode(true)
|
|
167
|
+
edge.parentNode.replaceChild(newEdge, edge)
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
findAndAttachEdgeListeners(svg) {
|
|
172
|
+
// Strategy 1: Look for elements with edge IDs from our relationship data
|
|
173
|
+
Object.entries(this.relationshipData).forEach(([edgeId, relationship]) => {
|
|
174
|
+
const edgeElement = svg.querySelector(`#${CSS.escape(edgeId)}`)
|
|
175
|
+
if (edgeElement) {
|
|
176
|
+
this.attachPreciseEdgeListeners(edgeElement, edgeId, relationship)
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Strategy 2: Try to find edge by relationship data
|
|
181
|
+
const foundEdge = this.findEdgeByRelationship(svg, relationship, edgeId)
|
|
182
|
+
if (foundEdge) {
|
|
183
|
+
this.attachPreciseEdgeListeners(foundEdge, edgeId, relationship)
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
findEdgeByRelationship(svg, relationship, edgeId) {
|
|
189
|
+
const { source_table, target_table } = relationship
|
|
190
|
+
|
|
191
|
+
// Look for edge groups that might represent this relationship
|
|
192
|
+
const edgeGroups = svg.querySelectorAll('g.edge, g[id*="edge"]')
|
|
193
|
+
|
|
194
|
+
for (const edgeGroup of edgeGroups) {
|
|
195
|
+
// Check if this edge connects our specific tables
|
|
196
|
+
if (this.edgeConnectsTables(edgeGroup, source_table, target_table)) {
|
|
197
|
+
return edgeGroup
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Fallback: look for individual path/polygon elements
|
|
202
|
+
const pathElements = svg.querySelectorAll('path, polygon')
|
|
203
|
+
for (const pathElement of pathElements) {
|
|
204
|
+
if (this.isRelationshipPath(pathElement, source_table, target_table)) {
|
|
205
|
+
return pathElement
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return null
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
edgeConnectsTables(edgeGroup, sourceTable, targetTable) {
|
|
213
|
+
// Check title element for table names
|
|
214
|
+
const title = edgeGroup.querySelector('title')
|
|
215
|
+
if (title) {
|
|
216
|
+
const titleText = title.textContent.trim()
|
|
217
|
+
return titleText.includes(sourceTable) && titleText.includes(targetTable)
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Check if edge is geometrically positioned between the two tables
|
|
221
|
+
return this.isEdgeGeometricallyBetweenTables(edgeGroup, sourceTable, targetTable)
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
isEdgeGeometricallyBetweenTables(edgeElement, sourceTable, targetTable) {
|
|
225
|
+
const svg = this.diagramTarget.querySelector('svg')
|
|
226
|
+
if (!svg) return false
|
|
227
|
+
|
|
228
|
+
try {
|
|
229
|
+
const edgeBBox = edgeElement.getBBox()
|
|
230
|
+
const sourceTableEl = svg.querySelector(`#${CSS.escape(sourceTable)}`)
|
|
231
|
+
const targetTableEl = svg.querySelector(`#${CSS.escape(targetTable)}`)
|
|
232
|
+
|
|
233
|
+
if (!sourceTableEl || !targetTableEl) return false
|
|
234
|
+
|
|
235
|
+
const sourceBBox = sourceTableEl.getBBox()
|
|
236
|
+
const targetBBox = targetTableEl.getBBox()
|
|
237
|
+
|
|
238
|
+
// Check if edge is positioned between the two tables
|
|
239
|
+
const edgeCenterX = edgeBBox.x + edgeBBox.width / 2
|
|
240
|
+
const edgeCenterY = edgeBBox.y + edgeBBox.height / 2
|
|
241
|
+
|
|
242
|
+
const sourceCenterX = sourceBBox.x + sourceBBox.width / 2
|
|
243
|
+
const sourceCenterY = sourceBBox.y + sourceBBox.height / 2
|
|
244
|
+
|
|
245
|
+
const targetCenterX = targetBBox.x + targetBBox.width / 2
|
|
246
|
+
const targetCenterY = targetBBox.y + targetBBox.height / 2
|
|
247
|
+
|
|
248
|
+
// Calculate if edge is roughly on the line between source and target
|
|
249
|
+
const distanceToLine = this.pointToLineDistance(
|
|
250
|
+
edgeCenterX, edgeCenterY,
|
|
251
|
+
sourceCenterX, sourceCenterY,
|
|
252
|
+
targetCenterX, targetCenterY
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
// Allow some tolerance for curved lines
|
|
256
|
+
return distanceToLine < 100
|
|
257
|
+
} catch (e) {
|
|
258
|
+
return false
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
pointToLineDistance(px, py, x1, y1, x2, y2) {
|
|
263
|
+
const A = px - x1
|
|
264
|
+
const B = py - y1
|
|
265
|
+
const C = x2 - x1
|
|
266
|
+
const D = y2 - y1
|
|
267
|
+
|
|
268
|
+
const dot = A * C + B * D
|
|
269
|
+
const lenSq = C * C + D * D
|
|
270
|
+
let param = -1
|
|
271
|
+
if (lenSq !== 0) {
|
|
272
|
+
param = dot / lenSq
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
let xx, yy
|
|
276
|
+
if (param < 0) {
|
|
277
|
+
xx = x1
|
|
278
|
+
yy = y1
|
|
279
|
+
} else if (param > 1) {
|
|
280
|
+
xx = x2
|
|
281
|
+
yy = y2
|
|
282
|
+
} else {
|
|
283
|
+
xx = x1 + param * C
|
|
284
|
+
yy = y1 + param * D
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const dx = px - xx
|
|
288
|
+
const dy = py - yy
|
|
289
|
+
return Math.sqrt(dx * dx + dy * dy)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
attachPreciseEdgeListeners(element, edgeId, relationship) {
|
|
293
|
+
// Clear any existing data
|
|
294
|
+
element.removeAttribute('data-edge-id')
|
|
295
|
+
|
|
296
|
+
element.style.cursor = 'pointer'
|
|
297
|
+
element.setAttribute('data-edge-id', edgeId)
|
|
298
|
+
element.setAttribute('data-source-table', relationship.source_table)
|
|
299
|
+
element.setAttribute('data-target-table', relationship.target_table)
|
|
300
|
+
element.setAttribute('data-source-column', relationship.source_column)
|
|
301
|
+
element.setAttribute('data-target-column', relationship.target_column)
|
|
302
|
+
|
|
303
|
+
// Use event delegation with precise event handling
|
|
304
|
+
element.addEventListener('mouseenter', (e) => this.handlePreciseEdgeHover(e, edgeId, relationship))
|
|
305
|
+
element.addEventListener('mouseleave', (e) => this.handlePreciseEdgeLeave(e, edgeId))
|
|
306
|
+
element.addEventListener('click', (e) => e.stopPropagation()) // Prevent interference with pan
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
handlePreciseEdgeHover(event, edgeId, relationship) {
|
|
310
|
+
// Stop event propagation to prevent multiple edges from triggering
|
|
311
|
+
event.stopPropagation()
|
|
312
|
+
|
|
313
|
+
const edge = event.currentTarget
|
|
314
|
+
|
|
315
|
+
// Double-check that we're hovering the correct edge
|
|
316
|
+
const hoveredEdgeId = edge.getAttribute('data-edge-id')
|
|
317
|
+
if (hoveredEdgeId !== edgeId) {
|
|
318
|
+
return
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Clear any existing highlights first
|
|
322
|
+
this.clearAllHighlights()
|
|
323
|
+
|
|
324
|
+
// Get the group color for this relationship
|
|
325
|
+
const highlightColor = this.getRelationshipGroupColor(relationship)
|
|
326
|
+
const highlightColorBg = this.hexToRgba(highlightColor, 0.2)
|
|
327
|
+
|
|
328
|
+
// Set CSS custom properties for the highlight color
|
|
329
|
+
document.documentElement.style.setProperty('--highlight-color', highlightColor)
|
|
330
|
+
document.documentElement.style.setProperty('--highlight-color-bg', highlightColorBg)
|
|
331
|
+
|
|
332
|
+
// Highlight the specific edge with group color
|
|
333
|
+
edge.classList.add('edge-highlighted')
|
|
334
|
+
|
|
335
|
+
// Highlight only the specific columns for this relationship with group color
|
|
336
|
+
this.highlightSpecificRelationship(relationship, highlightColor)
|
|
337
|
+
|
|
338
|
+
// Store the currently highlighted edge
|
|
339
|
+
this.currentlyHighlightedEdge = edgeId
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
handlePreciseEdgeLeave(event, edgeId) {
|
|
343
|
+
event.stopPropagation()
|
|
344
|
+
|
|
345
|
+
const edge = event.currentTarget
|
|
346
|
+
|
|
347
|
+
// Only clear if we're leaving the currently highlighted edge
|
|
348
|
+
if (this.currentlyHighlightedEdge === edgeId) {
|
|
349
|
+
edge.classList.remove('edge-highlighted')
|
|
350
|
+
this.clearAllHighlights()
|
|
351
|
+
this.currentlyHighlightedEdge = null
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
getRelationshipGroupColor(relationship) {
|
|
356
|
+
// Get the group color from the target table (or source table as fallback)
|
|
357
|
+
const targetTableColor = this.groupColors[relationship.target_table]
|
|
358
|
+
const sourceTableColor = this.groupColors[relationship.source_table]
|
|
359
|
+
|
|
360
|
+
// Return the target table color, or source table color, or default color
|
|
361
|
+
return targetTableColor || sourceTableColor || '#ff6b35'
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
hexToRgba(hex, alpha) {
|
|
365
|
+
// Convert hex color to rgba
|
|
366
|
+
const r = parseInt(hex.slice(1, 3), 16)
|
|
367
|
+
const g = parseInt(hex.slice(3, 5), 16)
|
|
368
|
+
const b = parseInt(hex.slice(5, 7), 16)
|
|
369
|
+
return `rgba(${r}, ${g}, ${b}, ${alpha})`
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
highlightSpecificRelationship(relationship, color) {
|
|
373
|
+
const { source_table, source_column, target_table, target_column } = relationship
|
|
374
|
+
|
|
375
|
+
// Set the color for highlighting
|
|
376
|
+
document.documentElement.style.setProperty('--highlight-color', color)
|
|
377
|
+
document.documentElement.style.setProperty('--highlight-color-bg', this.hexToRgba(color, 0.2))
|
|
378
|
+
|
|
379
|
+
// Only highlight the exact columns involved in this specific relationship
|
|
380
|
+
this.highlightExactColumn(source_table, source_column, color)
|
|
381
|
+
this.highlightExactColumn(target_table, target_column, color)
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
highlightExactColumn(tableName, columnName, color) {
|
|
385
|
+
const svg = this.diagramTarget.querySelector('svg')
|
|
386
|
+
if (!svg) return
|
|
387
|
+
|
|
388
|
+
// Find the specific table element
|
|
389
|
+
const tableElement = svg.querySelector(`#${CSS.escape(tableName)}`)
|
|
390
|
+
if (!tableElement) return
|
|
391
|
+
|
|
392
|
+
// Get column data for validation
|
|
393
|
+
const tableColumnData = this.columnData[tableName]
|
|
394
|
+
if (!tableColumnData || !tableColumnData[columnName]) return
|
|
395
|
+
|
|
396
|
+
// Find and highlight only the exact column
|
|
397
|
+
const textElements = tableElement.querySelectorAll('text')
|
|
398
|
+
textElements.forEach(textEl => {
|
|
399
|
+
const textContent = textEl.textContent.trim()
|
|
400
|
+
|
|
401
|
+
// Use exact matching to prevent cross-contamination
|
|
402
|
+
if (this.isExactColumnMatch(textContent, columnName, Object.keys(tableColumnData))) {
|
|
403
|
+
textEl.classList.add('column-text-highlighted')
|
|
404
|
+
|
|
405
|
+
// Highlight associated cells
|
|
406
|
+
this.highlightAssociatedCells(textEl, tableElement, color)
|
|
407
|
+
}
|
|
408
|
+
})
|
|
409
|
+
|
|
410
|
+
// Also handle HTML table cells
|
|
411
|
+
const htmlCells = tableElement.querySelectorAll('td')
|
|
412
|
+
htmlCells.forEach(cell => {
|
|
413
|
+
if (this.isExactColumnMatch(cell.textContent.trim(), columnName, Object.keys(tableColumnData))) {
|
|
414
|
+
cell.classList.add('column-highlighted')
|
|
415
|
+
}
|
|
416
|
+
})
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
highlightAssociatedCells(textElement, tableElement, color) {
|
|
420
|
+
try {
|
|
421
|
+
const textBBox = textElement.getBBox()
|
|
422
|
+
const cells = tableElement.querySelectorAll('rect, polygon')
|
|
423
|
+
|
|
424
|
+
cells.forEach(cell => {
|
|
425
|
+
try {
|
|
426
|
+
const cellBBox = cell.getBBox()
|
|
427
|
+
// More precise vertical alignment check
|
|
428
|
+
const verticalOverlap = Math.abs(cellBBox.y - textBBox.y) < 15 &&
|
|
429
|
+
Math.abs((cellBBox.y + cellBBox.height) - (textBBox.y + textBBox.height)) < 15
|
|
430
|
+
|
|
431
|
+
if (verticalOverlap) {
|
|
432
|
+
cell.classList.add('column-highlighted')
|
|
433
|
+
}
|
|
434
|
+
} catch (e) {
|
|
435
|
+
// Skip problematic elements
|
|
436
|
+
}
|
|
437
|
+
})
|
|
438
|
+
} catch (e) {
|
|
439
|
+
// Skip if bounding box calculation fails
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
clearAllHighlights() {
|
|
444
|
+
const svg = this.diagramTarget.querySelector('svg')
|
|
445
|
+
if (!svg) return
|
|
446
|
+
|
|
447
|
+
// Remove all highlight classes
|
|
448
|
+
svg.querySelectorAll('.edge-highlighted').forEach(el => {
|
|
449
|
+
el.classList.remove('edge-highlighted')
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
svg.querySelectorAll('.animated').forEach(el => {
|
|
453
|
+
el.classList.remove('animated')
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
svg.querySelectorAll('.column-highlighted').forEach(el => {
|
|
457
|
+
el.classList.remove('column-highlighted')
|
|
458
|
+
})
|
|
459
|
+
|
|
460
|
+
svg.querySelectorAll('.column-text-highlighted').forEach(el => {
|
|
461
|
+
el.classList.remove('column-text-highlighted')
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
// Clear CSS custom properties
|
|
465
|
+
document.documentElement.style.removeProperty('--highlight-color')
|
|
466
|
+
document.documentElement.style.removeProperty('--highlight-color-bg')
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
isExactColumnMatch(textContent, targetColumnName, allColumns) {
|
|
470
|
+
// Remove common symbols and attributes from the text
|
|
471
|
+
const cleanText = textContent.replace(/[🔑🔗]/g, '').replace(/\s+NN\s*$/g, '').trim()
|
|
472
|
+
|
|
473
|
+
// Method 1: Exact match
|
|
474
|
+
if (cleanText === targetColumnName) {
|
|
475
|
+
return true
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Method 2: Check if the text starts with the column name followed by space or symbol
|
|
479
|
+
const columnWithSpace = targetColumnName + ' '
|
|
480
|
+
const columnWithSymbol = targetColumnName + ' 🔑'
|
|
481
|
+
const columnWithFK = targetColumnName + ' 🔗'
|
|
482
|
+
const columnWithNN = targetColumnName + ' NN'
|
|
483
|
+
|
|
484
|
+
if (textContent.startsWith(columnWithSpace) ||
|
|
485
|
+
textContent.startsWith(columnWithSymbol) ||
|
|
486
|
+
textContent.startsWith(columnWithFK) ||
|
|
487
|
+
textContent.startsWith(columnWithNN)) {
|
|
488
|
+
return true
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Method 3: Word boundary check
|
|
492
|
+
const wordBoundaryRegex = new RegExp(`\\b${this.escapeRegExp(targetColumnName)}\\b`)
|
|
493
|
+
if (wordBoundaryRegex.test(cleanText)) {
|
|
494
|
+
const isSubstringOfLongerColumn = allColumns.some(col =>
|
|
495
|
+
col !== targetColumnName &&
|
|
496
|
+
col.includes(targetColumnName) &&
|
|
497
|
+
cleanText.includes(col)
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
if (!isSubstringOfLongerColumn) {
|
|
501
|
+
return true
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return false
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
escapeRegExp(string) {
|
|
509
|
+
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
clearColumnHighlights() {
|
|
513
|
+
const svg = this.diagramTarget.querySelector('svg')
|
|
514
|
+
if (!svg) return
|
|
515
|
+
|
|
516
|
+
// Remove column highlight classes
|
|
517
|
+
svg.querySelectorAll('.column-highlighted').forEach(el => {
|
|
518
|
+
el.classList.remove('column-highlighted')
|
|
519
|
+
})
|
|
520
|
+
|
|
521
|
+
svg.querySelectorAll('.column-text-highlighted').forEach(el => {
|
|
522
|
+
el.classList.remove('column-text-highlighted')
|
|
523
|
+
})
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
calculateDistance(bbox1, bbox2) {
|
|
527
|
+
const centerX1 = bbox1.x + bbox1.width / 2
|
|
528
|
+
const centerY1 = bbox1.y + bbox1.height / 2
|
|
529
|
+
const centerX2 = bbox2.x + bbox2.width / 2
|
|
530
|
+
const centerY2 = bbox2.y + bbox2.height / 2
|
|
531
|
+
|
|
532
|
+
return Math.sqrt(Math.pow(centerX2 - centerX1, 2) + Math.pow(centerY2 - centerY1, 2))
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// IMPROVED TOOLTIP LISTENERS - This is the key fix for your issue
|
|
536
|
+
addTooltipListeners() {
|
|
537
|
+
const svg = this.diagramTarget.querySelector('svg');
|
|
538
|
+
if (!svg) return;
|
|
539
|
+
|
|
540
|
+
// Clear any existing tooltip listeners
|
|
541
|
+
this.clearExistingTooltipListeners(svg);
|
|
542
|
+
|
|
543
|
+
// Method 1: Try direct table-based approach first
|
|
544
|
+
this.addDirectTableTooltips(svg);
|
|
545
|
+
|
|
546
|
+
// Method 2: Fallback to proximity-based approach
|
|
547
|
+
this.addProximityBasedTooltips(svg);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
clearExistingTooltipListeners(svg) {
|
|
551
|
+
// Remove existing tooltip attributes and listeners
|
|
552
|
+
const existingTooltipElements = svg.querySelectorAll('[data-tooltip-attached]');
|
|
553
|
+
existingTooltipElements.forEach(el => {
|
|
554
|
+
el.removeAttribute('data-tooltip-attached');
|
|
555
|
+
el.removeAttribute('data-table');
|
|
556
|
+
el.removeAttribute('data-column');
|
|
557
|
+
el.style.cursor = '';
|
|
558
|
+
// Clone and replace to remove event listeners
|
|
559
|
+
const newEl = el.cloneNode(true);
|
|
560
|
+
el.parentNode.replaceChild(newEl, el);
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
addDirectTableTooltips(svg) {
|
|
565
|
+
// For each table in our column data
|
|
566
|
+
Object.keys(this.columnData).forEach(tableName => {
|
|
567
|
+
const tableElement = svg.querySelector(`#${CSS.escape(tableName)}`);
|
|
568
|
+
if (!tableElement) return;
|
|
569
|
+
|
|
570
|
+
const tableColumnData = this.columnData[tableName];
|
|
571
|
+
const columnNames = Object.keys(tableColumnData);
|
|
572
|
+
|
|
573
|
+
// Find all text elements within this table
|
|
574
|
+
const textElements = tableElement.querySelectorAll('text');
|
|
575
|
+
|
|
576
|
+
textElements.forEach(textEl => {
|
|
577
|
+
const textContent = textEl.textContent.trim();
|
|
578
|
+
|
|
579
|
+
// Try to match this text to a column name
|
|
580
|
+
const matchedColumn = this.findBestColumnMatch(textContent, columnNames);
|
|
581
|
+
|
|
582
|
+
if (matchedColumn && !textEl.hasAttribute('data-tooltip-attached')) {
|
|
583
|
+
this.attachTooltipToElement(textEl, tableName, matchedColumn);
|
|
584
|
+
|
|
585
|
+
// Also try to find associated visual elements (rect, td) for the same row
|
|
586
|
+
this.attachTooltipToAssociatedElements(textEl, tableElement, tableName, matchedColumn);
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
// Also check HTML table cells if present
|
|
591
|
+
const htmlCells = tableElement.querySelectorAll('td');
|
|
592
|
+
htmlCells.forEach(cell => {
|
|
593
|
+
const cellContent = cell.textContent.trim();
|
|
594
|
+
const matchedColumn = this.findBestColumnMatch(cellContent, columnNames);
|
|
595
|
+
|
|
596
|
+
if (matchedColumn && !cell.hasAttribute('data-tooltip-attached')) {
|
|
597
|
+
this.attachTooltipToElement(cell, tableName, matchedColumn);
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
addProximityBasedTooltips(svg) {
|
|
604
|
+
// This is a fallback method for elements that weren't caught by the direct method
|
|
605
|
+
const allTextElements = svg.querySelectorAll('text:not([data-tooltip-attached])');
|
|
606
|
+
|
|
607
|
+
allTextElements.forEach(textEl => {
|
|
608
|
+
const textContent = textEl.textContent.trim();
|
|
609
|
+
|
|
610
|
+
// Skip if this looks like a table name or other non-column content
|
|
611
|
+
if (Object.keys(this.columnData).includes(textContent)) return;
|
|
612
|
+
if (textContent.length === 0) return;
|
|
613
|
+
|
|
614
|
+
// Find the closest table and try to match column
|
|
615
|
+
const closestTable = this.findClosestTable(textEl, svg);
|
|
616
|
+
if (!closestTable) return;
|
|
617
|
+
|
|
618
|
+
const tableColumnData = this.columnData[closestTable];
|
|
619
|
+
if (!tableColumnData) return;
|
|
620
|
+
|
|
621
|
+
const columnNames = Object.keys(tableColumnData);
|
|
622
|
+
const matchedColumn = this.findBestColumnMatch(textContent, columnNames);
|
|
623
|
+
|
|
624
|
+
if (matchedColumn) {
|
|
625
|
+
this.attachTooltipToElement(textEl, closestTable, matchedColumn);
|
|
626
|
+
}
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
findBestColumnMatch(textContent, columnNames) {
|
|
631
|
+
// Clean the text content
|
|
632
|
+
const cleanText = textContent.replace(/[🔑🔗]/g, '').replace(/\s+NN\s*$/g, '').trim();
|
|
633
|
+
|
|
634
|
+
// Method 1: Exact match
|
|
635
|
+
if (columnNames.includes(cleanText)) {
|
|
636
|
+
return cleanText;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// Method 2: Text starts with column name
|
|
640
|
+
for (const columnName of columnNames) {
|
|
641
|
+
if (textContent.startsWith(columnName + ' ') ||
|
|
642
|
+
textContent.startsWith(columnName + ' 🔑') ||
|
|
643
|
+
textContent.startsWith(columnName + ' 🔗') ||
|
|
644
|
+
textContent.startsWith(columnName + ' NN')) {
|
|
645
|
+
return columnName;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// Method 3: Column name is at the beginning of cleaned text
|
|
650
|
+
for (const columnName of columnNames) {
|
|
651
|
+
if (cleanText.startsWith(columnName)) {
|
|
652
|
+
// Make sure it's not a substring of a longer column name
|
|
653
|
+
const isWholeWord = cleanText === columnName ||
|
|
654
|
+
cleanText.charAt(columnName.length).match(/\s/);
|
|
655
|
+
if (isWholeWord) {
|
|
656
|
+
return columnName;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Method 4: Fuzzy matching for minor differences
|
|
662
|
+
for (const columnName of columnNames) {
|
|
663
|
+
if (this.isFuzzyMatch(cleanText, columnName)) {
|
|
664
|
+
return columnName;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
return null;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
isFuzzyMatch(text1, text2) {
|
|
672
|
+
// Simple fuzzy matching - you can make this more sophisticated
|
|
673
|
+
const text1Lower = text1.toLowerCase();
|
|
674
|
+
const text2Lower = text2.toLowerCase();
|
|
675
|
+
|
|
676
|
+
// Check if one contains the other
|
|
677
|
+
if (text1Lower.includes(text2Lower) || text2Lower.includes(text1Lower)) {
|
|
678
|
+
return true;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// Check for common variations (underscores vs spaces, etc.)
|
|
682
|
+
const normalized1 = text1Lower.replace(/[_\s-]/g, '');
|
|
683
|
+
const normalized2 = text2Lower.replace(/[_\s-]/g, '');
|
|
684
|
+
|
|
685
|
+
return normalized1 === normalized2;
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
findClosestTable(element, svg) {
|
|
689
|
+
try {
|
|
690
|
+
const elementRect = element.getBoundingClientRect();
|
|
691
|
+
const elementX = elementRect.left + elementRect.width / 2;
|
|
692
|
+
const elementY = elementRect.top + elementRect.height / 2;
|
|
693
|
+
|
|
694
|
+
let closestTable = null;
|
|
695
|
+
let closestDistance = Infinity;
|
|
696
|
+
|
|
697
|
+
Object.keys(this.columnData).forEach(tableName => {
|
|
698
|
+
const tableElement = svg.querySelector(`#${CSS.escape(tableName)}`);
|
|
699
|
+
if (!tableElement) return;
|
|
700
|
+
|
|
701
|
+
try {
|
|
702
|
+
const tableRect = tableElement.getBoundingClientRect();
|
|
703
|
+
const tableX = tableRect.left + tableRect.width / 2;
|
|
704
|
+
const tableY = tableRect.top + tableRect.height / 2;
|
|
705
|
+
|
|
706
|
+
const distance = Math.sqrt(
|
|
707
|
+
Math.pow(elementX - tableX, 2) + Math.pow(elementY - tableY, 2)
|
|
708
|
+
);
|
|
709
|
+
|
|
710
|
+
// Prefer elements that are below the table header (positive Y difference)
|
|
711
|
+
const isBelow = elementY > (tableRect.top + 20); // 20px buffer for header
|
|
712
|
+
const adjustedDistance = isBelow ? distance : distance * 2;
|
|
713
|
+
|
|
714
|
+
if (adjustedDistance < closestDistance) {
|
|
715
|
+
closestDistance = adjustedDistance;
|
|
716
|
+
closestTable = tableName;
|
|
717
|
+
}
|
|
718
|
+
} catch (e) {
|
|
719
|
+
// Skip tables that can't provide bounding rect
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
return closestTable;
|
|
724
|
+
} catch (e) {
|
|
725
|
+
return null;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
attachTooltipToElement(element, tableName, columnName) {
|
|
730
|
+
element.style.cursor = 'pointer';
|
|
731
|
+
element.setAttribute('data-table', tableName);
|
|
732
|
+
element.setAttribute('data-column', columnName);
|
|
733
|
+
element.setAttribute('data-tooltip-attached', 'true');
|
|
734
|
+
|
|
735
|
+
element.addEventListener('mouseenter', (e) => this.showColumnTooltip(e, tableName, columnName));
|
|
736
|
+
element.addEventListener('mouseleave', () => this.hideColumnTooltip());
|
|
737
|
+
element.addEventListener('mousemove', (e) => this.updateColumnTooltipPosition(e));
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
attachTooltipToAssociatedElements(textElement, tableElement, tableName, columnName) {
|
|
741
|
+
try {
|
|
742
|
+
const textBBox = textElement.getBBox();
|
|
743
|
+
|
|
744
|
+
// Find rect/polygon elements that are visually aligned with this text
|
|
745
|
+
const visualElements = tableElement.querySelectorAll('rect, polygon');
|
|
746
|
+
|
|
747
|
+
visualElements.forEach(visualEl => {
|
|
748
|
+
try {
|
|
749
|
+
const visualBBox = visualEl.getBBox();
|
|
750
|
+
|
|
751
|
+
// Check if they're on the same row (vertical overlap)
|
|
752
|
+
const verticalOverlap = Math.abs(visualBBox.y - textBBox.y) < 20;
|
|
753
|
+
|
|
754
|
+
if (verticalOverlap && !visualEl.hasAttribute('data-tooltip-attached')) {
|
|
755
|
+
this.attachTooltipToElement(visualEl, tableName, columnName);
|
|
756
|
+
}
|
|
757
|
+
} catch (e) {
|
|
758
|
+
// Skip elements that can't provide bounding box
|
|
759
|
+
}
|
|
760
|
+
});
|
|
761
|
+
} catch (e) {
|
|
762
|
+
// Skip if bounding box calculation fails
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
showColumnTooltip(event, tableName, columnName) {
|
|
767
|
+
const columnInfo = this.columnData[tableName][columnName]
|
|
768
|
+
if (!columnInfo) return
|
|
769
|
+
|
|
770
|
+
const attributes = []
|
|
771
|
+
if (columnInfo.is_primary_key) attributes.push('Primary Key')
|
|
772
|
+
if (columnInfo.is_foreign_key) attributes.push('Foreign Key')
|
|
773
|
+
if (columnInfo.not_null) attributes.push('Not Null')
|
|
774
|
+
if (columnInfo.is_unique_key) attributes.push('Unique Key')
|
|
775
|
+
if (columnInfo.is_candidate_key) attributes.push('Candidate Key')
|
|
776
|
+
if (columnInfo.is_indexed) attributes.push('Indexed')
|
|
777
|
+
|
|
778
|
+
const tooltipContent = `
|
|
779
|
+
<div class="fw-bold mb-1">${columnInfo.name}</div>
|
|
780
|
+
<div class="mb-1"><strong>Type:</strong> ${columnInfo.data_type}</div>
|
|
781
|
+
${attributes.length > 0 ? `<div class="mb-1"><strong>Attributes:</strong> ${attributes.join(', ')}</div>` : ''}
|
|
782
|
+
<div><strong>Description:</strong></div>
|
|
783
|
+
<div class="fst-italic">${columnInfo.description || 'No description available'}</div>
|
|
784
|
+
`
|
|
785
|
+
|
|
786
|
+
this.tooltip.innerHTML = tooltipContent
|
|
787
|
+
this.tooltip.style.display = 'block'
|
|
788
|
+
this.updateColumnTooltipPosition(event)
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
hideColumnTooltip() {
|
|
792
|
+
this.tooltip.style.display = 'none'
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
updateColumnTooltipPosition(event) {
|
|
796
|
+
const tooltipRect = this.tooltip.getBoundingClientRect()
|
|
797
|
+
const windowWidth = window.innerWidth
|
|
798
|
+
const windowHeight = window.innerHeight
|
|
799
|
+
|
|
800
|
+
let left = event.clientX + 10
|
|
801
|
+
let top = event.clientY + 10
|
|
802
|
+
|
|
803
|
+
if (left + tooltipRect.width > windowWidth) {
|
|
804
|
+
left = event.clientX - tooltipRect.width - 10
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
if (top + tooltipRect.height > windowHeight) {
|
|
808
|
+
top = event.clientY - tooltipRect.height - 10
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
this.tooltip.style.left = left + 'px'
|
|
812
|
+
this.tooltip.style.top = top + 'px'
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
groupTextElementsByTable(svg, tableNames) {
|
|
816
|
+
const textElementsByTable = {}
|
|
817
|
+
|
|
818
|
+
tableNames.forEach(tableName => {
|
|
819
|
+
textElementsByTable[tableName] = []
|
|
820
|
+
})
|
|
821
|
+
|
|
822
|
+
// Try to find elements by ID first
|
|
823
|
+
tableNames.forEach(tableName => {
|
|
824
|
+
const tableElement = svg.querySelector(`#${CSS.escape(tableName)}`)
|
|
825
|
+
if (tableElement) {
|
|
826
|
+
const textElements = tableElement.querySelectorAll('text')
|
|
827
|
+
textElements.forEach(textEl => {
|
|
828
|
+
textElementsByTable[tableName].push(textEl)
|
|
829
|
+
})
|
|
830
|
+
}
|
|
831
|
+
})
|
|
832
|
+
|
|
833
|
+
// Fallback to proximity-based grouping if no elements found
|
|
834
|
+
if (Object.values(textElementsByTable).every(arr => arr.length === 0)) {
|
|
835
|
+
return this.groupTextElementsByProximity(svg, tableNames)
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
return textElementsByTable
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
groupTextElementsByProximity(svg, tableNames) {
|
|
842
|
+
const textElementsByTable = {}
|
|
843
|
+
const allTexts = svg.querySelectorAll('text')
|
|
844
|
+
|
|
845
|
+
tableNames.forEach(tableName => {
|
|
846
|
+
textElementsByTable[tableName] = []
|
|
847
|
+
})
|
|
848
|
+
|
|
849
|
+
const tablePositions = {}
|
|
850
|
+
allTexts.forEach(textElement => {
|
|
851
|
+
const text = textElement.textContent.trim()
|
|
852
|
+
if (tableNames.includes(text)) {
|
|
853
|
+
try {
|
|
854
|
+
const rect = textElement.getBoundingClientRect()
|
|
855
|
+
tablePositions[text] = {
|
|
856
|
+
element: textElement,
|
|
857
|
+
x: rect.left + rect.width / 2,
|
|
858
|
+
y: rect.top + rect.height / 2
|
|
859
|
+
}
|
|
860
|
+
} catch (e) {
|
|
861
|
+
// Skip elements that can't provide bounding rect
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
})
|
|
865
|
+
|
|
866
|
+
allTexts.forEach(textElement => {
|
|
867
|
+
const text = textElement.textContent.trim()
|
|
868
|
+
|
|
869
|
+
if (tableNames.includes(text)) {
|
|
870
|
+
textElementsByTable[text].push(textElement)
|
|
871
|
+
return
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
try {
|
|
875
|
+
const rect = textElement.getBoundingClientRect()
|
|
876
|
+
const elementX = rect.left + rect.width / 2
|
|
877
|
+
const elementY = rect.top + rect.height / 2
|
|
878
|
+
|
|
879
|
+
let closestTable = null
|
|
880
|
+
let closestDistance = Infinity
|
|
881
|
+
|
|
882
|
+
Object.entries(tablePositions).forEach(([tableName, pos]) => {
|
|
883
|
+
const distance = Math.sqrt(
|
|
884
|
+
Math.pow(elementX - pos.x, 2) + Math.pow(elementY - pos.y, 2)
|
|
885
|
+
)
|
|
886
|
+
|
|
887
|
+
const isBelow = elementY > pos.y
|
|
888
|
+
const adjustedDistance = isBelow ? distance : distance * 2
|
|
889
|
+
|
|
890
|
+
if (adjustedDistance < closestDistance) {
|
|
891
|
+
closestDistance = adjustedDistance
|
|
892
|
+
closestTable = tableName
|
|
893
|
+
}
|
|
894
|
+
})
|
|
895
|
+
|
|
896
|
+
if (closestTable) {
|
|
897
|
+
textElementsByTable[closestTable].push(textElement)
|
|
898
|
+
}
|
|
899
|
+
} catch (e) {
|
|
900
|
+
// Skip elements that can't provide bounding rect
|
|
901
|
+
}
|
|
902
|
+
})
|
|
903
|
+
|
|
904
|
+
return textElementsByTable
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// Zoom and pan methods
|
|
908
|
+
applyZoom() {
|
|
909
|
+
const svg = this.diagramTarget.querySelector('svg')
|
|
910
|
+
if (svg) {
|
|
911
|
+
svg.style.transform = `scale(${this.zoom}) translate(${this.pan.x}px, ${this.pan.y}px)`
|
|
912
|
+
svg.style.transformOrigin = 'center center'
|
|
913
|
+
svg.style.transition = 'transform 0.1s ease-out'
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
zoomDiagram(delta) {
|
|
918
|
+
this.zoom = Math.max(0.1, Math.min(5, this.zoom + delta))
|
|
919
|
+
this.applyZoom()
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
resetView() {
|
|
923
|
+
this.zoom = 1
|
|
924
|
+
this.pan = { x: 0, y: 0 }
|
|
925
|
+
this.applyZoom()
|
|
926
|
+
this.fitSvgToContainer()
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
startPan(event) {
|
|
930
|
+
if (event.target.hasAttribute('data-column') || event.target.style.cursor === 'pointer') {
|
|
931
|
+
return
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
this.isDragging = true
|
|
935
|
+
this.lastMouse = { x: event.clientX, y: event.clientY }
|
|
936
|
+
this.diagramTarget.style.cursor = 'grabbing'
|
|
937
|
+
event.preventDefault()
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
handlePan(event) {
|
|
941
|
+
if (!this.isDragging) return
|
|
942
|
+
|
|
943
|
+
const dx = (event.clientX - this.lastMouse.x) / this.zoom
|
|
944
|
+
const dy = (event.clientY - this.lastMouse.y) / this.zoom
|
|
945
|
+
|
|
946
|
+
this.pan.x += dx
|
|
947
|
+
this.pan.y += dy
|
|
948
|
+
|
|
949
|
+
this.applyZoom()
|
|
950
|
+
this.lastMouse = { x: event.clientX, y: event.clientY }
|
|
951
|
+
|
|
952
|
+
event.preventDefault()
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
endPan() {
|
|
956
|
+
if (this.isDragging) {
|
|
957
|
+
this.isDragging = false
|
|
958
|
+
this.diagramTarget.style.cursor = 'grab'
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
disconnect() {
|
|
963
|
+
if (this.tooltip && this.tooltip.parentNode) {
|
|
964
|
+
this.tooltip.parentNode.removeChild(this.tooltip)
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
fitSvgToContainer() {
|
|
968
|
+
const svg = this.diagramTarget.querySelector('svg');
|
|
969
|
+
if (!svg) return;
|
|
970
|
+
|
|
971
|
+
// Remove width/height attributes so SVG scales to container
|
|
972
|
+
svg.removeAttribute('width');
|
|
973
|
+
svg.removeAttribute('height');
|
|
974
|
+
|
|
975
|
+
// If you want to be extra safe, set width to 100% and height to auto
|
|
976
|
+
svg.style.width = '100%';
|
|
977
|
+
svg.style.height = 'auto';
|
|
978
|
+
|
|
979
|
+
// Clear any inline overflow styles
|
|
980
|
+
svg.style.overflow = 'visible';
|
|
981
|
+
|
|
982
|
+
// If the SVG has a viewBox, it will automatically scale to fit
|
|
983
|
+
// If not, you can try to set it based on the graph's bounding box
|
|
984
|
+
// (see below for advanced options)
|
|
985
|
+
}
|
|
986
|
+
initializeTableHoverListeners() {
|
|
987
|
+
const svg = this.diagramTarget.querySelector('svg');
|
|
988
|
+
if (!svg) return;
|
|
989
|
+
|
|
990
|
+
svg.querySelectorAll('g.node').forEach(nodeGroup => {
|
|
991
|
+
// Get the table name from <title> or id (for fallback)
|
|
992
|
+
const title = nodeGroup.querySelector('title');
|
|
993
|
+
let tableName = title ? title.textContent.trim() : nodeGroup.id;
|
|
994
|
+
if (!tableName) return;
|
|
995
|
+
|
|
996
|
+
// Find the main shape (polygon or rect) for the table
|
|
997
|
+
const polygon = nodeGroup.querySelector('polygon, rect');
|
|
998
|
+
if (!polygon) return;
|
|
999
|
+
|
|
1000
|
+
// Find the text element with the table name
|
|
1001
|
+
const tableNameText = Array.from(nodeGroup.querySelectorAll('text'))
|
|
1002
|
+
.find(el => el.textContent.trim() === tableName);
|
|
1003
|
+
if (!tableNameText) return; // Skip if no matching text
|
|
1004
|
+
|
|
1005
|
+
// Function to trigger the highlight
|
|
1006
|
+
const highlightTable = () => {
|
|
1007
|
+
// Add bold border class to the node group
|
|
1008
|
+
nodeGroup.classList.add('table-hover-highlight');
|
|
1009
|
+
|
|
1010
|
+
// Highlight all connected edges and columns
|
|
1011
|
+
Object.entries(this.relationshipData).forEach(([edgeId, relationship]) => {
|
|
1012
|
+
if (relationship.source_table === tableName || relationship.target_table === tableName) {
|
|
1013
|
+
const edgeGroup = svg.querySelector(`#${CSS.escape(edgeId)}`);
|
|
1014
|
+
if (edgeGroup) {
|
|
1015
|
+
edgeGroup.classList.add('edge-highlighted', 'animated');
|
|
1016
|
+
const highlightColor = this.getRelationshipGroupColor(relationship);
|
|
1017
|
+
this.highlightSpecificRelationship(relationship, highlightColor);
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
};
|
|
1022
|
+
|
|
1023
|
+
// Function to clear the highlight
|
|
1024
|
+
const clearTableHighlight = () => {
|
|
1025
|
+
nodeGroup.classList.remove('table-hover-highlight');
|
|
1026
|
+
this.clearAllHighlights();
|
|
1027
|
+
};
|
|
1028
|
+
|
|
1029
|
+
// Attach listeners to the polygon
|
|
1030
|
+
polygon.style.cursor = 'pointer';
|
|
1031
|
+
polygon.addEventListener('mouseenter', highlightTable);
|
|
1032
|
+
polygon.addEventListener('mouseleave', clearTableHighlight);
|
|
1033
|
+
|
|
1034
|
+
// Attach listeners to the text
|
|
1035
|
+
tableNameText.style.cursor = 'pointer';
|
|
1036
|
+
tableNameText.addEventListener('mouseenter', highlightTable);
|
|
1037
|
+
tableNameText.addEventListener('mouseleave', clearTableHighlight);
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
downloadPNG() {
|
|
1042
|
+
const svgElement = this.diagramTarget.querySelector('svg');
|
|
1043
|
+
if (!svgElement) return;
|
|
1044
|
+
|
|
1045
|
+
const serializer = new XMLSerializer();
|
|
1046
|
+
let source = serializer.serializeToString(svgElement);
|
|
1047
|
+
|
|
1048
|
+
// Add namespaces if missing
|
|
1049
|
+
if (!source.match(/^<svg[^>]+xmlns="http:\/\/www\.w3\.org\/2000\/svg"/)) {
|
|
1050
|
+
source = source.replace(/^<svg/, '<svg xmlns="http://www.w3.org/2000/svg"');
|
|
1051
|
+
}
|
|
1052
|
+
if (!source.match(/^<svg[^>]+xmlns:xlink="http:\/\/www\.w3\.org\/1999\/xlink"/)) {
|
|
1053
|
+
source = source.replace(/^<svg/, '<svg xmlns:xlink="http://www.w3.org/1999/xlink"');
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
const image = new Image();
|
|
1057
|
+
const svgBlob = new Blob([source], {type: 'image/svg+xml;charset=utf-8'});
|
|
1058
|
+
const url = URL.createObjectURL(svgBlob);
|
|
1059
|
+
|
|
1060
|
+
image.onload = () => {
|
|
1061
|
+
const canvas = document.createElement('canvas');
|
|
1062
|
+
canvas.width = image.width;
|
|
1063
|
+
canvas.height = image.height;
|
|
1064
|
+
const context = canvas.getContext('2d');
|
|
1065
|
+
context.fillStyle = 'white';
|
|
1066
|
+
context.fillRect(0, 0, canvas.width, canvas.height);
|
|
1067
|
+
context.drawImage(image, 0, 0);
|
|
1068
|
+
URL.revokeObjectURL(url);
|
|
1069
|
+
canvas.toBlob((blob) => {
|
|
1070
|
+
const a = document.createElement('a');
|
|
1071
|
+
a.href = URL.createObjectURL(blob);
|
|
1072
|
+
a.download = 'schema_diagram.png';
|
|
1073
|
+
document.body.appendChild(a);
|
|
1074
|
+
a.click();
|
|
1075
|
+
document.body.removeChild(a);
|
|
1076
|
+
}, 'image/png');
|
|
1077
|
+
};
|
|
1078
|
+
image.src = url;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
downloadPDF() {
|
|
1082
|
+
const svgElement = this.diagramTarget.querySelector('svg')
|
|
1083
|
+
if (!svgElement) return
|
|
1084
|
+
|
|
1085
|
+
const serializer = new XMLSerializer()
|
|
1086
|
+
let source = serializer.serializeToString(svgElement)
|
|
1087
|
+
|
|
1088
|
+
// Add namespaces if missing
|
|
1089
|
+
if (!source.match(/^<svg[^>]+xmlns="http:\/\/www\.w3\.org\/2000\/svg"/)) {
|
|
1090
|
+
source = source.replace(/^<svg/, '<svg xmlns="http://www.w3.org/2000/svg"')
|
|
1091
|
+
}
|
|
1092
|
+
if (!source.match(/^<svg[^>]+xmlns:xlink="http:\/\/www\.w3\.org\/1999\/xlink"/)) {
|
|
1093
|
+
source = source.replace(/^<svg/, '<svg xmlns:xlink="http://www.w3.org/1999/xlink"')
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
const image = new Image()
|
|
1097
|
+
const svgBlob = new Blob([source], {type: 'image/svg+xml;charset=utf-8'})
|
|
1098
|
+
const url = URL.createObjectURL(svgBlob)
|
|
1099
|
+
|
|
1100
|
+
image.onload = () => {
|
|
1101
|
+
const canvas = document.createElement('canvas')
|
|
1102
|
+
canvas.width = image.width
|
|
1103
|
+
canvas.height = image.height
|
|
1104
|
+
const context = canvas.getContext('2d')
|
|
1105
|
+
context.fillStyle = 'white'
|
|
1106
|
+
context.fillRect(0, 0, canvas.width, canvas.height)
|
|
1107
|
+
context.drawImage(image, 0, 0)
|
|
1108
|
+
URL.revokeObjectURL(url)
|
|
1109
|
+
|
|
1110
|
+
import('https://cdn.jsdelivr.net/npm/jspdf@2.5.1/+esm').then(({ default: jsPDF }) => {
|
|
1111
|
+
const pdf = new jsPDF({
|
|
1112
|
+
orientation: canvas.width > canvas.height ? 'landscape' : 'portrait',
|
|
1113
|
+
unit: 'px',
|
|
1114
|
+
format: [canvas.width, canvas.height]
|
|
1115
|
+
})
|
|
1116
|
+
pdf.addImage(
|
|
1117
|
+
canvas.toDataURL('image/png'),
|
|
1118
|
+
'PNG',
|
|
1119
|
+
0,
|
|
1120
|
+
0,
|
|
1121
|
+
canvas.width,
|
|
1122
|
+
canvas.height
|
|
1123
|
+
)
|
|
1124
|
+
pdf.save('schema_diagram.pdf')
|
|
1125
|
+
})
|
|
1126
|
+
}
|
|
1127
|
+
image.src = url
|
|
1128
|
+
}
|
|
1129
|
+
}
|