/**
* 3D Foundation Project
* Copyright 2025 Smithsonian Institution
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Color from "@ff/core/Color";
import Property from "@ff/graph/Property";
import { customElement, property, PropertyValues, html } from "@ff/ui/CustomElement";
import "@ff/ui/Button";
import "@ff/ui/ColorEdit";
import Notification from "@ff/ui/Notification";
import { focusTrap, getFocusableElements } from "client/utils/focusHelpers";
import PropertyBase from "./PropertyBase";
////////////////////////////////////////////////////////////////////////////////
@customElement("sv-property-color")
export default class PropertyColor extends PropertyBase
{
type = "number";
@property({attribute: false, type: Boolean})
pickerActive :boolean = false;
@property({type: Boolean})
compact :boolean = false;
@property({type: Boolean})
floating :boolean = true;
protected color: Color = new Color();
get alphaEnabled(){
return this.property.elementCount === 4;
}
constructor()
{
super();
}
protected firstConnected()
{
super.firstConnected();
this.classList.add("sv-property-color");
}
protected disconnected()
{
this.pickerActive = false;
}
protected update(changedProperties: PropertyValues): void
{
if (changedProperties.has("property")) {
if (this.property.type !== "number" || 4 < this.property.elementCount ||this.property.elementCount < 3) {
throw new Error(`not a color property: '${this.property.path}'`);
}
const property = changedProperties.get("property") as Property;
if (property) {
property.off("value", this.onPropertyChange, this);
}
if (this.property) {
this.property.on("value", this.onPropertyChange, this);
this.color.fromArray(this.property.value);
}
}
if(changedProperties.has("pickerActive")){
if(this.pickerActive){
this.setPickerFocus();
document.addEventListener("pointerdown", this.onPointerDown, { capture: true, passive: true });
}else{
document.removeEventListener("pointerdown", this.onPointerDown, {capture: true});
}
}
super.update(changedProperties);
}
protected render()
{
const property = this.property;
const name = this.name || property.name;
const color = this.color.toString(this.alphaEnabled);
const colorEdit = html`this.onKeyDown(e)} @change=${this.onColorChange}>`;
const popupColorEdit = html`${colorEdit}`
return html`
${this.compact?null:html`{
try{
this.color.setString(ev.target.value);
this.onColorChange();
ev.target.setCustomValidity("");
}catch(e){
ev.target.setCustomValidity(e.message);
Notification.show(`Not a valid color: ${ev.target.value}`, "warning", 1000);
}
}}
>`}
${this.pickerActive ? (this.floating ? popupColorEdit : colorEdit) : null}
`;
}
protected async setPickerFocus()
{
await this.updateComplete;
const container = this.getElementsByTagName("ff-color-edit").item(0) as HTMLElement;
(getFocusableElements(container)[0] as HTMLElement).focus();
}
protected onButtonClick(event: Event)
{
this.pickerActive = !this.pickerActive;
}
protected onColorChange()
{
this.property.setValue( (this.alphaEnabled)? this.color.toRGBAArray() : this.color.toRGBArray() );
}
protected onPropertyChange(value: number[])
{
this.color.fromArray(value);
this.requestUpdate();
}
// if color picker is active and user clicks outside, close picker
protected onPointerDown = (event: PointerEvent) => {
if (!this.pickerActive) {
return;
}
if (event.composedPath()[0] instanceof Node && this.contains(event.composedPath()[0] as Node)) {
return;
}
this.pickerActive = false;
}
protected onKeyDown(e: KeyboardEvent)
{
if (e.code === "Escape" || e.code === "Enter") {
e.preventDefault();
e.stopPropagation();
this.pickerActive = false;
(this.getElementsByTagName("ff-button")[0] as HTMLElement).focus();
}
else if(e.code === "Tab") {
const element = this.getElementsByTagName("ff-color-edit")[0] as HTMLElement;
focusTrap(getFocusableElements(element) as HTMLElement[], e);
}
}
}