Commit 5e458015 authored by Ezekiel Kigbo's avatar Ezekiel Kigbo

Merge branch '332091-render-color-chips-in-the-content-editor' into 'master'

Render color chips in the Content Editor

See merge request gitlab-org/gitlab!71603
parents 6a244e11 76be221c
import { Node } from '@tiptap/core';
import { Plugin, PluginKey } from 'prosemirror-state';
import { Decoration, DecorationSet } from 'prosemirror-view';
import { isValidColorExpression } from '~/lib/utils/color_utils';
import { PARSE_HTML_PRIORITY_HIGHEST } from '../constants';
const colorExpressionTypes = ['#', 'hsl', 'rgb'];
const isValidColor = (color) => {
if (!colorExpressionTypes.some((type) => color.toLowerCase().startsWith(type))) {
return false;
}
return isValidColorExpression(color);
};
const highlightColors = (doc) => {
const decorations = [];
doc.descendants((node, position) => {
const { text, marks } = node;
if (!text || marks.length === 0 || marks[0].type.name !== 'code' || !isValidColor(text)) {
return;
}
const from = position;
const to = from + text.length;
const decoration = Decoration.inline(from, to, {
class: 'gl-display-inline-flex gl-align-items-center content-editor-color-chip',
style: `--gl-color-chip-color: ${text}`,
});
decorations.push(decoration);
});
return DecorationSet.create(doc, decorations);
};
export const colorDecoratorPlugin = new Plugin({
key: new PluginKey('colorDecorator'),
state: {
init(_, { doc }) {
return highlightColors(doc);
},
apply(transaction, oldState) {
return transaction.docChanged ? highlightColors(transaction.doc) : oldState;
},
},
props: {
decorations(state) {
return this.getState(state);
},
},
});
export default Node.create({
name: 'colorChip',
parseHTML() {
return [
{
tag: '.gfm-color_chip',
ignore: true,
priority: PARSE_HTML_PRIORITY_HIGHEST,
},
];
},
addProseMirrorPlugins() {
return [colorDecoratorPlugin];
},
});
......@@ -8,6 +8,7 @@ import Bold from '../extensions/bold';
import BulletList from '../extensions/bullet_list';
import Code from '../extensions/code';
import CodeBlockHighlight from '../extensions/code_block_highlight';
import ColorChip from '../extensions/color_chip';
import DescriptionItem from '../extensions/description_item';
import DescriptionList from '../extensions/description_list';
import Details from '../extensions/details';
......@@ -80,6 +81,7 @@ export const createContentEditor = ({
Bold,
BulletList,
Code,
ColorChip,
CodeBlockHighlight,
DescriptionItem,
DescriptionList,
......
const colorValidatorEl = document.createElement('div');
/**
* Validates whether the specified color expression
* is supported by the browser’s DOM API and has a valid form.
*
* This utility assigns the color expression to a detached DOM
* element’s color property. If the color expression is valid,
* the DOM API will accept the value.
*
* @param {String} color color expression rgba, hex, hsla, etc.
*/
export const isValidColorExpression = (colorExpression) => {
colorValidatorEl.style.color = '';
colorValidatorEl.style.color = colorExpression;
return colorValidatorEl.style.color.length > 0;
};
/**
* Convert hex color to rgb array
*
......
......@@ -104,3 +104,17 @@
@include gl-white-space-nowrap;
}
.content-editor-color-chip::after {
content: ' ';
display: inline-block;
align-items: center;
width: 11px;
height: 11px;
border-radius: 3px;
margin-left: 4px;
margin-top: -2px;
border: 1px solid $black-transparent;
background-color: var(--gl-color-chip-color);
}
import ColorChip, { colorDecoratorPlugin } from '~/content_editor/extensions/color_chip';
import Code from '~/content_editor/extensions/code';
import { createTestEditor } from '../test_utils';
describe('content_editor/extensions/color_chip', () => {
let tiptapEditor;
beforeEach(() => {
tiptapEditor = createTestEditor({ extensions: [ColorChip, Code] });
});
describe.each`
colorExpression | decorated
${'#F00'} | ${true}
${'rgba(0,0,0,0)'} | ${true}
${'hsl(540,70%,50%)'} | ${true}
${'F00'} | ${false}
${'F00'} | ${false}
${'gba(0,0,0,0)'} | ${false}
${'hls(540,70%,50%)'} | ${false}
${'red'} | ${false}
`(
'when a code span with $colorExpression color expression is found',
({ colorExpression, decorated }) => {
it(`${decorated ? 'adds' : 'does not add'} a color chip decorator`, () => {
tiptapEditor.commands.setContent(`<p><code>${colorExpression}</code></p>`);
const pluginState = colorDecoratorPlugin.getState(tiptapEditor.state);
expect(pluginState.children).toHaveLength(decorated ? 3 : 0);
});
},
);
});
......@@ -267,3 +267,14 @@
"title": "Page title"
}
;;;
- name: color_chips
markdown: |-
- `#F00`
- `#F00A`
- `#FF0000`
- `#FF0000AA`
- `RGB(0,255,0)`
- `RGB(0%,100%,0%)`
- `RGBA(0,255,0,0.3)`
- `HSL(540,70%,50%)`
- `HSLA(540,70%,50%,0.3)`
import {
isValidColorExpression,
textColorForBackground,
hexToRgb,
validateHexColor,
......@@ -72,4 +73,21 @@ describe('Color utils', () => {
},
);
});
describe('isValidColorExpression', () => {
it.each`
colorExpression | valid | desc
${'#F00'} | ${true} | ${'valid'}
${'rgba(0,0,0,0)'} | ${true} | ${'valid'}
${'hsl(540,70%,50%)'} | ${true} | ${'valid'}
${'red'} | ${true} | ${'valid'}
${'F00'} | ${false} | ${'invalid'}
${'F00'} | ${false} | ${'invalid'}
${'gba(0,0,0,0)'} | ${false} | ${'invalid'}
${'hls(540,70%,50%)'} | ${false} | ${'invalid'}
${'hello'} | ${false} | ${'invalid'}
`('color expression $colorExpression is $desc', ({ colorExpression, valid }) => {
expect(isValidColorExpression(colorExpression)).toBe(valid);
});
});
});
......@@ -9554,10 +9554,10 @@ prosemirror-transform@^1.0.0, prosemirror-transform@^1.1.0, prosemirror-transfor
dependencies:
prosemirror-model "^1.0.0"
prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.13.3, prosemirror-view@^1.16.5, prosemirror-view@^1.20.1:
version "1.20.1"
resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.20.1.tgz#174ba8ca358c73cc05e9a92a3d252bcf181ea337"
integrity sha512-djWORhy3a706mUH4A2dgEEV0IPZqQd1tFyz/ZVHJNoqhSgq82FwG6dq7uqHeUB2KdVSNfI2yc3rwfqlC/ll2pA==
prosemirror-view@^1.0.0, prosemirror-view@^1.1.0, prosemirror-view@^1.13.3, prosemirror-view@^1.16.5, prosemirror-view@^1.20.1, prosemirror-view@^1.20.2:
version "1.20.2"
resolved "https://registry.yarnpkg.com/prosemirror-view/-/prosemirror-view-1.20.2.tgz#fc073def4358fdbd617ea11f5cd4217c5123635d"
integrity sha512-zh67dsGCI7QKWDbtLEAdZLmadxBJYRArM8E0z2wfuNGpx4i6ObVGzHjbnblZs2n88IwqHdEA47rEhyUqZ+kAbg==
dependencies:
prosemirror-model "^1.14.3"
prosemirror-state "^1.0.0"
......
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