Source

frontend/controllers/tags_controller.js

/* This file is part of Ezra Bible App.

   Copyright (C) 2019 - 2023 Ezra Bible App Development Team <contact@ezrabibleapp.net>

   Ezra Bible App is free software: you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation, either version 2 of the License, or
   (at your option) any later version.

   Ezra Bible App is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with Ezra Bible App. See the file LICENSE.
   If not, see <http://www.gnu.org/licenses/>. */

const TagStore = require('../components/tags/tag_store.js');
const TagListFilter = require('../components/tags/tag_list_filter.js');
const VerseBoxHelper = require('../helpers/verse_box_helper.js');
const VerseBox = require('../ui_models/verse_box.js');
require('../components/emoji_button_trigger.js');
const { waitUntilIdle } = require('../helpers/ezra_helper.js');
const eventController = require('./event_controller.js');
const verseListController = require('../controllers/verse_list_controller.js');
const { showErrorDialog } = require('../helpers/ezra_helper.js');

/**
 * The TagsController handles most functionality related to tagging of verses.
 * 
 * Like all other controllers it is only initialized once. It is accessible at the
 * global object `app_controller.tags_controller`.
 * 
 * @category Controller
 */
class TagsController {
  constructor() {
    loadScript("app/templates/tag_list.js");

    this.tag_store = new TagStore();
    this.tag_list_filter = new TagListFilter();

    this.verse_box_helper = new VerseBoxHelper();

    this.new_standard_tag_button = $('#new-standard-tag-button');
    this.verse_selection_blocked = false;
    this.verses_were_selected_before = false;

    this.assign_tag_label = i18n.t("tags.assign-tag");
    this.unassign_tag_label = i18n.t("tags.remove-tag-assignment");
    this.assign_tag_hint = i18n.t("tags.assign-tag-hint");
  
    this.tag_to_be_deleted = null;
    this.tag_to_be_deleted_title = null;
    this.tag_to_be_deleted_is_global = false;
    this.permanently_delete_tag = true;
  
    this.remove_tag_assignment_job = null;
    this.new_tag_created = false;
    this.last_created_tag = "";
  
    this.edit_tag_id = null;
  
    //this.xml_tag_statistics = null; // FIXME
    this.loading_indicator = "<img class=\"loading-indicator\" style=\"float: left; margin-left: 0.5em;\" " +
                             "width=\"16\" height=\"16\" src=\"images/loading_animation.gif\" />";
  
    this.selected_verse_references = [];
    this.selected_verse_boxes = [];

    this.initialRenderingDone = false;
    this.newTagDialogInitDone = false;
    this.addTagsToGroupDialogInitDone = false;
    this.deleteTagConfirmationDialogInitDone = false;
    this.removeTagAssignmentConfirmationDialogInitDone = false;
    this.editTagDialogInitDone = false;
    this.lastContentId = null;
    this.currentTagGroupId = null;
    this.currentTagGroupTitle = null;

    this.subscribeEvents();
  }

  async handleTagPanelSwitched(isOpen) {
    if (isOpen) {
      await this.updateTagsView(undefined, !this.initialRenderingDone);
    } else if (platformHelper.isCordova() || platformHelper.isMobile()) {
      // Reset tag list on mobile when switching off the tag panel
      this.initialRenderingDone = false;
      document.getElementById('tags-content-global').innerHTML = "";
    }
  }

  subscribeEvents() {
    eventController.subscribe('on-tag-panel-switched', async (isOpen) => {
      await this.handleTagPanelSwitched(isOpen);
    });

    eventController.subscribePrioritized('on-tag-statistics-panel-switched', async (isOpen) => {
      await this.handleTagPanelSwitched(isOpen);
    });

    eventController.subscribe('on-tab-selected', async (tabIndex) => {
      const currentTab = app_controller.tab_controller.getTab(tabIndex);

      if (currentTab != null) {
        // Assume that verses were selected before, because otherwise the checkboxes may not be properly cleared
        this.verses_were_selected_before = true;

        if (this.tagPanelIsActive()) {
          await this.updateTagsView(tabIndex, !this.initialRenderingDone);
        }

        if (currentTab.addedInteractively) {
          this.resetActivePanelToTagPanel(tabIndex);
        }
      }
    });

    eventController.subscribe('on-bible-text-loaded', () => {
      var currentTabIndex = app_controller.tab_controller.getSelectedTabIndex();
      const currentTab = app_controller.tab_controller.getTab(currentTabIndex);

      if (currentTab != null && currentTab.addedInteractively) {
        this.resetActivePanelToTagPanel(currentTabIndex);
      }
    });

    eventController.subscribe('on-module-search-started', (tabIndex) => {
      const currentTab = app_controller.tab_controller.getTab(tabIndex);

      if (currentTab != null && currentTab.addedInteractively) {
        this.resetActivePanelToTagPanel(tabIndex);
      }
    });

    eventController.subscribe('on-locale-changed', async () => {
      this.updateTagsView(undefined, true);
      this.refreshTagDialogs();
    });

    eventController.subscribeMultiple(['on-translation-added', 'on-translation-removed'], async () => {
      await this.updateTagUiBasedOnTagAvailability();
    });

    eventController.subscribe('on-verses-selected', async () => {
      await this.updateTagsViewAfterVerseSelection(false);
    });

    eventController.subscribe('on-tag-group-list-activated', () => {
      this.tag_list_filter.reset();
      document.getElementById('tags-content-global').style.display = 'none';
      document.getElementById('tag-list-stats').style.visibility = 'hidden';
      document.getElementById('tag-panel-tag-group-list').style.removeProperty('display');
      document.getElementById('tag-list-filter-button').style.display = 'none';
      document.getElementById('tags-search-input').value = "";
      document.getElementById('tags-search-input').style.display = 'none';
    });

    eventController.subscribe('on-tag-group-selected', async (tagGroup) => {
      let tab = app_controller.tab_controller.getTab();
      let tagGroupId = tagGroup ? tagGroup.id : null;
      this.currentTagGroupId = tagGroupId;
      this.currentTagGroupTitle = tagGroup ? tagGroup.title : null;

      ipcSettings.set('lastUsedTagGroupId', tagGroupId);

      document.getElementById('tags-search-input').style.removeProperty('display');
      document.getElementById('tag-list-filter-button').style.removeProperty('display');
      document.getElementById('tag-panel-tag-group-list').style.display = 'none';
      document.getElementById('tags-content-global').innerHTML = "";
      document.getElementById('tags-content-global').style.display = '';
      document.getElementById('tag-list-stats').style.visibility = 'visible';

      if (this.isTagPanelActive()) {
        this.showTagListLoadingIndicator();
        await waitUntilIdle();
        await this.updateTagList(tab.getBook(), tagGroupId, tab.getContentId(), true);
        this.hideTagListLoadingIndicator();
      }
    });

    eventController.subscribe('on-db-refresh', async () => {
      const currentTabIndex = app_controller.tab_controller.getSelectedTabIndex();
      document.getElementById('tags-content-global').innerHTML = "";
      await this.updateTagsView(currentTabIndex, true);
    });

    eventController.subscribe('on-body-clicked', () => {
      const tagsSearchInput = document.getElementById('tags-search-input');
      tagsSearchInput.blur();
    });
  }

  tagPanelIsActive() {
    let tagPanelButton = document.getElementById('tag-panel-button');
    let isActive = tagPanelButton.classList.contains('active');
    return isActive;
  }

