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);
}