Source

frontend/components/tags/tag_selection_menu.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 { waitUntilIdle } = require('../../helpers/ezra_helper.js');
const eventController = require('../../controllers/event_controller.js');

/**
 * The TagSelectionMenu component implements the menu for selecting a tagged verse list.
 * 
 * @category Component
 */
class TagSelectionMenu {
  constructor() {
    this.tag_menu_is_opened = false;
    this.tag_menu_populated = false;
    this.currentTagGroupId = -1;

    this.bindUserEvents();
    this.subscribeAppEvents();
  }

  bindUserEvents() {
    $('#tag-selection-filter-input').bind('keyup', () => { this.handleTagSearchInput(); });

    // eslint-disable-next-line no-unused-vars
    $('#tag-selection-recently-used-checkbox').bind('click', (event) => {
      this.applyCurrentFilters();
    });

    $('#select-all-tags-button').bind('click', () => {
      this.selectAllTags();
    });

    $('#deselect-all-tags-button').bind('click', () => {
      this.deselectAllTags();
    });

    $('#confirm-tag-selection-button').bind('click', () => {
      this.handleConfirmButtonClick();
    });

    $('#tagSelectionBackButton').bind('click', () => {
      setTimeout(() => { this.hideTagMenu(); }, 100);
    });
  }

  subscribeAppEvents() {
    eventController.subscribe('on-tab-added', (tabIndex) => {
      this.initForTab(tabIndex);
    });

    eventController.subscribeMultiple(['on-tag-created', 'on-tag-deleted'], async () => {
      await this.requestTagsForMenu(this.currentTagGroupId, true);
    });

    eventController.subscribe('on-tag-renamed', async() => {
      await this.requestTagsForMenu(this.currentTagGroupId);
    });

    eventController.subscribe('on-tag-selection-menu-group-list-activated', () => {
      this.getTagListContainer()[0].style.display = 'none';
      document.getElementById('tag-selection-filter-buttons').style.display = 'none';
      document.getElementById('tag-selection-filter').style.display = 'none';
      document.getElementById('tag-selection-summary').style.display = 'none';
      document.getElementById('tag-selection-menu-tag-group-list').style.removeProperty('display');
    });

    eventController.subscribe('on-tag-selection-menu-group-selected', async (tagGroup) => {
      this.showTagGroup(tagGroup.id);
    });
  }

  initForTab(tabIndex=undefined) {
    var currentVerseListMenu = app_controller.getCurrentVerseListMenu(tabIndex);
    currentVerseListMenu.find('.tag-select-button').bind('click', (event) => { this.handleTagMenuClick(event); });
  }

  hideTagGroupList() {
    document.getElementById('tag-selection-menu-tag-group-list').style.display = 'none';
  }

  async showTagGroup(tagGroupId) {
    this.hideTagGroupList();

    this.currentTagGroupId = tagGroupId;
    await this.requestTagsForMenu(this.currentTagGroupId);
    await this.updateTagSelectionMenu();

    this.showTagListUi();
  }

  showTagListUi() {
    this.getTagListContainer()[0].style.removeProperty('display');
    document.getElementById('tag-selection-filter-buttons').style.display = 'flex';
    document.getElementById('tag-selection-filter').style.removeProperty('display');
    document.getElementById('tag-selection-summary').style.display = 'flex';
  }

  selectAllTags() {
    var tagListContainer = this.getTagListContainer()[0];
    tagListContainer.querySelectorAll('.tag-browser-tag-cb').forEach((cb) => { cb.checked = true; });
    this.handleTagSelection();

    var selectAllButton = document.getElementById('select-all-tags-button');
    selectAllButton.classList.add('ui-state-disabled');
  }

  deselectAllTags() {
    var tagListContainer = this.getTagListContainer()[0];
    tagListContainer.querySelectorAll('.tag-browser-tag-cb').forEach((cb) => { cb.checked = false; });
    this.handleTagSelection();

    var selectAllButton = document.getElementById('select-all-tags-button');
    selectAllButton.classList.remove('ui-state-disabled');
  }

  getTagListContainer() {
    return $('#tag-selection-taglist-global');
  }

