mirror of
https://github.com/tabler/tabler.git
synced 2026-10-12 05:06:55 +04:00
Replace the autosize package with an own implementation (#3052)
This commit is contained in:
1 parent
68c844c7d1
commit
62408b8c8f
9 files changed
+151
-83
No files matched your search
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@tabler/core": patch
|
||||
"@tabler/docs": patch
|
||||
---
|
||||
|
||||
Replaced the `autosize` package with a dependency-free `Autosize` component that fires `resized.bs.autosize`.
|
||||
+67
-8
@@ -5,9 +5,8 @@
|
||||
* --------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
import autosize from 'autosize'
|
||||
|
||||
import BaseComponent from './bootstrap/base-component'
|
||||
import EventHandler from './bootstrap/dom/event-handler'
|
||||
import SelectorEngine from './bootstrap/dom/selector-engine'
|
||||
import type { ElementSelector } from './bootstrap/types'
|
||||
|
||||
@@ -20,6 +19,10 @@ type ComponentConfigInput = Record<string, unknown>
|
||||
*/
|
||||
|
||||
const NAME = 'autosize'
|
||||
const DATA_KEY = `bs.${NAME}`
|
||||
const EVENT_KEY = `.${DATA_KEY}`
|
||||
|
||||
const EVENT_RESIZED = `resized${EVENT_KEY}`
|
||||
|
||||
const SELECTOR_DATA_TOGGLE = `[data-bs-toggle="${NAME}"], [data-tblr-toggle="${NAME}"]`
|
||||
|
||||
@@ -30,20 +33,47 @@ const DefaultType: Record<keyof ComponentConfig, string> = {}
|
||||
/**
|
||||
* Class definition
|
||||
*
|
||||
* Wraps the `autosize` plugin (https://github.com/jackmoore/autosize), bundled
|
||||
* with `tabler.js`.
|
||||
* Grows a textarea with its content and shrinks it back when text is removed.
|
||||
* The height follows `scrollHeight`, so `rows` sets the starting height and
|
||||
* `max-height` caps the growth, after which the field scrolls again.
|
||||
*/
|
||||
|
||||
class Autosize extends BaseComponent {
|
||||
declare _element: HTMLTextAreaElement
|
||||
declare _config: ComponentConfig
|
||||
_observer: ResizeObserver | null = null
|
||||
_inlineStyle = ''
|
||||
_width = 0
|
||||
_onInput = (): void => this.update()
|
||||
|
||||
constructor(element: ElementSelector, config?: ComponentConfigInput) {
|
||||
super(element, config)
|
||||
|
||||
if (this._element) {
|
||||
autosize(this._element)
|
||||
if (!this._element) {
|
||||
return
|
||||
}
|
||||
|
||||
this._inlineStyle = this._element.getAttribute('style') ?? ''
|
||||
this._element.style.resize = 'none'
|
||||
this._element.style.overflowY = 'hidden'
|
||||
|
||||
// `input` is not in EventHandler's list of native events, so it is bound
|
||||
// directly.
|
||||
this._element.addEventListener('input', this._onInput)
|
||||
|
||||
// Text wraps differently when the field changes width (a resized window,
|
||||
// a collapsing sidebar), so the height is measured again then.
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
this._observer = new ResizeObserver(() => {
|
||||
if (this._element.offsetWidth !== this._width) {
|
||||
this._width = this._element.offsetWidth
|
||||
this.update()
|
||||
}
|
||||
})
|
||||
this._observer.observe(this._element)
|
||||
}
|
||||
|
||||
this.update()
|
||||
}
|
||||
|
||||
// Getters
|
||||
@@ -61,11 +91,40 @@ class Autosize extends BaseComponent {
|
||||
|
||||
// Public
|
||||
update(): void {
|
||||
autosize.update(this._element)
|
||||
const element = this._element
|
||||
if (!element.isConnected || element.offsetParent === null) {
|
||||
return
|
||||
}
|
||||
|
||||
// Collapsing the field to measure it can move the page; the scroll
|
||||
// position is put back afterwards.
|
||||
const { scrollTop } = document.documentElement
|
||||
const style = getComputedStyle(element)
|
||||
const border = style.boxSizing === 'border-box' ? Number.parseFloat(style.borderTopWidth) + Number.parseFloat(style.borderBottomWidth) : 0
|
||||
const before = element.style.height
|
||||
|
||||
element.style.height = 'auto'
|
||||
const height = `${element.scrollHeight + border}px`
|
||||
element.style.height = height
|
||||
|
||||
// Past max-height the content no longer fits, so let the field scroll.
|
||||
element.style.overflowY = element.scrollHeight > element.clientHeight ? 'auto' : 'hidden'
|
||||
document.documentElement.scrollTop = scrollTop
|
||||
|
||||
if (height !== before) {
|
||||
EventHandler.trigger(element, EVENT_RESIZED)
|
||||
}
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
autosize.destroy(this._element)
|
||||
this._element.removeEventListener('input', this._onInput)
|
||||
this._observer?.disconnect()
|
||||
if (this._inlineStyle) {
|
||||
this._element.setAttribute('style', this._inlineStyle)
|
||||
} else {
|
||||
this._element.removeAttribute('style')
|
||||
}
|
||||
|
||||
super.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
-14
@@ -1,17 +1,3 @@
|
||||
// The `autosize` package ships no type definitions
|
||||
declare module 'autosize' {
|
||||
type AutosizeTarget = Element | ArrayLike<Element>
|
||||
|
||||
interface Autosize {
|
||||
(target: AutosizeTarget): AutosizeTarget
|
||||
update(target: AutosizeTarget): AutosizeTarget
|
||||
destroy(target: AutosizeTarget): AutosizeTarget
|
||||
}
|
||||
|
||||
const autosize: Autosize
|
||||
export default autosize
|
||||
}
|
||||
|
||||
// IMask (https://imask.js.org) is loaded separately, not bundled
|
||||
interface IMaskInstance {
|
||||
value: string
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
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
|
||||
|
||||
@@ -17,8 +10,7 @@ describe('Autosize', () => {
|
||||
})
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
fixtureEl.innerHTML = '<textarea data-bs-toggle="autosize"></textarea>'
|
||||
fixtureEl.innerHTML = '<textarea data-bs-toggle="autosize" rows="1" style="width: 200px; line-height: 20px; padding: 0; border: 0; box-sizing: border-box"></textarea>'
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -26,6 +18,8 @@ describe('Autosize', () => {
|
||||
})
|
||||
|
||||
const textarea = (): HTMLTextAreaElement => fixtureEl.querySelector('textarea')!
|
||||
const lines = (count: number): string => Array.from({ length: count }, (_, i) => `line ${i + 1}`).join('\n')
|
||||
const height = (): number => Number.parseFloat(textarea().style.height)
|
||||
|
||||
describe('NAME', () => {
|
||||
it('should return plugin name', () => {
|
||||
@@ -34,41 +28,73 @@ describe('Autosize', () => {
|
||||
})
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should call the autosize plugin with the element', () => {
|
||||
it('should size the field to its content and hide the resize handle', () => {
|
||||
textarea().value = lines(4)
|
||||
const instance = new Autosize(textarea())
|
||||
|
||||
expect(plugin).toHaveBeenCalledWith(textarea())
|
||||
expect(Autosize.getInstance(textarea())).toBe(instance)
|
||||
expect(height()).toBe(80)
|
||||
expect(textarea().style.resize).toBe('none')
|
||||
expect(textarea().style.overflowY).toBe('hidden')
|
||||
})
|
||||
})
|
||||
|
||||
describe('input', () => {
|
||||
it('should grow and shrink with the text', () => {
|
||||
new Autosize(textarea())
|
||||
const initial = height()
|
||||
|
||||
textarea().value = lines(3)
|
||||
textarea().dispatchEvent(new Event('input'))
|
||||
expect(height()).toBe(60)
|
||||
|
||||
textarea().value = ''
|
||||
textarea().dispatchEvent(new Event('input'))
|
||||
expect(height()).toBe(initial)
|
||||
})
|
||||
|
||||
it('should fire resized.bs.autosize when the height changes', () => {
|
||||
new Autosize(textarea())
|
||||
const spy = vi.fn()
|
||||
textarea().addEventListener('resized.bs.autosize', spy)
|
||||
|
||||
textarea().value = lines(2)
|
||||
textarea().dispatchEvent(new Event('input'))
|
||||
textarea().dispatchEvent(new Event('input'))
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe('update', () => {
|
||||
it('should call autosize.update with the element', () => {
|
||||
it('should scroll again past max-height', () => {
|
||||
textarea().style.maxHeight = '40px'
|
||||
const instance = new Autosize(textarea())
|
||||
|
||||
textarea().value = lines(5)
|
||||
instance.update()
|
||||
|
||||
expect(plugin.update).toHaveBeenCalledWith(textarea())
|
||||
expect(textarea().clientHeight).toBe(40)
|
||||
expect(textarea().style.overflowY).toBe('auto')
|
||||
})
|
||||
|
||||
it('should include the border when the box is border-box', () => {
|
||||
textarea().style.border = '3px solid red'
|
||||
textarea().value = lines(2)
|
||||
new Autosize(textarea())
|
||||
|
||||
expect(height()).toBe(46)
|
||||
})
|
||||
})
|
||||
|
||||
describe('dispose', () => {
|
||||
it('should call autosize.destroy and remove the instance', () => {
|
||||
it('should restore the inline style and remove the instance', () => {
|
||||
const style = textarea().getAttribute('style')
|
||||
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)
|
||||
expect(textarea().getAttribute('style')).toBe(style)
|
||||
expect(Autosize.getInstance(textarea())).toBeNull()
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -5,12 +5,6 @@
|
||||
"dist/imask.min.js"
|
||||
]
|
||||
},
|
||||
"autosize": {
|
||||
"npm": "autosize",
|
||||
"js": [
|
||||
"dist/autosize.min.js"
|
||||
]
|
||||
},
|
||||
"apexcharts": {
|
||||
"npm": "apexcharts",
|
||||
"js": [
|
||||
|
||||
+2
-3
@@ -249,10 +249,8 @@
|
||||
"@melloware/coloris": "^0.25.0",
|
||||
"@types/node": "^26.4.0",
|
||||
"@vitest/browser-playwright": "4.1.11",
|
||||
"playwright": "1.62.1",
|
||||
"@vitest/coverage-istanbul": "4.1.11",
|
||||
"apexcharts": "7.0.0",
|
||||
"autosize": "^6.0.1",
|
||||
"choices.js": "^11.2.4",
|
||||
"clipboard": "^2.0.11",
|
||||
"countup.js": "^2.10.1",
|
||||
@@ -268,10 +266,11 @@
|
||||
"list.js": "^2.3.1",
|
||||
"litepicker": "^2.0.12",
|
||||
"nouislider": "^15.8.1",
|
||||
"playwright": "1.62.1",
|
||||
"plyr": "^3.8.4",
|
||||
"signature_pad": "^5.1.4",
|
||||
"sass": "1.103.1",
|
||||
"sass-true": "^10.1.0",
|
||||
"signature_pad": "^5.1.4",
|
||||
"sortablejs": "^1.15.7",
|
||||
"star-rating.js": "^4.3.1",
|
||||
"tom-select": "^2.6.2",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: Autosize
|
||||
summary: The autosize element will automatically adjust the textarea height and make it easier for users to follow as they type.
|
||||
description: Make textareas grow automatically as users type with the autosize plugin, so longer text stays visible without manual resizing.
|
||||
description: Make textareas grow automatically as users type with autosize, so longer text stays visible without manual resizing.
|
||||
related: [/ui/forms/elements]
|
||||
---
|
||||
import Example from '@components/Example.astro';
|
||||
@@ -9,7 +9,7 @@ import CodeDocs from '@components/CodeDocs.astro';
|
||||
|
||||
## Overview
|
||||
|
||||
A textarea keeps its height no matter how much text it holds, so long answers end up in a small scrolling box. The [autosize](https://github.com/jackmoore/autosize) plugin grows the field as the user types and shrinks it again when text is removed.
|
||||
A textarea keeps its height no matter how much text it holds, so long answers end up in a small scrolling box. Autosize grows the field as the user types and shrinks it again when text is removed.
|
||||
|
||||
<Example column vertical>
|
||||
<label class="form-label">Autosize example</label> <textarea class="form-control" data-bs-toggle="autosize" placeholder="Type something…"></textarea>
|
||||
@@ -17,7 +17,7 @@ A textarea keeps its height no matter how much text it holds, so long answers en
|
||||
|
||||
## Installation
|
||||
|
||||
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.
|
||||
Autosize is part of `tabler.js` and has no dependency, so there is nothing extra to install or load. It only sets the height of the field, so it needs no CSS of its own.
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -27,11 +27,15 @@ Add `data-bs-toggle="autosize"` to a textarea. Tabler picks it up on page load,
|
||||
<label class="form-label">Message</label> <textarea class="form-control" data-bs-toggle="autosize" rows="1" placeholder="Write a message…"></textarea>
|
||||
</Example>
|
||||
|
||||
Set `rows` to choose the starting height. With `rows="1"` the field starts as a single line and grows from there.
|
||||
Set `rows` to choose the starting height. With `rows="1"` the field starts as a single line and grows from there. Set a `max-height` to cap the growth; past it the field scrolls again.
|
||||
|
||||
<Example column vertical>
|
||||
<label class="form-label">Notes, at most four lines</label> <textarea class="form-control" data-bs-toggle="autosize" rows="2" style="max-height: 6.5rem" placeholder="Keep typing to see the field stop growing…"></textarea>
|
||||
</Example>
|
||||
|
||||
## JavaScript
|
||||
|
||||
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:
|
||||
Tabler ships 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"`. It sets the height from the content on every `input` event and again when the field changes width. This is the code that runs:
|
||||
|
||||
<CodeDocs name="autosize-init" file="core/js/src/autosize.ts" />
|
||||
|
||||
@@ -43,13 +47,13 @@ const autosize = new tabler.Autosize(document.getElementById('message'));
|
||||
|
||||
`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:
|
||||
Autosize measures the field, so a hidden textarea is skipped. Call `update()` once it becomes visible, or after you set its value from code:
|
||||
|
||||
```js
|
||||
tabler.Autosize.getOrCreateInstance(document.getElementById('message')).update();
|
||||
```
|
||||
|
||||
Call `dispose()` to stop the plugin and remove the component from the element:
|
||||
Call `dispose()` to stop it and put the inline style back the way it was:
|
||||
|
||||
```js
|
||||
tabler.Autosize.getInstance(document.getElementById('message'))?.dispose();
|
||||
@@ -58,12 +62,20 @@ 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. |
|
||||
| `dispose()` | Stops listening, restores the inline style 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. |
|
||||
|
||||
Every change of height fires `resized.bs.autosize` on the textarea, for example to scroll a chat window to the bottom:
|
||||
|
||||
```js
|
||||
document.getElementById('message').addEventListener('resized.bs.autosize', () => {
|
||||
thread.scrollTop = thread.scrollHeight;
|
||||
});
|
||||
```
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Keep the `label` for the field. A growing textarea is still a normal form control and needs its name.
|
||||
- Let the field grow instead of setting a fixed `max-height`. Cutting the growth off brings back the scrolling box the plugin is meant to remove.
|
||||
- Do not remove the resize handle unless the field really grows on its own - without either one, a user cannot see long text.
|
||||
- Let the field grow freely when you can. A `max-height` brings back a scrolling box, so keep it generous, for example the height of the visible chat window.
|
||||
- Autosize hides the resize handle, because the field grows on its own. With a `max-height` the field scrolls past the cap, so long text stays reachable.
|
||||
Generated
+1
-9
@@ -132,9 +132,6 @@ importers:
|
||||
apexcharts:
|
||||
specifier: 7.0.0
|
||||
version: 7.0.0
|
||||
autosize:
|
||||
specifier: ^6.0.1
|
||||
version: 6.0.1
|
||||
choices.js:
|
||||
specifier: ^11.2.4
|
||||
version: 11.2.4
|
||||
@@ -2095,9 +2092,6 @@ packages:
|
||||
peerDependencies:
|
||||
postcss: ^8.1.0
|
||||
|
||||
autosize@6.0.1:
|
||||
resolution: {integrity: sha512-f86EjiUKE6Xvczc4ioP1JBlWG7FKrE13qe/DxBCpe8GCipCq2nFw73aO8QEBKHfSbYGDN5eB9jXWKen7tspDqQ==}
|
||||
|
||||
axios@0.33.0:
|
||||
resolution: {integrity: sha512-juz19g7B3UWT9r3+tgRFQf2Bdy6IKDVroiyuVOUrkILVKHpqHL++K1l8ybvfdEP9B/VE72vk8vllbnedVCm/og==}
|
||||
|
||||
@@ -6651,8 +6645,6 @@ snapshots:
|
||||
postcss: 8.5.28
|
||||
postcss-value-parser: 4.2.0
|
||||
|
||||
autosize@6.0.1: {}
|
||||
|
||||
axios@0.33.0(debug@4.4.3(supports-color@10.2.2)):
|
||||
dependencies:
|
||||
follow-redirects: 1.16.0(debug@4.4.3(supports-color@10.2.2))
|
||||
@@ -9615,7 +9607,7 @@ snapshots:
|
||||
vite@8.2.2(@types/node@26.4.0)(esbuild@0.28.1)(sass@1.103.1)(terser@5.51.2)(tsx@4.23.13)(yaml@2.9.0):
|
||||
dependencies:
|
||||
lightningcss: 1.33.0
|
||||
picomatch: 4.0.5
|
||||
picomatch: 4.0.7
|
||||
postcss: 8.5.28
|
||||
rolldown: 1.2.6
|
||||
tinyglobby: 0.2.17
|
||||
|
||||
@@ -11,12 +11,6 @@
|
||||
"description": "Vanilla JavaScript input masking library with support for various mask types and dynamic behavior.",
|
||||
"license": "MIT"
|
||||
},
|
||||
{
|
||||
"name": "Autosize",
|
||||
"url": "https://www.jacklmoore.com/autosize/",
|
||||
"description": "Automatically adjusts textarea height to fit text content.",
|
||||
"license": "MIT"
|
||||
},
|
||||
{
|
||||
"name": "ApexCharts",
|
||||
"url": "https://apexcharts.com/",
|
||||
|
||||
Reference in new issue
Block a user