Greasy Fork

tj-deck

TweetDeckをスマホで使いやすくするスクリプト

目前为 2019-05-31 提交的版本。查看 最新版本

此脚本不应直接安装,它是一个供其他脚本使用的外部库。如果您需要使用该库,请在脚本元属性加入:// @require https://update.greasyfork.icu/scripts/383989/703471/tj-deck.js

class TJScrollTask {
    constructor(tjDeck, targetL, duration) {
        this.tjDeck = tjDeck;
        this.$t = tjDeck.$wrap;
        this.x = targetL;
        this.d = duration;
        this.sl = tjDeck.wrapL;
        this.sTime = Date.now();
        this.ended = false;

        this._bindAnim = this._anim.bind(this);


        // 目標が画面外なら処理をしない
        var $clms = tjDeck.getClms();
        if (targetL < 0 || targetL > $clms[0].offsetWidth * ($clms.length - 1)) {
            this.ended = true;
        } else {
            requestAnimationFrame(this._bindAnim);
        }
    }

    stop() {
        if (this.ended) return;
        this.ended = true;
        cancelAnimationFrame(this._bindAnim);
    }

    _anim() {
        if (this.ended) return;
        var t = (Date.now() - this.sTime) / this.d,
            b = this.sl,
            c = this.x - this.sl,
            d = 1;
        if (t > 1 && !this.ended) {
            this.stop();
            t = 1;
        }
        this.tjDeck.scrollWrap(this._easeOut(t, b, c, d));
        if (t < 1) requestAnimationFrame(this._bindAnim);
    }
    _easeOut(t, b, c, d) {
        t /= d;
        t = t - 1;
        return c * (t * t * t + 1) + b;
    }
}

class TJDeck {
    constructor() {
        this.version = "0.0.9";
        this.$wrap = document.querySelector(".js-app-columns");
        this.wrapL = 0;
        this.scrollTask = null;
        this.options = this.getOptionObj();
        this.setOptionFromObj(this.options);

        this.$options = this.createOptionPanel();
        document.body.appendChild(this.$options);

        this.updateBlur();
        this.updateLight();
    }
    getOption(name, def) {
        var val = localStorage.getItem("tj_deck_" + name);
        return !val ? def : val == "true";
    }
    getOptionObj() {
        return {
            light: this.getOption("light", true),
            light_clm: this.getOption("light_clm", false),
            blur: this.getOption("blur", false)
        }
    }
    setOption(name, value) {
        localStorage.setItem("tj_deck_" + name, value);
    }
    setOptionFromObj(obj) {
        var keys = Object.keys(obj);
        for (var i = 0; i < keys.length; i++) {
            this.setOption(keys[i], obj[keys[i]]);
        }
    }
    getClms() {
        return this.$wrap.querySelectorAll("section.column");
    }
    back() {

        // TJDeck 設定画面が表示中なら消して終了
        if (this.$options.style.display != "none") {
            this.updateOption();
            this.hideOptionPanel();
            return;
        }

        // モーダルが表示中なら消して終了
        var $mdlDismiss = document.querySelector(".mdl-dismiss");
        if ($mdlDismiss) {
            $mdlDismiss.click();
            return;
        }

        // ツイートパネルが表示中なら消して終了
        if (this.isShownDrawer()) {
            this.hideDrawer();
            return;
        }

        // カラムに戻るボタンがあれば押して終了
        var $clm = this.getClosestColumn(this.wrapL);
        var $backToHome = $clm.querySelector(".js-column-back");
        if ($backToHome) {
            $backToHome.click();
            return;
        }

    }
    // 何か表示中ならtrue
    isShownItem() {
        return !!document.querySelector(".mdl-dismiss") || this.isShownDrawer();
    }
    // ドロワーが表示中ならtrue
    isShownDrawer() {
        return !!document.querySelector(".hide-detail-view-inline");
    }
    // ドロワーを非表示にする
    hideDrawer() {
        var $btn = document.querySelector(".js-hide-drawer");
        if ($btn) $btn.click();
    }
    // ドロワーを表示する
    showDrawer() {
        var $btn = document.querySelector(".js-show-drawer");
        if ($btn) $btn.click();
    }