  getCurrentTagGroup() {
    let currentTagGroup = null;

    if (tags_controller.currentTagGroupId != null) {
      currentTagGroup = {
        title: tags_controller.currentTagGroupTitle,
        id: tags_controller.currentTagGroupId
      };
    }

    return currentTagGroup;
  }

  isTagPanelActive() {
    const panelButtons = document.getElementById('panel-buttons');
    let activePanel = panelButtons.activePanel;
    return activePanel != '' && (activePanel == 'tag-panel' || activePanel == 'tag-statistics-panel');
  }

  resetActivePanelToTagPanel(tabIndex) {
    var panelButtons = document.getElementById('panel-buttons');
    var tab = app_controller.tab_controller.getTab(tabIndex);

    if (panelButtons.activePanel != "" && panelButtons.activePanel != 'tag-panel') {
      if (tab.isNew() || tab.isVerseList()) {
        panelButtons.activePanel = 'tag-panel';
      }
    }
  }

  /**
   * This is used to refresh the dialogs after the locale changed
   */
  refreshTagDialogs() {
    this.initNewTagDialog(true);
    this.initAddTagsToGroupDialog(true);
    this.initEditTagDialog(true);
    this.initRemoveTagAssignmentConfirmationDialog(true);
    this.initDeleteTagConfirmationDialog(true);
  }

  initNewTagDialog(force=false) {
    if (!force && this.newTagDialogInitDone) {
      return;
    }

    this.newTagDialogInitDone = true;

    var dialogWidth = 450;
    var dialogHeight = 420;
    var draggable = true;
    var position = [55, 120];

    let new_standard_tag_dlg_options = uiHelper.getDialogOptions(dialogWidth, dialogHeight, draggable, position);
    new_standard_tag_dlg_options.dialogClass = 'ezra-dialog new-tag-dialog';
    new_standard_tag_dlg_options.title = i18n.t("tags.new-tag");
    new_standard_tag_dlg_options.autoOpen = false;  
    new_standard_tag_dlg_options.buttons = {};

    new_standard_tag_dlg_options.buttons[i18n.t("general.cancel")] = function() {
      setTimeout(() => { $(this).dialog("close"); }, 100);
    };

    new_standard_tag_dlg_options.buttons[i18n.t("tags.create-tag")] = {
      id: 'create-tag-button',
      text: i18n.t("tags.create-tag"),
      click: function() {
        tags_controller.saveNewTag(this);
      }
    };

    document.getElementById('add-existing-tags-to-tag-group-link').addEventListener('click', async (event) => {
      event.preventDefault();

      tags_controller.initAddTagsToGroupDialog();

      const addTagsToGroupFilterInput = document.getElementById('add-tags-to-group-filter-input');
      addTagsToGroupFilterInput.value = '';

      const addTagsToGroupTagList = document.getElementById('add-tags-to-group-tag-list');
      addTagsToGroupTagList.style.removeProperty('display');

      if (platformHelper.isCordova()) {
        // eslint-disable-next-line no-undef
        if (Keyboard.isVisible) {
          // We need to remember the current window height as the keyboard is shown
          // When closing the current dialog the keyboard will go away and in that moment of "flickering"
          // it is hard to determine the window height right at the time when the new dialog is opened.
          let currentWindowHeight = $(window).height();

          // We slightly reduce the height of the add-tags-to-group-dialog - this is based on testing/experience.
          $('#add-tags-to-group-dialog').dialog("option", "height", currentWindowHeight - 18);
        }
      }

      $('#new-standard-tag-dialog').dialog("close");
      $('#add-tags-to-group-dialog').dialog("open");
      await waitUntilIdle();
    });
  
    $('#new-standard-tag-dialog').dialog(new_standard_tag_dlg_options);
    uiHelper.fixDialogCloseIconOnAndroid('new-tag-dialog');

    // Handle the enter key in the tag title field and create the tag when it is pressed
    $('#new-standard-tag-title-input:not(.bound)').addClass('bound').on("keypress", async (event) => {
      if (event.which == 13) {
        var tag_title = $('#new-standard-tag-title-input').val();
        var tagExisting = await this.updateButtonStateBasedOnTagTitleValidation(tag_title, 'create-tag-button');

        if (tagExisting) {
          return;
        }

        $('#new-standard-tag-dialog').dialog("close");
        tags_controller.saveNewTag(event);
      }
    // eslint-disable-next-line no-unused-vars
    }).on("keyup", async (event) => {
      var tag_title = $('#new-standard-tag-title-input').val();
      await this.updateButtonStateBasedOnTagTitleValidation(tag_title, 'create-tag-button');
    });
  }

  async updateAddTagToGroupTagList() {
    const addTagsToGroupTagList = document.getElementById('add-tags-to-group-tag-list');
    addTagsToGroupTagList.tagManager.reset();
    addTagsToGroupTagList.tagManager.setFilter('');
    await addTagsToGroupTagList.tagManager.refreshItemList();
    let tagList = await this.tag_store.getTagList();
    let tagIdList = await this.tag_store.getTagGroupMemberIds(this.currentTagGroupId, tagList);
    addTagsToGroupTagList.tagManager.setExcludeItems(tagIdList);
    addTagsToGroupTagList.tagManager.excludeItems();

    let tagCount = addTagsToGroupTagList.tagManager.getAllItemElements().length;
    return tagCount;
  }

  initAddTagsToGroupDialog(force=false) {
    if (!force && this.addTagsToGroupDialogInitDone) {
      return;
    }

    this.addTagsToGroupDialogInitDone = true;

    var dialogWidth = 450;
    var dialogHeight = 480;
    var draggable = true;
    var position = [55, 120];

    let addTagsToGroupDialogOptions = uiHelper.getDialogOptions(dialogWidth, dialogHeight, draggable, position);
    addTagsToGroupDialogOptions.dialogClass = 'ezra-dialog add-tags-to-group-dialog';
    addTagsToGroupDialogOptions.title = i18n.t("tags.add-tags-to-group");
    addTagsToGroupDialogOptions.autoOpen = false;
  
    addTagsToGroupDialogOptions.buttons = {};
    addTagsToGroupDialogOptions.buttons[i18n.t("general.cancel")] = function() {
      $(this).dialog("close");
    };
    addTagsToGroupDialogOptions.buttons[i18n.t("tags.add-tags-to-group")] = {
      id: 'add-tags-to-group-button',
      text: i18n.t("tags.add-tags-to-group"),
      click: function() {
        $(this).dialog("close");

        const addTagsToGroupTagList = document.getElementById('add-tags-to-group-tag-list');
        tags_controller.addTagsToGroup(tags_controller.currentTagGroupId, addTagsToGroupTagList.addList);
      }
    };

    document.getElementById('add-tags-to-group-filter-input').addEventListener('keyup', () => {
      let currentFilterString = document.getElementById('add-tags-to-group-filter-input').value;
      const addTagsToGroupTagList = document.getElementById('add-tags-to-group-tag-list');
      addTagsToGroupTagList.filter = currentFilterString;
    });

    $('#add-tags-to-group-dialog').dialog(addTagsToGroupDialogOptions);
    uiHelper.fixDialogCloseIconOnAndroid('add-tags-to-group-dialog');
  }

