unit uNameManager;

interface

uses
  System.SysUtils, JS, Web;

type
  TGetOptionsEvent = reference to function(const AFieldName: string): TJSArray;
  TRenderTableEvent = reference to procedure;
  TFocusTriggerEvent = reference to procedure(const ATriggerId: string);

  TNameManager = class
  private
    FCurrentField: string;
    FCurrentRowIndex: Integer;
    FCurrentTriggerId: string;
    FGetOptions: TGetOptionsEvent;
    FRenderTable: TRenderTableEvent;
    FFocusTrigger: TFocusTriggerEvent;
    procedure AddAnotherClick(Event: TJSEvent);
    procedure SaveClick(Event: TJSEvent);
    procedure NameInputKeyDown(Event: TJSEvent);
    procedure OffcanvasHidden(Event: TJSEvent);
    function GetOptionsForField(const AFieldName: string): TJSArray;
    function HasExactOption(const AItems: TJSArray; const AValue: string): Boolean;
    procedure ResetInputArea;
    procedure ShowValidation(const AMessage: string);
    procedure RenderExistingList;
    procedure ShowAddArea;
    procedure HandleAddClick;
    procedure HandleHidden;
  public
    constructor Create(
      const AGetOptions: TGetOptionsEvent;
      const ARenderTable: TRenderTableEvent;
      const AFocusTrigger: TFocusTriggerEvent
    );

    procedure BindControls;
    procedure OpenManager(const AFieldName: string; const ARowIndex: Integer; const ATriggerId: string);
    procedure CloseManager;
  end;

implementation

constructor TNameManager.Create(
  const AGetOptions: TGetOptionsEvent;
  const ARenderTable: TRenderTableEvent;
  const AFocusTrigger: TFocusTriggerEvent
);
begin
  inherited Create;
  FGetOptions := AGetOptions;
  FRenderTable := ARenderTable;
  FFocusTrigger := AFocusTrigger;
  FCurrentField := '';
  FCurrentRowIndex := -1;
  FCurrentTriggerId := '';
end;


