Skip to content

Commit

Permalink
Migrate autosize jQuery to stimulus controller w-autosize
Browse files Browse the repository at this point in the history
- Closes wagtail#10170
  • Loading branch information
suyash5053 authored and lb- committed Apr 17, 2023
1 parent cc23aa6 commit e72e454
Show file tree
Hide file tree
Showing 14 changed files with 210 additions and 318 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.txt
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ Changelog
* Fix: Avoid showing scrollbars in the block picker unless necessary (Babitha Kumari)
* Fix: Always show Add buttons, guide lines, Move up/down, Duplicate, Delete; in StreamField and Inline Panel (Thibaud Colas)
* Fix: Make admin JS i18n endpoint accessible to non-authenticated users (Matt Westcott)
* Autosize text area field will now correctly resize when switching between comments toggle states (Suyash Srivastava)
* Docs: Add code block to make it easier to understand contribution docs (Suyash Singh)
* Docs: Add new "Icons" page for icons customisation and reuse across the admin interface (Coen van der Kamp, Thibaud Colas)
* Docs: Fix broken formatting for MultiFieldPanel / FieldRowPanel permission kwarg docs (Matt Westcott)
Expand Down Expand Up @@ -112,6 +113,7 @@ Changelog
* Maintenance: Migrate select all checkbox in simple translation's submit translation page to Stimulus controller `w-bulk`, remove inline script usage (Hanoon)
* Maintenance: Refactor `SnippetViewSet` to extend `ModelViewSet` (Sage Abdullah)
* Maintenance: Migrate initDismissibles behaviour to a Stimulus controller `w-disimissible` (Loveth Omokaro)
* Maintenance: Replace jQuery autosize v3 with Stimulus `w-autosize` controller using autosize npm package v6 (Suyash Srivastava)


4.2.2 (03.04.2023)
Expand Down
119 changes: 119 additions & 0 deletions client/src/controllers/AutosizeController.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { Application } from '@hotwired/stimulus';
import autosize from 'autosize';
import { AutosizeController } from './AutosizeController';

jest.mock('autosize');
jest.useFakeTimers();

describe('AutosizeController', () => {
let application;
const resizeObserverMockObserve = jest.fn();
const resizeObserverMockUnobserve = jest.fn();
const resizeObserverMockDisconnect = jest.fn();

const ResizeObserverMock = jest.fn().mockImplementation(() => ({
observe: resizeObserverMockObserve,
unobserve: resizeObserverMockUnobserve,
disconnect: resizeObserverMockDisconnect,
}));

global.ResizeObserver = ResizeObserverMock;

describe('basic behaviour', () => {
beforeAll(() => {
document.body.innerHTML = `
<textarea
data-controller="w-autosize"
id="text"
></textarea>
`;
});

afterEach(() => {
jest.clearAllMocks();
});

afterAll(() => {
application.stop();
});

it('calls autosize when connected', async () => {
expect(autosize).not.toHaveBeenCalled();
expect(ResizeObserverMock).not.toHaveBeenCalled();
expect(resizeObserverMockObserve).not.toHaveBeenCalled();

application = Application.start();
application.register('w-autosize', AutosizeController);

// await next tick
await Promise.resolve();

const textarea = document.getElementById('text');

expect(autosize).toHaveBeenCalledWith(textarea);
expect(ResizeObserverMock).toHaveBeenCalledWith(expect.any(Function));
expect(resizeObserverMockObserve).toHaveBeenCalledWith(textarea);
});

it('cleans up on disconnect', async () => {
expect(autosize.destroy).not.toHaveBeenCalled();
expect(resizeObserverMockUnobserve).not.toHaveBeenCalled();

const textarea = document.getElementById('text');

textarea.remove();

await Promise.resolve();

expect(autosize.destroy).toHaveBeenCalledWith(textarea);
expect(resizeObserverMockDisconnect).toHaveBeenCalled();
});
});

describe('using actions to dispatch methods', () => {
beforeAll(() => {
document.body.innerHTML = `
<textarea
id="text"
data-controller="w-autosize"
data-action="some:event->w-autosize#resize"
></textarea>`;

application = Application.start();
application.register('w-autosize', AutosizeController);
});

afterEach(() => {
jest.clearAllMocks();
});

afterAll(() => {
application.stop();
});

it('calls autosize update from resize method', async () => {
// await next tick
await Promise.resolve();

expect(autosize.update).not.toHaveBeenCalled();

const textarea = document.getElementById('text');

textarea.dispatchEvent(new CustomEvent('some:event'));
jest.runAllTimers(); // resize is debounced

expect(autosize.update).toHaveBeenCalledWith(textarea);

// fire multiple events - confirm that the function is debounced

expect(autosize.update).toHaveBeenCalledTimes(1);
textarea.dispatchEvent(new CustomEvent('some:event'));
textarea.dispatchEvent(new CustomEvent('some:event'));
textarea.dispatchEvent(new CustomEvent('some:event'));
textarea.dispatchEvent(new CustomEvent('some:event'));
jest.runAllTimers(); // resize is debounced

expect(autosize.update).toHaveBeenCalledTimes(2);
});
});
});
33 changes: 33 additions & 0 deletions client/src/controllers/AutosizeController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Controller } from '@hotwired/stimulus';
import autosize from 'autosize';
import { debounce } from '../utils/debounce';

