ほぼGPTで遊んだだけ。
作成したツール
コード(html)
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>機能依存関係図</title>
<style>
body {
margin: 0;
overflow: auto;
}
.panel {
border: 1px solid #ccc;
margin: 10px;
padding: 10px;
width: 300px;
position: absolute;
cursor: move;
}
.panel-header {
font-weight: bold;
cursor: pointer;
background-color: #f1f1f1;
padding: 5px;
}
.panel-content {
display: none;
padding: 5px;
}
.control-button, .import-button, .export-button, .import-json-button, .comment-toggle-button {
margin: 10px;
padding: 10px;
cursor: pointer;
color: white;
border: none;
border-radius: 5px;
z-index: 1000;
}
.control-button {
background-color: #4CAF50;
}
.import-button {
background-color: #2196F3;
}
.export-button, .import-json-button {
background-color: #FFA500;
}
.comment-toggle-button {
background-color: #808080;
}
.screen-panel {
background-color: #add8e6;
}
.api-panel {
background-color: #f08080;
}
.batch-panel {
background-color: #ffcc99;
}
.table-panel {
background-color: #90ee90;
}
.external-panel {
background-color: #dda0dd;
}
.chart-container {
position: relative;
display: flex;
justify-content: space-around;
align-items: flex-start;
transform-origin: 0 0;
width: 10000px;
height: 10000px;
border-top: 2px solid #000;
}
.arrow {
position: absolute;
stroke: #000;
fill: none;
marker-end: url(#arrowhead);
}
.highlighted-item {
color: red;
}
.highlighted-arrow {
stroke: red;
}
.header-container {
position: fixed;
top: 0;
left: 0;
width: 100%;
background-color: #fff;
z-index: 1000;
display: flex;
align-items: center;
padding: 10px;
border-bottom: 2px solid #000;
}
.input-field {
margin-left: 10px;
padding: 5px;
}
.comment-bubble {
position: absolute;
background-color: #fff;
border: 1px solid #ccc;
padding: 5px;
display: none;
z-index: 1001;
}
.toggle-active {
background-color: #008000;
}
</style>
</head>
<body>
<div class="header-container">
<div>
<button id="toggleAll" class="control-button">すべて展開/閉じる</button>
<button id="importCsv" class="import-button">CSVインポート</button>
<button id="exportJson" class="export-button">編集エクスポート</button>
<button id="importJson" class="import-json-button">編集インポート</button>
<button id="toggleComments" class="comment-toggle-button">コメント表示</button>
<label for="widthInput">描画領域X(px)</label>
<input type="number" id="widthInput" class="input-field" value="10000">
<label for="heightInput">描画領域Y(px)</label>
<input type="number" id="heightInput" class="input-field" value="10000">
<button id="setDimensions" class="input-field">描画領域設定</button>
</div>
</div>
<div id="contents">
<div id="chart" class="chart-container"></div>
<svg id="arrows" width="100%" height="100%" style="position:absolute; top:0; left:0; pointer-events:none;">
<defs>
<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto" markerUnits="strokeWidth">
<polygon points="0 0, 10 3.5, 0 7" />
</marker>
</defs>
</svg>
</div>
<script>
var screenData = [];
var comments = {};
document.getElementById('importCsv').addEventListener('click', () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.csv';
input.onchange = (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
const text = e.target.result;
screenData = parseCsv(text);
renderChart();
drawArrows();
};
reader.readAsText(file);
}
};
input.click();
});
document.getElementById('setDimensions').addEventListener('click', () => {
const width = document.getElementById('widthInput').value;
const height = document.getElementById('heightInput').value;
const chart = document.getElementById('chart');
chart.style.width = `${width}px`;
chart.style.height = `${height}px`;
});
document.getElementById('exportJson').addEventListener('click', () => {
const state = {
panels: Object.values(panelPositions).map(panel => ({
id: panel.querySelector('.panel-header').textContent,
x: panel.style.left,
y: panel.style.top,
expanded: panel.querySelector('.panel-content').style.display === 'block',
items: Array.from(panel.querySelectorAll('.panel-content input')).map(input => ({
name: input.nextSibling.textContent,
checked: input.checked
})),
comment: comments[panel.querySelector('.panel-header').textContent] || ""
}))
};
const blob = new Blob([JSON.stringify(state)], { type: 'application/json' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = 'exported_content.json';
link.click();
});
document.getElementById('importJson').addEventListener('click', () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
const state = JSON.parse(e.target.result);
renderChart(state);
drawArrows();
};
reader.readAsText(file);
}
};
input.click();
});
document.getElementById('toggleComments').addEventListener('click', (e) => {
const commentBubbles = document.querySelectorAll('.comment-bubble');
const shouldShow = e.target.classList.toggle('toggle-active');
e.target.textContent = shouldShow ? 'コメント非表示' : 'コメント表示';
commentBubbles.forEach(bubble => {
bubble.style.display = shouldShow ? 'block' : 'none';
});
});
function parseCsv(csvText) {
const rows = csvText.split('\n');
const headers = rows[0].split(',');
return rows.slice(1).map(row => {
const values = row.split(',');
return headers.reduce((acc, header, index) => {
acc[header.trim()] = values[index]?.trim() || '';
return acc;
}, {});
});
}
var groupedData;
var panelPositions = {};
var itemPositions = {};
const chartDiv = document.getElementById('chart');
function renderChart(state = null) {
chartDiv.innerHTML = '';
panelPositions = {};
itemPositions = {};
groupedData = screenData.reduce((acc, row) => {
if (!acc[row.機能名]) {
acc[row.機能名] = {
type: row.機能種別,
screenName: row.機能名,
items: []
};
}
acc[row.機能名].items.push({
type: row.機能種別,
screenName: row.機能名,
itemName: row.項目名,
sourceScreen: row.リソース,
sourceItem: row.取得元項目,
resourceType: row.リソースの機能種別,
handoff: row.引渡し
});
return acc;
}, {});
const data = Object.values(groupedData);
data.forEach((screen, index) => {
const panel = document.createElement('div');
panel.className = `panel ${screen.type === '画面' ? 'screen-panel' : screen.type === 'API' ? 'api-panel' : screen.type === 'バッチ' ? 'batch-panel' : screen.type === '外部機能' ? 'external-panel' : 'table-panel'}`;
panel.style.top = state ? state.panels[index].y : `${index * 150 + 150}px`;
panel.style.left = state ? state.panels[index].x : `${index % 2 === 0 ? 100 : 500}px`;
const panelHeader = document.createElement('div');
panelHeader.className = 'panel-header';
panelHeader.textContent = screen.screenName;
panelHeader.addEventListener('click', (e) => {
if (!panel.classList.contains('dragging') && !panel.dataset.moving) {
const content = panel.querySelector('.panel-content');
content.style.display = content.style.display === 'none' ? 'block' : 'none';
drawArrows();
}
});
const commentBubble = document.createElement('div');
commentBubble.className = 'comment-bubble';
commentBubble.contentEditable = true;
commentBubble.style.display = 'none';
if (state && state.panels[index].comment) {
commentBubble.textContent = state.panels[index].comment;
}
comments[screen.screenName] = commentBubble.textContent;
panelHeader.addEventListener('contextmenu', (e) => {
e.preventDefault();
commentBubble.style.top = e.clientY - commentBubble.getBoundingClientRect().top;
commentBubble.style.left = e.clientX - commentBubble.getBoundingClientRect().left;
commentBubble.style.display = 'block';
commentBubble.focus();
});
commentBubble.addEventListener('blur', () => {
comments[screen.screenName] = commentBubble.textContent;
});
const panelContent = document.createElement('div');
panelContent.className = 'panel-content';
screen.items.forEach(item => {
if (!itemPositions[`${screen.screenName}-${item.itemName}`]) {
const itemDiv = document.createElement('div');
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.style.marginRight = '5px';
itemDiv.appendChild(checkbox);
itemDiv.appendChild(document.createTextNode(item.itemName));
itemDiv.dataset.screenName = screen.screenName;
itemDiv.dataset.itemName = item.itemName;
panelContent.appendChild(itemDiv);
itemPositions[`${screen.screenName}-${item.itemName}`] = itemDiv;
checkbox.addEventListener('change', () => {
if (checkbox.checked) {
itemDiv.classList.add('highlighted-item');
highlightRelatedItems(item, {}, true);
} else {
itemDiv.classList.remove('highlighted-item');
highlightRelatedItems(item, {}, false);
}
});
itemDiv.addEventListener('contextmenu', (e) => {
e.preventDefault();
commentBubble.style.top = e.clientY - commentBubble.getBoundingClientRect().top;
commentBubble.style.left = e.clientX - commentBubble.getBoundingClientRect().left;
commentBubble.style.display = 'block';
commentBubble.focus();
});
}
});
if (state) {
const panelState = state.panels.find(p => p.id === screen.screenName);
if (panelState) {
panelContent.style.display = panelState.expanded ? 'block' : 'none';
panelState.items.forEach(itemState => {
const itemDiv = Array.from(panelContent.children).find(div => div.textContent.trim() === itemState.name);
if (itemDiv) {
const checkbox = itemDiv.querySelector('input');
checkbox.checked = itemState.checked;
if (checkbox.checked) {
itemDiv.classList.add('highlighted-item');
highlightRelatedItems(itemState, {}, true);
}
}
});
}
}
panel.appendChild(panelHeader);
panel.appendChild(panelContent);
panel.appendChild(commentBubble);
chartDiv.appendChild(panel);
panelPositions[screen.screenName] = panel;
panel.addEventListener('mousedown', (e) => {
panel.dataset.moving = true;
let shiftX = e.clientX - panel.getBoundingClientRect().left;
let shiftY = e.clientY - panel.getBoundingClientRect().top;
function moveAt(pageX, pageY) {
panel.style.left = pageX - shiftX + 'px';
panel.style.top = pageY - shiftY + 'px';
drawArrows();
}
function onMouseMove(event) {
panel.classList.add('dragging');
moveAt(event.pageX, event.pageY);
}
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', function onMouseUp() {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
panel.classList.remove('dragging');
delete panel.dataset.moving;
});
});
panel.ondragstart = function() {
return false;
};
});
}
renderChart();
function drawArrows() {
const svg = document.getElementById('arrows');
svg.innerHTML = '<defs><marker id="arrowhead" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto" markerUnits="strokeWidth"><polygon points="0 0, 10 3.5, 0 7" /></marker></defs>';
let maxX = 0;
let maxY = 0;
screenData.forEach(row => {
if (
(row.リソース && row.取得元項目 && row.リソース !== row.機能名) ||
(row.引渡し && row.リソース)
) {
const sourceItem = row.取得元項目
? itemPositions[`${row.リソース}-${row.取得元項目}`]
: itemPositions[`${row.機能名}-${row.項目名}`];
const targetItem = row.取得元項目
? itemPositions[`${row.機能名}-${row.項目名}`]
: itemPositions[`${row.リソース}-${row.引渡し}`];
const sourcePanel = row.取得元項目
? panelPositions[row.リソース]
: panelPositions[row.機能名];
const targetPanel = row.取得元項目
? panelPositions[row.機能名]
: panelPositions[row.リソース];
if (sourcePanel && targetPanel) {
let sourceX, sourceY, targetX, targetY;
const sourceRect = sourceItem ? sourceItem.getBoundingClientRect() : sourcePanel.getBoundingClientRect();
const targetRect = targetItem ? targetItem.getBoundingClientRect() : targetPanel.getBoundingClientRect();
const sourceIsVisible = sourceItem ? sourceItem.closest('.panel-content').style.display === 'block' : false;
const targetIsVisible = targetItem ? targetItem.closest('.panel-content').style.display === 'block' : false;
if (sourceIsVisible) {
sourceX = sourceRect.right + window.pageXOffset;
sourceY = sourceRect.top + sourceRect.height / 2 + window.pageYOffset;
} else {
const sourcePanelRect = sourcePanel.getBoundingClientRect();
sourceX = sourcePanelRect.right + window.pageXOffset;
sourceY = sourcePanelRect.top + 20 + window.pageYOffset;
}
if (targetIsVisible) {
targetX = targetRect.left + window.pageXOffset;
targetY = targetRect.top + targetRect.height / 2 + window.pageYOffset;
} else {
const targetPanelRect = targetPanel.getBoundingClientRect();
targetX = targetPanelRect.left + window.pageXOffset;
targetY = targetPanelRect.top + 20 + window.pageYOffset;
}
if (sourceX < targetX) {
sourceX = sourceIsVisible ? sourceRect.right + window.pageXOffset : sourcePanel.getBoundingClientRect().right + window.pageXOffset;
targetX = targetIsVisible ? targetRect.left + window.pageXOffset : targetPanel.getBoundingClientRect().left + window.pageXOffset;
} else {
sourceX = sourceIsVisible ? sourceRect.left + window.pageXOffset : sourcePanel.getBoundingClientRect().left + window.pageXOffset;
targetX = targetIsVisible ? targetRect.right + window.pageXOffset : targetPanel.getBoundingClientRect().right + window.pageXOffset;
}
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', sourceX);
line.setAttribute('y1', sourceY);
line.setAttribute('x2', targetX);
line.setAttribute('y2', targetY);
line.setAttribute('class', 'arrow');
if (
(sourceItem && sourceItem.classList.contains('highlighted-item')) ||
(targetItem && targetItem.classList.contains('highlighted-item'))
) {
line.classList.add('highlighted-arrow');
}
svg.appendChild(line);
maxX = Math.max(maxX, sourceX, targetX);
maxY = Math.max(maxY, sourceY, targetY);
}
}
});
svg.setAttribute('width', maxX + 50);
svg.setAttribute('height', maxY + 50);
}
function highlightRelatedItems(item, searchedMap, highlight) {
if (searchedMap[item.screenName + "-" + item.itemName]) {
return;
}
searchedMap[item.screenName + "-" + item.itemName] = true;
const searchTargetList = screenData.filter(row => row.機能名 === item.screenName && row.項目名 === item.itemName);
searchTargetList.forEach(srow => {
screenData.forEach(row => {
if (
(srow.リソース === row.機能名 && srow.取得元項目 === row.項目名) ||
(srow.リソース === row.機能名 && srow.引渡し === row.項目名) ||
(srow.機能名 === row.リソース && srow.項目名 === row.取得元項目) ||
(srow.機能名 === row.リソース && srow.項目名 === row.引渡し)
) {
const targetItemDiv = itemPositions[`${row.機能名}-${row.項目名}`];
if (targetItemDiv) {
if (highlight) {
targetItemDiv.classList.add('highlighted-item');
const checkbox = targetItemDiv.querySelector('input');
checkbox.checked = true;
} else {
targetItemDiv.classList.remove('highlighted-item');
const checkbox = targetItemDiv.querySelector('input');
checkbox.checked = false;
}
}
highlightRelatedItems({
type: row.機能種別,
screenName: row.機能名,
itemName: row.項目名,
sourceScreen: row.リソース,
sourceItem: row.取得元項目,
resourceType: row.リソースの機能種別,
handoff: row.引渡し
}, searchedMap, highlight);
}
});
});
drawArrows();
}
requestAnimationFrame(drawArrows);
const toggleAllButton = document.getElementById('toggleAll');
toggleAllButton.addEventListener('click', () => {
const panels = document.querySelectorAll('.panel-content');
const allExpanded = Array.from(panels).every(panel => panel.style.display === 'block');
panels.forEach(panel => {
panel.style.display = allExpanded ? 'none' : 'block';
});
drawArrows();
});
window.addEventListener('resize', () => {
requestAnimationFrame(drawArrows);
});
</script>
</body>
</html>
データ(csv)
機能種別,クラス名,機能名,項目種別,項目名,リソースの機能種別,リソース,取得元項目,引渡し
画面,顧客管理系画面,顧客検索画面,-,顧客ID,-,,,
画面,顧客管理系画面,顧客検索画面,-,姓,-,,,
画面,顧客管理系画面,顧客検索画面,-,名,-,,,
画面,顧客管理系画面,顧客検索画面,-,電話番号,-,,,
画面,顧客管理系画面,顧客詳細画面,-,顧客ID,画面,顧客検索画面,顧客ID,
画面,顧客管理系画面,顧客詳細画面,-,顧客ID,API,顧客取得,,顧客ID
画面,顧客管理系画面,顧客詳細画面,-,あああ,API,顧客取得,,ああ
画面,顧客管理系画面,顧客詳細画面,-,あああ,画面,顧客検索画面,ああああ,
画面,顧客管理系画面,顧客詳細画面,-,姓,API,顧客取得,姓,
画面,顧客管理系画面,顧客詳細画面,-,名,API,顧客取得,名,
画面,顧客管理系画面,顧客詳細画面,-,電話番号,API,顧客取得,電話番号,
画面,顧客管理系画面,顧客詳細画面,-,生年月日,API,顧客取得,生年月日,
画面,顧客管理系画面,顧客詳細画面,-,住所,API,顧客取得,住所,
API,顧客情報API,顧客取得,入力,顧客ID,,,,
API,顧客情報API,顧客取得,出力,姓,テーブル,顧客情報,姓,
API,顧客情報API,顧客取得,出力,名,テーブル,顧客情報,名,
API,顧客情報API,顧客取得,出力,電話番号,テーブル,顧客情報,電話番号,
API,顧客情報API,顧客取得,出力,生年月日,テーブル,顧客情報,生年月日,
API,顧客情報API,顧客取得,出力,住所,テーブル,顧客情報,住所,
テーブル,オブジェクト,顧客情報,-,姓,-,-,-,-
テーブル,オブジェクト,顧客情報,-,名,-,-,-,-
テーブル,オブジェクト,顧客情報,-,電話番号,-,-,-,-
テーブル,オブジェクト,顧客情報,-,生年月日,-,-,-,-
テーブル,オブジェクト,顧客情報,-,住所,-,-,-,-
ツールの紹介
データの流れを確認したいときや、項目の削減や名称変更などの影響範囲を見る時につかえるかも?
- htmlファイルで完結しているため、単純にブラウザ起動で使える
- 「csvインポート」でcsvデータを読み取る
※小規模なら手作業でもできるが、大規模なら設計書や仕様書からこうした情報を収集するツールが別途必要そう - 主な編集操作は以下の通り
3-1. 機能単位でパネルになっていて移動できる。
3-2. 機能名部分をクリックすると項目部分の折り畳みと展開を切り替えられる
3-3. 項目名のチェックボックスをクリックすると関連のある項目を探索し全てチェック状態にする
3-4. 矢印の描画は基本的には項目対項目。関連先に該当項目が見つからない場合は機能名に線をつなげるようにした。
3-5. コメント機能。パネルに右クリックでコメントを付けることができる。 - 「編集エクスポート」ではパネルの配置やチェック状態を保存した結果をjson形式で出力する
- 「編集インポート」では上記エクスポートを取り込める。ただし編集データだけでは復元できないため、同じデータでcsvインポートしてから編集インポートする必要がある
※ここは余力があったら改善したい
作成過程
想定よりだいぶチャットが長期化したのですべては載せられないが一部参考として記載する。
- 初期状態
GPTに投げたメッセージ
最終的に実施したいことは、仕様に変更を加えた時の影響調査を容易にすること。
画面はブラウザでの動作を想定。業務処理はWEBAPIで実装しているとする。
データはRDBで扱っている。
この時、項目名を変えたり、追加や削除を行ったときにどこに影響があるかを調査する手助けになるツールが欲しい。
応答
この段階ではアプローチの方法の提案まででコードはあくまでサンプルレベル。
仕様変更時の影響調査を支援するツールを作成するためには、データの依存関係を管理し、変更がどの部分に影響を与えるかを可視化する仕組みが必要です。ここでは、ブラウザベースで動作するツールのアイデアをいくつか提案します。...
コード
// Expressを使った簡単なサーバーサイドAPIの例
const express = require('express');
const app = express();
const port = 3000;
// 仮の依存関係データ(データベース接続を想定)
const dependencyData = {
"table_a.column_x": ["screen_1", "api_2"],
"table_b.column_y": ["screen_3", "api_1"]
};
// 項目名を指定して依存関係を返すAPI
app.get('/api/dependencies', (req, res) => {
const itemName = req.query.item;
if (dependencyData[itemName]) {
res.json({ dependencies: dependencyData[itemName] });
} else {
res.status(404).send('No dependencies found');
}
});
app.listen(port, () => {
console.log(`Dependency analysis API running at http://localhost:${port}`);
});
2. 仕様の具体化
仕様を具体的に示すことで期待の結果が得られないかを試していく
2-1. 入力データのイメージとアウトプットのイメージを伝える
GPTに投げたメッセージ
htmlとjsを使うことと、元データのjsonはVBAまたはpythonで別途準備する前提はそのままとします。
出力イメージが少し違うのでいったん、これまでの内容を破棄。
1枚目のようなデータから、図として2枚目のようなものを作成するように変更してください。
四角いパネルは折り畳みが可能として、クリックで切り替える。展開した状態だと項目名まで見える。
GPTに送った画像
インプットイメージ
アウトプットイメージ
GPTの回答
コード
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>画面・項目依存関係図</title>
<style>
.panel {
border: 1px solid #ccc;
margin: 10px;
padding: 10px;
width: 300px;
}
.panel-header {
font-weight: bold;
cursor: pointer;
background-color: #f1f1f1;
padding: 5px;
}
.panel-content {
display: none;
padding: 5px;
}
.control-button {
margin: 10px;
padding: 10px;
cursor: pointer;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 5px;
}
</style>
</head>
<body>
<button id="toggleAll" class="control-button">すべて展開/閉じる</button>
<div id="chart"></div>
<script>
// JSONデータをJavaScript内で直接定義
const data = [
{
"class": "顧客管理",
"screens": [
{
"screenName": "顧客検索画面",
"items": ["姓", "名", "電話番号"]
},
{
"screenName": "顧客詳細画面",
"items": ["姓", "名", "電話番号", "生年月日", "住所"]
}
]
}
];
// HTMLに画面と項目のパネルを生成
const chartDiv = document.getElementById('chart');
data.forEach(system => {
system.screens.forEach(screen => {
// パネル作成
const panel = document.createElement('div');
panel.className = 'panel';
// パネルヘッダー(画面名)
const panelHeader = document.createElement('div');
panelHeader.className = 'panel-header';
panelHeader.textContent = screen.screenName;
panelHeader.addEventListener('click', () => {
const content = panel.querySelector('.panel-content');
content.style.display = content.style.display === 'none' ? 'block' : 'none';
});
// パネルコンテンツ(項目名)
const panelContent = document.createElement('div');
panelContent.className = 'panel-content';
screen.items.forEach(item => {
const itemDiv = document.createElement('div');
itemDiv.textContent = item;
panelContent.appendChild(itemDiv);
});
// パネルにヘッダーとコンテンツを追加
panel.appendChild(panelHeader);
panel.appendChild(panelContent);
chartDiv.appendChild(panel);
});
});
// すべてのパネルを展開/閉じるボタンの動作
const toggleAllButton = document.getElementById('toggleAll');
toggleAllButton.addEventListener('click', () => {
const panels = document.querySelectorAll('.panel-content');
const allExpanded = Array.from(panels).every(panel => panel.style.display === 'block');
panels.forEach(panel => {
panel.style.display = allExpanded ? 'none' : 'block';
});
});
</script>
</body>
</html>
動作
2-2.さらなる具体化
仕様を具体的に示すとある程度期待の結果が得られそうに見えた。
ここから、依存間の線での描画やパネルの移動などを指示して少しずつ具体化していった。
やり取りがあまりに多いので添付はここまでとするが、気づきを記載する。
・言葉だけで説明が難しい場合、図を作成して説明すると理解してくれる。とくにUIのイメージなどが画像で説明したほうがいい。
・想定した動作にならなかったとき、こちらで原因を特定してコード上の修正箇所を伝えなくても、期待動作になってない部分を説明するだけでも適切に直してくれる。
・原因箇所が複雑な箇所だとほとんど改善しない場合もある。以下のようにするのがよさそう。
①とりあえず動作を基準にNGを伝えてみる。
②自身でコードを確認して修正箇所を指定。
③それでも期待通りの結果にならなかったら自力で修正。
・再帰的関数は苦手かも?探索処理は自作してから処理の冗長さの排除をGPTに依頼した。
・800行を超えたあたりから生成がうまく行われなくなり「編集できませんでした」と表示されるようになった。
・行数が増えてからはまとめられそうな類似処理が生成されたり、同じ用途の変数が複数宣言されるようになった。
・「変更を加える部分だけ生成してください。」と指示することで機能をつぎ足して行くことはできた。with canvasの共同編集エリアは使わないのでこの段階デメリットが薄くなった。
・通常1ファイルがここまで大きくなることは少ないので今回のように1ファイルにすべてまとめたいということがなければ利用に問題はなさそう。
最後に
理想の形になるまで1.5日も費やしてしまった。
とはいえ、よほどスキルが高い人じゃない限り、自力で作るよりは速そうに思う。
もう少しシンプルな仕様のものなら量産できそう。
つぎ足しにはなるが拡張したので添付。
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>機能依存関係図</title>
<style>
/* ヘッダ */
.header-container {
top: 0;
left: 0;
width: 100%;
background-color: #fff;
z-index: 1000;
display: flex;
align-items: center;
padding: 2px;
border-bottom: 1px solid #000;
}
.control-button, .import-button, .export-button, .import-json-button, .comment-toggle-button {
margin: 2px;
padding: 2px;
cursor: pointer;
color: white;
border: none;
border-radius: 5px;
z-index: 1000;
}
.control-button {
background-color: #4CAF50;
}
.import-button {
background-color: #2196F3;
}
.export-button, .import-json-button {
background-color: #FFA500;
}
.comment-toggle-button {
background-color: #808080;
}
.slider-container {
display:inline;
align-items: center;
gap: 10px;
}
input[type="range"] {
-webkit-appearance: none;
appearance: none;
width: 150px;
height: 5px;
background: #ddd;
outline: none;
border-radius: 5px;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 15px;
height: 15px;
background: #007bff;
border-radius: 50%;
cursor: pointer;
}
input[type="range"]::-moz-range-thumb {
width: 15px;
height: 15px;
background: #007bff;
border-radius: 50%;
cursor: pointer;
}
input[type="number"] {
width: 50px;
text-align: center;
}
/* コンテンツ */
.contents-container{
background-color: #cbe7ce;
max-height: 92vh;
overflow: auto;
}
#contentsParent{
width: 10000px;
height: 10000px;
position: relative;
transform-origin: top left;
background-color: #e6ffe8;
}
#contentsParent > *{
height: 100%;
width: 100%;
position: absolute;
}
.chart-container {
display: flex;
justify-content: space-around;
align-items: flex-start;
transform-origin: 0 0;
}
#commentPanelsArea{
pointer-events:none;
}
#commentPanelsArea> * {
pointer-events: auto;
}
#arrows{
pointer-events:none;
}
#arrows> * {
pointer-events: auto;
}
#chart{
pointer-events:none;
}
#chart> * {
pointer-events: auto;
}
#controlPointsSvg{
pointer-events:none
}
#controlPointsSvg> * {
pointer-events: auto;
}
/* コンテンツ->パネル */
.panel {
border: 1px solid #ccc;
padding: 10px;
width: 300px;
position: absolute;
cursor: move;
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
font-weight: bold;
background-color: #f1f1f1;
padding: 5px;
}
.panel-class {
font-size: 0.8em;
color: #555;
margin-top: 5px;
}
.toggle-button {
cursor: pointer;
background-color: transparent;
border: none;
font-size: 1.2em;
}
.panel-content {
display: none;
padding: 5px;
}
.screen-panel {
background-color: #add8e6;
}
.api-panel {
background-color: #f08080;
}
.batch-panel {
background-color: #ffcc99;
}
.table-panel {
background-color: #90ee90;
}
.external-panel {
background-color: #dda0dd;
}
.item-hidden {
display: none;
}
.highlighted-item {
color: red;
}
.comment-bubble {
position: absolute;
background-color: #fff;
border: 1px solid #ccc;
padding: 5px;
display: none;
z-index: 1001;
width: 90%;
}
.selection-box {
position: absolute;
border: 2px dashed red;
background-color: rgba(173, 216, 230, 0.3);
pointer-events: none;
}
.bookMarked-panel {
border: 5px solid rgb(255, 179, 0);
}
.selected-panel {
border: 6px solid red;
}
/* 制御点が選択されたときのスタイル */
.selected-control-point {
fill: red;
}
/* 右クリック */
#customContextMenu button {
width: 100%;
padding: 8px;
text-align: left;
cursor: pointer;
}
#customContextMenu button:hover {
background-color: #f1f1f1;
}
/* コメントパネルのスタイル */
.comment-panel {
position: absolute;
width: 200px;
padding: 5px;
border: 1px solid #ccc;
background-color: #ffffff;
border-radius: 4px;
cursor: move;
}
/* ヘッダー部分 */
.panel-header {
display: flex;
align-items: center;
justify-content: space-between;
}
/* テキストエリア */
.comment-textarea {
flex: 1;
resize: none;
overflow: hidden;
font-size: 14px;
line-height: 1.2;
padding: 5px;
box-sizing: border-box;
}
/* 色変更ボタンと削除ボタンのコンテナ */
.panel-controls {
display: flex;
align-items: center;
}
/* 色変更ボタン */
.color-change-button {
width: auto;
cursor: pointer;
margin-left: 5px;
}
/* 削除ボタン */
.delete-button {
cursor: pointer;
margin-left: 5px;
}
/* 右下のリサイズハンドル */
.resize-handle {
width: 10px;
height: 10px;
position: absolute;
bottom: 5px;
right: 5px;
cursor: nwse-resize;
}
/* テキストエリアの下部に余白を追加 */
.bottom-padding {
height: 10px;
}
/* フッター(選択ツール) */
#selectionTool{
display: none; position: fixed; bottom: 0; left: 0; width: 100%; background-color: #fff; border-top: 1px solid #ccc; z-index: 1001; height: 400px;
}
#selectionTool > * {
font-size: 12px;
}
#selectionTool button {
color: white;
padding: 1px;
}
#footer{
position: fixed; bottom: 0; left: 0; width: 100%; background-color: #f1f1f1; text-align: center; border-top: 1px solid #ccc;overflow: hidden;
}
</style>
</head>
<body>
<div class="header-container">
<div>
<button id="toggleAll" class="control-button">すべて展開/閉じる</button>
<button id="importCsv" class="import-button">CSVインポート</button>
<button id="exportJson" class="export-button">編集エクスポート</button>
<button id="importJson" class="import-json-button">編集インポート</button>
<button id="toggleComments" class="comment-toggle-button">コメント表示</button>
<label for="drawModeSelect">描画モード</label>
<select id="drawModeSelect" class="input-field">
<option value="無効">無効</option>
<option value="全体">全体</option>
<option value="対象指定">対象指定</option>
</select>
<label for="itemDisplayModeSelect">項目表示モード</label>
<select id="itemDisplayModeSelect" class="input-field">
<option value="指定項目">指定項目</option> <!-- 指定された項目のみ表示するモード -->
<option value="全項目">全項目</option> <!-- 既存の全項目を表示するモード -->
</select>
<label for="widthInput">描画領域X(px)</label>
<input type="number" id="widthInput" class="input-field" value="10000">
<label for="heightInput">描画領域Y(px)</label>
<input type="number" id="heightInput" class="input-field" value="10000">
<button id="setDimensions" class="input-field">描画領域設定</button>
<label for="displayScale">表示倍率:</label>
<div class="slider-container">
<button id="displayScaleMinus">-</button>
<input type="range" id="displayScaleSlider" min="10" max="100" value="100" />
<button id="displayScalePlus">+</button>
<input type="number" id="displayScaleValue" value="100" min="0" max="100" />
</div>
</div>
</div>
<!--コンテンツ-->
<div id="contents" class="contents-container">
<!--コンテンツ部分のサイズのマスタになる要素。子要素はこれにたいして100%-->
<div id="contentsParent">
<div id="commentPanelsArea"></div>
<div id="chart" class="chart-container"></div>
<svg id="arrows">
</svg>
<!-- 制御点用のSVG -->
<svg id="controlPointsSvg">
<defs><marker id="arrowhead" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto" markerUnits="strokeWidth"><polygon points="0 0, 10 3.5, 0 7" /></marker></defs>
<defs><marker id="arrowheadR" markerWidth="10" markerHeight="7" refX="0" refY="3.5" orient="auto" markerUnits="strokeWidth"><polygon points="10 0, 0 3.5, 10 7" /></marker></defs>
</svg>
<!-- 詳細表示用ポップアップ -->
<div id="detailPopup" style="display: none; position: absolute; width: 1200px; max-height: 800px; background-color: #fff; border: 1px solid #ccc; padding: 10px; z-index: 1002;">
<button id="closeDetailPopup" style="position: absolute; top: 5px; right: 5px;">閉じる</button>
<h3 id="popupScreenName">機能名: </h3>
<h4 id="popupClassName">クラス名: </h4>
<div style="max-height: 600px; overflow-y: auto;">
<table border="1" style="width: 100%; border-collapse: collapse;">
<thead style="position: sticky; top: 0; background-color: #fff; z-index: 1;">
<tr>
<th>項目種別</th>
<th>項目名</th>
<th>リソースの機能種別</th>
<th>リソース</th>
<th>取得元項目</th>
<th>引渡し</th>
<th>項目表示</th>
</tr>
</thead>
<tbody id="popupTableBody">
<!-- 内容はJavaScriptで動的に挿入 -->
</tbody>
</table>
<h4>参照先機能一覧</h4>
<button id="selectAllDestination">まとめて選択</button>
<div style="max-height: 300px; overflow-y: auto;">
<table border="1" style="width: 100%; border-collapse: collapse;">
<thead style="position: sticky; top: 0; background-color: #fff; z-index: 1;">
<tr>
<th>機能種別</th>
<th>クラス名</th>
<th>機能名</th>
<th>入出力</th>
</tr>
</thead>
<tbody id="destinationTableBody">
<!-- 参照先機能一覧がここに動的に挿入される -->
</tbody>
</table>
</div>
<h4>参照元機能一覧</h4>
<button id="selectAllReference">まとめて選択</button>
<div style="max-height: 300px; overflow-y: auto;">
<table border="1" style="width: 100%; border-collapse: collapse;">
<thead style="position: sticky; top: 0; background-color: #fff; z-index: 1;">
<tr>
<th>機能種別</th>
<th>クラス名</th>
<th>機能名</th>
<th>入出力</th>
</tr>
</thead>
<tbody id="referenceTableBody">
<!-- 参照元機能一覧がここに動的に挿入される -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- フッターとして△ボタンを追加 -->
<div id="footer">
<button id="toggleSelectionTool" style="font-size: 15px;">△</button>
</div>
<!-- 選択ツールのUI -->
<div id="selectionTool">
<div id="selectionToolHeader" style="background-color: #ccc; cursor: ns-resize; text-align: center;">
<button id="closeSelectionTool" style="color: #000;">▽</button>
</div>
<!-- モード選択 -->
<div >
<label for="modeToggle">モード:</label>
<select id="modeToggle">
<option value="panel">パネル</option>
<option value="item">項目</option>
</select>
<button id="filterButton" onclick="doFilter()" style="background-color: #4CAF50;">フィルタ</button>
<button id="filterButton" style="background-color: #4CAF50;;">クリア</button>
<label for="modeToggle">まとめて操作:</label>
<button id="filterButton" onclick="doSelectAllOn()" style="background-color: #d01919;">選択on</button>
<button id="filterButton" onclick="doSelectAllOff()" style="background-color: #d01919;">選択off</button>
<button id="filterButton" onclick="doEnableLineAllOn()" style="background-color: #194dd0;">線描画on</button>
<button id="filterButton" onclick="doEnableLineAllOff()" style="background-color: #194dd0;">線描画off</button>
<button id="filterButton" onclick="doVisibleAllOn()" style="background-color: #19d02e;">非表示on</button>
<button id="filterButton" onclick="doVisibleAllOFF()" style="background-color: #19d02e;">非表示off</button>
<button id="filterButton" onclick="doBookMarkAllOn()" style="background-color: #e2890c;">★on</button>
<button id="filterButton" onclick="doBookMarkAllOff()" style="background-color: #e2890c;">★off</button>
<button id="filterButton" onclick="doItemVisibleAllOn()" style="background-color: #008000;" class="toggle-tool-item">項目表示on</button>
<button id="filterButton" onclick="doItemVisibleAllOff()" style="background-color: #008000; " class="toggle-tool-item">項目表示off</button>
</div>
<!-- 横断検索部分 -->
<div>
<label for="crossSearch">横断検索:</label>
<input type="text" id="crossSearch" style="width: 200px;">
<input type="checkbox" id="filterFeatureType">機能種別
<input type="checkbox" id="filterClassName">クラス名
<input type="checkbox" id="filterFeatureName">機能名
<input type="checkbox" id="searchComment"><span>コメント</span>
<input type="checkbox" id="filterItemType" class="toggle-tool-item"><span class="toggle-tool-item">項目種別</span>
<input type="checkbox" id="filterItemName" class="toggle-tool-item"><span class="toggle-tool-item">項目名</span>
<input type="checkbox" id="filterResourceType" class="toggle-tool-item"><span class="toggle-tool-item">リソースの機能種別</span>
<input type="checkbox" id="filterResource" class="toggle-tool-item"><span class="toggle-tool-item">リソース</span>
<input type="checkbox" id="filterSourceItem" class="toggle-tool-item"><span class="toggle-tool-item">取得元項目</span>
<input type="checkbox" id="filterHandoff" class="toggle-tool-item"><span class="toggle-tool-item">引渡し</span>
</div>
<!-- フィルタ部分: 横に並べたテキストボックス -->
<div>
<label for="filterTextFeatureType">フィルタ:</label>
<input type="text" id="filterTextFeatureType" style="width: 130px;" placeholder="機能種別">
<input type="text" id="filterTextClassName" style="width: 130px;" placeholder="クラス名">
<input type="text" id="filterTextFeatureName" style="width: 130px;" placeholder="機能名">
<input type="text" id="filterTextComment" style="width: 130px;" placeholder="コメント">
<input type="text" id="filterTextItemType" style="width: 130px;" placeholder="項目種別" class="toggle-column">
<input type="text" id="filterTextItemName" style="width: 130px;" placeholder="項目名" class="toggle-column">
<input type="text" id="filterTextResourceType" style="width: 130px;" placeholder="リソースの機能種別" class="toggle-column">
<input type="text" id="filterTextResource" style="width: 130px;" placeholder="リソース" class="toggle-column">
<input type="text" id="filterTextSourceItem" style="width: 130px;" placeholder="取得元項目" class="toggle-column">
<input type="text" id="filterTextHandoff" style="width: 130px;" placeholder="引渡し" class="toggle-column">
<label class="toggle-tool-item">項目表示:</label>
<select id="filterItemHidden" class="toggle-tool-item">
<option value="none">--</option>
<option value="false">□</option>
<option value="true">☑</option>
</select>
<label>選択:</label>
<select id="filterSelected">
<option value="none">--</option>
<option value="false">□</option>
<option value="true">☑</option>
</select>
<label>線描画:</label>
<select id="filterEnableLine">
<option value="none">--</option>
<option value="false">□</option>
<option value="true">☑</option>
</select>
<label>非表示:</label>
<select id="filterHidden">
<option value="none">--</option>
<option value="false">□</option>
<option value="true">☑</option>
</select>
<label>★:</label>
<select id="filterBookMark">
<option value="none">--</option>
<option value="false">□</option>
<option value="true">☑</option>
</select>
</div>
<!-- テーブル表示部分 -->
<div style="overflow-y: auto; height: calc(100% - 120px);">
<table border="1" style="width: 100%; border-collapse: collapse;">
<thead style="position: sticky; top: 0; background-color: #fff; z-index: 1;">
<tr id="panelModeHeader">
<th>機能種別</th>
<th>クラス名</th>
<th>機能名</th>
<th class="toggle-column-reverse" width="200px">コメント</th>
<th class="toggle-column">項目種別</th>
<th class="toggle-column">項目名</th>
<th class="toggle-column">リソースの機能種別</th>
<th class="toggle-column">リソース</th>
<th class="toggle-column">取得元項目</th>
<th class="toggle-column">引渡し</th>
<th class="toggle-column">項目表示</th>
<th>選択</th>
<th>線描画</th>
<th>非表示</th>
<th>★</th>
<th>アクション</th>
</tr>
</thead>
<tbody id="selectionTableBody"></tbody>
</table>
</div>
</div>
<!-- 右クリックメニューのHTML -->
<div id="customContextMenu" style="display:none; position:absolute; background-color:white; border:1px solid black; z-index:10000;">
<button id="moveHereButton">ここにまとめて移動</button>
<button id="gatherHereButton">ここに集める</button>
<button id="clearSelectionButton">選択をすべてクリア</button>
<button id="addCommentPanelButton">コメントパネルを追加</button>
</div>
<!-- コメントパネルのテンプレート -->
<div id="commentPanelTemplate" class="comment-panel" style="display: none;">
<div class="panel-header">
<textarea class="comment-textarea" style="resize: none; overflow: hidden;"></textarea>
<div class="panel-controls">
<input type="color" id="commentColorPicker" class="color-change-button">
<div style="display: flex;flex-direction:column;">
<button id="bringToFrontButton" class="">↑</button>
<button id="sendToBackButton" class="">↓</button>
</div>
<button id="deleteCommentButton" class="delete-button">×</button>
</div>
</div>
<div class="bottom-padding"></div>
<div class="resize-handle"></div>
</div>
<script>
/* 全体で使う変数 */
/** csvを読み込んで取得するデータ */
var screenData = [];
/** パネルのdomを保持 */
var panelPositions = {};
/** パネル上の項目のdomを保持 */
var itemPositions = {};
/** コメントパネルの情報を保持 */
let commentPanels = [];
/** パネル間を曲げるための制御点をの情報を保持 */
var controlPoints = {};
/** パネル間を曲げるための制御点をのdomを保持 */
var controlPointsPositions = {
sourceToTarget : [],
targetToSource : []
}
/** 依存関係を示す線のdomを保持 */
var arrowPositions = {
sourceToTarget : [],
targetToSource : []
};
var selectedPanels = new Set();
var selectedControlPoints = new Set();
/** 実行中の制御情報 */
var appControlInfo = {
lineDrawMode :'無効',
itemDisplayMode :'指定項目',
dimensions :{x:10000,y:10000},
scale:1
};
/* DOM */
const dom_chart = document.getElementById('chart');
const dom_contents = document.getElementById('contents');
const dom_contentsParent = document.getElementById('contentsParent');
const dom_commentPanelsArea = document.getElementById('commentPanelsArea');
const dom_arrows = document.getElementById('arrows');
const dom_toggleAll = document.getElementById('toggleAll');
const dom_displayScaleSlider = document.getElementById('displayScaleSlider');
const dom_displayScaleValue = document.getElementById('displayScaleValue');
const dom_displayScalePlus = document.getElementById('displayScalePlus');
const dom_displayScaleMinus = document.getElementById('displayScaleMinus');
const dom_controlPointsSvg = document.getElementById('controlPointsSvg');
const dom_drawModeSelect = document.getElementById('drawModeSelect');
/* csvインポート */
document.getElementById('importCsv').addEventListener('click', () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.csv';
input.onchange = (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
const text = e.target.result;
screenData = parseCsv(text);
renderChart();
requestAnimationFrame(()=>drawArrows());
};
reader.readAsText(file);
}
};
input.click();
});
function parseCsv(csvText) {
const rows = csvText.split('\n');
const headers = rows[0].split(',');
return rows.slice(1).map(row => {
const values = row.split(',');
return headers.reduce((acc, header, index) => {
acc[header.trim()] = values[index]?.trim() || '';
return acc;
}, {});
});
}
document.getElementById('setDimensions').addEventListener('click', () => {
const width = document.getElementById('widthInput').value;
const height = document.getElementById('heightInput').value;
contentsParent.style.width = `${width}px`;
contentsParent.style.height = `${height}px`;
appControlInfo.dimensions.x = width;
appControlInfo.dimensions.x = height;
});
document.getElementById('exportJson').addEventListener('click', () => {
const blob = new Blob([JSON.stringify({screenData : screenData, groupedData : groupedData,controlPoints:controlPoints,commentPanels:commentPanels})], { type: 'application/json' });
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = 'exported_content.json';
link.click();
});
document.getElementById('importJson').addEventListener('click', () => {
const input = document.createElement('input');
input.type = 'file';
input.accept = '.json';
input.onchange = (event) => {
const file = event.target.files[0];
if (file) {
const reader = new FileReader();
reader.onload = (e) => {
const state = JSON.parse(e.target.result);
renderChart(state);
requestAnimationFrame(()=>drawArrows());
};
reader.readAsText(file);
}
};
input.click();
});
document.getElementById('toggleComments').addEventListener('click', (e) => {
const commentBubbles = document.querySelectorAll('.comment-bubble');
const shouldShow = e.target.classList.toggle('toggle-active');
e.target.textContent = shouldShow ? 'コメント非表示' : 'コメント表示';
commentBubbles.forEach(bubble => {
bubble.style.display = shouldShow ? 'block' : 'none';
});
});
function parseCsv(csvText) {
const rows = csvText.split('\n');
const headers = rows[0].split(',');
return rows.slice(1).map(row => {
const values = row.split(',');
return headers.reduce((acc, header, index) => {
acc[header.trim()] = values[index]?.trim() || '';
return acc;
}, {});
});
}
/* コンテンツ部分に機能単位でパネルを描画する */
function renderChart(state = null) {
dom_chart.innerHTML = '';
panelPositions = {};
itemPositions = {};
controlPoints = {};
controlPointsPositions = {
sourceToTarget : [],
targetToSource : []
};
screenData = state ? state.screenData : screenData;
groupedData = state ? state.groupedData : screenData.reduce((acc, row) => {
if (!acc[row.機能名]) {
acc[row.機能名] = {
type: row.機能種別,
screenName: row.機能名,
className: row.クラス名,
isSelected:false,
isEnableLine:false,
isHidden:false,
isPanelOpen:false,
positionX:0,
positionY:0,
bookMark:false,
comment:"",
items: []
};
}
acc[row.機能名].items.push({
type: row.機能種別,
screenName: row.機能名,
itemType: row.項目種別,
itemName: row.項目名,
sourceScreen: row.リソース,
sourceItem: row.取得元項目,
resourceType: row.リソースの機能種別,
handoff: row.引渡し,
isSelected:false,
isVisible:false
});
return acc;
}, {});
const data = Object.values(groupedData);
data.forEach((screen, index) => {
const panel = document.createElement('div');
panel.className = `panel ${screen.type === '画面' ? 'screen-panel' : screen.type === 'API' ? 'api-panel' : screen.type === 'バッチ' ? 'batch-panel' : screen.type === '外部機能' ? 'external-panel' : 'table-panel'}`;
panel.style.top = state ? groupedData[screen.screenName].positionY : `${index * 150 + 150}px`;
panel.style.left = state ? groupedData[screen.screenName].positionX : `${index % 2 === 0 ? 100 : 500}px`;
if(!state){
groupedData[screen.screenName].positionX = panel.style.left;
groupedData[screen.screenName].positionY = panel.style.top;
}
if(screen.isSelected){
panel.classList.add('selected-panel');
selectedPanels.add(panel);
}
if(screen.bookMark){
panel.classList.add('bookMarked-panel');
}
panel.style.display = screen.isHidden ? 'none' : 'block';
const panelHeader = document.createElement('div');
panelHeader.className = 'panel-header';
const headerTitle = document.createElement('span');
headerTitle.textContent = screen.screenName;
const copyIconScreenName = document.createElement('span');
copyIconScreenName.className = 'clipboard-icon';
copyIconScreenName.textContent = '📋';
copyIconScreenName.addEventListener('click', () => {
navigator.clipboard.writeText(screen.screenName);
});
const detailIconScreenName = document.createElement('span');
detailIconScreenName.className = 'detail-icon';
detailIconScreenName.textContent = '🔍';
detailIconScreenName.addEventListener('click', () => {
showDetailPopup(screen, panel);
});
const classLabel = document.createElement('div');
classLabel.className = 'panel-class';
classLabel.textContent = screen.className;
const copyIconClassName = document.createElement('span');
copyIconClassName.className = 'clipboard-icon';
copyIconClassName.textContent = '📋';
copyIconClassName.addEventListener('click', () => {
navigator.clipboard.writeText(screen.className);
});
classLabel.appendChild(copyIconClassName);
const toggleButton = document.createElement('button');
toggleButton.className = 'toggle-button';
toggleButton.textContent = '△';
toggleButton.addEventListener('click', (e) => {
const content = panel.querySelector('.panel-content');
content.style.display = content.style.display === 'none' ? 'block' : 'none';
toggleButton.textContent = content.style.display === 'none' ? '△' : '▽';
screen.isPanelOpen = content.style.display === 'none' ? false : true;
requestAnimationFrame(()=>drawArrows([screen.screenName]));
});
// パネルのタイトル部分のセレクトボックスの追加とイベント設定
const headerSelectBox = document.createElement('select');
headerSelectBox.style.marginRight = '5px';
headerSelectBox.style.display = appControlInfo.lineDrawMode === '対象指定' ? 'block' : 'none';
// セレクトボックスの選択肢を追加
const optionNone = document.createElement('option');
optionNone.value = 'none';
optionNone.textContent = ' ';
headerSelectBox.appendChild(optionNone);
const optionMutual = document.createElement('option');
optionMutual.value = 'mutual'; // 両想いのイメージ
optionMutual.textContent = '><';
headerSelectBox.appendChild(optionMutual);
const optionOneway = document.createElement('option');
optionOneway.value = 'oneway'; // 一方通行のイメージ
optionOneway.textContent = '<>';
headerSelectBox.appendChild(optionOneway);
// groupedDataからの値をセレクトボックスに反映
if (groupedData[screen.screenName].arrowDirection) {
headerSelectBox.value = groupedData[screen.screenName].arrowDirection;
} else {
headerSelectBox.value = 'none'; // デフォルト値を設定
}
// セレクトボックスのイベントリスナー
headerSelectBox.addEventListener('change', () => {
groupedData[screen.screenName].arrowDirection = headerSelectBox.value;
requestAnimationFrame(()=>drawArrows([screen.screenName]));
});
panelHeader.appendChild(headerSelectBox);
panelHeader.appendChild(headerTitle);
panelHeader.appendChild(copyIconScreenName);
panelHeader.appendChild(detailIconScreenName);
panelHeader.appendChild(toggleButton);
const commentBubble = document.createElement('div');
commentBubble.className = 'comment-bubble';
commentBubble.contentEditable = true;
commentBubble.style.display = 'none';
commentBubble.textContent = groupedData[screen.screenName].comment;
groupedData[screen.screenName].comment = commentBubble.textContent;
panelHeader.addEventListener('contextmenu', (e) => {
e.preventDefault();
commentBubble.style.top = e.clientY - commentBubble.getBoundingClientRect().top;
commentBubble.style.left = e.clientX - commentBubble.getBoundingClientRect().left;
commentBubble.style.display = 'block';
commentBubble.focus();
});
commentBubble.addEventListener('blur', () => {
groupedData[screen.screenName].comment = commentBubble.textContent;
});
const panelContent = document.createElement('div');
panelContent.className = 'panel-content';
panelContent.style.display = groupedData[screen.screenName].isPanelOpen ? 'block' : 'none';
screen.items.forEach(item => {
if (!itemPositions[`${screen.screenName}【画面-項目】${item.itemName}`]) {
const itemDiv = document.createElement('div');
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.style.marginRight = '5px';
itemDiv.appendChild(checkbox);
itemDiv.appendChild(document.createTextNode(item.itemName));
itemDiv.dataset.screenName = screen.screenName;
itemDiv.dataset.itemName = item.itemName;
if(itemDisplayModeSelect.value === "指定項目" && !item.isVisible){
itemDiv.classList.add("item-hidden");
}
panelContent.appendChild(itemDiv);
checkbox.checked = item.isSelected;
itemPositions[`${screen.screenName}【画面-項目】${item.itemName}`] = itemDiv;
checkbox.addEventListener('change', () => {
if (checkbox.checked) {
itemDiv.classList.add('highlighted-item');
item.isSelected = true;
} else {
itemDiv.classList.remove('highlighted-item');
item.isSelected = false
}
});
itemDiv.addEventListener('contextmenu', (e) => {
e.preventDefault();
commentBubble.style.top = e.clientY - commentBubble.getBoundingClientRect().top;
commentBubble.style.left = e.clientX - commentBubble.getBoundingClientRect().left;
commentBubble.style.display = 'block';
commentBubble.focus();
});
}
});
panel.appendChild(panelHeader);
panel.appendChild(classLabel);
panel.appendChild(panelContent);
panel.appendChild(commentBubble);
dom_chart.appendChild(panel);
panelPositions[screen.screenName] = panel;
panel.addEventListener('mousedown', (e) => {
if (e.button !== 0 || e.target.tagName === 'INPUT') return;
panel.dataset.moving = true;
// パネルの初期位置とマウスの初期位置を記録する
let initialMouseX = calcPositionOnContents('x',e.clientX) ;
let initialMouseY = calcPositionOnContents('y',e.clientY) ;
let initialPositions = [];
let initalControlPositions = [];
if (selectedPanels.has(panel)) {
selectedPanels.forEach(selectedPanel => {
const rect = selectedPanel.getBoundingClientRect();
initialPositions.push({
panel: selectedPanel,
startX: calcPositionOnContents('x',rect.left),
startY: calcPositionOnContents('y',rect.top)
});
});
selectedControlPoints.forEach(selectedPoint => {
initalControlPositions.push({
point: selectedPoint,
startX: parseInt(selectedPoint.getAttribute('cx')),
startY: parseInt(selectedPoint.getAttribute('cy'))
});
});
} else {
initialPositions.push({
panel: panel,
startX: parseInt(panel.style.left),
startY: parseInt(panel.style.top)
});
}
function onMouseMove(event) {
panel.classList.add('dragging');
const updatePanel1 = movePanels(initialMouseX,initialMouseY,calcPositionOnContents('x',event.clientX), calcPositionOnContents('y',event.clientY),initialPositions);
const updatePanel2 = movePoints(initialMouseX,initialMouseY,calcPositionOnContents('x',event.clientX), calcPositionOnContents('y',event.clientY),initalControlPositions);
const mergedUpdatePanel = mergeArraysUnique(updatePanel1,updatePanel2);
requestAnimationFrame(()=>drawArrows(mergedUpdatePanel));
}
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', function onMouseUp() {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
panel.classList.remove('dragging');
delete panel.dataset.moving;
});
});
panel.ondragstart = function() {
return false;
};
panel.addEventListener('click', (e) => {
if (e.ctrlKey) {
if (selectedPanels.has(panel)) {
selectedPanels.delete(panel);
panel.classList.remove('selected-panel');
screen.isSelected = false;
} else {
selectedPanels.add(panel);
panel.classList.add('selected-panel');
screen.isSelected = true;
}
}
});
});
//依存関係にあるパネル間の関連事に線の制御点を持つための情報を作成する
state ? controlPoints = state.controlPoints : controlPoints;
screenData.forEach(row => {
if (
(row.リソース && row.取得元項目 && row.リソース !== row.機能名) ||
(row.引渡し && row.リソース)
) {
if(!controlPointsPositions.targetToSource[`${row.機能名}`]){
controlPointsPositions.targetToSource[`${row.機能名}`] = {};
controlPointsPositions.sourceToTarget[`${row.機能名}`] = {};
}
if(!controlPointsPositions.sourceToTarget[`${row.リソース}`]){
controlPointsPositions.sourceToTarget[`${row.リソース}`] = {};
controlPointsPositions.targetToSource[`${row.リソース}`] = {};
}
if(!controlPointsPositions.targetToSource[`${row.機能名}`][`${row.リソース}`] && !controlPointsPositions.targetToSource[`${row.リソース}`][`${row.機能名}`]){
const dependencyLine = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
dependencyLine.setAttribute('stroke', '#d3d3d3');
dependencyLine.setAttribute('stroke-width', '15');
dependencyLine.setAttribute('fill', 'none');
dependencyLine.classList.add('panel-dependency-line');
dependencyLine.style.display = 'none';
// 制御点追加イベント
dependencyLine.addEventListener('dblclick', (e) => addControlPoint(e, row.機能名, row.リソース));
dom_arrows.appendChild(dependencyLine);
let arrows = [];
controlPointsPositions.targetToSource[`${row.機能名}`][`${row.リソース}`] = {dependencyLine : dependencyLine, arrows : arrows};
controlPointsPositions.sourceToTarget[`${row.リソース}`][`${row.機能名}`] = {dependencyLine : dependencyLine, arrows : arrows};
if(!controlPoints[`${row.機能名}【機能間依存】${row.リソース}`]){
controlPoints[`${row.機能名}【機能間依存】${row.リソース}`] = {
key1:row.機能名,
key2:row.リソース,
points:[]
};
}
}
}
});
if(controlPoints){
const isHidden = appControlInfo.lineDrawMode === "無効" ? true: false;
Object.values(controlPoints).forEach(({ key1, key2, points }) => {
points.forEach(point=>{
addControlPoint(null,key1,key2,point,isHidden)
});
});
}
//依存関係にある項目間の線のdomを保持する情報を作成する。更新対象の線と特定の高速化のため。
screenData.forEach(row => {
if (
(row.リソース && row.取得元項目 && row.リソース !== row.機能名) ||
(row.引渡し && row.リソース)
) {
const sourceItemName = row.取得元項目 ? row.取得元項目 : row.引渡し;
const sourceKey = `${row.リソース}【項目間依存】${sourceItemName}`;
const targetKey = `${row.機能名}【項目間依存】${row.項目名}`;
const polyline = document.createElementNS('http://www.w3.org/2000/svg', 'polyline');
polyline.setAttribute('stroke', '#000');
polyline.setAttribute('stroke-width', '2');
polyline.setAttribute('fill', 'none');
if(row.取得元項目){
polyline.setAttribute('marker-end', 'url(#arrowhead)');
}else{
polyline.setAttribute('marker-start', 'url(#arrowheadR)');
}
polyline.style.display = 'none'; // 初期状態は非表示
// SVG要素をarrowPositionsに格納
if (!arrowPositions.sourceToTarget[sourceKey]) {
arrowPositions.sourceToTarget[sourceKey] = {};
}
arrowPositions.sourceToTarget[sourceKey][targetKey] = polyline;
controlPointsPositions.sourceToTarget[row.リソース][row.機能名].arrows.push({
sourceItemName : sourceItemName,
targetItemName : row.項目名,
arrow : polyline
});
if (!arrowPositions.targetToSource[targetKey]) {
arrowPositions.targetToSource[targetKey] = {};
}
arrowPositions.targetToSource[targetKey][sourceKey] = polyline;
// SVGコンテナに追加
dom_arrows.appendChild(polyline);
}
});
//コメントの復元
if(state){
commentPanels = state.commentPanels
importCommentPanels(commentPanels);
}
}
function movePanels(initialMouseX,initialMouseY,currentMouseX, currentMouseY,initialPositions) {
//return;
// マウスの移動量を計算
const deltaX = currentMouseX - initialMouseX;
const deltaY = currentMouseY - initialMouseY;
let updatePanels = [];
// 初期位置に移動量を加算してパネルの位置を更新
initialPositions.forEach(({ panel, startX, startY }) => {
panel.style.left = `${startX + deltaX}px`;
panel.style.top = `${startY + deltaY}px`;
groupedData[panel.querySelector('.panel-header span').textContent].positionX = panel.style.left;
groupedData[panel.querySelector('.panel-header span').textContent].positionY = panel.style.top;
updatePanels.push(panel.querySelector('.panel-header span').textContent);
});
return updatePanels;
}
function movePoints(initialMouseX,initialMouseY,currentMouseX, currentMouseY,initialPositions){
const deltaX = currentMouseX - initialMouseX;
const deltaY = currentMouseY - initialMouseY;
let updatePanels = [];
initialPositions.forEach(({ point, startX, startY }) => {
point.setAttribute('cx', startX + deltaX);
point.setAttribute('cy', startY + deltaY);
const pointId = point.getAttribute('data-id')
const panelPare = getPanelNamesFromId(pointId);
updatePanels.push(panelPare.panel1);
controlPoints[`${panelPare.panel1}【機能間依存】${panelPare.panel2}`].points.forEach(pointInfo=>{
if(pointInfo.id === pointId){
pointInfo.x = point.getAttribute('cx');
pointInfo.y = point.getAttribute('cy') ;
}
});
});
function getPanelNamesFromId(id) {
const parts = id.split('【制御点ID】');
const panel1 = parts[0]; // パネル名1
const panel2 = parts[1]; // パネル名2
return { panel1, panel2 };
}
return updatePanels;
}
function calcPositionOnContents(vector,voluem){
let scale = appControlInfo.scale;
if(vector === 'x'){
return (voluem - dom_contents.offsetLeft + dom_contents.scrollLeft) / scale;
}else{
return (voluem - dom_contents.offsetTop + dom_contents.scrollTop) / scale;
}
}
function drawArrows(panelsToUpdate = []) {
// 更新するパネルがない場合は全体を更新
const isFullUpdate = panelsToUpdate.length === 0;
if(isFullUpdate){
Object.values(groupedData).forEach((screen,index)=>{
panelsToUpdate.push(screen.screenName);
});
//全体更新の場合、いったん線を全部非表示にする
const lines = dom_arrows.querySelectorAll('polyline');
lines.forEach(line => {
line.style.display = "none";
});
const points = dom_controlPointsSvg.querySelectorAll('circle');
points.forEach(point => {
point.style.display = "none";
});
}
if(appControlInfo.lineDrawMode === '無効'){
return;
}
panelsToUpdate.forEach(screenName =>{
//console.log(screenName);
//パネル間の依存線
if(controlPointsPositions.targetToSource[screenName]){
//console.log(controlPointsPositions.targetToSource[screenName]);
Object.entries(controlPointsPositions.targetToSource[screenName]).forEach(([key,controlPoint])=>updateLines(screenName,key,controlPoint));
if(!isFullUpdate){
Object.entries(controlPointsPositions.sourceToTarget[screenName]).forEach(([key,controlPoint])=>updateLines(key,screenName,controlPoint));
}
}
});
}
function updateLines(key1,key2,controlPoint){
//console.log(key + ":");
//console.log(controlPoint);
const panel1 = panelPositions[key1];
const panel2 = panelPositions[key2];
let pointInfo = controlPoints[`${key1}【機能間依存】${key2}`];
if (panel1 && panel2) {
const panel1Rect = panel1.getBoundingClientRect();
const panel2Rect = panel2.getBoundingClientRect();
let startX, startY, endX, endY;
controlPoint.arrows.forEach(arrow => updateItemLines(arrow,key1,key2));
// パネルが非表示の場合は return し、制御点も非表示のままにする
if (groupedData[key1].isHidden || groupedData[key2].isHidden) {
return;
}
if (appControlInfo.lineDrawMode === '対象指定') {
const isSourceDrawMode = panel1.querySelector('.panel-header select').value;
const istargetDrawMode = panel2.querySelector('.panel-header select').value;
if (istargetDrawMode === 'none' && isSourceDrawMode === 'none' || istargetDrawMode === 'mutual' && isSourceDrawMode === 'none' || istargetDrawMode === 'none' && isSourceDrawMode === 'mutual') {
controlPoint.dependencyLine.style.display = 'none';
return;
}
}
// パネル間の始点と終点を設定
if (panel1Rect.right < panel2Rect.left) {
startX = panel1Rect.right;
startY = panel1Rect.top;
endX = panel2Rect.left;
endY = panel2Rect.top
} else {
startX = panel1Rect.left;
startY = panel1Rect.top;
endX = panel2Rect.right;
endY = panel2Rect.top;
}
startX = calcPositionOnContents('x',startX);
startY = calcPositionOnContents('y',startY) + 20;
endX = calcPositionOnContents('x',endX);
endY = calcPositionOnContents('y',endY) + 20;
// 条件に該当しない場合、制御点を表示に設定
pointInfo.points.forEach(point => {
const controlPointElement = document.querySelector(`[data-id="${point.id}"]`);
if (controlPointElement) {
controlPointElement.style.display = 'block';
}
});
// polyline の座標リストを作成
const polylinePoints = [
`${startX},${startY}`, // 始点
...pointInfo.points.slice().reverse().map(p => `${p.x},${p.y}`), // 制御点
`${endX},${endY}` // 終点
].join(" ");
controlPoint.dependencyLine.setAttribute('points', polylinePoints);
controlPoint.dependencyLine.style.display = 'block';
//項目間の依存線
function updateItemLines(arrow,targetPanelKey,sourcePanelKey){
const sourceItem = itemPositions[`${sourcePanelKey}【画面-項目】${arrow.sourceItemName}`];
const targetItem = itemPositions[`${targetPanelKey}【画面-項目】${arrow.targetItemName}`];
const sourcePanel = panelPositions[sourcePanelKey];
const targetPanel = panelPositions[targetPanelKey];
let sourceX, sourceY, targetX, targetY;
const sourceRect = sourceItem ? sourceItem.getBoundingClientRect() : sourcePanel.getBoundingClientRect();
const targetRect = targetItem ? targetItem.getBoundingClientRect() : targetPanel.getBoundingClientRect();
let sourceIsVisible;
let targetIsVisible;
sourceIsVisible = sourceItem ? sourceItem.closest('.panel-content').style.display === 'block' : false;
targetIsVisible = targetItem ? targetItem.closest('.panel-content').style.display === 'block' : false;
if(itemDisplayModeSelect.value == "指定項目"){
sourceIsVisible = (sourceItem && sourceItem.classList.contains("item-hidden")) ? false: sourceIsVisible;
targetIsVisible = (targetItem && targetItem.classList.contains("item-hidden")) ? false: targetIsVisible;
}
if((groupedData[sourcePanelKey].isHidden || groupedData[targetPanelKey].isHidden)
){return};
if (appControlInfo.lineDrawMode === '対象指定') {
const isSourceDrawMode = sourcePanel.querySelector('.panel-header select').value;
const istargetDrawMode = targetPanel.querySelector('.panel-header select').value;
if (istargetDrawMode === 'none' && isSourceDrawMode === 'none' || istargetDrawMode === 'mutual' && isSourceDrawMode === 'none' || istargetDrawMode === 'none' && isSourceDrawMode === 'mutual') {
arrow.arrow.style.display = 'none';
return;
}
}
if (sourceIsVisible) {
sourceX = sourceRect.right;
sourceY = sourceRect.top + sourceRect.height / 2;
} else {
const sourcePanelRect = sourcePanel.getBoundingClientRect();
sourceX = sourcePanelRect.right;
sourceY = sourcePanelRect.top;
}
if (targetIsVisible) {
targetX = targetRect.left;
targetY = targetRect.top + targetRect.height / 2;
} else {
const targetPanelRect = targetPanel.getBoundingClientRect();
targetX = targetPanelRect.left;
targetY = targetPanelRect.top;
}
if (sourceX < targetX) {
sourceX = sourceIsVisible ? sourceRect.right : sourcePanel.getBoundingClientRect().right;
targetX = targetIsVisible ? targetRect.left : targetPanel.getBoundingClientRect().left;
} else {
sourceX = sourceIsVisible ? sourceRect.left : sourcePanel.getBoundingClientRect().left;
targetX = targetIsVisible ? targetRect.right : targetPanel.getBoundingClientRect().right;
}
sourceX = calcPositionOnContents('x',sourceX);
sourceY = calcPositionOnContents('y',sourceY) + (sourceIsVisible ? 0 : 20);
targetX = calcPositionOnContents('x',targetX);
targetY = calcPositionOnContents('y',targetY) + (targetIsVisible ? 0 : 20);
// polyline の座標リストを作成
const polylinePoints = [
`${sourceX},${sourceY}`, // 始点
...(pointInfo.points).map(p => `${p.x},${p.y}`), // 制御点(順序を反転)
`${targetX},${targetY}` // 終点
].join(" ");
arrow.arrow.setAttribute('points', polylinePoints);
arrow.arrow.style.display = 'block';
};
}
};
/* ヘッダーUIイベント */
dom_toggleAll.addEventListener('click', () => {
const panels = document.querySelectorAll('.panel-content');
const allExpanded = Array.from(panels).every(panel => panel.style.display === 'block');
panels.forEach(panel => {
panel.style.display = allExpanded ? 'none' : 'block';
const toggleButton = panel.closest('.panel').querySelector('.toggle-button');
toggleButton.textContent = allExpanded ? '△' : '▽';
});
requestAnimationFrame(()=>drawArrows());
});
dom_displayScaleSlider.addEventListener('input', function () {
dom_displayScaleValue.value = dom_displayScaleSlider.value;
updateDisplayScale(dom_displayScaleSlider.value)
});
dom_displayScaleValue.addEventListener('input', function () {
let value = parseInt(dom_displayScaleValue.value, 10);
if (value >= parseInt(dom_displayScaleSlider.min, 10) && value <= parseInt(dom_displayScaleSlider.max, 10)) {
dom_displayScaleSlider.value = value;
updateDisplayScale(value);
}
});
dom_displayScalePlus.addEventListener('click', function () {
let newValue = Math.min(parseInt(dom_displayScaleSlider.value, 10) + 10, parseInt(dom_displayScaleSlider.max, 10));
dom_displayScaleSlider.value = newValue;
dom_displayScaleValue.value = newValue;
updateDisplayScale(newValue);
});
dom_displayScaleMinus.addEventListener('click', function () {
let newValue = Math.max(parseInt(dom_displayScaleSlider.value, 10) - 10, parseInt(dom_displayScaleSlider.min, 10));
dom_displayScaleSlider.value = newValue;
dom_displayScaleValue.value = newValue;
updateDisplayScale(newValue);
});
function updateDisplayScale(newScale){
//dom_contentsParent
let currentScale = dom_contentsParent.style.scale;
// 現在のスクロール位置と中央位置を取得
const scrollLeft = dom_contents.scrollLeft;
const scrollTop = dom_contents.scrollTop;
const centerX = scrollLeft + dom_contents.clientWidth / 2;
const centerY = scrollTop + dom_contents.clientHeight / 2;
// 中央位置をコンテンツ内の座標に変換
const contentCenterX = centerX / currentScale;
const contentCenterY = centerY / currentScale;
//倍率変更
dom_contentsParent.style.scale = newScale/100;
appControlInfo.scale = newScale/100;
// 中央位置が維持されるようにスクロール位置を調整
const newCenterX = contentCenterX * newScale/100;
const newCenterY = contentCenterY * newScale/100;
dom_contents.scrollLeft = newCenterX - dom_contents.clientWidth / 2;
dom_contents.scrollTop = newCenterY - dom_contents.clientHeight / 2;
}
/** 描画モード切替 */
drawModeSelect.addEventListener('change', (e) => {
appControlInfo.lineDrawMode = e.target.value;
const panels = document.querySelectorAll('.panel');
panels.forEach(panel => {
const selectBox = panel.querySelector('.panel-header select'); // セレクトボックスを取得
const panelName = panel.querySelector('.panel-header span').textContent;
if (appControlInfo.lineDrawMode === '対象指定') {
// 対象指定モードに切り替えた際に、以前の選択状態を復元
selectBox.value = groupedData[panelName].arrowDirection || 'none';
selectBox.style.display = 'block'; // セレクトボックスを表示
} else {
// 選択状態を保存して非表示にする
groupedData[panelName].arrowDirection = selectBox.value; // 現在の選択状態を保存
selectBox.style.display = 'none'; // セレクトボックスを非表示
}
});
requestAnimationFrame(()=>drawArrows());
});
/** 項目表示モード切替 */
document.getElementById('itemDisplayModeSelect').addEventListener('change', function (e) {
const selectedMode = e.target.value;
appControlInfo.itemDisplayMode = selectedMode;
if (selectedMode === '指定項目') {
// 項目表示がonになっている項目だけ表示
Object.keys(itemPositions).forEach(key => {
const item = itemPositions[key];
const [screenName, itemName] = key.split('【画面-項目】');
const itemDiv = groupedData[screenName].items.find(item => item.itemName === itemName)
const isItemVisible = itemDiv.isVisible;
if (!isItemVisible) {
//item.style.display = 'none';
item.classList.add('item-hidden');
} else {
//item.style.display = 'block';
item.classList.remove('item-hidden');
}
});
} else {
// 全ての項目を表示する
Object.keys(itemPositions).forEach(key => {
const item = itemPositions[key];
item.classList.remove('item-hidden');
});
}
requestAnimationFrame(()=>drawArrows());
});
/** コンテンツ部分の処理 */
/** 範囲選択処理 */
dom_contentsParent.addEventListener('mousedown', (e) => {
if (e.target !== dom_contentsParent) return;
// ページ全体のテキスト選択を無効化
dom_contentsParent.style.userSelect = 'none';
selectionStartX = calcPositionOnContents('x',e.clientX);
selectionStartY = calcPositionOnContents('y',e.clientY);
selectionBox = document.createElement('div');
selectionBox.className = 'selection-box';
selectionBox.style.left = `${selectionStartX}px`;
selectionBox.style.top = `${selectionStartY}px`;
selectionBox.style.width = '0px';
selectionBox.style.height= '0px';
dom_contentsParent.appendChild(selectionBox);
function onMouseMove(event) {
const currentX = calcPositionOnContents('x',event.clientX);
const currentY = calcPositionOnContents('y',event.clientY);
selectionBox.style.left = `${Math.min(selectionStartX, currentX)}px`;
selectionBox.style.top = `${Math.min(selectionStartY, currentY)}px`;
selectionBox.style.width = `${Math.abs(currentX - selectionStartX)}px`;
selectionBox.style.height = `${Math.abs(currentY - selectionStartY)}px`;
}
function onMouseUp(event) {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
// テキスト選択を再び有効化
document.body.style.userSelect = '';
const selectionRect = selectionBox.getBoundingClientRect();
dom_contentsParent.removeChild(selectionBox);
selectionBox = null;
const panels = document.querySelectorAll('.panel');
panels.forEach(panel => {
const panelRect = panel.getBoundingClientRect();
if (
panelRect.left < selectionRect.right &&
panelRect.right > selectionRect.left &&
panelRect.top < selectionRect.bottom &&
panelRect.bottom > selectionRect.top
) {
if (selectedPanels.has(panel)) {
// 選択を解除する
selectedPanels.delete(panel);
panel.classList.remove('selected-panel');
groupedData[panel.querySelector('.panel-header span').textContent].isSelected = false;
} else {
// 新たに選択する
selectedPanels.add(panel);
panel.classList.add('selected-panel');
groupedData[panel.querySelector('.panel-header span').textContent].isSelected = true;
}
}
});
// 制御点の選択処理
Object.values(controlPoints).forEach(({ key1, key2, points }) => {
points.forEach(point=>{
const dom_point = document.querySelector(`[data-id="${point.id}"]`);
const controlPointRect = dom_point.getBoundingClientRect();
if (
controlPointRect.left < selectionRect.right &&
controlPointRect.right > selectionRect.left &&
controlPointRect.top < selectionRect.bottom &&
controlPointRect.bottom > selectionRect.top
) {
if (selectedControlPoints.has(dom_point)) {
selectedControlPoints.delete(dom_point);
dom_point.classList.remove('selected-control-point');
}else{
selectedControlPoints.add(dom_point);
dom_point.classList.add('selected-control-point');
}
}
});
});
}
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
});
/** 詳細表示パネルの表示機能 */
function showDetailPopup(screen, panel) {
const detailPopup = document.getElementById('detailPopup');
const popupScreenName = document.getElementById('popupScreenName');
const popupClassName = document.getElementById('popupClassName');
const popupTableBody = document.getElementById('popupTableBody');
const referenceTableBody = document.getElementById('referenceTableBody'); // 参照元機能一覧のテーブル
const destinationTableBody = document.getElementById('destinationTableBody'); // 参照先機能一覧のテーブル
// 機能名とクラス名を設定
popupScreenName.textContent = `機能名: ${screen.screenName}`;
popupClassName.textContent = `クラス名: ${screen.className}`;
// 現在の機能に関する詳細情報をテーブルに表示
popupTableBody.innerHTML = screen.items.map(item => `
<tr>
<td>${item.type}</td>
<td>${item.itemName}</td>
<td>${item.resourceType}</td>
<td><a href="#" class="resource-link" data-resource="${item.sourceScreen}" data-resource-type="${item.resourceType}" data-item-type="${item.type}">${item.sourceScreen}</a></td>
<td>${item.sourceItem}</td>
<td>${item.handoff}</td>
<td>
<input type="checkbox" class="item-visible-checkbox" data-screen="${screen.screenName}" data-item="${item.itemName}" ${item.isVisible ? 'checked' : ''}>
</td>
</tr>
`).join('');
// 参照先機能一覧を生成
const destinationData = extractDestinationFunctions(screen);
destinationTableBody.innerHTML = destinationData.map(destItem => `
<tr>
<td>${destItem.type}</td>
<td>${destItem.className}</td>
<td><a href="#" class="resource-link" data-resource="${destItem.screenName}" data-resource-type="${destItem.type}">${destItem.screenName}</a></td>
<td>${destItem.inOut}</td>
</tr>
`).join('');
// 項目表示のトグル処理
document.querySelectorAll('.item-visible-checkbox').forEach(checkbox => {
checkbox.addEventListener('change', (e) => {
const screenName = e.target.getAttribute('data-screen');
const itemName = e.target.getAttribute('data-item');
toggleItemVisible(screenName, itemName, e.target.checked);
});
});
// リソースリンクのクリックイベントを設定
document.querySelectorAll('.resource-link').forEach(link => {
link.addEventListener('click', (event) => {
event.preventDefault();
const resourceType = event.target.getAttribute('data-resource-type');
const resourceName = event.target.getAttribute('data-resource');
const itemType = event.target.getAttribute('data-item-type');
// リソースとリソースの機能種別でパネルを探し、詳細表示
moveToResourcePanel(resourceType, resourceName, itemType);
});
});
// 参照元機能一覧を生成
const referenceData = extractReferenceFunctions(screen.type, screen.screenName);
referenceTableBody.innerHTML = referenceData.map(refItem => `
<tr>
<td>${refItem.type}</td>
<td>${refItem.className}</td>
<td><a href="#" class="resource-link" data-resource="${refItem.screenName}" data-resource-type="${refItem.type}">${refItem.screenName}</a></td>
<td>${refItem.inOut}</td>
</tr>
`).join('');
// 参照元の機能名リンクのクリックイベントも設定
document.querySelectorAll('#referenceTableBody .resource-link').forEach(link => {
link.addEventListener('click', (event) => {
event.preventDefault();
const resourceType = event.target.getAttribute('data-resource-type');
const resourceName = event.target.getAttribute('data-resource');
// リソースとリソースの機能種別でパネルを探し、詳細表示
moveToResourcePanel(resourceType, resourceName);
});
});
// ポップアップの位置をクリックしたパネルの右隣に設定
const panelRect = panel.getBoundingClientRect();
detailPopup.style.top = `${calcPositionOnContents('y',panelRect.top)}px`;
detailPopup.style.left = `${calcPositionOnContents('x',panelRect.right + 10)}px`;
// ポップアップを表示
detailPopup.style.display = 'block';
}
// 参照元機能の抽出
function extractReferenceFunctions(targetType, targetScreenName) {
// 参照元機能を抽出するためのロジック
const references = {};
Object.values(groupedData).forEach(screen => {
screen.items.forEach(item => {
if (
item.resourceType === targetType &&
item.sourceScreen === targetScreenName
) {
// 該当する参照元があれば、そのグループを追加
if (!references[screen.screenName]) {
references[screen.screenName] = {
type: screen.type,
className: screen.className,
screenName: screen.screenName,
inOut: '' // 入出力の初期化
};
}
// 「取得元項目」と「引渡し」で「入」「出」「入/出」を判定
if (item.sourceItem && item.sourceItem !== '-' && item.sourceItem !== '') {
if (references[screen.screenName].inOut === '入') {
references[screen.screenName].inOut = '入/出';
} else {
references[screen.screenName].inOut = '出';
}
}
if (item.handoff && item.handoff !== '-' && item.handoff !== '') {
if (references[screen.screenName].inOut === '出') {
references[screen.screenName].inOut = '入/出';
} else {
references[screen.screenName].inOut = '入';
}
}
}
});
});
return Object.values(references); // 1階層目のデータだけを返す
}
// 参照先機能の抽出
function extractDestinationFunctions(screen) {
const destinations = {};
// パネル内の各項目について処理
screen.items.forEach(item => {
if (item.resourceType && item.sourceScreen) {
// リソースが設定されている項目をもとに参照先を導出
const matchingScreen = Object.values(groupedData).find(data =>
data.type === item.resourceType && data.screenName === item.sourceScreen
);
if (matchingScreen) {
// すでに同じ参照先があれば、入出力の判定を更新
if (!destinations[matchingScreen.screenName]) {
destinations[matchingScreen.screenName] = {
type: matchingScreen.type,
className: matchingScreen.className,
screenName: matchingScreen.screenName,
inOut: ''
};
}
// 取得元項目がある場合は「入」
if (item.sourceItem && item.sourceItem !== '-') {
if (destinations[matchingScreen.screenName].inOut === '出') {
destinations[matchingScreen.screenName].inOut = '入/出';
} else {
destinations[matchingScreen.screenName].inOut = '入';
}
}
// 引渡しがある場合は「出」
if (item.handoff && item.handoff !== '-') {
if (destinations[matchingScreen.screenName].inOut === '入') {
destinations[matchingScreen.screenName].inOut = '入/出';
} else {
destinations[matchingScreen.screenName].inOut = '出';
}
}
}
}
});
return Object.values(destinations);
}
// リソースリンククリック時の処理
function moveToResourcePanel(resourceType, resourceName) {
// 対応するパネルを検索
const matchingPanel = Object.values(groupedData).find(screen =>
screen.type === resourceType && screen.screenName === resourceName
);
if (matchingPanel) {
const panel = panelPositions[matchingPanel.screenName];
if (panel) {
// パネルへスクロール
panel.scrollIntoView({ behavior: 'smooth', block: 'center' });
// パネルを強調表示
panel.style.border = '3px solid blue';
setTimeout(() => {
panel.style.border = '';
}, 3000);
// 詳細表示を起動
showDetailPopup(matchingPanel, panel);
}
} else {
alert('一致するリソースが見つかりません。');
}
}
// 閉じるボタンのクリックイベントを追加
document.getElementById('closeDetailPopup').addEventListener('click', () => {
document.getElementById('detailPopup').style.display = 'none';
});
// 参照先機能一覧のまとめて選択処理
document.getElementById('selectAllDestination').addEventListener('click', () => {
const destinationRows = document.querySelectorAll('#destinationTableBody tr');
destinationRows.forEach(row => {
const screenName = row.querySelector('td:nth-child(3) a').textContent; // 機能名
if (screenName) {
togglePanelSelection(screenName, true); // パネルを選択状態にする
}
});
});
// 参照元機能一覧のまとめて選択処理
document.getElementById('selectAllReference').addEventListener('click', () => {
const referenceRows = document.querySelectorAll('#referenceTableBody tr');
referenceRows.forEach(row => {
const screenName = row.querySelector('td:nth-child(3) a').textContent; // 機能名
if (screenName) {
togglePanelSelection(screenName, true); // パネルを選択状態にする
}
});
});
/** フッター部分の処理 */
const footer = document.getElementById('footer');
const selectionTool = document.getElementById('selectionTool');
const toggleSelectionToolButton = document.getElementById('toggleSelectionTool');
const closeSelectionToolButton = document.getElementById('closeSelectionTool');
const selectionToolHeader = document.getElementById('selectionToolHeader');
const modeToggle = document.getElementById('modeToggle');
const panelModeHeader = document.getElementById('panelModeHeader');
const itemModeHeader = document.getElementById('itemModeHeader');
const selectionTableBody = document.getElementById('selectionTableBody');
const itemDisplayModeSelect = document.getElementById('itemDisplayModeSelect');
// フッターの「△」ボタンで選択ツールを表示
toggleSelectionToolButton.addEventListener('click', () => {
selectionTool.style.display = 'block';
toggleSelectionToolButton.style.display = 'none';
});
// 「▽」ボタンで選択ツールを閉じる
closeSelectionToolButton.addEventListener('click', () => {
selectionTool.style.display = 'none';
toggleSelectionToolButton.style.display = 'inline';
});
// 選択ツールのヘッダー部分で高さを調整可能にする
let isResizing = false;
let lastY = 0;
selectionToolHeader.addEventListener('mousedown', (e) => {
isResizing = true;
lastY = e.clientY;
});
document.addEventListener('mousemove', (e) => {
if (!isResizing) return;
const deltaY = e.clientY - lastY;
const newHeight = selectionTool.offsetHeight - deltaY;
if (newHeight >= 100 && newHeight <= 600) { // 最小・最大高さの制限
selectionTool.style.height = `${newHeight}px`;
lastY = e.clientY;
}
});
document.addEventListener('mouseup', () => {
isResizing = false;
});
// モード切り替えイベント
let selectToolMode = 'panel';
modeToggle.addEventListener('change', (e) => {
const toggleColumns = document.querySelectorAll('.toggle-column');
const toggleToolItem = document.querySelectorAll('.toggle-tool-item');
const toggleColumnsReverse = document.querySelectorAll('.toggle-column-reverse');
if (e.target.value === 'item') {
toggleColumns.forEach(col => col.style.display = 'table-cell'); // 項目モードでは表示
toggleToolItem.forEach(col => col.style.display = ''); // 項目モードでは表示
toggleColumnsReverse.forEach(col => col.style.display = 'none'); // パネルモードでは非表示
selectToolMode = 'item';
renderTableBody('item');
} else {
toggleColumns.forEach(col => col.style.display = 'none'); // パネルモードでは非表示
toggleToolItem.forEach(col => col.style.display = 'none'); // パネルモードでは非表示
toggleColumnsReverse.forEach(col => col.style.display = 'table-cell'); // 項目モードでは表示
selectToolMode = 'panel';
renderTableBody('panel');
}
});
// テーブル内容の動的生成
function renderTableBody(mode) {
selectionTableBody.innerHTML = ''; // テーブル内容をクリア
let rowCount = 0;
let rows = [];
const LIMIT_ROW_COUNT = 500;//性能対策のため500で描画を打ち切る
const groupedDataArr = Object.values(groupedData);
//検索条件の情報収集
let crossSearchText = document.getElementById("crossSearch").value;
let searchFeatureType = document.getElementById("filterFeatureType").checked;
let searchClassName = document.getElementById("filterClassName").checked;
let searchComment = document.getElementById("searchComment").checked;
let searchFeatureName = document.getElementById("filterFeatureName").checked;
let searchItemType = document.getElementById("filterItemType").checked;
let searchItemName = document.getElementById("filterItemName").checked;
let searchResourceType = document.getElementById("filterResourceType").checked;
let searchResource = document.getElementById("filterResource").checked;
let searchSourceItem = document.getElementById("filterSourceItem").checked;
let searchHandoff = document.getElementById("filterHandoff").checked;
let searchDefine = {
crossSearchText:!crossSearchText ? null : new RegExp(crossSearchText),
searchFeatureType:searchFeatureType,
searchClassName:searchClassName,
searchFeatureName:searchFeatureName,
searchComment:searchComment,
searchItemType:searchItemType,
searchItemName:searchItemName,
searchResourceType:searchResourceType,
searchResource:searchResource,
searchSourceItem:searchSourceItem,
searchHandoff:searchHandoff
};
//フィルタの条件収集
let filterTextFeatureType = document.getElementById("filterTextFeatureType").value;
let filterTextClassName = document.getElementById("filterTextClassName").value;
let filterTextFeatureName = document.getElementById("filterTextFeatureName").value;
let filterTextComment = document.getElementById("filterTextComment").value;
let filterTextItemType = document.getElementById("filterTextItemType").value;
let filterTextItemName = document.getElementById("filterTextItemName").value;
let filterTextResourceType = document.getElementById("filterTextResourceType").value;
let filterTextResource = document.getElementById("filterTextResource").value;
let filterTextSourceItem = document.getElementById("filterTextSourceItem").value;
let filterTextHandoff = document.getElementById("filterTextHandoff").value;
let filterSelected = document.getElementById("filterSelected").value;
let filterEnableLine = document.getElementById("filterEnableLine").value;
let filterHidden = document.getElementById("filterHidden").value;
let filterBookMark = document.getElementById("filterBookMark").value;
let filterItemHidden = document.getElementById("filterItemHidden").value;
let filterDefine = {
filterTextFeatureType:new RegExp(filterTextFeatureType),
filterTextClassName:new RegExp(filterTextClassName),
filterTextFeatureName:new RegExp(filterTextFeatureName),
filterTextComment:new RegExp(filterTextComment),
filterTextItemType:new RegExp(filterTextItemType),
filterTextItemName:new RegExp(filterTextItemName),
filterTextResourceType:new RegExp(filterTextResourceType),
filterTextResource:new RegExp(filterTextResource),
filterTextSourceItem:new RegExp(filterTextSourceItem),
filterTextHandoff:new RegExp(filterTextHandoff),
filterSelected:filterSelected,
filterEnableLine:filterEnableLine,
filterHidden:filterHidden,
filterBookMark:filterBookMark,
filterItemHidden:filterItemHidden
};
for(let i = 0; i < groupedDataArr.length; i++){
let innerHtmlScreen = "";
let innerHtmlItem = "";
let innerHtmTool = "";
innerHtmlScreen = `
<td>${groupedDataArr[i].type}</td>
<td>${groupedDataArr[i].className}</td>
<td>${groupedDataArr[i].screenName}</td>
`;
if(mode === 'item'){
let matchList=[];
for(let j = 0; j < groupedDataArr[i].items.length; j++){
innerHtmlItem = `
<td>${groupedDataArr[i].items[j].itemType}</td>
<td>${groupedDataArr[i].items[j].itemName}</td>
<td>${groupedDataArr[i].items[j].resourceType}</td>
<td>${groupedDataArr[i].items[j].sourceScreen}</td>
<td>${groupedDataArr[i].items[j].sourceItem}</td>
<td>${groupedDataArr[i].items[j].handoff}</td>
<td><input type="checkbox" class="item-visible-checkbox" ${groupedDataArr[i].items[j].isVisible ? 'checked' : ''}></td>
`;
if(!searchAndFiter4Item(groupedDataArr[i],groupedDataArr[i].items[j],searchDefine,filterDefine)){
continue;
}else{
matchList.push(innerHtmlItem);
}
}
innerHtmTool = `
<td rowspan="${matchList.length}"><input type="checkbox" class="select-checkbox" ${groupedDataArr[i].isSelected ? 'checked' : ''}></td>
<td rowspan="${matchList.length}"><input type="checkbox" class="draw-checkbox" ${groupedDataArr[i].isEnableLine ? 'checked' : ''}></td>
<td rowspan="${matchList.length}"><input type="checkbox" class="hide-checkbox" ${groupedDataArr[i].isHidden ? 'checked' : ''}></td>
<td rowspan="${matchList.length}"><input type="checkbox" class="bookMark-checkbox" ${groupedDataArr[i].bookMark ? 'checked' : ''}></td>
<td rowspan="${matchList.length}"><button class="go-button">Go</button></td>
`;
for(let j = 0; j < matchList.length;j++){
const row = document.createElement('tr');
row.innerHTML = innerHtmlScreen + matchList[j] + (j === 0 ? innerHtmTool: '');
rows.push({screenName:groupedDataArr[i].screenName,row:row});
rowCount++;
}
}else{
innerHtmlScreen = innerHtmlScreen + `<td>${groupedDataArr[i].comment}</td>`;
innerHtmTool = `
<td><input type="checkbox" class="select-checkbox" ${groupedDataArr[i].isSelected ? 'checked' : ''}></td>
<td><input type="checkbox" class="draw-checkbox" ${groupedDataArr[i].isEnableLine ? 'checked' : ''}></td>
<td><input type="checkbox" class="hide-checkbox" ${groupedDataArr[i].isHidden ? 'checked' : ''}></td>
<td><input type="checkbox" class="bookMark-checkbox" ${groupedDataArr[i].bookMark ? 'checked' : ''}></td>
<td><button class="go-button">Go</button></td>
`;
if(!searchAndFiter4Panel(groupedDataArr[i],searchDefine,filterDefine)){ continue; };
const row = document.createElement('tr');
row.innerHTML = innerHtmlScreen + innerHtmTool;
rows.push({screenName:groupedDataArr[i].screenName,row:row});
rowCount++;
}
if(rowCount >= LIMIT_ROW_COUNT){
const row = document.createElement('tr');
row.innerHTML = `<td colspan="${mode==='panel'?8:14}">表示上限:${LIMIT_ROW_COUNT}件を超えています。フィルタ条件を見直してください。</td>`;
rows.push({screenName:'',row:row});
break;
}
}
rows.forEach(row =>{
selectionTableBody.appendChild(row.row);
if(mode === 'item'){
const itemVisibleCheckbox = row.row.querySelector('.item-visible-checkbox');
itemVisibleCheckbox.addEventListener('change', () => {
const itemName = row.row.querySelector('td:nth-child(5)').textContent;
toggleItemVisible(row.screenName, itemName, itemVisibleCheckbox.checked);
});
}
// Goボタンの動作を追加
const goButton = row.row.querySelector('.go-button');
if(!goButton){return}
goButton.addEventListener('click', () => {
scrollToPanel(row.screenName);
});
// パネルの選択や線描画とチェックボックスの連動
const selectCheckbox = row.row.querySelector('.select-checkbox');
selectCheckbox.addEventListener('change', () => {
togglePanelSelection(row.screenName, selectCheckbox.checked);
});
const drawCheckbox = row.row.querySelector('.draw-checkbox');
drawCheckbox.addEventListener('change', () => {
toggleDrawPanel(row.screenName, drawCheckbox.checked);
});
const hideCheckbox = row.row.querySelector('.hide-checkbox');
hideCheckbox.addEventListener('change', () => {
toggleHidePanel(row.screenName, hideCheckbox.checked);
});
const bookMarkCheckbox = row.row.querySelector('.bookMark-checkbox');
bookMarkCheckbox.addEventListener('change', () => {
toggleBookMark(row.screenName, bookMarkCheckbox.checked);
});
});
}
//除外対象ならfalseを変える
function searchAndFiter4Panel(screen,searchDefine,filterDefine){
let searchResult = true;
let filterResult = true;
//チェックボックスのいずれにもチェックがないっていない場合
if(!searchDefine.searchFeatureType && !searchDefine.searchClassName && !searchDefine.searchFeatureName && !searchDefine.searchComment){
if(
(!searchDefine.crossSearchText //検索条件の入力がないまたは、チェックがついているいずれかで条件に合う場合
|| searchDefine.crossSearchText.test(screen.type)
|| searchDefine.crossSearchText.test(screen.className)
|| searchDefine.crossSearchText.test(screen.screenName)
|| searchDefine.crossSearchText.test(screen.comment)
)
){
searchResult = true;
}else{
searchResult = false;
}
}else{//チェックボックスのいずれかにチェックが入っていた場合
if(!searchDefine.crossSearchText
|| (searchDefine.searchFeatureType && searchDefine.crossSearchText.test(screen.type))
|| (searchDefine.searchClassName && searchDefine.crossSearchText.test(screen.className))
|| (searchDefine.searchFeatureName && searchDefine.crossSearchText.test(screen.screenName))
|| (searchDefine.searchComment && searchDefine.crossSearchText.test(screen.comment))
){
searchResult = true;
}else{
searchResult = false;
}
}
//フィルタ側の判定
if(
(!filterDefine.filterTextFeatureType || filterDefine.filterTextFeatureType.test(screen.type))
&& (!filterDefine.filterTextClassName || filterDefine.filterTextClassName.test(screen.className))
&& (!filterDefine.filterTextFeatureName || filterDefine.filterTextFeatureName.test(screen.screenName))
&& (!filterDefine.filterTextComment || filterDefine.filterTextComment.test(screen.comment))
&& (filterDefine.filterSelected ==='none' || ((filterDefine.filterSelected ==='true' && screen.isSelected) ||filterDefine.filterSelected ==='false' && !screen.isSelected))
&& (filterDefine.filterEnableLine ==='none' || ((filterDefine.filterEnableLine ==='true' && screen.isEnableLine) ||filterDefine.filterEnableLine ==='false' && !screen.isEnableLine))
&& (filterDefine.filterHidden ==='none' || ((filterDefine.filterHidden ==='true' && screen.isHidden) ||filterDefine.filterHidden ==='false' && !screen.isHidden))
&& (filterDefine.filterBookMark ==='none' || ((filterDefine.filterBookMark ==='true' && screen.bookMark) ||filterDefine.filterBookMark ==='false' && !screen.bookMark))
){
filterResult = true;
}else{
filterResult = false;
}
return searchResult && filterResult;
}
function searchAndFiter4Item(screen,item,searchDefine,filterDefine){
let searchResult = true;
let filterResult = true;
//チェックボックスのいずれにもチェックがないっていない場合
if(!searchDefine.searchFeatureType && !searchDefine.searchClassName && !searchDefine.searchFeatureName && !searchDefine.searchComment
&& !searchDefine.searchItemType && !searchDefine.searchItemName && !searchDefine.searchResourceType
&& !searchDefine.searchResource && !searchDefine.searchSourceItem && !searchDefine.searchHandoff
){
if(
(!searchDefine.crossSearchText //検索条件の入力がないまたは、チェックがついているいずれかで条件に合う場合
|| searchDefine.crossSearchText.test(screen.type)
|| searchDefine.crossSearchText.test(screen.className)
|| searchDefine.crossSearchText.test(screen.screenName)
|| searchDefine.crossSearchText.test(screen.comment)
|| searchDefine.crossSearchText.test(item.itemType)
|| searchDefine.crossSearchText.test(item.itemName)
|| searchDefine.crossSearchText.test(item.sourceScreen)
|| searchDefine.crossSearchText.test(item.sourceItem)
|| searchDefine.crossSearchText.test(item.resourceType)
|| searchDefine.crossSearchText.test(item.handoff)
)
){
searchResult = true;
}else{
searchResult = false;
}
}else{//チェックボックスのいずれかにチェックが入っていた場合
if(!searchDefine.crossSearchText
|| (searchDefine.searchFeatureType && searchDefine.crossSearchText.test(screen.type))
|| (searchDefine.searchClassName && searchDefine.crossSearchText.test(screen.className))
|| (searchDefine.searchFeatureName && searchDefine.crossSearchText.test(screen.screenName))
|| (searchDefine.searchComment && searchDefine.crossSearchText.test(screen.comment))
|| (searchDefine.searchItemType && searchDefine.crossSearchText.test(item.itemType))
|| (searchDefine.searchItemName && searchDefine.crossSearchText.test(item.itemName))
|| (searchDefine.searchResourceType && searchDefine.crossSearchText.test(item.resourceType))
|| (searchDefine.searchResource && searchDefine.crossSearchText.test(item.sourceScreen)
|| (searchDefine.searchSourceItem && searchDefine.crossSearchText.test(item.sourceItem)))
|| (searchDefine.searchHandoff && searchDefine.crossSearchText.test(item.handoff))
){
searchResult = true;
}else{
searchResult = false;
}
}
//フィルタ側の判定
if(
(!filterDefine.filterTextFeatureType || filterDefine.filterTextFeatureType.test(screen.type))
&& (!filterDefine.filterTextClassName || filterDefine.filterTextClassName.test(screen.className))
&& (!filterDefine.filterTextFeatureName || filterDefine.filterTextFeatureName.test(screen.screenName))
&& (!filterDefine.filterTextComment || filterDefine.filterTextComment.test(screen.comment))
&& (filterDefine.filterSelected ==='none' || ((filterDefine.filterSelected ==='true' && screen.isSelected) ||filterDefine.filterSelected ==='false' && !screen.isSelected))
&& (filterDefine.filterEnableLine ==='none' || ((filterDefine.filterEnableLine ==='true' && screen.isEnableLine) ||filterDefine.filterEnableLine ==='false' && !screen.isEnableLine))
&& (filterDefine.filterHidden ==='none' || ((filterDefine.filterHidden ==='true' && screen.isHidden) ||filterDefine.filterHidden ==='false' && !screen.isHidden))
&& (filterDefine.filterBookMark ==='none' || ((filterDefine.filterBookMark ==='true' && screen.bookMark) ||filterDefine.filterBookMark ==='false' && !screen.bookMark))
&& (!filterDefine.filterTextItemType || filterDefine.filterTextItemType.test(item.itemType))
&& (!filterDefine.filterTextItemName || filterDefine.filterTextItemName.test(item.itemName))
&& (!filterDefine.filterTextResourceType || filterDefine.filterTextResourceType.test(item.resourceType))
&& (!filterDefine.filterTextResource || filterDefine.filterTextResource.test(item.sourceScreen))
&& (!filterDefine.filterTextSourceItem || filterDefine.filterTextSourceItem.test(item.sourceItem))
&& (!filterDefine.filterTextHandoff || filterDefine.filterTextHandoff.test(item.handoff))
&& (filterDefine.filterItemHidden ==='none' || ((filterDefine.filterItemHidden ==='true' && item.isVisible) ||filterDefine.filterItemHidden ==='false' && !item.isVisible))
){
filterResult = true;
}else{
filterResult = false;
}
return searchResult && filterResult;
}
// パネルの選択をトグル
function togglePanelSelection(panelName, isSelected) {
const panel = panelPositions[panelName];
if (isSelected) {
panel.classList.add('selected-panel');
selectedPanels.add(panel);
groupedData[panelName].isSelected = true;
} else {
panel.classList.remove('selected-panel');
selectedPanels.delete(panel);
groupedData[panelName].isSelected = false;
}
}
// パネルの線描画状態をトグル
function toggleDrawPanel(panelName, isDrawing) {
const panel = panelPositions[panelName];
const checkbox = panel.querySelector('.panel-header input[type="checkbox"]');
checkbox.checked = isDrawing;
groupedData[panelName].isEnableLine = isDrawing;
requestAnimationFrame(()=>drawArrows([panelName]));
}
// パネルの表示/非表示をトグル
function toggleHidePanel(panelName, isHidden) {
const panel = panelPositions[panelName];
panel.style.display = isHidden ? 'none' : 'block';
groupedData[panelName].isHidden = isHidden;
requestAnimationFrame(()=>drawArrows([panelName]));
}
// ブックマークの状態をトグル
function toggleBookMark(panelName, isBookMark) {
const panel = panelPositions[panelName];
if(isBookMark){
panel.classList.add('bookMarked-panel');
}else{
panel.classList.remove('bookMarked-panel');
}
groupedData[panelName].bookMark = isBookMark;
requestAnimationFrame(()=>drawArrows([panelName]));
}
// Goボタン押下時に該当パネルをスクロールして強調表示
function scrollToPanel(panelName) {
const panel = panelPositions[panelName];
panel.scrollIntoView({ behavior: 'smooth', block: 'center' });
// パネルの強調表示を3秒間行う
panel.style.border = '3px solid blue';
setTimeout(() => {
panel.style.border = '';
}, 3000);
}
// 項目表示状態をトグルする処理(itemPositionsを使用)
function toggleItemVisible(screenName, itemName, isVisible) {
const itemDiv = itemPositions[`${screenName}【画面-項目】${itemName}`]; // itemPositionsを参照
const itemDisplayMode = itemDisplayModeSelect.value;
if (itemDiv) {
if (isVisible) {
if(itemDisplayMode == "指定項目")itemDiv.classList.remove('item-hidden');
groupedData[screenName].items.find(item => item.itemName === itemName).isVisible = true;
} else {
if(itemDisplayMode == "指定項目")itemDiv.classList.add('item-hidden');
groupedData[screenName].items.find(item => item.itemName === itemName).isVisible = false;
}
}
// 項目表示モードに応じて再描画をリクエスト
//renderTableBody(selectToolMode);
requestAnimationFrame(()=>drawArrows([screenName]));
}
// 初期表示はパネルモードで、該当列は非表示に
document.querySelectorAll('.toggle-column').forEach(col => col.style.display = 'none');
document.querySelectorAll('.toggle-tool-item').forEach(col => col.style.display = 'none');
function doFilter(){
renderTableBody(selectToolMode);
}
// 全ての選択をONにする
function doSelectAllOn() {
const checkboxes = document.querySelectorAll('#selectionTableBody .select-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = true;
const screenName = checkbox.closest('tr').querySelector('td:nth-child(3)').textContent;
togglePanelSelection(screenName, true); // パネルの選択状態も反映
});
}
// 全ての選択をOFFにする
function doSelectAllOff() {
const checkboxes = document.querySelectorAll('#selectionTableBody .select-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = false;
const screenName = checkbox.closest('tr').querySelector('td:nth-child(3)').textContent;
togglePanelSelection(screenName, false); // パネルの選択状態も反映
});
}
// 全ての線描画をONにする
function doEnableLineAllOn() {
const checkboxes = document.querySelectorAll('#selectionTableBody .draw-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = true;
const screenName = checkbox.closest('tr').querySelector('td:nth-child(3)').textContent;
toggleDrawPanel(screenName, true); // 線描画状態をONに
});
}
// 全ての線描画をOFFにする
function doEnableLineAllOff() {
const checkboxes = document.querySelectorAll('#selectionTableBody .draw-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = false;
const screenName = checkbox.closest('tr').querySelector('td:nth-child(3)').textContent;
toggleDrawPanel(screenName, false); // 線描画状態をOFFに
});
}
// 全ての表示をONにする
function doVisibleAllOn() {
const checkboxes = document.querySelectorAll('#selectionTableBody .hide-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = true;
const screenName = checkbox.closest('tr').querySelector('td:nth-child(3)').textContent;
toggleHidePanel(screenName, true); // ブックマークをONに
});
}
// 全ての表示をOFFにする
function doVisibleAllOFF() {
const checkboxes = document.querySelectorAll('#selectionTableBody .hide-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = false; // 非表示チェックをOFFにして表示する
const screenName = checkbox.closest('tr').querySelector('td:nth-child(3)').textContent;
toggleHidePanel(screenName, false); // パネルを表示
});
}
// 全てのブックマークをONにする
function doBookMarkAllOn() {
const checkboxes = document.querySelectorAll('#selectionTableBody .bookMark-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = true; // 非表示チェックをONにして非表示にする
const screenName = checkbox.closest('tr').querySelector('td:nth-child(3)').textContent;
toggleBookMark(screenName, true); // パネルを非表示に
});
}
// 全てのブックマークをOFFにする
function doBookMarkAllOff() {
const checkboxes = document.querySelectorAll('#selectionTableBody .bookMark-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = false;
const screenName = checkbox.closest('tr').querySelector('td:nth-child(3)').textContent;
toggleBookMark(screenName, false); // ブックマークをOFFに
});
}
// 「項目表示on/off」ボタンの処理
function doItemVisibleAllOn() {
const checkboxes = document.querySelectorAll('#selectionTableBody .item-visible-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = true;
const screenName = checkbox.closest('tr').querySelector('td:nth-child(3)').textContent;
const itemName = checkbox.closest('tr').querySelector('td:nth-child(5)').textContent;
toggleItemVisible(screenName, itemName, true); // 項目表示をonに
});
}
function doItemVisibleAllOff() {
const checkboxes = document.querySelectorAll('#selectionTableBody .item-visible-checkbox');
checkboxes.forEach(checkbox => {
checkbox.checked = false;
const screenName = checkbox.closest('tr').querySelector('td:nth-child(3)').textContent;
const itemName = checkbox.closest('tr').querySelector('td:nth-child(5)').textContent;
toggleItemVisible(screenName, itemName, false); // 項目表示をoffに
});
}
const customContextMenu = document.getElementById('customContextMenu');
let clickX = 0, clickY = 0;
// chart部分で右クリックイベントを追加
dom_contentsParent.addEventListener('contextmenu', (e) => {
e.preventDefault(); // ブラウザのデフォルトコンテキストメニューを無効化
// 右クリック位置を記録 (chart内の相対座標)
clickX = calcPositionOnContents('x', e.clientX);
clickY = calcPositionOnContents('y', e.clientY);
// カスタムコンテキストメニューを表示
customContextMenu.style.display = 'block';
customContextMenu.style.left = `${e.pageX}px`;
customContextMenu.style.top = `${e.pageY}px`;
});
// クリックした場所のメニューを閉じる
document.addEventListener('click', () => {
customContextMenu.style.display = 'none';
});
// 「ここにまとめて移動」を押した際の処理
document.getElementById('moveHereButton').addEventListener('click', () => {
if (selectedPanels.size === 0) return; // 選択されたパネルがない場合は何もしない
// 選択されているパネルの最も上と左にあるパネルの座標を取得
let minX = Infinity, minY = Infinity;
selectedPanels.forEach(panel => {
const panelX = parseFloat(panel.style.left);
const panelY = parseFloat(panel.style.top);
if (panelX < minX) minX = panelX; // 一番左のパネルのX座標
if (panelY < minY) minY = panelY; // 一番上のパネルのY座標
});
let updatePanels = [];
// 選択されたパネルを右クリックした位置に移動
selectedPanels.forEach(panel => {
const offsetX = parseFloat(panel.style.left) - minX;
const offsetY = parseFloat(panel.style.top) - minY;
panel.style.left = `${clickX + offsetX}px`;
panel.style.top = `${clickY + offsetY}px`;
// groupedData の位置も更新
const panelName = panel.querySelector('.panel-header span').textContent;
groupedData[panelName].positionX = panel.style.left;
groupedData[panelName].positionY = panel.style.top;
updatePanels.push(panelName);
});
// メニューを非表示にする
customContextMenu.style.display = 'none';
requestAnimationFrame(()=>drawArrows(updatePanels));
});
// 「選択をすべてクリア」を押した際の処理
document.getElementById('clearSelectionButton').addEventListener('click', () => {
if (selectedPanels.size === 0) return; // 選択されているパネルがない場合は何もしない
// 選択状態のパネルをすべて解除
selectedPanels.forEach(panel => {
panel.classList.remove('selected-panel');
const panelName = panel.querySelector('.panel-header span').textContent;
groupedData[panelName].isSelected = false;
});
// 選択パネルのセットをクリア
selectedPanels.clear();
// メニューを非表示にする
customContextMenu.style.display = 'none';
});
// 「ここに集める」を押した際の処理
document.getElementById('gatherHereButton').addEventListener('click', (event) => {
if (selectedPanels.size === 0) return; // 選択されているパネルがない場合は何もしない
clickX ; // 右クリック時のX座標
clickY; // 右クリック時のY座標
const offsetStep = 100; // 各パネルの位置をずらす量
let currentX = clickX;
let currentY = clickY;
let updaePanels = [];
selectedPanels.forEach((panel) => {
// パネルをクリック位置に移動し、各パネルが重ならないようにオフセットする
panel.style.left = `${currentX}px`;
panel.style.top = `${currentY}px`;
// groupedData内のパネルの位置を更新
const panelName = panel.querySelector('.panel-header span').textContent;
groupedData[panelName].positionX = panel.style.left;
groupedData[panelName].positionY = panel.style.top;
// X座標をずらして次のパネルの位置を計算
//currentX += offsetStep;
currentY += offsetStep; // Y座標も少しずらして配置
// 次のパネルの表示領域がはみ出さないように必要であればX座標のリセットも考慮する
if (currentX + panel.offsetWidth > window.innerWidth) {
currentX = clickX;
currentY += panel.offsetHeight + offsetStep; // 行を増やす
}
updaePanels.push(panelName);
});
// メニューを非表示にする
customContextMenu.style.display = 'none';
requestAnimationFrame(()=>drawArrows(updaePanels));
});
/** 共通処理 */
// 制御点を `controlPointsSvg` に追加する関数
function addControlPoint(clickEvent, key1, key2,point,isHidden) {
// 指定した位置に制御点を挿入
if(clickEvent){
const clickX = calcPositionOnContents('x', clickEvent.pageX);
const clickY = calcPositionOnContents('y', clickEvent.pageY);
// 制御点リストを取得
let pointInfo = controlPoints[`${key1}【機能間依存】${key2}`];
if (!pointInfo) {
pointInfo = { points: [] };
controlPoints[`${key1}【機能間依存】${key2}`] = pointInfo;
}
// パネルの位置と既存の制御点のリストをまとめて、挿入位置を計算
const panel2 = panelPositions[key1].getBoundingClientRect();
const panel1 = panelPositions[key2].getBoundingClientRect();
const points = [
{ x: calcPositionOnContents('x',panel1.right), y: calcPositionOnContents('y',panel1.top) }, // パネル1の接続位置(例: 右上)
...pointInfo.points,
{ x: calcPositionOnContents('x',panel2.left), y: calcPositionOnContents('y',panel2.top) } // パネル2の接続位置(例: 左上)
];
// 最も近い線分を判定し、挿入位置を計算
const insertIndex = findInsertIndexBetweenPoints(points, clickX, clickY);
// 新しい制御点の座標を決定
const controlPointId = `${key1}【制御点ID】${key2}【制御点ID】${Date.now()}【制御点ID】${Math.random()}`;
const newPoint = { id: controlPointId, x: clickX, y: clickY };
pointInfo.points.splice(insertIndex, 0, newPoint);
createControlPointElement(newPoint, key1, key2);
requestAnimationFrame(() => drawArrows([key1, key2]));
}else{
createControlPointElement(point, key1, key2);
}
}
// 四角形の当たり判定に基づいて挿入位置を決定する
function findInsertIndexBetweenPoints(points, clickX, clickY) {
for (let i = 0; i < points.length - 1; i++) {
const rect = {
x1: points[i].x,
y1: points[i].y,
x2: points[i + 1].x,
y2: points[i + 1].y,
width: 40 // 判定する四角形の幅
};
if (isPointInRectangle(clickX, clickY, rect)) {
return i ; // この線分の間に挿入する
}
}
return points.length - 1; // デフォルトは最後に挿入
}
// 制御点のDOM要素を作成して追加する
function createControlPointElement(point, key1, key2) {
const controlPoint = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
controlPoint.setAttribute('cx', point.x);
controlPoint.setAttribute('cy', point.y);
controlPoint.setAttribute('r', '15');
controlPoint.setAttribute('fill', 'blue');
controlPoint.classList.add('control-point');
controlPoint.setAttribute('data-id', point.id);
dom_controlPointsSvg.appendChild(controlPoint);
// ドラッグして制御点を移動
let isDragging = false;
controlPoint.addEventListener('mousedown', (e) => {
isDragging = true;
let initialMouseX = calcPositionOnContents('x',e.clientX);
let initialMouseY = calcPositionOnContents('y',e.clientY);
let initialPositions = [];
let initalControlPositions = [];
if (selectedControlPoints.has(controlPoint)) {
selectedControlPoints.forEach(selectedPoint => {
initalControlPositions.push({
point: selectedPoint,
startX: parseInt(selectedPoint.getAttribute('cx')),
startY: parseInt(selectedPoint.getAttribute('cy'))
});
});
selectedPanels.forEach(selectedPanel => {
const rect = selectedPanel.getBoundingClientRect();
initialPositions.push({
panel: selectedPanel,
startX: calcPositionOnContents('x',rect.left),
startY: calcPositionOnContents('y',rect.top)
});
});
}else{
initalControlPositions.push({
point: controlPoint,
startX: parseInt(controlPoint.getAttribute('cx')),
startY: parseInt(controlPoint.getAttribute('cy'))
});
}
function onMouseMove(moveEvent) {
if (isDragging) {
const mepageX = calcPositionOnContents('x', moveEvent.pageX);
const mepageY = calcPositionOnContents('y', moveEvent.pageY);
const updatePanel1 = movePanels(initialMouseX,initialMouseY,mepageX, mepageY,initialPositions);
const updatePanel2 = movePoints(initialMouseX,initialMouseY,mepageX, mepageY,initalControlPositions);
const mergedUpdatePanel = mergeArraysUnique(updatePanel1,updatePanel2);
requestAnimationFrame(()=>drawArrows(mergedUpdatePanel));
}
}
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', function onMouseUp() {
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
isDragging = false;
});
});
// ダブルクリックで制御点を削除
controlPoint.addEventListener('dblclick', () => {
dom_controlPointsSvg.removeChild(controlPoint);
const points = controlPoints[`${key1}【機能間依存】${key2}`].points;
const pointIndex = points.findIndex(p => p.id === point.id);
if (pointIndex !== -1) {
points.splice(pointIndex, 1); // controlPointsから削除
}
requestAnimationFrame(() => drawArrows([key1, key2]));
});
}
function isPointInRectangle(px, py, rect) {
const { x1, y1, x2, y2, width } = rect;
const angle = Math.atan2(y2 - y1, x2 - x1); // 角度
const dx = Math.cos(angle) * width / 2;
const dy = Math.sin(angle) * width / 2;
// 四角形の4つの頂点を計算
const points = [
{ x: x1 - dy, y: y1 + dx },
{ x: x1 + dy, y: y1 - dx },
{ x: x2 + dy, y: y2 - dx },
{ x: x2 - dy, y: y2 + dx }
];
// 四角形の頂点リスト内に点(px, py)があるかを判定
return isPointInPolygon(px, py, points);
}
function isPointInPolygon(px, py, vertices) {
let collision = false;
let next = 0;
for (let current = 0; current < vertices.length; current++) {
next = (current + 1) % vertices.length;
const vc = vertices[current];
const vn = vertices[next];
if (((vc.y > py) != (vn.y > py)) && (px < (vn.x - vc.x) * (py - vc.y) / (vn.y - vc.y) + vc.x)) {
collision = !collision;
}
}
return collision;
}
function mergeArraysUnique(array1, array2) {
// 2つの配列をセットに追加して重複を排除し、最後に配列に戻す
if(array1 && array2) return [...new Set([...array1, ...array2])];
if(!array1 && array2)return array2;
if(!array2 && array1)return array1;
return [];
}
// コメントパネル追加ボタンにイベントリスナーを設定
document.getElementById("addCommentPanelButton").addEventListener("click", () => {
addCommentPanel(clickX,clickY);
});
function addCommentPanel(x,y) {
const panel = document.getElementById("commentPanelTemplate").cloneNode(true);
panel.style.display = "block";
panel.id = `commentPanel_${Date.now()}`;
panel.style.left = `${x}px`;
panel.style.top = `${y}px`;
addEvent4CommentPane(panel);
// 初期データ設定
commentPanels.push({
id: panel.id,
text: "",
color: "#ffffff",
x: x,
y: y,
width: 400,
height: 150
});
commentPanelsArea.appendChild(panel);
}
function importCommentPanels(data) {
data.forEach(panelData => {
const panel = document.getElementById("commentPanelTemplate").cloneNode(true);
panel.style.display = "block";
panel.id = panelData.id;
panel.style.left = `${panelData.x}px`;
panel.style.top = `${panelData.y}px`;
panel.style.width = `${panelData.width}px`;
panel.style.height = `${panelData.height}px`;
panel.style.backgroundColor = panelData.color;
const textarea = panel.querySelector(".comment-textarea");
textarea.value = panelData.text;
addEvent4CommentPane(panel);
commentPanelsArea.appendChild(panel);
});
}
function addEvent4CommentPane(panel){
// ドラッグによる移動機能
setupDrag(panel);
// 色変更ボタンの処理
const colorPicker = panel.querySelector(".color-change-button");
colorPicker.addEventListener("input", (e) => {
panel.style.backgroundColor = e.target.value;
updateCommentPanelData(panel.id, "color", e.target.value);
});
// テキストエリアの自動リサイズ
const textarea = panel.querySelector(".comment-textarea");
textarea.addEventListener("input", (e) => {
textarea.style.height = "auto";
textarea.style.height = `${textarea.scrollHeight}px`;
updateCommentPanelData(panel.id, "text", e.target.value);
});
// 削除ボタンの処理
const deleteButton = panel.querySelector(".delete-button");
deleteButton.addEventListener("click", () => {
commentPanelsArea.removeChild(panel);
removeCommentPanelData(panel.id);
});
//最前面に移動ボタンの処理
const bringToFrontButton = panel.querySelector("#bringToFrontButton");
bringToFrontButton.addEventListener("click", () => {
updateCommentPanelOrder(panel, "bringToFront");
});
const sendToBackButton = panel.querySelector("#sendToBackButton");
sendToBackButton.addEventListener("click", () => {
updateCommentPanelOrder(panel, "sendToBack");
});
// リサイズ機能
setupResize(panel);
}
// コメントパネルの位置・色などの更新を保持
function updateCommentPanelData(id, field, value) {
const panel = commentPanels.find(p => p.id === id);
if (panel) {
panel[field] = value;
}
}
function removeCommentPanelData(id) {
commentPanels = commentPanels.filter(p => p.id !== id);
}
// ドラッグ移動機能
function setupDrag(panel) {
let offsetX, offsetY;
panel.addEventListener("mousedown", (e) => {
if (e.target.classList.contains("comment-textarea") || e.target.classList.contains("resize-handle")) return;
let startX = calcPositionOnContents('x',panel.getBoundingClientRect().left);
let startY =calcPositionOnContents('y',panel.getBoundingClientRect().top);
let initialX = calcPositionOnContents('x',e.clientX);
let initialY = calcPositionOnContents('y',e.clientY);
function onMouseMove(e) {
let currentMouseX = calcPositionOnContents('x',e.clientX);
let currentMouseY = calcPositionOnContents('y',e.clientY);
let deltaX = currentMouseX - initialX;
let deltaY = currentMouseY - initialY;
panel.style.left = `${startX + deltaX}px`;
panel.style.top = `${startY + deltaY}px`;
updateCommentPanelData(panel.id, "x", parseInt(panel.style.left));
updateCommentPanelData(panel.id, "y", parseInt(panel.style.top));
}
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", () => {
document.removeEventListener("mousemove", onMouseMove);
}, { once: true });
});
}
// リサイズ機能
function setupResize(panel) {
const resizeHandle = panel.querySelector(".resize-handle");
resizeHandle.addEventListener("mousedown", (e) => {
e.preventDefault();
const startX = calcPositionOnContents('x',e.clientX);
const startY = calcPositionOnContents('y',e.clientY);
const startWidth = panel.offsetWidth;
const startHeight = panel.offsetHeight;
function onMouseMove(e) {
const newWidth = startWidth + (calcPositionOnContents('x',e.clientX) - startX);
const newHeight = startHeight + (calcPositionOnContents('y',e.clientY) - startY);
panel.style.width = `${newWidth}px`;
panel.style.height = `${newHeight}px`;
updateCommentPanelData(panel.id, "width", newWidth);
updateCommentPanelData(panel.id, "height", newHeight);
}
document.addEventListener("mousemove", onMouseMove);
document.addEventListener("mouseup", () => {
document.removeEventListener("mousemove", onMouseMove);
}, { once: true });
});
}
//パネルの順序入れ替え
function updateCommentPanelOrder(panelDOM, mode) {
// パネルのIDからcommentPanels内の対応オブジェクトを特定
const panelId = panelDOM.id;
const panelIndex = commentPanels.findIndex(panel => panel.id === panelId);
if (panelIndex === -1) return; // パネルが見つからなければ処理を終了
const panelData = commentPanels[panelIndex];
if (mode === "bringToFront") {
// 最前面に移動
// 1. commentPanelsから対象オブジェクトを削除し、末尾に追加
commentPanels.splice(panelIndex, 1);
commentPanels.push(panelData);
// 2. DOMからパネルを削除し、再度末尾に追加
commentPanelsArea.removeChild(panelDOM);
commentPanelsArea.appendChild(panelDOM);
} else if (mode === "sendToBack") {
// 最背面に移動
// 1. commentPanelsから対象オブジェクトを削除し、先頭に追加
commentPanels.splice(panelIndex, 1);
commentPanels.unshift(panelData);
// 2. DOMからパネルを削除し、先頭に挿入
commentPanelsArea.removeChild(panelDOM);
commentPanelsArea.insertBefore(panelDOM, commentPanelsArea.firstChild);
}
}
</script>
</body>
</html>