Greasy Fork is available in English.

Copy title

use Ctrl + q copy title; Alt + q copy title and url, create markdown

// ==UserScript==
// @name         Copy title
// @name:zh-CN   便捷复制页签 title
// @namespace    http://tampermonkey.net/Henry
// @version      1.0.3
// @description  use Ctrl + q copy title; Alt + q copy title and url, create markdown
// @description:zh-CN   Ctrl + q 复制页签标题;Alt + q 复制页签标题及链接,生成 markdown 格式
// @author       Henry
// @icon         https://tsz.netlify.app/img/favicon.png
// @match        http*://*/*
// @license      MIT
// ==/UserScript==

;(function () {
  'use strict'

  let messageComponent = null
  document.addEventListener('keydown', listener, false)

  function listener(event) {
    const { keyCode, ctrlKey, altKey } = event
    if (keyCode === 81 && ctrlKey) {
      event.preventDefault()
      event.stopPropagation()
      copyTextToClipboard(document.title)
      return false
    }
    if (keyCode === 81 && altKey) {
      event.preventDefault()
      event.stopPropagation()
      copyTextToClipboard(`[${document.title}](${location.href})`)
      return false
    }
  }

  function copyTextToClipboard(text) {
    if (!navigator.clipboard) {
      fallbackCopyTextToClipboard(text)
      return
    }
    navigator.clipboard.writeText(text).then(
      function () {
        wrapperMsg(`Copying: ${text}`, window.MessageType.SUCCESS)
      },
      function (err) {
        console.log('copyTextToClipboard ~ err:', err)
        wrapperMsg('Oops, unable to copy', window.MessageType.ERROR)
      }
    )
  }

  function wrapperMsg(content, type) {
    // 初始化绑定
    if (!messageComponent) {
      messageComponent = new window.MessageControl()
    }
    // 调用
    messageComponent.message({ content, type })
  }

  function fallbackCopyTextToClipboard(text) {
    let textArea = document.createElement('textarea')
    textArea.value = text

    // Avoid scrolling to bottom
    textArea.style.top = '0'
    textArea.style.left = '0'
    textArea.style.position = 'fixed'

    document.body.appendChild(textArea)
    textArea.focus()
    textArea.select()

    try {
      var successful = document.execCommand('copy')
      var msg = successful ? text : ''
      wrapperMsg(`Copying: ${msg}`, window.MessageType.SUCCESS)
    } catch (err) {
      console.log('fallbackCopyTextToClipboard ~ err:', err)
      wrapperMsg('Oops, unable to copy', window.MessageType.ERROR)
    }

    document.body.removeChild(textArea)
  }
})()
;(function () {
  // 消息类型
  const MessageType = {
    MESSAGE: 'message', // 普通
    SUCCESS: 'success', // 成功
    ERROR: 'error', // 错误
    WARNING: 'warning' // 警告
  }

  // 状态对应的主色
  const MessageTypeColor = {
    MESSAGE: '#909399',
    SUCCESS: '#67c23a',
    ERROR: '#f56c6c',
    WARNING: '#e6a23c'
  }

  // 创建DOM
  const createDom = ({ isId = false, name = '', tag = 'div' }) => {
    if (!tag) {
      return null
    }
    const ele = document.createElement(tag)
    if (name) {
      if (isId) {
        ele.id = name
      } else {
        ele.className = name
      }
    }
    return ele
  }

  // 获取类型对应的背景色
  const getTypeBGColor = type => {
    let bgColor = ''
    switch (type) {
      case MessageType.SUCCESS:
        bgColor = 'background-color: #f0f9eb'
        break
      case MessageType.ERROR:
        bgColor = 'background-color: #f0f9eb'
        break
      case MessageType.WARNING:
        bgColor = 'background-color: #f0f9eb'
        break
      default:
        bgColor = 'background-color: #edf2fc'
        break
    }
    return bgColor
  }

  // 获取类型对应的背景色、文字颜色
  const getTypeDomCss = type => {
    let cssStr = ''
    let commonCss = ''
    switch (type) {
      case MessageType.SUCCESS:
        cssStr = commonCss + `${getTypeBGColor(type)};color: ${MessageTypeColor.SUCCESS};`
        break
      case MessageType.ERROR:
        cssStr = commonCss + `${getTypeBGColor(type)};color: ${MessageTypeColor.ERROR};`
        break
      case MessageType.WARNING:
        cssStr = commonCss + `${getTypeBGColor(type)};color: ${MessageTypeColor.WARNING};`
        break
      default:
        cssStr = commonCss + `${getTypeBGColor(type)};color: ${MessageTypeColor.MESSAGE};`
        break
    }
    return cssStr
  }

  const createMessage = (
    { type, content, duration, delay, againBtn, minWidth, maxWidth },
    mainContainer
  ) => {
    if (!mainContainer) {
      console.error('主容器不存在,查看调用流程,确保doucument.body已生成!')
      return
    }
    /**随机的key */
    const randomKey = Math.floor(Math.random() * (99999 - 10002)) + 10002

    /**属性配置 */
    const config = {
      isRemove: false, // 是否被移除了
      type: type || MessageType.MESSAGE, // 类型 message success error warning
      content: content || '', // 提示内容
      duration: duration || 3000, // 显示时间
      delay: delay || 0, // 弹出延迟
      timeout: null, // 计时器事件
      againBtn: againBtn || false // 是否需要显示 不再提示 按钮
    }
    // #region 生成DOM、样式、关系
    const messageContainer = createDom({ name: `message-${randomKey}`, tag: 'div' })
    messageContainer.style = `
  min-width: ${minWidth}px;
  max-width:${maxWidth}px;
  padding: 12px 12px;
  margin-top: -20px;
  border-radius: 4px;
  box-shadow: -5px 5px 12px 0 rgba(204, 204, 204, 0.8);
  ${getTypeBGColor(config.type)};
  animation: all cubic-bezier(0.18, 0.89, 0.32, 1.28) 0.4s;
  transition: all .4s;
  pointer-events: auto;
  overflow:hidden;
  `
    /**内容区域 */
    const messageTypeDom = createDom({ tag: 'div' })
    messageTypeDom.style = getTypeDomCss(config.type)
    /**文本内容 */
    const messageTypeText = createDom({ tag: 'span' })
    messageTypeText.style = 'font-size: 14px;line-height: 20px;'
    messageTypeText.innerHTML = config.content
    /**建立html树关系 */
    messageTypeDom.appendChild(messageTypeText)
    messageContainer.appendChild(messageTypeDom)
    /**不再提示的按钮 */
    if (config.againBtn) {
      const messageAgainDiv = createDom({ name: 'message-again-btn', tag: 'div' })
      messageAgainDiv.style = `margin-top: 5px;text-align: right;`
      const messageAgainBtnText = createDom({ name: 'message-again-text', tag: 'span' })
      messageAgainBtnText.innerHTML = '不再提示'
      messageAgainBtnText.style = `
      font-size: 12px;
      color: rgb(204, 201, 201);
      border-bottom: 1px solid rgb(204, 201, 201);
      cursor: pointer;
      `
      // 鼠标移入
      messageAgainBtnText.onmouseover = () => {
        messageAgainBtnText.style.color = '#fdb906'
        messageAgainBtnText.style.borderBottom = '1px solid #fdb906'
      }
      // 鼠标移出
      messageAgainBtnText.onmouseout = () => {
        messageAgainBtnText.style.color = 'rgb(204, 201, 201)'
        messageAgainBtnText.style.borderBottom = '1px solid rgb(204, 201, 201)'
      }
      messageAgainDiv.appendChild(messageAgainBtnText)
      messageContainer.appendChild(messageAgainDiv)
      config.elsAgainBtn = messageAgainBtnText
    }
    mainContainer.appendChild(messageContainer)

    /**绑定DOM、销毁事件,以便进行控制内容与状态 */
    config.els = messageContainer
    config.destory = destory.bind(this)
    function destory(mainContainer, isClick) {
      if (!config.els || !mainContainer || config.isRemove) {
        // 不存在,或已经移除,则不再继续
        return
      }
      config.els.style.marginTop = '-20px' // 为了过渡效果
      config.els.style.opacity = '0' // 为了过渡效果
      config.isRemove = true
      if (isClick) {
        mainContainer.removeChild(messageContainer)
        _resetMianPosition(mainContainer)
        free()
      } else {
        setTimeout(() => {
          mainContainer.removeChild(messageContainer)
          _resetMianPosition(mainContainer)
          free()
        }, 400)
      }
    }

    // 销毁重置绑定
    function free() {
      config.els = null
      config.elsAgainBtn = null
      config.destory = null
    }

    return config
  }

  function _toBindEvents(domConfig, _self) {
    if (!domConfig) {
      return
    }
    // 不再提示按钮的事件绑定
    if (domConfig.againBtn && domConfig.elsAgainBtn) {
      // 鼠标点击:将内容记录下来,下次就不显示同内容的弹框
      domConfig.elsAgainBtn.onclick = () => {
        clearTimeout(domConfig.timeout)
        let sessionJson = sessionStorage.getItem('MESSAGE_DONT_REMIND_AGAIN')
        let tempArr = sessionJson ? JSON.parse(sessionJson) : []
        let dontRemindAgainList = Array.isArray(tempArr) ? tempArr : []
        dontRemindAgainList.push(domConfig.content)
        sessionStorage.setItem(_self.sessionStorageName, JSON.stringify(dontRemindAgainList))
        domConfig.destory(_self.mainContainer, true)
      }
    }

    // 鼠标移入:对销毁计时器进行销毁
    domConfig.els.onmouseover = () => {
      clearTimeout(domConfig.timeout)
    }
    // 鼠标移出: 一秒后销毁当前message
    domConfig.els.onmouseout = () => {
      domConfig.timeout = setTimeout(() => {
        domConfig.destory(_self.mainContainer)
        clearTimeout(domConfig.timeout)
      }, 1000)
    }

    // 延时隐藏
    domConfig.timeout = setTimeout(() => {
      domConfig.destory(_self.mainContainer)
      clearTimeout(domConfig.timeout)
    }, domConfig.duration)
  }

  function _resetMianPosition(mainContainer) {
    if (!mainContainer) {
      return
    }
    mainContainer.style.left = `calc(50vw - ${mainContainer.scrollWidth / 2}px)`
  }

  class MessageControl {
    constructor() {
      this.minWidth = 380 // 内容显示宽度:最小值
      this.maxWidth = 800 // 内容显示宽度:最大值
      this.top = 45 // 整体的最顶部距离
      this.zIndex = 999 // 层级
      this.mainContainerIdName = 'selfDefine-message-box' // 主体DOM的id名
      this.sessionStorageName = 'MESSAGE_DONT_REMIND_AGAIN' // 存储session信息的key
      /**生成主体DOM、样式容器 */
      let mainDom = document.getElementById(this.mainContainerIdName)
      if (mainDom) {
        document.body.removeChild(mainDom)
      }
      this.mainContainer = createDom({ isId: true, name: this.mainContainerIdName, tag: 'div' })
      this.mainContainer.style = `
    pointer-events:none;
    position:fixed;
    top:${this.top}px;
    left:calc(50vw - ${this.minWidth / 2}px);
    z-index:${this.zIndex};
    display: flex;
    flex-direction: column;
    align-items:center;
    `
      document.body.appendChild(this.mainContainer)
    }

    /**
     * 消息提示
     * @param {String} type 类型 | 必传 | 可选值:message success error warning
     * @param {String} content 内容 | 必传 | ''
     * @param {Number} duration 显示时间 | 非必传 | 默认3000毫秒
     * @param {Number} delay 出现的延时 | 非必传 | 默认0
     * @param {Boolean} againBtn 是否显示 不再提示 按钮 | 非必传 | 默认false
     */
    message(config = {}) {
      // 不再提示(相同文字内容)的存储与判断逻辑待优化
      let sessionJson = sessionStorage.getItem(this.sessionStorageName)
      let dontRemindAgainList = sessionJson ? JSON.parse(sessionJson) : null
      // 需要显示不再提示按钮,且内容有效,且不再提示的记录数组中包含本次内容,则不提示
      if (
        config.againBtn &&
        config.content &&
        dontRemindAgainList &&
        Array.isArray(dontRemindAgainList) &&
        dontRemindAgainList.includes(config.content)
      ) {
        return
      }

      const domConfig = createMessage(
        {
          type: config.type,
          content: config.content,
          duration: config.duration,
          delay: config.delay,
          againBtn: config.againBtn,
          minWidth: this.minWidth,
          maxWidth: this.maxWidth
        },
        this.mainContainer
      )
      this.mainContainer.appendChild(domConfig.els)
      domConfig.els.style.marginTop = '20px' // 为了过渡效果
      _resetMianPosition(this.mainContainer)
      _toBindEvents(domConfig, this)
    }

    beforeDestory() {
      if (this.mainContainer && this.mainContainer.remove) {
        this.mainContainer.remove()
      } else {
        document.body.removeChild(this.mainContainer)
      }
      this.mainContainer = null
    }
  }

  if (!window.MessageType) {
    window.MessageType = MessageType
  }

  if (!window.MessageControl) {
    window.MessageControl = MessageControl
  }
})()