  async addTagsToGroup(tagGroupId, tagList) {
    for (let i = 0; i < tagList.length; i++) {
      let tagId = tagList[i];
      let tag = await this.tag_store.getTag(tagId);

      let result = await ipcDb.updateTag(tagId, tag.title, [ tagGroupId ], []);

      if (result.success == false) {
        var message = `The tag <i>${tag.title}</i> could not be updated.<br>
                      An unexpected database error occurred:<br><br>
                      ${result.exception}<br><br>
                      Please restart the app.`;

        await showErrorDialog('Database Error', message);
        uiHelper.hideTextLoadingIndicator();
        return;
      } else {
        await eventController.publishAsync('on-tag-group-members-changed', {
          tagId: tagId,
          addTagGroups: [ tagGroupId ],
          removeTagGroups: []
        });
      }
    }

    if (this.tagGroupUsed()) {
      const currentTabIndex = app_controller.tab_controller.getSelectedTabIndex();
      await this.updateTagsView(currentTabIndex, true);
    }
  }

  tagGroupUsed() {
    return this.currentTagGroupId != null && this.currentTagGroupId > 0;
  }

  async updateButtonStateBasedOnTagTitleValidation(tagTitle, buttonId) {
    tagTitle = tagTitle.trim();
    const tagExisting = await this.tag_store.tagExists(tagTitle);
    let tagButton = document.getElementById(buttonId);

    if (tagExisting || tagTitle == "") {
      uiHelper.disableButton(tagButton);
    } else {
      uiHelper.enableButton(tagButton);
    }

    return tagExisting;
  }

  initDeleteTagConfirmationDialog(force=false) {
    if (!force && this.deleteTagConfirmationDialogInitDone) {
      return;
    }

    this.deleteTagConfirmationDialogInitDone = true;

    var dialogWidth = 400;
    var dialogHeight = null;
    var draggable = true;
    var position = [55, 120];

    let delete_tag_confirmation_dlg_options = uiHelper.getDialogOptions(dialogWidth, dialogHeight, draggable, position);
    delete_tag_confirmation_dlg_options.dialogClass = 'ezra-dialog delete-tag-confirmation-dialog';
    delete_tag_confirmation_dlg_options.title = i18n.t("tags.delete-tag");
    delete_tag_confirmation_dlg_options.autoOpen = false;
  
    delete_tag_confirmation_dlg_options.buttons = {};
    delete_tag_confirmation_dlg_options.buttons[i18n.t("general.cancel")] = function() {
      setTimeout(() => { $(this).dialog("close"); }, 100);
    };
    delete_tag_confirmation_dlg_options.buttons[i18n.t("tags.delete-tag")] = function() {
      tags_controller.deleteTagAfterConfirmation();
    };

    document.getElementById('permanently-delete-tag').addEventListener('change', function() {
      let permanentlyDeleteTagWarning = document.getElementById('permanently-delete-tag-warning');

      if (this.checked) {
        permanentlyDeleteTagWarning.style.visibility = 'visible';
      } else {
        permanentlyDeleteTagWarning.style.visibility = 'hidden';
      }
    });

    $('#delete-tag-confirmation-dialog').dialog(delete_tag_confirmation_dlg_options);
    uiHelper.fixDialogCloseIconOnAndroid('delete-tag-confirmation-dialog');
  }

  initRemoveTagAssignmentConfirmationDialog(force=false) {
    if (!force && this.removeTagAssignmentConfirmationDialogInitDone) {
      return;
    }

    this.removeTagAssignmentConfirmationDialogInitDone = true;

    let remove_tag_assignment_confirmation_dlg_options = uiHelper.getDialogOptions(360, null, true, [55, 120]);
    remove_tag_assignment_confirmation_dlg_options.dialogClass = 'ezra-dialog remove-tag-assignment-confirmation-dialog';
    remove_tag_assignment_confirmation_dlg_options.autoOpen = false;
    remove_tag_assignment_confirmation_dlg_options.title = i18n.t("tags.remove-tag-assignment");
  
    remove_tag_assignment_confirmation_dlg_options.buttons = {};
    remove_tag_assignment_confirmation_dlg_options.buttons[i18n.t("general.cancel")] = function() {
      tags_controller.remove_tag_assignment_job.tag_button.addClass('active');
      tags_controller.remove_tag_assignment_job = null;
  
      $(this).dialog("close");
    };

    remove_tag_assignment_confirmation_dlg_options.buttons[i18n.t("tags.remove-tag-assignment")] = function() {
      tags_controller.removeTagAssignmentAfterConfirmation();
    };

    $('#remove-tag-assignment-confirmation-dialog').dialog(remove_tag_assignment_confirmation_dlg_options);
    uiHelper.fixDialogCloseIconOnAndroid('remove-tag-assignment-confirmation-dialog');

    // eslint-disable-next-line no-unused-vars
    $('#remove-tag-assignment-confirmation-dialog').bind('dialogbeforeclose', function(event) {
      if (!tags_controller.persistence_ongoing && tags_controller.remove_tag_assignment_job != null) {
        tags_controller.remove_tag_assignment_job.tag_button.addClass('active');
        tags_controller.remove_tag_assignment_job = null;
      }
    });
  }

  initEditTagDialog(force=false) {
    if (!force && this.editTagDialogInitDone) {
      return;
    }

    this.editTagDialogInitDone = true;

    var dialogWidth = 450;
    var dialogHeight = 400;
    var draggable = true;
    var position = [55, 120];

    let edit_tag_dlg_options = uiHelper.getDialogOptions(dialogWidth, dialogHeight, draggable, position);
    edit_tag_dlg_options.dialogClass = 'ezra-dialog edit-tag-dialog';
    edit_tag_dlg_options.title = i18n.t("tags.edit-tag");
    edit_tag_dlg_options.autoOpen = false;

    edit_tag_dlg_options.buttons = {};
    edit_tag_dlg_options.buttons[i18n.t("general.cancel")] = function() {
      setTimeout(() => { $(this).dialog("close"); }, 100);
    };

    edit_tag_dlg_options.buttons[i18n.t("general.save")] = {
      id: 'edit-tag-button',
      text: i18n.t("general.save"),
      click: function() {
        tags_controller.closeDialogAndUpdateTag();
      }
    };

    $('#edit-tag-dialog').dialog(edit_tag_dlg_options);
    uiHelper.fixDialogCloseIconOnAndroid('edit-tag-dialog');
  
    // Handle the enter key in the tag title field and rename the tag when it is pressed
    $('#rename-tag-title-input:not(.bound)').addClass('bound').on("keypress", (event) => {
      if (event.which == 13) {
        tags_controller.closeDialogAndUpdateTag();
      }
    // eslint-disable-next-line no-unused-vars
    }).on("keyup", (event) => {
      this.handleEditTagChange();
    });
  }