    // 戻るボタンを管理する
    manageBack() {
        history.pushState(null, null, "");
        window.addEventListener("popstate", function(event) {
            this.back();
            history.pushState(null, null, "");

        }.bind(this));
    }

    observeModals() {
        var observer = new MutationObserver(function(records) {
            var record, $modal;
            for (var i = 0; i < records.length; i++) {
                record = records[i];
                for (var n = 0; n < record.addedNodes.length; n++) {
                    $modal = record.addedNodes[i];
                    this.stopAnkerFromModal($modal);
                }
            }
        }.bind(this));
        var options = {
            attributes: false,
            characterData: true,
            childList: true
        };

        var $targets = document.querySelectorAll(".js-modals-container, .js-modal");

        for (var i = 0; i < $targets.length; i++) {
            observer.observe($targets[i], options);
        }

    }

    stopAnkerFromModal($modal) {
        var $ankers = $modal.querySelectorAll("a"),
            $a;
        var cb = function(event) {
            event.preventDefault();
            event.target.removeEventListener("click", cb);
            return false;
        }
        for (var i = 0; i < $ankers.length; i++) {
            $a = $ankers[i];
            if ($a.href && $a.href.match(/#$/)) {
                $a.addEventListener("click", cb);
            }
        }
    }

    // カラムの増減を監視する
    observeClms() {
        var observer = new MutationObserver(function(records) {
            var $targetClm;

            // レコードの数だけ繰り返す
            var record;
            for (var i = 0; i < records.length; i++) {
                record = records[i];

                // 追加されたカラムがあればターゲットにする
                if (record.addedNodes[0]) {
                    $targetClm = record.addedNodes[0];
                }

                // 削除されたカラムがあれば前後のカラムをターゲットにする
                // なければ最初のカラム
                if (record.removedNodes[0]) {
                    if (record.nextSibling instanceof Element) {
                        $targetClm = record.nextSibling;
                    } else if (record.previousSibling instanceof Element) {
                        $targetClm = record.previousSibling;
                    } else {
                        $targetClm = this.getClms()[0];
                    }
                }
            }

            // ターゲットがあればスクロール処理
            if ($targetClm && $targetClm instanceof Element) {
                this.scrollWrapAnim($targetClm.offsetLeft);
            }
        }.bind(this));

        var options = {
            attributes: false,
            characterData: false,
            childList: true
        };

        observer.observe(this.$wrap, options);
    }

    // 横スクロールを管理する
    manageScroll() {
        var sPos;
        var sTime = Date.now();
        var prevPos;
        var $prevClm;
        var flag = null; // -1:開始前, 0:縦方向, 1:横方向


        // デフォルトのスクロールを止める
        document.querySelector(".js-app-columns-container").addEventListener("scroll", function(event) {
            event.target.scrollLeft = 0;
        }.bind(this));


        // タッチスタート
        document.querySelector(".js-app-columns").addEventListener("touchstart", function(event) {
            if (event.touches.length > 1 || this.isShownItem()) return;
            sPos = this._getPosObj(event);
            prevPos = sPos;
            flag = -1;
            sTime = Date.now();
            $prevClm = this.getClosestColumn(this.wrapL);
        }.bind(this));

        window.addEventListener("touchmove", function(event) {
            if (!flag) return;
            if (flag < 0) {
                var pos = this._getPosObj(event);
                if (Math.abs(pos.x - sPos.x) < Math.abs(pos.y - sPos.y)) {
                    flag = 0;
                    return;
                } else {
                    flag = 1;
                }
            }
            if (flag == 1) {
                if (this.scrollTask) this.scrollTask.stop();
                var pos = this._getPosObj(event);
                prevPos = pos;
                if (!this.options.light_clm) { // 軽量版じゃなければ動かす
                    this.scrollWrap(this.wrapL + prevPos.x - pos.x);
                }
            }
        }.bind(this));
        window.addEventListener("touchend", function(event) {
            if (flag < 1) return;
            flag = null;
            var time = Date.now(),
                pos = prevPos,
                distance = sPos.x - pos.x;

            var $targetClm;
            // スワイプ時
            if (Math.abs(distance) / (time - sTime) >= 0.5) {
                if (distance > 0) {
                    $targetClm = $prevClm.nextElementSibling;
                    this.hideMenu();
                } else {
                    $targetClm = $prevClm.previousElementSibling;
                    if (!$targetClm) this.showMenu();
                }
            } else {
                $targetClm = this.getClosestColumn(this.wrapL);
            }
            if ($targetClm && $targetClm instanceof Element) {
                this.scrollWrapAnim($targetClm.offsetLeft);
            }
        }.bind(this));
    }

    scrollWrapAnim(left) {
        if (this.scrollTask) this.scrollTask.stop();

        this.scrollTask = new TJScrollTask(this, left, this.options.light_clm ? 0 : 500);
    }

    // 指定位置までスクロール
    scrollWrap(left) {
        var $clms = this.getClms();
        // 画面外は処理しない
        if (left < 0 || left > $clms[0].offsetWidth * ($clms.length - 1) || !isFinite(left)) return;
        this.$wrap.style.transform = `translateX($ {
            -left
        }
        px)`;
        this.wrapL = left;
    }

    getClosestColumn(left) {
        var $clms = this.getClms();
        for (var i = 0; i < $clms.length; i++) {
            var distance = Math.abs(left - $clms[i].offsetLeft);
            if (distance <= $clms[i].offsetWidth / 2) {
                return $clms[i];
            }
        }
        return $clms[$clms.length - 1];
    }

    _getPosObj(event) {
        return {
            x: event.touches[0].pageX,
            y: event.touches[0].pageY
        }
    }

    hideMenu() {
        document.body.classList.add("tj_hide_menu");
    }
    showMenu() {
        document.body.classList.remove("tj_hide_menu");
    }

    showTJSetting() {

    }

    addTJNav() {
        var $nav = document.createElement("nav");
        $nav.classList.add("tj_nav");

        $nav.appendChild(this.createTweetBtn());
        $nav.appendChild(this.createSettingBtn());

        document.querySelector(".js-app-content").appendChild($nav);
    }

    createTweetBtn() {
        var $btn = document.createElement("button");
        $btn.classList.add("tj_tweet_btn", "Button", "Button--primary", "tweet-button");
        $btn.innerHTML = ` < i class = "Icon icon-compose icon-medium" > < /i>`;
		$btn.addEventListener("click", this.showDrawer.bind(this));
		return $btn;
	}

	createSettingBtn() {
		var $btn = document.createElement("a");
		$btn.classList.add("tj_setting_btn");
		$btn.href = "javascript:void(0)";
		$btn.innerHTML = `<i class="Icon icon-settings"></i > `;
        $btn.addEventListener("click", this.showOptionPanel.bind(this));
        return $btn;
    }

    createOptionPanel() {
        var $panel = document.createElement("div");
        $panel.classList.add("tj_options");
        $panel.style.display = "none";
        $panel.innerHTML = ` < p class = "title" > TJDeck設定 < /p>
<div>
	<label for="tj_ops_light">基本アニメーションをなくす:</label > < input type = "checkbox"
        name = "tj_ops_light"
        id = "tj_ops_light" > < /div>
<div>
	<label for="tj_ops_light_clm">カラム切り替えアニメーションをなくす:</label > < input type = "checkbox"
        name = "tj_ops_light_clm"
        id = "tj_ops_light_clm" > < /div>
<div>
	<label for="tj_ops_blur">カラムをぼかす(撮影用):</label > < input type = "checkbox"
        name = "tj_ops_blur"
        id = "tj_ops_blur" > < /div>
<div>
	<p>Script Version: ${this.version}</p > < /div>
<div>
	<a href="javascript:void(0)" class="tj_ops_close">閉じる</a > < /div>
`;
		$panel.querySelector(".tj_ops_close").addEventListener("click", function () {
			this.updateOption();
			this.hideOptionPanel();
		}.bind(this));
		return $panel;
	}

	hideOptionPanel() {
		var $panel = this.$options;
		$panel.style.display = "none";
	}
	showOptionPanel() {
		var $panel = this.$options;
		this.updateOptionPanel($panel);
		$panel.style.display = "";
	}

	updateOptionPanel() {
		var $panel = this.$options;
		["light", "light_clm", "blur"].forEach(function(key) {
			var $input = $panel.querySelector("#tj_ops_"+key);
			$input.checked = this.options[key];
		}.bind(this));
	}

	updateOption() {
		var $panel = this.$options;
		["light", "light_clm", "blur"].forEach(function(key) {
			var $input = $panel.querySelector("#tj_ops_"+key);
			this.options[key] = $input? $input.checked:false;
		}.bind(this));
		this.setOptionFromObj(this.options);

		this.updateBlur();
		this.updateLight();
	}

	updateBlur() {
		if (this.options.blur) {
			this.$wrap.classList.add("tj_blur");
		} else {
			this.$wrap.classList.remove("tj_blur");
		}
	}

	updateLight() {
		if (this.options.light) {
			document.body.classList.add("tj_light");
		} else {
			document.body.classList.remove("tj_light");
		}
	}
	
	manageStyle() {
		this.addStyle();
		var prevWidth = window.innerWidth;
		window.addEventListener("resize", function () {
			/ / 同じなら処理しない
        if (prevWidth == window.innerWidth) return;
        var $style = document.querySelector("#tj_deck_css");
        if ($style) $style.remove();
        this.addStyle();
        this.scrollWrap(this.wrapL * (window.innerWidth / prevWidth));
        prevWidth = window.innerWidth;
    }.bind(this));
}

refreshStyle() {}

addStyle() {
    var $head = document.querySelector("head"),
        $style = document.createElement("style");
    $style.type = "text/css";
    $style.id = "tj_deck_css";
    $style.innerHTML = `
    html {
        /*overscroll-behavior: none; プルダウンでリロードさせない */
    }

    body.tj_light,
    body.tj_light * {
        transition - duration: 0ms!important;
    }
    body.tj_light.inline - reply {
        /* 0にするとアニメーションイベントが発生せずに動作がおかしくなるので1ms */
        transition - duration: 1ms!important;
    }

    .js - column - options {
        display: none!important;
    }.is - options - open.js - column - options {
        display: block!important;
    }

    /* TJDeck オプションパネル */.tj_options {
        position: fixed;
        width: 100 % ;
        height: 100 % ;
        top: 0;
        left: 0;
        padding: 1em;
        background: #fff;
        color: #222;
	z-index: 300;
}
.tj_options .title {
	margin-bottom: 1em;
	font-size: 1.1em;
	font-weight: bold;
	text-align: center;
}
.tj_options > div {
	margin: 1em 0;
}
.tj_options label,
.tj_options input {
	display: inline-block!important;
	margin: 0!important;
	vertical-align: middle!important;
}


/* サイドメニューの表示切替 */
.js-app-header {
	position: fixed!important;
}
.tj_hide_menu .js-app-header {
	transform: translateX(-50px);
}

/* メインの位置を左端に */
.js-app-content {
	left: 0!important;
}


/* サイドバーが出たらナビを隠す */
.hide-detail-view-inline .tj_nav {
	display: none;
}

.tj_tweet_btn {
	position: fixed!important;
	width: 60px!important;
	height: 60px!important;
	bottom: 1em!important;
	right: 1em!important;
	padding: 0;
	background-color: # 1da1f2;
        color: #fff;
        border - radius: 36px;
        font - size: 16px;
        line - height: 1em;
        text - align: center;
        box - shadow: 1px 1px 5px rgba(0, 0, 0, .5);
        z - index: 200;
    }.tj_tweet_btn.icon - compose, .tj_setting_btn.icon - settings {
        display: inline - block;
        margin - top: 0;
        font - size: 20px!important;
    }.tj_setting_btn {
        position: fixed;
        width: 50px;
        height: 50px;
        top: 0!important;
        right: 40px!important;
        background - color: transparent;
        color: #333;
	text-align: center;
	box-shadow: none;
	z-index: 200;
}
.tj_setting_btn > i.icon-settings {
	margin-top: -2px;
	line-height: 50px;
}

.application {
	z-index: auto;
}

/* カラムの余白をなくす */
.app-columns {
	padding: 0!important;
}


/* カラムを幅いっぱいに表示 */
.column {
	width: ${document.body.clientWidth}px!important;
	height: ${document.body.clientHeight}px!important;
	max-width: 600px!important;
	margin: 0!important;
}

/* カラムの設定をabsoluteに */
.js-column-options-container {
	position: absolute!important;
	width: 100%;
}

/* サイドパネルを表示したときにメインを動かなくする */
.application > .app-content {
	margin-right: 0!important;
	transform: translateX(0px)!important;
}

/* メインエリアのスクロールを禁止 */
# container {
            overflow: hidden!important;
        }

        /* サイドパネルを幅いっぱいに表示 */.js - drawer {
            width: $ {
                document.body.clientWidth
            }
            px!important;
            max - width: 600px!important;
            /*left: -${document.body.clientWidth}px!important;*/
            left: 0!important;
            transform: translateX(-$ {
                document.body.clientWidth
            }
            px);
        }.hide - detail - view - inline.js - drawer { /* 表示中 */
            width: $ {
                document.body.clientWidth
            }
            px!important;
            max - width: 600px!important;
            /*left: 0!important;*/
            transform: translateX(0);
            z - index: 201!important;
        }.hide - detail - view - inline.js - drawer: after {
            display: none!important;
        }

        /* サイドパネルのタイトルを消す */.js - docked - compose.compose - text - title {
            display: none!important;
        }
        /* アカウント選択アイコン位置を上にずらす */.js - docked - compose.compose - accounts {
            width: 200px!important;
            margin - top: -50px;
        }

        /* ツイート入力エリアをすこし小さくする */.js - docked - compose.compose - text - container {
            padding: 5px!important;
        }.js - docked - compose.js - compose - text {
            height: 90px!important;
        }

        /* ツイートボタンを大きく */.js - docked - compose.js - send - button {
            width: 100px!important;
            text - align: center;
        }

        /* 各種ボタンを小さくして横並びにする */.js - docked - compose.compose - content button.js - add - image - button,
        .js - docked - compose.compose - content.js - schedule - button,
        .js - docked - compose.compose - content.js - tweet - button,
        .js - docked - compose.compose - content.js - dm - button {
            display: inline - block!important;
            width: auto!important;
        }.js - docked - compose.compose - content.js - tweet - button.is - hidden,
        .js - docked - compose.compose - content.js - dm - button.is - hidden {
            display: none!important;
        }.js - add - image - button > .label,
        .js - schedule - button > .label,
        .js - tweet - button > .label,
        .js - dm - button > .label {
            display: none!important;
        }.js - add - image - button,
        .js - scheduler,
        .js - tweet - type - button {
            display: inline - block;
            transform: translateY(-65px);
        }


        /* サイドパネルのフッターを消す */.js - docked - compose > footer {
            display: none!important;
        }.js - docked - compose.compose - content {
            bottom: 0!important;
        }

        /* サイドパネルのヘッダーを消す */.js - compose - header {
            position: absolute!important;
            right: 20px!important;
            border: 0!important;
        }
        header.js - compose - header div.compose - title {
            display: none!important;
        }.js - account - selector - grid - toggle {
            margin - right: 50px!important;
        }

        /* モーダルの位置調整 */.overlay: before,
        .ovl - plain: before,
        .ovl: before {
            display: none!important;
        }

        /* リツイートモーダルの幅設定 */#actions - modal > .mdl {
            max - width: 100 % !important;
        }

        /* モーダルのメディア表示調整 */.js - modal - panel.js - embeditem { /* 画面いっぱいに表示 */
            height: 100 % !important;
            top: 0!important;
            bottom: 0!important;
        }.js - modal - panel.js - embeditem iframe {
            max - width: 100 % !important;
            max - height: 100 % !important;
        }.js - modal - panel.js - med - tweet { /* ツイートを非表示 */
            display: none!important;
        }

        /* 閉じるボタン */.js - modal - panel.mdl - dismiss {
            z - index: 2;
        }

        /* 画像表示を調整する */.js - modal - panel.js - embeditem {
            display: flex!important;
            flex - direction: column;
            z - index: 1;
        }
        /* 画像表示部分 */.js - modal - panel.js - embeditem.l - table {
            position: relative!important;
            display: block!important;
            height: auto!important;
            flex: auto;
        }

        .js - modal - panel.js - embeditem.l - table div,
        .js - modal - panel.js - embeditem.l - table a {
            position: static!important;
        }.js - modal - panel.js - embeditem.l - table.js - media - image - link {
            pointer - events: none;
        }

        /* 画像サイズ指定 */.js - modal - panel.js - embeditem.l - table img,
        .js - modal - panel.js - embeditem.l - table iframe {
            position: absolute;
            max - width: 100 % !important;
            max - height: 100 % !important;
            width: auto!important;
            height: auto!important;
            top: 0!important;
            bottom: 0!important;
            left: 0!important;
            right: 0!important;
            margin: auto!important;
        }.js - modal - panel.js - embeditem.l - table iframe {
            width: 100 % !important;
            height: 100 % !important;
        }

        /* 画像検索ボタンの位置調整 */.js - modal - panel.js - embeditem.l - table.reverse - image - search {
            position: fixed!important;
            display: block!important;
            left: 10px!important;
        }

        /* 画像移動ボタンの表示位置を調整する */.js - modal - panel.js - embeditem.js - media - gallery - prev,
        .js - modal - panel.js - embeditem.js - media - gallery - next {
            position: relative!important;
            top: auto!important;
            width: 50 % !important;
            height: 60px!important;
        }.js - modal - panel.js - embeditem.js - media - gallery - next {
            margin - top: -60px;
            align - self: flex - end;
        }

        /* 画像下部のリンクを非表示 */.med - origlink,
        .med - flaglink {
            display: none!important;
        }


        /* デバッグ用モザイク */.tj_blur.js - stream - item - content {
            filter: blur(5px);
        }.tj_blur section.column: nth - child(1).js - stream - item - content {
            filter: none;
        }`;
        $head.appendChild($style);
    }
}


window.tj_deck = null;

function tjDeckStart() {
    console.log("TJDeckスタート!!!");
    window.tj_deck = new TJDeck();
    window.tj_deck.manageStyle();
    window.tj_deck.manageScroll();
    window.tj_deck.manageBack();
    window.tj_deck.observeClms();
    window.tj_deck.observeModals();
    window.tj_deck.hideMenu();
    window.tj_deck.addTJNav();
    document.querySelector("textarea.js-compose-text").spellcheck = false;
}



if (document.querySelector(".js-app-columns")) {
    tjDeckStart();
} else {
    var timer = setInterval(function() {
        if (document.querySelector(".js-app-columns")) {
            tjDeckStart();
            clearInterval(timer);
        } else {
            console.log("まだロード中");
        }
    }, 500);
}