便捷复制页签 title

Ctrl + q 复制页签标题;Alt + q 复制页签标题及链接,生成 markdown 格式

  1. // ==UserScript==
  2. // @name Copy title
  3. // @name:zh-CN 便捷复制页签 title
  4. // @namespace http://tampermonkey.net/Henry
  5. // @version 1.0.3
  6. // @description use Ctrl + q copy title; Alt + q copy title and url, create markdown
  7. // @description:zh-CN Ctrl + q 复制页签标题;Alt + q 复制页签标题及链接,生成 markdown 格式
  8. // @author Henry
  9. // @icon https://tsz.netlify.app/img/favicon.png
  10. // @match http*://*/*
  11. // @license MIT
  12. // ==/UserScript==
  13.  
  14. ;(function () {
  15. 'use strict'
  16.  
  17. let messageComponent = null
  18. document.addEventListener('keydown', listener, false)
  19.  
  20. function listener(event) {
  21. const { keyCode, ctrlKey, altKey } = event
  22. if (keyCode === 81 && ctrlKey) {
  23. event.preventDefault()
  24. event.stopPropagation()
  25. copyTextToClipboard(document.title)
  26. return false
  27. }
  28. if (keyCode === 81 && altKey) {
  29. event.preventDefault()
  30. event.stopPropagation()
  31. copyTextToClipboard(`[${document.title}](${location.href})`)
  32. return false
  33. }
  34. }
  35.  
  36. function copyTextToClipboard(text) {
  37. if (!navigator.clipboard) {
  38. fallbackCopyTextToClipboard(text)
  39. return
  40. }
  41. navigator.clipboard.writeText(text).then(
  42. function () {
  43. wrapperMsg(`Copying: ${text}`, window.MessageType.SUCCESS)
  44. },
  45. function (err) {
  46. console.log('copyTextToClipboard ~ err:', err)
  47. wrapperMsg('Oops, unable to copy', window.MessageType.ERROR)
  48. }
  49. )
  50. }
  51.  
  52. function wrapperMsg(content, type) {
  53. // 初始化绑定
  54. if (!messageComponent) {
  55. messageComponent = new window.MessageControl()
  56. }
  57. // 调用
  58. messageComponent.message({ content, type })
  59. }
  60.  
  61. function fallbackCopyTextToClipboard(text) {
  62. let textArea = document.createElement('textarea')
  63. textArea.value = text
  64.  
  65. // Avoid scrolling to bottom
  66. textArea.style.top = '0'
  67. textArea.style.left = '0'
  68. textArea.style.position = 'fixed'
  69.  
  70. document.body.appendChild(textArea)
  71. textArea.focus()
  72. textArea.select()
  73.  
  74. try {
  75. var successful = document.execCommand('copy')
  76. var msg = successful ? text : ''
  77. wrapperMsg(`Copying: ${msg}`, window.MessageType.SUCCESS)
  78. } catch (err) {
  79. console.log('fallbackCopyTextToClipboard ~ err:', err)
  80. wrapperMsg('Oops, unable to copy', window.MessageType.ERROR)
  81. }
  82.  
  83. document.body.removeChild(textArea)
  84. }
  85. })()
  86. ;(function () {
  87. // 消息类型
  88. const MessageType = {
  89. MESSAGE: 'message', // 普通
  90. SUCCESS: 'success', // 成功
  91. ERROR: 'error', // 错误
  92. WARNING: 'warning' // 警告
  93. }
  94.  
  95. // 状态对应的主色
  96. const MessageTypeColor = {
  97. MESSAGE: '#909399',
  98. SUCCESS: '#67c23a',
  99. ERROR: '#f56c6c',
  100. WARNING: '#e6a23c'
  101. }
  102.  
  103. // 创建DOM
  104. const createDom = ({ isId = false, name = '', tag = 'div' }) => {
  105. if (!tag) {
  106. return null
  107. }
  108. const ele = document.createElement(tag)
  109. if (name) {
  110. if (isId) {
  111. ele.id = name
  112. } else {
  113. ele.className = name
  114. }
  115. }
  116. return ele
  117. }
  118.  
  119. // 获取类型对应的背景色
  120. const getTypeBGColor = type => {
  121. let bgColor = ''
  122. switch (type) {
  123. case MessageType.SUCCESS:
  124. bgColor = 'background-color: #f0f9eb'
  125. break
  126. case MessageType.ERROR:
  127. bgColor = 'background-color: #f0f9eb'
  128. break
  129. case MessageType.WARNING:
  130. bgColor = 'background-color: #f0f9eb'
  131. break
  132. default:
  133. bgColor = 'background-color: #edf2fc'
  134. break
  135. }
  136. return bgColor
  137. }
  138.  
  139. // 获取类型对应的背景色、文字颜色
  140. const getTypeDomCss = type => {
  141. let cssStr = ''
  142. let commonCss = ''
  143. switch (type) {
  144. case MessageType.SUCCESS:
  145. cssStr = commonCss + `${getTypeBGColor(type)};color: ${MessageTypeColor.SUCCESS};`
  146. break
  147. case MessageType.ERROR:
  148. cssStr = commonCss + `${getTypeBGColor(type)};color: ${MessageTypeColor.ERROR};`
  149. break
  150. case MessageType.WARNING:
  151. cssStr = commonCss + `${getTypeBGColor(type)};color: ${MessageTypeColor.WARNING};`
  152. break
  153. default:
  154. cssStr = commonCss + `${getTypeBGColor(type)};color: ${MessageTypeColor.MESSAGE};`
  155. break
  156. }
  157. return cssStr
  158. }
  159.  
  160. const createMessage = (
  161. { type, content, duration, delay, againBtn, minWidth, maxWidth },
  162. mainContainer
  163. ) => {
  164. if (!mainContainer) {
  165. console.error('主容器不存在,查看调用流程,确保doucument.body已生成!')
  166. return
  167. }
  168. /**随机的key */
  169. const randomKey = Math.floor(Math.random() * (99999 - 10002)) + 10002
  170.  
  171. /**属性配置 */
  172. const config = {
  173. isRemove: false, // 是否被移除了
  174. type: type || MessageType.MESSAGE, // 类型 message success error warning
  175. content: content || '', // 提示内容
  176. duration: duration || 3000, // 显示时间
  177. delay: delay || 0, // 弹出延迟
  178. timeout: null, // 计时器事件
  179. againBtn: againBtn || false // 是否需要显示 不再提示 按钮
  180. }
  181. // #region 生成DOM、样式、关系
  182. const messageContainer = createDom({ name: `message-${randomKey}`, tag: 'div' })
  183. messageContainer.style = `
  184. min-width: ${minWidth}px;
  185. max-width:${maxWidth}px;
  186. padding: 12px 12px;
  187. margin-top: -20px;
  188. border-radius: 4px;
  189. box-shadow: -5px 5px 12px 0 rgba(204, 204, 204, 0.8);
  190. ${getTypeBGColor(config.type)};
  191. animation: all cubic-bezier(0.18, 0.89, 0.32, 1.28) 0.4s;
  192. transition: all .4s;
  193. pointer-events: auto;
  194. overflow:hidden;
  195. `
  196. /**内容区域 */
  197. const messageTypeDom = createDom({ tag: 'div' })
  198. messageTypeDom.style = getTypeDomCss(config.type)
  199. /**文本内容 */
  200. const messageTypeText = createDom({ tag: 'span' })
  201. messageTypeText.style = 'font-size: 14px;line-height: 20px;'
  202. messageTypeText.innerHTML = config.content
  203. /**建立html树关系 */
  204. messageTypeDom.appendChild(messageTypeText)
  205. messageContainer.appendChild(messageTypeDom)
  206. /**不再提示的按钮 */
  207. if (config.againBtn) {
  208. const messageAgainDiv = createDom({ name: 'message-again-btn', tag: 'div' })
  209. messageAgainDiv.style = `margin-top: 5px;text-align: right;`
  210. const messageAgainBtnText = createDom({ name: 'message-again-text', tag: 'span' })
  211. messageAgainBtnText.innerHTML = '不再提示'
  212. messageAgainBtnText.style = `
  213. font-size: 12px;
  214. color: rgb(204, 201, 201);
  215. border-bottom: 1px solid rgb(204, 201, 201);
  216. cursor: pointer;
  217. `
  218. // 鼠标移入
  219. messageAgainBtnText.onmouseover = () => {
  220. messageAgainBtnText.style.color = '#fdb906'
  221. messageAgainBtnText.style.borderBottom = '1px solid #fdb906'
  222. }
  223. // 鼠标移出
  224. messageAgainBtnText.onmouseout = () => {
  225. messageAgainBtnText.style.color = 'rgb(204, 201, 201)'
  226. messageAgainBtnText.style.borderBottom = '1px solid rgb(204, 201, 201)'
  227. }
  228. messageAgainDiv.appendChild(messageAgainBtnText)
  229. messageContainer.appendChild(messageAgainDiv)
  230. config.elsAgainBtn = messageAgainBtnText
  231. }
  232. mainContainer.appendChild(messageContainer)
  233.  
  234. /**绑定DOM、销毁事件,以便进行控制内容与状态 */
  235. config.els = messageContainer
  236. config.destory = destory.bind(this)
  237. function destory(mainContainer, isClick) {
  238. if (!config.els || !mainContainer || config.isRemove) {
  239. // 不存在,或已经移除,则不再继续
  240. return
  241. }
  242. config.els.style.marginTop = '-20px' // 为了过渡效果
  243. config.els.style.opacity = '0' // 为了过渡效果
  244. config.isRemove = true
  245. if (isClick) {
  246. mainContainer.removeChild(messageContainer)
  247. _resetMianPosition(mainContainer)
  248. free()
  249. } else {
  250. setTimeout(() => {
  251. mainContainer.removeChild(messageContainer)
  252. _resetMianPosition(mainContainer)
  253. free()
  254. }, 400)
  255. }
  256. }
  257.  
  258. // 销毁重置绑定
  259. function free() {
  260. config.els = null
  261. config.elsAgainBtn = null
  262. config.destory = null
  263. }
  264.  
  265. return config
  266. }
  267.  
  268. function _toBindEvents(domConfig, _self) {
  269. if (!domConfig) {
  270. return
  271. }
  272. // 不再提示按钮的事件绑定
  273. if (domConfig.againBtn && domConfig.elsAgainBtn) {
  274. // 鼠标点击:将内容记录下来,下次就不显示同内容的弹框
  275. domConfig.elsAgainBtn.onclick = () => {
  276. clearTimeout(domConfig.timeout)
  277. let sessionJson = sessionStorage.getItem('MESSAGE_DONT_REMIND_AGAIN')
  278. let tempArr = sessionJson ? JSON.parse(sessionJson) : []
  279. let dontRemindAgainList = Array.isArray(tempArr) ? tempArr : []
  280. dontRemindAgainList.push(domConfig.content)
  281. sessionStorage.setItem(_self.sessionStorageName, JSON.stringify(dontRemindAgainList))
  282. domConfig.destory(_self.mainContainer, true)
  283. }
  284. }
  285.  
  286. // 鼠标移入:对销毁计时器进行销毁
  287. domConfig.els.onmouseover = () => {
  288. clearTimeout(domConfig.timeout)
  289. }
  290. // 鼠标移出: 一秒后销毁当前message
  291. domConfig.els.onmouseout = () => {
  292. domConfig.timeout = setTimeout(() => {
  293. domConfig.destory(_self.mainContainer)
  294. clearTimeout(domConfig.timeout)
  295. }, 1000)
  296. }
  297.  
  298. // 延时隐藏
  299. domConfig.timeout = setTimeout(() => {
  300. domConfig.destory(_self.mainContainer)
  301. clearTimeout(domConfig.timeout)
  302. }, domConfig.duration)
  303. }
  304.  
  305. function _resetMianPosition(mainContainer) {
  306. if (!mainContainer) {
  307. return
  308. }
  309. mainContainer.style.left = `calc(50vw - ${mainContainer.scrollWidth / 2}px)`
  310. }
  311.  
  312. class MessageControl {
  313. constructor() {
  314. this.minWidth = 380 // 内容显示宽度:最小值
  315. this.maxWidth = 800 // 内容显示宽度:最大值
  316. this.top = 45 // 整体的最顶部距离
  317. this.zIndex = 999 // 层级
  318. this.mainContainerIdName = 'selfDefine-message-box' // 主体DOM的id名
  319. this.sessionStorageName = 'MESSAGE_DONT_REMIND_AGAIN' // 存储session信息的key
  320. /**生成主体DOM、样式容器 */
  321. let mainDom = document.getElementById(this.mainContainerIdName)
  322. if (mainDom) {
  323. document.body.removeChild(mainDom)
  324. }
  325. this.mainContainer = createDom({ isId: true, name: this.mainContainerIdName, tag: 'div' })
  326. this.mainContainer.style = `
  327. pointer-events:none;
  328. position:fixed;
  329. top:${this.top}px;
  330. left:calc(50vw - ${this.minWidth / 2}px);
  331. z-index:${this.zIndex};
  332. display: flex;
  333. flex-direction: column;
  334. align-items:center;
  335. `
  336. document.body.appendChild(this.mainContainer)
  337. }
  338.  
  339. /**
  340. * 消息提示
  341. * @param {String} type 类型 | 必传 | 可选值:message success error warning
  342. * @param {String} content 内容 | 必传 | ''
  343. * @param {Number} duration 显示时间 | 非必传 | 默认3000毫秒
  344. * @param {Number} delay 出现的延时 | 非必传 | 默认0
  345. * @param {Boolean} againBtn 是否显示 不再提示 按钮 | 非必传 | 默认false
  346. */
  347. message(config = {}) {
  348. // 不再提示(相同文字内容)的存储与判断逻辑待优化
  349. let sessionJson = sessionStorage.getItem(this.sessionStorageName)
  350. let dontRemindAgainList = sessionJson ? JSON.parse(sessionJson) : null
  351. // 需要显示不再提示按钮,且内容有效,且不再提示的记录数组中包含本次内容,则不提示
  352. if (
  353. config.againBtn &&
  354. config.content &&
  355. dontRemindAgainList &&
  356. Array.isArray(dontRemindAgainList) &&
  357. dontRemindAgainList.includes(config.content)
  358. ) {
  359. return
  360. }
  361.  
  362. const domConfig = createMessage(
  363. {
  364. type: config.type,
  365. content: config.content,
  366. duration: config.duration,
  367. delay: config.delay,
  368. againBtn: config.againBtn,
  369. minWidth: this.minWidth,
  370. maxWidth: this.maxWidth
  371. },
  372. this.mainContainer
  373. )
  374. this.mainContainer.appendChild(domConfig.els)
  375. domConfig.els.style.marginTop = '20px' // 为了过渡效果
  376. _resetMianPosition(this.mainContainer)
  377. _toBindEvents(domConfig, this)
  378. }
  379.  
  380. beforeDestory() {
  381. if (this.mainContainer && this.mainContainer.remove) {
  382. this.mainContainer.remove()
  383. } else {
  384. document.body.removeChild(this.mainContainer)
  385. }
  386. this.mainContainer = null
  387. }
  388. }
  389.  
  390. if (!window.MessageType) {
  391. window.MessageType = MessageType
  392. }
  393.  
  394. if (!window.MessageControl) {
  395. window.MessageControl = MessageControl
  396. }
  397. })()