dev-tools / dev-tools.js
soiz1's picture
Update dev-tools.js
f687325 verified
(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';
// LocalStorage表示
const localStorageTitle = document.createElement('h3');
localStorageTitle.textContent = 'Local Storage';
panel.appendChild(localStorageTitle);
const localStorageTable = document.createElement('table');
localStorageTable.className = 'storage-table';
panel.appendChild(localStorageTable);
const addLocalStorageBtn = document.createElement('button');
addLocalStorageBtn.className = 'add-btn';
addLocalStorageBtn.textContent = '+ Local Storageに追加';
addLocalStorageBtn.onclick = () => {
const key = prompt('キー名を入力');
if (key) {
const value = prompt('値を入力');
localStorage.setItem(key, value);
renderStorage();
}
};
panel.appendChild(addLocalStorageBtn);
// SessionStorage表示
const sessionStorageTitle = document.createElement('h3');
sessionStorageTitle.style.marginTop = '20px';
sessionStorageTitle.textContent = 'Session Storage';
panel.appendChild(sessionStorageTitle);
const sessionStorageTable = document.createElement('table');
sessionStorageTable.className = 'storage-table';
panel.appendChild(sessionStorageTable);
const addSessionStorageBtn = document.createElement('button');
addSessionStorageBtn.className = 'add-btn';
addSessionStorageBtn.textContent = '+ Session Storageに追加';
addSessionStorageBtn.onclick = () => {
const key = prompt('キー名を入力');
if (key) {
const value = prompt('値を入力');
sessionStorage.setItem(key, value);
renderStorage();
}
};
panel.appendChild(addSessionStorageBtn);
// Cookie表示
const cookiesTitle = document.createElement('h3');
cookiesTitle.style.marginTop = '20px';
cookiesTitle.textContent = 'Cookies';
panel.appendChild(cookiesTitle);
const cookiesTable = document.createElement('table');
cookiesTable.className = 'storage-table';
panel.appendChild(cookiesTable);
const addCookieBtn = document.createElement('button');
addCookieBtn.className = 'add-btn';
addCookieBtn.textContent = '+ Cookieに追加';
addCookieBtn.onclick = () => {
const name = prompt('Cookie名を入力');
if (name) {
const value = prompt('値を入力');
document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}; path=/`;
renderStorage();
}
};
panel.appendChild(addCookieBtn);
// ストレージを表示する関数
function renderStorage() {
renderTable(localStorageTable, localStorage, 'local');
renderTable(sessionStorageTable, sessionStorage, 'session');
renderCookiesTable(cookiesTable);
}
function renderTable(tableElement, storage, type) {
tableElement.innerHTML = `
<thead>
<tr>
<th>Key</th>
<th>Value</th>
<th>Actions</th>
</tr>
</thead>
<tbody></tbody>
`;
const tbody = tableElement.querySelector('tbody');
for (let i = 0; i < storage.length; i++) {
const key = storage.key(i);
const value = storage.getItem(key);
const row = document.createElement('tr');
const keyCell = document.createElement('td');
const keySpan = document.createElement('span');
keySpan.className = 'editable';
keySpan.textContent = key;
keySpan.onclick = () => {
const newKey = prompt('新しいキー名を入力', key);
if (newKey && newKey !== key) {
storage.setItem(newKey, value);
storage.removeItem(key);
renderStorage();
}
};
keyCell.appendChild(keySpan);
const valueCell = document.createElement('td');
const valueSpan = document.createElement('span');
valueSpan.className = 'editable';
valueSpan.textContent = value;
valueSpan.onclick = () => {
const newValue = prompt('新しい値を入力', value);
if (newValue !== null) {
storage.setItem(key, newValue);
renderStorage();
}
};
valueCell.appendChild(valueSpan);
const actionsCell = document.createElement('td');
actionsCell.className = 'storage-actions';
const deleteBtn = document.createElement('button');
deleteBtn.className = 'storage-btn';
deleteBtn.textContent = 'Delete';
deleteBtn.onclick = () => {
storage.removeItem(key);
renderStorage();
};
actionsCell.appendChild(deleteBtn);
row.appendChild(keyCell);
row.appendChild(valueCell);
row.appendChild(actionsCell);
tbody.appendChild(row);
}
}
function renderCookiesTable(tableElement) {
tableElement.innerHTML = `
<thead>
<tr>
<th>Name</th>
<th>Value</th>
<th>Actions</th>
</tr>
</thead>
<tbody></tbody>
`;
const tbody = tableElement.querySelector('tbody');
document.cookie.split(';').forEach(cookie => {
if (!cookie.trim()) return;
const [name, ...valueParts] = cookie.split('=');
const decodedName = decodeURIComponent(name.trim());
const value = valueParts.join('=').trim();
const row = document.createElement('tr');
const nameCell = document.createElement('td');
const nameSpan = document.createElement('span');
nameSpan.className = 'editable';
nameSpan.textContent = decodedName;
nameSpan.onclick = () => {
const newName = prompt('新しい名前を入力', decodedName);
if (newName && newName !== decodedName) {
const newValue = prompt('新しい値を入力', decodeURIComponent(value));
if (newValue !== null) {
document.cookie = `${encodeURIComponent(newName)}=${encodeURIComponent(newValue)}; expires=Fri, 31 Dec 9999 23:59:59 GMT; path=/`;
document.cookie = `${name.trim()}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
renderStorage();
}
}
};
nameCell.appendChild(nameSpan);
const valueCell = document.createElement('td');
const valueSpan = document.createElement('span');
valueSpan.className = 'editable';
valueSpan.textContent = decodeURIComponent(value);
valueSpan.onclick = () => {
const newValue = prompt('新しい値を入力', decodeURIComponent(value));
if (newValue !== null) {
document.cookie = `${name.trim()}=${encodeURIComponent(newValue)}; expires=Fri, 31 Dec 9999 23:59:59 GMT; path=/`;
renderStorage();
}
};
valueCell.appendChild(valueSpan);
const actionsCell = document.createElement('td');
actionsCell.className = 'storage-actions';
const deleteBtn = document.createElement('button');
deleteBtn.className = 'storage-btn';
deleteBtn.textContent = 'Delete';
deleteBtn.onclick = () => {
document.cookie = `${name.trim()}=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/`;
renderStorage();
};
actionsCell.appendChild(deleteBtn);
row.appendChild(nameCell);
row.appendChild(valueCell);
row.appendChild(actionsCell);
tbody.appendChild(row);
});
}
// 初期表示
renderStorage();
return panel;
}
// コンテキストメニューアクション処理
function handleContextMenuAction(action) {
if (!selectedElement) return;
switch (action) {
case 'edit-html':
// document.documentElement(html要素)は編集不可
if (selectedElement === document.documentElement) {
alert('ルートHTML要素は直接編集できません');
return;
}
startInlineEdit(selectedDOMNode, selectedElement.outerHTML, (newValue) => {
try {
selectedElement.outerHTML = newValue;
refreshElementsPanel();
} catch (e) {
alert('この要素は編集できません: ' + e.message);
}
});
break;
case 'add-attribute':
const attrName = prompt('属性名を入力');
if (attrName) {
const attrValue = prompt('属性値を入力');
selectedElement.setAttribute(attrName, attrValue || '');
refreshElementsPanel();
}
break;
case 'edit-element':
startInlineEdit(selectedDOMNode.querySelector('.dom-tag'), selectedElement.tagName.toLowerCase(), (newValue) => {
const newElement = document.createElement(newValue);
Array.from(selectedElement.attributes).forEach(attr => {
newElement.setAttribute(attr.name, attr.value);
});
newElement.innerHTML = selectedElement.innerHTML;
selectedElement.parentNode.replaceChild(newElement, selectedElement);
selectedElement = newElement;
refreshElementsPanel();
});
break;
case 'duplicate':
const clone = selectedElement.cloneNode(true);
selectedElement.parentNode.insertBefore(clone, selectedElement.nextSibling);
refreshElementsPanel();
break;
case 'remove':
if (confirm('要素を削除しますか?')) {
selectedElement.parentNode.removeChild(selectedElement);
refreshElementsPanel();
}
break;
case 'toggle-visibility':
if (selectedElement.style.display === 'none') {
selectedElement.style.display = '';
} else {
selectedElement.style.display = 'none';
}
refreshElementsPanel();
break;
case 'force-state':
const state = prompt('強制する状態を入力 (例: hover, active, focus)', 'hover');
if (state) {
selectedElement.classList.remove('force-hover', 'force-active', 'force-focus',
'force-focus-within', 'force-focus-visible', 'force-target');
selectedElement.classList.add(`force-${state}`);
refreshElementsPanel();
}
break;
}
}
// インライン編集関数
function startInlineEdit(element, initialValue, callback) {
if (activeEditElement) return;
const originalValue = element.textContent;
const rect = element.getBoundingClientRect();
const input = document.createElement('input');
input.className = 'dom-edit-input';
input.value = initialValue || originalValue;
input.style.position = 'absolute';
input.style.left = `${rect.left}px`;
input.style.top = `${rect.top}px`;
input.style.width = `${rect.width + 20}px`;
document.body.appendChild(input);
input.focus();
input.select();
activeEditElement = {
element: element,
input: input,
callback: callback
};
const clickOutsideHandler = (e) => {
if (!input.contains(e.target)) {
finishInlineEdit();
}
};
input.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
finishInlineEdit();
} else if (e.key === 'Escape') {
cancelInlineEdit();
}
});
setTimeout(() => {
document.addEventListener('click', clickOutsideHandler);
}, 0);
function finishInlineEdit() {
try {
if (input.value !== originalValue && callback) {
callback(input.value);
}
} catch (e) {
alert('編集に失敗しました: ' + e.message);
} finally {
cleanup();
}
}
function cancelInlineEdit() {
cleanup();
}
function cleanup() {
document.removeEventListener('click', clickOutsideHandler);
input.remove();
activeEditElement = null;
}
}
// Consoleパネル作成
function createConsolePanel() {
const panel = document.createElement('div');
panel.className = 'devtools-panel';
panel.id = 'console-panel';
const log = document.createElement('div');
log.id = 'console-log';
const input = document.createElement('input');
input.className = 'console-input';
input.placeholder = 'ここにJavaScriptを入力... (Enterで実行)';
input.onkeypress = (e) => {
if (e.key === 'Enter') {
try {
const result = eval(e.target.value);
if (result !== undefined) {
logMessage('> ' + e.target.value, '#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;
}
// Elementsパネル作成
function createElementsPanel() {
const panel = document.createElement('div');
panel.className = 'devtools-panel';
panel.id = 'elements-panel';
const container = document.createElement('div');
container.className = 'elements-container';
const tree = document.createElement('div');
tree.className = 'dom-tree';
tree.id = 'dom-tree';
const cssPanel = document.createElement('div');
cssPanel.className = 'css-panel';
cssPanel.id = 'css-panel';
container.appendChild(tree);
container.appendChild(cssPanel);
panel.appendChild(container);
// CSSパネル更新関数
function updateCSSPanel(element) {
const cssPanel = document.getElementById('css-panel');
cssPanel.innerHTML = '';
if (!element) return;
if (element.style.length > 0) {
const inlineRule = document.createElement('div');
inlineRule.className = 'css-rule';
const selector = document.createElement('div');
selector.className = 'css-selector';
selector.textContent = 'インラインスタイル';
inlineRule.appendChild(selector);
for (let i = 0; i < element.style.length; i++) {
const propName = element.style[i];
const propValue = element.style[propName];
const propDiv = document.createElement('div');
propDiv.className = 'css-property';
const nameSpan = document.createElement('span');
nameSpan.className = 'css-property-name editable';
nameSpan.textContent = propName;
nameSpan.onclick = () => editCSSProperty(element, propName, 'style');
const valueSpan = document.createElement('span');
valueSpan.className = 'css-property-value editable';
valueSpan.textContent = propValue;
valueSpan.onclick = () => editCSSProperty(element, propName, 'style');
const toggleSpan = document.createElement('span');
toggleSpan.className = 'css-toggle';
toggleSpan.textContent = '×';
toggleSpan.title = 'プロパティを無効化';
toggleSpan.onclick = () => {
element.style[propName] = '';
updateCSSPanel(element);
};
propDiv.appendChild(nameSpan);
propDiv.appendChild(valueSpan);
propDiv.appendChild(toggleSpan);
inlineRule.appendChild(propDiv);
}
cssPanel.appendChild(inlineRule);
}
const computedStyles = window.getComputedStyle(element);
const computedRule = document.createElement('div');
computedRule.className = 'css-rule';
const computedSelector = document.createElement('div');
computedSelector.className = 'css-selector';
computedSelector.textContent = '計算されたスタイル';
computedRule.appendChild(computedSelector);
const importantProps = [
'display', 'position', 'width', 'height', 'margin', 'padding',
'color', 'background', 'border', 'font', 'flex', 'grid'
];
importantProps.forEach(prop => {
const value = computedStyles[prop];
const propDiv = document.createElement('div');
propDiv.className = 'css-property';
const nameSpan = document.createElement('span');
nameSpan.className = 'css-property-name';
nameSpan.textContent = prop;
const valueSpan = document.createElement('span');
valueSpan.className = 'css-property-value';
valueSpan.textContent = value;
propDiv.appendChild(nameSpan);
propDiv.appendChild(valueSpan);
computedRule.appendChild(propDiv);
});
cssPanel.appendChild(computedRule);
}
// DOMツリー構築
function buildDOMTree(node, parentElement, depth = 0) {
if (node.nodeType === Node.ELEMENT_NODE) {
const element = document.createElement('div');
element.className = 'dom-node';
element.style.marginLeft = `${depth * 15}px`;
element.dataset.elementId = node.id || Math.random().toString(36).substr(2, 9);
// 右クリックイベント
element.oncontextmenu = (e) => {
e.preventDefault();
selectedElement = node;
selectedDOMNode = element;
document.querySelectorAll('.dom-node').forEach(el => el.classList.remove('selected'));
element.classList.add('selected');
// HTML要素にはコンテキストメニューを表示しない
if (node !== document.documentElement) {
contextMenu.style.display = 'block';
contextMenu.style.left = `${e.pageX}px`;
contextMenu.style.top = `${e.pageY}px`;
}
updateCSSPanel(node);
};
// タグ名(HTML要素は編集不可)
const tag = document.createElement('span');
tag.className = 'dom-tag';
tag.textContent = `<${node.tagName.toLowerCase()}`;
if (node !== document.documentElement) {
tag.classList.add('editable');
tag.onclick = (e) => {
e.stopPropagation();
startInlineEdit(tag, node.tagName.toLowerCase(), (newValue) => {
const newElement = document.createElement(newValue);
Array.from(node.attributes).forEach(attr => {
newElement.setAttribute(attr.name, attr.value);
});
newElement.innerHTML = node.innerHTML;
node.parentNode.replaceChild(newElement, node);
selectedElement = newElement;
refreshElementsPanel();
});
};
}
element.appendChild(tag);
// 属性(HTML要素は編集不可)
Array.from(node.attributes).forEach(attr => {
const attrSpan = document.createElement('span');
attrSpan.className = 'dom-attr';
attrSpan.textContent = ` ${attr.name}="${attr.value}"`;
if (node !== document.documentElement) {
attrSpan.classList.add('editable');
attrSpan.onclick = (e) => {
e.stopPropagation();
startInlineEdit(attrSpan, attr.value, (newValue) => {
node.setAttribute(attr.name, newValue);
refreshElementsPanel();
});
};
}
element.appendChild(attrSpan);
});
element.appendChild(document.createTextNode('>'));
if (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">&lt;/${node.tagName.toLowerCase()}&gt;</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);
}
}
// CSSプロパティ編集
function editCSSProperty(element, propName, styleType) {
let currentValue = '';
if (styleType === 'style') {
currentValue = element.style[propName];
}
const newValue = prompt(`${propName} の新しい値を入力`, currentValue);
if (newValue !== null) {
if (styleType === 'style') {
element.style[propName] = newValue;
}
updateCSSPanel(element);
refreshElementsPanel();
}
}
// 要素パネル更新
function refreshElementsPanel() {
let tree = document.getElementById('dom-tree');
if (!tree) {
const panel = document.getElementById('elements-panel');
if (panel) {
const container = panel.querySelector('.elements-container');
if (container) {
tree = document.createElement('div');
tree.className = 'dom-tree';
tree.id = 'dom-tree';
container.insertBefore(tree, container.querySelector('.css-panel'));
}
}
}
if (!tree) return;
tree.innerHTML = '';
buildDOMTree(document.documentElement, tree);
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;
}
// Storageパネル作成 (前回と同じなので省略)
// 開発者ツール表示/非表示
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を実行できます');
});
})();