Skip to content

Commit

Permalink
Merge pull request #1189 from codidact/0valt/1145/show-min-length-cha…
Browse files Browse the repository at this point in the history
…r-reqs
  • Loading branch information
ArtOfCode- authored Nov 25, 2023
2 parents a52c3a1 + 88080d1 commit 813dea4
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 73 deletions.
113 changes: 71 additions & 42 deletions app/assets/javascripts/character_count.js
Original file line number Diff line number Diff line change
@@ -1,60 +1,89 @@
$(() => {
const setIcon = (el, icon) => {
/**
* @typedef {'fa-ellipsis-h'|'fa-times'|'fa-exclamation-circle'|'fa-check'} CounterIcon
* @typedef {'info'|'warning'|'error'|'default'} CounterState
* @typedef {'valid'|'invalid'} InputValidationState
* @typedef {'disabled'|'enabled'} SubmitButtonDisabledState
*/

/**
* Sets the icon to show before the counter, if any
* @param {CounterIcon} icon name of the icon to show
*/
const setCounterIcon = (el, icon) => {
const icons = ['fa-ellipsis-h', 'fa-check', 'fa-exclamation-circle', 'fa-times'];
el.removeClass(icons.join(' ')).addClass(icon);
};

$(document).on('keyup change paste', '[data-character-count]', ev => {
/**
* Sets the counter's state
* @param {CounterState} state the state to set
*/
const setCounterState = (el, state) => {
if (state === 'info') {
el.removeClass('has-color-yellow-700 has-color-red-500').addClass('has-color-primary');
}
else if (state === 'warning') {
el.removeClass('has-color-red-500 has-color-primary').addClass('has-color-yellow-700');
}
else if (state === 'error') {
el.removeClass('has-color-yellow-700 has-color-primary').addClass('has-color-red-500');
}
else {
el.removeClass('has-color-red-500 has-color-yellow-700 has-color-primary');
}
};

/**
* Sets the input's validation state
* @param {InputValidationState} state the state to set
*/
const setInputValidationState = (el, state) => {
const isInvalid = state === 'invalid';
el.toggleClass('failed-validation', isInvalid);
};

/**
* Sets the submit button's disabled state
* @param {SubmitButtonDisabledState} state the state to set
*/
const setSubmitButtonDisabledState = (el, state) => {
const isDisabled = state === 'disabled';
el.attr('disabled', isDisabled).toggleClass('is-muted', isDisabled);
};

$(document).on('keyup change paste', '[data-character-count]', (ev) => {
const $tgt = $(ev.target);
const $counter = $($tgt.attr('data-character-count'));
const $button = $counter.parents('form').find('input[type="submit"]');
const $count = $counter.find('.js-character-count__count');
const $icon = $counter.find('.js-character-count__icon');

const displayAt = parseFloat($counter.attr('data-display-at'));
const count = $tgt.val().length;
const max = parseInt($counter.attr('data-max'), 10);
const min = parseInt($counter.attr('data-min'), 10);
const count = $tgt.val().length;
const text = `${count} / ${max}`;
const threshold = parseFloat($counter.attr('data-threshold'));

if (displayAt) {
if (count >= displayAt * max) {
$counter.removeClass('hide');
}
else {
$counter.addClass('hide');
}
}
const gtnMax = count > max;
const ltnMin = count < min;
const gteThreshold = count >= threshold * max;

if (count > max) {
$counter.removeClass('has-color-yellow-700 has-color-primary').addClass('has-color-red-500');
setIcon($icon, 'fa-times');
if ($button) {
$button.attr('disabled', true).addClass('is-muted');
}
}
else if (count > 0.75 * max) {
$counter.removeClass('has-color-red-500 has-color-primary').addClass('has-color-yellow-700');
setIcon($icon, 'fa-exclamation-circle');
if ($button) {
$button.attr('disabled', false).removeClass('is-muted');
}
}
else if (min && count < min) {
$counter.removeClass('has-color-yellow-700 has-color-red-500').addClass('has-color-primary');
setIcon($icon, 'fa-ellipsis-h');
if ($button) {
$button.attr('disabled', true).addClass('is-muted');
}
$tgt.addClass('failed-validation');
}
else {
$counter.removeClass('has-color-red-500 has-color-yellow-700 has-color-primary');
setIcon($icon, 'fa-check');
if ($button) {
$button.attr('disabled', false).removeClass('is-muted');
}
$tgt.removeClass('failed-validation');
const text = `${count} / ${ltnMin ? min : max}`;

if (gtnMax || ltnMin) {
setCounterState($counter, 'error');
setCounterIcon($icon, 'fa-times');
setSubmitButtonDisabledState($button, 'disabled');
setInputValidationState($tgt, 'invalid');
} else if (gteThreshold) {
setCounterState($counter, 'warning');
setCounterIcon($icon, 'fa-exclamation-circle');
setSubmitButtonDisabledState($button, 'enabled');
} else {
setCounterState($counter, 'default');
setCounterIcon($icon, 'fa-check');
setSubmitButtonDisabledState($button, 'enabled');
setInputValidationState($tgt, 'valid');
}

$count.text(text);
Expand Down
13 changes: 2 additions & 11 deletions app/views/comments/_new_thread_modal.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,15 @@
<div class="form-caption">Start the thread with a comment.</div>
<%= text_area_tag :body, '', class: 'form-element js-comment-field', required: true,
data: { post: post.id, thread: '-1', character_count: ".js-character-count-#{post.id}" } %>
<span class="has-float-right has-font-size-caption js-character-count-<%= post.id %>"
data-max="1000" data-min="15">
<i class="fas fa-ellipsis-h js-character-count__icon"></i>
<span class="js-character-count__count">0 / 1000</span>
</span>
<%= render 'shared/char_count', type: post.id, min: 15, max: 1000 %>

<%= label_tag :title, 'Comment thread title (optional)', class: 'form-element' %>
<span class="form-caption">
You can give your comment thread a title. If you leave this blank, the beginning of the initial comment will
be shown.
</span>
<%= text_field_tag :title, '', class: 'form-element', data: { character_count: ".js-character-count-thread-title" } %>

<span class="has-float-right has-font-size-caption js-character-count-thread-title hide"
data-max="255" data-min="0" data-display-at="0.75">
<i class="fas fa-ellipsis-h js-character-count__icon"></i>
<span class="js-character-count__count">0 / 255</span>
</span>
<%= render 'shared/char_count', type: 'thread-title' %>

<%= submit_tag 'Create thread', class: 'button is-filled', id: "create_thread_button_#{post.id}", disabled: true %>
<% end %>
Expand Down
8 changes: 2 additions & 6 deletions app/views/comments/thread.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -121,12 +121,8 @@
<%= text_area_tag :content, '', class: 'form-element js-comment-field',
data: { thread: @comment_thread.id, post: @comment_thread.post_id,
character_count: ".js-character-count-#{@post.id}" } %>
<span class="has-float-right has-font-size-caption js-character-count-<%= @post.id %>"
data-max="1000" data-min="15">
<i class="fas fa-ellipsis-h js-character-count__icon"></i>
<span class="js-character-count__count">0 / 1000</span>
</span>

<%= render 'shared/char_count', type: @post.id, min: 15, max: 1000 %>

<%= submit_tag 'Add reply', class: 'button is-muted is-filled', disabled:true %>
<% end %>
<% end %>
Expand Down
9 changes: 2 additions & 7 deletions app/views/posts/_form.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,9 @@
<%= f.text_field :title, class: 'form-element post_title', data: { character_count: ".js-character-count-post-title" } %>
</div>
<div>
<span class="has-float-right has-font-size-caption js-character-count-post-title hide"
data-min="<%= min_title_length(category) %>"
data-max="<%= max_title_length(category) %>"
data-display-at="0.75">
<i class="fas fa-ellipsis-h js-character-count__icon"></i>
<span class="js-character-count__count">0 / <%= max_title_length(category) %></span>
</span>
<%= render 'shared/char_count', type: 'post-title', cur: post.body_markdown&.length, max: max_title_length(category), min: min_title_length(category) %>
</div>

<% end %>

<% if post_type.has_tags? && category.present? %>
Expand Down
8 changes: 1 addition & 7 deletions app/views/posts/_mdhint.html.erb
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,5 @@
<div class="form-caption widget--footer js-post-field-footer">
We <a href="/help/formatting">support Markdown</a> for posts:
<strong>**bold**</strong>, <em>*italics*</em>, <code>`code`</code>, two newlines for paragraphs
<span class="has-float-right has-font-size-caption js-character-count-post-body hide"
data-min="<%= min_length %>"
data-max="<%= max_length %>"
data-display-at="0.75">
<i class="fas fa-ellipsis-h js-character-count__icon"></i>
<span class="js-character-count__count">0 / <%= max_length %></span>
</span>
<%= render 'shared/char_count', type: 'post-body', min: min_length, max: max_length %>
</div>
28 changes: 28 additions & 0 deletions app/views/shared/_char_count.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<%#
Reusable helper view for character count requirements.
Variables:
cur : current number of characters (default 0)
max : maximum number of characters allowed (default 255)
min : minimum number of characters allowed (default 0)
threshold : fraction of max to show the count at (default 0.75)
type : character count type (e.g.: post-title)
%>

<%
# defaults & normalization
cur ||= defined?(cur) && !cur.nil? ? cur.to_i : 0
max ||= defined?(max) && !max.nil? ? max.to_i : 255
min ||= defined?(min) && !min.nil? ? min.to_i : 0
threshold ||= defined?(threshold) && !threshold.nil? ? threshold.to_f : 0.75
%>

<span class="has-float-right has-font-size-caption js-character-count-<%= type %>"
data-max="<%= max %>"
data-min="<%= min %>"
data-threshold="<%= threshold %>">
<i class="fas fa-ellipsis-h js-character-count__icon"></i>
<span class="js-character-count__count">
<%= cur %> / <%= cur < min ? min : max %>
</span>
</span>

0 comments on commit 813dea4

Please sign in to comment.