Commit 3e46f22b authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch '339537-description-lists' into 'master'

Add support for description lists in content editor

See merge request gitlab-org/gitlab!69149
parents 719fe361 32b0de5f
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';
import BulletList from '../extensions/bullet_list';
import Code from '../extensions/code';
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 Document from '../extensions/document';
import Dropcursor from '../extensions/dropcursor';
......@@ -74,6 +76,8 @@ export const createContentEditor = ({
BulletList,
Code,
CodeBlockHighlight,
DescriptionItem,
DescriptionList,
Document,
Division,
Dropcursor,
......
......@@ -9,6 +9,8 @@ import Bold from '../extensions/bold';
import BulletList from '../extensions/bullet_list';
import Code from '../extensions/code';
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 Emoji from '../extensions/emoji';
import Figure from '../extensions/figure';
......@@ -122,6 +124,12 @@ const defaultSerializerConfig = {
state.closeBlock(node);
},
[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) => {
const { name } = node.attrs;
......
......@@ -6,6 +6,11 @@ const defaultAttrs = {
th: { colspan: 1, rowspan: 1, colwidth: null },
};
const ignoreAttrs = {
dd: ['isTerm'],
dt: ['isTerm'],
};
const tableMap = new WeakMap();
// Source taken from
......@@ -118,7 +123,8 @@ export function openTag(tagName, attrs) {
str += Object.entries(attrs || {})
.map(([key, value]) => {
if (defaultAttrs[tagName]?.[key] === value) return '';
if ((ignoreAttrs[tagName] || []).includes(key) || defaultAttrs[tagName]?.[key] === value)
return '';
return ` ${key}="${htmlEncode(value?.toString())}"`;
})
......
.ProseMirror {
td,
th,
li {
li,
dd,
dt {
:first-child {
margin-bottom: 0 !important;
}
......@@ -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 {
......
......@@ -3,6 +3,8 @@ import Bold from '~/content_editor/extensions/bold';
import BulletList from '~/content_editor/extensions/bullet_list';
import Code from '~/content_editor/extensions/code';
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 Emoji from '~/content_editor/extensions/emoji';
import Figure from '~/content_editor/extensions/figure';
......@@ -41,6 +43,8 @@ const tiptapEditor = createTestEditor({
BulletList,
Code,
CodeBlockHighlight,
DescriptionItem,
DescriptionList,
Division,
Emoji,
Figure,
......@@ -75,6 +79,8 @@ const {
code,
codeBlock,
division,
descriptionItem,
descriptionList,
emoji,
figure,
figureCaption,
......@@ -105,6 +111,8 @@ const {
code: { markType: Code.name },
codeBlock: { nodeType: CodeBlockHighlight.name },
division: { nodeType: Division.name },
descriptionItem: { nodeType: DescriptionItem.name },
descriptionList: { nodeType: DescriptionList.name },
emoji: { markType: Emoji.name },
figure: { nodeType: Figure.name },
figureCaption: { nodeType: FigureCaption.name },
......@@ -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', () => {
expect(
serialize(
......
......@@ -59,6 +59,24 @@
</figcaption>
</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
markdown: '[GitLab](https://gitlab.com)'
- 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