Skip to content
Open
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
23 changes: 22 additions & 1 deletion lib/GADS/Column/String.pm
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,33 @@ package GADS::Column::String;

use Log::Report 'linkspace';
use Moo;
use MooX::Types::MooseLike::Base qw/Bool Str Maybe/;
use MooX::Types::MooseLike::Base qw/Bool Str Int Maybe/;

extends 'GADS::Column';

with 'GADS::Role::Presentation::Column::String';

has '+option_names' => (
default => sub {
[+{
name => 'max_length',
user_configurable => 1,
}]
}
);

has max_length => (
is => 'rw',
isa => Maybe[Int],
lazy => 1,
builder => sub {
my $self = shift;
$self->has_options ? $self->options->{max_length} // undef : undef; # explicitly return undef if no options, to avoid confusion with 0
},
trigger => sub { $_[0]->reset_options },
coerce => sub { defined $_[0] && $_[0] ne "" && $_[0] =~ /^\d+$/i ? int($_[0]) : undef },
);

has textbox => (
is => 'rw',
isa => Bool,
Expand Down
3 changes: 2 additions & 1 deletion lib/GADS/Role/Presentation/Column/String.pm
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ use Moo::Role;
sub after_presentation
{ my ($self, $return) = @_;

$return->{textbox} = $self->textbox;
$return->{textbox} = $self->textbox;
$return->{max_length} = $self->max_length if defined $self->max_length;
}

1;
10 changes: 10 additions & 0 deletions src/frontend/components/form-group/field-length/_field-length.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
.counter {
@extend .d-flex;
@extend .justify-content-end;
@extend .text-muted;
@extend .small;

&.invalid {
@extend .text-danger
}
}
6 changes: 6 additions & 0 deletions src/frontend/components/form-group/field-length/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { initializeComponent } from "component"
import FieldLengthComponent from "./lib/component"

export default (scope) => {
initializeComponent(scope, "[data-max]", FieldLengthComponent);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import {describe, it, expect} from "@jest/globals";
import FieldLengthComponent from "./component";

describe("FieldLengthComponent", () => {
it("should error when not passed the correct type of component", () => {
const div = document.createElement("div");
expect(() => new FieldLengthComponent(div)).toThrow("Element must be a textarea or input");
});

it("should not create a counter if the element lacks the data-max attribute", () => {
const input = document.createElement("input");
document.body.appendChild(input);
new FieldLengthComponent(input);
const counter = input.nextElementSibling as HTMLElement;
expect(counter).toBeNull();
});

it("should create a counter with the correct values when the input is empty", () => {
const input = document.createElement("input");
input.dataset.max = "10";
document.body.appendChild(input);
new FieldLengthComponent(input);
const counter = input.nextElementSibling as HTMLElement;
expect(counter).not.toBeNull();
expect(counter.textContent).toBe("0/10");
});

it("should create a counter with the correct values when the input has some text", () => {
const input = document.createElement("input");
input.dataset.max = "10";
input.value = "Hello";
document.body.appendChild(input);
new FieldLengthComponent(input);
const counter = input.nextElementSibling as HTMLElement;
expect(counter).not.toBeNull();
expect(counter.textContent).toBe("5/10");
});

it("should update the counter with the correct values when the input value changes", () => {
const input = document.createElement("input");
input.dataset.max = "10";
document.body.appendChild(input);
new FieldLengthComponent(input);
const counter = input.nextElementSibling as HTMLElement;
expect(counter).not.toBeNull();
input.value = "Hello";
input.dispatchEvent(new Event("keyup"));
expect(counter.textContent).toBe("5/10");
});

it("should add the invalid class to the counter when the input value exceeds the max length", () => {
const input = document.createElement("input");
input.dataset.max = "10";
document.body.appendChild(input);
new FieldLengthComponent(input);
const counter = input.nextElementSibling as HTMLElement;
expect(counter).not.toBeNull();
input.value = "Hello World!";
input.dispatchEvent(new Event("keyup"));
expect(counter.classList.contains("invalid")).toBe(true);
});

it("should remove the invalid class from the counter when the input value is reduced to be within the max length", () => {
const input = document.createElement("input");
input.dataset.max = "10";
document.body.appendChild(input);
new FieldLengthComponent(input);
const counter = input.nextElementSibling as HTMLElement;
expect(counter).not.toBeNull();
input.value = "Hello World!";
input.dispatchEvent(new Event("keyup"));
expect(counter.classList.contains("invalid")).toBe(true);
input.value = "Hello";
input.dispatchEvent(new Event("keyup"));
expect(counter.classList.contains("invalid")).toBe(false);
});

it("Should work with textarea elements", () => {
const textarea = document.createElement("textarea");
textarea.dataset.max = "10";
document.body.appendChild(textarea);
new FieldLengthComponent(textarea);
const counter = textarea.nextElementSibling as HTMLElement;
expect(counter).not.toBeNull();
textarea.value = "Hello World!";
textarea.dispatchEvent(new Event("keyup"));
expect(counter.classList.contains("invalid")).toBe(true);
});
});
41 changes: 41 additions & 0 deletions src/frontend/components/form-group/field-length/lib/component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { Component } from "component";

export default class FieldLengthComponent extends Component {
constructor(element: HTMLElement) {
super(element);
if (!(element instanceof HTMLTextAreaElement) && !(element instanceof HTMLInputElement)) {
throw new Error("Element must be a textarea or input");
}
this.init();
}

private init() {
const input = this.element as HTMLTextAreaElement | HTMLInputElement;
if (!input) return;
if(!input.dataset.max) return;
this.createCounter(input);
input.addEventListener("keyup", () => this.updateCounter(input));
}

private createCounter(input: HTMLTextAreaElement | HTMLInputElement) {
const max = parseInt(input.dataset.max || "0", 10);
const counter = document.createElement("div");
counter.classList.add("counter");
counter.textContent = `${input.value.length}/${max}`;
input.insertAdjacentElement("afterend", counter);
}

private updateCounter(input: HTMLTextAreaElement | HTMLInputElement) {
const max = parseInt(input.dataset.max || "0", 10);
const counter = input.nextElementSibling as HTMLElement;
const length = input.value.length;
counter.textContent = `${length}/${max}`;
if(length>max) {
if(!counter.classList.contains("invalid")) {
counter.classList.add("invalid");
}
} else if(counter.classList.contains("invalid")) {
counter.classList.remove("invalid");
}
}
}
3 changes: 2 additions & 1 deletion src/frontend/css/stylesheets/general.scss
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@
"form-group/select-widget/select-widget",
"form-group/switch/switch",
"form-group/textarea/textarea",
"form-group/input-group/input-group";
"form-group/input-group/input-group",
"form-group/field-length/field-length";

@import "graph/graph";
@import "link/link";
Expand Down
17 changes: 15 additions & 2 deletions src/frontend/js/lib/validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,10 +169,10 @@ const validateRequiredFieldsetCheckboxes = (fieldset) => {
return isValid;
};

const addErrorMessage = (field, name, id) => {
const addErrorMessage = (field, name, id, raw = false) => {
const $errorDiv = $('<div class="error">')
let $span = $(`<span id="${id}-err" class="form-text form-text--error" aria-live="off"></span>`)
$span.text(`${name} is a required field.`)
$span.text(raw ? name : `${name} is a required field.`)
$errorDiv.html($span)
field.append($errorDiv)
}
Expand All @@ -193,6 +193,19 @@ const validateInput = (field) => {

removeErrorMessage($(field))

const maxEl = field.find('[data-max]');
if(maxEl && maxEl.length) {
const maxLen = maxEl.data('max');
const fieldLen = maxEl.val().length;
if(fieldLen > maxLen) {
maxEl.attr('aria-invalid', true)
addErrorMessage(field, `${strFieldName} must be ${maxLen} characters or less`, strID, true);
field.addClass('invalid');
field.closest('.fieldset--required').addClass('fieldset--invalid');
return false;
}
}

if (($inputEl.val() === '') && (!isHiddenDependentField(field))) {
$inputEl.attr('aria-invalid', true)
addErrorMessage(field, strFieldName, strID)
Expand Down
2 changes: 2 additions & 0 deletions src/frontend/js/site.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ import SelectAllComponent from "components/select-all";
import HelpView from "components/help-view";
import PeopleFilterComponent from "components/form-group/people-filter";
import handleActions from "util/actionsHandler";
import FieldLengthComponent from "components/form-group/field-length";

// Register them
registerComponent(AddTableModalComponent);
Expand Down Expand Up @@ -84,6 +85,7 @@ registerComponent(SelectAllComponent);
registerComponent(HelpView);
registerComponent(PeopleFilterComponent);
registerComponent(AutosaveComponent);
registerComponent(FieldLengthComponent);

// Initialize all components at some point
initializeRegisteredComponents(document.body);
Expand Down
12 changes: 8 additions & 4 deletions views/edit.tt
Original file line number Diff line number Diff line change
Expand Up @@ -1025,7 +1025,8 @@
popover_body = col.helptext_html
filter = "html"
is_required = field_is_required
is_readonly = readonly;
is_readonly = readonly
max_length = col.max_length;
ELSE;
INCLUDE fields/textarea.tt
id = col.id
Expand All @@ -1039,7 +1040,8 @@
is_required = field_is_required
is_readonly = readonly
hide_group = 1
rows = 10;
rows = 10
max_length = col.max_length;
END;
ELSE;
field_input_type = "text";
Expand Down Expand Up @@ -1079,7 +1081,8 @@
filter = "html"
is_required = field_is_required
is_readonly = readonly
type = field_input_type;
type = field_input_type
max_length = col.max_length;
ELSE;
INCLUDE fields/input.tt
id = col.id
Expand All @@ -1096,7 +1099,8 @@
is_required = field_is_required
is_readonly = readonly
hide_group = 1
type = field_input_type;
type = field_input_type
max_length = col.max_length;
END;


Expand Down
1 change: 1 addition & 0 deletions views/fields/input.tt
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
readonly[% END %][% IF is_required %]
required
aria-required="true"[% END %]
[% IF max_length %] data-max="[% max_length %]"[% END %]
>
</div>
</div>
Expand Down
6 changes: 4 additions & 2 deletions views/fields/multi_field.tt
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@
is_readonly = is_readonly
hide_group = 1
multi_value_style = 1
rows = 10;
rows = 10
max_length = max_length;
ELSE;
INCLUDE fields/input.tt
id = id
Expand All @@ -112,7 +113,8 @@
is_required = is_required
is_readonly = is_readonly
hide_group = 1
type = field_input_type;
type = field_input_type
max_length = max_length;
END;
%]
<button
Expand Down
1 change: 1 addition & 0 deletions views/fields/textarea.tt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
readonly[% END %][% IF is_required %]
required
aria-required="true"[% END %]
[% IF max_length %]data-max="[% max_length %]"[% END %]
>[% INCLUDE fields/sub/filter.tt; %]</textarea>
</div>
[% IF label %]
Expand Down
14 changes: 14 additions & 0 deletions views/layout.tt
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,20 @@
</div>
<div class="collapse" id="textFieldSettings">
<div class="card__content">
<div class="row">
<div class="col">
[%
INCLUDE fields/input.tt
id = "max_length"
name = "max_length"
label = "Maximum length of the text (characters)"
placeholder = "Leave blank for no limit"
value = column.max_length
type = "text"
filter = "html";
%]
</div>
</div>
<div class="row">
<div class="col">
[%
Expand Down
Loading