/* 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 { html, showErrorDialog } = require('../../helpers/ezra_helper.js');
const eventController = require('../../controllers/event_controller.js');
const TagGroupManager = require('./tag_group_manager.js');
const tagGroupValidator = require('./tag_group_validator.js');
const template = html`
<!-- FONT AWESOME STYLES -->
<link rel="preload" href="node_modules/@fortawesome/fontawesome-free/webfonts/fa-solid-900.woff2" as="font" type="font/woff2">
<link href="node_modules/@fortawesome/fontawesome-free/css/solid.min.css" rel="stylesheet" type="text/css" />
<link href="node_modules/@fortawesome/fontawesome-free/css/fontawesome.min.css" rel="stylesheet" type="text/css" />
<link href="css/main.css" media="screen" rel="stylesheet" type="text/css" />
<link href="css/tool_panel.css" media="screen" rel="stylesheet" type="text/css" />
<link href="css/mobile.css" media="screen" rel="stylesheet" type="text/css" />
<link id="theme-css" href="css/jquery-ui/cupertino/jquery-ui.css" media="screen" rel="stylesheet" type="text/css" />
<style>
:host {
display: block;
height: 95%;
}
#tag-group-list-content {
display: flex;
flex-direction: column;
margin: 0.1em 0 0 0;
padding: 0.5em;
padding-bottom: 0.7em;
user-select: none;
box-sizing: border-box;
border: 1px solid var(--border-color);
height: 100%;
overflow-y: scroll;
}
#tag-group-list-content.rounded-bottom-corners {
border-bottom-left-radius: 6px;
border-bottom-right-radius: 6px;
}
#tag-group-list-content.rounded-corners {
border-radius: 6px;
}
#tag-group-list-content.hidden-top-border {
border-top: 0;
}
.tag-group {
display: flex;
flex-direction: row;
margin: 0.2em;
padding: 0.1em;
padding-left: 0.5em;
min-height: 2em;
align-items: center;
}
.Android .tag-group {
height: 2.5em;
padding-top: 0.8em;
}
.tag-group:nth-child(odd) {
background: var(--background-color-darker);
}
#tag-group-list-content a:link,
#tag-group-list-content a:visited {
text-decoration: none;
color: var(--accent-color);
}
#tag-group-list-content a:hover {
text-decoration: underline;
}
#tag-group-list-content.darkmode--activated a:link,
#tag-group-list-content.darkmode--activated a:visited {
color: var(--accent-color-darkmode);
}
.item-count {
color: #808080;
margin-left: 0.5em;
}
</style>
<div id="tag-group-list-content" class="scrollable" style="display: none;">
</div>
`;
const TAG_GROUP_ALL_TAGS = -1;
/**
* The TagGroupList is a web component that lists all tag groups.
*
* The respective element is <tag-group-list></tag-group-list>.
*
* The following attributes are supported:
* - editable: A Boolean attribute that determines whether the list shows edit buttons.
* - persist: A Boolean attribute that determines whether changes of the list shall be written to the database or not.
* - activation-event: The event used for populating the list and then showing it.
* - selection-event: The event that shall be fired when an item is selected.
* - show-tag-count: A Boolean attribute that determines whether the tag count shall be shown behind each tag group.
* - hide-top-border: A Boolean attribute that determines whether the top border shall be hidden.
* - rounded-corners: A Boolean attribute that determines whether the borders of the box shall be rounded.
* - rounded-bottom-corners: A Boolean attribute that determines whether the bottom corners of the box shall be rounded.
* - book-filter: A boolean attribute that determines whether the tag group list should be filtered based on the current book.
*/
class TagGroupList extends HTMLElement {
constructor() {
super();
this._virtualTagGroups = [
{ id: TAG_GROUP_ALL_TAGS, title: i18n.t('tags.all-tags') }
];
this._editable = false;
this._subscriptionDone = false;
this._bookFilter = false;
this._tagGroupManager = new TagGroupManager((event) => { this.handleTagGroupClick(event); },
(itemId) => { this.handleTagGroupEdit(itemId); },
(itemId) => { this.handleTagGroupDelete(itemId); },
false,
this._editable,
'tag-group',
this._virtualTagGroups);
}
connectedCallback() {
const shadow = this.attachShadow({ mode: 'open' });
shadow.appendChild(template.content.cloneNode(true));
this.readAttributes();
this._contentDiv = this.shadowRoot.getElementById('tag-group-list-content');
platformHelper.addPlatformCssClass(this._contentDiv);
this._tagGroupManager.setContentDiv(this._contentDiv);
if (this._editable) {
this._tagGroupManager.setEditable();
}
if (this._showTagCount) {
this._tagGroupManager.showItemCount();
}
this.subscribeEvents();
}
subscribeEvents() {
if (this._subscriptionDone) {
return;
}
this._subscriptionDone = true;
eventController.subscribe('on-theme-changed', (theme) => {
if (theme == 'dark') {
uiHelper.switchToDarkTheme(this.shadowRoot, 'tag-group-list-content');
} else {
uiHelper.switchToRegularTheme(this.shadowRoot, 'tag-group-list-content');
}
});
if (this._activationEvent != null) {
eventController.subscribe(this._activationEvent, async () => {
const currentBook = this.getCurrentBook();
let bibleBookId = 0;
if (this._bookFilter && currentBook != null) {
this._tagGroupManager.setBookFilter(currentBook);
const dbBibleBook = await ipcDb.getBibleBook(currentBook);
bibleBookId = dbBibleBook.id;
}
this._virtualTagGroups[0].count = await ipcDb.getTagCount(bibleBookId);
await this._tagGroupManager.populateItemList();
this.showTagGroupList();
});
}
if (this._persist) {
eventController.subscribe('on-tag-group-creation', async (tagGroupTitle) => {
let tagGroup = await this.createTagGroupInDb(tagGroupTitle);
eventController.publishAsync('on-tag-group-created', tagGroup);
});
}
eventController.subscribe('on-tag-group-created', async (tagGroup) => {
if (tagGroup != null) {
if (tagGroup.count === undefined) {
tagGroup.count = 0;
}
await this._tagGroupManager.addItem(tagGroup);
}
});
eventController.subscribeMultiple(['on-tag-created', 'on-tag-deleted'], async () => {
const currentBook = this.getCurrentBook();
await this.updateTagCountAndRefreshList(currentBook);
});
if (this._bookFilter) {
eventController.subscribe('on-verse-list-init', async (tabIndex) => {
const currentBook = this.getCurrentBook(tabIndex);
if (currentBook != null) {
let tagGroupUsedInBook = true;
if (tags_controller.tagGroupUsed()) {
const currentTagGroupId = tags_controller.currentTagGroupId;
const dbBibleBook = await ipcDb.getBibleBook(currentBook);
const bibleBookId = dbBibleBook.id;
tagGroupUsedInBook = await ipcDb.isTagGroupUsedInBook(currentTagGroupId, bibleBookId);
}
if (tagGroupUsedInBook) {
this._tagGroupManager.setBookFilter(currentBook);
await this.updateTagCountAndRefreshList(currentBook);
} else {
// Switch back to all tags because the selected tag group is not present in the current book
await this.selectTagGroup(TAG_GROUP_ALL_TAGS);
}
}
});
}
}
getCurrentBook(tabIndex=undefined) {
const currentTab = app_controller.tab_controller.getTab(tabIndex);
const currentBook = currentTab.getBook();
const currentTextType = currentTab.getTextType();
if (currentTextType == 'book' && currentBook != null) {
return currentBook;
} else {
return null;
}
}
async updateTagCountAndRefreshList(currentBook) {
let bibleBookId = 0;
if (this._bookFilter && currentBook != null) {
const dbBibleBook = await ipcDb.getBibleBook(currentBook);
bibleBookId = dbBibleBook.id;
}
this._virtualTagGroups[0].count = await ipcDb.getTagCount(bibleBookId);
await this._tagGroupManager.refreshItemList();
}
readAttributes() {
this._persist = false;
this._editable = false;
this._activationEvent = null;
this._selectionEvent = null;
this._showTagCount = null;
this._roundedBottomCorners = null;
if (this.hasAttribute('editable')) {
this._editable = this.getAttribute('editable') == 'true';
}
if (this.hasAttribute('persist')) {
this._persist = this.getAttribute('persist') == "true";
}
if (this.hasAttribute('activation-event')) {
this._activationEvent = this.getAttribute('activation-event');
}
if (this.hasAttribute('selection-event')) {
this._selectionEvent = this.getAttribute('selection-event');
}
if (this.hasAttribute('show-tag-count')) {
this._showTagCount = this.getAttribute('show-tag-count') == "true";
}
if (this.hasAttribute('hide-top-border')) {
if (this.getAttribute('hide-top-border') == 'true') {
this.getContentDiv().classList.add('hidden-top-border');
}
}
if (this.hasAttribute('rounded-corners')) {
if (this.getAttribute('rounded-corners') == 'true') {
this.getContentDiv().classList.add('rounded-corners');
}
}
if (this.hasAttribute('rounded-bottom-corners')) {
if (this.getAttribute('rounded-bottom-corners') == 'true') {
this.getContentDiv().classList.add('rounded-bottom-corners');
}
}
if (this.hasAttribute('book-filter')) {
if (this.getAttribute('book-filter') == 'true') {
this._bookFilter = true;
} else {
this._bookFilter = false;
}
}
}
async createTagGroupInDb(tagGroupTitle) {
tagGroupTitle = tagGroupTitle.trim();
let result = await ipcDb.createTagGroup(tagGroupTitle);
if (!result.success) {
var message = `The tag group <i>${tagGroupTitle}</i> could not be created.<br>
An unexpected database error occurred:<br><br>
${result.exception}<br><br>
Please restart the app.`;
await showErrorDialog('Database Error', message);
return null;
}
let newId = result.dbObject.id;
let tagGroup = {
title: tagGroupTitle,
id: newId
};
return tagGroup;
}
async handleTagGroupClick(event) {
const tagGroupId = parseInt(event.target.getAttribute('item-id'));
await this.selectTagGroup(tagGroupId);
}
async selectTagGroup(tagGroupId) {
if (tagGroupId == null) {
return;
}
const tagGroup = await this._tagGroupManager.getItemById(tagGroupId);
this.hideTagGroupList();
if (this._selectionEvent != null) {
eventController.publishAsync(this._selectionEvent, tagGroup);
}
}
async handleTagGroupEdit(itemId) {
const dialogBoxTemplate = html`
<div id="rename-tag-group-dialog" style="padding-top: 2em;">
<span id="rename-tag-group-title-label" i18n="tags.title"></span>:
<input id="rename-tag-group-title-input" type="text" maxlength="255" style="width: 25em;" />
<emoji-button-trigger></emoji-button-trigger>
</div>
`;
const tagGroup = await this._tagGroupManager.getItemById(itemId);
return new Promise((resolve) => {
document.querySelector('#boxes').appendChild(dialogBoxTemplate.content);
const $dialogBox = $('#rename-tag-group-dialog');
$dialogBox.localize();
var width = 400;
var height = 200;
var position = [55, 120];
var draggable = true;
let dialogOptions = uiHelper.getDialogOptions(width, height, draggable, position);
dialogOptions.title = i18n.t('tags.rename-tag-group');
dialogOptions.buttons = {};
dialogOptions.dialogClass = 'ezra-dialog rename-tag-group-dialog';
dialogOptions.buttons[i18n.t('general.cancel')] = function() {
$(this).dialog('close');
};
dialogOptions.buttons[i18n.t('general.save')] = {
text: i18n.t('general.save'),
id: 'edit-tag-group-save-button',
click: () => {
this.renameTagGroupAndCloseDialog(itemId, $dialogBox);
}
};
dialogOptions.close = () => {
$dialogBox.dialog('destroy');
$dialogBox.remove();
resolve();
};
document.getElementById('rename-tag-group-title-input').value = tagGroup.title;
document.getElementById('rename-tag-group-title-input').addEventListener('keyup', async (event) => {
await tagGroupValidator.validateNewTagGroupTitle('rename-tag-group-title-input', 'edit-tag-group-save-button');
if (event.key == 'Enter') {
this.renameTagGroupAndCloseDialog(itemId, $dialogBox);
}
});
$dialogBox.dialog(dialogOptions);
uiHelper.fixDialogCloseIconOnAndroid('rename-tag-group-dialog');
tagGroupValidator.validateNewTagGroupTitle('rename-tag-group-title-input', 'edit-tag-group-save-button');
document.getElementById('rename-tag-group-title-input').focus();
});
}
renameTagGroupAndCloseDialog(itemId, $dialogBox) {
const newTagGroupTitle = document.getElementById('rename-tag-group-title-input').value;
this.renameTagGroupInDb(itemId, newTagGroupTitle);
$dialogBox.dialog('close');
}
async renameTagGroupInDb(tagGroupId, newTitle) {
newTitle = newTitle.trim();
var result = await ipcDb.updateTagGroup(tagGroupId, newTitle);
if (result.success == false) {
var message = `The tag group <i>${newTitle}</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);
return;
} else {
await eventController.publishAsync('on-tag-group-renamed', tagGroupId);
}
}
async handleTagGroupDelete(tagGroupId) {
const tagGroup = await this._tagGroupManager.getItemById(tagGroupId);
const message = i18n.t('tags.really-delete-tag-group', { tagGroupTitle: tagGroup.title, interpolation: {escapeValue: false} });
const dialogBoxTemplate = html`
<div id="delete-tag-group-confirmation-dialog" style="padding-top: 2em;">
${message}
</div>
`;
return new Promise((resolve) => {
document.querySelector('#boxes').appendChild(dialogBoxTemplate.content);
const $dialogBox = $('#delete-tag-group-confirmation-dialog');
$dialogBox.localize();
var width = 400;
var height = 200;
var position = [55, 120];
var draggable = true;
let dialogOptions = uiHelper.getDialogOptions(width, height, draggable, position);
dialogOptions.title = i18n.t('tags.delete-tag-group');
dialogOptions.dialogClass = 'ezra-dialog delete-tag-group-confirmation-dialog';
dialogOptions.buttons = {};
dialogOptions.buttons[i18n.t('general.cancel')] = function() {
$(this).dialog('close');
};
dialogOptions.buttons[i18n.t('tags.delete-tag-group')] = {
id: 'delete-tag-group-button',
text: i18n.t('tags.delete-tag-group'),
click: () => {
this.deleteTagGroupInDb(tagGroupId);
$dialogBox.dialog('close');
}
};
dialogOptions.close = () => {
$dialogBox.dialog('destroy');
$dialogBox.remove();
resolve();
};
$dialogBox.dialog(dialogOptions);
uiHelper.fixDialogCloseIconOnAndroid('delete-tag-group-confirmation-dialog');
});
}
async deleteTagGroupInDb(tagGroupId) {
var result = ipcDb.deleteTagGroup(tagGroupId);
if (result.success == false) {
const tagGroup = await this._tagGroupManager.getItemById(tagGroupId);
var message = `The tag group <i>${tagGroup.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);
return;
} else {
await eventController.publishAsync('on-tag-group-deleted', tagGroupId);
}
}
showTagGroupList() {
this.getContentDiv().style.display = '';
}
hideTagGroupList() {
this.getContentDiv().style.display = 'none';
}
getContentDiv() {
return this.shadowRoot.getElementById('tag-group-list-content');
}
get tagGroupManager() {
return this._tagGroupManager;
}
}
customElements.define('tag-group-list', TagGroupList);
module.exports = TagGroupList;
Source