注释墙

选中内容后添加注释并复制到剪贴板, 同时在本地的服务其中新建一个副本, 参见 https://github.com/ezirmusitua/snippet-board

目前为 2017-04-28 提交的版本。查看 最新版本

// ==UserScript==
// @name                annotation-board
// @name:zh-CN          注释墙
// @description         allow you to add annotation after selected content and copy to clipboard and save to local server
// @description:zh-CN   选中内容后添加注释并复制到剪贴板, 同时在本地的服务其中新建一个副本, 参见 https://github.com/ezirmusitua/snippet-board
// @version             0.1.5
// @author              jferroal
// @license             GPL-3.0
// @include             http://*
// @include             https://*
// @grant               GM_xmlhttpRequest
// @run-at              document-start
// @namespace           https://greasyfork.org/users/34556
// ==/UserScript==

function createSnippet(snippetContent, _host, _port) {
  let host = _host;
  if (!host) {
    host = 'http://127.0.0.1';
  }
  var port = _port;
  if (!port) {
    port = 5000;
  }
  const body = {
    link: window.location.href,
    raw_content: snippetContent,
  }
  GM_xmlhttpRequest({
    method: "POST",
    url: host + ':' + port.toString() + "/snippet/api/v0.1.0",
    data: JSON.stringify(body),
    headers: { "Content-Type": "application/json" },
    onreadystatechange: function (response) {
      console.log('trying to create snippet ... ');
    },
    onload: function (response) {
      console.log('snippet created! ');
    },
    onerror: function (response) {
      console.log('something wrong while creating snippet. ');
    },
    ontimeout: function (response) {
      console.log('request timeout! ');
    },
    onabort: function (response) {
      console.log('request aborted. ');
    }
  });
}

class AnnotationBoardStyle {
  constructor() { }
  static containerStyle(containerStyleObj, position) {
    containerStyleObj.fontFamily = 'Noto';
    containerStyleObj.display = 'flex';
    containerStyleObj.flexDirection = 'column';
    containerStyleObj.border = '4px';
    containerStyleObj.boxShadow = '0px 3px 8px 1px rgba(0, 0, 0, 0.26)';
    containerStyleObj.position = 'absolute';
    containerStyleObj.backgroundColor = 'rgba(0, 0, 0, 0.56)';
    containerStyleObj.padding = '16px 4px 8px 4px';
  }
  static textareaStyle(textareaStyleObj) {
    textareaStyleObj.fontFamily = 'Noto';
    textareaStyleObj.width = '240px';
    textareaStyleObj.height = '128px';
    textareaStyleObj.backgroundColor = 'rgba(255, 255, 255, 0.87)';
    textareaStyleObj.marginBottom = '8px';
    textareaStyleObj.borderRadius = '4px';
    textareaStyleObj.color = 'rgba(0, 0, 0, 0.76)'
    textareaStyleObj.fontSize = '12px';
  }
  static saveButtonStyle(buttonStyleObj) {
    buttonStyleObj.fontFamily = 'Noto';
    buttonStyleObj.border = 'none';
    buttonStyleObj.borderRadius = '4px';
    buttonStyleObj.height = '24px';
    buttonStyleObj.backgroundColor = 'rgba(255, 255, 255, 0.87)'
    buttonStyleObj.color = 'rgba(0, 0, 0, 0.76)'
    buttonStyleObj.fontSize = '14px';
  }
}

const formatDate = (dateObj) => {
  const year = dateObj.getFullYear();
  const month = dateObj.getMonth();
  const date = dateObj.getDate();
  const hour = dateObj.getHours();
  const minute = dateObj.getMinutes();
  return `Date: ${year}-${month + 1}-${date}[${hour}:${minute > 9 ? minute : '0' + minute}]`;
}

const AnnotationBoardId = {
  CONTAINER: 'annotation-container',
  TEXTAREA: 'annotation-textarea',
  BUTTON: 'annotation-button'
};

