Source

frontend/controllers/tags_controller.js

  1. /* This file is part of Ezra Bible App.
  2. Copyright (C) 2019 - 2023 Ezra Bible App Development Team <contact@ezrabibleapp.net>
  3. Ezra Bible App is free software: you can redistribute it and/or modify
  4. it under the terms of the GNU General Public License as published by
  5. the Free Software Foundation, either version 2 of the License, or
  6. (at your option) any later version.
  7. Ezra Bible App is distributed in the hope that it will be useful,
  8. but WITHOUT ANY WARRANTY; without even the implied warranty of
  9. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  10. GNU General Public License for more details.
  11. You should have received a copy of the GNU General Public License
  12. along with Ezra Bible App. See the file LICENSE.
  13. If not, see <http://www.gnu.org/licenses/>. */
  14. const TagStore = require('../components/tags/tag_store.js');
  15. const TagListFilter = require('../components/tags/tag_list_filter.js');
  16. const VerseBoxHelper = require('../helpers/verse_box_helper.js');
  17. const VerseBox = require('../ui_models/verse_box.js');
  18. require('../components/emoji_button_trigger.js');
  19. const { waitUntilIdle } = require('../helpers/ezra_helper.js');
  20. const eventController = require('./event_controller.js');
  21. const verseListController = require('../controllers/verse_list_controller.js');
  22. const { showErrorDialog } = require('../helpers/ezra_helper.js');
  23. /**
  24. * The TagsController handles most functionality related to tagging of verses.
  25. *
  26. * Like all other controllers it is only initialized once. It is accessible at the
  27. * global object `app_controller.tags_controller`.
  28. *
  29. * @category Controller
  30. */
  31. class TagsController {
  32. constructor() {
  33. loadScript("app/templates/tag_list.js");
  34. this.tag_store = new TagStore();
  35. this.tag_list_filter = new TagListFilter();
  36. this.verse_box_helper = new VerseBoxHelper();
  37. this.new_standard_tag_button = $('#new-standard-tag-button');
  38. this.verse_selection_blocked = false;
  39. this.verses_were_selected_before = false;
  40. this.assign_tag_label = i18n.t("tags.assign-tag");
  41. this.unassign_tag_label = i18n.t("tags.remove-tag-assignment");
  42. this.assign_tag_hint = i18n.t("tags.assign-tag-hint");
  43. this.tag_to_be_deleted = null;
  44. this.tag_to_be_deleted_title = null;
  45. this.tag_to_be_deleted_is_global = false;
  46. this.permanently_delete_tag = true;
  47. this.remove_tag_assignment_job = null;
  48. this.new_tag_created = false;
  49. this.last_created_tag = "";
  50. this.edit_tag_id = null;
  51. //this.xml_tag_statistics = null; // FIXME
  52. this.loading_indicator = "<img class=\"loading-indicator\" style=\"float: left; margin-left: 0.5em;\" " +
  53. "width=\"16\" height=\"16\" src=\"images/loading_animation.gif\" />";
  54. this.selected_verse_references = [];
  55. this.selected_verse_boxes = [];
  56. this.initialRenderingDone = false;
  57. this.newTagDialogInitDone = false;
  58. this.addTagsToGroupDialogInitDone = false;
  59. this.deleteTagConfirmationDialogInitDone = false;
  60. this.removeTagAssignmentConfirmationDialogInitDone = false;
  61. this.editTagDialogInitDone = false;
  62. this.lastContentId = null;
  63. this.currentTagGroupId = null;
  64. this.currentTagGroupTitle = null;
  65. this.subscribeEvents();
  66. }
  67. async handleTagPanelSwitched(isOpen) {
  68. if (isOpen) {
  69. await this.updateTagsView(undefined, !this.initialRenderingDone);
  70. } else if (platformHelper.isCordova() || platformHelper.isMobile()) {
  71. // Reset tag list on mobile when switching off the tag panel
  72. this.initialRenderingDone = false;
  73. document.getElementById('tags-content-global').innerHTML = "";
  74. }
  75. }
  76. subscribeEvents() {
  77. eventController.subscribe('on-tag-panel-switched', async (isOpen) => {
  78. await this.handleTagPanelSwitched(isOpen);
  79. });
  80. eventController.subscribePrioritized('on-tag-statistics-panel-switched', async (isOpen) => {
  81. await this.handleTagPanelSwitched(isOpen);
  82. });
  83. eventController.subscribe('on-tab-selected', async (tabIndex) => {
  84. const currentTab = app_controller.tab_controller.getTab(tabIndex);
  85. if (currentTab != null) {
  86. // Assume that verses were selected before, because otherwise the checkboxes may not be properly cleared
  87. this.verses_were_selected_before = true;
  88. if (this.tagPanelIsActive()) {
  89. await this.updateTagsView(tabIndex, !this.initialRenderingDone);
  90. }
  91. if (currentTab.addedInteractively) {
  92. this.resetActivePanelToTagPanel(tabIndex);
  93. }
  94. }
  95. });
  96. eventController.subscribe('on-bible-text-loaded', () => {
  97. var currentTabIndex = app_controller.tab_controller.getSelectedTabIndex();
  98. const currentTab = app_controller.tab_controller.getTab(currentTabIndex);
  99. if (currentTab != null && currentTab.addedInteractively) {
  100. this.resetActivePanelToTagPanel(currentTabIndex);
  101. }
  102. });
  103. eventController.subscribe('on-module-search-started', (tabIndex) => {
  104. const currentTab = app_controller.tab_controller.getTab(tabIndex);
  105. if (currentTab != null && currentTab.addedInteractively) {
  106. this.resetActivePanelToTagPanel(tabIndex);
  107. }
  108. });
  109. eventController.subscribe('on-locale-changed', async () => {
  110. this.updateTagsView(undefined, true);
  111. this.refreshTagDialogs();
  112. });
  113. eventController.subscribeMultiple(['on-translation-added', 'on-translation-removed'], async () => {
  114. await this.updateTagUiBasedOnTagAvailability();
  115. });
  116. eventController.subscribe('on-verses-selected', async () => {
  117. await this.updateTagsViewAfterVerseSelection(false);
  118. });
  119. eventController.subscribe('on-tag-group-list-activated', () => {
  120. this.tag_list_filter.reset();
  121. document.getElementById('tags-content-global').style.display = 'none';
  122. document.getElementById('tag-list-stats').style.visibility = 'hidden';
  123. document.getElementById('tag-panel-tag-group-list').style.removeProperty('display');
  124. document.getElementById('tag-list-filter-button').style.display = 'none';
  125. document.getElementById('tags-search-input').value = "";
  126. document.getElementById('tags-search-input').style.display = 'none';
  127. });
  128. eventController.subscribe('on-tag-group-selected', async (tagGroup) => {
  129. let tab = app_controller.tab_controller.getTab();
  130. let tagGroupId = tagGroup ? tagGroup.id : null;
  131. this.currentTagGroupId = tagGroupId;
  132. this.currentTagGroupTitle = tagGroup ? tagGroup.title : null;
  133. ipcSettings.set('lastUsedTagGroupId', tagGroupId);
  134. document.getElementById('tags-search-input').style.removeProperty('display');
  135. document.getElementById('tag-list-filter-button').style.removeProperty('display');
  136. document.getElementById('tag-panel-tag-group-list').style.display = 'none';
  137. document.getElementById('tags-content-global').innerHTML = "";
  138. document.getElementById('tags-content-global').style.display = '';
  139. document.getElementById('tag-list-stats').style.visibility = 'visible';
  140. if (this.isTagPanelActive()) {
  141. this.showTagListLoadingIndicator();
  142. await waitUntilIdle();
  143. await this.updateTagList(tab.getBook(), tagGroupId, tab.getContentId(), true);
  144. this.hideTagListLoadingIndicator();
  145. }
  146. });
  147. eventController.subscribe('on-db-refresh', async () => {
  148. const currentTabIndex = app_controller.tab_controller.getSelectedTabIndex();
  149. document.getElementById('tags-content-global').innerHTML = "";
  150. await this.updateTagsView(currentTabIndex, true);
  151. });
  152. eventController.subscribe('on-body-clicked', () => {
  153. const tagsSearchInput = document.getElementById('tags-search-input');
  154. tagsSearchInput.blur();
  155. });
  156. }
  157. tagPanelIsActive() {
  158. let tagPanelButton = document.getElementById('tag-panel-button');
  159. let isActive = tagPanelButton.classList.contains('active');
  160. return isActive;
  161. }
  162. getCurrentTagGroup() {
  163. let currentTagGroup = null;
  164. if (tags_controller.currentTagGroupId != null) {
  165. currentTagGroup = {
  166. title: tags_controller.currentTagGroupTitle,
  167. id: tags_controller.currentTagGroupId
  168. };
  169. }
  170. return currentTagGroup;
  171. }
  172. isTagPanelActive() {
  173. const panelButtons = document.getElementById('panel-buttons');
  174. let activePanel = panelButtons.activePanel;
  175. return activePanel != '' && (activePanel == 'tag-panel' || activePanel == 'tag-statistics-panel');
  176. }
  177. resetActivePanelToTagPanel(tabIndex) {
  178. var panelButtons = document.getElementById('panel-buttons');
  179. var tab = app_controller.tab_controller.getTab(tabIndex);
  180. if (panelButtons.activePanel != "" && panelButtons.activePanel != 'tag-panel') {
  181. if (tab.isNew() || tab.isVerseList()) {
  182. panelButtons.activePanel = 'tag-panel';
  183. }
  184. }
  185. }
  186. /**
  187. * This is used to refresh the dialogs after the locale changed
  188. */
  189. refreshTagDialogs() {
  190. this.initNewTagDialog(true);
  191. this.initAddTagsToGroupDialog(true);
  192. this.initEditTagDialog(true);
  193. this.initRemoveTagAssignmentConfirmationDialog(true);
  194. this.initDeleteTagConfirmationDialog(true);
  195. }
  196. initNewTagDialog(force=false) {
  197. if (!force && this.newTagDialogInitDone) {
  198. return;
  199. }
  200. this.newTagDialogInitDone = true;
  201. var dialogWidth = 450;
  202. var dialogHeight = 420;
  203. var draggable = true;
  204. var position = [55, 120];
  205. let new_standard_tag_dlg_options = uiHelper.getDialogOptions(dialogWidth, dialogHeight, draggable, position);
  206. new_standard_tag_dlg_options.dialogClass = 'ezra-dialog new-tag-dialog';
  207. new_standard_tag_dlg_options.title = i18n.t("tags.new-tag");
  208. new_standard_tag_dlg_options.autoOpen = false;
  209. new_standard_tag_dlg_options.buttons = {};
  210. new_standard_tag_dlg_options.buttons[i18n.t("general.cancel")] = function() {
  211. setTimeout(() => { $(this).dialog("close"); }, 100);
  212. };
  213. new_standard_tag_dlg_options.buttons[i18n.t("tags.create-tag")] = {
  214. id: 'create-tag-button',
  215. text: i18n.t("tags.create-tag"),
  216. click: function() {
  217. tags_controller.saveNewTag(this);
  218. }
  219. };
  220. document.getElementById('add-existing-tags-to-tag-group-link').addEventListener('click', async (event) => {
  221. event.preventDefault();
  222. tags_controller.initAddTagsToGroupDialog();
  223. const addTagsToGroupFilterInput = document.getElementById('add-tags-to-group-filter-input');
  224. addTagsToGroupFilterInput.value = '';
  225. const addTagsToGroupTagList = document.getElementById('add-tags-to-group-tag-list');
  226. addTagsToGroupTagList.style.removeProperty('display');
  227. if (platformHelper.isCordova()) {
  228. // eslint-disable-next-line no-undef
  229. if (Keyboard.isVisible) {
  230. // We need to remember the current window height as the keyboard is shown
  231. // When closing the current dialog the keyboard will go away and in that moment of "flickering"
  232. // it is hard to determine the window height right at the time when the new dialog is opened.
  233. let currentWindowHeight = $(window).height();
  234. // We slightly reduce the height of the add-tags-to-group-dialog - this is based on testing/experience.
  235. $('#add-tags-to-group-dialog').dialog("option", "height", currentWindowHeight - 18);
  236. }
  237. }
  238. $('#new-standard-tag-dialog').dialog("close");
  239. $('#add-tags-to-group-dialog').dialog("open");
  240. await waitUntilIdle();
  241. });
  242. $('#new-standard-tag-dialog').dialog(new_standard_tag_dlg_options);
  243. uiHelper.fixDialogCloseIconOnAndroid('new-tag-dialog');
  244. // Handle the enter key in the tag title field and create the tag when it is pressed
  245. $('#new-standard-tag-title-input:not(.bound)').addClass('bound').on("keypress", async (event) => {
  246. if (event.which == 13) {
  247. var tag_title = $('#new-standard-tag-title-input').val();
  248. var tagExisting = await this.updateButtonStateBasedOnTagTitleValidation(tag_title, 'create-tag-button');
  249. if (tagExisting) {
  250. return;
  251. }
  252. $('#new-standard-tag-dialog').dialog("close");
  253. tags_controller.saveNewTag(event);
  254. }
  255. // eslint-disable-next-line no-unused-vars
  256. }).on("keyup", async (event) => {
  257. var tag_title = $('#new-standard-tag-title-input').val();
  258. await this.updateButtonStateBasedOnTagTitleValidation(tag_title, 'create-tag-button');
  259. });
  260. }
  261. async updateAddTagToGroupTagList() {
  262. const addTagsToGroupTagList = document.getElementById('add-tags-to-group-tag-list');
  263. addTagsToGroupTagList.tagManager.reset();
  264. addTagsToGroupTagList.tagManager.setFilter('');
  265. await addTagsToGroupTagList.tagManager.refreshItemList();
  266. let tagList = await this.tag_store.getTagList();
  267. let tagIdList = await this.tag_store.getTagGroupMemberIds(this.currentTagGroupId, tagList);
  268. addTagsToGroupTagList.tagManager.setExcludeItems(tagIdList);
  269. addTagsToGroupTagList.tagManager.excludeItems();
  270. let tagCount = addTagsToGroupTagList.tagManager.getAllItemElements().length;
  271. return tagCount;
  272. }
  273. initAddTagsToGroupDialog(force=false) {
  274. if (!force && this.addTagsToGroupDialogInitDone) {
  275. return;
  276. }
  277. this.addTagsToGroupDialogInitDone = true;
  278. var dialogWidth = 450;
  279. var dialogHeight = 480;
  280. var draggable = true;
  281. var position = [55, 120];
  282. let addTagsToGroupDialogOptions = uiHelper.getDialogOptions(dialogWidth, dialogHeight, draggable, position);
  283. addTagsToGroupDialogOptions.dialogClass = 'ezra-dialog add-tags-to-group-dialog';
  284. addTagsToGroupDialogOptions.title = i18n.t("tags.add-tags-to-group");
  285. addTagsToGroupDialogOptions.autoOpen = false;
  286. addTagsToGroupDialogOptions.buttons = {};
  287. addTagsToGroupDialogOptions.buttons[i18n.t("general.cancel")] = function() {
  288. $(this).dialog("close");
  289. };
  290. addTagsToGroupDialogOptions.buttons[i18n.t("tags.add-tags-to-group")] = {
  291. id: 'add-tags-to-group-button',
  292. text: i18n.t("tags.add-tags-to-group"),
  293. click: function() {
  294. $(this).dialog("close");
  295. const addTagsToGroupTagList = document.getElementById('add-tags-to-group-tag-list');
  296. tags_controller.addTagsToGroup(tags_controller.currentTagGroupId, addTagsToGroupTagList.addList);
  297. }
  298. };
  299. document.getElementById('add-tags-to-group-filter-input').addEventListener('keyup', () => {
  300. let currentFilterString = document.getElementById('add-tags-to-group-filter-input').value;
  301. const addTagsToGroupTagList = document.getElementById('add-tags-to-group-tag-list');
  302. addTagsToGroupTagList.filter = currentFilterString;
  303. });
  304. $('#add-tags-to-group-dialog').dialog(addTagsToGroupDialogOptions);
  305. uiHelper.fixDialogCloseIconOnAndroid('add-tags-to-group-dialog');
  306. }
  307. async addTagsToGroup(tagGroupId, tagList) {
  308. for (let i = 0; i < tagList.length; i++) {
  309. let tagId = tagList[i];
  310. let tag = await this.tag_store.getTag(tagId);
  311. let result = await ipcDb.updateTag(tagId, tag.title, [ tagGroupId ], []);
  312. if (result.success == false) {
  313. var message = `The tag <i>${tag.title}</i> could not be updated.<br>
  314. An unexpected database error occurred:<br><br>
  315. ${result.exception}<br><br>
  316. Please restart the app.`;
  317. await showErrorDialog('Database Error', message);
  318. uiHelper.hideTextLoadingIndicator();
  319. return;
  320. } else {
  321. await eventController.publishAsync('on-tag-group-members-changed', {
  322. tagId: tagId,
  323. addTagGroups: [ tagGroupId ],
  324. removeTagGroups: []
  325. });
  326. }
  327. }
  328. if (this.tagGroupUsed()) {
  329. const currentTabIndex = app_controller.tab_controller.getSelectedTabIndex();
  330. await this.updateTagsView(currentTabIndex, true);
  331. }
  332. }
  333. tagGroupUsed() {
  334. return this.currentTagGroupId != null && this.currentTagGroupId > 0;
  335. }
  336. async updateButtonStateBasedOnTagTitleValidation(tagTitle, buttonId) {
  337. tagTitle = tagTitle.trim();
  338. const tagExisting = await this.tag_store.tagExists(tagTitle);
  339. let tagButton = document.getElementById(buttonId);
  340. if (tagExisting || tagTitle == "") {
  341. uiHelper.disableButton(tagButton);
  342. } else {
  343. uiHelper.enableButton(tagButton);
  344. }
  345. return tagExisting;
  346. }
  347. initDeleteTagConfirmationDialog(force=false) {
  348. if (!force && this.deleteTagConfirmationDialogInitDone) {
  349. return;
  350. }
  351. this.deleteTagConfirmationDialogInitDone = true;
  352. var dialogWidth = 400;
  353. var dialogHeight = null;
  354. var draggable = true;
  355. var position = [55, 120];
  356. let delete_tag_confirmation_dlg_options = uiHelper.getDialogOptions(dialogWidth, dialogHeight, draggable, position);
  357. delete_tag_confirmation_dlg_options.dialogClass = 'ezra-dialog delete-tag-confirmation-dialog';
  358. delete_tag_confirmation_dlg_options.title = i18n.t("tags.delete-tag");
  359. delete_tag_confirmation_dlg_options.autoOpen = false;
  360. delete_tag_confirmation_dlg_options.buttons = {};
  361. delete_tag_confirmation_dlg_options.buttons[i18n.t("general.cancel")] = function() {
  362. setTimeout(() => { $(this).dialog("close"); }, 100);
  363. };
  364. delete_tag_confirmation_dlg_options.buttons[i18n.t("tags.delete-tag")] = function() {
  365. tags_controller.deleteTagAfterConfirmation();
  366. };
  367. document.getElementById('permanently-delete-tag').addEventListener('change', function() {
  368. let permanentlyDeleteTagWarning = document.getElementById('permanently-delete-tag-warning');
  369. if (this.checked) {
  370. permanentlyDeleteTagWarning.style.visibility = 'visible';
  371. } else {
  372. permanentlyDeleteTagWarning.style.visibility = 'hidden';
  373. }
  374. });
  375. $('#delete-tag-confirmation-dialog').dialog(delete_tag_confirmation_dlg_options);
  376. uiHelper.fixDialogCloseIconOnAndroid('delete-tag-confirmation-dialog');
  377. }
  378. initRemoveTagAssignmentConfirmationDialog(force=false) {
  379. if (!force && this.removeTagAssignmentConfirmationDialogInitDone) {
  380. return;
  381. }
  382. this.removeTagAssignmentConfirmationDialogInitDone = true;
  383. let remove_tag_assignment_confirmation_dlg_options = uiHelper.getDialogOptions(360, null, true, [55, 120]);
  384. remove_tag_assignment_confirmation_dlg_options.dialogClass = 'ezra-dialog remove-tag-assignment-confirmation-dialog';
  385. remove_tag_assignment_confirmation_dlg_options.autoOpen = false;
  386. remove_tag_assignment_confirmation_dlg_options.title = i18n.t("tags.remove-tag-assignment");
  387. remove_tag_assignment_confirmation_dlg_options.buttons = {};
  388. remove_tag_assignment_confirmation_dlg_options.buttons[i18n.t("general.cancel")] = function() {
  389. tags_controller.remove_tag_assignment_job.tag_button.addClass('active');
  390. tags_controller.remove_tag_assignment_job = null;
  391. $(this).dialog("close");
  392. };
  393. remove_tag_assignment_confirmation_dlg_options.buttons[i18n.t("tags.remove-tag-assignment")] = function() {
  394. tags_controller.removeTagAssignmentAfterConfirmation();
  395. };
  396. $('#remove-tag-assignment-confirmation-dialog').dialog(remove_tag_assignment_confirmation_dlg_options);
  397. uiHelper.fixDialogCloseIconOnAndroid('remove-tag-assignment-confirmation-dialog');
  398. // eslint-disable-next-line no-unused-vars
  399. $('#remove-tag-assignment-confirmation-dialog').bind('dialogbeforeclose', function(event) {
  400. if (!tags_controller.persistence_ongoing && tags_controller.remove_tag_assignment_job != null) {
  401. tags_controller.remove_tag_assignment_job.tag_button.addClass('active');
  402. tags_controller.remove_tag_assignment_job = null;
  403. }
  404. });
  405. }
  406. initEditTagDialog(force=false) {
  407. if (!force && this.editTagDialogInitDone) {
  408. return;
  409. }
  410. this.editTagDialogInitDone = true;
  411. var dialogWidth = 450;
  412. var dialogHeight = 400;
  413. var draggable = true;
  414. var position = [55, 120];
  415. let edit_tag_dlg_options = uiHelper.getDialogOptions(dialogWidth, dialogHeight, draggable, position);
  416. edit_tag_dlg_options.dialogClass = 'ezra-dialog edit-tag-dialog';
  417. edit_tag_dlg_options.title = i18n.t("tags.edit-tag");
  418. edit_tag_dlg_options.autoOpen = false;
  419. edit_tag_dlg_options.buttons = {};
  420. edit_tag_dlg_options.buttons[i18n.t("general.cancel")] = function() {
  421. setTimeout(() => { $(this).dialog("close"); }, 100);
  422. };
  423. edit_tag_dlg_options.buttons[i18n.t("general.save")] = {
  424. id: 'edit-tag-button',
  425. text: i18n.t("general.save"),
  426. click: function() {
  427. tags_controller.closeDialogAndUpdateTag();
  428. }
  429. };
  430. $('#edit-tag-dialog').dialog(edit_tag_dlg_options);
  431. uiHelper.fixDialogCloseIconOnAndroid('edit-tag-dialog');
  432. // Handle the enter key in the tag title field and rename the tag when it is pressed
  433. $('#rename-tag-title-input:not(.bound)').addClass('bound').on("keypress", (event) => {
  434. if (event.which == 13) {
  435. tags_controller.closeDialogAndUpdateTag();
  436. }
  437. // eslint-disable-next-line no-unused-vars
  438. }).on("keyup", (event) => {
  439. this.handleEditTagChange();
  440. });
  441. }
  442. async closeDialogAndUpdateTag() {
  443. var oldTitle = tags_controller.edit_tag_title;
  444. var newTitle = $('#rename-tag-title-input').val();
  445. newTitle = newTitle.trim();
  446. if (newTitle != oldTitle) {
  447. let tagExisting = await this.updateButtonStateBasedOnTagTitleValidation(newTitle, 'edit-tag-button');
  448. if (tagExisting) {
  449. return;
  450. }
  451. }
  452. var tagGroupAssignment = document.getElementById('tag-group-assignment');
  453. var addTagGroups = tagGroupAssignment.addList;
  454. var removeTagGroups = tagGroupAssignment.removeList;
  455. $('#edit-tag-dialog').dialog('close');
  456. var checkboxTag = this.getCheckboxTag(tags_controller.edit_tag_id);
  457. var isGlobal = (checkboxTag.parent().attr('id') == 'tags-content-global');
  458. var result = await ipcDb.updateTag(tags_controller.edit_tag_id, newTitle, addTagGroups, removeTagGroups);
  459. if (result.success == false) {
  460. var message = `The tag <i>${tags_controller.edit_tag_title}</i> could not be updated.<br>
  461. An unexpected database error occurred:<br><br>
  462. ${result.exception}<br><br>
  463. Please restart the app.`;
  464. await showErrorDialog('Database Error', message);
  465. uiHelper.hideTextLoadingIndicator();
  466. return;
  467. }
  468. if (newTitle != oldTitle) {
  469. await eventController.publishAsync(
  470. 'on-tag-renamed',
  471. {
  472. tagId: tags_controller.edit_tag_id,
  473. oldTitle: tags_controller.edit_tag_title,
  474. newTitle: newTitle
  475. }
  476. );
  477. tags_controller.updateTagInView(tags_controller.edit_tag_id, newTitle);
  478. tags_controller.updateTagTitlesInVerseList(tags_controller.edit_tag_id, isGlobal, newTitle);
  479. tags_controller.sortTagLists();
  480. await tags_controller.updateTagsViewAfterVerseSelection(true);
  481. }
  482. if (addTagGroups.length > 0 || removeTagGroups.length > 0) {
  483. await eventController.publishAsync('on-tag-group-members-changed', {
  484. tagId: tags_controller.edit_tag_id,
  485. addTagGroups,
  486. removeTagGroups
  487. });
  488. if (this.tagGroupUsed()) {
  489. const currentTabIndex = app_controller.tab_controller.getSelectedTabIndex();
  490. await this.updateTagsView(currentTabIndex, true);
  491. }
  492. }
  493. await eventController.publishAsync('on-latest-tag-changed', {
  494. 'tagId': tags_controller.edit_tag_id,
  495. 'added': false
  496. });
  497. await waitUntilIdle();
  498. checkboxTag = this.getCheckboxTag(tags_controller.edit_tag_id);
  499. checkboxTag.effect('bounce', 'fast');
  500. }
  501. updateTagInView(id, title) {
  502. // Rename tag in tag list on the left side
  503. var checkboxTag = tags_controller.getCheckboxTag(id);
  504. var label = checkboxTag.find('.cb-label');
  505. label.text(title);
  506. // Rename tag in tag selection menu above bible browser
  507. var tag_selection_entry = $('#tag-browser-tag-' + id).find('.tag-browser-tag-title').find('.tag-browser-tag-title-content');
  508. tag_selection_entry.text(title);
  509. }
  510. async saveNewTag(e) {
  511. uiHelper.showTextLoadingIndicator();
  512. $(e).dialog("close");
  513. await waitUntilIdle(); // Give the dialog some time to close
  514. var new_tag_title = $('#new-standard-tag-title-input').val();
  515. tags_controller.new_tag_created = true;
  516. this.last_created_tag = new_tag_title;
  517. new_tag_title = new_tag_title.trim();
  518. let tagGroupAssignment = document.getElementById('new-tag-dialog-tag-group-assignment');
  519. let tagGroups = tagGroupAssignment.addList;
  520. var result = await ipcDb.createNewTag(new_tag_title, tagGroups);
  521. if (result.success == false) {
  522. var message = `The new tag <i>${new_tag_title}</i> could not be saved.<br>
  523. An unexpected database error occurred:<br><br>
  524. ${result.exception}<br><br>
  525. Please restart the app.`;
  526. await showErrorDialog('Database Error', message);
  527. uiHelper.hideTextLoadingIndicator();
  528. return;
  529. }
  530. await eventController.publishAsync('on-tag-created', result.dbObject.id);
  531. if (this.tagGroupUsed()) {
  532. await eventController.publishAsync('on-tag-group-members-changed', {
  533. tagId: result.dbObject.id,
  534. addTagGroups: [ this.currentTagGroupId ],
  535. removeTagGroups: []
  536. });
  537. }
  538. await eventController.publishAsync('on-latest-tag-changed', {
  539. 'tagId': result.dbObject.id,
  540. 'added': true
  541. });
  542. var tab = app_controller.tab_controller.getTab();
  543. await tags_controller.updateTagList(tab.getBook(), this.currentTagGroupId, tab.getContentId(), true);
  544. await tags_controller.updateTagsViewAfterVerseSelection(true);
  545. uiHelper.hideTextLoadingIndicator();
  546. }
  547. async handleNewTagButtonClick(event) {
  548. if (event.target.classList.contains('ui-state-disabled')) {
  549. return;
  550. }
  551. eventController.publish('on-button-clicked');
  552. await waitUntilIdle();
  553. tags_controller.initNewTagDialog();
  554. const tagInput = document.getElementById('new-standard-tag-title-input');
  555. tagInput.value = '';
  556. var $dialogContainer = $('#new-standard-tag-dialog');
  557. $dialogContainer.dialog('open');
  558. await waitUntilIdle();
  559. let allTagGroups = await ipcDb.getAllTagGroups();
  560. let tagGroupAssignmentSection = document.getElementById('tag-group-assignment-section');
  561. let tagGroupAssignment = document.getElementById('new-tag-dialog-tag-group-assignment');
  562. tagGroupAssignment.tagGroupManager._addList = [];
  563. if (allTagGroups.length == 0) {
  564. tagGroupAssignmentSection.style.display = 'none';
  565. } else {
  566. tagGroupAssignmentSection.style.removeProperty('display');
  567. await tagGroupAssignment.tagGroupManager.refreshItemList();
  568. if (this.tagGroupUsed()) {
  569. tagGroupAssignment.tagGroupManager.enableElementById(this.currentTagGroupId);
  570. tagGroupAssignment.tagGroupManager._addList = [ this.currentTagGroupId ];
  571. }
  572. }
  573. this.updateButtonStateBasedOnTagTitleValidation('', 'create-tag-button');
  574. let addExistingTagsLink = document.getElementById('add-existing-tags-to-tag-group-link').parentNode;
  575. if (this.tagGroupUsed()) {
  576. let remainingTagCount = await this.updateAddTagToGroupTagList();
  577. if (remainingTagCount > 0) {
  578. addExistingTagsLink.style.removeProperty('display');
  579. } else {
  580. addExistingTagsLink.style.display = 'none';
  581. }
  582. } else {
  583. addExistingTagsLink.style.display = 'none';
  584. }
  585. if (platformHelper.isCordova()) {
  586. // Focus the input field (and show the screen keyboard) a little bit delayed
  587. // to give the layout engine some time to render the input field.
  588. setTimeout(async () => {
  589. await waitUntilIdle();
  590. tagInput.focus();
  591. }, 1000);
  592. } else {
  593. tagInput.focus();
  594. }
  595. }
  596. handleDeleteTagButtonClick(event) {
  597. eventController.publish('on-button-clicked');
  598. tags_controller.initDeleteTagConfirmationDialog();
  599. var checkboxTag = $(event.target).closest('.checkbox-tag');
  600. var tag_id = checkboxTag.attr('tag-id');
  601. var parent_id = checkboxTag.parent().attr('id');
  602. var label = checkboxTag.find('.cb-label').html();
  603. tags_controller.tag_to_be_deleted_is_global = (parent_id == 'tags-content-global');
  604. tags_controller.tag_to_be_deleted_title = label;
  605. tags_controller.tag_to_be_deleted = tag_id;
  606. tags_controller.permanently_delete_tag = tags_controller.tagGroupUsed() ? false : true;
  607. var number_of_tagged_verses = checkboxTag.attr('global-assignment-count');
  608. let deleteTagFromGroupExplanation = document.getElementById('delete-tag-from-group-explanation');
  609. let reallyDeleteTagExplanation = document.getElementById('really-delete-tag-explanation');
  610. let permanentlyDeleteTagBox = document.getElementById('permanently-delete-tag-box');
  611. let permanentlyDeleteTagWarning = document.getElementById('permanently-delete-tag-warning');
  612. let tagGroup = this.currentTagGroupTitle;
  613. let permanentlyDeleteCheckbox = document.getElementById('permanently-delete-tag');
  614. permanentlyDeleteCheckbox.checked = false;
  615. if (this.tagGroupUsed()) {
  616. // Tag group used
  617. reallyDeleteTagExplanation.style.display = 'none';
  618. permanentlyDeleteTagWarning.style.visibility = 'hidden';
  619. permanentlyDeleteTagBox.style.removeProperty('display');
  620. deleteTagFromGroupExplanation.innerHTML = i18n.t('tags.delete-tag-from-group-explanation', { tag: label, group: tagGroup, interpolation: {escapeValue: false}});
  621. deleteTagFromGroupExplanation.style.display = 'block';
  622. } else {
  623. // All tags - no tag group
  624. deleteTagFromGroupExplanation.style.display = 'none';
  625. permanentlyDeleteTagWarning.style.visibility = 'visible';
  626. permanentlyDeleteTagBox.style.display = 'none';
  627. reallyDeleteTagExplanation.style.display = 'block';
  628. }
  629. $('#delete-tag-name').html(label);
  630. $('#delete-tag-number-of-verses').html(number_of_tagged_verses); // FIXME
  631. $('#delete-tag-confirmation-dialog').dialog('open');
  632. }
  633. deleteTagAfterConfirmation() {
  634. $('#delete-tag-confirmation-dialog').dialog('close');
  635. tags_controller.permanently_delete_tag = document.getElementById('permanently-delete-tag').checked;
  636. setTimeout(async () => {
  637. let result = null;
  638. if (!tags_controller.tagGroupUsed() || tags_controller.permanently_delete_tag) {
  639. // Permanently delete tag
  640. result = await ipcDb.removeTag(tags_controller.tag_to_be_deleted);
  641. } else {
  642. // Remove tag from current tag group
  643. result = await ipcDb.updateTag(tags_controller.tag_to_be_deleted,
  644. tags_controller.tag_to_be_deleted_title,
  645. [],
  646. [ tags_controller.currentTagGroupId ]);
  647. }
  648. if (result.success == false) {
  649. var message = `The tag <i>${tags_controller.tag_to_be_deleted_title}</i> could not be deleted.<br>
  650. An unexpected database error occurred:<br><br>
  651. ${result.exception}<br><br>
  652. Please restart the app.`;
  653. await showErrorDialog('Database Error', message);
  654. uiHelper.hideTextLoadingIndicator();
  655. return;
  656. }
  657. await tags_controller.removeTagById(tags_controller.tag_to_be_deleted, tags_controller.tag_to_be_deleted_title);
  658. if (tags_controller.tagGroupUsed()) {
  659. await eventController.publishAsync('on-tag-group-members-changed', {
  660. tagId: tags_controller.tag_to_be_deleted,
  661. addTagGroups: [],
  662. removeTagGroups: [ this.currentTagGroupId ]
  663. });
  664. } else {
  665. await eventController.publishAsync('on-tag-deleted', tags_controller.tag_to_be_deleted);
  666. }
  667. await tags_controller.updateTagsViewAfterVerseSelection(true);
  668. await tags_controller.updateTagUiBasedOnTagAvailability();
  669. }, 50);
  670. }
  671. async removeTagById(tag_id, tag_title) {
  672. var checkboxTag = tags_controller.getCheckboxTag(tag_id);
  673. checkboxTag.detach();
  674. if (!tags_controller.tagGroupUsed()) {
  675. if (this.tag_store.latest_tag_id != null && this.tag_store.latest_tag_id == tag_id) {
  676. this.tag_store.latest_tag_id = null;
  677. await this.tag_store.refreshTagList();
  678. }
  679. }
  680. tags_controller.updateTagCountAfterRendering();
  681. // eslint-disable-next-line no-unused-vars
  682. var tag_data_elements = $('.tag-id').filter(function(index){
  683. return ($(this).html() == tag_id);
  684. });
  685. if (!tags_controller.tagGroupUsed()) {
  686. var verse_list = $.create_xml_doc(
  687. app_controller.verse_selection.element_list_to_xml_verse_list(tag_data_elements)
  688. );
  689. tags_controller.changeVerseListTagInfo(tag_id, tag_title, verse_list, "remove");
  690. }
  691. }
  692. async assignLastTag() {
  693. app_controller.hideAllMenus();
  694. uiHelper.showTextLoadingIndicator();
  695. await waitUntilIdle();
  696. if (this.tag_store.latest_tag_id != null) {
  697. var checkboxTag = this.getCheckboxTag(this.tag_store.latest_tag_id);
  698. await this.clickCheckBoxTag(checkboxTag);
  699. }
  700. uiHelper.hideTextLoadingIndicator();
  701. }
  702. async handleTagLabelClick(event) {
  703. var checkboxTag = $(event.target).closest('.checkbox-tag');
  704. await this.clickCheckBoxTag(checkboxTag);
  705. }
  706. async clickCheckBoxTag(checkboxTag) {
  707. var current_verse_list = app_controller.verse_selection.selectedVerseReferences;
  708. if (!tags_controller.is_blocked && current_verse_list.length > 0) {
  709. this.toggleTagButton(checkboxTag);
  710. await tags_controller.handleCheckboxTagStateChange(checkboxTag);
  711. }
  712. }
  713. toggleTagButton(checkboxTag) {
  714. var tag_button = checkboxTag[0].querySelector('.tag-button');
  715. var isActive = tag_button.classList.contains('active');
  716. if (isActive) {
  717. tag_button.classList.remove('active');
  718. tag_button.classList.add('no-hl');
  719. if (platformHelper.isElectron()) {
  720. tag_button.addEventListener('mouseleave', tags_controller.removeTagButtonNoHl);
  721. }
  722. } else {
  723. tag_button.classList.add('active');
  724. }
  725. }
  726. removeTagButtonNoHl(event) {
  727. event.target.classList.remove('no-hl');
  728. event.target.removeEventListener('mouseleave', tags_controller.removeTagButtonNoHl);
  729. }
  730. async handleTagCbClick(event) {
  731. await waitUntilIdle();
  732. var checkboxTag = $(event.target).closest('.checkbox-tag');
  733. this.toggleTagButton(checkboxTag);
  734. await tags_controller.handleCheckboxTagStateChange(checkboxTag);
  735. }
  736. async handleCheckboxTagStateChange(checkboxTag) {
  737. var current_verse_list = app_controller.verse_selection.selectedVerseReferences;
  738. if (tags_controller.is_blocked || current_verse_list.length == 0) {
  739. return;
  740. }
  741. tags_controller.is_blocked = true;
  742. setTimeout(function() {
  743. tags_controller.is_blocked = false;
  744. }, 300);
  745. var id = parseInt(checkboxTag.attr('tag-id'));
  746. var tag_button = checkboxTag[0].querySelector('.tag-button');
  747. var cb_label = checkboxTag.find('.cb-label').html();
  748. var tag_button_is_active = tag_button.classList.contains('active');
  749. var current_verse_selection = app_controller.verse_selection.current_verse_selection_as_xml();
  750. var current_verse_reference_ids = app_controller.verse_selection.current_verse_selection_as_verse_reference_ids();
  751. checkboxTag.find('.cb-label').removeClass('underline');
  752. checkboxTag.find('.cb-label-postfix').html('');
  753. var is_global = false;
  754. if (checkboxTag.find('.is-global').html() == 'true') {
  755. is_global = true;
  756. }
  757. if (tag_button_is_active) {
  758. // Update last used timestamp
  759. var current_timestamp = new Date(Date.now()).getTime();
  760. checkboxTag.attr('last-used-timestamp', current_timestamp);
  761. this.tag_store.updateTagTimestamp(id, current_timestamp);
  762. await this.tag_store.updateLatestAndOldestTagData();
  763. app_controller.tag_selection_menu.updateLastUsedTimestamp(id, current_timestamp);
  764. app_controller.tag_selection_menu.applyCurrentFilters();
  765. $(tag_button).attr('title', i18n.t("tags.remove-tag-assignment"));
  766. var filteredVerseBoxes = [];
  767. var currentVerseList = verseListController.getCurrentVerseList();
  768. // Create a list of filtered ids, that only contains the verses that do not have the selected tag yet
  769. for (let i = 0; i < current_verse_reference_ids.length; i++) {
  770. var currentVerseReferenceId = current_verse_reference_ids[i];
  771. var currentVerseBox = currentVerseList[0].querySelector('.verse-reference-id-' + currentVerseReferenceId);
  772. if (currentVerseBox != null) {
  773. var existingTagIdElements = currentVerseBox.querySelectorAll('.tag-id');
  774. var existingTagIds = [];
  775. for (let j = 0; j < existingTagIdElements.length; j++) {
  776. var currentTagId = parseInt(existingTagIdElements[j].innerText);
  777. existingTagIds.push(currentTagId);
  778. }
  779. if (!existingTagIds.includes(id)) {
  780. filteredVerseBoxes.push(currentVerseBox);
  781. }
  782. }
  783. }
  784. var result = await ipcDb.assignTagToVerses(id, filteredVerseBoxes);
  785. if (result.success == false) {
  786. var message = `The tag <i>${cb_label}</i> could not be assigned to the selected verses.<br>
  787. An unexpected database error occurred:<br><br>
  788. ${result.exception}<br><br>
  789. Please restart the app.`;
  790. await showErrorDialog('Database Error', message);
  791. uiHelper.hideTextLoadingIndicator();
  792. return;
  793. }
  794. tags_controller.changeVerseListTagInfo(id,
  795. cb_label,
  796. $.create_xml_doc(current_verse_selection),
  797. "assign");
  798. await eventController.publishAsync('on-latest-tag-changed', {
  799. 'tagId': id,
  800. 'added': true
  801. });
  802. var currentBook = app_controller.tab_controller.getTab().getBook();
  803. tags_controller.updateTagCountAfterRendering(currentBook != null);
  804. await tags_controller.updateTagsViewAfterVerseSelection(true);
  805. await tags_controller.updateTagUiBasedOnTagAvailability();
  806. } else {
  807. tags_controller.remove_tag_assignment_job = {
  808. 'id': id,
  809. 'is_global': is_global,
  810. 'cb_label': cb_label,
  811. 'checkboxTag': checkboxTag,
  812. 'verse_list': current_verse_list,
  813. 'verse_ids': current_verse_reference_ids,
  814. 'xml_verse_selection': $.create_xml_doc(current_verse_selection),
  815. 'tag_button': $(tag_button)
  816. };
  817. if (current_verse_list.length > 1) {
  818. tags_controller.initRemoveTagAssignmentConfirmationDialog();
  819. $('#remove-tag-assignment-name').html(cb_label);
  820. $('#remove-tag-assignment-confirmation-dialog').dialog('open');
  821. } else {
  822. await tags_controller.removeTagAssignmentAfterConfirmation();
  823. await tags_controller.updateTagsViewAfterVerseSelection(true);
  824. }
  825. }
  826. }
  827. getCheckboxTag(id) {
  828. var checkboxTag = $('#tags-content-global').find('.checkbox-tag[tag-id="' + id + '"]');
  829. return checkboxTag;
  830. }
  831. updateTagVerseCount(id, verseBoxes, to_increment) {
  832. var count = verseBoxes.length;
  833. var checkboxTag = tags_controller.getCheckboxTag(id);
  834. var cb_label_element = checkboxTag.find('.cb-label');
  835. var tag_title = cb_label_element.text();
  836. var tag_assignment_count_element = checkboxTag.find('.cb-label-tag-assignment-count');
  837. var tag_assignment_count_values = tag_assignment_count_element.text().substring(
  838. 1, tag_assignment_count_element.text().length - 1
  839. );
  840. var current_book_count = 0;
  841. var current_global_count = 0;
  842. var new_book_count = 0;
  843. var new_global_count = 0;
  844. var currentBook = app_controller.tab_controller.getTab().getBook();
  845. if (currentBook == null) {
  846. current_global_count = parseInt(tag_assignment_count_values);
  847. } else {
  848. current_book_count = parseInt(tag_assignment_count_values.split('|')[0]);
  849. current_global_count = parseInt(tag_assignment_count_values.split('|')[1]);
  850. }
  851. if (to_increment) {
  852. new_book_count = current_book_count + count;
  853. new_global_count = current_global_count + count;
  854. } else {
  855. new_book_count = current_book_count - count;
  856. new_global_count = current_global_count - count;
  857. }
  858. if (new_book_count > 0) {
  859. cb_label_element.addClass('cb-label-assigned');
  860. } else {
  861. cb_label_element.removeClass('cb-label-assigned');
  862. }
  863. checkboxTag.attr('book-assignment-count', new_book_count);
  864. checkboxTag.attr('global-assignment-count', new_global_count);
  865. var new_label = "";
  866. if (currentBook == null) {
  867. new_label = "(" + new_global_count + ")";
  868. } else {
  869. new_label = "(" + new_book_count + " | " + new_global_count + ")";
  870. }
  871. tag_assignment_count_element.text(new_label);
  872. // Update tag count in tag store statistics
  873. var bookList = this.verse_box_helper.getBookListFromVerseBoxes(verseBoxes);
  874. tags_controller.tag_store.updateTagCount(id, bookList, count, to_increment);
  875. // Update tag count in tag selection menu as well
  876. app_controller.tag_selection_menu.updateVerseCountInTagMenu(tag_title, new_global_count);
  877. }
  878. async removeTagAssignmentAfterConfirmation() {
  879. tags_controller.persistence_ongoing = true;
  880. $('#remove-tag-assignment-confirmation-dialog').dialog('close');
  881. var job = tags_controller.remove_tag_assignment_job;
  882. tags_controller.changeVerseListTagInfo(job.id,
  883. job.cb_label,
  884. job.xml_verse_selection,
  885. "remove");
  886. job.tag_button.attr('title', i18n.t("tags.assign-tag"));
  887. job.checkboxTag.append(tags_controller.loading_indicator);
  888. var verse_boxes = [];
  889. var currentVerseList = verseListController.getCurrentVerseList();
  890. for (let i = 0; i < job.verse_ids.length; i++) {
  891. var currentVerseReferenceId = job.verse_ids[i];
  892. var currentVerseBox = currentVerseList[0].querySelector('.verse-reference-id-' + currentVerseReferenceId);
  893. verse_boxes.push(currentVerseBox);
  894. }
  895. var result = await ipcDb.removeTagFromVerses(job.id, verse_boxes);
  896. if (result.success == false) {
  897. var message = `The tag <i>${job.cb_label}</i> could not be removed from the selected verses.<br>
  898. An unexpected database error occurred:<br><br>
  899. ${result.exception}<br><br>
  900. Please restart the app.`;
  901. await showErrorDialog('Database Error', message);
  902. uiHelper.hideTextLoadingIndicator();
  903. return;
  904. }
  905. await eventController.publishAsync('on-latest-tag-changed', {
  906. 'tagId': job.id,
  907. 'added': false
  908. });
  909. var currentBook = app_controller.tab_controller.getTab().getBook();
  910. tags_controller.updateTagCountAfterRendering(currentBook != null);
  911. tags_controller.updateTagUiBasedOnTagAvailability();
  912. tags_controller.remove_tag_assignment_job = null;
  913. tags_controller.persistence_ongoing = false;
  914. }
  915. /**
  916. * This function updates the tag info in existing verse lists after tags have been assigned/removed.
  917. * It does this for the currently opened tab and also within all other tabs where the corresponding verse is loaded.
  918. */
  919. async changeVerseListTagInfo(tag_id,
  920. tag_title,
  921. verse_selection,
  922. action) {
  923. verse_selection = $(verse_selection);
  924. var selected_verses = verse_selection.find('verse');
  925. var current_verse_list_frame = verseListController.getCurrentVerseListFrame();
  926. for (let i = 0; i < selected_verses.length; i++) {
  927. let current_verse_reference_id = $(selected_verses[i]).find('verse-reference-id').text();
  928. let current_verse_box = current_verse_list_frame[0].querySelector('.verse-reference-id-' + current_verse_reference_id);
  929. let verseBoxObj = new VerseBox(current_verse_box);
  930. let highlight = (action == "assign");
  931. verseBoxObj.changeVerseListTagInfo(tag_id, tag_title, action, highlight);
  932. }
  933. for (let i = 0; i < selected_verses.length; i++) {
  934. let current_verse_reference_id = $(selected_verses[i]).find('verse-reference-id').text();
  935. let current_verse_box = current_verse_list_frame[0].querySelector('.verse-reference-id-' + current_verse_reference_id);
  936. await this.verse_box_helper.iterateAndChangeAllDuplicateVerseBoxes(current_verse_box, { tag_id: tag_id, tag_title: tag_title, action: action }, (changedValue, targetVerseBox) => {
  937. let verseBoxObj = new VerseBox(targetVerseBox);
  938. verseBoxObj.changeVerseListTagInfo(changedValue.tag_id, changedValue.tag_title, changedValue.action);
  939. });
  940. }
  941. }
  942. sortTagLists() {
  943. var global_tags_box = $('#tags-content-global');
  944. var sort_function = function(a,b) {
  945. return ($(a).find('.cb-label').text().toLowerCase() > $(b).find('.cb-label').text().toLowerCase()) ? 1 : -1;
  946. };
  947. global_tags_box.find('.checkbox-tag').sort_elements(sort_function);
  948. }
  949. async getTagList(forceRefresh=true) {
  950. var tagList = await this.tag_store.getTagList(forceRefresh);
  951. return tagList;
  952. }
  953. async updateTagList(currentBook, tagGroupId=null, contentId=null, forceRefresh=false) {
  954. if (tagGroupId == null) {
  955. tagGroupId = this.currentTagGroupId;
  956. }
  957. if (forceRefresh) {
  958. this.initialRenderingDone = false;
  959. }
  960. if (contentId == null) {
  961. contentId = currentBook;
  962. }
  963. if (contentId != this.lastContentId || forceRefresh) {
  964. var tagList = await this.tag_store.getTagList(forceRefresh);
  965. if (tagGroupId != null && tagGroupId > 0) {
  966. tagList = await this.tag_store.getTagGroupMembers(tagGroupId, tagList);
  967. }
  968. var tagStatistics = await this.tag_store.getBookTagStatistics(currentBook, forceRefresh);
  969. await this.renderTags(tagList, tagStatistics, currentBook != null);
  970. this.initialRenderingDone = true;
  971. await waitUntilIdle();
  972. this.lastContentId = contentId;
  973. } else {
  974. app_controller.tag_statistics.highlightFrequentlyUsedTags();
  975. }
  976. }
  977. async renderTags(tag_list, tag_statistics, is_book=false) {
  978. //console.time("renderTags");
  979. var current_book = app_controller.tab_controller.getTab().getBook();
  980. var global_tags_box_el = document.getElementById('tags-content-global');
  981. // Assume that verses were selected before, because otherwise the checkboxes may not be properly cleared
  982. this.verses_were_selected_before = true;
  983. // eslint-disable-next-line no-undef
  984. var all_tags_html = tagListTemplate({
  985. tags: tag_list,
  986. tagStatistics: tag_statistics,
  987. current_book: current_book,
  988. current_filter: $('#tags-search-input').val(),
  989. edit_tag_label: i18n.t("tags.edit-tag"),
  990. delete_tag_label: i18n.t("tags.delete-tag"),
  991. });
  992. global_tags_box_el.innerHTML = '';
  993. global_tags_box_el.innerHTML = all_tags_html;
  994. await app_controller.tag_statistics.refreshBookTagStatistics(tag_list, tag_statistics, current_book);
  995. uiHelper.configureButtonStyles('#tags-content');
  996. tags_controller.updateTagsViewAfterVerseSelection(true);
  997. tags_controller.updateTagCountAfterRendering(is_book);
  998. await tags_controller.updateTagUiBasedOnTagAvailability(tag_list.length);
  999. var old_tags_search_input_value = $('#tags-search-input')[0].value;
  1000. if (this.new_tag_created && old_tags_search_input_value != "") {
  1001. // If the newly created tag doesn't match the current search input
  1002. // we remove the current search condition. Otherwise the new tag
  1003. // wouldn't show up in the list as expected.
  1004. if (!tags_controller.tag_list_filter.stringMatches(this.last_created_tag,
  1005. $('#tags-search-input')[0].value)) {
  1006. $('#tags-search-input')[0].value = "";
  1007. old_tags_search_input_value = "";
  1008. }
  1009. }
  1010. this.new_tag_created = false;
  1011. tags_controller.hideTagListLoadingIndicator();
  1012. //console.timeEnd("renderTags");
  1013. }
  1014. async handleEditTagClick(event) {
  1015. eventController.publish('on-button-clicked');
  1016. tags_controller.initEditTagDialog();
  1017. var checkboxTag = $(event.target).closest('.checkbox-tag');
  1018. var cb_label = checkboxTag.find('.cb-label').text();
  1019. tags_controller.edit_tag_id = parseInt(checkboxTag.attr('tag-id'));
  1020. tags_controller.edit_tag_title = cb_label;
  1021. const $tagInput = $('#rename-tag-title-input');
  1022. let tagButton = document.getElementById('edit-tag-button');
  1023. uiHelper.disableButton(tagButton);
  1024. $tagInput.val(cb_label);
  1025. $('#edit-tag-dialog').dialog('open');
  1026. var tagGroupAssignment = document.getElementById('tag-group-assignment');
  1027. await tagGroupAssignment.tagGroupManager.refreshItemList();
  1028. tagGroupAssignment.tagid = tags_controller.edit_tag_id;
  1029. tagGroupAssignment.onChange = () => {
  1030. this.handleEditTagChange();
  1031. };
  1032. if (!platformHelper.isMobile()) {
  1033. $('#rename-tag-title-input').focus();
  1034. }
  1035. }
  1036. handleEditTagChange() {
  1037. let tagGroupAssignment = document.getElementById('tag-group-assignment');
  1038. let tagButton = document.getElementById('edit-tag-button');
  1039. var oldTitle = tags_controller.edit_tag_title;
  1040. var newTitle = document.getElementById('rename-tag-title-input').value;
  1041. if (newTitle != oldTitle || tagGroupAssignment.isChanged) {
  1042. uiHelper.enableButton(tagButton);
  1043. } else {
  1044. uiHelper.disableButton(tagButton);
  1045. }
  1046. if (!tagGroupAssignment.isChanged) {
  1047. this.updateButtonStateBasedOnTagTitleValidation(newTitle, 'edit-tag-button');
  1048. }
  1049. }
  1050. updateTagCountAfterRendering(is_book=false) {
  1051. var global_tag_count = $('#tags-content-global').find('.checkbox-tag').length;
  1052. var global_used_tag_count = $('#tags-content-global').find('.cb-label-assigned').length;
  1053. var tag_list_stats = $($('#tags-content').find('#tag-list-stats'));
  1054. var tag_list_stats_content = "";
  1055. if (is_book) {
  1056. tag_list_stats_content += global_used_tag_count + ' ' + i18n.t('tags.stats-used') + ' / ';
  1057. }
  1058. tag_list_stats_content += global_tag_count + ' ' + i18n.t('tags.stats-total');
  1059. tag_list_stats.html(tag_list_stats_content);
  1060. }
  1061. removeEventListeners(element_list, type, listener) {
  1062. for (let i = 0; i < element_list.length; i++) {
  1063. element_list[i].removeEventListener(type, listener);
  1064. }
  1065. }
  1066. addEventListeners(element_list, type, listener) {
  1067. for (let i = 0; i < element_list.length; i++) {
  1068. element_list[i].addEventListener(type, listener);
  1069. }
  1070. }
  1071. bindTagEvents() {
  1072. var tags_box = document.getElementById('tags-content-global');
  1073. tags_box.addEventListener('click', async function(event) {
  1074. // Use event delegation, so that we do not have to add an event listener to each element.
  1075. const CLICK_TIMEOUT = 100;
  1076. if (event.target.matches('.delete-icon') || event.target.matches('.delete-button')) {
  1077. setTimeout(() => { tags_controller.handleDeleteTagButtonClick(event); }, CLICK_TIMEOUT);
  1078. } else if (event.target.matches('.edit-icon') || event.target.matches('.edit-button')) {
  1079. setTimeout(() => { tags_controller.handleEditTagClick(event); }, CLICK_TIMEOUT);
  1080. } else if (event.target.matches('.tag-button')) {
  1081. await waitUntilIdle();
  1082. await tags_controller.handleTagCbClick(event);
  1083. } else if (event.target.matches('.cb-label')) {
  1084. await waitUntilIdle();
  1085. await tags_controller.handleTagLabelClick(event);
  1086. } else {
  1087. return;
  1088. }
  1089. }, false);
  1090. }
  1091. updateTagTitlesInVerseList(tag_id, is_global, title) {
  1092. var tag_class = is_global ? "tag-global" : "tag-book";
  1093. // eslint-disable-next-line no-unused-vars
  1094. var tag_data_elements = $('.tag-id').filter(function(index) {
  1095. return (($(this).html() == tag_id) && ($(this).parent().hasClass(tag_class)));
  1096. }).closest('.' + tag_class);
  1097. for (let i = 0; i < tag_data_elements.length; i++) {
  1098. var current_tag_data = $(tag_data_elements[i]);
  1099. current_tag_data.find('.tag-title').html(title);
  1100. var current_verse_box = new VerseBox(current_tag_data.closest('.verse-box')[0]);
  1101. current_verse_box.updateTagTooltip();
  1102. current_verse_box.updateVisibleTags();
  1103. }
  1104. }
  1105. async updateTagsViewAfterVerseSelection(force) {
  1106. //console.time('updateTagsViewAfterVerseSelection');
  1107. if (tags_controller.verse_selection_blocked && force !== true) {
  1108. return;
  1109. }
  1110. tags_controller.verse_selection_blocked = true;
  1111. setTimeout(function() {
  1112. tags_controller.verse_selection_blocked = false;
  1113. }, 300);
  1114. var versesSelected = app_controller.verse_selection.getSelectedVerseBoxes().length > 0;
  1115. var selected_verse_tags = [];
  1116. if (versesSelected) { // Verses are selected
  1117. selected_verse_tags = app_controller.verse_selection.getCurrentSelectionTags();
  1118. var checkboxTags = document.querySelectorAll('.checkbox-tag');
  1119. for (let i = 0; i < checkboxTags.length; i++) {
  1120. this.formatCheckboxElementBasedOnSelection(checkboxTags[i], selected_verse_tags);
  1121. }
  1122. this.verses_were_selected_before = true;
  1123. } else { // No verses are selected!
  1124. if (this.verses_were_selected_before) {
  1125. this.uncheckAllCheckboxElements();
  1126. }
  1127. this.verses_were_selected_before = false;
  1128. }
  1129. //console.timeEnd('updateTagsViewAfterVerseSelection');
  1130. }
  1131. formatCheckboxElementBasedOnSelection(cb_element, selected_verse_tags) {
  1132. var current_tag_button = cb_element.querySelector('.tag-button');
  1133. var current_title_element = cb_element.querySelector('.cb-label');
  1134. var current_title = current_title_element.innerHTML;
  1135. var current_title_element_postfix = cb_element.querySelector('.cb-label-postfix');
  1136. var match_found = false;
  1137. for (let j = 0; j < selected_verse_tags.length; j++) {
  1138. var current_tag_obj = selected_verse_tags[j];
  1139. if (current_tag_obj.title == current_title) {
  1140. if (current_tag_obj.complete) {
  1141. current_tag_button.setAttribute('title', this.unassign_tag_label);
  1142. current_tag_button.classList.add('active');
  1143. current_title_element_postfix.innerHTML = '';
  1144. current_title_element.classList.remove('underline');
  1145. } else {
  1146. current_tag_button.setAttribute('title', this.assign_tag_label);
  1147. current_tag_button.classList.remove('active');
  1148. current_title_element_postfix.innerHTML = '&nbsp;*';
  1149. current_title_element.classList.add('underline');
  1150. }
  1151. match_found = true;
  1152. }
  1153. }
  1154. if (!match_found) {
  1155. current_tag_button.classList.remove('active');
  1156. current_tag_button.setAttribute('title', this.assign_tag_label);
  1157. current_title_element.classList.remove('underline');
  1158. current_title_element_postfix.innerHTML = '';
  1159. }
  1160. if (!this.verses_were_selected_before) {
  1161. current_tag_button.classList.remove('disabled');
  1162. }
  1163. }
  1164. uncheckAllCheckboxElements() {
  1165. var all_checkbox_elements = document.querySelectorAll('.checkbox-tag');
  1166. if (all_checkbox_elements.length > 0) {
  1167. for (let i = 0; i < all_checkbox_elements.length; i++) {
  1168. var current_checkbox_element = all_checkbox_elements[i];
  1169. var current_tag_button = current_checkbox_element.querySelector('.tag-button');
  1170. current_tag_button.setAttribute('title', this.assign_tag_hint);
  1171. current_tag_button.classList.add('disabled');
  1172. current_tag_button.classList.remove('active');
  1173. var current_title_element = current_checkbox_element.querySelector('.cb-label');
  1174. current_title_element.classList.remove('underline');
  1175. var current_title_element_postfix = current_checkbox_element.querySelector('.cb-label-postfix');
  1176. current_title_element_postfix.innerHTML = '';
  1177. }
  1178. }
  1179. }
  1180. initTagsUI() {
  1181. $('#tag-list-filter-button').bind('click', (e) => { this.tag_list_filter.handleFilterButtonClick(e); });
  1182. $('#tags-content-global').bind('mouseover', () => { this.tag_list_filter.hideTagFilterMenuIfInToolBar(); });
  1183. $('#tag-filter-menu').find('input').bind('click', (e) => { tags_controller.tag_list_filter.handleTagFilterTypeClick(e); });
  1184. $('#tags-search-input').bind('keyup', (e) => { this.tag_list_filter.handleTagSearchInput(e); });
  1185. $('#tags-search-input').bind('keydown', (e) => {
  1186. e.stopPropagation();
  1187. });
  1188. $('#tags-search-input').bind('mouseup', (e) => {
  1189. e.stopPropagation();
  1190. $('#tags-search-input').select();
  1191. });
  1192. tags_controller.bindTagEvents();
  1193. }
  1194. async updateTagUiBasedOnTagAvailability(tagCount=undefined) {
  1195. var translationCount = app_controller.translation_controller.getTranslationCount();
  1196. if (tagCount === undefined) {
  1197. tagCount = await ipcDb.getTagCount();
  1198. }
  1199. var textType = app_controller.tab_controller.getTab().getTextType();
  1200. if (tagCount == 0) {
  1201. $('.tag-select-button').addClass('ui-state-disabled');
  1202. $('.show-book-tag-statistics-button').addClass('ui-state-disabled');
  1203. if (translationCount > 0) {
  1204. $('#new-standard-tag-button').removeClass('ui-state-disabled');
  1205. $('#tags-content-global').html(i18n.t("help.help-text-no-tags-book-opened", { interpolation: {escapeValue: false} }));
  1206. } else {
  1207. $('#new-standard-tag-button').addClass('ui-state-disabled');
  1208. $('#tags-content-global').html(i18n.t("help.help-text-no-tags-no-book-opened"));
  1209. }
  1210. } else {
  1211. $('.tag-select-button').removeClass('ui-state-disabled');
  1212. $('#new-standard-tag-button').removeClass('ui-state-disabled');
  1213. if (textType == 'book') {
  1214. $('.show-book-tag-statistics-button').removeClass('ui-state-disabled');
  1215. }
  1216. }
  1217. }
  1218. showTagListLoadingIndicator() {
  1219. let tagsContentGlobal = document.getElementById('tags-content-global');
  1220. let loadingIndicator = tagsContentGlobal.querySelector('loading-indicator');
  1221. if (loadingIndicator == null) {
  1222. let element = document.createElement('loading-indicator');
  1223. tagsContentGlobal.appendChild(element);
  1224. loadingIndicator = tagsContentGlobal.querySelector('loading-indicator');
  1225. }
  1226. $(loadingIndicator).find('.loader').show();
  1227. $(loadingIndicator).show();
  1228. }
  1229. hideTagListLoadingIndicator() {
  1230. let tagsContentGlobal = document.getElementById('tags-content-global');
  1231. let loadingIndicator = tagsContentGlobal.querySelector('loading-indicator');
  1232. $(loadingIndicator).hide();
  1233. }
  1234. async updateTagsView(tabIndex, forceRefresh = false) {
  1235. var currentTab = app_controller.tab_controller.getTab(tabIndex);
  1236. var tagCount = await ipcDb.getTagCount();
  1237. if (currentTab !== undefined) {
  1238. if (tagCount > 0) {
  1239. this.showTagListLoadingIndicator();
  1240. }
  1241. await waitUntilIdle();
  1242. var currentTabBook = currentTab.getBook();
  1243. var currentTabContentId = currentTab.getContentId();
  1244. await this.updateTagList(currentTabBook, this.currentTagGroupId, currentTabContentId, forceRefresh);
  1245. this.hideTagListLoadingIndicator();
  1246. }
  1247. await this.updateTagUiBasedOnTagAvailability(tagCount);
  1248. }
  1249. }
  1250. module.exports = TagsController;