Skip to content

Commit

Permalink
Merge pull request #264 from City-of-Helsinki/UHF-9216
Browse files Browse the repository at this point in the history
UHF-9216 Character counter
  • Loading branch information
khalima authored Feb 8, 2024
2 parents 8e29465 + 444b0ae commit cdafab3
Show file tree
Hide file tree
Showing 18 changed files with 404 additions and 44 deletions.
7 changes: 4 additions & 3 deletions .stylelintrc.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,13 @@ plugins:
rules:
# scss/at-import-no-partial-leading-underscore: null
string-quotes: single # We use single quotes in SCSS
declaration-block-no-redundant-longhand-properties: null # We prefer lonhand properties for clarity in SCSS
selector-class-pattern: null # We're not too stict about selector class pattern
max-line-length: null # Lets not limit line length at this point
declaration-block-no-redundant-longhand-properties: null # We prefer longhand properties for clarity in SCSS
selector-class-pattern: null # We're not too strict about selector class pattern
max-line-length: null # Let's not limit line length at this point
declaration-empty-line-before: never # No need for empty line before declaration
order/properties-alphabetical-order: true # We're following alphabetical order in properties
custom-property-pattern: "^([a-z][a-z0-9]*)(--?[a-z0-9]+)*$" # kebab-case pattern with allowed bem-like double --
max-nesting-depth: 4 # We're not using normal nesting rule of 3, as admin theme is all about overriding styles
overrides:
- files:
- '**/ckeditor.scss'
Expand Down
4 changes: 2 additions & 2 deletions dist/css/styles.min.css

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions dist/js/characterCounter.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion dist/js/heroToggle.min.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions hdbt_admin.libraries.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
character-counter:
js:
dist/js/characterCounter.min.js: {}
version: 1.0.0
dependencies:
- core/drupal
- core/once

language-switcher:
js:
dist/js/languageSwitcher.min.js: {}
Expand Down
37 changes: 37 additions & 0 deletions hdbt_admin.theme
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,12 @@ function _hdbt_admin_form_template_suggestions(array &$suggestions, array &$vari
if (in_array('koro', $variables['element']['#parents'], TRUE)) {
$suggestions[] = $variables['theme_hook_original'] . '__koro';
}
if (
in_array('field_hero_title', $variables['element']['#parents'], TRUE) ||
in_array('field_hero_desc', $variables['element']['#parents'], TRUE)
) {
$suggestions[] = $variables['theme_hook_original'] . '__character_count';
}
}

/**
Expand Down Expand Up @@ -449,3 +455,34 @@ function hdbt_admin_preprocess_field_multiple_value_form(&$variables) {
}
}
}

/**
* Implements hook_preprocess_HOOK().
*/
function hdbt_admin_preprocess_form_element(array &$variables) {
// Map the counter input tag, step value and total value to variables.
$counter_elements = [
'field_hero_title' => [
'counter_input_tag' => 'input',
'counter_step' => 0,
'counter_total' => 55,
],
'field_hero_desc' => [
'counter_input_tag' => 'textarea',
'counter_step' => 160,
'counter_total' => 200,
],
];

// Set the mapped values to corresponding fields.
foreach ($counter_elements as $counter_element => $counter_values) {
if (
array_key_exists('name', $variables) &&
str_contains($variables['name'], $counter_element)
) {
foreach ($counter_values as $key => $value) {
$variables[$key] = $value;
}
}
}
}
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"eslint-plugin-import": "^2.25.3",
"expose-loader": "^4.0.0",
"glob": "^10.0.0",
"hds-design-tokens": "^2.0.0",
"hds-design-tokens": "^3.0.0",
"html-loader": "^4.0.0",
"html-webpack-plugin": "^5.3.2",
"husky": "^8.0.0",
Expand Down
150 changes: 150 additions & 0 deletions src/js/characterCounter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
'use strict';

((Drupal, once) => {

const WARNING_GREEN = 0;
const WARNING_YELLOW = 1;
const WARNING_RED = 2;

// Translate character count texts.
const characterCounter = (count, total) => {
return Drupal.t('Characters: @counted/@total', {
'@counted': count,
'@total': total
}, {context: 'Character counter'});
};

// Determine the warning type based on character count.
const processWarningType = (count, step, total) => {
return (count >= total) ? WARNING_RED : (count > step && step > 0) ? WARNING_YELLOW : WARNING_GREEN;
};

// Translate warning texts based on warning type.
const characterWarning = (type, step, total, input, element) => {

// The maximum length has been reached, show warning.
if (type === WARNING_RED) {
// Show the warning text.
element.classList.remove('is-hidden');

// Use different translations for input and textarea fields.
if (input === 'input') {
return Drupal.t('The recommended maximum length for the title is @total characters.', {
'@total': total
}, {context: 'Character counter'});
} else {
return Drupal.t('The recommended maximum length for the lead is @total characters.', {
'@total': total
}, {context: 'Character counter'});
}
}

// The character length is more than 160 chars, show warning.
if (type === WARNING_YELLOW) {
element.classList.remove('is-hidden');
return Drupal.t('Consider shortening. A lead under @step characters works best for search engines.', {
'@step': step
}, {context: 'Character counter'});
}

// Remove warnings.
element.classList.add('is-hidden');
return WARNING_GREEN;
};

// Convert HTML tags to single spaces.
const convertHtmlTags = (data) => {
return data
// Replace all HTML tags with a single space.
.replace(/<[^>]*>/g, ' ')
// Replace all consecutive whitespace characters with a single space.
.replace(/\s+/g, ' ')
// Replace HTML character entities with a single space.
.replace(/&#?[a-z0-9]+;/i, ' ')
// Remove leading and trailing whitespace and return the length.
.trim().length;
};

Drupal.behaviors.characterCounter = {
attach: function attach(context) {
// Get all character counter instances using once().
const counterInstances = once('character-counter', '[data-character-counter]', context);

if (!counterInstances) {
return;
}

// Loop through all character counter instances.
counterInstances.forEach((counterInstance) => {
const counterCounter = counterInstance.dataset.characterCounter;
const counterInputTag = counterInstance.dataset.counterInputTag;
const counterTotalChars = counterInstance.dataset.counterTotal;
const counterStepChars = counterInstance.dataset.counterStep;
const counterWarning = counterInstance.querySelector('.character-counter__warning');
const formItem = context.querySelector(`.${counterCounter}`);

if (!formItem) {
return;
}

const charCounter = formItem.querySelector('[data-counter-id]');
const charWarning = formItem.querySelector('[data-warning-id]');
const textInput = formItem.querySelector(counterInputTag);

if (!textInput) {
return;
}

// Set initial warning type.
let warningType = WARNING_GREEN;

// The textarea counter needs to be inserted after the form item.
// Otherwise, it will be shown in a wrong position in the DOM.
if (
counterInputTag === 'textarea' &&
formItem.parentElement.classList.contains('form-item') &&
formItem.parentElement.querySelector('.form-item__description')
) {
formItem.parentElement
.querySelector('.form-item__description')
.insertAdjacentElement('afterend', counterInstance);
}

// Set initial value for the character counter.
if (textInput.value.length > 0) {
warningType = processWarningType(textInput.value.length, counterStepChars, counterTotalChars);
charCounter.textContent = characterCounter(textInput.value.length, counterTotalChars);
charWarning.textContent = characterWarning(warningType, counterStepChars, counterTotalChars, counterInputTag, counterWarning);
}

// Handle input tag and textarea tags separately.
if (counterInputTag === 'input') {
// Add event listener to the input tag and process
// the charCounter and charWarning.
textInput.addEventListener('input', function () {
warningType = processWarningType(textInput.value.length, counterStepChars, counterTotalChars);
charCounter.textContent = characterCounter(textInput.value.length, counterTotalChars);
charWarning.textContent = characterWarning(warningType, counterStepChars, counterTotalChars, counterInputTag, counterWarning);
});
} else {
setTimeout(function () {
const ckeditorEditable = textInput.parentElement.querySelector('.ck-editor__editable');

// Add event listener to the textarea tag (CKEditor) and process
// the charCounter and charWarning.
if (ckeditorEditable && ckeditorEditable.ckeditorInstance) {
const editor = ckeditorEditable.ckeditorInstance;
editor.model.document.on('change:data', () => {
// Output the number of words to the counter.
warningType = processWarningType(convertHtmlTags(editor.getData()), counterStepChars, counterTotalChars);
charCounter.textContent = characterCounter(convertHtmlTags(editor.getData()), counterTotalChars);
charWarning.textContent = characterWarning(warningType, counterStepChars, counterTotalChars, counterInputTag, counterWarning);
});
}
});
}
});
},
};

})(Drupal, once);
9 changes: 4 additions & 5 deletions src/js/heroToggle.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Functionality that adds a hero to an article if the hero-checkbox is checked.
* Functionality that adds a hero to entity if the hero-checkbox is checked.
*/

// Boolean checkbox field that determines if there is hero added or not.
Expand All @@ -8,10 +8,9 @@ const heroCheckbox = document.querySelector(
);

// Helper function to trigger event
function triggerEvent(el, type) {
var e = document.createEvent('Event');
e.initEvent(type, false, true);
el.dispatchEvent(e);
function triggerEvent(element, type) {
const event = new Event(type);
element.dispatchEvent(event);
}

if (heroCheckbox) {
Expand Down
2 changes: 2 additions & 0 deletions src/scss/06_components/forms/__index.scss
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
@import 'character-counter';
@import 'dropbutton';
@import 'form-items';
26 changes: 26 additions & 0 deletions src/scss/06_components/forms/_character-counter.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
.character-counter {
margin-top: -$spacing-quarter;
}

.character-counter__count {
color: $color-black-70;
font-size: remify(14px);
}

.character-counter__warning {
align-items: center;
background: $color-alert-light;
border-left: 10px solid $color-alert-dark;
display: flex;
margin-top: $spacing-half;
padding: $spacing-half;
}

.character-counter__icon {
color: $color-alert-dark;
}

.character-counter__warning-text {
color: $color-black;
margin-inline-start: $spacing-half;
}
5 changes: 5 additions & 0 deletions src/scss/06_components/forms/_form-items.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.hdbt-admin .form-item__description {
color: $color-black-70;
font-size: remify(14px);
max-width: none;
}
1 change: 0 additions & 1 deletion src/scss/06_components/layout/_page.scss
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@
}
}

ul.paragraph-selection.dropbutton,
ul.paragraph-selection.dropbutton {
@media (min-width: $breakpoint-s) {
width: 500px;
Expand Down
Loading

0 comments on commit cdafab3

Please sign in to comment.