LoginSignup
296
80

More than 3 years have passed since last update.

QiitaのLGTMをSushiにするユーザースクリプト

Last updated at Posted at 2020-03-12

【2020/10/09】
QiitaのLGTMをSUSHIにするユーザースタイルシート - Qiita
ユーザースタイルシートを用いた改良版を作りました。合わせてご覧ください。

概要

image.png

QiitaのいいねがLGTMに変わりましたから、当然の流れとしてSushiにしました。
記事のLGTMボタンの他、トレンドやマイページ、コメントなどに表示されるLGTMもSushiに置換します。

Greasemonkey/Tampermonkeyなどで実行できるユーザースクリプトとして実装しており、Greasy Forkからインストール可能です。

実装

偉大なる先人はユーザースタイルシートで実装していましたが、筆者としてはユーザースクリプトのほうが慣れてるのでそっちで実装することにします。

アイコンの置換

SVG要素の置換

新しいLGTMアイコンはSVG要素で描かれていますから、まずはそれら全てをquerySelectorAll()で発見し、その中身をOpenSushituna.svgで置換します。

image.png

image.png

さて、そのためには要素のinnerHTMLを直接置き換えれば良いのですが…

image.png

単にそうしただけだと、SVG要素のviewBox属性の違いからこんな小さなSushiになってしまいます。

image.png

食べ応えがないので、viewBox属性の値も合わせて置き換えることにしましょう。

それと後でコメント欄など遅延読み込みされる要素への対応をするときに繰り返し何度も置換してしまうのを防ぐため、置き換えが完了したSVG要素にはdata-sushinized属性を追加しておきます。

/*
 * OpenSushi
 * (c) remin
 */
const viewBox = '0 0 64 64';
const innerHTML = '<defs><style>.a{fill:#eee;}.b{fill:#bdbdbd;}.c{fill:#d32f2f;}.d{fill:#b71c1c;}</style></defs><title>Sushi</title><path class="a" d="M53.94,44.75l-3.07.77a16,16,0,0,1-12.76-2.21l-14.55-9.7A8,8,0,0,1,20,27V23a8,8,0,0,1,6.06-7.76l3.07-.77a16,16,0,0,1,12.76,2.21l14.55,9.7A8,8,0,0,1,60,33v4A8,8,0,0,1,53.94,44.75Z"/><path class="b" d="M60,33a8,8,0,0,1-6.06,7.73l-3.07.77A16,16,0,0,1,46,42V41a8,8,0,0,0-3.56-6.66l-14.55-9.7A16,16,0,0,0,20.07,22,8.08,8.08,0,0,0,20,23h0v4a8,8,0,0,0,3.56,6.66l14.55,9.7c.42.28.85.53,1.29.76l.29.15c.43.22.87.42,1.31.6l.25.09q.59.23,1.2.41l.29.09c.45.12.9.23,1.36.31l.37.06c.43.07.86.12,1.29.16l.17,0h0a16,16,0,0,0,4.93-.44l3.07-.77A8,8,0,0,0,60,37V33Z"/><path class="a" d="M37.94,54.75l-3.07.77a16,16,0,0,1-12.76-2.21L7.56,43.61A8,8,0,0,1,4,37V33a8,8,0,0,1,6.06-7.76l3.07-.77a16,16,0,0,1,12.76,2.21l14.55,9.7A8,8,0,0,1,44,43v4A8,8,0,0,1,37.94,54.75Z"/><path class="b" d="M37.94,50.75l-3.07.77a16,16,0,0,1-12.76-2.21L7.56,39.61A8,8,0,0,1,4,33v4a8,8,0,0,0,3.56,6.66l14.55,9.7a16,16,0,0,0,12.76,2.21l3.07-.77A8,8,0,0,0,44,47V43A8,8,0,0,1,37.94,50.75Z"/><path class="c" d="M16,17.76v3.42a1,1,0,0,0,.81,1c11.44,2.27,21,12,28.79,19.74a1,1,0,0,0,.95.26l14.69-3.67a1,1,0,0,0,.76-1V32.39a1.2,1.2,0,0,0-.29-.7C56,24,44,15.07,32.22,13a1,1,0,0,0-.43,0l-15,3.76A1,1,0,0,0,16,17.76Z"/><path class="d" d="M61.24,33.36,46.55,37a1,1,0,0,1-.95-.26C37.85,29,28.26,20.59,16.81,18.32a1,1,0,0,1-.78-.77,1,1,0,0,0,0,.21v3.42a1,1,0,0,0,.81,1c11.44,2.27,21,12,28.79,19.74a1,1,0,0,0,.95.26l14.69-3.67a1,1,0,0,0,.76-1V32.39h0A1,1,0,0,1,61.24,33.36Z"/><path class="c" d="M2,26.58V30a1,1,0,0,0,.81,1c11.44,2.27,21,13,28.79,20.74a1,1,0,0,0,.95.26L47.24,48.3a1,1,0,0,0,.76-1V42.22a1,1,0,0,0-.29-.7C39.81,33.61,30,23.9,18.22,21.84a1,1,0,0,0-.43,0l-15,3.76A1,1,0,0,0,2,26.58Z"/><path class="d" d="M47.24,43.19,32.55,46.86a1,1,0,0,1-.95-.26C23.85,38.86,14.26,29.41,2.81,27.15A1,1,0,0,1,2,26.37a1,1,0,0,0,0,.21V30a1,1,0,0,0,.81,1c11.44,2.27,21,13,28.79,20.74a1,1,0,0,0,.95.26L47.24,48.3a1,1,0,0,0,.76-1V42.22h0A1,1,0,0,1,47.24,43.19Z"/>';

