Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,9 @@ export default defineConfig([
// https://browsersl.ist/#q=supports+es6-module+and+not+supports+array-includes
'es-x/no-array-prototype-includes': 'off',

// Babel transpiles ES2021 `??=` logical assignment
'es-x/no-logical-assignment-operators': 'off',

// Babel transpiles ES2020 `??` nullish coalescing
'es-x/no-nullish-coalescing-operators': 'off',

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,40 @@ describe('Character count', () => {

expect(component.getCountMessage()).toBe('You have 97 words remaining')
})

it('uses custom `countFunction` for `maxlength` limit when set', async () => {
const component = new CharacterCount($root, {
maxlength: 100,
countFunction: jest.fn().mockReturnValue(10)
})

$textarea.focus()
await user.keyboard('Newly updated value')

expect(component.config.countFunction).toHaveBeenLastCalledWith(
'Newly updated value'
)

expect(component.getCountMessage()).toBe(
'You have 90 characters remaining'
)
})

it('uses custom `countFunction` for `maxwords` limit when set', async () => {
const component = new CharacterCount($root, {
maxwords: 100,
countFunction: jest.fn().mockReturnValue(10)
})

$textarea.focus()
await user.keyboard('Newly updated value')

expect(component.config.countFunction).toHaveBeenLastCalledWith(
'Newly updated value'
)

expect(component.getCountMessage()).toBe('You have 90 words remaining')
})
})

describe('with HTML lang attribute', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export class CharacterCount extends ConfigurableComponent {
const {
i18n,
maxlength,
countFunction,
countType,
screenReaderCountMessageClass,
textareaDescriptionClass,
Expand All @@ -72,7 +73,7 @@ export class CharacterCount extends ConfigurableComponent {
locale: closestAttributeValue(this.$root, 'lang')
})

if (countType === 'characters') {
if (countType === 'characters' || !!countFunction) {
if (!('Segmenter' in Intl)) {
throw new SupportError(
formatErrorMessage(
Expand All @@ -83,7 +84,7 @@ export class CharacterCount extends ConfigurableComponent {
}

this.segmenter = new Intl.Segmenter(this.i18n.locale, {
granularity: 'grapheme'
granularity: countType === 'words' ? 'word' : 'grapheme'
})
}

Expand Down Expand Up @@ -220,31 +221,12 @@ export class CharacterCount extends ConfigurableComponent {
* @param {string} [text] - Deprecated
*/
updateCount(text) {
const { $textarea } = this
const { countType } = this.config
const { $textarea, countFunctions } = this
let { countType, countFunction } = this.config

text = text ?? $textarea.value

switch (countType) {
case 'length':
// Count code points (string length)
this.length = text.length
break

case 'characters': {
// Count grapheme clusters (user-perceived characters)
this.length = this.segmenter
? Array.from(this.segmenter.segment(text)).length
: 0

break
}

case 'words':
// Count consecutive non-whitespace results
this.length = text.match(/\S+/g)?.length ?? 0
break
}
text ??= $textarea.value
countFunction ??= countFunctions[countType]
this.length = countFunction.call(this, text)
}

/**
Expand Down Expand Up @@ -467,6 +449,46 @@ export class CharacterCount extends ConfigurableComponent {
}
}

/**
* Character count functions
*
* @constant
* @satisfies {Record<string, CharacterCountConfig['countFunction']>}
*/
countFunctions = Object.freeze({
/**
* Count code points (string length)
*
* @param {string} text - Textarea value
* @returns {number} Count
*/
length(text) {
return text.length
},

/**
* Count grapheme clusters (user-perceived characters)
*
* @param {string} text - Textarea value
* @returns {number} Count
*/
characters(text) {
return this.segmenter
? Array.from(this.segmenter.segment(text)).length
: 0
},

/**
* Count consecutive non-whitespace results
*
* @param {string} text - Textarea value
* @returns {number} Count
*/
words(text) {
return text.match(/\S+/g)?.length ?? 0
}
})

/**
* Name for the component used when initialising using data-module attributes
*/
Expand Down Expand Up @@ -524,6 +546,7 @@ export class CharacterCount extends ConfigurableComponent {
maxlength: { type: 'number' },
threshold: { type: 'number' },
countType: { type: 'string' },
countFunction: { type: 'function' },
textareaDescriptionClass: { type: 'string' },
visibleCountMessageClass: { type: 'string' },
screenReaderCountMessageClass: { type: 'string' },
Expand Down Expand Up @@ -574,12 +597,20 @@ export function initCharacterCounts(options) {
* count message will be hidden by default.
* @property {'characters' | 'length' | 'words'} countType - The count type
* (`"characters"`, `"length"` or `"words"`) used to count the text.
* @property {CharacterCountFunction} [countFunction] - Custom character or
* word counting function.
* @property {string} textareaDescriptionClass - Textarea description class
* @property {string} visibleCountMessageClass - Visible count message class
* @property {string} screenReaderCountMessageClass - Screen reader count message class
* @property {CharacterCountTranslations} [i18n=CharacterCount.defaults.i18n] - Character count translations
*/

/**
* Character count function
*
* @typedef {(this: CharacterCount, text: string) => number} CharacterCountFunction
*/

/**
* Character count translations
*
Expand Down
Loading