  applyCurrentFilters() {
    this.showAllTags();
    
    this.applyTagSearchFilter();

    if ($('#tag-selection-recently-used-checkbox').prop('checked') == true) {
      this.filterRecentlyUsed();
    }
  }

  filterRecentlyUsed() {
    var tagListContainer = this.getTagListContainer();

    tagListContainer.find('.tag-browser-tag').filter(function() {
      return tags_controller.tag_store.filterRecentlyUsedTags(this);
    }).hide();
  }

  showAllTags() {
    var tagListContainer = this.getTagListContainer();
    tagListContainer.find('.tag-browser-tag').show();
  }

  async hideTagMenu() {
    if (this.tag_menu_is_opened) {
      document.getElementById('tag-selection-menu').style.display = 'none';
      document.getElementById('app-container').classList.remove('fullscreen-menu');

      let groupList = document.getElementById('tag-selection-menu-tag-group-list');
      let currentGroup = await groupList.tagGroupManager.getItemById(this.currentTagGroupId);

      eventController.publishAsync('on-tag-selection-menu-group-selected', currentGroup);

      var tag_button = $('#app-container').find('.tag-select-button');
      tag_button.removeClass('ui-state-active');

      this.tag_menu_is_opened = false;
    }
  }

  async handleTagMenuClick(event) {
    var currentVerseListMenu = app_controller.getCurrentVerseListMenu();
    var tagSelectButton = currentVerseListMenu.find('.tag-select-button');

    if (tagSelectButton.hasClass('ui-state-disabled')) {
      return;
    }

    if (this.tag_menu_is_opened) {
      app_controller.handleBodyClick();
    } else {
      app_controller.hideAllMenus();

      document.getElementById('app-container').classList.add('fullscreen-menu');

      tagSelectButton.addClass('ui-state-active');
      var tag_select_button_offset = tagSelectButton.offset();

      var menu = $('#app-container').find('#tag-selection-menu');
      var tagList = $('#app-container').find('#tag-selection-taglist-global');
      var tagListOverlay = $('#app-container').find('#tag-selection-taglist-overlay');

      var top_offset = tag_select_button_offset.top + tagSelectButton.height() + 1;
      var left_offset = tag_select_button_offset.left;

      menu.css('top', top_offset);
      menu.css('left', left_offset);

      // Show an overlay while the actual menu is rendering
      tagList.hide();
      tagListOverlay.css('display', 'flex');
      tagListOverlay.find('.loader').show();
      menu.show();

      await waitUntilIdle();
      await this.updateTagSelectionMenu();

      tagList.show();
      tagListOverlay.hide();
      this.showTagListUi();

      uiHelper.configureButtonStyles('#tag-selection-menu');

      if (platformHelper.isElectron()) {
        // We're only focussing the search filter on Electron, because on Android it would trigger the screen keyboard right away
        // and that would be disturbing from a usability perspective.
        $('#tag-selection-filter-input').select();
      }

      this.tag_menu_is_opened = true;
      event.stopPropagation();
    }
  }

  async requestTagsForMenu(tagGroupId=null, forceRefresh=false) {
    var tags = await tags_controller.getTagList(forceRefresh);

    if (tagGroupId != null && tagGroupId > 0) {
      tags = await tags_controller.tag_store.getTagGroupMembers(tagGroupId, tags);
    }

    this.renderTagsInMenu(tags);
    this.tag_menu_populated = true;
  }

  renderTagsInMenu(tags) {
    this.resetTagsInMenu();

    if (tags.length > 50) {
      document.getElementById('select-all-tags-button').style.display = 'none';
    } else {
      document.getElementById('select-all-tags-button').style.removeProperty('display');
    }

    var taglist_container = this.getTagListContainer();
    this.renderTagList(tags, taglist_container, false);
    this.bindClickToCheckboxLabels();
  }

  bindClickToCheckboxLabels() {
    $('.clickable-checkbox-label:not(.events-configured)').bind('click', function() {
      var closest_input = $(this).prevAll('input:first');

      if (closest_input.attr('type') == 'checkbox') {
        closest_input[0].click();
      }
    }).addClass('events-configured');
  }

