tj-deck

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

This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @require https://update.greasyfork.org/scripts/383989/703472/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);
}