  async closeDialogAndUpdateTag() {
    var oldTitle = tags_controller.edit_tag_title;
    var newTitle = $('#rename-tag-title-input').val();
    newTitle = newTitle.trim();

    if (newTitle != oldTitle) {
      let tagExisting = await this.updateButtonStateBasedOnTagTitleValidation(newTitle, 'edit-tag-button');

      if (tagExisting) {
        return;
      }
    }

    var tagGroupAssignment = document.getElementById('tag-group-assignment');
    var addTagGroups = tagGroupAssignment.addList;
    var removeTagGroups = tagGroupAssignment.removeList;

    $('#edit-tag-dialog').dialog('close');
    var checkboxTag = this.getCheckboxTag(tags_controller.edit_tag_id);
    var isGlobal = (checkboxTag.parent().attr('id') == 'tags-content-global');
    
    var result = await ipcDb.updateTag(tags_controller.edit_tag_id, newTitle, addTagGroups, removeTagGroups);
    if (result.success == false) {
      var message = `The tag <i>${tags_controller.edit_tag_title}</i> could not be updated.<br>
                     An unexpected database error occurred:<br><br>
                     ${result.exception}<br><br>
                     Please restart the app.`;

      await showErrorDialog('Database Error', message);
      uiHelper.hideTextLoadingIndicator();
      return;
    }

    if (newTitle != oldTitle) {
      await eventController.publishAsync(
        'on-tag-renamed',
        {
          tagId: tags_controller.edit_tag_id,
          oldTitle: tags_controller.edit_tag_title,
          newTitle: newTitle
        }
      );

      tags_controller.updateTagInView(tags_controller.edit_tag_id, newTitle);
      tags_controller.updateTagTitlesInVerseList(tags_controller.edit_tag_id, isGlobal, newTitle);

      tags_controller.sortTagLists();
      await tags_controller.updateTagsViewAfterVerseSelection(true);
    }

    if (addTagGroups.length > 0 || removeTagGroups.length > 0) {
      await eventController.publishAsync('on-tag-group-members-changed', {
        tagId: tags_controller.edit_tag_id,
        addTagGroups,
        removeTagGroups
      });

      if (this.tagGroupUsed()) {
        const currentTabIndex = app_controller.tab_controller.getSelectedTabIndex();
        await this.updateTagsView(currentTabIndex, true);
      }
    }

    await eventController.publishAsync('on-latest-tag-changed', {
      'tagId': tags_controller.edit_tag_id,
      'added': false
    });

    await waitUntilIdle();
    checkboxTag = this.getCheckboxTag(tags_controller.edit_tag_id);
    checkboxTag.effect('bounce', 'fast');
  }

  updateTagInView(id, title) {
    // Rename tag in tag list on the left side
    var checkboxTag = tags_controller.getCheckboxTag(id);
    var label = checkboxTag.find('.cb-label');
    label.text(title);

    // Rename tag in tag selection menu above bible browser
    var tag_selection_entry = $('#tag-browser-tag-' + id).find('.tag-browser-tag-title').find('.tag-browser-tag-title-content');
    tag_selection_entry.text(title);
  }

  async saveNewTag(e) {
    uiHelper.showTextLoadingIndicator();
    $(e).dialog("close");

    await waitUntilIdle(); // Give the dialog some time to close

    var new_tag_title = $('#new-standard-tag-title-input').val();
    tags_controller.new_tag_created = true;
    this.last_created_tag = new_tag_title;
    new_tag_title = new_tag_title.trim();

    let tagGroupAssignment = document.getElementById('new-tag-dialog-tag-group-assignment');
    let tagGroups = tagGroupAssignment.addList;

    var result = await ipcDb.createNewTag(new_tag_title, tagGroups);
    if (result.success == false) {
      var message = `The new tag <i>${new_tag_title}</i> could not be saved.<br>
                     An unexpected database error occurred:<br><br>
                     ${result.exception}<br><br>
                     Please restart the app.`;

      await showErrorDialog('Database Error', message);
      uiHelper.hideTextLoadingIndicator();
      return;
    }

    await eventController.publishAsync('on-tag-created', result.dbObject.id);

    if (this.tagGroupUsed()) {
      await eventController.publishAsync('on-tag-group-members-changed', {
        tagId: result.dbObject.id,
        addTagGroups: [ this.currentTagGroupId ],
        removeTagGroups: []
      });
    }

    await eventController.publishAsync('on-latest-tag-changed', {
      'tagId': result.dbObject.id,
      'added': true
    });

    var tab = app_controller.tab_controller.getTab();
    await tags_controller.updateTagList(tab.getBook(), this.currentTagGroupId, tab.getContentId(), true);
    await tags_controller.updateTagsViewAfterVerseSelection(true);
    uiHelper.hideTextLoadingIndicator();
  }

  async handleNewTagButtonClick(event) {
    if (event.target.classList.contains('ui-state-disabled')) {
      return;
    }

    eventController.publish('on-button-clicked');

    await waitUntilIdle();
    tags_controller.initNewTagDialog();

    const tagInput = document.getElementById('new-standard-tag-title-input');
    tagInput.value = '';
    var $dialogContainer = $('#new-standard-tag-dialog');
    $dialogContainer.dialog('open');

    await waitUntilIdle();

    let allTagGroups = await ipcDb.getAllTagGroups();
    let tagGroupAssignmentSection = document.getElementById('tag-group-assignment-section');
    let tagGroupAssignment = document.getElementById('new-tag-dialog-tag-group-assignment');
    tagGroupAssignment.tagGroupManager._addList = [];

    if (allTagGroups.length == 0) {
      tagGroupAssignmentSection.style.display = 'none';
    } else {
      tagGroupAssignmentSection.style.removeProperty('display');

      await tagGroupAssignment.tagGroupManager.refreshItemList();

      if (this.tagGroupUsed()) {
        tagGroupAssignment.tagGroupManager.enableElementById(this.currentTagGroupId);
        tagGroupAssignment.tagGroupManager._addList = [ this.currentTagGroupId ];
      }
    }

    this.updateButtonStateBasedOnTagTitleValidation('', 'create-tag-button');
    let addExistingTagsLink = document.getElementById('add-existing-tags-to-tag-group-link').parentNode;

    if (this.tagGroupUsed()) {
      let remainingTagCount = await this.updateAddTagToGroupTagList();

      if (remainingTagCount > 0) {
        addExistingTagsLink.style.removeProperty('display');
      } else {
        addExistingTagsLink.style.display = 'none';
      }
    } else {
      addExistingTagsLink.style.display = 'none';
    }

    if (platformHelper.isCordova()) {
      // Focus the input field (and show the screen keyboard) a little bit delayed
      // to give the layout engine some time to render the input field.
      setTimeout(async () => {
        await waitUntilIdle();
        tagInput.focus();
      }, 1000);
    } else {
      tagInput.focus();
    }
  }

  handleDeleteTagButtonClick(event) {
    eventController.publish('on-button-clicked');
    tags_controller.initDeleteTagConfirmationDialog();

    var checkboxTag = $(event.target).closest('.checkbox-tag');
    var tag_id = checkboxTag.attr('tag-id');
    var parent_id = checkboxTag.parent().attr('id');
    var label = checkboxTag.find('.cb-label').html();

    tags_controller.tag_to_be_deleted_is_global = (parent_id == 'tags-content-global');
    tags_controller.tag_to_be_deleted_title = label;
    tags_controller.tag_to_be_deleted = tag_id;
    tags_controller.permanently_delete_tag = tags_controller.tagGroupUsed() ? false : true;
    
    var number_of_tagged_verses = checkboxTag.attr('global-assignment-count');

    let deleteTagFromGroupExplanation = document.getElementById('delete-tag-from-group-explanation');
    let reallyDeleteTagExplanation = document.getElementById('really-delete-tag-explanation');
    let permanentlyDeleteTagBox = document.getElementById('permanently-delete-tag-box');
    let permanentlyDeleteTagWarning = document.getElementById('permanently-delete-tag-warning');
    let tagGroup = this.currentTagGroupTitle;

    let permanentlyDeleteCheckbox = document.getElementById('permanently-delete-tag');
    permanentlyDeleteCheckbox.checked = false;

    if (this.tagGroupUsed()) {
      // Tag group used

      reallyDeleteTagExplanation.style.display = 'none';
      permanentlyDeleteTagWarning.style.visibility = 'hidden';
      permanentlyDeleteTagBox.style.removeProperty('display');

      deleteTagFromGroupExplanation.innerHTML = i18n.t('tags.delete-tag-from-group-explanation', { tag: label, group: tagGroup, interpolation: {escapeValue: false}});
      deleteTagFromGroupExplanation.style.display = 'block';
    } else {
      // All tags - no tag group

      deleteTagFromGroupExplanation.style.display = 'none';
      permanentlyDeleteTagWarning.style.visibility = 'visible';
      permanentlyDeleteTagBox.style.display = 'none';
      reallyDeleteTagExplanation.style.display = 'block';
    }

    $('#delete-tag-name').html(label);
    $('#delete-tag-number-of-verses').html(number_of_tagged_verses); // FIXME
    $('#delete-tag-confirmation-dialog').dialog('open');
  }

