Hinatazaka46 Layout change NEWS / SCHEDULE

Change the layout of the "News" and "Schedule" pages on the Hinatazaka46 website

// ==UserScript==
// @name           Hinatazaka46 Layout change NEWS / SCHEDULE
// @name:ja        日向坂46 レイアウト変更 NEWS / SCHEDULE
// @namespace      naoqv.hinatazaka
// @description	   Change the layout of the "News" and "Schedule" pages on the Hinatazaka46 website
// @description:ja 日向坂46サイト「ニュース」「スケジュール」ページのレイアウト変更
// @version        1.17
// @match	       https://www.hinatazaka46.com/s/official/news/*
// @match          https://www.hinatazaka46.com/s/official/media/*
// @require        https://update.greasyfork.org/scripts/510022/1459768/HinatazakaStyleSetting.js
// @require        https://update.greasyfork.org/scripts/509934/1480445/HinatazakaExceptionHandlingWrapper.js
// @icon           https://cdn.hinatazaka46.com/files/14/hinata/img/favicons/favicon-32x32.png
// @compatible     chrome
// @compatible     firefox
// @grant          none
// @license        MIT
// ==/UserScript==

const SCRIPT_NAME = "日向坂46 NEWS / SCHEDULE レイアウト変更";

handleException(()=> {

	const PAGE_TYPE_ERROR_MSG = "Processing of out-of-scope pages. Check the settings @match.";

	const pageType = (location.href).match(new RegExp('\/(media|news)\/'))[1];
	const isDetail = ((location.href).match(new RegExp('\/detail\/')) != null);

	const SELECTORS = ((x) => {
			switch (x) {
				case "news":
					return {"pArrow": ".p-news__pager-arrow",
							"cArrowLeft": ".c-news_pager-arrow--left",
								"cArrowRight" : ".c-news_pager-arrow--right",
								"cPageMonth": ".c-news__page_month",
								"cPageYear": ".c-news__page_year",
								"lMainContentsUl": ".l-maincontents--news ul",
								"pDate": ".p-news__page_date",
								"lSubContents": ".l-sub-contents--news"};
				case "media":
					return {"pArrow": ".p-schedule__pager-arrow",
							"cArrowLeft": ".c-schedule_pager-arrow--left",
								"cArrowRight" : ".c-schedule_pager-arrow--right",
								"cPageMonth": ".c-schedule__page_month",
								"cPageYear": ".c-schedule__page_year",
								"lMainContentsUl": ".l-maincontents--schedule ul",
								"pDate": ".p-schedule__page_date",
								"lSubContents": ".l-sub-contents--schedule"};
				default:
						throw new Error(PAGE_TYPE_ERROR_MSG);
			}
	})(pageType);

	const pageYear = ((y) => {return (y === null || y === undefined) ? null : y.innerText;})(document.querySelector(SELECTORS['cPageYear']));

	(() => {

		if (isDetail) {
			return;
		}

    darkMode();
    menuBarSetting();

		if (pageYear !== "年") {

			//console.log("日向坂46 cal");

			const daysOfWeek = ['Su', 'M', 'Tu', 'W', 'Th', 'F', 'Sa'];
			const now = new Date();
			const year = now.getFullYear();
			const month = now.getMonth() + 1;
			// 月初
			const first = new Date(year, month - 1, 1);
			// 月末
			const end = new Date(year, month,	0);
			// 月末の日
			const endDate = end.getDate();
			// 前月末
			const endPrevMonth = new Date(year, month - 1, 0);
			// 前月末の日
			const endDatePrevMonth = endPrevMonth.getDate();
			// 月初の曜日
			const firstDayOfWeek = first.getDay();

			let numOfDay = 1;
			let calendarHtml = '';

			const pageMonth = ((m) => {return m !== null ? m.innerText : "";})(document.querySelector(SELECTORS['cPageMonth']));

			const leftArrowHref = document.querySelector(SELECTORS['cArrowLeft']).children[0].href;
			const rightArrowHref = document.querySelector(SELECTORS['cArrowRight']).children[0].href;

			calendarHtml += '<table class="cale_table" style="width: 210px; margin: -130px 0 20px -50px;">';
			calendarHtml += `<tr><td></td><td class="cale_prev"><a id="cale_prev" href="${leftArrowHref}"><</a></td>
          <td class="cale_month" colspan="3">${pageYear}&#160;${pageMonth}</td><td class="cale_next"><a href="${rightArrowHref}">></a></td><td></td></tr>`;

			calendarHtml += '<tr>';

			for (let i = 0; i < daysOfWeek.length; i++) {
					calendarHtml += '<td class="cale_wek' + i + '">' + daysOfWeek[i] + '</td>';
			}

			calendarHtml += '</tr>';

			for (let w = 0; w < 6; w++) {
				calendarHtml += '<tr>'

				for (let d = 0; d < 7; d++) {
					if (w == 0 && d < firstDayOfWeek) {
						// 前月
						let num = endDatePrevMonth - firstDayOfWeek + d + 1;
						calendarHtml += '<td class="cale_day' + d + ' is-disabled">' + num + '</td>';
					} else if (numOfDay > endDate) {
						// 次月
						let num = numOfDay - endDate;
						calendarHtml += '<td class="cale_day' + d + ' is-disabled">' + num + '</td>';
						numOfDay++;
						w = 99; // カレンダーの最下端が次月の日付のみになるのを防止
					} else {
						calendarHtml += '<td class="cale_day' + d + '">' + numOfDay + '</td>';
						numOfDay++;
					}
				}
				calendarHtml += '</tr>'
			}

			calendarHtml += '</table>';

			document.querySelector(SELECTORS["lSubContents"]).insertAdjacentHTML('afterbegin', calendarHtml);
		}
	})();

	(() => {
		//console.log("日向坂46 NEWS / スケジュール");

		// 選択カテゴリ(ALL / 握手会・・・)
		const categorySelectorSuffix
			= ((c) => {
            let value = ""; 
            if (c.length == 0) {
                value = "all";
            } else {
                const tempValue = c[0].value;
                
                switch (tempValue) {
                    case "birthday":
                        value = "birth";
                        break;
                    
                    case "fanclub":
                        value = "fanclubonly";
                        break;
    
                    default:
                        value = tempValue;
                }
            }

            return value;
        })(document.getElementsByName("cd"));

    const categoryElem = document.querySelector('.c-button-category.category_' + categorySelectorSuffix);
    const categoryStyles = window.getComputedStyle(categoryElem);
    const categoryParent = categoryElem.parentElement;
    categoryParent.style.marginLeft = "-5px";
    categoryParent.style.paddingLeft = "4.5px";
    categoryParent.style.marginRight = "40px";
    categoryElem.style.color = `rgb(from ${categoryStyles.color} calc(r - 64) calc(g - 64) calc(b - 64))`;
    categoryParent.style.backgroundColor = `rgb(from ${categoryStyles.color} calc(r + 64) calc(g + 64) calc(b + 64))`;
    categoryParent.style.border = `solid 0.5px ${categoryElem.style.color}`;

		const now = new Date();
		const nowYearMonth = String(now.getFullYear()) + String(now.getMonth() + 1).padStart( 2, '0');

		// 詳細ページの場合 処理を終了 
		if (isDetail) {
			return;
		}

		/*
		 * フルブラウザ上ではNEWS/スケジュールが多い月は見づらいため
		 * 自動スクロール、表示色を追加設定
		 */

    const HOVER_CL = "#ddffff";
		const HOVER_BG_CL_UPPER = "#20cccc";
		const HOVER_BG_CL_LOWER = "#202040";
		const PAST_BG_CL = "#303040";
		const TODAY_DATE_CL = "orange";
		const TODAY_BG_CL_UPPER = "#30aaaa";
		const TODAY_BG_CL_LOWER = "#303050";
		const TODAY_BORDER_CL_UPPER = "#5bbee5";
		const TODAY_BORDER_CL_LOWER = "#d7eeff";
		const PAGER_MARGIN_TOP = 20;

		const styleElem = document.createElement("style");
		styleElem.setAttribute("rel", "stylesheet");

		let styleText = `
			.is-disabled {
				color: silver;
			}
			.p-page-head {
				padding-top: 20px;
			}
      .l-container {
          color: ${DEFAULT_CL};
          background-color: #202050;
      }
      .c-pager__item a svg {
          fill: #7ab6db;
      }
      .module-modal.js-member-filter .mordal-box .member-box ul li p.check input[type=checkbox]:checked+label:before {
        background-color:#6bcaea;
        border:1px solid #6bcaea;
      }`;

		switch (pageType) {
			case "news":
				styleText += `
            .p-news__list {background-color: ${DEFAULT_BG_CL};}
						.p-news__item:hover {
							background: linear-gradient(${HOVER_BG_CL_UPPER}, 20%, ${HOVER_BG_CL_LOWER});
							outline: 1px solid ${TODAY_BORDER_CL_UPPER}; outline-offset: ipx;
						}
            .p-news__item:hover .c-news__date, .p-news__item:hover .c-news__text, .p-news__item:hover .c-news__time--list {
              color: ${HOVER_CL};
            }`;
				 break;
			case "media":
				styleText += `
						.p-schedule__item:hover {
            	background: linear-gradient(${HOVER_BG_CL_UPPER}, 20%, ${HOVER_BG_CL_LOWER});
							outline: 1px solid ${TODAY_BORDER_CL_UPPER}; outline-offset: ipx;
            }
            .p-schedule__item:hover .c-schedule__text, .p-schedule__item:hover .c-schedule__time--list {
              color: ${HOVER_CL};
            }
						.schedule__list-pastday {background-color: ${PAST_BG_CL};}
						.schedule__date-today {color: ${TODAY_DATE_CL};}
						.schedule__list-today {background: linear-gradient(${TODAY_BG_CL_UPPER}, 10%, ${TODAY_BG_CL_LOWER}); border: 2px solid;
						border-image: linear-gradient(to bottom, ${TODAY_BORDER_CL_UPPER}, ${TODAY_BORDER_CL_LOWER}) 1;}
            .schedule__list-future {background-color: ${DEFAULT_BG_CL};}
            `;
				break;
			default:
				throw new Error(PAGE_TYPE_ERROR_MSG);
		}

		styleElem.textContent = styleText;

		document.head.appendChild(styleElem);

		// リスト上方 "xxxx年 yy月" 行
		const pDate = document.querySelector(SELECTORS["pDate"]);
		
		// "xxxx年" ではなく "年"のみの場合
		if (pageYear === "年") {
			const cPageYear = document.querySelector(SELECTORS["cPageYear"]);
			cPageYear.innerText = String(now.getFullYear()) + "年";
			cPageYear.style.fontSize = "4.8rem";
			document.querySelector(SELECTORS["cPageMonth"]).remove();
			document.querySelector(SELECTORS["pArrow"]).remove();
		}

	 	pDate.style.marginBottom = "5px";

		// ニュース/スケジュール リスト
		const lMainContentsUl = document.querySelector(SELECTORS["lMainContentsUl"]);

		const lMainContentsUlTop = lMainContentsUl.getBoundingClientRect().top;

		const pDateHeight = pDate.offsetHeight;

		// リスト下方 前月/次月ページャ
		const pPager = document.querySelector(".p-pager");

		// "xxxx年" ではなく "年"のみの場合
		if (pageYear === "年") {
			pPager.innerText = "";
			
			pPager.style.marginTop = "0px";
		} else {
 
			pPager.style.marginTop = `${PAGER_MARGIN_TOP}px`;
		}

		const pPagerHeight = PAGER_MARGIN_TOP + pPager.offsetHeight;
		const lMainContentsUlHeight = document.documentElement.clientHeight - pDateHeight - pPagerHeight; 

		// スクロール表示
		lMainContentsUl.setAttribute("style", `height:${lMainContentsUlHeight}px; overflow: scroll; border: solid 1px #32a1ce;`);

    const scrollTop = lMainContentsUlTop - pDateHeight;

		// スクロール位置リセット 〜「再読み込み」ボタン押下時の位置ズレ対応
		scrollTo(0, 0);

		// リスト位置までページ内で縦スクロール
		scrollTo({
			top: scrollTop,
			behavior: "smooth"
		});
		
    const dispYear = document.querySelector(SELECTORS['cPageYear']);
    const dispMonth = document.querySelector(SELECTORS['cPageMonth']);

		// 表示対象の年月(ex.202404)を取得。設定がなければ当月
		const dispYearMonth
        = ((y, m) => {return (y == null || m == null) ? nowYearMonth : y.innerText.replace('年', '') + m.innerText.replace('月', '')})(dispYear, dispMonth);
		
		// NEWS/スケジュールが当月以前の月の場合
		if (dispYearMonth < nowYearMonth) {

			lMainContentsUl.style.background = `${PAST_BG_CL}`;
		}

		const DELTA = 2;
		const createAnchor
			= (y, d) => `<a href="javascript:document.querySelector('${SELECTORS['lMainContentsUl']}').scroll({top:${y}, behavior: 'smooth'});">${d}</a>`;

		switch(pageType) {

		case "news":

			const newsList = Array.prototype.map.call(document.getElementsByClassName("c-news__date"),
				(x) => [parseInt(x.innerText.match(new RegExp(/\d{4}\.\d{2}\.(\d{2})/))[1]), x.getBoundingClientRect().top] );

			const dayMap = new Map();

			Array.prototype.forEach.call(newsList, (x) => {

				if (! dayMap.has(x[0]) || x[1] < dayMap.get(x[0])) {
					// Map(day, top)
					dayMap.set(x[0], x[1]);
				}
			});

			Map.prototype.forEach.call(dayMap, (top, day) => {
				
				Array.prototype.some.call(document.querySelectorAll("table.cale_table tbody tr td"), (td) => {

					if (!td.classList.contains("is-disabled") && day === parseInt(td.innerText)) {

						td.innerHTML = createAnchor((top - lMainContentsUlTop - DELTA), day);

						return true;
					}
				});
			});

			break;

		case "media":

			const today = now.getDate();

			lMainContentsUl.scroll(0, 0);

			let isScrolled = false;

			Array.prototype.forEach.call(document.getElementsByClassName("c-schedule__date--list"), (dayElem) => {

				// 日付(innerText)の文字列 '(日付)\n(曜日)' から日付を抽出
				let day = ((x) => {return parseInt(x.substr(0, x.indexOf(`\n`)));})(dayElem.innerText);

				Array.prototype.some.call(document.querySelectorAll("table.cale_table tbody tr td"), (td) => {

					if ( !td.classList.contains("is-disabled") && day === parseInt(td.innerText)) {

						td.innerHTML = createAnchor((dayElem.getBoundingClientRect().top - lMainContentsUlTop - DELTA), day);

						return true;
					}
				});

				// 表示スケジュールが当月の場合
				if (dispYearMonth === nowYearMonth) {
					// 過去日
					if (day < today) {

						dayElem.parentElement.classList.add("schedule__list-pastday");

					// 「今日」(ページを表示した日付)
					} else if (day === today) {

						dayElem.classList.add("schedule__date-today");
						dayElem.parentElement.classList.add("schedule__list-today");
					}

					if (day >= today) {

            dayElem.parentElement.classList.add("schedule__list-future");

            if (!isScrolled) {
					      // 「今日」以降(「今日」を含めて)で最早のスケジュール日要素にスクロール
						    lMainContentsUl.scroll({

							     top: dayElem.getBoundingClientRect().top - lMainContentsUlTop - DELTA,	 
						       behavior: "smooth"
						    });

						    isScrolled = true;
            }
					}
				}
			});

			break;

		default:
			throw new Error(PAGE_TYPE_ERROR_MSG);
		}
	})();
}, SCRIPT_NAME);