|
(function() { |
|
|
|
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 |
|
}; |
|
|
|
|
|
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(); |
|
} |
|
|
|
|
|
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); |
|
|
|
|
|
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: < 0.1</div> |
|
<div class="vital-threshold">Needs Improvement: < 0.25</div> |
|
<div class="vital-threshold">Poor: ≥ 0.25</div> |
|
</div> |
|
`; |
|
container.appendChild(clsCard); |
|
|
|
|
|
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: < 1.8s</div> |
|
<div class="vital-threshold">Needs Improvement: < 3s</div> |
|
<div class="vital-threshold">Poor: ≥ 3s</div> |
|
</div> |
|
`; |
|
container.appendChild(fcpCard); |
|
|
|
|
|
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: < 100ms</div> |
|
<div class="vital-threshold">Needs Improvement: < 300ms</div> |
|
<div class="vital-threshold">Poor: ≥ 300ms</div> |
|
</div> |
|
`; |
|
container.appendChild(fidCard); |
|
|
|
|
|
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: < 2.5s</div> |
|
<div class="vital-threshold">Needs Improvement: < 4s</div> |
|
<div class="vital-threshold">Poor: ≥ 4s</div> |
|
</div> |
|
`; |
|
container.appendChild(lcpCard); |
|
|
|
|
|
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: < 800ms</div> |
|
<div class="vital-threshold">Needs Improvement: < 1.8s</div> |
|
<div class="vital-threshold">Poor: ≥ 1.8s</div> |
|
</div> |
|
`; |
|
container.appendChild(ttfbCard); |
|
|
|
|
|
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; |
|
|
|
|
|
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'; |
|
} |
|
}); |
|
} |
|
|
|
|
|
function createStoragePanel() { |
|
const panel = document.createElement('div'); |
|
panel.className = 'devtools-panel'; |
|
panel.id = 'storage-panel'; |
|
|
|
|
|
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); |
|
|
|
|
|
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); |
|
|
|
|
|
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': |
|
|
|
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; |
|
} |
|
} |
|
|
|
|
|
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; |
|
} |
|
|
|
|
|
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); |
|
|
|
|
|
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); |
|
} |
|
|
|
|
|
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'); |
|
|
|
|
|
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); |
|
} |
|
|
|
|
|
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); |
|
|
|
|
|
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"></${node.tagName.toLowerCase()}></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); |
|
} |
|
} |
|
|
|
|
|
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); |
|
} |
|
} |
|
} |
|
} |
|
|
|
|
|
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を実行できます'); |
|
}); |
|
})(); |