iPhone/iPad用 コンソールエミュレータ。既存のconsole.logをすべて画面にミラーリングします。
As of
This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @require https://update.greasyfork.org/scripts/577436/1822375/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 = `
#${this.id} {
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;
}
#${this.id}.minimized { height: 30px !important; overflow: hidden; }
#${this.id} * { 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;
}
#${this.id} .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;
this.container.innerHTML = `
<div class="tm-header" id="tm-header">
<span><b>DEBUG CONSOLE</b></span>
<div class="tm-controls">
<button id="tm-clear">Clear</button>
<button id="tm-toggle">Min/Max</button>
</div>
</div>
<div class="tm-log-area" id="tm-log-area"></div>
`;
document.documentElement.appendChild(this.container);
this.logArea = this.container.querySelector('#tm-log-area');
this.container.querySelector('#tm-clear').onclick = () => this.clear();
this.container.querySelector('#tm-toggle').onclick = () => this.toggle();
this.container.querySelector('#tm-header').onclick = (e) => {
if(e.target.tagName !== 'BUTTON') this.toggle();
};
}
toggle() {
this.isMinimized = !this.isMinimized;
this.container.classList.toggle('minimized', this.isMinimized);
}
clear() {
this.logArea.innerHTML = '';
}
/**
* 値を再帰的に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) {
const attrs = Array.from(arg.attributes)
.map(a => ` <span class="tm-attr">${a.name}</span>="<span class="tm-attr-val">${a.value}</span>"`)
.join('');
summary.innerHTML = `<<span class="tm-tag">${arg.tagName.toLowerCase()}</span>${attrs}>`;
// 子要素がある場合のみ中身を生成
if (arg.childNodes.length > 0) {
arg.childNodes.forEach(child => {
const item = document.createElement('div');
if (child.nodeType === Node.TEXT_NODE) {
if (child.textContent.trim()) {
item.textContent = child.textContent;
content.appendChild(item);
}
} else {
item.appendChild(this._renderTree(child));
content.appendChild(item);
}
});
const closing = document.createElement('div');
closing.innerHTML = `</<span class="tm-tag">${arg.tagName.toLowerCase()}</span>>`;
content.appendChild(closing);
} else {
return this._createSpan(summary.innerHTML, 'tm-tag'); // 子なしは展開不要
}
}
// --- 配列・オブジェクトの処理 ---
else {
const isArray = Array.isArray(arg);
const keys = Object.keys(arg);
summary.innerHTML = isArray ? `Array(${arg.length})` : `Object { ${keys.slice(0,2).join(', ')}${keys.length > 2 ? '...' : ''} }`;
if (keys.length === 0) return this._createSpan(isArray ? '[]' : '{}', 'tm-val');
keys.forEach(key => {
const row = document.createElement('div');
row.style.display = 'flex';
const keySpan = this._createSpan(`${key}: `, 'tm-key');
row.appendChild(keySpan);
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}`;
// 下端から30px以内にいれば「最下部にいる」とみなす(遊びを持たせる)
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 = document.createElement('span');
timeSpan.className = 'log-time';
timeSpan.textContent = 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;
}
}
_escapeHtml(str) {
const div = document.createElement('div');
div.textContent = str;
return div.innerHTML;
}
_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());
}
})();