Skip to content

Commit 4df2003

Browse files
committed
feat(color-picker): hex/rgb text input in the palette popup
The color popup only offered the fixed palette and the optional native browser picker — a copied hex code could not be pasted anywhere. Add a text input to the widget's extra area: accepts #RRGGBB, RRGGBB, short #RGB and rgb()/rgba(), applies on Enter or change, ignores invalid values. Also scope the form-level mousedown preventDefault so inputs inside the picker can be focused, and restrict the native-picker change handler to input[type=color]. Adds regression tests for styling a fully selected list (jodit-react#313). Closes jodit/jodit-react#310
1 parent 728c420 commit 4df2003

5 files changed

Lines changed: 214 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@
99
> - :house: [Internal]
1010
> - :nail_care: [Polish]
1111

12+
## 4.13.13
13+
14+
#### :rocket: New Feature
15+
16+
- **Color picker**: the palette popup now has a text input for the color value — paste or type a hex code (`#FF0000`, `FF0000`, short `#F00`) or an `rgb()`/`rgba()` value and press Enter (or blur) to apply it. Previously the only way to pick a non-palette color was the native browser picker. Invalid values are ignored. Requested in [jodit-react#310](https://github.com/jodit/jodit-react/issues/310).
17+
18+
#### :house: Internal
19+
20+
- Regression tests: applying styles/commands (`color`, `fontsize`, `bold`) to a fully selected list hits every `<li>`, not just one (reported in [jodit-react#313](https://github.com/jodit/jodit-react/issues/313) against an older jodit; not reproducible on current code).
21+
1222
## 4.13.12
1323

1424
#### :house: Internal

src/core/selection/style/style.test.js

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1032,6 +1032,58 @@ describe('Apply style', () => {
10321032
});
10331033
});
10341034

1035+
describe('For fully selected list', function () {
1036+
it('Should apply style to every list item', function () {
1037+
editor.value =
1038+
'<ul><li>one</li><li>two</li><li>three</li></ul>';
1039+
editor.execCommand('selectall');
1040+
1041+
const style = new Style({
1042+
attributes: {
1043+
style: {
1044+
color: 'yellow'
1045+
}
1046+
}
1047+
});
1048+
1049+
style.apply(editor);
1050+
1051+
expect(sortAttributes(editor.value)).equals(
1052+
'<ul>' +
1053+
'<li><span style="color:yellow">one</span></li>' +
1054+
'<li><span style="color:yellow">two</span></li>' +
1055+
'<li><span style="color:yellow">three</span></li>' +
1056+
'</ul>'
1057+
);
1058+
});
1059+
1060+
it('Should apply fontsize and bold commands to every list item', function () {
1061+
editor.value =
1062+
'<ul><li>one</li><li>two</li><li>three</li></ul>';
1063+
editor.execCommand('selectall');
1064+
editor.execCommand('fontsize', false, '24px');
1065+
1066+
expect(sortAttributes(editor.value)).equals(
1067+
'<ul>' +
1068+
'<li><span style="font-size:24px">one</span></li>' +
1069+
'<li><span style="font-size:24px">two</span></li>' +
1070+
'<li><span style="font-size:24px">three</span></li>' +
1071+
'</ul>'
1072+
);
1073+
1074+
editor.execCommand('selectall');
1075+
editor.execCommand('bold');
1076+
1077+
expect(sortAttributes(editor.value)).equals(
1078+
'<ul>' +
1079+
'<li><strong><span style="font-size:24px">one</span></strong></li>' +
1080+
'<li><strong><span style="font-size:24px">two</span></strong></li>' +
1081+
'<li><strong><span style="font-size:24px">three</span></strong></li>' +
1082+
'</ul>'
1083+
);
1084+
});
1085+
});
1086+
10351087
describe('For all content', function () {
10361088
it('Should apply style to all elements', function () {
10371089
editor.value = '<p><br></p><p>test</p>';

src/modules/widget/color-picker/color-picker.less

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,23 @@
4343
}
4444
}
4545

46+
&__extra {
47+
display: flex;
48+
align-items: center;
49+
justify-content: space-between;
50+
}
51+
52+
&__hex {
53+
input {
54+
width: 80px;
55+
padding: 2px 4px;
56+
border: 1px solid var(--color-border);
57+
border-radius: var(--border-radius-default);
58+
font-family: monospace;
59+
font-size: var(--font-size-small, 12px);
60+
}
61+
}
62+
4663
&__native {
4764
svg {
4865
display: inline-block;

src/modules/widget/color-picker/color-picker.ts

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,49 @@ export const ColorPickerWidget = (
9595

9696
const { extra } = refs(form);
9797

98+
extra.appendChild(
99+
editor.c.fromHTML(
100+
`<div class="${cn}__hex"><input data-ref="hexInput" type="text" spellcheck="false" aria-label="HEX" placeholder="#FF0000" value="${
101+
valueHex || ''
102+
}"/></div>`
103+
)
104+
);
105+
106+
const { hexInput } = refs<HTMLInputElement>(form);
107+
108+
const applyHexInput = (): void => {
109+
const raw = hexInput.value.trim();
110+
111+
const isHex = /^#?[0-9a-f]{3}(?:[0-9a-f]{3})?$/i.test(raw);
112+
const isRgb = /^rgba?\([\d\s.,%]+\)$/i.test(raw);
113+
114+
if (!isHex && !isRgb) {
115+
return;
116+
}
117+
118+
const color = normalizeColor(
119+
isHex && !raw.startsWith('#') ? '#' + raw : raw
120+
);
121+
122+
if (color && isFunction(callback)) {
123+
callback(color);
124+
}
125+
};
126+
127+
editor.e
128+
.on(hexInput, 'keydown', (e: KeyboardEvent) => {
129+
e.stopPropagation();
130+
131+
if (e.key === 'Enter') {
132+
e.preventDefault();
133+
applyHexInput();
134+
}
135+
})
136+
.on(hexInput, 'change', (e: Event) => {
137+
e.stopPropagation();
138+
applyHexInput();
139+
});
140+
98141
if (editor.o.showBrowserColorPicker && hasBrowserColorPicker()) {
99142
extra.appendChild(
100143
editor.c.fromHTML(
@@ -107,7 +150,12 @@ export const ColorPickerWidget = (
107150

108151
const target = e.target as HTMLInputElement;
109152

110-
if (!target || !target.tagName || !Dom.isTag(target, 'input')) {
153+
if (
154+
!target ||
155+
!target.tagName ||
156+
!Dom.isTag(target, 'input') ||
157+
target.type !== 'color'
158+
) {
111159
return;
112160
}
113161

@@ -122,6 +170,12 @@ export const ColorPickerWidget = (
122170
}
123171

124172
editor.e.on(form, 'mousedown touchend', (e: MouseEvent) => {
173+
// let the hex/native inputs receive focus and clicks
174+
if (Dom.isTag(e.target as Node, 'input')) {
175+
e.stopPropagation();
176+
return;
177+
}
178+
125179
e.stopPropagation();
126180
e.preventDefault();
127181

src/plugins/color/color.test.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,86 @@
4646
expect(popup2).is.null;
4747
});
4848

49+
describe('Hex input', function () {
50+
// https://github.com/jodit/jodit-react/issues/310
51+
it('Should apply pasted/typed hex color on Enter and on change', function () {
52+
const editor = getJodit();
53+
54+
editor.value = 'text2text';
55+
56+
const range = editor.s.createRange();
57+
range.setStart(editor.editor.firstChild.firstChild, 3);
58+
range.setEnd(editor.editor.firstChild.firstChild, 6);
59+
editor.s.selectRange(range);
60+
61+
clickButton('brush', editor);
62+
63+
const popup = getOpenedPopup(editor);
64+
const input = popup.querySelector(
65+
'.jodit-color-picker__hex input'
66+
);
67+
68+
expect(input).is.not.null;
69+
70+
input.value = '#FF0000';
71+
simulateEvent('change', input);
72+
73+
expect(editor.value).equals(
74+
'<p>tex<span style="background-color: rgb(255, 0, 0);">t2t</span>ext</p>'
75+
);
76+
});
77+
78+
it('Should accept a hex value without the leading hash', function () {
79+
const editor = getJodit();
80+
81+
editor.value = 'text2text';
82+
83+
const range = editor.s.createRange();
84+
range.setStart(editor.editor.firstChild.firstChild, 3);
85+
range.setEnd(editor.editor.firstChild.firstChild, 6);
86+
editor.s.selectRange(range);
87+
88+
clickButton('brush', editor);
89+
90+
const popup = getOpenedPopup(editor);
91+
const input = popup.querySelector(
92+
'.jodit-color-picker__hex input'
93+
);
94+
95+
input.value = '00FF00';
96+
simulateEvent('keydown', input, e => {
97+
e.key = 'Enter';
98+
});
99+
100+
expect(editor.value).equals(
101+
'<p>tex<span style="background-color: rgb(0, 255, 0);">t2t</span>ext</p>'
102+
);
103+
});
104+
105+
it('Should ignore an invalid value', function () {
106+
const editor = getJodit();
107+
108+
editor.value = 'text2text';
109+
110+
const range = editor.s.createRange();
111+
range.setStart(editor.editor.firstChild.firstChild, 3);
112+
range.setEnd(editor.editor.firstChild.firstChild, 6);
113+
editor.s.selectRange(range);
114+
115+
clickButton('brush', editor);
116+
117+
const popup = getOpenedPopup(editor);
118+
const input = popup.querySelector(
119+
'.jodit-color-picker__hex input'
120+
);
121+
122+
input.value = 'not-a-color';
123+
simulateEvent('change', input);
124+
125+
expect(editor.value).equals('<p>text2text</p>');
126+
});
127+
});
128+
49129
describe('Show native color picker', function () {
50130
describe('Enable', function () {
51131
describe('Select all content by edges', function () {

0 commit comments

Comments
 (0)