  renderTagList(tag_list, target_container, only_local) {
    var target_element = target_container[0];
    var all_tags_html = "";

    for (var i = 0; i < tag_list.length; i++) {
      var current_tag = tag_list[i];
      var current_tag_title = current_tag.title;
      var current_tag_id = current_tag.id;
      var current_book_id = current_tag.bibleBookId;
      var current_assignment_count = current_tag.globalAssignmentCount;
      var current_tag_last_used = current_tag.lastUsed;

      if (only_local && (current_book_id == null)) {
        continue;
      }

      var current_tag_html = this.getHtmlForTag(current_tag_id,
                                                current_tag_title,
                                                current_assignment_count,
                                                current_tag_last_used);
      all_tags_html += current_tag_html;
    }

    target_element.innerHTML = all_tags_html;
    this.initTagCBs();
  }

  updateCheckedTags(target_container) {
    var selected_tag_list = this.getSelectedTagList();

    // Check all the previously selected tags in the list
    var all_tags = target_container[0].querySelectorAll('.tag-browser-tag-title-content');
    
    for (var i = 0; i < all_tags.length; i++) {
      var current_tag = all_tags[i];

      var current_tag_is_checked = false;
      if (selected_tag_list !== null) {
        current_tag_is_checked = selected_tag_list.includes(current_tag.textContent);
      }

      var tag_browser_tag = current_tag.closest('.tag-browser-tag');
      var tag_cb = tag_browser_tag.querySelector('.tag-browser-tag-cb'); 

      if (current_tag_is_checked) {
        tag_cb.checked = true;
      } else {
        tag_cb.checked = false;
      }
    }
  }

  getHtmlForTag(tag_id, tag_title, tag_assignment_count, tag_last_used) {
    return "<div id='tag-browser-tag-" + tag_id + 
           "' class='tag-browser-tag' last-used-timestamp='" + tag_last_used + "'>" + 
           "<div class='tag-browser-tag-id'>" + tag_id + "</div>" +
           "<input class='tag-browser-tag-cb' type='checkbox'></input>" +
           "<div class='tag-browser-tag-title clickable-checkbox-label'>" +
           "<span class='tag-browser-tag-title-content'>" + tag_title + "</span>" +
           "<span class='tag-browser-tag-assignment-count'>(" + tag_assignment_count + ")</span>" +
           "</div>" +
           "</div>";
  }

  applyTagSearchFilter() {
    //console.time('filter-tag-list');
    
    var search_value = $('#tag-selection-filter-input').val();
    var tagListContainer = this.getTagListContainer();

    var labels = tagListContainer.find('.tag-browser-tag-title-content');
    tagListContainer.find('.tag-browser-tag').hide();

    for (var i = 0; i < labels.length; i++) {
      var current_label = $(labels[i]);

      if (current_label.text().toLowerCase().indexOf(search_value.toLowerCase()) != -1) {
        var current_tag_box = current_label.closest('.tag-browser-tag');
        current_tag_box.show();
      }
    }
    //console.timeEnd('filter-tag-list');
  }

  handleTagSearchInput() {
    clearTimeout(this.tag_search_timeout);
    this.tag_search_timeout = setTimeout(() => { this.applyCurrentFilters(); }, 300);
  }

  resetTagsInMenu() {
    var taglist_container = this.getTagListContainer();
    while (taglist_container.firstChild) {
      taglist_container.removeChild(taglist_container.firstChild);
    }
  }

  getSelectedTagList() {
    var currentTab = app_controller.tab_controller.getTab();
    var currentTagIdList = currentTab.getTagIdList();
    var currentTextType = currentTab.getTextType();
    
    if (currentTextType == 'tagged_verses' && currentTagIdList != null) {
      var currentTagTitleList = app_controller.tab_controller.getTab().getTagTitleList();
      if (currentTagTitleList != null) {
        var tag_list = currentTagTitleList.split(', ');
        return tag_list;
      }
    }

    return null;
  }

