Greasy Fork is available in English.

bilibili 新动态顶栏简易修改

修改 bilibili 新动态顶栏, 使其更方便使用

// ==UserScript==
// @name         bilibili 新动态顶栏简易修改
// @namespace    http://unown.moe
// @version      0.4.2
// @description  修改 bilibili 新动态顶栏, 使其更方便使用
// @author       Unown Hearn
// @license      MIT License
// @match        *://t.bilibili.com/*
// @grant        none
// ==/UserScript==

/*
    history:
        [2017.12.1] 0.0 简单地让 bilibili 网页端新版动态的顶栏固定,
   并隐藏原本冒出来的新顶栏

        [2017.12.1] 0.1 让顶栏在小窗口时也完全显示, 顶栏的左部分可以自动折叠

        [2017.12.1] 0.2 顶栏的搜索栏在聚焦时会伸长到 2.5 倍

        [2017.12.2] 0.3 右边栏也可以自动折叠了, 顶栏左部分的主站项不会消失

        [2017.12.2] 0.3.1 完善文档

        [2017.12.2] 0.3.2 偷了个懒, 让收进去的项横着显示, 规避了错位问题,
   另外解决了一个初始化时变成 2 行的 bug

        [2017.12.2] 0.3.3 修复了一行可能会变成两行的 bug
   (除了尺寸非常小的情况之外)

        [2017.12.2] 0.4 暴力解决了网络不好时需要等待网页加载而无法执行脚本的问题

        [2017.12.2] 0.4.1 修复 bug, 改进浏览器支持(chrome, safari)

        [2017.12.2] 0.4.1 正式发布于
   greasyfork(https://greasyfork.org/zh-CN/scripts/35912-bilibili-%E6%96%B0%E5%8A%A8%E6%80%81%E9%A1%B6%E6%A0%8F%E7%AE%80%E6%98%93%E4%BF%AE%E6%94%B9)

        [2017.12.2] 0.4.2 添加遗漏掉的协议

    TODO:
        * 点击省略号后, 使面板保持开启, 直到点击其他位置或再点一次省略号

        * 要不要解决宽度过小时会发生的问题? 那并不是应该算是正常使用...

*/