// LGTMのSVG要素をそれぞれSushiに変更する
const images = node.querySelectorAll('*[class*="like"] svg:not([data-sushinized]), .ItemLink__status svg:not([data-sushinized]), .ms-ItemList_counts svg:not([data-sushinized]), svg[class*="Lgtm"]:not([data-sushinized])');
images.forEach(image => {
    image.setAttribute('viewBox', viewBox);
    image.innerHTML = innerHTML;

    image.dataset.sushinized = "1";
});

既存のアニメーションの削除

WebpackerなしでNode.jsベースの最小のJavaScriptビルド環境を設定する「minimum_javascript_on_rails」 - Qiita - Mozilla Firefox 2020-03-13 16-34-07.gif

記事をLGTMするボタンのSVG要素には押したときにチェックマークへ変化するアニメーションが仕込まれていますが、これを無効化します。

ただ経験から言ってこういうのは「アニメーションそのものを削除する」より「アニメーションが発生する側から止める」ほうが楽です。

前項で設定したviewBox属性はアニメーションが発動するごとに元に戻されてしまうことを利用します。
MutationObserverで属性の変更を監視することができますから、属性に変化があったら直ちに元のSushiに戻すようにしてみましょう。

なお元のSushiに戻すときにも属性は変更されますからMutationObserverが反応します。
そうすると無限再帰呼び出しに陷ってしまうので、
元のSushiに戻している最中はMutationObserverを止めなければならないことに注意しましょう。

/*
 * OpenSushi
 * (c) remin
 */
