Commit 32b0de5f authored by Himanshu Kapoor's avatar Himanshu Kapoor

Allow support for description lists in content editor

Content editor now supports rendering and editing dl/dd/dt elements.
Creating and editing these elements works in a very similar way to
regular lists.

Type `<dl>` in content editor to insert a new description list.
Press Enter when within a description list to create a new description
term. Press Tab to convert a description term to a description details.
Press Shift-Tab to convert a description details to a description term.
Press Shift-Tab again to convert it to a regular paragraph.

Changelog: added
parent 964390f4
import { Node, mergeAttributes } from '@tiptap/core';
export default Node.create({
name: 'descriptionItem',
content: 'block+',
defining: true,
addAttributes() {
return {
isTerm: {
default: true,
parseHTML: (element) => ({
isTerm: element.tagName.toLowerCase() === 'dt',
}),
},
};
},
parseHTML() {
return [{ tag: 'dt' }, { tag: 'dd' }];
},
renderHTML({ HTMLAttributes: { isTerm, ...HTMLAttributes } }) {
return [
'li',
mergeAttributes(HTMLAttributes, { class: isTerm ? 'dl-term' : 'dl-description' }),
0,
];
},
addKeyboardShortcuts() {
return {
Enter: () => {
return this.editor.commands.splitListItem('descriptionItem');
},
Tab: () => {
const { isTerm } = this.editor.getAttributes('descriptionItem');
if (isTerm)
return this.editor.commands.updateAttributes('descriptionItem', { isTerm: !isTerm });
return false;
},
'Shift-Tab': () => {
const { isTerm } = this.editor.getAttributes('descriptionItem');
if (isTerm) return this.editor.commands.liftListItem('descriptionItem');
return this.editor.commands.updateAttributes('descriptionItem', { isTerm: true });
},
};
},
});
import { Node, mergeAttributes } from '@tiptap/core';
import { wrappingInputRule } from 'prosemirror-inputrules';
export const inputRegex = /^\s*(<dl>)$/;
export default Node.create({
name: 'descriptionList',
// eslint-disable-next-line @gitlab/require-i18n-strings
group: 'block list',
content: 'descriptionItem+',
parseHTML() {
return [{ tag: 'dl' }];
},
renderHTML({ HTMLAttributes }) {
return ['ul', mergeAttributes(HTMLAttributes, { class: 'dl-content' }), 0];
},
addInputRules() {
return [wrappingInputRule(inputRegex, this.type)];
},
});
...@@ -8,6 +8,8 @@ import Bold from '../extensions/bold'; ...@@ -8,6 +8,8 @@ import Bold from '../extensions/bold';
import BulletList from '../extensions/bullet_list'; import BulletList from '../extensions/bullet_list';
import Code from '../extensions/code'; import Code from '../extensions/code';
import CodeBlockHighlight from '../extensions/code_block_highlight'; import CodeBlockHighlight from '../extensions/code_block_highlight';
import DescriptionItem from '../extensions/description_item';
import DescriptionList from '../extensions/description_list';
import Division from '../extensions/division'; import Division from '../extensions/division';
import Document from '../extensions/document'; import Document from '../extensions/document';
import Dropcursor from '../extensions/dropcursor'; import Dropcursor from '../extensions/dropcursor';
...@@ -73,6 +75,8 @@ export const createContentEditor = ({ ...@@ -73,6 +75,8 @@ export const createContentEditor = ({
BulletList, BulletList,
Code, Code,
CodeBlockHighlight, CodeBlockHighlight,
DescriptionItem,
DescriptionList,
Document, Document,
Division, Division,
Dropcursor, Dropcursor,
......
...@@ -9,6 +9,8 @@ import Bold from '../extensions/bold'; ...@@ -9,6 +9,8 @@ import Bold from '../extensions/bold';
import BulletList from '../extensions/bullet_list'; import BulletList from '../extensions/bullet_list';
import Code from '../extensions/code'; import Code from '../extensions/code';
import CodeBlockHighlight from '../extensions/code_block_highlight'; import CodeBlockHighlight from '../extensions/code_block_highlight';
import DescriptionItem from '../extensions/description_item';
import DescriptionList from '../extensions/description_list';
import Division from '../extensions/division'; import Division from '../extensions/division';
import Emoji from '../extensions/emoji'; import Emoji from '../extensions/emoji';
import Figure from '../extensions/figure'; import Figure from '../extensions/figure';
...@@ -121,6 +123,12 @@ const defaultSerializerConfig = { ...@@ -121,6 +123,12 @@ const defaultSerializerConfig = {
state.closeBlock(node); state.closeBlock(node);
}, },
[Division.name]: renderHTMLNode('div'), [Division.name]: renderHTMLNode('div'),
[DescriptionList.name]: renderHTMLNode('dl', true),
[DescriptionItem.name]: (state, node, parent, index) => {
if (index === 1) state.ensureNewLine();
renderHTMLNode(node.attrs.isTerm ? 'dt' : 'dd')(state, node);
if (index === parent.childCount - 1) state.ensureNewLine();
},
[Emoji.name]: (state, node) => { [Emoji.name]: (state, node) => {
const { name } = node.attrs; const { name } = node.attrs;
......
...@@ -6,6 +6,11 @@ const defaultAttrs = { ...@@ -6,6 +6,11 @@ const defaultAttrs = {
th: { colspan: 1, rowspan: 1, colwidth: null }, th: { colspan: 1, rowspan: 1, colwidth: null },
}; };
const ignoreAttrs = {
dd: ['isTerm'],
dt: ['isTerm'],
};
const tableMap = new WeakMap(); const tableMap = new WeakMap();
// Source taken from // Source taken from
...@@ -118,7 +123,8 @@ export function openTag(tagName, attrs) { ...@@ -118,7 +123,8 @@ export function openTag(tagName, attrs) {
str += Object.entries(attrs || {}) str += Object.entries(attrs || {})
.map(([key, value]) => { .map(([key, value]) => {
if (defaultAttrs[tagName]?.[key] === value) return ''; if ((ignoreAttrs[tagName] || []).includes(key) || defaultAttrs[tagName]?.[key] === value)
return '';
return ` ${key}="${htmlEncode(value?.toString())}"`; return ` ${key}="${htmlEncode(value?.toString())}"`;
}) })
......
.ProseMirror { .ProseMirror {
td, td,
th, th,
li { li,
dd,
dt {
:first-child { :first-child {
margin-bottom: 0 !important; margin-bottom: 0 !important;
} }
...@@ -34,6 +36,20 @@ ...@@ -34,6 +36,20 @@
} }
} }
} }
.dl-content {
width: 100%;
> li {
list-style-type: none;
margin-left: $gl-spacing-scale-5;
&.dl-term {
margin: 0;
font-weight: 600;
}
}
}
} }
.table-creator-grid-item { .table-creator-grid-item {
......
...@@ -3,6 +3,8 @@ import Bold from '~/content_editor/extensions/bold'; ...@@ -3,6 +3,8 @@ import Bold from '~/content_editor/extensions/bold';
import BulletList from '~/content_editor/extensions/bullet_list'; import BulletList from '~/content_editor/extensions/bullet_list';
import Code from '~/content_editor/extensions/code'; import Code from '~/content_editor/extensions/code';
import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight'; import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
import DescriptionItem from '~/content_editor/extensions/description_item';
import DescriptionList from '~/content_editor/extensions/description_list';
import Division from '~/content_editor/extensions/division'; import Division from '~/content_editor/extensions/division';
import Emoji from '~/content_editor/extensions/emoji'; import Emoji from '~/content_editor/extensions/emoji';
import Figure from '~/content_editor/extensions/figure'; import Figure from '~/content_editor/extensions/figure';
...@@ -41,6 +43,8 @@ const tiptapEditor = createTestEditor({ ...@@ -41,6 +43,8 @@ const tiptapEditor = createTestEditor({
BulletList, BulletList,
Code, Code,
CodeBlockHighlight, CodeBlockHighlight,
DescriptionItem,
DescriptionList,
Division, Division,
Emoji, Emoji,
Figure, Figure,
...@@ -75,6 +79,8 @@ const { ...@@ -75,6 +79,8 @@ const {
code, code,
codeBlock, codeBlock,
division, division,
descriptionItem,
descriptionList,
emoji, emoji,
figure, figure,
figureCaption, figureCaption,
...@@ -105,6 +111,8 @@ const { ...@@ -105,6 +111,8 @@ const {
code: { markType: Code.name }, code: { markType: Code.name },
codeBlock: { nodeType: CodeBlockHighlight.name }, codeBlock: { nodeType: CodeBlockHighlight.name },
division: { nodeType: Division.name }, division: { nodeType: Division.name },
descriptionItem: { nodeType: DescriptionItem.name },
descriptionList: { nodeType: DescriptionList.name },
emoji: { markType: Emoji.name }, emoji: { markType: Emoji.name },
figure: { nodeType: Figure.name }, figure: { nodeType: Figure.name },
figureCaption: { nodeType: FigureCaption.name }, figureCaption: { nodeType: FigureCaption.name },
...@@ -545,6 +553,41 @@ this is not really json but just trying out whether this case works or not ...@@ -545,6 +553,41 @@ this is not really json but just trying out whether this case works or not
); );
}); });
it('correctly renders a description list', () => {
expect(
serialize(
descriptionList(
descriptionItem(paragraph('Beast of Bodmin')),
descriptionItem({ isTerm: false }, paragraph('A large feline inhabiting Bodmin Moor.')),
descriptionItem(paragraph('Morgawr')),
descriptionItem({ isTerm: false }, paragraph('A sea serpent.')),
descriptionItem(paragraph('Owlman')),
descriptionItem(
{ isTerm: false },
paragraph('A giant ', italic('owl-like'), ' creature.'),
),
),
),
).toBe(
`
<dl>
<dt>Beast of Bodmin</dt>
<dd>A large feline inhabiting Bodmin Moor.</dd>
<dt>Morgawr</dt>
<dd>A sea serpent.</dd>
<dt>Owlman</dt>
<dd>
A giant _owl-like_ creature.
</dd>
</dl>
`.trim(),
);
});
it('correctly renders div', () => { it('correctly renders div', () => {
expect( expect(
serialize( serialize(
......
...@@ -59,6 +59,24 @@ ...@@ -59,6 +59,24 @@
</figcaption> </figcaption>
</figure> </figure>
- name: description_list
markdown: |-
<dl>
<dt>Frog</dt>
<dd>Wet green thing</dd>
<dt>Rabbit</dt>
<dd>Warm fluffy thing</dd>
<dt>Punt</dt>
<dd>Kick a ball</dd>
<dd>Take a bet</dd>
<dt>Color</dt>
<dt>Colour</dt>
<dd>
Any hue except _white_ or **black**
</dd>
</dl>
- name: link - name: link
markdown: '[GitLab](https://gitlab.com)' markdown: '[GitLab](https://gitlab.com)'
- name: attachment_link - name: attachment_link
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment