(function() { // グローバル変数宣言 let dtContextMenu = null; let dtSelectedElement = null; let dtSelectedDOMNode = null; let dtActiveEditElement = null; let dtNetworkRequests = []; let dtSelectedRequest = null; let dtObserver = null; let dtRefreshElementsPanel = null; // DOM変更を監視するMutationObserver (最適化版) const setupMutationObserver = () => { if (dtObserver) dtObserver.disconnect(); dtObserver = new MutationObserver(() => { if (dtRefreshElementsPanel) { dtRefreshElementsPanel(); } }); dtObserver.observe(document.documentElement, { childList: true, subtree: true, attributes: false, characterData: false }); }; // スタイルの追加 (すべてのクラスにdt-プレフィックス) 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 = `
一般
URL: ${dtSelectedRequest.url}
メソッド: ${dtSelectedRequest.method}
ステータス: ${dtSelectedRequest.response ? dtSelectedRequest.response.status : '-'}
時間: ${dtSelectedRequest.duration ? Math.round(dtSelectedRequest.duration) + 'ms' : '-'}
`; 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 += `${key}: ${value}
`; }); } else if (dtSelectedRequest.requestHeaders instanceof Headers) { dtSelectedRequest.requestHeaders.forEach((value, key) => { headersContent.innerHTML += `${key}: ${value}
`; }); } 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 += `

${dtSelectedRequest.error.stack.replace(/\n/g, '
')}`; } errorSection.appendChild(errorTitle); errorSection.appendChild(errorContent); detailsPanel.appendChild(errorSection); } } // コンテキストメニュー作成 function createContextMenu() { dtContextMenu = document.createElement('div'); dtContextMenu.className = 'dt-context-menu'; dtContextMenu.innerHTML = `
HTMLとして編集
HTML全体を編集
属性を追加
要素を編集
要素を複製
要素を削除
要素を非表示
状態を強制
`; 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'; } }); } // Storageパネル作成 function createStoragePanel() { const panel = document.createElement('div'); panel.className = 'dt-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 = '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); // SessionStorage表示 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); // Cookie表示 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 = ` Key Value Actions `; 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 = ` Name Value Actions `; 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; } } // Consoleパネル作成 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 'null'; if (output === undefined) return 'undefined'; if (typeof output === 'boolean') return `${output}`; if (typeof output === 'number') return `${output}`; if (typeof output === 'string') return `"${output}"`; if (typeof output === 'function') return `function ${output.name}() { ... }`; if (Array.isArray(output)) return `[${output.map(formatOutput).join(', ')}]`; if (typeof output === 'object') { try { return `${JSON.stringify(output, null, 2) .replace(/"([^"]+)":/g, '"$1":') .replace(/"([^"]+)"/g, '"$1"') .replace(/\b(true|false)\b/g, '$1') .replace(/\b(null)\b/g, '$1') .replace(/\b(\d+)\b/g, '$1')}`; } catch (e) { return `${output.toString()}`; } } return output; } return panel; } // Elementsパネル作成 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); // CSSパネル更新関数 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); } // DOMツリー構築 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); } // タグ名(HTML要素は編集不可) 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); // 属性(HTML要素は編集不可) 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 = `</${node.tagName.toLowerCase()}>`; 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); } } // 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); 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; }); }; // MutationObserverの設定 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() { // 最小限のUIだけ先に表示 createOpenButton(); // メインのツールは遅延読み込み setTimeout(() => { createDevTools(); console.log('開発者ツールが初期化されました'); }, 1000); }); })();