function HtmlEncode(const s: string): string;
begin
  Result := s;
  Result := StringReplace(Result, '&', '&amp;', [rfReplaceAll]);
  Result := StringReplace(Result, '<', '&lt;', [rfReplaceAll]);
  Result := StringReplace(Result, '>', '&gt;', [rfReplaceAll]);
  Result := StringReplace(Result, '"', '&quot;', [rfReplaceAll]);
  Result := StringReplace(Result, '''', '&#39;', [rfReplaceAll]);
end;


function GetElement(const AId: string): TJSHTMLElement;
begin
  Result := TJSHTMLElement(document.getElementById(AId));
end;


function TNameManager.GetOptionsForField(const AFieldName: string): TJSArray;
begin
  if Assigned(FGetOptions) then
    Result := FGetOptions(AFieldName)
  else
    Result := nil;
end;


function TNameManager.HasExactOption(const AItems: TJSArray; const AValue: string): Boolean;
var
  i: Integer;
begin
  Result := False;

  if not Assigned(AItems) then
    Exit;

  for i := 0 to AItems.length - 1 do
  begin
    if string(AItems[i]) = AValue then
      Exit(True);
  end;
end;


procedure TNameManager.ResetInputArea;
var
  addWrap: TJSHTMLElement;
  addButton: TJSHTMLButtonElement;
  inputEl: TJSHTMLInputElement;
  invalidEl: TJSHTMLElement;
begin
  addWrap := GetElement('nm_add_wrap');
  addButton := TJSHTMLButtonElement(document.getElementById('btn_nm_add_another'));
  inputEl := TJSHTMLInputElement(document.getElementById('nm_name_input'));
  invalidEl := GetElement('nm_name_invalid');

  if Assigned(addWrap) then
    addWrap.classList.add('d-none');

  if Assigned(addButton) then
  begin
    addButton.disabled := False;
    addButton.classList.remove('disabled');
  end;

  if Assigned(inputEl) then
  begin
    inputEl.value := '';
    inputEl.classList.remove('is-invalid');
  end;

  if Assigned(invalidEl) then
  begin
    invalidEl.innerHTML := '';
    invalidEl.classList.add('d-none');
  end;
end;


procedure TNameManager.ShowValidation(const AMessage: string);
var
  inputEl: TJSHTMLInputElement;
  invalidEl: TJSHTMLElement;
begin
  inputEl := TJSHTMLInputElement(document.getElementById('nm_name_input'));
  invalidEl := GetElement('nm_name_invalid');

  if Assigned(inputEl) then
  begin
    inputEl.classList.add('is-invalid');
    inputEl.focus;
  end;

  if Assigned(invalidEl) then
  begin
    invalidEl.innerHTML := HtmlEncode(AMessage);
    invalidEl.classList.remove('d-none');
  end;
end;


procedure TNameManager.RenderExistingList;
var
  listEl: TJSHTMLElement;
  items: TJSArray;
  i: Integer;
  itemText: string;
  html: string;
begin
  listEl := GetElement('nm_existing_list');
  if not Assigned(listEl) then
    Exit;

  items := GetOptionsForField(FCurrentField);

  if Assigned(items) then
    console.log('NameManager.RenderExistingList field=' + FCurrentField + ' count=' + IntToStr(items.length))
  else
    console.log('NameManager.RenderExistingList field=' + FCurrentField + ' count=0');

  html := '';
  if Assigned(items) then
  begin
    for i := 0 to items.length - 1 do
    begin
      itemText := string(items[i]);
      html := html +
        '<div class="list-group-item disabled text-body bg-body-tertiary">' +
          HtmlEncode(itemText) +
        '</div>';
    end;
  end;

  listEl.innerHTML := html;
end;


procedure TNameManager.ShowAddArea;
var
  addWrap: TJSHTMLElement;
  addButton: TJSHTMLButtonElement;
  inputEl: TJSHTMLInputElement;
begin
  console.log('NameManager.ShowAddArea field=' + FCurrentField);

  addWrap := GetElement('nm_add_wrap');
  addButton := TJSHTMLButtonElement(document.getElementById('btn_nm_add_another'));
  inputEl := TJSHTMLInputElement(document.getElementById('nm_name_input'));

  if Assigned(addWrap) then
    addWrap.classList.remove('d-none');

  if Assigned(addButton) then
  begin
    addButton.disabled := True;
    addButton.classList.add('disabled');
  end;

  if Assigned(inputEl) then
    inputEl.focus;
end;


procedure TNameManager.HandleAddClick;
var
  items: TJSArray;
  inputEl: TJSHTMLInputElement;
  invalidEl: TJSHTMLElement;
  newName: string;
begin
  inputEl := TJSHTMLInputElement(document.getElementById('nm_name_input'));
  invalidEl := GetElement('nm_name_invalid');

  if not Assigned(inputEl) then
    Exit;

  newName := Trim(string(inputEl.value));

  console.log('NameManager.HandleAddClick field=' + FCurrentField + '" trimmed="' + newName + '"');

  inputEl.classList.remove('is-invalid');
  if Assigned(invalidEl) then
  begin
    invalidEl.innerHTML := '';
    invalidEl.classList.add('d-none');
  end;

  if newName = '' then
  begin
    ShowValidation('Enter a name.');
    Exit;
  end;

  items := GetOptionsForField(FCurrentField);
  if not Assigned(items) then
    Exit;

  if HasExactOption(items, newName) then
  begin
    console.log('NameManager.DuplicateName field=' + FCurrentField + ' value="' + newName + '"');
    ShowValidation('This name already exists.');
    Exit;
  end;

  items.push(newName);

  console.log('NameManager.AddedName field=' + FCurrentField + ' value="' + newName + '" newCount=' + IntToStr(items.length));

  if Assigned(FRenderTable) then
    FRenderTable;

  CloseManager;
end;


procedure TNameManager.BindControls;
var
  el: TJSHTMLElement;
begin
  el := GetElement('btn_nm_add_another');
  if Assigned(el) then
  begin
    if string(el.getAttribute('data-nm-bound-add-another')) <> '1' then
    begin
      el.addEventListener('click', TJSEventHandler(@AddAnotherClick));
      el.setAttribute('data-nm-bound-add-another', '1');
    end;
  end;

  el := GetElement('btn_nm_save');
  if Assigned(el) then
  begin
    if string(el.getAttribute('data-nm-bound-save')) <> '1' then
    begin
      el.addEventListener('click', TJSEventHandler(@SaveClick));
      el.setAttribute('data-nm-bound-save', '1');
    end;
  end;

  el := GetElement('nm_name_input');
  if Assigned(el) then
  begin
    if string(el.getAttribute('data-nm-bound-keydown')) <> '1' then
    begin
      el.addEventListener('keydown', TJSEventHandler(@NameInputKeyDown));
      el.setAttribute('data-nm-bound-keydown', '1');
    end;
  end;

  el := GetElement('offcanvasNameManager');
  if Assigned(el) then
  begin
    if string(el.getAttribute('data-nm-bound-hidden')) <> '1' then
    begin
      el.addEventListener('hidden.bs.offcanvas', TJSEventHandler(@OffcanvasHidden));
      el.setAttribute('data-nm-bound-hidden', '1');
    end;
  end;
end;


procedure TNameManager.AddAnotherClick(Event: TJSEvent);
begin
  Event.preventDefault;
  ShowAddArea;
end;


procedure TNameManager.SaveClick(Event: TJSEvent);
begin
  Event.preventDefault;
  HandleAddClick;
end;


procedure TNameManager.NameInputKeyDown(Event: TJSEvent);
var
  keyEvent: TJSKeyboardEvent;
begin
  keyEvent := TJSKeyboardEvent(Event);
  if keyEvent.key = 'Enter' then
  begin
    keyEvent.preventDefault;
    HandleAddClick;
  end;
end;


procedure TNameManager.OffcanvasHidden(Event: TJSEvent);
begin
  HandleHidden;
end;


procedure TNameManager.OpenManager(const AFieldName: string; const ARowIndex: Integer; const ATriggerId: string);
var
  titleEl: TJSHTMLElement;
begin
  console.log('NameManager.OpenManager field=' + AFieldName + ' row=' + IntToStr(ARowIndex));
  FCurrentField := AFieldName;
  FCurrentRowIndex := ARowIndex;
  FCurrentTriggerId := ATriggerId;

  titleEl := GetElement('nm_title');
  if Assigned(titleEl) then
  begin
    if SameText(AFieldName, 'reportedBy') then
      titleEl.innerHTML := 'Add Reported By'
    else
      titleEl.innerHTML := 'Add Assigned To';
  end;

  RenderExistingList;
  ResetInputArea;

  asm
    var el = document.getElementById('offcanvasNameManager');
    if (el) {
      var oc = bootstrap.Offcanvas.getOrCreateInstance(el);
      oc.show();
    }
  end;
end;


procedure TNameManager.CloseManager;
begin
  asm
    var el = document.getElementById('offcanvasNameManager');
    if (el) {
      var oc = bootstrap.Offcanvas.getOrCreateInstance(el);
      oc.hide();
    }
  end;
end;


procedure TNameManager.HandleHidden;
begin
  ResetInputArea;

  if Assigned(FFocusTrigger) and (FCurrentTriggerId <> '') then
    FFocusTrigger(FCurrentTriggerId);

  FCurrentField := '';
  FCurrentRowIndex := -1;
  FCurrentTriggerId := '';
end;

end.
