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:
		
					parent
					
						
							
								2adc3a45fb
							
						
					
				
			
			
				commit
				
					
						a53a94e1c2
					
				
			
		
					 6 changed files with 94 additions and 164 deletions
				
			
		
							
								
								
									
										12
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										12
									
								
								package-lock.json
									
										
									
										generated
									
									
									
								
							| 
						 | 
				
			
			@ -13,7 +13,6 @@
 | 
			
		|||
        "@github/relative-time-element": "4.4.0",
 | 
			
		||||
        "@github/text-expander-element": "2.6.1",
 | 
			
		||||
        "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
 | 
			
		||||
        "@melloware/coloris": "0.23.0",
 | 
			
		||||
        "@primer/octicons": "19.9.0",
 | 
			
		||||
        "add-asset-webpack-plugin": "2.0.1",
 | 
			
		||||
        "ansi_up": "6.0.2",
 | 
			
		||||
| 
						 | 
				
			
			@ -54,6 +53,7 @@
 | 
			
		|||
        "toastify-js": "1.12.0",
 | 
			
		||||
        "tributejs": "5.1.3",
 | 
			
		||||
        "uint8-to-base64": "0.2.0",
 | 
			
		||||
        "vanilla-colorful": "0.7.2",
 | 
			
		||||
        "vue": "3.4.21",
 | 
			
		||||
        "vue-bar-graph": "2.0.0",
 | 
			
		||||
        "vue-chartjs": "5.3.0",
 | 
			
		||||
