Replace coloris with vanilla-colorful (#30201)

Found [a better color
picker](https://github.com/web-padawan/vanilla-colorful) that [does not
rely](https://github.com/mdbassit/Coloris/issues/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">

(cherry picked from commit 1195be41a13d2198ab644c8558549edd74485510)
This commit is contained in:
silverwind 2024-04-03 11:15:06 +02:00 committed by Gergely Nagy
parent 2adc3a45fb
commit a53a94e1c2
No known key found for this signature in database
6 changed files with 94 additions and 164 deletions

12
package-lock.json generated
View file

@ -13,7 +13,6 @@
"@github/relative-time-element": "4.4.0", "@github/relative-time-element": "4.4.0",
"@github/text-expander-element": "2.6.1", "@github/text-expander-element": "2.6.1",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
"@melloware/coloris": "0.23.0",
"@primer/octicons": "19.9.0", "@primer/octicons": "19.9.0",
"add-asset-webpack-plugin": "2.0.1", "add-asset-webpack-plugin": "2.0.1",
"ansi_up": "6.0.2", "ansi_up": "6.0.2",
@ -54,6 +53,7 @@
"toastify-js": "1.12.0", "toastify-js": "1.12.0",
"tributejs": "5.1.3", "tributejs": "5.1.3",
"uint8-to-base64": "0.2.0", "uint8-to-base64": "0.2.0",
"vanilla-colorful": "0.7.2",
"vue": "3.4.21", "vue": "3.4.21",
"vue-bar-graph": "2.0.0", "vue-bar-graph": "2.0.0",
"vue-chartjs": "5.3.0", "vue-chartjs": "5.3.0",
@ -1291,11 +1291,6 @@
"@mcaptcha/core-glue": "^0.1.0-alpha-5" "@mcaptcha/core-glue": "^0.1.0-alpha-5"
} }
}, },
"node_modules/@melloware/coloris": {
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/@melloware/coloris/-/coloris-0.23.0.tgz",
"integrity": "sha512-VGIjI9+IQwg6BHjIE10yl0K2ARYz5bsjn6BgFEs1y1ErPAQymgdoxwVcSVL4Ai5t9OVs8xaCB7JKHqFu2N96Ow=="
},
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -12010,6 +12005,11 @@
"builtins": "^1.0.3" "builtins": "^1.0.3"
} }
}, },
"node_modules/vanilla-colorful": {
"version": "0.7.2",
"resolved": "https://registry.npmjs.org/vanilla-colorful/-/vanilla-colorful-0.7.2.tgz",
"integrity": "sha512-z2YZusTFC6KnLERx1cgoIRX2CjPRP0W75N+3CC6gbvdX5Ch47rZkEMGO2Xnf+IEmi3RiFLxS18gayMA27iU7Kg=="
},
"node_modules/vite": { "node_modules/vite": {
"version": "5.2.6", "version": "5.2.6",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.6.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.6.tgz",

View file

@ -12,7 +12,6 @@
"@github/relative-time-element": "4.4.0", "@github/relative-time-element": "4.4.0",
"@github/text-expander-element": "2.6.1", "@github/text-expander-element": "2.6.1",
"@mcaptcha/vanilla-glue": "0.1.0-alpha-3", "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
"@melloware/coloris": "0.23.0",
"@primer/octicons": "19.9.0", "@primer/octicons": "19.9.0",
"add-asset-webpack-plugin": "2.0.1", "add-asset-webpack-plugin": "2.0.1",
"ansi_up": "6.0.2", "ansi_up": "6.0.2",
@ -53,6 +52,7 @@
"toastify-js": "1.12.0", "toastify-js": "1.12.0",
"tributejs": "5.1.3", "tributejs": "5.1.3",
"uint8-to-base64": "0.2.0", "uint8-to-base64": "0.2.0",
"vanilla-colorful": "0.7.2",
"vue": "3.4.21", "vue": "3.4.21",
"vue-bar-graph": "2.0.0", "vue-bar-graph": "2.0.0",
"vue-chartjs": "5.3.0", "vue-chartjs": "5.3.0",

View file

@ -1,10 +1,6 @@
/* This is a stripped-down version of coloris's CSS tailored to our needs. It does only include
opaqua colors, and if more features like opacity are needed, the CSS needs to be extended
based on upstream: https://github.com/mdbassit/Coloris/blob/main/src/coloris.css. */
.js-color-picker-input { .js-color-picker-input {
display: flex; display: flex;
flex-wrap: wrap; position: relative;
} }
.js-color-picker-input input { .js-color-picker-input input {
@ -13,152 +9,39 @@
padding-left: 32px !important; padding-left: 32px !important;
} }
.clr-picker { .js-color-picker-input .preview-square {
display: none;
flex-wrap: wrap;
position: absolute;
width: 200px;
z-index: 1002; /* above .ui.modal which has 1001 */
border-radius: var(--border-radius);
background-color: var(--color-menu);
justify-content: flex-end;
direction: ltr;
box-shadow: 0 5px 20px var(--color-shadow);
user-select: none;
}
.clr-picker.clr-open {
display: flex;
}
.clr-gradient {
position: relative;
width: 100%;
height: 100px;
border-radius: 3px 3px 0 0;
background: linear-gradient(rgba(0,0,0,0), #000), linear-gradient(90deg, #fff, currentcolor); /* stylelint-disable-line scale-unlimited/declaration-strict-value */
cursor: pointer;
}
.clr-marker {
position: absolute;
width: 12px;
height: 12px;
margin: -6px 0 0 -6px;
border: 1px solid var(--color-white);
border-radius: 50%;
background-color: currentcolor;
cursor: pointer;
}
.clr-picker input[type="range"]::-webkit-slider-runnable-track {
width: 100%;
height: 16px;
}
.clr-picker input[type="range"]::-webkit-slider-thumb {
width: 16px;
height: 16px;
-webkit-appearance: none;
}
.clr-picker input[type="range"]::-moz-range-track {
width: 100%;
height: 16px;
border: 0;
}
.clr-picker input[type="range"]::-moz-range-thumb {
width: 16px;
height: 16px;
border: 0;
}
.clr-hue {
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 */
position: relative;
width: calc(100% - 40px);
height: 10px;
margin: 10px 20px;
border-radius: 4px;
}
.clr-hue input[type="range"] {
position: absolute;
width: calc(100% + 32px);
margin: 0;
background-color: transparent;
opacity: 0;
cursor: pointer;
appearance: none;
}
.clr-hue div {
position: absolute;
width: 16px;
height: 16px;
left: 0;
top: 50%;
transform: translate(-50%, -50%);
border: 2px solid var(--color-white);
border-radius: 50%;
background-color: currentcolor;
box-shadow: 0 0 1px var(--color-shadow);
pointer-events: none;
}
.clr-field {
flex: 1;
position: relative;
color: transparent;
}
.clr-field button {
position: absolute; position: absolute;
aspect-ratio: 1; aspect-ratio: 1;
height: 16px; height: 16px;
left: 10px; left: 10px;
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
margin: 0;
padding: 0;
border: 0;
color: inherit;
pointer-events: none;
border-radius: 2px; border-radius: 2px;
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 */ 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 */
background-position: 0 0, 4px 4px; background-position: 0 0, 4px 4px;
background-size: 8px 8px; background-size: 8px 8px;
} }
.clr-field button::after { .js-color-picker-input .preview-square::after {
content: ""; content: "";
display: block;
position: absolute; position: absolute;
width: 100%; width: 100%;
height: 100%; height: 100%;
left: 0;
top: 0;
border-radius: inherit; border-radius: inherit;
background-color: currentcolor; background-color: currentcolor;
} }
.clr-marker:focus { hex-color-picker {
outline: none; width: 180px;
height: 120px;
} }
.clr-keyboard-nav .clr-marker:focus, hex-color-picker::part(hue-pointer),
.clr-keyboard-nav .clr-hue input:focus + div, hex-color-picker::part(saturation-pointer) {
.clr-keyboard-nav .clr-alpha input:focus + div { width: 22px;
outline: none; height: 22px;
box-shadow: 0 0 2px 2px var(--color-white);
} }
.clr-picker .clr-preview, hex-color-picker::part(hue) {
.clr-picker .clr-clear, flex-basis: 16px;
.clr-picker .clr-swatches,
.clr-picker .clr-format,
.clr-picker .clr-alpha,
.clr-picker .clr-color {
display: none;
} }

View file

@ -29,6 +29,17 @@
z-index: 1; z-index: 1;
} }
/* bare theme, no styling at all, except box-shadow */
.tippy-box[data-theme="bare"] {
border: none;
box-shadow: 0 6px 18px var(--color-shadow);
}
.tippy-box[data-theme="bare"] .tippy-content {
padding: 0;
background: transparent;
}
/* tooltip theme for text tooltips */ /* tooltip theme for text tooltips */
.tippy-box[data-theme="tooltip"] { .tippy-box[data-theme="tooltip"] {

View file

@ -1,31 +1,66 @@
export async function initColorPickers(selector = '.js-color-picker-input input', opts = {}) { import {createTippy} from '../modules/tippy.js';
const inputEls = document.querySelectorAll(selector);
if (!inputEls.length) return;
const [{coloris, init}] = await Promise.all([ export async function initColorPickers() {
import(/* webpackChunkName: "colorpicker" */'@melloware/coloris'), const els = document.getElementsByClassName('js-color-picker-input');
if (!els.length) return;
await Promise.all([
import(/* webpackChunkName: "colorpicker" */'vanilla-colorful/hex-color-picker.js'),
import(/* webpackChunkName: "colorpicker" */'../../css/features/colorpicker.css'), import(/* webpackChunkName: "colorpicker" */'../../css/features/colorpicker.css'),
]); ]);
init(); for (const el of els) {
coloris({ initPicker(el);
el: selector, }
alpha: false, }
focusInput: true,
selectInput: false, function updateSquare(el, newValue) {
...opts, el.style.color = /#[0-9a-f]{6}/i.test(newValue) ? newValue : 'transparent';
}); }
for (const inputEl of inputEls) { function updatePicker(el, newValue) {
const parent = inputEl.closest('.js-color-picker-input'); el.setAttribute('color', newValue);
// prevent tabbing on the color preview `button` inside the input }
parent.querySelector('button').tabIndex = -1;
// init precolors function initPicker(el) {
for (const el of parent.querySelectorAll('.precolors .color')) { const input = el.querySelector('input');
el.addEventListener('click', (e) => {
inputEl.value = e.target.getAttribute('data-color-hex'); const square = document.createElement('div');
inputEl.dispatchEvent(new Event('input', {bubbles: true})); square.classList.add('preview-square');
}); updateSquare(square, input.value);
} el.append(square);
const picker = document.createElement('hex-color-picker');
picker.addEventListener('color-changed', (e) => {
input.value = e.detail.value;
input.focus();
updateSquare(square, e.detail.value);
});
input.addEventListener('input', (e) => {
updateSquare(square, e.target.value);
updatePicker(picker, e.target.value);
});
createTippy(input, {
trigger: 'focus click',
theme: 'bare',
hideOnClick: true,
content: picker,
placement: 'bottom-start',
interactive: true,
onShow() {
updatePicker(picker, input.value);
},
});
// init precolors
for (const colorEl of el.querySelectorAll('.precolors .color')) {
colorEl.addEventListener('click', (e) => {
const newValue = e.target.getAttribute('data-color-hex');
input.value = newValue;
input.dispatchEvent(new Event('input', {bubbles: true}));
updateSquare(square, newValue);
});
} }
} }

View file

@ -3,11 +3,12 @@ import {isDocumentFragmentOrElementNode} from '../utils/dom.js';
import {formatDatetime} from '../utils/time.js'; import {formatDatetime} from '../utils/time.js';
const visibleInstances = new Set(); const visibleInstances = new Set();
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>`;
export function createTippy(target, opts = {}) { export function createTippy(target, opts = {}) {
// the callback functions should be destructured from opts, // the callback functions should be destructured from opts,
// because we should use our own wrapper functions to handle them, do not let the user override them // because we should use our own wrapper functions to handle them, do not let the user override them
const {onHide, onShow, onDestroy, role, theme, ...other} = opts; const {onHide, onShow, onDestroy, role, theme, arrow, ...other} = opts;
const instance = tippy(target, { const instance = tippy(target, {
appendTo: document.body, appendTo: document.body,
@ -35,9 +36,9 @@ export function createTippy(target, opts = {}) {
visibleInstances.add(instance); visibleInstances.add(instance);
return onShow?.(instance); return onShow?.(instance);
}, },
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>`, arrow: arrow || (theme === 'bare' ? false : arrowSvg),
role: role || 'menu', // HTML role attribute role: role || 'menu', // HTML role attribute
theme: theme || role || 'menu', // CSS theme, either "tooltip", "menu" or "box-with-header" theme: theme || role || 'menu', // CSS theme, either "tooltip", "menu", "box-with-header" or "bare"
plugins: [followCursor], plugins: [followCursor],
...other, ...other,
}); });