(function() {
//'use strict';
//< 不然 safari 会报错

// 此脚本只管动态首页
if (!/^(\/|\/\?.*)$/.test(window.location.pathname)) {
  return;
}

console.log('启用了 userscript \'固定 bilibili 新动态的顶栏 1.0\'');

/**
 * 本 userscript 是否成功初始化
 *
 * @type boolean
 */
var initialized = false;

/**
 * 顶栏
 *
 * @type {HTMLElement}
 */
var bili_wrapper;
/**
 * 顶栏的左导航栏
 *
 * @type {HTMLElement}
 */
var nav_con_fl;
/**
 * 顶栏的右导航栏 (不包含右导航栏)
 *
 * @type {HTMLElement}
 */
var nav_con_fr;
/**
 * 顶栏的投稿栏
 *
 * @type {HTMLElement}
 */
var nav_upload;
/**
 * 顶栏的搜索栏 (从右导航栏分离)
 *
 * @type {HTMLElement}
 */
var nav_search;

/**
 * 是否在使用备选方案. 换句话说, 是否顶栏右部分被挤压了
 *
 * @type boolean
 */
var in_plan_b = false;

/**
 * 获取窗口宽度
 *
 * @returns {number} 窗口宽度
 */
function getWindowWidth() {
  return $(window).width();
}

/**
 * 获取元素的真实大小
 *
 * @param {HTMLElement} elem
 */
function getRealWidth(elem) {
  var width;

  if (elem.style.display === 'none') {
    var old_position = elem.style.position;
    var old_visibility = elem.style.visibility;
    elem.style.position = 'absolute';
    elem.style.visibility = 'hidden';
    elem.style.display = 'inline';
    width = elem.offsetWidth;
    elem.style.position = old_position;
    elem.style.display = 'none';
    elem.style.visibility = old_visibility;
    return width;
  }
  return elem.offsetWidth;
}

/**
 *  调整顶栏的某一部分导航栏
 *
 *  @param {HTMLElement} nav 导航栏
 *  @param {number} max_width 外部认为容许的最大宽度
 *  @param {HashMap} [options] 更多的选项, 包括:
 *  @param {HTMLElement} [options.plan_b] 备选可调整的导航栏
 *  @param {number} [options.nav_search_width_for_plan_b] 搜索框的最终长度, 在计算备选方案的长度时要用到
 *  @param {boolean} [options.is_left] 如果为真, 则代表正在调整的是左部分
 *
 *  @returns {number} 隐藏项的数量
 */
function __adjustNav(
    nav, max_width, options /*plan_b, nav_search_width_for_plan_b*/) {
  //< plan_b 为 undefined 代表只要管好自己就行了
  // console.log(["max width:", max_width]);
  options = options || {};

  // console.log(nav, nav.querySelector(".ul"));
  // querySelectorAll 蠢爆了, 不能限定到仅自己的子元素...
  // 包含外部项的 ul
  var nav_ul = nav.querySelector('ul');
  // "更多"的面板
  var more_panel = nav_ul.querySelector('.more-panel');
  // 外部项
  var outside_items = Array.prototype.slice.call(nav_ul.children);
  // 包含"更多"按钮的项, 位于外部项的最末尾. 可能会不 display, 但位置依旧不变
  var more_button_item = outside_items[outside_items.length - 1];



  var min_width = getRealWidth(more_button_item);
  if (options.is_left) {
    min_width += outside_items[0].offsetWidth;
    // 外部项的数组要忽略掉"第一项"和"更多"按钮
    outside_items = outside_items.slice(1, outside_items.length - 1);
  } else {
    // 外部项的数组要忽略掉"更多"按钮
    outside_items = outside_items.slice(0, outside_items.length - 1);
  }

  if (options.plan_b) {  // 有备用计划
    var max_width_for_plan_b = getWindowWidth() - min_width -
        nav_upload.offsetWidth - options.nav_search_width_for_plan_b;
    if (in_plan_b) {  //< 代表目前要先调整右部分
      if (__adjustNav(options.plan_b, max_width_for_plan_b) ===
          0) {  //< 右边没有隐藏项了, 代表可以试着调整左边了
        in_plan_b = false;

        var max_width = getWindowWidth() - nav_con_fr.offsetWidth -
            nav_upload.offsetWidth - options.nav_search_width_for_plan_b;
        __adjustNav(nav, max_width, {is_left: true});
        return;
      } else {  // 右边还没调整完, 不能调整左边, 所以直接返回
        return;
      }
    } else if (max_width < min_width) {  //< 左边不能再压榨空间了
      if (nav.offsetWidth > min_width) {
        __adjustNav(nav, min_width, {is_left: true});  //< 先把左边调整好
        // console.log([min_width, nav.offsetWidth]);
      }
      in_plan_b = true;
      console.log([nav_con_fr.offsetWidth, max_width_for_plan_b]);
      __adjustNav(options.plan_b, max_width_for_plan_b);
      return;
    }
  }

  nav.style.maxWidth = (max_width).toString() + 'px';
  // 包含隐藏项的 ul
  var more_panel_ul = more_panel.querySelector('ul');
  // 隐藏项
  var hidden_items = Array.prototype.slice.call((more_panel_ul.children));

  // 外部项的总长度
  var sum_width = 0;

  // 第一个隐藏的项(或 undefined), 用于插入新的隐藏项
  let first_hidden_item = hidden_items[0] || undefined;

  // 记录隐藏项的数量, 会在判定是否计算"更多"按钮时使用
  var hidden_count = hidden_items.length;

  // 如果为真, 则代表外边已经容不下更多项了
  var overflowed = false;

  // 将会溢出的项放入"更多"面板中
  for (var i = 0; i < outside_items.length; i++) {
    if (!overflowed) {
      sum_width += outside_items[i].offsetWidth;
      var actual_width =
          ((i === outside_items.length - 1) && hidden_count === 0) ?
          sum_width + min_width - getRealWidth(more_button_item) :
          sum_width + min_width;
      // console.log([sum_width, actual_width, getRealWidth(more_button_item),
      // more_button_item]);
      if (actual_width > max_width) {
        overflowed = true;
      }
    }

    if (overflowed) {
      // console.log(["hide:", i, outside_items[i]]);
      hidden_count++;
      if (first_hidden_item) {
        // console.log(["insert before", outside_items[i], first_hidden_item]);
        more_panel_ul.insertBefore(outside_items[i], first_hidden_item);
      } else {
        // console.log(["append child", outside_items[i]]);
        more_panel_ul.appendChild(outside_items[i]);
      }
    }
  }

  // 如果没有溢出, 试着将"更多"面板中的项取回顶栏的此半部分, 直到会溢出
  if (!overflowed) {
    for (var i = 0; i < hidden_items.length; i++) {
      sum_width += hidden_items[i].offsetWidth;

      var actual_width = (i === hidden_items.length - 1) ?
          sum_width + min_width - getRealWidth(more_button_item) :
          sum_width + min_width;
      if (actual_width > max_width) {
        overflowed = true;
        break;
      }

      hidden_count--;
      nav_ul.insertBefore(hidden_items[i], more_button_item);
    }
  }

  if (overflowed) {
    more_button_item.style.display = 'list-item';
  } else {
    more_button_item.style.display = 'none';
  }

  return hidden_count;
}

/**
 * 是否正在调整顶栏的导航栏
 *
 * @type boolean
 */
var in_adjusting = false;

/**
 * 调整顶栏
 *
 * @param {number} window_width 窗口的大小
 * @param {number} [nav_search_width] 搜索栏的最终大小
 *
 * @returns {void}
 */
function adjustNav(window_width, nav_search_width) {
  if (in_adjusting) {
    return;
  }
  in_adjusting = true;

  if (!nav_search_width) {
    nav_search_width = nav_search.offsetWidth;
  }

  var max_width = window_width - nav_con_fr.offsetWidth -
      nav_upload.offsetWidth - nav_search_width;

  __adjustNav(nav_con_fl, max_width, {
    plan_b: nav_con_fr,
    nav_search_width_for_plan_b: nav_search_width,
    is_left: true
  });

  in_adjusting = false;
}

/**
 *
 * @param {*} selector 选择器
 * @param {*} inv 尝试间隔
 * @param {*} callback 回调函数, 以毫秒为单位
 *
 * @returns {void}
 */
function waitUntilLoaded(selectors, inv, callback) {
  for (var i = 0; i < selectors.length; i++) {
    if (!document.querySelector(selectors[i])) {
      setTimeout(function() {
        waitUntilLoaded(selectors, inv, callback);
      }, inv);
      return;
    }
  }
  callback();
}

// 在页面加载后初始化本 userscript
function initialize() {
  // 让 home-container 能在正确的位置显示
  {
    var home_container = document.querySelector('.home-container');
    if (home_container) {
      home_container.style = 'position: absolute; top:42px;';
    } else {
      console.log('home-container 不存在!');
    }
  }

  // 使顶栏固定
  {
    var header = document.querySelector('.bili-header-m');
    if (header) {
      header.style = 'position: fixed; top:0; width: 100%;';
    } else {
      console.log('bili-header-m 不存在!');
      return;
    }
  }

  // ~~让顶栏的内容能正常显示~~
  {
    bili_wrapper = document.querySelector('.bili-wrapper');
    nav_con_fl = bili_wrapper.querySelector('.nav-con.fl');
    nav_con_fr = bili_wrapper.querySelector('.nav-con.fr');
    nav_upload = bili_wrapper.querySelector('.up-load');

    if (!bili_wrapper || !nav_con_fl || !nav_con_fr || !nav_upload) {
      console.log(
          '.bili-wrapper 或 .nav-con.fl 或 .nav-con.fr 或 .up-load.fr 不存在!');
      console.log([bili_wrapper, nav_con_fl, nav_con_fr, nav_upload]);
      return;
    } else {
      // nav_con_fl.style = "font-size: 0.8em; text-indent: -5px;";
      // nav_con_fr.style = "float: left;";
      // nav_upload.style = "float: left;";
      // bili_wrapper.appendChild(nav_upload);
      // nav_con_fl.style = "overflow: hidden;";
      // nav_con_fl_ul = nav_con_fl.querySelector("ul");
    }
  }

  // 把搜索栏挪出来
  {
    nav_search = bili_wrapper.querySelector('.nav-search');
    if (nav_search) {
      bili_wrapper.appendChild(nav_search);
    } else {
      console.log('.nav-search 不存在');
    }
    nav_search.style.marginRight = '0';
  }

  // 添加"更多"的按钮
  function addButtonMore(nav, is_right) {
    var ul = nav.querySelector('ul');

    var item_more = document.createElement('li');
    item_more.classList.add('nav-item');
    item_more.classList.add('more');
    item_more.style.paddingLeft = '11px';
    item_more.style.paddingRight = '11px';
    if (is_right) {
      item_more.style.display = 'none';
    }


    var button_area = document.createElement('div');
    button_area.classList.add('button-area');
    button_area.classList.add('c-pointer');  // 照猫画虎

    var more_button = document.createElement('div');
    more_button.classList.add('icon-font');
    more_button.classList.add('icon-more-1');

    var more_panel = document.createElement('div');
    more_panel.classList.add('more-panel');
    // more_panel.style = "position: absolute; display: none; left:";
    more_panel.style = 'position: absolute;';

    var more_panel_ul = document.createElement('ul');

    button_area.appendChild(more_button);
    item_more.appendChild(button_area);
    ul.appendChild(item_more);
    more_panel.appendChild(more_panel_ul);
    button_area.appendChild(more_panel);
  }

  addButtonMore(nav_con_fl);
  addButtonMore(nav_con_fr, true);

  // 让搜索框可以伸缩
  {
    var search_input = document.querySelector('.nav-search input');
    search_input.addEventListener('focusin', function() {
      // console.log(["focusin"]);
      adjustNav(window_width, 142);  // workaround
      search_input.style.width = '100px';
    });
    search_input.addEventListener('focusout', function() {
      search_input.style.width = '40px';
      setTimeout(function() {
        adjustNav(window_width);
      }, 250);  // workaround
    });
  }

  // css 相关
  {
    var sheet = document.createElement('style');

    sheet.innerHTML =
        // 抄自网站自己的 css, 用来显示面板
        '.nav-con .more-panel {' +
        'position: absolute;' +
        //'width: 94px;' +
        'text-align: center;' +
        'top: 45px;' +
        'left: -4px;' +
        //"right: 5px;"+
        'background: #fff;' +
        'border: 1px solid #e5e9ef;' +
        '-webkit-box-shadow: 0 11px 12px 0 rgba(106,115,133,0.12);' +
        'box-shadow: 0 11px 12px 0 rgba(106,115,133,0.12);' +
        'border-radius: 8px;' +
        'color: #222;' +
        'z-index: 10;' +
        // 适应大小
        'width: -moz-max-content;' +     // firefox
        'width: max-content;' +          // chrome
        'width: -webkit-max-content;' +  // safari
        '}\n' +
        // 右边
        '.nav-con.fr .more-panel {' +
        'right: 4px;' +
        'left: auto;' +
        '}\n' +
        // 抄自网站自己的 css, 用来显示面板的角
        '.nav-con .more-panel:after {' +
        'content: "";' +
        'display: block;' +
        'border-top: 1px solid #e5e9ef;' +
        'border-left: 1px solid #e5e9ef;' +
        '-webkit-transform: rotate(45deg);' +
        'transform: rotate(45deg);' +
        'width: 8px;' +
        'height: 8px;' +
        'position: absolute;' +
        'top: -5px;' +
        'left: 12px;' +
        'background: #fff;' +
        '}\n' +
        // 右边
        '.nav-con.fr .more-panel:after {' +
        'right: 12px;' +
        'left: auto;' +
        '}\n' +
        // ~~让更多面板上的项居中~~ 与面板中的项有关的样式
        '.more-panel .nav-item {' +
        //'float: none!important;' +
        'transition: none!important;' +
        //"clear: both;" +
        '}\n' +
        // 自适应宽度
        '@media screen and (max-width: 980px) {' +
        '.bili-header-m .bili-wrapper {' +
        'width: 100%;' +
        '}' +
        '}\n' +
        // 悬浮显示面板
        '.nav-item.more .more-panel {' +
        'visibility: hidden;' +
        'transition-duration: 0.1s;' +
        'transition-delay: 0.1s;' +
        '}\n' +
        '.nav-item.more:hover .more-panel {' +
        'visibility: visible;' +
        'transition-duration: 0.1s;' +
        'transition-delay: 0s;' +
        '}\n' +
        // 让 home 的位置好看些
        '.nav-item.home { ' +
        'margin-left: 0!important;' +
        'transition-duration: 0s;' +
        'transition-delay: 0s;' +
        '}\n';


    document.head.appendChild(sheet);
  }

  // setTimeout(function() {
  adjustNav(getWindowWidth());
  initialized = true;
  //}, 1000);



  // 隐藏会冒出来的新顶栏
  {
    var sticky_bar = document.querySelector('.sticky-bar');
    if (sticky_bar) {
      sticky_bar.style = 'visibility: hidden;';
    } else {
      console.log('sticky-bar 不存在!');
    }
  }
}

waitUntilLoaded(['.nav-con.fl', '.nav-con.fr', '.up-load.fr'], 50, initialize);

/**
 * 用于检测是否是窗口的宽度发生了变化
 *
 * @type {number}
 */
var window_width = getWindowWidth();

// 监视窗口调整大小
window.addEventListener('resize', function() {

  if (!initialized) {
    return;
  }

  var new_width = getWindowWidth();
  if (window_width === new_width) {
    return;
  }

  window_width = new_width;

  // nav_con_fl, nav_con_fr, nav_upload;

  adjustNav(window_width);

  // console.log(nav_con_fl.style.maxWidth);

});

})();