|
(function() { |
|
|
|
let dtContextMenu = null; |
|
let dtSelectedElement = null; |
|
let dtSelectedDOMNode = null; |
|
let dtActiveEditElement = null; |
|
let dtNetworkRequests = []; |
|
let dtSelectedRequest = null; |
|
let dtObserver = null; |
|
let dtRefreshElementsPanel = null; |
|
|
|
|
|
const setupMutationObserver = () => { |
|
if (dtObserver) dtObserver.disconnect(); |
|
|
|
dtObserver = new MutationObserver(() => { |
|
if (dtRefreshElementsPanel) { |
|
dtRefreshElementsPanel(); |
|
} |
|
}); |
|
|
|
dtObserver.observe(document.documentElement, { |
|
childList: true, |
|
subtree: true, |
|
attributes: false, |
|
characterData: false |
|
}); |
|
}; |
|
|
|
|
|
const style = document.createElement('style'); |
|
style.textContent = ` |
|
.dt-devtools-container { |
|
position: fixed; |
|
bottom: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 300px; |
|
background-color: var(--dt-bg-color); |
|
border-top: 1px solid var(--dt-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(--dt-text-color); |
|
} |
|
|
|
.dt-devtools-header { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
padding: 5px 10px; |
|
background-color: var(--dt-tab-bg); |
|
border-bottom: 1px solid var(--dt-border-color); |
|
} |
|
|
|
.dt-devtools-tabs { |
|
display: flex; |
|
gap: 5px; |
|
} |
|
|
|
.dt-devtools-tab { |
|
padding: 5px 10px; |
|
cursor: pointer; |
|
border-radius: 3px 3px 0 0; |
|
background-color: var(--dt-tab-bg); |
|
border: 1px solid var(--dt-border-color); |
|
border-bottom: none; |
|
font-size: 12px; |
|
color: var(--dt-text-muted); |
|
} |
|
|
|
.dt-devtools-tab.active { |
|
background-color: var(--dt-tab-active-bg); |
|
color: var(--dt-text-color); |
|
border-bottom: 1px solid var(--dt-tab-active-bg); |
|
margin-bottom: -1px; |
|
font-weight: bold; |
|
} |
|
|
|
.dt-devtools-close { |
|
background: none; |
|
border: none; |
|
font-size: 16px; |
|
cursor: pointer; |
|
padding: 0 5px; |
|
color: var(--dt-text-color); |
|
} |
|
|
|
.dt-devtools-content { |
|
flex: 1; |
|
overflow: auto; |
|
position: relative; |
|
background-color: var(--dt-panel-bg); |
|
} |
|
|
|
.dt-devtools-panel { |
|
position: absolute; |
|
top: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 100%; |
|
padding: 10px; |
|
overflow: auto; |
|
display: none; |
|
background-color: var(--dt-panel-bg); |
|
} |
|
|
|
.dt-devtools-panel.active { |
|
display: block; |
|
} |
|
|
|
/* Console スタイル */ |
|
#dt-console-log { |
|
white-space: pre-wrap; |
|
margin: 0; |
|
line-height: 1.4; |
|
flex: 1; |
|
color: var(--dt-console-log-color); |
|
font-family: 'Consolas', 'Monaco', monospace; |
|
font-size: 13px; |
|
} |
|
|
|
.dt-console-log { |
|
color: var(--dt-console-log-color); |
|
} |
|
|
|
.dt-console-error { |
|
color: var(--dt-console-error-color); |
|
} |
|
|
|
.dt-console-warn { |
|
color: var(--dt-console-warn-color); |
|
} |
|
|
|
.dt-console-info { |
|
color: var(--dt-console-info-color); |
|
} |
|
|
|
.dt-console-input { |
|
width: calc(100% - 16px); |
|
background: var(--dt-tab-bg); |
|
border: 1px solid var(--dt-border-color); |
|
color: var(--dt-text-color); |
|
padding: 8px; |
|
margin-top: 10px; |
|
font-family: monospace; |
|
border-radius: 3px; |
|
} |
|
|
|
/* Elements スタイル */ |
|
.dt-elements-container { |
|
display: flex; |
|
flex: 1; |
|
overflow: hidden; |
|
} |
|
|
|
.dt-dom-tree { |
|
font-family: 'Consolas', 'Monaco', monospace; |
|
flex: 1; |
|
overflow: auto; |
|
border-right: 1px solid var(--dt-border-color); |
|
padding-right: 10px; |
|
color: var(--dt-dom-text); |
|
font-size: 13px; |
|
} |
|
|
|
.dt-dom-node { |
|
margin-left: 15px; |
|
position: relative; |
|
line-height: 1.4; |
|
transition: background-color 0.3s; |
|
} |
|
|
|
.dt-dom-node.selected { |
|
background: var(--dt-highlight-bg); |
|
} |
|
|
|
.dt-dom-node.highlight { |
|
animation: dt-highlight-fade 1.5s; |
|
} |
|
|
|
@keyframes dt-highlight-fade { |
|
0% { background-color: rgba(79, 195, 247, 0.5); } |
|
100% { background-color: transparent; } |
|
} |
|
|
|
.dt-dom-tag { |
|
color: var(--dt-dom-tag); |
|
font-weight: bold; |
|
} |
|
|
|
.dt-dom-attr { |
|
color: var(--dt-dom-attr); |
|
} |
|
|
|
.dt-dom-attr.editable:hover { |
|
text-decoration: underline; |
|
cursor: pointer; |
|
} |
|
|
|
.dt-dom-text { |
|
color: var(--dt-dom-text); |
|
} |
|
|
|
.dt-dom-edit-input { |
|
background: var(--dt-panel-bg); |
|
border: 1px solid var(--dt-primary-color); |
|
padding: 0 2px; |
|
margin: -1px 0; |
|
font-family: monospace; |
|
min-width: 50px; |
|
color: var(--dt-text-color); |
|
} |
|
|
|
.dt-css-panel { |
|
flex: 1; |
|
overflow: auto; |
|
padding-left: 10px; |
|
font-size: 13px; |
|
} |
|
|
|
.dt-css-rule { |
|
margin-bottom: 15px; |
|
border: 1px solid var(--dt-border-color); |
|
padding: 8px; |
|
background-color: var(--dt-tab-bg); |
|
border-radius: 3px; |
|
} |
|
|
|
.dt-css-selector { |
|
color: var(--dt-primary-color); |
|
margin-bottom: 5px; |
|
font-weight: bold; |
|
} |
|
|
|
.dt-css-property { |
|
display: flex; |
|
margin-bottom: 3px; |
|
} |
|
|
|
.dt-css-property-name { |
|
color: var(--dt-dom-attr); |
|
min-width: 120px; |
|
} |
|
|
|
.dt-css-property-value { |
|
color: var(--dt-dom-text); |
|
flex: 1; |
|
} |
|
|
|
.dt-css-toggle { |
|
margin-left: 10px; |
|
color: var(--dt-error-color); |
|
cursor: pointer; |
|
} |
|
|
|
/* Context Menu */ |
|
.dt-context-menu { |
|
position: absolute; |
|
background: var(--dt-panel-bg); |
|
border: 1px solid var(--dt-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; |
|
} |
|
|
|
.dt-context-menu-item { |
|
padding: 8px 15px; |
|
cursor: pointer; |
|
color: var(--dt-text-color); |
|
font-size: 13px; |
|
} |
|
|
|
.dt-context-menu-item:hover { |
|
background: var(--dt-primary-color); |
|
color: #000; |
|
} |
|
|
|
/* Storage スタイル */ |
|
.dt-storage-table { |
|
width: 100%; |
|
border-collapse: collapse; |
|
margin-bottom: 10px; |
|
font-size: 13px; |
|
} |
|
|
|
.dt-storage-table th, .dt-storage-table td { |
|
border: 1px solid var(--dt-border-color); |
|
padding: 5px; |
|
text-align: left; |
|
} |
|
|
|
.dt-storage-table th { |
|
background: var(--dt-tab-bg); |
|
} |
|
|
|
.dt-storage-actions { |
|
display: flex; |
|
gap: 5px; |
|
} |
|
|
|
.dt-storage-btn { |
|
background: var(--dt-primary-color); |
|
border: none; |
|
padding: 2px 5px; |
|
cursor: pointer; |
|
border-radius: 3px; |
|
color: #000; |
|
font-size: 12px; |
|
} |
|
|
|
.dt-editable { |
|
cursor: pointer; |
|
padding: 2px 5px; |
|
border: 1px dashed transparent; |
|
} |
|
|
|
.dt-editable:hover { |
|
border-color: var(--dt-primary-color); |
|
} |
|
|
|
.dt-add-btn { |
|
background: var(--dt-primary-color); |
|
color: #000; |
|
border: none; |
|
padding: 5px 10px; |
|
margin-top: 10px; |
|
cursor: pointer; |
|
border-radius: 3px; |
|
font-size: 13px; |
|
} |
|
|
|
.dt-add-btn:hover { |
|
background: var(--dt-primary-hover); |
|
} |
|
|
|
/* Network スタイル */ |
|
.dt-network-container { |
|
display: flex; |
|
height: 100%; |
|
overflow: hidden; |
|
} |
|
|
|
.dt-network-requests { |
|
width: 40%; |
|
overflow-y: auto; |
|
border-right: 1px solid var(--dt-border-color); |
|
font-size: 13px; |
|
} |
|
|
|
.dt-network-details { |
|
width: 60%; |
|
overflow-y: auto; |
|
padding-left: 10px; |
|
} |
|
|
|
.dt-network-request { |
|
padding: 8px; |
|
border-bottom: 1px solid var(--dt-border-color); |
|
cursor: pointer; |
|
display: flex; |
|
align-items: center; |
|
} |
|
|
|
.dt-network-request:hover { |
|
background-color: rgba(0, 122, 204, 0.1); |
|
} |
|
|
|
.dt-network-request.selected { |
|
background-color: var(--dt-highlight-bg); |
|
} |
|
|
|
.dt-network-status { |
|
width: 20px; |
|
height: 20px; |
|
border-radius: 50%; |
|
display: flex; |
|
align-items: center; |
|
justify-content: center; |
|
margin-right: 8px; |
|
flex-shrink: 0; |
|
} |
|
|
|
.dt-network-status.success { |
|
background-color: var(--dt-success-color); |
|
color: white; |
|
} |
|
|
|
.dt-network-status.error { |
|
background-color: var(--dt-error-color); |
|
color: white; |
|
} |
|
|
|
.dt-network-method { |
|
font-weight: bold; |
|
margin-right: 8px; |
|
color: var(--dt-primary-color); |
|
min-width: 40px; |
|
} |
|
|
|
.dt-network-url { |
|
flex: 1; |
|
white-space: nowrap; |
|
overflow: hidden; |
|
text-overflow: ellipsis; |
|
} |
|
|
|
.dt-network-time { |
|
color: var(--dt-text-muted); |
|
font-size: 11px; |
|
margin-left: 8px; |
|
} |
|
|
|
.dt-network-detail-section { |
|
margin-bottom: 15px; |
|
} |
|
|
|
.dt-network-detail-title { |
|
font-weight: bold; |
|
margin-bottom: 5px; |
|
color: var(--dt-primary-color); |
|
} |
|
|
|
.dt-network-detail-content { |
|
background: var(--dt-tab-bg); |
|
padding: 8px; |
|
border-radius: 3px; |
|
border: 1px solid var(--dt-border-color); |
|
font-family: monospace; |
|
white-space: pre-wrap; |
|
font-size: 12px; |
|
max-height: 200px; |
|
overflow-y: auto; |
|
} |
|
|
|
/* DOM Tree Toggle */ |
|
.dt-dom-toggle { |
|
position: absolute; |
|
left: -12px; |
|
top: 2px; |
|
width: 10px; |
|
height: 10px; |
|
cursor: pointer; |
|
background-color: var(--dt-text-muted); |
|
clip-path: polygon(0 0, 100% 50%, 0 100%); |
|
transition: transform 0.2s; |
|
} |
|
|
|
.dt-dom-toggle.collapsed { |
|
transform: rotate(-90deg); |
|
} |
|
|
|
.dt-dom-children { |
|
overflow: hidden; |
|
transition: max-height 0.3s ease-out; |
|
} |
|
|
|
/* JSON スタイル */ |
|
.dt-json-key { |
|
color: var(--dt-json-key); |
|
} |
|
|
|
.dt-json-string { |
|
color: var(--dt-json-string); |
|
} |
|
|
|
.dt-json-number { |
|
color: var(--dt-json-number); |
|
} |
|
|
|
.dt-json-boolean { |
|
color: var(--dt-json-boolean); |
|
} |
|
|
|
.dt-json-null { |
|
color: var(--dt-json-null); |
|
} |
|
|
|
/* CSS変数定義 */ |
|
:root { |
|
--dt-bg-color: #1e1e1e; |
|
--dt-panel-bg: #252526; |
|
--dt-border-color: #3c3c3c; |
|
--dt-text-color: #e0e0e0; |
|
--dt-text-muted: #a0a0a0; |
|
--dt-primary-color: #007acc; |
|
--dt-primary-hover: #3e9fda; |
|
--dt-success-color: #4caf50; |
|
--dt-error-color: #f44336; |
|
--dt-warning-color: #ff9800; |
|
--dt-info-color: #2196f3; |
|
--dt-highlight-bg: rgba(0, 122, 204, 0.2); |
|
--dt-tab-bg: #2d2d2d; |
|
--dt-tab-active-bg: #1e1e1e; |
|
--dt-console-log-color: #e0e0e0; |
|
--dt-console-error-color: #f44336; |
|
--dt-console-warn-color: #ff9800; |
|
--dt-console-info-color: #4fc3f7; |
|
--dt-json-key: #9cdcfe; |
|
--dt-json-string: #ce9178; |
|
--dt-json-number: #b5cea8; |
|
--dt-json-boolean: #569cd6; |
|
--dt-json-null: #569cd6; |
|
--dt-dom-tag: #569cd6; |
|
--dt-dom-attr: #9cdcfe; |
|
--dt-dom-text: #d4d4d4; |
|
} |
|
`; |
|
document.head.appendChild(style); |
|
|
|
|
|
const createDevTools = () => { |
|
const container = document.createElement('div'); |
|
container.className = 'dt-devtools-container'; |
|
container.id = 'dt-devtools-container'; |
|
container.style.display = 'none'; |
|
|
|
|
|
const header = document.createElement('div'); |
|
header.className = 'dt-devtools-header'; |
|
|
|
const tabs = document.createElement('div'); |
|
tabs.className = 'dt-devtools-tabs'; |
|
|
|
const consoleTab = createTab('Console', 'console'); |
|
const elementsTab = createTab('Elements', 'elements'); |
|
const networkTab = createTab('Network', 'network'); |
|
const storageTab = createTab('Storage', 'storage'); |
|
|
|
tabs.appendChild(consoleTab); |
|
tabs.appendChild(elementsTab); |
|
tabs.appendChild(networkTab); |
|
tabs.appendChild(storageTab); |
|
|
|
const closeBtn = document.createElement('button'); |
|
closeBtn.className = 'dt-devtools-close'; |
|
closeBtn.textContent = '×'; |
|
closeBtn.onclick = toggleDevTools; |
|
|
|
header.appendChild(tabs); |
|
header.appendChild(closeBtn); |
|
|
|
|
|
const content = document.createElement('div'); |
|
content.className = 'dt-devtools-content'; |
|
|
|
const consolePanel = createConsolePanel(); |
|
const elementsPanel = createElementsPanel(); |
|
const networkPanel = createNetworkPanel(); |
|
const storagePanel = createStoragePanel(); |
|
|
|
content.appendChild(consolePanel); |
|
content.appendChild(elementsPanel); |
|
content.appendChild(networkPanel); |
|
content.appendChild(storagePanel); |
|
|
|
container.appendChild(header); |
|
container.appendChild(content); |
|
|
|
document.body.appendChild(container); |
|
|
|
|
|
createContextMenu(); |
|
|
|
|
|
function createTab(name, panelId) { |
|
const tab = document.createElement('div'); |
|
tab.className = 'dt-devtools-tab'; |
|
tab.textContent = name; |
|
tab.onclick = () => { |
|
document.querySelectorAll('.dt-devtools-tab').forEach(t => t.classList.remove('active')); |
|
document.querySelectorAll('.dt-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 createNetworkPanel() { |
|
const panel = document.createElement('div'); |
|
panel.className = 'dt-devtools-panel'; |
|
panel.id = 'network-panel'; |
|
|
|
const container = document.createElement('div'); |
|
container.className = 'dt-network-container'; |
|
panel.appendChild(container); |
|
|
|
|
|
const requestsList = document.createElement('div'); |
|
requestsList.className = 'dt-network-requests'; |
|
container.appendChild(requestsList); |
|
|
|
|
|
const detailsPanel = document.createElement('div'); |
|
detailsPanel.className = 'dt-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 |
|
}; |
|
|
|
dtNetworkRequests.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 |
|
}; |
|
|
|
dtNetworkRequests.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('.dt-network-requests'); |
|
const detailsPanel = panel.querySelector('.dt-network-details'); |
|
|
|
requestsList.innerHTML = ''; |
|
|
|
dtNetworkRequests.forEach(request => { |
|
const requestElement = document.createElement('div'); |
|
requestElement.className = 'dt-network-request'; |
|
if (dtSelectedRequest && dtSelectedRequest.id === request.id) { |
|
requestElement.classList.add('selected'); |
|
} |
|
|
|
requestElement.onclick = () => { |
|
dtSelectedRequest = request; |
|
renderNetworkRequests(); |
|
renderNetworkDetails(); |
|
}; |
|
|
|
const statusElement = document.createElement('div'); |
|
statusElement.className = `dt-network-status ${request.status}`; |
|
statusElement.textContent = request.status === 'success' ? '✓' : '✕'; |
|
|
|
const methodElement = document.createElement('div'); |
|
methodElement.className = 'dt-network-method'; |
|
methodElement.textContent = request.method; |
|
|
|
const urlElement = document.createElement('div'); |
|
urlElement.className = 'dt-network-url'; |
|
urlElement.textContent = request.url; |
|
|
|
const timeElement = document.createElement('div'); |
|
timeElement.className = 'dt-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 || !dtSelectedRequest) return; |
|
|
|
const detailsPanel = panel.querySelector('.dt-network-details'); |
|
detailsPanel.innerHTML = ''; |
|
|
|
|
|
const generalSection = document.createElement('div'); |
|
generalSection.className = 'dt-network-detail-section'; |
|
generalSection.innerHTML = ` |
|
<div class="dt-network-detail-title">一般</div> |
|
<div class="dt-network-detail-content"> |
|
<strong>URL:</strong> ${dtSelectedRequest.url}<br> |
|
<strong>メソッド:</strong> ${dtSelectedRequest.method}<br> |
|
<strong>ステータス:</strong> ${dtSelectedRequest.response ? dtSelectedRequest.response.status : '-'}<br> |
|
<strong>時間:</strong> ${dtSelectedRequest.duration ? Math.round(dtSelectedRequest.duration) + 'ms' : '-'} |
|
</div> |
|
`; |
|
detailsPanel.appendChild(generalSection); |
|
|
|
|
|
if (dtSelectedRequest.requestHeaders) { |
|
const headersSection = document.createElement('div'); |
|
headersSection.className = 'dt-network-detail-section'; |
|
|
|
const headersTitle = document.createElement('div'); |
|
headersTitle.className = 'dt-network-detail-title'; |
|
headersTitle.textContent = 'リクエストヘッダー'; |
|
|
|
const headersContent = document.createElement('div'); |
|
headersContent.className = 'dt-network-detail-content'; |
|
|
|
if (typeof dtSelectedRequest.requestHeaders === 'object' && !(dtSelectedRequest.requestHeaders instanceof Headers)) { |
|
Object.entries(dtSelectedRequest.requestHeaders).forEach(([key, value]) => { |
|
headersContent.innerHTML += `<strong>${key}:</strong> ${value}<br>`; |
|
}); |
|
} else if (dtSelectedRequest.requestHeaders instanceof Headers) { |
|
dtSelectedRequest.requestHeaders.forEach((value, key) => { |
|
headersContent.innerHTML += `<strong>${key}:</strong> ${value}<br>`; |
|
}); |
|
} |
|
|
|
headersSection.appendChild(headersTitle); |
|
headersSection.appendChild(headersContent); |
|
detailsPanel.appendChild(headersSection); |
|
} |
|
|
|
|
|
if (dtSelectedRequest.requestBody) { |
|
const bodySection = document.createElement('div'); |
|
bodySection.className = 'dt-network-detail-section'; |
|
|
|
const bodyTitle = document.createElement('div'); |
|
bodyTitle.className = 'dt-network-detail-title'; |
|
bodyTitle.textContent = 'リクエストボディ'; |
|
|
|
const bodyContent = document.createElement('div'); |
|
bodyContent.className = 'dt-network-detail-content'; |
|
|
|
try { |
|
if (typeof dtSelectedRequest.requestBody === 'string') { |
|
bodyContent.textContent = dtSelectedRequest.requestBody; |
|
} else if (typeof dtSelectedRequest.requestBody === 'object') { |
|
bodyContent.textContent = JSON.stringify(dtSelectedRequest.requestBody, null, 2); |
|
} |
|
} catch (e) { |
|
bodyContent.textContent = 'ボディを表示できません'; |
|
} |
|
|
|
bodySection.appendChild(bodyTitle); |
|
bodySection.appendChild(bodyContent); |
|
detailsPanel.appendChild(bodySection); |
|
} |
|
|
|
|
|
if (dtSelectedRequest.response) { |
|
const responseSection = document.createElement('div'); |
|
responseSection.className = 'dt-network-detail-section'; |
|
|
|
const responseTitle = document.createElement('div'); |
|
responseTitle.className = 'dt-network-detail-title'; |
|
responseTitle.textContent = 'レスポンス'; |
|
|
|
const responseContent = document.createElement('div'); |
|
responseContent.className = 'dt-network-detail-content'; |
|
|
|
if (dtSelectedRequest.response.body) { |
|
if (typeof dtSelectedRequest.response.body === 'object') { |
|
responseContent.textContent = JSON.stringify(dtSelectedRequest.response.body, null, 2); |
|
} else { |
|
responseContent.textContent = dtSelectedRequest.response.body; |
|
} |
|
} else { |
|
responseContent.textContent = 'レスポンスボディがありません'; |
|
} |
|
|
|
responseSection.appendChild(responseTitle); |
|
responseSection.appendChild(responseContent); |
|
detailsPanel.appendChild(responseSection); |
|
} |
|
|
|
|
|
if (dtSelectedRequest.error) { |
|
const errorSection = document.createElement('div'); |
|
errorSection.className = 'dt-network-detail-section'; |
|
|
|
const errorTitle = document.createElement('div'); |
|
errorTitle.className = 'dt-network-detail-title'; |
|
errorTitle.textContent = 'エラー'; |
|
|
|
const errorContent = document.createElement('div'); |
|
errorContent.className = 'dt-network-detail-content'; |
|
errorContent.textContent = `${dtSelectedRequest.error.name}: ${dtSelectedRequest.error.message}`; |
|
|
|
if (dtSelectedRequest.error.stack) { |
|
errorContent.innerHTML += `<br><br>${dtSelectedRequest.error.stack.replace(/\n/g, '<br>')}`; |
|
} |
|
|
|
errorSection.appendChild(errorTitle); |
|
errorSection.appendChild(errorContent); |
|
detailsPanel.appendChild(errorSection); |
|
} |
|
} |
|
|
|
|
|
function createContextMenu() { |
|
dtContextMenu = document.createElement('div'); |
|
dtContextMenu.className = 'dt-context-menu'; |
|
dtContextMenu.innerHTML = ` |
|
<div class="dt-context-menu-item" data-action="edit-html">HTMLとして編集</div> |
|
<div class="dt-context-menu-item" data-action="edit-whole-html">HTML全体を編集</div> |
|
<div class="dt-context-menu-item" data-action="add-attribute">属性を追加</div> |
|
<div class="dt-context-menu-item" data-action="edit-element">要素を編集</div> |
|
<div class="dt-context-menu-item" data-action="duplicate">要素を複製</div> |
|
<div class="dt-context-menu-item" data-action="remove">要素を削除</div> |
|
<div class="dt-context-menu-item" data-action="toggle-visibility">要素を非表示</div> |
|
<div class="dt-context-menu-item" data-action="force-state">状態を強制</div> |
|
`; |
|
document.body.appendChild(dtContextMenu); |
|
|
|
dtContextMenu.querySelectorAll('.dt-context-menu-item').forEach(item => { |
|
item.addEventListener('click', (e) => { |
|
const action = e.target.getAttribute('data-action'); |
|
handleContextMenuAction(action); |
|
dtContextMenu.style.display = 'none'; |
|
}); |
|
}); |
|
|
|
document.addEventListener('click', (e) => { |
|
if (e.target !== dtContextMenu && !dtContextMenu.contains(e.target)) { |
|
dtContextMenu.style.display = 'none'; |
|
} |
|
}); |
|
} |
|
|
|
|
|
function createStoragePanel() { |
|
const panel = document.createElement('div'); |
|
panel.className = 'dt-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 = 'dt-storage-table'; |
|
panel.appendChild(localStorageTable); |
|
|
|
const addLocalStorageBtn = document.createElement('button'); |
|
addLocalStorageBtn.className = 'dt-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 = 'dt-storage-table'; |
|
panel.appendChild(sessionStorageTable); |
|
|
|
const addSessionStorageBtn = document.createElement('button'); |
|
addSessionStorageBtn.className = 'dt-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 = 'dt-storage-table'; |
|
panel.appendChild(cookiesTable); |
|
|
|
const addCookieBtn = document.createElement('button'); |
|
addCookieBtn.className = 'dt-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 = 'dt-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 = 'dt-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 = 'dt-storage-actions'; |
|
|
|
const deleteBtn = document.createElement('button'); |
|
deleteBtn.className = 'dt-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 = 'dt-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 = 'dt-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 = 'dt-storage-actions'; |
|
|
|
const deleteBtn = document.createElement('button'); |
|
deleteBtn.className = 'dt-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 (!dtSelectedElement) return; |
|
|
|
switch (action) { |
|
case 'edit-html': |
|
if (dtSelectedElement === document.documentElement) { |
|
alert('ルートHTML要素は直接編集できません'); |
|
return; |
|
} |
|
startInlineEdit(dtSelectedDOMNode, dtSelectedElement.outerHTML, (newValue) => { |
|
try { |
|
dtSelectedElement.outerHTML = newValue; |
|
dtRefreshElementsPanel(); |
|
} catch (e) { |
|
alert('この要素は編集できません: ' + e.message); |
|
} |
|
}); |
|
break; |
|
case 'edit-whole-html': |
|
const htmlContent = document.documentElement.outerHTML; |
|
const textarea = document.createElement('textarea'); |
|
textarea.style.width = '100%'; |
|
textarea.style.height = '300px'; |
|
textarea.value = htmlContent; |
|
|
|
const modal = document.createElement('div'); |
|
modal.style.position = 'fixed'; |
|
modal.style.top = '0'; |
|
modal.style.left = '0'; |
|
modal.style.width = '100%'; |
|
modal.style.height = '100%'; |
|
modal.style.backgroundColor = 'rgba(0,0,0,0.8)'; |
|
modal.style.zIndex = '10000'; |
|
modal.style.display = 'flex'; |
|
modal.style.flexDirection = 'column'; |
|
modal.style.padding = '20px'; |
|
modal.style.boxSizing = 'border-box'; |
|
|
|
const buttonContainer = document.createElement('div'); |
|
buttonContainer.style.marginTop = '10px'; |
|
buttonContainer.style.display = 'flex'; |
|
buttonContainer.style.gap = '10px'; |
|
|
|
const saveButton = document.createElement('button'); |
|
saveButton.textContent = '保存'; |
|
saveButton.onclick = () => { |
|
try { |
|
document.documentElement.innerHTML = textarea.value; |
|
modal.remove(); |
|
dtRefreshElementsPanel(); |
|
} catch (e) { |
|
alert('HTMLの解析に失敗しました: ' + e.message); |
|
} |
|
}; |
|
|
|
const cancelButton = document.createElement('button'); |
|
cancelButton.textContent = 'キャンセル'; |
|
cancelButton.onclick = () => { |
|
modal.remove(); |
|
}; |
|
|
|
buttonContainer.appendChild(saveButton); |
|
buttonContainer.appendChild(cancelButton); |
|
|
|
modal.appendChild(textarea); |
|
modal.appendChild(buttonContainer); |
|
|
|
document.body.appendChild(modal); |
|
break; |
|
case 'add-attribute': |
|
const attrName = prompt('属性名を入力'); |
|
if (attrName) { |
|
const attrValue = prompt('属性値を入力'); |
|
dtSelectedElement.setAttribute(attrName, attrValue || ''); |
|
dtRefreshElementsPanel(); |
|
} |
|
break; |
|
case 'edit-element': |
|
startInlineEdit(dtSelectedDOMNode.querySelector('.dt-dom-tag'), dtSelectedElement.tagName.toLowerCase(), (newValue) => { |
|
const newElement = document.createElement(newValue); |
|
Array.from(dtSelectedElement.attributes).forEach(attr => { |
|
newElement.setAttribute(attr.name, attr.value); |
|
}); |
|
newElement.innerHTML = dtSelectedElement.innerHTML; |
|
dtSelectedElement.parentNode.replaceChild(newElement, dtSelectedElement); |
|
dtSelectedElement = newElement; |
|
dtRefreshElementsPanel(); |
|
}); |
|
break; |
|
case 'duplicate': |
|
const clone = dtSelectedElement.cloneNode(true); |
|
dtSelectedElement.parentNode.insertBefore(clone, dtSelectedElement.nextSibling); |
|
dtRefreshElementsPanel(); |
|
break; |
|
case 'remove': |
|
if (confirm('要素を削除しますか?')) { |
|
dtSelectedElement.parentNode.removeChild(dtSelectedElement); |
|
dtRefreshElementsPanel(); |
|
} |
|
break; |
|
case 'toggle-visibility': |
|
if (dtSelectedElement.style.display === 'none') { |
|
dtSelectedElement.style.display = ''; |
|
} else { |
|
dtSelectedElement.style.display = 'none'; |
|
} |
|
dtRefreshElementsPanel(); |
|
break; |
|
case 'force-state': |
|
const state = prompt('強制する状態を入力 (例: hover, active, focus)', 'hover'); |
|
if (state) { |
|
dtSelectedElement.classList.remove('force-hover', 'force-active', 'force-focus', |
|
'force-focus-within', 'force-focus-visible', 'force-target'); |
|
dtSelectedElement.classList.add(`force-${state}`); |
|
dtRefreshElementsPanel(); |
|
} |
|
break; |
|
} |
|
} |
|
|
|
|
|
function startInlineEdit(element, initialValue, callback) { |
|
if (dtActiveEditElement) return; |
|
|
|
const originalValue = element.textContent; |
|
const rect = element.getBoundingClientRect(); |
|
|
|
const input = document.createElement('input'); |
|
input.className = 'dt-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(); |
|
|
|
dtActiveEditElement = { |
|
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(); |
|
dtActiveEditElement = null; |
|
} |
|
} |
|
|
|
|
|
function createConsolePanel() { |
|
const panel = document.createElement('div'); |
|
panel.className = 'dt-devtools-panel'; |
|
panel.id = 'console-panel'; |
|
|
|
const log = document.createElement('div'); |
|
log.id = 'dt-console-log'; |
|
|
|
const input = document.createElement('input'); |
|
input.className = 'dt-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, 'dt-console-log'); |
|
logMessage('← ' + formatOutput(result), 'dt-console-log'); |
|
} |
|
} catch (err) { |
|
logMessage(err.message, 'dt-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(' '), 'dt-console-log'); |
|
}; |
|
|
|
console.error = (...args) => { |
|
originalConsole.error.apply(console, args); |
|
logMessage(args.map(arg => formatOutput(arg)).join(' '), 'dt-console-error'); |
|
}; |
|
|
|
console.warn = (...args) => { |
|
originalConsole.warn.apply(console, args); |
|
logMessage(args.map(arg => formatOutput(arg)).join(' '), 'dt-console-warn'); |
|
}; |
|
|
|
console.info = (...args) => { |
|
originalConsole.info.apply(console, args); |
|
logMessage(args.map(arg => formatOutput(arg)).join(' '), 'dt-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="dt-json-null">null</span>'; |
|
if (output === undefined) return '<span class="dt-json-null">undefined</span>'; |
|
if (typeof output === 'boolean') return `<span class="dt-json-boolean">${output}</span>`; |
|
if (typeof output === 'number') return `<span class="dt-json-number">${output}</span>`; |
|
if (typeof output === 'string') return `<span class="dt-json-string">"${output}"</span>`; |
|
if (typeof output === 'function') return `<span class="dt-json-object">function ${output.name}() { ... }</span>`; |
|
if (Array.isArray(output)) return `<span class="dt-json-object">[${output.map(formatOutput).join(', ')}]</span>`; |
|
if (typeof output === 'object') { |
|
try { |
|
return `<span class="dt-json-object">${JSON.stringify(output, null, 2) |
|
.replace(/"([^"]+)":/g, '<span class="dt-json-key">"$1"</span>:') |
|
.replace(/"([^"]+)"/g, '<span class="dt-json-string">"$1"</span>') |
|
.replace(/\b(true|false)\b/g, '<span class="dt-json-boolean">$1</span>') |
|
.replace(/\b(null)\b/g, '<span class="dt-json-null">$1</span>') |
|
.replace(/\b(\d+)\b/g, '<span class="dt-json-number">$1</span>')}</span>`; |
|
} catch (e) { |
|
return `<span class="dt-json-object">${output.toString()}</span>`; |
|
} |
|
} |
|
return output; |
|
} |
|
|
|
return panel; |
|
} |
|
|
|
|
|
function createElementsPanel() { |
|
const panel = document.createElement('div'); |
|
panel.className = 'dt-devtools-panel'; |
|
panel.id = 'elements-panel'; |
|
|
|
const container = document.createElement('div'); |
|
container.className = 'dt-elements-container'; |
|
|
|
const tree = document.createElement('div'); |
|
tree.className = 'dt-dom-tree'; |
|
tree.id = 'dt-dom-tree'; |
|
|
|
const cssPanel = document.createElement('div'); |
|
cssPanel.className = 'dt-css-panel'; |
|
cssPanel.id = 'dt-css-panel'; |
|
|
|
container.appendChild(tree); |
|
container.appendChild(cssPanel); |
|
panel.appendChild(container); |
|
|
|
|
|
function updateCSSPanel(element) { |
|
const cssPanel = document.getElementById('dt-css-panel'); |
|
cssPanel.innerHTML = ''; |
|
|
|
if (!element) return; |
|
|
|
if (element.style.length > 0) { |
|
const inlineRule = document.createElement('div'); |
|
inlineRule.className = 'dt-css-rule'; |
|
|
|
const selector = document.createElement('div'); |
|
selector.className = 'dt-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 = 'dt-css-property'; |
|
|
|
const nameSpan = document.createElement('span'); |
|
nameSpan.className = 'dt-css-property-name dt-editable'; |
|
nameSpan.textContent = propName; |
|
nameSpan.onclick = () => editCSSProperty(element, propName, 'style'); |
|
|
|
const valueSpan = document.createElement('span'); |
|
valueSpan.className = 'dt-css-property-value dt-editable'; |
|
valueSpan.textContent = propValue; |
|
valueSpan.onclick = () => editCSSProperty(element, propName, 'style'); |
|
|
|
const toggleSpan = document.createElement('span'); |
|
toggleSpan.className = 'dt-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 = 'dt-css-rule'; |
|
|
|
const computedSelector = document.createElement('div'); |
|
computedSelector.className = 'dt-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 = 'dt-css-property'; |
|
|
|
const nameSpan = document.createElement('span'); |
|
nameSpan.className = 'dt-css-property-name'; |
|
nameSpan.textContent = prop; |
|
|
|
const valueSpan = document.createElement('span'); |
|
valueSpan.className = 'dt-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 = 'dt-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(); |
|
dtSelectedElement = node; |
|
dtSelectedDOMNode = element; |
|
|
|
document.querySelectorAll('.dt-dom-node').forEach(el => el.classList.remove('selected')); |
|
element.classList.add('selected'); |
|
|
|
if (node !== document.documentElement) { |
|
dtContextMenu.style.display = 'block'; |
|
dtContextMenu.style.left = `${e.pageX}px`; |
|
dtContextMenu.style.top = `${e.pageY}px`; |
|
} |
|
|
|
updateCSSPanel(node); |
|
}; |
|
|
|
|
|
element.onclick = (e) => { |
|
if (e.target.classList.contains('dt-dom-toggle')) return; |
|
|
|
e.stopPropagation(); |
|
dtSelectedElement = node; |
|
dtSelectedDOMNode = element; |
|
|
|
document.querySelectorAll('.dt-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 = 'dt-dom-toggle'; |
|
toggle.onclick = (e) => { |
|
e.stopPropagation(); |
|
const children = element.querySelector('.dt-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 = 'dt-dom-tag'; |
|
tag.textContent = `<${node.tagName.toLowerCase()}`; |
|
|
|
if (node !== document.documentElement) { |
|
tag.classList.add('dt-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); |
|
dtSelectedElement = newElement; |
|
dtRefreshElementsPanel(); |
|
}); |
|
}; |
|
} |
|
|
|
element.appendChild(tag); |
|
|
|
|
|
Array.from(node.attributes).forEach(attr => { |
|
const attrSpan = document.createElement('span'); |
|
attrSpan.className = 'dt-dom-attr'; |
|
attrSpan.textContent = ` ${attr.name}="${attr.value}"`; |
|
|
|
if (node !== document.documentElement) { |
|
attrSpan.classList.add('dt-editable'); |
|
attrSpan.onclick = (e) => { |
|
e.stopPropagation(); |
|
startInlineEdit(attrSpan, attr.value, (newValue) => { |
|
node.setAttribute(attr.name, newValue); |
|
dtRefreshElementsPanel(); |
|
}); |
|
}; |
|
} |
|
|
|
element.appendChild(attrSpan); |
|
}); |
|
|
|
element.appendChild(document.createTextNode('>')); |
|
|
|
if (hasChildren) { |
|
const childrenContainer = document.createElement('div'); |
|
childrenContainer.className = 'dt-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="dt-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 = 'dt-dom-text dt-editable'; |
|
text.textContent = `"${node.textContent.trim()}"`; |
|
text.onclick = (e) => { |
|
e.stopPropagation(); |
|
startInlineEdit(text, node.textContent.trim(), (newValue) => { |
|
node.textContent = newValue; |
|
dtRefreshElementsPanel(); |
|
}); |
|
}; |
|
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); |
|
dtRefreshElementsPanel(); |
|
} |
|
} |
|
|
|
let isRefreshing = false; |
|
dtRefreshElementsPanel = function() { |
|
if (isRefreshing) return; |
|
isRefreshing = true; |
|
|
|
requestAnimationFrame(() => { |
|
let tree = document.getElementById('dt-dom-tree'); |
|
tree.innerHTML = ''; |
|
buildDOMTree(document.documentElement, tree, 0, true); |
|
isRefreshing = false; |
|
}); |
|
}; |
|
|
|
setupMutationObserver(); |
|
|
|
|
|
setTimeout(() => { |
|
dtRefreshElementsPanel(); |
|
}, 0); |
|
|
|
return panel; |
|
} |
|
|
|
|
|
function toggleDevTools() { |
|
const container = document.getElementById('dt-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 = 'dt-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(--dt-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() { |
|
|
|
createOpenButton(); |
|
|
|
|
|
setTimeout(() => { |
|
createDevTools(); |
|
console.log('開発者ツールが初期化されました'); |
|
}, 1000); |
|
}); |
|
})(); |