tag:blogger.com,1999:blog-2794476860308762522024-03-29T12:29:17.474+09:00バグ取りの日々プログラミング関連の記事を中心に好きなことを書いてるブログです。toshihttp://www.blogger.com/profile/02673765704159584610noreply@blogger.comBlogger129125tag:blogger.com,1999:blog-279447686030876252.post-12540395088374087742024-03-03T09:32:00.004+09:002024-03-27T08:44:35.732+09:00runtime.onStartup を有効無効切り替え時にも呼び出す<section id="toc-1"><h3>はじめに</h3><p>Manifest V3 で拡張機能のバックグラウンド処理は、無期限に生存できなくなりました。</p><p>それに伴い、<code>chrome.runtime.onInstalled</code>/<code>chrome.runtime.onStartup</code>が重要な処理を担うようになりました。ただし、これには問題があります。拡張機能の有効無効切り替えを認識できないことです。</p><p>無効状態の拡張機能が有効状態に遷移した時、<code>chrome.runtime.onStartup</code>は発火しません。この問題を解決する必要があります。</p></section><section id="toc-2"><h3>問題</h3><pre><span class="pre-code-title">background.js</span><code class="language-js:background.js">chrome.runtime.onInstalled.addListener(onInstalled);
chrome.runtime.onStartup.addListener(onStartup);</code></pre><p>拡張機能の有効無効切り替え時に<code>onStartup</code>が動作しません。</p></section><section id="toc-3"><h3>解決策1</h3><pre><span class="pre-code-title">background.js</span><code class="language-js:background.js">chrome.runtime.onInstalled.addListener(onInstalled);
//chrome.runtime.onStartup.addListener(onStartup);
onStartup();
// 備考:Service Worker の復帰毎に処理を実施します</code></pre><p>background.js の起動毎に<code>onStartup</code>処理を実行します。<br>愚かな行為であることは認識していますが、これはある程度有効に機能します。</p></section><section id="toc-4"><h3>解決策2</h3><pre><span class="pre-code-title">background.js</span><code class="language-js:background.js">chrome.runtime.onInstalled.addListener(onInstalled);
chrome.runtime.onStartup.addListener(onStartup);
chrome.management.onEnabled.addListener((info) => {
if (info.id === chrome.runtime.id) {
onStartup();
}
// 備考:起動時にブラウザ内の有効な拡張機能毎に呼び出されます
// もちろん、有効無効切り替え時にも呼び出されます
});</code></pre><p>この方法は、有効無効の切り替えに対して有効に機能しますが、問題を抱えています。</p><p><code>onStartup</code>は、ブラウザ起動時に2回呼び出されます。<code>chrome.runtime.onStartup</code>,<code>chrome.management.onEnabled</code>の両方から<code>onStartup</code>が呼び出されることが原因です。</p><p><code>management</code>権限を必要とします。<code>chrome.management</code>へアクセスするために追加で権限が必要になります。この権限は、ブラウザ上の他の拡張機能の状態を取得する権限です。これは、拡張機能に分不相応な権限を付与することになります。</p><h4>参考</h4><ul><li><a href="https://stackoverflow.com/questions/13979781/chrome-extension-how-to-handle-disable-and-enable-event-from-browser">Chrome Extension : How to handle disable and enable event from browser - Stack Overflow</a></li></ul></section><section id="toc-5"><h3>解決策3</h3><pre><span class="pre-code-title">background.js</span><code class="language-js:background.js">const startup = async () => {
const sessionStorage = await chrome.storage.session?.get({startup:false});
if (!sessionStorage?.startup) {
await chrome.storage.session?.set({startup:true});
await onStartup();
}
// 備考:有効無効時は chrome.runtime.onStartup が呼ばれない対策
// 備考:chrome.storage.session
// 更新時、内容は消える
// 有効無効時、内容は消える
// Service Worker 復帰時、内容は残る
// 対応時期:Chrome 102, Firefox115
};
chrome.runtime.onInstalled.addListener(onInstalled);
//chrome.runtime.onStartup.addListener(onStartup);
startup();</code></pre><p>新機能の<code>chrome.storage.session</code>を使用する方法です。<code>storage</code>権限を必要としますが、一般的にこの権限が問題となることはないはずです。</p><h4>参考</h4><ul><li><a href="https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/storage/session">storage.session - Mozilla | MDN</a></li><li><a href="https://developer.chrome.com/docs/extensions/reference/api/storage">chrome.storage | API | Chrome for Developers</a></li></ul></section><section id="toc-6"><h3>最終案</h3><pre><span class="pre-code-title">background.js</span><code class="language-js:background.js">let startupingPromise = null;
const startuping = async () => {
const sessionStorage = await chrome.storage.session?.get({startup:false});
if (!sessionStorage?.startup) {
await chrome.storage.session?.set({startup:true});
const manifest = chrome.runtime.getManifest();
const localStorage = await chrome.storage.local.get({extension_version:''});
if (manifest.version != localStorage.extension_version) {
await chrome.storage.local.set({extension_version:manifest.version});
await onInstalled();
}
// 備考:無効状態の拡張機能が更新しても chrome.runtime.onInstalled が呼ばれない対策(Chrome 限定?)
// see https://issues.chromium.org/issues/41116832
await onStartup();
}
// 備考:有効無効時は chrome.runtime.onStartup が呼ばれない対策
// 備考:chrome.storage.session
// 更新時、内容は消える
// 有効無効時、内容は消える
// Service Worker 復帰時、内容は残る
// 対応時期:Chrome 102, Firefox115
// 備考:onInstalled, onStartup の実行順・実行タイミングを保証します
// 標準機能では、 onStartup > onInstalled の順で実行されることがあります
// また、並行(非同期)に実行されることがあります
};
const startup = async () => {
if (startupingPromise) {
await startupingPromise;
} else {
startupingPromise = startuping();
await startupingPromise;
//startupingPromise = null;
}
// 備考:並行実行を阻止する。完了を待機する。シングルトン
};
chrome.runtime.onInstalled.addListener(startup);
chrome.runtime.onStartup.addListener(startup);
startup();
// 備考:chrome.runtime.onStartup を呼び出さないと、
// 起動時に background.js が動作しない対策(Firefox 限定?)</code></pre><p>※<code>onInstalled</code>は、バージョンアップ毎に呼び出されます。<br> 更新毎には、呼び出されません。<br> 通常は、問題になりませんがデバッグで問題になることがあります。<br>※次の要素名は変更可能です。<br> <code>chrome.storage.session</code>: <code>{startup}</code><br> <code>chrome.storage.local</code>: <code>{extension_version}</code></p></section><section id="toc-7"><h3>備考</h3><p><code>onStartup</code>は、次の機能の初期化に必要になります。</p><pre><code class="language-js">chrome.action.setPopup({popup});
// 備考:クリックとポップアップ両方に対応する場合、設定が必要になります
chrome.contextMenus.create();
// 備考:起動時は、メニューがないため、作成する必要があります</code></pre></section><section id="toc-8"><h3>備考:無効状態で更新されても更新イベントが発火しない</h3><p>無効状態の拡張機能が更新しても<code>chrome.runtime.onInstalled</code>が呼び出されない。<br>(Chrome 限定?)</p><ul><li><a href="https://issues.chromium.org/issues/41116832">https://issues.chromium.org/issues/41116832</a></li></ul></section><section id="toc-9"><h3>備考:スタートアップキャッシュクリア済みの起動時</h3><p>スタートアップキャッシュクリア済みの起動時は、 Firefox のコンテキストメニューの初期化が必要になる。だが、<code>chrome.runtime.onStartup</code>なしの起動時は、 background.js が呼び出されない。(スタートアップキャッシュクリアなしの Firefox 起動時は、コンテキストメニューがあるため、問題とはならない)</p><ul><li>Profile folder(xxx.default)/startupCache (手動削除)</li><li>[about:support] > [Clear startup cache..]</li></ul></section>toshihttp://www.blogger.com/profile/02673765704159584610noreply@blogger.com0tag:blogger.com,1999:blog-279447686030876252.post-53261621002525043282024-03-01T07:09:00.001+09:002024-03-01T07:09:19.676+09:00JavaScript で paste 内容を書き換える<section id="toc-1"><h3>はじめに</h3><p>テキストフィールド、テキストエリア、編集可能な要素(contenteditable)へのペースト処理でペースト文字列を上書きして別の文字列や一部変更した文字列をペーストする処理を実現します。</p></section><section id="toc-2"><h3>サンプル</h3><pre><code class="language-js">function onPaste(event) {
const data = event.clipboardData || window.clipboardData;
const paste = data.getData('text');
// 上書き文字列の作成(改行文字を空文字に置換する)
const text = paste.replace(/\r?\n/g, '');
if (paste != text) {
if (document.execCommand('insertText', false, text)) {
// 上書きに成功した場合、元々の貼り付けイベントを中断(伝搬阻止)する
event.preventDefault();
event.stopImmediatePropagation();
}
}
};
document.getElementById('input_id').addEventListener('paste', onPaste, true);</code></pre></section><section id="toc-3"><h3>使用案</h3><ul><li>電話番号のハイフンを削除する</li><li>住所の半角数字を全角数字に置き換える</li><li>行超行末のスペースを削除する</li><li>...</li></ul></section><section id="toc-4"><h3>備考</h3><p>Text 形式だけでなく、 HTML 形式で書き込むこともできます。</p><pre><code>document.execCommand('insertText', false, text)
document.execCommand('insertHTML', false, text)</code></pre></section><section id="toc-5"><h3>参考</h3><ul><li><a href="https://developer.mozilla.org/ja/docs/Web/API/Document/execCommand">Document: execCommand() メソッド - Web API | MDN</a></li><li><a href="https://developer.mozilla.org/ja/docs/Web/API/Element/paste_event">Element: paste イベント - Web API | MDN</a></li></ul></section>toshihttp://www.blogger.com/profile/02673765704159584610noreply@blogger.com0tag:blogger.com,1999:blog-279447686030876252.post-35277691110536957362023-02-13T15:54:00.006+09:002023-02-13T15:55:37.064+09:00合計/平均/分散/中央値/etc を計算する【JavaScript】<script type="application/json" id="post-data-json">{
"title": "【JavaScript】合計/平均/分散/中央値/etc を計算する"
,"labels": ["JavaScript", "", ""]
,"url": "https://www.bugbugnow.net/2023/02/calculate-sum-avg-var-median-etc.html"
}</script><div id="toc"><div id="toc-title">目次</div><div id="toc-content"><ol><li><a class="toc-link" href="#toc-1">合計</a></li><li><a class="toc-link" href="#toc-2">平均</a></li><li><a class="toc-link" href="#toc-3">分散</a></li><li><a class="toc-link" href="#toc-4">標準偏差</a></li><li><a class="toc-link" href="#toc-5">最大値</a></li><li><a class="toc-link" href="#toc-6">最小値</a></li><li><a class="toc-link" href="#toc-7">k番目の値</a></li><li><a class="toc-link" href="#toc-8">中央値</a></li><li><a class="toc-link" href="#toc-9">参考</a></li></ol></div></div><section id="toc-1"><h3>合計</h3><h4>1行実装</h4><pre><code class="language-js">const sumValue = array.reduce((pre, cr) => pre+cr);</code></pre><h4>簡易実装</h4><pre><code class="language-js">const sum = (array) => {
const len = array.length;
let sum = 0
for (let i=0; i<len; i++) {
sum = sum + array[i];
}
return sum;
};</code></pre><h4>サンプル</h4><pre><code class="language-js">var array = [1, 3, 5, 7, 9, 8, 6, 4, 2];
var sumValue = array.reduce((pre, cr) => pre+cr);
console.log(sumValue); // 45
console.log(sum(array)); // 45
console.log(sum([2])); // 2
console.log(sum([2, 3])); // 5
console.log(sum([2, 3, 5])); // 10</code></pre></section><section id="toc-2"><h3>平均</h3><h4>1行実装</h4><pre><code class="language-js">const avgValue = array.reduce((pre, cr) => pre+cr) / array.length;</code></pre><h4>簡易実装</h4><pre><code class="language-js">const average = (array) => {
const len = array.length;
if (len === 0) { return 0; }
let sum = 0
for (let i=0; i<len; i++) {
sum = sum + array[i];
}
return sum / len;
};</code></pre><h4>サンプル</h4><pre><code>var array = [1, 3, 5, 7, 9, 8, 6, 4, 2];
var avgValue = array.reduce((pre, cr) => pre+cr) / array.length;
console.log(avgValue); // 5
console.log(average(array)); // 5
console.log(average([2])); // 2
console.log(average([2, 3])); // 2.5
console.log(average([2, 3, 5]));// 3.3333333333333335</code></pre></section><section id="toc-3"><h3>分散</h3><h4>簡易実装</h4><pre><code class="language-js">const variance = (array) => {
const len = array.length;
if (len === 0) { throw new Error('Cannot calculate variance of an empty array.'); }
let sum = 0
for (let i=0; i<len; i++) {
sum = sum + array[i];
}
const avg = sum / len;
let vsum = 0;
for (let i=0; i<len; i++) {
vsum = vsum + (array[i] - avg)**2
}
return vsum / len; // 標本分散
//return vsum / (len-1); // 不偏分散
};</code></pre><h4>サンプル</h4><pre><code class="language-js">console.log(variance([1, 2, 3])); // 0.6666666666666666
console.log(variance([1, 5, 9])); // 10.666666666666666
console.log(variance([9, 7, 5, 3, 1, 2, 4, 6, 8])); // 6.666666666666667
console.log(variance([9, 7, 5, 3, 1, 0, 2, 4, 6, 8])); // 8.25</code></pre></section><section id="toc-4"><h3>標準偏差</h3><h4>簡易実装</h4><pre><code class="language-js">const stdev = (array, normalize) => {
const len = array.length;
if (len === 0) { throw new Error('Cannot calculate standard deviation of an empty array.'); }
let sum = 0
for (let i=0; i<len; i++) {
sum = sum + array[i];
}
const avg = sum / len;
let vsum = 0;
for (let i=0; i<len; i++) {
vsum = vsum + (array[i] - avg)**2
}
return Math.sqrt(vsum / len); // 母集団標準偏差
//return Math.sqrt(vsum / (len-1)); // 標本標準偏差
};</code></pre><h4>サンプル</h4><pre><code class="language-js">console.log(stdev([1, 2, 3])); // 0.816496580927726
console.log(stdev([1, 5, 9])); // 3.265986323710904
console.log(stdev([9, 7, 5, 3, 1, 2, 4, 6, 8])); // 2.581988897471611
console.log(stdev([9, 7, 5, 3, 1, 0, 2, 4, 6, 8])); // 2.8722813232690143</code></pre></section><section id="toc-5"><h3>最大値</h3><h4>1行実装</h4><pre><code class="language-js">const maxValue = array.reduce((pre, cr) => pre > cr ? pre : cr);</code></pre><hr><pre><code class="language-js">const maxValue = Math.max(...array);</code></pre><h4>簡易実装</h4><pre><code class="language-js">const max = function(array) {
const len = array.length;
if (len === 0) { throw new Error('Cannot calculate min of an empty array.'); }
let max = array[0];
for (let i=1; i<len; i++) {
if (max < array[i]) {
max = array[i];
}
}
return max;
};</code></pre><h4>サンプル</h4><pre><code class="language-js">var array = [1, 3, 5, 7, 9, 8, 6, 4, 2];
var maxValue = array.reduce((pre, cr) => pre > cr ? pre : cr);
console.log(maxValue); // 9
console.log(max(array)); // 9</code></pre></section><section id="toc-6"><h3>最小値</h3><h4>1行実装</h4><pre><code class="language-js">const minValue = array.reduce((pre, cr) => pre < cr ? pre : cr);</code></pre><hr><pre><code class="language-js">const minValue = Math.min(...array);</code></pre><h4>簡易実装</h4><pre><code class="language-js">const min = function(array) {
const len = array.length;
if (len === 0) { throw new Error('Cannot calculate max of an empty array.'); }
let min = array[0];
for (let i=1; i<len; i++) {
if (array[i] < min) {
min = array[i];
}
}
return min;
};</code></pre><h4>サンプル</h4><pre><code class="language-js">var array = [9, 7, 5, 3, 1, 2, 4, 6, 8];
var minValue = array.reduce((pre, cr) => pre - cr < 0 ? pre : cr);
console.log(minValue); // 1
console.log(min(array)); // 1</code></pre></section><section id="toc-7"><h3><a id="toc-selectKth"></a>k番目の値</h3><h4>簡易実装</h4><pre><code class="language-js">// see https://blog.teamleadnet.com/2012/07/quick-select-algorithm-find-kth-element.html
const selectKth = function(array, k) {
const len = array.length;
if (k < 0 || len <= k) { throw new Error('k is out of bounds.'); }
let count = 0;
let from = 0;
let to = len - 1;
while (from < to) {
let r = from;
let w = to;
let mid = array[(r+w)/2|0];
while (r < w) {
if (array[r] >= mid) {
const tmp = array[w];
array[w] = array[r];
array[r] = tmp;
w--;
} else {
r++;
}
}
if (array[r] > mid) {
r--;
}
if (k <= r) {
to = r;
} else {
from = r + 1;
}
}
return array[k];
};
const small = function(array, k) { return selectKth(array, k); };
const large = function(array, k) { return selectKth(array, array.length-k-1); };</code></pre><p>※<code>selectKth()</code>は、<code>array</code> を不完全に並び替えます。</p><h4>サンプル</h4><pre><code class="language-js">console.log(small([1, 3, 5, 7, 9, 8, 6, 4, 2], 0)); // 1
console.log(small([1, 3, 5, 7, 9, 8, 6, 4, 2], 1)); // 2
console.log(small([9, 8, 7, 6, 5, 4, 3, 2, 1], 2)); // 3
console.log(small([9, 8, 7, 6, 5, 4, 3, 2, 1], 3)); // 4
console.log(small([9, 7, 5, 3, 1, 2, 4, 6, 8], 4)); // 5
console.log(small([1, 2, 3, 4, 5, 6, 7, 8, 9], 5)); // 6
console.log(small([1, 2, 3, 4, 5, 6, 7, 8, 9], 6)); // 7
console.log(small([9, 7, 5, 3, 1, 2, 4, 6, 8], 7)); // 8
console.log(small([9, 7, 5, 3, 1, 2, 4, 6, 8], 8)); // 9
console.log(large([1, 2, 3, 4, 5, 6, 7, 8, 9], 0)); // 9
console.log(large([1, 2, 3, 4, 5, 6, 7, 8, 9], 1)); // 8
console.log(large([9, 7, 5, 3, 1, 2, 4, 6, 8], 2)); // 7
console.log(large([9, 7, 5, 3, 1, 2, 4, 6, 8], 3)); // 6
console.log(large([9, 7, 5, 3, 1, 2, 4, 6, 8], 4)); // 5
console.log(large([1, 3, 5, 7, 9, 8, 6, 4, 2], 5)); // 4
console.log(large([1, 3, 5, 7, 9, 8, 6, 4, 2], 6)); // 3
console.log(large([9, 8, 7, 6, 5, 4, 3, 2, 1], 7)); // 2
console.log(large([9, 8, 7, 6, 5, 4, 3, 2, 1], 8)); // 1</code></pre></section><section id="toc-8"><h3>中央値</h3><h4>簡易実装(<code>sort()</code>版)</h4><pre><code class="language-js">const median = (array) => {
const len = array.length;
if (len === 0) { throw new Error('Cannot calculate median of an empty array.'); }
array.sort((a, b) => a-b);
if (len % 2) {
return array[(len-1) / 2)];
} else {
const mid = len / 2;
return (array[mid-1] + array[mid]) / 2;
}
};</code></pre><h4>簡易実装(<code>selectKth()</code>版)</h4><pre><code class="language-js">const median = (array) => {
const len = array.length;
if (len === 0) { throw new Error('Cannot calculate median of an empty array.'); }
if (len % 2) {
return selectKth(array, (len-1)/2);
} else {
const mid = len/2 - 1;
const large = selectKth(array, mid+1);
let small = array[mid];
for (let i=0; i<mid; i++) {
if (small < array[i]) {
small = array[i]
}
}
return (small + large) / 2;
}
};</code></pre><p>※完全には並び替えないため、<code>sort()</code>より<code>selectKth()</code>の方が高速です。<br>※<code>selectKth()</code>は、「<a href="#toc-selectKth">k番目の値</a>」参照</p><h4>サンプル</h4><pre><code class="language-js">console.log(median([2, 3])); // 2.5
console.log(median([5, 2, 3])); // 3
console.log(median([9, 7, 5, 3, 1, 2, 4, 6, 8])); // 5
console.log(median([9, 7, 5, 3, 1, 0, 2, 4, 6, 8])); // 4.5</code></pre></section><section id="toc-9"><h3>参考</h3><ul><li><a href="https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce">Array.prototype.reduce() - JavaScript | MDN</a></li><li><a href="https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Math/max">Math.max() - JavaScript | MDN</a></li><li><a href="https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Math/min">Math.min() - JavaScript | MDN</a></li></ul></section>toshihttp://www.blogger.com/profile/02673765704159584610noreply@blogger.com0tag:blogger.com,1999:blog-279447686030876252.post-30684989858994378572023-02-11T21:20:00.007+09:002023-02-16T13:39:14.970+09:00Element から CSS Selector を取得する<script type="application/json" id="post-data-json">{
"title": "Element から CSS Selector を取得する"
,"labels": ["JavaScript", "CSS", ""]
,"url": "https://www.bugbugnow.net/2023/02/get-css-selector.html"
}</script><section id="toc-1"><h3>コード</h3><pre><span class="pre-code-title">getCSSSelector.js</span><code class="language-js:getCSSSelector.js">/**
* Element から CSS Selector を取得する
* @author toshi (https://github.com/k08045kk)
* @license MIT License | https://opensource.org/licenses/MIT
* @version 1
* @since 1 - 20230211 - 初版
* @see https://www.bugbugnow.net/2023/02/get-css-selector.html
* @see https://gist.github.com/k08045kk/d239bec86ae9b06c438e7e2bf67575fb
* @param {Element} element - 要素
* @return {string} - CSS Selector
*/
var getCSSSelector = function(element) {
if (!(element && element instanceof Node)
|| !(element.nodeType === Node.ELEMENT_NODE || (element=element.parentElement))) {
return 'unknown';
}
var array = [];
for (; element; element=element.parentElement) {
if (element.id) {
// #id
array.unshift('#'+element.id)
break;
} else {
// tagName.className:nth-of-type(n)
var tagName = element.tagName;
var text = tagName.toLowerCase();
var list = element.classList;
var len = list.length;
for (var i=0; i<len; i++) { text += '.'+list[i]; }
var n = 0;
for (var pre=element; (pre=pre.previousElementSibling) && (pre.tagName != tagName || ++n); ) {}
var nth = n+1;
if (!n) {
for (var next=element; (next=next.nextElementSibling) && (next.tagName != tagName || !++n); ) {}
}
if (n) {
text += ':nth-of-type('+nth+')';
}
array.unshift(text);
}
}
return array.join(' > ');
};</code></pre><hr><pre><span class="pre-code-title">getCSSSelector.min.js</span><code class="language-js:getCSSSelector.min.js">/*! getCSSSelector.js | MIT License | https://gist.github.com/k08045kk */var getCSSSelector=function(a){if(!(a&&a instanceof Node)||a.nodeType!==Node.ELEMENT_NODE&&!(a=a.parentElement))return"unknown";for(var e=[];a;a=a.parentElement)if(a.id){e.unshift("#"+a.id);break}else{for(var f=a.tagName,g=f.toLowerCase(),b=a.classList,c=b.length,d=0;d<c;d++)g+="."+b[d];b=0;for(c=a;(c=c.previousElementSibling)&&(c.tagName!=f||++b););c=b+1;if(!b)for(d=a;(d=d.nextElementSibling)&&(d.tagName!=f||!++b););b&&(g+=":nth-of-type("+c+")");e.unshift(g)}return e.join(" > ")};</code></pre></section><section id="toc-2"><h3>更新履歴</h3><ul><li>参照:<a href="https://gist.github.com/k08045kk/d239bec86ae9b06c438e7e2bf67575fb">getCSSSelector.js | GitHub Gist</a></li></ul></section><section id="toc-3"><h3>備考</h3><p><code>document.querySelector()</code> で <code>Element</code> を取得可能な <code>CSS Selector</code> を取得します。</p><p>ただ、上記コードが完全なコードではなく突き詰めれば多彩なバリエーションを作成できます。例えば次のような関数が考えられます。</p><ul><li>行数最小の関数</li><li>オプションによる高機能関数</li><li><code>.className</code> を含まない</li><li><code>:nth-of-type(n)</code> を含まない</li><li><code>:nth-child(n)</code> で実装する</li><li>一部の属性を追記する(<code><a></code>の <code>href</code>属性等)</li><li>Element 以外の対策除去(Node or null 対策)</li><li>XML モード対応(tagName の大文字小文字対策)</li></ul><p>上記コードは、「結果の読みやすさ」と「処理の継続性」を考慮して作成しています。「結果の読みやすさ」を無視できるのであれば、<code>.className</code> 部分を削除できます。同様に「処理の継続性」を無視できるのであれば、<code>Node</code>や<code>null</code>などへの対応部分を削除できます。<code>tagName</code>の大文字小文字を考慮すれば、XMLモードに対応することもできます。</p></section><section id="toc-4"><h3>使用例(Bookmarklet)</h3><p>ページ上の要素をクリックすると、 コンソールへ CSS Selector を出力します。</p><pre><span class="pre-code-title">getCSSSelector.bookmarklet.js</span><code class="language-js:getCSSSelector.bookmarklet.js">javascript:/*! included (getCSSSelector.js | MIT License | gist.github.com/k08045kk) */(function(){window.addEventListener("click",function(h){var k=console,l=k.log,a;if((a=h.target)&&a instanceof Node&&(a.nodeType===Node.ELEMENT_NODE||(a=a.parentElement))){for(var e=[];a;a=a.parentElement)if(a.id){e.unshift("#"+a.id);break}else{for(var f=a.tagName,g=f.toLowerCase(),b=a.classList,c=b.length,d=0;d<c;d++)g+="."+b[d];b=0;for(c=a;(c=c.previousElementSibling)&&(c.tagName!=f||++b););c=b+1;if(!b)for(d=a;(d=d.nextElementSibling)&&(d.tagName!=f||!++b););b&&(g+=":nth-of-type("+c+")");e.unshift(g)}a=e.join(" > ")}else a="unknown";l.call(k,a,h.target)})})();</code></pre><details><summary>無圧縮コード</summary><pre><span class="pre-code-title">getCSSSelector.bookmarklet.js</span><code class="language-js:getCSSSelector.bookmarklet.js">/*! included (getCSSSelector.js | MIT License | gist.github.com/k08045kk) */
(function () {
var getCSSSelector = function(element) {
if (!(element && element instanceof Node)
|| !(element.nodeType === Node.ELEMENT_NODE || (element=element.parentElement))) {
return 'unknown';
}
var array = [];
for (; element; element=element.parentElement) {
if (element.id) {
// #id
array.unshift('#'+element.id)
break;
} else {
// tagName.className:nth-of-type(n)
var tagName = element.tagName;
var text = tagName.toLowerCase();
var list = element.classList;
var len = list.length;
for (var i=0; i<len; i++) { text += '.'+list[i]; }
var n = 0;
for (var pre=element; (pre=pre.previousElementSibling) && (pre.tagName != tagName || ++n); ) {}
var nth = n+1;
if (!n) {
for (var next=element; (next=next.nextElementSibling) && (next.tagName != tagName || !++n); ) {}
}
if (n) {
text += ':nth-of-type('+nth+')';
}
array.unshift(text);
}
}
return array.join(' > ');
};
window.addEventListener('click', function(event) {
console.log(getCSSSelector(event.target), event.target);
});
})();</code></pre></details></section><section id="toc-5"><h3>参考</h3><ul><li><a href="https://stackoverflow.com/questions/3620116/get-css-path-from-dom-element">javascript - Get CSS path from Dom element - Stack Overflow</a></li></ul></section>toshihttp://www.blogger.com/profile/02673765704159584610noreply@blogger.com0tag:blogger.com,1999:blog-279447686030876252.post-6323644214485986872022-11-21T13:53:00.006+09:002023-11-27T09:55:08.660+09:00ビニール袋の容量を計算する<script type="application/json" id="post-data-json">{
"title": "ビニール袋の容量を計算する"
,"labels": ["JavaScript", "ウェブアプリ", ""]
,"url": "https://www.bugbugnow.net/2022/11/plastic-bag-capacity.html"
}</script><div><style> #app-form {
margin: 1em 0;
padding: 1em 0;
border: 1px solid #aaa;
background: #F8F8F0;
text-align: center;
}
#app-input-a,
#app-input-b,
.app-input {
width: 8em;
margin: 0 0.5em;
}
#app-input-a:invalid,
#app-input-b:invalid {
border: solid 2px red;
} </style><form id="app-form"> 横幅 a <input id="app-input-a" type="number" min="0" pattern="^[0-9]+$"> mm<br> 高さ b <input id="app-input-b" type="number" min="0" pattern="^[0-9]+$"> mm<br><br><button id="app-button" type="button">計算</button><br><br> 最大容積 V <input class="app-input" id="app-input-V1" type="text" readonly=""> L<br> 適量容積 V <input class="app-input" id="app-input-V2" type="text" readonly=""> L<br> 円柱容積 V <input class="app-input" id="app-input-V3" type="text" readonly=""> L<br></form><script type="none" id="post-body-script">//<![CDATA[
(function() {
const calc = function() {
const pi = 3.14;//Math.PI;
const a = document.getElementById('app-input-a').value / 10; // cm ← mm
const b = document.getElementById('app-input-b').value / 10; // cm ← mm
const V1 = (0.33 * a * b * b) - (0.11 * a * a * a); // 最大容積 cm3
const V2 = a/2 * a/2 * (b - a/2); // 最適容積 cm3
const r = a / pi;
const h = b - r - r;
const V3 = r * r * pi * h; // 円柱容積 cm3
document.getElementById('app-input-V1').value = Math.floor(V1) / 1000; // L ← ml(cm3)
document.getElementById('app-input-V2').value = Math.floor(V2) / 1000; // L ← ml(cm3)
document.getElementById('app-input-V3').value = Math.floor(V3) / 1000; // L ← ml(cm3)
};
document.getElementById('app-button').addEventListener('click', calc);
const keypress = function(event) {
if (event.keyCode === 13) {
calc();
}
return false;
};
document.getElementById('app-input-a').addEventListener('keypress', keypress);
document.getElementById('app-input-b').addEventListener('keypress', keypress);
const clear = function() {
document.getElementById('app-input-V1').value = '';
document.getElementById('app-input-V2').value = '';
document.getElementById('app-input-V3').value = '';
};
document.getElementById('app-input-a').addEventListener('input', clear);
document.getElementById('app-input-b').addEventListener('input', clear);
const focus = function(event) {
event.target.select();
};
document.getElementById('app-input-V1').addEventListener('focus', focus);
document.getElementById('app-input-V2').addEventListener('focus', focus);
document.getElementById('app-input-V3').addEventListener('focus', focus);
})();
//]]></script></div><section id="toc-1"><h3>はじめに</h3><p>ゴミ袋や規格袋の容量を計算します。</p><p>袋には、ゴミ袋のようにリットル数が明記されているものと、規格袋のようにサイズだけが記載されているものがあります。サイズだけが記載されている袋にどの程度の容積があるのか知りたかったため、計算しました。</p></section><section id="toc-2"><h3>早見表:袋の容積</h3><div class="responsive-table"><table><thead><tr><th align="right">袋</th><th align="right">サイズ</th><th align="right">最大容積</th><th align="right">適量容積</th><th align="right">円柱容積</th></tr></thead><tbody><tr><td align="right">規格袋 1号</td><td align="right">70 x 100 mm</td><td align="right">193 ml</td><td align="right">79 ml</td><td align="right">86 ml</td></tr><tr><td align="right">規格袋 2号</td><td align="right">80 x 120 mm</td><td align="right">323 ml</td><td align="right">128 ml</td><td align="right">140 ml</td></tr><tr><td align="right">規格袋 3号</td><td align="right">80 x 150 mm</td><td align="right">537 ml</td><td align="right">176 ml</td><td align="right">201 ml</td></tr><tr><td align="right">規格袋 4号</td><td align="right">90 x 170 mm</td><td align="right">778 ml</td><td align="right">253 ml</td><td align="right">290 ml</td></tr><tr><td align="right">規格袋 5号</td><td align="right">100 x 190 mm</td><td align="right">778 ml</td><td align="right">253 ml</td><td align="right">290 ml</td></tr><tr><td align="right">規格袋 6号</td><td align="right">100 x 210 mm</td><td align="right">1,081 ml</td><td align="right">350 ml</td><td align="right">402 ml</td></tr><tr><td align="right">規格袋 7号</td><td align="right">120 x 230 mm</td><td align="right">1,904 ml</td><td align="right">612 ml</td><td align="right">704 ml</td></tr><tr><td align="right">規格袋 8号</td><td align="right">130 x 250 mm</td><td align="right">2,439 ml</td><td align="right">781 ml</td><td align="right">899 ml</td></tr><tr><td align="right">規格袋 9号</td><td align="right">150 x 250 mm</td><td align="right">2,722 ml</td><td align="right">984 ml</td><td align="right">1,106 ml</td></tr><tr><td align="right">規格袋 10号</td><td align="right">180 x 270 mm</td><td align="right">3.6 L</td><td align="right">1.4 L</td><td align="right">1.6 L</td></tr><tr><td align="right">規格袋 11号</td><td align="right">200 x 300 mm</td><td align="right">5.0 L</td><td align="right">2.0 L</td><td align="right">2.1 L</td></tr><tr><td align="right">規格袋 12号</td><td align="right">230 x 340 mm</td><td align="right">7.4 L</td><td align="right">2.9 L</td><td align="right">3.2 L</td></tr><tr><td align="right">規格袋 13号</td><td align="right">260 x 380 mm</td><td align="right">10 L</td><td align="right">4.2 L</td><td align="right">4.6 L</td></tr><tr><td align="right">規格袋 14号</td><td align="right">280 x 410 mm</td><td align="right">13 L</td><td align="right">5.2 L</td><td align="right">5.7 L</td></tr><tr><td align="right">規格袋 15号</td><td align="right">300 x 450 mm</td><td align="right">17 L</td><td align="right">6.7 L</td><td align="right">7.4 L</td></tr><tr><td align="right">規格袋 16号</td><td align="right">340 x 480 mm</td><td align="right">21 L</td><td align="right">8.9 L</td><td align="right">9.6 L</td></tr><tr><td align="right">規格袋 17号</td><td align="right">360 x 500 mm</td><td align="right">24 L</td><td align="right">10 L</td><td align="right">11 L</td></tr><tr><td align="right">規格袋 18号</td><td align="right">380 x 530 mm</td><td align="right">31 L</td><td align="right">12 L</td><td align="right">14 L</td></tr><tr><td align="right">規格袋 19号</td><td align="right">400 x 550 mm</td><td align="right">32 L</td><td align="right">14 L</td><td align="right">15 L</td></tr><tr><td align="right">規格袋 20号</td><td align="right">460 x 600 mm</td><td align="right">43 L</td><td align="right">19 L</td><td align="right">20 L</td></tr></tbody></table></div><div class="responsive-table"><table><thead><tr><th align="right">袋</th><th align="right">サイズ</th><th align="right">最大容積</th><th align="right">適量容積</th><th align="right">円柱容積</th></tr></thead><tbody><tr><td align="right">ゴミ袋 10L</td><td align="right">400 x 500 mm</td><td align="right">25 L</td><td align="right">12 L</td><td align="right">12 L</td></tr><tr><td align="right">ゴミ袋 15L</td><td align="right">450 x 550 mm</td><td align="right">34 L</td><td align="right">16 L</td><td align="right">16 L</td></tr><tr><td align="right">ゴミ袋 20L</td><td align="right">500 x 600 mm</td><td align="right">45 L</td><td align="right">21 L</td><td align="right">22 L</td></tr><tr><td align="right">ゴミ袋 30L</td><td align="right">500 x 700 mm</td><td align="right">67 L</td><td align="right">28 L</td><td align="right">30 L</td></tr><tr><td align="right">ゴミ袋 45L</td><td align="right">650 x 800 mm</td><td align="right">107 L</td><td align="right">50 L</td><td align="right">51 L</td></tr><tr><td align="right">ゴミ袋 70L</td><td align="right">800 x 900 mm</td><td align="right">157 L</td><td align="right">80 L</td><td align="right">79 L</td></tr><tr><td align="right">ゴミ袋 90L</td><td align="right">900 x 1,000 mm</td><td align="right">216 L</td><td align="right">111 L</td><td align="right">110 L</td></tr><tr><td align="right">ゴミ袋 100L</td><td align="right">1,000 x 1,000 mm</td><td align="right">220 L</td><td align="right">125 L</td><td align="right">115 L</td></tr><tr><td align="right">ゴミ袋 120L</td><td align="right">1,000 x 1,200 mm</td><td align="right">365 L</td><td align="right">175 L</td><td align="right">179 L</td></tr><tr><td align="right">ゴミ袋 150L</td><td align="right">1,300 x 1,200 mm</td><td align="right">376 L</td><td align="right">232 L</td><td align="right">200 L</td></tr></tbody></table></div><p>※ゴミ袋のサイズは、各市町村により異なります。</p></section><section id="toc-3"><h3>計算式</h3><h4>最大容積</h4><pre><code>V = (0.33 × S × b) - (0.11 × a^3)
= (0.33 × a × b × b) - (0.11 × a × a × a)
S: 袋の表面積
a, b: 辺の長さ(b ≧ a)
V: 体積</code></pre><p>※参考:<a href="https://www.sanko-shoji.jp/lecture/cn8/pg128383.html">袋の大きさと適正内容量 of サンコー商事</a><br>※上記の計算機では、 <code>b ≧ a</code> の制約を考慮しません。</p><h4>適量容積</h4><pre><code>V = a/2 × a/2 × (b - a/2)
a, b: 辺の長さ(b ≧ a)
V: 体積</code></pre><p>※参考:<a href="https://www.sanko-shoji.jp/lecture/cn8/pg128383.html">袋の大きさと適正内容量 of サンコー商事</a><br>※上記の計算機では、 <code>b ≧ a</code> の制約を考慮しません。</p><h4>円柱容積</h4><pre><code>r = 2a / 2π
= a / 3.14
h = b - r - r
V = 2r × π × h
= r × r × 3.14 × h
r: 半径
π: 円周率
h: 高さ(袋を閉じる分補正)
a, b: 辺の長さ
V: 体積</code></pre><p>※参考:<a href="https://detail.chiebukuro.yahoo.co.jp/qa/question_detail/q1368950683">ゴミ袋の容積を数学的に求めるにはどうしたらいいでしょうか? - Yahoo!知恵袋</a><br>※円柱容積の公式 <code>体積 = 底面積×高さ = 半径×半径×円周率×高さ</code><br>※円周の公式 <code>円周 = 直径×円周率 = 2×半径×円周率</code><br>※上記の計算機では、 <code>π</code> を <code>3.14</code> で計算します。</p></section><section id="toc-4"><h3>備考:マチ(奥行き)の扱い</h3><p>マチ(奥行き)がある場合、横幅に加算して計算してください。</p><p>例えば、横幅が <code>450 mm</code> でマチが <code>200 mm</code> の場合、 <code>450 + 200 = 600 (mm)</code> を横幅として代用できます。</p></section><section id="toc-5"><h3>備考:持ち手の扱い</h3><p>上記の計算には、持ち手のサイズは考慮されていません。</p><p>袋の高さに持ち手を含む場合、持ち手分の高さを補正する必要があります。</p></section><section id="toc-6"><h3>備考:実測の結果</h3><p>実際に手元にあった規格袋10号(180 x 270 mm)に水を入れて確認したところ、適量容積である <code>1.4 L</code> の水道水をギリギリ入れることができました。</p><img alt="実測結果" loading="lazy" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjXh7XlLeYyN_1eGov3Jd0zV1j9G0UmSjuQja58fALRww21No3Dqk94MxR-KVJMWDYjYTxzADBMZPYxKibu99ofTlF6XavMBqTPh89DKHYqUR7dBgH4dGZsWlmpk-2sZjTuCcJNY0HL7KHyfS8y_kbOnVkLJO_zZOekRACwe94c0qIWIMtDqMzbSssHSQ/s632/20221121.jpg" width="600" height="632" decoding="async"><p>この結果から、ある程度妥当な計算結果であることを確認しました。ただし、サンプル数があまりに少ないことは否めません。</p><p>ちなみに、円柱容積の <code>1.6 L</code> を入れることもできましたが口を縛るのが極めて難しくなります。最大容積の <code>3.6 L</code> に至ってはどのように入れるのかわかりません。口を縛らない前提で入るだけ入れてみましたが <code>2.5 L</code> 程度が限界のようです。</p><p>※45Lなど袋で実測する場合、強度(袋の厚み)の問題に留意してください。</p></section>toshihttp://www.blogger.com/profile/02673765704159584610noreply@blogger.com0tag:blogger.com,1999:blog-279447686030876252.post-73841777661358875292022-10-10T16:38:00.005+09:002022-10-10T23:32:39.824+09:00canvas を画面サイズに合わせる方法<script type="application/json" id="post-data-json">{
"title": "canvas を画面サイズに合わせる方法"
,"labels": ["JavaScript", "HTML", "CSS"]
,"url": "https://www.bugbugnow.net/2022/10/fit-canvas-screen-size.html"
}</script><p>キャンバス要素を画面全体に表示します。</p><section id="toc-1"><h3>結論</h3><pre><span class="pre-code-title">sample-fit-canvas-screen-size.html</span><code class="language-html:sample-fit-canvas-screen-size.html"><!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<style>
html, body, #canvas {
display: block;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
const onRepaint = function() {
console.log('repaint');
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d');
context.fillStyle = 'red';
context.fillRect(0, 0, canvas.width, canvas.height);
context.fillStyle = 'blue';
context.fillRect(10, 10, canvas.width-20, canvas.height-20);
};
const onResize = function() {
console.log('resize');
const canvas = document.getElementById('canvas');
console.log(canvas.height, canvas.clientHeight, window.innerHeight);
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
onRepaint();
};
window.addEventListener('DOMContentLoaded', onResize);
window.addEventListener('resize', onResize);
</script>
</body>
</html></code></pre><p><a href="https://cdn.bugbugnow.net/blog/post/2022/sample-fit-canvas-screen-size.html">サンプルページ</a></p></section><section id="toc-2"><h3>スマホ用にビューポートを設定する</h3><pre><code class="language-html"><meta name="viewport" content="width=device-width,initial-scale=1"/></code></pre><p>スマホ環境でビューポートを端末サイズに合わせます。</p></section><section id="toc-3"><h3>要素を画面サイズに合わせる</h3><pre><code class="language-css">html, body, #canvas {
display: block;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}</code></pre><p>要素(<code><canvas id="canvas"></code>)を画面サイズに合わせます。<br>具体的には、次の5点を変更します。</p><ul><li><code><html></code>を画面サイズまで拡張する<ul><li><code>height: 100%;</code>で高さを拡張する</li></ul></li><li><code><body></code>の余白を削除する<ul><li><code>margin: 0;</code>で不要な余白を削除する</li></ul></li><li><code><body></code>を<code><html></code>まで拡張する<ul><li><code>height: 100%;</code>で高さを拡張する</li></ul></li><li><code><canvas></code>を<code><body></code>まで拡張する<ul><li><code>width: 100%;</code>で横幅を拡張する</li><li><code>height: 100%;</code>で高さを拡張する</li></ul></li><li><code><canvas></code>をブロック要素として表示する<ul><li><code>display: block;</code>でブロック要素として表示する<ul><li><code><canvas></code> の初期設定はインライン要素です</li></ul></li></ul></li></ul><h4>備考(<code>100vh</code> を使う)</h4><pre><code class="language-css">body {
height: 100vh;
margin: 0;
}
#canvas {
display: block;
width: 100%;
height: 100%;
}</code></pre><p><code>body { height: 100vh }</code> を設定する方法もあります。ただし、<code>100%</code>と<code>100vh</code>には、違いがあります。</p><p><code>100vh</code> の最大の問題は、スマホ環境のアドレスバーがある場合、アドレスバー分の領域がはみ出すことです。<code>100vh</code>は画面の高さであるため、アドレスバーの領域も含めて計算されるからです。<code>100vh</code>は画面のコンテンツ表示領域ではありません。画面のビューポートの高さなのです。他にもスマホ環境のキーボード周りの問題もあります。</p><h4>別解(キャンバスを浮かせる)</h4><pre><code class="language-css">#canvas {
display: block;
position: fixed;
top: 0;
left: 0;
z-index: 999;
width: 100%;
height: 100%;
margin: 0;
padding: 0;
/*opacity: 0.5;*/
}</code></pre><p><code><canvas id="canvas"></code> を他要素の前面に配置して、画面サイズまで拡張します。</p><p>「ページに別要素を表示している状態で、一時的にキャンバスでページを覆い尽くす」などの用途に利用できます。</p></section><section id="toc-4"><h3><code><canvas></code>の表示サイズとキャンバスサイズを一致させる</h3><p><code><canvas></code>には、2つのサイズがあります。「HTML要素として表示サイズ」と「キャンバスのサイズ」です。具体的には、要素の <code>clientWidth / clientHeight</code> (表示サイズ)と <code>width / height</code> (キャンバスサイズ)です。</p><p>表示サイズとキャンバスサイズは、常に一致しているわけではありません。表示サイズは、CSS的な要素サイズの変更によって常に変化します。キャンバスサイズは、明示的なサイズ指定を行うことで変更することができます。表示サイズとキャンバスサイズが異なる場合、ボヤケたような不自然な表示になります。</p><pre><code class="language-js">const canvas = document.getElementById('canvas');
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;</code></pre><h4>備考(<code>window.innerHeight</code> を使う)</h4><pre><code class="language-js">const canvas = document.getElementById('canvas');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;</code></pre><p><code>window.innerHeight</code> (ウィンドウの高さ)を使用します。ただし、次の問題があります。</p><p><code><canvas></code> のキャンバスサイズに<code>window.innerHeight</code> (ウィンドウの高さ)を指定しても、<code><canvas></code>の表示サイズがウィンドウの高さと異なる場合、表示サイズとキャンバスサイズの不一致により表示の歪みが発生します。キャンバスサイズをウィンドウの高さに設定しても、<code><canvas></code>の表示サイズが連動して変更されるわけではない点を考慮してください。</p></section><section id="toc-5"><h3>ブラウザのサイズ変更を監視する</h3><pre><code class="language-js">window.addEventListener('resize', onResize);</code></pre><p>ブラウザのサイズ変更を考慮して、<code>resize</code> イベントを監視し、表示サイズの変更をキャンバスサイズに随時再設定します。</p></section><section id="toc-6"><h3>まとめ</h3><ol><li>ビューポートを適切に設定する</li><li>要素の表示サイズを画面サイズに合わせる</li><li>キャンバスサイズを表示サイズに設定する</li><li>表示サイズの変更をキャンバスサイズに適時再設定する</li></ol></section>toshihttp://www.blogger.com/profile/02673765704159584610noreply@blogger.com0tag:blogger.com,1999:blog-279447686030876252.post-53509418666739721602022-01-22T11:46:00.007+09:002024-03-26T10:33:22.722+09:00beforeunload のダイアログが出現しないことがある<section id="toc-1"><h3>beforeunload とは?</h3><p><code>beforeunload</code>イベントは、ページがアンロードされる直前に発生します。<code>beforeunload</code>イベントが発生時は、ページがまだ表示されており、イベントもキャンセル可能です。</p><p><code>beforeunload</code>イベントは、ページにダイアログを表示し、ユーザーにページを閉じるか確認を求めることができます。ユーザーが確認すれば、ブラウザは新しいページへ遷移し、そうでなければ遷移をキャンセルします。(ユーザーの確認なしにページ遷移をキャンセルすることはできません)</p></section><section id="toc-2"><h3>サンプル(確認ダイアログを表示する)</h3><pre><span class="pre-code-title">beforeunload.html</span><code class="language-html:beforeunload.html"><script>
window.addEventListener('beforeunload', function(event) {
event.preventDefault();
event.returnValue = '';
});
</script></code></pre><p>※ HTML の仕様では、<code>event.preventDefault();</code>を使用する必要があります。<br> ただし、 Chrome では<code>event.returnValue = '';</code>を設定する必要があります。<br> そのため、 Chrome系とそれ以外に対応するため、両方の記載を併記しています。<br>※<code>returnValue</code>に空文字以外の値を設定するとユーザー確認に使用されます。<br> ただし、これは歴史的な機能であり現在は値を設定しても定型文が使用されます。<br> <a href="https://developers.google.com/web/updates/2016/04/chrome-51-deprecations?hl=en#remove_custom_messages_in_onbeforeunload_dialogs">Remove Custom Messages in onbeforeload Dialogs after Chrome 51</a></p><h4>ダイアログの表示例</h4><pre><code>Chrome
「このサイトを離れますか?」
「行った変更が保存されない可能性があります。」
「このページを離れる」「キャンセル」</code></pre><hr><pre><code>Firefox
「(host)」
「このページから移動しますか? 入力した情報は保存されません。」
「このページから移動する」「このページに留まる」</code></pre></section><section id="toc-3"><h3>ダイアログが出現しないことがある</h3><p>上記のサンプルを使用してもダイアログが出現しないことがあります。</p><h4>JavaScript が無効になっているため</h4><p><code>beforeunload</code>イベントの登録には JavaScript の実行が必要です。そのため、 JavaScript 無効環境ではダイアログが出現しません。</p><p>また、下記のようにタグに <code>onbeforeunload</code> を記述する方法でも内部的には JavaScript が使用されているため、 JavaScript 無効環境ではダイアログが出現しません。</p><pre><code><body onbeforeunload="return 'TEST'"></code></pre><h4>ブラウザが対応していないため</h4><p>Safari on iOS / WebView Android などの一部のブラウザは、<code>beforeunload</code>イベントのダイアログ表示(<code>event.preventDefault()</code> / <code>event.returnValue</code>)に対応していません。そのため、ダイアログ出現させることができません。</p><hr><p>また、 Firefox の about:config 設定で次の変更がある場合、<code>beforeunload</code>イベントのダイアログ表示が無効化されます。ただし、この設定は初期値でダイアログ表示が有効となっています。</p><pre><code>dom.disable_beforeunload
false → true</code></pre><h4>ページとユーザーの対話が存在しないため</h4><p>ページを表示後にユーザーとの対話が存在しない場合(ジェスチャ操作が存在しない場合)、ダイアログは出現しません。元々、ページに入力した情報を保護する機能であるため、ユーザーによる入力が存在しないのであれば、保護する必要もありません。</p><p>ユーザーとの対話は、ユーザーイベントだと考えられます。FID(First Input Delay:初回ユーザーイベント)などでは、<code>click / mousedown / keydown / touchstart / pointerdown</code>をユーザーイベントとして扱います。初回ユーザーイベントが発生していない状態ではユーザーとの対話がないものと考えられます。</p><p><code>Element.click()</code>等で JavaScript から意図的にイベントを発生させたとしてもそれは、ユーザーイベントとしてはカウントされない点に注意してください。また、ユーザーイベントに <code>scroll / mousemove</code> を含まない点も考慮してください。</p><p>そしてまた、<code>window.alert()</code> / <code>window.confirm()</code> / <code>window.prompt()</code> ダイアログへのユーザー入力と確認も考慮されません。<code>window.prompt()</code>でデータを入力しても他のユーザー操作がなしであればダイアログは出現しません。(これは仕様バグなのでは?)</p><hr><p>ユーザーイベントがある場合、ダイアログを表示しますが、ユーザーイベントがない場合、ダイアログを表示せず、デベロッパーツールに次のエラーを出力します。デベロッパーツールにエラーを出力したくない場合、ユーザーイベント発生後に<code>beforeunload</code>を登録する方法などが考えられます。</p><pre><code>Chrome
[Intervention] Blocked attempt to show a 'beforeunload' confirmation panel for a frame that never had a user gesture since its load. https://www.chromestatus.com/feature/5082396709879808</code></pre><aside class="ads-inarticle"><ins class="adsbygoogle ads-ad ads-pending"></ins></aside></section><section id="toc-4"><h3>意図的にダイアログを出現させない</h3><p><code>beforeunload</code>イベントのダイアログは、ユーザーにとって常に有益とは限りません。この機能は、ときに邪魔に思うユーザーがいるかもしれません。次のユーザースクリプトで問題を解決できるかもしれません。</p><pre><code class="language-js">// 他の beforeunload イベントへのイベント伝搬を阻害します
// このコードより早く window のキャプチャフェーズへイベント登録された場合、効果がありません
window.addEventListener('beforeunload', function(event) {
event.stopImmediatePropagation();
}, true);
// 既に登録済みの window.onbeforeunload を除去します
window.onbeforeunload = function(event) {};</code></pre><p>※上記のコードを常に使用することはおすすめできません。<br> 下記の「備考(キャッシュの不使用)」のため、ブラウザ体験が劣化します。<br> 導入するのであれば、特定のページ(サイト)に限定すべきです。</p></section><section id="toc-5"><h3>備考(キャッシュの不使用)</h3><p><code>beforeunload / unload</code>イベントが設定されている場合、ブラウザは bfcache (Back Forward Cache) or WebKit のページキャッシュを使用しなくなります。</p><p>キャッシュの使用により、ユーザー体験が向上します。不必要に <code>beforeunload / unload</code> を使用せずに <code>pagehide</code> の使用を検討してください。</p></section><section id="toc-6"><h3>備考(beforeunload 内では、ダイアログを表示できない)</h3><p><code>beforeunload</code>イベント中は、<code>window.alert()</code> / <code>window.confirm()</code> / <code>window.prompt()</code>などのメソッドが無視(ダイアログを表示しない)されます。</p></section><section id="toc-7"><h3>参考</h3><ul><li><a href="https://developer.mozilla.org/ja/docs/Web/API/Window/beforeunload_event">Window: beforeunload イベント - Web API | MDN</a></li><li><a href="https://developer.mozilla.org/ja/docs/Mozilla/Firefox/Releases/1.5/Using_Firefox_1.5_caching">Using Firefox 1.5 caching - Mozilla | MDN</a></li><li><a href="https://webkit.org/blog/516/webkit-page-cache-ii-the-unload-event/">WebKitページキャッシュII–アンロードイベント| WebKit</a></li></ul></section>toshihttp://www.blogger.com/profile/02673765704159584610noreply@blogger.com0tag:blogger.com,1999:blog-279447686030876252.post-73455772014782428162022-01-19T18:21:00.004+09:002023-02-23T07:51:10.300+09:00JavaScript でルートドメイン(root domain)を取得する<script type="application/json" id="post-data-json">{
"title": "JavaScript でルートドメイン(root domain)を取得する"
,"labels": ["JavaScript", "DNS", ""]
,"url": "https://www.bugbugnow.net/2022/01/get-root-domain.html"
}</script><div><style> #app-form {
margin: 1em 0;
padding: 1em 0;
border: 1px solid #aaa;
background: #F8F8F0;
text-align: center;
}
#app-input,
#app-output {
width: 400px;
} </style><form id="app-form"><p><input type="text" id="app-input" placeholder="入力(例:https://www.bugbugnow.net/)"></p><p><button id="app-encode" type="button">変換</button></p><p><input type="text" id="app-output" placeholder="出力(例:bugbugnow.net)" readonly=""></p></form><script defer="" src="https://cdn.bugbugnow.net/lib/psl.min.js"></script><script type="none" id="post-body-script">const GetRootDomain = function(url) {
if (!psl) { return 'Error: Failed to load library.'; }
let domain = url;
try {
domain = new URL(url).hostname;
// Note: `ttp://example.com/` で空文字を返す
} catch (e) {}
try {
if (domain.length) {
return psl.parse(domain).domain || 'Error';
}
} catch (e) {}
return 'Error';
};
document.getElementById('app-encode').addEventListener('click', function() {
const input = document.getElementById('app-input');
const output = document.getElementById('app-output');
output.value = GetRootDomain(input.value);
});</script></div><section id="toc-1"><h3>はじめに</h3><p>URLからルートドメインを取得する処理について考えます。</p><p>結果から言えば、正確な結果が必要ならば、素直にライブラリを使いましょう。</p></section><section id="toc-2"><h3>ルートドメインとは</h3><p>ここで言う「ルートドメイン」とは、サブドメイン名やホスト名を含まない一般的に個人や組織が取得できるドメイン名のことを指します。</p><p>例として、「<code>www.example.co.jp</code>」であれば、「<code>example.co.jp</code>」のことを指します。</p><p>※「独自ドメイン名」「カスタムドメイン名」「ドメイン名」と呼ばれることもあります。<br> 「ルートドメイン」は、ドメイン名の一番右側のドットです。一般的に省略されます。<br> そのため、ルートドメインは正確には誤用です。</p></section><section id="toc-3"><h3>サブドメインを含むドメイン名の取得処理</h3><p>サブドメインを含むドメイン名は、次のように簡単に取得することができます。</p><pre><code class="language-js">console.log(document.domain);
console.log(window.location.hostname);
console.log(new URL('https://www.bugbugnow.net/').hostname);
// www.bugbugnow.net</code></pre></section><section id="toc-4"><h3>簡易なルートドメインの取得処理</h3><p>安易な考えとして、「<code>.com</code>」「<code>.jp</code>」「<code>.co.jp</code>」などのトップレベルドメイン(TLD)を考慮すれば、容易にルートドメインを取得できるように思えます。</p><p>簡易な実装として次のようなものが考えられます。</p><pre><code class="language-js">function GetDomain(url) {
const hostname = new URL(url).hostname;
const level = hostname.split('.');
if (level[level.length-1] == '') { level.pop(); }
let domain;
const len = level.length;
if (len > 2 && level[len-2].length == 2 && level[len-1].length == 2) {
domain = level[len-3] + '.' + level[len-2] + '.' + level[len-1];
} else if (len > 1) {
domain = level[len-2] + '.' + level[len-1];
} else {
domain = level.join('.');
}
return domain;
}</code></pre><p>ただし、この処理では多くの問題を含みます。</p><p>※「<code>.co.jp</code>」は、正確にはセカンドレベルドメインです。</p></section><section id="toc-5"><h3>簡易な処理の問題点</h3><p>上記の処理の問題点は、次のパターンが漏れている点です。(他にも多く漏れています)</p><pre><code>https://www.pref.aichi.jp/
https://www.city.iwakura.aichi.jp/</code></pre><p>上記の <code>GetDomain()</code> では、共に「<code>aichi.jp</code>」を取得しますが、正しくは、「<code>pref.aichi.jp</code>」「<code>city.iwakura.aichi.jp</code>」を取得しなければなりません。これは、「都道府県型JPドメイン名」と「地域型JPドメイン名」と呼ばれるドメイン名です。</p><p>このURLは、「愛知県」「愛知県岩倉市」のホームページです。日本の公共的なURLを正確に判定できないのでは、簡易実装と言えど問題があります。</p></section><section id="toc-6"><h3>正確に取得する</h3><p>正確に取得する方法として、「<code>.com</code>」「<code>.jp</code>」「<code>.co.jp</code>」などのサフィックスをすべて保持しておく方法があります。</p><p>すべてのサフィックスリストは、 <a href="https://publicsuffix.org/">Public Suffix List</a> にあります。</p><p>すべてのサフィックスリストを保持することで正確なドメイン名を解釈できます。このため、正確なルートドメインを取得したい場合、ライブラリ等の利用を推奨します。</p></section><section id="toc-7"><h3>備考</h3><p>上記変換機には、次のライブラリを使用しています。</p><ul><li><a href="https://github.com/lupomontero/psl">lupomontero/psl - GitHub</a></li></ul><h4>試験用</h4><pre><code>入力
https://www.bugbugnow.net/
jprs.jp.
ttp://jprs.jp./
ttps://jprs.jp/
ttps://jprs.jp.
www.google.com
http://www.google.co.jp/
https://www.pref.aichi.jp/
https://www.city.iwakura.aichi.jp/
https://日本語.jp/
https://xn--wgv71a119e.jp/
https://はじめよう.みんな/</code></pre><hr><pre><code>出力
bugbugnow.net
jprs.jp
google.com
google.co.jp
pref.aichi.jp
city.iwakura.aichi.jp
xn--wgv71a119e.jp
xn--wgv71a119e.jp
xn--p8j9a0d9c9a.xn--q9jyb4c</code></pre><h4>備考(JPドメイン名)</h4><p>JPドメイン名には、次のようなドメインが存在します。</p><ul><li>「汎用JPドメイン名」(例:<code>example.jp</code>)</li><li>「属性型JPドメイン名」(例:<code>example.co.jp</code>)</li><li>「都道府県型JPドメイン名」(例:<code>example.aichi.jp</code>)</li><li>「地域型JPドメイン名」(例:<code>example.iwakura.aichi.jp</code>)</li></ul><p>※地域型JPドメイン名の新規登録は、2012年3月末で停止しています。<br>※参考:<a href="https://jprs.jp/about/jp-dom/prefecture.html">都道府県型JPドメイン名について | JPドメイン名について | JPRS</a></p><h4>備考(Punycode)</h4><p>Punycode とは、国際化ドメイン名で使用される文字符号化方式です。</p><p>ピリオドで区切られたドメイン名の各階層毎に、プレフィックスとして「<code>xn--</code>」を使用してエンコードされます。</p><ul><li><a href="https://ja.wikipedia.org/wiki/Punycode">Punycode - Wikipedia</a></li></ul></section>toshihttp://www.blogger.com/profile/02673765704159584610noreply@blogger.com3tag:blogger.com,1999:blog-279447686030876252.post-81256832133817290392022-01-18T11:11:00.003+09:002022-01-18T11:20:54.699+09:00Web SQL Database に関する覚書<script type="application/json" id="post-data-json">{
"title": "Web SQL Database に関する覚書"
,"labels": ["JavaScript", "UserScript", ""]
,"url": "https://www.bugbugnow.net/2022/01/web-sql-database.html"
}</script><section id="toc-1"><h3>Web SQL Database とは</h3><p>Web SQL Database とは、Cookie や Web Storage の用にデータをブラウザ側に保存する仕組みのひとつです。仕様書は、次の場所で公開されています。</p><ul><li><a href="https://www.w3.org/TR/webdatabase/">https://www.w3.org/TR/webdatabase/</a></li></ul><p>ただし、既に事実上の廃止されており、今後実装されることはまずありえません。ですが、次のブラウザで既に実装されており、今現在(2022年)動作させることができます。</p><ul><li>Chrome</li><li>Edge</li><li>Safari</li></ul><p>※他にもChrominum系のブラウザで使用できます。<br>※Web SQL Database の代わりに Indexed Database API の利用が推奨されています。</p><h4>備考</h4><ul><li><a href="https://www.tohoho-web.com/html5/web_sql_db.html">HTML5 - Web SQLデータベース - とほほのWWW入門</a></li><li><a href="https://docs.microsoft.com/ja-jp/microsoft-edge/devtools-guide-chromium/storage/websql">Web データの表示SQLする - Microsoft Edge Development | Microsoft Docs</a></li></ul></section><section id="toc-2"><h3>Web SQL Database の問題点</h3><p>Web SQL Database は、事実上廃止されており、既に殆ど利用されていません。ですが、一部ブラウザでは動作を継続しています。</p><p>Web SQL Database は、データを無期限に保存します。有効期限がありません。また、一度作成されてしまうとJavaScript経由での削除ができません。これの最大の問題は、拡張機能などで自動的に古いデータを削除できないことです。ユーザーにほとんど認知されないまま、データだけが残り続けることが考えられます。</p></section><section id="toc-3"><h3>Web SQL Database を無効化する(UserScrpt)</h3><pre><span class="pre-code-title">DisableWebSQLDatabase.user.js</span><code class="language-js:DisableWebSQLDatabase.user.js">// ==UserScript==
// @name Disable Web SQL Database
// @description Disable the Web SQL Database (Old Web Database).
// @include *
// @namespace https://www.bugbugnow.net/
// @author toshi (https://github.com/k08045kk)
// @license MIT License | https://opensource.org/licenses/MIT
// @version 1.0
// @since 1.0 - 20220117 - 初版
// @run-at document-start
// @grant none
// ==/UserScript==
window.openDatabase = null;
// Note: UserScrpt の実行速度では、完全に拒否することはできません。
// 完全に拒否するためには、拡張機能で無遅延に実行する必要があります。
// Note: 拡張機能などページ以外からの Web SQL Database 使用には対応していません。</code></pre></section><section id="toc-4"><h3>Web SQL Database を削除する</h3><p>Web SQL Database のデータベースを JavaScript 経由で削除することはできません。ですが、次の方法で削除することができます。</p><h4>設定から削除する</h4><pre><code>Chrome > 設定 > セキュリティとプライバシー > Cookieと他のサイトデータ > すべてのCookieとサイトデータを表示
chrome://settings/siteData
Edge > 設定 > Cookieとサイトのアクセス許可 > Cookie とサイト データの管理と削除 > すべてのCookieとサイトデータを表示する
edge://settings/siteData</code></pre><p>データベースストレージとして表示される。削除ボタンから削除できます。</p><p>※データベースストレージは、Web SQL Database だけでなく、IndexedDB も同名で表示します。</p><h4>開発ツールから削除する</h4><pre><code>Chrome > 開発ツール > Application > Storage
Edge > 開発ツール > アプリケーション > Storage</code></pre><p>サイトデータのクリアから削除できます。</p><h4>プロファイルフォルダから削除する</h4><p>ブラウザのプロファイルフォルダから削除することで削除できます。</p><pre><code>Chrome (Windows10)
C:\Users\(ユーザー名)\AppData\Local\Google\Chrome\User Data\(プロファイル名)\databases
Edge (Windows10)
C:\Users\(ユーザー名)\AppData\Local\Microsoft\Edge\User Data\(プロファイル名)\databases</code></pre><p>※プロファイル名は、<code>Default</code>, <code>Profile n</code>が標準で使用されます。</p></section>toshihttp://www.blogger.com/profile/02673765704159584610noreply@blogger.com0tag:blogger.com,1999:blog-279447686030876252.post-80516962137087147002021-12-15T20:31:00.006+09:002023-08-23T01:38:29.507+09:00HTMLを簡易に解析(tokenize / parse)する<div id="toc"><div id="toc-title">目次</div><div id="toc-content"><ol><li><a class="toc-link" href="#toc-1">はじめに</a></li><li><a class="toc-link" href="#toc-2">DOMParser を使用する</a></li><li><a class="toc-link" href="#toc-3">HTMLから文字列を抽出する</a></li><li><a class="toc-link" href="#toc-4">正規表現を使用して、簡易にタグとタグ以外を分解する</a></li><li><a class="toc-link" href="#toc-5">正規表現を使用して、もう少し考えて分解する</a></li><li><a class="toc-link" href="#toc-6">HTMLのドキュメントツリーを簡易に作成する</a></li></ol></div></div><section id="toc-1"><h3>はじめに</h3><p>JavaScript で HTML を簡易に解析(字句解析・構文解析)します。</p><p>巨大なライブラリを使用せずに解析する方法を考えます。主にウェブページのスクレイピングを前提としています。処理速度は、考慮しません。コード量を重視して機能を実現します。</p><p>※次のようなライブラリが使用できる場合、そちらを使用したほうが懸命です。<br> 「jsdoc」「cheerio」「libxmljs」<br> 「chromy」「puppeteer」「Nightmare」「PhantomJS」</p></section><section id="toc-2"><h3>DOMParser を使用する</h3><pre><span class="pre-code-title">サンプル</span><code class="language-js:サンプル">var html = '<html><body><h1 id="header">〇〇ページ</h1><p>〇〇は、<span class="red">△△</span>です。</p></body></html>';
var parser = new DOMParser();
var doc = parser.parseFromString(html, 'text/html');
console.log(doc.getElementById('header').textContent);
// 〇〇ページ</code></pre><p>※ブラウザ環境であれば、標準の DOMParser が使用できます。<br> <a href="https://developer.mozilla.org/ja/docs/Web/API/DOMParser">DOMParser - Web API | MDN</a></p></section><section id="toc-3"><h3>HTMLから文字列を抽出する</h3><pre><span class="pre-code-title">findText.js</span><code class="language-js:findText.js">/**
* 文字列を抽出する
* 開始文字列と終了文字列に囲まれた文字列を取得します。
* @see https://www.bugbugnow.net/2021/12/tokenize-parse-html.html
* @param {string} text - テキスト
* @param {string} stext - 開始文字列
* @param {string} etext - 終了文字列
* @return {string[]} - 抽出文字列
*/
function findText(text, stext, etext) {
const stack = [];
const slen = stext.length;
const elen = etext.length;
let si, sindex, ei, eindex = 0;
while (true) {
si = text.indexOf(stext, eindex);
if (si < 0) { break; }
sindex = si + slen;
ei = text.indexOf(etext, sindex);
if (ei < 0) { break; }
stack.push(text.substring(sindex, ei));
eindex = ei + elen;
}
return stack;
}</code></pre><pre><span class="pre-code-title">サンプル</span><code class="language-js:サンプル">var html = '<html><body><h1 id="header">〇〇ページ</h1><p>〇〇は、<span class="red">△△</span>です。</p></body></html>';
console.log(findText(html, '<', '>'));
// Array(10) [ "html", "body", "h1 id=\"header\"", "/h1", "p", "span class=\"red\"", "/span", "/p", "/body", "/html" ]
console.log(findText(html, '<h1', '</h1'));
// Array [ " id=\"header\">〇〇ページ" ]</code></pre><h4>解説</h4><p>開始文字列と終了文字列に囲まれた文字列を単純に取得しています。単純ではあるものの、HTMLの構造を理解して使用すれば必要十分ではあります。<br>(HTML以外でも使用できます)</p><p>ただし、入れ子構造に弱く、コメントや属性内の「<code>></code>」など不親切なページに完全に対応することは難しいです。</p></section><section id="toc-4"><h3>正規表現を使用して、簡易にタグとタグ以外を分解する</h3><pre><span class="pre-code-title">tokenizeHTMLEasy.js</span><code class="language-js:tokenizeHTMLEasy.js">/**
* HTMLを簡易に字句解析する
* 生テキストタグは、非対応です。
* @author toshi (https://github.com/k08045kk)
* @license MIT License | https://opensource.org/licenses/MIT
* @version 3
* @since 1 - 20211215 - 初版
* @since 2 - 20220328 - fix 「'」「"」をタグ名・属性名に使用できる
* @since 2 - 20220328 - fix 全角スペースをタグ名・属性名に使用できる
* @since 2 - 20220328 - fix 属性の途中に「"'」を含むことがある
* @since 3 - 20221013 - 正規表現を最適化
* @since 3 - 20221013 - fix 「?」をタグ名先頭に許可しない
* @see https://www.bugbugnow.net/2021/12/tokenize-parse-html.html
* @param {string} html - 生テキストのHTML
* @param {Object} [option={}] - オプション
* @param {boolean} option.trim - タグ間の空白を削除する
* @return {string[]} - 分解した文字列
*/
function tokenizeHTMLEasy(html, option={}) {
const stack = [];
let lastIndex = 0;
const findTag = /<[!/?A-Za-z][^\t\n\f\r />]*([\t\n\f\r /]+[^\t\n\f\r /][^\t\n\f\r /=]*([\t\n\f\r ]*=[\t\n\f\r ]*("[^"]*"|'[^']*'|[^\t\n\f\r >]*))?)*[\t\n\f\r /]*>/g;
for (let m; m=findTag.exec(html); ) {
if (lastIndex < m.index) {
let text = html.substring(lastIndex, m.index);
if (option.trim) { text = text.trim(); }
if (text.length > 0) { stack.push(text); }
}
lastIndex = findTag.lastIndex;
let tag = m[0];
if (option.trim) { tag = tag.trim(); }
stack.push(tag);
}
return stack;
}</code></pre><pre><span class="pre-code-title">サンプル</span><code class="language-js:サンプル">var html = '<html><body><h1 id="header">〇〇ページ</h1><p>〇〇は、<span class="red">△△</span>です。</p></body></html>';
console.log(tokenizeHTMLEasy(html));
// Array(14) [ "<html>", "<body>", "<h1 id=\"header\">", "〇〇ページ", "</h1>", "<p>", "〇〇は、", "<span class=\"red\">", "△△", "</span>", "です。", "</p>", "</body>", "</html>" ]</code></pre><h4>解説</h4><p>正規表現を利用した単純な方法です。(正規表現はお世辞にも単純ではありませんが…)</p><p>コメントや改行文字、属性内の「<code>></code>」などほとんどのパターンに対応できます。20行未満のコードであり、行数的にも簡易です。</p><p>正規表現の概略図を次に示します。</p><img alt="概略図" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEjF9WmuU212lPHs8-bWUreVFDYxayK1ZbLaEP_SeRl-81toGI1x7hbs6fjHqc3F86Om6oEsg3F0kA32trx_DUkQOWlItm9avE1_s4NYYyYoAD1iQkiFxISVMPdNyePz6cWKKHfovKUMYQKb_RigZAdq8FDS9KvPVOj2YMb67pb1pBKMIlWEJeTKlgoj1g/s1600/20211215_tokenizeHTML.png" loading="lazy" width="1790" height="600" decoding="async"><p>※概略図は、次のサイトで生成したものです。<br> <a href="https://jex.im/regulex/#!flags=&re=%3C%5B!%2F%3FA-Za-z%5D%5B%5E%5Ct%5Cn%5Cf%5Cr%20%2F%3E%5D*(%5B%5Ct%5Cn%5Cf%5Cr%20%2F%5D%2B%5B%5E%5Ct%5Cn%5Cf%5Cr%20%2F%5D%5B%5E%5Ct%5Cn%5Cf%5Cr%20%2F%3D%5D*(%5B%5Ct%5Cn%5Cf%5Cr%20%5D*%3D%5B%5Ct%5Cn%5Cf%5Cr%20%5D*(%22%5B%5E%22%5D*%22%7C'%5B%5E'%5D*'%7C%5B%5E%5Ct%5Cn%5Cf%5Cr%20%3E%5D*))%3F)*%5B%5Ct%5Cn%5Cf%5Cr%20%2F%5D*%3E">https://jex.im/regulex/</a></p><p>※生テキストタグ(<code><templete><script><style></code>等)は、非対応です。<br>※タグの開始文字が<code>[!/?A-Za-z]</code>以外の場合、タグとして認識しないため、弾いています。<br> 例:<code><>, < >, <0>, <あ></code><br> ブラウザはタグとして認識しません。文字列として処理します。<br>※一部のタグを間違って解釈します。<br> 例:<code></></code><br> タグとして処理しますが、ブラウザでは不正なタグとして消滅します。<br>※ファイル末尾の不完全なタグは、非対応です。<br> 例:<code>abc<a</code>(<code>abc</code>のみを表示する。<code><a</code>は消滅する)<br> ファイル末尾の不完全なタグは、ブラウザでは消滅します。</p><p>※タグ名の「<code>!</code>」「<code>?</code>」は、特殊なタグを判定します。<br> 例:<code><?xml version="1.0" encoding="UTF-8"?></code><br> 例:<code><!DOCTYPE html></code><br> 例:<code><!-- comment --></code><br>※タグ名の「<code>/</code>」は、閉じタグを判定します。<br> 例:<code></a></code><br>※タグ名判定で <code>[!/?A-Za-z]</code> の部分を <code>[!/?]?[A-Za-z]</code> と括りだす方が正確に判定しているように感じますが、次のパターンで問題が発生するため、実施しない。<br> 例:<code></></code>(不正なタグとして消滅すべき)</p><p>※タグ名・属性名に全角スペースを許容します。<br> 例:<code><a b></code>(<code>a b</code>がタグ名となる)<br> HTMLの区切り文字は、「<code>\t\n\f\r </code>」の5文字です。<br> <code>\t</code>:U+0009(水平タブ)<br> <code>\n</code>:U+000A(改行)<br> <code>\f</code>:U+000C(書式送り)<br> <code>\r</code>:U+000D(復帰)<br> <code> </code>:U+0020(半角スペース)<br> <code> </code>:U+3000(全角スペース)などを含む <code>\s</code> とは異なる点に注意してください。<br> see <a href="https://www.w3.org/TR/2011/WD-html5-20110525/common-microsyntaxes.html#space-character">https://www.w3.org/TR/2011/WD-html5-20110525/common-microsyntaxes.html#space-character</a><br>※属性名に「<code>"</code>」「<code>'</code>」を許容します。<br> 例:<code><a "=b></code>(属性<code>"</code>が<code>b</code>の値を持ちます)<br>※属性値に「<code>"</code>」「<code>'</code>」を途中から含めることができます。<br> 例:<code><a b=c'd></code>(属性<code>b</code>が<code>c'd</code>の値を持ちます)<br>※タグ内の「<code>/</code>」を一部無視します。(「<code>/</code>」の一部を区切り文字として扱います)<br> 例(無視する):<code><a/b=c></code>(<code><a b=c></code>と同じ)<br> 例(無視する):<code><a//=c></code>(<code><a =c></code>と同じ、 <code></code> (空文字)の属性を持つ)<br> 例(無視する):<code><a/b/=c></code>(<code><a b =c></code>と同じ、<code>b</code>と <code></code> (空文字)の属性を持つ)<br> 例(無視する):<code><a b="c"/d=e></code>(<code><a b=c d=e></code>と同じ)<br> 例(無視しない):<code><a b=c/d=e></code>(<code><a b="c/d=e"></code>と同じ)</p><p>※正確ではありませんが、<code>findTag</code>を次の簡易実装に置き換えることもできます。<br> 例:<code>/<[!/?A-Za-z][^\s/>]*([\s/]+[^\s/][^\s/=]*([\s]*=([\s]*("[^"]*"|'[^']*'|[^\s>]*)))?)*[\s/]*>/g</code><br> 「<code>\t\n\f\r </code>」を「<code>\s</code>」に置き換えた簡易実装です。<br> タグ名・属性名に全角スペースを含む場合、問題になります。<br> 例:<code>/<[!/?A-Za-z]("[^"]*"|'[^']*'|[^"'>])*>/g</code><br> タグ名と属性の括りだけを考慮した簡易実装です。<br> タグ名・属性名に全角スペース・「<code>"'</code>」を含む場合、問題になります。<br> 属性値の途中に「<code>"'</code>」を含む場合、問題になります。</p></section><section id="toc-5"><h3>正規表現を使用して、もう少し考えて分解する</h3><pre><span class="pre-code-title">tokenizerHTML.js</span><code class="language-js:tokenizerHTML.js">/**
* HTMLを字句解析する
* 正規表現を利用して、HTMLのタグとタグ以外を分離します。
* 生テキストタグ、空タグに対応します。
* @author toshi (https://github.com/k08045kk)
* @license MIT License | https://opensource.org/licenses/MIT
* @version 3
* @since 1 - 20211215 - 初版
* @since 2 - 20220328 - fix 「'」「"」をタグ名・属性名に使用できる
* @since 2 - 20220328 - fix 全角スペースをタグ名・属性名に使用できる
* @since 2 - 20220328 - fix 属性の途中に「"'」を含むことがある
* @since 3 - 20221013 - 正規表現を最適化
* @since 3 - 20221013 - fix 「?」をタグ名先頭に許可しない
* @see https://www.bugbugnow.net/2021/12/tokenize-parse-html.html
* @param {string} html - 生テキストのHTML
* @param {Object} [option={}] - オプション
* @param {boolean} option.trim - タグ間の空白を削除する
* @return {Object} - 分解したノード
*/
function *tokenizerHTML(html, option={}) {
const rawTags = ['SCRIPT','STYLE','TEMPLATE','TEXTAREA','TITLE'];
const emptyTags = ['AREA','BASE','BASEFONT','BR','COL','EMBED','FRAME','HR','IMG','INPUT','KEYGEN','ISINDEX','LINK','META','PARAM','SOURCE','TRACK','WBR'];
// Note: 廃止・非推奨:<basefont><keygen><frame><isindex>
let lastIndex = 0;
let findClosingRawTag = null;
const findTag = /<[!/?A-Za-z][^\t\n\f\r />]*([\t\n\f\r /]+[^\t\n\f\r /][^\t\n\f\r /=]*([\t\n\f\r ]*=[\t\n\f\r ]*("[^"]*"|'[^']*'|[^\t\n\f\r >]*))?)*[\t\n\f\r /]*>/g;
for (let m; m=findTag.exec(html); ) {
const tag = m[0];
if (findClosingRawTag) {
if (findClosingRawTag.test(tag)) { findClosingRawTag = null; }
else { continue; }
}
let type = 'opening-tag';
const pre = tag.substring(0, 2);
if (pre === '</') { type = 'closeing-tag'; }
if (pre === '<!') { type = 'comment'; }
// Note: <!DOCTYPE html> をコメントとして処理する
// Note: <?xml version="1.0" encoding="UTF-8"?> を無効として処理する(要検討)
let name;
if (type !== 'comment') {
if (tag.substring(tag.length-1) === '>') {
const findTagName = /^<\/?([A-Za-z][^\t\n\f\r />]*)/;
const n = tag.match(findTagName);
if (n) { name = n[1].toUpperCase(); }
}
if (name == null) { type = 'invalid'; }
}
let attribute;
if (type === 'opening-tag') {
attribute = {};
const content = tag.replace(/^<[A-Za-z][^\t\n\f\r />]*|>$/g, '');
const findTagAttribute = /([^\t\n\f\r /][^\t\n\f\r /=]*)([\t\n\f\r ]*=([\t\n\f\r ]*("([^"]*)"|'([^']*)'|([^\t\n\f\r >]*))))?/g;
for (let a; a=findTagAttribute.exec(content); ) {
if (!attribute.hasOwnProperty(a[1])) {
attribute[a[1]] = a[7] || a[6] || a[5]
|| a[3]
|| '';;
}
}
if (rawTags.indexOf(name) != -1) { findClosingRawTag = new RegExp('^</'+name+'[\\t\\n\\f\\r />]', 'i'); }
if (emptyTags.indexOf(name) != -1) { type = 'empty-tag'; }
// Note: <tagName /> を考慮しない
}
if (lastIndex < m.index) {
let text = html.substring(lastIndex, m.index);
if (option.trim) { text = text.trim(); }
if (text.length > 0) { yield {type:'text', name:'#text', content:text}; }
}
lastIndex = findTag.lastIndex;
const token = {type:type, name:(name ? name : '#'+type), content:(option.trim ? tag.trim() : tag)};
if (attribute) { token.attribute = attribute; }
yield token;
}
if (lastIndex < html.length) {
let text = html.substring(lastIndex);
if (option.trim) { text = text.trim(); }
if (text.length > 0) {
const findTagStart = /<[!/?A-Za-z]/;
const s = findTagStart.exec(text);
if (findClosingRawTag) {
yield {type:'text', name:'#text', content:text};
} else if (s) {
let text1 = text.substring(0, s.index);
let text2 = text.substring(s.index);
if (option.trim) {
text1 = text1.trim();
text2 = text2.trim();
}
if (text1.length > 0) { yield {type:'text', name:'#text', content:text1}; }
if (text2.length > 0) { yield {type:'invalid', name:'#invalid', content:text2}; }
} else {
yield {type:'text', name:'#text', content:text};
}
}
}
// Note: {type, name, content, attribute}
// Note: type = opening-tag / closeing-tag / empty-tag / comment / text / invalid
// Note: name = tagName / #comment / #text / #invalid
}
function tokenizeHTML(html, option) {
const stack = [];
for (const token of tokenizerHTML(html, option)) {
stack.push(token);
}
return stack;
}</code></pre><pre><span class="pre-code-title">サンプル</span><code class="language-js:サンプル">var html = '<html><body><h1 id="header">〇〇ページ</h1><p>〇〇は、<span class="red">△△</span>です。</p></body></html>';
console.log(tokenizeHTML(html));
//Array(14) [ {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, {…}, … ]
// 0: Object { type: "opening-tag", content: "<html>", name: "HTML", attribute: Object {} }
// 1: Object { type: "opening-tag", content: "<body>", name: "BODY", attribute: Object {} }
// 2: Object { type: "opening-tag", content: "<h1 id=\"header\">", name: "H1", attribute: Object {id: "header"} }
// 3: Object { type: "text", content: "〇〇ページ" }
// 4: Object { type: "closeing-tag", content: "</h1>", name: "H1" }
// 5: Object { type: "opening-tag", name: "P", content: "<p>", Object {} }
// 6: Object { type: "text", name: "#text", content: "〇〇は、" }
// 7: Object { type: "opening-tag", name: "SPAN", content: "<span class=\"red\">", Object {class: "red"} }
// 8: Object { type: "text", name: "#text", content: "△△" }
// 9: Object { type: "closeing-tag", name: "SPAN", content: "</span>" }
// 10: Object { type: "text", name: "#text", content: "です。" }
// 11: Object { type: "closeing-tag", name: "P", content: "</p>" }
// 12: Object { type: "closeing-tag", content: "</body>", name: "BODY" }
// 13: Object { type: "closeing-tag", content: "</html>", name: "HTML" }</code></pre><h4>解説</h4><p>生テキストタグや空タグを含めて、HTMLを分解します。タグ名と属性の抽出もサポートします。</p><p>HTMLの字句解析としては、フルスペックの性能があります。この次の段階は、字句解析の結果を使用して、構文解析でドキュメントツリーに変換します。</p></section><section id="toc-6"><h3>HTMLのドキュメントツリーを簡易に作成する</h3><pre><span class="pre-code-title">parseHTMLEasy.js</span><code class="language-js:parseHTMLEasy.js">/**
* HTMLを構文解析する
* HTMLから簡易なドキュメントツリーを作成します。
* @author toshi (https://github.com/k08045kk)
* @license MIT License | https://opensource.org/licenses/MIT
* @version 1
* @since 1 - 20211215 - 初版
* @see https://www.bugbugnow.net/2021/12/tokenize-parse-html.html
* @param {string} html - 生テキストのHTML
* @param {Object} [option={}] - オプション
* @param {boolean} option.trim - タグ間の空白を削除する
* @return {Object} - ドキュメントツリー
*/
function parseHTMLEasy(html, option) {
const Node = function(name, option) {
option && Object.keys(option).forEach((key) => {
this[key] = option[key];
});
this.parentNode = null;
this.nodeName = name;
this.childNodes = [];
this.firstChild = null;
this.lastChild = null;
this.previousSibling = null;
this.nextSibling = null;
};
const appendChild = (parent, node) => {
node.parentNode = parent;
node.previousSibling = null;
node.nextSibling = null;
if (parent.lastChild) {
parent.lastChild.nextSibling = node;
node.previousSibling = parent.lastChild;
}
if (parent.firstChild == null) { parent.firstChild = node; }
parent.lastChild = node;
parent.childNodes.push(node);
};
const dom = new Node('#document');
let parent = dom;
let lastIndex = 0;
for (const token of tokenizerHTML(html, option)) {
switch (token.type) {
case 'opening-tag':
case 'empty-tag':
const node = new Node(token.name, {attribute:token.attribute});
appendChild(parent, node);
if (token.type !== 'empty-tag') { parent = node; }
break;
case 'closeing-tag':
let valid = false;
for (let node=parent; node; node=node.parentNode) {
if (node.nodeName === token.name) {
parent = node.parentNode;
valid = true;
break;
}
}
if (valid) { break; }
case 'invalid':
break;
case 'comment':
case 'text':
appendChild(parent, new Node(token.name, {content:token.content}));
break;
default:
break;
}
}
return dom;
}</code></pre><pre><span class="pre-code-title">サンプル</span><code class="language-js:サンプル">var html = '<html><body><h1 id="header">〇〇ページ</h1><p>〇〇は、<span class="red">△△</span>です。</p></body></html>';
function showNode(node, deep=0) {
const option = node.attribute ? node.attribute : node.content;
console.log(Array(deep+1).join(' ')+node.nodeName, option);
for (const child of node.childNodes) { showNode(child, deep+1); }
}
showNode(parseHTMLEasy(html));
// #document undefined
// HTML Object { }
// BODY Object { }
// H1 Object { id: "header" }
// #text 〇〇ページ
// P Object { }
// #text 〇〇は、
// SPAN Object { class: "red" }
// #text △△
// #text です。</code></pre><h4>解説</h4><p>HTMLから簡易なドキュメントツリーを作成しています。</p><p>簡易な Node を使用して、ドキュメントツリーを構築しています。簡易であるため、省略可能なタグなどには対応していません。XML形式などのがっちりとしたHTMLならば、意図した動作をします。ですが、省略などを含む場合、意図した動作にはなりません。HTML5的な省略やブラウザ基準のドキュメントツリーを作成する場合、複雑度が跳ね上がるため、ここまでとします。</p><p>※前述の <code>tokenizerHTML()</code> を字句解析に使用しています。</p></section>toshihttp://www.blogger.com/profile/02673765704159584610noreply@blogger.com0tag:blogger.com,1999:blog-279447686030876252.post-65564637940779137292021-12-03T12:30:00.001+09:002021-12-03T12:30:41.939+09:00ウェブページにアウトラインを表示するブックマークレット<script type="application/json" id="post-data-json">{
"title": "ウェブページにアウトラインを表示するブックマークレット"
,"labels": ["Bookmarklet", "JavaScript", "CSS"]
,"url": "https://www.bugbugnow.net/2021/12/displaying-outlines.html"
}</script><section id="toc-1"><h3>はじめに</h3><p>文章のアウトラインではないです。DOM要素のアウトラインのレイアウトを表示します。</p><p>次の画像のような表示を確認するブックマークレットです。</p><img alt="アウトライン表示" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEiPapcyuOSKkxVNQNYyRxAyjWA9jiHvfjbSyLQ9nI6recqsOsgFldi3rrXjkSTrtlLxP9y6DpfnEm9eZkNFhQnnjXqli0i4MfzOxyX4vNmCy22jscl5sFBaRrg8jc6f_HQbog0ZJkfT9gE6/s0/20211203_outline.png" loading="lazy" decoding="async" width="1117" height="639"><p>この表示の何が良いのかといえば、要素の配置が一目瞭然になるところです。不要な要素の発見、意図しない位置に配置された要素の発見などに役立ちます。</p></section><section id="toc-2"><h3>コード</h3><pre><code class="language-js">(function() {
var style = document.createElement('style');
style.textContent = '* { outline: magenta solid 1px; }';
document.head.appendChild(style);
})();</code></pre><h4>コード解説</h4><p><code>* { outline: magenta solid 1px; }</code> のスタイルシートをヘッダー末尾に追加する。</p><h4>既知の問題</h4><ul><li>iframe 内部には適用できない</li><li>ShadowDOM 内部には適用できない</li></ul></section><section id="toc-3"><h3>ブックマークレット</h3><pre><code class="language-js">javascript:(function(){var a=document.createElement('style');a.textContent='* { outline: magenta solid 1px; }';document.head.appendChild(a)})();</code></pre><p>ブックマークレット:「<a href="javascript:(function(){var a=document.createElement('style');a.textContent='* { outline: magenta solid 1px; }';document.head.appendChild(a)})();">* { outline: magenta; }</a>」</p></section>toshihttp://www.blogger.com/profile/02673765704159584610noreply@blogger.com0tag:blogger.com,1999:blog-279447686030876252.post-32881134149250297252021-09-24T23:45:00.003+09:002021-10-31T09:26:56.051+09:00Firefoxに再起動のメニューを追加する<p>FirefoxのuserChrome.js用スクリプトです。<br>再起動のメニュー(メインメニュー・アプリメニュー)を追加します。</p><section id="toc-1"><h3>RestartInMenu.uc.js</h3><pre><span class="pre-code-title">RestartInMenu.uc.js</span><code class="language-js:RestartInMenu.uc.js">// ==UserScript==
// @name RestartInMenu.uc.js
// @description Restart from the menu (main menu / app menu).
// @include main
// @charset UTF-8
// @author toshi (https://github.com/k08045kk)
// @license MIT License | https://opensource.org/licenses/MIT
// @compatibility 78+
// @version 0.4
// @since 0.1 - 20210924 - 初版
// @since 0.2 - 20210926 - Thunderbird対応
// @since 0.3 - 20210926 - リファクタリング
// @since 0.4 - 20211031 - Firefox78対応
// @see https://github.com/k08045kk/userChrome.js
// @see https://www.bugbugnow.net/2021/09/firefox-restart-in-menu.html
// ==/UserScript==
(() => {
// -------------------- config --------------------
const MENU_ITEM_LABEL_NAME = 'Restart';
// --------------------/config --------------------
const cmd_restart = (event) => {
// see chrome://global/content/aboutProfiles.js
const Ci = Components.interfaces;
const flags = Ci.nsIAppStartup.eRestart | Ci.nsIAppStartup.eAttemptQuit;
Services.startup.quit(flags);
};
// File menu (Main menu)
try {
let filemenu = document.getElementById('menu_FileRestartItem');
const filequit = document.getElementById('menu_FileQuitItem');
if (filemenu == null && filequit) {
filemenu = document.createXULElement('menuitem');
filemenu.setAttribute('id', 'menu_FileRestartItem');
filemenu.setAttribute('label', MENU_ITEM_LABEL_NAME);
filemenu.setAttribute('accesskey', 'r');
filemenu.addEventListener('command', cmd_restart);
filequit.parentNode.insertBefore(filemenu, filequit);
}
} catch (e) { Components.utils.reportError(e); }
// App menu (Hamburger menu)
try {
const target = document.getElementById('appMenu-popup') // Firefox91/Thunderbird91
|| document.getElementById('appmenu-popup'); // ???
const observer = new MutationObserver((mutationsList, observer) => {
let appmenu = document.getElementById('appMenu-restart-button');
const appquit = document.getElementById('appMenu-quit-button2') // Firefox91
|| document.getElementById('appMenu-quit-button') // Firefox78
|| document.getElementById('appMenu-quit') // Thunderbird??
|| document.getElementById('appmenu-quit'); // Thunderbird91
if (appmenu == null && appquit) {
appmenu = document.createXULElement('toolbarbutton');
appmenu.setAttribute('id', 'appMenu-restart-button');
appmenu.setAttribute('class', 'subviewbutton');
appmenu.setAttribute('label', MENU_ITEM_LABEL_NAME);
appmenu.addEventListener('command', cmd_restart);
appquit.parentNode.insertBefore(appmenu, appquit);
observer.disconnect();
}
});
observer.observe(target, {attributes:true});
} catch (e) { Components.utils.reportError(e); }
})();</code></pre></section>toshihttp://www.blogger.com/profile/02673765704159584610noreply@blogger.com2tag:blogger.com,1999:blog-279447686030876252.post-30151714218226407172021-06-18T10:05:00.011+09:002021-06-20T08:24:28.090+09:00Workerの実行を阻止する<script type="application/json" id="post-data-json">{
"title": "Workerの実行を阻止する"
,"labels": ["UserScript", "JavaScript", ""]
,"url": "https://www.bugbugnow.net/2021/06/dummy-worker.html"
}</script><section id="toc-1"><h3>はじめに</h3><p>Worker (Dedicatedworker or SharedWorker) の実行を阻止するユーザスクリプトです。</p><p>不本意なマイニングなどの Worker が絡む問題を解決できます。</p><p>ただし、ServiceWorkerの実行阻止とは異なり、すべてのページで Worker の実行を阻止することは推奨しません。なぜならば、 Worker の処理がウェブページの一連の処理の一部分を占める可能性が極めて高いためです。 Worker の実行を阻止することでウェブページ全体が動作しなくなる可能性が極めて高いです。</p><p>特定のページのみで使用するか、 NoScript などで JavaScript そのものの動作を無効化することを強く推奨します。</p><p>※上記理由により、筆者は本ユーザスクリプトをほとんど使用していません。ユーザスクリプトのテストが不十分であったり、今後の機能拡張される可能性が極めて低いことを留意して使用してください。</p></section><section id="toc-2"><h3>仕様</h3><ul><li>対象ページのWorker (Dedicatedworker or SharedWorker) の実行を阻止する</li><li>対象ページ以外は、動作しない(なにもしない)</li><li>ユーザスクリプトに対象ページを追記する(<code>@include</code> or <code>@match</code>で追記する)<ul><li><code>// @include *://*/*</code> で全ページを対象にすることもできます</li></ul></li><li>ServiceWorker の実行(登録)は、阻止しません<ul><li>ServiceWorkerを対象とする場合、下記ページを参照してください</li><li><a href="https://www.bugbugnow.net/2020/03/Reject-to-register-a-ServiceWorker.html">ServiceWorkerを無効化する</a></li></ul></li><li>ユーザスクリプトの実行タイミングでは、完全な実行阻止はできません<ul><li>ページへのスクリプト挿入が少し遅れるため</li><li>拡張機能は、ユーザスクリプト版に比べてこの部分がより強力に動作します<ul><li>拡張機能版は、作成しておりません</li></ul></li></ul></li></ul></section><section id="toc-3"><h3>ユーザスクリプト</h3><pre><span class="pre-code-title">DummyWorker.user.js</span><code class="language-js:DummyWorker.user.js">// ==UserScript==
// @name DummyWorker
// @description Set up a DummyWorker to stop the Worker from executing.
// Dedicatedworker or SharedWorker is targeted; ServiceWorker is not targeted.
// @note ↓↓↓ Add target page URL ↓↓↓
// @include *://example.com/*
// @note ↑↑↑ Add target page URL ↑↑↑
// @author toshi (https://github.com/k08045kk)
// @license MIT License | https://opensource.org/licenses/MIT
// @version 0.1.1
// @since 0.1.0 - 20210618 - 初版
// @since 0.1.1 - 20210620 - this から prototype に設計変更
// @see https://www.bugbugnow.net/2021/06/dummy-worker.html
// @run-at document-start
// @grant unsafeWindow
// ==/UserScript==
(function(w) {
var DummyWorker = function(aURL, options) {};
DummyWorker.prototype.postMessage = function() {};
DummyWorker.prototype.terminate = function() {};
DummyWorker.prototype.addEventListener = function() {};
DummyWorker.prototype.removeEventListener = function() {};
DummyWorker.prototype.dispatchEvent = function() {};
DummyWorker.prototype.port = {};
DummyWorker.prototype.port.postMessage = function() {};
DummyWorker.prototype.port.start = function() {};
DummyWorker.prototype.port.close = function() {};
DummyWorker.prototype.port.addEventListener = function() {};
DummyWorker.prototype.port.removeEventListener = function() {};
DummyWorker.prototype.port.dispatchEvent = function() {};
// see https://developer.mozilla.org/en-US/docs/Web/API/Worker
// see https://developer.mozilla.org/en-US/docs/Web/API/SharedWorker
w.Worker = DummyWorker;
w.SharedWorker = DummyWorker;
})(unsafeWindow || window);</code></pre></section>toshihttp://www.blogger.com/profile/02673765704159584610noreply@blogger.com0tag:blogger.com,1999:blog-279447686030876252.post-49296897000328308062021-05-21T20:42:00.007+09:002021-10-08T15:37:53.566+09:00Firefoxのブックマークバーを編集不可にする<p>FirefoxのuserChrome.js用スクリプトです。<br>ブックマークバーを編集不可とすることで意図しない誤動作を防止する。</p><section id="toc-2"><h3>BookmarkbarNotEditable.uc.js</h3><pre><span class="pre-code-title">BookmarkbarNotEditable.uc.js</span><code class="language-js:BookmarkbarNotEditable.uc.js">// ==UserScript==
// @name BookmarkbarNotEditable.uc.js
// @description ブックマークツールバーのドラッグによる編集不可とする。
// ただし、Shiftキー+ドラッグで編集可能とする。
// @include main
// @charset UTF-8
// @author toshi (https://github.com/k08045kk)
// @license MIT License | https://opensource.org/licenses/MIT
// @version 1.1
// @since 0.1 - 20210521 - 初版
// @since 0.2 - 20210524 - ドラッグ時のブックマーククリックを無効化
// @since 0.3 - 20210603 - fix セパレータを編集できる
// @since 1.0 - 20210910 - リリース版(GitHub追加、ライセンス設定)
// @since 1.1 - 20211008 - リファクタリング
// @see https://github.com/k08045kk/userChrome.js
// @see https://www.bugbugnow.net/2021/05/bookmarkbar-not-editable-uc-js.html
// ==/UserScript==
(function() {
const toolbar = document.getElementById('PlacesToolbar');
const items = ['toolbarbutton','toolbarseparator','menupopup','menu','menuitem','menuseparator'];
let isInterrup = false;
// ドラッグ開始を無効化(Shift+ドラッグ開始は、有効とする)
toolbar.addEventListener('dragstart', function(event) {
if (!event.shiftKey && ~items.indexOf(event.target.tagName)) {
isInterrup = true;
event.preventDefault();
}
}, true);
// ドラッグ時のブックマーククリックを無効化
toolbar.addEventListener('mousedown', function(event) {
isInterrup = false;
});
toolbar.addEventListener('mouseup', function(event) {
if (isInterrup) {
event.preventDefault();
}
});
// 備考:イベント発生順(dragstartは、mousedown後のマウス移動で発生する)
// mousedown > dragstart > mouseup
})();</code></pre><h4>変更履歴</h4><ul><li><a href="https://github.com/k08045kk/userChrome.js">k08045kk/userChrome.js - GitHub</a></li></ul></section>toshihttp://www.blogger.com/profile/02673765704159584610noreply@blogger.com0tag:blogger.com,1999:blog-279447686030876252.post-42028080955907692952021-05-13T08:29:00.004+09:002023-02-22T12:05:04.678+09:00JavaScript 無効限定の CSS を適用する<script type="application/json" id="post-data-json">{
"title": "JavaScript 無効限定の CSS を適用する"
,"labels": ["CSS", "JavaScript", "NoScript"]
,"url": "https://www.bugbugnow.net/2021/05/noscript-css.html"
}</script><section id="toc-1"><h3>JavaScript を使用する方法</h3><pre><code class="language-html"><html class="nojs">
<head>
<script>
document.documentElement.classList.replace('nojs', 'js');
//document.documentElement.classList.remove('nojs');
//document.documentElement.classList.add('js');
</script>
<style>
/* JavaScript が有効の時 */
.element1 {
color: blue;
}
.js .element2 {
color: blue;
}
/* JavaScript が無効の時 */
.nojs .element1 {
color: red;
}
.element2 {
color: red;
}
</style>
</head>
<body>
<p class="element1">Hello World1</p>
<p class="element2">Hello World2</p>
</body>
</html></code></pre><h4>簡易解説</h4><p><code><script></code>を使用して<code><html></code>の<code>class</code>属性を置き換えます。</p><p>JavaScript が有効な場合、<code>class</code>が置き換わります。<br>JavaScript が無効な場合、 JavaScript が実行されないため、<code>class</code>は置き換わりません。</p></section><section id="toc-2"><h3>noscript を使用する方法</h3><pre><code class="language-html"><html>
<head>
<style>
/* JavaScript が有効の時 */
.element {
color: blue;
}
</style>
<noscript>
<style>
/* JavaScript が無効の時 */
.element {
color: red;
}
.js-code {
display: none;
}
</style>
<!-- link で JavaScript 無効時のスタイルシートを読み込む -->
<!-- link rel="stylesheet" href="/style-noscript.css"/ -->
</noscript>
</head>
<body>
<p class="element">Hello World</p>
<p>
Hello
<span class="js-code">JavaScript</span>
<noscript>NoScript</noscript>
World
</p>
</body>
</html></code></pre><h4>簡易解説</h4><p><code><noscript></code>を使用してスタイルシートを上書きします。</p><p>JavaScript が有効な場合、<code><noscript></code>が処理されず元々の CSS が適用されます。<br>JavaScript が無効な場合、<code><noscript></code>内の<code><style></code>or<code><link rel="stylesheet"></code>が元々の CSS を上書きします。</p></section>toshihttp://www.blogger.com/profile/02673765704159584610noreply@blogger.com0tag:blogger.com,1999:blog-279447686030876252.post-65154723295127545182021-04-18T17:46:00.006+09:002023-04-04T12:44:21.940+09:00JavaScript のライフサイクルに関するイベント<script type="application/json" id="post-data-json">{
"title": "JavaScript のライフサイクルに関するイベント"
,"labels": ["JavaScript", "覚書", ""]
,"url": "https://www.bugbugnow.net/2021/04/javascript-lifecycle-and-events.html"
}</script><div id="toc"><div id="toc-title">目次</div><div id="toc-content"><ol><li><a class="toc-link" href="#toc-1">はじめに</a></li><li><a class="toc-link" href="#toc-2">基本(script)</a></li><li><a class="toc-link" href="#toc-3">基本(状態遷移)</a></li><li><a class="toc-link" href="#toc-4">readystatechange イベント</a></li><li><a class="toc-link" href="#toc-5">DOMContentLoaded イベント</a></li><li><a class="toc-link" href="#toc-6">load イベント</a></li><li><a class="toc-link" href="#toc-7">pageshow イベント</a></li><li><a class="toc-link" href="#toc-8">First CPU Idle</a></li><li><a class="toc-link" href="#toc-9">First Contentful Paint (FCP)</a></li><li><a class="toc-link" href="#toc-10">First Input Delay (FID)</a></li><li><a class="toc-link" href="#toc-11">初回ユーザイベント</a></li><li><a class="toc-link" href="#toc-12">初回スクロール</a></li><li><a class="toc-link" href="#toc-13">対象要素が表示領域内に入ったタイミング</a></li><li><a class="toc-link" href="#toc-14">beforeunload イベント</a></li><li><a class="toc-link" href="#toc-15">pagehide イベント</a></li><li><a class="toc-link" href="#toc-16">unload イベント</a></li><li><a class="toc-link" href="#toc-17">visibilitychange イベント</a></li><li><a class="toc-link" href="#toc-18">focus / focusin / focusout / blur イベント</a></li><li><a class="toc-link" href="#toc-19">備考</a></li></ol></div></div><section id="toc-1"><h3>はじめに</h3><p>ここでは、ウェブページのライフサクル(ページの読み込みから開放まで)に関する、 JavaScript のイベント(実行タイミング)の覚書です。</p><p><a href="https://github.com/k08045kk/onLazy.js">onLazy.js</a> の開発で得られた知見をまとめて記載します。</p></section><section id="toc-2"><h3>基本(script)</h3><ul><li><code><script></code> を発見した場合の処理<ul><li>type 属性が <code>text/javascript</code> 以外の場合、スクリプトは実行されない<ul><li>type 属性未定義は、 <code>text/javascript</code> として解釈される</li></ul></li><li>async 属性がある場合、スクリプトの読み込みと実行を非同期で実行する<ul><li>src 属性が必須</li><li>HTML の解析処理は、スクリプトの処理と平行して継続する</li><li>スクリプトは、スクリプトの読み込み後に実行する<ul><li>HTML の解析処理中に実行されることがある</li><li>HTML の解析処理完了後に実行されることもある</li><li>async 属性のある <code><script></code> の実行順は、保証されない</li></ul></li></ul></li><li>defer 属性がある場合、スクリプトの読み込みを非同期で実行する<ul><li>src 属性が必須</li><li>HTML の解析処理は、スクリプトの読み込みと平行して継続する</li><li>HTML の解析完了後にスクリプトを実行する<ul><li>defer 属性のある <code><script></code> の実行順は、保証される</li></ul></li><li>async 属性と併記した場合、ブラウザが対応していれば async 属性が優先される</li></ul></li><li>src 属性がある場合、ファイルの読み込みを待機して、スクリプトを実行する<ul><li>スクリプト実行後に HTML の解析処理を再開する</li><li>async / defer 属性がある場合、 async / defer 属性として処理される</li></ul></li><li>上記以外の場合、 HTML の解析処理を中断する(タスクが分割される)<ul><li>スクリプトを即座に実行する</li></ul></li></ul></li></ul><p>※ async / defer について、次の記事が参考になります<br> <a href="https://qiita.com/phanect/items/82c85ea4b8f9c373d684"><script> タグに async / defer を付けた場合のタイミング - Qiita</a><br>※ <code>module/module async</code> 属性がある<br> 実行順だけに着目すれば、 <code>defer/async</code> 属性と同じ</p><h4>外部スタイルシートによるスクリプトのブロック</h4><p>外部ファイルのスタイルシート宣言が先にある場合、スタイルシートの読み込み完了まで <code><script></code> の実行を待機します。これは、スクリプト内でスタイルシートを参照する可能性があるための措置です。</p><p>スクリプトがスタイルシートに依存しない場合、 <code><link type="stylesheet"></code> より前に <code><script></code> を記載することでこの問題を回避できます。</p><h4>動的ロード</h4><pre><code class="language-html"><head>
<script>
var script = document.createElement('script');
script.src = 'script.js';
document.head.appendChild(script);
</script>
<body></code></pre><p>※同期実行はしない。 async 属性と類似する動作となる。</p><h4>埋め込みスクリプトの実行箇所を判別する</h4><pre><code class="language-js">if (!document.body) {
// <head>内で実行
}
if (document.readyState === 'loading') {
// <body>内で実行
}</code></pre><p>※ <code><script></code> を HTML に直接記述した場合の判定方法です。</p></section><section id="toc-3"><h3>基本(状態遷移)</h3><p>読み込み~開放までの状態遷移です。 focus / blur, visibilitychange, beforeunload / pagehide / unload, freeze / resume / pageshow, など複雑なイベントの状態遷移があります。主にフォーカス、表示非表示、アンロード、破棄と開放、再表示に関する状態を表しています。</p><p>次の記事が参考になります。</p><ul><li><a href="https://developers.google.com/web/updates/2018/07/page-lifecycle-api">Page Lifecycle API | Web | Google Developers</a></li></ul></section><section id="toc-4"><h3>readystatechange イベント</h3><ul><li><a href="https://developer.mozilla.org/ja/docs/Web/API/Document/readystatechange_event">Document: readystatechange イベント - Web API | MDN</a></li></ul><p><code>document.readyState</code> の値変更時に発火する。</p><p><code>document.readyState</code> は、 <code>loading/interactive/complete</code> の3つの値に遷移します。 readystatechange イベントは、 <code>interactive/complete</code> 変更時の合計2回呼び出されます。 <code>interactive</code> 変更時は、 HTML の解析完了直後で defer 属性のスクリプトの実行直前です。その後、 DOMContentLoaded イベントが実行されます。 <code>complete</code> 変更時は、loadイベントの実行直前です。</p><p>※ <code>document.readyState</code> の値は、 readystatechange イベント直前に変更されます。</p></section><section id="toc-5"><h3>DOMContentLoaded イベント</h3><p>最初の HTML 文書の読み込みと解析が完了時に発火する。</p><p>HTML の <code></html></code> まで解析が完了後に呼び出されます。 DOMContentLoaded イベント後に、リソース(外部ファイル等)の読み込みを実施します。</p><p>HTML の解析が完了しているため、 DOM が既に完成しているため、ドキュメントに関する処理を実施できます。</p><ul><li><a href="https://developer.mozilla.org/ja/docs/Web/Events/DOMContentLoaded">DOMContentLoaded - イベントリファレンス | MDN</a></li></ul><h4><code><script></code> の直接記述</h4><p>src 属性なしの <code><script></code> を直接記述した場合、スクリプトはその時点で実行されます。</p><p>その時点で実行されるため、まだ DOM が完全には完成していません。そのため、 <code><script></code> より後に記述のある DOM 要素は存在しません。</p><p>これを回避するため、 <code></body></code> 直前に <code><script></code> を記述したり、 <code>DOMContentLoaded</code> イベントに登録だけして実行を後回しにする回避策が存在します。</p><p>※<code>DOMContentLoaded</code> イベント後に <code>DOMContentLoaded</code> の登録を実施してもイベントは実行されません。(既に発生済みのため、)イベントのタイミングが発生しません。</p><h4>DOMContentLoaded イベントの完了を判定する</h4><pre><code class="language-js">if (document.readyState === 'loading') {
// DOMContentLoaded イベント前
} else {
// DOMContentLoaded イベント後
// 例外:readystatechange(interactive)/defer属性/DOMContentLoadedの実行中を含む
}</code></pre></section><section id="toc-6"><h3>load イベント</h3><ul><li><a href="https://developer.mozilla.org/ja/docs/Web/Events/load">load - イベントリファレンス | MDN</a></li></ul><p>リソースの読み込み完了時に発火する。</p><p><code>load</code> イベントは、ページ構成や通信状態によっては問題となるほど遅くに発火します。例として、ページ読み込み込みから1秒後に初回描画して、10秒後に<code>load</code>イベントが発生したとしても何も不思議はありません。そのため、ページ読み込み後の処理は、できうる限り <code>DOMContentLoaded</code> イベントで実施すべきです。</p><h4>load イベントの完了を判定する</h4><pre><code class="language-js">if (document.readyState !== 'complete') {
// load イベント前
} else {
// load イベント後
// 例外:readystatechange(complete)/loadイベント中を含む
}</code></pre></section><section id="toc-7"><h3>pageshow イベント</h3><p>pageshow イベントは、 load イベントと類似しています。</p><p>初回の pageshow イベントは、 load イベントの発動直後に発火します。初回の pageshow イベントは、 <code>persisted</code> に <code>true</code> が設定されています。</p><p>初回以降にページがロードされた場合、 pageshow イベントは <code>persisted</code> に <code>false</code> が設定されています。初回以外のページロードは、戻る機能などでページがキャッシュされていた場合に発生します。(ページがキャッシュされていた場合、 load イベントは発生しません)</p><p>pageshow イベントは、 load イベント後に実行されます。 DOMContentLoaded → load → pageshow の順にイベントが発生します。</p></section><section id="toc-8"><h3>First CPU Idle</h3><pre><code class="language-html"><head>
<script>
window.requestIdleCallback(function() {
// First CPU Idle
});
</script></code></pre><p>※<a href="https://developer.mozilla.org/ja/docs/Web/API/Window/requestIdleCallback">requestIdleCallback - Web API | MDN</a><br>※<code>window.requestIdleCallback</code>は、アイドル処理の直前に呼び出される。<br> <code><head></code>内の埋め込みスクリプトは、初回アイドル前である可能性が非常に高い。<br> そのため、初回アイドルを補足できる(初回アイドルでない可能性もある)</p></section><section id="toc-9"><h3>First Contentful Paint (FCP)</h3><h4>FCP直前 / FCP直後</h4><pre><code class="language-html"><head>
<script>
window.requestAnimationFrame(function() {
// FCP直前
window.requestAnimationFrame(function() {
// FCP直後(ただし、描画のフレームがスキップされる可能性がある)
});
});
</script></code></pre><p>※<a href="https://developer.mozilla.org/ja/docs/Web/API/Window/requestAnimationFrame">Window.requestAnimationFrame() - Web API | MDN</a><br>※<code>window.requestAnimationFrame</code> は、描画処理の直前に呼び出される。<br> <code><head></code> 内の埋め込みスクリプトは、FCP前が保証できる。<br> よって、次の描画処理は FCP の直前となる。<br> ただし、描画のフレームがスキップされる可能性がある。</p><h4>FCP後</h4><pre><code class="language-js">new PerformanceObserver(function(entryList) {
// FCP後(ただし、FCP直後とは限らない。FCP後すぐ実行される保証はない)
}).observe({type:'paint', buffered:true});</code></pre><p>※<a href="https://developer.mozilla.org/ja/docs/Web/API/PerformanceObserver/PerformanceObserver">PerformanceObserver() - Web API | MDN</a><br>※Largest Contentful Paint (LCP) も同様に検出できる</p></section><section id="toc-10"><h3>First Input Delay (FID)</h3><h4>FID後</h4><pre><code class="language-js">new PerformanceObserver((entryList) => {
// FID後(ただし、FID直後とは限らない。FID後すぐ実行される保証はない)
}).observe({type: 'first-input', buffered: true});</code></pre></section><section id="toc-11"><h3>初回ユーザイベント</h3><p><code>click / mousedown / keydown / touchstart / mousemove / scroll</code> などのイベントをページ読み込み後に初めて取得したタイミング</p><p>onLazy.jsの主たる機能の1つ。</p><p>※FID で使用されるユーザイベントは、 <code>click / mousedown / keydown / touchstart / pointerdown</code> です。<br> <code>mousemove / scroll</code> がないことに留意する。</p></section><section id="toc-12"><h3>初回スクロール</h3><p>scroll イベントをページ読み込み後に初めて取得したタイミング</p><p>onLazy.jsの主たる機能の1つ。</p><p>※ scroll イベントは、 DOMContentLoaded / load イベント前に発生することもある。</p></section><section id="toc-13"><h3>対象要素が表示領域内に入ったタイミング</h3><pre><code class="language-js">let options = {
root: document.querySelector('#scrollArea'),
rootMargin: '0px',
threshold: 1.0
}
let observer = new IntersectionObserver(callback, options);</code></pre><p>※<a href="https://developer.mozilla.org/ja/docs/Web/API/Intersection_Observer_API">Intersection Observer API - Web API | MDN</a></p></section><section id="toc-14"><h3>beforeunload イベント</h3><ul><li><a href="https://developer.mozilla.org/ja/docs/Web/API/Window/beforeunload_event">Window: beforeunload イベント - Web API | MDN</a></li></ul><p>ページがアンロードされる直前に発火する。そして、ダイアログを表示してページ遷移をキャンセルするか確認できる。</p><p>※ページとユーザーの対話が存在しない場合、例え beforeunload イベントを設定していてもダイアログは表示されません。これは、ユーザーとの対話が存在しない場合、ページ上から失われるデータが存在しないため、ページ遷移をキャンセルする理由そのものがなくなるためです。<br> beforeunload を設定したページをクリックあり・なしでクローズすることでこの動作を確認できます。<br> <a href="https://www.bugbugnow.net/2022/01/beforeunload-dialog.html">beforeunload のダイアログが出現しないことがある</a></p></section><section id="toc-15"><h3>pagehide イベント</h3><p>pagehide イベントは、 unload イベントと類似しています。</p><p>初回の pagehide イベントは、 unload イベントの発動直前に発火します。初回の pagehide イベントは、 <code>persisted</code> に <code>true</code> が設定されています。</p><p>初回以降にページがアンロードされた場合、 pagehide イベントは <code>persisted</code> に <code>false</code> が設定されています。初回以外のページのアンロードは、戻る機能などでページがキャッシュされていた場合に発生します。</p><p>※unload イベントを設定した場合、ページはキャッシュされなくなります。</p></section><section id="toc-16"><h3>unload イベント</h3><ul><li><a href="https://developer.mozilla.org/ja/docs/Web/API/Window/unload_event">unload - Web API | MDN</a></li></ul><p>文書または子リソースがアンロードされるときに発生します。</p><p><code>unload</code> イベントの処理は、実行が完了することが保証されません。そのため、時間のかかる処理は、 <code>pagehide</code> イベントで実行するべきです。</p></section><section id="toc-17"><h3>visibilitychange イベント</h3><p>タブの表示非表示に関するイベントです。</p><p>ウィンドウの別タブに切り替える、または対象タブに戻ってきた場合に発火します。</p><p>※<code>document.visibilityState</code> は、ページの可視性を示します。<br>※<code>document.hidden</code> は、ページが非表示になっているかを示します。</p></section><section id="toc-18"><h3>focus / focusin / focusout / blur イベント</h3><p>フォーカスを取得 / 失った時に発火します。</p><p>※<code>document.activeElement</code> は、現在フォーカスしている要素を返します。<br>※<code>document.hasFocus()</code> は、ドキュメントのフォーカスを判定できます。<br>※<a href="https://www.bugbugnow.net/2020/02/Get-focus-event-of-CORS-restricted-iframe.html">CORS制限付き外部iframeのfocusイベントを取得する</a></p></section><section id="toc-19"><h3>備考</h3><h4>addEventListener の一番最初に処理を実行する</h4><pre><code class="language-js">window.addEventListener('DOMContentLoaded', function() {
// DOMContentLoadedイベントの最初に処理する
}, true);</code></pre><p>キャプチャフェーズに登録することでより早くイベントを実行します。<br><code>window</code> に登録することで他の要素より先にイベントを実行します。</p><p>※同様の手順で登録した場合、前に登録したものがより前に実行される。<br> <code>addEventListener</code> は、登録順の実行が保証される。<br>※<code>DOMContentLoaded</code> 以外でも同様の方法が使用できる。</p><h4>addEventListener の一番最後に処理を実行する</h4><pre><code class="language-js">window.addEventListener('DOMContentLoaded', function() {
window.addEventListener('DOMContentLoaded', function() {
// DOMContentLoadedイベントの最後に処理する
}, false);
}, true);</code></pre><p>※同様の手順で登録した場合、後に登録したものがより後に実行される。<br> <code>addEventListener</code> は、登録順の実行が保証される。<br> キャプチャ・バブリングフェーズ中に同一フェーズのリスナー登録しても実行されない。<br>※<code>DOMContentLoaded</code> 以外でも同様の方法が使用できる。<br> 複数回呼び出されるリスナーの場合、 <code>onece</code> 指定や登録解除処理が必要になる。</p><h4>ページ読み込み直後にページの途中を判定する</h4><p>ウェブページは、基本的に読み込み直後にページ先頭を表示します。ですが、例外的にページの途中を表示することがあります。</p><p>次の操作で読み込み直後にページの途中を表示する。</p><ul><li>リロード時<ul><li>例:ページ途中までスクロールした状態でリロードする</li></ul></li><li>ページ内リンク<ul><li>例:URLにハッシュ(#~:フラグメント識別子)がある</li></ul></li><li>履歴・戻る<ul><li>例:戻るボタンでウェブページを戻る</li></ul></li></ul><hr><pre><span class="pre-code-title">スクロール位置で判定する</span><code class="language-js:スクロール位置で判定する">if (window.performance && !performance.navigation.type && !location.hash) {
// ページ先頭(通常表示のみフラグメント識別子で簡易判定)
} else if (!window.pageYOffset) {
// ページ先頭(スクロール位置で判定)
} else {
// ページ先頭ではない
}</code></pre><p>※<code>window.pageYOffset</code> にアクセスすると Reflow(リフロー)が発生する。<br> そのため、可能な限りアクセスを回避する。</p><hr><pre><span class="pre-code-title">スクロールイベントで判定する</span><code class="language-html:スクロールイベントで判定する"><html>
<script>
var onPageScroll = function() {
if (onPageScroll) {
window.removeEventListener('scroll', onPageScroll);
onPageScroll = 0;
// ページ先頭ではない
}
};
window.addEventListener('scroll', onPageScroll);
window.addEventListener('load', function() {
if (onPageScroll) {
window.removeEventListener('scroll', onPageScroll);
onPageScroll = 0;
// ページ先頭
}
});
</script></code></pre><p>※<code><head></code> 内の埋め込みスクリプトで上記処理を実行する必要がある<br> 初回スクロールイベントより先にスクロールリスナーを登録する必要がある</p><h4>bfcache (Back Forward Cache)</h4><p>ブラウザでページ遷移した場合、 bfcache に保存します。ブラウザで戻る・進むなどでページ遷移した場合、 bfcache からページをロードします。</p><p>bfcache は、ページの状態を JavaScript の実行状態を含めてキャッシュします。</p><p>bfcache への状態遷移には、 freeze / resume イベントが発火します。</p></section>toshihttp://www.blogger.com/profile/02673765704159584610noreply@blogger.com0tag:blogger.com,1999:blog-279447686030876252.post-41609107185638936942021-02-26T04:54:00.008+09:002021-02-27T08:17:08.075+09:00すべてのページで標準のスクロールバーを表示する<script type="application/json" id="post-data-json">{
"title": "すべてのページで標準のスクロールバーを表示する"
,"labels": ["JavaScript", "UserScript", "CSS", "UserCSS", "拡張機能"]
,"url": "https://www.bugbugnow.net/2021/02/default-scrollbar.html"
}</script><section><h3 id="toc-1">はじめに</h3><p>様々なウェブページがあります。この頃、モバイル対応やレスポンシブ対応の結果、デスクトップ環境のサイトでもスクロールバーを非表示にするサイトが出始めました。</p><p>モバイル環境であれば、スクロールバーを非表示にして表示領域を確保するのは良い方法です。ですが、デスクトップ環境では慣れ親しんだGUIとかけ離れた表示をする異質なサイトになってしまいます。どのような意思決定のもとにこのようなサイトが出来上がったのか不思議でなりませんが、存在するものはどうしようもありません。しかたがないので、ユーザ側で対応します。</p><p>具体的には、ブラウザの拡張機能のユーザスタイル・ユーザスクリプトで対応します。</p></section><section><h3 id="toc-2">ユーザスタイル</h3><pre><span class="pre-code-title">default-scrollbar.user.css</span><code class="language-css:default-scrollbar.user.css">/*! default-scrollbar.user.css */
/* ==UserStyle==
@name default-scrollbar
@description Reset scrollbar to default settings.
@author toshi (https://github.com/k08045kk)
@license MIT License
@see https://opensource.org/licenses/MIT
@version 1.0.0
@note 1.0.0 - 20210226 - 初版
@see https://www.bugbugnow.net/2021/02/default-scrollbar.html
@preprocessor default
==/UserStyle== */
html {
overflow-y: auto !important;
scrollbar-color: auto !important;
scrollbar-width: auto !important;
}
body {
overflow-y: visible !important;
}</code></pre><p>※上記のコードをすべてのページに適用する。</p><h4 id="toc-3">概要</h4><p>ページの<code><html></code>にスクロールバーを表示します。また、<code><body></code>のスクロールバーなしにします。それ以外のスクロールバーに関しては、変更しません。これにより、ページのトップレベルのスクロールバーのみ表示可能であれば表示します。</p></section><section><h3 id="toc-4">ユーザスクリプト</h3><pre><span class="pre-code-title">default-scrollbar.user.js</span><code class="language-js:default-scrollbar.user.js">// ==UserScript==
// @name default-scrollbar
// @description Reset all scrollbars to default settings.
// @match *://*/*
// @author toshi (https://github.com/k08045kk)
// @license MIT License
// @see https://opensource.org/licenses/MIT
// @version 0.1
// @note 0.1 - 20210226 - 初版
// @see https://www.bugbugnow.net/2021/02/default-scrollbar.html
// @grant none
// ==/UserScript==
(function() {
// add default-scrollbar.user.css
var style = document.createElement('style');
style.textContent = 'html { overflow-y: auto !important; scrollbar-color: auto !important; scrollbar-width: auto !important; } body { overflow-y: visible !important; } ';
document.documentElement.appendChild(style);
// delete webkit-scrollbar
window.addEventListener('load', function() {
for (var s=document.styleSheets.length; s--; ) {
var sheet = document.styleSheets[s];
for (var i=sheet.rules.length; i--; ) {
var text = sheet.rules[i].selectorText;
if (text && text.indexOf('::-webkit-scrollbar') >= 0) {
sheet.deleteRule(i);
}
}
}
});
})();</code></pre><h4 id="toc-5">解説</h4><p>WebKit系(Safari、Chrome、Edge、etc)のブラウザにはスクロールバー用の疑似セレクターが準備されています。この疑似セレクターを使用することでスクロールバーを自由に変更することができます。</p><p>問題は、この疑似セレクターが自由すぎることです。CSSだけでは元に戻すことができません。そのため、疑似セレクターが使用されていると思しきセレクターをJavaScriptで全削除することで対応しています。</p><p>ただし、問題があります。すべてのスクロールバーを対象にしています。例えば、特定のスクロールバー(主に横スクロールバー)のみを非表示にしていることがあります。その場合、意図した表示ではなくなってしまいます。そのため、ユーザスクリプトに関しては、すべてのスクロールバーを標準に戻す機能だと考えてください。</p><p>※<a href="https://developer.mozilla.org/en-US/docs/Web/CSS/::-webkit-scrollbar">::-webkit-scrollbar - CSS: Cascading Style Sheets | MDN</a></p></section><section><h3 id="toc-6">備考</h3><h4 id="toc-7">特定のドメインを除外する</h4><p><code>stylus</code>は、ユーザスタイルを特定のドメインのみ除外することができます。</p><p>設定方法は、目的のページを開いてブラウザアクションボタンをクリックします。今回のユーザスタイルである<code>default-scrollbar.user.css</code>が表示されるはずです。右端の「︙」ボタンをクリックすると「現在のドメインを除外」の項目が現れるため、チェックすれば特定ドメインのみ除外できます。</p><img alt="特定のドメインを除外する" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgVBD-K_EUzoaIRbkP06UsLWw5XwuNw6m4AV0mZD08Q1mxxCyOOz23MPpHRvmegomg7Df7-O2FC0YD00MPcp3Y6CJb7XQZw7DiphO8BSTCJPWOZdHD_IRtih6aSsnI45R8W_zjjVcFinSl9/s1600/20200221_02.png" loading="lazy" width="260" height="177"><h4 id="toc-8">スモーススクロールを無効化する</h4><pre><span class="pre-code-title">default-scroll-behavior.user.css</span><code class="language-css:default-scroll-behavior.user.css">/*! default-scroll-behavior.user.css */
/* ==UserStyle==
@name default-scroll-behavior
@description Reset scroll behavior to default settings.
Disable smoth scrolling.
@author toshi (https://github.com/k08045kk)
@license unlicense
@version 1.0.0
@note 1.0.0 - 20210227 - 初版
@see https://www.bugbugnow.net/2021/02/default-scrollbar.html
@preprocessor default
==/UserStyle== */
html {
scroll-behavior: auto !important;
}</code></pre></section><section><h3 id="toc-9">参考</h3><ul><li><a href="https://developer.mozilla.org/ja/docs/Web/CSS/scrollbar-color">scrollbar-color - CSS: カスケーディングスタイルシート | MDN</a></li><li><a href="https://developer.mozilla.org/ja/docs/Web/CSS/scrollbar-width">scrollbar-width - CSS: カスケーディングスタイルシート | MDN</a></li><li><a href="https://css-tricks.com/custom-scrollbars-in-webkit/">Custom Scrollbars in WebKit | CSS-Tricks</a></li><li><a href="https://github.com/ubershmekel/default-scrollbar">ubershmekel/default-scrollbar - GitHub</a></li><li><a href="https://superuser.com/questions/380629/is-there-a-way-to-disable-custom-webkit-scrollbars">browser - Is there a way to disable custom webkit scrollbars? - Super User</a></li></ul></section>toshihttp://www.blogger.com/profile/02673765704159584610noreply@blogger.com0tag:blogger.com,1999:blog-279447686030876252.post-38001912043364455082021-02-24T12:47:00.010+09:002024-03-26T10:30:39.514+09:00ユーザースクリプト(UserScript)作成時の覚書<script type="application/json" id="post-data-json">{
"title": "ユーザースクリプト(UserScript)作成時の覚書"
,"labels": ["JavaScript", "UserScript", ""]
,"url": "https://www.bugbugnow.net/2021/02/user-script.html"
}</script><div id="toc"><div id="toc-title">目次</div><div id="toc-content"><ol><li><a class="toc-link" href="#toc-1">ユーザースクリプトとは</a></li><li><a class="toc-link" href="#toc-2">拡張機能の種類</a></li><li><a class="toc-link" href="#toc-3">インストール</a></li><li><a class="toc-link" href="#toc-4">ユーザースクリプトの公開と検索</a></li><li><a class="toc-link" href="#toc-5">デバッグ</a></li><li><a class="toc-link" href="#toc-6">ドキュメント</a></li><li><a class="toc-link" href="#toc-7">メタデータブロック</a></li><li><a class="toc-link" href="#toc-8">実行タイミング(@run-at)</a></li><li><a class="toc-link" href="#toc-9">API(GM関数)</a></li><li><a class="toc-link" href="#toc-10">よくある失敗事例</a></li><li><a class="toc-link" href="#toc-11">簡単なバグフィックス・仕様の揺らぎ</a></li><li><a class="toc-link" href="#toc-12">後方互換性の問題</a></li><li><a class="toc-link" href="#toc-13">ブックマークレット</a></li><li><a class="toc-link" href="#toc-14">備考</a></li></ol></div></div><section id="toc-1"><h3>ユーザースクリプトとは</h3><p>ユーザースクリプトとは、ブラウザの対象ウェブページでユーザ作成のスクリプトを実行する為の仕組みです。元々は、Firefoxのアドオンとして作成されたGreasemonkey上で使用できるスクリプトでした。その後、後続の拡張機能も同様の仕様で実行可能なユーザースクリプトとして一般化しました。</p></section><section id="toc-2"><h3>拡張機能の種類</h3><p>ユーザースクリプト用の拡張機能として、主に次の3種類が開発されています。</p><ul><li><a href="https://github.com/greasemonkey/greasemonkey">Greasemonkey - GitHub</a></li><li><a href="https://github.com/Tampermonkey/tampermonkey">Tampermonkey - GitHub</a></li><li><a href="https://github.com/violentmonkey/violentmonkey">Violentmonkey - GitHub</a></li></ul><p>※AdGuardなどの例外も存在します。</p></section><section id="toc-3"><h3>インストール</h3><ul><li>Firefox<ul><li><a href="https://addons.mozilla.org/ja/firefox/addon/greasemonkey/">Greasemonkey – 🦊 Firefox (ja) 向け拡張機能を入手</a></li><li><a href="https://addons.mozilla.org/ja/firefox/addon/tampermonkey/">Tampermonkey – 🦊 Firefox (ja) 向け拡張機能を入手</a></li><li><a href="https://addons.mozilla.org/ja/firefox/addon/violentmonkey/">Violentmonkey – 🦊 Firefox (ja) 向け拡張機能を入手</a></li></ul></li><li>Chrome<ul><li>Greasemonkey<ul><li>標準機能としてChrome本体と統合</li><li>ただし、Chromeウェブストア以外からのインストール禁止の余波で、使用不可<ul><li>エラーメッセージ「この拡張機能は Chrome Web Storeで提供されていません。知らないうちに追加された可能性があります。」</li><li><a href="https://support.google.com/chrome_webstore/answer/2811969">Chrome によって無効にされた拡張機能 - Chrome ウェブストア ヘルプ</a></li></ul></li></ul></li><li><a href="https://chrome.google.com/webstore/detail/tampermonkey/dhdgffkkebhmkfjojejmpbldmpobfkfo?hl=ja">Tampermonkey - Chrome ウェブストア</a></li><li><a href="https://chrome.google.com/webstore/detail/violentmonkey/jinjaccalgkegednnccohejagnlnfdag">Violentmonkey - Chrome ウェブストア</a></li></ul></li><li>Edge<ul><li><a href="https://microsoftedge.microsoft.com/addons/detail/tampermonkey/iikmkjmpaadaobahmlepeloendndfphd">Tampermonkey - Microsoft Edge Addons</a></li><li><a href="https://microsoftedge.microsoft.com/addons/detail/violentmonkey/eeagobfjdenkkddmbclomhiblgggliao">Violentmonkey - Microsoft Edge Addons</a></li></ul></li><li>Safari<ul><li><a href="https://apps.apple.com/us/app/tampermonkey/id1482490089">Tampermonkey on the Mac App Store</a><ul><li>Mac App Store経由の場合、有料($1.99)<ul><li>(無料アプリでも)AppStoreの登録料が高額なため、致し方ないことです</li></ul></li><li>公式ページから直接ダウンロードする場合、無料<ul><li><a href="https://www.tampermonkey.net/?browser=safari">Tampermonkey • Home</a></li></ul></li></ul></li></ul></li></ul></section><section id="toc-4"><h3>ユーザースクリプトの公開と検索</h3><ul><li><a href="https://greasyfork.org/ja">Greasy Fork</a><ul><li>ユーザースクリプトホスティングサービス</li></ul></li><li><a href="https://openuserjs.org/">OpenUserJS</a></li><li><a href="https://gist.github.com/search?l=JavaScript&o=desc&q=%22==UserScript==%22&s=updated">GitHub Gist</a><ul><li>GitHubでのユーザースクリプトの検索</li></ul></li></ul><p>※ブログ等で公開されていることもあります。<br>※「<code>==UserScript==</code>」「<code>.user.js</code>」等のキーワードで検索できます。</p></section><section id="toc-5"><h3>デバッグ</h3><h4>ログ出力する</h4><ul><li><code>console.log()</code>関数を使用する<ul><li>Webコンソール上に出力する</li></ul></li><li><code>alert()</code>関数を使用する<ul><li>ページ上にアラームを表示する</li></ul></li><li><code>GM_log()</code>関数を使用する(廃止済み)<ul><li>Webコンソール上に出力する</li><li>使用には、明示的な@grant指定が必要です「<code>// @grant GM_log</code>」</li></ul></li></ul><h4>エラー出力を確認する</h4><p>ユーザースクリプトがエラーした場合、Webコンソール上に出力されます。ただし、確実にエラー出力されるとは限りません。</p><h4>デバッガーを使用する</h4><ul><li>Greasemonkey<ul><li>Firefox標準のデバッガーを使用する<ul><li>[開発ツール] > [デバッガー] > [ソースファイル] > [user-script://] > [対象ユーザースクリプト]</li></ul></li></ul></li><li>Tampermonkey<ul><li>ブラウザ標準のデバッガーを使用する<ul><li>[Firefox] > [開発ツール] > [デバッガー] > [Tampermonkey] > [userscripts] > [対象ユーザースクリプト]</li><li>[Chrome] > [開発ツール] > [Sources] > [Tampermonkey] > [userscript.html?name=対象ユーザースクリプト]</li></ul></li><li>スクリプト開始直後にデバッガーを起動する<ul><li>[オプション] > [設定] > [全般]<ul><li>[設定のモード] を [上級者] に設定する</li><li>[スクリプトをデバッグする] を有効にする</li></ul></li></ul></li></ul></li><li>Violentmonkey<ul><li>ブラウザ標準のデバッガーを使用する<ul><li>[Firefox] > [開発ツール] > [デバッガー] > [Violentmonkey] > [対象ユーザースクリプト]</li><li>[Chrome] > [開発ツール] > [Sources] > [Violentmonkey] > [対象ユーザースクリプト]</li></ul></li></ul></li></ul><hr><p>ブラウザ標準デバッガーの使用方法</p><ul><li>ブレークポイント: 行番号をクリック(選択)</li><li>ステップ実行(Play/pause): F8<ul><li>次のブレークポイントまで実行する</li></ul></li><li>ステップオーバー(Step over): F10<ul><li>次の行まで実行する</li></ul></li><li>ステップイン{Step in}: F11<ul><li>関数呼び出し以外、次の行まで実行する。関数呼び出し、呼び出した関数へ入る</li></ul></li><li>ステップアウト(Step out): Shift+F11<ul><li>現在の関数の終端まで実行する</li></ul></li></ul><p>※<a href="https://developer.mozilla.org/ja/docs/Tools/Debugger/How_to">デバッガの使い方 - 開発ツール | MDN</a><br> <a href="https://developer.mozilla.org/ja/docs/Tools/Debugger/How_to/Step_through_code">コードをステップ実行する - 開発ツール | MDN</a></p></section><section id="toc-6"><h3>ドキュメント</h3><ul><li><a href="https://wiki.greasespot.net/Main_Page">GreaseSpot Wiki</a></li><li><a href="https://www.tampermonkey.net/documentation.php">Tampermonkey • Documentation</a></li><li><a href="https://violentmonkey.github.io/">Violentmonkey</a></li></ul></section><section id="toc-7"><h3>メタデータブロック</h3><p>メタデータブロックを記述することでユーザースクリプトにメタ的な情報を付与できます。</p><pre><span class="pre-code-title">メタデータブロックの記述例</span><code class="language-js:メタデータブロックの記述例">// ==UserScript==
// @name スクリプト名
// @description スクリプト説明
// @match *://example.com/*
// @author You
// @version 1.2
// @since 1.0 - 20210416 - 初版
// @since 1.1 - 20210418 - fix 〇〇の修正
// @since 1.2 - 20210420 - △△の機能拡張
// @grant none
// ==/UserScript==</code></pre><h4>主なメタタグ</h4><div class="responsive-table"><table><thead><tr><th>メタタグ</th><th>概要</th></tr></thead><tbody><tr><td>@name</td><td>スクリプト名</td></tr><tr><td>@description</td><td>スクリプトの説明</td></tr><tr><td>@include</td><td>対象のページ</td></tr><tr><td>@match</td><td>対象のページ</td></tr><tr><td>@exclude</td><td>対象外のページ</td></tr><tr><td>@run-at</td><td>実行タイミング</td></tr><tr><td>@grant</td><td>特権の付与</td></tr></tbody></table></div><h4>参考</h4><ul><li><a href="https://wiki.greasespot.net/Metadata_Block">Metadata Block - GreaseSpot Wiki</a></li><li><a href="https://www.tampermonkey.net/documentation.php">Tampermonkey • Documentation</a></li><li><a href="https://violentmonkey.github.io/api/metadata-block/">Metadata Block - Violentmonkey</a></li></ul></section><section id="toc-8"><h3>実行タイミング(@run-at)</h3><p>ユーザースクリプトの実行タイミングは、<code>@run-at</code>で指定できます。</p><div class="responsive-table"><table><thead><tr><th>@run-at</th><th>概要</th></tr></thead><tbody><tr><td>document-start</td><td>スクリプトをできるだけ早く実行する(※)</td></tr><tr><td>document-end</td><td><code>DOMContentLoaded</code>時に実行する(既定)</td></tr><tr><td>document-idle</td><td><code>DOMContentLoaded</code>起動後に実行する</td></tr></tbody></table></div><p>※拡張機能のコンテンツスクリプトは、ウェブページの読み込みより先に実行できます。<code><head></code>を読み込むより早く実行することができます。ただし、WebExtensionAPIにアクセスする場合、非同期処理の影響で最終的に<code><head></code>読み込み中や<code><head></code>より後に実行することになります。そのため、「できるだけ早く実行する」と言う曖昧な表現になります。</p><h4>別のタイミングで実行する</h4><p>ページ読み込み時以外でユーザースクリプトを実行したいことがあります。拡張機能のユーザースクリプト実行タイミミングをこれ以上変更することはできませんが、JavaScriptの各種イベントのタイミングで処理を実行することはできます。</p><p>ユーザースクリプトで使えそうな主なイベント種類を次に示します。</p><ul><li>クリックしたタイミング<ul><li><code>click</code>イベント</li><li>ページ上の既存の要素にイベントを設定することもできます<ul><li>イベント設定のない画像やアイコンに新たなイベントを追加する</li></ul></li><li>要素そのものをユーザースクリプトで挿入することもできます<ul><li>既存の要素間に新たな要素(ボタンなど)を配置する</li><li><code>z-index</code>等で浮遊した要素(ボタンなど)を配置する</li></ul></li><li>ページ全体のクリックイベントを監視することもできます<ul><li><code>window.addEventListener('click', func);</code></li></ul></li></ul></li><li>要素の状態が変化したタイミング<ul><li><code>input/select/change</code>イベント</li></ul></li><li>コピーペーストしたタイミング<ul><li><code>cut/copy/paste</code>イベント</li></ul></li><li>キーボード入力したタイミング<ul><li><code>keydown</code>イベント</li><li>ページ独自のショートカットキーを設定することができます</li></ul></li><li>ジェスチャー操作したタイミング<ul><li><code>mousedown/mousemove/mouseup/draggesture</code>イベント</li><li>ページ独自のジェスチャーを設定することができます</li></ul></li><li>文字列選択したタイミング<ul><li><code>mouseup</code>イベント</li><li><code>window.getSelection();</code></li></ul></li><li>スクロールしたタイミング<ul><li><code>scroll</code>イベント</li><li><code>IntersectionObserver</code></li><li>無限スクロールを設定することができます</li></ul></li><li>DOM要素の変更したタイミング<ul><li><code>MutationObserver</code></li><li>ページ読み込み完了後に動的追加される要素を処理することができます</li></ul></li><li>ページを閉じるタイミング<ul><li><code>pagehide</code>イベント</li><li>データを保存して、次の訪問時に使用することができます<ul><li>データ保存には、<code>window.localStorage / GM.setValue()</code>などが使用できます</li></ul></li></ul></li><li>コンテキストメニューを選択したタイミング<ul><li><code>GM.registerMenuCommand()</code><ul><li>ブラウザアクションのコンテキストメニュー</li></ul></li></ul></li></ul></section><section id="toc-9"><h3>API(GM関数)</h3><div class="responsive-table"><table><thead><tr><th>API</th><th>概要</th></tr></thead><tbody><tr><td>GM.info</td><td>ユーザースクリプトに関する情報</td></tr><tr><td>GM.setValue() / GM.getValue()</td><td>データの保存と取得</td></tr><tr><td>GM.setClipboard()</td><td>クリップボードへの書き込み</td></tr><tr><td>GM.xmlHttpRequest()</td><td>XMLHttpRequestオブジェクトと同様の機能</td></tr><tr><td>unsafeWindow</td><td>ページの<code>window</code></td></tr></tbody></table></div><p>※GM関数は、拡張機能の権限を利用して処理されます。<br> 例:<code>GM.setValue()</code>は、データを Cookie や localStorage とは異なる拡張機能の領域に保持します。<br> 例:<code>GM.xmlHttpRequest()</code>は、CSP や CROS の問題を回避できます。</p><h4>JavaScriptの通常API</h4><ul><li><code>console.log()</code>:コンソールへ出力する</li><li><code>alert() / confirm() / prompt()</code>:ダイアログを表示する</li><li><code>document.querySelector()</code>:要素を取得する</li><li><code>document.elementFromPoint()</code>:マウスの位置にある要素を取得する</li><li>EventListener / Observer:各種実行タイミングを設定する</li><li>WebWorker:ページから隔離されたプロセスで実行する(重い処理用)</li><li>ShadowDOM:対象ページのスタイルに影響を受けない要素を作成する</li></ul><h4>参考</h4><ul><li><a href="https://wiki.greasespot.net/Greasemonkey_Manual:API">Greasemonkey Manual:API - GreaseSpot Wiki</a></li><li><a href="https://www.tampermonkey.net/documentation.php">Tampermonkey • Documentation</a></li><li><a href="https://violentmonkey.github.io/api/gm/">GM_* APIs - Violentmonkey</a></li></ul></section><section id="toc-10"><h3>よくある失敗事例</h3><h4><code>@run-at document-start</code>でドキュメント要素へアクセスする</h4><p><code>document-start</code>のタイミングは、<code>DOMContentLoaded</code>より前のタイミングです。ドキュメントの解析が完了していません。そのため、ドキュメント要素を取得できず、エラー終了や意図しない動作をすることがあります。<code>document-end</code>または、<code>document-idle</code>の指定、もしくは<code>@run-at</code>を未指定とすることで問題を解決できます。</p><p>※対象要素をドキュメントの解析完了後に動的追加している場合、<code>MutationObserver</code>でドキュメント変更を監視して、対象要素が追加されたタイミングで処理を実行する必要があります。</p><h4><code>@include/@match</code>の指定ミス</h4><p><code>@include/@match</code>の指定ミスにより、ユーザースクリプトが実行されない。または、意図しないページで実行される問題が発生します。</p><p>※<code>@include/@match</code>が未指定の場合、「すべてのサイトにユーザースクリプトを適用する」設定になります。</p></section><section id="toc-11"><h3>簡単なバグフィックス・仕様の揺らぎ</h3><h4>@grant</h4><ul><li><code>@grant</code>未指定<ul><li><code>@grant none</code>の動作となる</li></ul></li><li><code>@grant none</code>と<code>@grant GM</code>の重複指定<ul><li><code>@grant none</code>の動作となる</li><li><code>GM</code>関数は使用できない</li></ul></li></ul><h4>@require のローカルファイル指定</h4><ul><li>ローカルファイル(<code>file:</code>)の使用<ul><li>Greasemonkey / Violentmonkey<ul><li>不可</li></ul></li><li>Tampermonkey<ul><li>要設定<ul><li>Chrome設定([ファイルの URL へのアクセスを許可する])</li><li>拡張機能設定([セキュリティ] > [スクリプトによるローカル ファイルへのアクセスを許可する])</li></ul></li></ul></li></ul></li></ul><p>※ローカルファイルへのアクセスは、セキュリティ的にとても危険な機能です。不用意に許可しないことを推奨します。</p><h4>ポート番号(@match)</h4><ul><li>'@match'のポート番号<ul><li>Greasemonkey / Tampermonkey<ul><li>ポート番号ありが動作しない</li></ul></li><li>Violentmonkey<ul><li>ポート番号なしが動作しない(明示的なポート番号指定が必要)</li></ul></li><li>ポート番号あり・なしを併記することで回避できる</li></ul></li></ul><h4>ページのスクリプト無効</h4><p>NoScriptなどを使用してページのスクリプトを無効とした場合、WebWorkerが動作しません。また、FirefoxのTampermonkeyが動作しません。</p><h4>Firefox のコンテナが GM.xmlhttpRequest() に適用されない</h4><p>Firefox でコンテナで表示中のページから <code>GM.xmlhttpRequest()</code> を使用しても、コンテナの Cookie を使用してリクエストを取得できません。</p><p>クロスオリジンの問題がない場合、 <code>XMLHttpRequest</code> を直接使用することで問題を回避できます。</p><p>参考:<a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1670278">https://bugzilla.mozilla.org/show_bug.cgi?id=1670278</a></p></section><section id="toc-12"><h3>後方互換性の問題</h3><h4>2014年6月~:Greasemonkey2.0</h4><ul><li><code>@grant none</code>がデフォルト設定になる<ul><li><code>GM_</code>の関数が<code>@grant</code>指定なしで使用不可になる</li></ul></li><li>ユーザースクリプトの実行がウェブページのJavaScriptから分離される<ul><li>ユーザースクリプト→ウェブページは、unsafeWindowでアクセスが可能</li><li>ウェブページ→ユーザースクリプトは、アクセス不可<ul><li>ただし、<code>cloneInto(), exportFunction(), createObjectIn()</code>を使用すれば可能</li></ul></li></ul></li></ul><p>※詳細:<a href="https://www.greasespot.net/2014/06/greasemonkey-20-release.html">Greasespot: Greasemonkey 2.0 Release</a></p><h4>2017年9月~:Greasemonkey4.0 WebExtension対応</h4><ul><li><code>GM_</code>関数の廃止<ul><li><code>GM.</code>関数への移行</li><li><code>GM_</code>と<code>GM.</code>では関数の仕様が一部異なります<ul><li>同期関数だったものが、非同期関数へ移行しています</li></ul></li></ul></li><li><code>GM_log()</code>関数の廃止<ul><li>代替:<code>console.log()</code></li></ul></li><li><code>GM_addStyle()</code>関数の廃止</li><li><code>GM_registerMenuCommand()</code>関数の廃止<ul><li><code>GM.registerMenuCommand()</code>へ移行<ul><li>v4.11(2021年1月)で復活</li></ul></li></ul></li><li><code>GM_getResourceText()</code>関数の完全な廃止</li></ul><p>※詳細:<a href="https://www.greasespot.net/2017/09/greasemonkey-4-for-script-authors.html">Greasespot: Greasemonkey 4 For Script Authors</a></p></section><section id="toc-13"><h3>ブックマークレット</h3><p>ユーザースクリプトと類似するユーザー作成のスクリプトを実行する為の仕組みとして、ブックマークレットがあります。ユーザースクリプトと比べた場合のブックマークレトの利点と欠点を次に示します。</p><h4>ブックマークレットの利点</h4><p>ブックマークレットは、拡張機能なしで実行できます。拡張機能を利用できないIEなどの古いブラウザ環境で動作します。また、拡張機能が提供されていないモバイル環境でも動作します。</p><p>ブックマークレットは、ブックマークの選択時に実行できます。これは、GUIとしてとてもわかり易い実行タイミングです。(<code>GM.registerMenuCommand()</code>が類似的な機能を提供しています)</p><h4>ブックマークレットの欠点</h4><p>ブックマークレットは、CSP (Content-Security-Policy) 問題に対して無力です。CSPが設定されたページでは、ブックマークレットを起動できなくなります。</p><p>ブックマークレットは、CORS (Cross-Origin Resource Sharing) 問題に対して無力です。ブックマークレットでは、外部リソースの Access-Control-Allow-Origin しだいでダウンロードができません。ユーザースクリプトであれば、<code>GM.xmlHttpRequest()</code>関数を使用することでこの問題を回避できます。</p><p>ブックマークレットは、ページのスクリプト無効環境に対して無力です。NoScriptなどを利用してスクリプトを無効化した環境でブックマークレットは、動作しません。</p></section><section id="toc-14"><h3>備考</h3><h4>ユーザースクリプトの名称</h4><p>ユーザースクリプトの名称は、「<code>スクリプト名.user.js</code>」です。</p><p>※<code>@name</code>の名称に「<code>.user.js</code>」は付けない。<br>※<code>userChrome.js</code>の「<code>スクリプト名.uc.js</code>」と混同してはいけない。<br>※<code>@name</code>は、<code>@name:ja</code>/<code>@name:en</code>と言語毎に個別で指定できる。</p><h4>無名関数で囲む</h4><pre><span class="pre-code-title">無名関数で囲むの例</span><code class="language-js:無名関数で囲むの例">(function() {
// コード
})();</code></pre><p>旧バージョンのGreasemonkeyは、ユーザースクリプトを対象ページに直接書き込むことで、ユーザースクリプトの機能を実現していました。そのため、対象ページ本来のスクリプトと不用意に干渉しないよう無名関数で囲むことが一般的です。</p><p>ですが、現在のGreasemonkeyは、ユーザースクリプトを対象ページ本来のスクリプトとは隔離された環境で実行しています。そのため、無名関数で囲まなかったとしても特段問題が発生することはありません。</p><h4>ユーザースクリプトとコンテキスト</h4><ul><li>ページスクリプト<ul><li>ウェブページのスクリプト<ul><li>WebExtension API にアクセスできません</li></ul></li><li>例:ウェブページへの<code><script></code>を使用したスクリプトの埋め込み処理</li></ul></li><li>コンテンツスクリプト<ul><li>拡張機能のコンテンツスクリプト<ul><li>WebExtension API の一部のみアクセスできる(一定の制約がある)</li></ul></li></ul></li><li>バックグラウンドスクリプト<ul><li>拡張機能のバックグラウンドスクリプト<ul><li>WebExtension API の全体にアクセスできる</li></ul></li></ul></li></ul><p>※<a href="https://developer.mozilla.org/ja/docs/Mozilla/Add-ons/WebExtensions/Content_scripts">コンテンツスクリプト - Mozilla | MDN</a></p><h4>ユーザースタイル(UserCSS)</h4><p>ユーザースクリプトと同様にユーザースタイルも存在します。スクリプトによる動的処理が不要で、CSS的な静的処理だけで問題を解決できる場合、特に有用です。</p><p>使用例として、次のようなものが考えられます。</p><ul><li>特定要素を目立たせる</li><li>特定要素を非表示にする</li><li>ダークモード対応する</li><li>etc</li></ul><h4>ブロッカーのフィルター</h4><p>要素の削除や非表示のみを実現したい場合、ブロッカーのフィルターを利用することもできます。</p><p>ブロッカーは、一般的に「広告ブロッカー」として知られていますが、その用途は広告ブロックだけにとどまりません。対象要素がCSSセレクターで表現可能であれば、ユーザースクリプトやユーザースタイルを記述するよりも簡単にフィルターを記述することができます。</p><ul><li>参考:<a href="https://www.bugbugnow.net/2021/09/ublock-filter.html">uBlock Origin フィルター覚書(書き方・サンプル)</a></li></ul></section>toshihttp://www.blogger.com/profile/02673765704159584610noreply@blogger.com0tag:blogger.com,1999:blog-279447686030876252.post-57472320899710767842021-02-23T18:24:00.004+09:002021-02-24T12:50:05.513+09:00JavaScript疑似プロトコルとは:「javascript:」<script type="application/json" id="post-data-json">{
"title": "JavaScript疑似プロトコルとは:「javascript:」"
,"labels": ["JavaScript", "UserScript", ""]
,"url": "https://www.bugbugnow.net/2021/02/javascript-protocol.html"
}</script><section><h3 id="toc-1">JavaScriptプロトコル</h3><p>「<code>javascript:</code>」で始まるプロトコルです。<br>プロトコルであるため、「<code>http:</code>」や「<code>file:</code>」と同じ位置づけの機能です。</p><p>ただし、デファクトスタンダード(事実上の標準)な機能であり、有効なURIスキームではありません。そのため、IANA(インターネット資源のグローバルな管理する組織)は「<code>javascript:</code>」をURIとして管理していません。</p><p>そのため、「javascript疑似プロトコル」と言うのが一般的です。</p><p>※<a href="https://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml">Uniform Resource Identifier (URI) Schemes</a></p></section><section><h3 id="toc-2">主な使用用途</h3><h4 id="toc-3">アドレスバーからJavaScriptを使用する</h4><p>ブラウザのアドレスバーにjavascript疑似プロトコルを入力すると、表示中のページでJavaScriptを実行できます。</p><pre><code class="language-html">javascript:alert('Hello World')
javascript:/* 処理 */</code></pre><p>※最新のブラウザは、アドレスバーにjavascript疑似プロトコルをコピーすると先頭の「<code>javascript:</code>」が自動的に削除されます。セキュリティ対策の一環だと考えられます。<br> 「<code>javascript:</code>」を直接の手入力することで回避できます。<br>※Firefox63からアドレスバーでのJavaScript実行は無効化されました。</p><h4 id="toc-4">ブックマークからJavaScriptを使用する</h4><p>ブラウザのブックマーク(お気に入り)機能のURL欄にjavascript疑似プロトコルを設定することで、ブックマークを選択したタイミングで表示中のページにJavaScriptを挿入することができます。このことをブックマークレットと言います。</p><h4 id="toc-5"><code><a href></code>でJavaScript実行する</h4><p>javascript疑似プロトコルを利用してa要素のhref属性でスクリプトを実行します。</p><pre><code class="language-html"><a href="javascript:alert('Hello World')">click</a>
<a href="javascript:/* 処理 */;void(0);">click</a>
<a href="javascript:void((function(){/* 処理 */})())">click</a></code></pre><p>※応答(最後の処理)で<code>undefined</code>を返すように設計してください。<br> <code>undefined</code>以外を応答すると、ブラウザは応答内容を表示します。</p></section><section><h3 id="toc-6">備考(href属性の無効化)</h3><p>a要素は、それだけではリンクとして機能しません。a要素にhref属性がある場合、リンクとして機能します。リンクは、クリック後にリンク先ページに移動してしまいます。ですが、リンクをクリックしても動作のみを行い、ページ移動はしないでほしいことがあります。リンクをボタンとして利用したいことがあります。</p><h4 id="toc-7"><code><a href="javascript:void(0)"></code></h4><pre><code class="language-html"><a href="javascript:void(0)">click</a></code></pre><p><code>void(0)</code>は、<code>undefined</code>を返します。a要素のhref属性は、<code>undefined</code>が指定された場合何もしません。これにより、リンク機能自体を無効化することができます。</p><p>※<code>javascript:undefind</code>としない理由<br> <code>undefind</code>は、グローバル変数であるため、書き換えが可能なためです。<br> <code>void(0)</code>は、<code>undefined</code>が保証されるため、一般的に使用されます。<br> <code>void</code>演算子は、他の使用方法もありますが、一般的に<code>void(0)</code>が使用されます。</p><h4 id="toc-8">クリックイベントのキャンセル</h4><p>クリックイベントをキャンセルすることで親要素(クリックされたのは、子のテキストノード)であるa要素へのイベント伝搬を阻止し、リンク機能を無効化できます。</p><p>クリックイベントのキャンセルは、「<code>onclick</code>を<code>return false;</code>」するか「<code>event.preventDefault()</code>」でキャンセルすることができます。</p></section><section><h3 id="toc-9">備考(href属性のリンク先)</h3><pre><span class="pre-code-title">表示中のページへのリンク</span><code class="language-html:表示中のページへのリンク"><a href="">click</a></code></pre><pre><span class="pre-code-title">表示中のページ先頭へのリンク</span><code class="language-html:表示中のページ先頭へのリンク"><a href="#">click</a></code></pre></section><section><h3 id="toc-10">備考(CSP問題)</h3><blockquote><p>コンテンツセキュリティポリシー (CSP) は、クロスサイトスクリプティング (XSS) やデータインジェクション攻撃などのような、特定の種類の攻撃を検知し、影響を軽減するために追加できるセキュリティレイヤーです。これらの攻撃はデータの窃取からサイトの改ざん、マルウェアの拡散に至るまで、様々な目的に用いられます。</p><p><a href="https://developer.mozilla.org/ja/docs/Web/HTTP/CSP">https://developer.mozilla.org/ja/docs/Web/HTTP/CSP</a></p></blockquote><p>CSP対策を施したウェブページでは、外部からのスクリプト実行を制限できるようになります。これにより、ウェブページは、ブックマークレットからのJavaScriptの注入を受け付けなくなります。</p><h4 id="toc-11">CSP問題を回避する</h4><p>CSP問題を回避する方法は、ブラウザ拡張機能を使用することです。</p><p>ブックマークレットは、ページにスクリプトを注入します。そのため、ページスクリプトの権限で動作します。</p><p>ですが、ブラウザ拡張機能は、バックグラウンドスクリプト又は、コンテンツスクリプトの権限でスクリプトを実行することができます。このため、ブラウザ拡張機能であれば、CSP問題を回避できます。</p><p>ブラウザ拡張機能には、ユーザスクリプトを実行できる拡張機能が存在します。ユーザスクリプトは、ウェブページの読み込み時に実行されるスクリプトでブックマークレットと同様に使用できます。ただし、ユーザスクリプトでは実行タイミングを「ブックマーク選択時」とすることができません。</p></section><section><h3 id="toc-12">資料</h3><ul><li><a href="https://html.spec.whatwg.org/multipage/browsing-the-web.html#javascript-protocol">7.10.1 Navigating across documents | WHATWG</a></li><li><a href="https://www.w3.org/wiki/Graceful_degradation_versus_progressive_enhancement">Graceful degradation versus progressive enhancement - W3C Wiki</a></li><li><a href="https://docs.microsoft.com/en-us/previous-versions/aa767736(v=vs.85)">javascript Protocol | Microsoft Docs</a></li></ul></section><section><h3 id="toc-13">参考</h3><ul><li><a href="http://www.koikikukan.com/archives/2015/03/11-002222.php">javascript:void(0)のまとめ: 小粋空間</a></li><li><a href="https://uxmilk.jp/47058">javascript void(0) とは | UX MILK</a></li><li><a href="https://ja.railstoolkit.com/firefox-blocks-javascript-address-bar-default">FirefoxはデフォルトでアドレスバーのJavaScriptをブロックします | 記事</a></li></ul></section>toshihttp://www.blogger.com/profile/02673765704159584610noreply@blogger.com0tag:blogger.com,1999:blog-279447686030876252.post-65523479915010078042021-02-16T15:35:00.001+09:002021-02-16T15:36:02.787+09:00analytics.jsのブロックを検出する<script type="application/json" id="post-data-json">{
"title": "analytics.jsのブロックを検出する"
,"labels": ["JavaScript", "Analytics", ""]
,"url": "https://www.bugbugnow.net/2021/02/detect-blocks-in-ga.html"
}</script><section><h3 id="toc-1">はじめに</h3><p>GoogleAnalyticsをブロックされている場合、問題が発生することがあります。ブロックされた場合、機能制限をかけるためにブロックを検出する処理について考えます。</p></section><section><h3 id="toc-2">ブロックパターン</h3><ul><li><code>analytics.js/tags.js</code>の読み込みブロック</li><li><code>Measurement Protocol</code>の通信ブロック<ul><li><code>https://www.google-analytics.com/collect</code>への通信をブロック</li></ul></li><li>公式のオプトアウト機能を利用したブロック<ul><li><a href="https://tools.google.com/dlpage/gaoptout?hl=ja">https://tools.google.com/dlpage/gaoptout?hl=ja</a></li></ul></li></ul><p>※主なブロックフィルター<br> <a href="https://github.com/easylist/easylist/blob/master/easyprivacy/easyprivacy_general.txt">EasyPrivacy</a></p></section><section><h3 id="toc-3">判定する</h3><h4 id="toc-4">analytics.jsの読み込み判定</h4><pre><code class="language-js">function isloadedGoogleAnalytics() {
// GAオブジェクトが存在する && 初期化済み
return window.ga && ga.loaded;
}</code></pre><p>この方法は、analytics.jsの読み込みを判定します。</p><p>※<code>ga.create</code>の有無で判定する方法もある。<br>※<code>ga.q</code>の有無で判定する方法もある。</p><h4 id="toc-5"><code>Measurement Protocol</code>の通信ブロック</h4><p>GoogleAnalyticsのブロックを判定します。GoogleAnalyticsの上位APIである<code>Measurement Protocol</code>を使用してデータ収集を試み、データ収集の成功失敗を元にしてブロックを判定します。成功の判定は、GIFファイルの応答を元に確認します。</p><pre><code class="language-js">// GoogleAnalyticsのブロックを判定する
function canGoogleAnalytics(async) {
async = async === true;
const gif = [71,73,70,56,57,97,1,0,1,0,128,255,0,255,255,255,0,0,0,44,0,0,0,0,1,0,1,0,0,2,2,68,1,0,59];
const can = function(resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.responseType = async ? 'arraybuffer' : '';
xhr.open('POST', 'https://www.google-analytics.com/collect', async);
if (async) {
xhr.onload = function() {
const array = new Uint8Array(xhr.response);
resolve((200 <= xhr.status && xhr.status < 300 || xhr.status === 304)
&& gif.length == array.length
&& gif.toString() == array.toString());
};
xhr.onabort = function() { resolve(false); };
xhr.onerror = function() { resolve(false); };
xhr.ontimeout = function() { resolve(false); };
try {
xhr.send(null);
} catch (e) { resolve(false); }
} else {
try {
xhr.send(null);
const array = Uint16Array.from(xhr.responseText.split(''), c => c.charCodeAt(0));
return (200 <= xhr.status && xhr.status < 300 || xhr.status === 304)
&& gif.length == array.length
&& gif.slice(0, 10).toString() == array.slice(0, 10).toString();
// 補足:データの一部が`�`(65533)に置換されてしまうため、長さと前方のみ確認する。
// 非同期処理を使用することを推奨する。
} catch (e) { return false; }
}
};
return async ? new Promise(can) : can();
};
console.log('async', await canGoogleAnalytics(true));
console.log('sync', canGoogleAnalytics(false));</code></pre><p>※非同期処理の使用を推奨します。<br>※GIFファイル応答の仕様に依存する。<br>※GIFファイル応答まで偽装された場合は、すり抜ける。</p><h4 id="toc-6">公式のオプトアウト機能を利用したブロック</h4><p>公式のオプトアウト機能の判定方法は、不明です。上記の方法で判定できないことまでは確認しましたが、判定する方法を発見することはできませんでした。</p></section><section><h3 id="toc-7">備考</h3><h4 id="toc-8">hitCallbackによる判定</h4><pre><code class="language-js">ga('send', 'event', 'outbound', 'click', url, {
'transport': 'beacon',
'hitCallback': function() { console.log('send'); }
});</code></pre><p><code>hitCallback</code>は、ヒットの送信が完了したタイミングで通知されます。そのため、通知を待機することでデータ収集を確認できそうです。実際には、データを収集できません。ですが、コールバック関数はコールされます。これは、通信遮断による問題です。送信は完了しましたが、データはサーバに到達しなかっただけの問題です。</p><h4 id="toc-9">/debug/collect</h4><ul><li><code>https://www.google-analytics.com/debug/collect</code></li></ul><p>上記のURLを使用することもできます。テスト用のAPIでJSONを応答します。ただし、パスが<code>/debug/collect</code>になるため、判定をすり抜ける可能性があります。</p><h4 id="toc-10">GoogleAnalyticsでのブロック回避</h4><blockquote><p>お客様は本サービスに含まれるプライバシー機能(オプトアウトなど)を一切回避してはなりません。</p></blockquote><ul><li><a href="https://marketingplatform.google.com/about/analytics/terms/jp/">https://marketingplatform.google.com/about/analytics/terms/jp/</a></li></ul><hr><p>上記の通り、GoogleAnalyticsではブロック回避をすることは禁止されています。ブロックを回避してデータを収集してはいけません。ただし、ページの機能制限まで禁止されているわけではありません。</p><h4 id="toc-11">Googlebot</h4><p>Googlebotは、GoogleAnalyticsをブロックします。GoogleAnalytics無効時に単純にコンテンツを非表示等にするとGoogle検索からサイトが消える可能性があります。確認には次の記事が参考になります。</p><ul><li><a href="https://developers.google.com/search/docs/advanced/crawling/verifying-googlebot?hl=ja&visit_id=637483728291548327-4152137069&rd=1">Googlebot かどうかの確認 | Google 検索セントラル | Google Developers</a></li><li><a href="https://developers.google.com/search/docs/advanced/crawling/overview-google-crawlers?hl=ja">Google クローラの概要(ユーザー エージェント) | Google 検索セントラル | Google Developers</a></li></ul></section><section><h3 id="toc-12">参考</h3><ul><li><a href="https://qiita.com/qrusadorz/items/fed7d97e6133400fb591">Google Analyticsのブロックを検知する方法 -通信編- - Qiita</a></li><li><a href="https://dev.mozilla.jp/2016/04/google-analytics-privacy-and-event-tracking/">Google アナリティクスとプライバシー・イベントトラッキング | Mozilla Developer Street (modest) アーカイブ</a></li></ul></section>toshihttp://www.blogger.com/profile/02673765704159584610noreply@blogger.com0tag:blogger.com,1999:blog-279447686030876252.post-39324585872031542262021-02-08T12:50:00.005+09:002023-09-04T11:56:18.948+09:00JavaScript のグローバル変数未定義エラーの回避方法<section id="toc-1"><h3>はじめに</h3><p>JavaScript では、未定義のグローバル変数へのアクセスでエラーを出力します。ここでは、エラーを回避して、未定義を判定する方法を考えます。</p></section><section id="toc-2"><h3>失敗例</h3><p>下記のコード例では、グローバル変数の未定義エラーで失敗します。</p><pre><code class="language-js">if (a) {
console.log('OK');
} else {
console.log('NG');
}
// Uncaught ReferenceError: a is not defined</code></pre><hr><pre><code class="language-js">//var a = undefined;
if (a !== undefined) {
console.log('OK');
} else {
console.log('NG');
}
// Uncaught ReferenceError: a is not defined</code></pre><p>※<code>a = undefined;</code>を事前に設定した場合、意図した動作となる。<br> その場合、根本的に未定義ではなくなる。</p></section><section id="toc-3"><h3>回避策</h3><h4>window.property</h4><pre><code class="language-js">if (window.a) {
console.log('OK');
} else {
console.log('NG');
}</code></pre><p>グローバル変数に直接アクセスせずに、グローバル変数に間接的にアクセスします。</p><p>※変数が「<code>undefined</code>」「<code>null</code>」「<code>false</code>」「<code>0</code>」「<code>""</code>」の場合、誤判定する。<br> 完全ではないがほとんどの場合、「<code>window.a</code>」「<code>!window.a</code>」でこと足りる。<br>※<code>window</code>が存在しない環境の場合は、「window 以外のグローバル変数」を参照</p><h4>in window</h4><pre><code class="language-js">if ('a' in window) {
console.log('OK');
} else {
console.log('NG');
}</code></pre><p><code>in</code>演算子で指定したプロパティがオブジェクトに含まれるか判定します。</p><p>※<a href="https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/in">in 演算子 - JavaScript | MDN</a><br>※<code>window</code>が存在しない環境の場合は、「window 以外のグローバル変数」を参照</p><h4>typeof</h4><pre><code class="language-js">if (typeof(a) !== 'undefined') {
console.log('OK');
} else {
console.log('NG');
}</code></pre><p><code>typeof</code>演算子でオペランドの型名を判定します。</p><p>※<a href="https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/typeof">typeof - JavaScript | MDN</a><br>※<code>typeof(a) === void 0</code>では動作しない。<br> <code>typeof(a)</code>は、<code>undefined</code>のプリミティブ値ではなく、文字列を出力する。</p><h4>try/catch</h4><pre><code class="language-js">try {
a;
console.log('OK');
} catch (e) {
console.log('NG');
}</code></pre><p>エラーが発生することを逆に利用して、 try/catch で捕獲します。</p></section><section id="toc-4"><h3>備考</h3><h4>window 以外のグローバル変数</h4><p>上記の例でグローバル変数に<code>window</code>を使用しています。ですが、グローバル変数が<code>window</code>でないことがあります。<code>window</code>を使用せずにグローバル変数を取得する方法は、次の通りです。</p><pre><code class="language-js">var global = globalThis;
var global = Function('return this')();</code></pre><p>※<a href="https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/globalThis">globalThis - JavaScript | MDN</a><br>※ Function を利用した方法は、最新環境で CSP 絡み制約を受けることがあります。</p><h4>window 自身の有無を判定する</h4><pre><code class="language-js">if ('window' in globalThis) {
console.log('OK');
} else {
console.log('NG');
}</code></pre></section>toshihttp://www.blogger.com/profile/02673765704159584610noreply@blogger.com0tag:blogger.com,1999:blog-279447686030876252.post-75600329564887995912021-02-03T14:41:00.003+09:002021-03-26T12:40:42.068+09:00First Paint (FP) をJavaScriptで検出する<script type="application/json" id="post-data-json">{
"title": "First Paint (FP) をJavaScriptで検出する"
,"labels": ["JavaScript", "Web Vitals", ""]
,"url": "https://www.bugbugnow.net/2021/02/first-paint.html"
}</script><div id="toc"><div id="toc-title">目次</div><div id="toc-content"><ol><li><a class="toc-link" href="#toc-1">はじめに</a></li><li><a class="toc-link" href="#toc-2">FirstPaintの時間を取得する</a></li><li><a class="toc-link" href="#toc-3">タイミングを取得する:requestAnimationFrame()</a></li><li><a class="toc-link" href="#toc-5">タイミングを取得する:PerformanceObserver</a></li><li><a class="toc-link" href="#toc-7">タイミングを取得する:performance.getEntriesByType('paint')</a></li><li><a class="toc-link" href="#toc-9">実測</a></li></ol></div></div><section><h3 id="toc-1">はじめに</h3><p>FirstPaintの時間を取得することは容易に実現できます。ですが、FirstPaintが発生したタイミングを取得するのは容易ではありません。ここでは、筆者が調査した結果を記します。ですが、まだ完全な方法を発見することはできていません。同じ疑問を持った人の助けになれることを期待しています。(できるのならば、あなたが発見した方法を教えて下さい)</p></section><section><h3 id="toc-2">FirstPaintの時間を取得する</h3><pre><code class="language-js">// FP/FCP (First Paint / First Contentful Paint)
new PerformanceObserver(function(entryList) {
for (const entry of entryList.getEntries()) {
console.log(entry.name, entry.startTime, entry);
}
}).observe({type:'paint', buffered:true});
// LCP (Largest Contentful Paint)
new PerformanceObserver(function(entryList) {
for (const entry of entryList.getEntries()) {
console.log(entry.entryType, entry.startTime, entry);
}
}).observe({type:'largest-contentful-paint', buffered:true});</code></pre><p>※<code>PerformanceObserver</code>から取得する<br> <a href="https://developer.mozilla.org/ja/docs/Web/API/PerformanceObserver/PerformanceObserver">PerformanceObserver() - Web API | MDN</a><br>※<code>buffered:true</code>は、登録前に対象の通知が発生済みの場合、バッファーに予め保存されている通知で発火します。<br> 登録のタイミングに関わらずFP/FMP/LCPの発生時間を取得できます。</p></section><section><h3 id="toc-3">タイミングを取得する:requestAnimationFrame()</h3><pre><code class="language-js">requestAnimationFrame(function() {
console.log('before first paint');
// ...
requestAnimationFrame(function() {
console.log('after first paint');
// ...
});
});</code></pre><p>※上記コードを<code><head></code>内の<code><script></code>に直接記述(同期読み込み)する<br>※<code>requestAnimationFrame()</code>は、次の描画処理の直前に呼び出されます<br> <a href="https://developer.mozilla.org/ja/docs/Web/API/Window/requestAnimationFrame">Window.requestAnimationFrame() - Web API | MDN</a></p><h4 id="toc-4">理屈</h4><p>この方法は、不完全です。類推や仮定含みます。</p><p>まず、<code><head></code>内の<code><script></code>同期読み込みのタイミングは、FirstPaintより前です。</p><p>そして、<code>requestAnimationFrame()</code>は、次の描画処理の直前に呼び出されます。なので、仮定として、1回目の<code>requestAnimationFrame()</code>は、FirstPaint直前のタイミングです。2回目の<code>requestAnimationFrame()</code>はFirstPaint直後のタイミングです。ただし、実際はこの仮定は正しくありません。または、正しくないように見えます。</p><p>上記の方法で、FirstPaintの時間とFirstPaint前後のタイミングを取得します。すると、FirstPaintの時間がFirstPaint前後の時間よりも更に遅い時間を取得します。これは、1回目の<code>requestAnimationFrame()</code>後に描画しなかったことを意味するものと考えられます。このことから、2回目の<code>requestAnimationFrame()</code>時にFirstPaintが発生済みであると言う仮定が崩れます。また、1回目の<code>requestAnimationFrame()</code>直後にFirstPaint(画面上への表示)が発生しなかったこと意味します。</p><p>よって、この方法は正しくありません。ですが、次の方法よりもこのページを見に来たあなたの要望に答えるものであることを私は確信しています。</p></section><section><h3 id="toc-5">タイミングを取得する:PerformanceObserver</h3><pre><code class="language-js">new PerformanceObserver(function(list) {
console.log('after first paint');
// ...
}).observe({type:'paint'});</code></pre><p>※上記コードを<code><head></code>内の<code><script></code>に直接記述(同期読み込み)する<br>※<a href="https://developer.mozilla.org/ja/docs/Web/API/PerformanceObserver/PerformanceObserver">PerformanceObserver() - Web API | MDN</a></p><h4 id="toc-6">理屈</h4><p>この方法は、特に不思議なことは何もありません。FirstPaintの時間を取得する通知用のコールバック関数は、間違いなくFirstPaint後に発生します。ただし、FirstPaint直後に発生することを保証するものではありません。</p><p>このページを見に来たあなたの要望に答えるものでないことは容易に想像できます。</p><p>※もしも、FP/FCPが別に通知される場合、間違ってFCPで発火する可能性があります。<br> また、2回発火する可能性があります。</p></section><section><h3 id="toc-7">タイミングを取得する:performance.getEntriesByType('paint')</h3><pre><code class="language-js">var func = function() {
if (!performance.getEntriesByType('paint').length) {
requestAnimationFrame(func);
} else {
console.log('after first paint');
// ...
}
};
requestAnimationFrame(func);</code></pre><p>※上記コードを<code><head></code>内の<code><script></code>に直接記述(同期読み込み)する<br>※<a href="https://developer.mozilla.org/ja/docs/Web/API/Performance/getEntriesByType">performance.getEntriesByType() - Web API | MDN</a></p><h4 id="toc-8">理屈</h4><p><code>performance.getEntriesByType('paint')</code>が値を返すようになった時、FirstPaintが完了したと言う仮定です。</p><p>この仮定は、<code>PerformanceObserver</code>の方法とほとんど同じ結果を出力します。<code>PerformanceObserver</code>が通知される直前や直後のタイミングを取得できます。よって、ここでは<code>PerformanceObserver</code>と同じとして扱います。</p></section><section><h3 id="toc-9">実測</h3><img alt="requestAnimationFrame" width="860" height="438" loading="lazy" decoding="async" src="https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEg8Mxj6_e8vsBTW8bu81R1PTIli2fssk7ZBaTHlY2GgxiJLtg4CYh5pSbLJDiRuBC0BZbeSnku8NnsywULXBzUuSlUhJSlEFM7DnwCD22GfUCrUlFrf-kbsPH8xNhp7qgO9HL6ulL8eWq6v/s0/20210203_02_requestAnimationFrame.png"><p>上記の画像は、当サイトのトップページに対して行った計測結果です。画像からはわかりにくいですが、次のことが発生しています。</p><ol><li>Parse HTML処理</li><li>Recalculate Style処理</li><li>1回目の<code>requestAnimationFrame()</code></li><li>Layout処理</li><li>Paint処理</li><li>2回目~n-1回目の<code>requestAnimationFrame()</code><ul><li>n-1回目ではじめて<code>!!performance.getEntriesByType('paint').length</code>が<code>true</code>を返す</li></ul></li><li>FirstPaintの発生</li><li>n回目の<code>requestAnimationFrame()</code></li><li><code>PerformanceObserver</code>の通知</li></ol><p>このことから、Layout+Paint処理を挟む形で1回目と2回目の<code>requestAnimationFrame()</code>が発生していることが伺えます。これは、不正確ではあるものの、FirstPaintに近いタイミングを取得できているものと考えます。</p><p>※リロード毎に結果は、異なりますがほとんど同じ結果になります</p><h4 id="toc-10">テストコード</h4><pre><code class="language-js">(function() {
'use strict';
var logs = [];
var isPaint, isLoad;
var fp = function() {
logs.push(['animetion-frame', performance.now(), !!performance.getEntriesByType('paint').length]);
if (isPaint && isLoad) {
setTimeout(function() {
logs.forEach(function(log) {
console.log.apply(this, log);
});
}, 100);
} else {
requestAnimationFrame(fp);
}
}
requestAnimationFrame(fp);
new PerformanceObserver((list) => {
logs.push(['PerformanceObserver', performance.now()]);
for (const entry of list.getEntries()) {
logs.push([entry.name || entry.entryType, entry.startTime, entry.duration]);
}
isPaint = true;
}).observe({entryTypes:['paint', 'largest-contentful-paint']});
window.addEventListener('DOMContentLoaded', function() {
logs.push(['DOMContentLoaded', performance.now()]);
});
window.addEventListener('load', function() {
logs.push(['load', performance.now()]);
isLoad = true;
});
})();</code></pre><p>※上記コードを<code><head></code>内の<code><script></code>に直接記述(同期読み込み)する</p><h4 id="toc-11">出力結果</h4><pre><code class="language-txt">DOMContentLoaded 149.12999999069143
animetion-frame 304.374999992433 false
load 452.4949999904493
animetion-frame 458.31999999063555 false
animetion-frame 486.1499999969965 false
animetion-frame 502.424999998766 true
PerformanceObserver 508.4399999905145
largest-contentful-paint 461.725 0
first-paint 461.72500000102445 0
first-contentful-paint 461.72500000102445 0
animetion-frame 512.2449999907985 true</code></pre><p>※Chromeのプライベートウィンドウ・CPU 6x slowdownの結果<br>※いろいろあって、画像とは別で取得した結果</p><h4 id="toc-12">その他サイトの結果</h4><pre><code class="language-txt">https://www.google.com/
animetion-frame 188.95500000508036 false
animetion-frame 214.13000000757165 true
DOMContentLoaded 216.8450000026496
PerformanceObserver 217.8950000088662
first-paint 195.3300000022864 0
largest-contentful-paint 195.334 0
first-contentful-paint 195.33499999670312 0
animetion-frame 223.31500001018867 true
animetion-frame 256.4450000063516 true
animetion-frame 261.20999999693595 true
animetion-frame 281.7300000024261 true
PerformanceObserver 283.53000000061
largest-contentful-paint 252.64 0
animetion-frame 286.3050000014482 true
animetion-frame 342.22500000032596 true
load 342.8950000088662
animetion-frame 384.3849999975646 true
https://github.co.jp/
animetion-frame 116.95999999938067 false
animetion-frame 182.34499999380205 false
animetion-frame 194.27499998710118 true
PerformanceObserver 197.0149999979185
largest-contentful-paint 163.925 0
first-paint 163.92500000074506 0
first-contentful-paint 163.92500000074506 0
DOMContentLoaded 212.27499999804422
load 212.70999999251217
https://m.yahoo.co.jp/
animetion-frame 280.48000000126194 false
animetion-frame 301.1100000003353 true
PerformanceObserver 310.2249999938067
first-paint 284.6750000026077 0
animetion-frame 312.56000000576023 true
PerformanceObserver 315.4250000079628
first-contentful-paint 307.9049999942072 0
largest-contentful-paint 307.91 0
animetion-frame 335.5500000034226 true
animetion-frame 356.40499999863096 true
animetion-frame 407.83500000543427 true
animetion-frame 414.6349999937229 true
animetion-frame 420.07000000739936 true
PerformanceObserver 421.5799999947194
largest-contentful-paint 345.774 0
animetion-frame 428.2249999960186 true
DOMContentLoaded 439.81999999959953
animetion-frame 652.4700000009034 true
animetion-frame 760.9550000051968 true
animetion-frame 823.4050000028219 true
animetion-frame 857.8799999959301 true
animetion-frame 873.5400000005029 true
PerformanceObserver 924.1500000061933
largest-contentful-paint 828.28 0
animetion-frame 928.1199999968521 true
... 22個省略
animetion-frame 1345.6200000073295 true
load 1351.9050000031712
animetion-frame 1362.5949999986915 true</code></pre><p>※上記のテストコードをUserScriptの<code>@run-at document-start</code>で実行した結果</p></section>toshihttp://www.blogger.com/profile/02673765704159584610noreply@blogger.com0tag:blogger.com,1999:blog-279447686030876252.post-31561864240015715212021-01-28T08:40:00.010+09:002021-03-26T12:40:35.493+09:00Cumulative Layout Shift (CLS) をJavaScriptで検出する<script type="application/json" id="post-data-json">{
"title": "Cumulative Layout Shift (CLS) をJavaScriptで検出する"
,"labels": ["JavaScript", "Web Vitals", ""]
,"url": "https://www.bugbugnow.net/2021/01/layout-instability-api.html"
}</script><section><h3 id="toc-1">はじめに</h3><p>これまで、DevToolsのPerformanceタブからレイアウトシフトを確認していました。ですが、これだと目視する必要がある上に自動化できません。調べてみると、Layout Instability APIが見つかったのでそれの覚書です。</p><p>※PageSpeed Insightsでも確認できますが、ページ読み込み時のみです。<br> レイアウトシフトの計測期間は、ページのライフサイクル全体であるため、ページ読み込み時のみでは不足です。</p></section><section><h3 id="toc-2">CLSの検出コード</h3><pre><code class="language-js">let cls = 0;
new PerformanceObserver((entryList) => {
for (const entry of entryList.getEntries()) {
if (!entry.hadRecentInput) { // 500ms以内にユーザー入力が発生していない
cls += entry.value; // レイアウトシフトを累積する
console.log(new Date().toISOString(), entry.entryType, cls, entry.value, entry);
} else {
// ユーザー入力によるレイアウトシフト
//console.log(new Date().toISOString(), entry.entryType, 'user input', cls, entry.value, entry);
}
}
}).observe({type:'layout-shift', buffered:true});</code></pre><p>※<code>entry.sources</code>で移動要素の領域の変化を確認できます</p></section><section><h3 id="toc-3">対応状況</h3><ul><li><a href="https://caniuse.com/mdn-api_layoutshift">LayoutShift API | Can I use...</a></li></ul><p>2021年1月現在は、ChromeとEdgeなどのChromium系のブラウザが対応済みです。</p></section><section><h3 id="toc-4">思いつく使用用途</h3><ul><li>ブックマークレットなどによる手動での確認</li><li>ローカル環境の自動テスト</li><li>ページ解析(google analyticsなど)でのCSLの収集</li></ul></section><section><h3 id="toc-5">参考</h3><ul><li><a href="https://web.dev/cls/">Cumulative Layout Shift (CLS)</a></li><li><a href="https://developer.mozilla.org/ja/docs/Web/API/PerformanceObserver">PerformanceObserver - Web API | MDN</a></li><li><a href="https://developer.mozilla.org/en-US/docs/Web/API/LayoutShift">LayoutShift - Web APIs | MDN</a></li><li><a href="https://wicg.github.io/layout-instability/">Layout Instability API</a></li></ul></section>toshihttp://www.blogger.com/profile/02673765704159584610noreply@blogger.com0tag:blogger.com,1999:blog-279447686030876252.post-21650524623675071592021-01-19T16:55:00.005+09:002021-01-19T17:08:03.830+09:00JavaScriptでマウスの位置にある要素を取得する<script type="application/json" id="post-data-json">{
"title": "JavaScriptでマウスの位置にある要素を取得する"
,"labels": ["JavaScript", "忘却録", ""]
,"url": "https://www.bugbugnow.net/2021/01/element-from-point.html"
}</script><p>面白そうな関数なので覚書<br>φ(..)メモメモ...</p><section><h3 id="toc-1">document.elementFromPoint()</h3><pre><code class="language-js">var x = 100;
var y = 100;
var element = document.elementFromPoint(x, y);</code></pre><pre><code class="language-js">document.addEventListener('click', function(event) {
var x = event.clientX;
var y = event.clientY;
var element = document.elementFromPoint(x, y);
console.log(element);
});</code></pre><h4 id="toc-2">備考</h4><ul><li>要素が重なっている場合、最前面の要素を取得します</li><li><code>pointer-events: none;</code>の要素は無視されます</li><li>指定要素の親要素が<code><iframe></code>の場合、<code><iframe></code>を取得します</li><li>重なっている要素すべてを取得する関数もあります<ul><li><code>document.elementsFromPoint()</code></li></ul></li><li>キャレット位置(カーソル位置)を取得する関数もあります<ul><li><code>document.caretPositionFromPoint()</code></li></ul></li></ul><h4 id="toc-3">思いつく使用用途</h4><ul><li>テスト作業</li><li>要素の簡易な指定<ul><li>拡張機能/ユーザースクリプトで対象要素を指定する</li></ul></li><li><code>:hover</code>のJavaScriptでの代用</li></ul></section><section><h3 id="toc-4">参考</h3><ul><li><a href="https://developer.mozilla.org/en-US/docs/Web/API/DocumentOrShadowRoot/elementFromPoint">DocumentOrShadowRoot.elementFromPoint() - Web APIs | MDN</a></li><li><a href="https://developer.mozilla.org/en-US/docs/Web/API/DocumentOrShadowRoot/elementsFromPoint">DocumentOrShadowRoot.elementsFromPoint() - Web APIs | MDN</a></li><li><a href="https://developer.mozilla.org/en-US/docs/Web/API/DocumentOrShadowRoot/caretPositionFromPoint">DocumentOrShadowRoot.caretPositionFromPoint() - Web APIs | MDN</a></li></ul></section>toshihttp://www.blogger.com/profile/02673765704159584610noreply@blogger.com0tag:blogger.com,1999:blog-279447686030876252.post-74568186271498281462021-01-18T03:15:00.014+09:002022-04-01T20:11:52.930+09:00ZIPでファイルをまとめてダウンロード.user.js<script type="application/json" id="post-data-json">{
"title": "ZIPでファイルをまとめてダウンロード.user.js"
,"labels": ["JavaScript", "UserScript", ""]
,"url": "https://www.bugbugnow.net/2021/01/download-files-with-zip.html"
}</script><div id="toc"><div id="toc-title">目次</div><div id="toc-content"><ol><li><a class="toc-link" href="#toc-1">はじめに</a></li><li><a class="toc-link" href="#toc-2">本ユーザスクリプトの利点</a></li><li><a class="toc-link" href="#toc-3">動作概要</a></li><li><a class="toc-link" href="#toc-4">使い方</a></li><li><a class="toc-link" href="#toc-5">コード</a></li><li><a class="toc-link" href="#toc-6">既知の問題</a></li><li><a class="toc-link" href="#toc-7">参考</a></li></ol></div></div><section id="toc-1"><h3>はじめに</h3><p>ウェブページ内にあるファイル(主に画像)をまとめてダウンロードしたいと思ったことはないでしょうか?世の中には、たくさんのダウンロード方法があります。ですが、それは、大抵すべてをダウンロードするものです。ページ内の一部のファイルのみ選択してダウンロードしたいと考えてこのユーザースクリプトを作成しました。</p></section><section id="toc-2"><h3>本ユーザスクリプトの利点</h3><ul><li>CROS (Cross-Origin Resource Sharing) 問題を回避できます<ul><li>拡張機能の権限で実行できるため、CROS問題を回避できます<ul><li>通常の<code>XMLHttpRequest</code>, <code>fetch</code>では、ユーザスクリプトでも回避できません</li><li>GM系の特別関数を使用することで、拡張機能の権限でダウンロードできます</li></ul></li><li>外部サイトのファイルでもダウンロードできます</li><li><code>Access-Control-Allow-Origin</code>の値に関わらずファイルをダウンロードできます</li><li><a href="https://developer.mozilla.org/ja/docs/Web/HTTP/CORS">オリジン間リソース共有 (CORS) - HTTP | MDN</a></li></ul></li><li>CSP (Content-Security-Policy) 問題を回避できます<ul><li>拡張機能の権限で実行できるため、CSP問題を回避できます</li><li>ブックマークレットが禁止されたページでも動作します</li><li><a href="https://developer.mozilla.org/ja/docs/Web/HTTP/CSP">コンテンツセキュリティポリシー (CSP) - HTTP | MDN</a></li></ul></li><li>複数サイトに対応できます<ul><li>ユーザの必要に応じて対応サイト増やすことができます</li><li>CSSセレクタ記法を使用して、簡単に保存ファイルを選択できます<ul><li>全ファイルではなく、必要なファイルのみをダウンロードすることができます</li></ul></li></ul></li><li>WebWorkerを使用することで、バックグランドタブでもある程度動作します<ul><li>重いZIP圧縮処理による、ページの処理遅延を回避できます</li></ul></li></ul></section><section id="toc-3"><h3>動作概要</h3><ol><li>拡張機能側でページにショートカットキーを設定する<ul><li>[Alt+Shift+D]を設定します<ul><li>ショートカットキーを変更することもできます</li></ul></li></ul></li><li>ページからショートカットキーで拡張機能側の処理を開始する</li><li>拡張機能側で指定のURLからファイルをダウンロードする<ul><li><code>GM.xmlHttpRequest()</code>を使用することでCROS問題とCSP問題を回避します</li></ul></li><li>拡張機能側でダウンロードしたデータをWebWorkerに転送する<ul><li>WebWorkerが使用できない場合、拡張機能側で以降の処理を実施します</li></ul></li><li>WebWorkerでJSZipを使用してZIPデータに圧縮する<ul><li>WebWorkerを使用することで、処理をページから分離できます</li><li>処理をページから分離したことで、バックグランドタブでもある程度動作します</li></ul></li><li>WebWorkerから拡張機能側にZIPデータを転送データする</li><li>拡張機能側で<code><a download></code>を使用してZIPファイルを保存する</li></ol></section><section id="toc-4"><h3>使い方</h3><ol><li>ユーザスクリプト用の拡張機能に下記のコードを設定する</li><li>ユーザスクリプトに<code>@include</code>/<code>@match</code>で対象ページのURLを追加する<ul><li>対象ページを含むように設定します</li><li>また、スクリプトのユーザ設定から追加することもできます</li></ul></li><li>ユーザスクリプトにサイト毎で処理を追記する<ul><li>サイト毎にZIPファイル名とダウンロードファイルのURLを設定します<ul><li>ファイル名は、次のようなものが考えられます<ul><li>ページタイトル(例:<code>document.title</code>)</li><li>ページ内の要素文字列(例:<code>document.querySelector('.title').textContent</code>)</li><li>ユーザ入力(例:<code>window.prompt('ファイル名を入力して下さい', '')</code>)</li></ul></li><li>ファイルURLの取得方法は、次のようなものが考えられます<ul><li>すべての画像<ul><li>例:<code>[...document.querySelectorAll('img')].map(img => img.src)</code></li></ul></li><li>特定要素の子要素<ul><li>例:<code>[...document.querySelectorAll('#container a')].map(a => a.href)</code></li></ul></li></ul></li></ul></li><li>ページ毎に処理を変更することもできます</li></ul></li><li>対象ページを開きユーザスクリプトを読み込む</li><li>対象ページでショートカットキーを入力しダウンロードを開始する</li><li>進歩画面(ダウンロード中)を表示する</li><li>進歩画面(圧縮中)を表示する<ul><li>対象ページをアクティブにしていない場合、進歩が遅くなります<ul><li>ブラウザは、バックグランドタブのスクリプト実行を制限することがあります</li></ul></li></ul></li><li>圧縮したZIPファイルをダウンロードする<ul><li>ブラウザ側のダウンロード開始を確認してから、ページをクローズします<ul><li>それより先に、タブをクローズするとダウンロードされません</li></ul></li></ul></li></ol><p>※<strong>下記のコードを導入するだけでは、不十分です。</strong><br> 対象ページ用の処理を利用者自らが作成する必要があります。<br> ただし、動作テスト用のコードは記載されています。<br>※Greasemonkey/Violentmonkeyの使用を強く推奨します。<br> Tampermonkeyと比べて実行速度が高速になります。<br> この差は、ダウンロードするファイルサイズが大きくなるほど顕著になります。</p></section><section id="toc-5"><h3>コード</h3><pre><span class="pre-code-title">MatometeDownload.user.js</span><code class="language-js:MatometeDownload.user.js">// ==UserScript==
// @name MatometeDownload
// @name:en Download files with ZIP
// @name:ja ZIPでファイルをまとめてダウンロード
// @description Use the [Alt+Shift+D] shortcut keys to download files with ZIP.
// @description:en Use the [Alt+Shift+D] shortcut keys to download files with ZIP.
// @description:ja [Alt+Shift+D]のショートカットキーでZIPでファイルをまとめてダウンロードします。
// @note ↓↓↓ Add target page URL ↓↓↓
// @match *://example.com/*
// @match *://www.bugbugnow.net/*
// @note ↑↑↑ Add target page URL ↑↑↑
// @author toshi (https://github.com/k08045kk)
// @license MIT License | https://opensource.org/licenses/MIT
// @version 4.0.1
// @since 1.0.0 - 20210113 - 初版
// @since 2.0.0 - 20210115 - WebWorker対応(高速化対応)
// @since 3.0.0 - 20210116 - WebWorker/NoWorker対応(NoScript対応)
// @since 3.2.0 - 20210117 - Download_files_with_ZIP.user.js → MatometeDownload.user.js
// @since 3.3.0 - 20210118 - リリース版
// @since 3.4.0 - 20210131 - fix Blobを保存できない問題修正(Firefox+Greasemonkey+NoScript)
// @since 3.4.1 - 20210131 - fix arg.levelオプションを修正
// @since 3.4.2 - 20210131 - fix I/F修正等
// @since 3.4.3 - 20210408 - fix 完了時に背景がちらつくことがある
// @since 3.4.4 - 20210526 - エラー出力まわりを強化
// @since 4.0.0 - 20220210 - downloadFilesZipAsync() を async function に変更
// @since 4.0.1 - 20220401 - fix 「@@match」→「@match」
// @see https://github.com/k08045kk/UserScripts
// @see https://www.bugbugnow.net/2021/01/download-files-with-zip.html
// @grant GM.xmlHttpRequest
// @grant window.close
// @require https://cdn.jsdelivr.net/npm/jszip@3.5.0/dist/jszip.min.js
// @require https://cdn.jsdelivr.net/npm/hotkeys-js@3.8.1/dist/hotkeys.min.js
// ==/UserScript==
(function() {
'use strict';
// 進歩表示
const box = document.createElement('div');
box.setAttribute('style', `
z-index: 2147483647;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: rgba(0,0,0,.7);
color: #fff;
font-size: 64px;
font-family: monospace, monospace;
white-space: pre;
`);
const root = document.createElement('div');
root.attachShadow({mode:'closed'}).appendChild(box);
const drawProgress = (text) => {
if (!root.parentNode && text) {
document.body.appendChild(root);
}
if (text) {
box.textContent = text;
} else if (root.parentNode) {
root.remove();
}
};
// 更新処理
const onDefaultUpdate = (arg) => {
switch (arg.status) {
case 'ready':
drawProgress(' ');
break;
case 'download':
const len = (arg.urls.length+'').length;
drawProgress('Download: '+(Array(len).join(' ')+arg.success).slice(-len)+'/'+arg.urls.length);
break;
case 'compress':
drawProgress('Compress: '+(Array(3).join(' ')+Math.floor(arg.percent)).slice(-3)+'%');
break;
case 'complate':
const title = 'Download '+arg.status;
const second = Math.floor((arg.endtime - arg.starttime) / 1000);
const info = arg.success+'/'+arg.urls.length+':'+arg.failure+' | '+second+'s';
drawProgress(' ');
console.log('complate', arg);
arg.alert !== false && alert(title+'\n\n'+arg.name+'.zip\n'+info);
arg.close !== false && arg.close != null && setTimeout(() => window.close(), typeof arg.close === 'number' ? arg.close : 1000);
drawProgress();
break;
case 'error':
default:
drawProgress();
const status = arg.substatus || arg.status;
const message = arg.message;
console.log('error', arg);
alert('error ('+status+')\n\n'+message);
break;
}
};
/**
* ZIPダウンロード
* ファイルのダウンロード → ファイルのZIP圧縮 → ZIPファイルのダウンロード
* @param {Object} arg - 引数
* in {string} arg.name - ZIPファイルのファイル名(拡張子を含まない)
* in {(string|Blob|File)[]} arg.urls - ダウンロードファイルのURL(or Blob or File)
* in/out {string[]} arg.names - ダウンロードファイルのファイル名(拡張子を含む)
* out {boolean[]} arg.results - ダウンロードの結果
* in/out {Function} [arg.onupdate=onDefaultUpdate] - 更新時のコールバック関数(例:(arg) => {})
* in/out {boolean} [arg.worker=true] - WebWorkerを使用する
* in {number} [arg.level=0] - 圧縮レベル(0-9, 0:無圧縮 / 1:最高速度 / 9:最高圧縮)
* in {boolean} [arg.folder=true] - junk-pathsオプション(ディレクトリ構造を保持する)
* in {boolean} [arg.empty=true] - 取得失敗したファイルを保存する
* out {string} arg.status - 進歩状態
* out {string} arg.substatus - エラー以前の進歩状態
* out {string} arg.message - エラーメッセージ
* out {number} arg.success - ダウンロード成功件数
* out {number} arg.failure - ダウンロード失敗件数
* out {number} arg.percent - 圧縮の進歩(0-100の実数)
* out {number} arg.starttime - 開始時間
* out {number} arg.endtime - 終了時間
* in {boolean} [arg.alert=true] - 完了時にアラートを表示する
* in {(boolean|number)} [arg.close=false] - 完了時にタブをクローズする(Greasemonkeyは、対象外)
* @return 実行有無
* 同一ページ内での並列実行は許可されていません。
*/
const downloadFilesZipAsync = async (arg) => {
// 1. 前処理
arg.status = 'ready';
arg.starttime = Date.now();
arg.onupdate = arg.onupdate || onDefaultUpdate;
arg.success = 0;
arg.failure = 0;
arg.percent = 0;
arg.onupdate(arg);
// 6. 完了処理
const onComplate = (status, message) => {
arg.substatus = arg.substatus || arg.status;
arg.status = status || 'complate';
arg.message = message || 'complate';
arg.endtime = Date.now();
setTimeout(() => {
arg.onupdate(arg);
// 補足:簡易のブラウザ上のダウンロード開始待ち
}, 0);
};
// 3. ファイルのダウンロード
const downloadFilesAsync = async (obj) => {
const zip = obj instanceof JSZip ? obj : null;
const worker = obj instanceof Worker ? obj : null;
arg.status = 'download';
arg.names = arg.names || [];
arg.results = [];
const onDownload = (name, data) => {
arg.success++;
!data && arg.failure++;
arg.onupdate(arg);
if (arg.empty !== false || data) {
zip && zip.file(name, data);
worker && worker.postMessage({command:'file', name:name, buffer:data}, data ? [data] : null);
}
};
const promises = arg.urls.map((url, i) => {
const name = arg.names[i] = arg.names[i]
|| url.name
|| (typeof url === 'string' && url.slice(url.lastIndexOf('/') + 1))
|| String(i);
return new Promise((resolve, reject) => {
const success = (data) => { try { arg.results[i]=!!data; onDownload(name, data); } finally { resolve(); } };
const failure = () => success();
try {
const blob = url;
if (blob instanceof Blob) {
// ページの内部データ
blob.arrayBuffer().then((buffer) => {
try {
if (buffer instanceof ArrayBuffer === false) {
// JavaScript無効時にJSZip内部でエラーする問題対応(JSZipがページコンテキストを想定していないため)
// 補足:Firefox + Greasemonkey + NoScript の問題
// WebWorker時は、postMessage() で変換されるため、問題現象は発生しない。
// 説明:Greasemonkey の ArrayBuffer.arrayBuffer() / FileReader.readAsArrayBuffer() は、
// コンテンツコンテキスト(ArrayBuffer)ではなく、
// ページコンテキスト(window.ArrayBuffer)で ArrayBuffer を返す。
// see https://bugzilla.mozilla.org/show_bug.cgi?id=1427470
// see https://github.com/greasemonkey/greasemonkey/issues/2786
const temp = buffer;
buffer = new ArrayBuffer(temp.byteLength);
new Uint8Array(buffer).set(new Uint8Array(temp));
}
success(buffer);
} catch (e) {
//console.log(e);
failure();
}
});
} else if (typeof url === 'string') {
// ページの外部データ
GM.xmlHttpRequest({
method: 'GET',
url: url,
responseType: 'arraybuffer',
onload: (xhr) => {
const isSuccess = 200 <= xhr.status && xhr.status < 300 || xhr.status === 304;
success(isSuccess ? xhr.response : null);
},
onerror: failure,
onabort: failure,
ontimeout: failure
});
// 補足:Data URL/Blob URLは、使用禁止
} else { failure(); }
} catch (e) {
//console.log(e);
failure();
}
});
});
arg.onupdate(arg);
await Promise.all(promises);
};
// 5. ZIPのダウンロード
const downloadZip = (blob) => {
const dataUrl = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = dataUrl;
a.download = arg.name+'.zip';
a.dispatchEvent(new MouseEvent('click'));
setTimeout(() => { URL.revokeObjectURL(dataUrl); }, 1E4); // 10s
onComplate();
};
// 2a. Workerなしの処理
const noworkerAsync = async () => {
arg.worker = false;
try {
const zip = new JSZip();
await downloadFilesAsync(arg.folder !== false ? zip.folder(arg.name) : zip);
// 4a. ZIP圧縮
arg.status = 'compress';
arg.onupdate(arg);
const option = arg.level
? {type:'arraybuffer', compression:'DEFLATE', compressionOptions:{level:arg.level}}
: {type:'arraybuffer'};
const buffer = await zip.generateAsync(option, async (metadata) => {
arg.percent = metadata.percent;
arg.onupdate(arg);
});
arg.percent = 100;
arg.onupdate(arg);
downloadZip(new Blob([buffer]));
} catch (e) {
//console.log(e);
onComplate('error', e.message);
}
};
if (arg.worker === false) {
await noworkerAsync();
return true;
}
arg.worker = true;
// 2b. WebWorker作成
const promise = new Promise((resolve, reject) => {
const code = `
importScripts('https://cdn.jsdelivr.net/npm/jszip@3.5.0/dist/jszip.min.js');
const zip = new JSZip();
let folder = zip;
self.addEventListener('message', async (event) => {
const data = event.data;
try {
switch (data.command) {
case 'ready':
if (data.name) {
folder = zip.folder(data.name);
}
self.postMessage({command:'go'});
break;
case 'file':
folder.file(data.name, data.buffer);
break;
case 'generate':
// 4b. ZIP圧縮
const option = data.level
? {type:'arraybuffer', compression:'DEFLATE', compressionOptions:{level:data.level}}
: {type:'arraybuffer'};
const buffer = await zip.generateAsync(option, (metadata) => {
self.postMessage({command:'progress', percent:metadata.percent});
});
self.postMessage({command:'progress', percent:100});
self.postMessage({command:'complate', buffer:buffer}, [buffer]);
break;
default:
self.postMessage({command:'error', message:'The command cannot be interpreted. (command:'+data.command+')'});
break;
}
} catch (e) {
//console.log(e);
self.postMessage({command:'error', message:e.message});
}
});
`;
const workerUrl = URL.createObjectURL(new Blob([code]));
let worker = null;
try {
worker = new Worker(workerUrl);
worker.addEventListener('error', async (event) => {
let ret = 'NG';
if (arg.status == 'ready') {
// Firefox + NoScript
ret = 'noworker';
} else {
onComplate('error', event && event.message || '');
}
resolve(ret);
worker.terminate();
});
worker.addEventListener('message', async (event) => {
const data = event.data;
switch (data.command) {
case 'go':
await downloadFilesAsync(worker);
arg.status = 'compress';
arg.onupdate(arg);
worker.postMessage({command:'generate', level:arg.level});
break;
case 'progress':
arg.percent = data.percent;
arg.onupdate(arg);
break;
case 'complate':
downloadZip(new Blob([data.buffer]));
resolve('OK');
worker.terminate();
break;
case 'error':
default:
onComplate('error', data.message || 'The command cannot be interpreted. (command:'+data.command+')');
resolve('NG');
worker.terminate();
break;
}
});
worker.postMessage({command:'ready', name:(arg.folder !== false ? arg.name : null)});
} catch (e) {
// Chrome + NoScript
//console.log(e);
resolve('noworker');
} finally {
URL.revokeObjectURL(workerUrl);
}
}).then(async (value) => {
let ret = false;
switch (value) {
case 'noworker': // 処理未実施(noworkerで再実行)
await noworkerAsync();
// not break
case 'OK': // 処理完了
ret = true;
break;
case 'NG': // 処理失敗
default:
break;
}
return ret;
});
return await promise;
// 備考:staus: ready > download > compress > complate >> error
// 備考:command: ready > go > file > generate > progress > complate >> error
// 備考:complateから実際のダウンロードがブラウザ上で発生するまでに、
// 僅かなタイムラグが発生する可能性があります。
// 実際のダウンロードがブラウザで開始するまで、ページをクローズしないで下さい。
// 備考:urlsのファイル名に重複がある場合、最後のファイルのみ保存します。
// namesを指定して明示的にファイル名を指示することで回避できます。
// 備考:urlsにBlobを使用できます。その場合、ファイル名はnameプロパティを使用します。
// Blobではなく、Fileを使用することを検討して下さい。
// 備考:ファイル名が見つからない場合、最終的にインデックスの数値を使用します。
// 備考:データの取得に失敗した場合、空ファイルを保存します。
// これには、データ取得に失敗したことを明示的に示す意味合いがあります。
// 備考:onupdate()のargは、変更不可です。変更した場合、問題が発生する可能性があります。
// 備考:対象ページがJavaScript無効の場合、WebWorkerは動作しません。
};
// ショートカットキー設定
const shortcut = 'alt+shift+d';
hotkeys(shortcut, async (event, handler) => {
console.log('shortcut', 'start', location.hostname);
try {
// ↓↓↓ Add processing for each site ↓↓↓
if (location.hostname == 'www.bugbugnow.net') {
const urls = [...document.querySelectorAll('body img')].map(img => img.src);
urls.push(new File(['Download list.\n\n'+urls.join('\n')], 'list.txt', {type:'text/plain'}));
await downloadFilesZipAsync({
name: 'バグ取りの日々', //document.title.trim(),
urls: urls,
//names: [...document.querySelectorAll('body img')].map((img, i) => i+'.jpg'),
//worker: false,
//level: 6,
//folder: false,
//empty: false,
//alert: false,
//close: 1000,
});
} else {
alert('Missing download settings');
}
// ↑↑↑ Add processing for each site ↑↑↑
} catch (e) {
console.log('shortcut', 'error', e);
}
console.log('shortcut', 'end');
});
console.log('shortcut', shortcut);
})();</code></pre><h4>変更履歴</h4><p>最新版・変更履歴は、GitHubを参照して下さい。</p><ul><li><a href="https://github.com/k08045kk/UserScripts/tree/master/MatometeDownload">k08045kk/UserScripts · GitHub</a></li></ul><p>上記のコードは、動作テスト用の処理を追加しています。</p><h4>バージョン 4.0.0</h4><p><code>downloadFilesZipAsync()</code> の仕様が大きく変更されました。<br>下位バージョンからバージョンアップする際には、注意してください。</p><p>もしも、元に戻す必要がある場合、Gitに過去のバージョンが存在します。参照してください。</p><h4>動作テスト</h4><ol><li>下記のユーザスクリプトをインストールする</li><li>本ページをリロードする<ul><li>ユーザスクリプトを読み込む</li></ul></li><li>[Alt+Shift+D]キーを押下する<ul><li>外部ドメインへのアクセス許可が必要な場合は、許可する</li></ul></li><li>進歩画面を表示する</li><li>「バグ取りの日々.zip」をダウンロードする<ul><li>次のファイルが格納されている<ul><li>list.txt</li><li>icon-min.png(下記の画像ファイル)</li></ul></li></ul></li></ol><img alt="バグ取りの日々" loading="lazy" decoding="async" width="250" height="250" src="https://cdn.bugbugnow.net/blog/icon-min.png"></section><section id="toc-6"><h3>既知の問題</h3><ul><li>FirefoxのTampermonkeyがJavaScript無効時に動作しない<ul><li>FirefoxのJavaScript無効時、Tampermonkeyのすべてのスクリプトが動作しない<ul><li>Tampermonkeyの内部設計の問題</li></ul></li></ul></li><li><code>complate</code>でクローズした場合、ダウンロードが開始しないことがある<ul><li><code>window.close()</code>前に、<code>alert()</code>することで回避できます</li><li><code>setTimeout()</code>などで、十分な時間遅延することでも回避できます</li></ul></li><li>Greasemonkeyで<code>arg.close</code>が動作しない<ul><li>Greasemonkeyでは、タブをクローズする権限がありません</li></ul></li></ul></section><section id="toc-7"><h3>参考</h3><ul><li><a href="https://r17n.page/2020/01/12/js-download-zipped-images-to-local/">JavaScript で複数画像を zip に圧縮してローカルにダウンロード</a></li><li><a href="https://hatz48.hatenablog.com/entry/2014/01/13/175322">Web Worker を使って web ページ内の画像を zip してダウンロードする</a></li><li><a href="https://github.com/Stuk/jszip">Stuk/jszip</a></li><li><a href="https://github.com/eligrey/FileSaver.js">eligrey/FileSaver.js</a></li><li><a href="https://github.com/jaywcjlove/hotkeys">jaywcjlove/hotkeys</a></li></ul></section>toshihttp://www.blogger.com/profile/02673765704159584610noreply@blogger.com0