dev-tools / dev-tools.js
soiz1's picture
Update dev-tools.js
bd57d00 verified
raw
history blame
61.4 kB
(function() {
// Web Vitalsスクリプトを動的に読み込み
const webVitalsScript = document.createElement('script');
webVitalsScript.src = 'https://unpkg.com/[email protected]/dist/web-vitals.iife.js';
document.head.appendChild(webVitalsScript);
// スタイルの動的追加 (ダークテーマ)
const style = document.createElement('style');
style.textContent = `
:root {
--bg-color: #1e1e1e;
--panel-bg: #252526;
--border-color: #3c3c3c;
--text-color: #e0e0e0;
--text-muted: #a0a0a0;
--primary-color: #007acc;
--primary-hover: #3e9fda;
--success-color: #4caf50;
--error-color: #f44336;
--warning-color: #ff9800;
--info-color: #2196f3;
--highlight-bg: rgba(0, 122, 204, 0.2);
--tab-bg: #2d2d2d;
--tab-active-bg: #1e1e1e;
--console-log-color: #e0e0e0;
--console-error-color: #f44336;
--console-warn-color: #ff9800;
--console-info-color: #4fc3f7;
--json-key: #9cdcfe;
--json-string: #ce9178;
--json-number: #b5cea8;
--json-boolean: #569cd6;
--json-null: #569cd6;
--dom-tag: #569cd6;
--dom-attr: #9cdcfe;
--dom-text: #d4d4d4;
}
.devtools-container {
position: fixed;
bottom: 0;
left: 0;
width: 100%;
height: 300px;
background-color: var(--panel-bg);
border-top: 1px solid var(--border-color);
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.3);
z-index: 9999;
display: flex;
flex-direction: column;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
color: var(--text-color);
}
.devtools-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 5px 10px;
background-color: var(--tab-bg);
border-bottom: 1px solid var(--border-color);
}
.devtools-tabs {
display: flex;
gap: 5px;
}
.devtools-tab {
padding: 5px 10px;
cursor: pointer;
border-radius: 3px 3px 0 0;
background-color: var(--tab-bg);
border: 1px solid var(--border-color);
border-bottom: none;
font-size: 12px;
color: var(--text-muted);
}
.devtools-tab.active {
background-color: var(--tab-active-bg);
color: var(--text-color);
border-bottom: 1px solid var(--tab-active-bg);
margin-bottom: -1px;
font-weight: bold;
}
.devtools-close {
background: none;
border: none;
font-size: 16px;
cursor: pointer;
padding: 0 5px;
color: var(--text-color);
}
.devtools-content {
flex: 1;
overflow: auto;
position: relative;
background-color: var(--panel-bg);
}
.devtools-panel {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
padding: 10px;
overflow: auto;
display: none;
background-color: var(--panel-bg);
}
.devtools-panel.active {
display: block;
}
/* Console スタイル */
#console-log {
white-space: pre-wrap;
margin: 0;
line-height: 1.4;
flex: 1;
color: var(--console-log-color);
font-family: 'Consolas', 'Monaco', monospace;
font-size: 13px;
}
.console-log {
color: var(--console-log-color);
}
.console-error {
color: var(--console-error-color);
}
.console-warn {
color: var(--console-warn-color);
}
.console-info {
color: var(--console-info-color);
}
.console-input {
width: calc(100% - 16px);
background: var(--tab-bg);
border: 1px solid var(--border-color);
color: var(--text-color);
padding: 8px;
margin-top: 10px;
font-family: monospace;
border-radius: 3px;
}
/* Elements スタイル */
.elements-container {
display: flex;
flex: 1;
overflow: hidden;
}
.dom-tree {
font-family: 'Consolas', 'Monaco', monospace;
flex: 1;
overflow: auto;
border-right: 1px solid var(--border-color);
padding-right: 10px;
color: var(--dom-text);
font-size: 13px;
}
.dom-node {
margin-left: 15px;
position: relative;
line-height: 1.4;
transition: background-color 0.3s;
}
.dom-node.selected {
background: var(--highlight-bg);
}
.dom-node.highlight {
animation: highlight-fade 1.5s;
}
@keyframes highlight-fade {
0% { background-color: rgba(79, 195, 247, 0.5); }
100% { background-color: transparent; }
}
.dom-tag {
color: var(--dom-tag);
font-weight: bold;
}
.dom-attr {
color: var(--dom-attr);
}
.dom-attr.editable:hover {
text-decoration: underline;
cursor: pointer;
}
.dom-text {
color: var(--dom-text);
}
.dom-edit-input {
background: var(--panel-bg);
border: 1px solid var(--primary-color);
padding: 0 2px;
margin: -1px 0;
font-family: monospace;
min-width: 50px;
color: var(--text-color);
}
.css-panel {
flex: 1;
overflow: auto;
padding-left: 10px;
font-size: 13px;
}
.css-rule {
margin-bottom: 15px;
border: 1px solid var(--border-color);
padding: 8px;
background-color: var(--tab-bg);
border-radius: 3px;
}
.css-selector {
color: var(--primary-color);
margin-bottom: 5px;
font-weight: bold;
}
.css-property {
display: flex;
margin-bottom: 3px;
}
.css-property-name {
color: var(--dom-attr);
min-width: 120px;
}
.css-property-value {
color: var(--dom-text);
flex: 1;
}
.css-toggle {
margin-left: 10px;
color: var(--error-color);
cursor: pointer;
}
/* Context Menu */
.context-menu {
position: absolute;
background: var(--panel-bg);
border: 1px solid var(--border-color);
z-index: 10000;
min-width: 200px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.3);
display: none;
border-radius: 3px;
overflow: hidden;
}
.context-menu-item {
padding: 8px 15px;
cursor: pointer;
color: var(--text-color);
font-size: 13px;
}
.context-menu-item:hover {
background: var(--primary-color);
color: #000;
}
/* Storage スタイル */
.storage-table {
width: 100%;
border-collapse: collapse;
margin-bottom: 10px;
font-size: 13px;
}
.storage-table th, .storage-table td {
border: 1px solid var(--border-color);
padding: 5px;
text-align: left;
}
.storage-table th {
background: var(--tab-bg);
}
.storage-actions {
display: flex;
gap: 5px;
}
.storage-btn {
background: var(--primary-color);
border: none;
padding: 2px 5px;
cursor: pointer;
border-radius: 3px;
color: #000;
font-size: 12px;
}
.editable {
cursor: pointer;
padding: 2px 5px;
border: 1px dashed transparent;
}
.editable:hover {
border-color: var(--primary-color);
}
.add-btn {
background: var(--primary-color);
color: #000;
border: none;
padding: 5px 10px;
margin-top: 10px;
cursor: pointer;
border-radius: 3px;
font-size: 13px;
}
.add-btn:hover {
background: var(--primary-hover);
}
/* Network スタイル */
.network-container {
display: flex;
height: 100%;
overflow: hidden;
}
.network-requests {
width: 40%;
overflow-y: auto;
border-right: 1px solid var(--border-color);
font-size: 13px;
}
.network-details {
width: 60%;
overflow-y: auto;
padding-left: 10px;
}
.network-request {
padding: 8px;
border-bottom: 1px solid var(--border-color);
cursor: pointer;
display: flex;
align-items: center;
}
.network-request:hover {
background-color: rgba(0, 122, 204, 0.1);
}
.network-request.selected {
background-color: var(--highlight-bg);
}
.network-status {
width: 20px;
height: 20px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 8px;
flex-shrink: 0;
}
.network-status.success {
background-color: var(--success-color);
color: white;
}
.network-status.error {
background-color: var(--error-color);
color: white;
}
.network-method {
font-weight: bold;
margin-right: 8px;
color: var(--primary-color);
min-width: 40px;
}
.network-url {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.network-time {
color: var(--text-muted);
font-size: 11px;
margin-left: 8px;
}
.network-detail-section {
margin-bottom: 15px;
}
.network-detail-title {
font-weight: bold;
margin-bottom: 5px;
color: var(--primary-color);
}
.network-detail-content {
background: var(--tab-bg);
padding: 8px;
border-radius: 3px;
border: 1px solid var(--border-color);
font-family: monospace;
white-space: pre-wrap;
font-size: 12px;
max-height: 200px;
overflow-y: auto;
}
/* Web Vitals スタイル */
.vitals-container {
display: flex;
flex-direction: column;
gap: 15px;
}
.vital-card {
background: var(--tab-bg);
border: 1px solid var(--border-color);
border-radius: 5px;
padding: 15px;
}
.vital-title {
font-weight: bold;
margin-bottom: 10px;
color: var(--primary-color);
display: flex;
justify-content: space-between;
align-items: center;
}
.vital-value {
font-size: 24px;
font-weight: bold;
margin: 10px 0;
}
.vital-good {
color: var(--success-color);
}
.vital-needs-improvement {
color: var(--warning-color);
}
.vital-poor {
color: var(--error-color);
}
.vital-description {
font-size: 13px;
color: var(--text-muted);
}
.vital-thresholds {
display: flex;
margin-top: 10px;
font-size: 12px;
}
.vital-threshold {
flex: 1;
text-align: center;
padding: 5px;
border-radius: 3px;
}
.vital-threshold.active {
background: rgba(0, 122, 204, 0.2);
}
/* DOM Tree Toggle */
.dom-toggle {
position: absolute;
left: -12px;
top: 2px;
width: 10px;
height: 10px;
cursor: pointer;
background-color: var(--text-muted);
clip-path: polygon(0 0, 100% 50%, 0 100%);
transition: transform 0.2s;
}
.dom-toggle.collapsed {
transform: rotate(-90deg);
}
.dom-children {
overflow: hidden;
transition: max-height 0.3s ease-out;
}
/* JSON スタイル */
.json-key {
color: var(--json-key);
}
.json-string {
color: var(--json-string);
}
.json-number {
color: var(--json-number);
}
.json-boolean {
color: var(--json-boolean);
}
.json-null {
color: var(--json-null);
}
`;
document.head.appendChild(style);
// グローバル変数
let contextMenu = null;
let selectedElement = null;
let selectedDOMNode = null;
let activeEditElement = null;
let networkRequests = [];
let selectedRequest = null;
let vitalsData = {
CLS: null,
FCP: null,
FID: null,
LCP: null,
TTFB: null
};
// DOM変更を監視するMutationObserver
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
highlightNode(node);
}
});
}
});
refreshElementsPanel();
});
// ノードをハイライト表示
function highlightNode(node) {
const elementId = node.id || Math.random().toString(36).substr(2, 9);
node.setAttribute('data-element-id', elementId);
setTimeout(() => {
const domNode = document.querySelector(`[data-element-id="${elementId}"]`);
if (domNode) {
domNode.classList.add('highlight');
setTimeout(() => {
domNode.classList.remove('highlight');
}, 1500);
}
}, 100);
}
// 開発者ツールのメイン関数
function createDevTools() {
const container = document.createElement('div');
container.className = 'devtools-container';
container.id = 'devtools-container';
container.style.display = 'none';
// ヘッダー部分
const header = document.createElement('div');
header.className = 'devtools-header';
const tabs = document.createElement('div');
tabs.className = 'devtools-tabs';
const consoleTab = createTab('Console', 'console');
const elementsTab = createTab('Elements', 'elements');
const networkTab = createTab('Network', 'network');
const storageTab = createTab('Storage', 'storage');
const vitalsTab = createTab('Web Vitals', 'vitals');
tabs.appendChild(consoleTab);
tabs.appendChild(elementsTab);
tabs.appendChild(networkTab);
tabs.appendChild(storageTab);
tabs.appendChild(vitalsTab);
const closeBtn = document.createElement('button');
closeBtn.className = 'devtools-close';
closeBtn.textContent = '×';
closeBtn.onclick = toggleDevTools;
header.appendChild(tabs);
header.appendChild(closeBtn);
// コンテンツ部分
const content = document.createElement('div');
content.className = 'devtools-content';
const consolePanel = createConsolePanel();
const elementsPanel = createElementsPanel();
const networkPanel = createNetworkPanel();
const storagePanel = createStoragePanel();
const vitalsPanel = createVitalsPanel();
content.appendChild(consolePanel);
content.appendChild(elementsPanel);
content.appendChild(networkPanel);
content.appendChild(storagePanel);
content.appendChild(vitalsPanel);
container.appendChild(header);
container.appendChild(content);
document.body.appendChild(container);
// コンテキストメニュー作成
createContextMenu();
// タブ切り替え機能
function createTab(name, panelId) {
const tab = document.createElement('div');
tab.className = 'devtools-tab';
tab.textContent = name;
tab.onclick = () => {
document.querySelectorAll('.devtools-tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.devtools-panel').forEach(p => p.classList.remove('active'));
tab.classList.add('active');
document.getElementById(panelId + '-panel').classList.add('active');
// ネットワークタブを選択した場合はリクエストリストを更新
if (panelId === 'network') {
renderNetworkRequests();
}
};
return tab;
}
elementsTab.click();
}
// Web Vitalsパネル作成
function createVitalsPanel() {
const panel = document.createElement('div');
panel.className = 'devtools-panel';
panel.id = 'vitals-panel';
const container = document.createElement('div');
container.className = 'vitals-container';
panel.appendChild(container);
// CLS (Cumulative Layout Shift)
const clsCard = document.createElement('div');
clsCard.className = 'vital-card';
clsCard.innerHTML = `
<div class="vital-title">CLS (Cumulative Layout Shift) <span class="vital-description">視覚的な安定性</span></div>
<div class="vital-value" id="cls-value">-</div>
<div class="vital-thresholds">
<div class="vital-threshold">Good: &lt; 0.1</div>
<div class="vital-threshold">Needs Improvement: &lt; 0.25</div>
<div class="vital-threshold">Poor: ≥ 0.25</div>
</div>
`;
container.appendChild(clsCard);
// FCP (First Contentful Paint)
const fcpCard = document.createElement('div');
fcpCard.className = 'vital-card';
fcpCard.innerHTML = `
<div class="vital-title">FCP (First Contentful Paint) <span class="vital-description">最初のコンテンツ表示</span></div>
<div class="vital-value" id="fcp-value">-</div>
<div class="vital-thresholds">
<div class="vital-threshold">Good: &lt; 1.8s</div>
<div class="vital-threshold">Needs Improvement: &lt; 3s</div>
<div class="vital-threshold">Poor: ≥ 3s</div>
</div>
`;
container.appendChild(fcpCard);
// FID (First Input Delay)
const fidCard = document.createElement('div');
fidCard.className = 'vital-card';
fidCard.innerHTML = `
<div class="vital-title">FID (First Input Delay) <span class="vital-description">最初の入力遅延</span></div>
<div class="vital-value" id="fid-value">-</div>
<div class="vital-thresholds">
<div class="vital-threshold">Good: &lt; 100ms</div>
<div class="vital-threshold">Needs Improvement: &lt; 300ms</div>
<div class="vital-threshold">Poor: ≥ 300ms</div>
</div>
`;
container.appendChild(fidCard);
// LCP (Largest Contentful Paint)
const lcpCard = document.createElement('div');
lcpCard.className = 'vital-card';
lcpCard.innerHTML = `
<div class="vital-title">LCP (Largest Contentful Paint) <span class="vital-description">最大のコンテンツ表示</span></div>
<div class="vital-value" id="lcp-value">-</div>
<div class="vital-thresholds">
<div class="vital-threshold">Good: &lt; 2.5s</div>
<div class="vital-threshold">Needs Improvement: &lt; 4s</div>
<div class="vital-threshold">Poor: ≥ 4s</div>
</div>
`;
container.appendChild(lcpCard);
// TTFB (Time to First Byte)
const ttfbCard = document.createElement('div');
ttfbCard.className = 'vital-card';
ttfbCard.innerHTML = `
<div class="vital-title">TTFB (Time to First Byte) <span class="vital-description">最初のバイト到達時間</span></div>
<div class="vital-value" id="ttfb-value">-</div>
<div class="vital-thresholds">
<div class="vital-threshold">Good: &lt; 800ms</div>
<div class="vital-threshold">Needs Improvement: &lt; 1.8s</div>
<div class="vital-threshold">Poor: ≥ 1.8s</div>
</div>
`;
container.appendChild(ttfbCard);
// Web Vitalsの計測を開始
webVitalsScript.onload = function() {
if (window.webVitals) {
window.webVitals.getCLS(updateCLS);
window.webVitals.getFCP(updateFCP);
window.webVitals.getFID(updateFID);
window.webVitals.getLCP(updateLCP);
window.webVitals.getTTFB(updateTTFB);
}
};
function updateCLS(metric) {
vitalsData.CLS = metric.value;
updateVitalDisplay('cls', metric.value, 0.1, 0.25);
}
function updateFCP(metric) {
vitalsData.FCP = metric.value;
updateVitalDisplay('fcp', metric.value, 1800, 3000);
}
function updateFID(metric) {
vitalsData.FID = metric.value;
updateVitalDisplay('fid', metric.value, 100, 300);
}
function updateLCP(metric) {
vitalsData.LCP = metric.value;
updateVitalDisplay('lcp', metric.value, 2500, 4000);
}
function updateTTFB(metric) {
vitalsData.TTFB = metric.value;
updateVitalDisplay('ttfb', metric.value, 800, 1800);
}
function updateVitalDisplay(id, value, goodThreshold, needsImprovementThreshold) {
const element = document.getElementById(`${id}-value`);
if (!element) return;
// ミリ秒を秒に変換 (TTFB以外)
const displayValue = id === 'ttfb' ?
`${Math.round(value)}ms` :
`${value.toFixed(2)}${id === 'cls' ? '' : 's'}`;
element.textContent = displayValue;
// 閾値に基づいてクラスを設定
element.className = 'vital-value';
if (value <= goodThreshold) {
element.classList.add('vital-good');
} else if (value <= needsImprovementThreshold) {
element.classList.add('vital-needs-improvement');
} else {
element.classList.add('vital-poor');
}
// 閾値表示を更新
const thresholds = element.parentElement.querySelectorAll('.vital-threshold');
thresholds.forEach((threshold, index) => {
threshold.classList.remove('active');
if (
(index === 0 && value <= goodThreshold) ||
(index === 1 && value > goodThreshold && value <= needsImprovementThreshold) ||
(index === 2 && value > needsImprovementThreshold)
) {
threshold.classList.add('active');
}
});
}
return panel;
}
// ネットワークパネル作成
function createNetworkPanel() {
const panel = document.createElement('div');
panel.className = 'devtools-panel';
panel.id = 'network-panel';
const container = document.createElement('div');
container.className = 'network-container';
panel.appendChild(container);
// リクエストリスト
const requestsList = document.createElement('div');
requestsList.className = 'network-requests';
container.appendChild(requestsList);
// 詳細パネル
const detailsPanel = document.createElement('div');
detailsPanel.className = 'network-details';
container.appendChild(detailsPanel);
// ネットワークリクエストを監視
setupNetworkMonitoring();
return panel;
}
// ネットワーク監視を設定
function setupNetworkMonitoring() {
const originalFetch = window.fetch;
window.fetch = async function(...args) {
const [input, init] = args;
const url = typeof input === 'string' ? input : input.url;
const method = (init && init.method) || 'GET';
const startTime = performance.now();
const requestData = {
id: Math.random().toString(36).substr(2, 9),
url,
method,
status: 'pending',
startTime,
endTime: null,
duration: null,
requestHeaders: init ? init.headers : null,
requestBody: init ? init.body : null,
response: null,
error: null
};
networkRequests.push(requestData);
renderNetworkRequests();
try {
const response = await originalFetch.apply(this, args);
const endTime = performance.now();
requestData.status = response.ok ? 'success' : 'error';
requestData.endTime = endTime;
requestData.duration = endTime - startTime;
requestData.response = {
status: response.status,
statusText: response.statusText,
headers: Object.fromEntries(response.headers.entries()),
body: null
};
// レスポンスボディをクローンして読み取る
const clonedResponse = response.clone();
const contentType = clonedResponse.headers.get('content-type') || '';
if (contentType.includes('application/json')) {
requestData.response.body = await clonedResponse.json();
} else {
requestData.response.body = await clonedResponse.text();
}
renderNetworkRequests();
return response;
} catch (error) {
const endTime = performance.now();
requestData.status = 'error';
requestData.endTime = endTime;
requestData.duration = endTime - startTime;
requestData.error = {
name: error.name,
message: error.message,
stack: error.stack
};
renderNetworkRequests();
throw error;
}
};
const originalOpen = XMLHttpRequest.prototype.open;
const originalSend = XMLHttpRequest.prototype.send;
XMLHttpRequest.prototype.open = function(method, url) {
this._method = method;
this._url = url;
this._startTime = performance.now();
this._requestHeaders = {};
const originalSetRequestHeader = this.setRequestHeader;
this.setRequestHeader = function(header, value) {
this._requestHeaders[header] = value;
originalSetRequestHeader.call(this, header, value);
};
return originalOpen.apply(this, arguments);
};
XMLHttpRequest.prototype.send = function(body) {
const requestData = {
id: Math.random().toString(36).substr(2, 9),
url: this._url,
method: this._method,
status: 'pending',
startTime: this._startTime,
endTime: null,
duration: null,
requestHeaders: this._requestHeaders,
requestBody: body,
response: null,
error: null
};
networkRequests.push(requestData);
renderNetworkRequests();
this.addEventListener('load', function() {
const endTime = performance.now();
requestData.status = this.status >= 200 && this.status < 300 ? 'success' : 'error';
requestData.endTime = endTime;
requestData.duration = endTime - requestData.startTime;
try {
const contentType = this.getResponseHeader('content-type') || '';
requestData.response = {
status: this.status,
statusText: this.statusText,
headers: { 'content-type': contentType },
body: contentType.includes('application/json') ?
JSON.parse(this.responseText) : this.responseText
};
} catch (e) {
requestData.response = {
status: this.status,
statusText: this.statusText,
headers: {},
body: this.responseText
};
}
renderNetworkRequests();
});
this.addEventListener('error', function() {
const endTime = performance.now();
requestData.status = 'error';
requestData.endTime = endTime;
requestData.duration = endTime - requestData.startTime;
requestData.error = {
name: 'NetworkError',
message: 'Network request failed'
};
renderNetworkRequests();
});
return originalSend.call(this, body);
};
}
// ネットワークリクエストを表示
function renderNetworkRequests() {
const panel = document.getElementById('network-panel');
if (!panel || !panel.classList.contains('active')) return;
const requestsList = panel.querySelector('.network-requests');
const detailsPanel = panel.querySelector('.network-details');
requestsList.innerHTML = '';
networkRequests.forEach(request => {
const requestElement = document.createElement('div');
requestElement.className = 'network-request';
if (selectedRequest && selectedRequest.id === request.id) {
requestElement.classList.add('selected');
}
requestElement.onclick = () => {
selectedRequest = request;
renderNetworkRequests();
renderNetworkDetails();
};
const statusElement = document.createElement('div');
statusElement.className = `network-status ${request.status}`;
statusElement.textContent = request.status === 'success' ? '✓' : '✕';
const methodElement = document.createElement('div');
methodElement.className = 'network-method';
methodElement.textContent = request.method;
const urlElement = document.createElement('div');
urlElement.className = 'network-url';
urlElement.textContent = request.url;
const timeElement = document.createElement('div');
timeElement.className = 'network-time';
timeElement.textContent = request.duration ? `${Math.round(request.duration)}ms` : '';
requestElement.appendChild(statusElement);
requestElement.appendChild(methodElement);
requestElement.appendChild(urlElement);
requestElement.appendChild(timeElement);
requestsList.appendChild(requestElement);
});
}
// ネットワークリクエストの詳細を表示
function renderNetworkDetails() {
const panel = document.getElementById('network-panel');
if (!panel || !selectedRequest) return;
const detailsPanel = panel.querySelector('.network-details');
detailsPanel.innerHTML = '';
// 一般情報
const generalSection = document.createElement('div');
generalSection.className = 'network-detail-section';
generalSection.innerHTML = `
<div class="network-detail-title">一般</div>
<div class="network-detail-content">
<strong>URL:</strong> ${selectedRequest.url}<br>
<strong>メソッド:</strong> ${selectedRequest.method}<br>
<strong>ステータス:</strong> ${selectedRequest.response ? selectedRequest.response.status : '-'}<br>
<strong>時間:</strong> ${selectedRequest.duration ? Math.round(selectedRequest.duration) + 'ms' : '-'}
</div>
`;
detailsPanel.appendChild(generalSection);
// リクエストヘッダー
if (selectedRequest.requestHeaders) {
const headersSection = document.createElement('div');
headersSection.className = 'network-detail-section';
const headersTitle = document.createElement('div');
headersTitle.className = 'network-detail-title';
headersTitle.textContent = 'リクエストヘッダー';
const headersContent = document.createElement('div');
headersContent.className = 'network-detail-content';
if (typeof selectedRequest.requestHeaders === 'object' && !(selectedRequest.requestHeaders instanceof Headers)) {
Object.entries(selectedRequest.requestHeaders).forEach(([key, value]) => {
headersContent.innerHTML += `<strong>${key}:</strong> ${value}<br>`;
});
} else if (selectedRequest.requestHeaders instanceof Headers) {
selectedRequest.requestHeaders.forEach((value, key) => {
headersContent.innerHTML += `<strong>${key}:</strong> ${value}<br>`;
});
}
headersSection.appendChild(headersTitle);
headersSection.appendChild(headersContent);
detailsPanel.appendChild(headersSection);
}
// リクエストボディ
if (selectedRequest.requestBody) {
const bodySection = document.createElement('div');
bodySection.className = 'network-detail-section';
const bodyTitle = document.createElement('div');
bodyTitle.className = 'network-detail-title';
bodyTitle.textContent = 'リクエストボディ';
const bodyContent = document.createElement('div');
bodyContent.className = 'network-detail-content';
try {
if (typeof selectedRequest.requestBody === 'string') {
bodyContent.textContent = selectedRequest.requestBody;
} else if (typeof selectedRequest.requestBody === 'object') {
bodyContent.textContent = JSON.stringify(selectedRequest.requestBody, null, 2);
}
} catch (e) {
bodyContent.textContent = 'ボディを表示できません';
}
bodySection.appendChild(bodyTitle);
bodySection.appendChild(bodyContent);
detailsPanel.appendChild(bodySection);
}
// レスポンス
if (selectedRequest.response) {
const responseSection = document.createElement('div');
responseSection.className = 'network-detail-section';
const responseTitle = document.createElement('div');
responseTitle.className = 'network-detail-title';
responseTitle.textContent = 'レスポンス';
const responseContent = document.createElement('div');
responseContent.className = 'network-detail-content';
if (selectedRequest.response.body) {
if (typeof selectedRequest.response.body === 'object') {
responseContent.textContent = JSON.stringify(selectedRequest.response.body, null, 2);
} else {
responseContent.textContent = selectedRequest.response.body;
}
} else {
responseContent.textContent = 'レスポンスボディがありません';
}
responseSection.appendChild(responseTitle);
responseSection.appendChild(responseContent);
detailsPanel.appendChild(responseSection);
}
// エラー
if (selectedRequest.error) {
const errorSection = document.createElement('div');
errorSection.className = 'network-detail-section';
const errorTitle = document.createElement('div');
errorTitle.className = 'network-detail-title';
errorTitle.textContent = 'エラー';
const errorContent = document.createElement('div');
errorContent.className = 'network-detail-content';
errorContent.textContent = `${selectedRequest.error.name}: ${selectedRequest.error.message}`;
if (selectedRequest.error.stack) {
errorContent.innerHTML += `<br><br>${selectedRequest.error.stack.replace(/\n/g, '<br>')}`;
}
errorSection.appendChild(errorTitle);
errorSection.appendChild(errorContent);
detailsPanel.appendChild(errorSection);
}
}
// コンテキストメニュー作成
function createContextMenu() {
contextMenu = document.createElement('div');
contextMenu.className = 'context-menu';
contextMenu.innerHTML = `
<div class="context-menu-item" data-action="edit-html">HTMLとして編集</div>
<div class="context-menu-item" data-action="add-attribute">属性を追加</div>
<div class="context-menu-item" data-action="edit-element">要素を編集</div>
<div class="context-menu-item" data-action="duplicate">要素を複製</div>
<div class="context-menu-item" data-action="remove">要素を削除</div>
<div class="context-menu-item" data-action="toggle-visibility">要素を非表示</div>
<div class="context-menu-item" data-action="force-state">状態を強制</div>
`;
document.body.appendChild(contextMenu);
contextMenu.querySelectorAll('.context-menu-item').forEach(item => {
item.addEventListener('click', (e) => {
const action = e.target.getAttribute('data-action');
handleContextMenuAction(action);
contextMenu.style.display = 'none';
});
});
document.addEventListener('click', (e) => {
if (e.target !== contextMenu && !contextMenu.contains(e.target)) {
contextMenu.style.display = 'none';
}
});
}
// Storageパネル作成
function createStoragePanel() {
const panel = document.createElement('div');
panel.className = 'devtools-panel';
panel.id = 'storage-panel';
// LocalStorage表示
const localStorageTitle = document.createElement('h3');
localStorageTitle.textContent = 'Local Storage';
panel.appendChild(localStorageTitle);
const localStorageTable = document.createElement('table');
localStorageTable.className = 'storage-table';
panel.appendChild(localStorageTable);
const addLocalStorageBtn = document.createElement('button');
addLocalStorageBtn.className = 'add-btn';
addLocalStorageBtn.textContent = '+ Local Storageに追加';
addLocalStorageBtn.onclick = () => {
const key = prompt('キー名を入力');
if (key) {
const value = prompt('値を入力');
localStorage.setItem(key, value);
renderStorage();
}
};
panel.appendChild(addLocalStorageBtn);
// SessionStorage表示
const sessionStorageTitle = document.createElement('h3');
sessionStorageTitle.style.marginTop = '20px';
sessionStorageTitle.textContent = 'Session Storage';
panel.appendChild(sessionStorageTitle);
const sessionStorageTable = document.createElement('table');
sessionStorageTable.className = 'storage-table';
panel.appendChild(sessionStorageTable);
const addSessionStorageBtn = document.createElement('button');
addSessionStorageBtn.className = 'add-btn';
addSessionStorageBtn.textContent = '+ Session Storageに追加';
addSessionStorageBtn.onclick = () => {
const key = prompt('キー名を入力');
if (key) {
const value = prompt('値を入力');
sessionStorage.setItem(key, value);
renderStorage();
}
};
panel.appendChild(addSessionStorageBtn);
// Cookie表示
const cookiesTitle = document.createElement('h3');
cookiesTitle.style.marginTop = '20px';
cookiesTitle.textContent = 'Cookies';
panel.appendChild(cookiesTitle);
const cookiesTable = document.createElement('table');
cookiesTable.className = 'storage-table';
panel.appendChild(cookiesTable);
const addCookieBtn = document.createElement('button');
addCookieBtn.className = 'add-btn';
addCookieBtn.textContent = '+ Cookieに追加';
addCookieBtn.onclick = () => {
const name = prompt('Cookie名を入力');
if (name) {
const value = prompt('値を入力');
document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}; path=/`;
renderStorage();
}
};
panel.appendChild(addCookieBtn);
// ストレージを表示する関数
function renderStorage() {
renderTable(localStorageTable, localStorage, 'local');
renderTable(sessionStorageTable, sessionStorage, 'session');
renderCookiesTable(cookiesTable);
}
function renderTable(tableElement, storage, type) {
tableElement.innerHTML = `
<thead>
<tr>
<th>Key</th>
<th>Value</th>
<th>Actions</th>
</tr>
</thead>
<tbody></tbody>
`;
const tbody = tableElement.querySelector('tbody');
for (let i = 0; i < storage.length; i++) {
const key = storage.key(i);
const value = storage.getItem(key);
const row = document.createElement('tr');
const keyCell = document.createElement('td');
const keySpan = document.createElement('span');
keySpan.className = 'editable';
keySpan.textContent = key;
keySpan.onclick = () => {
const newKey = prompt('新しいキー名を入力', key);
if (newKey && newKey !== key) {
storage.setItem(newKey, value);
storage.removeItem(key);
renderStorage();
}
};
keyCell.appendChild(keySpan);
const valueCell = document.createElement('td');
const valueSpan = document.createElement('span');
valueSpan.className = 'editable';
valueSpan.textContent = value;
valueSpan.onclick = () => {
const newValue = prompt('新しい値を入力', value);
if (newValue !== null) {
storage.setItem(key, newValue);
renderStorage();
}
};
valueCell.appendChild(valueSpan);
const actionsCell = document.createElement('td');
actionsCell.className = 'storage-actions';
const deleteBtn = document.createElement('button');
deleteBtn.className = 'storage-btn';
deleteBtn.textContent = 'Delete';
deleteBtn.onclick = () => {
storage.removeItem(key);
renderStorage();
};
actionsCell.appendChild(deleteBtn);
row.appendChild(keyCell);
row.appendChild(valueCell);
row.appendChild(actionsCell);
tbody.appendChild(row);
}
}
function renderCookiesTable(tableElement) {
tableElement.innerHTML = `
<thead>
<tr>
<th>Name</th>
<th>Value</th>
<th>Actions</th>
</tr>
</thead>
<tbody></tbody>
`;
const tbody = tableElement.querySelector('tbody');
document.cookie.split(';').forEach(cookie => {
if (!cookie.trim()) return;
const [name, ...valueParts] = cookie.split('=');
const decodedName = decodeURIComponent(name.trim());
const value = valueParts.join('=').trim();
const row = document.createElement('tr');
const nameCell = document.createElement('td');
const nameSpan = document.createElement('span');
nameSpan.className = 'editable';
nameSpan.textContent = decodedName;
nameSpan.onclick = () => {
const newName = prompt('新しい名前を入力', decodedName);
if (newName && newName !== decodedName) {
const newValue = prompt('新しい値を入力', decodeURIComponent(value));
if (newValue !== null) {
document.cookie = `${encodeURIComponent(newName)}=${encodeURIComponent(newValue)}; expires=Fri, 31 Dec 9999 23:59:59 GMT; path=/`;
document.cookie = `${name.trim()}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
renderStorage();
}
}
};
nameCell.appendChild(nameSpan);
const valueCell = document.createElement('td');
const valueSpan = document.createElement('span');
valueSpan.className = 'editable';
valueSpan.textContent = decodeURIComponent(value);
valueSpan.onclick = () => {
const newValue = prompt('新しい値を入力', decodeURIComponent(value));
if (newValue !== null) {
document.cookie = `${name.trim()}=${encodeURIComponent(newValue)}; expires=Fri, 31 Dec 9999 23:59:59 GMT; path=/`;
renderStorage();
}
};
valueCell.appendChild(valueSpan);
const actionsCell = document.createElement('td');
actionsCell.className = 'storage-actions';
const deleteBtn = document.createElement('button');
deleteBtn.className = 'storage-btn';
deleteBtn.textContent = 'Delete';
deleteBtn.onclick = () => {
document.cookie = `${name.trim()}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
renderStorage();
};
actionsCell.appendChild(deleteBtn);
row.appendChild(nameCell);
row.appendChild(valueCell);
row.appendChild(actionsCell);
tbody.appendChild(row);
});
}
// 初期表示
renderStorage();
return panel;
}
// コンテキストメニューアクション処理
function handleContextMenuAction(action) {
if (!selectedElement) return;
switch (action) {
case 'edit-html':
// document.documentElement(html要素)は編集不可
if (selectedElement === document.documentElement) {
alert('ルートHTML要素は直接編集できません');
return;
}
startInlineEdit(selectedDOMNode, selectedElement.outerHTML, (newValue) => {
try {
selectedElement.outerHTML = newValue;
refreshElementsPanel();
} catch (e) {
alert('この要素は編集できません: ' + e.message);
}
});
break;
case 'add-attribute':
const attrName = prompt('属性名を入力');
if (attrName) {
const attrValue = prompt('属性値を入力');
selectedElement.setAttribute(attrName, attrValue || '');
refreshElementsPanel();
}
break;
case 'edit-element':
startInlineEdit(selectedDOMNode.querySelector('.dom-tag'), selectedElement.tagName.toLowerCase(), (newValue) => {
const newElement = document.createElement(newValue);
Array.from(selectedElement.attributes).forEach(attr => {
newElement.setAttribute(attr.name, attr.value);
});
newElement.innerHTML = selectedElement.innerHTML;
selectedElement.parentNode.replaceChild(newElement, selectedElement);
selectedElement = newElement;
refreshElementsPanel();
});
break;
case 'duplicate':
const clone = selectedElement.cloneNode(true);
selectedElement.parentNode.insertBefore(clone, selectedElement.nextSibling);
refreshElementsPanel();
break;
case 'remove':
if (confirm('要素を削除しますか?')) {
selectedElement.parentNode.removeChild(selectedElement);
refreshElementsPanel();
}
break;
case 'toggle-visibility':
if (selectedElement.style.display === 'none') {
selectedElement.style.display = '';
} else {
selectedElement.style.display = 'none';
}
refreshElementsPanel();
break;
case 'force-state':
const state = prompt('強制する状態を入力 (例: hover, active, focus)', 'hover');
if (state) {
selectedElement.classList.remove('force-hover', 'force-active', 'force-focus',
'force-focus-within', 'force-focus-visible', 'force-target');
selectedElement.classList.add(`force-${state}`);
refreshElementsPanel();
}
break;
}
}
// インライン編集関数
function startInlineEdit(element, initialValue, callback) {
if (activeEditElement) return;
const originalValue = element.textContent;
const rect = element.getBoundingClientRect();
const input = document.createElement('input');
input.className = 'dom-edit-input';
input.value = initialValue || originalValue;
input.style.position = 'absolute';
input.style.left = `${rect.left}px`;
input.style.top = `${rect.top}px`;
input.style.width = `${rect.width + 20}px`;
document.body.appendChild(input);
input.focus();
input.select();
activeEditElement = {
element: element,
input: input,
callback: callback
};
const clickOutsideHandler = (e) => {
if (!input.contains(e.target)) {
finishInlineEdit();
}
};
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
finishInlineEdit();
} else if (e.key === 'Escape') {
cancelInlineEdit();
}
});
setTimeout(() => {
document.addEventListener('click', clickOutsideHandler);
}, 0);
function finishInlineEdit() {
try {
if (input.value !== originalValue && callback) {
callback(input.value);
}
} catch (e) {
alert('編集に失敗しました: ' + e.message);
} finally {
cleanup();
}
}
function cancelInlineEdit() {
cleanup();
}
function cleanup() {
document.removeEventListener('click', clickOutsideHandler);
input.remove();
activeEditElement = null;
}
}
// Consoleパネル作成
function createConsolePanel() {
const panel = document.createElement('div');
panel.className = 'devtools-panel';
panel.id = 'console-panel';
const log = document.createElement('div');
log.id = 'console-log';
const input = document.createElement('input');
input.className = 'console-input';
input.placeholder = 'ここにJavaScriptを入力... (Enterで実行)';
input.onkeypress = (e) => {
if (e.key === 'Enter') {
try {
const result = eval(e.target.value);
if (result !== undefined) {
logMessage('> ' + e.target.value, 'console-log');
logMessage('← ' + formatOutput(result), 'console-log');
}
} catch (err) {
logMessage(err.message, 'console-error');
}
e.target.value = '';
}
};
panel.appendChild(log);
panel.appendChild(input);
// コンソールメソッドをオーバーライド
const originalConsole = {
log: console.log,
error: console.error,
warn: console.warn,
info: console.info
};
console.log = (...args) => {
originalConsole.log.apply(console, args);
logMessage(args.map(arg => formatOutput(arg)).join(' '), 'console-log');
};
console.error = (...args) => {
originalConsole.error.apply(console, args);
logMessage(args.map(arg => formatOutput(arg)).join(' '), 'console-error');
};
console.warn = (...args) => {
originalConsole.warn.apply(console, args);
logMessage(args.map(arg => formatOutput(arg)).join(' '), 'console-warn');
};
console.info = (...args) => {
originalConsole.info.apply(console, args);
logMessage(args.map(arg => formatOutput(arg)).join(' '), 'console-info');
};
function logMessage(message, className) {
const line = document.createElement('div');
line.className = className;
line.innerHTML = message;
log.appendChild(line);
log.scrollTop = log.scrollHeight;
}
function formatOutput(output) {
if (output === null) return '<span class="json-null">null</span>';
if (output === undefined) return '<span class="json-null">undefined</span>';
if (typeof output === 'boolean') return `<span class="json-boolean">${output}</span>`;
if (typeof output === 'number') return `<span class="json-number">${output}</span>`;
if (typeof output === 'string') return `<span class="json-string">"${output}"</span>`;
if (typeof output === 'function') return `<span class="json-object">function ${output.name}() { ... }</span>`;
if (Array.isArray(output)) return `<span class="json-object">[${output.map(formatOutput).join(', ')}]</span>`;
if (typeof output === 'object') {
try {
return `<span class="json-object">${JSON.stringify(output, null, 2)
.replace(/"([^"]+)":/g, '<span class="json-key">"$1"</span>:')
.replace(/"([^"]+)"/g, '<span class="json-string">"$1"</span>')
.replace(/\b(true|false)\b/g, '<span class="json-boolean">$1</span>')
.replace(/\b(null)\b/g, '<span class="json-null">$1</span>')
.replace(/\b(\d+)\b/g, '<span class="json-number">$1</span>')}</span>`;
} catch (e) {
return `<span class="json-object">${output.toString()}</span>`;
}
}
return output;
}
return panel;
}
// Elementsパネル作成
function createElementsPanel() {
const panel = document.createElement('div');
panel.className = 'devtools-panel';
panel.id = 'elements-panel';
const container = document.createElement('div');
container.className = 'elements-container';
const tree = document.createElement('div');
tree.className = 'dom-tree';
tree.id = 'dom-tree';
const cssPanel = document.createElement('div');
cssPanel.className = 'css-panel';
cssPanel.id = 'css-panel';
container.appendChild(tree);
container.appendChild(cssPanel);
panel.appendChild(container);
// CSSパネル更新関数
function updateCSSPanel(element) {
const cssPanel = document.getElementById('css-panel');
cssPanel.innerHTML = '';
if (!element) return;
if (element.style.length > 0) {
const inlineRule = document.createElement('div');
inlineRule.className = 'css-rule';
const selector = document.createElement('div');
selector.className = 'css-selector';
selector.textContent = 'インラインスタイル';
inlineRule.appendChild(selector);
for (let i = 0; i < element.style.length; i++) {
const propName = element.style[i];
const propValue = element.style[propName];
const propDiv = document.createElement('div');
propDiv.className = 'css-property';
const nameSpan = document.createElement('span');
nameSpan.className = 'css-property-name editable';
nameSpan.textContent = propName;
nameSpan.onclick = () => editCSSProperty(element, propName, 'style');
const valueSpan = document.createElement('span');
valueSpan.className = 'css-property-value editable';
valueSpan.textContent = propValue;
valueSpan.onclick = () => editCSSProperty(element, propName, 'style');
const toggleSpan = document.createElement('span');
toggleSpan.className = 'css-toggle';
toggleSpan.textContent = '×';
toggleSpan.title = 'プロパティを無効化';
toggleSpan.onclick = () => {
element.style[propName] = '';
updateCSSPanel(element);
};
propDiv.appendChild(nameSpan);
propDiv.appendChild(valueSpan);
propDiv.appendChild(toggleSpan);
inlineRule.appendChild(propDiv);
}
cssPanel.appendChild(inlineRule);
}
const computedStyles = window.getComputedStyle(element);
const computedRule = document.createElement('div');
computedRule.className = 'css-rule';
const computedSelector = document.createElement('div');
computedSelector.className = 'css-selector';
computedSelector.textContent = '計算されたスタイル';
computedRule.appendChild(computedSelector);
const importantProps = [
'display', 'position', 'width', 'height', 'margin', 'padding',
'color', 'background', 'border', 'font', 'flex', 'grid'
];
importantProps.forEach(prop => {
const value = computedStyles[prop];
const propDiv = document.createElement('div');
propDiv.className = 'css-property';
const nameSpan = document.createElement('span');
nameSpan.className = 'css-property-name';
nameSpan.textContent = prop;
const valueSpan = document.createElement('span');
valueSpan.className = 'css-property-value';
valueSpan.textContent = value;
propDiv.appendChild(nameSpan);
propDiv.appendChild(valueSpan);
computedRule.appendChild(propDiv);
});
cssPanel.appendChild(computedRule);
}
// DOMツリー構築
function buildDOMTree(node, parentElement, depth = 0, isRoot = false) {
if (node.nodeType === Node.ELEMENT_NODE) {
const element = document.createElement('div');
element.className = 'dom-node';
element.style.marginLeft = `${depth * 15}px`;
element.dataset.elementId = node.id || Math.random().toString(36).substr(2, 9);
// 右クリックイベント
element.oncontextmenu = (e) => {
e.preventDefault();
selectedElement = node;
selectedDOMNode = element;
document.querySelectorAll('.dom-node').forEach(el => el.classList.remove('selected'));
element.classList.add('selected');
// HTML要素にはコンテキストメニューを表示しない
if (node !== document.documentElement) {
contextMenu.style.display = 'block';
contextMenu.style.left = `${e.pageX}px`;
contextMenu.style.top = `${e.pageY}px`;
}
updateCSSPanel(node);
};
// 左クリックで選択
element.onclick = (e) => {
if (e.target.classList.contains('dom-toggle')) return;
e.stopPropagation();
selectedElement = node;
selectedDOMNode = element;
document.querySelectorAll('.dom-node').forEach(el => el.classList.remove('selected'));
element.classList.add('selected');
updateCSSPanel(node);
};
// 子要素がある場合はトグルボタンを追加
const hasChildren = node.childNodes.length > 0 &&
!(node.childNodes.length === 1 && node.childNodes[0].nodeType === Node.TEXT_NODE && !node.childNodes[0].textContent.trim());
if (hasChildren) {
const toggle = document.createElement('div');
toggle.className = 'dom-toggle';
toggle.onclick = (e) => {
e.stopPropagation();
const children = element.querySelector('.dom-children');
if (children) {
if (children.style.maxHeight === '0px') {
children.style.maxHeight = children.scrollHeight + 'px';
toggle.classList.remove('collapsed');
} else {
children.style.maxHeight = '0px';
toggle.classList.add('collapsed');
}
}
};
element.appendChild(toggle);
}
// タグ名(HTML要素は編集不可)
const tag = document.createElement('span');
tag.className = 'dom-tag';
tag.textContent = `<${node.tagName.toLowerCase()}`;
if (node !== document.documentElement) {
tag.classList.add('editable');
tag.onclick = (e) => {
e.stopPropagation();
startInlineEdit(tag, node.tagName.toLowerCase(), (newValue) => {
const newElement = document.createElement(newValue);
Array.from(node.attributes).forEach(attr => {
newElement.setAttribute(attr.name, attr.value);
});
newElement.innerHTML = node.innerHTML;
node.parentNode.replaceChild(newElement, node);
selectedElement = newElement;
refreshElementsPanel();
});
};
}
element.appendChild(tag);
// 属性(HTML要素は編集不可)
Array.from(node.attributes).forEach(attr => {
const attrSpan = document.createElement('span');
attrSpan.className = 'dom-attr';
attrSpan.textContent = ` ${attr.name}="${attr.value}"`;
if (node !== document.documentElement) {
attrSpan.classList.add('editable');
attrSpan.onclick = (e) => {
e.stopPropagation();
startInlineEdit(attrSpan, attr.value, (newValue) => {
node.setAttribute(attr.name, newValue);
refreshElementsPanel();
});
};
}
element.appendChild(attrSpan);
});
element.appendChild(document.createTextNode('>'));
if (hasChildren) {
const childrenContainer = document.createElement('div');
childrenContainer.className = 'dom-children';
childrenContainer.style.maxHeight = isRoot ? 'none' : '0px';
node.childNodes.forEach(child => {
buildDOMTree(child, childrenContainer, depth + 1);
});
if (node.tagName.toLowerCase() !== 'br') {
const closeTag = document.createElement('div');
closeTag.style.marginLeft = `${depth * 15}px`;
closeTag.innerHTML = `<span class="dom-tag">&lt;/${node.tagName.toLowerCase()}&gt;</span>`;
childrenContainer.appendChild(closeTag);
}
element.appendChild(childrenContainer);
}
parentElement.appendChild(element);
} else if (node.nodeType === Node.TEXT_NODE && node.textContent.trim()) {
const text = document.createElement('div');
text.style.marginLeft = `${depth * 15}px`;
text.className = 'dom-text editable';
text.textContent = `"${node.textContent.trim()}"`;
text.onclick = (e) => {
e.stopPropagation();
startInlineEdit(text, node.textContent.trim(), (newValue) => {
node.textContent = newValue;
refreshElementsPanel();
});
};
parentElement.appendChild(text);
}
}
// CSSプロパティ編集
function editCSSProperty(element, propName, styleType) {
let currentValue = '';
if (styleType === 'style') {
currentValue = element.style[propName];
}
const newValue = prompt(`${propName} の新しい値を入力`, currentValue);
if (newValue !== null) {
if (styleType === 'style') {
element.style[propName] = newValue;
}
updateCSSPanel(element);
refreshElementsPanel();
}
}
// 要素パネル更新
function refreshElementsPanel() {
let tree = document.getElementById('dom-tree');
if (!tree) {
const panel = document.getElementById('elements-panel');
if (panel) {
const container = panel.querySelector('.elements-container');
if (container) {
tree = document.createElement('div');
tree.className = 'dom-tree';
tree.id = 'dom-tree';
container.insertBefore(tree, container.querySelector('.css-panel'));
}
}
}
if (!tree) return;
tree.innerHTML = '';
buildDOMTree(document.documentElement, tree, 0, true);
if (selectedElement) {
const elementId = selectedElement.id || Array.from(selectedElement.attributes)
.find(attr => attr.name.startsWith('data-element-id'))?.value;
if (elementId) {
const node = document.querySelector(`[data-element-id="${elementId}"]`);
if (node) {
node.classList.add('selected');
updateCSSPanel(selectedElement);
}
}
}
}
// DOM変更を監視
observer.observe(document.documentElement, {
childList: true,
subtree: true,
attributes: true,
characterData: true
});
setTimeout(() => {
refreshElementsPanel();
}, 0);
return panel;
}
// 開発者ツール表示/非表示
function toggleDevTools() {
const container = document.getElementById('devtools-container');
if (container.style.display === 'none') {
container.style.display = 'flex';
} else {
container.style.display = 'none';
}
}
// 開くボタン作成
function createOpenButton() {
const button = document.createElement('button');
button.id = 'open-devtools-btn';
button.textContent = '開発者ツールを開く';
button.style.position = 'fixed';
button.style.bottom = '10px';
button.style.right = '10px';
button.style.padding = '8px 16px';
button.style.background = 'var(--primary-color)';
button.style.color = '#000';
button.style.border = 'none';
button.style.borderRadius = '4px';
button.style.cursor = 'pointer';
button.style.zIndex = '9998';
button.onclick = toggleDevTools;
document.body.appendChild(button);
}
// 初期化
document.addEventListener('DOMContentLoaded', function() {
createDevTools();
createOpenButton();
console.log('開発者ツールが初期化されました');
console.log('このコンソールでJavaScriptを実行できます');
});
})();