mirror of
				https://github.com/go-gitea/gitea
				synced 2025-10-31 03:18:24 +00:00 
			
		
		
		
	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">
This commit is contained in:
		
							
								
								
									
										12
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										12
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							| @@ -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", | ||||||
| @@ -1290,11 +1290,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", | ||||||
| @@ -11853,6 +11848,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", | ||||||
|   | |||||||
| @@ -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", | ||||||
|   | |||||||
| @@ -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; |  | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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"] { | ||||||
|   | |||||||
| @@ -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); | ||||||
|  |     }); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -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, | ||||||
|   }); |   }); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user