Migrate initAutoPopulate to Stimulus

- Removed initAutoPopultae function from editor.js
- Added the compare and urlify methos to SlugController
- Wrote tests for SyncController, added more test cases to slugController
- Closes #10088
pull/10262/head
Lovelyfin00 2023-03-06 08:20:34 +01:00 zatwierdzone przez LB (Ben Johnston)
rodzic d6d8b99f3e
commit a81242ca86
13 zmienionych plików z 669 dodań i 40 usunięć

Wyświetl plik

@ -88,6 +88,7 @@ Changelog
* Maintenance: Move snippet choosers and model check registration to `SnippetViewSet.on_register()` (Sage Abdullah)
* Maintenance: Remove unused snippets delete-multiple view (Sage Abdullah)
* Maintenance: Improve performance of determining live page URLs across the admin interface using `pageurl` template tag (Satvik Vashisht)
* Maintenance: Migrate `window.initSlugAutoPopulate` behaviour to a Stimulus Controller `w-sync` (Loveth Omokaro)
4.2.2 (xx.xx.xxxx) - IN DEVELOPMENT

Wyświetl plik

@ -60,3 +60,168 @@ describe('SlugController', () => {
expect(slugInput.value).toEqual('visiter-toulouse-en-été-2025');
});
});
describe('compare behaviour', () => {
let application;
beforeEach(() => {
application?.stop();
document.body.innerHTML = `
<input
id="id_slug"
name="slug"
type="text"
data-controller="w-slug"
/>`;
application = Application.start();
application.register('w-slug', SlugController);
const slugInput = document.querySelector('#id_slug');
slugInput.dataset.action = [
'blur->w-slug#slugify',
'custom:event->w-slug#compare',
].join(' ');
});
it('should not prevent default if input has no value', () => {
const event = new CustomEvent('custom:event', {
detail: { value: 'title alpha' },
});
event.preventDefault = jest.fn();
document.getElementById('id_slug').dispatchEvent(event);
expect(document.getElementById('id_slug').value).toBe('');
expect(event.preventDefault).not.toHaveBeenCalled();
});
it('should not prevent default if the values are the same', () => {
document.querySelector('#id_slug').setAttribute('value', 'title-alpha');
const event = new CustomEvent('custom:event', {
detail: { value: 'title alpha' },
});
event.preventDefault = jest.fn();
document.getElementById('id_slug').dispatchEvent(event);
expect(event.preventDefault).not.toHaveBeenCalled();
});
it('should prevent default if the values are not the same', () => {
document.querySelector('#id_slug').setAttribute('value', 'title-alpha');
const event = new CustomEvent('custom:event', {
detail: { value: 'title beta' },
});
event.preventDefault = jest.fn();
document.getElementById('id_slug').dispatchEvent(event);
expect(event.preventDefault).toHaveBeenCalled();
});
it('should not prevent default if both values are empty strings', () => {
const slugInput = document.querySelector('#id_slug');
slugInput.setAttribute('value', '');
const event = new CustomEvent('custom:event', {
detail: { value: '' },
});
event.preventDefault = jest.fn();
slugInput.dispatchEvent(event);
expect(event.preventDefault).not.toHaveBeenCalled();
});
it('should prevent default if the new value is an empty string but the existing value is not', () => {
const slugInput = document.querySelector('#id_slug');
slugInput.setAttribute('value', 'existing-value');
const event = new CustomEvent('custom:event', {
detail: { value: '' },
});
event.preventDefault = jest.fn();
slugInput.dispatchEvent(event);
expect(event.preventDefault).toHaveBeenCalled();
});
});
describe('urlify behaviour', () => {
require('../../../wagtail/admin/static_src/wagtailadmin/js/vendor/urlify')
.default;
let application;
beforeEach(() => {
application?.stop();
document.body.innerHTML = `
<input
id="id_slug"
name="slug"
type="text"
data-controller="w-slug"
/>`;
application = Application.start();
application.register('w-slug', SlugController);
const slugInput = document.querySelector('#id_slug');
slugInput.dataset.action = [
'blur->w-slug#slugify',
'custom:event->w-slug#urlify:prevent',
].join(' ');
});
it('should update slug input value if the values are the same', () => {
const slugInput = document.getElementById('id_slug');
slugInput.value = 'urlify Testing On edit page ';
const event = new CustomEvent('custom:event', {
detail: { value: 'urlify Testing On edit page' },
bubbles: false,
});
document.getElementById('id_slug').dispatchEvent(event);
expect(slugInput.value).toBe('urlify-testing-on-edit-page');
});
it('should transform input with special characters to their ASCII equivalent', () => {
const slugInput = document.getElementById('id_slug');
slugInput.value = 'Some Title with éçà Spaces';
const event = new CustomEvent('custom:event', {
detail: { value: 'Some Title with éçà Spaces' },
});
document.getElementById('id_slug').dispatchEvent(event);
expect(slugInput.value).toBe('some-title-with-eca-spaces');
});
it('should return an empty string when input contains only special characters', () => {
const slugInput = document.getElementById('id_slug');
slugInput.value = '$$!@#$%^&*';
const event = new CustomEvent('custom:event', {
detail: { value: '$$!@#$%^&*' },
});
document.getElementById('id_slug').dispatchEvent(event);
expect(slugInput.value).toBe('');
});
});

