(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 = `
`;
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);
});
})();