clickable has no pass-through to style button #98

Open
opened 2025-05-26 19:38:45 +00:00 by scopaf · 0 comments

createClickable creates an outer feature div and the style property is applied to that (the div). I would propose a buttonStyle property to style the button specifically while still being in line with with having both the VueFeature wrapper and a pass-through style (like the bar or infobox)

Also, I think theres a bug in the element where

                <Clickable
                    canClick={clickable.canClick}
                    onClick={clickable.onClick}
                    onHold={clickable.onClick} 
                    display={clickable.display}
                />

Seems like its meant to be:

                <Clickable
                    canClick={clickable.canClick}
                    onClick={clickable.onClick}
                    onHold={clickable.onHold} // onHold here
                    display={clickable.display}
                />

Here are the changes I made for myself:

clickable.tsx:

import Clickable from "features/clickables/Clickable.vue";
import type { BaseLayer } from "game/layers";
import type { Unsubscribe } from "nanoevents";
import { MaybeGetter, processGetter } from "util/computed";
import { createLazyProxy } from "util/proxies";
import {
    isJSXElement,
    render,
    Renderable,
    VueFeature,
    vueFeatureMixin,
    VueFeatureOptions
} from "util/vue";
import { computed, MaybeRef, MaybeRefOrGetter, unref, ref, Ref, CSSProperties } from "vue";

/** A symbol used to identify {@link Clickable} features. */
export const ClickableType = Symbol("Clickable");

/**
 * An object that configures a {@link Clickable}.
 */
export interface ClickableOptions extends VueFeatureOptions {
    /** Whether or not the clickable may be clicked. */
    canClick?: MaybeRefOrGetter<boolean>;
    /** The display to use for this clickable. */
    display?:
        | MaybeGetter<Renderable>
        | {
              /** A header to appear at the top of the display. */
              title?: MaybeGetter<Renderable>;
              /** The main text that appears in the display. */
              description: MaybeGetter<Renderable>;
          };
    /** A function that is called when the clickable is clicked. */
    onClick?: (e?: MouseEvent | TouchEvent) => void;
    /** A function that is called when the clickable is held down. */
    onHold?: VoidFunction;
		/** CSS to apply specifically to the button */
		buttonStyle?: MaybeRefOrGetter<CSSProperties>;
}

/** An object that represents a feature that can be clicked or held down. */
export interface Clickable extends VueFeature {
    /** A function that is called when the clickable is clicked. */
    onClick?: (e?: MouseEvent | TouchEvent) => void;
    /** A function that is called when the clickable is held down. */
    onHold?: VoidFunction;
		/** Tells if clickable is currently held down */
		isHolding: Ref<boolean>;
    /** Whether or not the clickable may be clicked. */
    canClick: MaybeRef<boolean>;
		/** CSS to apply specifically to the button */
		buttonStyle?: MaybeRef<CSSProperties>;
    /** The display to use for this clickable. */
    display?: MaybeGetter<Renderable>;
    /** A symbol that helps identify features of the same type. */
    type: typeof ClickableType;
}

/**
 * Lazily creates a clickable with the given options.
 * @param optionsFunc Clickable options.
 */
export function createClickable<T extends ClickableOptions>(optionsFunc?: () => T) {
    return createLazyProxy(() => {
        const options = optionsFunc?.() ?? ({} as T);
        const { canClick, display: _display, onClick: onClick, onHold: onHold, buttonStyle, ...props } = options;

        let display: MaybeGetter<Renderable> | undefined = undefined;
        if (typeof _display === "object" && !isJSXElement(_display)) {
            display = () => (
                <span>
                    {_display.title != null ? (
                        <div>
                            {render(_display.title, el => (
                                <h3>{el}</h3>
                            ))}
                        </div>
                    ) : null}
                    {render(_display.description, el => (
                        <div>{el}</div>
                    ))}
                </span>
            );
        } else if (_display != null) {
            display = _display;
        }

        const clickable = {
            type: ClickableType,
            ...(props as Omit<typeof props, keyof VueFeature | keyof ClickableOptions>),
            ...vueFeatureMixin("clickable", options, () => (
                <Clickable
                    canClick={clickable.canClick}
                    onClick={clickable.onClick}
                    onHold={clickable.onHold}
										isHolding={clickable.isHolding}
										buttonStyle={clickable.buttonStyle}
                    display={clickable.display}
                />
            )),
            canClick: processGetter(canClick) ?? true,
						buttonStyle: processGetter(buttonStyle),
            display,
            onClick:
                onClick == null
                    ? undefined
                    : function (e?: MouseEvent | TouchEvent) {
                          if (unref(clickable.canClick) !== false) {
                              onClick.call(clickable, e);
                          }
                      },
            onHold:
                onHold == null
                    ? undefined
                    : function () {
                          if (unref(clickable.canClick) !== false) {
                              onHold.call(clickable);
                          }
                      },
											isHolding: ref(false)

        } satisfies Clickable;

        return clickable;
    });
}