const viewBox = '0 0 64 64';
const innerHTML = '<defs><style>.a{fill:#eee;}.b{fill:#bdbdbd;}.c{fill:#d32f2f;}.d{fill:#b71c1c;}</style></defs><title>Sushi</title><path class="a" d="M53.94,44.75l-3.07.77a16,16,0,0,1-12.76-2.21l-14.55-9.7A8,8,0,0,1,20,27V23a8,8,0,0,1,6.06-7.76l3.07-.77a16,16,0,0,1,12.76,2.21l14.55,9.7A8,8,0,0,1,60,33v4A8,8,0,0,1,53.94,44.75Z"/><path class="b" d="M60,33a8,8,0,0,1-6.06,7.73l-3.07.77A16,16,0,0,1,46,42V41a8,8,0,0,0-3.56-6.66l-14.55-9.7A16,16,0,0,0,20.07,22,8.08,8.08,0,0,0,20,23h0v4a8,8,0,0,0,3.56,6.66l14.55,9.7c.42.28.85.53,1.29.76l.29.15c.43.22.87.42,1.31.6l.25.09q.59.23,1.2.41l.29.09c.45.12.9.23,1.36.31l.37.06c.43.07.86.12,1.29.16l.17,0h0a16,16,0,0,0,4.93-.44l3.07-.77A8,8,0,0,0,60,37V33Z"/><path class="a" d="M37.94,54.75l-3.07.77a16,16,0,0,1-12.76-2.21L7.56,43.61A8,8,0,0,1,4,37V33a8,8,0,0,1,6.06-7.76l3.07-.77a16,16,0,0,1,12.76,2.21l14.55,9.7A8,8,0,0,1,44,43v4A8,8,0,0,1,37.94,54.75Z"/><path class="b" d="M37.94,50.75l-3.07.77a16,16,0,0,1-12.76-2.21L7.56,39.61A8,8,0,0,1,4,33v4a8,8,0,0,0,3.56,6.66l14.55,9.7a16,16,0,0,0,12.76,2.21l3.07-.77A8,8,0,0,0,44,47V43A8,8,0,0,1,37.94,50.75Z"/><path class="c" d="M16,17.76v3.42a1,1,0,0,0,.81,1c11.44,2.27,21,12,28.79,19.74a1,1,0,0,0,.95.26l14.69-3.67a1,1,0,0,0,.76-1V32.39a1.2,1.2,0,0,0-.29-.7C56,24,44,15.07,32.22,13a1,1,0,0,0-.43,0l-15,3.76A1,1,0,0,0,16,17.76Z"/><path class="d" d="M61.24,33.36,46.55,37a1,1,0,0,1-.95-.26C37.85,29,28.26,20.59,16.81,18.32a1,1,0,0,1-.78-.77,1,1,0,0,0,0,.21v3.42a1,1,0,0,0,.81,1c11.44,2.27,21,12,28.79,19.74a1,1,0,0,0,.95.26l14.69-3.67a1,1,0,0,0,.76-1V32.39h0A1,1,0,0,1,61.24,33.36Z"/><path class="c" d="M2,26.58V30a1,1,0,0,0,.81,1c11.44,2.27,21,13,28.79,20.74a1,1,0,0,0,.95.26L47.24,48.3a1,1,0,0,0,.76-1V42.22a1,1,0,0,0-.29-.7C39.81,33.61,30,23.9,18.22,21.84a1,1,0,0,0-.43,0l-15,3.76A1,1,0,0,0,2,26.58Z"/><path class="d" d="M47.24,43.19,32.55,46.86a1,1,0,0,1-.95-.26C23.85,38.86,14.26,29.41,2.81,27.15A1,1,0,0,1,2,26.37a1,1,0,0,0,0,.21V30a1,1,0,0,0,.81,1c11.44,2.27,21,13,28.79,20.74a1,1,0,0,0,.95.26L47.24,48.3a1,1,0,0,0,.76-1V42.22h0A1,1,0,0,1,47.24,43.19Z"/>';

// SVG要素の中身をSushiに変更するメソッド
const replaceSvg = svg => {
    svg.setAttribute('viewBox', viewBox);
    svg.innerHTML = innerHTML;
};

// LGTMのSVG要素をそれぞれSushiに変更し、要素の変更を監視する
const images = node.querySelectorAll('*[class*="like"] svg:not([data-sushinized]), .ItemLink__status svg:not([data-sushinized]), .ms-ItemList_counts svg:not([data-sushinized]), svg[class*="Lgtm"]:not([data-sushinized])');
images.forEach(image => {
    // Sushiに変更する
    replaceSvg(image);

    // 属性の変更を監視
    new MutationObserver((records, observer) => {
        // 属性変更時に発動するメソッドの中で属性を変更し無限再帰に陥るので一旦observeを停止する
        observer.disconnect();

        // Sushiに再変更する
        replaceSvg(records[0].target);

        // observeを再開
        observer.observe(records[0].target, {attributes: true});
    }).observe(image, {attributes: true});

    // 何重にも監視するのを防止するため、data-sushinized属性を追加しておく
    image.dataset.sushinized = "1";
});