class Selection {
  constructor() {
    this.createAtStr = formatDate(new Date());
    this.selectionText = document.getSelection().toString();
  }
  concat() {
    return this.createAtStr + '\n' + `Content: \n${this.selectionText}`;
  }
  content(previousContent) {
    if (previousContent) {
      return previousContent + '\n' + '========' + '\n' + this.concat() + '\n';
    } else {
      return this.concat() + '\n';
    }
  }
  isEmpty() {
    return !this.selectionText;
  }
  static copyToClipboard(button, textarea) {
    document.getSelection().removeAllRanges();
    const range = document.createRange();
    range.selectNode(textarea);
    document.getSelection().addRange(range);
    try {
      document.execCommand('copy');
    } catch (err) {
      console.log('Oops, unable to copy');
    }
    document.getSelection().removeAllRanges();
  }
}

class Position {
  constructor(rect, offset) {
    this._left = (rect.left + (offset || 32)) + 'px';
    this._top = (rect.top + (offset || 16)) + 'px';
  }
  top() {
    return this._top;
  }
  left() {
    return this._left;
  }
}

class AnnotationTextArea {
  constructor() {
    this.container = document.createElement('div');
    this.container.id = AnnotationBoardId.CONTAINER;
    AnnotationBoardStyle.containerStyle(this.container.style);
    this.textarea = document.createElement('textarea');
    this.textarea.id = AnnotationBoardId.TEXTAREA;
    AnnotationBoardStyle.textareaStyle(this.textarea.style);
    this.saveBtn = document.createElement('button');
    this.saveBtn.innerHTML = '复制到剪贴板';
    this.saveBtn.id = AnnotationBoardId.BUTTON;
    AnnotationBoardStyle.saveButtonStyle(this.saveBtn.style);
    this.isShowing = false;
  }
  getPosition() {
    const focusNode = document.getSelection().focusNode;
    if (!focusNode) throw new Error('no selection, should not create node');
    const focusParentElement = focusNode.parentElement;
    return new Position(focusParentElement.getBoundingClientRect());
  }
  updateContainer() {
    try {
      const pos = this.getPosition();
      this.container.style.left = pos.left();
      this.container.style.top = pos.top();
    } catch (err) {
      console.error(err);
    }
  }
  updateSaveButton() {
    this.saveBtn.addEventListener('click', (event) => {
      Selection.copyToClipboard(this.saveBtn, this.textarea);
      createSnippet(this.textarea.value);
      this.hide();
    })
  }
  updateTextarea(selection) {
    this.textarea.value = selection.content(this.textarea.value);
  }
  show() {
    this.updateTextarea(new Selection());
    this.updateContainer();
    if (!this.isShowed) {
      this.container.appendChild(this.textarea);
      this.updateSaveButton();
      this.container.appendChild(this.saveBtn);
    }
    document.body.appendChild(this.container);
    this.isShowing = true;
    this.isShowed = true;
  }
  shouldShow() {
    return !this.isShowing && !(new Selection()).isEmpty();
  }
  hide() {
    this.isShowing = false;
    document.body.removeChild(this.container)
  }
  shouldHide(event) {
    const target = event && event.target;
    if (!target && this.iShowing) {
      return true;
    } else {
      const inContainer = event.target.id === AnnotationBoardId.CONTAINER;
      const inTextarea = event.target.id === AnnotationBoardId.TEXTAREA;
      const inButton = event.target.id === AnnotationBoardId.BUTTON;
      return !inContainer && !inTextarea && !inButton && this.isShowing;
    }
  }
  destory() {
    this.container.removeChild(this.saveBtn);
    this.saveBtn = null;
    this.container.removeChild(this.textarea);
    this.textarea = null;
    document.body.removeChild(this.container);
    this.container = null;
  }
}

class AnnotationBoard {
  constructor() {
    this.annotationTextArea = new AnnotationTextArea();
  }
  run() {
    this.eventBinding();
  }
  eventBinding() {
    window.addEventListener('mouseup', (event) => {
      this.handleMouseUp(event);
    }, false);
  }
  handleMouseUp(event) {
    if (this.annotationTextArea.shouldShow()) {
      this.annotationTextArea.show();
    } else if (this.annotationTextArea.shouldHide(event)) {
      this.annotationTextArea.hide();
    }
  }
}
const annotationBoard = new AnnotationBoard();
annotationBoard.run();