[ADD] web_editor: insert inline code by wrapping it in backticks

This transforms text between two backticks into a code element, when
typing the closing backtick.

task-2985108

closes odoo/odoo#100218

Signed-off-by: David Monjoie (dmo) <dmo@odoo.com>
This commit is contained in:
Antoine Guenet
2022-09-15 19:21:22 +02:00
parent d30cab0b7f
commit 33048c5dad
3 changed files with 110 additions and 0 deletions
@@ -66,6 +66,7 @@ import {
hasTableSelection,
pxToFloat,
parseHTML,
splitTextNode,
} from './utils/utils.js';
import { editorCommands } from './commands/commands.js';
import { Powerbox } from './powerbox/Powerbox.js';
@@ -3011,6 +3012,54 @@ export class OdooEditor extends EventTarget {
);
}
}
if (ev.data === '`' && !closestElement(selection.anchorNode, 'code')) {
// We just inserted a backtick, check if there was another
// one before.
const range = getDeepRange(this.editable);
let textNode = range.startContainer;
let offset = range.startOffset;
let sibling = textNode.previousSibling;
while (sibling && sibling.nodeType === Node.TEXT_NODE) {
offset += sibling.textContent.length;
sibling.textContent += textNode.textContent;
textNode.remove();
textNode = sibling;
sibling = textNode.previousSibling;
}
sibling = textNode.nextSibling;
while (sibling && sibling.nodeType === Node.TEXT_NODE) {
sibling.textContent =+ textNode.textContent;
textNode.remove();
textNode = sibling;
sibling = textNode.nextSibling;
}
setSelection(textNode, offset);
const textHasOpeningTick = /`.*`/.test(textNode.textContent);
if (textHasOpeningTick) {
this.historyStep();
if (offset !== textNode.textContent.length) {
splitTextNode(textNode, offset);
textNode = textNode.previousSibling;
}
const openingTickOffset = textNode.textContent.substring(0, textNode.textContent.length - 1).lastIndexOf('`');
if (openingTickOffset) {
splitTextNode(textNode, openingTickOffset);
}
// Remove ticks.
textNode.textContent = textNode.textContent.substring(1, textNode.textContent.length - 1);
// Insert code element.
const codeElement = this.document.createElement('code');
codeElement.classList.add('o_inline_code');
textNode.before(codeElement);
codeElement.append(textNode);
if (!codeElement.previousSibling || codeElement.previousSibling.nodeType !== Node.TEXT_NODE) {
codeElement.before(document.createTextNode('\u200B'));
}
// Move selection out of code element.
codeElement.after(document.createTextNode('\u200B'));
setSelection(codeElement.nextSibling, 1);
}
}
this.historyStep();
} else if (ev.inputType === 'insertLineBreak') {
this._compositionStep();
@@ -429,3 +429,10 @@ i.oe-powerbox-commandImg {
z-index: 1;
}
}
code.o_inline_code {
background-color: #c5c5c5;
padding: 2px;
margin: 2px;
color: black;
font-size: inherit;
}
@@ -5122,4 +5122,58 @@ X[]
});
});
});
describe('markdown', () => {
describe('inline code', () => {
it('should convert text into inline code (start)', async () => {
await testEditor(BasicEditor, {
contentBefore: '<p>`ab[]cd</p>',
stepFunction: async editor => insertText(editor, '`'),
contentAfter: '<p>\u200B<code class="o_inline_code">ab</code>\u200B[]cd</p>',
});
});
it('should convert text into inline code (middle)', async () => {
await testEditor(BasicEditor, {
contentBefore: '<p>ab`cd[]ef</p>',
stepFunction: async editor => insertText(editor, '`'),
contentAfter: '<p>ab<code class="o_inline_code">cd</code>\u200B[]ef</p>',
});
});
it('should convert text into inline code (end)', async () => {
await testEditor(BasicEditor, {
contentBefore: '<p>ab`cd[]</p>',
stepFunction: async editor => insertText(editor, '`'),
contentAfter: '<p>ab<code class="o_inline_code">cd</code>\u200B[]</p>',
});
});
it('should convert text into inline code and leave an earlier and a later backtick alone', async () => {
await testEditor(BasicEditor, {
contentBefore: '<p>a`b`cd[]e`f</p>',
stepFunction: async editor => insertText(editor, '`'),
contentAfter: '<p>a`b<code class="o_inline_code">cd</code>\u200B[]e`f</p>',
});
});
it('should not convert text into inline code when traversing HTMLElements', async () => {
await testEditor(BasicEditor, {
contentBefore: '<p>ab`c<strong>d</strong>e[]fg</p>',
stepFunction: async editor => insertText(editor, '`'),
contentAfter: '<p>ab`c<strong>d</strong>e`[]fg</p>',
});
});
it('should not convert text into inline code when interrupted by linebreak', async () => {
await testEditor(BasicEditor, {
contentBefore: '<p>ab`c<br>d[]ef</p>',
stepFunction: async editor => insertText(editor, '`'),
contentAfter: '<p>ab`c<br>d`[]ef</p>',
});
});
it('should not convert text into inline code when inside inline code', async () => {
await testEditor(BasicEditor, {
contentBefore: '<p>a<code class="o_inline_code">b`cd[]e</code>f</p>',
stepFunction: async editor => insertText(editor, '`'),
contentAfter: '<p>a<code class="o_inline_code">b`cd`[]e</code>f</p>',
});
});
});
});
});