Wyświetl plik

@ -14,9 +14,69 @@ export class SlugController extends Controller<HTMLInputElement> {
declare allowUnicodeValue: boolean;
slugify() {
this.element.value = cleanForSlug(this.element.value.trim(), false, {
unicodeSlugsEnabled: this.allowUnicodeValue,
});
/**
* Allow for a comparison value to be provided, if does not compare to the
* current value (once transformed), then the event's default will
* be prevented.
*/
compare(
event: CustomEvent<{ value: string }> & { params?: { compareAs?: string } },
) {
// do not attempt to compare if the current field is empty
if (!this.element.value) {
return true;
}
const {
detail: { value = '' } = {},
params: { compareAs = 'slugify' } = {},
} = event;
const compareValue = this[compareAs]({ detail: { value } }, true);
const currentValue = this.element.value;
const valuesAreSame = compareValue.trim() === currentValue.trim();
if (!valuesAreSame) {
event?.preventDefault();
}
return valuesAreSame;
}
/**
* Basic slugify of a string, updates the controlled element's value
* or can be used to simply return the transformed value.
* If a custom event with detail.value is provided, that value will be used
* instead of the field's value.
*/
slugify(event: CustomEvent<{ value: string }>, ignoreUpdate = false) {
const unicodeSlugsEnabled = this.allowUnicodeValue;
const { value = this.element.value } = event?.detail || {};
const newValue = cleanForSlug(value.trim(), false, { unicodeSlugsEnabled });
if (!ignoreUpdate) {
this.element.value = newValue;
}
return newValue;
}
/**
* Advanced slugify of a string, updates the controlled element's value
* or can be used to simply return the transformed value.
* If a custom event with detail.value is provided, that value will be used
* instead of the field's value.
*/
urlify(event: CustomEvent<{ value: string }>, ignoreUpdate = false) {
const unicodeSlugsEnabled = this.allowUnicodeValue;
const { value = this.element.value } = event?.detail || {};
const newValue = cleanForSlug(value.trim(), true, { unicodeSlugsEnabled });
if (!ignoreUpdate) {
this.element.value = newValue;
}
return newValue;
}
}

Wyświetl plik

@ -0,0 +1,271 @@
import { Application } from '@hotwired/stimulus';
import { SyncController } from './SyncController';
jest.useFakeTimers();
describe('SyncController', () => {
let application;
describe('basic sync between two fields', () => {
beforeAll(() => {
application?.stop();
document.body.innerHTML = `
<section>
<input type="text" name="title" id="title" />
<input
type="date"
id="event-date"
name="event-date"
value="2025-07-22"
data-controller="w-sync"
data-action="change->w-sync#apply cut->w-sync#clear custom:event->w-sync#ping"
data-w-sync-target-value="#title"
/>
</section>`;
application = Application.start();
});
afterAll(() => {
document.body.innerHTML = '';
jest.clearAllMocks();
jest.clearAllTimers();
});
it('should dispatch a start event on targeted element', () => {
const startListener = jest.fn();
document
.getElementById('title')
.addEventListener('w-sync:start', startListener);
expect(startListener).not.toHaveBeenCalled();
application.register('w-sync', SyncController);
expect(startListener).toHaveBeenCalledTimes(1);
expect(startListener.mock.calls[0][0].detail).toEqual({
element: document.getElementById('event-date'),
value: '2025-07-22',
});
});
it('should allow the sync field to apply its value to the target element', () => {
const changeListener = jest.fn();
document
.getElementById('title')
.addEventListener('change', changeListener);
expect(document.getElementById('title').value).toEqual('');
expect(changeListener).not.toHaveBeenCalled();
application.register('w-sync', SyncController);
const dateInput = document.getElementById('event-date');
dateInput.value = '2025-05-05';
dateInput.dispatchEvent(new Event('change'));
jest.runAllTimers();
expect(document.getElementById('title').value).toEqual('2025-05-05');
expect(changeListener).toHaveBeenCalledTimes(1);
});
it('should allow for a simple ping against the target field that bubbles', () => {
const pingListener = jest.fn();
document.addEventListener('w-sync:ping', pingListener);
expect(pingListener).not.toHaveBeenCalled();
application.register('w-sync', SyncController);
const dateInput = document.getElementById('event-date');
dateInput.dispatchEvent(new CustomEvent('custom:event'));
expect(pingListener).toHaveBeenCalledTimes(1);
const event = pingListener.mock.calls[0][0];
expect(event.target).toEqual(document.getElementById('title'));
expect(event.detail).toEqual({
element: document.getElementById('event-date'),
value: '2025-05-05',
});
});
it('should allow the sync field to clear the value of the target element', () => {
const changeListener = jest.fn();
document
.getElementById('title')
.addEventListener('change', changeListener);
expect(document.getElementById('title').value).toEqual('2025-05-05');
expect(changeListener).not.toHaveBeenCalled();
application.register('w-sync', SyncController);
const dateInput = document.getElementById('event-date');
dateInput.dispatchEvent(new Event('cut'));
jest.runAllTimers();
expect(document.getElementById('title').value).toEqual('');
expect(changeListener).toHaveBeenCalledTimes(1);
});
it('should allow for no change events to be dispatched', () => {
const dateInput = document.getElementById('event-date');
dateInput.setAttribute('data-w-sync-quiet-value', 'true');
application.register('w-sync', SyncController);
dateInput.value = '2025-05-05';
dateInput.dispatchEvent(new Event('change'));
expect(dateInput.getAttribute('data-w-sync-quiet-value')).toBeTruthy();
expect(document.getElementById('title').value).toEqual('');
dateInput.value = '2025-05-05';
dateInput.dispatchEvent(new Event('cut'));
expect(document.getElementById('title').value).toEqual('');
});
});
describe('delayed sync between two fields', () => {
beforeAll(() => {
application?.stop();
document.body.innerHTML = `
<section>
<input type="text" name="title" id="title" />
<input
type="date"
id="event-date"
name="event-date"
value="2025-07-22"
data-controller="w-sync"
data-action="change->w-sync#apply cut->w-sync#clear"
data-w-sync-target-value="#title"
data-w-sync-delay-value="500"
/>
</section>`;
application = Application.start();
});
it('should delay the update on change based on the set value', () => {
application.register('w-sync', SyncController);
const dateInput = document.getElementById('event-date');
dateInput.value = '2025-05-05';
dateInput.dispatchEvent(new Event('cut'));
jest.advanceTimersByTime(500);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 500);
expect(document.getElementById('title').value).toEqual('');
jest.runAllTimers();
});
it('should delay the update on apply based on the set value', () => {
const changeListener = jest.fn();
document
.getElementById('title')
.addEventListener('change', changeListener);
expect(document.getElementById('title').value).toEqual('');
expect(changeListener).not.toHaveBeenCalled();
application.register('w-sync', SyncController);
const dateInput = document.getElementById('event-date');
dateInput.value = '2025-05-05';
dateInput.dispatchEvent(new Event('change'));
jest.advanceTimersByTime(500);
expect(setTimeout).toHaveBeenLastCalledWith(expect.any(Function), 500);
expect(document.getElementById('title').value).toEqual('2025-05-05');
expect(changeListener).toHaveBeenCalledTimes(1);
jest.runAllTimers();
});
});
describe('ability for the sync to be disabled between two fields', () => {
beforeAll(() => {
application?.stop();
document.body.innerHTML = `
<section>
<input type="text" name="title" id="title" value="keep me"/>
<input
type="date"
id="event-date"
name="event-date"
value="2025-07-22"
data-controller="w-sync"
data-action="change->w-sync#apply cut->w-sync#clear focus->w-sync#check"
data-w-sync-target-value="#title"
/>
</section>`;
application = Application.start();
});
it('should allow for the target element to block syncing at the start', () => {
const titleElement = document.getElementById('title');
expect(titleElement.value).toEqual('keep me');
titleElement.addEventListener('w-sync:start', (event) => {
event.preventDefault();
});
application.register('w-sync', SyncController);
const dateInput = document.getElementById('event-date');
dateInput.value = '2025-05-05';
dateInput.dispatchEvent(new Event('change'));
jest.runAllTimers();
expect(titleElement.value).toEqual('keep me');
expect(dateInput.getAttribute('data-w-sync-disabled-value')).toBeTruthy();
});
it('should allow for the target element to block syncing with the check approach', () => {
const titleElement = document.getElementById('title');
expect(titleElement.value).toEqual('keep me');
titleElement.addEventListener('w-sync:check', (event) => {
event.preventDefault();
});
application.register('w-sync', SyncController);
const dateInput = document.getElementById('event-date');
dateInput.setAttribute('data-w-sync-disabled-value', '');
dateInput.value = '2025-05-05';
dateInput.dispatchEvent(new Event('focus'));
dateInput.dispatchEvent(new Event('cut'));
jest.runAllTimers();
expect(titleElement.value).toEqual('keep me');
expect(dateInput.getAttribute('data-w-sync-disabled-value')).toBeTruthy();
});
});
});