/**
* Adds the ability for a text area element to be auto-sized as the user
* types in the field so that it expands to show all content.
*
* @example
* <textarea data-controller="w-autosize"></textarea>
*/
export class AutosizeController extends Controller<HTMLTextAreaElement> {
resizeObserver?: ResizeObserver;

resize() {
autosize.update(this.element);
}

initialize() {
this.resize = debounce(this.resize.bind(this), 50);
}

connect() {
autosize(this.element);
this.resizeObserver = new ResizeObserver(this.resize);
this.resizeObserver.observe(this.element);
}

disconnect() {
this.resizeObserver?.disconnect();
autosize.destroy(this.element);
}
}
2 changes: 2 additions & 0 deletions client/src/controllers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Definition } from '@hotwired/stimulus';

// Order controller imports alphabetically.
import { ActionController } from './ActionController';
import { AutosizeController } from './AutosizeController';
import { BulkController } from './BulkController';
import { CountController } from './CountController';
import { DismissibleController } from './DismissibleController';
Expand All @@ -19,6 +20,7 @@ import { UpgradeController } from './UpgradeController';
export const coreControllerDefinitions: Definition[] = [
// Keep this list in alphabetical order
{ controllerConstructor: ActionController, identifier: 'w-action' },
{ controllerConstructor: AutosizeController, identifier: 'w-autosize' },
{ controllerConstructor: BulkController, identifier: 'w-bulk' },
{ controllerConstructor: CountController, identifier: 'w-count' },
{ controllerConstructor: DismissibleController, identifier: 'w-dismissible' },
Expand Down
13 changes: 0 additions & 13 deletions client/src/entrypoints/admin/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,19 +186,6 @@ $(() => {
// Add class to the body from which transitions may be hung so they don't appear to transition as the page loads
$('body').addClass('ready');

/* Functions that need to run/rerun when active tabs are changed */
function resizeTextAreas() {
// eslint-disable-next-line func-names
$('textarea[data-autosize-on]').each(function () {
// eslint-disable-next-line no-undef
autosize.update($(this).get());
});
}

// Resize textareas on page load and when tab changed
$(document).ready(resizeTextAreas);
document.addEventListener('wagtail:tab-changed', resizeTextAreas);

// eslint-disable-next-line func-names
$('.dropdown').each(function () {
const $dropdown = $(this);
Expand Down
18 changes: 0 additions & 18 deletions client/src/entrypoints/admin/telepath/widgets.js
Original file line number Diff line number Diff line change
Expand Up @@ -128,24 +128,6 @@ class Select extends Widget {
}
window.telepath.register('wagtail.widgets.Select', Select);

class AdminAutoHeightTextInput extends Widget {
render(placeholder, name, id, initialState, parentCapabilities) {
const boundWidget = super.render(
placeholder,
name,
id,
initialState,
parentCapabilities,
);
window.autosize($('#' + id));
return boundWidget;
}
}
window.telepath.register(
'wagtail.widgets.AdminAutoHeightTextInput',
AdminAutoHeightTextInput,
);

class DraftailInsertBlockCommand {
/* Definition for a command in the Draftail context menu that inserts a block.
* Constructor args:
Expand Down
58 changes: 0 additions & 58 deletions client/src/entrypoints/admin/telepath/widgets.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -211,64 +211,6 @@ describe('telepath: wagtail.widgets.Select', () => {
});
});

describe('telepath: wagtail.widgets.AdminAutoHeightTextInput', () => {
let boundWidget;

beforeEach(() => {
window.autosize = jest.fn();

// Create a placeholder to render the widget
document.body.innerHTML = '<div id="placeholder"></div>';

// Unpack and render a textarea using the AdminAutoHeightTextInput widget
const widgetDef = window.telepath.unpack({
_type: 'wagtail.widgets.AdminAutoHeightTextInput',
_args: [
'<textarea name="__NAME__" cols="40" rows="1" id="__ID__"></textarea>',
'__ID__',
],
});
boundWidget = widgetDef.render(
document.getElementById('placeholder'),
'the-name',
'the-id',
'The Value',
);
});

test('it renders correctly', () => {
expect(document.body.innerHTML).toBe(
'<textarea name="the-name" cols="40" rows="1" id="the-id"></textarea>',
);
expect(document.querySelector('textarea').value).toBe('The Value');
});

test('window.autosize was called', () => {
expect(window.autosize.mock.calls.length).toBe(1);
expect(window.autosize.mock.calls[0][0].get(0)).toBe(
document.querySelector('textarea'),
);
});

test('getValue() returns the current value', () => {
expect(boundWidget.getValue()).toBe('The Value');
});

test('getState() returns the current state', () => {
expect(boundWidget.getState()).toBe('The Value');
});

test('setState() changes the current state', () => {
boundWidget.setState('The new Value');
expect(document.querySelector('textarea').value).toBe('The new Value');
});

test('focus() focuses the text input', () => {
boundWidget.focus();
expect(document.activeElement).toBe(document.querySelector('textarea'));
});
});

describe('telepath: wagtail.widgets.DraftailRichTextArea', () => {
let boundWidget;
let inputElement;
Expand Down
23 changes: 23 additions & 0 deletions docs/releases/5.0.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ Those improvements were implemented by Albina Starykova as part of an [Outreachy
* Avoid showing scrollbars in the block picker unless necessary (Babitha Kumari)
* Always show Add buttons, guide lines, Move up/down, Duplicate, Delete; in StreamField and Inline Panel (Thibaud Colas)
* Make admin JS i18n endpoint accessible to non-authenticated users (Matt Westcott)
* Autosize text area field will now correctly resize when switching between comments toggle states (Suyash Srivastava)

### Documentation

Expand Down Expand Up @@ -159,6 +160,7 @@ Those improvements were implemented by Albina Starykova as part of an [Outreachy
* Migrate select all checkbox in simple translation's submit translation page to Stimulus controller `w-bulk`, remove inline script usage (Hanoon)
* Refactor `SnippetViewSet` to extend `ModelViewSet` (Sage Abdullah)
* Migrate initDismissibles behaviour to a Stimulus controller `w-disimissible` (Loveth Omokaro)
* Replace jQuery autosize v3 with Stimulus `w-autosize` controller using autosize npm package v6 (Suyash Srivastava)


## Upgrade considerations
Expand Down Expand Up @@ -290,6 +292,27 @@ To adjust what triggers the initial check (to see if the fields should be in syn

Above we have adjusted these attributes to add a 'change' event listener to trigger the sync and also adjusted to look for a field with `some_other_slug` instead.

### Auto height/size text area widget now relies on data attributes

If you are using the `wagtail.admin.widgets.AdminAutoHeightTextInput` only, this change will have no impact when upgrading. However, if you are relying on the global `autosize` function at `window.autosize` on the client, this will no longer work.

It is recommended that the `AdminAutoHeightTextInput` widget be used instead. You can also adopt the `data-controller` attribute and this will now function as before. Alternativey, you can simply add the required Stimulus data controller attribute as shown below.

**Old syntax**

```html
<textarea id="story" name="story">It was a dark and stormy night...</textarea>
<script>window.autosize($('story'));</script>
```

**New syntax**

```html
<textarea name="story" data-controller="w-autosize">It was a dark and stormy night...</textarea>
```

There are no additional data attributes supported at this time.

### Progress button (`button-longrunning`) now relies on data attributes

The `button-longrunning` class usage has been updated to use the newly adopted Stimulus approach, the previous data attributes will be deprecated in a future release.
Expand Down
Loading

0 comments on commit e72e454

Please sign in to comment.