/**
 * Utility to auto click a clickable whenever it can be.
 * @param layer The layer the clickable is apart of
 * @param clickable The clicker to click automatically
 * @param autoActive Whether or not the clickable should currently be auto-clicking
 */
export function setupAutoClick(
    layer: BaseLayer,
    clickable: Clickable,
    autoActive: MaybeRefOrGetter<boolean> = true
): Unsubscribe {
    const isActive: MaybeRef<boolean> =
        typeof autoActive === "function" ? computed(autoActive) : autoActive;
    return layer.on("update", () => {
        if (unref(isActive) && unref<boolean>(clickable.canClick)) {
            clickable.onClick?.();
        }
    });
}

... Clickable.vue

<template>
    <button
        @click="e => emits('click', e)"
        @mousedown="start"
        @mouseleave="stop"
        @mouseup="stop"
        @touchstart.passive="start"
        @touchend.passive="stop"
        @touchcancel.passive="stop"
        :class="{
            clickable: true,
            can: unref(canClick),
            locked: !unref(canClick)
        }"
				:style="unref(buttonStyle)"
        :disabled="!unref(canClick)"
    >
        <Component />
    </button>
</template>

<script setup lang="tsx">
import "components/common/features.css";
import { MaybeGetter } from "util/computed";
import {
    render,
    Renderable,
    setupHoldToClick
} from "util/vue";
import type { Component, MaybeRef, ref, Ref, CSSProperties } from "vue";
import { unref, computed } from "vue";

const props = defineProps<{
    canClick: MaybeRef<boolean>;
    display?: MaybeGetter<Renderable>;
		isHolding?: Ref<boolean>;
		buttonStyle?: MaybeRef<CSSProperties>
}>();

const emits = defineEmits<{
    (e: "click", event?: MouseEvent | TouchEvent): void;
    (e: "hold"): void;
}>();

const Component = () => props.display == null ? <></> : render(props.display);

const { start, stop } = setupHoldToClick(() => emits("hold"), props.isHolding);
</script>

<style scoped>
.clickable {
    min-height: 120px;
    width: 120px;
    font-size: 10px;
}

