Confluence Plus

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

  1. // ==UserScript==
  2. // @name Confluence Plus
  3. // @namespace https://blog.simplenaive.cn
  4. // @version 0.14
  5. // @description Add permalink to conflucence document, Enhanced side tree, Markdown Editor, Fast Access Badges.
  6. // @author Yidadaa
  7. // @match https://confluence.zhenguanyu.com/*
  8. // @match https://iwiki.woa.com/pages/*
  9. // @icon https://www.google.com/s2/favicons?domain=zhenguanyu.com
  10. // @grant none
  11. // @license MIT
  12. // @require https://cdn.jsdelivr.net/npm/markdown-it@12.2.0/dist/markdown-it.min.js
  13. // ==/UserScript==
  14.  
  15. // extend history listener
  16. (function (history) {
  17. const pushState = history.pushState;
  18. history.pushState = function (state) {
  19. if (typeof history.onpushstate == "function") {
  20. history.onpushstate({ state: state });
  21. }
  22. return pushState.apply(history, arguments);
  23. };
  24. })(window.history);
  25.  
  26. class Markdown {
  27. constructor() {
  28. this.createInputDom()
  29. this.mdit = new window.markdownit()
  30. this.dom = document.createElement('div')
  31. this.dom.className = '_yifei-md-content'
  32. }
  33.  
  34. createInputDom() {
  35. const mdWrapper = document.createElement('div')
  36. mdWrapper.className = '_yifei-markdown'
  37.  
  38. const mdInput = document.createElement('textarea')
  39. mdInput.className = '_yifei-md-input _yifei-markdown-hidden'
  40. mdInput.placeholder = '本编辑器会在页面的光标处插入 html 文本'
  41. const mdTitle = document.createElement('div')
  42. mdTitle.className = '_yifei-md-title'
  43. mdTitle.innerText = 'Markdown 编辑器'
  44. mdTitle.onclick = () => {
  45. mdTitle.shouldShow = !mdTitle.shouldShow
  46. if (mdTitle.shouldShow) {
  47. mdInput.classList.remove('_yifei-markdown-hidden')
  48. } else {
  49. mdInput.classList.add('_yifei-markdown-hidden')
  50. }
  51. }
  52.  
  53. mdInput.oninput = () => {
  54. const res = this.mdit.render(mdInput.value)
  55. console.log('[md] ', res)
  56. this.dom.innerHTML = res
  57. this.render()
  58. }
  59.  
  60. mdWrapper.appendChild(mdTitle)
  61. mdWrapper.appendChild(mdInput)
  62. document.body.appendChild(mdWrapper)
  63. }
  64.  
  65. render() {
  66. let contentDom = Array.from(window.frames).find(v => v.document.body.id == 'tinymce')
  67.  
  68. if (contentDom.enhanced) return
  69. const select = contentDom.getSelection()
  70. select.getRangeAt(0).insertNode(this.dom)
  71. contentDom.enhanced = true
  72. }
  73. }
  74.  
  75. (function () {
  76. 'use strict';
  77. const styles = `
  78. .header-with-link {
  79. display: flex;
  80. align-items: center;
  81. }
  82. .header-link {
  83. color: #0049B0!important;
  84. border: 2px solid #0049B0;
  85. border-radius: 5px;
  86. font-size: 14px;
  87. margin-left: 10px;
  88. padding: 0px 3px;
  89. }
  90. ._yifei-message {
  91. position: fixed;
  92. top: 150px;
  93. box-shadow: 0 2px 10px rgb(0 0 0 / 25%);
  94. background: white;
  95. color: black: translateY(-50px);
  96. transition: all ease .3s;
  97. left: 50%;
  98. padding: 10px 20px;
  99. border-radius: 5px;
  100. opacity: 0;
  101. }
  102.  
  103. ._yifei-message-show {
  104. transform: translateY(0);
  105. opacity: 1;
  106. }
  107.  
  108. ._yifei-markdown {
  109. position: fixed;
  110. z-index: 999;
  111. top: 20vh;
  112. right: 100px;
  113. z-index: 999;
  114. opacity: 0.8;
  115. background: white;
  116. padding: 10px;
  117. box-shadow: 0 0 10px rgba(0, 0, 0, 0.2);
  118. border-radius: 5px;
  119. }
  120. ._yifei-markdown-hidden {
  121. display: none;
  122. }
  123. ._yifei-md-title {
  124. cursor: pointer;
  125. line-height: 2;
  126. }
  127.  
  128. ._yifei-md-input {
  129. height: 60vh;
  130. width: 300px;
  131. padding: 10px;
  132. background: white;
  133. }
  134.  
  135. .plugin_pagetree_children_content:hover {
  136. background: #eee;
  137. cursor: pointer;
  138. }
  139.  
  140. .plugin_pagetree_children_list > li {
  141. margin: 0!important;
  142. }
  143.  
  144. .plugin_pagetree_children_content {
  145. padding: 5px;
  146. border-radius: 3px;
  147. }
  148.  
  149. .plugin_pagetree_childtoggle_container {
  150. padding-top: 3px;
  151. }
  152. `
  153.  
  154. // utils
  155. const $ = s => document.querySelector(s);
  156. const $$ = s => Array.from(document.querySelectorAll(s));
  157. const wait = (delay = 100) => new Promise((res) => {
  158. setTimeout(
  159. res, delay
  160. )
  161. });
  162.  
  163. // config
  164. const config = {
  165. debug: false
  166. }
  167. // 只在 iframe 中生效
  168. if (self == top) return
  169.  
  170. const addMouseMoveListener = (cb = () => { }) => {
  171. if (document.subs === undefined) {
  172. document.subs = new Set()
  173. document.onmousemove = () => {
  174. document.subs.forEach((cb, i) => {
  175. config.debug && console.log(`[Mouse Move Listenser] ${i} called`)
  176. cb()
  177. })
  178. }
  179. }
  180.  
  181. document.subs.add(cb)
  182. }
  183. const addStyle = () => {
  184. const styleSheet = document.createElement("style")
  185. styleSheet.innerText = styles
  186. document.head.appendChild(styleSheet)
  187. }
  188.  
  189. class Message {
  190. constructor() {
  191. this.dom = document.createElement('div')
  192. this.dom.className = '_yifei-message'
  193. this.SHOW_CLASS = '_yifei-message-show'
  194. this.timeout = null;
  195. document.body.appendChild(this.dom)
  196. }
  197. show(text) {
  198. this.timeout && clearTimeout(this.timeout)
  199. this.dom.innerText = text
  200. this.dom.classList.add(this.SHOW_CLASS)
  201. this.timeout = setTimeout(() => this.hide(), 1500)
  202. }
  203.  
  204. hide() {
  205. this.dom.classList.remove(this.SHOW_CLASS)
  206. }
  207. }
  208.  
  209. const message = new Message()
  210. const md = new Markdown()
  211.  
  212. const addLinkToHeader = () => {
  213. const headers = new Array(6).fill(0).map((v, i) => {
  214. return $$(`h${i + 1}`)
  215. }).reduce((p, c) => p.concat(c), []).filter(v => v.id)
  216. console.log(headers)
  217.  
  218. headers.forEach(h => {
  219. const link = document.createElement('a')
  220. link.className = 'header-link'
  221. link.innerText = '#'
  222.  
  223. link.href = location.hash ? location.href.replace(location.hash, `#${h.id}`) : location.href + `#${h.id}`
  224. link.title = 'click to copy link'
  225.  
  226. link.onclick = () => {
  227. console.log('click', link.href)
  228. message.show('链接已复制到剪切板')
  229. navigator.clipboard.writeText(link.href)
  230. };
  231.  
  232. h.classList.add('header-with-link')
  233. h.appendChild(link)
  234. })
  235. }
  236.  
  237. const addLinkToComment = () => {
  238. const comments = $$('.comment-thread')
  239. console.log(comments)
  240.  
  241. comments.forEach(c => {
  242. const actions = c.querySelector('.comment-actions')
  243.  
  244. const action = document.createElement('ul')
  245. action.className = 'comment-action-copy'
  246.  
  247. const link = document.createElement('a')
  248.  
  249. link.innerText = '复制评论链接'
  250. link.href = location.hash ? location.href.replace(location.hash, `#${c.id}`) : location.href + `#${c.id}`
  251. link.title = 'click to copy link'
  252.  
  253. link.onclick = () => {
  254. console.log('click', link.href)
  255. message.show('链接已复制到剪切板')
  256. navigator.clipboard.writeText(link.href)
  257. };
  258.  
  259. action.appendChild(link)
  260. actions.appendChild(action)
  261. })
  262. }
  263.  
  264. const addPreviewBtnToEditPage = () => {
  265. if (location.href.indexOf('resumedraft') < 0 || location.href.indexOf('editpage') < 0) return;
  266. console.log('add preview btn')
  267. const expandBtn = $('#rte-button-ellipsis')
  268. const btnContainer = $('.cancel-button-container-shared-draft')
  269.  
  270. const doPreview = () => {
  271. wait().then(() => {
  272. expandBtn.click()
  273. const previewBtn = $('#rte-button-preview')
  274. previewBtn.click()
  275. })
  276. }
  277.  
  278. const prevBtn = document.createElement('button')
  279. prevBtn.className = 'aui-button'
  280. btnContainer.appendChild(prevBtn)
  281. }
  282.  
  283. const enhanceTree = () => {
  284. const doEnhance = () => {
  285. const items = $$('.plugin_pagetree_children_content')
  286.  
  287. items.forEach(dom => {
  288. if (dom.enhanced) return
  289. dom.enhanced = true
  290. dom.onclick = () => {
  291. dom.previousElementSibling.children[0].click()
  292. }
  293. })
  294. }
  295.  
  296. const listenDom = () => {
  297. const side = $('.acs-side-bar')
  298. if (!side || side.enhanced) return
  299.  
  300. // observe side bar
  301. const config = { childList: true, attributes: true }
  302. const callback = function (mutationsList) {
  303. // enhance child tree when new items loaded
  304. doEnhance()
  305. };
  306.  
  307. const observer = new MutationObserver(callback)
  308. observer.observe(side, config)
  309.  
  310. side.enhanced = true
  311. console.log('observed', side)
  312.  
  313. // enhance first
  314. doEnhance()
  315.  
  316. // disable onmousemove event
  317. document.subs.delete(listenDom)
  318. }
  319.  
  320. addMouseMoveListener(listenDom)
  321. }
  322.  
  323. const openDialog = () => {
  324. const dialog = $('.content-macro')
  325. console.log('opening', dialog)
  326. dialog.click()
  327. closeDialog()
  328. }
  329.  
  330. const closeDialog = () => {
  331. const cancel = $('#macro-details-page .button-panel-cancel-link')
  332. cancel.click()
  333. }
  334.  
  335. const confirmDialog = (t = 500) => {
  336. setTimeout(() => $('#macro-details-page .button-panel-button.ok').click(), t)
  337. }
  338.  
  339. const addFastInfo = () => {
  340. const buttons = [
  341. ['#macro-info', 'info-filled', '信息', confirmDialog],
  342. ['#macro-children', 'overview', '子页面', () => {
  343. setTimeout(() => {
  344. $('#macro-param-all').click()
  345. confirmDialog(100)
  346. }, 500)
  347. }],
  348. ['#macro-status', ' confluence-icon-status-macro', '状态', () => {
  349. confirmDialog(500)
  350. }]
  351. ]
  352.  
  353. const tryToAddDom = () => {
  354. const toolbar = $('.aui-toolbar2-primary')
  355. if (!toolbar || toolbar.enhanced || !location.href.includes('resume')) return
  356.  
  357. openDialog()
  358. const newTools = document.createElement('ul')
  359. newTools.className = 'aui-buttons'
  360.  
  361. buttons.forEach(([bid, icon, name, cb]) => {
  362. console.log(bid, icon, name)
  363.  
  364. // create new icons
  365. const li = document.createElement('li')
  366. li.className = 'toolbar-item aui-button aui-button-subtle'
  367.  
  368. li.innerHTML = `
  369. <span class="icon aui-icon aui-icon-small aui-iconfont-${icon}">${name}</span>
  370. `
  371. li.onclick = () => {
  372. $(bid).click()
  373. console.log('click', bid)
  374. cb()
  375. }
  376.  
  377. newTools.appendChild(li)
  378. })
  379.  
  380. toolbar.enhanced = true
  381. document.subs.delete(tryToAddDom)
  382.  
  383. toolbar.appendChild(newTools)
  384. }
  385.  
  386.  
  387. addMouseMoveListener(tryToAddDom)
  388. }
  389.  
  390. const enhanceStatus = () => {
  391. const colorActionMap = {
  392. 'Grey': 'PLAN',
  393. 'Red': 'BLOCKED',
  394. 'Yellow': 'DELAY',
  395. 'Green': 'RESOLVED',
  396. 'Blue': 'PENDING'
  397. }
  398. const remapColor = {
  399. 'Yellow': '#ffab00'
  400. }
  401. const doEnhanceStatus = () => {
  402. const statusDoms = $$('.status-macro-title')
  403. statusDoms.forEach(input => {
  404. const statusDom = input.parentElement.parentElement;
  405. if (statusDom.enhanced) return
  406. input.click()
  407. const statusInput = statusDom.querySelector('.status-macro-title')
  408. console.log(statusInput)
  409.  
  410. Array.from(statusDom.querySelectorAll('.aui-button')).filter(v => v.className.includes('macro-property')).forEach(v => {
  411. const color = v.classList[1].split('-')[3]
  412.  
  413. const newStatusDom = document.createElement('div')
  414. newStatusDom.className = 'aui-button'
  415. newStatusDom.innerText = colorActionMap[color]
  416. newStatusDom.style.color = remapColor[color] || color.toLowerCase()
  417. newStatusDom.style.marginTop = '10px'
  418. newStatusDom.onclick = () => {
  419. statusInput.value = colorActionMap[color]
  420. v.click()
  421. }
  422.  
  423. statusDom.appendChild(newStatusDom)
  424. })
  425. statusDom.enhanced = true
  426.  
  427. })
  428. }
  429.  
  430. addMouseMoveListener(doEnhanceStatus)
  431. }
  432.  
  433. const debugMode = () => {
  434. const userLinks = $$('.confluence-userlink')
  435. userLinks.forEach(v => v.style.filter = 'blur(4px)')
  436. $('#breadcrumbs').style.filter = 'blur(4px)'
  437. document.onclick = () => $('#wm').style.filter = 'blur(5px)'
  438. }
  439. const addHighlight = () => {
  440. const link = document.createElement('link');
  441. link.href = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.3.1/styles/default.min.css'
  442. link.rel = 'stylesheet'
  443. document.head.appendChild(link);
  444. const script = document.createElement('script');
  445. script.src = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.3.1/highlight.min.js'
  446. script.onload = () => hljs.highlightAll();
  447. document.head.appendChild(script);
  448. }
  449.  
  450. addStyle()
  451. addLinkToHeader()
  452. addLinkToComment()
  453. addPreviewBtnToEditPage()
  454. enhanceTree()
  455. addFastInfo()
  456. enhanceStatus()
  457. addHighlight()
  458.  
  459. history.onpushstate = addFastInfo // listen history change
  460. config.debug && debugMode()
  461. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址