iPhone/iPad用 コンソールエミュレータ。既存のconsole.logをすべて画面にミラーリングします。
Ce script ne doit pas être installé directement. C'est une librairie destinée à être incluse dans d'autres scripts avec la méta-directive // @require https://update.greatest.deepsurf.us/scripts/577436/1822402/Screen%20Display%20Log.js
// ==UserScript==
// @name Screen Display Log
// @namespace http://tampermonkey.net/
// @version 2.0
// @description iPhone/iPad用 コンソールエミュレータ。既存のconsole.logをすべて画面にミラーリングします。
// @author Professional Debugger
// @match *://*/*
// @grant none
// @run-at document-start
// ==/UserScript==
(function() {
'use strict';
class ScreenLogger {
constructor() {
this.id = 'tm-pro-console';
this.maxLogs = 100; // 最大ログ保持数(メモリ対策)
this.container = null;
this.logArea = null;
this.isMinimized = false;
this._init();
}
_init() {
if (document.getElementById(this.id)) return;
this._injectStyles();
this._createUI();
this._hookConsole();
this._hookGlobalErrors();
}
_injectStyles() {
const style = document.createElement('style');
style.textContent = `
#tm-pro-console {
position: fixed; bottom: 0; right: 0; width: 100%; max-height: 40%;
color: #ddd; font-size: 10px;
font-family: 'SF Mono', Menlo, Monaco, Consolas, monospace;
z-index: 2147483647; pointer-events: none; border-top: 1px solid #444;
display: flex; flex-direction: column; transition: height 0.2s; overflow-y: hidden;
}
#tm-pro-console.minimized { height: 30px !important; overflow: hidden; }
#tm-pro-console * { pointer-events: auto; box-sizing: border-box; }
.tm-header {
background: #222; padding: 5px 10px; display: flex;
justify-content: space-between; align-items: center; cursor: pointer;
}
.tm-controls button {
background: #444; color: white; border: none; padding: 2px 8px;
margin-left: 5px; border-radius: 3px; font-size: 10px;
}
#tm-pro-console .tm-log-area { display: block; flex: 1; overflow-y: auto; padding: 5px; -webkit-overflow-scrolling: touch; background: #1e1e1e; }
.tm-line {
border-bottom: 1px solid #333; padding: 3px 0;
white-space: pre-wrap; word-break: break-all; line-height: 1.4;
}
.type-log { color: #00ff00; }
.type-warn { color: #ffaa00; background: rgba(255,170,0,0.2); }
.type-error { color: #ff5555; background: rgba(255,85,85,0.2); }
.type-info { color: #55aaff; }
.log-time { color: #888; font-size: 8px; margin-right: 5px; }
/* ツリー構造のスタイル */
.tm-tree { margin-left: 14px; border-left: 1px dotted #444; }
.tm-summary { list-style: none; cursor: pointer; outline: none; display: flex; align-items: center; color: #55aaff; }
.tm-summary::-webkit-details-marker { display: none; } /* デフォルトの矢印を消す */
.tm-summary::before {
content: '▶'; display: inline-block; width: 12px; font-size: 8px;
transition: transform 0.1s; margin-right: 2px; color: #888;
}
details[open] > .tm-summary::before { transform: rotate(90deg); }
.tm-key { color: #9cdcfe; margin-right: 5px; } /* プロパティ名 */
.tm-val { color: #ce9178; } /* 値(文字列など) */
.tm-tag { color: #569cd6; } /* HTMLタグ名 */
.tm-attr { color: #9cdcfe; } /* HTML属性名 */
.tm-attr-val { color: #ce9178; } /* HTML属性値 */
`;
document.head.appendChild(style);
}
_createUI() {
this.container = document.createElement('div');
this.container.id = this.id;
// ヘッダーの作成
const header = document.createElement('div');
header.className = 'tm-header';
header.id = 'tm-header';
const title = document.createElement('span');
const b = document.createElement('b');
b.textContent = 'DEBUG CONSOLE';
title.appendChild(b);
header.appendChild(title);
// コントロールボタンの作成
const controls = document.createElement('div');
controls.className = 'tm-controls';
const clearBtn = document.createElement('button');
clearBtn.id = 'tm-clear';
clearBtn.textContent = 'Clear';
const toggleBtn = document.createElement('button');
toggleBtn.id = 'tm-toggle';
toggleBtn.textContent = 'Min/Max';
controls.appendChild(clearBtn);
controls.appendChild(toggleBtn);
header.appendChild(controls);
// ログエリアの作成
this.logArea = document.createElement('div');
this.logArea.className = 'tm-log-area';
this.logArea.id = 'tm-log-area';
this.container.appendChild(header);
this.container.appendChild(this.logArea);
document.documentElement.appendChild(this.container);
// イベントリスナー
clearBtn.onclick = () => this.clear();
toggleBtn.onclick = () => this.toggle();
header.onclick = (e) => {
if (e.target.tagName !== 'BUTTON') this.toggle();
};
}
toggle() {
this.isMinimized = !this.isMinimized;
this.container.classList.toggle('minimized', this.isMinimized);
}
clear() {
if (this.logArea) {
this.logArea.replaceChildren();
}
}
/**
* 値を再帰的にDOM要素(ツリー構造)として描画
*/
_renderTree(arg, isRoot = false) {
if (arg === null) return this._createSpan('null', 'tm-val');
if (arg === undefined) return this._createSpan('undefined', 'tm-val');
if (typeof arg !== 'object') return this._createSpan(String(arg), 'tm-val');
const details = document.createElement('details');
const summary = document.createElement('summary');
summary.className = 'tm-summary';
const content = document.createElement('div');
content.className = 'tm-tree';
// --- HTML要素の処理 ---
if (arg instanceof HTMLElement) {
summary.appendChild(this._createSpan('<', ''));
summary.appendChild(this._createSpan(arg.tagName.toLowerCase(), 'tm-tag'));
Array.from(arg.attributes).forEach(a => {
summary.appendChild(this._createSpan(' ', ''));
summary.appendChild(this._createSpan(a.name, 'tm-attr'));
summary.appendChild(this._createSpan('="', ''));
summary.appendChild(this._createSpan(a.value, 'tm-attr-val'));
summary.appendChild(this._createSpan('"', ''));
});
summary.appendChild(this._createSpan('>', ''));
if (arg.childNodes.length > 0) {
arg.childNodes.forEach(child => {
const item = document.createElement('div');
if (child.nodeType === Node.TEXT_NODE) {
const text = child.textContent.trim();
if (text) {
item.textContent = text;
content.appendChild(item);
}
} else {
item.appendChild(this._renderTree(child));
content.appendChild(item);
}
});
const closing = document.createElement('div');
closing.appendChild(this._createSpan('</', ''));
closing.appendChild(this._createSpan(arg.tagName.toLowerCase(), 'tm-tag'));
closing.appendChild(this._createSpan('>', ''));
content.appendChild(closing);
} else {
// 子なし要素は展開させない
const singleTag = summary.cloneNode(true);
return singleTag;
}
}
// --- 配列・オブジェクトの処理 ---
else {
const isArray = Array.isArray(arg);
const keys = Object.keys(arg);
const label = isArray ? `Array(${arg.length})` : `Object { ${keys.slice(0, 2).join(', ')}${keys.length > 2 ? '...' : ''} }`;
summary.textContent = label;
if (keys.length === 0) return this._createSpan(isArray ? '[]' : '{}', 'tm-val');
keys.forEach(key => {
const row = document.createElement('div');
row.style.display = 'flex';
row.appendChild(this._createSpan(`${key}: `, 'tm-key'));
row.appendChild(this._renderTree(arg[key]));
content.appendChild(row);
});
}
details.appendChild(summary);
details.appendChild(content);
return details;
}
_createSpan(text, className) {
const span = document.createElement('span');
span.className = className;
span.textContent = text;
return span;
}
print(type, args) {
const line = document.createElement('div');
line.className = `tm-line type-${type}`;
const threshold = 30;
const isAtBottom = (this.logArea.scrollHeight - this.logArea.scrollTop - this.logArea.clientHeight) <= threshold;
const time = new Date().toLocaleTimeString([], { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit' });
const timeSpan = this._createSpan(time, 'log-time');
line.appendChild(timeSpan);
Array.from(args).forEach(arg => {
const itemContainer = document.createElement('div');
itemContainer.style.display = 'inline-block';
itemContainer.style.verticalAlign = 'top';
itemContainer.style.marginRight = '10px';
itemContainer.appendChild(this._renderTree(arg, true));
line.appendChild(itemContainer);
});
this.logArea.appendChild(line);
if (this.logArea.children.length > this.maxLogs) {
this.logArea.removeChild(this.logArea.firstChild);
}
// ユーザーが一番下を見ていた場合のみ、新しいログを追ってスクロールする
if (isAtBottom && !this.isMinimized) {
this.logArea.scrollTop = this.logArea.scrollHeight;
}
}
_hookConsole() {
const methods = ['log', 'warn', 'error', 'info'];
methods.forEach(method => {
const original = console[method];
console[method] = (...args) => {
this.print(method, args);
original.apply(console, args);
};
});
}
_hookGlobalErrors() {
window.onerror = (msg, url, line, col, error) => {
this.print('error', [`Runtime Error: ${msg}\nAt: ${url}:${line}:${col}`]);
};
window.onunhandledrejection = (event) => {
this.print('error', [`Unhandled Promise Rejection: ${event.reason}`]);
};
}
}
// 実行
if (document.readyState === 'complete' || document.readyState === 'interactive') {
new ScreenLogger();
} else {
window.addEventListener('DOMContentLoaded', () => new ScreenLogger());
}
})();