-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[space-picker] First iteration (#143)
* Use `value` for actual input (as a string) and `selectedSpace` for the corresponding space object * Support custom grouping of color spaces via the `groups()` prop * Don't drop non-existent spaces; replace them with the `{ id, name: id }` objects * Expose the `valuechange` and `spacechange` events * Retarget the `input` event * In `formAssociated`, use `change` as the `changeEvent` * Add the `groupBy()` prop that expects a space and returns the name of a group the space belongs to * Related to the previous one: `groups` is now a computed prop returning an object with all the groups * Use different UI for cases with grouping and without * Add `<space-picker>` to the list of upcoming elements * Add basic docs
- Loading branch information
1 parent
0813ef7
commit 1075f15
Showing
4 changed files
with
308 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
# `<space-picker>` | ||
|
||
## Usage | ||
|
||
### Basic usage | ||
|
||
```html | ||
<space-picker value="oklab"></space-picker> | ||
``` | ||
|
||
If no color space is provided (via the `value` attribute/property), | ||
the first one will be used: | ||
|
||
```html | ||
<space-picker></space-picker> | ||
``` | ||
|
||
You can specify what color spaces to use: | ||
```html | ||
<space-picker spaces="oklch, p3, srgb" value="p3"></space-picker> | ||
``` | ||
|
||
Unknown color spaces also work: | ||
```html | ||
<space-picker spaces="bar, oklch, p3, srgb, foo" value="foo"></space-picker> | ||
``` | ||
|
||
### Grouping the color spaces | ||
|
||
You can group the color spaces the way you like by specifying the `groupBy` property. Its value is a function | ||
accepting a color space as an argument and returning the name of a group the color space should be added to: | ||
|
||
```html | ||
<space-picker id="space_picker" spaces="oklch, p3, srgb" value="p3"></space-picker> | ||
<script> | ||
space_picker.groupBy = (space) => { | ||
let isPolar = space.coords.h?.type === "angle"; | ||
return isPolar ? "Polar" : "Rectangular"; | ||
}; | ||
</script> | ||
``` | ||
### Events | ||
You can listen to the `spacechange` event to get either the id of the current color space (the `value` property) | ||
or the color space object itself (the `selectedSpace` property): | ||
```html | ||
<space-picker onspacechange="this.nextElementSibling.textContent = this.value"></space-picker> | ||
<output></output> | ||
``` | ||
### Dynamic | ||
All properties are reactive and can be set programmatically: | ||
```html | ||
<button onclick="this.nextElementSibling.value = 'oklch'">Switch to Oklch</button> | ||
<space-picker value="p3"></space-picker> | ||
``` | ||
`<space-picker>` plays nicely with other color elements: | ||
```html | ||
<label> | ||
Space: | ||
<space-picker value="oklch" onspacechange="this.parentElement.nextElementSibling.space = this.selectedSpace"></space-picker> | ||
</label> | ||
<color-picker color="oklch(60% 30% 180)"></color-picker> | ||
``` | ||
## Reference | ||
### Attributes & Properties | ||
| Attribute | Property | Property type | Default value | Description | | ||
|-----------|-----------|-------------------------------------|-----------------------------------------|-----------------------------------------------------------------------------------------------------------| | ||
| `value` | `value` | `string` | The first color space in `this.spaces`. | The current value of the picker. | | ||
| `spaces` | `spaces` | `string` | `Array<ColorSpace>` | All known color spaces. | Comma-separated list of color spaces to use. | | ||
| — | `groupBy` | `Function` | — | Function to group the color spaces. Takes a color space object as an argument and returns the group name. | | ||
### Getters | ||
These properties are read-only. | ||
| Property | Type | Description | | ||
|-----------------|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | ||
| `selectedSpace` | `ColorSpace` | Color space object corresponding to the picker current value. | | ||
| `groups` | `Object` | Object containing the color spaces grouped by the `groupBy()` function. Keys are group names, values are objects with space ids as keys, and corresponding color space objects are values. | | ||
### Events | ||
| Name | Description | | ||
|---------------|------------------------------------------------------------------------------| | ||
| `input` | Fired when the space changes due to user action. | | ||
| `change` | Fired when the space changes due to user action. | | ||
| `valuechange` | Fired when the value changes for any reason, and once during initialization. | | ||
| `spacechange` | Fired when the space changes for any reason, and once during initialization. | | ||
### Parts | ||
| Name | Description | | ||
|----------|----------------------------------| | ||
| `picker` | The internal `<select>` element. | |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
#picker { | ||
font: inherit; | ||
color: inherit; | ||
background: inherit; | ||
field-sizing: content; | ||
cursor: pointer; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,197 @@ | ||
import ColorElement from "../common/color-element.js"; | ||
|
||
const Self = class SpacePicker extends ColorElement { | ||
static tagName = "space-picker"; | ||
static url = import.meta.url; | ||
static shadowStyle = true; | ||
static shadowTemplate = `<select id="picker" part="picker"></select>`; | ||
|
||
constructor () { | ||
super(); | ||
|
||
this._el = {}; | ||
this._el.picker = this.shadowRoot.querySelector("#picker"); | ||
} | ||
|
||
connectedCallback () { | ||
super.connectedCallback?.(); | ||
this._el.picker.addEventListener("change", this); | ||
} | ||
|
||
disconnectedCallback () { | ||
super.disconnectedCallback?.(); | ||
this._el.picker.removeEventListener("change", this); | ||
} | ||
|
||
handleEvent (event) { | ||
if (event.type === "change" && event.target === this._el.picker && event.target.value !== this.value) { | ||
this.value = event.target.value; | ||
} | ||
} | ||
|
||
propChangedCallback ({name, prop, detail: change}) { | ||
if (name === "spaces") { | ||
if (!this.groups) { | ||
this._el.picker.innerHTML = Object.entries(this.spaces) | ||
.map(([id, space]) => `<option value="${id}">${space.name}</option>`) | ||
.join("\n"); | ||
} | ||
else { | ||
let groups = this.groups; | ||
|
||
// Remove empty groups | ||
groups = Object.entries(groups).filter(([type, spaces]) => { | ||
if (Object.keys(spaces).length === 0) { | ||
console.warn(`Removed empty group of color spaces with the label "${type}."`); | ||
return false; | ||
} | ||
|
||
return true; | ||
}); | ||
|
||
if (!groups.length) { | ||
console.warn("All provided groups of color spaces are empty. Falling back to default grouping."); | ||
groups = [["All spaces", this.spaces]]; | ||
} | ||
|
||
this._el.picker.innerHTML = groups.map(([type, spaces]) => ` | ||
<optgroup label="${type}"> | ||
${Object.entries(spaces) | ||
.map(([id, space]) => `<option value="${id}">${space.name}</option>`) | ||
.join("\n")} | ||
</optgroup> | ||
`).join("\n"); | ||
} | ||
|
||
this._el.picker.value = this.value; | ||
} | ||
|
||
if (name === "value") { | ||
let value = this.value; | ||
|
||
if (value !== undefined && value !== null) { | ||
if (!(value in this.spaces)) { | ||
let spaces = Object.keys(this.spaces).join(", "); | ||
console.warn(`No color space found with id = "${ value }". Choose one of the following: ${ spaces }.`); | ||
} | ||
|
||
if (this._el.picker.value !== value) { | ||
this._el.picker.value = value; | ||
} | ||
} | ||
} | ||
} | ||
|
||
static props = { | ||
value: { | ||
default () { | ||
if (this.groups) { | ||
let groups = this.groups; | ||
let firstGroup = Object.values(groups)[0]; | ||
|
||
return firstGroup && Object.keys(firstGroup)[0]; | ||
} | ||
else { | ||
return Object.keys(this.spaces)[0]; | ||
} | ||
}, | ||
}, | ||
|
||
selectedSpace: { | ||
get () { | ||
let value = this.value; | ||
if (value === undefined || value === null) { | ||
return; | ||
} | ||
|
||
return this.spaces[value]; | ||
}, | ||
}, | ||
|
||
spaces: { | ||
type: { | ||
is: Object, | ||
get values () { | ||
return Self.Color.Space; | ||
}, | ||
defaultValue: (id, index) => { | ||
try { | ||
return Self.Color.Space.get(id); | ||
} | ||
catch (e) { | ||
console.error(e); | ||
} | ||
}, | ||
}, | ||
default: () => Self.Color.spaces, | ||
convert (value) { | ||
// Replace non-existing spaces with { id, name: id } | ||
for (let id in value) { | ||
if (!value[id]) { | ||
value[id] = { id, name: id }; | ||
} | ||
} | ||
|
||
return value; | ||
}, | ||
stringify (value) { | ||
return Object.entries(value).map(([id, space]) => id).join(", "); | ||
}, | ||
}, | ||
|
||
groupBy: { | ||
type: { | ||
is: Function, | ||
arguments: ["space"], | ||
}, | ||
reflect: false, | ||
}, | ||
|
||
groups: { | ||
get () { | ||
if (!this.groupBy) { | ||
return; | ||
} | ||
|
||
let ret = {}; | ||
for (let [id, space] of Object.entries(this.spaces)) { | ||
let group = this.groupBy(space); | ||
if (group) { | ||
(ret[group] ??= {})[id] = space; | ||
} | ||
} | ||
|
||
return ret; | ||
}, | ||
}, | ||
}; | ||
|
||
static events = { | ||
change: { | ||
from () { | ||
return this._el.picker; | ||
}, | ||
}, | ||
input: { | ||
from () { | ||
return this._el.picker; | ||
}, | ||
}, | ||
valuechange: { | ||
propchange: "value", | ||
}, | ||
spacechange: { | ||
propchange: "selectedSpace", | ||
}, | ||
}; | ||
|
||
static formAssociated = { | ||
like: el => el._el.picker, | ||
role: "combobox", | ||
changeEvent: "change", | ||
}; | ||
}; | ||
|
||
Self.define(); | ||
|
||
export default Self; |