|
(function() { |
|
|
|
const style = document.createElement('style'); |
|
style.textContent = ` |
|
.devtools-container { |
|
position: fixed; |
|
bottom: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 300px; |
|
background-color: #fff; |
|
border-top: 1px solid #ddd; |
|
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.1); |
|
z-index: 9999; |
|
display: flex; |
|
flex-direction: column; |
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; |
|
} |
|
|
|
.devtools-header { |
|
display: flex; |
|
justify-content: space-between; |
|
align-items: center; |
|
padding: 5px 10px; |
|
background-color: #f5f5f5; |
|
border-bottom: 1px solid #ddd; |
|
} |
|
|
|
.devtools-tabs { |
|
display: flex; |
|
gap: 10px; |
|
} |
|
|
|
.devtools-tab { |
|
padding: 5px 10px; |
|
cursor: pointer; |
|
border-radius: 3px 3px 0 0; |
|
background-color: #e0e0e0; |
|
border: 1px solid #ccc; |
|
border-bottom: none; |
|
font-size: 12px; |
|
} |
|
|
|
.devtools-tab.active { |
|
background-color: #fff; |
|
border-bottom: 1px solid #fff; |
|
margin-bottom: -1px; |
|
font-weight: bold; |
|
} |
|
|
|
.devtools-close { |
|
background: none; |
|
border: none; |
|
font-size: 16px; |
|
cursor: pointer; |
|
padding: 0 5px; |
|
} |
|
|
|
.devtools-content { |
|
flex: 1; |
|
overflow: auto; |
|
position: relative; |
|
} |
|
|
|
.devtools-panel { |
|
position: absolute; |
|
top: 0; |
|
left: 0; |
|
width: 100%; |
|
height: 100%; |
|
padding: 10px; |
|
overflow: auto; |
|
display: none; |
|
} |
|
|
|
.devtools-panel.active { |
|
display: block; |
|
} |
|
|
|
/* Console スタイル */ |
|
#console-log { |
|
white-space: pre-wrap; |
|
margin: 0; |
|
line-height: 1.4; |
|
flex: 1; |
|
color: #333; |
|
} |
|
|
|
.console-input { |
|
width: 100%; |
|
background: #f5f5f5; |
|
border: 1px solid #ddd; |
|
color: #333; |
|
padding: 8px; |
|
margin-top: 10px; |
|
font-family: monospace; |
|
} |
|
|
|
/* Elements スタイル */ |
|
.elements-container { |
|
display: flex; |
|
flex: 1; |
|
overflow: hidden; |
|
} |
|
|
|
.dom-tree { |
|
font-family: monospace; |
|
flex: 1; |
|
overflow: auto; |
|
border-right: 1px solid #ddd; |
|
padding-right: 10px; |
|
color: #333; |
|
} |
|
|
|
.dom-node { |
|
margin-left: 15px; |
|
position: relative; |
|
line-height: 1.4; |
|
} |
|
|
|
.dom-node.selected { |
|
background: rgba(79, 195, 247, 0.2); |
|
} |
|
|
|
.dom-tag { |
|
color: #0066cc; |
|
font-weight: bold; |
|
} |
|
|
|
.dom-attr { |
|
color: #d33682; |
|
} |
|
|
|
.dom-attr.editable:hover { |
|
text-decoration: underline; |
|
cursor: pointer; |
|
} |
|
|
|
.dom-text { |
|
color: #333; |
|
} |
|
|
|
.dom-edit-input { |
|
background: #fff; |
|
border: 1px solid #4fc3f7; |
|
padding: 0 2px; |
|
margin: -1px 0; |
|
font-family: monospace; |
|
min-width: 50px; |
|
} |
|
|
|
.css-panel { |
|
flex: 1; |
|
overflow: auto; |
|
padding-left: 10px; |
|
} |
|
|
|
.css-rule { |
|
margin-bottom: 15px; |
|
border: 1px solid #ddd; |
|
padding: 8px; |
|
background-color: #f9f9f9; |
|
} |
|
|
|
.css-selector { |
|
color: #0066cc; |
|
margin-bottom: 5px; |
|
font-weight: bold; |
|
} |
|
|
|
.css-property { |
|
display: flex; |
|
margin-bottom: 3px; |
|
} |
|
|
|
.css-property-name { |
|
color: #d33682; |
|
min-width: 120px; |
|
} |
|
|
|
.css-property-value { |
|
color: #333; |
|
flex: 1; |
|
} |
|
|
|
.css-toggle { |
|
margin-left: 10px; |
|
color: #dc322f; |
|
cursor: pointer; |
|
} |
|
|
|
/* Context Menu */ |
|
.context-menu { |
|
position: absolute; |
|
background: #fff; |
|
border: 1px solid #ddd; |
|
z-index: 10000; |
|
min-width: 200px; |
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); |
|
display: none; |
|
} |
|
|
|
.context-menu-item { |
|
padding: 8px 15px; |
|
cursor: pointer; |
|
color: #333; |
|
} |
|
|
|
.context-menu-item:hover { |
|
background: #4fc3f7; |
|
color: #000; |
|
} |
|
|
|
/* Storage スタイル */ |
|
.storage-table { |
|
width: 100%; |
|
border-collapse: collapse; |
|
margin-bottom: 10px; |
|
} |
|
|
|
.storage-table th, .storage-table td { |
|
border: 1px solid #ddd; |
|
padding: 5px; |
|
text-align: left; |
|
} |
|
|
|
.storage-table th { |
|
background: #f5f5f5; |
|
} |
|
|
|
.storage-actions { |
|
display: flex; |
|
gap: 5px; |
|
} |
|
|
|
.storage-btn { |
|
background: #4fc3f7; |
|
border: none; |
|
padding: 2px 5px; |
|
cursor: pointer; |
|
border-radius: 3px; |
|
} |
|
|
|
.editable { |
|
cursor: pointer; |
|
padding: 2px 5px; |
|
border: 1px dashed transparent; |
|
} |
|
|
|
.editable:hover { |
|
border-color: #4fc3f7; |
|
} |
|
|
|
.add-btn { |
|
background: #4fc3f7; |
|
color: #000; |
|
border: none; |
|
padding: 5px 10px; |
|
margin-top: 10px; |
|
cursor: pointer; |
|
border-radius: 3px; |
|
} |
|
|
|
.json-object { |
|
color: #268bd2; |
|
} |
|
|
|
.json-string { |
|
color: #2aa198; |
|
} |
|
|
|
.json-number { |
|
color: #d33682; |
|
} |
|
|
|
.json-boolean { |
|
color: #b58900; |
|
} |
|
|
|
.json-null { |
|
color: #6c71c4; |
|
} |
|
|
|
`; |
|
document.head.appendChild(style); |
|
|
|
|
|
let contextMenu = null; |
|
let selectedElement = null; |
|
let selectedDOMNode = null; |
|
let activeEditElement = null; |
|
|
|
|
|
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 storageTab = createTab('Storage', 'storage'); |
|
|
|
tabs.appendChild(consoleTab); |
|
tabs.appendChild(elementsTab); |
|
tabs.appendChild(storageTab); |
|
|
|
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 storagePanel = createStoragePanel(); |
|
|
|
content.appendChild(consolePanel); |
|
content.appendChild(elementsPanel); |
|
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 = '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'); |
|
}; |
|
return tab; |
|
} |
|
|
|
elementsTab.click(); |
|
} |
|
|
|
|
|
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, '#0066cc'); |
|
logMessage('← ' + formatOutput(result), '#2aa198'); |
|
} |
|
} catch (err) { |
|
logMessage(err.message, '#dc322f'); |
|
} |
|
e.target.value = ''; |
|
} |
|
}; |
|
|
|
panel.appendChild(log); |
|
panel.appendChild(input); |
|
|
|
['log', 'error', 'warn'].forEach(method => { |
|
const original = console[method]; |
|
console[method] = (...args) => { |
|
original.apply(console, args); |
|
const color = method === 'error' ? '#dc322f' : method === 'warn' ? '#b58900' : '#586e75'; |
|
logMessage(args.map(arg => formatOutput(arg)).join(' '), color); |
|
}; |
|
}); |
|
|
|
function logMessage(message, color) { |
|
const line = document.createElement('div'); |
|
line.style.color = color; |
|
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-string">"$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) { |
|
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); |
|
}; |
|
|
|
|
|
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 (node.childNodes.length > 0) { |
|
node.childNodes.forEach(child => { |
|
buildDOMTree(child, element, depth + 1); |
|
}); |
|
} |
|
|
|
if (node.childNodes.length > 0 || 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>`; |
|
element.appendChild(closeTag); |
|
} |
|
|
|
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); |
|
|
|
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); |
|
} |
|
} |
|
} |
|
} |
|
|
|
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 = '#4fc3f7'; |
|
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を実行できます'); |
|
}); |
|
})(); |