Skip to content

Commit

Permalink
feat(clock): implement clock without fuzzyClock
Browse files Browse the repository at this point in the history
  • Loading branch information
jxn-30 committed Jan 18, 2025
1 parent fe69a9b commit 621471c
Show file tree
Hide file tree
Showing 16 changed files with 719 additions and 31 deletions.
83 changes: 57 additions & 26 deletions src/_lib/Components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -269,17 +269,27 @@ export const Select = <
// endregion

// region Slider
export type SliderComponent = GenericSetting<
export type SliderComponent<
Group extends FeatureGroupID,
Feat extends FeatureID<Group>,
> = GenericSetting<
number,
HTMLDivElement,
Slider<Group, Feat>,
{
min: number;
max: number;
step: number;
labels: number | string[];
}
>;
type Slider = SliderComponent['element'];
type Slider<
Group extends FeatureGroupID,
Feat extends FeatureID<Group>,
> = HTMLDivElement & {
applyTranslations: (
translations: SettingTranslations<Group, Feat>['labels']
) => void;
};

/**
* creates a Slider component
Expand All @@ -292,14 +302,17 @@ type Slider = SliderComponent['element'];
* @param attributes.labels - the amount of labels to show below the slider or an array of label keys
* @returns the switch element
*/
export const Slider = ({
export const Slider = <
Group extends FeatureGroupID,
Feat extends FeatureID<Group>,
>({
id,
value,
min,
max,
step = 1,
labels = (max - min + 1) / step,
}: SliderComponent['props']): Slider => {
}: SliderComponent<Group, Feat>['props']): Slider<Group, Feat> => {
const datalistId = `${id}-datalist`;

const Input = (
Expand Down Expand Up @@ -349,26 +362,6 @@ export const Slider = ({
<datalist style={{ '--label-count': labelCount }}></datalist>
);

for (
let currentStep = min;
currentStep <= max;
currentStep += (max - min) / (labelCount - 1)
) {
if (fixLabels) {
valueToLabel.set(
currentStep,
// TODO: Translations
`settings.${id}.labels.${labels.shift()}`
);
}

const title = valueToLabel.get(currentStep) ?? stringify(currentStep);

labelDatalist.append(
<option value={currentStep} title={title} label={title}></option>
);
}

const Slider = (
<div
className={classNames(
Expand All @@ -381,7 +374,7 @@ export const Slider = ({
{datalist}
{labelDatalist}
</div>
) as Slider;
) as Slider<Group, Feat>;

Object.defineProperty(Slider, 'value', {
/**
Expand Down Expand Up @@ -418,6 +411,44 @@ export const Slider = ({
},
});

Object.defineProperty(Slider, 'applyTranslations', {
/**
* Creates the labels with their respective translations
* @param translations - the label translations to use
*/
value: (translations: SettingTranslations<Group, Feat>['labels']) => {
if (!translations) return;

for (
let currentStep = min;
currentStep <= max;
currentStep += (max - min) / (labelCount - 1)
) {
if (fixLabels) {
const label = labels.shift();
valueToLabel.set(
currentStep,
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-call
translations[label]?.() ?? label
);
}

const title =
valueToLabel.get(currentStep) ?? stringify(currentStep);

labelDatalist.append(
<option
value={currentStep}
title={title}
label={title}
></option>
);
}

Output.textContent = valueToLabel.get(value) ?? stringify(value);
},
});

// we want to update the output element whenever the slider is moved
Input.addEventListener('input', setOutput);

Expand Down
167 changes: 167 additions & 0 deletions src/_lib/Marquee.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
import classNames from 'classnames';
import { debounce } from '@/helpers';
import { ready } from '@/DOM';
import style from '../style/marquee.module.scss';

type Position = 'prepend';

/**
* A class that creates and manages a marquee
*/
export default class Marquee {
readonly #parentSelector: string;
readonly #parentPosition: Position;
#parent: HTMLElement | null;
#getMaxWidth: () => number;
readonly #span = document.createElement('span');
readonly #cloneSpan = document.createElement('span');
readonly #content = (
<div
className={classNames([style.marquee, 'd-flex align-items-center'])}
>
{this.#span}
{this.#cloneSpan}
</div>
) as HTMLDivElement;
readonly #observer = new ResizeObserver(
debounce(() => this.#recalculate())
);
readonly #observedElements = new Set<Element>();
readonly #minWidthPlaceholder = (
<div class={style.marqueeMinWidthPlaceholder}></div>
);
readonly #contentClones = new Map<Element, Element>();

/**
* Initializes the Marquee
* @param parentSelector - a selector that describes the element the marquee will be attached to
* @param position - where to put the marquee in relation to the parent
*/
constructor(parentSelector: string, position: Position) {
this.#parentSelector = parentSelector;
this.#parentPosition = position;

this.#observer.observe(this.#span);
}

/**
* Puts the marquee at the designated position to its designated parent
*/
async #put() {
await ready();
this.#parent = document.querySelector<HTMLElement>(
this.#parentSelector
);
if (!this.#parent) {
throw new Error(
`Could'nt find a parent with selector ${this.#parentSelector} for marquee`
);
}
this.#parent[this.#parentPosition](
this.#minWidthPlaceholder,
this.#content
);
this.#recalculate();
this.#observer.observe(this.#parent);
this.#observedElements.forEach(el => this.#observer.observe(el));
window.dispatchEvent(new Event('resize'));
}

/**
* Removes the marquee from the parent
*/
#remove() {
this.#parent = null;
this.#content.remove();
this.#minWidthPlaceholder.remove();
this.#observer.disconnect();
}

/**
* Override the default function to calculate the maximum available width
* @param maxWidthFn - the new function to use
*/
setMaxWidthFunction(maxWidthFn: () => number) {
this.#getMaxWidth = maxWidthFn;
this.#recalculate();
}

/**
* Get the current maximum available width;
* @returns the current maximum available width in pixels but without unit
*/
get #maxWidth() {
if (!this.#parent) return 0;
return (
this.#getMaxWidth?.() ??
parseFloat(getComputedStyle(this.#parent).width)
);
}

/**
* Adds new elements to the marquee
* @param els - all elements that shall be added
* @returns a list that contains the added elements and their respective clones
*/
add(...els: Element[]) {
const clones = els.map(el => {
const clone = el.cloneNode(true) as typeof el;
this.#span.append(el);
this.#cloneSpan.append(clone);
this.#contentClones.set(el, clone);
return [el, clone];
});

if (!this.#parent) void this.#put();

this.#recalculate();

return clones;
}

/**
* Remove an element and its clone from the marquee
* @param el - the element that is to be removed
*/
remove(el: Element) {
el.remove();
this.#contentClones.get(el)?.remove();
this.#contentClones.delete(el);
this.#recalculate();

if (this.#contentClones.size === 0) this.#remove();
}

/**
* Observe this element for resizing to trigger recalculation
* @param el - the element to observe
*/
observe(el?: HTMLElement) {
if (!el) return;
if (this.#parent) this.#observer.observe(el);
this.#observedElements.add(el);
}

/**
* Recalculate and set the maximum available width and position
*/
#recalculate() {
const maxWidth = Math.floor(this.#maxWidth);
const textWidth = Math.round(
parseFloat(getComputedStyle(this.#span).width)
);
this.#content.style.setProperty('--max-width', `${maxWidth}px`);
this.#content.style.setProperty('--text-width', textWidth);

if (this.#parent) {
this.#content.style.setProperty(
'--parent-left',
`${Math.round(this.#parent.getBoundingClientRect().left)}px`
);
}

if (textWidth > maxWidth) {
this.#span.dataset.rolling = '';
} else delete this.#span.dataset.rolling;
}
}
4 changes: 3 additions & 1 deletion src/_lib/Setting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ export type SettingTranslations<
Feat extends FeatureID<Group>,
T extends
FeatureTranslations<Group>[Feat] = FeatureTranslations<Group>[Feat],
> = 'settings' extends keyof T ? T['settings'] : Record<string, never>;
> =
'settings' extends keyof T ? T['settings'][keyof T['settings']]
: Record<string, never>;

type ComparisonCondition = '==' | '!=' | '>' | '<';

Expand Down
12 changes: 9 additions & 3 deletions src/_lib/Settings/SliderSetting.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ export class SliderSetting<
Group,
Feat,
number,
SliderComponent['params'],
SliderComponent
SliderComponent<Group, Feat>['params'],
SliderComponent<Group, Feat>
> {
/**
* Constructor
Expand All @@ -26,9 +26,15 @@ export class SliderSetting<
constructor(
id: string,
defaultValue: number,
params: SliderComponent['params']
params: SliderComponent<Group, Feat>['params']
) {
super(id, defaultValue, Slider, params);

void this.callWhenReady(() =>
// We really need to make translations more typesafe
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
this.formControl.applyTranslations(this.Translation.labels)
);
}

/**
Expand Down
34 changes: 34 additions & 0 deletions src/_lib/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,40 @@ export const debounce = <Args extends unknown[]>(
};
};

/**
* Animate things in a specific interval.
* @param delay - the delay in ms between two callback calls
* @param callback - the function to execute every delay ms
* @param runImmediate - wether to run the callback immediately
* @returns a method to cancel / abort the animation
*/
export const animate = <Args extends unknown[]>(
delay: number,
callback: (...args: Args) => void,
runImmediate = false
) => {
if (runImmediate) callback();

let last = 0;
/**
* call the callback if enough time has passed
* @param now - a time identifier
*/
const intervalCallback = now => {
currentId = requestAnimationFrame(intervalCallback);

const elapsed = now - last;

if (elapsed >= delay) {
last = now - (elapsed % delay);
callback();
}
};
let currentId = requestAnimationFrame(intervalCallback);

return () => cancelAnimationFrame(currentId);
};

/**
* Checks if the user is logged in by checking the current URL.
*/
Expand Down
Loading

0 comments on commit 621471c

Please sign in to comment.