  deleteTagAfterConfirmation() {
    $('#delete-tag-confirmation-dialog').dialog('close');

    tags_controller.permanently_delete_tag = document.getElementById('permanently-delete-tag').checked;
   
    setTimeout(async () => {
      let result = null;

      if (!tags_controller.tagGroupUsed() || tags_controller.permanently_delete_tag) {
        // Permanently delete tag
        result = await ipcDb.removeTag(tags_controller.tag_to_be_deleted);
      } else {
        // Remove tag from current tag group
        result = await ipcDb.updateTag(tags_controller.tag_to_be_deleted,
                                       tags_controller.tag_to_be_deleted_title,
                                       [],
                                       [ tags_controller.currentTagGroupId ]);
      }

      if (result.success == false) {
        var message = `The tag <i>${tags_controller.tag_to_be_deleted_title}</i> could not be deleted.<br>
                      An unexpected database error occurred:<br><br>
                      ${result.exception}<br><br>
                      Please restart the app.`;

        await showErrorDialog('Database Error', message);
        uiHelper.hideTextLoadingIndicator();
        return;
      }

      await tags_controller.removeTagById(tags_controller.tag_to_be_deleted, tags_controller.tag_to_be_deleted_title);

      if (tags_controller.tagGroupUsed()) {
        await eventController.publishAsync('on-tag-group-members-changed', {
          tagId: tags_controller.tag_to_be_deleted,
          addTagGroups: [],
          removeTagGroups: [ this.currentTagGroupId ]
        });
      } else {
        await eventController.publishAsync('on-tag-deleted', tags_controller.tag_to_be_deleted);
      }

      await tags_controller.updateTagsViewAfterVerseSelection(true);
      await tags_controller.updateTagUiBasedOnTagAvailability();
    }, 50);
  }

  async removeTagById(tag_id, tag_title) {
    var checkboxTag = tags_controller.getCheckboxTag(tag_id);
    checkboxTag.detach();

    if (!tags_controller.tagGroupUsed()) {
      if (this.tag_store.latest_tag_id != null && this.tag_store.latest_tag_id == tag_id) {
        this.tag_store.latest_tag_id = null;
        await this.tag_store.refreshTagList();
      }
    }

    tags_controller.updateTagCountAfterRendering();

    // eslint-disable-next-line no-unused-vars
    var tag_data_elements = $('.tag-id').filter(function(index){
      return ($(this).html() == tag_id);
    });

    if (!tags_controller.tagGroupUsed()) {
      var verse_list = $.create_xml_doc(
        app_controller.verse_selection.element_list_to_xml_verse_list(tag_data_elements)
      );

      tags_controller.changeVerseListTagInfo(tag_id, tag_title, verse_list, "remove");
    }
  }

  async assignLastTag() {
    app_controller.hideAllMenus();
    uiHelper.showTextLoadingIndicator();
    await waitUntilIdle();

    if (this.tag_store.latest_tag_id != null) {
      var checkboxTag = this.getCheckboxTag(this.tag_store.latest_tag_id);
      await this.clickCheckBoxTag(checkboxTag);
    }

    uiHelper.hideTextLoadingIndicator();
  }

  async handleTagLabelClick(event) {
    var checkboxTag = $(event.target).closest('.checkbox-tag');
    await this.clickCheckBoxTag(checkboxTag);
  }

  async clickCheckBoxTag(checkboxTag) {
    var current_verse_list = app_controller.verse_selection.selectedVerseReferences;

    if (!tags_controller.is_blocked && current_verse_list.length > 0) {
      this.toggleTagButton(checkboxTag);
      await tags_controller.handleCheckboxTagStateChange(checkboxTag);
    }
  }

  toggleTagButton(checkboxTag) {
    var tag_button = checkboxTag[0].querySelector('.tag-button');
    var isActive = tag_button.classList.contains('active');

    if (isActive) {
      tag_button.classList.remove('active');
      tag_button.classList.add('no-hl');

      if (platformHelper.isElectron()) {
        tag_button.addEventListener('mouseleave', tags_controller.removeTagButtonNoHl);
      }
    } else {
      tag_button.classList.add('active');
    }
  }

  removeTagButtonNoHl(event) {
    event.target.classList.remove('no-hl');
    event.target.removeEventListener('mouseleave', tags_controller.removeTagButtonNoHl);
  }

  async handleTagCbClick(event) {
    await waitUntilIdle();

    var checkboxTag = $(event.target).closest('.checkbox-tag');
    this.toggleTagButton(checkboxTag);
    await tags_controller.handleCheckboxTagStateChange(checkboxTag);
  }