.clickable > * {
    pointer-events: none;
}
</style>
createClickable creates an outer feature div and the style property is applied to that (the div). I would propose a buttonStyle property to style the button specifically while still being in line with with having both the VueFeature wrapper and a pass-through style (like the bar or infobox) Also, I think theres a bug in the <Clickable> element where ```ts <Clickable canClick={clickable.canClick} onClick={clickable.onClick} onHold={clickable.onClick} display={clickable.display} /> ``` Seems like its meant to be: ```ts <Clickable canClick={clickable.canClick} onClick={clickable.onClick} onHold={clickable.onHold} // onHold here display={clickable.display} /> ``` Here are the changes I made for myself: clickable.tsx: ```ts import Clickable from "features/clickables/Clickable.vue"; import type { BaseLayer } from "game/layers"; import type { Unsubscribe } from "nanoevents"; import { MaybeGetter, processGetter } from "util/computed"; import { createLazyProxy } from "util/proxies"; import { isJSXElement, render, Renderable, VueFeature, vueFeatureMixin, VueFeatureOptions } from "util/vue"; import { computed, MaybeRef, MaybeRefOrGetter, unref, ref, Ref, CSSProperties } from "vue"; /** A symbol used to identify {@link Clickable} features. */ export const ClickableType = Symbol("Clickable"); /** * An object that configures a {@link Clickable}. */ export interface ClickableOptions extends VueFeatureOptions { /** Whether or not the clickable may be clicked. */ canClick?: MaybeRefOrGetter<boolean>; /** The display to use for this clickable. */ display?: | MaybeGetter<Renderable> | { /** A header to appear at the top of the display. */ title?: MaybeGetter<Renderable>; /** The main text that appears in the display. */ description: MaybeGetter<Renderable>; }; /** A function that is called when the clickable is clicked. */ onClick?: (e?: MouseEvent | TouchEvent) => void; /** A function that is called when the clickable is held down. */ onHold?: VoidFunction; /** CSS to apply specifically to the button */ buttonStyle?: MaybeRefOrGetter<CSSProperties>; } /** An object that represents a feature that can be clicked or held down. */ export interface Clickable extends VueFeature { /** A function that is called when the clickable is clicked. */ onClick?: (e?: MouseEvent | TouchEvent) => void; /** A function that is called when the clickable is held down. */ onHold?: VoidFunction; /** Tells if clickable is currently held down */ isHolding: Ref<boolean>; /** Whether or not the clickable may be clicked. */ canClick: MaybeRef<boolean>; /** CSS to apply specifically to the button */ buttonStyle?: MaybeRef<CSSProperties>; /** The display to use for this clickable. */ display?: MaybeGetter<Renderable>; /** A symbol that helps identify features of the same type. */ type: typeof ClickableType; } /** * Lazily creates a clickable with the given options. * @param optionsFunc Clickable options. */ export function createClickable<T extends ClickableOptions>(optionsFunc?: () => T) { return createLazyProxy(() => { const options = optionsFunc?.() ?? ({} as T); const { canClick, display: _display, onClick: onClick, onHold: onHold, buttonStyle, ...props } = options; let display: MaybeGetter<Renderable> | undefined = undefined; if (typeof _display === "object" && !isJSXElement(_display)) { display = () => ( <span> {_display.title != null ? ( <div> {render(_display.title, el => ( <h3>{el}</h3> ))} </div> ) : null} {render(_display.description, el => ( <div>{el}</div> ))} </span> ); } else if (_display != null) { display = _display; } const clickable = { type: ClickableType, ...(props as Omit<typeof props, keyof VueFeature | keyof ClickableOptions>), ...vueFeatureMixin("clickable", options, () => ( <Clickable canClick={clickable.canClick} onClick={clickable.onClick} onHold={clickable.onHold} isHolding={clickable.isHolding} buttonStyle={clickable.buttonStyle} display={clickable.display} /> )), canClick: processGetter(canClick) ?? true, buttonStyle: processGetter(buttonStyle), display, onClick: onClick == null ? undefined : function (e?: MouseEvent | TouchEvent) { if (unref(clickable.canClick) !== false) { onClick.call(clickable, e); } }, onHold: onHold == null ? undefined : function () { if (unref(clickable.canClick) !== false) { onHold.call(clickable); } }, isHolding: ref(false) } satisfies Clickable; return clickable; }); } /** * Utility to auto click a clickable whenever it can be. * @param layer The layer the clickable is apart of * @param clickable The clicker to click automatically * @param autoActive Whether or not the clickable should currently be auto-clicking */ export function setupAutoClick( layer: BaseLayer, clickable: Clickable, autoActive: MaybeRefOrGetter<boolean> = true ): Unsubscribe { const isActive: MaybeRef<boolean> = typeof autoActive === "function" ? computed(autoActive) : autoActive; return layer.on("update", () => { if (unref(isActive) && unref<boolean>(clickable.canClick)) { clickable.onClick?.(); } }); } ``` ... Clickable.vue ```ts <template> <button @click="e => emits('click', e)" @mousedown="start" @mouseleave="stop" @mouseup="stop" @touchstart.passive="start" @touchend.passive="stop" @touchcancel.passive="stop" :class="{ clickable: true, can: unref(canClick), locked: !unref(canClick) }" :style="unref(buttonStyle)" :disabled="!unref(canClick)" > <Component /> </button> </template> <script setup lang="tsx"> import "components/common/features.css"; import { MaybeGetter } from "util/computed"; import { render, Renderable, setupHoldToClick } from "util/vue"; import type { Component, MaybeRef, ref, Ref, CSSProperties } from "vue"; import { unref, computed } from "vue"; const props = defineProps<{ canClick: MaybeRef<boolean>; display?: MaybeGetter<Renderable>; isHolding?: Ref<boolean>; buttonStyle?: MaybeRef<CSSProperties> }>(); const emits = defineEmits<{ (e: "click", event?: MouseEvent | TouchEvent): void; (e: "hold"): void; }>(); const Component = () => props.display == null ? <></> : render(props.display); const { start, stop } = setupHoldToClick(() => emits("hold"), props.isHolding); </script> <style scoped> .clickable { min-height: 120px; width: 120px; font-size: 10px; } .clickable > * { pointer-events: none; } </style> ```
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: profectus/Profectus#98
No description provided.