selection.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. import { LeafBlot, Scope } from 'parchment';
  2. import cloneDeep from 'lodash.clonedeep';
  3. import isEqual from 'lodash.isequal';
  4. import Emitter from './emitter';
  5. import logger from './logger';
  6. const debug = logger('quill:selection');
  7. class Range {
  8. constructor(index, length = 0) {
  9. this.index = index;
  10. this.length = length;
  11. }
  12. }
  13. class Selection {
  14. constructor(scroll, emitter) {
  15. this.emitter = emitter;
  16. this.scroll = scroll;
  17. this.composing = false;
  18. this.mouseDown = false;
  19. this.root = this.scroll.domNode;
  20. this.cursor = this.scroll.create('cursor', this);
  21. // savedRange is last non-null range
  22. this.savedRange = new Range(0, 0);
  23. this.lastRange = this.savedRange;
  24. this.lastNative = null;
  25. this.handleComposition();
  26. this.handleDragging();
  27. this.emitter.listenDOM('selectionchange', document, () => {
  28. if (!this.mouseDown && !this.composing) {
  29. setTimeout(this.update.bind(this, Emitter.sources.USER), 1);
  30. }
  31. });
  32. this.emitter.on(Emitter.events.SCROLL_BEFORE_UPDATE, () => {
  33. if (!this.hasFocus()) return;
  34. const native = this.getNativeRange();
  35. if (native == null) return;
  36. if (native.start.node === this.cursor.textNode) return; // cursor.restore() will handle
  37. this.emitter.once(Emitter.events.SCROLL_UPDATE, () => {
  38. try {
  39. if (
  40. this.root.contains(native.start.node) &&
  41. this.root.contains(native.end.node)
  42. ) {
  43. this.setNativeRange(
  44. native.start.node,
  45. native.start.offset,
  46. native.end.node,
  47. native.end.offset,
  48. );
  49. }
  50. this.update(Emitter.sources.SILENT);
  51. } catch (ignored) {
  52. // ignore
  53. }
  54. });
  55. });
  56. this.emitter.on(Emitter.events.SCROLL_OPTIMIZE, (mutations, context) => {
  57. if (context.range) {
  58. const { startNode, startOffset, endNode, endOffset } = context.range;
  59. this.setNativeRange(startNode, startOffset, endNode, endOffset);
  60. this.update(Emitter.sources.SILENT);
  61. }
  62. });
  63. this.update(Emitter.sources.SILENT);
  64. }
  65. handleComposition() {
  66. this.root.addEventListener('compositionstart', () => {
  67. this.composing = true;
  68. this.scroll.batchStart();
  69. });
  70. this.root.addEventListener('compositionend', () => {
  71. this.scroll.batchEnd();
  72. this.composing = false;
  73. if (this.cursor.parent) {
  74. const range = this.cursor.restore();
  75. if (!range) return;
  76. setTimeout(() => {
  77. this.setNativeRange(
  78. range.startNode,
  79. range.startOffset,
  80. range.endNode,
  81. range.endOffset,
  82. );
  83. }, 1);
  84. }
  85. });
  86. }
  87. handleDragging() {
  88. this.emitter.listenDOM('mousedown', document.body, () => {
  89. this.mouseDown = true;
  90. });
  91. this.emitter.listenDOM('mouseup', document.body, () => {
  92. this.mouseDown = false;
  93. this.update(Emitter.sources.USER);
  94. });
  95. }
  96. focus() {
  97. if (this.hasFocus()) return;
  98. this.root.focus();
  99. this.setRange(this.savedRange);
  100. }
  101. format(format, value) {
  102. this.scroll.update();
  103. const nativeRange = this.getNativeRange();
  104. if (
  105. nativeRange == null ||
  106. !nativeRange.native.collapsed ||
  107. this.scroll.query(format, Scope.BLOCK)
  108. )
  109. return;
  110. if (nativeRange.start.node !== this.cursor.textNode) {
  111. const blot = this.scroll.find(nativeRange.start.node, false);
  112. if (blot == null) return;
  113. // TODO Give blot ability to not split
  114. if (blot instanceof LeafBlot) {
  115. const after = blot.split(nativeRange.start.offset);
  116. blot.parent.insertBefore(this.cursor, after);
  117. } else {
  118. blot.insertBefore(this.cursor, nativeRange.start.node); // Should never happen
  119. }
  120. this.cursor.attach();
  121. }
  122. this.cursor.format(format, value);
  123. this.scroll.optimize();
  124. this.setNativeRange(this.cursor.textNode, this.cursor.textNode.data.length);
  125. this.update();
  126. }
  127. getBounds(index, length = 0) {
  128. const scrollLength = this.scroll.length();
  129. index = Math.min(index, scrollLength - 1);
  130. length = Math.min(index + length, scrollLength - 1) - index;
  131. let node;
  132. let [leaf, offset] = this.scroll.leaf(index);
  133. if (leaf == null) return null;
  134. [node, offset] = leaf.position(offset, true);
  135. const range = document.createRange();
  136. if (length > 0) {
  137. range.setStart(node, offset);
  138. [leaf, offset] = this.scroll.leaf(index + length);
  139. if (leaf == null) return null;
  140. [node, offset] = leaf.position(offset, true);
  141. range.setEnd(node, offset);
  142. return range.getBoundingClientRect();
  143. }
  144. let side = 'left';
  145. let rect;
  146. if (node instanceof Text) {
  147. if (offset < node.data.length) {
  148. range.setStart(node, offset);
  149. range.setEnd(node, offset + 1);
  150. } else {
  151. range.setStart(node, offset - 1);
  152. range.setEnd(node, offset);
  153. side = 'right';
  154. }
  155. rect = range.getBoundingClientRect();
  156. } else {
  157. rect = leaf.domNode.getBoundingClientRect();
  158. if (offset > 0) side = 'right';
  159. }
  160. return {
  161. bottom: rect.top + rect.height,
  162. height: rect.height,
  163. left: rect[side],
  164. right: rect[side],
  165. top: rect.top,
  166. width: 0,
  167. };
  168. }
  169. getNativeRange() {
  170. const selection = document.getSelection();
  171. if (selection == null || selection.rangeCount <= 0) return null;
  172. const nativeRange = selection.getRangeAt(0);
  173. if (nativeRange == null) return null;
  174. const range = this.normalizeNative(nativeRange);
  175. debug.info('getNativeRange', range);
  176. return range;
  177. }
  178. getRange() {
  179. const normalized = this.getNativeRange();
  180. if (normalized == null) return [null, null];
  181. const range = this.normalizedToRange(normalized);
  182. return [range, normalized];
  183. }
  184. hasFocus() {
  185. return (
  186. document.activeElement === this.root ||
  187. contains(this.root, document.activeElement)
  188. );
  189. }
  190. normalizedToRange(range) {
  191. const positions = [[range.start.node, range.start.offset]];
  192. if (!range.native.collapsed) {
  193. positions.push([range.end.node, range.end.offset]);
  194. }
  195. const indexes = positions.map(position => {
  196. const [node, offset] = position;
  197. const blot = this.scroll.find(node, true);
  198. const index = blot.offset(this.scroll);
  199. if (offset === 0) {
  200. return index;
  201. }
  202. if (blot instanceof LeafBlot) {
  203. return index + blot.index(node, offset);
  204. }
  205. return index + blot.length();
  206. });
  207. const end = Math.min(Math.max(...indexes), this.scroll.length() - 1);
  208. const start = Math.min(end, ...indexes);
  209. return new Range(start, end - start);
  210. }
  211. normalizeNative(nativeRange) {
  212. if (
  213. !contains(this.root, nativeRange.startContainer) ||
  214. (!nativeRange.collapsed && !contains(this.root, nativeRange.endContainer))
  215. ) {
  216. return null;
  217. }
  218. const range = {
  219. start: {
  220. node: nativeRange.startContainer,
  221. offset: nativeRange.startOffset,
  222. },
  223. end: { node: nativeRange.endContainer, offset: nativeRange.endOffset },
  224. native: nativeRange,
  225. };
  226. [range.start, range.end].forEach(position => {
  227. let { node, offset } = position;
  228. while (!(node instanceof Text) && node.childNodes.length > 0) {
  229. if (node.childNodes.length > offset) {
  230. node = node.childNodes[offset];
  231. offset = 0;
  232. } else if (node.childNodes.length === offset) {
  233. node = node.lastChild;
  234. if (node instanceof Text) {
  235. offset = node.data.length;
  236. } else if (node.childNodes.length > 0) {
  237. // Container case
  238. offset = node.childNodes.length;
  239. } else {
  240. // Embed case
  241. offset = node.childNodes.length + 1;
  242. }
  243. } else {
  244. break;
  245. }
  246. }
  247. position.node = node;
  248. position.offset = offset;
  249. });
  250. return range;
  251. }
  252. rangeToNative(range) {
  253. const indexes = range.collapsed
  254. ? [range.index]
  255. : [range.index, range.index + range.length];
  256. const args = [];
  257. const scrollLength = this.scroll.length();
  258. indexes.forEach((index, i) => {
  259. index = Math.min(scrollLength - 1, index);
  260. const [leaf, leafOffset] = this.scroll.leaf(index);
  261. const [node, offset] = leaf.position(leafOffset, i !== 0);
  262. args.push(node, offset);
  263. });
  264. if (args.length < 2) {
  265. return args.concat(args);
  266. }
  267. return args;
  268. }
  269. scrollIntoView(scrollingContainer) {
  270. const range = this.lastRange;
  271. if (range == null) return;
  272. const bounds = this.getBounds(range.index, range.length);
  273. if (bounds == null) return;
  274. const limit = this.scroll.length() - 1;
  275. const [first] = this.scroll.line(Math.min(range.index, limit));
  276. let last = first;
  277. if (range.length > 0) {
  278. [last] = this.scroll.line(Math.min(range.index + range.length, limit));
  279. }
  280. if (first == null || last == null) return;
  281. const scrollBounds = scrollingContainer.getBoundingClientRect();
  282. if (bounds.top < scrollBounds.top) {
  283. scrollingContainer.scrollTop -= scrollBounds.top - bounds.top;
  284. } else if (bounds.bottom > scrollBounds.bottom) {
  285. scrollingContainer.scrollTop += bounds.bottom - scrollBounds.bottom;
  286. }
  287. }
  288. setNativeRange(
  289. startNode,
  290. startOffset,
  291. endNode = startNode,
  292. endOffset = startOffset,
  293. force = false,
  294. ) {
  295. debug.info('setNativeRange', startNode, startOffset, endNode, endOffset);
  296. if (
  297. startNode != null &&
  298. (this.root.parentNode == null ||
  299. startNode.parentNode == null ||
  300. endNode.parentNode == null)
  301. ) {
  302. return;
  303. }
  304. const selection = document.getSelection();
  305. if (selection == null) return;
  306. if (startNode != null) {
  307. if (!this.hasFocus()) this.root.focus();
  308. const { native } = this.getNativeRange() || {};
  309. if (
  310. native == null ||
  311. force ||
  312. startNode !== native.startContainer ||
  313. startOffset !== native.startOffset ||
  314. endNode !== native.endContainer ||
  315. endOffset !== native.endOffset
  316. ) {
  317. if (startNode.tagName === 'BR') {
  318. startOffset = Array.from(startNode.parentNode.childNodes).indexOf(
  319. startNode,
  320. );
  321. startNode = startNode.parentNode;
  322. }
  323. if (endNode.tagName === 'BR') {
  324. endOffset = Array.from(endNode.parentNode.childNodes).indexOf(
  325. endNode,
  326. );
  327. endNode = endNode.parentNode;
  328. }
  329. const range = document.createRange();
  330. range.setStart(startNode, startOffset);
  331. range.setEnd(endNode, endOffset);
  332. selection.removeAllRanges();
  333. selection.addRange(range);
  334. }
  335. } else {
  336. selection.removeAllRanges();
  337. this.root.blur();
  338. }
  339. }
  340. setRange(range, force = false, source = Emitter.sources.API) {
  341. if (typeof force === 'string') {
  342. source = force;
  343. force = false;
  344. }
  345. debug.info('setRange', range);
  346. if (range != null) {
  347. const args = this.rangeToNative(range);
  348. this.setNativeRange(...args, force);
  349. } else {
  350. this.setNativeRange(null);
  351. }
  352. this.update(source);
  353. }
  354. update(source = Emitter.sources.USER) {
  355. const oldRange = this.lastRange;
  356. const [lastRange, nativeRange] = this.getRange();
  357. this.lastRange = lastRange;
  358. this.lastNative = nativeRange;
  359. if (this.lastRange != null) {
  360. this.savedRange = this.lastRange;
  361. }
  362. if (!isEqual(oldRange, this.lastRange)) {
  363. if (
  364. !this.composing &&
  365. nativeRange != null &&
  366. nativeRange.native.collapsed &&
  367. nativeRange.start.node !== this.cursor.textNode
  368. ) {
  369. const range = this.cursor.restore();
  370. if (range) {
  371. this.setNativeRange(
  372. range.startNode,
  373. range.startOffset,
  374. range.endNode,
  375. range.endOffset,
  376. );
  377. }
  378. }
  379. const args = [
  380. Emitter.events.SELECTION_CHANGE,
  381. cloneDeep(this.lastRange),
  382. cloneDeep(oldRange),
  383. source,
  384. ];
  385. this.emitter.emit(Emitter.events.EDITOR_CHANGE, ...args);
  386. if (source !== Emitter.sources.SILENT) {
  387. this.emitter.emit(...args);
  388. }
  389. }
  390. }
  391. }
  392. function contains(parent, descendant) {
  393. try {
  394. // Firefox inserts inaccessible nodes around video elements
  395. descendant.parentNode; // eslint-disable-line no-unused-expressions
  396. } catch (e) {
  397. return false;
  398. }
  399. return parent.contains(descendant);
  400. }
  401. export { Range, Selection as default };