  async handleCheckboxTagStateChange(checkboxTag) {
    var current_verse_list = app_controller.verse_selection.selectedVerseReferences;

    if (tags_controller.is_blocked || current_verse_list.length == 0) {
      return;
    }

    tags_controller.is_blocked = true;
    setTimeout(function() {
      tags_controller.is_blocked = false;
    }, 300);

    var id = parseInt(checkboxTag.attr('tag-id'));
    var tag_button = checkboxTag[0].querySelector('.tag-button');
    var cb_label = checkboxTag.find('.cb-label').html();
    var tag_button_is_active = tag_button.classList.contains('active');

    var current_verse_selection = app_controller.verse_selection.current_verse_selection_as_xml(); 
    var current_verse_reference_ids = app_controller.verse_selection.current_verse_selection_as_verse_reference_ids();

    checkboxTag.find('.cb-label').removeClass('underline');
    checkboxTag.find('.cb-label-postfix').html('');

    var is_global = false;
    if (checkboxTag.find('.is-global').html() == 'true') {
      is_global = true;
    }

    if (tag_button_is_active) {
      // Update last used timestamp
      var current_timestamp = new Date(Date.now()).getTime();
      checkboxTag.attr('last-used-timestamp', current_timestamp);

      this.tag_store.updateTagTimestamp(id, current_timestamp);
      await this.tag_store.updateLatestAndOldestTagData();

      app_controller.tag_selection_menu.updateLastUsedTimestamp(id, current_timestamp);
      app_controller.tag_selection_menu.applyCurrentFilters();

      $(tag_button).attr('title', i18n.t("tags.remove-tag-assignment"));

      var filteredVerseBoxes = [];
      var currentVerseList = verseListController.getCurrentVerseList();

      // Create a list of filtered ids, that only contains the verses that do not have the selected tag yet
      for (let i = 0; i < current_verse_reference_ids.length; i++) {
        var currentVerseReferenceId = current_verse_reference_ids[i];
        var currentVerseBox = currentVerseList[0].querySelector('.verse-reference-id-' + currentVerseReferenceId);

        if (currentVerseBox != null) {
          var existingTagIdElements = currentVerseBox.querySelectorAll('.tag-id');
          var existingTagIds = [];
          
          for (let j = 0; j < existingTagIdElements.length; j++) {
            var currentTagId = parseInt(existingTagIdElements[j].innerText);
            existingTagIds.push(currentTagId);
          }

          if (!existingTagIds.includes(id)) {
            filteredVerseBoxes.push(currentVerseBox);
          }
        }
      }

      var result = await ipcDb.assignTagToVerses(id, filteredVerseBoxes);
      if (result.success == false) {
        var message = `The tag <i>${cb_label}</i> could not be assigned to the selected verses.<br>
                      An unexpected database error occurred:<br><br>
                      ${result.exception}<br><br>
                      Please restart the app.`;

        await showErrorDialog('Database Error', message);
        uiHelper.hideTextLoadingIndicator();
        return;
      }

      tags_controller.changeVerseListTagInfo(id,
                                             cb_label,
                                             $.create_xml_doc(current_verse_selection),
                                             "assign");

      await eventController.publishAsync('on-latest-tag-changed', {
        'tagId': id,
        'added': true
      });

      var currentBook = app_controller.tab_controller.getTab().getBook();

      tags_controller.updateTagCountAfterRendering(currentBook != null);
      await tags_controller.updateTagsViewAfterVerseSelection(true);
      await tags_controller.updateTagUiBasedOnTagAvailability();

    } else {

      tags_controller.remove_tag_assignment_job = {
        'id': id,
        'is_global': is_global,
        'cb_label': cb_label,
        'checkboxTag': checkboxTag,
        'verse_list': current_verse_list,
        'verse_ids': current_verse_reference_ids,
        'xml_verse_selection': $.create_xml_doc(current_verse_selection),
        'tag_button': $(tag_button)
      };

      if (current_verse_list.length > 1) {
        tags_controller.initRemoveTagAssignmentConfirmationDialog();

        $('#remove-tag-assignment-name').html(cb_label);
        $('#remove-tag-assignment-confirmation-dialog').dialog('open');
      } else {
        await tags_controller.removeTagAssignmentAfterConfirmation();
        await tags_controller.updateTagsViewAfterVerseSelection(true);
      }
    }
  }
  
  getCheckboxTag(id) {
    var checkboxTag = $('#tags-content-global').find('.checkbox-tag[tag-id="' + id + '"]');
    return checkboxTag;
  }

  updateTagVerseCount(id, verseBoxes, to_increment) {
    var count = verseBoxes.length;
    var checkboxTag = tags_controller.getCheckboxTag(id);
    var cb_label_element = checkboxTag.find('.cb-label');
    var tag_title = cb_label_element.text();
    var tag_assignment_count_element = checkboxTag.find('.cb-label-tag-assignment-count');
    var tag_assignment_count_values = tag_assignment_count_element.text().substring(
      1, tag_assignment_count_element.text().length - 1
    );

    var current_book_count = 0;
    var current_global_count = 0;
    var new_book_count = 0;
    var new_global_count = 0;

    var currentBook = app_controller.tab_controller.getTab().getBook();

    if (currentBook == null) {
      current_global_count = parseInt(tag_assignment_count_values);
    } else {
      current_book_count = parseInt(tag_assignment_count_values.split('|')[0]);
      current_global_count = parseInt(tag_assignment_count_values.split('|')[1]);
    }

    if (to_increment) {
      new_book_count = current_book_count + count;
      new_global_count = current_global_count + count;
    } else {
      new_book_count = current_book_count - count;
      new_global_count = current_global_count - count;
    }

    if (new_book_count > 0) {
      cb_label_element.addClass('cb-label-assigned');
    } else {
      cb_label_element.removeClass('cb-label-assigned');
    }

    checkboxTag.attr('book-assignment-count', new_book_count);
    checkboxTag.attr('global-assignment-count', new_global_count);

    var new_label = "";
    if (currentBook == null) {
      new_label = "(" + new_global_count + ")";
    } else {
      new_label = "(" + new_book_count + " | " + new_global_count + ")";
    }

    tag_assignment_count_element.text(new_label);

    // Update tag count in tag store statistics
    var bookList = this.verse_box_helper.getBookListFromVerseBoxes(verseBoxes);
    tags_controller.tag_store.updateTagCount(id, bookList, count, to_increment);

    // Update tag count in tag selection menu as well
    app_controller.tag_selection_menu.updateVerseCountInTagMenu(tag_title, new_global_count);
  }

  async removeTagAssignmentAfterConfirmation() {
    tags_controller.persistence_ongoing = true;
    $('#remove-tag-assignment-confirmation-dialog').dialog('close');

    var job = tags_controller.remove_tag_assignment_job;
    tags_controller.changeVerseListTagInfo(job.id,
                                           job.cb_label,
                                           job.xml_verse_selection,
                                           "remove");

    job.tag_button.attr('title', i18n.t("tags.assign-tag"));
    job.checkboxTag.append(tags_controller.loading_indicator);

    var verse_boxes = [];

    var currentVerseList = verseListController.getCurrentVerseList();

    for (let i = 0; i < job.verse_ids.length; i++) {
      var currentVerseReferenceId = job.verse_ids[i];
      var currentVerseBox = currentVerseList[0].querySelector('.verse-reference-id-' + currentVerseReferenceId);
      verse_boxes.push(currentVerseBox);
    }

    var result = await ipcDb.removeTagFromVerses(job.id, verse_boxes);
    if (result.success == false) {
      var message = `The tag <i>${job.cb_label}</i> could not be removed from the selected verses.<br>
                    An unexpected database error occurred:<br><br>
                    ${result.exception}<br><br>
                    Please restart the app.`;

      await showErrorDialog('Database Error', message);
      uiHelper.hideTextLoadingIndicator();
      return;
    }

    await eventController.publishAsync('on-latest-tag-changed', {
      'tagId': job.id,
      'added': false
    });

    var currentBook = app_controller.tab_controller.getTab().getBook();
    tags_controller.updateTagCountAfterRendering(currentBook != null);
    tags_controller.updateTagUiBasedOnTagAvailability();

    tags_controller.remove_tag_assignment_job = null;
    tags_controller.persistence_ongoing = false;
  }

  /**
   * This function updates the tag info in existing verse lists after tags have been assigned/removed.
   * It does this for the currently opened tab and also within all other tabs where the corresponding verse is loaded.
   */
  async changeVerseListTagInfo(tag_id,
                               tag_title,
                               verse_selection,
                               action) {

    verse_selection = $(verse_selection);
    var selected_verses = verse_selection.find('verse');
    var current_verse_list_frame = verseListController.getCurrentVerseListFrame();

    for (let i = 0; i < selected_verses.length; i++) {
      let current_verse_reference_id = $(selected_verses[i]).find('verse-reference-id').text();
      let current_verse_box = current_verse_list_frame[0].querySelector('.verse-reference-id-' + current_verse_reference_id);

      let verseBoxObj = new VerseBox(current_verse_box);
      let highlight = (action == "assign");

      verseBoxObj.changeVerseListTagInfo(tag_id, tag_title, action, highlight);
    }

    for (let i = 0; i < selected_verses.length; i++) {
      let current_verse_reference_id = $(selected_verses[i]).find('verse-reference-id').text();
      let current_verse_box = current_verse_list_frame[0].querySelector('.verse-reference-id-' + current_verse_reference_id);

      await this.verse_box_helper.iterateAndChangeAllDuplicateVerseBoxes(current_verse_box, { tag_id: tag_id, tag_title: tag_title, action: action }, (changedValue, targetVerseBox) => {
        let verseBoxObj = new VerseBox(targetVerseBox);
        verseBoxObj.changeVerseListTagInfo(changedValue.tag_id, changedValue.tag_title, changedValue.action);
      });
    }
  }