Wyświetl plik

@ -0,0 +1,130 @@
import { Controller } from '@hotwired/stimulus';
/**
* Adds ability to sync the value or interactions with one input with one
* or more targeted other inputs.
*
* @example
* <section>
* <input type="text" name="title" id="title" />
* <input
* type="date"
* id="event-date"
* name="event-date"
* value="2025-07-22"
* data-controller="w-sync"
* data-action="change->w-sync#apply cut->w-sync#clear focus->w-sync#check"
* data-w-sync-target-value="#title"
* />
* </section>
*/
export class SyncController extends Controller<HTMLInputElement> {
static values = {
delay: { default: 0, type: Number },
disabled: { default: false, type: Boolean },
quiet: { default: false, type: Boolean },
target: String,
};
declare delayValue: number;
declare disabledValue: boolean;
declare quietValue: boolean;
declare readonly targetValue: string;
/**
* Dispatches an event to all target elements so that they can be notified
* that a sync has started, allowing them to disable the sync by preventing
* default.
*/
connect() {
this.processTargetElements('start', true);
}
/**
* Allows for targeted elements to determine, via preventing the default event,
* whether this sync controller should be disabled.
*/
check() {
this.processTargetElements('check', true);
}
/**
* Applies a value from the controlled element to the targeted
* elements.
*/
apply() {
this.processTargetElements('apply').forEach((target) => {
setTimeout(() => {
target.setAttribute('value', this.element.value);
if (this.quietValue) return;
this.dispatch('change', {
cancelable: false,
prefix: '',
target: target as HTMLInputElement,
});
}, this.delayValue);
});
}
/**
* Clears the value of the targeted elements.
*/
clear() {
this.processTargetElements('clear').forEach((target) => {
setTimeout(() => {
target.setAttribute('value', '');
if (this.quietValue) return;
this.dispatch('change', {
cancelable: false,
prefix: '',
target: target as HTMLInputElement,
});
}, this.delayValue);
});
}
/**
* Simple method to dispatch a ping event to the targeted elements.
*/
ping() {
this.processTargetElements('ping', false, { bubbles: true });
}
/**
* Returns the non-default prevented elements that are targets of this sync
* controller. Additionally allows this processing to enable or disable
* this controller instance's sync behaviour.
*/
processTargetElements(
eventName: string,
resetDisabledValue = false,
options = {},
) {
if (!resetDisabledValue && this.disabledValue) {
return [];
}
const targetElements = [
...document.querySelectorAll<HTMLElement>(this.targetValue),
];
const elements = targetElements.filter((target) => {
const event = this.dispatch(eventName, {
bubbles: false,
cancelable: true,
...options, // allow overriding some options but not detail & target
detail: { element: this.element, value: this.element.value },
target: target as HTMLInputElement,
});
return !event.defaultPrevented;
});
if (resetDisabledValue) {
this.disabledValue = targetElements.length > elements.length;
}
return elements;
}
}

