Confluence Plus

Add permalink to conflucence document, Enhanced side tree, Markdown Editor, Fast Access Badges.

// ==UserScript==
// @name         Confluence Plus
// @namespace    https://blog.simplenaive.cn
// @version      0.14
// @description  Add permalink to conflucence document, Enhanced side tree, Markdown Editor, Fast Access Badges.
// @author       Yidadaa
// @match        https://confluence.zhenguanyu.com/*
// @match        https://iwiki.woa.com/pages/*
// @icon         https://www.google.com/s2/favicons?domain=zhenguanyu.com
// @grant        none
// @license      MIT
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/markdown-it.min.js
// ==/UserScript==

// extend history listener
(function (history) {
  const pushState = history.pushState;
  history.pushState = function (state) {
    if (typeof history.onpushstate == "function") {
      history.onpushstate({ state: state });
    }
    return pushState.apply(history, arguments);
  };
})(window.history);

class Markdown {
  constructor() {
    this.createInputDom()
    this.mdit = new window.markdownit()
    this.dom = document.createElement('div')
    this.dom.className = '_yifei-md-content'
  }

  createInputDom() {
    const mdWrapper = document.createElement('div')
    mdWrapper.className = '_yifei-markdown'

    const mdInput = document.createElement('textarea')
    mdInput.className = '_yifei-md-input _yifei-markdown-hidden'
    mdInput.placeholder = '本编辑器会在页面的光标处插入 html 文本'
    
    const mdTitle = document.createElement('div')
    mdTitle.className = '_yifei-md-title'
    mdTitle.innerText = 'Markdown 编辑器'
    mdTitle.onclick = () => {
      mdTitle.shouldShow = !mdTitle.shouldShow
      if (mdTitle.shouldShow) {
        mdInput.classList.remove('_yifei-markdown-hidden')
      } else {
        mdInput.classList.add('_yifei-markdown-hidden')
      }
    }

    mdInput.oninput = () => {
      const res = this.mdit.render(mdInput.value)
      console.log('[md] ', res)
      this.dom.innerHTML = res
      this.render()
    }

    mdWrapper.appendChild(mdTitle)
    mdWrapper.appendChild(mdInput)
    document.body.appendChild(mdWrapper)
  }

  render() {
    let contentDom = Array.from(window.frames).find(v => v.document.body.id == 'tinymce')

    if (contentDom.enhanced) return
    const select = contentDom.getSelection()
    select.getRangeAt(0).insertNode(this.dom)
    contentDom.enhanced = true
  }
}