  sortTagLists() {
    var global_tags_box = $('#tags-content-global');
    var sort_function = function(a,b) {
      return ($(a).find('.cb-label').text().toLowerCase() > $(b).find('.cb-label').text().toLowerCase()) ? 1 : -1;
    };

    global_tags_box.find('.checkbox-tag').sort_elements(sort_function);
  }

  async getTagList(forceRefresh=true) {
    var tagList = await this.tag_store.getTagList(forceRefresh);
    return tagList;
  }

  async updateTagList(currentBook, tagGroupId=null, contentId=null, forceRefresh=false) {
    if (tagGroupId == null) {
      tagGroupId = this.currentTagGroupId;
    }

    if (forceRefresh) {
      this.initialRenderingDone = false;
    }

    if (contentId == null) {
      contentId = currentBook;
    }

    if (contentId != this.lastContentId || forceRefresh) {
      var tagList = await this.tag_store.getTagList(forceRefresh);
      if (tagGroupId != null && tagGroupId > 0) {
        tagList = await this.tag_store.getTagGroupMembers(tagGroupId, tagList);
      }

      var tagStatistics = await this.tag_store.getBookTagStatistics(currentBook, forceRefresh);
      await this.renderTags(tagList, tagStatistics, currentBook != null);
      this.initialRenderingDone = true;
      await waitUntilIdle();

      this.lastContentId = contentId;
    } else {
      app_controller.tag_statistics.highlightFrequentlyUsedTags();
    }
  }

  async renderTags(tag_list, tag_statistics, is_book=false) {
    //console.time("renderTags");
    var current_book = app_controller.tab_controller.getTab().getBook();
    var global_tags_box_el = document.getElementById('tags-content-global');

    // Assume that verses were selected before, because otherwise the checkboxes may not be properly cleared
    this.verses_were_selected_before = true;

    // eslint-disable-next-line no-undef
    var all_tags_html = tagListTemplate({
      tags: tag_list,
      tagStatistics: tag_statistics,
      current_book: current_book,
      current_filter: $('#tags-search-input').val(),
      edit_tag_label: i18n.t("tags.edit-tag"),
      delete_tag_label: i18n.t("tags.delete-tag"),
    });

    global_tags_box_el.innerHTML = '';
    global_tags_box_el.innerHTML = all_tags_html;

    await app_controller.tag_statistics.refreshBookTagStatistics(tag_list, tag_statistics, current_book);
    uiHelper.configureButtonStyles('#tags-content');

    tags_controller.updateTagsViewAfterVerseSelection(true);
    tags_controller.updateTagCountAfterRendering(is_book);
    await tags_controller.updateTagUiBasedOnTagAvailability(tag_list.length);

    var old_tags_search_input_value = $('#tags-search-input')[0].value;    
    if (this.new_tag_created && old_tags_search_input_value != "") {
      // If the newly created tag doesn't match the current search input
      // we remove the current search condition. Otherwise the new tag
      // wouldn't show up in the list as expected.
      if (!tags_controller.tag_list_filter.stringMatches(this.last_created_tag,
                                                         $('#tags-search-input')[0].value)) {
        $('#tags-search-input')[0].value = "";
        old_tags_search_input_value = "";
      }
    }    
    this.new_tag_created = false;

    tags_controller.hideTagListLoadingIndicator();
    //console.timeEnd("renderTags");
  }

  async handleEditTagClick(event) {
    eventController.publish('on-button-clicked');
    tags_controller.initEditTagDialog();

    var checkboxTag = $(event.target).closest('.checkbox-tag');
    var cb_label = checkboxTag.find('.cb-label').text();

    tags_controller.edit_tag_id = parseInt(checkboxTag.attr('tag-id'));
    tags_controller.edit_tag_title = cb_label;

    const $tagInput = $('#rename-tag-title-input');
    let tagButton = document.getElementById('edit-tag-button');
    uiHelper.disableButton(tagButton);

    $tagInput.val(cb_label);

    $('#edit-tag-dialog').dialog('open');

    var tagGroupAssignment = document.getElementById('tag-group-assignment');
    await tagGroupAssignment.tagGroupManager.refreshItemList();

    tagGroupAssignment.tagid = tags_controller.edit_tag_id;
    tagGroupAssignment.onChange = () => {
      this.handleEditTagChange();
    };

    if (!platformHelper.isMobile()) {
      $('#rename-tag-title-input').focus();
    }
  }

  handleEditTagChange() {
    let tagGroupAssignment = document.getElementById('tag-group-assignment');
    let tagButton = document.getElementById('edit-tag-button');
    var oldTitle = tags_controller.edit_tag_title;
    var newTitle = document.getElementById('rename-tag-title-input').value;

    if (newTitle != oldTitle || tagGroupAssignment.isChanged) {
      uiHelper.enableButton(tagButton);
    } else {
      uiHelper.disableButton(tagButton);
    }

    if (!tagGroupAssignment.isChanged) {
      this.updateButtonStateBasedOnTagTitleValidation(newTitle, 'edit-tag-button');
    }
  }

  updateTagCountAfterRendering(is_book=false) {
    var global_tag_count = $('#tags-content-global').find('.checkbox-tag').length;
    var global_used_tag_count = $('#tags-content-global').find('.cb-label-assigned').length;
    var tag_list_stats = $($('#tags-content').find('#tag-list-stats'));
    var tag_list_stats_content = "";

    if (is_book) {
      tag_list_stats_content += global_used_tag_count + ' ' + i18n.t('tags.stats-used') + ' / ';
    }

    tag_list_stats_content += global_tag_count + ' ' + i18n.t('tags.stats-total');
    tag_list_stats.html(tag_list_stats_content);
  }

  removeEventListeners(element_list, type, listener) {
    for (let i = 0; i < element_list.length; i++) {
      element_list[i].removeEventListener(type, listener);
    }
  }

  addEventListeners(element_list, type, listener) {
    for (let i = 0; i < element_list.length; i++) {
      element_list[i].addEventListener(type, listener);
    }
  }

  bindTagEvents() {
    var tags_box = document.getElementById('tags-content-global');

    tags_box.addEventListener('click', async function(event) {
      // Use event delegation, so that we do not have to add an event listener to each element.

      const CLICK_TIMEOUT = 100;

      if (event.target.matches('.delete-icon') || event.target.matches('.delete-button')) {
        setTimeout(() => { tags_controller.handleDeleteTagButtonClick(event); }, CLICK_TIMEOUT);
      } else if (event.target.matches('.edit-icon') || event.target.matches('.edit-button')) {
        setTimeout(() => { tags_controller.handleEditTagClick(event); }, CLICK_TIMEOUT);
      } else if (event.target.matches('.tag-button')) {
        await waitUntilIdle();
        await tags_controller.handleTagCbClick(event);
      } else if (event.target.matches('.cb-label')) {
        await waitUntilIdle();
        await tags_controller.handleTagLabelClick(event);
      } else {
        return;
      }
    }, false);
  }

