diff --git a/.changeset/autosize-component.md b/.changeset/autosize-component.md new file mode 100644 index 000000000..8782cbe05 --- /dev/null +++ b/.changeset/autosize-component.md @@ -0,0 +1,7 @@ +--- +"@tabler/core": minor +"@tabler/docs": patch +"@tabler/preview": patch +--- + +Added `Autosize` component to `tabler.js` with the `autosize` plugin bundled, plus `update()` and `dispose()` methods. diff --git a/core/js/src/autosize.ts b/core/js/src/autosize.ts index d100e541d..6c04e8fe3 100644 --- a/core/js/src/autosize.ts +++ b/core/js/src/autosize.ts @@ -1,11 +1,83 @@ -// js-docs-start autosize-init -const autosizeElements: NodeListOf = document.querySelectorAll('[data-bs-toggle="autosize"]') +/** + * -------------------------------------------------------------------------- + * Tabler autosize.ts + * Licensed under MIT (https://github.com/tabler/tabler/blob/dev/LICENSE) + * -------------------------------------------------------------------------- + */ -if (autosizeElements.length) { - autosizeElements.forEach(function (element: HTMLElement) { - if (window.autosize) { - window.autosize(element) +import autosize from 'autosize' + +import BaseComponent from './bootstrap/base-component' +import SelectorEngine from './bootstrap/dom/selector-engine' +import type { ElementSelector } from './bootstrap/types' + +type ComponentConfig = Record + +type ComponentConfigInput = Record + +/** + * Constants + */ + +const NAME = 'autosize' + +const SELECTOR_DATA_TOGGLE = `[data-bs-toggle="${NAME}"], [data-tblr-toggle="${NAME}"]` + +const Default: ComponentConfig = {} + +const DefaultType: Record = {} + +/** + * Class definition + * + * Wraps the `autosize` plugin (https://github.com/jackmoore/autosize), bundled + * with `tabler.js`. + */ + +class Autosize extends BaseComponent { + declare _element: HTMLTextAreaElement + declare _config: ComponentConfig + + constructor(element: ElementSelector, config?: ComponentConfigInput) { + super(element, config) + + if (this._element) { + autosize(this._element) } - }) + } + + // Getters + static get Default(): ComponentConfig { + return Default + } + + static get DefaultType(): Record { + return DefaultType + } + + static get NAME(): string { + return NAME + } + + // Public + update(): void { + autosize.update(this._element) + } + + dispose(): void { + autosize.destroy(this._element) + super.dispose() + } +} + +/** + * Data API implementation + */ + +// js-docs-start autosize-init +for (const element of SelectorEngine.find(SELECTOR_DATA_TOGGLE)) { + Autosize.getOrCreateInstance(element) } // js-docs-end autosize-init + +export default Autosize diff --git a/core/js/src/global.d.ts b/core/js/src/global.d.ts index a5373ac49..cc749baf0 100644 --- a/core/js/src/global.d.ts +++ b/core/js/src/global.d.ts @@ -1,5 +1,18 @@ +// The `autosize` package ships no type definitions +declare module 'autosize' { + type AutosizeTarget = Element | ArrayLike + + interface Autosize { + (target: AutosizeTarget): AutosizeTarget + update(target: AutosizeTarget): AutosizeTarget + destroy(target: AutosizeTarget): AutosizeTarget + } + + const autosize: Autosize + export default autosize +} + interface Window { - autosize?: (element: HTMLElement | HTMLTextAreaElement) => void countUp?: { CountUp: new ( target: HTMLElement, diff --git a/core/js/tabler.ts b/core/js/tabler.ts index e4822a3f1..794ce517d 100644 --- a/core/js/tabler.ts +++ b/core/js/tabler.ts @@ -12,3 +12,6 @@ import './src/sortable' // Re-export everything from bootstrap.ts (single source of truth) export * from './src/bootstrap' + +// Tabler's own components +export { default as Autosize } from './src/autosize' diff --git a/core/js/tests/unit/autosize.spec.ts b/core/js/tests/unit/autosize.spec.ts new file mode 100644 index 000000000..6ee061d1f --- /dev/null +++ b/core/js/tests/unit/autosize.spec.ts @@ -0,0 +1,74 @@ +import { describe, it, expect, beforeAll, beforeEach, afterEach, vi } from 'vitest' +import autosize from 'autosize' +import { clearFixture, getFixture } from '../helpers/fixture' +import Autosize from '../../src/autosize' + +vi.mock('autosize', () => ({ + default: Object.assign(vi.fn(), { update: vi.fn(), destroy: vi.fn() }), +})) + +const plugin = vi.mocked(autosize) + +describe('Autosize', () => { + let fixtureEl: HTMLElement + + beforeAll(() => { + fixtureEl = getFixture() + }) + + beforeEach(() => { + vi.clearAllMocks() + fixtureEl.innerHTML = '' + }) + + afterEach(() => { + clearFixture() + }) + + const textarea = (): HTMLTextAreaElement => fixtureEl.querySelector('textarea')! + + describe('NAME', () => { + it('should return plugin name', () => { + expect(Autosize.NAME).toBe('autosize') + }) + }) + + describe('constructor', () => { + it('should call the autosize plugin with the element', () => { + const instance = new Autosize(textarea()) + + expect(plugin).toHaveBeenCalledWith(textarea()) + expect(Autosize.getInstance(textarea())).toBe(instance) + }) + }) + + describe('update', () => { + it('should call autosize.update with the element', () => { + const instance = new Autosize(textarea()) + instance.update() + + expect(plugin.update).toHaveBeenCalledWith(textarea()) + }) + }) + + describe('dispose', () => { + it('should call autosize.destroy and remove the instance', () => { + const instance = new Autosize(textarea()) + const element = textarea() + instance.dispose() + + expect(plugin.destroy).toHaveBeenCalledWith(element) + expect(Autosize.getInstance(element)).toBeNull() + }) + }) + + describe('getOrCreateInstance', () => { + it('should reuse the instance and not initialise the plugin twice', () => { + const first = Autosize.getOrCreateInstance(textarea()) + const second = Autosize.getOrCreateInstance(textarea()) + + expect(second).toBe(first) + expect(plugin).toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/docs/content/ui/plugins/autosize.mdx b/docs/content/ui/plugins/autosize.mdx index 264dd473d..c05cce394 100644 --- a/docs/content/ui/plugins/autosize.mdx +++ b/docs/content/ui/plugins/autosize.mdx @@ -1,15 +1,11 @@ --- title: Autosize summary: The autosize element will automatically adjust the textarea height and make it easier for users to follow as they type. -docs-libs: [autosize] description: Make textareas grow automatically as users type with the autosize plugin, so longer text stays visible without manual resizing. related: [/ui/forms/elements] --- import Example from '@components/Example.astro'; import CodeDocs from '@components/CodeDocs.astro'; -import TabsPackage from '@components/TabsPackage.astro'; -import { Code } from 'astro:components'; -import { site } from '@shared/lib/site.ts'; ## Overview @@ -21,15 +17,7 @@ A textarea keeps its height no matter how much text it holds, so long answers en ## Installation -Install autosize with npm: - - - -Or include it from a CDN: - -`} /> - -The plugin only changes the height of the field, so it needs no CSS of its own. +The plugin is bundled with `tabler.js`, so there is nothing extra to install or load. It only changes the height of the field, so it needs no CSS of its own. ## Usage @@ -43,22 +31,37 @@ Set `rows` to choose the starting height. With `rows="1"` the field starts as a ## JavaScript -Tabler initializes every element with `data-bs-toggle="autosize"` on page load. This is the code that runs: +Tabler wraps the plugin in an `Autosize` component that works like the Bootstrap components: it is created once per element and stored on it. On page load Tabler creates one for every element with `data-bs-toggle="autosize"`. This is the code that runs: -Call the plugin yourself when you add a textarea later, for example inside a modal or a row added by JavaScript: +Create the component yourself when you add a textarea later, for example inside a modal or a row added by JavaScript: ```js -autosize(document.getElementById('message')); +const autosize = new tabler.Autosize(document.getElementById('message')); ``` -Autosize measures the field, so a hidden textarea gets the wrong height. Run `autosize.update()` once it becomes visible: +`tabler.Autosize.getOrCreateInstance(element)` returns the existing component or creates a new one, so it is safe to call more than once for the same field. + +Autosize measures the field, so a hidden textarea gets the wrong height. Call `update()` once it becomes visible: ```js -autosize.update(document.getElementById('message')); +tabler.Autosize.getOrCreateInstance(document.getElementById('message')).update(); ``` +Call `dispose()` to stop the plugin and remove the component from the element: + +```js +tabler.Autosize.getInstance(document.getElementById('message'))?.dispose(); +``` + +| Method | Description | +| --- | --- | +| `update()` | Recalculates the height, for example after the textarea becomes visible or its value changes from code. | +| `dispose()` | Stops the plugin and removes the component from the element. | +| `getInstance(element)` | Static. Returns the component for the element, or `null`. | +| `getOrCreateInstance(element)` | Static. Returns the component for the element and creates it when needed. | + ## Accessibility - Keep the `label` for the field. A growing textarea is still a normal form control and needs its name. diff --git a/preview/pages/form-elements.astro b/preview/pages/form-elements.astro index f74ea1b0d..ac4f8aa72 100644 --- a/preview/pages/form-elements.astro +++ b/preview/pages/form-elements.astro @@ -51,7 +51,7 @@ const rows = [ ] --- - +