Wyświetl plik

@ -8,6 +8,7 @@ import { ProgressController } from './ProgressController';
import { SkipLinkController } from './SkipLinkController';
import { SlugController } from './SlugController';
import { SubmitController } from './SubmitController';
import { SyncController } from './SyncController';
import { UpgradeController } from './UpgradeController';
/**
@ -22,5 +23,6 @@ export const coreControllerDefinitions: Definition[] = [
{ controllerConstructor: SkipLinkController, identifier: 'w-skip-link' },
{ controllerConstructor: SlugController, identifier: 'w-slug' },
{ controllerConstructor: SubmitController, identifier: 'w-submit' },
{ controllerConstructor: SyncController, identifier: 'w-sync' },
{ controllerConstructor: UpgradeController, identifier: 'w-upgrade' },
];

Wyświetl plik

@ -1,33 +1,9 @@
import $ from 'jquery';
import { cleanForSlug } from '../../utils/text';
import { InlinePanel } from '../../components/InlinePanel';
import { MultipleChooserPanel } from '../../components/MultipleChooserPanel';
window.InlinePanel = InlinePanel;
window.MultipleChooserPanel = MultipleChooserPanel;
window.cleanForSlug = cleanForSlug;
function initSlugAutoPopulate() {
let slugFollowsTitle = false;
// eslint-disable-next-line func-names
$('#id_title').on('focus', function () {
/* slug should only follow the title field if its value matched the title's value at the time of focus */
const currentSlug = $('#id_slug').val();
const slugifiedTitle = cleanForSlug(this.value, true);
slugFollowsTitle = currentSlug === slugifiedTitle;
});
// eslint-disable-next-line func-names
$('#id_title').on('keyup keydown keypress blur', function () {
if (slugFollowsTitle) {
const slugifiedTitle = cleanForSlug(this.value, true);
$('#id_slug').val(slugifiedTitle);
}
});
}
window.initSlugAutoPopulate = initSlugAutoPopulate;
function initKeyboardShortcuts() {
// eslint-disable-next-line no-undef
@ -49,10 +25,6 @@ function initKeyboardShortcuts() {
window.initKeyboardShortcuts = initKeyboardShortcuts;
$(() => {
/* Only non-live pages should auto-populate the slug from the title */
if (!$('body').hasClass('page-is-live')) {
initSlugAutoPopulate();
}
initKeyboardShortcuts();
});

Wyświetl plik

@ -112,6 +112,7 @@ Support for adding custom validation logic to StreamField blocks has been formal
* Move snippet choosers and model check registration to `SnippetViewSet.on_register()` (Sage Abdullah)
* Remove unused snippets delete-multiple view (Sage Abdullah)
* Improve performance of determining live page URLs across the admin interface using [`pageurl` template tag](performance_page_urls) (Satvik Vashisht)
* Migrate `window.initSlugAutoPopulate` behaviour to a Stimulus Controller `w-sync` (Loveth Omokaro)
## Upgrade considerations
@ -212,6 +213,33 @@ To allow unicode values, add the data attribute value;
/>
```
### Title field auto sync with the slug field on Pages is now implemented with Stimulus
The title field will sync its value with the slug field on Pages if the Page is not published and the slug has not been manually changed. This JavaScript behaviour previously attached to any field with an ID of `id_title`, this has now changed to be any field with the appropriate Stimulus data attributes.
If your project is using the built in `title` and `slug` fields without any customisations, you will not need to do anything differently. However, if you were relying on this behaviour or the `window.initSlugAutoPopulate` global, please read ahead as you may need to make some changes to adopt the new approach.
There is a new Stimulus controller `w-sync` which allows any field to change one or more other fields when its value changes, the other field in this case will be the slug field (`w-slug`) with the id `id_slug`.
If you need to hook into this behaviour, the new approach will now correctly dispatch `change` events on the slug field. Alternatively, you can modify the data attributes on the fields to adjust this behaviour.
To adjust the target field (the one to be updated), you cam modify `"data-w-sync-target-value"`, the default being `"body:not(.page-is-live) [data-edit-form] #id_slug"` (find the field with id `id_slug` when the page is not live).
To adjust what triggers the initial check (to see if the fields should be in sync), or the trigger to to the sync, you can use the Stimulus `data-action` attributes.
```html
<input
id="id_title"
type="text"
name="title"
data-controller="w-sync"
data-action="focus->w-sync#check blur->w-sync#apply change->w-sync#apply"
data-w-sync-target-value="body:not(.page-is-live) #some_other_slug"
/>
```
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.
### Progress button (`button-longrunning`) now implemented with Stimulus
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.
@ -281,14 +309,12 @@ document.dispatchEvent(
Note that this event name may change in the future and this functionality is still not officially supported.
### Changes to StreamField `ValidationError` classes
The client-side handling of StreamField validation errors has been updated. The JavaScript classes `StreamBlockValidationError`, `ListBlockValidationError`, `StructBlockValidationError` and `TypedTableBlockValidationError` have been removed, and the corresponding Python classes can no longer be serialised using Telepath. Instead, the `setError` methods on client-side block objects now accept a plain JSON representation of the error, obtained from the `as_json_data` method on the Python class. Custom JavaScript code that works with these objects must be updated accordingly.
Additionally, the Python `StreamBlockValidationError`, `ListBlockValidationError`, `StructBlockValidationError` and `TypedTableBlockValidationError` classes no longer provide a `params` dict with `block_errors` and `non_block_errors` items; these are now available as the attributes `block_errors` and `non_block_errors` on the exception itself (or `cell_errors` and `non_block_errors` in the case of `TypedTableBlockValidationError`).
### Snippets `delete-multiple` view removed
The ability to remove multiple snippet instances from the `DeleteView` and the undocumented `wagtailsnippets_{app_label}_{model_name}:delete-multiple` URL pattern have been removed. The view's functionality has been replaced by the delete action of the bulk actions feature introduced in Wagtail 4.0.

Wyświetl plik

@ -23,7 +23,11 @@ def set_default_page_edit_handlers(cls):
attrs={
"placeholder": format_lazy(
"{title}*", title=gettext_lazy("Page title")
)
),
"data-controller": "w-sync",
"data-action": "focus->w-sync#check blur->w-sync#apply",
# ensure that if the page is live, the slug field does not receive updates from changes to the title field
"data-w-sync-target-value": "body:not(.page-is-live) [data-edit-form] #id_slug",
}
),
),

Wyświetl plik

@ -4,7 +4,6 @@
JavaScript declarations to be included on the 'create page' and 'edit page' views
{% endcomment %}
{% allow_unicode_slugs as unicode_slugs_enabled %}
<script>
window.chooserUrls = {
'pageChooser': '{% url "wagtailadmin_choose_page" %}',
@ -13,7 +12,6 @@
'phoneLinkChooser': '{% url "wagtailadmin_choose_page_phone_link" %}',
'anchorLinkChooser': '{% url "wagtailadmin_choose_page_anchor_link" %}',
};
window.unicodeSlugsEnabled = {% if unicode_slugs_enabled %}true{% else %}false{% endif %};
</script>
<script src="{% versioned_static 'wagtailadmin/js/comments.js' %}"></script>

Wyświetl plik

@ -1579,7 +1579,7 @@ class TestPageEdit(WagtailTestUtils, TestCase):
reverse("wagtailadmin_pages:edit", args=(self.child_page.id,))
)
input_field_for_draft_slug = '<input type="text" name="slug" value="revised-slug-in-draft-only" data-controller="w-slug" data-action="blur-&gt;w-slug#slugify" data-w-slug-allow-unicode-value maxlength="255" aria-describedby="panel-child-promote-child-for_search_engines-child-slug-helptext" required id="id_slug">'
input_field_for_draft_slug = '<input type="text" name="slug" value="revised-slug-in-draft-only" data-controller="w-slug" data-action="blur-&gt;w-slug#slugify w-sync:check-&gt;w-slug#compare w-sync:apply-&gt;w-slug#urlify:prevent" data-w-slug-allow-unicode-value maxlength="255" aria-describedby="panel-child-promote-child-for_search_engines-child-slug-helptext" required id="id_slug">'
input_field_for_live_slug = '<input type="text" name="slug" value="hello-world" maxlength="255" aria-describedby="panel-child-promote-child-for_search_engines-child-slug-helptext" required id="id_slug" />'
# Status Link should be the live page (not revision)

Wyświetl plik

@ -651,7 +651,7 @@ class TestSlugInput(TestCase):
html = widget.render("test", None, attrs={"id": "test-id"})
self.assertInHTML(
'<input type="text" name="test" data-controller="w-slug" data-action="blur-&gt;w-slug#slugify" data-w-slug-allow-unicode-value id="test-id">',
'<input type="text" name="test" data-controller="w-slug" data-action="blur-&gt;w-slug#slugify w-sync:check-&gt;w-slug#compare w-sync:apply-&gt;w-slug#urlify:prevent" data-w-slug-allow-unicode-value id="test-id">',
html,
)

Wyświetl plik

@ -6,7 +6,7 @@ class SlugInput(widgets.TextInput):
def __init__(self, attrs=None):
default_attrs = {
"data-controller": "w-slug",
"data-action": "blur->w-slug#slugify",
"data-action": "blur->w-slug#slugify w-sync:check->w-slug#compare w-sync:apply->w-slug#urlify:prevent",
"data-w-slug-allow-unicode-value": getattr(
settings, "WAGTAIL_ALLOW_UNICODE_SLUGS", True
),