  updateTagTitlesInVerseList(tag_id, is_global, title) {
    var tag_class = is_global ? "tag-global" : "tag-book";

    // eslint-disable-next-line no-unused-vars
    var tag_data_elements = $('.tag-id').filter(function(index) {
      return (($(this).html() == tag_id) && ($(this).parent().hasClass(tag_class)));
    }).closest('.' + tag_class);

    for (let i = 0; i < tag_data_elements.length; i++) {
      var current_tag_data = $(tag_data_elements[i]);
      current_tag_data.find('.tag-title').html(title);

      var current_verse_box = new VerseBox(current_tag_data.closest('.verse-box')[0]);
      current_verse_box.updateTagTooltip();
      current_verse_box.updateVisibleTags();
    }
  }

  async updateTagsViewAfterVerseSelection(force) {
    //console.time('updateTagsViewAfterVerseSelection');
    if (tags_controller.verse_selection_blocked && force !== true) {
      return;
    }

    tags_controller.verse_selection_blocked = true;
    setTimeout(function() {
      tags_controller.verse_selection_blocked = false;
    }, 300);

    var versesSelected = app_controller.verse_selection.getSelectedVerseBoxes().length > 0;
    var selected_verse_tags = [];

    if (versesSelected) { // Verses are selected

      selected_verse_tags = app_controller.verse_selection.getCurrentSelectionTags();
      var checkboxTags = document.querySelectorAll('.checkbox-tag');

      for (let i = 0; i < checkboxTags.length; i++) {
        this.formatCheckboxElementBasedOnSelection(checkboxTags[i], selected_verse_tags);
      }

      this.verses_were_selected_before = true;

    } else { // No verses are selected!

      if (this.verses_were_selected_before) {
        this.uncheckAllCheckboxElements();
      }

      this.verses_were_selected_before = false;
    }

    //console.timeEnd('updateTagsViewAfterVerseSelection');
  }

  formatCheckboxElementBasedOnSelection(cb_element, selected_verse_tags) {
    var current_tag_button = cb_element.querySelector('.tag-button');
    var current_title_element = cb_element.querySelector('.cb-label');
    var current_title = current_title_element.innerHTML;
    var current_title_element_postfix = cb_element.querySelector('.cb-label-postfix');
    var match_found = false;

    for (let j = 0; j < selected_verse_tags.length; j++) {
      var current_tag_obj = selected_verse_tags[j];

      if (current_tag_obj.title == current_title) {
        if (current_tag_obj.complete) {
          current_tag_button.setAttribute('title', this.unassign_tag_label);
          current_tag_button.classList.add('active');
          current_title_element_postfix.innerHTML = '';
          current_title_element.classList.remove('underline');
        } else {
          current_tag_button.setAttribute('title', this.assign_tag_label);
          current_tag_button.classList.remove('active');
          current_title_element_postfix.innerHTML = '&nbsp;*';
          current_title_element.classList.add('underline');
        }

        match_found = true;
      }
    }

    if (!match_found) {
      current_tag_button.classList.remove('active');
      current_tag_button.setAttribute('title', this.assign_tag_label);
      current_title_element.classList.remove('underline');
      current_title_element_postfix.innerHTML = '';
    }

    if (!this.verses_were_selected_before) {
      current_tag_button.classList.remove('disabled');
    }
  }

  uncheckAllCheckboxElements() {
    var all_checkbox_elements = document.querySelectorAll('.checkbox-tag');

    if (all_checkbox_elements.length > 0) {
      for (let i = 0; i < all_checkbox_elements.length; i++) {
        var current_checkbox_element = all_checkbox_elements[i];

        var current_tag_button = current_checkbox_element.querySelector('.tag-button');
        current_tag_button.setAttribute('title', this.assign_tag_hint);
        current_tag_button.classList.add('disabled');
        current_tag_button.classList.remove('active');

        var current_title_element = current_checkbox_element.querySelector('.cb-label');
        current_title_element.classList.remove('underline');

        var current_title_element_postfix = current_checkbox_element.querySelector('.cb-label-postfix');
        current_title_element_postfix.innerHTML = '';
      }
    }
  }

  initTagsUI() {
    $('#tag-list-filter-button').bind('click', (e) => { this.tag_list_filter.handleFilterButtonClick(e); });

    $('#tags-content-global').bind('mouseover', () => { this.tag_list_filter.hideTagFilterMenuIfInToolBar(); });
    $('#tag-filter-menu').find('input').bind('click', (e) => { tags_controller.tag_list_filter.handleTagFilterTypeClick(e); });

    $('#tags-search-input').bind('keyup', (e) => { this.tag_list_filter.handleTagSearchInput(e); });
    $('#tags-search-input').bind('keydown', (e) => {
      e.stopPropagation();
    });

    $('#tags-search-input').bind('mouseup', (e) => {
      e.stopPropagation();
      $('#tags-search-input').select();
    });

    tags_controller.bindTagEvents();
  }

  async updateTagUiBasedOnTagAvailability(tagCount=undefined) {
    var translationCount = app_controller.translation_controller.getTranslationCount();
    if (tagCount === undefined) {
      tagCount = await ipcDb.getTagCount();
    }

    var textType = app_controller.tab_controller.getTab().getTextType();

    if (tagCount == 0) {
      $('.tag-select-button').addClass('ui-state-disabled');
      $('.show-book-tag-statistics-button').addClass('ui-state-disabled');

      if (translationCount > 0) {
        $('#new-standard-tag-button').removeClass('ui-state-disabled');
        $('#tags-content-global').html(i18n.t("help.help-text-no-tags-book-opened", { interpolation: {escapeValue: false} }));
      } else {
        $('#new-standard-tag-button').addClass('ui-state-disabled');
        $('#tags-content-global').html(i18n.t("help.help-text-no-tags-no-book-opened"));
      }
    } else {
      $('.tag-select-button').removeClass('ui-state-disabled');
      $('#new-standard-tag-button').removeClass('ui-state-disabled');

      if (textType == 'book') {
        $('.show-book-tag-statistics-button').removeClass('ui-state-disabled');
      }
    }
  }

  showTagListLoadingIndicator() {
    let tagsContentGlobal = document.getElementById('tags-content-global');
    let loadingIndicator = tagsContentGlobal.querySelector('loading-indicator');

    if (loadingIndicator == null) {
      let element = document.createElement('loading-indicator');
      tagsContentGlobal.appendChild(element);
      loadingIndicator = tagsContentGlobal.querySelector('loading-indicator');
    }

    $(loadingIndicator).find('.loader').show();
    $(loadingIndicator).show();
  }

  hideTagListLoadingIndicator() {
    let tagsContentGlobal = document.getElementById('tags-content-global');
    let loadingIndicator = tagsContentGlobal.querySelector('loading-indicator');
    $(loadingIndicator).hide();
  }

  async updateTagsView(tabIndex, forceRefresh = false) {
    var currentTab = app_controller.tab_controller.getTab(tabIndex);
    var tagCount = await ipcDb.getTagCount();

    if (currentTab !== undefined) {
      if (tagCount > 0) {
        this.showTagListLoadingIndicator();
      }

      await waitUntilIdle();

      var currentTabBook = currentTab.getBook();
      var currentTabContentId = currentTab.getContentId();
      await this.updateTagList(currentTabBook, this.currentTagGroupId, currentTabContentId, forceRefresh);

      this.hideTagListLoadingIndicator();
    }

    await this.updateTagUiBasedOnTagAvailability(tagCount);
  }
}

module.exports = TagsController;