PasteHelper.js 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
  1. import accepts from 'attr-accept'
  2. class PasteHelper {
  3. constructor() {
  4. // https://regex101.com/r/7BN2fR/3
  5. this.indentAndMarkPattern = /^([ \t]*)(?:>|\-|\+|\*|\d+\.) /;
  6. this.pasteText = this.pasteText.bind(this);
  7. this.adjustPastedData = this.adjustPastedData.bind(this);
  8. }
  9. /**
  10. * paste text
  11. * @param {any} editor An editor instance of CodeMirror
  12. * @param {any} event
  13. */
  14. pasteText(editor, event) {
  15. // get data in clipboard
  16. const text = event.clipboardData.getData('text/plain');
  17. if (text.length == 0) { return; }
  18. const curPos = editor.getCursor();
  19. const bol = { line: curPos.line, ch: 0 }; // beginning of line
  20. // get strings from BOL(beginning of line) to current position
  21. const strFromBol = editor.getDoc().getRange(bol, curPos);
  22. const matched = strFromBol.match(this.indentAndMarkPattern);
  23. // when match completely to pattern
  24. // (this means the current position is the beginning of the list item)
  25. if (matched && matched[0] == strFromBol) {
  26. const adjusted = this.adjustPastedData(strFromBol, text);
  27. // replace
  28. if (adjusted != null) {
  29. event.preventDefault();
  30. editor.getDoc().replaceRange(adjusted, bol, curPos);
  31. }
  32. }
  33. }
  34. /**
  35. * return adjusted pasted data by indentAndMark
  36. *
  37. * @param {string} indentAndMark
  38. * @param {string} text
  39. * @returns adjusted pasted data
  40. * returns null when adjustment is not necessary
  41. */
  42. adjustPastedData(indentAndMark, text) {
  43. let adjusted = null;
  44. // list data (starts with indent and mark)
  45. if (text.match(this.indentAndMarkPattern)) {
  46. const indent = indentAndMark.match(this.indentAndMarkPattern)[1];
  47. // splice to an array of line
  48. const lines = text.match(/[^\r\n]+/g);
  49. // indent
  50. const replacedLines = lines.map((line) => {
  51. return indent + line;
  52. })
  53. adjusted = replacedLines.join('\n');
  54. }
  55. // listful data
  56. else if (this.isListfulData(text)) {
  57. // do nothing (return null)
  58. }
  59. // not listful data
  60. else {
  61. // append `indentAndMark` at the beginning of all lines (except the first line)
  62. const replacedText = text.replace(/(\r\n|\r|\n)/g, "$1" + indentAndMark);
  63. // append `indentAndMark` to the first line
  64. adjusted = indentAndMark + replacedText;
  65. }
  66. return adjusted;
  67. }
  68. /**
  69. * evaluate whether `text` is list like data or not
  70. * @param {string} text
  71. */
  72. isListfulData(text) {
  73. // return false if includes at least one blank line
  74. // see https://stackoverflow.com/a/16369725
  75. if (text.match(/^\s*[\r\n]/m) != null) {
  76. return false;
  77. }
  78. const lines = text.match(/[^\r\n]+/g);
  79. // count lines that starts with indent and mark
  80. let isListful = false;
  81. let count = 0;
  82. lines.forEach((line) => {
  83. if (line.match(this.indentAndMarkPattern)) {
  84. count++;
  85. }
  86. // ensure to be true if it is 50% or more
  87. if (count >= lines.length / 2) {
  88. isListful = true;
  89. return;
  90. }
  91. });
  92. return isListful;
  93. }
  94. // Firefox versions prior to 53 return a bogus MIME type for every file drag, so dragovers with
  95. /**
  96. * transplanted from react-dropzone
  97. * @see https://github.com/react-dropzone/react-dropzone/blob/master/src/utils/index.js
  98. *
  99. * @param {*} file
  100. * @param {*} accept
  101. */
  102. fileAccepted(file, accept) {
  103. return file.type === 'application/x-moz-file' || accepts(file, accept)
  104. }
  105. /**
  106. * transplanted from react-dropzone
  107. * @see https://github.com/react-dropzone/react-dropzone/blob/master/src/utils/index.js
  108. *
  109. * @param {*} file
  110. * @param {number} maxSize
  111. * @param {number} minSize
  112. */
  113. fileMatchSize(file, maxSize, minSize) {
  114. return file.size <= maxSize && file.size >= minSize
  115. }
  116. }
  117. // singleton pattern
  118. const instance = new PasteHelper();
  119. Object.freeze(instance);
  120. export default instance;