(function () {
  'use strict';
  const styles = `
    .header-with-link {
        display: flex;
        align-items: center;
    }
    .header-link {
        color: #0049B0!important;
        border: 2px solid #0049B0;
        border-radius: 5px;
        font-size: 14px;
        margin-left: 10px;
        padding: 0px 3px;
    }
    ._yifei-message {
        position: fixed;
        top: 150px;
        box-shadow: 0 2px 10px rgb(0 0 0 / 25%);
        background: white;
        color: black: translateY(-50px);
        transition: all ease .3s;
        left: 50%;
        padding: 10px 20px;
        border-radius: 5px;
        opacity: 0;
    }

    ._yifei-message-show {
        transform: translateY(0);
        opacity: 1;
    }

    ._yifei-markdown {
      position: fixed;
      z-index: 999;
      top: 20vh;
      right: 100px;
      z-index: 999;
      opacity: 0.8;
      background: white;
      padding: 10px;
      box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
      border-radius: 5px;
    }
    
    ._yifei-markdown-hidden {
      display: none;
    }
    
    ._yifei-md-title {
      cursor: pointer;
      line-height: 2;
    }

    ._yifei-md-input {
      height: 60vh;
      width: 300px;
      padding: 10px;
      background: white;
    }

    .plugin_pagetree_children_content:hover {
        background: #eee;
        cursor: pointer;
    }

    .plugin_pagetree_children_list > li {
        margin: 0!important;
    }

    .plugin_pagetree_children_content {
        padding: 5px;
        border-radius: 3px;
    }

    .plugin_pagetree_childtoggle_container {
        padding-top: 3px;
    }
  `

  // utils
  const $ = s => document.querySelector(s);
  const $$ = s => Array.from(document.querySelectorAll(s));
  const wait = (delay = 100) => new Promise((res) => {
    setTimeout(
      res, delay
    )
  });

  // config
  const config = {
    debug: false
  }
  
  // 只在 iframe 中生效
  if (self == top) return

  const addMouseMoveListener = (cb = () => { }) => {
    if (document.subs === undefined) {
      document.subs = new Set()
      document.onmousemove = () => {
        document.subs.forEach((cb, i) => {
          config.debug && console.log(`[Mouse Move Listenser] ${i} called`)
          cb()
        })
      }
    }

    document.subs.add(cb)
  }
  const addStyle = () => {
    const styleSheet = document.createElement("style")
    styleSheet.innerText = styles
    document.head.appendChild(styleSheet)
  }

  class Message {
    constructor() {
      this.dom = document.createElement('div')
      this.dom.className = '_yifei-message'
      this.SHOW_CLASS = '_yifei-message-show'
      this.timeout = null;
      document.body.appendChild(this.dom)
    }
    show(text) {
      this.timeout && clearTimeout(this.timeout)
      this.dom.innerText = text
      this.dom.classList.add(this.SHOW_CLASS)
      this.timeout = setTimeout(() => this.hide(), 1500)
    }

    hide() {
      this.dom.classList.remove(this.SHOW_CLASS)
    }
  }

  const message = new Message()
  const md = new Markdown()

  const addLinkToHeader = () => {
    const headers = new Array(6).fill(0).map((v, i) => {
      return $$(`h${i + 1}`)
    }).reduce((p, c) => p.concat(c), []).filter(v => v.id)
    console.log(headers)

    headers.forEach(h => {
      const link = document.createElement('a')
      link.className = 'header-link'
      link.innerText = '#'

      link.href = location.hash ? location.href.replace(location.hash, `#${h.id}`) : location.href + `#${h.id}`
      link.title = 'click to copy link'

      link.onclick = () => {
        console.log('click', link.href)
        message.show('链接已复制到剪切板')
        navigator.clipboard.writeText(link.href)
      };

      h.classList.add('header-with-link')
      h.appendChild(link)
    })
  }

  const addLinkToComment = () => {
    const comments = $$('.comment-thread')
    console.log(comments)

    comments.forEach(c => {
      const actions = c.querySelector('.comment-actions')

      const action = document.createElement('ul')
      action.className = 'comment-action-copy'

      const link = document.createElement('a')

      link.innerText = '复制评论链接'
      link.href = location.hash ? location.href.replace(location.hash, `#${c.id}`) : location.href + `#${c.id}`
      link.title = 'click to copy link'

      link.onclick = () => {
        console.log('click', link.href)
        message.show('链接已复制到剪切板')
        navigator.clipboard.writeText(link.href)
      };

      action.appendChild(link)
      actions.appendChild(action)
    })
  }

  const addPreviewBtnToEditPage = () => {
    if (location.href.indexOf('resumedraft') < 0 || location.href.indexOf('editpage') < 0) return;
    console.log('add preview btn')
    const expandBtn = $('#rte-button-ellipsis')
    const btnContainer = $('.cancel-button-container-shared-draft')

    const doPreview = () => {
      wait().then(() => {
        expandBtn.click()
        const previewBtn = $('#rte-button-preview')
        previewBtn.click()
      })
    }

    const prevBtn = document.createElement('button')
    prevBtn.className = 'aui-button'
    btnContainer.appendChild(prevBtn)
  }

  const enhanceTree = () => {
    const doEnhance = () => {
      const items = $$('.plugin_pagetree_children_content')

      items.forEach(dom => {
        if (dom.enhanced) return
        dom.enhanced = true
        dom.onclick = () => {
          dom.previousElementSibling.children[0].click()
        }
      })
    }

    const listenDom = () => {
      const side = $('.acs-side-bar')
      if (!side || side.enhanced) return

      // observe side bar
      const config = { childList: true, attributes: true }
      const callback = function (mutationsList) {
        // enhance child tree when new items loaded
        doEnhance()
      };

      const observer = new MutationObserver(callback)
      observer.observe(side, config)

      side.enhanced = true
      console.log('observed', side)

      // enhance first
      doEnhance()

      // disable onmousemove event
      document.subs.delete(listenDom)
    }

    addMouseMoveListener(listenDom)
  }

  const openDialog = () => {
    const dialog = $('.content-macro')
    console.log('opening', dialog)
    dialog.click()
    closeDialog()
  }

  const closeDialog = () => {
    const cancel = $('#macro-details-page .button-panel-cancel-link')
    cancel.click()
  }

  const confirmDialog = (t = 500) => {
    setTimeout(() => $('#macro-details-page .button-panel-button.ok').click(), t)
  }

  const addFastInfo = () => {
    const buttons = [
      ['#macro-info', 'info-filled', '信息', confirmDialog],
      ['#macro-children', 'overview', '子页面', () => {
        setTimeout(() => {
          $('#macro-param-all').click()
          confirmDialog(100)
        }, 500)
      }],
      ['#macro-status', ' confluence-icon-status-macro', '状态', () => {
        confirmDialog(500)
      }]
    ]

    const tryToAddDom = () => {
      const toolbar = $('.aui-toolbar2-primary')
      if (!toolbar || toolbar.enhanced || !location.href.includes('resume')) return

      openDialog()
      const newTools = document.createElement('ul')
      newTools.className = 'aui-buttons'

      buttons.forEach(([bid, icon, name, cb]) => {
        console.log(bid, icon, name)

        // create new icons
        const li = document.createElement('li')
        li.className = 'toolbar-item aui-button aui-button-subtle'

        li.innerHTML = `
                  <span class="icon aui-icon aui-icon-small aui-iconfont-${icon}">${name}</span>
              `
        li.onclick = () => {
          $(bid).click()
          console.log('click', bid)
          cb()
        }

        newTools.appendChild(li)
      })

      toolbar.enhanced = true
      document.subs.delete(tryToAddDom)

      toolbar.appendChild(newTools)
    }


    addMouseMoveListener(tryToAddDom)
  }

  const enhanceStatus = () => {
    const colorActionMap = {
      'Grey': 'PLAN',
      'Red': 'BLOCKED',
      'Yellow': 'DELAY',
      'Green': 'RESOLVED',
      'Blue': 'PENDING'
    }
    const remapColor = {
      'Yellow': '#ffab00'
    }
    const doEnhanceStatus = () => {
      const statusDoms = $$('.status-macro-title')
      statusDoms.forEach(input => {
        const statusDom = input.parentElement.parentElement;
        if (statusDom.enhanced) return
        input.click()
        const statusInput = statusDom.querySelector('.status-macro-title')
        console.log(statusInput)

        Array.from(statusDom.querySelectorAll('.aui-button')).filter(v => v.className.includes('macro-property')).forEach(v => {
          const color = v.classList[1].split('-')[3]

          const newStatusDom = document.createElement('div')
          newStatusDom.className = 'aui-button'
          newStatusDom.innerText = colorActionMap[color]
          newStatusDom.style.color = remapColor[color] || color.toLowerCase()
          newStatusDom.style.marginTop = '10px'
          newStatusDom.onclick = () => {
            statusInput.value = colorActionMap[color]
            v.click()
          }

          statusDom.appendChild(newStatusDom)
        })
        statusDom.enhanced = true

      })
    }

    addMouseMoveListener(doEnhanceStatus)
  }

  const debugMode = () => {
    const userLinks = $$('.confluence-userlink')
    userLinks.forEach(v => v.style.filter = 'blur(4px)')
    $('#breadcrumbs').style.filter = 'blur(4px)'
    document.onclick = () => $('#wm').style.filter = 'blur(5px)'
  }
  
  const addHighlight = () => {
    const link = document.createElement('link');
    link.href = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.3.1/styles/default.min.css'
    link.rel = 'stylesheet'
    document.head.appendChild(link);
    const script = document.createElement('script');
    script.src = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.3.1/highlight.min.js'
    script.onload = () => hljs.highlightAll();
    document.head.appendChild(script);
  }

  addStyle()
  addLinkToHeader()
  addLinkToComment()
  addPreviewBtnToEditPage()
  enhanceTree()
  addFastInfo()
  enhanceStatus()
  addHighlight()

  history.onpushstate = addFastInfo // listen history change
  config.debug && debugMode()
})();