/* * This file is part of TREB. * * TREB is free software: you can redistribute it and/or modify it under the * terms of the GNU General Public License as published by the Free Software * Foundation, either version 3 of the License, or (at your option) any * later version. * * TREB is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License along * with TREB. If not, see . * * Copyright 2022-2026 trebco, llc. * info@treb.app * */ import { DOMContext } from 'treb-base-types'; import { NumberFormat, NumberFormatCache, ValueParser } from 'treb-format'; import { ValueType } from 'treb-base-types'; import { EventSource } from 'treb-utils'; export interface ScaleEvent { type: 'scale'; value: number; keep_focus?: boolean; } /** * updated scale control, broken out. this is based on what we did for * project planning, which worked out rather nicely. */ export class ScaleControl extends EventSource { private input: HTMLInputElement; private slider: HTMLInputElement; private scale = 0; private format: NumberFormat; private timeout = 0; public constructor(public container: HTMLElement) { super(); this.format = NumberFormatCache.Get('0.0'); const DOM = DOMContext.GetInstance(container.ownerDocument); this.input = DOM.Create('input', 'treb-scale-input', container, { events: { // is this for some x-browser issue? or did we just not // know which event to use and this is old junk? keypress: (event) => { switch (event.key) { case 'ArrowUp': case 'ArrowDown': event.stopPropagation(); event.preventDefault(); console.info('mark?'); break; } }, keydown: (event) => { switch (event.key) { case 'Enter': this.input.blur(); break; case 'ArrowUp': this.Tick(-1); break; case 'ArrowDown': this.Tick(1); break; case 'Escape': this.input.value = this.format.Format(this.scale) + '%'; this.input.blur(); break; default: return; } event.stopPropagation(); event.preventDefault(); }, // select text on click focusin: () => this.input.select(), change: () => { // what we're doing here is a little unusual. we always treat // the value as a percent, even if there's no percent sign. // for that to work, if there is a percent sign, we need to remove // it before we continue. then try to parse as a number. let text = this.input.value; text = text.replace(/%/g, ''); const value = ValueParser.TryParse(text); if (value.type === ValueType.number) { this.UpdateScale(Number(value.value), true); } else { this.input.value = this.format.Format(this.scale) + '%'; } }, } }); const popup = DOM.Div('treb-slider-container', container); this.slider = DOM.Create('input', undefined, popup, { attrs: { type: 'range', min: '50', max: '200', value: '100', step: '2.5', }, events: { input: () => this.UpdateScale(Number(this.slider.value), true), } }); // we're overriding scroll behavior here, but it's a tiny panel. // if we explicitly mark passive:false we can avoid the console // warning (I think) container.addEventListener('wheel', (event: WheelEvent) => { event.stopPropagation(); event.preventDefault(); this.Tick(event.deltaY) }, { passive: false }); } public Tick(value: number): void { // normalize const scale = Math.round(this.scale / 2.5) * 2.5; if (value > 0) { this.UpdateScale(scale - 2.5, true, true); } else if (value < 0){ this.UpdateScale(scale + 2.5, true, true); } } public UpdateScale(scale: number, notify = false, keep_focus = false): void { scale = Math.max(50, Math.min(200, scale)); if (scale !== this.scale) { this.scale = scale; this.input.value = this.format.Format(scale) + '%'; this.slider.value = scale.toFixed(1); if (notify) { if (!this.timeout) { this.timeout = requestAnimationFrame(() => { this.timeout = 0; this.Publish({ type: 'scale', value: this.scale / 100, keep_focus, }); }); } } } } }