Skip to content

Commit 1195be4

Browse files
authored
Replace coloris with vanilla-colorful (#30201)
Found [a better color picker](https://github.com/web-padawan/vanilla-colorful) that [does not rely](mdbassit/Coloris#139) on `querySelectorAll` or a global shared instance, and is also around a third of the size of the previous one. The popover is handled by tippy.js for which I introduced a new "bare" theme and it uses a new sibling-based mechanism which should prove useful later to create tippy popovers via HTML only. <img width="846" alt="Screenshot 2024-03-31 at 04 03 38" src="https://github.com/go-gitea/gitea/assets/115237/7639b911-a2d7-4f5c-bffd-a9d84561e747">
1 parent 654cfd1 commit 1195be4

File tree

6 files changed

+92
-162
lines changed

6 files changed

+92
-162
lines changed

package-lock.json

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
"@github/relative-time-element": "4.4.0",
1313
"@github/text-expander-element": "2.6.1",
1414
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
15-
"@melloware/coloris": "0.23.0",
1615
"@primer/octicons": "19.9.0",
1716
"add-asset-webpack-plugin": "2.0.1",
1817
"ansi_up": "6.0.2",
@@ -53,6 +52,7 @@
5352
"toastify-js": "1.12.0",
5453
"tributejs": "5.1.3",
5554
"uint8-to-base64": "0.2.0",
55+
"vanilla-colorful": "0.7.2",
5656
"vue": "3.4.21",
5757
"vue-bar-graph": "2.0.0",
5858
"vue-chartjs": "5.3.0",

web_src/css/features/colorpicker.css

Lines changed: 12 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,6 @@
1-
/* This is a stripped-down version of coloris's CSS tailored to our needs. It does only include
2-
opaqua colors, and if more features like opacity are needed, the CSS needs to be extended
3-
based on upstream: https://github.com/mdbassit/Coloris/blob/main/src/coloris.css. */
4-
51
.js-color-picker-input {
62
display: flex;
7-
flex-wrap: wrap;
3+
position: relative;
84
}
95

106
.js-color-picker-input input {
@@ -13,152 +9,39 @@
139
padding-left: 32px !important;
1410
}
1511

16-
.clr-picker {
17-
display: none;
18-
flex-wrap: wrap;
19-
position: absolute;
20-
width: 200px;
21-
z-index: 1002; /* above .ui.modal which has 1001 */
22-
border-radius: var(--border-radius);
23-
background-color: var(--color-menu);
24-
justify-content: flex-end;
25-
direction: ltr;
26-
box-shadow: 0 5px 20px var(--color-shadow);
27-
user-select: none;
28-
}
29-
30-
.clr-picker.clr-open {
31-
display: flex;
32-
}
33-
34-
.clr-gradient {
35-
position: relative;
36-
width: 100%;
37-
height: 100px;
38-
border-radius: 3px 3px 0 0;
39-
background: linear-gradient(rgba(0,0,0,0), #000), linear-gradient(90deg, #fff, currentcolor); /* stylelint-disable-line scale-unlimited/declaration-strict-value */
40-
cursor: pointer;
41-
}
42-
43-
.clr-marker {
44-
position: absolute;
45-
width: 12px;
46-
height: 12px;
47-
margin: -6px 0 0 -6px;
48-
border: 1px solid var(--color-white);
49-
border-radius: 50%;
50-
background-color: currentcolor;
51-
cursor: pointer;
52-
}
53-
54-
.clr-picker input[type="range"]::-webkit-slider-runnable-track {
55-
width: 100%;
56-
height: 16px;
57-
}
58-
59-
.clr-picker input[type="range"]::-webkit-slider-thumb {
60-
width: 16px;
61-
height: 16px;
62-
-webkit-appearance: none;
63-
}
64-
65-
.clr-picker input[type="range"]::-moz-range-track {
66-
width: 100%;
67-
height: 16px;
68-
border: 0;
69-
}
70-
71-
.clr-picker input[type="range"]::-moz-range-thumb {
72-
width: 16px;
73-
height: 16px;
74-
border: 0;
75-
}
76-
77-
.clr-hue {
78-
background: linear-gradient(to right, #f00 0%, #ff0 16.66%, #0f0 33.33%, #0ff 50%, #00f 66.66%, #f0f 83.33%, #f00 100%); /* stylelint-disable-line scale-unlimited/declaration-strict-value */
79-
position: relative;
80-
width: calc(100% - 40px);
81-
height: 10px;
82-
margin: 10px 20px;
83-
border-radius: 4px;
84-
}
85-
86-
.clr-hue input[type="range"] {
87-
position: absolute;
88-
width: calc(100% + 32px);
89-
margin: 0;
90-
background-color: transparent;
91-
opacity: 0;
92-
cursor: pointer;
93-
appearance: none;
94-
}
95-
96-
.clr-hue div {
97-
position: absolute;
98-
width: 16px;
99-
height: 16px;
100-
left: 0;
101-
top: 50%;
102-
transform: translate(-50%, -50%);
103-
border: 2px solid var(--color-white);
104-
border-radius: 50%;
105-
background-color: currentcolor;
106-
box-shadow: 0 0 1px var(--color-shadow);
107-
pointer-events: none;
108-
}
109-
110-
.clr-field {
111-
flex: 1;
112-
position: relative;
113-
color: transparent;
114-
}
115-
116-
.clr-field button {
12+
.js-color-picker-input .preview-square {
11713
position: absolute;
11814
aspect-ratio: 1;
11915
height: 16px;
12016
left: 10px;
12117
top: 50%;
12218
transform: translateY(-50%);
123-
margin: 0;
124-
padding: 0;
125-
border: 0;
126-
color: inherit;
127-
pointer-events: none;
12819
border-radius: 2px;
12920
background: repeating-linear-gradient(45deg, #aaa 25%, transparent 25%, transparent 75%, #aaa 75%, #aaa), repeating-linear-gradient(45deg, #aaa 25%, #fff 25%, #fff 75%, #aaa 75%, #aaa); /* stylelint-disable-line scale-unlimited/declaration-strict-value */
13021
background-position: 0 0, 4px 4px;
13122
background-size: 8px 8px;
13223
}
13324

134-
.clr-field button::after {
25+
.js-color-picker-input .preview-square::after {
13526
content: "";
136-
display: block;
13727
position: absolute;
13828
width: 100%;
13929
height: 100%;
140-
left: 0;
141-
top: 0;
14230
border-radius: inherit;
14331
background-color: currentcolor;
14432
}
14533

146-
.clr-marker:focus {
147-
outline: none;
34+
hex-color-picker {
35+
width: 180px;
36+
height: 120px;
14837
}
14938

150-
.clr-keyboard-nav .clr-marker:focus,
151-
.clr-keyboard-nav .clr-hue input:focus + div,
152-
.clr-keyboard-nav .clr-alpha input:focus + div {
153-
outline: none;
154-
box-shadow: 0 0 2px 2px var(--color-white);
39+
hex-color-picker::part(hue-pointer),
40+
hex-color-picker::part(saturation-pointer) {
41+
width: 22px;
42+
height: 22px;
15543
}
15644

157-
.clr-picker .clr-preview,
158-
.clr-picker .clr-clear,
159-
.clr-picker .clr-swatches,
160-
.clr-picker .clr-format,
161-
.clr-picker .clr-alpha,
162-
.clr-picker .clr-color {
163-
display: none;
45+
hex-color-picker::part(hue) {
46+
flex-basis: 16px;
16447
}

web_src/css/modules/tippy.css

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,17 @@
2929
z-index: 1;
3030
}
3131

32+
/* bare theme, no styling at all, except box-shadow */
33+
.tippy-box[data-theme="bare"] {
34+
border: none;
35+
box-shadow: 0 6px 18px var(--color-shadow);
36+
}
37+
38+
.tippy-box[data-theme="bare"] .tippy-content {
39+
padding: 0;
40+
background: transparent;
41+
}
42+
3243
/* tooltip theme for text tooltips */
3344

3445
.tippy-box[data-theme="tooltip"] {

web_src/js/features/colorpicker.js

Lines changed: 58 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,66 @@
1-
export async function initColorPickers(selector = '.js-color-picker-input input', opts = {}) {
2-
const inputEls = document.querySelectorAll(selector);
3-
if (!inputEls.length) return;
1+
import {createTippy} from '../modules/tippy.js';
42

5-
const [{coloris, init}] = await Promise.all([
6-
import(/* webpackChunkName: "colorpicker" */'@melloware/coloris'),
3+
export async function initColorPickers() {
4+
const els = document.getElementsByClassName('js-color-picker-input');
5+
if (!els.length) return;
6+
7+
await Promise.all([
8+
import(/* webpackChunkName: "colorpicker" */'vanilla-colorful/hex-color-picker.js'),
79
import(/* webpackChunkName: "colorpicker" */'../../css/features/colorpicker.css'),
810
]);
911

10-
init();
11-
coloris({
12-
el: selector,
13-
alpha: false,
14-
focusInput: true,
15-
selectInput: false,
16-
...opts,
12+
for (const el of els) {
13+
initPicker(el);
14+
}
15+
}
16+
17+
function updateSquare(el, newValue) {
18+
el.style.color = /#[0-9a-f]{6}/i.test(newValue) ? newValue : 'transparent';
19+
}
20+
21+
function updatePicker(el, newValue) {
22+
el.setAttribute('color', newValue);
23+
}
24+
25+
function initPicker(el) {
26+
const input = el.querySelector('input');
27+
28+
const square = document.createElement('div');
29+
square.classList.add('preview-square');
30+
updateSquare(square, input.value);
31+
el.append(square);
32+
33+
const picker = document.createElement('hex-color-picker');
34+
picker.addEventListener('color-changed', (e) => {
35+
input.value = e.detail.value;
36+
input.focus();
37+
updateSquare(square, e.detail.value);
38+
});
39+
40+
input.addEventListener('input', (e) => {
41+
updateSquare(square, e.target.value);
42+
updatePicker(picker, e.target.value);
43+
});
44+
45+
createTippy(input, {
46+
trigger: 'focus click',
47+
theme: 'bare',
48+
hideOnClick: true,
49+
content: picker,
50+
placement: 'bottom-start',
51+
interactive: true,
52+
onShow() {
53+
updatePicker(picker, input.value);
54+
},
1755
});
1856

19-
for (const inputEl of inputEls) {
20-
const parent = inputEl.closest('.js-color-picker-input');
21-
// prevent tabbing on the color preview `button` inside the input
22-
parent.querySelector('button').tabIndex = -1;
23-
// init precolors
24-
for (const el of parent.querySelectorAll('.precolors .color')) {
25-
el.addEventListener('click', (e) => {
26-
inputEl.value = e.target.getAttribute('data-color-hex');
27-
inputEl.dispatchEvent(new Event('input', {bubbles: true}));
28-
});
29-
}
57+
// init precolors
58+
for (const colorEl of el.querySelectorAll('.precolors .color')) {
59+
colorEl.addEventListener('click', (e) => {
60+
const newValue = e.target.getAttribute('data-color-hex');
61+
input.value = newValue;
62+
input.dispatchEvent(new Event('input', {bubbles: true}));
63+
updateSquare(square, newValue);
64+
});
3065
}
3166
}

web_src/js/modules/tippy.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ import {isDocumentFragmentOrElementNode} from '../utils/dom.js';
33
import {formatDatetime} from '../utils/time.js';
44

55
const visibleInstances = new Set();
6+
const arrowSvg = `<svg width="16" height="7"><path d="m0 7 8-7 8 7Z" class="tippy-svg-arrow-outer"/><path d="m0 8 8-7 8 7Z" class="tippy-svg-arrow-inner"/></svg>`;
67

78
export function createTippy(target, opts = {}) {
89
// the callback functions should be destructured from opts,
910
// because we should use our own wrapper functions to handle them, do not let the user override them
10-
const {onHide, onShow, onDestroy, role, theme, ...other} = opts;
11+
const {onHide, onShow, onDestroy, role, theme, arrow, ...other} = opts;
1112

1213
const instance = tippy(target, {
1314
appendTo: document.body,
@@ -35,9 +36,9 @@ export function createTippy(target, opts = {}) {
3536
visibleInstances.add(instance);
3637
return onShow?.(instance);
3738
},
38-
arrow: `<svg width="16" height="7"><path d="m0 7 8-7 8 7Z" class="tippy-svg-arrow-outer"/><path d="m0 8 8-7 8 7Z" class="tippy-svg-arrow-inner"/></svg>`,
39+
arrow: arrow || (theme === 'bare' ? false : arrowSvg),
3940
role: role || 'menu', // HTML role attribute
40-
theme: theme || role || 'menu', // CSS theme, either "tooltip", "menu" or "box-with-header"
41+
theme: theme || role || 'menu', // CSS theme, either "tooltip", "menu", "box-with-header" or "bare"
4142
plugins: [followCursor],
4243
...other,
4344
});

0 commit comments

Comments
 (0)