新しいアニメーションの追加

しかし何のアニメーションも無くなってしまうと今度はなんだか味気ありません。
そこで @reki さんのアイデアを元に、ボタンをクリックしたらSushiを回転させてみることにします。

Qiitaの通知欄から各ユーザのプロフィールに移動できるユーザースクリプト - Qiita - Mozilla Firefox 2020-03-13 16-28-00.gif

前項のMutationObserverでボタンがクリックされたことは知れるので、その時SVG要素にアニメーション用のclassを付与し、CSSアニメーションでSushiを回転させます。

/*
 * OpenSushi
 * (c) remin
 */
const viewBox = '0 0 64 64';
const innerHTML = '<defs><style>.a{fill:#eee;}.b{fill:#bdbdbd;}.c{fill:#d32f2f;}.d{fill:#b71c1c;}</style></defs><title>Sushi</title><path class="a" d="M53.94,44.75l-3.07.77a16,16,0,0,1-12.76-2.21l-14.55-9.7A8,8,0,0,1,20,27V23a8,8,0,0,1,6.06-7.76l3.07-.77a16,16,0,0,1,12.76,2.21l14.55,9.7A8,8,0,0,1,60,33v4A8,8,0,0,1,53.94,44.75Z"/><path class="b" d="M60,33a8,8,0,0,1-6.06,7.73l-3.07.77A16,16,0,0,1,46,42V41a8,8,0,0,0-3.56-6.66l-14.55-9.7A16,16,0,0,0,20.07,22,8.08,8.08,0,0,0,20,23h0v4a8,8,0,0,0,3.56,6.66l14.55,9.7c.42.28.85.53,1.29.76l.29.15c.43.22.87.42,1.31.6l.25.09q.59.23,1.2.41l.29.09c.45.12.9.23,1.36.31l.37.06c.43.07.86.12,1.29.16l.17,0h0a16,16,0,0,0,4.93-.44l3.07-.77A8,8,0,0,0,60,37V33Z"/><path class="a" d="M37.94,54.75l-3.07.77a16,16,0,0,1-12.76-2.21L7.56,43.61A8,8,0,0,1,4,37V33a8,8,0,0,1,6.06-7.76l3.07-.77a16,16,0,0,1,12.76,2.21l14.55,9.7A8,8,0,0,1,44,43v4A8,8,0,0,1,37.94,54.75Z"/><path class="b" d="M37.94,50.75l-3.07.77a16,16,0,0,1-12.76-2.21L7.56,39.61A8,8,0,0,1,4,33v4a8,8,0,0,0,3.56,6.66l14.55,9.7a16,16,0,0,0,12.76,2.21l3.07-.77A8,8,0,0,0,44,47V43A8,8,0,0,1,37.94,50.75Z"/><path class="c" d="M16,17.76v3.42a1,1,0,0,0,.81,1c11.44,2.27,21,12,28.79,19.74a1,1,0,0,0,.95.26l14.69-3.67a1,1,0,0,0,.76-1V32.39a1.2,1.2,0,0,0-.29-.7C56,24,44,15.07,32.22,13a1,1,0,0,0-.43,0l-15,3.76A1,1,0,0,0,16,17.76Z"/><path class="d" d="M61.24,33.36,46.55,37a1,1,0,0,1-.95-.26C37.85,29,28.26,20.59,16.81,18.32a1,1,0,0,1-.78-.77,1,1,0,0,0,0,.21v3.42a1,1,0,0,0,.81,1c11.44,2.27,21,12,28.79,19.74a1,1,0,0,0,.95.26l14.69-3.67a1,1,0,0,0,.76-1V32.39h0A1,1,0,0,1,61.24,33.36Z"/><path class="c" d="M2,26.58V30a1,1,0,0,0,.81,1c11.44,2.27,21,13,28.79,20.74a1,1,0,0,0,.95.26L47.24,48.3a1,1,0,0,0,.76-1V42.22a1,1,0,0,0-.29-.7C39.81,33.61,30,23.9,18.22,21.84a1,1,0,0,0-.43,0l-15,3.76A1,1,0,0,0,2,26.58Z"/><path class="d" d="M47.24,43.19,32.55,46.86a1,1,0,0,1-.95-.26C23.85,38.86,14.26,29.41,2.81,27.15A1,1,0,0,1,2,26.37a1,1,0,0,0,0,.21V30a1,1,0,0,0,.81,1c11.44,2.27,21,13,28.79,20.74a1,1,0,0,0,.95.26L47.24,48.3a1,1,0,0,0,.76-1V42.22h0A1,1,0,0,1,47.24,43.19Z"/>';

