Skip to content

Commit 7f7e675

Browse files
authored
Merge pull request #3837 from rtibbles/assessment_images
Make the image remove option work
2 parents 7ceff62 + 48a97be commit 7f7e675

File tree

4 files changed

+106
-142
lines changed

4 files changed

+106
-142
lines changed

contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/MarkdownEditor/MarkdownEditor.vue

+24-62
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@
6060
import * as Showdown from 'showdown';
6161
6262
import Editor from '@toast-ui/editor';
63-
import debounce from 'lodash/debounce';
6463
import { stripHtml } from 'string-strip-html';
6564
6665
import imageUpload, { paramsToImageFieldHTML } from '../plugins/image-upload';
@@ -75,16 +74,13 @@
7574
import { registerMarkdownFormulaField } from '../plugins/formulas/MarkdownFormulaField';
7675
import { registerMarkdownImageField } from '../plugins/image-upload/MarkdownImageField';
7776
import { clearNodeFormat, getExtensionMenuPosition } from './utils';
78-
import keyHandlers from './keyHandlers';
7977
import FormulasMenu from './FormulasMenu/FormulasMenu';
8078
import ImagesMenu from './ImagesMenu/ImagesMenu';
8179
import ClickOutside from 'shared/directives/click-outside';
8280
8381
registerMarkdownFormulaField();
8482
registerMarkdownImageField();
8583
86-
const wrapWithSpaces = html => ` ${html} `;
87-
8884
const AnalyticsActionMap = {
8985
Bold: 'Bold',
9086
Italic: 'Italicize',
@@ -164,7 +160,6 @@
164160
markdown(newMd, previousMd) {
165161
if (newMd !== previousMd && newMd !== this.editor.getMarkdown()) {
166162
this.editor.setMarkdown(newMd);
167-
this.updateCustomNodeSpacers();
168163
this.initImageFields();
169164
}
170165
},
@@ -182,12 +177,14 @@
182177
const Convertor = tmpEditor.convertor.constructor;
183178
class CustomConvertor extends Convertor {
184179
toMarkdown(content) {
185-
content = showdown.makeMarkdown(content);
186180
content = imagesHtmlToMd(content);
187181
content = formulaHtmlToMd(content);
188-
content = content.replaceAll(' ', ' ');
189-
182+
content = showdown.makeMarkdown(content);
190183
// 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+
191188
// any copy pasted rich text that renders as HTML but does not get converted
192189
// will linger here, so remove it as Kolibri will render it literally also.
193190
content = stripHtml(content).result;
@@ -306,35 +303,6 @@
306303
this.keyDownEventListener = this.$el.addEventListener('keydown', this.onKeyDown, true);
307304
this.clickEventListener = this.$el.addEventListener('click', this.onClick);
308305
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();
338306
},
339307
activated() {
340308
this.editor.focus();
@@ -358,15 +326,9 @@
358326
* a recommended solution here https://github.com/neilj/Squire/issues/107
359327
*/
360328
onKeyDown(event) {
361-
const squire = this.editor.getSquire();
362-
363329
// Apply squire selection workarounds
364330
this.fixSquireSelectionOnKeyDown(event);
365331
366-
if (event.key in keyHandlers) {
367-
keyHandlers[event.key](squire);
368-
}
369-
370332
// ESC should close menus if any are open
371333
// or close the editor if none are open
372334
if (event.key === 'Escape') {
@@ -413,8 +375,6 @@
413375
event.preventDefault();
414376
event.stopPropagation();
415377
}
416-
417-
this.updateCustomNodeSpacers();
418378
},
419379
onPaste(event) {
420380
const fragment = clearNodeFormat({
@@ -507,7 +467,7 @@
507467
const getRightwardElement = selection => getElementAtRelativeOffset(selection, 1);
508468
509469
const getCharacterAtRelativeOffset = (selection, relativeOffset) => {
510-
let { element, offset } = squire.getSelectionInfoByOffset(
470+
const { element, offset } = squire.getSelectionInfoByOffset(
511471
selection.startContainer,
512472
selection.startOffset + relativeOffset
513473
);
@@ -529,27 +489,31 @@
529489
/\s$/.test(getCharacterAtRelativeOffset(selection, 0));
530490
531491
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);
541495
return selection;
542496
};
543497
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)) {
547510
squire.setSelection(moveCursor(selection, -1));
511+
} else if (spacerAndCustomElementAreLeftward(selection)) {
512+
squire.setSelection(moveCursor(selection, -2));
548513
}
549514
}
550515
// make sure Squire doesn't get stuck with a broken cursor position when deleting
551516
// elements with `contenteditable="false"` in FireFox
552-
let leftwardElement = getLeftwardElement(selection);
553517
if (event.key === 'Backspace') {
554518
if (selection.startContainer.tagName === 'DIV') {
555519
// This happens normally when deleting from the beginning of an empty line...
@@ -791,7 +755,6 @@
791755
} else {
792756
let squire = this.editor.getSquire();
793757
squire.insertHTML(formulaHTML);
794-
this.updateCustomNodeSpacers();
795758
}
796759
},
797760
resetFormulasMenu() {
@@ -876,8 +839,7 @@
876839
const mdImageEl = template.content.firstElementChild;
877840
mdImageEl.setAttribute('editing', true);
878841
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);
881843
882844
this.initImageFields();
883845
}

contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/MarkdownEditor/keyHandlers.js

-74
This file was deleted.

contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/image-upload/MarkdownImageField.vue

+6-2
Original file line numberDiff line numberDiff line change
@@ -136,8 +136,12 @@
136136
);
137137
},
138138
handleRemove() {
139-
this.$destroy();
140-
this.$el.parentNode.removeChild(this.$el);
139+
this.editorField.dispatchEvent(
140+
new CustomEvent('remove', {
141+
bubbles: true,
142+
cancelable: true,
143+
})
144+
);
141145
},
142146
handleResize() {
143147
this.resizing = true;

contentcuration/contentcuration/frontend/shared/views/MarkdownEditor/plugins/registerCustomMarkdownField.js

+76-4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,54 @@ import Vue from 'vue';
22
import vueCustomElement from 'vue-custom-element';
33
import { v4 as uuidv4 } from 'uuid';
44

5+
const leftwardSpaceRegex = /\s$/;
6+
7+
const leftwardDoubleSpaceRegex = /\s\s$/;
8+
9+
const hasLeftwardSpace = el => {
10+
return (
11+
// Has a previous sibling
12+
el.previousSibling &&
13+
// Which has text content
14+
el.previousSibling.textContent &&
15+
// The text content has white space right before this element
16+
leftwardSpaceRegex.test(el.previousSibling.textContent) &&
17+
// And either this sibling doesn't have a previous sibling
18+
(!el.previousSibling.previousSibling ||
19+
// Or it doesn't have a hasAttribute function
20+
typeof el.previousSibling.previousSibling.hasAttribute !== 'function' ||
21+
// Or the previous sibling is not another custom field
22+
!el.previousSibling.previousSibling.hasAttribute('is') ||
23+
// Or the previous sibling has two white spaces, one for each
24+
// of the custom fields on either side.
25+
leftwardDoubleSpaceRegex.test(el.previousSibling.textContent))
26+
);
27+
};
28+
29+
const rightwardSpaceRegex = /^\s/;
30+
31+
const rightwardDoubleSpaceRegex = /^\s\s/;
32+
33+
const hasRightwardSpace = el => {
34+
return (
35+
// Has a next sibling
36+
el.nextSibling &&
37+
// Which has text content
38+
el.nextSibling.textContent &&
39+
// The text content has white space right after this element
40+
rightwardSpaceRegex.test(el.nextSibling.textContent) &&
41+
// And either this sibling doesn't have a next sibling
42+
(!el.nextSibling.nextSibling ||
43+
// Or it doesn't have a hasAttribute function
44+
typeof el.nextSibling.nextSibling.hasAttribute !== 'function' ||
45+
// Or the next sibling is not another custom field
46+
!el.nextSibling.nextSibling.hasAttribute('is') ||
47+
// Or the next sibling has two white spaces, one for each
48+
// of the custom fields on either side.
49+
rightwardDoubleSpaceRegex.test(el.nextSibling.textContent))
50+
);
51+
};
52+
553
export default VueComponent => {
654
const dashed = camel => camel.replace(/([a-zA-Z])(?=[A-Z])/g, '$1-').toLowerCase();
755
const name = dashed(VueComponent.name);
@@ -15,10 +63,10 @@ export default VueComponent => {
1563
vueInstanceCreatedCallback() {
1664
// by default, `contenteditable` will be false
1765
this.setAttribute('contenteditable', Boolean(VueComponent.contentEditable));
18-
66+
const id = `markdown-field-${uuidv4()}`;
1967
// a hack to prevent squire from merging custom element spans
2068
// see here: https://github.com/nhn/tui.editor/blob/master/libs/squire/source/Node.js#L92-L101
21-
this.classList.add(`markdown-field-${uuidv4()}`);
69+
this.classList.add(id);
2270

2371
// pass innerHTML of host element as the `markdown` property
2472
this.observer = new MutationObserver(mutations => {
@@ -35,14 +83,38 @@ export default VueComponent => {
3583
this.innerHTML = textNodesRemoved.map(n => n.nodeValue).join();
3684
} else {
3785
// otherwise, pass the innerHTML to inner Vue component as `markdown` prop
38-
this.getVueInstance().markdown = this.innerHTML;
86+
this.markdown = this.innerHTML;
3987
}
4088
});
4189
});
4290
this.observer.observe(this, { characterData: true, childList: true });
4391

92+
this.addEventListener('remove', () => {
93+
if (hasLeftwardSpace(this)) {
94+
this.previousSibling.textContent = this.previousSibling.textContent.replace(
95+
leftwardSpaceRegex,
96+
''
97+
);
98+
}
99+
if (hasRightwardSpace(this)) {
100+
this.nextSibling.textContent = this.nextSibling.textContent.replace(
101+
rightwardSpaceRegex,
102+
''
103+
);
104+
}
105+
this.parentNode.removeChild(this);
106+
});
107+
108+
this.editing = true;
109+
110+
if (!hasLeftwardSpace(this)) {
111+
this.insertAdjacentText('beforebegin', '\xa0');
112+
}
113+
if (!hasRightwardSpace(this)) {
114+
this.insertAdjacentText('afterend', '\xa0');
115+
}
44116
// initialize the `markdown` property
45-
this.getVueInstance().$root.markdown = this.innerHTML;
117+
this.markdown = this.innerHTML;
46118
},
47119
shadowCss: VueComponent.shadowCSS,
48120
shadow: true,

0 commit comments

Comments
 (0)