Confluence Plus

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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()
})();