// SVG要素の中身をSushiに変更するメソッド
const replaceSvg = svg => {
    svg.setAttribute('viewBox', viewBox);
    svg.innerHTML = innerHTML;
};

// LGTMのSVG要素をそれぞれSushiに変更し、要素の変更を監視する
const images = node.querySelectorAll('*[class*="like"] svg:not([data-sushinized]), .ItemLink__status svg:not([data-sushinized]), .ms-ItemList_counts svg:not([data-sushinized]), svg[class*="Lgtm"]:not([data-sushinized])');
images.forEach(image => {
    // Sushiに変更する
    replaceSvg(image);

    // 属性の変更を監視
    new MutationObserver((records, observer) => {
        // 属性変更時に発動するメソッドの中で属性を変更し無限再帰に陥るので一旦observeを停止する
        observer.disconnect();

        // Sushiに再変更する・Sushiを1回転させる
        replaceSvg(records[0].target);
        records[0].target.classList.add("sushi-go-round");

        // observeを再開
        observer.observe(records[0].target, {attributes: true});
    }).observe(image, {attributes: true});

    // 何重にも監視するのを防止するため、data-sushinized属性を追加しておく
    image.dataset.sushinized = "1";
});

// 回転SushiのCSSアニメーションを定義
const style = document.createElement('style');
style.innerText = '.liked svg.sushi-go-round, svg.sushi-go-round[color="#fff"] { animation: sushi-go-round 1s ease; } @keyframes sushi-go-round { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }';
document.head.append(style);

文字列の置換

文字列で書かれてる「LGTM」も「Sushi」に置換していきます。

文字列でLGTMという表記のある要素を見つけられる範囲でquerySelectorAll()を用いて発見し、textContentreplace()で置換しました。

今のところ置換しているのはマイページにある「LGTMした記事」、Organizationsのページにある「LGTM」(単位表記)、通知欄にある「LGTMしました」、マイルストーンにある「🎉10 LGTM達成!」などですが、探せば他にもLGTM表記は見つかりそうです。

// 文字列のLGTMをSushiに置換する
const texts = node.querySelectorAll('.userPopularItems_likeUnit, .notification_actionWrapper span.bold:last-of-type, li[role="presentation"] a[href*="like"], .ms-ItemHeader_likedCount, .op-CounterItem_name, *[class*="Label"], .msg-Item_body, *[class*="UserAnalyzeResult__TagStatsTitle"], a.st-Dropdown_item[href*="lgtm"], a.st-Dropdown_item[href*="like"]');
texts.forEach(text => {
    text.childNodes.forEach(child => {
        if (child.nodeType === Node.TEXT_NODE) {
            child.textContent = child.textContent.replace("LGTM", "Sushi");
        }
    });
});

遅延読み込みされる要素への対応

デフォルトでユーザースクリプトはDOMContentLoadedイベント後に実行されるので、単にそのときに上記の処理を行ったのでは遅延読み込みされる要素中のLGTMをSushiにできません。各要素の遅延読み込み後に処理を行う必要があります。

しかし便利なことにMutationObserverは要素の属性の変化だけでなく子要素の変化も監視することができます。