| 
						 | 
				
			
			@ -1291,11 +1291,6 @@
 | 
			
		|||
        "@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": {
 | 
			
		||||
      "version": "2.1.5",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
 | 
			
		||||
| 
						 | 
				
			
			@ -12010,6 +12005,11 @@
 | 
			
		|||
        "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": {
 | 
			
		||||
      "version": "5.2.6",
 | 
			
		||||
      "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.6.tgz",
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -12,7 +12,6 @@
 | 
			
		|||
    "@github/relative-time-element": "4.4.0",
 | 
			
		||||
    "@github/text-expander-element": "2.6.1",
 | 
			
		||||
    "@mcaptcha/vanilla-glue": "0.1.0-alpha-3",
 | 
			
		||||
    "@melloware/coloris": "0.23.0",
 | 
			
		||||
    "@primer/octicons": "19.9.0",
 | 
			
		||||
    "add-asset-webpack-plugin": "2.0.1",
 | 
			
		||||
    "ansi_up": "6.0.2",
 | 
			
		||||
| 
						 | 
				
			
			@ -53,6 +52,7 @@
 | 
			
		|||
    "toastify-js": "1.12.0",
 | 
			
		||||
    "tributejs": "5.1.3",
 | 
			
		||||
    "uint8-to-base64": "0.2.0",
 | 
			
		||||
    "vanilla-colorful": "0.7.2",
 | 
			
		||||
    "vue": "3.4.21",
 | 
			
		||||
    "vue-bar-graph": "2.0.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 {
 | 
			
		||||
  display: flex;
 | 
			
		||||
  flex-wrap: wrap;
 | 
			
		||||
  position: relative;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.js-color-picker-input input {
 | 
			
		||||
| 
						 | 
				
			
			@ -13,152 +9,39 @@
 | 
			
		|||
  padding-left: 32px !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.clr-picker {
 | 
			
		||||
  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 {
 | 
			
		||||
.js-color-picker-input .preview-square {
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  aspect-ratio: 1;
 | 
			
		||||
  height: 16px;
 | 
			
		||||
  left: 10px;
 | 
			
		||||
  top: 50%;
 | 
			
		||||
  transform: translateY(-50%);
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  padding: 0;
 | 
			
		||||
  border: 0;
 | 
			
		||||
  color: inherit;
 | 
			
		||||
  pointer-events: none;
 | 
			
		||||
  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-position: 0 0, 4px 4px;
 | 
			
		||||
  background-size: 8px 8px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.clr-field button::after {
 | 
			
		||||
.js-color-picker-input .preview-square::after {
 | 
			
		||||
  content: "";
 | 
			
		||||
  display: block;
 | 
			
		||||
  position: absolute;
 | 
			
		||||
  width: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  left: 0;
 | 
			
		||||
  top: 0;
 | 
			
		||||
  border-radius: inherit;
 | 
			
		||||
  background-color: currentcolor;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.clr-marker:focus {
 | 
			
		||||
  outline: none;
 | 
			
		||||
hex-color-picker {
 | 
			
		||||
  width: 180px;
 | 
			
		||||
  height: 120px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.clr-keyboard-nav .clr-marker:focus,
 | 
			
		||||
.clr-keyboard-nav .clr-hue input:focus + div,
 | 
			
		||||
.clr-keyboard-nav .clr-alpha input:focus + div {
 | 
			
		||||
  outline: none;
 | 
			
		||||
  box-shadow: 0 0 2px 2px var(--color-white);
 | 
			
		||||
hex-color-picker::part(hue-pointer),
 | 
			
		||||
hex-color-picker::part(saturation-pointer) {
 | 
			
		||||
  width: 22px;
 | 
			
		||||
  height: 22px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.clr-picker .clr-preview,
 | 
			
		||||
.clr-picker .clr-clear,
 | 
			
		||||
.clr-picker .clr-swatches,
 | 
			
		||||
.clr-picker .clr-format,
 | 
			
		||||
.clr-picker .clr-alpha,
 | 
			
		||||
.clr-picker .clr-color {
 | 
			
		||||
  display: none;
 | 
			
		||||
hex-color-picker::part(hue) {
 | 
			
		||||
  flex-basis: 16px;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -29,6 +29,17 @@
 | 
			
		|||
  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 */
 | 
			
		||||
 | 
			
		||||
.tippy-box[data-theme="tooltip"] {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,31 +1,66 @@
 | 
			
		|||
export async function initColorPickers(selector = '.js-color-picker-input input', opts = {}) {
 | 
			
		||||
  const inputEls = document.querySelectorAll(selector);
 | 
			
		||||
  if (!inputEls.length) return;
 | 
			
		||||
import {createTippy} from '../modules/tippy.js';
 | 
			
		||||
 | 
			
		||||
  const [{coloris, init}] = await Promise.all([
 | 
			
		||||
    import(/* webpackChunkName: "colorpicker" */'@melloware/coloris'),
 | 
			
		||||
export async function initColorPickers() {
 | 
			
		||||
  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'),
 | 
			
		||||
  ]);
 | 
			
		||||
 | 
			
		||||
  init();
 | 
			
		||||
  coloris({
 | 
			
		||||
    el: selector,
 | 
			
		||||
    alpha: false,
 | 
			
		||||
    focusInput: true,
 | 
			
		||||
    selectInput: false,
 | 
			
		||||
    ...opts,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  for (const inputEl of inputEls) {
 | 
			
		||||
    const parent = inputEl.closest('.js-color-picker-input');
 | 
			
		||||
    // prevent tabbing on the color preview `button` inside the input
 | 
			
		||||
    parent.querySelector('button').tabIndex = -1;
 | 
			
		||||
    // init precolors
 | 
			
		||||
    for (const el of parent.querySelectorAll('.precolors .color')) {
 | 
			
		||||
      el.addEventListener('click', (e) => {
 | 
			
		||||
        inputEl.value = e.target.getAttribute('data-color-hex');
 | 
			
		||||
        inputEl.dispatchEvent(new Event('input', {bubbles: true}));
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  for (const el of els) {
 | 
			
		||||
    initPicker(el);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function updateSquare(el, newValue) {
 | 
			
		||||
  el.style.color = /#[0-9a-f]{6}/i.test(newValue) ? newValue : 'transparent';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function updatePicker(el, newValue) {
 | 
			
		||||
  el.setAttribute('color', newValue);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function initPicker(el) {
 | 
			
		||||
  const input = el.querySelector('input');
 | 
			
		||||
 | 
			
		||||
  const square = document.createElement('div');
 | 
			
		||||
  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';
 | 
			
		||||
 | 
			
		||||
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 = {}) {
 | 
			
		||||
  // 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
 | 
			
		||||
  const {onHide, onShow, onDestroy, role, theme, ...other} = opts;
 | 
			
		||||
  const {onHide, onShow, onDestroy, role, theme, arrow, ...other} = opts;
 | 
			
		||||
 | 
			
		||||
  const instance = tippy(target, {
 | 
			
		||||
    appendTo: document.body,
 | 
			
		||||
| 
						 | 
				
			
			@ -35,9 +36,9 @@ export function createTippy(target, opts = {}) {
 | 
			
		|||
      visibleInstances.add(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
 | 
			
		||||
    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],
 | 
			
		||||
    ...other,
 | 
			
		||||
  });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue