|
60 | 60 | import * as Showdown from 'showdown';
|
61 | 61 |
|
62 | 62 | import Editor from '@toast-ui/editor';
|
63 |
| - import debounce from 'lodash/debounce'; |
64 | 63 | import { stripHtml } from 'string-strip-html';
|
65 | 64 |
|
66 | 65 | import imageUpload, { paramsToImageFieldHTML } from '../plugins/image-upload';
|
|
75 | 74 | import { registerMarkdownFormulaField } from '../plugins/formulas/MarkdownFormulaField';
|
76 | 75 | import { registerMarkdownImageField } from '../plugins/image-upload/MarkdownImageField';
|
77 | 76 | import { clearNodeFormat, getExtensionMenuPosition } from './utils';
|
78 |
| - import keyHandlers from './keyHandlers'; |
79 | 77 | import FormulasMenu from './FormulasMenu/FormulasMenu';
|
80 | 78 | import ImagesMenu from './ImagesMenu/ImagesMenu';
|
81 | 79 | import ClickOutside from 'shared/directives/click-outside';
|
82 | 80 |
|
83 | 81 | registerMarkdownFormulaField();
|
84 | 82 | registerMarkdownImageField();
|
85 | 83 |
|
86 |
| - const wrapWithSpaces = html => ` ${html} `; |
87 |
| -
|
88 | 84 | const AnalyticsActionMap = {
|
89 | 85 | Bold: 'Bold',
|
90 | 86 | Italic: 'Italicize',
|
|
164 | 160 | markdown(newMd, previousMd) {
|
165 | 161 | if (newMd !== previousMd && newMd !== this.editor.getMarkdown()) {
|
166 | 162 | this.editor.setMarkdown(newMd);
|
167 |
| - this.updateCustomNodeSpacers(); |
168 | 163 | this.initImageFields();
|
169 | 164 | }
|
170 | 165 | },
|
|
182 | 177 | const Convertor = tmpEditor.convertor.constructor;
|
183 | 178 | class CustomConvertor extends Convertor {
|
184 | 179 | toMarkdown(content) {
|
185 |
| - content = showdown.makeMarkdown(content); |
186 | 180 | content = imagesHtmlToMd(content);
|
187 | 181 | content = formulaHtmlToMd(content);
|
188 |
| - content = content.replaceAll(' ', ' '); |
189 |
| -
|
| 182 | + content = showdown.makeMarkdown(content); |
190 | 183 | // TUI.editor sprinkles in extra `<br>` tags that Kolibri renders literally
|
| 184 | + // When showdown has already added linebreaks to render these in markdown |
| 185 | + // so we just remove these here. |
| 186 | + content = content.replaceAll('<br>', ''); |
| 187 | +
|
191 | 188 | // any copy pasted rich text that renders as HTML but does not get converted
|
192 | 189 | // will linger here, so remove it as Kolibri will render it literally also.
|
193 | 190 | content = stripHtml(content).result;
|
|
306 | 303 | this.keyDownEventListener = this.$el.addEventListener('keydown', this.onKeyDown, true);
|
307 | 304 | this.clickEventListener = this.$el.addEventListener('click', this.onClick);
|
308 | 305 | this.editImageEventListener = this.$el.addEventListener('editImage', this.handleEditImage);
|
309 |
| -
|
310 |
| - // Make sure all custom nodes have spacers around them. |
311 |
| - // Note: this is debounced because it's called every keystroke |
312 |
| - const editorEl = this.$refs.editor; |
313 |
| - this.updateCustomNodeSpacers = debounce(() => { |
314 |
| - editorEl.querySelectorAll('span[is]').forEach(el => { |
315 |
| - el.editing = true; |
316 |
| - const hasLeftwardSpace = el => { |
317 |
| - return ( |
318 |
| - el.previousSibling && |
319 |
| - el.previousSibling.textContent && |
320 |
| - /\s$/.test(el.previousSibling.textContent) |
321 |
| - ); |
322 |
| - }; |
323 |
| - const hasRightwardSpace = el => { |
324 |
| - return ( |
325 |
| - el.nextSibling && el.nextSibling.textContent && /^\s/.test(el.nextSibling.textContent) |
326 |
| - ); |
327 |
| - }; |
328 |
| - if (!hasLeftwardSpace(el)) { |
329 |
| - el.insertAdjacentText('beforebegin', '\xa0'); |
330 |
| - } |
331 |
| - if (!hasRightwardSpace(el)) { |
332 |
| - el.insertAdjacentText('afterend', '\xa0'); |
333 |
| - } |
334 |
| - }); |
335 |
| - }, 150); |
336 |
| -
|
337 |
| - this.updateCustomNodeSpacers(); |
338 | 306 | },
|
339 | 307 | activated() {
|
340 | 308 | this.editor.focus();
|
|
358 | 326 | * a recommended solution here https://github.com/neilj/Squire/issues/107
|
359 | 327 | */
|
360 | 328 | onKeyDown(event) {
|
361 |
| - const squire = this.editor.getSquire(); |
362 |
| -
|
363 | 329 | // Apply squire selection workarounds
|
364 | 330 | this.fixSquireSelectionOnKeyDown(event);
|
365 | 331 |
|
366 |
| - if (event.key in keyHandlers) { |
367 |
| - keyHandlers[event.key](squire); |
368 |
| - } |
369 |
| -
|
370 | 332 | // ESC should close menus if any are open
|
371 | 333 | // or close the editor if none are open
|
372 | 334 | if (event.key === 'Escape') {
|
|
413 | 375 | event.preventDefault();
|
414 | 376 | event.stopPropagation();
|
415 | 377 | }
|
416 |
| -
|
417 |
| - this.updateCustomNodeSpacers(); |
418 | 378 | },
|
419 | 379 | onPaste(event) {
|
420 | 380 | const fragment = clearNodeFormat({
|
|
507 | 467 | const getRightwardElement = selection => getElementAtRelativeOffset(selection, 1);
|
508 | 468 |
|
509 | 469 | const getCharacterAtRelativeOffset = (selection, relativeOffset) => {
|
510 |
| - let { element, offset } = squire.getSelectionInfoByOffset( |
| 470 | + const { element, offset } = squire.getSelectionInfoByOffset( |
511 | 471 | selection.startContainer,
|
512 | 472 | selection.startOffset + relativeOffset
|
513 | 473 | );
|
|
529 | 489 | /\s$/.test(getCharacterAtRelativeOffset(selection, 0));
|
530 | 490 |
|
531 | 491 | const moveCursor = (selection, amount) => {
|
532 |
| - let { element, offset } = squire.getSelectionInfoByOffset( |
533 |
| - selection.startContainer, |
534 |
| - selection.startOffset + amount |
535 |
| - ); |
536 |
| - if (amount > 0) { |
537 |
| - selection.setStart(element, offset); |
538 |
| - } else { |
539 |
| - selection.setEnd(element, offset); |
540 |
| - } |
| 492 | + const element = getElementAtRelativeOffset(selection, amount); |
| 493 | + selection.setStart(element, 0); |
| 494 | + selection.setEnd(element, 0); |
541 | 495 | return selection;
|
542 | 496 | };
|
543 | 497 |
|
544 |
| - // make sure Squire doesn't delete rightward custom nodes when 'backspace' is pressed |
545 |
| - if (event.key !== 'ArrowRight' && event.key !== 'Delete') { |
546 |
| - if (isCustomNode(getRightwardElement(selection))) { |
| 498 | + const rightwardElement = getRightwardElement(selection); |
| 499 | + const leftwardElement = getLeftwardElement(selection); |
| 500 | +
|
| 501 | + if (event.key === 'ArrowRight') { |
| 502 | + if (isCustomNode(rightwardElement)) { |
| 503 | + squire.setSelection(moveCursor(selection, 1)); |
| 504 | + } else if (spacerAndCustomElementAreRightward(selection)) { |
| 505 | + squire.setSelection(moveCursor(selection, 2)); |
| 506 | + } |
| 507 | + } |
| 508 | + if (event.key === 'ArrowLeft') { |
| 509 | + if (isCustomNode(leftwardElement)) { |
547 | 510 | squire.setSelection(moveCursor(selection, -1));
|
| 511 | + } else if (spacerAndCustomElementAreLeftward(selection)) { |
| 512 | + squire.setSelection(moveCursor(selection, -2)); |
548 | 513 | }
|
549 | 514 | }
|
550 | 515 | // make sure Squire doesn't get stuck with a broken cursor position when deleting
|
551 | 516 | // elements with `contenteditable="false"` in FireFox
|
552 |
| - let leftwardElement = getLeftwardElement(selection); |
553 | 517 | if (event.key === 'Backspace') {
|
554 | 518 | if (selection.startContainer.tagName === 'DIV') {
|
555 | 519 | // This happens normally when deleting from the beginning of an empty line...
|
|
791 | 755 | } else {
|
792 | 756 | let squire = this.editor.getSquire();
|
793 | 757 | squire.insertHTML(formulaHTML);
|
794 |
| - this.updateCustomNodeSpacers(); |
795 | 758 | }
|
796 | 759 | },
|
797 | 760 | resetFormulasMenu() {
|
|
876 | 839 | const mdImageEl = template.content.firstElementChild;
|
877 | 840 | mdImageEl.setAttribute('editing', true);
|
878 | 841 |
|
879 |
| - // insert non-breaking spaces to allow users to write text before and after |
880 |
| - this.editor.getSquire().insertHTML(wrapWithSpaces(mdImageEl.outerHTML)); |
| 842 | + this.editor.getSquire().insertHTML(mdImageEl.outerHTML); |
881 | 843 |
|
882 | 844 | this.initImageFields();
|
883 | 845 | }
|
|
0 commit comments