そこで遅延読み込みされる要素を見つけられる範囲でquerySelectorAll()を用いて発見し、その子要素の変化(=遅延読み込みの完了)を監視、変化があったらその要素に対して上記の処理を行うことにします。

今のところ遅延読み込みされる要素として見ているのは記事のコメント欄マイルストーンにある記事一覧、タグごとの記事一覧などですが、探せば他にも遅延読み込みされている要素は見つかりそうです。

完成したコード

// ==UserScript==
// @name         Qiita Sushi
// @namespace    https://greasyfork.org/users/432749
// @description  QiitaのLGTMアイコンをSushiアイコンに変更する
// @author       fukuchan
// @match        https://qiita.com/*
// ==/UserScript==

/*
 * OpenSushi
 * (c) remin
 */
const viewBox = '0 0 64 64';
const innerHTML = '<defs><style>.a{fill:#eee;}.b{fill:#bdbdbd;}.c{fill:#d32f2f;}.d{fill:#b71c1c;}</style></defs><title>Sushi</title><path class="a" d="M53.94,44.75l-3.07.77a16,16,0,0,1-12.76-2.21l-14.55-9.7A8,8,0,0,1,20,27V23a8,8,0,0,1,6.06-7.76l3.07-.77a16,16,0,0,1,12.76,2.21l14.55,9.7A8,8,0,0,1,60,33v4A8,8,0,0,1,53.94,44.75Z"/><path class="b" d="M60,33a8,8,0,0,1-6.06,7.73l-3.07.77A16,16,0,0,1,46,42V41a8,8,0,0,0-3.56-6.66l-14.55-9.7A16,16,0,0,0,20.07,22,8.08,8.08,0,0,0,20,23h0v4a8,8,0,0,0,3.56,6.66l14.55,9.7c.42.28.85.53,1.29.76l.29.15c.43.22.87.42,1.31.6l.25.09q.59.23,1.2.41l.29.09c.45.12.9.23,1.36.31l.37.06c.43.07.86.12,1.29.16l.17,0h0a16,16,0,0,0,4.93-.44l3.07-.77A8,8,0,0,0,60,37V33Z"/><path class="a" d="M37.94,54.75l-3.07.77a16,16,0,0,1-12.76-2.21L7.56,43.61A8,8,0,0,1,4,37V33a8,8,0,0,1,6.06-7.76l3.07-.77a16,16,0,0,1,12.76,2.21l14.55,9.7A8,8,0,0,1,44,43v4A8,8,0,0,1,37.94,54.75Z"/><path class="b" d="M37.94,50.75l-3.07.77a16,16,0,0,1-12.76-2.21L7.56,39.61A8,8,0,0,1,4,33v4a8,8,0,0,0,3.56,6.66l14.55,9.7a16,16,0,0,0,12.76,2.21l3.07-.77A8,8,0,0,0,44,47V43A8,8,0,0,1,37.94,50.75Z"/><path class="c" d="M16,17.76v3.42a1,1,0,0,0,.81,1c11.44,2.27,21,12,28.79,19.74a1,1,0,0,0,.95.26l14.69-3.67a1,1,0,0,0,.76-1V32.39a1.2,1.2,0,0,0-.29-.7C56,24,44,15.07,32.22,13a1,1,0,0,0-.43,0l-15,3.76A1,1,0,0,0,16,17.76Z"/><path class="d" d="M61.24,33.36,46.55,37a1,1,0,0,1-.95-.26C37.85,29,28.26,20.59,16.81,18.32a1,1,0,0,1-.78-.77,1,1,0,0,0,0,.21v3.42a1,1,0,0,0,.81,1c11.44,2.27,21,12,28.79,19.74a1,1,0,0,0,.95.26l14.69-3.67a1,1,0,0,0,.76-1V32.39h0A1,1,0,0,1,61.24,33.36Z"/><path class="c" d="M2,26.58V30a1,1,0,0,0,.81,1c11.44,2.27,21,13,28.79,20.74a1,1,0,0,0,.95.26L47.24,48.3a1,1,0,0,0,.76-1V42.22a1,1,0,0,0-.29-.7C39.81,33.61,30,23.9,18.22,21.84a1,1,0,0,0-.43,0l-15,3.76A1,1,0,0,0,2,26.58Z"/><path class="d" d="M47.24,43.19,32.55,46.86a1,1,0,0,1-.95-.26C23.85,38.86,14.26,29.41,2.81,27.15A1,1,0,0,1,2,26.37a1,1,0,0,0,0,.21V30a1,1,0,0,0,.81,1c11.44,2.27,21,13,28.79,20.74a1,1,0,0,0,.95.26L47.24,48.3a1,1,0,0,0,.76-1V42.22h0A1,1,0,0,1,47.24,43.19Z"/>';

