kopia lustrzana https://github.com/wagtail/wagtail
Merge bdbdc84ba5
into 74b5933ae7
commit
c20f761c67
|
@ -0,0 +1,116 @@
|
|||
import React, { useCallback } from 'react';
|
||||
|
||||
import { StimulusWrapper } from '../../storybook/StimulusWrapper';
|
||||
import { CleanController } from './CleanController';
|
||||
|
||||
export default {
|
||||
title: 'Stimulus / CleanController',
|
||||
argTypes: {
|
||||
debug: {
|
||||
control: 'boolean',
|
||||
defaultValue: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const definitions = [
|
||||
{
|
||||
identifier: 'w-clean',
|
||||
controllerConstructor: CleanController,
|
||||
},
|
||||
];
|
||||
|
||||
const Template = ({ debug = false }) => {
|
||||
const [sourceValues, setSourceValue] = React.useState({});
|
||||
return (
|
||||
<StimulusWrapper debug={debug} definitions={definitions}>
|
||||
<form
|
||||
onSubmit={(event) => {
|
||||
event.preventDefault();
|
||||
}}
|
||||
ref={useCallback((node) => {
|
||||
node.addEventListener(
|
||||
'w-clean:applied',
|
||||
({ target, detail: { sourceValue } }) => {
|
||||
setSourceValue((state) => ({
|
||||
...state,
|
||||
[target.id]: sourceValue,
|
||||
}));
|
||||
},
|
||||
);
|
||||
}, [])}
|
||||
>
|
||||
<fieldset>
|
||||
<legend>
|
||||
Focus and then remove focus (blur) on fields to see changes, trim is
|
||||
enabled for all.
|
||||
</legend>
|
||||
<div className="w-m-4">
|
||||
<label htmlFor="slugify">
|
||||
<pre>slugify</pre>
|
||||
<input
|
||||
id="slugify-default"
|
||||
type="text"
|
||||
data-controller="w-clean"
|
||||
data-action="blur->w-clean#slugify"
|
||||
data-w-clean-trim-value
|
||||
/>
|
||||
<output className="w-inline-flex w-items-center">
|
||||
Source value: <pre>{sourceValues['slugify-default']}</pre>
|
||||
</output>
|
||||
</label>
|
||||
</div>
|
||||
<div className="w-m-4">
|
||||
<label htmlFor="slugify-unicode">
|
||||
<pre>slugify (allow unicode)</pre>
|
||||
<input
|
||||
id="slugify-unicode"
|
||||
type="text"
|
||||
data-controller="w-clean"
|
||||
data-action="blur->w-clean#slugify"
|
||||
data-w-clean-allow-unicode-value
|
||||
data-w-clean-trim-value
|
||||
/>
|
||||
<output className="w-inline-flex w-items-center">
|
||||
Source value: <pre>{sourceValues['slugify-unicode']}</pre>
|
||||
</output>
|
||||
</label>
|
||||
</div>
|
||||
<div className="w-m-4">
|
||||
<label htmlFor="urlify-default">
|
||||
<pre>urlify</pre>
|
||||
<input
|
||||
id="urlify-default"
|
||||
type="text"
|
||||
data-controller="w-clean"
|
||||
data-action="blur->w-clean#urlify"
|
||||
data-w-clean-trim-value
|
||||
/>
|
||||
<output className="w-inline-flex w-items-center">
|
||||
Source value: <pre>{sourceValues['urlify-default']}</pre>
|
||||
</output>
|
||||
</label>
|
||||
</div>
|
||||
<div className="w-m-4">
|
||||
<label htmlFor="urlify-unicode">
|
||||
<pre>urlify (allow unicode)</pre>
|
||||
<input
|
||||
id="urlify-unicode"
|
||||
type="text"
|
||||
data-controller="w-clean"
|
||||
data-action="blur->w-clean#urlify"
|
||||
data-w-clean-allow-unicode-value
|
||||
data-w-clean-trim-value
|
||||
/>
|
||||
<output className="w-inline-flex w-items-center">
|
||||
Source value: <pre>{sourceValues['urlify-unicode']}</pre>
|
||||
</output>
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</StimulusWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export const Base = Template.bind({});
|
|
@ -0,0 +1,484 @@
|
|||
import { Application } from '@hotwired/stimulus';
|
||||
import { CleanController } from './CleanController';
|
||||
|
||||
describe('CleanController', () => {
|
||||
let application;
|
||||
|
||||
const eventNames = ['w-clean:applied'];
|
||||
|
||||
const events = {};
|
||||
|
||||
eventNames.forEach((name) => {
|
||||
document.addEventListener(name, (event) => {
|
||||
events[name].push(event);
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
eventNames.forEach((name) => {
|
||||
events[name] = [];
|
||||
});
|
||||
});
|
||||
|
||||
describe('compare', () => {
|
||||
beforeEach(() => {
|
||||
application?.stop();
|
||||
|
||||
document.body.innerHTML = `
|
||||
<input
|
||||
id="slug"
|
||||
name="slug"
|
||||
type="text"
|
||||
data-controller="w-clean"
|
||||
/>`;
|
||||
|
||||
application = Application.start();
|
||||
application.register('w-clean', CleanController);
|
||||
|
||||
const input = document.getElementById('slug');
|
||||
|
||||
input.dataset.action = [
|
||||
'blur->w-clean#urlify',
|
||||
'custom:event->w-clean#compare',
|
||||
].join(' ');
|
||||
});
|
||||
|
||||
it('should not prevent default if input has no value', async () => {
|
||||
const event = new CustomEvent('custom:event', {
|
||||
detail: { value: 'title alpha' },
|
||||
});
|
||||
|
||||
event.preventDefault = jest.fn();
|
||||
|
||||
document.getElementById('slug').dispatchEvent(event);
|
||||
|
||||
await new Promise(process.nextTick);
|
||||
|
||||
expect(document.getElementById('slug').value).toBe('');
|
||||
expect(event.preventDefault).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not prevent default if the values are the same', async () => {
|
||||
document.getElementById('slug').setAttribute('value', 'title-alpha');
|
||||
|
||||
const event = new CustomEvent('custom:event', {
|
||||
detail: { value: 'title alpha' },
|
||||
});
|
||||
|
||||
event.preventDefault = jest.fn();
|
||||
|
||||
document.getElementById('slug').dispatchEvent(event);
|
||||
|
||||
await new Promise(process.nextTick);
|
||||
|
||||
expect(event.preventDefault).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should prevent default using the slugify (default) behavior as the compare function when urlify values is not equal', async () => {
|
||||
const input = document.getElementById('slug');
|
||||
|
||||
const title = 'Тестовий заголовок';
|
||||
|
||||
input.setAttribute('value', title);
|
||||
|
||||
// apply the urlify method to the content to ensure the value before check is urlify
|
||||
input.dispatchEvent(new Event('blur'));
|
||||
|
||||
await new Promise(process.nextTick);
|
||||
|
||||
expect(input.value).toEqual('testovij-zagolovok');
|
||||
|
||||
const event = new CustomEvent('custom:event', {
|
||||
detail: { value: title },
|
||||
});
|
||||
|
||||
event.preventDefault = jest.fn();
|
||||
|
||||
input.dispatchEvent(event);
|
||||
|
||||
await new Promise(process.nextTick);
|
||||
|
||||
// slugify used for the compareAs value by default, so 'compare' fails
|
||||
expect(event.preventDefault).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not prevent default using the slugify (default) behavior as the compare function when urlify value is equal', async () => {
|
||||
const input = document.getElementById('slug');
|
||||
|
||||
const title = 'the-french-dispatch-a-love-letter-to-journalists';
|
||||
|
||||
input.setAttribute('value', title);
|
||||
|
||||
// apply the urlify method to the content to ensure the value before check is urlify
|
||||
input.dispatchEvent(new Event('blur'));
|
||||
|
||||
expect(input.value).toEqual(
|
||||
'the-french-dispatch-a-love-letter-to-journalists',
|
||||
);
|
||||
|
||||
const event = new CustomEvent('custom:event', {
|
||||
detail: { value: title },
|
||||
});
|
||||
|
||||
event.preventDefault = jest.fn();
|
||||
|
||||
input.dispatchEvent(event);
|
||||
|
||||
await new Promise(process.nextTick);
|
||||
|
||||
// slugify used for the compareAs value by default, so 'compare' passes with the initial urlify value on blur
|
||||
expect(event.preventDefault).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not prevent default using the urlify behavior as the compare function when urlify value matches', async () => {
|
||||
const title = 'Тестовий заголовок';
|
||||
|
||||
const input = document.getElementById('slug');
|
||||
|
||||
input.setAttribute('data-w-clean-compare-as-param', 'urlify');
|
||||
input.setAttribute('value', title);
|
||||
|
||||
// apply the urlify method to the content to ensure the value before check is urlify
|
||||
input.dispatchEvent(new Event('blur'));
|
||||
|
||||
await new Promise(process.nextTick);
|
||||
|
||||
expect(input.value).toEqual('testovij-zagolovok');
|
||||
|
||||
const event = new CustomEvent('custom:event', {
|
||||
detail: { compareAs: 'urlify', value: title },
|
||||
});
|
||||
|
||||
event.preventDefault = jest.fn();
|
||||
|
||||
input.dispatchEvent(event);
|
||||
|
||||
await new Promise(process.nextTick);
|
||||
|
||||
expect(event.preventDefault).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should prevent default if the values are not the same', async () => {
|
||||
document.getElementById('slug').setAttribute('value', 'title-alpha');
|
||||
|
||||
const event = new CustomEvent('custom:event', {
|
||||
detail: { value: 'title beta' },
|
||||
});
|
||||
|
||||
event.preventDefault = jest.fn();
|
||||
|
||||
document.getElementById('slug').dispatchEvent(event);
|
||||
|
||||
await new Promise(process.nextTick);
|
||||
|
||||
expect(event.preventDefault).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not prevent default if both values are empty strings', async () => {
|
||||
const input = document.getElementById('slug');
|
||||
input.setAttribute('value', '');
|
||||
|
||||
const event = new CustomEvent('custom:event', {
|
||||
detail: { value: '' },
|
||||
});
|
||||
|
||||
event.preventDefault = jest.fn();
|
||||
|
||||
input.dispatchEvent(event);
|
||||
|
||||
await new Promise(process.nextTick);
|
||||
|
||||
expect(event.preventDefault).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should prevent default if the new value is an empty string but the existing value is not', async () => {
|
||||
const input = document.getElementById('slug');
|
||||
input.setAttribute('value', 'existing-value');
|
||||
|
||||
const event = new CustomEvent('custom:event', {
|
||||
detail: { value: '' },
|
||||
});
|
||||
|
||||
event.preventDefault = jest.fn();
|
||||
|
||||
input.dispatchEvent(event);
|
||||
|
||||
await new Promise(process.nextTick);
|
||||
|
||||
expect(event.preventDefault).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should allow the compare as identity to ensure that the values are always considered equal', async () => {
|
||||
expect(events['w-clean:applied']).toHaveLength(0);
|
||||
|
||||
const input = document.getElementById('slug');
|
||||
input.setAttribute('data-w-clean-compare-as-param', 'identity');
|
||||
|
||||
input.value = 'title-alpha';
|
||||
|
||||
const event = new CustomEvent('custom:event', {
|
||||
detail: { value: 'title beta' },
|
||||
});
|
||||
|
||||
event.preventDefault = jest.fn();
|
||||
|
||||
input.dispatchEvent(event);
|
||||
|
||||
await new Promise(process.nextTick);
|
||||
|
||||
expect(event.preventDefault).not.toHaveBeenCalled();
|
||||
expect(events['w-clean:applied']).toHaveLength(1);
|
||||
expect(events['w-clean:applied']).toHaveProperty('0.detail', {
|
||||
action: 'identity',
|
||||
cleanValue: 'title-alpha',
|
||||
sourceValue: 'title-alpha',
|
||||
});
|
||||
|
||||
// now use the compare from the event detail
|
||||
input.removeAttribute('data-w-clean-compare-as-param');
|
||||
|
||||
input.value = 'title-delta';
|
||||
|
||||
const event2 = new CustomEvent('custom:event', {
|
||||
detail: { value: 'title whatever', compareAs: 'identity' },
|
||||
});
|
||||
|
||||
event2.preventDefault = jest.fn();
|
||||
|
||||
input.dispatchEvent(event2);
|
||||
|
||||
await new Promise(process.nextTick);
|
||||
|
||||
expect(event2.preventDefault).not.toHaveBeenCalled();
|
||||
expect(events['w-clean:applied']).toHaveLength(2);
|
||||
expect(events['w-clean:applied']).toHaveProperty('1.detail', {
|
||||
action: 'identity',
|
||||
cleanValue: 'title-delta',
|
||||
sourceValue: 'title-delta',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('slugify', () => {
|
||||
beforeEach(() => {
|
||||
application?.stop();
|
||||
|
||||
document.body.innerHTML = `
|
||||
<input
|
||||
id="slug"
|
||||
name="slug"
|
||||
type="text"
|
||||
data-controller="w-clean"
|
||||
data-action="blur->w-clean#slugify"
|
||||
/>`;
|
||||
|
||||
application = Application.start();
|
||||
application.register('w-clean', CleanController);
|
||||
});
|
||||
|
||||
it('should trim and slugify the input value when focus is moved away from it', async () => {
|
||||
expect(events['w-clean:applied']).toHaveLength(0);
|
||||
|
||||
const input = document.getElementById('slug');
|
||||
input.value = ' slug testing on edit page ';
|
||||
|
||||
input.dispatchEvent(new CustomEvent('blur'));
|
||||
|
||||
await new Promise(process.nextTick);
|
||||
|
||||
expect(document.getElementById('slug').value).toEqual(
|
||||
'-slug-testing-on-edit-page-', // non-trimmed adds dashes for all spaces (inc. end/start)
|
||||
);
|
||||
|
||||
expect(events['w-clean:applied']).toHaveLength(1);
|
||||
expect(events['w-clean:applied']).toHaveProperty('0.detail', {
|
||||
action: 'slugify',
|
||||
cleanValue: '-slug-testing-on-edit-page-', // non-trimmed adds dashes for all spaces (inc. end/start)
|
||||
sourceValue: ' slug testing on edit page ',
|
||||
});
|
||||
});
|
||||
|
||||
it('should slugify & trim (when enabled) the input value when focus is moved away from it', async () => {
|
||||
expect(events['w-clean:applied']).toHaveLength(0);
|
||||
|
||||
const input = document.getElementById('slug');
|
||||
|
||||
input.setAttribute('data-w-clean-trim-value', 'true'); // enable trimmed values
|
||||
|
||||
input.value = ' slug testing on edit page ';
|
||||
|
||||
input.dispatchEvent(new CustomEvent('blur'));
|
||||
|
||||
await new Promise(process.nextTick);
|
||||
|
||||
expect(document.getElementById('slug').value).toEqual(
|
||||
'slug-testing-on-edit-page',
|
||||
);
|
||||
|
||||
expect(events['w-clean:applied']).toHaveLength(1);
|
||||
expect(events['w-clean:applied']).toHaveProperty('0.detail', {
|
||||
action: 'slugify',
|
||||
cleanValue: 'slug-testing-on-edit-page',
|
||||
sourceValue: ' slug testing on edit page ',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not allow unicode characters by default', async () => {
|
||||
const input = document.getElementById('slug');
|
||||
|
||||
expect(
|
||||
input.hasAttribute('data-w-clean-allow-unicode-value'),
|
||||
).toBeFalsy();
|
||||
|
||||
input.value = 'Visiter Toulouse en été 2025';
|
||||
|
||||
input.dispatchEvent(new CustomEvent('blur'));
|
||||
|
||||
await new Promise(process.nextTick);
|
||||
|
||||
expect(input.value).toEqual('visiter-toulouse-en-t-2025');
|
||||
});
|
||||
|
||||
it('should allow unicode characters when allow-unicode-value is set to truthy', async () => {
|
||||
const input = document.getElementById('slug');
|
||||
input.setAttribute('data-w-clean-allow-unicode-value', 'true');
|
||||
|
||||
expect(
|
||||
input.hasAttribute('data-w-clean-allow-unicode-value'),
|
||||
).toBeTruthy();
|
||||
|
||||
input.value = 'Visiter Toulouse en été 2025';
|
||||
|
||||
input.dispatchEvent(new CustomEvent('blur'));
|
||||
|
||||
await new Promise(process.nextTick);
|
||||
|
||||
expect(input.value).toEqual('visiter-toulouse-en-été-2025');
|
||||
});
|
||||
});
|
||||
|
||||
describe('urlify', () => {
|
||||
beforeEach(() => {
|
||||
application?.stop();
|
||||
|
||||
document.body.innerHTML = `
|
||||
<input
|
||||
id="slug"
|
||||
name="slug"
|
||||
type="text"
|
||||
data-controller="w-clean"
|
||||
/>`;
|
||||
|
||||
application = Application.start();
|
||||
application.register('w-clean', CleanController);
|
||||
|
||||
const input = document.getElementById('slug');
|
||||
|
||||
input.dataset.action = [
|
||||
'blur->w-clean#slugify',
|
||||
'custom:event->w-clean#urlify:prevent',
|
||||
].join(' ');
|
||||
});
|
||||
|
||||
it('should update slug input value if the values are the same', async () => {
|
||||
expect(events['w-clean:applied']).toHaveLength(0);
|
||||
|
||||
const input = document.getElementById('slug');
|
||||
input.value = 'urlify Testing On edit page ';
|
||||
|
||||
const event = new CustomEvent('custom:event', {
|
||||
detail: { value: 'urlify Testing On edit page' },
|
||||
bubbles: false,
|
||||
});
|
||||
|
||||
document.getElementById('slug').dispatchEvent(event);
|
||||
|
||||
await new Promise(process.nextTick);
|
||||
|
||||
expect(input.value).toBe('urlify-testing-on-edit-page');
|
||||
|
||||
expect(events['w-clean:applied']).toHaveLength(1);
|
||||
expect(events['w-clean:applied']).toHaveProperty('0.detail', {
|
||||
action: 'urlify',
|
||||
cleanValue: 'urlify-testing-on-edit-page',
|
||||
sourceValue: 'urlify Testing On edit page',
|
||||
});
|
||||
});
|
||||
|
||||
it('should transform input with special (unicode) characters to their ASCII equivalent by default', async () => {
|
||||
const input = document.getElementById('slug');
|
||||
input.value = 'Some Title with éçà Spaces';
|
||||
|
||||
const event = new CustomEvent('custom:event', {
|
||||
detail: { value: 'Some Title with éçà Spaces' },
|
||||
});
|
||||
|
||||
document.getElementById('slug').dispatchEvent(event);
|
||||
|
||||
await new Promise(process.nextTick);
|
||||
|
||||
expect(input.value).toBe('some-title-with-eca-spaces');
|
||||
});
|
||||
|
||||
it('should transform input with special (unicode) characters to keep unicode values if allow unicode value is truthy', async () => {
|
||||
const value = 'Dê-me fatias de pizza de manhã --ou-- à noite';
|
||||
|
||||
const input = document.getElementById('slug');
|
||||
input.setAttribute('data-w-clean-allow-unicode-value', 'true');
|
||||
|
||||
input.value = value;
|
||||
|
||||
const event = new CustomEvent('custom:event', { detail: { value } });
|
||||
|
||||
document.getElementById('slug').dispatchEvent(event);
|
||||
|
||||
await new Promise(process.nextTick);
|
||||
|
||||
expect(input.value).toBe('dê-me-fatias-de-pizza-de-manhã-ou-à-noite');
|
||||
});
|
||||
|
||||
it('should return an empty string when input contains only special characters', async () => {
|
||||
const input = document.getElementById('slug');
|
||||
input.value = '$$!@#$%^&*';
|
||||
|
||||
const event = new CustomEvent('custom:event', {
|
||||
detail: { value: '$$!@#$%^&*' },
|
||||
});
|
||||
|
||||
document.getElementById('slug').dispatchEvent(event);
|
||||
|
||||
await new Promise(process.nextTick);
|
||||
|
||||
expect(input.value).toBe('');
|
||||
});
|
||||
|
||||
it('should trim the value, only if trim is enabled', async () => {
|
||||
const testValue = ' I féta eínai kalýteri . ';
|
||||
|
||||
const input = document.getElementById('slug');
|
||||
|
||||
// the default behavior, with trim disabled
|
||||
input.value = testValue;
|
||||
|
||||
input.dispatchEvent(new Event('blur'));
|
||||
await new Promise(process.nextTick);
|
||||
expect(input.value).toBe('-i-fta-enai-kalteri--');
|
||||
|
||||
// after enabling trim
|
||||
input.setAttribute('data-w-clean-trim-value', 'true');
|
||||
input.value = testValue;
|
||||
|
||||
input.dispatchEvent(new Event('blur'));
|
||||
await new Promise(process.nextTick);
|
||||
expect(input.value).toBe('i-fta-enai-kalteri-');
|
||||
|
||||
// with unicode allowed & trim enabled
|
||||
input.setAttribute('data-w-clean-allow-unicode-value', 'true');
|
||||
input.value = testValue;
|
||||
|
||||
input.dispatchEvent(new Event('blur'));
|
||||
await new Promise(process.nextTick);
|
||||
expect(input.value).toBe('i-féta-eínai-kalýteri-');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,180 @@
|
|||
import { Controller } from '@hotwired/stimulus';
|
||||
import { slugify } from '../utils/slugify';
|
||||
import { urlify } from '../utils/urlify';
|
||||
|
||||
enum Actions {
|
||||
Identity = 'identity',
|
||||
Slugify = 'slugify',
|
||||
Urlify = 'urlify',
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds ability to clean values of an input element with methods such as slugify or urlify.
|
||||
*
|
||||
* @example - Using the slugify method
|
||||
* ```html
|
||||
* <input type="text" name="slug" data-controller="w-clean" data-action="blur->w-clean#slugify" />
|
||||
* <input type="text" name="slug-with-trim" data-controller="w-clean" data-action="blur->w-clean#slugify" data-w-slug-trim-value="true" />
|
||||
* ```
|
||||
*
|
||||
* @example - Using the urlify method (registered as w-slug)
|
||||
* ```html
|
||||
* <input type="text" name="url-path" data-controller="w-slug" data-action="change->w-slug#urlify" />
|
||||
* <input type="text" name="url-path-with-unicode" data-controller="w-slug" data-w-slug-allow-unicode="true" data-action="change->w-slug#urlify" />
|
||||
* ```
|
||||
*/
|
||||
export class CleanController extends Controller<HTMLInputElement> {
|
||||
static values = {
|
||||
allowUnicode: { default: false, type: Boolean },
|
||||
trim: { default: false, type: Boolean },
|
||||
};
|
||||
|
||||
/**
|
||||
* If true, unicode values in the cleaned values will be allowed.
|
||||
* Otherwise unicode values will try to be transliterated.
|
||||
* @see `WAGTAIL_ALLOW_UNICODE_SLUGS` in settings
|
||||
*/
|
||||
declare readonly allowUnicodeValue: boolean;
|
||||
/** If true, value will be trimmed in all clean methods before being processed by that method. */
|
||||
declare readonly trimValue: boolean;
|
||||
|
||||
/**
|
||||
* Writes the new value to the element & dispatches the applied event.
|
||||
*
|
||||
* @fires CleanController#applied - If a change applied to the input value, this event is dispatched.
|
||||
*
|
||||
* @event CleanController#applied
|
||||
* @type {CustomEvent}
|
||||
* @property {string} name - `w-slug:applied` | `w-clean:applied`
|
||||
* @property {Object} detail
|
||||
* @property {string} detail.action - The action that was applied (e.g. 'urlify' or 'slugify').
|
||||
* @property {string} detail.cleanValue - The the cleaned value that is applied.
|
||||
* @property {string} detail.sourceValue - The original value.
|
||||
*/
|
||||
applyUpdate(action: Actions, cleanValue: string, sourceValue?: string) {
|
||||
this.element.value = cleanValue;
|
||||
this.dispatch('applied', {
|
||||
cancelable: false,
|
||||
detail: { action, cleanValue, sourceValue },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow for a comparison value to be provided so that a dispatched event can be
|
||||
* prevented. This provides a way for other events to interact with this controller
|
||||
* to block further updates if a value is not in sync.
|
||||
* By default it will compare to the slugify method, this can be overridden by providing
|
||||
* either a Stimulus param value on the element or the event's detail.
|
||||
*/
|
||||
compare(
|
||||
event: CustomEvent<{ compareAs?: Actions; value: string }> & {
|
||||
params?: { compareAs?: Actions };
|
||||
},
|
||||
) {
|
||||
// do not attempt to compare if the field is empty
|
||||
if (!this.element.value) return true;
|
||||
|
||||
const compareAs =
|
||||
event.detail?.compareAs || event.params?.compareAs || Actions.Slugify;
|
||||
|
||||
const compareValue = this[compareAs](
|
||||
{ detail: { value: event.detail?.value || '' } },
|
||||
{ ignoreUpdate: true },
|
||||
);
|
||||
|
||||
const valuesAreSame = this.compareValues(compareValue, this.element.value);
|
||||
|
||||
if (!valuesAreSame) {
|
||||
event?.preventDefault();
|
||||
}
|
||||
|
||||
return valuesAreSame;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the provided strings, ensuring the values are the same.
|
||||
*/
|
||||
compareValues(...values: string[]): boolean {
|
||||
return new Set(values.map((value: string) => `${value}`)).size === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the element's value as is, without any modifications.
|
||||
* Useful for identity fields or when no cleaning is required but the event
|
||||
* is needed or comparison is required to always pass.
|
||||
*/
|
||||
identity() {
|
||||
const action = Actions.Identity;
|
||||
const value = this.element.value;
|
||||
this.applyUpdate(action, value, value);
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares the value before being processed by an action method.
|
||||
*/
|
||||
prepareValue(sourceValue = '') {
|
||||
const value = this.trimValue ? sourceValue.trim() : sourceValue;
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 }> | { detail: { value: string } },
|
||||
{ ignoreUpdate = false } = {},
|
||||
) {
|
||||
const { value: sourceValue = this.element.value } = event?.detail || {};
|
||||
const preparedValue = this.prepareValue(sourceValue);
|
||||
if (!preparedValue) return '';
|
||||
|
||||
const allowUnicode = this.allowUnicodeValue;
|
||||
|
||||
const cleanValue = slugify(preparedValue, { allowUnicode });
|
||||
|
||||
if (!ignoreUpdate) {
|
||||
this.applyUpdate(Actions.Slugify, cleanValue, sourceValue);
|
||||
}
|
||||
|
||||
return cleanValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Advanced slugify of a string, updates the controlled element's value
|
||||
* or can be used to simply return the transformed value.
|
||||
*
|
||||
* The urlify (Django port) function performs extra processing on the string &
|
||||
* is more suitable for creating a slug from the title, rather than sanitizing manually.
|
||||
* If the urlify util returns an empty string it will fall back to the slugify method.
|
||||
*
|
||||
* 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 }> | { detail: { value: string } },
|
||||
{ ignoreUpdate = false } = {},
|
||||
) {
|
||||
const { value: sourceValue = this.element.value } = event?.detail || {};
|
||||
const preparedValue = this.prepareValue(sourceValue);
|
||||
if (!preparedValue) return '';
|
||||
|
||||
const allowUnicode = this.allowUnicodeValue;
|
||||
|
||||
const cleanValue =
|
||||
urlify(preparedValue, { allowUnicode }) ||
|
||||
this.slugify(
|
||||
{ detail: { value: preparedValue } },
|
||||
{ ignoreUpdate: true },
|
||||
);
|
||||
|
||||
if (!ignoreUpdate) {
|
||||
this.applyUpdate(Actions.Urlify, cleanValue, sourceValue);
|
||||
}
|
||||
|
||||
return cleanValue;
|
||||
}
|
||||
}
|
|
@ -1,310 +0,0 @@
|
|||
import { Application } from '@hotwired/stimulus';
|
||||
import { SlugController } from './SlugController';
|
||||
|
||||
describe('SlugController', () => {
|
||||
let application;
|
||||
|
||||
beforeEach(() => {
|
||||
application?.stop();
|
||||
|
||||
document.body.innerHTML = `
|
||||
<input
|
||||
id="id_slug"
|
||||
name="slug"
|
||||
type="text"
|
||||
data-controller="w-slug"
|
||||
data-action="blur->w-slug#slugify"
|
||||
/>`;
|
||||
|
||||
application = Application.start();
|
||||
application.register('w-slug', SlugController);
|
||||
});
|
||||
|
||||
it('should trim and slugify the input value when focus is moved away from it', () => {
|
||||
const slugInput = document.querySelector('#id_slug');
|
||||
slugInput.value = ' slug testing on edit page ';
|
||||
|
||||
slugInput.dispatchEvent(new CustomEvent('blur'));
|
||||
|
||||
expect(document.querySelector('#id_slug').value).toEqual(
|
||||
'slug-testing-on-edit-page',
|
||||
);
|
||||
});
|
||||
|
||||
it('should not allow unicode characters by default', () => {
|
||||
const slugInput = document.querySelector('#id_slug');
|
||||
|
||||
expect(
|
||||
slugInput.hasAttribute('data-w-slug-allow-unicode-value'),
|
||||
).toBeFalsy();
|
||||
|
||||
slugInput.value = 'Visiter Toulouse en été 2025';
|
||||
|
||||
slugInput.dispatchEvent(new CustomEvent('blur'));
|
||||
|
||||
expect(slugInput.value).toEqual('visiter-toulouse-en-t-2025');
|
||||
});
|
||||
|
||||
it('should allow unicode characters when allow-unicode-value is set to truthy', () => {
|
||||
const slugInput = document.querySelector('#id_slug');
|
||||
slugInput.setAttribute('data-w-slug-allow-unicode-value', 'true');
|
||||
|
||||
expect(
|
||||
slugInput.hasAttribute('data-w-slug-allow-unicode-value'),
|
||||
).toBeTruthy();
|
||||
|
||||
slugInput.value = 'Visiter Toulouse en été 2025';
|
||||
|
||||
slugInput.dispatchEvent(new CustomEvent('blur'));
|
||||
|
||||
expect(slugInput.value).toEqual('visiter-toulouse-en-été-2025');
|
||||
});
|
||||
});
|
||||
|
||||
describe('compare behavior', () => {
|
||||
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#urlify',
|
||||
'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 using the slugify (default) behavior as the compare function when urlify values is not equal', () => {
|
||||
const slug = document.querySelector('#id_slug');
|
||||
|
||||
const title = 'Тестовий заголовок';
|
||||
|
||||
slug.setAttribute('value', title);
|
||||
|
||||
// apply the urlify method to the content to ensure the value before check is urlify
|
||||
slug.dispatchEvent(new Event('blur'));
|
||||
|
||||
expect(slug.value).toEqual('testovij-zagolovok');
|
||||
|
||||
const event = new CustomEvent('custom:event', { detail: { value: title } });
|
||||
|
||||
event.preventDefault = jest.fn();
|
||||
|
||||
slug.dispatchEvent(event);
|
||||
|
||||
// slugify used for the compareAs value by default, so 'compare' fails
|
||||
expect(event.preventDefault).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not prevent default using the slugify (default) behavior as the compare function when urlify value is equal', () => {
|
||||
const slug = document.querySelector('#id_slug');
|
||||
|
||||
const title = 'the-french-dispatch-a-love-letter-to-journalists';
|
||||
|
||||
slug.setAttribute('value', title);
|
||||
|
||||
// apply the urlify method to the content to ensure the value before check is urlify
|
||||
slug.dispatchEvent(new Event('blur'));
|
||||
|
||||
expect(slug.value).toEqual(
|
||||
'the-french-dispatch-a-love-letter-to-journalists',
|
||||
);
|
||||
|
||||
const event = new CustomEvent('custom:event', { detail: { value: title } });
|
||||
|
||||
event.preventDefault = jest.fn();
|
||||
|
||||
slug.dispatchEvent(event);
|
||||
|
||||
// slugify used for the compareAs value by default, so 'compare' passes with the initial urlify value on blur
|
||||
expect(event.preventDefault).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not prevent default using the urlify behavior as the compare function when urlify value matches', () => {
|
||||
const title = 'Тестовий заголовок';
|
||||
|
||||
const slug = document.querySelector('#id_slug');
|
||||
|
||||
slug.setAttribute('data-w-slug-compare-as-param', 'urlify');
|
||||
slug.setAttribute('value', title);
|
||||
|
||||
// apply the urlify method to the content to ensure the value before check is urlify
|
||||
slug.dispatchEvent(new Event('blur'));
|
||||
|
||||
expect(slug.value).toEqual('testovij-zagolovok');
|
||||
|
||||
const event = new CustomEvent('custom:event', {
|
||||
detail: { compareAs: 'urlify', value: title },
|
||||
});
|
||||
|
||||
event.preventDefault = jest.fn();
|
||||
|
||||
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 behavior', () => {
|
||||
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 (unicode) characters to their ASCII equivalent by default', () => {
|
||||
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 transform input with special (unicode) characters to keep unicode values if allow unicode value is truthy', () => {
|
||||
const value = 'Dê-me fatias de pizza de manhã --ou-- à noite';
|
||||
|
||||
const slugInput = document.getElementById('id_slug');
|
||||
slugInput.setAttribute('data-w-slug-allow-unicode-value', 'true');
|
||||
|
||||
slugInput.value = value;
|
||||
|
||||
const event = new CustomEvent('custom:event', { detail: { value } });
|
||||
|
||||
document.getElementById('id_slug').dispatchEvent(event);
|
||||
|
||||
expect(slugInput.value).toBe('dê-me-fatias-de-pizza-de-manhã-ou-à-noite');
|
||||
});
|
||||
|
||||
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('');
|
||||
});
|
||||
});
|
|
@ -1,108 +0,0 @@
|
|||
import { Controller } from '@hotwired/stimulus';
|
||||
import { slugify } from '../utils/slugify';
|
||||
import { urlify } from '../utils/urlify';
|
||||
|
||||
type SlugMethods = 'slugify' | 'urlify';
|
||||
|
||||
/**
|
||||
* Adds ability to slugify the value of an input element.
|
||||
*
|
||||
* @example
|
||||
* ```html
|
||||
* <input type="text" name="slug" data-controller="w-slug" data-action="blur->w-slug#slugify" />
|
||||
* ```
|
||||
*/
|
||||
export class SlugController extends Controller<HTMLInputElement> {
|
||||
static values = {
|
||||
allowUnicode: { default: false, type: Boolean },
|
||||
};
|
||||
|
||||
declare allowUnicodeValue: boolean;
|
||||
|
||||
/**
|
||||
* Allow for a comparison value to be provided so that a dispatched event can be
|
||||
* prevented. This provides a way for other events to interact with this controller
|
||||
* to block further updates if a value is not in sync.
|
||||
* By default it will compare to the slugify method, this can be overridden by providing
|
||||
* either a Stimulus param value on the element or the event's detail.
|
||||
*/
|
||||
compare(
|
||||
event: CustomEvent<{ compareAs?: SlugMethods; value: string }> & {
|
||||
params?: { compareAs?: SlugMethods };
|
||||
},
|
||||
) {
|
||||
// do not attempt to compare if the current field is empty
|
||||
if (!this.element.value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const compareAs =
|
||||
event.detail?.compareAs || event.params?.compareAs || 'slugify';
|
||||
|
||||
const compareValue = this[compareAs](
|
||||
{ detail: { value: event.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 }> | { detail: { value: string } },
|
||||
ignoreUpdate = false,
|
||||
) {
|
||||
const allowUnicode = this.allowUnicodeValue;
|
||||
const { value = this.element.value } = event?.detail || {};
|
||||
const newValue = slugify(value.trim(), { allowUnicode });
|
||||
|
||||
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.
|
||||
*
|
||||
* The urlify (Django port) function performs extra processing on the string &
|
||||
* is more suitable for creating a slug from the title, rather than sanitizing manually.
|
||||
* If the urlify util returns an empty string it will fall back to the slugify method.
|
||||
*
|
||||
* 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 }> | { detail: { value: string } },
|
||||
ignoreUpdate = false,
|
||||
) {
|
||||
const allowUnicode = this.allowUnicodeValue;
|
||||
const { value = this.element.value } = event?.detail || {};
|
||||
const trimmedValue = value.trim();
|
||||
|
||||
const newValue =
|
||||
urlify(trimmedValue, { allowUnicode }) ||
|
||||
this.slugify({ detail: { value: trimmedValue } }, true);
|
||||
|
||||
if (!ignoreUpdate) {
|
||||
this.element.value = newValue;
|
||||
}
|
||||
|
||||
return newValue;
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ import { ActionController } from './ActionController';
|
|||
import { AutosizeController } from './AutosizeController';
|
||||
import { BlockController } from './BlockController';
|
||||
import { BulkController } from './BulkController';
|
||||
import { CleanController } from './CleanController';
|
||||
import { ClipboardController } from './ClipboardController';
|
||||
import { CloneController } from './CloneController';
|
||||
import { CountController } from './CountController';
|
||||
|
@ -23,7 +24,6 @@ import { ProgressController } from './ProgressController';
|
|||
import { RevealController } from './RevealController';
|
||||
import { RulesController } from './RulesController';
|
||||
import { SessionController } from './SessionController';
|
||||
import { SlugController } from './SlugController';
|
||||
import { SubmitController } from './SubmitController';
|
||||
import { SwapController } from './SwapController';
|
||||
import { SyncController } from './SyncController';
|
||||
|
@ -43,6 +43,8 @@ export const coreControllerDefinitions: Definition[] = [
|
|||
{ controllerConstructor: AutosizeController, identifier: 'w-autosize' },
|
||||
{ controllerConstructor: BlockController, identifier: 'w-block' },
|
||||
{ controllerConstructor: BulkController, identifier: 'w-bulk' },
|
||||
{ controllerConstructor: CleanController, identifier: 'w-clean' },
|
||||
{ controllerConstructor: CleanController, identifier: 'w-slug' },
|
||||
{ controllerConstructor: ClipboardController, identifier: 'w-clipboard' },
|
||||
{ controllerConstructor: CloneController, identifier: 'w-clone' },
|
||||
{ controllerConstructor: CloneController, identifier: 'w-messages' },
|
||||
|
@ -63,7 +65,6 @@ export const coreControllerDefinitions: Definition[] = [
|
|||
{ controllerConstructor: RevealController, identifier: 'w-reveal' },
|
||||
{ controllerConstructor: RulesController, identifier: 'w-rules' },
|
||||
{ controllerConstructor: SessionController, identifier: 'w-session' },
|
||||
{ controllerConstructor: SlugController, identifier: 'w-slug' },
|
||||
{ controllerConstructor: SubmitController, identifier: 'w-submit' },
|
||||
{ controllerConstructor: SwapController, identifier: 'w-swap' },
|
||||
{ controllerConstructor: SyncController, identifier: 'w-sync' },
|
||||
|
|
|
@ -9,6 +9,10 @@ describe('slugify', () => {
|
|||
'lisboa--tima--beira-mar',
|
||||
);
|
||||
});
|
||||
|
||||
it('should keep leading spaces & convert to hyphens if supplied', () => {
|
||||
expect(slugify(' I like _ßpaces')).toBe('-i-like-_paces');
|
||||
});
|
||||
});
|
||||
|
||||
describe('slugify with unicode slugs enabled', () => {
|
||||
|
@ -33,5 +37,9 @@ describe('slugify', () => {
|
|||
);
|
||||
expect(slugify('উইকিপিডিয়ায় স্বাগতম!', options)).toBe('উইকপডযয-সবগতম');
|
||||
});
|
||||
|
||||
it('should keep leading spaces & convert to hyphens if supplied', () => {
|
||||
expect(slugify(' I like _ßpaces', options)).toBe('-i-like-_ßpaces');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,6 +9,10 @@ describe('urlify', () => {
|
|||
'lisboa-e-otima-a-beira-mar',
|
||||
);
|
||||
});
|
||||
|
||||
it('should keep leading spaces & convert to hyphens if supplied', () => {
|
||||
expect(urlify(' I like _ßpaces')).toBe('-i-like-_sspaces');
|
||||
});
|
||||
});
|
||||
|
||||
describe('urlify with unicode slugs enabled', () => {
|
||||
|
@ -30,5 +34,9 @@ describe('urlify', () => {
|
|||
'lisboa-é-ótima-à-beira-mar',
|
||||
);
|
||||
});
|
||||
|
||||
it('should keep leading spaces & convert to hyphens if supplied', () => {
|
||||
expect(urlify(' I like _ßpaces', options)).toBe('-i-like-_ßpaces');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,8 +12,9 @@ const downcodeMapping = config.reduce((acc, downcodeMap) => {
|
|||
const regex = new RegExp(Object.keys(downcodeMapping).join('|'), 'g');
|
||||
|
||||
/**
|
||||
* IMPORTANT This util and the mapping is a direct port of Django's urlify.js util,
|
||||
* without the need for a full Regex polyfill implementation.
|
||||
* This util and the mapping is refined port of Django's urlify.js util.
|
||||
* Without the Regex polyfill & without running trim (assuming the trim will be run before if needed).
|
||||
*
|
||||
* @see https://github.com/django/django/blob/main/django/contrib/admin/static/admin/js/urlify.js
|
||||
*/
|
||||
export const urlify = (
|
||||
|
@ -37,7 +38,6 @@ export const urlify = (
|
|||
} else {
|
||||
str = str.replace(/[^-\w\s]/g, ''); // remove unneeded chars
|
||||
}
|
||||
str = str.replace(/^\s+|\s+$/g, ''); // trim leading/trailing spaces
|
||||
str = str.replace(/[-\s]+/g, '-'); // convert spaces to hyphens
|
||||
str = str.substring(0, numChars); // trim to first num_chars chars
|
||||
str = str.replace(/-+$/g, ''); // trim any trailing hyphens
|
||||
|
|
|
@ -1925,8 +1925,8 @@ 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->w-slug#slugify w-sync:check->w-slug#compare w-sync:apply->w-slug#urlify:prevent" data-w-slug-compare-as-param="urlify" 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" data-controller="w-slug" data-action="blur->w-slug#slugify w-sync:check->w-slug#compare w-sync:apply->w-slug#urlify:prevent" data-w-slug-compare-as-param="urlify" 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->w-slug#slugify w-sync:check->w-slug#compare w-sync:apply->w-slug#urlify:prevent" data-w-slug-compare-as-param="urlify" data-w-slug-allow-unicode-value data-w-slug-trim-value="true" 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" data-controller="w-slug" data-action="blur->w-slug#slugify w-sync:check->w-slug#compare w-sync:apply->w-slug#urlify:prevent" data-w-slug-compare-as-param="urlify" data-w-slug-allow-unicode-value data-w-slug-trim-value="true" 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)
|
||||
self.assertNotContains(
|
||||
|
@ -1951,8 +1951,8 @@ class TestPageEdit(WagtailTestUtils, TestCase):
|
|||
reverse("wagtailadmin_pages:edit", args=(self.single_event_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->w-slug#slugify w-sync:check->w-slug#compare w-sync:apply->w-slug#urlify:prevent" data-w-slug-compare-as-param="urlify" data-w-slug-allow-unicode-value maxlength="255" aria-describedby="panel-child-promote-child-common_page_configuration-child-slug-helptext" required id="id_slug" />'
|
||||
input_field_for_live_slug = '<input type="text" name="slug" value="mars-landing" data-controller="w-slug" data-action="blur->w-slug#slugify w-sync:check->w-slug#compare w-sync:apply->w-slug#urlify:prevent" data-w-slug-compare-as-param="urlify" data-w-slug-allow-unicode-value maxlength="255" aria-describedby="panel-child-promote-child-common_page_configuration-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->w-slug#slugify w-sync:check->w-slug#compare w-sync:apply->w-slug#urlify:prevent" data-w-slug-compare-as-param="urlify" data-w-slug-allow-unicode-value data-w-slug-trim-value="true" maxlength="255" aria-describedby="panel-child-promote-child-common_page_configuration-child-slug-helptext" required id="id_slug" />'
|
||||
input_field_for_live_slug = '<input type="text" name="slug" value="mars-landing" data-controller="w-slug" data-action="blur->w-slug#slugify w-sync:check->w-slug#compare w-sync:apply->w-slug#urlify:prevent" data-w-slug-compare-as-param="urlify" data-w-slug-allow-unicode-value data-w-slug-trim-value="true" maxlength="255" aria-describedby="panel-child-promote-child-common_page_configuration-child-slug-helptext" required id="id_slug" />'
|
||||
|
||||
# Status Link should be the live page (not revision)
|
||||
self.assertNotContains(
|
||||
|
|
|
@ -704,7 +704,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->w-slug#slugify w-sync:check->w-slug#compare w-sync:apply->w-slug#urlify:prevent" data-w-slug-allow-unicode-value data-w-slug-compare-as-param="urlify" id="test-id">',
|
||||
'<input type="text" name="test" data-controller="w-slug" 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 data-w-slug-compare-as-param="urlify" data-w-slug-trim-value="true" id="test-id">',
|
||||
html,
|
||||
)
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ from django.forms import widgets
|
|||
|
||||
class SlugInput(widgets.TextInput):
|
||||
"""
|
||||
Associates the input field with the Stimulus w-slug (SlugController).
|
||||
Associates the input field with the Stimulus w-slug (CleanController).
|
||||
Slugifies content based on `WAGTAIL_ALLOW_UNICODE_SLUGS` and supports
|
||||
fields syncing their value to this field (see `TitleFieldPanel`) if
|
||||
also used.
|
||||
|
@ -18,6 +18,7 @@ class SlugInput(widgets.TextInput):
|
|||
settings, "WAGTAIL_ALLOW_UNICODE_SLUGS", True
|
||||
),
|
||||
"data-w-slug-compare-as-param": "urlify",
|
||||
"data-w-slug-trim-value": "true",
|
||||
}
|
||||
if attrs:
|
||||
default_attrs.update(attrs)
|
||||
|
|
Ładowanie…
Reference in New Issue