  selectedTagIds() {
    var checked_cbs = $('.tag-browser-tag-cb:checked');
    var tag_list = "";

    for (var i = 0; i < checked_cbs.length; i++) {
      var current_tag_id = $(checked_cbs[i]).closest('.tag-browser-tag').find('.tag-browser-tag-id').html();
      tag_list += current_tag_id;

      if (i < (checked_cbs.length - 1)) tag_list += ",";
    }

    return tag_list;
  }

  selectedTagTitles() {
    var checked_cbs = $('.tag-browser-tag-cb:checked');
    var tag_list = "";

    for (var i = 0; i < checked_cbs.length; i++) {
      var current_tag_title = $(checked_cbs[i]).closest('.tag-browser-tag').find('.tag-browser-tag-title-content').text();
      tag_list += current_tag_title;

      if (i < (checked_cbs.length - 1)) tag_list += ", ";
    }

    return tag_list;
  }

  async handleConfirmButtonClick() {
    this.hideTagMenu();
    var currentTagIdList = this.selectedTagIds();
    var currentTagTitleList = this.selectedTagTitles();
    app_controller.openTaggedVerses(currentTagIdList, currentTagTitleList);
  }

  handleTagSelection() {
    var tagCountSelectedLabel = document.getElementById('tag-count-selected-label');
    var confirmButton = document.getElementById('confirm-tag-selection-button');
    var deselectAllButton = document.getElementById('deselect-all-tags-button');
    var selectAllButton = document.getElementById('select-all-tags-button');
    var currentTagIdList = this.selectedTagIds();
    var currentTagTitleList = this.selectedTagTitles();
    var selectedTagCount = 0;
   
    if (currentTagIdList != "") { 
      selectedTagCount = currentTagIdList.split(',').length;
    }

    var taglistContainer = this.getTagListContainer();
    var tagList = taglistContainer[0].querySelectorAll('.tag-browser-tag');

    if (selectedTagCount == tagList.length) {
      selectAllButton.classList.add('ui-state-disabled');
    } else {
      selectAllButton.classList.remove('ui-state-disabled');
    }

    if (selectedTagCount > 0) {
      confirmButton.classList.remove('ui-state-disabled');
      deselectAllButton.classList.remove('ui-state-disabled');
    } else {
      confirmButton.classList.add('ui-state-disabled');
      deselectAllButton.classList.add('ui-state-disabled');
    }

    var tagCountLabelText = i18n.t('tags.tag-count-selected', { count: selectedTagCount });
    tagCountSelectedLabel.textContent = tagCountLabelText;
    tagCountSelectedLabel.setAttribute('title', currentTagTitleList);
  }

  initTagCBs() {
    var cbs = document.querySelectorAll('.tag-browser-tag-cb');
    for (var i = 0; i < cbs.length; i++) {
      cbs[i].addEventListener('click', async () => { 
        this.handleTagSelection(); 
      });

      cbs[i].removeAttribute('checked');
      cbs[i].removeAttribute('disabled');
    }
  }
  
  resetTagMenu() {
    var taglist_container = this.getTagListContainer();
    var tag_cb_list = taglist_container[0].querySelectorAll('.tag-browser-tag-cb');

    for (var i = 0; i < tag_cb_list.length; i++) {
      tag_cb_list[i].checked = false;
    }
  }

  // eslint-disable-next-line no-unused-vars
  async updateTagSelectionMenu() {
    if (!this.tag_menu_populated) {
      await this.requestTagsForMenu();
    }

    var taglist_container = this.getTagListContainer();
    this.updateCheckedTags(taglist_container);
    
    this.applyCurrentFilters();
    this.handleTagSelection();
  }

  updateVerseCountInTagMenu(tag_title, new_count) {
    var tag_list = $('.tag-browser-tag');

    for (var i = 0; i < tag_list.length; i++) {
      var current_tag = $(tag_list[i]).find('.tag-browser-tag-title-content').text();
      if (current_tag == tag_title) {
        $(tag_list[i]).find('.tag-browser-tag-assignment-count').text('(' + new_count + ')');
        break;
      }
    }
  }

  updateLastUsedTimestamp(tagId, timestamp) {
    var tagElement = $('#tag-browser-tag-' + tagId);
    tagElement.attr('last-used-timestamp', timestamp);
  }
}

module.exports = TagSelectionMenu;