// SVG要素の中身をSushiに変更するメソッド
const replaceSvg = svg => {
    svg.setAttribute('viewBox', viewBox);
    svg.innerHTML = innerHTML;
};

// SVG要素やテキストのLGTMを発見し、全てSushiに変更するメソッド
const sushinize = node => {
    // LGTMのSVG要素をそれぞれSushiに変更し、要素の変更を監視する
    const images = node.querySelectorAll('*[class*="like"] svg:not([data-sushinized]), .ItemLink__status svg:not([data-sushinized]), .ms-ItemList_counts svg:not([data-sushinized]), svg[class*="Lgtm"]:not([data-sushinized])');
    images.forEach(image => {
        // Sushiに変更する
        replaceSvg(image);

        // 属性の変更を監視
        new MutationObserver((records, observer) => {
            // 属性変更時に発動するメソッドの中で属性を変更し無限再帰に陥るので一旦observeを停止する
            observer.disconnect();

            // Sushiに再変更する・Sushiを1回転させる
            replaceSvg(records[0].target);
            records[0].target.classList.add("sushi-go-round");

            // observeを再開
            observer.observe(records[0].target, {attributes: true});
        }).observe(image, {attributes: true});

        // 何重にも監視するのを防止するため、data-sushinized属性を追加しておく
        image.dataset.sushinized = "1";
    });

    // 文字列のLGTMをSushiに置換する
    const texts = node.querySelectorAll('.userPopularItems_likeUnit, .notification_actionWrapper span.bold:last-of-type, li[role="presentation"] a[href*="like"], .ms-ItemHeader_likedCount, .op-CounterItem_name, *[class*="Label"], .msg-Item_body, *[class*="UserAnalyzeResult__TagStatsTitle"], a.st-Dropdown_item[href*="lgtm"], a.st-Dropdown_item[href*="like"]');
    texts.forEach(text => {
        text.childNodes.forEach(child => {
            if (child.nodeType === Node.TEXT_NODE) {
                child.textContent = child.textContent.replace("LGTM", "Sushi");
            }
        });
    });
};

// 現在読み込まれているdocument中のLGTMを全てSushiに置換
sushinize(document);

// コメントなど遅延読み込みされる動的な要素中のLGTMを読み込まれると同時に全てSushiに置換
const dynamicNodes = document.querySelectorAll('#comments, *[class*="List_view"], div[data-hyperapp-app="Milestones"], div[id*="Snackbar"], div[class*="UserMain__Content"], div[class*="UserAnalyzeResult"]');
dynamicNodes.forEach(node => {
    new MutationObserver((records, observer) => {
        observer.disconnect();
        sushinize(node);
        observer.observe(node, {childList: true});
    }).observe(node, {childList: true});
});

// 回転SushiのCSSアニメーションを定義
const style = document.createElement('style');
style.innerText = '.liked svg.sushi-go-round, svg.sushi-go-round[color="#fff"] { animation: sushi-go-round 1s ease; } @keyframes sushi-go-round { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }';
document.head.append(style);

ソースコードはGithubGreasy Forkでも公開しています。

スクリーンショット

image.png

image.png

image.png

image.png

image.png

TODO

  • まだ見つけられていないLGTMを発見し、いずれは全てのLGTMをSushiにする。

偉大な人たち

296
80
11

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
296
80